Adding admin purging of DB items and pictures. #904 #1331 (#1809)

* First pass at adding admin purge. #904 #1331

* Breaking out purge into 4 tables for the 4 purgeable types.

* Using CommunitySafe instead in view

* Fix db_schema features flags.

* Attempting to pass API key.

* Adding pictrs image purging

- Added pictrs_config block, for API_KEY
- Clear out image columns after purging

* Remove the remove_images field from a few of the purge API calls.

* Fix some suggestions by @nutomic.

* Add separate pictrs reqwest client.

* Update defaults.hjson

Co-authored-by: Nutomic <me@nutomic.com>
This commit is contained in:
Dessalines 2022-06-13 15:15:04 -04:00 committed by GitHub
parent 5b7376512f
commit 4e12e25c59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1320 additions and 62 deletions

View file

@ -70,6 +70,13 @@
# activities synchronously for easier testing. Do not use in production.
debug: false
}
# Pictrs image server configuration.
pictrs_config: {
# Address where pictrs is available (for image hosting)
url: "string"
# Set a custom pictrs API key. ( Required for deleting images )
api_key: "string"
}
captcha: {
# Whether captcha is required for signup
enabled: false
@ -108,8 +115,7 @@
port: 8536
# Whether the site is available over TLS. Needs to be true for federation to work.
tls_enabled: true
# Address where pictrs is available (for image hosting)
pictrs_url: "http://localhost:8080"
# A regex list of slurs to block / hide
slur_filter: "(\bThis\b)|(\bis\b)|(\bsample\b)"
# Maximum length of local community and user names
actor_name_max_length: 20

View file

@ -104,6 +104,16 @@ pub async fn match_websocket_operation(
UserOperation::SaveSiteConfig => {
do_websocket_operation::<SaveSiteConfig>(context, id, op, data).await
}
UserOperation::PurgePerson => {
do_websocket_operation::<PurgePerson>(context, id, op, data).await
}
UserOperation::PurgeCommunity => {
do_websocket_operation::<PurgeCommunity>(context, id, op, data).await
}
UserOperation::PurgePost => do_websocket_operation::<PurgePost>(context, id, op, data).await,
UserOperation::PurgeComment => {
do_websocket_operation::<PurgeComment>(context, id, op, data).await
}
UserOperation::Search => do_websocket_operation::<Search>(context, id, op, data).await,
UserOperation::ResolveObject => {
do_websocket_operation::<ResolveObject>(context, id, op, data).await

View file

@ -49,7 +49,13 @@ impl Perform for BanPerson {
// Remove their data if that's desired
let remove_data = data.remove_data.unwrap_or(false);
if remove_data {
remove_user_data(person.id, context.pool()).await?;
remove_user_data(
person.id,
context.pool(),
&context.settings(),
context.client(),
)
.await?;
}
// Mod tables

View file

@ -1,6 +1,7 @@
mod config;
mod leave_admin;
mod mod_log;
mod purge;
mod registration_applications;
mod resolve_object;
mod search;

View file

@ -5,6 +5,10 @@ use lemmy_api_common::{
utils::{blocking, check_private_instance, get_local_user_view_from_jwt_opt},
};
use lemmy_db_views_moderator::structs::{
AdminPurgeCommentView,
AdminPurgeCommunityView,
AdminPurgePersonView,
AdminPurgePostView,
ModAddCommunityView,
ModAddView,
ModBanFromCommunityView,
@ -83,17 +87,29 @@ impl Perform for GetModlog {
.await??;
// These arrays are only for the full modlog, when a community isn't given
let (removed_communities, banned, added) = if data.community_id.is_none() {
let (
removed_communities,
banned,
added,
admin_purged_persons,
admin_purged_communities,
admin_purged_posts,
admin_purged_comments,
) = if data.community_id.is_none() {
blocking(context.pool(), move |conn| {
Ok((
ModRemoveCommunityView::list(conn, mod_person_id, page, limit)?,
ModBanView::list(conn, mod_person_id, page, limit)?,
ModAddView::list(conn, mod_person_id, page, limit)?,
AdminPurgePersonView::list(conn, mod_person_id, page, limit)?,
AdminPurgeCommunityView::list(conn, mod_person_id, page, limit)?,
AdminPurgePostView::list(conn, mod_person_id, page, limit)?,
AdminPurgeCommentView::list(conn, mod_person_id, page, limit)?,
)) as Result<_, LemmyError>
})
.await??
} else {
(Vec::new(), Vec::new(), Vec::new())
Default::default()
};
// Return the jwt
@ -108,6 +124,10 @@ impl Perform for GetModlog {
added_to_community,
added,
transferred_to_community,
admin_purged_persons,
admin_purged_communities,
admin_purged_posts,
admin_purged_comments,
hidden_communities,
})
}

View file

@ -0,0 +1,63 @@
use crate::Perform;
use actix_web::web::Data;
use lemmy_api_common::{
site::{PurgeComment, PurgeItemResponse},
utils::{blocking, get_local_user_view_from_jwt, is_admin},
};
use lemmy_db_schema::{
source::{
comment::Comment,
moderator::{AdminPurgeComment, AdminPurgeCommentForm},
},
traits::Crud,
};
use lemmy_utils::{error::LemmyError, ConnectionId};
use lemmy_websocket::LemmyContext;
#[async_trait::async_trait(?Send)]
impl Perform for PurgeComment {
type Response = PurgeItemResponse;
#[tracing::instrument(skip(context, _websocket_id))]
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<Self::Response, LemmyError> {
let data: &Self = self;
let local_user_view =
get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
// Only let admins purge an item
is_admin(&local_user_view)?;
let comment_id = data.comment_id;
// Read the comment to get the post_id
let comment = blocking(context.pool(), move |conn| Comment::read(conn, comment_id)).await??;
let post_id = comment.post_id;
// TODO read comments for pictrs images and purge them
blocking(context.pool(), move |conn| {
Comment::delete(conn, comment_id)
})
.await??;
// Mod tables
let reason = data.reason.to_owned();
let form = AdminPurgeCommentForm {
admin_person_id: local_user_view.person.id,
reason,
post_id,
};
blocking(context.pool(), move |conn| {
AdminPurgeComment::create(conn, &form)
})
.await??;
Ok(PurgeItemResponse { success: true })
}
}

View file

@ -0,0 +1,82 @@
use crate::Perform;
use actix_web::web::Data;
use lemmy_api_common::{
request::purge_image_from_pictrs,
site::{PurgeCommunity, PurgeItemResponse},
utils::{blocking, get_local_user_view_from_jwt, is_admin, purge_image_posts_for_community},
};
use lemmy_db_schema::{
source::{
community::Community,
moderator::{AdminPurgeCommunity, AdminPurgeCommunityForm},
},
traits::Crud,
};
use lemmy_utils::{error::LemmyError, ConnectionId};
use lemmy_websocket::LemmyContext;
#[async_trait::async_trait(?Send)]
impl Perform for PurgeCommunity {
type Response = PurgeItemResponse;
#[tracing::instrument(skip(context, _websocket_id))]
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<Self::Response, LemmyError> {
let data: &Self = self;
let local_user_view =
get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
// Only let admins purge an item
is_admin(&local_user_view)?;
let community_id = data.community_id;
// Read the community to get its images
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
if let Some(banner) = community.banner {
purge_image_from_pictrs(context.client(), &context.settings(), &banner)
.await
.ok();
}
if let Some(icon) = community.icon {
purge_image_from_pictrs(context.client(), &context.settings(), &icon)
.await
.ok();
}
purge_image_posts_for_community(
community_id,
context.pool(),
&context.settings(),
context.client(),
)
.await?;
blocking(context.pool(), move |conn| {
Community::delete(conn, community_id)
})
.await??;
// Mod tables
let reason = data.reason.to_owned();
let form = AdminPurgeCommunityForm {
admin_person_id: local_user_view.person.id,
reason,
};
blocking(context.pool(), move |conn| {
AdminPurgeCommunity::create(conn, &form)
})
.await??;
Ok(PurgeItemResponse { success: true })
}
}

View file

@ -0,0 +1,4 @@
mod comment;
mod community;
mod person;
mod post;

View file

@ -0,0 +1,75 @@
use crate::Perform;
use actix_web::web::Data;
use lemmy_api_common::{
request::purge_image_from_pictrs,
site::{PurgeItemResponse, PurgePerson},
utils::{blocking, get_local_user_view_from_jwt, is_admin, purge_image_posts_for_person},
};
use lemmy_db_schema::{
source::{
moderator::{AdminPurgePerson, AdminPurgePersonForm},
person::Person,
},
traits::Crud,
};
use lemmy_utils::{error::LemmyError, ConnectionId};
use lemmy_websocket::LemmyContext;
#[async_trait::async_trait(?Send)]
impl Perform for PurgePerson {
type Response = PurgeItemResponse;
#[tracing::instrument(skip(context, _websocket_id))]
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<Self::Response, LemmyError> {
let data: &Self = self;
let local_user_view =
get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
// Only let admins purge an item
is_admin(&local_user_view)?;
// Read the person to get their images
let person_id = data.person_id;
let person = blocking(context.pool(), move |conn| Person::read(conn, person_id)).await??;
if let Some(banner) = person.banner {
purge_image_from_pictrs(context.client(), &context.settings(), &banner)
.await
.ok();
}
if let Some(avatar) = person.avatar {
purge_image_from_pictrs(context.client(), &context.settings(), &avatar)
.await
.ok();
}
purge_image_posts_for_person(
person_id,
context.pool(),
&context.settings(),
context.client(),
)
.await?;
blocking(context.pool(), move |conn| Person::delete(conn, person_id)).await??;
// Mod tables
let reason = data.reason.to_owned();
let form = AdminPurgePersonForm {
admin_person_id: local_user_view.person.id,
reason,
};
blocking(context.pool(), move |conn| {
AdminPurgePerson::create(conn, &form)
})
.await??;
Ok(PurgeItemResponse { success: true })
}
}

View file

@ -0,0 +1,72 @@
use crate::Perform;
use actix_web::web::Data;
use lemmy_api_common::{
request::purge_image_from_pictrs,
site::{PurgeItemResponse, PurgePost},
utils::{blocking, get_local_user_view_from_jwt, is_admin},
};
use lemmy_db_schema::{
source::{
moderator::{AdminPurgePost, AdminPurgePostForm},
post::Post,
},
traits::Crud,
};
use lemmy_utils::{error::LemmyError, ConnectionId};
use lemmy_websocket::LemmyContext;
#[async_trait::async_trait(?Send)]
impl Perform for PurgePost {
type Response = PurgeItemResponse;
#[tracing::instrument(skip(context, _websocket_id))]
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<Self::Response, LemmyError> {
let data: &Self = self;
let local_user_view =
get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
// Only let admins purge an item
is_admin(&local_user_view)?;
let post_id = data.post_id;
// Read the post to get the community_id
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
// Purge image
if let Some(url) = post.url {
purge_image_from_pictrs(context.client(), &context.settings(), &url)
.await
.ok();
}
// Purge thumbnail
if let Some(thumbnail_url) = post.thumbnail_url {
purge_image_from_pictrs(context.client(), &context.settings(), &thumbnail_url)
.await
.ok();
}
let community_id = post.community_id;
blocking(context.pool(), move |conn| Post::delete(conn, post_id)).await??;
// Mod tables
let reason = data.reason.to_owned();
let form = AdminPurgePostForm {
admin_person_id: local_user_view.person.id,
reason,
community_id,
};
blocking(context.pool(), move |conn| {
AdminPurgePost::create(conn, &form)
})
.await??;
Ok(PurgeItemResponse { success: true })
}
}

View file

@ -1,7 +1,12 @@
use crate::post::SiteMetadata;
use encoding::{all::encodings, DecoderTrap};
use lemmy_db_schema::newtypes::DbUrl;
use lemmy_utils::{error::LemmyError, settings::structs::Settings, version::VERSION};
use lemmy_utils::{
error::LemmyError,
settings::structs::Settings,
version::VERSION,
REQWEST_TIMEOUT,
};
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use reqwest_middleware::ClientWithMiddleware;
use serde::Deserialize;
@ -105,32 +110,75 @@ pub(crate) struct PictrsFile {
delete_token: String,
}
#[derive(Deserialize, Debug, Clone)]
pub(crate) struct PictrsPurgeResponse {
msg: String,
}
#[tracing::instrument(skip_all)]
pub(crate) async fn fetch_pictrs(
client: &ClientWithMiddleware,
settings: &Settings,
image_url: &Url,
) -> Result<PictrsResponse, LemmyError> {
if let Some(pictrs_url) = settings.pictrs_url.to_owned() {
is_image_content_type(client, image_url).await?;
let pictrs_config = settings.pictrs_config()?;
is_image_content_type(client, image_url).await?;
let fetch_url = format!(
"{}/image/download?url={}",
pictrs_url,
utf8_percent_encode(image_url.as_str(), NON_ALPHANUMERIC) // TODO this might not be needed
);
let fetch_url = format!(
"{}/image/download?url={}",
pictrs_config.url,
utf8_percent_encode(image_url.as_str(), NON_ALPHANUMERIC) // TODO this might not be needed
);
let response = client.get(&fetch_url).send().await?;
let response = client
.get(&fetch_url)
.timeout(REQWEST_TIMEOUT)
.send()
.await?;
let response: PictrsResponse = response.json().await.map_err(LemmyError::from)?;
let response: PictrsResponse = response.json().await.map_err(LemmyError::from)?;
if response.msg == "ok" {
Ok(response)
} else {
Err(LemmyError::from_message(&response.msg))
}
if response.msg == "ok" {
Ok(response)
} else {
Err(LemmyError::from_message("pictrs_url not set up in config"))
Err(LemmyError::from_message(&response.msg))
}
}
/// Purges an image from pictrs
/// Note: This should often be coerced from a Result to .ok() in order to fail softly, because:
/// - It might fail due to image being not local
/// - It might not be an image
/// - Pictrs might not be set up
pub async fn purge_image_from_pictrs(
client: &ClientWithMiddleware,
settings: &Settings,
image_url: &Url,
) -> Result<(), LemmyError> {
let pictrs_config = settings.pictrs_config()?;
is_image_content_type(client, image_url).await?;
let alias = image_url
.path_segments()
.ok_or_else(|| LemmyError::from_message("Image URL missing path segments"))?
.next_back()
.ok_or_else(|| LemmyError::from_message("Image URL missing last path segment"))?;
let purge_url = format!("{}/internal/purge?alias={}", pictrs_config.url, alias);
let response = client
.post(&purge_url)
.timeout(REQWEST_TIMEOUT)
.header("x-api-token", pictrs_config.api_key)
.send()
.await?;
let response: PictrsPurgeResponse = response.json().await.map_err(LemmyError::from)?;
if response.msg == "ok" {
Ok(())
} else {
Err(LemmyError::from_message(&response.msg))
}
}

View file

@ -1,6 +1,6 @@
use crate::sensitive::Sensitive;
use lemmy_db_schema::{
newtypes::{CommunityId, PersonId},
newtypes::{CommentId, CommunityId, PersonId, PostId},
ListingType,
SearchType,
SortType,
@ -21,6 +21,10 @@ use lemmy_db_views_actor::structs::{
PersonViewSafe,
};
use lemmy_db_views_moderator::structs::{
AdminPurgeCommentView,
AdminPurgeCommunityView,
AdminPurgePersonView,
AdminPurgePostView,
ModAddCommunityView,
ModAddView,
ModBanFromCommunityView,
@ -93,6 +97,10 @@ pub struct GetModlogResponse {
pub added_to_community: Vec<ModAddCommunityView>,
pub transferred_to_community: Vec<ModTransferCommunityView>,
pub added: Vec<ModAddView>,
pub admin_purged_persons: Vec<AdminPurgePersonView>,
pub admin_purged_communities: Vec<AdminPurgeCommunityView>,
pub admin_purged_posts: Vec<AdminPurgePostView>,
pub admin_purged_comments: Vec<AdminPurgeCommentView>,
pub hidden_communities: Vec<ModHideCommunityView>,
}
@ -194,6 +202,39 @@ pub struct FederatedInstances {
pub blocked: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PurgePerson {
pub person_id: PersonId,
pub reason: Option<String>,
pub auth: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PurgeCommunity {
pub community_id: CommunityId,
pub reason: Option<String>,
pub auth: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PurgePost {
pub post_id: PostId,
pub reason: Option<String>,
pub auth: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PurgeComment {
pub comment_id: CommentId,
pub reason: Option<String>,
pub auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct PurgeItemResponse {
pub success: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct ListRegistrationApplications {
/// Only shows the unread applications (IE those without an admin actor)

View file

@ -1,4 +1,4 @@
use crate::{sensitive::Sensitive, site::FederatedInstances};
use crate::{request::purge_image_from_pictrs, sensitive::Sensitive, site::FederatedInstances};
use lemmy_db_schema::{
newtypes::{CommunityId, LocalUserId, PersonId, PostId},
source::{
@ -32,6 +32,7 @@ use lemmy_utils::{
settings::structs::Settings,
utils::generate_random_string,
};
use reqwest_middleware::ClientWithMiddleware;
use rosetta_i18n::{Language, LanguageId};
use tracing::warn;
@ -505,13 +506,98 @@ pub async fn check_private_instance_and_federation_enabled(
Ok(())
}
pub async fn remove_user_data(banned_person_id: PersonId, pool: &DbPool) -> Result<(), LemmyError> {
pub async fn purge_image_posts_for_person(
banned_person_id: PersonId,
pool: &DbPool,
settings: &Settings,
client: &ClientWithMiddleware,
) -> Result<(), LemmyError> {
let posts = blocking(pool, move |conn: &'_ _| {
Post::fetch_pictrs_posts_for_creator(conn, banned_person_id)
})
.await??;
for post in posts {
if let Some(url) = post.url {
purge_image_from_pictrs(client, settings, &url).await.ok();
}
if let Some(thumbnail_url) = post.thumbnail_url {
purge_image_from_pictrs(client, settings, &thumbnail_url)
.await
.ok();
}
}
blocking(pool, move |conn| {
Post::remove_pictrs_post_images_and_thumbnails_for_creator(conn, banned_person_id)
})
.await??;
Ok(())
}
pub async fn purge_image_posts_for_community(
banned_community_id: CommunityId,
pool: &DbPool,
settings: &Settings,
client: &ClientWithMiddleware,
) -> Result<(), LemmyError> {
let posts = blocking(pool, move |conn: &'_ _| {
Post::fetch_pictrs_posts_for_community(conn, banned_community_id)
})
.await??;
for post in posts {
if let Some(url) = post.url {
purge_image_from_pictrs(client, settings, &url).await.ok();
}
if let Some(thumbnail_url) = post.thumbnail_url {
purge_image_from_pictrs(client, settings, &thumbnail_url)
.await
.ok();
}
}
blocking(pool, move |conn| {
Post::remove_pictrs_post_images_and_thumbnails_for_community(conn, banned_community_id)
})
.await??;
Ok(())
}
pub async fn remove_user_data(
banned_person_id: PersonId,
pool: &DbPool,
settings: &Settings,
client: &ClientWithMiddleware,
) -> Result<(), LemmyError> {
// Purge user images
let person = blocking(pool, move |conn| Person::read(conn, banned_person_id)).await??;
if let Some(avatar) = person.avatar {
purge_image_from_pictrs(client, settings, &avatar)
.await
.ok();
}
if let Some(banner) = person.banner {
purge_image_from_pictrs(client, settings, &banner)
.await
.ok();
}
// Update the fields to None
blocking(pool, move |conn| {
Person::remove_avatar_and_banner(conn, banned_person_id)
})
.await??;
// Posts
blocking(pool, move |conn: &'_ _| {
Post::update_removed_for_creator(conn, banned_person_id, None, true)
})
.await??;
// Purge image posts
purge_image_posts_for_person(banned_person_id, pool, settings, client).await?;
// Communities
// Remove all communities where they're the top mod
// for now, remove the communities manually
@ -527,8 +613,24 @@ pub async fn remove_user_data(banned_person_id: PersonId, pool: &DbPool) -> Resu
.collect();
for first_mod_community in banned_user_first_communities {
let community_id = first_mod_community.community.id;
blocking(pool, move |conn: &'_ _| {
Community::update_removed(conn, first_mod_community.community.id, true)
Community::update_removed(conn, community_id, true)
})
.await??;
// Delete the community images
if let Some(icon) = first_mod_community.community.icon {
purge_image_from_pictrs(client, settings, &icon).await.ok();
}
if let Some(banner) = first_mod_community.community.banner {
purge_image_from_pictrs(client, settings, &banner)
.await
.ok();
}
// Update the fields to None
blocking(pool, move |conn| {
Community::remove_avatar_and_banner(conn, community_id)
})
.await??;
}
@ -575,7 +677,26 @@ pub async fn remove_user_data_in_community(
Ok(())
}
pub async fn delete_user_account(person_id: PersonId, pool: &DbPool) -> Result<(), LemmyError> {
pub async fn delete_user_account(
person_id: PersonId,
pool: &DbPool,
settings: &Settings,
client: &ClientWithMiddleware,
) -> Result<(), LemmyError> {
// Delete their images
let person = blocking(pool, move |conn| Person::read(conn, person_id)).await??;
if let Some(avatar) = person.avatar {
purge_image_from_pictrs(client, settings, &avatar)
.await
.ok();
}
if let Some(banner) = person.banner {
purge_image_from_pictrs(client, settings, &banner)
.await
.ok();
}
// No need to update avatar and banner, those are handled in Person::delete_account
// Comments
let permadelete = move |conn: &'_ _| Comment::permadelete_for_creator(conn, person_id);
blocking(pool, permadelete)
@ -588,6 +709,9 @@ pub async fn delete_user_account(person_id: PersonId, pool: &DbPool) -> Result<(
.await?
.map_err(|e| LemmyError::from_error_message(e, "couldnt_update_post"))?;
// Purge image posts
purge_image_posts_for_person(person_id, pool, settings, client).await?;
blocking(pool, move |conn| Person::delete_account(conn, person_id)).await??;
Ok(())

View file

@ -33,7 +33,13 @@ impl PerformCrud for DeleteAccount {
return Err(LemmyError::from_message("password_incorrect"));
}
delete_user_account(local_user_view.person.id, context.pool()).await?;
delete_user_account(
local_user_view.person.id,
context.pool(),
&context.settings(),
context.client(),
)
.await?;
DeleteUser::send(&local_user_view.person.into(), context).await?;
Ok(DeleteAccountResponse {})

View file

@ -181,7 +181,13 @@ impl ActivityHandler for BlockUser {
})
.await??;
if self.remove_data.unwrap_or(false) {
remove_user_data(blocked_person.id, context.pool()).await?;
remove_user_data(
blocked_person.id,
context.pool(),
&context.settings(),
context.client(),
)
.await?;
}
// write mod log

View file

@ -51,7 +51,13 @@ impl ActivityHandler for DeleteUser {
.actor
.dereference(context, local_instance(context), request_counter)
.await?;
delete_user_account(actor.id, context.pool()).await?;
delete_user_account(
actor.id,
context.pool(),
&context.settings(),
context.client(),
)
.await?;
Ok(())
}
}

View file

@ -138,6 +138,19 @@ impl Community {
.set(community_form)
.get_result::<Self>(conn)
}
pub fn remove_avatar_and_banner(
conn: &PgConnection,
community_id: CommunityId,
) -> Result<Self, Error> {
use crate::schema::community::dsl::*;
diesel::update(community.find(community_id))
.set((
icon.eq::<Option<String>>(None),
banner.eq::<Option<String>>(None),
))
.get_result::<Self>(conn)
}
}
impl Joinable for CommunityModerator {

View file

@ -263,6 +263,98 @@ impl Crud for ModAdd {
}
}
impl Crud for AdminPurgePerson {
type Form = AdminPurgePersonForm;
type IdType = i32;
fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
use crate::schema::admin_purge_person::dsl::*;
admin_purge_person.find(from_id).first::<Self>(conn)
}
fn create(conn: &PgConnection, form: &Self::Form) -> Result<Self, Error> {
use crate::schema::admin_purge_person::dsl::*;
insert_into(admin_purge_person)
.values(form)
.get_result::<Self>(conn)
}
fn update(conn: &PgConnection, from_id: i32, form: &Self::Form) -> Result<Self, Error> {
use crate::schema::admin_purge_person::dsl::*;
diesel::update(admin_purge_person.find(from_id))
.set(form)
.get_result::<Self>(conn)
}
}
impl Crud for AdminPurgeCommunity {
type Form = AdminPurgeCommunityForm;
type IdType = i32;
fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
use crate::schema::admin_purge_community::dsl::*;
admin_purge_community.find(from_id).first::<Self>(conn)
}
fn create(conn: &PgConnection, form: &Self::Form) -> Result<Self, Error> {
use crate::schema::admin_purge_community::dsl::*;
insert_into(admin_purge_community)
.values(form)
.get_result::<Self>(conn)
}
fn update(conn: &PgConnection, from_id: i32, form: &Self::Form) -> Result<Self, Error> {
use crate::schema::admin_purge_community::dsl::*;
diesel::update(admin_purge_community.find(from_id))
.set(form)
.get_result::<Self>(conn)
}
}
impl Crud for AdminPurgePost {
type Form = AdminPurgePostForm;
type IdType = i32;
fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
use crate::schema::admin_purge_post::dsl::*;
admin_purge_post.find(from_id).first::<Self>(conn)
}
fn create(conn: &PgConnection, form: &Self::Form) -> Result<Self, Error> {
use crate::schema::admin_purge_post::dsl::*;
insert_into(admin_purge_post)
.values(form)
.get_result::<Self>(conn)
}
fn update(conn: &PgConnection, from_id: i32, form: &Self::Form) -> Result<Self, Error> {
use crate::schema::admin_purge_post::dsl::*;
diesel::update(admin_purge_post.find(from_id))
.set(form)
.get_result::<Self>(conn)
}
}
impl Crud for AdminPurgeComment {
type Form = AdminPurgeCommentForm;
type IdType = i32;
fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
use crate::schema::admin_purge_comment::dsl::*;
admin_purge_comment.find(from_id).first::<Self>(conn)
}
fn create(conn: &PgConnection, form: &Self::Form) -> Result<Self, Error> {
use crate::schema::admin_purge_comment::dsl::*;
insert_into(admin_purge_comment)
.values(form)
.get_result::<Self>(conn)
}
fn update(conn: &PgConnection, from_id: i32, form: &Self::Form) -> Result<Self, Error> {
use crate::schema::admin_purge_comment::dsl::*;
diesel::update(admin_purge_comment.find(from_id))
.set(form)
.get_result::<Self>(conn)
}
}
#[cfg(test)]
mod tests {
use crate::{

View file

@ -228,6 +228,8 @@ impl Person {
diesel::update(person.find(person_id))
.set((
display_name.eq::<Option<String>>(None),
avatar.eq::<Option<String>>(None),
banner.eq::<Option<String>>(None),
bio.eq::<Option<String>>(None),
matrix_user_id.eq::<Option<String>>(None),
deleted.eq(true),
@ -265,6 +267,15 @@ impl Person {
.set(admin.eq(false))
.get_result::<Self>(conn)
}
pub fn remove_avatar_and_banner(conn: &PgConnection, person_id: PersonId) -> Result<Self, Error> {
diesel::update(person.find(person_id))
.set((
avatar.eq::<Option<String>>(None),
banner.eq::<Option<String>>(None),
))
.get_result::<Self>(conn)
}
}
impl PersonSafe {

View file

@ -13,7 +13,7 @@ use crate::{
traits::{Crud, DeleteableOrRemoveable, Likeable, Readable, Saveable},
utils::naive_now,
};
use diesel::{dsl::*, result::Error, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl};
use diesel::{dsl::*, result::Error, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, *};
use url::Url;
impl Crud for Post {
@ -174,6 +174,71 @@ impl Post {
.map(Into::into),
)
}
pub fn fetch_pictrs_posts_for_creator(
conn: &PgConnection,
for_creator_id: PersonId,
) -> Result<Vec<Self>, Error> {
use crate::schema::post::dsl::*;
let pictrs_search = "%pictrs/image%";
post
.filter(creator_id.eq(for_creator_id))
.filter(url.like(pictrs_search))
.load::<Self>(conn)
}
/// Sets the url and thumbnails fields to None
pub fn remove_pictrs_post_images_and_thumbnails_for_creator(
conn: &PgConnection,
for_creator_id: PersonId,
) -> Result<Vec<Self>, Error> {
use crate::schema::post::dsl::*;
let pictrs_search = "%pictrs/image%";
diesel::update(
post
.filter(creator_id.eq(for_creator_id))
.filter(url.like(pictrs_search)),
)
.set((
url.eq::<Option<String>>(None),
thumbnail_url.eq::<Option<String>>(None),
))
.get_results::<Self>(conn)
}
pub fn fetch_pictrs_posts_for_community(
conn: &PgConnection,
for_community_id: CommunityId,
) -> Result<Vec<Self>, Error> {
use crate::schema::post::dsl::*;
let pictrs_search = "%pictrs/image%";
post
.filter(community_id.eq(for_community_id))
.filter(url.like(pictrs_search))
.load::<Self>(conn)
}
/// Sets the url and thumbnails fields to None
pub fn remove_pictrs_post_images_and_thumbnails_for_community(
conn: &PgConnection,
for_community_id: CommunityId,
) -> Result<Vec<Self>, Error> {
use crate::schema::post::dsl::*;
let pictrs_search = "%pictrs/image%";
diesel::update(
post
.filter(community_id.eq(for_community_id))
.filter(url.like(pictrs_search)),
)
.set((
url.eq::<Option<String>>(None),
thumbnail_url.eq::<Option<String>>(None),
))
.get_results::<Self>(conn)
}
}
impl Likeable for PostLike {

View file

@ -577,6 +577,16 @@ table! {
}
}
table! {
admin_purge_comment (id) {
id -> Int4,
admin_person_id -> Int4,
post_id -> Int4,
reason -> Nullable<Text>,
when_ -> Timestamp,
}
}
table! {
email_verification (id) {
id -> Int4,
@ -587,6 +597,34 @@ table! {
}
}
table! {
admin_purge_community (id) {
id -> Int4,
admin_person_id -> Int4,
reason -> Nullable<Text>,
when_ -> Timestamp,
}
}
table! {
admin_purge_person (id) {
id -> Int4,
admin_person_id -> Int4,
reason -> Nullable<Text>,
when_ -> Timestamp,
}
}
table! {
admin_purge_post (id) {
id -> Int4,
admin_person_id -> Int4,
community_id -> Int4,
reason -> Nullable<Text>,
when_ -> Timestamp,
}
}
table! {
registration_application (id) {
id -> Int4,
@ -675,6 +713,13 @@ joinable!(registration_application -> person (admin_id));
joinable!(mod_hide_community -> person (mod_person_id));
joinable!(mod_hide_community -> community (community_id));
joinable!(admin_purge_comment -> person (admin_person_id));
joinable!(admin_purge_comment -> post (post_id));
joinable!(admin_purge_community -> person (admin_person_id));
joinable!(admin_purge_person -> person (admin_person_id));
joinable!(admin_purge_post -> community (community_id));
joinable!(admin_purge_post -> person (admin_person_id));
allow_tables_to_appear_in_same_query!(
activity,
comment,
@ -718,6 +763,10 @@ allow_tables_to_appear_in_same_query!(
comment_alias_1,
person_alias_1,
person_alias_2,
admin_purge_comment,
admin_purge_community,
admin_purge_person,
admin_purge_post,
email_verification,
registration_application
);

View file

@ -3,6 +3,10 @@ use serde::{Deserialize, Serialize};
#[cfg(feature = "full")]
use crate::schema::{
admin_purge_comment,
admin_purge_community,
admin_purge_person,
admin_purge_post,
mod_add,
mod_add_community,
mod_ban,
@ -247,3 +251,75 @@ pub struct ModAddForm {
pub other_person_id: PersonId,
pub removed: Option<bool>,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "full", derive(Queryable, Identifiable))]
#[cfg_attr(feature = "full", table_name = "admin_purge_person")]
pub struct AdminPurgePerson {
pub id: i32,
pub admin_person_id: PersonId,
pub reason: Option<String>,
pub when_: chrono::NaiveDateTime,
}
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", table_name = "admin_purge_person")]
pub struct AdminPurgePersonForm {
pub admin_person_id: PersonId,
pub reason: Option<String>,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "full", derive(Queryable, Identifiable))]
#[cfg_attr(feature = "full", table_name = "admin_purge_community")]
pub struct AdminPurgeCommunity {
pub id: i32,
pub admin_person_id: PersonId,
pub reason: Option<String>,
pub when_: chrono::NaiveDateTime,
}
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", table_name = "admin_purge_community")]
pub struct AdminPurgeCommunityForm {
pub admin_person_id: PersonId,
pub reason: Option<String>,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "full", derive(Queryable, Identifiable))]
#[cfg_attr(feature = "full", table_name = "admin_purge_post")]
pub struct AdminPurgePost {
pub id: i32,
pub admin_person_id: PersonId,
pub community_id: CommunityId,
pub reason: Option<String>,
pub when_: chrono::NaiveDateTime,
}
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", table_name = "admin_purge_post")]
pub struct AdminPurgePostForm {
pub admin_person_id: PersonId,
pub community_id: CommunityId,
pub reason: Option<String>,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "full", derive(Queryable, Identifiable))]
#[cfg_attr(feature = "full", table_name = "admin_purge_comment")]
pub struct AdminPurgeComment {
pub id: i32,
pub admin_person_id: PersonId,
pub post_id: PostId,
pub reason: Option<String>,
pub when_: chrono::NaiveDateTime,
}
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", table_name = "admin_purge_comment")]
pub struct AdminPurgeCommentForm {
pub admin_person_id: PersonId,
pub post_id: PostId,
pub reason: Option<String>,
}

View file

@ -0,0 +1,62 @@
use crate::structs::AdminPurgeCommentView;
use diesel::{result::Error, *};
use lemmy_db_schema::{
newtypes::PersonId,
schema::{admin_purge_comment, person, post},
source::{
moderator::AdminPurgeComment,
person::{Person, PersonSafe},
post::Post,
},
traits::{ToSafe, ViewToVec},
utils::limit_and_offset,
};
type AdminPurgeCommentViewTuple = (AdminPurgeComment, PersonSafe, Post);
impl AdminPurgeCommentView {
pub fn list(
conn: &PgConnection,
admin_person_id: Option<PersonId>,
page: Option<i64>,
limit: Option<i64>,
) -> Result<Vec<Self>, Error> {
let mut query = admin_purge_comment::table
.inner_join(person::table.on(admin_purge_comment::admin_person_id.eq(person::id)))
.inner_join(post::table)
.select((
admin_purge_comment::all_columns,
Person::safe_columns_tuple(),
post::all_columns,
))
.into_boxed();
if let Some(admin_person_id) = admin_person_id {
query = query.filter(admin_purge_comment::admin_person_id.eq(admin_person_id));
};
let (limit, offset) = limit_and_offset(page, limit);
let res = query
.limit(limit)
.offset(offset)
.order_by(admin_purge_comment::when_.desc())
.load::<AdminPurgeCommentViewTuple>(conn)?;
Ok(Self::from_tuple_to_vec(res))
}
}
impl ViewToVec for AdminPurgeCommentView {
type DbTuple = AdminPurgeCommentViewTuple;
fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
items
.iter()
.map(|a| Self {
admin_purge_comment: a.0.to_owned(),
admin: a.1.to_owned(),
post: a.2.to_owned(),
})
.collect::<Vec<Self>>()
}
}

View file

@ -0,0 +1,58 @@
use crate::structs::AdminPurgeCommunityView;
use diesel::{result::Error, *};
use lemmy_db_schema::{
newtypes::PersonId,
schema::{admin_purge_community, person},
source::{
moderator::AdminPurgeCommunity,
person::{Person, PersonSafe},
},
traits::{ToSafe, ViewToVec},
utils::limit_and_offset,
};
type AdminPurgeCommunityViewTuple = (AdminPurgeCommunity, PersonSafe);
impl AdminPurgeCommunityView {
pub fn list(
conn: &PgConnection,
admin_person_id: Option<PersonId>,
page: Option<i64>,
limit: Option<i64>,
) -> Result<Vec<Self>, Error> {
let mut query = admin_purge_community::table
.inner_join(person::table.on(admin_purge_community::admin_person_id.eq(person::id)))
.select((
admin_purge_community::all_columns,
Person::safe_columns_tuple(),
))
.into_boxed();
if let Some(admin_person_id) = admin_person_id {
query = query.filter(admin_purge_community::admin_person_id.eq(admin_person_id));
};
let (limit, offset) = limit_and_offset(page, limit);
let res = query
.limit(limit)
.offset(offset)
.order_by(admin_purge_community::when_.desc())
.load::<AdminPurgeCommunityViewTuple>(conn)?;
Ok(Self::from_tuple_to_vec(res))
}
}
impl ViewToVec for AdminPurgeCommunityView {
type DbTuple = AdminPurgeCommunityViewTuple;
fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
items
.iter()
.map(|a| Self {
admin_purge_community: a.0.to_owned(),
admin: a.1.to_owned(),
})
.collect::<Vec<Self>>()
}
}

View file

@ -0,0 +1,58 @@
use crate::structs::AdminPurgePersonView;
use diesel::{result::Error, *};
use lemmy_db_schema::{
newtypes::PersonId,
schema::{admin_purge_person, person},
source::{
moderator::AdminPurgePerson,
person::{Person, PersonSafe},
},
traits::{ToSafe, ViewToVec},
utils::limit_and_offset,
};
type AdminPurgePersonViewTuple = (AdminPurgePerson, PersonSafe);
impl AdminPurgePersonView {
pub fn list(
conn: &PgConnection,
admin_person_id: Option<PersonId>,
page: Option<i64>,
limit: Option<i64>,
) -> Result<Vec<Self>, Error> {
let mut query = admin_purge_person::table
.inner_join(person::table.on(admin_purge_person::admin_person_id.eq(person::id)))
.select((
admin_purge_person::all_columns,
Person::safe_columns_tuple(),
))
.into_boxed();
if let Some(admin_person_id) = admin_person_id {
query = query.filter(admin_purge_person::admin_person_id.eq(admin_person_id));
};
let (limit, offset) = limit_and_offset(page, limit);
let res = query
.limit(limit)
.offset(offset)
.order_by(admin_purge_person::when_.desc())
.load::<AdminPurgePersonViewTuple>(conn)?;
Ok(Self::from_tuple_to_vec(res))
}
}
impl ViewToVec for AdminPurgePersonView {
type DbTuple = AdminPurgePersonViewTuple;
fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
items
.iter()
.map(|a| Self {
admin_purge_person: a.0.to_owned(),
admin: a.1.to_owned(),
})
.collect::<Vec<Self>>()
}
}

View file

@ -0,0 +1,62 @@
use crate::structs::AdminPurgePostView;
use diesel::{result::Error, *};
use lemmy_db_schema::{
newtypes::PersonId,
schema::{admin_purge_post, community, person},
source::{
community::{Community, CommunitySafe},
moderator::AdminPurgePost,
person::{Person, PersonSafe},
},
traits::{ToSafe, ViewToVec},
utils::limit_and_offset,
};
type AdminPurgePostViewTuple = (AdminPurgePost, PersonSafe, CommunitySafe);
impl AdminPurgePostView {
pub fn list(
conn: &PgConnection,
admin_person_id: Option<PersonId>,
page: Option<i64>,
limit: Option<i64>,
) -> Result<Vec<Self>, Error> {
let mut query = admin_purge_post::table
.inner_join(person::table.on(admin_purge_post::admin_person_id.eq(person::id)))
.inner_join(community::table)
.select((
admin_purge_post::all_columns,
Person::safe_columns_tuple(),
Community::safe_columns_tuple(),
))
.into_boxed();
if let Some(admin_person_id) = admin_person_id {
query = query.filter(admin_purge_post::admin_person_id.eq(admin_person_id));
};
let (limit, offset) = limit_and_offset(page, limit);
let res = query
.limit(limit)
.offset(offset)
.order_by(admin_purge_post::when_.desc())
.load::<AdminPurgePostViewTuple>(conn)?;
Ok(Self::from_tuple_to_vec(res))
}
}
impl ViewToVec for AdminPurgePostView {
type DbTuple = AdminPurgePostViewTuple;
fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
items
.iter()
.map(|a| Self {
admin_purge_post: a.0.to_owned(),
admin: a.1.to_owned(),
community: a.2.to_owned(),
})
.collect::<Vec<Self>>()
}
}

View file

@ -1,4 +1,12 @@
#[cfg(feature = "full")]
pub mod admin_purge_comment_view;
#[cfg(feature = "full")]
pub mod admin_purge_community_view;
#[cfg(feature = "full")]
pub mod admin_purge_person_view;
#[cfg(feature = "full")]
pub mod admin_purge_post_view;
#[cfg(feature = "full")]
pub mod mod_add_community_view;
#[cfg(feature = "full")]
pub mod mod_add_view;

View file

@ -2,6 +2,10 @@ use lemmy_db_schema::source::{
comment::Comment,
community::CommunitySafe,
moderator::{
AdminPurgeComment,
AdminPurgeCommunity,
AdminPurgePerson,
AdminPurgePost,
ModAdd,
ModAddCommunity,
ModBan,
@ -104,3 +108,29 @@ pub struct ModTransferCommunityView {
pub community: CommunitySafe,
pub modded_person: PersonSafeAlias1,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AdminPurgeCommentView {
pub admin_purge_comment: AdminPurgeComment,
pub admin: PersonSafe,
pub post: Post,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AdminPurgeCommunityView {
pub admin_purge_community: AdminPurgeCommunity,
pub admin: PersonSafe,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AdminPurgePersonView {
pub admin_purge_person: AdminPurgePerson,
pub admin: PersonSafe,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AdminPurgePostView {
pub admin_purge_post: AdminPurgePost,
pub admin: PersonSafe,
pub community: CommunitySafe,
}

View file

@ -10,9 +10,8 @@ use actix_web::{
HttpRequest,
HttpResponse,
};
use anyhow::anyhow;
use futures::stream::{Stream, StreamExt};
use lemmy_utils::{claims::Claims, error::LemmyError, rate_limit::RateLimit};
use lemmy_utils::{claims::Claims, rate_limit::RateLimit, REQWEST_TIMEOUT};
use lemmy_websocket::LemmyContext;
use reqwest::Body;
use reqwest_middleware::{ClientWithMiddleware, RequestBuilder};
@ -28,7 +27,8 @@ pub fn config(cfg: &mut web::ServiceConfig, client: ClientWithMiddleware, rate_l
)
// This has optional query params: /image/{filename}?format=jpg&thumbnail=256
.service(web::resource("/pictrs/image/{filename}").route(web::get().to(full_res)))
.service(web::resource("/pictrs/image/delete/{token}/{filename}").route(web::get().to(delete)));
.service(web::resource("/pictrs/image/delete/{token}/{filename}").route(web::get().to(delete)))
.service(web::resource("/pictrs/internal/purge").route(web::post().to(purge)));
}
#[derive(Debug, Serialize, Deserialize)]
@ -49,6 +49,14 @@ struct PictrsParams {
thumbnail: Option<String>,
}
#[derive(Deserialize)]
enum PictrsPurgeParams {
#[serde(rename = "file")]
File(String),
#[serde(rename = "alias")]
Alias(String),
}
fn adapt_request(
request: &HttpRequest,
client: &ClientWithMiddleware,
@ -57,7 +65,9 @@ fn adapt_request(
// remove accept-encoding header so that pictrs doesnt compress the response
const INVALID_HEADERS: &[HeaderName] = &[ACCEPT_ENCODING, HOST];
let client_request = client.request(request.method().clone(), url);
let client_request = client
.request(request.method().clone(), url)
.timeout(REQWEST_TIMEOUT);
request
.headers()
@ -86,7 +96,8 @@ async fn upload(
return Ok(HttpResponse::Unauthorized().finish());
};
let image_url = format!("{}/image", pictrs_url(context.settings().pictrs_url)?);
let pictrs_config = context.settings().pictrs_config()?;
let image_url = format!("{}/image", pictrs_config.url);
let mut client_req = adapt_request(&req, &client, image_url);
@ -116,22 +127,16 @@ async fn full_res(
let name = &filename.into_inner();
// If there are no query params, the URL is original
let pictrs_url_settings = context.settings().pictrs_url;
let pictrs_config = context.settings().pictrs_config()?;
let url = if params.format.is_none() && params.thumbnail.is_none() {
format!(
"{}/image/original/{}",
pictrs_url(pictrs_url_settings)?,
name,
)
format!("{}/image/original/{}", pictrs_config.url, name,)
} else {
// Use jpg as a default when none is given
let format = params.format.unwrap_or_else(|| "jpg".to_string());
let mut url = format!(
"{}/image/process.{}?src={}",
pictrs_url(pictrs_url_settings)?,
format,
name,
pictrs_config.url, format, name,
);
if let Some(size) = params.thumbnail {
@ -181,12 +186,8 @@ async fn delete(
) -> Result<HttpResponse, Error> {
let (token, file) = components.into_inner();
let url = format!(
"{}/image/delete/{}/{}",
pictrs_url(context.settings().pictrs_url)?,
&token,
&file
);
let pictrs_config = context.settings().pictrs_config()?;
let url = format!("{}/image/delete/{}/{}", pictrs_config.url, &token, &file);
let mut client_req = adapt_request(&req, &client, url);
@ -199,8 +200,32 @@ async fn delete(
Ok(HttpResponse::build(res.status()).body(BodyStream::new(res.bytes_stream())))
}
fn pictrs_url(pictrs_url: Option<String>) -> Result<String, LemmyError> {
pictrs_url.ok_or_else(|| anyhow!("images_disabled").into())
async fn purge(
web::Query(params): web::Query<PictrsPurgeParams>,
req: HttpRequest,
client: web::Data<ClientWithMiddleware>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse, Error> {
let purge_string = match params {
PictrsPurgeParams::File(f) => format!("file={}", f),
PictrsPurgeParams::Alias(a) => format!("alias={}", a),
};
let pictrs_config = context.settings().pictrs_config()?;
let url = format!("{}/internal/purge?{}", pictrs_config.url, &purge_string);
let mut client_req = adapt_request(&req, &client, url);
if let Some(addr) = req.head().peer_addr {
client_req = client_req.header("X-Forwarded-For", addr.to_string())
}
// Add the API token, X-Api-Token header
client_req = client_req.header("x-api-token", pictrs_config.api_key);
let res = client_req.send().await.map_err(error::ErrorBadRequest)?;
Ok(HttpResponse::build(res.status()).body(BodyStream::new(res.bytes_stream())))
}
fn make_send<S>(mut stream: S) -> impl Stream<Item = S::Item> + Send + Unpin + 'static

View file

@ -1,4 +1,8 @@
use crate::{error::LemmyError, location_info, settings::structs::Settings};
use crate::{
error::LemmyError,
location_info,
settings::structs::{PictrsConfig, Settings},
};
use anyhow::{anyhow, Context};
use deser_hjson::from_str;
use once_cell::sync::Lazy;
@ -116,4 +120,11 @@ impl Settings {
.expect("compile regex")
})
}
pub fn pictrs_config(&self) -> Result<PictrsConfig, LemmyError> {
self
.pictrs_config
.to_owned()
.ok_or_else(|| anyhow!("images_disabled").into())
}
}

View file

@ -14,6 +14,9 @@ pub struct Settings {
/// Settings related to activitypub federation
#[default(FederationConfig::default())]
pub federation: FederationConfig,
/// Pictrs image server configuration.
#[default(None)]
pub(crate) pictrs_config: Option<PictrsConfig>,
#[default(CaptchaConfig::default())]
pub captcha: CaptchaConfig,
/// Email sending configuration. All options except login/password are mandatory
@ -36,12 +39,9 @@ pub struct Settings {
/// Whether the site is available over TLS. Needs to be true for federation to work.
#[default(true)]
pub tls_enabled: bool,
/// Address where pictrs is available (for image hosting)
#[default(None)]
#[doku(example = "http://localhost:8080")]
pub pictrs_url: Option<String>,
#[default(None)]
#[doku(example = "(\\bThis\\b)|(\\bis\\b)|(\\bsample\\b)")]
/// A regex list of slurs to block / hide
pub slur_filter: Option<String>,
/// Maximum length of local community and user names
#[default(20)]
@ -56,6 +56,18 @@ pub struct Settings {
pub opentelemetry_url: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone, SmartDefault, Document)]
#[serde(default)]
pub struct PictrsConfig {
/// Address where pictrs is available (for image hosting)
#[default("http://pictrs:8080")]
pub url: String,
/// Set a custom pictrs API key. ( Required for deleting images )
#[default("API_KEY")]
pub api_key: String,
}
#[derive(Debug, Deserialize, Serialize, Clone, SmartDefault, Document)]
#[serde(default)]
pub struct CaptchaConfig {

View file

@ -142,6 +142,10 @@ pub enum UserOperation {
GetSiteMetadata,
BlockCommunity,
BlockPerson,
PurgePerson,
PurgeCommunity,
PurgePost,
PurgeComment,
}
#[derive(EnumString, Display, Debug, Clone)]

View file

@ -56,8 +56,10 @@ services:
user: 991:991
environment:
- PICTRS_OPENTELEMETRY_URL=http://otel:4137
- PICTRS__API_KEY=API_KEY
ports:
- "6670:6669"
- "8080:8080"
volumes:
- ./volumes/pictrs:/mnt
restart: always

View file

@ -20,8 +20,10 @@
# port where lemmy should listen for incoming requests
port: 8536
# settings related to the postgresql database
# address where pictrs is available
pictrs_url: "http://pictrs:8080"
pictrs_config: {
url: "http://pictrs:8080"
api_key: "API_KEY"
}
database: {
# name of the postgres database for lemmy
database: "lemmy"

View file

@ -0,0 +1,4 @@
drop table admin_purge_person;
drop table admin_purge_community;
drop table admin_purge_post;
drop table admin_purge_comment;

View file

@ -0,0 +1,31 @@
-- Add the admin_purge tables
create table admin_purge_person (
id serial primary key,
admin_person_id int references person on update cascade on delete cascade not null,
reason text,
when_ timestamp not null default now()
);
create table admin_purge_community (
id serial primary key,
admin_person_id int references person on update cascade on delete cascade not null,
reason text,
when_ timestamp not null default now()
);
create table admin_purge_post (
id serial primary key,
admin_person_id int references person on update cascade on delete cascade not null,
community_id int references community on update cascade on delete cascade not null,
reason text,
when_ timestamp not null default now()
);
create table admin_purge_comment (
id serial primary key,
admin_person_id int references person on update cascade on delete cascade not null,
post_id int references post on update cascade on delete cascade not null,
reason text,
when_ timestamp not null default now()
);

View file

@ -232,6 +232,14 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
"/registration_application/approve",
web::put().to(route_post::<ApproveRegistrationApplication>),
),
)
.service(
web::scope("/admin/purge")
.wrap(rate_limit.message())
.route("/person", web::post().to(route_post::<PurgePerson>))
.route("/community", web::post().to(route_post::<PurgeCommunity>))
.route("/post", web::post().to(route_post::<PurgePost>))
.route("/comment", web::post().to(route_post::<PurgeComment>)),
),
);
}

View file

@ -99,7 +99,7 @@ async fn main() -> Result<(), LemmyError> {
settings.bind, settings.port
);
let client = Client::builder()
let reqwest_client = Client::builder()
.user_agent(build_user_agent(&settings))
.timeout(REQWEST_TIMEOUT)
.build()?;
@ -111,11 +111,16 @@ async fn main() -> Result<(), LemmyError> {
backoff_exponent: 2,
};
let client = ClientBuilder::new(client)
let client = ClientBuilder::new(reqwest_client.clone())
.with(TracingMiddleware)
.with(RetryTransientMiddleware::new_with_policy(retry_policy))
.build();
// Pictrs cannot use the retry middleware
let pictrs_client = ClientBuilder::new(reqwest_client.clone())
.with(TracingMiddleware)
.build();
check_private_instance_and_federation_enabled(&pool, &settings).await?;
let chat_server = ChatServer::startup(
@ -149,7 +154,7 @@ async fn main() -> Result<(), LemmyError> {
.configure(|cfg| api_routes::config(cfg, &rate_limiter))
.configure(|cfg| lemmy_apub::http::routes::config(cfg, &settings))
.configure(feeds::config)
.configure(|cfg| images::config(cfg, client.clone(), &rate_limiter))
.configure(|cfg| images::config(cfg, pictrs_client.clone(), &rate_limiter))
.configure(nodeinfo::config)
.configure(|cfg| webfinger::config(cfg, &settings))
})