Plume/src/routes/blogs.rs

528 lines
17 KiB
Rust
Raw Normal View History

use activitystreams::collection::{OrderedCollection, OrderedCollectionPage};
use diesel::SaveChangesDsl;
2018-05-19 09:39:59 +02:00
use rocket::{
http::ContentType,
2018-06-24 18:58:57 +02:00
request::LenientForm,
2019-03-20 17:56:17 +01:00
response::{content::Content, Flash, Redirect},
2018-05-19 09:39:59 +02:00
};
use rocket_i18n::I18n;
2019-03-20 17:56:17 +01:00
use std::{borrow::Cow, collections::HashMap};
2018-07-06 11:51:19 +02:00
use validator::{Validate, ValidationError, ValidationErrors};
2018-04-23 12:54:37 +02:00
2020-01-21 07:02:03 +01:00
use crate::routes::{errors::ErrorPage, Page, RespondOrRedirect};
use crate::template_utils::{IntoContext, Ructe};
use crate::utils::requires_login;
use plume_common::activity_pub::{ActivityStream, ApRequest};
use plume_common::utils;
use plume_models::{
2021-01-30 13:44:29 +01:00
blog_authors::*, blogs::*, db_conn::DbConn, instance::Instance, medias::*, posts::Post,
safe_string::SafeString, users::User, Connection, PlumeRocket,
};
2018-04-23 12:54:37 +02:00
#[get("/~/<name>?<page>", rank = 2)]
2021-01-30 13:44:29 +01:00
pub fn details(
name: String,
page: Option<Page>,
conn: DbConn,
rockets: PlumeRocket,
) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default();
2021-01-30 13:44:29 +01:00
let blog = Blog::find_by_fqn(&conn, &name)?;
let posts = Post::blog_page(&conn, &blog, page.limits())?;
let articles_count = Post::count_for_blog(&conn, &blog)?;
let authors = &blog.list_authors(&conn)?;
Ok(render!(blogs::details(
2021-01-30 13:44:29 +01:00
&(&conn, &rockets).to_context(),
blog,
authors,
page.0,
Page::total(articles_count as i32),
posts
)))
2018-04-23 12:54:37 +02:00
}
#[get("/~/<name>", rank = 1)]
2019-03-20 17:56:17 +01:00
pub fn activity_details(
name: String,
2021-01-30 13:44:29 +01:00
conn: DbConn,
2019-03-20 17:56:17 +01:00
_ap: ApRequest,
) -> Option<ActivityStream<CustomGroup>> {
2021-01-30 13:44:29 +01:00
let blog = Blog::find_by_fqn(&conn, &name).ok()?;
Some(ActivityStream::new(blog.to_activity(&conn).ok()?))
2018-04-23 17:09:05 +02:00
}
2018-04-23 12:54:37 +02:00
#[get("/blogs/new")]
2021-01-30 13:44:29 +01:00
pub fn new(conn: DbConn, rockets: PlumeRocket, _user: User) -> Ructe {
render!(blogs::new(
2021-01-30 13:44:29 +01:00
&(&conn, &rockets).to_context(),
&NewBlogForm::default(),
ValidationErrors::default()
))
2018-04-23 12:54:37 +02:00
}
2018-06-04 21:57:03 +02:00
#[get("/blogs/new", rank = 2)]
2019-03-20 17:56:17 +01:00
pub fn new_auth(i18n: I18n) -> Flash<Redirect> {
requires_login(
2019-03-20 17:56:17 +01:00
&i18n!(
i18n.catalog,
"To create a new blog, you need to be logged in"
2019-03-20 17:56:17 +01:00
),
uri!(new),
)
2018-06-04 21:57:03 +02:00
}
#[derive(Default, FromForm, Validate)]
pub struct NewBlogForm {
#[validate(custom(function = "valid_slug", message = "Invalid name"))]
pub title: String,
2018-04-23 12:54:37 +02:00
}
2018-06-29 14:56:00 +02:00
fn valid_slug(title: &str) -> Result<(), ValidationError> {
let slug = utils::make_actor_id(title);
if slug.is_empty() {
2018-06-29 14:56:00 +02:00
Err(ValidationError::new("empty_slug"))
} else {
Ok(())
}
}
#[post("/blogs/new", data = "<form>")]
2021-01-30 13:44:29 +01:00
pub fn create(
form: LenientForm<NewBlogForm>,
conn: DbConn,
rockets: PlumeRocket,
) -> RespondOrRedirect {
let slug = utils::make_actor_id(&form.title);
Big refactoring of the Inbox (#443) * Big refactoring of the Inbox We now have a type that routes an activity through the registered handlers until one of them matches. Each Actor/Activity/Object combination is represented by an implementation of AsObject These combinations are then registered on the Inbox type, which will try to deserialize the incoming activity in the requested types. Advantages: - nicer syntax: the final API is clearer and more idiomatic - more generic: only two traits (`AsActor` and `AsObject`) instead of one for each kind of activity - it is easier to see which activities we handle and which one we don't * Small fixes - Avoid panics - Don't search for AP ID infinitely - Code style issues * Fix tests * Introduce a new trait: FromId It should be implemented for any AP object. It allows to look for an object in database using its AP ID, or to dereference it if it was not present in database Also moves the inbox code to plume-models to test it (and write a basic test for each activity type we handle) * Use if let instead of match * Don't require PlumeRocket::intl for tests * Return early and remove a forgotten dbg! * Add more tests to try to understand where the issues come from * Also add a test for comment federation * Don't check creation_date is the same for blogs * Make user and blog federation more tolerant to errors/missing fields * Make clippy happy * Use the correct Accept header when dereferencing * Fix follow approval with Mastodon * Add spaces to characters that should not be in usernames And validate blog names too * Smarter dereferencing: only do it once for each actor/object * Forgot some files * Cargo fmt * Delete plume_test * Delete plume_tests * Update get_id docs + Remove useless : Sized * Appease cargo fmt * Remove dbg! + Use as_ref instead of clone when possible + Use and_then instead of map when possible * Remove .po~ * send unfollow to local instance * read cover from update activity * Make sure "cc" and "to" are never empty and fix a typo in a constant name * Cargo fmt
2019-04-17 19:31:47 +02:00
let intl = &rockets.intl.catalog;
let user = rockets.user.clone().unwrap();
2018-07-06 11:51:19 +02:00
let mut errors = match form.validate() {
Ok(_) => ValidationErrors::new(),
2019-03-20 17:56:17 +01:00
Err(e) => e,
2018-07-06 11:51:19 +02:00
};
2021-01-30 13:44:29 +01:00
if Blog::find_by_fqn(&conn, &slug).is_ok() {
2019-03-20 17:56:17 +01:00
errors.add(
"title",
ValidationError {
code: Cow::from("existing_slug"),
Big refactoring of the Inbox (#443) * Big refactoring of the Inbox We now have a type that routes an activity through the registered handlers until one of them matches. Each Actor/Activity/Object combination is represented by an implementation of AsObject These combinations are then registered on the Inbox type, which will try to deserialize the incoming activity in the requested types. Advantages: - nicer syntax: the final API is clearer and more idiomatic - more generic: only two traits (`AsActor` and `AsObject`) instead of one for each kind of activity - it is easier to see which activities we handle and which one we don't * Small fixes - Avoid panics - Don't search for AP ID infinitely - Code style issues * Fix tests * Introduce a new trait: FromId It should be implemented for any AP object. It allows to look for an object in database using its AP ID, or to dereference it if it was not present in database Also moves the inbox code to plume-models to test it (and write a basic test for each activity type we handle) * Use if let instead of match * Don't require PlumeRocket::intl for tests * Return early and remove a forgotten dbg! * Add more tests to try to understand where the issues come from * Also add a test for comment federation * Don't check creation_date is the same for blogs * Make user and blog federation more tolerant to errors/missing fields * Make clippy happy * Use the correct Accept header when dereferencing * Fix follow approval with Mastodon * Add spaces to characters that should not be in usernames And validate blog names too * Smarter dereferencing: only do it once for each actor/object * Forgot some files * Cargo fmt * Delete plume_test * Delete plume_tests * Update get_id docs + Remove useless : Sized * Appease cargo fmt * Remove dbg! + Use as_ref instead of clone when possible + Use and_then instead of map when possible * Remove .po~ * send unfollow to local instance * read cover from update activity * Make sure "cc" and "to" are never empty and fix a typo in a constant name * Cargo fmt
2019-04-17 19:31:47 +02:00
message: Some(Cow::from(i18n!(
intl,
"A blog with the same name already exists."
))),
2019-03-20 17:56:17 +01:00
params: HashMap::new(),
},
);
2018-07-06 11:51:19 +02:00
}
if !errors.is_empty() {
2021-01-30 13:44:29 +01:00
return render!(blogs::new(&(&conn, &rockets).to_context(), &*form, errors)).into();
}
2019-03-20 17:56:17 +01:00
let blog = Blog::insert(
2021-01-30 13:44:29 +01:00
&conn,
NewBlog::new_local(
slug.clone(),
form.title.to_string(),
String::from(""),
Instance::get_local()
.expect("blog::create: instance error")
.id,
2019-03-20 17:56:17 +01:00
)
.expect("blog::create: new local error"),
)
.expect("blog::create: error");
BlogAuthor::insert(
2021-01-30 13:44:29 +01:00
&conn,
NewBlogAuthor {
blog_id: blog.id,
author_id: user.id,
is_owner: true,
},
)
.expect("blog::create: author error");
Flash::success(
Redirect::to(uri!(details: name = slug, page = _)),
&i18n!(intl, "Your blog was successfully created!"),
)
.into()
2018-04-23 12:54:37 +02:00
}
2018-04-29 19:49:56 +02:00
2018-10-20 15:03:59 +02:00
#[post("/~/<name>/delete")]
2021-01-30 13:44:29 +01:00
pub fn delete(name: String, conn: DbConn, rockets: PlumeRocket) -> RespondOrRedirect {
let blog = Blog::find_by_fqn(&conn, &name).expect("blog::delete: blog not found");
if rockets
.user
2019-03-20 17:56:17 +01:00
.clone()
2021-01-30 13:44:29 +01:00
.and_then(|u| u.is_author_in(&conn, &blog).ok())
2019-03-20 17:56:17 +01:00
.unwrap_or(false)
{
2021-01-30 13:44:29 +01:00
blog.delete(&*conn).expect("blog::expect: deletion error");
Flash::success(
Redirect::to(uri!(super::instance::index)),
i18n!(rockets.intl.catalog, "Your blog was deleted."),
)
.into()
2018-10-20 15:03:59 +02:00
} else {
// TODO actually return 403 error code
render!(errors::not_authorized(
2021-01-30 13:44:29 +01:00
&(&conn, &rockets).to_context(),
i18n!(
rockets.intl.catalog,
"You are not allowed to delete this blog."
)
))
.into()
2018-10-20 15:03:59 +02:00
}
}
#[derive(FromForm, Validate)]
pub struct EditForm {
#[validate(custom(function = "valid_slug", message = "Invalid name"))]
pub title: String,
pub summary: String,
pub icon: Option<i32>,
pub banner: Option<i32>,
pub theme: Option<String>,
}
#[get("/~/<name>/edit")]
2021-01-30 13:44:29 +01:00
pub fn edit(name: String, conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
let blog = Blog::find_by_fqn(&conn, &name)?;
Big refactoring of the Inbox (#443) * Big refactoring of the Inbox We now have a type that routes an activity through the registered handlers until one of them matches. Each Actor/Activity/Object combination is represented by an implementation of AsObject These combinations are then registered on the Inbox type, which will try to deserialize the incoming activity in the requested types. Advantages: - nicer syntax: the final API is clearer and more idiomatic - more generic: only two traits (`AsActor` and `AsObject`) instead of one for each kind of activity - it is easier to see which activities we handle and which one we don't * Small fixes - Avoid panics - Don't search for AP ID infinitely - Code style issues * Fix tests * Introduce a new trait: FromId It should be implemented for any AP object. It allows to look for an object in database using its AP ID, or to dereference it if it was not present in database Also moves the inbox code to plume-models to test it (and write a basic test for each activity type we handle) * Use if let instead of match * Don't require PlumeRocket::intl for tests * Return early and remove a forgotten dbg! * Add more tests to try to understand where the issues come from * Also add a test for comment federation * Don't check creation_date is the same for blogs * Make user and blog federation more tolerant to errors/missing fields * Make clippy happy * Use the correct Accept header when dereferencing * Fix follow approval with Mastodon * Add spaces to characters that should not be in usernames And validate blog names too * Smarter dereferencing: only do it once for each actor/object * Forgot some files * Cargo fmt * Delete plume_test * Delete plume_tests * Update get_id docs + Remove useless : Sized * Appease cargo fmt * Remove dbg! + Use as_ref instead of clone when possible + Use and_then instead of map when possible * Remove .po~ * send unfollow to local instance * read cover from update activity * Make sure "cc" and "to" are never empty and fix a typo in a constant name * Cargo fmt
2019-04-17 19:31:47 +02:00
if rockets
.user
.clone()
2021-01-30 13:44:29 +01:00
.and_then(|u| u.is_author_in(&conn, &blog).ok())
.unwrap_or(false)
{
Big refactoring of the Inbox (#443) * Big refactoring of the Inbox We now have a type that routes an activity through the registered handlers until one of them matches. Each Actor/Activity/Object combination is represented by an implementation of AsObject These combinations are then registered on the Inbox type, which will try to deserialize the incoming activity in the requested types. Advantages: - nicer syntax: the final API is clearer and more idiomatic - more generic: only two traits (`AsActor` and `AsObject`) instead of one for each kind of activity - it is easier to see which activities we handle and which one we don't * Small fixes - Avoid panics - Don't search for AP ID infinitely - Code style issues * Fix tests * Introduce a new trait: FromId It should be implemented for any AP object. It allows to look for an object in database using its AP ID, or to dereference it if it was not present in database Also moves the inbox code to plume-models to test it (and write a basic test for each activity type we handle) * Use if let instead of match * Don't require PlumeRocket::intl for tests * Return early and remove a forgotten dbg! * Add more tests to try to understand where the issues come from * Also add a test for comment federation * Don't check creation_date is the same for blogs * Make user and blog federation more tolerant to errors/missing fields * Make clippy happy * Use the correct Accept header when dereferencing * Fix follow approval with Mastodon * Add spaces to characters that should not be in usernames And validate blog names too * Smarter dereferencing: only do it once for each actor/object * Forgot some files * Cargo fmt * Delete plume_test * Delete plume_tests * Update get_id docs + Remove useless : Sized * Appease cargo fmt * Remove dbg! + Use as_ref instead of clone when possible + Use and_then instead of map when possible * Remove .po~ * send unfollow to local instance * read cover from update activity * Make sure "cc" and "to" are never empty and fix a typo in a constant name * Cargo fmt
2019-04-17 19:31:47 +02:00
let user = rockets
.user
.clone()
Big refactoring of the Inbox (#443) * Big refactoring of the Inbox We now have a type that routes an activity through the registered handlers until one of them matches. Each Actor/Activity/Object combination is represented by an implementation of AsObject These combinations are then registered on the Inbox type, which will try to deserialize the incoming activity in the requested types. Advantages: - nicer syntax: the final API is clearer and more idiomatic - more generic: only two traits (`AsActor` and `AsObject`) instead of one for each kind of activity - it is easier to see which activities we handle and which one we don't * Small fixes - Avoid panics - Don't search for AP ID infinitely - Code style issues * Fix tests * Introduce a new trait: FromId It should be implemented for any AP object. It allows to look for an object in database using its AP ID, or to dereference it if it was not present in database Also moves the inbox code to plume-models to test it (and write a basic test for each activity type we handle) * Use if let instead of match * Don't require PlumeRocket::intl for tests * Return early and remove a forgotten dbg! * Add more tests to try to understand where the issues come from * Also add a test for comment federation * Don't check creation_date is the same for blogs * Make user and blog federation more tolerant to errors/missing fields * Make clippy happy * Use the correct Accept header when dereferencing * Fix follow approval with Mastodon * Add spaces to characters that should not be in usernames And validate blog names too * Smarter dereferencing: only do it once for each actor/object * Forgot some files * Cargo fmt * Delete plume_test * Delete plume_tests * Update get_id docs + Remove useless : Sized * Appease cargo fmt * Remove dbg! + Use as_ref instead of clone when possible + Use and_then instead of map when possible * Remove .po~ * send unfollow to local instance * read cover from update activity * Make sure "cc" and "to" are never empty and fix a typo in a constant name * Cargo fmt
2019-04-17 19:31:47 +02:00
.expect("blogs::edit: User was None while it shouldn't");
2021-01-30 13:44:29 +01:00
let medias = Media::for_user(&conn, user.id).expect("Couldn't list media");
Ok(render!(blogs::edit(
2021-01-30 13:44:29 +01:00
&(&conn, &rockets).to_context(),
&blog,
medias,
&EditForm {
title: blog.title.clone(),
summary: blog.summary.clone(),
icon: blog.icon_id,
banner: blog.banner_id,
theme: blog.theme.clone(),
},
ValidationErrors::default()
)))
} else {
// TODO actually return 403 error code
Ok(render!(errors::not_authorized(
2021-01-30 13:44:29 +01:00
&(&conn, &rockets).to_context(),
Big refactoring of the Inbox (#443) * Big refactoring of the Inbox We now have a type that routes an activity through the registered handlers until one of them matches. Each Actor/Activity/Object combination is represented by an implementation of AsObject These combinations are then registered on the Inbox type, which will try to deserialize the incoming activity in the requested types. Advantages: - nicer syntax: the final API is clearer and more idiomatic - more generic: only two traits (`AsActor` and `AsObject`) instead of one for each kind of activity - it is easier to see which activities we handle and which one we don't * Small fixes - Avoid panics - Don't search for AP ID infinitely - Code style issues * Fix tests * Introduce a new trait: FromId It should be implemented for any AP object. It allows to look for an object in database using its AP ID, or to dereference it if it was not present in database Also moves the inbox code to plume-models to test it (and write a basic test for each activity type we handle) * Use if let instead of match * Don't require PlumeRocket::intl for tests * Return early and remove a forgotten dbg! * Add more tests to try to understand where the issues come from * Also add a test for comment federation * Don't check creation_date is the same for blogs * Make user and blog federation more tolerant to errors/missing fields * Make clippy happy * Use the correct Accept header when dereferencing * Fix follow approval with Mastodon * Add spaces to characters that should not be in usernames And validate blog names too * Smarter dereferencing: only do it once for each actor/object * Forgot some files * Cargo fmt * Delete plume_test * Delete plume_tests * Update get_id docs + Remove useless : Sized * Appease cargo fmt * Remove dbg! + Use as_ref instead of clone when possible + Use and_then instead of map when possible * Remove .po~ * send unfollow to local instance * read cover from update activity * Make sure "cc" and "to" are never empty and fix a typo in a constant name * Cargo fmt
2019-04-17 19:31:47 +02:00
i18n!(
rockets.intl.catalog,
"You are not allowed to edit this blog."
)
)))
}
}
/// Returns true if the media is owned by `user` and is a picture
fn check_media(conn: &Connection, id: i32, user: &User) -> bool {
if let Ok(media) = Media::get(conn, id) {
media.owner_id == user.id && media.category() == MediaCategory::Image
} else {
false
}
}
#[put("/~/<name>/edit", data = "<form>")]
pub fn update(
name: String,
form: LenientForm<EditForm>,
2021-01-30 13:44:29 +01:00
conn: DbConn,
Big refactoring of the Inbox (#443) * Big refactoring of the Inbox We now have a type that routes an activity through the registered handlers until one of them matches. Each Actor/Activity/Object combination is represented by an implementation of AsObject These combinations are then registered on the Inbox type, which will try to deserialize the incoming activity in the requested types. Advantages: - nicer syntax: the final API is clearer and more idiomatic - more generic: only two traits (`AsActor` and `AsObject`) instead of one for each kind of activity - it is easier to see which activities we handle and which one we don't * Small fixes - Avoid panics - Don't search for AP ID infinitely - Code style issues * Fix tests * Introduce a new trait: FromId It should be implemented for any AP object. It allows to look for an object in database using its AP ID, or to dereference it if it was not present in database Also moves the inbox code to plume-models to test it (and write a basic test for each activity type we handle) * Use if let instead of match * Don't require PlumeRocket::intl for tests * Return early and remove a forgotten dbg! * Add more tests to try to understand where the issues come from * Also add a test for comment federation * Don't check creation_date is the same for blogs * Make user and blog federation more tolerant to errors/missing fields * Make clippy happy * Use the correct Accept header when dereferencing * Fix follow approval with Mastodon * Add spaces to characters that should not be in usernames And validate blog names too * Smarter dereferencing: only do it once for each actor/object * Forgot some files * Cargo fmt * Delete plume_test * Delete plume_tests * Update get_id docs + Remove useless : Sized * Appease cargo fmt * Remove dbg! + Use as_ref instead of clone when possible + Use and_then instead of map when possible * Remove .po~ * send unfollow to local instance * read cover from update activity * Make sure "cc" and "to" are never empty and fix a typo in a constant name * Cargo fmt
2019-04-17 19:31:47 +02:00
rockets: PlumeRocket,
) -> RespondOrRedirect {
Big refactoring of the Inbox (#443) * Big refactoring of the Inbox We now have a type that routes an activity through the registered handlers until one of them matches. Each Actor/Activity/Object combination is represented by an implementation of AsObject These combinations are then registered on the Inbox type, which will try to deserialize the incoming activity in the requested types. Advantages: - nicer syntax: the final API is clearer and more idiomatic - more generic: only two traits (`AsActor` and `AsObject`) instead of one for each kind of activity - it is easier to see which activities we handle and which one we don't * Small fixes - Avoid panics - Don't search for AP ID infinitely - Code style issues * Fix tests * Introduce a new trait: FromId It should be implemented for any AP object. It allows to look for an object in database using its AP ID, or to dereference it if it was not present in database Also moves the inbox code to plume-models to test it (and write a basic test for each activity type we handle) * Use if let instead of match * Don't require PlumeRocket::intl for tests * Return early and remove a forgotten dbg! * Add more tests to try to understand where the issues come from * Also add a test for comment federation * Don't check creation_date is the same for blogs * Make user and blog federation more tolerant to errors/missing fields * Make clippy happy * Use the correct Accept header when dereferencing * Fix follow approval with Mastodon * Add spaces to characters that should not be in usernames And validate blog names too * Smarter dereferencing: only do it once for each actor/object * Forgot some files * Cargo fmt * Delete plume_test * Delete plume_tests * Update get_id docs + Remove useless : Sized * Appease cargo fmt * Remove dbg! + Use as_ref instead of clone when possible + Use and_then instead of map when possible * Remove .po~ * send unfollow to local instance * read cover from update activity * Make sure "cc" and "to" are never empty and fix a typo in a constant name * Cargo fmt
2019-04-17 19:31:47 +02:00
let intl = &rockets.intl.catalog;
2021-01-30 13:44:29 +01:00
let mut blog = Blog::find_by_fqn(&conn, &name).expect("blog::update: blog not found");
if !rockets
Big refactoring of the Inbox (#443) * Big refactoring of the Inbox We now have a type that routes an activity through the registered handlers until one of them matches. Each Actor/Activity/Object combination is represented by an implementation of AsObject These combinations are then registered on the Inbox type, which will try to deserialize the incoming activity in the requested types. Advantages: - nicer syntax: the final API is clearer and more idiomatic - more generic: only two traits (`AsActor` and `AsObject`) instead of one for each kind of activity - it is easier to see which activities we handle and which one we don't * Small fixes - Avoid panics - Don't search for AP ID infinitely - Code style issues * Fix tests * Introduce a new trait: FromId It should be implemented for any AP object. It allows to look for an object in database using its AP ID, or to dereference it if it was not present in database Also moves the inbox code to plume-models to test it (and write a basic test for each activity type we handle) * Use if let instead of match * Don't require PlumeRocket::intl for tests * Return early and remove a forgotten dbg! * Add more tests to try to understand where the issues come from * Also add a test for comment federation * Don't check creation_date is the same for blogs * Make user and blog federation more tolerant to errors/missing fields * Make clippy happy * Use the correct Accept header when dereferencing * Fix follow approval with Mastodon * Add spaces to characters that should not be in usernames And validate blog names too * Smarter dereferencing: only do it once for each actor/object * Forgot some files * Cargo fmt * Delete plume_test * Delete plume_tests * Update get_id docs + Remove useless : Sized * Appease cargo fmt * Remove dbg! + Use as_ref instead of clone when possible + Use and_then instead of map when possible * Remove .po~ * send unfollow to local instance * read cover from update activity * Make sure "cc" and "to" are never empty and fix a typo in a constant name * Cargo fmt
2019-04-17 19:31:47 +02:00
.user
.clone()
2021-01-30 13:44:29 +01:00
.and_then(|u| u.is_author_in(&conn, &blog).ok())
.unwrap_or(false)
{
// TODO actually return 403 error code
return render!(errors::not_authorized(
2021-01-30 13:44:29 +01:00
&(&conn, &rockets).to_context(),
Big refactoring of the Inbox (#443) * Big refactoring of the Inbox We now have a type that routes an activity through the registered handlers until one of them matches. Each Actor/Activity/Object combination is represented by an implementation of AsObject These combinations are then registered on the Inbox type, which will try to deserialize the incoming activity in the requested types. Advantages: - nicer syntax: the final API is clearer and more idiomatic - more generic: only two traits (`AsActor` and `AsObject`) instead of one for each kind of activity - it is easier to see which activities we handle and which one we don't * Small fixes - Avoid panics - Don't search for AP ID infinitely - Code style issues * Fix tests * Introduce a new trait: FromId It should be implemented for any AP object. It allows to look for an object in database using its AP ID, or to dereference it if it was not present in database Also moves the inbox code to plume-models to test it (and write a basic test for each activity type we handle) * Use if let instead of match * Don't require PlumeRocket::intl for tests * Return early and remove a forgotten dbg! * Add more tests to try to understand where the issues come from * Also add a test for comment federation * Don't check creation_date is the same for blogs * Make user and blog federation more tolerant to errors/missing fields * Make clippy happy * Use the correct Accept header when dereferencing * Fix follow approval with Mastodon * Add spaces to characters that should not be in usernames And validate blog names too * Smarter dereferencing: only do it once for each actor/object * Forgot some files * Cargo fmt * Delete plume_test * Delete plume_tests * Update get_id docs + Remove useless : Sized * Appease cargo fmt * Remove dbg! + Use as_ref instead of clone when possible + Use and_then instead of map when possible * Remove .po~ * send unfollow to local instance * read cover from update activity * Make sure "cc" and "to" are never empty and fix a typo in a constant name * Cargo fmt
2019-04-17 19:31:47 +02:00
i18n!(
rockets.intl.catalog,
"You are not allowed to edit this blog."
)
))
.into();
}
let user = rockets
.user
.clone()
.expect("blogs::edit: User was None while it shouldn't");
form.validate()
.and_then(|_| {
if let Some(icon) = form.icon {
2021-01-30 13:44:29 +01:00
if !check_media(&conn, icon, &user) {
let mut errors = ValidationErrors::new();
errors.add(
"",
ValidationError {
code: Cow::from("icon"),
message: Some(Cow::from(i18n!(
intl,
"You can't use this media as a blog icon."
))),
params: HashMap::new(),
},
);
return Err(errors);
}
}
if let Some(banner) = form.banner {
2021-01-30 13:44:29 +01:00
if !check_media(&conn, banner, &user) {
let mut errors = ValidationErrors::new();
errors.add(
"",
ValidationError {
code: Cow::from("banner"),
message: Some(Cow::from(i18n!(
intl,
"You can't use this media as a blog banner."
))),
params: HashMap::new(),
},
);
return Err(errors);
}
}
blog.title = form.title.clone();
blog.summary = form.summary.clone();
blog.summary_html = SafeString::new(
&utils::md_to_html(
&form.summary,
None,
true,
Some(Media::get_media_processor(
&conn,
blog.list_authors(&conn)
.expect("Couldn't get list of authors")
.iter()
.collect(),
)),
)
.0,
);
blog.icon_id = form.icon;
blog.banner_id = form.banner;
blog.theme = form.theme.clone();
blog.save_changes::<Blog>(&*conn)
.expect("Couldn't save blog changes");
Ok(Flash::success(
Redirect::to(uri!(details: name = name, page = _)),
i18n!(intl, "Your blog information have been updated."),
))
})
.map_err(|err| {
2021-01-30 13:44:29 +01:00
let medias = Media::for_user(&conn, user.id).expect("Couldn't list media");
render!(blogs::edit(
2021-01-30 13:44:29 +01:00
&(&conn, &rockets).to_context(),
&blog,
medias,
&*form,
err
))
})
.unwrap()
.into()
}
2018-04-29 19:49:56 +02:00
#[get("/~/<name>/outbox")]
pub fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection>> {
2021-01-30 13:44:29 +01:00
let blog = Blog::find_by_fqn(&conn, &name).ok()?;
blog.outbox07(&conn).ok()
2018-04-29 19:49:56 +02:00
}
#[allow(unused_variables)]
#[get("/~/<name>/outbox?<page>")]
pub fn outbox_page(
name: String,
page: Page,
2021-01-30 13:44:29 +01:00
conn: DbConn,
) -> Option<ActivityStream<OrderedCollectionPage>> {
2021-01-30 13:44:29 +01:00
let blog = Blog::find_by_fqn(&conn, &name).ok()?;
blog.outbox_page07(&conn, page.limits()).ok()
}
2018-09-01 22:08:26 +02:00
#[get("/~/<name>/atom.xml")]
2021-01-30 13:44:29 +01:00
pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
let blog = Blog::find_by_fqn(&conn, &name).ok()?;
let entries = Post::get_recents_for_blog(&*conn, &blog, 15).ok()?;
let uri = Instance::get_local()
.ok()?
.compute_box("~", &name, "atom.xml");
let title = &blog.title;
let default_updated = &blog.creation_date;
2021-01-30 13:44:29 +01:00
let feed = super::build_atom_feed(entries, &uri, title, default_updated, &conn);
2019-03-20 17:56:17 +01:00
Some(Content(
ContentType::new("application", "atom+xml"),
feed.to_string(),
))
2018-09-01 22:08:26 +02:00
}
2021-01-24 11:37:49 +01:00
#[cfg(test)]
mod tests {
use crate::init_rocket;
2021-01-24 14:31:57 +01:00
use diesel::Connection;
2021-01-24 11:37:49 +01:00
use plume_common::utils::random_hex;
use plume_models::{
blog_authors::{BlogAuthor, NewBlogAuthor},
blogs::{Blog, NewBlog},
db_conn::{DbConn, DbPool},
instance::{Instance, NewInstance},
post_authors::{NewPostAuthor, PostAuthor},
posts::{NewPost, Post},
safe_string::SafeString,
users::{NewUser, User, AUTH_COOKIE},
2021-12-05 16:33:48 +01:00
Connection as Conn, CONFIG,
2021-01-24 11:37:49 +01:00
};
use rocket::{
http::{Cookie, Cookies, SameSite},
2021-01-24 13:38:51 +01:00
local::{Client, LocalRequest},
2021-01-24 11:37:49 +01:00
};
#[test]
fn edit_link_within_post_card() {
2021-12-05 16:33:48 +01:00
let conn = Conn::establish(CONFIG.database_url.as_str()).unwrap();
Instance::insert(
&conn,
NewInstance {
public_domain: "example.org".to_string(),
name: "Plume".to_string(),
local: true,
long_description: SafeString::new(""),
short_description: SafeString::new(""),
default_license: "CC-BY-SA".to_string(),
open_registrations: true,
short_description_html: String::new(),
long_description_html: String::new(),
},
)
.unwrap();
2021-01-24 11:37:49 +01:00
let rocket = init_rocket();
let client = Client::new(rocket).expect("valid rocket instance");
let dbpool = client.rocket().state::<DbPool>().unwrap();
let conn = &DbConn(dbpool.get().unwrap());
2021-01-24 14:31:57 +01:00
let (_instance, user, blog, post) = create_models(conn);
2021-01-24 11:37:49 +01:00
2021-01-24 15:00:53 +01:00
let blog_path = uri!(super::activity_details: name = &blog.fqn).to_string();
let edit_link = uri!(
super::super::posts::edit: blog = &blog.fqn,
slug = &post.slug
)
.to_string();
2021-01-24 11:37:49 +01:00
let mut response = client.get(&blog_path).dispatch();
let body = response.body_string().unwrap();
assert!(!body.contains(&edit_link));
let request = client.get(&blog_path);
2021-01-24 13:38:51 +01:00
login(&request, &user);
2021-01-24 11:37:49 +01:00
let mut response = request.dispatch();
let body = response.body_string().unwrap();
assert!(body.contains(&edit_link));
}
fn create_models(conn: &DbConn) -> (Instance, User, Blog, Post) {
conn.transaction::<(Instance, User, Blog, Post), diesel::result::Error, _>(|| {
let instance = Instance::get_local().unwrap_or_else(|_| {
2021-01-24 15:12:44 +01:00
let instance = Instance::insert(
2021-01-24 11:37:49 +01:00
conn,
NewInstance {
default_license: "CC-0-BY-SA".to_string(),
local: true,
long_description: SafeString::new("Good morning"),
long_description_html: "<p>Good morning</p>".to_string(),
short_description: SafeString::new("Hello"),
short_description_html: "<p>Hello</p>".to_string(),
name: random_hex().to_string(),
open_registrations: true,
public_domain: random_hex().to_string(),
},
)
2021-01-24 15:12:44 +01:00
.unwrap();
Instance::cache_local(conn);
instance
2021-01-24 11:37:49 +01:00
});
let mut user = NewUser::default();
user.instance_id = instance.id;
user.username = random_hex().to_string();
user.ap_url = random_hex().to_string();
user.inbox_url = random_hex().to_string();
user.outbox_url = random_hex().to_string();
user.followers_endpoint = random_hex().to_string();
let user = User::insert(conn, user).unwrap();
let mut blog = NewBlog::default();
blog.instance_id = instance.id;
blog.actor_id = random_hex().to_string();
blog.ap_url = random_hex().to_string();
blog.inbox_url = random_hex().to_string();
blog.outbox_url = random_hex().to_string();
let blog = Blog::insert(conn, blog).unwrap();
BlogAuthor::insert(
conn,
NewBlogAuthor {
blog_id: blog.id,
author_id: user.id,
is_owner: true,
},
)
.unwrap();
let post = Post::insert(
conn,
NewPost {
blog_id: blog.id,
slug: random_hex()[..8].to_owned(),
title: random_hex()[..8].to_owned(),
content: SafeString::new(""),
published: true,
license: "CC-By-SA".to_owned(),
ap_url: "".to_owned(),
creation_date: None,
subtitle: "".to_owned(),
source: "".to_owned(),
cover_id: None,
},
)
.unwrap();
PostAuthor::insert(
conn,
NewPostAuthor {
post_id: post.id,
author_id: user.id,
},
)
.unwrap();
Ok((instance, user, blog, post))
})
.unwrap()
}
2021-01-24 13:38:51 +01:00
fn login(request: &LocalRequest, user: &User) {
request.inner().guard::<Cookies>().unwrap().add_private(
Cookie::build(AUTH_COOKIE, user.id.to_string())
.same_site(SameSite::Lax)
.finish(),
);
}
2021-01-24 11:37:49 +01:00
}