Compare commits

...

9 Commits

Author SHA1 Message Date
Dessalines
a5eaad7afd Merge remote-tracking branch 'origin/main' into remove_success_responses 2024-09-27 08:49:50 -04:00
SleeplessOne1917
33cbd95b7e
Add skip_serialize_none to OAuth structs with option fields (#5046)
* Add skip_serialize_none to OAuth structs with option fields

* PR feedback

* Remove serde and ts export from SSO db-only structs
2024-09-26 10:24:51 +02:00
Nutomic
f6a24e133a
Replace clippy allow annotation with expect (fixes #5012) (#5048) 2024-09-24 13:29:02 -04:00
Nutomic
61a02482ff
Cleanup remaining use of Result<bool, Error> (fixes #4862) (#5047) 2024-09-24 13:25:33 -04:00
Dessalines
0fab5bed24
Add ability to search for Community by its description (or title only). (#5044)
- This changes the post_title_only for Search to title_only, since its
  also used in the community query now.
- Fixes #4785
2024-09-24 13:24:28 -04:00
Dessalines
a65be776e3
Remove redundant local_user.auto_expand setting. (#5041)
- Fixes #4643

Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com>
2024-09-24 08:55:09 -04:00
Nutomic
9eee61dd06
Post scheduling (fixes #234) (#5025)
* Post scheduling (fixes #234)

* clippy

* replace map_err with inspect_err

* ignore unpublished posts in read queries

* add api test

* fmt

* add some checks

* address some review comments

* allow updating schedule time

* rewrite scheduled task

* fmt

* machete

* compare date in sql, more filters

* check for community ban in sql

* remove api test (scheduled task only runs every 10 mins)

* remove mut

* add index

* remove Post::read impl

* fmt

* fix

* correctly handle changes to schedule time

* normal users can only schedule up to 10 posts
2024-09-24 05:39:40 -04:00
Nutomic
bab5c93062
Conditionally hide comments on nsfw posts (fixes #4237) (#5028)
* Conditionally hide comments on nsfw posts (fixes #4237)

* fix test
2024-09-24 10:33:53 +02:00
Dessalines
d476d32200
Removing a few more Result<bool> . (#4977)
* Removing a few more Result<bool> .

* Running taplo fmt.

* Running fmt.

* Adding email taken test.

* Fixing tests.

* Adding back in missing admin check.

* Rename check_has_local_followers function.
2024-09-23 20:55:35 -04:00
102 changed files with 622 additions and 497 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.12",
"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

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

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

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

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

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

@ -35,8 +35,7 @@ use crate::{
use chrono::{DateTime, Utc};
use diesel::{
deserialize,
dsl,
dsl::{exists, insert_into},
dsl::{self, exists, insert_into},
pg::Pg,
result::Error,
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
/// 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())
}
}
@ -430,7 +431,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,
@ -172,6 +173,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()
@ -245,6 +247,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]
@ -444,6 +468,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

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

@ -87,39 +87,30 @@ impl Serialize for PublicOAuthProvider {
}
#[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", ts(export))]
pub struct OAuthProviderInsertForm {
pub display_name: String,
#[cfg_attr(feature = "full", ts(type = "string"))]
pub issuer: DbUrl,
#[cfg_attr(feature = "full", ts(type = "string"))]
pub authorization_endpoint: DbUrl,
#[cfg_attr(feature = "full", ts(type = "string"))]
pub token_endpoint: DbUrl,
#[cfg_attr(feature = "full", ts(type = "string"))]
pub userinfo_endpoint: DbUrl,
pub id_claim: String,
pub client_id: String,
pub client_secret: String,
pub scopes: String,
pub auto_verify_email: bool,
pub account_linking_enabled: bool,
pub enabled: bool,
pub auto_verify_email: Option<bool>,
pub account_linking_enabled: Option<bool>,
pub enabled: Option<bool>,
}
#[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", ts(export))]
pub struct OAuthProviderUpdateForm {
pub display_name: Option<String>,
#[cfg_attr(feature = "full", ts(type = "string"))]
pub authorization_endpoint: Option<DbUrl>,
#[cfg_attr(feature = "full", ts(type = "string"))]
pub token_endpoint: Option<DbUrl>,
#[cfg_attr(feature = "full", ts(type = "string"))]
pub userinfo_endpoint: Option<DbUrl>,
pub id_claim: Option<String>,
pub client_secret: Option<String>,

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

@ -14,7 +14,7 @@ CREATE TABLE oauth_provider (
scopes text NOT NULL,
auto_verify_email boolean DEFAULT TRUE 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,
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(),
);
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() {
serve_prometheus(prometheus, context.clone())?;
}
@ -187,7 +182,14 @@ pub async fn start_lemmy_server(args: CmdArgs) -> LemmyResult<()> {
}))
.expect("set function pointer");
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 {
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