Run 'cargo fmt' to format code (#489)
This commit is contained in:
parent
732f514da7
commit
b945d1f602
|
@ -1,3 +1,5 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "MacTypes-sys"
|
name = "MacTypes-sys"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
|
|
29
build.rs
29
build.rs
|
@ -1,8 +1,8 @@
|
||||||
extern crate ructe;
|
|
||||||
extern crate rsass;
|
extern crate rsass;
|
||||||
|
extern crate ructe;
|
||||||
use ructe::*;
|
use ructe::*;
|
||||||
use std::{env, fs::*, io::Write, path::PathBuf};
|
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
|
use std::{env, fs::*, io::Write, path::PathBuf};
|
||||||
|
|
||||||
fn compute_static_hash() -> String {
|
fn compute_static_hash() -> String {
|
||||||
//"find static/ -type f ! -path 'static/media/*' | sort | xargs stat --printf='%n %Y\n' | sha256sum"
|
//"find static/ -type f ! -path 'static/media/*' | sort | xargs stat --printf='%n %Y\n' | sha256sum"
|
||||||
|
@ -34,25 +34,36 @@ fn compute_static_hash() -> String {
|
||||||
String::from_utf8(sha.stdout).unwrap()
|
String::from_utf8(sha.stdout).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||||
let in_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
|
let in_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("templates");
|
||||||
.join("templates");
|
|
||||||
compile_templates(&in_dir, &out_dir).expect("compile templates");
|
compile_templates(&in_dir, &out_dir).expect("compile templates");
|
||||||
|
|
||||||
println!("cargo:rerun-if-changed=static/css");
|
println!("cargo:rerun-if-changed=static/css");
|
||||||
let mut out = File::create("static/css/main.css").expect("Couldn't create main.css");
|
let mut out = File::create("static/css/main.css").expect("Couldn't create main.css");
|
||||||
out.write_all(
|
out.write_all(
|
||||||
&rsass::compile_scss_file("static/css/main.scss".as_ref(), rsass::OutputStyle::Compressed)
|
&rsass::compile_scss_file(
|
||||||
.expect("Error during SCSS compilation")
|
"static/css/main.scss".as_ref(),
|
||||||
).expect("Couldn't write CSS output");
|
rsass::OutputStyle::Compressed,
|
||||||
|
)
|
||||||
|
.expect("Error during SCSS compilation"),
|
||||||
|
)
|
||||||
|
.expect("Couldn't write CSS output");
|
||||||
|
|
||||||
let cache_id = &compute_static_hash()[..8];
|
let cache_id = &compute_static_hash()[..8];
|
||||||
println!("cargo:rerun-if-changed=target/deploy/plume-front.wasm");
|
println!("cargo:rerun-if-changed=target/deploy/plume-front.wasm");
|
||||||
copy("target/deploy/plume-front.wasm", "static/plume-front.wasm")
|
copy("target/deploy/plume-front.wasm", "static/plume-front.wasm")
|
||||||
.and_then(|_| read_to_string("target/deploy/plume-front.js"))
|
.and_then(|_| read_to_string("target/deploy/plume-front.js"))
|
||||||
.and_then(|js| write("static/plume-front.js", js.replace("\"plume-front.wasm\"", &format!("\"/static/cached/{}/plume-front.wasm\"", cache_id)))).ok();
|
.and_then(|js| {
|
||||||
|
write(
|
||||||
|
"static/plume-front.js",
|
||||||
|
js.replace(
|
||||||
|
"\"plume-front.wasm\"",
|
||||||
|
&format!("\"/static/cached/{}/plume-front.wasm\"", cache_id),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
|
||||||
println!("cargo:rustc-env=CACHE_ID={}", cache_id)
|
println!("cargo:rustc-env=CACHE_ID={}", cache_id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,4 +21,4 @@ pub mod posts;
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Api {
|
pub struct Api {
|
||||||
pub posts: posts::PostEndpoint,
|
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 std::env;
|
||||||
use plume_models::{
|
|
||||||
Connection,
|
|
||||||
instance::*,
|
|
||||||
safe_string::SafeString,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
||||||
SubCommand::with_name("instance")
|
SubCommand::with_name("instance")
|
||||||
|
@ -42,22 +38,33 @@ pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||||
let domain = args.value_of("domain").map(String::from)
|
let domain = args
|
||||||
.unwrap_or_else(|| env::var("BASE_URL")
|
.value_of("domain")
|
||||||
.unwrap_or_else(|_| super::ask_for("Domain name")));
|
.map(String::from)
|
||||||
let name = args.value_of("name").map(String::from).unwrap_or_else(|| super::ask_for("Instance name"));
|
.unwrap_or_else(|| env::var("BASE_URL").unwrap_or_else(|_| super::ask_for("Domain name")));
|
||||||
let license = args.value_of("default-license").map(String::from).unwrap_or_else(|| String::from("CC-BY-SA"));
|
let name = args
|
||||||
|
.value_of("name")
|
||||||
|
.map(String::from)
|
||||||
|
.unwrap_or_else(|| super::ask_for("Instance name"));
|
||||||
|
let license = args
|
||||||
|
.value_of("default-license")
|
||||||
|
.map(String::from)
|
||||||
|
.unwrap_or_else(|| String::from("CC-BY-SA"));
|
||||||
let open_reg = !args.is_present("private");
|
let open_reg = !args.is_present("private");
|
||||||
|
|
||||||
Instance::insert(conn, NewInstance {
|
Instance::insert(
|
||||||
public_domain: domain,
|
conn,
|
||||||
name,
|
NewInstance {
|
||||||
local: true,
|
public_domain: domain,
|
||||||
long_description: SafeString::new(""),
|
name,
|
||||||
short_description: SafeString::new(""),
|
local: true,
|
||||||
default_license: license,
|
long_description: SafeString::new(""),
|
||||||
open_registrations: open_reg,
|
short_description: SafeString::new(""),
|
||||||
short_description_html: String::new(),
|
default_license: license,
|
||||||
long_description_html: String::new()
|
open_registrations: open_reg,
|
||||||
}).expect("Couldn't save instance");
|
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 clap::App;
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
|
use plume_models::{Connection as Conn, DATABASE_URL};
|
||||||
use std::io::{self, prelude::*};
|
use std::io::{self, prelude::*};
|
||||||
use plume_models::{DATABASE_URL, Connection as Conn};
|
|
||||||
|
|
||||||
mod instance;
|
mod instance;
|
||||||
mod users;
|
|
||||||
mod search;
|
mod search;
|
||||||
|
mod users;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut app = App::new("Plume CLI")
|
let mut app = App::new("Plume CLI")
|
||||||
|
@ -27,10 +27,16 @@ fn main() {
|
||||||
let conn = Conn::establish(DATABASE_URL.as_str());
|
let conn = Conn::establish(DATABASE_URL.as_str());
|
||||||
|
|
||||||
match matches.subcommand() {
|
match matches.subcommand() {
|
||||||
("instance", Some(args)) => instance::run(args, &conn.expect("Couldn't connect to the database.")),
|
("instance", Some(args)) => {
|
||||||
("users", Some(args)) => users::run(args, &conn.expect("Couldn't connect to the database.")),
|
instance::run(args, &conn.expect("Couldn't connect to the database."))
|
||||||
("search", Some(args)) => search::run(args, &conn.expect("Couldn't connect to the database.")),
|
}
|
||||||
_ => app.print_help().expect("Couldn't print help")
|
("users", Some(args)) => {
|
||||||
|
users::run(args, &conn.expect("Couldn't connect to the database."))
|
||||||
|
}
|
||||||
|
("search", Some(args)) => {
|
||||||
|
search::run(args, &conn.expect("Couldn't connect to the database."))
|
||||||
|
}
|
||||||
|
_ => app.print_help().expect("Couldn't print help"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +44,9 @@ pub fn ask_for(something: &str) -> String {
|
||||||
print!("{}: ", something);
|
print!("{}: ", something);
|
||||||
io::stdout().flush().expect("Couldn't flush STDOUT");
|
io::stdout().flush().expect("Couldn't flush STDOUT");
|
||||||
let mut input = String::new();
|
let mut input = String::new();
|
||||||
io::stdin().read_line(&mut input).expect("Unable to read line");
|
io::stdin()
|
||||||
|
.read_line(&mut input)
|
||||||
|
.expect("Unable to read line");
|
||||||
input.retain(|c| c != '\n');
|
input.retain(|c| c != '\n');
|
||||||
input
|
input
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,47 +1,56 @@
|
||||||
use clap::{Arg, ArgMatches, App, SubCommand};
|
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
|
|
||||||
|
use plume_models::{posts::Post, schema::posts, search::Searcher, Connection};
|
||||||
use std::fs::{read_dir, remove_file};
|
use std::fs::{read_dir, remove_file};
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use plume_models::{
|
|
||||||
Connection,
|
|
||||||
posts::Post,
|
|
||||||
schema::posts,
|
|
||||||
search::Searcher,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
||||||
SubCommand::with_name("search")
|
SubCommand::with_name("search")
|
||||||
.about("Manage search index")
|
.about("Manage search index")
|
||||||
.subcommand(SubCommand::with_name("init")
|
.subcommand(
|
||||||
.arg(Arg::with_name("path")
|
SubCommand::with_name("init")
|
||||||
.short("p")
|
.arg(
|
||||||
.long("path")
|
Arg::with_name("path")
|
||||||
.takes_value(true)
|
.short("p")
|
||||||
.required(false)
|
.long("path")
|
||||||
.help("Path to Plume's working directory"))
|
.takes_value(true)
|
||||||
.arg(Arg::with_name("force")
|
.required(false)
|
||||||
.short("f")
|
.help("Path to Plume's working directory"),
|
||||||
.long("force")
|
)
|
||||||
.help("Ignore already using directory")
|
.arg(
|
||||||
).about("Initialize Plume's internal search engine"))
|
Arg::with_name("force")
|
||||||
.subcommand(SubCommand::with_name("refill")
|
.short("f")
|
||||||
.arg(Arg::with_name("path")
|
.long("force")
|
||||||
.short("p")
|
.help("Ignore already using directory"),
|
||||||
.long("path")
|
)
|
||||||
.takes_value(true)
|
.about("Initialize Plume's internal search engine"),
|
||||||
.required(false)
|
)
|
||||||
.help("Path to Plume's working directory")
|
.subcommand(
|
||||||
).about("Regenerate Plume's search index"))
|
SubCommand::with_name("refill")
|
||||||
.subcommand(SubCommand::with_name("unlock")
|
.arg(
|
||||||
.arg(Arg::with_name("path")
|
Arg::with_name("path")
|
||||||
.short("p")
|
.short("p")
|
||||||
.long("path")
|
.long("path")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.required(false)
|
.required(false)
|
||||||
.help("Path to Plume's working directory")
|
.help("Path to Plume's working directory"),
|
||||||
).about("Release lock on search directory"))
|
)
|
||||||
|
.about("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) {
|
pub fn run<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||||
|
@ -59,19 +68,25 @@ fn init<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||||
let force = args.is_present("force");
|
let force = args.is_present("force");
|
||||||
let path = Path::new(path).join("search_index");
|
let path = Path::new(path).join("search_index");
|
||||||
|
|
||||||
let can_do = match read_dir(path.clone()) { // try to read the directory specified
|
let can_do = match read_dir(path.clone()) {
|
||||||
Ok(mut contents) => contents.next().is_none(),
|
// try to read the directory specified
|
||||||
Err(e) => if e.kind() == ErrorKind::NotFound {
|
Ok(mut contents) => contents.next().is_none(),
|
||||||
true
|
Err(e) => {
|
||||||
} else {
|
if e.kind() == ErrorKind::NotFound {
|
||||||
panic!("Error while initialising search index : {}", e);
|
true
|
||||||
|
} else {
|
||||||
|
panic!("Error while initialising search index : {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if can_do || force {
|
if can_do || force {
|
||||||
let searcher = Searcher::create(&path).unwrap();
|
let searcher = Searcher::create(&path).unwrap();
|
||||||
refill(args, conn, Some(searcher));
|
refill(args, conn, Some(searcher));
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Can't create new index, {} exist and is not empty", path.to_str().unwrap());
|
eprintln!(
|
||||||
|
"Can't create new index, {} exist and is not empty",
|
||||||
|
path.to_str().unwrap()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,15 +101,16 @@ fn refill<'a>(args: &ArgMatches<'a>, conn: &Connection, searcher: Option<Searche
|
||||||
.expect("Post::get_recents: loading error");
|
.expect("Post::get_recents: loading error");
|
||||||
|
|
||||||
let len = posts.len();
|
let len = posts.len();
|
||||||
for (i,post) in posts.iter().enumerate() {
|
for (i, post) in posts.iter().enumerate() {
|
||||||
println!("Importing {}/{} : {}", i+1, len, post.title);
|
println!("Importing {}/{} : {}", i + 1, len, post.title);
|
||||||
searcher.update_document(conn, &post).expect("Couldn't import post");
|
searcher
|
||||||
|
.update_document(conn, &post)
|
||||||
|
.expect("Couldn't import post");
|
||||||
}
|
}
|
||||||
println!("Commiting result");
|
println!("Commiting result");
|
||||||
searcher.commit();
|
searcher.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn unlock<'a>(args: &ArgMatches<'a>) {
|
fn unlock<'a>(args: &ArgMatches<'a>) {
|
||||||
let path = args.value_of("path").unwrap_or(".");
|
let path = args.value_of("path").unwrap_or(".");
|
||||||
let path = Path::new(path).join("search_index/.tantivy-indexer.lock");
|
let path = Path::new(path).join("search_index/.tantivy-indexer.lock");
|
||||||
|
|
|
@ -1,62 +1,78 @@
|
||||||
use clap::{Arg, ArgMatches, App, SubCommand};
|
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||||
|
|
||||||
|
use plume_models::{instance::Instance, users::*, Connection};
|
||||||
use rpassword;
|
use rpassword;
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use plume_models::{
|
|
||||||
Connection,
|
|
||||||
instance::Instance,
|
|
||||||
users::*,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
||||||
SubCommand::with_name("users")
|
SubCommand::with_name("users")
|
||||||
.about("Manage users")
|
.about("Manage users")
|
||||||
.subcommand(SubCommand::with_name("new")
|
.subcommand(
|
||||||
.arg(Arg::with_name("name")
|
SubCommand::with_name("new")
|
||||||
.short("n")
|
.arg(
|
||||||
.long("name")
|
Arg::with_name("name")
|
||||||
.alias("username")
|
.short("n")
|
||||||
.takes_value(true)
|
.long("name")
|
||||||
.help("The username of the new user")
|
.alias("username")
|
||||||
).arg(Arg::with_name("display-name")
|
.takes_value(true)
|
||||||
.short("N")
|
.help("The username of the new user"),
|
||||||
.long("display-name")
|
)
|
||||||
.takes_value(true)
|
.arg(
|
||||||
.help("The display name of the new user")
|
Arg::with_name("display-name")
|
||||||
).arg(Arg::with_name("biography")
|
.short("N")
|
||||||
.short("b")
|
.long("display-name")
|
||||||
.long("bio")
|
.takes_value(true)
|
||||||
.alias("biography")
|
.help("The display name of the new user"),
|
||||||
.takes_value(true)
|
)
|
||||||
.help("The biography of the new user")
|
.arg(
|
||||||
).arg(Arg::with_name("email")
|
Arg::with_name("biography")
|
||||||
.short("e")
|
.short("b")
|
||||||
.long("email")
|
.long("bio")
|
||||||
.takes_value(true)
|
.alias("biography")
|
||||||
.help("Email address of the new user")
|
.takes_value(true)
|
||||||
).arg(Arg::with_name("password")
|
.help("The biography of the new user"),
|
||||||
.short("p")
|
)
|
||||||
.long("password")
|
.arg(
|
||||||
.takes_value(true)
|
Arg::with_name("email")
|
||||||
.help("The password of the new user")
|
.short("e")
|
||||||
).arg(Arg::with_name("admin")
|
.long("email")
|
||||||
.short("a")
|
.takes_value(true)
|
||||||
.long("admin")
|
.help("Email address of the new user"),
|
||||||
.help("Makes the user an administrator of the instance")
|
)
|
||||||
).about("Create a new user on this instance"))
|
.arg(
|
||||||
.subcommand(SubCommand::with_name("reset-password")
|
Arg::with_name("password")
|
||||||
.arg(Arg::with_name("name")
|
.short("p")
|
||||||
.short("u")
|
.long("password")
|
||||||
.long("user")
|
.takes_value(true)
|
||||||
.alias("username")
|
.help("The password of the new user"),
|
||||||
.takes_value(true)
|
)
|
||||||
.help("The username of the user to reset password to")
|
.arg(
|
||||||
).arg(Arg::with_name("password")
|
Arg::with_name("admin")
|
||||||
.short("p")
|
.short("a")
|
||||||
.long("password")
|
.long("admin")
|
||||||
.takes_value(true)
|
.help("Makes the user an administrator of the instance"),
|
||||||
.help("The password new for the user")
|
)
|
||||||
).about("Reset user password"))
|
.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) {
|
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) {
|
fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||||
let username = args.value_of("name").map(String::from).unwrap_or_else(|| super::ask_for("Username"));
|
let username = args
|
||||||
let display_name = args.value_of("display-name").map(String::from).unwrap_or_else(|| super::ask_for("Display name"));
|
.value_of("name")
|
||||||
|
.map(String::from)
|
||||||
|
.unwrap_or_else(|| super::ask_for("Username"));
|
||||||
|
let display_name = args
|
||||||
|
.value_of("display-name")
|
||||||
|
.map(String::from)
|
||||||
|
.unwrap_or_else(|| super::ask_for("Display name"));
|
||||||
let admin = args.is_present("admin");
|
let admin = args.is_present("admin");
|
||||||
let bio = args.value_of("biography").unwrap_or("").to_string();
|
let bio = args.value_of("biography").unwrap_or("").to_string();
|
||||||
let email = args.value_of("email").map(String::from).unwrap_or_else(|| super::ask_for("Email address"));
|
let email = args
|
||||||
let password = args.value_of("password").map(String::from).unwrap_or_else(|| {
|
.value_of("email")
|
||||||
print!("Password: ");
|
.map(String::from)
|
||||||
io::stdout().flush().expect("Couldn't flush STDOUT");
|
.unwrap_or_else(|| super::ask_for("Email address"));
|
||||||
rpassword::read_password().expect("Couldn't read your password.")
|
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(
|
NewUser::new_local(
|
||||||
conn,
|
conn,
|
||||||
|
@ -88,17 +116,31 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||||
&bio,
|
&bio,
|
||||||
email,
|
email,
|
||||||
User::hash_pass(&password).expect("Couldn't hash password"),
|
User::hash_pass(&password).expect("Couldn't hash password"),
|
||||||
).expect("Couldn't save new user");
|
)
|
||||||
|
.expect("Couldn't save new user");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_password<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
fn reset_password<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||||
let username = args.value_of("name").map(String::from).unwrap_or_else(|| super::ask_for("Username"));
|
let username = args
|
||||||
let user = User::find_by_name(conn, &username, Instance::get_local(conn).expect("Failed to get local instance").id)
|
.value_of("name")
|
||||||
.expect("Failed to get user");
|
.map(String::from)
|
||||||
let password = args.value_of("password").map(String::from).unwrap_or_else(|| {
|
.unwrap_or_else(|| super::ask_for("Username"));
|
||||||
print!("Password: ");
|
let user = User::find_by_name(
|
||||||
io::stdout().flush().expect("Couldn't flush STDOUT");
|
conn,
|
||||||
rpassword::read_password().expect("Couldn't read your password.")
|
&username,
|
||||||
});
|
Instance::get_local(conn)
|
||||||
user.reset_password(conn, &password).expect("Failed to reset password");
|
.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 CONTEXT_URL: &str = "https://www.w3.org/ns/activitystreams";
|
||||||
pub const PUBLIC_VISIBILTY: &str = "https://www.w3.org/ns/activitystreams#Public";
|
pub const PUBLIC_VISIBILTY: &str = "https://www.w3.org/ns/activitystreams#Public";
|
||||||
|
|
||||||
pub const AP_CONTENT_TYPE: &str = r#"application/ld+json; profile="https://www.w3.org/ns/activitystreams""#;
|
pub const AP_CONTENT_TYPE: &str =
|
||||||
|
r#"application/ld+json; profile="https://www.w3.org/ns/activitystreams""#;
|
||||||
|
|
||||||
pub fn ap_accept_header() -> Vec<&'static str> {
|
pub fn ap_accept_header() -> Vec<&'static str> {
|
||||||
vec![
|
vec![
|
||||||
|
@ -114,13 +115,18 @@ pub fn broadcast<S: sign::Signer, A: Activity, T: inbox::WithInbox>(
|
||||||
let boxes = to
|
let boxes = to
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|u| !u.is_local())
|
.filter(|u| !u.is_local())
|
||||||
.map(|u| u.get_shared_inbox_url().unwrap_or_else(|| u.get_inbox_url()))
|
.map(|u| {
|
||||||
|
u.get_shared_inbox_url()
|
||||||
|
.unwrap_or_else(|| u.get_inbox_url())
|
||||||
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.unique();
|
.unique();
|
||||||
|
|
||||||
let mut act = serde_json::to_value(act).expect("activity_pub::broadcast: serialization error");
|
let mut act = serde_json::to_value(act).expect("activity_pub::broadcast: serialization error");
|
||||||
act["@context"] = context();
|
act["@context"] = context();
|
||||||
let signed = act.sign(sender).expect("activity_pub::broadcast: signature error");
|
let signed = act
|
||||||
|
.sign(sender)
|
||||||
|
.expect("activity_pub::broadcast: signature error");
|
||||||
|
|
||||||
for inbox in boxes {
|
for inbox in boxes {
|
||||||
// TODO: run it in Sidekiq or something like that
|
// TODO: run it in Sidekiq or something like that
|
||||||
|
@ -130,7 +136,11 @@ pub fn broadcast<S: sign::Signer, A: Activity, T: inbox::WithInbox>(
|
||||||
let res = Client::new()
|
let res = Client::new()
|
||||||
.post(&inbox)
|
.post(&inbox)
|
||||||
.headers(headers.clone())
|
.headers(headers.clone())
|
||||||
.header("Signature", request::signature(sender, &headers).expect("activity_pub::broadcast: request signature error"))
|
.header(
|
||||||
|
"Signature",
|
||||||
|
request::signature(sender, &headers)
|
||||||
|
.expect("activity_pub::broadcast: request signature error"),
|
||||||
|
)
|
||||||
.body(body)
|
.body(body)
|
||||||
.send();
|
.send();
|
||||||
match res {
|
match res {
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
use base64;
|
use base64;
|
||||||
use chrono::{offset::Utc, DateTime};
|
use chrono::{offset::Utc, DateTime};
|
||||||
use openssl::hash::{Hasher, MessageDigest};
|
use openssl::hash::{Hasher, MessageDigest};
|
||||||
use reqwest::header::{ACCEPT, CONTENT_TYPE, DATE, HeaderMap, HeaderValue, USER_AGENT};
|
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, DATE, USER_AGENT};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
use activity_pub::{AP_CONTENT_TYPE, ap_accept_header};
|
|
||||||
use activity_pub::sign::Signer;
|
use activity_pub::sign::Signer;
|
||||||
|
use activity_pub::{ap_accept_header, AP_CONTENT_TYPE};
|
||||||
|
|
||||||
const PLUME_USER_AGENT: &str = concat!("Plume/", env!("CARGO_PKG_VERSION"));
|
const PLUME_USER_AGENT: &str = concat!("Plume/", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ impl Digest {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verify_header(&self, other: &Digest) -> bool {
|
pub fn verify_header(&self, other: &Digest) -> bool {
|
||||||
self.value()==other.value()
|
self.value() == other.value()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn algorithm(&self) -> &str {
|
pub fn algorithm(&self) -> &str {
|
||||||
|
@ -57,7 +57,8 @@ impl Digest {
|
||||||
let pos = self
|
let pos = self
|
||||||
.0
|
.0
|
||||||
.find('=')
|
.find('=')
|
||||||
.expect("Digest::value: invalid header error") + 1;
|
.expect("Digest::value: invalid header error")
|
||||||
|
+ 1;
|
||||||
base64::decode(&self.0[pos..]).expect("Digest::value: invalid encoding error")
|
base64::decode(&self.0[pos..]).expect("Digest::value: invalid encoding error")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,8 +76,11 @@ impl Digest {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_body(body: &str) -> Self {
|
pub fn from_body(body: &str) -> Self {
|
||||||
let mut hasher = Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error");
|
let mut hasher =
|
||||||
hasher.update(body.as_bytes()).expect("Digest::digest: content insertion error");
|
Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error");
|
||||||
|
hasher
|
||||||
|
.update(body.as_bytes())
|
||||||
|
.expect("Digest::digest: content insertion error");
|
||||||
let res = base64::encode(&hasher.finish().expect("Digest::digest: finalizing error"));
|
let res = base64::encode(&hasher.finish().expect("Digest::digest: finalizing error"));
|
||||||
Digest(format!("SHA-256={}", res))
|
Digest(format!("SHA-256={}", res))
|
||||||
}
|
}
|
||||||
|
@ -99,7 +103,8 @@ pub fn headers() -> HeaderMap {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", "),
|
.join(", "),
|
||||||
).expect("request::headers: accept error"),
|
)
|
||||||
|
.expect("request::headers: accept error"),
|
||||||
);
|
);
|
||||||
headers.insert(CONTENT_TYPE, HeaderValue::from_static(AP_CONTENT_TYPE));
|
headers.insert(CONTENT_TYPE, HeaderValue::from_static(AP_CONTENT_TYPE));
|
||||||
headers
|
headers
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use super::request;
|
use super::request;
|
||||||
use base64;
|
use base64;
|
||||||
use chrono::{DateTime, Duration,
|
use chrono::{naive::NaiveDateTime, DateTime, Duration, Utc};
|
||||||
naive::NaiveDateTime, Utc};
|
|
||||||
use hex;
|
use hex;
|
||||||
use openssl::{pkey::PKey, rsa::Rsa, sha::sha256};
|
use openssl::{pkey::PKey, rsa::Rsa, sha::sha256};
|
||||||
use rocket::http::HeaderMap;
|
use rocket::http::HeaderMap;
|
||||||
|
@ -57,9 +56,10 @@ impl Signable for serde_json::Value {
|
||||||
|
|
||||||
let options_hash = Self::hash(
|
let options_hash = Self::hash(
|
||||||
&json!({
|
&json!({
|
||||||
"@context": "https://w3id.org/identity/v1",
|
"@context": "https://w3id.org/identity/v1",
|
||||||
"created": creation_date
|
"created": creation_date
|
||||||
}).to_string(),
|
})
|
||||||
|
.to_string(),
|
||||||
);
|
);
|
||||||
let document_hash = Self::hash(&self.to_string());
|
let document_hash = Self::hash(&self.to_string());
|
||||||
let to_be_signed = options_hash + &document_hash;
|
let to_be_signed = options_hash + &document_hash;
|
||||||
|
@ -91,7 +91,8 @@ impl Signable for serde_json::Value {
|
||||||
&json!({
|
&json!({
|
||||||
"@context": "https://w3id.org/identity/v1",
|
"@context": "https://w3id.org/identity/v1",
|
||||||
"created": creation_date
|
"created": creation_date
|
||||||
}).to_string(),
|
})
|
||||||
|
.to_string(),
|
||||||
);
|
);
|
||||||
let creation_date = creation_date.as_str();
|
let creation_date = creation_date.as_str();
|
||||||
if creation_date.is_none() {
|
if creation_date.is_none() {
|
||||||
|
@ -169,7 +170,10 @@ pub fn verify_http_headers<S: Signer + ::std::fmt::Debug>(
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
if !sender.verify(&h, &base64::decode(signature).unwrap_or_default()).unwrap_or(false) {
|
if !sender
|
||||||
|
.verify(&h, &base64::decode(signature).unwrap_or_default())
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
return SignatureValidity::Invalid;
|
return SignatureValidity::Invalid;
|
||||||
}
|
}
|
||||||
if !headers.contains(&"digest") {
|
if !headers.contains(&"digest") {
|
||||||
|
|
|
@ -10,8 +10,8 @@ extern crate chrono;
|
||||||
extern crate failure;
|
extern crate failure;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate failure_derive;
|
extern crate failure_derive;
|
||||||
extern crate hex;
|
|
||||||
extern crate heck;
|
extern crate heck;
|
||||||
|
extern crate hex;
|
||||||
extern crate openssl;
|
extern crate openssl;
|
||||||
extern crate pulldown_cmark;
|
extern crate pulldown_cmark;
|
||||||
extern crate reqwest;
|
extern crate reqwest;
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
use heck::CamelCase;
|
use heck::CamelCase;
|
||||||
use openssl::rand::rand_bytes;
|
use openssl::rand::rand_bytes;
|
||||||
use pulldown_cmark::{Event, Parser, Options, Tag, html};
|
use pulldown_cmark::{html, Event, Options, Parser, Tag};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::uri::Uri,
|
http::uri::Uri,
|
||||||
response::{Redirect, Flash}
|
response::{Flash, Redirect},
|
||||||
};
|
};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
/// Generates an hexadecimal representation of 32 bytes of random data
|
/// Generates an hexadecimal representation of 32 bytes of random data
|
||||||
pub fn random_hex() -> String {
|
pub fn random_hex() -> String {
|
||||||
let mut bytes = [0; 32];
|
let mut bytes = [0; 32];
|
||||||
rand_bytes(&mut bytes).expect("Error while generating client id");
|
rand_bytes(&mut bytes).expect("Error while generating client id");
|
||||||
bytes.iter().fold(String::new(), |res, byte| format!("{}{:x}", res, byte))
|
bytes
|
||||||
|
.iter()
|
||||||
|
.fold(String::new(), |res, byte| format!("{}{:x}", res, byte))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove non alphanumeric characters and CamelCase a string
|
/// Remove non alphanumeric characters and CamelCase a string
|
||||||
|
@ -29,7 +31,11 @@ pub fn make_actor_id(name: &str) -> String {
|
||||||
* Note that the message should be translated before passed to this function.
|
* Note that the message should be translated before passed to this function.
|
||||||
*/
|
*/
|
||||||
pub fn requires_login<T: Into<Uri<'static>>>(message: &str, url: T) -> Flash<Redirect> {
|
pub fn requires_login<T: Into<Uri<'static>>>(message: &str, url: T) -> Flash<Redirect> {
|
||||||
Flash::new(Redirect::to(format!("/login?m={}", Uri::percent_encode(message))), "callback", url.into().to_string())
|
Flash::new(
|
||||||
|
Redirect::to(format!("/login?m={}", Uri::percent_encode(message))),
|
||||||
|
"callback",
|
||||||
|
url.into().to_string(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -45,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 = Parser::new_ext(md, Options::all());
|
||||||
|
|
||||||
let (parser, mentions, hashtags): (Vec<Event>, Vec<String>, Vec<String>) = parser
|
let (parser, mentions, hashtags): (Vec<Event>, Vec<String>, Vec<String>) = parser
|
||||||
.scan(None, |state: &mut Option<String>, evt|{
|
.scan(None, |state: &mut Option<String>, evt| {
|
||||||
let (s, res) = match evt {
|
let (s, res) = match evt {
|
||||||
Event::Text(txt) => match state.take() {
|
Event::Text(txt) => match state.take() {
|
||||||
Some(mut prev_txt) => {
|
Some(mut prev_txt) => {
|
||||||
prev_txt.push_str(&txt);
|
prev_txt.push_str(&txt);
|
||||||
(Some(prev_txt), vec![])
|
(Some(prev_txt), vec![])
|
||||||
},
|
|
||||||
None => {
|
|
||||||
(Some(txt.into_owned()), vec![])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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 => {
|
None => (Some(txt.into_owned()), vec![]),
|
||||||
let char_matches = c.is_alphanumeric() || "-_".contains(c);
|
},
|
||||||
if char_matches && (n < (txt.chars().count() -1)) {
|
e => match state.take() {
|
||||||
text_acc.push(c);
|
Some(prev) => (None, vec![Event::Text(Cow::Owned(prev)), e]),
|
||||||
(events, State::Hashtag, text_acc, n+1, mentions, hashtags)
|
None => (None, vec![e]),
|
||||||
} else {
|
},
|
||||||
if char_matches {
|
};
|
||||||
|
*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);
|
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 == '@' {
|
(evts, new_mentions, new_hashtags)
|
||||||
events.push(Event::Text(text_acc.into()));
|
}
|
||||||
(events, State::Mention, String::new(), n + 1, mentions, hashtags)
|
_ => (vec![evt], vec![], vec![]),
|
||||||
} else if c == '#' {
|
})
|
||||||
events.push(Event::Text(text_acc.into()));
|
.fold(
|
||||||
(events, State::Hashtag, String::new(), n + 1, mentions, hashtags)
|
(vec![], vec![], vec![]),
|
||||||
} else if c.is_alphanumeric() {
|
|(mut parser, mut mention, mut hashtag), (mut p, mut m, mut h)| {
|
||||||
text_acc.push(c);
|
parser.append(&mut p);
|
||||||
if n >= (txt.chars().count() - 1) { // Add the text after at the end, even if it is not followed by a mention.
|
mention.append(&mut m);
|
||||||
events.push(Event::Text(text_acc.clone().into()))
|
hashtag.append(&mut h);
|
||||||
}
|
(parser, mention, hashtag)
|
||||||
(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)
|
|
||||||
});
|
|
||||||
let parser = parser.into_iter();
|
let parser = parser.into_iter();
|
||||||
let mentions = mentions.into_iter().map(|m| String::from(m.trim()));
|
let mentions = mentions.into_iter().map(|m| String::from(m.trim()));
|
||||||
let hashtags = hashtags.into_iter().map(|h| String::from(h.trim()));
|
let hashtags = hashtags.into_iter().map(|h| String::from(h.trim()));
|
||||||
|
@ -188,7 +238,13 @@ mod tests {
|
||||||
];
|
];
|
||||||
|
|
||||||
for (md, mentions) in tests {
|
for (md, mentions) in tests {
|
||||||
assert_eq!(md_to_html(md, "").1, mentions.into_iter().map(|s| s.to_string()).collect::<HashSet<String>>());
|
assert_eq!(
|
||||||
|
md_to_html(md, "").1,
|
||||||
|
mentions
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect::<HashSet<String>>()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,7 +263,13 @@ mod tests {
|
||||||
];
|
];
|
||||||
|
|
||||||
for (md, mentions) in tests {
|
for (md, mentions) in tests {
|
||||||
assert_eq!(md_to_html(md, "").2, mentions.into_iter().map(|s| s.to_string()).collect::<HashSet<String>>());
|
assert_eq!(
|
||||||
|
md_to_html(md, "").2,
|
||||||
|
mentions
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect::<HashSet<String>>()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
use stdweb::{unstable::{TryInto, TryFrom}, web::{*, html_element::*, event::*}};
|
use stdweb::{
|
||||||
|
unstable::{TryFrom, TryInto},
|
||||||
|
web::{event::*, html_element::*, *},
|
||||||
|
};
|
||||||
use CATALOG;
|
use CATALOG;
|
||||||
|
|
||||||
macro_rules! mv {
|
macro_rules! mv {
|
||||||
|
@ -14,7 +17,8 @@ fn get_elt_value(id: &'static str) -> String {
|
||||||
let elt = document().get_element_by_id(id).unwrap();
|
let elt = document().get_element_by_id(id).unwrap();
|
||||||
let inp: Result<InputElement, _> = elt.clone().try_into();
|
let inp: Result<InputElement, _> = elt.clone().try_into();
|
||||||
let textarea: Result<TextAreaElement, _> = elt.try_into();
|
let textarea: Result<TextAreaElement, _> = elt.try_into();
|
||||||
inp.map(|i| i.raw_value()).unwrap_or_else(|_| textarea.unwrap().value())
|
inp.map(|i| i.raw_value())
|
||||||
|
.unwrap_or_else(|_| textarea.unwrap().value())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_value<S: AsRef<str>>(id: &'static str, val: S) {
|
fn set_value<S: AsRef<str>>(id: &'static str, val: S) {
|
||||||
|
@ -64,7 +68,7 @@ fn init_widget(
|
||||||
tag: &'static str,
|
tag: &'static str,
|
||||||
placeholder_text: String,
|
placeholder_text: String,
|
||||||
content: String,
|
content: String,
|
||||||
disable_return: bool
|
disable_return: bool,
|
||||||
) -> Result<HtmlElement, EditorError> {
|
) -> Result<HtmlElement, EditorError> {
|
||||||
let widget = placeholder(make_editable(tag).try_into()?, &placeholder_text);
|
let widget = placeholder(make_editable(tag).try_into()?, &placeholder_text);
|
||||||
if !content.is_empty() {
|
if !content.is_empty() {
|
||||||
|
@ -86,7 +90,7 @@ fn init_widget(
|
||||||
pub fn init() -> Result<(), EditorError> {
|
pub fn init() -> Result<(), EditorError> {
|
||||||
if let Some(ed) = document().get_element_by_id("plume-editor") {
|
if let Some(ed) = document().get_element_by_id("plume-editor") {
|
||||||
// Show the editor
|
// Show the editor
|
||||||
js!{ @{&ed}.style.display = "block"; };
|
js! { @{&ed}.style.display = "block"; };
|
||||||
// And hide the HTML-only fallback
|
// And hide the HTML-only fallback
|
||||||
let old_ed = document().get_element_by_id("plume-fallback-editor")?;
|
let old_ed = document().get_element_by_id("plume-fallback-editor")?;
|
||||||
let old_title = document().get_element_by_id("plume-editor-title")?;
|
let old_title = document().get_element_by_id("plume-editor-title")?;
|
||||||
|
@ -101,8 +105,20 @@ pub fn init() -> Result<(), EditorError> {
|
||||||
let content_val = get_elt_value("editor-content");
|
let content_val = get_elt_value("editor-content");
|
||||||
// And pre-fill the new editor with this values
|
// And pre-fill the new editor with this values
|
||||||
let title = init_widget(&ed, "h1", i18n!(CATALOG, "Title"), title_val, true)?;
|
let title = init_widget(&ed, "h1", i18n!(CATALOG, "Title"), title_val, true)?;
|
||||||
let subtitle = init_widget(&ed, "h2", i18n!(CATALOG, "Subtitle or summary"), subtitle_val, true)?;
|
let subtitle = init_widget(
|
||||||
let content = init_widget(&ed, "article", i18n!(CATALOG, "Write your article here. Markdown is supported."), content_val.clone(), true)?;
|
&ed,
|
||||||
|
"h2",
|
||||||
|
i18n!(CATALOG, "Subtitle or summary"),
|
||||||
|
subtitle_val,
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
|
let content = init_widget(
|
||||||
|
&ed,
|
||||||
|
"article",
|
||||||
|
i18n!(CATALOG, "Write your article here. Markdown is supported."),
|
||||||
|
content_val.clone(),
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
js! { @{&content}.innerHTML = @{content_val}; };
|
js! { @{&content}.innerHTML = @{content_val}; };
|
||||||
|
|
||||||
// character counter
|
// character counter
|
||||||
|
@ -118,27 +134,38 @@ pub fn init() -> Result<(), EditorError> {
|
||||||
}), 0);
|
}), 0);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
document().get_element_by_id("publish")?.add_event_listener(mv!(title, subtitle, content, old_ed => move |_: ClickEvent| {
|
document().get_element_by_id("publish")?.add_event_listener(
|
||||||
let popup = document().get_element_by_id("publish-popup").or_else(||
|
mv!(title, subtitle, content, old_ed => move |_: ClickEvent| {
|
||||||
init_popup(&title, &subtitle, &content, &old_ed).ok()
|
let popup = document().get_element_by_id("publish-popup").or_else(||
|
||||||
).unwrap();
|
init_popup(&title, &subtitle, &content, &old_ed).ok()
|
||||||
let bg = document().get_element_by_id("popup-bg").or_else(||
|
).unwrap();
|
||||||
init_popup_bg().ok()
|
let bg = document().get_element_by_id("popup-bg").or_else(||
|
||||||
).unwrap();
|
init_popup_bg().ok()
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
popup.class_list().add("show").unwrap();
|
popup.class_list().add("show").unwrap();
|
||||||
bg.class_list().add("show").unwrap();
|
bg.class_list().add("show").unwrap();
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_popup(title: &HtmlElement, subtitle: &HtmlElement, content: &HtmlElement, old_ed: &Element) -> Result<Element, EditorError> {
|
fn init_popup(
|
||||||
|
title: &HtmlElement,
|
||||||
|
subtitle: &HtmlElement,
|
||||||
|
content: &HtmlElement,
|
||||||
|
old_ed: &Element,
|
||||||
|
) -> Result<Element, EditorError> {
|
||||||
let popup = document().create_element("div")?;
|
let popup = document().create_element("div")?;
|
||||||
popup.class_list().add("popup")?;
|
popup.class_list().add("popup")?;
|
||||||
popup.set_attribute("id", "publish-popup")?;
|
popup.set_attribute("id", "publish-popup")?;
|
||||||
|
|
||||||
let tags = get_elt_value("tags").split(',').map(str::trim).map(str::to_string).collect::<Vec<_>>();
|
let tags = get_elt_value("tags")
|
||||||
|
.split(',')
|
||||||
|
.map(str::trim)
|
||||||
|
.map(str::to_string)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
let license = get_elt_value("license");
|
let license = get_elt_value("license");
|
||||||
make_input(&i18n!(CATALOG, "Tags"), "popup-tags", &popup).set_raw_value(&tags.join(", "));
|
make_input(&i18n!(CATALOG, "Tags"), "popup-tags", &popup).set_raw_value(&tags.join(", "));
|
||||||
make_input(&i18n!(CATALOG, "License"), "popup-license", &popup).set_raw_value(&license);
|
make_input(&i18n!(CATALOG, "License"), "popup-license", &popup).set_raw_value(&license);
|
||||||
|
@ -152,7 +179,7 @@ fn init_popup(title: &HtmlElement, subtitle: &HtmlElement, content: &HtmlElement
|
||||||
popup.append_child(&cover);
|
popup.append_child(&cover);
|
||||||
|
|
||||||
let button = document().create_element("input")?;
|
let button = document().create_element("input")?;
|
||||||
js!{
|
js! {
|
||||||
@{&button}.type = "submit";
|
@{&button}.type = "submit";
|
||||||
@{&button}.value = @{i18n!(CATALOG, "Publish")};
|
@{&button}.value = @{i18n!(CATALOG, "Publish")};
|
||||||
};
|
};
|
||||||
|
@ -189,7 +216,10 @@ fn init_popup_bg() -> Result<Element, EditorError> {
|
||||||
fn chars_left(selector: &str, content: &HtmlElement) -> Option<i32> {
|
fn chars_left(selector: &str, content: &HtmlElement) -> Option<i32> {
|
||||||
match document().query_selector(selector) {
|
match document().query_selector(selector) {
|
||||||
Ok(Some(form)) => HtmlElement::try_from(form).ok().and_then(|form| {
|
Ok(Some(form)) => HtmlElement::try_from(form).ok().and_then(|form| {
|
||||||
if let Some(len) = form.get_attribute("content-size").and_then(|s| s.parse::<i32>().ok()) {
|
if let Some(len) = form
|
||||||
|
.get_attribute("content-size")
|
||||||
|
.and_then(|s| s.parse::<i32>().ok())
|
||||||
|
{
|
||||||
(js! {
|
(js! {
|
||||||
let x = encodeURIComponent(@{content}.innerHTML)
|
let x = encodeURIComponent(@{content}.innerHTML)
|
||||||
.replace(/%20/g, "+")
|
.replace(/%20/g, "+")
|
||||||
|
@ -198,7 +228,10 @@ fn chars_left(selector: &str, content: &HtmlElement) -> Option<i32> {
|
||||||
.length + 2;
|
.length + 2;
|
||||||
console.log(x);
|
console.log(x);
|
||||||
return x;
|
return x;
|
||||||
}).try_into().map(|c: i32| len - c).ok()
|
})
|
||||||
|
.try_into()
|
||||||
|
.map(|c: i32| len - c)
|
||||||
|
.ok()
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -218,7 +251,11 @@ fn make_input(label_text: &str, name: &'static str, form: &Element) -> InputElem
|
||||||
label.append_child(&document().create_text_node(label_text));
|
label.append_child(&document().create_text_node(label_text));
|
||||||
label.set_attribute("for", name).unwrap();
|
label.set_attribute("for", name).unwrap();
|
||||||
|
|
||||||
let inp: InputElement = document().create_element("input").unwrap().try_into().unwrap();
|
let inp: InputElement = document()
|
||||||
|
.create_element("input")
|
||||||
|
.unwrap()
|
||||||
|
.try_into()
|
||||||
|
.unwrap();
|
||||||
inp.set_attribute("name", name).unwrap();
|
inp.set_attribute("name", name).unwrap();
|
||||||
inp.set_attribute("id", name).unwrap();
|
inp.set_attribute("id", name).unwrap();
|
||||||
|
|
||||||
|
@ -228,8 +265,11 @@ fn make_input(label_text: &str, name: &'static str, form: &Element) -> InputElem
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_editable(tag: &'static str) -> Element {
|
fn make_editable(tag: &'static str) -> Element {
|
||||||
let elt = document().create_element(tag).expect("Couldn't create editable element");
|
let elt = document()
|
||||||
elt.set_attribute("contenteditable", "true").expect("Couldn't make element editable");
|
.create_element(tag)
|
||||||
|
.expect("Couldn't create editable element");
|
||||||
|
elt.set_attribute("contenteditable", "true")
|
||||||
|
.expect("Couldn't make element editable");
|
||||||
elt
|
elt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#![recursion_limit="128"]
|
#![recursion_limit = "128"]
|
||||||
#![feature(decl_macro, proc_macro_hygiene, try_trait)]
|
#![feature(decl_macro, proc_macro_hygiene, try_trait)]
|
||||||
|
|
||||||
extern crate gettext;
|
extern crate gettext;
|
||||||
|
@ -9,7 +9,7 @@ extern crate lazy_static;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate stdweb;
|
extern crate stdweb;
|
||||||
|
|
||||||
use stdweb::{web::{*, event::*}};
|
use stdweb::web::{event::*, *};
|
||||||
|
|
||||||
init_i18n!("plume-front", en, fr);
|
init_i18n!("plume-front", en, fr);
|
||||||
|
|
||||||
|
@ -20,9 +20,14 @@ compile_i18n!();
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref CATALOG: gettext::Catalog = {
|
static ref CATALOG: gettext::Catalog = {
|
||||||
let catalogs = include_i18n!();
|
let catalogs = include_i18n!();
|
||||||
let lang = js!{ return navigator.language }.into_string().unwrap();
|
let lang = js! { return navigator.language }.into_string().unwrap();
|
||||||
let lang = lang.splitn(2, '-').next().unwrap_or("en");
|
let lang = lang.splitn(2, '-').next().unwrap_or("en");
|
||||||
catalogs.iter().find(|(l, _)| l == &lang).unwrap_or(&catalogs[0]).clone().1
|
catalogs
|
||||||
|
.iter()
|
||||||
|
.find(|(l, _)| l == &lang)
|
||||||
|
.unwrap_or(&catalogs[0])
|
||||||
|
.clone()
|
||||||
|
.1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +35,8 @@ fn main() {
|
||||||
menu();
|
menu();
|
||||||
search();
|
search();
|
||||||
editor::init()
|
editor::init()
|
||||||
.map_err(|e| console!(error, format!("Editor error: {:?}", e))).ok();
|
.map_err(|e| console!(error, format!("Editor error: {:?}", e)))
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle menu on mobile device
|
/// Toggle menu on mobile device
|
||||||
|
@ -41,10 +47,14 @@ fn menu() {
|
||||||
if let Some(button) = document().get_element_by_id("menu") {
|
if let Some(button) = document().get_element_by_id("menu") {
|
||||||
if let Some(menu) = document().get_element_by_id("content") {
|
if let Some(menu) = document().get_element_by_id("content") {
|
||||||
button.add_event_listener(|_: ClickEvent| {
|
button.add_event_listener(|_: ClickEvent| {
|
||||||
document().get_element_by_id("menu").map(|menu| menu.class_list().add("show"));
|
document()
|
||||||
|
.get_element_by_id("menu")
|
||||||
|
.map(|menu| menu.class_list().add("show"));
|
||||||
});
|
});
|
||||||
menu.add_event_listener(|_: ClickEvent| {
|
menu.add_event_listener(|_: ClickEvent| {
|
||||||
document().get_element_by_id("menu").map(|menu| menu.class_list().remove("show"));
|
document()
|
||||||
|
.get_element_by_id("menu")
|
||||||
|
.map(|menu| menu.class_list().remove("show"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,18 +64,21 @@ fn menu() {
|
||||||
fn search() {
|
fn search() {
|
||||||
if let Some(form) = document().get_element_by_id("form") {
|
if let Some(form) = document().get_element_by_id("form") {
|
||||||
form.add_event_listener(|_: SubmitEvent| {
|
form.add_event_listener(|_: SubmitEvent| {
|
||||||
document().query_selector_all("#form input").map(|inputs| {
|
document()
|
||||||
for input in inputs {
|
.query_selector_all("#form input")
|
||||||
js! {
|
.map(|inputs| {
|
||||||
if (@{&input}.name === "") {
|
for input in inputs {
|
||||||
@{&input}.name = @{&input}.id
|
js! {
|
||||||
}
|
if (@{&input}.name === "") {
|
||||||
if (@{&input}.name && !@{&input}.value) {
|
@{&input}.name = @{&input}.id
|
||||||
@{&input}.name = "";
|
}
|
||||||
|
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 mut parsed_header = headers[0].split(' ');
|
||||||
let auth_type = parsed_header.next()
|
let auth_type = parsed_header.next().map_or_else(
|
||||||
.map_or_else(|| Outcome::Failure((Status::BadRequest, TokenError::NoType)), Outcome::Success)?;
|
|| Outcome::Failure((Status::BadRequest, TokenError::NoType)),
|
||||||
let val = parsed_header.next()
|
Outcome::Success,
|
||||||
.map_or_else(|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)), Outcome::Success)?;
|
)?;
|
||||||
|
let val = parsed_header.next().map_or_else(
|
||||||
|
|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)),
|
||||||
|
Outcome::Success,
|
||||||
|
)?;
|
||||||
|
|
||||||
if auth_type == "Bearer" {
|
if auth_type == "Bearer" {
|
||||||
let conn = request.guard::<DbConn>().map_failure(|_| (Status::InternalServerError, TokenError::DbError))?;
|
let conn = request
|
||||||
|
.guard::<DbConn>()
|
||||||
|
.map_failure(|_| (Status::InternalServerError, TokenError::DbError))?;
|
||||||
if let Ok(token) = ApiToken::find_by_value(&*conn, val) {
|
if let Ok(token) = ApiToken::find_by_value(&*conn, val) {
|
||||||
return Outcome::Success(token);
|
return Outcome::Success(token);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
use plume_api::apps::AppEndpoint;
|
use plume_api::apps::AppEndpoint;
|
||||||
use plume_common::utils::random_hex;
|
use plume_common::utils::random_hex;
|
||||||
use schema::apps;
|
use schema::apps;
|
||||||
use {Connection, Error, Result, ApiResult};
|
use {ApiResult, Connection, Error, Result};
|
||||||
|
|
||||||
#[derive(Clone, Queryable)]
|
#[derive(Clone, Queryable)]
|
||||||
pub struct App {
|
pub struct App {
|
||||||
|
@ -52,7 +52,8 @@ impl Provider<Connection> for App {
|
||||||
redirect_uri: data.redirect_uri,
|
redirect_uri: data.redirect_uri,
|
||||||
website: data.website,
|
website: data.website,
|
||||||
},
|
},
|
||||||
).map_err(|_| ApiError::NotFound("Couldn't register app".into()))?;
|
)
|
||||||
|
.map_err(|_| ApiError::NotFound("Couldn't register app".into()))?;
|
||||||
|
|
||||||
Ok(AppEndpoint {
|
Ok(AppEndpoint {
|
||||||
id: Some(app.id),
|
id: Some(app.id),
|
||||||
|
|
|
@ -26,7 +26,7 @@ use safe_string::SafeString;
|
||||||
use schema::blogs;
|
use schema::blogs;
|
||||||
use search::Searcher;
|
use search::Searcher;
|
||||||
use users::User;
|
use users::User;
|
||||||
use {Connection, BASE_URL, USE_HTTPS, Error, Result};
|
use {Connection, Error, Result, BASE_URL, USE_HTTPS};
|
||||||
|
|
||||||
pub type CustomGroup = CustomObject<ApSignature, Group>;
|
pub type CustomGroup = CustomObject<ApSignature, Group>;
|
||||||
|
|
||||||
|
@ -66,27 +66,15 @@ impl Blog {
|
||||||
insert!(blogs, NewBlog, |inserted, conn| {
|
insert!(blogs, NewBlog, |inserted, conn| {
|
||||||
let instance = inserted.get_instance(conn)?;
|
let instance = inserted.get_instance(conn)?;
|
||||||
if inserted.outbox_url.is_empty() {
|
if inserted.outbox_url.is_empty() {
|
||||||
inserted.outbox_url = instance.compute_box(
|
inserted.outbox_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, "outbox");
|
||||||
BLOG_PREFIX,
|
|
||||||
&inserted.actor_id,
|
|
||||||
"outbox",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if inserted.inbox_url.is_empty() {
|
if inserted.inbox_url.is_empty() {
|
||||||
inserted.inbox_url = instance.compute_box(
|
inserted.inbox_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, "inbox");
|
||||||
BLOG_PREFIX,
|
|
||||||
&inserted.actor_id,
|
|
||||||
"inbox",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if inserted.ap_url.is_empty() {
|
if inserted.ap_url.is_empty() {
|
||||||
inserted.ap_url = instance.compute_box(
|
inserted.ap_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, "");
|
||||||
BLOG_PREFIX,
|
|
||||||
&inserted.actor_id,
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if inserted.fqn.is_empty() {
|
if inserted.fqn.is_empty() {
|
||||||
|
@ -154,16 +142,12 @@ impl Blog {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_from_webfinger(conn: &Connection, acct: &str) -> Result<Blog> {
|
fn fetch_from_webfinger(conn: &Connection, acct: &str) -> Result<Blog> {
|
||||||
resolve(acct.to_owned(), *USE_HTTPS)?.links
|
resolve(acct.to_owned(), *USE_HTTPS)?
|
||||||
|
.links
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
|
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
|
||||||
.ok_or(Error::Webfinger)
|
.ok_or(Error::Webfinger)
|
||||||
.and_then(|l| {
|
.and_then(|l| Blog::fetch_from_url(conn, &l.href?))
|
||||||
Blog::fetch_from_url(
|
|
||||||
conn,
|
|
||||||
&l.href?
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_from_url(conn: &Connection, url: &str) -> Result<Blog> {
|
fn fetch_from_url(conn: &Connection, url: &str) -> Result<Blog> {
|
||||||
|
@ -181,20 +165,14 @@ impl Blog {
|
||||||
.send()?;
|
.send()?;
|
||||||
|
|
||||||
let text = &res.text()?;
|
let text = &res.text()?;
|
||||||
let ap_sign: ApSignature =
|
let ap_sign: ApSignature = serde_json::from_str(text)?;
|
||||||
serde_json::from_str(text)?;
|
let mut json: CustomGroup = serde_json::from_str(text)?;
|
||||||
let mut json: CustomGroup =
|
|
||||||
serde_json::from_str(text)?;
|
|
||||||
json.custom_props = ap_sign; // without this workaround, publicKey is not correctly deserialized
|
json.custom_props = ap_sign; // without this workaround, publicKey is not correctly deserialized
|
||||||
Blog::from_activity(
|
Blog::from_activity(conn, &json, Url::parse(url)?.host_str()?)
|
||||||
conn,
|
|
||||||
&json,
|
|
||||||
Url::parse(url)?.host_str()?,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_activity(conn: &Connection, acct: &CustomGroup, inst: &str) -> Result<Blog> {
|
fn from_activity(conn: &Connection, acct: &CustomGroup, inst: &str) -> Result<Blog> {
|
||||||
let instance = Instance::find_by_domain(conn, inst).or_else(|_|
|
let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
|
||||||
Instance::insert(
|
Instance::insert(
|
||||||
conn,
|
conn,
|
||||||
NewInstance {
|
NewInstance {
|
||||||
|
@ -210,35 +188,17 @@ impl Blog {
|
||||||
long_description_html: String::new(),
|
long_description_html: String::new(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
)?;
|
})?;
|
||||||
Blog::insert(
|
Blog::insert(
|
||||||
conn,
|
conn,
|
||||||
NewBlog {
|
NewBlog {
|
||||||
actor_id: acct
|
actor_id: acct.object.ap_actor_props.preferred_username_string()?,
|
||||||
.object
|
title: acct.object.object_props.name_string()?,
|
||||||
.ap_actor_props
|
outbox_url: acct.object.ap_actor_props.outbox_string()?,
|
||||||
.preferred_username_string()?,
|
inbox_url: acct.object.ap_actor_props.inbox_string()?,
|
||||||
title: acct
|
summary: acct.object.object_props.summary_string()?,
|
||||||
.object
|
|
||||||
.object_props
|
|
||||||
.name_string()?,
|
|
||||||
outbox_url: acct
|
|
||||||
.object
|
|
||||||
.ap_actor_props
|
|
||||||
.outbox_string()?,
|
|
||||||
inbox_url: acct
|
|
||||||
.object
|
|
||||||
.ap_actor_props
|
|
||||||
.inbox_string()?,
|
|
||||||
summary: acct
|
|
||||||
.object
|
|
||||||
.object_props
|
|
||||||
.summary_string()?,
|
|
||||||
instance_id: instance.id,
|
instance_id: instance.id,
|
||||||
ap_url: acct
|
ap_url: acct.object.object_props.id_string()?,
|
||||||
.object
|
|
||||||
.object_props
|
|
||||||
.id_string()?,
|
|
||||||
public_key: acct
|
public_key: acct
|
||||||
.custom_props
|
.custom_props
|
||||||
.public_key_publickey()?
|
.public_key_publickey()?
|
||||||
|
@ -252,27 +212,20 @@ impl Blog {
|
||||||
let mut blog = Group::default();
|
let mut blog = Group::default();
|
||||||
blog.ap_actor_props
|
blog.ap_actor_props
|
||||||
.set_preferred_username_string(self.actor_id.clone())?;
|
.set_preferred_username_string(self.actor_id.clone())?;
|
||||||
blog.object_props
|
blog.object_props.set_name_string(self.title.clone())?;
|
||||||
.set_name_string(self.title.clone())?;
|
|
||||||
blog.ap_actor_props
|
blog.ap_actor_props
|
||||||
.set_outbox_string(self.outbox_url.clone())?;
|
.set_outbox_string(self.outbox_url.clone())?;
|
||||||
blog.ap_actor_props
|
blog.ap_actor_props
|
||||||
.set_inbox_string(self.inbox_url.clone())?;
|
.set_inbox_string(self.inbox_url.clone())?;
|
||||||
blog.object_props
|
blog.object_props.set_summary_string(self.summary.clone())?;
|
||||||
.set_summary_string(self.summary.clone())?;
|
blog.object_props.set_id_string(self.ap_url.clone())?;
|
||||||
blog.object_props
|
|
||||||
.set_id_string(self.ap_url.clone())?;
|
|
||||||
|
|
||||||
let mut public_key = PublicKey::default();
|
let mut public_key = PublicKey::default();
|
||||||
public_key
|
public_key.set_id_string(format!("{}#main-key", self.ap_url))?;
|
||||||
.set_id_string(format!("{}#main-key", self.ap_url))?;
|
public_key.set_owner_string(self.ap_url.clone())?;
|
||||||
public_key
|
public_key.set_public_key_pem_string(self.public_key.clone())?;
|
||||||
.set_owner_string(self.ap_url.clone())?;
|
|
||||||
public_key
|
|
||||||
.set_public_key_pem_string(self.public_key.clone())?;
|
|
||||||
let mut ap_signature = ApSignature::default();
|
let mut ap_signature = ApSignature::default();
|
||||||
ap_signature
|
ap_signature.set_public_key_publickey(public_key)?;
|
||||||
.set_public_key_publickey(public_key)?;
|
|
||||||
|
|
||||||
Ok(CustomGroup::new(blog, ap_signature))
|
Ok(CustomGroup::new(blog, ap_signature))
|
||||||
}
|
}
|
||||||
|
@ -290,13 +243,10 @@ impl Blog {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_keypair(&self) -> Result<PKey<Private>> {
|
pub fn get_keypair(&self) -> Result<PKey<Private>> {
|
||||||
PKey::from_rsa(
|
PKey::from_rsa(Rsa::private_key_from_pem(
|
||||||
Rsa::private_key_from_pem(
|
self.private_key.clone()?.as_ref(),
|
||||||
self.private_key
|
)?)
|
||||||
.clone()?
|
.map_err(Error::from)
|
||||||
.as_ref(),
|
|
||||||
)?,
|
|
||||||
).map_err(Error::from)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn webfinger(&self, conn: &Connection) -> Result<Webfinger> {
|
pub fn webfinger(&self, conn: &Connection) -> Result<Webfinger> {
|
||||||
|
@ -386,25 +336,16 @@ impl sign::Signer for Blog {
|
||||||
|
|
||||||
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
|
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
|
||||||
let key = self.get_keypair()?;
|
let key = self.get_keypair()?;
|
||||||
let mut signer =
|
let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
|
||||||
Signer::new(MessageDigest::sha256(), &key)?;
|
signer.update(to_sign.as_bytes())?;
|
||||||
signer
|
signer.sign_to_vec().map_err(Error::from)
|
||||||
.update(to_sign.as_bytes())?;
|
|
||||||
signer
|
|
||||||
.sign_to_vec()
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
|
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
|
||||||
let key = PKey::from_rsa(
|
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
|
||||||
Rsa::public_key_from_pem(self.public_key.as_ref())?
|
|
||||||
)?;
|
|
||||||
let mut verifier = Verifier::new(MessageDigest::sha256(), &key)?;
|
let mut verifier = Verifier::new(MessageDigest::sha256(), &key)?;
|
||||||
verifier
|
verifier.update(data.as_bytes())?;
|
||||||
.update(data.as_bytes())?;
|
verifier.verify(&signature).map_err(Error::from)
|
||||||
verifier
|
|
||||||
.verify(&signature)
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -434,32 +375,47 @@ pub(crate) mod tests {
|
||||||
use blog_authors::*;
|
use blog_authors::*;
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
use instance::tests as instance_tests;
|
use instance::tests as instance_tests;
|
||||||
|
use search::tests::get_searcher;
|
||||||
use tests::db;
|
use tests::db;
|
||||||
use users::tests as usersTests;
|
use users::tests as usersTests;
|
||||||
use search::tests::get_searcher;
|
|
||||||
use Connection as Conn;
|
use Connection as Conn;
|
||||||
|
|
||||||
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Blog>) {
|
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Blog>) {
|
||||||
instance_tests::fill_database(conn);
|
instance_tests::fill_database(conn);
|
||||||
let users = usersTests::fill_database(conn);
|
let users = usersTests::fill_database(conn);
|
||||||
let blog1 = Blog::insert(conn, NewBlog::new_local(
|
let blog1 = Blog::insert(
|
||||||
"BlogName".to_owned(),
|
conn,
|
||||||
"Blog name".to_owned(),
|
NewBlog::new_local(
|
||||||
"This is a small blog".to_owned(),
|
"BlogName".to_owned(),
|
||||||
Instance::get_local(conn).unwrap().id
|
"Blog name".to_owned(),
|
||||||
).unwrap()).unwrap();
|
"This is a small blog".to_owned(),
|
||||||
let blog2 = Blog::insert(conn, NewBlog::new_local(
|
Instance::get_local(conn).unwrap().id,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let blog2 = Blog::insert(
|
||||||
|
conn,
|
||||||
|
NewBlog::new_local(
|
||||||
"MyBlog".to_owned(),
|
"MyBlog".to_owned(),
|
||||||
"My blog".to_owned(),
|
"My blog".to_owned(),
|
||||||
"Welcome to my blog".to_owned(),
|
"Welcome to my blog".to_owned(),
|
||||||
Instance::get_local(conn).unwrap().id
|
Instance::get_local(conn).unwrap().id,
|
||||||
).unwrap()).unwrap();
|
)
|
||||||
let blog3 = Blog::insert(conn, NewBlog::new_local(
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let blog3 = Blog::insert(
|
||||||
|
conn,
|
||||||
|
NewBlog::new_local(
|
||||||
"WhyILikePlume".to_owned(),
|
"WhyILikePlume".to_owned(),
|
||||||
"Why I like Plume".to_owned(),
|
"Why I like Plume".to_owned(),
|
||||||
"In this blog I will explay you why I like Plume so much".to_owned(),
|
"In this blog I will explay you why I like Plume so much".to_owned(),
|
||||||
Instance::get_local(conn).unwrap().id
|
Instance::get_local(conn).unwrap().id,
|
||||||
).unwrap()).unwrap();
|
)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
conn,
|
conn,
|
||||||
|
@ -468,7 +424,8 @@ pub(crate) mod tests {
|
||||||
author_id: users[0].id,
|
author_id: users[0].id,
|
||||||
is_owner: true,
|
is_owner: true,
|
||||||
},
|
},
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
conn,
|
conn,
|
||||||
|
@ -477,7 +434,8 @@ pub(crate) mod tests {
|
||||||
author_id: users[1].id,
|
author_id: users[1].id,
|
||||||
is_owner: false,
|
is_owner: false,
|
||||||
},
|
},
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
conn,
|
conn,
|
||||||
|
@ -486,7 +444,8 @@ pub(crate) mod tests {
|
||||||
author_id: users[1].id,
|
author_id: users[1].id,
|
||||||
is_owner: true,
|
is_owner: true,
|
||||||
},
|
},
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
conn,
|
conn,
|
||||||
|
@ -495,8 +454,9 @@ pub(crate) mod tests {
|
||||||
author_id: users[2].id,
|
author_id: users[2].id,
|
||||||
is_owner: true,
|
is_owner: true,
|
||||||
},
|
},
|
||||||
).unwrap();
|
)
|
||||||
(users, vec![ blog1, blog2, blog3 ])
|
.unwrap();
|
||||||
|
(users, vec![blog1, blog2, blog3])
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -511,11 +471,16 @@ pub(crate) mod tests {
|
||||||
"SomeName".to_owned(),
|
"SomeName".to_owned(),
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
"This is some blog".to_owned(),
|
"This is some blog".to_owned(),
|
||||||
Instance::get_local(conn).unwrap().id
|
Instance::get_local(conn).unwrap().id,
|
||||||
).unwrap(),
|
)
|
||||||
).unwrap();
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(blog.get_instance(conn).unwrap().id, Instance::get_local(conn).unwrap().id);
|
assert_eq!(
|
||||||
|
blog.get_instance(conn).unwrap().id,
|
||||||
|
Instance::get_local(conn).unwrap().id
|
||||||
|
);
|
||||||
// TODO add tests for remote instance
|
// TODO add tests for remote instance
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -535,18 +500,22 @@ pub(crate) mod tests {
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
"This is some blog".to_owned(),
|
"This is some blog".to_owned(),
|
||||||
Instance::get_local(conn).unwrap().id,
|
Instance::get_local(conn).unwrap().id,
|
||||||
).unwrap(),
|
)
|
||||||
).unwrap();
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let b2 = Blog::insert(
|
let b2 = Blog::insert(
|
||||||
conn,
|
conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
"Blog".to_owned(),
|
"Blog".to_owned(),
|
||||||
"Blog".to_owned(),
|
"Blog".to_owned(),
|
||||||
"I've named my blog Blog".to_owned(),
|
"I've named my blog Blog".to_owned(),
|
||||||
Instance::get_local(conn).unwrap().id
|
Instance::get_local(conn).unwrap().id,
|
||||||
).unwrap(),
|
)
|
||||||
).unwrap();
|
.unwrap(),
|
||||||
let blog = vec![ b1, b2 ];
|
)
|
||||||
|
.unwrap();
|
||||||
|
let blog = vec![b1, b2];
|
||||||
|
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
conn,
|
conn,
|
||||||
|
@ -555,7 +524,8 @@ pub(crate) mod tests {
|
||||||
author_id: user[0].id,
|
author_id: user[0].id,
|
||||||
is_owner: true,
|
is_owner: true,
|
||||||
},
|
},
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
conn,
|
conn,
|
||||||
|
@ -564,7 +534,8 @@ pub(crate) mod tests {
|
||||||
author_id: user[1].id,
|
author_id: user[1].id,
|
||||||
is_owner: false,
|
is_owner: false,
|
||||||
},
|
},
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
conn,
|
conn,
|
||||||
|
@ -573,53 +544,46 @@ pub(crate) mod tests {
|
||||||
author_id: user[0].id,
|
author_id: user[0].id,
|
||||||
is_owner: true,
|
is_owner: true,
|
||||||
},
|
},
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(blog[0]
|
||||||
blog[0]
|
.list_authors(conn)
|
||||||
.list_authors(conn).unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|a| a.id == user[0].id)
|
.any(|a| a.id == user[0].id));
|
||||||
);
|
assert!(blog[0]
|
||||||
assert!(
|
.list_authors(conn)
|
||||||
blog[0]
|
.unwrap()
|
||||||
.list_authors(conn).unwrap()
|
.iter()
|
||||||
.iter()
|
.any(|a| a.id == user[1].id));
|
||||||
.any(|a| a.id == user[1].id)
|
assert!(blog[1]
|
||||||
);
|
.list_authors(conn)
|
||||||
assert!(
|
.unwrap()
|
||||||
blog[1]
|
.iter()
|
||||||
.list_authors(conn).unwrap()
|
.any(|a| a.id == user[0].id));
|
||||||
.iter()
|
assert!(!blog[1]
|
||||||
.any(|a| a.id == user[0].id)
|
.list_authors(conn)
|
||||||
);
|
.unwrap()
|
||||||
assert!(
|
.iter()
|
||||||
!blog[1]
|
.any(|a| a.id == user[1].id));
|
||||||
.list_authors(conn).unwrap()
|
|
||||||
.iter()
|
|
||||||
.any(|a| a.id == user[1].id)
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(
|
assert!(Blog::find_for_author(conn, &user[0])
|
||||||
Blog::find_for_author(conn, &user[0]).unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|b| b.id == blog[0].id)
|
.any(|b| b.id == blog[0].id));
|
||||||
);
|
assert!(Blog::find_for_author(conn, &user[1])
|
||||||
assert!(
|
.unwrap()
|
||||||
Blog::find_for_author(conn, &user[1]).unwrap()
|
.iter()
|
||||||
.iter()
|
.any(|b| b.id == blog[0].id));
|
||||||
.any(|b| b.id == blog[0].id)
|
assert!(Blog::find_for_author(conn, &user[0])
|
||||||
);
|
.unwrap()
|
||||||
assert!(
|
.iter()
|
||||||
Blog::find_for_author(conn, &user[0]).unwrap()
|
.any(|b| b.id == blog[1].id));
|
||||||
.iter()
|
assert!(!Blog::find_for_author(conn, &user[1])
|
||||||
.any(|b| b.id == blog[1].id)
|
.unwrap()
|
||||||
);
|
.iter()
|
||||||
assert!(
|
.any(|b| b.id == blog[1].id));
|
||||||
!Blog::find_for_author(conn, &user[1]).unwrap()
|
|
||||||
.iter()
|
|
||||||
.any(|b| b.id == blog[1].id)
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
@ -638,13 +602,12 @@ pub(crate) mod tests {
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
"This is some blog".to_owned(),
|
"This is some blog".to_owned(),
|
||||||
Instance::get_local(conn).unwrap().id,
|
Instance::get_local(conn).unwrap().id,
|
||||||
).unwrap(),
|
)
|
||||||
).unwrap();
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(Blog::find_by_fqn(conn, "SomeName").unwrap().id, blog.id);
|
||||||
Blog::find_by_fqn(conn, "SomeName").unwrap().id,
|
|
||||||
blog.id
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
@ -663,8 +626,10 @@ pub(crate) mod tests {
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
"This is some blog".to_owned(),
|
"This is some blog".to_owned(),
|
||||||
Instance::get_local(conn).unwrap().id,
|
Instance::get_local(conn).unwrap().id,
|
||||||
).unwrap(),
|
)
|
||||||
).unwrap();
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(blog.fqn, "SomeName");
|
assert_eq!(blog.fqn, "SomeName");
|
||||||
|
|
||||||
|
@ -699,8 +664,10 @@ pub(crate) mod tests {
|
||||||
"Some name".to_owned(),
|
"Some name".to_owned(),
|
||||||
"This is some blog".to_owned(),
|
"This is some blog".to_owned(),
|
||||||
Instance::get_local(conn).unwrap().id,
|
Instance::get_local(conn).unwrap().id,
|
||||||
).unwrap(),
|
)
|
||||||
).unwrap();
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let b2 = Blog::insert(
|
let b2 = Blog::insert(
|
||||||
conn,
|
conn,
|
||||||
NewBlog::new_local(
|
NewBlog::new_local(
|
||||||
|
@ -708,9 +675,11 @@ pub(crate) mod tests {
|
||||||
"Blog".to_owned(),
|
"Blog".to_owned(),
|
||||||
"I've named my blog Blog".to_owned(),
|
"I've named my blog Blog".to_owned(),
|
||||||
Instance::get_local(conn).unwrap().id,
|
Instance::get_local(conn).unwrap().id,
|
||||||
).unwrap(),
|
)
|
||||||
).unwrap();
|
.unwrap(),
|
||||||
let blog = vec![ b1, b2 ];
|
)
|
||||||
|
.unwrap();
|
||||||
|
let blog = vec![b1, b2];
|
||||||
|
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
conn,
|
conn,
|
||||||
|
@ -719,7 +688,8 @@ pub(crate) mod tests {
|
||||||
author_id: user[0].id,
|
author_id: user[0].id,
|
||||||
is_owner: true,
|
is_owner: true,
|
||||||
},
|
},
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
conn,
|
conn,
|
||||||
|
@ -728,7 +698,8 @@ pub(crate) mod tests {
|
||||||
author_id: user[1].id,
|
author_id: user[1].id,
|
||||||
is_owner: false,
|
is_owner: false,
|
||||||
},
|
},
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
conn,
|
conn,
|
||||||
|
@ -737,7 +708,8 @@ pub(crate) mod tests {
|
||||||
author_id: user[0].id,
|
author_id: user[0].id,
|
||||||
is_owner: true,
|
is_owner: true,
|
||||||
},
|
},
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
user[0].delete(conn, &searcher).unwrap();
|
user[0].delete(conn, &searcher).unwrap();
|
||||||
assert!(Blog::get(conn, blog[0].id).is_ok());
|
assert!(Blog::get(conn, blog[0].id).is_ok());
|
||||||
|
|
|
@ -23,7 +23,8 @@ impl CommentSeers {
|
||||||
insert!(comment_seers, NewCommentSeers);
|
insert!(comment_seers, NewCommentSeers);
|
||||||
|
|
||||||
pub fn can_see(conn: &Connection, c: &Comment, u: &User) -> Result<bool> {
|
pub fn can_see(conn: &Connection, c: &Comment, u: &User) -> Result<bool> {
|
||||||
comment_seers::table.filter(comment_seers::comment_id.eq(c.id))
|
comment_seers::table
|
||||||
|
.filter(comment_seers::comment_id.eq(c.id))
|
||||||
.filter(comment_seers::user_id.eq(u.id))
|
.filter(comment_seers::user_id.eq(u.id))
|
||||||
.load::<CommentSeers>(conn)
|
.load::<CommentSeers>(conn)
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
|
|
|
@ -1,19 +1,23 @@
|
||||||
use activitypub::{activity::{Create, Delete}, link, object::{Note, Tombstone}};
|
use activitypub::{
|
||||||
|
activity::{Create, Delete},
|
||||||
|
link,
|
||||||
|
object::{Note, Tombstone},
|
||||||
|
};
|
||||||
use chrono::{self, NaiveDateTime};
|
use chrono::{self, NaiveDateTime};
|
||||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use comment_seers::{CommentSeers, NewCommentSeers};
|
||||||
use instance::Instance;
|
use instance::Instance;
|
||||||
use mentions::Mention;
|
use mentions::Mention;
|
||||||
use notifications::*;
|
use notifications::*;
|
||||||
use plume_common::activity_pub::{
|
use plume_common::activity_pub::{
|
||||||
inbox::{FromActivity, Notify, Deletable},
|
inbox::{Deletable, FromActivity, Notify},
|
||||||
Id, IntoId, PUBLIC_VISIBILTY,
|
Id, IntoId, PUBLIC_VISIBILTY,
|
||||||
};
|
};
|
||||||
use plume_common::utils;
|
use plume_common::utils;
|
||||||
use comment_seers::{CommentSeers, NewCommentSeers};
|
|
||||||
use posts::Post;
|
use posts::Post;
|
||||||
use safe_string::SafeString;
|
use safe_string::SafeString;
|
||||||
use schema::comments;
|
use schema::comments;
|
||||||
|
@ -50,7 +54,11 @@ pub struct NewComment {
|
||||||
impl Comment {
|
impl Comment {
|
||||||
insert!(comments, NewComment, |inserted, conn| {
|
insert!(comments, NewComment, |inserted, conn| {
|
||||||
if inserted.ap_url.is_none() {
|
if inserted.ap_url.is_none() {
|
||||||
inserted.ap_url = Some(format!("{}comment/{}", inserted.get_post(conn)?.ap_url, inserted.id));
|
inserted.ap_url = Some(format!(
|
||||||
|
"{}comment/{}",
|
||||||
|
inserted.get_post(conn)?.ap_url,
|
||||||
|
inserted.id
|
||||||
|
));
|
||||||
let _: Comment = inserted.save_changes(conn)?;
|
let _: Comment = inserted.save_changes(conn)?;
|
||||||
}
|
}
|
||||||
Ok(inserted)
|
Ok(inserted)
|
||||||
|
@ -80,20 +88,25 @@ impl Comment {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_responses(&self, conn: &Connection) -> Result<Vec<Comment>> {
|
pub fn get_responses(&self, conn: &Connection) -> Result<Vec<Comment>> {
|
||||||
comments::table.filter(comments::in_response_to_id.eq(self.id))
|
comments::table
|
||||||
|
.filter(comments::in_response_to_id.eq(self.id))
|
||||||
.load::<Comment>(conn)
|
.load::<Comment>(conn)
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn can_see(&self, conn: &Connection, user: Option<&User>) -> bool {
|
pub fn can_see(&self, conn: &Connection, user: Option<&User>) -> bool {
|
||||||
self.public_visibility ||
|
self.public_visibility
|
||||||
user.as_ref().map(|u| CommentSeers::can_see(conn, self, u).unwrap_or(false))
|
|| user
|
||||||
|
.as_ref()
|
||||||
|
.map(|u| CommentSeers::can_see(conn, self, u).unwrap_or(false))
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_activity(&self, conn: &Connection) -> Result<Note> {
|
pub fn to_activity(&self, conn: &Connection) -> Result<Note> {
|
||||||
let (html, mentions, _hashtags) = utils::md_to_html(self.content.get().as_ref(),
|
let (html, mentions, _hashtags) = utils::md_to_html(
|
||||||
&Instance::get_local(conn)?.public_domain);
|
self.content.get().as_ref(),
|
||||||
|
&Instance::get_local(conn)?.public_domain,
|
||||||
|
);
|
||||||
|
|
||||||
let author = User::get(conn, self.author_id)?;
|
let author = User::get(conn, self.author_id)?;
|
||||||
let mut note = Note::default();
|
let mut note = Note::default();
|
||||||
|
@ -103,8 +116,7 @@ impl Comment {
|
||||||
.set_id_string(self.ap_url.clone().unwrap_or_default())?;
|
.set_id_string(self.ap_url.clone().unwrap_or_default())?;
|
||||||
note.object_props
|
note.object_props
|
||||||
.set_summary_string(self.spoiler_text.clone())?;
|
.set_summary_string(self.spoiler_text.clone())?;
|
||||||
note.object_props
|
note.object_props.set_content_string(html)?;
|
||||||
.set_content_string(html)?;
|
|
||||||
note.object_props
|
note.object_props
|
||||||
.set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else(
|
.set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else(
|
||||||
|| Ok(Post::get(conn, self.post_id)?.ap_url),
|
|| Ok(Post::get(conn, self.post_id)?.ap_url),
|
||||||
|
@ -114,41 +126,28 @@ impl Comment {
|
||||||
.set_published_string(chrono::Utc::now().to_rfc3339())?;
|
.set_published_string(chrono::Utc::now().to_rfc3339())?;
|
||||||
note.object_props
|
note.object_props
|
||||||
.set_attributed_to_link(author.clone().into_id())?;
|
.set_attributed_to_link(author.clone().into_id())?;
|
||||||
note.object_props
|
note.object_props.set_to_link_vec(to.clone())?;
|
||||||
.set_to_link_vec(to.clone())?;
|
note.object_props.set_tag_link_vec(
|
||||||
note.object_props
|
mentions
|
||||||
.set_tag_link_vec(
|
.into_iter()
|
||||||
mentions
|
.filter_map(|m| Mention::build_activity(conn, &m).ok())
|
||||||
.into_iter()
|
.collect::<Vec<link::Mention>>(),
|
||||||
.filter_map(|m| Mention::build_activity(conn, &m).ok())
|
)?;
|
||||||
.collect::<Vec<link::Mention>>(),
|
|
||||||
)?;
|
|
||||||
Ok(note)
|
Ok(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_activity(&self, conn: &Connection) -> Result<Create> {
|
pub fn create_activity(&self, conn: &Connection) -> Result<Create> {
|
||||||
let author =
|
let author = User::get(conn, self.author_id)?;
|
||||||
User::get(conn, self.author_id)?;
|
|
||||||
|
|
||||||
let note = self.to_activity(conn)?;
|
let note = self.to_activity(conn)?;
|
||||||
let mut act = Create::default();
|
let mut act = Create::default();
|
||||||
act.create_props
|
act.create_props.set_actor_link(author.into_id())?;
|
||||||
.set_actor_link(author.into_id())?;
|
act.create_props.set_object_object(note.clone())?;
|
||||||
act.create_props
|
|
||||||
.set_object_object(note.clone())?;
|
|
||||||
act.object_props
|
act.object_props
|
||||||
.set_id_string(format!(
|
.set_id_string(format!("{}/activity", self.ap_url.clone()?,))?;
|
||||||
"{}/activity",
|
|
||||||
self.ap_url
|
|
||||||
.clone()?,
|
|
||||||
))?;
|
|
||||||
act.object_props
|
act.object_props
|
||||||
.set_to_link_vec(
|
.set_to_link_vec(note.object_props.to_link_vec::<Id>()?)?;
|
||||||
note.object_props
|
act.object_props.set_cc_link_vec::<Id>(vec![])?;
|
||||||
.to_link_vec::<Id>()?,
|
|
||||||
)?;
|
|
||||||
act.object_props
|
|
||||||
.set_cc_link_vec::<Id>(vec![])?;
|
|
||||||
Ok(act)
|
Ok(act)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,43 +157,39 @@ impl FromActivity<Note, Connection> for Comment {
|
||||||
|
|
||||||
fn from_activity(conn: &Connection, note: Note, actor: Id) -> Result<Comment> {
|
fn from_activity(conn: &Connection, note: Note, actor: Id) -> Result<Comment> {
|
||||||
let comm = {
|
let comm = {
|
||||||
let previous_url = note
|
let previous_url = note.object_props.in_reply_to.as_ref()?.as_str()?;
|
||||||
.object_props
|
|
||||||
.in_reply_to
|
|
||||||
.as_ref()?
|
|
||||||
.as_str()?;
|
|
||||||
let previous_comment = Comment::find_by_ap_url(conn, previous_url);
|
let previous_comment = Comment::find_by_ap_url(conn, previous_url);
|
||||||
|
|
||||||
let is_public = |v: &Option<serde_json::Value>| match v.as_ref().unwrap_or(&serde_json::Value::Null) {
|
let is_public = |v: &Option<serde_json::Value>| match v
|
||||||
serde_json::Value::Array(v) => v.iter().filter_map(serde_json::Value::as_str).any(|s| s==PUBLIC_VISIBILTY),
|
.as_ref()
|
||||||
|
.unwrap_or(&serde_json::Value::Null)
|
||||||
|
{
|
||||||
|
serde_json::Value::Array(v) => v
|
||||||
|
.iter()
|
||||||
|
.filter_map(serde_json::Value::as_str)
|
||||||
|
.any(|s| s == PUBLIC_VISIBILTY),
|
||||||
serde_json::Value::String(s) => s == PUBLIC_VISIBILTY,
|
serde_json::Value::String(s) => s == PUBLIC_VISIBILTY,
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let public_visibility = is_public(¬e.object_props.to) ||
|
let public_visibility = is_public(¬e.object_props.to)
|
||||||
is_public(¬e.object_props.bto) ||
|
|| is_public(¬e.object_props.bto)
|
||||||
is_public(¬e.object_props.cc) ||
|
|| is_public(¬e.object_props.cc)
|
||||||
is_public(¬e.object_props.bcc);
|
|| is_public(¬e.object_props.bcc);
|
||||||
|
|
||||||
let comm = Comment::insert(
|
let comm = Comment::insert(
|
||||||
conn,
|
conn,
|
||||||
NewComment {
|
NewComment {
|
||||||
content: SafeString::new(
|
content: SafeString::new(¬e.object_props.content_string()?),
|
||||||
¬e
|
spoiler_text: note.object_props.summary_string().unwrap_or_default(),
|
||||||
.object_props
|
|
||||||
.content_string()?
|
|
||||||
),
|
|
||||||
spoiler_text: note
|
|
||||||
.object_props
|
|
||||||
.summary_string()
|
|
||||||
.unwrap_or_default(),
|
|
||||||
ap_url: note.object_props.id_string().ok(),
|
ap_url: note.object_props.id_string().ok(),
|
||||||
in_response_to_id: previous_comment.iter().map(|c| c.id).next(),
|
in_response_to_id: previous_comment.iter().map(|c| c.id).next(),
|
||||||
post_id: previous_comment.map(|c| c.post_id)
|
post_id: previous_comment.map(|c| c.post_id).or_else(|_| {
|
||||||
.or_else(|_| Ok(Post::find_by_ap_url(conn, previous_url)?.id) as Result<i32>)?,
|
Ok(Post::find_by_ap_url(conn, previous_url)?.id) as Result<i32>
|
||||||
|
})?,
|
||||||
author_id: User::from_url(conn, actor.as_ref())?.id,
|
author_id: User::from_url(conn, actor.as_ref())?.id,
|
||||||
sensitive: false, // "sensitive" is not a standard property, we need to think about how to support it with the activitypub crate
|
sensitive: false, // "sensitive" is not a standard property, we need to think about how to support it with the activitypub crate
|
||||||
public_visibility
|
public_visibility,
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
@ -204,13 +199,11 @@ impl FromActivity<Note, Connection> for Comment {
|
||||||
serde_json::from_value::<link::Mention>(tag)
|
serde_json::from_value::<link::Mention>(tag)
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
.and_then(|m| {
|
.and_then(|m| {
|
||||||
let author = &Post::get(conn, comm.post_id)?
|
let author = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0];
|
||||||
.get_authors(conn)?[0];
|
let not_author = m.link_props.href_string()? != author.ap_url.clone();
|
||||||
let not_author = m
|
Ok(Mention::from_activity(
|
||||||
.link_props
|
conn, &m, comm.id, false, not_author,
|
||||||
.href_string()?
|
)?)
|
||||||
!= author.ap_url.clone();
|
|
||||||
Ok(Mention::from_activity(conn, &m, comm.id, false, not_author)?)
|
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
@ -218,14 +211,21 @@ impl FromActivity<Note, Connection> for Comment {
|
||||||
comm
|
comm
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
if !comm.public_visibility {
|
if !comm.public_visibility {
|
||||||
let receivers_ap_url = |v: Option<serde_json::Value>| {
|
let receivers_ap_url = |v: Option<serde_json::Value>| {
|
||||||
let filter = |e: serde_json::Value| if let serde_json::Value::String(s) = e { Some(s) } else { None };
|
let filter = |e: serde_json::Value| {
|
||||||
|
if let serde_json::Value::String(s) = e {
|
||||||
|
Some(s)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
match v.unwrap_or(serde_json::Value::Null) {
|
match v.unwrap_or(serde_json::Value::Null) {
|
||||||
serde_json::Value::Array(v) => v,
|
serde_json::Value::Array(v) => v,
|
||||||
v => vec![v],
|
v => vec![v],
|
||||||
}.into_iter().filter_map(filter)
|
}
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(filter)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut note = note;
|
let mut note = note;
|
||||||
|
@ -235,25 +235,30 @@ impl FromActivity<Note, Connection> for Comment {
|
||||||
let bto = receivers_ap_url(note.object_props.bto.take());
|
let bto = receivers_ap_url(note.object_props.bto.take());
|
||||||
let bcc = receivers_ap_url(note.object_props.bcc.take());
|
let bcc = receivers_ap_url(note.object_props.bcc.take());
|
||||||
|
|
||||||
let receivers_ap_url = to.chain(cc).chain(bto).chain(bcc)
|
let receivers_ap_url = to
|
||||||
.collect::<HashSet<_>>()//remove duplicates (don't do a query more than once)
|
.chain(cc)
|
||||||
|
.chain(bto)
|
||||||
|
.chain(bcc)
|
||||||
|
.collect::<HashSet<_>>() //remove duplicates (don't do a query more than once)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|v| if let Ok(user) = User::from_url(conn,&v) {
|
.map(|v| {
|
||||||
vec![user]
|
if let Ok(user) = User::from_url(conn, &v) {
|
||||||
} else {
|
vec![user]
|
||||||
vec![]// TODO try to fetch collection
|
} else {
|
||||||
|
vec![] // TODO try to fetch collection
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.flatten()
|
.flatten()
|
||||||
.filter(|u| u.get_instance(conn).map(|i| i.local).unwrap_or(false))
|
.filter(|u| u.get_instance(conn).map(|i| i.local).unwrap_or(false))
|
||||||
.collect::<HashSet<User>>();//remove duplicates (prevent db error)
|
.collect::<HashSet<User>>(); //remove duplicates (prevent db error)
|
||||||
|
|
||||||
for user in &receivers_ap_url {
|
for user in &receivers_ap_url {
|
||||||
CommentSeers::insert(
|
CommentSeers::insert(
|
||||||
conn,
|
conn,
|
||||||
NewCommentSeers {
|
NewCommentSeers {
|
||||||
comment_id: comm.id,
|
comment_id: comm.id,
|
||||||
user_id: user.id
|
user_id: user.id,
|
||||||
}
|
},
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -288,7 +293,8 @@ pub struct CommentTree {
|
||||||
|
|
||||||
impl CommentTree {
|
impl CommentTree {
|
||||||
pub fn from_post(conn: &Connection, p: &Post, user: Option<&User>) -> Result<Vec<Self>> {
|
pub fn from_post(conn: &Connection, p: &Post, user: Option<&User>) -> Result<Vec<Self>> {
|
||||||
Ok(Comment::list_by_post(conn, p.id)?.into_iter()
|
Ok(Comment::list_by_post(conn, p.id)?
|
||||||
|
.into_iter()
|
||||||
.filter(|c| c.in_response_to_id.is_none())
|
.filter(|c| c.in_response_to_id.is_none())
|
||||||
.filter(|c| c.can_see(conn, user))
|
.filter(|c| c.can_see(conn, user))
|
||||||
.filter_map(|c| Self::from_comment(conn, c, user).ok())
|
.filter_map(|c| Self::from_comment(conn, c, user).ok())
|
||||||
|
@ -296,14 +302,13 @@ impl CommentTree {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_comment(conn: &Connection, comment: Comment, user: Option<&User>) -> Result<Self> {
|
pub fn from_comment(conn: &Connection, comment: Comment, user: Option<&User>) -> Result<Self> {
|
||||||
let responses = comment.get_responses(conn)?.into_iter()
|
let responses = comment
|
||||||
|
.get_responses(conn)?
|
||||||
|
.into_iter()
|
||||||
.filter(|c| c.can_see(conn, user))
|
.filter(|c| c.can_see(conn, user))
|
||||||
.filter_map(|c| Self::from_comment(conn, c, user).ok())
|
.filter_map(|c| Self::from_comment(conn, c, user).ok())
|
||||||
.collect();
|
.collect();
|
||||||
Ok(CommentTree {
|
Ok(CommentTree { comment, responses })
|
||||||
comment,
|
|
||||||
responses,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -316,11 +321,8 @@ impl<'a> Deletable<Connection, Delete> for Comment {
|
||||||
.set_actor_link(self.get_author(conn)?.into_id())?;
|
.set_actor_link(self.get_author(conn)?.into_id())?;
|
||||||
|
|
||||||
let mut tombstone = Tombstone::default();
|
let mut tombstone = Tombstone::default();
|
||||||
tombstone
|
tombstone.object_props.set_id_string(self.ap_url.clone()?)?;
|
||||||
.object_props
|
act.delete_props.set_object_object(tombstone)?;
|
||||||
.set_id_string(self.ap_url.clone()?)?;
|
|
||||||
act.delete_props
|
|
||||||
.set_object_object(tombstone)?;
|
|
||||||
|
|
||||||
act.object_props
|
act.object_props
|
||||||
.set_id_string(format!("{}#delete", self.ap_url.clone().unwrap()))?;
|
.set_id_string(format!("{}#delete", self.ap_url.clone().unwrap()))?;
|
||||||
|
@ -330,11 +332,11 @@ impl<'a> Deletable<Connection, Delete> for Comment {
|
||||||
for m in Mention::list_for_comment(&conn, self.id)? {
|
for m in Mention::list_for_comment(&conn, self.id)? {
|
||||||
m.delete(conn)?;
|
m.delete(conn)?;
|
||||||
}
|
}
|
||||||
diesel::update(comments::table).filter(comments::in_response_to_id.eq(self.id))
|
diesel::update(comments::table)
|
||||||
|
.filter(comments::in_response_to_id.eq(self.id))
|
||||||
.set(comments::in_response_to_id.eq(self.in_response_to_id))
|
.set(comments::in_response_to_id.eq(self.in_response_to_id))
|
||||||
.execute(conn)?;
|
.execute(conn)?;
|
||||||
diesel::delete(self)
|
diesel::delete(self).execute(conn)?;
|
||||||
.execute(conn)?;
|
|
||||||
Ok(act)
|
Ok(act)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
use diesel::{r2d2::{ConnectionManager, CustomizeConnection, Error as ConnError, Pool, PooledConnection}};
|
use diesel::r2d2::{
|
||||||
|
ConnectionManager, CustomizeConnection, Error as ConnError, Pool, PooledConnection,
|
||||||
|
};
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
use diesel::{dsl::sql_query, ConnectionError, RunQueryDsl};
|
use diesel::{dsl::sql_query, ConnectionError, RunQueryDsl};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
|
@ -47,8 +49,13 @@ pub struct PragmaForeignKey;
|
||||||
impl CustomizeConnection<Connection, ConnError> for PragmaForeignKey {
|
impl CustomizeConnection<Connection, ConnError> for PragmaForeignKey {
|
||||||
#[cfg(feature = "sqlite")] // will default to an empty function for postgres
|
#[cfg(feature = "sqlite")] // will default to an empty function for postgres
|
||||||
fn on_acquire(&self, conn: &mut Connection) -> Result<(), ConnError> {
|
fn on_acquire(&self, conn: &mut Connection) -> Result<(), ConnError> {
|
||||||
sql_query("PRAGMA foreign_keys = on;").execute(conn)
|
sql_query("PRAGMA foreign_keys = on;")
|
||||||
|
.execute(conn)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(|_| ConnError::ConnectionError(ConnectionError::BadConnection(String::from("PRAGMA foreign_keys = on failed"))))
|
.map_err(|_| {
|
||||||
|
ConnError::ConnectionError(ConnectionError::BadConnection(String::from(
|
||||||
|
"PRAGMA foreign_keys = on failed",
|
||||||
|
)))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ use plume_common::activity_pub::{
|
||||||
};
|
};
|
||||||
use schema::follows;
|
use schema::follows;
|
||||||
use users::User;
|
use users::User;
|
||||||
use {ap_url, Connection, BASE_URL, Error, Result};
|
use {ap_url, Connection, Error, Result, BASE_URL};
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Identifiable, Associations, AsChangeset)]
|
#[derive(Clone, Queryable, Identifiable, Associations, AsChangeset)]
|
||||||
#[belongs_to(User, foreign_key = "following_id")]
|
#[belongs_to(User, foreign_key = "following_id")]
|
||||||
|
@ -62,12 +62,9 @@ impl Follow {
|
||||||
.set_actor_link::<Id>(user.clone().into_id())?;
|
.set_actor_link::<Id>(user.clone().into_id())?;
|
||||||
act.follow_props
|
act.follow_props
|
||||||
.set_object_link::<Id>(target.clone().into_id())?;
|
.set_object_link::<Id>(target.clone().into_id())?;
|
||||||
act.object_props
|
act.object_props.set_id_string(self.ap_url.clone())?;
|
||||||
.set_id_string(self.ap_url.clone())?;
|
act.object_props.set_to_link(target.into_id())?;
|
||||||
act.object_props
|
act.object_props.set_cc_link_vec::<Id>(vec![])?;
|
||||||
.set_to_link(target.into_id())?;
|
|
||||||
act.object_props
|
|
||||||
.set_cc_link_vec::<Id>(vec![])?;
|
|
||||||
Ok(act)
|
Ok(act)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,21 +89,13 @@ impl Follow {
|
||||||
|
|
||||||
let mut accept = Accept::default();
|
let mut accept = Accept::default();
|
||||||
let accept_id = ap_url(&format!("{}/follow/{}/accept", BASE_URL.as_str(), &res.id));
|
let accept_id = ap_url(&format!("{}/follow/{}/accept", BASE_URL.as_str(), &res.id));
|
||||||
accept
|
accept.object_props.set_id_string(accept_id)?;
|
||||||
.object_props
|
accept.object_props.set_to_link(from.clone().into_id())?;
|
||||||
.set_id_string(accept_id)?;
|
accept.object_props.set_cc_link_vec::<Id>(vec![])?;
|
||||||
accept
|
|
||||||
.object_props
|
|
||||||
.set_to_link(from.clone().into_id())?;
|
|
||||||
accept
|
|
||||||
.object_props
|
|
||||||
.set_cc_link_vec::<Id>(vec![])?;
|
|
||||||
accept
|
accept
|
||||||
.accept_props
|
.accept_props
|
||||||
.set_actor_link::<Id>(target.clone().into_id())?;
|
.set_actor_link::<Id>(target.clone().into_id())?;
|
||||||
accept
|
accept.accept_props.set_object_object(follow)?;
|
||||||
.accept_props
|
|
||||||
.set_object_object(follow)?;
|
|
||||||
broadcast(&*target, accept, vec![from.clone()]);
|
broadcast(&*target, accept, vec![from.clone()]);
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
@ -120,29 +109,18 @@ impl FromActivity<FollowAct, Connection> for Follow {
|
||||||
.follow_props
|
.follow_props
|
||||||
.actor_link::<Id>()
|
.actor_link::<Id>()
|
||||||
.map(|l| l.into())
|
.map(|l| l.into())
|
||||||
.or_else(|_| Ok(follow
|
.or_else(|_| {
|
||||||
.follow_props
|
Ok(follow
|
||||||
.actor_object::<Person>()?
|
.follow_props
|
||||||
.object_props
|
.actor_object::<Person>()?
|
||||||
.id_string()?) as Result<String>)?;
|
.object_props
|
||||||
let from =
|
.id_string()?) as Result<String>
|
||||||
User::from_url(conn, &from_id)?;
|
})?;
|
||||||
match User::from_url(
|
let from = User::from_url(conn, &from_id)?;
|
||||||
conn,
|
match User::from_url(conn, follow.follow_props.object.as_str()?) {
|
||||||
follow
|
|
||||||
.follow_props
|
|
||||||
.object
|
|
||||||
.as_str()?,
|
|
||||||
) {
|
|
||||||
Ok(user) => Follow::accept_follow(conn, &from, &user, follow, from.id, user.id),
|
Ok(user) => Follow::accept_follow(conn, &from, &user, follow, from.id, user.id),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
let blog = Blog::from_url(
|
let blog = Blog::from_url(conn, follow.follow_props.object.as_str()?)?;
|
||||||
conn,
|
|
||||||
follow
|
|
||||||
.follow_props
|
|
||||||
.object
|
|
||||||
.as_str()?,
|
|
||||||
)?;
|
|
||||||
Follow::accept_follow(conn, &from, &blog, follow, from.id, blog.id)
|
Follow::accept_follow(conn, &from, &blog, follow, from.id, blog.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -160,7 +138,8 @@ impl Notify<Connection> for Follow {
|
||||||
object_id: self.id,
|
object_id: self.id,
|
||||||
user_id: self.following_id,
|
user_id: self.following_id,
|
||||||
},
|
},
|
||||||
).map(|_| ())
|
)
|
||||||
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,21 +147,16 @@ impl Deletable<Connection, Undo> for Follow {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn delete(&self, conn: &Connection) -> Result<Undo> {
|
fn delete(&self, conn: &Connection) -> Result<Undo> {
|
||||||
diesel::delete(self)
|
diesel::delete(self).execute(conn)?;
|
||||||
.execute(conn)?;
|
|
||||||
|
|
||||||
// delete associated notification if any
|
// delete associated notification if any
|
||||||
if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) {
|
if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) {
|
||||||
diesel::delete(¬if)
|
diesel::delete(¬if).execute(conn)?;
|
||||||
.execute(conn)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut undo = Undo::default();
|
let mut undo = Undo::default();
|
||||||
undo.undo_props
|
undo.undo_props
|
||||||
.set_actor_link(
|
.set_actor_link(User::get(conn, self.follower_id)?.into_id())?;
|
||||||
User::get(conn, self.follower_id)?
|
|
||||||
.into_id(),
|
|
||||||
)?;
|
|
||||||
undo.object_props
|
undo.object_props
|
||||||
.set_id_string(format!("{}/undo", self.ap_url))?;
|
.set_id_string(format!("{}/undo", self.ap_url))?;
|
||||||
undo.undo_props
|
undo.undo_props
|
||||||
|
@ -209,8 +183,8 @@ impl IntoId for Follow {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use diesel::Connection;
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use diesel::Connection;
|
||||||
use tests::db;
|
use tests::db;
|
||||||
use users::tests as user_tests;
|
use users::tests as user_tests;
|
||||||
|
|
||||||
|
@ -219,20 +193,31 @@ mod tests {
|
||||||
let conn = db();
|
let conn = db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let users = user_tests::fill_database(&conn);
|
let users = user_tests::fill_database(&conn);
|
||||||
let follow = Follow::insert(&conn, NewFollow {
|
let follow = Follow::insert(
|
||||||
follower_id: users[0].id,
|
&conn,
|
||||||
following_id: users[1].id,
|
NewFollow {
|
||||||
ap_url: String::new(),
|
follower_id: users[0].id,
|
||||||
}).expect("Couldn't insert new follow");
|
following_id: users[1].id,
|
||||||
assert_eq!(follow.ap_url, format!("https://{}/follows/{}", *BASE_URL, follow.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 {
|
let follow = Follow::insert(
|
||||||
follower_id: users[1].id,
|
&conn,
|
||||||
following_id: users[0].id,
|
NewFollow {
|
||||||
ap_url: String::from("https://some.url/"),
|
follower_id: users[1].id,
|
||||||
}).expect("Couldn't insert new follow");
|
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/"));
|
assert_eq!(follow.ap_url, String::from("https://some.url/"));
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ pub struct Instance {
|
||||||
pub open_registrations: bool,
|
pub open_registrations: bool,
|
||||||
pub short_description: SafeString,
|
pub short_description: SafeString,
|
||||||
pub long_description: SafeString,
|
pub long_description: SafeString,
|
||||||
pub default_license : String,
|
pub default_license: String,
|
||||||
pub long_description_html: SafeString,
|
pub long_description_html: SafeString,
|
||||||
pub short_description_html: SafeString,
|
pub short_description_html: SafeString,
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,8 @@ impl Instance {
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.load::<Instance>(conn)?
|
.load::<Instance>(conn)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.nth(0).ok_or(Error::NotFound)
|
.nth(0)
|
||||||
|
.ok_or(Error::NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_remotes(conn: &Connection) -> Result<Vec<Instance>> {
|
pub fn get_remotes(conn: &Connection) -> Result<Vec<Instance>> {
|
||||||
|
@ -109,12 +110,7 @@ impl Instance {
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compute_box(
|
pub fn compute_box(&self, prefix: &str, name: &str, box_name: &str) -> String {
|
||||||
&self,
|
|
||||||
prefix: &str,
|
|
||||||
name: &str,
|
|
||||||
box_name: &str,
|
|
||||||
) -> String {
|
|
||||||
ap_url(&format!(
|
ap_url(&format!(
|
||||||
"{instance}/{prefix}/{name}/{box_name}",
|
"{instance}/{prefix}/{name}/{box_name}",
|
||||||
instance = self.public_domain,
|
instance = self.public_domain,
|
||||||
|
@ -209,15 +205,16 @@ pub(crate) mod tests {
|
||||||
open_registrations: true,
|
open_registrations: true,
|
||||||
public_domain: "3plu.me".to_string(),
|
public_domain: "3plu.me".to_string(),
|
||||||
},
|
},
|
||||||
].into_iter()
|
]
|
||||||
.map(|inst| {
|
.into_iter()
|
||||||
(
|
.map(|inst| {
|
||||||
inst.clone(),
|
(
|
||||||
Instance::find_by_domain(conn, &inst.public_domain)
|
inst.clone(),
|
||||||
.unwrap_or_else(|_| Instance::insert(conn, inst).unwrap()),
|
Instance::find_by_domain(conn, &inst.public_domain)
|
||||||
)
|
.unwrap_or_else(|_| Instance::insert(conn, inst).unwrap()),
|
||||||
})
|
)
|
||||||
.collect()
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -244,8 +241,14 @@ pub(crate) mod tests {
|
||||||
public_domain
|
public_domain
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
assert_eq!(res.long_description_html.get(), &inserted.long_description_html);
|
assert_eq!(
|
||||||
assert_eq!(res.short_description_html.get(), &inserted.short_description_html);
|
res.long_description_html.get(),
|
||||||
|
&inserted.long_description_html
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
res.short_description_html.get(),
|
||||||
|
&inserted.short_description_html
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
@ -282,8 +285,14 @@ pub(crate) mod tests {
|
||||||
public_domain
|
public_domain
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
assert_eq!(&newinst.long_description_html, inst.long_description_html.get());
|
assert_eq!(
|
||||||
assert_eq!(&newinst.short_description_html, inst.short_description_html.get());
|
&newinst.long_description_html,
|
||||||
|
inst.long_description_html.get()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&newinst.short_description_html,
|
||||||
|
inst.short_description_html.get()
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
let page = Instance::page(conn, (0, 2)).unwrap();
|
let page = Instance::page(conn, (0, 2)).unwrap();
|
||||||
|
@ -292,7 +301,9 @@ pub(crate) mod tests {
|
||||||
let page2 = &page[1];
|
let page2 = &page[1];
|
||||||
assert!(page1.public_domain <= page2.public_domain);
|
assert!(page1.public_domain <= page2.public_domain);
|
||||||
|
|
||||||
let mut last_domaine: String = Instance::page(conn, (0, 1)).unwrap()[0].public_domain.clone();
|
let mut last_domaine: String = Instance::page(conn, (0, 1)).unwrap()[0]
|
||||||
|
.public_domain
|
||||||
|
.clone();
|
||||||
for i in 1..inserted.len() as i32 {
|
for i in 1..inserted.len() as i32 {
|
||||||
let page = Instance::page(conn, (i, i + 1)).unwrap();
|
let page = Instance::page(conn, (i, i + 1)).unwrap();
|
||||||
assert_eq!(page.len(), 1);
|
assert_eq!(page.len(), 1);
|
||||||
|
@ -326,11 +337,13 @@ pub(crate) mod tests {
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Instance::is_blocked(conn, &format!("https://{}/something", inst.public_domain)).unwrap(),
|
Instance::is_blocked(conn, &format!("https://{}/something", inst.public_domain))
|
||||||
|
.unwrap(),
|
||||||
inst.blocked
|
inst.blocked
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Instance::is_blocked(conn, &format!("https://{}a/something", inst.public_domain)).unwrap(),
|
Instance::is_blocked(conn, &format!("https://{}a/something", inst.public_domain))
|
||||||
|
.unwrap(),
|
||||||
Instance::find_by_domain(conn, &format!("{}a", inst.public_domain))
|
Instance::find_by_domain(conn, &format!("{}a", inst.public_domain))
|
||||||
.map(|inst| inst.blocked)
|
.map(|inst| inst.blocked)
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
|
@ -340,11 +353,13 @@ pub(crate) mod tests {
|
||||||
let inst = Instance::get(conn, inst.id).unwrap();
|
let inst = Instance::get(conn, inst.id).unwrap();
|
||||||
assert_eq!(inst.blocked, blocked);
|
assert_eq!(inst.blocked, blocked);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Instance::is_blocked(conn, &format!("https://{}/something", inst.public_domain)).unwrap(),
|
Instance::is_blocked(conn, &format!("https://{}/something", inst.public_domain))
|
||||||
|
.unwrap(),
|
||||||
inst.blocked
|
inst.blocked
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Instance::is_blocked(conn, &format!("https://{}a/something", inst.public_domain)).unwrap(),
|
Instance::is_blocked(conn, &format!("https://{}a/something", inst.public_domain))
|
||||||
|
.unwrap(),
|
||||||
Instance::find_by_domain(conn, &format!("{}a", inst.public_domain))
|
Instance::find_by_domain(conn, &format!("{}a", inst.public_domain))
|
||||||
.map(|inst| inst.blocked)
|
.map(|inst| inst.blocked)
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
|
@ -375,7 +390,8 @@ pub(crate) mod tests {
|
||||||
false,
|
false,
|
||||||
SafeString::new("[short](#link)"),
|
SafeString::new("[short](#link)"),
|
||||||
SafeString::new("[long_description](/with_link)"),
|
SafeString::new("[long_description](/with_link)"),
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
let inst = Instance::get(conn, inst.id).unwrap();
|
let inst = Instance::get(conn, inst.id).unwrap();
|
||||||
assert_eq!(inst.name, "NewName".to_owned());
|
assert_eq!(inst.name, "NewName".to_owned());
|
||||||
assert_eq!(inst.open_registrations, false);
|
assert_eq!(inst.open_registrations, false);
|
||||||
|
|
|
@ -292,8 +292,8 @@ static DB_NAME: &str = "plume_tests";
|
||||||
|
|
||||||
#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
|
#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref DATABASE_URL: String =
|
pub static ref DATABASE_URL: String = env::var("DATABASE_URL")
|
||||||
env::var("DATABASE_URL").unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME));
|
.unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "sqlite", not(feature = "postgres")))]
|
#[cfg(all(feature = "sqlite", not(feature = "postgres")))]
|
||||||
|
@ -336,7 +336,9 @@ mod tests {
|
||||||
Conn::establish(&*DATABASE_URL.as_str()).expect("Couldn't connect to the database");
|
Conn::establish(&*DATABASE_URL.as_str()).expect("Couldn't connect to the database");
|
||||||
embedded_migrations::run(&conn).expect("Couldn't run migrations");
|
embedded_migrations::run(&conn).expect("Couldn't run migrations");
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
sql_query("PRAGMA foreign_keys = on;").execute(&conn).expect("PRAGMA foreign_keys fail");
|
sql_query("PRAGMA foreign_keys = on;")
|
||||||
|
.execute(&conn)
|
||||||
|
.expect("PRAGMA foreign_keys fail");
|
||||||
conn
|
conn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -346,8 +348,8 @@ pub mod api_tokens;
|
||||||
pub mod apps;
|
pub mod apps;
|
||||||
pub mod blog_authors;
|
pub mod blog_authors;
|
||||||
pub mod blogs;
|
pub mod blogs;
|
||||||
pub mod comments;
|
|
||||||
pub mod comment_seers;
|
pub mod comment_seers;
|
||||||
|
pub mod comments;
|
||||||
pub mod db_conn;
|
pub mod db_conn;
|
||||||
pub mod follows;
|
pub mod follows;
|
||||||
pub mod headers;
|
pub mod headers;
|
||||||
|
@ -360,7 +362,7 @@ pub mod post_authors;
|
||||||
pub mod posts;
|
pub mod posts;
|
||||||
pub mod reshares;
|
pub mod reshares;
|
||||||
pub mod safe_string;
|
pub mod safe_string;
|
||||||
pub mod search;
|
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
|
pub mod search;
|
||||||
pub mod tags;
|
pub mod tags;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
|
|
|
@ -38,21 +38,13 @@ impl Like {
|
||||||
pub fn to_activity(&self, conn: &Connection) -> Result<activity::Like> {
|
pub fn to_activity(&self, conn: &Connection) -> Result<activity::Like> {
|
||||||
let mut act = activity::Like::default();
|
let mut act = activity::Like::default();
|
||||||
act.like_props
|
act.like_props
|
||||||
.set_actor_link(
|
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
|
||||||
User::get(conn, self.user_id)?
|
|
||||||
.into_id(),
|
|
||||||
)?;
|
|
||||||
act.like_props
|
act.like_props
|
||||||
.set_object_link(
|
.set_object_link(Post::get(conn, self.post_id)?.into_id())?;
|
||||||
Post::get(conn, self.post_id)?
|
|
||||||
.into_id(),
|
|
||||||
)?;
|
|
||||||
act.object_props
|
act.object_props
|
||||||
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
|
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
|
||||||
act.object_props
|
act.object_props.set_cc_link_vec::<Id>(vec![])?;
|
||||||
.set_cc_link_vec::<Id>(vec![])?;
|
act.object_props.set_id_string(self.ap_url.clone())?;
|
||||||
act.object_props
|
|
||||||
.set_id_string(self.ap_url.clone())?;
|
|
||||||
|
|
||||||
Ok(act)
|
Ok(act)
|
||||||
}
|
}
|
||||||
|
@ -62,18 +54,8 @@ impl FromActivity<activity::Like, Connection> for Like {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_activity(conn: &Connection, like: activity::Like, _actor: Id) -> Result<Like> {
|
fn from_activity(conn: &Connection, like: activity::Like, _actor: Id) -> Result<Like> {
|
||||||
let liker = User::from_url(
|
let liker = User::from_url(conn, like.like_props.actor.as_str()?)?;
|
||||||
conn,
|
let post = Post::find_by_ap_url(conn, like.like_props.object.as_str()?)?;
|
||||||
like.like_props
|
|
||||||
.actor
|
|
||||||
.as_str()?,
|
|
||||||
)?;
|
|
||||||
let post = Post::find_by_ap_url(
|
|
||||||
conn,
|
|
||||||
like.like_props
|
|
||||||
.object
|
|
||||||
.as_str()?,
|
|
||||||
)?;
|
|
||||||
let res = Like::insert(
|
let res = Like::insert(
|
||||||
conn,
|
conn,
|
||||||
NewLike {
|
NewLike {
|
||||||
|
@ -110,26 +92,22 @@ impl Deletable<Connection, activity::Undo> for Like {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn delete(&self, conn: &Connection) -> Result<activity::Undo> {
|
fn delete(&self, conn: &Connection) -> Result<activity::Undo> {
|
||||||
diesel::delete(self)
|
diesel::delete(self).execute(conn)?;
|
||||||
.execute(conn)?;
|
|
||||||
|
|
||||||
// delete associated notification if any
|
// delete associated notification if any
|
||||||
if let Ok(notif) = Notification::find(conn, notification_kind::LIKE, self.id) {
|
if let Ok(notif) = Notification::find(conn, notification_kind::LIKE, self.id) {
|
||||||
diesel::delete(¬if)
|
diesel::delete(¬if).execute(conn)?;
|
||||||
.execute(conn)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut act = activity::Undo::default();
|
let mut act = activity::Undo::default();
|
||||||
act.undo_props
|
act.undo_props
|
||||||
.set_actor_link(User::get(conn, self.user_id)?.into_id(),)?;
|
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
|
||||||
act.undo_props
|
act.undo_props.set_object_object(self.to_activity(conn)?)?;
|
||||||
.set_object_object(self.to_activity(conn)?)?;
|
|
||||||
act.object_props
|
act.object_props
|
||||||
.set_id_string(format!("{}#delete", self.ap_url))?;
|
.set_id_string(format!("{}#delete", self.ap_url))?;
|
||||||
act.object_props
|
act.object_props
|
||||||
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
|
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
|
||||||
act.object_props
|
act.object_props.set_cc_link_vec::<Id>(vec![])?;
|
||||||
.set_cc_link_vec::<Id>(vec![])?;
|
|
||||||
|
|
||||||
Ok(act)
|
Ok(act)
|
||||||
}
|
}
|
||||||
|
@ -151,7 +129,7 @@ impl NewLike {
|
||||||
NewLike {
|
NewLike {
|
||||||
post_id: p.id,
|
post_id: p.id,
|
||||||
user_id: u.id,
|
user_id: u.id,
|
||||||
ap_url
|
ap_url,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,12 +62,14 @@ impl Media {
|
||||||
list_by!(medias, for_user, owner_id as i32);
|
list_by!(medias, for_user, owner_id as i32);
|
||||||
|
|
||||||
pub fn list_all_medias(conn: &Connection) -> Result<Vec<Media>> {
|
pub fn list_all_medias(conn: &Connection) -> Result<Vec<Media>> {
|
||||||
medias::table
|
medias::table.load::<Media>(conn).map_err(Error::from)
|
||||||
.load::<Media>(conn)
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn page_for_user(conn: &Connection, user: &User, (min, max): (i32, i32)) -> Result<Vec<Media>> {
|
pub fn page_for_user(
|
||||||
|
conn: &Connection,
|
||||||
|
user: &User,
|
||||||
|
(min, max): (i32, i32),
|
||||||
|
) -> Result<Vec<Media>> {
|
||||||
medias::table
|
medias::table
|
||||||
.filter(medias::owner_id.eq(user.id))
|
.filter(medias::owner_id.eq(user.id))
|
||||||
.offset(i64::from(min))
|
.offset(i64::from(min))
|
||||||
|
@ -124,7 +126,9 @@ impl Media {
|
||||||
pub fn markdown(&self, conn: &Connection) -> Result<SafeString> {
|
pub fn markdown(&self, conn: &Connection) -> Result<SafeString> {
|
||||||
let url = self.url(conn)?;
|
let url = self.url(conn)?;
|
||||||
Ok(match self.category() {
|
Ok(match self.category() {
|
||||||
MediaCategory::Image => SafeString::new(&format!("![{}]({})", escape(&self.alt_text), url)),
|
MediaCategory::Image => {
|
||||||
|
SafeString::new(&format!("![{}]({})", escape(&self.alt_text), url))
|
||||||
|
}
|
||||||
MediaCategory::Audio | MediaCategory::Video => self.html(conn)?,
|
MediaCategory::Audio | MediaCategory::Video => self.html(conn)?,
|
||||||
MediaCategory::Unknown => SafeString::new(""),
|
MediaCategory::Unknown => SafeString::new(""),
|
||||||
})
|
})
|
||||||
|
@ -216,7 +220,8 @@ impl Media {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.next()?
|
.next()?
|
||||||
.as_ref(),
|
.as_ref(),
|
||||||
)?.id,
|
)?
|
||||||
|
.id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -249,37 +254,41 @@ pub(crate) mod tests {
|
||||||
let f2 = "static/media/2.mp3".to_owned();
|
let f2 = "static/media/2.mp3".to_owned();
|
||||||
fs::write(f1.clone(), []).unwrap();
|
fs::write(f1.clone(), []).unwrap();
|
||||||
fs::write(f2.clone(), []).unwrap();
|
fs::write(f2.clone(), []).unwrap();
|
||||||
(users, vec![
|
(
|
||||||
NewMedia {
|
users,
|
||||||
file_path: f1,
|
vec![
|
||||||
alt_text: "some alt".to_owned(),
|
NewMedia {
|
||||||
is_remote: false,
|
file_path: f1,
|
||||||
remote_url: None,
|
alt_text: "some alt".to_owned(),
|
||||||
sensitive: false,
|
is_remote: false,
|
||||||
content_warning: None,
|
remote_url: None,
|
||||||
owner_id: user_one,
|
sensitive: false,
|
||||||
},
|
content_warning: None,
|
||||||
NewMedia {
|
owner_id: user_one,
|
||||||
file_path: f2,
|
},
|
||||||
alt_text: "alt message".to_owned(),
|
NewMedia {
|
||||||
is_remote: false,
|
file_path: f2,
|
||||||
remote_url: None,
|
alt_text: "alt message".to_owned(),
|
||||||
sensitive: true,
|
is_remote: false,
|
||||||
content_warning: Some("Content warning".to_owned()),
|
remote_url: None,
|
||||||
owner_id: user_one,
|
sensitive: true,
|
||||||
},
|
content_warning: Some("Content warning".to_owned()),
|
||||||
NewMedia {
|
owner_id: user_one,
|
||||||
file_path: "".to_owned(),
|
},
|
||||||
alt_text: "another alt".to_owned(),
|
NewMedia {
|
||||||
is_remote: true,
|
file_path: "".to_owned(),
|
||||||
remote_url: Some("https://example.com/".to_owned()),
|
alt_text: "another alt".to_owned(),
|
||||||
sensitive: false,
|
is_remote: true,
|
||||||
content_warning: None,
|
remote_url: Some("https://example.com/".to_owned()),
|
||||||
owner_id: user_two,
|
sensitive: false,
|
||||||
},
|
content_warning: None,
|
||||||
].into_iter()
|
owner_id: user_two,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
.map(|nm| Media::insert(conn, nm).unwrap())
|
.map(|nm| Media::insert(conn, nm).unwrap())
|
||||||
.collect())
|
.collect(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn clean(conn: &Conn) {
|
pub(crate) fn clean(conn: &Conn) {
|
||||||
|
@ -311,7 +320,8 @@ pub(crate) mod tests {
|
||||||
content_warning: None,
|
content_warning: None,
|
||||||
owner_id: user,
|
owner_id: user,
|
||||||
},
|
},
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert!(Path::new(&path).exists());
|
assert!(Path::new(&path).exists());
|
||||||
media.delete(conn).unwrap();
|
media.delete(conn).unwrap();
|
||||||
|
@ -346,29 +356,26 @@ pub(crate) mod tests {
|
||||||
content_warning: None,
|
content_warning: None,
|
||||||
owner_id: u1.id,
|
owner_id: u1.id,
|
||||||
},
|
},
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(Media::for_user(conn, u1.id)
|
||||||
Media::for_user(conn, u1.id).unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|m| m.id == media.id)
|
.any(|m| m.id == media.id));
|
||||||
);
|
assert!(!Media::for_user(conn, u2.id)
|
||||||
assert!(
|
.unwrap()
|
||||||
!Media::for_user(conn, u2.id).unwrap()
|
.iter()
|
||||||
.iter()
|
.any(|m| m.id == media.id));
|
||||||
.any(|m| m.id == media.id)
|
|
||||||
);
|
|
||||||
media.set_owner(conn, u2).unwrap();
|
media.set_owner(conn, u2).unwrap();
|
||||||
assert!(
|
assert!(!Media::for_user(conn, u1.id)
|
||||||
!Media::for_user(conn, u1.id).unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|m| m.id == media.id)
|
.any(|m| m.id == media.id));
|
||||||
);
|
assert!(Media::for_user(conn, u2.id)
|
||||||
assert!(
|
.unwrap()
|
||||||
Media::for_user(conn, u2.id).unwrap()
|
.iter()
|
||||||
.iter()
|
.any(|m| m.id == media.id));
|
||||||
.any(|m| m.id == media.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
clean(conn);
|
clean(conn);
|
||||||
|
|
||||||
|
|
|
@ -37,11 +37,15 @@ impl Mention {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_post(&self, conn: &Connection) -> Result<Post> {
|
pub fn get_post(&self, conn: &Connection) -> Result<Post> {
|
||||||
self.post_id.ok_or(Error::NotFound).and_then(|id| Post::get(conn, id))
|
self.post_id
|
||||||
|
.ok_or(Error::NotFound)
|
||||||
|
.and_then(|id| Post::get(conn, id))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_comment(&self, conn: &Connection) -> Result<Comment> {
|
pub fn get_comment(&self, conn: &Connection) -> Result<Comment> {
|
||||||
self.comment_id.ok_or(Error::NotFound).and_then(|id| Comment::get(conn, id))
|
self.comment_id
|
||||||
|
.ok_or(Error::NotFound)
|
||||||
|
.and_then(|id| Comment::get(conn, id))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_user(&self, conn: &Connection) -> Result<User> {
|
pub fn get_user(&self, conn: &Connection) -> Result<User> {
|
||||||
|
@ -54,21 +58,15 @@ impl Mention {
|
||||||
pub fn build_activity(conn: &Connection, ment: &str) -> Result<link::Mention> {
|
pub fn build_activity(conn: &Connection, ment: &str) -> Result<link::Mention> {
|
||||||
let user = User::find_by_fqn(conn, ment)?;
|
let user = User::find_by_fqn(conn, ment)?;
|
||||||
let mut mention = link::Mention::default();
|
let mut mention = link::Mention::default();
|
||||||
mention
|
mention.link_props.set_href_string(user.ap_url)?;
|
||||||
.link_props
|
mention.link_props.set_name_string(format!("@{}", ment))?;
|
||||||
.set_href_string(user.ap_url)?;
|
|
||||||
mention
|
|
||||||
.link_props
|
|
||||||
.set_name_string(format!("@{}", ment))?;
|
|
||||||
Ok(mention)
|
Ok(mention)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_activity(&self, conn: &Connection) -> Result<link::Mention> {
|
pub fn to_activity(&self, conn: &Connection) -> Result<link::Mention> {
|
||||||
let user = self.get_mentioned(conn)?;
|
let user = self.get_mentioned(conn)?;
|
||||||
let mut mention = link::Mention::default();
|
let mut mention = link::Mention::default();
|
||||||
mention
|
mention.link_props.set_href_string(user.ap_url.clone())?;
|
||||||
.link_props
|
|
||||||
.set_href_string(user.ap_url.clone())?;
|
|
||||||
mention
|
mention
|
||||||
.link_props
|
.link_props
|
||||||
.set_name_string(format!("@{}", user.fqn))?;
|
.set_name_string(format!("@{}", user.fqn))?;
|
||||||
|
@ -141,6 +139,7 @@ impl Notify<Connection> for Mention {
|
||||||
object_id: self.id,
|
object_id: self.id,
|
||||||
user_id: m.id,
|
user_id: m.id,
|
||||||
},
|
},
|
||||||
).map(|_| ())
|
)
|
||||||
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,24 +80,40 @@ impl Notification {
|
||||||
|
|
||||||
pub fn get_url(&self, conn: &Connection) -> Option<String> {
|
pub fn get_url(&self, conn: &Connection) -> Option<String> {
|
||||||
match self.kind.as_ref() {
|
match self.kind.as_ref() {
|
||||||
notification_kind::COMMENT => self.get_post(conn).and_then(|p| Some(format!("{}#comment-{}", p.url(conn).ok()?, self.object_id))),
|
notification_kind::COMMENT => self
|
||||||
|
.get_post(conn)
|
||||||
|
.and_then(|p| Some(format!("{}#comment-{}", p.url(conn).ok()?, self.object_id))),
|
||||||
notification_kind::FOLLOW => Some(format!("/@/{}/", self.get_actor(conn).ok()?.fqn)),
|
notification_kind::FOLLOW => Some(format!("/@/{}/", self.get_actor(conn).ok()?.fqn)),
|
||||||
notification_kind::MENTION => Mention::get(conn, self.object_id).and_then(|mention|
|
notification_kind::MENTION => Mention::get(conn, self.object_id)
|
||||||
mention.get_post(conn).and_then(|p| p.url(conn))
|
.and_then(|mention| {
|
||||||
.or_else(|_| {
|
mention
|
||||||
let comment = mention.get_comment(conn)?;
|
.get_post(conn)
|
||||||
Ok(format!("{}#comment-{}", comment.get_post(conn)?.url(conn)?, comment.id))
|
.and_then(|p| p.url(conn))
|
||||||
})
|
.or_else(|_| {
|
||||||
).ok(),
|
let comment = mention.get_comment(conn)?;
|
||||||
|
Ok(format!(
|
||||||
|
"{}#comment-{}",
|
||||||
|
comment.get_post(conn)?.url(conn)?,
|
||||||
|
comment.id
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.ok(),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_post(&self, conn: &Connection) -> Option<Post> {
|
pub fn get_post(&self, conn: &Connection) -> Option<Post> {
|
||||||
match self.kind.as_ref() {
|
match self.kind.as_ref() {
|
||||||
notification_kind::COMMENT => Comment::get(conn, self.object_id).and_then(|comment| comment.get_post(conn)).ok(),
|
notification_kind::COMMENT => Comment::get(conn, self.object_id)
|
||||||
notification_kind::LIKE => Like::get(conn, self.object_id).and_then(|like| Post::get(conn, like.post_id)).ok(),
|
.and_then(|comment| comment.get_post(conn))
|
||||||
notification_kind::RESHARE => Reshare::get(conn, self.object_id).and_then(|reshare| reshare.get_post(conn)).ok(),
|
.ok(),
|
||||||
|
notification_kind::LIKE => Like::get(conn, self.object_id)
|
||||||
|
.and_then(|like| Post::get(conn, like.post_id))
|
||||||
|
.ok(),
|
||||||
|
notification_kind::RESHARE => Reshare::get(conn, self.object_id)
|
||||||
|
.and_then(|reshare| reshare.get_post(conn))
|
||||||
|
.ok(),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,7 +121,9 @@ impl Notification {
|
||||||
pub fn get_actor(&self, conn: &Connection) -> Result<User> {
|
pub fn get_actor(&self, conn: &Connection) -> Result<User> {
|
||||||
Ok(match self.kind.as_ref() {
|
Ok(match self.kind.as_ref() {
|
||||||
notification_kind::COMMENT => Comment::get(conn, self.object_id)?.get_author(conn)?,
|
notification_kind::COMMENT => Comment::get(conn, self.object_id)?.get_author(conn)?,
|
||||||
notification_kind::FOLLOW => User::get(conn, Follow::get(conn, self.object_id)?.follower_id)?,
|
notification_kind::FOLLOW => {
|
||||||
|
User::get(conn, Follow::get(conn, self.object_id)?.follower_id)?
|
||||||
|
}
|
||||||
notification_kind::LIKE => User::get(conn, Like::get(conn, self.object_id)?.user_id)?,
|
notification_kind::LIKE => User::get(conn, Like::get(conn, self.object_id)?.user_id)?,
|
||||||
notification_kind::MENTION => Mention::get(conn, self.object_id)?.get_user(conn)?,
|
notification_kind::MENTION => Mention::get(conn, self.object_id)?.get_user(conn)?,
|
||||||
notification_kind::RESHARE => Reshare::get(conn, self.object_id)?.get_user(conn)?,
|
notification_kind::RESHARE => Reshare::get(conn, self.object_id)?.get_user(conn)?,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use activitypub::{
|
use activitypub::{
|
||||||
CustomObject,
|
|
||||||
activity::{Create, Delete, Update},
|
activity::{Create, Delete, Update},
|
||||||
link,
|
link,
|
||||||
object::{Article, Image, Tombstone},
|
object::{Article, Image, Tombstone},
|
||||||
|
CustomObject,
|
||||||
};
|
};
|
||||||
use canapi::{Error as ApiError, Provider};
|
use canapi::{Error as ApiError, Provider};
|
||||||
use chrono::{NaiveDateTime, TimeZone, Utc};
|
use chrono::{NaiveDateTime, TimeZone, Utc};
|
||||||
|
@ -12,25 +12,26 @@ use scheduled_thread_pool::ScheduledThreadPool as Worker;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use plume_api::posts::PostEndpoint;
|
|
||||||
use plume_common::{
|
|
||||||
activity_pub::{
|
|
||||||
inbox::{Deletable, FromActivity},
|
|
||||||
broadcast, Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILTY,
|
|
||||||
},
|
|
||||||
utils::md_to_html,
|
|
||||||
};
|
|
||||||
use blogs::Blog;
|
use blogs::Blog;
|
||||||
use instance::Instance;
|
use instance::Instance;
|
||||||
use medias::Media;
|
use medias::Media;
|
||||||
use mentions::Mention;
|
use mentions::Mention;
|
||||||
|
use plume_api::posts::PostEndpoint;
|
||||||
|
use plume_common::{
|
||||||
|
activity_pub::{
|
||||||
|
broadcast,
|
||||||
|
inbox::{Deletable, FromActivity},
|
||||||
|
Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILTY,
|
||||||
|
},
|
||||||
|
utils::md_to_html,
|
||||||
|
};
|
||||||
use post_authors::*;
|
use post_authors::*;
|
||||||
use safe_string::SafeString;
|
use safe_string::SafeString;
|
||||||
use search::Searcher;
|
|
||||||
use schema::posts;
|
use schema::posts;
|
||||||
|
use search::Searcher;
|
||||||
use tags::*;
|
use tags::*;
|
||||||
use users::User;
|
use users::User;
|
||||||
use {ap_url, Connection, BASE_URL, Error, Result, ApiResult};
|
use {ap_url, ApiResult, Connection, Error, Result, BASE_URL};
|
||||||
|
|
||||||
pub type LicensedArticle = CustomObject<Licensed, Article>;
|
pub type LicensedArticle = CustomObject<Licensed, Article>;
|
||||||
|
|
||||||
|
@ -75,7 +76,11 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
|
||||||
id: i32,
|
id: i32,
|
||||||
) -> ApiResult<PostEndpoint> {
|
) -> ApiResult<PostEndpoint> {
|
||||||
if let Ok(post) = Post::get(conn, id) {
|
if let Ok(post) = Post::get(conn, id) {
|
||||||
if !post.published && !user_id.map(|u| post.is_author(conn, u).unwrap_or(false)).unwrap_or(false) {
|
if !post.published
|
||||||
|
&& !user_id
|
||||||
|
.map(|u| post.is_author(conn, u).unwrap_or(false))
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
return Err(ApiError::Authorization(
|
return Err(ApiError::Authorization(
|
||||||
"You are not authorized to access this post yet.".to_string(),
|
"You are not authorized to access this post yet.".to_string(),
|
||||||
));
|
));
|
||||||
|
@ -86,12 +91,23 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
|
||||||
subtitle: Some(post.subtitle.clone()),
|
subtitle: Some(post.subtitle.clone()),
|
||||||
content: Some(post.content.get().clone()),
|
content: Some(post.content.get().clone()),
|
||||||
source: Some(post.source.clone()),
|
source: Some(post.source.clone()),
|
||||||
author: Some(post.get_authors(conn).map_err(|_| ApiError::NotFound("Authors not found".into()))?[0].username.clone()),
|
author: Some(
|
||||||
|
post.get_authors(conn)
|
||||||
|
.map_err(|_| ApiError::NotFound("Authors not found".into()))?[0]
|
||||||
|
.username
|
||||||
|
.clone(),
|
||||||
|
),
|
||||||
blog_id: Some(post.blog_id),
|
blog_id: Some(post.blog_id),
|
||||||
published: Some(post.published),
|
published: Some(post.published),
|
||||||
creation_date: Some(post.creation_date.format("%Y-%m-%d").to_string()),
|
creation_date: Some(post.creation_date.format("%Y-%m-%d").to_string()),
|
||||||
license: Some(post.license.clone()),
|
license: Some(post.license.clone()),
|
||||||
tags: Some(Tag::for_post(conn, post.id).map_err(|_| ApiError::NotFound("Tags not found".into()))?.into_iter().map(|t| t.tag).collect()),
|
tags: Some(
|
||||||
|
Tag::for_post(conn, post.id)
|
||||||
|
.map_err(|_| ApiError::NotFound("Tags not found".into()))?
|
||||||
|
.into_iter()
|
||||||
|
.map(|t| t.tag)
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
cover_id: post.cover_id,
|
cover_id: post.cover_id,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -114,24 +130,39 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
|
||||||
query = query.filter(posts::content.eq(content));
|
query = query.filter(posts::content.eq(content));
|
||||||
}
|
}
|
||||||
|
|
||||||
query.get_results::<Post>(*conn).map(|ps| ps.into_iter()
|
query
|
||||||
.filter(|p| p.published || user_id.map(|u| p.is_author(conn, u).unwrap_or(false)).unwrap_or(false))
|
.get_results::<Post>(*conn)
|
||||||
.map(|p| PostEndpoint {
|
.map(|ps| {
|
||||||
id: Some(p.id),
|
ps.into_iter()
|
||||||
title: Some(p.title.clone()),
|
.filter(|p| {
|
||||||
subtitle: Some(p.subtitle.clone()),
|
p.published
|
||||||
content: Some(p.content.get().clone()),
|
|| user_id
|
||||||
source: Some(p.source.clone()),
|
.map(|u| p.is_author(conn, u).unwrap_or(false))
|
||||||
author: Some(p.get_authors(conn).unwrap_or_default()[0].username.clone()),
|
.unwrap_or(false)
|
||||||
blog_id: Some(p.blog_id),
|
})
|
||||||
published: Some(p.published),
|
.map(|p| PostEndpoint {
|
||||||
creation_date: Some(p.creation_date.format("%Y-%m-%d").to_string()),
|
id: Some(p.id),
|
||||||
license: Some(p.license.clone()),
|
title: Some(p.title.clone()),
|
||||||
tags: Some(Tag::for_post(conn, p.id).unwrap_or_else(|_| vec![]).into_iter().map(|t| t.tag).collect()),
|
subtitle: Some(p.subtitle.clone()),
|
||||||
cover_id: p.cover_id,
|
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(
|
fn update(
|
||||||
|
@ -142,11 +173,15 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete((conn, _worker, search, user_id): &(&Connection, &Worker, &Searcher, Option<i32>), id: i32) {
|
fn delete(
|
||||||
|
(conn, _worker, search, user_id): &(&Connection, &Worker, &Searcher, Option<i32>),
|
||||||
|
id: i32,
|
||||||
|
) {
|
||||||
let user_id = user_id.expect("Post as Provider::delete: not authenticated");
|
let user_id = user_id.expect("Post as Provider::delete: not authenticated");
|
||||||
if let Ok(post) = Post::get(conn, id) {
|
if let Ok(post) = Post::get(conn, id) {
|
||||||
if post.is_author(conn, user_id).unwrap_or(false) {
|
if post.is_author(conn, user_id).unwrap_or(false) {
|
||||||
post.delete(&(conn, search)).expect("Post as Provider::delete: delete error");
|
post.delete(&(conn, search))
|
||||||
|
.expect("Post as Provider::delete: delete error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -156,84 +191,124 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
|
||||||
query: PostEndpoint,
|
query: PostEndpoint,
|
||||||
) -> ApiResult<PostEndpoint> {
|
) -> ApiResult<PostEndpoint> {
|
||||||
if user_id.is_none() {
|
if user_id.is_none() {
|
||||||
return Err(ApiError::Authorization("You are not authorized to create new articles.".to_string()));
|
return Err(ApiError::Authorization(
|
||||||
|
"You are not authorized to create new articles.".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = query.title.clone().expect("No title for new post in API");
|
let title = query.title.clone().expect("No title for new post in API");
|
||||||
let slug = query.title.unwrap().to_kebab_case();
|
let slug = query.title.unwrap().to_kebab_case();
|
||||||
|
|
||||||
let date = query.creation_date.clone()
|
let date = query.creation_date.clone().and_then(|d| {
|
||||||
.and_then(|d| NaiveDateTime::parse_from_str(format!("{} 00:00:00", d).as_ref(), "%Y-%m-%d %H:%M:%S").ok());
|
NaiveDateTime::parse_from_str(format!("{} 00:00:00", d).as_ref(), "%Y-%m-%d %H:%M:%S")
|
||||||
|
.ok()
|
||||||
|
});
|
||||||
|
|
||||||
let domain = &Instance::get_local(&conn)
|
let domain = &Instance::get_local(&conn)
|
||||||
.map_err(|_| ApiError::NotFound("posts::update: Error getting local instance".into()))?
|
.map_err(|_| ApiError::NotFound("posts::update: Error getting local instance".into()))?
|
||||||
.public_domain;
|
.public_domain;
|
||||||
let (content, mentions, hashtags) = md_to_html(query.source.clone().unwrap_or_default().clone().as_ref(), domain);
|
let (content, mentions, hashtags) = md_to_html(
|
||||||
|
query.source.clone().unwrap_or_default().clone().as_ref(),
|
||||||
|
domain,
|
||||||
|
);
|
||||||
|
|
||||||
let author = User::get(conn, user_id.expect("<Post as Provider>::create: no user_id error"))
|
let author = User::get(
|
||||||
.map_err(|_| ApiError::NotFound("Author not found".into()))?;
|
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 {
|
let blog = match query.blog_id {
|
||||||
Some(x) => x,
|
Some(x) => x,
|
||||||
None => Blog::find_for_author(conn, &author).map_err(|_| ApiError::NotFound("No default blog".into()))?[0].id
|
None => {
|
||||||
|
Blog::find_for_author(conn, &author)
|
||||||
|
.map_err(|_| ApiError::NotFound("No default blog".into()))?[0]
|
||||||
|
.id
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if Post::find_by_slug(conn, &slug, blog).is_ok() {
|
if Post::find_by_slug(conn, &slug, blog).is_ok() {
|
||||||
// Not an actual authorization problem, but we have nothing better for now…
|
// Not an actual authorization problem, but we have nothing better for now…
|
||||||
// TODO: add another error variant to canapi and add it there
|
// TODO: add another error variant to canapi and add it there
|
||||||
return Err(ApiError::Authorization("A post with the same slug already exists".to_string()));
|
return Err(ApiError::Authorization(
|
||||||
|
"A post with the same slug already exists".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let post = Post::insert(conn, NewPost {
|
let post = Post::insert(
|
||||||
blog_id: blog,
|
conn,
|
||||||
slug,
|
NewPost {
|
||||||
title,
|
blog_id: blog,
|
||||||
content: SafeString::new(content.as_ref()),
|
slug,
|
||||||
published: query.published.unwrap_or(true),
|
title,
|
||||||
license: query.license.unwrap_or_else(|| Instance::get_local(conn)
|
content: SafeString::new(content.as_ref()),
|
||||||
.map(|i| i.default_license)
|
published: query.published.unwrap_or(true),
|
||||||
.unwrap_or_else(|_| String::from("CC-BY-SA"))),
|
license: query.license.unwrap_or_else(|| {
|
||||||
creation_date: date,
|
Instance::get_local(conn)
|
||||||
ap_url: String::new(),
|
.map(|i| i.default_license)
|
||||||
subtitle: query.subtitle.unwrap_or_default(),
|
.unwrap_or_else(|_| String::from("CC-BY-SA"))
|
||||||
source: query.source.expect("Post API::create: no source error"),
|
}),
|
||||||
cover_id: query.cover_id,
|
creation_date: date,
|
||||||
}, search).map_err(|_| ApiError::NotFound("Creation error".into()))?;
|
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 {
|
PostAuthor::insert(
|
||||||
author_id: author.id,
|
conn,
|
||||||
post_id: post.id
|
NewPostAuthor {
|
||||||
}).map_err(|_| ApiError::NotFound("Error saving authors".into()))?;
|
author_id: author.id,
|
||||||
|
post_id: post.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|_| ApiError::NotFound("Error saving authors".into()))?;
|
||||||
|
|
||||||
if let Some(tags) = query.tags {
|
if let Some(tags) = query.tags {
|
||||||
for tag in tags {
|
for tag in tags {
|
||||||
Tag::insert(conn, NewTag {
|
Tag::insert(
|
||||||
tag,
|
conn,
|
||||||
is_hashtag: false,
|
NewTag {
|
||||||
post_id: post.id
|
tag,
|
||||||
}).map_err(|_| ApiError::NotFound("Error saving tags".into()))?;
|
is_hashtag: false,
|
||||||
|
post_id: post.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|_| ApiError::NotFound("Error saving tags".into()))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for hashtag in hashtags {
|
for hashtag in hashtags {
|
||||||
Tag::insert(conn, NewTag {
|
Tag::insert(
|
||||||
tag: hashtag.to_camel_case(),
|
conn,
|
||||||
is_hashtag: true,
|
NewTag {
|
||||||
post_id: post.id
|
tag: hashtag.to_camel_case(),
|
||||||
}).map_err(|_| ApiError::NotFound("Error saving hashtags".into()))?;
|
is_hashtag: true,
|
||||||
|
post_id: post.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|_| ApiError::NotFound("Error saving hashtags".into()))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if post.published {
|
if post.published {
|
||||||
for m in mentions.into_iter() {
|
for m in mentions.into_iter() {
|
||||||
Mention::from_activity(
|
Mention::from_activity(
|
||||||
&*conn,
|
&*conn,
|
||||||
&Mention::build_activity(&*conn, &m).map_err(|_| ApiError::NotFound("Couldn't build mentions".into()))?,
|
&Mention::build_activity(&*conn, &m)
|
||||||
|
.map_err(|_| ApiError::NotFound("Couldn't build mentions".into()))?,
|
||||||
post.id,
|
post.id,
|
||||||
true,
|
true,
|
||||||
true
|
true,
|
||||||
).map_err(|_| ApiError::NotFound("Error saving mentions".into()))?;
|
)
|
||||||
|
.map_err(|_| ApiError::NotFound("Error saving mentions".into()))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let act = post.create_activity(&*conn).map_err(|_| ApiError::NotFound("Couldn't create activity".into()))?;
|
let act = post
|
||||||
let dest = User::one_by_instance(&*conn).map_err(|_| ApiError::NotFound("Couldn't list remote instances".into()))?;
|
.create_activity(&*conn)
|
||||||
|
.map_err(|_| ApiError::NotFound("Couldn't create activity".into()))?;
|
||||||
|
let dest = User::one_by_instance(&*conn)
|
||||||
|
.map_err(|_| ApiError::NotFound("Couldn't list remote instances".into()))?;
|
||||||
worker.execute(move || broadcast(&author, act, dest));
|
worker.execute(move || broadcast(&author, act, dest));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,12 +318,23 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
|
||||||
subtitle: Some(post.subtitle.clone()),
|
subtitle: Some(post.subtitle.clone()),
|
||||||
content: Some(post.content.get().clone()),
|
content: Some(post.content.get().clone()),
|
||||||
source: Some(post.source.clone()),
|
source: Some(post.source.clone()),
|
||||||
author: Some(post.get_authors(conn).map_err(|_| ApiError::NotFound("No authors".into()))?[0].username.clone()),
|
author: Some(
|
||||||
|
post.get_authors(conn)
|
||||||
|
.map_err(|_| ApiError::NotFound("No authors".into()))?[0]
|
||||||
|
.username
|
||||||
|
.clone(),
|
||||||
|
),
|
||||||
blog_id: Some(post.blog_id),
|
blog_id: Some(post.blog_id),
|
||||||
published: Some(post.published),
|
published: Some(post.published),
|
||||||
creation_date: Some(post.creation_date.format("%Y-%m-%d").to_string()),
|
creation_date: Some(post.creation_date.format("%Y-%m-%d").to_string()),
|
||||||
license: Some(post.license.clone()),
|
license: Some(post.license.clone()),
|
||||||
tags: Some(Tag::for_post(conn, post.id).map_err(|_| ApiError::NotFound("Tags not found".into()))?.into_iter().map(|t| t.tag).collect()),
|
tags: Some(
|
||||||
|
Tag::for_post(conn, post.id)
|
||||||
|
.map_err(|_| ApiError::NotFound("Tags not found".into()))?
|
||||||
|
.into_iter()
|
||||||
|
.map(|t| t.tag)
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
cover_id: post.cover_id,
|
cover_id: post.cover_id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -279,15 +365,17 @@ impl Post {
|
||||||
Ok(post)
|
Ok(post)
|
||||||
}
|
}
|
||||||
pub fn update(&self, conn: &Connection, searcher: &Searcher) -> Result<Self> {
|
pub fn update(&self, conn: &Connection, searcher: &Searcher) -> Result<Self> {
|
||||||
diesel::update(self)
|
diesel::update(self).set(self).execute(conn)?;
|
||||||
.set(self)
|
|
||||||
.execute(conn)?;
|
|
||||||
let post = Self::get(conn, self.id)?;
|
let post = Self::get(conn, self.id)?;
|
||||||
searcher.update_document(conn, &post)?;
|
searcher.update_document(conn, &post)?;
|
||||||
Ok(post)
|
Ok(post)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_by_tag(conn: &Connection, tag: String, (min, max): (i32, i32)) -> Result<Vec<Post>> {
|
pub fn list_by_tag(
|
||||||
|
conn: &Connection,
|
||||||
|
tag: String,
|
||||||
|
(min, max): (i32, i32),
|
||||||
|
) -> Result<Vec<Post>> {
|
||||||
use schema::tags;
|
use schema::tags;
|
||||||
|
|
||||||
let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id);
|
let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id);
|
||||||
|
@ -349,7 +437,11 @@ impl Post {
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_recents_for_author(conn: &Connection, author: &User, limit: i64) -> Result<Vec<Post>> {
|
pub fn get_recents_for_author(
|
||||||
|
conn: &Connection,
|
||||||
|
author: &User,
|
||||||
|
limit: i64,
|
||||||
|
) -> Result<Vec<Post>> {
|
||||||
use schema::post_authors;
|
use schema::post_authors;
|
||||||
|
|
||||||
let posts = PostAuthor::belonging_to(author).select(post_authors::post_id);
|
let posts = PostAuthor::belonging_to(author).select(post_authors::post_id);
|
||||||
|
@ -481,7 +573,8 @@ impl Post {
|
||||||
Ok(PostAuthor::belonging_to(self)
|
Ok(PostAuthor::belonging_to(self)
|
||||||
.filter(post_authors::author_id.eq(author_id))
|
.filter(post_authors::author_id.eq(author_id))
|
||||||
.count()
|
.count()
|
||||||
.get_result::<i64>(conn)? > 0)
|
.get_result::<i64>(conn)?
|
||||||
|
> 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_blog(&self, conn: &Connection) -> Result<Blog> {
|
pub fn get_blog(&self, conn: &Connection) -> Result<Blog> {
|
||||||
|
@ -529,7 +622,7 @@ impl Post {
|
||||||
|
|
||||||
pub fn to_activity(&self, conn: &Connection) -> Result<LicensedArticle> {
|
pub fn to_activity(&self, conn: &Connection) -> Result<LicensedArticle> {
|
||||||
let cc = self.get_receivers_urls(conn)?;
|
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)?
|
let mut mentions_json = Mention::list_for_post(conn, self.id)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -542,12 +635,8 @@ impl Post {
|
||||||
mentions_json.append(&mut tags_json);
|
mentions_json.append(&mut tags_json);
|
||||||
|
|
||||||
let mut article = Article::default();
|
let mut article = Article::default();
|
||||||
article
|
article.object_props.set_name_string(self.title.clone())?;
|
||||||
.object_props
|
article.object_props.set_id_string(self.ap_url.clone())?;
|
||||||
.set_name_string(self.title.clone())?;
|
|
||||||
article
|
|
||||||
.object_props
|
|
||||||
.set_id_string(self.ap_url.clone())?;
|
|
||||||
|
|
||||||
let mut authors = self
|
let mut authors = self
|
||||||
.get_authors(conn)?
|
.get_authors(conn)?
|
||||||
|
@ -561,12 +650,10 @@ impl Post {
|
||||||
article
|
article
|
||||||
.object_props
|
.object_props
|
||||||
.set_content_string(self.content.get().clone())?;
|
.set_content_string(self.content.get().clone())?;
|
||||||
article
|
article.ap_object_props.set_source_object(Source {
|
||||||
.ap_object_props
|
content: self.source.clone(),
|
||||||
.set_source_object(Source {
|
media_type: String::from("text/markdown"),
|
||||||
content: self.source.clone(),
|
})?;
|
||||||
media_type: String::from("text/markdown"),
|
|
||||||
})?;
|
|
||||||
article
|
article
|
||||||
.object_props
|
.object_props
|
||||||
.set_published_utctime(Utc.from_utc_datetime(&self.creation_date))?;
|
.set_published_utctime(Utc.from_utc_datetime(&self.creation_date))?;
|
||||||
|
@ -578,31 +665,20 @@ impl Post {
|
||||||
if let Some(media_id) = self.cover_id {
|
if let Some(media_id) = self.cover_id {
|
||||||
let media = Media::get(conn, media_id)?;
|
let media = Media::get(conn, media_id)?;
|
||||||
let mut cover = Image::default();
|
let mut cover = Image::default();
|
||||||
cover
|
cover.object_props.set_url_string(media.url(conn)?)?;
|
||||||
.object_props
|
|
||||||
.set_url_string(media.url(conn)?)?;
|
|
||||||
if media.sensitive {
|
if media.sensitive {
|
||||||
cover
|
cover
|
||||||
.object_props
|
.object_props
|
||||||
.set_summary_string(media.content_warning.unwrap_or_default())?;
|
.set_summary_string(media.content_warning.unwrap_or_default())?;
|
||||||
}
|
}
|
||||||
|
cover.object_props.set_content_string(media.alt_text)?;
|
||||||
cover
|
cover
|
||||||
.object_props
|
.object_props
|
||||||
.set_content_string(media.alt_text)?;
|
.set_attributed_to_link_vec(vec![User::get(conn, media.owner_id)?.into_id()])?;
|
||||||
cover
|
article.object_props.set_icon_object(cover)?;
|
||||||
.object_props
|
|
||||||
.set_attributed_to_link_vec(vec![
|
|
||||||
User::get(conn, media.owner_id)?
|
|
||||||
.into_id(),
|
|
||||||
])?;
|
|
||||||
article
|
|
||||||
.object_props
|
|
||||||
.set_icon_object(cover)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
article
|
article.object_props.set_url_string(self.ap_url.clone())?;
|
||||||
.object_props
|
|
||||||
.set_url_string(self.ap_url.clone())?;
|
|
||||||
article
|
article
|
||||||
.object_props
|
.object_props
|
||||||
.set_to_link_vec::<Id>(to.into_iter().map(Id::new).collect())?;
|
.set_to_link_vec::<Id>(to.into_iter().map(Id::new).collect())?;
|
||||||
|
@ -620,52 +696,39 @@ impl Post {
|
||||||
act.object_props
|
act.object_props
|
||||||
.set_id_string(format!("{}activity", self.ap_url))?;
|
.set_id_string(format!("{}activity", self.ap_url))?;
|
||||||
act.object_props
|
act.object_props
|
||||||
.set_to_link_vec::<Id>(
|
.set_to_link_vec::<Id>(article.object.object_props.to_link_vec()?)?;
|
||||||
article.object
|
|
||||||
.object_props
|
|
||||||
.to_link_vec()?,
|
|
||||||
)?;
|
|
||||||
act.object_props
|
act.object_props
|
||||||
.set_cc_link_vec::<Id>(
|
.set_cc_link_vec::<Id>(article.object.object_props.cc_link_vec()?)?;
|
||||||
article.object
|
|
||||||
.object_props
|
|
||||||
.cc_link_vec()?,
|
|
||||||
)?;
|
|
||||||
act.create_props
|
act.create_props
|
||||||
.set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?;
|
.set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?;
|
||||||
act.create_props
|
act.create_props.set_object_object(article)?;
|
||||||
.set_object_object(article)?;
|
|
||||||
Ok(act)
|
Ok(act)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_activity(&self, conn: &Connection) -> Result<Update> {
|
pub fn update_activity(&self, conn: &Connection) -> Result<Update> {
|
||||||
let article = self.to_activity(conn)?;
|
let article = self.to_activity(conn)?;
|
||||||
let mut act = Update::default();
|
let mut act = Update::default();
|
||||||
|
act.object_props.set_id_string(format!(
|
||||||
|
"{}/update-{}",
|
||||||
|
self.ap_url,
|
||||||
|
Utc::now().timestamp()
|
||||||
|
))?;
|
||||||
act.object_props
|
act.object_props
|
||||||
.set_id_string(format!("{}/update-{}", self.ap_url, Utc::now().timestamp()))?;
|
.set_to_link_vec::<Id>(article.object.object_props.to_link_vec()?)?;
|
||||||
act.object_props
|
act.object_props
|
||||||
.set_to_link_vec::<Id>(
|
.set_cc_link_vec::<Id>(article.object.object_props.cc_link_vec()?)?;
|
||||||
article.object
|
|
||||||
.object_props
|
|
||||||
.to_link_vec()?,
|
|
||||||
)?;
|
|
||||||
act.object_props
|
|
||||||
.set_cc_link_vec::<Id>(
|
|
||||||
article.object
|
|
||||||
.object_props
|
|
||||||
.cc_link_vec()?,
|
|
||||||
)?;
|
|
||||||
act.update_props
|
act.update_props
|
||||||
.set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?;
|
.set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?;
|
||||||
act.update_props
|
act.update_props.set_object_object(article)?;
|
||||||
.set_object_object(article)?;
|
|
||||||
Ok(act)
|
Ok(act)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_update(conn: &Connection, updated: &LicensedArticle, searcher: &Searcher) -> Result<()> {
|
pub fn handle_update(
|
||||||
let id = updated.object
|
conn: &Connection,
|
||||||
.object_props
|
updated: &LicensedArticle,
|
||||||
.id_string()?;
|
searcher: &Searcher,
|
||||||
|
) -> Result<()> {
|
||||||
|
let id = updated.object.object_props.id_string()?;
|
||||||
let mut post = Post::find_by_ap_url(conn, &id)?;
|
let mut post = Post::find_by_ap_url(conn, &id)?;
|
||||||
|
|
||||||
if let Ok(title) = updated.object.object_props.name_string() {
|
if let Ok(title) = updated.object.object_props.name_string() {
|
||||||
|
@ -698,7 +761,9 @@ impl Post {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| s.to_camel_case())
|
.map(|s| s.to_camel_case())
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
if let Some(serde_json::Value::Array(mention_tags)) = updated.object.object_props.tag.clone() {
|
if let Some(serde_json::Value::Array(mention_tags)) =
|
||||||
|
updated.object.object_props.tag.clone()
|
||||||
|
{
|
||||||
let mut mentions = vec![];
|
let mut mentions = vec![];
|
||||||
let mut tags = vec![];
|
let mut tags = vec![];
|
||||||
let mut hashtags = vec![];
|
let mut hashtags = vec![];
|
||||||
|
@ -710,8 +775,7 @@ impl Post {
|
||||||
serde_json::from_value::<Hashtag>(tag.clone())
|
serde_json::from_value::<Hashtag>(tag.clone())
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
.and_then(|t| {
|
.and_then(|t| {
|
||||||
let tag_name = t
|
let tag_name = t.name_string()?;
|
||||||
.name_string()?;
|
|
||||||
if txt_hashtags.remove(&tag_name) {
|
if txt_hashtags.remove(&tag_name) {
|
||||||
hashtags.push(t);
|
hashtags.push(t);
|
||||||
} else {
|
} else {
|
||||||
|
@ -854,20 +918,25 @@ impl Post {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cover_url(&self, conn: &Connection) -> Option<String> {
|
pub fn cover_url(&self, conn: &Connection) -> Option<String> {
|
||||||
self.cover_id.and_then(|i| Media::get(conn, i).ok()).and_then(|c| c.url(conn).ok())
|
self.cover_id
|
||||||
|
.and_then(|i| Media::get(conn, i).ok())
|
||||||
|
.and_then(|c| c.url(conn).ok())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for Post {
|
impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for Post {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_activity((conn, searcher): &(&'a Connection, &'a Searcher), article: LicensedArticle, _actor: Id) -> Result<Post> {
|
fn from_activity(
|
||||||
|
(conn, searcher): &(&'a Connection, &'a Searcher),
|
||||||
|
article: LicensedArticle,
|
||||||
|
_actor: Id,
|
||||||
|
) -> Result<Post> {
|
||||||
let license = article.custom_props.license_string().unwrap_or_default();
|
let license = article.custom_props.license_string().unwrap_or_default();
|
||||||
let article = article.object;
|
let article = article.object;
|
||||||
if let Ok(post) = Post::find_by_ap_url(
|
if let Ok(post) =
|
||||||
conn,
|
Post::find_by_ap_url(conn, &article.object_props.id_string().unwrap_or_default())
|
||||||
&article.object_props.id_string().unwrap_or_default(),
|
{
|
||||||
) {
|
|
||||||
Ok(post)
|
Ok(post)
|
||||||
} else {
|
} else {
|
||||||
let (blog, authors) = article
|
let (blog, authors) = article
|
||||||
|
@ -880,10 +949,8 @@ impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for Post
|
||||||
Ok(u) => {
|
Ok(u) => {
|
||||||
authors.push(u);
|
authors.push(u);
|
||||||
(blog, authors)
|
(blog, authors)
|
||||||
},
|
}
|
||||||
Err(_) => {
|
Err(_) => (blog.or_else(|| Blog::from_url(conn, &url).ok()), authors),
|
||||||
(blog.or_else(|| Blog::from_url(conn, &url).ok()), authors)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -893,41 +960,24 @@ impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for Post
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id));
|
.and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id));
|
||||||
|
|
||||||
let title = article
|
let title = article.object_props.name_string()?;
|
||||||
.object_props
|
|
||||||
.name_string()?;
|
|
||||||
let post = Post::insert(
|
let post = Post::insert(
|
||||||
conn,
|
conn,
|
||||||
NewPost {
|
NewPost {
|
||||||
blog_id: blog?.id,
|
blog_id: blog?.id,
|
||||||
slug: title.to_kebab_case(),
|
slug: title.to_kebab_case(),
|
||||||
title,
|
title,
|
||||||
content: SafeString::new(
|
content: SafeString::new(&article.object_props.content_string()?),
|
||||||
&article
|
|
||||||
.object_props
|
|
||||||
.content_string()?,
|
|
||||||
),
|
|
||||||
published: true,
|
published: true,
|
||||||
license,
|
license,
|
||||||
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
|
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
|
||||||
ap_url: article.object_props.url_string().or_else(|_|
|
ap_url: article
|
||||||
article
|
|
||||||
.object_props
|
|
||||||
.id_string()
|
|
||||||
)?,
|
|
||||||
creation_date: Some(
|
|
||||||
article
|
|
||||||
.object_props
|
|
||||||
.published_utctime()?
|
|
||||||
.naive_utc(),
|
|
||||||
),
|
|
||||||
subtitle: article
|
|
||||||
.object_props
|
.object_props
|
||||||
.summary_string()?,
|
.url_string()
|
||||||
source: article
|
.or_else(|_| article.object_props.id_string())?,
|
||||||
.ap_object_props
|
creation_date: Some(article.object_props.published_utctime()?.naive_utc()),
|
||||||
.source_object::<Source>()?
|
subtitle: article.object_props.summary_string()?,
|
||||||
.content,
|
source: article.ap_object_props.source_object::<Source>()?.content,
|
||||||
cover_id: cover,
|
cover_id: cover,
|
||||||
},
|
},
|
||||||
searcher,
|
searcher,
|
||||||
|
@ -959,7 +1009,12 @@ impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for Post
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
.and_then(|t| {
|
.and_then(|t| {
|
||||||
let tag_name = t.name_string()?;
|
let tag_name = t.name_string()?;
|
||||||
Ok(Tag::from_activity(conn, &t, post.id, hashtags.remove(&tag_name)))
|
Ok(Tag::from_activity(
|
||||||
|
conn,
|
||||||
|
&t,
|
||||||
|
post.id,
|
||||||
|
hashtags.remove(&tag_name),
|
||||||
|
))
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
@ -978,11 +1033,8 @@ impl<'a> Deletable<(&'a Connection, &'a Searcher), Delete> for Post {
|
||||||
.set_actor_link(self.get_authors(conn)?[0].clone().into_id())?;
|
.set_actor_link(self.get_authors(conn)?[0].clone().into_id())?;
|
||||||
|
|
||||||
let mut tombstone = Tombstone::default();
|
let mut tombstone = Tombstone::default();
|
||||||
tombstone
|
tombstone.object_props.set_id_string(self.ap_url.clone())?;
|
||||||
.object_props
|
act.delete_props.set_object_object(tombstone)?;
|
||||||
.set_id_string(self.ap_url.clone())?;
|
|
||||||
act.delete_props
|
|
||||||
.set_object_object(tombstone)?;
|
|
||||||
|
|
||||||
act.object_props
|
act.object_props
|
||||||
.set_id_string(format!("{}#delete", self.ap_url))?;
|
.set_id_string(format!("{}#delete", self.ap_url))?;
|
||||||
|
@ -992,16 +1044,22 @@ impl<'a> Deletable<(&'a Connection, &'a Searcher), Delete> for Post {
|
||||||
for m in Mention::list_for_post(&conn, self.id)? {
|
for m in Mention::list_for_post(&conn, self.id)? {
|
||||||
m.delete(conn)?;
|
m.delete(conn)?;
|
||||||
}
|
}
|
||||||
diesel::delete(self)
|
diesel::delete(self).execute(*conn)?;
|
||||||
.execute(*conn)?;
|
|
||||||
searcher.delete_document(self);
|
searcher.delete_document(self);
|
||||||
Ok(act)
|
Ok(act)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_id(id: &str, actor_id: &str, (conn, searcher): &(&Connection, &Searcher)) -> Result<Delete> {
|
fn delete_id(
|
||||||
|
id: &str,
|
||||||
|
actor_id: &str,
|
||||||
|
(conn, searcher): &(&Connection, &Searcher),
|
||||||
|
) -> Result<Delete> {
|
||||||
let actor = User::find_by_ap_url(conn, actor_id)?;
|
let actor = User::find_by_ap_url(conn, actor_id)?;
|
||||||
let post = Post::find_by_ap_url(conn, id)?;
|
let post = Post::find_by_ap_url(conn, id)?;
|
||||||
let can_delete = post.get_authors(conn)?.into_iter().any(|a| actor.id == a.id);
|
let can_delete = post
|
||||||
|
.get_authors(conn)?
|
||||||
|
.into_iter()
|
||||||
|
.any(|a| actor.id == a.id);
|
||||||
if can_delete {
|
if can_delete {
|
||||||
post.delete(&(conn, searcher))
|
post.delete(&(conn, searcher))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -40,7 +40,11 @@ impl Reshare {
|
||||||
post_id as i32
|
post_id as i32
|
||||||
);
|
);
|
||||||
|
|
||||||
pub fn get_recents_for_author(conn: &Connection, user: &User, limit: i64) -> Result<Vec<Reshare>> {
|
pub fn get_recents_for_author(
|
||||||
|
conn: &Connection,
|
||||||
|
user: &User,
|
||||||
|
limit: i64,
|
||||||
|
) -> Result<Vec<Reshare>> {
|
||||||
reshares::table
|
reshares::table
|
||||||
.filter(reshares::user_id.eq(user.id))
|
.filter(reshares::user_id.eq(user.id))
|
||||||
.order(reshares::creation_date.desc())
|
.order(reshares::creation_date.desc())
|
||||||
|
@ -63,12 +67,10 @@ impl Reshare {
|
||||||
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
|
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
|
||||||
act.announce_props
|
act.announce_props
|
||||||
.set_object_link(Post::get(conn, self.post_id)?.into_id())?;
|
.set_object_link(Post::get(conn, self.post_id)?.into_id())?;
|
||||||
act.object_props
|
act.object_props.set_id_string(self.ap_url.clone())?;
|
||||||
.set_id_string(self.ap_url.clone())?;
|
|
||||||
act.object_props
|
act.object_props
|
||||||
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
|
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
|
||||||
act.object_props
|
act.object_props.set_cc_link_vec::<Id>(vec![])?;
|
||||||
.set_cc_link_vec::<Id>(vec![])?;
|
|
||||||
|
|
||||||
Ok(act)
|
Ok(act)
|
||||||
}
|
}
|
||||||
|
@ -78,29 +80,15 @@ impl FromActivity<Announce, Connection> for Reshare {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_activity(conn: &Connection, announce: Announce, _actor: Id) -> Result<Reshare> {
|
fn from_activity(conn: &Connection, announce: Announce, _actor: Id) -> Result<Reshare> {
|
||||||
let user = User::from_url(
|
let user = User::from_url(conn, announce.announce_props.actor_link::<Id>()?.as_ref())?;
|
||||||
conn,
|
let post =
|
||||||
announce
|
Post::find_by_ap_url(conn, announce.announce_props.object_link::<Id>()?.as_ref())?;
|
||||||
.announce_props
|
|
||||||
.actor_link::<Id>()?
|
|
||||||
.as_ref(),
|
|
||||||
)?;
|
|
||||||
let post = Post::find_by_ap_url(
|
|
||||||
conn,
|
|
||||||
announce
|
|
||||||
.announce_props
|
|
||||||
.object_link::<Id>()?
|
|
||||||
.as_ref(),
|
|
||||||
)?;
|
|
||||||
let reshare = Reshare::insert(
|
let reshare = Reshare::insert(
|
||||||
conn,
|
conn,
|
||||||
NewReshare {
|
NewReshare {
|
||||||
post_id: post.id,
|
post_id: post.id,
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
ap_url: announce
|
ap_url: announce.object_props.id_string().unwrap_or_default(),
|
||||||
.object_props
|
|
||||||
.id_string()
|
|
||||||
.unwrap_or_default(),
|
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
reshare.notify(conn)?;
|
reshare.notify(conn)?;
|
||||||
|
@ -131,26 +119,22 @@ impl Deletable<Connection, Undo> for Reshare {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn delete(&self, conn: &Connection) -> Result<Undo> {
|
fn delete(&self, conn: &Connection) -> Result<Undo> {
|
||||||
diesel::delete(self)
|
diesel::delete(self).execute(conn)?;
|
||||||
.execute(conn)?;
|
|
||||||
|
|
||||||
// delete associated notification if any
|
// delete associated notification if any
|
||||||
if let Ok(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) {
|
if let Ok(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) {
|
||||||
diesel::delete(¬if)
|
diesel::delete(¬if).execute(conn)?;
|
||||||
.execute(conn)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut act = Undo::default();
|
let mut act = Undo::default();
|
||||||
act.undo_props
|
act.undo_props
|
||||||
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
|
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
|
||||||
act.undo_props
|
act.undo_props.set_object_object(self.to_activity(conn)?)?;
|
||||||
.set_object_object(self.to_activity(conn)?)?;
|
|
||||||
act.object_props
|
act.object_props
|
||||||
.set_id_string(format!("{}#delete", self.ap_url))?;
|
.set_id_string(format!("{}#delete", self.ap_url))?;
|
||||||
act.object_props
|
act.object_props
|
||||||
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
|
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
|
||||||
act.object_props
|
act.object_props.set_cc_link_vec::<Id>(vec![])?;
|
||||||
.set_cc_link_vec::<Id>(vec![])?;
|
|
||||||
|
|
||||||
Ok(act)
|
Ok(act)
|
||||||
}
|
}
|
||||||
|
@ -172,7 +156,7 @@ impl NewReshare {
|
||||||
NewReshare {
|
NewReshare {
|
||||||
post_id: p.id,
|
post_id: p.id,
|
||||||
user_id: u.id,
|
user_id: u.id,
|
||||||
ap_url
|
ap_url,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,21 +19,15 @@ lazy_static! {
|
||||||
static ref CLEAN: Builder<'static> = {
|
static ref CLEAN: Builder<'static> = {
|
||||||
let mut b = Builder::new();
|
let mut b = Builder::new();
|
||||||
b.add_generic_attributes(iter::once("id"))
|
b.add_generic_attributes(iter::once("id"))
|
||||||
.add_tags(&[ "iframe", "video", "audio" ])
|
.add_tags(&["iframe", "video", "audio"])
|
||||||
.id_prefix(Some("postcontent-"))
|
.id_prefix(Some("postcontent-"))
|
||||||
.url_relative(UrlRelative::Custom(Box::new(url_add_prefix)))
|
.url_relative(UrlRelative::Custom(Box::new(url_add_prefix)))
|
||||||
.add_tag_attributes(
|
.add_tag_attributes(
|
||||||
"iframe",
|
"iframe",
|
||||||
[ "width", "height", "src", "frameborder" ].iter().cloned(),
|
["width", "height", "src", "frameborder"].iter().cloned(),
|
||||||
)
|
)
|
||||||
.add_tag_attributes(
|
.add_tag_attributes("video", ["src", "title", "controls"].iter())
|
||||||
"video",
|
.add_tag_attributes("audio", ["src", "title", "controls"].iter());
|
||||||
[ "src", "title", "controls" ].iter(),
|
|
||||||
)
|
|
||||||
.add_tag_attributes(
|
|
||||||
"audio",
|
|
||||||
[ "src", "title", "controls" ].iter(),
|
|
||||||
);
|
|
||||||
b
|
b
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -69,7 +63,7 @@ impl SafeString {
|
||||||
/// Prefer `SafeString::new` as much as possible.
|
/// Prefer `SafeString::new` as much as possible.
|
||||||
pub fn trusted(value: impl AsRef<str>) -> Self {
|
pub fn trusted(value: impl AsRef<str>) -> Self {
|
||||||
SafeString {
|
SafeString {
|
||||||
value: value.as_ref().to_string()
|
value: value.as_ref().to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,33 +1,32 @@
|
||||||
mod searcher;
|
|
||||||
mod query;
|
mod query;
|
||||||
|
mod searcher;
|
||||||
mod tokenizer;
|
mod tokenizer;
|
||||||
pub use self::searcher::*;
|
|
||||||
pub use self::query::PlumeQuery as Query;
|
pub use self::query::PlumeQuery as Query;
|
||||||
|
pub use self::searcher::*;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use super::{Query, Searcher};
|
use super::{Query, Searcher};
|
||||||
|
use diesel::Connection;
|
||||||
use std::env::temp_dir;
|
use std::env::temp_dir;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use diesel::Connection;
|
|
||||||
|
|
||||||
|
use blogs::tests::fill_database;
|
||||||
use plume_common::activity_pub::inbox::Deletable;
|
use plume_common::activity_pub::inbox::Deletable;
|
||||||
use plume_common::utils::random_hex;
|
use plume_common::utils::random_hex;
|
||||||
use blogs::tests::fill_database;
|
|
||||||
use posts::{NewPost, Post};
|
|
||||||
use post_authors::*;
|
use post_authors::*;
|
||||||
|
use posts::{NewPost, Post};
|
||||||
use safe_string::SafeString;
|
use safe_string::SafeString;
|
||||||
use tests::db;
|
use tests::db;
|
||||||
|
|
||||||
|
|
||||||
pub(crate) fn get_searcher() -> Searcher {
|
pub(crate) fn get_searcher() -> Searcher {
|
||||||
let dir = temp_dir().join("plume-test");
|
let dir = temp_dir().join("plume-test");
|
||||||
if dir.exists() {
|
if dir.exists() {
|
||||||
Searcher::open(&dir)
|
Searcher::open(&dir)
|
||||||
} else {
|
} else {
|
||||||
Searcher::create(&dir)
|
Searcher::create(&dir)
|
||||||
}.unwrap()
|
}
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -98,7 +97,9 @@ pub(crate) mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn open() {
|
fn open() {
|
||||||
{get_searcher()};//make sure $tmp/plume-test-tantivy exist
|
{
|
||||||
|
get_searcher()
|
||||||
|
}; //make sure $tmp/plume-test-tantivy exist
|
||||||
|
|
||||||
let dir = temp_dir().join("plume-test");
|
let dir = temp_dir().join("plume-test");
|
||||||
Searcher::open(&dir).unwrap();
|
Searcher::open(&dir).unwrap();
|
||||||
|
@ -109,8 +110,10 @@ pub(crate) mod tests {
|
||||||
let dir = temp_dir().join(format!("plume-test-{}", random_hex()));
|
let dir = temp_dir().join(format!("plume-test-{}", random_hex()));
|
||||||
|
|
||||||
assert!(Searcher::open(&dir).is_err());
|
assert!(Searcher::open(&dir).is_err());
|
||||||
{Searcher::create(&dir).unwrap();}
|
{
|
||||||
Searcher::open(&dir).unwrap();//verify it's well created
|
Searcher::create(&dir).unwrap();
|
||||||
|
}
|
||||||
|
Searcher::open(&dir).unwrap(); //verify it's well created
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -123,37 +126,56 @@ pub(crate) mod tests {
|
||||||
|
|
||||||
let title = random_hex()[..8].to_owned();
|
let title = random_hex()[..8].to_owned();
|
||||||
|
|
||||||
let mut post = Post::insert(conn, NewPost {
|
let mut post = Post::insert(
|
||||||
blog_id: blog.id,
|
conn,
|
||||||
slug: title.clone(),
|
NewPost {
|
||||||
title: title.clone(),
|
blog_id: blog.id,
|
||||||
content: SafeString::new(""),
|
slug: title.clone(),
|
||||||
published: true,
|
title: title.clone(),
|
||||||
license: "CC-BY-SA".to_owned(),
|
content: SafeString::new(""),
|
||||||
ap_url: "".to_owned(),
|
published: true,
|
||||||
creation_date: None,
|
license: "CC-BY-SA".to_owned(),
|
||||||
subtitle: "".to_owned(),
|
ap_url: "".to_owned(),
|
||||||
source: "".to_owned(),
|
creation_date: None,
|
||||||
cover_id: None,
|
subtitle: "".to_owned(),
|
||||||
}, &searcher).unwrap();
|
source: "".to_owned(),
|
||||||
PostAuthor::insert(conn, NewPostAuthor {
|
cover_id: None,
|
||||||
post_id: post.id,
|
},
|
||||||
author_id: author.id,
|
&searcher,
|
||||||
}).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
PostAuthor::insert(
|
||||||
|
conn,
|
||||||
|
NewPostAuthor {
|
||||||
|
post_id: post.id,
|
||||||
|
author_id: author.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
searcher.commit();
|
searcher.commit();
|
||||||
assert_eq!(searcher.search_document(conn, Query::from_str(&title).unwrap(), (0,1))[0].id, post.id);
|
assert_eq!(
|
||||||
|
searcher.search_document(conn, Query::from_str(&title).unwrap(), (0, 1))[0].id,
|
||||||
|
post.id
|
||||||
|
);
|
||||||
|
|
||||||
let newtitle = random_hex()[..8].to_owned();
|
let newtitle = random_hex()[..8].to_owned();
|
||||||
post.title = newtitle.clone();
|
post.title = newtitle.clone();
|
||||||
post.update(conn, &searcher).unwrap();
|
post.update(conn, &searcher).unwrap();
|
||||||
searcher.commit();
|
searcher.commit();
|
||||||
assert_eq!(searcher.search_document(conn, Query::from_str(&newtitle).unwrap(), (0,1))[0].id, post.id);
|
assert_eq!(
|
||||||
assert!(searcher.search_document(conn, Query::from_str(&title).unwrap(), (0,1)).is_empty());
|
searcher.search_document(conn, Query::from_str(&newtitle).unwrap(), (0, 1))[0].id,
|
||||||
|
post.id
|
||||||
|
);
|
||||||
|
assert!(searcher
|
||||||
|
.search_document(conn, Query::from_str(&title).unwrap(), (0, 1))
|
||||||
|
.is_empty());
|
||||||
|
|
||||||
post.delete(&(conn, &searcher)).unwrap();
|
post.delete(&(conn, &searcher)).unwrap();
|
||||||
searcher.commit();
|
searcher.commit();
|
||||||
assert!(searcher.search_document(conn, Query::from_str(&newtitle).unwrap(), (0,1)).is_empty());
|
assert!(searcher
|
||||||
|
.search_document(conn, Query::from_str(&newtitle).unwrap(), (0, 1))
|
||||||
|
.is_empty());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
use chrono::{Datelike, naive::NaiveDate, offset::Utc};
|
use chrono::{naive::NaiveDate, offset::Utc, Datelike};
|
||||||
use tantivy::{query::*, schema::*, Term};
|
|
||||||
use std::{cmp,ops::Bound};
|
|
||||||
use search::searcher::Searcher;
|
use search::searcher::Searcher;
|
||||||
|
use std::{cmp, ops::Bound};
|
||||||
|
use tantivy::{query::*, schema::*, Term};
|
||||||
|
|
||||||
//Generate functions for advanced search
|
//Generate functions for advanced search
|
||||||
macro_rules! gen_func {
|
macro_rules! gen_func {
|
||||||
|
@ -142,13 +141,11 @@ pub struct PlumeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlumeQuery {
|
impl PlumeQuery {
|
||||||
|
|
||||||
/// Create a new empty Query
|
/// Create a new empty Query
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Default::default()
|
Default::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Parse a query string into this Query
|
/// Parse a query string into this Query
|
||||||
pub fn parse_query(&mut self, query: &str) -> &mut Self {
|
pub fn parse_query(&mut self, query: &str) -> &mut Self {
|
||||||
self.from_str_req(&query.trim())
|
self.from_str_req(&query.trim())
|
||||||
|
@ -160,9 +157,11 @@ impl PlumeQuery {
|
||||||
gen_to_query!(self, result; normal: title, subtitle, content, tag;
|
gen_to_query!(self, result; normal: title, subtitle, content, tag;
|
||||||
oneoff: instance, author, blog, lang, license);
|
oneoff: instance, author, blog, lang, license);
|
||||||
|
|
||||||
for (occur, token) in self.text { // text entries need to be added as multiple Terms
|
for (occur, token) in self.text {
|
||||||
|
// text entries need to be added as multiple Terms
|
||||||
match occur {
|
match occur {
|
||||||
Occur::Must => { // a Must mean this must be in one of title subtitle or content, not in all 3
|
Occur::Must => {
|
||||||
|
// a Must mean this must be in one of title subtitle or content, not in all 3
|
||||||
let subresult = vec![
|
let subresult = vec![
|
||||||
(Occur::Should, Self::token_to_query(&token, "title")),
|
(Occur::Should, Self::token_to_query(&token, "title")),
|
||||||
(Occur::Should, Self::token_to_query(&token, "subtitle")),
|
(Occur::Should, Self::token_to_query(&token, "subtitle")),
|
||||||
|
@ -170,20 +169,26 @@ impl PlumeQuery {
|
||||||
];
|
];
|
||||||
|
|
||||||
result.push((Occur::Must, Box::new(BooleanQuery::from(subresult))));
|
result.push((Occur::Must, Box::new(BooleanQuery::from(subresult))));
|
||||||
},
|
}
|
||||||
occur => {
|
occur => {
|
||||||
result.push((occur, Self::token_to_query(&token, "title")));
|
result.push((occur, Self::token_to_query(&token, "title")));
|
||||||
result.push((occur, Self::token_to_query(&token, "subtitle")));
|
result.push((occur, Self::token_to_query(&token, "subtitle")));
|
||||||
result.push((occur, Self::token_to_query(&token, "content")));
|
result.push((occur, Self::token_to_query(&token, "content")));
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.before.is_some() || self.after.is_some() { // if at least one range bound is provided
|
if self.before.is_some() || self.after.is_some() {
|
||||||
let after = self.after.unwrap_or_else(|| i64::from(NaiveDate::from_ymd(2000, 1, 1).num_days_from_ce()));
|
// if at least one range bound is provided
|
||||||
let before = self.before.unwrap_or_else(|| i64::from(Utc::today().num_days_from_ce()));
|
let after = self
|
||||||
|
.after
|
||||||
|
.unwrap_or_else(|| i64::from(NaiveDate::from_ymd(2000, 1, 1).num_days_from_ce()));
|
||||||
|
let before = self
|
||||||
|
.before
|
||||||
|
.unwrap_or_else(|| i64::from(Utc::today().num_days_from_ce()));
|
||||||
let field = Searcher::schema().get_field("creation_date").unwrap();
|
let field = Searcher::schema().get_field("creation_date").unwrap();
|
||||||
let range = RangeQuery::new_i64_bounds(field, Bound::Included(after), Bound::Included(before));
|
let range =
|
||||||
|
RangeQuery::new_i64_bounds(field, Bound::Included(after), Bound::Included(before));
|
||||||
result.push((Occur::Must, Box::new(range)));
|
result.push((Occur::Must, Box::new(range)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,14 +200,18 @@ impl PlumeQuery {
|
||||||
|
|
||||||
// documents newer than the provided date will be ignored
|
// documents newer than the provided date will be ignored
|
||||||
pub fn before<D: Datelike>(&mut self, date: &D) -> &mut Self {
|
pub fn before<D: Datelike>(&mut self, date: &D) -> &mut Self {
|
||||||
let before = self.before.unwrap_or_else(|| i64::from(Utc::today().num_days_from_ce()));
|
let before = self
|
||||||
|
.before
|
||||||
|
.unwrap_or_else(|| i64::from(Utc::today().num_days_from_ce()));
|
||||||
self.before = Some(cmp::min(before, i64::from(date.num_days_from_ce())));
|
self.before = Some(cmp::min(before, i64::from(date.num_days_from_ce())));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
// documents older than the provided date will be ignored
|
// documents older than the provided date will be ignored
|
||||||
pub fn after<D: Datelike>(&mut self, date: &D) -> &mut Self {
|
pub fn after<D: Datelike>(&mut self, date: &D) -> &mut Self {
|
||||||
let after = self.after.unwrap_or_else(|| i64::from(NaiveDate::from_ymd(2000, 1, 1).num_days_from_ce()));
|
let after = self
|
||||||
|
.after
|
||||||
|
.unwrap_or_else(|| i64::from(NaiveDate::from_ymd(2000, 1, 1).num_days_from_ce()));
|
||||||
self.after = Some(cmp::max(after, i64::from(date.num_days_from_ce())));
|
self.after = Some(cmp::max(after, i64::from(date.num_days_from_ce())));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -212,18 +221,22 @@ impl PlumeQuery {
|
||||||
query = query.trim();
|
query = query.trim();
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
("", "")
|
("", "")
|
||||||
} else if query.get(0..1).map(|v| v=="\"").unwrap_or(false) {
|
} else if query.get(0..1).map(|v| v == "\"").unwrap_or(false) {
|
||||||
if let Some(index) = query[1..].find('"') {
|
if let Some(index) = query[1..].find('"') {
|
||||||
query.split_at(index+2)
|
query.split_at(index + 2)
|
||||||
} else {
|
} else {
|
||||||
(query, "")
|
(query, "")
|
||||||
}
|
}
|
||||||
} else if query.get(0..2).map(|v| v=="+\"" || v=="-\"").unwrap_or(false) {
|
} else if query
|
||||||
if let Some(index) = query[2..].find('"') {
|
.get(0..2)
|
||||||
query.split_at(index+3)
|
.map(|v| v == "+\"" || v == "-\"")
|
||||||
} else {
|
.unwrap_or(false)
|
||||||
(query, "")
|
{
|
||||||
}
|
if let Some(index) = query[2..].find('"') {
|
||||||
|
query.split_at(index + 3)
|
||||||
|
} else {
|
||||||
|
(query, "")
|
||||||
|
}
|
||||||
} else if let Some(index) = query.find(' ') {
|
} else if let Some(index) = query.find(' ') {
|
||||||
query.split_at(index)
|
query.split_at(index)
|
||||||
} else {
|
} else {
|
||||||
|
@ -247,13 +260,13 @@ impl PlumeQuery {
|
||||||
fn from_str_req(&mut self, mut query: &str) -> &mut Self {
|
fn from_str_req(&mut self, mut query: &str) -> &mut Self {
|
||||||
query = query.trim_left();
|
query = query.trim_left();
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
return self
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
let occur = if query.get(0..1).map(|v| v=="+").unwrap_or(false) {
|
let occur = if query.get(0..1).map(|v| v == "+").unwrap_or(false) {
|
||||||
query = &query[1..];
|
query = &query[1..];
|
||||||
Occur::Must
|
Occur::Must
|
||||||
} else if query.get(0..1).map(|v| v=="-").unwrap_or(false) {
|
} else if query.get(0..1).map(|v| v == "-").unwrap_or(false) {
|
||||||
query = &query[1..];
|
query = &query[1..];
|
||||||
Occur::MustNot
|
Occur::MustNot
|
||||||
} else {
|
} else {
|
||||||
|
@ -270,31 +283,59 @@ impl PlumeQuery {
|
||||||
let token = token.to_lowercase();
|
let token = token.to_lowercase();
|
||||||
let token = token.as_str();
|
let token = token.as_str();
|
||||||
let field = Searcher::schema().get_field(field_name).unwrap();
|
let field = Searcher::schema().get_field(field_name).unwrap();
|
||||||
if token.contains('@') && (field_name=="author" || field_name=="blog") {
|
if token.contains('@') && (field_name == "author" || field_name == "blog") {
|
||||||
let pos = token.find('@').unwrap();
|
let pos = token.find('@').unwrap();
|
||||||
let user_term = Term::from_field_text(field, &token[..pos]);
|
let user_term = Term::from_field_text(field, &token[..pos]);
|
||||||
let instance_term = Term::from_field_text(Searcher::schema().get_field("instance").unwrap(), &token[pos+1..]);
|
let instance_term = Term::from_field_text(
|
||||||
|
Searcher::schema().get_field("instance").unwrap(),
|
||||||
|
&token[pos + 1..],
|
||||||
|
);
|
||||||
Box::new(BooleanQuery::from(vec![
|
Box::new(BooleanQuery::from(vec![
|
||||||
(Occur::Must, Box::new(TermQuery::new(user_term, if field_name=="author" { IndexRecordOption::Basic }
|
(
|
||||||
else { IndexRecordOption::WithFreqsAndPositions }
|
Occur::Must,
|
||||||
)) as Box<dyn Query + 'static>),
|
Box::new(TermQuery::new(
|
||||||
(Occur::Must, Box::new(TermQuery::new(instance_term, IndexRecordOption::Basic))),
|
user_term,
|
||||||
|
if field_name == "author" {
|
||||||
|
IndexRecordOption::Basic
|
||||||
|
} else {
|
||||||
|
IndexRecordOption::WithFreqsAndPositions
|
||||||
|
},
|
||||||
|
)) as Box<dyn Query + 'static>,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Occur::Must,
|
||||||
|
Box::new(TermQuery::new(instance_term, IndexRecordOption::Basic)),
|
||||||
|
),
|
||||||
]))
|
]))
|
||||||
} else if token.contains(' ') { // phrase query
|
} else if token.contains(' ') {
|
||||||
|
// phrase query
|
||||||
match field_name {
|
match field_name {
|
||||||
"instance" | "author" | "tag" => // phrase query are not available on these fields, treat it as multiple Term queries
|
"instance" | "author" | "tag" =>
|
||||||
Box::new(BooleanQuery::from(token.split_whitespace()
|
// phrase query are not available on these fields, treat it as multiple Term queries
|
||||||
.map(|token| {
|
{
|
||||||
let term = Term::from_field_text(field, token);
|
Box::new(BooleanQuery::from(
|
||||||
(Occur::Should, Box::new(TermQuery::new(term, IndexRecordOption::Basic))
|
token
|
||||||
as Box<dyn Query + 'static>)
|
.split_whitespace()
|
||||||
})
|
.map(|token| {
|
||||||
.collect::<Vec<_>>())),
|
let term = Term::from_field_text(field, token);
|
||||||
_ => Box::new(PhraseQuery::new(token.split_whitespace()
|
(
|
||||||
.map(|token| Term::from_field_text(field, token))
|
Occur::Should,
|
||||||
.collect()))
|
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 term = Term::from_field_text(field, token);
|
||||||
let index_option = match field_name {
|
let index_option = match field_name {
|
||||||
"instance" | "author" | "tag" => IndexRecordOption::Basic,
|
"instance" | "author" | "tag" => IndexRecordOption::Basic,
|
||||||
|
@ -306,7 +347,6 @@ impl PlumeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::str::FromStr for PlumeQuery {
|
impl std::str::FromStr for PlumeQuery {
|
||||||
|
|
||||||
type Err = !;
|
type Err = !;
|
||||||
|
|
||||||
/// Create a new Query from &str
|
/// Create a new Query from &str
|
||||||
|
@ -340,7 +380,7 @@ impl ToString for PlumeQuery {
|
||||||
instance, author, blog, lang, license;
|
instance, author, blog, lang, license;
|
||||||
date: before, after);
|
date: before, after);
|
||||||
|
|
||||||
result.pop();// remove trailing ' '
|
result.pop(); // remove trailing ' '
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,15 +5,14 @@ use Connection;
|
||||||
|
|
||||||
use chrono::Datelike;
|
use chrono::Datelike;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use std::{cmp, fs::create_dir_all, path::Path, sync::Mutex};
|
||||||
use tantivy::{
|
use tantivy::{
|
||||||
collector::TopDocs, directory::MmapDirectory,
|
collector::TopDocs, directory::MmapDirectory, schema::*, tokenizer::*, Index, IndexWriter, Term,
|
||||||
schema::*, tokenizer::*, Index, IndexWriter, Term
|
|
||||||
};
|
};
|
||||||
use whatlang::{detect as detect_lang, Lang};
|
use whatlang::{detect as detect_lang, Lang};
|
||||||
use std::{cmp, fs::create_dir_all, path::Path, sync::Mutex};
|
|
||||||
|
|
||||||
use search::query::PlumeQuery;
|
|
||||||
use super::tokenizer;
|
use super::tokenizer;
|
||||||
|
use search::query::PlumeQuery;
|
||||||
use Result;
|
use Result;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -31,20 +30,23 @@ pub struct Searcher {
|
||||||
|
|
||||||
impl Searcher {
|
impl Searcher {
|
||||||
pub fn schema() -> Schema {
|
pub fn schema() -> Schema {
|
||||||
let tag_indexing = TextOptions::default()
|
let tag_indexing = TextOptions::default().set_indexing_options(
|
||||||
.set_indexing_options(TextFieldIndexing::default()
|
TextFieldIndexing::default()
|
||||||
.set_tokenizer("whitespace_tokenizer")
|
.set_tokenizer("whitespace_tokenizer")
|
||||||
.set_index_option(IndexRecordOption::Basic));
|
.set_index_option(IndexRecordOption::Basic),
|
||||||
|
);
|
||||||
|
|
||||||
let content_indexing = TextOptions::default()
|
let content_indexing = TextOptions::default().set_indexing_options(
|
||||||
.set_indexing_options(TextFieldIndexing::default()
|
TextFieldIndexing::default()
|
||||||
.set_tokenizer("content_tokenizer")
|
.set_tokenizer("content_tokenizer")
|
||||||
.set_index_option(IndexRecordOption::WithFreqsAndPositions));
|
.set_index_option(IndexRecordOption::WithFreqsAndPositions),
|
||||||
|
);
|
||||||
|
|
||||||
let property_indexing = TextOptions::default()
|
let property_indexing = TextOptions::default().set_indexing_options(
|
||||||
.set_indexing_options(TextFieldIndexing::default()
|
TextFieldIndexing::default()
|
||||||
.set_tokenizer("property_tokenizer")
|
.set_tokenizer("property_tokenizer")
|
||||||
.set_index_option(IndexRecordOption::WithFreqsAndPositions));
|
.set_index_option(IndexRecordOption::WithFreqsAndPositions),
|
||||||
|
);
|
||||||
|
|
||||||
let mut schema_builder = SchemaBuilder::default();
|
let mut schema_builder = SchemaBuilder::default();
|
||||||
|
|
||||||
|
@ -66,56 +68,65 @@ impl Searcher {
|
||||||
schema_builder.build()
|
schema_builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn create(path: &AsRef<Path>) -> Result<Self> {
|
pub fn create(path: &AsRef<Path>) -> Result<Self> {
|
||||||
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer
|
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer.filter(LowerCaser);
|
||||||
.filter(LowerCaser);
|
|
||||||
|
|
||||||
let content_tokenizer = SimpleTokenizer
|
let content_tokenizer = SimpleTokenizer
|
||||||
.filter(RemoveLongFilter::limit(40))
|
.filter(RemoveLongFilter::limit(40))
|
||||||
.filter(LowerCaser);
|
.filter(LowerCaser);
|
||||||
|
|
||||||
let property_tokenizer = NgramTokenizer::new(2, 8, false)
|
let property_tokenizer = NgramTokenizer::new(2, 8, false).filter(LowerCaser);
|
||||||
.filter(LowerCaser);
|
|
||||||
|
|
||||||
let schema = Self::schema();
|
let schema = Self::schema();
|
||||||
|
|
||||||
create_dir_all(path).map_err(|_| SearcherError::IndexCreationError)?;
|
create_dir_all(path).map_err(|_| SearcherError::IndexCreationError)?;
|
||||||
let index = Index::create(MmapDirectory::open(path).map_err(|_| SearcherError::IndexCreationError)?, schema).map_err(|_| SearcherError::IndexCreationError)?;
|
let index = Index::create(
|
||||||
|
MmapDirectory::open(path).map_err(|_| SearcherError::IndexCreationError)?,
|
||||||
|
schema,
|
||||||
|
)
|
||||||
|
.map_err(|_| SearcherError::IndexCreationError)?;
|
||||||
|
|
||||||
{
|
{
|
||||||
let tokenizer_manager = index.tokenizers();
|
let tokenizer_manager = index.tokenizers();
|
||||||
tokenizer_manager.register("whitespace_tokenizer", whitespace_tokenizer);
|
tokenizer_manager.register("whitespace_tokenizer", whitespace_tokenizer);
|
||||||
tokenizer_manager.register("content_tokenizer", content_tokenizer);
|
tokenizer_manager.register("content_tokenizer", content_tokenizer);
|
||||||
tokenizer_manager.register("property_tokenizer", property_tokenizer);
|
tokenizer_manager.register("property_tokenizer", property_tokenizer);
|
||||||
}//to please the borrow checker
|
} //to please the borrow checker
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
writer: Mutex::new(Some(index.writer(50_000_000).map_err(|_| SearcherError::WriteLockAcquisitionError)?)),
|
writer: Mutex::new(Some(
|
||||||
index
|
index
|
||||||
|
.writer(50_000_000)
|
||||||
|
.map_err(|_| SearcherError::WriteLockAcquisitionError)?,
|
||||||
|
)),
|
||||||
|
index,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open(path: &AsRef<Path>) -> Result<Self> {
|
pub fn open(path: &AsRef<Path>) -> Result<Self> {
|
||||||
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer
|
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer.filter(LowerCaser);
|
||||||
.filter(LowerCaser);
|
|
||||||
|
|
||||||
let content_tokenizer = SimpleTokenizer
|
let content_tokenizer = SimpleTokenizer
|
||||||
.filter(RemoveLongFilter::limit(40))
|
.filter(RemoveLongFilter::limit(40))
|
||||||
.filter(LowerCaser);
|
.filter(LowerCaser);
|
||||||
|
|
||||||
let property_tokenizer = NgramTokenizer::new(2, 8, false)
|
let property_tokenizer = NgramTokenizer::new(2, 8, false).filter(LowerCaser);
|
||||||
.filter(LowerCaser);
|
|
||||||
|
|
||||||
let index = Index::open(MmapDirectory::open(path).map_err(|_| SearcherError::IndexOpeningError)?).map_err(|_| SearcherError::IndexOpeningError)?;
|
let index =
|
||||||
|
Index::open(MmapDirectory::open(path).map_err(|_| SearcherError::IndexOpeningError)?)
|
||||||
|
.map_err(|_| SearcherError::IndexOpeningError)?;
|
||||||
|
|
||||||
{
|
{
|
||||||
let tokenizer_manager = index.tokenizers();
|
let tokenizer_manager = index.tokenizers();
|
||||||
tokenizer_manager.register("whitespace_tokenizer", whitespace_tokenizer);
|
tokenizer_manager.register("whitespace_tokenizer", whitespace_tokenizer);
|
||||||
tokenizer_manager.register("content_tokenizer", content_tokenizer);
|
tokenizer_manager.register("content_tokenizer", content_tokenizer);
|
||||||
tokenizer_manager.register("property_tokenizer", property_tokenizer);
|
tokenizer_manager.register("property_tokenizer", property_tokenizer);
|
||||||
}//to please the borrow checker
|
} //to please the borrow checker
|
||||||
let mut writer = index.writer(50_000_000).map_err(|_| SearcherError::WriteLockAcquisitionError)?;
|
let mut writer = index
|
||||||
writer.garbage_collect_files().map_err(|_| SearcherError::IndexEditionError)?;
|
.writer(50_000_000)
|
||||||
|
.map_err(|_| SearcherError::WriteLockAcquisitionError)?;
|
||||||
|
writer
|
||||||
|
.garbage_collect_files()
|
||||||
|
.map_err(|_| SearcherError::IndexEditionError)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
writer: Mutex::new(Some(writer)),
|
writer: Mutex::new(Some(writer)),
|
||||||
index,
|
index,
|
||||||
|
@ -173,18 +184,24 @@ impl Searcher {
|
||||||
self.add_document(conn, post)
|
self.add_document(conn, post)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn search_document(&self, conn: &Connection, query: PlumeQuery, (min, max): (i32, i32)) -> Vec<Post>{
|
pub fn search_document(
|
||||||
|
&self,
|
||||||
|
conn: &Connection,
|
||||||
|
query: PlumeQuery,
|
||||||
|
(min, max): (i32, i32),
|
||||||
|
) -> Vec<Post> {
|
||||||
let schema = self.index.schema();
|
let schema = self.index.schema();
|
||||||
let post_id = schema.get_field("post_id").unwrap();
|
let post_id = schema.get_field("post_id").unwrap();
|
||||||
|
|
||||||
let collector = TopDocs::with_limit(cmp::max(1,max) as usize);
|
let collector = TopDocs::with_limit(cmp::max(1, max) as usize);
|
||||||
|
|
||||||
let searcher = self.index.searcher();
|
let searcher = self.index.searcher();
|
||||||
let res = searcher.search(&query.into_query(), &collector).unwrap();
|
let res = searcher.search(&query.into_query(), &collector).unwrap();
|
||||||
|
|
||||||
res.get(min as usize..).unwrap_or(&[])
|
res.get(min as usize..)
|
||||||
|
.unwrap_or(&[])
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(_,doc_add)| {
|
.filter_map(|(_, doc_add)| {
|
||||||
let doc = searcher.doc(*doc_add).ok()?;
|
let doc = searcher.doc(*doc_add).ok()?;
|
||||||
let id = doc.get_first(post_id)?;
|
let id = doc.get_first(post_id)?;
|
||||||
Post::get(conn, id.i64_value() as i32).ok()
|
Post::get(conn, id.i64_value() as i32).ok()
|
||||||
|
|
|
@ -38,7 +38,12 @@ impl Tag {
|
||||||
Ok(ht)
|
Ok(ht)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_activity(conn: &Connection, tag: &Hashtag, post: i32, is_hashtag: bool) -> Result<Tag> {
|
pub fn from_activity(
|
||||||
|
conn: &Connection,
|
||||||
|
tag: &Hashtag,
|
||||||
|
post: i32,
|
||||||
|
is_hashtag: bool,
|
||||||
|
) -> Result<Tag> {
|
||||||
Tag::insert(
|
Tag::insert(
|
||||||
conn,
|
conn,
|
||||||
NewTag {
|
NewTag {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use activitypub::{
|
use activitypub::{
|
||||||
actor::Person, collection::OrderedCollection, object::Image, Activity, CustomObject,
|
actor::Person, collection::OrderedCollection, object::Image, Activity, CustomObject, Endpoint,
|
||||||
Endpoint,
|
|
||||||
};
|
};
|
||||||
use bcrypt;
|
use bcrypt;
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
|
@ -27,7 +26,10 @@ use rocket::{
|
||||||
request::{self, FromRequest, Request},
|
request::{self, FromRequest, Request},
|
||||||
};
|
};
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use std::{cmp::PartialEq, hash::{Hash, Hasher}};
|
use std::{
|
||||||
|
cmp::PartialEq,
|
||||||
|
hash::{Hash, Hasher},
|
||||||
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use webfinger::*;
|
use webfinger::*;
|
||||||
|
|
||||||
|
@ -41,7 +43,7 @@ use posts::Post;
|
||||||
use safe_string::SafeString;
|
use safe_string::SafeString;
|
||||||
use schema::users;
|
use schema::users;
|
||||||
use search::Searcher;
|
use search::Searcher;
|
||||||
use {ap_url, Connection, BASE_URL, USE_HTTPS, Error, Result};
|
use {ap_url, Connection, Error, Result, BASE_URL, USE_HTTPS};
|
||||||
|
|
||||||
pub type CustomPerson = CustomObject<ApSignature, Person>;
|
pub type CustomPerson = CustomObject<ApSignature, Person>;
|
||||||
|
|
||||||
|
@ -97,42 +99,24 @@ impl User {
|
||||||
insert!(users, NewUser, |inserted, conn| {
|
insert!(users, NewUser, |inserted, conn| {
|
||||||
let instance = inserted.get_instance(conn)?;
|
let instance = inserted.get_instance(conn)?;
|
||||||
if inserted.outbox_url.is_empty() {
|
if inserted.outbox_url.is_empty() {
|
||||||
inserted.outbox_url = instance.compute_box(
|
inserted.outbox_url = instance.compute_box(USER_PREFIX, &inserted.username, "outbox");
|
||||||
USER_PREFIX,
|
|
||||||
&inserted.username,
|
|
||||||
"outbox",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if inserted.inbox_url.is_empty() {
|
if inserted.inbox_url.is_empty() {
|
||||||
inserted.inbox_url = instance.compute_box(
|
inserted.inbox_url = instance.compute_box(USER_PREFIX, &inserted.username, "inbox");
|
||||||
USER_PREFIX,
|
|
||||||
&inserted.username,
|
|
||||||
"inbox",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if inserted.ap_url.is_empty() {
|
if inserted.ap_url.is_empty() {
|
||||||
inserted.ap_url = instance.compute_box(
|
inserted.ap_url = instance.compute_box(USER_PREFIX, &inserted.username, "");
|
||||||
USER_PREFIX,
|
|
||||||
&inserted.username,
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if inserted.shared_inbox_url.is_none() {
|
if inserted.shared_inbox_url.is_none() {
|
||||||
inserted.shared_inbox_url = Some(ap_url(&format!(
|
inserted.shared_inbox_url = Some(ap_url(&format!("{}/inbox", instance.public_domain)));
|
||||||
"{}/inbox",
|
|
||||||
instance.public_domain
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if inserted.followers_endpoint.is_empty() {
|
if inserted.followers_endpoint.is_empty() {
|
||||||
inserted.followers_endpoint = instance.compute_box(
|
inserted.followers_endpoint =
|
||||||
USER_PREFIX,
|
instance.compute_box(USER_PREFIX, &inserted.username, "followers");
|
||||||
&inserted.username,
|
|
||||||
"followers",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if inserted.fqn.is_empty() {
|
if inserted.fqn.is_empty() {
|
||||||
|
@ -162,7 +146,8 @@ impl User {
|
||||||
|
|
||||||
for blog in Blog::find_for_author(conn, self)?
|
for blog in Blog::find_for_author(conn, self)?
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|b| b.count_authors(conn).map(|c| c <= 1).unwrap_or(false)) {
|
.filter(|b| b.count_authors(conn).map(|c| c <= 1).unwrap_or(false))
|
||||||
|
{
|
||||||
blog.delete(conn, searcher)?;
|
blog.delete(conn, searcher)?;
|
||||||
}
|
}
|
||||||
// delete the posts if they is the only author
|
// delete the posts if they is the only author
|
||||||
|
@ -180,10 +165,10 @@ impl User {
|
||||||
.count()
|
.count()
|
||||||
.load(conn)?
|
.load(conn)?
|
||||||
.first()
|
.first()
|
||||||
.unwrap_or(&0) > &0;
|
.unwrap_or(&0)
|
||||||
|
> &0;
|
||||||
if !has_other_authors {
|
if !has_other_authors {
|
||||||
Post::get(conn, post_id)?
|
Post::get(conn, post_id)?.delete(&(conn, searcher))?;
|
||||||
.delete(&(conn, searcher))?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,12 +198,18 @@ impl User {
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&self, conn: &Connection, name: String, email: String, summary: String) -> Result<User> {
|
pub fn update(
|
||||||
|
&self,
|
||||||
|
conn: &Connection,
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
summary: String,
|
||||||
|
) -> Result<User> {
|
||||||
diesel::update(self)
|
diesel::update(self)
|
||||||
.set((
|
.set((
|
||||||
users::display_name.eq(name),
|
users::display_name.eq(name),
|
||||||
users::email.eq(email),
|
users::email.eq(email),
|
||||||
users::summary_html.eq(utils::md_to_html(&summary,"").0),
|
users::summary_html.eq(utils::md_to_html(&summary, "").0),
|
||||||
users::summary.eq(summary),
|
users::summary.eq(summary),
|
||||||
))
|
))
|
||||||
.execute(conn)?;
|
.execute(conn)?;
|
||||||
|
@ -278,16 +269,13 @@ impl User {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch_from_url(conn: &Connection, url: &str) -> Result<User> {
|
pub fn fetch_from_url(conn: &Connection, url: &str) -> Result<User> {
|
||||||
User::fetch(url).and_then(|json| User::from_activity(
|
User::fetch(url)
|
||||||
conn,
|
.and_then(|json| User::from_activity(conn, &json, Url::parse(url)?.host_str()?))
|
||||||
&json,
|
|
||||||
Url::parse(url)?.host_str()?,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_activity(conn: &Connection, acct: &CustomPerson, inst: &str) -> Result<User> {
|
fn from_activity(conn: &Connection, acct: &CustomPerson, inst: &str) -> Result<User> {
|
||||||
let instance = Instance::find_by_domain(conn, inst)
|
let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
|
||||||
.or_else(|_| Instance::insert(
|
Instance::insert(
|
||||||
conn,
|
conn,
|
||||||
NewInstance {
|
NewInstance {
|
||||||
name: inst.to_owned(),
|
name: inst.to_owned(),
|
||||||
|
@ -301,9 +289,15 @@ impl User {
|
||||||
short_description_html: String::new(),
|
short_description_html: String::new(),
|
||||||
long_description_html: String::new(),
|
long_description_html: String::new(),
|
||||||
},
|
},
|
||||||
))?;
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
if acct.object.ap_actor_props.preferred_username_string()?.contains(&['<', '>', '&', '@', '\'', '"'][..]) {
|
if acct
|
||||||
|
.object
|
||||||
|
.ap_actor_props
|
||||||
|
.preferred_username_string()?
|
||||||
|
.contains(&['<', '>', '&', '@', '\'', '"'][..])
|
||||||
|
{
|
||||||
return Err(Error::InvalidValue);
|
return Err(Error::InvalidValue);
|
||||||
}
|
}
|
||||||
let user = User::insert(
|
let user = User::insert(
|
||||||
|
@ -314,20 +308,11 @@ impl User {
|
||||||
.ap_actor_props
|
.ap_actor_props
|
||||||
.preferred_username_string()
|
.preferred_username_string()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
display_name: acct
|
display_name: acct.object.object_props.name_string()?,
|
||||||
.object
|
outbox_url: acct.object.ap_actor_props.outbox_string()?,
|
||||||
.object_props
|
inbox_url: acct.object.ap_actor_props.inbox_string()?,
|
||||||
.name_string()?,
|
|
||||||
outbox_url: acct
|
|
||||||
.object
|
|
||||||
.ap_actor_props
|
|
||||||
.outbox_string()?,
|
|
||||||
inbox_url: acct
|
|
||||||
.object
|
|
||||||
.ap_actor_props
|
|
||||||
.inbox_string()?,
|
|
||||||
is_admin: false,
|
is_admin: false,
|
||||||
summary:acct
|
summary: acct
|
||||||
.object
|
.object
|
||||||
.object_props
|
.object_props
|
||||||
.summary_string()
|
.summary_string()
|
||||||
|
@ -342,10 +327,7 @@ impl User {
|
||||||
email: None,
|
email: None,
|
||||||
hashed_password: None,
|
hashed_password: None,
|
||||||
instance_id: instance.id,
|
instance_id: instance.id,
|
||||||
ap_url: acct
|
ap_url: acct.object.object_props.id_string()?,
|
||||||
.object
|
|
||||||
.object_props
|
|
||||||
.id_string()?,
|
|
||||||
public_key: acct
|
public_key: acct
|
||||||
.custom_props
|
.custom_props
|
||||||
.public_key_publickey()?
|
.public_key_publickey()?
|
||||||
|
@ -357,10 +339,7 @@ impl User {
|
||||||
.endpoints_endpoint()
|
.endpoints_endpoint()
|
||||||
.and_then(|e| e.shared_inbox_string())
|
.and_then(|e| e.shared_inbox_string())
|
||||||
.ok(),
|
.ok(),
|
||||||
followers_endpoint: acct
|
followers_endpoint: acct.object.ap_actor_props.followers_string()?,
|
||||||
.object
|
|
||||||
.ap_actor_props
|
|
||||||
.followers_string()?,
|
|
||||||
avatar_id: None,
|
avatar_id: None,
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
@ -392,26 +371,15 @@ impl User {
|
||||||
.object_props
|
.object_props
|
||||||
.url_string()?,
|
.url_string()?,
|
||||||
&self,
|
&self,
|
||||||
).ok();
|
)
|
||||||
|
.ok();
|
||||||
|
|
||||||
diesel::update(self)
|
diesel::update(self)
|
||||||
.set((
|
.set((
|
||||||
users::username.eq(json
|
users::username.eq(json.object.ap_actor_props.preferred_username_string()?),
|
||||||
.object
|
users::display_name.eq(json.object.object_props.name_string()?),
|
||||||
.ap_actor_props
|
users::outbox_url.eq(json.object.ap_actor_props.outbox_string()?),
|
||||||
.preferred_username_string()?),
|
users::inbox_url.eq(json.object.ap_actor_props.inbox_string()?),
|
||||||
users::display_name.eq(json
|
|
||||||
.object
|
|
||||||
.object_props
|
|
||||||
.name_string()?),
|
|
||||||
users::outbox_url.eq(json
|
|
||||||
.object
|
|
||||||
.ap_actor_props
|
|
||||||
.outbox_string()?),
|
|
||||||
users::inbox_url.eq(json
|
|
||||||
.object
|
|
||||||
.ap_actor_props
|
|
||||||
.inbox_string()?),
|
|
||||||
users::summary.eq(SafeString::new(
|
users::summary.eq(SafeString::new(
|
||||||
&json
|
&json
|
||||||
.object
|
.object
|
||||||
|
@ -419,10 +387,7 @@ impl User {
|
||||||
.summary_string()
|
.summary_string()
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
)),
|
)),
|
||||||
users::followers_endpoint.eq(json
|
users::followers_endpoint.eq(json.object.ap_actor_props.followers_string()?),
|
||||||
.object
|
|
||||||
.ap_actor_props
|
|
||||||
.followers_string()?),
|
|
||||||
users::avatar_id.eq(avatar.map(|a| a.id)),
|
users::avatar_id.eq(avatar.map(|a| a.id)),
|
||||||
users::last_fetched_date.eq(Utc::now().naive_utc()),
|
users::last_fetched_date.eq(Utc::now().naive_utc()),
|
||||||
users::public_key.eq(json
|
users::public_key.eq(json
|
||||||
|
@ -441,7 +406,8 @@ impl User {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn auth(&self, pass: &str) -> bool {
|
pub fn auth(&self, pass: &str) -> bool {
|
||||||
self.hashed_password.clone()
|
self.hashed_password
|
||||||
|
.clone()
|
||||||
.map(|hashed| bcrypt::verify(pass, hashed.as_ref()).unwrap_or(false))
|
.map(|hashed| bcrypt::verify(pass, hashed.as_ref()).unwrap_or(false))
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
@ -468,8 +434,7 @@ impl User {
|
||||||
let n_acts = acts.len();
|
let n_acts = acts.len();
|
||||||
let mut coll = OrderedCollection::default();
|
let mut coll = OrderedCollection::default();
|
||||||
coll.collection_props.items = serde_json::to_value(acts)?;
|
coll.collection_props.items = serde_json::to_value(acts)?;
|
||||||
coll.collection_props
|
coll.collection_props.set_total_items_u64(n_acts as u64)?;
|
||||||
.set_total_items_u64(n_acts as u64)?;
|
|
||||||
Ok(ActivityStream::new(coll))
|
Ok(ActivityStream::new(coll))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -483,12 +448,11 @@ impl User {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", "),
|
.join(", "),
|
||||||
)?
|
)?,
|
||||||
)
|
)
|
||||||
.send()?;
|
.send()?;
|
||||||
let text = &res.text()?;
|
let text = &res.text()?;
|
||||||
let json: serde_json::Value =
|
let json: serde_json::Value = serde_json::from_str(text)?;
|
||||||
serde_json::from_str(text)?;
|
|
||||||
Ok(json["items"]
|
Ok(json["items"]
|
||||||
.as_array()
|
.as_array()
|
||||||
.unwrap_or(&vec![])
|
.unwrap_or(&vec![])
|
||||||
|
@ -507,7 +471,7 @@ impl User {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", "),
|
.join(", "),
|
||||||
)?
|
)?,
|
||||||
)
|
)
|
||||||
.send()?;
|
.send()?;
|
||||||
let text = &res.text()?;
|
let text = &res.text()?;
|
||||||
|
@ -531,7 +495,9 @@ impl User {
|
||||||
Ok(posts
|
Ok(posts
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|p| {
|
.filter_map(|p| {
|
||||||
p.create_activity(conn).ok().and_then(|a| serde_json::to_value(a).ok())
|
p.create_activity(conn)
|
||||||
|
.ok()
|
||||||
|
.and_then(|a| serde_json::to_value(a).ok())
|
||||||
})
|
})
|
||||||
.collect::<Vec<serde_json::Value>>())
|
.collect::<Vec<serde_json::Value>>())
|
||||||
}
|
}
|
||||||
|
@ -555,7 +521,11 @@ impl User {
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_followers_page(&self, conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<User>> {
|
pub fn get_followers_page(
|
||||||
|
&self,
|
||||||
|
conn: &Connection,
|
||||||
|
(min, max): (i32, i32),
|
||||||
|
) -> Result<Vec<User>> {
|
||||||
use schema::follows;
|
use schema::follows;
|
||||||
let follows = Follow::belonging_to(self).select(follows::follower_id);
|
let follows = Follow::belonging_to(self).select(follows::follower_id);
|
||||||
users::table
|
users::table
|
||||||
|
@ -584,7 +554,11 @@ impl User {
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_followed_page(&self, conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<User>> {
|
pub fn get_followed_page(
|
||||||
|
&self,
|
||||||
|
conn: &Connection,
|
||||||
|
(min, max): (i32, i32),
|
||||||
|
) -> Result<Vec<User>> {
|
||||||
use schema::follows;
|
use schema::follows;
|
||||||
let follows = follows::table
|
let follows = follows::table
|
||||||
.filter(follows::follower_id.eq(self.id))
|
.filter(follows::follower_id.eq(self.id))
|
||||||
|
@ -653,33 +627,32 @@ impl User {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_keypair(&self) -> Result<PKey<Private>> {
|
pub fn get_keypair(&self) -> Result<PKey<Private>> {
|
||||||
PKey::from_rsa(
|
PKey::from_rsa(Rsa::private_key_from_pem(
|
||||||
Rsa::private_key_from_pem(
|
self.private_key.clone()?.as_ref(),
|
||||||
self.private_key
|
)?)
|
||||||
.clone()?
|
.map_err(Error::from)
|
||||||
.as_ref(),
|
|
||||||
)?,
|
|
||||||
).map_err(Error::from)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rotate_keypair(&self, conn: &Connection) -> Result<PKey<Private>> {
|
pub fn rotate_keypair(&self, conn: &Connection) -> Result<PKey<Private>> {
|
||||||
if self.private_key.is_none() {
|
if self.private_key.is_none() {
|
||||||
return Err(Error::InvalidValue)
|
return Err(Error::InvalidValue);
|
||||||
}
|
}
|
||||||
if (Utc::now().naive_utc() - self.last_fetched_date).num_minutes() < 10 {
|
if (Utc::now().naive_utc() - self.last_fetched_date).num_minutes() < 10 {
|
||||||
//rotated recently
|
//rotated recently
|
||||||
self.get_keypair()
|
self.get_keypair()
|
||||||
} else {
|
} else {
|
||||||
let (public_key, private_key) = gen_keypair();
|
let (public_key, private_key) = gen_keypair();
|
||||||
let public_key = String::from_utf8(public_key).expect("NewUser::new_local: public key error");
|
let public_key =
|
||||||
let private_key = String::from_utf8(private_key).expect("NewUser::new_local: private key error");
|
String::from_utf8(public_key).expect("NewUser::new_local: public key error");
|
||||||
let res = PKey::from_rsa(
|
let private_key =
|
||||||
Rsa::private_key_from_pem(private_key.as_ref())?
|
String::from_utf8(private_key).expect("NewUser::new_local: private key error");
|
||||||
)?;
|
let res = PKey::from_rsa(Rsa::private_key_from_pem(private_key.as_ref())?)?;
|
||||||
diesel::update(self)
|
diesel::update(self)
|
||||||
.set((users::public_key.eq(public_key),
|
.set((
|
||||||
|
users::public_key.eq(public_key),
|
||||||
users::private_key.eq(Some(private_key)),
|
users::private_key.eq(Some(private_key)),
|
||||||
users::last_fetched_date.eq(Utc::now().naive_utc())))
|
users::last_fetched_date.eq(Utc::now().naive_utc()),
|
||||||
|
))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
.map(|_| res)
|
.map(|_| res)
|
||||||
|
@ -688,18 +661,14 @@ impl User {
|
||||||
|
|
||||||
pub fn to_activity(&self, conn: &Connection) -> Result<CustomPerson> {
|
pub fn to_activity(&self, conn: &Connection) -> Result<CustomPerson> {
|
||||||
let mut actor = Person::default();
|
let mut actor = Person::default();
|
||||||
actor
|
actor.object_props.set_id_string(self.ap_url.clone())?;
|
||||||
.object_props
|
|
||||||
.set_id_string(self.ap_url.clone())?;
|
|
||||||
actor
|
actor
|
||||||
.object_props
|
.object_props
|
||||||
.set_name_string(self.display_name.clone())?;
|
.set_name_string(self.display_name.clone())?;
|
||||||
actor
|
actor
|
||||||
.object_props
|
.object_props
|
||||||
.set_summary_string(self.summary_html.get().clone())?;
|
.set_summary_string(self.summary_html.get().clone())?;
|
||||||
actor
|
actor.object_props.set_url_string(self.ap_url.clone())?;
|
||||||
.object_props
|
|
||||||
.set_url_string(self.ap_url.clone())?;
|
|
||||||
actor
|
actor
|
||||||
.ap_actor_props
|
.ap_actor_props
|
||||||
.set_inbox_string(self.inbox_url.clone())?;
|
.set_inbox_string(self.inbox_url.clone())?;
|
||||||
|
@ -714,42 +683,31 @@ impl User {
|
||||||
.set_followers_string(self.followers_endpoint.clone())?;
|
.set_followers_string(self.followers_endpoint.clone())?;
|
||||||
|
|
||||||
let mut endpoints = Endpoint::default();
|
let mut endpoints = Endpoint::default();
|
||||||
endpoints
|
endpoints.set_shared_inbox_string(ap_url(&format!("{}/inbox/", BASE_URL.as_str())))?;
|
||||||
.set_shared_inbox_string(ap_url(&format!("{}/inbox/", BASE_URL.as_str())))?;
|
actor.ap_actor_props.set_endpoints_endpoint(endpoints)?;
|
||||||
actor
|
|
||||||
.ap_actor_props
|
|
||||||
.set_endpoints_endpoint(endpoints)?;
|
|
||||||
|
|
||||||
let mut public_key = PublicKey::default();
|
let mut public_key = PublicKey::default();
|
||||||
public_key
|
public_key.set_id_string(format!("{}#main-key", self.ap_url))?;
|
||||||
.set_id_string(format!("{}#main-key", self.ap_url))?;
|
public_key.set_owner_string(self.ap_url.clone())?;
|
||||||
public_key
|
public_key.set_public_key_pem_string(self.public_key.clone())?;
|
||||||
.set_owner_string(self.ap_url.clone())?;
|
|
||||||
public_key
|
|
||||||
.set_public_key_pem_string(self.public_key.clone())?;
|
|
||||||
let mut ap_signature = ApSignature::default();
|
let mut ap_signature = ApSignature::default();
|
||||||
ap_signature
|
ap_signature.set_public_key_publickey(public_key)?;
|
||||||
.set_public_key_publickey(public_key)?;
|
|
||||||
|
|
||||||
let mut avatar = Image::default();
|
let mut avatar = Image::default();
|
||||||
avatar
|
avatar.object_props.set_url_string(
|
||||||
.object_props
|
self.avatar_id
|
||||||
.set_url_string(
|
.and_then(|id| Media::get(conn, id).and_then(|m| m.url(conn)).ok())
|
||||||
self.avatar_id
|
.unwrap_or_default(),
|
||||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url(conn)).ok())
|
)?;
|
||||||
.unwrap_or_default(),
|
actor.object_props.set_icon_object(avatar)?;
|
||||||
)?;
|
|
||||||
actor
|
|
||||||
.object_props
|
|
||||||
.set_icon_object(avatar)?;
|
|
||||||
|
|
||||||
Ok(CustomPerson::new(actor, ap_signature))
|
Ok(CustomPerson::new(actor, ap_signature))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn avatar_url(&self, conn: &Connection) -> String {
|
pub fn avatar_url(&self, conn: &Connection) -> String {
|
||||||
self.avatar_id.and_then(|id|
|
self.avatar_id
|
||||||
Media::get(conn, id).and_then(|m| m.url(conn)).ok()
|
.and_then(|id| Media::get(conn, id).and_then(|m| m.url(conn)).ok())
|
||||||
).unwrap_or_else(|| "/static/default-avatar.png".to_string())
|
.unwrap_or_else(|| "/static/default-avatar.png".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn webfinger(&self, conn: &Connection) -> Result<Webfinger> {
|
pub fn webfinger(&self, conn: &Connection) -> Result<Webfinger> {
|
||||||
|
@ -866,21 +824,15 @@ impl Signer for User {
|
||||||
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
|
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
|
||||||
let key = self.get_keypair()?;
|
let key = self.get_keypair()?;
|
||||||
let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?;
|
let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?;
|
||||||
signer
|
signer.update(to_sign.as_bytes())?;
|
||||||
.update(to_sign.as_bytes())?;
|
signer.sign_to_vec().map_err(Error::from)
|
||||||
signer
|
|
||||||
.sign_to_vec()
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
|
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
|
||||||
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
|
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
|
||||||
let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?;
|
let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?;
|
||||||
verifier
|
verifier.update(data.as_bytes())?;
|
||||||
.update(data.as_bytes())?;
|
verifier.verify(&signature).map_err(Error::from)
|
||||||
verifier
|
|
||||||
.verify(&signature)
|
|
||||||
.map_err(Error::from)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -915,7 +867,7 @@ impl NewUser {
|
||||||
display_name,
|
display_name,
|
||||||
is_admin,
|
is_admin,
|
||||||
summary: summary.to_owned(),
|
summary: summary.to_owned(),
|
||||||
summary_html: SafeString::new(&utils::md_to_html(&summary,"").0),
|
summary_html: SafeString::new(&utils::md_to_html(&summary, "").0),
|
||||||
email: Some(email),
|
email: Some(email),
|
||||||
hashed_password: Some(password),
|
hashed_password: Some(password),
|
||||||
instance_id: Instance::get_local(conn)?.id,
|
instance_id: Instance::get_local(conn)?.id,
|
||||||
|
@ -947,7 +899,8 @@ pub(crate) mod tests {
|
||||||
"Hello there, I'm the admin",
|
"Hello there, I'm the admin",
|
||||||
"admin@example.com".to_owned(),
|
"admin@example.com".to_owned(),
|
||||||
"invalid_admin_password".to_owned(),
|
"invalid_admin_password".to_owned(),
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
let user = NewUser::new_local(
|
let user = NewUser::new_local(
|
||||||
conn,
|
conn,
|
||||||
"user".to_owned(),
|
"user".to_owned(),
|
||||||
|
@ -956,7 +909,8 @@ pub(crate) mod tests {
|
||||||
"Hello there, I'm no one",
|
"Hello there, I'm no one",
|
||||||
"user@example.com".to_owned(),
|
"user@example.com".to_owned(),
|
||||||
"invalid_user_password".to_owned(),
|
"invalid_user_password".to_owned(),
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
let other = NewUser::new_local(
|
let other = NewUser::new_local(
|
||||||
conn,
|
conn,
|
||||||
"other".to_owned(),
|
"other".to_owned(),
|
||||||
|
@ -965,8 +919,9 @@ pub(crate) mod tests {
|
||||||
"Hello there, I'm someone else",
|
"Hello there, I'm someone else",
|
||||||
"other@example.com".to_owned(),
|
"other@example.com".to_owned(),
|
||||||
"invalid_other_password".to_owned(),
|
"invalid_other_password".to_owned(),
|
||||||
).unwrap();
|
)
|
||||||
vec![ admin, user, other ]
|
.unwrap();
|
||||||
|
vec![admin, user, other]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -982,7 +937,8 @@ pub(crate) mod tests {
|
||||||
"Hello I'm a test",
|
"Hello I'm a test",
|
||||||
"test@example.com".to_owned(),
|
"test@example.com".to_owned(),
|
||||||
User::hash_pass("test_password").unwrap(),
|
User::hash_pass("test_password").unwrap(),
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
test_user.id,
|
test_user.id,
|
||||||
|
@ -996,9 +952,7 @@ pub(crate) mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
test_user.id,
|
test_user.id,
|
||||||
User::find_by_email(conn, "test@example.com")
|
User::find_by_email(conn, "test@example.com").unwrap().id
|
||||||
.unwrap()
|
|
||||||
.id
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
test_user.id,
|
test_user.id,
|
||||||
|
@ -1009,8 +963,9 @@ pub(crate) mod tests {
|
||||||
Instance::get_local(conn).unwrap().public_domain,
|
Instance::get_local(conn).unwrap().public_domain,
|
||||||
"test"
|
"test"
|
||||||
)
|
)
|
||||||
).unwrap()
|
)
|
||||||
.id
|
.unwrap()
|
||||||
|
.id
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1040,7 +995,11 @@ pub(crate) mod tests {
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while local_inst.has_admin(conn).unwrap() {
|
while local_inst.has_admin(conn).unwrap() {
|
||||||
assert!(i < 100); //prevent from looping indefinitelly
|
assert!(i < 100); //prevent from looping indefinitelly
|
||||||
local_inst.main_admin(conn).unwrap().revoke_admin_rights(conn).unwrap();
|
local_inst
|
||||||
|
.main_admin(conn)
|
||||||
|
.unwrap()
|
||||||
|
.revoke_admin_rights(conn)
|
||||||
|
.unwrap();
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
inserted[0].grant_admin_rights(conn).unwrap();
|
inserted[0].grant_admin_rights(conn).unwrap();
|
||||||
|
@ -1055,12 +1014,14 @@ pub(crate) mod tests {
|
||||||
let conn = &db();
|
let conn = &db();
|
||||||
conn.test_transaction::<_, (), _>(|| {
|
conn.test_transaction::<_, (), _>(|| {
|
||||||
let inserted = fill_database(conn);
|
let inserted = fill_database(conn);
|
||||||
let updated = inserted[0].update(
|
let updated = inserted[0]
|
||||||
conn,
|
.update(
|
||||||
"new name".to_owned(),
|
conn,
|
||||||
"em@il".to_owned(),
|
"new name".to_owned(),
|
||||||
"<p>summary</p><script></script>".to_owned(),
|
"em@il".to_owned(),
|
||||||
).unwrap();
|
"<p>summary</p><script></script>".to_owned(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(updated.display_name, "new name");
|
assert_eq!(updated.display_name, "new name");
|
||||||
assert_eq!(updated.email.unwrap(), "em@il");
|
assert_eq!(updated.email.unwrap(), "em@il");
|
||||||
assert_eq!(updated.summary_html.get(), "<p>summary</p>");
|
assert_eq!(updated.summary_html.get(), "<p>summary</p>");
|
||||||
|
@ -1082,7 +1043,8 @@ pub(crate) mod tests {
|
||||||
"Hello I'm a test",
|
"Hello I'm a test",
|
||||||
"test@example.com".to_owned(),
|
"test@example.com".to_owned(),
|
||||||
User::hash_pass("test_password").unwrap(),
|
User::hash_pass("test_password").unwrap(),
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert!(test_user.auth("test_password"));
|
assert!(test_user.auth("test_password"));
|
||||||
assert!(!test_user.auth("other_password"));
|
assert!(!test_user.auth("other_password"));
|
||||||
|
@ -1101,7 +1063,9 @@ pub(crate) mod tests {
|
||||||
assert_eq!(page.len(), 2);
|
assert_eq!(page.len(), 2);
|
||||||
assert!(page[0].username <= page[1].username);
|
assert!(page[0].username <= page[1].username);
|
||||||
|
|
||||||
let mut last_username = User::get_local_page(conn, (0, 1)).unwrap()[0].username.clone();
|
let mut last_username = User::get_local_page(conn, (0, 1)).unwrap()[0]
|
||||||
|
.username
|
||||||
|
.clone();
|
||||||
for i in 1..User::count_local(conn).unwrap() as i32 {
|
for i in 1..User::count_local(conn).unwrap() as i32 {
|
||||||
let page = User::get_local_page(conn, (i, i + 1)).unwrap();
|
let page = User::get_local_page(conn, (i, i + 1)).unwrap();
|
||||||
assert_eq!(page.len(), 1);
|
assert_eq!(page.len(), 1);
|
||||||
|
@ -1109,7 +1073,9 @@ pub(crate) mod tests {
|
||||||
last_username = page[0].username.clone();
|
last_username = page[0].username.clone();
|
||||||
}
|
}
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
User::get_local_page(conn, (0, User::count_local(conn).unwrap() as i32 + 10)).unwrap().len() as i64,
|
User::get_local_page(conn, (0, User::count_local(conn).unwrap() as i32 + 10))
|
||||||
|
.unwrap()
|
||||||
|
.len() as i64,
|
||||||
User::count_local(conn).unwrap()
|
User::count_local(conn).unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,7 @@ use rocket_contrib::json::Json;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
use plume_api::apps::AppEndpoint;
|
use plume_api::apps::AppEndpoint;
|
||||||
use plume_models::{
|
use plume_models::{apps::App, db_conn::DbConn, Connection};
|
||||||
Connection,
|
|
||||||
db_conn::DbConn,
|
|
||||||
apps::App,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[post("/apps", data = "<data>")]
|
#[post("/apps", data = "<data>")]
|
||||||
pub fn create(conn: DbConn, data: Json<AppEndpoint>) -> Json<serde_json::Value> {
|
pub fn create(conn: DbConn, data: Json<AppEndpoint>) -> Json<serde_json::Value> {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
|
use plume_models::{self, api_tokens::ApiToken};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
Outcome,
|
|
||||||
http::Status,
|
http::Status,
|
||||||
request::{self, FromRequest, Request}
|
request::{self, FromRequest, Request},
|
||||||
|
Outcome,
|
||||||
};
|
};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use plume_models::{self, api_tokens::ApiToken};
|
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
pub trait Action {
|
pub trait Action {
|
||||||
|
@ -33,22 +33,25 @@ impl Scope for plume_models::posts::Post {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Authorization<A, S> (pub ApiToken, PhantomData<(A, S)>);
|
pub struct Authorization<A, S>(pub ApiToken, PhantomData<(A, S)>);
|
||||||
|
|
||||||
impl<'a, 'r, A, S> FromRequest<'a, 'r> for Authorization<A, S>
|
impl<'a, 'r, A, S> FromRequest<'a, 'r> for Authorization<A, S>
|
||||||
where A: Action,
|
where
|
||||||
S: Scope
|
A: Action,
|
||||||
|
S: Scope,
|
||||||
{
|
{
|
||||||
type Error = ();
|
type Error = ();
|
||||||
|
|
||||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Authorization<A, S>, ()> {
|
fn from_request(request: &'a Request<'r>) -> request::Outcome<Authorization<A, S>, ()> {
|
||||||
request.guard::<ApiToken>()
|
request
|
||||||
|
.guard::<ApiToken>()
|
||||||
.map_failure(|_| (Status::Unauthorized, ()))
|
.map_failure(|_| (Status::Unauthorized, ()))
|
||||||
.and_then(|token| if token.can(A::to_str(), S::to_str()) {
|
.and_then(|token| {
|
||||||
Outcome::Success(Authorization(token, PhantomData))
|
if token.can(A::to_str(), S::to_str()) {
|
||||||
} else {
|
Outcome::Success(Authorization(token, PhantomData))
|
||||||
Outcome::Failure((Status::Unauthorized, ()))
|
} else {
|
||||||
|
Outcome::Failure((Status::Unauthorized, ()))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
#![warn(clippy::too_many_arguments)]
|
#![warn(clippy::too_many_arguments)]
|
||||||
use rocket::{response::{self, Responder}, request::{Form, Request}};
|
use rocket::{
|
||||||
|
request::{Form, Request},
|
||||||
|
response::{self, Responder},
|
||||||
|
};
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
use plume_common::utils::random_hex;
|
use plume_common::utils::random_hex;
|
||||||
use plume_models::{
|
use plume_models::{api_tokens::*, apps::App, db_conn::DbConn, users::User, Error};
|
||||||
Error,
|
|
||||||
apps::App,
|
|
||||||
api_tokens::*,
|
|
||||||
db_conn::DbConn,
|
|
||||||
users::User,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ApiError(Error);
|
pub struct ApiError(Error);
|
||||||
|
@ -26,13 +23,16 @@ impl<'r> Responder<'r> for ApiError {
|
||||||
match self.0 {
|
match self.0 {
|
||||||
Error::NotFound => Json(json!({
|
Error::NotFound => Json(json!({
|
||||||
"error": "Not found"
|
"error": "Not found"
|
||||||
})).respond_to(req),
|
}))
|
||||||
|
.respond_to(req),
|
||||||
Error::Unauthorized => Json(json!({
|
Error::Unauthorized => Json(json!({
|
||||||
"error": "You are not authorized to access this resource"
|
"error": "You are not authorized to access this resource"
|
||||||
})).respond_to(req),
|
}))
|
||||||
|
.respond_to(req),
|
||||||
_ => Json(json!({
|
_ => Json(json!({
|
||||||
"error": "Server error"
|
"error": "Server error"
|
||||||
})).respond_to(req)
|
}))
|
||||||
|
.respond_to(req),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,12 +52,15 @@ pub fn oauth(query: Form<OAuthRequest>, conn: DbConn) -> Result<Json<serde_json:
|
||||||
if app.client_secret == query.client_secret {
|
if app.client_secret == query.client_secret {
|
||||||
if let Ok(user) = User::find_by_fqn(&*conn, &query.username) {
|
if let Ok(user) = User::find_by_fqn(&*conn, &query.username) {
|
||||||
if user.auth(&query.password) {
|
if user.auth(&query.password) {
|
||||||
let token = ApiToken::insert(&*conn, NewApiToken {
|
let token = ApiToken::insert(
|
||||||
app_id: app.id,
|
&*conn,
|
||||||
user_id: user.id,
|
NewApiToken {
|
||||||
value: random_hex(),
|
app_id: app.id,
|
||||||
scopes: query.scopes.clone(),
|
user_id: user.id,
|
||||||
})?;
|
value: random_hex(),
|
||||||
|
scopes: query.scopes.clone(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"token": token.value
|
"token": token.value
|
||||||
})))
|
})))
|
||||||
|
|
|
@ -5,43 +5,79 @@ use scheduled_thread_pool::ScheduledThreadPool;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use serde_qs;
|
use serde_qs;
|
||||||
|
|
||||||
|
use api::authorization::*;
|
||||||
use plume_api::posts::PostEndpoint;
|
use plume_api::posts::PostEndpoint;
|
||||||
use plume_models::{
|
use plume_models::{
|
||||||
Connection,
|
db_conn::DbConn, posts::Post, search::Searcher as UnmanagedSearcher, Connection,
|
||||||
db_conn::DbConn,
|
|
||||||
posts::Post,
|
|
||||||
search::Searcher as UnmanagedSearcher,
|
|
||||||
};
|
};
|
||||||
use api::authorization::*;
|
|
||||||
use {Searcher, Worker};
|
use {Searcher, Worker};
|
||||||
|
|
||||||
#[get("/posts/<id>")]
|
#[get("/posts/<id>")]
|
||||||
pub fn get(id: i32, conn: DbConn, worker: Worker, auth: Option<Authorization<Read, Post>>, search: Searcher) -> Json<serde_json::Value> {
|
pub fn get(
|
||||||
let post = <Post as Provider<(&Connection, &ScheduledThreadPool, &UnmanagedSearcher, Option<i32>)>>
|
id: i32,
|
||||||
::get(&(&*conn, &worker, &search, auth.map(|a| a.0.user_id)), id).ok();
|
conn: DbConn,
|
||||||
|
worker: Worker,
|
||||||
|
auth: Option<Authorization<Read, Post>>,
|
||||||
|
search: Searcher,
|
||||||
|
) -> Json<serde_json::Value> {
|
||||||
|
let post = <Post as Provider<(
|
||||||
|
&Connection,
|
||||||
|
&ScheduledThreadPool,
|
||||||
|
&UnmanagedSearcher,
|
||||||
|
Option<i32>,
|
||||||
|
)>>::get(&(&*conn, &worker, &search, auth.map(|a| a.0.user_id)), id)
|
||||||
|
.ok();
|
||||||
Json(json!(post))
|
Json(json!(post))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/posts")]
|
#[get("/posts")]
|
||||||
pub fn list(conn: DbConn, uri: &Origin, worker: Worker, auth: Option<Authorization<Read, Post>>, search: Searcher) -> Json<serde_json::Value> {
|
pub fn list(
|
||||||
let query: PostEndpoint = serde_qs::from_str(uri.query().unwrap_or("")).expect("api::list: invalid query error");
|
conn: DbConn,
|
||||||
let post = <Post as Provider<(&Connection, &ScheduledThreadPool, &UnmanagedSearcher, Option<i32>)>>
|
uri: &Origin,
|
||||||
::list(&(&*conn, &worker, &search, auth.map(|a| a.0.user_id)), query);
|
worker: Worker,
|
||||||
|
auth: Option<Authorization<Read, Post>>,
|
||||||
|
search: Searcher,
|
||||||
|
) -> Json<serde_json::Value> {
|
||||||
|
let query: PostEndpoint =
|
||||||
|
serde_qs::from_str(uri.query().unwrap_or("")).expect("api::list: invalid query error");
|
||||||
|
let post = <Post as Provider<(
|
||||||
|
&Connection,
|
||||||
|
&ScheduledThreadPool,
|
||||||
|
&UnmanagedSearcher,
|
||||||
|
Option<i32>,
|
||||||
|
)>>::list(
|
||||||
|
&(&*conn, &worker, &search, auth.map(|a| a.0.user_id)),
|
||||||
|
query,
|
||||||
|
);
|
||||||
Json(json!(post))
|
Json(json!(post))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/posts", data = "<payload>")]
|
#[post("/posts", data = "<payload>")]
|
||||||
pub fn create(conn: DbConn, payload: Json<PostEndpoint>, worker: Worker, auth: Authorization<Write, Post>, search: Searcher) -> Json<serde_json::Value> {
|
pub fn create(
|
||||||
let new_post = <Post as Provider<(&Connection, &ScheduledThreadPool, &UnmanagedSearcher, Option<i32>)>>
|
conn: DbConn,
|
||||||
::create(&(&*conn, &worker, &search, Some(auth.0.user_id)), (*payload).clone());
|
payload: Json<PostEndpoint>,
|
||||||
Json(new_post.map(|p| json!(p)).unwrap_or_else(|e| json!({
|
worker: Worker,
|
||||||
"error": "Invalid data, couldn't create new post",
|
auth: Authorization<Write, Post>,
|
||||||
"details": match e {
|
search: Searcher,
|
||||||
ApiError::Fetch(msg) => msg,
|
) -> Json<serde_json::Value> {
|
||||||
ApiError::SerDe(msg) => msg,
|
let new_post = <Post as Provider<(
|
||||||
ApiError::NotFound(msg) => msg,
|
&Connection,
|
||||||
ApiError::Authorization(msg) => msg,
|
&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)]
|
#![warn(clippy::too_many_arguments)]
|
||||||
use activitypub::{
|
use activitypub::{
|
||||||
activity::{
|
activity::{Announce, Create, Delete, Follow as FollowAct, Like, Undo, Update},
|
||||||
Announce,
|
object::Tombstone,
|
||||||
Create,
|
|
||||||
Delete,
|
|
||||||
Follow as FollowAct,
|
|
||||||
Like,
|
|
||||||
Undo,
|
|
||||||
Update
|
|
||||||
},
|
|
||||||
object::Tombstone
|
|
||||||
};
|
};
|
||||||
use failure::Error;
|
use failure::Error;
|
||||||
use rocket::{
|
use rocket::{data::*, http::Status, Outcome::*, Request};
|
||||||
data::*,
|
|
||||||
http::Status,
|
|
||||||
Outcome::*,
|
|
||||||
Request,
|
|
||||||
};
|
|
||||||
use rocket_contrib::json::*;
|
use rocket_contrib::json::*;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
@ -26,15 +13,21 @@ use std::io::Read;
|
||||||
|
|
||||||
use plume_common::activity_pub::{
|
use plume_common::activity_pub::{
|
||||||
inbox::{Deletable, FromActivity, InboxError, Notify},
|
inbox::{Deletable, FromActivity, InboxError, Notify},
|
||||||
Id,request::Digest,
|
request::Digest,
|
||||||
|
Id,
|
||||||
};
|
};
|
||||||
use plume_models::{
|
use plume_models::{
|
||||||
comments::Comment, follows::Follow, instance::Instance, likes, posts::Post, reshares::Reshare,
|
comments::Comment, follows::Follow, instance::Instance, likes, posts::Post, reshares::Reshare,
|
||||||
users::User, search::Searcher, Connection,
|
search::Searcher, users::User, Connection,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait Inbox {
|
pub trait Inbox {
|
||||||
fn received(&self, conn: &Connection, searcher: &Searcher, act: serde_json::Value) -> Result<(), Error> {
|
fn received(
|
||||||
|
&self,
|
||||||
|
conn: &Connection,
|
||||||
|
searcher: &Searcher,
|
||||||
|
act: serde_json::Value,
|
||||||
|
) -> Result<(), Error> {
|
||||||
let actor_id = Id::new(act["actor"].as_str().unwrap_or_else(|| {
|
let actor_id = Id::new(act["actor"].as_str().unwrap_or_else(|| {
|
||||||
act["actor"]["id"]
|
act["actor"]["id"]
|
||||||
.as_str()
|
.as_str()
|
||||||
|
@ -66,7 +59,8 @@ pub trait Inbox {
|
||||||
.id_string()?,
|
.id_string()?,
|
||||||
actor_id.as_ref(),
|
actor_id.as_ref(),
|
||||||
&(conn, searcher),
|
&(conn, searcher),
|
||||||
).ok();
|
)
|
||||||
|
.ok();
|
||||||
Comment::delete_id(
|
Comment::delete_id(
|
||||||
&act.delete_props
|
&act.delete_props
|
||||||
.object_object::<Tombstone>()?
|
.object_object::<Tombstone>()?
|
||||||
|
@ -74,12 +68,14 @@ pub trait Inbox {
|
||||||
.id_string()?,
|
.id_string()?,
|
||||||
actor_id.as_ref(),
|
actor_id.as_ref(),
|
||||||
conn,
|
conn,
|
||||||
).ok();
|
)
|
||||||
|
.ok();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
"Follow" => {
|
"Follow" => {
|
||||||
Follow::from_activity(conn, serde_json::from_value(act.clone())?, actor_id)
|
Follow::from_activity(conn, serde_json::from_value(act.clone())?, actor_id)
|
||||||
.and_then(|f| f.notify(conn)).expect("Inbox::received: follow from activity error");;
|
.and_then(|f| f.notify(conn))
|
||||||
|
.expect("Inbox::received: follow from activity error");;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
"Like" => {
|
"Like" => {
|
||||||
|
@ -87,7 +83,8 @@ pub trait Inbox {
|
||||||
conn,
|
conn,
|
||||||
serde_json::from_value(act.clone())?,
|
serde_json::from_value(act.clone())?,
|
||||||
actor_id,
|
actor_id,
|
||||||
).expect("Inbox::received: like from activity error");;
|
)
|
||||||
|
.expect("Inbox::received: like from activity error");;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
"Undo" => {
|
"Undo" => {
|
||||||
|
@ -102,7 +99,8 @@ pub trait Inbox {
|
||||||
.id_string()?,
|
.id_string()?,
|
||||||
actor_id.as_ref(),
|
actor_id.as_ref(),
|
||||||
conn,
|
conn,
|
||||||
).expect("Inbox::received: undo like fail");;
|
)
|
||||||
|
.expect("Inbox::received: undo like fail");;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
"Announce" => {
|
"Announce" => {
|
||||||
|
@ -113,7 +111,8 @@ pub trait Inbox {
|
||||||
.id_string()?,
|
.id_string()?,
|
||||||
actor_id.as_ref(),
|
actor_id.as_ref(),
|
||||||
conn,
|
conn,
|
||||||
).expect("Inbox::received: undo reshare fail");;
|
)
|
||||||
|
.expect("Inbox::received: undo reshare fail");;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
"Follow" => {
|
"Follow" => {
|
||||||
|
@ -124,21 +123,28 @@ pub trait Inbox {
|
||||||
.id_string()?,
|
.id_string()?,
|
||||||
actor_id.as_ref(),
|
actor_id.as_ref(),
|
||||||
conn,
|
conn,
|
||||||
).expect("Inbox::received: undo follow error");;
|
)
|
||||||
|
.expect("Inbox::received: undo follow error");;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
_ => Err(InboxError::CantUndo)?,
|
_ => Err(InboxError::CantUndo)?,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let link = act.undo_props.object.as_str().expect("Inbox::received: undo doesn't contain a type and isn't Link");
|
let link =
|
||||||
|
act.undo_props.object.as_str().expect(
|
||||||
|
"Inbox::received: undo doesn't contain a type and isn't Link",
|
||||||
|
);
|
||||||
if let Ok(like) = likes::Like::find_by_ap_url(conn, link) {
|
if let Ok(like) = likes::Like::find_by_ap_url(conn, link) {
|
||||||
likes::Like::delete_id(&like.ap_url, actor_id.as_ref(), conn).expect("Inbox::received: delete Like error");
|
likes::Like::delete_id(&like.ap_url, actor_id.as_ref(), conn)
|
||||||
|
.expect("Inbox::received: delete Like error");
|
||||||
Ok(())
|
Ok(())
|
||||||
} else if let Ok(reshare) = Reshare::find_by_ap_url(conn, link) {
|
} else if let Ok(reshare) = Reshare::find_by_ap_url(conn, link) {
|
||||||
Reshare::delete_id(&reshare.ap_url, actor_id.as_ref(), conn).expect("Inbox::received: delete Announce error");
|
Reshare::delete_id(&reshare.ap_url, actor_id.as_ref(), conn)
|
||||||
|
.expect("Inbox::received: delete Announce error");
|
||||||
Ok(())
|
Ok(())
|
||||||
} else if let Ok(follow) = Follow::find_by_ap_url(conn, link) {
|
} else if let Ok(follow) = Follow::find_by_ap_url(conn, link) {
|
||||||
Follow::delete_id(&follow.ap_url, actor_id.as_ref(), conn).expect("Inbox::received: delete Follow error");
|
Follow::delete_id(&follow.ap_url, actor_id.as_ref(), conn)
|
||||||
|
.expect("Inbox::received: delete Follow error");
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(InboxError::NoType)?
|
Err(InboxError::NoType)?
|
||||||
|
@ -147,7 +153,8 @@ pub trait Inbox {
|
||||||
}
|
}
|
||||||
"Update" => {
|
"Update" => {
|
||||||
let act: Update = serde_json::from_value(act.clone())?;
|
let act: Update = serde_json::from_value(act.clone())?;
|
||||||
Post::handle_update(conn, &act.update_props.object_object()?, searcher).expect("Inbox::received: post update error");
|
Post::handle_update(conn, &act.update_props.object_object()?, searcher)
|
||||||
|
.expect("Inbox::received: post update error");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
_ => Err(InboxError::InvalidType)?,
|
_ => Err(InboxError::InvalidType)?,
|
||||||
|
@ -169,19 +176,25 @@ impl<'a, T: Deserialize<'a>> FromData<'a> for SignedJson<T> {
|
||||||
type Owned = String;
|
type Owned = String;
|
||||||
type Borrowed = str;
|
type Borrowed = str;
|
||||||
|
|
||||||
fn transform(r: &Request, d: Data) -> Transform<rocket::data::Outcome<Self::Owned, Self::Error>> {
|
fn transform(
|
||||||
|
r: &Request,
|
||||||
|
d: Data,
|
||||||
|
) -> Transform<rocket::data::Outcome<Self::Owned, Self::Error>> {
|
||||||
let size_limit = r.limits().get("json").unwrap_or(JSON_LIMIT);
|
let size_limit = r.limits().get("json").unwrap_or(JSON_LIMIT);
|
||||||
let mut s = String::with_capacity(512);
|
let mut s = String::with_capacity(512);
|
||||||
match d.open().take(size_limit).read_to_string(&mut s) {
|
match d.open().take(size_limit).read_to_string(&mut s) {
|
||||||
Ok(_) => Transform::Borrowed(Success(s)),
|
Ok(_) => Transform::Borrowed(Success(s)),
|
||||||
Err(e) => Transform::Borrowed(Failure((Status::BadRequest, JsonError::Io(e))))
|
Err(e) => Transform::Borrowed(Failure((Status::BadRequest, JsonError::Io(e)))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_data(_: &Request, o: Transformed<'a, Self>) -> rocket::data::Outcome<Self, Self::Error> {
|
fn from_data(
|
||||||
|
_: &Request,
|
||||||
|
o: Transformed<'a, Self>,
|
||||||
|
) -> rocket::data::Outcome<Self, Self::Error> {
|
||||||
let string = o.borrowed()?;
|
let string = o.borrowed()?;
|
||||||
match serde_json::from_str(&string) {
|
match serde_json::from_str(&string) {
|
||||||
Ok(v) => Success(SignedJson(Digest::from_body(&string),Json(v))),
|
Ok(v) => Success(SignedJson(Digest::from_body(&string), Json(v))),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if e.is_data() {
|
if e.is_data() {
|
||||||
Failure((Status::UnprocessableEntity, JsonError::Parse(string, e)))
|
Failure((Status::UnprocessableEntity, JsonError::Parse(string, e)))
|
||||||
|
|
35
src/mail.rs
35
src/mail.rs
|
@ -6,8 +6,8 @@ pub use self::mailer::*;
|
||||||
|
|
||||||
#[cfg(feature = "debug-mailer")]
|
#[cfg(feature = "debug-mailer")]
|
||||||
mod mailer {
|
mod mailer {
|
||||||
use lettre::{Transport, SendableEmail};
|
use lettre::{SendableEmail, Transport};
|
||||||
use std::{io::Read};
|
use std::io::Read;
|
||||||
|
|
||||||
pub struct DebugTransport;
|
pub struct DebugTransport;
|
||||||
|
|
||||||
|
@ -18,11 +18,18 @@ mod mailer {
|
||||||
println!(
|
println!(
|
||||||
"{}: from=<{}> to=<{:?}>\n{:#?}",
|
"{}: from=<{}> to=<{:?}>\n{:#?}",
|
||||||
email.message_id().to_string(),
|
email.message_id().to_string(),
|
||||||
email.envelope().from().map(ToString::to_string).unwrap_or_default(),
|
email
|
||||||
|
.envelope()
|
||||||
|
.from()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.unwrap_or_default(),
|
||||||
email.envelope().to().to_vec(),
|
email.envelope().to().to_vec(),
|
||||||
{
|
{
|
||||||
let mut message = String::new();
|
let mut message = String::new();
|
||||||
email.message().read_to_string(&mut message).map_err(|_| ())?;
|
email
|
||||||
|
.message()
|
||||||
|
.read_to_string(&mut message)
|
||||||
|
.map_err(|_| ())?;
|
||||||
message
|
message
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -40,13 +47,12 @@ mod mailer {
|
||||||
#[cfg(not(feature = "debug-mailer"))]
|
#[cfg(not(feature = "debug-mailer"))]
|
||||||
mod mailer {
|
mod mailer {
|
||||||
use lettre::{
|
use lettre::{
|
||||||
SmtpTransport,
|
|
||||||
SmtpClient,
|
|
||||||
smtp::{
|
smtp::{
|
||||||
authentication::{Credentials, Mechanism},
|
authentication::{Credentials, Mechanism},
|
||||||
extension::ClientId,
|
extension::ClientId,
|
||||||
ConnectionReuseParameters,
|
ConnectionReuseParameters,
|
||||||
},
|
},
|
||||||
|
SmtpClient, SmtpTransport,
|
||||||
};
|
};
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
|
@ -57,7 +63,8 @@ mod mailer {
|
||||||
let helo_name = env::var("MAIL_HELO_NAME").unwrap_or_else(|_| "localhost".to_owned());
|
let helo_name = env::var("MAIL_HELO_NAME").unwrap_or_else(|_| "localhost".to_owned());
|
||||||
let username = env::var("MAIL_USER").ok()?;
|
let username = env::var("MAIL_USER").ok()?;
|
||||||
let password = env::var("MAIL_PASSWORD").ok()?;
|
let password = env::var("MAIL_PASSWORD").ok()?;
|
||||||
let mail = SmtpClient::new_simple(&server).unwrap()
|
let mail = SmtpClient::new_simple(&server)
|
||||||
|
.unwrap()
|
||||||
.hello_name(ClientId::Domain(helo_name))
|
.hello_name(ClientId::Domain(helo_name))
|
||||||
.credentials(Credentials::new(username, password))
|
.credentials(Credentials::new(username, password))
|
||||||
.smtp_utf8(true)
|
.smtp_utf8(true)
|
||||||
|
@ -70,9 +77,17 @@ mod mailer {
|
||||||
|
|
||||||
pub fn build_mail(dest: String, subject: String, body: String) -> Option<Email> {
|
pub fn build_mail(dest: String, subject: String, body: String) -> Option<Email> {
|
||||||
Email::builder()
|
Email::builder()
|
||||||
.from(env::var("MAIL_ADDRESS")
|
.from(
|
||||||
.or_else(|_| Ok(format!("{}@{}", env::var("MAIL_USER")?, env::var("MAIL_SERVER")?)) as Result<_, env::VarError>)
|
env::var("MAIL_ADDRESS")
|
||||||
.expect("Mail server is not correctly configured"))
|
.or_else(|_| {
|
||||||
|
Ok(format!(
|
||||||
|
"{}@{}",
|
||||||
|
env::var("MAIL_USER")?,
|
||||||
|
env::var("MAIL_SERVER")?
|
||||||
|
)) as Result<_, env::VarError>
|
||||||
|
})
|
||||||
|
.expect("Mail server is not correctly configured"),
|
||||||
|
)
|
||||||
.to(dest)
|
.to(dest)
|
||||||
.subject(subject)
|
.subject(subject)
|
||||||
.text(body)
|
.text(body)
|
||||||
|
|
299
src/main.rs
299
src/main.rs
|
@ -39,16 +39,13 @@ extern crate validator_derive;
|
||||||
extern crate webfinger;
|
extern crate webfinger;
|
||||||
|
|
||||||
use diesel::r2d2::ConnectionManager;
|
use diesel::r2d2::ConnectionManager;
|
||||||
use rocket::{
|
|
||||||
Config, State,
|
|
||||||
config::Limits
|
|
||||||
};
|
|
||||||
use rocket_csrf::CsrfFairingBuilder;
|
|
||||||
use plume_models::{
|
use plume_models::{
|
||||||
DATABASE_URL, Connection, Error,
|
|
||||||
db_conn::{DbPool, PragmaForeignKey},
|
db_conn::{DbPool, PragmaForeignKey},
|
||||||
search::{Searcher as UnmanagedSearcher, SearcherError},
|
search::{Searcher as UnmanagedSearcher, SearcherError},
|
||||||
|
Connection, Error, DATABASE_URL,
|
||||||
};
|
};
|
||||||
|
use rocket::{config::Limits, Config, State};
|
||||||
|
use rocket_csrf::CsrfFairingBuilder;
|
||||||
use scheduled_thread_pool::ScheduledThreadPool;
|
use scheduled_thread_pool::ScheduledThreadPool;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
|
@ -78,7 +75,8 @@ fn init_pool() -> Option<DbPool> {
|
||||||
let manager = ConnectionManager::<Connection>::new(DATABASE_URL.as_str());
|
let manager = ConnectionManager::<Connection>::new(DATABASE_URL.as_str());
|
||||||
DbPool::builder()
|
DbPool::builder()
|
||||||
.connection_customizer(Box::new(PragmaForeignKey))
|
.connection_customizer(Box::new(PragmaForeignKey))
|
||||||
.build(manager).ok()
|
.build(manager)
|
||||||
|
.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
@ -89,37 +87,58 @@ fn main() {
|
||||||
let searcher = match UnmanagedSearcher::open(&"search_index") {
|
let searcher = match UnmanagedSearcher::open(&"search_index") {
|
||||||
Err(Error::Search(e)) => match e {
|
Err(Error::Search(e)) => match e {
|
||||||
SearcherError::WriteLockAcquisitionError => panic!(
|
SearcherError::WriteLockAcquisitionError => panic!(
|
||||||
r#"Your search index is locked. Plume can't start. To fix this issue
|
r#"Your search index is locked. Plume can't start. To fix this issue
|
||||||
make sure no other Plume instance is started, and run:
|
make sure no other Plume instance is started, and run:
|
||||||
|
|
||||||
plm search unlock
|
plm search unlock
|
||||||
|
|
||||||
Then try to restart Plume.
|
Then try to restart Plume.
|
||||||
"#),
|
"#
|
||||||
e => Err(e).unwrap()
|
),
|
||||||
|
e => Err(e).unwrap(),
|
||||||
},
|
},
|
||||||
Err(_) => panic!("Unexpected error while opening search index"),
|
Err(_) => panic!("Unexpected error while opening search index"),
|
||||||
Ok(s) => Arc::new(s)
|
Ok(s) => Arc::new(s),
|
||||||
};
|
};
|
||||||
|
|
||||||
let commiter = searcher.clone();
|
let commiter = searcher.clone();
|
||||||
workpool.execute_with_fixed_delay(Duration::from_secs(5), Duration::from_secs(60*30), move || commiter.commit());
|
workpool.execute_with_fixed_delay(
|
||||||
|
Duration::from_secs(5),
|
||||||
|
Duration::from_secs(60 * 30),
|
||||||
|
move || commiter.commit(),
|
||||||
|
);
|
||||||
|
|
||||||
let search_unlocker = searcher.clone();
|
let search_unlocker = searcher.clone();
|
||||||
ctrlc::set_handler(move || {
|
ctrlc::set_handler(move || {
|
||||||
search_unlocker.drop_writer();
|
search_unlocker.drop_writer();
|
||||||
exit(0);
|
exit(0);
|
||||||
}).expect("Error setting Ctrl-c handler");
|
})
|
||||||
|
.expect("Error setting Ctrl-c handler");
|
||||||
|
|
||||||
let mut config = Config::active().unwrap();
|
let mut config = Config::active().unwrap();
|
||||||
config.set_address(env::var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned())).unwrap();
|
config
|
||||||
config.set_port(env::var("ROCKET_PORT").ok().map(|s| s.parse::<u16>().unwrap()).unwrap_or(7878));
|
.set_address(env::var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned()))
|
||||||
|
.unwrap();
|
||||||
|
config.set_port(
|
||||||
|
env::var("ROCKET_PORT")
|
||||||
|
.ok()
|
||||||
|
.map(|s| s.parse::<u16>().unwrap())
|
||||||
|
.unwrap_or(7878),
|
||||||
|
);
|
||||||
let _ = env::var("ROCKET_SECRET_KEY").map(|k| config.set_secret_key(k).unwrap());
|
let _ = env::var("ROCKET_SECRET_KEY").map(|k| config.set_secret_key(k).unwrap());
|
||||||
let form_size = &env::var("FORM_SIZE").unwrap_or_else(|_| "32".to_owned()).parse::<u64>().unwrap();
|
let form_size = &env::var("FORM_SIZE")
|
||||||
let activity_size = &env::var("ACTIVITY_SIZE").unwrap_or_else(|_| "1024".to_owned()).parse::<u64>().unwrap();
|
.unwrap_or_else(|_| "32".to_owned())
|
||||||
config.set_limits(Limits::new()
|
.parse::<u64>()
|
||||||
.limit("forms", form_size * 1024)
|
.unwrap();
|
||||||
.limit("json", activity_size * 1024));
|
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();
|
let mail = mail::init();
|
||||||
if mail.is_none() && config.environment.is_prod() {
|
if mail.is_none() && config.environment.is_prod() {
|
||||||
|
@ -128,110 +147,100 @@ Then try to restart Plume.
|
||||||
}
|
}
|
||||||
|
|
||||||
rocket::custom(config)
|
rocket::custom(config)
|
||||||
.mount("/", routes![
|
.mount(
|
||||||
routes::blogs::details,
|
"/",
|
||||||
routes::blogs::activity_details,
|
routes![
|
||||||
routes::blogs::outbox,
|
routes::blogs::details,
|
||||||
routes::blogs::new,
|
routes::blogs::activity_details,
|
||||||
routes::blogs::new_auth,
|
routes::blogs::outbox,
|
||||||
routes::blogs::create,
|
routes::blogs::new,
|
||||||
routes::blogs::delete,
|
routes::blogs::new_auth,
|
||||||
routes::blogs::atom_feed,
|
routes::blogs::create,
|
||||||
|
routes::blogs::delete,
|
||||||
routes::comments::create,
|
routes::blogs::atom_feed,
|
||||||
routes::comments::delete,
|
routes::comments::create,
|
||||||
routes::comments::activity_pub,
|
routes::comments::delete,
|
||||||
|
routes::comments::activity_pub,
|
||||||
routes::instance::index,
|
routes::instance::index,
|
||||||
routes::instance::local,
|
routes::instance::local,
|
||||||
routes::instance::feed,
|
routes::instance::feed,
|
||||||
routes::instance::federated,
|
routes::instance::federated,
|
||||||
routes::instance::admin,
|
routes::instance::admin,
|
||||||
routes::instance::admin_instances,
|
routes::instance::admin_instances,
|
||||||
routes::instance::admin_users,
|
routes::instance::admin_users,
|
||||||
routes::instance::ban,
|
routes::instance::ban,
|
||||||
routes::instance::toggle_block,
|
routes::instance::toggle_block,
|
||||||
routes::instance::update_settings,
|
routes::instance::update_settings,
|
||||||
routes::instance::shared_inbox,
|
routes::instance::shared_inbox,
|
||||||
routes::instance::nodeinfo,
|
routes::instance::nodeinfo,
|
||||||
routes::instance::about,
|
routes::instance::about,
|
||||||
routes::instance::web_manifest,
|
routes::instance::web_manifest,
|
||||||
|
routes::likes::create,
|
||||||
routes::likes::create,
|
routes::likes::create_auth,
|
||||||
routes::likes::create_auth,
|
routes::medias::list,
|
||||||
|
routes::medias::new,
|
||||||
routes::medias::list,
|
routes::medias::upload,
|
||||||
routes::medias::new,
|
routes::medias::details,
|
||||||
routes::medias::upload,
|
routes::medias::delete,
|
||||||
routes::medias::details,
|
routes::medias::set_avatar,
|
||||||
routes::medias::delete,
|
routes::notifications::notifications,
|
||||||
routes::medias::set_avatar,
|
routes::notifications::notifications_auth,
|
||||||
|
routes::posts::details,
|
||||||
routes::notifications::notifications,
|
routes::posts::activity_details,
|
||||||
routes::notifications::notifications_auth,
|
routes::posts::edit,
|
||||||
|
routes::posts::update,
|
||||||
routes::posts::details,
|
routes::posts::new,
|
||||||
routes::posts::activity_details,
|
routes::posts::new_auth,
|
||||||
routes::posts::edit,
|
routes::posts::create,
|
||||||
routes::posts::update,
|
routes::posts::delete,
|
||||||
routes::posts::new,
|
routes::reshares::create,
|
||||||
routes::posts::new_auth,
|
routes::reshares::create_auth,
|
||||||
routes::posts::create,
|
routes::search::search,
|
||||||
routes::posts::delete,
|
routes::session::new,
|
||||||
|
routes::session::create,
|
||||||
routes::reshares::create,
|
routes::session::delete,
|
||||||
routes::reshares::create_auth,
|
routes::session::password_reset_request_form,
|
||||||
|
routes::session::password_reset_request,
|
||||||
routes::search::search,
|
routes::session::password_reset_form,
|
||||||
|
routes::session::password_reset,
|
||||||
routes::session::new,
|
routes::plume_static_files,
|
||||||
routes::session::create,
|
routes::static_files,
|
||||||
routes::session::delete,
|
routes::tags::tag,
|
||||||
routes::session::password_reset_request_form,
|
routes::user::me,
|
||||||
routes::session::password_reset_request,
|
routes::user::details,
|
||||||
routes::session::password_reset_form,
|
routes::user::dashboard,
|
||||||
routes::session::password_reset,
|
routes::user::dashboard_auth,
|
||||||
|
routes::user::followers,
|
||||||
routes::plume_static_files,
|
routes::user::followed,
|
||||||
routes::static_files,
|
routes::user::edit,
|
||||||
|
routes::user::edit_auth,
|
||||||
routes::tags::tag,
|
routes::user::update,
|
||||||
|
routes::user::delete,
|
||||||
routes::user::me,
|
routes::user::follow,
|
||||||
routes::user::details,
|
routes::user::follow_auth,
|
||||||
routes::user::dashboard,
|
routes::user::activity_details,
|
||||||
routes::user::dashboard_auth,
|
routes::user::outbox,
|
||||||
routes::user::followers,
|
routes::user::inbox,
|
||||||
routes::user::followed,
|
routes::user::ap_followers,
|
||||||
routes::user::edit,
|
routes::user::new,
|
||||||
routes::user::edit_auth,
|
routes::user::create,
|
||||||
routes::user::update,
|
routes::user::atom_feed,
|
||||||
routes::user::delete,
|
routes::well_known::host_meta,
|
||||||
routes::user::follow,
|
routes::well_known::nodeinfo,
|
||||||
routes::user::follow_auth,
|
routes::well_known::webfinger,
|
||||||
routes::user::activity_details,
|
routes::errors::csrf_violation
|
||||||
routes::user::outbox,
|
],
|
||||||
routes::user::inbox,
|
)
|
||||||
routes::user::ap_followers,
|
.mount(
|
||||||
routes::user::new,
|
"/api/v1",
|
||||||
routes::user::create,
|
routes![
|
||||||
routes::user::atom_feed,
|
api::oauth,
|
||||||
|
api::apps::create,
|
||||||
routes::well_known::host_meta,
|
api::posts::get,
|
||||||
routes::well_known::nodeinfo,
|
api::posts::list,
|
||||||
routes::well_known::webfinger,
|
api::posts::create,
|
||||||
|
],
|
||||||
routes::errors::csrf_violation
|
)
|
||||||
])
|
|
||||||
.mount("/api/v1", routes![
|
|
||||||
api::oauth,
|
|
||||||
|
|
||||||
api::apps::create,
|
|
||||||
|
|
||||||
api::posts::get,
|
|
||||||
api::posts::list,
|
|
||||||
api::posts::create,
|
|
||||||
])
|
|
||||||
.register(catchers![
|
.register(catchers![
|
||||||
routes::errors::not_found,
|
routes::errors::not_found,
|
||||||
routes::errors::unprocessable_entity,
|
routes::errors::unprocessable_entity,
|
||||||
|
@ -243,15 +252,41 @@ Then try to restart Plume.
|
||||||
.manage(workpool)
|
.manage(workpool)
|
||||||
.manage(searcher)
|
.manage(searcher)
|
||||||
.manage(include_i18n!())
|
.manage(include_i18n!())
|
||||||
.attach(CsrfFairingBuilder::new()
|
.attach(
|
||||||
.set_default_target("/csrf-violation?target=<uri>".to_owned(), rocket::http::Method::Post)
|
CsrfFairingBuilder::new()
|
||||||
|
.set_default_target(
|
||||||
|
"/csrf-violation?target=<uri>".to_owned(),
|
||||||
|
rocket::http::Method::Post,
|
||||||
|
)
|
||||||
.add_exceptions(vec![
|
.add_exceptions(vec![
|
||||||
("/inbox".to_owned(), "/inbox".to_owned(), rocket::http::Method::Post),
|
(
|
||||||
("/@/<name>/inbox".to_owned(), "/@/<name>/inbox".to_owned(), rocket::http::Method::Post),
|
"/inbox".to_owned(),
|
||||||
("/login".to_owned(), "/login".to_owned(), rocket::http::Method::Post),
|
"/inbox".to_owned(),
|
||||||
("/users/new".to_owned(), "/users/new".to_owned(), rocket::http::Method::Post),
|
rocket::http::Method::Post,
|
||||||
("/api/<path..>".to_owned(), "/api/<path..>".to_owned(), rocket::http::Method::Post)
|
),
|
||||||
|
(
|
||||||
|
"/@/<name>/inbox".to_owned(),
|
||||||
|
"/@/<name>/inbox".to_owned(),
|
||||||
|
rocket::http::Method::Post,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/login".to_owned(),
|
||||||
|
"/login".to_owned(),
|
||||||
|
rocket::http::Method::Post,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/users/new".to_owned(),
|
||||||
|
"/users/new".to_owned(),
|
||||||
|
rocket::http::Method::Post,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/api/<path..>".to_owned(),
|
||||||
|
"/api/<path..>".to_owned(),
|
||||||
|
rocket::http::Method::Post,
|
||||||
|
),
|
||||||
])
|
])
|
||||||
.finalize().expect("main: csrf fairing creation error"))
|
.finalize()
|
||||||
|
.expect("main: csrf fairing creation error"),
|
||||||
|
)
|
||||||
.launch();
|
.launch();
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,22 +3,16 @@ use atom_syndication::{Entry, FeedBuilder};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::ContentType,
|
http::ContentType,
|
||||||
request::LenientForm,
|
request::LenientForm,
|
||||||
response::{Redirect, Flash, content::Content}
|
response::{content::Content, Flash, Redirect},
|
||||||
};
|
};
|
||||||
use rocket_i18n::I18n;
|
use rocket_i18n::I18n;
|
||||||
use std::{collections::HashMap, borrow::Cow};
|
use std::{borrow::Cow, collections::HashMap};
|
||||||
use validator::{Validate, ValidationError, ValidationErrors};
|
use validator::{Validate, ValidationError, ValidationErrors};
|
||||||
|
|
||||||
use plume_common::activity_pub::{ActivityStream, ApRequest};
|
use plume_common::activity_pub::{ActivityStream, ApRequest};
|
||||||
use plume_common::utils;
|
use plume_common::utils;
|
||||||
use plume_models::{
|
use plume_models::{blog_authors::*, blogs::*, db_conn::DbConn, instance::Instance, posts::Post};
|
||||||
blog_authors::*,
|
use routes::{errors::ErrorPage, Page, PlumeRocket};
|
||||||
blogs::*,
|
|
||||||
db_conn::DbConn,
|
|
||||||
instance::Instance,
|
|
||||||
posts::Post,
|
|
||||||
};
|
|
||||||
use routes::{Page, PlumeRocket, errors::ErrorPage};
|
|
||||||
use template_utils::Ructe;
|
use template_utils::Ructe;
|
||||||
|
|
||||||
#[get("/~/<name>?<page>", rank = 2)]
|
#[get("/~/<name>?<page>", rank = 2)]
|
||||||
|
@ -39,13 +33,18 @@ pub fn details(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result
|
||||||
articles_count,
|
articles_count,
|
||||||
page.0,
|
page.0,
|
||||||
Page::total(articles_count as i32),
|
Page::total(articles_count as i32),
|
||||||
user.and_then(|x| x.is_author_in(&*conn, &blog).ok()).unwrap_or(false),
|
user.and_then(|x| x.is_author_in(&*conn, &blog).ok())
|
||||||
|
.unwrap_or(false),
|
||||||
posts
|
posts
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/~/<name>", rank = 1)]
|
#[get("/~/<name>", rank = 1)]
|
||||||
pub fn activity_details(name: String, conn: DbConn, _ap: ApRequest) -> Option<ActivityStream<CustomGroup>> {
|
pub fn activity_details(
|
||||||
|
name: String,
|
||||||
|
conn: DbConn,
|
||||||
|
_ap: ApRequest,
|
||||||
|
) -> Option<ActivityStream<CustomGroup>> {
|
||||||
let blog = Blog::find_by_fqn(&*conn, &name).ok()?;
|
let blog = Blog::find_by_fqn(&*conn, &name).ok()?;
|
||||||
Some(ActivityStream::new(blog.to_activity(&*conn).ok()?))
|
Some(ActivityStream::new(blog.to_activity(&*conn).ok()?))
|
||||||
}
|
}
|
||||||
|
@ -64,10 +63,13 @@ pub fn new(rockets: PlumeRocket) -> Ructe {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/blogs/new", rank = 2)]
|
#[get("/blogs/new", rank = 2)]
|
||||||
pub fn new_auth(i18n: I18n) -> Flash<Redirect>{
|
pub fn new_auth(i18n: I18n) -> Flash<Redirect> {
|
||||||
utils::requires_login(
|
utils::requires_login(
|
||||||
&i18n!(i18n.catalog, "You need to be logged in order to create a new blog"),
|
&i18n!(
|
||||||
uri!(new)
|
i18n.catalog,
|
||||||
|
"You need to be logged in order to create a new blog"
|
||||||
|
),
|
||||||
|
uri!(new),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,29 +97,43 @@ pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> Result<Re
|
||||||
|
|
||||||
let mut errors = match form.validate() {
|
let mut errors = match form.validate() {
|
||||||
Ok(_) => ValidationErrors::new(),
|
Ok(_) => ValidationErrors::new(),
|
||||||
Err(e) => e
|
Err(e) => e,
|
||||||
};
|
};
|
||||||
if Blog::find_by_fqn(&*conn, &slug).is_ok() {
|
if Blog::find_by_fqn(&*conn, &slug).is_ok() {
|
||||||
errors.add("title", ValidationError {
|
errors.add(
|
||||||
code: Cow::from("existing_slug"),
|
"title",
|
||||||
message: Some(Cow::from("A blog with the same name already exists.")),
|
ValidationError {
|
||||||
params: HashMap::new()
|
code: Cow::from("existing_slug"),
|
||||||
});
|
message: Some(Cow::from("A blog with the same name already exists.")),
|
||||||
|
params: HashMap::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.is_empty() {
|
if errors.is_empty() {
|
||||||
let blog = Blog::insert(&*conn, NewBlog::new_local(
|
let blog = Blog::insert(
|
||||||
slug.clone(),
|
&*conn,
|
||||||
form.title.to_string(),
|
NewBlog::new_local(
|
||||||
String::from(""),
|
slug.clone(),
|
||||||
Instance::get_local(&*conn).expect("blog::create: instance error").id
|
form.title.to_string(),
|
||||||
).expect("blog::create: new local error")).expect("blog::create: error");
|
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 {
|
BlogAuthor::insert(
|
||||||
blog_id: blog.id,
|
&*conn,
|
||||||
author_id: user.id,
|
NewBlogAuthor {
|
||||||
is_owner: true
|
blog_id: blog.id,
|
||||||
}).expect("blog::create: author error");
|
author_id: user.id,
|
||||||
|
is_owner: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("blog::create: author error");
|
||||||
|
|
||||||
Ok(Redirect::to(uri!(details: name = slug.clone(), page = _)))
|
Ok(Redirect::to(uri!(details: name = slug.clone(), page = _)))
|
||||||
} else {
|
} else {
|
||||||
|
@ -130,15 +146,20 @@ pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> Result<Re
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/~/<name>/delete")]
|
#[post("/~/<name>/delete")]
|
||||||
pub fn delete(name: String, rockets: PlumeRocket) -> Result<Redirect, Ructe>{
|
pub fn delete(name: String, rockets: PlumeRocket) -> Result<Redirect, Ructe> {
|
||||||
let conn = rockets.conn;
|
let conn = rockets.conn;
|
||||||
let blog = Blog::find_by_fqn(&*conn, &name).expect("blog::delete: blog not found");
|
let blog = Blog::find_by_fqn(&*conn, &name).expect("blog::delete: blog not found");
|
||||||
let user = rockets.user;
|
let user = rockets.user;
|
||||||
let intl = rockets.intl;
|
let intl = rockets.intl;
|
||||||
let searcher = rockets.searcher;
|
let searcher = rockets.searcher;
|
||||||
|
|
||||||
if user.clone().and_then(|u| u.is_author_in(&*conn, &blog).ok()).unwrap_or(false) {
|
if user
|
||||||
blog.delete(&conn, &searcher).expect("blog::expect: deletion error");
|
.clone()
|
||||||
|
.and_then(|u| u.is_author_in(&*conn, &blog).ok())
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
blog.delete(&conn, &searcher)
|
||||||
|
.expect("blog::expect: deletion error");
|
||||||
Ok(Redirect::to(uri!(super::instance::index)))
|
Ok(Redirect::to(uri!(super::instance::index)))
|
||||||
} else {
|
} else {
|
||||||
// TODO actually return 403 error code
|
// TODO actually return 403 error code
|
||||||
|
@ -160,12 +181,20 @@ pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
|
||||||
let blog = Blog::find_by_fqn(&*conn, &name).ok()?;
|
let blog = Blog::find_by_fqn(&*conn, &name).ok()?;
|
||||||
let feed = FeedBuilder::default()
|
let feed = FeedBuilder::default()
|
||||||
.title(blog.title.clone())
|
.title(blog.title.clone())
|
||||||
.id(Instance::get_local(&*conn).ok()?
|
.id(Instance::get_local(&*conn)
|
||||||
|
.ok()?
|
||||||
.compute_box("~", &name, "atom.xml"))
|
.compute_box("~", &name, "atom.xml"))
|
||||||
.entries(Post::get_recents_for_blog(&*conn, &blog, 15).ok()?
|
.entries(
|
||||||
.into_iter()
|
Post::get_recents_for_blog(&*conn, &blog, 15)
|
||||||
.map(|p| super::post_to_atom(p, &*conn))
|
.ok()?
|
||||||
.collect::<Vec<Entry>>())
|
.into_iter()
|
||||||
.build().ok()?;
|
.map(|p| super::post_to_atom(p, &*conn))
|
||||||
Some(Content(ContentType::new("application", "atom+xml"), feed.to_string()))
|
.collect::<Vec<Entry>>(),
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
.ok()?;
|
||||||
|
Some(Content(
|
||||||
|
ContentType::new("application", "atom+xml"),
|
||||||
|
feed.to_string(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,21 @@
|
||||||
use activitypub::object::Note;
|
use activitypub::object::Note;
|
||||||
use rocket::{
|
use rocket::{request::LenientForm, response::Redirect};
|
||||||
request::LenientForm,
|
|
||||||
response::Redirect
|
|
||||||
};
|
|
||||||
use rocket_i18n::I18n;
|
use rocket_i18n::I18n;
|
||||||
use validator::Validate;
|
|
||||||
use template_utils::Ructe;
|
use template_utils::Ructe;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use plume_common::{utils, activity_pub::{broadcast, ApRequest,
|
use plume_common::{
|
||||||
ActivityStream, inbox::Deletable}};
|
activity_pub::{broadcast, inbox::Deletable, ActivityStream, ApRequest},
|
||||||
use plume_models::{
|
utils,
|
||||||
blogs::Blog,
|
};
|
||||||
comments::*,
|
use plume_models::{
|
||||||
db_conn::DbConn,
|
blogs::Blog, comments::*, db_conn::DbConn, instance::Instance, mentions::Mention, posts::Post,
|
||||||
instance::Instance,
|
safe_string::SafeString, tags::Tag, users::User,
|
||||||
mentions::Mention,
|
|
||||||
posts::Post,
|
|
||||||
safe_string::SafeString,
|
|
||||||
tags::Tag,
|
|
||||||
users::User
|
|
||||||
};
|
};
|
||||||
use Worker;
|
|
||||||
use routes::errors::ErrorPage;
|
use routes::errors::ErrorPage;
|
||||||
|
use Worker;
|
||||||
|
|
||||||
#[derive(Default, FromForm, Debug, Validate)]
|
#[derive(Default, FromForm, Debug, Validate)]
|
||||||
pub struct NewCommentForm {
|
pub struct NewCommentForm {
|
||||||
|
@ -34,37 +26,54 @@ pub struct NewCommentForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/~/<blog_name>/<slug>/comment", data = "<form>")]
|
#[post("/~/<blog_name>/<slug>/comment", data = "<form>")]
|
||||||
pub fn create(blog_name: String, slug: String, form: LenientForm<NewCommentForm>, user: User, conn: DbConn, worker: Worker, intl: I18n)
|
pub fn create(
|
||||||
-> Result<Redirect, Ructe> {
|
blog_name: String,
|
||||||
|
slug: String,
|
||||||
|
form: LenientForm<NewCommentForm>,
|
||||||
|
user: User,
|
||||||
|
conn: DbConn,
|
||||||
|
worker: Worker,
|
||||||
|
intl: I18n,
|
||||||
|
) -> Result<Redirect, Ructe> {
|
||||||
let blog = Blog::find_by_fqn(&*conn, &blog_name).expect("comments::create: blog error");
|
let blog = Blog::find_by_fqn(&*conn, &blog_name).expect("comments::create: blog error");
|
||||||
let post = Post::find_by_slug(&*conn, &slug, blog.id).expect("comments::create: post error");
|
let post = Post::find_by_slug(&*conn, &slug, blog.id).expect("comments::create: post error");
|
||||||
form.validate()
|
form.validate()
|
||||||
.map(|_| {
|
.map(|_| {
|
||||||
let (html, mentions, _hashtags) = utils::md_to_html(
|
let (html, mentions, _hashtags) = utils::md_to_html(
|
||||||
form.content.as_ref(),
|
form.content.as_ref(),
|
||||||
&Instance::get_local(&conn).expect("comments::create: local instance error").public_domain
|
&Instance::get_local(&conn)
|
||||||
|
.expect("comments::create: local instance error")
|
||||||
|
.public_domain,
|
||||||
);
|
);
|
||||||
let comm = Comment::insert(&*conn, NewComment {
|
let comm = Comment::insert(
|
||||||
content: SafeString::new(html.as_ref()),
|
&*conn,
|
||||||
in_response_to_id: form.responding_to,
|
NewComment {
|
||||||
post_id: post.id,
|
content: SafeString::new(html.as_ref()),
|
||||||
author_id: user.id,
|
in_response_to_id: form.responding_to,
|
||||||
ap_url: None,
|
post_id: post.id,
|
||||||
sensitive: !form.warning.is_empty(),
|
author_id: user.id,
|
||||||
spoiler_text: form.warning.clone(),
|
ap_url: None,
|
||||||
public_visibility: true
|
sensitive: !form.warning.is_empty(),
|
||||||
}).expect("comments::create: insert error");
|
spoiler_text: form.warning.clone(),
|
||||||
let new_comment = comm.create_activity(&*conn).expect("comments::create: activity error");
|
public_visibility: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("comments::create: insert error");
|
||||||
|
let new_comment = comm
|
||||||
|
.create_activity(&*conn)
|
||||||
|
.expect("comments::create: activity error");
|
||||||
|
|
||||||
// save mentions
|
// save mentions
|
||||||
for ment in mentions {
|
for ment in mentions {
|
||||||
Mention::from_activity(
|
Mention::from_activity(
|
||||||
&*conn,
|
&*conn,
|
||||||
&Mention::build_activity(&*conn, &ment).expect("comments::create: build mention error"),
|
&Mention::build_activity(&*conn, &ment)
|
||||||
|
.expect("comments::create: build mention error"),
|
||||||
post.id,
|
post.id,
|
||||||
true,
|
true,
|
||||||
true
|
true,
|
||||||
).expect("comments::create: mention save error");
|
)
|
||||||
|
.expect("comments::create: mention save error");
|
||||||
}
|
}
|
||||||
|
|
||||||
// federate
|
// federate
|
||||||
|
@ -72,13 +81,18 @@ pub fn create(blog_name: String, slug: String, form: LenientForm<NewCommentForm>
|
||||||
let user_clone = user.clone();
|
let user_clone = user.clone();
|
||||||
worker.execute(move || broadcast(&user_clone, new_comment, dest));
|
worker.execute(move || broadcast(&user_clone, new_comment, dest));
|
||||||
|
|
||||||
Redirect::to(uri!(super::posts::details: blog = blog_name, slug = slug, responding_to = _))
|
Redirect::to(
|
||||||
|
uri!(super::posts::details: blog = blog_name, slug = slug, responding_to = _),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.map_err(|errors| {
|
.map_err(|errors| {
|
||||||
// TODO: de-duplicate this code
|
// TODO: de-duplicate this code
|
||||||
let comments = CommentTree::from_post(&*conn, &post, Some(&user)).expect("comments::create: comments error");
|
let comments = CommentTree::from_post(&*conn, &post, Some(&user))
|
||||||
|
.expect("comments::create: comments error");
|
||||||
|
|
||||||
let previous = form.responding_to.and_then(|r| Comment::get(&*conn, r).ok());
|
let previous = form
|
||||||
|
.responding_to
|
||||||
|
.and_then(|r| Comment::get(&*conn, r).ok());
|
||||||
|
|
||||||
render!(posts::details(
|
render!(posts::details(
|
||||||
&(&*conn, &intl.catalog, Some(user.clone())),
|
&(&*conn, &intl.catalog, Some(user.clone())),
|
||||||
|
@ -89,33 +103,62 @@ pub fn create(blog_name: String, slug: String, form: LenientForm<NewCommentForm>
|
||||||
Tag::for_post(&*conn, post.id).expect("comments::create: tags error"),
|
Tag::for_post(&*conn, post.id).expect("comments::create: tags error"),
|
||||||
comments,
|
comments,
|
||||||
previous,
|
previous,
|
||||||
post.count_likes(&*conn).expect("comments::create: count likes error"),
|
post.count_likes(&*conn)
|
||||||
post.count_reshares(&*conn).expect("comments::create: count reshares error"),
|
.expect("comments::create: count likes error"),
|
||||||
user.has_liked(&*conn, &post).expect("comments::create: liked error"),
|
post.count_reshares(&*conn)
|
||||||
user.has_reshared(&*conn, &post).expect("comments::create: reshared error"),
|
.expect("comments::create: count reshares error"),
|
||||||
user.is_following(&*conn, post.get_authors(&*conn).expect("comments::create: authors error")[0].id)
|
user.has_liked(&*conn, &post)
|
||||||
.expect("comments::create: following error"),
|
.expect("comments::create: liked error"),
|
||||||
post.get_authors(&*conn).expect("comments::create: authors error")[0].clone()
|
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")]
|
#[post("/~/<blog>/<slug>/comment/<id>/delete")]
|
||||||
pub fn delete(blog: String, slug: String, id: i32, user: User, conn: DbConn, worker: Worker) -> Result<Redirect, ErrorPage> {
|
pub fn delete(
|
||||||
|
blog: String,
|
||||||
|
slug: String,
|
||||||
|
id: i32,
|
||||||
|
user: User,
|
||||||
|
conn: DbConn,
|
||||||
|
worker: Worker,
|
||||||
|
) -> Result<Redirect, ErrorPage> {
|
||||||
if let Ok(comment) = Comment::get(&*conn, id) {
|
if let Ok(comment) = Comment::get(&*conn, id) {
|
||||||
if comment.author_id == user.id {
|
if comment.author_id == user.id {
|
||||||
let dest = User::one_by_instance(&*conn)?;
|
let dest = User::one_by_instance(&*conn)?;
|
||||||
let delete_activity = comment.delete(&*conn)?;
|
let delete_activity = comment.delete(&*conn)?;
|
||||||
let user_c = user.clone();
|
let user_c = user.clone();
|
||||||
worker.execute(move || broadcast(&user_c, delete_activity, dest));
|
worker.execute(move || broadcast(&user_c, delete_activity, dest));
|
||||||
worker.execute_after(Duration::from_secs(10*60), move || {user.rotate_keypair(&conn).expect("Failed to rotate keypair");});
|
worker.execute_after(Duration::from_secs(10 * 60), move || {
|
||||||
|
user.rotate_keypair(&conn)
|
||||||
|
.expect("Failed to rotate keypair");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _)))
|
Ok(Redirect::to(
|
||||||
|
uri!(super::posts::details: blog = blog, slug = slug, responding_to = _),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/~/<_blog>/<_slug>/comment/<id>")]
|
#[get("/~/<_blog>/<_slug>/comment/<id>")]
|
||||||
pub fn activity_pub(_blog: String, _slug: String, id: i32, _ap: ApRequest, conn: DbConn) -> Option<ActivityStream<Note>> {
|
pub fn activity_pub(
|
||||||
|
_blog: String,
|
||||||
|
_slug: String,
|
||||||
|
id: i32,
|
||||||
|
_ap: ApRequest,
|
||||||
|
conn: DbConn,
|
||||||
|
) -> Option<ActivityStream<Note>> {
|
||||||
Comment::get(&*conn, id)
|
Comment::get(&*conn, id)
|
||||||
.and_then(|c| c.to_activity(&*conn))
|
.and_then(|c| c.to_activity(&*conn))
|
||||||
.ok()
|
.ok()
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
|
use plume_models::users::User;
|
||||||
|
use plume_models::{db_conn::DbConn, Error};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
Request,
|
|
||||||
request::FromRequest,
|
request::FromRequest,
|
||||||
response::{self, Responder},
|
response::{self, Responder},
|
||||||
|
Request,
|
||||||
};
|
};
|
||||||
use rocket_i18n::I18n;
|
use rocket_i18n::I18n;
|
||||||
use plume_models::{Error, db_conn::DbConn};
|
|
||||||
use plume_models::users::User;
|
|
||||||
use template_utils::Ructe;
|
use template_utils::Ructe;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -24,15 +24,24 @@ impl<'r> Responder<'r> for ErrorPage {
|
||||||
let user = User::from_request(req).succeeded();
|
let user = User::from_request(req).succeeded();
|
||||||
|
|
||||||
match self.0 {
|
match self.0 {
|
||||||
Error::NotFound => render!(errors::not_found(
|
Error::NotFound => render!(errors::not_found(&(
|
||||||
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
|
&*conn.unwrap(),
|
||||||
)).respond_to(req),
|
&intl.unwrap().catalog,
|
||||||
Error::Unauthorized => render!(errors::not_found(
|
user
|
||||||
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
|
)))
|
||||||
)).respond_to(req),
|
.respond_to(req),
|
||||||
_ => render!(errors::not_found(
|
Error::Unauthorized => render!(errors::not_found(&(
|
||||||
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
|
&*conn.unwrap(),
|
||||||
)).respond_to(req)
|
&intl.unwrap().catalog,
|
||||||
|
user
|
||||||
|
)))
|
||||||
|
.respond_to(req),
|
||||||
|
_ => render!(errors::not_found(&(
|
||||||
|
&*conn.unwrap(),
|
||||||
|
&intl.unwrap().catalog,
|
||||||
|
user
|
||||||
|
)))
|
||||||
|
.respond_to(req),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,9 +51,11 @@ pub fn not_found(req: &Request) -> Ructe {
|
||||||
let conn = req.guard::<DbConn>().succeeded();
|
let conn = req.guard::<DbConn>().succeeded();
|
||||||
let intl = req.guard::<I18n>().succeeded();
|
let intl = req.guard::<I18n>().succeeded();
|
||||||
let user = User::from_request(req).succeeded();
|
let user = User::from_request(req).succeeded();
|
||||||
render!(errors::not_found(
|
render!(errors::not_found(&(
|
||||||
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
|
&*conn.unwrap(),
|
||||||
))
|
&intl.unwrap().catalog,
|
||||||
|
user
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[catch(422)]
|
#[catch(422)]
|
||||||
|
@ -52,9 +63,11 @@ pub fn unprocessable_entity(req: &Request) -> Ructe {
|
||||||
let conn = req.guard::<DbConn>().succeeded();
|
let conn = req.guard::<DbConn>().succeeded();
|
||||||
let intl = req.guard::<I18n>().succeeded();
|
let intl = req.guard::<I18n>().succeeded();
|
||||||
let user = User::from_request(req).succeeded();
|
let user = User::from_request(req).succeeded();
|
||||||
render!(errors::unprocessable_entity(
|
render!(errors::unprocessable_entity(&(
|
||||||
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
|
&*conn.unwrap(),
|
||||||
))
|
&intl.unwrap().catalog,
|
||||||
|
user
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[catch(500)]
|
#[catch(500)]
|
||||||
|
@ -62,17 +75,22 @@ pub fn server_error(req: &Request) -> Ructe {
|
||||||
let conn = req.guard::<DbConn>().succeeded();
|
let conn = req.guard::<DbConn>().succeeded();
|
||||||
let intl = req.guard::<I18n>().succeeded();
|
let intl = req.guard::<I18n>().succeeded();
|
||||||
let user = User::from_request(req).succeeded();
|
let user = User::from_request(req).succeeded();
|
||||||
render!(errors::server_error(
|
render!(errors::server_error(&(
|
||||||
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
|
&*conn.unwrap(),
|
||||||
))
|
&intl.unwrap().catalog,
|
||||||
|
user
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/csrf-violation?<target>")]
|
#[post("/csrf-violation?<target>")]
|
||||||
pub fn csrf_violation(target: Option<String>, conn: DbConn, intl: I18n, user: Option<User>) -> Ructe {
|
pub fn csrf_violation(
|
||||||
|
target: Option<String>,
|
||||||
|
conn: DbConn,
|
||||||
|
intl: I18n,
|
||||||
|
user: Option<User>,
|
||||||
|
) -> Ructe {
|
||||||
if let Some(uri) = target {
|
if let Some(uri) = target {
|
||||||
eprintln!("Csrf violation while acceding \"{}\"", uri)
|
eprintln!("Csrf violation while acceding \"{}\"", uri)
|
||||||
}
|
}
|
||||||
render!(errors::csrf(
|
render!(errors::csrf(&(&*conn, &intl.catalog, user)))
|
||||||
&(&*conn, &intl.catalog, user)
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,19 @@
|
||||||
use rocket::{request::LenientForm, response::{status, Redirect}};
|
use rocket::{
|
||||||
|
request::LenientForm,
|
||||||
|
response::{status, Redirect},
|
||||||
|
};
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use rocket_i18n::I18n;
|
use rocket_i18n::I18n;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use validator::{Validate, ValidationErrors};
|
use validator::{Validate, ValidationErrors};
|
||||||
|
|
||||||
use plume_common::activity_pub::sign::{Signable,
|
|
||||||
verify_http_headers};
|
|
||||||
use plume_models::{
|
|
||||||
admin::Admin,
|
|
||||||
comments::Comment,
|
|
||||||
db_conn::DbConn,
|
|
||||||
Error,
|
|
||||||
headers::Headers,
|
|
||||||
posts::Post,
|
|
||||||
users::User,
|
|
||||||
safe_string::SafeString,
|
|
||||||
instance::*
|
|
||||||
};
|
|
||||||
use inbox::{Inbox, SignedJson};
|
use inbox::{Inbox, SignedJson};
|
||||||
use routes::{Page, errors::ErrorPage};
|
use plume_common::activity_pub::sign::{verify_http_headers, Signable};
|
||||||
|
use plume_models::{
|
||||||
|
admin::Admin, comments::Comment, db_conn::DbConn, headers::Headers, instance::*, posts::Post,
|
||||||
|
safe_string::SafeString, users::User, Error,
|
||||||
|
};
|
||||||
|
use routes::{errors::ErrorPage, Page};
|
||||||
use template_utils::Ructe;
|
use template_utils::Ructe;
|
||||||
use Searcher;
|
use Searcher;
|
||||||
|
|
||||||
|
@ -46,7 +41,12 @@ pub fn index(conn: DbConn, user: Option<User>, intl: I18n) -> Result<Ructe, Erro
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/local?<page>")]
|
#[get("/local?<page>")]
|
||||||
pub fn local(conn: DbConn, user: Option<User>, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
|
pub fn local(
|
||||||
|
conn: DbConn,
|
||||||
|
user: Option<User>,
|
||||||
|
page: Option<Page>,
|
||||||
|
intl: I18n,
|
||||||
|
) -> Result<Ructe, ErrorPage> {
|
||||||
let page = page.unwrap_or_default();
|
let page = page.unwrap_or_default();
|
||||||
let instance = Instance::get_local(&*conn)?;
|
let instance = Instance::get_local(&*conn)?;
|
||||||
let articles = Post::get_instance_page(&*conn, instance.id, page.limits())?;
|
let articles = Post::get_instance_page(&*conn, instance.id, page.limits())?;
|
||||||
|
@ -75,7 +75,12 @@ pub fn feed(conn: DbConn, user: User, page: Option<Page>, intl: I18n) -> Result<
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/federated?<page>")]
|
#[get("/federated?<page>")]
|
||||||
pub fn federated(conn: DbConn, user: Option<User>, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
|
pub fn federated(
|
||||||
|
conn: DbConn,
|
||||||
|
user: Option<User>,
|
||||||
|
page: Option<Page>,
|
||||||
|
intl: I18n,
|
||||||
|
) -> Result<Ructe, ErrorPage> {
|
||||||
let page = page.unwrap_or_default();
|
let page = page.unwrap_or_default();
|
||||||
let articles = Post::get_recents_page(&*conn, page.limits())?;
|
let articles = Post::get_recents_page(&*conn, page.limits())?;
|
||||||
Ok(render!(instance::federated(
|
Ok(render!(instance::federated(
|
||||||
|
@ -111,23 +116,34 @@ pub struct InstanceSettingsForm {
|
||||||
pub short_description: SafeString,
|
pub short_description: SafeString,
|
||||||
pub long_description: SafeString,
|
pub long_description: SafeString,
|
||||||
#[validate(length(min = "1"))]
|
#[validate(length(min = "1"))]
|
||||||
pub default_license: String
|
pub default_license: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/admin", data = "<form>")]
|
#[post("/admin", data = "<form>")]
|
||||||
pub fn update_settings(conn: DbConn, admin: Admin, form: LenientForm<InstanceSettingsForm>, intl: I18n) -> Result<Redirect, Ructe> {
|
pub fn update_settings(
|
||||||
|
conn: DbConn,
|
||||||
|
admin: Admin,
|
||||||
|
form: LenientForm<InstanceSettingsForm>,
|
||||||
|
intl: I18n,
|
||||||
|
) -> Result<Redirect, Ructe> {
|
||||||
form.validate()
|
form.validate()
|
||||||
.and_then(|_| {
|
.and_then(|_| {
|
||||||
let instance = Instance::get_local(&*conn).expect("instance::update_settings: local instance error");
|
let instance = Instance::get_local(&*conn)
|
||||||
instance.update(&*conn,
|
.expect("instance::update_settings: local instance error");
|
||||||
form.name.clone(),
|
instance
|
||||||
form.open_registrations,
|
.update(
|
||||||
form.short_description.clone(),
|
&*conn,
|
||||||
form.long_description.clone()).expect("instance::update_settings: save error");
|
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)))
|
Ok(Redirect::to(uri!(admin)))
|
||||||
})
|
})
|
||||||
.or_else(|e| {
|
.or_else(|e| {
|
||||||
let local_inst = Instance::get_local(&*conn).expect("instance::update_settings: local instance error");
|
let local_inst = Instance::get_local(&*conn)
|
||||||
|
.expect("instance::update_settings: local instance error");
|
||||||
Err(render!(instance::admin(
|
Err(render!(instance::admin(
|
||||||
&(&*conn, &intl.catalog, Some(admin.0)),
|
&(&*conn, &intl.catalog, Some(admin.0)),
|
||||||
local_inst,
|
local_inst,
|
||||||
|
@ -138,7 +154,12 @@ pub fn update_settings(conn: DbConn, admin: Admin, form: LenientForm<InstanceSet
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/admin/instances?<page>")]
|
#[get("/admin/instances?<page>")]
|
||||||
pub fn admin_instances(admin: Admin, conn: DbConn, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
|
pub fn admin_instances(
|
||||||
|
admin: Admin,
|
||||||
|
conn: DbConn,
|
||||||
|
page: Option<Page>,
|
||||||
|
intl: I18n,
|
||||||
|
) -> Result<Ructe, ErrorPage> {
|
||||||
let page = page.unwrap_or_default();
|
let page = page.unwrap_or_default();
|
||||||
let instances = Instance::page(&*conn, page.limits())?;
|
let instances = Instance::page(&*conn, page.limits())?;
|
||||||
Ok(render!(instance::list(
|
Ok(render!(instance::list(
|
||||||
|
@ -160,7 +181,12 @@ pub fn toggle_block(_admin: Admin, conn: DbConn, id: i32) -> Result<Redirect, Er
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/admin/users?<page>")]
|
#[get("/admin/users?<page>")]
|
||||||
pub fn admin_users(admin: Admin, conn: DbConn, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
|
pub fn admin_users(
|
||||||
|
admin: Admin,
|
||||||
|
conn: DbConn,
|
||||||
|
page: Option<Page>,
|
||||||
|
intl: I18n,
|
||||||
|
) -> Result<Ructe, ErrorPage> {
|
||||||
let page = page.unwrap_or_default();
|
let page = page.unwrap_or_default();
|
||||||
Ok(render!(instance::users(
|
Ok(render!(instance::users(
|
||||||
&(&*conn, &intl.catalog, Some(admin.0)),
|
&(&*conn, &intl.catalog, Some(admin.0)),
|
||||||
|
@ -171,7 +197,12 @@ pub fn admin_users(admin: Admin, conn: DbConn, page: Option<Page>, intl: I18n) -
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/admin/users/<id>/ban")]
|
#[post("/admin/users/<id>/ban")]
|
||||||
pub fn ban(_admin: Admin, conn: DbConn, id: i32, searcher: Searcher) -> Result<Redirect, ErrorPage> {
|
pub fn ban(
|
||||||
|
_admin: Admin,
|
||||||
|
conn: DbConn,
|
||||||
|
id: i32,
|
||||||
|
searcher: Searcher,
|
||||||
|
) -> Result<Redirect, ErrorPage> {
|
||||||
if let Ok(u) = User::get(&*conn, id) {
|
if let Ok(u) = User::get(&*conn, id) {
|
||||||
u.delete(&*conn, &searcher)?;
|
u.delete(&*conn, &searcher)?;
|
||||||
}
|
}
|
||||||
|
@ -179,34 +210,50 @@ pub fn ban(_admin: Admin, conn: DbConn, id: i32, searcher: Searcher) -> Result<R
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/inbox", data = "<data>")]
|
#[post("/inbox", data = "<data>")]
|
||||||
pub fn shared_inbox(conn: DbConn, data: SignedJson<serde_json::Value>, headers: Headers, searcher: Searcher) -> Result<String, status::BadRequest<&'static str>> {
|
pub fn shared_inbox(
|
||||||
|
conn: DbConn,
|
||||||
|
data: SignedJson<serde_json::Value>,
|
||||||
|
headers: Headers,
|
||||||
|
searcher: Searcher,
|
||||||
|
) -> Result<String, status::BadRequest<&'static str>> {
|
||||||
let act = data.1.into_inner();
|
let act = data.1.into_inner();
|
||||||
let sig = data.0;
|
let sig = data.0;
|
||||||
|
|
||||||
let activity = act.clone();
|
let activity = act.clone();
|
||||||
let actor_id = activity["actor"].as_str()
|
let actor_id = activity["actor"]
|
||||||
.or_else(|| activity["actor"]["id"].as_str()).ok_or(status::BadRequest(Some("Missing actor id for activity")))?;
|
.as_str()
|
||||||
|
.or_else(|| activity["actor"]["id"].as_str())
|
||||||
|
.ok_or(status::BadRequest(Some("Missing actor id for activity")))?;
|
||||||
|
|
||||||
let actor = User::from_url(&conn, actor_id).expect("instance::shared_inbox: user error");
|
let actor = User::from_url(&conn, actor_id).expect("instance::shared_inbox: user error");
|
||||||
if !verify_http_headers(&actor, &headers.0, &sig).is_secure() &&
|
if !verify_http_headers(&actor, &headers.0, &sig).is_secure() && !act.clone().verify(&actor) {
|
||||||
!act.clone().verify(&actor) {
|
|
||||||
// maybe we just know an old key?
|
// maybe we just know an old key?
|
||||||
actor.refetch(&conn).and_then(|_| User::get(&conn, actor.id))
|
actor
|
||||||
.and_then(|u| if verify_http_headers(&u, &headers.0, &sig).is_secure() ||
|
.refetch(&conn)
|
||||||
act.clone().verify(&u) {
|
.and_then(|_| User::get(&conn, actor.id))
|
||||||
Ok(())
|
.and_then(|u| {
|
||||||
} else {
|
if verify_http_headers(&u, &headers.0, &sig).is_secure() || act.clone().verify(&u) {
|
||||||
Err(Error::Signature)
|
Ok(())
|
||||||
})
|
} else {
|
||||||
|
Err(Error::Signature)
|
||||||
|
}
|
||||||
|
})
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
println!("Rejected invalid activity supposedly from {}, with headers {:?}", actor.username, headers.0);
|
println!(
|
||||||
status::BadRequest(Some("Invalid signature"))})?;
|
"Rejected invalid activity supposedly from {}, with headers {:?}",
|
||||||
|
actor.username, headers.0
|
||||||
|
);
|
||||||
|
status::BadRequest(Some("Invalid signature"))
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if Instance::is_blocked(&*conn, actor_id).map_err(|_| status::BadRequest(Some("Can't tell if instance is blocked")))? {
|
if Instance::is_blocked(&*conn, actor_id)
|
||||||
|
.map_err(|_| status::BadRequest(Some("Can't tell if instance is blocked")))?
|
||||||
|
{
|
||||||
return Ok(String::new());
|
return Ok(String::new());
|
||||||
}
|
}
|
||||||
let instance = Instance::get_local(&*conn).expect("instance::shared_inbox: local instance not found error");
|
let instance = Instance::get_local(&*conn)
|
||||||
|
.expect("instance::shared_inbox: local instance not found error");
|
||||||
Ok(match instance.received(&*conn, &searcher, act) {
|
Ok(match instance.received(&*conn, &searcher, act) {
|
||||||
Ok(_) => String::new(),
|
Ok(_) => String::new(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
|
@ -1,25 +1,28 @@
|
||||||
use rocket::response::{Redirect, Flash};
|
use rocket::response::{Flash, Redirect};
|
||||||
use rocket_i18n::I18n;
|
use rocket_i18n::I18n;
|
||||||
|
|
||||||
use plume_common::activity_pub::{broadcast, inbox::{Notify, Deletable}};
|
use plume_common::activity_pub::{
|
||||||
use plume_common::utils;
|
broadcast,
|
||||||
use plume_models::{
|
inbox::{Deletable, Notify},
|
||||||
blogs::Blog,
|
|
||||||
db_conn::DbConn,
|
|
||||||
likes,
|
|
||||||
posts::Post,
|
|
||||||
users::User
|
|
||||||
};
|
};
|
||||||
use Worker;
|
use plume_common::utils;
|
||||||
|
use plume_models::{blogs::Blog, db_conn::DbConn, likes, posts::Post, users::User};
|
||||||
use routes::errors::ErrorPage;
|
use routes::errors::ErrorPage;
|
||||||
|
use Worker;
|
||||||
|
|
||||||
#[post("/~/<blog>/<slug>/like")]
|
#[post("/~/<blog>/<slug>/like")]
|
||||||
pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Result<Redirect, ErrorPage> {
|
pub fn create(
|
||||||
|
blog: String,
|
||||||
|
slug: String,
|
||||||
|
user: User,
|
||||||
|
conn: DbConn,
|
||||||
|
worker: Worker,
|
||||||
|
) -> Result<Redirect, ErrorPage> {
|
||||||
let b = Blog::find_by_fqn(&*conn, &blog)?;
|
let b = Blog::find_by_fqn(&*conn, &blog)?;
|
||||||
let post = Post::find_by_slug(&*conn, &slug, b.id)?;
|
let post = Post::find_by_slug(&*conn, &slug, b.id)?;
|
||||||
|
|
||||||
if !user.has_liked(&*conn, &post)? {
|
if !user.has_liked(&*conn, &post)? {
|
||||||
let like = likes::Like::insert(&*conn, likes::NewLike::new(&post ,&user))?;
|
let like = likes::Like::insert(&*conn, likes::NewLike::new(&post, &user))?;
|
||||||
like.notify(&*conn)?;
|
like.notify(&*conn)?;
|
||||||
|
|
||||||
let dest = User::one_by_instance(&*conn)?;
|
let dest = User::one_by_instance(&*conn)?;
|
||||||
|
@ -32,13 +35,18 @@ pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Work
|
||||||
worker.execute(move || broadcast(&user, delete_act, dest));
|
worker.execute(move || broadcast(&user, delete_act, dest));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _)))
|
Ok(Redirect::to(
|
||||||
|
uri!(super::posts::details: blog = blog, slug = slug, responding_to = _),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/~/<blog>/<slug>/like", rank = 2)]
|
#[post("/~/<blog>/<slug>/like", rank = 2)]
|
||||||
pub fn create_auth(blog: String, slug: String, i18n: I18n) -> Flash<Redirect>{
|
pub fn create_auth(blog: String, slug: String, i18n: I18n) -> Flash<Redirect> {
|
||||||
utils::requires_login(
|
utils::requires_login(
|
||||||
&i18n!(i18n.catalog, "You need to be logged in order to like a post"),
|
&i18n!(
|
||||||
uri!(create: blog = blog, slug = slug)
|
i18n.catalog,
|
||||||
|
"You need to be logged in order to like a post"
|
||||||
|
),
|
||||||
|
uri!(create: blog = blog, slug = slug),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
use guid_create::GUID;
|
use guid_create::GUID;
|
||||||
use multipart::server::{Multipart, save::{SavedData, SaveResult}};
|
use multipart::server::{
|
||||||
use rocket::{Data, http::ContentType, response::{Redirect, status}};
|
save::{SaveResult, SavedData},
|
||||||
|
Multipart,
|
||||||
|
};
|
||||||
|
use plume_models::{db_conn::DbConn, medias::*, users::User, Error};
|
||||||
|
use rocket::{
|
||||||
|
http::ContentType,
|
||||||
|
response::{status, Redirect},
|
||||||
|
Data,
|
||||||
|
};
|
||||||
use rocket_i18n::I18n;
|
use rocket_i18n::I18n;
|
||||||
|
use routes::{errors::ErrorPage, Page};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use plume_models::{Error, db_conn::DbConn, medias::*, users::User};
|
|
||||||
use template_utils::Ructe;
|
use template_utils::Ructe;
|
||||||
use routes::{Page, errors::ErrorPage};
|
|
||||||
|
|
||||||
#[get("/medias?<page>")]
|
#[get("/medias?<page>")]
|
||||||
pub fn list(user: User, conn: DbConn, intl: I18n, page: Option<Page>) -> Result<Ructe, ErrorPage> {
|
pub fn list(user: User, conn: DbConn, intl: I18n, page: Option<Page>) -> Result<Ructe, ErrorPage> {
|
||||||
|
@ -21,64 +28,85 @@ pub fn list(user: User, conn: DbConn, intl: I18n, page: Option<Page>) -> Result<
|
||||||
|
|
||||||
#[get("/medias/new")]
|
#[get("/medias/new")]
|
||||||
pub fn new(user: User, conn: DbConn, intl: I18n) -> Ructe {
|
pub fn new(user: User, conn: DbConn, intl: I18n) -> Ructe {
|
||||||
render!(medias::new(
|
render!(medias::new(&(&*conn, &intl.catalog, Some(user))))
|
||||||
&(&*conn, &intl.catalog, Some(user))
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/medias/new", data = "<data>")]
|
#[post("/medias/new", data = "<data>")]
|
||||||
pub fn upload(user: User, data: Data, ct: &ContentType, conn: DbConn) -> Result<Redirect, status::BadRequest<&'static str>> {
|
pub fn upload(
|
||||||
|
user: User,
|
||||||
|
data: Data,
|
||||||
|
ct: &ContentType,
|
||||||
|
conn: DbConn,
|
||||||
|
) -> Result<Redirect, status::BadRequest<&'static str>> {
|
||||||
if ct.is_form_data() {
|
if ct.is_form_data() {
|
||||||
let (_, boundary) = ct.params().find(|&(k, _)| k == "boundary").ok_or_else(|| status::BadRequest(Some("No boundary")))?;
|
let (_, boundary) = ct
|
||||||
|
.params()
|
||||||
|
.find(|&(k, _)| k == "boundary")
|
||||||
|
.ok_or_else(|| status::BadRequest(Some("No boundary")))?;
|
||||||
|
|
||||||
match Multipart::with_body(data.open(), boundary).save().temp() {
|
match Multipart::with_body(data.open(), boundary).save().temp() {
|
||||||
SaveResult::Full(entries) => {
|
SaveResult::Full(entries) => {
|
||||||
let fields = entries.fields;
|
let fields = entries.fields;
|
||||||
|
|
||||||
let filename = fields.get("file").and_then(|v| v.iter().next())
|
let filename = fields
|
||||||
.ok_or_else(|| status::BadRequest(Some("No file uploaded")))?.headers
|
.get("file")
|
||||||
.filename.clone();
|
.and_then(|v| v.iter().next())
|
||||||
|
.ok_or_else(|| status::BadRequest(Some("No file uploaded")))?
|
||||||
|
.headers
|
||||||
|
.filename
|
||||||
|
.clone();
|
||||||
// Remove extension if it contains something else than just letters and numbers
|
// Remove extension if it contains something else than just letters and numbers
|
||||||
let ext = filename
|
let ext = filename
|
||||||
.and_then(|f| f
|
.and_then(|f| {
|
||||||
.rsplit('.')
|
f.rsplit('.')
|
||||||
.next()
|
.next()
|
||||||
.and_then(|ext| if ext.chars().any(|c| !c.is_alphanumeric()) {
|
.and_then(|ext| {
|
||||||
None
|
if ext.chars().any(|c| !c.is_alphanumeric()) {
|
||||||
} else {
|
None
|
||||||
Some(ext.to_lowercase())
|
} else {
|
||||||
})
|
Some(ext.to_lowercase())
|
||||||
.map(|ext| format!(".{}", ext))
|
}
|
||||||
).unwrap_or_default();
|
})
|
||||||
|
.map(|ext| format!(".{}", ext))
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
let dest = format!("static/media/{}{}", GUID::rand().to_string(), ext);
|
let dest = format!("static/media/{}{}", GUID::rand().to_string(), ext);
|
||||||
|
|
||||||
match fields["file"][0].data {
|
match fields["file"][0].data {
|
||||||
SavedData::Bytes(ref bytes) => fs::write(&dest, bytes).map_err(|_| status::BadRequest(Some("Couldn't save upload")))?,
|
SavedData::Bytes(ref bytes) => fs::write(&dest, bytes)
|
||||||
SavedData::File(ref path, _) => {fs::copy(path, &dest).map_err(|_| status::BadRequest(Some("Couldn't copy upload")))?;},
|
.map_err(|_| status::BadRequest(Some("Couldn't save upload")))?,
|
||||||
|
SavedData::File(ref path, _) => {
|
||||||
|
fs::copy(path, &dest)
|
||||||
|
.map_err(|_| status::BadRequest(Some("Couldn't copy upload")))?;
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Ok(Redirect::to(uri!(new)));
|
return Ok(Redirect::to(uri!(new)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let has_cw = !read(&fields["cw"][0].data).map(|cw| cw.is_empty()).unwrap_or(false);
|
let has_cw = !read(&fields["cw"][0].data)
|
||||||
let media = Media::insert(&*conn, NewMedia {
|
.map(|cw| cw.is_empty())
|
||||||
file_path: dest,
|
.unwrap_or(false);
|
||||||
alt_text: read(&fields["alt"][0].data)?,
|
let media = Media::insert(
|
||||||
is_remote: false,
|
&*conn,
|
||||||
remote_url: None,
|
NewMedia {
|
||||||
sensitive: has_cw,
|
file_path: dest,
|
||||||
content_warning: if has_cw {
|
alt_text: read(&fields["alt"][0].data)?,
|
||||||
Some(read(&fields["cw"][0].data)?)
|
is_remote: false,
|
||||||
} else {
|
remote_url: None,
|
||||||
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)))
|
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 {
|
} else {
|
||||||
Ok(Redirect::to(uri!(new)))
|
Ok(Redirect::to(uri!(new)))
|
||||||
|
|
|
@ -2,25 +2,21 @@
|
||||||
use atom_syndication::{ContentBuilder, Entry, EntryBuilder, LinkBuilder, Person, PersonBuilder};
|
use atom_syndication::{ContentBuilder, Entry, EntryBuilder, LinkBuilder, Person, PersonBuilder};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::{
|
http::{
|
||||||
RawStr, Status, uri::{FromUriParam, Query},
|
hyper::header::{CacheControl, CacheDirective},
|
||||||
hyper::header::{CacheControl, CacheDirective}
|
uri::{FromUriParam, Query},
|
||||||
|
RawStr, Status,
|
||||||
},
|
},
|
||||||
Outcome,
|
|
||||||
request::{self, FromFormValue, FromRequest, Request},
|
request::{self, FromFormValue, FromRequest, Request},
|
||||||
response::NamedFile,
|
response::NamedFile,
|
||||||
|
Outcome,
|
||||||
};
|
};
|
||||||
use rocket_i18n::I18n;
|
use rocket_i18n::I18n;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use plume_models::{
|
use plume_models::{db_conn::DbConn, posts::Post, users::User, Connection};
|
||||||
Connection,
|
|
||||||
users::User,
|
|
||||||
posts::Post,
|
|
||||||
db_conn::DbConn,
|
|
||||||
};
|
|
||||||
|
|
||||||
use Worker;
|
|
||||||
use Searcher;
|
use Searcher;
|
||||||
|
use Worker;
|
||||||
|
|
||||||
pub struct PlumeRocket<'a> {
|
pub struct PlumeRocket<'a> {
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
|
@ -100,7 +96,6 @@ impl<'a, 'r> FromRequest<'a, 'r> for ContentLen {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl Default for Page {
|
impl Default for Page {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Page(1)
|
Page(1)
|
||||||
|
@ -110,20 +105,33 @@ impl Default for Page {
|
||||||
pub fn post_to_atom(post: Post, conn: &Connection) -> Entry {
|
pub fn post_to_atom(post: Post, conn: &Connection) -> Entry {
|
||||||
EntryBuilder::default()
|
EntryBuilder::default()
|
||||||
.title(format!("<![CDATA[{}]]>", post.title))
|
.title(format!("<![CDATA[{}]]>", post.title))
|
||||||
.content(ContentBuilder::default()
|
.content(
|
||||||
.value(format!("<![CDATA[{}]]>", *post.content.get()))
|
ContentBuilder::default()
|
||||||
.src(post.ap_url.clone())
|
.value(format!("<![CDATA[{}]]>", *post.content.get()))
|
||||||
.content_type("html".to_string())
|
.src(post.ap_url.clone())
|
||||||
.build().expect("Atom feed: content error"))
|
.content_type("html".to_string())
|
||||||
.authors(post.get_authors(&*conn).expect("Atom feed: author error")
|
.build()
|
||||||
.into_iter()
|
.expect("Atom feed: content error"),
|
||||||
.map(|a| PersonBuilder::default()
|
)
|
||||||
.name(a.display_name)
|
.authors(
|
||||||
.uri(a.ap_url)
|
post.get_authors(&*conn)
|
||||||
.build().expect("Atom feed: author error"))
|
.expect("Atom feed: author error")
|
||||||
.collect::<Vec<Person>>())
|
.into_iter()
|
||||||
.links(vec![LinkBuilder::default().href(post.ap_url).build().expect("Atom feed: link error")])
|
.map(|a| {
|
||||||
.build().expect("Atom feed: entry error")
|
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;
|
pub mod blogs;
|
||||||
|
@ -135,17 +143,17 @@ pub mod medias;
|
||||||
pub mod notifications;
|
pub mod notifications;
|
||||||
pub mod posts;
|
pub mod posts;
|
||||||
pub mod reshares;
|
pub mod reshares;
|
||||||
|
pub mod search;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod tags;
|
pub mod tags;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod search;
|
|
||||||
pub mod well_known;
|
pub mod well_known;
|
||||||
|
|
||||||
#[derive(Responder)]
|
#[derive(Responder)]
|
||||||
#[response()]
|
#[response()]
|
||||||
pub struct CachedFile {
|
pub struct CachedFile {
|
||||||
inner: NamedFile,
|
inner: NamedFile,
|
||||||
cache_control: CacheControl
|
cache_control: CacheControl,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/static/cached/<_build_id>/<file..>", rank = 2)]
|
#[get("/static/cached/<_build_id>/<file..>", rank = 2)]
|
||||||
|
@ -155,10 +163,10 @@ pub fn plume_static_files(file: PathBuf, _build_id: &RawStr) -> Option<CachedFil
|
||||||
|
|
||||||
#[get("/static/<file..>", rank = 3)]
|
#[get("/static/<file..>", rank = 3)]
|
||||||
pub fn static_files(file: PathBuf) -> Option<CachedFile> {
|
pub fn static_files(file: PathBuf) -> Option<CachedFile> {
|
||||||
NamedFile::open(Path::new("static/").join(file)).ok()
|
NamedFile::open(Path::new("static/").join(file))
|
||||||
.map(|f|
|
.ok()
|
||||||
CachedFile {
|
.map(|f| CachedFile {
|
||||||
inner: f,
|
inner: f,
|
||||||
cache_control: CacheControl(vec![CacheDirective::MaxAge(60*60*24*30)])
|
cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
use rocket::response::{Redirect, Flash};
|
use rocket::response::{Flash, Redirect};
|
||||||
use rocket_i18n::I18n;
|
use rocket_i18n::I18n;
|
||||||
|
|
||||||
use plume_common::utils;
|
use plume_common::utils;
|
||||||
use plume_models::{db_conn::DbConn, notifications::Notification, users::User};
|
use plume_models::{db_conn::DbConn, notifications::Notification, users::User};
|
||||||
use routes::{Page, errors::ErrorPage};
|
use routes::{errors::ErrorPage, Page};
|
||||||
use template_utils::Ructe;
|
use template_utils::Ructe;
|
||||||
|
|
||||||
#[get("/notifications?<page>")]
|
#[get("/notifications?<page>")]
|
||||||
pub fn notifications(conn: DbConn, user: User, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
|
pub fn notifications(
|
||||||
|
conn: DbConn,
|
||||||
|
user: User,
|
||||||
|
page: Option<Page>,
|
||||||
|
intl: I18n,
|
||||||
|
) -> Result<Ructe, ErrorPage> {
|
||||||
let page = page.unwrap_or_default();
|
let page = page.unwrap_or_default();
|
||||||
Ok(render!(notifications::index(
|
Ok(render!(notifications::index(
|
||||||
&(&*conn, &intl.catalog, Some(user.clone())),
|
&(&*conn, &intl.catalog, Some(user.clone())),
|
||||||
|
@ -18,9 +23,12 @@ pub fn notifications(conn: DbConn, user: User, page: Option<Page>, intl: I18n) -
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/notifications?<page>", rank = 2)]
|
#[get("/notifications?<page>", rank = 2)]
|
||||||
pub fn notifications_auth(i18n: I18n, page: Option<Page>) -> Flash<Redirect>{
|
pub fn notifications_auth(i18n: I18n, page: Option<Page>) -> Flash<Redirect> {
|
||||||
utils::requires_login(
|
utils::requires_login(
|
||||||
&i18n!(i18n.catalog, "You need to be logged in order to see your notifications"),
|
&i18n!(
|
||||||
uri!(notifications: page = page)
|
i18n.catalog,
|
||||||
|
"You need to be logged in order to see your notifications"
|
||||||
|
),
|
||||||
|
uri!(notifications: page = page),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use heck::{CamelCase, KebabCase};
|
use heck::{CamelCase, KebabCase};
|
||||||
use rocket::request::LenientForm;
|
use rocket::request::LenientForm;
|
||||||
use rocket::response::{Redirect, Flash};
|
use rocket::response::{Flash, Redirect};
|
||||||
use rocket_i18n::I18n;
|
use rocket_i18n::I18n;
|
||||||
use std::{
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
borrow::Cow, time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use validator::{Validate, ValidationError, ValidationErrors};
|
use validator::{Validate, ValidationError, ValidationErrors};
|
||||||
|
|
||||||
use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest, inbox::Deletable};
|
use plume_common::activity_pub::{broadcast, inbox::Deletable, ActivityStream, ApRequest};
|
||||||
use plume_common::utils;
|
use plume_common::utils;
|
||||||
use plume_models::{
|
use plume_models::{
|
||||||
blogs::*,
|
blogs::*,
|
||||||
db_conn::DbConn,
|
|
||||||
comments::{Comment, CommentTree},
|
comments::{Comment, CommentTree},
|
||||||
|
db_conn::DbConn,
|
||||||
instance::Instance,
|
instance::Instance,
|
||||||
medias::Media,
|
medias::Media,
|
||||||
mentions::Mention,
|
mentions::Mention,
|
||||||
|
@ -22,16 +23,28 @@ use plume_models::{
|
||||||
posts::*,
|
posts::*,
|
||||||
safe_string::SafeString,
|
safe_string::SafeString,
|
||||||
tags::*,
|
tags::*,
|
||||||
users::User
|
users::User,
|
||||||
};
|
};
|
||||||
use routes::{PlumeRocket, errors::ErrorPage, comments::NewCommentForm, ContentLen};
|
use routes::{comments::NewCommentForm, errors::ErrorPage, ContentLen, PlumeRocket};
|
||||||
use template_utils::Ructe;
|
use template_utils::Ructe;
|
||||||
|
|
||||||
#[get("/~/<blog>/<slug>?<responding_to>", rank = 4)]
|
#[get("/~/<blog>/<slug>?<responding_to>", rank = 4)]
|
||||||
pub fn details(blog: String, slug: String, conn: DbConn, user: Option<User>, responding_to: Option<i32>, intl: I18n) -> Result<Ructe, ErrorPage> {
|
pub fn details(
|
||||||
|
blog: String,
|
||||||
|
slug: String,
|
||||||
|
conn: DbConn,
|
||||||
|
user: Option<User>,
|
||||||
|
responding_to: Option<i32>,
|
||||||
|
intl: I18n,
|
||||||
|
) -> Result<Ructe, ErrorPage> {
|
||||||
let blog = Blog::find_by_fqn(&*conn, &blog)?;
|
let blog = Blog::find_by_fqn(&*conn, &blog)?;
|
||||||
let post = Post::find_by_slug(&*conn, &slug, blog.id)?;
|
let post = Post::find_by_slug(&*conn, &slug, blog.id)?;
|
||||||
if post.published || post.get_authors(&*conn)?.into_iter().any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0)) {
|
if post.published
|
||||||
|
|| post
|
||||||
|
.get_authors(&*conn)?
|
||||||
|
.into_iter()
|
||||||
|
.any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0))
|
||||||
|
{
|
||||||
let comments = CommentTree::from_post(&*conn, &post, user.as_ref())?;
|
let comments = CommentTree::from_post(&*conn, &post, user.as_ref())?;
|
||||||
|
|
||||||
let previous = responding_to.and_then(|r| Comment::get(&*conn, r).ok());
|
let previous = responding_to.and_then(|r| Comment::get(&*conn, r).ok());
|
||||||
|
@ -82,11 +95,19 @@ pub fn details(blog: String, slug: String, conn: DbConn, user: Option<User>, res
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/~/<blog>/<slug>", rank = 3)]
|
#[get("/~/<blog>/<slug>", rank = 3)]
|
||||||
pub fn activity_details(blog: String, slug: String, conn: DbConn, _ap: ApRequest) -> Result<ActivityStream<LicensedArticle>, Option<String>> {
|
pub fn activity_details(
|
||||||
|
blog: String,
|
||||||
|
slug: String,
|
||||||
|
conn: DbConn,
|
||||||
|
_ap: ApRequest,
|
||||||
|
) -> Result<ActivityStream<LicensedArticle>, Option<String>> {
|
||||||
let blog = Blog::find_by_fqn(&*conn, &blog).map_err(|_| None)?;
|
let blog = Blog::find_by_fqn(&*conn, &blog).map_err(|_| None)?;
|
||||||
let post = Post::find_by_slug(&*conn, &slug, blog.id).map_err(|_| None)?;
|
let post = Post::find_by_slug(&*conn, &slug, blog.id).map_err(|_| None)?;
|
||||||
if post.published {
|
if post.published {
|
||||||
Ok(ActivityStream::new(post.to_activity(&*conn).map_err(|_| String::from("Post serialization error"))?))
|
Ok(ActivityStream::new(
|
||||||
|
post.to_activity(&*conn)
|
||||||
|
.map_err(|_| String::from("Post serialization error"))?,
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
Err(Some(String::from("Not published yet.")))
|
Err(Some(String::from("Not published yet.")))
|
||||||
}
|
}
|
||||||
|
@ -95,8 +116,11 @@ pub fn activity_details(blog: String, slug: String, conn: DbConn, _ap: ApRequest
|
||||||
#[get("/~/<blog>/new", rank = 2)]
|
#[get("/~/<blog>/new", rank = 2)]
|
||||||
pub fn new_auth(blog: String, i18n: I18n) -> Flash<Redirect> {
|
pub fn new_auth(blog: String, i18n: I18n) -> Flash<Redirect> {
|
||||||
utils::requires_login(
|
utils::requires_login(
|
||||||
&i18n!(i18n.catalog, "You need to be logged in order to write a new post"),
|
&i18n!(
|
||||||
uri!(new: blog = blog)
|
i18n.catalog,
|
||||||
|
"You need to be logged in order to write a new post"
|
||||||
|
),
|
||||||
|
uri!(new: blog = blog),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,7 +136,7 @@ pub fn new(blog: String, cl: ContentLen, rockets: PlumeRocket) -> Result<Ructe,
|
||||||
return Ok(render!(errors::not_authorized(
|
return Ok(render!(errors::not_authorized(
|
||||||
&(&*conn, &intl.catalog, Some(user)),
|
&(&*conn, &intl.catalog, Some(user)),
|
||||||
i18n!(intl.catalog, "You are not author in this blog.")
|
i18n!(intl.catalog, "You are not author in this blog.")
|
||||||
)))
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let medias = Media::for_user(&*conn, user.id)?;
|
let medias = Media::for_user(&*conn, user.id)?;
|
||||||
|
@ -134,7 +158,12 @@ pub fn new(blog: String, cl: ContentLen, rockets: PlumeRocket) -> Result<Ructe,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/~/<blog>/<slug>/edit")]
|
#[get("/~/<blog>/<slug>/edit")]
|
||||||
pub fn edit(blog: String, slug: String, cl: ContentLen, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
pub fn edit(
|
||||||
|
blog: String,
|
||||||
|
slug: String,
|
||||||
|
cl: ContentLen,
|
||||||
|
rockets: PlumeRocket,
|
||||||
|
) -> Result<Ructe, ErrorPage> {
|
||||||
let conn = rockets.conn;
|
let conn = rockets.conn;
|
||||||
let intl = rockets.intl;
|
let intl = rockets.intl;
|
||||||
let b = Blog::find_by_fqn(&*conn, &blog)?;
|
let b = Blog::find_by_fqn(&*conn, &blog)?;
|
||||||
|
@ -145,10 +174,9 @@ pub fn edit(blog: String, slug: String, cl: ContentLen, rockets: PlumeRocket) ->
|
||||||
return Ok(render!(errors::not_authorized(
|
return Ok(render!(errors::not_authorized(
|
||||||
&(&*conn, &intl.catalog, Some(user)),
|
&(&*conn, &intl.catalog, Some(user)),
|
||||||
i18n!(intl.catalog, "You are not author in this blog.")
|
i18n!(intl.catalog, "You are not author in this blog.")
|
||||||
)))
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let source = if !post.source.is_empty() {
|
let source = if !post.source.is_empty() {
|
||||||
post.source.clone()
|
post.source.clone()
|
||||||
} else {
|
} else {
|
||||||
|
@ -168,7 +196,7 @@ pub fn edit(blog: String, slug: String, cl: ContentLen, rockets: PlumeRocket) ->
|
||||||
content: source,
|
content: source,
|
||||||
tags: Tag::for_post(&*conn, post.id)?
|
tags: Tag::for_post(&*conn, post.id)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|t| if !t.is_hashtag {Some(t.tag)} else {None})
|
.filter_map(|t| if !t.is_hashtag { Some(t.tag) } else { None })
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join(", "),
|
.join(", "),
|
||||||
license: post.license.clone(),
|
license: post.license.clone(),
|
||||||
|
@ -184,11 +212,17 @@ pub fn edit(blog: String, slug: String, cl: ContentLen, rockets: PlumeRocket) ->
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/~/<blog>/<slug>/edit", data = "<form>")]
|
#[post("/~/<blog>/<slug>/edit", data = "<form>")]
|
||||||
pub fn update(blog: String, slug: String, cl: ContentLen, form: LenientForm<NewPostForm>, rockets: PlumeRocket)
|
pub fn update(
|
||||||
-> Result<Redirect, Ructe> {
|
blog: String,
|
||||||
|
slug: String,
|
||||||
|
cl: ContentLen,
|
||||||
|
form: LenientForm<NewPostForm>,
|
||||||
|
rockets: PlumeRocket,
|
||||||
|
) -> Result<Redirect, Ructe> {
|
||||||
let conn = rockets.conn;
|
let conn = rockets.conn;
|
||||||
let b = Blog::find_by_fqn(&*conn, &blog).expect("post::update: blog error");
|
let b = Blog::find_by_fqn(&*conn, &blog).expect("post::update: blog error");
|
||||||
let mut post = Post::find_by_slug(&*conn, &slug, b.id).expect("post::update: find by slug error");
|
let mut post =
|
||||||
|
Post::find_by_slug(&*conn, &slug, b.id).expect("post::update: find by slug error");
|
||||||
let user = rockets.user.unwrap();
|
let user = rockets.user.unwrap();
|
||||||
let intl = rockets.intl;
|
let intl = rockets.intl;
|
||||||
|
|
||||||
|
@ -200,23 +234,36 @@ pub fn update(blog: String, slug: String, cl: ContentLen, form: LenientForm<NewP
|
||||||
|
|
||||||
let mut errors = match form.validate() {
|
let mut errors = match form.validate() {
|
||||||
Ok(_) => ValidationErrors::new(),
|
Ok(_) => ValidationErrors::new(),
|
||||||
Err(e) => e
|
Err(e) => e,
|
||||||
};
|
};
|
||||||
|
|
||||||
if new_slug != slug && Post::find_by_slug(&*conn, &new_slug, b.id).is_ok() {
|
if new_slug != slug && Post::find_by_slug(&*conn, &new_slug, b.id).is_ok() {
|
||||||
errors.add("title", ValidationError {
|
errors.add(
|
||||||
code: Cow::from("existing_slug"),
|
"title",
|
||||||
message: Some(Cow::from("A post with the same title already exists.")),
|
ValidationError {
|
||||||
params: HashMap::new()
|
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 errors.is_empty() {
|
||||||
if !user.is_author_in(&*conn, &b).expect("posts::update: is author in error") {
|
if !user
|
||||||
|
.is_author_in(&*conn, &b)
|
||||||
|
.expect("posts::update: is author in error")
|
||||||
|
{
|
||||||
// actually it's not "Ok"…
|
// actually it's not "Ok"…
|
||||||
Ok(Redirect::to(uri!(super::blogs::details: name = blog, page = _)))
|
Ok(Redirect::to(
|
||||||
|
uri!(super::blogs::details: name = blog, page = _),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
let (content, mentions, hashtags) = utils::md_to_html(form.content.to_string().as_ref(), &Instance::get_local(&conn).expect("posts::update: Error getting local instance").public_domain);
|
let (content, mentions, hashtags) = utils::md_to_html(
|
||||||
|
form.content.to_string().as_ref(),
|
||||||
|
&Instance::get_local(&conn)
|
||||||
|
.expect("posts::update: Error getting local instance")
|
||||||
|
.public_domain,
|
||||||
|
);
|
||||||
|
|
||||||
// update publication date if when this article is no longer a draft
|
// update publication date if when this article is no longer a draft
|
||||||
let newly_published = if !post.published && !form.draft {
|
let newly_published = if !post.published && !form.draft {
|
||||||
|
@ -236,34 +283,61 @@ pub fn update(blog: String, slug: String, cl: ContentLen, form: LenientForm<NewP
|
||||||
post.source = form.content.clone();
|
post.source = form.content.clone();
|
||||||
post.license = form.license.clone();
|
post.license = form.license.clone();
|
||||||
post.cover_id = form.cover;
|
post.cover_id = form.cover;
|
||||||
post.update(&*conn, &searcher).expect("post::update: update error");;
|
post.update(&*conn, &searcher)
|
||||||
|
.expect("post::update: update error");;
|
||||||
|
|
||||||
if post.published {
|
if post.published {
|
||||||
post.update_mentions(&conn, mentions.into_iter().filter_map(|m| Mention::build_activity(&conn, &m).ok()).collect())
|
post.update_mentions(
|
||||||
.expect("post::update: mentions error");;
|
&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())
|
let tags = form
|
||||||
.collect::<HashSet<_>>().into_iter().filter_map(|t| Tag::build_activity(&conn, t).ok()).collect::<Vec<_>>();
|
.tags
|
||||||
post.update_tags(&conn, tags).expect("post::update: tags error");
|
.split(',')
|
||||||
|
.map(|t| t.trim().to_camel_case())
|
||||||
|
.filter(|t| !t.is_empty())
|
||||||
|
.collect::<HashSet<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|t| Tag::build_activity(&conn, t).ok())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
post.update_tags(&conn, tags)
|
||||||
|
.expect("post::update: tags error");
|
||||||
|
|
||||||
let hashtags = hashtags.into_iter().map(|h| h.to_camel_case()).collect::<HashSet<_>>()
|
let hashtags = hashtags
|
||||||
.into_iter().filter_map(|t| Tag::build_activity(&conn, t).ok()).collect::<Vec<_>>();
|
.into_iter()
|
||||||
post.update_hashtags(&conn, hashtags).expect("post::update: hashtags error");
|
.map(|h| h.to_camel_case())
|
||||||
|
.collect::<HashSet<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|t| Tag::build_activity(&conn, t).ok())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
post.update_hashtags(&conn, hashtags)
|
||||||
|
.expect("post::update: hashtags error");
|
||||||
|
|
||||||
if post.published {
|
if post.published {
|
||||||
if newly_published {
|
if newly_published {
|
||||||
let act = post.create_activity(&conn).expect("post::update: act error");
|
let act = post
|
||||||
|
.create_activity(&conn)
|
||||||
|
.expect("post::update: act error");
|
||||||
let dest = User::one_by_instance(&*conn).expect("post::update: dest error");
|
let dest = User::one_by_instance(&*conn).expect("post::update: dest error");
|
||||||
worker.execute(move || broadcast(&user, act, dest));
|
worker.execute(move || broadcast(&user, act, dest));
|
||||||
} else {
|
} else {
|
||||||
let act = post.update_activity(&*conn).expect("post::update: act error");
|
let act = post
|
||||||
|
.update_activity(&*conn)
|
||||||
|
.expect("post::update: act error");
|
||||||
let dest = User::one_by_instance(&*conn).expect("posts::update: dest error");
|
let dest = User::one_by_instance(&*conn).expect("posts::update: dest error");
|
||||||
worker.execute(move || broadcast(&user, act, dest));
|
worker.execute(move || broadcast(&user, act, dest));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Redirect::to(uri!(details: blog = blog, slug = new_slug, responding_to = _)))
|
Ok(Redirect::to(
|
||||||
|
uri!(details: blog = blog, slug = new_slug, responding_to = _),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let medias = Media::for_user(&*conn, user.id).expect("posts:update: medias error");
|
let medias = Media::for_user(&*conn, user.id).expect("posts:update: medias error");
|
||||||
|
@ -306,7 +380,12 @@ pub fn valid_slug(title: &str) -> Result<(), ValidationError> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/~/<blog_name>/new", data = "<form>")]
|
#[post("/~/<blog_name>/new", data = "<form>")]
|
||||||
pub fn create(blog_name: String, form: LenientForm<NewPostForm>, cl: ContentLen, rockets: PlumeRocket) -> Result<Redirect, Result<Ructe, ErrorPage>> {
|
pub fn create(
|
||||||
|
blog_name: String,
|
||||||
|
form: LenientForm<NewPostForm>,
|
||||||
|
cl: ContentLen,
|
||||||
|
rockets: PlumeRocket,
|
||||||
|
) -> Result<Redirect, Result<Ructe, ErrorPage>> {
|
||||||
let conn = rockets.conn;
|
let conn = rockets.conn;
|
||||||
let blog = Blog::find_by_fqn(&*conn, &blog_name).expect("post::create: blog error");;
|
let blog = Blog::find_by_fqn(&*conn, &blog_name).expect("post::create: blog error");;
|
||||||
let slug = form.title.to_string().to_kebab_case();
|
let slug = form.title.to_string().to_kebab_case();
|
||||||
|
@ -314,86 +393,119 @@ pub fn create(blog_name: String, form: LenientForm<NewPostForm>, cl: ContentLen,
|
||||||
|
|
||||||
let mut errors = match form.validate() {
|
let mut errors = match form.validate() {
|
||||||
Ok(_) => ValidationErrors::new(),
|
Ok(_) => ValidationErrors::new(),
|
||||||
Err(e) => e
|
Err(e) => e,
|
||||||
};
|
};
|
||||||
if Post::find_by_slug(&*conn, &slug, blog.id).is_ok() {
|
if Post::find_by_slug(&*conn, &slug, blog.id).is_ok() {
|
||||||
errors.add("title", ValidationError {
|
errors.add(
|
||||||
code: Cow::from("existing_slug"),
|
"title",
|
||||||
message: Some(Cow::from("A post with the same title already exists.")),
|
ValidationError {
|
||||||
params: HashMap::new()
|
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 errors.is_empty() {
|
||||||
if !user.is_author_in(&*conn, &blog).expect("post::create: is author in error") {
|
if !user
|
||||||
|
.is_author_in(&*conn, &blog)
|
||||||
|
.expect("post::create: is author in error")
|
||||||
|
{
|
||||||
// actually it's not "Ok"…
|
// actually it's not "Ok"…
|
||||||
return Ok(Redirect::to(uri!(super::blogs::details: name = blog_name, page = _)))
|
return Ok(Redirect::to(
|
||||||
|
uri!(super::blogs::details: name = blog_name, page = _),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (content, mentions, hashtags) = utils::md_to_html(
|
let (content, mentions, hashtags) = utils::md_to_html(
|
||||||
form.content.to_string().as_ref(),
|
form.content.to_string().as_ref(),
|
||||||
&Instance::get_local(&conn).expect("post::create: local instance error").public_domain
|
&Instance::get_local(&conn)
|
||||||
|
.expect("post::create: local instance error")
|
||||||
|
.public_domain,
|
||||||
);
|
);
|
||||||
|
|
||||||
let searcher = rockets.searcher;
|
let searcher = rockets.searcher;
|
||||||
let post = Post::insert(&*conn, NewPost {
|
let post = Post::insert(
|
||||||
blog_id: blog.id,
|
&*conn,
|
||||||
slug: slug.to_string(),
|
NewPost {
|
||||||
title: form.title.to_string(),
|
blog_id: blog.id,
|
||||||
content: SafeString::new(&content),
|
slug: slug.to_string(),
|
||||||
published: !form.draft,
|
title: form.title.to_string(),
|
||||||
license: form.license.clone(),
|
content: SafeString::new(&content),
|
||||||
ap_url: "".to_string(),
|
published: !form.draft,
|
||||||
creation_date: None,
|
license: form.license.clone(),
|
||||||
subtitle: form.subtitle.clone(),
|
ap_url: "".to_string(),
|
||||||
source: form.content.clone(),
|
creation_date: None,
|
||||||
cover_id: form.cover,
|
subtitle: form.subtitle.clone(),
|
||||||
|
source: form.content.clone(),
|
||||||
|
cover_id: form.cover,
|
||||||
},
|
},
|
||||||
&searcher,
|
&searcher,
|
||||||
).expect("post::create: post save error");
|
)
|
||||||
|
.expect("post::create: post save error");
|
||||||
|
|
||||||
PostAuthor::insert(&*conn, NewPostAuthor {
|
PostAuthor::insert(
|
||||||
post_id: post.id,
|
&*conn,
|
||||||
author_id: user.id
|
NewPostAuthor {
|
||||||
}).expect("post::create: author save error");
|
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())
|
.map(|t| t.trim().to_camel_case())
|
||||||
.filter(|t| !t.is_empty())
|
.filter(|t| !t.is_empty())
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
for tag in tags {
|
for tag in tags {
|
||||||
Tag::insert(&*conn, NewTag {
|
Tag::insert(
|
||||||
tag,
|
&*conn,
|
||||||
is_hashtag: false,
|
NewTag {
|
||||||
post_id: post.id
|
tag,
|
||||||
}).expect("post::create: tags save error");
|
is_hashtag: false,
|
||||||
|
post_id: post.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("post::create: tags save error");
|
||||||
}
|
}
|
||||||
for hashtag in hashtags {
|
for hashtag in hashtags {
|
||||||
Tag::insert(&*conn, NewTag {
|
Tag::insert(
|
||||||
tag: hashtag.to_camel_case(),
|
&*conn,
|
||||||
is_hashtag: true,
|
NewTag {
|
||||||
post_id: post.id
|
tag: hashtag.to_camel_case(),
|
||||||
}).expect("post::create: hashtags save error");
|
is_hashtag: true,
|
||||||
|
post_id: post.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("post::create: hashtags save error");
|
||||||
}
|
}
|
||||||
|
|
||||||
if post.published {
|
if post.published {
|
||||||
for m in mentions {
|
for m in mentions {
|
||||||
Mention::from_activity(
|
Mention::from_activity(
|
||||||
&*conn,
|
&*conn,
|
||||||
&Mention::build_activity(&*conn, &m).expect("post::create: mention build error"),
|
&Mention::build_activity(&*conn, &m)
|
||||||
|
.expect("post::create: mention build error"),
|
||||||
post.id,
|
post.id,
|
||||||
true,
|
true,
|
||||||
true
|
true,
|
||||||
).expect("post::create: mention save error");
|
)
|
||||||
|
.expect("post::create: mention save error");
|
||||||
}
|
}
|
||||||
|
|
||||||
let act = post.create_activity(&*conn).expect("posts::create: activity error");
|
let act = post
|
||||||
|
.create_activity(&*conn)
|
||||||
|
.expect("posts::create: activity error");
|
||||||
let dest = User::one_by_instance(&*conn).expect("posts::create: dest error");
|
let dest = User::one_by_instance(&*conn).expect("posts::create: dest error");
|
||||||
let worker = rockets.worker;
|
let worker = rockets.worker;
|
||||||
worker.execute(move || broadcast(&user, act, dest));
|
worker.execute(move || broadcast(&user, act, dest));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Redirect::to(uri!(details: blog = blog_name, slug = slug, responding_to = _)))
|
Ok(Redirect::to(
|
||||||
|
uri!(details: blog = blog_name, slug = slug, responding_to = _),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
let medias = Media::for_user(&*conn, user.id).expect("posts::create: medias error");
|
let medias = Media::for_user(&*conn, user.id).expect("posts::create: medias error");
|
||||||
let intl = rockets.intl;
|
let intl = rockets.intl;
|
||||||
|
@ -413,15 +525,25 @@ pub fn create(blog_name: String, form: LenientForm<NewPostForm>, cl: ContentLen,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/~/<blog_name>/<slug>/delete")]
|
#[post("/~/<blog_name>/<slug>/delete")]
|
||||||
pub fn delete(blog_name: String, slug: String, rockets: PlumeRocket) -> Result<Redirect, ErrorPage> {
|
pub fn delete(
|
||||||
|
blog_name: String,
|
||||||
|
slug: String,
|
||||||
|
rockets: PlumeRocket,
|
||||||
|
) -> Result<Redirect, ErrorPage> {
|
||||||
let conn = rockets.conn;
|
let conn = rockets.conn;
|
||||||
let user = rockets.user.unwrap();
|
let user = rockets.user.unwrap();
|
||||||
let post = Blog::find_by_fqn(&*conn, &blog_name)
|
let post = Blog::find_by_fqn(&*conn, &blog_name)
|
||||||
.and_then(|blog| Post::find_by_slug(&*conn, &slug, blog.id));
|
.and_then(|blog| Post::find_by_slug(&*conn, &slug, blog.id));
|
||||||
|
|
||||||
if let Ok(post) = post {
|
if let Ok(post) = post {
|
||||||
if !post.get_authors(&*conn)?.into_iter().any(|a| a.id == user.id) {
|
if !post
|
||||||
return Ok(Redirect::to(uri!(details: blog = blog_name.clone(), slug = slug.clone(), responding_to = _)))
|
.get_authors(&*conn)?
|
||||||
|
.into_iter()
|
||||||
|
.any(|a| a.id == user.id)
|
||||||
|
{
|
||||||
|
return Ok(Redirect::to(
|
||||||
|
uri!(details: blog = blog_name.clone(), slug = slug.clone(), responding_to = _),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let searcher = rockets.searcher;
|
let searcher = rockets.searcher;
|
||||||
|
@ -432,10 +554,17 @@ pub fn delete(blog_name: String, slug: String, rockets: PlumeRocket) -> Result<R
|
||||||
let user_c = user.clone();
|
let user_c = user.clone();
|
||||||
|
|
||||||
worker.execute(move || broadcast(&user_c, delete_activity, dest));
|
worker.execute(move || broadcast(&user_c, delete_activity, dest));
|
||||||
worker.execute_after(Duration::from_secs(10*60), move || {user.rotate_keypair(&conn).expect("Failed to rotate keypair");});
|
worker.execute_after(Duration::from_secs(10 * 60), move || {
|
||||||
|
user.rotate_keypair(&conn)
|
||||||
|
.expect("Failed to rotate keypair");
|
||||||
|
});
|
||||||
|
|
||||||
Ok(Redirect::to(uri!(super::blogs::details: name = blog_name, page = _)))
|
Ok(Redirect::to(
|
||||||
|
uri!(super::blogs::details: name = blog_name, page = _),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
Ok(Redirect::to(uri!(super::blogs::details: name = blog_name, page = _)))
|
Ok(Redirect::to(
|
||||||
|
uri!(super::blogs::details: name = blog_name, page = _),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,23 @@
|
||||||
use rocket::response::{Redirect, Flash};
|
use rocket::response::{Flash, Redirect};
|
||||||
use rocket_i18n::I18n;
|
use rocket_i18n::I18n;
|
||||||
|
|
||||||
use plume_common::activity_pub::{broadcast, inbox::{Deletable, Notify}};
|
use plume_common::activity_pub::{
|
||||||
use plume_common::utils;
|
broadcast,
|
||||||
use plume_models::{
|
inbox::{Deletable, Notify},
|
||||||
blogs::Blog,
|
|
||||||
db_conn::DbConn,
|
|
||||||
posts::Post,
|
|
||||||
reshares::*,
|
|
||||||
users::User
|
|
||||||
};
|
};
|
||||||
|
use plume_common::utils;
|
||||||
|
use plume_models::{blogs::Blog, db_conn::DbConn, posts::Post, reshares::*, users::User};
|
||||||
use routes::errors::ErrorPage;
|
use routes::errors::ErrorPage;
|
||||||
use Worker;
|
use Worker;
|
||||||
|
|
||||||
#[post("/~/<blog>/<slug>/reshare")]
|
#[post("/~/<blog>/<slug>/reshare")]
|
||||||
pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Result<Redirect, ErrorPage> {
|
pub fn create(
|
||||||
|
blog: String,
|
||||||
|
slug: String,
|
||||||
|
user: User,
|
||||||
|
conn: DbConn,
|
||||||
|
worker: Worker,
|
||||||
|
) -> Result<Redirect, ErrorPage> {
|
||||||
let b = Blog::find_by_fqn(&*conn, &blog)?;
|
let b = Blog::find_by_fqn(&*conn, &blog)?;
|
||||||
let post = Post::find_by_slug(&*conn, &slug, b.id)?;
|
let post = Post::find_by_slug(&*conn, &slug, b.id)?;
|
||||||
|
|
||||||
|
@ -32,13 +35,18 @@ pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Work
|
||||||
worker.execute(move || broadcast(&user, delete_act, dest));
|
worker.execute(move || broadcast(&user, delete_act, dest));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _)))
|
Ok(Redirect::to(
|
||||||
|
uri!(super::posts::details: blog = blog, slug = slug, responding_to = _),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/~/<blog>/<slug>/reshare", rank=1)]
|
#[post("/~/<blog>/<slug>/reshare", rank = 1)]
|
||||||
pub fn create_auth(blog: String, slug: String, i18n: I18n) -> Flash<Redirect> {
|
pub fn create_auth(blog: String, slug: String, i18n: I18n) -> Flash<Redirect> {
|
||||||
utils::requires_login(
|
utils::requires_login(
|
||||||
&i18n!(i18n.catalog, "You need to be logged in order to reshare a post"),
|
&i18n!(
|
||||||
uri!(create: blog = blog, slug = slug)
|
i18n.catalog,
|
||||||
|
"You need to be logged in order to reshare a post"
|
||||||
|
),
|
||||||
|
uri!(create: blog = blog, slug = slug),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,11 @@ use chrono::offset::Utc;
|
||||||
use rocket::request::Form;
|
use rocket::request::Form;
|
||||||
use rocket_i18n::I18n;
|
use rocket_i18n::I18n;
|
||||||
|
|
||||||
use plume_models::{
|
use plume_models::{db_conn::DbConn, search::Query, users::User};
|
||||||
db_conn::DbConn, users::User,
|
|
||||||
search::Query};
|
|
||||||
use routes::Page;
|
use routes::Page;
|
||||||
|
use std::str::FromStr;
|
||||||
use template_utils::Ructe;
|
use template_utils::Ructe;
|
||||||
use Searcher;
|
use Searcher;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[derive(Default, FromForm)]
|
#[derive(Default, FromForm)]
|
||||||
pub struct SearchQuery {
|
pub struct SearchQuery {
|
||||||
|
@ -53,12 +51,19 @@ macro_rules! param_to_query {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[get("/search?<query..>")]
|
#[get("/search?<query..>")]
|
||||||
pub fn search(query: Option<Form<SearchQuery>>, conn: DbConn, searcher: Searcher, user: Option<User>, intl: I18n) -> Ructe {
|
pub fn search(
|
||||||
|
query: Option<Form<SearchQuery>>,
|
||||||
|
conn: DbConn,
|
||||||
|
searcher: Searcher,
|
||||||
|
user: Option<User>,
|
||||||
|
intl: I18n,
|
||||||
|
) -> Ructe {
|
||||||
let query = query.map(|f| f.into_inner()).unwrap_or_default();
|
let query = query.map(|f| f.into_inner()).unwrap_or_default();
|
||||||
let page = query.page.unwrap_or_default();
|
let page = query.page.unwrap_or_default();
|
||||||
let mut parsed_query = Query::from_str(&query.q.as_ref().map(|q| q.as_str()).unwrap_or_default()).unwrap_or_default();
|
let mut parsed_query =
|
||||||
|
Query::from_str(&query.q.as_ref().map(|q| q.as_str()).unwrap_or_default())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
param_to_query!(query, parsed_query; normal: title, subtitle, content, tag,
|
param_to_query!(query, parsed_query; normal: title, subtitle, content, tag,
|
||||||
instance, author, blog, lang, license;
|
instance, author, blog, lang, license;
|
||||||
|
|
|
@ -1,22 +1,26 @@
|
||||||
use lettre::Transport;
|
use lettre::Transport;
|
||||||
use rocket::{
|
|
||||||
State,
|
|
||||||
http::{Cookie, Cookies, SameSite, uri::Uri},
|
|
||||||
response::Redirect,
|
|
||||||
request::{LenientForm, FlashMessage, Form}
|
|
||||||
};
|
|
||||||
use rocket::http::ext::IntoOwned;
|
use rocket::http::ext::IntoOwned;
|
||||||
use rocket_i18n::I18n;
|
use rocket::{
|
||||||
use std::{borrow::Cow, sync::{Arc, Mutex}, time::Instant};
|
http::{uri::Uri, Cookie, Cookies, SameSite},
|
||||||
use validator::{Validate, ValidationError, ValidationErrors};
|
request::{FlashMessage, Form, LenientForm},
|
||||||
use template_utils::Ructe;
|
response::Redirect,
|
||||||
|
State,
|
||||||
use plume_models::{
|
|
||||||
BASE_URL, Error,
|
|
||||||
db_conn::DbConn,
|
|
||||||
users::{User, AUTH_COOKIE}
|
|
||||||
};
|
};
|
||||||
|
use rocket_i18n::I18n;
|
||||||
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
time::Instant,
|
||||||
|
};
|
||||||
|
use template_utils::Ructe;
|
||||||
|
use validator::{Validate, ValidationError, ValidationErrors};
|
||||||
|
|
||||||
use mail::{build_mail, Mailer};
|
use mail::{build_mail, Mailer};
|
||||||
|
use plume_models::{
|
||||||
|
db_conn::DbConn,
|
||||||
|
users::{User, AUTH_COOKIE},
|
||||||
|
Error, BASE_URL,
|
||||||
|
};
|
||||||
use routes::errors::ErrorPage;
|
use routes::errors::ErrorPage;
|
||||||
|
|
||||||
#[get("/login?<m>")]
|
#[get("/login?<m>")]
|
||||||
|
@ -34,16 +38,22 @@ pub struct LoginForm {
|
||||||
#[validate(length(min = "1", message = "We need an email or a username to identify you"))]
|
#[validate(length(min = "1", message = "We need an email or a username to identify you"))]
|
||||||
pub email_or_name: String,
|
pub email_or_name: String,
|
||||||
#[validate(length(min = "1", message = "Your password can't be empty"))]
|
#[validate(length(min = "1", message = "Your password can't be empty"))]
|
||||||
pub password: String
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/login", data = "<form>")]
|
#[post("/login", data = "<form>")]
|
||||||
pub fn create(conn: DbConn, form: LenientForm<LoginForm>, flash: Option<FlashMessage>, mut cookies: Cookies, intl: I18n) -> Result<Redirect, Ructe> {
|
pub fn create(
|
||||||
|
conn: DbConn,
|
||||||
|
form: LenientForm<LoginForm>,
|
||||||
|
flash: Option<FlashMessage>,
|
||||||
|
mut cookies: Cookies,
|
||||||
|
intl: I18n,
|
||||||
|
) -> Result<Redirect, Ructe> {
|
||||||
let user = User::find_by_email(&*conn, &form.email_or_name)
|
let user = User::find_by_email(&*conn, &form.email_or_name)
|
||||||
.or_else(|_| User::find_by_fqn(&*conn, &form.email_or_name));
|
.or_else(|_| User::find_by_fqn(&*conn, &form.email_or_name));
|
||||||
let mut errors = match form.validate() {
|
let mut errors = match form.validate() {
|
||||||
Ok(_) => ValidationErrors::new(),
|
Ok(_) => ValidationErrors::new(),
|
||||||
Err(e) => e
|
Err(e) => e,
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = if let Ok(user) = user {
|
let user_id = if let Ok(user) = user {
|
||||||
|
@ -58,7 +68,9 @@ pub fn create(conn: DbConn, form: LenientForm<LoginForm>, flash: Option<FlashMes
|
||||||
} else {
|
} else {
|
||||||
// Fake password verification, only to avoid different login times
|
// Fake password verification, only to avoid different login times
|
||||||
// that could be used to see if an email adress is registered or not
|
// that could be used to see if an email adress is registered or not
|
||||||
User::get(&*conn, 1).map(|u| u.auth(&form.password)).expect("No user is registered");
|
User::get(&*conn, 1)
|
||||||
|
.map(|u| u.auth(&form.password))
|
||||||
|
.expect("No user is registered");
|
||||||
|
|
||||||
let mut err = ValidationError::new("invalid_login");
|
let mut err = ValidationError::new("invalid_login");
|
||||||
err.message = Some(Cow::from("Invalid username or password"));
|
err.message = Some(Cow::from("Invalid username or password"));
|
||||||
|
@ -67,25 +79,31 @@ pub fn create(conn: DbConn, form: LenientForm<LoginForm>, flash: Option<FlashMes
|
||||||
};
|
};
|
||||||
|
|
||||||
if errors.is_empty() {
|
if errors.is_empty() {
|
||||||
cookies.add_private(Cookie::build(AUTH_COOKIE, user_id)
|
cookies.add_private(
|
||||||
.same_site(SameSite::Lax)
|
Cookie::build(AUTH_COOKIE, user_id)
|
||||||
.finish());
|
.same_site(SameSite::Lax)
|
||||||
|
.finish(),
|
||||||
|
);
|
||||||
let destination = flash
|
let destination = flash
|
||||||
.and_then(|f| if f.name() == "callback" {
|
.and_then(|f| {
|
||||||
Some(f.msg().to_owned())
|
if f.name() == "callback" {
|
||||||
} else {
|
Some(f.msg().to_owned())
|
||||||
None
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| "/".to_owned());
|
.unwrap_or_else(|| "/".to_owned());
|
||||||
|
|
||||||
let uri = Uri::parse(&destination)
|
let uri = Uri::parse(&destination)
|
||||||
.map(|x| x.into_owned())
|
.map(|x| x.into_owned())
|
||||||
.map_err(|_| render!(session::login(
|
.map_err(|_| {
|
||||||
&(&*conn, &intl.catalog, None),
|
render!(session::login(
|
||||||
None,
|
&(&*conn, &intl.catalog, None),
|
||||||
&*form,
|
None,
|
||||||
errors
|
&*form,
|
||||||
)))?;
|
errors
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(Redirect::to(uri))
|
Ok(Redirect::to(uri))
|
||||||
} else {
|
} else {
|
||||||
|
@ -140,13 +158,15 @@ pub fn password_reset_request(
|
||||||
intl: I18n,
|
intl: I18n,
|
||||||
mail: State<Arc<Mutex<Mailer>>>,
|
mail: State<Arc<Mutex<Mailer>>>,
|
||||||
form: Form<ResetForm>,
|
form: Form<ResetForm>,
|
||||||
requests: State<Arc<Mutex<Vec<ResetRequest>>>>
|
requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
|
||||||
) -> Ructe {
|
) -> Ructe {
|
||||||
let mut requests = requests.lock().unwrap();
|
let mut requests = requests.lock().unwrap();
|
||||||
// Remove outdated requests (more than 1 day old) to avoid the list to grow too much
|
// Remove outdated requests (more than 1 day old) to avoid the list to grow too much
|
||||||
requests.retain(|r| r.creation_date.elapsed().as_secs() < 24 * 60 * 60);
|
requests.retain(|r| r.creation_date.elapsed().as_secs() < 24 * 60 * 60);
|
||||||
|
|
||||||
if User::find_by_email(&*conn, &form.email).is_ok() && !requests.iter().any(|x| x.mail == form.email.clone()) {
|
if User::find_by_email(&*conn, &form.email).is_ok()
|
||||||
|
&& !requests.iter().any(|x| x.mail == form.email.clone())
|
||||||
|
{
|
||||||
let id = plume_common::utils::random_hex();
|
let id = plume_common::utils::random_hex();
|
||||||
|
|
||||||
requests.push(ResetRequest {
|
requests.push(ResetRequest {
|
||||||
|
@ -159,22 +179,35 @@ pub fn password_reset_request(
|
||||||
if let Some(message) = build_mail(
|
if let Some(message) = build_mail(
|
||||||
form.email.clone(),
|
form.email.clone(),
|
||||||
i18n!(intl.catalog, "Password reset"),
|
i18n!(intl.catalog, "Password reset"),
|
||||||
i18n!(intl.catalog, "Here is the link to reset your password: {0}"; link)
|
i18n!(intl.catalog, "Here is the link to reset your password: {0}"; link),
|
||||||
) {
|
) {
|
||||||
if let Some(ref mut mail) = *mail.lock().unwrap() {
|
if let Some(ref mut mail) = *mail.lock().unwrap() {
|
||||||
mail
|
mail.send(message.into())
|
||||||
.send(message.into())
|
.map_err(|_| eprintln!("Couldn't send password reset mail"))
|
||||||
.map_err(|_| eprintln!("Couldn't send password reset mail")).ok(); }
|
.ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
render!(session::password_reset_request_ok(
|
render!(session::password_reset_request_ok(&(
|
||||||
&(&*conn, &intl.catalog, None)
|
&*conn,
|
||||||
))
|
&intl.catalog,
|
||||||
|
None
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/password-reset/<token>")]
|
#[get("/password-reset/<token>")]
|
||||||
pub fn password_reset_form(conn: DbConn, intl: I18n, token: String, requests: State<Arc<Mutex<Vec<ResetRequest>>>>) -> Result<Ructe, ErrorPage> {
|
pub fn password_reset_form(
|
||||||
requests.lock().unwrap().iter().find(|x| x.id == token.clone()).ok_or(Error::NotFound)?;
|
conn: DbConn,
|
||||||
|
intl: I18n,
|
||||||
|
token: String,
|
||||||
|
requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
|
||||||
|
) -> Result<Ructe, ErrorPage> {
|
||||||
|
requests
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find(|x| x.id == token.clone())
|
||||||
|
.ok_or(Error::NotFound)?;
|
||||||
Ok(render!(session::password_reset(
|
Ok(render!(session::password_reset(
|
||||||
&(&*conn, &intl.catalog, None),
|
&(&*conn, &intl.catalog, None),
|
||||||
&NewPasswordForm::default(),
|
&NewPasswordForm::default(),
|
||||||
|
@ -183,13 +216,11 @@ pub fn password_reset_form(conn: DbConn, intl: I18n, token: String, requests: St
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromForm, Default, Validate)]
|
#[derive(FromForm, Default, Validate)]
|
||||||
#[validate(
|
#[validate(schema(
|
||||||
schema(
|
function = "passwords_match",
|
||||||
function = "passwords_match",
|
skip_on_field_errors = "false",
|
||||||
skip_on_field_errors = "false",
|
message = "Passwords are not matching"
|
||||||
message = "Passwords are not matching"
|
))]
|
||||||
)
|
|
||||||
)]
|
|
||||||
pub struct NewPasswordForm {
|
pub struct NewPasswordForm {
|
||||||
pub password: String,
|
pub password: String,
|
||||||
pub password_confirmation: String,
|
pub password_confirmation: String,
|
||||||
|
@ -209,19 +240,28 @@ pub fn password_reset(
|
||||||
intl: I18n,
|
intl: I18n,
|
||||||
token: String,
|
token: String,
|
||||||
requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
|
requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
|
||||||
form: Form<NewPasswordForm>
|
form: Form<NewPasswordForm>,
|
||||||
) -> Result<Redirect, Ructe> {
|
) -> Result<Redirect, Ructe> {
|
||||||
form.validate()
|
form.validate()
|
||||||
.and_then(|_| {
|
.and_then(|_| {
|
||||||
let mut requests = requests.lock().unwrap();
|
let mut requests = requests.lock().unwrap();
|
||||||
let req = requests.iter().find(|x| x.id == token.clone()).ok_or_else(|| to_validation(0))?.clone();
|
let req = requests
|
||||||
if req.creation_date.elapsed().as_secs() < 60 * 60 * 2 { // Reset link is only valid for 2 hours
|
.iter()
|
||||||
|
.find(|x| x.id == token.clone())
|
||||||
|
.ok_or_else(|| to_validation(0))?
|
||||||
|
.clone();
|
||||||
|
if req.creation_date.elapsed().as_secs() < 60 * 60 * 2 {
|
||||||
|
// Reset link is only valid for 2 hours
|
||||||
requests.retain(|r| *r != req);
|
requests.retain(|r| *r != req);
|
||||||
let user = User::find_by_email(&*conn, &req.mail).map_err(to_validation)?;
|
let user = User::find_by_email(&*conn, &req.mail).map_err(to_validation)?;
|
||||||
user.reset_password(&*conn, &form.password).ok();
|
user.reset_password(&*conn, &form.password).ok();
|
||||||
Ok(Redirect::to(uri!(new: m = i18n!(intl.catalog, "Your password was successfully reset."))))
|
Ok(Redirect::to(uri!(
|
||||||
|
new: m = i18n!(intl.catalog, "Your password was successfully reset.")
|
||||||
|
)))
|
||||||
} else {
|
} else {
|
||||||
Ok(Redirect::to(uri!(new: m = i18n!(intl.catalog, "Sorry, but the link expired. Try again"))))
|
Ok(Redirect::to(uri!(
|
||||||
|
new: m = i18n!(intl.catalog, "Sorry, but the link expired. Try again")
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
|
@ -235,10 +275,13 @@ pub fn password_reset(
|
||||||
|
|
||||||
fn to_validation<T>(_: T) -> ValidationErrors {
|
fn to_validation<T>(_: T) -> ValidationErrors {
|
||||||
let mut errors = ValidationErrors::new();
|
let mut errors = ValidationErrors::new();
|
||||||
errors.add("", ValidationError {
|
errors.add(
|
||||||
code: Cow::from("server_error"),
|
"",
|
||||||
message: Some(Cow::from("An unknown error occured")),
|
ValidationError {
|
||||||
params: std::collections::HashMap::new()
|
code: Cow::from("server_error"),
|
||||||
});
|
message: Some(Cow::from("An unknown error occured")),
|
||||||
|
params: std::collections::HashMap::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
errors
|
errors
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
use rocket_i18n::I18n;
|
use rocket_i18n::I18n;
|
||||||
|
|
||||||
use plume_models::{
|
use plume_models::{db_conn::DbConn, posts::Post, users::User};
|
||||||
db_conn::DbConn,
|
use routes::{errors::ErrorPage, Page};
|
||||||
posts::Post,
|
|
||||||
users::User,
|
|
||||||
};
|
|
||||||
use routes::{Page, errors::ErrorPage};
|
|
||||||
use template_utils::Ructe;
|
use template_utils::Ructe;
|
||||||
|
|
||||||
#[get("/tag/<name>?<page>")]
|
#[get("/tag/<name>?<page>")]
|
||||||
pub fn tag(user: Option<User>, conn: DbConn, name: String, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
|
pub fn tag(
|
||||||
|
user: Option<User>,
|
||||||
|
conn: DbConn,
|
||||||
|
name: String,
|
||||||
|
page: Option<Page>,
|
||||||
|
intl: I18n,
|
||||||
|
) -> Result<Ructe, ErrorPage> {
|
||||||
let page = page.unwrap_or_default();
|
let page = page.unwrap_or_default();
|
||||||
let posts = Post::list_by_tag(&*conn, name.clone(), page.limits())?;
|
let posts = Post::list_by_tag(&*conn, name.clone(), page.limits())?;
|
||||||
Ok(render!(tags::index(
|
Ok(render!(tags::index(
|
||||||
|
|
|
@ -19,14 +19,20 @@ use plume_common::activity_pub::{
|
||||||
};
|
};
|
||||||
use plume_common::utils;
|
use plume_common::utils;
|
||||||
use plume_models::{
|
use plume_models::{
|
||||||
|
blogs::Blog,
|
||||||
|
db_conn::DbConn,
|
||||||
|
follows,
|
||||||
|
headers::Headers,
|
||||||
|
instance::Instance,
|
||||||
|
posts::{LicensedArticle, Post},
|
||||||
|
reshares::Reshare,
|
||||||
|
users::*,
|
||||||
Error,
|
Error,
|
||||||
blogs::Blog, db_conn::DbConn, follows, headers::Headers, instance::Instance, posts::{LicensedArticle, Post},
|
|
||||||
reshares::Reshare, users::*,
|
|
||||||
};
|
};
|
||||||
use routes::{Page, PlumeRocket, errors::ErrorPage};
|
use routes::{errors::ErrorPage, Page, PlumeRocket};
|
||||||
use template_utils::Ructe;
|
use template_utils::Ructe;
|
||||||
use Worker;
|
|
||||||
use Searcher;
|
use Searcher;
|
||||||
|
use Worker;
|
||||||
|
|
||||||
#[get("/me")]
|
#[get("/me")]
|
||||||
pub fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
|
pub fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
|
||||||
|
@ -56,19 +62,21 @@ pub fn details(
|
||||||
let user_clone = user.clone();
|
let user_clone = user.clone();
|
||||||
let searcher = searcher.clone();
|
let searcher = searcher.clone();
|
||||||
worker.execute(move || {
|
worker.execute(move || {
|
||||||
for create_act in user_clone.fetch_outbox::<Create>().expect("Remote user: outbox couldn't be fetched") {
|
for create_act in user_clone
|
||||||
|
.fetch_outbox::<Create>()
|
||||||
|
.expect("Remote user: outbox couldn't be fetched")
|
||||||
|
{
|
||||||
match create_act.create_props.object_object::<LicensedArticle>() {
|
match create_act.create_props.object_object::<LicensedArticle>() {
|
||||||
Ok(article) => {
|
Ok(article) => {
|
||||||
Post::from_activity(
|
Post::from_activity(
|
||||||
&(&*fetch_articles_conn, &searcher),
|
&(&*fetch_articles_conn, &searcher),
|
||||||
article,
|
article,
|
||||||
user_clone.clone().into_id(),
|
user_clone.clone().into_id(),
|
||||||
).expect("Article from remote user couldn't be saved");
|
)
|
||||||
|
.expect("Article from remote user couldn't be saved");
|
||||||
println!("Fetched article from remote user");
|
println!("Fetched article from remote user");
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => println!("Error while fetching articles in background: {:?}", e),
|
||||||
println!("Error while fetching articles in background: {:?}", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -76,8 +84,12 @@ pub fn details(
|
||||||
// Fetch followers
|
// Fetch followers
|
||||||
let user_clone = user.clone();
|
let user_clone = user.clone();
|
||||||
worker.execute(move || {
|
worker.execute(move || {
|
||||||
for user_id in user_clone.fetch_followers_ids().expect("Remote user: fetching followers error") {
|
for user_id in user_clone
|
||||||
let follower = User::from_url(&*fetch_followers_conn, &user_id).expect("user::details: Couldn't fetch follower");
|
.fetch_followers_ids()
|
||||||
|
.expect("Remote user: fetching followers error")
|
||||||
|
{
|
||||||
|
let follower = User::from_url(&*fetch_followers_conn, &user_id)
|
||||||
|
.expect("user::details: Couldn't fetch follower");
|
||||||
follows::Follow::insert(
|
follows::Follow::insert(
|
||||||
&*fetch_followers_conn,
|
&*fetch_followers_conn,
|
||||||
follows::NewFollow {
|
follows::NewFollow {
|
||||||
|
@ -85,7 +97,8 @@ pub fn details(
|
||||||
following_id: user_clone.id,
|
following_id: user_clone.id,
|
||||||
ap_url: String::new(),
|
ap_url: String::new(),
|
||||||
},
|
},
|
||||||
).expect("Couldn't save follower for remote user");
|
)
|
||||||
|
.expect("Couldn't save follower for remote user");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -93,7 +106,9 @@ pub fn details(
|
||||||
let user_clone = user.clone();
|
let user_clone = user.clone();
|
||||||
if user.needs_update() {
|
if user.needs_update() {
|
||||||
worker.execute(move || {
|
worker.execute(move || {
|
||||||
user_clone.refetch(&*update_conn).expect("Couldn't update user info");
|
user_clone
|
||||||
|
.refetch(&*update_conn)
|
||||||
|
.expect("Couldn't update user info");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,11 +118,16 @@ pub fn details(
|
||||||
Ok(render!(users::details(
|
Ok(render!(users::details(
|
||||||
&(&*conn, &intl.catalog, account.clone()),
|
&(&*conn, &intl.catalog, account.clone()),
|
||||||
user.clone(),
|
user.clone(),
|
||||||
account.and_then(|x| x.is_following(&*conn, user.id).ok()).unwrap_or(false),
|
account
|
||||||
|
.and_then(|x| x.is_following(&*conn, user.id).ok())
|
||||||
|
.unwrap_or(false),
|
||||||
user.instance_id != Instance::get_local(&*conn)?.id,
|
user.instance_id != Instance::get_local(&*conn)?.id,
|
||||||
user.get_instance(&*conn)?.public_domain,
|
user.get_instance(&*conn)?.public_domain,
|
||||||
recents,
|
recents,
|
||||||
reshares.into_iter().filter_map(|r| r.get_post(&*conn).ok()).collect()
|
reshares
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|r| r.get_post(&*conn).ok())
|
||||||
|
.collect()
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,19 +144,25 @@ pub fn dashboard(user: User, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPag
|
||||||
#[get("/dashboard", rank = 2)]
|
#[get("/dashboard", rank = 2)]
|
||||||
pub fn dashboard_auth(i18n: I18n) -> Flash<Redirect> {
|
pub fn dashboard_auth(i18n: I18n) -> Flash<Redirect> {
|
||||||
utils::requires_login(
|
utils::requires_login(
|
||||||
&i18n!(i18n.catalog, "You need to be logged in order to access your dashboard"),
|
&i18n!(
|
||||||
|
i18n.catalog,
|
||||||
|
"You need to be logged in order to access your dashboard"
|
||||||
|
),
|
||||||
uri!(dashboard),
|
uri!(dashboard),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/@/<name>/follow")]
|
#[post("/@/<name>/follow")]
|
||||||
pub fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Result<Redirect, ErrorPage> {
|
pub fn follow(
|
||||||
|
name: String,
|
||||||
|
conn: DbConn,
|
||||||
|
user: User,
|
||||||
|
worker: Worker,
|
||||||
|
) -> Result<Redirect, ErrorPage> {
|
||||||
let target = User::find_by_fqn(&*conn, &name)?;
|
let target = User::find_by_fqn(&*conn, &name)?;
|
||||||
if let Ok(follow) = follows::Follow::find(&*conn, user.id, target.id) {
|
if let Ok(follow) = follows::Follow::find(&*conn, user.id, target.id) {
|
||||||
let delete_act = follow.delete(&*conn)?;
|
let delete_act = follow.delete(&*conn)?;
|
||||||
worker.execute(move || {
|
worker.execute(move || broadcast(&user, delete_act, vec![target]));
|
||||||
broadcast(&user, delete_act, vec![target])
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
let f = follows::Follow::insert(
|
let f = follows::Follow::insert(
|
||||||
&*conn,
|
&*conn,
|
||||||
|
@ -157,13 +183,22 @@ pub fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Result<
|
||||||
#[post("/@/<name>/follow", rank = 2)]
|
#[post("/@/<name>/follow", rank = 2)]
|
||||||
pub fn follow_auth(name: String, i18n: I18n) -> Flash<Redirect> {
|
pub fn follow_auth(name: String, i18n: I18n) -> Flash<Redirect> {
|
||||||
utils::requires_login(
|
utils::requires_login(
|
||||||
&i18n!(i18n.catalog, "You need to be logged in order to subscribe to someone"),
|
&i18n!(
|
||||||
|
i18n.catalog,
|
||||||
|
"You need to be logged in order to subscribe to someone"
|
||||||
|
),
|
||||||
uri!(follow: name = name),
|
uri!(follow: name = name),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/@/<name>/followers?<page>", rank = 2)]
|
#[get("/@/<name>/followers?<page>", rank = 2)]
|
||||||
pub fn followers(name: String, conn: DbConn, account: Option<User>, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
|
pub fn followers(
|
||||||
|
name: String,
|
||||||
|
conn: DbConn,
|
||||||
|
account: Option<User>,
|
||||||
|
page: Option<Page>,
|
||||||
|
intl: I18n,
|
||||||
|
) -> Result<Ructe, ErrorPage> {
|
||||||
let page = page.unwrap_or_default();
|
let page = page.unwrap_or_default();
|
||||||
let user = User::find_by_fqn(&*conn, &name)?;
|
let user = User::find_by_fqn(&*conn, &name)?;
|
||||||
let followers_count = user.count_followers(&*conn)?;
|
let followers_count = user.count_followers(&*conn)?;
|
||||||
|
@ -171,7 +206,9 @@ pub fn followers(name: String, conn: DbConn, account: Option<User>, page: Option
|
||||||
Ok(render!(users::followers(
|
Ok(render!(users::followers(
|
||||||
&(&*conn, &intl.catalog, account.clone()),
|
&(&*conn, &intl.catalog, account.clone()),
|
||||||
user.clone(),
|
user.clone(),
|
||||||
account.and_then(|x| x.is_following(&*conn, user.id).ok()).unwrap_or(false),
|
account
|
||||||
|
.and_then(|x| x.is_following(&*conn, user.id).ok())
|
||||||
|
.unwrap_or(false),
|
||||||
user.instance_id != Instance::get_local(&*conn)?.id,
|
user.instance_id != Instance::get_local(&*conn)?.id,
|
||||||
user.get_instance(&*conn)?.public_domain,
|
user.get_instance(&*conn)?.public_domain,
|
||||||
user.get_followers_page(&*conn, page.limits())?,
|
user.get_followers_page(&*conn, page.limits())?,
|
||||||
|
@ -181,7 +218,13 @@ pub fn followers(name: String, conn: DbConn, account: Option<User>, page: Option
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/@/<name>/followed?<page>", rank = 2)]
|
#[get("/@/<name>/followed?<page>", rank = 2)]
|
||||||
pub fn followed(name: String, conn: DbConn, account: Option<User>, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
|
pub fn followed(
|
||||||
|
name: String,
|
||||||
|
conn: DbConn,
|
||||||
|
account: Option<User>,
|
||||||
|
page: Option<Page>,
|
||||||
|
intl: I18n,
|
||||||
|
) -> Result<Ructe, ErrorPage> {
|
||||||
let page = page.unwrap_or_default();
|
let page = page.unwrap_or_default();
|
||||||
let user = User::find_by_fqn(&*conn, &name)?;
|
let user = User::find_by_fqn(&*conn, &name)?;
|
||||||
let followed_count = user.count_followed(&*conn)?;
|
let followed_count = user.count_followed(&*conn)?;
|
||||||
|
@ -189,7 +232,9 @@ pub fn followed(name: String, conn: DbConn, account: Option<User>, page: Option<
|
||||||
Ok(render!(users::followed(
|
Ok(render!(users::followed(
|
||||||
&(&*conn, &intl.catalog, account.clone()),
|
&(&*conn, &intl.catalog, account.clone()),
|
||||||
user.clone(),
|
user.clone(),
|
||||||
account.and_then(|x| x.is_following(&*conn, user.id).ok()).unwrap_or(false),
|
account
|
||||||
|
.and_then(|x| x.is_following(&*conn, user.id).ok())
|
||||||
|
.unwrap_or(false),
|
||||||
user.instance_id != Instance::get_local(&*conn)?.id,
|
user.instance_id != Instance::get_local(&*conn)?.id,
|
||||||
user.get_instance(&*conn)?.public_domain,
|
user.get_instance(&*conn)?.public_domain,
|
||||||
user.get_followed_page(&*conn, page.limits())?,
|
user.get_followed_page(&*conn, page.limits())?,
|
||||||
|
@ -238,7 +283,10 @@ pub fn edit(name: String, user: User, conn: DbConn, intl: I18n) -> Result<Ructe,
|
||||||
#[get("/@/<name>/edit", rank = 2)]
|
#[get("/@/<name>/edit", rank = 2)]
|
||||||
pub fn edit_auth(name: String, i18n: I18n) -> Flash<Redirect> {
|
pub fn edit_auth(name: String, i18n: I18n) -> Flash<Redirect> {
|
||||||
utils::requires_login(
|
utils::requires_login(
|
||||||
&i18n!(i18n.catalog, "You need to be logged in order to edit your profile"),
|
&i18n!(
|
||||||
|
i18n.catalog,
|
||||||
|
"You need to be logged in order to edit your profile"
|
||||||
|
),
|
||||||
uri!(edit: name = name),
|
uri!(edit: name = name),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -251,18 +299,41 @@ pub struct UpdateUserForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/@/<_name>/edit", data = "<form>")]
|
#[put("/@/<_name>/edit", data = "<form>")]
|
||||||
pub fn update(_name: String, conn: DbConn, user: User, form: LenientForm<UpdateUserForm>) -> Result<Redirect, ErrorPage> {
|
pub fn update(
|
||||||
|
_name: String,
|
||||||
|
conn: DbConn,
|
||||||
|
user: User,
|
||||||
|
form: LenientForm<UpdateUserForm>,
|
||||||
|
) -> Result<Redirect, ErrorPage> {
|
||||||
user.update(
|
user.update(
|
||||||
&*conn,
|
&*conn,
|
||||||
if !form.display_name.is_empty() { form.display_name.clone() } else { user.display_name.clone() },
|
if !form.display_name.is_empty() {
|
||||||
if !form.email.is_empty() { form.email.clone() } else { user.email.clone().unwrap_or_default() },
|
form.display_name.clone()
|
||||||
if !form.summary.is_empty() { form.summary.clone() } else { user.summary.to_string() },
|
} else {
|
||||||
|
user.display_name.clone()
|
||||||
|
},
|
||||||
|
if !form.email.is_empty() {
|
||||||
|
form.email.clone()
|
||||||
|
} else {
|
||||||
|
user.email.clone().unwrap_or_default()
|
||||||
|
},
|
||||||
|
if !form.summary.is_empty() {
|
||||||
|
form.summary.clone()
|
||||||
|
} else {
|
||||||
|
user.summary.to_string()
|
||||||
|
},
|
||||||
)?;
|
)?;
|
||||||
Ok(Redirect::to(uri!(me)))
|
Ok(Redirect::to(uri!(me)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/@/<name>/delete")]
|
#[post("/@/<name>/delete")]
|
||||||
pub fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies, searcher: Searcher) -> Result<Redirect, ErrorPage> {
|
pub fn delete(
|
||||||
|
name: String,
|
||||||
|
conn: DbConn,
|
||||||
|
user: User,
|
||||||
|
mut cookies: Cookies,
|
||||||
|
searcher: Searcher,
|
||||||
|
) -> Result<Redirect, ErrorPage> {
|
||||||
let account = User::find_by_fqn(&*conn, &name)?;
|
let account = User::find_by_fqn(&*conn, &name)?;
|
||||||
if user.id == account.id {
|
if user.id == account.id {
|
||||||
account.delete(&*conn, &searcher)?;
|
account.delete(&*conn, &searcher)?;
|
||||||
|
@ -278,32 +349,25 @@ pub fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies, sear
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, FromForm, Validate)]
|
#[derive(Default, FromForm, Validate)]
|
||||||
#[validate(
|
#[validate(schema(
|
||||||
schema(
|
function = "passwords_match",
|
||||||
function = "passwords_match",
|
skip_on_field_errors = "false",
|
||||||
skip_on_field_errors = "false",
|
message = "Passwords are not matching"
|
||||||
message = "Passwords are not matching"
|
))]
|
||||||
)
|
|
||||||
)]
|
|
||||||
pub struct NewUserForm {
|
pub struct NewUserForm {
|
||||||
#[validate(length(min = "1", message = "Username can't be empty"),
|
#[validate(
|
||||||
custom( function = "validate_username", message = "User name is not allowed to contain any of < > & @ ' or \""))]
|
length(min = "1", message = "Username can't be empty"),
|
||||||
|
custom(
|
||||||
|
function = "validate_username",
|
||||||
|
message = "User name is not allowed to contain any of < > & @ ' or \""
|
||||||
|
)
|
||||||
|
)]
|
||||||
pub username: String,
|
pub username: String,
|
||||||
#[validate(email(message = "Invalid email"))]
|
#[validate(email(message = "Invalid email"))]
|
||||||
pub email: String,
|
pub email: String,
|
||||||
#[validate(
|
#[validate(length(min = "8", message = "Password should be at least 8 characters long"))]
|
||||||
length(
|
|
||||||
min = "8",
|
|
||||||
message = "Password should be at least 8 characters long"
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
pub password: String,
|
pub password: String,
|
||||||
#[validate(
|
#[validate(length(min = "8", message = "Password should be at least 8 characters long"))]
|
||||||
length(
|
|
||||||
min = "8",
|
|
||||||
message = "Password should be at least 8 characters long"
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
pub password_confirmation: String,
|
pub password_confirmation: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -325,17 +389,20 @@ pub fn validate_username(username: &str) -> Result<(), ValidationError> {
|
||||||
|
|
||||||
fn to_validation(_: Error) -> ValidationErrors {
|
fn to_validation(_: Error) -> ValidationErrors {
|
||||||
let mut errors = ValidationErrors::new();
|
let mut errors = ValidationErrors::new();
|
||||||
errors.add("", ValidationError {
|
errors.add(
|
||||||
code: Cow::from("server_error"),
|
"",
|
||||||
message: Some(Cow::from("An unknown error occured")),
|
ValidationError {
|
||||||
params: HashMap::new()
|
code: Cow::from("server_error"),
|
||||||
});
|
message: Some(Cow::from("An unknown error occured")),
|
||||||
|
params: HashMap::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
errors
|
errors
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/users/new", data = "<form>")]
|
#[post("/users/new", data = "<form>")]
|
||||||
pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Result<Redirect, Ructe> {
|
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)
|
.map(|i| i.open_registrations)
|
||||||
.unwrap_or(true)
|
.unwrap_or(true)
|
||||||
{
|
{
|
||||||
|
@ -355,13 +422,16 @@ pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Resul
|
||||||
"",
|
"",
|
||||||
form.email.to_string(),
|
form.email.to_string(),
|
||||||
User::hash_pass(&form.password).map_err(to_validation)?,
|
User::hash_pass(&form.password).map_err(to_validation)?,
|
||||||
).map_err(to_validation)?;
|
)
|
||||||
|
.map_err(to_validation)?;
|
||||||
Ok(Redirect::to(uri!(super::session::new: m = _)))
|
Ok(Redirect::to(uri!(super::session::new: m = _)))
|
||||||
})
|
})
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
render!(users::new(
|
render!(users::new(
|
||||||
&(&*conn, &intl.catalog, None),
|
&(&*conn, &intl.catalog, None),
|
||||||
Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true),
|
Instance::get_local(&*conn)
|
||||||
|
.map(|i| i.open_registrations)
|
||||||
|
.unwrap_or(true),
|
||||||
&form,
|
&form,
|
||||||
err
|
err
|
||||||
))
|
))
|
||||||
|
@ -395,21 +465,27 @@ pub fn inbox(
|
||||||
))))?;
|
))))?;
|
||||||
|
|
||||||
let actor = User::from_url(&conn, actor_id).expect("user::inbox: user error");
|
let actor = User::from_url(&conn, actor_id).expect("user::inbox: user error");
|
||||||
if !verify_http_headers(&actor, &headers.0, &sig).is_secure()
|
if !verify_http_headers(&actor, &headers.0, &sig).is_secure() && !act.clone().verify(&actor) {
|
||||||
&& !act.clone().verify(&actor)
|
|
||||||
{
|
|
||||||
// maybe we just know an old key?
|
// maybe we just know an old key?
|
||||||
actor.refetch(&conn).and_then(|_| User::get(&conn, actor.id))
|
actor
|
||||||
.and_then(|actor| if verify_http_headers(&actor, &headers.0, &sig).is_secure()
|
.refetch(&conn)
|
||||||
|| act.clone().verify(&actor)
|
.and_then(|_| User::get(&conn, actor.id))
|
||||||
{
|
.and_then(|actor| {
|
||||||
Ok(())
|
if verify_http_headers(&actor, &headers.0, &sig).is_secure()
|
||||||
} else {
|
|| act.clone().verify(&actor)
|
||||||
Err(Error::Signature)
|
{
|
||||||
})
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::Signature)
|
||||||
|
}
|
||||||
|
})
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
println!("Rejected invalid activity supposedly from {}, with headers {:?}", actor.username, headers.0);
|
println!(
|
||||||
status::BadRequest(Some("Invalid signature"))})?;
|
"Rejected invalid activity supposedly from {}, with headers {:?}",
|
||||||
|
actor.username, headers.0
|
||||||
|
);
|
||||||
|
status::BadRequest(Some("Invalid signature"))
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if Instance::is_blocked(&*conn, actor_id).map_err(|_| None)? {
|
if Instance::is_blocked(&*conn, actor_id).map_err(|_| None)? {
|
||||||
|
@ -432,18 +508,20 @@ pub fn ap_followers(
|
||||||
) -> Option<ActivityStream<OrderedCollection>> {
|
) -> Option<ActivityStream<OrderedCollection>> {
|
||||||
let user = User::find_by_fqn(&*conn, &name).ok()?;
|
let user = User::find_by_fqn(&*conn, &name).ok()?;
|
||||||
let followers = user
|
let followers = user
|
||||||
.get_followers(&*conn).ok()?
|
.get_followers(&*conn)
|
||||||
|
.ok()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|f| Id::new(f.ap_url))
|
.map(|f| Id::new(f.ap_url))
|
||||||
.collect::<Vec<Id>>();
|
.collect::<Vec<Id>>();
|
||||||
|
|
||||||
let mut coll = OrderedCollection::default();
|
let mut coll = OrderedCollection::default();
|
||||||
coll.object_props
|
coll.object_props
|
||||||
.set_id_string(user.followers_endpoint).ok()?;
|
.set_id_string(user.followers_endpoint)
|
||||||
|
.ok()?;
|
||||||
coll.collection_props
|
coll.collection_props
|
||||||
.set_total_items_u64(followers.len() as u64).ok()?;
|
.set_total_items_u64(followers.len() as u64)
|
||||||
coll.collection_props
|
.ok()?;
|
||||||
.set_items_link_vec(followers).ok()?;
|
coll.collection_props.set_items_link_vec(followers).ok()?;
|
||||||
Some(ActivityStream::new(coll))
|
Some(ActivityStream::new(coll))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -456,7 +534,8 @@ pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.compute_box("~", &name, "atom.xml"))
|
.compute_box("~", &name, "atom.xml"))
|
||||||
.entries(
|
.entries(
|
||||||
Post::get_recents_for_author(&*conn, &author, 15).ok()?
|
Post::get_recents_for_author(&*conn, &author, 15)
|
||||||
|
.ok()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|p| super::post_to_atom(p, &*conn))
|
.map(|p| super::post_to_atom(p, &*conn))
|
||||||
.collect::<Vec<Entry>>(),
|
.collect::<Vec<Entry>>(),
|
||||||
|
|
|
@ -3,32 +3,42 @@ use rocket::response::Content;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use webfinger::*;
|
use webfinger::*;
|
||||||
|
|
||||||
use plume_models::{BASE_URL, ap_url, db_conn::DbConn, blogs::Blog, users::User};
|
use plume_models::{ap_url, blogs::Blog, db_conn::DbConn, users::User, BASE_URL};
|
||||||
|
|
||||||
#[get("/.well-known/nodeinfo")]
|
#[get("/.well-known/nodeinfo")]
|
||||||
pub fn nodeinfo() -> Content<String> {
|
pub fn nodeinfo() -> Content<String> {
|
||||||
Content(ContentType::new("application", "jrd+json"), json!({
|
Content(
|
||||||
"links": [
|
ContentType::new("application", "jrd+json"),
|
||||||
{
|
json!({
|
||||||
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
"links": [
|
||||||
"href": ap_url(&format!("{domain}/nodeinfo/2.0", domain = BASE_URL.as_str()))
|
{
|
||||||
},
|
"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()))
|
{
|
||||||
}
|
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.1",
|
||||||
]
|
"href": ap_url(&format!("{domain}/nodeinfo/2.1", domain = BASE_URL.as_str()))
|
||||||
}).to_string())
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/.well-known/host-meta")]
|
#[get("/.well-known/host-meta")]
|
||||||
pub fn host_meta() -> String {
|
pub fn host_meta() -> String {
|
||||||
format!(r#"
|
format!(
|
||||||
|
r#"
|
||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
||||||
<Link rel="lrdd" type="application/xrd+xml" template="{url}"/>
|
<Link rel="lrdd" type="application/xrd+xml" template="{url}"/>
|
||||||
</XRD>
|
</XRD>
|
||||||
"#, url = ap_url(&format!("{domain}/.well-known/webfinger?resource={{uri}}", domain = BASE_URL.as_str())))
|
"#,
|
||||||
|
url = ap_url(&format!(
|
||||||
|
"{domain}/.well-known/webfinger?resource={{uri}}",
|
||||||
|
domain = BASE_URL.as_str()
|
||||||
|
))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WebfingerResolver;
|
struct WebfingerResolver;
|
||||||
|
@ -41,20 +51,31 @@ impl Resolver<DbConn> for WebfingerResolver {
|
||||||
fn find(acct: String, conn: DbConn) -> Result<Webfinger, ResolverError> {
|
fn find(acct: String, conn: DbConn) -> Result<Webfinger, ResolverError> {
|
||||||
User::find_by_fqn(&*conn, &acct)
|
User::find_by_fqn(&*conn, &acct)
|
||||||
.and_then(|usr| usr.webfinger(&*conn))
|
.and_then(|usr| usr.webfinger(&*conn))
|
||||||
.or_else(|_| Blog::find_by_fqn(&*conn, &acct)
|
.or_else(|_| {
|
||||||
.and_then(|blog| blog.webfinger(&*conn))
|
Blog::find_by_fqn(&*conn, &acct)
|
||||||
.or(Err(ResolverError::NotFound)))
|
.and_then(|blog| blog.webfinger(&*conn))
|
||||||
|
.or(Err(ResolverError::NotFound))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/.well-known/webfinger?<resource>")]
|
#[get("/.well-known/webfinger?<resource>")]
|
||||||
pub fn webfinger(resource: String, conn: DbConn) -> Content<String> {
|
pub fn webfinger(resource: String, conn: DbConn) -> Content<String> {
|
||||||
match WebfingerResolver::endpoint(resource, conn).and_then(|wf| serde_json::to_string(&wf).map_err(|_| ResolverError::NotFound)) {
|
match WebfingerResolver::endpoint(resource, conn)
|
||||||
|
.and_then(|wf| serde_json::to_string(&wf).map_err(|_| ResolverError::NotFound))
|
||||||
|
{
|
||||||
Ok(wf) => Content(ContentType::new("application", "jrd+json"), wf),
|
Ok(wf) => Content(ContentType::new("application", "jrd+json"), wf),
|
||||||
Err(err) => Content(ContentType::new("text", "plain"), String::from(match err {
|
Err(err) => Content(
|
||||||
ResolverError::InvalidResource => "Invalid resource. Make sure to request an acct: URI",
|
ContentType::new("text", "plain"),
|
||||||
ResolverError::NotFound => "Requested resource was not found",
|
String::from(match err {
|
||||||
ResolverError::WrongInstance => "This is not the instance of the requested resource"
|
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::hyper::header::{ETag, EntityTag};
|
||||||
|
use rocket::http::{Method, Status};
|
||||||
use rocket::request::Request;
|
use rocket::request::Request;
|
||||||
use rocket::response::{self, Response, Responder, content::Html as HtmlCt};
|
use rocket::response::{self, content::Html as HtmlCt, Responder, Response};
|
||||||
use rocket_i18n::Catalog;
|
use rocket_i18n::Catalog;
|
||||||
use std::collections::hash_map::DefaultHasher;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
use std::hash::Hasher;
|
use std::hash::Hasher;
|
||||||
|
@ -13,7 +13,7 @@ pub use askama_escape::escape;
|
||||||
|
|
||||||
pub static CACHE_NAME: &str = env!("CACHE_ID");
|
pub static CACHE_NAME: &str = env!("CACHE_ID");
|
||||||
|
|
||||||
pub type BaseContext<'a> = &'a(&'a Connection, &'a Catalog, Option<User>);
|
pub type BaseContext<'a> = &'a (&'a Connection, &'a Catalog, Option<User>);
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Ructe(pub Vec<u8>);
|
pub struct Ructe(pub Vec<u8>);
|
||||||
|
@ -27,7 +27,10 @@ impl<'r> Responder<'r> for Ructe {
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
hasher.write(&self.0);
|
hasher.write(&self.0);
|
||||||
let etag = format!("{:x}", hasher.finish());
|
let etag = format!("{:x}", hasher.finish());
|
||||||
if r.headers().get("If-None-Match").any(|s| s[1..s.len()-1] == etag) {
|
if r.headers()
|
||||||
|
.get("If-None-Match")
|
||||||
|
.any(|s| s[1..s.len() - 1] == etag)
|
||||||
|
{
|
||||||
Response::build()
|
Response::build()
|
||||||
.status(Status::NotModified)
|
.status(Status::NotModified)
|
||||||
.header(ETag(EntityTag::strong(etag)))
|
.header(ETag(EntityTag::strong(etag)))
|
||||||
|
@ -85,7 +88,13 @@ impl Size {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn avatar(conn: &Connection, user: &User, size: Size, pad: bool, catalog: &Catalog) -> Html<String> {
|
pub fn avatar(
|
||||||
|
conn: &Connection,
|
||||||
|
user: &User,
|
||||||
|
size: Size,
|
||||||
|
pad: bool,
|
||||||
|
catalog: &Catalog,
|
||||||
|
) -> Html<String> {
|
||||||
let name = escape(&user.name()).to_string();
|
let name = escape(&user.name()).to_string();
|
||||||
Html(format!(
|
Html(format!(
|
||||||
r#"<div class="avatar {size} {padded}"
|
r#"<div class="avatar {size} {padded}"
|
||||||
|
@ -120,49 +129,82 @@ pub fn tabs(links: &[(&str, String, bool)]) -> Html<String> {
|
||||||
pub fn paginate(catalog: &Catalog, page: i32, total: i32) -> Html<String> {
|
pub fn paginate(catalog: &Catalog, page: i32, total: i32) -> Html<String> {
|
||||||
paginate_param(catalog, page, total, None)
|
paginate_param(catalog, page, total, None)
|
||||||
}
|
}
|
||||||
pub fn paginate_param(catalog: &Catalog, page: i32, total: i32, param: Option<String>) -> Html<String> {
|
pub fn paginate_param(
|
||||||
|
catalog: &Catalog,
|
||||||
|
page: i32,
|
||||||
|
total: i32,
|
||||||
|
param: Option<String>,
|
||||||
|
) -> Html<String> {
|
||||||
let mut res = String::new();
|
let mut res = String::new();
|
||||||
let param = param.map(|mut p| {p.push('&'); p}).unwrap_or_default();
|
let param = param
|
||||||
|
.map(|mut p| {
|
||||||
|
p.push('&');
|
||||||
|
p
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
res.push_str(r#"<div class="pagination">"#);
|
res.push_str(r#"<div class="pagination">"#);
|
||||||
if page != 1 {
|
if page != 1 {
|
||||||
res.push_str(format!(r#"<a href="?{}page={}">{}</a>"#, param, page - 1, catalog.gettext("Previous page")).as_str());
|
res.push_str(
|
||||||
|
format!(
|
||||||
|
r#"<a href="?{}page={}">{}</a>"#,
|
||||||
|
param,
|
||||||
|
page - 1,
|
||||||
|
catalog.gettext("Previous page")
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if page < total {
|
if page < total {
|
||||||
res.push_str(format!(r#"<a href="?{}page={}">{}</a>"#, param, page + 1, catalog.gettext("Next page")).as_str());
|
res.push_str(
|
||||||
|
format!(
|
||||||
|
r#"<a href="?{}page={}">{}</a>"#,
|
||||||
|
param,
|
||||||
|
page + 1,
|
||||||
|
catalog.gettext("Next page")
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
res.push_str("</div>");
|
res.push_str("</div>");
|
||||||
Html(res)
|
Html(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn encode_query_param(param: &str) -> String {
|
pub fn encode_query_param(param: &str) -> String {
|
||||||
param.chars().map(|c| match c {
|
param
|
||||||
'+' => Ok("%2B"),
|
.chars()
|
||||||
' ' => Err('+'),
|
.map(|c| match c {
|
||||||
c => Err(c),
|
'+' => Ok("%2B"),
|
||||||
}).fold(String::new(), |mut s,r| {
|
' ' => Err('+'),
|
||||||
match r {
|
c => Err(c),
|
||||||
Ok(r) => s.push_str(r),
|
})
|
||||||
Err(r) => s.push(r),
|
.fold(String::new(), |mut s, r| {
|
||||||
};
|
match r {
|
||||||
s
|
Ok(r) => s.push_str(r),
|
||||||
})
|
Err(r) => s.push(r),
|
||||||
|
};
|
||||||
|
s
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! icon {
|
macro_rules! icon {
|
||||||
($name:expr) => {
|
($name:expr) => {
|
||||||
Html(concat!(r#"<svg class="feather"><use xlink:href="/static/images/feather-sprite.svg#"#, $name, "\"/></svg>"))
|
Html(concat!(
|
||||||
}
|
r#"<svg class="feather"><use xlink:href="/static/images/feather-sprite.svg#"#,
|
||||||
|
$name,
|
||||||
|
"\"/></svg>"
|
||||||
|
))
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! input {
|
macro_rules! input {
|
||||||
($catalog:expr, $name:tt ($kind:tt), $label:expr, $optional:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {
|
($catalog:expr, $name:tt ($kind:tt), $label:expr, $optional:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {{
|
||||||
{
|
use std::borrow::Cow;
|
||||||
use validator::ValidationErrorsKind;
|
use validator::ValidationErrorsKind;
|
||||||
use std::borrow::Cow;
|
let cat = $catalog;
|
||||||
let cat = $catalog;
|
|
||||||
|
|
||||||
Html(format!(r#"
|
Html(format!(
|
||||||
|
r#"
|
||||||
<label for="{name}">
|
<label for="{name}">
|
||||||
{label}
|
{label}
|
||||||
{optional}
|
{optional}
|
||||||
|
@ -171,52 +213,98 @@ macro_rules! input {
|
||||||
{error}
|
{error}
|
||||||
<input type="{kind}" id="{name}" name="{name}" value="{val}" {props}/>
|
<input type="{kind}" id="{name}" name="{name}" value="{val}" {props}/>
|
||||||
"#,
|
"#,
|
||||||
name = stringify!($name),
|
name = stringify!($name),
|
||||||
label = i18n!(cat, $label),
|
label = i18n!(cat, $label),
|
||||||
kind = stringify!($kind),
|
kind = stringify!($kind),
|
||||||
optional = if $optional { format!("<small>{}</small>", i18n!(cat, "Optional")) } else { String::new() },
|
optional = if $optional {
|
||||||
details = if $details.len() > 0 {
|
format!("<small>{}</small>", i18n!(cat, "Optional"))
|
||||||
format!("<small>{}</small>", i18n!(cat, $details))
|
} else {
|
||||||
} else {
|
String::new()
|
||||||
String::new()
|
},
|
||||||
},
|
details = if $details.len() > 0 {
|
||||||
error = if let Some(ValidationErrorsKind::Field(errs)) = $err.errors().get(stringify!($name)) {
|
format!("<small>{}</small>", i18n!(cat, $details))
|
||||||
format!(r#"<p class="error">{}</p>"#, errs[0].message.clone().unwrap_or(Cow::from("Unknown error")))
|
} else {
|
||||||
} else {
|
String::new()
|
||||||
String::new()
|
},
|
||||||
},
|
error = if let Some(ValidationErrorsKind::Field(errs)) =
|
||||||
val = escape(&$form.$name),
|
$err.errors().get(stringify!($name))
|
||||||
props = $props
|
{
|
||||||
))
|
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) => {
|
($catalog:expr, $name:tt (optional $kind:tt), $label:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {
|
||||||
input!($catalog, $name ($kind), $label, true, $details, $form, $err, $props)
|
input!(
|
||||||
|
$catalog,
|
||||||
|
$name($kind),
|
||||||
|
$label,
|
||||||
|
true,
|
||||||
|
$details,
|
||||||
|
$form,
|
||||||
|
$err,
|
||||||
|
$props
|
||||||
|
)
|
||||||
};
|
};
|
||||||
($catalog:expr, $name:tt (optional $kind:tt), $label:expr, $form:expr, $err:expr, $props:expr) => {
|
($catalog:expr, $name:tt (optional $kind:tt), $label:expr, $form:expr, $err:expr, $props:expr) => {
|
||||||
input!($catalog, $name ($kind), $label, true, "", $form, $err, $props)
|
input!(
|
||||||
|
$catalog,
|
||||||
|
$name($kind),
|
||||||
|
$label,
|
||||||
|
true,
|
||||||
|
"",
|
||||||
|
$form,
|
||||||
|
$err,
|
||||||
|
$props
|
||||||
|
)
|
||||||
};
|
};
|
||||||
($catalog:expr, $name:tt ($kind:tt), $label:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {
|
($catalog:expr, $name:tt ($kind:tt), $label:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {
|
||||||
input!($catalog, $name ($kind), $label, false, $details, $form, $err, $props)
|
input!(
|
||||||
|
$catalog,
|
||||||
|
$name($kind),
|
||||||
|
$label,
|
||||||
|
false,
|
||||||
|
$details,
|
||||||
|
$form,
|
||||||
|
$err,
|
||||||
|
$props
|
||||||
|
)
|
||||||
};
|
};
|
||||||
($catalog:expr, $name:tt ($kind:tt), $label:expr, $form:expr, $err:expr, $props:expr) => {
|
($catalog:expr, $name:tt ($kind:tt), $label:expr, $form:expr, $err:expr, $props:expr) => {
|
||||||
input!($catalog, $name ($kind), $label, false, "", $form, $err, $props)
|
input!(
|
||||||
|
$catalog,
|
||||||
|
$name($kind),
|
||||||
|
$label,
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
$form,
|
||||||
|
$err,
|
||||||
|
$props
|
||||||
|
)
|
||||||
};
|
};
|
||||||
($catalog:expr, $name:tt ($kind:tt), $label:expr, $form:expr, $err:expr) => {
|
($catalog:expr, $name:tt ($kind:tt), $label:expr, $form:expr, $err:expr) => {
|
||||||
input!($catalog, $name ($kind), $label, false, "", $form, $err, "")
|
input!($catalog, $name($kind), $label, false, "", $form, $err, "")
|
||||||
};
|
};
|
||||||
($catalog:expr, $name:tt ($kind:tt), $label:expr, $props:expr) => {
|
($catalog:expr, $name:tt ($kind:tt), $label:expr, $props:expr) => {{
|
||||||
{
|
let cat = $catalog;
|
||||||
let cat = $catalog;
|
Html(format!(
|
||||||
Html(format!(r#"
|
r#"
|
||||||
<label for="{name}">{label}</label>
|
<label for="{name}">{label}</label>
|
||||||
<input type="{kind}" id="{name}" name="{name}" {props}/>
|
<input type="{kind}" id="{name}" name="{name}" {props}/>
|
||||||
"#,
|
"#,
|
||||||
name = stringify!($name),
|
name = stringify!($name),
|
||||||
label = i18n!(cat, $label),
|
label = i18n!(cat, $label),
|
||||||
kind = stringify!($kind),
|
kind = stringify!($kind),
|
||||||
props = $props
|
props = $props
|
||||||
))
|
))
|
||||||
}
|
}};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue