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

This commit is contained in:
Dessalines 2024-09-24 14:07:29 -04:00
commit f5106960f6
103 changed files with 619 additions and 493 deletions

1
Cargo.lock generated
View File

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

View File

@ -21,16 +21,16 @@
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "^22.0.2",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"eslint": "^9.8.0",
"@types/node": "^22.3.0",
"@typescript-eslint/eslint-plugin": "^8.1.0",
"@typescript-eslint/parser": "^8.1.0",
"eslint": "^9.9.0",
"eslint-plugin-prettier": "^5.1.3",
"jest": "^29.5.0",
"lemmy-js-client": "0.20.0-alpha.11",
"prettier": "^3.2.5",
"ts-jest": "^29.1.0",
"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
version: 29.5.12
'@types/node':
specifier: ^22.0.2
specifier: ^22.3.0
version: 22.3.0
'@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)
'@typescript-eslint/parser':
specifier: ^8.0.0
specifier: ^8.1.0
version: 8.1.0(eslint@9.9.0)(typescript@5.5.4)
eslint:
specifier: ^9.8.0
specifier: ^9.9.0
version: 9.9.0
eslint-plugin-prettier:
specifier: ^5.1.3
@ -42,7 +42,7 @@ importers:
specifier: ^5.5.4
version: 5.5.4
typescript-eslint:
specifier: ^8.0.0
specifier: ^8.1.0
version: 8.1.0(eslint@9.9.0)(typescript@5.5.4)
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
await expect(
createPost(alpha, betaCommunity.community.id),
).rejects.toStrictEqual(Error("banned_from_community"));
).rejects.toStrictEqual(Error("person_is_banned_from_community"));
// Unban alpha
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
// by the community's home instance.
if local_user_view.local_user.admin && !community.local {
let is_mod = CommunityModeratorView::is_community_moderator(
CommunityModeratorView::check_is_community_moderator(
&mut context.pool(),
community.id,
local_user_view.person.id,
)
.await?;
if !is_mod {
Err(LemmyErrorType::NotAModerator)?
}
}
// Update in local database

View File

@ -12,7 +12,7 @@ use lemmy_db_schema::source::{
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::CommunityView;
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
pub async fn get_random_community(
@ -27,7 +27,6 @@ pub async fn get_random_community(
let random_community_id = Community::get_random_local_community(&mut context.pool())
.await?
.ok_or(LemmyErrorType::NotFound)?
.id;
let is_mod_or_admin = is_mod_or_admin_opt(

View File

@ -265,8 +265,6 @@ pub async fn local_user_view_from_jwt(
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
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();
// if email was changed, check that it is not taken and send verification mail
if previous_email.deref() != email {
if LocalUser::is_email_taken(&mut context.pool(), email).await? {
return Err(LemmyErrorType::EmailAlreadyExists)?;
}
LocalUser::check_is_email_taken(&mut context.pool(), email).await?;
send_verification_email(
&local_user_view,
email,
@ -132,7 +130,6 @@ pub async fn save_user_settings(
send_notifications_to_email: data.send_notifications_to_email,
show_nsfw: data.show_nsfw,
blur_nsfw: data.blur_nsfw,
auto_expand: data.auto_expand,
show_bot_accounts: data.show_bot_accounts,
default_post_sort_type,
default_comment_sort_type,

View File

@ -63,7 +63,7 @@ pub async fn leave_admin(
let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
let oauth_providers = OAuthProvider::get_all_public(&mut context.pool()).await?;
let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?;
let tagline = Tagline::get_random(&mut context.pool()).await?;
let tagline = Tagline::get_random(&mut context.pool()).await.ok();
Ok(Json(GetSiteResponse {
site_view,

View File

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

View File

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

View File

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

View File

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

View File

@ -30,6 +30,8 @@ pub struct CreatePost {
pub language_id: Option<LanguageId>,
/// Instead of fetching a thumbnail, use a custom one.
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)]
@ -124,6 +126,8 @@ pub struct EditPost {
pub language_id: Option<LanguageId>,
/// Instead of fetching a thumbnail, use a custom one.
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)]

View File

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

View File

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

View File

@ -73,13 +73,7 @@ pub async fn is_mod_or_admin(
community_id: CommunityId,
) -> LemmyResult<()> {
check_user_valid(person)?;
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(())
}
CommunityView::check_is_mod_or_admin(pool, person.id, community_id).await
}
#[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;
check_user_valid(person)?;
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(())
}
CommunityView::check_is_mod_of_any_or_admin(pool, person.id).await
}
pub fn is_admin(local_user_view: &LocalUserView) -> LemmyResult<()> {
@ -242,7 +230,7 @@ pub async fn check_community_user_action(
) -> LemmyResult<()> {
check_user_valid(person)?;
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(())
}
@ -257,19 +245,6 @@ async fn check_community_deleted_removed(
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.
///
/// 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<'_>,
) -> LemmyResult<()> {
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
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)]
pub async fn check_person_instance_community_block(
my_id: PersonId,
@ -360,9 +290,9 @@ pub async fn check_person_instance_community_block(
community_id: CommunityId,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
check_person_block(my_id, potential_blocker_id, pool).await?;
check_instance_block(community_instance_id, potential_blocker_id, pool).await?;
check_community_block(community_id, potential_blocker_id, pool).await?;
PersonBlock::read(pool, potential_blocker_id, my_id).await?;
InstanceBlock::read(pool, potential_blocker_id, community_instance_id).await?;
CommunityBlock::read(pool, potential_blocker_id, community_id).await?;
Ok(())
}
@ -846,12 +776,13 @@ pub async fn remove_or_restore_user_data_in_community(
// Comments
// 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 {
creator_id: Some(banned_person_id),
community_id: Some(community_id),
..Default::default()
}
.list(pool)
.list(&site, pool)
.await?;
for comment_view in &comments {
@ -1136,8 +1067,7 @@ fn build_proxied_image_url(
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
#[expect(clippy::unwrap_used)]
mod tests {
use super::*;

View File

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

View File

@ -1,3 +1,4 @@
use super::convert_published_time;
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{
@ -95,15 +96,12 @@ pub async fn create_post(
let community = Community::read(&mut context.pool(), community_id).await?;
if community.posting_restricted_to_mods {
let community_id = data.community_id;
let is_mod = CommunityModeratorView::is_community_moderator(
CommunityModeratorView::check_is_community_moderator(
&mut context.pool(),
community_id,
local_user_view.local_user.person_id,
)
.await?;
if !is_mod {
Err(LemmyErrorType::OnlyModsCanPostInCommunity)?
}
}
// 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 {
url: url.map(Into::into),
body,
alt_text: data.alt_text.clone(),
nsfw: data.nsfw,
language_id,
scheduled_publish_time,
..PostInsertForm::new(
data.name.trim().to_string(),
local_user_view.person.id,
@ -145,10 +146,16 @@ pub async fn create_post(
.await
.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(
inserted_post.clone(),
custom_thumbnail.map(Into::into),
|post| Some(SendActivityData::CreatePost(post)),
federate_post,
context.reset_request_count(),
)
.await?;
@ -168,11 +175,14 @@ pub async fn create_post(
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 {
spawn_try_task(async move {
let mut webmention =
Webmention::new::<Url>(inserted_post.ap_id.clone().into(), url.clone().into())?;
let mut webmention = Webmention::new::<Url>(post.ap_id.clone().into(), url.clone().into())?;
webmention.set_checked(true);
match webmention
.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 delete;
pub mod read;
pub mod remove;
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 actix_web::web::Json;
use lemmy_api_common::{
@ -16,6 +17,7 @@ use lemmy_api_common::{
use lemmy_db_schema::{
source::{
actor_language::CommunityLanguage,
community::Community,
local_site::LocalSite,
post::{Post, PostUpdateForm},
},
@ -107,6 +109,21 @@ pub async fn update_post(
)
.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 {
name: data.name.clone(),
url,
@ -115,6 +132,7 @@ pub async fn update_post(
nsfw: data.nsfw,
language_id: data.language_id,
updated: Some(Some(naive_now())),
scheduled_publish_time,
..Default::default()
};
@ -123,13 +141,36 @@ pub async fn update_post(
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePost)?;
generate_post_link_metadata(
updated_post.clone(),
custom_thumbnail.flatten().map(Into::into),
|post| Some(SendActivityData::UpdatePost(post)),
context.reset_request_count(),
)
.await?;
// 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(
updated_post.clone(),
custom_thumbnail.flatten().map(Into::into),
|post| Some(SendActivityData::UpdatePost(post)),
context.reset_request_count(),
)
.await?
}
// schedule was changed, do nothing
(Some(_), Some(_)) => {}
};
build_post_response(
context.deref(),

View File

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

View File

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

View File

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

View File

@ -43,7 +43,7 @@ pub async fn get_site(
let all_languages = Language::read_all(&mut context.pool()).await?;
let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?;
let tagline = Tagline::get_random(&mut context.pool()).await?;
let tagline = Tagline::get_random(&mut context.pool()).await.ok();
let admin_oauth_providers = OAuthProvider::get_all(&mut context.pool()).await?;
let oauth_providers =
OAuthProvider::convert_providers_to_public(admin_oauth_providers.clone());

View File

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

View File

@ -92,36 +92,25 @@ pub async fn register(
}
if local_site.site_setup && local_site.captcha_enabled {
if let Some(captcha_uuid) = &data.captcha_uuid {
let uuid = uuid::Uuid::parse_str(captcha_uuid)?;
let check = CaptchaAnswer::check_captcha(
&mut context.pool(),
CheckCaptchaAnswer {
uuid,
answer: data.captcha_answer.clone().unwrap_or_default(),
},
)
.await?;
if !check {
Err(LemmyErrorType::CaptchaIncorrect)?
}
} else {
Err(LemmyErrorType::CaptchaIncorrect)?
}
let uuid = uuid::Uuid::parse_str(&data.captcha_uuid.clone().unwrap_or_default())?;
CaptchaAnswer::check_captcha(
&mut context.pool(),
CheckCaptchaAnswer {
uuid,
answer: data.captcha_answer.clone().unwrap_or_default(),
},
)
.await?;
}
let slur_regex = local_site_to_slur_regex(&local_site);
check_slurs(&data.username, &slur_regex)?;
check_slurs_opt(&data.answer, &slur_regex)?;
if Person::is_username_taken(&mut context.pool(), &data.username).await? {
return Err(LemmyErrorType::UsernameAlreadyExists)?;
}
Person::check_username_taken(&mut context.pool(), &data.username).await?;
if let Some(email) = &data.email {
if LocalUser::is_email_taken(&mut context.pool(), email).await? {
Err(LemmyErrorType::EmailAlreadyExists)?
}
LocalUser::check_is_email_taken(&mut context.pool(), email).await?;
}
// 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_opt(&data.answer, &slur_regex)?;
if Person::is_username_taken(&mut context.pool(), username).await? {
return Err(LemmyErrorType::UsernameAlreadyExists)?;
}
Person::check_username_taken(&mut context.pool(), username).await?;
// We have to create a person, a local_user, and an oauth_account
person = create_person(

View File

@ -213,15 +213,13 @@ async fn can_accept_activity_in_community(
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
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
if community.visibility != CommunityVisibility::Public {
return Err(LemmyErrorType::NotFound.into());
}
if !community.local {
CommunityFollower::check_has_local_followers(&mut context.pool(), community.id).await?
}
}
Ok(())
}

View File

@ -87,12 +87,7 @@ pub(crate) async fn verify_person_in_community(
}
let person_id = person.id;
let community_id = community.id;
let is_banned = CommunityPersonBanView::get(&mut context.pool(), person_id, community_id).await?;
if is_banned {
Err(LemmyErrorType::PersonIsBannedFromCommunity)?
} else {
Ok(())
}
CommunityPersonBanView::check(&mut context.pool(), person_id, community_id).await
}
/// Verify that mod action in community was performed by a moderator.
@ -106,14 +101,6 @@ pub(crate) async fn verify_mod_action(
community: &Community,
context: &Data<LemmyContext>,
) -> 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
// by an instance admin.
// TODO: federate instance admin status and check it here
@ -121,7 +108,8 @@ pub(crate) async fn verify_mod_action(
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<()> {

View File

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

View File

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

View File

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

View File

@ -47,7 +47,7 @@ pub async fn search(
listing_type,
page,
limit,
post_title_only,
title_only,
post_url_only,
saved_only,
liked_only,
@ -78,7 +78,7 @@ pub async fn search(
search_term: Some(q.clone()),
page,
limit,
title_only: post_title_only,
title_only,
url_only: post_url_only,
liked_only,
disliked_only,
@ -105,6 +105,7 @@ pub async fn search(
sort,
listing_type,
search_term: Some(q.clone()),
title_only,
local_user,
is_mod_or_admin: is_admin,
page,
@ -127,7 +128,9 @@ pub async fn search(
.await?;
}
SearchType::Comments => {
comments = comment_query.list(&mut context.pool()).await?;
comments = comment_query
.list(&local_site.site, &mut context.pool())
.await?;
}
SearchType::Communities => {
communities = community_query
@ -146,7 +149,9 @@ pub async fn search(
.list(&local_site.site, &mut context.pool())
.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 {
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),
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),
auto_expand: data.settings.as_ref().map(|s| s.auto_expand),
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),
..Default::default()
@ -308,8 +307,9 @@ where
});
Ok(failed_items.into_iter().join(","))
}
#[cfg(test)]
#[allow(clippy::indexing_slicing)]
#[expect(clippy::indexing_slicing)]
mod tests {
use crate::api::user_settings_backup::{export_settings, import_settings, UserSettingsBackup};

View File

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

View File

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

View File

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

View File

@ -15,12 +15,13 @@ use activitypub_federation::{
use chrono::{DateTime, Utc};
use lemmy_api_common::{
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::{
source::{
local_site::LocalSite,
person::Person,
person_block::PersonBlock,
private_message::{PrivateMessage, PrivateMessageInsertForm},
},
traits::Crud,
@ -126,7 +127,7 @@ impl Object for ApubPrivateMessage {
) -> LemmyResult<ApubPrivateMessage> {
let creator = note.attributed_to.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 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;
}
#[allow(non_camel_case_types)]
#[expect(non_camel_case_types)]
#[derive(QueryId, Clone, Copy, Debug)]
pub struct current_value;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -194,7 +194,7 @@ impl Community {
Ok(())
}
pub async fn get_random_local_community(pool: &mut DbPool<'_>) -> Result<Option<Self>, Error> {
pub async fn get_random_local_community(pool: &mut DbPool<'_>) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
sql_function!(fn random() -> Text);
community::table
@ -205,7 +205,6 @@ impl Community {
.limit(1)
.first::<Self>(conn)
.await
.optional()
}
}
@ -333,16 +332,18 @@ impl CommunityFollower {
/// 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.
pub async fn has_local_followers(
pub async fn check_has_local_followers(
pool: &mut DbPool<'_>,
remote_community_id: CommunityId,
) -> Result<bool, Error> {
) -> LemmyResult<()> {
let conn = &mut get_conn(pool).await?;
select(exists(community_follower::table.filter(
community_follower::community_id.eq(remote_community_id),
)))
.get_result(conn)
.await
.get_result::<bool>(conn)
.await?
.then_some(())
.ok_or(LemmyErrorType::CommunityHasNoFollowers.into())
}
}
@ -443,7 +444,6 @@ impl ApubActor for Community {
}
#[cfg(test)]
#[allow(clippy::indexing_slicing)]
mod tests {
use crate::{
source::{

View File

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

View File

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

View File

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

View File

@ -41,8 +41,8 @@ impl Language {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
#[expect(clippy::unwrap_used)]
#[expect(clippy::indexing_slicing)]
mod 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
}
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};
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()),
)))
.get_result(conn)
.await
))))
.get_result::<bool>(conn)
.await?
.then_some(())
.ok_or(LemmyErrorType::EmailAlreadyExists.into())
}
// TODO: maybe move this and pass in LocalUserView
@ -367,7 +369,6 @@ pub struct UserBackupLists {
}
#[cfg(test)]
#[allow(clippy::indexing_slicing)]
mod tests {
use crate::{
source::{
@ -419,4 +420,32 @@ mod tests {
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_async::RunQueryDsl;
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
impl LoginToken {
pub async fn create(pool: &mut DbPool<'_>, form: LoginTokenCreateForm) -> Result<Self, Error> {
@ -22,13 +23,15 @@ impl LoginToken {
pool: &mut DbPool<'_>,
user_id_: LocalUserId,
token_: &str,
) -> Result<bool, Error> {
) -> LemmyResult<()> {
let conn = &mut get_conn(pool).await?;
select(exists(
login_token.find(token_).filter(user_id.eq(user_id_)),
))
.get_result(conn)
.await
.get_result::<bool>(conn)
.await?
.then_some(())
.ok_or(LemmyErrorType::NotLoggedIn.into())
}
pub async fn list(

View File

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

View File

@ -1,32 +1,13 @@
use crate::{
newtypes::{LocalUserId, OAuthProviderId},
newtypes::LocalUserId,
schema::{oauth_account, oauth_account::dsl::local_user_id},
source::oauth_account::{OAuthAccount, OAuthAccountInsertForm},
utils::{get_conn, DbPool},
};
use diesel::{
dsl::{exists, insert_into},
result::Error,
select,
ExpressionMethods,
QueryDsl,
};
use diesel::{insert_into, result::Error, ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
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> {
let conn = &mut get_conn(pool).await?;
insert_into(oauth_account::table)
@ -35,17 +16,6 @@ impl OAuthAccount {
.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(
pool: &mut DbPool<'_>,
for_local_user_id: LocalUserId,

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
use crate::{
diesel::OptionalExtension,
diesel::{BoolExpressionMethods, OptionalExtension},
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::{
Post,
PostHide,
@ -20,6 +20,7 @@ use crate::{
functions::coalesce,
get_conn,
naive_now,
now,
DbPool,
DELETED_REPLACEMENT_TEXT,
FETCH_LIMIT_MAX,
@ -30,7 +31,7 @@ use crate::{
use ::url::Url;
use chrono::{DateTime, Utc};
use diesel::{
dsl::insert_into,
dsl::{count, insert_into, not},
result::Error,
DecoratableTarget,
ExpressionMethods,
@ -173,6 +174,7 @@ impl Post {
let object_id: DbUrl = object_id.into();
post::table
.filter(post::ap_id.eq(object_id))
.filter(post::scheduled_publish_time.is_null())
.first(conn)
.await
.optional()
@ -246,6 +248,28 @@ impl Post {
.get_results::<Self>(conn)
.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]
@ -368,8 +392,7 @@ impl PostHide {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
#[expect(clippy::unwrap_used)]
mod tests {
use crate::{
@ -459,6 +482,7 @@ mod tests {
featured_community: false,
featured_local: false,
url_content_type: None,
scheduled_publish_time: None,
};
// Post Like

View File

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

View File

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

View File

@ -5,7 +5,7 @@ use crate::{
traits::Crud,
utils::{get_conn, limit_and_offset, DbPool},
};
use diesel::{insert_into, result::Error, ExpressionMethods, OptionalExtension, QueryDsl};
use diesel::{insert_into, result::Error, ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
#[async_trait]
@ -51,14 +51,9 @@ impl Tagline {
.await
}
pub async fn get_random(pool: &mut DbPool<'_>) -> Result<Option<Self>, Error> {
pub async fn get_random(pool: &mut DbPool<'_>) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
sql_function!(fn random() -> Text);
tagline
.order(random())
.limit(1)
.first::<Self>(conn)
.await
.optional()
tagline.order(random()).limit(1).first::<Self>(conn).await
}
}

View File

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

View File

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

View File

@ -459,7 +459,6 @@ diesel::table! {
totp_2fa_secret -> Nullable<Text>,
open_links_in_new_tab -> Bool,
blur_nsfw -> Bool,
auto_expand -> Bool,
infinite_scroll_enabled -> Bool,
admin -> Bool,
post_listing_mode -> PostListingModeEnum,
@ -770,6 +769,7 @@ diesel::table! {
featured_local -> Bool,
url_content_type -> 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.
pub open_links_in_new_tab: bool,
pub blur_nsfw: bool,
pub auto_expand: bool,
/// Whether infinite scroll is enabled.
pub infinite_scroll_enabled: bool,
/// Whether the person is an admin.
@ -104,8 +103,6 @@ pub struct LocalUserInsertForm {
#[new(default)]
pub blur_nsfw: Option<bool>,
#[new(default)]
pub auto_expand: Option<bool>,
#[new(default)]
pub infinite_scroll_enabled: Option<bool>,
#[new(default)]
pub admin: Option<bool>,
@ -143,7 +140,6 @@ pub struct LocalUserUpdateForm {
pub totp_2fa_secret: Option<Option<String>>,
pub open_links_in_new_tab: Option<bool>,
pub blur_nsfw: Option<bool>,
pub auto_expand: Option<bool>,
pub infinite_scroll_enabled: Option<bool>,
pub admin: Option<bool>,
pub post_listing_mode: Option<PostListingMode>,

View File

@ -57,6 +57,8 @@ pub struct Post {
pub url_content_type: Option<String>,
/// An optional alt_text, usable for image posts.
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)]
@ -104,6 +106,8 @@ pub struct PostInsertForm {
pub url_content_type: Option<String>,
#[new(default)]
pub alt_text: Option<String>,
#[new(default)]
pub scheduled_publish_time: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Default)]
@ -130,6 +134,7 @@ pub struct PostUpdateForm {
pub featured_local: Option<bool>,
pub url_content_type: Option<Option<String>>,
pub alt_text: Option<Option<String>>,
pub scheduled_publish_time: Option<Option<DateTime<Utc>>>,
}
#[derive(PartialEq, Eq, Debug)]

View File

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

View File

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

View File

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

View File

@ -318,11 +318,18 @@ fn queries<'a>() -> Queries<
// hide posts from deleted communities
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() {
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 {
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
@ -387,14 +394,12 @@ fn queries<'a>() -> Queries<
query = query.filter(post::url.eq(search_term));
} else {
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.filter(post::name.ilike(searcher))
query.filter(name_filter)
} else {
query.filter(
post::name
.ilike(searcher.clone())
.or(post::body.ilike(searcher)),
)
query.filter(name_filter.or(body_filter))
}
.filter(not(post::removed.or(post::deleted)));
}
@ -734,7 +739,7 @@ impl<'a> PostQuery<'a> {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[expect(clippy::unwrap_used)]
mod tests {
use crate::{
post_view::{PaginationCursorData, PostQuery, PostView},
@ -1771,6 +1776,7 @@ mod tests {
featured_community: false,
featured_local: false,
url_content_type: None,
scheduled_publish_time: None,
},
my_vote: None,
unread_comments: 0,

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,13 @@ doctest = false
workspace = true
[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]
lemmy_db_schema = { workspace = true }
@ -33,6 +39,7 @@ serde_with = { workspace = true }
ts-rs = { workspace = true, optional = true }
chrono.workspace = true
strum = { workspace = true }
lemmy_utils = { workspace = true, optional = true }
[dev-dependencies]
serial_test = { workspace = true }

View File

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

View File

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

View File

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

View File

@ -26,6 +26,7 @@ use lemmy_db_schema::{
ListingType,
PostSortType,
};
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
fn queries<'a>() -> Queries<
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 {
let searcher = fuzzy_search(&search_term);
query = query
.filter(community::name.ilike(searcher.clone()))
.or_filter(community::title.ilike(searcher))
let name_filter = community::name.ilike(searcher.clone());
let title_filter = community::title.ilike(searcher.clone());
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
@ -185,35 +191,39 @@ impl CommunityView {
.await
}
pub async fn is_mod_or_admin(
pub async fn check_is_mod_or_admin(
pool: &mut DbPool<'_>,
person_id: PersonId,
community_id: CommunityId,
) -> Result<bool, Error> {
) -> LemmyResult<()> {
let is_mod =
CommunityModeratorView::is_community_moderator(pool, community_id, person_id).await?;
if is_mod {
Ok(true)
} else if let Ok(person_view) = PersonView::read(pool, person_id).await {
Ok(person_view.is_admin)
CommunityModeratorView::check_is_community_moderator(pool, community_id, person_id).await;
if is_mod.is_ok()
|| PersonView::read(pool, person_id)
.await
.is_ok_and(|t| t.is_admin)
{
Ok(())
} else {
Ok(false)
Err(LemmyErrorType::NotAModOrAdmin)?
}
}
/// 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<'_>,
person_id: PersonId,
) -> Result<bool, Error> {
) -> LemmyResult<()> {
let is_mod_of_any =
CommunityModeratorView::is_community_moderator_of_any(pool, person_id).await?;
if is_mod_of_any {
Ok(true)
} else if let Ok(person_view) = PersonView::read(pool, person_id).await {
Ok(person_view.is_admin)
CommunityModeratorView::is_community_moderator_of_any(pool, person_id).await;
if is_mod_of_any.is_ok()
|| PersonView::read(pool, person_id)
.await
.is_ok_and(|t| t.is_admin)
{
Ok(())
} else {
Ok(false)
Err(LemmyErrorType::NotAModOrAdmin)?
}
}
}
@ -224,6 +234,7 @@ pub struct CommunityQuery<'a> {
pub sort: Option<PostSortType>,
pub local_user: Option<&'a LocalUser>,
pub search_term: Option<String>,
pub title_only: Option<bool>,
pub is_mod_or_admin: bool,
pub show_nsfw: bool,
pub page: Option<i64>,
@ -237,8 +248,7 @@ impl<'a> CommunityQuery<'a> {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
#[expect(clippy::unwrap_used)]
mod tests {
use crate::{community_view::CommunityQuery, structs::CommunityView};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -134,8 +134,6 @@ pub fn add(markdown_parser: &mut MarkdownIt) {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
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)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
#[expect(clippy::indexing_slicing)]
mod test {
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)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
#[expect(clippy::unwrap_used)]
mod test {
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)]
#[allow(clippy::indexing_slicing)]
mod tests {
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

@ -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;

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