From b26aaac523b9d03874d320889d93606dc5bbcce9 Mon Sep 17 00:00:00 2001 From: privacyguard <92675882+privacyguard@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:52:33 +0300 Subject: [PATCH 1/8] SSO Support (#4881) * Added OAUTH2 OIDC support * Fixes and improvements based on review feedback * use derive_new::new instead of TypedBuilder * merge migrations into a single file * fixes based on review feedback * remove unnecessary hostname_ui config * improvement based on review feedback * improvements based on review feedback * delete user oauth accounts at account deletion * fixes and improvements based on review feedback * removed auto_approve_application * support registration application with sso * improvements based on review feedback * making the TokenResponse an internal struct as it should be * remove duplicate struct * prevent oauth linking to unverified accounts * switched to manually entered username and removed the oauth name claim * fix cargo fmt * fix compile error * improvements based on review feedback * fixes and improvements based on review feedback --------- Co-authored-by: privacyguard --- Cargo.lock | 7 +- crates/api/src/local_user/change_password.rs | 12 +- crates/api/src/local_user/login.rs | 45 +- crates/api/src/local_user/mod.rs | 15 - crates/api/src/local_user/reset_password.rs | 3 +- crates/api/src/site/leave_admin.rs | 4 + crates/api_common/src/lib.rs | 1 + crates/api_common/src/oauth_provider.rs | 69 +++ crates/api_common/src/request.rs | 1 + crates/api_common/src/site.rs | 7 + crates/api_common/src/utils.rs | 50 +- crates/api_crud/Cargo.toml | 3 + crates/api_crud/src/lib.rs | 1 + crates/api_crud/src/oauth_provider/create.rs | 42 ++ crates/api_crud/src/oauth_provider/delete.rs | 25 + crates/api_crud/src/oauth_provider/mod.rs | 3 + crates/api_crud/src/oauth_provider/update.rs | 44 ++ crates/api_crud/src/site/create.rs | 1 + crates/api_crud/src/site/read.rs | 19 +- crates/api_crud/src/site/update.rs | 13 + crates/api_crud/src/user/create.rs | 508 +++++++++++++++--- crates/api_crud/src/user/delete.rs | 18 +- crates/db_schema/src/impls/local_user.rs | 10 +- crates/db_schema/src/impls/mod.rs | 2 + crates/db_schema/src/impls/oauth_account.rs | 59 ++ crates/db_schema/src/impls/oauth_provider.rs | 71 +++ crates/db_schema/src/impls/person.rs | 12 + crates/db_schema/src/newtypes.rs | 6 + crates/db_schema/src/schema.rs | 37 +- crates/db_schema/src/source/local_site.rs | 4 + crates/db_schema/src/source/local_user.rs | 4 +- crates/db_schema/src/source/mod.rs | 2 + crates/db_schema/src/source/oauth_account.rs | 32 ++ crates/db_schema/src/source/oauth_provider.rs | 131 +++++ crates/db_schema/src/utils.rs | 26 +- crates/db_views/src/local_user_view.rs | 34 +- crates/routes/src/images.rs | 3 +- crates/utils/src/error.rs | 7 + .../down.sql | 10 + .../up.sql | 34 ++ src/api_routes_http.rs | 22 +- src/code_migrations.rs | 2 +- 42 files changed, 1234 insertions(+), 165 deletions(-) create mode 100644 crates/api_common/src/oauth_provider.rs create mode 100644 crates/api_crud/src/oauth_provider/create.rs create mode 100644 crates/api_crud/src/oauth_provider/delete.rs create mode 100644 crates/api_crud/src/oauth_provider/mod.rs create mode 100644 crates/api_crud/src/oauth_provider/update.rs create mode 100644 crates/db_schema/src/impls/oauth_account.rs create mode 100644 crates/db_schema/src/impls/oauth_provider.rs create mode 100644 crates/db_schema/src/source/oauth_account.rs create mode 100644 crates/db_schema/src/source/oauth_provider.rs create mode 100644 migrations/2024-09-16-174833_create_oauth_provider/down.sql create mode 100644 migrations/2024-09-16-174833_create_oauth_provider/up.sql diff --git a/Cargo.lock b/Cargo.lock index 31815bc004..c6fa6255bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2545,6 +2545,9 @@ dependencies = [ "lemmy_db_views_actor", "lemmy_utils", "moka", + "serde", + "serde_json", + "serde_with", "tracing", "url", "uuid", @@ -3314,9 +3317,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ea5043e58958ee56f3e15a90aee535795cd7dfd319846288d93c5b57d85cbe" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "overload" diff --git a/crates/api/src/local_user/change_password.rs b/crates/api/src/local_user/change_password.rs index 50ee10bb65..03f873a0f0 100644 --- a/crates/api/src/local_user/change_password.rs +++ b/crates/api/src/local_user/change_password.rs @@ -28,11 +28,13 @@ pub async fn change_password( } // Check the old password - let valid: bool = verify( - &data.old_password, - &local_user_view.local_user.password_encrypted, - ) - .unwrap_or(false); + let valid: bool = if let Some(password_encrypted) = &local_user_view.local_user.password_encrypted + { + verify(&data.old_password, password_encrypted).unwrap_or(false) + } else { + data.old_password.is_empty() + }; + if !valid { Err(LemmyErrorType::IncorrectLogin)? } diff --git a/crates/api/src/local_user/login.rs b/crates/api/src/local_user/login.rs index e6ae385100..a8f65d7583 100644 --- a/crates/api/src/local_user/login.rs +++ b/crates/api/src/local_user/login.rs @@ -1,4 +1,4 @@ -use crate::{check_totp_2fa_valid, local_user::check_email_verified}; +use crate::check_totp_2fa_valid; use actix_web::{ web::{Data, Json}, HttpRequest, @@ -8,12 +8,7 @@ use lemmy_api_common::{ claims::Claims, context::LemmyContext, person::{Login, LoginResponse}, - utils::check_user_valid, -}; -use lemmy_db_schema::{ - source::{local_site::LocalSite, registration_application::RegistrationApplication}, - utils::DbPool, - RegistrationMode, + utils::{check_email_verified, check_registration_application, check_user_valid}, }; use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; @@ -34,11 +29,12 @@ pub async fn login( .ok_or(LemmyErrorType::IncorrectLogin)?; // Verify the password - let valid: bool = verify( - &data.password, - &local_user_view.local_user.password_encrypted, - ) - .unwrap_or(false); + let valid: bool = local_user_view + .local_user + .password_encrypted + .as_ref() + .and_then(|password_encrypted| verify(&data.password, password_encrypted).ok()) + .unwrap_or(false); if !valid { Err(LemmyErrorType::IncorrectLogin)? } @@ -65,28 +61,3 @@ pub async fn login( registration_created: false, })) } - -async fn check_registration_application( - local_user_view: &LocalUserView, - local_site: &LocalSite, - pool: &mut DbPool<'_>, -) -> LemmyResult<()> { - if (local_site.registration_mode == RegistrationMode::RequireApplication - || local_site.registration_mode == RegistrationMode::Closed) - && !local_user_view.local_user.accepted_application - && !local_user_view.local_user.admin - { - // Fetch the registration application. If no admin id is present its still pending. Otherwise it - // was processed (either accepted or denied). - let local_user_id = local_user_view.local_user.id; - let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id) - .await? - .ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?; - if registration.admin_id.is_some() { - Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))? - } else { - Err(LemmyErrorType::RegistrationApplicationIsPending)? - } - } - Ok(()) -} diff --git a/crates/api/src/local_user/mod.rs b/crates/api/src/local_user/mod.rs index c00a4516e4..b1ee7c0b6e 100644 --- a/crates/api/src/local_user/mod.rs +++ b/crates/api/src/local_user/mod.rs @@ -1,6 +1,3 @@ -use lemmy_db_views::structs::{LocalUserView, SiteView}; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; - pub mod add_admin; pub mod ban_person; pub mod block; @@ -20,15 +17,3 @@ pub mod save_settings; pub mod update_totp; pub mod validate_auth; pub mod verify_email; - -/// Check if the user's email is verified if email verification is turned on -/// However, skip checking verification if the user is an admin -fn check_email_verified(local_user_view: &LocalUserView, site_view: &SiteView) -> LemmyResult<()> { - if !local_user_view.local_user.admin - && site_view.local_site.require_email_verification - && !local_user_view.local_user.email_verified - { - Err(LemmyErrorType::EmailNotVerified)? - } - Ok(()) -} diff --git a/crates/api/src/local_user/reset_password.rs b/crates/api/src/local_user/reset_password.rs index 1c47e6c4e8..4854d13765 100644 --- a/crates/api/src/local_user/reset_password.rs +++ b/crates/api/src/local_user/reset_password.rs @@ -1,9 +1,8 @@ -use crate::local_user::check_email_verified; use actix_web::web::{Data, Json}; use lemmy_api_common::{ context::LemmyContext, person::PasswordReset, - utils::send_password_reset_email, + utils::{check_email_verified, send_password_reset_email}, SuccessResponse, }; use lemmy_db_views::structs::{LocalUserView, SiteView}; diff --git a/crates/api/src/site/leave_admin.rs b/crates/api/src/site/leave_admin.rs index 52b8a32ef7..d3581995a2 100644 --- a/crates/api/src/site/leave_admin.rs +++ b/crates/api/src/site/leave_admin.rs @@ -7,6 +7,7 @@ use lemmy_db_schema::{ local_site_url_blocklist::LocalSiteUrlBlocklist, local_user::{LocalUser, LocalUserUpdateForm}, moderator::{ModAdd, ModAddForm}, + oauth_provider::OAuthProvider, tagline::Tagline, }, traits::Crud, @@ -63,6 +64,7 @@ pub async fn leave_admin( let taglines = Tagline::get_all(&mut context.pool(), site_view.local_site.id).await?; let custom_emojis = CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?; + let oauth_providers = OAuthProvider::get_all_public(&mut context.pool()).await?; let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?; Ok(Json(GetSiteResponse { @@ -74,6 +76,8 @@ pub async fn leave_admin( discussion_languages, taglines, custom_emojis, + oauth_providers: Some(oauth_providers), + admin_oauth_providers: None, blocked_urls, })) } diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs index 9d12d2e13f..48acaad216 100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@ -7,6 +7,7 @@ pub mod community; #[cfg(feature = "full")] pub mod context; pub mod custom_emoji; +pub mod oauth_provider; pub mod person; pub mod post; pub mod private_message; diff --git a/crates/api_common/src/oauth_provider.rs b/crates/api_common/src/oauth_provider.rs new file mode 100644 index 0000000000..c51edc7a45 --- /dev/null +++ b/crates/api_common/src/oauth_provider.rs @@ -0,0 +1,69 @@ +use lemmy_db_schema::newtypes::OAuthProviderId; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; +use url::Url; + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Create an external auth method. +pub struct CreateOAuthProvider { + pub display_name: String, + pub issuer: String, + pub authorization_endpoint: String, + pub token_endpoint: String, + pub userinfo_endpoint: String, + 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, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Edit an external auth method. +pub struct EditOAuthProvider { + pub id: OAuthProviderId, + pub display_name: Option, + pub authorization_endpoint: Option, + pub token_endpoint: Option, + pub userinfo_endpoint: Option, + pub id_claim: Option, + pub client_secret: Option, + pub scopes: Option, + pub auto_verify_email: Option, + pub account_linking_enabled: Option, + pub enabled: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Delete an external auth method. +pub struct DeleteOAuthProvider { + pub id: OAuthProviderId, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Logging in with an OAuth 2.0 authorization +pub struct AuthenticateWithOauth { + pub code: String, + #[cfg_attr(feature = "full", ts(type = "string"))] + pub oauth_provider_id: OAuthProviderId, + #[cfg_attr(feature = "full", ts(type = "string"))] + pub redirect_uri: Url, + pub show_nsfw: Option, + /// Username is mandatory at registration time + pub username: Option, + /// An answer is mandatory if require application is enabled on the server + pub answer: Option, +} diff --git a/crates/api_common/src/request.rs b/crates/api_common/src/request.rs index 90a626e4f7..970aa17dff 100644 --- a/crates/api_common/src/request.rs +++ b/crates/api_common/src/request.rs @@ -44,6 +44,7 @@ pub fn client_builder(settings: &Settings) -> ClientBuilder { .user_agent(user_agent.clone()) .timeout(REQWEST_TIMEOUT) .connect_timeout(REQWEST_TIMEOUT) + .use_rustls_tls() } /// Fetches metadata for the given link and optionally generates thumbnail. diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index fa43f2a39e..6fa6e37009 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -16,6 +16,7 @@ use lemmy_db_schema::{ instance::Instance, language::Language, local_site_url_blocklist::LocalSiteUrlBlocklist, + oauth_provider::{OAuthProvider, PublicOAuthProvider}, person::Person, tagline::Tagline, }, @@ -200,6 +201,7 @@ pub struct CreateSite { pub blocked_instances: Option>, pub taglines: Option>, pub registration_mode: Option, + pub oauth_registration: Option, pub content_warning: Option, pub default_post_listing_mode: Option, } @@ -282,6 +284,8 @@ pub struct EditSite { /// A list of taglines shown at the top of the front page. pub taglines: Option>, pub registration_mode: Option, + /// Whether or not external auth methods can auto-register users. + pub oauth_registration: Option, /// Whether to email admins for new reports. pub reports_email_admins: Option, /// If present, nsfw content is visible by default. Should be displayed by frontends/clients @@ -316,6 +320,9 @@ pub struct GetSiteResponse { pub taglines: Vec, /// A list of custom emojis your site supports. pub custom_emojis: Vec, + /// A list of external auth methods your site supports. + pub oauth_providers: Option>, + pub admin_oauth_providers: Option>, pub blocked_urls: Vec, } diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index ebcc237e53..e41b574c52 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -23,18 +23,21 @@ use lemmy_db_schema::{ local_site::LocalSite, local_site_rate_limit::LocalSiteRateLimit, local_site_url_blocklist::LocalSiteUrlBlocklist, + oauth_account::OAuthAccount, password_reset_request::PasswordResetRequest, person::{Person, PersonUpdateForm}, person_block::PersonBlock, post::{Post, PostRead}, + registration_application::RegistrationApplication, site::Site, }, traits::Crud, utils::DbPool, + RegistrationMode, }; use lemmy_db_views::{ comment_view::CommentQuery, - structs::{LocalImageView, LocalUserView}, + structs::{LocalImageView, LocalUserView, SiteView}, }; use lemmy_db_views_actor::structs::{ CommunityModeratorView, @@ -192,6 +195,46 @@ pub fn check_user_valid(person: &Person) -> LemmyResult<()> { } } +/// Check if the user's email is verified if email verification is turned on +/// However, skip checking verification if the user is an admin +pub fn check_email_verified( + local_user_view: &LocalUserView, + site_view: &SiteView, +) -> LemmyResult<()> { + if !local_user_view.local_user.admin + && site_view.local_site.require_email_verification + && !local_user_view.local_user.email_verified + { + Err(LemmyErrorType::EmailNotVerified)? + } + Ok(()) +} + +pub async fn check_registration_application( + local_user_view: &LocalUserView, + local_site: &LocalSite, + pool: &mut DbPool<'_>, +) -> LemmyResult<()> { + if (local_site.registration_mode == RegistrationMode::RequireApplication + || local_site.registration_mode == RegistrationMode::Closed) + && !local_user_view.local_user.accepted_application + && !local_user_view.local_user.admin + { + // Fetch the registration application. If no admin id is present its still pending. Otherwise it + // was processed (either accepted or denied). + let local_user_id = local_user_view.local_user.id; + let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id) + .await? + .ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?; + if registration.admin_id.is_some() { + Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))? + } else { + Err(LemmyErrorType::RegistrationApplicationIsPending)? + } + } + Ok(()) +} + /// Checks that a normal user action (eg posting or voting) is allowed in a given community. /// /// In particular it checks that neither the user nor community are banned or deleted, and that @@ -852,6 +895,11 @@ pub async fn purge_user_account(person_id: PersonId, context: &LemmyContext) -> // Leave communities they mod CommunityModerator::leave_all_communities(pool, person_id).await?; + // Delete the oauth accounts linked to the local user + if let Ok(Some(local_user)) = LocalUserView::read_person(pool, person_id).await { + OAuthAccount::delete_user_accounts(pool, local_user.local_user.id).await?; + } + Person::delete_account(pool, person_id).await?; Ok(()) diff --git a/crates/api_crud/Cargo.toml b/crates/api_crud/Cargo.toml index 5114eb8cf3..259116a38d 100644 --- a/crates/api_crud/Cargo.toml +++ b/crates/api_crud/Cargo.toml @@ -29,6 +29,9 @@ moka.workspace = true anyhow.workspace = true webmention = "0.6.0" accept-language = "3.1.0" +serde_json = { workspace = true } +serde = { workspace = true } +serde_with = { workspace = true } [package.metadata.cargo-machete] ignored = ["futures"] diff --git a/crates/api_crud/src/lib.rs b/crates/api_crud/src/lib.rs index aee3e81345..b138fbd30a 100644 --- a/crates/api_crud/src/lib.rs +++ b/crates/api_crud/src/lib.rs @@ -1,6 +1,7 @@ pub mod comment; pub mod community; pub mod custom_emoji; +pub mod oauth_provider; pub mod post; pub mod private_message; pub mod site; diff --git a/crates/api_crud/src/oauth_provider/create.rs b/crates/api_crud/src/oauth_provider/create.rs new file mode 100644 index 0000000000..fe44ae56e8 --- /dev/null +++ b/crates/api_crud/src/oauth_provider/create.rs @@ -0,0 +1,42 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + context::LemmyContext, + oauth_provider::CreateOAuthProvider, + utils::is_admin, +}; +use lemmy_db_schema::{ + source::oauth_provider::{OAuthProvider, OAuthProviderInsertForm}, + traits::Crud, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyError; +use url::Url; + +#[tracing::instrument(skip(context))] +pub async fn create_oauth_provider( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> Result, LemmyError> { + // Make sure user is an admin + is_admin(&local_user_view)?; + + let cloned_data = data.clone(); + let oauth_provider_form = OAuthProviderInsertForm { + display_name: cloned_data.display_name, + issuer: Url::parse(&cloned_data.issuer)?.into(), + authorization_endpoint: Url::parse(&cloned_data.authorization_endpoint)?.into(), + token_endpoint: Url::parse(&cloned_data.token_endpoint)?.into(), + userinfo_endpoint: Url::parse(&cloned_data.userinfo_endpoint)?.into(), + id_claim: cloned_data.id_claim, + client_id: data.client_id.to_string(), + client_secret: data.client_secret.to_string(), + scopes: data.scopes.to_string(), + auto_verify_email: data.auto_verify_email, + account_linking_enabled: data.account_linking_enabled, + enabled: data.enabled, + }; + let oauth_provider = OAuthProvider::create(&mut context.pool(), &oauth_provider_form).await?; + Ok(Json(oauth_provider)) +} diff --git a/crates/api_crud/src/oauth_provider/delete.rs b/crates/api_crud/src/oauth_provider/delete.rs new file mode 100644 index 0000000000..0d4d616cc8 --- /dev/null +++ b/crates/api_crud/src/oauth_provider/delete.rs @@ -0,0 +1,25 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + context::LemmyContext, + oauth_provider::DeleteOAuthProvider, + utils::is_admin, + SuccessResponse, +}; +use lemmy_db_schema::{source::oauth_provider::OAuthProvider, traits::Crud}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType}; + +#[tracing::instrument(skip(context))] +pub async fn delete_oauth_provider( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> Result, LemmyError> { + // Make sure user is an admin + is_admin(&local_user_view)?; + OAuthProvider::delete(&mut context.pool(), data.id) + .await + .with_lemmy_type(LemmyErrorType::CouldntDeleteOauthProvider)?; + Ok(Json(SuccessResponse::default())) +} diff --git a/crates/api_crud/src/oauth_provider/mod.rs b/crates/api_crud/src/oauth_provider/mod.rs new file mode 100644 index 0000000000..fdb2f55613 --- /dev/null +++ b/crates/api_crud/src/oauth_provider/mod.rs @@ -0,0 +1,3 @@ +pub mod create; +pub mod delete; +pub mod update; diff --git a/crates/api_crud/src/oauth_provider/update.rs b/crates/api_crud/src/oauth_provider/update.rs new file mode 100644 index 0000000000..61d5b0adce --- /dev/null +++ b/crates/api_crud/src/oauth_provider/update.rs @@ -0,0 +1,44 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{context::LemmyContext, oauth_provider::EditOAuthProvider, utils::is_admin}; +use lemmy_db_schema::{ + source::oauth_provider::{OAuthProvider, OAuthProviderUpdateForm}, + traits::Crud, + utils::{diesel_required_string_update, diesel_required_url_update, naive_now}, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::{error::LemmyError, LemmyErrorType}; + +#[tracing::instrument(skip(context))] +pub async fn update_oauth_provider( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> Result, LemmyError> { + // Make sure user is an admin + is_admin(&local_user_view)?; + + let cloned_data = data.clone(); + let oauth_provider_form = OAuthProviderUpdateForm { + display_name: diesel_required_string_update(cloned_data.display_name.as_deref()), + authorization_endpoint: diesel_required_url_update( + cloned_data.authorization_endpoint.as_deref(), + )?, + token_endpoint: diesel_required_url_update(cloned_data.token_endpoint.as_deref())?, + userinfo_endpoint: diesel_required_url_update(cloned_data.userinfo_endpoint.as_deref())?, + id_claim: diesel_required_string_update(data.id_claim.as_deref()), + client_secret: diesel_required_string_update(data.client_secret.as_deref()), + scopes: diesel_required_string_update(data.scopes.as_deref()), + auto_verify_email: data.auto_verify_email, + account_linking_enabled: data.account_linking_enabled, + enabled: data.enabled, + updated: Some(Some(naive_now())), + }; + + let update_result = + OAuthProvider::update(&mut context.pool(), data.id, &oauth_provider_form).await?; + let oauth_provider = OAuthProvider::read(&mut context.pool(), update_result.id) + .await? + .ok_or(LemmyErrorType::CouldntFindOauthProvider)?; + Ok(Json(oauth_provider)) +} diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index 6566a7a9fc..3d96d20cf7 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -591,6 +591,7 @@ mod tests { blocked_instances: None, taglines: None, registration_mode: site_registration_mode, + oauth_registration: None, content_warning: None, default_post_listing_mode: None, } diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs index 94a28a4ad7..6f524dd7d2 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -9,6 +9,7 @@ use lemmy_db_schema::source::{ instance_block::InstanceBlock, language::Language, local_site_url_blocklist::LocalSiteUrlBlocklist, + oauth_provider::OAuthProvider, person_block::PersonBlock, tagline::Tagline, }; @@ -45,6 +46,10 @@ pub async fn get_site( let custom_emojis = CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?; let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?; + let admin_oauth_providers = OAuthProvider::get_all(&mut context.pool()).await?; + let oauth_providers = + OAuthProvider::convert_providers_to_public(admin_oauth_providers.clone()); + Ok(GetSiteResponse { site_view, admins, @@ -55,13 +60,15 @@ pub async fn get_site( taglines, custom_emojis, blocked_urls, + oauth_providers: Some(oauth_providers), + admin_oauth_providers: Some(admin_oauth_providers), }) }) .await .map_err(|e| anyhow::anyhow!("Failed to construct site response: {e}"))?; // Build the local user with parallel queries and add it to site response - site_response.my_user = if let Some(local_user_view) = local_user_view { + site_response.my_user = if let Some(ref local_user_view) = local_user_view { let person_id = local_user_view.person.id; let local_user_id = local_user_view.local_user.id; let pool = &mut context.pool(); @@ -84,7 +91,7 @@ pub async fn get_site( .with_lemmy_type(LemmyErrorType::SystemErrLogin)?; Some(MyUserInfo { - local_user_view, + local_user_view: local_user_view.clone(), follows, moderates, community_blocks, @@ -96,5 +103,13 @@ pub async fn get_site( None }; + // filter oauth_providers for public access + if !local_user_view + .map(|l| l.local_user.admin) + .unwrap_or_default() + { + site_response.admin_oauth_providers = None; + } + Ok(Json(site_response)) } diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index f68b00c041..7e9dc8f039 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -119,6 +119,7 @@ pub async fn update_site( captcha_difficulty: data.captcha_difficulty.clone(), reports_email_admins: data.reports_email_admins, default_post_listing_mode: data.default_post_listing_mode, + oauth_registration: data.oauth_registration, ..Default::default() }; @@ -278,6 +279,7 @@ mod tests { None::, None::, None::, + None::, ), ), ( @@ -301,6 +303,7 @@ mod tests { None::, None::, None::, + None::, ), ), ( @@ -324,6 +327,7 @@ mod tests { None::, None::, None::, + None::, ), ), ( @@ -347,6 +351,7 @@ mod tests { Some(true), None::, None::, + None::, ), ), ( @@ -370,6 +375,7 @@ mod tests { Some(true), None::, None::, + None::, ), ), ( @@ -393,6 +399,7 @@ mod tests { None::, None::, Some(RegistrationMode::RequireApplication), + None::, ), ), ]; @@ -447,6 +454,7 @@ mod tests { None::, None::, None::, + None::, ), ), ( @@ -469,6 +477,7 @@ mod tests { Some(true), Some(String::new()), Some(RegistrationMode::Open), + None::, ), ), ( @@ -491,6 +500,7 @@ mod tests { None::, None::, None::, + None::, ), ), ( @@ -513,6 +523,7 @@ mod tests { None::, None::, Some(RegistrationMode::RequireApplication), + None::, ), ), ]; @@ -561,6 +572,7 @@ mod tests { site_is_federated: Option, site_application_question: Option, site_registration_mode: Option, + site_oauth_registration: Option, ) -> EditSite { EditSite { name: site_name, @@ -607,6 +619,7 @@ mod tests { reports_email_admins: None, content_warning: None, default_post_listing_mode: None, + oauth_registration: site_oauth_registration, } } } diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index b717d88160..1fb14a6e2c 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -3,8 +3,12 @@ use actix_web::{web::Json, HttpRequest}; use lemmy_api_common::{ claims::Claims, context::LemmyContext, + oauth_provider::AuthenticateWithOauth, person::{LoginResponse, Register}, utils::{ + check_email_verified, + check_registration_application, + check_user_valid, generate_inbox_url, generate_local_apub_endpoint, generate_shared_inbox_url, @@ -18,11 +22,15 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ aggregates::structs::PersonAggregates, + newtypes::{InstanceId, OAuthProviderId}, source::{ captcha_answer::{CaptchaAnswer, CheckCaptchaAnswer}, language::Language, + local_site::LocalSite, local_user::{LocalUser, LocalUserInsertForm}, local_user_vote_display_mode::LocalUserVoteDisplayMode, + oauth_account::{OAuthAccount, OAuthAccountInsertForm}, + oauth_provider::OAuthProvider, person::{Person, PersonInsertForm}, registration_application::{RegistrationApplication, RegistrationApplicationInsertForm}, }, @@ -31,15 +39,27 @@ use lemmy_db_schema::{ }; use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_utils::{ - error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, + error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}, utils::{ slurs::{check_slurs, check_slurs_opt}, validation::is_valid_actor_name, }, }; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; use std::collections::HashSet; -#[tracing::instrument(skip(context))] +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +/// Response from OAuth token endpoint +struct TokenResponse { + pub access_token: String, + pub token_type: String, + pub expires_in: Option, + pub refresh_token: Option, + pub scope: Option, +} + pub async fn register( data: Json, req: HttpRequest, @@ -61,8 +81,9 @@ pub async fn register( Err(LemmyErrorType::EmailRequired)? } - if local_site.site_setup && require_registration_application && data.answer.is_none() { - Err(LemmyErrorType::RegistrationApplicationAnswerRequired)? + // make sure the registration answer is provided when the registration application is required + if local_site.site_setup { + validate_registration_answer(require_registration_application, &data.answer)?; } // Make sure passwords match @@ -93,13 +114,9 @@ pub async fn register( check_slurs(&data.username, &slur_regex)?; check_slurs_opt(&data.answer, &slur_regex)?; - let actor_keypair = generate_actor_keypair()?; - is_valid_actor_name(&data.username, local_site.actor_name_max_length as usize)?; - let actor_id = generate_local_apub_endpoint( - EndpointType::Person, - &data.username, - &context.settings().get_protocol_and_hostname(), - )?; + if Person::is_username_taken(&mut context.pool(), &data.username).await? { + return Err(LemmyErrorType::UsernameAlreadyExists)?; + } if let Some(email) = &data.email { if LocalUser::is_email_taken(&mut context.pool(), email).await? { @@ -108,49 +125,28 @@ pub async fn register( } // We have to create both a person, and local_user - - // Register the new person - let person_form = PersonInsertForm { - actor_id: Some(actor_id.clone()), - inbox_url: Some(generate_inbox_url(&actor_id)?), - shared_inbox_url: Some(generate_shared_inbox_url(context.settings())?), - private_key: Some(actor_keypair.private_key), - ..PersonInsertForm::new( - data.username.clone(), - actor_keypair.public_key, - site_view.site.instance_id, - ) - }; - - // insert the person - let inserted_person = Person::create(&mut context.pool(), &person_form) - .await - .with_lemmy_type(LemmyErrorType::UserAlreadyExists)?; + let inserted_person = create_person( + data.username.clone(), + &local_site, + site_view.site.instance_id, + &context, + ) + .await?; // Automatically set their application as accepted, if they created this with open registration. // Also fixes a bug which allows users to log in when registrations are changed to closed. let accepted_application = Some(!require_registration_application); - // Get the user's preferred language using the Accept-Language header - let language_tags: Vec = req - .headers() - .get("Accept-Language") - .map(|hdr| accept_language::parse(hdr.to_str().unwrap_or_default())) - .iter() - .flatten() - // Remove the optional region code - .map(|lang_str| lang_str.split('-').next().unwrap_or_default().to_string()) - .collect(); - // Show nsfw content if param is true, or if content_warning exists let show_nsfw = data .show_nsfw .unwrap_or(site_view.site.content_warning.is_some()); + let language_tags = get_language_tags(&req); + // Create the local user let local_user_form = LocalUserInsertForm { email: data.email.as_deref().map(str::to_lowercase), - password_encrypted: data.password.to_string(), show_nsfw: Some(show_nsfw), accepted_application, default_listing_type: Some(local_site.default_post_listing_type), @@ -158,21 +154,10 @@ pub async fn register( interface_language: language_tags.first().cloned(), // If its the initial site setup, they are an admin admin: Some(!local_site.site_setup), - ..LocalUserInsertForm::new(inserted_person.id, data.password.to_string()) + ..LocalUserInsertForm::new(inserted_person.id, Some(data.password.to_string())) }; - let all_languages = Language::read_all(&mut context.pool()).await?; - // use hashset to avoid duplicates - let mut language_ids = HashSet::new(); - for l in language_tags { - if let Some(found) = all_languages.iter().find(|all| all.code == l) { - language_ids.insert(found.id); - } - } - let language_ids = language_ids.into_iter().collect(); - - let inserted_local_user = - LocalUser::create(&mut context.pool(), &local_user_form, language_ids).await?; + let inserted_local_user = create_local_user(&context, language_tags, &local_user_form).await?; if local_site.site_setup && require_registration_application { // Create the registration application @@ -205,34 +190,405 @@ pub async fn register( let jwt = Claims::generate(inserted_local_user.id, req, &context).await?; login_response.jwt = Some(jwt); } else { - if local_site.require_email_verification { - let local_user_view = LocalUserView { - local_user: inserted_local_user, - local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), - person: inserted_person, - counts: PersonAggregates::default(), - }; - // we check at the beginning of this method that email is set - let email = local_user_view - .local_user - .email - .clone() - .expect("email was provided"); + login_response.verify_email_sent = send_verification_email_if_required( + &context, + &local_site, + &inserted_local_user, + &inserted_person, + ) + .await?; - send_verification_email( - &local_user_view, - &email, - &mut context.pool(), - context.settings(), + if require_registration_application { + login_response.registration_created = true; + } + } + + Ok(Json(login_response)) +} + +#[tracing::instrument(skip(context))] +pub async fn authenticate_with_oauth( + data: Json, + req: HttpRequest, + context: Data, +) -> LemmyResult> { + let site_view = SiteView::read_local(&mut context.pool()).await?; + let local_site = site_view.local_site.clone(); + + // validate inputs + if data.oauth_provider_id == OAuthProviderId(0) || data.code.is_empty() || data.code.len() > 300 { + return Err(LemmyErrorType::OauthAuthorizationInvalid)?; + } + + // validate the redirect_uri + let redirect_uri = &data.redirect_uri; + if redirect_uri.host_str().unwrap_or("").is_empty() + || !redirect_uri.path().eq(&String::from("/oauth/callback")) + || !redirect_uri.query().unwrap_or("").is_empty() + { + Err(LemmyErrorType::OauthAuthorizationInvalid)? + } + + // Fetch the OAUTH provider and make sure it's enabled + let oauth_provider_id = data.oauth_provider_id; + let oauth_provider = OAuthProvider::read(&mut context.pool(), oauth_provider_id) + .await + .ok() + .flatten() + .ok_or(LemmyErrorType::OauthAuthorizationInvalid)?; + + if !oauth_provider.enabled { + return Err(LemmyErrorType::OauthAuthorizationInvalid)?; + } + + let token_response = + oauth_request_access_token(&context, &oauth_provider, &data.code, redirect_uri.as_str()) + .await?; + + let user_info = oidc_get_user_info( + &context, + &oauth_provider, + token_response.access_token.as_str(), + ) + .await?; + + let oauth_user_id = read_user_info(&user_info, oauth_provider.id_claim.as_str())?; + + let mut login_response = LoginResponse { + jwt: None, + registration_created: false, + verify_email_sent: false, + }; + + // Lookup user by oauth_user_id + let mut local_user_view = + LocalUserView::find_by_oauth_id(&mut context.pool(), oauth_provider.id, &oauth_user_id).await?; + + let local_user: LocalUser; + if let Some(user_view) = local_user_view { + // user found by oauth_user_id => Login user + local_user = user_view.clone().local_user; + + check_user_valid(&user_view.person)?; + check_email_verified(&user_view, &site_view)?; + check_registration_application(&user_view, &site_view.local_site, &mut context.pool()).await?; + } else { + // user has never previously registered using oauth + + // prevent registration if registration is closed + if local_site.registration_mode == RegistrationMode::Closed { + Err(LemmyErrorType::RegistrationClosed)? + } + + // prevent registration if registration is closed for OAUTH providers + if !local_site.oauth_registration { + return Err(LemmyErrorType::OauthRegistrationClosed)?; + } + + // Extract the OAUTH email claim from the returned user_info + let email = read_user_info(&user_info, "email")?; + + let require_registration_application = + local_site.registration_mode == RegistrationMode::RequireApplication; + + // Lookup user by OAUTH email and link accounts + local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email).await?; + + let person; + if let Some(user_view) = local_user_view { + // user found by email => link and login if linking is allowed + + // we only allow linking by email when email_verification is required otherwise emails cannot + // be trusted + if oauth_provider.account_linking_enabled && site_view.local_site.require_email_verification { + // WARNING: + // If an admin switches the require_email_verification config from false to true, + // users who signed up before the switch could have accounts with unverified emails falsely + // marked as verified. + + check_user_valid(&user_view.person)?; + check_email_verified(&user_view, &site_view)?; + check_registration_application(&user_view, &site_view.local_site, &mut context.pool()) + .await?; + + // Link with OAUTH => Login user + let oauth_account_form = + OAuthAccountInsertForm::new(user_view.local_user.id, oauth_provider.id, oauth_user_id); + + OAuthAccount::create(&mut context.pool(), &oauth_account_form) + .await + .map_err(|_| LemmyErrorType::OauthLoginFailed)?; + + local_user = user_view.local_user.clone(); + } else { + return Err(LemmyErrorType::EmailAlreadyExists)?; + } + } else { + // No user was found by email => Register as new user + + // make sure the registration answer is provided when the registration application is required + validate_registration_answer(require_registration_application, &data.answer)?; + + // make sure the username is provided + let username = data + .username + .as_ref() + .ok_or(LemmyErrorType::RegistrationUsernameRequired)?; + + let slur_regex = local_site_to_slur_regex(&local_site); + 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)?; + } + + // We have to create a person, a local_user, and an oauth_account + person = create_person( + username.clone(), + &local_site, + site_view.site.instance_id, + &context, ) .await?; - login_response.verify_email_sent = true; + + // Show nsfw content if param is true, or if content_warning exists + let show_nsfw = data + .show_nsfw + .unwrap_or(site_view.site.content_warning.is_some()); + + let language_tags = get_language_tags(&req); + + // Create the local user + let local_user_form = LocalUserInsertForm { + email: Some(str::to_lowercase(&email)), + show_nsfw: Some(show_nsfw), + accepted_application: Some(!require_registration_application), + email_verified: Some(oauth_provider.auto_verify_email), + post_listing_mode: Some(local_site.default_post_listing_mode), + interface_language: language_tags.first().cloned(), + // If its the initial site setup, they are an admin + admin: Some(!local_site.site_setup), + ..LocalUserInsertForm::new(person.id, None) + }; + + local_user = create_local_user(&context, language_tags, &local_user_form).await?; + + // Create the oauth account + let oauth_account_form = + OAuthAccountInsertForm::new(local_user.id, oauth_provider.id, oauth_user_id); + + OAuthAccount::create(&mut context.pool(), &oauth_account_form) + .await + .map_err(|_| LemmyErrorType::IncorrectLogin)?; + + // prevent sign in until application is accepted + if local_site.site_setup + && require_registration_application + && !local_user.accepted_application + && !local_user.admin + { + // Create the registration application + RegistrationApplication::create( + &mut context.pool(), + &RegistrationApplicationInsertForm { + local_user_id: local_user.id, + answer: data.answer.clone().expect("must have an answer"), + }, + ) + .await?; + + login_response.registration_created = true; + } + + // Check email is verified when required + login_response.verify_email_sent = + send_verification_email_if_required(&context, &local_site, &local_user, &person).await?; } + } - if require_registration_application { - login_response.registration_created = true; + if !login_response.registration_created && !login_response.verify_email_sent { + let jwt = Claims::generate(local_user.id, req, &context).await?; + login_response.jwt = Some(jwt); + } + + return Ok(Json(login_response)); +} + +async fn create_person( + username: String, + local_site: &LocalSite, + instance_id: InstanceId, + context: &Data, +) -> Result { + let actor_keypair = generate_actor_keypair()?; + is_valid_actor_name(&username, local_site.actor_name_max_length as usize)?; + let actor_id = generate_local_apub_endpoint( + EndpointType::Person, + &username, + &context.settings().get_protocol_and_hostname(), + )?; + + // Register the new person + let person_form = PersonInsertForm { + actor_id: Some(actor_id.clone()), + inbox_url: Some(generate_inbox_url(&actor_id)?), + shared_inbox_url: Some(generate_shared_inbox_url(context.settings())?), + private_key: Some(actor_keypair.private_key), + ..PersonInsertForm::new(username.clone(), actor_keypair.public_key, instance_id) + }; + + // insert the person + let inserted_person = Person::create(&mut context.pool(), &person_form) + .await + .with_lemmy_type(LemmyErrorType::UserAlreadyExists)?; + + Ok(inserted_person) +} + +fn get_language_tags(req: &HttpRequest) -> Vec { + req + .headers() + .get("Accept-Language") + .map(|hdr| accept_language::parse(hdr.to_str().unwrap_or_default())) + .iter() + .flatten() + // Remove the optional region code + .map(|lang_str| lang_str.split('-').next().unwrap_or_default().to_string()) + .collect::>() +} + +async fn create_local_user( + context: &Data, + language_tags: Vec, + local_user_form: &LocalUserInsertForm, +) -> Result { + let all_languages = Language::read_all(&mut context.pool()).await?; + // use hashset to avoid duplicates + let mut language_ids = HashSet::new(); + for l in language_tags { + if let Some(found) = all_languages.iter().find(|all| all.code == l) { + language_ids.insert(found.id); } } + let language_ids = language_ids.into_iter().collect(); - Ok(Json(login_response)) + let inserted_local_user = + LocalUser::create(&mut context.pool(), local_user_form, language_ids).await?; + + Ok(inserted_local_user) +} + +async fn send_verification_email_if_required( + context: &Data, + local_site: &LocalSite, + local_user: &LocalUser, + person: &Person, +) -> LemmyResult { + let mut sent = false; + if !local_user.admin && local_site.require_email_verification && !local_user.email_verified { + let local_user_view = LocalUserView { + local_user: local_user.clone(), + local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), + person: person.clone(), + counts: PersonAggregates::default(), + }; + + send_verification_email( + &local_user_view, + &local_user + .email + .clone() + .expect("invalid verification email"), + &mut context.pool(), + context.settings(), + ) + .await?; + + sent = true; + } + Ok(sent) +} + +fn validate_registration_answer( + require_registration_application: bool, + answer: &Option, +) -> LemmyResult<()> { + if require_registration_application && answer.is_none() { + Err(LemmyErrorType::RegistrationApplicationAnswerRequired)? + } + + Ok(()) +} + +async fn oauth_request_access_token( + context: &Data, + oauth_provider: &OAuthProvider, + code: &str, + redirect_uri: &str, +) -> LemmyResult { + // Request an Access Token from the OAUTH provider + let response = context + .client() + .post(oauth_provider.token_endpoint.as_str()) + .header("Accept", "application/json") + .form(&[ + ("grant_type", "authorization_code"), + ("code", code), + ("redirect_uri", redirect_uri), + ("client_id", &oauth_provider.client_id), + ("client_secret", &oauth_provider.client_secret), + ]) + .send() + .await; + + let response = response.map_err(|_| LemmyErrorType::OauthLoginFailed)?; + if !response.status().is_success() { + Err(LemmyErrorType::OauthLoginFailed)?; + } + + // Extract the access token + let token_response = response + .json::() + .await + .map_err(|_| LemmyErrorType::OauthLoginFailed)?; + + Ok(token_response) +} + +async fn oidc_get_user_info( + context: &Data, + oauth_provider: &OAuthProvider, + access_token: &str, +) -> LemmyResult { + // Request the user info from the OAUTH provider + let response = context + .client() + .get(oauth_provider.userinfo_endpoint.as_str()) + .header("Accept", "application/json") + .bearer_auth(access_token) + .send() + .await; + + let response = response.map_err(|_| LemmyErrorType::OauthLoginFailed)?; + if !response.status().is_success() { + Err(LemmyErrorType::OauthLoginFailed)?; + } + + // Extract the OAUTH user_id claim from the returned user_info + let user_info = response + .json::() + .await + .map_err(|_| LemmyErrorType::OauthLoginFailed)?; + + Ok(user_info) +} + +fn read_user_info(user_info: &serde_json::Value, key: &str) -> LemmyResult { + if let Some(value) = user_info.get(key) { + let result = serde_json::from_value::(value.clone()) + .map_err(|_| LemmyErrorType::OauthLoginFailed)?; + return Ok(result); + } + Err(LemmyErrorType::OauthLoginFailed)? } diff --git a/crates/api_crud/src/user/delete.rs b/crates/api_crud/src/user/delete.rs index 363230d836..d1825425c8 100644 --- a/crates/api_crud/src/user/delete.rs +++ b/crates/api_crud/src/user/delete.rs @@ -8,7 +8,11 @@ use lemmy_api_common::{ utils::purge_user_account, SuccessResponse, }; -use lemmy_db_schema::source::{login_token::LoginToken, person::Person}; +use lemmy_db_schema::source::{ + login_token::LoginToken, + oauth_account::OAuthAccount, + person::Person, +}; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; @@ -19,11 +23,12 @@ pub async fn delete_account( local_user_view: LocalUserView, ) -> LemmyResult> { // Verify the password - let valid: bool = verify( - &data.password, - &local_user_view.local_user.password_encrypted, - ) - .unwrap_or(false); + let valid: bool = local_user_view + .local_user + .password_encrypted + .as_ref() + .and_then(|password_encrypted| verify(&data.password, password_encrypted).ok()) + .unwrap_or(false); if !valid { Err(LemmyErrorType::IncorrectLogin)? } @@ -31,6 +36,7 @@ pub async fn delete_account( if data.delete_content { purge_user_account(local_user_view.person.id, &context).await?; } else { + OAuthAccount::delete_user_accounts(&mut context.pool(), local_user_view.local_user.id).await?; Person::delete_account(&mut context.pool(), local_user_view.person.id).await?; } diff --git a/crates/db_schema/src/impls/local_user.rs b/crates/db_schema/src/impls/local_user.rs index acff6af2a4..87f2ac6389 100644 --- a/crates/db_schema/src/impls/local_user.rs +++ b/crates/db_schema/src/impls/local_user.rs @@ -35,9 +35,11 @@ impl LocalUser { ) -> Result { let conn = &mut get_conn(pool).await?; let mut form_with_encrypted_password = form.clone(); - let password_hash = - hash(&form.password_encrypted, DEFAULT_COST).expect("Couldn't hash password"); - form_with_encrypted_password.password_encrypted = password_hash; + + if let Some(password_encrypted) = &form.password_encrypted { + let password_hash = hash(password_encrypted, DEFAULT_COST).expect("Couldn't hash password"); + form_with_encrypted_password.password_encrypted = Some(password_hash); + } let local_user_ = insert_into(local_user::table) .values(form_with_encrypted_password) @@ -346,7 +348,7 @@ impl LocalUserOptionHelper for Option<&LocalUser> { impl LocalUserInsertForm { pub fn test_form(person_id: PersonId) -> Self { - Self::new(person_id, String::new()) + Self::new(person_id, Some(String::new())) } pub fn test_form_admin(person_id: PersonId) -> Self { diff --git a/crates/db_schema/src/impls/mod.rs b/crates/db_schema/src/impls/mod.rs index 3a4e71307c..f115a101fe 100644 --- a/crates/db_schema/src/impls/mod.rs +++ b/crates/db_schema/src/impls/mod.rs @@ -22,6 +22,8 @@ pub mod local_user; pub mod local_user_vote_display_mode; pub mod login_token; pub mod moderator; +pub mod oauth_account; +pub mod oauth_provider; pub mod password_reset_request; pub mod person; pub mod person_block; diff --git a/crates/db_schema/src/impls/oauth_account.rs b/crates/db_schema/src/impls/oauth_account.rs new file mode 100644 index 0000000000..921a21d3dc --- /dev/null +++ b/crates/db_schema/src/impls/oauth_account.rs @@ -0,0 +1,59 @@ +use crate::{ + newtypes::{LocalUserId, OAuthProviderId}, + 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_async::RunQueryDsl; + +impl OAuthAccount { + pub async fn read( + pool: &mut DbPool<'_>, + for_oauth_provider_id: OAuthProviderId, + for_local_user_id: LocalUserId, + ) -> Result { + 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 { + let conn = &mut get_conn(pool).await?; + insert_into(oauth_account::table) + .values(form) + .get_result::(conn) + .await + } + + pub async fn delete( + pool: &mut DbPool<'_>, + for_oauth_provider_id: OAuthProviderId, + for_local_user_id: LocalUserId, + ) -> Result { + 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, + ) -> Result { + let conn = &mut get_conn(pool).await?; + + diesel::delete(oauth_account::table.filter(local_user_id.eq(for_local_user_id))) + .execute(conn) + .await + } +} diff --git a/crates/db_schema/src/impls/oauth_provider.rs b/crates/db_schema/src/impls/oauth_provider.rs new file mode 100644 index 0000000000..9d7e791e73 --- /dev/null +++ b/crates/db_schema/src/impls/oauth_provider.rs @@ -0,0 +1,71 @@ +use crate::{ + newtypes::OAuthProviderId, + schema::oauth_provider, + source::oauth_provider::{ + OAuthProvider, + OAuthProviderInsertForm, + OAuthProviderUpdateForm, + PublicOAuthProvider, + }, + traits::Crud, + utils::{get_conn, DbPool}, +}; +use diesel::{dsl::insert_into, result::Error, QueryDsl}; +use diesel_async::RunQueryDsl; + +#[async_trait] +impl Crud for OAuthProvider { + type InsertForm = OAuthProviderInsertForm; + type UpdateForm = OAuthProviderUpdateForm; + type IdType = OAuthProviderId; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { + let conn = &mut get_conn(pool).await?; + insert_into(oauth_provider::table) + .values(form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + oauth_provider_id: OAuthProviderId, + form: &Self::UpdateForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + diesel::update(oauth_provider::table.find(oauth_provider_id)) + .set(form) + .get_result::(conn) + .await + } +} + +impl OAuthProvider { + pub async fn get_all(pool: &mut DbPool<'_>) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + let oauth_providers = oauth_provider::table + .order(oauth_provider::id) + .select(oauth_provider::all_columns) + .load::(conn) + .await?; + + Ok(oauth_providers) + } + + pub fn convert_providers_to_public( + oauth_providers: Vec, + ) -> Vec { + let mut result = Vec::::new(); + for oauth_provider in &oauth_providers { + if oauth_provider.enabled { + result.push(PublicOAuthProvider(oauth_provider.clone())); + } + } + result + } + + pub async fn get_all_public(pool: &mut DbPool<'_>) -> Result, Error> { + let oauth_providers = OAuthProvider::get_all(pool).await?; + Ok(Self::convert_providers_to_public(oauth_providers)) + } +} diff --git a/crates/db_schema/src/impls/person.rs b/crates/db_schema/src/impls/person.rs index f2909218ca..312bbcf219 100644 --- a/crates/db_schema/src/impls/person.rs +++ b/crates/db_schema/src/impls/person.rs @@ -121,6 +121,18 @@ impl Person { .load::(conn) .await } + + pub async fn is_username_taken(pool: &mut DbPool<'_>, username: &str) -> Result { + use diesel::dsl::{exists, select}; + let conn = &mut get_conn(pool).await?; + select(exists( + person::table + .filter(lower(person::name).eq(username.to_lowercase())) + .filter(person::local.eq(true)), + )) + .get_result(conn) + .await + } } impl PersonInsertForm { diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index c715305bba..d90b1f3f6a 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -154,6 +154,12 @@ pub struct CustomEmojiId(i32); /// The registration application id. pub struct RegistrationApplicationId(i32); +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The oauth provider id. +pub struct OAuthProviderId(pub i32); + #[cfg(feature = "full")] #[derive(Serialize, Deserialize)] #[serde(remote = "Ltree")] diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index aa143c4c9b..de3e4fa48c 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -392,6 +392,7 @@ diesel::table! { federation_signed_fetch -> Bool, default_post_listing_mode -> PostListingModeEnum, default_sort_type -> SortTypeEnum, + oauth_registration -> Bool, } } @@ -435,7 +436,7 @@ diesel::table! { local_user (id) { id -> Int4, person_id -> Int4, - password_encrypted -> Text, + password_encrypted -> Nullable, email -> Nullable, show_nsfw -> Bool, theme -> Text, @@ -611,6 +612,36 @@ diesel::table! { } } +diesel::table! { + oauth_account (oauth_provider_id, local_user_id) { + local_user_id -> Int4, + oauth_provider_id -> Int4, + oauth_user_id -> Text, + published -> Timestamptz, + updated -> Nullable, + } +} + +diesel::table! { + oauth_provider (id) { + id -> Int4, + display_name -> Text, + issuer -> Text, + authorization_endpoint -> Text, + token_endpoint -> Text, + userinfo_endpoint -> Text, + id_claim -> Text, + client_id -> Text, + client_secret -> Text, + scopes -> Text, + auto_verify_email -> Bool, + account_linking_enabled -> Bool, + enabled -> Bool, + published -> Timestamptz, + updated -> Nullable, + } +} + diesel::table! { password_reset_request (id) { id -> Int4, @@ -1003,6 +1034,8 @@ diesel::joinable!(mod_remove_community -> person (mod_person_id)); diesel::joinable!(mod_remove_post -> person (mod_person_id)); diesel::joinable!(mod_remove_post -> post (post_id)); diesel::joinable!(mod_transfer_community -> community (community_id)); +diesel::joinable!(oauth_account -> local_user (local_user_id)); +diesel::joinable!(oauth_account -> oauth_provider (oauth_provider_id)); diesel::joinable!(password_reset_request -> local_user (local_user_id)); diesel::joinable!(person -> instance (instance_id)); diesel::joinable!(person_aggregates -> person (person_id)); @@ -1084,6 +1117,8 @@ diesel::allow_tables_to_appear_in_same_query!( mod_remove_community, mod_remove_post, mod_transfer_community, + oauth_account, + oauth_provider, password_reset_request, person, person_aggregates, diff --git a/crates/db_schema/src/source/local_site.rs b/crates/db_schema/src/source/local_site.rs index 21af1f6cac..8dc81a9a55 100644 --- a/crates/db_schema/src/source/local_site.rs +++ b/crates/db_schema/src/source/local_site.rs @@ -68,6 +68,8 @@ pub struct LocalSite { pub default_post_listing_mode: PostListingMode, /// Default value for [LocalUser.post_listing_mode] pub default_sort_type: SortType, + /// Whether or not external auth methods can auto-register users. + pub oauth_registration: bool, } #[derive(Clone, TypedBuilder)] @@ -94,6 +96,7 @@ pub struct LocalSiteInsertForm { pub captcha_enabled: Option, pub captcha_difficulty: Option, pub registration_mode: Option, + pub oauth_registration: Option, pub reports_email_admins: Option, pub federation_signed_fetch: Option, pub default_post_listing_mode: Option, @@ -121,6 +124,7 @@ pub struct LocalSiteUpdateForm { pub captcha_enabled: Option, pub captcha_difficulty: Option, pub registration_mode: Option, + pub oauth_registration: Option, pub reports_email_admins: Option, pub updated: Option>>, pub federation_signed_fetch: Option, diff --git a/crates/db_schema/src/source/local_user.rs b/crates/db_schema/src/source/local_user.rs index 89bdb1b55e..e184d3605b 100644 --- a/crates/db_schema/src/source/local_user.rs +++ b/crates/db_schema/src/source/local_user.rs @@ -24,7 +24,7 @@ pub struct LocalUser { /// The person_id for the local user. pub person_id: PersonId, #[serde(skip)] - pub password_encrypted: SensitiveString, + pub password_encrypted: Option, pub email: Option, /// Whether to show NSFW content. pub show_nsfw: bool, @@ -70,7 +70,7 @@ pub struct LocalUser { #[cfg_attr(feature = "full", diesel(table_name = local_user))] pub struct LocalUserInsertForm { pub person_id: PersonId, - pub password_encrypted: String, + pub password_encrypted: Option, #[new(default)] pub email: Option, #[new(default)] diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index bbc8aafa29..377c1aaefa 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -27,6 +27,8 @@ pub mod local_user; pub mod local_user_vote_display_mode; pub mod login_token; pub mod moderator; +pub mod oauth_account; +pub mod oauth_provider; pub mod password_reset_request; pub mod person; pub mod person_block; diff --git a/crates/db_schema/src/source/oauth_account.rs b/crates/db_schema/src/source/oauth_account.rs new file mode 100644 index 0000000000..83b578e229 --- /dev/null +++ b/crates/db_schema/src/source/oauth_account.rs @@ -0,0 +1,32 @@ +use crate::newtypes::{LocalUserId, OAuthProviderId}; +#[cfg(feature = "full")] +use crate::schema::oauth_account; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[skip_serializing_none] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, TS))] +#[cfg_attr(feature = "full", diesel(table_name = oauth_account))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// An auth account method. +pub struct OAuthAccount { + pub local_user_id: LocalUserId, + pub oauth_provider_id: OAuthProviderId, + pub oauth_user_id: String, + pub published: DateTime, + pub updated: Option>, +} + +#[derive(Debug, Clone, derive_new::new)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = oauth_account))] +pub struct OAuthAccountInsertForm { + pub local_user_id: LocalUserId, + pub oauth_provider_id: OAuthProviderId, + pub oauth_user_id: String, +} diff --git a/crates/db_schema/src/source/oauth_provider.rs b/crates/db_schema/src/source/oauth_provider.rs new file mode 100644 index 0000000000..40046c83c7 --- /dev/null +++ b/crates/db_schema/src/source/oauth_provider.rs @@ -0,0 +1,131 @@ +#[cfg(feature = "full")] +use crate::schema::oauth_provider; +use crate::{ + newtypes::{DbUrl, OAuthProviderId}, + sensitive::SensitiveString, +}; +use chrono::{DateTime, Utc}; +use serde::{ + ser::{SerializeStruct, Serializer}, + Deserialize, + Serialize, +}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[skip_serializing_none] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] +#[cfg_attr(feature = "full", diesel(table_name = oauth_provider))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// oauth provider with client_secret - should never be sent to the client +pub struct OAuthProvider { + pub id: OAuthProviderId, + /// The OAuth 2.0 provider name displayed to the user on the Login page + pub display_name: String, + /// The issuer url of the OAUTH provider. + #[cfg_attr(feature = "full", ts(type = "string"))] + pub issuer: DbUrl, + /// The authorization endpoint is used to interact with the resource owner and obtain an + /// authorization grant. This is usually provided by the OAUTH provider. + #[cfg_attr(feature = "full", ts(type = "string"))] + pub authorization_endpoint: DbUrl, + /// The token endpoint is used by the client to obtain an access token by presenting its + /// authorization grant or refresh token. This is usually provided by the OAUTH provider. + #[cfg_attr(feature = "full", ts(type = "string"))] + pub token_endpoint: DbUrl, + /// The UserInfo Endpoint is an OAuth 2.0 Protected Resource that returns Claims about the + /// authenticated End-User. This is defined in the OIDC specification. + #[cfg_attr(feature = "full", ts(type = "string"))] + pub userinfo_endpoint: DbUrl, + /// The OAuth 2.0 claim containing the unique user ID returned by the provider. Usually this + /// should be set to "sub". + pub id_claim: String, + /// The client_id is provided by the OAuth 2.0 provider and is a unique identifier to this + /// service + pub client_id: String, + /// The client_secret is provided by the OAuth 2.0 provider and is used to authenticate this + /// service with the provider + #[serde(skip)] + pub client_secret: SensitiveString, + /// Lists the scopes requested from users. Users will have to grant access to the requested scope + /// at sign up. + pub scopes: String, + /// Automatically sets email as verified on registration + pub auto_verify_email: bool, + /// Allows linking an OAUTH account to an existing user account by matching emails + pub account_linking_enabled: bool, + /// switch to enable or disable an oauth provider + pub enabled: bool, + pub published: DateTime, + pub updated: Option>, +} + +#[derive(Clone, PartialEq, Eq, Debug, Deserialize)] +#[serde(transparent)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +// A subset of OAuthProvider used for public requests, for example to display the OAUTH buttons on +// the login page +pub struct PublicOAuthProvider(pub OAuthProvider); + +impl Serialize for PublicOAuthProvider { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("PublicOAuthProvider", 5)?; + state.serialize_field("id", &self.0.id)?; + state.serialize_field("display_name", &self.0.display_name)?; + state.serialize_field("authorization_endpoint", &self.0.authorization_endpoint)?; + state.serialize_field("client_id", &self.0.client_id)?; + state.serialize_field("scopes", &self.0.scopes)?; + state.end() + } +} + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset, TS))] +#[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, +} + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset, TS))] +#[cfg_attr(feature = "full", diesel(table_name = oauth_provider))] +#[cfg_attr(feature = "full", ts(export))] +pub struct OAuthProviderUpdateForm { + pub display_name: Option, + #[cfg_attr(feature = "full", ts(type = "string"))] + pub authorization_endpoint: Option, + #[cfg_attr(feature = "full", ts(type = "string"))] + pub token_endpoint: Option, + #[cfg_attr(feature = "full", ts(type = "string"))] + pub userinfo_endpoint: Option, + pub id_claim: Option, + pub client_secret: Option, + pub scopes: Option, + pub auto_verify_email: Option, + pub account_linking_enabled: Option, + pub enabled: Option, + pub updated: Option>>, +} diff --git a/crates/db_schema/src/utils.rs b/crates/db_schema/src/utils.rs index 8e4e35006f..a174e3cb90 100644 --- a/crates/db_schema/src/utils.rs +++ b/crates/db_schema/src/utils.rs @@ -288,7 +288,7 @@ pub fn is_email_regex(test: &str) -> bool { EMAIL_REGEX.is_match(test) } -/// Takes an API text input, and converts it to an optional diesel DB update. +/// Takes an API optional text input, and converts it to an optional diesel DB update. pub fn diesel_string_update(opt: Option<&str>) -> Option> { match opt { // An empty string is an erase @@ -298,6 +298,17 @@ pub fn diesel_string_update(opt: Option<&str>) -> Option> { } } +/// Takes an API optional text input, and converts it to an optional diesel DB update (for non +/// nullable properties). +pub fn diesel_required_string_update(opt: Option<&str>) -> Option { + match opt { + // An empty string is no change + Some("") => None, + Some(str) => Some(str.into()), + None => None, + } +} + /// Takes an optional API URL-type input, and converts it to an optional diesel DB update. /// Also cleans the url params. pub fn diesel_url_update(opt: Option<&str>) -> LemmyResult>> { @@ -311,6 +322,19 @@ pub fn diesel_url_update(opt: Option<&str>) -> LemmyResult> } } +/// Takes an optional API URL-type input, and converts it to an optional diesel DB update (for non +/// nullable properties). Also cleans the url params. +pub fn diesel_required_url_update(opt: Option<&str>) -> LemmyResult> { + match opt { + // An empty string is no change + Some("") => Ok(None), + Some(str_url) => Url::parse(str_url) + .map(|u| Some(clean_url(&u).into())) + .with_lemmy_type(LemmyErrorType::InvalidUrl), + None => Ok(None), + } +} + /// Takes an optional API URL-type input, and converts it to an optional diesel DB create. /// Also cleans the url params. pub fn diesel_url_create(opt: Option<&str>) -> LemmyResult> { diff --git a/crates/db_views/src/local_user_view.rs b/crates/db_views/src/local_user_view.rs index 0c13b0a684..b20dfe2357 100644 --- a/crates/db_views/src/local_user_view.rs +++ b/crates/db_views/src/local_user_view.rs @@ -3,8 +3,8 @@ use actix_web::{dev::Payload, FromRequest, HttpMessage, HttpRequest}; use diesel::{result::Error, BoolExpressionMethods, ExpressionMethods, JoinOnDsl, QueryDsl}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ - newtypes::{LocalUserId, PersonId}, - schema::{local_user, local_user_vote_display_mode, person, person_aggregates}, + newtypes::{LocalUserId, OAuthProviderId, PersonId}, + schema::{local_user, local_user_vote_display_mode, oauth_account, person, person_aggregates}, utils::{ functions::{coalesce, lower}, DbConn, @@ -23,6 +23,7 @@ enum ReadBy<'a> { Name(&'a str), NameOrEmail(&'a str), Email(&'a str), + OAuthID(OAuthProviderId, &'a str), } enum ListMode { @@ -58,12 +59,21 @@ fn queries<'a>( ), _ => query, }; - query + let query = query .inner_join(local_user_vote_display_mode::table) - .inner_join(person_aggregates::table.on(person::id.eq(person_aggregates::person_id))) - .select(selection) - .first(&mut conn) - .await + .inner_join(person_aggregates::table.on(person::id.eq(person_aggregates::person_id))); + + if let ReadBy::OAuthID(oauth_provider_id, oauth_user_id) = search { + query + .inner_join(oauth_account::table) + .filter(oauth_account::oauth_provider_id.eq(oauth_provider_id)) + .filter(oauth_account::oauth_user_id.eq(oauth_user_id)) + .select(selection) + .first(&mut conn) + .await + } else { + query.select(selection).first(&mut conn).await + } }; let list = move |mut conn: DbConn<'a>, mode: ListMode| async move { @@ -120,6 +130,16 @@ impl LocalUserView { queries().read(pool, ReadBy::Email(from_email)).await } + pub async fn find_by_oauth_id( + pool: &mut DbPool<'_>, + oauth_provider_id: OAuthProviderId, + oauth_user_id: &str, + ) -> Result, Error> { + queries() + .read(pool, ReadBy::OAuthID(oauth_provider_id, oauth_user_id)) + .await + } + pub async fn list_admins_with_emails(pool: &mut DbPool<'_>) -> Result, Error> { queries().list(pool, ListMode::AdminsWithEmails).await } diff --git a/crates/routes/src/images.rs b/crates/routes/src/images.rs index 768a607c26..a0f804b6ba 100644 --- a/crates/routes/src/images.rs +++ b/crates/routes/src/images.rs @@ -5,8 +5,7 @@ use actix_web::{ Method, StatusCode, }, - web, - web::Query, + web::{self, Query}, HttpRequest, HttpResponse, }; diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index 4e634bde38..1935e41324 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -55,6 +55,7 @@ pub enum LemmyErrorType { CouldntFindCommentReply, CouldntFindPrivateMessage, CouldntFindActivity, + CouldntFindOauthProvider, PersonIsBlocked, CommunityIsBlocked, InstanceIsBlocked, @@ -83,7 +84,9 @@ pub enum LemmyErrorType { InvalidDefaultPostListingType, RegistrationClosed, RegistrationApplicationAnswerRequired, + RegistrationUsernameRequired, EmailAlreadyExists, + UsernameAlreadyExists, FederationForbiddenByStrictAllowList, PersonIsBannedFromCommunity, ObjectIsNotPublic, @@ -178,6 +181,10 @@ pub enum LemmyErrorType { CantBlockLocalInstance, UrlWithoutDomain, InboxTimeout, + OauthAuthorizationInvalid, + OauthLoginFailed, + OauthRegistrationClosed, + CouldntDeleteOauthProvider, Unknown(String), CantDeleteSite, UrlLengthOverflow, diff --git a/migrations/2024-09-16-174833_create_oauth_provider/down.sql b/migrations/2024-09-16-174833_create_oauth_provider/down.sql new file mode 100644 index 0000000000..d1e62bc461 --- /dev/null +++ b/migrations/2024-09-16-174833_create_oauth_provider/down.sql @@ -0,0 +1,10 @@ +DROP TABLE oauth_account; + +DROP TABLE oauth_provider; + +ALTER TABLE local_site + DROP COLUMN oauth_registration; + +ALTER TABLE local_user + ALTER COLUMN password_encrypted SET NOT NULL; + diff --git a/migrations/2024-09-16-174833_create_oauth_provider/up.sql b/migrations/2024-09-16-174833_create_oauth_provider/up.sql new file mode 100644 index 0000000000..a75f012283 --- /dev/null +++ b/migrations/2024-09-16-174833_create_oauth_provider/up.sql @@ -0,0 +1,34 @@ +ALTER TABLE local_user + ALTER COLUMN password_encrypted DROP NOT NULL; + +CREATE TABLE oauth_provider ( + id serial PRIMARY KEY, + display_name text NOT NULL, + issuer text NOT NULL, + authorization_endpoint text NOT NULL, + token_endpoint text NOT NULL, + userinfo_endpoint text NOT NULL, + id_claim text NOT NULL, + client_id text NOT NULL UNIQUE, + client_secret text NOT NULL, + 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, + published timestamp with time zone DEFAULT now() NOT NULL, + updated timestamp with time zone +); + +ALTER TABLE local_site + ADD COLUMN oauth_registration boolean DEFAULT FALSE NOT NULL; + +CREATE TABLE oauth_account ( + local_user_id int REFERENCES local_user ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + oauth_provider_id int REFERENCES oauth_provider ON UPDATE CASCADE ON DELETE RESTRICT NOT NULL, + oauth_user_id text NOT NULL, + published timestamp with time zone DEFAULT now() NOT NULL, + updated timestamp with time zone, + UNIQUE (oauth_provider_id, oauth_user_id), + PRIMARY KEY (oauth_provider_id, local_user_id) +); + diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index 7b4b34158f..faa7d78f2b 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -109,6 +109,11 @@ use lemmy_api_crud::{ delete::delete_custom_emoji, update::update_custom_emoji, }, + oauth_provider::{ + create::create_oauth_provider, + delete::delete_oauth_provider, + update::update_oauth_provider, + }, post::{ create::create_post, delete::delete_post, @@ -123,7 +128,10 @@ use lemmy_api_crud::{ update::update_private_message, }, site::{create::create_site, read::get_site, update::update_site}, - user::{create::register, delete::delete_account}, + user::{ + create::{authenticate_with_oauth, register}, + delete::delete_account, + }, }; use lemmy_apub::api::{ list_comments::list_comments, @@ -381,6 +389,18 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { .route("", web::post().to(create_custom_emoji)) .route("", web::put().to(update_custom_emoji)) .route("/delete", web::post().to(delete_custom_emoji)), + ) + .service( + web::scope("/oauth_provider") + .wrap(rate_limit.message()) + .route("", web::post().to(create_oauth_provider)) + .route("", web::put().to(update_oauth_provider)) + .route("/delete", web::post().to(delete_oauth_provider)), + ) + .service( + web::scope("/oauth") + .wrap(rate_limit.register()) + .route("/authenticate", web::post().to(authenticate_with_oauth)), ), ); cfg.service( diff --git a/src/code_migrations.rs b/src/code_migrations.rs index 12a688b80b..bc03d513ad 100644 --- a/src/code_migrations.rs +++ b/src/code_migrations.rs @@ -471,7 +471,7 @@ async fn initialize_local_site_2022_10_10( let local_user_form = LocalUserInsertForm { email: setup.admin_email.clone(), admin: Some(true), - ..LocalUserInsertForm::new(person_inserted.id, setup.admin_password.clone()) + ..LocalUserInsertForm::new(person_inserted.id, Some(setup.admin_password.clone())) }; LocalUser::create(pool, &local_user_form, vec![]).await?; }; From 2b3fd70afddbe93b60f61ad92acbf246ee0a2206 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Wed, 18 Sep 2024 09:11:42 -0400 Subject: [PATCH 2/8] Adding ability to restore content on user unban. (#4845) * Adding ability to restore content on user unban. - Fixes #4721 * Fixing api tests. * Fix package.json * Fixing lemmy-js-client dep. * Adding API test for restoring content. --- api_tests/package.json | 2 +- api_tests/pnpm-lock.yaml | 263 +++++++++--------- api_tests/src/post.spec.ts | 9 +- api_tests/src/shared.ts | 8 +- crates/api/src/community/ban.rs | 20 +- crates/api/src/lib.rs | 4 +- crates/api/src/local_user/ban_person.rs | 17 +- crates/api/src/site/purge/person.rs | 2 +- crates/api_common/src/community.rs | 4 +- crates/api_common/src/person.rs | 5 +- crates/api_common/src/send_activity.rs | 2 +- crates/api_common/src/utils.rs | 23 +- .../apub/src/activities/block/block_user.rs | 11 +- crates/apub/src/activities/block/mod.rs | 8 +- .../src/activities/block/undo_block_user.rs | 21 +- crates/apub/src/activities/mod.rs | 4 +- .../activities/block/undo_block_user.rs | 4 + 17 files changed, 239 insertions(+), 168 deletions(-) diff --git a/api_tests/package.json b/api_tests/package.json index bc2d3ec2d6..cc58c83e99 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -27,7 +27,7 @@ "eslint": "^9.8.0", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.5.0", - "lemmy-js-client": "0.19.5-alpha.1", + "lemmy-js-client": "0.20.0-alpha.4", "prettier": "^3.2.5", "ts-jest": "^29.1.0", "typescript": "^5.5.4", diff --git a/api_tests/pnpm-lock.yaml b/api_tests/pnpm-lock.yaml index 3b19706816..0b6a19f812 100644 --- a/api_tests/pnpm-lock.yaml +++ b/api_tests/pnpm-lock.yaml @@ -13,37 +13,37 @@ importers: version: 29.5.12 '@types/node': specifier: ^22.0.2 - version: 22.5.1 + version: 22.3.0 '@typescript-eslint/eslint-plugin': specifier: ^8.0.0 - version: 8.0.0(@typescript-eslint/parser@8.0.0(eslint@9.9.1)(typescript@5.5.4))(eslint@9.9.1)(typescript@5.5.4) + version: 8.1.0(@typescript-eslint/parser@8.1.0(eslint@9.9.0)(typescript@5.5.4))(eslint@9.9.0)(typescript@5.5.4) '@typescript-eslint/parser': specifier: ^8.0.0 - version: 8.0.0(eslint@9.9.1)(typescript@5.5.4) + version: 8.1.0(eslint@9.9.0)(typescript@5.5.4) eslint: specifier: ^9.8.0 - version: 9.9.1 + version: 9.9.0 eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.2.1(eslint@9.9.1)(prettier@3.3.3) + version: 5.2.1(eslint@9.9.0)(prettier@3.3.3) jest: specifier: ^29.5.0 - version: 29.7.0(@types/node@22.5.1) + version: 29.7.0(@types/node@22.3.0) lemmy-js-client: - specifier: 0.19.5-alpha.1 - version: 0.19.5-alpha.1 + specifier: 0.20.0-alpha.4 + version: 0.20.0-alpha.4 prettier: specifier: ^3.2.5 version: 3.3.3 ts-jest: specifier: ^29.1.0 - version: 29.2.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.5.1))(typescript@5.5.4) + version: 29.2.4(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.3.0))(typescript@5.5.4) typescript: specifier: ^5.5.4 version: 5.5.4 typescript-eslint: specifier: ^8.0.0 - version: 8.0.0(eslint@9.9.1)(typescript@5.5.4) + version: 8.1.0(eslint@9.9.0)(typescript@5.5.4) packages: @@ -228,16 +228,16 @@ packages: resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.18.0': - resolution: {integrity: sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==} + '@eslint/config-array@0.17.1': + resolution: {integrity: sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/eslintrc@3.1.0': resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.9.1': - resolution: {integrity: sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==} + '@eslint/js@9.9.0': + resolution: {integrity: sha512-hhetes6ZHP3BlXLxmd8K2SNgkhNSi+UcecbnwWKwpP7kyi/uC75DJ1lOOBO3xrC4jyojtGE3YxKZPHfk4yrgug==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.4': @@ -396,8 +396,8 @@ packages: '@types/jest@29.5.12': resolution: {integrity: sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==} - '@types/node@22.5.1': - resolution: {integrity: sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==} + '@types/node@22.3.0': + resolution: {integrity: sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g==} '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -408,8 +408,8 @@ packages: '@types/yargs@17.0.32': resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} - '@typescript-eslint/eslint-plugin@8.0.0': - resolution: {integrity: sha512-STIZdwEQRXAHvNUS6ILDf5z3u95Gc8jzywunxSNqX00OooIemaaNIA0vEgynJlycL5AjabYLLrIyHd4iazyvtg==} + '@typescript-eslint/eslint-plugin@8.1.0': + resolution: {integrity: sha512-LlNBaHFCEBPHyD4pZXb35mzjGkuGKXU5eeCA1SxvHfiRES0E82dOounfVpL4DCqYvJEKab0bZIA0gCRpdLKkCw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 @@ -419,8 +419,8 @@ packages: typescript: optional: true - '@typescript-eslint/parser@8.0.0': - resolution: {integrity: sha512-pS1hdZ+vnrpDIxuFXYQpLTILglTjSYJ9MbetZctrUawogUsPdz31DIIRZ9+rab0LhYNTsk88w4fIzVheiTbWOQ==} + '@typescript-eslint/parser@8.1.0': + resolution: {integrity: sha512-U7iTAtGgJk6DPX9wIWPPOlt1gO57097G06gIcl0N0EEnNw8RGD62c+2/DiP/zL7KrkqnnqF7gtFGR7YgzPllTA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -429,12 +429,12 @@ packages: typescript: optional: true - '@typescript-eslint/scope-manager@8.0.0': - resolution: {integrity: sha512-V0aa9Csx/ZWWv2IPgTfY7T4agYwJyILESu/PVqFtTFz9RIS823mAze+NbnBI8xiwdX3iqeQbcTYlvB04G9wyQw==} + '@typescript-eslint/scope-manager@8.1.0': + resolution: {integrity: sha512-DsuOZQji687sQUjm4N6c9xABJa7fjvfIdjqpSIIVOgaENf2jFXiM9hIBZOL3hb6DHK9Nvd2d7zZnoMLf9e0OtQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@8.0.0': - resolution: {integrity: sha512-mJAFP2mZLTBwAn5WI4PMakpywfWFH5nQZezUQdSKV23Pqo6o9iShQg1hP2+0hJJXP2LnZkWPphdIq4juYYwCeg==} + '@typescript-eslint/type-utils@8.1.0': + resolution: {integrity: sha512-oLYvTxljVvsMnldfl6jIKxTaU7ok7km0KDrwOt1RHYu6nxlhN3TIx8k5Q52L6wR33nOwDgM7VwW1fT1qMNfFIA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -442,12 +442,12 @@ packages: typescript: optional: true - '@typescript-eslint/types@8.0.0': - resolution: {integrity: sha512-wgdSGs9BTMWQ7ooeHtu5quddKKs5Z5dS+fHLbrQI+ID0XWJLODGMHRfhwImiHoeO2S5Wir2yXuadJN6/l4JRxw==} + '@typescript-eslint/types@8.1.0': + resolution: {integrity: sha512-q2/Bxa0gMOu/2/AKALI0tCKbG2zppccnRIRCW6BaaTlRVaPKft4oVYPp7WOPpcnsgbr0qROAVCVKCvIQ0tbWog==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.0.0': - resolution: {integrity: sha512-5b97WpKMX+Y43YKi4zVcCVLtK5F98dFls3Oxui8LbnmRsseKenbbDinmvxrWegKDMmlkIq/XHuyy0UGLtpCDKg==} + '@typescript-eslint/typescript-estree@8.1.0': + resolution: {integrity: sha512-NTHhmufocEkMiAord/g++gWKb0Fr34e9AExBRdqgWdVBaKoei2dIyYKD9Q0jBnvfbEA5zaf8plUFMUH6kQ0vGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -455,14 +455,14 @@ packages: typescript: optional: true - '@typescript-eslint/utils@8.0.0': - resolution: {integrity: sha512-k/oS/A/3QeGLRvOWCg6/9rATJL5rec7/5s1YmdS0ZU6LHveJyGFwBvLhSRBv6i9xaj7etmosp+l+ViN1I9Aj/Q==} + '@typescript-eslint/utils@8.1.0': + resolution: {integrity: sha512-ypRueFNKTIFwqPeJBfeIpxZ895PQhNyH4YID6js0UoBImWYoSjBsahUn9KMiJXh94uOjVBgHD9AmkyPsPnFwJA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - '@typescript-eslint/visitor-keys@8.0.0': - resolution: {integrity: sha512-oN0K4nkHuOyF3PVMyETbpP5zp6wfyOvm7tWhTMfoqxSSsPmJIh6JNASuZDlODE8eE+0EB9uar+6+vxr9DBTYOA==} + '@typescript-eslint/visitor-keys@8.1.0': + resolution: {integrity: sha512-ba0lNI19awqZ5ZNKh6wCModMwoZs457StTebQ0q1NP58zSi2F6MOZRXwfKZy+jB78JNJ/WH8GSh2IQNzXX8Nag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} acorn-jsx@5.3.2: @@ -512,8 +512,8 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} - async@3.2.6: - resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + async@3.2.5: + resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} babel-jest@29.7.0: resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} @@ -741,8 +741,8 @@ packages: resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.9.1: - resolution: {integrity: sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==} + eslint@9.9.0: + resolution: {integrity: sha512-JfiKJrbx0506OEerjK2Y1QlldtBxkAlLxT5OEcRF8uaQ86noDe2k31Vw9rnSWv+MXZHj7OOUV/dA0AhdLFcyvA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -916,10 +916,6 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} - ignore@5.3.1: - resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} - engines: {node: '>= 4'} - ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1179,8 +1175,8 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - lemmy-js-client@0.19.5-alpha.1: - resolution: {integrity: sha512-GOhaiTQzrpwdmc3DFYemT2SmNmpuQJe2BWUms9QOzdYlkA1WZ0uu7axPE3s+T5OOxfy7K9Q2gsLe72dcVSlffw==} + lemmy-js-client@0.20.0-alpha.4: + resolution: {integrity: sha512-MuVE8u/IFz59ks2vxOUXMRU8x6SIz3Keu4zRhmhNt+n+gI25JAggsTDrtxVCBtrqBhFEe+a3PdXdwm+fXbs0Dw==} leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} @@ -1412,6 +1408,11 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.6.2: + resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} + engines: {node: '>=10'} + hasBin: true + semver@7.6.3: resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} @@ -1517,8 +1518,8 @@ packages: peerDependencies: typescript: '>=4.2.0' - ts-jest@29.2.5: - resolution: {integrity: sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==} + ts-jest@29.2.4: + resolution: {integrity: sha512-3d6tgDyhCI29HlpwIq87sNuI+3Q6GLTTCeYRHCs7vDz+/3GCMwEtV9jezLyl4ZtnBgx00I7hm8PCP8cTksMGrw==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -1556,8 +1557,8 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} - typescript-eslint@8.0.0: - resolution: {integrity: sha512-yQWBJutWL1PmpmDddIOl9/Mi6vZjqNCjqSGBMQ4vsc2Aiodk0SnbQQWPXbSy0HNuKCuGkw1+u4aQ2mO40TdhDQ==} + typescript-eslint@8.1.0: + resolution: {integrity: sha512-prB2U3jXPJLpo1iVLN338Lvolh6OrcCZO+9Yv6AR+tvegPPptYCDBIHiEEUdqRi8gAv2bXNKfMUrgAd2ejn/ow==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -1570,8 +1571,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici-types@6.18.2: + resolution: {integrity: sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ==} update-browserslist-db@1.0.13: resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} @@ -1834,14 +1835,14 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@eslint-community/eslint-utils@4.4.0(eslint@9.9.1)': + '@eslint-community/eslint-utils@4.4.0(eslint@9.9.0)': dependencies: - eslint: 9.9.1 + eslint: 9.9.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.11.0': {} - '@eslint/config-array@0.18.0': + '@eslint/config-array@0.17.1': dependencies: '@eslint/object-schema': 2.1.4 debug: 4.3.6 @@ -1863,7 +1864,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.9.1': {} + '@eslint/js@9.9.0': {} '@eslint/object-schema@2.1.4': {} @@ -1884,7 +1885,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 22.5.1 + '@types/node': 22.3.0 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -1897,14 +1898,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.5.1 + '@types/node': 22.3.0 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.5.1) + jest-config: 29.7.0(@types/node@22.3.0) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -1929,7 +1930,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.5.1 + '@types/node': 22.3.0 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -1947,7 +1948,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.5.1 + '@types/node': 22.3.0 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -1969,7 +1970,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.22 - '@types/node': 22.5.1 + '@types/node': 22.3.0 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -2039,7 +2040,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.5.1 + '@types/node': 22.3.0 '@types/yargs': 17.0.32 chalk: 4.1.2 @@ -2107,7 +2108,7 @@ snapshots: '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.5.1 + '@types/node': 22.3.0 '@types/istanbul-lib-coverage@2.0.6': {} @@ -2124,9 +2125,9 @@ snapshots: expect: 29.7.0 pretty-format: 29.7.0 - '@types/node@22.5.1': + '@types/node@22.3.0': dependencies: - undici-types: 6.19.8 + undici-types: 6.18.2 '@types/stack-utils@2.0.3': {} @@ -2136,17 +2137,17 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.0.0(@typescript-eslint/parser@8.0.0(eslint@9.9.1)(typescript@5.5.4))(eslint@9.9.1)(typescript@5.5.4)': + '@typescript-eslint/eslint-plugin@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)': dependencies: '@eslint-community/regexpp': 4.11.0 - '@typescript-eslint/parser': 8.0.0(eslint@9.9.1)(typescript@5.5.4) - '@typescript-eslint/scope-manager': 8.0.0 - '@typescript-eslint/type-utils': 8.0.0(eslint@9.9.1)(typescript@5.5.4) - '@typescript-eslint/utils': 8.0.0(eslint@9.9.1)(typescript@5.5.4) - '@typescript-eslint/visitor-keys': 8.0.0 - eslint: 9.9.1 + '@typescript-eslint/parser': 8.1.0(eslint@9.9.0)(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.1.0 + '@typescript-eslint/type-utils': 8.1.0(eslint@9.9.0)(typescript@5.5.4) + '@typescript-eslint/utils': 8.1.0(eslint@9.9.0)(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.1.0 + eslint: 9.9.0 graphemer: 1.4.0 - ignore: 5.3.1 + ignore: 5.3.2 natural-compare: 1.4.0 ts-api-utils: 1.3.0(typescript@5.5.4) optionalDependencies: @@ -2154,28 +2155,28 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.0.0(eslint@9.9.1)(typescript@5.5.4)': + '@typescript-eslint/parser@8.1.0(eslint@9.9.0)(typescript@5.5.4)': dependencies: - '@typescript-eslint/scope-manager': 8.0.0 - '@typescript-eslint/types': 8.0.0 - '@typescript-eslint/typescript-estree': 8.0.0(typescript@5.5.4) - '@typescript-eslint/visitor-keys': 8.0.0 + '@typescript-eslint/scope-manager': 8.1.0 + '@typescript-eslint/types': 8.1.0 + '@typescript-eslint/typescript-estree': 8.1.0(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.1.0 debug: 4.3.6 - eslint: 9.9.1 + eslint: 9.9.0 optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.0.0': + '@typescript-eslint/scope-manager@8.1.0': dependencies: - '@typescript-eslint/types': 8.0.0 - '@typescript-eslint/visitor-keys': 8.0.0 + '@typescript-eslint/types': 8.1.0 + '@typescript-eslint/visitor-keys': 8.1.0 - '@typescript-eslint/type-utils@8.0.0(eslint@9.9.1)(typescript@5.5.4)': + '@typescript-eslint/type-utils@8.1.0(eslint@9.9.0)(typescript@5.5.4)': dependencies: - '@typescript-eslint/typescript-estree': 8.0.0(typescript@5.5.4) - '@typescript-eslint/utils': 8.0.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/typescript-estree': 8.1.0(typescript@5.5.4) + '@typescript-eslint/utils': 8.1.0(eslint@9.9.0)(typescript@5.5.4) debug: 4.3.6 ts-api-utils: 1.3.0(typescript@5.5.4) optionalDependencies: @@ -2184,12 +2185,12 @@ snapshots: - eslint - supports-color - '@typescript-eslint/types@8.0.0': {} + '@typescript-eslint/types@8.1.0': {} - '@typescript-eslint/typescript-estree@8.0.0(typescript@5.5.4)': + '@typescript-eslint/typescript-estree@8.1.0(typescript@5.5.4)': dependencies: - '@typescript-eslint/types': 8.0.0 - '@typescript-eslint/visitor-keys': 8.0.0 + '@typescript-eslint/types': 8.1.0 + '@typescript-eslint/visitor-keys': 8.1.0 debug: 4.3.6 globby: 11.1.0 is-glob: 4.0.3 @@ -2201,20 +2202,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.0.0(eslint@9.9.1)(typescript@5.5.4)': + '@typescript-eslint/utils@8.1.0(eslint@9.9.0)(typescript@5.5.4)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.1) - '@typescript-eslint/scope-manager': 8.0.0 - '@typescript-eslint/types': 8.0.0 - '@typescript-eslint/typescript-estree': 8.0.0(typescript@5.5.4) - eslint: 9.9.1 + '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.0) + '@typescript-eslint/scope-manager': 8.1.0 + '@typescript-eslint/types': 8.1.0 + '@typescript-eslint/typescript-estree': 8.1.0(typescript@5.5.4) + eslint: 9.9.0 transitivePeerDependencies: - supports-color - typescript - '@typescript-eslint/visitor-keys@8.0.0': + '@typescript-eslint/visitor-keys@8.1.0': dependencies: - '@typescript-eslint/types': 8.0.0 + '@typescript-eslint/types': 8.1.0 eslint-visitor-keys: 3.4.3 acorn-jsx@5.3.2(acorn@8.12.1): @@ -2259,7 +2260,7 @@ snapshots: array-union@2.1.0: {} - async@3.2.6: {} + async@3.2.5: {} babel-jest@29.7.0(@babel/core@7.23.9): dependencies: @@ -2400,13 +2401,13 @@ snapshots: convert-source-map@2.0.0: {} - create-jest@29.7.0(@types/node@22.5.1): + create-jest@29.7.0(@types/node@22.3.0): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.5.1) + jest-config: 29.7.0(@types/node@22.3.0) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -2461,9 +2462,9 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-prettier@5.2.1(eslint@9.9.1)(prettier@3.3.3): + eslint-plugin-prettier@5.2.1(eslint@9.9.0)(prettier@3.3.3): dependencies: - eslint: 9.9.1 + eslint: 9.9.0 prettier: 3.3.3 prettier-linter-helpers: 1.0.0 synckit: 0.9.1 @@ -2477,13 +2478,13 @@ snapshots: eslint-visitor-keys@4.0.0: {} - eslint@9.9.1: + eslint@9.9.0: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.1) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.0) '@eslint-community/regexpp': 4.11.0 - '@eslint/config-array': 0.18.0 + '@eslint/config-array': 0.17.1 '@eslint/eslintrc': 3.1.0 - '@eslint/js': 9.9.1 + '@eslint/js': 9.9.0 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.3.0 '@nodelib/fs.walk': 1.2.8 @@ -2676,8 +2677,6 @@ snapshots: human-signals@2.1.0: {} - ignore@5.3.1: {} - ignore@5.3.2: {} import-fresh@3.3.0: @@ -2766,7 +2765,7 @@ snapshots: jake@10.9.2: dependencies: - async: 3.2.6 + async: 3.2.5 chalk: 4.1.2 filelist: 1.0.4 minimatch: 3.1.2 @@ -2783,7 +2782,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.5.1 + '@types/node': 22.3.0 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.1 @@ -2803,16 +2802,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@22.5.1): + jest-cli@29.7.0(@types/node@22.3.0): dependencies: '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.5.1) + create-jest: 29.7.0(@types/node@22.3.0) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@22.5.1) + jest-config: 29.7.0(@types/node@22.3.0) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -2822,7 +2821,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@22.5.1): + jest-config@29.7.0(@types/node@22.3.0): dependencies: '@babel/core': 7.23.9 '@jest/test-sequencer': 29.7.0 @@ -2847,7 +2846,7 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 22.5.1 + '@types/node': 22.3.0 transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -2876,7 +2875,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.5.1 + '@types/node': 22.3.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -2886,7 +2885,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 22.5.1 + '@types/node': 22.3.0 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -2925,7 +2924,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.5.1 + '@types/node': 22.3.0 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -2960,7 +2959,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.5.1 + '@types/node': 22.3.0 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -2988,7 +2987,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.5.1 + '@types/node': 22.3.0 chalk: 4.1.2 cjs-module-lexer: 1.2.3 collect-v8-coverage: 1.0.2 @@ -3027,14 +3026,14 @@ snapshots: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.6.3 + semver: 7.6.2 transitivePeerDependencies: - supports-color jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.5.1 + '@types/node': 22.3.0 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -3053,7 +3052,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.5.1 + '@types/node': 22.3.0 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -3062,17 +3061,17 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 22.5.1 + '@types/node': 22.3.0 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.5.1): + jest@29.7.0(@types/node@22.3.0): dependencies: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@22.5.1) + jest-cli: 29.7.0(@types/node@22.3.0) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -3108,7 +3107,7 @@ snapshots: kleur@3.0.3: {} - lemmy-js-client@0.19.5-alpha.1: {} + lemmy-js-client@0.20.0-alpha.4: {} leven@3.1.0: {} @@ -3306,6 +3305,8 @@ snapshots: semver@6.3.1: {} + semver@7.6.2: {} + semver@7.6.3: {} shebang-command@2.0.0: @@ -3393,12 +3394,12 @@ snapshots: dependencies: typescript: 5.5.4 - ts-jest@29.2.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.5.1))(typescript@5.5.4): + ts-jest@29.2.4(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.3.0))(typescript@5.5.4): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@22.5.1) + jest: 29.7.0(@types/node@22.3.0) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -3422,11 +3423,11 @@ snapshots: type-fest@0.21.3: {} - typescript-eslint@8.0.0(eslint@9.9.1)(typescript@5.5.4): + typescript-eslint@8.1.0(eslint@9.9.0)(typescript@5.5.4): dependencies: - '@typescript-eslint/eslint-plugin': 8.0.0(@typescript-eslint/parser@8.0.0(eslint@9.9.1)(typescript@5.5.4))(eslint@9.9.1)(typescript@5.5.4) - '@typescript-eslint/parser': 8.0.0(eslint@9.9.1)(typescript@5.5.4) - '@typescript-eslint/utils': 8.0.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/eslint-plugin': 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': 8.1.0(eslint@9.9.0)(typescript@5.5.4) + '@typescript-eslint/utils': 8.1.0(eslint@9.9.0)(typescript@5.5.4) optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: @@ -3435,7 +3436,7 @@ snapshots: typescript@5.5.4: {} - undici-types@6.19.8: {} + undici-types@6.18.2: {} update-browserslist-db@1.0.13(browserslist@4.22.3): dependencies: diff --git a/api_tests/src/post.spec.ts b/api_tests/src/post.spec.ts index 6b5c8d812e..e059d4585a 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -502,10 +502,17 @@ test("Enforce site ban federation for local user", async () => { alpha, alphaPerson.person.id, false, - false, + true, ); expect(unBanAlpha.banned).toBe(false); + // existing alpha post should be restored on beta + betaBanRes = await waitUntil( + () => getPost(beta, searchBeta1.post.id), + s => !s.post_view.post.removed, + ); + expect(betaBanRes.post_view.post.removed).toBe(false); + // Login gets invalidated by ban, need to login again if (!alphaUserPerson) { throw "Missing alpha person"; diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index 3ca37dac4f..84e0551a42 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -419,13 +419,13 @@ export async function banPersonFromSite( api: LemmyHttp, person_id: number, ban: boolean, - remove_data: boolean, + remove_or_restore_data: boolean, ): Promise { // Make sure lemmy-beta/c/main is cached on lemmy_alpha let form: BanPerson = { person_id, ban, - remove_data, + remove_or_restore_data, }; return api.banPerson(form); } @@ -434,13 +434,13 @@ export async function banPersonFromCommunity( api: LemmyHttp, person_id: number, community_id: number, - remove_data: boolean, + remove_or_restore_data: boolean, ban: boolean, ): Promise { let form: BanFromCommunity = { person_id, community_id, - remove_data: remove_data, + remove_or_restore_data, ban, }; return api.banFromCommunity(form); diff --git a/crates/api/src/community/ban.rs b/crates/api/src/community/ban.rs index 8e527d2acd..d00a1b0d05 100644 --- a/crates/api/src/community/ban.rs +++ b/crates/api/src/community/ban.rs @@ -4,7 +4,11 @@ use lemmy_api_common::{ community::{BanFromCommunity, BanFromCommunityResponse}, context::LemmyContext, send_activity::{ActivityChannel, SendActivityData}, - utils::{check_community_mod_action, check_expire_time, remove_user_data_in_community}, + utils::{ + check_community_mod_action, + check_expire_time, + remove_or_restore_user_data_in_community, + }, }; use lemmy_db_schema::{ source::{ @@ -33,7 +37,6 @@ pub async fn ban_from_community( local_user_view: LocalUserView, ) -> LemmyResult> { let banned_person_id = data.person_id; - let remove_data = data.remove_data.unwrap_or(false); let expires = check_expire_time(data.expires)?; // Verify that only mods or admins can ban @@ -85,9 +88,16 @@ pub async fn ban_from_community( } // Remove/Restore their data if that's desired - if remove_data { - remove_user_data_in_community(data.community_id, banned_person_id, &mut context.pool()).await?; - } + if data.remove_or_restore_data.unwrap_or(false) { + let remove_data = data.ban; + remove_or_restore_user_data_in_community( + data.community_id, + banned_person_id, + remove_data, + &mut context.pool(), + ) + .await?; + }; // Mod tables let form = ModBanFromCommunityForm { diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 2b8e12d372..3cae4737a0 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -172,7 +172,7 @@ pub(crate) async fn ban_nonlocal_user_from_local_communities( target: &Person, ban: bool, reason: &Option, - remove_data: &Option, + remove_or_restore_data: &Option, expires: &Option, context: &Data, ) -> LemmyResult<()> { @@ -230,7 +230,7 @@ pub(crate) async fn ban_nonlocal_user_from_local_communities( person_id: target.id, ban, reason: reason.clone(), - remove_data: *remove_data, + remove_or_restore_data: *remove_or_restore_data, expires: *expires, }; diff --git a/crates/api/src/local_user/ban_person.rs b/crates/api/src/local_user/ban_person.rs index 58392cefd8..bea8247a36 100644 --- a/crates/api/src/local_user/ban_person.rs +++ b/crates/api/src/local_user/ban_person.rs @@ -5,7 +5,7 @@ use lemmy_api_common::{ context::LemmyContext, person::{BanPerson, BanPersonResponse}, send_activity::{ActivityChannel, SendActivityData}, - utils::{check_expire_time, is_admin, remove_user_data}, + utils::{check_expire_time, is_admin, remove_user_data, restore_user_data}, }; use lemmy_db_schema::{ source::{ @@ -65,10 +65,13 @@ pub async fn ban_from_site( } // Remove their data if that's desired - let remove_data = data.remove_data.unwrap_or(false); - if remove_data { - remove_user_data(person.id, &context).await?; - } + if data.remove_or_restore_data.unwrap_or(false) { + if data.ban { + remove_user_data(person.id, &context).await?; + } else { + restore_user_data(person.id, &context).await?; + } + }; // Mod tables let form = ModBanForm { @@ -90,7 +93,7 @@ pub async fn ban_from_site( &person, data.ban, &data.reason, - &data.remove_data, + &data.remove_or_restore_data, &data.expires, &context, ) @@ -101,7 +104,7 @@ pub async fn ban_from_site( moderator: local_user_view.person, banned_user: person_view.person.clone(), reason: data.reason.clone(), - remove_data: data.remove_data, + remove_or_restore_data: data.remove_or_restore_data, ban: data.ban, expires: data.expires, }, diff --git a/crates/api/src/site/purge/person.rs b/crates/api/src/site/purge/person.rs index dc824b1632..1ce8bcd868 100644 --- a/crates/api/src/site/purge/person.rs +++ b/crates/api/src/site/purge/person.rs @@ -77,7 +77,7 @@ pub async fn purge_person( moderator: local_user_view.person, banned_user: person, reason: data.reason.clone(), - remove_data: Some(true), + remove_or_restore_data: Some(true), ban: true, expires: None, }, diff --git a/crates/api_common/src/community.rs b/crates/api_common/src/community.rs index 9d306ff7a1..3dad04af5e 100644 --- a/crates/api_common/src/community.rs +++ b/crates/api_common/src/community.rs @@ -97,7 +97,9 @@ pub struct BanFromCommunity { pub community_id: CommunityId, pub person_id: PersonId, pub ban: bool, - pub remove_data: Option, + /// Optionally remove or restore all their data. Useful for new troll accounts. + /// If ban is true, then this means remove. If ban is false, it means restore. + pub remove_or_restore_data: Option, pub reason: Option, /// A time that the ban will expire, in unix epoch seconds. /// diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index 29e9ef0840..64b09e1327 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -217,8 +217,9 @@ pub struct AddAdminResponse { pub struct BanPerson { pub person_id: PersonId, pub ban: bool, - /// Optionally remove all their data. Useful for new troll accounts. - pub remove_data: Option, + /// Optionally remove or restore all their data. Useful for new troll accounts. + /// If ban is true, then this means remove. If ban is false, it means restore. + pub remove_or_restore_data: Option, pub reason: Option, /// A time that the ban will expire, in unix epoch seconds. /// diff --git a/crates/api_common/src/send_activity.rs b/crates/api_common/src/send_activity.rs index 02518ca33a..465e074f44 100644 --- a/crates/api_common/src/send_activity.rs +++ b/crates/api_common/src/send_activity.rs @@ -83,7 +83,7 @@ pub enum SendActivityData { moderator: Person, banned_user: Person, reason: Option, - remove_data: Option, + remove_or_restore_data: Option, ban: bool, expires: Option, }, diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index e41b574c52..0e790fe99f 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -827,13 +827,30 @@ pub async fn remove_user_data( Ok(()) } -pub async fn remove_user_data_in_community( +/// We can't restore their images, but we can unremove their posts and comments +pub async fn restore_user_data( + banned_person_id: PersonId, + context: &LemmyContext, +) -> LemmyResult<()> { + let pool = &mut context.pool(); + + // Posts + Post::update_removed_for_creator(pool, banned_person_id, None, false).await?; + + // Comments + Comment::update_removed_for_creator(pool, banned_person_id, false).await?; + + Ok(()) +} + +pub async fn remove_or_restore_user_data_in_community( community_id: CommunityId, banned_person_id: PersonId, + remove: bool, pool: &mut DbPool<'_>, ) -> LemmyResult<()> { // Posts - Post::update_removed_for_creator(pool, banned_person_id, Some(community_id), true).await?; + Post::update_removed_for_creator(pool, banned_person_id, Some(community_id), remove).await?; // Comments // TODO Diesel doesn't allow updates with joins, so this has to be a loop @@ -851,7 +868,7 @@ pub async fn remove_user_data_in_community( pool, comment_id, &CommentUpdateForm { - removed: Some(true), + removed: Some(remove), ..Default::default() }, ) diff --git a/crates/apub/src/activities/block/block_user.rs b/crates/apub/src/activities/block/block_user.rs index e6c97c4243..29412cdfe6 100644 --- a/crates/apub/src/activities/block/block_user.rs +++ b/crates/apub/src/activities/block/block_user.rs @@ -23,7 +23,7 @@ use anyhow::anyhow; use chrono::{DateTime, Utc}; use lemmy_api_common::{ context::LemmyContext, - utils::{remove_user_data, remove_user_data_in_community}, + utils::{remove_or_restore_user_data_in_community, remove_user_data}, }; use lemmy_db_schema::{ source::{ @@ -205,8 +205,13 @@ impl ActivityHandler for BlockUser { .ok(); if self.remove_data.unwrap_or(false) { - remove_user_data_in_community(community.id, blocked_person.id, &mut context.pool()) - .await?; + remove_or_restore_user_data_in_community( + community.id, + blocked_person.id, + true, + &mut context.pool(), + ) + .await?; } // write to mod log diff --git a/crates/apub/src/activities/block/mod.rs b/crates/apub/src/activities/block/mod.rs index d1ba4c5747..4641dd1d86 100644 --- a/crates/apub/src/activities/block/mod.rs +++ b/crates/apub/src/activities/block/mod.rs @@ -136,7 +136,7 @@ pub(crate) async fn send_ban_from_site( moderator: Person, banned_user: Person, reason: Option, - remove_data: Option, + remove_or_restore_data: Option, ban: bool, expires: Option, context: Data, @@ -151,7 +151,7 @@ pub(crate) async fn send_ban_from_site( &site, &banned_user.into(), &moderator.into(), - remove_data.unwrap_or(false), + remove_or_restore_data.unwrap_or(false), reason.clone(), expires, &context, @@ -162,6 +162,7 @@ pub(crate) async fn send_ban_from_site( &site, &banned_user.into(), &moderator.into(), + remove_or_restore_data.unwrap_or(false), reason.clone(), &context, ) @@ -190,7 +191,7 @@ pub(crate) async fn send_ban_from_community( &SiteOrCommunity::Community(community), &banned_person.into(), &mod_.into(), - data.remove_data.unwrap_or(false), + data.remove_or_restore_data.unwrap_or(false), data.reason.clone(), expires, &context, @@ -201,6 +202,7 @@ pub(crate) async fn send_ban_from_community( &SiteOrCommunity::Community(community), &banned_person.into(), &mod_.into(), + data.remove_or_restore_data.unwrap_or(false), data.reason.clone(), &context, ) diff --git a/crates/apub/src/activities/block/undo_block_user.rs b/crates/apub/src/activities/block/undo_block_user.rs index 69f3eeebf6..4167703488 100644 --- a/crates/apub/src/activities/block/undo_block_user.rs +++ b/crates/apub/src/activities/block/undo_block_user.rs @@ -17,7 +17,10 @@ use activitypub_federation::{ protocol::verification::verify_domains_match, traits::{ActivityHandler, Actor}, }; -use lemmy_api_common::context::LemmyContext; +use lemmy_api_common::{ + context::LemmyContext, + utils::{remove_or_restore_user_data_in_community, restore_user_data}, +}; use lemmy_db_schema::{ source::{ activity::ActivitySendTargets, @@ -36,6 +39,7 @@ impl UndoBlockUser { target: &SiteOrCommunity, user: &ApubPerson, mod_: &ApubPerson, + restore_data: bool, reason: Option, context: &Data, ) -> LemmyResult<()> { @@ -58,6 +62,7 @@ impl UndoBlockUser { kind: UndoType::Undo, id: id.clone(), audience, + restore_data: Some(restore_data), }; let mut inboxes = ActivitySendTargets::to_inbox(user.shared_inbox_or_inbox()); @@ -114,6 +119,10 @@ impl ActivityHandler for UndoBlockUser { ) .await?; + if self.restore_data.unwrap_or(false) { + restore_user_data(blocked_person.id, context).await?; + } + // write mod log let form = ModBanForm { mod_person_id: mod_person.id, @@ -132,6 +141,16 @@ impl ActivityHandler for UndoBlockUser { }; CommunityPersonBan::unban(&mut context.pool(), &community_user_ban_form).await?; + if self.restore_data.unwrap_or(false) { + remove_or_restore_user_data_in_community( + community.id, + blocked_person.id, + false, + &mut context.pool(), + ) + .await?; + } + // write to mod log let form = ModBanFromCommunityForm { mod_person_id: mod_person.id, diff --git a/crates/apub/src/activities/mod.rs b/crates/apub/src/activities/mod.rs index d81e7cabf3..b256a91f7d 100644 --- a/crates/apub/src/activities/mod.rs +++ b/crates/apub/src/activities/mod.rs @@ -352,7 +352,7 @@ pub async fn match_outgoing_activities( moderator, banned_user, reason, - remove_data, + remove_or_restore_data, ban, expires, } => { @@ -360,7 +360,7 @@ pub async fn match_outgoing_activities( moderator, banned_user, reason, - remove_data, + remove_or_restore_data, ban, expires, context, diff --git a/crates/apub/src/protocol/activities/block/undo_block_user.rs b/crates/apub/src/protocol/activities/block/undo_block_user.rs index 491ec7ed1f..e038fa2dcb 100644 --- a/crates/apub/src/protocol/activities/block/undo_block_user.rs +++ b/crates/apub/src/protocol/activities/block/undo_block_user.rs @@ -29,6 +29,10 @@ pub struct UndoBlockUser { pub(crate) kind: UndoType, pub(crate) id: Url, pub(crate) audience: Option>, + + /// Quick and dirty solution. + /// TODO: send a separate Delete activity instead + pub(crate) restore_data: Option, } #[async_trait::async_trait] From 6b6457cc54fea989513a31958244ad89abf3fb08 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 19 Sep 2024 04:03:58 -0400 Subject: [PATCH 3/8] Adding a default_comment_sort_type column for local_site and local_user. (#4469) * Adding a default_comment_sort_type column for local_site and local_user. - Renamed SortType to PostSortType in the DB and code. - Renamed references to default_sort_type to default_post_sort_type. - Fixes #4128 * Renaming migration to current date. * Simplifying PostSortType. --- crates/api/src/local_user/save_settings.rs | 6 +- crates/api_common/src/community.rs | 4 +- crates/api_common/src/person.rs | 14 +++-- crates/api_common/src/post.rs | 4 +- crates/api_common/src/site.rs | 21 ++++--- crates/api_crud/src/site/create.rs | 50 +++++++++++----- crates/api_crud/src/site/update.rs | 47 ++++++++++----- crates/apub/src/api/list_comments.rs | 8 ++- crates/apub/src/api/list_posts.rs | 4 +- crates/apub/src/api/mod.rs | 29 ++++++--- crates/apub/src/api/user_settings_backup.rs | 3 +- .../apub/src/collections/community_outbox.rs | 4 +- crates/db_perf/src/main.rs | 4 +- crates/db_schema/src/lib.rs | 16 +++-- crates/db_schema/src/schema.rs | 24 +++++--- crates/db_schema/src/source/local_site.rs | 15 +++-- crates/db_schema/src/source/local_user.rs | 13 ++-- crates/db_schema/src/utils.rs | 26 +++----- crates/db_views/src/post_view.rs | 59 ++++++++++--------- .../src/registration_application_view.rs | 3 +- crates/db_views_actor/src/community_view.rs | 6 +- crates/db_views_actor/src/person_view.rs | 17 +++--- crates/routes/src/feeds.rs | 16 ++--- .../down.sql | 19 ++++++ .../up.sql | 24 ++++++++ 25 files changed, 286 insertions(+), 150 deletions(-) create mode 100644 migrations/2024-09-16-000000_default_comment_sort_type/down.sql create mode 100644 migrations/2024-09-16-000000_default_comment_sort_type/up.sql diff --git a/crates/api/src/local_user/save_settings.rs b/crates/api/src/local_user/save_settings.rs index d5b6a7ec1e..c62ab0e014 100644 --- a/crates/api/src/local_user/save_settings.rs +++ b/crates/api/src/local_user/save_settings.rs @@ -102,7 +102,8 @@ pub async fn save_user_settings( let local_user_id = local_user_view.local_user.id; let person_id = local_user_view.person.id; let default_listing_type = data.default_listing_type; - let default_sort_type = data.default_sort_type; + let default_post_sort_type = data.default_post_sort_type; + let default_comment_sort_type = data.default_comment_sort_type; let person_form = PersonUpdateForm { display_name, @@ -133,7 +134,8 @@ pub async fn save_user_settings( blur_nsfw: data.blur_nsfw, auto_expand: data.auto_expand, show_bot_accounts: data.show_bot_accounts, - default_sort_type, + default_post_sort_type, + default_comment_sort_type, default_listing_type, theme: data.theme.clone(), interface_language: data.interface_language.clone(), diff --git a/crates/api_common/src/community.rs b/crates/api_common/src/community.rs index 3dad04af5e..e1c1c5d768 100644 --- a/crates/api_common/src/community.rs +++ b/crates/api_common/src/community.rs @@ -3,7 +3,7 @@ use lemmy_db_schema::{ source::site::Site, CommunityVisibility, ListingType, - SortType, + PostSortType, }; use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView, PersonView}; use serde::{Deserialize, Serialize}; @@ -74,7 +74,7 @@ pub struct CommunityResponse { /// Fetches a list of communities. pub struct ListCommunities { pub type_: Option, - pub sort: Option, + pub sort: Option, pub show_nsfw: Option, pub page: Option, pub limit: Option, diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index 64b09e1327..40e8df4cef 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -5,7 +5,7 @@ use lemmy_db_schema::{ CommentSortType, ListingType, PostListingMode, - SortType, + PostSortType, }; use lemmy_db_views::structs::{CommentView, LocalImageView, PostView}; use lemmy_db_views_actor::structs::{ @@ -88,8 +88,14 @@ pub struct SaveUserSettings { pub auto_expand: Option, /// Your user's theme. pub theme: Option, - pub default_sort_type: Option, + /// The default post listing type, usually "local" pub default_listing_type: Option, + /// A post-view mode that changes how multiple post listings look. + pub post_listing_mode: Option, + /// The default post sort, usually "active" + pub default_post_sort_type: Option, + /// The default comment sort, usually "hot" + pub default_comment_sort_type: Option, /// The language of the lemmy interface pub interface_language: Option, /// A URL for your avatar. @@ -120,8 +126,6 @@ pub struct SaveUserSettings { pub open_links_in_new_tab: Option, /// Enable infinite scroll pub infinite_scroll_enabled: Option, - /// A post-view mode that changes how multiple post listings look. - pub post_listing_mode: Option, /// Whether to allow keyboard navigation (for browsing and interacting with posts and comments). pub enable_keyboard_navigation: Option, /// Whether user avatars or inline images in the UI that are gifs should be allowed to play or @@ -172,7 +176,7 @@ pub struct GetPersonDetails { pub person_id: Option, /// Example: dessalines , or dessalines@xyz.tld pub username: Option, - pub sort: Option, + pub sort: Option, pub page: Option, pub limit: Option, pub community_id: Option, diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index 74369173be..44436fa843 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -2,7 +2,7 @@ use lemmy_db_schema::{ newtypes::{CommentId, CommunityId, DbUrl, LanguageId, PostId, PostReportId}, ListingType, PostFeatureType, - SortType, + PostSortType, }; use lemmy_db_views::structs::{PaginationCursor, PostReportView, PostView, VoteView}; use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView}; @@ -69,7 +69,7 @@ pub struct GetPostResponse { /// Get a list of posts. pub struct GetPosts { pub type_: Option, - pub sort: Option, + pub sort: Option, /// DEPRECATED, use page_cursor pub page: Option, pub limit: Option, diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index 6fa6e37009..f25dcf94bc 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -20,12 +20,13 @@ use lemmy_db_schema::{ person::Person, tagline::Tagline, }, + CommentSortType, ListingType, ModlogActionType, PostListingMode, + PostSortType, RegistrationMode, SearchType, - SortType, }; use lemmy_db_views::structs::{ CommentView, @@ -74,7 +75,7 @@ pub struct Search { pub community_name: Option, pub creator_id: Option, pub type_: Option, - pub sort: Option, + pub sort: Option, pub listing_type: Option, pub page: Option, pub limit: Option, @@ -174,7 +175,9 @@ pub struct CreateSite { pub private_instance: Option, pub default_theme: Option, pub default_post_listing_type: Option, - pub default_sort_type: Option, + pub default_post_listing_mode: Option, + pub default_post_sort_type: Option, + pub default_comment_sort_type: Option, pub legal_information: Option, pub application_email_admins: Option, pub hide_modlog_mod_names: Option, @@ -203,7 +206,6 @@ pub struct CreateSite { pub registration_mode: Option, pub oauth_registration: Option, pub content_warning: Option, - pub default_post_listing_mode: Option, } #[skip_serializing_none] @@ -234,9 +236,14 @@ pub struct EditSite { pub private_instance: Option, /// The default theme. Usually "browser" pub default_theme: Option, + /// The default post listing type, usually "local" pub default_post_listing_type: Option, - /// The default sort, usually "active" - pub default_sort_type: Option, + /// Default value for listing mode, usually "list" + pub default_post_listing_mode: Option, + /// The default post sort, usually "active" + pub default_post_sort_type: Option, + /// The default comment sort, usually "hot" + pub default_comment_sort_type: Option, /// An optional page of legal information pub legal_information: Option, /// Whether to email admins when receiving a new application. @@ -291,8 +298,6 @@ pub struct EditSite { /// If present, nsfw content is visible by default. Should be displayed by frontends/clients /// when the site is first opened by a user. pub content_warning: Option, - /// Default value for [LocalUser.post_listing_mode] - pub default_post_listing_mode: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index 3d96d20cf7..70c0e48e96 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -98,7 +98,8 @@ pub async fn create_site( private_instance: data.private_instance, default_theme: data.default_theme.clone(), default_post_listing_type: data.default_post_listing_type, - default_sort_type: data.default_sort_type, + default_post_sort_type: data.default_post_sort_type, + default_comment_sort_type: data.default_comment_sort_type, legal_information: diesel_string_update(data.legal_information.as_deref()), application_email_admins: data.application_email_admins, hide_modlog_mod_names: data.hide_modlog_mod_names, @@ -200,7 +201,13 @@ mod tests { use crate::site::create::validate_create_payload; use lemmy_api_common::site::CreateSite; - use lemmy_db_schema::{source::local_site::LocalSite, ListingType, RegistrationMode, SortType}; + use lemmy_db_schema::{ + source::local_site::LocalSite, + CommentSortType, + ListingType, + PostSortType, + RegistrationMode, + }; use lemmy_utils::error::LemmyErrorType; #[test] @@ -222,7 +229,8 @@ mod tests { None::, None::, None::, - None::, + None::, + None::, None::, None::, None::, @@ -246,7 +254,8 @@ mod tests { None::, None::, None::, - None::, + None::, + None::, None::, None::, None::, @@ -270,7 +279,8 @@ mod tests { None::, None::, None::, - None::, + None::, + None::, Some(String::from("(zeta|alpha)")), None::, None::, @@ -294,7 +304,8 @@ mod tests { None::, None::, Some(ListingType::Subscribed), - None::, + None::, + None::, None::, None::, None::, @@ -318,7 +329,8 @@ mod tests { None::, None::, None::, - None::, + None::, + None::, None::, Some(true), Some(true), @@ -342,7 +354,8 @@ mod tests { None::, None::, None::, - None::, + None::, + None::, None::, None::, Some(true), @@ -366,7 +379,8 @@ mod tests { None::, None::, None::, - None::, + None::, + None::, None::, None::, None::, @@ -424,7 +438,8 @@ mod tests { None::, None::, None::, - None::, + None::, + None::, None::, None::, None::, @@ -447,7 +462,8 @@ mod tests { Some(String::new()), Some(String::new()), Some(ListingType::All), - Some(SortType::Active), + Some(PostSortType::Active), + Some(CommentSortType::Hot), Some(String::new()), Some(false), Some(true), @@ -470,7 +486,8 @@ mod tests { None::, None::, None::, - None::, + None::, + None::, Some(String::new()), None::, None::, @@ -493,7 +510,8 @@ mod tests { None::, None::, None::, - None::, + None::, + None::, None::, None::, None::, @@ -543,7 +561,8 @@ mod tests { site_description: Option, site_sidebar: Option, site_listing_type: Option, - site_sort_type: Option, + site_post_sort_type: Option, + site_comment_sort_type: Option, site_slur_filter_regex: Option, site_is_private: Option, site_is_federated: Option, @@ -564,7 +583,8 @@ mod tests { private_instance: site_is_private, default_theme: None, default_post_listing_type: site_listing_type, - default_sort_type: site_sort_type, + default_post_sort_type: site_post_sort_type, + default_comment_sort_type: site_comment_sort_type, legal_information: None, application_email_admins: None, hide_modlog_mod_names: None, diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index 7e9dc8f039..e0896bed5f 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -107,7 +107,8 @@ pub async fn update_site( private_instance: data.private_instance, default_theme: data.default_theme.clone(), default_post_listing_type: data.default_post_listing_type, - default_sort_type: data.default_sort_type, + default_post_sort_type: data.default_post_sort_type, + default_comment_sort_type: data.default_comment_sort_type, legal_information: diesel_string_update(data.legal_information.as_deref()), application_email_admins: data.application_email_admins, hide_modlog_mod_names: data.hide_modlog_mod_names, @@ -252,7 +253,13 @@ mod tests { use crate::site::update::validate_update_payload; use lemmy_api_common::site::EditSite; - use lemmy_db_schema::{source::local_site::LocalSite, ListingType, RegistrationMode, SortType}; + use lemmy_db_schema::{ + source::local_site::LocalSite, + CommentSortType, + ListingType, + PostSortType, + RegistrationMode, + }; use lemmy_utils::error::LemmyErrorType; #[test] @@ -273,7 +280,8 @@ mod tests { None::, None::, None::, - None::, + None::, + None::, None::, None::, None::, @@ -297,7 +305,8 @@ mod tests { None::, None::, None::, - None::, + None::, + None::, Some(String::from("(zeta|alpha)")), None::, None::, @@ -321,7 +330,8 @@ mod tests { None::, None::, Some(ListingType::Subscribed), - None::, + None::, + None::, None::, None::, None::, @@ -345,7 +355,8 @@ mod tests { None::, None::, None::, - None::, + None::, + None::, None::, Some(true), Some(true), @@ -369,7 +380,8 @@ mod tests { None::, None::, None::, - None::, + None::, + None::, None::, None::, Some(true), @@ -393,7 +405,8 @@ mod tests { None::, None::, None::, - None::, + None::, + None::, None::, None::, None::, @@ -448,7 +461,8 @@ mod tests { None::, None::, None::, - None::, + None::, + None::, None::, None::, None::, @@ -471,7 +485,8 @@ mod tests { Some(String::new()), Some(String::new()), Some(ListingType::All), - Some(SortType::Active), + Some(PostSortType::Active), + Some(CommentSortType::Hot), Some(String::new()), Some(false), Some(true), @@ -494,7 +509,8 @@ mod tests { None::, None::, None::, - None::, + None::, + None::, Some(String::new()), None::, None::, @@ -517,7 +533,8 @@ mod tests { None::, None::, None::, - None::, + None::, + None::, None::, None::, None::, @@ -566,7 +583,8 @@ mod tests { site_description: Option, site_sidebar: Option, site_listing_type: Option, - site_sort_type: Option, + site_post_sort_type: Option, + site_comment_sort_type: Option, site_slur_filter_regex: Option, site_is_private: Option, site_is_federated: Option, @@ -588,7 +606,8 @@ mod tests { private_instance: site_is_private, default_theme: None, default_post_listing_type: site_listing_type, - default_sort_type: site_sort_type, + default_post_sort_type: site_post_sort_type, + default_comment_sort_type: site_comment_sort_type, legal_information: None, application_email_admins: None, hide_modlog_mod_names: None, diff --git a/crates/apub/src/api/list_comments.rs b/crates/apub/src/api/list_comments.rs index 12d18110e9..3223921bc1 100644 --- a/crates/apub/src/api/list_comments.rs +++ b/crates/apub/src/api/list_comments.rs @@ -1,3 +1,4 @@ +use super::comment_sort_type_with_default; use crate::{ api::listing_type_with_default, fetcher::resolve_actor_identifier, @@ -35,7 +36,12 @@ pub async fn list_comments( } else { data.community_id }; - let sort = data.sort; + let local_user_ref = local_user_view.as_ref().map(|u| &u.local_user); + let sort = Some(comment_sort_type_with_default( + data.sort, + local_user_ref, + &local_site, + )); let max_depth = data.max_depth; let saved_only = data.saved_only; diff --git a/crates/apub/src/api/list_posts.rs b/crates/apub/src/api/list_posts.rs index cb2a37a3cc..9b504dbe35 100644 --- a/crates/apub/src/api/list_posts.rs +++ b/crates/apub/src/api/list_posts.rs @@ -1,5 +1,5 @@ use crate::{ - api::{listing_type_with_default, sort_type_with_default}, + api::{listing_type_with_default, post_sort_type_with_default}, fetcher::resolve_actor_identifier, objects::community::ApubCommunity, }; @@ -57,7 +57,7 @@ pub async fn list_posts( community_id, )); - let sort = Some(sort_type_with_default( + let sort = Some(post_sort_type_with_default( data.sort, local_user, &local_site.local_site, diff --git a/crates/apub/src/api/mod.rs b/crates/apub/src/api/mod.rs index dab2ace068..580be32286 100644 --- a/crates/apub/src/api/mod.rs +++ b/crates/apub/src/api/mod.rs @@ -1,8 +1,9 @@ use lemmy_db_schema::{ newtypes::CommunityId, source::{local_site::LocalSite, local_user::LocalUser}, + CommentSortType, ListingType, - SortType, + PostSortType, }; pub mod list_comments; @@ -33,16 +34,30 @@ fn listing_type_with_default( } } -/// Returns a default instance-level sort type, if none is given by the user. +/// Returns a default instance-level post sort type, if none is given by the user. /// Order is type, local user default, then site default. -fn sort_type_with_default( - type_: Option, +fn post_sort_type_with_default( + type_: Option, local_user: Option<&LocalUser>, local_site: &LocalSite, -) -> SortType { +) -> PostSortType { type_.unwrap_or( local_user - .map(|u| u.default_sort_type) - .unwrap_or(local_site.default_sort_type), + .map(|u| u.default_post_sort_type) + .unwrap_or(local_site.default_post_sort_type), + ) +} + +/// Returns a default instance-level comment sort type, if none is given by the user. +/// Order is type, local user default, then site default. +fn comment_sort_type_with_default( + type_: Option, + local_user: Option<&LocalUser>, + local_site: &LocalSite, +) -> CommentSortType { + type_.unwrap_or( + local_user + .map(|u| u.default_comment_sort_type) + .unwrap_or(local_site.default_comment_sort_type), ) } diff --git a/crates/apub/src/api/user_settings_backup.rs b/crates/apub/src/api/user_settings_backup.rs index fdf8dc2ad8..c94fee9b83 100644 --- a/crates/apub/src/api/user_settings_backup.rs +++ b/crates/apub/src/api/user_settings_backup.rs @@ -114,7 +114,8 @@ pub async fn import_settings( let local_user_form = LocalUserUpdateForm { show_nsfw: data.settings.as_ref().map(|s| s.show_nsfw), theme: data.settings.clone().map(|s| s.theme.clone()), - default_sort_type: data.settings.as_ref().map(|s| s.default_sort_type), + default_post_sort_type: data.settings.as_ref().map(|s| s.default_post_sort_type), + default_comment_sort_type: data.settings.as_ref().map(|s| s.default_comment_sort_type), default_listing_type: data.settings.as_ref().map(|s| s.default_listing_type), interface_language: data.settings.clone().map(|s| s.interface_language), show_avatars: data.settings.as_ref().map(|s| s.show_avatars), diff --git a/crates/apub/src/collections/community_outbox.rs b/crates/apub/src/collections/community_outbox.rs index 38a66c62b1..01199bc2bb 100644 --- a/crates/apub/src/collections/community_outbox.rs +++ b/crates/apub/src/collections/community_outbox.rs @@ -18,7 +18,7 @@ use activitypub_federation::{ }; use futures::future::join_all; use lemmy_api_common::{context::LemmyContext, utils::generate_outbox_url}; -use lemmy_db_schema::{source::site::Site, utils::FETCH_LIMIT_MAX, SortType}; +use lemmy_db_schema::{source::site::Site, utils::FETCH_LIMIT_MAX, PostSortType}; use lemmy_db_views::post_view::PostQuery; use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; @@ -39,7 +39,7 @@ impl Collection for ApubCommunityOutbox { let post_views = PostQuery { community_id: Some(owner.id), - sort: Some(SortType::New), + sort: Some(PostSortType::New), limit: Some(FETCH_LIMIT_MAX), ..Default::default() } diff --git a/crates/db_perf/src/main.rs b/crates/db_perf/src/main.rs index 8e03a0a1dd..4abe14794e 100644 --- a/crates/db_perf/src/main.rs +++ b/crates/db_perf/src/main.rs @@ -20,7 +20,7 @@ use lemmy_db_schema::{ }, traits::Crud, utils::{build_db_pool, get_conn, now}, - SortType, + PostSortType, }; use lemmy_db_views::{post_view::PostQuery, structs::PaginationCursor}; use lemmy_utils::error::{LemmyErrorExt2, LemmyResult}; @@ -151,7 +151,7 @@ async fn try_main() -> LemmyResult<()> { // TODO: include local_user let post_views = PostQuery { community_id: community_ids.as_slice().first().cloned(), - sort: Some(SortType::New), + sort: Some(PostSortType::New), limit: Some(20), page_after, ..Default::default() diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index c29ec64439..1e3d96a1df 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -58,13 +58,13 @@ use ts_rs::TS; #[cfg_attr(feature = "full", derive(DbEnum, TS))] #[cfg_attr( feature = "full", - ExistingTypePath = "crate::schema::sql_types::SortTypeEnum" + ExistingTypePath = "crate::schema::sql_types::PostSortTypeEnum" )] #[cfg_attr(feature = "full", DbValueStyle = "verbatim")] #[cfg_attr(feature = "full", ts(export))] // TODO add the controversial and scaled rankings to the doc below /// The post sort types. See here for descriptions: https://join-lemmy.org/docs/en/users/03-votes-and-ranking.html -pub enum SortType { +pub enum PostSortType { #[default] Active, Hot, @@ -87,11 +87,19 @@ pub enum SortType { Scaled, } -#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] +#[derive( + EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default, Hash, +)] +#[cfg_attr(feature = "full", derive(DbEnum, TS))] +#[cfg_attr( + feature = "full", + ExistingTypePath = "crate::schema::sql_types::CommentSortTypeEnum" +)] +#[cfg_attr(feature = "full", DbValueStyle = "verbatim")] #[cfg_attr(feature = "full", ts(export))] /// The comment sort types. See here for descriptions: https://join-lemmy.org/docs/en/users/03-votes-and-ranking.html pub enum CommentSortType { + #[default] Hot, Top, New, diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index de3e4fa48c..b5f68822ce 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -5,6 +5,10 @@ pub mod sql_types { #[diesel(postgres_type(name = "actor_type_enum"))] pub struct ActorTypeEnum; + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "comment_sort_type_enum"))] + pub struct CommentSortTypeEnum; + #[derive(diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "community_visibility"))] pub struct CommunityVisibility; @@ -22,12 +26,12 @@ pub mod sql_types { pub struct PostListingModeEnum; #[derive(diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "registration_mode_enum"))] - pub struct RegistrationModeEnum; + #[diesel(postgres_type(name = "post_sort_type_enum"))] + pub struct PostSortTypeEnum; #[derive(diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "sort_type_enum"))] - pub struct SortTypeEnum; + #[diesel(postgres_type(name = "registration_mode_enum"))] + pub struct RegistrationModeEnum; } diesel::table! { @@ -363,7 +367,8 @@ diesel::table! { use super::sql_types::ListingTypeEnum; use super::sql_types::RegistrationModeEnum; use super::sql_types::PostListingModeEnum; - use super::sql_types::SortTypeEnum; + use super::sql_types::PostSortTypeEnum; + use super::sql_types::CommentSortTypeEnum; local_site (id) { id -> Int4, @@ -391,7 +396,8 @@ diesel::table! { reports_email_admins -> Bool, federation_signed_fetch -> Bool, default_post_listing_mode -> PostListingModeEnum, - default_sort_type -> SortTypeEnum, + default_post_sort_type -> PostSortTypeEnum, + default_comment_sort_type -> CommentSortTypeEnum, oauth_registration -> Bool, } } @@ -429,9 +435,10 @@ diesel::table! { diesel::table! { use diesel::sql_types::*; - use super::sql_types::SortTypeEnum; + use super::sql_types::PostSortTypeEnum; use super::sql_types::ListingTypeEnum; use super::sql_types::PostListingModeEnum; + use super::sql_types::CommentSortTypeEnum; local_user (id) { id -> Int4, @@ -440,7 +447,7 @@ diesel::table! { email -> Nullable, show_nsfw -> Bool, theme -> Text, - default_sort_type -> SortTypeEnum, + default_post_sort_type -> PostSortTypeEnum, default_listing_type -> ListingTypeEnum, #[max_length = 20] interface_language -> Varchar, @@ -461,6 +468,7 @@ diesel::table! { enable_keyboard_navigation -> Bool, enable_animated_images -> Bool, collapse_bot_comments -> Bool, + default_comment_sort_type -> CommentSortTypeEnum, } } diff --git a/crates/db_schema/src/source/local_site.rs b/crates/db_schema/src/source/local_site.rs index 8dc81a9a55..001c8cc522 100644 --- a/crates/db_schema/src/source/local_site.rs +++ b/crates/db_schema/src/source/local_site.rs @@ -2,10 +2,11 @@ use crate::schema::local_site; use crate::{ newtypes::{LocalSiteId, SiteId}, + CommentSortType, ListingType, PostListingMode, + PostSortType, RegistrationMode, - SortType, }; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -66,8 +67,10 @@ pub struct LocalSite { pub federation_signed_fetch: bool, /// Default value for [LocalSite.post_listing_mode] pub default_post_listing_mode: PostListingMode, - /// Default value for [LocalUser.post_listing_mode] - pub default_sort_type: SortType, + /// Default value for [LocalUser.post_sort_type] + pub default_post_sort_type: PostSortType, + /// Default value for [LocalUser.comment_sort_type] + pub default_comment_sort_type: CommentSortType, /// Whether or not external auth methods can auto-register users. pub oauth_registration: bool, } @@ -100,7 +103,8 @@ pub struct LocalSiteInsertForm { pub reports_email_admins: Option, pub federation_signed_fetch: Option, pub default_post_listing_mode: Option, - pub default_sort_type: Option, + pub default_post_sort_type: Option, + pub default_comment_sort_type: Option, } #[derive(Clone, Default)] @@ -129,5 +133,6 @@ pub struct LocalSiteUpdateForm { pub updated: Option>>, pub federation_signed_fetch: Option, pub default_post_listing_mode: Option, - pub default_sort_type: Option, + pub default_post_sort_type: Option, + pub default_comment_sort_type: Option, } diff --git a/crates/db_schema/src/source/local_user.rs b/crates/db_schema/src/source/local_user.rs index e184d3605b..876bfa4870 100644 --- a/crates/db_schema/src/source/local_user.rs +++ b/crates/db_schema/src/source/local_user.rs @@ -3,9 +3,10 @@ use crate::schema::local_user; use crate::{ newtypes::{LocalUserId, PersonId}, sensitive::SensitiveString, + CommentSortType, ListingType, PostListingMode, - SortType, + PostSortType, }; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -29,7 +30,7 @@ pub struct LocalUser { /// Whether to show NSFW content. pub show_nsfw: bool, pub theme: String, - pub default_sort_type: SortType, + pub default_post_sort_type: PostSortType, pub default_listing_type: ListingType, pub interface_language: String, /// Whether to show avatars. @@ -63,6 +64,7 @@ pub struct LocalUser { pub enable_animated_images: bool, /// Whether to auto-collapse bot comments. pub collapse_bot_comments: bool, + pub default_comment_sort_type: CommentSortType, } #[derive(Clone, derive_new::new)] @@ -78,7 +80,7 @@ pub struct LocalUserInsertForm { #[new(default)] pub theme: Option, #[new(default)] - pub default_sort_type: Option, + pub default_post_sort_type: Option, #[new(default)] pub default_listing_type: Option, #[new(default)] @@ -117,6 +119,8 @@ pub struct LocalUserInsertForm { pub enable_animated_images: Option, #[new(default)] pub collapse_bot_comments: Option, + #[new(default)] + pub default_comment_sort_type: Option, } #[derive(Clone, Default)] @@ -127,7 +131,7 @@ pub struct LocalUserUpdateForm { pub email: Option>, pub show_nsfw: Option, pub theme: Option, - pub default_sort_type: Option, + pub default_post_sort_type: Option, pub default_listing_type: Option, pub interface_language: Option, pub show_avatars: Option, @@ -147,4 +151,5 @@ pub struct LocalUserUpdateForm { pub enable_keyboard_navigation: Option, pub enable_animated_images: Option, pub collapse_bot_comments: Option, + pub default_comment_sort_type: Option, } diff --git a/crates/db_schema/src/utils.rs b/crates/db_schema/src/utils.rs index a174e3cb90..a61a230faa 100644 --- a/crates/db_schema/src/utils.rs +++ b/crates/db_schema/src/utils.rs @@ -1,4 +1,4 @@ -use crate::{newtypes::DbUrl, CommentSortType, SortType}; +use crate::{newtypes::DbUrl, CommentSortType, PostSortType}; use chrono::{DateTime, TimeDelta, Utc}; use deadpool::Runtime; use diesel::{ @@ -481,23 +481,15 @@ pub fn naive_now() -> DateTime { Utc::now() } -pub fn post_to_comment_sort_type(sort: SortType) -> CommentSortType { +pub fn post_to_comment_sort_type(sort: PostSortType) -> CommentSortType { + use PostSortType::*; match sort { - SortType::Active | SortType::Hot | SortType::Scaled => CommentSortType::Hot, - SortType::New | SortType::NewComments | SortType::MostComments => CommentSortType::New, - SortType::Old => CommentSortType::Old, - SortType::Controversial => CommentSortType::Controversial, - SortType::TopHour - | SortType::TopSixHour - | SortType::TopTwelveHour - | SortType::TopDay - | SortType::TopAll - | SortType::TopWeek - | SortType::TopYear - | SortType::TopMonth - | SortType::TopThreeMonths - | SortType::TopSixMonths - | SortType::TopNineMonths => CommentSortType::Top, + Active | Hot | Scaled => CommentSortType::Hot, + New | NewComments | MostComments => CommentSortType::New, + Old => CommentSortType::Old, + Controversial => CommentSortType::Controversial, + TopHour | TopSixHour | TopTwelveHour | TopDay | TopAll | TopWeek | TopYear | TopMonth + | TopThreeMonths | TopSixMonths | TopNineMonths => CommentSortType::Top, } } diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index df6216bf78..acd8debf33 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -58,9 +58,10 @@ use lemmy_db_schema::{ ReverseTimestampKey, }, ListingType, - SortType, + PostSortType, }; use tracing::debug; +use PostSortType::*; fn queries<'a>() -> Queries< impl ReadFn<'a, PostView, (PostId, Option<&'a LocalUser>, bool)>, @@ -510,33 +511,33 @@ fn queries<'a>() -> Queries< let time = |interval| post_aggregates::published.gt(now() - interval); // then use the main sort - query = match options.sort.unwrap_or(SortType::Hot) { - SortType::Active => query.then_desc(key::hot_rank_active), - SortType::Hot => query.then_desc(key::hot_rank), - SortType::Scaled => query.then_desc(key::scaled_rank), - SortType::Controversial => query.then_desc(key::controversy_rank), - SortType::New => query.then_desc(key::published), - SortType::Old => query.then_desc(ReverseTimestampKey(key::published)), - SortType::NewComments => query.then_desc(key::newest_comment_time), - SortType::MostComments => query.then_desc(key::comments), - SortType::TopAll => query.then_desc(key::score), - SortType::TopYear => query.then_desc(key::score).filter(time(1.years())), - SortType::TopMonth => query.then_desc(key::score).filter(time(1.months())), - SortType::TopWeek => query.then_desc(key::score).filter(time(1.weeks())), - SortType::TopDay => query.then_desc(key::score).filter(time(1.days())), - SortType::TopHour => query.then_desc(key::score).filter(time(1.hours())), - SortType::TopSixHour => query.then_desc(key::score).filter(time(6.hours())), - SortType::TopTwelveHour => query.then_desc(key::score).filter(time(12.hours())), - SortType::TopThreeMonths => query.then_desc(key::score).filter(time(3.months())), - SortType::TopSixMonths => query.then_desc(key::score).filter(time(6.months())), - SortType::TopNineMonths => query.then_desc(key::score).filter(time(9.months())), + query = match options.sort.unwrap_or(Hot) { + Active => query.then_desc(key::hot_rank_active), + Hot => query.then_desc(key::hot_rank), + Scaled => query.then_desc(key::scaled_rank), + Controversial => query.then_desc(key::controversy_rank), + New => query.then_desc(key::published), + Old => query.then_desc(ReverseTimestampKey(key::published)), + NewComments => query.then_desc(key::newest_comment_time), + MostComments => query.then_desc(key::comments), + TopAll => query.then_desc(key::score), + TopYear => query.then_desc(key::score).filter(time(1.years())), + TopMonth => query.then_desc(key::score).filter(time(1.months())), + TopWeek => query.then_desc(key::score).filter(time(1.weeks())), + TopDay => query.then_desc(key::score).filter(time(1.days())), + TopHour => query.then_desc(key::score).filter(time(1.hours())), + TopSixHour => query.then_desc(key::score).filter(time(6.hours())), + TopTwelveHour => query.then_desc(key::score).filter(time(12.hours())), + TopThreeMonths => query.then_desc(key::score).filter(time(3.months())), + TopSixMonths => query.then_desc(key::score).filter(time(6.months())), + TopNineMonths => query.then_desc(key::score).filter(time(9.months())), }; // use publish as fallback. especially useful for hot rank which reaches zero after some days. // necessary because old posts can be fetched over federation and inserted with high post id - query = match options.sort.unwrap_or(SortType::Hot) { + query = match options.sort.unwrap_or(Hot) { // A second time-based sort would not be very useful - SortType::New | SortType::Old | SortType::NewComments => query, + New | Old | NewComments => query, _ => query.then_desc(key::published), }; @@ -608,7 +609,7 @@ pub struct PaginationCursorData(PostAggregates); #[derive(Clone, Default)] pub struct PostQuery<'a> { pub listing_type: Option, - pub sort: Option, + pub sort: Option, pub creator_id: Option, pub community_id: Option, // if true, the query should be handled as if community_id was not given except adding the @@ -770,7 +771,7 @@ mod tests { traits::{Bannable, Blockable, Crud, Joinable, Likeable}, utils::{build_db_pool, build_db_pool_for_tests, DbPool, RANK_DEFAULT}, CommunityVisibility, - SortType, + PostSortType, SubscribedType, }; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; @@ -801,7 +802,7 @@ mod tests { impl Data { fn default_post_query(&self) -> PostQuery<'_> { PostQuery { - sort: Some(SortType::New), + sort: Some(PostSortType::New), local_user: Some(&self.local_user_view.local_user), ..Default::default() } @@ -1445,7 +1446,7 @@ mod tests { let options = PostQuery { community_id: Some(inserted_community.id), - sort: Some(SortType::MostComments), + sort: Some(PostSortType::MostComments), limit: Some(10), ..Default::default() }; @@ -1577,7 +1578,7 @@ mod tests { // Make sure it does come back with the show_hidden option let post_listings_show_hidden = PostQuery { - sort: Some(SortType::New), + sort: Some(PostSortType::New), local_user: Some(&data.local_user_view.local_user), show_hidden: Some(true), ..Default::default() @@ -1618,7 +1619,7 @@ mod tests { // Make sure it does come back with the show_nsfw option let post_listings_show_nsfw = PostQuery { - sort: Some(SortType::New), + sort: Some(PostSortType::New), show_nsfw: Some(true), local_user: Some(&data.local_user_view.local_user), ..Default::default() diff --git a/crates/db_views/src/registration_application_view.rs b/crates/db_views/src/registration_application_view.rs index 6f806be13c..51e2ff1a6a 100644 --- a/crates/db_views/src/registration_application_view.rs +++ b/crates/db_views/src/registration_application_view.rs @@ -246,7 +246,8 @@ mod tests { auto_expand: inserted_sara_local_user.auto_expand, blur_nsfw: inserted_sara_local_user.blur_nsfw, theme: inserted_sara_local_user.theme, - default_sort_type: inserted_sara_local_user.default_sort_type, + default_post_sort_type: inserted_sara_local_user.default_post_sort_type, + default_comment_sort_type: inserted_sara_local_user.default_comment_sort_type, default_listing_type: inserted_sara_local_user.default_listing_type, interface_language: inserted_sara_local_user.interface_language, show_avatars: inserted_sara_local_user.show_avatars, diff --git a/crates/db_views_actor/src/community_view.rs b/crates/db_views_actor/src/community_view.rs index 0e731878a6..4e09c4c438 100644 --- a/crates/db_views_actor/src/community_view.rs +++ b/crates/db_views_actor/src/community_view.rs @@ -24,7 +24,7 @@ use lemmy_db_schema::{ source::{community::CommunityFollower, local_user::LocalUser, site::Site}, utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, ListingType, - SortType, + PostSortType, }; fn queries<'a>() -> Queries< @@ -102,7 +102,7 @@ fn queries<'a>() -> Queries< }; let list = move |mut conn: DbConn<'a>, (options, site): (CommunityQuery<'a>, &'a Site)| async move { - use SortType::*; + use PostSortType::*; // The left join below will return None in this case let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1)); @@ -221,7 +221,7 @@ impl CommunityView { #[derive(Default)] pub struct CommunityQuery<'a> { pub listing_type: Option, - pub sort: Option, + pub sort: Option, pub local_user: Option<&'a LocalUser>, pub search_term: Option, pub is_mod_or_admin: bool, diff --git a/crates/db_views_actor/src/person_view.rs b/crates/db_views_actor/src/person_view.rs index 7a2edfb449..fe9877d014 100644 --- a/crates/db_views_actor/src/person_view.rs +++ b/crates/db_views_actor/src/person_view.rs @@ -24,7 +24,7 @@ use lemmy_db_schema::{ ReadFn, }, ListingType, - SortType, + PostSortType, }; use serde::{Deserialize, Serialize}; use strum::{Display, EnumString}; @@ -46,12 +46,13 @@ enum PersonSortType { PostCount, } -fn post_to_person_sort_type(sort: SortType) -> PersonSortType { +fn post_to_person_sort_type(sort: PostSortType) -> PersonSortType { + use PostSortType::*; match sort { - SortType::Active | SortType::Hot | SortType::Controversial => PersonSortType::CommentScore, - SortType::New | SortType::NewComments => PersonSortType::New, - SortType::MostComments => PersonSortType::MostComments, - SortType::Old => PersonSortType::Old, + Active | Hot | Controversial => PersonSortType::CommentScore, + New | NewComments => PersonSortType::New, + MostComments => PersonSortType::MostComments, + Old => PersonSortType::Old, _ => PersonSortType::CommentScore, } } @@ -149,7 +150,7 @@ impl PersonView { #[derive(Default)] pub struct PersonQuery { - pub sort: Option, + pub sort: Option, pub search_term: Option, pub listing_type: Option, pub page: Option, @@ -246,7 +247,7 @@ mod tests { assert!(read.is_none()); let list = PersonQuery { - sort: Some(SortType::New), + sort: Some(PostSortType::New), ..Default::default() } .list(pool) diff --git a/crates/routes/src/feeds.rs b/crates/routes/src/feeds.rs index 02114f3829..80c1c7281c 100644 --- a/crates/routes/src/feeds.rs +++ b/crates/routes/src/feeds.rs @@ -9,7 +9,7 @@ use lemmy_db_schema::{ CommentSortType, CommunityVisibility, ListingType, - SortType, + PostSortType, }; use lemmy_db_views::{ post_view::PostQuery, @@ -45,12 +45,12 @@ struct Params { } impl Params { - fn sort_type(&self) -> Result { + fn sort_type(&self) -> Result { let sort_query = self .sort .clone() - .unwrap_or_else(|| SortType::Hot.to_string()); - SortType::from_str(&sort_query).map_err(ErrorBadRequest) + .unwrap_or_else(|| PostSortType::Hot.to_string()); + PostSortType::from_str(&sort_query).map_err(ErrorBadRequest) } fn get_limit(&self) -> i64 { self.limit.unwrap_or(RSS_FETCH_LIMIT) @@ -147,7 +147,7 @@ async fn get_local_feed( async fn get_feed_data( context: &LemmyContext, listing_type: ListingType, - sort_type: SortType, + sort_type: PostSortType, limit: i64, page: i64, ) -> LemmyResult { @@ -251,7 +251,7 @@ async fn get_feed( #[tracing::instrument(skip_all)] async fn get_feed_user( context: &LemmyContext, - sort_type: &SortType, + sort_type: &PostSortType, limit: &i64, page: &i64, user_name: &str, @@ -289,7 +289,7 @@ async fn get_feed_user( #[tracing::instrument(skip_all)] async fn get_feed_community( context: &LemmyContext, - sort_type: &SortType, + sort_type: &PostSortType, limit: &i64, page: &i64, community_name: &str, @@ -334,7 +334,7 @@ async fn get_feed_community( #[tracing::instrument(skip_all)] async fn get_feed_front( context: &LemmyContext, - sort_type: &SortType, + sort_type: &PostSortType, limit: &i64, page: &i64, jwt: &str, diff --git a/migrations/2024-09-16-000000_default_comment_sort_type/down.sql b/migrations/2024-09-16-000000_default_comment_sort_type/down.sql new file mode 100644 index 0000000000..2a7ba16ea1 --- /dev/null +++ b/migrations/2024-09-16-000000_default_comment_sort_type/down.sql @@ -0,0 +1,19 @@ +-- This file should undo anything in `up.sql` +-- Rename the post sort enum +ALTER TYPE post_sort_type_enum RENAME TO sort_type_enum; + +-- Rename the default post sort columns +ALTER TABLE local_user RENAME COLUMN default_post_sort_type TO default_sort_type; + +ALTER TABLE local_site RENAME COLUMN default_post_sort_type TO default_sort_type; + +-- Create the comment sort type enum +ALTER TABLE local_user + DROP COLUMN default_comment_sort_type; + +ALTER TABLE local_site + DROP COLUMN default_comment_sort_type; + +-- Drop the comment enum +DROP TYPE comment_sort_type_enum; + diff --git a/migrations/2024-09-16-000000_default_comment_sort_type/up.sql b/migrations/2024-09-16-000000_default_comment_sort_type/up.sql new file mode 100644 index 0000000000..adea1896ab --- /dev/null +++ b/migrations/2024-09-16-000000_default_comment_sort_type/up.sql @@ -0,0 +1,24 @@ +-- Rename the post sort enum +ALTER TYPE sort_type_enum RENAME TO post_sort_type_enum; + +-- Rename the default post sort columns +ALTER TABLE local_user RENAME COLUMN default_sort_type TO default_post_sort_type; + +ALTER TABLE local_site RENAME COLUMN default_sort_type TO default_post_sort_type; + +-- Create the comment sort type enum +CREATE TYPE comment_sort_type_enum AS ENUM ( + 'Hot', + 'Top', + 'New', + 'Old', + 'Controversial' +); + +-- Add the new default comment sort columns to local_user and local_site +ALTER TABLE local_user + ADD COLUMN default_comment_sort_type comment_sort_type_enum NOT NULL DEFAULT 'Hot'; + +ALTER TABLE local_site + ADD COLUMN default_comment_sort_type comment_sort_type_enum NOT NULL DEFAULT 'Hot'; + From 026e23cf32ed8e0db2fbf1beb102aef211a4d37a Mon Sep 17 00:00:00 2001 From: Nutomic Date: Thu, 19 Sep 2024 10:43:27 +0200 Subject: [PATCH 4/8] Simplify tests using default (#5026) --- crates/api_crud/src/site/create.rs | 456 +++++++++-------------------- crates/api_crud/src/site/update.rs | 413 ++++++++------------------ 2 files changed, 259 insertions(+), 610 deletions(-) diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index 70c0e48e96..30321d8dda 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -203,7 +203,6 @@ mod tests { use lemmy_api_common::site::CreateSite; use lemmy_db_schema::{ source::local_site::LocalSite, - CommentSortType, ListingType, PostSortType, RegistrationMode, @@ -216,177 +215,114 @@ mod tests { ( "CreateSite attempted on set up LocalSite", LemmyErrorType::SiteAlreadyExists, - &generate_local_site( - true, - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_create_site( - String::from("site_name"), - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - ), + &LocalSite { + site_setup: true, + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &CreateSite { + name: String::from("site_name"), + ..Default::default() + }, ), ( "CreateSite name matches LocalSite slur filter", LemmyErrorType::Slurs, - &generate_local_site( - false, - Some(String::from("(foo|bar)")), - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_create_site( - String::from("foo site_name"), - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - ), + &LocalSite { + site_setup: false, + private_instance: true, + slur_filter_regex: Some(String::from("(foo|bar)")), + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &CreateSite { + name: String::from("foo site_name"), + ..Default::default() + }, ), ( "CreateSite name matches new slur filter", LemmyErrorType::Slurs, - &generate_local_site( - false, - Some(String::from("(foo|bar)")), - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_create_site( - String::from("zeta site_name"), - None::, - None::, - None::, - None::, - None::, - Some(String::from("(zeta|alpha)")), - None::, - None::, - None::, - None::, - ), + &LocalSite { + site_setup: false, + private_instance: true, + slur_filter_regex: Some(String::from("(foo|bar)")), + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &CreateSite { + name: String::from("zeta site_name"), + slur_filter_regex: Some(String::from("(zeta|alpha)")), + ..Default::default() + }, ), ( "CreateSite listing type is Subscribed, which is invalid", LemmyErrorType::InvalidDefaultPostListingType, - &generate_local_site( - false, - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_create_site( - String::from("site_name"), - None::, - None::, - Some(ListingType::Subscribed), - None::, - None::, - None::, - None::, - None::, - None::, - None::, - ), + &LocalSite { + site_setup: false, + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &CreateSite { + name: String::from("site_name"), + default_post_listing_type: Some(ListingType::Subscribed), + ..Default::default() + }, ), ( "CreateSite is both private and federated", LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether, - &generate_local_site( - false, - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_create_site( - String::from("site_name"), - None::, - None::, - None::, - None::, - None::, - None::, - Some(true), - Some(true), - None::, - None::, - ), + &LocalSite { + site_setup: false, + private_instance: true, + federation_enabled: false, + ..Default::default() + }, + &CreateSite { + name: String::from("site_name"), + private_instance: Some(true), + federation_enabled: Some(true), + ..Default::default() + }, ), ( "LocalSite is private, but CreateSite also makes it federated", LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether, - &generate_local_site( - false, - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_create_site( - String::from("site_name"), - None::, - None::, - None::, - None::, - None::, - None::, - None::, - Some(true), - None::, - None::, - ), + &LocalSite { + site_setup: false, + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &CreateSite { + name: String::from("site_name"), + federation_enabled: Some(true), + ..Default::default() + }, ), ( "CreateSite requires application, but neither it nor LocalSite has an application question", LemmyErrorType::ApplicationQuestionRequired, - &generate_local_site( - false, - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_create_site( - String::from("site_name"), - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - Some(RegistrationMode::RequireApplication), - ), + &LocalSite { + site_setup: false, + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &CreateSite { + name: String::from("site_name"), + registration_mode: Some(RegistrationMode::RequireApplication), + ..Default::default() + }, ), ]; @@ -425,99 +361,72 @@ mod tests { let valid_payloads = [ ( "No changes between LocalSite and CreateSite", - &generate_local_site( - false, - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_create_site( - String::from("site_name"), - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - ), + &LocalSite { + site_setup: false, + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &CreateSite { + name: String::from("site_name"), + ..Default::default() + }, ), ( "CreateSite allows clearing and changing values", - &generate_local_site( - false, - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_create_site( - String::from("site_name"), - Some(String::new()), - Some(String::new()), - Some(ListingType::All), - Some(PostSortType::Active), - Some(CommentSortType::Hot), - Some(String::new()), - Some(false), - Some(true), - Some(String::new()), - Some(RegistrationMode::Open), - ), + &LocalSite { + site_setup: false, + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &CreateSite { + name: String::from("site_name"), + sidebar: Some(String::new()), + description: Some(String::new()), + application_question: Some(String::new()), + private_instance: Some(false), + default_post_listing_type: Some(ListingType::All), + default_post_sort_type: Some(PostSortType::Active), + slur_filter_regex: Some(String::new()), + federation_enabled: Some(true), + registration_mode: Some(RegistrationMode::Open), + ..Default::default() + }, ), ( "CreateSite clears existing slur filter regex", - &generate_local_site( - false, - Some(String::from("(foo|bar)")), - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_create_site( - String::from("foo site_name"), - None::, - None::, - None::, - None::, - None::, - Some(String::new()), - None::, - None::, - None::, - None::, - ), + &LocalSite { + site_setup: false, + private_instance: true, + slur_filter_regex: Some(String::from("(foo|bar)")), + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &CreateSite { + name: String::from("foo site_name"), + slur_filter_regex: Some(String::new()), + ..Default::default() + }, ), ( "LocalSite has application question and CreateSite now requires applications,", - &generate_local_site( - false, - None::, - true, - false, - Some(String::from("question")), - RegistrationMode::Open, - ), - &generate_create_site( - String::from("site_name"), - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - Some(RegistrationMode::RequireApplication), - ), + &LocalSite { + site_setup: false, + application_question: Some(String::from("question")), + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &CreateSite { + name: String::from("site_name"), + registration_mode: Some(RegistrationMode::RequireApplication), + ..Default::default() + }, ), ]; @@ -533,87 +442,4 @@ mod tests { ); }) } - - fn generate_local_site( - site_setup: bool, - site_slur_filter_regex: Option, - site_is_private: bool, - site_is_federated: bool, - site_application_question: Option, - site_registration_mode: RegistrationMode, - ) -> LocalSite { - LocalSite { - site_setup, - application_question: site_application_question, - private_instance: site_is_private, - slur_filter_regex: site_slur_filter_regex, - federation_enabled: site_is_federated, - registration_mode: site_registration_mode, - ..Default::default() - } - } - - // Allow the test helper function to have too many arguments. - // It's either this or generate the entire struct each time for testing. - #[allow(clippy::too_many_arguments)] - fn generate_create_site( - site_name: String, - site_description: Option, - site_sidebar: Option, - site_listing_type: Option, - site_post_sort_type: Option, - site_comment_sort_type: Option, - site_slur_filter_regex: Option, - site_is_private: Option, - site_is_federated: Option, - site_application_question: Option, - site_registration_mode: Option, - ) -> CreateSite { - CreateSite { - name: site_name, - sidebar: site_sidebar, - description: site_description, - icon: None, - banner: None, - enable_downvotes: None, - enable_nsfw: None, - community_creation_admin_only: None, - require_email_verification: None, - application_question: site_application_question, - private_instance: site_is_private, - default_theme: None, - default_post_listing_type: site_listing_type, - default_post_sort_type: site_post_sort_type, - default_comment_sort_type: site_comment_sort_type, - legal_information: None, - application_email_admins: None, - hide_modlog_mod_names: None, - discussion_languages: None, - slur_filter_regex: site_slur_filter_regex, - actor_name_max_length: None, - rate_limit_message: None, - rate_limit_message_per_second: None, - rate_limit_post: None, - rate_limit_post_per_second: None, - rate_limit_register: None, - rate_limit_register_per_second: None, - rate_limit_image: None, - rate_limit_image_per_second: None, - rate_limit_comment: None, - rate_limit_comment_per_second: None, - rate_limit_search: None, - rate_limit_search_per_second: None, - federation_enabled: site_is_federated, - federation_debug: None, - captcha_enabled: None, - captcha_difficulty: None, - allowed_instances: None, - blocked_instances: None, - taglines: None, - registration_mode: site_registration_mode, - oauth_registration: None, - content_warning: None, - default_post_listing_mode: None, - } - } } diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index e0896bed5f..a6046b4235 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -255,7 +255,6 @@ mod tests { use lemmy_api_common::site::EditSite; use lemmy_db_schema::{ source::local_site::LocalSite, - CommentSortType, ListingType, PostSortType, RegistrationMode, @@ -268,152 +267,94 @@ mod tests { ( "EditSite name matches LocalSite slur filter", LemmyErrorType::Slurs, - &generate_local_site( - Some(String::from("(foo|bar)")), - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_edit_site( - Some(String::from("foo site_name")), - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - ), + &LocalSite { + private_instance: true, + slur_filter_regex: Some(String::from("(foo|bar)")), + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &EditSite { + name: Some(String::from("foo site_name")), + ..Default::default() + }, ), ( "EditSite name matches new slur filter", LemmyErrorType::Slurs, - &generate_local_site( - Some(String::from("(foo|bar)")), - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_edit_site( - Some(String::from("zeta site_name")), - None::, - None::, - None::, - None::, - None::, - Some(String::from("(zeta|alpha)")), - None::, - None::, - None::, - None::, - None::, - ), + &LocalSite { + private_instance: true, + slur_filter_regex: Some(String::from("(foo|bar)")), + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &EditSite { + name: Some(String::from("zeta site_name")), + slur_filter_regex: Some(String::from("(zeta|alpha)")), + ..Default::default() + }, ), ( "EditSite listing type is Subscribed, which is invalid", LemmyErrorType::InvalidDefaultPostListingType, - &generate_local_site( - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_edit_site( - Some(String::from("site_name")), - None::, - None::, - Some(ListingType::Subscribed), - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - ), + &LocalSite { + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &EditSite { + name: Some(String::from("site_name")), + default_post_listing_type: Some(ListingType::Subscribed), + ..Default::default() + }, ), ( "EditSite is both private and federated", LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether, - &generate_local_site( - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_edit_site( - Some(String::from("site_name")), - None::, - None::, - None::, - None::, - None::, - None::, - Some(true), - Some(true), - None::, - None::, - None::, - ), + &LocalSite { + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &EditSite { + name: Some(String::from("site_name")), + private_instance: Some(true), + federation_enabled: Some(true), + ..Default::default() + }, ), ( "LocalSite is private, but EditSite also makes it federated", LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether, - &generate_local_site( - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_edit_site( - Some(String::from("site_name")), - None::, - None::, - None::, - None::, - None::, - None::, - None::, - Some(true), - None::, - None::, - None::, - ), + &LocalSite { + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &EditSite { + name: Some(String::from("site_name")), + federation_enabled: Some(true), + ..Default::default() + }, ), ( "EditSite requires application, but neither it nor LocalSite has an application question", LemmyErrorType::ApplicationQuestionRequired, - &generate_local_site( - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_edit_site( - Some(String::from("site_name")), - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - Some(RegistrationMode::RequireApplication), - None::, - ), + &LocalSite { + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &EditSite { + name: Some(String::from("site_name")), + registration_mode: Some(RegistrationMode::RequireApplication), + ..Default::default() + }, ), ]; @@ -449,99 +390,65 @@ mod tests { let valid_payloads = [ ( "No changes between LocalSite and EditSite", - &generate_local_site( - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_edit_site( - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - ), + &LocalSite { + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &EditSite::default(), ), ( "EditSite allows clearing and changing values", - &generate_local_site( - None::, - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_edit_site( - Some(String::from("site_name")), - Some(String::new()), - Some(String::new()), - Some(ListingType::All), - Some(PostSortType::Active), - Some(CommentSortType::Hot), - Some(String::new()), - Some(false), - Some(true), - Some(String::new()), - Some(RegistrationMode::Open), - None::, - ), + &LocalSite { + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &EditSite { + name: Some(String::from("site_name")), + sidebar: Some(String::new()), + description: Some(String::new()), + application_question: Some(String::new()), + private_instance: Some(false), + default_post_listing_type: Some(ListingType::All), + default_post_sort_type: Some(PostSortType::Active), + slur_filter_regex: Some(String::new()), + registration_mode: Some(RegistrationMode::Open), + federation_enabled: Some(true), + ..Default::default() + }, ), ( "EditSite name passes slur filter regex", - &generate_local_site( - Some(String::from("(foo|bar)")), - true, - false, - None::, - RegistrationMode::Open, - ), - &generate_edit_site( - Some(String::from("foo site_name")), - None::, - None::, - None::, - None::, - None::, - Some(String::new()), - None::, - None::, - None::, - None::, - None::, - ), + &LocalSite { + private_instance: true, + slur_filter_regex: Some(String::from("(foo|bar)")), + registration_mode: RegistrationMode::Open, + federation_enabled: false, + ..Default::default() + }, + &EditSite { + name: Some(String::from("foo site_name")), + slur_filter_regex: Some(String::new()), + ..Default::default() + }, ), ( "LocalSite has application question and EditSite now requires applications,", - &generate_local_site( - None::, - true, - false, - Some(String::from("question")), - RegistrationMode::Open, - ), - &generate_edit_site( - Some(String::from("site_name")), - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - None::, - Some(RegistrationMode::RequireApplication), - None::, - ), + &LocalSite { + application_question: Some(String::from("question")), + private_instance: true, + federation_enabled: false, + registration_mode: RegistrationMode::Open, + ..Default::default() + }, + &EditSite { + name: Some(String::from("site_name")), + registration_mode: Some(RegistrationMode::RequireApplication), + ..Default::default() + }, ), ]; @@ -557,88 +464,4 @@ mod tests { ); }) } - - fn generate_local_site( - site_slur_filter_regex: Option, - site_is_private: bool, - site_is_federated: bool, - site_application_question: Option, - site_registration_mode: RegistrationMode, - ) -> LocalSite { - LocalSite { - application_question: site_application_question, - private_instance: site_is_private, - slur_filter_regex: site_slur_filter_regex, - federation_enabled: site_is_federated, - registration_mode: site_registration_mode, - ..Default::default() - } - } - - // Allow the test helper function to have too many arguments. - // It's either this or generate the entire struct each time for testing. - #[allow(clippy::too_many_arguments)] - fn generate_edit_site( - site_name: Option, - site_description: Option, - site_sidebar: Option, - site_listing_type: Option, - site_post_sort_type: Option, - site_comment_sort_type: Option, - site_slur_filter_regex: Option, - site_is_private: Option, - site_is_federated: Option, - site_application_question: Option, - site_registration_mode: Option, - site_oauth_registration: Option, - ) -> EditSite { - EditSite { - name: site_name, - sidebar: site_sidebar, - description: site_description, - icon: None, - banner: None, - enable_downvotes: None, - enable_nsfw: None, - community_creation_admin_only: None, - require_email_verification: None, - application_question: site_application_question, - private_instance: site_is_private, - default_theme: None, - default_post_listing_type: site_listing_type, - default_post_sort_type: site_post_sort_type, - default_comment_sort_type: site_comment_sort_type, - legal_information: None, - application_email_admins: None, - hide_modlog_mod_names: None, - discussion_languages: None, - slur_filter_regex: site_slur_filter_regex, - actor_name_max_length: None, - rate_limit_message: None, - rate_limit_message_per_second: None, - rate_limit_post: None, - rate_limit_post_per_second: None, - rate_limit_register: None, - rate_limit_register_per_second: None, - rate_limit_image: None, - rate_limit_image_per_second: None, - rate_limit_comment: None, - rate_limit_comment_per_second: None, - rate_limit_search: None, - rate_limit_search_per_second: None, - federation_enabled: site_is_federated, - federation_debug: None, - captcha_enabled: None, - captcha_difficulty: None, - allowed_instances: None, - blocked_instances: None, - blocked_urls: None, - taglines: None, - registration_mode: site_registration_mode, - reports_email_admins: None, - content_warning: None, - default_post_listing_mode: None, - oauth_registration: site_oauth_registration, - } - } } From 43f20881cb2057d5784fac69a8944c00e57d5042 Mon Sep 17 00:00:00 2001 From: Freakazoid182 <5238563+Freakazoid182@users.noreply.github.com> Date: Thu, 19 Sep 2024 11:15:04 +0200 Subject: [PATCH 5/8] Feature/custom emoji and tagline views (#4580) * Add custom_emoji list route * Add tagline list route * Apply linting * Remove unecessary TaglineView * Add category filter for custom emoji * Add create tagline endpoint * Add update tagline endpoint * Add delete tagline endpoint * Format through lint.sh * Remove custom_emojis and taglines from site resource * Get random tagline on site requets * Impl Crud for Tagline Remove superfluous properties * Move tagline endpoints under /admin * Impl Crud for CustomEmoji * Remove delete from tagline and custom emoji impls * Check markdown for tagline * Validate markdown on tagline * Make content fields non optional Add error types for tagline validation * Use process_markdown instead of process_markdown_opt * Consolidate Tagline error types * Remove unecessary clone * Updat misleading comments * Remove local_site_id from tagline and custom_emoji * Update TaglineInserForm and TaglineUpdateForm * Add ignore_page_limits for custom emojis EmojiPicker needs to be able to retrieve all emojis in 1 call * Update custom_emoji_view Only keep get_all als helper function calling list with paging ignored Only order on category when filtering on category * Removing pointless get_all fn. * remove tagline length checks * make fields of TaglineInsertForm and TaglineUpdateForm mandatory * move emoji order statement * add comment for GetSiteResponse.tagline --------- Co-authored-by: Freakazoid182 <> Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com> Co-authored-by: Dessalines Co-authored-by: Felix Ableitner --- crates/api/src/site/leave_admin.rs | 9 +- crates/api_common/src/custom_emoji.rs | 21 +++++ crates/api_common/src/lib.rs | 1 + crates/api_common/src/site.rs | 11 +-- crates/api_common/src/tagline.rs | 55 ++++++++++++ crates/api_crud/src/custom_emoji/create.rs | 12 +-- crates/api_crud/src/custom_emoji/delete.rs | 2 +- crates/api_crud/src/custom_emoji/list.rs | 25 ++++++ crates/api_crud/src/custom_emoji/mod.rs | 1 + crates/api_crud/src/custom_emoji/update.rs | 12 +-- crates/api_crud/src/lib.rs | 1 + crates/api_crud/src/site/create.rs | 9 +- crates/api_crud/src/site/read.rs | 9 +- crates/api_crud/src/site/update.rs | 9 +- crates/api_crud/src/tagline/create.rs | 38 ++++++++ crates/api_crud/src/tagline/delete.rs | 25 ++++++ crates/api_crud/src/tagline/list.rs | 19 ++++ crates/api_crud/src/tagline/mod.rs | 4 + crates/api_crud/src/tagline/update.rs | 42 +++++++++ crates/db_schema/src/impls/custom_emoji.rs | 25 +++--- crates/db_schema/src/impls/tagline.rs | 90 ++++++++++--------- crates/db_schema/src/newtypes.rs | 6 ++ crates/db_schema/src/schema.rs | 4 - crates/db_schema/src/source/custom_emoji.rs | 14 +-- crates/db_schema/src/source/tagline.rs | 23 +++-- crates/db_views/src/custom_emoji_view.rs | 30 +++++-- .../down.sql | 32 +++++++ .../up.sql | 6 ++ src/api_routes_http.rs | 18 +++- 29 files changed, 412 insertions(+), 141 deletions(-) create mode 100644 crates/api_common/src/tagline.rs create mode 100644 crates/api_crud/src/custom_emoji/list.rs create mode 100644 crates/api_crud/src/tagline/create.rs create mode 100644 crates/api_crud/src/tagline/delete.rs create mode 100644 crates/api_crud/src/tagline/list.rs create mode 100644 crates/api_crud/src/tagline/mod.rs create mode 100644 crates/api_crud/src/tagline/update.rs create mode 100644 migrations/2024-04-08-204327_custom_emoji_tagline_changes/down.sql create mode 100644 migrations/2024-04-08-204327_custom_emoji_tagline_changes/up.sql diff --git a/crates/api/src/site/leave_admin.rs b/crates/api/src/site/leave_admin.rs index d3581995a2..5e1e69d494 100644 --- a/crates/api/src/site/leave_admin.rs +++ b/crates/api/src/site/leave_admin.rs @@ -12,7 +12,7 @@ use lemmy_db_schema::{ }, traits::Crud, }; -use lemmy_db_views::structs::{CustomEmojiView, LocalUserView, SiteView}; +use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views_actor::structs::PersonView; use lemmy_utils::{ error::{LemmyErrorType, LemmyResult}, @@ -61,11 +61,9 @@ pub async fn leave_admin( let all_languages = Language::read_all(&mut context.pool()).await?; let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?; - let taglines = Tagline::get_all(&mut context.pool(), site_view.local_site.id).await?; - let custom_emojis = - CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).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?; Ok(Json(GetSiteResponse { site_view, @@ -74,10 +72,9 @@ pub async fn leave_admin( my_user: None, all_languages, discussion_languages, - taglines, - custom_emojis, oauth_providers: Some(oauth_providers), admin_oauth_providers: None, blocked_urls, + tagline, })) } diff --git a/crates/api_common/src/custom_emoji.rs b/crates/api_common/src/custom_emoji.rs index 468d2128d9..3804b71af2 100644 --- a/crates/api_common/src/custom_emoji.rs +++ b/crates/api_common/src/custom_emoji.rs @@ -1,6 +1,7 @@ use lemmy_db_schema::newtypes::CustomEmojiId; use lemmy_db_views::structs::CustomEmojiView; use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; #[cfg(feature = "full")] use ts_rs::TS; use url::Url; @@ -46,3 +47,23 @@ pub struct DeleteCustomEmoji { pub struct CustomEmojiResponse { pub custom_emoji: CustomEmojiView, } + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// A response for custom emojis. +pub struct ListCustomEmojisResponse { + pub custom_emojis: Vec, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Fetches a list of custom emojis. +pub struct ListCustomEmojis { + pub page: Option, + pub limit: Option, + pub category: Option, + pub ignore_page_limits: Option, +} diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs index 48acaad216..68eeadecc5 100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@ -16,6 +16,7 @@ pub mod request; #[cfg(feature = "full")] pub mod send_activity; pub mod site; +pub mod tagline; #[cfg(feature = "full")] pub mod utils; diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index f25dcf94bc..a4f4ea71e0 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -30,7 +30,6 @@ use lemmy_db_schema::{ }; use lemmy_db_views::structs::{ CommentView, - CustomEmojiView, LocalUserView, PostView, RegistrationApplicationView, @@ -202,7 +201,6 @@ pub struct CreateSite { pub captcha_difficulty: Option, pub allowed_instances: Option>, pub blocked_instances: Option>, - pub taglines: Option>, pub registration_mode: Option, pub oauth_registration: Option, pub content_warning: Option, @@ -288,8 +286,6 @@ pub struct EditSite { pub blocked_instances: Option>, /// A list of blocked URLs pub blocked_urls: Option>, - /// A list of taglines shown at the top of the front page. - pub taglines: Option>, pub registration_mode: Option, /// Whether or not external auth methods can auto-register users. pub oauth_registration: Option, @@ -306,7 +302,6 @@ pub struct EditSite { /// The response for a site. pub struct SiteResponse { pub site_view: SiteView, - pub taglines: Vec, } #[skip_serializing_none] @@ -321,10 +316,8 @@ pub struct GetSiteResponse { pub my_user: Option, pub all_languages: Vec, pub discussion_languages: Vec, - /// A list of taglines shown at the top of the front page. - pub taglines: Vec, - /// A list of custom emojis your site supports. - pub custom_emojis: Vec, + /// If the site has any taglines, a random one is included here for displaying + pub tagline: Option, /// A list of external auth methods your site supports. pub oauth_providers: Option>, pub admin_oauth_providers: Option>, diff --git a/crates/api_common/src/tagline.rs b/crates/api_common/src/tagline.rs new file mode 100644 index 0000000000..3090a2678a --- /dev/null +++ b/crates/api_common/src/tagline.rs @@ -0,0 +1,55 @@ +use lemmy_db_schema::{newtypes::TaglineId, source::tagline::Tagline}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Create a tagline +pub struct CreateTagline { + pub content: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Update a tagline +pub struct UpdateTagline { + pub id: TaglineId, + pub content: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Delete a tagline +pub struct DeleteTagline { + pub id: TaglineId, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct TaglineResponse { + pub tagline: Tagline, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// A response for taglines. +pub struct ListTaglinesResponse { + pub taglines: Vec, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Fetches a list of taglines. +pub struct ListTaglines { + pub page: Option, + pub limit: Option, +} diff --git a/crates/api_crud/src/custom_emoji/create.rs b/crates/api_crud/src/custom_emoji/create.rs index 3c5ce32961..654b756313 100644 --- a/crates/api_crud/src/custom_emoji/create.rs +++ b/crates/api_crud/src/custom_emoji/create.rs @@ -5,10 +5,12 @@ use lemmy_api_common::{ custom_emoji::{CreateCustomEmoji, CustomEmojiResponse}, utils::is_admin, }; -use lemmy_db_schema::source::{ - custom_emoji::{CustomEmoji, CustomEmojiInsertForm}, - custom_emoji_keyword::{CustomEmojiKeyword, CustomEmojiKeywordInsertForm}, - local_site::LocalSite, +use lemmy_db_schema::{ + source::{ + custom_emoji::{CustomEmoji, CustomEmojiInsertForm}, + custom_emoji_keyword::{CustomEmojiKeyword, CustomEmojiKeywordInsertForm}, + }, + traits::Crud, }; use lemmy_db_views::structs::{CustomEmojiView, LocalUserView}; use lemmy_utils::error::LemmyResult; @@ -19,12 +21,10 @@ pub async fn create_custom_emoji( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let local_site = LocalSite::read(&mut context.pool()).await?; // Make sure user is an admin is_admin(&local_user_view)?; let emoji_form = CustomEmojiInsertForm::builder() - .local_site_id(local_site.id) .shortcode(data.shortcode.to_lowercase().trim().to_string()) .alt_text(data.alt_text.to_string()) .category(data.category.to_string()) diff --git a/crates/api_crud/src/custom_emoji/delete.rs b/crates/api_crud/src/custom_emoji/delete.rs index 45ac8d0baf..818fd4d88a 100644 --- a/crates/api_crud/src/custom_emoji/delete.rs +++ b/crates/api_crud/src/custom_emoji/delete.rs @@ -6,7 +6,7 @@ use lemmy_api_common::{ utils::is_admin, SuccessResponse, }; -use lemmy_db_schema::source::custom_emoji::CustomEmoji; +use lemmy_db_schema::{source::custom_emoji::CustomEmoji, traits::Crud}; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyResult; diff --git a/crates/api_crud/src/custom_emoji/list.rs b/crates/api_crud/src/custom_emoji/list.rs new file mode 100644 index 0000000000..6ee5a44b03 --- /dev/null +++ b/crates/api_crud/src/custom_emoji/list.rs @@ -0,0 +1,25 @@ +use actix_web::web::{Data, Json, Query}; +use lemmy_api_common::{ + context::LemmyContext, + custom_emoji::{ListCustomEmojis, ListCustomEmojisResponse}, +}; +use lemmy_db_views::structs::{CustomEmojiView, LocalUserView}; +use lemmy_utils::error::LemmyError; + +#[tracing::instrument(skip(context))] +pub async fn list_custom_emojis( + data: Query, + local_user_view: Option, + context: Data, +) -> Result, LemmyError> { + let custom_emojis = CustomEmojiView::list( + &mut context.pool(), + &data.category, + data.page, + data.limit, + data.ignore_page_limits.unwrap_or(false), + ) + .await?; + + Ok(Json(ListCustomEmojisResponse { custom_emojis })) +} diff --git a/crates/api_crud/src/custom_emoji/mod.rs b/crates/api_crud/src/custom_emoji/mod.rs index fdb2f55613..ffd48daf6b 100644 --- a/crates/api_crud/src/custom_emoji/mod.rs +++ b/crates/api_crud/src/custom_emoji/mod.rs @@ -1,3 +1,4 @@ pub mod create; pub mod delete; +pub mod list; pub mod update; diff --git a/crates/api_crud/src/custom_emoji/update.rs b/crates/api_crud/src/custom_emoji/update.rs index 63246f85da..62d1d819d1 100644 --- a/crates/api_crud/src/custom_emoji/update.rs +++ b/crates/api_crud/src/custom_emoji/update.rs @@ -5,10 +5,12 @@ use lemmy_api_common::{ custom_emoji::{CustomEmojiResponse, EditCustomEmoji}, utils::is_admin, }; -use lemmy_db_schema::source::{ - custom_emoji::{CustomEmoji, CustomEmojiUpdateForm}, - custom_emoji_keyword::{CustomEmojiKeyword, CustomEmojiKeywordInsertForm}, - local_site::LocalSite, +use lemmy_db_schema::{ + source::{ + custom_emoji::{CustomEmoji, CustomEmojiUpdateForm}, + custom_emoji_keyword::{CustomEmojiKeyword, CustomEmojiKeywordInsertForm}, + }, + traits::Crud, }; use lemmy_db_views::structs::{CustomEmojiView, LocalUserView}; use lemmy_utils::error::LemmyResult; @@ -19,12 +21,10 @@ pub async fn update_custom_emoji( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let local_site = LocalSite::read(&mut context.pool()).await?; // Make sure user is an admin is_admin(&local_user_view)?; let emoji_form = CustomEmojiUpdateForm::builder() - .local_site_id(local_site.id) .alt_text(data.alt_text.to_string()) .category(data.category.to_string()) .image_url(data.clone().image_url.into()) diff --git a/crates/api_crud/src/lib.rs b/crates/api_crud/src/lib.rs index b138fbd30a..7d1b901b91 100644 --- a/crates/api_crud/src/lib.rs +++ b/crates/api_crud/src/lib.rs @@ -5,4 +5,5 @@ pub mod oauth_provider; pub mod post; pub mod private_message; pub mod site; +pub mod tagline; pub mod user; diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index 30321d8dda..162f5a355d 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -20,7 +20,6 @@ use lemmy_db_schema::{ local_site::{LocalSite, LocalSiteUpdateForm}, local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitUpdateForm}, site::{Site, SiteUpdateForm}, - tagline::Tagline, }, traits::Crud, utils::{diesel_string_update, diesel_url_create, naive_now}, @@ -135,17 +134,11 @@ pub async fn create_site( let site_view = SiteView::read_local(&mut context.pool()).await?; - let new_taglines = data.taglines.clone(); - let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?; - let rate_limit_config = local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit); context.rate_limit_cell().set_config(rate_limit_config); - Ok(Json(SiteResponse { - site_view, - taglines, - })) + Ok(Json(SiteResponse { site_view })) } fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) -> LemmyResult<()> { diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs index 6f524dd7d2..0901b9186b 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -13,7 +13,7 @@ use lemmy_db_schema::source::{ person_block::PersonBlock, tagline::Tagline, }; -use lemmy_db_views::structs::{CustomEmojiView, LocalUserView, SiteView}; +use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views_actor::structs::{CommunityFollowerView, CommunityModeratorView, PersonView}; use lemmy_utils::{ error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}, @@ -42,10 +42,8 @@ pub async fn get_site( let admins = PersonView::admins(&mut context.pool()).await?; let all_languages = Language::read_all(&mut context.pool()).await?; let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?; - let taglines = Tagline::get_all(&mut context.pool(), site_view.local_site.id).await?; - let custom_emojis = - CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?; let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?; + let tagline = Tagline::get_random(&mut context.pool()).await?; let admin_oauth_providers = OAuthProvider::get_all(&mut context.pool()).await?; let oauth_providers = OAuthProvider::convert_providers_to_public(admin_oauth_providers.clone()); @@ -57,9 +55,8 @@ pub async fn get_site( my_user: None, all_languages, discussion_languages, - taglines, - custom_emojis, blocked_urls, + tagline, oauth_providers: Some(oauth_providers), admin_oauth_providers: Some(admin_oauth_providers), }) diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index a6046b4235..1ae531bb7c 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -24,7 +24,6 @@ use lemmy_db_schema::{ local_site_url_blocklist::LocalSiteUrlBlocklist, local_user::LocalUser, site::{Site, SiteUpdateForm}, - tagline::Tagline, }, traits::Crud, utils::{diesel_string_update, diesel_url_update, naive_now}, @@ -187,19 +186,13 @@ pub async fn update_site( .with_lemmy_type(LemmyErrorType::CouldntSetAllEmailVerified)?; } - let new_taglines = data.taglines.clone(); - let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?; - let site_view = SiteView::read_local(&mut context.pool()).await?; let rate_limit_config = local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit); context.rate_limit_cell().set_config(rate_limit_config); - Ok(Json(SiteResponse { - site_view, - taglines, - })) + Ok(Json(SiteResponse { site_view })) } fn validate_update_payload(local_site: &LocalSite, edit_site: &EditSite) -> LemmyResult<()> { diff --git a/crates/api_crud/src/tagline/create.rs b/crates/api_crud/src/tagline/create.rs new file mode 100644 index 0000000000..f67a26f68b --- /dev/null +++ b/crates/api_crud/src/tagline/create.rs @@ -0,0 +1,38 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + context::LemmyContext, + tagline::{CreateTagline, TaglineResponse}, + utils::{get_url_blocklist, is_admin, local_site_to_slur_regex, process_markdown}, +}; +use lemmy_db_schema::{ + source::{ + local_site::LocalSite, + tagline::{Tagline, TaglineInsertForm}, + }, + traits::Crud, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyError; + +#[tracing::instrument(skip(context))] +pub async fn create_tagline( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> Result, LemmyError> { + // Make sure user is an admin + is_admin(&local_user_view)?; + + let local_site = LocalSite::read(&mut context.pool()).await?; + + let slur_regex = local_site_to_slur_regex(&local_site); + let url_blocklist = get_url_blocklist(&context).await?; + let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?; + + let tagline_form = TaglineInsertForm { content }; + + let tagline = Tagline::create(&mut context.pool(), &tagline_form).await?; + + Ok(Json(TaglineResponse { tagline })) +} diff --git a/crates/api_crud/src/tagline/delete.rs b/crates/api_crud/src/tagline/delete.rs new file mode 100644 index 0000000000..9add3cfe64 --- /dev/null +++ b/crates/api_crud/src/tagline/delete.rs @@ -0,0 +1,25 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + context::LemmyContext, + tagline::DeleteTagline, + utils::is_admin, + SuccessResponse, +}; +use lemmy_db_schema::{source::tagline::Tagline, traits::Crud}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyError; + +#[tracing::instrument(skip(context))] +pub async fn delete_tagline( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> Result, LemmyError> { + // Make sure user is an admin + is_admin(&local_user_view)?; + + Tagline::delete(&mut context.pool(), data.id).await?; + + Ok(Json(SuccessResponse::default())) +} diff --git a/crates/api_crud/src/tagline/list.rs b/crates/api_crud/src/tagline/list.rs new file mode 100644 index 0000000000..21929f5478 --- /dev/null +++ b/crates/api_crud/src/tagline/list.rs @@ -0,0 +1,19 @@ +use actix_web::web::{Data, Json, Query}; +use lemmy_api_common::{ + context::LemmyContext, + tagline::{ListTaglines, ListTaglinesResponse}, +}; +use lemmy_db_schema::source::tagline::Tagline; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyError; + +#[tracing::instrument(skip(context))] +pub async fn list_taglines( + data: Query, + local_user_view: Option, + context: Data, +) -> Result, LemmyError> { + let taglines = Tagline::list(&mut context.pool(), data.page, data.limit).await?; + + Ok(Json(ListTaglinesResponse { taglines })) +} diff --git a/crates/api_crud/src/tagline/mod.rs b/crates/api_crud/src/tagline/mod.rs new file mode 100644 index 0000000000..ffd48daf6b --- /dev/null +++ b/crates/api_crud/src/tagline/mod.rs @@ -0,0 +1,4 @@ +pub mod create; +pub mod delete; +pub mod list; +pub mod update; diff --git a/crates/api_crud/src/tagline/update.rs b/crates/api_crud/src/tagline/update.rs new file mode 100644 index 0000000000..043589d26b --- /dev/null +++ b/crates/api_crud/src/tagline/update.rs @@ -0,0 +1,42 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + context::LemmyContext, + tagline::{TaglineResponse, UpdateTagline}, + utils::{get_url_blocklist, is_admin, local_site_to_slur_regex, process_markdown}, +}; +use lemmy_db_schema::{ + source::{ + local_site::LocalSite, + tagline::{Tagline, TaglineUpdateForm}, + }, + traits::Crud, + utils::naive_now, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyError; + +#[tracing::instrument(skip(context))] +pub async fn update_tagline( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> Result, LemmyError> { + // Make sure user is an admin + is_admin(&local_user_view)?; + + let local_site = LocalSite::read(&mut context.pool()).await?; + + let slur_regex = local_site_to_slur_regex(&local_site); + let url_blocklist = get_url_blocklist(&context).await?; + let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?; + + let tagline_form = TaglineUpdateForm { + content, + updated: naive_now(), + }; + + let tagline = Tagline::update(&mut context.pool(), data.id, &tagline_form).await?; + + Ok(Json(TaglineResponse { tagline })) +} diff --git a/crates/db_schema/src/impls/custom_emoji.rs b/crates/db_schema/src/impls/custom_emoji.rs index 0503016594..9ba3590716 100644 --- a/crates/db_schema/src/impls/custom_emoji.rs +++ b/crates/db_schema/src/impls/custom_emoji.rs @@ -8,36 +8,37 @@ use crate::{ custom_emoji::{CustomEmoji, CustomEmojiInsertForm, CustomEmojiUpdateForm}, custom_emoji_keyword::{CustomEmojiKeyword, CustomEmojiKeywordInsertForm}, }, + traits::Crud, utils::{get_conn, DbPool}, }; use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; -impl CustomEmoji { - pub async fn create(pool: &mut DbPool<'_>, form: &CustomEmojiInsertForm) -> Result { +#[async_trait] +impl Crud for CustomEmoji { + type InsertForm = CustomEmojiInsertForm; + type UpdateForm = CustomEmojiUpdateForm; + type IdType = CustomEmojiId; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { let conn = &mut get_conn(pool).await?; insert_into(custom_emoji) .values(form) .get_result::(conn) .await } - pub async fn update( + + async fn update( pool: &mut DbPool<'_>, - emoji_id: CustomEmojiId, - form: &CustomEmojiUpdateForm, + emoji_id: Self::IdType, + new_custom_emoji: &Self::UpdateForm, ) -> Result { let conn = &mut get_conn(pool).await?; diesel::update(custom_emoji.find(emoji_id)) - .set(form) + .set(new_custom_emoji) .get_result::(conn) .await } - pub async fn delete(pool: &mut DbPool<'_>, emoji_id: CustomEmojiId) -> Result { - let conn = &mut get_conn(pool).await?; - diesel::delete(custom_emoji.find(emoji_id)) - .execute(conn) - .await - } } impl CustomEmojiKeyword { diff --git a/crates/db_schema/src/impls/tagline.rs b/crates/db_schema/src/impls/tagline.rs index be4860e17f..656d537d6b 100644 --- a/crates/db_schema/src/impls/tagline.rs +++ b/crates/db_schema/src/impls/tagline.rs @@ -1,58 +1,64 @@ use crate::{ - newtypes::LocalSiteId, - schema::tagline::dsl::{local_site_id, tagline}, - source::tagline::{Tagline, TaglineForm}, - utils::{get_conn, DbPool}, + newtypes::TaglineId, + schema::tagline::dsl::{published, tagline}, + source::tagline::{Tagline, TaglineInsertForm, TaglineUpdateForm}, + traits::Crud, + utils::{get_conn, limit_and_offset, DbPool}, }; -use diesel::{insert_into, result::Error, ExpressionMethods, QueryDsl}; -use diesel_async::{AsyncPgConnection, RunQueryDsl}; +use diesel::{insert_into, result::Error, ExpressionMethods, OptionalExtension, QueryDsl}; +use diesel_async::RunQueryDsl; -impl Tagline { - pub async fn replace( - pool: &mut DbPool<'_>, - for_local_site_id: LocalSiteId, - list_content: Option>, - ) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - if let Some(list) = list_content { - conn - .build_transaction() - .run(|conn| { - Box::pin(async move { - Self::clear(conn).await?; +#[async_trait] +impl Crud for Tagline { + type InsertForm = TaglineInsertForm; + type UpdateForm = TaglineUpdateForm; + type IdType = TaglineId; - for item in list { - let form = TaglineForm { - local_site_id: for_local_site_id, - content: item, - updated: None, - }; - insert_into(tagline) - .values(form) - .get_result::(conn) - .await?; - } - Self::get_all(&mut conn.into(), for_local_site_id).await - }) as _ - }) - .await - } else { - Self::get_all(&mut conn.into(), for_local_site_id).await - } + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { + let conn = &mut get_conn(pool).await?; + insert_into(tagline) + .values(form) + .get_result::(conn) + .await } - async fn clear(conn: &mut AsyncPgConnection) -> Result { - diesel::delete(tagline).execute(conn).await + async fn update( + pool: &mut DbPool<'_>, + tagline_id: TaglineId, + new_tagline: &Self::UpdateForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + diesel::update(tagline.find(tagline_id)) + .set(new_tagline) + .get_result::(conn) + .await } +} - pub async fn get_all( +impl Tagline { + pub async fn list( pool: &mut DbPool<'_>, - for_local_site_id: LocalSiteId, + page: Option, + limit: Option, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; + let (limit, offset) = limit_and_offset(page, limit)?; tagline - .filter(local_site_id.eq(for_local_site_id)) + .order(published.desc()) + .offset(offset) + .limit(limit) .get_results::(conn) .await } + + pub async fn get_random(pool: &mut DbPool<'_>) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + sql_function!(fn random() -> Text); + tagline + .order(random()) + .limit(1) + .first::(conn) + .await + .optional() + } } diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index d90b1f3f6a..58396c66ae 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -148,6 +148,12 @@ pub struct LocalSiteId(i32); /// The custom emoji id. pub struct CustomEmojiId(i32); +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The tagline id. +pub struct TaglineId(i32); + #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index b5f68822ce..289032e008 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -258,7 +258,6 @@ diesel::table! { diesel::table! { custom_emoji (id) { id -> Int4, - local_site_id -> Int4, #[max_length = 128] shortcode -> Varchar, image_url -> Text, @@ -974,7 +973,6 @@ diesel::table! { diesel::table! { tagline (id) { id -> Int4, - local_site_id -> Int4, content -> Text, published -> Timestamptz, updated -> Nullable, @@ -1011,7 +1009,6 @@ diesel::joinable!(community_moderator -> community (community_id)); diesel::joinable!(community_moderator -> person (person_id)); diesel::joinable!(community_person_ban -> community (community_id)); diesel::joinable!(community_person_ban -> person (person_id)); -diesel::joinable!(custom_emoji -> local_site (local_site_id)); diesel::joinable!(custom_emoji_keyword -> custom_emoji (custom_emoji_id)); diesel::joinable!(email_verification -> local_user (local_user_id)); diesel::joinable!(federation_allowlist -> instance (instance_id)); @@ -1075,7 +1072,6 @@ diesel::joinable!(site -> instance (instance_id)); diesel::joinable!(site_aggregates -> site (site_id)); diesel::joinable!(site_language -> language (language_id)); diesel::joinable!(site_language -> site (site_id)); -diesel::joinable!(tagline -> local_site (local_site_id)); diesel::allow_tables_to_appear_in_same_query!( admin_purge_comment, diff --git a/crates/db_schema/src/source/custom_emoji.rs b/crates/db_schema/src/source/custom_emoji.rs index 3217c9736b..788885c978 100644 --- a/crates/db_schema/src/source/custom_emoji.rs +++ b/crates/db_schema/src/source/custom_emoji.rs @@ -1,4 +1,4 @@ -use crate::newtypes::{CustomEmojiId, DbUrl, LocalSiteId}; +use crate::newtypes::{CustomEmojiId, DbUrl}; #[cfg(feature = "full")] use crate::schema::custom_emoji; use chrono::{DateTime, Utc}; @@ -10,21 +10,13 @@ use typed_builder::TypedBuilder; #[skip_serializing_none] #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] -#[cfg_attr( - feature = "full", - derive(Queryable, Selectable, Associations, Identifiable, TS) -)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] #[cfg_attr(feature = "full", diesel(table_name = custom_emoji))] -#[cfg_attr( - feature = "full", - diesel(belongs_to(crate::source::local_site::LocalSite)) -)] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] /// A custom emoji. pub struct CustomEmoji { pub id: CustomEmojiId, - pub local_site_id: LocalSiteId, pub shortcode: String, pub image_url: DbUrl, pub alt_text: String, @@ -37,7 +29,6 @@ pub struct CustomEmoji { #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = custom_emoji))] pub struct CustomEmojiInsertForm { - pub local_site_id: LocalSiteId, pub shortcode: String, pub image_url: DbUrl, pub alt_text: String, @@ -48,7 +39,6 @@ pub struct CustomEmojiInsertForm { #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = custom_emoji))] pub struct CustomEmojiUpdateForm { - pub local_site_id: LocalSiteId, pub image_url: DbUrl, pub alt_text: String, pub category: String, diff --git a/crates/db_schema/src/source/tagline.rs b/crates/db_schema/src/source/tagline.rs index dbc904a785..05f7e0520b 100644 --- a/crates/db_schema/src/source/tagline.rs +++ b/crates/db_schema/src/source/tagline.rs @@ -1,4 +1,3 @@ -use crate::newtypes::LocalSiteId; #[cfg(feature = "full")] use crate::schema::tagline; use chrono::{DateTime, Utc}; @@ -9,21 +8,13 @@ use ts_rs::TS; #[skip_serializing_none] #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] -#[cfg_attr( - feature = "full", - derive(Queryable, Selectable, Associations, Identifiable, TS) -)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] #[cfg_attr(feature = "full", diesel(table_name = tagline))] -#[cfg_attr( - feature = "full", - diesel(belongs_to(crate::source::local_site::LocalSite)) -)] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] /// A tagline, shown at the top of your site. pub struct Tagline { pub id: i32, - pub local_site_id: LocalSiteId, pub content: String, pub published: DateTime, pub updated: Option>, @@ -32,8 +23,14 @@ pub struct Tagline { #[derive(Clone, Default)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = tagline))] -pub struct TaglineForm { - pub local_site_id: LocalSiteId, +pub struct TaglineInsertForm { pub content: String, - pub updated: Option>, +} + +#[derive(Clone, Default)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = tagline))] +pub struct TaglineUpdateForm { + pub content: String, + pub updated: DateTime, } diff --git a/crates/db_views/src/custom_emoji_view.rs b/crates/db_views/src/custom_emoji_view.rs index 4d2f1fd859..a346c086dd 100644 --- a/crates/db_views/src/custom_emoji_view.rs +++ b/crates/db_views/src/custom_emoji_view.rs @@ -2,10 +2,10 @@ use crate::structs::CustomEmojiView; use diesel::{result::Error, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ - newtypes::{CustomEmojiId, LocalSiteId}, + newtypes::CustomEmojiId, schema::{custom_emoji, custom_emoji_keyword}, source::{custom_emoji::CustomEmoji, custom_emoji_keyword::CustomEmojiKeyword}, - utils::{get_conn, DbPool}, + utils::{get_conn, limit_and_offset, DbPool}, }; use std::collections::HashMap; @@ -35,18 +35,34 @@ impl CustomEmojiView { } } - pub async fn get_all( + pub async fn list( pool: &mut DbPool<'_>, - for_local_site_id: LocalSiteId, + category: &Option, + page: Option, + limit: Option, + ignore_page_limits: bool, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - let emojis = custom_emoji::table - .filter(custom_emoji::local_site_id.eq(for_local_site_id)) + + let mut query = custom_emoji::table .left_join( custom_emoji_keyword::table.on(custom_emoji_keyword::custom_emoji_id.eq(custom_emoji::id)), ) .order(custom_emoji::category) - .then_order_by(custom_emoji::id) + .into_boxed(); + + if !ignore_page_limits { + let (limit, offset) = limit_and_offset(page, limit)?; + query = query.limit(limit).offset(offset); + } + + if let Some(category) = category { + query = query.filter(custom_emoji::category.eq(category)) + } + + query = query.then_order_by(custom_emoji::id); + + let emojis = query .select(( custom_emoji::all_columns, custom_emoji_keyword::all_columns.nullable(), // (or all the columns if you want) diff --git a/migrations/2024-04-08-204327_custom_emoji_tagline_changes/down.sql b/migrations/2024-04-08-204327_custom_emoji_tagline_changes/down.sql new file mode 100644 index 0000000000..a6b01a1d1f --- /dev/null +++ b/migrations/2024-04-08-204327_custom_emoji_tagline_changes/down.sql @@ -0,0 +1,32 @@ +ALTER TABLE custom_emoji + ADD COLUMN local_site_id int REFERENCES local_site (site_id) ON UPDATE CASCADE ON DELETE CASCADE; + +UPDATE + custom_emoji +SET + local_site_id = ( + SELECT + site_id + FROM + local_site + LIMIT 1); + +ALTER TABLE custom_emoji + ALTER COLUMN local_site_id SET NOT NULL; + +ALTER TABLE tagline + ADD COLUMN local_site_id int REFERENCES local_site (site_id) ON UPDATE CASCADE ON DELETE CASCADE; + +UPDATE + tagline +SET + local_site_id = ( + SELECT + site_id + FROM + local_site + LIMIT 1); + +ALTER TABLE tagline + ALTER COLUMN local_site_id SET NOT NULL; + diff --git a/migrations/2024-04-08-204327_custom_emoji_tagline_changes/up.sql b/migrations/2024-04-08-204327_custom_emoji_tagline_changes/up.sql new file mode 100644 index 0000000000..5aa073f766 --- /dev/null +++ b/migrations/2024-04-08-204327_custom_emoji_tagline_changes/up.sql @@ -0,0 +1,6 @@ +ALTER TABLE custom_emoji + DROP COLUMN local_site_id; + +ALTER TABLE tagline + DROP COLUMN local_site_id; + diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index faa7d78f2b..65931e8102 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -107,6 +107,7 @@ use lemmy_api_crud::{ custom_emoji::{ create::create_custom_emoji, delete::delete_custom_emoji, + list::list_custom_emojis, update::update_custom_emoji, }, oauth_provider::{ @@ -128,6 +129,12 @@ use lemmy_api_crud::{ update::update_private_message, }, site::{create::create_site, read::get_site, update::update_site}, + tagline::{ + create::create_tagline, + delete::delete_tagline, + list::list_taglines, + update::update_tagline, + }, user::{ create::{authenticate_with_oauth, register}, delete::delete_account, @@ -381,6 +388,14 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { .route("/community", web::post().to(purge_community)) .route("/post", web::post().to(purge_post)) .route("/comment", web::post().to(purge_comment)), + ) + .service( + web::scope("/tagline") + .wrap(rate_limit.message()) + .route("", web::post().to(create_tagline)) + .route("", web::put().to(update_tagline)) + .route("/delete", web::post().to(delete_tagline)) + .route("/list", web::get().to(list_taglines)), ), ) .service( @@ -388,7 +403,8 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { .wrap(rate_limit.message()) .route("", web::post().to(create_custom_emoji)) .route("", web::put().to(update_custom_emoji)) - .route("/delete", web::post().to(delete_custom_emoji)), + .route("/delete", web::post().to(delete_custom_emoji)) + .route("/list", web::get().to(list_custom_emojis)), ) .service( web::scope("/oauth_provider") From 89745bb37d184a4144c860a0eac8a1aec3684000 Mon Sep 17 00:00:00 2001 From: Nutomic Date: Thu, 19 Sep 2024 15:43:58 +0200 Subject: [PATCH 6/8] Add category to RSS feeds (fixes #3446) (#5030) --- crates/routes/src/feeds.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/routes/src/feeds.rs b/crates/routes/src/feeds.rs index 80c1c7281c..2db3f96614 100644 --- a/crates/routes/src/feeds.rs +++ b/crates/routes/src/feeds.rs @@ -27,6 +27,7 @@ use lemmy_utils::{ }; use rss::{ extension::{dublincore::DublinCoreExtension, ExtensionBuilder, ExtensionMap}, + Category, Channel, EnclosureBuilder, Guid, @@ -559,6 +560,10 @@ fn create_post_items(posts: Vec, protocol_and_hostname: &str) -> Lemmy BTreeMap::from([("content".to_string(), vec![thumbnail_ext.build()])]), ); } + let category = Category { + name: p.community.title, + domain: Some(p.community.actor_id.to_string()), + }; let i = Item { title: Some(sanitize_html(sanitize_xml(p.post.name).as_str())), @@ -570,6 +575,7 @@ fn create_post_items(posts: Vec, protocol_and_hostname: &str) -> Lemmy link: Some(post_url.clone()), extensions, enclosure: enclosure_opt, + categories: vec![category], ..Default::default() }; From dbb8f9553ae1b0ce0d9bdcf0c2f1c0be09a96ff5 Mon Sep 17 00:00:00 2001 From: leoseg <70430884+leoseg@users.noreply.github.com> Date: Thu, 19 Sep 2024 23:00:07 +0200 Subject: [PATCH 7/8] Unittest for Search by title only (#5033) * added test for search by title only * formatted rust files --- crates/db_views/src/post_view.rs | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index acd8debf33..3eab04af54 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -780,6 +780,7 @@ mod tests { use std::{collections::HashSet, time::Duration}; use url::Url; + const POST_WITH_ANOTHER_TITLE: &str = "Another title"; const POST_BY_BLOCKED_PERSON: &str = "post by blocked person"; const POST_BY_BOT: &str = "post by bot"; const POST: &str = "post"; @@ -1028,6 +1029,63 @@ mod tests { cleanup(data, pool).await } + #[tokio::test] + #[serial] + async fn post_listing_title_only() -> LemmyResult<()> { + let pool = &build_db_pool().await?; + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // A post which contains the search them 'Post' not in the title (but in the body) + let new_post = PostInsertForm::builder() + .name(POST_WITH_ANOTHER_TITLE.to_string()) + .creator_id(data.local_user_view.person.id) + .community_id(data.inserted_community.id) + .language_id(Some(LanguageId(47))) + .body(Some("Post".to_string())) + .build(); + + let inserted_post = Post::create(pool, &new_post).await?; + + let read_post_listing_by_title_only = PostQuery { + community_id: Some(data.inserted_community.id), + local_user: None, + search_term: Some("Post".to_string()), + title_only: Some(true), + ..data.default_post_query() + } + .list(&data.site, pool) + .await?; + + let read_post_listing = PostQuery { + community_id: Some(data.inserted_community.id), + local_user: None, + search_term: Some("Post".to_string()), + ..data.default_post_query() + } + .list(&data.site, pool) + .await?; + + // Should be 4 posts when we do not search for title only + assert_eq!( + vec![ + POST_WITH_ANOTHER_TITLE, + POST_BY_BOT, + POST, + POST_BY_BLOCKED_PERSON + ], + names(&read_post_listing) + ); + + // Should be 3 posts when we search for title only + assert_eq!( + vec![POST_BY_BOT, POST, POST_BY_BLOCKED_PERSON], + names(&read_post_listing_by_title_only) + ); + Post::delete(pool, inserted_post.id).await?; + cleanup(data, pool).await + } + #[tokio::test] #[serial] async fn post_listing_block_community() -> LemmyResult<()> { From 5a722146b5a9eb6ecd98e6752561006acfc30bee Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 19 Sep 2024 17:00:20 -0400 Subject: [PATCH 8/8] Upgrading to rust 1.81 (#5032) --- .woodpecker.yml | 2 +- docker/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 078561be64..55f0ec8b2a 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -2,7 +2,7 @@ # See https://github.com/woodpecker-ci/woodpecker/issues/1677 variables: - - &rust_image "rust:1.80" + - &rust_image "rust:1.81" - &rust_nightly_image "rustlang/rust:nightly" - &install_pnpm "corepack enable pnpm" - &install_binstall "wget -O- https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz | tar -xvz -C /usr/local/cargo/bin" diff --git a/docker/Dockerfile b/docker/Dockerfile index 156d30dcc3..68490bfd9f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1.9 -ARG RUST_VERSION=1.80 +ARG RUST_VERSION=1.81 ARG CARGO_BUILD_FEATURES=default ARG RUST_RELEASE_MODE=debug