Run 'cargo fmt' to format code (#489)

This commit is contained in:
Atul Bhosale 2019-03-20 22:26:17 +05:30 committed by Baptiste Gelez
parent 732f514da7
commit b945d1f602
58 changed files with 3160 additions and 2195 deletions

2
Cargo.lock generated
View File

@ -1,3 +1,5 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]] [[package]]
name = "MacTypes-sys" name = "MacTypes-sys"
version = "2.1.0" version = "2.1.0"

View File

@ -1,8 +1,8 @@
extern crate ructe;
extern crate rsass; extern crate rsass;
extern crate ructe;
use ructe::*; use ructe::*;
use std::{env, fs::*, io::Write, path::PathBuf};
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::{env, fs::*, io::Write, path::PathBuf};
fn compute_static_hash() -> String { fn compute_static_hash() -> String {
//"find static/ -type f ! -path 'static/media/*' | sort | xargs stat --printf='%n %Y\n' | sha256sum" //"find static/ -type f ! -path 'static/media/*' | sort | xargs stat --printf='%n %Y\n' | sha256sum"
@ -34,25 +34,36 @@ fn compute_static_hash() -> String {
String::from_utf8(sha.stdout).unwrap() String::from_utf8(sha.stdout).unwrap()
} }
fn main() { fn main() {
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let in_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()) let in_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("templates");
.join("templates");
compile_templates(&in_dir, &out_dir).expect("compile templates"); compile_templates(&in_dir, &out_dir).expect("compile templates");
println!("cargo:rerun-if-changed=static/css"); println!("cargo:rerun-if-changed=static/css");
let mut out = File::create("static/css/main.css").expect("Couldn't create main.css"); let mut out = File::create("static/css/main.css").expect("Couldn't create main.css");
out.write_all( out.write_all(
&rsass::compile_scss_file("static/css/main.scss".as_ref(), rsass::OutputStyle::Compressed) &rsass::compile_scss_file(
.expect("Error during SCSS compilation") "static/css/main.scss".as_ref(),
).expect("Couldn't write CSS output"); rsass::OutputStyle::Compressed,
)
.expect("Error during SCSS compilation"),
)
.expect("Couldn't write CSS output");
let cache_id = &compute_static_hash()[..8]; let cache_id = &compute_static_hash()[..8];
println!("cargo:rerun-if-changed=target/deploy/plume-front.wasm"); println!("cargo:rerun-if-changed=target/deploy/plume-front.wasm");
copy("target/deploy/plume-front.wasm", "static/plume-front.wasm") copy("target/deploy/plume-front.wasm", "static/plume-front.wasm")
.and_then(|_| read_to_string("target/deploy/plume-front.js")) .and_then(|_| read_to_string("target/deploy/plume-front.js"))
.and_then(|js| write("static/plume-front.js", js.replace("\"plume-front.wasm\"", &format!("\"/static/cached/{}/plume-front.wasm\"", cache_id)))).ok(); .and_then(|js| {
write(
"static/plume-front.js",
js.replace(
"\"plume-front.wasm\"",
&format!("\"/static/cached/{}/plume-front.wasm\"", cache_id),
),
)
})
.ok();
println!("cargo:rustc-env=CACHE_ID={}", cache_id) println!("cargo:rustc-env=CACHE_ID={}", cache_id)
} }

View File

@ -1,11 +1,7 @@
use clap::{Arg, ArgMatches, App, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use plume_models::{instance::*, safe_string::SafeString, Connection};
use std::env; use std::env;
use plume_models::{
Connection,
instance::*,
safe_string::SafeString,
};
pub fn command<'a, 'b>() -> App<'a, 'b> { pub fn command<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("instance") SubCommand::with_name("instance")
@ -42,14 +38,23 @@ pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) {
} }
fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) { fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
let domain = args.value_of("domain").map(String::from) let domain = args
.unwrap_or_else(|| env::var("BASE_URL") .value_of("domain")
.unwrap_or_else(|_| super::ask_for("Domain name"))); .map(String::from)
let name = args.value_of("name").map(String::from).unwrap_or_else(|| super::ask_for("Instance name")); .unwrap_or_else(|| env::var("BASE_URL").unwrap_or_else(|_| super::ask_for("Domain name")));
let license = args.value_of("default-license").map(String::from).unwrap_or_else(|| String::from("CC-BY-SA")); let name = args
.value_of("name")
.map(String::from)
.unwrap_or_else(|| super::ask_for("Instance name"));
let license = args
.value_of("default-license")
.map(String::from)
.unwrap_or_else(|| String::from("CC-BY-SA"));
let open_reg = !args.is_present("private"); let open_reg = !args.is_present("private");
Instance::insert(conn, NewInstance { Instance::insert(
conn,
NewInstance {
public_domain: domain, public_domain: domain,
name, name,
local: true, local: true,
@ -58,6 +63,8 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
default_license: license, default_license: license,
open_registrations: open_reg, open_registrations: open_reg,
short_description_html: String::new(), short_description_html: String::new(),
long_description_html: String::new() long_description_html: String::new(),
}).expect("Couldn't save instance"); },
)
.expect("Couldn't save instance");
} }

View File

@ -6,12 +6,12 @@ extern crate rpassword;
use clap::App; use clap::App;
use diesel::Connection; use diesel::Connection;
use plume_models::{Connection as Conn, DATABASE_URL};
use std::io::{self, prelude::*}; use std::io::{self, prelude::*};
use plume_models::{DATABASE_URL, Connection as Conn};
mod instance; mod instance;
mod users;
mod search; mod search;
mod users;
fn main() { fn main() {
let mut app = App::new("Plume CLI") let mut app = App::new("Plume CLI")
@ -27,10 +27,16 @@ fn main() {
let conn = Conn::establish(DATABASE_URL.as_str()); let conn = Conn::establish(DATABASE_URL.as_str());
match matches.subcommand() { match matches.subcommand() {
("instance", Some(args)) => instance::run(args, &conn.expect("Couldn't connect to the database.")), ("instance", Some(args)) => {
("users", Some(args)) => users::run(args, &conn.expect("Couldn't connect to the database.")), instance::run(args, &conn.expect("Couldn't connect to the database."))
("search", Some(args)) => search::run(args, &conn.expect("Couldn't connect to the database.")), }
_ => app.print_help().expect("Couldn't print help") ("users", Some(args)) => {
users::run(args, &conn.expect("Couldn't connect to the database."))
}
("search", Some(args)) => {
search::run(args, &conn.expect("Couldn't connect to the database."))
}
_ => app.print_help().expect("Couldn't print help"),
}; };
} }
@ -38,7 +44,9 @@ pub fn ask_for(something: &str) -> String {
print!("{}: ", something); print!("{}: ", something);
io::stdout().flush().expect("Couldn't flush STDOUT"); io::stdout().flush().expect("Couldn't flush STDOUT");
let mut input = String::new(); let mut input = String::new();
io::stdin().read_line(&mut input).expect("Unable to read line"); io::stdin()
.read_line(&mut input)
.expect("Unable to read line");
input.retain(|c| c != '\n'); input.retain(|c| c != '\n');
input input
} }

View File

@ -1,47 +1,56 @@
use clap::{Arg, ArgMatches, App, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_models::{posts::Post, schema::posts, search::Searcher, Connection};
use std::fs::{read_dir, remove_file}; use std::fs::{read_dir, remove_file};
use std::io::ErrorKind; use std::io::ErrorKind;
use std::path::Path; use std::path::Path;
use plume_models::{
Connection,
posts::Post,
schema::posts,
search::Searcher,
};
pub fn command<'a, 'b>() -> App<'a, 'b> { pub fn command<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("search") SubCommand::with_name("search")
.about("Manage search index") .about("Manage search index")
.subcommand(SubCommand::with_name("init") .subcommand(
.arg(Arg::with_name("path") SubCommand::with_name("init")
.arg(
Arg::with_name("path")
.short("p") .short("p")
.long("path") .long("path")
.takes_value(true) .takes_value(true)
.required(false) .required(false)
.help("Path to Plume's working directory")) .help("Path to Plume's working directory"),
.arg(Arg::with_name("force") )
.arg(
Arg::with_name("force")
.short("f") .short("f")
.long("force") .long("force")
.help("Ignore already using directory") .help("Ignore already using directory"),
).about("Initialize Plume's internal search engine")) )
.subcommand(SubCommand::with_name("refill") .about("Initialize Plume's internal search engine"),
.arg(Arg::with_name("path") )
.subcommand(
SubCommand::with_name("refill")
.arg(
Arg::with_name("path")
.short("p") .short("p")
.long("path") .long("path")
.takes_value(true) .takes_value(true)
.required(false) .required(false)
.help("Path to Plume's working directory") .help("Path to Plume's working directory"),
).about("Regenerate Plume's search index")) )
.subcommand(SubCommand::with_name("unlock") .about("Regenerate Plume's search index"),
.arg(Arg::with_name("path") )
.subcommand(
SubCommand::with_name("unlock")
.arg(
Arg::with_name("path")
.short("p") .short("p")
.long("path") .long("path")
.takes_value(true) .takes_value(true)
.required(false) .required(false)
.help("Path to Plume's working directory") .help("Path to Plume's working directory"),
).about("Release lock on search directory")) )
.about("Release lock on search directory"),
)
} }
pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) { pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) {
@ -59,19 +68,25 @@ fn init<'a>(args: &ArgMatches<'a>, conn: &Connection) {
let force = args.is_present("force"); let force = args.is_present("force");
let path = Path::new(path).join("search_index"); let path = Path::new(path).join("search_index");
let can_do = match read_dir(path.clone()) { // try to read the directory specified let can_do = match read_dir(path.clone()) {
// try to read the directory specified
Ok(mut contents) => contents.next().is_none(), Ok(mut contents) => contents.next().is_none(),
Err(e) => if e.kind() == ErrorKind::NotFound { Err(e) => {
if e.kind() == ErrorKind::NotFound {
true true
} else { } else {
panic!("Error while initialising search index : {}", e); panic!("Error while initialising search index : {}", e);
} }
}
}; };
if can_do || force { if can_do || force {
let searcher = Searcher::create(&path).unwrap(); let searcher = Searcher::create(&path).unwrap();
refill(args, conn, Some(searcher)); refill(args, conn, Some(searcher));
} else { } else {
eprintln!("Can't create new index, {} exist and is not empty", path.to_str().unwrap()); eprintln!(
"Can't create new index, {} exist and is not empty",
path.to_str().unwrap()
);
} }
} }
@ -86,15 +101,16 @@ fn refill<'a>(args: &ArgMatches<'a>, conn: &Connection, searcher: Option<Searche
.expect("Post::get_recents: loading error"); .expect("Post::get_recents: loading error");
let len = posts.len(); let len = posts.len();
for (i,post) in posts.iter().enumerate() { for (i, post) in posts.iter().enumerate() {
println!("Importing {}/{} : {}", i+1, len, post.title); println!("Importing {}/{} : {}", i + 1, len, post.title);
searcher.update_document(conn, &post).expect("Couldn't import post"); searcher
.update_document(conn, &post)
.expect("Couldn't import post");
} }
println!("Commiting result"); println!("Commiting result");
searcher.commit(); searcher.commit();
} }
fn unlock<'a>(args: &ArgMatches<'a>) { fn unlock<'a>(args: &ArgMatches<'a>) {
let path = args.value_of("path").unwrap_or("."); let path = args.value_of("path").unwrap_or(".");
let path = Path::new(path).join("search_index/.tantivy-indexer.lock"); let path = Path::new(path).join("search_index/.tantivy-indexer.lock");

View File

@ -1,62 +1,78 @@
use clap::{Arg, ArgMatches, App, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use plume_models::{instance::Instance, users::*, Connection};
use rpassword; use rpassword;
use std::io::{self, Write}; use std::io::{self, Write};
use plume_models::{
Connection,
instance::Instance,
users::*,
};
pub fn command<'a, 'b>() -> App<'a, 'b> { pub fn command<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("users") SubCommand::with_name("users")
.about("Manage users") .about("Manage users")
.subcommand(SubCommand::with_name("new") .subcommand(
.arg(Arg::with_name("name") SubCommand::with_name("new")
.arg(
Arg::with_name("name")
.short("n") .short("n")
.long("name") .long("name")
.alias("username") .alias("username")
.takes_value(true) .takes_value(true)
.help("The username of the new user") .help("The username of the new user"),
).arg(Arg::with_name("display-name") )
.arg(
Arg::with_name("display-name")
.short("N") .short("N")
.long("display-name") .long("display-name")
.takes_value(true) .takes_value(true)
.help("The display name of the new user") .help("The display name of the new user"),
).arg(Arg::with_name("biography") )
.arg(
Arg::with_name("biography")
.short("b") .short("b")
.long("bio") .long("bio")
.alias("biography") .alias("biography")
.takes_value(true) .takes_value(true)
.help("The biography of the new user") .help("The biography of the new user"),
).arg(Arg::with_name("email") )
.arg(
Arg::with_name("email")
.short("e") .short("e")
.long("email") .long("email")
.takes_value(true) .takes_value(true)
.help("Email address of the new user") .help("Email address of the new user"),
).arg(Arg::with_name("password") )
.arg(
Arg::with_name("password")
.short("p") .short("p")
.long("password") .long("password")
.takes_value(true) .takes_value(true)
.help("The password of the new user") .help("The password of the new user"),
).arg(Arg::with_name("admin") )
.arg(
Arg::with_name("admin")
.short("a") .short("a")
.long("admin") .long("admin")
.help("Makes the user an administrator of the instance") .help("Makes the user an administrator of the instance"),
).about("Create a new user on this instance")) )
.subcommand(SubCommand::with_name("reset-password") .about("Create a new user on this instance"),
.arg(Arg::with_name("name") )
.subcommand(
SubCommand::with_name("reset-password")
.arg(
Arg::with_name("name")
.short("u") .short("u")
.long("user") .long("user")
.alias("username") .alias("username")
.takes_value(true) .takes_value(true)
.help("The username of the user to reset password to") .help("The username of the user to reset password to"),
).arg(Arg::with_name("password") )
.arg(
Arg::with_name("password")
.short("p") .short("p")
.long("password") .long("password")
.takes_value(true) .takes_value(true)
.help("The password new for the user") .help("The password new for the user"),
).about("Reset user password")) )
.about("Reset user password"),
)
} }
pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) { pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) {
@ -69,12 +85,24 @@ pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) {
} }
fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) { fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
let username = args.value_of("name").map(String::from).unwrap_or_else(|| super::ask_for("Username")); let username = args
let display_name = args.value_of("display-name").map(String::from).unwrap_or_else(|| super::ask_for("Display name")); .value_of("name")
.map(String::from)
.unwrap_or_else(|| super::ask_for("Username"));
let display_name = args
.value_of("display-name")
.map(String::from)
.unwrap_or_else(|| super::ask_for("Display name"));
let admin = args.is_present("admin"); let admin = args.is_present("admin");
let bio = args.value_of("biography").unwrap_or("").to_string(); let bio = args.value_of("biography").unwrap_or("").to_string();
let email = args.value_of("email").map(String::from).unwrap_or_else(|| super::ask_for("Email address")); let email = args
let password = args.value_of("password").map(String::from).unwrap_or_else(|| { .value_of("email")
.map(String::from)
.unwrap_or_else(|| super::ask_for("Email address"));
let password = args
.value_of("password")
.map(String::from)
.unwrap_or_else(|| {
print!("Password: "); print!("Password: ");
io::stdout().flush().expect("Couldn't flush STDOUT"); io::stdout().flush().expect("Couldn't flush STDOUT");
rpassword::read_password().expect("Couldn't read your password.") rpassword::read_password().expect("Couldn't read your password.")
@ -88,17 +116,31 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
&bio, &bio,
email, email,
User::hash_pass(&password).expect("Couldn't hash password"), User::hash_pass(&password).expect("Couldn't hash password"),
).expect("Couldn't save new user"); )
.expect("Couldn't save new user");
} }
fn reset_password<'a>(args: &ArgMatches<'a>, conn: &Connection) { fn reset_password<'a>(args: &ArgMatches<'a>, conn: &Connection) {
let username = args.value_of("name").map(String::from).unwrap_or_else(|| super::ask_for("Username")); let username = args
let user = User::find_by_name(conn, &username, Instance::get_local(conn).expect("Failed to get local instance").id) .value_of("name")
.map(String::from)
.unwrap_or_else(|| super::ask_for("Username"));
let user = User::find_by_name(
conn,
&username,
Instance::get_local(conn)
.expect("Failed to get local instance")
.id,
)
.expect("Failed to get user"); .expect("Failed to get user");
let password = args.value_of("password").map(String::from).unwrap_or_else(|| { let password = args
.value_of("password")
.map(String::from)
.unwrap_or_else(|| {
print!("Password: "); print!("Password: ");
io::stdout().flush().expect("Couldn't flush STDOUT"); io::stdout().flush().expect("Couldn't flush STDOUT");
rpassword::read_password().expect("Couldn't read your password.") rpassword::read_password().expect("Couldn't read your password.")
}); });
user.reset_password(conn, &password).expect("Failed to reset password"); user.reset_password(conn, &password)
.expect("Failed to reset password");
} }

View File

@ -18,7 +18,8 @@ pub mod sign;
pub const CONTEXT_URL: &str = "https://www.w3.org/ns/activitystreams"; pub const CONTEXT_URL: &str = "https://www.w3.org/ns/activitystreams";
pub const PUBLIC_VISIBILTY: &str = "https://www.w3.org/ns/activitystreams#Public"; pub const PUBLIC_VISIBILTY: &str = "https://www.w3.org/ns/activitystreams#Public";
pub const AP_CONTENT_TYPE: &str = r#"application/ld+json; profile="https://www.w3.org/ns/activitystreams""#; pub const AP_CONTENT_TYPE: &str =
r#"application/ld+json; profile="https://www.w3.org/ns/activitystreams""#;
pub fn ap_accept_header() -> Vec<&'static str> { pub fn ap_accept_header() -> Vec<&'static str> {
vec![ vec![
@ -114,13 +115,18 @@ pub fn broadcast<S: sign::Signer, A: Activity, T: inbox::WithInbox>(
let boxes = to let boxes = to
.into_iter() .into_iter()
.filter(|u| !u.is_local()) .filter(|u| !u.is_local())
.map(|u| u.get_shared_inbox_url().unwrap_or_else(|| u.get_inbox_url())) .map(|u| {
u.get_shared_inbox_url()
.unwrap_or_else(|| u.get_inbox_url())
})
.collect::<Vec<String>>() .collect::<Vec<String>>()
.unique(); .unique();
let mut act = serde_json::to_value(act).expect("activity_pub::broadcast: serialization error"); let mut act = serde_json::to_value(act).expect("activity_pub::broadcast: serialization error");
act["@context"] = context(); act["@context"] = context();
let signed = act.sign(sender).expect("activity_pub::broadcast: signature error"); let signed = act
.sign(sender)
.expect("activity_pub::broadcast: signature error");
for inbox in boxes { for inbox in boxes {
// TODO: run it in Sidekiq or something like that // TODO: run it in Sidekiq or something like that
@ -130,7 +136,11 @@ pub fn broadcast<S: sign::Signer, A: Activity, T: inbox::WithInbox>(
let res = Client::new() let res = Client::new()
.post(&inbox) .post(&inbox)
.headers(headers.clone()) .headers(headers.clone())
.header("Signature", request::signature(sender, &headers).expect("activity_pub::broadcast: request signature error")) .header(
"Signature",
request::signature(sender, &headers)
.expect("activity_pub::broadcast: request signature error"),
)
.body(body) .body(body)
.send(); .send();
match res { match res {

View File

@ -1,12 +1,12 @@
use base64; 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::{ACCEPT, CONTENT_TYPE, DATE, HeaderMap, HeaderValue, USER_AGENT}; use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, DATE, USER_AGENT};
use std::ops::Deref; use std::ops::Deref;
use std::time::SystemTime; use std::time::SystemTime;
use activity_pub::{AP_CONTENT_TYPE, ap_accept_header};
use activity_pub::sign::Signer; use activity_pub::sign::Signer;
use activity_pub::{ap_accept_header, AP_CONTENT_TYPE};
const PLUME_USER_AGENT: &str = concat!("Plume/", env!("CARGO_PKG_VERSION")); const PLUME_USER_AGENT: &str = concat!("Plume/", env!("CARGO_PKG_VERSION"));
@ -42,7 +42,7 @@ impl Digest {
} }
pub fn verify_header(&self, other: &Digest) -> bool { pub fn verify_header(&self, other: &Digest) -> bool {
self.value()==other.value() self.value() == other.value()
} }
pub fn algorithm(&self) -> &str { pub fn algorithm(&self) -> &str {
@ -57,7 +57,8 @@ impl Digest {
let pos = self let pos = self
.0 .0
.find('=') .find('=')
.expect("Digest::value: invalid header error") + 1; .expect("Digest::value: invalid header error")
+ 1;
base64::decode(&self.0[pos..]).expect("Digest::value: invalid encoding error") base64::decode(&self.0[pos..]).expect("Digest::value: invalid encoding error")
} }
@ -75,8 +76,11 @@ impl Digest {
} }
pub fn from_body(body: &str) -> Self { pub fn from_body(body: &str) -> Self {
let mut hasher = Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error"); let mut hasher =
hasher.update(body.as_bytes()).expect("Digest::digest: content insertion error"); Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error");
hasher
.update(body.as_bytes())
.expect("Digest::digest: content insertion error");
let res = base64::encode(&hasher.finish().expect("Digest::digest: finalizing error")); let res = base64::encode(&hasher.finish().expect("Digest::digest: finalizing error"));
Digest(format!("SHA-256={}", res)) Digest(format!("SHA-256={}", res))
} }
@ -99,7 +103,8 @@ pub fn headers() -> HeaderMap {
.into_iter() .into_iter()
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", "), .join(", "),
).expect("request::headers: accept error"), )
.expect("request::headers: accept error"),
); );
headers.insert(CONTENT_TYPE, HeaderValue::from_static(AP_CONTENT_TYPE)); headers.insert(CONTENT_TYPE, HeaderValue::from_static(AP_CONTENT_TYPE));
headers headers

View File

@ -1,7 +1,6 @@
use super::request; use super::request;
use base64; use base64;
use chrono::{DateTime, Duration, use chrono::{naive::NaiveDateTime, DateTime, Duration, Utc};
naive::NaiveDateTime, Utc};
use hex; 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;
@ -59,7 +58,8 @@ impl Signable for serde_json::Value {
&json!({ &json!({
"@context": "https://w3id.org/identity/v1", "@context": "https://w3id.org/identity/v1",
"created": creation_date "created": creation_date
}).to_string(), })
.to_string(),
); );
let document_hash = Self::hash(&self.to_string()); let document_hash = Self::hash(&self.to_string());
let to_be_signed = options_hash + &document_hash; let to_be_signed = options_hash + &document_hash;
@ -91,7 +91,8 @@ impl Signable for serde_json::Value {
&json!({ &json!({
"@context": "https://w3id.org/identity/v1", "@context": "https://w3id.org/identity/v1",
"created": creation_date "created": creation_date
}).to_string(), })
.to_string(),
); );
let creation_date = creation_date.as_str(); let creation_date = creation_date.as_str();
if creation_date.is_none() { if creation_date.is_none() {
@ -169,7 +170,10 @@ pub fn verify_http_headers<S: Signer + ::std::fmt::Debug>(
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\n"); .join("\n");
if !sender.verify(&h, &base64::decode(signature).unwrap_or_default()).unwrap_or(false) { if !sender
.verify(&h, &base64::decode(signature).unwrap_or_default())
.unwrap_or(false)
{
return SignatureValidity::Invalid; return SignatureValidity::Invalid;
} }
if !headers.contains(&"digest") { if !headers.contains(&"digest") {

View File

@ -10,8 +10,8 @@ extern crate chrono;
extern crate failure; extern crate failure;
#[macro_use] #[macro_use]
extern crate failure_derive; extern crate failure_derive;
extern crate hex;
extern crate heck; extern crate heck;
extern crate hex;
extern crate openssl; extern crate openssl;
extern crate pulldown_cmark; extern crate pulldown_cmark;
extern crate reqwest; extern crate reqwest;

View File

@ -1,9 +1,9 @@
use heck::CamelCase; use heck::CamelCase;
use openssl::rand::rand_bytes; use openssl::rand::rand_bytes;
use pulldown_cmark::{Event, Parser, Options, Tag, html}; use pulldown_cmark::{html, Event, Options, Parser, Tag};
use rocket::{ use rocket::{
http::uri::Uri, http::uri::Uri,
response::{Redirect, Flash} response::{Flash, Redirect},
}; };
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashSet; use std::collections::HashSet;
@ -12,7 +12,9 @@ use std::collections::HashSet;
pub fn random_hex() -> String { pub fn random_hex() -> String {
let mut bytes = [0; 32]; let mut bytes = [0; 32];
rand_bytes(&mut bytes).expect("Error while generating client id"); rand_bytes(&mut bytes).expect("Error while generating client id");
bytes.iter().fold(String::new(), |res, byte| format!("{}{:x}", res, byte)) bytes
.iter()
.fold(String::new(), |res, byte| format!("{}{:x}", res, byte))
} }
/// Remove non alphanumeric characters and CamelCase a string /// Remove non alphanumeric characters and CamelCase a string
@ -29,7 +31,11 @@ pub fn make_actor_id(name: &str) -> String {
* Note that the message should be translated before passed to this function. * Note that the message should be translated before passed to this function.
*/ */
pub fn requires_login<T: Into<Uri<'static>>>(message: &str, url: T) -> Flash<Redirect> { pub fn requires_login<T: Into<Uri<'static>>>(message: &str, url: T) -> Flash<Redirect> {
Flash::new(Redirect::to(format!("/login?m={}", Uri::percent_encode(message))), "callback", url.into().to_string()) Flash::new(
Redirect::to(format!("/login?m={}", Uri::percent_encode(message))),
"callback",
url.into().to_string(),
)
} }
#[derive(Debug)] #[derive(Debug)]
@ -45,21 +51,19 @@ pub fn md_to_html(md: &str, base_url: &str) -> (String, HashSet<String>, HashSet
let parser = Parser::new_ext(md, Options::all()); let parser = Parser::new_ext(md, Options::all());
let (parser, mentions, hashtags): (Vec<Event>, Vec<String>, Vec<String>) = parser let (parser, mentions, hashtags): (Vec<Event>, Vec<String>, Vec<String>) = parser
.scan(None, |state: &mut Option<String>, evt|{ .scan(None, |state: &mut Option<String>, evt| {
let (s, res) = match evt { let (s, res) = match evt {
Event::Text(txt) => match state.take() { Event::Text(txt) => match state.take() {
Some(mut prev_txt) => { Some(mut prev_txt) => {
prev_txt.push_str(&txt); prev_txt.push_str(&txt);
(Some(prev_txt), vec![]) (Some(prev_txt), vec![])
},
None => {
(Some(txt.into_owned()), vec![])
} }
None => (Some(txt.into_owned()), vec![]),
}, },
e => match state.take() { e => match state.take() {
Some(prev) => (None, vec![Event::Text(Cow::Owned(prev)), e]), Some(prev) => (None, vec![Event::Text(Cow::Owned(prev)), e]),
None => (None, vec![e]), None => (None, vec![e]),
} },
}; };
*state = s; *state = s;
Some(res) Some(res)
@ -67,7 +71,9 @@ pub fn md_to_html(md: &str, base_url: &str) -> (String, HashSet<String>, HashSet
.flat_map(|v| v.into_iter()) .flat_map(|v| v.into_iter())
.map(|evt| match evt { .map(|evt| match evt {
Event::Text(txt) => { Event::Text(txt) => {
let (evts, _, _, _, new_mentions, new_hashtags) = txt.chars().fold((vec![], State::Ready, String::new(), 0, vec![], vec![]), |(mut events, state, mut text_acc, n, mut mentions, mut hashtags), c| { let (evts, _, _, _, new_mentions, new_hashtags) = txt.chars().fold(
(vec![], State::Ready, String::new(), 0, vec![], vec![]),
|(mut events, state, mut text_acc, n, mut mentions, mut hashtags), c| {
match state { match state {
State::Mention => { State::Mention => {
let char_matches = c.is_alphanumeric() || "@.-_".contains(c); let char_matches = c.is_alphanumeric() || "@.-_".contains(c);
@ -80,52 +86,89 @@ pub fn md_to_html(md: &str, base_url: &str) -> (String, HashSet<String>, HashSet
} }
let mention = text_acc; let mention = text_acc;
let short_mention = mention.splitn(1, '@').nth(0).unwrap_or(""); let short_mention = mention.splitn(1, '@').nth(0).unwrap_or("");
let link = Tag::Link(format!("//{}/@/{}/", base_url, &mention).into(), short_mention.to_owned().into()); let link = Tag::Link(
format!("//{}/@/{}/", base_url, &mention).into(),
short_mention.to_owned().into(),
);
mentions.push(mention.clone()); mentions.push(mention.clone());
events.push(Event::Start(link.clone())); events.push(Event::Start(link.clone()));
events.push(Event::Text(format!("@{}", &short_mention).into())); events.push(Event::Text(format!("@{}", &short_mention).into()));
events.push(Event::End(link)); events.push(Event::End(link));
(events, State::Ready, c.to_string(), n + 1, mentions, hashtags) (
events,
State::Ready,
c.to_string(),
n + 1,
mentions,
hashtags,
)
} }
} }
State::Hashtag => { State::Hashtag => {
let char_matches = c.is_alphanumeric() || "-_".contains(c); let char_matches = c.is_alphanumeric() || "-_".contains(c);
if char_matches && (n < (txt.chars().count() -1)) { if char_matches && (n < (txt.chars().count() - 1)) {
text_acc.push(c); text_acc.push(c);
(events, State::Hashtag, text_acc, n+1, mentions, hashtags) (events, State::Hashtag, text_acc, n + 1, mentions, hashtags)
} else { } else {
if char_matches { if char_matches {
text_acc.push(c); text_acc.push(c);
} }
let hashtag = text_acc; let hashtag = text_acc;
let link = Tag::Link(format!("//{}/tag/{}", base_url, &hashtag.to_camel_case()).into(), hashtag.to_owned().into()); let link = Tag::Link(
format!("//{}/tag/{}", base_url, &hashtag.to_camel_case())
.into(),
hashtag.to_owned().into(),
);
hashtags.push(hashtag.clone()); hashtags.push(hashtag.clone());
events.push(Event::Start(link.clone())); events.push(Event::Start(link.clone()));
events.push(Event::Text(format!("#{}", &hashtag).into())); events.push(Event::Text(format!("#{}", &hashtag).into()));
events.push(Event::End(link)); events.push(Event::End(link));
(events, State::Ready, c.to_string(), n + 1, mentions, hashtags) (
events,
State::Ready,
c.to_string(),
n + 1,
mentions,
hashtags,
)
} }
} }
State::Ready => { State::Ready => {
if c == '@' { if c == '@' {
events.push(Event::Text(text_acc.into())); events.push(Event::Text(text_acc.into()));
(events, State::Mention, String::new(), n + 1, mentions, hashtags) (
events,
State::Mention,
String::new(),
n + 1,
mentions,
hashtags,
)
} else if c == '#' { } else if c == '#' {
events.push(Event::Text(text_acc.into())); events.push(Event::Text(text_acc.into()));
(events, State::Hashtag, String::new(), n + 1, mentions, hashtags) (
events,
State::Hashtag,
String::new(),
n + 1,
mentions,
hashtags,
)
} else if c.is_alphanumeric() { } else if c.is_alphanumeric() {
text_acc.push(c); text_acc.push(c);
if n >= (txt.chars().count() - 1) { // Add the text after at the end, even if it is not followed by a mention. if n >= (txt.chars().count() - 1) {
// Add the text after at the end, even if it is not followed by a mention.
events.push(Event::Text(text_acc.clone().into())) events.push(Event::Text(text_acc.clone().into()))
} }
(events, State::Word, text_acc, n + 1, mentions, hashtags) (events, State::Word, text_acc, n + 1, mentions, hashtags)
} else { } else {
text_acc.push(c); text_acc.push(c);
if n >= (txt.chars().count() - 1) { // Add the text after at the end, even if it is not followed by a mention. if n >= (txt.chars().count() - 1) {
// Add the text after at the end, even if it is not followed by a mention.
events.push(Event::Text(text_acc.clone().into())) events.push(Event::Text(text_acc.clone().into()))
} }
(events, State::Ready, text_acc, n + 1, mentions, hashtags) (events, State::Ready, text_acc, n + 1, mentions, hashtags)
@ -134,28 +177,35 @@ pub fn md_to_html(md: &str, base_url: &str) -> (String, HashSet<String>, HashSet
State::Word => { State::Word => {
text_acc.push(c); text_acc.push(c);
if c.is_alphanumeric() { if c.is_alphanumeric() {
if n >= (txt.chars().count() - 1) { // Add the text after at the end, even if it is not followed by a mention. if n >= (txt.chars().count() - 1) {
// Add the text after at the end, even if it is not followed by a mention.
events.push(Event::Text(text_acc.clone().into())) events.push(Event::Text(text_acc.clone().into()))
} }
(events, State::Word, text_acc, n + 1, mentions, hashtags) (events, State::Word, text_acc, n + 1, mentions, hashtags)
} else { } else {
if n >= (txt.chars().count() - 1) { // Add the text after at the end, even if it is not followed by a mention. if n >= (txt.chars().count() - 1) {
// Add the text after at the end, even if it is not followed by a mention.
events.push(Event::Text(text_acc.clone().into())) events.push(Event::Text(text_acc.clone().into()))
} }
(events, State::Ready, text_acc, n + 1, mentions, hashtags) (events, State::Ready, text_acc, n + 1, mentions, hashtags)
} }
} }
} }
});
(evts, new_mentions, new_hashtags)
}, },
_ => (vec![evt], vec![], vec![]) );
}).fold((vec![],vec![],vec![]), |(mut parser, mut mention, mut hashtag), (mut p, mut m, mut h)| { (evts, new_mentions, new_hashtags)
}
_ => (vec![evt], vec![], vec![]),
})
.fold(
(vec![], vec![], vec![]),
|(mut parser, mut mention, mut hashtag), (mut p, mut m, mut h)| {
parser.append(&mut p); parser.append(&mut p);
mention.append(&mut m); mention.append(&mut m);
hashtag.append(&mut h); hashtag.append(&mut h);
(parser, mention, hashtag) (parser, mention, hashtag)
}); },
);
let parser = parser.into_iter(); let parser = parser.into_iter();
let mentions = mentions.into_iter().map(|m| String::from(m.trim())); let mentions = mentions.into_iter().map(|m| String::from(m.trim()));
let hashtags = hashtags.into_iter().map(|h| String::from(h.trim())); let hashtags = hashtags.into_iter().map(|h| String::from(h.trim()));
@ -188,7 +238,13 @@ mod tests {
]; ];
for (md, mentions) in tests { for (md, mentions) in tests {
assert_eq!(md_to_html(md, "").1, mentions.into_iter().map(|s| s.to_string()).collect::<HashSet<String>>()); assert_eq!(
md_to_html(md, "").1,
mentions
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>()
);
} }
} }
@ -207,7 +263,13 @@ mod tests {
]; ];
for (md, mentions) in tests { for (md, mentions) in tests {
assert_eq!(md_to_html(md, "").2, mentions.into_iter().map(|s| s.to_string()).collect::<HashSet<String>>()); assert_eq!(
md_to_html(md, "").2,
mentions
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>()
);
} }
} }
} }

View File

@ -1,4 +1,7 @@
use stdweb::{unstable::{TryInto, TryFrom}, web::{*, html_element::*, event::*}}; use stdweb::{
unstable::{TryFrom, TryInto},
web::{event::*, html_element::*, *},
};
use CATALOG; use CATALOG;
macro_rules! mv { macro_rules! mv {
@ -14,7 +17,8 @@ fn get_elt_value(id: &'static str) -> String {
let elt = document().get_element_by_id(id).unwrap(); let elt = document().get_element_by_id(id).unwrap();
let inp: Result<InputElement, _> = elt.clone().try_into(); let inp: Result<InputElement, _> = elt.clone().try_into();
let textarea: Result<TextAreaElement, _> = elt.try_into(); let textarea: Result<TextAreaElement, _> = elt.try_into();
inp.map(|i| i.raw_value()).unwrap_or_else(|_| textarea.unwrap().value()) inp.map(|i| i.raw_value())
.unwrap_or_else(|_| textarea.unwrap().value())
} }
fn set_value<S: AsRef<str>>(id: &'static str, val: S) { fn set_value<S: AsRef<str>>(id: &'static str, val: S) {
@ -64,7 +68,7 @@ fn init_widget(
tag: &'static str, tag: &'static str,
placeholder_text: String, placeholder_text: String,
content: String, content: String,
disable_return: bool disable_return: bool,
) -> Result<HtmlElement, EditorError> { ) -> Result<HtmlElement, EditorError> {
let widget = placeholder(make_editable(tag).try_into()?, &placeholder_text); let widget = placeholder(make_editable(tag).try_into()?, &placeholder_text);
if !content.is_empty() { if !content.is_empty() {
@ -86,7 +90,7 @@ fn init_widget(
pub fn init() -> Result<(), EditorError> { pub fn init() -> Result<(), EditorError> {
if let Some(ed) = document().get_element_by_id("plume-editor") { if let Some(ed) = document().get_element_by_id("plume-editor") {
// Show the editor // Show the editor
js!{ @{&ed}.style.display = "block"; }; js! { @{&ed}.style.display = "block"; };
// And hide the HTML-only fallback // And hide the HTML-only fallback
let old_ed = document().get_element_by_id("plume-fallback-editor")?; let old_ed = document().get_element_by_id("plume-fallback-editor")?;
let old_title = document().get_element_by_id("plume-editor-title")?; let old_title = document().get_element_by_id("plume-editor-title")?;
@ -101,8 +105,20 @@ pub fn init() -> Result<(), EditorError> {
let content_val = get_elt_value("editor-content"); let content_val = get_elt_value("editor-content");
// And pre-fill the new editor with this values // And pre-fill the new editor with this values
let title = init_widget(&ed, "h1", i18n!(CATALOG, "Title"), title_val, true)?; let title = init_widget(&ed, "h1", i18n!(CATALOG, "Title"), title_val, true)?;
let subtitle = init_widget(&ed, "h2", i18n!(CATALOG, "Subtitle or summary"), subtitle_val, true)?; let subtitle = init_widget(
let content = init_widget(&ed, "article", i18n!(CATALOG, "Write your article here. Markdown is supported."), content_val.clone(), true)?; &ed,
"h2",
i18n!(CATALOG, "Subtitle or summary"),
subtitle_val,
true,
)?;
let content = init_widget(
&ed,
"article",
i18n!(CATALOG, "Write your article here. Markdown is supported."),
content_val.clone(),
true,
)?;
js! { @{&content}.innerHTML = @{content_val}; }; js! { @{&content}.innerHTML = @{content_val}; };
// character counter // character counter
@ -118,7 +134,8 @@ pub fn init() -> Result<(), EditorError> {
}), 0); }), 0);
})); }));
document().get_element_by_id("publish")?.add_event_listener(mv!(title, subtitle, content, old_ed => move |_: ClickEvent| { document().get_element_by_id("publish")?.add_event_listener(
mv!(title, subtitle, content, old_ed => move |_: ClickEvent| {
let popup = document().get_element_by_id("publish-popup").or_else(|| let popup = document().get_element_by_id("publish-popup").or_else(||
init_popup(&title, &subtitle, &content, &old_ed).ok() init_popup(&title, &subtitle, &content, &old_ed).ok()
).unwrap(); ).unwrap();
@ -128,17 +145,27 @@ pub fn init() -> Result<(), EditorError> {
popup.class_list().add("show").unwrap(); popup.class_list().add("show").unwrap();
bg.class_list().add("show").unwrap(); bg.class_list().add("show").unwrap();
})); }),
);
} }
Ok(()) Ok(())
} }
fn init_popup(title: &HtmlElement, subtitle: &HtmlElement, content: &HtmlElement, old_ed: &Element) -> Result<Element, EditorError> { fn init_popup(
title: &HtmlElement,
subtitle: &HtmlElement,
content: &HtmlElement,
old_ed: &Element,
) -> Result<Element, EditorError> {
let popup = document().create_element("div")?; let popup = document().create_element("div")?;
popup.class_list().add("popup")?; popup.class_list().add("popup")?;
popup.set_attribute("id", "publish-popup")?; popup.set_attribute("id", "publish-popup")?;
let tags = get_elt_value("tags").split(',').map(str::trim).map(str::to_string).collect::<Vec<_>>(); let tags = get_elt_value("tags")
.split(',')
.map(str::trim)
.map(str::to_string)
.collect::<Vec<_>>();
let license = get_elt_value("license"); let license = get_elt_value("license");
make_input(&i18n!(CATALOG, "Tags"), "popup-tags", &popup).set_raw_value(&tags.join(", ")); make_input(&i18n!(CATALOG, "Tags"), "popup-tags", &popup).set_raw_value(&tags.join(", "));
make_input(&i18n!(CATALOG, "License"), "popup-license", &popup).set_raw_value(&license); make_input(&i18n!(CATALOG, "License"), "popup-license", &popup).set_raw_value(&license);
@ -152,7 +179,7 @@ fn init_popup(title: &HtmlElement, subtitle: &HtmlElement, content: &HtmlElement
popup.append_child(&cover); popup.append_child(&cover);
let button = document().create_element("input")?; let button = document().create_element("input")?;
js!{ js! {
@{&button}.type = "submit"; @{&button}.type = "submit";
@{&button}.value = @{i18n!(CATALOG, "Publish")}; @{&button}.value = @{i18n!(CATALOG, "Publish")};
}; };
@ -189,7 +216,10 @@ fn init_popup_bg() -> Result<Element, EditorError> {
fn chars_left(selector: &str, content: &HtmlElement) -> Option<i32> { fn chars_left(selector: &str, content: &HtmlElement) -> Option<i32> {
match document().query_selector(selector) { match document().query_selector(selector) {
Ok(Some(form)) => HtmlElement::try_from(form).ok().and_then(|form| { Ok(Some(form)) => HtmlElement::try_from(form).ok().and_then(|form| {
if let Some(len) = form.get_attribute("content-size").and_then(|s| s.parse::<i32>().ok()) { if let Some(len) = form
.get_attribute("content-size")
.and_then(|s| s.parse::<i32>().ok())
{
(js! { (js! {
let x = encodeURIComponent(@{content}.innerHTML) let x = encodeURIComponent(@{content}.innerHTML)
.replace(/%20/g, "+") .replace(/%20/g, "+")
@ -198,7 +228,10 @@ fn chars_left(selector: &str, content: &HtmlElement) -> Option<i32> {
.length + 2; .length + 2;
console.log(x); console.log(x);
return x; return x;
}).try_into().map(|c: i32| len - c).ok() })
.try_into()
.map(|c: i32| len - c)
.ok()
} else { } else {
None None
} }
@ -218,7 +251,11 @@ fn make_input(label_text: &str, name: &'static str, form: &Element) -> InputElem
label.append_child(&document().create_text_node(label_text)); label.append_child(&document().create_text_node(label_text));
label.set_attribute("for", name).unwrap(); label.set_attribute("for", name).unwrap();
let inp: InputElement = document().create_element("input").unwrap().try_into().unwrap(); let inp: InputElement = document()
.create_element("input")
.unwrap()
.try_into()
.unwrap();
inp.set_attribute("name", name).unwrap(); inp.set_attribute("name", name).unwrap();
inp.set_attribute("id", name).unwrap(); inp.set_attribute("id", name).unwrap();
@ -228,8 +265,11 @@ fn make_input(label_text: &str, name: &'static str, form: &Element) -> InputElem
} }
fn make_editable(tag: &'static str) -> Element { fn make_editable(tag: &'static str) -> Element {
let elt = document().create_element(tag).expect("Couldn't create editable element"); let elt = document()
elt.set_attribute("contenteditable", "true").expect("Couldn't make element editable"); .create_element(tag)
.expect("Couldn't create editable element");
elt.set_attribute("contenteditable", "true")
.expect("Couldn't make element editable");
elt elt
} }

View File

@ -1,4 +1,4 @@
#![recursion_limit="128"] #![recursion_limit = "128"]
#![feature(decl_macro, proc_macro_hygiene, try_trait)] #![feature(decl_macro, proc_macro_hygiene, try_trait)]
extern crate gettext; extern crate gettext;
@ -9,7 +9,7 @@ extern crate lazy_static;
#[macro_use] #[macro_use]
extern crate stdweb; extern crate stdweb;
use stdweb::{web::{*, event::*}}; use stdweb::web::{event::*, *};
init_i18n!("plume-front", en, fr); init_i18n!("plume-front", en, fr);
@ -20,9 +20,14 @@ compile_i18n!();
lazy_static! { lazy_static! {
static ref CATALOG: gettext::Catalog = { static ref CATALOG: gettext::Catalog = {
let catalogs = include_i18n!(); let catalogs = include_i18n!();
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");
catalogs.iter().find(|(l, _)| l == &lang).unwrap_or(&catalogs[0]).clone().1 catalogs
.iter()
.find(|(l, _)| l == &lang)
.unwrap_or(&catalogs[0])
.clone()
.1
}; };
} }
@ -30,7 +35,8 @@ fn main() {
menu(); menu();
search(); search();
editor::init() editor::init()
.map_err(|e| console!(error, format!("Editor error: {:?}", e))).ok(); .map_err(|e| console!(error, format!("Editor error: {:?}", e)))
.ok();
} }
/// Toggle menu on mobile device /// Toggle menu on mobile device
@ -41,10 +47,14 @@ fn menu() {
if let Some(button) = document().get_element_by_id("menu") { if let Some(button) = document().get_element_by_id("menu") {
if let Some(menu) = document().get_element_by_id("content") { if let Some(menu) = document().get_element_by_id("content") {
button.add_event_listener(|_: ClickEvent| { button.add_event_listener(|_: ClickEvent| {
document().get_element_by_id("menu").map(|menu| menu.class_list().add("show")); document()
.get_element_by_id("menu")
.map(|menu| menu.class_list().add("show"));
}); });
menu.add_event_listener(|_: ClickEvent| { menu.add_event_listener(|_: ClickEvent| {
document().get_element_by_id("menu").map(|menu| menu.class_list().remove("show")); document()
.get_element_by_id("menu")
.map(|menu| menu.class_list().remove("show"));
}); });
} }
} }
@ -54,7 +64,9 @@ fn menu() {
fn search() { fn search() {
if let Some(form) = document().get_element_by_id("form") { if let Some(form) = document().get_element_by_id("form") {
form.add_event_listener(|_: SubmitEvent| { form.add_event_listener(|_: SubmitEvent| {
document().query_selector_all("#form input").map(|inputs| { document()
.query_selector_all("#form input")
.map(|inputs| {
for input in inputs { for input in inputs {
js! { js! {
if (@{&input}.name === "") { if (@{&input}.name === "") {
@ -65,7 +77,8 @@ fn search() {
} }
} }
} }
}).ok(); })
.ok();
}); });
} }
} }

View File

@ -89,13 +89,19 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApiToken {
} }
let mut parsed_header = headers[0].split(' '); let mut parsed_header = headers[0].split(' ');
let auth_type = parsed_header.next() let auth_type = parsed_header.next().map_or_else(
.map_or_else(|| Outcome::Failure((Status::BadRequest, TokenError::NoType)), Outcome::Success)?; || Outcome::Failure((Status::BadRequest, TokenError::NoType)),
let val = parsed_header.next() Outcome::Success,
.map_or_else(|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)), Outcome::Success)?; )?;
let val = parsed_header.next().map_or_else(
|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)),
Outcome::Success,
)?;
if auth_type == "Bearer" { if auth_type == "Bearer" {
let conn = request.guard::<DbConn>().map_failure(|_| (Status::InternalServerError, TokenError::DbError))?; let conn = request
.guard::<DbConn>()
.map_failure(|_| (Status::InternalServerError, TokenError::DbError))?;
if let Ok(token) = ApiToken::find_by_value(&*conn, val) { if let Ok(token) = ApiToken::find_by_value(&*conn, val) {
return Outcome::Success(token); return Outcome::Success(token);
} }

View File

@ -5,7 +5,7 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_api::apps::AppEndpoint; use plume_api::apps::AppEndpoint;
use plume_common::utils::random_hex; use plume_common::utils::random_hex;
use schema::apps; use schema::apps;
use {Connection, Error, Result, ApiResult}; use {ApiResult, Connection, Error, Result};
#[derive(Clone, Queryable)] #[derive(Clone, Queryable)]
pub struct App { pub struct App {
@ -52,7 +52,8 @@ impl Provider<Connection> for App {
redirect_uri: data.redirect_uri, redirect_uri: data.redirect_uri,
website: data.website, website: data.website,
}, },
).map_err(|_| ApiError::NotFound("Couldn't register app".into()))?; )
.map_err(|_| ApiError::NotFound("Couldn't register app".into()))?;
Ok(AppEndpoint { Ok(AppEndpoint {
id: Some(app.id), id: Some(app.id),

View File

@ -26,7 +26,7 @@ use safe_string::SafeString;
use schema::blogs; use schema::blogs;
use search::Searcher; use search::Searcher;
use users::User; use users::User;
use {Connection, BASE_URL, USE_HTTPS, Error, Result}; use {Connection, Error, Result, BASE_URL, USE_HTTPS};
pub type CustomGroup = CustomObject<ApSignature, Group>; pub type CustomGroup = CustomObject<ApSignature, Group>;
@ -66,27 +66,15 @@ impl Blog {
insert!(blogs, NewBlog, |inserted, conn| { insert!(blogs, NewBlog, |inserted, conn| {
let instance = inserted.get_instance(conn)?; let instance = inserted.get_instance(conn)?;
if inserted.outbox_url.is_empty() { if inserted.outbox_url.is_empty() {
inserted.outbox_url = instance.compute_box( inserted.outbox_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, "outbox");
BLOG_PREFIX,
&inserted.actor_id,
"outbox",
);
} }
if inserted.inbox_url.is_empty() { if inserted.inbox_url.is_empty() {
inserted.inbox_url = instance.compute_box( inserted.inbox_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, "inbox");
BLOG_PREFIX,
&inserted.actor_id,
"inbox",
);
} }
if inserted.ap_url.is_empty() { if inserted.ap_url.is_empty() {
inserted.ap_url = instance.compute_box( inserted.ap_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, "");
BLOG_PREFIX,
&inserted.actor_id,
"",
);
} }
if inserted.fqn.is_empty() { if inserted.fqn.is_empty() {
@ -154,16 +142,12 @@ impl Blog {
} }
fn fetch_from_webfinger(conn: &Connection, acct: &str) -> Result<Blog> { fn fetch_from_webfinger(conn: &Connection, acct: &str) -> Result<Blog> {
resolve(acct.to_owned(), *USE_HTTPS)?.links resolve(acct.to_owned(), *USE_HTTPS)?
.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")))
.ok_or(Error::Webfinger) .ok_or(Error::Webfinger)
.and_then(|l| { .and_then(|l| Blog::fetch_from_url(conn, &l.href?))
Blog::fetch_from_url(
conn,
&l.href?
)
})
} }
fn fetch_from_url(conn: &Connection, url: &str) -> Result<Blog> { fn fetch_from_url(conn: &Connection, url: &str) -> Result<Blog> {
@ -181,20 +165,14 @@ impl Blog {
.send()?; .send()?;
let text = &res.text()?; let text = &res.text()?;
let ap_sign: ApSignature = let ap_sign: ApSignature = serde_json::from_str(text)?;
serde_json::from_str(text)?; let mut json: CustomGroup = serde_json::from_str(text)?;
let mut json: CustomGroup =
serde_json::from_str(text)?;
json.custom_props = ap_sign; // without this workaround, publicKey is not correctly deserialized json.custom_props = ap_sign; // without this workaround, publicKey is not correctly deserialized
Blog::from_activity( Blog::from_activity(conn, &json, Url::parse(url)?.host_str()?)
conn,
&json,
Url::parse(url)?.host_str()?,
)
} }
fn from_activity(conn: &Connection, acct: &CustomGroup, inst: &str) -> Result<Blog> { fn from_activity(conn: &Connection, acct: &CustomGroup, inst: &str) -> Result<Blog> {
let instance = Instance::find_by_domain(conn, inst).or_else(|_| let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
Instance::insert( Instance::insert(
conn, conn,
NewInstance { NewInstance {
@ -210,35 +188,17 @@ impl Blog {
long_description_html: String::new(), long_description_html: String::new(),
}, },
) )
)?; })?;
Blog::insert( Blog::insert(
conn, conn,
NewBlog { NewBlog {
actor_id: acct actor_id: acct.object.ap_actor_props.preferred_username_string()?,
.object title: acct.object.object_props.name_string()?,
.ap_actor_props outbox_url: acct.object.ap_actor_props.outbox_string()?,
.preferred_username_string()?, inbox_url: acct.object.ap_actor_props.inbox_string()?,
title: acct summary: acct.object.object_props.summary_string()?,
.object
.object_props
.name_string()?,
outbox_url: acct
.object
.ap_actor_props
.outbox_string()?,
inbox_url: acct
.object
.ap_actor_props
.inbox_string()?,
summary: acct
.object
.object_props
.summary_string()?,
instance_id: instance.id, instance_id: instance.id,
ap_url: acct ap_url: acct.object.object_props.id_string()?,
.object
.object_props
.id_string()?,
public_key: acct public_key: acct
.custom_props .custom_props
.public_key_publickey()? .public_key_publickey()?
@ -252,27 +212,20 @@ impl Blog {
let mut blog = Group::default(); let mut blog = Group::default();
blog.ap_actor_props blog.ap_actor_props
.set_preferred_username_string(self.actor_id.clone())?; .set_preferred_username_string(self.actor_id.clone())?;
blog.object_props blog.object_props.set_name_string(self.title.clone())?;
.set_name_string(self.title.clone())?;
blog.ap_actor_props blog.ap_actor_props
.set_outbox_string(self.outbox_url.clone())?; .set_outbox_string(self.outbox_url.clone())?;
blog.ap_actor_props blog.ap_actor_props
.set_inbox_string(self.inbox_url.clone())?; .set_inbox_string(self.inbox_url.clone())?;
blog.object_props blog.object_props.set_summary_string(self.summary.clone())?;
.set_summary_string(self.summary.clone())?; blog.object_props.set_id_string(self.ap_url.clone())?;
blog.object_props
.set_id_string(self.ap_url.clone())?;
let mut public_key = PublicKey::default(); let mut public_key = PublicKey::default();
public_key public_key.set_id_string(format!("{}#main-key", self.ap_url))?;
.set_id_string(format!("{}#main-key", self.ap_url))?; public_key.set_owner_string(self.ap_url.clone())?;
public_key public_key.set_public_key_pem_string(self.public_key.clone())?;
.set_owner_string(self.ap_url.clone())?;
public_key
.set_public_key_pem_string(self.public_key.clone())?;
let mut ap_signature = ApSignature::default(); let mut ap_signature = ApSignature::default();
ap_signature ap_signature.set_public_key_publickey(public_key)?;
.set_public_key_publickey(public_key)?;
Ok(CustomGroup::new(blog, ap_signature)) Ok(CustomGroup::new(blog, ap_signature))
} }
@ -290,13 +243,10 @@ impl Blog {
} }
pub fn get_keypair(&self) -> Result<PKey<Private>> { pub fn get_keypair(&self) -> Result<PKey<Private>> {
PKey::from_rsa( PKey::from_rsa(Rsa::private_key_from_pem(
Rsa::private_key_from_pem( self.private_key.clone()?.as_ref(),
self.private_key )?)
.clone()? .map_err(Error::from)
.as_ref(),
)?,
).map_err(Error::from)
} }
pub fn webfinger(&self, conn: &Connection) -> Result<Webfinger> { pub fn webfinger(&self, conn: &Connection) -> Result<Webfinger> {
@ -386,25 +336,16 @@ impl sign::Signer for Blog {
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> { fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
let key = self.get_keypair()?; let key = self.get_keypair()?;
let mut signer = let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
Signer::new(MessageDigest::sha256(), &key)?; signer.update(to_sign.as_bytes())?;
signer signer.sign_to_vec().map_err(Error::from)
.update(to_sign.as_bytes())?;
signer
.sign_to_vec()
.map_err(Error::from)
} }
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> { fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
let key = PKey::from_rsa( let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
Rsa::public_key_from_pem(self.public_key.as_ref())?
)?;
let mut verifier = Verifier::new(MessageDigest::sha256(), &key)?; let mut verifier = Verifier::new(MessageDigest::sha256(), &key)?;
verifier verifier.update(data.as_bytes())?;
.update(data.as_bytes())?; verifier.verify(&signature).map_err(Error::from)
verifier
.verify(&signature)
.map_err(Error::from)
} }
} }
@ -434,32 +375,47 @@ pub(crate) mod tests {
use blog_authors::*; use blog_authors::*;
use diesel::Connection; use diesel::Connection;
use instance::tests as instance_tests; use instance::tests as instance_tests;
use search::tests::get_searcher;
use tests::db; use tests::db;
use users::tests as usersTests; use users::tests as usersTests;
use search::tests::get_searcher;
use Connection as Conn; use Connection as Conn;
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Blog>) { pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Blog>) {
instance_tests::fill_database(conn); instance_tests::fill_database(conn);
let users = usersTests::fill_database(conn); let users = usersTests::fill_database(conn);
let blog1 = Blog::insert(conn, NewBlog::new_local( let blog1 = Blog::insert(
conn,
NewBlog::new_local(
"BlogName".to_owned(), "BlogName".to_owned(),
"Blog name".to_owned(), "Blog name".to_owned(),
"This is a small blog".to_owned(), "This is a small blog".to_owned(),
Instance::get_local(conn).unwrap().id Instance::get_local(conn).unwrap().id,
).unwrap()).unwrap(); )
let blog2 = Blog::insert(conn, NewBlog::new_local( .unwrap(),
)
.unwrap();
let blog2 = Blog::insert(
conn,
NewBlog::new_local(
"MyBlog".to_owned(), "MyBlog".to_owned(),
"My blog".to_owned(), "My blog".to_owned(),
"Welcome to my blog".to_owned(), "Welcome to my blog".to_owned(),
Instance::get_local(conn).unwrap().id Instance::get_local(conn).unwrap().id,
).unwrap()).unwrap(); )
let blog3 = Blog::insert(conn, NewBlog::new_local( .unwrap(),
)
.unwrap();
let blog3 = Blog::insert(
conn,
NewBlog::new_local(
"WhyILikePlume".to_owned(), "WhyILikePlume".to_owned(),
"Why I like Plume".to_owned(), "Why I like Plume".to_owned(),
"In this blog I will explay you why I like Plume so much".to_owned(), "In this blog I will explay you why I like Plume so much".to_owned(),
Instance::get_local(conn).unwrap().id Instance::get_local(conn).unwrap().id,
).unwrap()).unwrap(); )
.unwrap(),
)
.unwrap();
BlogAuthor::insert( BlogAuthor::insert(
conn, conn,
@ -468,7 +424,8 @@ pub(crate) mod tests {
author_id: users[0].id, author_id: users[0].id,
is_owner: true, is_owner: true,
}, },
).unwrap(); )
.unwrap();
BlogAuthor::insert( BlogAuthor::insert(
conn, conn,
@ -477,7 +434,8 @@ pub(crate) mod tests {
author_id: users[1].id, author_id: users[1].id,
is_owner: false, is_owner: false,
}, },
).unwrap(); )
.unwrap();
BlogAuthor::insert( BlogAuthor::insert(
conn, conn,
@ -486,7 +444,8 @@ pub(crate) mod tests {
author_id: users[1].id, author_id: users[1].id,
is_owner: true, is_owner: true,
}, },
).unwrap(); )
.unwrap();
BlogAuthor::insert( BlogAuthor::insert(
conn, conn,
@ -495,8 +454,9 @@ pub(crate) mod tests {
author_id: users[2].id, author_id: users[2].id,
is_owner: true, is_owner: true,
}, },
).unwrap(); )
(users, vec![ blog1, blog2, blog3 ]) .unwrap();
(users, vec![blog1, blog2, blog3])
} }
#[test] #[test]
@ -511,11 +471,16 @@ pub(crate) mod tests {
"SomeName".to_owned(), "SomeName".to_owned(),
"Some name".to_owned(), "Some name".to_owned(),
"This is some blog".to_owned(), "This is some blog".to_owned(),
Instance::get_local(conn).unwrap().id Instance::get_local(conn).unwrap().id,
).unwrap(), )
).unwrap(); .unwrap(),
)
.unwrap();
assert_eq!(blog.get_instance(conn).unwrap().id, Instance::get_local(conn).unwrap().id); assert_eq!(
blog.get_instance(conn).unwrap().id,
Instance::get_local(conn).unwrap().id
);
// TODO add tests for remote instance // TODO add tests for remote instance
Ok(()) Ok(())
@ -535,18 +500,22 @@ pub(crate) mod tests {
"Some name".to_owned(), "Some name".to_owned(),
"This is some blog".to_owned(), "This is some blog".to_owned(),
Instance::get_local(conn).unwrap().id, Instance::get_local(conn).unwrap().id,
).unwrap(), )
).unwrap(); .unwrap(),
)
.unwrap();
let b2 = Blog::insert( let b2 = Blog::insert(
conn, conn,
NewBlog::new_local( NewBlog::new_local(
"Blog".to_owned(), "Blog".to_owned(),
"Blog".to_owned(), "Blog".to_owned(),
"I've named my blog Blog".to_owned(), "I've named my blog Blog".to_owned(),
Instance::get_local(conn).unwrap().id Instance::get_local(conn).unwrap().id,
).unwrap(), )
).unwrap(); .unwrap(),
let blog = vec![ b1, b2 ]; )
.unwrap();
let blog = vec![b1, b2];
BlogAuthor::insert( BlogAuthor::insert(
conn, conn,
@ -555,7 +524,8 @@ pub(crate) mod tests {
author_id: user[0].id, author_id: user[0].id,
is_owner: true, is_owner: true,
}, },
).unwrap(); )
.unwrap();
BlogAuthor::insert( BlogAuthor::insert(
conn, conn,
@ -564,7 +534,8 @@ pub(crate) mod tests {
author_id: user[1].id, author_id: user[1].id,
is_owner: false, is_owner: false,
}, },
).unwrap(); )
.unwrap();
BlogAuthor::insert( BlogAuthor::insert(
conn, conn,
@ -573,53 +544,46 @@ pub(crate) mod tests {
author_id: user[0].id, author_id: user[0].id,
is_owner: true, is_owner: true,
}, },
).unwrap(); )
.unwrap();
assert!( assert!(blog[0]
blog[0] .list_authors(conn)
.list_authors(conn).unwrap() .unwrap()
.iter() .iter()
.any(|a| a.id == user[0].id) .any(|a| a.id == user[0].id));
); assert!(blog[0]
assert!( .list_authors(conn)
blog[0] .unwrap()
.list_authors(conn).unwrap()
.iter() .iter()
.any(|a| a.id == user[1].id) .any(|a| a.id == user[1].id));
); assert!(blog[1]
assert!( .list_authors(conn)
blog[1] .unwrap()
.list_authors(conn).unwrap()
.iter() .iter()
.any(|a| a.id == user[0].id) .any(|a| a.id == user[0].id));
); assert!(!blog[1]
assert!( .list_authors(conn)
!blog[1] .unwrap()
.list_authors(conn).unwrap()
.iter() .iter()
.any(|a| a.id == user[1].id) .any(|a| a.id == user[1].id));
);
assert!( assert!(Blog::find_for_author(conn, &user[0])
Blog::find_for_author(conn, &user[0]).unwrap() .unwrap()
.iter() .iter()
.any(|b| b.id == blog[0].id) .any(|b| b.id == blog[0].id));
); assert!(Blog::find_for_author(conn, &user[1])
assert!( .unwrap()
Blog::find_for_author(conn, &user[1]).unwrap()
.iter() .iter()
.any(|b| b.id == blog[0].id) .any(|b| b.id == blog[0].id));
); assert!(Blog::find_for_author(conn, &user[0])
assert!( .unwrap()
Blog::find_for_author(conn, &user[0]).unwrap()
.iter() .iter()
.any(|b| b.id == blog[1].id) .any(|b| b.id == blog[1].id));
); assert!(!Blog::find_for_author(conn, &user[1])
assert!( .unwrap()
!Blog::find_for_author(conn, &user[1]).unwrap()
.iter() .iter()
.any(|b| b.id == blog[1].id) .any(|b| b.id == blog[1].id));
);
Ok(()) Ok(())
}); });
@ -638,13 +602,12 @@ pub(crate) mod tests {
"Some name".to_owned(), "Some name".to_owned(),
"This is some blog".to_owned(), "This is some blog".to_owned(),
Instance::get_local(conn).unwrap().id, Instance::get_local(conn).unwrap().id,
).unwrap(), )
).unwrap(); .unwrap(),
)
.unwrap();
assert_eq!( assert_eq!(Blog::find_by_fqn(conn, "SomeName").unwrap().id, blog.id);
Blog::find_by_fqn(conn, "SomeName").unwrap().id,
blog.id
);
Ok(()) Ok(())
}); });
@ -663,8 +626,10 @@ pub(crate) mod tests {
"Some name".to_owned(), "Some name".to_owned(),
"This is some blog".to_owned(), "This is some blog".to_owned(),
Instance::get_local(conn).unwrap().id, Instance::get_local(conn).unwrap().id,
).unwrap(), )
).unwrap(); .unwrap(),
)
.unwrap();
assert_eq!(blog.fqn, "SomeName"); assert_eq!(blog.fqn, "SomeName");
@ -699,8 +664,10 @@ pub(crate) mod tests {
"Some name".to_owned(), "Some name".to_owned(),
"This is some blog".to_owned(), "This is some blog".to_owned(),
Instance::get_local(conn).unwrap().id, Instance::get_local(conn).unwrap().id,
).unwrap(), )
).unwrap(); .unwrap(),
)
.unwrap();
let b2 = Blog::insert( let b2 = Blog::insert(
conn, conn,
NewBlog::new_local( NewBlog::new_local(
@ -708,9 +675,11 @@ pub(crate) mod tests {
"Blog".to_owned(), "Blog".to_owned(),
"I've named my blog Blog".to_owned(), "I've named my blog Blog".to_owned(),
Instance::get_local(conn).unwrap().id, Instance::get_local(conn).unwrap().id,
).unwrap(), )
).unwrap(); .unwrap(),
let blog = vec![ b1, b2 ]; )
.unwrap();
let blog = vec![b1, b2];
BlogAuthor::insert( BlogAuthor::insert(
conn, conn,
@ -719,7 +688,8 @@ pub(crate) mod tests {
author_id: user[0].id, author_id: user[0].id,
is_owner: true, is_owner: true,
}, },
).unwrap(); )
.unwrap();
BlogAuthor::insert( BlogAuthor::insert(
conn, conn,
@ -728,7 +698,8 @@ pub(crate) mod tests {
author_id: user[1].id, author_id: user[1].id,
is_owner: false, is_owner: false,
}, },
).unwrap(); )
.unwrap();
BlogAuthor::insert( BlogAuthor::insert(
conn, conn,
@ -737,7 +708,8 @@ pub(crate) mod tests {
author_id: user[0].id, author_id: user[0].id,
is_owner: true, is_owner: true,
}, },
).unwrap(); )
.unwrap();
user[0].delete(conn, &searcher).unwrap(); user[0].delete(conn, &searcher).unwrap();
assert!(Blog::get(conn, blog[0].id).is_ok()); assert!(Blog::get(conn, blog[0].id).is_ok());

View File

@ -23,7 +23,8 @@ impl CommentSeers {
insert!(comment_seers, NewCommentSeers); insert!(comment_seers, NewCommentSeers);
pub fn can_see(conn: &Connection, c: &Comment, u: &User) -> Result<bool> { pub fn can_see(conn: &Connection, c: &Comment, u: &User) -> Result<bool> {
comment_seers::table.filter(comment_seers::comment_id.eq(c.id)) comment_seers::table
.filter(comment_seers::comment_id.eq(c.id))
.filter(comment_seers::user_id.eq(u.id)) .filter(comment_seers::user_id.eq(u.id))
.load::<CommentSeers>(conn) .load::<CommentSeers>(conn)
.map_err(Error::from) .map_err(Error::from)

View File

@ -1,19 +1,23 @@
use activitypub::{activity::{Create, Delete}, link, object::{Note, Tombstone}}; use activitypub::{
activity::{Create, Delete},
link,
object::{Note, Tombstone},
};
use chrono::{self, NaiveDateTime}; use chrono::{self, NaiveDateTime};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use serde_json; use serde_json;
use std::collections::HashSet; use std::collections::HashSet;
use comment_seers::{CommentSeers, NewCommentSeers};
use instance::Instance; use instance::Instance;
use mentions::Mention; use mentions::Mention;
use notifications::*; use notifications::*;
use plume_common::activity_pub::{ use plume_common::activity_pub::{
inbox::{FromActivity, Notify, Deletable}, inbox::{Deletable, FromActivity, Notify},
Id, IntoId, PUBLIC_VISIBILTY, Id, IntoId, PUBLIC_VISIBILTY,
}; };
use plume_common::utils; use plume_common::utils;
use comment_seers::{CommentSeers, NewCommentSeers};
use posts::Post; use posts::Post;
use safe_string::SafeString; use safe_string::SafeString;
use schema::comments; use schema::comments;
@ -50,7 +54,11 @@ pub struct NewComment {
impl Comment { impl Comment {
insert!(comments, NewComment, |inserted, conn| { insert!(comments, NewComment, |inserted, conn| {
if inserted.ap_url.is_none() { if inserted.ap_url.is_none() {
inserted.ap_url = Some(format!("{}comment/{}", inserted.get_post(conn)?.ap_url, inserted.id)); inserted.ap_url = Some(format!(
"{}comment/{}",
inserted.get_post(conn)?.ap_url,
inserted.id
));
let _: Comment = inserted.save_changes(conn)?; let _: Comment = inserted.save_changes(conn)?;
} }
Ok(inserted) Ok(inserted)
@ -80,20 +88,25 @@ impl Comment {
} }
pub fn get_responses(&self, conn: &Connection) -> Result<Vec<Comment>> { pub fn get_responses(&self, conn: &Connection) -> Result<Vec<Comment>> {
comments::table.filter(comments::in_response_to_id.eq(self.id)) comments::table
.filter(comments::in_response_to_id.eq(self.id))
.load::<Comment>(conn) .load::<Comment>(conn)
.map_err(Error::from) .map_err(Error::from)
} }
pub fn can_see(&self, conn: &Connection, user: Option<&User>) -> bool { pub fn can_see(&self, conn: &Connection, user: Option<&User>) -> bool {
self.public_visibility || self.public_visibility
user.as_ref().map(|u| CommentSeers::can_see(conn, self, u).unwrap_or(false)) || user
.as_ref()
.map(|u| CommentSeers::can_see(conn, self, u).unwrap_or(false))
.unwrap_or(false) .unwrap_or(false)
} }
pub fn to_activity(&self, conn: &Connection) -> Result<Note> { pub fn to_activity(&self, conn: &Connection) -> Result<Note> {
let (html, mentions, _hashtags) = utils::md_to_html(self.content.get().as_ref(), let (html, mentions, _hashtags) = utils::md_to_html(
&Instance::get_local(conn)?.public_domain); self.content.get().as_ref(),
&Instance::get_local(conn)?.public_domain,
);
let author = User::get(conn, self.author_id)?; let author = User::get(conn, self.author_id)?;
let mut note = Note::default(); let mut note = Note::default();
@ -103,8 +116,7 @@ impl Comment {
.set_id_string(self.ap_url.clone().unwrap_or_default())?; .set_id_string(self.ap_url.clone().unwrap_or_default())?;
note.object_props note.object_props
.set_summary_string(self.spoiler_text.clone())?; .set_summary_string(self.spoiler_text.clone())?;
note.object_props note.object_props.set_content_string(html)?;
.set_content_string(html)?;
note.object_props note.object_props
.set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else( .set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else(
|| Ok(Post::get(conn, self.post_id)?.ap_url), || Ok(Post::get(conn, self.post_id)?.ap_url),
@ -114,10 +126,8 @@ impl Comment {
.set_published_string(chrono::Utc::now().to_rfc3339())?; .set_published_string(chrono::Utc::now().to_rfc3339())?;
note.object_props note.object_props
.set_attributed_to_link(author.clone().into_id())?; .set_attributed_to_link(author.clone().into_id())?;
note.object_props note.object_props.set_to_link_vec(to.clone())?;
.set_to_link_vec(to.clone())?; note.object_props.set_tag_link_vec(
note.object_props
.set_tag_link_vec(
mentions mentions
.into_iter() .into_iter()
.filter_map(|m| Mention::build_activity(conn, &m).ok()) .filter_map(|m| Mention::build_activity(conn, &m).ok())
@ -127,28 +137,17 @@ impl Comment {
} }
pub fn create_activity(&self, conn: &Connection) -> Result<Create> { pub fn create_activity(&self, conn: &Connection) -> Result<Create> {
let author = let author = User::get(conn, self.author_id)?;
User::get(conn, self.author_id)?;
let note = self.to_activity(conn)?; let note = self.to_activity(conn)?;
let mut act = Create::default(); let mut act = Create::default();
act.create_props act.create_props.set_actor_link(author.into_id())?;
.set_actor_link(author.into_id())?; act.create_props.set_object_object(note.clone())?;
act.create_props
.set_object_object(note.clone())?;
act.object_props act.object_props
.set_id_string(format!( .set_id_string(format!("{}/activity", self.ap_url.clone()?,))?;
"{}/activity",
self.ap_url
.clone()?,
))?;
act.object_props act.object_props
.set_to_link_vec( .set_to_link_vec(note.object_props.to_link_vec::<Id>()?)?;
note.object_props act.object_props.set_cc_link_vec::<Id>(vec![])?;
.to_link_vec::<Id>()?,
)?;
act.object_props
.set_cc_link_vec::<Id>(vec![])?;
Ok(act) Ok(act)
} }
} }
@ -158,43 +157,39 @@ impl FromActivity<Note, Connection> for Comment {
fn from_activity(conn: &Connection, note: Note, actor: Id) -> Result<Comment> { fn from_activity(conn: &Connection, note: Note, actor: Id) -> Result<Comment> {
let comm = { let comm = {
let previous_url = note let previous_url = note.object_props.in_reply_to.as_ref()?.as_str()?;
.object_props
.in_reply_to
.as_ref()?
.as_str()?;
let previous_comment = Comment::find_by_ap_url(conn, previous_url); let previous_comment = Comment::find_by_ap_url(conn, previous_url);
let is_public = |v: &Option<serde_json::Value>| match v.as_ref().unwrap_or(&serde_json::Value::Null) { let is_public = |v: &Option<serde_json::Value>| match v
serde_json::Value::Array(v) => v.iter().filter_map(serde_json::Value::as_str).any(|s| s==PUBLIC_VISIBILTY), .as_ref()
.unwrap_or(&serde_json::Value::Null)
{
serde_json::Value::Array(v) => v
.iter()
.filter_map(serde_json::Value::as_str)
.any(|s| s == PUBLIC_VISIBILTY),
serde_json::Value::String(s) => s == PUBLIC_VISIBILTY, serde_json::Value::String(s) => s == PUBLIC_VISIBILTY,
_ => false, _ => false,
}; };
let public_visibility = is_public(&note.object_props.to) || let public_visibility = is_public(&note.object_props.to)
is_public(&note.object_props.bto) || || is_public(&note.object_props.bto)
is_public(&note.object_props.cc) || || is_public(&note.object_props.cc)
is_public(&note.object_props.bcc); || is_public(&note.object_props.bcc);
let comm = Comment::insert( let comm = Comment::insert(
conn, conn,
NewComment { NewComment {
content: SafeString::new( content: SafeString::new(&note.object_props.content_string()?),
&note spoiler_text: note.object_props.summary_string().unwrap_or_default(),
.object_props
.content_string()?
),
spoiler_text: note
.object_props
.summary_string()
.unwrap_or_default(),
ap_url: note.object_props.id_string().ok(), ap_url: note.object_props.id_string().ok(),
in_response_to_id: previous_comment.iter().map(|c| c.id).next(), in_response_to_id: previous_comment.iter().map(|c| c.id).next(),
post_id: previous_comment.map(|c| c.post_id) post_id: previous_comment.map(|c| c.post_id).or_else(|_| {
.or_else(|_| Ok(Post::find_by_ap_url(conn, previous_url)?.id) as Result<i32>)?, Ok(Post::find_by_ap_url(conn, previous_url)?.id) as Result<i32>
})?,
author_id: User::from_url(conn, actor.as_ref())?.id, author_id: User::from_url(conn, actor.as_ref())?.id,
sensitive: false, // "sensitive" is not a standard property, we need to think about how to support it with the activitypub crate sensitive: false, // "sensitive" is not a standard property, we need to think about how to support it with the activitypub crate
public_visibility public_visibility,
}, },
)?; )?;
@ -204,13 +199,11 @@ impl FromActivity<Note, Connection> for Comment {
serde_json::from_value::<link::Mention>(tag) serde_json::from_value::<link::Mention>(tag)
.map_err(Error::from) .map_err(Error::from)
.and_then(|m| { .and_then(|m| {
let author = &Post::get(conn, comm.post_id)? let author = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0];
.get_authors(conn)?[0]; let not_author = m.link_props.href_string()? != author.ap_url.clone();
let not_author = m Ok(Mention::from_activity(
.link_props conn, &m, comm.id, false, not_author,
.href_string()? )?)
!= author.ap_url.clone();
Ok(Mention::from_activity(conn, &m, comm.id, false, not_author)?)
}) })
.ok(); .ok();
} }
@ -218,14 +211,21 @@ impl FromActivity<Note, Connection> for Comment {
comm comm
}; };
if !comm.public_visibility { if !comm.public_visibility {
let receivers_ap_url = |v: Option<serde_json::Value>| { let receivers_ap_url = |v: Option<serde_json::Value>| {
let filter = |e: serde_json::Value| if let serde_json::Value::String(s) = e { Some(s) } else { None }; let filter = |e: serde_json::Value| {
if let serde_json::Value::String(s) = e {
Some(s)
} else {
None
}
};
match v.unwrap_or(serde_json::Value::Null) { match v.unwrap_or(serde_json::Value::Null) {
serde_json::Value::Array(v) => v, serde_json::Value::Array(v) => v,
v => vec![v], v => vec![v],
}.into_iter().filter_map(filter) }
.into_iter()
.filter_map(filter)
}; };
let mut note = note; let mut note = note;
@ -235,25 +235,30 @@ impl FromActivity<Note, Connection> for Comment {
let bto = receivers_ap_url(note.object_props.bto.take()); let bto = receivers_ap_url(note.object_props.bto.take());
let bcc = receivers_ap_url(note.object_props.bcc.take()); let bcc = receivers_ap_url(note.object_props.bcc.take());
let receivers_ap_url = to.chain(cc).chain(bto).chain(bcc) let receivers_ap_url = to
.collect::<HashSet<_>>()//remove duplicates (don't do a query more than once) .chain(cc)
.chain(bto)
.chain(bcc)
.collect::<HashSet<_>>() //remove duplicates (don't do a query more than once)
.into_iter() .into_iter()
.map(|v| if let Ok(user) = User::from_url(conn,&v) { .map(|v| {
if let Ok(user) = User::from_url(conn, &v) {
vec![user] vec![user]
} else { } else {
vec![]// TODO try to fetch collection vec![] // TODO try to fetch collection
}
}) })
.flatten() .flatten()
.filter(|u| u.get_instance(conn).map(|i| i.local).unwrap_or(false)) .filter(|u| u.get_instance(conn).map(|i| i.local).unwrap_or(false))
.collect::<HashSet<User>>();//remove duplicates (prevent db error) .collect::<HashSet<User>>(); //remove duplicates (prevent db error)
for user in &receivers_ap_url { for user in &receivers_ap_url {
CommentSeers::insert( CommentSeers::insert(
conn, conn,
NewCommentSeers { NewCommentSeers {
comment_id: comm.id, comment_id: comm.id,
user_id: user.id user_id: user.id,
} },
)?; )?;
} }
} }
@ -288,7 +293,8 @@ pub struct CommentTree {
impl CommentTree { impl CommentTree {
pub fn from_post(conn: &Connection, p: &Post, user: Option<&User>) -> Result<Vec<Self>> { pub fn from_post(conn: &Connection, p: &Post, user: Option<&User>) -> Result<Vec<Self>> {
Ok(Comment::list_by_post(conn, p.id)?.into_iter() Ok(Comment::list_by_post(conn, p.id)?
.into_iter()
.filter(|c| c.in_response_to_id.is_none()) .filter(|c| c.in_response_to_id.is_none())
.filter(|c| c.can_see(conn, user)) .filter(|c| c.can_see(conn, user))
.filter_map(|c| Self::from_comment(conn, c, user).ok()) .filter_map(|c| Self::from_comment(conn, c, user).ok())
@ -296,14 +302,13 @@ impl CommentTree {
} }
pub fn from_comment(conn: &Connection, comment: Comment, user: Option<&User>) -> Result<Self> { pub fn from_comment(conn: &Connection, comment: Comment, user: Option<&User>) -> Result<Self> {
let responses = comment.get_responses(conn)?.into_iter() let responses = comment
.get_responses(conn)?
.into_iter()
.filter(|c| c.can_see(conn, user)) .filter(|c| c.can_see(conn, user))
.filter_map(|c| Self::from_comment(conn, c, user).ok()) .filter_map(|c| Self::from_comment(conn, c, user).ok())
.collect(); .collect();
Ok(CommentTree { Ok(CommentTree { comment, responses })
comment,
responses,
})
} }
} }
@ -316,11 +321,8 @@ impl<'a> Deletable<Connection, Delete> for Comment {
.set_actor_link(self.get_author(conn)?.into_id())?; .set_actor_link(self.get_author(conn)?.into_id())?;
let mut tombstone = Tombstone::default(); let mut tombstone = Tombstone::default();
tombstone tombstone.object_props.set_id_string(self.ap_url.clone()?)?;
.object_props act.delete_props.set_object_object(tombstone)?;
.set_id_string(self.ap_url.clone()?)?;
act.delete_props
.set_object_object(tombstone)?;
act.object_props act.object_props
.set_id_string(format!("{}#delete", self.ap_url.clone().unwrap()))?; .set_id_string(format!("{}#delete", self.ap_url.clone().unwrap()))?;
@ -330,11 +332,11 @@ impl<'a> Deletable<Connection, Delete> for Comment {
for m in Mention::list_for_comment(&conn, self.id)? { for m in Mention::list_for_comment(&conn, self.id)? {
m.delete(conn)?; m.delete(conn)?;
} }
diesel::update(comments::table).filter(comments::in_response_to_id.eq(self.id)) diesel::update(comments::table)
.filter(comments::in_response_to_id.eq(self.id))
.set(comments::in_response_to_id.eq(self.in_response_to_id)) .set(comments::in_response_to_id.eq(self.in_response_to_id))
.execute(conn)?; .execute(conn)?;
diesel::delete(self) diesel::delete(self).execute(conn)?;
.execute(conn)?;
Ok(act) Ok(act)
} }

View File

@ -1,4 +1,6 @@
use diesel::{r2d2::{ConnectionManager, CustomizeConnection, Error as ConnError, Pool, PooledConnection}}; use diesel::r2d2::{
ConnectionManager, CustomizeConnection, Error as ConnError, Pool, PooledConnection,
};
#[cfg(feature = "sqlite")] #[cfg(feature = "sqlite")]
use diesel::{dsl::sql_query, ConnectionError, RunQueryDsl}; use diesel::{dsl::sql_query, ConnectionError, RunQueryDsl};
use rocket::{ use rocket::{
@ -47,8 +49,13 @@ pub struct PragmaForeignKey;
impl CustomizeConnection<Connection, ConnError> for PragmaForeignKey { impl CustomizeConnection<Connection, ConnError> for PragmaForeignKey {
#[cfg(feature = "sqlite")] // will default to an empty function for postgres #[cfg(feature = "sqlite")] // will default to an empty function for postgres
fn on_acquire(&self, conn: &mut Connection) -> Result<(), ConnError> { fn on_acquire(&self, conn: &mut Connection) -> Result<(), ConnError> {
sql_query("PRAGMA foreign_keys = on;").execute(conn) sql_query("PRAGMA foreign_keys = on;")
.execute(conn)
.map(|_| ()) .map(|_| ())
.map_err(|_| ConnError::ConnectionError(ConnectionError::BadConnection(String::from("PRAGMA foreign_keys = on failed")))) .map_err(|_| {
ConnError::ConnectionError(ConnectionError::BadConnection(String::from(
"PRAGMA foreign_keys = on failed",
)))
})
} }
} }

View File

@ -14,7 +14,7 @@ use plume_common::activity_pub::{
}; };
use schema::follows; use schema::follows;
use users::User; use users::User;
use {ap_url, Connection, BASE_URL, Error, Result}; use {ap_url, Connection, Error, Result, BASE_URL};
#[derive(Clone, Queryable, Identifiable, Associations, AsChangeset)] #[derive(Clone, Queryable, Identifiable, Associations, AsChangeset)]
#[belongs_to(User, foreign_key = "following_id")] #[belongs_to(User, foreign_key = "following_id")]
@ -62,12 +62,9 @@ impl Follow {
.set_actor_link::<Id>(user.clone().into_id())?; .set_actor_link::<Id>(user.clone().into_id())?;
act.follow_props act.follow_props
.set_object_link::<Id>(target.clone().into_id())?; .set_object_link::<Id>(target.clone().into_id())?;
act.object_props act.object_props.set_id_string(self.ap_url.clone())?;
.set_id_string(self.ap_url.clone())?; act.object_props.set_to_link(target.into_id())?;
act.object_props act.object_props.set_cc_link_vec::<Id>(vec![])?;
.set_to_link(target.into_id())?;
act.object_props
.set_cc_link_vec::<Id>(vec![])?;
Ok(act) Ok(act)
} }
@ -92,21 +89,13 @@ impl Follow {
let mut accept = Accept::default(); let mut accept = Accept::default();
let accept_id = ap_url(&format!("{}/follow/{}/accept", BASE_URL.as_str(), &res.id)); let accept_id = ap_url(&format!("{}/follow/{}/accept", BASE_URL.as_str(), &res.id));
accept accept.object_props.set_id_string(accept_id)?;
.object_props accept.object_props.set_to_link(from.clone().into_id())?;
.set_id_string(accept_id)?; accept.object_props.set_cc_link_vec::<Id>(vec![])?;
accept
.object_props
.set_to_link(from.clone().into_id())?;
accept
.object_props
.set_cc_link_vec::<Id>(vec![])?;
accept accept
.accept_props .accept_props
.set_actor_link::<Id>(target.clone().into_id())?; .set_actor_link::<Id>(target.clone().into_id())?;
accept accept.accept_props.set_object_object(follow)?;
.accept_props
.set_object_object(follow)?;
broadcast(&*target, accept, vec![from.clone()]); broadcast(&*target, accept, vec![from.clone()]);
Ok(res) Ok(res)
} }
@ -120,29 +109,18 @@ impl FromActivity<FollowAct, Connection> for Follow {
.follow_props .follow_props
.actor_link::<Id>() .actor_link::<Id>()
.map(|l| l.into()) .map(|l| l.into())
.or_else(|_| Ok(follow .or_else(|_| {
Ok(follow
.follow_props .follow_props
.actor_object::<Person>()? .actor_object::<Person>()?
.object_props .object_props
.id_string()?) as Result<String>)?; .id_string()?) as Result<String>
let from = })?;
User::from_url(conn, &from_id)?; let from = User::from_url(conn, &from_id)?;
match User::from_url( match User::from_url(conn, follow.follow_props.object.as_str()?) {
conn,
follow
.follow_props
.object
.as_str()?,
) {
Ok(user) => Follow::accept_follow(conn, &from, &user, follow, from.id, user.id), Ok(user) => Follow::accept_follow(conn, &from, &user, follow, from.id, user.id),
Err(_) => { Err(_) => {
let blog = Blog::from_url( let blog = Blog::from_url(conn, follow.follow_props.object.as_str()?)?;
conn,
follow
.follow_props
.object
.as_str()?,
)?;
Follow::accept_follow(conn, &from, &blog, follow, from.id, blog.id) Follow::accept_follow(conn, &from, &blog, follow, from.id, blog.id)
} }
} }
@ -160,7 +138,8 @@ impl Notify<Connection> for Follow {
object_id: self.id, object_id: self.id,
user_id: self.following_id, user_id: self.following_id,
}, },
).map(|_| ()) )
.map(|_| ())
} }
} }
@ -168,21 +147,16 @@ impl Deletable<Connection, Undo> for Follow {
type Error = Error; type Error = Error;
fn delete(&self, conn: &Connection) -> Result<Undo> { fn delete(&self, conn: &Connection) -> Result<Undo> {
diesel::delete(self) diesel::delete(self).execute(conn)?;
.execute(conn)?;
// delete associated notification if any // delete associated notification if any
if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) { if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) {
diesel::delete(&notif) diesel::delete(&notif).execute(conn)?;
.execute(conn)?;
} }
let mut undo = Undo::default(); let mut undo = Undo::default();
undo.undo_props undo.undo_props
.set_actor_link( .set_actor_link(User::get(conn, self.follower_id)?.into_id())?;
User::get(conn, self.follower_id)?
.into_id(),
)?;
undo.object_props undo.object_props
.set_id_string(format!("{}/undo", self.ap_url))?; .set_id_string(format!("{}/undo", self.ap_url))?;
undo.undo_props undo.undo_props
@ -209,8 +183,8 @@ impl IntoId for Follow {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use diesel::Connection;
use super::*; use super::*;
use diesel::Connection;
use tests::db; use tests::db;
use users::tests as user_tests; use users::tests as user_tests;
@ -219,18 +193,29 @@ mod tests {
let conn = db(); let conn = db();
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let users = user_tests::fill_database(&conn); let users = user_tests::fill_database(&conn);
let follow = Follow::insert(&conn, NewFollow { let follow = Follow::insert(
&conn,
NewFollow {
follower_id: users[0].id, follower_id: users[0].id,
following_id: users[1].id, following_id: users[1].id,
ap_url: String::new(), ap_url: String::new(),
}).expect("Couldn't insert new follow"); },
assert_eq!(follow.ap_url, format!("https://{}/follows/{}", *BASE_URL, follow.id)); )
.expect("Couldn't insert new follow");
assert_eq!(
follow.ap_url,
format!("https://{}/follows/{}", *BASE_URL, follow.id)
);
let follow = Follow::insert(&conn, NewFollow { let follow = Follow::insert(
&conn,
NewFollow {
follower_id: users[1].id, follower_id: users[1].id,
following_id: users[0].id, following_id: users[0].id,
ap_url: String::from("https://some.url/"), ap_url: String::from("https://some.url/"),
}).expect("Couldn't insert new follow"); },
)
.expect("Couldn't insert new follow");
assert_eq!(follow.ap_url, String::from("https://some.url/")); assert_eq!(follow.ap_url, String::from("https://some.url/"));
Ok(()) Ok(())
}); });

View File

@ -20,7 +20,7 @@ pub struct Instance {
pub open_registrations: bool, pub open_registrations: bool,
pub short_description: SafeString, pub short_description: SafeString,
pub long_description: SafeString, pub long_description: SafeString,
pub default_license : String, pub default_license: String,
pub long_description_html: SafeString, pub long_description_html: SafeString,
pub short_description_html: SafeString, pub short_description_html: SafeString,
} }
@ -46,7 +46,8 @@ impl Instance {
.limit(1) .limit(1)
.load::<Instance>(conn)? .load::<Instance>(conn)?
.into_iter() .into_iter()
.nth(0).ok_or(Error::NotFound) .nth(0)
.ok_or(Error::NotFound)
} }
pub fn get_remotes(conn: &Connection) -> Result<Vec<Instance>> { pub fn get_remotes(conn: &Connection) -> Result<Vec<Instance>> {
@ -109,12 +110,7 @@ impl Instance {
.map_err(Error::from) .map_err(Error::from)
} }
pub fn compute_box( pub fn compute_box(&self, prefix: &str, name: &str, box_name: &str) -> String {
&self,
prefix: &str,
name: &str,
box_name: &str,
) -> String {
ap_url(&format!( ap_url(&format!(
"{instance}/{prefix}/{name}/{box_name}", "{instance}/{prefix}/{name}/{box_name}",
instance = self.public_domain, instance = self.public_domain,
@ -209,7 +205,8 @@ pub(crate) mod tests {
open_registrations: true, open_registrations: true,
public_domain: "3plu.me".to_string(), public_domain: "3plu.me".to_string(),
}, },
].into_iter() ]
.into_iter()
.map(|inst| { .map(|inst| {
( (
inst.clone(), inst.clone(),
@ -244,8 +241,14 @@ pub(crate) mod tests {
public_domain public_domain
] ]
); );
assert_eq!(res.long_description_html.get(), &inserted.long_description_html); assert_eq!(
assert_eq!(res.short_description_html.get(), &inserted.short_description_html); res.long_description_html.get(),
&inserted.long_description_html
);
assert_eq!(
res.short_description_html.get(),
&inserted.short_description_html
);
Ok(()) Ok(())
}); });
@ -282,8 +285,14 @@ pub(crate) mod tests {
public_domain public_domain
] ]
); );
assert_eq!(&newinst.long_description_html, inst.long_description_html.get()); assert_eq!(
assert_eq!(&newinst.short_description_html, inst.short_description_html.get()); &newinst.long_description_html,
inst.long_description_html.get()
);
assert_eq!(
&newinst.short_description_html,
inst.short_description_html.get()
);
}); });
let page = Instance::page(conn, (0, 2)).unwrap(); let page = Instance::page(conn, (0, 2)).unwrap();
@ -292,7 +301,9 @@ pub(crate) mod tests {
let page2 = &page[1]; let page2 = &page[1];
assert!(page1.public_domain <= page2.public_domain); assert!(page1.public_domain <= page2.public_domain);
let mut last_domaine: String = Instance::page(conn, (0, 1)).unwrap()[0].public_domain.clone(); let mut last_domaine: String = Instance::page(conn, (0, 1)).unwrap()[0]
.public_domain
.clone();
for i in 1..inserted.len() as i32 { for i in 1..inserted.len() as i32 {
let page = Instance::page(conn, (i, i + 1)).unwrap(); let page = Instance::page(conn, (i, i + 1)).unwrap();
assert_eq!(page.len(), 1); assert_eq!(page.len(), 1);
@ -326,11 +337,13 @@ pub(crate) mod tests {
0 0
); );
assert_eq!( assert_eq!(
Instance::is_blocked(conn, &format!("https://{}/something", inst.public_domain)).unwrap(), Instance::is_blocked(conn, &format!("https://{}/something", inst.public_domain))
.unwrap(),
inst.blocked inst.blocked
); );
assert_eq!( assert_eq!(
Instance::is_blocked(conn, &format!("https://{}a/something", inst.public_domain)).unwrap(), Instance::is_blocked(conn, &format!("https://{}a/something", inst.public_domain))
.unwrap(),
Instance::find_by_domain(conn, &format!("{}a", inst.public_domain)) Instance::find_by_domain(conn, &format!("{}a", inst.public_domain))
.map(|inst| inst.blocked) .map(|inst| inst.blocked)
.unwrap_or(false) .unwrap_or(false)
@ -340,11 +353,13 @@ pub(crate) mod tests {
let inst = Instance::get(conn, inst.id).unwrap(); let inst = Instance::get(conn, inst.id).unwrap();
assert_eq!(inst.blocked, blocked); assert_eq!(inst.blocked, blocked);
assert_eq!( assert_eq!(
Instance::is_blocked(conn, &format!("https://{}/something", inst.public_domain)).unwrap(), Instance::is_blocked(conn, &format!("https://{}/something", inst.public_domain))
.unwrap(),
inst.blocked inst.blocked
); );
assert_eq!( assert_eq!(
Instance::is_blocked(conn, &format!("https://{}a/something", inst.public_domain)).unwrap(), Instance::is_blocked(conn, &format!("https://{}a/something", inst.public_domain))
.unwrap(),
Instance::find_by_domain(conn, &format!("{}a", inst.public_domain)) Instance::find_by_domain(conn, &format!("{}a", inst.public_domain))
.map(|inst| inst.blocked) .map(|inst| inst.blocked)
.unwrap_or(false) .unwrap_or(false)
@ -375,7 +390,8 @@ pub(crate) mod tests {
false, false,
SafeString::new("[short](#link)"), SafeString::new("[short](#link)"),
SafeString::new("[long_description](/with_link)"), SafeString::new("[long_description](/with_link)"),
).unwrap(); )
.unwrap();
let inst = Instance::get(conn, inst.id).unwrap(); let inst = Instance::get(conn, inst.id).unwrap();
assert_eq!(inst.name, "NewName".to_owned()); assert_eq!(inst.name, "NewName".to_owned());
assert_eq!(inst.open_registrations, false); assert_eq!(inst.open_registrations, false);

View File

@ -292,8 +292,8 @@ static DB_NAME: &str = "plume_tests";
#[cfg(all(feature = "postgres", not(feature = "sqlite")))] #[cfg(all(feature = "postgres", not(feature = "sqlite")))]
lazy_static! { lazy_static! {
pub static ref DATABASE_URL: String = pub static ref DATABASE_URL: String = env::var("DATABASE_URL")
env::var("DATABASE_URL").unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME)); .unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME));
} }
#[cfg(all(feature = "sqlite", not(feature = "postgres")))] #[cfg(all(feature = "sqlite", not(feature = "postgres")))]
@ -336,7 +336,9 @@ mod tests {
Conn::establish(&*DATABASE_URL.as_str()).expect("Couldn't connect to the database"); Conn::establish(&*DATABASE_URL.as_str()).expect("Couldn't connect to the database");
embedded_migrations::run(&conn).expect("Couldn't run migrations"); embedded_migrations::run(&conn).expect("Couldn't run migrations");
#[cfg(feature = "sqlite")] #[cfg(feature = "sqlite")]
sql_query("PRAGMA foreign_keys = on;").execute(&conn).expect("PRAGMA foreign_keys fail"); sql_query("PRAGMA foreign_keys = on;")
.execute(&conn)
.expect("PRAGMA foreign_keys fail");
conn conn
} }
} }
@ -346,8 +348,8 @@ pub mod api_tokens;
pub mod apps; pub mod apps;
pub mod blog_authors; pub mod blog_authors;
pub mod blogs; pub mod blogs;
pub mod comments;
pub mod comment_seers; pub mod comment_seers;
pub mod comments;
pub mod db_conn; pub mod db_conn;
pub mod follows; pub mod follows;
pub mod headers; pub mod headers;
@ -360,7 +362,7 @@ pub mod post_authors;
pub mod posts; pub mod posts;
pub mod reshares; pub mod reshares;
pub mod safe_string; pub mod safe_string;
pub mod search;
pub mod schema; pub mod schema;
pub mod search;
pub mod tags; pub mod tags;
pub mod users; pub mod users;

View File

@ -38,21 +38,13 @@ impl Like {
pub fn to_activity(&self, conn: &Connection) -> Result<activity::Like> { pub fn to_activity(&self, conn: &Connection) -> Result<activity::Like> {
let mut act = activity::Like::default(); let mut act = activity::Like::default();
act.like_props act.like_props
.set_actor_link( .set_actor_link(User::get(conn, self.user_id)?.into_id())?;
User::get(conn, self.user_id)?
.into_id(),
)?;
act.like_props act.like_props
.set_object_link( .set_object_link(Post::get(conn, self.post_id)?.into_id())?;
Post::get(conn, self.post_id)?
.into_id(),
)?;
act.object_props act.object_props
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?; .set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
act.object_props act.object_props.set_cc_link_vec::<Id>(vec![])?;
.set_cc_link_vec::<Id>(vec![])?; act.object_props.set_id_string(self.ap_url.clone())?;
act.object_props
.set_id_string(self.ap_url.clone())?;
Ok(act) Ok(act)
} }
@ -62,18 +54,8 @@ impl FromActivity<activity::Like, Connection> for Like {
type Error = Error; type Error = Error;
fn from_activity(conn: &Connection, like: activity::Like, _actor: Id) -> Result<Like> { fn from_activity(conn: &Connection, like: activity::Like, _actor: Id) -> Result<Like> {
let liker = User::from_url( let liker = User::from_url(conn, like.like_props.actor.as_str()?)?;
conn, let post = Post::find_by_ap_url(conn, like.like_props.object.as_str()?)?;
like.like_props
.actor
.as_str()?,
)?;
let post = Post::find_by_ap_url(
conn,
like.like_props
.object
.as_str()?,
)?;
let res = Like::insert( let res = Like::insert(
conn, conn,
NewLike { NewLike {
@ -110,26 +92,22 @@ impl Deletable<Connection, activity::Undo> for Like {
type Error = Error; type Error = Error;
fn delete(&self, conn: &Connection) -> Result<activity::Undo> { fn delete(&self, conn: &Connection) -> Result<activity::Undo> {
diesel::delete(self) diesel::delete(self).execute(conn)?;
.execute(conn)?;
// delete associated notification if any // delete associated notification if any
if let Ok(notif) = Notification::find(conn, notification_kind::LIKE, self.id) { if let Ok(notif) = Notification::find(conn, notification_kind::LIKE, self.id) {
diesel::delete(&notif) diesel::delete(&notif).execute(conn)?;
.execute(conn)?;
} }
let mut act = activity::Undo::default(); let mut act = activity::Undo::default();
act.undo_props act.undo_props
.set_actor_link(User::get(conn, self.user_id)?.into_id(),)?; .set_actor_link(User::get(conn, self.user_id)?.into_id())?;
act.undo_props act.undo_props.set_object_object(self.to_activity(conn)?)?;
.set_object_object(self.to_activity(conn)?)?;
act.object_props act.object_props
.set_id_string(format!("{}#delete", self.ap_url))?; .set_id_string(format!("{}#delete", self.ap_url))?;
act.object_props act.object_props
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?; .set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
act.object_props act.object_props.set_cc_link_vec::<Id>(vec![])?;
.set_cc_link_vec::<Id>(vec![])?;
Ok(act) Ok(act)
} }
@ -151,7 +129,7 @@ impl NewLike {
NewLike { NewLike {
post_id: p.id, post_id: p.id,
user_id: u.id, user_id: u.id,
ap_url ap_url,
} }
} }
} }

View File

@ -62,12 +62,14 @@ impl Media {
list_by!(medias, for_user, owner_id as i32); list_by!(medias, for_user, owner_id as i32);
pub fn list_all_medias(conn: &Connection) -> Result<Vec<Media>> { pub fn list_all_medias(conn: &Connection) -> Result<Vec<Media>> {
medias::table medias::table.load::<Media>(conn).map_err(Error::from)
.load::<Media>(conn)
.map_err(Error::from)
} }
pub fn page_for_user(conn: &Connection, user: &User, (min, max): (i32, i32)) -> Result<Vec<Media>> { pub fn page_for_user(
conn: &Connection,
user: &User,
(min, max): (i32, i32),
) -> Result<Vec<Media>> {
medias::table medias::table
.filter(medias::owner_id.eq(user.id)) .filter(medias::owner_id.eq(user.id))
.offset(i64::from(min)) .offset(i64::from(min))
@ -124,7 +126,9 @@ impl Media {
pub fn markdown(&self, conn: &Connection) -> Result<SafeString> { pub fn markdown(&self, conn: &Connection) -> Result<SafeString> {
let url = self.url(conn)?; let url = self.url(conn)?;
Ok(match self.category() { Ok(match self.category() {
MediaCategory::Image => SafeString::new(&format!("![{}]({})", escape(&self.alt_text), url)), MediaCategory::Image => {
SafeString::new(&format!("![{}]({})", escape(&self.alt_text), url))
}
MediaCategory::Audio | MediaCategory::Video => self.html(conn)?, MediaCategory::Audio | MediaCategory::Video => self.html(conn)?,
MediaCategory::Unknown => SafeString::new(""), MediaCategory::Unknown => SafeString::new(""),
}) })
@ -216,7 +220,8 @@ impl Media {
.into_iter() .into_iter()
.next()? .next()?
.as_ref(), .as_ref(),
)?.id, )?
.id,
}, },
) )
} }
@ -249,7 +254,9 @@ pub(crate) mod tests {
let f2 = "static/media/2.mp3".to_owned(); let f2 = "static/media/2.mp3".to_owned();
fs::write(f1.clone(), []).unwrap(); fs::write(f1.clone(), []).unwrap();
fs::write(f2.clone(), []).unwrap(); fs::write(f2.clone(), []).unwrap();
(users, vec![ (
users,
vec![
NewMedia { NewMedia {
file_path: f1, file_path: f1,
alt_text: "some alt".to_owned(), alt_text: "some alt".to_owned(),
@ -277,9 +284,11 @@ pub(crate) mod tests {
content_warning: None, content_warning: None,
owner_id: user_two, owner_id: user_two,
}, },
].into_iter() ]
.into_iter()
.map(|nm| Media::insert(conn, nm).unwrap()) .map(|nm| Media::insert(conn, nm).unwrap())
.collect()) .collect(),
)
} }
pub(crate) fn clean(conn: &Conn) { pub(crate) fn clean(conn: &Conn) {
@ -311,7 +320,8 @@ pub(crate) mod tests {
content_warning: None, content_warning: None,
owner_id: user, owner_id: user,
}, },
).unwrap(); )
.unwrap();
assert!(Path::new(&path).exists()); assert!(Path::new(&path).exists());
media.delete(conn).unwrap(); media.delete(conn).unwrap();
@ -346,29 +356,26 @@ pub(crate) mod tests {
content_warning: None, content_warning: None,
owner_id: u1.id, owner_id: u1.id,
}, },
).unwrap(); )
.unwrap();
assert!( assert!(Media::for_user(conn, u1.id)
Media::for_user(conn, u1.id).unwrap() .unwrap()
.iter() .iter()
.any(|m| m.id == media.id) .any(|m| m.id == media.id));
); assert!(!Media::for_user(conn, u2.id)
assert!( .unwrap()
!Media::for_user(conn, u2.id).unwrap()
.iter() .iter()
.any(|m| m.id == media.id) .any(|m| m.id == media.id));
);
media.set_owner(conn, u2).unwrap(); media.set_owner(conn, u2).unwrap();
assert!( assert!(!Media::for_user(conn, u1.id)
!Media::for_user(conn, u1.id).unwrap() .unwrap()
.iter() .iter()
.any(|m| m.id == media.id) .any(|m| m.id == media.id));
); assert!(Media::for_user(conn, u2.id)
assert!( .unwrap()
Media::for_user(conn, u2.id).unwrap()
.iter() .iter()
.any(|m| m.id == media.id) .any(|m| m.id == media.id));
);
clean(conn); clean(conn);

View File

@ -37,11 +37,15 @@ impl Mention {
} }
pub fn get_post(&self, conn: &Connection) -> Result<Post> { pub fn get_post(&self, conn: &Connection) -> Result<Post> {
self.post_id.ok_or(Error::NotFound).and_then(|id| Post::get(conn, id)) self.post_id
.ok_or(Error::NotFound)
.and_then(|id| Post::get(conn, id))
} }
pub fn get_comment(&self, conn: &Connection) -> Result<Comment> { pub fn get_comment(&self, conn: &Connection) -> Result<Comment> {
self.comment_id.ok_or(Error::NotFound).and_then(|id| Comment::get(conn, id)) self.comment_id
.ok_or(Error::NotFound)
.and_then(|id| Comment::get(conn, id))
} }
pub fn get_user(&self, conn: &Connection) -> Result<User> { pub fn get_user(&self, conn: &Connection) -> Result<User> {
@ -54,21 +58,15 @@ impl Mention {
pub fn build_activity(conn: &Connection, ment: &str) -> Result<link::Mention> { pub fn build_activity(conn: &Connection, ment: &str) -> Result<link::Mention> {
let user = User::find_by_fqn(conn, ment)?; let user = User::find_by_fqn(conn, ment)?;
let mut mention = link::Mention::default(); let mut mention = link::Mention::default();
mention mention.link_props.set_href_string(user.ap_url)?;
.link_props mention.link_props.set_name_string(format!("@{}", ment))?;
.set_href_string(user.ap_url)?;
mention
.link_props
.set_name_string(format!("@{}", ment))?;
Ok(mention) Ok(mention)
} }
pub fn to_activity(&self, conn: &Connection) -> Result<link::Mention> { pub fn to_activity(&self, conn: &Connection) -> Result<link::Mention> {
let user = self.get_mentioned(conn)?; let user = self.get_mentioned(conn)?;
let mut mention = link::Mention::default(); let mut mention = link::Mention::default();
mention mention.link_props.set_href_string(user.ap_url.clone())?;
.link_props
.set_href_string(user.ap_url.clone())?;
mention mention
.link_props .link_props
.set_name_string(format!("@{}", user.fqn))?; .set_name_string(format!("@{}", user.fqn))?;
@ -141,6 +139,7 @@ impl Notify<Connection> for Mention {
object_id: self.id, object_id: self.id,
user_id: m.id, user_id: m.id,
}, },
).map(|_| ()) )
.map(|_| ())
} }
} }

View File

@ -80,24 +80,40 @@ impl Notification {
pub fn get_url(&self, conn: &Connection) -> Option<String> { pub fn get_url(&self, conn: &Connection) -> Option<String> {
match self.kind.as_ref() { match self.kind.as_ref() {
notification_kind::COMMENT => self.get_post(conn).and_then(|p| Some(format!("{}#comment-{}", p.url(conn).ok()?, self.object_id))), notification_kind::COMMENT => self
.get_post(conn)
.and_then(|p| Some(format!("{}#comment-{}", p.url(conn).ok()?, self.object_id))),
notification_kind::FOLLOW => Some(format!("/@/{}/", self.get_actor(conn).ok()?.fqn)), notification_kind::FOLLOW => Some(format!("/@/{}/", self.get_actor(conn).ok()?.fqn)),
notification_kind::MENTION => Mention::get(conn, self.object_id).and_then(|mention| notification_kind::MENTION => Mention::get(conn, self.object_id)
mention.get_post(conn).and_then(|p| p.url(conn)) .and_then(|mention| {
mention
.get_post(conn)
.and_then(|p| p.url(conn))
.or_else(|_| { .or_else(|_| {
let comment = mention.get_comment(conn)?; let comment = mention.get_comment(conn)?;
Ok(format!("{}#comment-{}", comment.get_post(conn)?.url(conn)?, comment.id)) Ok(format!(
"{}#comment-{}",
comment.get_post(conn)?.url(conn)?,
comment.id
))
}) })
).ok(), })
.ok(),
_ => None, _ => None,
} }
} }
pub fn get_post(&self, conn: &Connection) -> Option<Post> { pub fn get_post(&self, conn: &Connection) -> Option<Post> {
match self.kind.as_ref() { match self.kind.as_ref() {
notification_kind::COMMENT => Comment::get(conn, self.object_id).and_then(|comment| comment.get_post(conn)).ok(), notification_kind::COMMENT => Comment::get(conn, self.object_id)
notification_kind::LIKE => Like::get(conn, self.object_id).and_then(|like| Post::get(conn, like.post_id)).ok(), .and_then(|comment| comment.get_post(conn))
notification_kind::RESHARE => Reshare::get(conn, self.object_id).and_then(|reshare| reshare.get_post(conn)).ok(), .ok(),
notification_kind::LIKE => Like::get(conn, self.object_id)
.and_then(|like| Post::get(conn, like.post_id))
.ok(),
notification_kind::RESHARE => Reshare::get(conn, self.object_id)
.and_then(|reshare| reshare.get_post(conn))
.ok(),
_ => None, _ => None,
} }
} }
@ -105,7 +121,9 @@ impl Notification {
pub fn get_actor(&self, conn: &Connection) -> Result<User> { pub fn get_actor(&self, conn: &Connection) -> Result<User> {
Ok(match self.kind.as_ref() { Ok(match self.kind.as_ref() {
notification_kind::COMMENT => Comment::get(conn, self.object_id)?.get_author(conn)?, notification_kind::COMMENT => Comment::get(conn, self.object_id)?.get_author(conn)?,
notification_kind::FOLLOW => User::get(conn, Follow::get(conn, self.object_id)?.follower_id)?, notification_kind::FOLLOW => {
User::get(conn, Follow::get(conn, self.object_id)?.follower_id)?
}
notification_kind::LIKE => User::get(conn, Like::get(conn, self.object_id)?.user_id)?, notification_kind::LIKE => User::get(conn, Like::get(conn, self.object_id)?.user_id)?,
notification_kind::MENTION => Mention::get(conn, self.object_id)?.get_user(conn)?, notification_kind::MENTION => Mention::get(conn, self.object_id)?.get_user(conn)?,
notification_kind::RESHARE => Reshare::get(conn, self.object_id)?.get_user(conn)?, notification_kind::RESHARE => Reshare::get(conn, self.object_id)?.get_user(conn)?,

View File

@ -1,8 +1,8 @@
use activitypub::{ use activitypub::{
CustomObject,
activity::{Create, Delete, Update}, activity::{Create, Delete, Update},
link, link,
object::{Article, Image, Tombstone}, object::{Article, Image, Tombstone},
CustomObject,
}; };
use canapi::{Error as ApiError, Provider}; use canapi::{Error as ApiError, Provider};
use chrono::{NaiveDateTime, TimeZone, Utc}; use chrono::{NaiveDateTime, TimeZone, Utc};
@ -12,25 +12,26 @@ use scheduled_thread_pool::ScheduledThreadPool as Worker;
use serde_json; use serde_json;
use std::collections::HashSet; use std::collections::HashSet;
use plume_api::posts::PostEndpoint;
use plume_common::{
activity_pub::{
inbox::{Deletable, FromActivity},
broadcast, Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILTY,
},
utils::md_to_html,
};
use blogs::Blog; use blogs::Blog;
use instance::Instance; use instance::Instance;
use medias::Media; use medias::Media;
use mentions::Mention; use mentions::Mention;
use plume_api::posts::PostEndpoint;
use plume_common::{
activity_pub::{
broadcast,
inbox::{Deletable, FromActivity},
Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILTY,
},
utils::md_to_html,
};
use post_authors::*; use post_authors::*;
use safe_string::SafeString; use safe_string::SafeString;
use search::Searcher;
use schema::posts; use schema::posts;
use search::Searcher;
use tags::*; use tags::*;
use users::User; use users::User;
use {ap_url, Connection, BASE_URL, Error, Result, ApiResult}; use {ap_url, ApiResult, Connection, Error, Result, BASE_URL};
pub type LicensedArticle = CustomObject<Licensed, Article>; pub type LicensedArticle = CustomObject<Licensed, Article>;
@ -75,7 +76,11 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
id: i32, id: i32,
) -> ApiResult<PostEndpoint> { ) -> ApiResult<PostEndpoint> {
if let Ok(post) = Post::get(conn, id) { if let Ok(post) = Post::get(conn, id) {
if !post.published && !user_id.map(|u| post.is_author(conn, u).unwrap_or(false)).unwrap_or(false) { if !post.published
&& !user_id
.map(|u| post.is_author(conn, u).unwrap_or(false))
.unwrap_or(false)
{
return Err(ApiError::Authorization( return Err(ApiError::Authorization(
"You are not authorized to access this post yet.".to_string(), "You are not authorized to access this post yet.".to_string(),
)); ));
@ -86,12 +91,23 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
subtitle: Some(post.subtitle.clone()), subtitle: Some(post.subtitle.clone()),
content: Some(post.content.get().clone()), content: Some(post.content.get().clone()),
source: Some(post.source.clone()), source: Some(post.source.clone()),
author: Some(post.get_authors(conn).map_err(|_| ApiError::NotFound("Authors not found".into()))?[0].username.clone()), author: Some(
post.get_authors(conn)
.map_err(|_| ApiError::NotFound("Authors not found".into()))?[0]
.username
.clone(),
),
blog_id: Some(post.blog_id), blog_id: Some(post.blog_id),
published: Some(post.published), published: Some(post.published),
creation_date: Some(post.creation_date.format("%Y-%m-%d").to_string()), creation_date: Some(post.creation_date.format("%Y-%m-%d").to_string()),
license: Some(post.license.clone()), license: Some(post.license.clone()),
tags: Some(Tag::for_post(conn, post.id).map_err(|_| ApiError::NotFound("Tags not found".into()))?.into_iter().map(|t| t.tag).collect()), tags: Some(
Tag::for_post(conn, post.id)
.map_err(|_| ApiError::NotFound("Tags not found".into()))?
.into_iter()
.map(|t| t.tag)
.collect(),
),
cover_id: post.cover_id, cover_id: post.cover_id,
}) })
} else { } else {
@ -114,8 +130,16 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
query = query.filter(posts::content.eq(content)); query = query.filter(posts::content.eq(content));
} }
query.get_results::<Post>(*conn).map(|ps| ps.into_iter() query
.filter(|p| p.published || user_id.map(|u| p.is_author(conn, u).unwrap_or(false)).unwrap_or(false)) .get_results::<Post>(*conn)
.map(|ps| {
ps.into_iter()
.filter(|p| {
p.published
|| user_id
.map(|u| p.is_author(conn, u).unwrap_or(false))
.unwrap_or(false)
})
.map(|p| PostEndpoint { .map(|p| PostEndpoint {
id: Some(p.id), id: Some(p.id),
title: Some(p.title.clone()), title: Some(p.title.clone()),
@ -127,11 +151,18 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
published: Some(p.published), published: Some(p.published),
creation_date: Some(p.creation_date.format("%Y-%m-%d").to_string()), creation_date: Some(p.creation_date.format("%Y-%m-%d").to_string()),
license: Some(p.license.clone()), license: Some(p.license.clone()),
tags: Some(Tag::for_post(conn, p.id).unwrap_or_else(|_| vec![]).into_iter().map(|t| t.tag).collect()), tags: Some(
Tag::for_post(conn, p.id)
.unwrap_or_else(|_| vec![])
.into_iter()
.map(|t| t.tag)
.collect(),
),
cover_id: p.cover_id, cover_id: p.cover_id,
}) })
.collect() .collect()
).unwrap_or_else(|_| vec![]) })
.unwrap_or_else(|_| vec![])
} }
fn update( fn update(
@ -142,11 +173,15 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
unimplemented!() unimplemented!()
} }
fn delete((conn, _worker, search, user_id): &(&Connection, &Worker, &Searcher, Option<i32>), id: i32) { fn delete(
(conn, _worker, search, user_id): &(&Connection, &Worker, &Searcher, Option<i32>),
id: i32,
) {
let user_id = user_id.expect("Post as Provider::delete: not authenticated"); let user_id = user_id.expect("Post as Provider::delete: not authenticated");
if let Ok(post) = Post::get(conn, id) { if let Ok(post) = Post::get(conn, id) {
if post.is_author(conn, user_id).unwrap_or(false) { if post.is_author(conn, user_id).unwrap_or(false) {
post.delete(&(conn, search)).expect("Post as Provider::delete: delete error"); post.delete(&(conn, search))
.expect("Post as Provider::delete: delete error");
} }
} }
} }
@ -156,84 +191,124 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
query: PostEndpoint, query: PostEndpoint,
) -> ApiResult<PostEndpoint> { ) -> ApiResult<PostEndpoint> {
if user_id.is_none() { if user_id.is_none() {
return Err(ApiError::Authorization("You are not authorized to create new articles.".to_string())); return Err(ApiError::Authorization(
"You are not authorized to create new articles.".to_string(),
));
} }
let title = query.title.clone().expect("No title for new post in API"); let title = query.title.clone().expect("No title for new post in API");
let slug = query.title.unwrap().to_kebab_case(); let slug = query.title.unwrap().to_kebab_case();
let date = query.creation_date.clone() let date = query.creation_date.clone().and_then(|d| {
.and_then(|d| NaiveDateTime::parse_from_str(format!("{} 00:00:00", d).as_ref(), "%Y-%m-%d %H:%M:%S").ok()); NaiveDateTime::parse_from_str(format!("{} 00:00:00", d).as_ref(), "%Y-%m-%d %H:%M:%S")
.ok()
});
let domain = &Instance::get_local(&conn) let domain = &Instance::get_local(&conn)
.map_err(|_| ApiError::NotFound("posts::update: Error getting local instance".into()))? .map_err(|_| ApiError::NotFound("posts::update: Error getting local instance".into()))?
.public_domain; .public_domain;
let (content, mentions, hashtags) = md_to_html(query.source.clone().unwrap_or_default().clone().as_ref(), domain); let (content, mentions, hashtags) = md_to_html(
query.source.clone().unwrap_or_default().clone().as_ref(),
domain,
);
let author = User::get(conn, user_id.expect("<Post as Provider>::create: no user_id error")) let author = User::get(
conn,
user_id.expect("<Post as Provider>::create: no user_id error"),
)
.map_err(|_| ApiError::NotFound("Author not found".into()))?; .map_err(|_| ApiError::NotFound("Author not found".into()))?;
let blog = match query.blog_id { let blog = match query.blog_id {
Some(x) => x, Some(x) => x,
None => Blog::find_for_author(conn, &author).map_err(|_| ApiError::NotFound("No default blog".into()))?[0].id None => {
Blog::find_for_author(conn, &author)
.map_err(|_| ApiError::NotFound("No default blog".into()))?[0]
.id
}
}; };
if Post::find_by_slug(conn, &slug, blog).is_ok() { if Post::find_by_slug(conn, &slug, blog).is_ok() {
// Not an actual authorization problem, but we have nothing better for now… // Not an actual authorization problem, but we have nothing better for now…
// TODO: add another error variant to canapi and add it there // TODO: add another error variant to canapi and add it there
return Err(ApiError::Authorization("A post with the same slug already exists".to_string())); return Err(ApiError::Authorization(
"A post with the same slug already exists".to_string(),
));
} }
let post = Post::insert(conn, NewPost { let post = Post::insert(
conn,
NewPost {
blog_id: blog, blog_id: blog,
slug, slug,
title, title,
content: SafeString::new(content.as_ref()), content: SafeString::new(content.as_ref()),
published: query.published.unwrap_or(true), published: query.published.unwrap_or(true),
license: query.license.unwrap_or_else(|| Instance::get_local(conn) license: query.license.unwrap_or_else(|| {
Instance::get_local(conn)
.map(|i| i.default_license) .map(|i| i.default_license)
.unwrap_or_else(|_| String::from("CC-BY-SA"))), .unwrap_or_else(|_| String::from("CC-BY-SA"))
}),
creation_date: date, creation_date: date,
ap_url: String::new(), ap_url: String::new(),
subtitle: query.subtitle.unwrap_or_default(), subtitle: query.subtitle.unwrap_or_default(),
source: query.source.expect("Post API::create: no source error"), source: query.source.expect("Post API::create: no source error"),
cover_id: query.cover_id, cover_id: query.cover_id,
}, search).map_err(|_| ApiError::NotFound("Creation error".into()))?; },
search,
)
.map_err(|_| ApiError::NotFound("Creation error".into()))?;
PostAuthor::insert(conn, NewPostAuthor { PostAuthor::insert(
conn,
NewPostAuthor {
author_id: author.id, author_id: author.id,
post_id: post.id post_id: post.id,
}).map_err(|_| ApiError::NotFound("Error saving authors".into()))?; },
)
.map_err(|_| ApiError::NotFound("Error saving authors".into()))?;
if let Some(tags) = query.tags { if let Some(tags) = query.tags {
for tag in tags { for tag in tags {
Tag::insert(conn, NewTag { Tag::insert(
conn,
NewTag {
tag, tag,
is_hashtag: false, is_hashtag: false,
post_id: post.id post_id: post.id,
}).map_err(|_| ApiError::NotFound("Error saving tags".into()))?; },
)
.map_err(|_| ApiError::NotFound("Error saving tags".into()))?;
} }
} }
for hashtag in hashtags { for hashtag in hashtags {
Tag::insert(conn, NewTag { Tag::insert(
conn,
NewTag {
tag: hashtag.to_camel_case(), tag: hashtag.to_camel_case(),
is_hashtag: true, is_hashtag: true,
post_id: post.id post_id: post.id,
}).map_err(|_| ApiError::NotFound("Error saving hashtags".into()))?; },
)
.map_err(|_| ApiError::NotFound("Error saving hashtags".into()))?;
} }
if post.published { if post.published {
for m in mentions.into_iter() { for m in mentions.into_iter() {
Mention::from_activity( Mention::from_activity(
&*conn, &*conn,
&Mention::build_activity(&*conn, &m).map_err(|_| ApiError::NotFound("Couldn't build mentions".into()))?, &Mention::build_activity(&*conn, &m)
.map_err(|_| ApiError::NotFound("Couldn't build mentions".into()))?,
post.id, post.id,
true, true,
true true,
).map_err(|_| ApiError::NotFound("Error saving mentions".into()))?; )
.map_err(|_| ApiError::NotFound("Error saving mentions".into()))?;
} }
let act = post.create_activity(&*conn).map_err(|_| ApiError::NotFound("Couldn't create activity".into()))?; let act = post
let dest = User::one_by_instance(&*conn).map_err(|_| ApiError::NotFound("Couldn't list remote instances".into()))?; .create_activity(&*conn)
.map_err(|_| ApiError::NotFound("Couldn't create activity".into()))?;
let dest = User::one_by_instance(&*conn)
.map_err(|_| ApiError::NotFound("Couldn't list remote instances".into()))?;
worker.execute(move || broadcast(&author, act, dest)); worker.execute(move || broadcast(&author, act, dest));
} }
@ -243,12 +318,23 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
subtitle: Some(post.subtitle.clone()), subtitle: Some(post.subtitle.clone()),
content: Some(post.content.get().clone()), content: Some(post.content.get().clone()),
source: Some(post.source.clone()), source: Some(post.source.clone()),
author: Some(post.get_authors(conn).map_err(|_| ApiError::NotFound("No authors".into()))?[0].username.clone()), author: Some(
post.get_authors(conn)
.map_err(|_| ApiError::NotFound("No authors".into()))?[0]
.username
.clone(),
),
blog_id: Some(post.blog_id), blog_id: Some(post.blog_id),
published: Some(post.published), published: Some(post.published),
creation_date: Some(post.creation_date.format("%Y-%m-%d").to_string()), creation_date: Some(post.creation_date.format("%Y-%m-%d").to_string()),
license: Some(post.license.clone()), license: Some(post.license.clone()),
tags: Some(Tag::for_post(conn, post.id).map_err(|_| ApiError::NotFound("Tags not found".into()))?.into_iter().map(|t| t.tag).collect()), tags: Some(
Tag::for_post(conn, post.id)
.map_err(|_| ApiError::NotFound("Tags not found".into()))?
.into_iter()
.map(|t| t.tag)
.collect(),
),
cover_id: post.cover_id, cover_id: post.cover_id,
}) })
} }
@ -279,15 +365,17 @@ impl Post {
Ok(post) Ok(post)
} }
pub fn update(&self, conn: &Connection, searcher: &Searcher) -> Result<Self> { pub fn update(&self, conn: &Connection, searcher: &Searcher) -> Result<Self> {
diesel::update(self) diesel::update(self).set(self).execute(conn)?;
.set(self)
.execute(conn)?;
let post = Self::get(conn, self.id)?; let post = Self::get(conn, self.id)?;
searcher.update_document(conn, &post)?; searcher.update_document(conn, &post)?;
Ok(post) Ok(post)
} }
pub fn list_by_tag(conn: &Connection, tag: String, (min, max): (i32, i32)) -> Result<Vec<Post>> { pub fn list_by_tag(
conn: &Connection,
tag: String,
(min, max): (i32, i32),
) -> Result<Vec<Post>> {
use schema::tags; use schema::tags;
let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id); let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id);
@ -349,7 +437,11 @@ impl Post {
.map_err(Error::from) .map_err(Error::from)
} }
pub fn get_recents_for_author(conn: &Connection, author: &User, limit: i64) -> Result<Vec<Post>> { pub fn get_recents_for_author(
conn: &Connection,
author: &User,
limit: i64,
) -> Result<Vec<Post>> {
use schema::post_authors; use schema::post_authors;
let posts = PostAuthor::belonging_to(author).select(post_authors::post_id); let posts = PostAuthor::belonging_to(author).select(post_authors::post_id);
@ -481,7 +573,8 @@ impl Post {
Ok(PostAuthor::belonging_to(self) Ok(PostAuthor::belonging_to(self)
.filter(post_authors::author_id.eq(author_id)) .filter(post_authors::author_id.eq(author_id))
.count() .count()
.get_result::<i64>(conn)? > 0) .get_result::<i64>(conn)?
> 0)
} }
pub fn get_blog(&self, conn: &Connection) -> Result<Blog> { pub fn get_blog(&self, conn: &Connection) -> Result<Blog> {
@ -542,12 +635,8 @@ impl Post {
mentions_json.append(&mut tags_json); mentions_json.append(&mut tags_json);
let mut article = Article::default(); let mut article = Article::default();
article article.object_props.set_name_string(self.title.clone())?;
.object_props article.object_props.set_id_string(self.ap_url.clone())?;
.set_name_string(self.title.clone())?;
article
.object_props
.set_id_string(self.ap_url.clone())?;
let mut authors = self let mut authors = self
.get_authors(conn)? .get_authors(conn)?
@ -561,9 +650,7 @@ impl Post {
article article
.object_props .object_props
.set_content_string(self.content.get().clone())?; .set_content_string(self.content.get().clone())?;
article article.ap_object_props.set_source_object(Source {
.ap_object_props
.set_source_object(Source {
content: self.source.clone(), content: self.source.clone(),
media_type: String::from("text/markdown"), media_type: String::from("text/markdown"),
})?; })?;
@ -578,31 +665,20 @@ impl Post {
if let Some(media_id) = self.cover_id { if let Some(media_id) = self.cover_id {
let media = Media::get(conn, media_id)?; let media = Media::get(conn, media_id)?;
let mut cover = Image::default(); let mut cover = Image::default();
cover cover.object_props.set_url_string(media.url(conn)?)?;
.object_props
.set_url_string(media.url(conn)?)?;
if media.sensitive { if media.sensitive {
cover cover
.object_props .object_props
.set_summary_string(media.content_warning.unwrap_or_default())?; .set_summary_string(media.content_warning.unwrap_or_default())?;
} }
cover.object_props.set_content_string(media.alt_text)?;
cover cover
.object_props .object_props
.set_content_string(media.alt_text)?; .set_attributed_to_link_vec(vec![User::get(conn, media.owner_id)?.into_id()])?;
cover article.object_props.set_icon_object(cover)?;
.object_props
.set_attributed_to_link_vec(vec![
User::get(conn, media.owner_id)?
.into_id(),
])?;
article
.object_props
.set_icon_object(cover)?;
} }
article article.object_props.set_url_string(self.ap_url.clone())?;
.object_props
.set_url_string(self.ap_url.clone())?;
article article
.object_props .object_props
.set_to_link_vec::<Id>(to.into_iter().map(Id::new).collect())?; .set_to_link_vec::<Id>(to.into_iter().map(Id::new).collect())?;
@ -620,52 +696,39 @@ impl Post {
act.object_props act.object_props
.set_id_string(format!("{}activity", self.ap_url))?; .set_id_string(format!("{}activity", self.ap_url))?;
act.object_props act.object_props
.set_to_link_vec::<Id>( .set_to_link_vec::<Id>(article.object.object_props.to_link_vec()?)?;
article.object
.object_props
.to_link_vec()?,
)?;
act.object_props act.object_props
.set_cc_link_vec::<Id>( .set_cc_link_vec::<Id>(article.object.object_props.cc_link_vec()?)?;
article.object
.object_props
.cc_link_vec()?,
)?;
act.create_props act.create_props
.set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?; .set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?;
act.create_props act.create_props.set_object_object(article)?;
.set_object_object(article)?;
Ok(act) Ok(act)
} }
pub fn update_activity(&self, conn: &Connection) -> Result<Update> { pub fn update_activity(&self, conn: &Connection) -> Result<Update> {
let article = self.to_activity(conn)?; let article = self.to_activity(conn)?;
let mut act = Update::default(); let mut act = Update::default();
act.object_props.set_id_string(format!(
"{}/update-{}",
self.ap_url,
Utc::now().timestamp()
))?;
act.object_props act.object_props
.set_id_string(format!("{}/update-{}", self.ap_url, Utc::now().timestamp()))?; .set_to_link_vec::<Id>(article.object.object_props.to_link_vec()?)?;
act.object_props act.object_props
.set_to_link_vec::<Id>( .set_cc_link_vec::<Id>(article.object.object_props.cc_link_vec()?)?;
article.object
.object_props
.to_link_vec()?,
)?;
act.object_props
.set_cc_link_vec::<Id>(
article.object
.object_props
.cc_link_vec()?,
)?;
act.update_props act.update_props
.set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?; .set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?;
act.update_props act.update_props.set_object_object(article)?;
.set_object_object(article)?;
Ok(act) Ok(act)
} }
pub fn handle_update(conn: &Connection, updated: &LicensedArticle, searcher: &Searcher) -> Result<()> { pub fn handle_update(
let id = updated.object conn: &Connection,
.object_props updated: &LicensedArticle,
.id_string()?; searcher: &Searcher,
) -> Result<()> {
let id = updated.object.object_props.id_string()?;
let mut post = Post::find_by_ap_url(conn, &id)?; let mut post = Post::find_by_ap_url(conn, &id)?;
if let Ok(title) = updated.object.object_props.name_string() { if let Ok(title) = updated.object.object_props.name_string() {
@ -698,7 +761,9 @@ impl Post {
.into_iter() .into_iter()
.map(|s| s.to_camel_case()) .map(|s| s.to_camel_case())
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
if let Some(serde_json::Value::Array(mention_tags)) = updated.object.object_props.tag.clone() { if let Some(serde_json::Value::Array(mention_tags)) =
updated.object.object_props.tag.clone()
{
let mut mentions = vec![]; let mut mentions = vec![];
let mut tags = vec![]; let mut tags = vec![];
let mut hashtags = vec![]; let mut hashtags = vec![];
@ -710,8 +775,7 @@ impl Post {
serde_json::from_value::<Hashtag>(tag.clone()) serde_json::from_value::<Hashtag>(tag.clone())
.map_err(Error::from) .map_err(Error::from)
.and_then(|t| { .and_then(|t| {
let tag_name = t let tag_name = t.name_string()?;
.name_string()?;
if txt_hashtags.remove(&tag_name) { if txt_hashtags.remove(&tag_name) {
hashtags.push(t); hashtags.push(t);
} else { } else {
@ -854,20 +918,25 @@ impl Post {
} }
pub fn cover_url(&self, conn: &Connection) -> Option<String> { pub fn cover_url(&self, conn: &Connection) -> Option<String> {
self.cover_id.and_then(|i| Media::get(conn, i).ok()).and_then(|c| c.url(conn).ok()) self.cover_id
.and_then(|i| Media::get(conn, i).ok())
.and_then(|c| c.url(conn).ok())
} }
} }
impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for Post { impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for Post {
type Error = Error; type Error = Error;
fn from_activity((conn, searcher): &(&'a Connection, &'a Searcher), article: LicensedArticle, _actor: Id) -> Result<Post> { fn from_activity(
(conn, searcher): &(&'a Connection, &'a Searcher),
article: LicensedArticle,
_actor: Id,
) -> Result<Post> {
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;
if let Ok(post) = Post::find_by_ap_url( if let Ok(post) =
conn, Post::find_by_ap_url(conn, &article.object_props.id_string().unwrap_or_default())
&article.object_props.id_string().unwrap_or_default(), {
) {
Ok(post) Ok(post)
} else { } else {
let (blog, authors) = article let (blog, authors) = article
@ -880,10 +949,8 @@ impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for Post
Ok(u) => { Ok(u) => {
authors.push(u); authors.push(u);
(blog, authors) (blog, authors)
}, }
Err(_) => { Err(_) => (blog.or_else(|| Blog::from_url(conn, &url).ok()), authors),
(blog.or_else(|| Blog::from_url(conn, &url).ok()), authors)
},
} }
}); });
@ -893,41 +960,24 @@ impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for Post
.ok() .ok()
.and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id)); .and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id));
let title = article let title = article.object_props.name_string()?;
.object_props
.name_string()?;
let post = Post::insert( let post = Post::insert(
conn, conn,
NewPost { NewPost {
blog_id: blog?.id, blog_id: blog?.id,
slug: title.to_kebab_case(), slug: title.to_kebab_case(),
title, title,
content: SafeString::new( content: SafeString::new(&article.object_props.content_string()?),
&article
.object_props
.content_string()?,
),
published: true, published: true,
license, license,
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields // FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
ap_url: article.object_props.url_string().or_else(|_| ap_url: article
article
.object_props .object_props
.id_string() .url_string()
)?, .or_else(|_| article.object_props.id_string())?,
creation_date: Some( creation_date: Some(article.object_props.published_utctime()?.naive_utc()),
article subtitle: article.object_props.summary_string()?,
.object_props source: article.ap_object_props.source_object::<Source>()?.content,
.published_utctime()?
.naive_utc(),
),
subtitle: article
.object_props
.summary_string()?,
source: article
.ap_object_props
.source_object::<Source>()?
.content,
cover_id: cover, cover_id: cover,
}, },
searcher, searcher,
@ -959,7 +1009,12 @@ impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for Post
.map_err(Error::from) .map_err(Error::from)
.and_then(|t| { .and_then(|t| {
let tag_name = t.name_string()?; let tag_name = t.name_string()?;
Ok(Tag::from_activity(conn, &t, post.id, hashtags.remove(&tag_name))) Ok(Tag::from_activity(
conn,
&t,
post.id,
hashtags.remove(&tag_name),
))
}) })
.ok(); .ok();
} }
@ -978,11 +1033,8 @@ impl<'a> Deletable<(&'a Connection, &'a Searcher), Delete> for Post {
.set_actor_link(self.get_authors(conn)?[0].clone().into_id())?; .set_actor_link(self.get_authors(conn)?[0].clone().into_id())?;
let mut tombstone = Tombstone::default(); let mut tombstone = Tombstone::default();
tombstone tombstone.object_props.set_id_string(self.ap_url.clone())?;
.object_props act.delete_props.set_object_object(tombstone)?;
.set_id_string(self.ap_url.clone())?;
act.delete_props
.set_object_object(tombstone)?;
act.object_props act.object_props
.set_id_string(format!("{}#delete", self.ap_url))?; .set_id_string(format!("{}#delete", self.ap_url))?;
@ -992,16 +1044,22 @@ impl<'a> Deletable<(&'a Connection, &'a Searcher), Delete> for Post {
for m in Mention::list_for_post(&conn, self.id)? { for m in Mention::list_for_post(&conn, self.id)? {
m.delete(conn)?; m.delete(conn)?;
} }
diesel::delete(self) diesel::delete(self).execute(*conn)?;
.execute(*conn)?;
searcher.delete_document(self); searcher.delete_document(self);
Ok(act) Ok(act)
} }
fn delete_id(id: &str, actor_id: &str, (conn, searcher): &(&Connection, &Searcher)) -> Result<Delete> { fn delete_id(
id: &str,
actor_id: &str,
(conn, searcher): &(&Connection, &Searcher),
) -> Result<Delete> {
let actor = User::find_by_ap_url(conn, actor_id)?; let actor = User::find_by_ap_url(conn, actor_id)?;
let post = Post::find_by_ap_url(conn, id)?; let post = Post::find_by_ap_url(conn, id)?;
let can_delete = post.get_authors(conn)?.into_iter().any(|a| actor.id == a.id); let can_delete = post
.get_authors(conn)?
.into_iter()
.any(|a| actor.id == a.id);
if can_delete { if can_delete {
post.delete(&(conn, searcher)) post.delete(&(conn, searcher))
} else { } else {

View File

@ -40,7 +40,11 @@ impl Reshare {
post_id as i32 post_id as i32
); );
pub fn get_recents_for_author(conn: &Connection, user: &User, limit: i64) -> Result<Vec<Reshare>> { pub fn get_recents_for_author(
conn: &Connection,
user: &User,
limit: i64,
) -> Result<Vec<Reshare>> {
reshares::table reshares::table
.filter(reshares::user_id.eq(user.id)) .filter(reshares::user_id.eq(user.id))
.order(reshares::creation_date.desc()) .order(reshares::creation_date.desc())
@ -63,12 +67,10 @@ impl Reshare {
.set_actor_link(User::get(conn, self.user_id)?.into_id())?; .set_actor_link(User::get(conn, self.user_id)?.into_id())?;
act.announce_props act.announce_props
.set_object_link(Post::get(conn, self.post_id)?.into_id())?; .set_object_link(Post::get(conn, self.post_id)?.into_id())?;
act.object_props act.object_props.set_id_string(self.ap_url.clone())?;
.set_id_string(self.ap_url.clone())?;
act.object_props act.object_props
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?; .set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
act.object_props act.object_props.set_cc_link_vec::<Id>(vec![])?;
.set_cc_link_vec::<Id>(vec![])?;
Ok(act) Ok(act)
} }
@ -78,29 +80,15 @@ impl FromActivity<Announce, Connection> for Reshare {
type Error = Error; type Error = Error;
fn from_activity(conn: &Connection, announce: Announce, _actor: Id) -> Result<Reshare> { fn from_activity(conn: &Connection, announce: Announce, _actor: Id) -> Result<Reshare> {
let user = User::from_url( let user = User::from_url(conn, announce.announce_props.actor_link::<Id>()?.as_ref())?;
conn, let post =
announce Post::find_by_ap_url(conn, announce.announce_props.object_link::<Id>()?.as_ref())?;
.announce_props
.actor_link::<Id>()?
.as_ref(),
)?;
let post = Post::find_by_ap_url(
conn,
announce
.announce_props
.object_link::<Id>()?
.as_ref(),
)?;
let reshare = Reshare::insert( let reshare = Reshare::insert(
conn, conn,
NewReshare { NewReshare {
post_id: post.id, post_id: post.id,
user_id: user.id, user_id: user.id,
ap_url: announce ap_url: announce.object_props.id_string().unwrap_or_default(),
.object_props
.id_string()
.unwrap_or_default(),
}, },
)?; )?;
reshare.notify(conn)?; reshare.notify(conn)?;
@ -131,26 +119,22 @@ impl Deletable<Connection, Undo> for Reshare {
type Error = Error; type Error = Error;
fn delete(&self, conn: &Connection) -> Result<Undo> { fn delete(&self, conn: &Connection) -> Result<Undo> {
diesel::delete(self) diesel::delete(self).execute(conn)?;
.execute(conn)?;
// delete associated notification if any // delete associated notification if any
if let Ok(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) { if let Ok(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) {
diesel::delete(&notif) diesel::delete(&notif).execute(conn)?;
.execute(conn)?;
} }
let mut act = Undo::default(); let mut act = Undo::default();
act.undo_props act.undo_props
.set_actor_link(User::get(conn, self.user_id)?.into_id())?; .set_actor_link(User::get(conn, self.user_id)?.into_id())?;
act.undo_props act.undo_props.set_object_object(self.to_activity(conn)?)?;
.set_object_object(self.to_activity(conn)?)?;
act.object_props act.object_props
.set_id_string(format!("{}#delete", self.ap_url))?; .set_id_string(format!("{}#delete", self.ap_url))?;
act.object_props act.object_props
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?; .set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
act.object_props act.object_props.set_cc_link_vec::<Id>(vec![])?;
.set_cc_link_vec::<Id>(vec![])?;
Ok(act) Ok(act)
} }
@ -172,7 +156,7 @@ impl NewReshare {
NewReshare { NewReshare {
post_id: p.id, post_id: p.id,
user_id: u.id, user_id: u.id,
ap_url ap_url,
} }
} }
} }

View File

@ -19,21 +19,15 @@ lazy_static! {
static ref CLEAN: Builder<'static> = { static ref CLEAN: Builder<'static> = {
let mut b = Builder::new(); let mut b = Builder::new();
b.add_generic_attributes(iter::once("id")) b.add_generic_attributes(iter::once("id"))
.add_tags(&[ "iframe", "video", "audio" ]) .add_tags(&["iframe", "video", "audio"])
.id_prefix(Some("postcontent-")) .id_prefix(Some("postcontent-"))
.url_relative(UrlRelative::Custom(Box::new(url_add_prefix))) .url_relative(UrlRelative::Custom(Box::new(url_add_prefix)))
.add_tag_attributes( .add_tag_attributes(
"iframe", "iframe",
[ "width", "height", "src", "frameborder" ].iter().cloned(), ["width", "height", "src", "frameborder"].iter().cloned(),
) )
.add_tag_attributes( .add_tag_attributes("video", ["src", "title", "controls"].iter())
"video", .add_tag_attributes("audio", ["src", "title", "controls"].iter());
[ "src", "title", "controls" ].iter(),
)
.add_tag_attributes(
"audio",
[ "src", "title", "controls" ].iter(),
);
b b
}; };
} }
@ -69,7 +63,7 @@ impl SafeString {
/// Prefer `SafeString::new` as much as possible. /// Prefer `SafeString::new` as much as possible.
pub fn trusted(value: impl AsRef<str>) -> Self { pub fn trusted(value: impl AsRef<str>) -> Self {
SafeString { SafeString {
value: value.as_ref().to_string() value: value.as_ref().to_string(),
} }
} }

View File

@ -1,33 +1,32 @@
mod searcher;
mod query; mod query;
mod searcher;
mod tokenizer; mod tokenizer;
pub use self::searcher::*;
pub use self::query::PlumeQuery as Query; pub use self::query::PlumeQuery as Query;
pub use self::searcher::*;
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use super::{Query, Searcher}; use super::{Query, Searcher};
use diesel::Connection;
use std::env::temp_dir; use std::env::temp_dir;
use std::str::FromStr; use std::str::FromStr;
use diesel::Connection;
use blogs::tests::fill_database;
use plume_common::activity_pub::inbox::Deletable; use plume_common::activity_pub::inbox::Deletable;
use plume_common::utils::random_hex; use plume_common::utils::random_hex;
use blogs::tests::fill_database;
use posts::{NewPost, Post};
use post_authors::*; use post_authors::*;
use posts::{NewPost, Post};
use safe_string::SafeString; use safe_string::SafeString;
use tests::db; use tests::db;
pub(crate) fn get_searcher() -> Searcher { pub(crate) fn get_searcher() -> Searcher {
let dir = temp_dir().join("plume-test"); let dir = temp_dir().join("plume-test");
if dir.exists() { if dir.exists() {
Searcher::open(&dir) Searcher::open(&dir)
} else { } else {
Searcher::create(&dir) Searcher::create(&dir)
}.unwrap() }
.unwrap()
} }
#[test] #[test]
@ -98,7 +97,9 @@ pub(crate) mod tests {
#[test] #[test]
fn open() { fn open() {
{get_searcher()};//make sure $tmp/plume-test-tantivy exist {
get_searcher()
}; //make sure $tmp/plume-test-tantivy exist
let dir = temp_dir().join("plume-test"); let dir = temp_dir().join("plume-test");
Searcher::open(&dir).unwrap(); Searcher::open(&dir).unwrap();
@ -109,8 +110,10 @@ pub(crate) mod tests {
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).is_err());
{Searcher::create(&dir).unwrap();} {
Searcher::open(&dir).unwrap();//verify it's well created Searcher::create(&dir).unwrap();
}
Searcher::open(&dir).unwrap(); //verify it's well created
} }
#[test] #[test]
@ -123,7 +126,9 @@ pub(crate) mod tests {
let title = random_hex()[..8].to_owned(); let title = random_hex()[..8].to_owned();
let mut post = Post::insert(conn, NewPost { let mut post = Post::insert(
conn,
NewPost {
blog_id: blog.id, blog_id: blog.id,
slug: title.clone(), slug: title.clone(),
title: title.clone(), title: title.clone(),
@ -135,25 +140,42 @@ pub(crate) mod tests {
subtitle: "".to_owned(), subtitle: "".to_owned(),
source: "".to_owned(), source: "".to_owned(),
cover_id: None, cover_id: None,
}, &searcher).unwrap(); },
PostAuthor::insert(conn, NewPostAuthor { &searcher,
)
.unwrap();
PostAuthor::insert(
conn,
NewPostAuthor {
post_id: post.id, post_id: post.id,
author_id: author.id, author_id: author.id,
}).unwrap(); },
)
.unwrap();
searcher.commit(); searcher.commit();
assert_eq!(searcher.search_document(conn, Query::from_str(&title).unwrap(), (0,1))[0].id, post.id); assert_eq!(
searcher.search_document(conn, Query::from_str(&title).unwrap(), (0, 1))[0].id,
post.id
);
let newtitle = random_hex()[..8].to_owned(); let newtitle = random_hex()[..8].to_owned();
post.title = newtitle.clone(); post.title = newtitle.clone();
post.update(conn, &searcher).unwrap(); post.update(conn, &searcher).unwrap();
searcher.commit(); searcher.commit();
assert_eq!(searcher.search_document(conn, Query::from_str(&newtitle).unwrap(), (0,1))[0].id, post.id); assert_eq!(
assert!(searcher.search_document(conn, Query::from_str(&title).unwrap(), (0,1)).is_empty()); searcher.search_document(conn, Query::from_str(&newtitle).unwrap(), (0, 1))[0].id,
post.id
);
assert!(searcher
.search_document(conn, Query::from_str(&title).unwrap(), (0, 1))
.is_empty());
post.delete(&(conn, &searcher)).unwrap(); post.delete(&(conn, &searcher)).unwrap();
searcher.commit(); searcher.commit();
assert!(searcher.search_document(conn, Query::from_str(&newtitle).unwrap(), (0,1)).is_empty()); assert!(searcher
.search_document(conn, Query::from_str(&newtitle).unwrap(), (0, 1))
.is_empty());
Ok(()) Ok(())
}); });

View File

@ -1,8 +1,7 @@
use chrono::{Datelike, naive::NaiveDate, offset::Utc}; use chrono::{naive::NaiveDate, offset::Utc, Datelike};
use tantivy::{query::*, schema::*, Term};
use std::{cmp,ops::Bound};
use search::searcher::Searcher; use search::searcher::Searcher;
use std::{cmp, ops::Bound};
use tantivy::{query::*, schema::*, Term};
//Generate functions for advanced search //Generate functions for advanced search
macro_rules! gen_func { macro_rules! gen_func {
@ -142,13 +141,11 @@ pub struct PlumeQuery {
} }
impl PlumeQuery { impl PlumeQuery {
/// Create a new empty Query /// Create a new empty Query
pub fn new() -> Self { pub fn new() -> Self {
Default::default() Default::default()
} }
/// Parse a query string into this Query /// Parse a query string into this Query
pub fn parse_query(&mut self, query: &str) -> &mut Self { pub fn parse_query(&mut self, query: &str) -> &mut Self {
self.from_str_req(&query.trim()) self.from_str_req(&query.trim())
@ -160,9 +157,11 @@ impl PlumeQuery {
gen_to_query!(self, result; normal: title, subtitle, content, tag; gen_to_query!(self, result; normal: title, subtitle, content, tag;
oneoff: instance, author, blog, lang, license); oneoff: instance, author, blog, lang, license);
for (occur, token) in self.text { // text entries need to be added as multiple Terms for (occur, token) in self.text {
// text entries need to be added as multiple Terms
match occur { match occur {
Occur::Must => { // a Must mean this must be in one of title subtitle or content, not in all 3 Occur::Must => {
// a Must mean this must be in one of title subtitle or content, not in all 3
let subresult = vec![ let subresult = vec![
(Occur::Should, Self::token_to_query(&token, "title")), (Occur::Should, Self::token_to_query(&token, "title")),
(Occur::Should, Self::token_to_query(&token, "subtitle")), (Occur::Should, Self::token_to_query(&token, "subtitle")),
@ -170,20 +169,26 @@ impl PlumeQuery {
]; ];
result.push((Occur::Must, Box::new(BooleanQuery::from(subresult)))); result.push((Occur::Must, Box::new(BooleanQuery::from(subresult))));
}, }
occur => { occur => {
result.push((occur, Self::token_to_query(&token, "title"))); result.push((occur, Self::token_to_query(&token, "title")));
result.push((occur, Self::token_to_query(&token, "subtitle"))); result.push((occur, Self::token_to_query(&token, "subtitle")));
result.push((occur, Self::token_to_query(&token, "content"))); result.push((occur, Self::token_to_query(&token, "content")));
}, }
} }
} }
if self.before.is_some() || self.after.is_some() { // if at least one range bound is provided if self.before.is_some() || self.after.is_some() {
let after = self.after.unwrap_or_else(|| i64::from(NaiveDate::from_ymd(2000, 1, 1).num_days_from_ce())); // if at least one range bound is provided
let before = self.before.unwrap_or_else(|| i64::from(Utc::today().num_days_from_ce())); let after = self
.after
.unwrap_or_else(|| i64::from(NaiveDate::from_ymd(2000, 1, 1).num_days_from_ce()));
let before = self
.before
.unwrap_or_else(|| i64::from(Utc::today().num_days_from_ce()));
let field = Searcher::schema().get_field("creation_date").unwrap(); let field = Searcher::schema().get_field("creation_date").unwrap();
let range = RangeQuery::new_i64_bounds(field, Bound::Included(after), Bound::Included(before)); let range =
RangeQuery::new_i64_bounds(field, Bound::Included(after), Bound::Included(before));
result.push((Occur::Must, Box::new(range))); result.push((Occur::Must, Box::new(range)));
} }
@ -195,14 +200,18 @@ impl PlumeQuery {
// documents newer than the provided date will be ignored // documents newer than the provided date will be ignored
pub fn before<D: Datelike>(&mut self, date: &D) -> &mut Self { pub fn before<D: Datelike>(&mut self, date: &D) -> &mut Self {
let before = self.before.unwrap_or_else(|| i64::from(Utc::today().num_days_from_ce())); let before = self
.before
.unwrap_or_else(|| i64::from(Utc::today().num_days_from_ce()));
self.before = Some(cmp::min(before, i64::from(date.num_days_from_ce()))); self.before = Some(cmp::min(before, i64::from(date.num_days_from_ce())));
self self
} }
// documents older than the provided date will be ignored // documents older than the provided date will be ignored
pub fn after<D: Datelike>(&mut self, date: &D) -> &mut Self { pub fn after<D: Datelike>(&mut self, date: &D) -> &mut Self {
let after = self.after.unwrap_or_else(|| i64::from(NaiveDate::from_ymd(2000, 1, 1).num_days_from_ce())); let after = self
.after
.unwrap_or_else(|| i64::from(NaiveDate::from_ymd(2000, 1, 1).num_days_from_ce()));
self.after = Some(cmp::max(after, i64::from(date.num_days_from_ce()))); self.after = Some(cmp::max(after, i64::from(date.num_days_from_ce())));
self self
} }
@ -212,15 +221,19 @@ impl PlumeQuery {
query = query.trim(); query = query.trim();
if query.is_empty() { if query.is_empty() {
("", "") ("", "")
} else if query.get(0..1).map(|v| v=="\"").unwrap_or(false) { } else if query.get(0..1).map(|v| v == "\"").unwrap_or(false) {
if let Some(index) = query[1..].find('"') { if let Some(index) = query[1..].find('"') {
query.split_at(index+2) query.split_at(index + 2)
} else { } else {
(query, "") (query, "")
} }
} else if query.get(0..2).map(|v| v=="+\"" || v=="-\"").unwrap_or(false) { } else if query
.get(0..2)
.map(|v| v == "+\"" || v == "-\"")
.unwrap_or(false)
{
if let Some(index) = query[2..].find('"') { if let Some(index) = query[2..].find('"') {
query.split_at(index+3) query.split_at(index + 3)
} else { } else {
(query, "") (query, "")
} }
@ -247,13 +260,13 @@ impl PlumeQuery {
fn from_str_req(&mut self, mut query: &str) -> &mut Self { fn from_str_req(&mut self, mut query: &str) -> &mut Self {
query = query.trim_left(); query = query.trim_left();
if query.is_empty() { if query.is_empty() {
return self return self;
} }
let occur = if query.get(0..1).map(|v| v=="+").unwrap_or(false) { let occur = if query.get(0..1).map(|v| v == "+").unwrap_or(false) {
query = &query[1..]; query = &query[1..];
Occur::Must Occur::Must
} else if query.get(0..1).map(|v| v=="-").unwrap_or(false) { } else if query.get(0..1).map(|v| v == "-").unwrap_or(false) {
query = &query[1..]; query = &query[1..];
Occur::MustNot Occur::MustNot
} else { } else {
@ -270,31 +283,59 @@ impl PlumeQuery {
let token = token.to_lowercase(); let token = token.to_lowercase();
let token = token.as_str(); let token = token.as_str();
let field = Searcher::schema().get_field(field_name).unwrap(); let field = Searcher::schema().get_field(field_name).unwrap();
if token.contains('@') && (field_name=="author" || field_name=="blog") { if token.contains('@') && (field_name == "author" || field_name == "blog") {
let pos = token.find('@').unwrap(); let pos = token.find('@').unwrap();
let user_term = Term::from_field_text(field, &token[..pos]); let user_term = Term::from_field_text(field, &token[..pos]);
let instance_term = Term::from_field_text(Searcher::schema().get_field("instance").unwrap(), &token[pos+1..]); let instance_term = Term::from_field_text(
Searcher::schema().get_field("instance").unwrap(),
&token[pos + 1..],
);
Box::new(BooleanQuery::from(vec![ Box::new(BooleanQuery::from(vec![
(Occur::Must, Box::new(TermQuery::new(user_term, if field_name=="author" { IndexRecordOption::Basic } (
else { IndexRecordOption::WithFreqsAndPositions } Occur::Must,
)) as Box<dyn Query + 'static>), Box::new(TermQuery::new(
(Occur::Must, Box::new(TermQuery::new(instance_term, IndexRecordOption::Basic))), user_term,
if field_name == "author" {
IndexRecordOption::Basic
} else {
IndexRecordOption::WithFreqsAndPositions
},
)) as Box<dyn Query + 'static>,
),
(
Occur::Must,
Box::new(TermQuery::new(instance_term, IndexRecordOption::Basic)),
),
])) ]))
} else if token.contains(' ') { // phrase query } else if token.contains(' ') {
// phrase query
match field_name { match field_name {
"instance" | "author" | "tag" => // phrase query are not available on these fields, treat it as multiple Term queries "instance" | "author" | "tag" =>
Box::new(BooleanQuery::from(token.split_whitespace() // phrase query are not available on these fields, treat it as multiple Term queries
{
Box::new(BooleanQuery::from(
token
.split_whitespace()
.map(|token| { .map(|token| {
let term = Term::from_field_text(field, token); let term = Term::from_field_text(field, token);
(Occur::Should, Box::new(TermQuery::new(term, IndexRecordOption::Basic)) (
as Box<dyn Query + 'static>) Occur::Should,
Box::new(TermQuery::new(term, IndexRecordOption::Basic))
as Box<dyn Query + 'static>,
)
}) })
.collect::<Vec<_>>())), .collect::<Vec<_>>(),
_ => Box::new(PhraseQuery::new(token.split_whitespace() ))
.map(|token| Term::from_field_text(field, token))
.collect()))
} }
} else { // Term Query _ => Box::new(PhraseQuery::new(
token
.split_whitespace()
.map(|token| Term::from_field_text(field, token))
.collect(),
)),
}
} else {
// Term Query
let term = Term::from_field_text(field, token); let term = Term::from_field_text(field, token);
let index_option = match field_name { let index_option = match field_name {
"instance" | "author" | "tag" => IndexRecordOption::Basic, "instance" | "author" | "tag" => IndexRecordOption::Basic,
@ -306,7 +347,6 @@ impl PlumeQuery {
} }
impl std::str::FromStr for PlumeQuery { impl std::str::FromStr for PlumeQuery {
type Err = !; type Err = !;
/// Create a new Query from &str /// Create a new Query from &str
@ -340,7 +380,7 @@ impl ToString for PlumeQuery {
instance, author, blog, lang, license; instance, author, blog, lang, license;
date: before, after); date: before, after);
result.pop();// remove trailing ' ' result.pop(); // remove trailing ' '
result result
} }
} }

View File

@ -5,15 +5,14 @@ use Connection;
use chrono::Datelike; use chrono::Datelike;
use itertools::Itertools; use itertools::Itertools;
use std::{cmp, fs::create_dir_all, path::Path, sync::Mutex};
use tantivy::{ use tantivy::{
collector::TopDocs, directory::MmapDirectory, collector::TopDocs, directory::MmapDirectory, schema::*, tokenizer::*, Index, IndexWriter, Term,
schema::*, tokenizer::*, Index, IndexWriter, Term
}; };
use whatlang::{detect as detect_lang, Lang}; use whatlang::{detect as detect_lang, Lang};
use std::{cmp, fs::create_dir_all, path::Path, sync::Mutex};
use search::query::PlumeQuery;
use super::tokenizer; use super::tokenizer;
use search::query::PlumeQuery;
use Result; use Result;
#[derive(Debug)] #[derive(Debug)]
@ -31,20 +30,23 @@ pub struct Searcher {
impl Searcher { impl Searcher {
pub fn schema() -> Schema { pub fn schema() -> Schema {
let tag_indexing = TextOptions::default() let tag_indexing = TextOptions::default().set_indexing_options(
.set_indexing_options(TextFieldIndexing::default() TextFieldIndexing::default()
.set_tokenizer("whitespace_tokenizer") .set_tokenizer("whitespace_tokenizer")
.set_index_option(IndexRecordOption::Basic)); .set_index_option(IndexRecordOption::Basic),
);
let content_indexing = TextOptions::default() let content_indexing = TextOptions::default().set_indexing_options(
.set_indexing_options(TextFieldIndexing::default() TextFieldIndexing::default()
.set_tokenizer("content_tokenizer") .set_tokenizer("content_tokenizer")
.set_index_option(IndexRecordOption::WithFreqsAndPositions)); .set_index_option(IndexRecordOption::WithFreqsAndPositions),
);
let property_indexing = TextOptions::default() let property_indexing = TextOptions::default().set_indexing_options(
.set_indexing_options(TextFieldIndexing::default() TextFieldIndexing::default()
.set_tokenizer("property_tokenizer") .set_tokenizer("property_tokenizer")
.set_index_option(IndexRecordOption::WithFreqsAndPositions)); .set_index_option(IndexRecordOption::WithFreqsAndPositions),
);
let mut schema_builder = SchemaBuilder::default(); let mut schema_builder = SchemaBuilder::default();
@ -66,56 +68,65 @@ impl Searcher {
schema_builder.build() schema_builder.build()
} }
pub fn create(path: &AsRef<Path>) -> Result<Self> { pub fn create(path: &AsRef<Path>) -> Result<Self> {
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer let whitespace_tokenizer = tokenizer::WhitespaceTokenizer.filter(LowerCaser);
.filter(LowerCaser);
let content_tokenizer = SimpleTokenizer let content_tokenizer = SimpleTokenizer
.filter(RemoveLongFilter::limit(40)) .filter(RemoveLongFilter::limit(40))
.filter(LowerCaser); .filter(LowerCaser);
let property_tokenizer = NgramTokenizer::new(2, 8, false) let property_tokenizer = NgramTokenizer::new(2, 8, false).filter(LowerCaser);
.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)?;
let index = Index::create(MmapDirectory::open(path).map_err(|_| SearcherError::IndexCreationError)?, schema).map_err(|_| SearcherError::IndexCreationError)?; let index = Index::create(
MmapDirectory::open(path).map_err(|_| SearcherError::IndexCreationError)?,
schema,
)
.map_err(|_| SearcherError::IndexCreationError)?;
{ {
let tokenizer_manager = index.tokenizers(); let tokenizer_manager = index.tokenizers();
tokenizer_manager.register("whitespace_tokenizer", whitespace_tokenizer); tokenizer_manager.register("whitespace_tokenizer", whitespace_tokenizer);
tokenizer_manager.register("content_tokenizer", content_tokenizer); tokenizer_manager.register("content_tokenizer", content_tokenizer);
tokenizer_manager.register("property_tokenizer", property_tokenizer); tokenizer_manager.register("property_tokenizer", property_tokenizer);
}//to please the borrow checker } //to please the borrow checker
Ok(Self { Ok(Self {
writer: Mutex::new(Some(index.writer(50_000_000).map_err(|_| SearcherError::WriteLockAcquisitionError)?)), writer: Mutex::new(Some(
index index
.writer(50_000_000)
.map_err(|_| SearcherError::WriteLockAcquisitionError)?,
)),
index,
}) })
} }
pub fn open(path: &AsRef<Path>) -> Result<Self> { pub fn open(path: &AsRef<Path>) -> Result<Self> {
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer let whitespace_tokenizer = tokenizer::WhitespaceTokenizer.filter(LowerCaser);
.filter(LowerCaser);
let content_tokenizer = SimpleTokenizer let content_tokenizer = SimpleTokenizer
.filter(RemoveLongFilter::limit(40)) .filter(RemoveLongFilter::limit(40))
.filter(LowerCaser); .filter(LowerCaser);
let property_tokenizer = NgramTokenizer::new(2, 8, false) let property_tokenizer = NgramTokenizer::new(2, 8, false).filter(LowerCaser);
.filter(LowerCaser);
let index = Index::open(MmapDirectory::open(path).map_err(|_| SearcherError::IndexOpeningError)?).map_err(|_| SearcherError::IndexOpeningError)?; let index =
Index::open(MmapDirectory::open(path).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("whitespace_tokenizer", whitespace_tokenizer);
tokenizer_manager.register("content_tokenizer", content_tokenizer); tokenizer_manager.register("content_tokenizer", content_tokenizer);
tokenizer_manager.register("property_tokenizer", property_tokenizer); tokenizer_manager.register("property_tokenizer", property_tokenizer);
}//to please the borrow checker } //to please the borrow checker
let mut writer = index.writer(50_000_000).map_err(|_| SearcherError::WriteLockAcquisitionError)?; let mut writer = index
writer.garbage_collect_files().map_err(|_| SearcherError::IndexEditionError)?; .writer(50_000_000)
.map_err(|_| SearcherError::WriteLockAcquisitionError)?;
writer
.garbage_collect_files()
.map_err(|_| SearcherError::IndexEditionError)?;
Ok(Self { Ok(Self {
writer: Mutex::new(Some(writer)), writer: Mutex::new(Some(writer)),
index, index,
@ -173,18 +184,24 @@ impl Searcher {
self.add_document(conn, post) self.add_document(conn, post)
} }
pub fn search_document(&self, conn: &Connection, query: PlumeQuery, (min, max): (i32, i32)) -> Vec<Post>{ pub fn search_document(
&self,
conn: &Connection,
query: PlumeQuery,
(min, max): (i32, i32),
) -> Vec<Post> {
let schema = self.index.schema(); let schema = self.index.schema();
let post_id = schema.get_field("post_id").unwrap(); let post_id = schema.get_field("post_id").unwrap();
let collector = TopDocs::with_limit(cmp::max(1,max) as usize); let collector = TopDocs::with_limit(cmp::max(1, max) as usize);
let searcher = self.index.searcher(); let searcher = self.index.searcher();
let res = searcher.search(&query.into_query(), &collector).unwrap(); let res = searcher.search(&query.into_query(), &collector).unwrap();
res.get(min as usize..).unwrap_or(&[]) res.get(min as usize..)
.unwrap_or(&[])
.iter() .iter()
.filter_map(|(_,doc_add)| { .filter_map(|(_, doc_add)| {
let doc = searcher.doc(*doc_add).ok()?; let doc = searcher.doc(*doc_add).ok()?;
let id = doc.get_first(post_id)?; let id = doc.get_first(post_id)?;
Post::get(conn, id.i64_value() as i32).ok() Post::get(conn, id.i64_value() as i32).ok()

View File

@ -38,7 +38,12 @@ impl Tag {
Ok(ht) Ok(ht)
} }
pub fn from_activity(conn: &Connection, tag: &Hashtag, post: i32, is_hashtag: bool) -> Result<Tag> { pub fn from_activity(
conn: &Connection,
tag: &Hashtag,
post: i32,
is_hashtag: bool,
) -> Result<Tag> {
Tag::insert( Tag::insert(
conn, conn,
NewTag { NewTag {

View File

@ -1,6 +1,5 @@
use activitypub::{ use activitypub::{
actor::Person, collection::OrderedCollection, object::Image, Activity, CustomObject, actor::Person, collection::OrderedCollection, object::Image, Activity, CustomObject, Endpoint,
Endpoint,
}; };
use bcrypt; use bcrypt;
use chrono::{NaiveDateTime, Utc}; use chrono::{NaiveDateTime, Utc};
@ -27,7 +26,10 @@ use rocket::{
request::{self, FromRequest, Request}, request::{self, FromRequest, Request},
}; };
use serde_json; use serde_json;
use std::{cmp::PartialEq, hash::{Hash, Hasher}}; use std::{
cmp::PartialEq,
hash::{Hash, Hasher},
};
use url::Url; use url::Url;
use webfinger::*; use webfinger::*;
@ -41,7 +43,7 @@ use posts::Post;
use safe_string::SafeString; use safe_string::SafeString;
use schema::users; use schema::users;
use search::Searcher; use search::Searcher;
use {ap_url, Connection, BASE_URL, USE_HTTPS, Error, Result}; use {ap_url, Connection, Error, Result, BASE_URL, USE_HTTPS};
pub type CustomPerson = CustomObject<ApSignature, Person>; pub type CustomPerson = CustomObject<ApSignature, Person>;
@ -97,42 +99,24 @@ impl User {
insert!(users, NewUser, |inserted, conn| { insert!(users, NewUser, |inserted, conn| {
let instance = inserted.get_instance(conn)?; let instance = inserted.get_instance(conn)?;
if inserted.outbox_url.is_empty() { if inserted.outbox_url.is_empty() {
inserted.outbox_url = instance.compute_box( inserted.outbox_url = instance.compute_box(USER_PREFIX, &inserted.username, "outbox");
USER_PREFIX,
&inserted.username,
"outbox",
);
} }
if inserted.inbox_url.is_empty() { if inserted.inbox_url.is_empty() {
inserted.inbox_url = instance.compute_box( inserted.inbox_url = instance.compute_box(USER_PREFIX, &inserted.username, "inbox");
USER_PREFIX,
&inserted.username,
"inbox",
);
} }
if inserted.ap_url.is_empty() { if inserted.ap_url.is_empty() {
inserted.ap_url = instance.compute_box( inserted.ap_url = instance.compute_box(USER_PREFIX, &inserted.username, "");
USER_PREFIX,
&inserted.username,
"",
);
} }
if inserted.shared_inbox_url.is_none() { if inserted.shared_inbox_url.is_none() {
inserted.shared_inbox_url = Some(ap_url(&format!( inserted.shared_inbox_url = Some(ap_url(&format!("{}/inbox", instance.public_domain)));
"{}/inbox",
instance.public_domain
)));
} }
if inserted.followers_endpoint.is_empty() { if inserted.followers_endpoint.is_empty() {
inserted.followers_endpoint = instance.compute_box( inserted.followers_endpoint =
USER_PREFIX, instance.compute_box(USER_PREFIX, &inserted.username, "followers");
&inserted.username,
"followers",
);
} }
if inserted.fqn.is_empty() { if inserted.fqn.is_empty() {
@ -162,7 +146,8 @@ impl User {
for blog in Blog::find_for_author(conn, self)? for blog in Blog::find_for_author(conn, self)?
.iter() .iter()
.filter(|b| b.count_authors(conn).map(|c| c <= 1).unwrap_or(false)) { .filter(|b| b.count_authors(conn).map(|c| c <= 1).unwrap_or(false))
{
blog.delete(conn, searcher)?; blog.delete(conn, searcher)?;
} }
// delete the posts if they is the only author // delete the posts if they is the only author
@ -180,10 +165,10 @@ impl User {
.count() .count()
.load(conn)? .load(conn)?
.first() .first()
.unwrap_or(&0) > &0; .unwrap_or(&0)
> &0;
if !has_other_authors { if !has_other_authors {
Post::get(conn, post_id)? Post::get(conn, post_id)?.delete(&(conn, searcher))?;
.delete(&(conn, searcher))?;
} }
} }
@ -213,12 +198,18 @@ impl User {
.map_err(Error::from) .map_err(Error::from)
} }
pub fn update(&self, conn: &Connection, name: String, email: String, summary: String) -> Result<User> { pub fn update(
&self,
conn: &Connection,
name: String,
email: String,
summary: String,
) -> Result<User> {
diesel::update(self) diesel::update(self)
.set(( .set((
users::display_name.eq(name), users::display_name.eq(name),
users::email.eq(email), users::email.eq(email),
users::summary_html.eq(utils::md_to_html(&summary,"").0), users::summary_html.eq(utils::md_to_html(&summary, "").0),
users::summary.eq(summary), users::summary.eq(summary),
)) ))
.execute(conn)?; .execute(conn)?;
@ -278,16 +269,13 @@ impl User {
} }
pub fn fetch_from_url(conn: &Connection, url: &str) -> Result<User> { pub fn fetch_from_url(conn: &Connection, url: &str) -> Result<User> {
User::fetch(url).and_then(|json| User::from_activity( User::fetch(url)
conn, .and_then(|json| User::from_activity(conn, &json, Url::parse(url)?.host_str()?))
&json,
Url::parse(url)?.host_str()?,
))
} }
fn from_activity(conn: &Connection, acct: &CustomPerson, inst: &str) -> Result<User> { fn from_activity(conn: &Connection, acct: &CustomPerson, inst: &str) -> Result<User> {
let instance = Instance::find_by_domain(conn, inst) let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
.or_else(|_| Instance::insert( Instance::insert(
conn, conn,
NewInstance { NewInstance {
name: inst.to_owned(), name: inst.to_owned(),
@ -301,9 +289,15 @@ impl User {
short_description_html: String::new(), short_description_html: String::new(),
long_description_html: String::new(), long_description_html: String::new(),
}, },
))?; )
})?;
if acct.object.ap_actor_props.preferred_username_string()?.contains(&['<', '>', '&', '@', '\'', '"'][..]) { if acct
.object
.ap_actor_props
.preferred_username_string()?
.contains(&['<', '>', '&', '@', '\'', '"'][..])
{
return Err(Error::InvalidValue); return Err(Error::InvalidValue);
} }
let user = User::insert( let user = User::insert(
@ -314,20 +308,11 @@ impl User {
.ap_actor_props .ap_actor_props
.preferred_username_string() .preferred_username_string()
.unwrap(), .unwrap(),
display_name: acct display_name: acct.object.object_props.name_string()?,
.object outbox_url: acct.object.ap_actor_props.outbox_string()?,
.object_props inbox_url: acct.object.ap_actor_props.inbox_string()?,
.name_string()?,
outbox_url: acct
.object
.ap_actor_props
.outbox_string()?,
inbox_url: acct
.object
.ap_actor_props
.inbox_string()?,
is_admin: false, is_admin: false,
summary:acct summary: acct
.object .object
.object_props .object_props
.summary_string() .summary_string()
@ -342,10 +327,7 @@ impl User {
email: None, email: None,
hashed_password: None, hashed_password: None,
instance_id: instance.id, instance_id: instance.id,
ap_url: acct ap_url: acct.object.object_props.id_string()?,
.object
.object_props
.id_string()?,
public_key: acct public_key: acct
.custom_props .custom_props
.public_key_publickey()? .public_key_publickey()?
@ -357,10 +339,7 @@ impl User {
.endpoints_endpoint() .endpoints_endpoint()
.and_then(|e| e.shared_inbox_string()) .and_then(|e| e.shared_inbox_string())
.ok(), .ok(),
followers_endpoint: acct followers_endpoint: acct.object.ap_actor_props.followers_string()?,
.object
.ap_actor_props
.followers_string()?,
avatar_id: None, avatar_id: None,
}, },
)?; )?;
@ -392,26 +371,15 @@ impl User {
.object_props .object_props
.url_string()?, .url_string()?,
&self, &self,
).ok(); )
.ok();
diesel::update(self) diesel::update(self)
.set(( .set((
users::username.eq(json users::username.eq(json.object.ap_actor_props.preferred_username_string()?),
.object users::display_name.eq(json.object.object_props.name_string()?),
.ap_actor_props users::outbox_url.eq(json.object.ap_actor_props.outbox_string()?),
.preferred_username_string()?), users::inbox_url.eq(json.object.ap_actor_props.inbox_string()?),
users::display_name.eq(json
.object
.object_props
.name_string()?),
users::outbox_url.eq(json
.object
.ap_actor_props
.outbox_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
@ -419,10 +387,7 @@ impl User {
.summary_string() .summary_string()
.unwrap_or_default(), .unwrap_or_default(),
)), )),
users::followers_endpoint.eq(json users::followers_endpoint.eq(json.object.ap_actor_props.followers_string()?),
.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
@ -441,7 +406,8 @@ impl User {
} }
pub fn auth(&self, pass: &str) -> bool { pub fn auth(&self, pass: &str) -> bool {
self.hashed_password.clone() self.hashed_password
.clone()
.map(|hashed| bcrypt::verify(pass, hashed.as_ref()).unwrap_or(false)) .map(|hashed| bcrypt::verify(pass, hashed.as_ref()).unwrap_or(false))
.unwrap_or(false) .unwrap_or(false)
} }
@ -468,8 +434,7 @@ impl User {
let n_acts = acts.len(); let n_acts = acts.len();
let mut coll = OrderedCollection::default(); let mut coll = OrderedCollection::default();
coll.collection_props.items = serde_json::to_value(acts)?; coll.collection_props.items = serde_json::to_value(acts)?;
coll.collection_props coll.collection_props.set_total_items_u64(n_acts as u64)?;
.set_total_items_u64(n_acts as u64)?;
Ok(ActivityStream::new(coll)) Ok(ActivityStream::new(coll))
} }
@ -483,12 +448,11 @@ impl User {
.into_iter() .into_iter()
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", "), .join(", "),
)? )?,
) )
.send()?; .send()?;
let text = &res.text()?; let text = &res.text()?;
let json: serde_json::Value = let json: serde_json::Value = serde_json::from_str(text)?;
serde_json::from_str(text)?;
Ok(json["items"] Ok(json["items"]
.as_array() .as_array()
.unwrap_or(&vec![]) .unwrap_or(&vec![])
@ -507,7 +471,7 @@ impl User {
.into_iter() .into_iter()
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", "), .join(", "),
)? )?,
) )
.send()?; .send()?;
let text = &res.text()?; let text = &res.text()?;
@ -531,7 +495,9 @@ impl User {
Ok(posts Ok(posts
.into_iter() .into_iter()
.filter_map(|p| { .filter_map(|p| {
p.create_activity(conn).ok().and_then(|a| serde_json::to_value(a).ok()) p.create_activity(conn)
.ok()
.and_then(|a| serde_json::to_value(a).ok())
}) })
.collect::<Vec<serde_json::Value>>()) .collect::<Vec<serde_json::Value>>())
} }
@ -555,7 +521,11 @@ impl User {
.map_err(Error::from) .map_err(Error::from)
} }
pub fn get_followers_page(&self, conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<User>> { pub fn get_followers_page(
&self,
conn: &Connection,
(min, max): (i32, i32),
) -> Result<Vec<User>> {
use schema::follows; use schema::follows;
let follows = Follow::belonging_to(self).select(follows::follower_id); let follows = Follow::belonging_to(self).select(follows::follower_id);
users::table users::table
@ -584,7 +554,11 @@ impl User {
.map_err(Error::from) .map_err(Error::from)
} }
pub fn get_followed_page(&self, conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<User>> { pub fn get_followed_page(
&self,
conn: &Connection,
(min, max): (i32, i32),
) -> Result<Vec<User>> {
use schema::follows; use schema::follows;
let follows = follows::table let follows = follows::table
.filter(follows::follower_id.eq(self.id)) .filter(follows::follower_id.eq(self.id))
@ -653,33 +627,32 @@ impl User {
} }
pub fn get_keypair(&self) -> Result<PKey<Private>> { pub fn get_keypair(&self) -> Result<PKey<Private>> {
PKey::from_rsa( PKey::from_rsa(Rsa::private_key_from_pem(
Rsa::private_key_from_pem( self.private_key.clone()?.as_ref(),
self.private_key )?)
.clone()? .map_err(Error::from)
.as_ref(),
)?,
).map_err(Error::from)
} }
pub fn rotate_keypair(&self, conn: &Connection) -> Result<PKey<Private>> { pub fn rotate_keypair(&self, conn: &Connection) -> Result<PKey<Private>> {
if self.private_key.is_none() { if self.private_key.is_none() {
return Err(Error::InvalidValue) return Err(Error::InvalidValue);
} }
if (Utc::now().naive_utc() - self.last_fetched_date).num_minutes() < 10 { if (Utc::now().naive_utc() - self.last_fetched_date).num_minutes() < 10 {
//rotated recently //rotated recently
self.get_keypair() self.get_keypair()
} else { } else {
let (public_key, private_key) = gen_keypair(); let (public_key, private_key) = gen_keypair();
let public_key = String::from_utf8(public_key).expect("NewUser::new_local: public key error"); let public_key =
let private_key = String::from_utf8(private_key).expect("NewUser::new_local: private key error"); String::from_utf8(public_key).expect("NewUser::new_local: public key error");
let res = PKey::from_rsa( let private_key =
Rsa::private_key_from_pem(private_key.as_ref())? String::from_utf8(private_key).expect("NewUser::new_local: private key error");
)?; let res = PKey::from_rsa(Rsa::private_key_from_pem(private_key.as_ref())?)?;
diesel::update(self) diesel::update(self)
.set((users::public_key.eq(public_key), .set((
users::public_key.eq(public_key),
users::private_key.eq(Some(private_key)), users::private_key.eq(Some(private_key)),
users::last_fetched_date.eq(Utc::now().naive_utc()))) users::last_fetched_date.eq(Utc::now().naive_utc()),
))
.execute(conn) .execute(conn)
.map_err(Error::from) .map_err(Error::from)
.map(|_| res) .map(|_| res)
@ -688,18 +661,14 @@ impl User {
pub fn to_activity(&self, conn: &Connection) -> Result<CustomPerson> { pub fn to_activity(&self, conn: &Connection) -> Result<CustomPerson> {
let mut actor = Person::default(); let mut actor = Person::default();
actor actor.object_props.set_id_string(self.ap_url.clone())?;
.object_props
.set_id_string(self.ap_url.clone())?;
actor actor
.object_props .object_props
.set_name_string(self.display_name.clone())?; .set_name_string(self.display_name.clone())?;
actor actor
.object_props .object_props
.set_summary_string(self.summary_html.get().clone())?; .set_summary_string(self.summary_html.get().clone())?;
actor actor.object_props.set_url_string(self.ap_url.clone())?;
.object_props
.set_url_string(self.ap_url.clone())?;
actor actor
.ap_actor_props .ap_actor_props
.set_inbox_string(self.inbox_url.clone())?; .set_inbox_string(self.inbox_url.clone())?;
@ -714,42 +683,31 @@ impl User {
.set_followers_string(self.followers_endpoint.clone())?; .set_followers_string(self.followers_endpoint.clone())?;
let mut endpoints = Endpoint::default(); let mut endpoints = Endpoint::default();
endpoints endpoints.set_shared_inbox_string(ap_url(&format!("{}/inbox/", BASE_URL.as_str())))?;
.set_shared_inbox_string(ap_url(&format!("{}/inbox/", BASE_URL.as_str())))?; actor.ap_actor_props.set_endpoints_endpoint(endpoints)?;
actor
.ap_actor_props
.set_endpoints_endpoint(endpoints)?;
let mut public_key = PublicKey::default(); let mut public_key = PublicKey::default();
public_key public_key.set_id_string(format!("{}#main-key", self.ap_url))?;
.set_id_string(format!("{}#main-key", self.ap_url))?; public_key.set_owner_string(self.ap_url.clone())?;
public_key public_key.set_public_key_pem_string(self.public_key.clone())?;
.set_owner_string(self.ap_url.clone())?;
public_key
.set_public_key_pem_string(self.public_key.clone())?;
let mut ap_signature = ApSignature::default(); let mut ap_signature = ApSignature::default();
ap_signature ap_signature.set_public_key_publickey(public_key)?;
.set_public_key_publickey(public_key)?;
let mut avatar = Image::default(); let mut avatar = Image::default();
avatar avatar.object_props.set_url_string(
.object_props
.set_url_string(
self.avatar_id self.avatar_id
.and_then(|id| Media::get(conn, id).and_then(|m| m.url(conn)).ok()) .and_then(|id| Media::get(conn, id).and_then(|m| m.url(conn)).ok())
.unwrap_or_default(), .unwrap_or_default(),
)?; )?;
actor actor.object_props.set_icon_object(avatar)?;
.object_props
.set_icon_object(avatar)?;
Ok(CustomPerson::new(actor, ap_signature)) Ok(CustomPerson::new(actor, ap_signature))
} }
pub fn avatar_url(&self, conn: &Connection) -> String { pub fn avatar_url(&self, conn: &Connection) -> String {
self.avatar_id.and_then(|id| self.avatar_id
Media::get(conn, id).and_then(|m| m.url(conn)).ok() .and_then(|id| Media::get(conn, id).and_then(|m| m.url(conn)).ok())
).unwrap_or_else(|| "/static/default-avatar.png".to_string()) .unwrap_or_else(|| "/static/default-avatar.png".to_string())
} }
pub fn webfinger(&self, conn: &Connection) -> Result<Webfinger> { pub fn webfinger(&self, conn: &Connection) -> Result<Webfinger> {
@ -866,21 +824,15 @@ impl Signer for User {
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> { fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
let key = self.get_keypair()?; let key = self.get_keypair()?;
let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?; let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?;
signer signer.update(to_sign.as_bytes())?;
.update(to_sign.as_bytes())?; signer.sign_to_vec().map_err(Error::from)
signer
.sign_to_vec()
.map_err(Error::from)
} }
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> { fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?; let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?; let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?;
verifier verifier.update(data.as_bytes())?;
.update(data.as_bytes())?; verifier.verify(&signature).map_err(Error::from)
verifier
.verify(&signature)
.map_err(Error::from)
} }
} }
@ -915,7 +867,7 @@ impl NewUser {
display_name, display_name,
is_admin, is_admin,
summary: summary.to_owned(), summary: summary.to_owned(),
summary_html: SafeString::new(&utils::md_to_html(&summary,"").0), summary_html: SafeString::new(&utils::md_to_html(&summary, "").0),
email: Some(email), email: Some(email),
hashed_password: Some(password), hashed_password: Some(password),
instance_id: Instance::get_local(conn)?.id, instance_id: Instance::get_local(conn)?.id,
@ -947,7 +899,8 @@ pub(crate) mod tests {
"Hello there, I'm the admin", "Hello there, I'm the admin",
"admin@example.com".to_owned(), "admin@example.com".to_owned(),
"invalid_admin_password".to_owned(), "invalid_admin_password".to_owned(),
).unwrap(); )
.unwrap();
let user = NewUser::new_local( let user = NewUser::new_local(
conn, conn,
"user".to_owned(), "user".to_owned(),
@ -956,7 +909,8 @@ pub(crate) mod tests {
"Hello there, I'm no one", "Hello there, I'm no one",
"user@example.com".to_owned(), "user@example.com".to_owned(),
"invalid_user_password".to_owned(), "invalid_user_password".to_owned(),
).unwrap(); )
.unwrap();
let other = NewUser::new_local( let other = NewUser::new_local(
conn, conn,
"other".to_owned(), "other".to_owned(),
@ -965,8 +919,9 @@ pub(crate) mod tests {
"Hello there, I'm someone else", "Hello there, I'm someone else",
"other@example.com".to_owned(), "other@example.com".to_owned(),
"invalid_other_password".to_owned(), "invalid_other_password".to_owned(),
).unwrap(); )
vec![ admin, user, other ] .unwrap();
vec![admin, user, other]
} }
#[test] #[test]
@ -982,7 +937,8 @@ pub(crate) mod tests {
"Hello I'm a test", "Hello I'm a test",
"test@example.com".to_owned(), "test@example.com".to_owned(),
User::hash_pass("test_password").unwrap(), User::hash_pass("test_password").unwrap(),
).unwrap(); )
.unwrap();
assert_eq!( assert_eq!(
test_user.id, test_user.id,
@ -996,9 +952,7 @@ pub(crate) mod tests {
); );
assert_eq!( assert_eq!(
test_user.id, test_user.id,
User::find_by_email(conn, "test@example.com") User::find_by_email(conn, "test@example.com").unwrap().id
.unwrap()
.id
); );
assert_eq!( assert_eq!(
test_user.id, test_user.id,
@ -1009,7 +963,8 @@ pub(crate) mod tests {
Instance::get_local(conn).unwrap().public_domain, Instance::get_local(conn).unwrap().public_domain,
"test" "test"
) )
).unwrap() )
.unwrap()
.id .id
); );
@ -1040,7 +995,11 @@ pub(crate) mod tests {
let mut i = 0; let mut i = 0;
while local_inst.has_admin(conn).unwrap() { while local_inst.has_admin(conn).unwrap() {
assert!(i < 100); //prevent from looping indefinitelly assert!(i < 100); //prevent from looping indefinitelly
local_inst.main_admin(conn).unwrap().revoke_admin_rights(conn).unwrap(); local_inst
.main_admin(conn)
.unwrap()
.revoke_admin_rights(conn)
.unwrap();
i += 1; i += 1;
} }
inserted[0].grant_admin_rights(conn).unwrap(); inserted[0].grant_admin_rights(conn).unwrap();
@ -1055,12 +1014,14 @@ pub(crate) mod tests {
let conn = &db(); let conn = &db();
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let inserted = fill_database(conn); let inserted = fill_database(conn);
let updated = inserted[0].update( let updated = inserted[0]
.update(
conn, conn,
"new name".to_owned(), "new name".to_owned(),
"em@il".to_owned(), "em@il".to_owned(),
"<p>summary</p><script></script>".to_owned(), "<p>summary</p><script></script>".to_owned(),
).unwrap(); )
.unwrap();
assert_eq!(updated.display_name, "new name"); assert_eq!(updated.display_name, "new name");
assert_eq!(updated.email.unwrap(), "em@il"); assert_eq!(updated.email.unwrap(), "em@il");
assert_eq!(updated.summary_html.get(), "<p>summary</p>"); assert_eq!(updated.summary_html.get(), "<p>summary</p>");
@ -1082,7 +1043,8 @@ pub(crate) mod tests {
"Hello I'm a test", "Hello I'm a test",
"test@example.com".to_owned(), "test@example.com".to_owned(),
User::hash_pass("test_password").unwrap(), User::hash_pass("test_password").unwrap(),
).unwrap(); )
.unwrap();
assert!(test_user.auth("test_password")); assert!(test_user.auth("test_password"));
assert!(!test_user.auth("other_password")); assert!(!test_user.auth("other_password"));
@ -1101,7 +1063,9 @@ pub(crate) mod tests {
assert_eq!(page.len(), 2); assert_eq!(page.len(), 2);
assert!(page[0].username <= page[1].username); assert!(page[0].username <= page[1].username);
let mut last_username = User::get_local_page(conn, (0, 1)).unwrap()[0].username.clone(); let mut last_username = User::get_local_page(conn, (0, 1)).unwrap()[0]
.username
.clone();
for i in 1..User::count_local(conn).unwrap() as i32 { for i in 1..User::count_local(conn).unwrap() as i32 {
let page = User::get_local_page(conn, (i, i + 1)).unwrap(); let page = User::get_local_page(conn, (i, i + 1)).unwrap();
assert_eq!(page.len(), 1); assert_eq!(page.len(), 1);
@ -1109,7 +1073,9 @@ pub(crate) mod tests {
last_username = page[0].username.clone(); last_username = page[0].username.clone();
} }
assert_eq!( assert_eq!(
User::get_local_page(conn, (0, User::count_local(conn).unwrap() as i32 + 10)).unwrap().len() as i64, User::get_local_page(conn, (0, User::count_local(conn).unwrap() as i32 + 10))
.unwrap()
.len() as i64,
User::count_local(conn).unwrap() User::count_local(conn).unwrap()
); );

View File

@ -3,11 +3,7 @@ use rocket_contrib::json::Json;
use serde_json; use serde_json;
use plume_api::apps::AppEndpoint; use plume_api::apps::AppEndpoint;
use plume_models::{ use plume_models::{apps::App, db_conn::DbConn, Connection};
Connection,
db_conn::DbConn,
apps::App,
};
#[post("/apps", data = "<data>")] #[post("/apps", data = "<data>")]
pub fn create(conn: DbConn, data: Json<AppEndpoint>) -> Json<serde_json::Value> { pub fn create(conn: DbConn, data: Json<AppEndpoint>) -> Json<serde_json::Value> {

View File

@ -1,10 +1,10 @@
use plume_models::{self, api_tokens::ApiToken};
use rocket::{ use rocket::{
Outcome,
http::Status, http::Status,
request::{self, FromRequest, Request} request::{self, FromRequest, Request},
Outcome,
}; };
use std::marker::PhantomData; use std::marker::PhantomData;
use plume_models::{self, api_tokens::ApiToken};
// Actions // Actions
pub trait Action { pub trait Action {
@ -33,22 +33,25 @@ 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)>);
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 A: Action, where
S: Scope A: Action,
S: Scope,
{ {
type Error = (); type Error = ();
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.guard::<ApiToken>() request
.guard::<ApiToken>()
.map_failure(|_| (Status::Unauthorized, ())) .map_failure(|_| (Status::Unauthorized, ()))
.and_then(|token| if token.can(A::to_str(), S::to_str()) { .and_then(|token| {
if token.can(A::to_str(), S::to_str()) {
Outcome::Success(Authorization(token, PhantomData)) Outcome::Success(Authorization(token, PhantomData))
} else { } else {
Outcome::Failure((Status::Unauthorized, ())) Outcome::Failure((Status::Unauthorized, ()))
}
}) })
} }
} }

View File

@ -1,16 +1,13 @@
#![warn(clippy::too_many_arguments)] #![warn(clippy::too_many_arguments)]
use rocket::{response::{self, Responder}, request::{Form, Request}}; use rocket::{
request::{Form, Request},
response::{self, Responder},
};
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
use serde_json; use serde_json;
use plume_common::utils::random_hex; use plume_common::utils::random_hex;
use plume_models::{ use plume_models::{api_tokens::*, apps::App, db_conn::DbConn, users::User, Error};
Error,
apps::App,
api_tokens::*,
db_conn::DbConn,
users::User,
};
#[derive(Debug)] #[derive(Debug)]
pub struct ApiError(Error); pub struct ApiError(Error);
@ -26,13 +23,16 @@ impl<'r> Responder<'r> for ApiError {
match self.0 { match self.0 {
Error::NotFound => Json(json!({ Error::NotFound => Json(json!({
"error": "Not found" "error": "Not found"
})).respond_to(req), }))
.respond_to(req),
Error::Unauthorized => Json(json!({ Error::Unauthorized => Json(json!({
"error": "You are not authorized to access this resource" "error": "You are not authorized to access this resource"
})).respond_to(req), }))
.respond_to(req),
_ => Json(json!({ _ => Json(json!({
"error": "Server error" "error": "Server error"
})).respond_to(req) }))
.respond_to(req),
} }
} }
} }
@ -52,12 +52,15 @@ pub fn oauth(query: Form<OAuthRequest>, conn: DbConn) -> Result<Json<serde_json:
if app.client_secret == query.client_secret { if app.client_secret == query.client_secret {
if let Ok(user) = User::find_by_fqn(&*conn, &query.username) { if let Ok(user) = User::find_by_fqn(&*conn, &query.username) {
if user.auth(&query.password) { if user.auth(&query.password) {
let token = ApiToken::insert(&*conn, NewApiToken { let token = ApiToken::insert(
&*conn,
NewApiToken {
app_id: app.id, app_id: app.id,
user_id: user.id, user_id: user.id,
value: random_hex(), value: random_hex(),
scopes: query.scopes.clone(), scopes: query.scopes.clone(),
})?; },
)?;
Ok(Json(json!({ Ok(Json(json!({
"token": token.value "token": token.value
}))) })))

View File

@ -5,36 +5,72 @@ use scheduled_thread_pool::ScheduledThreadPool;
use serde_json; use serde_json;
use serde_qs; use serde_qs;
use api::authorization::*;
use plume_api::posts::PostEndpoint; use plume_api::posts::PostEndpoint;
use plume_models::{ use plume_models::{
Connection, db_conn::DbConn, posts::Post, search::Searcher as UnmanagedSearcher, Connection,
db_conn::DbConn,
posts::Post,
search::Searcher as UnmanagedSearcher,
}; };
use api::authorization::*;
use {Searcher, Worker}; use {Searcher, Worker};
#[get("/posts/<id>")] #[get("/posts/<id>")]
pub fn get(id: i32, conn: DbConn, worker: Worker, auth: Option<Authorization<Read, Post>>, search: Searcher) -> Json<serde_json::Value> { pub fn get(
let post = <Post as Provider<(&Connection, &ScheduledThreadPool, &UnmanagedSearcher, Option<i32>)>> id: i32,
::get(&(&*conn, &worker, &search, auth.map(|a| a.0.user_id)), id).ok(); conn: DbConn,
worker: Worker,
auth: Option<Authorization<Read, Post>>,
search: Searcher,
) -> Json<serde_json::Value> {
let post = <Post as Provider<(
&Connection,
&ScheduledThreadPool,
&UnmanagedSearcher,
Option<i32>,
)>>::get(&(&*conn, &worker, &search, auth.map(|a| a.0.user_id)), id)
.ok();
Json(json!(post)) Json(json!(post))
} }
#[get("/posts")] #[get("/posts")]
pub fn list(conn: DbConn, uri: &Origin, worker: Worker, auth: Option<Authorization<Read, Post>>, search: Searcher) -> Json<serde_json::Value> { pub fn list(
let query: PostEndpoint = serde_qs::from_str(uri.query().unwrap_or("")).expect("api::list: invalid query error"); conn: DbConn,
let post = <Post as Provider<(&Connection, &ScheduledThreadPool, &UnmanagedSearcher, Option<i32>)>> uri: &Origin,
::list(&(&*conn, &worker, &search, auth.map(|a| a.0.user_id)), query); worker: Worker,
auth: Option<Authorization<Read, Post>>,
search: Searcher,
) -> Json<serde_json::Value> {
let query: PostEndpoint =
serde_qs::from_str(uri.query().unwrap_or("")).expect("api::list: invalid query error");
let post = <Post as Provider<(
&Connection,
&ScheduledThreadPool,
&UnmanagedSearcher,
Option<i32>,
)>>::list(
&(&*conn, &worker, &search, auth.map(|a| a.0.user_id)),
query,
);
Json(json!(post)) Json(json!(post))
} }
#[post("/posts", data = "<payload>")] #[post("/posts", data = "<payload>")]
pub fn create(conn: DbConn, payload: Json<PostEndpoint>, worker: Worker, auth: Authorization<Write, Post>, search: Searcher) -> Json<serde_json::Value> { pub fn create(
let new_post = <Post as Provider<(&Connection, &ScheduledThreadPool, &UnmanagedSearcher, Option<i32>)>> conn: DbConn,
::create(&(&*conn, &worker, &search, Some(auth.0.user_id)), (*payload).clone()); payload: Json<PostEndpoint>,
Json(new_post.map(|p| json!(p)).unwrap_or_else(|e| json!({ worker: Worker,
auth: Authorization<Write, Post>,
search: Searcher,
) -> Json<serde_json::Value> {
let new_post = <Post as Provider<(
&Connection,
&ScheduledThreadPool,
&UnmanagedSearcher,
Option<i32>,
)>>::create(
&(&*conn, &worker, &search, Some(auth.0.user_id)),
(*payload).clone(),
);
Json(new_post.map(|p| json!(p)).unwrap_or_else(|e| {
json!({
"error": "Invalid data, couldn't create new post", "error": "Invalid data, couldn't create new post",
"details": match e { "details": match e {
ApiError::Fetch(msg) => msg, ApiError::Fetch(msg) => msg,
@ -42,6 +78,6 @@ pub fn create(conn: DbConn, payload: Json<PostEndpoint>, worker: Worker, auth: A
ApiError::NotFound(msg) => msg, ApiError::NotFound(msg) => msg,
ApiError::Authorization(msg) => msg, ApiError::Authorization(msg) => msg,
} }
}))) })
}))
} }

View File

@ -1,23 +1,10 @@
#![warn(clippy::too_many_arguments)] #![warn(clippy::too_many_arguments)]
use activitypub::{ use activitypub::{
activity::{ activity::{Announce, Create, Delete, Follow as FollowAct, Like, Undo, Update},
Announce, object::Tombstone,
Create,
Delete,
Follow as FollowAct,
Like,
Undo,
Update
},
object::Tombstone
}; };
use failure::Error; use failure::Error;
use rocket::{ use rocket::{data::*, http::Status, Outcome::*, Request};
data::*,
http::Status,
Outcome::*,
Request,
};
use rocket_contrib::json::*; use rocket_contrib::json::*;
use serde::Deserialize; use serde::Deserialize;
use serde_json; use serde_json;
@ -26,15 +13,21 @@ use std::io::Read;
use plume_common::activity_pub::{ use plume_common::activity_pub::{
inbox::{Deletable, FromActivity, InboxError, Notify}, inbox::{Deletable, FromActivity, InboxError, Notify},
Id,request::Digest, request::Digest,
Id,
}; };
use plume_models::{ use plume_models::{
comments::Comment, follows::Follow, instance::Instance, likes, posts::Post, reshares::Reshare, comments::Comment, follows::Follow, instance::Instance, likes, posts::Post, reshares::Reshare,
users::User, search::Searcher, Connection, search::Searcher, users::User, Connection,
}; };
pub trait Inbox { pub trait Inbox {
fn received(&self, conn: &Connection, searcher: &Searcher, act: serde_json::Value) -> Result<(), Error> { fn received(
&self,
conn: &Connection,
searcher: &Searcher,
act: serde_json::Value,
) -> Result<(), Error> {
let actor_id = Id::new(act["actor"].as_str().unwrap_or_else(|| { let actor_id = Id::new(act["actor"].as_str().unwrap_or_else(|| {
act["actor"]["id"] act["actor"]["id"]
.as_str() .as_str()
@ -66,7 +59,8 @@ pub trait Inbox {
.id_string()?, .id_string()?,
actor_id.as_ref(), actor_id.as_ref(),
&(conn, searcher), &(conn, searcher),
).ok(); )
.ok();
Comment::delete_id( Comment::delete_id(
&act.delete_props &act.delete_props
.object_object::<Tombstone>()? .object_object::<Tombstone>()?
@ -74,12 +68,14 @@ pub trait Inbox {
.id_string()?, .id_string()?,
actor_id.as_ref(), actor_id.as_ref(),
conn, conn,
).ok(); )
.ok();
Ok(()) Ok(())
} }
"Follow" => { "Follow" => {
Follow::from_activity(conn, serde_json::from_value(act.clone())?, actor_id) Follow::from_activity(conn, serde_json::from_value(act.clone())?, actor_id)
.and_then(|f| f.notify(conn)).expect("Inbox::received: follow from activity error");; .and_then(|f| f.notify(conn))
.expect("Inbox::received: follow from activity error");;
Ok(()) Ok(())
} }
"Like" => { "Like" => {
@ -87,7 +83,8 @@ pub trait Inbox {
conn, conn,
serde_json::from_value(act.clone())?, serde_json::from_value(act.clone())?,
actor_id, actor_id,
).expect("Inbox::received: like from activity error");; )
.expect("Inbox::received: like from activity error");;
Ok(()) Ok(())
} }
"Undo" => { "Undo" => {
@ -102,7 +99,8 @@ pub trait Inbox {
.id_string()?, .id_string()?,
actor_id.as_ref(), actor_id.as_ref(),
conn, conn,
).expect("Inbox::received: undo like fail");; )
.expect("Inbox::received: undo like fail");;
Ok(()) Ok(())
} }
"Announce" => { "Announce" => {
@ -113,7 +111,8 @@ pub trait Inbox {
.id_string()?, .id_string()?,
actor_id.as_ref(), actor_id.as_ref(),
conn, conn,
).expect("Inbox::received: undo reshare fail");; )
.expect("Inbox::received: undo reshare fail");;
Ok(()) Ok(())
} }
"Follow" => { "Follow" => {
@ -124,21 +123,28 @@ pub trait Inbox {
.id_string()?, .id_string()?,
actor_id.as_ref(), actor_id.as_ref(),
conn, conn,
).expect("Inbox::received: undo follow error");; )
.expect("Inbox::received: undo follow error");;
Ok(()) Ok(())
} }
_ => Err(InboxError::CantUndo)?, _ => Err(InboxError::CantUndo)?,
} }
} else { } else {
let link = act.undo_props.object.as_str().expect("Inbox::received: undo doesn't contain a type and isn't Link"); let link =
act.undo_props.object.as_str().expect(
"Inbox::received: undo doesn't contain a type and isn't Link",
);
if let Ok(like) = likes::Like::find_by_ap_url(conn, link) { if let Ok(like) = likes::Like::find_by_ap_url(conn, link) {
likes::Like::delete_id(&like.ap_url, actor_id.as_ref(), conn).expect("Inbox::received: delete Like error"); likes::Like::delete_id(&like.ap_url, actor_id.as_ref(), conn)
.expect("Inbox::received: delete Like error");
Ok(()) Ok(())
} else if let Ok(reshare) = Reshare::find_by_ap_url(conn, link) { } else if let Ok(reshare) = Reshare::find_by_ap_url(conn, link) {
Reshare::delete_id(&reshare.ap_url, actor_id.as_ref(), conn).expect("Inbox::received: delete Announce error"); Reshare::delete_id(&reshare.ap_url, actor_id.as_ref(), conn)
.expect("Inbox::received: delete Announce error");
Ok(()) Ok(())
} else if let Ok(follow) = Follow::find_by_ap_url(conn, link) { } else if let Ok(follow) = Follow::find_by_ap_url(conn, link) {
Follow::delete_id(&follow.ap_url, actor_id.as_ref(), conn).expect("Inbox::received: delete Follow error"); Follow::delete_id(&follow.ap_url, actor_id.as_ref(), conn)
.expect("Inbox::received: delete Follow error");
Ok(()) Ok(())
} else { } else {
Err(InboxError::NoType)? Err(InboxError::NoType)?
@ -147,7 +153,8 @@ pub trait Inbox {
} }
"Update" => { "Update" => {
let act: Update = serde_json::from_value(act.clone())?; let act: Update = serde_json::from_value(act.clone())?;
Post::handle_update(conn, &act.update_props.object_object()?, searcher).expect("Inbox::received: post update error"); Post::handle_update(conn, &act.update_props.object_object()?, searcher)
.expect("Inbox::received: post update error");
Ok(()) Ok(())
} }
_ => Err(InboxError::InvalidType)?, _ => Err(InboxError::InvalidType)?,
@ -169,19 +176,25 @@ 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: &Request, d: Data) -> Transform<rocket::data::Outcome<Self::Owned, Self::Error>> { fn transform(
r: &Request,
d: Data,
) -> Transform<rocket::data::Outcome<Self::Owned, Self::Error>> {
let size_limit = r.limits().get("json").unwrap_or(JSON_LIMIT); let size_limit = r.limits().get("json").unwrap_or(JSON_LIMIT);
let mut s = String::with_capacity(512); let mut s = String::with_capacity(512);
match d.open().take(size_limit).read_to_string(&mut s) { match d.open().take(size_limit).read_to_string(&mut s) {
Ok(_) => Transform::Borrowed(Success(s)), Ok(_) => Transform::Borrowed(Success(s)),
Err(e) => Transform::Borrowed(Failure((Status::BadRequest, JsonError::Io(e)))) Err(e) => Transform::Borrowed(Failure((Status::BadRequest, JsonError::Io(e)))),
} }
} }
fn from_data(_: &Request, o: Transformed<'a, Self>) -> rocket::data::Outcome<Self, Self::Error> { fn from_data(
_: &Request,
o: Transformed<'a, Self>,
) -> rocket::data::Outcome<Self, Self::Error> {
let string = o.borrowed()?; let string = 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() {
Failure((Status::UnprocessableEntity, JsonError::Parse(string, e))) Failure((Status::UnprocessableEntity, JsonError::Parse(string, e)))

View File

@ -6,8 +6,8 @@ pub use self::mailer::*;
#[cfg(feature = "debug-mailer")] #[cfg(feature = "debug-mailer")]
mod mailer { mod mailer {
use lettre::{Transport, SendableEmail}; use lettre::{SendableEmail, Transport};
use std::{io::Read}; use std::io::Read;
pub struct DebugTransport; pub struct DebugTransport;
@ -18,11 +18,18 @@ mod mailer {
println!( println!(
"{}: from=<{}> to=<{:?}>\n{:#?}", "{}: from=<{}> to=<{:?}>\n{:#?}",
email.message_id().to_string(), email.message_id().to_string(),
email.envelope().from().map(ToString::to_string).unwrap_or_default(), email
.envelope()
.from()
.map(ToString::to_string)
.unwrap_or_default(),
email.envelope().to().to_vec(), email.envelope().to().to_vec(),
{ {
let mut message = String::new(); let mut message = String::new();
email.message().read_to_string(&mut message).map_err(|_| ())?; email
.message()
.read_to_string(&mut message)
.map_err(|_| ())?;
message message
}, },
); );
@ -40,13 +47,12 @@ mod mailer {
#[cfg(not(feature = "debug-mailer"))] #[cfg(not(feature = "debug-mailer"))]
mod mailer { mod mailer {
use lettre::{ use lettre::{
SmtpTransport,
SmtpClient,
smtp::{ smtp::{
authentication::{Credentials, Mechanism}, authentication::{Credentials, Mechanism},
extension::ClientId, extension::ClientId,
ConnectionReuseParameters, ConnectionReuseParameters,
}, },
SmtpClient, SmtpTransport,
}; };
use std::env; use std::env;
@ -57,7 +63,8 @@ mod mailer {
let helo_name = env::var("MAIL_HELO_NAME").unwrap_or_else(|_| "localhost".to_owned()); let helo_name = env::var("MAIL_HELO_NAME").unwrap_or_else(|_| "localhost".to_owned());
let username = env::var("MAIL_USER").ok()?; let username = env::var("MAIL_USER").ok()?;
let password = env::var("MAIL_PASSWORD").ok()?; let password = env::var("MAIL_PASSWORD").ok()?;
let mail = SmtpClient::new_simple(&server).unwrap() let mail = SmtpClient::new_simple(&server)
.unwrap()
.hello_name(ClientId::Domain(helo_name)) .hello_name(ClientId::Domain(helo_name))
.credentials(Credentials::new(username, password)) .credentials(Credentials::new(username, password))
.smtp_utf8(true) .smtp_utf8(true)
@ -70,9 +77,17 @@ mod mailer {
pub fn build_mail(dest: String, subject: String, body: String) -> Option<Email> { pub fn build_mail(dest: String, subject: String, body: String) -> Option<Email> {
Email::builder() Email::builder()
.from(env::var("MAIL_ADDRESS") .from(
.or_else(|_| Ok(format!("{}@{}", env::var("MAIL_USER")?, env::var("MAIL_SERVER")?)) as Result<_, env::VarError>) env::var("MAIL_ADDRESS")
.expect("Mail server is not correctly configured")) .or_else(|_| {
Ok(format!(
"{}@{}",
env::var("MAIL_USER")?,
env::var("MAIL_SERVER")?
)) as Result<_, env::VarError>
})
.expect("Mail server is not correctly configured"),
)
.to(dest) .to(dest)
.subject(subject) .subject(subject)
.text(body) .text(body)

View File

@ -39,16 +39,13 @@ extern crate validator_derive;
extern crate webfinger; extern crate webfinger;
use diesel::r2d2::ConnectionManager; use diesel::r2d2::ConnectionManager;
use rocket::{
Config, State,
config::Limits
};
use rocket_csrf::CsrfFairingBuilder;
use plume_models::{ use plume_models::{
DATABASE_URL, Connection, Error,
db_conn::{DbPool, PragmaForeignKey}, db_conn::{DbPool, PragmaForeignKey},
search::{Searcher as UnmanagedSearcher, SearcherError}, search::{Searcher as UnmanagedSearcher, SearcherError},
Connection, Error, DATABASE_URL,
}; };
use rocket::{config::Limits, Config, State};
use rocket_csrf::CsrfFairingBuilder;
use scheduled_thread_pool::ScheduledThreadPool; use scheduled_thread_pool::ScheduledThreadPool;
use std::env; use std::env;
use std::process::exit; use std::process::exit;
@ -78,7 +75,8 @@ fn init_pool() -> Option<DbPool> {
let manager = ConnectionManager::<Connection>::new(DATABASE_URL.as_str()); let manager = ConnectionManager::<Connection>::new(DATABASE_URL.as_str());
DbPool::builder() DbPool::builder()
.connection_customizer(Box::new(PragmaForeignKey)) .connection_customizer(Box::new(PragmaForeignKey))
.build(manager).ok() .build(manager)
.ok()
} }
fn main() { fn main() {
@ -89,37 +87,58 @@ fn main() {
let searcher = match UnmanagedSearcher::open(&"search_index") { let searcher = match UnmanagedSearcher::open(&"search_index") {
Err(Error::Search(e)) => match e { Err(Error::Search(e)) => match e {
SearcherError::WriteLockAcquisitionError => panic!( SearcherError::WriteLockAcquisitionError => panic!(
r#"Your search index is locked. Plume can't start. To fix this issue r#"Your search index is locked. Plume can't start. To fix this issue
make sure no other Plume instance is started, and run: make sure no other Plume instance is started, and run:
plm search unlock plm search unlock
Then try to restart Plume. Then try to restart Plume.
"#), "#
e => Err(e).unwrap() ),
e => Err(e).unwrap(),
}, },
Err(_) => panic!("Unexpected error while opening search index"), Err(_) => panic!("Unexpected error while opening search index"),
Ok(s) => Arc::new(s) Ok(s) => Arc::new(s),
}; };
let commiter = searcher.clone(); let commiter = searcher.clone();
workpool.execute_with_fixed_delay(Duration::from_secs(5), Duration::from_secs(60*30), move || commiter.commit()); workpool.execute_with_fixed_delay(
Duration::from_secs(5),
Duration::from_secs(60 * 30),
move || commiter.commit(),
);
let search_unlocker = searcher.clone(); let search_unlocker = searcher.clone();
ctrlc::set_handler(move || { ctrlc::set_handler(move || {
search_unlocker.drop_writer(); search_unlocker.drop_writer();
exit(0); exit(0);
}).expect("Error setting Ctrl-c handler"); })
.expect("Error setting Ctrl-c handler");
let mut config = Config::active().unwrap(); let mut config = Config::active().unwrap();
config.set_address(env::var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned())).unwrap(); config
config.set_port(env::var("ROCKET_PORT").ok().map(|s| s.parse::<u16>().unwrap()).unwrap_or(7878)); .set_address(env::var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned()))
.unwrap();
config.set_port(
env::var("ROCKET_PORT")
.ok()
.map(|s| s.parse::<u16>().unwrap())
.unwrap_or(7878),
);
let _ = env::var("ROCKET_SECRET_KEY").map(|k| config.set_secret_key(k).unwrap()); let _ = env::var("ROCKET_SECRET_KEY").map(|k| config.set_secret_key(k).unwrap());
let form_size = &env::var("FORM_SIZE").unwrap_or_else(|_| "32".to_owned()).parse::<u64>().unwrap(); let form_size = &env::var("FORM_SIZE")
let activity_size = &env::var("ACTIVITY_SIZE").unwrap_or_else(|_| "1024".to_owned()).parse::<u64>().unwrap(); .unwrap_or_else(|_| "32".to_owned())
config.set_limits(Limits::new() .parse::<u64>()
.unwrap();
let activity_size = &env::var("ACTIVITY_SIZE")
.unwrap_or_else(|_| "1024".to_owned())
.parse::<u64>()
.unwrap();
config.set_limits(
Limits::new()
.limit("forms", form_size * 1024) .limit("forms", form_size * 1024)
.limit("json", activity_size * 1024)); .limit("json", activity_size * 1024),
);
let mail = mail::init(); let mail = mail::init();
if mail.is_none() && config.environment.is_prod() { if mail.is_none() && config.environment.is_prod() {
@ -128,7 +147,9 @@ Then try to restart Plume.
} }
rocket::custom(config) rocket::custom(config)
.mount("/", routes![ .mount(
"/",
routes![
routes::blogs::details, routes::blogs::details,
routes::blogs::activity_details, routes::blogs::activity_details,
routes::blogs::outbox, routes::blogs::outbox,
@ -137,11 +158,9 @@ Then try to restart Plume.
routes::blogs::create, routes::blogs::create,
routes::blogs::delete, routes::blogs::delete,
routes::blogs::atom_feed, routes::blogs::atom_feed,
routes::comments::create, routes::comments::create,
routes::comments::delete, routes::comments::delete,
routes::comments::activity_pub, routes::comments::activity_pub,
routes::instance::index, routes::instance::index,
routes::instance::local, routes::instance::local,
routes::instance::feed, routes::instance::feed,
@ -156,20 +175,16 @@ Then try to restart Plume.
routes::instance::nodeinfo, routes::instance::nodeinfo,
routes::instance::about, routes::instance::about,
routes::instance::web_manifest, routes::instance::web_manifest,
routes::likes::create, routes::likes::create,
routes::likes::create_auth, routes::likes::create_auth,
routes::medias::list, routes::medias::list,
routes::medias::new, routes::medias::new,
routes::medias::upload, routes::medias::upload,
routes::medias::details, routes::medias::details,
routes::medias::delete, routes::medias::delete,
routes::medias::set_avatar, routes::medias::set_avatar,
routes::notifications::notifications, routes::notifications::notifications,
routes::notifications::notifications_auth, routes::notifications::notifications_auth,
routes::posts::details, routes::posts::details,
routes::posts::activity_details, routes::posts::activity_details,
routes::posts::edit, routes::posts::edit,
@ -178,12 +193,9 @@ Then try to restart Plume.
routes::posts::new_auth, routes::posts::new_auth,
routes::posts::create, routes::posts::create,
routes::posts::delete, routes::posts::delete,
routes::reshares::create, routes::reshares::create,
routes::reshares::create_auth, routes::reshares::create_auth,
routes::search::search, routes::search::search,
routes::session::new, routes::session::new,
routes::session::create, routes::session::create,
routes::session::delete, routes::session::delete,
@ -191,12 +203,9 @@ Then try to restart Plume.
routes::session::password_reset_request, routes::session::password_reset_request,
routes::session::password_reset_form, routes::session::password_reset_form,
routes::session::password_reset, routes::session::password_reset,
routes::plume_static_files, routes::plume_static_files,
routes::static_files, routes::static_files,
routes::tags::tag, routes::tags::tag,
routes::user::me, routes::user::me,
routes::user::details, routes::user::details,
routes::user::dashboard, routes::user::dashboard,
@ -216,22 +225,22 @@ Then try to restart Plume.
routes::user::new, routes::user::new,
routes::user::create, routes::user::create,
routes::user::atom_feed, routes::user::atom_feed,
routes::well_known::host_meta, routes::well_known::host_meta,
routes::well_known::nodeinfo, routes::well_known::nodeinfo,
routes::well_known::webfinger, routes::well_known::webfinger,
routes::errors::csrf_violation routes::errors::csrf_violation
]) ],
.mount("/api/v1", routes![ )
.mount(
"/api/v1",
routes![
api::oauth, api::oauth,
api::apps::create, api::apps::create,
api::posts::get, api::posts::get,
api::posts::list, api::posts::list,
api::posts::create, api::posts::create,
]) ],
)
.register(catchers![ .register(catchers![
routes::errors::not_found, routes::errors::not_found,
routes::errors::unprocessable_entity, routes::errors::unprocessable_entity,
@ -243,15 +252,41 @@ Then try to restart Plume.
.manage(workpool) .manage(workpool)
.manage(searcher) .manage(searcher)
.manage(include_i18n!()) .manage(include_i18n!())
.attach(CsrfFairingBuilder::new() .attach(
.set_default_target("/csrf-violation?target=<uri>".to_owned(), rocket::http::Method::Post) CsrfFairingBuilder::new()
.set_default_target(
"/csrf-violation?target=<uri>".to_owned(),
rocket::http::Method::Post,
)
.add_exceptions(vec![ .add_exceptions(vec![
("/inbox".to_owned(), "/inbox".to_owned(), rocket::http::Method::Post), (
("/@/<name>/inbox".to_owned(), "/@/<name>/inbox".to_owned(), rocket::http::Method::Post), "/inbox".to_owned(),
("/login".to_owned(), "/login".to_owned(), rocket::http::Method::Post), "/inbox".to_owned(),
("/users/new".to_owned(), "/users/new".to_owned(), rocket::http::Method::Post), rocket::http::Method::Post,
("/api/<path..>".to_owned(), "/api/<path..>".to_owned(), rocket::http::Method::Post) ),
(
"/@/<name>/inbox".to_owned(),
"/@/<name>/inbox".to_owned(),
rocket::http::Method::Post,
),
(
"/login".to_owned(),
"/login".to_owned(),
rocket::http::Method::Post,
),
(
"/users/new".to_owned(),
"/users/new".to_owned(),
rocket::http::Method::Post,
),
(
"/api/<path..>".to_owned(),
"/api/<path..>".to_owned(),
rocket::http::Method::Post,
),
]) ])
.finalize().expect("main: csrf fairing creation error")) .finalize()
.expect("main: csrf fairing creation error"),
)
.launch(); .launch();
} }

View File

@ -3,22 +3,16 @@ use atom_syndication::{Entry, FeedBuilder};
use rocket::{ use rocket::{
http::ContentType, http::ContentType,
request::LenientForm, request::LenientForm,
response::{Redirect, Flash, content::Content} response::{content::Content, Flash, Redirect},
}; };
use rocket_i18n::I18n; use rocket_i18n::I18n;
use std::{collections::HashMap, borrow::Cow}; use std::{borrow::Cow, collections::HashMap};
use validator::{Validate, ValidationError, ValidationErrors}; use validator::{Validate, ValidationError, ValidationErrors};
use plume_common::activity_pub::{ActivityStream, ApRequest}; use plume_common::activity_pub::{ActivityStream, ApRequest};
use plume_common::utils; use plume_common::utils;
use plume_models::{ use plume_models::{blog_authors::*, blogs::*, db_conn::DbConn, instance::Instance, posts::Post};
blog_authors::*, use routes::{errors::ErrorPage, Page, PlumeRocket};
blogs::*,
db_conn::DbConn,
instance::Instance,
posts::Post,
};
use routes::{Page, PlumeRocket, errors::ErrorPage};
use template_utils::Ructe; use template_utils::Ructe;
#[get("/~/<name>?<page>", rank = 2)] #[get("/~/<name>?<page>", rank = 2)]
@ -39,13 +33,18 @@ pub fn details(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result
articles_count, articles_count,
page.0, page.0,
Page::total(articles_count as i32), Page::total(articles_count as i32),
user.and_then(|x| x.is_author_in(&*conn, &blog).ok()).unwrap_or(false), user.and_then(|x| x.is_author_in(&*conn, &blog).ok())
.unwrap_or(false),
posts posts
))) )))
} }
#[get("/~/<name>", rank = 1)] #[get("/~/<name>", rank = 1)]
pub fn activity_details(name: String, conn: DbConn, _ap: ApRequest) -> Option<ActivityStream<CustomGroup>> { pub fn activity_details(
name: String,
conn: DbConn,
_ap: ApRequest,
) -> Option<ActivityStream<CustomGroup>> {
let blog = Blog::find_by_fqn(&*conn, &name).ok()?; let blog = Blog::find_by_fqn(&*conn, &name).ok()?;
Some(ActivityStream::new(blog.to_activity(&*conn).ok()?)) Some(ActivityStream::new(blog.to_activity(&*conn).ok()?))
} }
@ -64,10 +63,13 @@ pub fn new(rockets: PlumeRocket) -> Ructe {
} }
#[get("/blogs/new", rank = 2)] #[get("/blogs/new", rank = 2)]
pub fn new_auth(i18n: I18n) -> Flash<Redirect>{ pub fn new_auth(i18n: I18n) -> Flash<Redirect> {
utils::requires_login( utils::requires_login(
&i18n!(i18n.catalog, "You need to be logged in order to create a new blog"), &i18n!(
uri!(new) i18n.catalog,
"You need to be logged in order to create a new blog"
),
uri!(new),
) )
} }
@ -95,29 +97,43 @@ pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> Result<Re
let mut errors = match form.validate() { let mut errors = match form.validate() {
Ok(_) => ValidationErrors::new(), Ok(_) => ValidationErrors::new(),
Err(e) => e Err(e) => e,
}; };
if Blog::find_by_fqn(&*conn, &slug).is_ok() { if Blog::find_by_fqn(&*conn, &slug).is_ok() {
errors.add("title", ValidationError { errors.add(
"title",
ValidationError {
code: Cow::from("existing_slug"), code: Cow::from("existing_slug"),
message: Some(Cow::from("A blog with the same name already exists.")), message: Some(Cow::from("A blog with the same name already exists.")),
params: HashMap::new() params: HashMap::new(),
}); },
);
} }
if errors.is_empty() { if errors.is_empty() {
let blog = Blog::insert(&*conn, NewBlog::new_local( let blog = Blog::insert(
&*conn,
NewBlog::new_local(
slug.clone(), slug.clone(),
form.title.to_string(), form.title.to_string(),
String::from(""), String::from(""),
Instance::get_local(&*conn).expect("blog::create: instance error").id Instance::get_local(&*conn)
).expect("blog::create: new local error")).expect("blog::create: error"); .expect("blog::create: instance error")
.id,
)
.expect("blog::create: new local error"),
)
.expect("blog::create: error");
BlogAuthor::insert(&*conn, NewBlogAuthor { BlogAuthor::insert(
&*conn,
NewBlogAuthor {
blog_id: blog.id, blog_id: blog.id,
author_id: user.id, author_id: user.id,
is_owner: true is_owner: true,
}).expect("blog::create: author error"); },
)
.expect("blog::create: author error");
Ok(Redirect::to(uri!(details: name = slug.clone(), page = _))) Ok(Redirect::to(uri!(details: name = slug.clone(), page = _)))
} else { } else {
@ -130,15 +146,20 @@ pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> Result<Re
} }
#[post("/~/<name>/delete")] #[post("/~/<name>/delete")]
pub fn delete(name: String, rockets: PlumeRocket) -> Result<Redirect, Ructe>{ pub fn delete(name: String, rockets: PlumeRocket) -> Result<Redirect, Ructe> {
let conn = rockets.conn; let conn = rockets.conn;
let blog = Blog::find_by_fqn(&*conn, &name).expect("blog::delete: blog not found"); let blog = Blog::find_by_fqn(&*conn, &name).expect("blog::delete: blog not found");
let user = rockets.user; let user = rockets.user;
let intl = rockets.intl; let intl = rockets.intl;
let searcher = rockets.searcher; let searcher = rockets.searcher;
if user.clone().and_then(|u| u.is_author_in(&*conn, &blog).ok()).unwrap_or(false) { if user
blog.delete(&conn, &searcher).expect("blog::expect: deletion error"); .clone()
.and_then(|u| u.is_author_in(&*conn, &blog).ok())
.unwrap_or(false)
{
blog.delete(&conn, &searcher)
.expect("blog::expect: deletion error");
Ok(Redirect::to(uri!(super::instance::index))) Ok(Redirect::to(uri!(super::instance::index)))
} else { } else {
// TODO actually return 403 error code // TODO actually return 403 error code
@ -160,12 +181,20 @@ pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
let blog = Blog::find_by_fqn(&*conn, &name).ok()?; let blog = Blog::find_by_fqn(&*conn, &name).ok()?;
let feed = FeedBuilder::default() let feed = FeedBuilder::default()
.title(blog.title.clone()) .title(blog.title.clone())
.id(Instance::get_local(&*conn).ok()? .id(Instance::get_local(&*conn)
.ok()?
.compute_box("~", &name, "atom.xml")) .compute_box("~", &name, "atom.xml"))
.entries(Post::get_recents_for_blog(&*conn, &blog, 15).ok()? .entries(
Post::get_recents_for_blog(&*conn, &blog, 15)
.ok()?
.into_iter() .into_iter()
.map(|p| super::post_to_atom(p, &*conn)) .map(|p| super::post_to_atom(p, &*conn))
.collect::<Vec<Entry>>()) .collect::<Vec<Entry>>(),
.build().ok()?; )
Some(Content(ContentType::new("application", "atom+xml"), feed.to_string())) .build()
.ok()?;
Some(Content(
ContentType::new("application", "atom+xml"),
feed.to_string(),
))
} }

View File

@ -1,29 +1,21 @@
use activitypub::object::Note; use activitypub::object::Note;
use rocket::{ use rocket::{request::LenientForm, response::Redirect};
request::LenientForm,
response::Redirect
};
use rocket_i18n::I18n; use rocket_i18n::I18n;
use validator::Validate;
use template_utils::Ructe; use template_utils::Ructe;
use validator::Validate;
use std::time::Duration; use std::time::Duration;
use plume_common::{utils, activity_pub::{broadcast, ApRequest, use plume_common::{
ActivityStream, inbox::Deletable}}; activity_pub::{broadcast, inbox::Deletable, ActivityStream, ApRequest},
use plume_models::{ utils,
blogs::Blog, };
comments::*, use plume_models::{
db_conn::DbConn, blogs::Blog, comments::*, db_conn::DbConn, instance::Instance, mentions::Mention, posts::Post,
instance::Instance, safe_string::SafeString, tags::Tag, users::User,
mentions::Mention,
posts::Post,
safe_string::SafeString,
tags::Tag,
users::User
}; };
use Worker;
use routes::errors::ErrorPage; use routes::errors::ErrorPage;
use Worker;
#[derive(Default, FromForm, Debug, Validate)] #[derive(Default, FromForm, Debug, Validate)]
pub struct NewCommentForm { pub struct NewCommentForm {
@ -34,17 +26,28 @@ pub struct NewCommentForm {
} }
#[post("/~/<blog_name>/<slug>/comment", data = "<form>")] #[post("/~/<blog_name>/<slug>/comment", data = "<form>")]
pub fn create(blog_name: String, slug: String, form: LenientForm<NewCommentForm>, user: User, conn: DbConn, worker: Worker, intl: I18n) pub fn create(
-> Result<Redirect, Ructe> { blog_name: String,
slug: String,
form: LenientForm<NewCommentForm>,
user: User,
conn: DbConn,
worker: Worker,
intl: I18n,
) -> Result<Redirect, Ructe> {
let blog = Blog::find_by_fqn(&*conn, &blog_name).expect("comments::create: blog error"); let blog = Blog::find_by_fqn(&*conn, &blog_name).expect("comments::create: blog error");
let post = Post::find_by_slug(&*conn, &slug, blog.id).expect("comments::create: post error"); let post = Post::find_by_slug(&*conn, &slug, blog.id).expect("comments::create: post error");
form.validate() form.validate()
.map(|_| { .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(),
&Instance::get_local(&conn).expect("comments::create: local instance error").public_domain &Instance::get_local(&conn)
.expect("comments::create: local instance error")
.public_domain,
); );
let comm = Comment::insert(&*conn, NewComment { let comm = Comment::insert(
&*conn,
NewComment {
content: SafeString::new(html.as_ref()), content: SafeString::new(html.as_ref()),
in_response_to_id: form.responding_to, in_response_to_id: form.responding_to,
post_id: post.id, post_id: post.id,
@ -52,19 +55,25 @@ pub fn create(blog_name: String, slug: String, form: LenientForm<NewCommentForm>
ap_url: None, ap_url: None,
sensitive: !form.warning.is_empty(), sensitive: !form.warning.is_empty(),
spoiler_text: form.warning.clone(), spoiler_text: form.warning.clone(),
public_visibility: true public_visibility: true,
}).expect("comments::create: insert error"); },
let new_comment = comm.create_activity(&*conn).expect("comments::create: activity error"); )
.expect("comments::create: insert error");
let new_comment = comm
.create_activity(&*conn)
.expect("comments::create: activity error");
// save mentions // save mentions
for ment in mentions { for ment in mentions {
Mention::from_activity( Mention::from_activity(
&*conn, &*conn,
&Mention::build_activity(&*conn, &ment).expect("comments::create: build mention error"), &Mention::build_activity(&*conn, &ment)
.expect("comments::create: build mention error"),
post.id, post.id,
true, true,
true true,
).expect("comments::create: mention save error"); )
.expect("comments::create: mention save error");
} }
// federate // federate
@ -72,13 +81,18 @@ pub fn create(blog_name: String, slug: String, form: LenientForm<NewCommentForm>
let user_clone = user.clone(); let user_clone = user.clone();
worker.execute(move || broadcast(&user_clone, new_comment, dest)); worker.execute(move || broadcast(&user_clone, new_comment, dest));
Redirect::to(uri!(super::posts::details: blog = blog_name, slug = slug, responding_to = _)) Redirect::to(
uri!(super::posts::details: blog = blog_name, slug = slug, responding_to = _),
)
}) })
.map_err(|errors| { .map_err(|errors| {
// TODO: de-duplicate this code // TODO: de-duplicate this code
let comments = CommentTree::from_post(&*conn, &post, Some(&user)).expect("comments::create: comments error"); let comments = CommentTree::from_post(&*conn, &post, Some(&user))
.expect("comments::create: comments error");
let previous = form.responding_to.and_then(|r| Comment::get(&*conn, r).ok()); let previous = form
.responding_to
.and_then(|r| Comment::get(&*conn, r).ok());
render!(posts::details( render!(posts::details(
&(&*conn, &intl.catalog, Some(user.clone())), &(&*conn, &intl.catalog, Some(user.clone())),
@ -89,33 +103,62 @@ pub fn create(blog_name: String, slug: String, form: LenientForm<NewCommentForm>
Tag::for_post(&*conn, post.id).expect("comments::create: tags error"), Tag::for_post(&*conn, post.id).expect("comments::create: tags error"),
comments, comments,
previous, previous,
post.count_likes(&*conn).expect("comments::create: count likes error"), post.count_likes(&*conn)
post.count_reshares(&*conn).expect("comments::create: count reshares error"), .expect("comments::create: count likes error"),
user.has_liked(&*conn, &post).expect("comments::create: liked error"), post.count_reshares(&*conn)
user.has_reshared(&*conn, &post).expect("comments::create: reshared error"), .expect("comments::create: count reshares error"),
user.is_following(&*conn, post.get_authors(&*conn).expect("comments::create: authors error")[0].id) user.has_liked(&*conn, &post)
.expect("comments::create: liked error"),
user.has_reshared(&*conn, &post)
.expect("comments::create: reshared error"),
user.is_following(
&*conn,
post.get_authors(&*conn)
.expect("comments::create: authors error")[0]
.id
)
.expect("comments::create: following error"), .expect("comments::create: following error"),
post.get_authors(&*conn).expect("comments::create: authors error")[0].clone() post.get_authors(&*conn)
.expect("comments::create: authors error")[0]
.clone()
)) ))
}) })
} }
#[post("/~/<blog>/<slug>/comment/<id>/delete")] #[post("/~/<blog>/<slug>/comment/<id>/delete")]
pub fn delete(blog: String, slug: String, id: i32, user: User, conn: DbConn, worker: Worker) -> Result<Redirect, ErrorPage> { pub fn delete(
blog: String,
slug: String,
id: i32,
user: User,
conn: DbConn,
worker: Worker,
) -> Result<Redirect, ErrorPage> {
if let Ok(comment) = Comment::get(&*conn, id) { if let Ok(comment) = Comment::get(&*conn, id) {
if comment.author_id == user.id { if comment.author_id == user.id {
let dest = User::one_by_instance(&*conn)?; let dest = User::one_by_instance(&*conn)?;
let delete_activity = comment.delete(&*conn)?; let delete_activity = comment.delete(&*conn)?;
let user_c = user.clone(); let user_c = user.clone();
worker.execute(move || broadcast(&user_c, delete_activity, dest)); worker.execute(move || broadcast(&user_c, delete_activity, dest));
worker.execute_after(Duration::from_secs(10*60), move || {user.rotate_keypair(&conn).expect("Failed to rotate keypair");}); worker.execute_after(Duration::from_secs(10 * 60), move || {
user.rotate_keypair(&conn)
.expect("Failed to rotate keypair");
});
} }
} }
Ok(Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _))) Ok(Redirect::to(
uri!(super::posts::details: blog = blog, slug = slug, responding_to = _),
))
} }
#[get("/~/<_blog>/<_slug>/comment/<id>")] #[get("/~/<_blog>/<_slug>/comment/<id>")]
pub fn activity_pub(_blog: String, _slug: String, id: i32, _ap: ApRequest, conn: DbConn) -> Option<ActivityStream<Note>> { pub fn activity_pub(
_blog: String,
_slug: String,
id: i32,
_ap: ApRequest,
conn: DbConn,
) -> Option<ActivityStream<Note>> {
Comment::get(&*conn, id) Comment::get(&*conn, id)
.and_then(|c| c.to_activity(&*conn)) .and_then(|c| c.to_activity(&*conn))
.ok() .ok()

View File

@ -1,11 +1,11 @@
use plume_models::users::User;
use plume_models::{db_conn::DbConn, Error};
use rocket::{ use rocket::{
Request,
request::FromRequest, request::FromRequest,
response::{self, Responder}, response::{self, Responder},
Request,
}; };
use rocket_i18n::I18n; use rocket_i18n::I18n;
use plume_models::{Error, db_conn::DbConn};
use plume_models::users::User;
use template_utils::Ructe; use template_utils::Ructe;
#[derive(Debug)] #[derive(Debug)]
@ -24,15 +24,24 @@ impl<'r> Responder<'r> for ErrorPage {
let user = User::from_request(req).succeeded(); let user = User::from_request(req).succeeded();
match self.0 { match self.0 {
Error::NotFound => render!(errors::not_found( Error::NotFound => render!(errors::not_found(&(
&(&*conn.unwrap(), &intl.unwrap().catalog, user) &*conn.unwrap(),
)).respond_to(req), &intl.unwrap().catalog,
Error::Unauthorized => render!(errors::not_found( user
&(&*conn.unwrap(), &intl.unwrap().catalog, user) )))
)).respond_to(req), .respond_to(req),
_ => render!(errors::not_found( Error::Unauthorized => render!(errors::not_found(&(
&(&*conn.unwrap(), &intl.unwrap().catalog, user) &*conn.unwrap(),
)).respond_to(req) &intl.unwrap().catalog,
user
)))
.respond_to(req),
_ => render!(errors::not_found(&(
&*conn.unwrap(),
&intl.unwrap().catalog,
user
)))
.respond_to(req),
} }
} }
} }
@ -42,9 +51,11 @@ pub fn not_found(req: &Request) -> Ructe {
let conn = req.guard::<DbConn>().succeeded(); let conn = req.guard::<DbConn>().succeeded();
let intl = req.guard::<I18n>().succeeded(); let intl = req.guard::<I18n>().succeeded();
let user = User::from_request(req).succeeded(); let user = User::from_request(req).succeeded();
render!(errors::not_found( render!(errors::not_found(&(
&(&*conn.unwrap(), &intl.unwrap().catalog, user) &*conn.unwrap(),
)) &intl.unwrap().catalog,
user
)))
} }
#[catch(422)] #[catch(422)]
@ -52,9 +63,11 @@ pub fn unprocessable_entity(req: &Request) -> Ructe {
let conn = req.guard::<DbConn>().succeeded(); let conn = req.guard::<DbConn>().succeeded();
let intl = req.guard::<I18n>().succeeded(); let intl = req.guard::<I18n>().succeeded();
let user = User::from_request(req).succeeded(); let user = User::from_request(req).succeeded();
render!(errors::unprocessable_entity( render!(errors::unprocessable_entity(&(
&(&*conn.unwrap(), &intl.unwrap().catalog, user) &*conn.unwrap(),
)) &intl.unwrap().catalog,
user
)))
} }
#[catch(500)] #[catch(500)]
@ -62,17 +75,22 @@ pub fn server_error(req: &Request) -> Ructe {
let conn = req.guard::<DbConn>().succeeded(); let conn = req.guard::<DbConn>().succeeded();
let intl = req.guard::<I18n>().succeeded(); let intl = req.guard::<I18n>().succeeded();
let user = User::from_request(req).succeeded(); let user = User::from_request(req).succeeded();
render!(errors::server_error( render!(errors::server_error(&(
&(&*conn.unwrap(), &intl.unwrap().catalog, user) &*conn.unwrap(),
)) &intl.unwrap().catalog,
user
)))
} }
#[post("/csrf-violation?<target>")] #[post("/csrf-violation?<target>")]
pub fn csrf_violation(target: Option<String>, conn: DbConn, intl: I18n, user: Option<User>) -> Ructe { pub fn csrf_violation(
target: Option<String>,
conn: DbConn,
intl: I18n,
user: Option<User>,
) -> Ructe {
if let Some(uri) = target { if let Some(uri) = target {
eprintln!("Csrf violation while acceding \"{}\"", uri) eprintln!("Csrf violation while acceding \"{}\"", uri)
} }
render!(errors::csrf( render!(errors::csrf(&(&*conn, &intl.catalog, user)))
&(&*conn, &intl.catalog, user)
))
} }

View File

@ -1,24 +1,19 @@
use rocket::{request::LenientForm, response::{status, Redirect}}; use rocket::{
request::LenientForm,
response::{status, Redirect},
};
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
use rocket_i18n::I18n; use rocket_i18n::I18n;
use serde_json; use serde_json;
use validator::{Validate, ValidationErrors}; use validator::{Validate, ValidationErrors};
use plume_common::activity_pub::sign::{Signable,
verify_http_headers};
use plume_models::{
admin::Admin,
comments::Comment,
db_conn::DbConn,
Error,
headers::Headers,
posts::Post,
users::User,
safe_string::SafeString,
instance::*
};
use inbox::{Inbox, SignedJson}; use inbox::{Inbox, SignedJson};
use routes::{Page, errors::ErrorPage}; use plume_common::activity_pub::sign::{verify_http_headers, Signable};
use plume_models::{
admin::Admin, comments::Comment, db_conn::DbConn, headers::Headers, instance::*, posts::Post,
safe_string::SafeString, users::User, Error,
};
use routes::{errors::ErrorPage, Page};
use template_utils::Ructe; use template_utils::Ructe;
use Searcher; use Searcher;
@ -46,7 +41,12 @@ pub fn index(conn: DbConn, user: Option<User>, intl: I18n) -> Result<Ructe, Erro
} }
#[get("/local?<page>")] #[get("/local?<page>")]
pub fn local(conn: DbConn, user: Option<User>, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> { pub fn local(
conn: DbConn,
user: Option<User>,
page: Option<Page>,
intl: I18n,
) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
let instance = Instance::get_local(&*conn)?; let instance = Instance::get_local(&*conn)?;
let articles = Post::get_instance_page(&*conn, instance.id, page.limits())?; let articles = Post::get_instance_page(&*conn, instance.id, page.limits())?;
@ -75,7 +75,12 @@ pub fn feed(conn: DbConn, user: User, page: Option<Page>, intl: I18n) -> Result<
} }
#[get("/federated?<page>")] #[get("/federated?<page>")]
pub fn federated(conn: DbConn, user: Option<User>, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> { pub fn federated(
conn: DbConn,
user: Option<User>,
page: Option<Page>,
intl: I18n,
) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
let articles = Post::get_recents_page(&*conn, page.limits())?; let articles = Post::get_recents_page(&*conn, page.limits())?;
Ok(render!(instance::federated( Ok(render!(instance::federated(
@ -111,23 +116,34 @@ pub struct InstanceSettingsForm {
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,
} }
#[post("/admin", data = "<form>")] #[post("/admin", data = "<form>")]
pub fn update_settings(conn: DbConn, admin: Admin, form: LenientForm<InstanceSettingsForm>, intl: I18n) -> Result<Redirect, Ructe> { pub fn update_settings(
conn: DbConn,
admin: Admin,
form: LenientForm<InstanceSettingsForm>,
intl: I18n,
) -> Result<Redirect, Ructe> {
form.validate() form.validate()
.and_then(|_| { .and_then(|_| {
let instance = Instance::get_local(&*conn).expect("instance::update_settings: local instance error"); let instance = Instance::get_local(&*conn)
instance.update(&*conn, .expect("instance::update_settings: local instance error");
instance
.update(
&*conn,
form.name.clone(), form.name.clone(),
form.open_registrations, form.open_registrations,
form.short_description.clone(), form.short_description.clone(),
form.long_description.clone()).expect("instance::update_settings: save error"); form.long_description.clone(),
)
.expect("instance::update_settings: save error");
Ok(Redirect::to(uri!(admin))) Ok(Redirect::to(uri!(admin)))
}) })
.or_else(|e| { .or_else(|e| {
let local_inst = Instance::get_local(&*conn).expect("instance::update_settings: local instance error"); let local_inst = Instance::get_local(&*conn)
.expect("instance::update_settings: local instance error");
Err(render!(instance::admin( Err(render!(instance::admin(
&(&*conn, &intl.catalog, Some(admin.0)), &(&*conn, &intl.catalog, Some(admin.0)),
local_inst, local_inst,
@ -138,7 +154,12 @@ pub fn update_settings(conn: DbConn, admin: Admin, form: LenientForm<InstanceSet
} }
#[get("/admin/instances?<page>")] #[get("/admin/instances?<page>")]
pub fn admin_instances(admin: Admin, conn: DbConn, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> { pub fn admin_instances(
admin: Admin,
conn: DbConn,
page: Option<Page>,
intl: I18n,
) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
let instances = Instance::page(&*conn, page.limits())?; let instances = Instance::page(&*conn, page.limits())?;
Ok(render!(instance::list( Ok(render!(instance::list(
@ -160,7 +181,12 @@ pub fn toggle_block(_admin: Admin, conn: DbConn, id: i32) -> Result<Redirect, Er
} }
#[get("/admin/users?<page>")] #[get("/admin/users?<page>")]
pub fn admin_users(admin: Admin, conn: DbConn, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> { pub fn admin_users(
admin: Admin,
conn: DbConn,
page: Option<Page>,
intl: I18n,
) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
Ok(render!(instance::users( Ok(render!(instance::users(
&(&*conn, &intl.catalog, Some(admin.0)), &(&*conn, &intl.catalog, Some(admin.0)),
@ -171,7 +197,12 @@ pub fn admin_users(admin: Admin, conn: DbConn, page: Option<Page>, intl: I18n) -
} }
#[post("/admin/users/<id>/ban")] #[post("/admin/users/<id>/ban")]
pub fn ban(_admin: Admin, conn: DbConn, id: i32, searcher: Searcher) -> Result<Redirect, ErrorPage> { pub fn ban(
_admin: Admin,
conn: DbConn,
id: i32,
searcher: Searcher,
) -> Result<Redirect, ErrorPage> {
if let Ok(u) = User::get(&*conn, id) { if let Ok(u) = User::get(&*conn, id) {
u.delete(&*conn, &searcher)?; u.delete(&*conn, &searcher)?;
} }
@ -179,34 +210,50 @@ pub fn ban(_admin: Admin, conn: DbConn, id: i32, searcher: Searcher) -> Result<R
} }
#[post("/inbox", data = "<data>")] #[post("/inbox", data = "<data>")]
pub fn shared_inbox(conn: DbConn, data: SignedJson<serde_json::Value>, headers: Headers, searcher: Searcher) -> Result<String, status::BadRequest<&'static str>> { pub fn shared_inbox(
conn: DbConn,
data: SignedJson<serde_json::Value>,
headers: Headers,
searcher: Searcher,
) -> Result<String, status::BadRequest<&'static str>> {
let act = data.1.into_inner(); let act = data.1.into_inner();
let sig = data.0; let sig = data.0;
let activity = act.clone(); let activity = act.clone();
let actor_id = activity["actor"].as_str() let actor_id = activity["actor"]
.or_else(|| activity["actor"]["id"].as_str()).ok_or(status::BadRequest(Some("Missing actor id for activity")))?; .as_str()
.or_else(|| activity["actor"]["id"].as_str())
.ok_or(status::BadRequest(Some("Missing actor id for activity")))?;
let actor = User::from_url(&conn, actor_id).expect("instance::shared_inbox: user error"); let actor = User::from_url(&conn, actor_id).expect("instance::shared_inbox: user error");
if !verify_http_headers(&actor, &headers.0, &sig).is_secure() && if !verify_http_headers(&actor, &headers.0, &sig).is_secure() && !act.clone().verify(&actor) {
!act.clone().verify(&actor) {
// maybe we just know an old key? // maybe we just know an old key?
actor.refetch(&conn).and_then(|_| User::get(&conn, actor.id)) actor
.and_then(|u| if verify_http_headers(&u, &headers.0, &sig).is_secure() || .refetch(&conn)
act.clone().verify(&u) { .and_then(|_| User::get(&conn, actor.id))
.and_then(|u| {
if verify_http_headers(&u, &headers.0, &sig).is_secure() || act.clone().verify(&u) {
Ok(()) Ok(())
} else { } else {
Err(Error::Signature) Err(Error::Signature)
}
}) })
.map_err(|_| { .map_err(|_| {
println!("Rejected invalid activity supposedly from {}, with headers {:?}", actor.username, headers.0); println!(
status::BadRequest(Some("Invalid signature"))})?; "Rejected invalid activity supposedly from {}, with headers {:?}",
actor.username, headers.0
);
status::BadRequest(Some("Invalid signature"))
})?;
} }
if Instance::is_blocked(&*conn, actor_id).map_err(|_| status::BadRequest(Some("Can't tell if instance is blocked")))? { if Instance::is_blocked(&*conn, actor_id)
.map_err(|_| status::BadRequest(Some("Can't tell if instance is blocked")))?
{
return Ok(String::new()); return Ok(String::new());
} }
let instance = Instance::get_local(&*conn).expect("instance::shared_inbox: local instance not found error"); let instance = Instance::get_local(&*conn)
.expect("instance::shared_inbox: local instance not found error");
Ok(match instance.received(&*conn, &searcher, act) { Ok(match instance.received(&*conn, &searcher, act) {
Ok(_) => String::new(), Ok(_) => String::new(),
Err(e) => { Err(e) => {

View File

@ -1,25 +1,28 @@
use rocket::response::{Redirect, Flash}; use rocket::response::{Flash, Redirect};
use rocket_i18n::I18n; use rocket_i18n::I18n;
use plume_common::activity_pub::{broadcast, inbox::{Notify, Deletable}}; use plume_common::activity_pub::{
use plume_common::utils; broadcast,
use plume_models::{ inbox::{Deletable, Notify},
blogs::Blog,
db_conn::DbConn,
likes,
posts::Post,
users::User
}; };
use Worker; use plume_common::utils;
use plume_models::{blogs::Blog, db_conn::DbConn, likes, posts::Post, users::User};
use routes::errors::ErrorPage; use routes::errors::ErrorPage;
use Worker;
#[post("/~/<blog>/<slug>/like")] #[post("/~/<blog>/<slug>/like")]
pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Result<Redirect, ErrorPage> { pub fn create(
blog: String,
slug: String,
user: User,
conn: DbConn,
worker: Worker,
) -> Result<Redirect, ErrorPage> {
let b = Blog::find_by_fqn(&*conn, &blog)?; let b = Blog::find_by_fqn(&*conn, &blog)?;
let post = Post::find_by_slug(&*conn, &slug, b.id)?; let post = Post::find_by_slug(&*conn, &slug, b.id)?;
if !user.has_liked(&*conn, &post)? { if !user.has_liked(&*conn, &post)? {
let like = likes::Like::insert(&*conn, likes::NewLike::new(&post ,&user))?; let like = likes::Like::insert(&*conn, likes::NewLike::new(&post, &user))?;
like.notify(&*conn)?; like.notify(&*conn)?;
let dest = User::one_by_instance(&*conn)?; let dest = User::one_by_instance(&*conn)?;
@ -32,13 +35,18 @@ pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Work
worker.execute(move || broadcast(&user, delete_act, dest)); worker.execute(move || broadcast(&user, delete_act, dest));
} }
Ok(Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _))) Ok(Redirect::to(
uri!(super::posts::details: blog = blog, slug = slug, responding_to = _),
))
} }
#[post("/~/<blog>/<slug>/like", rank = 2)] #[post("/~/<blog>/<slug>/like", rank = 2)]
pub fn create_auth(blog: String, slug: String, i18n: I18n) -> Flash<Redirect>{ pub fn create_auth(blog: String, slug: String, i18n: I18n) -> Flash<Redirect> {
utils::requires_login( utils::requires_login(
&i18n!(i18n.catalog, "You need to be logged in order to like a post"), &i18n!(
uri!(create: blog = blog, slug = slug) i18n.catalog,
"You need to be logged in order to like a post"
),
uri!(create: blog = blog, slug = slug),
) )
} }

View File

@ -1,11 +1,18 @@
use guid_create::GUID; use guid_create::GUID;
use multipart::server::{Multipart, save::{SavedData, SaveResult}}; use multipart::server::{
use rocket::{Data, http::ContentType, response::{Redirect, status}}; save::{SaveResult, SavedData},
Multipart,
};
use plume_models::{db_conn::DbConn, medias::*, users::User, Error};
use rocket::{
http::ContentType,
response::{status, Redirect},
Data,
};
use rocket_i18n::I18n; use rocket_i18n::I18n;
use routes::{errors::ErrorPage, Page};
use std::fs; use std::fs;
use plume_models::{Error, db_conn::DbConn, medias::*, users::User};
use template_utils::Ructe; use template_utils::Ructe;
use routes::{Page, errors::ErrorPage};
#[get("/medias?<page>")] #[get("/medias?<page>")]
pub fn list(user: User, conn: DbConn, intl: I18n, page: Option<Page>) -> Result<Ructe, ErrorPage> { pub fn list(user: User, conn: DbConn, intl: I18n, page: Option<Page>) -> Result<Ructe, ErrorPage> {
@ -21,47 +28,68 @@ pub fn list(user: User, conn: DbConn, intl: I18n, page: Option<Page>) -> Result<
#[get("/medias/new")] #[get("/medias/new")]
pub fn new(user: User, conn: DbConn, intl: I18n) -> Ructe { pub fn new(user: User, conn: DbConn, intl: I18n) -> Ructe {
render!(medias::new( render!(medias::new(&(&*conn, &intl.catalog, Some(user))))
&(&*conn, &intl.catalog, Some(user))
))
} }
#[post("/medias/new", data = "<data>")] #[post("/medias/new", data = "<data>")]
pub fn upload(user: User, data: Data, ct: &ContentType, conn: DbConn) -> Result<Redirect, status::BadRequest<&'static str>> { pub fn upload(
user: User,
data: Data,
ct: &ContentType,
conn: DbConn,
) -> Result<Redirect, status::BadRequest<&'static str>> {
if ct.is_form_data() { if ct.is_form_data() {
let (_, boundary) = ct.params().find(|&(k, _)| k == "boundary").ok_or_else(|| status::BadRequest(Some("No boundary")))?; let (_, boundary) = ct
.params()
.find(|&(k, _)| k == "boundary")
.ok_or_else(|| status::BadRequest(Some("No boundary")))?;
match Multipart::with_body(data.open(), boundary).save().temp() { match Multipart::with_body(data.open(), boundary).save().temp() {
SaveResult::Full(entries) => { SaveResult::Full(entries) => {
let fields = entries.fields; let fields = entries.fields;
let filename = fields.get("file").and_then(|v| v.iter().next()) let filename = fields
.ok_or_else(|| status::BadRequest(Some("No file uploaded")))?.headers .get("file")
.filename.clone(); .and_then(|v| v.iter().next())
.ok_or_else(|| status::BadRequest(Some("No file uploaded")))?
.headers
.filename
.clone();
// Remove extension if it contains something else than just letters and numbers // Remove extension if it contains something else than just letters and numbers
let ext = filename let ext = filename
.and_then(|f| f .and_then(|f| {
.rsplit('.') f.rsplit('.')
.next() .next()
.and_then(|ext| if ext.chars().any(|c| !c.is_alphanumeric()) { .and_then(|ext| {
if ext.chars().any(|c| !c.is_alphanumeric()) {
None None
} else { } else {
Some(ext.to_lowercase()) Some(ext.to_lowercase())
}
}) })
.map(|ext| format!(".{}", ext)) .map(|ext| format!(".{}", ext))
).unwrap_or_default(); })
.unwrap_or_default();
let dest = format!("static/media/{}{}", GUID::rand().to_string(), ext); let dest = format!("static/media/{}{}", GUID::rand().to_string(), ext);
match fields["file"][0].data { match fields["file"][0].data {
SavedData::Bytes(ref bytes) => fs::write(&dest, bytes).map_err(|_| status::BadRequest(Some("Couldn't save upload")))?, SavedData::Bytes(ref bytes) => fs::write(&dest, bytes)
SavedData::File(ref path, _) => {fs::copy(path, &dest).map_err(|_| status::BadRequest(Some("Couldn't copy upload")))?;}, .map_err(|_| status::BadRequest(Some("Couldn't save upload")))?,
SavedData::File(ref path, _) => {
fs::copy(path, &dest)
.map_err(|_| status::BadRequest(Some("Couldn't copy upload")))?;
}
_ => { _ => {
return Ok(Redirect::to(uri!(new))); return Ok(Redirect::to(uri!(new)));
} }
} }
let has_cw = !read(&fields["cw"][0].data).map(|cw| cw.is_empty()).unwrap_or(false); let has_cw = !read(&fields["cw"][0].data)
let media = Media::insert(&*conn, NewMedia { .map(|cw| cw.is_empty())
.unwrap_or(false);
let media = Media::insert(
&*conn,
NewMedia {
file_path: dest, file_path: dest,
alt_text: read(&fields["alt"][0].data)?, alt_text: read(&fields["alt"][0].data)?,
is_remote: false, is_remote: false,
@ -72,13 +100,13 @@ pub fn upload(user: User, data: Data, ct: &ContentType, conn: DbConn) -> Result<
} else { } else {
None None
}, },
owner_id: user.id owner_id: user.id,
}).map_err(|_| status::BadRequest(Some("Error while saving media")))?;
Ok(Redirect::to(uri!(details: id = media.id)))
}, },
SaveResult::Partial(_, _) | SaveResult::Error(_) => { )
Ok(Redirect::to(uri!(new))) .map_err(|_| status::BadRequest(Some("Error while saving media")))?;
Ok(Redirect::to(uri!(details: id = media.id)))
} }
SaveResult::Partial(_, _) | SaveResult::Error(_) => Ok(Redirect::to(uri!(new))),
} }
} else { } else {
Ok(Redirect::to(uri!(new))) Ok(Redirect::to(uri!(new)))

View File

@ -2,25 +2,21 @@
use atom_syndication::{ContentBuilder, Entry, EntryBuilder, LinkBuilder, Person, PersonBuilder}; use atom_syndication::{ContentBuilder, Entry, EntryBuilder, LinkBuilder, Person, PersonBuilder};
use rocket::{ use rocket::{
http::{ http::{
RawStr, Status, uri::{FromUriParam, Query}, hyper::header::{CacheControl, CacheDirective},
hyper::header::{CacheControl, CacheDirective} uri::{FromUriParam, Query},
RawStr, Status,
}, },
Outcome,
request::{self, FromFormValue, FromRequest, Request}, request::{self, FromFormValue, FromRequest, Request},
response::NamedFile, response::NamedFile,
Outcome,
}; };
use rocket_i18n::I18n; use rocket_i18n::I18n;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use plume_models::{ use plume_models::{db_conn::DbConn, posts::Post, users::User, Connection};
Connection,
users::User,
posts::Post,
db_conn::DbConn,
};
use Worker;
use Searcher; use Searcher;
use Worker;
pub struct PlumeRocket<'a> { pub struct PlumeRocket<'a> {
conn: DbConn, conn: DbConn,
@ -100,7 +96,6 @@ impl<'a, 'r> FromRequest<'a, 'r> for ContentLen {
} }
} }
impl Default for Page { impl Default for Page {
fn default() -> Self { fn default() -> Self {
Page(1) Page(1)
@ -110,20 +105,33 @@ impl Default for Page {
pub fn post_to_atom(post: Post, conn: &Connection) -> Entry { pub fn post_to_atom(post: Post, conn: &Connection) -> Entry {
EntryBuilder::default() EntryBuilder::default()
.title(format!("<![CDATA[{}]]>", post.title)) .title(format!("<![CDATA[{}]]>", post.title))
.content(ContentBuilder::default() .content(
ContentBuilder::default()
.value(format!("<![CDATA[{}]]>", *post.content.get())) .value(format!("<![CDATA[{}]]>", *post.content.get()))
.src(post.ap_url.clone()) .src(post.ap_url.clone())
.content_type("html".to_string()) .content_type("html".to_string())
.build().expect("Atom feed: content error")) .build()
.authors(post.get_authors(&*conn).expect("Atom feed: author error") .expect("Atom feed: content error"),
)
.authors(
post.get_authors(&*conn)
.expect("Atom feed: author error")
.into_iter() .into_iter()
.map(|a| PersonBuilder::default() .map(|a| {
PersonBuilder::default()
.name(a.display_name) .name(a.display_name)
.uri(a.ap_url) .uri(a.ap_url)
.build().expect("Atom feed: author error")) .build()
.collect::<Vec<Person>>()) .expect("Atom feed: author error")
.links(vec![LinkBuilder::default().href(post.ap_url).build().expect("Atom feed: link error")]) })
.build().expect("Atom feed: entry error") .collect::<Vec<Person>>(),
)
.links(vec![LinkBuilder::default()
.href(post.ap_url)
.build()
.expect("Atom feed: link error")])
.build()
.expect("Atom feed: entry error")
} }
pub mod blogs; pub mod blogs;
@ -135,17 +143,17 @@ pub mod medias;
pub mod notifications; pub mod notifications;
pub mod posts; pub mod posts;
pub mod reshares; pub mod reshares;
pub mod search;
pub mod session; pub mod session;
pub mod tags; pub mod tags;
pub mod user; pub mod user;
pub mod search;
pub mod well_known; pub mod well_known;
#[derive(Responder)] #[derive(Responder)]
#[response()] #[response()]
pub struct CachedFile { pub struct CachedFile {
inner: NamedFile, inner: NamedFile,
cache_control: CacheControl cache_control: CacheControl,
} }
#[get("/static/cached/<_build_id>/<file..>", rank = 2)] #[get("/static/cached/<_build_id>/<file..>", rank = 2)]
@ -155,10 +163,10 @@ pub fn plume_static_files(file: PathBuf, _build_id: &RawStr) -> Option<CachedFil
#[get("/static/<file..>", rank = 3)] #[get("/static/<file..>", rank = 3)]
pub fn static_files(file: PathBuf) -> Option<CachedFile> { pub fn static_files(file: PathBuf) -> Option<CachedFile> {
NamedFile::open(Path::new("static/").join(file)).ok() NamedFile::open(Path::new("static/").join(file))
.map(|f| .ok()
CachedFile { .map(|f| CachedFile {
inner: f, inner: f,
cache_control: CacheControl(vec![CacheDirective::MaxAge(60*60*24*30)]) cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]),
}) })
} }

View File

@ -1,13 +1,18 @@
use rocket::response::{Redirect, Flash}; use rocket::response::{Flash, Redirect};
use rocket_i18n::I18n; use rocket_i18n::I18n;
use plume_common::utils; use plume_common::utils;
use plume_models::{db_conn::DbConn, notifications::Notification, users::User}; use plume_models::{db_conn::DbConn, notifications::Notification, users::User};
use routes::{Page, errors::ErrorPage}; use routes::{errors::ErrorPage, Page};
use template_utils::Ructe; use template_utils::Ructe;
#[get("/notifications?<page>")] #[get("/notifications?<page>")]
pub fn notifications(conn: DbConn, user: User, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> { pub fn notifications(
conn: DbConn,
user: User,
page: Option<Page>,
intl: I18n,
) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
Ok(render!(notifications::index( Ok(render!(notifications::index(
&(&*conn, &intl.catalog, Some(user.clone())), &(&*conn, &intl.catalog, Some(user.clone())),
@ -18,9 +23,12 @@ pub fn notifications(conn: DbConn, user: User, page: Option<Page>, intl: I18n) -
} }
#[get("/notifications?<page>", rank = 2)] #[get("/notifications?<page>", rank = 2)]
pub fn notifications_auth(i18n: I18n, page: Option<Page>) -> Flash<Redirect>{ pub fn notifications_auth(i18n: I18n, page: Option<Page>) -> Flash<Redirect> {
utils::requires_login( utils::requires_login(
&i18n!(i18n.catalog, "You need to be logged in order to see your notifications"), &i18n!(
uri!(notifications: page = page) i18n.catalog,
"You need to be logged in order to see your notifications"
),
uri!(notifications: page = page),
) )
} }

View File

@ -1,20 +1,21 @@
use chrono::Utc; use chrono::Utc;
use heck::{CamelCase, KebabCase}; use heck::{CamelCase, KebabCase};
use rocket::request::LenientForm; use rocket::request::LenientForm;
use rocket::response::{Redirect, Flash}; use rocket::response::{Flash, Redirect};
use rocket_i18n::I18n; use rocket_i18n::I18n;
use std::{ use std::{
borrow::Cow,
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
borrow::Cow, time::Duration, time::Duration,
}; };
use validator::{Validate, ValidationError, ValidationErrors}; use validator::{Validate, ValidationError, ValidationErrors};
use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest, inbox::Deletable}; use plume_common::activity_pub::{broadcast, inbox::Deletable, ActivityStream, ApRequest};
use plume_common::utils; use plume_common::utils;
use plume_models::{ use plume_models::{
blogs::*, blogs::*,
db_conn::DbConn,
comments::{Comment, CommentTree}, comments::{Comment, CommentTree},
db_conn::DbConn,
instance::Instance, instance::Instance,
medias::Media, medias::Media,
mentions::Mention, mentions::Mention,
@ -22,16 +23,28 @@ use plume_models::{
posts::*, posts::*,
safe_string::SafeString, safe_string::SafeString,
tags::*, tags::*,
users::User users::User,
}; };
use routes::{PlumeRocket, errors::ErrorPage, comments::NewCommentForm, ContentLen}; use routes::{comments::NewCommentForm, errors::ErrorPage, ContentLen, PlumeRocket};
use template_utils::Ructe; use template_utils::Ructe;
#[get("/~/<blog>/<slug>?<responding_to>", rank = 4)] #[get("/~/<blog>/<slug>?<responding_to>", rank = 4)]
pub fn details(blog: String, slug: String, conn: DbConn, user: Option<User>, responding_to: Option<i32>, intl: I18n) -> Result<Ructe, ErrorPage> { pub fn details(
blog: String,
slug: String,
conn: DbConn,
user: Option<User>,
responding_to: Option<i32>,
intl: I18n,
) -> Result<Ructe, ErrorPage> {
let blog = Blog::find_by_fqn(&*conn, &blog)?; let blog = Blog::find_by_fqn(&*conn, &blog)?;
let post = Post::find_by_slug(&*conn, &slug, blog.id)?; let post = Post::find_by_slug(&*conn, &slug, blog.id)?;
if post.published || post.get_authors(&*conn)?.into_iter().any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0)) { if post.published
|| post
.get_authors(&*conn)?
.into_iter()
.any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0))
{
let comments = CommentTree::from_post(&*conn, &post, user.as_ref())?; let comments = CommentTree::from_post(&*conn, &post, user.as_ref())?;
let previous = responding_to.and_then(|r| Comment::get(&*conn, r).ok()); let previous = responding_to.and_then(|r| Comment::get(&*conn, r).ok());
@ -82,11 +95,19 @@ pub fn details(blog: String, slug: String, conn: DbConn, user: Option<User>, res
} }
#[get("/~/<blog>/<slug>", rank = 3)] #[get("/~/<blog>/<slug>", rank = 3)]
pub fn activity_details(blog: String, slug: String, conn: DbConn, _ap: ApRequest) -> Result<ActivityStream<LicensedArticle>, Option<String>> { pub fn activity_details(
blog: String,
slug: String,
conn: DbConn,
_ap: ApRequest,
) -> Result<ActivityStream<LicensedArticle>, Option<String>> {
let blog = Blog::find_by_fqn(&*conn, &blog).map_err(|_| None)?; let blog = Blog::find_by_fqn(&*conn, &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(post.to_activity(&*conn).map_err(|_| String::from("Post serialization error"))?)) Ok(ActivityStream::new(
post.to_activity(&*conn)
.map_err(|_| String::from("Post serialization error"))?,
))
} else { } else {
Err(Some(String::from("Not published yet."))) Err(Some(String::from("Not published yet.")))
} }
@ -95,8 +116,11 @@ pub fn activity_details(blog: String, slug: String, conn: DbConn, _ap: ApRequest
#[get("/~/<blog>/new", rank = 2)] #[get("/~/<blog>/new", rank = 2)]
pub fn new_auth(blog: String, i18n: I18n) -> Flash<Redirect> { pub fn new_auth(blog: String, i18n: I18n) -> Flash<Redirect> {
utils::requires_login( utils::requires_login(
&i18n!(i18n.catalog, "You need to be logged in order to write a new post"), &i18n!(
uri!(new: blog = blog) i18n.catalog,
"You need to be logged in order to write a new post"
),
uri!(new: blog = blog),
) )
} }
@ -112,7 +136,7 @@ pub fn new(blog: String, cl: ContentLen, rockets: PlumeRocket) -> Result<Ructe,
return Ok(render!(errors::not_authorized( return Ok(render!(errors::not_authorized(
&(&*conn, &intl.catalog, Some(user)), &(&*conn, &intl.catalog, Some(user)),
i18n!(intl.catalog, "You are not author in this blog.") i18n!(intl.catalog, "You are not author in this blog.")
))) )));
} }
let medias = Media::for_user(&*conn, user.id)?; let medias = Media::for_user(&*conn, user.id)?;
@ -134,7 +158,12 @@ pub fn new(blog: String, cl: ContentLen, rockets: PlumeRocket) -> Result<Ructe,
} }
#[get("/~/<blog>/<slug>/edit")] #[get("/~/<blog>/<slug>/edit")]
pub fn edit(blog: String, slug: String, cl: ContentLen, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> { pub fn edit(
blog: String,
slug: String,
cl: ContentLen,
rockets: PlumeRocket,
) -> Result<Ructe, ErrorPage> {
let conn = rockets.conn; let conn = rockets.conn;
let intl = rockets.intl; let intl = rockets.intl;
let b = Blog::find_by_fqn(&*conn, &blog)?; let b = Blog::find_by_fqn(&*conn, &blog)?;
@ -145,10 +174,9 @@ pub fn edit(blog: String, slug: String, cl: ContentLen, rockets: PlumeRocket) ->
return Ok(render!(errors::not_authorized( return Ok(render!(errors::not_authorized(
&(&*conn, &intl.catalog, Some(user)), &(&*conn, &intl.catalog, Some(user)),
i18n!(intl.catalog, "You are not author in this blog.") i18n!(intl.catalog, "You are not author in this blog.")
))) )));
} }
let source = if !post.source.is_empty() { let source = if !post.source.is_empty() {
post.source.clone() post.source.clone()
} else { } else {
@ -168,7 +196,7 @@ pub fn edit(blog: String, slug: String, cl: ContentLen, rockets: PlumeRocket) ->
content: source, content: source,
tags: Tag::for_post(&*conn, post.id)? tags: Tag::for_post(&*conn, post.id)?
.into_iter() .into_iter()
.filter_map(|t| if !t.is_hashtag {Some(t.tag)} else {None}) .filter_map(|t| if !t.is_hashtag { Some(t.tag) } else { None })
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(", "), .join(", "),
license: post.license.clone(), license: post.license.clone(),
@ -184,11 +212,17 @@ pub fn edit(blog: String, slug: String, cl: ContentLen, rockets: PlumeRocket) ->
} }
#[post("/~/<blog>/<slug>/edit", data = "<form>")] #[post("/~/<blog>/<slug>/edit", data = "<form>")]
pub fn update(blog: String, slug: String, cl: ContentLen, form: LenientForm<NewPostForm>, rockets: PlumeRocket) pub fn update(
-> Result<Redirect, Ructe> { blog: String,
slug: String,
cl: ContentLen,
form: LenientForm<NewPostForm>,
rockets: PlumeRocket,
) -> Result<Redirect, Ructe> {
let conn = rockets.conn; let conn = rockets.conn;
let b = Blog::find_by_fqn(&*conn, &blog).expect("post::update: blog error"); let b = Blog::find_by_fqn(&*conn, &blog).expect("post::update: blog error");
let mut post = Post::find_by_slug(&*conn, &slug, b.id).expect("post::update: find by slug error"); let mut post =
Post::find_by_slug(&*conn, &slug, b.id).expect("post::update: find by slug error");
let user = rockets.user.unwrap(); let user = rockets.user.unwrap();
let intl = rockets.intl; let intl = rockets.intl;
@ -200,23 +234,36 @@ pub fn update(blog: String, slug: String, cl: ContentLen, form: LenientForm<NewP
let mut errors = match form.validate() { let mut errors = match form.validate() {
Ok(_) => ValidationErrors::new(), Ok(_) => ValidationErrors::new(),
Err(e) => e Err(e) => e,
}; };
if new_slug != slug && Post::find_by_slug(&*conn, &new_slug, b.id).is_ok() { if new_slug != slug && Post::find_by_slug(&*conn, &new_slug, b.id).is_ok() {
errors.add("title", ValidationError { errors.add(
"title",
ValidationError {
code: Cow::from("existing_slug"), code: Cow::from("existing_slug"),
message: Some(Cow::from("A post with the same title already exists.")), message: Some(Cow::from("A post with the same title already exists.")),
params: HashMap::new() params: HashMap::new(),
}); },
);
} }
if errors.is_empty() { if errors.is_empty() {
if !user.is_author_in(&*conn, &b).expect("posts::update: is author in error") { if !user
.is_author_in(&*conn, &b)
.expect("posts::update: is author in error")
{
// actually it's not "Ok"… // actually it's not "Ok"…
Ok(Redirect::to(uri!(super::blogs::details: name = blog, page = _))) Ok(Redirect::to(
uri!(super::blogs::details: name = blog, page = _),
))
} else { } else {
let (content, mentions, hashtags) = utils::md_to_html(form.content.to_string().as_ref(), &Instance::get_local(&conn).expect("posts::update: Error getting local instance").public_domain); let (content, mentions, hashtags) = utils::md_to_html(
form.content.to_string().as_ref(),
&Instance::get_local(&conn)
.expect("posts::update: Error getting local instance")
.public_domain,
);
// update publication date if when this article is no longer a draft // update publication date if when this article is no longer a draft
let newly_published = if !post.published && !form.draft { let newly_published = if !post.published && !form.draft {
@ -236,34 +283,61 @@ pub fn update(blog: String, slug: String, cl: ContentLen, form: LenientForm<NewP
post.source = form.content.clone(); post.source = form.content.clone();
post.license = form.license.clone(); post.license = form.license.clone();
post.cover_id = form.cover; post.cover_id = form.cover;
post.update(&*conn, &searcher).expect("post::update: update error");; post.update(&*conn, &searcher)
.expect("post::update: update error");;
if post.published { if post.published {
post.update_mentions(&conn, mentions.into_iter().filter_map(|m| Mention::build_activity(&conn, &m).ok()).collect()) post.update_mentions(
&conn,
mentions
.into_iter()
.filter_map(|m| Mention::build_activity(&conn, &m).ok())
.collect(),
)
.expect("post::update: mentions error");; .expect("post::update: mentions error");;
} }
let tags = form.tags.split(',').map(|t| t.trim().to_camel_case()).filter(|t| !t.is_empty()) let tags = form
.collect::<HashSet<_>>().into_iter().filter_map(|t| Tag::build_activity(&conn, t).ok()).collect::<Vec<_>>(); .tags
post.update_tags(&conn, tags).expect("post::update: tags error"); .split(',')
.map(|t| t.trim().to_camel_case())
.filter(|t| !t.is_empty())
.collect::<HashSet<_>>()
.into_iter()
.filter_map(|t| Tag::build_activity(&conn, t).ok())
.collect::<Vec<_>>();
post.update_tags(&conn, tags)
.expect("post::update: tags error");
let hashtags = hashtags.into_iter().map(|h| h.to_camel_case()).collect::<HashSet<_>>() let hashtags = hashtags
.into_iter().filter_map(|t| Tag::build_activity(&conn, t).ok()).collect::<Vec<_>>(); .into_iter()
post.update_hashtags(&conn, hashtags).expect("post::update: hashtags error"); .map(|h| h.to_camel_case())
.collect::<HashSet<_>>()
.into_iter()
.filter_map(|t| Tag::build_activity(&conn, t).ok())
.collect::<Vec<_>>();
post.update_hashtags(&conn, hashtags)
.expect("post::update: hashtags error");
if post.published { if post.published {
if newly_published { if newly_published {
let act = post.create_activity(&conn).expect("post::update: act error"); let act = post
.create_activity(&conn)
.expect("post::update: act error");
let dest = User::one_by_instance(&*conn).expect("post::update: dest error"); let dest = User::one_by_instance(&*conn).expect("post::update: dest error");
worker.execute(move || broadcast(&user, act, dest)); worker.execute(move || broadcast(&user, act, dest));
} else { } else {
let act = post.update_activity(&*conn).expect("post::update: act error"); let act = post
.update_activity(&*conn)
.expect("post::update: act error");
let dest = User::one_by_instance(&*conn).expect("posts::update: dest error"); let dest = User::one_by_instance(&*conn).expect("posts::update: dest error");
worker.execute(move || broadcast(&user, act, dest)); worker.execute(move || broadcast(&user, act, dest));
} }
} }
Ok(Redirect::to(uri!(details: blog = blog, slug = new_slug, responding_to = _))) Ok(Redirect::to(
uri!(details: blog = blog, slug = new_slug, responding_to = _),
))
} }
} else { } else {
let medias = Media::for_user(&*conn, user.id).expect("posts:update: medias error"); let medias = Media::for_user(&*conn, user.id).expect("posts:update: medias error");
@ -306,7 +380,12 @@ pub fn valid_slug(title: &str) -> Result<(), ValidationError> {
} }
#[post("/~/<blog_name>/new", data = "<form>")] #[post("/~/<blog_name>/new", data = "<form>")]
pub fn create(blog_name: String, form: LenientForm<NewPostForm>, cl: ContentLen, rockets: PlumeRocket) -> Result<Redirect, Result<Ructe, ErrorPage>> { pub fn create(
blog_name: String,
form: LenientForm<NewPostForm>,
cl: ContentLen,
rockets: PlumeRocket,
) -> Result<Redirect, Result<Ructe, ErrorPage>> {
let conn = rockets.conn; let conn = rockets.conn;
let blog = Blog::find_by_fqn(&*conn, &blog_name).expect("post::create: blog error");; let blog = Blog::find_by_fqn(&*conn, &blog_name).expect("post::create: blog error");;
let slug = form.title.to_string().to_kebab_case(); let slug = form.title.to_string().to_kebab_case();
@ -314,29 +393,41 @@ pub fn create(blog_name: String, form: LenientForm<NewPostForm>, cl: ContentLen,
let mut errors = match form.validate() { let mut errors = match form.validate() {
Ok(_) => ValidationErrors::new(), Ok(_) => ValidationErrors::new(),
Err(e) => e Err(e) => e,
}; };
if Post::find_by_slug(&*conn, &slug, blog.id).is_ok() { if Post::find_by_slug(&*conn, &slug, blog.id).is_ok() {
errors.add("title", ValidationError { errors.add(
"title",
ValidationError {
code: Cow::from("existing_slug"), code: Cow::from("existing_slug"),
message: Some(Cow::from("A post with the same title already exists.")), message: Some(Cow::from("A post with the same title already exists.")),
params: HashMap::new() params: HashMap::new(),
}); },
);
} }
if errors.is_empty() { if errors.is_empty() {
if !user.is_author_in(&*conn, &blog).expect("post::create: is author in error") { if !user
.is_author_in(&*conn, &blog)
.expect("post::create: is author in error")
{
// actually it's not "Ok"… // actually it's not "Ok"…
return Ok(Redirect::to(uri!(super::blogs::details: name = blog_name, page = _))) return Ok(Redirect::to(
uri!(super::blogs::details: name = blog_name, page = _),
));
} }
let (content, mentions, hashtags) = utils::md_to_html( let (content, mentions, hashtags) = utils::md_to_html(
form.content.to_string().as_ref(), form.content.to_string().as_ref(),
&Instance::get_local(&conn).expect("post::create: local instance error").public_domain &Instance::get_local(&conn)
.expect("post::create: local instance error")
.public_domain,
); );
let searcher = rockets.searcher; let searcher = rockets.searcher;
let post = Post::insert(&*conn, NewPost { let post = Post::insert(
&*conn,
NewPost {
blog_id: blog.id, blog_id: blog.id,
slug: slug.to_string(), slug: slug.to_string(),
title: form.title.to_string(), title: form.title.to_string(),
@ -350,50 +441,71 @@ pub fn create(blog_name: String, form: LenientForm<NewPostForm>, cl: ContentLen,
cover_id: form.cover, cover_id: form.cover,
}, },
&searcher, &searcher,
).expect("post::create: post save error"); )
.expect("post::create: post save error");
PostAuthor::insert(&*conn, NewPostAuthor { PostAuthor::insert(
&*conn,
NewPostAuthor {
post_id: post.id, post_id: post.id,
author_id: user.id author_id: user.id,
}).expect("post::create: author save error"); },
)
.expect("post::create: author save error");
let tags = form.tags.split(',') let tags = form
.tags
.split(',')
.map(|t| t.trim().to_camel_case()) .map(|t| t.trim().to_camel_case())
.filter(|t| !t.is_empty()) .filter(|t| !t.is_empty())
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
for tag in tags { for tag in tags {
Tag::insert(&*conn, NewTag { Tag::insert(
&*conn,
NewTag {
tag, tag,
is_hashtag: false, is_hashtag: false,
post_id: post.id post_id: post.id,
}).expect("post::create: tags save error"); },
)
.expect("post::create: tags save error");
} }
for hashtag in hashtags { for hashtag in hashtags {
Tag::insert(&*conn, NewTag { Tag::insert(
&*conn,
NewTag {
tag: hashtag.to_camel_case(), tag: hashtag.to_camel_case(),
is_hashtag: true, is_hashtag: true,
post_id: post.id post_id: post.id,
}).expect("post::create: hashtags save error"); },
)
.expect("post::create: hashtags save error");
} }
if post.published { if post.published {
for m in mentions { for m in mentions {
Mention::from_activity( Mention::from_activity(
&*conn, &*conn,
&Mention::build_activity(&*conn, &m).expect("post::create: mention build error"), &Mention::build_activity(&*conn, &m)
.expect("post::create: mention build error"),
post.id, post.id,
true, true,
true true,
).expect("post::create: mention save error"); )
.expect("post::create: mention save error");
} }
let act = post.create_activity(&*conn).expect("posts::create: activity error"); let act = post
.create_activity(&*conn)
.expect("posts::create: activity error");
let dest = User::one_by_instance(&*conn).expect("posts::create: dest error"); let dest = User::one_by_instance(&*conn).expect("posts::create: dest error");
let worker = rockets.worker; let worker = rockets.worker;
worker.execute(move || broadcast(&user, act, dest)); worker.execute(move || broadcast(&user, act, dest));
} }
Ok(Redirect::to(uri!(details: blog = blog_name, slug = slug, responding_to = _))) Ok(Redirect::to(
uri!(details: blog = blog_name, slug = slug, responding_to = _),
))
} else { } else {
let medias = Media::for_user(&*conn, user.id).expect("posts::create: medias error"); let medias = Media::for_user(&*conn, user.id).expect("posts::create: medias error");
let intl = rockets.intl; let intl = rockets.intl;
@ -413,15 +525,25 @@ pub fn create(blog_name: String, form: LenientForm<NewPostForm>, cl: ContentLen,
} }
#[post("/~/<blog_name>/<slug>/delete")] #[post("/~/<blog_name>/<slug>/delete")]
pub fn delete(blog_name: String, slug: String, rockets: PlumeRocket) -> Result<Redirect, ErrorPage> { pub fn delete(
blog_name: String,
slug: String,
rockets: PlumeRocket,
) -> Result<Redirect, ErrorPage> {
let conn = rockets.conn; let conn = rockets.conn;
let user = rockets.user.unwrap(); let user = rockets.user.unwrap();
let post = Blog::find_by_fqn(&*conn, &blog_name) let post = Blog::find_by_fqn(&*conn, &blog_name)
.and_then(|blog| Post::find_by_slug(&*conn, &slug, blog.id)); .and_then(|blog| Post::find_by_slug(&*conn, &slug, blog.id));
if let Ok(post) = post { if let Ok(post) = post {
if !post.get_authors(&*conn)?.into_iter().any(|a| a.id == user.id) { if !post
return Ok(Redirect::to(uri!(details: blog = blog_name.clone(), slug = slug.clone(), responding_to = _))) .get_authors(&*conn)?
.into_iter()
.any(|a| a.id == user.id)
{
return Ok(Redirect::to(
uri!(details: blog = blog_name.clone(), slug = slug.clone(), responding_to = _),
));
} }
let searcher = rockets.searcher; let searcher = rockets.searcher;
@ -432,10 +554,17 @@ pub fn delete(blog_name: String, slug: String, rockets: PlumeRocket) -> Result<R
let user_c = user.clone(); let user_c = user.clone();
worker.execute(move || broadcast(&user_c, delete_activity, dest)); worker.execute(move || broadcast(&user_c, delete_activity, dest));
worker.execute_after(Duration::from_secs(10*60), move || {user.rotate_keypair(&conn).expect("Failed to rotate keypair");}); worker.execute_after(Duration::from_secs(10 * 60), move || {
user.rotate_keypair(&conn)
.expect("Failed to rotate keypair");
});
Ok(Redirect::to(uri!(super::blogs::details: name = blog_name, page = _))) Ok(Redirect::to(
uri!(super::blogs::details: name = blog_name, page = _),
))
} else { } else {
Ok(Redirect::to(uri!(super::blogs::details: name = blog_name, page = _))) Ok(Redirect::to(
uri!(super::blogs::details: name = blog_name, page = _),
))
} }
} }

View File

@ -1,20 +1,23 @@
use rocket::response::{Redirect, Flash}; use rocket::response::{Flash, Redirect};
use rocket_i18n::I18n; use rocket_i18n::I18n;
use plume_common::activity_pub::{broadcast, inbox::{Deletable, Notify}}; use plume_common::activity_pub::{
use plume_common::utils; broadcast,
use plume_models::{ inbox::{Deletable, Notify},
blogs::Blog,
db_conn::DbConn,
posts::Post,
reshares::*,
users::User
}; };
use plume_common::utils;
use plume_models::{blogs::Blog, db_conn::DbConn, posts::Post, reshares::*, users::User};
use routes::errors::ErrorPage; use routes::errors::ErrorPage;
use Worker; use Worker;
#[post("/~/<blog>/<slug>/reshare")] #[post("/~/<blog>/<slug>/reshare")]
pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Result<Redirect, ErrorPage> { pub fn create(
blog: String,
slug: String,
user: User,
conn: DbConn,
worker: Worker,
) -> Result<Redirect, ErrorPage> {
let b = Blog::find_by_fqn(&*conn, &blog)?; let b = Blog::find_by_fqn(&*conn, &blog)?;
let post = Post::find_by_slug(&*conn, &slug, b.id)?; let post = Post::find_by_slug(&*conn, &slug, b.id)?;
@ -32,13 +35,18 @@ pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Work
worker.execute(move || broadcast(&user, delete_act, dest)); worker.execute(move || broadcast(&user, delete_act, dest));
} }
Ok(Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _))) Ok(Redirect::to(
uri!(super::posts::details: blog = blog, slug = slug, responding_to = _),
))
} }
#[post("/~/<blog>/<slug>/reshare", rank=1)] #[post("/~/<blog>/<slug>/reshare", rank = 1)]
pub fn create_auth(blog: String, slug: String, i18n: I18n) -> Flash<Redirect> { pub fn create_auth(blog: String, slug: String, i18n: I18n) -> Flash<Redirect> {
utils::requires_login( utils::requires_login(
&i18n!(i18n.catalog, "You need to be logged in order to reshare a post"), &i18n!(
uri!(create: blog = blog, slug = slug) i18n.catalog,
"You need to be logged in order to reshare a post"
),
uri!(create: blog = blog, slug = slug),
) )
} }

View File

@ -2,13 +2,11 @@ use chrono::offset::Utc;
use rocket::request::Form; use rocket::request::Form;
use rocket_i18n::I18n; use rocket_i18n::I18n;
use plume_models::{ use plume_models::{db_conn::DbConn, search::Query, users::User};
db_conn::DbConn, users::User,
search::Query};
use routes::Page; use routes::Page;
use std::str::FromStr;
use template_utils::Ructe; use template_utils::Ructe;
use Searcher; use Searcher;
use std::str::FromStr;
#[derive(Default, FromForm)] #[derive(Default, FromForm)]
pub struct SearchQuery { pub struct SearchQuery {
@ -53,12 +51,19 @@ macro_rules! param_to_query {
} }
} }
#[get("/search?<query..>")] #[get("/search?<query..>")]
pub fn search(query: Option<Form<SearchQuery>>, conn: DbConn, searcher: Searcher, user: Option<User>, intl: I18n) -> Ructe { pub fn search(
query: Option<Form<SearchQuery>>,
conn: DbConn,
searcher: Searcher,
user: Option<User>,
intl: I18n,
) -> Ructe {
let query = query.map(|f| f.into_inner()).unwrap_or_default(); let query = query.map(|f| f.into_inner()).unwrap_or_default();
let page = query.page.unwrap_or_default(); let page = query.page.unwrap_or_default();
let mut parsed_query = Query::from_str(&query.q.as_ref().map(|q| q.as_str()).unwrap_or_default()).unwrap_or_default(); let mut parsed_query =
Query::from_str(&query.q.as_ref().map(|q| q.as_str()).unwrap_or_default())
.unwrap_or_default();
param_to_query!(query, parsed_query; normal: title, subtitle, content, tag, param_to_query!(query, parsed_query; normal: title, subtitle, content, tag,
instance, author, blog, lang, license; instance, author, blog, lang, license;

View File

@ -1,22 +1,26 @@
use lettre::Transport; use lettre::Transport;
use rocket::{
State,
http::{Cookie, Cookies, SameSite, uri::Uri},
response::Redirect,
request::{LenientForm, FlashMessage, Form}
};
use rocket::http::ext::IntoOwned; use rocket::http::ext::IntoOwned;
use rocket_i18n::I18n; use rocket::{
use std::{borrow::Cow, sync::{Arc, Mutex}, time::Instant}; http::{uri::Uri, Cookie, Cookies, SameSite},
use validator::{Validate, ValidationError, ValidationErrors}; request::{FlashMessage, Form, LenientForm},
use template_utils::Ructe; response::Redirect,
State,
use plume_models::{
BASE_URL, Error,
db_conn::DbConn,
users::{User, AUTH_COOKIE}
}; };
use rocket_i18n::I18n;
use std::{
borrow::Cow,
sync::{Arc, Mutex},
time::Instant,
};
use template_utils::Ructe;
use validator::{Validate, ValidationError, ValidationErrors};
use mail::{build_mail, Mailer}; use mail::{build_mail, Mailer};
use plume_models::{
db_conn::DbConn,
users::{User, AUTH_COOKIE},
Error, BASE_URL,
};
use routes::errors::ErrorPage; use routes::errors::ErrorPage;
#[get("/login?<m>")] #[get("/login?<m>")]
@ -34,16 +38,22 @@ pub struct LoginForm {
#[validate(length(min = "1", message = "We need an email or a username to identify you"))] #[validate(length(min = "1", message = "We need an email or a username to identify you"))]
pub email_or_name: String, pub email_or_name: String,
#[validate(length(min = "1", message = "Your password can't be empty"))] #[validate(length(min = "1", message = "Your password can't be empty"))]
pub password: String pub password: String,
} }
#[post("/login", data = "<form>")] #[post("/login", data = "<form>")]
pub fn create(conn: DbConn, form: LenientForm<LoginForm>, flash: Option<FlashMessage>, mut cookies: Cookies, intl: I18n) -> Result<Redirect, Ructe> { pub fn create(
conn: DbConn,
form: LenientForm<LoginForm>,
flash: Option<FlashMessage>,
mut cookies: Cookies,
intl: I18n,
) -> Result<Redirect, Ructe> {
let user = User::find_by_email(&*conn, &form.email_or_name) let user = User::find_by_email(&*conn, &form.email_or_name)
.or_else(|_| User::find_by_fqn(&*conn, &form.email_or_name)); .or_else(|_| User::find_by_fqn(&*conn, &form.email_or_name));
let mut errors = match form.validate() { let mut errors = match form.validate() {
Ok(_) => ValidationErrors::new(), Ok(_) => ValidationErrors::new(),
Err(e) => e Err(e) => e,
}; };
let user_id = if let Ok(user) = user { let user_id = if let Ok(user) = user {
@ -58,7 +68,9 @@ pub fn create(conn: DbConn, form: LenientForm<LoginForm>, flash: Option<FlashMes
} else { } else {
// Fake password verification, only to avoid different login times // Fake password verification, only to avoid different login times
// that could be used to see if an email adress is registered or not // that could be used to see if an email adress is registered or not
User::get(&*conn, 1).map(|u| u.auth(&form.password)).expect("No user is registered"); User::get(&*conn, 1)
.map(|u| u.auth(&form.password))
.expect("No user is registered");
let mut err = ValidationError::new("invalid_login"); let mut err = ValidationError::new("invalid_login");
err.message = Some(Cow::from("Invalid username or password")); err.message = Some(Cow::from("Invalid username or password"));
@ -67,25 +79,31 @@ pub fn create(conn: DbConn, form: LenientForm<LoginForm>, flash: Option<FlashMes
}; };
if errors.is_empty() { if errors.is_empty() {
cookies.add_private(Cookie::build(AUTH_COOKIE, user_id) cookies.add_private(
Cookie::build(AUTH_COOKIE, user_id)
.same_site(SameSite::Lax) .same_site(SameSite::Lax)
.finish()); .finish(),
);
let destination = flash let destination = flash
.and_then(|f| if f.name() == "callback" { .and_then(|f| {
if f.name() == "callback" {
Some(f.msg().to_owned()) Some(f.msg().to_owned())
} else { } else {
None None
}
}) })
.unwrap_or_else(|| "/".to_owned()); .unwrap_or_else(|| "/".to_owned());
let uri = Uri::parse(&destination) let uri = Uri::parse(&destination)
.map(|x| x.into_owned()) .map(|x| x.into_owned())
.map_err(|_| render!(session::login( .map_err(|_| {
render!(session::login(
&(&*conn, &intl.catalog, None), &(&*conn, &intl.catalog, None),
None, None,
&*form, &*form,
errors errors
)))?; ))
})?;
Ok(Redirect::to(uri)) Ok(Redirect::to(uri))
} else { } else {
@ -140,13 +158,15 @@ pub fn password_reset_request(
intl: I18n, intl: I18n,
mail: State<Arc<Mutex<Mailer>>>, mail: State<Arc<Mutex<Mailer>>>,
form: Form<ResetForm>, form: Form<ResetForm>,
requests: State<Arc<Mutex<Vec<ResetRequest>>>> requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
) -> Ructe { ) -> Ructe {
let mut requests = requests.lock().unwrap(); let mut requests = requests.lock().unwrap();
// Remove outdated requests (more than 1 day old) to avoid the list to grow too much // Remove outdated requests (more than 1 day old) to avoid the list to grow too much
requests.retain(|r| r.creation_date.elapsed().as_secs() < 24 * 60 * 60); requests.retain(|r| r.creation_date.elapsed().as_secs() < 24 * 60 * 60);
if User::find_by_email(&*conn, &form.email).is_ok() && !requests.iter().any(|x| x.mail == form.email.clone()) { if User::find_by_email(&*conn, &form.email).is_ok()
&& !requests.iter().any(|x| x.mail == form.email.clone())
{
let id = plume_common::utils::random_hex(); let id = plume_common::utils::random_hex();
requests.push(ResetRequest { requests.push(ResetRequest {
@ -159,22 +179,35 @@ pub fn password_reset_request(
if let Some(message) = build_mail( if let Some(message) = build_mail(
form.email.clone(), form.email.clone(),
i18n!(intl.catalog, "Password reset"), i18n!(intl.catalog, "Password reset"),
i18n!(intl.catalog, "Here is the link to reset your password: {0}"; link) i18n!(intl.catalog, "Here is the link to reset your password: {0}"; link),
) { ) {
if let Some(ref mut mail) = *mail.lock().unwrap() { if let Some(ref mut mail) = *mail.lock().unwrap() {
mail mail.send(message.into())
.send(message.into()) .map_err(|_| eprintln!("Couldn't send password reset mail"))
.map_err(|_| eprintln!("Couldn't send password reset mail")).ok(); } .ok();
} }
} }
render!(session::password_reset_request_ok( }
&(&*conn, &intl.catalog, None) render!(session::password_reset_request_ok(&(
)) &*conn,
&intl.catalog,
None
)))
} }
#[get("/password-reset/<token>")] #[get("/password-reset/<token>")]
pub fn password_reset_form(conn: DbConn, intl: I18n, token: String, requests: State<Arc<Mutex<Vec<ResetRequest>>>>) -> Result<Ructe, ErrorPage> { pub fn password_reset_form(
requests.lock().unwrap().iter().find(|x| x.id == token.clone()).ok_or(Error::NotFound)?; conn: DbConn,
intl: I18n,
token: String,
requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
) -> Result<Ructe, ErrorPage> {
requests
.lock()
.unwrap()
.iter()
.find(|x| x.id == token.clone())
.ok_or(Error::NotFound)?;
Ok(render!(session::password_reset( Ok(render!(session::password_reset(
&(&*conn, &intl.catalog, None), &(&*conn, &intl.catalog, None),
&NewPasswordForm::default(), &NewPasswordForm::default(),
@ -183,13 +216,11 @@ pub fn password_reset_form(conn: DbConn, intl: I18n, token: String, requests: St
} }
#[derive(FromForm, Default, Validate)] #[derive(FromForm, Default, Validate)]
#[validate( #[validate(schema(
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 {
pub password: String, pub password: String,
pub password_confirmation: String, pub password_confirmation: String,
@ -209,19 +240,28 @@ pub fn password_reset(
intl: I18n, intl: I18n,
token: String, token: String,
requests: State<Arc<Mutex<Vec<ResetRequest>>>>, requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
form: Form<NewPasswordForm> form: Form<NewPasswordForm>,
) -> Result<Redirect, Ructe> { ) -> Result<Redirect, Ructe> {
form.validate() form.validate()
.and_then(|_| { .and_then(|_| {
let mut requests = requests.lock().unwrap(); let mut requests = requests.lock().unwrap();
let req = requests.iter().find(|x| x.id == token.clone()).ok_or_else(|| to_validation(0))?.clone(); let req = requests
if req.creation_date.elapsed().as_secs() < 60 * 60 * 2 { // Reset link is only valid for 2 hours .iter()
.find(|x| x.id == token.clone())
.ok_or_else(|| to_validation(0))?
.clone();
if req.creation_date.elapsed().as_secs() < 60 * 60 * 2 {
// Reset link is only valid for 2 hours
requests.retain(|r| *r != req); requests.retain(|r| *r != req);
let user = User::find_by_email(&*conn, &req.mail).map_err(to_validation)?; let user = User::find_by_email(&*conn, &req.mail).map_err(to_validation)?;
user.reset_password(&*conn, &form.password).ok(); user.reset_password(&*conn, &form.password).ok();
Ok(Redirect::to(uri!(new: m = i18n!(intl.catalog, "Your password was successfully reset.")))) Ok(Redirect::to(uri!(
new: m = i18n!(intl.catalog, "Your password was successfully reset.")
)))
} else { } else {
Ok(Redirect::to(uri!(new: m = i18n!(intl.catalog, "Sorry, but the link expired. Try again")))) Ok(Redirect::to(uri!(
new: m = i18n!(intl.catalog, "Sorry, but the link expired. Try again")
)))
} }
}) })
.map_err(|err| { .map_err(|err| {
@ -235,10 +275,13 @@ pub fn password_reset(
fn to_validation<T>(_: T) -> ValidationErrors { fn to_validation<T>(_: T) -> ValidationErrors {
let mut errors = ValidationErrors::new(); let mut errors = ValidationErrors::new();
errors.add("", ValidationError { errors.add(
"",
ValidationError {
code: Cow::from("server_error"), code: Cow::from("server_error"),
message: Some(Cow::from("An unknown error occured")), message: Some(Cow::from("An unknown error occured")),
params: std::collections::HashMap::new() params: std::collections::HashMap::new(),
}); },
);
errors errors
} }

View File

@ -1,15 +1,17 @@
use rocket_i18n::I18n; use rocket_i18n::I18n;
use plume_models::{ use plume_models::{db_conn::DbConn, posts::Post, users::User};
db_conn::DbConn, use routes::{errors::ErrorPage, Page};
posts::Post,
users::User,
};
use routes::{Page, errors::ErrorPage};
use template_utils::Ructe; use template_utils::Ructe;
#[get("/tag/<name>?<page>")] #[get("/tag/<name>?<page>")]
pub fn tag(user: Option<User>, conn: DbConn, name: String, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> { pub fn tag(
user: Option<User>,
conn: DbConn,
name: String,
page: Option<Page>,
intl: I18n,
) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
let posts = Post::list_by_tag(&*conn, name.clone(), page.limits())?; let posts = Post::list_by_tag(&*conn, name.clone(), page.limits())?;
Ok(render!(tags::index( Ok(render!(tags::index(

View File

@ -19,14 +19,20 @@ use plume_common::activity_pub::{
}; };
use plume_common::utils; use plume_common::utils;
use plume_models::{ use plume_models::{
blogs::Blog,
db_conn::DbConn,
follows,
headers::Headers,
instance::Instance,
posts::{LicensedArticle, Post},
reshares::Reshare,
users::*,
Error, Error,
blogs::Blog, db_conn::DbConn, follows, headers::Headers, instance::Instance, posts::{LicensedArticle, Post},
reshares::Reshare, users::*,
}; };
use routes::{Page, PlumeRocket, errors::ErrorPage}; use routes::{errors::ErrorPage, Page, PlumeRocket};
use template_utils::Ructe; use template_utils::Ructe;
use Worker;
use Searcher; use Searcher;
use Worker;
#[get("/me")] #[get("/me")]
pub fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> { pub fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
@ -56,19 +62,21 @@ pub fn details(
let user_clone = user.clone(); let user_clone = user.clone();
let searcher = searcher.clone(); let searcher = searcher.clone();
worker.execute(move || { worker.execute(move || {
for create_act in user_clone.fetch_outbox::<Create>().expect("Remote user: outbox couldn't be fetched") { for create_act in user_clone
.fetch_outbox::<Create>()
.expect("Remote user: outbox couldn't be fetched")
{
match create_act.create_props.object_object::<LicensedArticle>() { match create_act.create_props.object_object::<LicensedArticle>() {
Ok(article) => { Ok(article) => {
Post::from_activity( Post::from_activity(
&(&*fetch_articles_conn, &searcher), &(&*fetch_articles_conn, &searcher),
article, article,
user_clone.clone().into_id(), user_clone.clone().into_id(),
).expect("Article from remote user couldn't be saved"); )
.expect("Article from remote user couldn't be saved");
println!("Fetched article from remote user"); println!("Fetched article from remote user");
} }
Err(e) => { Err(e) => println!("Error while fetching articles in background: {:?}", e),
println!("Error while fetching articles in background: {:?}", e)
}
} }
} }
}); });
@ -76,8 +84,12 @@ pub fn details(
// Fetch followers // Fetch followers
let user_clone = user.clone(); let user_clone = user.clone();
worker.execute(move || { worker.execute(move || {
for user_id in user_clone.fetch_followers_ids().expect("Remote user: fetching followers error") { for user_id in user_clone
let follower = User::from_url(&*fetch_followers_conn, &user_id).expect("user::details: Couldn't fetch follower"); .fetch_followers_ids()
.expect("Remote user: fetching followers error")
{
let follower = User::from_url(&*fetch_followers_conn, &user_id)
.expect("user::details: Couldn't fetch follower");
follows::Follow::insert( follows::Follow::insert(
&*fetch_followers_conn, &*fetch_followers_conn,
follows::NewFollow { follows::NewFollow {
@ -85,7 +97,8 @@ pub fn details(
following_id: user_clone.id, following_id: user_clone.id,
ap_url: String::new(), ap_url: String::new(),
}, },
).expect("Couldn't save follower for remote user"); )
.expect("Couldn't save follower for remote user");
} }
}); });
@ -93,7 +106,9 @@ pub fn details(
let user_clone = user.clone(); let user_clone = user.clone();
if user.needs_update() { if user.needs_update() {
worker.execute(move || { worker.execute(move || {
user_clone.refetch(&*update_conn).expect("Couldn't update user info"); user_clone
.refetch(&*update_conn)
.expect("Couldn't update user info");
}); });
} }
} }
@ -103,11 +118,16 @@ pub fn details(
Ok(render!(users::details( Ok(render!(users::details(
&(&*conn, &intl.catalog, account.clone()), &(&*conn, &intl.catalog, account.clone()),
user.clone(), user.clone(),
account.and_then(|x| x.is_following(&*conn, user.id).ok()).unwrap_or(false), account
.and_then(|x| x.is_following(&*conn, user.id).ok())
.unwrap_or(false),
user.instance_id != Instance::get_local(&*conn)?.id, user.instance_id != Instance::get_local(&*conn)?.id,
user.get_instance(&*conn)?.public_domain, user.get_instance(&*conn)?.public_domain,
recents, recents,
reshares.into_iter().filter_map(|r| r.get_post(&*conn).ok()).collect() reshares
.into_iter()
.filter_map(|r| r.get_post(&*conn).ok())
.collect()
))) )))
} }
@ -124,19 +144,25 @@ pub fn dashboard(user: User, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPag
#[get("/dashboard", rank = 2)] #[get("/dashboard", rank = 2)]
pub fn dashboard_auth(i18n: I18n) -> Flash<Redirect> { pub fn dashboard_auth(i18n: I18n) -> Flash<Redirect> {
utils::requires_login( utils::requires_login(
&i18n!(i18n.catalog, "You need to be logged in order to access your dashboard"), &i18n!(
i18n.catalog,
"You need to be logged in order to access your dashboard"
),
uri!(dashboard), uri!(dashboard),
) )
} }
#[post("/@/<name>/follow")] #[post("/@/<name>/follow")]
pub fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Result<Redirect, ErrorPage> { pub fn follow(
name: String,
conn: DbConn,
user: User,
worker: Worker,
) -> Result<Redirect, ErrorPage> {
let target = User::find_by_fqn(&*conn, &name)?; let target = User::find_by_fqn(&*conn, &name)?;
if let Ok(follow) = follows::Follow::find(&*conn, user.id, target.id) { if let Ok(follow) = follows::Follow::find(&*conn, user.id, target.id) {
let delete_act = follow.delete(&*conn)?; let delete_act = follow.delete(&*conn)?;
worker.execute(move || { worker.execute(move || broadcast(&user, delete_act, vec![target]));
broadcast(&user, delete_act, vec![target])
});
} else { } else {
let f = follows::Follow::insert( let f = follows::Follow::insert(
&*conn, &*conn,
@ -157,13 +183,22 @@ pub fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Result<
#[post("/@/<name>/follow", rank = 2)] #[post("/@/<name>/follow", rank = 2)]
pub fn follow_auth(name: String, i18n: I18n) -> Flash<Redirect> { pub fn follow_auth(name: String, i18n: I18n) -> Flash<Redirect> {
utils::requires_login( utils::requires_login(
&i18n!(i18n.catalog, "You need to be logged in order to subscribe to someone"), &i18n!(
i18n.catalog,
"You need to be logged in order to subscribe to someone"
),
uri!(follow: name = name), uri!(follow: name = name),
) )
} }
#[get("/@/<name>/followers?<page>", rank = 2)] #[get("/@/<name>/followers?<page>", rank = 2)]
pub fn followers(name: String, conn: DbConn, account: Option<User>, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> { pub fn followers(
name: String,
conn: DbConn,
account: Option<User>,
page: Option<Page>,
intl: I18n,
) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
let user = User::find_by_fqn(&*conn, &name)?; let user = User::find_by_fqn(&*conn, &name)?;
let followers_count = user.count_followers(&*conn)?; let followers_count = user.count_followers(&*conn)?;
@ -171,7 +206,9 @@ pub fn followers(name: String, conn: DbConn, account: Option<User>, page: Option
Ok(render!(users::followers( Ok(render!(users::followers(
&(&*conn, &intl.catalog, account.clone()), &(&*conn, &intl.catalog, account.clone()),
user.clone(), user.clone(),
account.and_then(|x| x.is_following(&*conn, user.id).ok()).unwrap_or(false), account
.and_then(|x| x.is_following(&*conn, user.id).ok())
.unwrap_or(false),
user.instance_id != Instance::get_local(&*conn)?.id, user.instance_id != Instance::get_local(&*conn)?.id,
user.get_instance(&*conn)?.public_domain, user.get_instance(&*conn)?.public_domain,
user.get_followers_page(&*conn, page.limits())?, user.get_followers_page(&*conn, page.limits())?,
@ -181,7 +218,13 @@ pub fn followers(name: String, conn: DbConn, account: Option<User>, page: Option
} }
#[get("/@/<name>/followed?<page>", rank = 2)] #[get("/@/<name>/followed?<page>", rank = 2)]
pub fn followed(name: String, conn: DbConn, account: Option<User>, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> { pub fn followed(
name: String,
conn: DbConn,
account: Option<User>,
page: Option<Page>,
intl: I18n,
) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
let user = User::find_by_fqn(&*conn, &name)?; let user = User::find_by_fqn(&*conn, &name)?;
let followed_count = user.count_followed(&*conn)?; let followed_count = user.count_followed(&*conn)?;
@ -189,7 +232,9 @@ pub fn followed(name: String, conn: DbConn, account: Option<User>, page: Option<
Ok(render!(users::followed( Ok(render!(users::followed(
&(&*conn, &intl.catalog, account.clone()), &(&*conn, &intl.catalog, account.clone()),
user.clone(), user.clone(),
account.and_then(|x| x.is_following(&*conn, user.id).ok()).unwrap_or(false), account
.and_then(|x| x.is_following(&*conn, user.id).ok())
.unwrap_or(false),
user.instance_id != Instance::get_local(&*conn)?.id, user.instance_id != Instance::get_local(&*conn)?.id,
user.get_instance(&*conn)?.public_domain, user.get_instance(&*conn)?.public_domain,
user.get_followed_page(&*conn, page.limits())?, user.get_followed_page(&*conn, page.limits())?,
@ -238,7 +283,10 @@ pub fn edit(name: String, user: User, conn: DbConn, intl: I18n) -> Result<Ructe,
#[get("/@/<name>/edit", rank = 2)] #[get("/@/<name>/edit", rank = 2)]
pub fn edit_auth(name: String, i18n: I18n) -> Flash<Redirect> { pub fn edit_auth(name: String, i18n: I18n) -> Flash<Redirect> {
utils::requires_login( utils::requires_login(
&i18n!(i18n.catalog, "You need to be logged in order to edit your profile"), &i18n!(
i18n.catalog,
"You need to be logged in order to edit your profile"
),
uri!(edit: name = name), uri!(edit: name = name),
) )
} }
@ -251,18 +299,41 @@ pub struct UpdateUserForm {
} }
#[put("/@/<_name>/edit", data = "<form>")] #[put("/@/<_name>/edit", data = "<form>")]
pub fn update(_name: String, conn: DbConn, user: User, form: LenientForm<UpdateUserForm>) -> Result<Redirect, ErrorPage> { pub fn update(
_name: String,
conn: DbConn,
user: User,
form: LenientForm<UpdateUserForm>,
) -> Result<Redirect, ErrorPage> {
user.update( user.update(
&*conn, &*conn,
if !form.display_name.is_empty() { form.display_name.clone() } else { user.display_name.clone() }, if !form.display_name.is_empty() {
if !form.email.is_empty() { form.email.clone() } else { user.email.clone().unwrap_or_default() }, form.display_name.clone()
if !form.summary.is_empty() { form.summary.clone() } else { user.summary.to_string() }, } else {
user.display_name.clone()
},
if !form.email.is_empty() {
form.email.clone()
} else {
user.email.clone().unwrap_or_default()
},
if !form.summary.is_empty() {
form.summary.clone()
} else {
user.summary.to_string()
},
)?; )?;
Ok(Redirect::to(uri!(me))) Ok(Redirect::to(uri!(me)))
} }
#[post("/@/<name>/delete")] #[post("/@/<name>/delete")]
pub fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies, searcher: Searcher) -> Result<Redirect, ErrorPage> { pub fn delete(
name: String,
conn: DbConn,
user: User,
mut cookies: Cookies,
searcher: Searcher,
) -> Result<Redirect, ErrorPage> {
let account = User::find_by_fqn(&*conn, &name)?; let account = User::find_by_fqn(&*conn, &name)?;
if user.id == account.id { if user.id == account.id {
account.delete(&*conn, &searcher)?; account.delete(&*conn, &searcher)?;
@ -278,32 +349,25 @@ pub fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies, sear
} }
#[derive(Default, FromForm, Validate)] #[derive(Default, FromForm, Validate)]
#[validate( #[validate(schema(
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(length(min = "1", message = "Username can't be empty"), #[validate(
custom( function = "validate_username", message = "User name is not allowed to contain any of < > & @ ' or \""))] length(min = "1", message = "Username can't be empty"),
custom(
function = "validate_username",
message = "User name is not allowed to contain any of < > & @ ' or \""
)
)]
pub username: String, pub username: String,
#[validate(email(message = "Invalid email"))] #[validate(email(message = "Invalid email"))]
pub email: String, pub email: String,
#[validate( #[validate(length(min = "8", message = "Password should be at least 8 characters long"))]
length(
min = "8",
message = "Password should be at least 8 characters long"
)
)]
pub password: String, pub password: String,
#[validate( #[validate(length(min = "8", message = "Password should be at least 8 characters long"))]
length(
min = "8",
message = "Password should be at least 8 characters long"
)
)]
pub password_confirmation: String, pub password_confirmation: String,
} }
@ -325,11 +389,14 @@ pub fn validate_username(username: &str) -> Result<(), ValidationError> {
fn to_validation(_: Error) -> ValidationErrors { fn to_validation(_: Error) -> ValidationErrors {
let mut errors = ValidationErrors::new(); let mut errors = ValidationErrors::new();
errors.add("", ValidationError { errors.add(
"",
ValidationError {
code: Cow::from("server_error"), code: Cow::from("server_error"),
message: Some(Cow::from("An unknown error occured")), message: Some(Cow::from("An unknown error occured")),
params: HashMap::new() params: HashMap::new(),
}); },
);
errors errors
} }
@ -355,13 +422,16 @@ pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Resul
"", "",
form.email.to_string(), form.email.to_string(),
User::hash_pass(&form.password).map_err(to_validation)?, User::hash_pass(&form.password).map_err(to_validation)?,
).map_err(to_validation)?; )
.map_err(to_validation)?;
Ok(Redirect::to(uri!(super::session::new: m = _))) Ok(Redirect::to(uri!(super::session::new: m = _)))
}) })
.map_err(|err| { .map_err(|err| {
render!(users::new( render!(users::new(
&(&*conn, &intl.catalog, None), &(&*conn, &intl.catalog, None),
Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true), Instance::get_local(&*conn)
.map(|i| i.open_registrations)
.unwrap_or(true),
&form, &form,
err err
)) ))
@ -395,21 +465,27 @@ pub fn inbox(
))))?; ))))?;
let actor = User::from_url(&conn, actor_id).expect("user::inbox: user error"); let actor = User::from_url(&conn, actor_id).expect("user::inbox: user error");
if !verify_http_headers(&actor, &headers.0, &sig).is_secure() if !verify_http_headers(&actor, &headers.0, &sig).is_secure() && !act.clone().verify(&actor) {
&& !act.clone().verify(&actor)
{
// maybe we just know an old key? // maybe we just know an old key?
actor.refetch(&conn).and_then(|_| User::get(&conn, actor.id)) actor
.and_then(|actor| if verify_http_headers(&actor, &headers.0, &sig).is_secure() .refetch(&conn)
.and_then(|_| User::get(&conn, actor.id))
.and_then(|actor| {
if verify_http_headers(&actor, &headers.0, &sig).is_secure()
|| act.clone().verify(&actor) || act.clone().verify(&actor)
{ {
Ok(()) Ok(())
} else { } else {
Err(Error::Signature) Err(Error::Signature)
}
}) })
.map_err(|_| { .map_err(|_| {
println!("Rejected invalid activity supposedly from {}, with headers {:?}", actor.username, headers.0); println!(
status::BadRequest(Some("Invalid signature"))})?; "Rejected invalid activity supposedly from {}, with headers {:?}",
actor.username, headers.0
);
status::BadRequest(Some("Invalid signature"))
})?;
} }
if Instance::is_blocked(&*conn, actor_id).map_err(|_| None)? { if Instance::is_blocked(&*conn, actor_id).map_err(|_| None)? {
@ -432,18 +508,20 @@ pub fn ap_followers(
) -> Option<ActivityStream<OrderedCollection>> { ) -> Option<ActivityStream<OrderedCollection>> {
let user = User::find_by_fqn(&*conn, &name).ok()?; let user = User::find_by_fqn(&*conn, &name).ok()?;
let followers = user let followers = user
.get_followers(&*conn).ok()? .get_followers(&*conn)
.ok()?
.into_iter() .into_iter()
.map(|f| Id::new(f.ap_url)) .map(|f| Id::new(f.ap_url))
.collect::<Vec<Id>>(); .collect::<Vec<Id>>();
let mut coll = OrderedCollection::default(); let mut coll = OrderedCollection::default();
coll.object_props coll.object_props
.set_id_string(user.followers_endpoint).ok()?; .set_id_string(user.followers_endpoint)
.ok()?;
coll.collection_props coll.collection_props
.set_total_items_u64(followers.len() as u64).ok()?; .set_total_items_u64(followers.len() as u64)
coll.collection_props .ok()?;
.set_items_link_vec(followers).ok()?; coll.collection_props.set_items_link_vec(followers).ok()?;
Some(ActivityStream::new(coll)) Some(ActivityStream::new(coll))
} }
@ -456,7 +534,8 @@ pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
.unwrap() .unwrap()
.compute_box("~", &name, "atom.xml")) .compute_box("~", &name, "atom.xml"))
.entries( .entries(
Post::get_recents_for_author(&*conn, &author, 15).ok()? Post::get_recents_for_author(&*conn, &author, 15)
.ok()?
.into_iter() .into_iter()
.map(|p| super::post_to_atom(p, &*conn)) .map(|p| super::post_to_atom(p, &*conn))
.collect::<Vec<Entry>>(), .collect::<Vec<Entry>>(),

View File

@ -3,11 +3,13 @@ use rocket::response::Content;
use serde_json; use serde_json;
use webfinger::*; use webfinger::*;
use plume_models::{BASE_URL, ap_url, db_conn::DbConn, blogs::Blog, users::User}; use plume_models::{ap_url, blogs::Blog, db_conn::DbConn, users::User, BASE_URL};
#[get("/.well-known/nodeinfo")] #[get("/.well-known/nodeinfo")]
pub fn nodeinfo() -> Content<String> { pub fn nodeinfo() -> Content<String> {
Content(ContentType::new("application", "jrd+json"), json!({ Content(
ContentType::new("application", "jrd+json"),
json!({
"links": [ "links": [
{ {
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0", "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
@ -18,17 +20,25 @@ pub fn nodeinfo() -> Content<String> {
"href": ap_url(&format!("{domain}/nodeinfo/2.1", domain = BASE_URL.as_str())) "href": ap_url(&format!("{domain}/nodeinfo/2.1", domain = BASE_URL.as_str()))
} }
] ]
}).to_string()) })
.to_string(),
)
} }
#[get("/.well-known/host-meta")] #[get("/.well-known/host-meta")]
pub fn host_meta() -> String { pub fn host_meta() -> String {
format!(r#" format!(
r#"
<?xml version="1.0"?> <?xml version="1.0"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"> <XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
<Link rel="lrdd" type="application/xrd+xml" template="{url}"/> <Link rel="lrdd" type="application/xrd+xml" template="{url}"/>
</XRD> </XRD>
"#, url = ap_url(&format!("{domain}/.well-known/webfinger?resource={{uri}}", domain = BASE_URL.as_str()))) "#,
url = ap_url(&format!(
"{domain}/.well-known/webfinger?resource={{uri}}",
domain = BASE_URL.as_str()
))
)
} }
struct WebfingerResolver; struct WebfingerResolver;
@ -41,20 +51,31 @@ impl Resolver<DbConn> for WebfingerResolver {
fn find(acct: String, conn: DbConn) -> Result<Webfinger, ResolverError> { fn find(acct: String, conn: DbConn) -> Result<Webfinger, ResolverError> {
User::find_by_fqn(&*conn, &acct) User::find_by_fqn(&*conn, &acct)
.and_then(|usr| usr.webfinger(&*conn)) .and_then(|usr| usr.webfinger(&*conn))
.or_else(|_| Blog::find_by_fqn(&*conn, &acct) .or_else(|_| {
Blog::find_by_fqn(&*conn, &acct)
.and_then(|blog| blog.webfinger(&*conn)) .and_then(|blog| blog.webfinger(&*conn))
.or(Err(ResolverError::NotFound))) .or(Err(ResolverError::NotFound))
})
} }
} }
#[get("/.well-known/webfinger?<resource>")] #[get("/.well-known/webfinger?<resource>")]
pub fn webfinger(resource: String, conn: DbConn) -> Content<String> { pub fn webfinger(resource: String, conn: DbConn) -> Content<String> {
match WebfingerResolver::endpoint(resource, conn).and_then(|wf| serde_json::to_string(&wf).map_err(|_| ResolverError::NotFound)) { match WebfingerResolver::endpoint(resource, conn)
.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),
Err(err) => Content(ContentType::new("text", "plain"), String::from(match err { Err(err) => Content(
ResolverError::InvalidResource => "Invalid resource. Make sure to request an acct: URI", ContentType::new("text", "plain"),
String::from(match err {
ResolverError::InvalidResource => {
"Invalid resource. Make sure to request an acct: URI"
}
ResolverError::NotFound => "Requested resource was not found", ResolverError::NotFound => "Requested resource was not found",
ResolverError::WrongInstance => "This is not the instance of the requested resource" ResolverError::WrongInstance => {
})) "This is not the instance of the requested resource"
}
}),
),
} }
} }

View File

@ -1,9 +1,9 @@
use plume_models::{Connection, notifications::*, users::User}; use plume_models::{notifications::*, users::User, Connection};
use rocket::http::{Method, Status};
use rocket::http::hyper::header::{ETag, EntityTag}; use rocket::http::hyper::header::{ETag, EntityTag};
use rocket::http::{Method, Status};
use rocket::request::Request; use rocket::request::Request;
use rocket::response::{self, Response, Responder, content::Html as HtmlCt}; use rocket::response::{self, content::Html as HtmlCt, Responder, Response};
use rocket_i18n::Catalog; use rocket_i18n::Catalog;
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher; use std::hash::Hasher;
@ -13,7 +13,7 @@ pub use askama_escape::escape;
pub static CACHE_NAME: &str = env!("CACHE_ID"); pub static CACHE_NAME: &str = env!("CACHE_ID");
pub type BaseContext<'a> = &'a(&'a Connection, &'a Catalog, Option<User>); pub type BaseContext<'a> = &'a (&'a Connection, &'a Catalog, Option<User>);
#[derive(Debug)] #[derive(Debug)]
pub struct Ructe(pub Vec<u8>); pub struct Ructe(pub Vec<u8>);
@ -27,7 +27,10 @@ impl<'r> Responder<'r> for Ructe {
let mut hasher = DefaultHasher::new(); let mut hasher = DefaultHasher::new();
hasher.write(&self.0); hasher.write(&self.0);
let etag = format!("{:x}", hasher.finish()); let etag = format!("{:x}", hasher.finish());
if r.headers().get("If-None-Match").any(|s| s[1..s.len()-1] == etag) { if r.headers()
.get("If-None-Match")
.any(|s| s[1..s.len() - 1] == etag)
{
Response::build() Response::build()
.status(Status::NotModified) .status(Status::NotModified)
.header(ETag(EntityTag::strong(etag))) .header(ETag(EntityTag::strong(etag)))
@ -85,7 +88,13 @@ impl Size {
} }
} }
pub fn avatar(conn: &Connection, user: &User, size: Size, pad: bool, catalog: &Catalog) -> Html<String> { pub fn avatar(
conn: &Connection,
user: &User,
size: Size,
pad: bool,
catalog: &Catalog,
) -> Html<String> {
let name = escape(&user.name()).to_string(); let name = escape(&user.name()).to_string();
Html(format!( Html(format!(
r#"<div class="avatar {size} {padded}" r#"<div class="avatar {size} {padded}"
@ -120,26 +129,55 @@ pub fn tabs(links: &[(&str, String, bool)]) -> Html<String> {
pub fn paginate(catalog: &Catalog, page: i32, total: i32) -> Html<String> { pub fn paginate(catalog: &Catalog, page: i32, total: i32) -> Html<String> {
paginate_param(catalog, page, total, None) paginate_param(catalog, page, total, None)
} }
pub fn paginate_param(catalog: &Catalog, page: i32, total: i32, param: Option<String>) -> Html<String> { pub fn paginate_param(
catalog: &Catalog,
page: i32,
total: i32,
param: Option<String>,
) -> Html<String> {
let mut res = String::new(); let mut res = String::new();
let param = param.map(|mut p| {p.push('&'); p}).unwrap_or_default(); let param = param
.map(|mut p| {
p.push('&');
p
})
.unwrap_or_default();
res.push_str(r#"<div class="pagination">"#); res.push_str(r#"<div class="pagination">"#);
if page != 1 { if page != 1 {
res.push_str(format!(r#"<a href="?{}page={}">{}</a>"#, param, page - 1, catalog.gettext("Previous page")).as_str()); res.push_str(
format!(
r#"<a href="?{}page={}">{}</a>"#,
param,
page - 1,
catalog.gettext("Previous page")
)
.as_str(),
);
} }
if page < total { if page < total {
res.push_str(format!(r#"<a href="?{}page={}">{}</a>"#, param, page + 1, catalog.gettext("Next page")).as_str()); res.push_str(
format!(
r#"<a href="?{}page={}">{}</a>"#,
param,
page + 1,
catalog.gettext("Next page")
)
.as_str(),
);
} }
res.push_str("</div>"); res.push_str("</div>");
Html(res) Html(res)
} }
pub fn encode_query_param(param: &str) -> String { pub fn encode_query_param(param: &str) -> String {
param.chars().map(|c| match c { param
.chars()
.map(|c| match c {
'+' => Ok("%2B"), '+' => Ok("%2B"),
' ' => Err('+'), ' ' => Err('+'),
c => Err(c), c => Err(c),
}).fold(String::new(), |mut s,r| { })
.fold(String::new(), |mut s, r| {
match r { match r {
Ok(r) => s.push_str(r), Ok(r) => s.push_str(r),
Err(r) => s.push(r), Err(r) => s.push(r),
@ -151,18 +189,22 @@ pub fn encode_query_param(param: &str) -> String {
#[macro_export] #[macro_export]
macro_rules! icon { macro_rules! icon {
($name:expr) => { ($name:expr) => {
Html(concat!(r#"<svg class="feather"><use xlink:href="/static/images/feather-sprite.svg#"#, $name, "\"/></svg>")) Html(concat!(
} r#"<svg class="feather"><use xlink:href="/static/images/feather-sprite.svg#"#,
$name,
"\"/></svg>"
))
};
} }
macro_rules! input { macro_rules! input {
($catalog:expr, $name:tt ($kind:tt), $label:expr, $optional:expr, $details:expr, $form:expr, $err:expr, $props:expr) => { ($catalog:expr, $name:tt ($kind:tt), $label:expr, $optional:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {{
{
use validator::ValidationErrorsKind;
use std::borrow::Cow; use std::borrow::Cow;
use validator::ValidationErrorsKind;
let cat = $catalog; let cat = $catalog;
Html(format!(r#" Html(format!(
r#"
<label for="{name}"> <label for="{name}">
{label} {label}
{optional} {optional}
@ -174,41 +216,88 @@ macro_rules! input {
name = stringify!($name), name = stringify!($name),
label = i18n!(cat, $label), label = i18n!(cat, $label),
kind = stringify!($kind), kind = stringify!($kind),
optional = if $optional { format!("<small>{}</small>", i18n!(cat, "Optional")) } else { String::new() }, optional = if $optional {
format!("<small>{}</small>", i18n!(cat, "Optional"))
} else {
String::new()
},
details = if $details.len() > 0 { details = if $details.len() > 0 {
format!("<small>{}</small>", i18n!(cat, $details)) format!("<small>{}</small>", i18n!(cat, $details))
} else { } else {
String::new() String::new()
}, },
error = if let Some(ValidationErrorsKind::Field(errs)) = $err.errors().get(stringify!($name)) { error = if let Some(ValidationErrorsKind::Field(errs)) =
format!(r#"<p class="error">{}</p>"#, errs[0].message.clone().unwrap_or(Cow::from("Unknown error"))) $err.errors().get(stringify!($name))
{
format!(
r#"<p class="error">{}</p>"#,
errs[0]
.message
.clone()
.unwrap_or(Cow::from("Unknown error"))
)
} else { } else {
String::new() String::new()
}, },
val = escape(&$form.$name), val = escape(&$form.$name),
props = $props props = $props
)) ))
} }};
};
($catalog:expr, $name:tt (optional $kind:tt), $label:expr, $details:expr, $form:expr, $err:expr, $props:expr) => { ($catalog:expr, $name:tt (optional $kind:tt), $label:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {
input!($catalog, $name ($kind), $label, true, $details, $form, $err, $props) input!(
$catalog,
$name($kind),
$label,
true,
$details,
$form,
$err,
$props
)
}; };
($catalog:expr, $name:tt (optional $kind:tt), $label:expr, $form:expr, $err:expr, $props:expr) => { ($catalog:expr, $name:tt (optional $kind:tt), $label:expr, $form:expr, $err:expr, $props:expr) => {
input!($catalog, $name ($kind), $label, true, "", $form, $err, $props) input!(
$catalog,
$name($kind),
$label,
true,
"",
$form,
$err,
$props
)
}; };
($catalog:expr, $name:tt ($kind:tt), $label:expr, $details:expr, $form:expr, $err:expr, $props:expr) => { ($catalog:expr, $name:tt ($kind:tt), $label:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {
input!($catalog, $name ($kind), $label, false, $details, $form, $err, $props) input!(
$catalog,
$name($kind),
$label,
false,
$details,
$form,
$err,
$props
)
}; };
($catalog:expr, $name:tt ($kind:tt), $label:expr, $form:expr, $err:expr, $props:expr) => { ($catalog:expr, $name:tt ($kind:tt), $label:expr, $form:expr, $err:expr, $props:expr) => {
input!($catalog, $name ($kind), $label, false, "", $form, $err, $props) input!(
$catalog,
$name($kind),
$label,
false,
"",
$form,
$err,
$props
)
}; };
($catalog:expr, $name:tt ($kind:tt), $label:expr, $form:expr, $err:expr) => { ($catalog:expr, $name:tt ($kind:tt), $label:expr, $form:expr, $err:expr) => {
input!($catalog, $name ($kind), $label, false, "", $form, $err, "") input!($catalog, $name($kind), $label, false, "", $form, $err, "")
}; };
($catalog:expr, $name:tt ($kind:tt), $label:expr, $props:expr) => { ($catalog:expr, $name:tt ($kind:tt), $label:expr, $props:expr) => {{
{
let cat = $catalog; let cat = $catalog;
Html(format!(r#" Html(format!(
r#"
<label for="{name}">{label}</label> <label for="{name}">{label}</label>
<input type="{kind}" id="{name}" name="{name}" {props}/> <input type="{kind}" id="{name}" name="{name}" {props}/>
"#, "#,
@ -217,6 +306,5 @@ macro_rules! input {
kind = stringify!($kind), kind = stringify!($kind),
props = $props props = $props
)) ))
} }};
};
} }