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

This commit is contained in:
Atul Bhosale
2019-03-20 22:26:17 +05:30
committed by Baptiste Gelez
parent 732f514da7
commit b945d1f602
58 changed files with 3160 additions and 2195 deletions
+1 -5
View File
@@ -3,11 +3,7 @@ use rocket_contrib::json::Json;
use serde_json;
use plume_api::apps::AppEndpoint;
use plume_models::{
Connection,
db_conn::DbConn,
apps::App,
};
use plume_models::{apps::App, db_conn::DbConn, Connection};
#[post("/apps", data = "<data>")]
pub fn create(conn: DbConn, data: Json<AppEndpoint>) -> Json<serde_json::Value> {
+15 -12
View File
@@ -1,10 +1,10 @@
use plume_models::{self, api_tokens::ApiToken};
use rocket::{
Outcome,
http::Status,
request::{self, FromRequest, Request}
request::{self, FromRequest, Request},
Outcome,
};
use std::marker::PhantomData;
use plume_models::{self, api_tokens::ApiToken};
// Actions
pub trait Action {
@@ -33,22 +33,25 @@ impl Scope for plume_models::posts::Post {
}
}
pub struct Authorization<A, S> (pub ApiToken, PhantomData<(A, S)>);
pub struct Authorization<A, S>(pub ApiToken, PhantomData<(A, S)>);
impl<'a, 'r, A, S> FromRequest<'a, 'r> for Authorization<A, S>
where A: Action,
S: Scope
where
A: Action,
S: Scope,
{
type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<Authorization<A, S>, ()> {
request.guard::<ApiToken>()
request
.guard::<ApiToken>()
.map_failure(|_| (Status::Unauthorized, ()))
.and_then(|token| if token.can(A::to_str(), S::to_str()) {
Outcome::Success(Authorization(token, PhantomData))
} else {
Outcome::Failure((Status::Unauthorized, ()))
.and_then(|token| {
if token.can(A::to_str(), S::to_str()) {
Outcome::Success(Authorization(token, PhantomData))
} else {
Outcome::Failure((Status::Unauthorized, ()))
}
})
}
}
+20 -17
View File
@@ -1,16 +1,13 @@
#![warn(clippy::too_many_arguments)]
use rocket::{response::{self, Responder}, request::{Form, Request}};
use rocket::{
request::{Form, Request},
response::{self, Responder},
};
use rocket_contrib::json::Json;
use serde_json;
use plume_common::utils::random_hex;
use plume_models::{
Error,
apps::App,
api_tokens::*,
db_conn::DbConn,
users::User,
};
use plume_models::{api_tokens::*, apps::App, db_conn::DbConn, users::User, Error};
#[derive(Debug)]
pub struct ApiError(Error);
@@ -26,13 +23,16 @@ impl<'r> Responder<'r> for ApiError {
match self.0 {
Error::NotFound => Json(json!({
"error": "Not found"
})).respond_to(req),
}))
.respond_to(req),
Error::Unauthorized => Json(json!({
"error": "You are not authorized to access this resource"
})).respond_to(req),
}))
.respond_to(req),
_ => Json(json!({
"error": "Server error"
})).respond_to(req)
}))
.respond_to(req),
}
}
}
@@ -52,12 +52,15 @@ pub fn oauth(query: Form<OAuthRequest>, conn: DbConn) -> Result<Json<serde_json:
if app.client_secret == query.client_secret {
if let Ok(user) = User::find_by_fqn(&*conn, &query.username) {
if user.auth(&query.password) {
let token = ApiToken::insert(&*conn, NewApiToken {
app_id: app.id,
user_id: user.id,
value: random_hex(),
scopes: query.scopes.clone(),
})?;
let token = ApiToken::insert(
&*conn,
NewApiToken {
app_id: app.id,
user_id: user.id,
value: random_hex(),
scopes: query.scopes.clone(),
},
)?;
Ok(Json(json!({
"token": token.value
})))
+61 -25
View File
@@ -5,43 +5,79 @@ use scheduled_thread_pool::ScheduledThreadPool;
use serde_json;
use serde_qs;
use api::authorization::*;
use plume_api::posts::PostEndpoint;
use plume_models::{
Connection,
db_conn::DbConn,
posts::Post,
search::Searcher as UnmanagedSearcher,
db_conn::DbConn, posts::Post, search::Searcher as UnmanagedSearcher, Connection,
};
use api::authorization::*;
use {Searcher, Worker};
#[get("/posts/<id>")]
pub fn get(id: i32, conn: DbConn, worker: Worker, auth: Option<Authorization<Read, Post>>, search: Searcher) -> Json<serde_json::Value> {
let post = <Post as Provider<(&Connection, &ScheduledThreadPool, &UnmanagedSearcher, Option<i32>)>>
::get(&(&*conn, &worker, &search, auth.map(|a| a.0.user_id)), id).ok();
pub fn get(
id: i32,
conn: DbConn,
worker: Worker,
auth: Option<Authorization<Read, Post>>,
search: Searcher,
) -> Json<serde_json::Value> {
let post = <Post as Provider<(
&Connection,
&ScheduledThreadPool,
&UnmanagedSearcher,
Option<i32>,
)>>::get(&(&*conn, &worker, &search, auth.map(|a| a.0.user_id)), id)
.ok();
Json(json!(post))
}
#[get("/posts")]
pub fn list(conn: DbConn, uri: &Origin, worker: Worker, auth: Option<Authorization<Read, Post>>, search: Searcher) -> Json<serde_json::Value> {
let query: PostEndpoint = serde_qs::from_str(uri.query().unwrap_or("")).expect("api::list: invalid query error");
let post = <Post as Provider<(&Connection, &ScheduledThreadPool, &UnmanagedSearcher, Option<i32>)>>
::list(&(&*conn, &worker, &search, auth.map(|a| a.0.user_id)), query);
pub fn list(
conn: DbConn,
uri: &Origin,
worker: Worker,
auth: Option<Authorization<Read, Post>>,
search: Searcher,
) -> Json<serde_json::Value> {
let query: PostEndpoint =
serde_qs::from_str(uri.query().unwrap_or("")).expect("api::list: invalid query error");
let post = <Post as Provider<(
&Connection,
&ScheduledThreadPool,
&UnmanagedSearcher,
Option<i32>,
)>>::list(
&(&*conn, &worker, &search, auth.map(|a| a.0.user_id)),
query,
);
Json(json!(post))
}
#[post("/posts", data = "<payload>")]
pub fn create(conn: DbConn, payload: Json<PostEndpoint>, worker: Worker, auth: Authorization<Write, Post>, search: Searcher) -> Json<serde_json::Value> {
let new_post = <Post as Provider<(&Connection, &ScheduledThreadPool, &UnmanagedSearcher, Option<i32>)>>
::create(&(&*conn, &worker, &search, Some(auth.0.user_id)), (*payload).clone());
Json(new_post.map(|p| json!(p)).unwrap_or_else(|e| json!({
"error": "Invalid data, couldn't create new post",
"details": match e {
ApiError::Fetch(msg) => msg,
ApiError::SerDe(msg) => msg,
ApiError::NotFound(msg) => msg,
ApiError::Authorization(msg) => msg,
}
})))
pub fn create(
conn: DbConn,
payload: Json<PostEndpoint>,
worker: Worker,
auth: Authorization<Write, Post>,
search: Searcher,
) -> Json<serde_json::Value> {
let new_post = <Post as Provider<(
&Connection,
&ScheduledThreadPool,
&UnmanagedSearcher,
Option<i32>,
)>>::create(
&(&*conn, &worker, &search, Some(auth.0.user_id)),
(*payload).clone(),
);
Json(new_post.map(|p| json!(p)).unwrap_or_else(|e| {
json!({
"error": "Invalid data, couldn't create new post",
"details": match e {
ApiError::Fetch(msg) => msg,
ApiError::SerDe(msg) => msg,
ApiError::NotFound(msg) => msg,
ApiError::Authorization(msg) => msg,
}
})
}))
}
+48 -35
View File
@@ -1,23 +1,10 @@
#![warn(clippy::too_many_arguments)]
use activitypub::{
activity::{
Announce,
Create,
Delete,
Follow as FollowAct,
Like,
Undo,
Update
},
object::Tombstone
activity::{Announce, Create, Delete, Follow as FollowAct, Like, Undo, Update},
object::Tombstone,
};
use failure::Error;
use rocket::{
data::*,
http::Status,
Outcome::*,
Request,
};
use rocket::{data::*, http::Status, Outcome::*, Request};
use rocket_contrib::json::*;
use serde::Deserialize;
use serde_json;
@@ -26,15 +13,21 @@ use std::io::Read;
use plume_common::activity_pub::{
inbox::{Deletable, FromActivity, InboxError, Notify},
Id,request::Digest,
request::Digest,
Id,
};
use plume_models::{
comments::Comment, follows::Follow, instance::Instance, likes, posts::Post, reshares::Reshare,
users::User, search::Searcher, Connection,
search::Searcher, users::User, Connection,
};
pub trait Inbox {
fn received(&self, conn: &Connection, searcher: &Searcher, act: serde_json::Value) -> Result<(), Error> {
fn received(
&self,
conn: &Connection,
searcher: &Searcher,
act: serde_json::Value,
) -> Result<(), Error> {
let actor_id = Id::new(act["actor"].as_str().unwrap_or_else(|| {
act["actor"]["id"]
.as_str()
@@ -66,7 +59,8 @@ pub trait Inbox {
.id_string()?,
actor_id.as_ref(),
&(conn, searcher),
).ok();
)
.ok();
Comment::delete_id(
&act.delete_props
.object_object::<Tombstone>()?
@@ -74,12 +68,14 @@ pub trait Inbox {
.id_string()?,
actor_id.as_ref(),
conn,
).ok();
)
.ok();
Ok(())
}
"Follow" => {
Follow::from_activity(conn, serde_json::from_value(act.clone())?, actor_id)
.and_then(|f| f.notify(conn)).expect("Inbox::received: follow from activity error");;
.and_then(|f| f.notify(conn))
.expect("Inbox::received: follow from activity error");;
Ok(())
}
"Like" => {
@@ -87,7 +83,8 @@ pub trait Inbox {
conn,
serde_json::from_value(act.clone())?,
actor_id,
).expect("Inbox::received: like from activity error");;
)
.expect("Inbox::received: like from activity error");;
Ok(())
}
"Undo" => {
@@ -102,7 +99,8 @@ pub trait Inbox {
.id_string()?,
actor_id.as_ref(),
conn,
).expect("Inbox::received: undo like fail");;
)
.expect("Inbox::received: undo like fail");;
Ok(())
}
"Announce" => {
@@ -113,7 +111,8 @@ pub trait Inbox {
.id_string()?,
actor_id.as_ref(),
conn,
).expect("Inbox::received: undo reshare fail");;
)
.expect("Inbox::received: undo reshare fail");;
Ok(())
}
"Follow" => {
@@ -124,21 +123,28 @@ pub trait Inbox {
.id_string()?,
actor_id.as_ref(),
conn,
).expect("Inbox::received: undo follow error");;
)
.expect("Inbox::received: undo follow error");;
Ok(())
}
_ => Err(InboxError::CantUndo)?,
}
} else {
let link = act.undo_props.object.as_str().expect("Inbox::received: undo doesn't contain a type and isn't Link");
let link =
act.undo_props.object.as_str().expect(
"Inbox::received: undo doesn't contain a type and isn't Link",
);
if let Ok(like) = likes::Like::find_by_ap_url(conn, link) {
likes::Like::delete_id(&like.ap_url, actor_id.as_ref(), conn).expect("Inbox::received: delete Like error");
likes::Like::delete_id(&like.ap_url, actor_id.as_ref(), conn)
.expect("Inbox::received: delete Like error");
Ok(())
} else if let Ok(reshare) = Reshare::find_by_ap_url(conn, link) {
Reshare::delete_id(&reshare.ap_url, actor_id.as_ref(), conn).expect("Inbox::received: delete Announce error");
Reshare::delete_id(&reshare.ap_url, actor_id.as_ref(), conn)
.expect("Inbox::received: delete Announce error");
Ok(())
} else if let Ok(follow) = Follow::find_by_ap_url(conn, link) {
Follow::delete_id(&follow.ap_url, actor_id.as_ref(), conn).expect("Inbox::received: delete Follow error");
Follow::delete_id(&follow.ap_url, actor_id.as_ref(), conn)
.expect("Inbox::received: delete Follow error");
Ok(())
} else {
Err(InboxError::NoType)?
@@ -147,7 +153,8 @@ pub trait Inbox {
}
"Update" => {
let act: Update = serde_json::from_value(act.clone())?;
Post::handle_update(conn, &act.update_props.object_object()?, searcher).expect("Inbox::received: post update error");
Post::handle_update(conn, &act.update_props.object_object()?, searcher)
.expect("Inbox::received: post update error");
Ok(())
}
_ => Err(InboxError::InvalidType)?,
@@ -169,19 +176,25 @@ impl<'a, T: Deserialize<'a>> FromData<'a> for SignedJson<T> {
type Owned = String;
type Borrowed = str;
fn transform(r: &Request, d: Data) -> Transform<rocket::data::Outcome<Self::Owned, Self::Error>> {
fn transform(
r: &Request,
d: Data,
) -> Transform<rocket::data::Outcome<Self::Owned, Self::Error>> {
let size_limit = r.limits().get("json").unwrap_or(JSON_LIMIT);
let mut s = String::with_capacity(512);
match d.open().take(size_limit).read_to_string(&mut s) {
Ok(_) => Transform::Borrowed(Success(s)),
Err(e) => Transform::Borrowed(Failure((Status::BadRequest, JsonError::Io(e))))
Err(e) => Transform::Borrowed(Failure((Status::BadRequest, JsonError::Io(e)))),
}
}
fn from_data(_: &Request, o: Transformed<'a, Self>) -> rocket::data::Outcome<Self, Self::Error> {
fn from_data(
_: &Request,
o: Transformed<'a, Self>,
) -> rocket::data::Outcome<Self, Self::Error> {
let string = o.borrowed()?;
match serde_json::from_str(&string) {
Ok(v) => Success(SignedJson(Digest::from_body(&string),Json(v))),
Ok(v) => Success(SignedJson(Digest::from_body(&string), Json(v))),
Err(e) => {
if e.is_data() {
Failure((Status::UnprocessableEntity, JsonError::Parse(string, e)))
+25 -10
View File
@@ -6,8 +6,8 @@ pub use self::mailer::*;
#[cfg(feature = "debug-mailer")]
mod mailer {
use lettre::{Transport, SendableEmail};
use std::{io::Read};
use lettre::{SendableEmail, Transport};
use std::io::Read;
pub struct DebugTransport;
@@ -18,11 +18,18 @@ mod mailer {
println!(
"{}: from=<{}> to=<{:?}>\n{:#?}",
email.message_id().to_string(),
email.envelope().from().map(ToString::to_string).unwrap_or_default(),
email
.envelope()
.from()
.map(ToString::to_string)
.unwrap_or_default(),
email.envelope().to().to_vec(),
{
let mut message = String::new();
email.message().read_to_string(&mut message).map_err(|_| ())?;
email
.message()
.read_to_string(&mut message)
.map_err(|_| ())?;
message
},
);
@@ -40,13 +47,12 @@ mod mailer {
#[cfg(not(feature = "debug-mailer"))]
mod mailer {
use lettre::{
SmtpTransport,
SmtpClient,
smtp::{
authentication::{Credentials, Mechanism},
extension::ClientId,
ConnectionReuseParameters,
},
SmtpClient, SmtpTransport,
};
use std::env;
@@ -57,7 +63,8 @@ mod mailer {
let helo_name = env::var("MAIL_HELO_NAME").unwrap_or_else(|_| "localhost".to_owned());
let username = env::var("MAIL_USER").ok()?;
let password = env::var("MAIL_PASSWORD").ok()?;
let mail = SmtpClient::new_simple(&server).unwrap()
let mail = SmtpClient::new_simple(&server)
.unwrap()
.hello_name(ClientId::Domain(helo_name))
.credentials(Credentials::new(username, password))
.smtp_utf8(true)
@@ -70,9 +77,17 @@ mod mailer {
pub fn build_mail(dest: String, subject: String, body: String) -> Option<Email> {
Email::builder()
.from(env::var("MAIL_ADDRESS")
.or_else(|_| Ok(format!("{}@{}", env::var("MAIL_USER")?, env::var("MAIL_SERVER")?)) as Result<_, env::VarError>)
.expect("Mail server is not correctly configured"))
.from(
env::var("MAIL_ADDRESS")
.or_else(|_| {
Ok(format!(
"{}@{}",
env::var("MAIL_USER")?,
env::var("MAIL_SERVER")?
)) as Result<_, env::VarError>
})
.expect("Mail server is not correctly configured"),
)
.to(dest)
.subject(subject)
.text(body)
+167 -132
View File
@@ -39,16 +39,13 @@ extern crate validator_derive;
extern crate webfinger;
use diesel::r2d2::ConnectionManager;
use rocket::{
Config, State,
config::Limits
};
use rocket_csrf::CsrfFairingBuilder;
use plume_models::{
DATABASE_URL, Connection, Error,
db_conn::{DbPool, PragmaForeignKey},
search::{Searcher as UnmanagedSearcher, SearcherError},
Connection, Error, DATABASE_URL,
};
use rocket::{config::Limits, Config, State};
use rocket_csrf::CsrfFairingBuilder;
use scheduled_thread_pool::ScheduledThreadPool;
use std::env;
use std::process::exit;
@@ -78,7 +75,8 @@ fn init_pool() -> Option<DbPool> {
let manager = ConnectionManager::<Connection>::new(DATABASE_URL.as_str());
DbPool::builder()
.connection_customizer(Box::new(PragmaForeignKey))
.build(manager).ok()
.build(manager)
.ok()
}
fn main() {
@@ -89,37 +87,58 @@ fn main() {
let searcher = match UnmanagedSearcher::open(&"search_index") {
Err(Error::Search(e)) => match e {
SearcherError::WriteLockAcquisitionError => panic!(
r#"Your search index is locked. Plume can't start. To fix this issue
r#"Your search index is locked. Plume can't start. To fix this issue
make sure no other Plume instance is started, and run:
plm search unlock
Then try to restart Plume.
"#),
e => Err(e).unwrap()
"#
),
e => Err(e).unwrap(),
},
Err(_) => panic!("Unexpected error while opening search index"),
Ok(s) => Arc::new(s)
Ok(s) => Arc::new(s),
};
let commiter = searcher.clone();
workpool.execute_with_fixed_delay(Duration::from_secs(5), Duration::from_secs(60*30), move || commiter.commit());
workpool.execute_with_fixed_delay(
Duration::from_secs(5),
Duration::from_secs(60 * 30),
move || commiter.commit(),
);
let search_unlocker = searcher.clone();
ctrlc::set_handler(move || {
search_unlocker.drop_writer();
exit(0);
}).expect("Error setting Ctrl-c handler");
})
.expect("Error setting Ctrl-c handler");
let mut config = Config::active().unwrap();
config.set_address(env::var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned())).unwrap();
config.set_port(env::var("ROCKET_PORT").ok().map(|s| s.parse::<u16>().unwrap()).unwrap_or(7878));
config
.set_address(env::var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned()))
.unwrap();
config.set_port(
env::var("ROCKET_PORT")
.ok()
.map(|s| s.parse::<u16>().unwrap())
.unwrap_or(7878),
);
let _ = env::var("ROCKET_SECRET_KEY").map(|k| config.set_secret_key(k).unwrap());
let form_size = &env::var("FORM_SIZE").unwrap_or_else(|_| "32".to_owned()).parse::<u64>().unwrap();
let activity_size = &env::var("ACTIVITY_SIZE").unwrap_or_else(|_| "1024".to_owned()).parse::<u64>().unwrap();
config.set_limits(Limits::new()
.limit("forms", form_size * 1024)
.limit("json", activity_size * 1024));
let form_size = &env::var("FORM_SIZE")
.unwrap_or_else(|_| "32".to_owned())
.parse::<u64>()
.unwrap();
let activity_size = &env::var("ACTIVITY_SIZE")
.unwrap_or_else(|_| "1024".to_owned())
.parse::<u64>()
.unwrap();
config.set_limits(
Limits::new()
.limit("forms", form_size * 1024)
.limit("json", activity_size * 1024),
);
let mail = mail::init();
if mail.is_none() && config.environment.is_prod() {
@@ -128,110 +147,100 @@ Then try to restart Plume.
}
rocket::custom(config)
.mount("/", routes![
routes::blogs::details,
routes::blogs::activity_details,
routes::blogs::outbox,
routes::blogs::new,
routes::blogs::new_auth,
routes::blogs::create,
routes::blogs::delete,
routes::blogs::atom_feed,
routes::comments::create,
routes::comments::delete,
routes::comments::activity_pub,
routes::instance::index,
routes::instance::local,
routes::instance::feed,
routes::instance::federated,
routes::instance::admin,
routes::instance::admin_instances,
routes::instance::admin_users,
routes::instance::ban,
routes::instance::toggle_block,
routes::instance::update_settings,
routes::instance::shared_inbox,
routes::instance::nodeinfo,
routes::instance::about,
routes::instance::web_manifest,
routes::likes::create,
routes::likes::create_auth,
routes::medias::list,
routes::medias::new,
routes::medias::upload,
routes::medias::details,
routes::medias::delete,
routes::medias::set_avatar,
routes::notifications::notifications,
routes::notifications::notifications_auth,
routes::posts::details,
routes::posts::activity_details,
routes::posts::edit,
routes::posts::update,
routes::posts::new,
routes::posts::new_auth,
routes::posts::create,
routes::posts::delete,
routes::reshares::create,
routes::reshares::create_auth,
routes::search::search,
routes::session::new,
routes::session::create,
routes::session::delete,
routes::session::password_reset_request_form,
routes::session::password_reset_request,
routes::session::password_reset_form,
routes::session::password_reset,
routes::plume_static_files,
routes::static_files,
routes::tags::tag,
routes::user::me,
routes::user::details,
routes::user::dashboard,
routes::user::dashboard_auth,
routes::user::followers,
routes::user::followed,
routes::user::edit,
routes::user::edit_auth,
routes::user::update,
routes::user::delete,
routes::user::follow,
routes::user::follow_auth,
routes::user::activity_details,
routes::user::outbox,
routes::user::inbox,
routes::user::ap_followers,
routes::user::new,
routes::user::create,
routes::user::atom_feed,
routes::well_known::host_meta,
routes::well_known::nodeinfo,
routes::well_known::webfinger,
routes::errors::csrf_violation
])
.mount("/api/v1", routes![
api::oauth,
api::apps::create,
api::posts::get,
api::posts::list,
api::posts::create,
])
.mount(
"/",
routes![
routes::blogs::details,
routes::blogs::activity_details,
routes::blogs::outbox,
routes::blogs::new,
routes::blogs::new_auth,
routes::blogs::create,
routes::blogs::delete,
routes::blogs::atom_feed,
routes::comments::create,
routes::comments::delete,
routes::comments::activity_pub,
routes::instance::index,
routes::instance::local,
routes::instance::feed,
routes::instance::federated,
routes::instance::admin,
routes::instance::admin_instances,
routes::instance::admin_users,
routes::instance::ban,
routes::instance::toggle_block,
routes::instance::update_settings,
routes::instance::shared_inbox,
routes::instance::nodeinfo,
routes::instance::about,
routes::instance::web_manifest,
routes::likes::create,
routes::likes::create_auth,
routes::medias::list,
routes::medias::new,
routes::medias::upload,
routes::medias::details,
routes::medias::delete,
routes::medias::set_avatar,
routes::notifications::notifications,
routes::notifications::notifications_auth,
routes::posts::details,
routes::posts::activity_details,
routes::posts::edit,
routes::posts::update,
routes::posts::new,
routes::posts::new_auth,
routes::posts::create,
routes::posts::delete,
routes::reshares::create,
routes::reshares::create_auth,
routes::search::search,
routes::session::new,
routes::session::create,
routes::session::delete,
routes::session::password_reset_request_form,
routes::session::password_reset_request,
routes::session::password_reset_form,
routes::session::password_reset,
routes::plume_static_files,
routes::static_files,
routes::tags::tag,
routes::user::me,
routes::user::details,
routes::user::dashboard,
routes::user::dashboard_auth,
routes::user::followers,
routes::user::followed,
routes::user::edit,
routes::user::edit_auth,
routes::user::update,
routes::user::delete,
routes::user::follow,
routes::user::follow_auth,
routes::user::activity_details,
routes::user::outbox,
routes::user::inbox,
routes::user::ap_followers,
routes::user::new,
routes::user::create,
routes::user::atom_feed,
routes::well_known::host_meta,
routes::well_known::nodeinfo,
routes::well_known::webfinger,
routes::errors::csrf_violation
],
)
.mount(
"/api/v1",
routes![
api::oauth,
api::apps::create,
api::posts::get,
api::posts::list,
api::posts::create,
],
)
.register(catchers![
routes::errors::not_found,
routes::errors::unprocessable_entity,
@@ -243,15 +252,41 @@ Then try to restart Plume.
.manage(workpool)
.manage(searcher)
.manage(include_i18n!())
.attach(CsrfFairingBuilder::new()
.set_default_target("/csrf-violation?target=<uri>".to_owned(), rocket::http::Method::Post)
.attach(
CsrfFairingBuilder::new()
.set_default_target(
"/csrf-violation?target=<uri>".to_owned(),
rocket::http::Method::Post,
)
.add_exceptions(vec![
("/inbox".to_owned(), "/inbox".to_owned(), rocket::http::Method::Post),
("/@/<name>/inbox".to_owned(), "/@/<name>/inbox".to_owned(), rocket::http::Method::Post),
("/login".to_owned(), "/login".to_owned(), rocket::http::Method::Post),
("/users/new".to_owned(), "/users/new".to_owned(), rocket::http::Method::Post),
("/api/<path..>".to_owned(), "/api/<path..>".to_owned(), rocket::http::Method::Post)
(
"/inbox".to_owned(),
"/inbox".to_owned(),
rocket::http::Method::Post,
),
(
"/@/<name>/inbox".to_owned(),
"/@/<name>/inbox".to_owned(),
rocket::http::Method::Post,
),
(
"/login".to_owned(),
"/login".to_owned(),
rocket::http::Method::Post,
),
(
"/users/new".to_owned(),
"/users/new".to_owned(),
rocket::http::Method::Post,
),
(
"/api/<path..>".to_owned(),
"/api/<path..>".to_owned(),
rocket::http::Method::Post,
),
])
.finalize().expect("main: csrf fairing creation error"))
.finalize()
.expect("main: csrf fairing creation error"),
)
.launch();
}
+71 -42
View File
@@ -3,22 +3,16 @@ use atom_syndication::{Entry, FeedBuilder};
use rocket::{
http::ContentType,
request::LenientForm,
response::{Redirect, Flash, content::Content}
response::{content::Content, Flash, Redirect},
};
use rocket_i18n::I18n;
use std::{collections::HashMap, borrow::Cow};
use std::{borrow::Cow, collections::HashMap};
use validator::{Validate, ValidationError, ValidationErrors};
use plume_common::activity_pub::{ActivityStream, ApRequest};
use plume_common::utils;
use plume_models::{
blog_authors::*,
blogs::*,
db_conn::DbConn,
instance::Instance,
posts::Post,
};
use routes::{Page, PlumeRocket, errors::ErrorPage};
use plume_models::{blog_authors::*, blogs::*, db_conn::DbConn, instance::Instance, posts::Post};
use routes::{errors::ErrorPage, Page, PlumeRocket};
use template_utils::Ructe;
#[get("/~/<name>?<page>", rank = 2)]
@@ -39,13 +33,18 @@ pub fn details(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result
articles_count,
page.0,
Page::total(articles_count as i32),
user.and_then(|x| x.is_author_in(&*conn, &blog).ok()).unwrap_or(false),
user.and_then(|x| x.is_author_in(&*conn, &blog).ok())
.unwrap_or(false),
posts
)))
}
#[get("/~/<name>", rank = 1)]
pub fn activity_details(name: String, conn: DbConn, _ap: ApRequest) -> Option<ActivityStream<CustomGroup>> {
pub fn activity_details(
name: String,
conn: DbConn,
_ap: ApRequest,
) -> Option<ActivityStream<CustomGroup>> {
let blog = Blog::find_by_fqn(&*conn, &name).ok()?;
Some(ActivityStream::new(blog.to_activity(&*conn).ok()?))
}
@@ -64,10 +63,13 @@ pub fn new(rockets: PlumeRocket) -> Ructe {
}
#[get("/blogs/new", rank = 2)]
pub fn new_auth(i18n: I18n) -> Flash<Redirect>{
pub fn new_auth(i18n: I18n) -> Flash<Redirect> {
utils::requires_login(
&i18n!(i18n.catalog, "You need to be logged in order to create a new blog"),
uri!(new)
&i18n!(
i18n.catalog,
"You need to be logged in order to create a new blog"
),
uri!(new),
)
}
@@ -95,29 +97,43 @@ pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> Result<Re
let mut errors = match form.validate() {
Ok(_) => ValidationErrors::new(),
Err(e) => e
Err(e) => e,
};
if Blog::find_by_fqn(&*conn, &slug).is_ok() {
errors.add("title", ValidationError {
code: Cow::from("existing_slug"),
message: Some(Cow::from("A blog with the same name already exists.")),
params: HashMap::new()
});
errors.add(
"title",
ValidationError {
code: Cow::from("existing_slug"),
message: Some(Cow::from("A blog with the same name already exists.")),
params: HashMap::new(),
},
);
}
if errors.is_empty() {
let blog = Blog::insert(&*conn, NewBlog::new_local(
slug.clone(),
form.title.to_string(),
String::from(""),
Instance::get_local(&*conn).expect("blog::create: instance error").id
).expect("blog::create: new local error")).expect("blog::create: error");
let blog = Blog::insert(
&*conn,
NewBlog::new_local(
slug.clone(),
form.title.to_string(),
String::from(""),
Instance::get_local(&*conn)
.expect("blog::create: instance error")
.id,
)
.expect("blog::create: new local error"),
)
.expect("blog::create: error");
BlogAuthor::insert(&*conn, NewBlogAuthor {
blog_id: blog.id,
author_id: user.id,
is_owner: true
}).expect("blog::create: author error");
BlogAuthor::insert(
&*conn,
NewBlogAuthor {
blog_id: blog.id,
author_id: user.id,
is_owner: true,
},
)
.expect("blog::create: author error");
Ok(Redirect::to(uri!(details: name = slug.clone(), page = _)))
} else {
@@ -130,15 +146,20 @@ pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> Result<Re
}
#[post("/~/<name>/delete")]
pub fn delete(name: String, rockets: PlumeRocket) -> Result<Redirect, Ructe>{
pub fn delete(name: String, rockets: PlumeRocket) -> Result<Redirect, Ructe> {
let conn = rockets.conn;
let blog = Blog::find_by_fqn(&*conn, &name).expect("blog::delete: blog not found");
let user = rockets.user;
let intl = rockets.intl;
let searcher = rockets.searcher;
if user.clone().and_then(|u| u.is_author_in(&*conn, &blog).ok()).unwrap_or(false) {
blog.delete(&conn, &searcher).expect("blog::expect: deletion error");
if user
.clone()
.and_then(|u| u.is_author_in(&*conn, &blog).ok())
.unwrap_or(false)
{
blog.delete(&conn, &searcher)
.expect("blog::expect: deletion error");
Ok(Redirect::to(uri!(super::instance::index)))
} else {
// TODO actually return 403 error code
@@ -160,12 +181,20 @@ pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
let blog = Blog::find_by_fqn(&*conn, &name).ok()?;
let feed = FeedBuilder::default()
.title(blog.title.clone())
.id(Instance::get_local(&*conn).ok()?
.id(Instance::get_local(&*conn)
.ok()?
.compute_box("~", &name, "atom.xml"))
.entries(Post::get_recents_for_blog(&*conn, &blog, 15).ok()?
.into_iter()
.map(|p| super::post_to_atom(p, &*conn))
.collect::<Vec<Entry>>())
.build().ok()?;
Some(Content(ContentType::new("application", "atom+xml"), feed.to_string()))
.entries(
Post::get_recents_for_blog(&*conn, &blog, 15)
.ok()?
.into_iter()
.map(|p| super::post_to_atom(p, &*conn))
.collect::<Vec<Entry>>(),
)
.build()
.ok()?;
Some(Content(
ContentType::new("application", "atom+xml"),
feed.to_string(),
))
}
+92 -49
View File
@@ -1,29 +1,21 @@
use activitypub::object::Note;
use rocket::{
request::LenientForm,
response::Redirect
};
use rocket::{request::LenientForm, response::Redirect};
use rocket_i18n::I18n;
use validator::Validate;
use template_utils::Ructe;
use validator::Validate;
use std::time::Duration;
use plume_common::{utils, activity_pub::{broadcast, ApRequest,
ActivityStream, inbox::Deletable}};
use plume_models::{
blogs::Blog,
comments::*,
db_conn::DbConn,
instance::Instance,
mentions::Mention,
posts::Post,
safe_string::SafeString,
tags::Tag,
users::User
use plume_common::{
activity_pub::{broadcast, inbox::Deletable, ActivityStream, ApRequest},
utils,
};
use plume_models::{
blogs::Blog, comments::*, db_conn::DbConn, instance::Instance, mentions::Mention, posts::Post,
safe_string::SafeString, tags::Tag, users::User,
};
use Worker;
use routes::errors::ErrorPage;
use Worker;
#[derive(Default, FromForm, Debug, Validate)]
pub struct NewCommentForm {
@@ -34,37 +26,54 @@ pub struct NewCommentForm {
}
#[post("/~/<blog_name>/<slug>/comment", data = "<form>")]
pub fn create(blog_name: String, slug: String, form: LenientForm<NewCommentForm>, user: User, conn: DbConn, worker: Worker, intl: I18n)
-> Result<Redirect, Ructe> {
pub fn create(
blog_name: String,
slug: String,
form: LenientForm<NewCommentForm>,
user: User,
conn: DbConn,
worker: Worker,
intl: I18n,
) -> Result<Redirect, Ructe> {
let blog = Blog::find_by_fqn(&*conn, &blog_name).expect("comments::create: blog error");
let post = Post::find_by_slug(&*conn, &slug, blog.id).expect("comments::create: post error");
form.validate()
.map(|_| {
let (html, mentions, _hashtags) = utils::md_to_html(
form.content.as_ref(),
&Instance::get_local(&conn).expect("comments::create: local instance error").public_domain
&Instance::get_local(&conn)
.expect("comments::create: local instance error")
.public_domain,
);
let comm = Comment::insert(&*conn, NewComment {
content: SafeString::new(html.as_ref()),
in_response_to_id: form.responding_to,
post_id: post.id,
author_id: user.id,
ap_url: None,
sensitive: !form.warning.is_empty(),
spoiler_text: form.warning.clone(),
public_visibility: true
}).expect("comments::create: insert error");
let new_comment = comm.create_activity(&*conn).expect("comments::create: activity error");
let comm = Comment::insert(
&*conn,
NewComment {
content: SafeString::new(html.as_ref()),
in_response_to_id: form.responding_to,
post_id: post.id,
author_id: user.id,
ap_url: None,
sensitive: !form.warning.is_empty(),
spoiler_text: form.warning.clone(),
public_visibility: true,
},
)
.expect("comments::create: insert error");
let new_comment = comm
.create_activity(&*conn)
.expect("comments::create: activity error");
// save mentions
for ment in mentions {
Mention::from_activity(
&*conn,
&Mention::build_activity(&*conn, &ment).expect("comments::create: build mention error"),
&Mention::build_activity(&*conn, &ment)
.expect("comments::create: build mention error"),
post.id,
true,
true
).expect("comments::create: mention save error");
true,
)
.expect("comments::create: mention save error");
}
// federate
@@ -72,13 +81,18 @@ pub fn create(blog_name: String, slug: String, form: LenientForm<NewCommentForm>
let user_clone = user.clone();
worker.execute(move || broadcast(&user_clone, new_comment, dest));
Redirect::to(uri!(super::posts::details: blog = blog_name, slug = slug, responding_to = _))
Redirect::to(
uri!(super::posts::details: blog = blog_name, slug = slug, responding_to = _),
)
})
.map_err(|errors| {
// TODO: de-duplicate this code
let comments = CommentTree::from_post(&*conn, &post, Some(&user)).expect("comments::create: comments error");
let comments = CommentTree::from_post(&*conn, &post, Some(&user))
.expect("comments::create: comments error");
let previous = form.responding_to.and_then(|r| Comment::get(&*conn, r).ok());
let previous = form
.responding_to
.and_then(|r| Comment::get(&*conn, r).ok());
render!(posts::details(
&(&*conn, &intl.catalog, Some(user.clone())),
@@ -89,33 +103,62 @@ pub fn create(blog_name: String, slug: String, form: LenientForm<NewCommentForm>
Tag::for_post(&*conn, post.id).expect("comments::create: tags error"),
comments,
previous,
post.count_likes(&*conn).expect("comments::create: count likes error"),
post.count_reshares(&*conn).expect("comments::create: count reshares error"),
user.has_liked(&*conn, &post).expect("comments::create: liked error"),
user.has_reshared(&*conn, &post).expect("comments::create: reshared error"),
user.is_following(&*conn, post.get_authors(&*conn).expect("comments::create: authors error")[0].id)
.expect("comments::create: following error"),
post.get_authors(&*conn).expect("comments::create: authors error")[0].clone()
post.count_likes(&*conn)
.expect("comments::create: count likes error"),
post.count_reshares(&*conn)
.expect("comments::create: count reshares error"),
user.has_liked(&*conn, &post)
.expect("comments::create: liked error"),
user.has_reshared(&*conn, &post)
.expect("comments::create: reshared error"),
user.is_following(
&*conn,
post.get_authors(&*conn)
.expect("comments::create: authors error")[0]
.id
)
.expect("comments::create: following error"),
post.get_authors(&*conn)
.expect("comments::create: authors error")[0]
.clone()
))
})
}
#[post("/~/<blog>/<slug>/comment/<id>/delete")]
pub fn delete(blog: String, slug: String, id: i32, user: User, conn: DbConn, worker: Worker) -> Result<Redirect, ErrorPage> {
pub fn delete(
blog: String,
slug: String,
id: i32,
user: User,
conn: DbConn,
worker: Worker,
) -> Result<Redirect, ErrorPage> {
if let Ok(comment) = Comment::get(&*conn, id) {
if comment.author_id == user.id {
let dest = User::one_by_instance(&*conn)?;
let delete_activity = comment.delete(&*conn)?;
let user_c = user.clone();
worker.execute(move || broadcast(&user_c, delete_activity, dest));
worker.execute_after(Duration::from_secs(10*60), move || {user.rotate_keypair(&conn).expect("Failed to rotate keypair");});
worker.execute_after(Duration::from_secs(10 * 60), move || {
user.rotate_keypair(&conn)
.expect("Failed to rotate keypair");
});
}
}
Ok(Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _)))
Ok(Redirect::to(
uri!(super::posts::details: blog = blog, slug = slug, responding_to = _),
))
}
#[get("/~/<_blog>/<_slug>/comment/<id>")]
pub fn activity_pub(_blog: String, _slug: String, id: i32, _ap: ApRequest, conn: DbConn) -> Option<ActivityStream<Note>> {
pub fn activity_pub(
_blog: String,
_slug: String,
id: i32,
_ap: ApRequest,
conn: DbConn,
) -> Option<ActivityStream<Note>> {
Comment::get(&*conn, id)
.and_then(|c| c.to_activity(&*conn))
.ok()
+43 -25
View File
@@ -1,11 +1,11 @@
use plume_models::users::User;
use plume_models::{db_conn::DbConn, Error};
use rocket::{
Request,
request::FromRequest,
response::{self, Responder},
Request,
};
use rocket_i18n::I18n;
use plume_models::{Error, db_conn::DbConn};
use plume_models::users::User;
use template_utils::Ructe;
#[derive(Debug)]
@@ -24,15 +24,24 @@ impl<'r> Responder<'r> for ErrorPage {
let user = User::from_request(req).succeeded();
match self.0 {
Error::NotFound => render!(errors::not_found(
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
)).respond_to(req),
Error::Unauthorized => render!(errors::not_found(
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
)).respond_to(req),
_ => render!(errors::not_found(
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
)).respond_to(req)
Error::NotFound => render!(errors::not_found(&(
&*conn.unwrap(),
&intl.unwrap().catalog,
user
)))
.respond_to(req),
Error::Unauthorized => render!(errors::not_found(&(
&*conn.unwrap(),
&intl.unwrap().catalog,
user
)))
.respond_to(req),
_ => render!(errors::not_found(&(
&*conn.unwrap(),
&intl.unwrap().catalog,
user
)))
.respond_to(req),
}
}
}
@@ -42,9 +51,11 @@ pub fn not_found(req: &Request) -> Ructe {
let conn = req.guard::<DbConn>().succeeded();
let intl = req.guard::<I18n>().succeeded();
let user = User::from_request(req).succeeded();
render!(errors::not_found(
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
))
render!(errors::not_found(&(
&*conn.unwrap(),
&intl.unwrap().catalog,
user
)))
}
#[catch(422)]
@@ -52,9 +63,11 @@ pub fn unprocessable_entity(req: &Request) -> Ructe {
let conn = req.guard::<DbConn>().succeeded();
let intl = req.guard::<I18n>().succeeded();
let user = User::from_request(req).succeeded();
render!(errors::unprocessable_entity(
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
))
render!(errors::unprocessable_entity(&(
&*conn.unwrap(),
&intl.unwrap().catalog,
user
)))
}
#[catch(500)]
@@ -62,17 +75,22 @@ pub fn server_error(req: &Request) -> Ructe {
let conn = req.guard::<DbConn>().succeeded();
let intl = req.guard::<I18n>().succeeded();
let user = User::from_request(req).succeeded();
render!(errors::server_error(
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
))
render!(errors::server_error(&(
&*conn.unwrap(),
&intl.unwrap().catalog,
user
)))
}
#[post("/csrf-violation?<target>")]
pub fn csrf_violation(target: Option<String>, conn: DbConn, intl: I18n, user: Option<User>) -> Ructe {
pub fn csrf_violation(
target: Option<String>,
conn: DbConn,
intl: I18n,
user: Option<User>,
) -> Ructe {
if let Some(uri) = target {
eprintln!("Csrf violation while acceding \"{}\"", uri)
}
render!(errors::csrf(
&(&*conn, &intl.catalog, user)
))
render!(errors::csrf(&(&*conn, &intl.catalog, user)))
}
+93 -46
View File
@@ -1,24 +1,19 @@
use rocket::{request::LenientForm, response::{status, Redirect}};
use rocket::{
request::LenientForm,
response::{status, Redirect},
};
use rocket_contrib::json::Json;
use rocket_i18n::I18n;
use serde_json;
use validator::{Validate, ValidationErrors};
use plume_common::activity_pub::sign::{Signable,
verify_http_headers};
use plume_models::{
admin::Admin,
comments::Comment,
db_conn::DbConn,
Error,
headers::Headers,
posts::Post,
users::User,
safe_string::SafeString,
instance::*
};
use inbox::{Inbox, SignedJson};
use routes::{Page, errors::ErrorPage};
use plume_common::activity_pub::sign::{verify_http_headers, Signable};
use plume_models::{
admin::Admin, comments::Comment, db_conn::DbConn, headers::Headers, instance::*, posts::Post,
safe_string::SafeString, users::User, Error,
};
use routes::{errors::ErrorPage, Page};
use template_utils::Ructe;
use Searcher;
@@ -46,7 +41,12 @@ pub fn index(conn: DbConn, user: Option<User>, intl: I18n) -> Result<Ructe, Erro
}
#[get("/local?<page>")]
pub fn local(conn: DbConn, user: Option<User>, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
pub fn local(
conn: DbConn,
user: Option<User>,
page: Option<Page>,
intl: I18n,
) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default();
let instance = Instance::get_local(&*conn)?;
let articles = Post::get_instance_page(&*conn, instance.id, page.limits())?;
@@ -75,7 +75,12 @@ pub fn feed(conn: DbConn, user: User, page: Option<Page>, intl: I18n) -> Result<
}
#[get("/federated?<page>")]
pub fn federated(conn: DbConn, user: Option<User>, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
pub fn federated(
conn: DbConn,
user: Option<User>,
page: Option<Page>,
intl: I18n,
) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default();
let articles = Post::get_recents_page(&*conn, page.limits())?;
Ok(render!(instance::federated(
@@ -111,23 +116,34 @@ pub struct InstanceSettingsForm {
pub short_description: SafeString,
pub long_description: SafeString,
#[validate(length(min = "1"))]
pub default_license: String
pub default_license: String,
}
#[post("/admin", data = "<form>")]
pub fn update_settings(conn: DbConn, admin: Admin, form: LenientForm<InstanceSettingsForm>, intl: I18n) -> Result<Redirect, Ructe> {
pub fn update_settings(
conn: DbConn,
admin: Admin,
form: LenientForm<InstanceSettingsForm>,
intl: I18n,
) -> Result<Redirect, Ructe> {
form.validate()
.and_then(|_| {
let instance = Instance::get_local(&*conn).expect("instance::update_settings: local instance error");
instance.update(&*conn,
form.name.clone(),
form.open_registrations,
form.short_description.clone(),
form.long_description.clone()).expect("instance::update_settings: save error");
let instance = Instance::get_local(&*conn)
.expect("instance::update_settings: local instance error");
instance
.update(
&*conn,
form.name.clone(),
form.open_registrations,
form.short_description.clone(),
form.long_description.clone(),
)
.expect("instance::update_settings: save error");
Ok(Redirect::to(uri!(admin)))
})
.or_else(|e| {
let local_inst = Instance::get_local(&*conn).expect("instance::update_settings: local instance error");
.or_else(|e| {
let local_inst = Instance::get_local(&*conn)
.expect("instance::update_settings: local instance error");
Err(render!(instance::admin(
&(&*conn, &intl.catalog, Some(admin.0)),
local_inst,
@@ -138,7 +154,12 @@ pub fn update_settings(conn: DbConn, admin: Admin, form: LenientForm<InstanceSet
}
#[get("/admin/instances?<page>")]
pub fn admin_instances(admin: Admin, conn: DbConn, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
pub fn admin_instances(
admin: Admin,
conn: DbConn,
page: Option<Page>,
intl: I18n,
) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default();
let instances = Instance::page(&*conn, page.limits())?;
Ok(render!(instance::list(
@@ -160,7 +181,12 @@ pub fn toggle_block(_admin: Admin, conn: DbConn, id: i32) -> Result<Redirect, Er
}
#[get("/admin/users?<page>")]
pub fn admin_users(admin: Admin, conn: DbConn, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
pub fn admin_users(
admin: Admin,
conn: DbConn,
page: Option<Page>,
intl: I18n,
) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default();
Ok(render!(instance::users(
&(&*conn, &intl.catalog, Some(admin.0)),
@@ -171,7 +197,12 @@ pub fn admin_users(admin: Admin, conn: DbConn, page: Option<Page>, intl: I18n) -
}
#[post("/admin/users/<id>/ban")]
pub fn ban(_admin: Admin, conn: DbConn, id: i32, searcher: Searcher) -> Result<Redirect, ErrorPage> {
pub fn ban(
_admin: Admin,
conn: DbConn,
id: i32,
searcher: Searcher,
) -> Result<Redirect, ErrorPage> {
if let Ok(u) = User::get(&*conn, id) {
u.delete(&*conn, &searcher)?;
}
@@ -179,34 +210,50 @@ pub fn ban(_admin: Admin, conn: DbConn, id: i32, searcher: Searcher) -> Result<R
}
#[post("/inbox", data = "<data>")]
pub fn shared_inbox(conn: DbConn, data: SignedJson<serde_json::Value>, headers: Headers, searcher: Searcher) -> Result<String, status::BadRequest<&'static str>> {
pub fn shared_inbox(
conn: DbConn,
data: SignedJson<serde_json::Value>,
headers: Headers,
searcher: Searcher,
) -> Result<String, status::BadRequest<&'static str>> {
let act = data.1.into_inner();
let sig = data.0;
let activity = act.clone();
let actor_id = activity["actor"].as_str()
.or_else(|| activity["actor"]["id"].as_str()).ok_or(status::BadRequest(Some("Missing actor id for activity")))?;
let actor_id = activity["actor"]
.as_str()
.or_else(|| activity["actor"]["id"].as_str())
.ok_or(status::BadRequest(Some("Missing actor id for activity")))?;
let actor = User::from_url(&conn, actor_id).expect("instance::shared_inbox: user error");
if !verify_http_headers(&actor, &headers.0, &sig).is_secure() &&
!act.clone().verify(&actor) {
if !verify_http_headers(&actor, &headers.0, &sig).is_secure() && !act.clone().verify(&actor) {
// maybe we just know an old key?
actor.refetch(&conn).and_then(|_| User::get(&conn, actor.id))
.and_then(|u| if verify_http_headers(&u, &headers.0, &sig).is_secure() ||
act.clone().verify(&u) {
Ok(())
} else {
Err(Error::Signature)
})
actor
.refetch(&conn)
.and_then(|_| User::get(&conn, actor.id))
.and_then(|u| {
if verify_http_headers(&u, &headers.0, &sig).is_secure() || act.clone().verify(&u) {
Ok(())
} else {
Err(Error::Signature)
}
})
.map_err(|_| {
println!("Rejected invalid activity supposedly from {}, with headers {:?}", actor.username, headers.0);
status::BadRequest(Some("Invalid signature"))})?;
println!(
"Rejected invalid activity supposedly from {}, with headers {:?}",
actor.username, headers.0
);
status::BadRequest(Some("Invalid signature"))
})?;
}
if Instance::is_blocked(&*conn, actor_id).map_err(|_| status::BadRequest(Some("Can't tell if instance is blocked")))? {
if Instance::is_blocked(&*conn, actor_id)
.map_err(|_| status::BadRequest(Some("Can't tell if instance is blocked")))?
{
return Ok(String::new());
}
let instance = Instance::get_local(&*conn).expect("instance::shared_inbox: local instance not found error");
let instance = Instance::get_local(&*conn)
.expect("instance::shared_inbox: local instance not found error");
Ok(match instance.received(&*conn, &searcher, act) {
Ok(_) => String::new(),
Err(e) => {
+24 -16
View File
@@ -1,25 +1,28 @@
use rocket::response::{Redirect, Flash};
use rocket::response::{Flash, Redirect};
use rocket_i18n::I18n;
use plume_common::activity_pub::{broadcast, inbox::{Notify, Deletable}};
use plume_common::utils;
use plume_models::{
blogs::Blog,
db_conn::DbConn,
likes,
posts::Post,
users::User
use plume_common::activity_pub::{
broadcast,
inbox::{Deletable, Notify},
};
use Worker;
use plume_common::utils;
use plume_models::{blogs::Blog, db_conn::DbConn, likes, posts::Post, users::User};
use routes::errors::ErrorPage;
use Worker;
#[post("/~/<blog>/<slug>/like")]
pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Result<Redirect, ErrorPage> {
pub fn create(
blog: String,
slug: String,
user: User,
conn: DbConn,
worker: Worker,
) -> Result<Redirect, ErrorPage> {
let b = Blog::find_by_fqn(&*conn, &blog)?;
let post = Post::find_by_slug(&*conn, &slug, b.id)?;
if !user.has_liked(&*conn, &post)? {
let like = likes::Like::insert(&*conn, likes::NewLike::new(&post ,&user))?;
let like = likes::Like::insert(&*conn, likes::NewLike::new(&post, &user))?;
like.notify(&*conn)?;
let dest = User::one_by_instance(&*conn)?;
@@ -32,13 +35,18 @@ pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Work
worker.execute(move || broadcast(&user, delete_act, dest));
}
Ok(Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _)))
Ok(Redirect::to(
uri!(super::posts::details: blog = blog, slug = slug, responding_to = _),
))
}
#[post("/~/<blog>/<slug>/like", rank = 2)]
pub fn create_auth(blog: String, slug: String, i18n: I18n) -> Flash<Redirect>{
pub fn create_auth(blog: String, slug: String, i18n: I18n) -> Flash<Redirect> {
utils::requires_login(
&i18n!(i18n.catalog, "You need to be logged in order to like a post"),
uri!(create: blog = blog, slug = slug)
&i18n!(
i18n.catalog,
"You need to be logged in order to like a post"
),
uri!(create: blog = blog, slug = slug),
)
}
+68 -40
View File
@@ -1,11 +1,18 @@
use guid_create::GUID;
use multipart::server::{Multipart, save::{SavedData, SaveResult}};
use rocket::{Data, http::ContentType, response::{Redirect, status}};
use multipart::server::{
save::{SaveResult, SavedData},
Multipart,
};
use plume_models::{db_conn::DbConn, medias::*, users::User, Error};
use rocket::{
http::ContentType,
response::{status, Redirect},
Data,
};
use rocket_i18n::I18n;
use routes::{errors::ErrorPage, Page};
use std::fs;
use plume_models::{Error, db_conn::DbConn, medias::*, users::User};
use template_utils::Ructe;
use routes::{Page, errors::ErrorPage};
#[get("/medias?<page>")]
pub fn list(user: User, conn: DbConn, intl: I18n, page: Option<Page>) -> Result<Ructe, ErrorPage> {
@@ -21,64 +28,85 @@ pub fn list(user: User, conn: DbConn, intl: I18n, page: Option<Page>) -> Result<
#[get("/medias/new")]
pub fn new(user: User, conn: DbConn, intl: I18n) -> Ructe {
render!(medias::new(
&(&*conn, &intl.catalog, Some(user))
))
render!(medias::new(&(&*conn, &intl.catalog, Some(user))))
}
#[post("/medias/new", data = "<data>")]
pub fn upload(user: User, data: Data, ct: &ContentType, conn: DbConn) -> Result<Redirect, status::BadRequest<&'static str>> {
pub fn upload(
user: User,
data: Data,
ct: &ContentType,
conn: DbConn,
) -> Result<Redirect, status::BadRequest<&'static str>> {
if ct.is_form_data() {
let (_, boundary) = ct.params().find(|&(k, _)| k == "boundary").ok_or_else(|| status::BadRequest(Some("No boundary")))?;
let (_, boundary) = ct
.params()
.find(|&(k, _)| k == "boundary")
.ok_or_else(|| status::BadRequest(Some("No boundary")))?;
match Multipart::with_body(data.open(), boundary).save().temp() {
SaveResult::Full(entries) => {
let fields = entries.fields;
let filename = fields.get("file").and_then(|v| v.iter().next())
.ok_or_else(|| status::BadRequest(Some("No file uploaded")))?.headers
.filename.clone();
let filename = fields
.get("file")
.and_then(|v| v.iter().next())
.ok_or_else(|| status::BadRequest(Some("No file uploaded")))?
.headers
.filename
.clone();
// Remove extension if it contains something else than just letters and numbers
let ext = filename
.and_then(|f| f
.rsplit('.')
.next()
.and_then(|ext| if ext.chars().any(|c| !c.is_alphanumeric()) {
None
} else {
Some(ext.to_lowercase())
})
.map(|ext| format!(".{}", ext))
).unwrap_or_default();
.and_then(|f| {
f.rsplit('.')
.next()
.and_then(|ext| {
if ext.chars().any(|c| !c.is_alphanumeric()) {
None
} else {
Some(ext.to_lowercase())
}
})
.map(|ext| format!(".{}", ext))
})
.unwrap_or_default();
let dest = format!("static/media/{}{}", GUID::rand().to_string(), ext);
match fields["file"][0].data {
SavedData::Bytes(ref bytes) => fs::write(&dest, bytes).map_err(|_| status::BadRequest(Some("Couldn't save upload")))?,
SavedData::File(ref path, _) => {fs::copy(path, &dest).map_err(|_| status::BadRequest(Some("Couldn't copy upload")))?;},
SavedData::Bytes(ref bytes) => fs::write(&dest, bytes)
.map_err(|_| status::BadRequest(Some("Couldn't save upload")))?,
SavedData::File(ref path, _) => {
fs::copy(path, &dest)
.map_err(|_| status::BadRequest(Some("Couldn't copy upload")))?;
}
_ => {
return Ok(Redirect::to(uri!(new)));
}
}
let has_cw = !read(&fields["cw"][0].data).map(|cw| cw.is_empty()).unwrap_or(false);
let media = Media::insert(&*conn, NewMedia {
file_path: dest,
alt_text: read(&fields["alt"][0].data)?,
is_remote: false,
remote_url: None,
sensitive: has_cw,
content_warning: if has_cw {
Some(read(&fields["cw"][0].data)?)
} else {
None
let has_cw = !read(&fields["cw"][0].data)
.map(|cw| cw.is_empty())
.unwrap_or(false);
let media = Media::insert(
&*conn,
NewMedia {
file_path: dest,
alt_text: read(&fields["alt"][0].data)?,
is_remote: false,
remote_url: None,
sensitive: has_cw,
content_warning: if has_cw {
Some(read(&fields["cw"][0].data)?)
} else {
None
},
owner_id: user.id,
},
owner_id: user.id
}).map_err(|_| status::BadRequest(Some("Error while saving media")))?;
)
.map_err(|_| status::BadRequest(Some("Error while saving media")))?;
Ok(Redirect::to(uri!(details: id = media.id)))
},
SaveResult::Partial(_, _) | SaveResult::Error(_) => {
Ok(Redirect::to(uri!(new)))
}
SaveResult::Partial(_, _) | SaveResult::Error(_) => Ok(Redirect::to(uri!(new))),
}
} else {
Ok(Redirect::to(uri!(new)))
+41 -33
View File
@@ -2,25 +2,21 @@
use atom_syndication::{ContentBuilder, Entry, EntryBuilder, LinkBuilder, Person, PersonBuilder};
use rocket::{
http::{
RawStr, Status, uri::{FromUriParam, Query},
hyper::header::{CacheControl, CacheDirective}
hyper::header::{CacheControl, CacheDirective},
uri::{FromUriParam, Query},
RawStr, Status,
},
Outcome,
request::{self, FromFormValue, FromRequest, Request},
response::NamedFile,
Outcome,
};
use rocket_i18n::I18n;
use std::path::{Path, PathBuf};
use plume_models::{
Connection,
users::User,
posts::Post,
db_conn::DbConn,
};
use plume_models::{db_conn::DbConn, posts::Post, users::User, Connection};
use Worker;
use Searcher;
use Worker;
pub struct PlumeRocket<'a> {
conn: DbConn,
@@ -100,7 +96,6 @@ impl<'a, 'r> FromRequest<'a, 'r> for ContentLen {
}
}
impl Default for Page {
fn default() -> Self {
Page(1)
@@ -110,20 +105,33 @@ impl Default for Page {
pub fn post_to_atom(post: Post, conn: &Connection) -> Entry {
EntryBuilder::default()
.title(format!("<![CDATA[{}]]>", post.title))
.content(ContentBuilder::default()
.value(format!("<![CDATA[{}]]>", *post.content.get()))
.src(post.ap_url.clone())
.content_type("html".to_string())
.build().expect("Atom feed: content error"))
.authors(post.get_authors(&*conn).expect("Atom feed: author error")
.into_iter()
.map(|a| PersonBuilder::default()
.name(a.display_name)
.uri(a.ap_url)
.build().expect("Atom feed: author error"))
.collect::<Vec<Person>>())
.links(vec![LinkBuilder::default().href(post.ap_url).build().expect("Atom feed: link error")])
.build().expect("Atom feed: entry error")
.content(
ContentBuilder::default()
.value(format!("<![CDATA[{}]]>", *post.content.get()))
.src(post.ap_url.clone())
.content_type("html".to_string())
.build()
.expect("Atom feed: content error"),
)
.authors(
post.get_authors(&*conn)
.expect("Atom feed: author error")
.into_iter()
.map(|a| {
PersonBuilder::default()
.name(a.display_name)
.uri(a.ap_url)
.build()
.expect("Atom feed: author error")
})
.collect::<Vec<Person>>(),
)
.links(vec![LinkBuilder::default()
.href(post.ap_url)
.build()
.expect("Atom feed: link error")])
.build()
.expect("Atom feed: entry error")
}
pub mod blogs;
@@ -135,17 +143,17 @@ pub mod medias;
pub mod notifications;
pub mod posts;
pub mod reshares;
pub mod search;
pub mod session;
pub mod tags;
pub mod user;
pub mod search;
pub mod well_known;
#[derive(Responder)]
#[response()]
pub struct CachedFile {
inner: NamedFile,
cache_control: CacheControl
cache_control: CacheControl,
}
#[get("/static/cached/<_build_id>/<file..>", rank = 2)]
@@ -155,10 +163,10 @@ pub fn plume_static_files(file: PathBuf, _build_id: &RawStr) -> Option<CachedFil
#[get("/static/<file..>", rank = 3)]
pub fn static_files(file: PathBuf) -> Option<CachedFile> {
NamedFile::open(Path::new("static/").join(file)).ok()
.map(|f|
CachedFile {
inner: f,
cache_control: CacheControl(vec![CacheDirective::MaxAge(60*60*24*30)])
})
NamedFile::open(Path::new("static/").join(file))
.ok()
.map(|f| CachedFile {
inner: f,
cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]),
})
}
+14 -6
View File
@@ -1,13 +1,18 @@
use rocket::response::{Redirect, Flash};
use rocket::response::{Flash, Redirect};
use rocket_i18n::I18n;
use plume_common::utils;
use plume_models::{db_conn::DbConn, notifications::Notification, users::User};
use routes::{Page, errors::ErrorPage};
use routes::{errors::ErrorPage, Page};
use template_utils::Ructe;
#[get("/notifications?<page>")]
pub fn notifications(conn: DbConn, user: User, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
pub fn notifications(
conn: DbConn,
user: User,
page: Option<Page>,
intl: I18n,
) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default();
Ok(render!(notifications::index(
&(&*conn, &intl.catalog, Some(user.clone())),
@@ -18,9 +23,12 @@ pub fn notifications(conn: DbConn, user: User, page: Option<Page>, intl: I18n) -
}
#[get("/notifications?<page>", rank = 2)]
pub fn notifications_auth(i18n: I18n, page: Option<Page>) -> Flash<Redirect>{
pub fn notifications_auth(i18n: I18n, page: Option<Page>) -> Flash<Redirect> {
utils::requires_login(
&i18n!(i18n.catalog, "You need to be logged in order to see your notifications"),
uri!(notifications: page = page)
&i18n!(
i18n.catalog,
"You need to be logged in order to see your notifications"
),
uri!(notifications: page = page),
)
}
+219 -90
View File
@@ -1,20 +1,21 @@
use chrono::Utc;
use heck::{CamelCase, KebabCase};
use rocket::request::LenientForm;
use rocket::response::{Redirect, Flash};
use rocket::response::{Flash, Redirect};
use rocket_i18n::I18n;
use std::{
borrow::Cow,
collections::{HashMap, HashSet},
borrow::Cow, time::Duration,
time::Duration,
};
use validator::{Validate, ValidationError, ValidationErrors};
use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest, inbox::Deletable};
use plume_common::activity_pub::{broadcast, inbox::Deletable, ActivityStream, ApRequest};
use plume_common::utils;
use plume_models::{
blogs::*,
db_conn::DbConn,
comments::{Comment, CommentTree},
db_conn::DbConn,
instance::Instance,
medias::Media,
mentions::Mention,
@@ -22,16 +23,28 @@ use plume_models::{
posts::*,
safe_string::SafeString,
tags::*,
users::User
users::User,
};
use routes::{PlumeRocket, errors::ErrorPage, comments::NewCommentForm, ContentLen};
use routes::{comments::NewCommentForm, errors::ErrorPage, ContentLen, PlumeRocket};
use template_utils::Ructe;
#[get("/~/<blog>/<slug>?<responding_to>", rank = 4)]
pub fn details(blog: String, slug: String, conn: DbConn, user: Option<User>, responding_to: Option<i32>, intl: I18n) -> Result<Ructe, ErrorPage> {
pub fn details(
blog: String,
slug: String,
conn: DbConn,
user: Option<User>,
responding_to: Option<i32>,
intl: I18n,
) -> Result<Ructe, ErrorPage> {
let blog = Blog::find_by_fqn(&*conn, &blog)?;
let post = Post::find_by_slug(&*conn, &slug, blog.id)?;
if post.published || post.get_authors(&*conn)?.into_iter().any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0)) {
if post.published
|| post
.get_authors(&*conn)?
.into_iter()
.any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0))
{
let comments = CommentTree::from_post(&*conn, &post, user.as_ref())?;
let previous = responding_to.and_then(|r| Comment::get(&*conn, r).ok());
@@ -82,11 +95,19 @@ pub fn details(blog: String, slug: String, conn: DbConn, user: Option<User>, res
}
#[get("/~/<blog>/<slug>", rank = 3)]
pub fn activity_details(blog: String, slug: String, conn: DbConn, _ap: ApRequest) -> Result<ActivityStream<LicensedArticle>, Option<String>> {
pub fn activity_details(
blog: String,
slug: String,
conn: DbConn,
_ap: ApRequest,
) -> Result<ActivityStream<LicensedArticle>, Option<String>> {
let blog = Blog::find_by_fqn(&*conn, &blog).map_err(|_| None)?;
let post = Post::find_by_slug(&*conn, &slug, blog.id).map_err(|_| None)?;
if post.published {
Ok(ActivityStream::new(post.to_activity(&*conn).map_err(|_| String::from("Post serialization error"))?))
Ok(ActivityStream::new(
post.to_activity(&*conn)
.map_err(|_| String::from("Post serialization error"))?,
))
} else {
Err(Some(String::from("Not published yet.")))
}
@@ -95,8 +116,11 @@ pub fn activity_details(blog: String, slug: String, conn: DbConn, _ap: ApRequest
#[get("/~/<blog>/new", rank = 2)]
pub fn new_auth(blog: String, i18n: I18n) -> Flash<Redirect> {
utils::requires_login(
&i18n!(i18n.catalog, "You need to be logged in order to write a new post"),
uri!(new: blog = blog)
&i18n!(
i18n.catalog,
"You need to be logged in order to write a new post"
),
uri!(new: blog = blog),
)
}
@@ -112,7 +136,7 @@ pub fn new(blog: String, cl: ContentLen, rockets: PlumeRocket) -> Result<Ructe,
return Ok(render!(errors::not_authorized(
&(&*conn, &intl.catalog, Some(user)),
i18n!(intl.catalog, "You are not author in this blog.")
)))
)));
}
let medias = Media::for_user(&*conn, user.id)?;
@@ -134,7 +158,12 @@ pub fn new(blog: String, cl: ContentLen, rockets: PlumeRocket) -> Result<Ructe,
}
#[get("/~/<blog>/<slug>/edit")]
pub fn edit(blog: String, slug: String, cl: ContentLen, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
pub fn edit(
blog: String,
slug: String,
cl: ContentLen,
rockets: PlumeRocket,
) -> Result<Ructe, ErrorPage> {
let conn = rockets.conn;
let intl = rockets.intl;
let b = Blog::find_by_fqn(&*conn, &blog)?;
@@ -145,10 +174,9 @@ pub fn edit(blog: String, slug: String, cl: ContentLen, rockets: PlumeRocket) ->
return Ok(render!(errors::not_authorized(
&(&*conn, &intl.catalog, Some(user)),
i18n!(intl.catalog, "You are not author in this blog.")
)))
)));
}
let source = if !post.source.is_empty() {
post.source.clone()
} else {
@@ -168,7 +196,7 @@ pub fn edit(blog: String, slug: String, cl: ContentLen, rockets: PlumeRocket) ->
content: source,
tags: Tag::for_post(&*conn, post.id)?
.into_iter()
.filter_map(|t| if !t.is_hashtag {Some(t.tag)} else {None})
.filter_map(|t| if !t.is_hashtag { Some(t.tag) } else { None })
.collect::<Vec<String>>()
.join(", "),
license: post.license.clone(),
@@ -184,11 +212,17 @@ pub fn edit(blog: String, slug: String, cl: ContentLen, rockets: PlumeRocket) ->
}
#[post("/~/<blog>/<slug>/edit", data = "<form>")]
pub fn update(blog: String, slug: String, cl: ContentLen, form: LenientForm<NewPostForm>, rockets: PlumeRocket)
-> Result<Redirect, Ructe> {
pub fn update(
blog: String,
slug: String,
cl: ContentLen,
form: LenientForm<NewPostForm>,
rockets: PlumeRocket,
) -> Result<Redirect, Ructe> {
let conn = rockets.conn;
let b = Blog::find_by_fqn(&*conn, &blog).expect("post::update: blog error");
let mut post = Post::find_by_slug(&*conn, &slug, b.id).expect("post::update: find by slug error");
let mut post =
Post::find_by_slug(&*conn, &slug, b.id).expect("post::update: find by slug error");
let user = rockets.user.unwrap();
let intl = rockets.intl;
@@ -200,23 +234,36 @@ pub fn update(blog: String, slug: String, cl: ContentLen, form: LenientForm<NewP
let mut errors = match form.validate() {
Ok(_) => ValidationErrors::new(),
Err(e) => e
Err(e) => e,
};
if new_slug != slug && Post::find_by_slug(&*conn, &new_slug, b.id).is_ok() {
errors.add("title", ValidationError {
code: Cow::from("existing_slug"),
message: Some(Cow::from("A post with the same title already exists.")),
params: HashMap::new()
});
errors.add(
"title",
ValidationError {
code: Cow::from("existing_slug"),
message: Some(Cow::from("A post with the same title already exists.")),
params: HashMap::new(),
},
);
}
if errors.is_empty() {
if !user.is_author_in(&*conn, &b).expect("posts::update: is author in error") {
if !user
.is_author_in(&*conn, &b)
.expect("posts::update: is author in error")
{
// actually it's not "Ok"…
Ok(Redirect::to(uri!(super::blogs::details: name = blog, page = _)))
Ok(Redirect::to(
uri!(super::blogs::details: name = blog, page = _),
))
} else {
let (content, mentions, hashtags) = utils::md_to_html(form.content.to_string().as_ref(), &Instance::get_local(&conn).expect("posts::update: Error getting local instance").public_domain);
let (content, mentions, hashtags) = utils::md_to_html(
form.content.to_string().as_ref(),
&Instance::get_local(&conn)
.expect("posts::update: Error getting local instance")
.public_domain,
);
// update publication date if when this article is no longer a draft
let newly_published = if !post.published && !form.draft {
@@ -236,34 +283,61 @@ pub fn update(blog: String, slug: String, cl: ContentLen, form: LenientForm<NewP
post.source = form.content.clone();
post.license = form.license.clone();
post.cover_id = form.cover;
post.update(&*conn, &searcher).expect("post::update: update error");;
post.update(&*conn, &searcher)
.expect("post::update: update error");;
if post.published {
post.update_mentions(&conn, mentions.into_iter().filter_map(|m| Mention::build_activity(&conn, &m).ok()).collect())
.expect("post::update: mentions error");;
post.update_mentions(
&conn,
mentions
.into_iter()
.filter_map(|m| Mention::build_activity(&conn, &m).ok())
.collect(),
)
.expect("post::update: mentions error");;
}
let tags = form.tags.split(',').map(|t| t.trim().to_camel_case()).filter(|t| !t.is_empty())
.collect::<HashSet<_>>().into_iter().filter_map(|t| Tag::build_activity(&conn, t).ok()).collect::<Vec<_>>();
post.update_tags(&conn, tags).expect("post::update: tags error");
let tags = form
.tags
.split(',')
.map(|t| t.trim().to_camel_case())
.filter(|t| !t.is_empty())
.collect::<HashSet<_>>()
.into_iter()
.filter_map(|t| Tag::build_activity(&conn, t).ok())
.collect::<Vec<_>>();
post.update_tags(&conn, tags)
.expect("post::update: tags error");
let hashtags = hashtags.into_iter().map(|h| h.to_camel_case()).collect::<HashSet<_>>()
.into_iter().filter_map(|t| Tag::build_activity(&conn, t).ok()).collect::<Vec<_>>();
post.update_hashtags(&conn, hashtags).expect("post::update: hashtags error");
let hashtags = hashtags
.into_iter()
.map(|h| h.to_camel_case())
.collect::<HashSet<_>>()
.into_iter()
.filter_map(|t| Tag::build_activity(&conn, t).ok())
.collect::<Vec<_>>();
post.update_hashtags(&conn, hashtags)
.expect("post::update: hashtags error");
if post.published {
if newly_published {
let act = post.create_activity(&conn).expect("post::update: act error");
let act = post
.create_activity(&conn)
.expect("post::update: act error");
let dest = User::one_by_instance(&*conn).expect("post::update: dest error");
worker.execute(move || broadcast(&user, act, dest));
} else {
let act = post.update_activity(&*conn).expect("post::update: act error");
let act = post
.update_activity(&*conn)
.expect("post::update: act error");
let dest = User::one_by_instance(&*conn).expect("posts::update: dest error");
worker.execute(move || broadcast(&user, act, dest));
}
}
Ok(Redirect::to(uri!(details: blog = blog, slug = new_slug, responding_to = _)))
Ok(Redirect::to(
uri!(details: blog = blog, slug = new_slug, responding_to = _),
))
}
} else {
let medias = Media::for_user(&*conn, user.id).expect("posts:update: medias error");
@@ -306,7 +380,12 @@ pub fn valid_slug(title: &str) -> Result<(), ValidationError> {
}
#[post("/~/<blog_name>/new", data = "<form>")]
pub fn create(blog_name: String, form: LenientForm<NewPostForm>, cl: ContentLen, rockets: PlumeRocket) -> Result<Redirect, Result<Ructe, ErrorPage>> {
pub fn create(
blog_name: String,
form: LenientForm<NewPostForm>,
cl: ContentLen,
rockets: PlumeRocket,
) -> Result<Redirect, Result<Ructe, ErrorPage>> {
let conn = rockets.conn;
let blog = Blog::find_by_fqn(&*conn, &blog_name).expect("post::create: blog error");;
let slug = form.title.to_string().to_kebab_case();
@@ -314,86 +393,119 @@ pub fn create(blog_name: String, form: LenientForm<NewPostForm>, cl: ContentLen,
let mut errors = match form.validate() {
Ok(_) => ValidationErrors::new(),
Err(e) => e
Err(e) => e,
};
if Post::find_by_slug(&*conn, &slug, blog.id).is_ok() {
errors.add("title", ValidationError {
code: Cow::from("existing_slug"),
message: Some(Cow::from("A post with the same title already exists.")),
params: HashMap::new()
});
errors.add(
"title",
ValidationError {
code: Cow::from("existing_slug"),
message: Some(Cow::from("A post with the same title already exists.")),
params: HashMap::new(),
},
);
}
if errors.is_empty() {
if !user.is_author_in(&*conn, &blog).expect("post::create: is author in error") {
if !user
.is_author_in(&*conn, &blog)
.expect("post::create: is author in error")
{
// actually it's not "Ok"…
return Ok(Redirect::to(uri!(super::blogs::details: name = blog_name, page = _)))
return Ok(Redirect::to(
uri!(super::blogs::details: name = blog_name, page = _),
));
}
let (content, mentions, hashtags) = utils::md_to_html(
form.content.to_string().as_ref(),
&Instance::get_local(&conn).expect("post::create: local instance error").public_domain
&Instance::get_local(&conn)
.expect("post::create: local instance error")
.public_domain,
);
let searcher = rockets.searcher;
let post = Post::insert(&*conn, NewPost {
blog_id: blog.id,
slug: slug.to_string(),
title: form.title.to_string(),
content: SafeString::new(&content),
published: !form.draft,
license: form.license.clone(),
ap_url: "".to_string(),
creation_date: None,
subtitle: form.subtitle.clone(),
source: form.content.clone(),
cover_id: form.cover,
let post = Post::insert(
&*conn,
NewPost {
blog_id: blog.id,
slug: slug.to_string(),
title: form.title.to_string(),
content: SafeString::new(&content),
published: !form.draft,
license: form.license.clone(),
ap_url: "".to_string(),
creation_date: None,
subtitle: form.subtitle.clone(),
source: form.content.clone(),
cover_id: form.cover,
},
&searcher,
).expect("post::create: post save error");
)
.expect("post::create: post save error");
PostAuthor::insert(&*conn, NewPostAuthor {
post_id: post.id,
author_id: user.id
}).expect("post::create: author save error");
PostAuthor::insert(
&*conn,
NewPostAuthor {
post_id: post.id,
author_id: user.id,
},
)
.expect("post::create: author save error");
let tags = form.tags.split(',')
let tags = form
.tags
.split(',')
.map(|t| t.trim().to_camel_case())
.filter(|t| !t.is_empty())
.collect::<HashSet<_>>();
for tag in tags {
Tag::insert(&*conn, NewTag {
tag,
is_hashtag: false,
post_id: post.id
}).expect("post::create: tags save error");
Tag::insert(
&*conn,
NewTag {
tag,
is_hashtag: false,
post_id: post.id,
},
)
.expect("post::create: tags save error");
}
for hashtag in hashtags {
Tag::insert(&*conn, NewTag {
tag: hashtag.to_camel_case(),
is_hashtag: true,
post_id: post.id
}).expect("post::create: hashtags save error");
Tag::insert(
&*conn,
NewTag {
tag: hashtag.to_camel_case(),
is_hashtag: true,
post_id: post.id,
},
)
.expect("post::create: hashtags save error");
}
if post.published {
for m in mentions {
Mention::from_activity(
&*conn,
&Mention::build_activity(&*conn, &m).expect("post::create: mention build error"),
&Mention::build_activity(&*conn, &m)
.expect("post::create: mention build error"),
post.id,
true,
true
).expect("post::create: mention save error");
true,
)
.expect("post::create: mention save error");
}
let act = post.create_activity(&*conn).expect("posts::create: activity error");
let act = post
.create_activity(&*conn)
.expect("posts::create: activity error");
let dest = User::one_by_instance(&*conn).expect("posts::create: dest error");
let worker = rockets.worker;
worker.execute(move || broadcast(&user, act, dest));
}
Ok(Redirect::to(uri!(details: blog = blog_name, slug = slug, responding_to = _)))
Ok(Redirect::to(
uri!(details: blog = blog_name, slug = slug, responding_to = _),
))
} else {
let medias = Media::for_user(&*conn, user.id).expect("posts::create: medias error");
let intl = rockets.intl;
@@ -413,15 +525,25 @@ pub fn create(blog_name: String, form: LenientForm<NewPostForm>, cl: ContentLen,
}
#[post("/~/<blog_name>/<slug>/delete")]
pub fn delete(blog_name: String, slug: String, rockets: PlumeRocket) -> Result<Redirect, ErrorPage> {
pub fn delete(
blog_name: String,
slug: String,
rockets: PlumeRocket,
) -> Result<Redirect, ErrorPage> {
let conn = rockets.conn;
let user = rockets.user.unwrap();
let post = Blog::find_by_fqn(&*conn, &blog_name)
.and_then(|blog| Post::find_by_slug(&*conn, &slug, blog.id));
if let Ok(post) = post {
if !post.get_authors(&*conn)?.into_iter().any(|a| a.id == user.id) {
return Ok(Redirect::to(uri!(details: blog = blog_name.clone(), slug = slug.clone(), responding_to = _)))
if !post
.get_authors(&*conn)?
.into_iter()
.any(|a| a.id == user.id)
{
return Ok(Redirect::to(
uri!(details: blog = blog_name.clone(), slug = slug.clone(), responding_to = _),
));
}
let searcher = rockets.searcher;
@@ -432,10 +554,17 @@ pub fn delete(blog_name: String, slug: String, rockets: PlumeRocket) -> Result<R
let user_c = user.clone();
worker.execute(move || broadcast(&user_c, delete_activity, dest));
worker.execute_after(Duration::from_secs(10*60), move || {user.rotate_keypair(&conn).expect("Failed to rotate keypair");});
worker.execute_after(Duration::from_secs(10 * 60), move || {
user.rotate_keypair(&conn)
.expect("Failed to rotate keypair");
});
Ok(Redirect::to(uri!(super::blogs::details: name = blog_name, page = _)))
Ok(Redirect::to(
uri!(super::blogs::details: name = blog_name, page = _),
))
} else {
Ok(Redirect::to(uri!(super::blogs::details: name = blog_name, page = _)))
Ok(Redirect::to(
uri!(super::blogs::details: name = blog_name, page = _),
))
}
}
+22 -14
View File
@@ -1,20 +1,23 @@
use rocket::response::{Redirect, Flash};
use rocket::response::{Flash, Redirect};
use rocket_i18n::I18n;
use plume_common::activity_pub::{broadcast, inbox::{Deletable, Notify}};
use plume_common::utils;
use plume_models::{
blogs::Blog,
db_conn::DbConn,
posts::Post,
reshares::*,
users::User
use plume_common::activity_pub::{
broadcast,
inbox::{Deletable, Notify},
};
use plume_common::utils;
use plume_models::{blogs::Blog, db_conn::DbConn, posts::Post, reshares::*, users::User};
use routes::errors::ErrorPage;
use Worker;
#[post("/~/<blog>/<slug>/reshare")]
pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Result<Redirect, ErrorPage> {
pub fn create(
blog: String,
slug: String,
user: User,
conn: DbConn,
worker: Worker,
) -> Result<Redirect, ErrorPage> {
let b = Blog::find_by_fqn(&*conn, &blog)?;
let post = Post::find_by_slug(&*conn, &slug, b.id)?;
@@ -32,13 +35,18 @@ pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Work
worker.execute(move || broadcast(&user, delete_act, dest));
}
Ok(Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _)))
Ok(Redirect::to(
uri!(super::posts::details: blog = blog, slug = slug, responding_to = _),
))
}
#[post("/~/<blog>/<slug>/reshare", rank=1)]
#[post("/~/<blog>/<slug>/reshare", rank = 1)]
pub fn create_auth(blog: String, slug: String, i18n: I18n) -> Flash<Redirect> {
utils::requires_login(
&i18n!(i18n.catalog, "You need to be logged in order to reshare a post"),
uri!(create: blog = blog, slug = slug)
&i18n!(
i18n.catalog,
"You need to be logged in order to reshare a post"
),
uri!(create: blog = blog, slug = slug),
)
}
+12 -7
View File
@@ -2,13 +2,11 @@ use chrono::offset::Utc;
use rocket::request::Form;
use rocket_i18n::I18n;
use plume_models::{
db_conn::DbConn, users::User,
search::Query};
use plume_models::{db_conn::DbConn, search::Query, users::User};
use routes::Page;
use std::str::FromStr;
use template_utils::Ructe;
use Searcher;
use std::str::FromStr;
#[derive(Default, FromForm)]
pub struct SearchQuery {
@@ -53,12 +51,19 @@ macro_rules! param_to_query {
}
}
#[get("/search?<query..>")]
pub fn search(query: Option<Form<SearchQuery>>, conn: DbConn, searcher: Searcher, user: Option<User>, intl: I18n) -> Ructe {
pub fn search(
query: Option<Form<SearchQuery>>,
conn: DbConn,
searcher: Searcher,
user: Option<User>,
intl: I18n,
) -> Ructe {
let query = query.map(|f| f.into_inner()).unwrap_or_default();
let page = query.page.unwrap_or_default();
let mut parsed_query = Query::from_str(&query.q.as_ref().map(|q| q.as_str()).unwrap_or_default()).unwrap_or_default();
let mut parsed_query =
Query::from_str(&query.q.as_ref().map(|q| q.as_str()).unwrap_or_default())
.unwrap_or_default();
param_to_query!(query, parsed_query; normal: title, subtitle, content, tag,
instance, author, blog, lang, license;
+103 -60
View File
@@ -1,22 +1,26 @@
use lettre::Transport;
use rocket::{
State,
http::{Cookie, Cookies, SameSite, uri::Uri},
response::Redirect,
request::{LenientForm, FlashMessage, Form}
};
use rocket::http::ext::IntoOwned;
use rocket_i18n::I18n;
use std::{borrow::Cow, sync::{Arc, Mutex}, time::Instant};
use validator::{Validate, ValidationError, ValidationErrors};
use template_utils::Ructe;
use plume_models::{
BASE_URL, Error,
db_conn::DbConn,
users::{User, AUTH_COOKIE}
use rocket::{
http::{uri::Uri, Cookie, Cookies, SameSite},
request::{FlashMessage, Form, LenientForm},
response::Redirect,
State,
};
use rocket_i18n::I18n;
use std::{
borrow::Cow,
sync::{Arc, Mutex},
time::Instant,
};
use template_utils::Ructe;
use validator::{Validate, ValidationError, ValidationErrors};
use mail::{build_mail, Mailer};
use plume_models::{
db_conn::DbConn,
users::{User, AUTH_COOKIE},
Error, BASE_URL,
};
use routes::errors::ErrorPage;
#[get("/login?<m>")]
@@ -34,16 +38,22 @@ pub struct LoginForm {
#[validate(length(min = "1", message = "We need an email or a username to identify you"))]
pub email_or_name: String,
#[validate(length(min = "1", message = "Your password can't be empty"))]
pub password: String
pub password: String,
}
#[post("/login", data = "<form>")]
pub fn create(conn: DbConn, form: LenientForm<LoginForm>, flash: Option<FlashMessage>, mut cookies: Cookies, intl: I18n) -> Result<Redirect, Ructe> {
pub fn create(
conn: DbConn,
form: LenientForm<LoginForm>,
flash: Option<FlashMessage>,
mut cookies: Cookies,
intl: I18n,
) -> Result<Redirect, Ructe> {
let user = User::find_by_email(&*conn, &form.email_or_name)
.or_else(|_| User::find_by_fqn(&*conn, &form.email_or_name));
let mut errors = match form.validate() {
Ok(_) => ValidationErrors::new(),
Err(e) => e
Err(e) => e,
};
let user_id = if let Ok(user) = user {
@@ -58,7 +68,9 @@ pub fn create(conn: DbConn, form: LenientForm<LoginForm>, flash: Option<FlashMes
} else {
// Fake password verification, only to avoid different login times
// that could be used to see if an email adress is registered or not
User::get(&*conn, 1).map(|u| u.auth(&form.password)).expect("No user is registered");
User::get(&*conn, 1)
.map(|u| u.auth(&form.password))
.expect("No user is registered");
let mut err = ValidationError::new("invalid_login");
err.message = Some(Cow::from("Invalid username or password"));
@@ -67,25 +79,31 @@ pub fn create(conn: DbConn, form: LenientForm<LoginForm>, flash: Option<FlashMes
};
if errors.is_empty() {
cookies.add_private(Cookie::build(AUTH_COOKIE, user_id)
.same_site(SameSite::Lax)
.finish());
cookies.add_private(
Cookie::build(AUTH_COOKIE, user_id)
.same_site(SameSite::Lax)
.finish(),
);
let destination = flash
.and_then(|f| if f.name() == "callback" {
Some(f.msg().to_owned())
} else {
None
.and_then(|f| {
if f.name() == "callback" {
Some(f.msg().to_owned())
} else {
None
}
})
.unwrap_or_else(|| "/".to_owned());
let uri = Uri::parse(&destination)
.map(|x| x.into_owned())
.map_err(|_| render!(session::login(
&(&*conn, &intl.catalog, None),
None,
&*form,
errors
)))?;
.map_err(|_| {
render!(session::login(
&(&*conn, &intl.catalog, None),
None,
&*form,
errors
))
})?;
Ok(Redirect::to(uri))
} else {
@@ -140,13 +158,15 @@ pub fn password_reset_request(
intl: I18n,
mail: State<Arc<Mutex<Mailer>>>,
form: Form<ResetForm>,
requests: State<Arc<Mutex<Vec<ResetRequest>>>>
requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
) -> Ructe {
let mut requests = requests.lock().unwrap();
// Remove outdated requests (more than 1 day old) to avoid the list to grow too much
requests.retain(|r| r.creation_date.elapsed().as_secs() < 24 * 60 * 60);
if User::find_by_email(&*conn, &form.email).is_ok() && !requests.iter().any(|x| x.mail == form.email.clone()) {
if User::find_by_email(&*conn, &form.email).is_ok()
&& !requests.iter().any(|x| x.mail == form.email.clone())
{
let id = plume_common::utils::random_hex();
requests.push(ResetRequest {
@@ -159,22 +179,35 @@ pub fn password_reset_request(
if let Some(message) = build_mail(
form.email.clone(),
i18n!(intl.catalog, "Password reset"),
i18n!(intl.catalog, "Here is the link to reset your password: {0}"; link)
i18n!(intl.catalog, "Here is the link to reset your password: {0}"; link),
) {
if let Some(ref mut mail) = *mail.lock().unwrap() {
mail
.send(message.into())
.map_err(|_| eprintln!("Couldn't send password reset mail")).ok(); }
mail.send(message.into())
.map_err(|_| eprintln!("Couldn't send password reset mail"))
.ok();
}
}
}
render!(session::password_reset_request_ok(
&(&*conn, &intl.catalog, None)
))
render!(session::password_reset_request_ok(&(
&*conn,
&intl.catalog,
None
)))
}
#[get("/password-reset/<token>")]
pub fn password_reset_form(conn: DbConn, intl: I18n, token: String, requests: State<Arc<Mutex<Vec<ResetRequest>>>>) -> Result<Ructe, ErrorPage> {
requests.lock().unwrap().iter().find(|x| x.id == token.clone()).ok_or(Error::NotFound)?;
pub fn password_reset_form(
conn: DbConn,
intl: I18n,
token: String,
requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
) -> Result<Ructe, ErrorPage> {
requests
.lock()
.unwrap()
.iter()
.find(|x| x.id == token.clone())
.ok_or(Error::NotFound)?;
Ok(render!(session::password_reset(
&(&*conn, &intl.catalog, None),
&NewPasswordForm::default(),
@@ -183,13 +216,11 @@ pub fn password_reset_form(conn: DbConn, intl: I18n, token: String, requests: St
}
#[derive(FromForm, Default, Validate)]
#[validate(
schema(
function = "passwords_match",
skip_on_field_errors = "false",
message = "Passwords are not matching"
)
)]
#[validate(schema(
function = "passwords_match",
skip_on_field_errors = "false",
message = "Passwords are not matching"
))]
pub struct NewPasswordForm {
pub password: String,
pub password_confirmation: String,
@@ -209,19 +240,28 @@ pub fn password_reset(
intl: I18n,
token: String,
requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
form: Form<NewPasswordForm>
form: Form<NewPasswordForm>,
) -> Result<Redirect, Ructe> {
form.validate()
.and_then(|_| {
let mut requests = requests.lock().unwrap();
let req = requests.iter().find(|x| x.id == token.clone()).ok_or_else(|| to_validation(0))?.clone();
if req.creation_date.elapsed().as_secs() < 60 * 60 * 2 { // Reset link is only valid for 2 hours
let req = requests
.iter()
.find(|x| x.id == token.clone())
.ok_or_else(|| to_validation(0))?
.clone();
if req.creation_date.elapsed().as_secs() < 60 * 60 * 2 {
// Reset link is only valid for 2 hours
requests.retain(|r| *r != req);
let user = User::find_by_email(&*conn, &req.mail).map_err(to_validation)?;
user.reset_password(&*conn, &form.password).ok();
Ok(Redirect::to(uri!(new: m = i18n!(intl.catalog, "Your password was successfully reset."))))
Ok(Redirect::to(uri!(
new: m = i18n!(intl.catalog, "Your password was successfully reset.")
)))
} else {
Ok(Redirect::to(uri!(new: m = i18n!(intl.catalog, "Sorry, but the link expired. Try again"))))
Ok(Redirect::to(uri!(
new: m = i18n!(intl.catalog, "Sorry, but the link expired. Try again")
)))
}
})
.map_err(|err| {
@@ -235,10 +275,13 @@ pub fn password_reset(
fn to_validation<T>(_: T) -> ValidationErrors {
let mut errors = ValidationErrors::new();
errors.add("", ValidationError {
code: Cow::from("server_error"),
message: Some(Cow::from("An unknown error occured")),
params: std::collections::HashMap::new()
});
errors.add(
"",
ValidationError {
code: Cow::from("server_error"),
message: Some(Cow::from("An unknown error occured")),
params: std::collections::HashMap::new(),
},
);
errors
}
+9 -7
View File
@@ -1,15 +1,17 @@
use rocket_i18n::I18n;
use plume_models::{
db_conn::DbConn,
posts::Post,
users::User,
};
use routes::{Page, errors::ErrorPage};
use plume_models::{db_conn::DbConn, posts::Post, users::User};
use routes::{errors::ErrorPage, Page};
use template_utils::Ructe;
#[get("/tag/<name>?<page>")]
pub fn tag(user: Option<User>, conn: DbConn, name: String, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
pub fn tag(
user: Option<User>,
conn: DbConn,
name: String,
page: Option<Page>,
intl: I18n,
) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default();
let posts = Post::list_by_tag(&*conn, name.clone(), page.limits())?;
Ok(render!(tags::index(
+159 -80
View File
@@ -19,14 +19,20 @@ use plume_common::activity_pub::{
};
use plume_common::utils;
use plume_models::{
blogs::Blog,
db_conn::DbConn,
follows,
headers::Headers,
instance::Instance,
posts::{LicensedArticle, Post},
reshares::Reshare,
users::*,
Error,
blogs::Blog, db_conn::DbConn, follows, headers::Headers, instance::Instance, posts::{LicensedArticle, Post},
reshares::Reshare, users::*,
};
use routes::{Page, PlumeRocket, errors::ErrorPage};
use routes::{errors::ErrorPage, Page, PlumeRocket};
use template_utils::Ructe;
use Worker;
use Searcher;
use Worker;
#[get("/me")]
pub fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
@@ -56,19 +62,21 @@ pub fn details(
let user_clone = user.clone();
let searcher = searcher.clone();
worker.execute(move || {
for create_act in user_clone.fetch_outbox::<Create>().expect("Remote user: outbox couldn't be fetched") {
for create_act in user_clone
.fetch_outbox::<Create>()
.expect("Remote user: outbox couldn't be fetched")
{
match create_act.create_props.object_object::<LicensedArticle>() {
Ok(article) => {
Post::from_activity(
&(&*fetch_articles_conn, &searcher),
article,
user_clone.clone().into_id(),
).expect("Article from remote user couldn't be saved");
)
.expect("Article from remote user couldn't be saved");
println!("Fetched article from remote user");
}
Err(e) => {
println!("Error while fetching articles in background: {:?}", e)
}
Err(e) => println!("Error while fetching articles in background: {:?}", e),
}
}
});
@@ -76,8 +84,12 @@ pub fn details(
// Fetch followers
let user_clone = user.clone();
worker.execute(move || {
for user_id in user_clone.fetch_followers_ids().expect("Remote user: fetching followers error") {
let follower = User::from_url(&*fetch_followers_conn, &user_id).expect("user::details: Couldn't fetch follower");
for user_id in user_clone
.fetch_followers_ids()
.expect("Remote user: fetching followers error")
{
let follower = User::from_url(&*fetch_followers_conn, &user_id)
.expect("user::details: Couldn't fetch follower");
follows::Follow::insert(
&*fetch_followers_conn,
follows::NewFollow {
@@ -85,7 +97,8 @@ pub fn details(
following_id: user_clone.id,
ap_url: String::new(),
},
).expect("Couldn't save follower for remote user");
)
.expect("Couldn't save follower for remote user");
}
});
@@ -93,7 +106,9 @@ pub fn details(
let user_clone = user.clone();
if user.needs_update() {
worker.execute(move || {
user_clone.refetch(&*update_conn).expect("Couldn't update user info");
user_clone
.refetch(&*update_conn)
.expect("Couldn't update user info");
});
}
}
@@ -103,11 +118,16 @@ pub fn details(
Ok(render!(users::details(
&(&*conn, &intl.catalog, account.clone()),
user.clone(),
account.and_then(|x| x.is_following(&*conn, user.id).ok()).unwrap_or(false),
account
.and_then(|x| x.is_following(&*conn, user.id).ok())
.unwrap_or(false),
user.instance_id != Instance::get_local(&*conn)?.id,
user.get_instance(&*conn)?.public_domain,
recents,
reshares.into_iter().filter_map(|r| r.get_post(&*conn).ok()).collect()
reshares
.into_iter()
.filter_map(|r| r.get_post(&*conn).ok())
.collect()
)))
}
@@ -124,19 +144,25 @@ pub fn dashboard(user: User, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPag
#[get("/dashboard", rank = 2)]
pub fn dashboard_auth(i18n: I18n) -> Flash<Redirect> {
utils::requires_login(
&i18n!(i18n.catalog, "You need to be logged in order to access your dashboard"),
&i18n!(
i18n.catalog,
"You need to be logged in order to access your dashboard"
),
uri!(dashboard),
)
}
#[post("/@/<name>/follow")]
pub fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Result<Redirect, ErrorPage> {
pub fn follow(
name: String,
conn: DbConn,
user: User,
worker: Worker,
) -> Result<Redirect, ErrorPage> {
let target = User::find_by_fqn(&*conn, &name)?;
if let Ok(follow) = follows::Follow::find(&*conn, user.id, target.id) {
let delete_act = follow.delete(&*conn)?;
worker.execute(move || {
broadcast(&user, delete_act, vec![target])
});
worker.execute(move || broadcast(&user, delete_act, vec![target]));
} else {
let f = follows::Follow::insert(
&*conn,
@@ -157,13 +183,22 @@ pub fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Result<
#[post("/@/<name>/follow", rank = 2)]
pub fn follow_auth(name: String, i18n: I18n) -> Flash<Redirect> {
utils::requires_login(
&i18n!(i18n.catalog, "You need to be logged in order to subscribe to someone"),
&i18n!(
i18n.catalog,
"You need to be logged in order to subscribe to someone"
),
uri!(follow: name = name),
)
}
#[get("/@/<name>/followers?<page>", rank = 2)]
pub fn followers(name: String, conn: DbConn, account: Option<User>, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
pub fn followers(
name: String,
conn: DbConn,
account: Option<User>,
page: Option<Page>,
intl: I18n,
) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default();
let user = User::find_by_fqn(&*conn, &name)?;
let followers_count = user.count_followers(&*conn)?;
@@ -171,7 +206,9 @@ pub fn followers(name: String, conn: DbConn, account: Option<User>, page: Option
Ok(render!(users::followers(
&(&*conn, &intl.catalog, account.clone()),
user.clone(),
account.and_then(|x| x.is_following(&*conn, user.id).ok()).unwrap_or(false),
account
.and_then(|x| x.is_following(&*conn, user.id).ok())
.unwrap_or(false),
user.instance_id != Instance::get_local(&*conn)?.id,
user.get_instance(&*conn)?.public_domain,
user.get_followers_page(&*conn, page.limits())?,
@@ -181,7 +218,13 @@ pub fn followers(name: String, conn: DbConn, account: Option<User>, page: Option
}
#[get("/@/<name>/followed?<page>", rank = 2)]
pub fn followed(name: String, conn: DbConn, account: Option<User>, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
pub fn followed(
name: String,
conn: DbConn,
account: Option<User>,
page: Option<Page>,
intl: I18n,
) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default();
let user = User::find_by_fqn(&*conn, &name)?;
let followed_count = user.count_followed(&*conn)?;
@@ -189,7 +232,9 @@ pub fn followed(name: String, conn: DbConn, account: Option<User>, page: Option<
Ok(render!(users::followed(
&(&*conn, &intl.catalog, account.clone()),
user.clone(),
account.and_then(|x| x.is_following(&*conn, user.id).ok()).unwrap_or(false),
account
.and_then(|x| x.is_following(&*conn, user.id).ok())
.unwrap_or(false),
user.instance_id != Instance::get_local(&*conn)?.id,
user.get_instance(&*conn)?.public_domain,
user.get_followed_page(&*conn, page.limits())?,
@@ -238,7 +283,10 @@ pub fn edit(name: String, user: User, conn: DbConn, intl: I18n) -> Result<Ructe,
#[get("/@/<name>/edit", rank = 2)]
pub fn edit_auth(name: String, i18n: I18n) -> Flash<Redirect> {
utils::requires_login(
&i18n!(i18n.catalog, "You need to be logged in order to edit your profile"),
&i18n!(
i18n.catalog,
"You need to be logged in order to edit your profile"
),
uri!(edit: name = name),
)
}
@@ -251,18 +299,41 @@ pub struct UpdateUserForm {
}
#[put("/@/<_name>/edit", data = "<form>")]
pub fn update(_name: String, conn: DbConn, user: User, form: LenientForm<UpdateUserForm>) -> Result<Redirect, ErrorPage> {
pub fn update(
_name: String,
conn: DbConn,
user: User,
form: LenientForm<UpdateUserForm>,
) -> Result<Redirect, ErrorPage> {
user.update(
&*conn,
if !form.display_name.is_empty() { form.display_name.clone() } else { user.display_name.clone() },
if !form.email.is_empty() { form.email.clone() } else { user.email.clone().unwrap_or_default() },
if !form.summary.is_empty() { form.summary.clone() } else { user.summary.to_string() },
if !form.display_name.is_empty() {
form.display_name.clone()
} else {
user.display_name.clone()
},
if !form.email.is_empty() {
form.email.clone()
} else {
user.email.clone().unwrap_or_default()
},
if !form.summary.is_empty() {
form.summary.clone()
} else {
user.summary.to_string()
},
)?;
Ok(Redirect::to(uri!(me)))
}
#[post("/@/<name>/delete")]
pub fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies, searcher: Searcher) -> Result<Redirect, ErrorPage> {
pub fn delete(
name: String,
conn: DbConn,
user: User,
mut cookies: Cookies,
searcher: Searcher,
) -> Result<Redirect, ErrorPage> {
let account = User::find_by_fqn(&*conn, &name)?;
if user.id == account.id {
account.delete(&*conn, &searcher)?;
@@ -278,32 +349,25 @@ pub fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies, sear
}
#[derive(Default, FromForm, Validate)]
#[validate(
schema(
function = "passwords_match",
skip_on_field_errors = "false",
message = "Passwords are not matching"
)
)]
#[validate(schema(
function = "passwords_match",
skip_on_field_errors = "false",
message = "Passwords are not matching"
))]
pub struct NewUserForm {
#[validate(length(min = "1", message = "Username can't be empty"),
custom( function = "validate_username", message = "User name is not allowed to contain any of < > & @ ' or \""))]
#[validate(
length(min = "1", message = "Username can't be empty"),
custom(
function = "validate_username",
message = "User name is not allowed to contain any of < > & @ ' or \""
)
)]
pub username: String,
#[validate(email(message = "Invalid email"))]
pub email: String,
#[validate(
length(
min = "8",
message = "Password should be at least 8 characters long"
)
)]
#[validate(length(min = "8", message = "Password should be at least 8 characters long"))]
pub password: String,
#[validate(
length(
min = "8",
message = "Password should be at least 8 characters long"
)
)]
#[validate(length(min = "8", message = "Password should be at least 8 characters long"))]
pub password_confirmation: String,
}
@@ -325,17 +389,20 @@ pub fn validate_username(username: &str) -> Result<(), ValidationError> {
fn to_validation(_: Error) -> ValidationErrors {
let mut errors = ValidationErrors::new();
errors.add("", ValidationError {
code: Cow::from("server_error"),
message: Some(Cow::from("An unknown error occured")),
params: HashMap::new()
});
errors.add(
"",
ValidationError {
code: Cow::from("server_error"),
message: Some(Cow::from("An unknown error occured")),
params: HashMap::new(),
},
);
errors
}
#[post("/users/new", data = "<form>")]
pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Result<Redirect, Ructe> {
if !Instance::get_local(&*conn)
if !Instance::get_local(&*conn)
.map(|i| i.open_registrations)
.unwrap_or(true)
{
@@ -355,13 +422,16 @@ pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Resul
"",
form.email.to_string(),
User::hash_pass(&form.password).map_err(to_validation)?,
).map_err(to_validation)?;
)
.map_err(to_validation)?;
Ok(Redirect::to(uri!(super::session::new: m = _)))
})
.map_err(|err| {
.map_err(|err| {
render!(users::new(
&(&*conn, &intl.catalog, None),
Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true),
Instance::get_local(&*conn)
.map(|i| i.open_registrations)
.unwrap_or(true),
&form,
err
))
@@ -395,21 +465,27 @@ pub fn inbox(
))))?;
let actor = User::from_url(&conn, actor_id).expect("user::inbox: user error");
if !verify_http_headers(&actor, &headers.0, &sig).is_secure()
&& !act.clone().verify(&actor)
{
if !verify_http_headers(&actor, &headers.0, &sig).is_secure() && !act.clone().verify(&actor) {
// maybe we just know an old key?
actor.refetch(&conn).and_then(|_| User::get(&conn, actor.id))
.and_then(|actor| if verify_http_headers(&actor, &headers.0, &sig).is_secure()
|| act.clone().verify(&actor)
{
Ok(())
} else {
Err(Error::Signature)
})
actor
.refetch(&conn)
.and_then(|_| User::get(&conn, actor.id))
.and_then(|actor| {
if verify_http_headers(&actor, &headers.0, &sig).is_secure()
|| act.clone().verify(&actor)
{
Ok(())
} else {
Err(Error::Signature)
}
})
.map_err(|_| {
println!("Rejected invalid activity supposedly from {}, with headers {:?}", actor.username, headers.0);
status::BadRequest(Some("Invalid signature"))})?;
println!(
"Rejected invalid activity supposedly from {}, with headers {:?}",
actor.username, headers.0
);
status::BadRequest(Some("Invalid signature"))
})?;
}
if Instance::is_blocked(&*conn, actor_id).map_err(|_| None)? {
@@ -432,18 +508,20 @@ pub fn ap_followers(
) -> Option<ActivityStream<OrderedCollection>> {
let user = User::find_by_fqn(&*conn, &name).ok()?;
let followers = user
.get_followers(&*conn).ok()?
.get_followers(&*conn)
.ok()?
.into_iter()
.map(|f| Id::new(f.ap_url))
.collect::<Vec<Id>>();
let mut coll = OrderedCollection::default();
coll.object_props
.set_id_string(user.followers_endpoint).ok()?;
.set_id_string(user.followers_endpoint)
.ok()?;
coll.collection_props
.set_total_items_u64(followers.len() as u64).ok()?;
coll.collection_props
.set_items_link_vec(followers).ok()?;
.set_total_items_u64(followers.len() as u64)
.ok()?;
coll.collection_props.set_items_link_vec(followers).ok()?;
Some(ActivityStream::new(coll))
}
@@ -456,7 +534,8 @@ pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
.unwrap()
.compute_box("~", &name, "atom.xml"))
.entries(
Post::get_recents_for_author(&*conn, &author, 15).ok()?
Post::get_recents_for_author(&*conn, &author, 15)
.ok()?
.into_iter()
.map(|p| super::post_to_atom(p, &*conn))
.collect::<Vec<Entry>>(),
+45 -24
View File
@@ -3,32 +3,42 @@ use rocket::response::Content;
use serde_json;
use webfinger::*;
use plume_models::{BASE_URL, ap_url, db_conn::DbConn, blogs::Blog, users::User};
use plume_models::{ap_url, blogs::Blog, db_conn::DbConn, users::User, BASE_URL};
#[get("/.well-known/nodeinfo")]
pub fn nodeinfo() -> Content<String> {
Content(ContentType::new("application", "jrd+json"), json!({
"links": [
{
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
"href": ap_url(&format!("{domain}/nodeinfo/2.0", domain = BASE_URL.as_str()))
},
{
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.1",
"href": ap_url(&format!("{domain}/nodeinfo/2.1", domain = BASE_URL.as_str()))
}
]
}).to_string())
Content(
ContentType::new("application", "jrd+json"),
json!({
"links": [
{
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
"href": ap_url(&format!("{domain}/nodeinfo/2.0", domain = BASE_URL.as_str()))
},
{
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.1",
"href": ap_url(&format!("{domain}/nodeinfo/2.1", domain = BASE_URL.as_str()))
}
]
})
.to_string(),
)
}
#[get("/.well-known/host-meta")]
pub fn host_meta() -> String {
format!(r#"
format!(
r#"
<?xml version="1.0"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
<Link rel="lrdd" type="application/xrd+xml" template="{url}"/>
</XRD>
"#, url = ap_url(&format!("{domain}/.well-known/webfinger?resource={{uri}}", domain = BASE_URL.as_str())))
"#,
url = ap_url(&format!(
"{domain}/.well-known/webfinger?resource={{uri}}",
domain = BASE_URL.as_str()
))
)
}
struct WebfingerResolver;
@@ -41,20 +51,31 @@ impl Resolver<DbConn> for WebfingerResolver {
fn find(acct: String, conn: DbConn) -> Result<Webfinger, ResolverError> {
User::find_by_fqn(&*conn, &acct)
.and_then(|usr| usr.webfinger(&*conn))
.or_else(|_| Blog::find_by_fqn(&*conn, &acct)
.and_then(|blog| blog.webfinger(&*conn))
.or(Err(ResolverError::NotFound)))
.or_else(|_| {
Blog::find_by_fqn(&*conn, &acct)
.and_then(|blog| blog.webfinger(&*conn))
.or(Err(ResolverError::NotFound))
})
}
}
#[get("/.well-known/webfinger?<resource>")]
pub fn webfinger(resource: String, conn: DbConn) -> Content<String> {
match WebfingerResolver::endpoint(resource, conn).and_then(|wf| serde_json::to_string(&wf).map_err(|_| ResolverError::NotFound)) {
match WebfingerResolver::endpoint(resource, conn)
.and_then(|wf| serde_json::to_string(&wf).map_err(|_| ResolverError::NotFound))
{
Ok(wf) => Content(ContentType::new("application", "jrd+json"), wf),
Err(err) => Content(ContentType::new("text", "plain"), String::from(match err {
ResolverError::InvalidResource => "Invalid resource. Make sure to request an acct: URI",
ResolverError::NotFound => "Requested resource was not found",
ResolverError::WrongInstance => "This is not the instance of the requested resource"
}))
Err(err) => Content(
ContentType::new("text", "plain"),
String::from(match err {
ResolverError::InvalidResource => {
"Invalid resource. Make sure to request an acct: URI"
}
ResolverError::NotFound => "Requested resource was not found",
ResolverError::WrongInstance => {
"This is not the instance of the requested resource"
}
}),
),
}
}
+152 -64
View File
@@ -1,9 +1,9 @@
use plume_models::{Connection, notifications::*, users::User};
use plume_models::{notifications::*, users::User, Connection};
use rocket::http::{Method, Status};
use rocket::http::hyper::header::{ETag, EntityTag};
use rocket::http::{Method, Status};
use rocket::request::Request;
use rocket::response::{self, Response, Responder, content::Html as HtmlCt};
use rocket::response::{self, content::Html as HtmlCt, Responder, Response};
use rocket_i18n::Catalog;
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
@@ -13,7 +13,7 @@ pub use askama_escape::escape;
pub static CACHE_NAME: &str = env!("CACHE_ID");
pub type BaseContext<'a> = &'a(&'a Connection, &'a Catalog, Option<User>);
pub type BaseContext<'a> = &'a (&'a Connection, &'a Catalog, Option<User>);
#[derive(Debug)]
pub struct Ructe(pub Vec<u8>);
@@ -27,7 +27,10 @@ impl<'r> Responder<'r> for Ructe {
let mut hasher = DefaultHasher::new();
hasher.write(&self.0);
let etag = format!("{:x}", hasher.finish());
if r.headers().get("If-None-Match").any(|s| s[1..s.len()-1] == etag) {
if r.headers()
.get("If-None-Match")
.any(|s| s[1..s.len() - 1] == etag)
{
Response::build()
.status(Status::NotModified)
.header(ETag(EntityTag::strong(etag)))
@@ -85,7 +88,13 @@ impl Size {
}
}
pub fn avatar(conn: &Connection, user: &User, size: Size, pad: bool, catalog: &Catalog) -> Html<String> {
pub fn avatar(
conn: &Connection,
user: &User,
size: Size,
pad: bool,
catalog: &Catalog,
) -> Html<String> {
let name = escape(&user.name()).to_string();
Html(format!(
r#"<div class="avatar {size} {padded}"
@@ -120,49 +129,82 @@ pub fn tabs(links: &[(&str, String, bool)]) -> Html<String> {
pub fn paginate(catalog: &Catalog, page: i32, total: i32) -> Html<String> {
paginate_param(catalog, page, total, None)
}
pub fn paginate_param(catalog: &Catalog, page: i32, total: i32, param: Option<String>) -> Html<String> {
pub fn paginate_param(
catalog: &Catalog,
page: i32,
total: i32,
param: Option<String>,
) -> Html<String> {
let mut res = String::new();
let param = param.map(|mut p| {p.push('&'); p}).unwrap_or_default();
let param = param
.map(|mut p| {
p.push('&');
p
})
.unwrap_or_default();
res.push_str(r#"<div class="pagination">"#);
if page != 1 {
res.push_str(format!(r#"<a href="?{}page={}">{}</a>"#, param, page - 1, catalog.gettext("Previous page")).as_str());
res.push_str(
format!(
r#"<a href="?{}page={}">{}</a>"#,
param,
page - 1,
catalog.gettext("Previous page")
)
.as_str(),
);
}
if page < total {
res.push_str(format!(r#"<a href="?{}page={}">{}</a>"#, param, page + 1, catalog.gettext("Next page")).as_str());
res.push_str(
format!(
r#"<a href="?{}page={}">{}</a>"#,
param,
page + 1,
catalog.gettext("Next page")
)
.as_str(),
);
}
res.push_str("</div>");
Html(res)
}
pub fn encode_query_param(param: &str) -> String {
param.chars().map(|c| match c {
'+' => Ok("%2B"),
' ' => Err('+'),
c => Err(c),
}).fold(String::new(), |mut s,r| {
match r {
Ok(r) => s.push_str(r),
Err(r) => s.push(r),
};
s
})
param
.chars()
.map(|c| match c {
'+' => Ok("%2B"),
' ' => Err('+'),
c => Err(c),
})
.fold(String::new(), |mut s, r| {
match r {
Ok(r) => s.push_str(r),
Err(r) => s.push(r),
};
s
})
}
#[macro_export]
macro_rules! icon {
($name:expr) => {
Html(concat!(r#"<svg class="feather"><use xlink:href="/static/images/feather-sprite.svg#"#, $name, "\"/></svg>"))
}
Html(concat!(
r#"<svg class="feather"><use xlink:href="/static/images/feather-sprite.svg#"#,
$name,
"\"/></svg>"
))
};
}
macro_rules! input {
($catalog:expr, $name:tt ($kind:tt), $label:expr, $optional:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {
{
use validator::ValidationErrorsKind;
use std::borrow::Cow;
let cat = $catalog;
($catalog:expr, $name:tt ($kind:tt), $label:expr, $optional:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {{
use std::borrow::Cow;
use validator::ValidationErrorsKind;
let cat = $catalog;
Html(format!(r#"
Html(format!(
r#"
<label for="{name}">
{label}
{optional}
@@ -171,52 +213,98 @@ macro_rules! input {
{error}
<input type="{kind}" id="{name}" name="{name}" value="{val}" {props}/>
"#,
name = stringify!($name),
label = i18n!(cat, $label),
kind = stringify!($kind),
optional = if $optional { format!("<small>{}</small>", i18n!(cat, "Optional")) } else { String::new() },
details = if $details.len() > 0 {
format!("<small>{}</small>", i18n!(cat, $details))
} else {
String::new()
},
error = if let Some(ValidationErrorsKind::Field(errs)) = $err.errors().get(stringify!($name)) {
format!(r#"<p class="error">{}</p>"#, errs[0].message.clone().unwrap_or(Cow::from("Unknown error")))
} else {
String::new()
},
val = escape(&$form.$name),
props = $props
))
}
};
name = stringify!($name),
label = i18n!(cat, $label),
kind = stringify!($kind),
optional = if $optional {
format!("<small>{}</small>", i18n!(cat, "Optional"))
} else {
String::new()
},
details = if $details.len() > 0 {
format!("<small>{}</small>", i18n!(cat, $details))
} else {
String::new()
},
error = if let Some(ValidationErrorsKind::Field(errs)) =
$err.errors().get(stringify!($name))
{
format!(
r#"<p class="error">{}</p>"#,
errs[0]
.message
.clone()
.unwrap_or(Cow::from("Unknown error"))
)
} else {
String::new()
},
val = escape(&$form.$name),
props = $props
))
}};
($catalog:expr, $name:tt (optional $kind:tt), $label:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {
input!($catalog, $name ($kind), $label, true, $details, $form, $err, $props)
input!(
$catalog,
$name($kind),
$label,
true,
$details,
$form,
$err,
$props
)
};
($catalog:expr, $name:tt (optional $kind:tt), $label:expr, $form:expr, $err:expr, $props:expr) => {
input!($catalog, $name ($kind), $label, true, "", $form, $err, $props)
input!(
$catalog,
$name($kind),
$label,
true,
"",
$form,
$err,
$props
)
};
($catalog:expr, $name:tt ($kind:tt), $label:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {
input!($catalog, $name ($kind), $label, false, $details, $form, $err, $props)
input!(
$catalog,
$name($kind),
$label,
false,
$details,
$form,
$err,
$props
)
};
($catalog:expr, $name:tt ($kind:tt), $label:expr, $form:expr, $err:expr, $props:expr) => {
input!($catalog, $name ($kind), $label, false, "", $form, $err, $props)
input!(
$catalog,
$name($kind),
$label,
false,
"",
$form,
$err,
$props
)
};
($catalog:expr, $name:tt ($kind:tt), $label:expr, $form:expr, $err:expr) => {
input!($catalog, $name ($kind), $label, false, "", $form, $err, "")
input!($catalog, $name($kind), $label, false, "", $form, $err, "")
};
($catalog:expr, $name:tt ($kind:tt), $label:expr, $props:expr) => {
{
let cat = $catalog;
Html(format!(r#"
($catalog:expr, $name:tt ($kind:tt), $label:expr, $props:expr) => {{
let cat = $catalog;
Html(format!(
r#"
<label for="{name}">{label}</label>
<input type="{kind}" id="{name}" name="{name}" {props}/>
"#,
name = stringify!($name),
label = i18n!(cat, $label),
kind = stringify!($kind),
props = $props
))
}
};
name = stringify!($name),
label = i18n!(cat, $label),
kind = stringify!($kind),
props = $props
))
}};
}