Merge remote-tracking branch 'origin/main' into remove_success_responses

This commit is contained in:
Dessalines 2024-09-27 08:49:50 -04:00
commit a5eaad7afd
102 changed files with 622 additions and 497 deletions

1
Cargo.lock generated
View File

@ -2538,6 +2538,7 @@ dependencies = [
"actix-web", "actix-web",
"anyhow", "anyhow",
"bcrypt", "bcrypt",
"chrono",
"futures", "futures",
"lemmy_api_common", "lemmy_api_common",
"lemmy_db_schema", "lemmy_db_schema",

View File

@ -21,16 +21,16 @@
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/node": "^22.0.2", "@types/node": "^22.3.0",
"@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/eslint-plugin": "^8.1.0",
"@typescript-eslint/parser": "^8.0.0", "@typescript-eslint/parser": "^8.1.0",
"eslint": "^9.8.0", "eslint": "^9.9.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"jest": "^29.5.0", "jest": "^29.5.0",
"lemmy-js-client": "0.20.0-alpha.12", "lemmy-js-client": "0.20.0-alpha.12",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"ts-jest": "^29.1.0", "ts-jest": "^29.1.0",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"typescript-eslint": "^8.0.0" "typescript-eslint": "^8.1.0"
} }
} }

View File

@ -12,16 +12,16 @@ importers:
specifier: ^29.5.12 specifier: ^29.5.12
version: 29.5.12 version: 29.5.12
'@types/node': '@types/node':
specifier: ^22.0.2 specifier: ^22.3.0
version: 22.3.0 version: 22.3.0
'@typescript-eslint/eslint-plugin': '@typescript-eslint/eslint-plugin':
specifier: ^8.0.0 specifier: ^8.1.0
version: 8.1.0(@typescript-eslint/parser@8.1.0(eslint@9.9.0)(typescript@5.5.4))(eslint@9.9.0)(typescript@5.5.4) version: 8.1.0(@typescript-eslint/parser@8.1.0(eslint@9.9.0)(typescript@5.5.4))(eslint@9.9.0)(typescript@5.5.4)
'@typescript-eslint/parser': '@typescript-eslint/parser':
specifier: ^8.0.0 specifier: ^8.1.0
version: 8.1.0(eslint@9.9.0)(typescript@5.5.4) version: 8.1.0(eslint@9.9.0)(typescript@5.5.4)
eslint: eslint:
specifier: ^9.8.0 specifier: ^9.9.0
version: 9.9.0 version: 9.9.0
eslint-plugin-prettier: eslint-plugin-prettier:
specifier: ^5.1.3 specifier: ^5.1.3
@ -42,7 +42,7 @@ importers:
specifier: ^5.5.4 specifier: ^5.5.4
version: 5.5.4 version: 5.5.4
typescript-eslint: typescript-eslint:
specifier: ^8.0.0 specifier: ^8.1.0
version: 8.1.0(eslint@9.9.0)(typescript@5.5.4) version: 8.1.0(eslint@9.9.0)(typescript@5.5.4)
packages: packages:

View File

@ -628,7 +628,7 @@ test("Enforce community ban for federated user", async () => {
// Alpha tries to make post on beta, but it fails because of ban // Alpha tries to make post on beta, but it fails because of ban
await expect( await expect(
createPost(alpha, betaCommunity.community.id), createPost(alpha, betaCommunity.community.id),
).rejects.toStrictEqual(Error("banned_from_community")); ).rejects.toStrictEqual(Error("person_is_banned_from_community"));
// Unban alpha // Unban alpha
let unBanAlpha = await banPersonFromCommunity( let unBanAlpha = await banPersonFromCommunity(

View File

@ -52,15 +52,12 @@ pub async fn add_mod_to_community(
// moderator. This is necessary because otherwise the action would be rejected // moderator. This is necessary because otherwise the action would be rejected
// by the community's home instance. // by the community's home instance.
if local_user_view.local_user.admin && !community.local { if local_user_view.local_user.admin && !community.local {
let is_mod = CommunityModeratorView::is_community_moderator( CommunityModeratorView::check_is_community_moderator(
&mut context.pool(), &mut context.pool(),
community.id, community.id,
local_user_view.person.id, local_user_view.person.id,
) )
.await?; .await?;
if !is_mod {
Err(LemmyErrorType::NotAModerator)?
}
} }
// Update in local database // Update in local database

View File

@ -265,8 +265,6 @@ pub async fn local_user_view_from_jwt(
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -63,9 +63,7 @@ pub async fn save_user_settings(
let previous_email = local_user_view.local_user.email.clone().unwrap_or_default(); let previous_email = local_user_view.local_user.email.clone().unwrap_or_default();
// if email was changed, check that it is not taken and send verification mail // if email was changed, check that it is not taken and send verification mail
if previous_email.deref() != email { if previous_email.deref() != email {
if LocalUser::is_email_taken(&mut context.pool(), email).await? { LocalUser::check_is_email_taken(&mut context.pool(), email).await?;
return Err(LemmyErrorType::EmailAlreadyExists)?;
}
send_verification_email( send_verification_email(
&local_user_view, &local_user_view,
email, email,
@ -132,7 +130,6 @@ pub async fn save_user_settings(
send_notifications_to_email: data.send_notifications_to_email, send_notifications_to_email: data.send_notifications_to_email,
show_nsfw: data.show_nsfw, show_nsfw: data.show_nsfw,
blur_nsfw: data.blur_nsfw, blur_nsfw: data.blur_nsfw,
auto_expand: data.auto_expand,
show_bot_accounts: data.show_bot_accounts, show_bot_accounts: data.show_bot_accounts,
default_post_sort_type, default_post_sort_type,
default_comment_sort_type, default_comment_sort_type,

View File

@ -34,7 +34,7 @@ use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{error::LemmyResult, LemmyErrorType, CACHE_DURATION_API}; use lemmy_utils::{error::LemmyResult, LemmyErrorType, CACHE_DURATION_API};
use serial_test::serial; use serial_test::serial;
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
async fn create_test_site(context: &Data<LemmyContext>) -> LemmyResult<(Instance, LocalUserView)> { async fn create_test_site(context: &Data<LemmyContext>) -> LemmyResult<(Instance, LocalUserView)> {
let pool = &mut context.pool(); let pool = &mut context.pool();
@ -109,7 +109,7 @@ async fn signup(
Ok((local_user, application)) Ok((local_user, application))
} }
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
async fn get_application_statuses( async fn get_application_statuses(
context: &Data<LemmyContext>, context: &Data<LemmyContext>,
admin: LocalUserView, admin: LocalUserView,
@ -138,10 +138,9 @@ async fn get_application_statuses(
Ok((application_count, unread_applications, all_applications)) Ok((application_count, unread_applications, all_applications))
} }
#[allow(clippy::indexing_slicing)]
#[allow(clippy::unwrap_used)]
#[tokio::test]
#[serial] #[serial]
#[tokio::test]
#[expect(clippy::indexing_slicing)]
async fn test_application_approval() -> LemmyResult<()> { async fn test_application_approval() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await; let context = LemmyContext::init_test_context().await;
let pool = &mut context.pool(); let pool = &mut context.pool();

View File

@ -42,7 +42,7 @@ pub async fn get_sitemap(context: Data<LemmyContext>) -> LemmyResult<HttpRespons
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
pub(crate) mod tests { pub(crate) mod tests {
use crate::sitemap::generate_urlset; use crate::sitemap::generate_urlset;

View File

@ -29,13 +29,9 @@ impl Claims {
let claims = let claims =
decode::<Claims>(jwt, &key, &validation).with_lemmy_type(LemmyErrorType::NotLoggedIn)?; decode::<Claims>(jwt, &key, &validation).with_lemmy_type(LemmyErrorType::NotLoggedIn)?;
let user_id = LocalUserId(claims.claims.sub.parse()?); let user_id = LocalUserId(claims.claims.sub.parse()?);
let is_valid = LoginToken::validate(&mut context.pool(), user_id, jwt).await?; LoginToken::validate(&mut context.pool(), user_id, jwt).await?;
if !is_valid {
Err(LemmyErrorType::NotLoggedIn)?
} else {
Ok(user_id) Ok(user_id)
} }
}
pub async fn generate( pub async fn generate(
user_id: LocalUserId, user_id: LocalUserId,
@ -73,8 +69,7 @@ impl Claims {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{claims::Claims, context::LemmyContext}; use crate::{claims::Claims, context::LemmyContext};

View File

@ -19,11 +19,12 @@ pub struct CreateOAuthProvider {
pub client_id: String, pub client_id: String,
pub client_secret: String, pub client_secret: String,
pub scopes: String, pub scopes: String,
pub auto_verify_email: bool, pub auto_verify_email: Option<bool>,
pub account_linking_enabled: bool, pub account_linking_enabled: Option<bool>,
pub enabled: bool, pub enabled: Option<bool>,
} }
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]

View File

@ -84,8 +84,8 @@ pub struct CaptchaResponse {
pub struct SaveUserSettings { pub struct SaveUserSettings {
/// Show nsfw posts. /// Show nsfw posts.
pub show_nsfw: Option<bool>, pub show_nsfw: Option<bool>,
/// Blur nsfw posts.
pub blur_nsfw: Option<bool>, pub blur_nsfw: Option<bool>,
pub auto_expand: Option<bool>,
/// Your user's theme. /// Your user's theme.
pub theme: Option<String>, pub theme: Option<String>,
/// The default post listing type, usually "local" /// The default post listing type, usually "local"

View File

@ -30,6 +30,8 @@ pub struct CreatePost {
pub language_id: Option<LanguageId>, pub language_id: Option<LanguageId>,
/// Instead of fetching a thumbnail, use a custom one. /// Instead of fetching a thumbnail, use a custom one.
pub custom_thumbnail: Option<String>, pub custom_thumbnail: Option<String>,
/// Time when this post should be scheduled. Null means publish immediately.
pub scheduled_publish_time: Option<i64>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
@ -124,6 +126,8 @@ pub struct EditPost {
pub language_id: Option<LanguageId>, pub language_id: Option<LanguageId>,
/// Instead of fetching a thumbnail, use a custom one. /// Instead of fetching a thumbnail, use a custom one.
pub custom_thumbnail: Option<String>, pub custom_thumbnail: Option<String>,
/// Time when this post should be scheduled. Null means publish immediately.
pub scheduled_publish_time: Option<i64>,
} }
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]

View File

@ -471,8 +471,7 @@ pub async fn replace_image(
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{ use crate::{

View File

@ -78,7 +78,7 @@ pub struct Search {
pub listing_type: Option<ListingType>, pub listing_type: Option<ListingType>,
pub page: Option<i64>, pub page: Option<i64>,
pub limit: Option<i64>, pub limit: Option<i64>,
pub post_title_only: Option<bool>, pub title_only: Option<bool>,
pub post_url_only: Option<bool>, pub post_url_only: Option<bool>,
pub saved_only: Option<bool>, pub saved_only: Option<bool>,
pub liked_only: Option<bool>, pub liked_only: Option<bool>,

View File

@ -73,13 +73,7 @@ pub async fn is_mod_or_admin(
community_id: CommunityId, community_id: CommunityId,
) -> LemmyResult<()> { ) -> LemmyResult<()> {
check_user_valid(person)?; check_user_valid(person)?;
CommunityView::check_is_mod_or_admin(pool, person.id, community_id).await
let is_mod_or_admin = CommunityView::is_mod_or_admin(pool, person.id, community_id).await?;
if !is_mod_or_admin {
Err(LemmyErrorType::NotAModOrAdmin)?
} else {
Ok(())
}
} }
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
@ -110,13 +104,7 @@ pub async fn check_community_mod_of_any_or_admin_action(
let person = &local_user_view.person; let person = &local_user_view.person;
check_user_valid(person)?; check_user_valid(person)?;
CommunityView::check_is_mod_of_any_or_admin(pool, person.id).await
let is_mod_of_any_or_admin = CommunityView::is_mod_of_any_or_admin(pool, person.id).await?;
if !is_mod_of_any_or_admin {
Err(LemmyErrorType::NotAModOrAdmin)?
} else {
Ok(())
}
} }
pub fn is_admin(local_user_view: &LocalUserView) -> LemmyResult<()> { pub fn is_admin(local_user_view: &LocalUserView) -> LemmyResult<()> {
@ -242,7 +230,7 @@ pub async fn check_community_user_action(
) -> LemmyResult<()> { ) -> LemmyResult<()> {
check_user_valid(person)?; check_user_valid(person)?;
check_community_deleted_removed(community_id, pool).await?; check_community_deleted_removed(community_id, pool).await?;
check_community_ban(person, community_id, pool).await?; CommunityPersonBanView::check(pool, person.id, community_id).await?;
Ok(()) Ok(())
} }
@ -257,19 +245,6 @@ async fn check_community_deleted_removed(
Ok(()) Ok(())
} }
async fn check_community_ban(
person: &Person,
community_id: CommunityId,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
// check if user was banned from site or community
let is_banned = CommunityPersonBanView::get(pool, person.id, community_id).await?;
if is_banned {
Err(LemmyErrorType::BannedFromCommunity)?
}
Ok(())
}
/// Check that the given user can perform a mod action in the community. /// Check that the given user can perform a mod action in the community.
/// ///
/// In particular it checks that he is an admin or mod, wasn't banned and the community isn't /// In particular it checks that he is an admin or mod, wasn't banned and the community isn't
@ -281,7 +256,7 @@ pub async fn check_community_mod_action(
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
) -> LemmyResult<()> { ) -> LemmyResult<()> {
is_mod_or_admin(pool, person, community_id).await?; is_mod_or_admin(pool, person, community_id).await?;
check_community_ban(person, community_id, pool).await?; CommunityPersonBanView::check(pool, person.id, community_id).await?;
// it must be possible to restore deleted community // it must be possible to restore deleted community
if !allow_deleted { if !allow_deleted {
@ -307,51 +282,6 @@ pub fn check_comment_deleted_or_removed(comment: &Comment) -> LemmyResult<()> {
} }
} }
/// Throws an error if a recipient has blocked a person.
#[tracing::instrument(skip_all)]
pub async fn check_person_block(
my_id: PersonId,
potential_blocker_id: PersonId,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
let is_blocked = PersonBlock::read(pool, potential_blocker_id, my_id).await?;
if is_blocked {
Err(LemmyErrorType::PersonIsBlocked)?
} else {
Ok(())
}
}
/// Throws an error if a recipient has blocked a community.
#[tracing::instrument(skip_all)]
async fn check_community_block(
community_id: CommunityId,
person_id: PersonId,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
let is_blocked = CommunityBlock::read(pool, person_id, community_id).await?;
if is_blocked {
Err(LemmyErrorType::CommunityIsBlocked)?
} else {
Ok(())
}
}
/// Throws an error if a recipient has blocked an instance.
#[tracing::instrument(skip_all)]
async fn check_instance_block(
instance_id: InstanceId,
person_id: PersonId,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
let is_blocked = InstanceBlock::read(pool, person_id, instance_id).await?;
if is_blocked {
Err(LemmyErrorType::InstanceIsBlocked)?
} else {
Ok(())
}
}
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn check_person_instance_community_block( pub async fn check_person_instance_community_block(
my_id: PersonId, my_id: PersonId,
@ -360,9 +290,9 @@ pub async fn check_person_instance_community_block(
community_id: CommunityId, community_id: CommunityId,
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
) -> LemmyResult<()> { ) -> LemmyResult<()> {
check_person_block(my_id, potential_blocker_id, pool).await?; PersonBlock::read(pool, potential_blocker_id, my_id).await?;
check_instance_block(community_instance_id, potential_blocker_id, pool).await?; InstanceBlock::read(pool, potential_blocker_id, community_instance_id).await?;
check_community_block(community_id, potential_blocker_id, pool).await?; CommunityBlock::read(pool, potential_blocker_id, community_id).await?;
Ok(()) Ok(())
} }
@ -846,12 +776,13 @@ pub async fn remove_or_restore_user_data_in_community(
// Comments // Comments
// TODO Diesel doesn't allow updates with joins, so this has to be a loop // TODO Diesel doesn't allow updates with joins, so this has to be a loop
let site = Site::read_local(pool).await?;
let comments = CommentQuery { let comments = CommentQuery {
creator_id: Some(banned_person_id), creator_id: Some(banned_person_id),
community_id: Some(community_id), community_id: Some(community_id),
..Default::default() ..Default::default()
} }
.list(pool) .list(&site, pool)
.await?; .await?;
for comment_view in &comments { for comment_view in &comments {
@ -1136,8 +1067,7 @@ fn build_proxied_image_url(
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -27,6 +27,7 @@ futures.workspace = true
uuid = { workspace = true } uuid = { workspace = true }
moka.workspace = true moka.workspace = true
anyhow.workspace = true anyhow.workspace = true
chrono.workspace = true
webmention = "0.6.0" webmention = "0.6.0"
accept-language = "3.1.0" accept-language = "3.1.0"
serde_json = { workspace = true } serde_json = { workspace = true }

View File

@ -1,3 +1,4 @@
use super::convert_published_time;
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use actix_web::web::Json; use actix_web::web::Json;
use lemmy_api_common::{ use lemmy_api_common::{
@ -95,15 +96,12 @@ pub async fn create_post(
let community = Community::read(&mut context.pool(), community_id).await?; let community = Community::read(&mut context.pool(), community_id).await?;
if community.posting_restricted_to_mods { if community.posting_restricted_to_mods {
let community_id = data.community_id; let community_id = data.community_id;
let is_mod = CommunityModeratorView::is_community_moderator( CommunityModeratorView::check_is_community_moderator(
&mut context.pool(), &mut context.pool(),
community_id, community_id,
local_user_view.local_user.person_id, local_user_view.local_user.person_id,
) )
.await?; .await?;
if !is_mod {
Err(LemmyErrorType::OnlyModsCanPostInCommunity)?
}
} }
// Only need to check if language is allowed in case user set it explicitly. When using default // Only need to check if language is allowed in case user set it explicitly. When using default
@ -128,12 +126,15 @@ pub async fn create_post(
} }
}; };
let scheduled_publish_time =
convert_published_time(data.scheduled_publish_time, &local_user_view, &context).await?;
let post_form = PostInsertForm { let post_form = PostInsertForm {
url: url.map(Into::into), url: url.map(Into::into),
body, body,
alt_text: data.alt_text.clone(), alt_text: data.alt_text.clone(),
nsfw: data.nsfw, nsfw: data.nsfw,
language_id, language_id,
scheduled_publish_time,
..PostInsertForm::new( ..PostInsertForm::new(
data.name.trim().to_string(), data.name.trim().to_string(),
local_user_view.person.id, local_user_view.person.id,
@ -145,10 +146,16 @@ pub async fn create_post(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntCreatePost)?; .with_lemmy_type(LemmyErrorType::CouldntCreatePost)?;
let federate_post = if scheduled_publish_time.is_none() {
send_webmention(inserted_post.clone(), community);
|post| Some(SendActivityData::CreatePost(post))
} else {
|_| None
};
generate_post_link_metadata( generate_post_link_metadata(
inserted_post.clone(), inserted_post.clone(),
custom_thumbnail.map(Into::into), custom_thumbnail.map(Into::into),
|post| Some(SendActivityData::CreatePost(post)), federate_post,
context.reset_request_count(), context.reset_request_count(),
) )
.await?; .await?;
@ -168,11 +175,14 @@ pub async fn create_post(
mark_post_as_read(person_id, post_id, &mut context.pool()).await?; mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
if let Some(url) = inserted_post.url.clone() { build_post_response(&context, community_id, local_user_view, post_id).await
}
pub fn send_webmention(post: Post, community: Community) {
if let Some(url) = post.url.clone() {
if community.visibility == CommunityVisibility::Public { if community.visibility == CommunityVisibility::Public {
spawn_try_task(async move { spawn_try_task(async move {
let mut webmention = let mut webmention = Webmention::new::<Url>(post.ap_id.clone().into(), url.clone().into())?;
Webmention::new::<Url>(inserted_post.ap_id.clone().into(), url.clone().into())?;
webmention.set_checked(true); webmention.set_checked(true);
match webmention match webmention
.send() .send()
@ -186,6 +196,4 @@ pub async fn create_post(
}); });
} }
}; };
build_post_response(&context, community_id, local_user_view, post_id).await
} }

View File

@ -1,5 +1,38 @@
use chrono::{DateTime, TimeZone, Utc};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::source::post::Post;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
pub mod create; pub mod create;
pub mod delete; pub mod delete;
pub mod read; pub mod read;
pub mod remove; pub mod remove;
pub mod update; pub mod update;
async fn convert_published_time(
scheduled_publish_time: Option<i64>,
local_user_view: &LocalUserView,
context: &LemmyContext,
) -> LemmyResult<Option<DateTime<Utc>>> {
const MAX_SCHEDULED_POSTS: i64 = 10;
if let Some(scheduled_publish_time) = scheduled_publish_time {
let converted = Utc
.timestamp_opt(scheduled_publish_time, 0)
.single()
.ok_or(LemmyErrorType::InvalidUnixTime)?;
if converted < Utc::now() {
Err(LemmyErrorType::PostScheduleTimeMustBeInFuture)?;
}
if !local_user_view.local_user.admin {
let count =
Post::user_scheduled_post_count(local_user_view.person.id, &mut context.pool()).await?;
if count >= MAX_SCHEDULED_POSTS {
Err(LemmyErrorType::TooManyScheduledPosts)?;
}
}
Ok(Some(converted))
} else {
Ok(None)
}
}

View File

@ -1,3 +1,4 @@
use super::{convert_published_time, create::send_webmention};
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use actix_web::web::Json; use actix_web::web::Json;
use lemmy_api_common::{ use lemmy_api_common::{
@ -16,6 +17,7 @@ use lemmy_api_common::{
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
actor_language::CommunityLanguage, actor_language::CommunityLanguage,
community::Community,
local_site::LocalSite, local_site::LocalSite,
post::{Post, PostUpdateForm}, post::{Post, PostUpdateForm},
}, },
@ -107,6 +109,21 @@ pub async fn update_post(
) )
.await?; .await?;
// handle changes to scheduled_publish_time
let scheduled_publish_time = match (
orig_post.scheduled_publish_time,
data.scheduled_publish_time,
) {
// schedule time can be changed if post is still scheduled (and not published yet)
(Some(_), Some(_)) => {
Some(convert_published_time(data.scheduled_publish_time, &local_user_view, &context).await?)
}
// post was scheduled, gets changed to publish immediately
(Some(_), None) => Some(None),
// unchanged
(_, _) => None,
};
let post_form = PostUpdateForm { let post_form = PostUpdateForm {
name: data.name.clone(), name: data.name.clone(),
url, url,
@ -115,6 +132,7 @@ pub async fn update_post(
nsfw: data.nsfw, nsfw: data.nsfw,
language_id: data.language_id, language_id: data.language_id,
updated: Some(Some(naive_now())), updated: Some(Some(naive_now())),
scheduled_publish_time,
..Default::default() ..Default::default()
}; };
@ -123,13 +141,36 @@ pub async fn update_post(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePost)?; .with_lemmy_type(LemmyErrorType::CouldntUpdatePost)?;
// send out federation/webmention if necessary
match (
orig_post.scheduled_publish_time,
data.scheduled_publish_time,
) {
// schedule was removed, send create activity and webmention
(Some(_), None) => {
let community = Community::read(&mut context.pool(), orig_post.community_id).await?;
send_webmention(updated_post.clone(), community);
generate_post_link_metadata(
updated_post.clone(),
custom_thumbnail.flatten().map(Into::into),
|post| Some(SendActivityData::CreatePost(post)),
context.reset_request_count(),
)
.await?;
}
// post was already public, send update
(None, _) => {
generate_post_link_metadata( generate_post_link_metadata(
updated_post.clone(), updated_post.clone(),
custom_thumbnail.flatten().map(Into::into), custom_thumbnail.flatten().map(Into::into),
|post| Some(SendActivityData::UpdatePost(post)), |post| Some(SendActivityData::UpdatePost(post)),
context.reset_request_count(), context.reset_request_count(),
) )
.await?; .await?
}
// schedule was changed, do nothing
(Some(_), Some(_)) => {}
};
build_post_response( build_post_response(
context.deref(), context.deref(),

View File

@ -5,7 +5,6 @@ use lemmy_api_common::{
private_message::{CreatePrivateMessage, PrivateMessageResponse}, private_message::{CreatePrivateMessage, PrivateMessageResponse},
send_activity::{ActivityChannel, SendActivityData}, send_activity::{ActivityChannel, SendActivityData},
utils::{ utils::{
check_person_block,
get_interface_language, get_interface_language,
get_url_blocklist, get_url_blocklist,
local_site_to_slur_regex, local_site_to_slur_regex,
@ -16,6 +15,7 @@ use lemmy_api_common::{
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
local_site::LocalSite, local_site::LocalSite,
person_block::PersonBlock,
private_message::{PrivateMessage, PrivateMessageInsertForm}, private_message::{PrivateMessage, PrivateMessageInsertForm},
}, },
traits::Crud, traits::Crud,
@ -39,10 +39,10 @@ pub async fn create_private_message(
let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?; let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?;
is_valid_body_field(&content, false)?; is_valid_body_field(&content, false)?;
check_person_block( PersonBlock::read(
local_user_view.person.id,
data.recipient_id,
&mut context.pool(), &mut context.pool(),
data.recipient_id,
local_user_view.person.id,
) )
.await?; .await?;

View File

@ -189,8 +189,6 @@ fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) ->
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::site::create::validate_create_payload; use crate::site::create::validate_create_payload;

View File

@ -48,8 +48,6 @@ fn not_zero(val: Option<i32>) -> Option<i32> {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::site::{application_question_check, not_zero, site_default_post_listing_type_check}; use crate::site::{application_question_check, not_zero, site_default_post_listing_type_check};

View File

@ -241,8 +241,6 @@ fn validate_update_payload(local_site: &LocalSite, edit_site: &EditSite) -> Lemm
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::site::update::validate_update_payload; use crate::site::update::validate_update_payload;

View File

@ -92,9 +92,8 @@ pub async fn register(
} }
if local_site.site_setup && local_site.captcha_enabled { if local_site.site_setup && local_site.captcha_enabled {
if let Some(captcha_uuid) = &data.captcha_uuid { let uuid = uuid::Uuid::parse_str(&data.captcha_uuid.clone().unwrap_or_default())?;
let uuid = uuid::Uuid::parse_str(captcha_uuid)?; CaptchaAnswer::check_captcha(
let check = CaptchaAnswer::check_captcha(
&mut context.pool(), &mut context.pool(),
CheckCaptchaAnswer { CheckCaptchaAnswer {
uuid, uuid,
@ -102,26 +101,16 @@ pub async fn register(
}, },
) )
.await?; .await?;
if !check {
Err(LemmyErrorType::CaptchaIncorrect)?
}
} else {
Err(LemmyErrorType::CaptchaIncorrect)?
}
} }
let slur_regex = local_site_to_slur_regex(&local_site); let slur_regex = local_site_to_slur_regex(&local_site);
check_slurs(&data.username, &slur_regex)?; check_slurs(&data.username, &slur_regex)?;
check_slurs_opt(&data.answer, &slur_regex)?; check_slurs_opt(&data.answer, &slur_regex)?;
if Person::is_username_taken(&mut context.pool(), &data.username).await? { Person::check_username_taken(&mut context.pool(), &data.username).await?;
return Err(LemmyErrorType::UsernameAlreadyExists)?;
}
if let Some(email) = &data.email { if let Some(email) = &data.email {
if LocalUser::is_email_taken(&mut context.pool(), email).await? { LocalUser::check_is_email_taken(&mut context.pool(), email).await?;
Err(LemmyErrorType::EmailAlreadyExists)?
}
} }
// We have to create both a person, and local_user // We have to create both a person, and local_user
@ -338,9 +327,7 @@ pub async fn authenticate_with_oauth(
check_slurs(username, &slur_regex)?; check_slurs(username, &slur_regex)?;
check_slurs_opt(&data.answer, &slur_regex)?; check_slurs_opt(&data.answer, &slur_regex)?;
if Person::is_username_taken(&mut context.pool(), username).await? { Person::check_username_taken(&mut context.pool(), username).await?;
return Err(LemmyErrorType::UsernameAlreadyExists)?;
}
// We have to create a person, a local_user, and an oauth_account // We have to create a person, a local_user, and an oauth_account
person = create_person( person = create_person(

View File

@ -213,15 +213,13 @@ async fn can_accept_activity_in_community(
context: &Data<LemmyContext>, context: &Data<LemmyContext>,
) -> LemmyResult<()> { ) -> LemmyResult<()> {
if let Some(community) = community { if let Some(community) = community {
if !community.local
&& !CommunityFollower::has_local_followers(&mut context.pool(), community.id).await?
{
Err(LemmyErrorType::CommunityHasNoFollowers)?
}
// Local only community can't federate // Local only community can't federate
if community.visibility != CommunityVisibility::Public { if community.visibility != CommunityVisibility::Public {
return Err(LemmyErrorType::NotFound.into()); return Err(LemmyErrorType::NotFound.into());
} }
if !community.local {
CommunityFollower::check_has_local_followers(&mut context.pool(), community.id).await?
}
} }
Ok(()) Ok(())
} }

View File

@ -87,12 +87,7 @@ pub(crate) async fn verify_person_in_community(
} }
let person_id = person.id; let person_id = person.id;
let community_id = community.id; let community_id = community.id;
let is_banned = CommunityPersonBanView::get(&mut context.pool(), person_id, community_id).await?; CommunityPersonBanView::check(&mut context.pool(), person_id, community_id).await
if is_banned {
Err(LemmyErrorType::PersonIsBannedFromCommunity)?
} else {
Ok(())
}
} }
/// Verify that mod action in community was performed by a moderator. /// Verify that mod action in community was performed by a moderator.
@ -106,14 +101,6 @@ pub(crate) async fn verify_mod_action(
community: &Community, community: &Community,
context: &Data<LemmyContext>, context: &Data<LemmyContext>,
) -> LemmyResult<()> { ) -> LemmyResult<()> {
let mod_ = mod_id.dereference(context).await?;
let is_mod_or_admin =
CommunityView::is_mod_or_admin(&mut context.pool(), mod_.id, community.id).await?;
if is_mod_or_admin {
return Ok(());
}
// mod action comes from the same instance as the community, so it was presumably done // mod action comes from the same instance as the community, so it was presumably done
// by an instance admin. // by an instance admin.
// TODO: federate instance admin status and check it here // TODO: federate instance admin status and check it here
@ -121,7 +108,8 @@ pub(crate) async fn verify_mod_action(
return Ok(()); return Ok(());
} }
Err(LemmyErrorType::NotAModerator)? let mod_ = mod_id.dereference(context).await?;
CommunityView::check_is_mod_or_admin(&mut context.pool(), mod_.id, community.id).await
} }
pub(crate) fn verify_is_public(to: &[Url], cc: &[Url]) -> LemmyResult<()> { pub(crate) fn verify_is_public(to: &[Url], cc: &[Url]) -> LemmyResult<()> {

View File

@ -123,7 +123,6 @@ impl InCommunity for AnnouncableActivities {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{ use crate::{

View File

@ -12,10 +12,13 @@ use lemmy_api_common::{
utils::check_private_instance, utils::check_private_instance,
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{comment::Comment, community::Community, local_site::LocalSite}, source::{comment::Comment, community::Community},
traits::Crud, traits::Crud,
}; };
use lemmy_db_views::{comment_view::CommentQuery, structs::LocalUserView}; use lemmy_db_views::{
comment_view::CommentQuery,
structs::{LocalUserView, SiteView},
};
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -24,8 +27,8 @@ pub async fn list_comments(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: Option<LocalUserView>, local_user_view: Option<LocalUserView>,
) -> LemmyResult<Json<GetCommentsResponse>> { ) -> LemmyResult<Json<GetCommentsResponse>> {
let local_site = LocalSite::read(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool()).await?;
check_private_instance(&local_user_view, &local_site)?; check_private_instance(&local_user_view, &site_view.local_site)?;
let community_id = if let Some(name) = &data.community_name { let community_id = if let Some(name) = &data.community_name {
Some( Some(
@ -40,7 +43,7 @@ pub async fn list_comments(
let sort = Some(comment_sort_type_with_default( let sort = Some(comment_sort_type_with_default(
data.sort, data.sort,
local_user_ref, local_user_ref,
&local_site, &site_view.local_site,
)); ));
let max_depth = data.max_depth; let max_depth = data.max_depth;
let saved_only = data.saved_only; let saved_only = data.saved_only;
@ -58,7 +61,7 @@ pub async fn list_comments(
let listing_type = Some(listing_type_with_default( let listing_type = Some(listing_type_with_default(
data.type_, data.type_,
local_user_view.as_ref().map(|u| &u.local_user), local_user_view.as_ref().map(|u| &u.local_user),
&local_site, &site_view.local_site,
community_id, community_id,
)); ));
@ -88,7 +91,7 @@ pub async fn list_comments(
limit, limit,
..Default::default() ..Default::default()
} }
.list(&mut context.pool()) .list(&site_view.site, &mut context.pool())
.await .await
.with_lemmy_type(LemmyErrorType::CouldntGetComments)?; .with_lemmy_type(LemmyErrorType::CouldntGetComments)?;

View File

@ -85,7 +85,7 @@ pub async fn read_person(
creator_id, creator_id,
..Default::default() ..Default::default()
} }
.list(&mut context.pool()) .list(&local_site.site, &mut context.pool())
.await?; .await?;
let moderates = CommunityModeratorView::for_person( let moderates = CommunityModeratorView::for_person(

View File

@ -47,7 +47,7 @@ pub async fn search(
listing_type, listing_type,
page, page,
limit, limit,
post_title_only, title_only,
post_url_only, post_url_only,
saved_only, saved_only,
liked_only, liked_only,
@ -78,7 +78,7 @@ pub async fn search(
search_term: Some(q.clone()), search_term: Some(q.clone()),
page, page,
limit, limit,
title_only: post_title_only, title_only,
url_only: post_url_only, url_only: post_url_only,
liked_only, liked_only,
disliked_only, disliked_only,
@ -105,6 +105,7 @@ pub async fn search(
sort, sort,
listing_type, listing_type,
search_term: Some(q.clone()), search_term: Some(q.clone()),
title_only,
local_user, local_user,
is_mod_or_admin: is_admin, is_mod_or_admin: is_admin,
page, page,
@ -127,7 +128,9 @@ pub async fn search(
.await?; .await?;
} }
SearchType::Comments => { SearchType::Comments => {
comments = comment_query.list(&mut context.pool()).await?; comments = comment_query
.list(&local_site.site, &mut context.pool())
.await?;
} }
SearchType::Communities => { SearchType::Communities => {
communities = community_query communities = community_query
@ -146,7 +149,9 @@ pub async fn search(
.list(&local_site.site, &mut context.pool()) .list(&local_site.site, &mut context.pool())
.await?; .await?;
comments = comment_query.list(&mut context.pool()).await?; comments = comment_query
.list(&local_site.site, &mut context.pool())
.await?;
communities = if community_or_creator_included { communities = if community_or_creator_included {
vec![] vec![]

View File

@ -127,7 +127,6 @@ pub async fn import_settings(
show_read_posts: data.settings.as_ref().map(|s| s.show_read_posts), show_read_posts: data.settings.as_ref().map(|s| s.show_read_posts),
open_links_in_new_tab: data.settings.as_ref().map(|s| s.open_links_in_new_tab), open_links_in_new_tab: data.settings.as_ref().map(|s| s.open_links_in_new_tab),
blur_nsfw: data.settings.as_ref().map(|s| s.blur_nsfw), blur_nsfw: data.settings.as_ref().map(|s| s.blur_nsfw),
auto_expand: data.settings.as_ref().map(|s| s.auto_expand),
infinite_scroll_enabled: data.settings.as_ref().map(|s| s.infinite_scroll_enabled), infinite_scroll_enabled: data.settings.as_ref().map(|s| s.infinite_scroll_enabled),
post_listing_mode: data.settings.as_ref().map(|s| s.post_listing_mode), post_listing_mode: data.settings.as_ref().map(|s| s.post_listing_mode),
..Default::default() ..Default::default()
@ -308,8 +307,9 @@ where
}); });
Ok(failed_items.into_iter().join(",")) Ok(failed_items.into_iter().join(","))
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::indexing_slicing)] #[expect(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::api::user_settings_backup::{export_settings, import_settings, UserSettingsBackup}; use crate::api::user_settings_backup::{export_settings, import_settings, UserSettingsBackup};

View File

@ -98,7 +98,7 @@ impl Collection for ApubCommunityModerators {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::indexing_slicing)] #[expect(clippy::indexing_slicing)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -120,8 +120,7 @@ pub(crate) async fn get_apub_community_featured(
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
pub(crate) mod tests { pub(crate) mod tests {
use super::*; use super::*;

View File

@ -39,7 +39,7 @@ use lemmy_db_schema::{
}; };
use lemmy_db_views_actor::structs::CommunityModeratorView; use lemmy_db_views_actor::structs::CommunityModeratorView;
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyError, LemmyErrorType, LemmyResult}, error::{LemmyError, LemmyResult},
spawn_try_task, spawn_try_task,
utils::{ utils::{
markdown::markdown_to_html, markdown::markdown_to_html,
@ -180,15 +180,12 @@ impl Object for ApubPost {
let creator = page.creator()?.dereference(context).await?; let creator = page.creator()?.dereference(context).await?;
let community = page.community(context).await?; let community = page.community(context).await?;
if community.posting_restricted_to_mods { if community.posting_restricted_to_mods {
let is_mod = CommunityModeratorView::is_community_moderator( CommunityModeratorView::check_is_community_moderator(
&mut context.pool(), &mut context.pool(),
community.id, community.id,
creator.id, creator.id,
) )
.await?; .await?;
if !is_mod {
Err(LemmyErrorType::OnlyModsCanPostInCommunity)?
}
} }
let mut name = page let mut name = page
.name .name

View File

@ -15,12 +15,13 @@ use activitypub_federation::{
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
utils::{check_person_block, get_url_blocklist, local_site_opt_to_slur_regex, process_markdown}, utils::{get_url_blocklist, local_site_opt_to_slur_regex, process_markdown},
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
local_site::LocalSite, local_site::LocalSite,
person::Person, person::Person,
person_block::PersonBlock,
private_message::{PrivateMessage, PrivateMessageInsertForm}, private_message::{PrivateMessage, PrivateMessageInsertForm},
}, },
traits::Crud, traits::Crud,
@ -126,7 +127,7 @@ impl Object for ApubPrivateMessage {
) -> LemmyResult<ApubPrivateMessage> { ) -> LemmyResult<ApubPrivateMessage> {
let creator = note.attributed_to.dereference(context).await?; let creator = note.attributed_to.dereference(context).await?;
let recipient = note.to[0].dereference(context).await?; let recipient = note.to[0].dereference(context).await?;
check_person_block(creator.id, recipient.id, &mut context.pool()).await?; PersonBlock::read(&mut context.pool(), recipient.id, creator.id).await?;
let local_site = LocalSite::read(&mut context.pool()).await.ok(); let local_site = LocalSite::read(&mut context.pool()).await.ok();
let slur_regex = &local_site_opt_to_slur_regex(&local_site); let slur_regex = &local_site_opt_to_slur_regex(&local_site);

View File

@ -75,7 +75,7 @@ impl<S: ValidGrouping<(), IsAggregate = is_aggregate::No>> ValidGrouping<()>
type IsAggregate = is_aggregate::No; type IsAggregate = is_aggregate::No;
} }
#[allow(non_camel_case_types)] #[expect(non_camel_case_types)]
#[derive(QueryId, Clone, Copy, Debug)] #[derive(QueryId, Clone, Copy, Debug)]
pub struct current_value; pub struct current_value;

View File

@ -30,8 +30,7 @@ impl CommentAggregates {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{ use crate::{

View File

@ -36,8 +36,7 @@ impl CommunityAggregates {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{ use crate::{

View File

@ -20,8 +20,7 @@ impl PersonAggregates {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{ use crate::{

View File

@ -49,8 +49,8 @@ impl PostAggregates {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{ use crate::{

View File

@ -15,8 +15,8 @@ impl SiteAggregates {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{ use crate::{

View File

@ -58,8 +58,7 @@ impl ReceivedActivity {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -392,8 +392,8 @@ async fn convert_read_languages(
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)] #[expect(clippy::indexing_slicing)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -13,6 +13,7 @@ use diesel::{
QueryDsl, QueryDsl,
}; };
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
impl CaptchaAnswer { impl CaptchaAnswer {
pub async fn insert(pool: &mut DbPool<'_>, captcha: &CaptchaAnswerForm) -> Result<Self, Error> { pub async fn insert(pool: &mut DbPool<'_>, captcha: &CaptchaAnswerForm) -> Result<Self, Error> {
@ -27,7 +28,7 @@ impl CaptchaAnswer {
pub async fn check_captcha( pub async fn check_captcha(
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
to_check: CheckCaptchaAnswer, to_check: CheckCaptchaAnswer,
) -> Result<bool, Error> { ) -> LemmyResult<()> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
// fetch requested captcha // fetch requested captcha
@ -43,13 +44,13 @@ impl CaptchaAnswer {
.execute(conn) .execute(conn)
.await?; .await?;
Ok(captcha_exists) captcha_exists
.then_some(())
.ok_or(LemmyErrorType::CaptchaIncorrect.into())
} }
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{ use crate::{
@ -83,7 +84,6 @@ mod tests {
.await; .await;
assert!(result.is_ok()); assert!(result.is_ok());
assert!(result.unwrap());
} }
#[tokio::test] #[tokio::test]
@ -119,7 +119,6 @@ mod tests {
) )
.await; .await;
assert!(result_repeat.is_ok()); assert!(result_repeat.is_err());
assert!(!result_repeat.unwrap());
} }
} }

View File

@ -196,8 +196,7 @@ impl Saveable for CommentSaved {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{ use crate::{

View File

@ -35,8 +35,7 @@ use crate::{
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use diesel::{ use diesel::{
deserialize, deserialize,
dsl, dsl::{self, exists, insert_into},
dsl::{exists, insert_into},
pg::Pg, pg::Pg,
result::Error, result::Error,
select, select,
@ -320,16 +319,18 @@ impl CommunityFollower {
/// Check if a remote instance has any followers on local instance. For this it is enough to check /// Check if a remote instance has any followers on local instance. For this it is enough to check
/// if any follow relation is stored. Dont use this for local community. /// if any follow relation is stored. Dont use this for local community.
pub async fn has_local_followers( pub async fn check_has_local_followers(
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
remote_community_id: CommunityId, remote_community_id: CommunityId,
) -> Result<bool, Error> { ) -> LemmyResult<()> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
select(exists(community_follower::table.filter( select(exists(community_follower::table.filter(
community_follower::community_id.eq(remote_community_id), community_follower::community_id.eq(remote_community_id),
))) )))
.get_result(conn) .get_result::<bool>(conn)
.await .await?
.then_some(())
.ok_or(LemmyErrorType::CommunityHasNoFollowers.into())
} }
} }
@ -430,7 +431,6 @@ impl ApubActor for Community {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{ use crate::{
source::{ source::{

View File

@ -9,26 +9,29 @@ use crate::{
utils::{get_conn, DbPool}, utils::{get_conn, DbPool},
}; };
use diesel::{ use diesel::{
dsl::{exists, insert_into}, dsl::{exists, insert_into, not},
result::Error, result::Error,
select, select,
ExpressionMethods, ExpressionMethods,
QueryDsl, QueryDsl,
}; };
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
impl CommunityBlock { impl CommunityBlock {
pub async fn read( pub async fn read(
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
for_person_id: PersonId, for_person_id: PersonId,
for_community_id: CommunityId, for_community_id: CommunityId,
) -> Result<bool, Error> { ) -> LemmyResult<()> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
select(exists( select(not(exists(
community_block::table.find((for_person_id, for_community_id)), community_block::table.find((for_person_id, for_community_id)),
)) )))
.get_result(conn) .get_result::<bool>(conn)
.await .await?
.then_some(())
.ok_or(LemmyErrorType::CommunityIsBlocked.into())
} }
pub async fn for_person( pub async fn for_person(

View File

@ -48,8 +48,7 @@ impl FederationAllowList {
} }
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{ use crate::{

View File

@ -9,26 +9,29 @@ use crate::{
utils::{get_conn, DbPool}, utils::{get_conn, DbPool},
}; };
use diesel::{ use diesel::{
dsl::{exists, insert_into}, dsl::{exists, insert_into, not},
result::Error, result::Error,
select, select,
ExpressionMethods, ExpressionMethods,
QueryDsl, QueryDsl,
}; };
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
impl InstanceBlock { impl InstanceBlock {
pub async fn read( pub async fn read(
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
for_person_id: PersonId, for_person_id: PersonId,
for_instance_id: InstanceId, for_instance_id: InstanceId,
) -> Result<bool, Error> { ) -> LemmyResult<()> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
select(exists( select(not(exists(
instance_block::table.find((for_person_id, for_instance_id)), instance_block::table.find((for_person_id, for_instance_id)),
)) )))
.get_result(conn) .get_result::<bool>(conn)
.await .await?
.then_some(())
.ok_or(LemmyErrorType::InstanceIsBlocked.into())
} }
pub async fn for_person( pub async fn for_person(

View File

@ -41,8 +41,8 @@ impl Language {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)] #[expect(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{source::language::Language, utils::build_db_pool_for_tests}; use crate::{source::language::Language, utils::build_db_pool_for_tests};

View File

@ -136,14 +136,16 @@ impl LocalUser {
diesel::delete(persons).execute(conn).await diesel::delete(persons).execute(conn).await
} }
pub async fn is_email_taken(pool: &mut DbPool<'_>, email: &str) -> Result<bool, Error> { pub async fn check_is_email_taken(pool: &mut DbPool<'_>, email: &str) -> LemmyResult<()> {
use diesel::dsl::{exists, select}; use diesel::dsl::{exists, select};
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
select(exists(local_user::table.filter( select(not(exists(local_user::table.filter(
lower(coalesce(local_user::email, "")).eq(email.to_lowercase()), lower(coalesce(local_user::email, "")).eq(email.to_lowercase()),
))) ))))
.get_result(conn) .get_result::<bool>(conn)
.await .await?
.then_some(())
.ok_or(LemmyErrorType::EmailAlreadyExists.into())
} }
// TODO: maybe move this and pass in LocalUserView // TODO: maybe move this and pass in LocalUserView
@ -367,7 +369,6 @@ pub struct UserBackupLists {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{ use crate::{
source::{ source::{
@ -419,4 +420,32 @@ mod tests {
Ok(()) Ok(())
} }
#[tokio::test]
#[serial]
async fn test_email_taken() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let darwin_email = "charles.darwin@gmail.com";
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let darwin_person = PersonInsertForm::test_form(inserted_instance.id, "darwin");
let inserted_darwin_person = Person::create(pool, &darwin_person).await?;
let mut darwin_local_user_form =
LocalUserInsertForm::test_form_admin(inserted_darwin_person.id);
darwin_local_user_form.email = Some(darwin_email.into());
let _inserted_darwin_local_user =
LocalUser::create(pool, &darwin_local_user_form, vec![]).await?;
let check = LocalUser::check_is_email_taken(pool, darwin_email).await;
assert!(check.is_err());
let passed_check = LocalUser::check_is_email_taken(pool, "not_charles@gmail.com").await;
assert!(passed_check.is_ok());
Ok(())
}
} }

View File

@ -7,6 +7,7 @@ use crate::{
}; };
use diesel::{delete, dsl::exists, insert_into, result::Error, select}; use diesel::{delete, dsl::exists, insert_into, result::Error, select};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
impl LoginToken { impl LoginToken {
pub async fn create(pool: &mut DbPool<'_>, form: LoginTokenCreateForm) -> Result<Self, Error> { pub async fn create(pool: &mut DbPool<'_>, form: LoginTokenCreateForm) -> Result<Self, Error> {
@ -22,13 +23,15 @@ impl LoginToken {
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
user_id_: LocalUserId, user_id_: LocalUserId,
token_: &str, token_: &str,
) -> Result<bool, Error> { ) -> LemmyResult<()> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
select(exists( select(exists(
login_token.find(token_).filter(user_id.eq(user_id_)), login_token.find(token_).filter(user_id.eq(user_id_)),
)) ))
.get_result(conn) .get_result::<bool>(conn)
.await .await?
.then_some(())
.ok_or(LemmyErrorType::NotLoggedIn.into())
} }
pub async fn list( pub async fn list(

View File

@ -465,8 +465,7 @@ impl Crud for AdminPurgeComment {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{ use crate::{

View File

@ -1,32 +1,13 @@
use crate::{ use crate::{
newtypes::{LocalUserId, OAuthProviderId}, newtypes::LocalUserId,
schema::{oauth_account, oauth_account::dsl::local_user_id}, schema::{oauth_account, oauth_account::dsl::local_user_id},
source::oauth_account::{OAuthAccount, OAuthAccountInsertForm}, source::oauth_account::{OAuthAccount, OAuthAccountInsertForm},
utils::{get_conn, DbPool}, utils::{get_conn, DbPool},
}; };
use diesel::{ use diesel::{insert_into, result::Error, ExpressionMethods, QueryDsl};
dsl::{exists, insert_into},
result::Error,
select,
ExpressionMethods,
QueryDsl,
};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
impl OAuthAccount { impl OAuthAccount {
pub async fn read(
pool: &mut DbPool<'_>,
for_oauth_provider_id: OAuthProviderId,
for_local_user_id: LocalUserId,
) -> Result<bool, Error> {
let conn = &mut get_conn(pool).await?;
select(exists(
oauth_account::table.find((for_oauth_provider_id, for_local_user_id)),
))
.get_result(conn)
.await
}
pub async fn create(pool: &mut DbPool<'_>, form: &OAuthAccountInsertForm) -> Result<Self, Error> { pub async fn create(pool: &mut DbPool<'_>, form: &OAuthAccountInsertForm) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
insert_into(oauth_account::table) insert_into(oauth_account::table)
@ -35,17 +16,6 @@ impl OAuthAccount {
.await .await
} }
pub async fn delete(
pool: &mut DbPool<'_>,
for_oauth_provider_id: OAuthProviderId,
for_local_user_id: LocalUserId,
) -> Result<usize, Error> {
let conn = &mut get_conn(pool).await?;
diesel::delete(oauth_account::table.find((for_oauth_provider_id, for_local_user_id)))
.execute(conn)
.await
}
pub async fn delete_user_accounts( pub async fn delete_user_accounts(
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
for_local_user_id: LocalUserId, for_local_user_id: LocalUserId,

View File

@ -42,8 +42,6 @@ impl PasswordResetRequest {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{ use crate::{

View File

@ -21,6 +21,7 @@ use diesel::{
QueryDsl, QueryDsl,
}; };
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
#[async_trait] #[async_trait]
impl Crud for Person { impl Crud for Person {
@ -121,16 +122,18 @@ impl Person {
.await .await
} }
pub async fn is_username_taken(pool: &mut DbPool<'_>, username: &str) -> Result<bool, Error> { pub async fn check_username_taken(pool: &mut DbPool<'_>, username: &str) -> LemmyResult<()> {
use diesel::dsl::{exists, select}; use diesel::dsl::{exists, select};
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
select(exists( select(not(exists(
person::table person::table
.filter(lower(person::name).eq(username.to_lowercase())) .filter(lower(person::name).eq(username.to_lowercase()))
.filter(person::local.eq(true)), .filter(person::local.eq(true)),
)) )))
.get_result(conn) .get_result::<bool>(conn)
.await .await?
.then_some(())
.ok_or(LemmyErrorType::UsernameAlreadyExists.into())
} }
} }
@ -232,7 +235,6 @@ impl PersonFollower {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{ use crate::{

View File

@ -9,7 +9,7 @@ use crate::{
utils::{get_conn, DbPool}, utils::{get_conn, DbPool},
}; };
use diesel::{ use diesel::{
dsl::{exists, insert_into}, dsl::{exists, insert_into, not},
result::Error, result::Error,
select, select,
ExpressionMethods, ExpressionMethods,
@ -17,19 +17,22 @@ use diesel::{
QueryDsl, QueryDsl,
}; };
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
impl PersonBlock { impl PersonBlock {
pub async fn read( pub async fn read(
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
for_person_id: PersonId, for_person_id: PersonId,
for_recipient_id: PersonId, for_recipient_id: PersonId,
) -> Result<bool, Error> { ) -> LemmyResult<()> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
select(exists( select(not(exists(
person_block::table.find((for_person_id, for_recipient_id)), person_block::table.find((for_person_id, for_recipient_id)),
)) )))
.get_result(conn) .get_result::<bool>(conn)
.await .await?
.then_some(())
.ok_or(LemmyErrorType::PersonIsBlocked.into())
} }
pub async fn for_person( pub async fn for_person(

View File

@ -1,7 +1,7 @@
use crate::{ use crate::{
diesel::OptionalExtension, diesel::{BoolExpressionMethods, OptionalExtension},
newtypes::{CommunityId, DbUrl, PersonId, PostId}, newtypes::{CommunityId, DbUrl, PersonId, PostId},
schema::{post, post_hide, post_like, post_read, post_saved}, schema::{community, person, post, post_hide, post_like, post_read, post_saved},
source::post::{ source::post::{
Post, Post,
PostHide, PostHide,
@ -20,6 +20,7 @@ use crate::{
functions::coalesce, functions::coalesce,
get_conn, get_conn,
naive_now, naive_now,
now,
DbPool, DbPool,
DELETED_REPLACEMENT_TEXT, DELETED_REPLACEMENT_TEXT,
FETCH_LIMIT_MAX, FETCH_LIMIT_MAX,
@ -30,7 +31,7 @@ use crate::{
use ::url::Url; use ::url::Url;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use diesel::{ use diesel::{
dsl::insert_into, dsl::{count, insert_into, not},
result::Error, result::Error,
DecoratableTarget, DecoratableTarget,
ExpressionMethods, ExpressionMethods,
@ -172,6 +173,7 @@ impl Post {
let object_id: DbUrl = object_id.into(); let object_id: DbUrl = object_id.into();
post::table post::table
.filter(post::ap_id.eq(object_id)) .filter(post::ap_id.eq(object_id))
.filter(post::scheduled_publish_time.is_null())
.first(conn) .first(conn)
.await .await
.optional() .optional()
@ -245,6 +247,28 @@ impl Post {
.get_results::<Self>(conn) .get_results::<Self>(conn)
.await .await
} }
pub async fn user_scheduled_post_count(
person_id: PersonId,
pool: &mut DbPool<'_>,
) -> Result<i64, Error> {
let conn = &mut get_conn(pool).await?;
post::table
.inner_join(person::table)
.inner_join(community::table)
// find all posts which have scheduled_publish_time that is in the past
.filter(post::scheduled_publish_time.is_not_null())
.filter(coalesce(post::scheduled_publish_time, now()).lt(now()))
// make sure the post and community are still around
.filter(not(post::deleted.or(post::removed)))
.filter(not(community::removed.or(community::deleted)))
// only posts by specified user
.filter(post::creator_id.eq(person_id))
.select(count(post::id))
.first::<i64>(conn)
.await
}
} }
#[async_trait] #[async_trait]
@ -444,6 +468,7 @@ mod tests {
featured_community: false, featured_community: false,
featured_local: false, featured_local: false,
url_content_type: None, url_content_type: None,
scheduled_publish_time: None,
}; };
// Post Like // Post Like

View File

@ -80,8 +80,7 @@ impl Reportable for PostReport {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -85,8 +85,7 @@ impl PrivateMessage {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{ use crate::{

View File

@ -27,7 +27,6 @@ pub mod newtypes;
pub mod sensitive; pub mod sensitive;
#[cfg(feature = "full")] #[cfg(feature = "full")]
#[rustfmt::skip] #[rustfmt::skip]
#[allow(clippy::wildcard_imports)]
pub mod schema; pub mod schema;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod aliases { pub mod aliases {

View File

@ -191,13 +191,13 @@ impl Display for DbUrl {
} }
// the project doesn't compile with From // the project doesn't compile with From
#[allow(clippy::from_over_into)] #[expect(clippy::from_over_into)]
impl Into<DbUrl> for Url { impl Into<DbUrl> for Url {
fn into(self) -> DbUrl { fn into(self) -> DbUrl {
DbUrl(Box::new(self)) DbUrl(Box::new(self))
} }
} }
#[allow(clippy::from_over_into)] #[expect(clippy::from_over_into)]
impl Into<Url> for DbUrl { impl Into<Url> for DbUrl {
fn into(self) -> Url { fn into(self) -> Url {
*self.0 *self.0

View File

@ -459,7 +459,6 @@ diesel::table! {
totp_2fa_secret -> Nullable<Text>, totp_2fa_secret -> Nullable<Text>,
open_links_in_new_tab -> Bool, open_links_in_new_tab -> Bool,
blur_nsfw -> Bool, blur_nsfw -> Bool,
auto_expand -> Bool,
infinite_scroll_enabled -> Bool, infinite_scroll_enabled -> Bool,
admin -> Bool, admin -> Bool,
post_listing_mode -> PostListingModeEnum, post_listing_mode -> PostListingModeEnum,
@ -770,6 +769,7 @@ diesel::table! {
featured_local -> Bool, featured_local -> Bool,
url_content_type -> Nullable<Text>, url_content_type -> Nullable<Text>,
alt_text -> Nullable<Text>, alt_text -> Nullable<Text>,
scheduled_publish_time -> Nullable<Timestamptz>
} }
} }

View File

@ -49,7 +49,6 @@ pub struct LocalUser {
/// Open links in a new tab. /// Open links in a new tab.
pub open_links_in_new_tab: bool, pub open_links_in_new_tab: bool,
pub blur_nsfw: bool, pub blur_nsfw: bool,
pub auto_expand: bool,
/// Whether infinite scroll is enabled. /// Whether infinite scroll is enabled.
pub infinite_scroll_enabled: bool, pub infinite_scroll_enabled: bool,
/// Whether the person is an admin. /// Whether the person is an admin.
@ -104,8 +103,6 @@ pub struct LocalUserInsertForm {
#[new(default)] #[new(default)]
pub blur_nsfw: Option<bool>, pub blur_nsfw: Option<bool>,
#[new(default)] #[new(default)]
pub auto_expand: Option<bool>,
#[new(default)]
pub infinite_scroll_enabled: Option<bool>, pub infinite_scroll_enabled: Option<bool>,
#[new(default)] #[new(default)]
pub admin: Option<bool>, pub admin: Option<bool>,
@ -143,7 +140,6 @@ pub struct LocalUserUpdateForm {
pub totp_2fa_secret: Option<Option<String>>, pub totp_2fa_secret: Option<Option<String>>,
pub open_links_in_new_tab: Option<bool>, pub open_links_in_new_tab: Option<bool>,
pub blur_nsfw: Option<bool>, pub blur_nsfw: Option<bool>,
pub auto_expand: Option<bool>,
pub infinite_scroll_enabled: Option<bool>, pub infinite_scroll_enabled: Option<bool>,
pub admin: Option<bool>, pub admin: Option<bool>,
pub post_listing_mode: Option<PostListingMode>, pub post_listing_mode: Option<PostListingMode>,

View File

@ -87,39 +87,30 @@ impl Serialize for PublicOAuthProvider {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset, TS))] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = oauth_provider))] #[cfg_attr(feature = "full", diesel(table_name = oauth_provider))]
#[cfg_attr(feature = "full", ts(export))]
pub struct OAuthProviderInsertForm { pub struct OAuthProviderInsertForm {
pub display_name: String, pub display_name: String,
#[cfg_attr(feature = "full", ts(type = "string"))]
pub issuer: DbUrl, pub issuer: DbUrl,
#[cfg_attr(feature = "full", ts(type = "string"))]
pub authorization_endpoint: DbUrl, pub authorization_endpoint: DbUrl,
#[cfg_attr(feature = "full", ts(type = "string"))]
pub token_endpoint: DbUrl, pub token_endpoint: DbUrl,
#[cfg_attr(feature = "full", ts(type = "string"))]
pub userinfo_endpoint: DbUrl, pub userinfo_endpoint: DbUrl,
pub id_claim: String, pub id_claim: String,
pub client_id: String, pub client_id: String,
pub client_secret: String, pub client_secret: String,
pub scopes: String, pub scopes: String,
pub auto_verify_email: bool, pub auto_verify_email: Option<bool>,
pub account_linking_enabled: bool, pub account_linking_enabled: Option<bool>,
pub enabled: bool, pub enabled: Option<bool>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset, TS))] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = oauth_provider))] #[cfg_attr(feature = "full", diesel(table_name = oauth_provider))]
#[cfg_attr(feature = "full", ts(export))]
pub struct OAuthProviderUpdateForm { pub struct OAuthProviderUpdateForm {
pub display_name: Option<String>, pub display_name: Option<String>,
#[cfg_attr(feature = "full", ts(type = "string"))]
pub authorization_endpoint: Option<DbUrl>, pub authorization_endpoint: Option<DbUrl>,
#[cfg_attr(feature = "full", ts(type = "string"))]
pub token_endpoint: Option<DbUrl>, pub token_endpoint: Option<DbUrl>,
#[cfg_attr(feature = "full", ts(type = "string"))]
pub userinfo_endpoint: Option<DbUrl>, pub userinfo_endpoint: Option<DbUrl>,
pub id_claim: Option<String>, pub id_claim: Option<String>,
pub client_secret: Option<String>, pub client_secret: Option<String>,

View File

@ -57,6 +57,8 @@ pub struct Post {
pub url_content_type: Option<String>, pub url_content_type: Option<String>,
/// An optional alt_text, usable for image posts. /// An optional alt_text, usable for image posts.
pub alt_text: Option<String>, pub alt_text: Option<String>,
/// Time at which the post will be published. None means publish immediately.
pub scheduled_publish_time: Option<DateTime<Utc>>,
} }
#[derive(Debug, Clone, derive_new::new)] #[derive(Debug, Clone, derive_new::new)]
@ -104,6 +106,8 @@ pub struct PostInsertForm {
pub url_content_type: Option<String>, pub url_content_type: Option<String>,
#[new(default)] #[new(default)]
pub alt_text: Option<String>, pub alt_text: Option<String>,
#[new(default)]
pub scheduled_publish_time: Option<DateTime<Utc>>,
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@ -130,6 +134,7 @@ pub struct PostUpdateForm {
pub featured_local: Option<bool>, pub featured_local: Option<bool>,
pub url_content_type: Option<Option<String>>, pub url_content_type: Option<Option<String>>,
pub alt_text: Option<Option<String>>, pub alt_text: Option<Option<String>>,
pub scheduled_publish_time: Option<Option<DateTime<Utc>>>,
} }
#[derive(PartialEq, Eq, Debug)] #[derive(PartialEq, Eq, Debug)]

View File

@ -595,7 +595,6 @@ impl<RF, LF> Queries<RF, LF> {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -259,8 +259,8 @@ impl CommentReportQuery {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)] #[expect(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{ use crate::{

View File

@ -35,7 +35,7 @@ use lemmy_db_schema::{
person_block, person_block,
post, post,
}, },
source::local_user::LocalUser, source::{local_user::LocalUser, site::Site},
utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
CommentSortType, CommentSortType,
ListingType, ListingType,
@ -43,7 +43,7 @@ use lemmy_db_schema::{
fn queries<'a>() -> Queries< fn queries<'a>() -> Queries<
impl ReadFn<'a, CommentView, (CommentId, Option<&'a LocalUser>)>, impl ReadFn<'a, CommentView, (CommentId, Option<&'a LocalUser>)>,
impl ListFn<'a, CommentView, CommentQuery<'a>>, impl ListFn<'a, CommentView, (CommentQuery<'a>, &'a Site)>,
> { > {
let is_creator_banned_from_community = exists( let is_creator_banned_from_community = exists(
community_person_ban::table.filter( community_person_ban::table.filter(
@ -182,7 +182,7 @@ fn queries<'a>() -> Queries<
query.first(&mut conn).await query.first(&mut conn).await
}; };
let list = move |mut conn: DbConn<'a>, options: CommentQuery<'a>| async move { let list = move |mut conn: DbConn<'a>, (options, site): (CommentQuery<'a>, &'a Site)| async move {
// The left join below will return None in this case // The left join below will return None in this case
let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1)); let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1));
let local_user_id_join = options let local_user_id_join = options
@ -295,6 +295,12 @@ fn queries<'a>() -> Queries<
query = query.filter(not(is_creator_blocked(person_id_join))); query = query.filter(not(is_creator_blocked(person_id_join)));
}; };
if !options.local_user.show_nsfw(site) {
query = query
.filter(post::nsfw.eq(false))
.filter(community::nsfw.eq(false));
};
query = options.local_user.visible_communities_only(query); query = options.local_user.visible_communities_only(query);
// A Max depth given means its a tree fetch // A Max depth given means its a tree fetch
@ -398,10 +404,10 @@ pub struct CommentQuery<'a> {
} }
impl<'a> CommentQuery<'a> { impl<'a> CommentQuery<'a> {
pub async fn list(self, pool: &mut DbPool<'_>) -> Result<Vec<CommentView>, Error> { pub async fn list(self, site: &Site, pool: &mut DbPool<'_>) -> Result<Vec<CommentView>, Error> {
Ok( Ok(
queries() queries()
.list(pool, self) .list(pool, (self, site))
.await? .await?
.into_iter() .into_iter()
.map(|mut c| { .map(|mut c| {
@ -416,8 +422,8 @@ impl<'a> CommentQuery<'a> {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::indexing_slicing)] #[expect(clippy::indexing_slicing)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
mod tests { mod tests {
use crate::{ use crate::{
@ -455,7 +461,8 @@ mod tests {
local_user_vote_display_mode::LocalUserVoteDisplayMode, local_user_vote_display_mode::LocalUserVoteDisplayMode,
person::{Person, PersonInsertForm}, person::{Person, PersonInsertForm},
person_block::{PersonBlock, PersonBlockForm}, person_block::{PersonBlock, PersonBlockForm},
post::{Post, PostInsertForm}, post::{Post, PostInsertForm, PostUpdateForm},
site::{Site, SiteInsertForm},
}, },
traits::{Bannable, Blockable, Crud, Joinable, Likeable, Saveable}, traits::{Bannable, Blockable, Crud, Joinable, Likeable, Saveable},
utils::{build_db_pool_for_tests, RANK_DEFAULT}, utils::{build_db_pool_for_tests, RANK_DEFAULT},
@ -475,6 +482,7 @@ mod tests {
timmy_local_user_view: LocalUserView, timmy_local_user_view: LocalUserView,
inserted_sara_person: Person, inserted_sara_person: Person,
inserted_community: Community, inserted_community: Community,
site: Site,
} }
async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult<Data> { async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult<Data> {
@ -611,6 +619,8 @@ mod tests {
person: inserted_timmy_person.clone(), person: inserted_timmy_person.clone(),
counts: Default::default(), counts: Default::default(),
}; };
let site_form = SiteInsertForm::new("test site".to_string(), inserted_instance.id);
let site = Site::create(pool, &site_form).await?;
Ok(Data { Ok(Data {
inserted_instance, inserted_instance,
inserted_comment_0, inserted_comment_0,
@ -620,6 +630,7 @@ mod tests {
timmy_local_user_view, timmy_local_user_view,
inserted_sara_person, inserted_sara_person,
inserted_community, inserted_community,
site,
}) })
} }
@ -640,7 +651,7 @@ mod tests {
post_id: (Some(data.inserted_post.id)), post_id: (Some(data.inserted_post.id)),
..Default::default() ..Default::default()
} }
.list(pool) .list(&data.site, pool)
.await?; .await?;
assert_eq!( assert_eq!(
@ -654,7 +665,7 @@ mod tests {
local_user: (Some(&data.timmy_local_user_view.local_user)), local_user: (Some(&data.timmy_local_user_view.local_user)),
..Default::default() ..Default::default()
} }
.list(pool) .list(&data.site, pool)
.await?; .await?;
assert_eq!( assert_eq!(
@ -706,7 +717,7 @@ mod tests {
liked_only: Some(true), liked_only: Some(true),
..Default::default() ..Default::default()
} }
.list(pool) .list(&data.site, pool)
.await? .await?
.into_iter() .into_iter()
.map(|c| c.comment.content) .map(|c| c.comment.content)
@ -722,7 +733,7 @@ mod tests {
disliked_only: Some(true), disliked_only: Some(true),
..Default::default() ..Default::default()
} }
.list(pool) .list(&data.site, pool)
.await?; .await?;
assert!(read_disliked_comment_views.is_empty()); assert!(read_disliked_comment_views.is_empty());
@ -743,7 +754,7 @@ mod tests {
parent_path: (Some(top_path)), parent_path: (Some(top_path)),
..Default::default() ..Default::default()
} }
.list(pool) .list(&data.site, pool)
.await?; .await?;
let child_path = data.inserted_comment_1.path.clone(); let child_path = data.inserted_comment_1.path.clone();
@ -752,7 +763,7 @@ mod tests {
parent_path: (Some(child_path)), parent_path: (Some(child_path)),
..Default::default() ..Default::default()
} }
.list(pool) .list(&data.site, pool)
.await?; .await?;
// Make sure the comment parent-limited fetch is correct // Make sure the comment parent-limited fetch is correct
@ -772,7 +783,7 @@ mod tests {
max_depth: (Some(1)), max_depth: (Some(1)),
..Default::default() ..Default::default()
} }
.list(pool) .list(&data.site, pool)
.await?; .await?;
// Make sure a depth limited one only has the top comment // Make sure a depth limited one only has the top comment
@ -790,7 +801,7 @@ mod tests {
sort: (Some(CommentSortType::New)), sort: (Some(CommentSortType::New)),
..Default::default() ..Default::default()
} }
.list(pool) .list(&data.site, pool)
.await?; .await?;
// Make sure a depth limited one, and given child comment 1, has 3 // Make sure a depth limited one, and given child comment 1, has 3
@ -816,7 +827,7 @@ mod tests {
local_user: (Some(&data.timmy_local_user_view.local_user)), local_user: (Some(&data.timmy_local_user_view.local_user)),
..Default::default() ..Default::default()
} }
.list(pool) .list(&data.site, pool)
.await?; .await?;
assert_length!(5, all_languages); assert_length!(5, all_languages);
@ -834,7 +845,7 @@ mod tests {
local_user: (Some(&data.timmy_local_user_view.local_user)), local_user: (Some(&data.timmy_local_user_view.local_user)),
..Default::default() ..Default::default()
} }
.list(pool) .list(&data.site, pool)
.await?; .await?;
assert_length!(2, finnish_comments); assert_length!(2, finnish_comments);
let finnish_comment = finnish_comments let finnish_comment = finnish_comments
@ -857,7 +868,7 @@ mod tests {
local_user: (Some(&data.timmy_local_user_view.local_user)), local_user: (Some(&data.timmy_local_user_view.local_user)),
..Default::default() ..Default::default()
} }
.list(pool) .list(&data.site, pool)
.await?; .await?;
assert_length!(1, undetermined_comment); assert_length!(1, undetermined_comment);
@ -881,7 +892,7 @@ mod tests {
post_id: Some(data.inserted_comment_2.post_id), post_id: Some(data.inserted_comment_2.post_id),
..Default::default() ..Default::default()
} }
.list(pool) .list(&data.site, pool)
.await?; .await?;
assert_eq!(comments[0].comment.id, data.inserted_comment_2.id); assert_eq!(comments[0].comment.id, data.inserted_comment_2.id);
assert!(comments[0].comment.distinguished); assert!(comments[0].comment.distinguished);
@ -910,7 +921,7 @@ mod tests {
sort: (Some(CommentSortType::Old)), sort: (Some(CommentSortType::Old)),
..Default::default() ..Default::default()
} }
.list(pool) .list(&data.site, pool)
.await?; .await?;
assert_eq!(comments[1].creator.name, "sara"); assert_eq!(comments[1].creator.name, "sara");
@ -931,7 +942,7 @@ mod tests {
sort: (Some(CommentSortType::Old)), sort: (Some(CommentSortType::Old)),
..Default::default() ..Default::default()
} }
.list(pool) .list(&data.site, pool)
.await?; .await?;
// Timmy is an admin, and make sure that field is true // Timmy is an admin, and make sure that field is true
@ -971,7 +982,7 @@ mod tests {
saved_only: Some(true), saved_only: Some(true),
..Default::default() ..Default::default()
} }
.list(pool) .list(&data.site, pool)
.await?; .await?;
// There should only be two comments // There should only be two comments
@ -1001,6 +1012,7 @@ mod tests {
LocalUser::delete(pool, data.timmy_local_user_view.local_user.id).await?; LocalUser::delete(pool, data.timmy_local_user_view.local_user.id).await?;
Person::delete(pool, data.inserted_sara_person.id).await?; Person::delete(pool, data.inserted_sara_person.id).await?;
Instance::delete(pool, data.inserted_instance.id).await?; Instance::delete(pool, data.inserted_instance.id).await?;
Site::delete(pool, data.site.id).await?;
Ok(()) Ok(())
} }
@ -1078,6 +1090,7 @@ mod tests {
featured_community: false, featured_community: false,
featured_local: false, featured_local: false,
url_content_type: None, url_content_type: None,
scheduled_publish_time: None,
}, },
community: Community { community: Community {
id: data.inserted_community.id, id: data.inserted_community.id,
@ -1139,7 +1152,7 @@ mod tests {
let unauthenticated_query = CommentQuery { let unauthenticated_query = CommentQuery {
..Default::default() ..Default::default()
} }
.list(pool) .list(&data.site, pool)
.await?; .await?;
assert_eq!(0, unauthenticated_query.len()); assert_eq!(0, unauthenticated_query.len());
@ -1147,7 +1160,7 @@ mod tests {
local_user: Some(&data.timmy_local_user_view.local_user), local_user: Some(&data.timmy_local_user_view.local_user),
..Default::default() ..Default::default()
} }
.list(pool) .list(&data.site, pool)
.await?; .await?;
assert_eq!(5, authenticated_query.len()); assert_eq!(5, authenticated_query.len());
@ -1225,4 +1238,33 @@ mod tests {
cleanup(data, pool).await cleanup(data, pool).await
} }
#[tokio::test]
#[serial]
async fn comment_listings_hide_nsfw() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let data = init_data(pool).await?;
// Mark a post as nsfw
let update_form = PostUpdateForm {
nsfw: Some(true),
..Default::default()
};
Post::update(pool, data.inserted_post.id, &update_form).await?;
// Make sure comments of this post are not returned
let comments = CommentQuery::default().list(&data.site, pool).await?;
assert_eq!(0, comments.len());
// Mark site as nsfw
let mut site = data.site.clone();
site.content_warning = Some("nsfw".to_string());
// Now comments of nsfw post are returned
let comments = CommentQuery::default().list(&site, pool).await?;
assert_eq!(6, comments.len());
cleanup(data, pool).await
}
} }

View File

@ -284,8 +284,8 @@ impl PostReportQuery {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)] #[expect(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{ use crate::{

View File

@ -318,11 +318,18 @@ fn queries<'a>() -> Queries<
// hide posts from deleted communities // hide posts from deleted communities
query = query.filter(community::deleted.eq(false)); query = query.filter(community::deleted.eq(false));
// only show deleted posts to creator // only creator can see deleted posts and unpublished scheduled posts
if let Some(person_id) = options.local_user.person_id() { if let Some(person_id) = options.local_user.person_id() {
query = query.filter(post::deleted.eq(false).or(post::creator_id.eq(person_id))); query = query.filter(post::deleted.eq(false).or(post::creator_id.eq(person_id)));
query = query.filter(
post::scheduled_publish_time
.is_null()
.or(post::creator_id.eq(person_id)),
);
} else { } else {
query = query.filter(post::deleted.eq(false)); query = query
.filter(post::deleted.eq(false))
.filter(post::scheduled_publish_time.is_null());
} }
// only show removed posts to admin when viewing user profile // only show removed posts to admin when viewing user profile
@ -387,14 +394,12 @@ fn queries<'a>() -> Queries<
query = query.filter(post::url.eq(search_term)); query = query.filter(post::url.eq(search_term));
} else { } else {
let searcher = fuzzy_search(search_term); let searcher = fuzzy_search(search_term);
let name_filter = post::name.ilike(searcher.clone());
let body_filter = post::body.ilike(searcher.clone());
query = if options.title_only.unwrap_or_default() { query = if options.title_only.unwrap_or_default() {
query.filter(post::name.ilike(searcher)) query.filter(name_filter)
} else { } else {
query.filter( query.filter(name_filter.or(body_filter))
post::name
.ilike(searcher.clone())
.or(post::body.ilike(searcher)),
)
} }
.filter(not(post::removed.or(post::deleted))); .filter(not(post::removed.or(post::deleted)));
} }
@ -734,7 +739,7 @@ impl<'a> PostQuery<'a> {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
mod tests { mod tests {
use crate::{ use crate::{
post_view::{PaginationCursorData, PostQuery, PostView}, post_view::{PaginationCursorData, PostQuery, PostView},
@ -1771,6 +1776,7 @@ mod tests {
featured_community: false, featured_community: false,
featured_local: false, featured_local: false,
url_content_type: None, url_content_type: None,
scheduled_publish_time: None,
}, },
my_vote: None, my_vote: None,
unread_comments: 0, unread_comments: 0,

View File

@ -111,8 +111,8 @@ impl PrivateMessageReportQuery {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)] #[expect(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::private_message_report_view::PrivateMessageReportQuery; use crate::private_message_report_view::PrivateMessageReportQuery;

View File

@ -173,8 +173,8 @@ impl PrivateMessageQuery {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)] #[expect(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{private_message_view::PrivateMessageQuery, structs::PrivateMessageView}; use crate::{private_message_view::PrivateMessageQuery, structs::PrivateMessageView};

View File

@ -135,8 +135,7 @@ impl RegistrationApplicationQuery {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::registration_application_view::{ use crate::registration_application_view::{
@ -235,7 +234,6 @@ mod tests {
person_id: inserted_sara_local_user.person_id, person_id: inserted_sara_local_user.person_id,
email: inserted_sara_local_user.email, email: inserted_sara_local_user.email,
show_nsfw: inserted_sara_local_user.show_nsfw, show_nsfw: inserted_sara_local_user.show_nsfw,
auto_expand: inserted_sara_local_user.auto_expand,
blur_nsfw: inserted_sara_local_user.blur_nsfw, blur_nsfw: inserted_sara_local_user.blur_nsfw,
theme: inserted_sara_local_user.theme, theme: inserted_sara_local_user.theme,
default_post_sort_type: inserted_sara_local_user.default_post_sort_type, default_post_sort_type: inserted_sara_local_user.default_post_sort_type,

View File

@ -83,8 +83,7 @@ impl VoteView {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::structs::VoteView; use crate::structs::VoteView;

View File

@ -15,7 +15,13 @@ doctest = false
workspace = true workspace = true
[features] [features]
full = ["lemmy_db_schema/full", "diesel", "diesel-async", "ts-rs"] full = [
"lemmy_db_schema/full",
"lemmy_utils/full",
"diesel",
"diesel-async",
"ts-rs",
]
[dependencies] [dependencies]
lemmy_db_schema = { workspace = true } lemmy_db_schema = { workspace = true }
@ -33,6 +39,7 @@ serde_with = { workspace = true }
ts-rs = { workspace = true, optional = true } ts-rs = { workspace = true, optional = true }
chrono.workspace = true chrono.workspace = true
strum = { workspace = true } strum = { workspace = true }
lemmy_utils = { workspace = true, optional = true }
[dev-dependencies] [dev-dependencies]
serial_test = { workspace = true } serial_test = { workspace = true }

View File

@ -303,7 +303,6 @@ impl CommentReplyQuery {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{comment_reply_view::CommentReplyQuery, structs::CommentReplyView}; use crate::{comment_reply_view::CommentReplyQuery, structs::CommentReplyView};

View File

@ -8,13 +8,14 @@ use lemmy_db_schema::{
source::local_user::LocalUser, source::local_user::LocalUser,
utils::{get_conn, DbPool}, utils::{get_conn, DbPool},
}; };
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
impl CommunityModeratorView { impl CommunityModeratorView {
pub async fn is_community_moderator( pub async fn check_is_community_moderator(
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
find_community_id: CommunityId, find_community_id: CommunityId,
find_person_id: PersonId, find_person_id: PersonId,
) -> Result<bool, Error> { ) -> LemmyResult<()> {
use lemmy_db_schema::schema::community_moderator::dsl::{ use lemmy_db_schema::schema::community_moderator::dsl::{
community_id, community_id,
community_moderator, community_moderator,
@ -27,20 +28,24 @@ impl CommunityModeratorView {
.filter(person_id.eq(find_person_id)), .filter(person_id.eq(find_person_id)),
)) ))
.get_result::<bool>(conn) .get_result::<bool>(conn)
.await .await?
.then_some(())
.ok_or(LemmyErrorType::NotAModerator.into())
} }
pub(crate) async fn is_community_moderator_of_any( pub(crate) async fn is_community_moderator_of_any(
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
find_person_id: PersonId, find_person_id: PersonId,
) -> Result<bool, Error> { ) -> LemmyResult<()> {
use lemmy_db_schema::schema::community_moderator::dsl::{community_moderator, person_id}; use lemmy_db_schema::schema::community_moderator::dsl::{community_moderator, person_id};
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
select(exists( select(exists(
community_moderator.filter(person_id.eq(find_person_id)), community_moderator.filter(person_id.eq(find_person_id)),
)) ))
.get_result::<bool>(conn) .get_result::<bool>(conn)
.await .await?
.then_some(())
.ok_or(LemmyErrorType::NotAModerator.into())
} }
pub async fn for_community( pub async fn for_community(

View File

@ -1,25 +1,33 @@
use crate::structs::CommunityPersonBanView; use crate::structs::CommunityPersonBanView;
use diesel::{dsl::exists, result::Error, select, ExpressionMethods, QueryDsl}; use diesel::{
dsl::{exists, not},
select,
ExpressionMethods,
QueryDsl,
};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::{CommunityId, PersonId}, newtypes::{CommunityId, PersonId},
schema::community_person_ban, schema::community_person_ban,
utils::{get_conn, DbPool}, utils::{get_conn, DbPool},
}; };
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
impl CommunityPersonBanView { impl CommunityPersonBanView {
pub async fn get( pub async fn check(
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
from_person_id: PersonId, from_person_id: PersonId,
from_community_id: CommunityId, from_community_id: CommunityId,
) -> Result<bool, Error> { ) -> LemmyResult<()> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
select(exists( select(not(exists(
community_person_ban::table community_person_ban::table
.filter(community_person_ban::community_id.eq(from_community_id)) .filter(community_person_ban::community_id.eq(from_community_id))
.filter(community_person_ban::person_id.eq(from_person_id)), .filter(community_person_ban::person_id.eq(from_person_id)),
)) )))
.get_result::<bool>(conn) .get_result::<bool>(conn)
.await .await?
.then_some(())
.ok_or(LemmyErrorType::PersonIsBannedFromCommunity.into())
} }
} }

View File

@ -26,6 +26,7 @@ use lemmy_db_schema::{
ListingType, ListingType,
PostSortType, PostSortType,
}; };
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
fn queries<'a>() -> Queries< fn queries<'a>() -> Queries<
impl ReadFn<'a, CommunityView, (CommunityId, Option<&'a LocalUser>, bool)>, impl ReadFn<'a, CommunityView, (CommunityId, Option<&'a LocalUser>, bool)>,
@ -111,9 +112,14 @@ fn queries<'a>() -> Queries<
if let Some(search_term) = options.search_term { if let Some(search_term) = options.search_term {
let searcher = fuzzy_search(&search_term); let searcher = fuzzy_search(&search_term);
query = query let name_filter = community::name.ilike(searcher.clone());
.filter(community::name.ilike(searcher.clone())) let title_filter = community::title.ilike(searcher.clone());
.or_filter(community::title.ilike(searcher)) let description_filter = community::description.ilike(searcher.clone());
query = if options.title_only.unwrap_or_default() {
query.filter(name_filter.or(title_filter))
} else {
query.filter(name_filter.or(title_filter.or(description_filter)))
}
} }
// Hide deleted and removed for non-admins or mods // Hide deleted and removed for non-admins or mods
@ -185,35 +191,39 @@ impl CommunityView {
.await .await
} }
pub async fn is_mod_or_admin( pub async fn check_is_mod_or_admin(
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
person_id: PersonId, person_id: PersonId,
community_id: CommunityId, community_id: CommunityId,
) -> Result<bool, Error> { ) -> LemmyResult<()> {
let is_mod = let is_mod =
CommunityModeratorView::is_community_moderator(pool, community_id, person_id).await?; CommunityModeratorView::check_is_community_moderator(pool, community_id, person_id).await;
if is_mod { if is_mod.is_ok()
Ok(true) || PersonView::read(pool, person_id)
} else if let Ok(person_view) = PersonView::read(pool, person_id).await { .await
Ok(person_view.is_admin) .is_ok_and(|t| t.is_admin)
{
Ok(())
} else { } else {
Ok(false) Err(LemmyErrorType::NotAModOrAdmin)?
} }
} }
/// Checks if a person is an admin, or moderator of any community. /// Checks if a person is an admin, or moderator of any community.
pub async fn is_mod_of_any_or_admin( pub async fn check_is_mod_of_any_or_admin(
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
person_id: PersonId, person_id: PersonId,
) -> Result<bool, Error> { ) -> LemmyResult<()> {
let is_mod_of_any = let is_mod_of_any =
CommunityModeratorView::is_community_moderator_of_any(pool, person_id).await?; CommunityModeratorView::is_community_moderator_of_any(pool, person_id).await;
if is_mod_of_any { if is_mod_of_any.is_ok()
Ok(true) || PersonView::read(pool, person_id)
} else if let Ok(person_view) = PersonView::read(pool, person_id).await { .await
Ok(person_view.is_admin) .is_ok_and(|t| t.is_admin)
{
Ok(())
} else { } else {
Ok(false) Err(LemmyErrorType::NotAModOrAdmin)?
} }
} }
} }
@ -224,6 +234,7 @@ pub struct CommunityQuery<'a> {
pub sort: Option<PostSortType>, pub sort: Option<PostSortType>,
pub local_user: Option<&'a LocalUser>, pub local_user: Option<&'a LocalUser>,
pub search_term: Option<String>, pub search_term: Option<String>,
pub title_only: Option<bool>,
pub is_mod_or_admin: bool, pub is_mod_or_admin: bool,
pub show_nsfw: bool, pub show_nsfw: bool,
pub page: Option<i64>, pub page: Option<i64>,
@ -237,8 +248,7 @@ impl<'a> CommunityQuery<'a> {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{community_view::CommunityQuery, structs::CommunityView}; use crate::{community_view::CommunityQuery, structs::CommunityView};

View File

@ -303,7 +303,6 @@ impl PersonMentionQuery {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{person_mention_view::PersonMentionQuery, structs::PersonMentionView}; use crate::{person_mention_view::PersonMentionQuery, structs::PersonMentionView};

View File

@ -164,7 +164,7 @@ impl PersonQuery {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::indexing_slicing)] #[expect(clippy::indexing_slicing)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -222,8 +222,8 @@ impl<T: DataSource> CommunityInboxCollector<T> {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)] #[expect(clippy::indexing_slicing)]
mod tests { mod tests {
use super::*; use super::*;
use lemmy_db_schema::{ use lemmy_db_schema::{

View File

@ -192,8 +192,8 @@ impl SendManager {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)] #[expect(clippy::indexing_slicing)]
mod test { mod test {
use super::*; use super::*;

View File

@ -439,8 +439,8 @@ impl InstanceWorker {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)] #[expect(clippy::indexing_slicing)]
mod test { mod test {
use super::*; use super::*;

View File

@ -172,6 +172,8 @@ pub enum LemmyErrorType {
Unknown(String), Unknown(String),
CantDeleteSite, CantDeleteSite,
UrlLengthOverflow, UrlLengthOverflow,
PostScheduleTimeMustBeInFuture,
TooManyScheduledPosts,
NotFound, NotFound,
} }

View File

@ -221,8 +221,6 @@ fn parse_ip(addr: &str) -> Option<IpAddr> {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#[test] #[test]

View File

@ -136,7 +136,6 @@ impl<K: Eq + Hash, C: MapLevel> MapLevel for Map<K, C> {
.entry(addr_part) .entry(addr_part)
.or_insert(RateLimitedGroup::new(now, adjusted_configs)); .or_insert(RateLimitedGroup::new(now, adjusted_configs));
#[allow(clippy::indexing_slicing)]
let total_passes = group.check_total(action_type, now, adjusted_configs[action_type]); let total_passes = group.check_total(action_type, now, adjusted_configs[action_type]);
let children_pass = group.children.check( let children_pass = group.children.check(
@ -161,7 +160,6 @@ impl<K: Eq + Hash, C: MapLevel> MapLevel for Map<K, C> {
// Evaluated if `some_children_remaining` is false // Evaluated if `some_children_remaining` is false
let total_has_refill_in_future = || { let total_has_refill_in_future = || {
group.total.into_iter().any(|(action_type, bucket)| { group.total.into_iter().any(|(action_type, bucket)| {
#[allow(clippy::indexing_slicing)]
let config = configs[action_type]; let config = configs[action_type];
bucket.update(now, config).tokens != config.capacity bucket.update(now, config).tokens != config.capacity
}) })
@ -214,7 +212,6 @@ impl<C: Default> RateLimitedGroup<C> {
now: InstantSecs, now: InstantSecs,
config: BucketConfig, config: BucketConfig,
) -> bool { ) -> bool {
#[allow(clippy::indexing_slicing)] // `EnumMap` has no `get` function
let bucket = &mut self.total[action_type]; let bucket = &mut self.total[action_type];
let new_bucket = bucket.update(now, config); let new_bucket = bucket.update(now, config);
@ -311,8 +308,7 @@ fn split_ipv6(ip: Ipv6Addr) -> ([u8; 6], u8, u8) {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use super::{ActionType, BucketConfig, InstantSecs, RateLimitState, RateLimitedGroup}; use super::{ActionType, BucketConfig, InstantSecs, RateLimitState, RateLimitedGroup};
@ -361,7 +357,6 @@ mod tests {
assert!(post_passed); assert!(post_passed);
} }
#[allow(clippy::indexing_slicing)]
let expected_buckets = |factor: u32, tokens_consumed: u32| { let expected_buckets = |factor: u32, tokens_consumed: u32| {
let adjusted_configs = bucket_configs.map(|_, config| BucketConfig { let adjusted_configs = bucket_configs.map(|_, config| BucketConfig {
capacity: config.capacity.saturating_mul(factor), capacity: config.capacity.saturating_mul(factor),

View File

@ -107,8 +107,7 @@ pub fn markdown_check_for_blocked_urls(text: &str, blocklist: &RegexSet) -> Lemm
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -134,8 +134,6 @@ pub fn add(markdown_parser: &mut MarkdownIt) {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::utils::markdown::spoiler_rule::add; use crate::utils::markdown::spoiler_rule::add;

View File

@ -34,8 +34,7 @@ pub fn scrape_text_for_mentions(text: &str) -> Vec<MentionData> {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::indexing_slicing)]
#[allow(clippy::indexing_slicing)]
mod test { mod test {
use crate::utils::mention::scrape_text_for_mentions; use crate::utils::mention::scrape_text_for_mentions;

View File

@ -61,8 +61,7 @@ pub(crate) fn slurs_vec_to_str(slurs: &[&str]) -> String {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod test { mod test {
use crate::utils::slurs::{remove_slurs, slur_check, slurs_vec_to_str}; use crate::utils::slurs::{remove_slurs, slur_check, slurs_vec_to_str};

View File

@ -351,7 +351,6 @@ pub fn build_url_str_without_scheme(url_str: &str) -> LemmyResult<String> {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::{ use crate::{

View File

@ -0,0 +1,3 @@
ALTER TABLE post
DROP COLUMN scheduled_publish_time;

View File

@ -0,0 +1,5 @@
ALTER TABLE post
ADD COLUMN scheduled_publish_time timestamptz;
CREATE INDEX idx_post_scheduled_publish_time ON post (scheduled_publish_time);

View File

@ -14,7 +14,7 @@ CREATE TABLE oauth_provider (
scopes text NOT NULL, scopes text NOT NULL,
auto_verify_email boolean DEFAULT TRUE NOT NULL, auto_verify_email boolean DEFAULT TRUE NOT NULL,
account_linking_enabled boolean DEFAULT FALSE NOT NULL, account_linking_enabled boolean DEFAULT FALSE NOT NULL,
enabled boolean DEFAULT FALSE NOT NULL, enabled boolean DEFAULT TRUE NOT NULL,
published timestamp with time zone DEFAULT now() NOT NULL, published timestamp with time zone DEFAULT now() NOT NULL,
updated timestamp with time zone updated timestamp with time zone
); );

View File

@ -0,0 +1,3 @@
ALTER TABLE local_user
ADD COLUMN auto_expand boolean NOT NULL DEFAULT FALSE;

View File

@ -0,0 +1,3 @@
ALTER TABLE local_user
DROP COLUMN auto_expand;

View File

@ -157,11 +157,6 @@ pub async fn start_lemmy_server(args: CmdArgs) -> LemmyResult<()> {
rate_limit_cell.clone(), rate_limit_cell.clone(),
); );
let scheduled_tasks = (!args.disable_scheduled_tasks).then(|| {
// Schedules various cleanup tasks for the DB
tokio::task::spawn(scheduled_tasks::setup(context.clone()))
});
if let Some(prometheus) = SETTINGS.prometheus.clone() { if let Some(prometheus) = SETTINGS.prometheus.clone() {
serve_prometheus(prometheus, context.clone())?; serve_prometheus(prometheus, context.clone())?;
} }
@ -187,7 +182,14 @@ pub async fn start_lemmy_server(args: CmdArgs) -> LemmyResult<()> {
})) }))
.expect("set function pointer"); .expect("set function pointer");
let request_data = federation_config.to_request_data(); let request_data = federation_config.to_request_data();
let outgoing_activities_task = tokio::task::spawn(handle_outgoing_activities(request_data)); let outgoing_activities_task = tokio::task::spawn(handle_outgoing_activities(
request_data.reset_request_count(),
));
let scheduled_tasks = (!args.disable_scheduled_tasks).then(|| {
// Schedules various cleanup tasks for the DB
tokio::task::spawn(scheduled_tasks::setup(request_data.reset_request_count()))
});
let server = if !args.disable_http_server { let server = if !args.disable_http_server {
if let Some(startup_server_handle) = startup_server_handle { if let Some(startup_server_handle) = startup_server_handle {

Some files were not shown because too many files have changed in this diff Show More