# 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
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
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
// 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
mod config;
mod leave_admin;
mod mod_log;
+mod purge;
mod registration_applications;
mod resolve_object;
mod search;
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,
.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
added_to_community,
added,
transferred_to_community,
+ admin_purged_persons,
+ admin_purged_communities,
+ admin_purged_posts,
+ admin_purged_comments,
hidden_communities,
})
}
--- /dev/null
+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 })
+ }
+}
--- /dev/null
+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 })
+ }
+}
--- /dev/null
+mod comment;
+mod community;
+mod person;
+mod post;
--- /dev/null
+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 })
+ }
+}
--- /dev/null
+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 })
+ }
+}
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;
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(&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("pictrs_url not set up in config"))
+ Err(LemmyError::from_message(&response.msg))
}
}
use crate::sensitive::Sensitive;
use lemmy_db_schema::{
- newtypes::{CommunityId, PersonId},
+ newtypes::{CommentId, CommunityId, PersonId, PostId},
ListingType,
SearchType,
SortType,
PersonViewSafe,
};
use lemmy_db_views_moderator::structs::{
+ AdminPurgeCommentView,
+ AdminPurgeCommunityView,
+ AdminPurgePersonView,
+ AdminPurgePostView,
ModAddCommunityView,
ModAddView,
ModBanFromCommunityView,
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>,
}
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)
-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::{
settings::structs::Settings,
utils::generate_random_string,
};
+use reqwest_middleware::ClientWithMiddleware;
use rosetta_i18n::{Language, LanguageId};
use tracing::warn;
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
.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??;
}
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)
.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(())
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 {})
})
.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
.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(())
}
}
.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 {
}
}
+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::{
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),
.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 {
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 {
.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 {
}
}
+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,
}
}
+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,
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,
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
);
#[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,
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>,
+}
--- /dev/null
+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>>()
+ }
+}
--- /dev/null
+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>>()
+ }
+}
--- /dev/null
+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>>()
+ }
+}
--- /dev/null
+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>>()
+ }
+}
#[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;
comment::Comment,
community::CommunitySafe,
moderator::{
+ AdminPurgeComment,
+ AdminPurgeCommunity,
+ AdminPurgePerson,
+ AdminPurgePost,
ModAdd,
ModAddCommunity,
ModBan,
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,
+}
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};
)
// 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)]
thumbnail: Option<String>,
}
+#[derive(Deserialize)]
+enum PictrsPurgeParams {
+ #[serde(rename = "file")]
+ File(String),
+ #[serde(rename = "alias")]
+ Alias(String),
+}
+
fn adapt_request(
request: &HttpRequest,
client: &ClientWithMiddleware,
// 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()
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);
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 {
) -> 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);
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
-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;
.expect("compile regex")
})
}
+
+ pub fn pictrs_config(&self) -> Result<PictrsConfig, LemmyError> {
+ self
+ .pictrs_config
+ .to_owned()
+ .ok_or_else(|| anyhow!("images_disabled").into())
+ }
}
/// 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
/// 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)]
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 {
GetSiteMetadata,
BlockCommunity,
BlockPerson,
+ PurgePerson,
+ PurgeCommunity,
+ PurgePost,
+ PurgeComment,
}
#[derive(EnumString, Display, Debug, Clone)]
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
# 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"
--- /dev/null
+drop table admin_purge_person;
+drop table admin_purge_community;
+drop table admin_purge_post;
+drop table admin_purge_comment;
--- /dev/null
+-- 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()
+);
"/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>)),
),
);
}
settings.bind, settings.port
);
- let client = Client::builder()
+ let reqwest_client = Client::builder()
.user_agent(build_user_agent(&settings))
.timeout(REQWEST_TIMEOUT)
.build()?;
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(
.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))
})