Compare commits

...

4 Commits

Author SHA1 Message Date
Richard Schwab
e5af66d72e
Allow admins to resolve removed or deleted objects via API 2024-09-30 14:04:30 +02:00
Dessalines
f7d881ac78
Adding skip_serializing_none to another OAuth API request. (#5060) 2024-09-27 11:15:44 -04:00
Nutomic
e82f72d3c8
Avoid breaking changes, keep response fields as deprecated (#5058) 2024-09-27 09:23:19 -04:00
Joseph Silva
50ce7961d1
Apply scheduled post limit to future posts instead of past posts, and verify this in test (#5054)
* test scheduled_post_count

* fix syntax error

* fix formatting

* fix argument order

* fix user_scheduled_post_count function
2024-09-27 08:51:10 -04:00
10 changed files with 193 additions and 37 deletions

View File

@ -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);
});

View File

@ -76,5 +76,7 @@ pub async fn leave_admin(
admin_oauth_providers: None,
blocked_urls,
tagline,
taglines: vec![],
custom_emojis: vec![],
}))
}

View File

@ -5,6 +5,7 @@ use serde_with::skip_serializing_none;
use ts_rs::TS;
use url::Url;
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]

View File

@ -306,6 +306,8 @@ pub struct EditSite {
/// The response for a site.
pub struct SiteResponse {
pub site_view: SiteView,
/// deprecated, use field `tagline` or /api/v3/tagline/list
pub taglines: Vec<()>,
}
#[skip_serializing_none]
@ -320,6 +322,10 @@ pub struct GetSiteResponse {
pub my_user: Option<MyUserInfo>,
pub all_languages: Vec<Language>,
pub discussion_languages: Vec<LanguageId>,
/// deprecated, use field `tagline` or /api/v3/tagline/list
pub taglines: Vec<()>,
/// deprecated, use /api/v3/custom_emoji/list
pub custom_emojis: Vec<()>,
/// If the site has any taglines, a random one is included here for displaying
pub tagline: Option<Tagline>,
/// A list of external auth methods your site supports.

View File

@ -139,7 +139,10 @@ pub async fn create_site(
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 }))
Ok(Json(SiteResponse {
site_view,
taglines: vec![],
}))
}
fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) -> LemmyResult<()> {

View File

@ -59,6 +59,8 @@ pub async fn get_site(
tagline,
oauth_providers: Some(oauth_providers),
admin_oauth_providers: Some(admin_oauth_providers),
taglines: vec![],
custom_emojis: vec![],
})
})
.await

View File

@ -193,7 +193,10 @@ pub async fn update_site(
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 }))
Ok(Json(SiteResponse {
site_view,
taglines: vec![],
}))
}
fn validate_update_payload(local_site: &LocalSite, edit_site: &EditSite) -> LemmyResult<()> {

View File

@ -4,7 +4,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,34 +46,159 @@ async fn convert_response(
pool: &mut DbPool<'_>,
) -> LemmyResult<Json<ResolveObjectResponse>> {
use SearchableObjects::*;
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 {
Post(p) => {
removed_or_deleted = p.deleted || p.removed;
res.post = Some(PostView::read(pool, p.id, local_user.as_ref(), false).await?)
}
Comment(c) => {
removed_or_deleted = c.deleted || c.removed;
res.comment = Some(CommentView::read(pool, c.id, local_user.as_ref()).await?)
}
Post(p) => res.post = Some(PostView::read(pool, p.id, local_user.as_ref(), is_admin).await?),
Comment(c) => res.comment = Some(CommentView::read(pool, c.id, local_user.as_ref()).await?),
PersonOrCommunity(p) => match *p {
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;
use activitypub_federation::config::Data;
use actix_web::web::Query;
use lemmy_api_common::{context::LemmyContext, site::ResolveObject};
use lemmy_db_schema::{
newtypes::InstanceId,
source::{
community::{Community, CommunityInsertForm},
instance::Instance,
local_site::{LocalSite, LocalSiteInsertForm},
local_user::{LocalUser, LocalUserInsertForm},
person::{Person, PersonInsertForm},
post::{Post, PostInsertForm, PostUpdateForm},
site::{Site, SiteInsertForm},
},
traits::Crud,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
use serial_test::serial;
async fn create_user(
instance_id: InstanceId,
name: String,
admin: bool,
context: &Data<LemmyContext>,
) -> LemmyResult<LocalUserView> {
let person_form = PersonInsertForm::test_form(instance_id, &name);
let person = Person::create(&mut context.pool(), &person_form).await?;
let user_form = match admin {
true => LocalUserInsertForm::test_form_admin(person.id),
false => 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]
#[expect(clippy::unwrap_used)]
async fn test_object_visibility() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let instance = Instance::read_or_create(&mut context.pool(), "example.com".to_string()).await?;
let site_form = SiteInsertForm::new("test site".to_string(), instance.id);
let site = Site::create(&mut context.pool(), &site_form).await?;
let local_site_form = LocalSiteInsertForm {
site_setup: Some(true),
private_instance: Some(false),
..LocalSiteInsertForm::new(site.id)
};
LocalSite::create(&mut context.pool(), &local_site_form).await?;
let creator = create_user(instance.id, "creator".to_string(), false, &context).await?;
let regular_user = create_user(instance.id, "user".to_string(), false, &context).await?;
let admin_user = create_user(instance.id, "admin".to_string(), true, &context).await?;
//let community_insert_form = ;
let community = Community::create(
&mut context.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(&mut context.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(
&mut context.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(&mut context.pool()).await?;
Site::delete(&mut context.pool(), site.id).await?;
Instance::delete(&mut context.pool(), instance.id).await?;
Ok(())
}
}

View File

@ -258,9 +258,9 @@ impl Post {
post::table
.inner_join(person::table)
.inner_join(community::table)
// find all posts which have scheduled_publish_time that is in the past
// find all posts which have scheduled_publish_time that is in the future
.filter(post::scheduled_publish_time.is_not_null())
.filter(coalesce(post::scheduled_publish_time, now()).lt(now()))
.filter(coalesce(post::scheduled_publish_time, now()).gt(now()))
// make sure the post and community are still around
.filter(not(post::deleted.or(post::removed)))
.filter(not(community::removed.or(community::deleted)))
@ -414,6 +414,7 @@ mod tests {
traits::{Crud, Likeable, Saveable},
utils::build_db_pool_for_tests,
};
use chrono::DateTime;
use pretty_assertions::assert_eq;
use serial_test::serial;
use std::collections::HashSet;
@ -456,6 +457,12 @@ mod tests {
);
let inserted_post2 = Post::create(pool, &new_post2).await.unwrap();
let new_scheduled_post = PostInsertForm {
scheduled_publish_time: Some(DateTime::from_timestamp_nanos(i64::MAX)),
..PostInsertForm::new("beans".into(), inserted_person.id, inserted_community.id)
};
let inserted_scheduled_post = Post::create(pool, &new_scheduled_post).await.unwrap();
let expected_post = Post {
id: inserted_post.id,
name: "A test post".into(),
@ -535,6 +542,12 @@ mod tests {
.await
.unwrap();
// Scheduled post count
let scheduled_post_count = Post::user_scheduled_post_count(inserted_person.id, pool)
.await
.unwrap();
assert_eq!(1, scheduled_post_count);
let like_removed = PostLike::remove(pool, inserted_person.id, inserted_post.id)
.await
.unwrap();
@ -551,8 +564,11 @@ mod tests {
assert_eq!(2, read_removed);
let num_deleted = Post::delete(pool, inserted_post.id).await.unwrap()
+ Post::delete(pool, inserted_post2.id).await.unwrap();
assert_eq!(2, num_deleted);
+ Post::delete(pool, inserted_post2.id).await.unwrap()
+ Post::delete(pool, inserted_scheduled_post.id)
.await
.unwrap();
assert_eq!(3, num_deleted);
Community::delete(pool, inserted_community.id)
.await
.unwrap();

View File

@ -769,7 +769,7 @@ diesel::table! {
featured_local -> Bool,
url_content_type -> Nullable<Text>,
alt_text -> Nullable<Text>,
scheduled_publish_time -> Nullable<Timestamptz>
scheduled_publish_time -> Nullable<Timestamptz>,
}
}