Implement multi-community (fixes #818, fixes #5340) (#5601)

* Implement multi-community (fixes #818, fixes #5340)

* db methods

* add api methods

* ts opt

* wip

* sql queries

* cleanup

* wip: federation

* query by name

* add ap_id column

* add read_apub, compiles now

* validate multi-comm name

* disallow removed, deleted, private

* scheduled task

* remove piefed test

* resolve_object with workaround

* review

* avoid db read

* api client

* fix api test fetch

* wip: test cases

* wip

* add max elements, array_remove comments

* simplify post view query

* mvoe structs

* fix api test

* fix api test

* rename to suggested_communities, add api param

* filter removed/deleted

* check_api_elements_count

* filter out removed/deleted during update

* inner join

* add listing type suggested

* db schema changes

* transaction

* address some review comments

* separate methods for create, delete entry

* update js client

* get multi

* remove CommunityOrMulti

* check helper, other stuff

* fix api test

* change get multi comm return type

* Replace MultiCommunityView with GetMultiCommunityResponse

* get rid of todo

* add local column, admins can edit local multi-comm

* implement multi-comm follow (db and api)

* api for multi-comm follow

* move and rename MultiCommunityApub

* move multi-comm to apub-objects

* move multi-comm url to top-level

* list multi-comms followed by user

* add todo

* remove param

* update local follows

* update query

* db functions and tests

* cleanup

* fix api test

* add entry limit

* rewrite links

* federation changes

* wip federation

* simplify generate_activity_id

* more wip

* more wip

* multi-comm follow

* federate changes

* cleanup

* clippy

* test fixes

* fmt

* fix

* wip: update follows after federated multi-comm change

* remove scheduled task

* update follows after multi update

* fmt

* fixes

* review comments

* indexes

* remove MultiCommunityApub

* fix test

* ts fix

* review

* db schema for local_site.multi_comm_follower

* adjust code and tests

* fixes

* cleanup, comment

* fix tests

* fix

* remove more test code

* fix new install

* add index

* fix api tests

* fix

* remove index

* more fix

* Implement multi-community search (fixes #5778) (#5779)

* Implement multi-community search (fixes #5778)

* fixes

* search title and description

* MultiCommunityView

* rename fields

* revert test change
This commit is contained in:
Nutomic 2025-06-17 09:04:47 +00:00 committed by GitHub
parent c198c98bed
commit bc23e95f50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
116 changed files with 2602 additions and 687 deletions

5
Cargo.lock generated
View file

@ -1763,6 +1763,9 @@ name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
dependencies = [
"serde",
]
[[package]]
name = "elementtree"
@ -3847,6 +3850,7 @@ dependencies = [
"chrono",
"diesel",
"diesel-async",
"either",
"futures",
"lemmy_api_utils",
"lemmy_apub_objects",
@ -3900,6 +3904,7 @@ dependencies = [
"percent-encoding",
"pretty_assertions",
"prometheus",
"rand 0.9.1",
"reqwest 0.12.19",
"reqwest-middleware",
"rss",

View file

@ -216,7 +216,7 @@ derive-new = "0.7.0"
tuplex = "0.1.2"
html2text = "0.15.1"
async-trait = "0.1.88"
either = "1.15.0"
either = { version = "1.15.0", features = ["serde"] }
extism = { git = "https://github.com/extism/extism.git", branch = "main" }
extism-convert = { git = "https://github.com/extism/extism.git", branch = "main" }

View file

@ -10,13 +10,13 @@
"scripts": {
"lint": "tsc --noEmit && eslint --report-unused-disable-directives && prettier --check 'src/**/*.ts'",
"fix": "prettier --write src && eslint --fix src",
"api-test": "jest -i follow.spec.ts && jest -i image.spec.ts && jest -i user.spec.ts && jest -i private_message.spec.ts && jest -i community.spec.ts && jest -i private_community.spec.ts && jest -i post.spec.ts && jest -i comment.spec.ts && jest -i tags.spec.ts",
"api-test": "jest -i follow.spec.ts && jest -i image.spec.ts && jest -i user.spec.ts && jest -i private_message.spec.ts && jest -i community.spec.ts && jest -i private_comm.spec.ts && jest -i post.spec.ts && jest -i comment.spec.ts && jest -i tags.spec.ts",
"api-test-follow": "jest -i follow.spec.ts",
"api-test-comment": "jest -i comment.spec.ts",
"api-test-post": "jest -i post.spec.ts",
"api-test-user": "jest -i user.spec.ts",
"api-test-community": "jest -i community.spec.ts",
"api-test-private-community": "jest -i private_community.spec.ts",
"api-test-private-community": "jest -i private_comm.spec.ts",
"api-test-private-message": "jest -i private_message.spec.ts",
"api-test-image": "jest -i image.spec.ts",
"api-test-tags": "jest -i tags.spec.ts"
@ -31,7 +31,7 @@
"eslint-plugin-prettier": "^5.4.0",
"jest": "^29.5.0",
"joi": "^17.13.3",
"lemmy-js-client": "1.0.0-search-query-mandatory.1",
"lemmy-js-client": "1.0.0-multi-community.20",
"prettier": "^3.5.3",
"ts-jest": "^29.3.2",
"tsoa": "^6.6.0",

View file

@ -36,8 +36,8 @@ importers:
specifier: ^17.13.3
version: 17.13.3
lemmy-js-client:
specifier: 1.0.0-search-query-mandatory.1
version: 1.0.0-search-query-mandatory.1
specifier: 1.0.0-multi-community.20
version: 1.0.0-multi-community.20
prettier:
specifier: ^3.5.3
version: 3.5.3
@ -1594,8 +1594,8 @@ packages:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
lemmy-js-client@1.0.0-search-query-mandatory.1:
resolution: {integrity: sha512-Km4w/7BjL/aMnVVADQ6eqdGAYPzaXzOaCbeideWJ4XbNR3ESzXAuMyjK9ynw9Q65NWzXyAFs5VoaTALFVjU3aA==}
lemmy-js-client@1.0.0-multi-community.20:
resolution: {integrity: sha512-CQdQMlzn5SMLhJx1RI6KEn+RGYKiQf3burfXLCGkCzUTkaT3oDFieDi/Az6EI1JFHhi5nSys7Ch7zZMUEBJF3w==}
leven@3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
@ -4404,7 +4404,7 @@ snapshots:
kleur@3.0.3: {}
lemmy-js-client@1.0.0-search-query-mandatory.1:
lemmy-js-client@1.0.0-multi-community.20:
dependencies:
'@tsoa/runtime': 6.6.0
transitivePeerDependencies:

View file

@ -32,6 +32,7 @@ import {
unfollows,
getMyUser,
userBlockInstance,
resolveBetaCommunity,
reportCommunity,
randomString,
listReports,
@ -41,8 +42,11 @@ import {
CommunityReport,
CommunityReportView,
EditCommunity,
FollowMultiCommunity,
GetPosts,
LemmyError,
MultiCommunity,
MultiCommunityView,
ReportCombinedView,
ResolveCommunityReport,
Search,
@ -684,9 +688,86 @@ test("Community name with non-ascii chars", async () => {
community_name: fediName,
};
let posts = await beta.getPosts(form);
expect(posts.posts.length).toBe(1);
expect(posts.posts[0].post.name).toBe(postRes.post_view.post.name);
});
test("Multi-community", async () => {
// create multi
let res = await alpha.createMultiCommunity({ name: "multi-comm" });
let myUser = await getMyUser(alpha);
expect(res.multi_community_view.multi.name).toBe("multi-comm");
expect(res.multi_community_view.multi.ap_id).toBe(
"http://lemmy-alpha:8541/m/multi-comm",
);
expect(res.multi_community_view.owner.id).toBe(
myUser.local_user_view.person.id,
);
// add initial community
let community1 = (await createCommunity(alpha)).community_view.community;
let success1 = await alpha.createMultiCommunityEntry({
id: res.multi_community_view.multi.id,
community_id: community1.id,
});
expect(success1.success).toBeTruthy();
// resolve over federation
let betaMulti = (
await beta.resolveObject({ q: res.multi_community_view.multi.ap_id })
).results[0] as MultiCommunityView;
expect(betaMulti.multi.ap_id).toBe(res.multi_community_view.multi.ap_id);
var betaRes = await waitUntil(
() => beta.getMultiCommunity({ id: betaMulti.multi.id }),
m => m.communities.length == 1,
);
expect(betaRes.communities[0].community.ap_id).toBe(community1.ap_id);
// follow multi over federation
let form: FollowMultiCommunity = {
multi_community_id: betaMulti.multi.id,
follow: true,
};
await beta.followMultiCommunity(form);
let followed = await waitUntil(
() => beta.listMultiCommunities({ followed_only: true }),
m => m.multi_communities.length == 1,
);
expect(followed.multi_communities[0].multi.ap_id).toBe(betaMulti.multi.ap_id);
await delay();
// add community to multi
let community2 = await resolveBetaCommunity(alpha);
let success2 = await alpha.createMultiCommunityEntry({
id: res.multi_community_view.multi.id,
community_id: community2!.community.id,
});
expect(success2.success).toBeTruthy();
// federated to beta
betaRes = await waitUntil(
() => beta.getMultiCommunity({ id: betaMulti.multi.id }),
m => m.communities.length == 2,
);
let ap_ids = betaRes.communities.map(c => c.community.ap_id);
expect(ap_ids.includes(community2!.community.ap_id)).toBeTruthy();
let post = await createPost(alpha, community2!.community.id);
let multi_post_listing = await waitUntil(
() =>
beta.getPosts({
multi_community_id: betaRes.multi_community_view.multi.id,
}),
p => p.posts.length == 1,
);
expect(multi_post_listing.posts[0].post.ap_id).toBe(
post.post_view.post.ap_id,
);
});
function checkCommunityReportName(
rcv: ReportCombinedView,
report: CommunityReport,

View file

@ -284,7 +284,6 @@ test("No image proxying if setting is disabled", async () => {
expect(post.post_view.post.body).toBe(`![](${sampleImage})`);
let betaPost = await waitForPost(beta, post.post_view.post, res => {
console.log(res?.post.alt_text);
return res?.post.alt_text != null;
});
expect(betaPost.post).toBeDefined();

View file

@ -12,14 +12,19 @@ import {
reportPrivateMessage,
unfollows,
listInbox,
resolvePerson,
} from "./shared";
let recipient_id: number;
beforeAll(async () => {
await setupLogins();
await followBeta(alpha);
recipient_id = 3;
let betaUser = await beta.getMyUser();
let betaUserOnAlpha = await resolvePerson(
alpha,
betaUser.local_user_view.person.ap_id,
);
recipient_id = betaUserOnAlpha!.person.id;
});
afterAll(unfollows);

View file

@ -39,17 +39,16 @@ pub async fn follow_community(
CommunityPersonBanView::check(&mut context.pool(), person_id, community.id).await?;
}
let follow_state = if community.local {
// Local follow is accepted immediately
CommunityFollowerState::Accepted
} else if community.visibility == CommunityVisibility::Private {
let follow_state = if community.visibility == CommunityVisibility::Private {
// Private communities require manual approval
CommunityFollowerState::ApprovalRequired
} else if community.local {
// Local follow is accepted immediately
CommunityFollowerState::Accepted
} else {
// remote follow needs to be federated first
CommunityFollowerState::Pending
};
let form = CommunityFollowerForm::new(community.id, person_id, follow_state);
// Write to db

View file

@ -2,6 +2,7 @@ pub mod add_mod;
pub mod ban;
pub mod block;
pub mod follow;
pub mod multi_community_follow;
pub mod pending_follows;
pub mod random;
pub mod tag;

View file

@ -0,0 +1,53 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_utils::{
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::check_local_user_valid,
};
use lemmy_db_schema::{
source::multi_community::{MultiCommunity, MultiCommunityFollowForm},
traits::Crud,
};
use lemmy_db_schema_file::enums::CommunityFollowerState;
use lemmy_db_views_community::api::FollowMultiCommunity;
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_site::api::SuccessResponse;
use lemmy_utils::error::LemmyResult;
pub async fn follow_multi_community(
data: Json<FollowMultiCommunity>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
check_local_user_valid(&local_user_view)?;
let multi_community_id = data.multi_community_id;
let person_id = local_user_view.person.id;
let multi = MultiCommunity::read(&mut context.pool(), multi_community_id).await?;
let follow_state = if multi.local {
CommunityFollowerState::Accepted
} else {
CommunityFollowerState::Pending
};
let form = MultiCommunityFollowForm {
multi_community_id,
person_id,
follow_state,
};
if data.follow {
MultiCommunity::follow(&mut context.pool(), &form).await?;
} else {
MultiCommunity::unfollow(&mut context.pool(), person_id, multi_community_id).await?;
}
if !multi.local {
ActivityChannel::submit_activity(
SendActivityData::FollowMultiCommunity(multi, local_user_view.person.clone(), data.follow),
&context,
)?;
}
Ok(Json(SuccessResponse::default()))
}

View file

@ -4,7 +4,7 @@ use lemmy_db_schema::{source::post::PostActions, traits::Readable};
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_post::api::MarkManyPostsAsRead;
use lemmy_db_views_site::api::SuccessResponse;
use lemmy_utils::error::{LemmyErrorType, LemmyResult, MAX_API_PARAM_ELEMENTS};
use lemmy_utils::{error::LemmyResult, utils::validation::check_api_elements_count};
pub async fn mark_posts_as_read(
data: Json<MarkManyPostsAsRead>,
@ -12,9 +12,7 @@ pub async fn mark_posts_as_read(
local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
let post_ids = &data.post_ids;
if post_ids.len() > MAX_API_PARAM_ELEMENTS {
Err(LemmyErrorType::TooManyItems)?;
}
check_api_elements_count(post_ids.len())?;
let person_id = local_user_view.person.id;

View file

@ -10,14 +10,12 @@ use lemmy_api_utils::context::LemmyContext;
use lemmy_db_schema::{
newtypes::InstanceId,
source::{
instance::Instance,
local_site::{LocalSite, LocalSiteInsertForm},
local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitInsertForm},
local_site::{LocalSite, LocalSiteUpdateForm},
local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm},
person::{Person, PersonInsertForm},
registration_application::{RegistrationApplication, RegistrationApplicationInsertForm},
site::{Site, SiteInsertForm},
},
test_data::TestData,
traits::Crud,
utils::DbPool,
};
@ -35,14 +33,23 @@ use lemmy_utils::{
};
use serial_test::serial;
async fn create_test_site(context: &Data<LemmyContext>) -> LemmyResult<(Instance, LocalUserView)> {
async fn create_test_site(context: &Data<LemmyContext>) -> LemmyResult<(TestData, LocalUserView)> {
let pool = &mut context.pool();
let data = TestData::create(pool).await?;
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
// Enable some local site settings
let local_site_form = LocalSiteUpdateForm {
require_email_verification: Some(true),
application_question: Some(Some(".".to_string())),
registration_mode: Some(RegistrationMode::RequireApplication),
site_setup: Some(true),
..Default::default()
};
LocalSite::update(pool, &local_site_form).await?;
let admin_person = Person::create(
pool,
&PersonInsertForm::test_form(inserted_instance.id, "admin"),
&PersonInsertForm::test_form(data.instance.id, "admin"),
)
.await?;
LocalUser::create(
@ -52,28 +59,9 @@ async fn create_test_site(context: &Data<LemmyContext>) -> LemmyResult<(Instance
)
.await?;
let site_form = SiteInsertForm::new("test site".to_string(), inserted_instance.id);
let site = Site::create(pool, &site_form).await?;
// Create a local site, since this is necessary for determining if email verification is
// required
let local_site_form = LocalSiteInsertForm {
require_email_verification: Some(true),
application_question: Some(".".to_string()),
registration_mode: Some(RegistrationMode::RequireApplication),
site_setup: Some(true),
..LocalSiteInsertForm::new(site.id)
};
let local_site = LocalSite::create(pool, &local_site_form).await?;
// Required to have a working local SiteView when updating the site to change email verification
// requirement or registration mode
let rate_limit_form = LocalSiteRateLimitInsertForm::new(local_site.id);
LocalSiteRateLimit::create(pool, &rate_limit_form).await?;
let admin_local_user_view = LocalUserView::read_person(pool, admin_person.id).await?;
Ok((inserted_instance, admin_local_user_view))
Ok((data, admin_local_user_view))
}
async fn signup(
@ -140,14 +128,19 @@ async fn test_application_approval() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let pool = &mut context.pool();
let (instance, admin_local_user_view) = create_test_site(&context).await?;
let (data, admin_local_user_view) = create_test_site(&context).await?;
// Non-unread counts unfortunately are duplicated due to different types (i64 vs usize)
let mut expected_total_applications = 0;
let mut expected_unread_applications = 0u8;
let (local_user_with_email, app_with_email) =
signup(pool, instance.id, "user_w_email", Some("lemmy@localhost")).await?;
let (local_user_with_email, app_with_email) = signup(
pool,
data.instance.id,
"user_w_email",
Some("lemmy@localhost"),
)
.await?;
let (application_count, unread_applications, all_applications) =
get_application_statuses(&context, admin_local_user_view.clone()).await?;
@ -242,7 +235,7 @@ async fn test_application_approval() -> LemmyResult<()> {
let (_local_user, app_with_email_2) = signup(
pool,
instance.id,
data.instance.id,
"user_w_email_2",
Some("lemmy2@localhost"),
)
@ -328,7 +321,7 @@ async fn test_application_approval() -> LemmyResult<()> {
expected_total_applications,
);
signup(pool, instance.id, "user_wo_email", None).await?;
signup(pool, data.instance.id, "user_wo_email", None).await?;
expected_total_applications += 1;
expected_unread_applications += 1;
@ -350,7 +343,7 @@ async fn test_application_approval() -> LemmyResult<()> {
expected_total_applications,
);
signup(pool, instance.id, "user_w_email_3", None).await?;
signup(pool, data.instance.id, "user_w_email_3", None).await?;
expected_total_applications += 1;
expected_unread_applications += 1;
@ -410,7 +403,7 @@ async fn test_application_approval() -> LemmyResult<()> {
LocalSite::delete(pool).await?;
// Instance deletion cascades cleanup of all created persons
Instance::delete(pool, instance.id).await?;
data.delete(pool).await?;
Ok(())
}

View file

@ -41,6 +41,7 @@ pub async fn list_communities(
cursor_data,
page_back: data.page_back,
limit: data.limit,
..Default::default()
}
.list(&local_site.site, &mut context.pool())
.await?;

View file

@ -4,6 +4,7 @@ use lemmy_db_schema::source::community::{Community, CommunityActions};
pub mod comment;
pub mod community;
pub mod custom_emoji;
pub mod multi_community;
pub mod oauth_provider;
pub mod post;
pub mod private_message;

View file

@ -0,0 +1,51 @@
use crate::multi_community::get_multi;
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_utils::{context::LemmyContext, utils::slur_regex};
use lemmy_db_schema::{
source::multi_community::{MultiCommunity, MultiCommunityInsertForm},
traits::Crud,
};
use lemmy_db_views_community::api::{CreateMultiCommunity, GetMultiCommunityResponse};
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_site::SiteView;
use lemmy_utils::{
error::LemmyResult,
utils::{slurs::check_slurs, validation::is_valid_display_name},
};
use url::Url;
pub async fn create_multi_community(
data: Json<CreateMultiCommunity>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<GetMultiCommunityResponse>> {
let site_view = SiteView::read_local(&mut context.pool()).await?;
is_valid_display_name(&data.name, site_view.local_site.actor_name_max_length)?;
let slur_regex = slur_regex(&context).await?;
check_slurs(&data.name, &slur_regex)?;
let ap_id = Url::parse(&format!(
"{}/m/{}",
context.settings().get_protocol_and_hostname(),
&data.name
))?;
let following_url = Url::parse(&format!("{}/following", ap_id))?;
let form = MultiCommunityInsertForm {
title: data.title.clone(),
description: data.description.clone(),
ap_id: Some(ap_id.into()),
private_key: site_view.site.private_key,
inbox_url: Some(site_view.site.inbox_url),
following_url: Some(following_url.into()),
..MultiCommunityInsertForm::new(
local_user_view.person.id,
local_user_view.person.instance_id,
data.name.clone(),
site_view.site.public_key,
)
};
let multi = MultiCommunity::create(&mut context.pool(), &form).await?;
get_multi(multi.id, context).await
}

View file

@ -0,0 +1,58 @@
use super::{check_multi_community_creator, send_federation_update};
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_utils::{
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::check_community_deleted_removed,
};
use lemmy_db_schema::{
source::{
community::{Community, CommunityActions, CommunityFollowerForm},
multi_community::MultiCommunity,
},
traits::{Crud, Followable},
};
use lemmy_db_schema_file::enums::CommunityFollowerState;
use lemmy_db_views_community::api::CreateOrDeleteMultiCommunityEntry;
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_site::{api::SuccessResponse, SiteView};
use lemmy_utils::error::LemmyResult;
pub async fn create_multi_community_entry(
data: Json<CreateOrDeleteMultiCommunityEntry>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
let multi = check_multi_community_creator(data.id, &local_user_view, &context).await?;
let community = Community::read(&mut context.pool(), data.community_id).await?;
check_community_deleted_removed(&community)?;
MultiCommunity::create_entry(&mut context.pool(), data.id, &community).await?;
if !community.local {
let multicomm_follower = SiteView::read_multicomm_follower(&mut context.pool()).await?;
let actions = CommunityActions::read(&mut context.pool(), community.id, multicomm_follower.id)
.await
.unwrap_or_default();
// follow the community if not already followed
if actions.followed_at.is_none() {
let form = CommunityFollowerForm::new(
community.id,
multicomm_follower.id,
CommunityFollowerState::Pending,
);
CommunityActions::follow(&mut context.pool(), &form).await?;
ActivityChannel::submit_activity(
SendActivityData::FollowCommunity(community, local_user_view.person.clone(), true),
&context,
)?;
}
}
send_federation_update(multi, local_user_view, &context).await?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -0,0 +1,48 @@
use super::{check_multi_community_creator, send_federation_update};
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_utils::{
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
};
use lemmy_db_schema::{
source::{
community::{Community, CommunityActions},
multi_community::MultiCommunity,
},
traits::{Crud, Followable},
};
use lemmy_db_views_community::api::CreateOrDeleteMultiCommunityEntry;
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_site::{api::SuccessResponse, SiteView};
use lemmy_utils::error::LemmyResult;
pub async fn delete_multi_community_entry(
data: Json<CreateOrDeleteMultiCommunityEntry>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
let multi = check_multi_community_creator(data.id, &local_user_view, &context).await?;
let community = Community::read(&mut context.pool(), data.community_id).await?;
MultiCommunity::delete_entry(&mut context.pool(), data.id, &community).await?;
if !community.local {
let used_in_multiple =
MultiCommunity::community_used_in_multiple(&mut context.pool(), multi.id, community.id)
.await?;
// unfollow the community only if its not used in another multi-community
if !used_in_multiple {
let multicomm_follower = SiteView::read_multicomm_follower(&mut context.pool()).await?;
CommunityActions::unfollow(&mut context.pool(), multicomm_follower.id, community.id).await?;
ActivityChannel::submit_activity(
SendActivityData::FollowCommunity(community, local_user_view.person.clone(), false),
&context,
)?;
}
}
send_federation_update(multi, local_user_view, &context).await?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -0,0 +1,13 @@
use crate::multi_community::get_multi;
use activitypub_federation::config::Data;
use actix_web::web::{Json, Query};
use lemmy_api_utils::context::LemmyContext;
use lemmy_db_views_community::api::{GetMultiCommunity, GetMultiCommunityResponse};
use lemmy_utils::error::LemmyResult;
pub async fn get_multi_community(
data: Query<GetMultiCommunity>,
context: Data<LemmyContext>,
) -> LemmyResult<Json<GetMultiCommunityResponse>> {
get_multi(data.id, context).await
}

View file

@ -0,0 +1,24 @@
use activitypub_federation::config::Data;
use actix_web::web::{Json, Query};
use lemmy_api_utils::context::LemmyContext;
use lemmy_db_views_community::{
api::{ListMultiCommunities, ListMultiCommunitiesResponse},
MultiCommunityView,
};
use lemmy_db_views_local_user::LocalUserView;
use lemmy_utils::error::LemmyResult;
pub async fn list_multi_communities(
data: Query<ListMultiCommunities>,
context: Data<LemmyContext>,
local_user_view: Option<LocalUserView>,
) -> LemmyResult<Json<ListMultiCommunitiesResponse>> {
let followed_by = if let Some(true) = data.followed_only {
local_user_view.map(|l| l.person.id)
} else {
None
};
let multi_communities =
MultiCommunityView::list(&mut context.pool(), data.creator_id, followed_by).await?;
Ok(Json(ListMultiCommunitiesResponse { multi_communities }))
}

View file

@ -0,0 +1,72 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_utils::{
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
};
use lemmy_db_schema::{
newtypes::MultiCommunityId,
source::multi_community::MultiCommunity,
traits::Crud,
};
use lemmy_db_views_community::{
api::GetMultiCommunityResponse,
impls::CommunityQuery,
MultiCommunityView,
};
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_site::SiteView;
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
pub mod create;
pub mod create_entry;
pub mod delete_entry;
pub mod get;
pub mod list;
pub mod update;
/// Check that current user is creator of multi-comm and can modify it.
async fn check_multi_community_creator(
id: MultiCommunityId,
local_user_view: &LocalUserView,
context: &LemmyContext,
) -> LemmyResult<MultiCommunity> {
let multi = MultiCommunity::read(&mut context.pool(), id).await?;
if multi.local && local_user_view.local_user.admin {
return Ok(multi);
}
if multi.creator_id != local_user_view.person.id {
return Err(LemmyErrorType::MultiCommunityUpdateWrongUser.into());
}
Ok(multi)
}
async fn send_federation_update(
multi: MultiCommunity,
local_user_view: LocalUserView,
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
ActivityChannel::submit_activity(
SendActivityData::UpdateMultiCommunity(multi, local_user_view.person),
context,
)?;
Ok(())
}
async fn get_multi(
id: MultiCommunityId,
context: Data<LemmyContext>,
) -> LemmyResult<Json<GetMultiCommunityResponse>> {
let local_site = SiteView::read_local(&mut context.pool()).await?;
let multi_community_view = MultiCommunityView::read(&mut context.pool(), id).await?;
let communities = CommunityQuery {
multi_community_id: Some(multi_community_view.multi.id),
..Default::default()
}
.list(&local_site.site, &mut context.pool())
.await?;
Ok(Json(GetMultiCommunityResponse {
multi_community_view,
communities,
}))
}

View file

@ -0,0 +1,34 @@
use super::{check_multi_community_creator, send_federation_update};
use activitypub_federation::config::Data;
use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_utils::context::LemmyContext;
use lemmy_db_schema::{
source::multi_community::{MultiCommunity, MultiCommunityUpdateForm},
traits::Crud,
utils::diesel_string_update,
};
use lemmy_db_views_community::api::UpdateMultiCommunity;
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_site::api::SuccessResponse;
use lemmy_utils::error::LemmyResult;
pub async fn update_multi_community(
data: Json<UpdateMultiCommunity>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
check_multi_community_creator(data.id, &local_user_view, &context).await?;
let form = MultiCommunityUpdateForm {
title: diesel_string_update(data.title.as_deref()),
description: diesel_string_update(data.description.as_deref()),
deleted: data.deleted,
updated_at: Some(Utc::now()),
};
let multi = MultiCommunity::update(&mut context.pool(), data.id, &form).await?;
send_federation_update(multi, local_user_view, &context).await?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -107,6 +107,7 @@ pub async fn create_site(
comment_downvotes: data.comment_downvotes,
disallow_nsfw_content: data.disallow_nsfw_content,
disable_email_notifications: data.disable_email_notifications,
suggested_communities: data.suggested_communities,
..Default::default()
};

View file

@ -116,6 +116,7 @@ pub async fn update_site(
comment_downvotes: data.comment_downvotes,
disallow_nsfw_content: data.disallow_nsfw_content,
disable_email_notifications: data.disable_email_notifications,
suggested_communities: data.suggested_communities,
..Default::default()
};

View file

@ -200,7 +200,7 @@ pub async fn send_local_notifs(
&mention_user_view,
&comment_content_or_post_body,
person,
link,
link.into(),
context.settings(),
)
.await;

View file

@ -7,6 +7,7 @@ use lemmy_db_schema::{
source::{
comment::Comment,
community::Community,
multi_community::MultiCommunity,
person::Person,
post::Post,
private_message::PrivateMessage,
@ -63,6 +64,7 @@ pub enum SendActivityData {
score: i16,
},
FollowCommunity(Community, Person, bool),
FollowMultiCommunity(MultiCommunity, Person, bool),
AcceptFollower(CommunityId, PersonId),
RejectFollower(CommunityId, PersonId),
UpdateCommunity(Person, Community),
@ -109,6 +111,7 @@ pub enum SendActivityData {
report_creator: Person,
receiver: Either<Site, Community>,
},
UpdateMultiCommunity(MultiCommunity, Person),
}
// TODO: instead of static, move this into LemmyContext. make sure that stopping the process with

View file

@ -57,10 +57,7 @@ impl BlockUser {
kind: BlockType::Block,
remove_data,
summary: reason,
id: generate_activity_id(
BlockType::Block,
&context.settings().get_protocol_and_hostname(),
)?,
id: generate_activity_id(BlockType::Block, context)?,
end_time: expires,
})
}

View file

@ -48,10 +48,7 @@ impl UndoBlockUser {
let block = BlockUser::new(target, user, mod_, None, reason, None, context).await?;
let to = to(target)?;
let id = generate_activity_id(
UndoType::Undo,
&context.settings().get_protocol_and_hostname(),
)?;
let id = generate_activity_id(UndoType::Undo, context)?;
let undo = UndoBlockUser {
actor: mod_.id().into(),
to,

View file

@ -115,10 +115,7 @@ impl AnnounceActivity {
// Hack: need to convert Page into a format which can be sent as activity, which requires
// adding actor field.
let announcable_page = RawAnnouncableActivities {
id: generate_activity_id(
AnnounceType::Announce,
&context.settings().get_protocol_and_hostname(),
)?,
id: generate_activity_id(AnnounceType::Announce, context)?,
actor: c.actor.clone().into_inner(),
other: serde_json::to_value(c.object)?
.as_object()

View file

@ -40,16 +40,13 @@ use lemmy_utils::error::{LemmyError, LemmyResult};
use url::Url;
impl CollectionAdd {
pub async fn send_add_mod(
async fn send_add_mod(
community: &ApubCommunity,
added_mod: &ApubPerson,
actor: &ApubPerson,
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
let id = generate_activity_id(
AddType::Add,
&context.settings().get_protocol_and_hostname(),
)?;
let id = generate_activity_id(AddType::Add, context)?;
let add = CollectionAdd {
actor: actor.id().into(),
to: generate_to(community)?,
@ -65,16 +62,13 @@ impl CollectionAdd {
send_activity_in_community(activity, actor, community, inboxes, true, context).await
}
pub async fn send_add_featured_post(
async fn send_add_featured_post(
community: &ApubCommunity,
featured_post: &ApubPost,
actor: &ApubPerson,
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
let id = generate_activity_id(
AddType::Add,
&context.settings().get_protocol_and_hostname(),
)?;
let id = generate_activity_id(AddType::Add, context)?;
let add = CollectionAdd {
actor: actor.id().into(),
to: generate_to(community)?,
@ -148,7 +142,6 @@ impl ActivityHandler for CollectionAdd {
};
ModAddCommunity::create(&mut context.pool(), &form).await?;
}
// TODO: send websocket notification about added mod
}
CollectionType::Featured => {
let post = ObjectId::<ApubPost>::from(self.object)

View file

@ -35,16 +35,13 @@ use lemmy_utils::error::{LemmyError, LemmyResult};
use url::Url;
impl CollectionRemove {
pub async fn send_remove_mod(
pub(super) async fn send_remove_mod(
community: &ApubCommunity,
removed_mod: &ApubPerson,
actor: &ApubPerson,
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
let id = generate_activity_id(
RemoveType::Remove,
&context.settings().get_protocol_and_hostname(),
)?;
let id = generate_activity_id(RemoveType::Remove, context)?;
let remove = CollectionRemove {
actor: actor.id().into(),
to: generate_to(community)?,
@ -60,16 +57,13 @@ impl CollectionRemove {
send_activity_in_community(activity, actor, community, inboxes, true, context).await
}
pub async fn send_remove_featured_post(
pub(super) async fn send_remove_featured_post(
community: &ApubCommunity,
featured_post: &ApubPost,
actor: &ApubPerson,
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
let id = generate_activity_id(
RemoveType::Remove,
&context.settings().get_protocol_and_hostname(),
)?;
let id = generate_activity_id(RemoveType::Remove, context)?;
let remove = CollectionRemove {
actor: actor.id().into(),
to: generate_to(community)?,

View file

@ -136,10 +136,7 @@ pub(crate) async fn send_lock_post(
let community: ApubCommunity = Community::read(&mut context.pool(), post.community_id)
.await?
.into();
let id = generate_activity_id(
LockType::Lock,
&context.settings().get_protocol_and_hostname(),
)?;
let id = generate_activity_id(LockType::Lock, &context)?;
let community_id = community.ap_id.inner().clone();
let lock = LockPage {
@ -154,10 +151,7 @@ pub(crate) async fn send_lock_post(
let activity = if locked {
AnnouncableActivities::LockPost(lock)
} else {
let id = generate_activity_id(
UndoType::Undo,
&context.settings().get_protocol_and_hostname(),
)?;
let id = generate_activity_id(UndoType::Undo, &context)?;
let undo = UndoLockPage {
actor: lock.actor.clone(),
to: generate_to(&community)?,

View file

@ -66,12 +66,7 @@ pub(crate) async fn send_activity_in_community(
// send to user followers
if !is_mod_action {
inboxes.add_inboxes(
PersonActions::list_followers(&mut context.pool(), actor.id)
.await?
.into_iter()
.map(|p| ApubPerson(p).shared_inbox_or_inbox()),
);
inboxes.add_inboxes(PersonActions::follower_inboxes(&mut context.pool(), actor.id).await?);
}
if community.local {

View file

@ -53,10 +53,7 @@ impl Report {
context: &Data<LemmyContext>,
) -> LemmyResult<Self> {
let kind = FlagType::Flag;
let id = generate_activity_id(
kind.clone(),
&context.settings().get_protocol_and_hostname(),
)?;
let id = generate_activity_id(kind.clone(), context)?;
Ok(Report {
actor: actor.id().into(),
to: [receiver.id().into()],

View file

@ -47,10 +47,7 @@ impl ResolveReport {
context: Data<LemmyContext>,
) -> LemmyResult<()> {
let kind = ResolveType::Resolve;
let id = generate_activity_id(
kind.clone(),
&context.settings().get_protocol_and_hostname(),
)?;
let id = generate_activity_id(kind.clone(), &context)?;
let object = Report::new(&object_id, report_creator, receiver, None, &context)?;
let resolve = ResolveReport {
actor: actor.id().into(),

View file

@ -1,17 +1,23 @@
use crate::{
activities::{community::send_activity_in_community, generate_activity_id, verify_mod_action},
activities::{
community::send_activity_in_community,
generate_activity_id,
send_lemmy_activity,
verify_mod_action,
},
activity_lists::AnnouncableActivities,
insert_received_activity,
protocol::activities::community::update::UpdateCommunity,
protocol::activities::community::update::Update,
};
use activitypub_federation::{
config::Data,
kinds::activity::UpdateType,
kinds::{activity::UpdateType, public},
traits::{ActivityHandler, Actor, Object},
};
use either::Either;
use lemmy_api_utils::context::LemmyContext;
use lemmy_apub_objects::{
objects::{community::ApubCommunity, person::ApubPerson},
objects::{community::ApubCommunity, multi_community::ApubMultiCommunity, person::ApubPerson},
utils::{
functions::{generate_to, verify_person_in_community, verify_visibility},
protocol::InCommunity,
@ -22,6 +28,7 @@ use lemmy_db_schema::{
activity::ActivitySendTargets,
community::Community,
mod_log::moderator::{ModChangeCommunityVisibility, ModChangeCommunityVisibilityForm},
multi_community::MultiCommunity,
person::Person,
},
traits::Crud,
@ -36,20 +43,17 @@ pub(crate) async fn send_update_community(
) -> LemmyResult<()> {
let community: ApubCommunity = community.into();
let actor: ApubPerson = actor.into();
let id = generate_activity_id(
UpdateType::Update,
&context.settings().get_protocol_and_hostname(),
)?;
let update = UpdateCommunity {
let id = generate_activity_id(UpdateType::Update, &context)?;
let update = Update {
actor: actor.id().into(),
to: generate_to(&community)?,
object: Box::new(community.clone().into_json(&context).await?),
object: Either::Left(community.clone().into_json(&context).await?),
cc: vec![community.id()],
kind: UpdateType::Update,
id: id.clone(),
};
let activity = AnnouncableActivities::UpdateCommunity(update);
let activity = AnnouncableActivities::UpdateCommunity(Box::new(update));
send_activity_in_community(
activity,
&actor,
@ -61,8 +65,31 @@ pub(crate) async fn send_update_community(
.await
}
pub(crate) async fn send_update_multi_community(
multi: MultiCommunity,
actor: Person,
context: Data<LemmyContext>,
) -> LemmyResult<()> {
let multi: ApubMultiCommunity = multi.into();
let actor: ApubPerson = actor.into();
let id = generate_activity_id(UpdateType::Update, &context)?;
let update = Update {
actor: actor.id().into(),
to: vec![multi.ap_id.clone().into(), public()],
object: Either::Right(multi.clone().into_json(&context).await?),
cc: vec![],
kind: UpdateType::Update,
id: id.clone(),
};
let activity = AnnouncableActivities::UpdateCommunity(Box::new(update));
let mut inboxes = ActivitySendTargets::empty();
inboxes.add_inboxes(MultiCommunity::follower_inboxes(&mut context.pool(), multi.id).await?);
send_lemmy_activity(&context, activity, &actor, inboxes, false).await
}
#[async_trait::async_trait]
impl ActivityHandler for UpdateCommunity {
impl ActivityHandler for Update {
type DataType = LemmyContext;
type Error = LemmyError;
@ -75,28 +102,41 @@ impl ActivityHandler for UpdateCommunity {
}
async fn verify(&self, context: &Data<Self::DataType>) -> LemmyResult<()> {
let community = self.community(context).await?;
verify_visibility(&self.to, &self.cc, &community)?;
verify_person_in_community(&self.actor, &community, context).await?;
verify_mod_action(&self.actor, &community, context).await?;
ApubCommunity::verify(&self.object, &community.ap_id.clone().into(), context).await?;
match &self.object {
Either::Left(c) => {
let community = self.community(context).await?;
verify_visibility(&self.to, &self.cc, &community)?;
verify_person_in_community(&self.actor, &community, context).await?;
verify_mod_action(&self.actor, &community, context).await?;
ApubCommunity::verify(c, &community.ap_id.clone().into(), context).await?;
}
Either::Right(m) => ApubMultiCommunity::verify(m, &self.id, context).await?,
}
Ok(())
}
async fn receive(self, context: &Data<Self::DataType>) -> LemmyResult<()> {
insert_received_activity(&self.id, context).await?;
let old_community = self.community(context).await?;
let community = ApubCommunity::from_json(*self.object, context).await?;
match self.object {
Either::Left(ref c) => {
let old_community = self.community(context).await?;
if old_community.visibility != community.visibility {
let actor = self.actor.dereference(context).await?;
let form = ModChangeCommunityVisibilityForm {
mod_person_id: actor.id,
community_id: old_community.id,
visibility: old_community.visibility,
};
ModChangeCommunityVisibility::create(&mut context.pool(), &form).await?;
let community = ApubCommunity::from_json(c.clone(), context).await?;
if old_community.visibility != community.visibility {
let actor = self.actor.dereference(context).await?;
let form = ModChangeCommunityVisibilityForm {
mod_person_id: actor.id,
community_id: old_community.id,
visibility: old_community.visibility,
};
ModChangeCommunityVisibility::create(&mut context.pool(), &form).await?;
}
}
Either::Right(m) => {
ApubMultiCommunity::from_json(m, context).await?;
}
}
Ok(())
}

View file

@ -62,10 +62,7 @@ impl CreateOrUpdateNote {
.await?
.into();
let id = generate_activity_id(
kind.clone(),
&context.settings().get_protocol_and_hostname(),
)?;
let id = generate_activity_id(kind.clone(), &context)?;
let note = ApubComment(comment).into_json(&context).await?;
let create_or_update = CreateOrUpdateNote {

View file

@ -46,10 +46,7 @@ impl CreateOrUpdatePage {
kind: CreateOrUpdateType,
context: &Data<LemmyContext>,
) -> LemmyResult<CreateOrUpdatePage> {
let id = generate_activity_id(
kind.clone(),
&context.settings().get_protocol_and_hostname(),
)?;
let id = generate_activity_id(kind.clone(), context)?;
Ok(CreateOrUpdatePage {
actor: actor.id().into(),
to: generate_to(community)?,

View file

@ -26,10 +26,7 @@ pub(crate) async fn send_create_or_update_pm(
let actor: ApubPerson = pm_view.creator.into();
let recipient: ApubPerson = pm_view.recipient.into();
let id = generate_activity_id(
kind.clone(),
&context.settings().get_protocol_and_hostname(),
)?;
let id = generate_activity_id(kind.clone(), &context)?;
let create_or_update = CreateOrUpdatePrivateMessage {
id: id.clone(),
actor: actor.id().into(),

View file

@ -88,10 +88,7 @@ impl Delete {
summary: Option<String>,
context: &Data<LemmyContext>,
) -> LemmyResult<Delete> {
let id = generate_activity_id(
DeleteType::Delete,
&context.settings().get_protocol_and_hostname(),
)?;
let id = generate_activity_id(DeleteType::Delete, context)?;
let cc: Option<Url> = community.map(|c| c.ap_id.clone().into());
Ok(Delete {
actor: actor.ap_id.clone().into(),

View file

@ -73,10 +73,7 @@ impl UndoDelete {
) -> LemmyResult<UndoDelete> {
let object = Delete::new(actor, object, to.clone(), community, summary, context)?;
let id = generate_activity_id(
UndoType::Undo,
&context.settings().get_protocol_and_hostname(),
)?;
let id = generate_activity_id(UndoType::Undo, context)?;
let cc: Option<Url> = community.map(|c| c.ap_id.clone().into());
Ok(UndoDelete {
actor: actor.ap_id.clone().into(),

View file

@ -1,6 +1,5 @@
use super::send_activity_from_user_or_community;
use crate::{
activities::generate_activity_id,
activities::{generate_activity_id, send_lemmy_activity},
insert_received_activity,
protocol::activities::following::{accept::AcceptFollow, follow::Follow},
};
@ -20,20 +19,17 @@ use url::Url;
impl AcceptFollow {
pub async fn send(follow: Follow, context: &Data<LemmyContext>) -> LemmyResult<()> {
let user_or_community = follow.object.dereference_local(context).await?;
let target = follow.object.dereference_local(context).await?;
let person = follow.actor.clone().dereference(context).await?;
let accept = AcceptFollow {
actor: user_or_community.id().into(),
actor: target.id().into(),
to: Some([person.id().into()]),
object: follow,
kind: AcceptType::Accept,
id: generate_activity_id(
AcceptType::Accept,
&context.settings().get_protocol_and_hostname(),
)?,
id: generate_activity_id(AcceptType::Accept, context)?,
};
let inbox = ActivitySendTargets::to_inbox(person.shared_inbox_or_inbox());
send_activity_from_user_or_community(context, accept, user_or_community, inbox).await
send_lemmy_activity(context, accept, &target, inbox, true).await
}
}

View file

@ -9,9 +9,10 @@ use activitypub_federation::{
protocol::verification::verify_urls_match,
traits::{ActivityHandler, Actor},
};
use either::Either::*;
use lemmy_api_utils::context::LemmyContext;
use lemmy_apub_objects::{
objects::{community::ApubCommunity, person::ApubPerson, UserOrCommunity},
objects::{person::ApubPerson, CommunityOrMulti},
utils::functions::verify_person_in_community,
};
use lemmy_db_schema::{
@ -19,6 +20,7 @@ use lemmy_db_schema::{
activity::ActivitySendTargets,
community::{CommunityActions, CommunityFollowerForm},
instance::Instance,
multi_community::{MultiCommunity, MultiCommunityFollowForm},
person::{PersonActions, PersonFollowerForm},
},
traits::Followable,
@ -30,32 +32,25 @@ use url::Url;
impl Follow {
pub(in crate::activities::following) fn new(
actor: &ApubPerson,
community: &ApubCommunity,
target: &CommunityOrMulti,
context: &Data<LemmyContext>,
) -> LemmyResult<Follow> {
Ok(Follow {
actor: actor.id().into(),
object: community.id().into(),
to: Some([community.id().into()]),
object: target.id().into(),
to: Some([target.id().into()]),
kind: FollowType::Follow,
id: generate_activity_id(
FollowType::Follow,
&context.settings().get_protocol_and_hostname(),
)?,
id: generate_activity_id(FollowType::Follow, context)?,
})
}
pub async fn send(
actor: &ApubPerson,
community: &ApubCommunity,
target: &CommunityOrMulti,
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
let follow = Follow::new(actor, community, context)?;
let inbox = if community.local {
ActivitySendTargets::empty()
} else {
ActivitySendTargets::to_inbox(community.shared_inbox_or_inbox())
};
let follow = Follow::new(actor, target, context)?;
let inbox = ActivitySendTargets::to_inbox(target.shared_inbox_or_inbox());
send_lemmy_activity(context, follow, actor, inbox, true).await
}
}
@ -76,7 +71,7 @@ impl ActivityHandler for Follow {
async fn verify(&self, context: &Data<LemmyContext>) -> LemmyResult<()> {
verify_person(&self.actor, context).await?;
let object = self.object.dereference(context).await?;
if let UserOrCommunity::Right(c) = object {
if let Right(Left(c)) = object {
verify_person_in_community(&self.actor, &c, context).await?;
}
if let Some(to) = &self.to {
@ -91,12 +86,12 @@ impl ActivityHandler for Follow {
let actor = self.actor.dereference(context).await?;
let object = self.object.dereference(context).await?;
match object {
UserOrCommunity::Left(u) => {
Left(u) => {
let form = PersonFollowerForm::new(u.id, actor.id, false);
PersonActions::follow(&mut context.pool(), &form).await?;
AcceptFollow::send(self, context).await?;
}
UserOrCommunity::Right(c) => {
Right(Left(c)) => {
if c.visibility == CommunityVisibility::Private {
let instance = Instance::read(&mut context.pool(), actor.instance_id).await?;
if [Some("kbin"), Some("mbin")].contains(&instance.software.as_deref()) {
@ -116,6 +111,16 @@ impl ActivityHandler for Follow {
AcceptFollow::send(self, context).await?;
}
}
Right(Right(m)) => {
let form = MultiCommunityFollowForm {
multi_community_id: m.id,
person_id: actor.id,
follow_state: CommunityFollowerState::Accepted,
};
MultiCommunity::follow(&mut context.pool(), &form).await?;
AcceptFollow::send(self, context).await?;
}
}
Ok(())
}

View file

@ -6,8 +6,9 @@ use crate::protocol::activities::following::{
undo_follow::UndoFollow,
};
use activitypub_federation::{config::Data, kinds::activity::FollowType, traits::ActivityHandler};
use either::Either::*;
use lemmy_api_utils::context::LemmyContext;
use lemmy_apub_objects::objects::{community::ApubCommunity, person::ApubPerson, UserOrCommunity};
use lemmy_apub_objects::objects::{person::ApubPerson, CommunityOrMulti, UserOrCommunityOrMulti};
use lemmy_db_schema::{
newtypes::{CommunityId, PersonId},
source::{activity::ActivitySendTargets, community::Community, person::Person},
@ -21,18 +22,17 @@ pub(crate) mod follow;
pub(crate) mod reject;
pub(crate) mod undo_follow;
pub async fn send_follow_community(
community: Community,
pub async fn send_follow(
target: CommunityOrMulti,
person: Person,
follow: bool,
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
let community: ApubCommunity = community.into();
let actor: ApubPerson = person.into();
if follow {
Follow::send(&actor, &community, context).await
Follow::send(&actor, &target, context).await
} else {
UndoFollow::send(&actor, &community, context).await
UndoFollow::send(&actor, &target, context).await
}
}
@ -50,10 +50,7 @@ pub async fn send_accept_or_reject_follow(
to: Some([community.ap_id.clone().into()]),
object: community.ap_id.into(),
kind: FollowType::Follow,
id: generate_activity_id(
FollowType::Follow,
&context.settings().get_protocol_and_hostname(),
)?,
id: generate_activity_id(FollowType::Follow, context)?,
};
if accepted {
AcceptFollow::send(follow, context).await
@ -63,21 +60,20 @@ pub async fn send_accept_or_reject_follow(
}
/// Wrapper type which is needed because we cant implement ActorT for Either.
async fn send_activity_from_user_or_community<Activity>(
async fn send_activity_from_user_or_community_or_multi<Activity>(
context: &Data<LemmyContext>,
activity: Activity,
user_or_community: UserOrCommunity,
target: UserOrCommunityOrMulti,
send_targets: ActivitySendTargets,
) -> LemmyResult<()>
where
Activity: ActivityHandler + Serialize + Send + Sync + Clone + ActivityHandler<Error = LemmyError>,
{
match user_or_community {
UserOrCommunity::Left(user) => {
send_lemmy_activity(context, activity, &user, send_targets, true).await
}
UserOrCommunity::Right(community) => {
match target {
Left(user) => send_lemmy_activity(context, activity, &user, send_targets, true).await,
Right(Left(community)) => {
send_lemmy_activity(context, activity, &community, send_targets, true).await
}
Right(Right(multi)) => send_lemmy_activity(context, activity, &multi, send_targets, true).await,
}
}

View file

@ -1,4 +1,4 @@
use super::send_activity_from_user_or_community;
use super::send_activity_from_user_or_community_or_multi;
use crate::{
activities::generate_activity_id,
insert_received_activity,
@ -27,13 +27,10 @@ impl RejectFollow {
to: Some([person.id().into()]),
object: follow,
kind: RejectType::Reject,
id: generate_activity_id(
RejectType::Reject,
&context.settings().get_protocol_and_hostname(),
)?,
id: generate_activity_id(RejectType::Reject, context)?,
};
let inbox = ActivitySendTargets::to_inbox(person.shared_inbox_or_inbox());
send_activity_from_user_or_community(context, reject, user_or_community, inbox).await
send_activity_from_user_or_community_or_multi(context, reject, user_or_community, inbox).await
}
}

View file

@ -9,10 +9,16 @@ use activitypub_federation::{
protocol::verification::verify_urls_match,
traits::{ActivityHandler, Actor},
};
use either::Either::*;
use lemmy_api_utils::context::LemmyContext;
use lemmy_apub_objects::objects::{community::ApubCommunity, person::ApubPerson, UserOrCommunity};
use lemmy_apub_objects::objects::{person::ApubPerson, CommunityOrMulti};
use lemmy_db_schema::{
source::{activity::ActivitySendTargets, community::CommunityActions, person::PersonActions},
source::{
activity::ActivitySendTargets,
community::CommunityActions,
multi_community::MultiCommunity,
person::PersonActions,
},
traits::Followable,
};
use lemmy_utils::error::{LemmyError, LemmyResult};
@ -21,25 +27,18 @@ use url::Url;
impl UndoFollow {
pub async fn send(
actor: &ApubPerson,
community: &ApubCommunity,
target: &CommunityOrMulti,
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
let object = Follow::new(actor, community, context)?;
let object = Follow::new(actor, target, context)?;
let undo = UndoFollow {
actor: actor.id().into(),
to: Some([community.id().into()]),
to: Some([target.id().into()]),
object,
kind: UndoType::Undo,
id: generate_activity_id(
UndoType::Undo,
&context.settings().get_protocol_and_hostname(),
)?,
};
let inbox = if community.local {
ActivitySendTargets::empty()
} else {
ActivitySendTargets::to_inbox(community.shared_inbox_or_inbox())
id: generate_activity_id(UndoType::Undo, context)?,
};
let inbox = ActivitySendTargets::to_inbox(target.shared_inbox_or_inbox());
send_lemmy_activity(context, undo, actor, inbox, true).await
}
}
@ -73,12 +72,13 @@ impl ActivityHandler for UndoFollow {
let object = self.object.object.dereference(context).await?;
match object {
UserOrCommunity::Left(u) => {
Left(u) => {
PersonActions::unfollow(&mut context.pool(), person.id, u.id).await?;
}
UserOrCommunity::Right(c) => {
Right(Left(c)) => {
CommunityActions::unfollow(&mut context.pool(), person.id, c.id).await?;
}
Right(Right(m)) => MultiCommunity::unfollow(&mut context.pool(), person.id, m.id).await?,
}
Ok(())

View file

@ -1,11 +1,10 @@
use self::following::send_follow_community;
use crate::{
activities::{
block::{send_ban_from_community, send_ban_from_site},
community::{
collection_add::{send_add_mod_to_community, send_feature_post},
lock_page::send_lock_post,
update::send_update_community,
update::{send_update_community, send_update_multi_community},
},
create_or_update::private_message::send_create_or_update_pm,
deletion::{
@ -14,6 +13,7 @@ use crate::{
send_apub_delete_user,
DeletableObjects,
},
following::send_follow,
voting::send_like_activity,
},
protocol::activities::{
@ -28,6 +28,7 @@ use activitypub_federation::{
kinds::activity::AnnounceType,
traits::{ActivityHandler, Actor},
};
use either::Either;
use following::send_accept_or_reject_follow;
use lemmy_api_utils::{
context::LemmyContext,
@ -108,13 +109,13 @@ pub(crate) fn check_community_deleted_or_removed(community: &Community) -> Lemmy
/// Generate a unique ID for an activity, in the format:
/// `http(s)://example.com/receive/create/202daf0a-1489-45df-8d2e-c8a3173fed36`
fn generate_activity_id<T>(kind: T, protocol_and_hostname: &str) -> Result<Url, ParseError>
fn generate_activity_id<T>(kind: T, context: &LemmyContext) -> Result<Url, ParseError>
where
T: ToString,
{
let id = format!(
"{}/activities/{}/{}",
protocol_and_hostname,
&context.settings().get_protocol_and_hostname(),
kind.to_string().to_lowercase(),
Uuid::new_v4()
);
@ -258,7 +259,10 @@ pub async fn match_outgoing_activities(
score,
} => send_like_activity(object_id, actor, community, score, context).await,
FollowCommunity(community, person, follow) => {
send_follow_community(community, person, follow, &context).await
send_follow(Either::Left(community.into()), person, follow, &context).await
}
FollowMultiCommunity(multi, person, follow) => {
send_follow(Either::Right(multi.into()), person, follow, &context).await
}
UpdateCommunity(actor, community) => send_update_community(community, actor, context).await,
DeleteCommunity(actor, community, removed) => {
@ -359,6 +363,9 @@ pub async fn match_outgoing_activities(
RejectFollower(community_id, person_id) => {
send_accept_or_reject_follow(community_id, person_id, false, &context).await
}
UpdateMultiCommunity(multi, actor) => {
send_update_multi_community(multi, actor, context).await
}
}
};
fed_task.await?;

View file

@ -30,10 +30,7 @@ impl UndoVote {
actor: actor.id().into(),
object: vote,
kind: UndoType::Undo,
id: generate_activity_id(
UndoType::Undo,
&context.settings().get_protocol_and_hostname(),
)?,
id: generate_activity_id(UndoType::Undo, context)?,
})
}
}

View file

@ -32,7 +32,7 @@ impl Vote {
actor: actor.id().into(),
object: object_id,
kind: kind.clone(),
id: generate_activity_id(kind, &context.settings().get_protocol_and_hostname())?,
id: generate_activity_id(kind, context)?,
})
}
}

View file

@ -7,7 +7,7 @@ use crate::protocol::activities::{
lock_page::{LockPage, UndoLockPage},
report::Report,
resolve_report::ResolveReport,
update::UpdateCommunity,
update::Update,
},
create_or_update::{note_wrapper::CreateOrUpdateNoteWrapper, page::CreateOrUpdatePage},
deletion::{delete::Delete, undo_delete::UndoDelete},
@ -60,7 +60,7 @@ pub enum AnnouncableActivities {
UndoVote(UndoVote),
Delete(Delete),
UndoDelete(UndoDelete),
UpdateCommunity(UpdateCommunity),
UpdateCommunity(Box<Update>),
BlockUser(BlockUser),
UndoBlockUser(UndoBlockUser),
CollectionAdd(CollectionAdd),

View file

@ -43,6 +43,7 @@ pub async fn list_posts(
} else {
data.community_id
};
let multi_community_id = data.multi_community_id;
let show_hidden = data.show_hidden;
let show_read = data.show_read;
// Show nsfw content if param is true, or if content_warning exists
@ -87,6 +88,7 @@ pub async fn list_posts(
sort,
time_range_seconds,
community_id,
multi_community_id,
limit,
show_hidden,
show_read,

View file

@ -4,7 +4,7 @@ use actix_web::web::{Json, Query};
use either::Either::*;
use lemmy_api_utils::{context::LemmyContext, utils::check_private_instance};
use lemmy_db_views_comment::CommentView;
use lemmy_db_views_community::CommunityView;
use lemmy_db_views_community::{CommunityView, MultiCommunityView};
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_person::PersonView;
use lemmy_db_views_post::PostView;
@ -54,18 +54,19 @@ pub(super) async fn resolve_object_internal(
let local_instance_id = SiteView::read_local(pool).await?.site.instance_id;
Ok(match object {
Left(Left(p)) => {
Left(Left(Left(p))) => {
Post(PostView::read(pool, p.id, local_user.as_ref(), local_instance_id, is_admin).await?)
}
Left(Right(c)) => {
Left(Left(Right(c))) => {
Comment(CommentView::read(pool, c.id, local_user.as_ref(), local_instance_id).await?)
}
Right(Left(u)) => {
Left(Right(Left(u))) => {
Person(PersonView::read(pool, u.id, my_person_id, local_instance_id, is_admin).await?)
}
Right(Right(c)) => {
Left(Right(Right(c))) => {
Community(CommunityView::read(pool, c.id, local_user.as_ref(), is_admin).await?)
}
Right(multi) => MultiCommunity(MultiCommunityView::read(pool, multi.id).await?),
})
}
@ -75,13 +76,12 @@ mod tests {
use lemmy_db_schema::{
source::{
community::{Community, CommunityInsertForm},
instance::Instance,
local_site::LocalSite,
post::{Post, PostInsertForm, PostUpdateForm},
},
test_data::TestData,
traits::Crud,
};
use lemmy_db_views_site::impls::create_test_instance;
use serial_test::serial;
#[tokio::test]
@ -89,7 +89,7 @@ mod tests {
async fn test_object_visibility() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let pool = &mut context.pool();
let instance = create_test_instance(pool).await?;
let data = TestData::create(pool).await?;
let name = "test_local_user_name";
let bio = "test_local_user_bio";
@ -101,7 +101,7 @@ mod tests {
let community = Community::create(
pool,
&CommunityInsertForm::new(
instance.id,
data.instance.id,
"test".to_string(),
"test".to_string(),
"pubkey".to_string(),
@ -145,7 +145,7 @@ mod tests {
assert_response(res, &post);
LocalSite::delete(pool).await?;
Instance::delete(pool, instance.id).await?;
data.delete(pool).await?;
Ok(())
}

View file

@ -25,8 +25,9 @@ use lemmy_db_schema_file::enums::CommunityFollowerState;
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_site::api::SuccessResponse;
use lemmy_utils::{
error::{LemmyErrorType, LemmyResult, MAX_API_PARAM_ELEMENTS},
error::LemmyResult,
spawn_try_task,
utils::validation::check_api_elements_count,
};
use serde::{Deserialize, Serialize};
use std::future::Future;
@ -145,9 +146,7 @@ pub async fn import_settings(
+ data.blocked_instances.len()
+ data.saved_posts.len()
+ data.saved_comments.len();
if url_count > MAX_API_PARAM_ELEMENTS {
Err(LemmyErrorType::TooManyItems)?;
}
check_api_elements_count(url_count)?;
spawn_try_task(async move {
let person_id = local_user_view.person.id;
@ -279,14 +278,13 @@ pub(crate) mod tests {
use lemmy_db_schema::{
source::{
community::{Community, CommunityActions, CommunityFollowerForm, CommunityInsertForm},
instance::Instance,
person::Person,
},
test_data::TestData,
traits::{Crud, Followable},
};
use lemmy_db_views_community_follower::CommunityFollowerView;
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_site::impls::create_test_instance;
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
use serial_test::serial;
use std::time::Duration;
@ -297,7 +295,7 @@ pub(crate) mod tests {
async fn test_settings_export_import() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let pool = &mut context.pool();
let instance = create_test_instance(pool).await?;
let data = TestData::create(pool).await?;
let export_user = LocalUserView::create_test_user(pool, "hanna", "my bio", false).await?;
@ -339,7 +337,7 @@ pub(crate) mod tests {
Person::delete(pool, export_user.person.id).await?;
Person::delete(pool, import_user.person.id).await?;
Instance::delete(&mut context.pool(), instance.id).await?;
data.delete(&mut context.pool()).await?;
Ok(())
}
@ -348,7 +346,7 @@ pub(crate) mod tests {
async fn disallow_large_backup() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let pool = &mut context.pool();
let instance = create_test_instance(pool).await?;
let data = TestData::create(pool).await?;
let export_user = LocalUserView::create_test_user(pool, "harry", "harry bio", false).await?;
@ -376,7 +374,7 @@ pub(crate) mod tests {
Person::delete(pool, export_user.person.id).await?;
Person::delete(pool, import_user.person.id).await?;
Instance::delete(&mut context.pool(), instance.id).await?;
data.delete(&mut context.pool()).await?;
Ok(())
}
@ -385,7 +383,7 @@ pub(crate) mod tests {
async fn import_partial_backup() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let pool = &mut context.pool();
let instance = create_test_instance(pool).await?;
let data = TestData::create(pool).await?;
let import_user = LocalUserView::create_test_user(pool, "larry", "larry bio", false).await?;
@ -401,7 +399,7 @@ pub(crate) mod tests {
// local_user can be deserialized without id/person_id fields
assert_eq!("my_theme", import_user_updated.local_user.theme);
Instance::delete(&mut context.pool(), instance.id).await?;
data.delete(&mut context.pool()).await?;
Ok(())
}
}

View file

@ -4,9 +4,10 @@ use activitypub_federation::{
traits::{Actor, Object},
};
use diesel::NotFound;
use either::Either::*;
use itertools::Itertools;
use lemmy_api_utils::context::LemmyContext;
use lemmy_apub_objects::objects::{SiteOrCommunityOrUser, UserOrCommunity};
use lemmy_apub_objects::objects::SiteOrMultiOrCommunityOrUser;
use lemmy_db_schema::{newtypes::InstanceId, traits::ApubActor};
use lemmy_db_views_local_user::LocalUserView;
use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult};
@ -65,10 +66,11 @@ where
}
}
pub(crate) fn get_instance_id(s: &SiteOrCommunityOrUser) -> InstanceId {
pub(crate) fn get_instance_id(s: &SiteOrMultiOrCommunityOrUser) -> InstanceId {
match s {
SiteOrCommunityOrUser::Left(s) => s.instance_id,
SiteOrCommunityOrUser::Right(UserOrCommunity::Left(u)) => u.instance_id,
SiteOrCommunityOrUser::Right(UserOrCommunity::Right(c)) => c.instance_id,
Left(Left(s)) => s.instance_id,
Left(Right(m)) => m.instance_id,
Right(Left(u)) => u.instance_id,
Right(Right(c)) => c.instance_id,
}
}

View file

@ -2,6 +2,7 @@ use activitypub_federation::{
config::Data,
fetch::{object_id::ObjectId, webfinger::webfinger_resolve_actor},
};
use either::Either::*;
use lemmy_api_utils::context::LemmyContext;
use lemmy_apub_objects::objects::{SearchableObjects, UserOrCommunity};
use lemmy_utils::error::LemmyResult;
@ -24,9 +25,9 @@ pub(crate) async fn search_query_to_object_id(
if query.starts_with('!') || query.starts_with('@') {
query.remove(0);
}
SearchableObjects::Right(
Left(Right(
webfinger_resolve_actor::<LemmyContext, UserOrCommunity>(&query, context).await?,
)
))
}
})
}

View file

@ -21,8 +21,16 @@ use actix_web::{
HttpResponse,
};
use lemmy_api_utils::context::LemmyContext;
use lemmy_apub_objects::objects::{community::ApubCommunity, SiteOrCommunityOrUser};
use lemmy_db_schema::{source::community::Community, traits::ApubActor};
use lemmy_apub_objects::objects::{
community::ApubCommunity,
multi_community::ApubMultiCommunity,
multi_community_collection::ApubFeedCollection,
SiteOrMultiOrCommunityOrUser,
};
use lemmy_db_schema::{
source::{community::Community, multi_community::MultiCommunity},
traits::ApubActor,
};
use lemmy_db_schema_file::enums::CommunityVisibility;
use lemmy_db_views_community_follower::CommunityFollowerView;
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
@ -35,7 +43,7 @@ pub(crate) struct CommunityPath {
#[derive(Deserialize, Clone)]
pub struct CommunityIsFollowerQuery {
is_follower: Option<ObjectId<SiteOrCommunityOrUser>>,
is_follower: Option<ObjectId<SiteOrMultiOrCommunityOrUser>>,
}
/// Return the ActivityPub json representation of a local community over HTTP.
@ -79,7 +87,7 @@ pub(crate) async fn get_apub_community_followers(
/// Checks if a given actor follows the private community. Returns status 200 if true.
async fn check_is_follower(
community: Community,
is_follower: &ObjectId<SiteOrCommunityOrUser>,
is_follower: &ObjectId<SiteOrMultiOrCommunityOrUser>,
context: Data<LemmyContext>,
request: HttpRequest,
) -> LemmyResult<HttpResponse> {
@ -87,7 +95,8 @@ async fn check_is_follower(
return Ok(HttpResponse::BadRequest().body("must be a private community"));
}
// also check for http sig so that followers are not exposed publicly
let signing_actor = signing_actor::<SiteOrCommunityOrUser>(&request, None, &context).await?;
let signing_actor =
signing_actor::<SiteOrMultiOrCommunityOrUser>(&request, None, &context).await?;
CommunityFollowerView::check_has_followers_from_instance(
community.id,
get_instance_id(&signing_actor),
@ -156,6 +165,35 @@ pub(crate) async fn get_apub_community_featured(
create_apub_response(&featured)
}
#[derive(Deserialize)]
pub(crate) struct MultiCommunityQuery {
multi_name: String,
}
pub(crate) async fn get_apub_person_multi_community(
query: Path<MultiCommunityQuery>,
context: Data<LemmyContext>,
) -> LemmyResult<HttpResponse> {
let multi: ApubMultiCommunity =
MultiCommunity::read_from_name(&mut context.pool(), &query.multi_name)
.await?
.into();
create_apub_response(&multi.into_json(&context).await?)
}
pub(crate) async fn get_apub_person_multi_community_follows(
query: Path<MultiCommunityQuery>,
context: Data<LemmyContext>,
) -> LemmyResult<HttpResponse> {
let multi = MultiCommunity::read_from_name(&mut context.pool(), &query.multi_name)
.await?
.into();
let collection = ApubFeedCollection::read_local(&multi, &context).await?;
create_apub_response(&collection)
}
#[cfg(test)]
pub(crate) mod tests {
@ -163,16 +201,12 @@ pub(crate) mod tests {
use actix_web::{body::to_bytes, test::TestRequest};
use lemmy_apub_objects::protocol::{group::Group, tombstone::Tombstone};
use lemmy_db_schema::{
newtypes::InstanceId,
source::{
community::CommunityInsertForm,
instance::Instance,
local_site::{LocalSite, LocalSiteInsertForm},
local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitInsertForm},
person::{Person, PersonInsertForm},
post::{Post, PostInsertForm},
site::{Site, SiteInsertForm},
},
test_data::TestData,
traits::Crud,
};
use serde::de::DeserializeOwned;
@ -182,16 +216,14 @@ pub(crate) mod tests {
deleted: bool,
visibility: CommunityVisibility,
context: &Data<LemmyContext>,
) -> LemmyResult<(Instance, Community, Path<CommunityPath>)> {
let instance =
Instance::read_or_create(&mut context.pool(), "my_domain.tld".to_string()).await?;
create_local_site(context, instance.id).await?;
) -> LemmyResult<(TestData, Community, Path<CommunityPath>)> {
let data = TestData::create(&mut context.pool()).await?;
let community_form = CommunityInsertForm {
deleted: Some(deleted),
visibility: Some(visibility),
..CommunityInsertForm::new(
instance.id,
data.instance.id,
"testcom6".to_string(),
"nada".to_owned(),
"pubkey".to_string(),
@ -202,24 +234,7 @@ pub(crate) mod tests {
community_name: community.name.clone(),
}
.into();
Ok((instance, community, path))
}
/// Necessary for the community outbox fetching
async fn create_local_site(
context: &Data<LemmyContext>,
instance_id: InstanceId,
) -> LemmyResult<()> {
// Create a local site, since this is necessary for community fetching.
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::new(site.id);
let local_site = LocalSite::create(&mut context.pool(), &local_site_form).await?;
let local_site_rate_limit_form = LocalSiteRateLimitInsertForm::new(local_site.id);
LocalSiteRateLimit::create(&mut context.pool(), &local_site_rate_limit_form).await?;
Ok(())
Ok((data, community, path))
}
async fn decode_response<T: DeserializeOwned>(res: HttpResponse) -> LemmyResult<T> {
@ -232,7 +247,7 @@ pub(crate) mod tests {
#[serial]
async fn test_get_community() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let (instance, community, path) = init(false, CommunityVisibility::Public, &context).await?;
let (data, community, path) = init(false, CommunityVisibility::Public, &context).await?;
let request = TestRequest::default().to_http_request();
// fetch invalid community
@ -263,7 +278,7 @@ pub(crate) mod tests {
let res = get_apub_community_outbox(path, context.clone(), request).await?;
assert_eq!(200, res.status());
Instance::delete(&mut context.pool(), instance.id).await?;
data.delete(&mut context.pool()).await?;
Ok(())
}
@ -271,7 +286,7 @@ pub(crate) mod tests {
#[serial]
async fn test_get_deleted_community() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let (instance, _, path) = init(true, CommunityVisibility::Public, &context).await?;
let (data, _, path) = init(true, CommunityVisibility::Public, &context).await?;
let request = TestRequest::default().to_http_request();
// should return tombstone
@ -294,7 +309,7 @@ pub(crate) mod tests {
assert!(res.is_err());
//Community::delete(&mut context.pool(), community.id).await?;
Instance::delete(&mut context.pool(), instance.id).await?;
data.delete(&mut context.pool()).await?;
Ok(())
}
@ -302,7 +317,7 @@ pub(crate) mod tests {
#[serial]
async fn test_get_local_only_community() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let (instance, _, path) = init(false, CommunityVisibility::LocalOnlyPrivate, &context).await?;
let (data, _, path) = init(false, CommunityVisibility::LocalOnlyPrivate, &context).await?;
let request = TestRequest::default().to_http_request();
let res = get_apub_community_http(path.clone().into(), context.clone()).await;
@ -320,7 +335,7 @@ pub(crate) mod tests {
let res = get_apub_community_outbox(path, context.clone(), request).await;
assert!(res.is_err());
Instance::delete(&mut context.pool(), instance.id).await?;
data.delete(&mut context.pool()).await?;
Ok(())
}
@ -328,11 +343,11 @@ pub(crate) mod tests {
#[serial]
async fn test_outbox_deleted_user() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let (instance, community, path) = init(false, CommunityVisibility::Public, &context).await?;
let (data, community, path) = init(false, CommunityVisibility::Public, &context).await?;
let request = TestRequest::default().to_http_request();
// post from deleted user shouldnt break outbox
let mut form = PersonInsertForm::new("jerry".to_string(), String::new(), instance.id);
let mut form = PersonInsertForm::new("jerry".to_string(), String::new(), data.instance.id);
form.deleted = Some(true);
let person = Person::create(&mut context.pool(), &form).await?;
@ -342,7 +357,7 @@ pub(crate) mod tests {
let res = get_apub_community_outbox(path, context.clone(), request).await?;
assert_eq!(200, res.status());
Instance::delete(&mut context.pool(), instance.id).await?;
data.delete(&mut context.pool()).await?;
Ok(())
}
}

View file

@ -9,7 +9,7 @@ use activitypub_federation::{
use actix_web::{web, web::Bytes, HttpRequest, HttpResponse};
use lemmy_api_utils::context::LemmyContext;
use lemmy_apub_objects::{
objects::{SiteOrCommunityOrUser, UserOrCommunity},
objects::{SiteOrMultiOrCommunityOrUser, UserOrCommunity},
protocol::tombstone::Tombstone,
};
use lemmy_db_schema::{
@ -139,7 +139,8 @@ async fn check_community_content_fetchable(
match community.visibility {
Public | Unlisted => Ok(()),
Private => {
let signing_actor = signing_actor::<SiteOrCommunityOrUser>(request, None, context).await?;
let signing_actor =
signing_actor::<SiteOrMultiOrCommunityOrUser>(request, None, context).await?;
if community.local {
Ok(
CommunityFollowerView::check_has_followers_from_instance(

View file

@ -3,7 +3,7 @@ use crate::{
protocol::collections::empty_outbox::EmptyOutbox,
};
use activitypub_federation::{config::Data, traits::Object};
use actix_web::{web, HttpResponse};
use actix_web::{web::Path, HttpResponse};
use lemmy_api_utils::{context::LemmyContext, utils::generate_outbox_url};
use lemmy_apub_objects::objects::person::ApubPerson;
use lemmy_db_schema::{source::person::Person, traits::ApubActor};
@ -17,7 +17,7 @@ pub struct PersonQuery {
/// Return the ActivityPub json representation of a local person over HTTP.
pub(crate) async fn get_apub_person_http(
info: web::Path<PersonQuery>,
info: Path<PersonQuery>,
context: Data<LemmyContext>,
) -> LemmyResult<HttpResponse> {
let user_name = info.into_inner().user_name;
@ -37,7 +37,7 @@ pub(crate) async fn get_apub_person_http(
}
pub(crate) async fn get_apub_person_outbox(
info: web::Path<PersonQuery>,
info: Path<PersonQuery>,
context: Data<LemmyContext>,
) -> LemmyResult<HttpResponse> {
let person = Person::read_from_name(&mut context.pool(), &info.user_name, false)

View file

@ -6,6 +6,8 @@ use crate::http::{
get_apub_community_http,
get_apub_community_moderators,
get_apub_community_outbox,
get_apub_person_multi_community,
get_apub_person_multi_community_follows,
},
get_activity,
person::{get_apub_person_http, get_apub_person_outbox},
@ -48,6 +50,14 @@ pub fn config(cfg: &mut web::ServiceConfig) {
"/u/{user_name}/outbox",
web::get().to(get_apub_person_outbox),
)
.route(
"/m/{multi_name}",
web::get().to(get_apub_person_multi_community),
)
.route(
"/m/{multi_name}/following",
web::get().to(get_apub_person_multi_community_follows),
)
.route("/post/{post_id}", web::get().to(get_apub_post))
.route("/comment/{comment_id}", web::get().to(get_apub_comment))
.route("/activities/{type_}/{id}", web::get().to(get_activity));

View file

@ -15,7 +15,7 @@ mod tests {
collection_remove::CollectionRemove,
lock_page::{LockPage, UndoLockPage},
report::Report,
update::UpdateCommunity,
update::Update,
};
use lemmy_apub_objects::utils::test::test_parse_lemmy_item;
use lemmy_utils::error::LemmyResult;
@ -39,9 +39,7 @@ mod tests {
test_parse_lemmy_item::<LockPage>("assets/lemmy/activities/community/lock_page.json")?;
test_parse_lemmy_item::<UndoLockPage>("assets/lemmy/activities/community/undo_lock_page.json")?;
test_parse_lemmy_item::<UpdateCommunity>(
"assets/lemmy/activities/community/update_community.json",
)?;
test_parse_lemmy_item::<Update>("assets/lemmy/activities/community/update_community.json")?;
test_parse_lemmy_item::<Report>("assets/lemmy/activities/community/report_page.json")?;
test_parse_lemmy_item::<ResolveReport>(

View file

@ -4,13 +4,14 @@ use activitypub_federation::{
kinds::activity::UpdateType,
protocol::helpers::deserialize_one_or_many,
};
use either::Either;
use lemmy_api_utils::context::LemmyContext;
use lemmy_apub_objects::{
objects::{community::ApubCommunity, person::ApubPerson},
protocol::group::Group,
protocol::{group::Group, multi_community::Feed},
utils::protocol::InCommunity,
};
use lemmy_utils::error::LemmyResult;
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
use serde::{Deserialize, Serialize};
use url::Url;
@ -18,12 +19,12 @@ use url::Url;
/// fields of a local community.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateCommunity {
pub struct Update {
pub(crate) actor: ObjectId<ApubPerson>,
#[serde(deserialize_with = "deserialize_one_or_many")]
pub(crate) to: Vec<Url>,
// TODO: would be nice to use a separate struct here, which only contains the fields updated here
pub(crate) object: Box<Group>,
#[serde(with = "either::serde_untagged")]
pub(crate) object: Either<Group, Feed>,
#[serde(deserialize_with = "deserialize_one_or_many")]
pub(crate) cc: Vec<Url>,
#[serde(rename = "type")]
@ -31,9 +32,14 @@ pub struct UpdateCommunity {
pub(crate) id: Url,
}
impl InCommunity for UpdateCommunity {
impl InCommunity for Update {
async fn community(&self, context: &Data<LemmyContext>) -> LemmyResult<ApubCommunity> {
let community: ApubCommunity = self.object.id.clone().dereference(context).await?;
Ok(community)
match &self.object {
Either::Left(c) => {
let community: ApubCommunity = c.id.clone().dereference(context).await?;
Ok(community)
}
Either::Right(_) => Err(LemmyErrorType::NotFound.into()),
}
}
}

View file

@ -3,7 +3,7 @@ use activitypub_federation::{
kinds::activity::FollowType,
protocol::helpers::deserialize_skip_error,
};
use lemmy_apub_objects::objects::{person::ApubPerson, UserOrCommunity};
use lemmy_apub_objects::objects::{person::ApubPerson, UserOrCommunityOrMulti};
use serde::{Deserialize, Serialize};
use url::Url;
@ -13,8 +13,8 @@ pub struct Follow {
pub(crate) actor: ObjectId<ApubPerson>,
/// Optional, for compatibility with platforms that always expect recipient field
#[serde(deserialize_with = "deserialize_skip_error", default)]
pub(crate) to: Option<[ObjectId<UserOrCommunity>; 1]>,
pub(crate) object: ObjectId<UserOrCommunity>,
pub(crate) to: Option<[ObjectId<UserOrCommunityOrMulti>; 1]>,
pub(crate) object: ObjectId<UserOrCommunityOrMulti>,
#[serde(rename = "type")]
pub(crate) kind: FollowType,
pub(crate) id: Url,

View file

@ -241,8 +241,10 @@ pub(crate) mod tests {
};
use assert_json_diff::assert_json_include;
use html2md::parse_html;
use lemmy_db_schema::source::{instance::Instance, local_site::LocalSite, site::Site};
use lemmy_db_views_site::impls::create_test_instance;
use lemmy_db_schema::{
source::{local_site::LocalSite, site::Site},
test_data::TestData,
};
use pretty_assertions::assert_eq;
use serial_test::serial;
@ -276,7 +278,7 @@ pub(crate) mod tests {
#[serial]
pub(crate) async fn test_parse_lemmy_comment() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let instance = create_test_instance(&mut context.pool()).await?;
let test_data = TestData::create(&mut context.pool()).await?;
let url = Url::parse("https://enterprise.lemmy.ml/comment/38741")?;
let data = prepare_comment_test(&url, &context).await?;
@ -295,7 +297,7 @@ pub(crate) mod tests {
Comment::delete(&mut context.pool(), comment_id).await?;
cleanup(data, &context).await?;
Instance::delete(&mut context.pool(), instance.id).await?;
test_data.delete(&mut context.pool()).await?;
Ok(())
}
@ -303,7 +305,7 @@ pub(crate) mod tests {
#[serial]
async fn test_parse_pleroma_comment() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let instance = create_test_instance(&mut context.pool()).await?;
let test_data = TestData::create(&mut context.pool()).await?;
let url = Url::parse("https://enterprise.lemmy.ml/comment/38741")?;
let data = prepare_comment_test(&url, &context).await?;
@ -323,7 +325,7 @@ pub(crate) mod tests {
Comment::delete(&mut context.pool(), comment.id).await?;
cleanup(data, &context).await?;
Instance::delete(&mut context.pool(), instance.id).await?;
test_data.delete(&mut context.pool()).await?;
Ok(())
}

View file

@ -48,7 +48,7 @@ use tracing::debug;
use url::Url;
#[derive(Clone, Debug)]
pub struct ApubSite(Site);
pub struct ApubSite(pub Site);
impl Deref for ApubSite {
type Target = Site;

View file

@ -1,6 +1,8 @@
pub mod comment;
pub mod community;
pub mod instance;
pub mod multi_community;
pub mod multi_community_collection;
pub mod person;
pub mod post;
pub mod private_message;
@ -9,15 +11,23 @@ use comment::ApubComment;
use community::ApubCommunity;
use either::Either;
use instance::ApubSite;
use multi_community::ApubMultiCommunity;
use person::ApubPerson;
use post::ApubPost;
// TODO: some of these are redundant?
pub type PostOrComment = Either<ApubPost, ApubComment>;
pub type SearchableObjects = Either<Either<PostOrComment, UserOrCommunity>, ApubMultiCommunity>;
pub type ReportableObjects = Either<PostOrComment, ApubCommunity>;
pub type SearchableObjects = Either<PostOrComment, UserOrCommunity>;
pub type UserOrCommunity = Either<ApubPerson, ApubCommunity>;
pub type SiteOrCommunityOrUser = Either<ApubSite, UserOrCommunity>;
pub type SiteOrMultiOrCommunityOrUser =
Either<Either<ApubSite, ApubMultiCommunity>, UserOrCommunity>;
pub type CommunityOrMulti = Either<ApubCommunity, ApubMultiCommunity>;
pub type UserOrCommunityOrMulti = Either<ApubPerson, CommunityOrMulti>;

View file

@ -0,0 +1,143 @@
use crate::{objects::ApubSite, protocol::multi_community::Feed, utils::functions::GetActorType};
use activitypub_federation::{
config::Data,
protocol::verification::verify_domains_match,
traits::{Actor, Object},
};
use chrono::{DateTime, Utc};
use lemmy_api_utils::context::LemmyContext;
use lemmy_db_schema::{
sensitive::SensitiveString,
source::{
multi_community::{MultiCommunity, MultiCommunityInsertForm},
person::Person,
},
traits::Crud,
};
use lemmy_db_schema_file::enums::ActorType;
use lemmy_db_views_site::SiteView;
use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult};
use std::ops::Deref;
use url::Url;
#[derive(Clone, Debug)]
pub struct ApubMultiCommunity(MultiCommunity);
impl Deref for ApubMultiCommunity {
type Target = MultiCommunity;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<MultiCommunity> for ApubMultiCommunity {
fn from(m: MultiCommunity) -> Self {
ApubMultiCommunity(m)
}
}
#[async_trait::async_trait]
impl Object for ApubMultiCommunity {
type DataType = LemmyContext;
type Kind = Feed;
type Error = LemmyError;
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
Some(self.last_refreshed_at)
}
async fn read_from_id(
object_id: Url,
context: &Data<Self::DataType>,
) -> LemmyResult<Option<Self>> {
Ok(
MultiCommunity::read_from_ap_id(&mut context.pool(), &object_id.into())
.await?
.map(Into::into),
)
}
async fn delete(self, _context: &Data<Self::DataType>) -> LemmyResult<()> {
Err(LemmyErrorType::NotFound.into())
}
async fn into_json(self, context: &Data<Self::DataType>) -> LemmyResult<Self::Kind> {
let site_view = SiteView::read_local(&mut context.pool()).await?;
let site = ApubSite(site_view.site.clone());
let creator = Person::read(&mut context.pool(), self.creator_id).await?;
Ok(Feed {
r#type: Default::default(),
id: self.ap_id.clone().into(),
inbox: site_view.site.inbox_url.into(),
// reusing pubkey from site instead of generating new one
public_key: site.public_key(),
following: self.following_url.clone().into(),
name: self.name.clone(),
summary: self.title.clone(),
content: self.description.clone(),
attributed_to: creator.ap_id.into(),
})
}
async fn verify(
json: &Self::Kind,
expected_domain: &Url,
_context: &Data<LemmyContext>,
) -> LemmyResult<()> {
verify_domains_match(expected_domain, json.id.inner())?;
Ok(())
}
async fn from_json(json: Self::Kind, context: &Data<LemmyContext>) -> LemmyResult<Self> {
let creator = json.attributed_to.dereference(context).await?;
let form = MultiCommunityInsertForm {
creator_id: creator.id,
instance_id: creator.instance_id,
name: json.name,
ap_id: Some(json.id.into()),
local: Some(false),
title: json.summary,
description: json.content,
public_key: json.public_key.public_key_pem,
private_key: None,
inbox_url: Some(json.inbox.into()),
following_url: Some(json.following.clone().into()),
last_refreshed_at: Some(Utc::now()),
};
let multi = MultiCommunity::upsert(&mut context.pool(), &form)
.await?
.into();
json.following.dereference(&multi, context).await?;
Ok(multi)
}
}
impl Actor for ApubMultiCommunity {
fn id(&self) -> Url {
self.ap_id.inner().clone()
}
fn public_key_pem(&self) -> &str {
&self.public_key
}
fn private_key_pem(&self) -> Option<String> {
self.private_key.clone().map(SensitiveString::into_inner)
}
fn inbox(&self) -> Url {
self.inbox_url.clone().into()
}
fn shared_inbox(&self) -> Option<Url> {
None
}
}
impl GetActorType for ApubMultiCommunity {
fn actor_type(&self) -> ActorType {
ActorType::MultiCommunity
}
}

View file

@ -0,0 +1,113 @@
use super::multi_community::ApubMultiCommunity;
use crate::protocol::multi_community::FeedCollection;
use activitypub_federation::{
config::Data,
protocol::verification::verify_domains_match,
traits::Collection,
};
use futures::future::join_all;
use lemmy_api_utils::{
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
};
use lemmy_db_schema::{
newtypes::CommunityId,
source::{
community::{CommunityActions, CommunityFollowerForm},
multi_community::MultiCommunity,
},
traits::Followable,
};
use lemmy_db_schema_file::enums::CommunityFollowerState;
use lemmy_db_views_site::SiteView;
use lemmy_utils::error::{LemmyError, LemmyResult};
use tracing::info;
use url::Url;
pub struct ApubFeedCollection;
#[async_trait::async_trait]
impl Collection for ApubFeedCollection {
type DataType = LemmyContext;
type Kind = FeedCollection;
type Owner = ApubMultiCommunity;
type Error = LemmyError;
async fn read_local(
owner: &Self::Owner,
context: &Data<Self::DataType>,
) -> Result<Self::Kind, Self::Error> {
let entries = MultiCommunity::read_entry_ap_ids(&mut context.pool(), &owner.name).await?;
Ok(Self::Kind {
r#type: Default::default(),
id: owner.following_url.clone().into(),
total_items: entries.len().try_into()?,
items: entries.into_iter().map(Into::into).collect(),
})
}
async fn verify(
json: &Self::Kind,
expected_domain: &Url,
_context: &Data<LemmyContext>,
) -> LemmyResult<()> {
verify_domains_match(expected_domain, &json.id.clone().into())?;
Ok(())
}
async fn from_json(
json: Self::Kind,
owner: &Self::Owner,
context: &Data<LemmyContext>,
) -> LemmyResult<Self> {
let communities = join_all(
json
.items
.into_iter()
.map(|ap_id| async move { Ok(ap_id.dereference(context).await?.id) }),
)
.await
.into_iter()
.flat_map(|c: LemmyResult<CommunityId>| match c {
Ok(c) => Some(c),
Err(e) => {
info!("Failed to fetch multi-community item: {e}");
None
}
})
.collect();
let (remote_added, remote_removed, has_local_followers) =
MultiCommunity::update_entries(&mut context.pool(), owner.id, &communities).await?;
// Have multi-comm follower bot follow all communities which were added to multi-comm,
// and unfollow those that were removed.
// If the multi-comm has no local followers its ignored.
// TODO: This means there will be posts missing in multi-comm without local followers.
if has_local_followers {
let multicomm_follower = SiteView::read_multicomm_follower(&mut context.pool()).await?;
for community in remote_added {
let form = CommunityFollowerForm::new(
community.id,
multicomm_follower.id,
CommunityFollowerState::Pending,
);
CommunityActions::follow(&mut context.pool(), &form).await?;
ActivityChannel::submit_activity(
SendActivityData::FollowCommunity(community.clone(), multicomm_follower.clone(), true),
context,
)?;
}
for community in remote_removed {
CommunityActions::unfollow(&mut context.pool(), multicomm_follower.id, community.id)
.await?;
ActivityChannel::submit_activity(
SendActivityData::FollowCommunity(community.clone(), multicomm_follower.clone(), false),
context,
)?;
}
}
Ok(ApubFeedCollection)
}
}

View file

@ -174,8 +174,7 @@ mod tests {
utils::test::{file_to_json_object, parse_lemmy_instance},
};
use assert_json_diff::assert_json_include;
use lemmy_db_schema::source::site::Site;
use lemmy_db_views_site::impls::create_test_instance;
use lemmy_db_schema::{source::site::Site, test_data::TestData};
use pretty_assertions::assert_eq;
use serial_test::serial;
@ -209,7 +208,7 @@ mod tests {
#[serial]
async fn test_parse_lemmy_pm() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let instance = create_test_instance(&mut context.pool()).await?;
let test_data = TestData::create(&mut context.pool()).await?;
let url = Url::parse("https://enterprise.lemmy.ml/private_message/1621")?;
let data = prepare_comment_test(&url, &context).await?;
let json: PrivateMessage =
@ -227,7 +226,7 @@ mod tests {
DbPrivateMessage::delete(&mut context.pool(), pm_id).await?;
cleanup(data, &context).await?;
Instance::delete(&mut context.pool(), instance.id).await?;
test_data.delete(&mut context.pool()).await?;
Ok(())
}
@ -235,7 +234,7 @@ mod tests {
#[serial]
async fn test_parse_pleroma_pm() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let instance = create_test_instance(&mut context.pool()).await?;
let test_data = TestData::create(&mut context.pool()).await?;
let url = Url::parse("https://enterprise.lemmy.ml/private_message/1621")?;
let data = prepare_comment_test(&url, &context).await?;
let pleroma_url = Url::parse("https://queer.hacktivis.me/objects/2")?;
@ -249,7 +248,7 @@ mod tests {
DbPrivateMessage::delete(&mut context.pool(), pm.id).await?;
cleanup(data, &context).await?;
Instance::delete(&mut context.pool(), instance.id).await?;
test_data.delete(&mut context.pool()).await?;
Ok(())
}
}

View file

@ -1,5 +1,6 @@
pub mod group;
pub mod instance;
pub mod multi_community;
pub mod note;
pub mod page;
pub mod person;

View file

@ -0,0 +1,43 @@
use crate::objects::{
community::ApubCommunity,
multi_community::ApubMultiCommunity,
multi_community_collection::ApubFeedCollection,
person::ApubPerson,
};
use activitypub_federation::{
fetch::{collection_id::CollectionId, object_id::ObjectId},
kinds::collection::CollectionType,
protocol::public_key::PublicKey,
};
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Feed {
pub r#type: FeedType,
pub id: ObjectId<ApubMultiCommunity>,
pub inbox: Url,
pub public_key: PublicKey,
pub following: CollectionId<ApubFeedCollection>,
pub name: String,
pub summary: Option<String>,
pub content: Option<String>,
pub attributed_to: ObjectId<ApubPerson>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
pub enum FeedType {
#[default]
Feed,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FeedCollection {
pub r#type: CollectionType,
pub id: CollectionId<ApubFeedCollection>,
pub total_items: i32,
pub items: Vec<ObjectId<ApubCommunity>>,
}

View file

@ -189,6 +189,15 @@ pub trait GetActorType {
fn actor_type(&self) -> ActorType;
}
impl<L: GetActorType, R: GetActorType> GetActorType for either::Either<L, R> {
fn actor_type(&self) -> ActorType {
match self {
Either::Right(r) => r.actor_type(),
Either::Left(l) => l.actor_type(),
}
}
}
pub async fn handle_community_moderators(
new_mods: &Vec<ObjectId<ApubPerson>>,
community: &ApubCommunity,

View file

@ -1,5 +1,6 @@
use crate::objects::{PostOrComment, SearchableObjects, UserOrCommunity};
use crate::objects::SearchableObjects;
use activitypub_federation::{config::Data, fetch::object_id::ObjectId};
use either::Either::*;
use lemmy_api_utils::context::LemmyContext;
use lemmy_db_schema::traits::ApubActor;
use lemmy_utils::utils::markdown::image_links::{markdown_find_links, markdown_handle_title};
@ -54,18 +55,14 @@ pub(crate) async fn to_local_url(url: &str, context: &Data<LemmyContext>) -> Opt
}
let dereferenced = object_id.dereference_local(context).await.ok()?;
match dereferenced {
SearchableObjects::Left(pc) => match pc {
PostOrComment::Left(post) => post.local_url(context.settings()),
PostOrComment::Right(comment) => comment.local_url(context.settings()),
}
.ok()
.map(Into::into),
SearchableObjects::Right(pc) => match pc {
UserOrCommunity::Left(user) => user.actor_url(context.settings()),
UserOrCommunity::Right(community) => community.actor_url(context.settings()),
}
.ok(),
Left(Left(Left(post))) => post.local_url(context.settings()),
Left(Left(Right(comment))) => comment.local_url(context.settings()),
Left(Right(Left(user))) => user.actor_url(context.settings()),
Left(Right(Right(community))) => community.actor_url(context.settings()),
Right(multi) => multi.format_url(context.settings()),
}
.ok()
.map(Into::into)
}
#[cfg(test)]
@ -74,13 +71,12 @@ mod tests {
use lemmy_db_schema::{
source::{
community::{Community, CommunityInsertForm},
instance::Instance,
post::{Post, PostInsertForm},
},
test_data::TestData,
traits::Crud,
};
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_site::impls::create_test_instance;
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq;
use serial_test::serial;
@ -89,17 +85,17 @@ mod tests {
#[tokio::test]
async fn test_markdown_rewrite_remote_links() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let instance = create_test_instance(&mut context.pool()).await?;
let community_form = CommunityInsertForm {
ap_id: Some(Url::parse("https://example.com/c/my_community")?.into()),
..CommunityInsertForm::new(
instance.id,
let data = TestData::create(&mut context.pool()).await?;
let community = Community::create(
&mut context.pool(),
&CommunityInsertForm::new(
data.instance.id,
"my_community".to_string(),
"My Community".to_string(),
"pubkey".to_string(),
)
};
let community = Community::create(&mut context.pool(), &community_form).await?;
),
)
.await?;
let user =
LocalUserView::create_test_user(&mut context.pool(), "garda", "garda bio", false).await?;
@ -120,7 +116,7 @@ mod tests {
(
"rewrite community link",
format!("[link]({})", community.ap_id),
"[link](https://lemmy-alpha/c/my_community@example.com)",
"[link](https://lemmy-alpha/c/my_community@changeme.invalid)",
),
(
"dont rewrite local post link",
@ -155,7 +151,7 @@ mod tests {
);
}
Instance::delete(&mut context.pool(), instance.id).await?;
data.delete(&mut context.pool()).await?;
Ok(())
}

View file

@ -74,6 +74,7 @@ derive-new.workspace = true
tuplex = { workspace = true, optional = true }
moka = { workspace = true, optional = true }
[dev-dependencies]
serial_test = { workspace = true }
pretty_assertions = { workspace = true }

View file

@ -401,12 +401,11 @@ mod tests {
use crate::{
source::{
community::{Community, CommunityInsertForm},
instance::Instance,
local_site::{LocalSite, LocalSiteInsertForm},
local_site::LocalSite,
local_user::{LocalUser, LocalUserInsertForm},
person::{Person, PersonInsertForm},
site::SiteInsertForm,
},
test_data::TestData,
traits::Crud,
utils::build_db_pool_for_tests,
};
@ -427,19 +426,6 @@ mod tests {
])
}
async fn create_test_site(pool: &mut DbPool<'_>) -> LemmyResult<(Site, Instance)> {
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let site_form = SiteInsertForm::new("test site".to_string(), inserted_instance.id);
let site = Site::create(pool, &site_form).await?;
// Create a local site, since this is necessary for local languages
let local_site_form = LocalSiteInsertForm::new(site.id);
LocalSite::create(pool, &local_site_form).await?;
Ok((site, inserted_instance))
}
#[tokio::test]
#[serial]
async fn test_convert_update_languages() -> LemmyResult<()> {
@ -485,21 +471,19 @@ mod tests {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let (site, instance) = create_test_site(pool).await?;
let data = TestData::create(pool).await?;
let site_languages1 = SiteLanguage::read_local_raw(pool).await?;
// site is created with all languages
assert_eq!(184, site_languages1.len());
let test_langs = test_langs1(pool).await?;
SiteLanguage::update(pool, test_langs.clone(), &site).await?;
SiteLanguage::update(pool, test_langs.clone(), &data.site).await?;
let site_languages2 = SiteLanguage::read_local_raw(pool).await?;
// after update, site only has new languages
assert_eq!(test_langs, site_languages2);
Site::delete(pool, site.id).await?;
Instance::delete(pool, instance.id).await?;
LocalSite::delete(pool).await?;
data.delete(pool).await?;
Ok(())
}
@ -510,9 +494,9 @@ mod tests {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let (site, instance) = create_test_site(pool).await?;
let data = TestData::create(pool).await?;
let person_form = PersonInsertForm::test_form(instance.id, "my test person");
let person_form = PersonInsertForm::test_form(data.instance.id, "my test person");
let person = Person::create(pool, &person_form).await?;
let local_user_form = LocalUserInsertForm::test_form(person.id);
@ -530,9 +514,8 @@ mod tests {
Person::delete(pool, person.id).await?;
LocalUser::delete(pool, local_user.id).await?;
Site::delete(pool, site.id).await?;
LocalSite::delete(pool).await?;
Instance::delete(pool, instance.id).await?;
data.delete(pool).await?;
Ok(())
}
@ -542,11 +525,11 @@ mod tests {
async fn test_community_languages() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let (site, instance) = create_test_site(pool).await?;
let data = TestData::create(pool).await?;
let test_langs = test_langs1(pool).await?;
SiteLanguage::update(pool, test_langs.clone(), &site).await?;
SiteLanguage::update(pool, test_langs.clone(), &data.site).await?;
let read_site_langs = SiteLanguage::read(pool, site.id).await?;
let read_site_langs = SiteLanguage::read(pool, data.site.id).await?;
assert_eq!(test_langs, read_site_langs);
// Test the local ones are the same
@ -554,7 +537,7 @@ mod tests {
assert_eq!(test_langs, read_local_site_langs);
let community_form = CommunityInsertForm::new(
instance.id,
data.instance.id,
"test community".to_string(),
"test community".to_string(),
"pubkey".to_string(),
@ -576,7 +559,7 @@ mod tests {
// limit site languages to en, fi. after this, community languages should be updated to
// intersection of old languages (en, fr, ru) and (en, fi), which is only fi.
SiteLanguage::update(pool, vec![test_langs[0], test_langs2[0]], &site).await?;
SiteLanguage::update(pool, vec![test_langs[0], test_langs2[0]], &data.site).await?;
let community_langs2 = CommunityLanguage::read(pool, community.id).await?;
assert_eq!(vec![test_langs[0]], community_langs2);
@ -586,9 +569,8 @@ mod tests {
assert_eq!(test_langs2, community_langs3);
Community::delete(pool, community.id).await?;
Site::delete(pool, site.id).await?;
LocalSite::delete(pool).await?;
Instance::delete(pool, instance.id).await?;
data.delete(pool).await?;
Ok(())
}
@ -598,12 +580,12 @@ mod tests {
async fn test_validate_post_language() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let (site, instance) = create_test_site(pool).await?;
let data = TestData::create(pool).await?;
let test_langs = test_langs1(pool).await?;
let test_langs2 = test_langs2(pool).await?;
let community_form = CommunityInsertForm::new(
instance.id,
data.instance.id,
"test community".to_string(),
"test community".to_string(),
"pubkey".to_string(),
@ -611,7 +593,7 @@ mod tests {
let community = Community::create(pool, &community_form).await?;
CommunityLanguage::update(pool, test_langs, community.id).await?;
let person_form = PersonInsertForm::test_form(instance.id, "my test person");
let person_form = PersonInsertForm::test_form(data.instance.id, "my test person");
let person = Person::create(pool, &person_form).await?;
let local_user_form = LocalUserInsertForm::test_form(person.id);
let local_user = LocalUser::create(pool, &local_user_form, vec![]).await?;
@ -640,9 +622,8 @@ mod tests {
Person::delete(pool, person.id).await?;
Community::delete(pool, community.id).await?;
LocalUser::delete(pool, local_user.id).await?;
Site::delete(pool, site.id).await?;
LocalSite::delete(pool).await?;
Instance::delete(pool, instance.id).await?;
data.delete(pool).await?;
Ok(())
}

View file

@ -201,9 +201,9 @@ impl Comment {
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)
}
pub fn local_url(&self, settings: &Settings) -> LemmyResult<DbUrl> {
pub fn local_url(&self, settings: &Settings) -> LemmyResult<Url> {
let domain = settings.get_protocol_and_hostname();
Ok(Url::parse(&format!("{domain}/comment/{}", self.id))?.into())
Ok(Url::parse(&format!("{domain}/comment/{}", self.id))?)
}
/// The comment was created locally and sent back, indicating that the community accepted it

View file

@ -111,6 +111,7 @@ impl Joinable for CommunityActions {
}
}
#[derive(Debug)]
pub enum CollectionType {
Moderators,
Featured,

View file

@ -17,16 +17,6 @@ impl LocalSite {
.with_lemmy_type(LemmyErrorType::CouldntCreateSite)
}
/// Only used for tests
#[cfg(test)]
async fn read(pool: &mut DbPool<'_>) -> LemmyResult<Self> {
let conn = &mut get_conn(pool).await?;
local_site::table
.first(conn)
.await
.with_lemmy_type(LemmyErrorType::NotFound)
}
pub async fn update(pool: &mut DbPool<'_>, form: &LocalSiteUpdateForm) -> LemmyResult<Self> {
let conn = &mut get_conn(pool).await?;
diesel::update(local_site::table)
@ -35,6 +25,7 @@ impl LocalSite {
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdateSite)
}
pub async fn delete(pool: &mut DbPool<'_>) -> LemmyResult<usize> {
let conn = &mut get_conn(pool).await?;
diesel::delete(local_site::table)
@ -55,8 +46,9 @@ mod tests {
instance::Instance,
person::{Person, PersonInsertForm},
post::{Post, PostInsertForm},
site::{Site, SiteInsertForm},
site::Site,
},
test_data::TestData,
traits::Crud,
utils::{build_db_pool_for_tests, DbPool},
};
@ -64,23 +56,24 @@ mod tests {
use pretty_assertions::assert_eq;
use serial_test::serial;
async fn read_local_site(pool: &mut DbPool<'_>) -> LemmyResult<LocalSite> {
let conn = &mut get_conn(pool).await?;
local_site::table
.first(conn)
.await
.with_lemmy_type(LemmyErrorType::NotFound)
}
async fn prepare_site_with_community(
pool: &mut DbPool<'_>,
) -> LemmyResult<(Instance, Person, Site, Community)> {
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_site_agg");
) -> LemmyResult<(TestData, Person, Community)> {
let data = TestData::create(pool).await?;
let new_person = PersonInsertForm::test_form(data.instance.id, "thommy_site_agg");
let inserted_person = Person::create(pool, &new_person).await?;
let site_form = SiteInsertForm::new("test_site".into(), inserted_instance.id);
let inserted_site = Site::create(pool, &site_form).await?;
let local_site_form = LocalSiteInsertForm::new(inserted_site.id);
LocalSite::create(pool, &local_site_form).await?;
let new_community = CommunityInsertForm::new(
inserted_instance.id,
data.instance.id,
"TIL_site_agg".into(),
"nada".to_owned(),
"pubkey".to_string(),
@ -88,12 +81,7 @@ mod tests {
let inserted_community = Community::create(pool, &new_community).await?;
Ok((
inserted_instance,
inserted_person,
inserted_site,
inserted_community,
))
Ok((data, inserted_person, inserted_community))
}
#[tokio::test]
@ -102,8 +90,7 @@ mod tests {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let (inserted_instance, inserted_person, inserted_site, inserted_community) =
prepare_site_with_community(pool).await?;
let (data, inserted_person, inserted_community) = prepare_site_with_community(pool).await?;
let new_post = PostInsertForm::new(
"A test post".into(),
@ -132,7 +119,7 @@ mod tests {
let _inserted_child_comment =
Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?;
let site_aggregates_before_delete = LocalSite::read(pool).await?;
let site_aggregates_before_delete = read_local_site(pool).await?;
// TODO: this is unstable, sometimes it returns 0 users, sometimes 1
//assert_eq!(0, site_aggregates_before_delete.users);
@ -142,7 +129,7 @@ mod tests {
// Try a post delete
Post::delete(pool, inserted_post.id).await?;
let site_aggregates_after_post_delete = LocalSite::read(pool).await?;
let site_aggregates_after_post_delete = read_local_site(pool).await?;
assert_eq!(1, site_aggregates_after_post_delete.posts);
assert_eq!(0, site_aggregates_after_post_delete.comments);
@ -155,14 +142,14 @@ mod tests {
assert_eq!(1, community_num_deleted);
// Site should still exist, it can without a site creator.
let after_delete_creator = LocalSite::read(pool).await;
let after_delete_creator = read_local_site(pool).await;
assert!(after_delete_creator.is_ok());
Site::delete(pool, inserted_site.id).await?;
let after_delete_site = LocalSite::read(pool).await;
Site::delete(pool, data.site.id).await?;
let after_delete_site = read_local_site(pool).await;
assert!(after_delete_site.is_err());
Instance::delete(pool, inserted_instance.id).await?;
Instance::delete(pool, data.instance.id).await?;
Ok(())
}
@ -173,10 +160,9 @@ mod tests {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let (inserted_instance, inserted_person, inserted_site, inserted_community) =
prepare_site_with_community(pool).await?;
let (data, inserted_person, inserted_community) = prepare_site_with_community(pool).await?;
let site_aggregates_before = LocalSite::read(pool).await?;
let site_aggregates_before = read_local_site(pool).await?;
assert_eq!(1, site_aggregates_before.communities);
Community::update(
@ -189,7 +175,7 @@ mod tests {
)
.await?;
let site_aggregates_after_delete = LocalSite::read(pool).await?;
let site_aggregates_after_delete = read_local_site(pool).await?;
assert_eq!(0, site_aggregates_after_delete.communities);
Community::update(
@ -212,7 +198,7 @@ mod tests {
)
.await?;
let site_aggregates_after_remove = LocalSite::read(pool).await?;
let site_aggregates_after_remove = read_local_site(pool).await?;
assert_eq!(0, site_aggregates_after_remove.communities);
Community::update(
@ -225,13 +211,12 @@ mod tests {
)
.await?;
let site_aggregates_after_remove_delete = LocalSite::read(pool).await?;
let site_aggregates_after_remove_delete = read_local_site(pool).await?;
assert_eq!(0, site_aggregates_after_remove_delete.communities);
Community::delete(pool, inserted_community.id).await?;
Site::delete(pool, inserted_site.id).await?;
Person::delete(pool, inserted_person.id).await?;
Instance::delete(pool, inserted_instance.id).await?;
data.delete(pool).await?;
Ok(())
}

View file

@ -21,6 +21,7 @@ pub mod local_site_url_blocklist;
pub mod local_user;
pub mod login_token;
pub mod mod_log;
pub mod multi_community;
pub mod oauth_account;
pub mod oauth_provider;
pub mod password_reset_request;

View file

@ -0,0 +1,383 @@
use crate::{
diesel::{BoolExpressionMethods, OptionalExtension, PgExpressionMethods},
newtypes::{CommunityId, DbUrl, MultiCommunityId, PersonId},
source::{
community::Community,
multi_community::{
MultiCommunity,
MultiCommunityFollow,
MultiCommunityFollowForm,
MultiCommunityInsertForm,
MultiCommunityUpdateForm,
},
},
traits::Crud,
utils::{format_actor_url, functions::lower, get_conn, DbPool},
};
use diesel::{
dsl::{count, delete, exists, insert_into, not},
select,
update,
ExpressionMethods,
NullableExpressionMethods,
QueryDsl,
};
use diesel_async::RunQueryDsl;
use lemmy_db_schema_file::schema::{
community,
multi_community,
multi_community_entry,
multi_community_follow,
person,
};
use lemmy_utils::{
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
settings::structs::Settings,
};
use url::Url;
const MULTI_COMMUNITY_ENTRY_LIMIT: i8 = 50;
impl Crud for MultiCommunity {
type InsertForm = MultiCommunityInsertForm;
type UpdateForm = MultiCommunityUpdateForm;
type IdType = MultiCommunityId;
async fn create(pool: &mut DbPool<'_>, form: &MultiCommunityInsertForm) -> LemmyResult<Self> {
let conn = &mut get_conn(pool).await?;
Ok(
insert_into(multi_community::table)
.values(form)
.get_result(conn)
.await?,
)
}
async fn update(
pool: &mut DbPool<'_>,
id: MultiCommunityId,
form: &MultiCommunityUpdateForm,
) -> LemmyResult<Self> {
let conn = &mut get_conn(pool).await?;
Ok(
update(multi_community::table.find(id))
.set(form)
.get_result(conn)
.await?,
)
}
}
impl MultiCommunity {
pub async fn read_from_name(pool: &mut DbPool<'_>, multi_name: &str) -> LemmyResult<Self> {
let conn = &mut get_conn(pool).await?;
Ok(
multi_community::table
.filter(multi_community::local.eq(true))
.filter(multi_community::deleted.eq(false))
.filter(lower(multi_community::name).eq(multi_name.to_lowercase()))
.first(conn)
.await?,
)
}
pub async fn read_from_ap_id(pool: &mut DbPool<'_>, ap_id: &DbUrl) -> LemmyResult<Option<Self>> {
let conn = &mut get_conn(pool).await?;
multi_community::table
.filter(multi_community::ap_id.eq(ap_id))
.first(conn)
.await
.optional()
.with_lemmy_type(LemmyErrorType::NotFound)
}
pub async fn create_entry(
pool: &mut DbPool<'_>,
id: MultiCommunityId,
new_community: &Community,
) -> LemmyResult<()> {
let conn = &mut get_conn(pool).await?;
let count: i64 = multi_community::table
.left_join(multi_community_entry::table)
.filter(multi_community::id.eq(id))
.select(count(multi_community_entry::community_id.nullable()))
.first(conn)
.await?;
if count >= MULTI_COMMUNITY_ENTRY_LIMIT.into() {
return Err(LemmyErrorType::MultiCommunityEntryLimitReached.into());
}
insert_into(multi_community_entry::table)
.values((
multi_community_entry::multi_community_id.eq(id),
multi_community_entry::community_id.eq(new_community.id),
))
.execute(conn)
.await?;
Ok(())
}
pub async fn delete_entry(
pool: &mut DbPool<'_>,
id: MultiCommunityId,
old_community: &Community,
) -> LemmyResult<()> {
let conn = &mut get_conn(pool).await?;
delete(
multi_community_entry::table
.filter(multi_community_entry::multi_community_id.eq(id))
.filter(multi_community_entry::community_id.eq(old_community.id)),
)
.execute(conn)
.await?;
Ok(())
}
pub async fn follow(
pool: &mut DbPool<'_>,
form: &MultiCommunityFollowForm,
) -> LemmyResult<MultiCommunityFollow> {
let conn = &mut get_conn(pool).await?;
Ok(
insert_into(multi_community_follow::table)
.values(form)
.on_conflict((
multi_community_follow::multi_community_id,
multi_community_follow::person_id,
))
.do_update()
.set(form)
.get_result(conn)
.await?,
)
}
pub async fn unfollow(
pool: &mut DbPool<'_>,
person_id: PersonId,
multi_community_id: MultiCommunityId,
) -> LemmyResult<()> {
let conn = &mut get_conn(pool).await?;
delete(
multi_community_follow::table
.filter(multi_community_follow::multi_community_id.eq(multi_community_id))
.filter(multi_community_follow::person_id.eq(person_id)),
)
.execute(conn)
.await?;
Ok(())
}
pub async fn follower_inboxes(
pool: &mut DbPool<'_>,
multi_community_id: MultiCommunityId,
) -> LemmyResult<Vec<DbUrl>> {
let conn = &mut get_conn(pool).await?;
multi_community_follow::table
.inner_join(person::table)
.filter(multi_community_follow::multi_community_id.eq(multi_community_id))
.select(person::inbox_url)
.distinct()
.load(conn)
.await
.optional()?
.ok_or(LemmyErrorType::NotFound.into())
}
pub async fn upsert(
pool: &mut DbPool<'_>,
form: &MultiCommunityInsertForm,
) -> LemmyResult<MultiCommunity> {
let conn = &mut get_conn(pool).await?;
Ok(
insert_into(multi_community::table)
.values(form)
.on_conflict(multi_community::ap_id)
.do_update()
.set(form)
.get_result(conn)
.await?,
)
}
/// Should be called in a transaction together with update() or upsert()
pub async fn update_entries(
pool: &mut DbPool<'_>,
id: MultiCommunityId,
new_communities: &Vec<CommunityId>,
) -> LemmyResult<(Vec<Community>, Vec<Community>, bool)> {
let conn = &mut get_conn(pool).await?;
if new_communities.len() >= usize::try_from(MULTI_COMMUNITY_ENTRY_LIMIT)? {
return Err(LemmyErrorType::MultiCommunityEntryLimitReached.into());
}
let removed: Vec<CommunityId> = delete(
multi_community_entry::table
.filter(multi_community_entry::multi_community_id.eq(id))
.filter(multi_community_entry::community_id.ne_all(new_communities)),
)
.returning(multi_community_entry::community_id)
.get_results::<CommunityId>(conn)
.await?;
let removed: Vec<Community> = community::table
.filter(community::id.eq_any(removed))
.filter(not(community::local))
.get_results(conn)
.await?;
let forms = new_communities
.iter()
.map(|k| {
(
multi_community_entry::multi_community_id.eq(id),
multi_community_entry::community_id.eq(k),
)
})
.collect::<Vec<_>>();
let added: Vec<_> = insert_into(multi_community_entry::table)
.values(forms)
.on_conflict_do_nothing()
.returning(multi_community_entry::community_id)
.get_results::<CommunityId>(conn)
.await?;
let added: Vec<Community> = community::table
.filter(community::id.eq_any(added))
.filter(not(community::local))
.get_results(conn)
.await?;
// check if any local user follows the multi-comm
let has_local_followers: bool = select(exists(
multi_community_follow::table
.inner_join(person::table)
.inner_join(multi_community::table)
.filter(person::local),
))
.get_result(conn)
.await?;
Ok((added, removed, has_local_followers))
}
pub async fn read_entry_ap_ids(
pool: &mut DbPool<'_>,
multi_name: &str,
) -> LemmyResult<Vec<DbUrl>> {
let conn = &mut get_conn(pool).await?;
let entries = multi_community::table
.inner_join(multi_community_entry::table.inner_join(community::table))
.left_join(person::table)
.filter(
community::removed
.or(community::deleted)
.is_distinct_from(true),
)
.filter(person::local)
.filter(multi_community::name.eq(multi_name))
.select(community::ap_id)
.get_results(conn)
.await?;
Ok(entries)
}
pub async fn community_used_in_multiple(
pool: &mut DbPool<'_>,
multi_id: MultiCommunityId,
community_id: CommunityId,
) -> LemmyResult<bool> {
let conn = &mut get_conn(pool).await?;
Ok(
select(exists(
multi_community::table
.inner_join(multi_community_entry::table)
.filter(multi_community::id.ne(multi_id))
.filter(multi_community_entry::community_id.eq(community_id)),
))
.get_result(conn)
.await?,
)
}
pub fn format_url(&self, settings: &Settings) -> LemmyResult<Url> {
let domain = self
.ap_id
.inner()
.domain()
.ok_or(LemmyErrorType::NotFound)?;
format_actor_url(&self.name, domain, 'u', settings)
}
}
#[cfg(test)]
#[allow(clippy::indexing_slicing)]
mod tests {
use super::*;
use crate::{
source::{
community::{Community, CommunityInsertForm},
instance::Instance,
multi_community::{MultiCommunity, MultiCommunityInsertForm},
person::{Person, PersonInsertForm},
},
traits::Crud,
utils::build_db_pool_for_tests,
};
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq;
use serial_test::serial;
struct Data {
multi: MultiCommunity,
instance: Instance,
community: Community,
}
async fn setup(pool: &mut DbPool<'_>) -> LemmyResult<Data> {
let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let form = PersonInsertForm::test_form(instance.id, "bobby");
let person = Person::create(pool, &form).await?;
let form = CommunityInsertForm::new(
instance.id,
"TIL".into(),
"nada".to_owned(),
"pubkey".to_string(),
);
let community = Community::create(pool, &form).await?;
let form =
MultiCommunityInsertForm::new(person.id, instance.id, "multi".to_string(), String::new());
let multi = MultiCommunity::create(pool, &form).await?;
assert_eq!(form.creator_id, multi.creator_id);
assert_eq!(form.name, multi.name);
Ok(Data {
multi,
instance,
community,
})
}
#[tokio::test]
#[serial]
async fn test_multi_community_apub() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let data = setup(pool).await?;
let multi_read_apub_empty = MultiCommunity::read_entry_ap_ids(pool, &data.multi.name).await?;
assert!(multi_read_apub_empty.is_empty());
let multi_entries = vec![data.community.id];
MultiCommunity::update_entries(pool, data.multi.id, &multi_entries).await?;
let multi_read_apub = MultiCommunity::read_entry_ap_ids(pool, &data.multi.name).await?;
assert_eq!(vec![data.community.ap_id], multi_read_apub);
Instance::delete(pool, data.instance.id).await?;
Ok(())
}
}

View file

@ -328,16 +328,17 @@ impl Blockable for PersonActions {
}
impl PersonActions {
pub async fn list_followers(
pub async fn follower_inboxes(
pool: &mut DbPool<'_>,
for_person_id: PersonId,
) -> LemmyResult<Vec<Person>> {
) -> LemmyResult<Vec<DbUrl>> {
let conn = &mut get_conn(pool).await?;
person_actions::table
.filter(person_actions::followed_at.is_not_null())
.inner_join(person::table.on(person_actions::person_id.eq(person::id)))
.filter(person_actions::target_id.eq(for_person_id))
.select(person::all_columns)
.select(person::inbox_url)
.distinct()
.load(conn)
.await
.with_lemmy_type(LemmyErrorType::NotFound)
@ -463,8 +464,8 @@ mod tests {
assert_eq!(person_2.id, person_follower.person_id);
assert!(person_follower.follow_pending.is_some_and(|x| !x));
let followers = PersonActions::list_followers(pool, person_1.id).await?;
assert_eq!(vec![person_2], followers);
let followers = PersonActions::follower_inboxes(pool, person_1.id).await?;
assert_eq!(vec![person_2.inbox_url], followers);
let unfollow =
PersonActions::unfollow(pool, follow_form.person_id, follow_form.target_id).await?;

View file

@ -270,9 +270,9 @@ impl Post {
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePost)
}
pub fn local_url(&self, settings: &Settings) -> LemmyResult<DbUrl> {
pub fn local_url(&self, settings: &Settings) -> LemmyResult<Url> {
let domain = settings.get_protocol_and_hostname();
Ok(Url::parse(&format!("{domain}/post/{}", self.id))?.into())
Ok(Url::parse(&format!("{domain}/post/{}", self.id))?)
}
/// The comment was created locally and sent back, indicating that the community accepted it

View file

@ -10,6 +10,8 @@ pub mod impls;
pub mod newtypes;
pub mod sensitive;
#[cfg(feature = "full")]
pub mod test_data;
#[cfg(feature = "full")]
pub mod aliases {
use lemmy_db_schema_file::schema::{community_actions, instance_actions, local_user, person};
diesel::alias!(
@ -82,6 +84,7 @@ pub enum SearchType {
Posts,
Communities,
Users,
MultiCommunities,
}
#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]

View file

@ -354,6 +354,12 @@ pub struct ModTransferCommunityId(pub i32);
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
pub struct ModAddId(pub i32);
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "full", derive(DieselNewType))]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
pub struct MultiCommunityId(pub i32);
impl DbUrl {
pub fn inner(&self) -> &Url {
&self.0

View file

@ -47,8 +47,8 @@ impl ActivitySendTargets {
pub fn add_inbox(&mut self, inbox: Url) {
self.inboxes.insert(inbox);
}
pub fn add_inboxes(&mut self, inboxes: impl Iterator<Item = Url>) {
self.inboxes.extend(inboxes);
pub fn add_inboxes(&mut self, inboxes: Vec<DbUrl>) {
self.inboxes.extend(inboxes.into_iter().map(Into::into));
}
}

View file

@ -1,4 +1,11 @@
use crate::newtypes::{CommentId, CommunityId, PersonId, PostId, SearchCombinedId};
use crate::newtypes::{
CommentId,
CommunityId,
MultiCommunityId,
PersonId,
PostId,
SearchCombinedId,
};
use chrono::{DateTime, Utc};
#[cfg(feature = "full")]
use i_love_jesus::CursorKeysModule;
@ -25,4 +32,5 @@ pub struct SearchCombined {
pub comment_id: Option<CommentId>,
pub community_id: Option<CommunityId>,
pub person_id: Option<PersonId>,
pub multi_community_id: Option<MultiCommunityId>,
}

View file

@ -175,7 +175,7 @@ pub struct CommunityUpdateForm {
}
#[skip_serializing_none]
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, Default)]
#[cfg_attr(
feature = "full",
derive(Identifiable, Queryable, Selectable, Associations, CursorKeysModule)

View file

@ -1,4 +1,4 @@
use crate::newtypes::{LocalSiteId, SiteId};
use crate::newtypes::{LocalSiteId, MultiCommunityId, PersonId, SiteId};
use chrono::{DateTime, Utc};
use lemmy_db_schema_file::enums::{
CommentSortType,
@ -94,6 +94,8 @@ pub struct LocalSite {
pub users_active_half_year: i64,
/// Dont send email notifications to users for new replies, mentions etc
pub disable_email_notifications: bool,
pub suggested_communities: Option<MultiCommunityId>,
pub multi_comm_follower: PersonId,
}
#[derive(Clone, derive_new::new)]
@ -157,6 +159,10 @@ pub struct LocalSiteInsertForm {
pub disallow_nsfw_content: bool,
#[new(default)]
pub disable_email_notifications: bool,
#[new(default)]
pub suggested_communities: Option<MultiCommunityId>,
#[new(default)]
pub multi_comm_follower: Option<PersonId>,
}
#[derive(Clone, Default)]
@ -192,4 +198,5 @@ pub struct LocalSiteUpdateForm {
pub default_post_time_range_seconds: Option<Option<i32>>,
pub disallow_nsfw_content: Option<bool>,
pub disable_email_notifications: Option<bool>,
pub suggested_communities: Option<MultiCommunityId>,
}

View file

@ -27,6 +27,7 @@ pub mod local_site_url_blocklist;
pub mod local_user;
pub mod login_token;
pub mod mod_log;
pub mod multi_community;
pub mod oauth_account;
pub mod oauth_provider;
pub mod password_reset_request;

View file

@ -0,0 +1,99 @@
use crate::{
newtypes::{DbUrl, InstanceId, MultiCommunityId, PersonId},
sensitive::SensitiveString,
source::placeholder_apub_url,
};
use chrono::{DateTime, Utc};
use lemmy_db_schema_file::enums::CommunityFollowerState;
#[cfg(feature = "full")]
use lemmy_db_schema_file::schema::{multi_community, multi_community_follow};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[skip_serializing_none]
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable))]
#[cfg_attr(feature = "full", diesel(table_name = multi_community))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
pub struct MultiCommunity {
pub id: MultiCommunityId,
pub creator_id: PersonId,
pub instance_id: InstanceId,
pub name: String,
pub title: Option<String>,
pub description: Option<String>,
pub local: bool,
pub deleted: bool,
pub ap_id: DbUrl,
#[serde(skip)]
pub public_key: String,
#[serde(skip)]
pub private_key: Option<SensitiveString>,
#[serde(skip, default = "placeholder_apub_url")]
pub inbox_url: DbUrl,
#[serde(skip)]
pub last_refreshed_at: DateTime<Utc>,
#[serde(skip, default = "placeholder_apub_url")]
pub following_url: DbUrl,
pub published_at: DateTime<Utc>,
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, derive_new::new)]
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = multi_community))]
pub struct MultiCommunityInsertForm {
pub creator_id: PersonId,
pub instance_id: InstanceId,
pub name: String,
pub public_key: String,
#[new(default)]
pub ap_id: Option<DbUrl>,
#[new(default)]
pub local: Option<bool>,
#[new(default)]
pub title: Option<String>,
#[new(default)]
pub description: Option<String>,
#[new(default)]
pub last_refreshed_at: Option<DateTime<Utc>>,
#[new(default)]
pub private_key: Option<SensitiveString>,
#[new(default)]
pub inbox_url: Option<DbUrl>,
#[new(default)]
pub following_url: Option<DbUrl>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "full", derive(AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = multi_community))]
pub struct MultiCommunityUpdateForm {
pub title: Option<Option<String>>,
pub description: Option<Option<String>>,
pub deleted: Option<bool>,
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "full", derive(Queryable, Selectable))]
#[cfg_attr(feature = "full", diesel(table_name = multi_community_follow))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
pub struct MultiCommunityFollow {
pub multi_community_id: MultiCommunityId,
pub person_id: PersonId,
pub follow_state: CommunityFollowerState,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = multi_community_follow))]
pub struct MultiCommunityFollowForm {
pub multi_community_id: MultiCommunityId,
pub person_id: PersonId,
pub follow_state: CommunityFollowerState,
}

View file

@ -0,0 +1,42 @@
use crate::{
source::{
instance::Instance,
local_site::{LocalSite, LocalSiteInsertForm},
local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitInsertForm},
person::{Person, PersonInsertForm},
site::{Site, SiteInsertForm},
},
traits::Crud,
utils::DbPool,
};
use lemmy_utils::error::LemmyResult;
pub struct TestData {
pub instance: Instance,
pub site: Site,
}
impl TestData {
pub async fn create(pool: &mut DbPool<'_>) -> LemmyResult<Self> {
let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let site_form = SiteInsertForm::new("test site".to_string(), instance.id);
let site = Site::create(pool, &site_form).await?;
let person = Person::create(pool, &PersonInsertForm::test_form(instance.id, "langs")).await?;
let local_site_form = LocalSiteInsertForm {
multi_comm_follower: Some(person.id),
..LocalSiteInsertForm::new(site.id)
};
let local_site = LocalSite::create(pool, &local_site_form).await?;
LocalSiteRateLimit::create(pool, &LocalSiteRateLimitInsertForm::new(local_site.id)).await?;
Ok(Self { instance, site })
}
pub async fn delete(self, pool: &mut DbPool<'_>) -> LemmyResult<()> {
Instance::delete(pool, self.instance.id).await?;
Site::delete(pool, self.site.id).await?;
Ok(())
}
}

View file

@ -35,7 +35,10 @@ use lemmy_db_schema_file::{
community_actions,
image_details,
instance_actions,
local_site,
local_user,
multi_community,
multi_community_entry,
person,
person_actions,
post,
@ -392,3 +395,13 @@ pub fn creator_community_actions_join() -> _ {
),
)
}
#[diesel::dsl::auto_type]
pub fn suggested_communities() -> _ {
community::id.eq_any(
local_site::table
.left_join(multi_community::table.inner_join(multi_community_entry::table))
.filter(multi_community_entry::community_id.is_not_null())
.select(multi_community_entry::community_id.assume_not_null()),
)
}

View file

@ -692,7 +692,7 @@ CREATE TRIGGER require_uplete
BEFORE DELETE ON post_actions
FOR EACH STATEMENT
EXECUTE FUNCTION r.require_uplete ();
-- search: (post, comment, community, person)
-- search: (post, comment, community, person, multi_community)
CREATE PROCEDURE r.create_search_combined_trigger (table_name text)
LANGUAGE plpgsql
AS $a$
@ -720,6 +720,7 @@ CALL r.create_search_combined_trigger ('post');
CALL r.create_search_combined_trigger ('comment');
CALL r.create_search_combined_trigger ('community');
CALL r.create_search_combined_trigger ('person');
CALL r.create_search_combined_trigger ('multi_community');
-- You also need to triggers to update the `score` column.
-- post | post::score
-- comment | comment_aggregates::score

View file

@ -72,6 +72,8 @@ pub enum ListingType {
Subscribed,
/// Content that you can moderate (because you are a moderator of the community it is posted to)
ModeratorView,
/// Communities which are recommended by local instance admins
Suggested,
}
#[derive(
@ -188,6 +190,7 @@ pub enum ActorType {
Site,
Community,
Person,
MultiCommunity,
}
#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]

View file

@ -451,6 +451,8 @@ diesel::table! {
users_active_month -> Int8,
users_active_half_year -> Int8,
disable_email_notifications -> Bool,
suggested_communities -> Nullable<Int4>,
multi_comm_follower -> Int4,
}
}
@ -707,6 +709,48 @@ diesel::table! {
}
}
diesel::table! {
multi_community (id) {
id -> Int4,
creator_id -> Int4,
instance_id -> Int4,
#[max_length = 255]
name -> Varchar,
#[max_length = 255]
title -> Nullable<Varchar>,
#[max_length = 255]
description -> Nullable<Varchar>,
local -> Bool,
deleted -> Bool,
ap_id -> Text,
public_key -> Text,
private_key -> Nullable<Text>,
inbox_url -> Text,
last_refreshed_at -> Timestamptz,
following_url -> Text,
published_at -> Timestamptz,
updated_at -> Nullable<Timestamptz>,
}
}
diesel::table! {
multi_community_entry (multi_community_id, community_id) {
multi_community_id -> Int4,
community_id -> Int4,
}
}
diesel::table! {
use diesel::sql_types::*;
use super::sql_types::CommunityFollowerState;
multi_community_follow (multi_community_id, person_id) {
multi_community_id -> Int4,
person_id -> Int4,
follow_state -> CommunityFollowerState,
}
}
diesel::table! {
oauth_account (oauth_provider_id, local_user_id) {
local_user_id -> Int4,
@ -1015,6 +1059,7 @@ diesel::table! {
comment_id -> Nullable<Int4>,
community_id -> Nullable<Int4>,
person_id -> Nullable<Int4>,
multi_community_id -> Nullable<Int4>
}
}
@ -1131,6 +1176,8 @@ diesel::joinable!(instance_actions -> instance (instance_id));
diesel::joinable!(instance_actions -> person (person_id));
diesel::joinable!(local_image -> person (person_id));
diesel::joinable!(local_image -> post (thumbnail_for_post_id));
diesel::joinable!(local_site -> multi_community (suggested_communities));
diesel::joinable!(local_site -> person (multi_comm_follower));
diesel::joinable!(local_site -> site (site_id));
diesel::joinable!(local_site_rate_limit -> local_site (local_site_id));
diesel::joinable!(local_user -> person (person_id));
@ -1171,6 +1218,12 @@ diesel::joinable!(modlog_combined -> mod_remove_comment (mod_remove_comment_id))
diesel::joinable!(modlog_combined -> mod_remove_community (mod_remove_community_id));
diesel::joinable!(modlog_combined -> mod_remove_post (mod_remove_post_id));
diesel::joinable!(modlog_combined -> mod_transfer_community (mod_transfer_community_id));
diesel::joinable!(multi_community -> instance (instance_id));
diesel::joinable!(multi_community -> person (creator_id));
diesel::joinable!(multi_community_entry -> community (community_id));
diesel::joinable!(multi_community_entry -> multi_community (multi_community_id));
diesel::joinable!(multi_community_follow -> multi_community (multi_community_id));
diesel::joinable!(multi_community_follow -> person (person_id));
diesel::joinable!(oauth_account -> local_user (local_user_id));
diesel::joinable!(oauth_account -> oauth_provider (oauth_provider_id));
diesel::joinable!(password_reset_request -> local_user (local_user_id));
@ -1259,6 +1312,9 @@ diesel::allow_tables_to_appear_in_same_query!(
mod_remove_post,
mod_transfer_community,
modlog_combined,
multi_community,
multi_community_entry,
multi_community_follow,
oauth_account,
oauth_provider,
password_reset_request,

View file

@ -35,6 +35,7 @@ use lemmy_db_schema::{
my_instance_actions_community_join,
my_local_user_admin_join,
my_person_actions_join,
suggested_communities,
},
seconds_to_pg_interval,
DbPool,
@ -197,6 +198,7 @@ impl CommentQuery<'_> {
ListingType::ModeratorView => {
query.filter(community_actions::became_moderator_at.is_not_null())
}
ListingType::Suggested => query.filter(suggested_communities()),
};
if !o.local_user.show_bot_accounts() {

View file

@ -1,6 +1,6 @@
use crate::CommunityView;
use crate::{CommunityView, MultiCommunityView};
use lemmy_db_schema::{
newtypes::{CommunityId, LanguageId, PaginationCursor, PersonId, TagId},
newtypes::{CommunityId, LanguageId, MultiCommunityId, PaginationCursor, PersonId, TagId},
source::site::Site,
CommunitySortType,
};
@ -299,3 +299,68 @@ pub struct UpdateCommunityTag {
pub struct DeleteCommunityTag {
pub tag_id: TagId,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
pub struct CreateMultiCommunity {
pub name: String,
pub title: Option<String>,
pub description: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
pub struct UpdateMultiCommunity {
pub id: MultiCommunityId,
pub title: Option<String>,
pub description: Option<String>,
pub deleted: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
pub struct CreateOrDeleteMultiCommunityEntry {
pub id: MultiCommunityId,
pub community_id: CommunityId,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
pub struct ListMultiCommunities {
pub creator_id: Option<PersonId>,
pub followed_only: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
pub struct ListMultiCommunitiesResponse {
pub multi_communities: Vec<MultiCommunityView>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
pub struct GetMultiCommunity {
pub id: MultiCommunityId,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
pub struct GetMultiCommunityResponse {
pub multi_community_view: MultiCommunityView,
pub communities: Vec<CommunityView>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
pub struct FollowMultiCommunity {
pub multi_community_id: MultiCommunityId,
pub follow: bool,
}

View file

@ -1,10 +1,10 @@
use crate::CommunityView;
use crate::{CommunityView, MultiCommunityView};
use diesel::{ExpressionMethods, QueryDsl, SelectableHelper};
use diesel_async::RunQueryDsl;
use i_love_jesus::asc_if;
use lemmy_db_schema::{
impls::local_user::LocalUserOptionHelper,
newtypes::{CommunityId, PaginationCursor, PersonId},
newtypes::{CommunityId, MultiCommunityId, PaginationCursor, PersonId},
source::{
community::{community_keys as key, Community},
local_user::LocalUser,
@ -22,6 +22,7 @@ use lemmy_db_schema::{
my_community_actions_join,
my_instance_actions_community_join,
my_local_user_admin_join,
suggested_communities,
},
seconds_to_pg_interval,
DbPool,
@ -31,7 +32,15 @@ use lemmy_db_schema::{
};
use lemmy_db_schema_file::{
enums::ListingType,
schema::{community, community_actions, instance_actions},
schema::{
community,
community_actions,
instance_actions,
multi_community,
multi_community_entry,
multi_community_follow,
person,
},
};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
@ -99,6 +108,7 @@ pub struct CommunityQuery<'a> {
pub time_range_seconds: Option<i32>,
pub local_user: Option<&'a LocalUser>,
pub show_nsfw: Option<bool>,
pub multi_community_id: Option<MultiCommunityId>,
pub cursor_data: Option<Community>,
pub page_back: Option<bool>,
pub limit: Option<i64>,
@ -134,6 +144,7 @@ impl CommunityQuery<'_> {
ListingType::ModeratorView => {
query.filter(community_actions::became_moderator_at.is_not_null())
}
ListingType::Suggested => query.filter(suggested_communities()),
};
}
@ -147,6 +158,13 @@ impl CommunityQuery<'_> {
query = o.local_user.visible_communities_only(query);
if let Some(multi_community_id) = o.multi_community_id {
let communities = multi_community_entry::table
.filter(multi_community_entry::multi_community_id.eq(multi_community_id))
.select(multi_community_entry::community_id);
query = query.filter(community::id.eq_any(communities))
}
// Filter by the time range
if let Some(time_range_seconds) = o.time_range_seconds {
query = query
@ -184,10 +202,48 @@ impl CommunityQuery<'_> {
}
}
impl MultiCommunityView {
pub async fn read(pool: &mut DbPool<'_>, id: MultiCommunityId) -> LemmyResult<Self> {
let conn = &mut get_conn(pool).await?;
Ok(
multi_community::table
.find(id)
.inner_join(person::table)
.get_result(conn)
.await?,
)
}
pub async fn list(
pool: &mut DbPool<'_>,
owner_id: Option<PersonId>,
followed_by: Option<PersonId>,
) -> LemmyResult<Vec<Self>> {
let conn = &mut get_conn(pool).await?;
let mut query = multi_community::table
.left_join(multi_community_follow::table)
.inner_join(person::table)
.select(multi_community::all_columns)
.into_boxed();
if let Some(owner_id) = owner_id {
query = query.filter(multi_community::creator_id.eq(owner_id));
}
if let Some(followed_by) = followed_by {
query = query.filter(multi_community_follow::person_id.eq(followed_by));
}
query
.select(MultiCommunityView::as_select())
.load::<MultiCommunityView>(conn)
.await
.with_lemmy_type(LemmyErrorType::NotFound)
}
}
#[cfg(test)]
#[allow(clippy::indexing_slicing)]
mod tests {
use crate::{impls::CommunityQuery, CommunityView};
use crate::{impls::CommunityQuery, CommunityView, MultiCommunityView};
use lemmy_db_schema::{
source::{
community::{
@ -200,6 +256,7 @@ mod tests {
},
instance::Instance,
local_user::{LocalUser, LocalUserInsertForm},
multi_community::{MultiCommunity, MultiCommunityFollowForm, MultiCommunityInsertForm},
person::{Person, PersonInsertForm},
site::Site,
},
@ -489,4 +546,64 @@ mod tests {
cleanup(data, pool).await
}
#[tokio::test]
#[serial]
async fn test_multi_community_list() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let data = init_data(pool).await?;
let form = PersonInsertForm::test_form(data.instance.id, "tom");
let person2 = Person::create(pool, &form).await?;
let form = MultiCommunityInsertForm::new(
data.local_user.person_id,
data.instance.id,
"multi2".to_string(),
String::new(),
);
let multi = MultiCommunity::create(pool, &form).await?;
let form = MultiCommunityInsertForm::new(
person2.id,
person2.instance_id,
"multi2".to_string(),
String::new(),
);
let multi2 = MultiCommunity::create(pool, &form).await?;
// list all multis
let list_all = MultiCommunityView::list(pool, None, None)
.await?
.iter()
.map(|m| m.multi.id)
.collect::<HashSet<_>>();
assert_eq!(list_all, HashSet::from([multi.id, multi2.id]));
// list multis by owner
let list_owner = MultiCommunityView::list(pool, Some(data.local_user.person_id), None).await?;
assert_eq!(list_owner.len(), 1);
assert_eq!(list_owner[0].multi.id, multi.id);
// list multis followed by user
let form = MultiCommunityFollowForm {
multi_community_id: multi2.id,
person_id: data.local_user.person_id,
follow_state: CommunityFollowerState::Accepted,
};
MultiCommunity::follow(pool, &form).await?;
let list_followed =
MultiCommunityView::list(pool, None, Some(data.local_user.person_id)).await?;
assert_eq!(list_followed.len(), 1);
assert_eq!(list_followed[0].multi.id, multi2.id);
MultiCommunity::unfollow(pool, data.local_user.person_id, multi2.id).await?;
let list_followed =
MultiCommunityView::list(pool, None, Some(data.local_user.person_id)).await?;
assert_eq!(list_followed.len(), 0);
cleanup(data, pool).await?;
Ok(())
}
}

View file

@ -1,6 +1,8 @@
use lemmy_db_schema::source::{
community::{Community, CommunityActions},
instance::InstanceActions,
multi_community::MultiCommunity,
person::Person,
tag::TagsView,
};
use serde::{Deserialize, Serialize};
@ -42,3 +44,16 @@ pub struct CommunityView {
)]
pub post_tags: TagsView,
}
#[skip_serializing_none]
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(Queryable, Selectable))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
pub struct MultiCommunityView {
#[cfg_attr(feature = "full", diesel(embed))]
pub multi: MultiCommunity,
#[cfg_attr(feature = "full", diesel(embed))]
pub owner: Person,
}

View file

@ -42,7 +42,7 @@ use lemmy_db_schema::{
get_conn,
limit_fetch,
paginate,
queries::{filter_is_subscribed, filter_not_unlisted_or_is_subscribed},
queries::{filter_is_subscribed, filter_not_unlisted_or_is_subscribed, suggested_communities},
DbPool,
},
ModlogActionType,
@ -382,6 +382,7 @@ impl ModlogCombinedQuery<'_> {
ListingType::ModeratorView => {
query.filter(community_actions::became_moderator_at.is_not_null())
}
ListingType::Suggested => query.filter(suggested_communities()),
};
// Sorting by published

View file

@ -1,6 +1,15 @@
use crate::PostView;
use lemmy_db_schema::{
newtypes::{CommentId, CommunityId, DbUrl, LanguageId, PaginationCursor, PostId, TagId},
newtypes::{
CommentId,
CommunityId,
DbUrl,
LanguageId,
MultiCommunityId,
PaginationCursor,
PostId,
TagId,
},
PostFeatureType,
};
use lemmy_db_schema_file::enums::{ListingType, PostSortType};
@ -121,6 +130,7 @@ pub struct GetPosts {
pub time_range_seconds: Option<i32>,
pub community_id: Option<CommunityId>,
pub community_name: Option<String>,
pub multi_community_id: Option<MultiCommunityId>,
pub show_hidden: Option<bool>,
/// If true, then show the read posts (even if your user setting is to hide them)
pub show_read: Option<bool>,

Some files were not shown because too many files have changed in this diff Show more