Run 'cargo fmt' to format code (#489)
This commit is contained in:
parent
732f514da7
commit
b945d1f602
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1,3 +1,5 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "MacTypes-sys"
|
||||
version = "2.1.0"
|
||||
|
29
build.rs
29
build.rs
@ -1,8 +1,8 @@
|
||||
extern crate ructe;
|
||||
extern crate rsass;
|
||||
extern crate ructe;
|
||||
use ructe::*;
|
||||
use std::{env, fs::*, io::Write, path::PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::{env, fs::*, io::Write, path::PathBuf};
|
||||
|
||||
fn compute_static_hash() -> String {
|
||||
//"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()
|
||||
}
|
||||
|
||||
|
||||
fn main() {
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let in_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
|
||||
.join("templates");
|
||||
let in_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("templates");
|
||||
compile_templates(&in_dir, &out_dir).expect("compile templates");
|
||||
|
||||
println!("cargo:rerun-if-changed=static/css");
|
||||
let mut out = File::create("static/css/main.css").expect("Couldn't create main.css");
|
||||
out.write_all(
|
||||
&rsass::compile_scss_file("static/css/main.scss".as_ref(), rsass::OutputStyle::Compressed)
|
||||
.expect("Error during SCSS compilation")
|
||||
).expect("Couldn't write CSS output");
|
||||
&rsass::compile_scss_file(
|
||||
"static/css/main.scss".as_ref(),
|
||||
rsass::OutputStyle::Compressed,
|
||||
)
|
||||
.expect("Error during SCSS compilation"),
|
||||
)
|
||||
.expect("Couldn't write CSS output");
|
||||
|
||||
let cache_id = &compute_static_hash()[..8];
|
||||
println!("cargo:rerun-if-changed=target/deploy/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(|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)
|
||||
}
|
||||
|
@ -21,4 +21,4 @@ pub mod posts;
|
||||
#[derive(Default)]
|
||||
pub struct Api {
|
||||
pub posts: posts::PostEndpoint,
|
||||
}
|
||||
}
|
||||
|
@ -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 plume_models::{
|
||||
Connection,
|
||||
instance::*,
|
||||
safe_string::SafeString,
|
||||
};
|
||||
|
||||
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("instance")
|
||||
@ -42,22 +38,33 @@ pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||
}
|
||||
|
||||
fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||
let domain = args.value_of("domain").map(String::from)
|
||||
.unwrap_or_else(|| env::var("BASE_URL")
|
||||
.unwrap_or_else(|_| super::ask_for("Domain name")));
|
||||
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 domain = args
|
||||
.value_of("domain")
|
||||
.map(String::from)
|
||||
.unwrap_or_else(|| env::var("BASE_URL").unwrap_or_else(|_| super::ask_for("Domain name")));
|
||||
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");
|
||||
|
||||
Instance::insert(conn, NewInstance {
|
||||
public_domain: domain,
|
||||
name,
|
||||
local: true,
|
||||
long_description: SafeString::new(""),
|
||||
short_description: SafeString::new(""),
|
||||
default_license: license,
|
||||
open_registrations: open_reg,
|
||||
short_description_html: String::new(),
|
||||
long_description_html: String::new()
|
||||
}).expect("Couldn't save instance");
|
||||
Instance::insert(
|
||||
conn,
|
||||
NewInstance {
|
||||
public_domain: domain,
|
||||
name,
|
||||
local: true,
|
||||
long_description: SafeString::new(""),
|
||||
short_description: SafeString::new(""),
|
||||
default_license: license,
|
||||
open_registrations: open_reg,
|
||||
short_description_html: String::new(),
|
||||
long_description_html: String::new(),
|
||||
},
|
||||
)
|
||||
.expect("Couldn't save instance");
|
||||
}
|
||||
|
@ -6,12 +6,12 @@ extern crate rpassword;
|
||||
|
||||
use clap::App;
|
||||
use diesel::Connection;
|
||||
use plume_models::{Connection as Conn, DATABASE_URL};
|
||||
use std::io::{self, prelude::*};
|
||||
use plume_models::{DATABASE_URL, Connection as Conn};
|
||||
|
||||
mod instance;
|
||||
mod users;
|
||||
mod search;
|
||||
mod users;
|
||||
|
||||
fn main() {
|
||||
let mut app = App::new("Plume CLI")
|
||||
@ -27,10 +27,16 @@ fn main() {
|
||||
let conn = Conn::establish(DATABASE_URL.as_str());
|
||||
|
||||
match matches.subcommand() {
|
||||
("instance", Some(args)) => instance::run(args, &conn.expect("Couldn't connect to the database.")),
|
||||
("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")
|
||||
("instance", Some(args)) => {
|
||||
instance::run(args, &conn.expect("Couldn't connect to the database."))
|
||||
}
|
||||
("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);
|
||||
io::stdout().flush().expect("Couldn't flush STDOUT");
|
||||
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
|
||||
}
|
||||
|
@ -1,47 +1,56 @@
|
||||
use clap::{Arg, ArgMatches, App, SubCommand};
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
|
||||
use plume_models::{posts::Post, schema::posts, search::Searcher, Connection};
|
||||
use std::fs::{read_dir, remove_file};
|
||||
use std::io::ErrorKind;
|
||||
use std::path::Path;
|
||||
use plume_models::{
|
||||
Connection,
|
||||
posts::Post,
|
||||
schema::posts,
|
||||
search::Searcher,
|
||||
};
|
||||
|
||||
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("search")
|
||||
.about("Manage search index")
|
||||
.subcommand(SubCommand::with_name("init")
|
||||
.arg(Arg::with_name("path")
|
||||
.short("p")
|
||||
.long("path")
|
||||
.takes_value(true)
|
||||
.required(false)
|
||||
.help("Path to Plume's working directory"))
|
||||
.arg(Arg::with_name("force")
|
||||
.short("f")
|
||||
.long("force")
|
||||
.help("Ignore already using directory")
|
||||
).about("Initialize Plume's internal search engine"))
|
||||
.subcommand(SubCommand::with_name("refill")
|
||||
.arg(Arg::with_name("path")
|
||||
.short("p")
|
||||
.long("path")
|
||||
.takes_value(true)
|
||||
.required(false)
|
||||
.help("Path to Plume's working directory")
|
||||
).about("Regenerate Plume's search index"))
|
||||
.subcommand(SubCommand::with_name("unlock")
|
||||
.arg(Arg::with_name("path")
|
||||
.short("p")
|
||||
.long("path")
|
||||
.takes_value(true)
|
||||
.required(false)
|
||||
.help("Path to Plume's working directory")
|
||||
).about("Release lock on search directory"))
|
||||
.subcommand(
|
||||
SubCommand::with_name("init")
|
||||
.arg(
|
||||
Arg::with_name("path")
|
||||
.short("p")
|
||||
.long("path")
|
||||
.takes_value(true)
|
||||
.required(false)
|
||||
.help("Path to Plume's working directory"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("force")
|
||||
.short("f")
|
||||
.long("force")
|
||||
.help("Ignore already using directory"),
|
||||
)
|
||||
.about("Initialize Plume's internal search engine"),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("refill")
|
||||
.arg(
|
||||
Arg::with_name("path")
|
||||
.short("p")
|
||||
.long("path")
|
||||
.takes_value(true)
|
||||
.required(false)
|
||||
.help("Path to Plume's working directory"),
|
||||
)
|
||||
.about("Regenerate Plume's search index"),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("unlock")
|
||||
.arg(
|
||||
Arg::with_name("path")
|
||||
.short("p")
|
||||
.long("path")
|
||||
.takes_value(true)
|
||||
.required(false)
|
||||
.help("Path to Plume's working directory"),
|
||||
)
|
||||
.about("Release lock on search directory"),
|
||||
)
|
||||
}
|
||||
|
||||
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 path = Path::new(path).join("search_index");
|
||||
|
||||
let can_do = match read_dir(path.clone()) { // try to read the directory specified
|
||||
Ok(mut contents) => contents.next().is_none(),
|
||||
Err(e) => if e.kind() == ErrorKind::NotFound {
|
||||
true
|
||||
} else {
|
||||
panic!("Error while initialising search index : {}", e);
|
||||
let can_do = match read_dir(path.clone()) {
|
||||
// try to read the directory specified
|
||||
Ok(mut contents) => contents.next().is_none(),
|
||||
Err(e) => {
|
||||
if e.kind() == ErrorKind::NotFound {
|
||||
true
|
||||
} else {
|
||||
panic!("Error while initialising search index : {}", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
if can_do || force {
|
||||
let searcher = Searcher::create(&path).unwrap();
|
||||
refill(args, conn, Some(searcher));
|
||||
} 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");
|
||||
|
||||
let len = posts.len();
|
||||
for (i,post) in posts.iter().enumerate() {
|
||||
println!("Importing {}/{} : {}", i+1, len, post.title);
|
||||
searcher.update_document(conn, &post).expect("Couldn't import post");
|
||||
for (i, post) in posts.iter().enumerate() {
|
||||
println!("Importing {}/{} : {}", i + 1, len, post.title);
|
||||
searcher
|
||||
.update_document(conn, &post)
|
||||
.expect("Couldn't import post");
|
||||
}
|
||||
println!("Commiting result");
|
||||
searcher.commit();
|
||||
}
|
||||
|
||||
|
||||
fn unlock<'a>(args: &ArgMatches<'a>) {
|
||||
let path = args.value_of("path").unwrap_or(".");
|
||||
let path = Path::new(path).join("search_index/.tantivy-indexer.lock");
|
||||
|
@ -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 std::io::{self, Write};
|
||||
use plume_models::{
|
||||
Connection,
|
||||
instance::Instance,
|
||||
users::*,
|
||||
};
|
||||
|
||||
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("users")
|
||||
.about("Manage users")
|
||||
.subcommand(SubCommand::with_name("new")
|
||||
.arg(Arg::with_name("name")
|
||||
.short("n")
|
||||
.long("name")
|
||||
.alias("username")
|
||||
.takes_value(true)
|
||||
.help("The username of the new user")
|
||||
).arg(Arg::with_name("display-name")
|
||||
.short("N")
|
||||
.long("display-name")
|
||||
.takes_value(true)
|
||||
.help("The display name of the new user")
|
||||
).arg(Arg::with_name("biography")
|
||||
.short("b")
|
||||
.long("bio")
|
||||
.alias("biography")
|
||||
.takes_value(true)
|
||||
.help("The biography of the new user")
|
||||
).arg(Arg::with_name("email")
|
||||
.short("e")
|
||||
.long("email")
|
||||
.takes_value(true)
|
||||
.help("Email address of the new user")
|
||||
).arg(Arg::with_name("password")
|
||||
.short("p")
|
||||
.long("password")
|
||||
.takes_value(true)
|
||||
.help("The password of the new user")
|
||||
).arg(Arg::with_name("admin")
|
||||
.short("a")
|
||||
.long("admin")
|
||||
.help("Makes the user an administrator of the instance")
|
||||
).about("Create a new user on this instance"))
|
||||
.subcommand(SubCommand::with_name("reset-password")
|
||||
.arg(Arg::with_name("name")
|
||||
.short("u")
|
||||
.long("user")
|
||||
.alias("username")
|
||||
.takes_value(true)
|
||||
.help("The username of the user to reset password to")
|
||||
).arg(Arg::with_name("password")
|
||||
.short("p")
|
||||
.long("password")
|
||||
.takes_value(true)
|
||||
.help("The password new for the user")
|
||||
).about("Reset user password"))
|
||||
.subcommand(
|
||||
SubCommand::with_name("new")
|
||||
.arg(
|
||||
Arg::with_name("name")
|
||||
.short("n")
|
||||
.long("name")
|
||||
.alias("username")
|
||||
.takes_value(true)
|
||||
.help("The username of the new user"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("display-name")
|
||||
.short("N")
|
||||
.long("display-name")
|
||||
.takes_value(true)
|
||||
.help("The display name of the new user"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("biography")
|
||||
.short("b")
|
||||
.long("bio")
|
||||
.alias("biography")
|
||||
.takes_value(true)
|
||||
.help("The biography of the new user"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("email")
|
||||
.short("e")
|
||||
.long("email")
|
||||
.takes_value(true)
|
||||
.help("Email address of the new user"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("password")
|
||||
.short("p")
|
||||
.long("password")
|
||||
.takes_value(true)
|
||||
.help("The password of the new user"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("admin")
|
||||
.short("a")
|
||||
.long("admin")
|
||||
.help("Makes the user an administrator of the instance"),
|
||||
)
|
||||
.about("Create a new user on this instance"),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("reset-password")
|
||||
.arg(
|
||||
Arg::with_name("name")
|
||||
.short("u")
|
||||
.long("user")
|
||||
.alias("username")
|
||||
.takes_value(true)
|
||||
.help("The username of the user to reset password to"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("password")
|
||||
.short("p")
|
||||
.long("password")
|
||||
.takes_value(true)
|
||||
.help("The password new for the user"),
|
||||
)
|
||||
.about("Reset user password"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||
@ -69,16 +85,28 @@ pub fn run<'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 display_name = args.value_of("display-name").map(String::from).unwrap_or_else(|| super::ask_for("Display name"));
|
||||
let username = args
|
||||
.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 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 password = args.value_of("password").map(String::from).unwrap_or_else(|| {
|
||||
print!("Password: ");
|
||||
io::stdout().flush().expect("Couldn't flush STDOUT");
|
||||
rpassword::read_password().expect("Couldn't read your password.")
|
||||
});
|
||||
let email = args
|
||||
.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: ");
|
||||
io::stdout().flush().expect("Couldn't flush STDOUT");
|
||||
rpassword::read_password().expect("Couldn't read your password.")
|
||||
});
|
||||
|
||||
NewUser::new_local(
|
||||
conn,
|
||||
@ -88,17 +116,31 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||
&bio,
|
||||
email,
|
||||
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) {
|
||||
let username = args.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");
|
||||
let password = args.value_of("password").map(String::from).unwrap_or_else(|| {
|
||||
print!("Password: ");
|
||||
io::stdout().flush().expect("Couldn't flush STDOUT");
|
||||
rpassword::read_password().expect("Couldn't read your password.")
|
||||
});
|
||||
user.reset_password(conn, &password).expect("Failed to reset password");
|
||||
let username = args
|
||||
.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");
|
||||
let password = args
|
||||
.value_of("password")
|
||||
.map(String::from)
|
||||
.unwrap_or_else(|| {
|
||||
print!("Password: ");
|
||||
io::stdout().flush().expect("Couldn't flush STDOUT");
|
||||
rpassword::read_password().expect("Couldn't read your password.")
|
||||
});
|
||||
user.reset_password(conn, &password)
|
||||
.expect("Failed to reset password");
|
||||
}
|
||||
|
@ -18,7 +18,8 @@ pub mod sign;
|
||||
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 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> {
|
||||
vec![
|
||||
@ -114,13 +115,18 @@ pub fn broadcast<S: sign::Signer, A: Activity, T: inbox::WithInbox>(
|
||||
let boxes = to
|
||||
.into_iter()
|
||||
.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>>()
|
||||
.unique();
|
||||
|
||||
let mut act = serde_json::to_value(act).expect("activity_pub::broadcast: serialization error");
|
||||
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 {
|
||||
// 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()
|
||||
.post(&inbox)
|
||||
.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)
|
||||
.send();
|
||||
match res {
|
||||
|
@ -1,12 +1,12 @@
|
||||
use base64;
|
||||
use chrono::{offset::Utc, DateTime};
|
||||
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::time::SystemTime;
|
||||
|
||||
use activity_pub::{AP_CONTENT_TYPE, ap_accept_header};
|
||||
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"));
|
||||
|
||||
@ -42,7 +42,7 @@ impl Digest {
|
||||
}
|
||||
|
||||
pub fn verify_header(&self, other: &Digest) -> bool {
|
||||
self.value()==other.value()
|
||||
self.value() == other.value()
|
||||
}
|
||||
|
||||
pub fn algorithm(&self) -> &str {
|
||||
@ -57,7 +57,8 @@ impl Digest {
|
||||
let pos = self
|
||||
.0
|
||||
.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")
|
||||
}
|
||||
|
||||
@ -75,8 +76,11 @@ impl Digest {
|
||||
}
|
||||
|
||||
pub fn from_body(body: &str) -> Self {
|
||||
let mut hasher = Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error");
|
||||
hasher.update(body.as_bytes()).expect("Digest::digest: content insertion error");
|
||||
let mut hasher =
|
||||
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"));
|
||||
Digest(format!("SHA-256={}", res))
|
||||
}
|
||||
@ -99,7 +103,8 @@ pub fn headers() -> HeaderMap {
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
).expect("request::headers: accept error"),
|
||||
)
|
||||
.expect("request::headers: accept error"),
|
||||
);
|
||||
headers.insert(CONTENT_TYPE, HeaderValue::from_static(AP_CONTENT_TYPE));
|
||||
headers
|
||||
|
@ -1,7 +1,6 @@
|
||||
use super::request;
|
||||
use base64;
|
||||
use chrono::{DateTime, Duration,
|
||||
naive::NaiveDateTime, Utc};
|
||||
use chrono::{naive::NaiveDateTime, DateTime, Duration, Utc};
|
||||
use hex;
|
||||
use openssl::{pkey::PKey, rsa::Rsa, sha::sha256};
|
||||
use rocket::http::HeaderMap;
|
||||
@ -57,9 +56,10 @@ impl Signable for serde_json::Value {
|
||||
|
||||
let options_hash = Self::hash(
|
||||
&json!({
|
||||
"@context": "https://w3id.org/identity/v1",
|
||||
"created": creation_date
|
||||
}).to_string(),
|
||||
"@context": "https://w3id.org/identity/v1",
|
||||
"created": creation_date
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
let document_hash = Self::hash(&self.to_string());
|
||||
let to_be_signed = options_hash + &document_hash;
|
||||
@ -91,7 +91,8 @@ impl Signable for serde_json::Value {
|
||||
&json!({
|
||||
"@context": "https://w3id.org/identity/v1",
|
||||
"created": creation_date
|
||||
}).to_string(),
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
let creation_date = creation_date.as_str();
|
||||
if creation_date.is_none() {
|
||||
@ -169,7 +170,10 @@ pub fn verify_http_headers<S: Signer + ::std::fmt::Debug>(
|
||||
.collect::<Vec<_>>()
|
||||
.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;
|
||||
}
|
||||
if !headers.contains(&"digest") {
|
||||
|
@ -10,8 +10,8 @@ extern crate chrono;
|
||||
extern crate failure;
|
||||
#[macro_use]
|
||||
extern crate failure_derive;
|
||||
extern crate hex;
|
||||
extern crate heck;
|
||||
extern crate hex;
|
||||
extern crate openssl;
|
||||
extern crate pulldown_cmark;
|
||||
extern crate reqwest;
|
||||
|
@ -1,18 +1,20 @@
|
||||
use heck::CamelCase;
|
||||
use openssl::rand::rand_bytes;
|
||||
use pulldown_cmark::{Event, Parser, Options, Tag, html};
|
||||
use pulldown_cmark::{html, Event, Options, Parser, Tag};
|
||||
use rocket::{
|
||||
http::uri::Uri,
|
||||
response::{Redirect, Flash}
|
||||
response::{Flash, Redirect},
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Generates an hexadecimal representation of 32 bytes of random data
|
||||
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");
|
||||
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
|
||||
@ -29,7 +31,11 @@ pub fn make_actor_id(name: &str) -> String {
|
||||
* 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> {
|
||||
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)]
|
||||
@ -45,117 +51,161 @@ pub fn md_to_html(md: &str, base_url: &str) -> (String, HashSet<String>, HashSet
|
||||
let parser = Parser::new_ext(md, Options::all());
|
||||
|
||||
let (parser, mentions, hashtags): (Vec<Event>, Vec<String>, Vec<String>) = parser
|
||||
.scan(None, |state: &mut Option<String>, evt|{
|
||||
let (s, res) = match evt {
|
||||
Event::Text(txt) => match state.take() {
|
||||
Some(mut prev_txt) => {
|
||||
prev_txt.push_str(&txt);
|
||||
(Some(prev_txt), vec![])
|
||||
},
|
||||
None => {
|
||||
(Some(txt.into_owned()), vec![])
|
||||
}
|
||||
},
|
||||
e => match state.take() {
|
||||
Some(prev) => (None, vec![Event::Text(Cow::Owned(prev)), e]),
|
||||
None => (None, vec![e]),
|
||||
}
|
||||
};
|
||||
*state = s;
|
||||
Some(res)
|
||||
})
|
||||
.flat_map(|v| v.into_iter())
|
||||
.map(|evt| match evt {
|
||||
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| {
|
||||
match state {
|
||||
State::Mention => {
|
||||
let char_matches = c.is_alphanumeric() || "@.-_".contains(c);
|
||||
if char_matches && (n < (txt.chars().count() - 1)) {
|
||||
text_acc.push(c);
|
||||
(events, State::Mention, text_acc, n + 1, mentions, hashtags)
|
||||
} else {
|
||||
if char_matches {
|
||||
text_acc.push(c)
|
||||
}
|
||||
let mention = text_acc;
|
||||
let short_mention = mention.splitn(1, '@').nth(0).unwrap_or("");
|
||||
let link = Tag::Link(format!("//{}/@/{}/", base_url, &mention).into(), short_mention.to_owned().into());
|
||||
|
||||
mentions.push(mention.clone());
|
||||
events.push(Event::Start(link.clone()));
|
||||
events.push(Event::Text(format!("@{}", &short_mention).into()));
|
||||
events.push(Event::End(link));
|
||||
|
||||
(events, State::Ready, c.to_string(), n + 1, mentions, hashtags)
|
||||
}
|
||||
.scan(None, |state: &mut Option<String>, evt| {
|
||||
let (s, res) = match evt {
|
||||
Event::Text(txt) => match state.take() {
|
||||
Some(mut prev_txt) => {
|
||||
prev_txt.push_str(&txt);
|
||||
(Some(prev_txt), vec![])
|
||||
}
|
||||
State::Hashtag => {
|
||||
let char_matches = c.is_alphanumeric() || "-_".contains(c);
|
||||
if char_matches && (n < (txt.chars().count() -1)) {
|
||||
text_acc.push(c);
|
||||
(events, State::Hashtag, text_acc, n+1, mentions, hashtags)
|
||||
} else {
|
||||
if char_matches {
|
||||
None => (Some(txt.into_owned()), vec![]),
|
||||
},
|
||||
e => match state.take() {
|
||||
Some(prev) => (None, vec![Event::Text(Cow::Owned(prev)), e]),
|
||||
None => (None, vec![e]),
|
||||
},
|
||||
};
|
||||
*state = s;
|
||||
Some(res)
|
||||
})
|
||||
.flat_map(|v| v.into_iter())
|
||||
.map(|evt| match evt {
|
||||
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| {
|
||||
match state {
|
||||
State::Mention => {
|
||||
let char_matches = c.is_alphanumeric() || "@.-_".contains(c);
|
||||
if char_matches && (n < (txt.chars().count() - 1)) {
|
||||
text_acc.push(c);
|
||||
(events, State::Mention, text_acc, n + 1, mentions, hashtags)
|
||||
} else {
|
||||
if char_matches {
|
||||
text_acc.push(c)
|
||||
}
|
||||
let mention = text_acc;
|
||||
let short_mention = mention.splitn(1, '@').nth(0).unwrap_or("");
|
||||
let link = Tag::Link(
|
||||
format!("//{}/@/{}/", base_url, &mention).into(),
|
||||
short_mention.to_owned().into(),
|
||||
);
|
||||
|
||||
mentions.push(mention.clone());
|
||||
events.push(Event::Start(link.clone()));
|
||||
events.push(Event::Text(format!("@{}", &short_mention).into()));
|
||||
events.push(Event::End(link));
|
||||
|
||||
(
|
||||
events,
|
||||
State::Ready,
|
||||
c.to_string(),
|
||||
n + 1,
|
||||
mentions,
|
||||
hashtags,
|
||||
)
|
||||
}
|
||||
}
|
||||
State::Hashtag => {
|
||||
let char_matches = c.is_alphanumeric() || "-_".contains(c);
|
||||
if char_matches && (n < (txt.chars().count() - 1)) {
|
||||
text_acc.push(c);
|
||||
(events, State::Hashtag, text_acc, n + 1, mentions, hashtags)
|
||||
} else {
|
||||
if char_matches {
|
||||
text_acc.push(c);
|
||||
}
|
||||
let hashtag = text_acc;
|
||||
let link = Tag::Link(
|
||||
format!("//{}/tag/{}", base_url, &hashtag.to_camel_case())
|
||||
.into(),
|
||||
hashtag.to_owned().into(),
|
||||
);
|
||||
|
||||
hashtags.push(hashtag.clone());
|
||||
events.push(Event::Start(link.clone()));
|
||||
events.push(Event::Text(format!("#{}", &hashtag).into()));
|
||||
events.push(Event::End(link));
|
||||
|
||||
(
|
||||
events,
|
||||
State::Ready,
|
||||
c.to_string(),
|
||||
n + 1,
|
||||
mentions,
|
||||
hashtags,
|
||||
)
|
||||
}
|
||||
}
|
||||
State::Ready => {
|
||||
if c == '@' {
|
||||
events.push(Event::Text(text_acc.into()));
|
||||
(
|
||||
events,
|
||||
State::Mention,
|
||||
String::new(),
|
||||
n + 1,
|
||||
mentions,
|
||||
hashtags,
|
||||
)
|
||||
} else if c == '#' {
|
||||
events.push(Event::Text(text_acc.into()));
|
||||
(
|
||||
events,
|
||||
State::Hashtag,
|
||||
String::new(),
|
||||
n + 1,
|
||||
mentions,
|
||||
hashtags,
|
||||
)
|
||||
} else if c.is_alphanumeric() {
|
||||
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.
|
||||
events.push(Event::Text(text_acc.clone().into()))
|
||||
}
|
||||
(events, State::Word, text_acc, n + 1, mentions, hashtags)
|
||||
} else {
|
||||
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.
|
||||
events.push(Event::Text(text_acc.clone().into()))
|
||||
}
|
||||
(events, State::Ready, text_acc, n + 1, mentions, hashtags)
|
||||
}
|
||||
}
|
||||
State::Word => {
|
||||
text_acc.push(c);
|
||||
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.
|
||||
events.push(Event::Text(text_acc.clone().into()))
|
||||
}
|
||||
(events, State::Word, text_acc, n + 1, mentions, hashtags)
|
||||
} else {
|
||||
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, State::Ready, text_acc, n + 1, mentions, hashtags)
|
||||
}
|
||||
}
|
||||
let hashtag = text_acc;
|
||||
let link = Tag::Link(format!("//{}/tag/{}", base_url, &hashtag.to_camel_case()).into(), hashtag.to_owned().into());
|
||||
|
||||
hashtags.push(hashtag.clone());
|
||||
events.push(Event::Start(link.clone()));
|
||||
events.push(Event::Text(format!("#{}", &hashtag).into()));
|
||||
events.push(Event::End(link));
|
||||
|
||||
(events, State::Ready, c.to_string(), n + 1, mentions, hashtags)
|
||||
}
|
||||
}
|
||||
State::Ready => {
|
||||
if c == '@' {
|
||||
events.push(Event::Text(text_acc.into()));
|
||||
(events, State::Mention, String::new(), n + 1, mentions, hashtags)
|
||||
} else if c == '#' {
|
||||
events.push(Event::Text(text_acc.into()));
|
||||
(events, State::Hashtag, String::new(), n + 1, mentions, hashtags)
|
||||
} else if c.is_alphanumeric() {
|
||||
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.
|
||||
events.push(Event::Text(text_acc.clone().into()))
|
||||
}
|
||||
(events, State::Word, text_acc, n + 1, mentions, hashtags)
|
||||
} else {
|
||||
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.
|
||||
events.push(Event::Text(text_acc.clone().into()))
|
||||
}
|
||||
(events, State::Ready, text_acc, n + 1, mentions, hashtags)
|
||||
}
|
||||
}
|
||||
State::Word => {
|
||||
text_acc.push(c);
|
||||
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.
|
||||
events.push(Event::Text(text_acc.clone().into()))
|
||||
}
|
||||
(events, State::Word, text_acc, n + 1, mentions, hashtags)
|
||||
} else {
|
||||
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, 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)| {
|
||||
parser.append(&mut p);
|
||||
mention.append(&mut m);
|
||||
hashtag.append(&mut h);
|
||||
(parser, mention, hashtag)
|
||||
});
|
||||
},
|
||||
);
|
||||
(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);
|
||||
mention.append(&mut m);
|
||||
hashtag.append(&mut h);
|
||||
(parser, mention, hashtag)
|
||||
},
|
||||
);
|
||||
let parser = parser.into_iter();
|
||||
let mentions = mentions.into_iter().map(|m| String::from(m.trim()));
|
||||
let hashtags = hashtags.into_iter().map(|h| String::from(h.trim()));
|
||||
@ -188,7 +238,13 @@ mod 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 {
|
||||
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>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,7 @@
|
||||
use stdweb::{unstable::{TryInto, TryFrom}, web::{*, html_element::*, event::*}};
|
||||
use stdweb::{
|
||||
unstable::{TryFrom, TryInto},
|
||||
web::{event::*, html_element::*, *},
|
||||
};
|
||||
use CATALOG;
|
||||
|
||||
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 inp: Result<InputElement, _> = elt.clone().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) {
|
||||
@ -64,7 +68,7 @@ fn init_widget(
|
||||
tag: &'static str,
|
||||
placeholder_text: String,
|
||||
content: String,
|
||||
disable_return: bool
|
||||
disable_return: bool,
|
||||
) -> Result<HtmlElement, EditorError> {
|
||||
let widget = placeholder(make_editable(tag).try_into()?, &placeholder_text);
|
||||
if !content.is_empty() {
|
||||
@ -86,7 +90,7 @@ fn init_widget(
|
||||
pub fn init() -> Result<(), EditorError> {
|
||||
if let Some(ed) = document().get_element_by_id("plume-editor") {
|
||||
// Show the editor
|
||||
js!{ @{&ed}.style.display = "block"; };
|
||||
js! { @{&ed}.style.display = "block"; };
|
||||
// And hide the HTML-only fallback
|
||||
let old_ed = document().get_element_by_id("plume-fallback-editor")?;
|
||||
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");
|
||||
// And pre-fill the new editor with this values
|
||||
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 content = init_widget(&ed, "article", i18n!(CATALOG, "Write your article here. Markdown is supported."), content_val.clone(), true)?;
|
||||
let subtitle = init_widget(
|
||||
&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}; };
|
||||
|
||||
// character counter
|
||||
@ -118,27 +134,38 @@ pub fn init() -> Result<(), EditorError> {
|
||||
}), 0);
|
||||
}));
|
||||
|
||||
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(||
|
||||
init_popup(&title, &subtitle, &content, &old_ed).ok()
|
||||
).unwrap();
|
||||
let bg = document().get_element_by_id("popup-bg").or_else(||
|
||||
init_popup_bg().ok()
|
||||
).unwrap();
|
||||
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(||
|
||||
init_popup(&title, &subtitle, &content, &old_ed).ok()
|
||||
).unwrap();
|
||||
let bg = document().get_element_by_id("popup-bg").or_else(||
|
||||
init_popup_bg().ok()
|
||||
).unwrap();
|
||||
|
||||
popup.class_list().add("show").unwrap();
|
||||
bg.class_list().add("show").unwrap();
|
||||
}));
|
||||
popup.class_list().add("show").unwrap();
|
||||
bg.class_list().add("show").unwrap();
|
||||
}),
|
||||
);
|
||||
}
|
||||
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")?;
|
||||
popup.class_list().add("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");
|
||||
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);
|
||||
@ -152,7 +179,7 @@ fn init_popup(title: &HtmlElement, subtitle: &HtmlElement, content: &HtmlElement
|
||||
popup.append_child(&cover);
|
||||
|
||||
let button = document().create_element("input")?;
|
||||
js!{
|
||||
js! {
|
||||
@{&button}.type = "submit";
|
||||
@{&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> {
|
||||
match document().query_selector(selector) {
|
||||
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! {
|
||||
let x = encodeURIComponent(@{content}.innerHTML)
|
||||
.replace(/%20/g, "+")
|
||||
@ -198,7 +228,10 @@ fn chars_left(selector: &str, content: &HtmlElement) -> Option<i32> {
|
||||
.length + 2;
|
||||
console.log(x);
|
||||
return x;
|
||||
}).try_into().map(|c: i32| len - c).ok()
|
||||
})
|
||||
.try_into()
|
||||
.map(|c: i32| len - c)
|
||||
.ok()
|
||||
} else {
|
||||
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.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("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 {
|
||||
let elt = document().create_element(tag).expect("Couldn't create editable element");
|
||||
elt.set_attribute("contenteditable", "true").expect("Couldn't make element editable");
|
||||
let elt = document()
|
||||
.create_element(tag)
|
||||
.expect("Couldn't create editable element");
|
||||
elt.set_attribute("contenteditable", "true")
|
||||
.expect("Couldn't make element editable");
|
||||
elt
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
#![recursion_limit="128"]
|
||||
#![recursion_limit = "128"]
|
||||
#![feature(decl_macro, proc_macro_hygiene, try_trait)]
|
||||
|
||||
extern crate gettext;
|
||||
@ -9,7 +9,7 @@ extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate stdweb;
|
||||
|
||||
use stdweb::{web::{*, event::*}};
|
||||
use stdweb::web::{event::*, *};
|
||||
|
||||
init_i18n!("plume-front", en, fr);
|
||||
|
||||
@ -20,9 +20,14 @@ compile_i18n!();
|
||||
lazy_static! {
|
||||
static ref CATALOG: gettext::Catalog = {
|
||||
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");
|
||||
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();
|
||||
search();
|
||||
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
|
||||
@ -41,10 +47,14 @@ fn menu() {
|
||||
if let Some(button) = document().get_element_by_id("menu") {
|
||||
if let Some(menu) = document().get_element_by_id("content") {
|
||||
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| {
|
||||
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,18 +64,21 @@ fn menu() {
|
||||
fn search() {
|
||||
if let Some(form) = document().get_element_by_id("form") {
|
||||
form.add_event_listener(|_: SubmitEvent| {
|
||||
document().query_selector_all("#form input").map(|inputs| {
|
||||
for input in inputs {
|
||||
js! {
|
||||
if (@{&input}.name === "") {
|
||||
@{&input}.name = @{&input}.id
|
||||
}
|
||||
if (@{&input}.name && !@{&input}.value) {
|
||||
@{&input}.name = "";
|
||||
document()
|
||||
.query_selector_all("#form input")
|
||||
.map(|inputs| {
|
||||
for input in inputs {
|
||||
js! {
|
||||
if (@{&input}.name === "") {
|
||||
@{&input}.name = @{&input}.id
|
||||
}
|
||||
if (@{&input}.name && !@{&input}.value) {
|
||||
@{&input}.name = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}).ok();
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -89,13 +89,19 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApiToken {
|
||||
}
|
||||
|
||||
let mut parsed_header = headers[0].split(' ');
|
||||
let auth_type = parsed_header.next()
|
||||
.map_or_else(|| Outcome::Failure((Status::BadRequest, TokenError::NoType)), Outcome::Success)?;
|
||||
let val = parsed_header.next()
|
||||
.map_or_else(|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)), Outcome::Success)?;
|
||||
let auth_type = parsed_header.next().map_or_else(
|
||||
|| Outcome::Failure((Status::BadRequest, TokenError::NoType)),
|
||||
Outcome::Success,
|
||||
)?;
|
||||
let val = parsed_header.next().map_or_else(
|
||||
|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)),
|
||||
Outcome::Success,
|
||||
)?;
|
||||
|
||||
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) {
|
||||
return Outcome::Success(token);
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use plume_api::apps::AppEndpoint;
|
||||
use plume_common::utils::random_hex;
|
||||
use schema::apps;
|
||||
use {Connection, Error, Result, ApiResult};
|
||||
use {ApiResult, Connection, Error, Result};
|
||||
|
||||
#[derive(Clone, Queryable)]
|
||||
pub struct App {
|
||||
@ -52,7 +52,8 @@ impl Provider<Connection> for App {
|
||||
redirect_uri: data.redirect_uri,
|
||||
website: data.website,
|
||||
},
|
||||
).map_err(|_| ApiError::NotFound("Couldn't register app".into()))?;
|
||||
)
|
||||
.map_err(|_| ApiError::NotFound("Couldn't register app".into()))?;
|
||||
|
||||
Ok(AppEndpoint {
|
||||
id: Some(app.id),
|
||||
|
@ -26,7 +26,7 @@ use safe_string::SafeString;
|
||||
use schema::blogs;
|
||||
use search::Searcher;
|
||||
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>;
|
||||
|
||||
@ -66,27 +66,15 @@ impl Blog {
|
||||
insert!(blogs, NewBlog, |inserted, conn| {
|
||||
let instance = inserted.get_instance(conn)?;
|
||||
if inserted.outbox_url.is_empty() {
|
||||
inserted.outbox_url = instance.compute_box(
|
||||
BLOG_PREFIX,
|
||||
&inserted.actor_id,
|
||||
"outbox",
|
||||
);
|
||||
inserted.outbox_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, "outbox");
|
||||
}
|
||||
|
||||
if inserted.inbox_url.is_empty() {
|
||||
inserted.inbox_url = instance.compute_box(
|
||||
BLOG_PREFIX,
|
||||
&inserted.actor_id,
|
||||
"inbox",
|
||||
);
|
||||
inserted.inbox_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, "inbox");
|
||||
}
|
||||
|
||||
if inserted.ap_url.is_empty() {
|
||||
inserted.ap_url = instance.compute_box(
|
||||
BLOG_PREFIX,
|
||||
&inserted.actor_id,
|
||||
"",
|
||||
);
|
||||
inserted.ap_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, "");
|
||||
}
|
||||
|
||||
if inserted.fqn.is_empty() {
|
||||
@ -154,16 +142,12 @@ impl 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()
|
||||
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
|
||||
.ok_or(Error::Webfinger)
|
||||
.and_then(|l| {
|
||||
Blog::fetch_from_url(
|
||||
conn,
|
||||
&l.href?
|
||||
)
|
||||
})
|
||||
.and_then(|l| Blog::fetch_from_url(conn, &l.href?))
|
||||
}
|
||||
|
||||
fn fetch_from_url(conn: &Connection, url: &str) -> Result<Blog> {
|
||||
@ -181,20 +165,14 @@ impl Blog {
|
||||
.send()?;
|
||||
|
||||
let text = &res.text()?;
|
||||
let ap_sign: ApSignature =
|
||||
serde_json::from_str(text)?;
|
||||
let mut json: CustomGroup =
|
||||
serde_json::from_str(text)?;
|
||||
let ap_sign: ApSignature = 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
|
||||
Blog::from_activity(
|
||||
conn,
|
||||
&json,
|
||||
Url::parse(url)?.host_str()?,
|
||||
)
|
||||
Blog::from_activity(conn, &json, Url::parse(url)?.host_str()?)
|
||||
}
|
||||
|
||||
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(
|
||||
conn,
|
||||
NewInstance {
|
||||
@ -210,35 +188,17 @@ impl Blog {
|
||||
long_description_html: String::new(),
|
||||
},
|
||||
)
|
||||
)?;
|
||||
})?;
|
||||
Blog::insert(
|
||||
conn,
|
||||
NewBlog {
|
||||
actor_id: acct
|
||||
.object
|
||||
.ap_actor_props
|
||||
.preferred_username_string()?,
|
||||
title: acct
|
||||
.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()?,
|
||||
actor_id: acct.object.ap_actor_props.preferred_username_string()?,
|
||||
title: acct.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,
|
||||
ap_url: acct
|
||||
.object
|
||||
.object_props
|
||||
.id_string()?,
|
||||
ap_url: acct.object.object_props.id_string()?,
|
||||
public_key: acct
|
||||
.custom_props
|
||||
.public_key_publickey()?
|
||||
@ -252,27 +212,20 @@ impl Blog {
|
||||
let mut blog = Group::default();
|
||||
blog.ap_actor_props
|
||||
.set_preferred_username_string(self.actor_id.clone())?;
|
||||
blog.object_props
|
||||
.set_name_string(self.title.clone())?;
|
||||
blog.object_props.set_name_string(self.title.clone())?;
|
||||
blog.ap_actor_props
|
||||
.set_outbox_string(self.outbox_url.clone())?;
|
||||
blog.ap_actor_props
|
||||
.set_inbox_string(self.inbox_url.clone())?;
|
||||
blog.object_props
|
||||
.set_summary_string(self.summary.clone())?;
|
||||
blog.object_props
|
||||
.set_id_string(self.ap_url.clone())?;
|
||||
blog.object_props.set_summary_string(self.summary.clone())?;
|
||||
blog.object_props.set_id_string(self.ap_url.clone())?;
|
||||
|
||||
let mut public_key = PublicKey::default();
|
||||
public_key
|
||||
.set_id_string(format!("{}#main-key", self.ap_url))?;
|
||||
public_key
|
||||
.set_owner_string(self.ap_url.clone())?;
|
||||
public_key
|
||||
.set_public_key_pem_string(self.public_key.clone())?;
|
||||
public_key.set_id_string(format!("{}#main-key", self.ap_url))?;
|
||||
public_key.set_owner_string(self.ap_url.clone())?;
|
||||
public_key.set_public_key_pem_string(self.public_key.clone())?;
|
||||
let mut ap_signature = ApSignature::default();
|
||||
ap_signature
|
||||
.set_public_key_publickey(public_key)?;
|
||||
ap_signature.set_public_key_publickey(public_key)?;
|
||||
|
||||
Ok(CustomGroup::new(blog, ap_signature))
|
||||
}
|
||||
@ -290,13 +243,10 @@ impl Blog {
|
||||
}
|
||||
|
||||
pub fn get_keypair(&self) -> Result<PKey<Private>> {
|
||||
PKey::from_rsa(
|
||||
Rsa::private_key_from_pem(
|
||||
self.private_key
|
||||
.clone()?
|
||||
.as_ref(),
|
||||
)?,
|
||||
).map_err(Error::from)
|
||||
PKey::from_rsa(Rsa::private_key_from_pem(
|
||||
self.private_key.clone()?.as_ref(),
|
||||
)?)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
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>> {
|
||||
let key = self.get_keypair()?;
|
||||
let mut signer =
|
||||
Signer::new(MessageDigest::sha256(), &key)?;
|
||||
signer
|
||||
.update(to_sign.as_bytes())?;
|
||||
signer
|
||||
.sign_to_vec()
|
||||
.map_err(Error::from)
|
||||
let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
|
||||
signer.update(to_sign.as_bytes())?;
|
||||
signer.sign_to_vec().map_err(Error::from)
|
||||
}
|
||||
|
||||
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 = Verifier::new(MessageDigest::sha256(), &key)?;
|
||||
verifier
|
||||
.update(data.as_bytes())?;
|
||||
verifier
|
||||
.verify(&signature)
|
||||
.map_err(Error::from)
|
||||
verifier.update(data.as_bytes())?;
|
||||
verifier.verify(&signature).map_err(Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
@ -434,32 +375,47 @@ pub(crate) mod tests {
|
||||
use blog_authors::*;
|
||||
use diesel::Connection;
|
||||
use instance::tests as instance_tests;
|
||||
use search::tests::get_searcher;
|
||||
use tests::db;
|
||||
use users::tests as usersTests;
|
||||
use search::tests::get_searcher;
|
||||
use Connection as Conn;
|
||||
|
||||
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Blog>) {
|
||||
instance_tests::fill_database(conn);
|
||||
let users = usersTests::fill_database(conn);
|
||||
let blog1 = Blog::insert(conn, NewBlog::new_local(
|
||||
"BlogName".to_owned(),
|
||||
"Blog name".to_owned(),
|
||||
"This is a small blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id
|
||||
).unwrap()).unwrap();
|
||||
let blog2 = Blog::insert(conn, NewBlog::new_local(
|
||||
let blog1 = Blog::insert(
|
||||
conn,
|
||||
NewBlog::new_local(
|
||||
"BlogName".to_owned(),
|
||||
"Blog name".to_owned(),
|
||||
"This is a small blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let blog2 = Blog::insert(
|
||||
conn,
|
||||
NewBlog::new_local(
|
||||
"MyBlog".to_owned(),
|
||||
"My blog".to_owned(),
|
||||
"Welcome to my blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id
|
||||
).unwrap()).unwrap();
|
||||
let blog3 = Blog::insert(conn, NewBlog::new_local(
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let blog3 = Blog::insert(
|
||||
conn,
|
||||
NewBlog::new_local(
|
||||
"WhyILikePlume".to_owned(),
|
||||
"Why I like Plume".to_owned(),
|
||||
"In this blog I will explay you why I like Plume so much".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id
|
||||
).unwrap()).unwrap();
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
@ -468,7 +424,8 @@ pub(crate) mod tests {
|
||||
author_id: users[0].id,
|
||||
is_owner: true,
|
||||
},
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
@ -477,7 +434,8 @@ pub(crate) mod tests {
|
||||
author_id: users[1].id,
|
||||
is_owner: false,
|
||||
},
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
@ -486,7 +444,8 @@ pub(crate) mod tests {
|
||||
author_id: users[1].id,
|
||||
is_owner: true,
|
||||
},
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
@ -495,8 +454,9 @@ pub(crate) mod tests {
|
||||
author_id: users[2].id,
|
||||
is_owner: true,
|
||||
},
|
||||
).unwrap();
|
||||
(users, vec![ blog1, blog2, blog3 ])
|
||||
)
|
||||
.unwrap();
|
||||
(users, vec![blog1, blog2, blog3])
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -511,11 +471,16 @@ pub(crate) mod tests {
|
||||
"SomeName".to_owned(),
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id
|
||||
).unwrap(),
|
||||
).unwrap();
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
)
|
||||
.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
|
||||
|
||||
Ok(())
|
||||
@ -535,18 +500,22 @@ pub(crate) mod tests {
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
).unwrap(),
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let b2 = Blog::insert(
|
||||
conn,
|
||||
NewBlog::new_local(
|
||||
"Blog".to_owned(),
|
||||
"Blog".to_owned(),
|
||||
"I've named my blog Blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id
|
||||
).unwrap(),
|
||||
).unwrap();
|
||||
let blog = vec![ b1, b2 ];
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let blog = vec![b1, b2];
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
@ -555,7 +524,8 @@ pub(crate) mod tests {
|
||||
author_id: user[0].id,
|
||||
is_owner: true,
|
||||
},
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
@ -564,7 +534,8 @@ pub(crate) mod tests {
|
||||
author_id: user[1].id,
|
||||
is_owner: false,
|
||||
},
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
@ -573,53 +544,46 @@ pub(crate) mod tests {
|
||||
author_id: user[0].id,
|
||||
is_owner: true,
|
||||
},
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
blog[0]
|
||||
.list_authors(conn).unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[0].id)
|
||||
);
|
||||
assert!(
|
||||
blog[0]
|
||||
.list_authors(conn).unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[1].id)
|
||||
);
|
||||
assert!(
|
||||
blog[1]
|
||||
.list_authors(conn).unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[0].id)
|
||||
);
|
||||
assert!(
|
||||
!blog[1]
|
||||
.list_authors(conn).unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[1].id)
|
||||
);
|
||||
assert!(blog[0]
|
||||
.list_authors(conn)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[0].id));
|
||||
assert!(blog[0]
|
||||
.list_authors(conn)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[1].id));
|
||||
assert!(blog[1]
|
||||
.list_authors(conn)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[0].id));
|
||||
assert!(!blog[1]
|
||||
.list_authors(conn)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[1].id));
|
||||
|
||||
assert!(
|
||||
Blog::find_for_author(conn, &user[0]).unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[0].id)
|
||||
);
|
||||
assert!(
|
||||
Blog::find_for_author(conn, &user[1]).unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[0].id)
|
||||
);
|
||||
assert!(
|
||||
Blog::find_for_author(conn, &user[0]).unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[1].id)
|
||||
);
|
||||
assert!(
|
||||
!Blog::find_for_author(conn, &user[1]).unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[1].id)
|
||||
);
|
||||
assert!(Blog::find_for_author(conn, &user[0])
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[0].id));
|
||||
assert!(Blog::find_for_author(conn, &user[1])
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[0].id));
|
||||
assert!(Blog::find_for_author(conn, &user[0])
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[1].id));
|
||||
assert!(!Blog::find_for_author(conn, &user[1])
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[1].id));
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@ -638,13 +602,12 @@ pub(crate) mod tests {
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
).unwrap(),
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Blog::find_by_fqn(conn, "SomeName").unwrap().id,
|
||||
blog.id
|
||||
);
|
||||
assert_eq!(Blog::find_by_fqn(conn, "SomeName").unwrap().id, blog.id);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@ -663,8 +626,10 @@ pub(crate) mod tests {
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
).unwrap(),
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(blog.fqn, "SomeName");
|
||||
|
||||
@ -699,8 +664,10 @@ pub(crate) mod tests {
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
).unwrap(),
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let b2 = Blog::insert(
|
||||
conn,
|
||||
NewBlog::new_local(
|
||||
@ -708,9 +675,11 @@ pub(crate) mod tests {
|
||||
"Blog".to_owned(),
|
||||
"I've named my blog Blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
).unwrap(),
|
||||
).unwrap();
|
||||
let blog = vec![ b1, b2 ];
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let blog = vec![b1, b2];
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
@ -719,7 +688,8 @@ pub(crate) mod tests {
|
||||
author_id: user[0].id,
|
||||
is_owner: true,
|
||||
},
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
@ -728,7 +698,8 @@ pub(crate) mod tests {
|
||||
author_id: user[1].id,
|
||||
is_owner: false,
|
||||
},
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
@ -737,7 +708,8 @@ pub(crate) mod tests {
|
||||
author_id: user[0].id,
|
||||
is_owner: true,
|
||||
},
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
user[0].delete(conn, &searcher).unwrap();
|
||||
assert!(Blog::get(conn, blog[0].id).is_ok());
|
||||
|
@ -23,7 +23,8 @@ impl CommentSeers {
|
||||
insert!(comment_seers, NewCommentSeers);
|
||||
|
||||
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))
|
||||
.load::<CommentSeers>(conn)
|
||||
.map_err(Error::from)
|
||||
|
@ -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 diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||
use serde_json;
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use comment_seers::{CommentSeers, NewCommentSeers};
|
||||
use instance::Instance;
|
||||
use mentions::Mention;
|
||||
use notifications::*;
|
||||
use plume_common::activity_pub::{
|
||||
inbox::{FromActivity, Notify, Deletable},
|
||||
inbox::{Deletable, FromActivity, Notify},
|
||||
Id, IntoId, PUBLIC_VISIBILTY,
|
||||
};
|
||||
use plume_common::utils;
|
||||
use comment_seers::{CommentSeers, NewCommentSeers};
|
||||
use posts::Post;
|
||||
use safe_string::SafeString;
|
||||
use schema::comments;
|
||||
@ -50,7 +54,11 @@ pub struct NewComment {
|
||||
impl Comment {
|
||||
insert!(comments, NewComment, |inserted, conn| {
|
||||
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)?;
|
||||
}
|
||||
Ok(inserted)
|
||||
@ -80,20 +88,25 @@ impl 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)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn can_see(&self, conn: &Connection, user: Option<&User>) -> bool {
|
||||
self.public_visibility ||
|
||||
user.as_ref().map(|u| CommentSeers::can_see(conn, self, u).unwrap_or(false))
|
||||
self.public_visibility
|
||||
|| user
|
||||
.as_ref()
|
||||
.map(|u| CommentSeers::can_see(conn, self, u).unwrap_or(false))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<Note> {
|
||||
let (html, mentions, _hashtags) = utils::md_to_html(self.content.get().as_ref(),
|
||||
&Instance::get_local(conn)?.public_domain);
|
||||
let (html, mentions, _hashtags) = utils::md_to_html(
|
||||
self.content.get().as_ref(),
|
||||
&Instance::get_local(conn)?.public_domain,
|
||||
);
|
||||
|
||||
let author = User::get(conn, self.author_id)?;
|
||||
let mut note = Note::default();
|
||||
@ -103,8 +116,7 @@ impl Comment {
|
||||
.set_id_string(self.ap_url.clone().unwrap_or_default())?;
|
||||
note.object_props
|
||||
.set_summary_string(self.spoiler_text.clone())?;
|
||||
note.object_props
|
||||
.set_content_string(html)?;
|
||||
note.object_props.set_content_string(html)?;
|
||||
note.object_props
|
||||
.set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else(
|
||||
|| Ok(Post::get(conn, self.post_id)?.ap_url),
|
||||
@ -114,41 +126,28 @@ impl Comment {
|
||||
.set_published_string(chrono::Utc::now().to_rfc3339())?;
|
||||
note.object_props
|
||||
.set_attributed_to_link(author.clone().into_id())?;
|
||||
note.object_props
|
||||
.set_to_link_vec(to.clone())?;
|
||||
note.object_props
|
||||
.set_tag_link_vec(
|
||||
mentions
|
||||
.into_iter()
|
||||
.filter_map(|m| Mention::build_activity(conn, &m).ok())
|
||||
.collect::<Vec<link::Mention>>(),
|
||||
)?;
|
||||
note.object_props.set_to_link_vec(to.clone())?;
|
||||
note.object_props.set_tag_link_vec(
|
||||
mentions
|
||||
.into_iter()
|
||||
.filter_map(|m| Mention::build_activity(conn, &m).ok())
|
||||
.collect::<Vec<link::Mention>>(),
|
||||
)?;
|
||||
Ok(note)
|
||||
}
|
||||
|
||||
pub fn create_activity(&self, conn: &Connection) -> Result<Create> {
|
||||
let author =
|
||||
User::get(conn, self.author_id)?;
|
||||
let author = User::get(conn, self.author_id)?;
|
||||
|
||||
let note = self.to_activity(conn)?;
|
||||
let mut act = Create::default();
|
||||
act.create_props
|
||||
.set_actor_link(author.into_id())?;
|
||||
act.create_props
|
||||
.set_object_object(note.clone())?;
|
||||
act.create_props.set_actor_link(author.into_id())?;
|
||||
act.create_props.set_object_object(note.clone())?;
|
||||
act.object_props
|
||||
.set_id_string(format!(
|
||||
"{}/activity",
|
||||
self.ap_url
|
||||
.clone()?,
|
||||
))?;
|
||||
.set_id_string(format!("{}/activity", self.ap_url.clone()?,))?;
|
||||
act.object_props
|
||||
.set_to_link_vec(
|
||||
note.object_props
|
||||
.to_link_vec::<Id>()?,
|
||||
)?;
|
||||
act.object_props
|
||||
.set_cc_link_vec::<Id>(vec![])?;
|
||||
.set_to_link_vec(note.object_props.to_link_vec::<Id>()?)?;
|
||||
act.object_props.set_cc_link_vec::<Id>(vec![])?;
|
||||
Ok(act)
|
||||
}
|
||||
}
|
||||
@ -158,43 +157,39 @@ impl FromActivity<Note, Connection> for Comment {
|
||||
|
||||
fn from_activity(conn: &Connection, note: Note, actor: Id) -> Result<Comment> {
|
||||
let comm = {
|
||||
let previous_url = note
|
||||
.object_props
|
||||
.in_reply_to
|
||||
.as_ref()?
|
||||
.as_str()?;
|
||||
let previous_url = note.object_props.in_reply_to.as_ref()?.as_str()?;
|
||||
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) {
|
||||
serde_json::Value::Array(v) => v.iter().filter_map(serde_json::Value::as_str).any(|s| s==PUBLIC_VISIBILTY),
|
||||
let is_public = |v: &Option<serde_json::Value>| match v
|
||||
.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,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let public_visibility = is_public(¬e.object_props.to) ||
|
||||
is_public(¬e.object_props.bto) ||
|
||||
is_public(¬e.object_props.cc) ||
|
||||
is_public(¬e.object_props.bcc);
|
||||
let public_visibility = is_public(¬e.object_props.to)
|
||||
|| is_public(¬e.object_props.bto)
|
||||
|| is_public(¬e.object_props.cc)
|
||||
|| is_public(¬e.object_props.bcc);
|
||||
|
||||
let comm = Comment::insert(
|
||||
conn,
|
||||
NewComment {
|
||||
content: SafeString::new(
|
||||
¬e
|
||||
.object_props
|
||||
.content_string()?
|
||||
),
|
||||
spoiler_text: note
|
||||
.object_props
|
||||
.summary_string()
|
||||
.unwrap_or_default(),
|
||||
content: SafeString::new(¬e.object_props.content_string()?),
|
||||
spoiler_text: note.object_props.summary_string().unwrap_or_default(),
|
||||
ap_url: note.object_props.id_string().ok(),
|
||||
in_response_to_id: previous_comment.iter().map(|c| c.id).next(),
|
||||
post_id: previous_comment.map(|c| c.post_id)
|
||||
.or_else(|_| Ok(Post::find_by_ap_url(conn, previous_url)?.id) as Result<i32>)?,
|
||||
post_id: previous_comment.map(|c| c.post_id).or_else(|_| {
|
||||
Ok(Post::find_by_ap_url(conn, previous_url)?.id) as Result<i32>
|
||||
})?,
|
||||
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
|
||||
public_visibility
|
||||
public_visibility,
|
||||
},
|
||||
)?;
|
||||
|
||||
@ -204,13 +199,11 @@ impl FromActivity<Note, Connection> for Comment {
|
||||
serde_json::from_value::<link::Mention>(tag)
|
||||
.map_err(Error::from)
|
||||
.and_then(|m| {
|
||||
let author = &Post::get(conn, comm.post_id)?
|
||||
.get_authors(conn)?[0];
|
||||
let not_author = m
|
||||
.link_props
|
||||
.href_string()?
|
||||
!= author.ap_url.clone();
|
||||
Ok(Mention::from_activity(conn, &m, comm.id, false, not_author)?)
|
||||
let author = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0];
|
||||
let not_author = m.link_props.href_string()? != author.ap_url.clone();
|
||||
Ok(Mention::from_activity(
|
||||
conn, &m, comm.id, false, not_author,
|
||||
)?)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@ -218,14 +211,21 @@ impl FromActivity<Note, Connection> for Comment {
|
||||
comm
|
||||
};
|
||||
|
||||
|
||||
if !comm.public_visibility {
|
||||
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) {
|
||||
serde_json::Value::Array(v) => v,
|
||||
v => vec![v],
|
||||
}.into_iter().filter_map(filter)
|
||||
}
|
||||
.into_iter()
|
||||
.filter_map(filter)
|
||||
};
|
||||
|
||||
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 bcc = receivers_ap_url(note.object_props.bcc.take());
|
||||
|
||||
let receivers_ap_url = to.chain(cc).chain(bto).chain(bcc)
|
||||
.collect::<HashSet<_>>()//remove duplicates (don't do a query more than once)
|
||||
let receivers_ap_url = to
|
||||
.chain(cc)
|
||||
.chain(bto)
|
||||
.chain(bcc)
|
||||
.collect::<HashSet<_>>() //remove duplicates (don't do a query more than once)
|
||||
.into_iter()
|
||||
.map(|v| if let Ok(user) = User::from_url(conn,&v) {
|
||||
vec![user]
|
||||
} else {
|
||||
vec![]// TODO try to fetch collection
|
||||
.map(|v| {
|
||||
if let Ok(user) = User::from_url(conn, &v) {
|
||||
vec![user]
|
||||
} else {
|
||||
vec![] // TODO try to fetch collection
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
.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 {
|
||||
CommentSeers::insert(
|
||||
conn,
|
||||
NewCommentSeers {
|
||||
comment_id: comm.id,
|
||||
user_id: user.id
|
||||
}
|
||||
user_id: user.id,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@ -288,7 +293,8 @@ pub struct CommentTree {
|
||||
|
||||
impl CommentTree {
|
||||
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.can_see(conn, user))
|
||||
.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> {
|
||||
let responses = comment.get_responses(conn)?.into_iter()
|
||||
let responses = comment
|
||||
.get_responses(conn)?
|
||||
.into_iter()
|
||||
.filter(|c| c.can_see(conn, user))
|
||||
.filter_map(|c| Self::from_comment(conn, c, user).ok())
|
||||
.collect();
|
||||
Ok(CommentTree {
|
||||
comment,
|
||||
responses,
|
||||
})
|
||||
Ok(CommentTree { comment, responses })
|
||||
}
|
||||
}
|
||||
|
||||
@ -316,11 +321,8 @@ impl<'a> Deletable<Connection, Delete> for Comment {
|
||||
.set_actor_link(self.get_author(conn)?.into_id())?;
|
||||
|
||||
let mut tombstone = Tombstone::default();
|
||||
tombstone
|
||||
.object_props
|
||||
.set_id_string(self.ap_url.clone()?)?;
|
||||
act.delete_props
|
||||
.set_object_object(tombstone)?;
|
||||
tombstone.object_props.set_id_string(self.ap_url.clone()?)?;
|
||||
act.delete_props.set_object_object(tombstone)?;
|
||||
|
||||
act.object_props
|
||||
.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)? {
|
||||
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))
|
||||
.execute(conn)?;
|
||||
diesel::delete(self)
|
||||
.execute(conn)?;
|
||||
diesel::delete(self).execute(conn)?;
|
||||
Ok(act)
|
||||
}
|
||||
|
||||
|
@ -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")]
|
||||
use diesel::{dsl::sql_query, ConnectionError, RunQueryDsl};
|
||||
use rocket::{
|
||||
@ -47,8 +49,13 @@ pub struct PragmaForeignKey;
|
||||
impl CustomizeConnection<Connection, ConnError> for PragmaForeignKey {
|
||||
#[cfg(feature = "sqlite")] // will default to an empty function for postgres
|
||||
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_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",
|
||||
)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ use plume_common::activity_pub::{
|
||||
};
|
||||
use schema::follows;
|
||||
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)]
|
||||
#[belongs_to(User, foreign_key = "following_id")]
|
||||
@ -62,12 +62,9 @@ impl Follow {
|
||||
.set_actor_link::<Id>(user.clone().into_id())?;
|
||||
act.follow_props
|
||||
.set_object_link::<Id>(target.clone().into_id())?;
|
||||
act.object_props
|
||||
.set_id_string(self.ap_url.clone())?;
|
||||
act.object_props
|
||||
.set_to_link(target.into_id())?;
|
||||
act.object_props
|
||||
.set_cc_link_vec::<Id>(vec![])?;
|
||||
act.object_props.set_id_string(self.ap_url.clone())?;
|
||||
act.object_props.set_to_link(target.into_id())?;
|
||||
act.object_props.set_cc_link_vec::<Id>(vec![])?;
|
||||
Ok(act)
|
||||
}
|
||||
|
||||
@ -92,21 +89,13 @@ impl Follow {
|
||||
|
||||
let mut accept = Accept::default();
|
||||
let accept_id = ap_url(&format!("{}/follow/{}/accept", BASE_URL.as_str(), &res.id));
|
||||
accept
|
||||
.object_props
|
||||
.set_id_string(accept_id)?;
|
||||
accept
|
||||
.object_props
|
||||
.set_to_link(from.clone().into_id())?;
|
||||
accept
|
||||
.object_props
|
||||
.set_cc_link_vec::<Id>(vec![])?;
|
||||
accept.object_props.set_id_string(accept_id)?;
|
||||
accept.object_props.set_to_link(from.clone().into_id())?;
|
||||
accept.object_props.set_cc_link_vec::<Id>(vec![])?;
|
||||
accept
|
||||
.accept_props
|
||||
.set_actor_link::<Id>(target.clone().into_id())?;
|
||||
accept
|
||||
.accept_props
|
||||
.set_object_object(follow)?;
|
||||
accept.accept_props.set_object_object(follow)?;
|
||||
broadcast(&*target, accept, vec![from.clone()]);
|
||||
Ok(res)
|
||||
}
|
||||
@ -120,29 +109,18 @@ impl FromActivity<FollowAct, Connection> for Follow {
|
||||
.follow_props
|
||||
.actor_link::<Id>()
|
||||
.map(|l| l.into())
|
||||
.or_else(|_| Ok(follow
|
||||
.follow_props
|
||||
.actor_object::<Person>()?
|
||||
.object_props
|
||||
.id_string()?) as Result<String>)?;
|
||||
let from =
|
||||
User::from_url(conn, &from_id)?;
|
||||
match User::from_url(
|
||||
conn,
|
||||
follow
|
||||
.follow_props
|
||||
.object
|
||||
.as_str()?,
|
||||
) {
|
||||
.or_else(|_| {
|
||||
Ok(follow
|
||||
.follow_props
|
||||
.actor_object::<Person>()?
|
||||
.object_props
|
||||
.id_string()?) as Result<String>
|
||||
})?;
|
||||
let from = User::from_url(conn, &from_id)?;
|
||||
match User::from_url(conn, follow.follow_props.object.as_str()?) {
|
||||
Ok(user) => Follow::accept_follow(conn, &from, &user, follow, from.id, user.id),
|
||||
Err(_) => {
|
||||
let blog = Blog::from_url(
|
||||
conn,
|
||||
follow
|
||||
.follow_props
|
||||
.object
|
||||
.as_str()?,
|
||||
)?;
|
||||
let blog = Blog::from_url(conn, follow.follow_props.object.as_str()?)?;
|
||||
Follow::accept_follow(conn, &from, &blog, follow, from.id, blog.id)
|
||||
}
|
||||
}
|
||||
@ -160,7 +138,8 @@ impl Notify<Connection> for Follow {
|
||||
object_id: self.id,
|
||||
user_id: self.following_id,
|
||||
},
|
||||
).map(|_| ())
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,21 +147,16 @@ impl Deletable<Connection, Undo> for Follow {
|
||||
type Error = Error;
|
||||
|
||||
fn delete(&self, conn: &Connection) -> Result<Undo> {
|
||||
diesel::delete(self)
|
||||
.execute(conn)?;
|
||||
diesel::delete(self).execute(conn)?;
|
||||
|
||||
// delete associated notification if any
|
||||
if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) {
|
||||
diesel::delete(¬if)
|
||||
.execute(conn)?;
|
||||
diesel::delete(¬if).execute(conn)?;
|
||||
}
|
||||
|
||||
let mut undo = Undo::default();
|
||||
undo.undo_props
|
||||
.set_actor_link(
|
||||
User::get(conn, self.follower_id)?
|
||||
.into_id(),
|
||||
)?;
|
||||
.set_actor_link(User::get(conn, self.follower_id)?.into_id())?;
|
||||
undo.object_props
|
||||
.set_id_string(format!("{}/undo", self.ap_url))?;
|
||||
undo.undo_props
|
||||
@ -209,8 +183,8 @@ impl IntoId for Follow {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use diesel::Connection;
|
||||
use super::*;
|
||||
use diesel::Connection;
|
||||
use tests::db;
|
||||
use users::tests as user_tests;
|
||||
|
||||
@ -219,20 +193,31 @@ mod tests {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let users = user_tests::fill_database(&conn);
|
||||
let follow = Follow::insert(&conn, NewFollow {
|
||||
follower_id: users[0].id,
|
||||
following_id: users[1].id,
|
||||
ap_url: String::new(),
|
||||
}).expect("Couldn't insert new follow");
|
||||
assert_eq!(follow.ap_url, format!("https://{}/follows/{}", *BASE_URL, follow.id));
|
||||
let follow = Follow::insert(
|
||||
&conn,
|
||||
NewFollow {
|
||||
follower_id: users[0].id,
|
||||
following_id: users[1].id,
|
||||
ap_url: String::new(),
|
||||
},
|
||||
)
|
||||
.expect("Couldn't insert new follow");
|
||||
assert_eq!(
|
||||
follow.ap_url,
|
||||
format!("https://{}/follows/{}", *BASE_URL, follow.id)
|
||||
);
|
||||
|
||||
let follow = Follow::insert(&conn, NewFollow {
|
||||
follower_id: users[1].id,
|
||||
following_id: users[0].id,
|
||||
ap_url: String::from("https://some.url/"),
|
||||
}).expect("Couldn't insert new follow");
|
||||
let follow = Follow::insert(
|
||||
&conn,
|
||||
NewFollow {
|
||||
follower_id: users[1].id,
|
||||
following_id: users[0].id,
|
||||
ap_url: String::from("https://some.url/"),
|
||||
},
|
||||
)
|
||||
.expect("Couldn't insert new follow");
|
||||
assert_eq!(follow.ap_url, String::from("https://some.url/"));
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ pub struct Instance {
|
||||
pub open_registrations: bool,
|
||||
pub short_description: SafeString,
|
||||
pub long_description: SafeString,
|
||||
pub default_license : String,
|
||||
pub default_license: String,
|
||||
pub long_description_html: SafeString,
|
||||
pub short_description_html: SafeString,
|
||||
}
|
||||
@ -46,7 +46,8 @@ impl Instance {
|
||||
.limit(1)
|
||||
.load::<Instance>(conn)?
|
||||
.into_iter()
|
||||
.nth(0).ok_or(Error::NotFound)
|
||||
.nth(0)
|
||||
.ok_or(Error::NotFound)
|
||||
}
|
||||
|
||||
pub fn get_remotes(conn: &Connection) -> Result<Vec<Instance>> {
|
||||
@ -109,12 +110,7 @@ impl Instance {
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn compute_box(
|
||||
&self,
|
||||
prefix: &str,
|
||||
name: &str,
|
||||
box_name: &str,
|
||||
) -> String {
|
||||
pub fn compute_box(&self, prefix: &str, name: &str, box_name: &str) -> String {
|
||||
ap_url(&format!(
|
||||
"{instance}/{prefix}/{name}/{box_name}",
|
||||
instance = self.public_domain,
|
||||
@ -209,15 +205,16 @@ pub(crate) mod tests {
|
||||
open_registrations: true,
|
||||
public_domain: "3plu.me".to_string(),
|
||||
},
|
||||
].into_iter()
|
||||
.map(|inst| {
|
||||
(
|
||||
inst.clone(),
|
||||
Instance::find_by_domain(conn, &inst.public_domain)
|
||||
.unwrap_or_else(|_| Instance::insert(conn, inst).unwrap()),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
]
|
||||
.into_iter()
|
||||
.map(|inst| {
|
||||
(
|
||||
inst.clone(),
|
||||
Instance::find_by_domain(conn, &inst.public_domain)
|
||||
.unwrap_or_else(|_| Instance::insert(conn, inst).unwrap()),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -244,8 +241,14 @@ pub(crate) mod tests {
|
||||
public_domain
|
||||
]
|
||||
);
|
||||
assert_eq!(res.long_description_html.get(), &inserted.long_description_html);
|
||||
assert_eq!(res.short_description_html.get(), &inserted.short_description_html);
|
||||
assert_eq!(
|
||||
res.long_description_html.get(),
|
||||
&inserted.long_description_html
|
||||
);
|
||||
assert_eq!(
|
||||
res.short_description_html.get(),
|
||||
&inserted.short_description_html
|
||||
);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@ -282,8 +285,14 @@ pub(crate) mod tests {
|
||||
public_domain
|
||||
]
|
||||
);
|
||||
assert_eq!(&newinst.long_description_html, inst.long_description_html.get());
|
||||
assert_eq!(&newinst.short_description_html, inst.short_description_html.get());
|
||||
assert_eq!(
|
||||
&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();
|
||||
@ -292,7 +301,9 @@ pub(crate) mod tests {
|
||||
let page2 = &page[1];
|
||||
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 {
|
||||
let page = Instance::page(conn, (i, i + 1)).unwrap();
|
||||
assert_eq!(page.len(), 1);
|
||||
@ -326,11 +337,13 @@ pub(crate) mod tests {
|
||||
0
|
||||
);
|
||||
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
|
||||
);
|
||||
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))
|
||||
.map(|inst| inst.blocked)
|
||||
.unwrap_or(false)
|
||||
@ -340,11 +353,13 @@ pub(crate) mod tests {
|
||||
let inst = Instance::get(conn, inst.id).unwrap();
|
||||
assert_eq!(inst.blocked, blocked);
|
||||
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
|
||||
);
|
||||
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))
|
||||
.map(|inst| inst.blocked)
|
||||
.unwrap_or(false)
|
||||
@ -375,7 +390,8 @@ pub(crate) mod tests {
|
||||
false,
|
||||
SafeString::new("[short](#link)"),
|
||||
SafeString::new("[long_description](/with_link)"),
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
let inst = Instance::get(conn, inst.id).unwrap();
|
||||
assert_eq!(inst.name, "NewName".to_owned());
|
||||
assert_eq!(inst.open_registrations, false);
|
||||
|
@ -292,8 +292,8 @@ static DB_NAME: &str = "plume_tests";
|
||||
|
||||
#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
|
||||
lazy_static! {
|
||||
pub static ref DATABASE_URL: String =
|
||||
env::var("DATABASE_URL").unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME));
|
||||
pub static ref DATABASE_URL: String = env::var("DATABASE_URL")
|
||||
.unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME));
|
||||
}
|
||||
|
||||
#[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");
|
||||
embedded_migrations::run(&conn).expect("Couldn't run migrations");
|
||||
#[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
|
||||
}
|
||||
}
|
||||
@ -346,8 +348,8 @@ pub mod api_tokens;
|
||||
pub mod apps;
|
||||
pub mod blog_authors;
|
||||
pub mod blogs;
|
||||
pub mod comments;
|
||||
pub mod comment_seers;
|
||||
pub mod comments;
|
||||
pub mod db_conn;
|
||||
pub mod follows;
|
||||
pub mod headers;
|
||||
@ -360,7 +362,7 @@ pub mod post_authors;
|
||||
pub mod posts;
|
||||
pub mod reshares;
|
||||
pub mod safe_string;
|
||||
pub mod search;
|
||||
pub mod schema;
|
||||
pub mod search;
|
||||
pub mod tags;
|
||||
pub mod users;
|
||||
|
@ -38,21 +38,13 @@ impl Like {
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<activity::Like> {
|
||||
let mut act = activity::Like::default();
|
||||
act.like_props
|
||||
.set_actor_link(
|
||||
User::get(conn, self.user_id)?
|
||||
.into_id(),
|
||||
)?;
|
||||
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
|
||||
act.like_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
|
||||
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
|
||||
act.object_props
|
||||
.set_cc_link_vec::<Id>(vec![])?;
|
||||
act.object_props
|
||||
.set_id_string(self.ap_url.clone())?;
|
||||
act.object_props.set_cc_link_vec::<Id>(vec![])?;
|
||||
act.object_props.set_id_string(self.ap_url.clone())?;
|
||||
|
||||
Ok(act)
|
||||
}
|
||||
@ -62,18 +54,8 @@ impl FromActivity<activity::Like, Connection> for Like {
|
||||
type Error = Error;
|
||||
|
||||
fn from_activity(conn: &Connection, like: activity::Like, _actor: Id) -> Result<Like> {
|
||||
let liker = User::from_url(
|
||||
conn,
|
||||
like.like_props
|
||||
.actor
|
||||
.as_str()?,
|
||||
)?;
|
||||
let post = Post::find_by_ap_url(
|
||||
conn,
|
||||
like.like_props
|
||||
.object
|
||||
.as_str()?,
|
||||
)?;
|
||||
let liker = User::from_url(conn, like.like_props.actor.as_str()?)?;
|
||||
let post = Post::find_by_ap_url(conn, like.like_props.object.as_str()?)?;
|
||||
let res = Like::insert(
|
||||
conn,
|
||||
NewLike {
|
||||
@ -110,26 +92,22 @@ impl Deletable<Connection, activity::Undo> for Like {
|
||||
type Error = Error;
|
||||
|
||||
fn delete(&self, conn: &Connection) -> Result<activity::Undo> {
|
||||
diesel::delete(self)
|
||||
.execute(conn)?;
|
||||
diesel::delete(self).execute(conn)?;
|
||||
|
||||
// delete associated notification if any
|
||||
if let Ok(notif) = Notification::find(conn, notification_kind::LIKE, self.id) {
|
||||
diesel::delete(¬if)
|
||||
.execute(conn)?;
|
||||
diesel::delete(¬if).execute(conn)?;
|
||||
}
|
||||
|
||||
let mut act = activity::Undo::default();
|
||||
act.undo_props
|
||||
.set_actor_link(User::get(conn, self.user_id)?.into_id(),)?;
|
||||
act.undo_props
|
||||
.set_object_object(self.to_activity(conn)?)?;
|
||||
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
|
||||
act.undo_props.set_object_object(self.to_activity(conn)?)?;
|
||||
act.object_props
|
||||
.set_id_string(format!("{}#delete", self.ap_url))?;
|
||||
act.object_props
|
||||
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
|
||||
act.object_props
|
||||
.set_cc_link_vec::<Id>(vec![])?;
|
||||
act.object_props.set_cc_link_vec::<Id>(vec![])?;
|
||||
|
||||
Ok(act)
|
||||
}
|
||||
@ -151,7 +129,7 @@ impl NewLike {
|
||||
NewLike {
|
||||
post_id: p.id,
|
||||
user_id: u.id,
|
||||
ap_url
|
||||
ap_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,12 +62,14 @@ impl Media {
|
||||
list_by!(medias, for_user, owner_id as i32);
|
||||
|
||||
pub fn list_all_medias(conn: &Connection) -> Result<Vec<Media>> {
|
||||
medias::table
|
||||
.load::<Media>(conn)
|
||||
.map_err(Error::from)
|
||||
medias::table.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
|
||||
.filter(medias::owner_id.eq(user.id))
|
||||
.offset(i64::from(min))
|
||||
@ -124,7 +126,9 @@ impl Media {
|
||||
pub fn markdown(&self, conn: &Connection) -> Result<SafeString> {
|
||||
let url = self.url(conn)?;
|
||||
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::Unknown => SafeString::new(""),
|
||||
})
|
||||
@ -216,7 +220,8 @@ impl Media {
|
||||
.into_iter()
|
||||
.next()?
|
||||
.as_ref(),
|
||||
)?.id,
|
||||
)?
|
||||
.id,
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -249,37 +254,41 @@ pub(crate) mod tests {
|
||||
let f2 = "static/media/2.mp3".to_owned();
|
||||
fs::write(f1.clone(), []).unwrap();
|
||||
fs::write(f2.clone(), []).unwrap();
|
||||
(users, vec![
|
||||
NewMedia {
|
||||
file_path: f1,
|
||||
alt_text: "some alt".to_owned(),
|
||||
is_remote: false,
|
||||
remote_url: None,
|
||||
sensitive: false,
|
||||
content_warning: None,
|
||||
owner_id: user_one,
|
||||
},
|
||||
NewMedia {
|
||||
file_path: f2,
|
||||
alt_text: "alt message".to_owned(),
|
||||
is_remote: false,
|
||||
remote_url: None,
|
||||
sensitive: true,
|
||||
content_warning: Some("Content warning".to_owned()),
|
||||
owner_id: user_one,
|
||||
},
|
||||
NewMedia {
|
||||
file_path: "".to_owned(),
|
||||
alt_text: "another alt".to_owned(),
|
||||
is_remote: true,
|
||||
remote_url: Some("https://example.com/".to_owned()),
|
||||
sensitive: false,
|
||||
content_warning: None,
|
||||
owner_id: user_two,
|
||||
},
|
||||
].into_iter()
|
||||
(
|
||||
users,
|
||||
vec![
|
||||
NewMedia {
|
||||
file_path: f1,
|
||||
alt_text: "some alt".to_owned(),
|
||||
is_remote: false,
|
||||
remote_url: None,
|
||||
sensitive: false,
|
||||
content_warning: None,
|
||||
owner_id: user_one,
|
||||
},
|
||||
NewMedia {
|
||||
file_path: f2,
|
||||
alt_text: "alt message".to_owned(),
|
||||
is_remote: false,
|
||||
remote_url: None,
|
||||
sensitive: true,
|
||||
content_warning: Some("Content warning".to_owned()),
|
||||
owner_id: user_one,
|
||||
},
|
||||
NewMedia {
|
||||
file_path: "".to_owned(),
|
||||
alt_text: "another alt".to_owned(),
|
||||
is_remote: true,
|
||||
remote_url: Some("https://example.com/".to_owned()),
|
||||
sensitive: false,
|
||||
content_warning: None,
|
||||
owner_id: user_two,
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.map(|nm| Media::insert(conn, nm).unwrap())
|
||||
.collect())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn clean(conn: &Conn) {
|
||||
@ -311,7 +320,8 @@ pub(crate) mod tests {
|
||||
content_warning: None,
|
||||
owner_id: user,
|
||||
},
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(Path::new(&path).exists());
|
||||
media.delete(conn).unwrap();
|
||||
@ -346,29 +356,26 @@ pub(crate) mod tests {
|
||||
content_warning: None,
|
||||
owner_id: u1.id,
|
||||
},
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
Media::for_user(conn, u1.id).unwrap()
|
||||
.iter()
|
||||
.any(|m| m.id == media.id)
|
||||
);
|
||||
assert!(
|
||||
!Media::for_user(conn, u2.id).unwrap()
|
||||
.iter()
|
||||
.any(|m| m.id == media.id)
|
||||
);
|
||||
assert!(Media::for_user(conn, u1.id)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|m| m.id == media.id));
|
||||
assert!(!Media::for_user(conn, u2.id)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|m| m.id == media.id));
|
||||
media.set_owner(conn, u2).unwrap();
|
||||
assert!(
|
||||
!Media::for_user(conn, u1.id).unwrap()
|
||||
.iter()
|
||||
.any(|m| m.id == media.id)
|
||||
);
|
||||
assert!(
|
||||
Media::for_user(conn, u2.id).unwrap()
|
||||
.iter()
|
||||
.any(|m| m.id == media.id)
|
||||
);
|
||||
assert!(!Media::for_user(conn, u1.id)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|m| m.id == media.id));
|
||||
assert!(Media::for_user(conn, u2.id)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|m| m.id == media.id));
|
||||
|
||||
clean(conn);
|
||||
|
||||
|
@ -37,11 +37,15 @@ impl Mention {
|
||||
}
|
||||
|
||||
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> {
|
||||
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> {
|
||||
@ -54,21 +58,15 @@ impl Mention {
|
||||
pub fn build_activity(conn: &Connection, ment: &str) -> Result<link::Mention> {
|
||||
let user = User::find_by_fqn(conn, ment)?;
|
||||
let mut mention = link::Mention::default();
|
||||
mention
|
||||
.link_props
|
||||
.set_href_string(user.ap_url)?;
|
||||
mention
|
||||
.link_props
|
||||
.set_name_string(format!("@{}", ment))?;
|
||||
mention.link_props.set_href_string(user.ap_url)?;
|
||||
mention.link_props.set_name_string(format!("@{}", ment))?;
|
||||
Ok(mention)
|
||||
}
|
||||
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<link::Mention> {
|
||||
let user = self.get_mentioned(conn)?;
|
||||
let mut mention = link::Mention::default();
|
||||
mention
|
||||
.link_props
|
||||
.set_href_string(user.ap_url.clone())?;
|
||||
mention.link_props.set_href_string(user.ap_url.clone())?;
|
||||
mention
|
||||
.link_props
|
||||
.set_name_string(format!("@{}", user.fqn))?;
|
||||
@ -141,6 +139,7 @@ impl Notify<Connection> for Mention {
|
||||
object_id: self.id,
|
||||
user_id: m.id,
|
||||
},
|
||||
).map(|_| ())
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
@ -80,24 +80,40 @@ impl Notification {
|
||||
|
||||
pub fn get_url(&self, conn: &Connection) -> Option<String> {
|
||||
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::MENTION => Mention::get(conn, self.object_id).and_then(|mention|
|
||||
mention.get_post(conn).and_then(|p| p.url(conn))
|
||||
.or_else(|_| {
|
||||
let comment = mention.get_comment(conn)?;
|
||||
Ok(format!("{}#comment-{}", comment.get_post(conn)?.url(conn)?, comment.id))
|
||||
})
|
||||
).ok(),
|
||||
notification_kind::MENTION => Mention::get(conn, self.object_id)
|
||||
.and_then(|mention| {
|
||||
mention
|
||||
.get_post(conn)
|
||||
.and_then(|p| p.url(conn))
|
||||
.or_else(|_| {
|
||||
let comment = mention.get_comment(conn)?;
|
||||
Ok(format!(
|
||||
"{}#comment-{}",
|
||||
comment.get_post(conn)?.url(conn)?,
|
||||
comment.id
|
||||
))
|
||||
})
|
||||
})
|
||||
.ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_post(&self, conn: &Connection) -> Option<Post> {
|
||||
match self.kind.as_ref() {
|
||||
notification_kind::COMMENT => Comment::get(conn, self.object_id).and_then(|comment| comment.get_post(conn)).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(),
|
||||
notification_kind::COMMENT => Comment::get(conn, self.object_id)
|
||||
.and_then(|comment| comment.get_post(conn))
|
||||
.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,
|
||||
}
|
||||
}
|
||||
@ -105,7 +121,9 @@ impl Notification {
|
||||
pub fn get_actor(&self, conn: &Connection) -> Result<User> {
|
||||
Ok(match self.kind.as_ref() {
|
||||
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::MENTION => Mention::get(conn, self.object_id)?.get_user(conn)?,
|
||||
notification_kind::RESHARE => Reshare::get(conn, self.object_id)?.get_user(conn)?,
|
||||
|
@ -1,8 +1,8 @@
|
||||
use activitypub::{
|
||||
CustomObject,
|
||||
activity::{Create, Delete, Update},
|
||||
link,
|
||||
object::{Article, Image, Tombstone},
|
||||
CustomObject,
|
||||
};
|
||||
use canapi::{Error as ApiError, Provider};
|
||||
use chrono::{NaiveDateTime, TimeZone, Utc};
|
||||
@ -12,25 +12,26 @@ use scheduled_thread_pool::ScheduledThreadPool as Worker;
|
||||
use serde_json;
|
||||
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 instance::Instance;
|
||||
use medias::Media;
|
||||
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 safe_string::SafeString;
|
||||
use search::Searcher;
|
||||
use schema::posts;
|
||||
use search::Searcher;
|
||||
use tags::*;
|
||||
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>;
|
||||
|
||||
@ -75,7 +76,11 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
|
||||
id: i32,
|
||||
) -> ApiResult<PostEndpoint> {
|
||||
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(
|
||||
"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()),
|
||||
content: Some(post.content.get().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),
|
||||
published: Some(post.published),
|
||||
creation_date: Some(post.creation_date.format("%Y-%m-%d").to_string()),
|
||||
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,
|
||||
})
|
||||
} else {
|
||||
@ -114,24 +130,39 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
|
||||
query = query.filter(posts::content.eq(content));
|
||||
}
|
||||
|
||||
query.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 {
|
||||
id: Some(p.id),
|
||||
title: Some(p.title.clone()),
|
||||
subtitle: Some(p.subtitle.clone()),
|
||||
content: Some(p.content.get().clone()),
|
||||
source: Some(p.source.clone()),
|
||||
author: Some(p.get_authors(conn).unwrap_or_default()[0].username.clone()),
|
||||
blog_id: Some(p.blog_id),
|
||||
published: Some(p.published),
|
||||
creation_date: Some(p.creation_date.format("%Y-%m-%d").to_string()),
|
||||
license: Some(p.license.clone()),
|
||||
tags: Some(Tag::for_post(conn, p.id).unwrap_or_else(|_| vec![]).into_iter().map(|t| t.tag).collect()),
|
||||
cover_id: p.cover_id,
|
||||
query
|
||||
.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 {
|
||||
id: Some(p.id),
|
||||
title: Some(p.title.clone()),
|
||||
subtitle: Some(p.subtitle.clone()),
|
||||
content: Some(p.content.get().clone()),
|
||||
source: Some(p.source.clone()),
|
||||
author: Some(p.get_authors(conn).unwrap_or_default()[0].username.clone()),
|
||||
blog_id: Some(p.blog_id),
|
||||
published: Some(p.published),
|
||||
creation_date: Some(p.creation_date.format("%Y-%m-%d").to_string()),
|
||||
license: Some(p.license.clone()),
|
||||
tags: Some(
|
||||
Tag::for_post(conn, p.id)
|
||||
.unwrap_or_else(|_| vec![])
|
||||
.into_iter()
|
||||
.map(|t| t.tag)
|
||||
.collect(),
|
||||
),
|
||||
cover_id: p.cover_id,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect()
|
||||
).unwrap_or_else(|_| vec![])
|
||||
.unwrap_or_else(|_| vec![])
|
||||
}
|
||||
|
||||
fn update(
|
||||
@ -142,11 +173,15 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
|
||||
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");
|
||||
if let Ok(post) = Post::get(conn, id) {
|
||||
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,
|
||||
) -> ApiResult<PostEndpoint> {
|
||||
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 slug = query.title.unwrap().to_kebab_case();
|
||||
|
||||
let date = query.creation_date.clone()
|
||||
.and_then(|d| NaiveDateTime::parse_from_str(format!("{} 00:00:00", d).as_ref(), "%Y-%m-%d %H:%M:%S").ok());
|
||||
let date = query.creation_date.clone().and_then(|d| {
|
||||
NaiveDateTime::parse_from_str(format!("{} 00:00:00", d).as_ref(), "%Y-%m-%d %H:%M:%S")
|
||||
.ok()
|
||||
});
|
||||
|
||||
let domain = &Instance::get_local(&conn)
|
||||
.map_err(|_| ApiError::NotFound("posts::update: Error getting local instance".into()))?
|
||||
.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"))
|
||||
.map_err(|_| ApiError::NotFound("Author not found".into()))?;
|
||||
let author = User::get(
|
||||
conn,
|
||||
user_id.expect("<Post as Provider>::create: no user_id error"),
|
||||
)
|
||||
.map_err(|_| ApiError::NotFound("Author not found".into()))?;
|
||||
let blog = match query.blog_id {
|
||||
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() {
|
||||
// Not an actual authorization problem, but we have nothing better for now…
|
||||
// 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 {
|
||||
blog_id: blog,
|
||||
slug,
|
||||
title,
|
||||
content: SafeString::new(content.as_ref()),
|
||||
published: query.published.unwrap_or(true),
|
||||
license: query.license.unwrap_or_else(|| Instance::get_local(conn)
|
||||
.map(|i| i.default_license)
|
||||
.unwrap_or_else(|_| String::from("CC-BY-SA"))),
|
||||
creation_date: date,
|
||||
ap_url: String::new(),
|
||||
subtitle: query.subtitle.unwrap_or_default(),
|
||||
source: query.source.expect("Post API::create: no source error"),
|
||||
cover_id: query.cover_id,
|
||||
}, search).map_err(|_| ApiError::NotFound("Creation error".into()))?;
|
||||
let post = Post::insert(
|
||||
conn,
|
||||
NewPost {
|
||||
blog_id: blog,
|
||||
slug,
|
||||
title,
|
||||
content: SafeString::new(content.as_ref()),
|
||||
published: query.published.unwrap_or(true),
|
||||
license: query.license.unwrap_or_else(|| {
|
||||
Instance::get_local(conn)
|
||||
.map(|i| i.default_license)
|
||||
.unwrap_or_else(|_| String::from("CC-BY-SA"))
|
||||
}),
|
||||
creation_date: date,
|
||||
ap_url: String::new(),
|
||||
subtitle: query.subtitle.unwrap_or_default(),
|
||||
source: query.source.expect("Post API::create: no source error"),
|
||||
cover_id: query.cover_id,
|
||||
},
|
||||
search,
|
||||
)
|
||||
.map_err(|_| ApiError::NotFound("Creation error".into()))?;
|
||||
|
||||
PostAuthor::insert(conn, NewPostAuthor {
|
||||
author_id: author.id,
|
||||
post_id: post.id
|
||||
}).map_err(|_| ApiError::NotFound("Error saving authors".into()))?;
|
||||
PostAuthor::insert(
|
||||
conn,
|
||||
NewPostAuthor {
|
||||
author_id: author.id,
|
||||
post_id: post.id,
|
||||
},
|
||||
)
|
||||
.map_err(|_| ApiError::NotFound("Error saving authors".into()))?;
|
||||
|
||||
if let Some(tags) = query.tags {
|
||||
for tag in tags {
|
||||
Tag::insert(conn, NewTag {
|
||||
tag,
|
||||
is_hashtag: false,
|
||||
post_id: post.id
|
||||
}).map_err(|_| ApiError::NotFound("Error saving tags".into()))?;
|
||||
Tag::insert(
|
||||
conn,
|
||||
NewTag {
|
||||
tag,
|
||||
is_hashtag: false,
|
||||
post_id: post.id,
|
||||
},
|
||||
)
|
||||
.map_err(|_| ApiError::NotFound("Error saving tags".into()))?;
|
||||
}
|
||||
}
|
||||
for hashtag in hashtags {
|
||||
Tag::insert(conn, NewTag {
|
||||
tag: hashtag.to_camel_case(),
|
||||
is_hashtag: true,
|
||||
post_id: post.id
|
||||
}).map_err(|_| ApiError::NotFound("Error saving hashtags".into()))?;
|
||||
Tag::insert(
|
||||
conn,
|
||||
NewTag {
|
||||
tag: hashtag.to_camel_case(),
|
||||
is_hashtag: true,
|
||||
post_id: post.id,
|
||||
},
|
||||
)
|
||||
.map_err(|_| ApiError::NotFound("Error saving hashtags".into()))?;
|
||||
}
|
||||
|
||||
if post.published {
|
||||
for m in mentions.into_iter() {
|
||||
Mention::from_activity(
|
||||
&*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,
|
||||
true,
|
||||
true
|
||||
).map_err(|_| ApiError::NotFound("Error saving mentions".into()))?;
|
||||
true,
|
||||
)
|
||||
.map_err(|_| ApiError::NotFound("Error saving mentions".into()))?;
|
||||
}
|
||||
|
||||
let act = post.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()))?;
|
||||
let act = post
|
||||
.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));
|
||||
}
|
||||
|
||||
@ -243,12 +318,23 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
|
||||
subtitle: Some(post.subtitle.clone()),
|
||||
content: Some(post.content.get().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),
|
||||
published: Some(post.published),
|
||||
creation_date: Some(post.creation_date.format("%Y-%m-%d").to_string()),
|
||||
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,
|
||||
})
|
||||
}
|
||||
@ -279,15 +365,17 @@ impl Post {
|
||||
Ok(post)
|
||||
}
|
||||
pub fn update(&self, conn: &Connection, searcher: &Searcher) -> Result<Self> {
|
||||
diesel::update(self)
|
||||
.set(self)
|
||||
.execute(conn)?;
|
||||
diesel::update(self).set(self).execute(conn)?;
|
||||
let post = Self::get(conn, self.id)?;
|
||||
searcher.update_document(conn, &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;
|
||||
|
||||
let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id);
|
||||
@ -349,7 +437,11 @@ impl Post {
|
||||
.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;
|
||||
|
||||
let posts = PostAuthor::belonging_to(author).select(post_authors::post_id);
|
||||
@ -481,7 +573,8 @@ impl Post {
|
||||
Ok(PostAuthor::belonging_to(self)
|
||||
.filter(post_authors::author_id.eq(author_id))
|
||||
.count()
|
||||
.get_result::<i64>(conn)? > 0)
|
||||
.get_result::<i64>(conn)?
|
||||
> 0)
|
||||
}
|
||||
|
||||
pub fn get_blog(&self, conn: &Connection) -> Result<Blog> {
|
||||
@ -529,7 +622,7 @@ impl Post {
|
||||
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<LicensedArticle> {
|
||||
let cc = self.get_receivers_urls(conn)?;
|
||||
let to = vec![PUBLIC_VISIBILTY.to_string()];
|
||||
let to = vec![PUBLIC_VISIBILTY.to_string()];
|
||||
|
||||
let mut mentions_json = Mention::list_for_post(conn, self.id)?
|
||||
.into_iter()
|
||||
@ -542,12 +635,8 @@ impl Post {
|
||||
mentions_json.append(&mut tags_json);
|
||||
|
||||
let mut article = Article::default();
|
||||
article
|
||||
.object_props
|
||||
.set_name_string(self.title.clone())?;
|
||||
article
|
||||
.object_props
|
||||
.set_id_string(self.ap_url.clone())?;
|
||||
article.object_props.set_name_string(self.title.clone())?;
|
||||
article.object_props.set_id_string(self.ap_url.clone())?;
|
||||
|
||||
let mut authors = self
|
||||
.get_authors(conn)?
|
||||
@ -561,12 +650,10 @@ impl Post {
|
||||
article
|
||||
.object_props
|
||||
.set_content_string(self.content.get().clone())?;
|
||||
article
|
||||
.ap_object_props
|
||||
.set_source_object(Source {
|
||||
content: self.source.clone(),
|
||||
media_type: String::from("text/markdown"),
|
||||
})?;
|
||||
article.ap_object_props.set_source_object(Source {
|
||||
content: self.source.clone(),
|
||||
media_type: String::from("text/markdown"),
|
||||
})?;
|
||||
article
|
||||
.object_props
|
||||
.set_published_utctime(Utc.from_utc_datetime(&self.creation_date))?;
|
||||
@ -578,31 +665,20 @@ impl Post {
|
||||
if let Some(media_id) = self.cover_id {
|
||||
let media = Media::get(conn, media_id)?;
|
||||
let mut cover = Image::default();
|
||||
cover
|
||||
.object_props
|
||||
.set_url_string(media.url(conn)?)?;
|
||||
cover.object_props.set_url_string(media.url(conn)?)?;
|
||||
if media.sensitive {
|
||||
cover
|
||||
.object_props
|
||||
.set_summary_string(media.content_warning.unwrap_or_default())?;
|
||||
}
|
||||
cover.object_props.set_content_string(media.alt_text)?;
|
||||
cover
|
||||
.object_props
|
||||
.set_content_string(media.alt_text)?;
|
||||
cover
|
||||
.object_props
|
||||
.set_attributed_to_link_vec(vec![
|
||||
User::get(conn, media.owner_id)?
|
||||
.into_id(),
|
||||
])?;
|
||||
article
|
||||
.object_props
|
||||
.set_icon_object(cover)?;
|
||||
.set_attributed_to_link_vec(vec![User::get(conn, media.owner_id)?.into_id()])?;
|
||||
article.object_props.set_icon_object(cover)?;
|
||||
}
|
||||
|
||||
article
|
||||
.object_props
|
||||
.set_url_string(self.ap_url.clone())?;
|
||||
article.object_props.set_url_string(self.ap_url.clone())?;
|
||||
article
|
||||
.object_props
|
||||
.set_to_link_vec::<Id>(to.into_iter().map(Id::new).collect())?;
|
||||
@ -620,52 +696,39 @@ impl Post {
|
||||
act.object_props
|
||||
.set_id_string(format!("{}activity", self.ap_url))?;
|
||||
act.object_props
|
||||
.set_to_link_vec::<Id>(
|
||||
article.object
|
||||
.object_props
|
||||
.to_link_vec()?,
|
||||
)?;
|
||||
.set_to_link_vec::<Id>(article.object.object_props.to_link_vec()?)?;
|
||||
act.object_props
|
||||
.set_cc_link_vec::<Id>(
|
||||
article.object
|
||||
.object_props
|
||||
.cc_link_vec()?,
|
||||
)?;
|
||||
.set_cc_link_vec::<Id>(article.object.object_props.cc_link_vec()?)?;
|
||||
act.create_props
|
||||
.set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?;
|
||||
act.create_props
|
||||
.set_object_object(article)?;
|
||||
act.create_props.set_object_object(article)?;
|
||||
Ok(act)
|
||||
}
|
||||
|
||||
pub fn update_activity(&self, conn: &Connection) -> Result<Update> {
|
||||
let article = self.to_activity(conn)?;
|
||||
let mut act = Update::default();
|
||||
act.object_props.set_id_string(format!(
|
||||
"{}/update-{}",
|
||||
self.ap_url,
|
||||
Utc::now().timestamp()
|
||||
))?;
|
||||
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
|
||||
.set_to_link_vec::<Id>(
|
||||
article.object
|
||||
.object_props
|
||||
.to_link_vec()?,
|
||||
)?;
|
||||
act.object_props
|
||||
.set_cc_link_vec::<Id>(
|
||||
article.object
|
||||
.object_props
|
||||
.cc_link_vec()?,
|
||||
)?;
|
||||
.set_cc_link_vec::<Id>(article.object.object_props.cc_link_vec()?)?;
|
||||
act.update_props
|
||||
.set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?;
|
||||
act.update_props
|
||||
.set_object_object(article)?;
|
||||
act.update_props.set_object_object(article)?;
|
||||
Ok(act)
|
||||
}
|
||||
|
||||
pub fn handle_update(conn: &Connection, updated: &LicensedArticle, searcher: &Searcher) -> Result<()> {
|
||||
let id = updated.object
|
||||
.object_props
|
||||
.id_string()?;
|
||||
pub fn handle_update(
|
||||
conn: &Connection,
|
||||
updated: &LicensedArticle,
|
||||
searcher: &Searcher,
|
||||
) -> Result<()> {
|
||||
let id = updated.object.object_props.id_string()?;
|
||||
let mut post = Post::find_by_ap_url(conn, &id)?;
|
||||
|
||||
if let Ok(title) = updated.object.object_props.name_string() {
|
||||
@ -698,7 +761,9 @@ impl Post {
|
||||
.into_iter()
|
||||
.map(|s| s.to_camel_case())
|
||||
.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 tags = vec![];
|
||||
let mut hashtags = vec![];
|
||||
@ -710,8 +775,7 @@ impl Post {
|
||||
serde_json::from_value::<Hashtag>(tag.clone())
|
||||
.map_err(Error::from)
|
||||
.and_then(|t| {
|
||||
let tag_name = t
|
||||
.name_string()?;
|
||||
let tag_name = t.name_string()?;
|
||||
if txt_hashtags.remove(&tag_name) {
|
||||
hashtags.push(t);
|
||||
} else {
|
||||
@ -854,20 +918,25 @@ impl Post {
|
||||
}
|
||||
|
||||
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 {
|
||||
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 article = article.object;
|
||||
if let Ok(post) = Post::find_by_ap_url(
|
||||
conn,
|
||||
&article.object_props.id_string().unwrap_or_default(),
|
||||
) {
|
||||
if let Ok(post) =
|
||||
Post::find_by_ap_url(conn, &article.object_props.id_string().unwrap_or_default())
|
||||
{
|
||||
Ok(post)
|
||||
} else {
|
||||
let (blog, authors) = article
|
||||
@ -880,10 +949,8 @@ impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for Post
|
||||
Ok(u) => {
|
||||
authors.push(u);
|
||||
(blog, authors)
|
||||
},
|
||||
Err(_) => {
|
||||
(blog.or_else(|| Blog::from_url(conn, &url).ok()), authors)
|
||||
},
|
||||
}
|
||||
Err(_) => (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()
|
||||
.and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id));
|
||||
|
||||
let title = article
|
||||
.object_props
|
||||
.name_string()?;
|
||||
let title = article.object_props.name_string()?;
|
||||
let post = Post::insert(
|
||||
conn,
|
||||
NewPost {
|
||||
blog_id: blog?.id,
|
||||
slug: title.to_kebab_case(),
|
||||
title,
|
||||
content: SafeString::new(
|
||||
&article
|
||||
.object_props
|
||||
.content_string()?,
|
||||
),
|
||||
content: SafeString::new(&article.object_props.content_string()?),
|
||||
published: true,
|
||||
license,
|
||||
// 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(|_|
|
||||
article
|
||||
.object_props
|
||||
.id_string()
|
||||
)?,
|
||||
creation_date: Some(
|
||||
article
|
||||
.object_props
|
||||
.published_utctime()?
|
||||
.naive_utc(),
|
||||
),
|
||||
subtitle: article
|
||||
ap_url: article
|
||||
.object_props
|
||||
.summary_string()?,
|
||||
source: article
|
||||
.ap_object_props
|
||||
.source_object::<Source>()?
|
||||
.content,
|
||||
.url_string()
|
||||
.or_else(|_| article.object_props.id_string())?,
|
||||
creation_date: Some(article.object_props.published_utctime()?.naive_utc()),
|
||||
subtitle: article.object_props.summary_string()?,
|
||||
source: article.ap_object_props.source_object::<Source>()?.content,
|
||||
cover_id: cover,
|
||||
},
|
||||
searcher,
|
||||
@ -959,7 +1009,12 @@ impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for Post
|
||||
.map_err(Error::from)
|
||||
.and_then(|t| {
|
||||
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();
|
||||
}
|
||||
@ -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())?;
|
||||
|
||||
let mut tombstone = Tombstone::default();
|
||||
tombstone
|
||||
.object_props
|
||||
.set_id_string(self.ap_url.clone())?;
|
||||
act.delete_props
|
||||
.set_object_object(tombstone)?;
|
||||
tombstone.object_props.set_id_string(self.ap_url.clone())?;
|
||||
act.delete_props.set_object_object(tombstone)?;
|
||||
|
||||
act.object_props
|
||||
.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)? {
|
||||
m.delete(conn)?;
|
||||
}
|
||||
diesel::delete(self)
|
||||
.execute(*conn)?;
|
||||
diesel::delete(self).execute(*conn)?;
|
||||
searcher.delete_document(self);
|
||||
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 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 {
|
||||
post.delete(&(conn, searcher))
|
||||
} else {
|
||||
|
@ -40,7 +40,11 @@ impl Reshare {
|
||||
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
|
||||
.filter(reshares::user_id.eq(user.id))
|
||||
.order(reshares::creation_date.desc())
|
||||
@ -63,12 +67,10 @@ impl Reshare {
|
||||
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
|
||||
act.announce_props
|
||||
.set_object_link(Post::get(conn, self.post_id)?.into_id())?;
|
||||
act.object_props
|
||||
.set_id_string(self.ap_url.clone())?;
|
||||
act.object_props.set_id_string(self.ap_url.clone())?;
|
||||
act.object_props
|
||||
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
|
||||
act.object_props
|
||||
.set_cc_link_vec::<Id>(vec![])?;
|
||||
act.object_props.set_cc_link_vec::<Id>(vec![])?;
|
||||
|
||||
Ok(act)
|
||||
}
|
||||
@ -78,29 +80,15 @@ impl FromActivity<Announce, Connection> for Reshare {
|
||||
type Error = Error;
|
||||
|
||||
fn from_activity(conn: &Connection, announce: Announce, _actor: Id) -> Result<Reshare> {
|
||||
let user = User::from_url(
|
||||
conn,
|
||||
announce
|
||||
.announce_props
|
||||
.actor_link::<Id>()?
|
||||
.as_ref(),
|
||||
)?;
|
||||
let post = Post::find_by_ap_url(
|
||||
conn,
|
||||
announce
|
||||
.announce_props
|
||||
.object_link::<Id>()?
|
||||
.as_ref(),
|
||||
)?;
|
||||
let user = User::from_url(conn, announce.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(
|
||||
conn,
|
||||
NewReshare {
|
||||
post_id: post.id,
|
||||
user_id: user.id,
|
||||
ap_url: announce
|
||||
.object_props
|
||||
.id_string()
|
||||
.unwrap_or_default(),
|
||||
ap_url: announce.object_props.id_string().unwrap_or_default(),
|
||||
},
|
||||
)?;
|
||||
reshare.notify(conn)?;
|
||||
@ -131,26 +119,22 @@ impl Deletable<Connection, Undo> for Reshare {
|
||||
type Error = Error;
|
||||
|
||||
fn delete(&self, conn: &Connection) -> Result<Undo> {
|
||||
diesel::delete(self)
|
||||
.execute(conn)?;
|
||||
diesel::delete(self).execute(conn)?;
|
||||
|
||||
// delete associated notification if any
|
||||
if let Ok(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) {
|
||||
diesel::delete(¬if)
|
||||
.execute(conn)?;
|
||||
diesel::delete(¬if).execute(conn)?;
|
||||
}
|
||||
|
||||
let mut act = Undo::default();
|
||||
act.undo_props
|
||||
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
|
||||
act.undo_props
|
||||
.set_object_object(self.to_activity(conn)?)?;
|
||||
act.undo_props.set_object_object(self.to_activity(conn)?)?;
|
||||
act.object_props
|
||||
.set_id_string(format!("{}#delete", self.ap_url))?;
|
||||
act.object_props
|
||||
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
|
||||
act.object_props
|
||||
.set_cc_link_vec::<Id>(vec![])?;
|
||||
act.object_props.set_cc_link_vec::<Id>(vec![])?;
|
||||
|
||||
Ok(act)
|
||||
}
|
||||
@ -172,7 +156,7 @@ impl NewReshare {
|
||||
NewReshare {
|
||||
post_id: p.id,
|
||||
user_id: u.id,
|
||||
ap_url
|
||||
ap_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,21 +19,15 @@ lazy_static! {
|
||||
static ref CLEAN: Builder<'static> = {
|
||||
let mut b = Builder::new();
|
||||
b.add_generic_attributes(iter::once("id"))
|
||||
.add_tags(&[ "iframe", "video", "audio" ])
|
||||
.add_tags(&["iframe", "video", "audio"])
|
||||
.id_prefix(Some("postcontent-"))
|
||||
.url_relative(UrlRelative::Custom(Box::new(url_add_prefix)))
|
||||
.add_tag_attributes(
|
||||
"iframe",
|
||||
[ "width", "height", "src", "frameborder" ].iter().cloned(),
|
||||
["width", "height", "src", "frameborder"].iter().cloned(),
|
||||
)
|
||||
.add_tag_attributes(
|
||||
"video",
|
||||
[ "src", "title", "controls" ].iter(),
|
||||
)
|
||||
.add_tag_attributes(
|
||||
"audio",
|
||||
[ "src", "title", "controls" ].iter(),
|
||||
);
|
||||
.add_tag_attributes("video", ["src", "title", "controls"].iter())
|
||||
.add_tag_attributes("audio", ["src", "title", "controls"].iter());
|
||||
b
|
||||
};
|
||||
}
|
||||
@ -69,7 +63,7 @@ impl SafeString {
|
||||
/// Prefer `SafeString::new` as much as possible.
|
||||
pub fn trusted(value: impl AsRef<str>) -> Self {
|
||||
SafeString {
|
||||
value: value.as_ref().to_string()
|
||||
value: value.as_ref().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,33 +1,32 @@
|
||||
mod searcher;
|
||||
mod query;
|
||||
mod searcher;
|
||||
mod tokenizer;
|
||||
pub use self::searcher::*;
|
||||
pub use self::query::PlumeQuery as Query;
|
||||
|
||||
pub use self::searcher::*;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::{Query, Searcher};
|
||||
use diesel::Connection;
|
||||
use std::env::temp_dir;
|
||||
use std::str::FromStr;
|
||||
use diesel::Connection;
|
||||
|
||||
use blogs::tests::fill_database;
|
||||
use plume_common::activity_pub::inbox::Deletable;
|
||||
use plume_common::utils::random_hex;
|
||||
use blogs::tests::fill_database;
|
||||
use posts::{NewPost, Post};
|
||||
use post_authors::*;
|
||||
use posts::{NewPost, Post};
|
||||
use safe_string::SafeString;
|
||||
use tests::db;
|
||||
|
||||
|
||||
pub(crate) fn get_searcher() -> Searcher {
|
||||
let dir = temp_dir().join("plume-test");
|
||||
if dir.exists() {
|
||||
Searcher::open(&dir)
|
||||
} else {
|
||||
Searcher::create(&dir)
|
||||
}.unwrap()
|
||||
}
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -98,7 +97,9 @@ pub(crate) mod tests {
|
||||
|
||||
#[test]
|
||||
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");
|
||||
Searcher::open(&dir).unwrap();
|
||||
@ -109,8 +110,10 @@ pub(crate) mod tests {
|
||||
let dir = temp_dir().join(format!("plume-test-{}", random_hex()));
|
||||
|
||||
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]
|
||||
@ -123,37 +126,56 @@ pub(crate) mod tests {
|
||||
|
||||
let title = random_hex()[..8].to_owned();
|
||||
|
||||
let mut post = Post::insert(conn, NewPost {
|
||||
blog_id: blog.id,
|
||||
slug: title.clone(),
|
||||
title: title.clone(),
|
||||
content: SafeString::new(""),
|
||||
published: true,
|
||||
license: "CC-BY-SA".to_owned(),
|
||||
ap_url: "".to_owned(),
|
||||
creation_date: None,
|
||||
subtitle: "".to_owned(),
|
||||
source: "".to_owned(),
|
||||
cover_id: None,
|
||||
}, &searcher).unwrap();
|
||||
PostAuthor::insert(conn, NewPostAuthor {
|
||||
post_id: post.id,
|
||||
author_id: author.id,
|
||||
}).unwrap();
|
||||
let mut post = Post::insert(
|
||||
conn,
|
||||
NewPost {
|
||||
blog_id: blog.id,
|
||||
slug: title.clone(),
|
||||
title: title.clone(),
|
||||
content: SafeString::new(""),
|
||||
published: true,
|
||||
license: "CC-BY-SA".to_owned(),
|
||||
ap_url: "".to_owned(),
|
||||
creation_date: None,
|
||||
subtitle: "".to_owned(),
|
||||
source: "".to_owned(),
|
||||
cover_id: None,
|
||||
},
|
||||
&searcher,
|
||||
)
|
||||
.unwrap();
|
||||
PostAuthor::insert(
|
||||
conn,
|
||||
NewPostAuthor {
|
||||
post_id: post.id,
|
||||
author_id: author.id,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
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();
|
||||
post.title = newtitle.clone();
|
||||
post.update(conn, &searcher).unwrap();
|
||||
searcher.commit();
|
||||
assert_eq!(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());
|
||||
assert_eq!(
|
||||
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();
|
||||
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(())
|
||||
});
|
||||
|
@ -1,8 +1,7 @@
|
||||
use chrono::{Datelike, naive::NaiveDate, offset::Utc};
|
||||
use tantivy::{query::*, schema::*, Term};
|
||||
use std::{cmp,ops::Bound};
|
||||
use chrono::{naive::NaiveDate, offset::Utc, Datelike};
|
||||
use search::searcher::Searcher;
|
||||
|
||||
use std::{cmp, ops::Bound};
|
||||
use tantivy::{query::*, schema::*, Term};
|
||||
|
||||
//Generate functions for advanced search
|
||||
macro_rules! gen_func {
|
||||
@ -142,13 +141,11 @@ pub struct PlumeQuery {
|
||||
}
|
||||
|
||||
impl PlumeQuery {
|
||||
|
||||
/// Create a new empty Query
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
|
||||
/// Parse a query string into this Query
|
||||
pub fn parse_query(&mut self, query: &str) -> &mut Self {
|
||||
self.from_str_req(&query.trim())
|
||||
@ -160,9 +157,11 @@ impl PlumeQuery {
|
||||
gen_to_query!(self, result; normal: title, subtitle, content, tag;
|
||||
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 {
|
||||
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![
|
||||
(Occur::Should, Self::token_to_query(&token, "title")),
|
||||
(Occur::Should, Self::token_to_query(&token, "subtitle")),
|
||||
@ -170,20 +169,26 @@ impl PlumeQuery {
|
||||
];
|
||||
|
||||
result.push((Occur::Must, Box::new(BooleanQuery::from(subresult))));
|
||||
},
|
||||
}
|
||||
occur => {
|
||||
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, "content")));
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.before.is_some() || self.after.is_some() { // if at least one range bound is provided
|
||||
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()));
|
||||
if self.before.is_some() || self.after.is_some() {
|
||||
// if at least one range bound is provided
|
||||
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 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)));
|
||||
}
|
||||
|
||||
@ -195,14 +200,18 @@ impl PlumeQuery {
|
||||
|
||||
// documents newer than the provided date will be ignored
|
||||
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
|
||||
}
|
||||
|
||||
// documents older than the provided date will be ignored
|
||||
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
|
||||
}
|
||||
@ -212,18 +221,22 @@ impl PlumeQuery {
|
||||
query = query.trim();
|
||||
if query.is_empty() {
|
||||
("", "")
|
||||
} else if query.get(0..1).map(|v| v=="\"").unwrap_or(false) {
|
||||
if let Some(index) = query[1..].find('"') {
|
||||
query.split_at(index+2)
|
||||
} else {
|
||||
(query, "")
|
||||
}
|
||||
} else if query.get(0..2).map(|v| v=="+\"" || v=="-\"").unwrap_or(false) {
|
||||
if let Some(index) = query[2..].find('"') {
|
||||
query.split_at(index+3)
|
||||
} else {
|
||||
(query, "")
|
||||
}
|
||||
} else if query.get(0..1).map(|v| v == "\"").unwrap_or(false) {
|
||||
if let Some(index) = query[1..].find('"') {
|
||||
query.split_at(index + 2)
|
||||
} else {
|
||||
(query, "")
|
||||
}
|
||||
} else if query
|
||||
.get(0..2)
|
||||
.map(|v| v == "+\"" || v == "-\"")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
if let Some(index) = query[2..].find('"') {
|
||||
query.split_at(index + 3)
|
||||
} else {
|
||||
(query, "")
|
||||
}
|
||||
} else if let Some(index) = query.find(' ') {
|
||||
query.split_at(index)
|
||||
} else {
|
||||
@ -247,13 +260,13 @@ impl PlumeQuery {
|
||||
fn from_str_req(&mut self, mut query: &str) -> &mut Self {
|
||||
query = query.trim_left();
|
||||
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..];
|
||||
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..];
|
||||
Occur::MustNot
|
||||
} else {
|
||||
@ -270,31 +283,59 @@ impl PlumeQuery {
|
||||
let token = token.to_lowercase();
|
||||
let token = token.as_str();
|
||||
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 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![
|
||||
(Occur::Must, Box::new(TermQuery::new(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))),
|
||||
(
|
||||
Occur::Must,
|
||||
Box::new(TermQuery::new(
|
||||
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 {
|
||||
"instance" | "author" | "tag" => // phrase query are not available on these fields, treat it as multiple Term queries
|
||||
Box::new(BooleanQuery::from(token.split_whitespace()
|
||||
.map(|token| {
|
||||
let term = Term::from_field_text(field, token);
|
||||
(Occur::Should, Box::new(TermQuery::new(term, IndexRecordOption::Basic))
|
||||
as Box<dyn Query + 'static>)
|
||||
})
|
||||
.collect::<Vec<_>>())),
|
||||
_ => Box::new(PhraseQuery::new(token.split_whitespace()
|
||||
.map(|token| Term::from_field_text(field, token))
|
||||
.collect()))
|
||||
"instance" | "author" | "tag" =>
|
||||
// phrase query are not available on these fields, treat it as multiple Term queries
|
||||
{
|
||||
Box::new(BooleanQuery::from(
|
||||
token
|
||||
.split_whitespace()
|
||||
.map(|token| {
|
||||
let term = Term::from_field_text(field, token);
|
||||
(
|
||||
Occur::Should,
|
||||
Box::new(TermQuery::new(term, IndexRecordOption::Basic))
|
||||
as Box<dyn Query + 'static>,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
}
|
||||
_ => Box::new(PhraseQuery::new(
|
||||
token
|
||||
.split_whitespace()
|
||||
.map(|token| Term::from_field_text(field, token))
|
||||
.collect(),
|
||||
)),
|
||||
}
|
||||
} else { // Term Query
|
||||
} else {
|
||||
// Term Query
|
||||
let term = Term::from_field_text(field, token);
|
||||
let index_option = match field_name {
|
||||
"instance" | "author" | "tag" => IndexRecordOption::Basic,
|
||||
@ -306,7 +347,6 @@ impl PlumeQuery {
|
||||
}
|
||||
|
||||
impl std::str::FromStr for PlumeQuery {
|
||||
|
||||
type Err = !;
|
||||
|
||||
/// Create a new Query from &str
|
||||
@ -340,7 +380,7 @@ impl ToString for PlumeQuery {
|
||||
instance, author, blog, lang, license;
|
||||
date: before, after);
|
||||
|
||||
result.pop();// remove trailing ' '
|
||||
result.pop(); // remove trailing ' '
|
||||
result
|
||||
}
|
||||
}
|
||||
|
@ -5,15 +5,14 @@ use Connection;
|
||||
|
||||
use chrono::Datelike;
|
||||
use itertools::Itertools;
|
||||
use std::{cmp, fs::create_dir_all, path::Path, sync::Mutex};
|
||||
use tantivy::{
|
||||
collector::TopDocs, directory::MmapDirectory,
|
||||
schema::*, tokenizer::*, Index, IndexWriter, Term
|
||||
collector::TopDocs, directory::MmapDirectory, schema::*, tokenizer::*, Index, IndexWriter, Term,
|
||||
};
|
||||
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 search::query::PlumeQuery;
|
||||
use Result;
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -31,20 +30,23 @@ pub struct Searcher {
|
||||
|
||||
impl Searcher {
|
||||
pub fn schema() -> Schema {
|
||||
let tag_indexing = TextOptions::default()
|
||||
.set_indexing_options(TextFieldIndexing::default()
|
||||
.set_tokenizer("whitespace_tokenizer")
|
||||
.set_index_option(IndexRecordOption::Basic));
|
||||
let tag_indexing = TextOptions::default().set_indexing_options(
|
||||
TextFieldIndexing::default()
|
||||
.set_tokenizer("whitespace_tokenizer")
|
||||
.set_index_option(IndexRecordOption::Basic),
|
||||
);
|
||||
|
||||
let content_indexing = TextOptions::default()
|
||||
.set_indexing_options(TextFieldIndexing::default()
|
||||
.set_tokenizer("content_tokenizer")
|
||||
.set_index_option(IndexRecordOption::WithFreqsAndPositions));
|
||||
let content_indexing = TextOptions::default().set_indexing_options(
|
||||
TextFieldIndexing::default()
|
||||
.set_tokenizer("content_tokenizer")
|
||||
.set_index_option(IndexRecordOption::WithFreqsAndPositions),
|
||||
);
|
||||
|
||||
let property_indexing = TextOptions::default()
|
||||
.set_indexing_options(TextFieldIndexing::default()
|
||||
.set_tokenizer("property_tokenizer")
|
||||
.set_index_option(IndexRecordOption::WithFreqsAndPositions));
|
||||
let property_indexing = TextOptions::default().set_indexing_options(
|
||||
TextFieldIndexing::default()
|
||||
.set_tokenizer("property_tokenizer")
|
||||
.set_index_option(IndexRecordOption::WithFreqsAndPositions),
|
||||
);
|
||||
|
||||
let mut schema_builder = SchemaBuilder::default();
|
||||
|
||||
@ -66,56 +68,65 @@ impl Searcher {
|
||||
schema_builder.build()
|
||||
}
|
||||
|
||||
|
||||
pub fn create(path: &AsRef<Path>) -> Result<Self> {
|
||||
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer
|
||||
.filter(LowerCaser);
|
||||
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer.filter(LowerCaser);
|
||||
|
||||
let content_tokenizer = SimpleTokenizer
|
||||
.filter(RemoveLongFilter::limit(40))
|
||||
.filter(LowerCaser);
|
||||
|
||||
let property_tokenizer = NgramTokenizer::new(2, 8, false)
|
||||
.filter(LowerCaser);
|
||||
let property_tokenizer = NgramTokenizer::new(2, 8, false).filter(LowerCaser);
|
||||
|
||||
let schema = Self::schema();
|
||||
|
||||
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();
|
||||
tokenizer_manager.register("whitespace_tokenizer", whitespace_tokenizer);
|
||||
tokenizer_manager.register("content_tokenizer", content_tokenizer);
|
||||
tokenizer_manager.register("property_tokenizer", property_tokenizer);
|
||||
}//to please the borrow checker
|
||||
} //to please the borrow checker
|
||||
Ok(Self {
|
||||
writer: Mutex::new(Some(index.writer(50_000_000).map_err(|_| SearcherError::WriteLockAcquisitionError)?)),
|
||||
index
|
||||
writer: Mutex::new(Some(
|
||||
index
|
||||
.writer(50_000_000)
|
||||
.map_err(|_| SearcherError::WriteLockAcquisitionError)?,
|
||||
)),
|
||||
index,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open(path: &AsRef<Path>) -> Result<Self> {
|
||||
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer
|
||||
.filter(LowerCaser);
|
||||
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer.filter(LowerCaser);
|
||||
|
||||
let content_tokenizer = SimpleTokenizer
|
||||
.filter(RemoveLongFilter::limit(40))
|
||||
.filter(LowerCaser);
|
||||
|
||||
let property_tokenizer = NgramTokenizer::new(2, 8, false)
|
||||
.filter(LowerCaser);
|
||||
let property_tokenizer = NgramTokenizer::new(2, 8, false).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();
|
||||
tokenizer_manager.register("whitespace_tokenizer", whitespace_tokenizer);
|
||||
tokenizer_manager.register("content_tokenizer", content_tokenizer);
|
||||
tokenizer_manager.register("property_tokenizer", property_tokenizer);
|
||||
}//to please the borrow checker
|
||||
let mut writer = index.writer(50_000_000).map_err(|_| SearcherError::WriteLockAcquisitionError)?;
|
||||
writer.garbage_collect_files().map_err(|_| SearcherError::IndexEditionError)?;
|
||||
} //to please the borrow checker
|
||||
let mut writer = index
|
||||
.writer(50_000_000)
|
||||
.map_err(|_| SearcherError::WriteLockAcquisitionError)?;
|
||||
writer
|
||||
.garbage_collect_files()
|
||||
.map_err(|_| SearcherError::IndexEditionError)?;
|
||||
Ok(Self {
|
||||
writer: Mutex::new(Some(writer)),
|
||||
index,
|
||||
@ -173,18 +184,24 @@ impl Searcher {
|
||||
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 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 res = searcher.search(&query.into_query(), &collector).unwrap();
|
||||
|
||||
res.get(min as usize..).unwrap_or(&[])
|
||||
res.get(min as usize..)
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.filter_map(|(_,doc_add)| {
|
||||
.filter_map(|(_, doc_add)| {
|
||||
let doc = searcher.doc(*doc_add).ok()?;
|
||||
let id = doc.get_first(post_id)?;
|
||||
Post::get(conn, id.i64_value() as i32).ok()
|
||||
|
@ -38,7 +38,12 @@ impl Tag {
|
||||
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(
|
||||
conn,
|
||||
NewTag {
|
||||
|
@ -1,6 +1,5 @@
|
||||
use activitypub::{
|
||||
actor::Person, collection::OrderedCollection, object::Image, Activity, CustomObject,
|
||||
Endpoint,
|
||||
actor::Person, collection::OrderedCollection, object::Image, Activity, CustomObject, Endpoint,
|
||||
};
|
||||
use bcrypt;
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
@ -27,7 +26,10 @@ use rocket::{
|
||||
request::{self, FromRequest, Request},
|
||||
};
|
||||
use serde_json;
|
||||
use std::{cmp::PartialEq, hash::{Hash, Hasher}};
|
||||
use std::{
|
||||
cmp::PartialEq,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
use url::Url;
|
||||
use webfinger::*;
|
||||
|
||||
@ -41,7 +43,7 @@ use posts::Post;
|
||||
use safe_string::SafeString;
|
||||
use schema::users;
|
||||
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>;
|
||||
|
||||
@ -97,42 +99,24 @@ impl User {
|
||||
insert!(users, NewUser, |inserted, conn| {
|
||||
let instance = inserted.get_instance(conn)?;
|
||||
if inserted.outbox_url.is_empty() {
|
||||
inserted.outbox_url = instance.compute_box(
|
||||
USER_PREFIX,
|
||||
&inserted.username,
|
||||
"outbox",
|
||||
);
|
||||
inserted.outbox_url = instance.compute_box(USER_PREFIX, &inserted.username, "outbox");
|
||||
}
|
||||
|
||||
if inserted.inbox_url.is_empty() {
|
||||
inserted.inbox_url = instance.compute_box(
|
||||
USER_PREFIX,
|
||||
&inserted.username,
|
||||
"inbox",
|
||||
);
|
||||
inserted.inbox_url = instance.compute_box(USER_PREFIX, &inserted.username, "inbox");
|
||||
}
|
||||
|
||||
if inserted.ap_url.is_empty() {
|
||||
inserted.ap_url = instance.compute_box(
|
||||
USER_PREFIX,
|
||||
&inserted.username,
|
||||
"",
|
||||
);
|
||||
inserted.ap_url = instance.compute_box(USER_PREFIX, &inserted.username, "");
|
||||
}
|
||||
|
||||
if inserted.shared_inbox_url.is_none() {
|
||||
inserted.shared_inbox_url = Some(ap_url(&format!(
|
||||
"{}/inbox",
|
||||
instance.public_domain
|
||||
)));
|
||||
inserted.shared_inbox_url = Some(ap_url(&format!("{}/inbox", instance.public_domain)));
|
||||
}
|
||||
|
||||
if inserted.followers_endpoint.is_empty() {
|
||||
inserted.followers_endpoint = instance.compute_box(
|
||||
USER_PREFIX,
|
||||
&inserted.username,
|
||||
"followers",
|
||||
);
|
||||
inserted.followers_endpoint =
|
||||
instance.compute_box(USER_PREFIX, &inserted.username, "followers");
|
||||
}
|
||||
|
||||
if inserted.fqn.is_empty() {
|
||||
@ -162,7 +146,8 @@ impl User {
|
||||
|
||||
for blog in Blog::find_for_author(conn, self)?
|
||||
.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)?;
|
||||
}
|
||||
// delete the posts if they is the only author
|
||||
@ -180,10 +165,10 @@ impl User {
|
||||
.count()
|
||||
.load(conn)?
|
||||
.first()
|
||||
.unwrap_or(&0) > &0;
|
||||
.unwrap_or(&0)
|
||||
> &0;
|
||||
if !has_other_authors {
|
||||
Post::get(conn, post_id)?
|
||||
.delete(&(conn, searcher))?;
|
||||
Post::get(conn, post_id)?.delete(&(conn, searcher))?;
|
||||
}
|
||||
}
|
||||
|
||||
@ -213,12 +198,18 @@ impl User {
|
||||
.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)
|
||||
.set((
|
||||
users::display_name.eq(name),
|
||||
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),
|
||||
))
|
||||
.execute(conn)?;
|
||||
@ -278,16 +269,13 @@ impl User {
|
||||
}
|
||||
|
||||
pub fn fetch_from_url(conn: &Connection, url: &str) -> Result<User> {
|
||||
User::fetch(url).and_then(|json| User::from_activity(
|
||||
conn,
|
||||
&json,
|
||||
Url::parse(url)?.host_str()?,
|
||||
))
|
||||
User::fetch(url)
|
||||
.and_then(|json| User::from_activity(conn, &json, Url::parse(url)?.host_str()?))
|
||||
}
|
||||
|
||||
fn from_activity(conn: &Connection, acct: &CustomPerson, inst: &str) -> Result<User> {
|
||||
let instance = Instance::find_by_domain(conn, inst)
|
||||
.or_else(|_| Instance::insert(
|
||||
let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
|
||||
Instance::insert(
|
||||
conn,
|
||||
NewInstance {
|
||||
name: inst.to_owned(),
|
||||
@ -301,9 +289,15 @@ impl User {
|
||||
short_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);
|
||||
}
|
||||
let user = User::insert(
|
||||
@ -314,20 +308,11 @@ impl User {
|
||||
.ap_actor_props
|
||||
.preferred_username_string()
|
||||
.unwrap(),
|
||||
display_name: acct
|
||||
.object
|
||||
.object_props
|
||||
.name_string()?,
|
||||
outbox_url: acct
|
||||
.object
|
||||
.ap_actor_props
|
||||
.outbox_string()?,
|
||||
inbox_url: acct
|
||||
.object
|
||||
.ap_actor_props
|
||||
.inbox_string()?,
|
||||
display_name: acct.object.object_props.name_string()?,
|
||||
outbox_url: acct.object.ap_actor_props.outbox_string()?,
|
||||
inbox_url: acct.object.ap_actor_props.inbox_string()?,
|
||||
is_admin: false,
|
||||
summary:acct
|
||||
summary: acct
|
||||
.object
|
||||
.object_props
|
||||
.summary_string()
|
||||
@ -342,10 +327,7 @@ impl User {
|
||||
email: None,
|
||||
hashed_password: None,
|
||||
instance_id: instance.id,
|
||||
ap_url: acct
|
||||
.object
|
||||
.object_props
|
||||
.id_string()?,
|
||||
ap_url: acct.object.object_props.id_string()?,
|
||||
public_key: acct
|
||||
.custom_props
|
||||
.public_key_publickey()?
|
||||
@ -357,10 +339,7 @@ impl User {
|
||||
.endpoints_endpoint()
|
||||
.and_then(|e| e.shared_inbox_string())
|
||||
.ok(),
|
||||
followers_endpoint: acct
|
||||
.object
|
||||
.ap_actor_props
|
||||
.followers_string()?,
|
||||
followers_endpoint: acct.object.ap_actor_props.followers_string()?,
|
||||
avatar_id: None,
|
||||
},
|
||||
)?;
|
||||
@ -392,26 +371,15 @@ impl User {
|
||||
.object_props
|
||||
.url_string()?,
|
||||
&self,
|
||||
).ok();
|
||||
)
|
||||
.ok();
|
||||
|
||||
diesel::update(self)
|
||||
.set((
|
||||
users::username.eq(json
|
||||
.object
|
||||
.ap_actor_props
|
||||
.preferred_username_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::username.eq(json.object.ap_actor_props.preferred_username_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(
|
||||
&json
|
||||
.object
|
||||
@ -419,10 +387,7 @@ impl User {
|
||||
.summary_string()
|
||||
.unwrap_or_default(),
|
||||
)),
|
||||
users::followers_endpoint.eq(json
|
||||
.object
|
||||
.ap_actor_props
|
||||
.followers_string()?),
|
||||
users::followers_endpoint.eq(json.object.ap_actor_props.followers_string()?),
|
||||
users::avatar_id.eq(avatar.map(|a| a.id)),
|
||||
users::last_fetched_date.eq(Utc::now().naive_utc()),
|
||||
users::public_key.eq(json
|
||||
@ -441,7 +406,8 @@ impl User {
|
||||
}
|
||||
|
||||
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))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
@ -468,8 +434,7 @@ impl User {
|
||||
let n_acts = acts.len();
|
||||
let mut coll = OrderedCollection::default();
|
||||
coll.collection_props.items = serde_json::to_value(acts)?;
|
||||
coll.collection_props
|
||||
.set_total_items_u64(n_acts as u64)?;
|
||||
coll.collection_props.set_total_items_u64(n_acts as u64)?;
|
||||
Ok(ActivityStream::new(coll))
|
||||
}
|
||||
|
||||
@ -483,12 +448,11 @@ impl User {
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
)?
|
||||
)?,
|
||||
)
|
||||
.send()?;
|
||||
let text = &res.text()?;
|
||||
let json: serde_json::Value =
|
||||
serde_json::from_str(text)?;
|
||||
let json: serde_json::Value = serde_json::from_str(text)?;
|
||||
Ok(json["items"]
|
||||
.as_array()
|
||||
.unwrap_or(&vec![])
|
||||
@ -507,7 +471,7 @@ impl User {
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
)?
|
||||
)?,
|
||||
)
|
||||
.send()?;
|
||||
let text = &res.text()?;
|
||||
@ -531,7 +495,9 @@ impl User {
|
||||
Ok(posts
|
||||
.into_iter()
|
||||
.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>>())
|
||||
}
|
||||
@ -555,7 +521,11 @@ impl User {
|
||||
.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;
|
||||
let follows = Follow::belonging_to(self).select(follows::follower_id);
|
||||
users::table
|
||||
@ -584,7 +554,11 @@ impl User {
|
||||
.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;
|
||||
let follows = follows::table
|
||||
.filter(follows::follower_id.eq(self.id))
|
||||
@ -653,33 +627,32 @@ impl User {
|
||||
}
|
||||
|
||||
pub fn get_keypair(&self) -> Result<PKey<Private>> {
|
||||
PKey::from_rsa(
|
||||
Rsa::private_key_from_pem(
|
||||
self.private_key
|
||||
.clone()?
|
||||
.as_ref(),
|
||||
)?,
|
||||
).map_err(Error::from)
|
||||
PKey::from_rsa(Rsa::private_key_from_pem(
|
||||
self.private_key.clone()?.as_ref(),
|
||||
)?)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn rotate_keypair(&self, conn: &Connection) -> Result<PKey<Private>> {
|
||||
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 {
|
||||
//rotated recently
|
||||
self.get_keypair()
|
||||
} else {
|
||||
let (public_key, private_key) = gen_keypair();
|
||||
let public_key = String::from_utf8(public_key).expect("NewUser::new_local: public key error");
|
||||
let private_key = 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())?
|
||||
)?;
|
||||
let public_key =
|
||||
String::from_utf8(public_key).expect("NewUser::new_local: public key error");
|
||||
let private_key =
|
||||
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)
|
||||
.set((users::public_key.eq(public_key),
|
||||
.set((
|
||||
users::public_key.eq(public_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)
|
||||
.map_err(Error::from)
|
||||
.map(|_| res)
|
||||
@ -688,18 +661,14 @@ impl User {
|
||||
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<CustomPerson> {
|
||||
let mut actor = Person::default();
|
||||
actor
|
||||
.object_props
|
||||
.set_id_string(self.ap_url.clone())?;
|
||||
actor.object_props.set_id_string(self.ap_url.clone())?;
|
||||
actor
|
||||
.object_props
|
||||
.set_name_string(self.display_name.clone())?;
|
||||
actor
|
||||
.object_props
|
||||
.set_summary_string(self.summary_html.get().clone())?;
|
||||
actor
|
||||
.object_props
|
||||
.set_url_string(self.ap_url.clone())?;
|
||||
actor.object_props.set_url_string(self.ap_url.clone())?;
|
||||
actor
|
||||
.ap_actor_props
|
||||
.set_inbox_string(self.inbox_url.clone())?;
|
||||
@ -714,42 +683,31 @@ impl User {
|
||||
.set_followers_string(self.followers_endpoint.clone())?;
|
||||
|
||||
let mut endpoints = Endpoint::default();
|
||||
endpoints
|
||||
.set_shared_inbox_string(ap_url(&format!("{}/inbox/", BASE_URL.as_str())))?;
|
||||
actor
|
||||
.ap_actor_props
|
||||
.set_endpoints_endpoint(endpoints)?;
|
||||
endpoints.set_shared_inbox_string(ap_url(&format!("{}/inbox/", BASE_URL.as_str())))?;
|
||||
actor.ap_actor_props.set_endpoints_endpoint(endpoints)?;
|
||||
|
||||
let mut public_key = PublicKey::default();
|
||||
public_key
|
||||
.set_id_string(format!("{}#main-key", self.ap_url))?;
|
||||
public_key
|
||||
.set_owner_string(self.ap_url.clone())?;
|
||||
public_key
|
||||
.set_public_key_pem_string(self.public_key.clone())?;
|
||||
public_key.set_id_string(format!("{}#main-key", self.ap_url))?;
|
||||
public_key.set_owner_string(self.ap_url.clone())?;
|
||||
public_key.set_public_key_pem_string(self.public_key.clone())?;
|
||||
let mut ap_signature = ApSignature::default();
|
||||
ap_signature
|
||||
.set_public_key_publickey(public_key)?;
|
||||
ap_signature.set_public_key_publickey(public_key)?;
|
||||
|
||||
let mut avatar = Image::default();
|
||||
avatar
|
||||
.object_props
|
||||
.set_url_string(
|
||||
self.avatar_id
|
||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url(conn)).ok())
|
||||
.unwrap_or_default(),
|
||||
)?;
|
||||
actor
|
||||
.object_props
|
||||
.set_icon_object(avatar)?;
|
||||
avatar.object_props.set_url_string(
|
||||
self.avatar_id
|
||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url(conn)).ok())
|
||||
.unwrap_or_default(),
|
||||
)?;
|
||||
actor.object_props.set_icon_object(avatar)?;
|
||||
|
||||
Ok(CustomPerson::new(actor, ap_signature))
|
||||
}
|
||||
|
||||
pub fn avatar_url(&self, conn: &Connection) -> String {
|
||||
self.avatar_id.and_then(|id|
|
||||
Media::get(conn, id).and_then(|m| m.url(conn)).ok()
|
||||
).unwrap_or_else(|| "/static/default-avatar.png".to_string())
|
||||
self.avatar_id
|
||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url(conn)).ok())
|
||||
.unwrap_or_else(|| "/static/default-avatar.png".to_string())
|
||||
}
|
||||
|
||||
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>> {
|
||||
let key = self.get_keypair()?;
|
||||
let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?;
|
||||
signer
|
||||
.update(to_sign.as_bytes())?;
|
||||
signer
|
||||
.sign_to_vec()
|
||||
.map_err(Error::from)
|
||||
signer.update(to_sign.as_bytes())?;
|
||||
signer.sign_to_vec().map_err(Error::from)
|
||||
}
|
||||
|
||||
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 mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?;
|
||||
verifier
|
||||
.update(data.as_bytes())?;
|
||||
verifier
|
||||
.verify(&signature)
|
||||
.map_err(Error::from)
|
||||
verifier.update(data.as_bytes())?;
|
||||
verifier.verify(&signature).map_err(Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
@ -915,7 +867,7 @@ impl NewUser {
|
||||
display_name,
|
||||
is_admin,
|
||||
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),
|
||||
hashed_password: Some(password),
|
||||
instance_id: Instance::get_local(conn)?.id,
|
||||
@ -947,7 +899,8 @@ pub(crate) mod tests {
|
||||
"Hello there, I'm the admin",
|
||||
"admin@example.com".to_owned(),
|
||||
"invalid_admin_password".to_owned(),
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
let user = NewUser::new_local(
|
||||
conn,
|
||||
"user".to_owned(),
|
||||
@ -956,7 +909,8 @@ pub(crate) mod tests {
|
||||
"Hello there, I'm no one",
|
||||
"user@example.com".to_owned(),
|
||||
"invalid_user_password".to_owned(),
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
let other = NewUser::new_local(
|
||||
conn,
|
||||
"other".to_owned(),
|
||||
@ -965,8 +919,9 @@ pub(crate) mod tests {
|
||||
"Hello there, I'm someone else",
|
||||
"other@example.com".to_owned(),
|
||||
"invalid_other_password".to_owned(),
|
||||
).unwrap();
|
||||
vec![ admin, user, other ]
|
||||
)
|
||||
.unwrap();
|
||||
vec![admin, user, other]
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -982,7 +937,8 @@ pub(crate) mod tests {
|
||||
"Hello I'm a test",
|
||||
"test@example.com".to_owned(),
|
||||
User::hash_pass("test_password").unwrap(),
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
test_user.id,
|
||||
@ -996,9 +952,7 @@ pub(crate) mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
test_user.id,
|
||||
User::find_by_email(conn, "test@example.com")
|
||||
.unwrap()
|
||||
.id
|
||||
User::find_by_email(conn, "test@example.com").unwrap().id
|
||||
);
|
||||
assert_eq!(
|
||||
test_user.id,
|
||||
@ -1009,8 +963,9 @@ pub(crate) mod tests {
|
||||
Instance::get_local(conn).unwrap().public_domain,
|
||||
"test"
|
||||
)
|
||||
).unwrap()
|
||||
.id
|
||||
)
|
||||
.unwrap()
|
||||
.id
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@ -1040,7 +995,11 @@ pub(crate) mod tests {
|
||||
let mut i = 0;
|
||||
while local_inst.has_admin(conn).unwrap() {
|
||||
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;
|
||||
}
|
||||
inserted[0].grant_admin_rights(conn).unwrap();
|
||||
@ -1055,12 +1014,14 @@ pub(crate) mod tests {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let inserted = fill_database(conn);
|
||||
let updated = inserted[0].update(
|
||||
conn,
|
||||
"new name".to_owned(),
|
||||
"em@il".to_owned(),
|
||||
"<p>summary</p><script></script>".to_owned(),
|
||||
).unwrap();
|
||||
let updated = inserted[0]
|
||||
.update(
|
||||
conn,
|
||||
"new name".to_owned(),
|
||||
"em@il".to_owned(),
|
||||
"<p>summary</p><script></script>".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(updated.display_name, "new name");
|
||||
assert_eq!(updated.email.unwrap(), "em@il");
|
||||
assert_eq!(updated.summary_html.get(), "<p>summary</p>");
|
||||
@ -1082,7 +1043,8 @@ pub(crate) mod tests {
|
||||
"Hello I'm a test",
|
||||
"test@example.com".to_owned(),
|
||||
User::hash_pass("test_password").unwrap(),
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(test_user.auth("test_password"));
|
||||
assert!(!test_user.auth("other_password"));
|
||||
@ -1101,7 +1063,9 @@ pub(crate) mod tests {
|
||||
assert_eq!(page.len(), 2);
|
||||
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 {
|
||||
let page = User::get_local_page(conn, (i, i + 1)).unwrap();
|
||||
assert_eq!(page.len(), 1);
|
||||
@ -1109,7 +1073,9 @@ pub(crate) mod tests {
|
||||
last_username = page[0].username.clone();
|
||||
}
|
||||
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()
|
||||
);
|
||||
|
||||
|
@ -3,11 +3,7 @@ use rocket_contrib::json::Json;
|
||||
use serde_json;
|
||||
|
||||
use plume_api::apps::AppEndpoint;
|
||||
use plume_models::{
|
||||
Connection,
|
||||
db_conn::DbConn,
|
||||
apps::App,
|
||||
};
|
||||
use plume_models::{apps::App, db_conn::DbConn, Connection};
|
||||
|
||||
#[post("/apps", data = "<data>")]
|
||||
pub fn create(conn: DbConn, data: Json<AppEndpoint>) -> Json<serde_json::Value> {
|
||||
|
@ -1,10 +1,10 @@
|
||||
use plume_models::{self, api_tokens::ApiToken};
|
||||
use rocket::{
|
||||
Outcome,
|
||||
http::Status,
|
||||
request::{self, FromRequest, Request}
|
||||
request::{self, FromRequest, Request},
|
||||
Outcome,
|
||||
};
|
||||
use std::marker::PhantomData;
|
||||
use plume_models::{self, api_tokens::ApiToken};
|
||||
|
||||
// Actions
|
||||
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>
|
||||
where A: Action,
|
||||
S: Scope
|
||||
where
|
||||
A: Action,
|
||||
S: Scope,
|
||||
{
|
||||
type Error = ();
|
||||
|
||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Authorization<A, S>, ()> {
|
||||
request.guard::<ApiToken>()
|
||||
request
|
||||
.guard::<ApiToken>()
|
||||
.map_failure(|_| (Status::Unauthorized, ()))
|
||||
.and_then(|token| if token.can(A::to_str(), S::to_str()) {
|
||||
Outcome::Success(Authorization(token, PhantomData))
|
||||
} else {
|
||||
Outcome::Failure((Status::Unauthorized, ()))
|
||||
.and_then(|token| {
|
||||
if token.can(A::to_str(), S::to_str()) {
|
||||
Outcome::Success(Authorization(token, PhantomData))
|
||||
} else {
|
||||
Outcome::Failure((Status::Unauthorized, ()))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,16 +1,13 @@
|
||||
#![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 serde_json;
|
||||
|
||||
use plume_common::utils::random_hex;
|
||||
use plume_models::{
|
||||
Error,
|
||||
apps::App,
|
||||
api_tokens::*,
|
||||
db_conn::DbConn,
|
||||
users::User,
|
||||
};
|
||||
use plume_models::{api_tokens::*, apps::App, db_conn::DbConn, users::User, Error};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ApiError(Error);
|
||||
@ -26,13 +23,16 @@ impl<'r> Responder<'r> for ApiError {
|
||||
match self.0 {
|
||||
Error::NotFound => Json(json!({
|
||||
"error": "Not found"
|
||||
})).respond_to(req),
|
||||
}))
|
||||
.respond_to(req),
|
||||
Error::Unauthorized => Json(json!({
|
||||
"error": "You are not authorized to access this resource"
|
||||
})).respond_to(req),
|
||||
}))
|
||||
.respond_to(req),
|
||||
_ => Json(json!({
|
||||
"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 let Ok(user) = User::find_by_fqn(&*conn, &query.username) {
|
||||
if user.auth(&query.password) {
|
||||
let token = ApiToken::insert(&*conn, NewApiToken {
|
||||
app_id: app.id,
|
||||
user_id: user.id,
|
||||
value: random_hex(),
|
||||
scopes: query.scopes.clone(),
|
||||
})?;
|
||||
let token = ApiToken::insert(
|
||||
&*conn,
|
||||
NewApiToken {
|
||||
app_id: app.id,
|
||||
user_id: user.id,
|
||||
value: random_hex(),
|
||||
scopes: query.scopes.clone(),
|
||||
},
|
||||
)?;
|
||||
Ok(Json(json!({
|
||||
"token": token.value
|
||||
})))
|
||||
|
@ -5,43 +5,79 @@ use scheduled_thread_pool::ScheduledThreadPool;
|
||||
use serde_json;
|
||||
use serde_qs;
|
||||
|
||||
use api::authorization::*;
|
||||
use plume_api::posts::PostEndpoint;
|
||||
use plume_models::{
|
||||
Connection,
|
||||
db_conn::DbConn,
|
||||
posts::Post,
|
||||
search::Searcher as UnmanagedSearcher,
|
||||
db_conn::DbConn, posts::Post, search::Searcher as UnmanagedSearcher, Connection,
|
||||
};
|
||||
use api::authorization::*;
|
||||
use {Searcher, Worker};
|
||||
|
||||
#[get("/posts/<id>")]
|
||||
pub fn get(id: i32, 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();
|
||||
pub fn get(
|
||||
id: i32,
|
||||
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))
|
||||
}
|
||||
|
||||
#[get("/posts")]
|
||||
pub fn list(conn: DbConn, uri: &Origin, 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);
|
||||
pub fn list(
|
||||
conn: DbConn,
|
||||
uri: &Origin,
|
||||
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))
|
||||
}
|
||||
|
||||
#[post("/posts", data = "<payload>")]
|
||||
pub fn create(conn: DbConn, payload: Json<PostEndpoint>, 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",
|
||||
"details": match e {
|
||||
ApiError::Fetch(msg) => msg,
|
||||
ApiError::SerDe(msg) => msg,
|
||||
ApiError::NotFound(msg) => msg,
|
||||
ApiError::Authorization(msg) => msg,
|
||||
}
|
||||
})))
|
||||
pub fn create(
|
||||
conn: DbConn,
|
||||
payload: Json<PostEndpoint>,
|
||||
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",
|
||||
"details": match e {
|
||||
ApiError::Fetch(msg) => msg,
|
||||
ApiError::SerDe(msg) => msg,
|
||||
ApiError::NotFound(msg) => msg,
|
||||
ApiError::Authorization(msg) => msg,
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
|
83
src/inbox.rs
83
src/inbox.rs
@ -1,23 +1,10 @@
|
||||
#![warn(clippy::too_many_arguments)]
|
||||
use activitypub::{
|
||||
activity::{
|
||||
Announce,
|
||||
Create,
|
||||
Delete,
|
||||
Follow as FollowAct,
|
||||
Like,
|
||||
Undo,
|
||||
Update
|
||||
},
|
||||
object::Tombstone
|
||||
activity::{Announce, Create, Delete, Follow as FollowAct, Like, Undo, Update},
|
||||
object::Tombstone,
|
||||
};
|
||||
use failure::Error;
|
||||
use rocket::{
|
||||
data::*,
|
||||
http::Status,
|
||||
Outcome::*,
|
||||
Request,
|
||||
};
|
||||
use rocket::{data::*, http::Status, Outcome::*, Request};
|
||||
use rocket_contrib::json::*;
|
||||
use serde::Deserialize;
|
||||
use serde_json;
|
||||
@ -26,15 +13,21 @@ use std::io::Read;
|
||||
|
||||
use plume_common::activity_pub::{
|
||||
inbox::{Deletable, FromActivity, InboxError, Notify},
|
||||
Id,request::Digest,
|
||||
request::Digest,
|
||||
Id,
|
||||
};
|
||||
use plume_models::{
|
||||
comments::Comment, follows::Follow, instance::Instance, likes, posts::Post, reshares::Reshare,
|
||||
users::User, search::Searcher, Connection,
|
||||
search::Searcher, users::User, Connection,
|
||||
};
|
||||
|
||||
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(|| {
|
||||
act["actor"]["id"]
|
||||
.as_str()
|
||||
@ -66,7 +59,8 @@ pub trait Inbox {
|
||||
.id_string()?,
|
||||
actor_id.as_ref(),
|
||||
&(conn, searcher),
|
||||
).ok();
|
||||
)
|
||||
.ok();
|
||||
Comment::delete_id(
|
||||
&act.delete_props
|
||||
.object_object::<Tombstone>()?
|
||||
@ -74,12 +68,14 @@ pub trait Inbox {
|
||||
.id_string()?,
|
||||
actor_id.as_ref(),
|
||||
conn,
|
||||
).ok();
|
||||
)
|
||||
.ok();
|
||||
Ok(())
|
||||
}
|
||||
"Follow" => {
|
||||
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(())
|
||||
}
|
||||
"Like" => {
|
||||
@ -87,7 +83,8 @@ pub trait Inbox {
|
||||
conn,
|
||||
serde_json::from_value(act.clone())?,
|
||||
actor_id,
|
||||
).expect("Inbox::received: like from activity error");;
|
||||
)
|
||||
.expect("Inbox::received: like from activity error");;
|
||||
Ok(())
|
||||
}
|
||||
"Undo" => {
|
||||
@ -102,7 +99,8 @@ pub trait Inbox {
|
||||
.id_string()?,
|
||||
actor_id.as_ref(),
|
||||
conn,
|
||||
).expect("Inbox::received: undo like fail");;
|
||||
)
|
||||
.expect("Inbox::received: undo like fail");;
|
||||
Ok(())
|
||||
}
|
||||
"Announce" => {
|
||||
@ -113,7 +111,8 @@ pub trait Inbox {
|
||||
.id_string()?,
|
||||
actor_id.as_ref(),
|
||||
conn,
|
||||
).expect("Inbox::received: undo reshare fail");;
|
||||
)
|
||||
.expect("Inbox::received: undo reshare fail");;
|
||||
Ok(())
|
||||
}
|
||||
"Follow" => {
|
||||
@ -124,21 +123,28 @@ pub trait Inbox {
|
||||
.id_string()?,
|
||||
actor_id.as_ref(),
|
||||
conn,
|
||||
).expect("Inbox::received: undo follow error");;
|
||||
)
|
||||
.expect("Inbox::received: undo follow error");;
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(InboxError::CantUndo)?,
|
||||
}
|
||||
} 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) {
|
||||
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(())
|
||||
} 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(())
|
||||
} 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(())
|
||||
} else {
|
||||
Err(InboxError::NoType)?
|
||||
@ -147,7 +153,8 @@ pub trait Inbox {
|
||||
}
|
||||
"Update" => {
|
||||
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(())
|
||||
}
|
||||
_ => Err(InboxError::InvalidType)?,
|
||||
@ -169,19 +176,25 @@ impl<'a, T: Deserialize<'a>> FromData<'a> for SignedJson<T> {
|
||||
type Owned = String;
|
||||
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 mut s = String::with_capacity(512);
|
||||
match d.open().take(size_limit).read_to_string(&mut 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()?;
|
||||
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) => {
|
||||
if e.is_data() {
|
||||
Failure((Status::UnprocessableEntity, JsonError::Parse(string, e)))
|
||||
|
35
src/mail.rs
35
src/mail.rs
@ -6,8 +6,8 @@ pub use self::mailer::*;
|
||||
|
||||
#[cfg(feature = "debug-mailer")]
|
||||
mod mailer {
|
||||
use lettre::{Transport, SendableEmail};
|
||||
use std::{io::Read};
|
||||
use lettre::{SendableEmail, Transport};
|
||||
use std::io::Read;
|
||||
|
||||
pub struct DebugTransport;
|
||||
|
||||
@ -18,11 +18,18 @@ mod mailer {
|
||||
println!(
|
||||
"{}: from=<{}> to=<{:?}>\n{:#?}",
|
||||
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(),
|
||||
{
|
||||
let mut message = String::new();
|
||||
email.message().read_to_string(&mut message).map_err(|_| ())?;
|
||||
email
|
||||
.message()
|
||||
.read_to_string(&mut message)
|
||||
.map_err(|_| ())?;
|
||||
message
|
||||
},
|
||||
);
|
||||
@ -40,13 +47,12 @@ mod mailer {
|
||||
#[cfg(not(feature = "debug-mailer"))]
|
||||
mod mailer {
|
||||
use lettre::{
|
||||
SmtpTransport,
|
||||
SmtpClient,
|
||||
smtp::{
|
||||
authentication::{Credentials, Mechanism},
|
||||
extension::ClientId,
|
||||
ConnectionReuseParameters,
|
||||
},
|
||||
SmtpClient, SmtpTransport,
|
||||
};
|
||||
use std::env;
|
||||
|
||||
@ -57,7 +63,8 @@ mod mailer {
|
||||
let helo_name = env::var("MAIL_HELO_NAME").unwrap_or_else(|_| "localhost".to_owned());
|
||||
let username = env::var("MAIL_USER").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))
|
||||
.credentials(Credentials::new(username, password))
|
||||
.smtp_utf8(true)
|
||||
@ -70,9 +77,17 @@ mod mailer {
|
||||
|
||||
pub fn build_mail(dest: String, subject: String, body: String) -> Option<Email> {
|
||||
Email::builder()
|
||||
.from(env::var("MAIL_ADDRESS")
|
||||
.or_else(|_| Ok(format!("{}@{}", env::var("MAIL_USER")?, env::var("MAIL_SERVER")?)) as Result<_, env::VarError>)
|
||||
.expect("Mail server is not correctly configured"))
|
||||
.from(
|
||||
env::var("MAIL_ADDRESS")
|
||||
.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)
|
||||
.subject(subject)
|
||||
.text(body)
|
||||
|
299
src/main.rs
299
src/main.rs
@ -39,16 +39,13 @@ extern crate validator_derive;
|
||||
extern crate webfinger;
|
||||
|
||||
use diesel::r2d2::ConnectionManager;
|
||||
use rocket::{
|
||||
Config, State,
|
||||
config::Limits
|
||||
};
|
||||
use rocket_csrf::CsrfFairingBuilder;
|
||||
use plume_models::{
|
||||
DATABASE_URL, Connection, Error,
|
||||
db_conn::{DbPool, PragmaForeignKey},
|
||||
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 std::env;
|
||||
use std::process::exit;
|
||||
@ -78,7 +75,8 @@ fn init_pool() -> Option<DbPool> {
|
||||
let manager = ConnectionManager::<Connection>::new(DATABASE_URL.as_str());
|
||||
DbPool::builder()
|
||||
.connection_customizer(Box::new(PragmaForeignKey))
|
||||
.build(manager).ok()
|
||||
.build(manager)
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@ -89,37 +87,58 @@ fn main() {
|
||||
let searcher = match UnmanagedSearcher::open(&"search_index") {
|
||||
Err(Error::Search(e)) => match e {
|
||||
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:
|
||||
|
||||
plm search unlock
|
||||
|
||||
Then try to restart Plume.
|
||||
"#),
|
||||
e => Err(e).unwrap()
|
||||
"#
|
||||
),
|
||||
e => Err(e).unwrap(),
|
||||
},
|
||||
Err(_) => panic!("Unexpected error while opening search index"),
|
||||
Ok(s) => Arc::new(s)
|
||||
Ok(s) => Arc::new(s),
|
||||
};
|
||||
|
||||
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();
|
||||
ctrlc::set_handler(move || {
|
||||
search_unlocker.drop_writer();
|
||||
exit(0);
|
||||
}).expect("Error setting Ctrl-c handler");
|
||||
})
|
||||
.expect("Error setting Ctrl-c handler");
|
||||
|
||||
let mut config = Config::active().unwrap();
|
||||
config.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));
|
||||
config
|
||||
.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 form_size = &env::var("FORM_SIZE").unwrap_or_else(|_| "32".to_owned()).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("json", activity_size * 1024));
|
||||
let form_size = &env::var("FORM_SIZE")
|
||||
.unwrap_or_else(|_| "32".to_owned())
|
||||
.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("json", activity_size * 1024),
|
||||
);
|
||||
|
||||
let mail = mail::init();
|
||||
if mail.is_none() && config.environment.is_prod() {
|
||||
@ -128,110 +147,100 @@ Then try to restart Plume.
|
||||
}
|
||||
|
||||
rocket::custom(config)
|
||||
.mount("/", routes![
|
||||
routes::blogs::details,
|
||||
routes::blogs::activity_details,
|
||||
routes::blogs::outbox,
|
||||
routes::blogs::new,
|
||||
routes::blogs::new_auth,
|
||||
routes::blogs::create,
|
||||
routes::blogs::delete,
|
||||
routes::blogs::atom_feed,
|
||||
|
||||
routes::comments::create,
|
||||
routes::comments::delete,
|
||||
routes::comments::activity_pub,
|
||||
|
||||
routes::instance::index,
|
||||
routes::instance::local,
|
||||
routes::instance::feed,
|
||||
routes::instance::federated,
|
||||
routes::instance::admin,
|
||||
routes::instance::admin_instances,
|
||||
routes::instance::admin_users,
|
||||
routes::instance::ban,
|
||||
routes::instance::toggle_block,
|
||||
routes::instance::update_settings,
|
||||
routes::instance::shared_inbox,
|
||||
routes::instance::nodeinfo,
|
||||
routes::instance::about,
|
||||
routes::instance::web_manifest,
|
||||
|
||||
routes::likes::create,
|
||||
routes::likes::create_auth,
|
||||
|
||||
routes::medias::list,
|
||||
routes::medias::new,
|
||||
routes::medias::upload,
|
||||
routes::medias::details,
|
||||
routes::medias::delete,
|
||||
routes::medias::set_avatar,
|
||||
|
||||
routes::notifications::notifications,
|
||||
routes::notifications::notifications_auth,
|
||||
|
||||
routes::posts::details,
|
||||
routes::posts::activity_details,
|
||||
routes::posts::edit,
|
||||
routes::posts::update,
|
||||
routes::posts::new,
|
||||
routes::posts::new_auth,
|
||||
routes::posts::create,
|
||||
routes::posts::delete,
|
||||
|
||||
routes::reshares::create,
|
||||
routes::reshares::create_auth,
|
||||
|
||||
routes::search::search,
|
||||
|
||||
routes::session::new,
|
||||
routes::session::create,
|
||||
routes::session::delete,
|
||||
routes::session::password_reset_request_form,
|
||||
routes::session::password_reset_request,
|
||||
routes::session::password_reset_form,
|
||||
routes::session::password_reset,
|
||||
|
||||
routes::plume_static_files,
|
||||
routes::static_files,
|
||||
|
||||
routes::tags::tag,
|
||||
|
||||
routes::user::me,
|
||||
routes::user::details,
|
||||
routes::user::dashboard,
|
||||
routes::user::dashboard_auth,
|
||||
routes::user::followers,
|
||||
routes::user::followed,
|
||||
routes::user::edit,
|
||||
routes::user::edit_auth,
|
||||
routes::user::update,
|
||||
routes::user::delete,
|
||||
routes::user::follow,
|
||||
routes::user::follow_auth,
|
||||
routes::user::activity_details,
|
||||
routes::user::outbox,
|
||||
routes::user::inbox,
|
||||
routes::user::ap_followers,
|
||||
routes::user::new,
|
||||
routes::user::create,
|
||||
routes::user::atom_feed,
|
||||
|
||||
routes::well_known::host_meta,
|
||||
routes::well_known::nodeinfo,
|
||||
routes::well_known::webfinger,
|
||||
|
||||
routes::errors::csrf_violation
|
||||
])
|
||||
.mount("/api/v1", routes![
|
||||
api::oauth,
|
||||
|
||||
api::apps::create,
|
||||
|
||||
api::posts::get,
|
||||
api::posts::list,
|
||||
api::posts::create,
|
||||
])
|
||||
.mount(
|
||||
"/",
|
||||
routes![
|
||||
routes::blogs::details,
|
||||
routes::blogs::activity_details,
|
||||
routes::blogs::outbox,
|
||||
routes::blogs::new,
|
||||
routes::blogs::new_auth,
|
||||
routes::blogs::create,
|
||||
routes::blogs::delete,
|
||||
routes::blogs::atom_feed,
|
||||
routes::comments::create,
|
||||
routes::comments::delete,
|
||||
routes::comments::activity_pub,
|
||||
routes::instance::index,
|
||||
routes::instance::local,
|
||||
routes::instance::feed,
|
||||
routes::instance::federated,
|
||||
routes::instance::admin,
|
||||
routes::instance::admin_instances,
|
||||
routes::instance::admin_users,
|
||||
routes::instance::ban,
|
||||
routes::instance::toggle_block,
|
||||
routes::instance::update_settings,
|
||||
routes::instance::shared_inbox,
|
||||
routes::instance::nodeinfo,
|
||||
routes::instance::about,
|
||||
routes::instance::web_manifest,
|
||||
routes::likes::create,
|
||||
routes::likes::create_auth,
|
||||
routes::medias::list,
|
||||
routes::medias::new,
|
||||
routes::medias::upload,
|
||||
routes::medias::details,
|
||||
routes::medias::delete,
|
||||
routes::medias::set_avatar,
|
||||
routes::notifications::notifications,
|
||||
routes::notifications::notifications_auth,
|
||||
routes::posts::details,
|
||||
routes::posts::activity_details,
|
||||
routes::posts::edit,
|
||||
routes::posts::update,
|
||||
routes::posts::new,
|
||||
routes::posts::new_auth,
|
||||
routes::posts::create,
|
||||
routes::posts::delete,
|
||||
routes::reshares::create,
|
||||
routes::reshares::create_auth,
|
||||
routes::search::search,
|
||||
routes::session::new,
|
||||
routes::session::create,
|
||||
routes::session::delete,
|
||||
routes::session::password_reset_request_form,
|
||||
routes::session::password_reset_request,
|
||||
routes::session::password_reset_form,
|
||||
routes::session::password_reset,
|
||||
routes::plume_static_files,
|
||||
routes::static_files,
|
||||
routes::tags::tag,
|
||||
routes::user::me,
|
||||
routes::user::details,
|
||||
routes::user::dashboard,
|
||||
routes::user::dashboard_auth,
|
||||
routes::user::followers,
|
||||
routes::user::followed,
|
||||
routes::user::edit,
|
||||
routes::user::edit_auth,
|
||||
routes::user::update,
|
||||
routes::user::delete,
|
||||
routes::user::follow,
|
||||
routes::user::follow_auth,
|
||||
routes::user::activity_details,
|
||||
routes::user::outbox,
|
||||
routes::user::inbox,
|
||||
routes::user::ap_followers,
|
||||
routes::user::new,
|
||||
routes::user::create,
|
||||
routes::user::atom_feed,
|
||||
routes::well_known::host_meta,
|
||||
routes::well_known::nodeinfo,
|
||||
routes::well_known::webfinger,
|
||||
routes::errors::csrf_violation
|
||||
],
|
||||
)
|
||||
.mount(
|
||||
"/api/v1",
|
||||
routes![
|
||||
api::oauth,
|
||||
api::apps::create,
|
||||
api::posts::get,
|
||||
api::posts::list,
|
||||
api::posts::create,
|
||||
],
|
||||
)
|
||||
.register(catchers![
|
||||
routes::errors::not_found,
|
||||
routes::errors::unprocessable_entity,
|
||||
@ -243,15 +252,41 @@ Then try to restart Plume.
|
||||
.manage(workpool)
|
||||
.manage(searcher)
|
||||
.manage(include_i18n!())
|
||||
.attach(CsrfFairingBuilder::new()
|
||||
.set_default_target("/csrf-violation?target=<uri>".to_owned(), rocket::http::Method::Post)
|
||||
.attach(
|
||||
CsrfFairingBuilder::new()
|
||||
.set_default_target(
|
||||
"/csrf-violation?target=<uri>".to_owned(),
|
||||
rocket::http::Method::Post,
|
||||
)
|
||||
.add_exceptions(vec![
|
||||
("/inbox".to_owned(), "/inbox".to_owned(), 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)
|
||||
(
|
||||
"/inbox".to_owned(),
|
||||
"/inbox".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();
|
||||
}
|
||||
|
@ -3,22 +3,16 @@ use atom_syndication::{Entry, FeedBuilder};
|
||||
use rocket::{
|
||||
http::ContentType,
|
||||
request::LenientForm,
|
||||
response::{Redirect, Flash, content::Content}
|
||||
response::{content::Content, Flash, Redirect},
|
||||
};
|
||||
use rocket_i18n::I18n;
|
||||
use std::{collections::HashMap, borrow::Cow};
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
use validator::{Validate, ValidationError, ValidationErrors};
|
||||
|
||||
use plume_common::activity_pub::{ActivityStream, ApRequest};
|
||||
use plume_common::utils;
|
||||
use plume_models::{
|
||||
blog_authors::*,
|
||||
blogs::*,
|
||||
db_conn::DbConn,
|
||||
instance::Instance,
|
||||
posts::Post,
|
||||
};
|
||||
use routes::{Page, PlumeRocket, errors::ErrorPage};
|
||||
use plume_models::{blog_authors::*, blogs::*, db_conn::DbConn, instance::Instance, posts::Post};
|
||||
use routes::{errors::ErrorPage, Page, PlumeRocket};
|
||||
use template_utils::Ructe;
|
||||
|
||||
#[get("/~/<name>?<page>", rank = 2)]
|
||||
@ -39,13 +33,18 @@ pub fn details(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result
|
||||
articles_count,
|
||||
page.0,
|
||||
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
|
||||
)))
|
||||
}
|
||||
|
||||
#[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()?;
|
||||
Some(ActivityStream::new(blog.to_activity(&*conn).ok()?))
|
||||
}
|
||||
@ -64,10 +63,13 @@ pub fn new(rockets: PlumeRocket) -> Ructe {
|
||||
}
|
||||
|
||||
#[get("/blogs/new", rank = 2)]
|
||||
pub fn new_auth(i18n: I18n) -> Flash<Redirect>{
|
||||
pub fn new_auth(i18n: I18n) -> Flash<Redirect> {
|
||||
utils::requires_login(
|
||||
&i18n!(i18n.catalog, "You need to be logged in order to create a new blog"),
|
||||
uri!(new)
|
||||
&i18n!(
|
||||
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() {
|
||||
Ok(_) => ValidationErrors::new(),
|
||||
Err(e) => e
|
||||
Err(e) => e,
|
||||
};
|
||||
if Blog::find_by_fqn(&*conn, &slug).is_ok() {
|
||||
errors.add("title", ValidationError {
|
||||
code: Cow::from("existing_slug"),
|
||||
message: Some(Cow::from("A blog with the same name already exists.")),
|
||||
params: HashMap::new()
|
||||
});
|
||||
errors.add(
|
||||
"title",
|
||||
ValidationError {
|
||||
code: Cow::from("existing_slug"),
|
||||
message: Some(Cow::from("A blog with the same name already exists.")),
|
||||
params: HashMap::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
let blog = Blog::insert(&*conn, NewBlog::new_local(
|
||||
slug.clone(),
|
||||
form.title.to_string(),
|
||||
String::from(""),
|
||||
Instance::get_local(&*conn).expect("blog::create: instance error").id
|
||||
).expect("blog::create: new local error")).expect("blog::create: error");
|
||||
let blog = Blog::insert(
|
||||
&*conn,
|
||||
NewBlog::new_local(
|
||||
slug.clone(),
|
||||
form.title.to_string(),
|
||||
String::from(""),
|
||||
Instance::get_local(&*conn)
|
||||
.expect("blog::create: instance error")
|
||||
.id,
|
||||
)
|
||||
.expect("blog::create: new local error"),
|
||||
)
|
||||
.expect("blog::create: error");
|
||||
|
||||
BlogAuthor::insert(&*conn, NewBlogAuthor {
|
||||
blog_id: blog.id,
|
||||
author_id: user.id,
|
||||
is_owner: true
|
||||
}).expect("blog::create: author error");
|
||||
BlogAuthor::insert(
|
||||
&*conn,
|
||||
NewBlogAuthor {
|
||||
blog_id: blog.id,
|
||||
author_id: user.id,
|
||||
is_owner: true,
|
||||
},
|
||||
)
|
||||
.expect("blog::create: author error");
|
||||
|
||||
Ok(Redirect::to(uri!(details: name = slug.clone(), page = _)))
|
||||
} else {
|
||||
@ -130,15 +146,20 @@ pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> Result<Re
|
||||
}
|
||||
|
||||
#[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 blog = Blog::find_by_fqn(&*conn, &name).expect("blog::delete: blog not found");
|
||||
let user = rockets.user;
|
||||
let intl = rockets.intl;
|
||||
let searcher = rockets.searcher;
|
||||
|
||||
if user.clone().and_then(|u| u.is_author_in(&*conn, &blog).ok()).unwrap_or(false) {
|
||||
blog.delete(&conn, &searcher).expect("blog::expect: deletion error");
|
||||
if user
|
||||
.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)))
|
||||
} else {
|
||||
// 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 feed = FeedBuilder::default()
|
||||
.title(blog.title.clone())
|
||||
.id(Instance::get_local(&*conn).ok()?
|
||||
.id(Instance::get_local(&*conn)
|
||||
.ok()?
|
||||
.compute_box("~", &name, "atom.xml"))
|
||||
.entries(Post::get_recents_for_blog(&*conn, &blog, 15).ok()?
|
||||
.into_iter()
|
||||
.map(|p| super::post_to_atom(p, &*conn))
|
||||
.collect::<Vec<Entry>>())
|
||||
.build().ok()?;
|
||||
Some(Content(ContentType::new("application", "atom+xml"), feed.to_string()))
|
||||
.entries(
|
||||
Post::get_recents_for_blog(&*conn, &blog, 15)
|
||||
.ok()?
|
||||
.into_iter()
|
||||
.map(|p| super::post_to_atom(p, &*conn))
|
||||
.collect::<Vec<Entry>>(),
|
||||
)
|
||||
.build()
|
||||
.ok()?;
|
||||
Some(Content(
|
||||
ContentType::new("application", "atom+xml"),
|
||||
feed.to_string(),
|
||||
))
|
||||
}
|
||||
|
@ -1,29 +1,21 @@
|
||||
use activitypub::object::Note;
|
||||
use rocket::{
|
||||
request::LenientForm,
|
||||
response::Redirect
|
||||
};
|
||||
use rocket::{request::LenientForm, response::Redirect};
|
||||
use rocket_i18n::I18n;
|
||||
use validator::Validate;
|
||||
use template_utils::Ructe;
|
||||
use validator::Validate;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use plume_common::{utils, activity_pub::{broadcast, ApRequest,
|
||||
ActivityStream, inbox::Deletable}};
|
||||
use plume_models::{
|
||||
blogs::Blog,
|
||||
comments::*,
|
||||
db_conn::DbConn,
|
||||
instance::Instance,
|
||||
mentions::Mention,
|
||||
posts::Post,
|
||||
safe_string::SafeString,
|
||||
tags::Tag,
|
||||
users::User
|
||||
use plume_common::{
|
||||
activity_pub::{broadcast, inbox::Deletable, ActivityStream, ApRequest},
|
||||
utils,
|
||||
};
|
||||
use plume_models::{
|
||||
blogs::Blog, comments::*, db_conn::DbConn, instance::Instance, mentions::Mention, posts::Post,
|
||||
safe_string::SafeString, tags::Tag, users::User,
|
||||
};
|
||||
use Worker;
|
||||
use routes::errors::ErrorPage;
|
||||
use Worker;
|
||||
|
||||
#[derive(Default, FromForm, Debug, Validate)]
|
||||
pub struct NewCommentForm {
|
||||
@ -34,37 +26,54 @@ pub struct NewCommentForm {
|
||||
}
|
||||
|
||||
#[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)
|
||||
-> Result<Redirect, Ructe> {
|
||||
pub fn create(
|
||||
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 post = Post::find_by_slug(&*conn, &slug, blog.id).expect("comments::create: post error");
|
||||
form.validate()
|
||||
.map(|_| {
|
||||
let (html, mentions, _hashtags) = utils::md_to_html(
|
||||
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 {
|
||||
content: SafeString::new(html.as_ref()),
|
||||
in_response_to_id: form.responding_to,
|
||||
post_id: post.id,
|
||||
author_id: user.id,
|
||||
ap_url: None,
|
||||
sensitive: !form.warning.is_empty(),
|
||||
spoiler_text: form.warning.clone(),
|
||||
public_visibility: true
|
||||
}).expect("comments::create: insert error");
|
||||
let new_comment = comm.create_activity(&*conn).expect("comments::create: activity error");
|
||||
let comm = Comment::insert(
|
||||
&*conn,
|
||||
NewComment {
|
||||
content: SafeString::new(html.as_ref()),
|
||||
in_response_to_id: form.responding_to,
|
||||
post_id: post.id,
|
||||
author_id: user.id,
|
||||
ap_url: None,
|
||||
sensitive: !form.warning.is_empty(),
|
||||
spoiler_text: form.warning.clone(),
|
||||
public_visibility: true,
|
||||
},
|
||||
)
|
||||
.expect("comments::create: insert error");
|
||||
let new_comment = comm
|
||||
.create_activity(&*conn)
|
||||
.expect("comments::create: activity error");
|
||||
|
||||
// save mentions
|
||||
for ment in mentions {
|
||||
Mention::from_activity(
|
||||
&*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,
|
||||
true,
|
||||
true
|
||||
).expect("comments::create: mention save error");
|
||||
true,
|
||||
)
|
||||
.expect("comments::create: mention save error");
|
||||
}
|
||||
|
||||
// federate
|
||||
@ -72,13 +81,18 @@ pub fn create(blog_name: String, slug: String, form: LenientForm<NewCommentForm>
|
||||
let user_clone = user.clone();
|
||||
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| {
|
||||
// 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(
|
||||
&(&*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"),
|
||||
comments,
|
||||
previous,
|
||||
post.count_likes(&*conn).expect("comments::create: count likes error"),
|
||||
post.count_reshares(&*conn).expect("comments::create: count reshares error"),
|
||||
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"),
|
||||
post.get_authors(&*conn).expect("comments::create: authors error")[0].clone()
|
||||
post.count_likes(&*conn)
|
||||
.expect("comments::create: count likes error"),
|
||||
post.count_reshares(&*conn)
|
||||
.expect("comments::create: count reshares error"),
|
||||
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"),
|
||||
post.get_authors(&*conn)
|
||||
.expect("comments::create: authors error")[0]
|
||||
.clone()
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
#[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 comment.author_id == user.id {
|
||||
let dest = User::one_by_instance(&*conn)?;
|
||||
let delete_activity = comment.delete(&*conn)?;
|
||||
let user_c = user.clone();
|
||||
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>")]
|
||||
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)
|
||||
.and_then(|c| c.to_activity(&*conn))
|
||||
.ok()
|
||||
|
@ -1,11 +1,11 @@
|
||||
use plume_models::users::User;
|
||||
use plume_models::{db_conn::DbConn, Error};
|
||||
use rocket::{
|
||||
Request,
|
||||
request::FromRequest,
|
||||
response::{self, Responder},
|
||||
Request,
|
||||
};
|
||||
use rocket_i18n::I18n;
|
||||
use plume_models::{Error, db_conn::DbConn};
|
||||
use plume_models::users::User;
|
||||
use template_utils::Ructe;
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -24,15 +24,24 @@ impl<'r> Responder<'r> for ErrorPage {
|
||||
let user = User::from_request(req).succeeded();
|
||||
|
||||
match self.0 {
|
||||
Error::NotFound => render!(errors::not_found(
|
||||
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
|
||||
)).respond_to(req),
|
||||
Error::Unauthorized => render!(errors::not_found(
|
||||
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
|
||||
)).respond_to(req),
|
||||
_ => render!(errors::not_found(
|
||||
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
|
||||
)).respond_to(req)
|
||||
Error::NotFound => render!(errors::not_found(&(
|
||||
&*conn.unwrap(),
|
||||
&intl.unwrap().catalog,
|
||||
user
|
||||
)))
|
||||
.respond_to(req),
|
||||
Error::Unauthorized => render!(errors::not_found(&(
|
||||
&*conn.unwrap(),
|
||||
&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 intl = req.guard::<I18n>().succeeded();
|
||||
let user = User::from_request(req).succeeded();
|
||||
render!(errors::not_found(
|
||||
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
|
||||
))
|
||||
render!(errors::not_found(&(
|
||||
&*conn.unwrap(),
|
||||
&intl.unwrap().catalog,
|
||||
user
|
||||
)))
|
||||
}
|
||||
|
||||
#[catch(422)]
|
||||
@ -52,9 +63,11 @@ pub fn unprocessable_entity(req: &Request) -> Ructe {
|
||||
let conn = req.guard::<DbConn>().succeeded();
|
||||
let intl = req.guard::<I18n>().succeeded();
|
||||
let user = User::from_request(req).succeeded();
|
||||
render!(errors::unprocessable_entity(
|
||||
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
|
||||
))
|
||||
render!(errors::unprocessable_entity(&(
|
||||
&*conn.unwrap(),
|
||||
&intl.unwrap().catalog,
|
||||
user
|
||||
)))
|
||||
}
|
||||
|
||||
#[catch(500)]
|
||||
@ -62,17 +75,22 @@ pub fn server_error(req: &Request) -> Ructe {
|
||||
let conn = req.guard::<DbConn>().succeeded();
|
||||
let intl = req.guard::<I18n>().succeeded();
|
||||
let user = User::from_request(req).succeeded();
|
||||
render!(errors::server_error(
|
||||
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
|
||||
))
|
||||
render!(errors::server_error(&(
|
||||
&*conn.unwrap(),
|
||||
&intl.unwrap().catalog,
|
||||
user
|
||||
)))
|
||||
}
|
||||
|
||||
#[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 {
|
||||
eprintln!("Csrf violation while acceding \"{}\"", uri)
|
||||
}
|
||||
render!(errors::csrf(
|
||||
&(&*conn, &intl.catalog, user)
|
||||
))
|
||||
render!(errors::csrf(&(&*conn, &intl.catalog, user)))
|
||||
}
|
||||
|
@ -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_i18n::I18n;
|
||||
use serde_json;
|
||||
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 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 Searcher;
|
||||
|
||||
@ -46,7 +41,12 @@ pub fn index(conn: DbConn, user: Option<User>, intl: I18n) -> Result<Ructe, Erro
|
||||
}
|
||||
|
||||
#[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 instance = Instance::get_local(&*conn)?;
|
||||
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>")]
|
||||
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 articles = Post::get_recents_page(&*conn, page.limits())?;
|
||||
Ok(render!(instance::federated(
|
||||
@ -111,23 +116,34 @@ pub struct InstanceSettingsForm {
|
||||
pub short_description: SafeString,
|
||||
pub long_description: SafeString,
|
||||
#[validate(length(min = "1"))]
|
||||
pub default_license: String
|
||||
pub default_license: String,
|
||||
}
|
||||
|
||||
#[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()
|
||||
.and_then(|_| {
|
||||
let instance = Instance::get_local(&*conn).expect("instance::update_settings: local instance error");
|
||||
instance.update(&*conn,
|
||||
form.name.clone(),
|
||||
form.open_registrations,
|
||||
form.short_description.clone(),
|
||||
form.long_description.clone()).expect("instance::update_settings: save error");
|
||||
let instance = Instance::get_local(&*conn)
|
||||
.expect("instance::update_settings: local instance error");
|
||||
instance
|
||||
.update(
|
||||
&*conn,
|
||||
form.name.clone(),
|
||||
form.open_registrations,
|
||||
form.short_description.clone(),
|
||||
form.long_description.clone(),
|
||||
)
|
||||
.expect("instance::update_settings: save error");
|
||||
Ok(Redirect::to(uri!(admin)))
|
||||
})
|
||||
.or_else(|e| {
|
||||
let local_inst = Instance::get_local(&*conn).expect("instance::update_settings: local instance error");
|
||||
.or_else(|e| {
|
||||
let local_inst = Instance::get_local(&*conn)
|
||||
.expect("instance::update_settings: local instance error");
|
||||
Err(render!(instance::admin(
|
||||
&(&*conn, &intl.catalog, Some(admin.0)),
|
||||
local_inst,
|
||||
@ -138,7 +154,12 @@ pub fn update_settings(conn: DbConn, admin: Admin, form: LenientForm<InstanceSet
|
||||
}
|
||||
|
||||
#[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 instances = Instance::page(&*conn, page.limits())?;
|
||||
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>")]
|
||||
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();
|
||||
Ok(render!(instance::users(
|
||||
&(&*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")]
|
||||
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) {
|
||||
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>")]
|
||||
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 sig = data.0;
|
||||
|
||||
let activity = act.clone();
|
||||
let actor_id = activity["actor"].as_str()
|
||||
.or_else(|| activity["actor"]["id"].as_str()).ok_or(status::BadRequest(Some("Missing actor id for activity")))?;
|
||||
let actor_id = activity["actor"]
|
||||
.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");
|
||||
if !verify_http_headers(&actor, &headers.0, &sig).is_secure() &&
|
||||
!act.clone().verify(&actor) {
|
||||
if !verify_http_headers(&actor, &headers.0, &sig).is_secure() && !act.clone().verify(&actor) {
|
||||
// maybe we just know an old key?
|
||||
actor.refetch(&conn).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(())
|
||||
} else {
|
||||
Err(Error::Signature)
|
||||
})
|
||||
actor
|
||||
.refetch(&conn)
|
||||
.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(())
|
||||
} else {
|
||||
Err(Error::Signature)
|
||||
}
|
||||
})
|
||||
.map_err(|_| {
|
||||
println!("Rejected invalid activity supposedly from {}, with headers {:?}", actor.username, headers.0);
|
||||
status::BadRequest(Some("Invalid signature"))})?;
|
||||
println!(
|
||||
"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());
|
||||
}
|
||||
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(_) => String::new(),
|
||||
Err(e) => {
|
||||
|
@ -1,25 +1,28 @@
|
||||
use rocket::response::{Redirect, Flash};
|
||||
use rocket::response::{Flash, Redirect};
|
||||
use rocket_i18n::I18n;
|
||||
|
||||
use plume_common::activity_pub::{broadcast, inbox::{Notify, Deletable}};
|
||||
use plume_common::utils;
|
||||
use plume_models::{
|
||||
blogs::Blog,
|
||||
db_conn::DbConn,
|
||||
likes,
|
||||
posts::Post,
|
||||
users::User
|
||||
use plume_common::activity_pub::{
|
||||
broadcast,
|
||||
inbox::{Deletable, Notify},
|
||||
};
|
||||
use Worker;
|
||||
use plume_common::utils;
|
||||
use plume_models::{blogs::Blog, db_conn::DbConn, likes, posts::Post, users::User};
|
||||
use routes::errors::ErrorPage;
|
||||
use Worker;
|
||||
|
||||
#[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 post = Post::find_by_slug(&*conn, &slug, b.id)?;
|
||||
|
||||
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)?;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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)]
|
||||
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(
|
||||
&i18n!(i18n.catalog, "You need to be logged in order to like a post"),
|
||||
uri!(create: blog = blog, slug = slug)
|
||||
&i18n!(
|
||||
i18n.catalog,
|
||||
"You need to be logged in order to like a post"
|
||||
),
|
||||
uri!(create: blog = blog, slug = slug),
|
||||
)
|
||||
}
|
||||
|
@ -1,11 +1,18 @@
|
||||
use guid_create::GUID;
|
||||
use multipart::server::{Multipart, save::{SavedData, SaveResult}};
|
||||
use rocket::{Data, http::ContentType, response::{Redirect, status}};
|
||||
use multipart::server::{
|
||||
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 routes::{errors::ErrorPage, Page};
|
||||
use std::fs;
|
||||
use plume_models::{Error, db_conn::DbConn, medias::*, users::User};
|
||||
use template_utils::Ructe;
|
||||
use routes::{Page, errors::ErrorPage};
|
||||
|
||||
#[get("/medias?<page>")]
|
||||
pub fn list(user: User, conn: DbConn, intl: I18n, page: Option<Page>) -> Result<Ructe, ErrorPage> {
|
||||
@ -21,64 +28,85 @@ pub fn list(user: User, conn: DbConn, intl: I18n, page: Option<Page>) -> Result<
|
||||
|
||||
#[get("/medias/new")]
|
||||
pub fn new(user: User, conn: DbConn, intl: I18n) -> Ructe {
|
||||
render!(medias::new(
|
||||
&(&*conn, &intl.catalog, Some(user))
|
||||
))
|
||||
render!(medias::new(&(&*conn, &intl.catalog, Some(user))))
|
||||
}
|
||||
|
||||
#[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() {
|
||||
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() {
|
||||
SaveResult::Full(entries) => {
|
||||
let fields = entries.fields;
|
||||
|
||||
let filename = fields.get("file").and_then(|v| v.iter().next())
|
||||
.ok_or_else(|| status::BadRequest(Some("No file uploaded")))?.headers
|
||||
.filename.clone();
|
||||
let filename = fields
|
||||
.get("file")
|
||||
.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
|
||||
let ext = filename
|
||||
.and_then(|f| f
|
||||
.rsplit('.')
|
||||
.next()
|
||||
.and_then(|ext| if ext.chars().any(|c| !c.is_alphanumeric()) {
|
||||
None
|
||||
} else {
|
||||
Some(ext.to_lowercase())
|
||||
})
|
||||
.map(|ext| format!(".{}", ext))
|
||||
).unwrap_or_default();
|
||||
.and_then(|f| {
|
||||
f.rsplit('.')
|
||||
.next()
|
||||
.and_then(|ext| {
|
||||
if ext.chars().any(|c| !c.is_alphanumeric()) {
|
||||
None
|
||||
} else {
|
||||
Some(ext.to_lowercase())
|
||||
}
|
||||
})
|
||||
.map(|ext| format!(".{}", ext))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let dest = format!("static/media/{}{}", GUID::rand().to_string(), ext);
|
||||
|
||||
match fields["file"][0].data {
|
||||
SavedData::Bytes(ref bytes) => fs::write(&dest, bytes).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")))?;},
|
||||
SavedData::Bytes(ref bytes) => fs::write(&dest, bytes)
|
||||
.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)));
|
||||
}
|
||||
}
|
||||
|
||||
let has_cw = !read(&fields["cw"][0].data).map(|cw| cw.is_empty()).unwrap_or(false);
|
||||
let media = Media::insert(&*conn, NewMedia {
|
||||
file_path: dest,
|
||||
alt_text: read(&fields["alt"][0].data)?,
|
||||
is_remote: false,
|
||||
remote_url: None,
|
||||
sensitive: has_cw,
|
||||
content_warning: if has_cw {
|
||||
Some(read(&fields["cw"][0].data)?)
|
||||
} else {
|
||||
None
|
||||
let has_cw = !read(&fields["cw"][0].data)
|
||||
.map(|cw| cw.is_empty())
|
||||
.unwrap_or(false);
|
||||
let media = Media::insert(
|
||||
&*conn,
|
||||
NewMedia {
|
||||
file_path: dest,
|
||||
alt_text: read(&fields["alt"][0].data)?,
|
||||
is_remote: false,
|
||||
remote_url: None,
|
||||
sensitive: has_cw,
|
||||
content_warning: if has_cw {
|
||||
Some(read(&fields["cw"][0].data)?)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
owner_id: user.id,
|
||||
},
|
||||
owner_id: user.id
|
||||
}).map_err(|_| status::BadRequest(Some("Error while saving media")))?;
|
||||
)
|
||||
.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)))
|
||||
}
|
||||
SaveResult::Partial(_, _) | SaveResult::Error(_) => Ok(Redirect::to(uri!(new))),
|
||||
}
|
||||
} else {
|
||||
Ok(Redirect::to(uri!(new)))
|
||||
|
@ -2,25 +2,21 @@
|
||||
use atom_syndication::{ContentBuilder, Entry, EntryBuilder, LinkBuilder, Person, PersonBuilder};
|
||||
use rocket::{
|
||||
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},
|
||||
response::NamedFile,
|
||||
Outcome,
|
||||
};
|
||||
use rocket_i18n::I18n;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use plume_models::{
|
||||
Connection,
|
||||
users::User,
|
||||
posts::Post,
|
||||
db_conn::DbConn,
|
||||
};
|
||||
use plume_models::{db_conn::DbConn, posts::Post, users::User, Connection};
|
||||
|
||||
use Worker;
|
||||
use Searcher;
|
||||
use Worker;
|
||||
|
||||
pub struct PlumeRocket<'a> {
|
||||
conn: DbConn,
|
||||
@ -100,7 +96,6 @@ impl<'a, 'r> FromRequest<'a, 'r> for ContentLen {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Default for Page {
|
||||
fn default() -> Self {
|
||||
Page(1)
|
||||
@ -110,20 +105,33 @@ impl Default for Page {
|
||||
pub fn post_to_atom(post: Post, conn: &Connection) -> Entry {
|
||||
EntryBuilder::default()
|
||||
.title(format!("<![CDATA[{}]]>", post.title))
|
||||
.content(ContentBuilder::default()
|
||||
.value(format!("<![CDATA[{}]]>", *post.content.get()))
|
||||
.src(post.ap_url.clone())
|
||||
.content_type("html".to_string())
|
||||
.build().expect("Atom feed: content error"))
|
||||
.authors(post.get_authors(&*conn).expect("Atom feed: author error")
|
||||
.into_iter()
|
||||
.map(|a| PersonBuilder::default()
|
||||
.name(a.display_name)
|
||||
.uri(a.ap_url)
|
||||
.build().expect("Atom feed: author error"))
|
||||
.collect::<Vec<Person>>())
|
||||
.links(vec![LinkBuilder::default().href(post.ap_url).build().expect("Atom feed: link error")])
|
||||
.build().expect("Atom feed: entry error")
|
||||
.content(
|
||||
ContentBuilder::default()
|
||||
.value(format!("<![CDATA[{}]]>", *post.content.get()))
|
||||
.src(post.ap_url.clone())
|
||||
.content_type("html".to_string())
|
||||
.build()
|
||||
.expect("Atom feed: content error"),
|
||||
)
|
||||
.authors(
|
||||
post.get_authors(&*conn)
|
||||
.expect("Atom feed: author error")
|
||||
.into_iter()
|
||||
.map(|a| {
|
||||
PersonBuilder::default()
|
||||
.name(a.display_name)
|
||||
.uri(a.ap_url)
|
||||
.build()
|
||||
.expect("Atom feed: author 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;
|
||||
@ -135,17 +143,17 @@ pub mod medias;
|
||||
pub mod notifications;
|
||||
pub mod posts;
|
||||
pub mod reshares;
|
||||
pub mod search;
|
||||
pub mod session;
|
||||
pub mod tags;
|
||||
pub mod user;
|
||||
pub mod search;
|
||||
pub mod well_known;
|
||||
|
||||
#[derive(Responder)]
|
||||
#[response()]
|
||||
pub struct CachedFile {
|
||||
inner: NamedFile,
|
||||
cache_control: CacheControl
|
||||
cache_control: CacheControl,
|
||||
}
|
||||
|
||||
#[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)]
|
||||
pub fn static_files(file: PathBuf) -> Option<CachedFile> {
|
||||
NamedFile::open(Path::new("static/").join(file)).ok()
|
||||
.map(|f|
|
||||
CachedFile {
|
||||
inner: f,
|
||||
cache_control: CacheControl(vec![CacheDirective::MaxAge(60*60*24*30)])
|
||||
})
|
||||
NamedFile::open(Path::new("static/").join(file))
|
||||
.ok()
|
||||
.map(|f| CachedFile {
|
||||
inner: f,
|
||||
cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]),
|
||||
})
|
||||
}
|
||||
|
@ -1,13 +1,18 @@
|
||||
use rocket::response::{Redirect, Flash};
|
||||
use rocket::response::{Flash, Redirect};
|
||||
use rocket_i18n::I18n;
|
||||
|
||||
use plume_common::utils;
|
||||
use plume_models::{db_conn::DbConn, notifications::Notification, users::User};
|
||||
use routes::{Page, errors::ErrorPage};
|
||||
use routes::{errors::ErrorPage, Page};
|
||||
use template_utils::Ructe;
|
||||
|
||||
#[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();
|
||||
Ok(render!(notifications::index(
|
||||
&(&*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)]
|
||||
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(
|
||||
&i18n!(i18n.catalog, "You need to be logged in order to see your notifications"),
|
||||
uri!(notifications: page = page)
|
||||
&i18n!(
|
||||
i18n.catalog,
|
||||
"You need to be logged in order to see your notifications"
|
||||
),
|
||||
uri!(notifications: page = page),
|
||||
)
|
||||
}
|
||||
|
@ -1,20 +1,21 @@
|
||||
use chrono::Utc;
|
||||
use heck::{CamelCase, KebabCase};
|
||||
use rocket::request::LenientForm;
|
||||
use rocket::response::{Redirect, Flash};
|
||||
use rocket::response::{Flash, Redirect};
|
||||
use rocket_i18n::I18n;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{HashMap, HashSet},
|
||||
borrow::Cow, time::Duration,
|
||||
time::Duration,
|
||||
};
|
||||
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_models::{
|
||||
blogs::*,
|
||||
db_conn::DbConn,
|
||||
comments::{Comment, CommentTree},
|
||||
db_conn::DbConn,
|
||||
instance::Instance,
|
||||
medias::Media,
|
||||
mentions::Mention,
|
||||
@ -22,16 +23,28 @@ use plume_models::{
|
||||
posts::*,
|
||||
safe_string::SafeString,
|
||||
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;
|
||||
|
||||
#[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 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 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)]
|
||||
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 post = Post::find_by_slug(&*conn, &slug, blog.id).map_err(|_| None)?;
|
||||
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 {
|
||||
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)]
|
||||
pub fn new_auth(blog: String, i18n: I18n) -> Flash<Redirect> {
|
||||
utils::requires_login(
|
||||
&i18n!(i18n.catalog, "You need to be logged in order to write a new post"),
|
||||
uri!(new: blog = blog)
|
||||
&i18n!(
|
||||
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(
|
||||
&(&*conn, &intl.catalog, Some(user)),
|
||||
i18n!(intl.catalog, "You are not author in this blog.")
|
||||
)))
|
||||
)));
|
||||
}
|
||||
|
||||
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")]
|
||||
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 intl = rockets.intl;
|
||||
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(
|
||||
&(&*conn, &intl.catalog, Some(user)),
|
||||
i18n!(intl.catalog, "You are not author in this blog.")
|
||||
)))
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
let source = if !post.source.is_empty() {
|
||||
post.source.clone()
|
||||
} else {
|
||||
@ -168,7 +196,7 @@ pub fn edit(blog: String, slug: String, cl: ContentLen, rockets: PlumeRocket) ->
|
||||
content: source,
|
||||
tags: Tag::for_post(&*conn, post.id)?
|
||||
.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>>()
|
||||
.join(", "),
|
||||
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>")]
|
||||
pub fn update(blog: String, slug: String, cl: ContentLen, form: LenientForm<NewPostForm>, rockets: PlumeRocket)
|
||||
-> Result<Redirect, Ructe> {
|
||||
pub fn update(
|
||||
blog: String,
|
||||
slug: String,
|
||||
cl: ContentLen,
|
||||
form: LenientForm<NewPostForm>,
|
||||
rockets: PlumeRocket,
|
||||
) -> Result<Redirect, Ructe> {
|
||||
let conn = rockets.conn;
|
||||
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 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() {
|
||||
Ok(_) => ValidationErrors::new(),
|
||||
Err(e) => e
|
||||
Err(e) => e,
|
||||
};
|
||||
|
||||
if new_slug != slug && Post::find_by_slug(&*conn, &new_slug, b.id).is_ok() {
|
||||
errors.add("title", ValidationError {
|
||||
code: Cow::from("existing_slug"),
|
||||
message: Some(Cow::from("A post with the same title already exists.")),
|
||||
params: HashMap::new()
|
||||
});
|
||||
errors.add(
|
||||
"title",
|
||||
ValidationError {
|
||||
code: Cow::from("existing_slug"),
|
||||
message: Some(Cow::from("A post with the same title already exists.")),
|
||||
params: HashMap::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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"…
|
||||
Ok(Redirect::to(uri!(super::blogs::details: name = blog, page = _)))
|
||||
Ok(Redirect::to(
|
||||
uri!(super::blogs::details: name = blog, page = _),
|
||||
))
|
||||
} 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
|
||||
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.license = form.license.clone();
|
||||
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 {
|
||||
post.update_mentions(&conn, mentions.into_iter().filter_map(|m| Mention::build_activity(&conn, &m).ok()).collect())
|
||||
.expect("post::update: mentions error");;
|
||||
post.update_mentions(
|
||||
&conn,
|
||||
mentions
|
||||
.into_iter()
|
||||
.filter_map(|m| Mention::build_activity(&conn, &m).ok())
|
||||
.collect(),
|
||||
)
|
||||
.expect("post::update: mentions error");;
|
||||
}
|
||||
|
||||
let tags = form.tags.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 tags = form
|
||||
.tags
|
||||
.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<_>>()
|
||||
.into_iter().filter_map(|t| Tag::build_activity(&conn, t).ok()).collect::<Vec<_>>();
|
||||
post.update_hashtags(&conn, hashtags).expect("post::update: hashtags error");
|
||||
let hashtags = hashtags
|
||||
.into_iter()
|
||||
.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 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");
|
||||
worker.execute(move || broadcast(&user, act, dest));
|
||||
} 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");
|
||||
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 {
|
||||
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>")]
|
||||
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 blog = Blog::find_by_fqn(&*conn, &blog_name).expect("post::create: blog error");;
|
||||
let slug = form.title.to_string().to_kebab_case();
|
||||
@ -314,86 +393,119 @@ pub fn create(blog_name: String, form: LenientForm<NewPostForm>, cl: ContentLen,
|
||||
|
||||
let mut errors = match form.validate() {
|
||||
Ok(_) => ValidationErrors::new(),
|
||||
Err(e) => e
|
||||
Err(e) => e,
|
||||
};
|
||||
if Post::find_by_slug(&*conn, &slug, blog.id).is_ok() {
|
||||
errors.add("title", ValidationError {
|
||||
code: Cow::from("existing_slug"),
|
||||
message: Some(Cow::from("A post with the same title already exists.")),
|
||||
params: HashMap::new()
|
||||
});
|
||||
errors.add(
|
||||
"title",
|
||||
ValidationError {
|
||||
code: Cow::from("existing_slug"),
|
||||
message: Some(Cow::from("A post with the same title already exists.")),
|
||||
params: HashMap::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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"…
|
||||
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(
|
||||
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 post = Post::insert(&*conn, NewPost {
|
||||
blog_id: blog.id,
|
||||
slug: slug.to_string(),
|
||||
title: form.title.to_string(),
|
||||
content: SafeString::new(&content),
|
||||
published: !form.draft,
|
||||
license: form.license.clone(),
|
||||
ap_url: "".to_string(),
|
||||
creation_date: None,
|
||||
subtitle: form.subtitle.clone(),
|
||||
source: form.content.clone(),
|
||||
cover_id: form.cover,
|
||||
let post = Post::insert(
|
||||
&*conn,
|
||||
NewPost {
|
||||
blog_id: blog.id,
|
||||
slug: slug.to_string(),
|
||||
title: form.title.to_string(),
|
||||
content: SafeString::new(&content),
|
||||
published: !form.draft,
|
||||
license: form.license.clone(),
|
||||
ap_url: "".to_string(),
|
||||
creation_date: None,
|
||||
subtitle: form.subtitle.clone(),
|
||||
source: form.content.clone(),
|
||||
cover_id: form.cover,
|
||||
},
|
||||
&searcher,
|
||||
).expect("post::create: post save error");
|
||||
)
|
||||
.expect("post::create: post save error");
|
||||
|
||||
PostAuthor::insert(&*conn, NewPostAuthor {
|
||||
post_id: post.id,
|
||||
author_id: user.id
|
||||
}).expect("post::create: author save error");
|
||||
PostAuthor::insert(
|
||||
&*conn,
|
||||
NewPostAuthor {
|
||||
post_id: post.id,
|
||||
author_id: user.id,
|
||||
},
|
||||
)
|
||||
.expect("post::create: author save error");
|
||||
|
||||
let tags = form.tags.split(',')
|
||||
let tags = form
|
||||
.tags
|
||||
.split(',')
|
||||
.map(|t| t.trim().to_camel_case())
|
||||
.filter(|t| !t.is_empty())
|
||||
.collect::<HashSet<_>>();
|
||||
for tag in tags {
|
||||
Tag::insert(&*conn, NewTag {
|
||||
tag,
|
||||
is_hashtag: false,
|
||||
post_id: post.id
|
||||
}).expect("post::create: tags save error");
|
||||
Tag::insert(
|
||||
&*conn,
|
||||
NewTag {
|
||||
tag,
|
||||
is_hashtag: false,
|
||||
post_id: post.id,
|
||||
},
|
||||
)
|
||||
.expect("post::create: tags save error");
|
||||
}
|
||||
for hashtag in hashtags {
|
||||
Tag::insert(&*conn, NewTag {
|
||||
tag: hashtag.to_camel_case(),
|
||||
is_hashtag: true,
|
||||
post_id: post.id
|
||||
}).expect("post::create: hashtags save error");
|
||||
Tag::insert(
|
||||
&*conn,
|
||||
NewTag {
|
||||
tag: hashtag.to_camel_case(),
|
||||
is_hashtag: true,
|
||||
post_id: post.id,
|
||||
},
|
||||
)
|
||||
.expect("post::create: hashtags save error");
|
||||
}
|
||||
|
||||
if post.published {
|
||||
for m in mentions {
|
||||
Mention::from_activity(
|
||||
&*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,
|
||||
true,
|
||||
true
|
||||
).expect("post::create: mention save error");
|
||||
true,
|
||||
)
|
||||
.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 worker = rockets.worker;
|
||||
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 {
|
||||
let medias = Media::for_user(&*conn, user.id).expect("posts::create: medias error");
|
||||
let intl = rockets.intl;
|
||||
@ -413,15 +525,25 @@ pub fn create(blog_name: String, form: LenientForm<NewPostForm>, cl: ContentLen,
|
||||
}
|
||||
|
||||
#[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 user = rockets.user.unwrap();
|
||||
let post = Blog::find_by_fqn(&*conn, &blog_name)
|
||||
.and_then(|blog| Post::find_by_slug(&*conn, &slug, blog.id));
|
||||
|
||||
if let Ok(post) = post {
|
||||
if !post.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 = _)))
|
||||
if !post
|
||||
.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;
|
||||
@ -432,10 +554,17 @@ pub fn delete(blog_name: String, slug: String, rockets: PlumeRocket) -> Result<R
|
||||
let user_c = user.clone();
|
||||
|
||||
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 {
|
||||
Ok(Redirect::to(uri!(super::blogs::details: name = blog_name, page = _)))
|
||||
Ok(Redirect::to(
|
||||
uri!(super::blogs::details: name = blog_name, page = _),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,23 @@
|
||||
use rocket::response::{Redirect, Flash};
|
||||
use rocket::response::{Flash, Redirect};
|
||||
use rocket_i18n::I18n;
|
||||
|
||||
use plume_common::activity_pub::{broadcast, inbox::{Deletable, Notify}};
|
||||
use plume_common::utils;
|
||||
use plume_models::{
|
||||
blogs::Blog,
|
||||
db_conn::DbConn,
|
||||
posts::Post,
|
||||
reshares::*,
|
||||
users::User
|
||||
use plume_common::activity_pub::{
|
||||
broadcast,
|
||||
inbox::{Deletable, Notify},
|
||||
};
|
||||
use plume_common::utils;
|
||||
use plume_models::{blogs::Blog, db_conn::DbConn, posts::Post, reshares::*, users::User};
|
||||
use routes::errors::ErrorPage;
|
||||
use Worker;
|
||||
|
||||
#[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 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));
|
||||
}
|
||||
|
||||
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> {
|
||||
utils::requires_login(
|
||||
&i18n!(i18n.catalog, "You need to be logged in order to reshare a post"),
|
||||
uri!(create: blog = blog, slug = slug)
|
||||
&i18n!(
|
||||
i18n.catalog,
|
||||
"You need to be logged in order to reshare a post"
|
||||
),
|
||||
uri!(create: blog = blog, slug = slug),
|
||||
)
|
||||
}
|
||||
|
@ -2,13 +2,11 @@ use chrono::offset::Utc;
|
||||
use rocket::request::Form;
|
||||
use rocket_i18n::I18n;
|
||||
|
||||
use plume_models::{
|
||||
db_conn::DbConn, users::User,
|
||||
search::Query};
|
||||
use plume_models::{db_conn::DbConn, search::Query, users::User};
|
||||
use routes::Page;
|
||||
use std::str::FromStr;
|
||||
use template_utils::Ructe;
|
||||
use Searcher;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Default, FromForm)]
|
||||
pub struct SearchQuery {
|
||||
@ -53,12 +51,19 @@ macro_rules! param_to_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 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,
|
||||
instance, author, blog, lang, license;
|
||||
|
@ -1,22 +1,26 @@
|
||||
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_i18n::I18n;
|
||||
use std::{borrow::Cow, sync::{Arc, Mutex}, time::Instant};
|
||||
use validator::{Validate, ValidationError, ValidationErrors};
|
||||
use template_utils::Ructe;
|
||||
|
||||
use plume_models::{
|
||||
BASE_URL, Error,
|
||||
db_conn::DbConn,
|
||||
users::{User, AUTH_COOKIE}
|
||||
use rocket::{
|
||||
http::{uri::Uri, Cookie, Cookies, SameSite},
|
||||
request::{FlashMessage, Form, LenientForm},
|
||||
response::Redirect,
|
||||
State,
|
||||
};
|
||||
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 plume_models::{
|
||||
db_conn::DbConn,
|
||||
users::{User, AUTH_COOKIE},
|
||||
Error, BASE_URL,
|
||||
};
|
||||
use routes::errors::ErrorPage;
|
||||
|
||||
#[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"))]
|
||||
pub email_or_name: String,
|
||||
#[validate(length(min = "1", message = "Your password can't be empty"))]
|
||||
pub password: String
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[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)
|
||||
.or_else(|_| User::find_by_fqn(&*conn, &form.email_or_name));
|
||||
let mut errors = match form.validate() {
|
||||
Ok(_) => ValidationErrors::new(),
|
||||
Err(e) => e
|
||||
Err(e) => e,
|
||||
};
|
||||
|
||||
let user_id = if let Ok(user) = user {
|
||||
@ -58,7 +68,9 @@ pub fn create(conn: DbConn, form: LenientForm<LoginForm>, flash: Option<FlashMes
|
||||
} else {
|
||||
// Fake password verification, only to avoid different login times
|
||||
// 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");
|
||||
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() {
|
||||
cookies.add_private(Cookie::build(AUTH_COOKIE, user_id)
|
||||
.same_site(SameSite::Lax)
|
||||
.finish());
|
||||
cookies.add_private(
|
||||
Cookie::build(AUTH_COOKIE, user_id)
|
||||
.same_site(SameSite::Lax)
|
||||
.finish(),
|
||||
);
|
||||
let destination = flash
|
||||
.and_then(|f| if f.name() == "callback" {
|
||||
Some(f.msg().to_owned())
|
||||
} else {
|
||||
None
|
||||
.and_then(|f| {
|
||||
if f.name() == "callback" {
|
||||
Some(f.msg().to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| "/".to_owned());
|
||||
|
||||
let uri = Uri::parse(&destination)
|
||||
.map(|x| x.into_owned())
|
||||
.map_err(|_| render!(session::login(
|
||||
&(&*conn, &intl.catalog, None),
|
||||
None,
|
||||
&*form,
|
||||
errors
|
||||
)))?;
|
||||
.map_err(|_| {
|
||||
render!(session::login(
|
||||
&(&*conn, &intl.catalog, None),
|
||||
None,
|
||||
&*form,
|
||||
errors
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(Redirect::to(uri))
|
||||
} else {
|
||||
@ -140,13 +158,15 @@ pub fn password_reset_request(
|
||||
intl: I18n,
|
||||
mail: State<Arc<Mutex<Mailer>>>,
|
||||
form: Form<ResetForm>,
|
||||
requests: State<Arc<Mutex<Vec<ResetRequest>>>>
|
||||
requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
|
||||
) -> Ructe {
|
||||
let mut requests = requests.lock().unwrap();
|
||||
// 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);
|
||||
|
||||
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();
|
||||
|
||||
requests.push(ResetRequest {
|
||||
@ -159,22 +179,35 @@ pub fn password_reset_request(
|
||||
if let Some(message) = build_mail(
|
||||
form.email.clone(),
|
||||
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() {
|
||||
mail
|
||||
.send(message.into())
|
||||
.map_err(|_| eprintln!("Couldn't send password reset mail")).ok(); }
|
||||
mail.send(message.into())
|
||||
.map_err(|_| eprintln!("Couldn't send password reset mail"))
|
||||
.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>")]
|
||||
pub fn password_reset_form(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)?;
|
||||
pub fn password_reset_form(
|
||||
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(
|
||||
&(&*conn, &intl.catalog, None),
|
||||
&NewPasswordForm::default(),
|
||||
@ -183,13 +216,11 @@ pub fn password_reset_form(conn: DbConn, intl: I18n, token: String, requests: St
|
||||
}
|
||||
|
||||
#[derive(FromForm, Default, Validate)]
|
||||
#[validate(
|
||||
schema(
|
||||
function = "passwords_match",
|
||||
skip_on_field_errors = "false",
|
||||
message = "Passwords are not matching"
|
||||
)
|
||||
)]
|
||||
#[validate(schema(
|
||||
function = "passwords_match",
|
||||
skip_on_field_errors = "false",
|
||||
message = "Passwords are not matching"
|
||||
))]
|
||||
pub struct NewPasswordForm {
|
||||
pub password: String,
|
||||
pub password_confirmation: String,
|
||||
@ -209,19 +240,28 @@ pub fn password_reset(
|
||||
intl: I18n,
|
||||
token: String,
|
||||
requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
|
||||
form: Form<NewPasswordForm>
|
||||
form: Form<NewPasswordForm>,
|
||||
) -> Result<Redirect, Ructe> {
|
||||
form.validate()
|
||||
.and_then(|_| {
|
||||
let mut requests = requests.lock().unwrap();
|
||||
let req = requests.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
|
||||
let req = requests
|
||||
.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);
|
||||
let user = User::find_by_email(&*conn, &req.mail).map_err(to_validation)?;
|
||||
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 {
|
||||
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| {
|
||||
@ -235,10 +275,13 @@ pub fn password_reset(
|
||||
|
||||
fn to_validation<T>(_: T) -> ValidationErrors {
|
||||
let mut errors = ValidationErrors::new();
|
||||
errors.add("", ValidationError {
|
||||
code: Cow::from("server_error"),
|
||||
message: Some(Cow::from("An unknown error occured")),
|
||||
params: std::collections::HashMap::new()
|
||||
});
|
||||
errors.add(
|
||||
"",
|
||||
ValidationError {
|
||||
code: Cow::from("server_error"),
|
||||
message: Some(Cow::from("An unknown error occured")),
|
||||
params: std::collections::HashMap::new(),
|
||||
},
|
||||
);
|
||||
errors
|
||||
}
|
||||
|
@ -1,15 +1,17 @@
|
||||
use rocket_i18n::I18n;
|
||||
|
||||
use plume_models::{
|
||||
db_conn::DbConn,
|
||||
posts::Post,
|
||||
users::User,
|
||||
};
|
||||
use routes::{Page, errors::ErrorPage};
|
||||
use plume_models::{db_conn::DbConn, posts::Post, users::User};
|
||||
use routes::{errors::ErrorPage, Page};
|
||||
use template_utils::Ructe;
|
||||
|
||||
#[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 posts = Post::list_by_tag(&*conn, name.clone(), page.limits())?;
|
||||
Ok(render!(tags::index(
|
||||
|
@ -19,14 +19,20 @@ use plume_common::activity_pub::{
|
||||
};
|
||||
use plume_common::utils;
|
||||
use plume_models::{
|
||||
blogs::Blog,
|
||||
db_conn::DbConn,
|
||||
follows,
|
||||
headers::Headers,
|
||||
instance::Instance,
|
||||
posts::{LicensedArticle, Post},
|
||||
reshares::Reshare,
|
||||
users::*,
|
||||
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 Worker;
|
||||
use Searcher;
|
||||
use Worker;
|
||||
|
||||
#[get("/me")]
|
||||
pub fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
|
||||
@ -56,19 +62,21 @@ pub fn details(
|
||||
let user_clone = user.clone();
|
||||
let searcher = searcher.clone();
|
||||
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>() {
|
||||
Ok(article) => {
|
||||
Post::from_activity(
|
||||
&(&*fetch_articles_conn, &searcher),
|
||||
article,
|
||||
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");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error while fetching articles in background: {:?}", e)
|
||||
}
|
||||
Err(e) => println!("Error while fetching articles in background: {:?}", e),
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -76,8 +84,12 @@ pub fn details(
|
||||
// Fetch followers
|
||||
let user_clone = user.clone();
|
||||
worker.execute(move || {
|
||||
for user_id in user_clone.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");
|
||||
for user_id in user_clone
|
||||
.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(
|
||||
&*fetch_followers_conn,
|
||||
follows::NewFollow {
|
||||
@ -85,7 +97,8 @@ pub fn details(
|
||||
following_id: user_clone.id,
|
||||
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();
|
||||
if user.needs_update() {
|
||||
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(
|
||||
&(&*conn, &intl.catalog, account.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.get_instance(&*conn)?.public_domain,
|
||||
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)]
|
||||
pub fn dashboard_auth(i18n: I18n) -> Flash<Redirect> {
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
#[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)?;
|
||||
if let Ok(follow) = follows::Follow::find(&*conn, user.id, target.id) {
|
||||
let delete_act = follow.delete(&*conn)?;
|
||||
worker.execute(move || {
|
||||
broadcast(&user, delete_act, vec![target])
|
||||
});
|
||||
worker.execute(move || broadcast(&user, delete_act, vec![target]));
|
||||
} else {
|
||||
let f = follows::Follow::insert(
|
||||
&*conn,
|
||||
@ -157,13 +183,22 @@ pub fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Result<
|
||||
#[post("/@/<name>/follow", rank = 2)]
|
||||
pub fn follow_auth(name: String, i18n: I18n) -> Flash<Redirect> {
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
#[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 user = User::find_by_fqn(&*conn, &name)?;
|
||||
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(
|
||||
&(&*conn, &intl.catalog, account.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.get_instance(&*conn)?.public_domain,
|
||||
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)]
|
||||
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 user = User::find_by_fqn(&*conn, &name)?;
|
||||
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(
|
||||
&(&*conn, &intl.catalog, account.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.get_instance(&*conn)?.public_domain,
|
||||
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)]
|
||||
pub fn edit_auth(name: String, i18n: I18n) -> Flash<Redirect> {
|
||||
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),
|
||||
)
|
||||
}
|
||||
@ -251,18 +299,41 @@ pub struct UpdateUserForm {
|
||||
}
|
||||
|
||||
#[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(
|
||||
&*conn,
|
||||
if !form.display_name.is_empty() { form.display_name.clone() } 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() },
|
||||
if !form.display_name.is_empty() {
|
||||
form.display_name.clone()
|
||||
} 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)))
|
||||
}
|
||||
|
||||
#[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)?;
|
||||
if user.id == account.id {
|
||||
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)]
|
||||
#[validate(
|
||||
schema(
|
||||
function = "passwords_match",
|
||||
skip_on_field_errors = "false",
|
||||
message = "Passwords are not matching"
|
||||
)
|
||||
)]
|
||||
#[validate(schema(
|
||||
function = "passwords_match",
|
||||
skip_on_field_errors = "false",
|
||||
message = "Passwords are not matching"
|
||||
))]
|
||||
pub struct NewUserForm {
|
||||
#[validate(length(min = "1", message = "Username can't be empty"),
|
||||
custom( function = "validate_username", message = "User name is not allowed to contain any of < > & @ ' or \""))]
|
||||
#[validate(
|
||||
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,
|
||||
#[validate(email(message = "Invalid email"))]
|
||||
pub email: String,
|
||||
#[validate(
|
||||
length(
|
||||
min = "8",
|
||||
message = "Password should be at least 8 characters long"
|
||||
)
|
||||
)]
|
||||
#[validate(length(min = "8", message = "Password should be at least 8 characters long"))]
|
||||
pub password: String,
|
||||
#[validate(
|
||||
length(
|
||||
min = "8",
|
||||
message = "Password should be at least 8 characters long"
|
||||
)
|
||||
)]
|
||||
#[validate(length(min = "8", message = "Password should be at least 8 characters long"))]
|
||||
pub password_confirmation: String,
|
||||
}
|
||||
|
||||
@ -325,17 +389,20 @@ pub fn validate_username(username: &str) -> Result<(), ValidationError> {
|
||||
|
||||
fn to_validation(_: Error) -> ValidationErrors {
|
||||
let mut errors = ValidationErrors::new();
|
||||
errors.add("", ValidationError {
|
||||
code: Cow::from("server_error"),
|
||||
message: Some(Cow::from("An unknown error occured")),
|
||||
params: HashMap::new()
|
||||
});
|
||||
errors.add(
|
||||
"",
|
||||
ValidationError {
|
||||
code: Cow::from("server_error"),
|
||||
message: Some(Cow::from("An unknown error occured")),
|
||||
params: HashMap::new(),
|
||||
},
|
||||
);
|
||||
errors
|
||||
}
|
||||
|
||||
#[post("/users/new", data = "<form>")]
|
||||
pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Result<Redirect, Ructe> {
|
||||
if !Instance::get_local(&*conn)
|
||||
if !Instance::get_local(&*conn)
|
||||
.map(|i| i.open_registrations)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
@ -355,13 +422,16 @@ pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Resul
|
||||
"",
|
||||
form.email.to_string(),
|
||||
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 = _)))
|
||||
})
|
||||
.map_err(|err| {
|
||||
.map_err(|err| {
|
||||
render!(users::new(
|
||||
&(&*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,
|
||||
err
|
||||
))
|
||||
@ -395,21 +465,27 @@ pub fn inbox(
|
||||
))))?;
|
||||
|
||||
let actor = User::from_url(&conn, actor_id).expect("user::inbox: user error");
|
||||
if !verify_http_headers(&actor, &headers.0, &sig).is_secure()
|
||||
&& !act.clone().verify(&actor)
|
||||
{
|
||||
if !verify_http_headers(&actor, &headers.0, &sig).is_secure() && !act.clone().verify(&actor) {
|
||||
// maybe we just know an old key?
|
||||
actor.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)
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Signature)
|
||||
})
|
||||
actor
|
||||
.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)
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Signature)
|
||||
}
|
||||
})
|
||||
.map_err(|_| {
|
||||
println!("Rejected invalid activity supposedly from {}, with headers {:?}", actor.username, headers.0);
|
||||
status::BadRequest(Some("Invalid signature"))})?;
|
||||
println!(
|
||||
"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)? {
|
||||
@ -432,18 +508,20 @@ pub fn ap_followers(
|
||||
) -> Option<ActivityStream<OrderedCollection>> {
|
||||
let user = User::find_by_fqn(&*conn, &name).ok()?;
|
||||
let followers = user
|
||||
.get_followers(&*conn).ok()?
|
||||
.get_followers(&*conn)
|
||||
.ok()?
|
||||
.into_iter()
|
||||
.map(|f| Id::new(f.ap_url))
|
||||
.collect::<Vec<Id>>();
|
||||
|
||||
let mut coll = OrderedCollection::default();
|
||||
coll.object_props
|
||||
.set_id_string(user.followers_endpoint).ok()?;
|
||||
.set_id_string(user.followers_endpoint)
|
||||
.ok()?;
|
||||
coll.collection_props
|
||||
.set_total_items_u64(followers.len() as u64).ok()?;
|
||||
coll.collection_props
|
||||
.set_items_link_vec(followers).ok()?;
|
||||
.set_total_items_u64(followers.len() as u64)
|
||||
.ok()?;
|
||||
coll.collection_props.set_items_link_vec(followers).ok()?;
|
||||
Some(ActivityStream::new(coll))
|
||||
}
|
||||
|
||||
@ -456,7 +534,8 @@ pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
|
||||
.unwrap()
|
||||
.compute_box("~", &name, "atom.xml"))
|
||||
.entries(
|
||||
Post::get_recents_for_author(&*conn, &author, 15).ok()?
|
||||
Post::get_recents_for_author(&*conn, &author, 15)
|
||||
.ok()?
|
||||
.into_iter()
|
||||
.map(|p| super::post_to_atom(p, &*conn))
|
||||
.collect::<Vec<Entry>>(),
|
||||
|
@ -3,32 +3,42 @@ use rocket::response::Content;
|
||||
use serde_json;
|
||||
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")]
|
||||
pub fn nodeinfo() -> Content<String> {
|
||||
Content(ContentType::new("application", "jrd+json"), json!({
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||
"href": ap_url(&format!("{domain}/nodeinfo/2.0", domain = BASE_URL.as_str()))
|
||||
},
|
||||
{
|
||||
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.1",
|
||||
"href": ap_url(&format!("{domain}/nodeinfo/2.1", domain = BASE_URL.as_str()))
|
||||
}
|
||||
]
|
||||
}).to_string())
|
||||
Content(
|
||||
ContentType::new("application", "jrd+json"),
|
||||
json!({
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||
"href": ap_url(&format!("{domain}/nodeinfo/2.0", domain = BASE_URL.as_str()))
|
||||
},
|
||||
{
|
||||
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.1",
|
||||
"href": ap_url(&format!("{domain}/nodeinfo/2.1", domain = BASE_URL.as_str()))
|
||||
}
|
||||
]
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/.well-known/host-meta")]
|
||||
pub fn host_meta() -> String {
|
||||
format!(r#"
|
||||
format!(
|
||||
r#"
|
||||
<?xml version="1.0"?>
|
||||
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
||||
<Link rel="lrdd" type="application/xrd+xml" template="{url}"/>
|
||||
</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;
|
||||
@ -41,20 +51,31 @@ impl Resolver<DbConn> for WebfingerResolver {
|
||||
fn find(acct: String, conn: DbConn) -> Result<Webfinger, ResolverError> {
|
||||
User::find_by_fqn(&*conn, &acct)
|
||||
.and_then(|usr| usr.webfinger(&*conn))
|
||||
.or_else(|_| Blog::find_by_fqn(&*conn, &acct)
|
||||
.and_then(|blog| blog.webfinger(&*conn))
|
||||
.or(Err(ResolverError::NotFound)))
|
||||
.or_else(|_| {
|
||||
Blog::find_by_fqn(&*conn, &acct)
|
||||
.and_then(|blog| blog.webfinger(&*conn))
|
||||
.or(Err(ResolverError::NotFound))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/.well-known/webfinger?<resource>")]
|
||||
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),
|
||||
Err(err) => Content(ContentType::new("text", "plain"), String::from(match err {
|
||||
ResolverError::InvalidResource => "Invalid resource. Make sure to request an acct: URI",
|
||||
ResolverError::NotFound => "Requested resource was not found",
|
||||
ResolverError::WrongInstance => "This is not the instance of the requested resource"
|
||||
}))
|
||||
Err(err) => Content(
|
||||
ContentType::new("text", "plain"),
|
||||
String::from(match err {
|
||||
ResolverError::InvalidResource => {
|
||||
"Invalid resource. Make sure to request an acct: URI"
|
||||
}
|
||||
ResolverError::NotFound => "Requested resource was not found",
|
||||
ResolverError::WrongInstance => {
|
||||
"This is not the instance of the requested resource"
|
||||
}
|
||||
}),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@ -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::{Method, Status};
|
||||
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 std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::Hasher;
|
||||
@ -13,7 +13,7 @@ pub use askama_escape::escape;
|
||||
|
||||
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)]
|
||||
pub struct Ructe(pub Vec<u8>);
|
||||
@ -27,7 +27,10 @@ impl<'r> Responder<'r> for Ructe {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
hasher.write(&self.0);
|
||||
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()
|
||||
.status(Status::NotModified)
|
||||
.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();
|
||||
Html(format!(
|
||||
r#"<div class="avatar {size} {padded}"
|
||||
@ -120,49 +129,82 @@ pub fn tabs(links: &[(&str, String, bool)]) -> Html<String> {
|
||||
pub fn paginate(catalog: &Catalog, page: i32, total: i32) -> Html<String> {
|
||||
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 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">"#);
|
||||
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 {
|
||||
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>");
|
||||
Html(res)
|
||||
}
|
||||
|
||||
pub fn encode_query_param(param: &str) -> String {
|
||||
param.chars().map(|c| match c {
|
||||
'+' => Ok("%2B"),
|
||||
' ' => Err('+'),
|
||||
c => Err(c),
|
||||
}).fold(String::new(), |mut s,r| {
|
||||
match r {
|
||||
Ok(r) => s.push_str(r),
|
||||
Err(r) => s.push(r),
|
||||
};
|
||||
s
|
||||
})
|
||||
param
|
||||
.chars()
|
||||
.map(|c| match c {
|
||||
'+' => Ok("%2B"),
|
||||
' ' => Err('+'),
|
||||
c => Err(c),
|
||||
})
|
||||
.fold(String::new(), |mut s, r| {
|
||||
match r {
|
||||
Ok(r) => s.push_str(r),
|
||||
Err(r) => s.push(r),
|
||||
};
|
||||
s
|
||||
})
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! icon {
|
||||
($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 {
|
||||
($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;
|
||||
let cat = $catalog;
|
||||
($catalog:expr, $name:tt ($kind:tt), $label:expr, $optional:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {{
|
||||
use std::borrow::Cow;
|
||||
use validator::ValidationErrorsKind;
|
||||
let cat = $catalog;
|
||||
|
||||
Html(format!(r#"
|
||||
Html(format!(
|
||||
r#"
|
||||
<label for="{name}">
|
||||
{label}
|
||||
{optional}
|
||||
@ -171,52 +213,98 @@ macro_rules! input {
|
||||
{error}
|
||||
<input type="{kind}" id="{name}" name="{name}" value="{val}" {props}/>
|
||||
"#,
|
||||
name = stringify!($name),
|
||||
label = i18n!(cat, $label),
|
||||
kind = stringify!($kind),
|
||||
optional = if $optional { format!("<small>{}</small>", i18n!(cat, "Optional")) } else { String::new() },
|
||||
details = if $details.len() > 0 {
|
||||
format!("<small>{}</small>", i18n!(cat, $details))
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
error = if let Some(ValidationErrorsKind::Field(errs)) = $err.errors().get(stringify!($name)) {
|
||||
format!(r#"<p class="error">{}</p>"#, errs[0].message.clone().unwrap_or(Cow::from("Unknown error")))
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
val = escape(&$form.$name),
|
||||
props = $props
|
||||
))
|
||||
}
|
||||
};
|
||||
name = stringify!($name),
|
||||
label = i18n!(cat, $label),
|
||||
kind = stringify!($kind),
|
||||
optional = if $optional {
|
||||
format!("<small>{}</small>", i18n!(cat, "Optional"))
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
details = if $details.len() > 0 {
|
||||
format!("<small>{}</small>", i18n!(cat, $details))
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
error = if let Some(ValidationErrorsKind::Field(errs)) =
|
||||
$err.errors().get(stringify!($name))
|
||||
{
|
||||
format!(
|
||||
r#"<p class="error">{}</p>"#,
|
||||
errs[0]
|
||||
.message
|
||||
.clone()
|
||||
.unwrap_or(Cow::from("Unknown error"))
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
val = escape(&$form.$name),
|
||||
props = $props
|
||||
))
|
||||
}};
|
||||
($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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
{
|
||||
let cat = $catalog;
|
||||
Html(format!(r#"
|
||||
($catalog:expr, $name:tt ($kind:tt), $label:expr, $props:expr) => {{
|
||||
let cat = $catalog;
|
||||
Html(format!(
|
||||
r#"
|
||||
<label for="{name}">{label}</label>
|
||||
<input type="{kind}" id="{name}" name="{name}" {props}/>
|
||||
"#,
|
||||
name = stringify!($name),
|
||||
label = i18n!(cat, $label),
|
||||
kind = stringify!($kind),
|
||||
props = $props
|
||||
))
|
||||
}
|
||||
};
|
||||
name = stringify!($name),
|
||||
label = i18n!(cat, $label),
|
||||
kind = stringify!($kind),
|
||||
props = $props
|
||||
))
|
||||
}};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user