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:
Baptiste Gelez
2018-12-06 18:54:16 +01:00
committed by GitHub
parent 5f059c3e98
commit 70af57c6e1
121 changed files with 3132 additions and 3260 deletions
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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));
+5 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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())
+5 -10
View File
@@ -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",
+160
View File
@@ -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
))
}
};
}