mirror of
https://github.com/LemmyNet/lemmy.git
synced 2025-09-03 03:33:50 +00:00
* 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:
parent
c198c98bed
commit
bc23e95f50
116 changed files with 2602 additions and 687 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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" }
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -284,7 +284,6 @@ test("No image proxying if setting is disabled", async () => {
|
|||
expect(post.post_view.post.body).toBe(``);
|
||||
|
||||
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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
53
crates/api/api/src/community/multi_community_follow.rs
Normal file
53
crates/api/api/src/community/multi_community_follow.rs
Normal 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()))
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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;
|
||||
|
|
51
crates/api/api_crud/src/multi_community/create.rs
Normal file
51
crates/api/api_crud/src/multi_community/create.rs
Normal 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
|
||||
}
|
58
crates/api/api_crud/src/multi_community/create_entry.rs
Normal file
58
crates/api/api_crud/src/multi_community/create_entry.rs
Normal 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()))
|
||||
}
|
48
crates/api/api_crud/src/multi_community/delete_entry.rs
Normal file
48
crates/api/api_crud/src/multi_community/delete_entry.rs
Normal 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()))
|
||||
}
|
13
crates/api/api_crud/src/multi_community/get.rs
Normal file
13
crates/api/api_crud/src/multi_community/get.rs
Normal 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
|
||||
}
|
24
crates/api/api_crud/src/multi_community/list.rs
Normal file
24
crates/api/api_crud/src/multi_community/list.rs
Normal 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 }))
|
||||
}
|
72
crates/api/api_crud/src/multi_community/mod.rs
Normal file
72
crates/api/api_crud/src/multi_community/mod.rs
Normal 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,
|
||||
}))
|
||||
}
|
34
crates/api/api_crud/src/multi_community/update.rs
Normal file
34
crates/api/api_crud/src/multi_community/update.rs
Normal 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()))
|
||||
}
|
|
@ -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()
|
||||
};
|
||||
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)?,
|
||||
|
|
|
@ -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)?,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()],
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)?,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?,
|
||||
)
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>;
|
||||
|
|
143
crates/apub_objects/src/objects/multi_community.rs
Normal file
143
crates/apub_objects/src/objects/multi_community.rs
Normal 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
|
||||
}
|
||||
}
|
113
crates/apub_objects/src/objects/multi_community_collection.rs
Normal file
113
crates/apub_objects/src/objects/multi_community_collection.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
pub mod group;
|
||||
pub mod instance;
|
||||
pub mod multi_community;
|
||||
pub mod note;
|
||||
pub mod page;
|
||||
pub mod person;
|
||||
|
|
43
crates/apub_objects/src/protocol/multi_community.rs
Normal file
43
crates/apub_objects/src/protocol/multi_community.rs
Normal 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>>,
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -111,6 +111,7 @@ impl Joinable for CommunityActions {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CollectionType {
|
||||
Moderators,
|
||||
Featured,
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
383
crates/db_schema/src/impls/multi_community.rs
Normal file
383
crates/db_schema/src/impls/multi_community.rs
Normal 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(())
|
||||
}
|
||||
}
|
|
@ -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?;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
99
crates/db_schema/src/source/multi_community.rs
Normal file
99
crates/db_schema/src/source/multi_community.rs
Normal 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,
|
||||
}
|
42
crates/db_schema/src/test_data.rs
Normal file
42
crates/db_schema/src/test_data.rs
Normal 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(())
|
||||
}
|
||||
}
|
|
@ -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()),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue