mirror of
https://git.joinplu.me/Plume/Plume.git
synced 2024-06-03 04:09:24 +00:00
Compare commits
13 commits
115b5b31a4
...
9bd8f5272f
Author | SHA1 | Date | |
---|---|---|---|
9bd8f5272f | |||
39cd4f830d | |||
cd9cb311c7 | |||
83ed168f9c | |||
83c628d490 | |||
badff3f3cb | |||
ba00e36884 | |||
5ee84427bf | |||
f203dddae5 | |||
ba1eac9482 | |||
3dad83b179 | |||
4eab51b159 | |||
abf0b28fd4 |
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -166,12 +166,6 @@ dependencies = [
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anyhow"
|
|
||||||
version = "1.0.68"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arc-swap"
|
name = "arc-swap"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
|
@ -3341,7 +3335,6 @@ version = "0.7.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activitystreams",
|
"activitystreams",
|
||||||
"activitystreams-ext",
|
"activitystreams-ext",
|
||||||
"anyhow",
|
|
||||||
"array_tool",
|
"array_tool",
|
||||||
"askama_escape",
|
"askama_escape",
|
||||||
"assert-json-diff",
|
"assert-json-diff",
|
||||||
|
@ -3349,7 +3342,6 @@ dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"flume",
|
"flume",
|
||||||
"futures 0.3.25",
|
"futures 0.3.25",
|
||||||
"heck",
|
|
||||||
"hex",
|
"hex",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"openssl",
|
"openssl",
|
||||||
|
@ -3362,6 +3354,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"shrinkwraprs",
|
"shrinkwraprs",
|
||||||
"syntect",
|
"syntect",
|
||||||
|
"thiserror",
|
||||||
"tokio 1.24.1",
|
"tokio 1.24.1",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url 2.3.1",
|
"url 2.3.1",
|
||||||
|
@ -3407,6 +3400,7 @@ dependencies = [
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
"glob",
|
"glob",
|
||||||
"guid-create",
|
"guid-create",
|
||||||
|
"heck",
|
||||||
"itertools 0.10.5",
|
"itertools 0.10.5",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"ldap3",
|
"ldap3",
|
||||||
|
|
|
@ -25,8 +25,7 @@ url = "2.2.2"
|
||||||
flume = "0.10.13"
|
flume = "0.10.13"
|
||||||
tokio = { version = "1.19.2", features = ["full"] }
|
tokio = { version = "1.19.2", features = ["full"] }
|
||||||
futures = "0.3.25"
|
futures = "0.3.25"
|
||||||
heck = "0.4.0"
|
thiserror = "1.0.38"
|
||||||
anyhow = "1.0.68"
|
|
||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use ::anyhow::{self, anyhow};
|
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
actor::{ApActor, Group, Person},
|
actor::{ApActor, Group, Person},
|
||||||
base::{AnyBase, Base, Extends},
|
base::{AnyBase, Base, Extends},
|
||||||
|
@ -247,16 +246,24 @@ pub trait IntoId {
|
||||||
fn into_id(self) -> Id;
|
fn into_id(self) -> Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum PreferredUsernameError {
|
||||||
|
#[error("preferredUsername must be longer than 2 characters")]
|
||||||
|
TooShort,
|
||||||
|
#[error("Invaliad character at {character:?}: {position:?}")]
|
||||||
|
InvalidCharacter { character: char, position: usize },
|
||||||
|
}
|
||||||
|
|
||||||
#[repr(transparent)]
|
#[repr(transparent)]
|
||||||
#[derive(Shrinkwrap, PartialEq, Eq, Clone, Serialize, Deserialize, Debug)]
|
#[derive(Shrinkwrap, PartialEq, Eq, Clone, Serialize, Deserialize, Debug)]
|
||||||
pub struct PreferredUsername(String);
|
pub struct PreferredUsername(String);
|
||||||
|
|
||||||
// Mastodon allows only /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i for `preferredUsername`
|
// Mastodon allows only /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i for `preferredUsername`
|
||||||
impl PreferredUsername {
|
impl PreferredUsername {
|
||||||
fn validate(name: &str) -> anyhow::Result<()> {
|
fn validate(name: &str) -> std::result::Result<(), PreferredUsernameError> {
|
||||||
let len = name.len();
|
let len = name.len();
|
||||||
if len < 3 {
|
if len < 3 {
|
||||||
return Err(anyhow!("FQN must be longer than 2 characters"));
|
return Err(PreferredUsernameError::TooShort);
|
||||||
}
|
}
|
||||||
match name.chars().enumerate().find(|(pos, c)| {
|
match name.chars().enumerate().find(|(pos, c)| {
|
||||||
if pos == &0 || pos == &(len - 1) {
|
if pos == &0 || pos == &(len - 1) {
|
||||||
|
@ -268,7 +275,10 @@ impl PreferredUsername {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Some((pos, c)) => Err(anyhow!("Invaliad character at {}: {}", pos, c)),
|
Some((pos, c)) => Err(PreferredUsernameError::InvalidCharacter {
|
||||||
|
character: c,
|
||||||
|
position: pos,
|
||||||
|
}),
|
||||||
None => Ok(()),
|
None => Ok(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -280,7 +290,7 @@ impl PreferredUsername {
|
||||||
Self(name)
|
Self(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(name: String) -> anyhow::Result<Self> {
|
pub fn new(name: String) -> std::result::Result<Self, PreferredUsernameError> {
|
||||||
Self::validate(&name).map(|_| unsafe { Self::new_unchecked(name) })
|
Self::validate(&name).map(|_| unsafe { Self::new_unchecked(name) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -292,7 +302,7 @@ impl fmt::Display for PreferredUsername {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<String> for PreferredUsername {
|
impl TryFrom<String> for PreferredUsername {
|
||||||
type Error = anyhow::Error;
|
type Error = PreferredUsernameError;
|
||||||
|
|
||||||
fn try_from(name: String) -> std::result::Result<Self, Self::Error> {
|
fn try_from(name: String) -> std::result::Result<Self, Self::Error> {
|
||||||
Self::new(name)
|
Self::new(name)
|
||||||
|
@ -300,7 +310,7 @@ impl TryFrom<String> for PreferredUsername {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&str> for PreferredUsername {
|
impl TryFrom<&str> for PreferredUsername {
|
||||||
type Error = anyhow::Error;
|
type Error = PreferredUsernameError;
|
||||||
|
|
||||||
fn try_from(name: &str) -> std::result::Result<Self, Self::Error> {
|
fn try_from(name: &str) -> std::result::Result<Self, Self::Error> {
|
||||||
Self::new(name.to_owned())
|
Self::new(name.to_owned())
|
||||||
|
@ -320,7 +330,7 @@ impl AsRef<str> for PreferredUsername {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for PreferredUsername {
|
impl FromStr for PreferredUsername {
|
||||||
type Err = anyhow::Error;
|
type Err = PreferredUsernameError;
|
||||||
|
|
||||||
fn from_str(name: &str) -> std::result::Result<Self, Self::Err> {
|
fn from_str(name: &str) -> std::result::Result<Self, Self::Err> {
|
||||||
name.try_into()
|
name.try_into()
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
use activitystreams::iri_string::percent_encode::PercentEncodedForIri;
|
use activitystreams::iri_string::percent_encode::PercentEncodedForIri;
|
||||||
use heck::ToUpperCamelCase;
|
|
||||||
use openssl::rand::rand_bytes;
|
use openssl::rand::rand_bytes;
|
||||||
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag};
|
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag};
|
||||||
use regex_syntax::is_word_character;
|
use regex_syntax::is_word_character;
|
||||||
|
@ -25,13 +24,6 @@ pub fn iri_percent_encode_seg(segment: &str) -> String {
|
||||||
PercentEncodedForIri::from_path_segment(segment).to_string()
|
PercentEncodedForIri::from_path_segment(segment).to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn make_fqn(name: &str) -> String {
|
|
||||||
name.to_upper_camel_case()
|
|
||||||
.chars()
|
|
||||||
.filter(|c| c.is_ascii_alphanumeric())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum State {
|
enum State {
|
||||||
Mention,
|
Mention,
|
||||||
|
|
|
@ -35,6 +35,7 @@ once_cell = "1.12.0"
|
||||||
lettre = "0.9.6"
|
lettre = "0.9.6"
|
||||||
native-tls = "0.2.10"
|
native-tls = "0.2.10"
|
||||||
activitystreams = "=0.7.0-alpha.20"
|
activitystreams = "=0.7.0-alpha.20"
|
||||||
|
heck = "0.4.0"
|
||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
|
|
|
@ -25,7 +25,7 @@ use plume_common::{
|
||||||
sign, ActivityStream, ApSignature, CustomGroup, Id, IntoId, PublicKey, Source,
|
sign, ActivityStream, ApSignature, CustomGroup, Id, IntoId, PublicKey, Source,
|
||||||
SourceProperty, ToAsString, ToAsUri,
|
SourceProperty, ToAsString, ToAsUri,
|
||||||
},
|
},
|
||||||
utils::{iri_percent_encode_seg, make_fqn},
|
utils::iri_percent_encode_seg,
|
||||||
};
|
};
|
||||||
use webfinger::*;
|
use webfinger::*;
|
||||||
|
|
||||||
|
@ -89,11 +89,10 @@ impl Blog {
|
||||||
if inserted.fqn.to_string().is_empty() {
|
if inserted.fqn.to_string().is_empty() {
|
||||||
// This might not enough for some titles such as all-Japanese title,
|
// This might not enough for some titles such as all-Japanese title,
|
||||||
// but better than doing nothing.
|
// but better than doing nothing.
|
||||||
let username = make_fqn(&inserted.title);
|
|
||||||
if instance.local {
|
if instance.local {
|
||||||
inserted.fqn = Fqn::new_local(username)?;
|
inserted.fqn = Fqn::make_local(&inserted.title)?;
|
||||||
} else {
|
} else {
|
||||||
inserted.fqn = Fqn::new_remote(username, instance.public_domain)?;
|
inserted.fqn = Fqn::make_remote(&inserted.title, instance.public_domain)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -555,7 +554,7 @@ impl NewBlog {
|
||||||
let (pub_key, priv_key) = sign::gen_keypair();
|
let (pub_key, priv_key) = sign::gen_keypair();
|
||||||
Ok(NewBlog {
|
Ok(NewBlog {
|
||||||
actor_id,
|
actor_id,
|
||||||
fqn: Fqn::new_local(make_fqn(&title))?,
|
fqn: Fqn::make_local(&title)?,
|
||||||
title,
|
title,
|
||||||
summary,
|
summary,
|
||||||
instance_id,
|
instance_id,
|
||||||
|
|
|
@ -41,7 +41,7 @@ pub enum InvalidRocketConfig {
|
||||||
SecretKey,
|
SecretKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_rocket_config() -> Result<RocketConfig, InvalidRocketConfig> {
|
pub fn get_rocket_config() -> Result<RocketConfig, InvalidRocketConfig> {
|
||||||
let mut c = RocketConfig::active().map_err(|_| InvalidRocketConfig::Env)?;
|
let mut c = RocketConfig::active().map_err(|_| InvalidRocketConfig::Env)?;
|
||||||
|
|
||||||
let address = var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned());
|
let address = var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned());
|
||||||
|
|
|
@ -20,9 +20,11 @@ use activitystreams::iri_string;
|
||||||
use diesel::backend::Backend;
|
use diesel::backend::Backend;
|
||||||
use diesel::sql_types::Text;
|
use diesel::sql_types::Text;
|
||||||
use diesel::types::{FromSql, ToSql};
|
use diesel::types::{FromSql, ToSql};
|
||||||
|
use heck::ToUpperCamelCase;
|
||||||
pub use lettre;
|
pub use lettre;
|
||||||
pub use lettre::smtp;
|
pub use lettre::smtp;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
use plume_common::activity_pub::PreferredUsernameError;
|
||||||
use plume_common::activity_pub::{inbox::InboxError, request, sign, PreferredUsername};
|
use plume_common::activity_pub::{inbox::InboxError, request, sign, PreferredUsername};
|
||||||
use posts::PostEvent;
|
use posts::PostEvent;
|
||||||
use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder};
|
use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder};
|
||||||
|
@ -174,6 +176,13 @@ impl From<request::Error> for Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<PreferredUsernameError> for Error {
|
||||||
|
fn from(err: PreferredUsernameError) -> Error {
|
||||||
|
tracing::trace!("{:?}", err);
|
||||||
|
Error::InvalidValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
/// Adds a function to a model, that returns the first
|
/// Adds a function to a model, that returns the first
|
||||||
|
@ -305,7 +314,7 @@ macro_rules! last {
|
||||||
}
|
}
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
pub use config::CONFIG;
|
pub use config::{get_rocket_config, Config, SearchTokenizerConfig, CONFIG};
|
||||||
|
|
||||||
pub fn ap_url(url: &str) -> String {
|
pub fn ap_url(url: &str) -> String {
|
||||||
format!("https://{}", url)
|
format!("https://{}", url)
|
||||||
|
@ -348,16 +357,31 @@ pub enum Fqn {
|
||||||
impl Fqn {
|
impl Fqn {
|
||||||
pub fn new_local(username: String) -> Result<Self> {
|
pub fn new_local(username: String) -> Result<Self> {
|
||||||
Ok(Self::Local(
|
Ok(Self::Local(
|
||||||
PreferredUsername::new(username).map_err(|_| Error::InvalidValue)?,
|
PreferredUsername::new(username)?,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_remote(username: String, domain: String) -> Result<Self> {
|
pub fn new_remote(username: String, domain: String) -> Result<Self> {
|
||||||
Ok(Self::Remote(
|
Ok(Self::Remote(
|
||||||
PreferredUsername::new(username).map_err(|_| Error::InvalidValue)?,
|
PreferredUsername::new(username)?,
|
||||||
domain,
|
domain,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn make_local_string(base: &str) -> String {
|
||||||
|
base.to_upper_camel_case()
|
||||||
|
.chars()
|
||||||
|
.filter(|c| c.is_ascii_alphanumeric())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_local(base: &str) -> Result<Self> {
|
||||||
|
Self::new_local(Self::make_local_string(base))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_remote(base: &str, domain: String) -> Result<Self> {
|
||||||
|
Self::new_remote(Self::make_local_string(base), domain)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Fqn> for String {
|
impl From<&Fqn> for String {
|
||||||
|
|
|
@ -91,7 +91,7 @@ mod tests {
|
||||||
Connection as Conn, CONFIG,
|
Connection as Conn, CONFIG,
|
||||||
};
|
};
|
||||||
use diesel::r2d2::ConnectionManager;
|
use diesel::r2d2::ConnectionManager;
|
||||||
use plume_common::utils::{make_fqn, random_hex};
|
use plume_common::utils::random_hex;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
|
@ -198,7 +198,7 @@ mod tests {
|
||||||
ap_url: random_hex(),
|
ap_url: random_hex(),
|
||||||
inbox_url: random_hex(),
|
inbox_url: random_hex(),
|
||||||
outbox_url: random_hex(),
|
outbox_url: random_hex(),
|
||||||
fqn: Fqn::new_local(make_fqn(&title)).unwrap(),
|
fqn: Fqn::make_local(&title).unwrap(),
|
||||||
title,
|
title,
|
||||||
summary: Default::default(),
|
summary: Default::default(),
|
||||||
summary_html: Default::default(),
|
summary_html: Default::default(),
|
||||||
|
|
63
src/main.rs
63
src/main.rs
|
@ -16,7 +16,7 @@ use plume_models::{
|
||||||
migrations::IMPORTED_MIGRATIONS,
|
migrations::IMPORTED_MIGRATIONS,
|
||||||
remote_fetch_actor::RemoteFetchActor,
|
remote_fetch_actor::RemoteFetchActor,
|
||||||
search::{actor::SearchActor, Searcher as UnmanagedSearcher},
|
search::{actor::SearchActor, Searcher as UnmanagedSearcher},
|
||||||
Connection, CONFIG,
|
Config, Connection, CONFIG,
|
||||||
};
|
};
|
||||||
use rocket_csrf::CsrfFairingBuilder;
|
use rocket_csrf::CsrfFairingBuilder;
|
||||||
use scheduled_thread_pool::ScheduledThreadPool;
|
use scheduled_thread_pool::ScheduledThreadPool;
|
||||||
|
@ -47,12 +47,12 @@ include!(concat!(env!("OUT_DIR"), "/templates.rs"));
|
||||||
compile_i18n!();
|
compile_i18n!();
|
||||||
|
|
||||||
/// Initializes a database pool.
|
/// Initializes a database pool.
|
||||||
fn init_pool() -> Option<DbPool> {
|
fn init_pool(config: &Config) -> Option<DbPool> {
|
||||||
let manager = ConnectionManager::<Connection>::new(CONFIG.database_url.as_str());
|
let manager = ConnectionManager::<Connection>::new(CONFIG.database_url.as_str());
|
||||||
let mut builder = DbPool::builder()
|
let mut builder = DbPool::builder()
|
||||||
.connection_customizer(Box::new(PragmaForeignKey))
|
.connection_customizer(Box::new(PragmaForeignKey))
|
||||||
.min_idle(CONFIG.db_min_idle);
|
.min_idle(config.db_min_idle);
|
||||||
if let Some(max_size) = CONFIG.db_max_size {
|
if let Some(max_size) = config.db_max_size {
|
||||||
builder = builder.max_size(max_size);
|
builder = builder.max_size(max_size);
|
||||||
};
|
};
|
||||||
let pool = builder.build(manager).ok()?;
|
let pool = builder.build(manager).ok()?;
|
||||||
|
@ -63,28 +63,8 @@ fn init_pool() -> Option<DbPool> {
|
||||||
Some(pool)
|
Some(pool)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn init_rocket() -> rocket::Rocket {
|
pub(crate) fn init_rocket(config: &Config) -> rocket::Rocket {
|
||||||
match dotenv::dotenv() {
|
let dbpool = init_pool(config).expect("main: database pool initialization error");
|
||||||
Ok(path) => eprintln!("Configuration read from {}", path.display()),
|
|
||||||
Err(ref e) if e.not_found() => eprintln!("no .env was found"),
|
|
||||||
e => e.map(|_| ()).unwrap(),
|
|
||||||
}
|
|
||||||
tracing_subscriber::fmt::init();
|
|
||||||
|
|
||||||
App::new("Plume")
|
|
||||||
.bin_name("plume")
|
|
||||||
.version(env!("CARGO_PKG_VERSION"))
|
|
||||||
.about("Plume backend server")
|
|
||||||
.after_help(
|
|
||||||
r#"
|
|
||||||
The plume command should be run inside the directory
|
|
||||||
containing the `.env` configuration file and `static` directory.
|
|
||||||
See https://docs.joinplu.me/installation/config
|
|
||||||
and https://docs.joinplu.me/installation/init for more info.
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.get_matches();
|
|
||||||
let dbpool = init_pool().expect("main: database pool initialization error");
|
|
||||||
if IMPORTED_MIGRATIONS
|
if IMPORTED_MIGRATIONS
|
||||||
.is_pending(&dbpool.get().unwrap())
|
.is_pending(&dbpool.get().unwrap())
|
||||||
.unwrap_or(true)
|
.unwrap_or(true)
|
||||||
|
@ -104,8 +84,8 @@ Then try to restart Plume.
|
||||||
let workpool = ScheduledThreadPool::with_name("worker {}", num_cpus::get());
|
let workpool = ScheduledThreadPool::with_name("worker {}", num_cpus::get());
|
||||||
// we want a fast exit here, so
|
// we want a fast exit here, so
|
||||||
let searcher = Arc::new(UnmanagedSearcher::open_or_recreate(
|
let searcher = Arc::new(UnmanagedSearcher::open_or_recreate(
|
||||||
&CONFIG.search_index,
|
&config.search_index,
|
||||||
&CONFIG.search_tokenizers,
|
&config.search_tokenizers,
|
||||||
));
|
));
|
||||||
RemoteFetchActor::init(dbpool.clone());
|
RemoteFetchActor::init(dbpool.clone());
|
||||||
SearchActor::init(searcher.clone(), dbpool.clone());
|
SearchActor::init(searcher.clone(), dbpool.clone());
|
||||||
|
@ -125,12 +105,12 @@ Then try to restart Plume.
|
||||||
.expect("Error setting Ctrl-c handler");
|
.expect("Error setting Ctrl-c handler");
|
||||||
|
|
||||||
let mail = mail::init();
|
let mail = mail::init();
|
||||||
if mail.is_none() && CONFIG.rocket.as_ref().unwrap().environment.is_prod() {
|
if mail.is_none() && config.rocket.as_ref().unwrap().environment.is_prod() {
|
||||||
warn!("Warning: the email server is not configured (or not completely).");
|
warn!("Warning: the email server is not configured (or not completely).");
|
||||||
warn!("Please refer to the documentation to see how to configure it.");
|
warn!("Please refer to the documentation to see how to configure it.");
|
||||||
}
|
}
|
||||||
|
|
||||||
rocket::custom(CONFIG.rocket.clone().unwrap())
|
rocket::custom(config.rocket.clone().unwrap())
|
||||||
.mount(
|
.mount(
|
||||||
"/",
|
"/",
|
||||||
routes![
|
routes![
|
||||||
|
@ -280,7 +260,28 @@ Then try to restart Plume.
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let rocket = init_rocket();
|
match dotenv::dotenv() {
|
||||||
|
Ok(path) => eprintln!("Configuration read from {}", path.display()),
|
||||||
|
Err(ref e) if e.not_found() => eprintln!("no .env was found"),
|
||||||
|
e => e.map(|_| ()).unwrap(),
|
||||||
|
}
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
App::new("Plume")
|
||||||
|
.bin_name("plume")
|
||||||
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
|
.about("Plume backend server")
|
||||||
|
.after_help(
|
||||||
|
r#"
|
||||||
|
The plume command should be run inside the directory
|
||||||
|
containing the `.env` configuration file and `static` directory.
|
||||||
|
See https://docs.joinplu.me/installation/config
|
||||||
|
and https://docs.joinplu.me/installation/init for more info.
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.get_matches();
|
||||||
|
|
||||||
|
let rocket = init_rocket(&CONFIG);
|
||||||
|
|
||||||
#[cfg(feature = "test")]
|
#[cfg(feature = "test")]
|
||||||
let rocket = rocket.mount("/test", routes![test_routes::health,]);
|
let rocket = rocket.mount("/test", routes![test_routes::health,]);
|
||||||
|
|
|
@ -382,29 +382,53 @@ pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::env::var;
|
||||||
|
|
||||||
use super::valid_slug;
|
use super::valid_slug;
|
||||||
use crate::init_rocket;
|
use crate::init_rocket;
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
use plume_common::utils::{random_hex, make_fqn};
|
use plume_common::utils::random_hex;
|
||||||
use plume_models::{
|
use plume_models::{
|
||||||
blog_authors::{BlogAuthor, NewBlogAuthor},
|
blog_authors::{BlogAuthor, NewBlogAuthor},
|
||||||
blogs::{Blog, NewBlog},
|
blogs::{Blog, NewBlog},
|
||||||
db_conn::{DbConn, DbPool},
|
db_conn::{DbConn, DbPool},
|
||||||
|
get_rocket_config,
|
||||||
instance::{Instance, NewInstance},
|
instance::{Instance, NewInstance},
|
||||||
post_authors::{NewPostAuthor, PostAuthor},
|
post_authors::{NewPostAuthor, PostAuthor},
|
||||||
posts::{NewPost, Post},
|
posts::{NewPost, Post},
|
||||||
safe_string::SafeString,
|
safe_string::SafeString,
|
||||||
users::{NewUser, User, AUTH_COOKIE}, Fqn,
|
search::Searcher,
|
||||||
|
users::{NewUser, User, AUTH_COOKIE},
|
||||||
|
Config, Fqn, SearchTokenizerConfig,
|
||||||
};
|
};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::{Cookie, Cookies, SameSite},
|
http::{ContentType, Cookie, Cookies, SameSite, Status},
|
||||||
local::{Client, LocalRequest},
|
local::{Client, LocalRequest},
|
||||||
};
|
};
|
||||||
|
|
||||||
type Models = (Instance, User, Blog, Post);
|
type Models = (Instance, User, Blog, Post);
|
||||||
|
|
||||||
fn setup() -> (Client, Models) {
|
fn setup() -> (Client, Models) {
|
||||||
let rocket = init_rocket();
|
dotenv::from_path(".env.test").unwrap();
|
||||||
|
let config = Config {
|
||||||
|
base_url: var("BASE_URL").unwrap(),
|
||||||
|
db_name: "plume",
|
||||||
|
db_max_size: None,
|
||||||
|
db_min_idle: None,
|
||||||
|
signup: Default::default(),
|
||||||
|
database_url: var("DATABASE_URL").unwrap(),
|
||||||
|
search_index: format!("/tmp/plume_test-{}", random_hex()),
|
||||||
|
search_tokenizers: SearchTokenizerConfig::init(),
|
||||||
|
rocket: get_rocket_config(),
|
||||||
|
logo: Default::default(),
|
||||||
|
default_theme: Default::default(),
|
||||||
|
media_directory: format!("/tmp/plume_test-{}", random_hex()),
|
||||||
|
mail: None,
|
||||||
|
ldap: None,
|
||||||
|
proxy: None,
|
||||||
|
};
|
||||||
|
let _ = Searcher::create(&config.search_index, &config.search_tokenizers).unwrap();
|
||||||
|
let rocket = init_rocket(&config);
|
||||||
let client = Client::new(rocket).expect("valid rocket instance");
|
let client = Client::new(rocket).expect("valid rocket instance");
|
||||||
let dbpool = client.rocket().state::<DbPool>().unwrap();
|
let dbpool = client.rocket().state::<DbPool>().unwrap();
|
||||||
let conn = &DbConn(dbpool.get().unwrap());
|
let conn = &DbConn(dbpool.get().unwrap());
|
||||||
|
@ -413,7 +437,9 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn teardown((client, (instance, user, _blog, _post)): (&Client, Models)) {
|
fn teardown((client, (instance, user, _blog, _post)): (&Client, Models)) {
|
||||||
let dbpool = client.rocket().state::<DbPool>().unwrap();
|
let rocket = client.rocket();
|
||||||
|
|
||||||
|
let dbpool = rocket.state::<DbPool>().unwrap();
|
||||||
let conn = &DbConn(dbpool.get().unwrap());
|
let conn = &DbConn(dbpool.get().unwrap());
|
||||||
|
|
||||||
user.delete(conn).unwrap();
|
user.delete(conn).unwrap();
|
||||||
|
@ -494,13 +520,14 @@ mod tests {
|
||||||
inbox_url: random_hex(),
|
inbox_url: random_hex(),
|
||||||
outbox_url: random_hex(),
|
outbox_url: random_hex(),
|
||||||
followers_endpoint: random_hex(),
|
followers_endpoint: random_hex(),
|
||||||
|
fqn: random_hex(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let user = User::insert(conn, user).unwrap();
|
let user = User::insert(conn, user).unwrap();
|
||||||
let title = random_hex();
|
let title = random_hex();
|
||||||
let blog = NewBlog {
|
let blog = NewBlog {
|
||||||
instance_id: instance.id,
|
instance_id: instance.id,
|
||||||
fqn: Fqn::new_local(make_fqn(&title)).unwrap(),
|
fqn: Fqn::make_local(&title).unwrap(),
|
||||||
title,
|
title,
|
||||||
actor_id: random_hex(),
|
actor_id: random_hex(),
|
||||||
ap_url: random_hex(),
|
ap_url: random_hex(),
|
||||||
|
@ -513,7 +540,6 @@ mod tests {
|
||||||
icon_id: Default::default(),
|
icon_id: Default::default(),
|
||||||
banner_id: Default::default(),
|
banner_id: Default::default(),
|
||||||
theme: Default::default(),
|
theme: Default::default(),
|
||||||
|
|
||||||
};
|
};
|
||||||
let blog = Blog::insert(conn, blog).unwrap();
|
let blog = Blog::insert(conn, blog).unwrap();
|
||||||
BlogAuthor::insert(
|
BlogAuthor::insert(
|
||||||
|
@ -569,4 +595,38 @@ mod tests {
|
||||||
assert!(valid_slug("Blog Title").is_ok());
|
assert!(valid_slug("Blog Title").is_ok());
|
||||||
assert!(valid_slug("ブログ タイトル").is_ok());
|
assert!(valid_slug("ブログ タイトル").is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_blog_with_same_title_twice() {
|
||||||
|
let (client, (instance, user, blog, post)) = setup();
|
||||||
|
|
||||||
|
let new_path = uri!(super::new).to_string();
|
||||||
|
let request = client.get(new_path);
|
||||||
|
login(&request, &user);
|
||||||
|
let mut response = request.dispatch();
|
||||||
|
let body = response.body_string().unwrap();
|
||||||
|
let prefix = r#"<input type="hidden" name="csrf-token" value=""#;
|
||||||
|
let pos = body.find(prefix).unwrap();
|
||||||
|
let token = body[pos + prefix.len()..pos + prefix.len() + 123].to_string();
|
||||||
|
|
||||||
|
let create_path = uri!(super::create).to_string();
|
||||||
|
let response = client
|
||||||
|
.post(&create_path)
|
||||||
|
.body(format!("title=My%20Blog&csrf-token={}", &token))
|
||||||
|
.header(ContentType::Form)
|
||||||
|
.dispatch();
|
||||||
|
let first_attempt = response;
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(&create_path)
|
||||||
|
.body(format!("title=My%20Blog&csrf-token={}", &token))
|
||||||
|
.header(ContentType::Form)
|
||||||
|
.dispatch();
|
||||||
|
let second_attempt = response;
|
||||||
|
|
||||||
|
teardown((&client, (instance, user, blog, post)));
|
||||||
|
|
||||||
|
assert_eq!(first_attempt.status(), Status::SeeOther);
|
||||||
|
assert_eq!(second_attempt.status(), Status::SeeOther);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue