mirror of
https://github.com/LemmyNet/lemmy
synced 2024-11-21 20:13:05 +00:00
Local only community (#4350)
* Add support for local only community (fixes #1576) * add filters and tests to db views * dont federate local only community * test get apub community http * tests * more checks * wip * api test * fix tests * change community.local_only column to visibility enum (for private communities) * sql fmt * rename vars * clippy * fix tests * update lib * review * fix js client version * update client
This commit is contained in:
parent
8cde452fca
commit
0f414a95d5
37 changed files with 962 additions and 535 deletions
|
@ -27,7 +27,7 @@
|
||||||
"eslint": "^8.55.0",
|
"eslint": "^8.55.0",
|
||||||
"eslint-plugin-prettier": "^5.0.1",
|
"eslint-plugin-prettier": "^5.0.1",
|
||||||
"jest": "^29.5.0",
|
"jest": "^29.5.0",
|
||||||
"lemmy-js-client": "0.19.2-alpha.2",
|
"lemmy-js-client": "0.19.3-alpha.2",
|
||||||
"prettier": "^3.1.1",
|
"prettier": "^3.1.1",
|
||||||
"ts-jest": "^29.1.0",
|
"ts-jest": "^29.1.0",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
|
|
|
@ -32,8 +32,9 @@ import {
|
||||||
resolveBetaCommunity,
|
resolveBetaCommunity,
|
||||||
longDelay,
|
longDelay,
|
||||||
delay,
|
delay,
|
||||||
|
editCommunity,
|
||||||
} from "./shared";
|
} from "./shared";
|
||||||
import { EditSite } from "lemmy-js-client";
|
import { EditCommunity, EditSite } from "lemmy-js-client";
|
||||||
|
|
||||||
beforeAll(setupLogins);
|
beforeAll(setupLogins);
|
||||||
|
|
||||||
|
@ -511,3 +512,24 @@ test("Fetch community, includes posts", async () => {
|
||||||
expect(post_listing.posts.length).toBe(1);
|
expect(post_listing.posts.length).toBe(1);
|
||||||
expect(post_listing.posts[0].post.ap_id).toBe(postRes.post_view.post.ap_id);
|
expect(post_listing.posts[0].post.ap_id).toBe(postRes.post_view.post.ap_id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Content in local-only community doesnt federate", async () => {
|
||||||
|
// create a community and set it local-only
|
||||||
|
let communityRes = (await createCommunity(alpha)).community_view.community;
|
||||||
|
let form: EditCommunity = {
|
||||||
|
community_id: communityRes.id,
|
||||||
|
visibility: "LocalOnly",
|
||||||
|
};
|
||||||
|
await editCommunity(alpha, form);
|
||||||
|
|
||||||
|
// cant resolve the community from another instance
|
||||||
|
await expect(
|
||||||
|
resolveCommunity(beta, communityRes.actor_id),
|
||||||
|
).rejects.toStrictEqual(Error("couldnt_find_object"));
|
||||||
|
|
||||||
|
// create a post, also cant resolve it
|
||||||
|
let postRes = await createPost(alpha, communityRes.id);
|
||||||
|
await expect(resolvePost(beta, postRes.post_view.post)).rejects.toStrictEqual(
|
||||||
|
Error("couldnt_find_object"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
BlockInstanceResponse,
|
BlockInstanceResponse,
|
||||||
CommunityId,
|
CommunityId,
|
||||||
CreatePrivateMessageReport,
|
CreatePrivateMessageReport,
|
||||||
|
EditCommunity,
|
||||||
GetReplies,
|
GetReplies,
|
||||||
GetRepliesResponse,
|
GetRepliesResponse,
|
||||||
GetUnreadCountResponse,
|
GetUnreadCountResponse,
|
||||||
|
@ -532,6 +533,13 @@ export async function createCommunity(
|
||||||
return api.createCommunity(form);
|
return api.createCommunity(form);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function editCommunity(
|
||||||
|
api: LemmyHttp,
|
||||||
|
form: EditCommunity,
|
||||||
|
): Promise<CommunityResponse> {
|
||||||
|
return api.editCommunity(form);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getCommunity(
|
export async function getCommunity(
|
||||||
api: LemmyHttp,
|
api: LemmyHttp,
|
||||||
id: number,
|
id: number,
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,7 @@
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
newtypes::{CommunityId, LanguageId, PersonId},
|
newtypes::{CommunityId, LanguageId, PersonId},
|
||||||
source::site::Site,
|
source::site::Site,
|
||||||
|
CommunityVisibility,
|
||||||
ListingType,
|
ListingType,
|
||||||
SortType,
|
SortType,
|
||||||
};
|
};
|
||||||
|
@ -54,6 +55,7 @@ pub struct CreateCommunity {
|
||||||
/// Whether to restrict posting only to moderators.
|
/// Whether to restrict posting only to moderators.
|
||||||
pub posting_restricted_to_mods: Option<bool>,
|
pub posting_restricted_to_mods: Option<bool>,
|
||||||
pub discussion_languages: Option<Vec<LanguageId>>,
|
pub discussion_languages: Option<Vec<LanguageId>>,
|
||||||
|
pub visibility: Option<CommunityVisibility>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
@ -150,6 +152,7 @@ pub struct EditCommunity {
|
||||||
/// Whether to restrict posting only to moderators.
|
/// Whether to restrict posting only to moderators.
|
||||||
pub posting_restricted_to_mods: Option<bool>,
|
pub posting_restricted_to_mods: Option<bool>,
|
||||||
pub discussion_languages: Option<Vec<LanguageId>>,
|
pub discussion_languages: Option<Vec<LanguageId>>,
|
||||||
|
pub visibility: Option<CommunityVisibility>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[skip_serializing_none]
|
#[skip_serializing_none]
|
||||||
|
|
|
@ -92,6 +92,7 @@ pub async fn create_community(
|
||||||
.shared_inbox_url(Some(generate_shared_inbox_url(context.settings())?))
|
.shared_inbox_url(Some(generate_shared_inbox_url(context.settings())?))
|
||||||
.posting_restricted_to_mods(data.posting_restricted_to_mods)
|
.posting_restricted_to_mods(data.posting_restricted_to_mods)
|
||||||
.instance_id(site_view.site.instance_id)
|
.instance_id(site_view.site.instance_id)
|
||||||
|
.visibility(data.visibility)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let inserted_community = Community::create(&mut context.pool(), &community_form)
|
let inserted_community = Community::create(&mut context.pool(), &community_form)
|
||||||
|
|
|
@ -72,6 +72,7 @@ pub async fn update_community(
|
||||||
banner,
|
banner,
|
||||||
nsfw: data.nsfw,
|
nsfw: data.nsfw,
|
||||||
posting_restricted_to_mods: data.posting_restricted_to_mods,
|
posting_restricted_to_mods: data.posting_restricted_to_mods,
|
||||||
|
visibility: data.visibility,
|
||||||
updated: Some(Some(naive_now())),
|
updated: Some(Some(naive_now())),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,6 +26,7 @@ use lemmy_db_schema::{
|
||||||
post::{Post, PostInsertForm, PostLike, PostLikeForm, PostUpdateForm},
|
post::{Post, PostInsertForm, PostLike, PostLikeForm, PostUpdateForm},
|
||||||
},
|
},
|
||||||
traits::{Crud, Likeable},
|
traits::{Crud, Likeable},
|
||||||
|
CommunityVisibility,
|
||||||
};
|
};
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
use lemmy_db_views::structs::LocalUserView;
|
||||||
use lemmy_db_views_actor::structs::CommunityView;
|
use lemmy_db_views_actor::structs::CommunityView;
|
||||||
|
@ -165,20 +166,22 @@ pub async fn create_post(
|
||||||
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
|
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
|
||||||
|
|
||||||
if let Some(url) = updated_post.url.clone() {
|
if let Some(url) = updated_post.url.clone() {
|
||||||
spawn_try_task(async move {
|
if community.visibility == CommunityVisibility::Public {
|
||||||
let mut webmention =
|
spawn_try_task(async move {
|
||||||
Webmention::new::<Url>(updated_post.ap_id.clone().into(), url.clone().into())?;
|
let mut webmention =
|
||||||
webmention.set_checked(true);
|
Webmention::new::<Url>(updated_post.ap_id.clone().into(), url.clone().into())?;
|
||||||
match webmention
|
webmention.set_checked(true);
|
||||||
.send()
|
match webmention
|
||||||
.instrument(tracing::info_span!("Sending webmention"))
|
.send()
|
||||||
.await
|
.instrument(tracing::info_span!("Sending webmention"))
|
||||||
{
|
.await
|
||||||
Err(WebmentionError::NoEndpointDiscovered(_)) => Ok(()),
|
{
|
||||||
Ok(_) => Ok(()),
|
Err(WebmentionError::NoEndpointDiscovered(_)) => Ok(()),
|
||||||
Err(e) => Err(e).with_lemmy_type(LemmyErrorType::CouldntSendWebmention),
|
Ok(_) => Ok(()),
|
||||||
}
|
Err(e) => Err(e).with_lemmy_type(LemmyErrorType::CouldntSendWebmention),
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
build_post_response(&context, community_id, &local_user_view.person, post_id).await
|
build_post_response(&context, community_id, &local_user_view.person, post_id).await
|
||||||
|
|
|
@ -79,7 +79,7 @@ pub async fn get_site(
|
||||||
|pool| CommunityBlockView::for_person(pool, person_id),
|
|pool| CommunityBlockView::for_person(pool, person_id),
|
||||||
|pool| InstanceBlockView::for_person(pool, person_id),
|
|pool| InstanceBlockView::for_person(pool, person_id),
|
||||||
|pool| PersonBlockView::for_person(pool, person_id),
|
|pool| PersonBlockView::for_person(pool, person_id),
|
||||||
|pool| CommunityModeratorView::for_person(pool, person_id),
|
|pool| CommunityModeratorView::for_person(pool, person_id, true),
|
||||||
|pool| LocalUserLanguage::read(pool, local_user_id)
|
|pool| LocalUserLanguage::read(pool, local_user_id)
|
||||||
))
|
))
|
||||||
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
|
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
|
||||||
|
|
|
@ -22,7 +22,10 @@ use activitypub_federation::{
|
||||||
traits::{ActivityHandler, Actor},
|
traits::{ActivityHandler, Actor},
|
||||||
};
|
};
|
||||||
use lemmy_api_common::context::LemmyContext;
|
use lemmy_api_common::context::LemmyContext;
|
||||||
use lemmy_db_schema::source::{activity::ActivitySendTargets, community::CommunityFollower};
|
use lemmy_db_schema::{
|
||||||
|
source::{activity::ActivitySendTargets, community::CommunityFollower},
|
||||||
|
CommunityVisibility,
|
||||||
|
};
|
||||||
use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult};
|
use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
@ -210,6 +213,10 @@ async fn can_accept_activity_in_community(
|
||||||
{
|
{
|
||||||
Err(LemmyErrorType::CommunityHasNoFollowers)?
|
Err(LemmyErrorType::CommunityHasNoFollowers)?
|
||||||
}
|
}
|
||||||
|
// Local only community can't federate
|
||||||
|
if community.visibility != CommunityVisibility::Public {
|
||||||
|
return Err(LemmyErrorType::CouldntFindCommunity.into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,10 @@ use crate::{
|
||||||
};
|
};
|
||||||
use activitypub_federation::{config::Data, traits::Actor};
|
use activitypub_federation::{config::Data, traits::Actor};
|
||||||
use lemmy_api_common::context::LemmyContext;
|
use lemmy_api_common::context::LemmyContext;
|
||||||
use lemmy_db_schema::source::{activity::ActivitySendTargets, person::PersonFollower};
|
use lemmy_db_schema::{
|
||||||
|
source::{activity::ActivitySendTargets, person::PersonFollower},
|
||||||
|
CommunityVisibility,
|
||||||
|
};
|
||||||
use lemmy_utils::error::LemmyError;
|
use lemmy_utils::error::LemmyError;
|
||||||
|
|
||||||
pub mod announce;
|
pub mod announce;
|
||||||
|
@ -37,6 +40,11 @@ pub(crate) async fn send_activity_in_community(
|
||||||
is_mod_action: bool,
|
is_mod_action: bool,
|
||||||
context: &Data<LemmyContext>,
|
context: &Data<LemmyContext>,
|
||||||
) -> Result<(), LemmyError> {
|
) -> Result<(), LemmyError> {
|
||||||
|
// If community is local only, don't send anything out
|
||||||
|
if community.visibility != CommunityVisibility::Public {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
// send to any users which are mentioned or affected directly
|
// send to any users which are mentioned or affected directly
|
||||||
let mut inboxes = extra_inboxes;
|
let mut inboxes = extra_inboxes;
|
||||||
|
|
||||||
|
|
|
@ -97,15 +97,10 @@ impl ActivityHandler for UpdateCommunity {
|
||||||
&None,
|
&None,
|
||||||
&self.object.source,
|
&self.object.source,
|
||||||
)),
|
)),
|
||||||
removed: None,
|
|
||||||
published: self.object.published.map(Into::into),
|
published: self.object.published.map(Into::into),
|
||||||
updated: Some(self.object.updated.map(Into::into)),
|
updated: Some(self.object.updated.map(Into::into)),
|
||||||
deleted: None,
|
|
||||||
nsfw: Some(self.object.sensitive.unwrap_or(false)),
|
nsfw: Some(self.object.sensitive.unwrap_or(false)),
|
||||||
actor_id: Some(self.object.id.into()),
|
actor_id: Some(self.object.id.into()),
|
||||||
local: None,
|
|
||||||
private_key: None,
|
|
||||||
hidden: None,
|
|
||||||
public_key: Some(self.object.public_key.public_key_pem),
|
public_key: Some(self.object.public_key.public_key_pem),
|
||||||
last_refreshed_at: Some(naive_now()),
|
last_refreshed_at: Some(naive_now()),
|
||||||
icon: Some(self.object.icon.map(|i| i.url.into())),
|
icon: Some(self.object.icon.map(|i| i.url.into())),
|
||||||
|
@ -116,6 +111,7 @@ impl ActivityHandler for UpdateCommunity {
|
||||||
moderators_url: self.object.attributed_to.map(Into::into),
|
moderators_url: self.object.attributed_to.map(Into::into),
|
||||||
posting_restricted_to_mods: self.object.posting_restricted_to_mods,
|
posting_restricted_to_mods: self.object.posting_restricted_to_mods,
|
||||||
featured_url: self.object.featured.map(Into::into),
|
featured_url: self.object.featured.map(Into::into),
|
||||||
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
Community::update(&mut context.pool(), community.id, &community_update_form).await?;
|
Community::update(&mut context.pool(), community.id, &community_update_form).await?;
|
||||||
|
|
|
@ -24,8 +24,9 @@ use lemmy_db_schema::{
|
||||||
person::{PersonFollower, PersonFollowerForm},
|
person::{PersonFollower, PersonFollowerForm},
|
||||||
},
|
},
|
||||||
traits::Followable,
|
traits::Followable,
|
||||||
|
CommunityVisibility,
|
||||||
};
|
};
|
||||||
use lemmy_utils::error::LemmyError;
|
use lemmy_utils::error::{LemmyError, LemmyErrorType};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
impl Follow {
|
impl Follow {
|
||||||
|
@ -103,6 +104,10 @@ impl ActivityHandler for Follow {
|
||||||
PersonFollower::follow(&mut context.pool(), &form).await?;
|
PersonFollower::follow(&mut context.pool(), &form).await?;
|
||||||
}
|
}
|
||||||
UserOrCommunity::Community(c) => {
|
UserOrCommunity::Community(c) => {
|
||||||
|
// Dont allow following local-only community via federation.
|
||||||
|
if c.visibility != CommunityVisibility::Public {
|
||||||
|
return Err(LemmyErrorType::CouldntFindCommunity.into());
|
||||||
|
}
|
||||||
let form = CommunityFollowerForm {
|
let form = CommunityFollowerForm {
|
||||||
community_id: c.id,
|
community_id: c.id,
|
||||||
person_id: actor.id,
|
person_id: actor.id,
|
||||||
|
|
|
@ -86,8 +86,12 @@ pub async fn read_person(
|
||||||
.list(&mut context.pool())
|
.list(&mut context.pool())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let moderates =
|
let moderates = CommunityModeratorView::for_person(
|
||||||
CommunityModeratorView::for_person(&mut context.pool(), person_details_id).await?;
|
&mut context.pool(),
|
||||||
|
person_details_id,
|
||||||
|
local_user_view.is_some(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let site = read_site_for_actor(person_view.person.actor_id.clone(), &context).await?;
|
let site = read_site_for_actor(person_view.person.actor_id.clone(), &context).await?;
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ use lemmy_db_schema::{source::post::Post, utils::FETCH_LIMIT_MAX};
|
||||||
use lemmy_utils::error::LemmyError;
|
use lemmy_utils::error::LemmyError;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub(crate) struct ApubCommunityFeatured(());
|
pub(crate) struct ApubCommunityFeatured(());
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
|
|
|
@ -1,11 +1,20 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
http::{create_apub_response, create_apub_tombstone_response, redirect_remote_object},
|
http::{
|
||||||
|
check_community_public,
|
||||||
|
create_apub_response,
|
||||||
|
create_apub_tombstone_response,
|
||||||
|
redirect_remote_object,
|
||||||
|
},
|
||||||
objects::comment::ApubComment,
|
objects::comment::ApubComment,
|
||||||
};
|
};
|
||||||
use activitypub_federation::{config::Data, traits::Object};
|
use activitypub_federation::{config::Data, traits::Object};
|
||||||
use actix_web::{web::Path, HttpResponse};
|
use actix_web::{web::Path, HttpResponse};
|
||||||
use lemmy_api_common::context::LemmyContext;
|
use lemmy_api_common::context::LemmyContext;
|
||||||
use lemmy_db_schema::{newtypes::CommentId, source::comment::Comment, traits::Crud};
|
use lemmy_db_schema::{
|
||||||
|
newtypes::CommentId,
|
||||||
|
source::{comment::Comment, community::Community, post::Post},
|
||||||
|
traits::Crud,
|
||||||
|
};
|
||||||
use lemmy_utils::error::LemmyError;
|
use lemmy_utils::error::LemmyError;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
@ -21,7 +30,12 @@ pub(crate) async fn get_apub_comment(
|
||||||
context: Data<LemmyContext>,
|
context: Data<LemmyContext>,
|
||||||
) -> Result<HttpResponse, LemmyError> {
|
) -> Result<HttpResponse, LemmyError> {
|
||||||
let id = CommentId(info.comment_id.parse::<i32>()?);
|
let id = CommentId(info.comment_id.parse::<i32>()?);
|
||||||
|
// Can't use CommentView here because it excludes deleted/removed/local-only items
|
||||||
let comment: ApubComment = Comment::read(&mut context.pool(), id).await?.into();
|
let comment: ApubComment = Comment::read(&mut context.pool(), id).await?.into();
|
||||||
|
let post = Post::read(&mut context.pool(), comment.post_id).await?;
|
||||||
|
let community = Community::read(&mut context.pool(), post.community_id).await?;
|
||||||
|
check_community_public(&community)?;
|
||||||
|
|
||||||
if !comment.local {
|
if !comment.local {
|
||||||
Ok(redirect_remote_object(&comment.ap_id))
|
Ok(redirect_remote_object(&comment.ap_id))
|
||||||
} else if !comment.deleted && !comment.removed {
|
} else if !comment.deleted && !comment.removed {
|
||||||
|
|
|
@ -6,7 +6,7 @@ use crate::{
|
||||||
community_moderators::ApubCommunityModerators,
|
community_moderators::ApubCommunityModerators,
|
||||||
community_outbox::ApubCommunityOutbox,
|
community_outbox::ApubCommunityOutbox,
|
||||||
},
|
},
|
||||||
http::{create_apub_response, create_apub_tombstone_response},
|
http::{check_community_public, create_apub_response, create_apub_tombstone_response},
|
||||||
objects::{community::ApubCommunity, person::ApubPerson},
|
objects::{community::ApubCommunity, person::ApubPerson},
|
||||||
};
|
};
|
||||||
use activitypub_federation::{
|
use activitypub_federation::{
|
||||||
|
@ -18,10 +18,10 @@ use activitypub_federation::{
|
||||||
use actix_web::{web, web::Bytes, HttpRequest, HttpResponse};
|
use actix_web::{web, web::Bytes, HttpRequest, HttpResponse};
|
||||||
use lemmy_api_common::context::LemmyContext;
|
use lemmy_api_common::context::LemmyContext;
|
||||||
use lemmy_db_schema::{source::community::Community, traits::ApubActor};
|
use lemmy_db_schema::{source::community::Community, traits::ApubActor};
|
||||||
use lemmy_utils::error::{LemmyError, LemmyErrorType};
|
use lemmy_utils::error::LemmyError;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Clone)]
|
||||||
pub(crate) struct CommunityQuery {
|
pub(crate) struct CommunityQuery {
|
||||||
community_name: String,
|
community_name: String,
|
||||||
}
|
}
|
||||||
|
@ -37,13 +37,13 @@ pub(crate) async fn get_apub_community_http(
|
||||||
.await?
|
.await?
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
if !community.deleted && !community.removed {
|
if community.deleted || community.removed {
|
||||||
let apub = community.into_json(&context).await?;
|
return create_apub_tombstone_response(community.actor_id.clone());
|
||||||
|
|
||||||
create_apub_response(&apub)
|
|
||||||
} else {
|
|
||||||
create_apub_tombstone_response(community.actor_id.clone())
|
|
||||||
}
|
}
|
||||||
|
check_community_public(&community)?;
|
||||||
|
|
||||||
|
let apub = community.into_json(&context).await?;
|
||||||
|
create_apub_response(&apub)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handler for all incoming receive to community inboxes.
|
/// Handler for all incoming receive to community inboxes.
|
||||||
|
@ -66,6 +66,7 @@ pub(crate) async fn get_apub_community_followers(
|
||||||
) -> Result<HttpResponse, LemmyError> {
|
) -> Result<HttpResponse, LemmyError> {
|
||||||
let community =
|
let community =
|
||||||
Community::read_from_name(&mut context.pool(), &info.community_name, false).await?;
|
Community::read_from_name(&mut context.pool(), &info.community_name, false).await?;
|
||||||
|
check_community_public(&community)?;
|
||||||
let followers = ApubCommunityFollower::read_local(&community.into(), &context).await?;
|
let followers = ApubCommunityFollower::read_local(&community.into(), &context).await?;
|
||||||
create_apub_response(&followers)
|
create_apub_response(&followers)
|
||||||
}
|
}
|
||||||
|
@ -80,9 +81,7 @@ pub(crate) async fn get_apub_community_outbox(
|
||||||
Community::read_from_name(&mut context.pool(), &info.community_name, false)
|
Community::read_from_name(&mut context.pool(), &info.community_name, false)
|
||||||
.await?
|
.await?
|
||||||
.into();
|
.into();
|
||||||
if community.deleted || community.removed {
|
check_community_public(&community)?;
|
||||||
Err(LemmyErrorType::Deleted)?
|
|
||||||
}
|
|
||||||
let outbox = ApubCommunityOutbox::read_local(&community, &context).await?;
|
let outbox = ApubCommunityOutbox::read_local(&community, &context).await?;
|
||||||
create_apub_response(&outbox)
|
create_apub_response(&outbox)
|
||||||
}
|
}
|
||||||
|
@ -96,9 +95,7 @@ pub(crate) async fn get_apub_community_moderators(
|
||||||
Community::read_from_name(&mut context.pool(), &info.community_name, false)
|
Community::read_from_name(&mut context.pool(), &info.community_name, false)
|
||||||
.await?
|
.await?
|
||||||
.into();
|
.into();
|
||||||
if community.deleted || community.removed {
|
check_community_public(&community)?;
|
||||||
Err(LemmyErrorType::Deleted)?
|
|
||||||
}
|
|
||||||
let moderators = ApubCommunityModerators::read_local(&community, &context).await?;
|
let moderators = ApubCommunityModerators::read_local(&community, &context).await?;
|
||||||
create_apub_response(&moderators)
|
create_apub_response(&moderators)
|
||||||
}
|
}
|
||||||
|
@ -112,9 +109,153 @@ pub(crate) async fn get_apub_community_featured(
|
||||||
Community::read_from_name(&mut context.pool(), &info.community_name, false)
|
Community::read_from_name(&mut context.pool(), &info.community_name, false)
|
||||||
.await?
|
.await?
|
||||||
.into();
|
.into();
|
||||||
if community.deleted || community.removed {
|
check_community_public(&community)?;
|
||||||
Err(LemmyErrorType::Deleted)?
|
|
||||||
}
|
|
||||||
let featured = ApubCommunityFeatured::read_local(&community, &context).await?;
|
let featured = ApubCommunityFeatured::read_local(&community, &context).await?;
|
||||||
create_apub_response(&featured)
|
create_apub_response(&featured)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
#![allow(clippy::indexing_slicing)]
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::protocol::objects::{group::Group, tombstone::Tombstone};
|
||||||
|
use actix_web::body::to_bytes;
|
||||||
|
use lemmy_db_schema::{
|
||||||
|
source::{
|
||||||
|
community::{Community, CommunityInsertForm},
|
||||||
|
instance::Instance,
|
||||||
|
},
|
||||||
|
traits::Crud,
|
||||||
|
CommunityVisibility,
|
||||||
|
};
|
||||||
|
use lemmy_utils::error::LemmyResult;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serial_test::serial;
|
||||||
|
|
||||||
|
async fn init(
|
||||||
|
deleted: bool,
|
||||||
|
visibility: CommunityVisibility,
|
||||||
|
context: &Data<LemmyContext>,
|
||||||
|
) -> Result<(Instance, Community), LemmyError> {
|
||||||
|
let instance =
|
||||||
|
Instance::read_or_create(&mut context.pool(), "my_domain.tld".to_string()).await?;
|
||||||
|
let community_form = CommunityInsertForm::builder()
|
||||||
|
.name("testcom6".to_string())
|
||||||
|
.title("nada".to_owned())
|
||||||
|
.public_key("pubkey".to_string())
|
||||||
|
.instance_id(instance.id)
|
||||||
|
.deleted(Some(deleted))
|
||||||
|
.visibility(Some(visibility))
|
||||||
|
.build();
|
||||||
|
let community = Community::create(&mut context.pool(), &community_form).await?;
|
||||||
|
Ok((instance, community))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn decode_response<T: DeserializeOwned>(res: HttpResponse) -> Result<T, LemmyError> {
|
||||||
|
let body = to_bytes(res.into_body()).await.unwrap();
|
||||||
|
let body = std::str::from_utf8(&body)?;
|
||||||
|
Ok(serde_json::from_str(body)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_get_community() -> LemmyResult<()> {
|
||||||
|
let context = LemmyContext::init_test_context().await;
|
||||||
|
|
||||||
|
// fetch invalid community
|
||||||
|
let query = CommunityQuery {
|
||||||
|
community_name: "asd".to_string(),
|
||||||
|
};
|
||||||
|
let res = get_apub_community_http(query.into(), context.reset_request_count()).await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
|
||||||
|
let (instance, community) = init(false, CommunityVisibility::Public, &context).await?;
|
||||||
|
|
||||||
|
// fetch valid community
|
||||||
|
let query = CommunityQuery {
|
||||||
|
community_name: community.name.clone(),
|
||||||
|
};
|
||||||
|
let res = get_apub_community_http(query.clone().into(), context.reset_request_count()).await?;
|
||||||
|
assert_eq!(200, res.status());
|
||||||
|
let res_group: Group = decode_response(res).await?;
|
||||||
|
let community: ApubCommunity = community.into();
|
||||||
|
let group = community.clone().into_json(&context).await?;
|
||||||
|
assert_eq!(group, res_group);
|
||||||
|
|
||||||
|
let res =
|
||||||
|
get_apub_community_featured(query.clone().into(), context.reset_request_count()).await?;
|
||||||
|
assert_eq!(200, res.status());
|
||||||
|
let res =
|
||||||
|
get_apub_community_followers(query.clone().into(), context.reset_request_count()).await?;
|
||||||
|
assert_eq!(200, res.status());
|
||||||
|
let res =
|
||||||
|
get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await?;
|
||||||
|
assert_eq!(200, res.status());
|
||||||
|
let res = get_apub_community_outbox(query.into(), context.reset_request_count()).await?;
|
||||||
|
assert_eq!(200, res.status());
|
||||||
|
|
||||||
|
Instance::delete(&mut context.pool(), instance.id).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_get_deleted_community() -> LemmyResult<()> {
|
||||||
|
let context = LemmyContext::init_test_context().await;
|
||||||
|
let (instance, community) = init(true, CommunityVisibility::LocalOnly, &context).await?;
|
||||||
|
|
||||||
|
// should return tombstone
|
||||||
|
let query = CommunityQuery {
|
||||||
|
community_name: community.name.clone(),
|
||||||
|
};
|
||||||
|
let res = get_apub_community_http(query.clone().into(), context.reset_request_count()).await?;
|
||||||
|
assert_eq!(410, res.status());
|
||||||
|
let res_tombstone = decode_response::<Tombstone>(res).await;
|
||||||
|
assert!(res_tombstone.is_ok());
|
||||||
|
|
||||||
|
let res =
|
||||||
|
get_apub_community_featured(query.clone().into(), context.reset_request_count()).await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
let res =
|
||||||
|
get_apub_community_followers(query.clone().into(), context.reset_request_count()).await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
let res =
|
||||||
|
get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
let res = get_apub_community_outbox(query.into(), context.reset_request_count()).await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
|
||||||
|
//Community::delete(&mut context.pool(), community.id).await?;
|
||||||
|
Instance::delete(&mut context.pool(), instance.id).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_get_local_only_community() -> LemmyResult<()> {
|
||||||
|
let context = LemmyContext::init_test_context().await;
|
||||||
|
let (instance, community) = init(false, CommunityVisibility::LocalOnly, &context).await?;
|
||||||
|
|
||||||
|
let query = CommunityQuery {
|
||||||
|
community_name: community.name.clone(),
|
||||||
|
};
|
||||||
|
let res = get_apub_community_http(query.clone().into(), context.reset_request_count()).await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
let res =
|
||||||
|
get_apub_community_featured(query.clone().into(), context.reset_request_count()).await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
let res =
|
||||||
|
get_apub_community_followers(query.clone().into(), context.reset_request_count()).await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
let res =
|
||||||
|
get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
let res = get_apub_community_outbox(query.into(), context.reset_request_count()).await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
|
||||||
|
Instance::delete(&mut context.pool(), instance.id).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -13,8 +13,12 @@ use activitypub_federation::{
|
||||||
use actix_web::{web, web::Bytes, HttpRequest, HttpResponse};
|
use actix_web::{web, web::Bytes, HttpRequest, HttpResponse};
|
||||||
use http::{header::LOCATION, StatusCode};
|
use http::{header::LOCATION, StatusCode};
|
||||||
use lemmy_api_common::context::LemmyContext;
|
use lemmy_api_common::context::LemmyContext;
|
||||||
use lemmy_db_schema::{newtypes::DbUrl, source::activity::SentActivity};
|
use lemmy_db_schema::{
|
||||||
use lemmy_utils::error::{LemmyError, LemmyResult};
|
newtypes::DbUrl,
|
||||||
|
source::{activity::SentActivity, community::Community},
|
||||||
|
CommunityVisibility,
|
||||||
|
};
|
||||||
|
use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
@ -102,3 +106,14 @@ pub(crate) async fn get_activity(
|
||||||
create_apub_response(&activity.data)
|
create_apub_response(&activity.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensure that the community is public and not removed/deleted.
|
||||||
|
fn check_community_public(community: &Community) -> LemmyResult<()> {
|
||||||
|
if community.deleted || community.removed {
|
||||||
|
Err(LemmyErrorType::Deleted)?
|
||||||
|
}
|
||||||
|
if community.visibility != CommunityVisibility::Public {
|
||||||
|
return Err(LemmyErrorType::CouldntFindCommunity.into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,20 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
http::{create_apub_response, create_apub_tombstone_response, redirect_remote_object},
|
http::{
|
||||||
|
check_community_public,
|
||||||
|
create_apub_response,
|
||||||
|
create_apub_tombstone_response,
|
||||||
|
redirect_remote_object,
|
||||||
|
},
|
||||||
objects::post::ApubPost,
|
objects::post::ApubPost,
|
||||||
};
|
};
|
||||||
use activitypub_federation::{config::Data, traits::Object};
|
use activitypub_federation::{config::Data, traits::Object};
|
||||||
use actix_web::{web, HttpResponse};
|
use actix_web::{web, HttpResponse};
|
||||||
use lemmy_api_common::context::LemmyContext;
|
use lemmy_api_common::context::LemmyContext;
|
||||||
use lemmy_db_schema::{newtypes::PostId, source::post::Post, traits::Crud};
|
use lemmy_db_schema::{
|
||||||
|
newtypes::PostId,
|
||||||
|
source::{community::Community, post::Post},
|
||||||
|
traits::Crud,
|
||||||
|
};
|
||||||
use lemmy_utils::error::LemmyError;
|
use lemmy_utils::error::LemmyError;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
@ -21,7 +30,11 @@ pub(crate) async fn get_apub_post(
|
||||||
context: Data<LemmyContext>,
|
context: Data<LemmyContext>,
|
||||||
) -> Result<HttpResponse, LemmyError> {
|
) -> Result<HttpResponse, LemmyError> {
|
||||||
let id = PostId(info.post_id.parse::<i32>()?);
|
let id = PostId(info.post_id.parse::<i32>()?);
|
||||||
|
// Can't use PostView here because it excludes deleted/removed/local-only items
|
||||||
let post: ApubPost = Post::read(&mut context.pool(), id).await?.into();
|
let post: ApubPost = Post::read(&mut context.pool(), id).await?.into();
|
||||||
|
let community = Community::read(&mut context.pool(), post.community_id).await?;
|
||||||
|
check_community_public(&community)?;
|
||||||
|
|
||||||
if !post.local {
|
if !post.local {
|
||||||
Ok(redirect_remote_object(&post.ap_id))
|
Ok(redirect_remote_object(&post.ap_id))
|
||||||
} else if !post.deleted && !post.removed {
|
} else if !post.deleted && !post.removed {
|
||||||
|
|
|
@ -150,15 +150,12 @@ impl Object for ApubCommunity {
|
||||||
name: group.preferred_username.clone(),
|
name: group.preferred_username.clone(),
|
||||||
title: group.name.unwrap_or(group.preferred_username.clone()),
|
title: group.name.unwrap_or(group.preferred_username.clone()),
|
||||||
description,
|
description,
|
||||||
removed: None,
|
|
||||||
published: group.published,
|
published: group.published,
|
||||||
updated: group.updated,
|
updated: group.updated,
|
||||||
deleted: Some(false),
|
deleted: Some(false),
|
||||||
nsfw: Some(group.sensitive.unwrap_or(false)),
|
nsfw: Some(group.sensitive.unwrap_or(false)),
|
||||||
actor_id: Some(group.id.into()),
|
actor_id: Some(group.id.into()),
|
||||||
local: Some(false),
|
local: Some(false),
|
||||||
private_key: None,
|
|
||||||
hidden: None,
|
|
||||||
public_key: group.public_key.public_key_pem,
|
public_key: group.public_key.public_key_pem,
|
||||||
last_refreshed_at: Some(naive_now()),
|
last_refreshed_at: Some(naive_now()),
|
||||||
icon,
|
icon,
|
||||||
|
@ -170,6 +167,7 @@ impl Object for ApubCommunity {
|
||||||
posting_restricted_to_mods: group.posting_restricted_to_mods,
|
posting_restricted_to_mods: group.posting_restricted_to_mods,
|
||||||
instance_id,
|
instance_id,
|
||||||
featured_url: group.featured.map(Into::into),
|
featured_url: group.featured.map(Into::into),
|
||||||
|
..Default::default()
|
||||||
};
|
};
|
||||||
let languages =
|
let languages =
|
||||||
LanguageTag::to_language_id_multiple(group.language, &mut context.pool()).await?;
|
LanguageTag::to_language_id_multiple(group.language, &mut context.pool()).await?;
|
||||||
|
|
|
@ -54,7 +54,7 @@ use url::Url;
|
||||||
|
|
||||||
const MAX_TITLE_LENGTH: usize = 200;
|
const MAX_TITLE_LENGTH: usize = 200;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct ApubPost(pub(crate) Post);
|
pub struct ApubPost(pub(crate) Post);
|
||||||
|
|
||||||
impl Deref for ApubPost {
|
impl Deref for ApubPost {
|
||||||
|
|
|
@ -16,7 +16,7 @@ pub mod activities;
|
||||||
pub(crate) mod collections;
|
pub(crate) mod collections;
|
||||||
pub(crate) mod objects;
|
pub(crate) mod objects;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Source {
|
pub struct Source {
|
||||||
pub(crate) content: String,
|
pub(crate) content: String,
|
||||||
|
@ -32,7 +32,7 @@ impl Source {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ImageObject {
|
pub struct ImageObject {
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
|
|
|
@ -35,7 +35,7 @@ use std::fmt::Debug;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[skip_serializing_none]
|
#[skip_serializing_none]
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Group {
|
pub struct Group {
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
|
|
|
@ -16,14 +16,14 @@ pub(crate) mod page;
|
||||||
pub(crate) mod person;
|
pub(crate) mod person;
|
||||||
pub(crate) mod tombstone;
|
pub(crate) mod tombstone;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Endpoints {
|
pub struct Endpoints {
|
||||||
pub shared_inbox: Url,
|
pub shared_inbox: Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// As specified in https://schema.org/Language
|
/// As specified in https://schema.org/Language
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct LanguageTag {
|
pub(crate) struct LanguageTag {
|
||||||
pub(crate) identifier: String,
|
pub(crate) identifier: String,
|
||||||
|
|
|
@ -365,6 +365,7 @@ mod tests {
|
||||||
},
|
},
|
||||||
traits::{Bannable, Crud, Followable, Joinable},
|
traits::{Bannable, Crud, Followable, Joinable},
|
||||||
utils::build_db_pool_for_tests,
|
utils::build_db_pool_for_tests,
|
||||||
|
CommunityVisibility,
|
||||||
};
|
};
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
|
@ -421,6 +422,7 @@ mod tests {
|
||||||
hidden: false,
|
hidden: false,
|
||||||
posting_restricted_to_mods: false,
|
posting_restricted_to_mods: false,
|
||||||
instance_id: inserted_instance.id,
|
instance_id: inserted_instance.id,
|
||||||
|
visibility: CommunityVisibility::Public,
|
||||||
};
|
};
|
||||||
|
|
||||||
let community_follower_form = CommunityFollowerForm {
|
let community_follower_form = CommunityFollowerForm {
|
||||||
|
|
|
@ -217,6 +217,27 @@ pub enum PostFeatureType {
|
||||||
Community,
|
Community,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default,
|
||||||
|
)]
|
||||||
|
#[cfg_attr(feature = "full", derive(DbEnum, TS))]
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "full",
|
||||||
|
ExistingTypePath = "crate::schema::sql_types::CommunityVisibility"
|
||||||
|
)]
|
||||||
|
#[cfg_attr(feature = "full", DbValueStyle = "verbatim")]
|
||||||
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
|
/// Defines who can browse and interact with content in a community.
|
||||||
|
///
|
||||||
|
/// TODO: Also use this to define private communities
|
||||||
|
pub enum CommunityVisibility {
|
||||||
|
/// Public community, any local or federated user can interact.
|
||||||
|
#[default]
|
||||||
|
Public,
|
||||||
|
/// Unfederated community, only local users can interact.
|
||||||
|
LocalOnly,
|
||||||
|
}
|
||||||
|
|
||||||
/// 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]
|
||||||
|
|
|
@ -5,6 +5,10 @@ pub mod sql_types {
|
||||||
#[diesel(postgres_type(name = "actor_type_enum"))]
|
#[diesel(postgres_type(name = "actor_type_enum"))]
|
||||||
pub struct ActorTypeEnum;
|
pub struct ActorTypeEnum;
|
||||||
|
|
||||||
|
#[derive(diesel::sql_types::SqlType)]
|
||||||
|
#[diesel(postgres_type(name = "community_visibility"))]
|
||||||
|
pub struct CommunityVisibility;
|
||||||
|
|
||||||
#[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;
|
||||||
|
@ -150,6 +154,9 @@ diesel::table! {
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use super::sql_types::CommunityVisibility;
|
||||||
|
|
||||||
community (id) {
|
community (id) {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
#[max_length = 255]
|
#[max_length = 255]
|
||||||
|
@ -183,6 +190,7 @@ diesel::table! {
|
||||||
moderators_url -> Nullable<Varchar>,
|
moderators_url -> Nullable<Varchar>,
|
||||||
#[max_length = 255]
|
#[max_length = 255]
|
||||||
featured_url -> Nullable<Varchar>,
|
featured_url -> Nullable<Varchar>,
|
||||||
|
visibility -> CommunityVisibility,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ use crate::schema::{community, community_follower, community_moderator, communit
|
||||||
use crate::{
|
use crate::{
|
||||||
newtypes::{CommunityId, DbUrl, InstanceId, PersonId},
|
newtypes::{CommunityId, DbUrl, InstanceId, PersonId},
|
||||||
source::placeholder_apub_url,
|
source::placeholder_apub_url,
|
||||||
|
CommunityVisibility,
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -66,9 +67,10 @@ pub struct Community {
|
||||||
/// Url where featured posts collection is served over Activitypub
|
/// Url where featured posts collection is served over Activitypub
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub featured_url: Option<DbUrl>,
|
pub featured_url: Option<DbUrl>,
|
||||||
|
pub visibility: CommunityVisibility,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, TypedBuilder)]
|
#[derive(Debug, Clone, TypedBuilder, Default)]
|
||||||
#[builder(field_defaults(default))]
|
#[builder(field_defaults(default))]
|
||||||
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
|
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
|
||||||
#[cfg_attr(feature = "full", diesel(table_name = community))]
|
#[cfg_attr(feature = "full", diesel(table_name = community))]
|
||||||
|
@ -99,6 +101,7 @@ pub struct CommunityInsertForm {
|
||||||
pub posting_restricted_to_mods: Option<bool>,
|
pub posting_restricted_to_mods: Option<bool>,
|
||||||
#[builder(!default)]
|
#[builder(!default)]
|
||||||
pub instance_id: InstanceId,
|
pub instance_id: InstanceId,
|
||||||
|
pub visibility: Option<CommunityVisibility>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
|
@ -126,6 +129,7 @@ pub struct CommunityUpdateForm {
|
||||||
pub featured_url: Option<DbUrl>,
|
pub featured_url: Option<DbUrl>,
|
||||||
pub hidden: Option<bool>,
|
pub hidden: Option<bool>,
|
||||||
pub posting_restricted_to_mods: Option<bool>,
|
pub posting_restricted_to_mods: Option<bool>,
|
||||||
|
pub visibility: Option<CommunityVisibility>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug)]
|
#[derive(PartialEq, Eq, Debug)]
|
||||||
|
|
|
@ -233,6 +233,7 @@ mod tests {
|
||||||
},
|
},
|
||||||
traits::{Crud, Joinable, Reportable},
|
traits::{Crud, Joinable, Reportable},
|
||||||
utils::{build_db_pool_for_tests, RANK_DEFAULT},
|
utils::{build_db_pool_for_tests, RANK_DEFAULT},
|
||||||
|
CommunityVisibility,
|
||||||
};
|
};
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
|
@ -379,6 +380,7 @@ mod tests {
|
||||||
moderators_url: inserted_community.moderators_url,
|
moderators_url: inserted_community.moderators_url,
|
||||||
featured_url: inserted_community.featured_url,
|
featured_url: inserted_community.featured_url,
|
||||||
instance_id: inserted_instance.id,
|
instance_id: inserted_instance.id,
|
||||||
|
visibility: CommunityVisibility::Public,
|
||||||
},
|
},
|
||||||
creator: Person {
|
creator: Person {
|
||||||
id: inserted_jessica.id,
|
id: inserted_jessica.id,
|
||||||
|
|
|
@ -36,6 +36,7 @@ use lemmy_db_schema::{
|
||||||
},
|
},
|
||||||
utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
|
utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
|
||||||
CommentSortType,
|
CommentSortType,
|
||||||
|
CommunityVisibility,
|
||||||
ListingType,
|
ListingType,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -167,13 +168,16 @@ fn queries<'a>() -> Queries<
|
||||||
|
|
||||||
let read = move |mut conn: DbConn<'a>,
|
let read = move |mut conn: DbConn<'a>,
|
||||||
(comment_id, my_person_id): (CommentId, Option<PersonId>)| async move {
|
(comment_id, my_person_id): (CommentId, Option<PersonId>)| async move {
|
||||||
all_joins(
|
let mut query = all_joins(
|
||||||
comment::table.find(comment_id).into_boxed(),
|
comment::table.find(comment_id).into_boxed(),
|
||||||
my_person_id,
|
my_person_id,
|
||||||
false,
|
false,
|
||||||
)
|
);
|
||||||
.first::<CommentView>(&mut conn)
|
// Hide local only communities from unauthenticated users
|
||||||
.await
|
if my_person_id.is_none() {
|
||||||
|
query = query.filter(community::visibility.eq(CommunityVisibility::Public));
|
||||||
|
}
|
||||||
|
query.first::<CommentView>(&mut conn).await
|
||||||
};
|
};
|
||||||
|
|
||||||
let list = move |mut conn: DbConn<'a>, options: CommentQuery<'a>| async move {
|
let list = move |mut conn: DbConn<'a>, options: CommentQuery<'a>| async move {
|
||||||
|
@ -287,6 +291,11 @@ fn queries<'a>() -> Queries<
|
||||||
query = query.filter(not(is_creator_blocked(person_id_join)));
|
query = query.filter(not(is_creator_blocked(person_id_join)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Hide comments in local only communities from unauthenticated users
|
||||||
|
if options.local_user.is_none() {
|
||||||
|
query = query.filter(community::visibility.eq(CommunityVisibility::Public));
|
||||||
|
}
|
||||||
|
|
||||||
// A Max depth given means its a tree fetch
|
// A Max depth given means its a tree fetch
|
||||||
let (limit, offset) = if let Some(max_depth) = options.max_depth {
|
let (limit, offset) = if let Some(max_depth) = options.max_depth {
|
||||||
let depth_limit = if let Some(parent_path) = options.parent_path.as_ref() {
|
let depth_limit = if let Some(parent_path) = options.parent_path.as_ref() {
|
||||||
|
@ -405,7 +414,13 @@ mod tests {
|
||||||
source::{
|
source::{
|
||||||
actor_language::LocalUserLanguage,
|
actor_language::LocalUserLanguage,
|
||||||
comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm, CommentUpdateForm},
|
comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm, CommentUpdateForm},
|
||||||
community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm},
|
community::{
|
||||||
|
Community,
|
||||||
|
CommunityInsertForm,
|
||||||
|
CommunityModerator,
|
||||||
|
CommunityModeratorForm,
|
||||||
|
CommunityUpdateForm,
|
||||||
|
},
|
||||||
instance::Instance,
|
instance::Instance,
|
||||||
language::Language,
|
language::Language,
|
||||||
local_user::{LocalUser, LocalUserInsertForm},
|
local_user::{LocalUser, LocalUserInsertForm},
|
||||||
|
@ -415,6 +430,7 @@ mod tests {
|
||||||
},
|
},
|
||||||
traits::{Blockable, Crud, Joinable, Likeable},
|
traits::{Blockable, Crud, Joinable, Likeable},
|
||||||
utils::{build_db_pool_for_tests, RANK_DEFAULT},
|
utils::{build_db_pool_for_tests, RANK_DEFAULT},
|
||||||
|
CommunityVisibility,
|
||||||
SubscribedType,
|
SubscribedType,
|
||||||
};
|
};
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
@ -1042,6 +1058,7 @@ mod tests {
|
||||||
shared_inbox_url: data.inserted_community.shared_inbox_url.clone(),
|
shared_inbox_url: data.inserted_community.shared_inbox_url.clone(),
|
||||||
moderators_url: data.inserted_community.moderators_url.clone(),
|
moderators_url: data.inserted_community.moderators_url.clone(),
|
||||||
featured_url: data.inserted_community.featured_url.clone(),
|
featured_url: data.inserted_community.featured_url.clone(),
|
||||||
|
visibility: CommunityVisibility::Public,
|
||||||
},
|
},
|
||||||
counts: CommentAggregates {
|
counts: CommentAggregates {
|
||||||
comment_id: data.inserted_comment_0.id,
|
comment_id: data.inserted_comment_0.id,
|
||||||
|
@ -1055,4 +1072,53 @@ mod tests {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn local_only_instance() {
|
||||||
|
let pool = &build_db_pool_for_tests().await;
|
||||||
|
let pool = &mut pool.into();
|
||||||
|
let data = init_data(pool).await;
|
||||||
|
|
||||||
|
Community::update(
|
||||||
|
pool,
|
||||||
|
data.inserted_community.id,
|
||||||
|
&CommunityUpdateForm {
|
||||||
|
visibility: Some(CommunityVisibility::LocalOnly),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let unauthenticated_query = CommentQuery {
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.list(pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(0, unauthenticated_query.len());
|
||||||
|
|
||||||
|
let authenticated_query = CommentQuery {
|
||||||
|
local_user: Some(&data.timmy_local_user_view),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.list(pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(5, authenticated_query.len());
|
||||||
|
|
||||||
|
let unauthenticated_comment = CommentView::read(pool, data.inserted_comment_0.id, None).await;
|
||||||
|
assert!(unauthenticated_comment.is_err());
|
||||||
|
|
||||||
|
let authenticated_comment = CommentView::read(
|
||||||
|
pool,
|
||||||
|
data.inserted_comment_0.id,
|
||||||
|
Some(data.timmy_local_user_view.person.id),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(authenticated_comment.is_ok());
|
||||||
|
|
||||||
|
cleanup(data, pool).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@ use lemmy_db_schema::{
|
||||||
ReadFn,
|
ReadFn,
|
||||||
ReverseTimestampKey,
|
ReverseTimestampKey,
|
||||||
},
|
},
|
||||||
|
CommunityVisibility,
|
||||||
ListingType,
|
ListingType,
|
||||||
SortType,
|
SortType,
|
||||||
};
|
};
|
||||||
|
@ -258,6 +259,11 @@ fn queries<'a>() -> Queries<
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide posts in local only communities from unauthenticated users
|
||||||
|
if my_person_id.is_none() {
|
||||||
|
query = query.filter(community::visibility.eq(CommunityVisibility::Public));
|
||||||
|
}
|
||||||
|
|
||||||
Commented::new(query)
|
Commented::new(query)
|
||||||
.text("PostView::read")
|
.text("PostView::read")
|
||||||
.first::<PostView>(&mut conn)
|
.first::<PostView>(&mut conn)
|
||||||
|
@ -405,6 +411,11 @@ fn queries<'a>() -> Queries<
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Hide posts in local only communities from unauthenticated users
|
||||||
|
if options.local_user.is_none() {
|
||||||
|
query = query.filter(community::visibility.eq(CommunityVisibility::Public));
|
||||||
|
}
|
||||||
|
|
||||||
// Dont filter blocks or missing languages for moderator view type
|
// Dont filter blocks or missing languages for moderator view type
|
||||||
if let (Some(person_id), false) = (
|
if let (Some(person_id), false) = (
|
||||||
my_person_id,
|
my_person_id,
|
||||||
|
@ -691,7 +702,13 @@ mod tests {
|
||||||
source::{
|
source::{
|
||||||
actor_language::LocalUserLanguage,
|
actor_language::LocalUserLanguage,
|
||||||
comment::{Comment, CommentInsertForm},
|
comment::{Comment, CommentInsertForm},
|
||||||
community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm},
|
community::{
|
||||||
|
Community,
|
||||||
|
CommunityInsertForm,
|
||||||
|
CommunityModerator,
|
||||||
|
CommunityModeratorForm,
|
||||||
|
CommunityUpdateForm,
|
||||||
|
},
|
||||||
community_block::{CommunityBlock, CommunityBlockForm},
|
community_block::{CommunityBlock, CommunityBlockForm},
|
||||||
instance::Instance,
|
instance::Instance,
|
||||||
instance_block::{InstanceBlock, InstanceBlockForm},
|
instance_block::{InstanceBlock, InstanceBlockForm},
|
||||||
|
@ -702,7 +719,8 @@ mod tests {
|
||||||
post::{Post, PostInsertForm, PostLike, PostLikeForm, PostRead, PostUpdateForm},
|
post::{Post, PostInsertForm, PostLike, PostLikeForm, PostRead, PostUpdateForm},
|
||||||
},
|
},
|
||||||
traits::{Blockable, Crud, Joinable, Likeable},
|
traits::{Blockable, Crud, Joinable, Likeable},
|
||||||
utils::{build_db_pool, DbPool, RANK_DEFAULT},
|
utils::{build_db_pool, build_db_pool_for_tests, DbPool, RANK_DEFAULT},
|
||||||
|
CommunityVisibility,
|
||||||
SortType,
|
SortType,
|
||||||
SubscribedType,
|
SubscribedType,
|
||||||
};
|
};
|
||||||
|
@ -1523,6 +1541,7 @@ mod tests {
|
||||||
shared_inbox_url: inserted_community.shared_inbox_url.clone(),
|
shared_inbox_url: inserted_community.shared_inbox_url.clone(),
|
||||||
moderators_url: inserted_community.moderators_url.clone(),
|
moderators_url: inserted_community.moderators_url.clone(),
|
||||||
featured_url: inserted_community.featured_url.clone(),
|
featured_url: inserted_community.featured_url.clone(),
|
||||||
|
visibility: CommunityVisibility::Public,
|
||||||
},
|
},
|
||||||
counts: PostAggregates {
|
counts: PostAggregates {
|
||||||
post_id: inserted_post.id,
|
post_id: inserted_post.id,
|
||||||
|
@ -1549,4 +1568,52 @@ mod tests {
|
||||||
creator_blocked: false,
|
creator_blocked: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn local_only_instance() -> LemmyResult<()> {
|
||||||
|
let pool = &build_db_pool_for_tests().await;
|
||||||
|
let pool = &mut pool.into();
|
||||||
|
let data = init_data(pool).await?;
|
||||||
|
|
||||||
|
Community::update(
|
||||||
|
pool,
|
||||||
|
data.inserted_community.id,
|
||||||
|
&CommunityUpdateForm {
|
||||||
|
visibility: Some(CommunityVisibility::LocalOnly),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let unauthenticated_query = PostQuery {
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.list(pool)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(0, unauthenticated_query.len());
|
||||||
|
|
||||||
|
let authenticated_query = PostQuery {
|
||||||
|
local_user: Some(&data.local_user_view),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.list(pool)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(2, authenticated_query.len());
|
||||||
|
|
||||||
|
let unauthenticated_post = PostView::read(pool, data.inserted_post.id, None, false).await;
|
||||||
|
assert!(unauthenticated_post.is_err());
|
||||||
|
|
||||||
|
let authenticated_post = PostView::read(
|
||||||
|
pool,
|
||||||
|
data.inserted_post.id,
|
||||||
|
Some(data.local_user_view.person.id),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(authenticated_post.is_ok());
|
||||||
|
|
||||||
|
cleanup(data, pool).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ use lemmy_db_schema::{
|
||||||
newtypes::{CommunityId, PersonId},
|
newtypes::{CommunityId, PersonId},
|
||||||
schema::{community, community_moderator, person},
|
schema::{community, community_moderator, person},
|
||||||
utils::{get_conn, DbPool},
|
utils::{get_conn, DbPool},
|
||||||
|
CommunityVisibility,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl CommunityModeratorView {
|
impl CommunityModeratorView {
|
||||||
|
@ -56,17 +57,24 @@ impl CommunityModeratorView {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn for_person(pool: &mut DbPool<'_>, person_id: PersonId) -> Result<Vec<Self>, Error> {
|
pub async fn for_person(
|
||||||
|
pool: &mut DbPool<'_>,
|
||||||
|
person_id: PersonId,
|
||||||
|
is_authenticated: bool,
|
||||||
|
) -> Result<Vec<Self>, Error> {
|
||||||
let conn = &mut get_conn(pool).await?;
|
let conn = &mut get_conn(pool).await?;
|
||||||
community_moderator::table
|
let mut query = community_moderator::table
|
||||||
.inner_join(community::table)
|
.inner_join(community::table)
|
||||||
.inner_join(person::table)
|
.inner_join(person::table)
|
||||||
.filter(community_moderator::person_id.eq(person_id))
|
.filter(community_moderator::person_id.eq(person_id))
|
||||||
.filter(community::deleted.eq(false))
|
.filter(community::deleted.eq(false))
|
||||||
.filter(community::removed.eq(false))
|
.filter(community::removed.eq(false))
|
||||||
.select((community::all_columns, person::all_columns))
|
.select((community::all_columns, person::all_columns))
|
||||||
.load::<CommunityModeratorView>(conn)
|
.into_boxed();
|
||||||
.await
|
if !is_authenticated {
|
||||||
|
query = query.filter(community::visibility.eq(CommunityVisibility::Public));
|
||||||
|
}
|
||||||
|
query.load::<CommunityModeratorView>(conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds all communities first mods / creators
|
/// Finds all communities first mods / creators
|
||||||
|
|
|
@ -22,6 +22,7 @@ use lemmy_db_schema::{
|
||||||
},
|
},
|
||||||
source::{community::CommunityFollower, local_user::LocalUser},
|
source::{community::CommunityFollower, local_user::LocalUser},
|
||||||
utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
|
utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
|
||||||
|
CommunityVisibility,
|
||||||
ListingType,
|
ListingType,
|
||||||
SortType,
|
SortType,
|
||||||
};
|
};
|
||||||
|
@ -87,6 +88,11 @@ fn queries<'a>() -> Queries<
|
||||||
query = query.filter(not_removed_or_deleted);
|
query = query.filter(not_removed_or_deleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide local only communities from unauthenticated users
|
||||||
|
if my_person_id.is_none() {
|
||||||
|
query = query.filter(community::visibility.eq(CommunityVisibility::Public));
|
||||||
|
}
|
||||||
|
|
||||||
query.first::<CommunityView>(&mut conn).await
|
query.first::<CommunityView>(&mut conn).await
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -156,6 +162,8 @@ fn queries<'a>() -> Queries<
|
||||||
if !options.show_nsfw {
|
if !options.show_nsfw {
|
||||||
query = query.filter(community::nsfw.eq(false));
|
query = query.filter(community::nsfw.eq(false));
|
||||||
}
|
}
|
||||||
|
// Hide local only communities from unauthenticated users
|
||||||
|
query = query.filter(community::visibility.eq(CommunityVisibility::Public));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (limit, offset) = limit_and_offset(options.page, options.limit)?;
|
let (limit, offset) = limit_and_offset(options.page, options.limit)?;
|
||||||
|
@ -229,3 +237,129 @@ impl<'a> CommunityQuery<'a> {
|
||||||
queries().list(pool, self).await
|
queries().list(pool, self).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
#![allow(clippy::indexing_slicing)]
|
||||||
|
|
||||||
|
use crate::{community_view::CommunityQuery, structs::CommunityView};
|
||||||
|
use lemmy_db_schema::{
|
||||||
|
source::{
|
||||||
|
community::{Community, CommunityInsertForm, CommunityUpdateForm},
|
||||||
|
instance::Instance,
|
||||||
|
local_user::{LocalUser, LocalUserInsertForm},
|
||||||
|
person::{Person, PersonInsertForm},
|
||||||
|
},
|
||||||
|
traits::Crud,
|
||||||
|
utils::{build_db_pool_for_tests, DbPool},
|
||||||
|
CommunityVisibility,
|
||||||
|
};
|
||||||
|
use serial_test::serial;
|
||||||
|
|
||||||
|
struct Data {
|
||||||
|
inserted_instance: Instance,
|
||||||
|
local_user: LocalUser,
|
||||||
|
inserted_community: Community,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init_data(pool: &mut DbPool<'_>) -> Data {
|
||||||
|
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let person_name = "tegan".to_string();
|
||||||
|
|
||||||
|
let new_person = PersonInsertForm::builder()
|
||||||
|
.name(person_name.clone())
|
||||||
|
.public_key("pubkey".to_string())
|
||||||
|
.instance_id(inserted_instance.id)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let inserted_person = Person::create(pool, &new_person).await.unwrap();
|
||||||
|
|
||||||
|
let local_user_form = LocalUserInsertForm::builder()
|
||||||
|
.person_id(inserted_person.id)
|
||||||
|
.password_encrypted(String::new())
|
||||||
|
.build();
|
||||||
|
let local_user = LocalUser::create(pool, &local_user_form).await.unwrap();
|
||||||
|
|
||||||
|
let new_community = CommunityInsertForm::builder()
|
||||||
|
.name("test_community_3".to_string())
|
||||||
|
.title("nada".to_owned())
|
||||||
|
.public_key("pubkey".to_string())
|
||||||
|
.instance_id(inserted_instance.id)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let inserted_community = Community::create(pool, &new_community).await.unwrap();
|
||||||
|
|
||||||
|
Data {
|
||||||
|
inserted_instance,
|
||||||
|
local_user,
|
||||||
|
inserted_community,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cleanup(data: Data, pool: &mut DbPool<'_>) {
|
||||||
|
Community::delete(pool, data.inserted_community.id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
Person::delete(pool, data.local_user.person_id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
Instance::delete(pool, data.inserted_instance.id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn local_only_community() {
|
||||||
|
let pool = &build_db_pool_for_tests().await;
|
||||||
|
let pool = &mut pool.into();
|
||||||
|
let data = init_data(pool).await;
|
||||||
|
|
||||||
|
Community::update(
|
||||||
|
pool,
|
||||||
|
data.inserted_community.id,
|
||||||
|
&CommunityUpdateForm {
|
||||||
|
visibility: Some(CommunityVisibility::LocalOnly),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let unauthenticated_query = CommunityQuery {
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.list(pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(0, unauthenticated_query.len());
|
||||||
|
|
||||||
|
let authenticated_query = CommunityQuery {
|
||||||
|
local_user: Some(&data.local_user),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.list(pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(1, authenticated_query.len());
|
||||||
|
|
||||||
|
let unauthenticated_community =
|
||||||
|
CommunityView::read(pool, data.inserted_community.id, None, false).await;
|
||||||
|
assert!(unauthenticated_community.is_err());
|
||||||
|
|
||||||
|
let authenticated_community = CommunityView::read(
|
||||||
|
pool,
|
||||||
|
data.inserted_community.id,
|
||||||
|
Some(data.local_user.person_id),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(authenticated_community.is_ok());
|
||||||
|
|
||||||
|
cleanup(data, pool).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ use lemmy_db_schema::{
|
||||||
source::{community::Community, person::Person},
|
source::{community::Community, person::Person},
|
||||||
traits::ApubActor,
|
traits::ApubActor,
|
||||||
CommentSortType,
|
CommentSortType,
|
||||||
|
CommunityVisibility,
|
||||||
ListingType,
|
ListingType,
|
||||||
SortType,
|
SortType,
|
||||||
};
|
};
|
||||||
|
@ -21,7 +22,7 @@ use lemmy_db_views_actor::{
|
||||||
};
|
};
|
||||||
use lemmy_utils::{
|
use lemmy_utils::{
|
||||||
cache_header::cache_1hour,
|
cache_header::cache_1hour,
|
||||||
error::LemmyError,
|
error::{LemmyError, LemmyErrorType},
|
||||||
utils::markdown::{markdown_to_html, sanitize_html},
|
utils::markdown::{markdown_to_html, sanitize_html},
|
||||||
};
|
};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
@ -268,6 +269,9 @@ async fn get_feed_community(
|
||||||
) -> Result<Channel, LemmyError> {
|
) -> Result<Channel, LemmyError> {
|
||||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||||
let community = Community::read_from_name(&mut context.pool(), community_name, false).await?;
|
let community = Community::read_from_name(&mut context.pool(), community_name, false).await?;
|
||||||
|
if community.visibility != CommunityVisibility::Public {
|
||||||
|
return Err(LemmyErrorType::CouldntFindCommunity.into());
|
||||||
|
}
|
||||||
|
|
||||||
check_private_instance(&None, &site_view.local_site)?;
|
check_private_instance(&None, &site_view.local_site)?;
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ use lemmy_api_common::context::LemmyContext;
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
source::{community::Community, person::Person},
|
source::{community::Community, person::Person},
|
||||||
traits::ApubActor,
|
traits::ApubActor,
|
||||||
|
CommunityVisibility,
|
||||||
};
|
};
|
||||||
use lemmy_utils::{cache_header::cache_3days, error::LemmyError};
|
use lemmy_utils::{cache_header::cache_3days, error::LemmyError};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
@ -44,7 +45,14 @@ async fn get_webfinger_response(
|
||||||
let community_id: Option<Url> = Community::read_from_name(&mut context.pool(), name, false)
|
let community_id: Option<Url> = Community::read_from_name(&mut context.pool(), name, false)
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
.map(|c| c.actor_id.into());
|
.and_then(|c| {
|
||||||
|
if c.visibility == CommunityVisibility::Public {
|
||||||
|
let id: Url = c.actor_id.into();
|
||||||
|
Some(id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Mastodon seems to prioritize the last webfinger item in case of duplicates. Put
|
// Mastodon seems to prioritize the last webfinger item in case of duplicates. Put
|
||||||
// community last so that it gets prioritized. For Lemmy the order doesnt matter.
|
// community last so that it gets prioritized. For Lemmy the order doesnt matter.
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
ALTER TABLE community
|
||||||
|
DROP COLUMN visibility;
|
||||||
|
|
||||||
|
DROP TYPE community_visibility;
|
||||||
|
|
8
migrations/2024-01-15-100133_local-only-community/up.sql
Normal file
8
migrations/2024-01-15-100133_local-only-community/up.sql
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
CREATE TYPE community_visibility AS enum (
|
||||||
|
'Public',
|
||||||
|
'LocalOnly'
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE community
|
||||||
|
ADD COLUMN visibility community_visibility NOT NULL DEFAULT 'Public';
|
||||||
|
|
Loading…
Reference in a new issue