mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-10-01 01:36:12 -04:00
Compare commits
4 Commits
e227bce140
...
215b67095c
Author | SHA1 | Date | |
---|---|---|---|
|
215b67095c | ||
|
44dda08b13 | ||
|
5115ed4c09 | ||
|
a697bb92c5 |
@ -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"
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
@ -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>,
|
||||||
|
@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
() => {
|
() => {
|
||||||
|
Loading…
Reference in New Issue
Block a user