Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion crates/api/src/site/purge/post.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use lemmy_db_schema::{
local_user::LocalUser,
mod_log::admin::{AdminPurgePost, AdminPurgePostForm},
post::Post,
post_gallery::PostGallery,
},
traits::Crud,
};
Expand All @@ -37,7 +38,15 @@ pub async fn purge_post(
)
.await?;

purge_post_images(post.url.clone(), post.thumbnail_url.clone(), &context).await;
let gallery = PostGallery::list_from_post_id(post.id, &mut context.pool()).await?;

purge_post_images(
post.url.clone(),
post.thumbnail_url.clone(),
&gallery,
&context,
)
.await;

Post::delete(&mut context.pool(), data.post_id).await?;

Expand Down
21 changes: 21 additions & 0 deletions crates/api_common/src/post.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ pub struct CreatePost {
pub community_id: CommunityId,
#[cfg_attr(feature = "full", ts(optional))]
pub url: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub gallery: Option<Vec<CreateGalleryItem>>,
/// An optional body for the post in markdown.
#[cfg_attr(feature = "full", ts(optional))]
pub body: Option<String>,
Expand Down Expand Up @@ -51,6 +53,23 @@ pub struct PostResponse {
pub post_view: PostView,
}

#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct CreateGalleryItem {
pub url: String,
/// Can be used to set the order of images in a gallery.
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i32>,
/// An optional alt_text.
#[cfg_attr(feature = "full", ts(optional))]
pub alt_text: Option<String>,
/// Optional caption to be displayed with the image.
#[cfg_attr(feature = "full", ts(optional))]
pub caption: Option<String>,
}

#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
Expand Down Expand Up @@ -162,6 +181,8 @@ pub struct EditPost {
/// An optional body for the post in markdown.
#[cfg_attr(feature = "full", ts(optional))]
pub body: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub gallery: Option<Vec<CreateGalleryItem>>,
/// An optional alt_text, usable for image posts.
#[cfg_attr(feature = "full", ts(optional))]
pub alt_text: Option<String>,
Expand Down
33 changes: 31 additions & 2 deletions crates/api_common/src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use futures::StreamExt;
use lemmy_db_schema::source::{
images::{ImageDetailsInsertForm, LocalImage, LocalImageForm},
post::{Post, PostUpdateForm},
post_gallery::PostGalleryInsertForm,
site::Site,
};
use lemmy_utils::{
Expand Down Expand Up @@ -244,15 +245,16 @@ pub async fn generate_post_link_metadata(
};

let form = PostUpdateForm {
url,
url: url.clone(),
embed_title: Some(metadata.opengraph_data.title),
embed_description: Some(metadata.opengraph_data.description),
embed_video_url: Some(metadata.opengraph_data.embed_video_url),
thumbnail_url: Some(thumbnail_url),
url_content_type: Some(metadata.content_type),
url_content_type: Some(metadata.content_type.clone()),
..Default::default()
};
let updated_post = Post::update(&mut context.pool(), post.id, &form).await?;

if let Some(send_activity) = send_activity(updated_post) {
ActivityChannel::submit_activity(send_activity, &context)?;
}
Expand Down Expand Up @@ -310,6 +312,33 @@ fn extract_opengraph_data(html_bytes: &[u8], url: &Url) -> LemmyResult<OpenGraph
})
}

pub async fn check_gallery_items_are_images(
gallery_items: &Vec<PostGalleryInsertForm>,
context: &LemmyContext,
) -> Result<Vec<PostGalleryInsertForm>, LemmyError> {
let mut validated = vec![];
for item in gallery_items {
let metadata = fetch_link_metadata(&item.url, context, false).await?;
let is_image = metadata
.content_type
.as_ref()
.is_some_and(|content_type| content_type.starts_with("image"));

if !is_image {
Err(LemmyErrorType::UrlNotImage(item.url.to_string()))?
} else {
let proxied = proxy_image_link(item.url.clone().into(), false, context).await?;
validated.push(PostGalleryInsertForm {
url: proxied,
url_content_type: metadata.content_type,
..item.clone()
});
}
}

Ok(validated)
}

#[derive(Deserialize, Serialize, Debug)]
pub struct PictrsResponse {
#[serde(default)]
Expand Down
72 changes: 70 additions & 2 deletions crates/api_common/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{
claims::Claims,
context::LemmyContext,
post::CreateGalleryItem,
request::{
delete_image_from_pictrs,
fetch_pictrs_proxied_image_details,
Expand Down Expand Up @@ -31,12 +32,13 @@ use lemmy_db_schema::{
oauth_account::OAuthAccount,
person::{Person, PersonActions, PersonUpdateForm},
post::{Post, PostActions, PostReadCommentsForm},
post_gallery::{PostGallery, PostGalleryInsertForm},
private_message::PrivateMessage,
registration_application::RegistrationApplication,
site::Site,
},
traits::{Blockable, Crud, Likeable, ReadComments},
utils::DbPool,
utils::{diesel_url_create, DbPool},
};
use lemmy_db_schema_file::enums::{FederationMode, RegistrationMode};
use lemmy_db_views_community_follower::CommunityFollowerView;
Expand All @@ -54,7 +56,15 @@ use lemmy_utils::{
utils::{
markdown::{image_links::markdown_rewrite_image_links, markdown_check_for_blocked_urls},
slurs::remove_slurs,
validation::{build_and_check_regex, clean_urls_in_text},
validation::{
build_and_check_regex,
clean_urls_in_text,
is_url_blocked,
is_valid_alt_text_field,
is_valid_post_title,
is_valid_url,
MAX_GALLERY_LENGTH,
},
},
CacheLock,
CACHE_DURATION_FEDERATION,
Expand Down Expand Up @@ -522,6 +532,60 @@ pub fn check_nsfw_allowed(nsfw: Option<bool>, local_site: Option<&LocalSite>) ->
Ok(())
}

pub fn process_gallery(
gallery_items: Option<&Vec<CreateGalleryItem>>,
url_blocklist: &RegexSet,
) -> LemmyResult<Option<Vec<PostGalleryInsertForm>>> {
if let Some(gallery_items) = gallery_items {
if gallery_items.len() > MAX_GALLERY_LENGTH {
Err(LemmyErrorType::TooManyItems)?
}

let mut gallery_forms = vec![];

// Sort the items. Anything with a number is put at the start and ordered by
// that number. Ones without are pushed to the end, in the order they were received.
let mut gallery_items = gallery_items.clone();
gallery_items.sort_by(|left, right| {
if let (Some(left), Some(right)) = (left.page, right.page) {
left.cmp(&right)
} else {
right.page.cmp(&left.page)
}
});

for (index, item) in gallery_items.iter().enumerate() {
let url = diesel_url_create(Some(&item.url))?.ok_or(LemmyErrorType::InvalidUrl)?;
is_url_blocked(&url, url_blocklist)?;
is_valid_url(&url)?;

if let Some(alt_text) = &item.alt_text {
is_valid_alt_text_field(alt_text)?;
}

if let Some(caption) = &item.caption {
if is_valid_post_title(caption).is_err() {
Err(LemmyErrorType::InvalidGalleryCaption)?;
}
}

gallery_forms.push(PostGalleryInsertForm {
// We overwrite this later.
post_id: PostId(0),
page: index.try_into()?,
url,
url_content_type: None,
caption: item.caption.clone(),
alt_text: item.alt_text.clone(),
});
}

Ok(Some(gallery_forms))
} else {
Ok(None)
}
}

/// Read the site for an ap_id.
///
/// Used for GetCommunityResponse and GetPersonDetails
Expand All @@ -537,6 +601,7 @@ pub async fn read_site_for_actor(
pub async fn purge_post_images(
url: Option<DbUrl>,
thumbnail_url: Option<DbUrl>,
gallery: &[PostGallery],
context: &LemmyContext,
) {
if let Some(url) = url {
Expand All @@ -547,6 +612,9 @@ pub async fn purge_post_images(
.await
.ok();
}
for item in gallery {
purge_image_from_pictrs_url(&item.url, context).await.ok();
}
}

/// Delete a local_user's images
Expand Down
67 changes: 57 additions & 10 deletions crates/api_crud/src/post/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,42 @@ use super::convert_published_time;
use crate::community_use_pending;
use activitypub_federation::config::Data;
use actix_web::web::Json;
use diesel_async::{scoped_futures::ScopedFutureExt, AsyncConnection};
use lemmy_api_common::{
build_response::{build_post_response, send_local_notifs},
context::LemmyContext,
plugins::{plugin_hook_after, plugin_hook_before},
post::{CreatePost, PostResponse},
request::generate_post_link_metadata,
request::{check_gallery_items_are_images, generate_post_link_metadata},
send_activity::SendActivityData,
tags::update_post_tags,
utils::{
check_community_user_action,
check_nsfw_allowed,
get_url_blocklist,
honeypot_check,
process_gallery,
process_markdown_opt,
send_webmention,
slur_regex,
},
}, LemmyErrorType,
};
use lemmy_db_schema::{
impls::actor_language::validate_post_language,
newtypes::PostOrCommentId,
source::post::{Post, PostActions, PostInsertForm, PostLikeForm, PostReadForm},
source::{
post::{Post, PostActions, PostInsertForm, PostLikeForm, PostReadForm},
post_gallery::{PostGallery, PostGalleryInsertForm},
},
traits::{Crud, Likeable, Readable},
utils::diesel_url_create,
utils::{diesel_url_create, get_conn},
};
use lemmy_db_views_community::CommunityView;
use lemmy_db_views_community_moderator::CommunityModeratorView;
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_site::SiteView;
use lemmy_utils::{
error::LemmyResult,
error::{LemmyError, LemmyResult},
utils::{
mention::scrape_text_for_mentions,
slurs::check_slurs,
Expand Down Expand Up @@ -60,11 +65,16 @@ pub async fn create_post(

let body = process_markdown_opt(&data.body, &slur_regex, &url_blocklist, &context).await?;
let url = diesel_url_create(data.url.as_deref())?;
let gallery_forms = process_gallery(data.gallery.as_ref(), &url_blocklist)?;
let custom_thumbnail = diesel_url_create(data.custom_thumbnail.as_deref())?;
check_nsfw_allowed(data.nsfw, Some(&local_site))?;

is_valid_post_title(&data.name)?;

if url.is_some() && gallery_forms.is_some() {
Err(LemmyErrorType::PostHasGalleryAndUrl)?
}

if let Some(url) = &url {
is_url_blocked(url, &url_blocklist)?;
is_valid_url(url)?;
Expand All @@ -74,13 +84,22 @@ pub async fn create_post(
is_valid_url(custom_thumbnail)?;
}

if let Some(body) = &body {
is_valid_body_field(body, true)?;
}

if let Some(alt_text) = &data.alt_text {
is_valid_alt_text_field(alt_text)?;
}

if let Some(body) = &body {
is_valid_body_field(body, true)?;
}
let gallery_forms = if let Some(gallery_forms) = gallery_forms {
Some(check_gallery_items_are_images(&gallery_forms, &context).await?)
} else {
None
};
let url_content_type = gallery_forms
.as_ref()
.and_then(|f| f.first().and_then(|item| item.url_content_type.clone()));

let community_view = CommunityView::read(
&mut context.pool(),
Expand Down Expand Up @@ -119,6 +138,7 @@ pub async fn create_post(

let scheduled_publish_time =
convert_published_time(data.scheduled_publish_time, &local_user_view, &context).await?;

let mut post_form = PostInsertForm {
url,
body,
Expand All @@ -127,6 +147,7 @@ pub async fn create_post(
language_id: Some(language_id),
federation_pending: Some(community_use_pending(community, &context).await),
scheduled_publish_time,
url_content_type,
..PostInsertForm::new(
data.name.trim().to_string(),
local_user_view.person.id,
Expand All @@ -136,7 +157,33 @@ pub async fn create_post(

post_form = plugin_hook_before("before_create_local_post", post_form).await?;

let inserted_post = Post::create(&mut context.pool(), &post_form).await?;
let pool = &mut context.pool();
let conn = &mut get_conn(pool).await?;
let inserted_post = conn
.transaction::<_, LemmyError, _>(|conn| {
async move {
let post = Post::create(&mut conn.into(), &post_form).await?;

let _gallery = if let Some(gallery_forms) = gallery_forms {
let post_id = post.id;
let gallert_forms = gallery_forms
.iter()
.map(|f| PostGalleryInsertForm {
post_id,
..f.clone()
})
.collect::<Vec<_>>();

Some(PostGallery::create_from_vec(&gallert_forms, &mut conn.into()).await?)
} else {
None
};

Ok(post)
}
.scope_boxed()
})
.await?;

plugin_hook_after("after_create_local_post", &inserted_post)?;

Expand Down Expand Up @@ -179,7 +226,7 @@ pub async fn create_post(
let do_send_email = !local_site.disable_email_notifications;
send_local_notifs(
mentions,
PostOrCommentId::Post(inserted_post.id),
PostOrCommentId::Post(post_id),
&local_user_view.person,
do_send_email,
&context,
Expand Down
Loading