mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-11-29 07:41:20 +00:00
Allow admins to resolve removed or deleted objects via API
This commit is contained in:
parent
c7210e39e7
commit
fbd219a370
6 changed files with 224 additions and 72 deletions
|
@ -158,16 +158,16 @@ test("Delete a comment", async () => {
|
|||
expect(deleteCommentRes.comment_view.comment.deleted).toBe(true);
|
||||
expect(deleteCommentRes.comment_view.comment.content).toBe("");
|
||||
|
||||
// Make sure that comment is undefined on beta
|
||||
// Make sure that comment is deleted on beta
|
||||
await waitUntil(
|
||||
() => resolveComment(beta, commentRes.comment_view.comment).catch(e => e),
|
||||
e => e.message == "not_found",
|
||||
() => resolveComment(beta, commentRes.comment_view.comment),
|
||||
c => c.comment?.comment.deleted === true,
|
||||
);
|
||||
|
||||
// Make sure that comment is undefined on gamma after delete
|
||||
// Make sure that comment is deleted on gamma after delete
|
||||
await waitUntil(
|
||||
() => resolveComment(gamma, commentRes.comment_view.comment).catch(e => e),
|
||||
e => e.message === "not_found",
|
||||
() => resolveComment(gamma, commentRes.comment_view.comment),
|
||||
c => c.comment?.comment.deleted === true,
|
||||
);
|
||||
|
||||
// Test undeleting the comment
|
||||
|
@ -181,11 +181,10 @@ test("Delete a comment", async () => {
|
|||
// Make sure that comment is undeleted on beta
|
||||
let betaComment2 = (
|
||||
await waitUntil(
|
||||
() => resolveComment(beta, commentRes.comment_view.comment).catch(e => e),
|
||||
e => e.message !== "not_found",
|
||||
() => resolveComment(beta, commentRes.comment_view.comment),
|
||||
c => c.comment?.comment.deleted === false,
|
||||
)
|
||||
).comment;
|
||||
expect(betaComment2?.comment.deleted).toBe(false);
|
||||
assertCommentFederation(betaComment2, undeleteCommentRes.comment_view);
|
||||
});
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@ pub mod read_community;
|
|||
pub mod read_person;
|
||||
pub mod resolve_object;
|
||||
pub mod search;
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test;
|
||||
pub mod user_settings_backup;
|
||||
|
||||
/// Returns default listing type, depending if the query is for frontpage or community.
|
||||
|
|
|
@ -5,7 +5,6 @@ use crate::fetcher::{
|
|||
};
|
||||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::{Json, Query};
|
||||
use diesel::NotFound;
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
site::{ResolveObject, ResolveObjectResponse},
|
||||
|
@ -47,36 +46,146 @@ async fn convert_response(
|
|||
local_user_view: Option<LocalUserView>,
|
||||
pool: &mut DbPool<'_>,
|
||||
) -> LemmyResult<Json<ResolveObjectResponse>> {
|
||||
let removed_or_deleted;
|
||||
let mut res = ResolveObjectResponse::default();
|
||||
let local_user = local_user_view.map(|l| l.local_user);
|
||||
let is_admin = local_user.clone().map(|l| l.admin).unwrap_or_default();
|
||||
|
||||
match object {
|
||||
SearchableObjects::PostOrComment(pc) => match *pc {
|
||||
PostOrComment::Post(p) => {
|
||||
removed_or_deleted = p.deleted || p.removed;
|
||||
res.post = Some(PostView::read(pool, p.id, local_user.as_ref(), false).await?)
|
||||
res.post = Some(PostView::read(pool, p.id, local_user.as_ref(), is_admin).await?)
|
||||
}
|
||||
PostOrComment::Comment(c) => {
|
||||
removed_or_deleted = c.deleted || c.removed;
|
||||
res.comment = Some(CommentView::read(pool, c.id, local_user.as_ref()).await?)
|
||||
}
|
||||
},
|
||||
SearchableObjects::PersonOrCommunity(pc) => match *pc {
|
||||
UserOrCommunity::User(u) => {
|
||||
removed_or_deleted = u.deleted;
|
||||
res.person = Some(PersonView::read(pool, u.id).await?)
|
||||
}
|
||||
UserOrCommunity::User(u) => res.person = Some(PersonView::read(pool, u.id).await?),
|
||||
UserOrCommunity::Community(c) => {
|
||||
removed_or_deleted = c.deleted || c.removed;
|
||||
res.community = Some(CommunityView::read(pool, c.id, local_user.as_ref(), false).await?)
|
||||
res.community = Some(CommunityView::read(pool, c.id, local_user.as_ref(), is_admin).await?)
|
||||
}
|
||||
},
|
||||
};
|
||||
// if the object was deleted from database, dont return it
|
||||
if removed_or_deleted {
|
||||
Err(NotFound {}.into())
|
||||
} else {
|
||||
Ok(Json(res))
|
||||
|
||||
Ok(Json(res))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::{resolve_object::resolve_object, test::TestUser};
|
||||
use actix_web::web::Query;
|
||||
use lemmy_api_common::{context::LemmyContext, site::ResolveObject};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
community::{Community, CommunityInsertForm},
|
||||
instance::Instance,
|
||||
local_site::{LocalSite, LocalSiteInsertForm},
|
||||
post::{Post, PostInsertForm, PostUpdateForm},
|
||||
site::{Site, SiteInsertForm},
|
||||
},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||
use serial_test::serial;
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
#[expect(clippy::unwrap_used)]
|
||||
async fn test_object_visibility() -> LemmyResult<()> {
|
||||
let context = LemmyContext::init_test_context().await;
|
||||
let pool = &mut context.pool();
|
||||
|
||||
let creator = TestUser::default().create(pool).await?;
|
||||
let regular_user = TestUser::default().create(pool).await?;
|
||||
let admin_user = TestUser {
|
||||
admin: true,
|
||||
..Default::default()
|
||||
}
|
||||
.create(pool)
|
||||
.await?;
|
||||
|
||||
let instance_id = creator.person.instance_id;
|
||||
let site_form = SiteInsertForm::new("test site".to_string(), instance_id);
|
||||
let site = Site::create(pool, &site_form).await?;
|
||||
|
||||
let local_site_form = LocalSiteInsertForm {
|
||||
site_setup: Some(true),
|
||||
private_instance: Some(false),
|
||||
..LocalSiteInsertForm::new(site.id)
|
||||
};
|
||||
LocalSite::create(pool, &local_site_form).await?;
|
||||
|
||||
let community = Community::create(
|
||||
pool,
|
||||
&CommunityInsertForm::new(
|
||||
instance_id,
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
"pubkey".to_string(),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let post_insert_form = PostInsertForm::new("Test".to_string(), creator.person.id, community.id);
|
||||
let post = Post::create(pool, &post_insert_form).await?;
|
||||
|
||||
let query = format!("q={}", post.ap_id).to_string();
|
||||
let query: Query<ResolveObject> = Query::from_query(&query)?;
|
||||
|
||||
// Objects should be resolvable without authentication
|
||||
let res = resolve_object(query.clone(), context.reset_request_count(), None).await?;
|
||||
assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id);
|
||||
// Objects should be resolvable by regular users
|
||||
let res = resolve_object(
|
||||
query.clone(),
|
||||
context.reset_request_count(),
|
||||
Some(regular_user.clone()),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id);
|
||||
// Objects should be resolvable by admins
|
||||
let res = resolve_object(
|
||||
query.clone(),
|
||||
context.reset_request_count(),
|
||||
Some(admin_user.clone()),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id);
|
||||
|
||||
Post::update(
|
||||
pool,
|
||||
post.id,
|
||||
&PostUpdateForm {
|
||||
deleted: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Deleted objects should not be resolvable without authentication
|
||||
let res = resolve_object(query.clone(), context.reset_request_count(), None).await;
|
||||
assert!(res.is_err_and(|e| e.error_type == LemmyErrorType::NotFound));
|
||||
// Deleted objects should not be resolvable by regular users
|
||||
let res = resolve_object(
|
||||
query.clone(),
|
||||
context.reset_request_count(),
|
||||
Some(regular_user.clone()),
|
||||
)
|
||||
.await;
|
||||
assert!(res.is_err_and(|e| e.error_type == LemmyErrorType::NotFound));
|
||||
// Deleted objects should be resolvable by admins
|
||||
let res = resolve_object(
|
||||
query.clone(),
|
||||
context.reset_request_count(),
|
||||
Some(admin_user.clone()),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id);
|
||||
|
||||
LocalSite::delete(pool).await?;
|
||||
Site::delete(pool, site.id).await?;
|
||||
Instance::delete(pool, instance_id).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
44
crates/apub/src/api/test.rs
Normal file
44
crates/apub/src/api/test.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
use lemmy_db_schema::{
|
||||
source::{
|
||||
instance::Instance,
|
||||
local_user::{LocalUser, LocalUserInsertForm},
|
||||
person::{Person, PersonInsertForm},
|
||||
},
|
||||
traits::Crud,
|
||||
utils::DbPool,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TestUser {
|
||||
pub name: Option<&'static str>,
|
||||
pub bio: Option<&'static str>,
|
||||
pub admin: bool,
|
||||
}
|
||||
|
||||
impl TestUser {
|
||||
pub async fn create(self, pool: &mut DbPool<'_>) -> LemmyResult<LocalUserView> {
|
||||
let instance_id = Instance::read_or_create(pool, "example.com".to_string())
|
||||
.await?
|
||||
.id;
|
||||
let name = self
|
||||
.name
|
||||
.map_or_else(|| uuid::Uuid::new_v4().to_string(), ToString::to_string);
|
||||
|
||||
let person_form = PersonInsertForm {
|
||||
display_name: Some(name.clone()),
|
||||
bio: self.bio.map(ToString::to_string),
|
||||
..PersonInsertForm::test_form(instance_id, &name)
|
||||
};
|
||||
let person = Person::create(pool, &person_form).await?;
|
||||
|
||||
let user_form = match self.admin {
|
||||
true => LocalUserInsertForm::test_form_admin(person.id),
|
||||
false => LocalUserInsertForm::test_form(person.id),
|
||||
};
|
||||
let local_user = LocalUser::create(pool, &user_form, vec![]).await?;
|
||||
|
||||
Ok(LocalUserView::read(pool, local_user.id).await?)
|
||||
}
|
||||
}
|
|
@ -314,17 +314,16 @@ where
|
|||
#[cfg(test)]
|
||||
#[expect(clippy::indexing_slicing)]
|
||||
pub(crate) mod tests {
|
||||
|
||||
use crate::api::user_settings_backup::{export_settings, import_settings};
|
||||
use activitypub_federation::config::Data;
|
||||
use crate::api::{
|
||||
test::TestUser,
|
||||
user_settings_backup::{export_settings, import_settings},
|
||||
};
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::context::LemmyContext;
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
community::{Community, CommunityFollower, CommunityFollowerForm, CommunityInsertForm},
|
||||
instance::Instance,
|
||||
local_user::{LocalUser, LocalUserInsertForm},
|
||||
person::{Person, PersonInsertForm},
|
||||
local_user::LocalUser,
|
||||
},
|
||||
traits::{Crud, Followable},
|
||||
};
|
||||
|
@ -336,32 +335,19 @@ pub(crate) mod tests {
|
|||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
pub(crate) async fn create_user(
|
||||
name: String,
|
||||
bio: Option<String>,
|
||||
context: &Data<LemmyContext>,
|
||||
) -> LemmyResult<LocalUserView> {
|
||||
let instance = Instance::read_or_create(&mut context.pool(), "example.com".to_string()).await?;
|
||||
let person_form = PersonInsertForm {
|
||||
display_name: Some(name.clone()),
|
||||
bio,
|
||||
..PersonInsertForm::test_form(instance.id, &name)
|
||||
};
|
||||
let person = Person::create(&mut context.pool(), &person_form).await?;
|
||||
|
||||
let user_form = LocalUserInsertForm::test_form(person.id);
|
||||
let local_user = LocalUser::create(&mut context.pool(), &user_form, vec![]).await?;
|
||||
|
||||
Ok(LocalUserView::read(&mut context.pool(), local_user.id).await?)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_settings_export_import() -> LemmyResult<()> {
|
||||
let context = LemmyContext::init_test_context().await;
|
||||
let pool = &mut context.pool();
|
||||
|
||||
let export_user =
|
||||
create_user("hanna".to_string(), Some("my bio".to_string()), &context).await?;
|
||||
let export_user = TestUser {
|
||||
name: "hanna".into(),
|
||||
bio: "my bio".into(),
|
||||
..Default::default()
|
||||
}
|
||||
.create(pool)
|
||||
.await?;
|
||||
|
||||
let community_form = CommunityInsertForm::new(
|
||||
export_user.person.instance_id,
|
||||
|
@ -369,25 +355,29 @@ pub(crate) mod tests {
|
|||
"testcom".to_string(),
|
||||
"pubkey".to_string(),
|
||||
);
|
||||
let community = Community::create(&mut context.pool(), &community_form).await?;
|
||||
let community = Community::create(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?;
|
||||
CommunityFollower::follow(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 import_user = TestUser {
|
||||
name: "charles".into(),
|
||||
..Default::default()
|
||||
}
|
||||
.create(pool)
|
||||
.await?;
|
||||
|
||||
import_settings(backup, import_user.clone(), context.reset_request_count()).await?;
|
||||
|
||||
// wait for background task to finish
|
||||
sleep(Duration::from_millis(1000)).await;
|
||||
|
||||
let import_user_updated =
|
||||
LocalUserView::read(&mut context.pool(), import_user.local_user.id).await?;
|
||||
let import_user_updated = LocalUserView::read(pool, import_user.local_user.id).await?;
|
||||
|
||||
assert_eq!(
|
||||
export_user.person.display_name,
|
||||
|
@ -395,13 +385,12 @@ pub(crate) mod tests {
|
|||
);
|
||||
assert_eq!(export_user.person.bio, import_user_updated.person.bio);
|
||||
|
||||
let follows =
|
||||
CommunityFollowerView::for_person(&mut context.pool(), import_user.person.id).await?;
|
||||
let follows = CommunityFollowerView::for_person(pool, import_user.person.id).await?;
|
||||
assert_eq!(follows.len(), 1);
|
||||
assert_eq!(follows[0].community.actor_id, community.actor_id);
|
||||
|
||||
LocalUser::delete(&mut context.pool(), export_user.local_user.id).await?;
|
||||
LocalUser::delete(&mut context.pool(), import_user.local_user.id).await?;
|
||||
LocalUser::delete(pool, export_user.local_user.id).await?;
|
||||
LocalUser::delete(pool, import_user.local_user.id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -409,9 +398,14 @@ pub(crate) mod tests {
|
|||
#[serial]
|
||||
async fn disallow_large_backup() -> LemmyResult<()> {
|
||||
let context = LemmyContext::init_test_context().await;
|
||||
let pool = &mut context.pool();
|
||||
|
||||
let export_user =
|
||||
create_user("hanna".to_string(), Some("my bio".to_string()), &context).await?;
|
||||
let export_user = TestUser {
|
||||
bio: "my bio".into(),
|
||||
..Default::default()
|
||||
}
|
||||
.create(pool)
|
||||
.await?;
|
||||
|
||||
let mut backup = export_settings(export_user.clone(), context.reset_request_count()).await?;
|
||||
|
||||
|
@ -426,7 +420,7 @@ pub(crate) mod tests {
|
|||
backup.saved_comments.push("http://example4.com".parse()?);
|
||||
}
|
||||
|
||||
let import_user = create_user("charles".to_string(), None, &context).await?;
|
||||
let import_user = TestUser::default().create(pool).await?;
|
||||
|
||||
let imported =
|
||||
import_settings(backup, import_user.clone(), context.reset_request_count()).await;
|
||||
|
@ -436,8 +430,8 @@ pub(crate) mod tests {
|
|||
Some(LemmyErrorType::TooManyItems)
|
||||
);
|
||||
|
||||
LocalUser::delete(&mut context.pool(), export_user.local_user.id).await?;
|
||||
LocalUser::delete(&mut context.pool(), import_user.local_user.id).await?;
|
||||
LocalUser::delete(pool, export_user.local_user.id).await?;
|
||||
LocalUser::delete(pool, import_user.local_user.id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -445,9 +439,14 @@ pub(crate) mod tests {
|
|||
#[serial]
|
||||
async fn import_partial_backup() -> LemmyResult<()> {
|
||||
let context = LemmyContext::init_test_context().await;
|
||||
let pool = &mut context.pool();
|
||||
|
||||
let import_user =
|
||||
create_user("hanna".to_string(), Some("my bio".to_string()), &context).await?;
|
||||
let import_user = TestUser {
|
||||
bio: "my bio".into(),
|
||||
..Default::default()
|
||||
}
|
||||
.create(pool)
|
||||
.await?;
|
||||
|
||||
let backup =
|
||||
serde_json::from_str("{\"bot_account\": true, \"settings\": {\"theme\": \"my_theme\"}}")?;
|
||||
|
@ -458,8 +457,7 @@ pub(crate) mod tests {
|
|||
)
|
||||
.await?;
|
||||
|
||||
let import_user_updated =
|
||||
LocalUserView::read(&mut context.pool(), import_user.local_user.id).await?;
|
||||
let import_user_updated = LocalUserView::read(pool, import_user.local_user.id).await?;
|
||||
// mark as bot account
|
||||
assert!(import_user_updated.person.bot_account);
|
||||
// dont remove existing bio
|
||||
|
|
|
@ -104,7 +104,7 @@ async fn format_actor_url(
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::api::user_settings_backup::tests::create_user;
|
||||
use crate::api::test::TestUser;
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
community::{Community, CommunityInsertForm},
|
||||
|
@ -130,7 +130,7 @@ mod tests {
|
|||
),
|
||||
)
|
||||
.await?;
|
||||
let user = create_user("john".to_string(), None, &context).await?;
|
||||
let user = TestUser::default().create(&mut context.pool()).await?;
|
||||
|
||||
// insert a remote post which is already fetched
|
||||
let post_form = PostInsertForm {
|
||||
|
|
Loading…
Reference in a new issue