Use Ructe (#327)
All the template are now compiled at compile-time with the `ructe` crate.
I preferred to use it instead of askama because it allows more complex Rust expressions, where askama only supports a small subset of expressions and doesn't allow them everywhere (for instance, `{{ macro!() | filter }}` would result in a parsing error).
The diff is quite huge, but there is normally no changes in functionality.
Fixes #161 and unblocks #110 and #273
This commit is contained in:
+2
-2
@@ -1,5 +1,5 @@
|
||||
use canapi::Provider;
|
||||
use rocket_contrib::Json;
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json;
|
||||
|
||||
use plume_api::apps::AppEndpoint;
|
||||
@@ -10,7 +10,7 @@ use plume_models::{
|
||||
};
|
||||
|
||||
#[post("/apps", data = "<data>")]
|
||||
fn create(conn: DbConn, data: Json<AppEndpoint>) -> Json<serde_json::Value> {
|
||||
pub fn create(conn: DbConn, data: Json<AppEndpoint>) -> Json<serde_json::Value> {
|
||||
let post = <App as Provider<Connection>>::create(&*conn, (*data).clone()).ok();
|
||||
Json(json!(post))
|
||||
}
|
||||
|
||||
+6
-5
@@ -1,4 +1,5 @@
|
||||
use rocket_contrib::Json;
|
||||
use rocket::request::Form;
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json;
|
||||
|
||||
use plume_common::utils::random_hex;
|
||||
@@ -10,7 +11,7 @@ use plume_models::{
|
||||
};
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct OAuthRequest {
|
||||
pub struct OAuthRequest {
|
||||
client_id: String,
|
||||
client_secret: String,
|
||||
password: String,
|
||||
@@ -18,8 +19,8 @@ struct OAuthRequest {
|
||||
scopes: String,
|
||||
}
|
||||
|
||||
#[get("/oauth2?<query>")]
|
||||
fn oauth(query: OAuthRequest, conn: DbConn) -> Json<serde_json::Value> {
|
||||
#[get("/oauth2?<query..>")]
|
||||
pub fn oauth(query: Form<OAuthRequest>, conn: DbConn) -> Json<serde_json::Value> {
|
||||
let app = App::find_by_client_id(&*conn, &query.client_id).expect("OAuth request from unknown client");
|
||||
if app.client_secret == query.client_secret {
|
||||
if let Some(user) = User::find_local(&*conn, &query.username) {
|
||||
@@ -28,7 +29,7 @@ fn oauth(query: OAuthRequest, conn: DbConn) -> Json<serde_json::Value> {
|
||||
app_id: app.id,
|
||||
user_id: user.id,
|
||||
value: random_hex(),
|
||||
scopes: query.scopes,
|
||||
scopes: query.scopes.clone(),
|
||||
});
|
||||
Json(json!({
|
||||
"token": token.value
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
use canapi::Provider;
|
||||
use rocket::http::uri::Origin;
|
||||
use rocket_contrib::Json;
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json;
|
||||
use serde_qs;
|
||||
|
||||
@@ -15,13 +15,13 @@ use api::authorization::*;
|
||||
use Searcher;
|
||||
|
||||
#[get("/posts/<id>")]
|
||||
fn get(id: i32, conn: DbConn, auth: Option<Authorization<Read, Post>>, search: Searcher) -> Json<serde_json::Value> {
|
||||
pub fn get(id: i32, conn: DbConn, auth: Option<Authorization<Read, Post>>, search: Searcher) -> Json<serde_json::Value> {
|
||||
let post = <Post as Provider<(&Connection, &UnmanagedSearcher, Option<i32>)>>::get(&(&*conn, &search, auth.map(|a| a.0.user_id)), id).ok();
|
||||
Json(json!(post))
|
||||
}
|
||||
|
||||
#[get("/posts")]
|
||||
fn list(conn: DbConn, uri: &Origin, auth: Option<Authorization<Read, Post>>, search: Searcher) -> Json<serde_json::Value> {
|
||||
pub fn list(conn: DbConn, uri: &Origin, 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, &UnmanagedSearcher, Option<i32>)>>::list(&(&*conn, &search, auth.map(|a| a.0.user_id)), query);
|
||||
Json(json!(post))
|
||||
|
||||
+10
-12
@@ -1,7 +1,7 @@
|
||||
#![feature(custom_derive, plugin, decl_macro)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![feature(custom_derive, plugin, decl_macro, proc_macro_hygiene)]
|
||||
|
||||
extern crate activitypub;
|
||||
extern crate askama_escape;
|
||||
extern crate atom_syndication;
|
||||
extern crate canapi;
|
||||
extern crate chrono;
|
||||
@@ -10,7 +10,6 @@ extern crate ctrlc;
|
||||
extern crate diesel;
|
||||
extern crate dotenv;
|
||||
extern crate failure;
|
||||
extern crate gettextrs;
|
||||
extern crate guid_create;
|
||||
extern crate heck;
|
||||
extern crate multipart;
|
||||
@@ -22,6 +21,7 @@ extern crate plume_models;
|
||||
extern crate rocket;
|
||||
extern crate rocket_contrib;
|
||||
extern crate rocket_csrf;
|
||||
#[macro_use]
|
||||
extern crate rocket_i18n;
|
||||
extern crate rpassword;
|
||||
extern crate scheduled_thread_pool;
|
||||
@@ -38,7 +38,6 @@ extern crate webfinger;
|
||||
|
||||
use diesel::r2d2::ConnectionManager;
|
||||
use rocket::State;
|
||||
use rocket_contrib::Template;
|
||||
use rocket_csrf::CsrfFairingBuilder;
|
||||
use plume_models::{DATABASE_URL, Connection,
|
||||
db_conn::DbPool, search::Searcher as UnmanagedSearcher};
|
||||
@@ -49,6 +48,8 @@ use std::time::Duration;
|
||||
|
||||
mod api;
|
||||
mod inbox;
|
||||
#[macro_use]
|
||||
mod template_utils;
|
||||
mod routes;
|
||||
|
||||
type Worker<'a> = State<'a, ScheduledThreadPool>;
|
||||
@@ -77,7 +78,6 @@ fn main() {
|
||||
exit(0);
|
||||
}).expect("Error setting Ctrl-c handler");
|
||||
|
||||
|
||||
rocket::ignite()
|
||||
.mount("/", routes![
|
||||
routes::blogs::paginated_details,
|
||||
@@ -140,8 +140,7 @@ fn main() {
|
||||
routes::reshares::create,
|
||||
routes::reshares::create_auth,
|
||||
|
||||
routes::search::index,
|
||||
routes::search::query,
|
||||
routes::search::search,
|
||||
|
||||
routes::session::new,
|
||||
routes::session::new_message,
|
||||
@@ -187,17 +186,14 @@ fn main() {
|
||||
api::posts::get,
|
||||
api::posts::list,
|
||||
])
|
||||
.catch(catchers![
|
||||
.register(catchers![
|
||||
routes::errors::not_found,
|
||||
routes::errors::server_error
|
||||
])
|
||||
.manage(dbpool)
|
||||
.manage(workpool)
|
||||
.manage(searcher)
|
||||
.attach(Template::custom(|engines| {
|
||||
rocket_i18n::tera(&mut engines.tera);
|
||||
}))
|
||||
.attach(rocket_i18n::I18n::new("plume"))
|
||||
.manage(include_i18n!("plume", [ "de", "en", "fr", "gl", "it", "ja", "nb", "pl", "ru" ]))
|
||||
.attach(CsrfFairingBuilder::new()
|
||||
.set_default_target("/csrf-violation?target=<uri>".to_owned(), rocket::http::Method::Post)
|
||||
.add_exceptions(vec![
|
||||
@@ -210,3 +206,5 @@ fn main() {
|
||||
.finalize().expect("main: csrf fairing creation error"))
|
||||
.launch();
|
||||
}
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
|
||||
|
||||
+49
-49
@@ -5,8 +5,7 @@ use rocket::{
|
||||
request::LenientForm,
|
||||
response::{Redirect, Flash, content::Content}
|
||||
};
|
||||
use rocket_contrib::Template;
|
||||
use serde_json;
|
||||
use rocket_i18n::I18n;
|
||||
use std::{collections::HashMap, borrow::Cow};
|
||||
use validator::{Validate, ValidationError, ValidationErrors};
|
||||
|
||||
@@ -21,61 +20,62 @@ use plume_models::{
|
||||
users::User
|
||||
};
|
||||
use routes::Page;
|
||||
use template_utils::Ructe;
|
||||
use Searcher;
|
||||
|
||||
#[get("/~/<name>?<page>", rank = 2)]
|
||||
fn paginated_details(name: String, conn: DbConn, user: Option<User>, page: Page) -> Template {
|
||||
may_fail!(user.map(|u| u.to_json(&*conn)), Blog::find_by_fqn(&*conn, &name), "Requested blog couldn't be found", |blog| {
|
||||
let posts = Post::blog_page(&*conn, &blog, page.limits());
|
||||
let articles = Post::get_for_blog(&*conn, &blog);
|
||||
let authors = &blog.list_authors(&*conn);
|
||||
pub fn paginated_details(intl: I18n, name: String, conn: DbConn, user: Option<User>, page: Page) -> Result<Ructe, Ructe> {
|
||||
let blog = Blog::find_by_fqn(&*conn, &name)
|
||||
.ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, user.clone()))))?;
|
||||
let posts = Post::blog_page(&*conn, &blog, page.limits());
|
||||
let articles = Post::get_for_blog(&*conn, &blog); // TODO only count them in DB
|
||||
let authors = &blog.list_authors(&*conn);
|
||||
|
||||
Template::render("blogs/details", json!({
|
||||
"blog": &blog.to_json(&*conn),
|
||||
"account": user.clone().map(|u| u.to_json(&*conn)),
|
||||
"is_author": user.map(|x| x.is_author_in(&*conn, &blog)),
|
||||
"posts": posts.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
|
||||
"authors": authors.into_iter().map(|u| u.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
|
||||
"n_authors": authors.len(),
|
||||
"n_articles": articles.len(),
|
||||
"page": page.page,
|
||||
"n_pages": Page::total(articles.len() as i32)
|
||||
}))
|
||||
})
|
||||
Ok(render!(blogs::details(
|
||||
&(&*conn, &intl.catalog, user.clone()),
|
||||
blog.clone(),
|
||||
blog.get_fqn(&*conn),
|
||||
authors,
|
||||
articles.len(),
|
||||
page.0,
|
||||
Page::total(articles.len() as i32),
|
||||
user.map(|x| x.is_author_in(&*conn, &blog)).unwrap_or(false),
|
||||
posts
|
||||
)))
|
||||
}
|
||||
|
||||
#[get("/~/<name>", rank = 3)]
|
||||
fn details(name: String, conn: DbConn, user: Option<User>) -> Template {
|
||||
paginated_details(name, conn, user, Page::first())
|
||||
pub fn details(intl: I18n, name: String, conn: DbConn, user: Option<User>) -> Result<Ructe, Ructe> {
|
||||
paginated_details(intl, name, conn, user, Page::first())
|
||||
}
|
||||
|
||||
#[get("/~/<name>", rank = 1)]
|
||||
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_local(&*conn, &name)?;
|
||||
Some(ActivityStream::new(blog.to_activity(&*conn)))
|
||||
}
|
||||
|
||||
#[get("/blogs/new")]
|
||||
fn new(user: User, conn: DbConn) -> Template {
|
||||
Template::render("blogs/new", json!({
|
||||
"account": user.to_json(&*conn),
|
||||
"errors": null,
|
||||
"form": null
|
||||
}))
|
||||
pub fn new(user: User, conn: DbConn, intl: I18n) -> Ructe {
|
||||
render!(blogs::new(
|
||||
&(&*conn, &intl.catalog, Some(user)),
|
||||
&NewBlogForm::default(),
|
||||
ValidationErrors::default()
|
||||
))
|
||||
}
|
||||
|
||||
#[get("/blogs/new", rank = 2)]
|
||||
fn new_auth() -> Flash<Redirect>{
|
||||
pub fn new_auth(i18n: I18n) -> Flash<Redirect>{
|
||||
utils::requires_login(
|
||||
"You need to be logged in order to create a new blog",
|
||||
i18n!(i18n.catalog, "You need to be logged in order to create a new blog"),
|
||||
uri!(new)
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(FromForm, Validate, Serialize)]
|
||||
struct NewBlogForm {
|
||||
#[derive(Default, FromForm, Validate, Serialize)]
|
||||
pub struct NewBlogForm {
|
||||
#[validate(custom(function = "valid_slug", message = "Invalid name"))]
|
||||
pub title: String
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
fn valid_slug(title: &str) -> Result<(), ValidationError> {
|
||||
@@ -87,9 +87,8 @@ fn valid_slug(title: &str) -> Result<(), ValidationError> {
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/blogs/new", data = "<data>")]
|
||||
fn create(conn: DbConn, data: LenientForm<NewBlogForm>, user: User) -> Result<Redirect, Template> {
|
||||
let form = data.get();
|
||||
#[post("/blogs/new", data = "<form>")]
|
||||
pub fn create(conn: DbConn, form: LenientForm<NewBlogForm>, user: User, intl: I18n) -> Result<Redirect, Ructe> {
|
||||
let slug = utils::make_actor_id(&form.title);
|
||||
|
||||
let mut errors = match form.validate() {
|
||||
@@ -121,36 +120,37 @@ fn create(conn: DbConn, data: LenientForm<NewBlogForm>, user: User) -> Result<Re
|
||||
|
||||
Ok(Redirect::to(uri!(details: name = slug.clone())))
|
||||
} else {
|
||||
println!("{:?}", errors);
|
||||
Err(Template::render("blogs/new", json!({
|
||||
"account": user.to_json(&*conn),
|
||||
"errors": errors.inner(),
|
||||
"form": form
|
||||
})))
|
||||
Err(render!(blogs::new(
|
||||
&(&*conn, &intl.catalog, Some(user)),
|
||||
&*form,
|
||||
errors
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/~/<name>/delete")]
|
||||
fn delete(conn: DbConn, name: String, user: Option<User>, searcher: Searcher) -> Result<Redirect, Option<Template>>{
|
||||
pub fn delete(conn: DbConn, name: String, user: Option<User>, intl: I18n, searcher: Searcher) -> Result<Redirect, Option<Ructe>>{
|
||||
let blog = Blog::find_local(&*conn, &name).ok_or(None)?;
|
||||
if user.map(|u| u.is_author_in(&*conn, &blog)).unwrap_or(false) {
|
||||
if user.clone().map(|u| u.is_author_in(&*conn, &blog)).unwrap_or(false) {
|
||||
blog.delete(&conn, &searcher);
|
||||
Ok(Redirect::to(uri!(super::instance::index)))
|
||||
} else {
|
||||
Err(Some(Template::render("errors/403", json!({// TODO actually return 403 error code
|
||||
"error_message": "You are not allowed to delete this blog."
|
||||
}))))
|
||||
// TODO actually return 403 error code
|
||||
Err(Some(render!(errors::not_authorized(
|
||||
&(&*conn, &intl.catalog, user),
|
||||
"You are not allowed to delete this blog."
|
||||
))))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/~/<name>/outbox")]
|
||||
fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection>> {
|
||||
pub fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection>> {
|
||||
let blog = Blog::find_local(&*conn, &name)?;
|
||||
Some(blog.outbox(&*conn))
|
||||
}
|
||||
|
||||
#[get("/~/<name>/atom.xml")]
|
||||
fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
|
||||
pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
|
||||
let blog = Blog::find_by_fqn(&*conn, &name)?;
|
||||
let feed = FeedBuilder::default()
|
||||
.title(blog.title.clone())
|
||||
|
||||
+28
-26
@@ -3,9 +3,9 @@ use rocket::{
|
||||
request::LenientForm,
|
||||
response::Redirect
|
||||
};
|
||||
use rocket_contrib::Template;
|
||||
use serde_json;
|
||||
use rocket_i18n::I18n;
|
||||
use validator::Validate;
|
||||
use template_utils::Ructe;
|
||||
|
||||
use plume_common::{utils, activity_pub::{broadcast, ApRequest, ActivityStream}};
|
||||
use plume_models::{
|
||||
@@ -15,24 +15,24 @@ use plume_models::{
|
||||
mentions::Mention,
|
||||
posts::Post,
|
||||
safe_string::SafeString,
|
||||
tags::Tag,
|
||||
users::User
|
||||
};
|
||||
use Worker;
|
||||
|
||||
#[derive(FromForm, Debug, Validate, Serialize)]
|
||||
struct NewCommentForm {
|
||||
#[derive(Default, FromForm, Debug, Validate, Serialize)]
|
||||
pub struct NewCommentForm {
|
||||
pub responding_to: Option<i32>,
|
||||
#[validate(length(min = "1", message = "Your comment can't be empty"))]
|
||||
pub content: String,
|
||||
pub warning: String,
|
||||
}
|
||||
|
||||
#[post("/~/<blog_name>/<slug>/comment", data = "<data>")]
|
||||
fn create(blog_name: String, slug: String, data: LenientForm<NewCommentForm>, user: User, conn: DbConn, worker: Worker)
|
||||
-> Result<Redirect, Option<Template>> {
|
||||
#[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, Option<Ructe>> {
|
||||
let blog = Blog::find_by_fqn(&*conn, &blog_name).ok_or(None)?;
|
||||
let post = Post::find_by_slug(&*conn, &slug, blog.id).ok_or(None)?;
|
||||
let form = data.get();
|
||||
form.validate()
|
||||
.map(|_| {
|
||||
let (html, mentions, _hashtags) = utils::md_to_html(form.content.as_ref());
|
||||
@@ -62,28 +62,30 @@ fn create(blog_name: String, slug: String, data: LenientForm<NewCommentForm>, us
|
||||
.map_err(|errors| {
|
||||
// TODO: de-duplicate this code
|
||||
let comments = Comment::list_by_post(&*conn, post.id);
|
||||
let comms = comments.clone();
|
||||
|
||||
Some(Template::render("posts/details", json!({
|
||||
"author": post.get_authors(&*conn)[0].to_json(&*conn),
|
||||
"post": post,
|
||||
"blog": blog,
|
||||
"comments": &comments.into_iter().map(|c| c.to_json(&*conn, &comms)).collect::<Vec<serde_json::Value>>(),
|
||||
"n_likes": post.get_likes(&*conn).len(),
|
||||
"has_liked": user.has_liked(&*conn, &post),
|
||||
"n_reshares": post.get_reshares(&*conn).len(),
|
||||
"has_reshared": user.has_reshared(&*conn, &post),
|
||||
"account": user.to_json(&*conn),
|
||||
"date": &post.creation_date.timestamp(),
|
||||
"previous": form.responding_to.and_then(|r| Comment::get(&*conn, r)).map(|r| r.to_json(&*conn, &[])),
|
||||
"user_fqn": user.get_fqn(&*conn),
|
||||
"comment_form": form,
|
||||
"comment_errors": errors,
|
||||
})))
|
||||
let previous = form.responding_to.map(|r| Comment::get(&*conn, r)
|
||||
.expect("posts::details_reponse: Error retrieving previous comment"));
|
||||
|
||||
Some(render!(posts::details(
|
||||
&(&*conn, &intl.catalog, Some(user.clone())),
|
||||
post.clone(),
|
||||
blog,
|
||||
&*form,
|
||||
errors,
|
||||
Tag::for_post(&*conn, post.id),
|
||||
comments.into_iter().filter(|c| c.in_response_to_id.is_none()).collect::<Vec<Comment>>(),
|
||||
previous,
|
||||
post.get_likes(&*conn).len(),
|
||||
post.get_reshares(&*conn).len(),
|
||||
user.has_liked(&*conn, &post),
|
||||
user.has_reshared(&*conn, &post),
|
||||
user.is_following(&*conn, post.get_authors(&*conn)[0].id),
|
||||
post.get_authors(&*conn)[0].clone()
|
||||
)))
|
||||
})
|
||||
}
|
||||
|
||||
#[get("/~/<_blog>/<_slug>/comment/<id>")]
|
||||
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).map(|c| ActivityStream::new(c.to_activity(&*conn)))
|
||||
}
|
||||
|
||||
+19
-23
@@ -1,40 +1,36 @@
|
||||
use rocket_contrib::Template;
|
||||
use rocket::Request;
|
||||
use rocket::request::FromRequest;
|
||||
use rocket_i18n::I18n;
|
||||
use plume_models::db_conn::DbConn;
|
||||
use plume_models::users::User;
|
||||
use template_utils::Ructe;
|
||||
|
||||
#[catch(404)]
|
||||
fn not_found(req: &Request) -> Template {
|
||||
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();
|
||||
Template::render("errors/404", json!({
|
||||
"error_message": "Page not found",
|
||||
"account": user.and_then(|u| conn.map(|conn| u.to_json(&*conn)))
|
||||
}))
|
||||
render!(errors::not_found(
|
||||
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
|
||||
))
|
||||
}
|
||||
|
||||
#[catch(500)]
|
||||
fn server_error(req: &Request) -> Template {
|
||||
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();
|
||||
Template::render("errors/500", json!({
|
||||
"error_message": "Server error",
|
||||
"account": user.and_then(|u| conn.map(|conn| u.to_json(&*conn)))
|
||||
}))
|
||||
render!(errors::server_error(
|
||||
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct Uri {
|
||||
target: String,
|
||||
}
|
||||
|
||||
#[post("/csrf-violation?<uri>")]
|
||||
fn csrf_violation(uri: Option<Uri>) -> Template {
|
||||
if let Some(uri) = uri {
|
||||
eprintln!("Csrf violation while acceding \"{}\"", uri.target)
|
||||
#[post("/csrf-violation?<target>")]
|
||||
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)
|
||||
}
|
||||
Template::render("errors/csrf", json!({
|
||||
"error_message":""
|
||||
}))
|
||||
render!(errors::csrf(
|
||||
&(&*conn, &intl.catalog, user)
|
||||
))
|
||||
}
|
||||
|
||||
+109
-103
@@ -1,8 +1,8 @@
|
||||
use gettextrs::gettext;
|
||||
use rocket::{request::LenientForm, response::{status, Redirect}};
|
||||
use rocket_contrib::{Json, Template};
|
||||
use rocket_contrib::json::Json;
|
||||
use rocket_i18n::I18n;
|
||||
use serde_json;
|
||||
use validator::{Validate};
|
||||
use validator::{Validate, ValidationErrors};
|
||||
|
||||
use plume_common::activity_pub::sign::{Signable,
|
||||
verify_http_headers};
|
||||
@@ -18,10 +18,11 @@ use plume_models::{
|
||||
};
|
||||
use inbox::Inbox;
|
||||
use routes::Page;
|
||||
use template_utils::Ructe;
|
||||
use Searcher;
|
||||
|
||||
#[get("/")]
|
||||
fn index(conn: DbConn, user: Option<User>) -> Template {
|
||||
pub fn index(conn: DbConn, user: Option<User>, intl: I18n) -> Ructe {
|
||||
match Instance::get_local(&*conn) {
|
||||
Some(inst) => {
|
||||
let federated = Post::get_recents_page(&*conn, Page::first().limits());
|
||||
@@ -33,101 +34,107 @@ fn index(conn: DbConn, user: Option<User>) -> Template {
|
||||
Post::user_feed_page(&*conn, in_feed, Page::first().limits())
|
||||
});
|
||||
|
||||
Template::render("instance/index", json!({
|
||||
"instance": inst,
|
||||
"account": user.map(|u| u.to_json(&*conn)),
|
||||
"federated": federated.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
|
||||
"local": local.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
|
||||
"user_feed": user_feed.map(|f| f.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>()),
|
||||
"n_users": User::count_local(&*conn),
|
||||
"n_articles": Post::count_local(&*conn)
|
||||
}))
|
||||
render!(instance::index(
|
||||
&(&*conn, &intl.catalog, user),
|
||||
inst,
|
||||
User::count_local(&*conn) as i32,
|
||||
Post::count_local(&*conn) as i32,
|
||||
local,
|
||||
federated,
|
||||
user_feed
|
||||
))
|
||||
}
|
||||
None => {
|
||||
Template::render("errors/500", json!({
|
||||
"error_message": gettext("You need to configure your instance before using it.".to_string())
|
||||
}))
|
||||
render!(errors::server_error(
|
||||
&(&*conn, &intl.catalog, user)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/local?<page>")]
|
||||
fn paginated_local(conn: DbConn, user: Option<User>, page: Page) -> Template {
|
||||
pub fn paginated_local(conn: DbConn, user: Option<User>, page: Page, intl: I18n) -> Ructe {
|
||||
let instance = Instance::get_local(&*conn).expect("instance::paginated_local: local instance not found error");
|
||||
let articles = Post::get_instance_page(&*conn, instance.id, page.limits());
|
||||
Template::render("instance/local", json!({
|
||||
"account": user.map(|u| u.to_json(&*conn)),
|
||||
"instance": instance,
|
||||
"page": page.page,
|
||||
"n_pages": Page::total(Post::count_local(&*conn) as i32),
|
||||
"articles": articles.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>()
|
||||
}))
|
||||
render!(instance::local(
|
||||
&(&*conn, &intl.catalog, user),
|
||||
instance,
|
||||
articles,
|
||||
page.0,
|
||||
Page::total(Post::count_local(&*conn) as i32)
|
||||
))
|
||||
}
|
||||
|
||||
#[get("/local")]
|
||||
fn local(conn: DbConn, user: Option<User>) -> Template {
|
||||
paginated_local(conn, user, Page::first())
|
||||
pub fn local(conn: DbConn, user: Option<User>, intl: I18n) -> Ructe {
|
||||
paginated_local(conn, user, Page::first(), intl)
|
||||
}
|
||||
|
||||
#[get("/feed")]
|
||||
fn feed(conn: DbConn, user: User) -> Template {
|
||||
paginated_feed(conn, user, Page::first())
|
||||
pub fn feed(conn: DbConn, user: User, intl: I18n) -> Ructe {
|
||||
paginated_feed(conn, user, Page::first(), intl)
|
||||
}
|
||||
|
||||
#[get("/feed?<page>")]
|
||||
fn paginated_feed(conn: DbConn, user: User, page: Page) -> Template {
|
||||
pub fn paginated_feed(conn: DbConn, user: User, page: Page, intl: I18n) -> Ructe {
|
||||
let followed = user.get_following(&*conn);
|
||||
let mut in_feed = followed.into_iter().map(|u| u.id).collect::<Vec<i32>>();
|
||||
in_feed.push(user.id);
|
||||
let articles = Post::user_feed_page(&*conn, in_feed, page.limits());
|
||||
Template::render("instance/feed", json!({
|
||||
"account": user.to_json(&*conn),
|
||||
"page": page.page,
|
||||
"n_pages": Page::total(Post::count_local(&*conn) as i32),
|
||||
"articles": articles.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>()
|
||||
}))
|
||||
render!(instance::feed(
|
||||
&(&*conn, &intl.catalog, Some(user)),
|
||||
articles,
|
||||
page.0,
|
||||
Page::total(Post::count_local(&*conn) as i32)
|
||||
))
|
||||
}
|
||||
|
||||
#[get("/federated")]
|
||||
fn federated(conn: DbConn, user: Option<User>) -> Template {
|
||||
paginated_federated(conn, user, Page::first())
|
||||
pub fn federated(conn: DbConn, user: Option<User>, intl: I18n) -> Ructe {
|
||||
paginated_federated(conn, user, Page::first(), intl)
|
||||
}
|
||||
|
||||
#[get("/federated?<page>")]
|
||||
fn paginated_federated(conn: DbConn, user: Option<User>, page: Page) -> Template {
|
||||
pub fn paginated_federated(conn: DbConn, user: Option<User>, page: Page, intl: I18n) -> Ructe {
|
||||
let articles = Post::get_recents_page(&*conn, page.limits());
|
||||
Template::render("instance/federated", json!({
|
||||
"account": user.map(|u| u.to_json(&*conn)),
|
||||
"page": page.page,
|
||||
"n_pages": Page::total(Post::count_local(&*conn) as i32),
|
||||
"articles": articles.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>()
|
||||
}))
|
||||
render!(instance::federated(
|
||||
&(&*conn, &intl.catalog, user),
|
||||
articles,
|
||||
page.0,
|
||||
Page::total(Post::count_local(&*conn) as i32)
|
||||
))
|
||||
}
|
||||
|
||||
#[get("/admin")]
|
||||
fn admin(conn: DbConn, admin: Admin) -> Template {
|
||||
Template::render("instance/admin", json!({
|
||||
"account": admin.0.to_json(&*conn),
|
||||
"instance": Instance::get_local(&*conn),
|
||||
"errors": null,
|
||||
"form": null
|
||||
}))
|
||||
pub fn admin(conn: DbConn, admin: Admin, intl: I18n) -> Ructe {
|
||||
let local_inst = Instance::get_local(&*conn).expect("instance::admin: local instance not found");
|
||||
render!(instance::admin(
|
||||
&(&*conn, &intl.catalog, Some(admin.0)),
|
||||
local_inst.clone(),
|
||||
InstanceSettingsForm {
|
||||
name: local_inst.name.clone(),
|
||||
open_registrations: local_inst.open_registrations,
|
||||
short_description: local_inst.short_description,
|
||||
long_description: local_inst.long_description,
|
||||
default_license: local_inst.default_license,
|
||||
},
|
||||
ValidationErrors::default()
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(FromForm, Validate, Serialize)]
|
||||
struct InstanceSettingsForm {
|
||||
#[derive(Clone, FromForm, Validate, Serialize)]
|
||||
pub struct InstanceSettingsForm {
|
||||
#[validate(length(min = "1"))]
|
||||
name: String,
|
||||
open_registrations: bool,
|
||||
short_description: SafeString,
|
||||
long_description: SafeString,
|
||||
pub name: String,
|
||||
pub open_registrations: bool,
|
||||
pub short_description: SafeString,
|
||||
pub long_description: SafeString,
|
||||
#[validate(length(min = "1"))]
|
||||
default_license: String
|
||||
pub default_license: String
|
||||
}
|
||||
|
||||
#[post("/admin", data = "<form>")]
|
||||
fn update_settings(conn: DbConn, admin: Admin, form: LenientForm<InstanceSettingsForm>) -> Result<Redirect, Template> {
|
||||
let form = form.get();
|
||||
pub fn update_settings(conn: DbConn, admin: Admin, form: LenientForm<InstanceSettingsForm>, intl: I18n) -> Result<Redirect, Ructe> {
|
||||
form.validate()
|
||||
.map(|_| {
|
||||
let instance = Instance::get_local(&*conn).expect("instance::update_settings: local instance not found error");
|
||||
@@ -138,33 +145,36 @@ fn update_settings(conn: DbConn, admin: Admin, form: LenientForm<InstanceSetting
|
||||
form.long_description.clone());
|
||||
Redirect::to(uri!(admin))
|
||||
})
|
||||
.map_err(|e| Template::render("instance/admin", json!({
|
||||
"account": admin.0.to_json(&*conn),
|
||||
"instance": Instance::get_local(&*conn),
|
||||
"errors": e.inner(),
|
||||
"form": form
|
||||
})))
|
||||
.map_err(|e| {
|
||||
let local_inst = Instance::get_local(&*conn).expect("instance::update_settings: local instance not found");
|
||||
render!(instance::admin(
|
||||
&(&*conn, &intl.catalog, Some(admin.0)),
|
||||
local_inst,
|
||||
form.clone(),
|
||||
e
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
#[get("/admin/instances")]
|
||||
fn admin_instances(admin: Admin, conn: DbConn) -> Template {
|
||||
admin_instances_paginated(admin, conn, Page::first())
|
||||
pub fn admin_instances(admin: Admin, conn: DbConn, intl: I18n) -> Ructe {
|
||||
admin_instances_paginated(admin, conn, Page::first(), intl)
|
||||
}
|
||||
|
||||
#[get("/admin/instances?<page>")]
|
||||
fn admin_instances_paginated(admin: Admin, conn: DbConn, page: Page) -> Template {
|
||||
pub fn admin_instances_paginated(admin: Admin, conn: DbConn, page: Page, intl: I18n) -> Ructe {
|
||||
let instances = Instance::page(&*conn, page.limits());
|
||||
Template::render("instance/list", json!({
|
||||
"account": admin.0.to_json(&*conn),
|
||||
"instances": instances,
|
||||
"instance": Instance::get_local(&*conn),
|
||||
"page": page.page,
|
||||
"n_pages": Page::total(Instance::count(&*conn) as i32),
|
||||
}))
|
||||
render!(instance::list(
|
||||
&(&*conn, &intl.catalog, Some(admin.0)),
|
||||
Instance::get_local(&*conn).expect("admin_instances: local instance error"),
|
||||
instances,
|
||||
page.0,
|
||||
Page::total(Instance::count(&*conn) as i32)
|
||||
))
|
||||
}
|
||||
|
||||
#[post("/admin/instances/<id>/block")]
|
||||
fn toggle_block(_admin: Admin, conn: DbConn, id: i32) -> Redirect {
|
||||
pub fn toggle_block(_admin: Admin, conn: DbConn, id: i32) -> Redirect {
|
||||
if let Some(inst) = Instance::get(&*conn, id) {
|
||||
inst.toggle_block(&*conn);
|
||||
}
|
||||
@@ -173,25 +183,22 @@ fn toggle_block(_admin: Admin, conn: DbConn, id: i32) -> Redirect {
|
||||
}
|
||||
|
||||
#[get("/admin/users")]
|
||||
fn admin_users(admin: Admin, conn: DbConn) -> Template {
|
||||
admin_users_paginated(admin, conn, Page::first())
|
||||
pub fn admin_users(admin: Admin, conn: DbConn, intl: I18n) -> Ructe {
|
||||
admin_users_paginated(admin, conn, Page::first(), intl)
|
||||
}
|
||||
|
||||
#[get("/admin/users?<page>")]
|
||||
fn admin_users_paginated(admin: Admin, conn: DbConn, page: Page) -> Template {
|
||||
let users = User::get_local_page(&*conn, page.limits()).into_iter()
|
||||
.map(|u| u.to_json(&*conn)).collect::<Vec<serde_json::Value>>();
|
||||
|
||||
Template::render("instance/users", json!({
|
||||
"account": admin.0.to_json(&*conn),
|
||||
"users": users,
|
||||
"page": page.page,
|
||||
"n_pages": Page::total(User::count_local(&*conn) as i32)
|
||||
}))
|
||||
pub fn admin_users_paginated(admin: Admin, conn: DbConn, page: Page, intl: I18n) -> Ructe {
|
||||
render!(instance::users(
|
||||
&(&*conn, &intl.catalog, Some(admin.0)),
|
||||
User::get_local_page(&*conn, page.limits()),
|
||||
page.0,
|
||||
Page::total(User::count_local(&*conn) as i32)
|
||||
))
|
||||
}
|
||||
|
||||
#[post("/admin/users/<id>/ban")]
|
||||
fn ban(_admin: Admin, conn: DbConn, id: i32, searcher: Searcher) -> Redirect {
|
||||
pub fn ban(_admin: Admin, conn: DbConn, id: i32, searcher: Searcher) -> Redirect {
|
||||
if let Some(u) = User::get(&*conn, id) {
|
||||
u.delete(&*conn, &searcher);
|
||||
}
|
||||
@@ -199,7 +206,7 @@ fn ban(_admin: Admin, conn: DbConn, id: i32, searcher: Searcher) -> Redirect {
|
||||
}
|
||||
|
||||
#[post("/inbox", data = "<data>")]
|
||||
fn shared_inbox(conn: DbConn, data: String, headers: Headers, searcher: Searcher) -> Result<String, status::BadRequest<&'static str>> {
|
||||
pub fn shared_inbox(conn: DbConn, data: String, headers: Headers, searcher: Searcher) -> Result<String, status::BadRequest<&'static str>> {
|
||||
let act: serde_json::Value = serde_json::from_str(&data[..]).expect("instance::shared_inbox: deserialization error");
|
||||
|
||||
let activity = act.clone();
|
||||
@@ -227,7 +234,7 @@ fn shared_inbox(conn: DbConn, data: String, headers: Headers, searcher: Searcher
|
||||
}
|
||||
|
||||
#[get("/nodeinfo")]
|
||||
fn nodeinfo(conn: DbConn) -> Json<serde_json::Value> {
|
||||
pub fn nodeinfo(conn: DbConn) -> Json<serde_json::Value> {
|
||||
Json(json!({
|
||||
"version": "2.0",
|
||||
"software": {
|
||||
@@ -252,20 +259,19 @@ fn nodeinfo(conn: DbConn) -> Json<serde_json::Value> {
|
||||
}
|
||||
|
||||
#[get("/about")]
|
||||
fn about(user: Option<User>, conn: DbConn) -> Template {
|
||||
Template::render("instance/about", json!({
|
||||
"account": user.map(|u| u.to_json(&*conn)),
|
||||
"instance": Instance::get_local(&*conn),
|
||||
"admin": Instance::get_local(&*conn).map(|i| i.main_admin(&*conn).to_json(&*conn)),
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
"n_users": User::count_local(&*conn),
|
||||
"n_articles": Post::count_local(&*conn),
|
||||
"n_instances": Instance::count(&*conn) - 1
|
||||
}))
|
||||
pub fn about(user: Option<User>, conn: DbConn, intl: I18n) -> Ructe {
|
||||
render!(instance::about(
|
||||
&(&*conn, &intl.catalog, user),
|
||||
Instance::get_local(&*conn).expect("Local instance not found"),
|
||||
Instance::get_local(&*conn).expect("Local instance not found").main_admin(&*conn),
|
||||
User::count_local(&*conn),
|
||||
Post::count_local(&*conn),
|
||||
Instance::count(&*conn) - 1
|
||||
))
|
||||
}
|
||||
|
||||
#[get("/manifest.json")]
|
||||
fn web_manifest(conn: DbConn) -> Json<serde_json::Value> {
|
||||
pub fn web_manifest(conn: DbConn) -> Json<serde_json::Value> {
|
||||
let instance = Instance::get_local(&*conn).expect("instance::web_manifest: local instance not found error");
|
||||
Json(json!({
|
||||
"name": &instance.name,
|
||||
|
||||
+5
-4
@@ -1,4 +1,5 @@
|
||||
use rocket::{response::{Redirect, Flash}};
|
||||
use rocket::response::{Redirect, Flash};
|
||||
use rocket_i18n::I18n;
|
||||
|
||||
use plume_common::activity_pub::{broadcast, inbox::{Notify, Deletable}};
|
||||
use plume_common::utils;
|
||||
@@ -12,7 +13,7 @@ use plume_models::{
|
||||
use Worker;
|
||||
|
||||
#[post("/~/<blog>/<slug>/like")]
|
||||
fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Option<Redirect> {
|
||||
pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Option<Redirect> {
|
||||
let b = Blog::find_by_fqn(&*conn, &blog)?;
|
||||
let post = Post::find_by_slug(&*conn, &slug, b.id)?;
|
||||
|
||||
@@ -39,9 +40,9 @@ fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker)
|
||||
}
|
||||
|
||||
#[post("/~/<blog>/<slug>/like", rank = 2)]
|
||||
fn create_auth(blog: String, slug: String) -> Flash<Redirect>{
|
||||
pub fn create_auth(blog: String, slug: String, i18n: I18n) -> Flash<Redirect>{
|
||||
utils::requires_login(
|
||||
"You need to be logged in order to like a post",
|
||||
i18n!(i18n.catalog, "You need to be logged in order to like a post"),
|
||||
uri!(create: blog = blog, slug = slug)
|
||||
)
|
||||
}
|
||||
|
||||
+20
-22
@@ -1,31 +1,29 @@
|
||||
use guid_create::GUID;
|
||||
use multipart::server::{Multipart, save::{SavedData, SaveResult}};
|
||||
use rocket::{Data, http::ContentType, response::{Redirect, status}};
|
||||
use rocket_contrib::Template;
|
||||
use serde_json;
|
||||
use rocket_i18n::I18n;
|
||||
use std::fs;
|
||||
use plume_models::{db_conn::DbConn, medias::*, users::User};
|
||||
use template_utils::Ructe;
|
||||
|
||||
#[get("/medias")]
|
||||
fn list(user: User, conn: DbConn) -> Template {
|
||||
pub fn list(user: User, conn: DbConn, intl: I18n) -> Ructe {
|
||||
let medias = Media::for_user(&*conn, user.id);
|
||||
Template::render("medias/index", json!({
|
||||
"account": user.to_json(&*conn),
|
||||
"medias": medias.into_iter().map(|m| m.to_json(&*conn)).collect::<Vec<serde_json::Value>>()
|
||||
}))
|
||||
render!(medias::index(
|
||||
&(&*conn, &intl.catalog, Some(user)),
|
||||
medias
|
||||
))
|
||||
}
|
||||
|
||||
#[get("/medias/new")]
|
||||
fn new(user: User, conn: DbConn) -> Template {
|
||||
Template::render("medias/new", json!({
|
||||
"account": user.to_json(&*conn),
|
||||
"form": {},
|
||||
"errors": {}
|
||||
}))
|
||||
pub fn new(user: User, conn: DbConn, intl: I18n) -> Ructe {
|
||||
render!(medias::new(
|
||||
&(&*conn, &intl.catalog, Some(user))
|
||||
))
|
||||
}
|
||||
|
||||
#[post("/medias/new", data = "<data>")]
|
||||
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")))?;
|
||||
|
||||
@@ -86,23 +84,23 @@ fn read(data: &SavedData) -> String {
|
||||
}
|
||||
|
||||
#[get("/medias/<id>")]
|
||||
fn details(id: i32, user: User, conn: DbConn) -> Template {
|
||||
let media = Media::get(&*conn, id);
|
||||
Template::render("medias/details", json!({
|
||||
"account": user.to_json(&*conn),
|
||||
"media": media.map(|m| m.to_json(&*conn))
|
||||
}))
|
||||
pub fn details(id: i32, user: User, conn: DbConn, intl: I18n) -> Ructe {
|
||||
let media = Media::get(&*conn, id).expect("Media::details: media not found");
|
||||
render!(medias::details(
|
||||
&(&*conn, &intl.catalog, Some(user)),
|
||||
media
|
||||
))
|
||||
}
|
||||
|
||||
#[post("/medias/<id>/delete")]
|
||||
fn delete(id: i32, _user: User, conn: DbConn) -> Option<Redirect> {
|
||||
pub fn delete(id: i32, _user: User, conn: DbConn) -> Option<Redirect> {
|
||||
let media = Media::get(&*conn, id)?;
|
||||
media.delete(&*conn);
|
||||
Some(Redirect::to(uri!(list)))
|
||||
}
|
||||
|
||||
#[post("/medias/<id>/avatar")]
|
||||
fn set_avatar(id: i32, user: User, conn: DbConn) -> Option<Redirect> {
|
||||
pub fn set_avatar(id: i32, user: User, conn: DbConn) -> Option<Redirect> {
|
||||
let media = Media::get(&*conn, id)?;
|
||||
user.set_avatar(&*conn, media.id);
|
||||
Some(Redirect::to(uri!(details: id = id)))
|
||||
|
||||
+9
-58
@@ -1,70 +1,23 @@
|
||||
use atom_syndication::{ContentBuilder, Entry, EntryBuilder, LinkBuilder, Person, PersonBuilder};
|
||||
use rocket::{
|
||||
http::{RawStr,
|
||||
uri::{FromUriParam, UriDisplay}},
|
||||
http::RawStr,
|
||||
request::FromFormValue,
|
||||
response::NamedFile
|
||||
};
|
||||
use std::{
|
||||
fmt,
|
||||
path::{Path, PathBuf}
|
||||
response::NamedFile,
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use plume_models::{Connection, posts::Post};
|
||||
|
||||
macro_rules! may_fail {
|
||||
($account:expr, $expr:expr, $template:expr, $msg:expr, | $res:ident | $block:block) => {
|
||||
{
|
||||
let res = $expr;
|
||||
if res.is_some() {
|
||||
let $res = res.unwrap();
|
||||
$block
|
||||
} else {
|
||||
Template::render(concat!("errors/", $template), json!({
|
||||
"error_message": $msg,
|
||||
"account": $account
|
||||
}))
|
||||
}
|
||||
}
|
||||
};
|
||||
($account:expr, $expr:expr, $msg:expr, | $res:ident | $block:block) => {
|
||||
may_fail!($account, $expr, "404", $msg, |$res| {
|
||||
$block
|
||||
})
|
||||
};
|
||||
($account:expr, $expr:expr, | $res:ident | $block:block) => {
|
||||
may_fail!($account, $expr, "", |$res| {
|
||||
$block
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
const ITEMS_PER_PAGE: i32 = 12;
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct Page {
|
||||
page: i32
|
||||
}
|
||||
|
||||
impl UriDisplay for Page {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "page={}", &self.page as &UriDisplay)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromUriParam<i32> for Page {
|
||||
type Target = Page;
|
||||
fn from_uri_param(num: i32) -> Page {
|
||||
Page { page: num }
|
||||
}
|
||||
}
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Page(i32);
|
||||
|
||||
impl<'v> FromFormValue<'v> for Page {
|
||||
type Error = &'v RawStr;
|
||||
|
||||
fn from_form_value(form_value: &'v RawStr) -> Result<Page, &'v RawStr> {
|
||||
match form_value.parse::<i32>() {
|
||||
Ok(page) => Ok(Page{page}),
|
||||
Ok(page) => Ok(Page(page)),
|
||||
_ => Err(form_value),
|
||||
}
|
||||
}
|
||||
@@ -72,9 +25,7 @@ impl<'v> FromFormValue<'v> for Page {
|
||||
|
||||
impl Page {
|
||||
pub fn first() -> Page {
|
||||
Page {
|
||||
page: 1
|
||||
}
|
||||
Page(1)
|
||||
}
|
||||
|
||||
/// Computes the total number of pages needed to display n_items
|
||||
@@ -87,7 +38,7 @@ impl Page {
|
||||
}
|
||||
|
||||
pub fn limits(&self) -> (i32, i32) {
|
||||
((self.page - 1) * ITEMS_PER_PAGE, self.page * ITEMS_PER_PAGE)
|
||||
((self.0 - 1) * ITEMS_PER_PAGE, self.0 * ITEMS_PER_PAGE)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +77,6 @@ pub mod search;
|
||||
pub mod well_known;
|
||||
|
||||
#[get("/static/<file..>", rank = 2)]
|
||||
fn static_files(file: PathBuf) -> Option<NamedFile> {
|
||||
pub fn static_files(file: PathBuf) -> Option<NamedFile> {
|
||||
NamedFile::open(Path::new("static/").join(file)).ok()
|
||||
}
|
||||
|
||||
+13
-12
@@ -1,29 +1,30 @@
|
||||
use rocket::response::{Redirect, Flash};
|
||||
use rocket_contrib::Template;
|
||||
use rocket_i18n::I18n;
|
||||
|
||||
use plume_common::utils;
|
||||
use plume_models::{db_conn::DbConn, notifications::Notification, users::User};
|
||||
use routes::Page;
|
||||
use template_utils::Ructe;
|
||||
|
||||
#[get("/notifications?<page>")]
|
||||
fn paginated_notifications(conn: DbConn, user: User, page: Page) -> Template {
|
||||
Template::render("notifications/index", json!({
|
||||
"account": user.to_json(&*conn),
|
||||
"notifications": Notification::page_for_user(&*conn, &user, page.limits()).into_iter().map(|n| n.to_json(&*conn)).collect::<Vec<_>>(),
|
||||
"page": page.page,
|
||||
"n_pages": Page::total(Notification::find_for_user(&*conn, &user).len() as i32)
|
||||
}))
|
||||
pub fn paginated_notifications(conn: DbConn, user: User, page: Page, intl: I18n) -> Ructe {
|
||||
render!(notifications::index(
|
||||
&(&*conn, &intl.catalog, Some(user.clone())),
|
||||
Notification::page_for_user(&*conn, &user, page.limits()),
|
||||
page.0,
|
||||
Page::total(Notification::find_for_user(&*conn, &user).len() as i32)
|
||||
))
|
||||
}
|
||||
|
||||
#[get("/notifications")]
|
||||
fn notifications(conn: DbConn, user: User) -> Template {
|
||||
paginated_notifications(conn, user, Page::first())
|
||||
pub fn notifications(conn: DbConn, user: User, intl: I18n) -> Ructe {
|
||||
paginated_notifications(conn, user, Page::first(), intl)
|
||||
}
|
||||
|
||||
#[get("/notifications", rank = 2)]
|
||||
fn notifications_auth() -> Flash<Redirect>{
|
||||
pub fn notifications_auth(i18n: I18n) -> Flash<Redirect>{
|
||||
utils::requires_login(
|
||||
"You need to be logged in order to see your notifications",
|
||||
i18n!(i18n.catalog, "You need to be logged in order to see your notifications"),
|
||||
uri!(notifications)
|
||||
)
|
||||
}
|
||||
|
||||
+119
-110
@@ -1,10 +1,9 @@
|
||||
use activitypub::object::Article;
|
||||
use chrono::Utc;
|
||||
use heck::{CamelCase, KebabCase};
|
||||
use rocket::{request::LenientForm};
|
||||
use rocket::request::LenientForm;
|
||||
use rocket::response::{Redirect, Flash};
|
||||
use rocket_contrib::Template;
|
||||
use serde_json;
|
||||
use rocket_i18n::I18n;
|
||||
use std::{collections::{HashMap, HashSet}, borrow::Cow};
|
||||
use validator::{Validate, ValidationError, ValidationErrors};
|
||||
|
||||
@@ -23,66 +22,74 @@ use plume_models::{
|
||||
tags::*,
|
||||
users::User
|
||||
};
|
||||
use routes::comments::NewCommentForm;
|
||||
use template_utils::Ructe;
|
||||
use Worker;
|
||||
use Searcher;
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct CommentQuery {
|
||||
responding_to: Option<i32>
|
||||
}
|
||||
|
||||
// See: https://github.com/SergioBenitez/Rocket/pull/454
|
||||
#[get("/~/<blog>/<slug>", rank = 4)]
|
||||
fn details(blog: String, slug: String, conn: DbConn, user: Option<User>) -> Template {
|
||||
details_response(blog, slug, conn, user, None)
|
||||
#[get("/~/<blog>/<slug>", rank = 5)]
|
||||
pub fn details(blog: String, slug: String, conn: DbConn, user: Option<User>, intl: I18n) -> Result<Ructe, Ructe> {
|
||||
details_response(blog, slug, conn, user, None, intl)
|
||||
}
|
||||
|
||||
#[get("/~/<blog>/<slug>?<query>")]
|
||||
fn details_response(blog: String, slug: String, conn: DbConn, user: Option<User>, query: Option<CommentQuery>) -> Template {
|
||||
may_fail!(user.map(|u| u.to_json(&*conn)), Blog::find_by_fqn(&*conn, &blog), "Couldn't find this blog", |blog| {
|
||||
may_fail!(user.map(|u| u.to_json(&*conn)), Post::find_by_slug(&*conn, &slug, blog.id), "Couldn't find this post", |post| {
|
||||
if post.published || post.get_authors(&*conn).into_iter().any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0)) {
|
||||
let comments = Comment::list_by_post(&*conn, post.id);
|
||||
let comms = comments.clone();
|
||||
#[get("/~/<blog>/<slug>?<responding_to>", rank = 4)]
|
||||
pub fn details_response(blog: String, slug: String, conn: DbConn, user: Option<User>, responding_to: Option<i32>, intl: I18n) -> Result<Ructe, Ructe> {
|
||||
let blog = Blog::find_by_fqn(&*conn, &blog).ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, user.clone()))))?;
|
||||
let post = Post::find_by_slug(&*conn, &slug, blog.id).ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, user.clone()))))?;
|
||||
if post.published || post.get_authors(&*conn).into_iter().any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0)) {
|
||||
let comments = Comment::list_by_post(&*conn, post.id);
|
||||
|
||||
let previous = query.and_then(|q| q.responding_to.map(|r| Comment::get(&*conn, r)
|
||||
.expect("posts::details_reponse: Error retrieving previous comment").to_json(&*conn, &[])));
|
||||
Template::render("posts/details", json!({
|
||||
"author": post.get_authors(&*conn)[0].to_json(&*conn),
|
||||
"article": post.to_json(&*conn),
|
||||
"blog": blog.to_json(&*conn),
|
||||
"comments": &comments.into_iter().filter_map(|c| if c.in_response_to_id.is_none() {
|
||||
Some(c.to_json(&*conn, &comms))
|
||||
} else {
|
||||
None
|
||||
}).collect::<Vec<serde_json::Value>>(),
|
||||
"n_likes": post.get_likes(&*conn).len(),
|
||||
"has_liked": user.clone().map(|u| u.has_liked(&*conn, &post)).unwrap_or(false),
|
||||
"n_reshares": post.get_reshares(&*conn).len(),
|
||||
"has_reshared": user.clone().map(|u| u.has_reshared(&*conn, &post)).unwrap_or(false),
|
||||
"account": &user.clone().map(|u| u.to_json(&*conn)),
|
||||
"date": &post.creation_date.timestamp(),
|
||||
"previous": previous,
|
||||
"default": {
|
||||
"warning": previous.map(|p| p["spoiler_text"].clone())
|
||||
},
|
||||
"user_fqn": user.clone().map(|u| u.get_fqn(&*conn)).unwrap_or_default(),
|
||||
"is_author": user.clone().map(|u| post.get_authors(&*conn).into_iter().any(|a| u.id == a.id)).unwrap_or(false),
|
||||
"is_following": user.map(|u| u.is_following(&*conn, post.get_authors(&*conn)[0].id)).unwrap_or(false),
|
||||
"comment_form": null,
|
||||
"comment_errors": null,
|
||||
}))
|
||||
} else {
|
||||
Template::render("errors/403", json!({
|
||||
"error_message": "This post isn't published yet."
|
||||
}))
|
||||
}
|
||||
})
|
||||
})
|
||||
let previous = responding_to.map(|r| Comment::get(&*conn, r)
|
||||
.expect("posts::details_reponse: Error retrieving previous comment"));
|
||||
|
||||
Ok(render!(posts::details(
|
||||
&(&*conn, &intl.catalog, user.clone()),
|
||||
post.clone(),
|
||||
blog,
|
||||
&NewCommentForm {
|
||||
warning: previous.clone().map(|p| p.spoiler_text).unwrap_or_default(),
|
||||
content: previous.clone().map(|p| format!(
|
||||
"@{} {}",
|
||||
p.get_author(&*conn).get_fqn(&*conn),
|
||||
Mention::list_for_comment(&*conn, p.id)
|
||||
.into_iter()
|
||||
.filter_map(|m| {
|
||||
let user = user.clone();
|
||||
if let Some(mentioned) = m.get_mentioned(&*conn) {
|
||||
if user.is_none() || mentioned.id != user.expect("posts::details_response: user error while listing mentions").id {
|
||||
Some(format!("@{}", mentioned.get_fqn(&*conn)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}).collect::<Vec<String>>().join(" "))
|
||||
).unwrap_or_default(),
|
||||
..NewCommentForm::default()
|
||||
},
|
||||
ValidationErrors::default(),
|
||||
Tag::for_post(&*conn, post.id),
|
||||
comments.into_iter().filter(|c| c.in_response_to_id.is_none()).collect::<Vec<Comment>>(),
|
||||
previous,
|
||||
post.get_likes(&*conn).len(),
|
||||
post.get_reshares(&*conn).len(),
|
||||
user.clone().map(|u| u.has_liked(&*conn, &post)).unwrap_or(false),
|
||||
user.clone().map(|u| u.has_reshared(&*conn, &post)).unwrap_or(false),
|
||||
user.map(|u| u.is_following(&*conn, post.get_authors(&*conn)[0].id)).unwrap_or(false),
|
||||
post.get_authors(&*conn)[0].clone()
|
||||
)))
|
||||
} else {
|
||||
Err(render!(errors::not_authorized(
|
||||
&(&*conn, &intl.catalog, user.clone()),
|
||||
"This post isn't published yet."
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/~/<blog>/<slug>", rank = 3)]
|
||||
fn activity_details(blog: String, slug: String, conn: DbConn, _ap: ApRequest) -> Result<ActivityStream<Article>, Option<String>> {
|
||||
pub fn activity_details(blog: String, slug: String, conn: DbConn, _ap: ApRequest) -> Result<ActivityStream<Article>, Option<String>> {
|
||||
let blog = Blog::find_by_fqn(&*conn, &blog).ok_or(None)?;
|
||||
let post = Post::find_by_slug(&*conn, &slug, blog.id).ok_or(None)?;
|
||||
if post.published {
|
||||
@@ -93,44 +100,47 @@ fn activity_details(blog: String, slug: String, conn: DbConn, _ap: ApRequest) ->
|
||||
}
|
||||
|
||||
#[get("/~/<blog>/new", rank = 2)]
|
||||
fn new_auth(blog: String) -> Flash<Redirect> {
|
||||
pub fn new_auth(blog: String, i18n: I18n) -> Flash<Redirect> {
|
||||
utils::requires_login(
|
||||
"You need to be logged in order to write a new post",
|
||||
i18n!(i18n.catalog, "You need to be logged in order to write a new post"),
|
||||
uri!(new: blog = blog)
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/~/<blog>/new", rank = 1)]
|
||||
fn new(blog: String, user: User, conn: DbConn) -> Option<Template> {
|
||||
pub fn new(blog: String, user: User, conn: DbConn, intl: I18n) -> Option<Ructe> {
|
||||
let b = Blog::find_by_fqn(&*conn, &blog)?;
|
||||
|
||||
if !user.is_author_in(&*conn, &b) {
|
||||
Some(Template::render("errors/403", json!({// TODO actually return 403 error code
|
||||
"error_message": "You are not author in this blog."
|
||||
})))
|
||||
// TODO actually return 403 error code
|
||||
Some(render!(errors::not_authorized(
|
||||
&(&*conn, &intl.catalog, Some(user)),
|
||||
"You are not author in this blog."
|
||||
)))
|
||||
} else {
|
||||
let medias = Media::for_user(&*conn, user.id);
|
||||
Some(Template::render("posts/new", json!({
|
||||
"account": user.to_json(&*conn),
|
||||
"instance": Instance::get_local(&*conn),
|
||||
"editing": false,
|
||||
"errors": null,
|
||||
"form": null,
|
||||
"is_draft": true,
|
||||
"medias": medias.into_iter().map(|m| m.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
|
||||
})))
|
||||
Some(render!(posts::new(
|
||||
&(&*conn, &intl.catalog, Some(user)),
|
||||
false,
|
||||
&NewPostForm::default(),
|
||||
ValidationErrors::default(),
|
||||
Instance::get_local(&*conn).expect("posts::new error: Local instance is null").default_license,
|
||||
medias,
|
||||
true
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/~/<blog>/<slug>/edit")]
|
||||
fn edit(blog: String, slug: String, user: User, conn: DbConn) -> Option<Template> {
|
||||
pub fn edit(blog: String, slug: String, user: User, conn: DbConn, intl: I18n) -> Option<Ructe> {
|
||||
let b = Blog::find_by_fqn(&*conn, &blog)?;
|
||||
let post = Post::find_by_slug(&*conn, &slug, b.id)?;
|
||||
|
||||
if !user.is_author_in(&*conn, &b) {
|
||||
Some(Template::render("errors/403", json!({// TODO actually return 403 error code
|
||||
"error_message": "You are not author in this blog."
|
||||
})))
|
||||
Some(render!(errors::not_authorized(
|
||||
&(&*conn, &intl.catalog, Some(user)),
|
||||
"You are not author in this blog."
|
||||
)))
|
||||
} else {
|
||||
let source = if !post.source.is_empty() {
|
||||
post.source
|
||||
@@ -139,12 +149,10 @@ fn edit(blog: String, slug: String, user: User, conn: DbConn) -> Option<Template
|
||||
};
|
||||
|
||||
let medias = Media::for_user(&*conn, user.id);
|
||||
Some(Template::render("posts/new", json!({
|
||||
"account": user.to_json(&*conn),
|
||||
"instance": Instance::get_local(&*conn),
|
||||
"editing": true,
|
||||
"errors": null,
|
||||
"form": NewPostForm {
|
||||
Some(render!(posts::new(
|
||||
&(&*conn, &intl.catalog, Some(user)),
|
||||
true,
|
||||
&NewPostForm {
|
||||
title: post.title.clone(),
|
||||
subtitle: post.subtitle.clone(),
|
||||
content: source,
|
||||
@@ -157,19 +165,20 @@ fn edit(blog: String, slug: String, user: User, conn: DbConn) -> Option<Template
|
||||
draft: true,
|
||||
cover: post.cover_id,
|
||||
},
|
||||
"is_draft": !post.published,
|
||||
"medias": medias.into_iter().map(|m| m.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
|
||||
})))
|
||||
ValidationErrors::default(),
|
||||
Instance::get_local(&*conn).expect("posts::new error: Local instance is null").default_license,
|
||||
medias,
|
||||
!post.published
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/~/<blog>/<slug>/edit", data = "<data>")]
|
||||
fn update(blog: String, slug: String, user: User, conn: DbConn, data: LenientForm<NewPostForm>, worker: Worker, searcher: Searcher)
|
||||
-> Result<Redirect, Option<Template>> {
|
||||
#[post("/~/<blog>/<slug>/edit", data = "<form>")]
|
||||
pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: LenientForm<NewPostForm>, worker: Worker, intl: I18n, searcher: Searcher)
|
||||
-> Result<Redirect, Option<Ructe>> {
|
||||
let b = Blog::find_by_fqn(&*conn, &blog).ok_or(None)?;
|
||||
let mut post = Post::find_by_slug(&*conn, &slug, b.id).ok_or(None)?;
|
||||
|
||||
let form = data.get();
|
||||
let new_slug = if !post.published {
|
||||
form.title.to_string().to_kebab_case()
|
||||
} else {
|
||||
@@ -249,20 +258,21 @@ fn update(blog: String, slug: String, user: User, conn: DbConn, data: LenientFor
|
||||
}
|
||||
} else {
|
||||
let medias = Media::for_user(&*conn, user.id);
|
||||
Err(Some(Template::render("posts/new", json!({
|
||||
"account": user.to_json(&*conn),
|
||||
"instance": Instance::get_local(&*conn),
|
||||
"editing": true,
|
||||
"errors": errors.inner(),
|
||||
"form": form,
|
||||
"is_draft": form.draft,
|
||||
"medias": medias.into_iter().map(|m| m.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
|
||||
}))))
|
||||
let temp = render!(posts::new(
|
||||
&(&*conn, &intl.catalog, Some(user)),
|
||||
true,
|
||||
&*form,
|
||||
errors.clone(),
|
||||
Instance::get_local(&*conn).expect("posts::new error: Local instance is null").default_license,
|
||||
medias.clone(),
|
||||
form.draft.clone()
|
||||
));
|
||||
Err(Some(temp))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Validate, Serialize)]
|
||||
struct NewPostForm {
|
||||
#[derive(Default, FromForm, Validate, Serialize)]
|
||||
pub struct NewPostForm {
|
||||
#[validate(custom(function = "valid_slug", message = "Invalid title"))]
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
@@ -273,7 +283,7 @@ struct NewPostForm {
|
||||
pub cover: Option<i32>,
|
||||
}
|
||||
|
||||
fn valid_slug(title: &str) -> Result<(), ValidationError> {
|
||||
pub fn valid_slug(title: &str) -> Result<(), ValidationError> {
|
||||
let slug = title.to_string().to_kebab_case();
|
||||
if slug.is_empty() {
|
||||
Err(ValidationError::new("empty_slug"))
|
||||
@@ -284,10 +294,9 @@ fn valid_slug(title: &str) -> Result<(), ValidationError> {
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/~/<blog_name>/new", data = "<data>")]
|
||||
fn create(blog_name: String, data: LenientForm<NewPostForm>, user: User, conn: DbConn, worker: Worker, searcher: Searcher) -> Result<Redirect, Option<Template>> {
|
||||
#[post("/~/<blog_name>/new", data = "<form>")]
|
||||
pub fn create(blog_name: String, form: LenientForm<NewPostForm>, user: User, conn: DbConn, worker: Worker, intl: I18n, searcher: Searcher) -> Result<Redirect, Option<Ructe>> {
|
||||
let blog = Blog::find_by_fqn(&*conn, &blog_name).ok_or(None)?;
|
||||
let form = data.get();
|
||||
let slug = form.title.to_string().to_kebab_case();
|
||||
|
||||
let mut errors = match form.validate() {
|
||||
@@ -367,20 +376,20 @@ fn create(blog_name: String, data: LenientForm<NewPostForm>, user: User, conn: D
|
||||
}
|
||||
} else {
|
||||
let medias = Media::for_user(&*conn, user.id);
|
||||
Err(Some(Template::render("posts/new", json!({
|
||||
"account": user.to_json(&*conn),
|
||||
"instance": Instance::get_local(&*conn),
|
||||
"editing": false,
|
||||
"errors": errors.inner(),
|
||||
"form": form,
|
||||
"is_draft": form.draft,
|
||||
"medias": medias.into_iter().map(|m| m.to_json(&*conn)).collect::<Vec<serde_json::Value>>()
|
||||
}))))
|
||||
Err(Some(render!(posts::new(
|
||||
&(&*conn, &intl.catalog, Some(user)),
|
||||
false,
|
||||
&*form,
|
||||
errors.clone(),
|
||||
Instance::get_local(&*conn).expect("posts::new error: Local instance is null").default_license,
|
||||
medias,
|
||||
form.draft
|
||||
))))
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/~/<blog_name>/<slug>/delete")]
|
||||
fn delete(blog_name: String, slug: String, conn: DbConn, user: User, worker: Worker, searcher: Searcher) -> Redirect {
|
||||
pub fn delete(blog_name: String, slug: String, conn: DbConn, user: User, worker: Worker, searcher: Searcher) -> Redirect {
|
||||
let post = Blog::find_by_fqn(&*conn, &blog_name)
|
||||
.and_then(|blog| Post::find_by_slug(&*conn, &slug, blog.id));
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use rocket::{response::{Redirect, Flash}};
|
||||
use rocket::response::{Redirect, Flash};
|
||||
use rocket_i18n::I18n;
|
||||
|
||||
use plume_common::activity_pub::{broadcast, inbox::{Deletable, Notify}};
|
||||
use plume_common::utils;
|
||||
@@ -12,7 +13,7 @@ use plume_models::{
|
||||
use Worker;
|
||||
|
||||
#[post("/~/<blog>/<slug>/reshare")]
|
||||
fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Option<Redirect> {
|
||||
pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Option<Redirect> {
|
||||
let b = Blog::find_by_fqn(&*conn, &blog)?;
|
||||
let post = Post::find_by_slug(&*conn, &slug, b.id)?;
|
||||
|
||||
@@ -40,9 +41,9 @@ fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker)
|
||||
}
|
||||
|
||||
#[post("/~/<blog>/<slug>/reshare", rank=1)]
|
||||
fn create_auth(blog: String, slug: String) -> Flash<Redirect> {
|
||||
pub fn create_auth(blog: String, slug: String, i18n: I18n) -> Flash<Redirect> {
|
||||
utils::requires_login(
|
||||
"You need to be logged in order to reshare a post",
|
||||
i18n!(i18n.catalog, "You need to be logged in order to reshare a post"),
|
||||
uri!(create: blog = blog, slug = slug)
|
||||
)
|
||||
}
|
||||
|
||||
+27
-26
@@ -1,23 +1,16 @@
|
||||
use chrono::offset::Utc;
|
||||
use rocket_contrib::Template;
|
||||
use serde_json;
|
||||
use rocket::request::Form;
|
||||
use rocket_i18n::I18n;
|
||||
|
||||
use plume_models::{
|
||||
db_conn::DbConn, users::User,
|
||||
search::Query};
|
||||
use routes::Page;
|
||||
use template_utils::Ructe;
|
||||
use Searcher;
|
||||
|
||||
#[get("/search")]
|
||||
fn index(conn: DbConn, user: Option<User>) -> Template {
|
||||
Template::render("search/index", json!({
|
||||
"account": user.map(|u| u.to_json(&*conn)),
|
||||
"now": format!("{}", Utc::today().format("%Y-%m-d")),
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct SearchQuery {
|
||||
#[derive(Default, FromForm)]
|
||||
pub struct SearchQuery {
|
||||
q: Option<String>,
|
||||
title: Option<String>,
|
||||
subtitle: Option<String>,
|
||||
@@ -36,7 +29,7 @@ struct SearchQuery {
|
||||
macro_rules! param_to_query {
|
||||
( $query:ident, $parsed_query:ident; normal: $($field:ident),*; date: $($date:ident),*) => {
|
||||
$(
|
||||
if let Some(field) = $query.$field {
|
||||
if let Some(ref field) = $query.$field {
|
||||
let mut rest = field.as_str();
|
||||
while !rest.is_empty() {
|
||||
let (token, r) = Query::get_first_token(rest);
|
||||
@@ -46,7 +39,7 @@ macro_rules! param_to_query {
|
||||
}
|
||||
)*
|
||||
$(
|
||||
if let Some(field) = $query.$date {
|
||||
if let Some(ref field) = $query.$date {
|
||||
let mut rest = field.as_str();
|
||||
while !rest.is_empty() {
|
||||
use chrono::naive::NaiveDate;
|
||||
@@ -62,23 +55,31 @@ macro_rules! param_to_query {
|
||||
}
|
||||
|
||||
|
||||
#[get("/search?<query>")]
|
||||
fn query(query: SearchQuery, conn: DbConn, searcher: Searcher, user: Option<User>) -> Template {
|
||||
#[get("/search?<query..>")]
|
||||
pub fn search(query: Form<SearchQuery>, conn: DbConn, searcher: Searcher, user: Option<User>, intl: I18n) -> Ructe {
|
||||
let page = query.page.unwrap_or(Page::first());
|
||||
let mut parsed_query = Query::from_str(&query.q.unwrap_or_default());
|
||||
let mut parsed_query = Query::from_str(&query.q.as_ref().map(|q| q.as_str()).unwrap_or_default());
|
||||
|
||||
param_to_query!(query, parsed_query; normal: title, subtitle, content, tag,
|
||||
instance, author, blog, lang, license;
|
||||
date: before, after);
|
||||
|
||||
let str_q = parsed_query.to_string();
|
||||
let res = searcher.search_document(&conn, parsed_query, page.limits());
|
||||
let str_query = parsed_query.to_string();
|
||||
|
||||
Template::render("search/result", json!({
|
||||
"query":str_q,
|
||||
"account": user.map(|u| u.to_json(&*conn)),
|
||||
"next_page": if res.is_empty() { 0 } else { page.page+1 },
|
||||
"posts": res.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
|
||||
"page": page.page,
|
||||
}))
|
||||
if str_query.is_empty() {
|
||||
render!(search::index(
|
||||
&(&*conn, &intl.catalog, user),
|
||||
&format!("{}", Utc::today().format("%Y-%m-d"))
|
||||
))
|
||||
} else {
|
||||
let res = searcher.search_document(&conn, parsed_query, page.limits());
|
||||
let next_page = if res.is_empty() { 0 } else { page.0+1 };
|
||||
render!(search::result(
|
||||
&(&*conn, &intl.catalog, user),
|
||||
&str_query,
|
||||
res,
|
||||
page.0,
|
||||
next_page
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
+37
-42
@@ -3,10 +3,11 @@ use rocket::{
|
||||
response::Redirect,
|
||||
request::{LenientForm,FlashMessage}
|
||||
};
|
||||
use rocket_contrib::Template;
|
||||
use rocket::http::ext::IntoOwned;
|
||||
use rocket_i18n::I18n;
|
||||
use std::borrow::Cow;
|
||||
use validator::{Validate, ValidationError, ValidationErrors};
|
||||
use template_utils::Ructe;
|
||||
|
||||
use plume_models::{
|
||||
db_conn::DbConn,
|
||||
@@ -14,48 +15,43 @@ use plume_models::{
|
||||
};
|
||||
|
||||
#[get("/login")]
|
||||
fn new(user: Option<User>, conn: DbConn) -> Template {
|
||||
Template::render("session/login", json!({
|
||||
"account": user.map(|u| u.to_json(&*conn)),
|
||||
"errors": null,
|
||||
"form": null
|
||||
}))
|
||||
pub fn new(user: Option<User>, conn: DbConn, intl: I18n) -> Ructe {
|
||||
render!(session::login(
|
||||
&(&*conn, &intl.catalog, user),
|
||||
None,
|
||||
&LoginForm::default(),
|
||||
ValidationErrors::default()
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct Message {
|
||||
m: String
|
||||
}
|
||||
|
||||
#[get("/login?<message>")]
|
||||
fn new_message(user: Option<User>, message: Message, conn: DbConn) -> Template {
|
||||
Template::render("session/login", json!({
|
||||
"account": user.map(|u| u.to_json(&*conn)),
|
||||
"message": message.m,
|
||||
"errors": null,
|
||||
"form": null
|
||||
}))
|
||||
#[get("/login?<m>")]
|
||||
pub fn new_message(user: Option<User>, m: String, conn: DbConn, intl: I18n) -> Ructe {
|
||||
render!(session::login(
|
||||
&(&*conn, &intl.catalog, user),
|
||||
Some(i18n!(intl.catalog, &m).to_string()),
|
||||
&LoginForm::default(),
|
||||
ValidationErrors::default()
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
#[derive(FromForm, Validate, Serialize)]
|
||||
struct LoginForm {
|
||||
#[derive(Default, FromForm, Validate, Serialize)]
|
||||
pub struct LoginForm {
|
||||
#[validate(length(min = "1", message = "We need an email or a username to identify you"))]
|
||||
email_or_name: String,
|
||||
pub email_or_name: String,
|
||||
#[validate(length(min = "1", message = "Your password can't be empty"))]
|
||||
password: String
|
||||
pub password: String
|
||||
}
|
||||
|
||||
#[post("/login", data = "<data>")]
|
||||
fn create(conn: DbConn, data: LenientForm<LoginForm>, flash: Option<FlashMessage>, mut cookies: Cookies) -> Result<Redirect, Template> {
|
||||
let form = data.get();
|
||||
#[post("/login", data = "<form>")]
|
||||
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_local(&*conn, &form.email_or_name));
|
||||
let mut errors = match form.validate() {
|
||||
Ok(_) => ValidationErrors::new(),
|
||||
Err(e) => e
|
||||
};
|
||||
|
||||
|
||||
if let Some(user) = user.clone() {
|
||||
if !user.auth(&form.password) {
|
||||
let mut err = ValidationError::new("invalid_login");
|
||||
@@ -87,27 +83,26 @@ fn create(conn: DbConn, data: LenientForm<LoginForm>, flash: Option<FlashMessage
|
||||
|
||||
let uri = Uri::parse(&destination)
|
||||
.map(|x| x.into_owned())
|
||||
.map_err(|_| {
|
||||
Template::render("session/login", json!({
|
||||
"account": null,
|
||||
"errors": errors.inner(),
|
||||
"form": form
|
||||
}))
|
||||
})?;
|
||||
.map_err(|_| render!(session::login(
|
||||
&(&*conn, &intl.catalog, None),
|
||||
None,
|
||||
&*form,
|
||||
errors
|
||||
)))?;
|
||||
|
||||
Ok(Redirect::to(uri))
|
||||
} else {
|
||||
println!("{:?}", errors);
|
||||
Err(Template::render("session/login", json!({
|
||||
"account": null,
|
||||
"errors": errors.inner(),
|
||||
"form": form
|
||||
})))
|
||||
Err(render!(session::login(
|
||||
&(&*conn, &intl.catalog, None),
|
||||
None,
|
||||
&*form,
|
||||
errors
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/logout")]
|
||||
fn delete(mut cookies: Cookies) -> Redirect {
|
||||
pub fn delete(mut cookies: Cookies) -> Redirect {
|
||||
if let Some(cookie) = cookies.get_private(AUTH_COOKIE) {
|
||||
cookies.remove_private(cookie);
|
||||
}
|
||||
|
||||
+12
-12
@@ -1,5 +1,4 @@
|
||||
use rocket_contrib::Template;
|
||||
use serde_json;
|
||||
use rocket_i18n::I18n;
|
||||
|
||||
use plume_models::{
|
||||
db_conn::DbConn,
|
||||
@@ -7,20 +6,21 @@ use plume_models::{
|
||||
users::User,
|
||||
};
|
||||
use routes::Page;
|
||||
use template_utils::Ructe;
|
||||
|
||||
#[get("/tag/<name>")]
|
||||
fn tag(user: Option<User>, conn: DbConn, name: String) -> Template {
|
||||
paginated_tag(user, conn, name, Page::first())
|
||||
pub fn tag(user: Option<User>, conn: DbConn, name: String, intl: I18n) -> Ructe {
|
||||
paginated_tag(user, conn, name, Page::first(), intl)
|
||||
}
|
||||
|
||||
#[get("/tag/<name>?<page>")]
|
||||
fn paginated_tag(user: Option<User>, conn: DbConn, name: String, page: Page) -> Template {
|
||||
pub fn paginated_tag(user: Option<User>, conn: DbConn, name: String, page: Page, intl: I18n) -> Ructe {
|
||||
let posts = Post::list_by_tag(&*conn, name.clone(), page.limits());
|
||||
Template::render("tags/index", json!({
|
||||
"tag": name.clone(),
|
||||
"account": user.map(|u| u.to_json(&*conn)),
|
||||
"articles": posts.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
|
||||
"page": page.page,
|
||||
"n_pages": Page::total(Post::count_for_tag(&*conn, name) as i32)
|
||||
}))
|
||||
render!(tags::index(
|
||||
&(&*conn, &intl.catalog, user),
|
||||
name.clone(),
|
||||
posts,
|
||||
page.0,
|
||||
Page::total(Post::count_for_tag(&*conn, name) as i32)
|
||||
))
|
||||
}
|
||||
|
||||
+143
-183
@@ -5,9 +5,9 @@ use rocket::{
|
||||
request::LenientForm,
|
||||
response::{status, Content, Flash, Redirect},
|
||||
};
|
||||
use rocket_contrib::Template;
|
||||
use rocket_i18n::I18n;
|
||||
use serde_json;
|
||||
use validator::{Validate, ValidationError};
|
||||
use validator::{Validate, ValidationError, ValidationErrors};
|
||||
|
||||
use inbox::Inbox;
|
||||
use plume_common::activity_pub::{
|
||||
@@ -22,11 +22,12 @@ use plume_models::{
|
||||
reshares::Reshare, users::*,
|
||||
};
|
||||
use routes::Page;
|
||||
use template_utils::Ructe;
|
||||
use Worker;
|
||||
use Searcher;
|
||||
|
||||
#[get("/me")]
|
||||
fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
|
||||
pub fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
|
||||
match user {
|
||||
Some(user) => Ok(Redirect::to(uri!(details: name = user.username))),
|
||||
None => Err(utils::requires_login("", uri!(me))),
|
||||
@@ -34,7 +35,7 @@ fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
|
||||
}
|
||||
|
||||
#[get("/@/<name>", rank = 2)]
|
||||
fn details(
|
||||
pub fn details(
|
||||
name: String,
|
||||
conn: DbConn,
|
||||
account: Option<User>,
|
||||
@@ -42,111 +43,96 @@ fn details(
|
||||
fetch_articles_conn: DbConn,
|
||||
fetch_followers_conn: DbConn,
|
||||
update_conn: DbConn,
|
||||
intl: I18n,
|
||||
searcher: Searcher,
|
||||
) -> Template {
|
||||
may_fail!(
|
||||
account.map(|a| a.to_json(&*conn)),
|
||||
User::find_by_fqn(&*conn, &name),
|
||||
"Couldn't find requested user",
|
||||
|user| {
|
||||
let recents = Post::get_recents_for_author(&*conn, &user, 6);
|
||||
let reshares = Reshare::get_recents_for_author(&*conn, &user, 6);
|
||||
let user_id = user.id;
|
||||
let n_followers = user.get_followers(&*conn).len();
|
||||
) -> Result<Ructe, Ructe> {
|
||||
let user = User::find_by_fqn(&*conn, &name).ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, account.clone()))))?;
|
||||
let recents = Post::get_recents_for_author(&*conn, &user, 6);
|
||||
let reshares = Reshare::get_recents_for_author(&*conn, &user, 6);
|
||||
|
||||
if !user.get_instance(&*conn).local {
|
||||
// Fetch new articles
|
||||
let user_clone = user.clone();
|
||||
let searcher = searcher.clone();
|
||||
worker.execute(move || {
|
||||
for create_act in user_clone.fetch_outbox::<Create>() {
|
||||
match create_act.create_props.object_object::<Article>() {
|
||||
Ok(article) => {
|
||||
Post::from_activity(
|
||||
&(&fetch_articles_conn, &searcher),
|
||||
article,
|
||||
user_clone.clone().into_id(),
|
||||
);
|
||||
println!("Fetched article from remote user");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error while fetching articles in background: {:?}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch followers
|
||||
let user_clone = user.clone();
|
||||
worker.execute(move || {
|
||||
for user_id in user_clone.fetch_followers_ids() {
|
||||
let follower =
|
||||
User::find_by_ap_url(&*fetch_followers_conn, &user_id)
|
||||
.unwrap_or_else(|| {
|
||||
User::fetch_from_url(&*fetch_followers_conn, &user_id)
|
||||
.expect("user::details: Couldn't fetch follower")
|
||||
});
|
||||
follows::Follow::insert(
|
||||
&*fetch_followers_conn,
|
||||
follows::NewFollow {
|
||||
follower_id: follower.id,
|
||||
following_id: user_clone.id,
|
||||
ap_url: format!("{}/follow/{}", follower.ap_url, user_clone.ap_url),
|
||||
},
|
||||
if !user.get_instance(&*conn).local {
|
||||
// Fetch new articles
|
||||
let user_clone = user.clone();
|
||||
let searcher = searcher.clone();
|
||||
worker.execute(move || {
|
||||
for create_act in user_clone.fetch_outbox::<Create>() {
|
||||
match create_act.create_props.object_object::<Article>() {
|
||||
Ok(article) => {
|
||||
Post::from_activity(
|
||||
&(&*fetch_articles_conn, &searcher),
|
||||
article,
|
||||
user_clone.clone().into_id(),
|
||||
);
|
||||
println!("Fetched article from remote user");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error while fetching articles in background: {:?}", e)
|
||||
}
|
||||
});
|
||||
|
||||
// Update profile information if needed
|
||||
let user_clone = user.clone();
|
||||
if user.needs_update() {
|
||||
worker.execute(move || {
|
||||
user_clone.refetch(&*update_conn);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Template::render(
|
||||
"users/details",
|
||||
json!({
|
||||
"user": user.to_json(&*conn),
|
||||
"instance_url": user.get_instance(&*conn).public_domain,
|
||||
"is_remote": user.instance_id != Instance::local_id(&*conn),
|
||||
"follows": account.clone().map(|x| x.is_following(&*conn, user.id)).unwrap_or(false),
|
||||
"account": account.clone().map(|a| a.to_json(&*conn)),
|
||||
"recents": recents.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
|
||||
"reshares": reshares.into_iter().map(|r| r.get_post(&*conn).unwrap().to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
|
||||
"is_self": account.map(|a| a.id == user_id).unwrap_or(false),
|
||||
"n_followers": n_followers
|
||||
}),
|
||||
)
|
||||
// Fetch followers
|
||||
let user_clone = user.clone();
|
||||
worker.execute(move || {
|
||||
for user_id in user_clone.fetch_followers_ids() {
|
||||
let follower =
|
||||
User::find_by_ap_url(&*fetch_followers_conn, &user_id)
|
||||
.unwrap_or_else(|| {
|
||||
User::fetch_from_url(&*fetch_followers_conn, &user_id)
|
||||
.expect("user::details: Couldn't fetch follower")
|
||||
});
|
||||
follows::Follow::insert(
|
||||
&*fetch_followers_conn,
|
||||
follows::NewFollow {
|
||||
follower_id: follower.id,
|
||||
following_id: user_clone.id,
|
||||
ap_url: format!("{}/follow/{}", follower.ap_url, user_clone.ap_url),
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Update profile information if needed
|
||||
let user_clone = user.clone();
|
||||
if user.needs_update() {
|
||||
worker.execute(move || {
|
||||
user_clone.refetch(&*update_conn);
|
||||
});
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Ok(render!(users::details(
|
||||
&(&*conn, &intl.catalog, account.clone()),
|
||||
user.clone(),
|
||||
account.map(|x| x.is_following(&*conn, user.id)).unwrap_or(false),
|
||||
user.instance_id != Instance::local_id(&*conn),
|
||||
user.get_instance(&*conn).public_domain,
|
||||
recents,
|
||||
reshares.into_iter().map(|r| r.get_post(&*conn).expect("user::details: Reshared post error")).collect()
|
||||
)))
|
||||
}
|
||||
|
||||
#[get("/dashboard")]
|
||||
fn dashboard(user: User, conn: DbConn) -> Template {
|
||||
pub fn dashboard(user: User, conn: DbConn, intl: I18n) -> Ructe {
|
||||
let blogs = Blog::find_for_author(&*conn, &user);
|
||||
Template::render(
|
||||
"users/dashboard",
|
||||
json!({
|
||||
"account": user.to_json(&*conn),
|
||||
"blogs": blogs,
|
||||
"drafts": Post::drafts_by_author(&*conn, &user).into_iter().map(|a| a.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
|
||||
}),
|
||||
)
|
||||
render!(users::dashboard(
|
||||
&(&*conn, &intl.catalog, Some(user.clone())),
|
||||
blogs,
|
||||
Post::drafts_by_author(&*conn, &user)
|
||||
))
|
||||
}
|
||||
|
||||
#[get("/dashboard", rank = 2)]
|
||||
fn dashboard_auth() -> Flash<Redirect> {
|
||||
pub fn dashboard_auth(i18n: I18n) -> Flash<Redirect> {
|
||||
utils::requires_login(
|
||||
"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")]
|
||||
fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Option<Redirect> {
|
||||
pub fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Option<Redirect> {
|
||||
let target = User::find_by_fqn(&*conn, &name)?;
|
||||
if let Some(follow) = follows::Follow::find(&*conn, user.id, target.id) {
|
||||
let delete_act = follow.delete(&*conn);
|
||||
@@ -171,49 +157,37 @@ fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Option<Redi
|
||||
}
|
||||
|
||||
#[post("/@/<name>/follow", rank = 2)]
|
||||
fn follow_auth(name: String) -> Flash<Redirect> {
|
||||
pub fn follow_auth(name: String, i18n: I18n) -> Flash<Redirect> {
|
||||
utils::requires_login(
|
||||
"You need to be logged in order to follow someone",
|
||||
i18n!(i18n.catalog, "You need to be logged in order to follow someone"),
|
||||
uri!(follow: name = name),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/@/<name>/followers?<page>")]
|
||||
fn followers_paginated(name: String, conn: DbConn, account: Option<User>, page: Page) -> Template {
|
||||
may_fail!(
|
||||
account.map(|a| a.to_json(&*conn)),
|
||||
User::find_by_fqn(&*conn, &name),
|
||||
"Couldn't find requested user",
|
||||
|user| {
|
||||
let user_id = user.id;
|
||||
let followers_count = user.get_followers(&*conn).len();
|
||||
pub fn followers_paginated(name: String, conn: DbConn, account: Option<User>, page: Page, intl: I18n) -> Result<Ructe, Ructe> {
|
||||
let user = User::find_by_fqn(&*conn, &name).ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, account.clone()))))?;
|
||||
let followers_count = user.get_followers(&*conn).len(); // TODO: count in DB
|
||||
|
||||
Template::render(
|
||||
"users/followers",
|
||||
json!({
|
||||
"user": user.to_json(&*conn),
|
||||
"instance_url": user.get_instance(&*conn).public_domain,
|
||||
"is_remote": user.instance_id != Instance::local_id(&*conn),
|
||||
"follows": account.clone().map(|x| x.is_following(&*conn, user.id)).unwrap_or(false),
|
||||
"followers": user.get_followers_page(&*conn, page.limits()).into_iter().map(|f| f.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
|
||||
"account": account.clone().map(|a| a.to_json(&*conn)),
|
||||
"is_self": account.map(|a| a.id == user_id).unwrap_or(false),
|
||||
"n_followers": followers_count,
|
||||
"page": page.page,
|
||||
"n_pages": Page::total(followers_count as i32)
|
||||
}),
|
||||
)
|
||||
}
|
||||
)
|
||||
Ok(render!(users::followers(
|
||||
&(&*conn, &intl.catalog, account.clone()),
|
||||
user.clone(),
|
||||
account.map(|x| x.is_following(&*conn, user.id)).unwrap_or(false),
|
||||
user.instance_id != Instance::local_id(&*conn),
|
||||
user.get_instance(&*conn).public_domain,
|
||||
user.get_followers_page(&*conn, page.limits()),
|
||||
page.0,
|
||||
Page::total(followers_count as i32)
|
||||
)))
|
||||
}
|
||||
|
||||
#[get("/@/<name>/followers", rank = 2)]
|
||||
fn followers(name: String, conn: DbConn, account: Option<User>) -> Template {
|
||||
followers_paginated(name, conn, account, Page::first())
|
||||
pub fn followers(name: String, conn: DbConn, account: Option<User>, intl: I18n) -> Result<Ructe, Ructe> {
|
||||
followers_paginated(name, conn, account, Page::first(), intl)
|
||||
}
|
||||
|
||||
#[get("/@/<name>", rank = 1)]
|
||||
fn activity_details(
|
||||
pub fn activity_details(
|
||||
name: String,
|
||||
conn: DbConn,
|
||||
_ap: ApRequest,
|
||||
@@ -223,71 +197,60 @@ fn activity_details(
|
||||
}
|
||||
|
||||
#[get("/users/new")]
|
||||
fn new(user: Option<User>, conn: DbConn) -> Template {
|
||||
Template::render(
|
||||
"users/new",
|
||||
json!({
|
||||
"enabled": Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true),
|
||||
"account": user.map(|u| u.to_json(&*conn)),
|
||||
"errors": null,
|
||||
"form": null
|
||||
}),
|
||||
)
|
||||
pub fn new(user: Option<User>, conn: DbConn, intl: I18n) -> Ructe {
|
||||
render!(users::new(
|
||||
&(&*conn, &intl.catalog, user),
|
||||
Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true),
|
||||
&NewUserForm::default(),
|
||||
ValidationErrors::default()
|
||||
))
|
||||
}
|
||||
|
||||
#[get("/@/<name>/edit")]
|
||||
fn edit(name: String, user: User, conn: DbConn) -> Option<Template> {
|
||||
pub fn edit(name: String, user: User, conn: DbConn, intl: I18n) -> Option<Ructe> {
|
||||
if user.username == name && !name.contains('@') {
|
||||
Some(Template::render(
|
||||
"users/edit",
|
||||
json!({
|
||||
"account": user.to_json(&*conn)
|
||||
}),
|
||||
))
|
||||
Some(render!(users::edit(
|
||||
&(&*conn, &intl.catalog, Some(user.clone())),
|
||||
UpdateUserForm {
|
||||
display_name: user.display_name.clone(),
|
||||
email: user.email.clone().unwrap_or_default(),
|
||||
summary: user.summary.to_string(),
|
||||
},
|
||||
ValidationErrors::default()
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/@/<name>/edit", rank = 2)]
|
||||
fn edit_auth(name: String) -> Flash<Redirect> {
|
||||
pub fn edit_auth(name: String, i18n: I18n) -> Flash<Redirect> {
|
||||
utils::requires_login(
|
||||
"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),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct UpdateUserForm {
|
||||
display_name: Option<String>,
|
||||
email: Option<String>,
|
||||
summary: Option<String>,
|
||||
pub struct UpdateUserForm {
|
||||
pub display_name: String,
|
||||
pub email: String,
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
#[put("/@/<_name>/edit", data = "<data>")]
|
||||
fn update(_name: String, conn: DbConn, user: User, data: LenientForm<UpdateUserForm>) -> Redirect {
|
||||
#[put("/@/<_name>/edit", data = "<form>")]
|
||||
pub fn update(_name: String, conn: DbConn, user: User, form: LenientForm<UpdateUserForm>) -> Redirect {
|
||||
user.update(
|
||||
&*conn,
|
||||
data.get()
|
||||
.display_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| user.display_name.to_string())
|
||||
.to_string(),
|
||||
data.get()
|
||||
.email
|
||||
.clone()
|
||||
.unwrap_or_else(|| user.email.clone().unwrap())
|
||||
.to_string(),
|
||||
data.get()
|
||||
.summary
|
||||
.clone()
|
||||
.unwrap_or_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() },
|
||||
);
|
||||
Redirect::to(uri!(me))
|
||||
}
|
||||
|
||||
#[post("/@/<name>/delete")]
|
||||
fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies, searcher: Searcher) -> Option<Redirect> {
|
||||
pub fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies, searcher: Searcher) -> Option<Redirect> {
|
||||
let account = User::find_by_fqn(&*conn, &name)?;
|
||||
if user.id == account.id {
|
||||
account.delete(&*conn, &searcher);
|
||||
@@ -302,7 +265,7 @@ fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies, searcher
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Serialize, Validate)]
|
||||
#[derive(Default, FromForm, Serialize, Validate)]
|
||||
#[validate(
|
||||
schema(
|
||||
function = "passwords_match",
|
||||
@@ -310,29 +273,29 @@ fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies, searcher
|
||||
message = "Passwords are not matching"
|
||||
)
|
||||
)]
|
||||
struct NewUserForm {
|
||||
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 \""))]
|
||||
username: String,
|
||||
pub username: String,
|
||||
#[validate(email(message = "Invalid email"))]
|
||||
email: String,
|
||||
pub email: String,
|
||||
#[validate(
|
||||
length(
|
||||
min = "8",
|
||||
message = "Password should be at least 8 characters long"
|
||||
)
|
||||
)]
|
||||
password: String,
|
||||
pub password: String,
|
||||
#[validate(
|
||||
length(
|
||||
min = "8",
|
||||
message = "Password should be at least 8 characters long"
|
||||
)
|
||||
)]
|
||||
password_confirmation: String,
|
||||
pub password_confirmation: String,
|
||||
}
|
||||
|
||||
fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> {
|
||||
pub fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> {
|
||||
if form.password != form.password_confirmation {
|
||||
Err(ValidationError::new("password_match"))
|
||||
} else {
|
||||
@@ -340,7 +303,7 @@ fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_username(username: &str) -> Result<(), ValidationError> {
|
||||
pub fn validate_username(username: &str) -> Result<(), ValidationError> {
|
||||
if username.contains(&['<', '>', '&', '@', '\'', '"'][..]) {
|
||||
Err(ValidationError::new("username_illegal_char"))
|
||||
} else {
|
||||
@@ -348,16 +311,15 @@ fn validate_username(username: &str) -> Result<(), ValidationError> {
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/users/new", data = "<data>")]
|
||||
fn create(conn: DbConn, data: LenientForm<NewUserForm>) -> Result<Redirect, Template> {
|
||||
if !Instance::get_local(&*conn)
|
||||
#[post("/users/new", data = "<form>")]
|
||||
pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Result<Redirect, Ructe> {
|
||||
if !Instance::get_local(&*conn)
|
||||
.map(|i| i.open_registrations)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
return Ok(Redirect::to(uri!(new))); // Actually, it is an error
|
||||
}
|
||||
|
||||
let form = data.get();
|
||||
form.validate()
|
||||
.map(|_| {
|
||||
NewUser::new_local(
|
||||
@@ -371,26 +333,24 @@ fn create(conn: DbConn, data: LenientForm<NewUserForm>) -> Result<Redirect, Temp
|
||||
).update_boxes(&*conn);
|
||||
Redirect::to(uri!(super::session::new))
|
||||
})
|
||||
.map_err(|e| {
|
||||
Template::render(
|
||||
"users/new",
|
||||
json!({
|
||||
"enabled": Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true),
|
||||
"errors": e.inner(),
|
||||
"form": form
|
||||
}),
|
||||
)
|
||||
.map_err(|err| {
|
||||
render!(users::new(
|
||||
&(&*conn, &intl.catalog, None),
|
||||
Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true),
|
||||
&*form,
|
||||
err
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
#[get("/@/<name>/outbox")]
|
||||
fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection>> {
|
||||
pub fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection>> {
|
||||
let user = User::find_local(&*conn, &name)?;
|
||||
Some(user.outbox(&*conn))
|
||||
}
|
||||
|
||||
#[post("/@/<name>/inbox", data = "<data>")]
|
||||
fn inbox(
|
||||
pub fn inbox(
|
||||
name: String,
|
||||
conn: DbConn,
|
||||
data: String,
|
||||
@@ -433,7 +393,7 @@ fn inbox(
|
||||
}
|
||||
|
||||
#[get("/@/<name>/followers")]
|
||||
fn ap_followers(
|
||||
pub fn ap_followers(
|
||||
name: String,
|
||||
conn: DbConn,
|
||||
_ap: ApRequest,
|
||||
@@ -459,7 +419,7 @@ fn ap_followers(
|
||||
}
|
||||
|
||||
#[get("/@/<name>/atom.xml")]
|
||||
fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
|
||||
pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
|
||||
let author = User::find_by_fqn(&*conn, &name)?;
|
||||
let feed = FeedBuilder::default()
|
||||
.title(author.display_name.clone())
|
||||
|
||||
@@ -6,7 +6,7 @@ use webfinger::*;
|
||||
use plume_models::{BASE_URL, ap_url, db_conn::DbConn, blogs::Blog, users::User};
|
||||
|
||||
#[get("/.well-known/nodeinfo")]
|
||||
fn nodeinfo() -> Content<String> {
|
||||
pub fn nodeinfo() -> Content<String> {
|
||||
Content(ContentType::new("application", "jrd+json"), json!({
|
||||
"links": [
|
||||
{
|
||||
@@ -18,7 +18,7 @@ fn nodeinfo() -> Content<String> {
|
||||
}
|
||||
|
||||
#[get("/.well-known/host-meta")]
|
||||
fn host_meta() -> String {
|
||||
pub fn host_meta() -> String {
|
||||
format!(r#"
|
||||
<?xml version="1.0"?>
|
||||
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
||||
@@ -27,11 +27,6 @@ fn host_meta() -> String {
|
||||
"#, url = ap_url(&format!("{domain}/.well-known/webfinger?resource={{uri}}", domain = BASE_URL.as_str())))
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct WebfingerQuery {
|
||||
resource: String
|
||||
}
|
||||
|
||||
struct WebfingerResolver;
|
||||
|
||||
impl Resolver<DbConn> for WebfingerResolver {
|
||||
@@ -50,9 +45,9 @@ impl Resolver<DbConn> for WebfingerResolver {
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/.well-known/webfinger?<query>")]
|
||||
fn webfinger(query: WebfingerQuery, conn: DbConn) -> Content<String> {
|
||||
match WebfingerResolver::endpoint(query.resource, conn).and_then(|wf| serde_json::to_string(&wf).map_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)) {
|
||||
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",
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
use plume_models::{Connection, users::User};
|
||||
use rocket::response::Content;
|
||||
use rocket_i18n::Catalog;
|
||||
use templates::Html;
|
||||
|
||||
pub use askama_escape::escape;
|
||||
|
||||
pub type BaseContext<'a> = &'a(&'a Connection, &'a Catalog, Option<User>);
|
||||
|
||||
pub type Ructe = Content<Vec<u8>>;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! render {
|
||||
($group:tt :: $page:tt ( $( $param:expr ),* ) ) => {
|
||||
{
|
||||
use rocket::{http::ContentType, response::Content};
|
||||
use templates;
|
||||
|
||||
let mut res = vec![];
|
||||
templates::$group::$page(
|
||||
&mut res,
|
||||
$(
|
||||
$param
|
||||
),*
|
||||
).unwrap();
|
||||
Content(ContentType::HTML, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Size {
|
||||
Small,
|
||||
Medium,
|
||||
}
|
||||
|
||||
impl Size {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Size::Small => "small",
|
||||
Size::Medium => "medium",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn avatar(conn: &Connection, user: &User, size: Size, pad: bool, catalog: &Catalog) -> Html<String> {
|
||||
let name = escape(&user.name(conn)).to_string();
|
||||
Html(format!(
|
||||
r#"<div
|
||||
class="avatar {size} {padded}"
|
||||
style="background-image: url('{url}');"
|
||||
title="{title}"
|
||||
aria-label="{title}"
|
||||
></div>"#,
|
||||
size = size.as_str(),
|
||||
padded = if pad { "padded" } else { "" },
|
||||
url = user.avatar_url(conn),
|
||||
title = i18n!(catalog, "{0}'s avatar"; name),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn tabs(links: &[(&str, &str, bool)]) -> Html<String> {
|
||||
let mut res = String::from(r#"<div class="tabs">"#);
|
||||
for (url, title, selected) in links {
|
||||
res.push_str(r#"<a href=""#);
|
||||
res.push_str(url);
|
||||
if *selected {
|
||||
res.push_str(r#"" class="selected">"#);
|
||||
} else {
|
||||
res.push_str("\">");
|
||||
}
|
||||
res.push_str(title);
|
||||
res.push_str("</a>");
|
||||
}
|
||||
res.push_str("</div>");
|
||||
Html(res)
|
||||
}
|
||||
|
||||
pub fn paginate(catalog: &Catalog, page: i32, total: i32) -> Html<String> {
|
||||
let mut res = String::new();
|
||||
res.push_str(r#"<div class="pagination">"#);
|
||||
if page != 1 {
|
||||
res.push_str(format!(r#"<a href="?page={}">{}</a>"#, page - 1, catalog.gettext("Previous page")).as_str());
|
||||
}
|
||||
if page < total {
|
||||
res.push_str(format!(r#"<a href="?page={}">{}</a>"#, page + 1, catalog.gettext("Next page")).as_str());
|
||||
}
|
||||
res.push_str("</div>");
|
||||
Html(res)
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! icon {
|
||||
($name:expr) => {
|
||||
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;
|
||||
|
||||
Html(format!(r#"
|
||||
<label for="{name}">
|
||||
{label}
|
||||
{optional}
|
||||
{details}
|
||||
</label>
|
||||
{error}
|
||||
<input type="{kind}" id="{name}" name="{name}" value="{val}" {props}/>
|
||||
"#,
|
||||
name = stringify!($name),
|
||||
label = i18n!($catalog, $label),
|
||||
kind = stringify!($kind),
|
||||
optional = if $optional { format!("<small>{}</small>", i18n!($catalog, "Optional")) } else { String::new() },
|
||||
details = if $details.len() > 0 {
|
||||
format!("<small>{}</small>", i18n!($catalog, $details))
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
error = if let Some(ValidationErrorsKind::Field(errs)) = $err.errors().get(stringify!($name)) {
|
||||
format!(r#"<p class="error">{}</p>"#, i18n!($catalog, &*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)
|
||||
};
|
||||
($catalog:expr, $name:tt (optional $kind:tt), $label:expr, $form:expr, $err:expr, $props:expr) => {
|
||||
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)
|
||||
};
|
||||
($catalog:expr, $name:tt ($kind:tt), $label:expr, $form:expr, $err:expr, $props:expr) => {
|
||||
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, "")
|
||||
};
|
||||
($catalog:expr, $name:tt ($kind:tt), $label:expr, $props:expr) => {
|
||||
{
|
||||
Html(format!(r#"
|
||||
<label for="{name}">{label}</label>
|
||||
<input type="{kind}" id="{name}" name="{name}" {props}/>
|
||||
"#,
|
||||
name = stringify!($name),
|
||||
label = i18n!($catalog, $label),
|
||||
kind = stringify!($kind),
|
||||
props = $props
|
||||
))
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user