Compare commits

...

10 Commits

Author SHA1 Message Date
Dessalines
dff5feb486
Merge c4e0c24408 into 777992e829 2024-09-30 23:11:09 -04:00
renovate[bot]
777992e829
Update Rust crate reqwest to v0.12.8 (#5068)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 22:04:24 -04:00
renovate[bot]
0fcbd25ad5
Update Rust crate clap to v4.5.18 (#5066)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 21:42:11 -04:00
Dessalines
c4e0c24408 Simpler activitypub vote check. 2024-09-30 20:32:09 -04:00
Dessalines
f583041d5f Merge remote-tracking branch 'origin/main' into reject_federated_votes 2024-09-30 20:28:28 -04:00
Nutomic
44dda08b13
Avoid stack overflow when fetching nested comments, reduce max comment depth to 50 (#5009)
* Avoid stack overflow when fetching deeply nested comments

* add test case

* reduce comment depth, add docs

* decrease

* reduce max comment depth to 50

* fmt

* clippy

* cleanup
2024-09-30 20:27:14 -04:00
Nutomic
5115ed4c09
Handle partial settings backup (fixes #4307) (#5063)
* Handle partial settings backup (fixes #4307)

* clippy
2024-09-30 20:21:06 -04:00
Dessalines
4ad1fffee8 Adding new vote mode types. 2024-09-25 09:24:19 -04:00
Dessalines
a3b7a46e93 Merge remote-tracking branch 'origin/main' into reject_federated_votes 2024-09-24 19:43:18 -04:00
Dessalines
f0adbaab72 Adding local site settings to reject federated upvotes or downvotes.
- Should help defend against downvote spamming instances.
- Fixes #4086
2024-09-20 10:21:38 -04:00
21 changed files with 292 additions and 105 deletions

40
Cargo.lock generated
View File

@ -42,7 +42,7 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"rand", "rand",
"regex", "regex",
"reqwest 0.12.7", "reqwest 0.12.8",
"reqwest-middleware", "reqwest-middleware",
"rsa", "rsa",
"serde", "serde",
@ -839,9 +839,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.17" version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -849,9 +849,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.17" version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -861,9 +861,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.13" version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@ -1989,7 +1989,7 @@ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"http-signature-normalization", "http-signature-normalization",
"httpdate", "httpdate",
"reqwest 0.12.7", "reqwest 0.12.8",
"reqwest-middleware", "reqwest-middleware",
"sha2", "sha2",
"thiserror", "thiserror",
@ -2514,7 +2514,7 @@ dependencies = [
"moka", "moka",
"pretty_assertions", "pretty_assertions",
"regex", "regex",
"reqwest 0.12.7", "reqwest 0.12.8",
"reqwest-middleware", "reqwest-middleware",
"rosetta-i18n", "rosetta-i18n",
"serde", "serde",
@ -2579,7 +2579,7 @@ dependencies = [
"lemmy_utils", "lemmy_utils",
"moka", "moka",
"pretty_assertions", "pretty_assertions",
"reqwest 0.12.7", "reqwest 0.12.8",
"serde", "serde",
"serde_json", "serde_json",
"serde_with", "serde_with",
@ -2718,7 +2718,7 @@ dependencies = [
"lemmy_utils", "lemmy_utils",
"mockall", "mockall",
"moka", "moka",
"reqwest 0.12.7", "reqwest 0.12.8",
"serde_json", "serde_json",
"serial_test", "serial_test",
"test-context", "test-context",
@ -2745,7 +2745,7 @@ dependencies = [
"lemmy_db_views", "lemmy_db_views",
"lemmy_db_views_actor", "lemmy_db_views_actor",
"lemmy_utils", "lemmy_utils",
"reqwest 0.12.7", "reqwest 0.12.8",
"reqwest-middleware", "reqwest-middleware",
"rss", "rss",
"serde", "serde",
@ -2778,7 +2778,7 @@ dependencies = [
"lemmy_utils", "lemmy_utils",
"pretty_assertions", "pretty_assertions",
"prometheus", "prometheus",
"reqwest 0.12.7", "reqwest 0.12.8",
"reqwest-middleware", "reqwest-middleware",
"reqwest-tracing", "reqwest-tracing",
"rustls 0.23.13", "rustls 0.23.13",
@ -2811,7 +2811,7 @@ dependencies = [
"markdown-it", "markdown-it",
"pretty_assertions", "pretty_assertions",
"regex", "regex",
"reqwest 0.12.7", "reqwest 0.12.8",
"reqwest-middleware", "reqwest-middleware",
"rosetta-build", "rosetta-build",
"rosetta-i18n", "rosetta-i18n",
@ -2870,7 +2870,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-targets 0.52.6", "windows-targets 0.48.5",
] ]
[[package]] [[package]]
@ -3966,9 +3966,9 @@ dependencies = [
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.7" version = "0.12.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b"
dependencies = [ dependencies = [
"async-compression", "async-compression",
"base64 0.22.1", "base64 0.22.1",
@ -4019,7 +4019,7 @@ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"http 1.1.0", "http 1.1.0",
"reqwest 0.12.7", "reqwest 0.12.8",
"serde", "serde",
"thiserror", "thiserror",
"tower-service", "tower-service",
@ -4036,7 +4036,7 @@ dependencies = [
"getrandom", "getrandom",
"http 1.1.0", "http 1.1.0",
"matchit", "matchit",
"reqwest 0.12.7", "reqwest 0.12.8",
"reqwest-middleware", "reqwest-middleware",
"tracing", "tracing",
] ]
@ -5579,7 +5579,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.48.0",
] ]
[[package]] [[package]]

View File

@ -90,7 +90,7 @@ lemmy_db_views = { version = "=0.19.6-beta.7", path = "./crates/db_views" }
lemmy_db_views_actor = { version = "=0.19.6-beta.7", path = "./crates/db_views_actor" } lemmy_db_views_actor = { version = "=0.19.6-beta.7", path = "./crates/db_views_actor" }
lemmy_db_views_moderator = { version = "=0.19.6-beta.7", path = "./crates/db_views_moderator" } lemmy_db_views_moderator = { version = "=0.19.6-beta.7", path = "./crates/db_views_moderator" }
lemmy_federate = { version = "=0.19.6-beta.7", path = "./crates/federate" } lemmy_federate = { version = "=0.19.6-beta.7", path = "./crates/federate" }
activitypub_federation = { version = "0.6.0-alpha1", default-features = false, features = [ activitypub_federation = { version = "0.6.0-alpha2", default-features = false, features = [
"actix-web", "actix-web",
] } ] }
diesel = "2.1.6" diesel = "2.1.6"

View File

@ -858,3 +858,26 @@ test("Dont send a comment reply to a blocked community", async () => {
blockRes = await blockCommunity(beta, newCommunityId, false); blockRes = await blockCommunity(beta, newCommunityId, false);
expect(blockRes.blocked).toBe(false); expect(blockRes.blocked).toBe(false);
}); });
/// Fetching a deeply nested comment can lead to stack overflow as all parent comments are also
/// fetched recursively. Ensure that it works properly.
test("Fetch a deeply nested comment", async () => {
let lastComment;
for (let i = 0; i < 50; i++) {
let commentRes = await createComment(
alpha,
postOnAlphaRes.post_view.post.id,
lastComment?.comment_view.comment.id,
);
expect(commentRes.comment_view.comment).toBeDefined();
lastComment = commentRes;
}
let betaComment = await resolveComment(
beta,
lastComment!.comment_view.comment,
);
expect(betaComment!.comment!.comment).toBeDefined();
expect(betaComment?.comment?.post).toBeDefined();
});

View File

@ -5,7 +5,7 @@ use lemmy_api_common::{
comment::{CommentResponse, CreateCommentLike}, comment::{CommentResponse, CreateCommentLike},
context::LemmyContext, context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData}, send_activity::{ActivityChannel, SendActivityData},
utils::{check_bot_account, check_community_user_action, check_downvotes_enabled}, utils::{check_bot_account, check_community_user_action, check_local_vote_mode, VoteItem},
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::LocalUserId, newtypes::LocalUserId,
@ -30,8 +30,7 @@ pub async fn like_comment(
let mut recipient_ids = Vec::<LocalUserId>::new(); let mut recipient_ids = Vec::<LocalUserId>::new();
// Don't do a downvote if site has downvotes disabled check_local_vote_mode(data.score, VoteItem::Comment, &local_site)?;
check_downvotes_enabled(data.score, &local_site)?;
check_bot_account(&local_user_view.person)?; check_bot_account(&local_user_view.person)?;
let comment_id = data.comment_id; let comment_id = data.comment_id;

View File

@ -8,8 +8,9 @@ use lemmy_api_common::{
utils::{ utils::{
check_bot_account, check_bot_account,
check_community_user_action, check_community_user_action,
check_downvotes_enabled, check_local_vote_mode,
mark_post_as_read, mark_post_as_read,
VoteItem,
}, },
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
@ -32,8 +33,7 @@ pub async fn like_post(
) -> LemmyResult<Json<PostResponse>> { ) -> LemmyResult<Json<PostResponse>> {
let local_site = LocalSite::read(&mut context.pool()).await?; let local_site = LocalSite::read(&mut context.pool()).await?;
// Don't do a downvote if site has downvotes disabled check_local_vote_mode(data.score, VoteItem::Post, &local_site)?;
check_downvotes_enabled(data.score, &local_site)?;
check_bot_account(&local_user_view.person)?; check_bot_account(&local_user_view.person)?;
// Check for a community ban // Check for a community ban

View File

@ -21,6 +21,7 @@ use lemmy_db_schema::{
tagline::Tagline, tagline::Tagline,
}, },
CommentSortType, CommentSortType,
FederationMode,
ListingType, ListingType,
ModlogActionType, ModlogActionType,
PostListingMode, PostListingMode,
@ -170,7 +171,6 @@ pub struct CreateSite {
pub description: Option<String>, pub description: Option<String>,
pub icon: Option<String>, pub icon: Option<String>,
pub banner: Option<String>, pub banner: Option<String>,
pub enable_downvotes: Option<bool>,
pub enable_nsfw: Option<bool>, pub enable_nsfw: Option<bool>,
pub community_creation_admin_only: Option<bool>, pub community_creation_admin_only: Option<bool>,
pub require_email_verification: Option<bool>, pub require_email_verification: Option<bool>,
@ -208,6 +208,10 @@ pub struct CreateSite {
pub registration_mode: Option<RegistrationMode>, pub registration_mode: Option<RegistrationMode>,
pub oauth_registration: Option<bool>, pub oauth_registration: Option<bool>,
pub content_warning: Option<String>, pub content_warning: Option<String>,
pub post_upvotes: Option<FederationMode>,
pub post_downvotes: Option<FederationMode>,
pub comment_upvotes: Option<FederationMode>,
pub comment_downvotes: Option<FederationMode>,
} }
#[skip_serializing_none] #[skip_serializing_none]
@ -224,8 +228,6 @@ pub struct EditSite {
pub icon: Option<String>, pub icon: Option<String>,
/// A url for your site's banner. /// A url for your site's banner.
pub banner: Option<String>, pub banner: Option<String>,
/// Whether to enable downvotes.
pub enable_downvotes: Option<bool>,
/// Whether to enable NSFW. /// Whether to enable NSFW.
pub enable_nsfw: Option<bool>, pub enable_nsfw: Option<bool>,
/// Limits community creation to admins only. /// Limits community creation to admins only.
@ -291,13 +293,21 @@ pub struct EditSite {
/// A list of blocked URLs /// A list of blocked URLs
pub blocked_urls: Option<Vec<String>>, pub blocked_urls: Option<Vec<String>>,
pub registration_mode: Option<RegistrationMode>, pub registration_mode: Option<RegistrationMode>,
/// Whether or not external auth methods can auto-register users.
pub oauth_registration: Option<bool>,
/// Whether to email admins for new reports. /// Whether to email admins for new reports.
pub reports_email_admins: Option<bool>, pub reports_email_admins: Option<bool>,
/// If present, nsfw content is visible by default. Should be displayed by frontends/clients /// If present, nsfw content is visible by default. Should be displayed by frontends/clients
/// when the site is first opened by a user. /// when the site is first opened by a user.
pub content_warning: Option<String>, pub content_warning: Option<String>,
/// Whether or not external auth methods can auto-register users.
pub oauth_registration: Option<bool>,
/// What kind of post upvotes your site allows.
pub post_upvotes: Option<FederationMode>,
/// What kind of post downvotes your site allows.
pub post_downvotes: Option<FederationMode>,
/// What kind of comment upvotes your site allows.
pub comment_upvotes: Option<FederationMode>,
/// What kind of comment downvotes your site allows.
pub comment_downvotes: Option<FederationMode>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]

View File

@ -33,6 +33,7 @@ use lemmy_db_schema::{
}, },
traits::Crud, traits::Crud,
utils::DbPool, utils::DbPool,
FederationMode,
RegistrationMode, RegistrationMode,
}; };
use lemmy_db_views::{ use lemmy_db_views::{
@ -296,10 +297,28 @@ pub async fn check_person_instance_community_block(
Ok(()) Ok(())
} }
/// A vote item type used to check the vote mode.
pub enum VoteItem {
Post,
Comment,
}
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub fn check_downvotes_enabled(score: i16, local_site: &LocalSite) -> LemmyResult<()> { pub fn check_local_vote_mode(
if score == -1 && !local_site.enable_downvotes { score: i16,
Err(LemmyErrorType::DownvotesAreDisabled)? vote_item: VoteItem,
local_site: &LocalSite,
) -> LemmyResult<()> {
let (downvote_setting, upvote_setting) = match vote_item {
VoteItem::Post => (local_site.post_downvotes, local_site.post_upvotes),
VoteItem::Comment => (local_site.comment_downvotes, local_site.comment_upvotes),
};
let downvote_fail = score == -1 && downvote_setting == FederationMode::Disable;
let upvote_fail = score == 1 && upvote_setting == FederationMode::Disable;
if downvote_fail || upvote_fail {
Err(LemmyErrorType::VoteNotAllowed)?
} else { } else {
Ok(()) Ok(())
} }

View File

@ -30,10 +30,9 @@ use lemmy_db_views::structs::{LocalUserView, PostView};
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
utils::{mention::scrape_text_for_mentions, validation::is_valid_body_field}, utils::{mention::scrape_text_for_mentions, validation::is_valid_body_field},
MAX_COMMENT_DEPTH_LIMIT,
}; };
const MAX_COMMENT_DEPTH_LIMIT: usize = 100;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn create_comment( pub async fn create_comment(
data: Json<CreateComment>, data: Json<CreateComment>,

View File

@ -90,7 +90,6 @@ pub async fn create_site(
let local_site_form = LocalSiteUpdateForm { let local_site_form = LocalSiteUpdateForm {
// Set the site setup to true // Set the site setup to true
site_setup: Some(true), site_setup: Some(true),
enable_downvotes: data.enable_downvotes,
registration_mode: data.registration_mode, registration_mode: data.registration_mode,
community_creation_admin_only: data.community_creation_admin_only, community_creation_admin_only: data.community_creation_admin_only,
require_email_verification: data.require_email_verification, require_email_verification: data.require_email_verification,
@ -110,6 +109,10 @@ pub async fn create_site(
captcha_enabled: data.captcha_enabled, captcha_enabled: data.captcha_enabled,
captcha_difficulty: data.captcha_difficulty.clone(), captcha_difficulty: data.captcha_difficulty.clone(),
default_post_listing_mode: data.default_post_listing_mode, default_post_listing_mode: data.default_post_listing_mode,
post_upvotes: data.post_upvotes,
post_downvotes: data.post_downvotes,
comment_upvotes: data.comment_upvotes,
comment_downvotes: data.comment_downvotes,
..Default::default() ..Default::default()
}; };

View File

@ -99,7 +99,6 @@ pub async fn update_site(
.ok(); .ok();
let local_site_form = LocalSiteUpdateForm { let local_site_form = LocalSiteUpdateForm {
enable_downvotes: data.enable_downvotes,
registration_mode: data.registration_mode, registration_mode: data.registration_mode,
community_creation_admin_only: data.community_creation_admin_only, community_creation_admin_only: data.community_creation_admin_only,
require_email_verification: data.require_email_verification, require_email_verification: data.require_email_verification,
@ -121,6 +120,10 @@ pub async fn update_site(
reports_email_admins: data.reports_email_admins, reports_email_admins: data.reports_email_admins,
default_post_listing_mode: data.default_post_listing_mode, default_post_listing_mode: data.default_post_listing_mode,
oauth_registration: data.oauth_registration, oauth_registration: data.oauth_registration,
post_upvotes: data.post_upvotes,
post_downvotes: data.post_downvotes,
comment_upvotes: data.comment_upvotes,
comment_downvotes: data.comment_downvotes,
..Default::default() ..Default::default()
}; };

View File

@ -18,7 +18,7 @@ use activitypub_federation::{
traits::{ActivityHandler, Actor}, traits::{ActivityHandler, Actor},
}; };
use lemmy_api_common::{context::LemmyContext, utils::check_bot_account}; use lemmy_api_common::{context::LemmyContext, utils::check_bot_account};
use lemmy_db_schema::source::local_site::LocalSite; use lemmy_db_schema::{source::local_site::LocalSite, FederationMode};
use lemmy_utils::error::{LemmyError, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyResult};
use url::Url; use url::Url;
@ -68,12 +68,22 @@ impl ActivityHandler for Vote {
check_bot_account(&actor.0)?; check_bot_account(&actor.0)?;
let enable_downvotes = LocalSite::read(&mut context.pool()) // Check for enabled federation votes
let local_site = LocalSite::read(&mut context.pool())
.await .await
.map(|l| l.enable_downvotes) .unwrap_or_default();
.unwrap_or(true);
if self.kind == VoteType::Dislike && !enable_downvotes { let (downvote_setting, upvote_setting) = match object {
// If this is a downvote but downvotes are ignored, only undo any existing vote PostOrComment::Post(_) => (local_site.post_downvotes, local_site.post_upvotes),
PostOrComment::Comment(_) => (local_site.comment_downvotes, local_site.comment_upvotes),
};
// Don't allow dislikes for either disabled, or local only votes
let downvote_fail = self.kind == VoteType::Dislike && downvote_setting != FederationMode::All;
let upvote_fail = self.kind == VoteType::Like && upvote_setting != FederationMode::All;
if downvote_fail || upvote_fail {
// If this is a rejection, undo the vote
match object { match object {
PostOrComment::Post(p) => undo_vote_post(actor, &p, context).await, PostOrComment::Post(p) => undo_vote_post(actor, &p, context).await,
PostOrComment::Comment(c) => undo_vote_comment(actor, &c, context).await, PostOrComment::Comment(c) => undo_vote_comment(actor, &c, context).await,

View File

@ -103,13 +103,16 @@ pub async fn import_settings(
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> { ) -> LemmyResult<Json<SuccessResponse>> {
let person_form = PersonUpdateForm { let person_form = PersonUpdateForm {
display_name: Some(data.display_name.clone()), display_name: data.display_name.clone().map(Some),
bio: Some(data.bio.clone()), bio: data.bio.clone().map(Some),
matrix_user_id: Some(data.matrix_id.clone()), matrix_user_id: data.bio.clone().map(Some),
bot_account: data.bot_account, bot_account: data.bot_account,
..Default::default() ..Default::default()
}; };
Person::update(&mut context.pool(), local_user_view.person.id, &person_form).await?; // ignore error in case form is empty
Person::update(&mut context.pool(), local_user_view.person.id, &person_form)
.await
.ok();
let local_user_form = LocalUserUpdateForm { let local_user_form = LocalUserUpdateForm {
show_nsfw: data.settings.as_ref().map(|s| s.show_nsfw), show_nsfw: data.settings.as_ref().map(|s| s.show_nsfw),
@ -312,8 +315,9 @@ where
#[expect(clippy::indexing_slicing)] #[expect(clippy::indexing_slicing)]
mod tests { mod tests {
use crate::api::user_settings_backup::{export_settings, import_settings, UserSettingsBackup}; use crate::api::user_settings_backup::{export_settings, import_settings};
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::context::LemmyContext; use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
@ -401,45 +405,6 @@ mod tests {
Ok(()) Ok(())
} }
#[tokio::test]
#[serial]
async fn test_settings_partial_import() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let export_user =
create_user("hanna".to_string(), Some("my bio".to_string()), &context).await?;
let community_form = CommunityInsertForm::new(
export_user.person.instance_id,
"testcom".to_string(),
"testcom".to_string(),
"pubkey".to_string(),
);
let community = Community::create(&mut context.pool(), &community_form).await?;
let follower_form = CommunityFollowerForm {
community_id: community.id,
person_id: export_user.person.id,
pending: false,
};
CommunityFollower::follow(&mut context.pool(), &follower_form).await?;
let backup = export_settings(export_user.clone(), context.reset_request_count()).await?;
let import_user = create_user("charles".to_string(), None, &context).await?;
let backup2 = UserSettingsBackup {
followed_communities: backup.followed_communities.clone(),
..Default::default()
};
import_settings(
actix_web::web::Json(backup2),
import_user.clone(),
context.reset_request_count(),
)
.await?;
Ok(())
}
#[tokio::test] #[tokio::test]
#[serial] #[serial]
async fn disallow_large_backup() -> LemmyResult<()> { async fn disallow_large_backup() -> LemmyResult<()> {
@ -475,4 +440,33 @@ mod tests {
LocalUser::delete(&mut context.pool(), import_user.local_user.id).await?; LocalUser::delete(&mut context.pool(), import_user.local_user.id).await?;
Ok(()) Ok(())
} }
#[tokio::test]
#[serial]
async fn import_partial_backup() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let import_user =
create_user("hanna".to_string(), Some("my bio".to_string()), &context).await?;
let backup =
serde_json::from_str("{\"bot_account\": true, \"settings\": {\"theme\": \"my_theme\"}}")?;
import_settings(
Json(backup),
import_user.clone(),
context.reset_request_count(),
)
.await?;
let import_user_updated =
LocalUserView::read(&mut context.pool(), import_user.local_user.id).await?;
// mark as bot account
assert!(import_user_updated.person.bot_account);
// dont remove existing bio
assert_eq!(import_user.person.bio, import_user_updated.person.bio);
// local_user can be deserialized without id/person_id fields
assert_eq!("my_theme", import_user_updated.local_user.theme);
Ok(())
}
} }

View File

@ -20,10 +20,9 @@ use lemmy_db_schema::{
source::{community::Community, post::Post}, source::{community::Community, post::Post},
traits::Crud, traits::Crud,
}; };
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType, MAX_COMMENT_DEPTH_LIMIT};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
use std::ops::Deref;
use url::Url; use url::Url;
#[skip_serializing_none] #[skip_serializing_none]
@ -58,9 +57,19 @@ impl Note {
&self, &self,
context: &Data<LemmyContext>, context: &Data<LemmyContext>,
) -> LemmyResult<(ApubPost, Option<ApubComment>)> { ) -> LemmyResult<(ApubPost, Option<ApubComment>)> {
// Fetch parent comment chain in a box, otherwise it can cause a stack overflow. // We use recursion here to fetch the entire comment chain up to the top-level parent. This is
let parent = Box::pin(self.in_reply_to.dereference(context).await?); // necessary because we need to know the post and parent comment in order to insert a new
match parent.deref() { // comment. However it can also lead to stack overflow when fetching many comments recursively.
// To avoid this we check the request count against max comment depth, which based on testing
// can be handled without risking stack overflow. This is not a perfect solution, because in
// some cases we have to fetch user profiles too, and reach the limit after only 25 comments
// or so.
// A cleaner solution would be converting the recursion into a loop, but that is tricky.
if context.request_count() > MAX_COMMENT_DEPTH_LIMIT as u32 {
Err(LemmyErrorType::MaxCommentDepthReached)?;
}
let parent = self.in_reply_to.dereference(context).await?;
match parent {
PostOrComment::Post(p) => Ok((p.clone(), None)), PostOrComment::Post(p) => Ok((p.clone(), None)),
PostOrComment::Comment(c) => { PostOrComment::Comment(c) => {
let post_id = c.post_id; let post_id = c.post_id;

View File

@ -251,6 +251,27 @@ pub enum CommunityVisibility {
LocalOnly, LocalOnly,
} }
#[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::FederationModeEnum"
)]
#[cfg_attr(feature = "full", DbValueStyle = "verbatim")]
#[cfg_attr(feature = "full", ts(export))]
/// The federation mode for an item
pub enum FederationMode {
#[default]
/// Allows all
All,
/// Allows only local
Local,
/// Disables
Disable,
}
/// Wrapper for assert_eq! macro. Checks that vec matches the given length, and prints the /// Wrapper for assert_eq! macro. Checks that vec matches the given length, and prints the
/// vec on failure. /// vec on failure.
#[macro_export] #[macro_export]

View File

@ -13,6 +13,10 @@ pub mod sql_types {
#[diesel(postgres_type(name = "community_visibility"))] #[diesel(postgres_type(name = "community_visibility"))]
pub struct CommunityVisibility; pub struct CommunityVisibility;
#[derive(diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "federation_mode_enum"))]
pub struct FederationModeEnum;
#[derive(diesel::sql_types::SqlType)] #[derive(diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "listing_type_enum"))] #[diesel(postgres_type(name = "listing_type_enum"))]
pub struct ListingTypeEnum; pub struct ListingTypeEnum;
@ -368,12 +372,12 @@ diesel::table! {
use super::sql_types::PostListingModeEnum; use super::sql_types::PostListingModeEnum;
use super::sql_types::PostSortTypeEnum; use super::sql_types::PostSortTypeEnum;
use super::sql_types::CommentSortTypeEnum; use super::sql_types::CommentSortTypeEnum;
use super::sql_types::FederationModeEnum;
local_site (id) { local_site (id) {
id -> Int4, id -> Int4,
site_id -> Int4, site_id -> Int4,
site_setup -> Bool, site_setup -> Bool,
enable_downvotes -> Bool,
community_creation_admin_only -> Bool, community_creation_admin_only -> Bool,
require_email_verification -> Bool, require_email_verification -> Bool,
application_question -> Nullable<Text>, application_question -> Nullable<Text>,
@ -398,6 +402,10 @@ diesel::table! {
default_post_sort_type -> PostSortTypeEnum, default_post_sort_type -> PostSortTypeEnum,
default_comment_sort_type -> CommentSortTypeEnum, default_comment_sort_type -> CommentSortTypeEnum,
oauth_registration -> Bool, oauth_registration -> Bool,
post_upvotes -> FederationModeEnum,
post_downvotes -> FederationModeEnum,
comment_upvotes -> FederationModeEnum,
comment_downvotes -> FederationModeEnum,
} }
} }

View File

@ -3,6 +3,7 @@ use crate::schema::local_site;
use crate::{ use crate::{
newtypes::{LocalSiteId, SiteId}, newtypes::{LocalSiteId, SiteId},
CommentSortType, CommentSortType,
FederationMode,
ListingType, ListingType,
PostListingMode, PostListingMode,
PostSortType, PostSortType,
@ -27,8 +28,6 @@ pub struct LocalSite {
pub site_id: SiteId, pub site_id: SiteId,
/// True if the site is set up. /// True if the site is set up.
pub site_setup: bool, pub site_setup: bool,
/// Whether downvotes are enabled.
pub enable_downvotes: bool,
/// Whether only admins can create communities. /// Whether only admins can create communities.
pub community_creation_admin_only: bool, pub community_creation_admin_only: bool,
/// Whether emails are required. /// Whether emails are required.
@ -72,6 +71,14 @@ pub struct LocalSite {
pub default_comment_sort_type: CommentSortType, pub default_comment_sort_type: CommentSortType,
/// Whether or not external auth methods can auto-register users. /// Whether or not external auth methods can auto-register users.
pub oauth_registration: bool, pub oauth_registration: bool,
/// What kind of post upvotes your site allows.
pub post_upvotes: FederationMode,
/// What kind of post downvotes your site allows.
pub post_downvotes: FederationMode,
/// What kind of comment upvotes your site allows.
pub comment_upvotes: FederationMode,
/// What kind of comment downvotes your site allows.
pub comment_downvotes: FederationMode,
} }
#[derive(Clone, derive_new::new)] #[derive(Clone, derive_new::new)]
@ -82,8 +89,6 @@ pub struct LocalSiteInsertForm {
#[new(default)] #[new(default)]
pub site_setup: Option<bool>, pub site_setup: Option<bool>,
#[new(default)] #[new(default)]
pub enable_downvotes: Option<bool>,
#[new(default)]
pub community_creation_admin_only: Option<bool>, pub community_creation_admin_only: Option<bool>,
#[new(default)] #[new(default)]
pub require_email_verification: Option<bool>, pub require_email_verification: Option<bool>,
@ -114,8 +119,6 @@ pub struct LocalSiteInsertForm {
#[new(default)] #[new(default)]
pub registration_mode: Option<RegistrationMode>, pub registration_mode: Option<RegistrationMode>,
#[new(default)] #[new(default)]
pub oauth_registration: Option<bool>,
#[new(default)]
pub reports_email_admins: Option<bool>, pub reports_email_admins: Option<bool>,
#[new(default)] #[new(default)]
pub federation_signed_fetch: Option<bool>, pub federation_signed_fetch: Option<bool>,
@ -125,6 +128,16 @@ pub struct LocalSiteInsertForm {
pub default_post_sort_type: Option<PostSortType>, pub default_post_sort_type: Option<PostSortType>,
#[new(default)] #[new(default)]
pub default_comment_sort_type: Option<CommentSortType>, pub default_comment_sort_type: Option<CommentSortType>,
#[new(default)]
pub oauth_registration: Option<bool>,
#[new(default)]
pub post_upvotes: Option<FederationMode>,
#[new(default)]
pub post_downvotes: Option<FederationMode>,
#[new(default)]
pub comment_upvotes: Option<FederationMode>,
#[new(default)]
pub comment_downvotes: Option<FederationMode>,
} }
#[derive(Clone, Default)] #[derive(Clone, Default)]
@ -132,7 +145,6 @@ pub struct LocalSiteInsertForm {
#[cfg_attr(feature = "full", diesel(table_name = local_site))] #[cfg_attr(feature = "full", diesel(table_name = local_site))]
pub struct LocalSiteUpdateForm { pub struct LocalSiteUpdateForm {
pub site_setup: Option<bool>, pub site_setup: Option<bool>,
pub enable_downvotes: Option<bool>,
pub community_creation_admin_only: Option<bool>, pub community_creation_admin_only: Option<bool>,
pub require_email_verification: Option<bool>, pub require_email_verification: Option<bool>,
pub application_question: Option<Option<String>>, pub application_question: Option<Option<String>>,
@ -148,11 +160,15 @@ pub struct LocalSiteUpdateForm {
pub captcha_enabled: Option<bool>, pub captcha_enabled: Option<bool>,
pub captcha_difficulty: Option<String>, pub captcha_difficulty: Option<String>,
pub registration_mode: Option<RegistrationMode>, pub registration_mode: Option<RegistrationMode>,
pub oauth_registration: Option<bool>,
pub reports_email_admins: Option<bool>, pub reports_email_admins: Option<bool>,
pub updated: Option<Option<DateTime<Utc>>>, pub updated: Option<Option<DateTime<Utc>>>,
pub federation_signed_fetch: Option<bool>, pub federation_signed_fetch: Option<bool>,
pub default_post_listing_mode: Option<PostListingMode>, pub default_post_listing_mode: Option<PostListingMode>,
pub default_post_sort_type: Option<PostSortType>, pub default_post_sort_type: Option<PostSortType>,
pub default_comment_sort_type: Option<CommentSortType>, pub default_comment_sort_type: Option<CommentSortType>,
pub oauth_registration: Option<bool>,
pub post_upvotes: Option<FederationMode>,
pub post_downvotes: Option<FederationMode>,
pub comment_upvotes: Option<FederationMode>,
pub comment_downvotes: Option<FederationMode>,
} }

View File

@ -14,11 +14,12 @@ use serde_with::skip_serializing_none;
use ts_rs::TS; use ts_rs::TS;
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] #[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))]
#[cfg_attr(feature = "full", diesel(table_name = local_user))] #[cfg_attr(feature = "full", diesel(table_name = local_user))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
#[serde(default)]
/// A local user. /// A local user.
pub struct LocalUser { pub struct LocalUser {
pub id: LocalUserId, pub id: LocalUserId,

View File

@ -46,7 +46,7 @@ pub enum LemmyErrorType {
PersonIsBlocked, PersonIsBlocked,
CommunityIsBlocked, CommunityIsBlocked,
InstanceIsBlocked, InstanceIsBlocked,
DownvotesAreDisabled, VoteNotAllowed,
InstanceIsPrivate, InstanceIsPrivate,
/// Password must be between 10 and 60 characters /// Password must be between 10 and 60 characters
InvalidPassword, InvalidPassword,

View File

@ -29,6 +29,8 @@ pub const CACHE_DURATION_FEDERATION: Duration = Duration::from_secs(60);
pub const CACHE_DURATION_API: Duration = Duration::from_secs(1); pub const CACHE_DURATION_API: Duration = Duration::from_secs(1);
pub const MAX_COMMENT_DEPTH_LIMIT: usize = 50;
#[macro_export] #[macro_export]
macro_rules! location_info { macro_rules! location_info {
() => { () => {

View File

@ -0,0 +1,31 @@
-- Add back the enable_downvotes column
ALTER TABLE local_site
ADD COLUMN enable_downvotes boolean DEFAULT TRUE NOT NULL;
-- regenerate their values (from post_downvotes alone)
WITH subquery AS (
SELECT
post_downvotes,
CASE WHEN post_downvotes = 'Disable'::federation_mode_enum THEN
FALSE
ELSE
TRUE
END
FROM
local_site)
UPDATE
local_site
SET
enable_downvotes = subquery.case
FROM
subquery;
-- Drop the new columns
ALTER TABLE local_site
DROP COLUMN post_upvotes,
DROP COLUMN post_downvotes,
DROP COLUMN comment_upvotes,
DROP COLUMN comment_downvotes;
DROP TYPE federation_mode_enum;

View File

@ -0,0 +1,39 @@
-- This removes the simple enable_downvotes setting, in favor of an
-- expanded federation mode type for post/comment up/downvotes.
-- Create the federation mode enum
CREATE TYPE federation_mode_enum AS ENUM (
'All',
'Local',
'Disable'
);
-- Add the new columns
ALTER TABLE local_site
ADD COLUMN post_upvotes federation_mode_enum DEFAULT 'All'::federation_mode_enum NOT NULL,
ADD COLUMN post_downvotes federation_mode_enum DEFAULT 'All'::federation_mode_enum NOT NULL,
ADD COLUMN comment_upvotes federation_mode_enum DEFAULT 'All'::federation_mode_enum NOT NULL,
ADD COLUMN comment_downvotes federation_mode_enum DEFAULT 'All'::federation_mode_enum NOT NULL;
-- Copy over the enable_downvotes into the post and comment downvote settings
WITH subquery AS (
SELECT
enable_downvotes,
CASE WHEN enable_downvotes = TRUE THEN
'All'::federation_mode_enum
ELSE
'Disable'::federation_mode_enum
END
FROM
local_site)
UPDATE
local_site
SET
post_downvotes = subquery.case,
comment_downvotes = subquery.case
FROM
subquery;
-- Drop the enable_downvotes column
ALTER TABLE local_site
DROP COLUMN enable_downvotes;