Run 'cargo fmt' to format code (#489)
This commit is contained in:
committed by
Baptiste Gelez
parent
732f514da7
commit
b945d1f602
+1
-5
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -2,25 +2,21 @@
|
||||
use atom_syndication::{ContentBuilder, Entry, EntryBuilder, LinkBuilder, Person, PersonBuilder};
|
||||
use rocket::{
|
||||
http::{
|
||||
RawStr, Status, uri::{FromUriParam, Query},
|
||||
hyper::header::{CacheControl, CacheDirective}
|
||||
hyper::header::{CacheControl, CacheDirective},
|
||||
uri::{FromUriParam, Query},
|
||||
RawStr, Status,
|
||||
},
|
||||
Outcome,
|
||||
request::{self, FromFormValue, FromRequest, Request},
|
||||
response::NamedFile,
|
||||
Outcome,
|
||||
};
|
||||
use rocket_i18n::I18n;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use plume_models::{
|
||||
Connection,
|
||||
users::User,
|
||||
posts::Post,
|
||||
db_conn::DbConn,
|
||||
};
|
||||
use plume_models::{db_conn::DbConn, posts::Post, users::User, Connection};
|
||||
|
||||
use Worker;
|
||||
use Searcher;
|
||||
use Worker;
|
||||
|
||||
pub struct PlumeRocket<'a> {
|
||||
conn: DbConn,
|
||||
@@ -100,7 +96,6 @@ impl<'a, 'r> FromRequest<'a, 'r> for ContentLen {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Default for Page {
|
||||
fn default() -> Self {
|
||||
Page(1)
|
||||
@@ -110,20 +105,33 @@ impl Default for Page {
|
||||
pub fn post_to_atom(post: Post, conn: &Connection) -> Entry {
|
||||
EntryBuilder::default()
|
||||
.title(format!("<![CDATA[{}]]>", post.title))
|
||||
.content(ContentBuilder::default()
|
||||
.value(format!("<![CDATA[{}]]>", *post.content.get()))
|
||||
.src(post.ap_url.clone())
|
||||
.content_type("html".to_string())
|
||||
.build().expect("Atom feed: content error"))
|
||||
.authors(post.get_authors(&*conn).expect("Atom feed: author error")
|
||||
.into_iter()
|
||||
.map(|a| PersonBuilder::default()
|
||||
.name(a.display_name)
|
||||
.uri(a.ap_url)
|
||||
.build().expect("Atom feed: author error"))
|
||||
.collect::<Vec<Person>>())
|
||||
.links(vec![LinkBuilder::default().href(post.ap_url).build().expect("Atom feed: link error")])
|
||||
.build().expect("Atom feed: entry error")
|
||||
.content(
|
||||
ContentBuilder::default()
|
||||
.value(format!("<![CDATA[{}]]>", *post.content.get()))
|
||||
.src(post.ap_url.clone())
|
||||
.content_type("html".to_string())
|
||||
.build()
|
||||
.expect("Atom feed: content error"),
|
||||
)
|
||||
.authors(
|
||||
post.get_authors(&*conn)
|
||||
.expect("Atom feed: author error")
|
||||
.into_iter()
|
||||
.map(|a| {
|
||||
PersonBuilder::default()
|
||||
.name(a.display_name)
|
||||
.uri(a.ap_url)
|
||||
.build()
|
||||
.expect("Atom feed: author error")
|
||||
})
|
||||
.collect::<Vec<Person>>(),
|
||||
)
|
||||
.links(vec![LinkBuilder::default()
|
||||
.href(post.ap_url)
|
||||
.build()
|
||||
.expect("Atom feed: link error")])
|
||||
.build()
|
||||
.expect("Atom feed: entry error")
|
||||
}
|
||||
|
||||
pub mod blogs;
|
||||
@@ -135,17 +143,17 @@ pub mod medias;
|
||||
pub mod notifications;
|
||||
pub mod posts;
|
||||
pub mod reshares;
|
||||
pub mod search;
|
||||
pub mod session;
|
||||
pub mod tags;
|
||||
pub mod user;
|
||||
pub mod search;
|
||||
pub mod well_known;
|
||||
|
||||
#[derive(Responder)]
|
||||
#[response()]
|
||||
pub struct CachedFile {
|
||||
inner: NamedFile,
|
||||
cache_control: CacheControl
|
||||
cache_control: CacheControl,
|
||||
}
|
||||
|
||||
#[get("/static/cached/<_build_id>/<file..>", rank = 2)]
|
||||
@@ -155,10 +163,10 @@ pub fn plume_static_files(file: PathBuf, _build_id: &RawStr) -> Option<CachedFil
|
||||
|
||||
#[get("/static/<file..>", rank = 3)]
|
||||
pub fn static_files(file: PathBuf) -> Option<CachedFile> {
|
||||
NamedFile::open(Path::new("static/").join(file)).ok()
|
||||
.map(|f|
|
||||
CachedFile {
|
||||
inner: f,
|
||||
cache_control: CacheControl(vec![CacheDirective::MaxAge(60*60*24*30)])
|
||||
})
|
||||
NamedFile::open(Path::new("static/").join(file))
|
||||
.ok()
|
||||
.map(|f| CachedFile {
|
||||
inner: f,
|
||||
cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
use rocket::response::{Redirect, Flash};
|
||||
use rocket::response::{Flash, Redirect};
|
||||
use rocket_i18n::I18n;
|
||||
|
||||
use plume_common::utils;
|
||||
use plume_models::{db_conn::DbConn, notifications::Notification, users::User};
|
||||
use routes::{Page, errors::ErrorPage};
|
||||
use routes::{errors::ErrorPage, Page};
|
||||
use template_utils::Ructe;
|
||||
|
||||
#[get("/notifications?<page>")]
|
||||
pub fn notifications(conn: DbConn, user: User, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
|
||||
pub fn notifications(
|
||||
conn: DbConn,
|
||||
user: User,
|
||||
page: Option<Page>,
|
||||
intl: I18n,
|
||||
) -> Result<Ructe, ErrorPage> {
|
||||
let page = page.unwrap_or_default();
|
||||
Ok(render!(notifications::index(
|
||||
&(&*conn, &intl.catalog, Some(user.clone())),
|
||||
@@ -18,9 +23,12 @@ pub fn notifications(conn: DbConn, user: User, page: Option<Page>, intl: I18n) -
|
||||
}
|
||||
|
||||
#[get("/notifications?<page>", rank = 2)]
|
||||
pub fn notifications_auth(i18n: I18n, page: Option<Page>) -> Flash<Redirect>{
|
||||
pub fn notifications_auth(i18n: I18n, page: Option<Page>) -> Flash<Redirect> {
|
||||
utils::requires_login(
|
||||
&i18n!(i18n.catalog, "You need to be logged in order to see your notifications"),
|
||||
uri!(notifications: page = page)
|
||||
&i18n!(
|
||||
i18n.catalog,
|
||||
"You need to be logged in order to see your notifications"
|
||||
),
|
||||
uri!(notifications: page = page),
|
||||
)
|
||||
}
|
||||
|
||||
+219
-90
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
))
|
||||
}};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user