Implement private communities (#5076)

* add private visibility

* filter private communities in post_view.rs

* also filter in comment_view

* community follower state

* remove unused method

* sql fmt

* add CommunityFollower.approved_by

* implement api endpoints

* api changes

* only admins can create private community for now

* add local api tests

* fix api tests

* follow remote private community

* use authorized fetch for content in private community

* federate community visibility

* dont mark content in private community as public

* expose ApprovalRequired in api

* also check content fetchable for outbox/featured

* address private community content to followers

* implement reject activity

* fix tests

* add files

* remove local api tests

* dont use delay

* is_new_instance

* single query for is_new_instance

* return subscribed type for pending follow

* working

* need to catch errors in waitUntil

* clippy

* fix query

* lint for unused async

* diesel.toml comment

* add comment

* avoid db reads

* rename approved_by to approver_id

* add helper

* form init

* list pending follows should return items for all communities

* clippy

* ci

* fix down migration

* fix api tests

* references

* rename

* run git diff

* ci

* fix schema check

* fix joins

* ci

* ci

* skip_serializing_none

* fix test

---------

Co-authored-by: Dessalines <dessalines@users.noreply.github.com>
This commit is contained in:
Nutomic 2024-11-07 11:49:05 +01:00 committed by GitHub
parent 917e408735
commit ad90cd77f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
136 changed files with 1980 additions and 551 deletions

View file

@ -133,8 +133,8 @@ steps:
DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
commands:
- <<: *install_diesel_cli
- cp crates/db_schema/src/schema.rs tmp.schema
- diesel migration run
- diesel print-schema --config-file=diesel.toml > tmp.schema
- diff tmp.schema crates/db_schema/src/schema.rs
when: *slow_check_paths

View file

@ -78,6 +78,7 @@ uninlined_format_args = "allow"
unused_self = "deny"
unwrap_used = "deny"
unimplemented = "deny"
unused_async = "deny"
[workspace.dependencies]
lemmy_api = { version = "=0.19.6-beta.7", path = "./crates/api" }

View file

@ -10,12 +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 post.spec.ts && jest -i comment.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_community.spec.ts && jest -i post.spec.ts && jest -i comment.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-message": "jest -i private_message.spec.ts",
"api-test-image": "jest -i image.spec.ts"
},
@ -27,7 +28,7 @@
"eslint": "^9.9.0",
"eslint-plugin-prettier": "^5.1.3",
"jest": "^29.5.0",
"lemmy-js-client": "0.20.0-alpha.11",
"lemmy-js-client": "0.20.0-private-community.9",
"prettier": "^3.2.5",
"ts-jest": "^29.1.0",
"typescript": "^5.5.4",

View file

@ -30,8 +30,8 @@ importers:
specifier: ^29.5.0
version: 29.7.0(@types/node@22.8.6)
lemmy-js-client:
specifier: 0.20.0-alpha.11
version: 0.20.0-alpha.11
specifier: 0.20.0-private-community.9
version: 0.20.0-private-community.9
prettier:
specifier: ^3.2.5
version: 3.3.3
@ -1159,8 +1159,8 @@ packages:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
lemmy-js-client@0.20.0-alpha.11:
resolution: {integrity: sha512-iRSG4xHMjPDIreQqVIoJ5JrMY71uk07G0Zbgyf068xKbib22J3+i1x/XgCTs6tiHlqTnw1Ig/KRq7p7qJoA4uw==}
lemmy-js-client@0.20.0-private-community.9:
resolution: {integrity: sha512-iuFezswCzIco5U5Q4Eo8HAWVE65pDW2zeO+fYLEyFl30SLw9a3gqJkip2vbDfVvoAjDXyUskZKddf1Nnj8mVcg==}
leven@3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
@ -3061,7 +3061,7 @@ snapshots:
kleur@3.0.3: {}
lemmy-js-client@0.20.0-alpha.11: {}
lemmy-js-client@0.20.0-private-community.9: {}
leven@3.1.0: {}

View file

@ -0,0 +1,214 @@
jest.setTimeout(120000);
import { FollowCommunity } from "lemmy-js-client";
import {
alpha,
setupLogins,
createCommunity,
unfollows,
registerUser,
listCommunityPendingFollows,
getCommunity,
getCommunityPendingFollowsCount,
approveCommunityPendingFollow,
randomString,
createPost,
createComment,
beta,
resolveCommunity,
betaUrl,
resolvePost,
resolveComment,
likeComment,
waitUntil,
} from "./shared";
beforeAll(setupLogins);
afterAll(unfollows);
test("Follow a private community", async () => {
// create private community
const community = await createCommunity(alpha, randomString(10), "Private");
expect(community.community_view.community.visibility).toBe("Private");
const alphaCommunityId = community.community_view.community.id;
// No pending follows yet
const pendingFollows0 = await listCommunityPendingFollows(alpha);
expect(pendingFollows0.items.length).toBe(0);
const pendingFollowsCount0 = await getCommunityPendingFollowsCount(
alpha,
alphaCommunityId,
);
expect(pendingFollowsCount0.count).toBe(0);
// follow as new user
const user = await registerUser(beta, betaUrl);
const betaCommunity = (
await resolveCommunity(user, community.community_view.community.actor_id)
).community;
expect(betaCommunity).toBeDefined();
const betaCommunityId = betaCommunity!.community.id;
const follow_form: FollowCommunity = {
community_id: betaCommunityId,
follow: true,
};
await user.followCommunity(follow_form);
// Follow listed as pending
const follow1 = await getCommunity(user, betaCommunityId);
expect(follow1.community_view.subscribed).toBe("ApprovalRequired");
// Wait for follow to federate, shown as pending
let pendingFollows1 = await waitUntil(
() => listCommunityPendingFollows(alpha),
f => f.items.length == 1,
);
expect(pendingFollows1.items[0].is_new_instance).toBe(true);
const pendingFollowsCount1 = await getCommunityPendingFollowsCount(
alpha,
alphaCommunityId,
);
expect(pendingFollowsCount1.count).toBe(1);
// user still sees approval required at this point
const betaCommunity2 = await getCommunity(user, betaCommunityId);
expect(betaCommunity2.community_view.subscribed).toBe("ApprovalRequired");
// Approve the follow
const approve = await approveCommunityPendingFollow(
alpha,
alphaCommunityId,
pendingFollows1.items[0].person.id,
);
expect(approve.success).toBe(true);
// Follow is confirmed
await waitUntil(
() => getCommunity(user, betaCommunityId),
c => c.community_view.subscribed == "Subscribed",
);
const pendingFollows2 = await listCommunityPendingFollows(alpha);
expect(pendingFollows2.items.length).toBe(0);
const pendingFollowsCount2 = await getCommunityPendingFollowsCount(
alpha,
alphaCommunityId,
);
expect(pendingFollowsCount2.count).toBe(0);
// follow with another user from that instance, is_new_instance should be false now
const user2 = await registerUser(beta, betaUrl);
await user2.followCommunity(follow_form);
let pendingFollows3 = await waitUntil(
() => listCommunityPendingFollows(alpha),
f => f.items.length == 1,
);
expect(pendingFollows3.items[0].is_new_instance).toBe(false);
// cleanup pending follow
const approve2 = await approveCommunityPendingFollow(
alpha,
alphaCommunityId,
pendingFollows3.items[0].person.id,
);
expect(approve2.success).toBe(true);
});
test("Only followers can view and interact with private community content", async () => {
// create private community
const community = await createCommunity(alpha, randomString(10), "Private");
expect(community.community_view.community.visibility).toBe("Private");
const alphaCommunityId = community.community_view.community.id;
// create post and comment
const post0 = await createPost(alpha, alphaCommunityId);
const post_id = post0.post_view.post.id;
expect(post_id).toBeDefined();
const comment = await createComment(alpha, post_id);
const comment_id = comment.comment_view.comment.id;
expect(comment_id).toBeDefined();
// user is not following the community and cannot view nor create posts
const user = await registerUser(beta, betaUrl);
const betaCommunity = (
await resolveCommunity(user, community.community_view.community.actor_id)
).community!.community;
await expect(resolvePost(user, post0.post_view.post)).rejects.toStrictEqual(
Error("not_found"),
);
await expect(
resolveComment(user, comment.comment_view.comment),
).rejects.toStrictEqual(Error("not_found"));
await expect(createPost(user, betaCommunity.id)).rejects.toStrictEqual(
Error("not_found"),
);
// follow the community and approve
const follow_form: FollowCommunity = {
community_id: betaCommunity.id,
follow: true,
};
await user.followCommunity(follow_form);
const pendingFollows1 = await waitUntil(
() => listCommunityPendingFollows(alpha),
f => f.items.length == 1,
);
const approve = await approveCommunityPendingFollow(
alpha,
alphaCommunityId,
pendingFollows1.items[0].person.id,
);
expect(approve.success).toBe(true);
// now user can fetch posts and comments in community (using signed fetch), and create posts
await waitUntil(
() => resolvePost(user, post0.post_view.post),
p => p?.post?.post.id != undefined,
);
const resolvedComment = (
await resolveComment(user, comment.comment_view.comment)
).comment;
expect(resolvedComment?.comment.id).toBeDefined();
const post1 = await createPost(user, betaCommunity.id);
expect(post1.post_view).toBeDefined();
const like = await likeComment(user, 1, resolvedComment!.comment);
expect(like.comment_view.my_vote).toBe(1);
});
test("Reject follower", async () => {
// create private community
const community = await createCommunity(alpha, randomString(10), "Private");
expect(community.community_view.community.visibility).toBe("Private");
const alphaCommunityId = community.community_view.community.id;
// user is not following the community and cannot view nor create posts
const user = await registerUser(beta, betaUrl);
const betaCommunity1 = (
await resolveCommunity(user, community.community_view.community.actor_id)
).community!.community;
// follow the community and reject
const follow_form: FollowCommunity = {
community_id: betaCommunity1.id,
follow: true,
};
const follow = await user.followCommunity(follow_form);
expect(follow.community_view.subscribed).toBe("ApprovalRequired");
const pendingFollows1 = await waitUntil(
() => listCommunityPendingFollows(alpha),
f => f.items.length == 1,
);
const approve = await approveCommunityPendingFollow(
alpha,
alphaCommunityId,
pendingFollows1.items[0].person.id,
false,
);
expect(approve.success).toBe(true);
await waitUntil(
() => getCommunity(user, betaCommunity1.id),
c => c.community_view.subscribed == "NotSubscribed",
);
});

View file

@ -1,17 +1,24 @@
import {
ApproveCommunityPendingFollower,
BlockCommunity,
BlockCommunityResponse,
BlockInstance,
BlockInstanceResponse,
CommunityId,
CommunityVisibility,
CreatePrivateMessageReport,
DeleteImage,
EditCommunity,
GetCommunityPendingFollowsCount,
GetCommunityPendingFollowsCountResponse,
GetReplies,
GetRepliesResponse,
GetUnreadCountResponse,
InstanceId,
LemmyHttp,
ListCommunityPendingFollows,
ListCommunityPendingFollowsResponse,
PersonId,
PostView,
PrivateMessageReportResponse,
SuccessResponse,
@ -198,7 +205,7 @@ export async function setupLogins() {
// only needed the first time so do in this try
await delay(10_000);
} catch {
console.log("Communities already exist");
//console.log("Communities already exist");
}
}
@ -554,12 +561,14 @@ export async function likeComment(
export async function createCommunity(
api: LemmyHttp,
name_: string = randomString(10),
visibility: CommunityVisibility = "Public",
): Promise<CommunityResponse> {
let description = "a sample description";
let form: CreateCommunity = {
name: name_,
title: name_,
description,
visibility,
};
return api.createCommunity(form);
}
@ -688,7 +697,6 @@ export async function saveUserSettingsBio(
let form: SaveUserSettings = {
show_nsfw: true,
blur_nsfw: false,
auto_expand: true,
theme: "darkly",
default_post_sort_type: "Active",
default_listing_type: "All",
@ -709,7 +717,6 @@ export async function saveUserSettingsFederated(
let form: SaveUserSettings = {
show_nsfw: false,
blur_nsfw: true,
auto_expand: false,
default_post_sort_type: "Hot",
default_listing_type: "All",
interface_language: "",
@ -872,6 +879,39 @@ export function blockCommunity(
return api.blockCommunity(form);
}
export function listCommunityPendingFollows(
api: LemmyHttp,
): Promise<ListCommunityPendingFollowsResponse> {
let form: ListCommunityPendingFollows = {
pending_only: true,
all_communities: false,
page: 1,
limit: 50,
};
return api.listCommunityPendingFollows(form);
}
export function getCommunityPendingFollowsCount(
api: LemmyHttp,
community_id: CommunityId,
): Promise<GetCommunityPendingFollowsCountResponse> {
return api.getCommunityPendingFollowsCount(community_id);
}
export function approveCommunityPendingFollow(
api: LemmyHttp,
community_id: CommunityId,
follower_id: PersonId,
approve: boolean = true,
): Promise<SuccessResponse> {
let form: ApproveCommunityPendingFollower = {
community_id,
follower_id,
approve,
};
return api.approveCommunityPendingFollow(form);
}
export function delay(millis = 500) {
return new Promise(resolve => setTimeout(resolve, millis));
}
@ -962,8 +1002,12 @@ export async function waitUntil<T>(
let retry = 0;
let result;
while (retry++ < retries) {
result = await fetcher();
if (checker(result)) return result;
try {
result = await fetcher();
if (checker(result)) return result;
} catch (error) {
//console.error(error);
}
await delay(
delaySeconds[Math.min(retry - 1, delaySeconds.length - 1)] * 1000,
);

View file

@ -26,7 +26,7 @@ pub async fn distinguish_comment(
check_community_user_action(
&local_user_view.person,
orig_comment.community.id,
&orig_comment.community,
&mut context.pool(),
)
.await?;
@ -39,7 +39,7 @@ pub async fn distinguish_comment(
// Verify that only a mod or admin can distinguish a comment
check_community_mod_action(
&local_user_view.person,
orig_comment.community.id,
&orig_comment.community,
false,
&mut context.pool(),
)

View file

@ -50,7 +50,7 @@ pub async fn like_comment(
check_community_user_action(
&local_user_view.person,
orig_comment.community.id,
&orig_comment.community,
&mut context.pool(),
)
.await?;
@ -92,8 +92,7 @@ pub async fn like_comment(
score: data.score,
},
&context,
)
.await?;
)?;
Ok(Json(
build_comment_response(

View file

@ -44,7 +44,7 @@ pub async fn create_comment_report(
check_community_user_action(
&local_user_view.person,
comment_view.community.id,
&comment_view.community,
&mut context.pool(),
)
.await?;
@ -85,8 +85,7 @@ pub async fn create_comment_report(
reason: data.reason.clone(),
},
&context,
)
.await?;
)?;
Ok(Json(CommentReportResponse {
comment_report_view,

View file

@ -22,7 +22,7 @@ pub async fn resolve_comment_report(
let person_id = local_user_view.person.id;
check_community_mod_action(
&local_user_view.person,
report.community.id,
&report.community,
true,
&mut context.pool(),
)

View file

@ -24,12 +24,11 @@ pub async fn add_mod_to_community(
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<AddModToCommunityResponse>> {
let community_id = data.community_id;
let community = Community::read(&mut context.pool(), data.community_id).await?;
// Verify that only mods or admins can add mod
check_community_mod_action(
&local_user_view.person,
community_id,
&community,
false,
&mut context.pool(),
)
@ -39,15 +38,13 @@ pub async fn add_mod_to_community(
if !data.added {
LocalUser::is_higher_mod_or_admin_check(
&mut context.pool(),
community_id,
community.id,
local_user_view.person.id,
vec![data.person_id],
)
.await?;
}
let community = Community::read(&mut context.pool(), community_id).await?;
// If user is admin and community is remote, explicitly check that he is a
// moderator. This is necessary because otherwise the action would be rejected
// by the community's home instance.
@ -98,8 +95,7 @@ pub async fn add_mod_to_community(
added: data.added,
},
&context,
)
.await?;
)?;
Ok(Json(AddModToCommunityResponse { moderators }))
}

View file

@ -13,6 +13,7 @@ use lemmy_api_common::{
use lemmy_db_schema::{
source::{
community::{
Community,
CommunityFollower,
CommunityFollowerForm,
CommunityPersonBan,
@ -38,11 +39,12 @@ pub async fn ban_from_community(
) -> LemmyResult<Json<BanFromCommunityResponse>> {
let banned_person_id = data.person_id;
let expires = check_expire_time(data.expires)?;
let community = Community::read(&mut context.pool(), data.community_id).await?;
// Verify that only mods or admins can ban
check_community_mod_action(
&local_user_view.person,
data.community_id,
&community,
false,
&mut context.pool(),
)
@ -72,12 +74,7 @@ pub async fn ban_from_community(
.with_lemmy_type(LemmyErrorType::CommunityUserAlreadyBanned)?;
// Also unsubscribe them from the community, if they are subscribed
let community_follower_form = CommunityFollowerForm {
community_id: data.community_id,
person_id: banned_person_id,
pending: false,
};
let community_follower_form = CommunityFollowerForm::new(data.community_id, banned_person_id);
CommunityFollower::unfollow(&mut context.pool(), &community_follower_form)
.await
.ok();
@ -123,8 +120,7 @@ pub async fn ban_from_community(
data: data.0.clone(),
},
&context,
)
.await?;
)?;
Ok(Json(BanFromCommunityResponse {
person_view,

View file

@ -35,12 +35,7 @@ pub async fn block_community(
.with_lemmy_type(LemmyErrorType::CommunityBlockAlreadyExists)?;
// Also, unfollow the community, and send a federated unfollow
let community_follower_form = CommunityFollowerForm {
community_id: data.community_id,
person_id,
pending: false,
};
let community_follower_form = CommunityFollowerForm::new(data.community_id, person_id);
CommunityFollower::unfollow(&mut context.pool(), &community_follower_form)
.await
.ok();
@ -65,8 +60,7 @@ pub async fn block_community(
false,
),
&context,
)
.await?;
)?;
Ok(Json(BlockCommunityResponse {
blocked: data.block,

View file

@ -4,17 +4,18 @@ use lemmy_api_common::{
community::{CommunityResponse, FollowCommunity},
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::check_community_user_action,
utils::{check_community_deleted_removed, check_user_valid},
};
use lemmy_db_schema::{
source::{
actor_language::CommunityLanguage,
community::{Community, CommunityFollower, CommunityFollowerForm},
community::{Community, CommunityFollower, CommunityFollowerForm, CommunityFollowerState},
},
traits::{Crud, Followable},
CommunityVisibility,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::CommunityView;
use lemmy_db_views_actor::structs::{CommunityPersonBanView, CommunityView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))]
@ -23,40 +24,52 @@ pub async fn follow_community(
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<CommunityResponse>> {
check_user_valid(&local_user_view.person)?;
let community = Community::read(&mut context.pool(), data.community_id).await?;
let mut community_follower_form = CommunityFollowerForm {
community_id: community.id,
person_id: local_user_view.person.id,
pending: false,
};
let form = CommunityFollowerForm::new(community.id, local_user_view.person.id);
if data.follow {
// Only run these checks for local community, in case of remote community the local
// state may be outdated. Can't use check_community_user_action() here as it only allows
// actions from existing followers for private community (so following would be impossible).
if community.local {
check_community_user_action(&local_user_view.person, community.id, &mut context.pool())
check_community_deleted_removed(&community)?;
CommunityPersonBanView::check(&mut context.pool(), local_user_view.person.id, community.id)
.await?;
CommunityFollower::follow(&mut context.pool(), &community_follower_form)
.await
.with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?;
} else {
// Mark as pending, the actual federation activity is sent via `SendActivity` handler
community_follower_form.pending = true;
CommunityFollower::follow(&mut context.pool(), &community_follower_form)
.await
.with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?;
}
let state = if community.local {
// Local follow is accepted immediately
Some(CommunityFollowerState::Accepted)
} else if community.visibility == CommunityVisibility::Private {
// Private communities require manual approval
Some(CommunityFollowerState::ApprovalRequired)
} else {
// remote follow needs to be federated first
Some(CommunityFollowerState::Pending)
};
let form = CommunityFollowerForm {
state,
..CommunityFollowerForm::new(community.id, local_user_view.person.id)
};
// Write to db
CommunityFollower::follow(&mut context.pool(), &form)
.await
.with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?;
} else {
CommunityFollower::unfollow(&mut context.pool(), &community_follower_form)
CommunityFollower::unfollow(&mut context.pool(), &form)
.await
.with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?;
}
// Send the federated follow
if !community.local {
ActivityChannel::submit_activity(
SendActivityData::FollowCommunity(community, local_user_view.person.clone(), data.follow),
&context,
)
.await?;
)?;
}
let community_id = data.community_id;

View file

@ -48,8 +48,7 @@ pub async fn hide_community(
ActivityChannel::submit_activity(
SendActivityData::UpdateCommunity(local_user_view.person.clone(), community),
&context,
)
.await?;
)?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -3,5 +3,6 @@ pub mod ban;
pub mod block;
pub mod follow;
pub mod hide;
pub mod pending_follows;
pub mod random;
pub mod transfer;

View file

@ -0,0 +1,46 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{
community::ApproveCommunityPendingFollower,
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::is_mod_or_admin,
SuccessResponse,
};
use lemmy_db_schema::{
source::community::{CommunityFollower, CommunityFollowerForm},
traits::Followable,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult;
pub async fn post_pending_follows_approve(
data: Json<ApproveCommunityPendingFollower>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
is_mod_or_admin(
&mut context.pool(),
&local_user_view.person,
data.community_id,
)
.await?;
let activity_data = if data.approve {
CommunityFollower::approve(
&mut context.pool(),
data.community_id,
data.follower_id,
local_user_view.person.id,
)
.await?;
SendActivityData::AcceptFollower(data.community_id, data.follower_id)
} else {
let form = CommunityFollowerForm::new(data.community_id, data.follower_id);
CommunityFollower::unfollow(&mut context.pool(), &form).await?;
SendActivityData::RejectFollower(data.community_id, data.follower_id)
};
ActivityChannel::submit_activity(activity_data, &context)?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -0,0 +1,25 @@
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
community::{GetCommunityPendingFollowsCount, GetCommunityPendingFollowsCountResponse},
context::LemmyContext,
utils::is_mod_or_admin,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::CommunityFollowerView;
use lemmy_utils::error::LemmyResult;
pub async fn get_pending_follows_count(
data: Query<GetCommunityPendingFollowsCount>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<GetCommunityPendingFollowsCountResponse>> {
is_mod_or_admin(
&mut context.pool(),
&local_user_view.person,
data.community_id,
)
.await?;
let count =
CommunityFollowerView::count_approval_required(&mut context.pool(), data.community_id).await?;
Ok(Json(GetCommunityPendingFollowsCountResponse { count }))
}

View file

@ -0,0 +1,29 @@
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
community::{ListCommunityPendingFollows, ListCommunityPendingFollowsResponse},
context::LemmyContext,
utils::check_community_mod_of_any_or_admin_action,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::CommunityFollowerView;
use lemmy_utils::error::LemmyResult;
pub async fn get_pending_follows_list(
data: Query<ListCommunityPendingFollows>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<ListCommunityPendingFollowsResponse>> {
check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?;
let all_communities =
data.all_communities.unwrap_or_default() && local_user_view.local_user.admin;
let items = CommunityFollowerView::list_approval_required(
&mut context.pool(),
local_user_view.person.id,
all_communities,
data.pending_only.unwrap_or_default(),
data.page,
data.limit,
)
.await?;
Ok(Json(ListCommunityPendingFollowsResponse { items }))
}

View file

@ -0,0 +1,3 @@
pub mod approve;
pub mod count;
pub mod list;

View file

@ -7,7 +7,7 @@ use lemmy_api_common::{
};
use lemmy_db_schema::{
source::{
community::{CommunityModerator, CommunityModeratorForm},
community::{Community, CommunityModerator, CommunityModeratorForm},
moderator::{ModTransferCommunity, ModTransferCommunityForm},
},
traits::{Crud, Joinable},
@ -27,11 +27,11 @@ pub async fn transfer_community(
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<GetCommunityResponse>> {
let community_id = data.community_id;
let community = Community::read(&mut context.pool(), data.community_id).await?;
let mut community_mods =
CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
CommunityModeratorView::for_community(&mut context.pool(), community.id).await?;
check_community_user_action(&local_user_view.person, community_id, &mut context.pool()).await?;
check_community_user_action(&local_user_view.person, &community, &mut context.pool()).await?;
// Make sure transferrer is either the top community mod, or an admin
if !(is_top_mod(&local_user_view, &community_mods).is_ok() || is_admin(&local_user_view).is_ok())

View file

@ -197,11 +197,7 @@ pub(crate) async fn ban_nonlocal_user_from_local_communities(
.ok();
// Also unsubscribe them from the community, if they are subscribed
let community_follower_form = CommunityFollowerForm {
community_id,
person_id: target.id,
pending: false,
};
let community_follower_form = CommunityFollowerForm::new(community_id, target.id);
CommunityFollower::unfollow(&mut context.pool(), &community_follower_form)
.await
@ -242,8 +238,7 @@ pub(crate) async fn ban_nonlocal_user_from_local_communities(
data: ban_from_community,
},
context,
)
.await?;
)?;
}
}

View file

@ -111,8 +111,7 @@ pub async fn ban_from_site(
expires: data.expires,
},
&context,
)
.await?;
)?;
Ok(Json(BanPersonResponse {
person_view,

View file

@ -9,6 +9,7 @@ use lemmy_api_common::{
};
use lemmy_db_schema::{
source::{
community::Community,
moderator::{ModFeaturePost, ModFeaturePostForm},
post::{Post, PostUpdateForm},
},
@ -27,9 +28,10 @@ pub async fn feature_post(
let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id).await?;
let community = Community::read(&mut context.pool(), orig_post.community_id).await?;
check_community_mod_action(
&local_user_view.person,
orig_post.community_id,
&community,
false,
&mut context.pool(),
)
@ -67,8 +69,7 @@ pub async fn feature_post(
ActivityChannel::submit_activity(
SendActivityData::FeaturePost(post, local_user_view.person.clone(), data.featured),
&context,
)
.await?;
)?;
build_post_response(&context, orig_post.community_id, local_user_view, post_id).await
}

View file

@ -15,13 +15,12 @@ use lemmy_api_common::{
};
use lemmy_db_schema::{
source::{
community::Community,
local_site::LocalSite,
post::{Post, PostLike, PostLikeForm},
post::{PostLike, PostLikeForm},
},
traits::{Crud, Likeable},
traits::Likeable,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views::structs::{LocalUserView, PostView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
use std::ops::Deref;
@ -45,11 +44,11 @@ pub async fn like_post(
check_bot_account(&local_user_view.person)?;
// Check for a community ban
let post = Post::read(&mut context.pool(), post_id).await?;
let post = PostView::read(&mut context.pool(), post_id, None, false).await?;
check_community_user_action(
&local_user_view.person,
post.community_id,
&post.community,
&mut context.pool(),
)
.await?;
@ -75,18 +74,15 @@ pub async fn like_post(
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
let community = Community::read(&mut context.pool(), post.community_id).await?;
ActivityChannel::submit_activity(
SendActivityData::LikePostOrComment {
object_id: post.ap_id,
object_id: post.post.ap_id,
actor: local_user_view.person.clone(),
community,
community: post.community.clone(),
score: data.score,
},
&context,
)
.await?;
)?;
build_post_response(context.deref(), post.community_id, local_user_view, post_id).await
build_post_response(context.deref(), post.community.id, local_user_view, post_id).await
}

View file

@ -14,7 +14,7 @@ use lemmy_db_schema::{
},
traits::Crud,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views::structs::{LocalUserView, PostView};
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
@ -24,11 +24,11 @@ pub async fn lock_post(
local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> {
let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id).await?;
let orig_post = PostView::read(&mut context.pool(), post_id, None, false).await?;
check_community_mod_action(
&local_user_view.person,
orig_post.community_id,
&orig_post.community,
false,
&mut context.pool(),
)
@ -58,8 +58,7 @@ pub async fn lock_post(
ActivityChannel::submit_activity(
SendActivityData::LockPost(post, local_user_view.person.clone(), data.locked),
&context,
)
.await?;
)?;
build_post_response(&context, orig_post.community_id, local_user_view, post_id).await
build_post_response(&context, orig_post.community.id, local_user_view, post_id).await
}

View file

@ -39,7 +39,7 @@ pub async fn create_post_report(
check_community_user_action(
&local_user_view.person,
post_view.community.id,
&post_view.community,
&mut context.pool(),
)
.await?;
@ -80,8 +80,7 @@ pub async fn create_post_report(
reason: data.reason.clone(),
},
&context,
)
.await?;
)?;
Ok(Json(PostReportResponse { post_report_view }))
}

View file

@ -22,7 +22,7 @@ pub async fn resolve_post_report(
let person_id = local_user_view.person.id;
check_community_mod_action(
&local_user_view.person,
report.community.id,
&report.community,
true,
&mut context.pool(),
)

View file

@ -67,8 +67,7 @@ pub async fn purge_comment(
reason: data.reason.clone(),
},
&context,
)
.await?;
)?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -75,8 +75,7 @@ pub async fn purge_community(
removed: true,
},
&context,
)
.await?;
)?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -80,8 +80,7 @@ pub async fn purge_person(
expires: None,
},
&context,
)
.await?;
)?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -66,8 +66,7 @@ pub async fn purge_post(
removed: true,
},
&context,
)
.await?;
)?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -9,9 +9,7 @@ use lemmy_utils::error::LemmyResult;
use sitemap_rs::{url::Url, url_set::UrlSet};
use tracing::info;
async fn generate_urlset(
posts: Vec<(DbUrl, chrono::DateTime<chrono::Utc>)>,
) -> LemmyResult<UrlSet> {
fn generate_urlset(posts: Vec<(DbUrl, chrono::DateTime<chrono::Utc>)>) -> LemmyResult<UrlSet> {
let urls = posts
.into_iter()
.map_while(|(url, date_time)| {
@ -31,7 +29,7 @@ pub async fn get_sitemap(context: Data<LemmyContext>) -> LemmyResult<HttpRespons
info!("Loaded latest {} posts", posts.len());
let mut buf = Vec::<u8>::new();
generate_urlset(posts).await?.write(&mut buf)?;
generate_urlset(posts)?.write(&mut buf)?;
Ok(
HttpResponse::Ok()
@ -74,7 +72,7 @@ pub(crate) mod tests {
];
let mut buf = Vec::<u8>::new();
generate_urlset(posts).await?.write(&mut buf)?;
generate_urlset(posts)?.write(&mut buf)?;
let root = Element::from_reader(buf.as_slice())?;
assert_eq!(root.tag().name(), "urlset");

View file

@ -92,7 +92,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_should_not_validate_user_token_after_password_change() -> LemmyResult<()> {
let pool_ = build_db_pool_for_tests().await;
let pool_ = build_db_pool_for_tests();
let pool = &mut (&pool_).into();
let secret = Secret::init(pool).await?;
let context = LemmyContext::create(

View file

@ -8,6 +8,7 @@ use lemmy_db_views_actor::structs::{
CommunityModeratorView,
CommunitySortType,
CommunityView,
PendingFollow,
PersonView,
};
use serde::{Deserialize, Serialize};
@ -274,3 +275,50 @@ pub struct GetRandomCommunity {
#[cfg_attr(feature = "full", ts(optional))]
pub type_: Option<ListingType>,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct ListCommunityPendingFollows {
/// Only shows the unapproved applications
#[cfg_attr(feature = "full", ts(optional))]
pub pending_only: Option<bool>,
// Only for admins, show pending follows for communities which you dont moderate
#[cfg_attr(feature = "full", ts(optional))]
pub all_communities: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct GetCommunityPendingFollowsCount {
pub community_id: CommunityId,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct GetCommunityPendingFollowsCountResponse {
pub count: i64,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct ListCommunityPendingFollowsResponse {
pub items: Vec<PendingFollow>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct ApproveCommunityPendingFollower {
pub community_id: CommunityId,
pub follower_id: PersonId,
pub approve: bool,
}

View file

@ -57,7 +57,7 @@ impl LemmyContext {
/// Do not use this in production code.
pub async fn init_test_federation_config() -> FederationConfig<LemmyContext> {
// call this to run migrations
let pool = build_db_pool_for_tests().await;
let pool = build_db_pool_for_tests();
let client = client_builder(&SETTINGS).build().expect("build client");

View file

@ -175,7 +175,7 @@ pub async fn generate_post_link_metadata(
};
let updated_post = Post::update(&mut context.pool(), post.id, &form).await?;
if let Some(send_activity) = send_activity(updated_post) {
ActivityChannel::submit_activity(send_activity, &context).await?;
ActivityChannel::submit_activity(send_activity, &context)?;
}
Ok(())
}

View file

@ -59,6 +59,8 @@ pub enum SendActivityData {
score: i16,
},
FollowCommunity(Community, Person, bool),
AcceptFollower(CommunityId, PersonId),
RejectFollower(CommunityId, PersonId),
UpdateCommunity(Person, Community),
DeleteCommunity(Person, Community, bool),
RemoveCommunity {
@ -123,10 +125,7 @@ impl ActivityChannel {
lock.recv().await
}
pub async fn submit_activity(
data: SendActivityData,
_context: &Data<LemmyContext>,
) -> LemmyResult<()> {
pub fn submit_activity(data: SendActivityData, _context: &Data<LemmyContext>) -> LemmyResult<()> {
// could do `ACTIVITY_CHANNEL.keepalive_sender.lock()` instead and get rid of weak_sender,
// not sure which way is more efficient
if let Some(sender) = ACTIVITY_CHANNEL.weak_sender.upgrade() {

View file

@ -42,6 +42,7 @@ use lemmy_db_views::{
structs::{LocalImageView, LocalUserView, SiteView},
};
use lemmy_db_views_actor::structs::{
CommunityFollowerView,
CommunityModeratorView,
CommunityPersonBanView,
CommunityView,
@ -232,20 +233,17 @@ pub async fn check_registration_application(
/// the user isn't banned.
pub async fn check_community_user_action(
person: &Person,
community_id: CommunityId,
community: &Community,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
check_user_valid(person)?;
check_community_deleted_removed(community_id, pool).await?;
CommunityPersonBanView::check(pool, person.id, community_id).await?;
check_community_deleted_removed(community)?;
CommunityPersonBanView::check(pool, person.id, community.id).await?;
CommunityFollowerView::check_private_community_action(pool, person.id, community).await?;
Ok(())
}
async fn check_community_deleted_removed(
community_id: CommunityId,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
let community = Community::read(pool, community_id).await?;
pub fn check_community_deleted_removed(community: &Community) -> LemmyResult<()> {
if community.deleted || community.removed {
Err(LemmyErrorType::Deleted)?
}
@ -258,16 +256,16 @@ async fn check_community_deleted_removed(
/// removed/deleted.
pub async fn check_community_mod_action(
person: &Person,
community_id: CommunityId,
community: &Community,
allow_deleted: bool,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
is_mod_or_admin(pool, person, community_id).await?;
CommunityPersonBanView::check(pool, person.id, community_id).await?;
is_mod_or_admin(pool, person, community.id).await?;
CommunityPersonBanView::check(pool, person.id, community.id).await?;
// it must be possible to restore deleted community
if !allow_deleted {
check_community_deleted_removed(community_id, pool).await?;
check_community_deleted_removed(community)?;
}
Ok(())
}

View file

@ -61,7 +61,12 @@ pub async fn create_comment(
let post = post_view.post;
let community_id = post_view.community.id;
check_community_user_action(&local_user_view.person, community_id, &mut context.pool()).await?;
check_community_user_action(
&local_user_view.person,
&post_view.community,
&mut context.pool(),
)
.await?;
check_post_deleted_or_removed(&post)?;
// Check if post is locked, no new comments
@ -143,8 +148,7 @@ pub async fn create_comment(
ActivityChannel::submit_activity(
SendActivityData::CreateComment(inserted_comment.clone()),
&context,
)
.await?;
)?;
// Update the read comments, so your own new comment doesn't appear as a +1 unread
update_read_comments(

View file

@ -35,7 +35,7 @@ pub async fn delete_comment(
check_community_user_action(
&local_user_view.person,
orig_comment.community.id,
&orig_comment.community,
&mut context.pool(),
)
.await?;
@ -76,8 +76,7 @@ pub async fn delete_comment(
orig_comment.community,
),
&context,
)
.await?;
)?;
Ok(Json(
build_comment_response(

View file

@ -35,7 +35,7 @@ pub async fn remove_comment(
check_community_mod_action(
&local_user_view.person,
orig_comment.community.id,
&orig_comment.community,
false,
&mut context.pool(),
)
@ -99,8 +99,7 @@ pub async fn remove_comment(
reason: data.reason.clone(),
},
&context,
)
.await?;
)?;
Ok(Json(
build_comment_response(

View file

@ -45,7 +45,7 @@ pub async fn update_comment(
check_community_user_action(
&local_user_view.person,
orig_comment.community.id,
&orig_comment.community,
&mut context.pool(),
)
.await?;
@ -98,8 +98,7 @@ pub async fn update_comment(
ActivityChannel::submit_activity(
SendActivityData::UpdateComment(updated_comment.clone()),
&context,
)
.await?;
)?;
Ok(Json(
build_comment_response(

View file

@ -1,3 +1,4 @@
use super::check_community_visibility_allowed;
use activitypub_federation::{config::Data, http_signatures::generate_actor_keypair};
use actix_web::web::Json;
use lemmy_api_common::{
@ -23,6 +24,7 @@ use lemmy_db_schema::{
Community,
CommunityFollower,
CommunityFollowerForm,
CommunityFollowerState,
CommunityInsertForm,
CommunityModerator,
CommunityModeratorForm,
@ -82,6 +84,12 @@ pub async fn create_community(
is_valid_actor_name(&data.name, local_site.actor_name_max_length as usize)?;
if let Some(desc) = &data.description {
is_valid_body_field(desc, false)?;
}
check_community_visibility_allowed(data.visibility, &local_user_view)?;
// Double check for duplicate community actor_ids
let community_actor_id = generate_local_apub_endpoint(
EndpointType::Community,
@ -135,7 +143,8 @@ pub async fn create_community(
let community_follower_form = CommunityFollowerForm {
community_id: inserted_community.id,
person_id: local_user_view.person.id,
pending: false,
state: Some(CommunityFollowerState::Accepted),
approver_id: None,
};
CommunityFollower::follow(&mut context.pool(), &community_follower_form)

View file

@ -22,13 +22,13 @@ pub async fn delete_community(
local_user_view: LocalUserView,
) -> LemmyResult<Json<CommunityResponse>> {
// Fetch the community mods
let community_id = data.community_id;
let community_mods =
CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
CommunityModeratorView::for_community(&mut context.pool(), data.community_id).await?;
let community = Community::read(&mut context.pool(), data.community_id).await?;
check_community_mod_action(
&local_user_view.person,
community_id,
&community,
true,
&mut context.pool(),
)
@ -54,8 +54,7 @@ pub async fn delete_community(
ActivityChannel::submit_activity(
SendActivityData::DeleteCommunity(local_user_view.person.clone(), community, data.deleted),
&context,
)
.await?;
)?;
build_community_response(&context, local_user_view, community_id).await
}

View file

@ -1,5 +1,22 @@
use lemmy_api_common::utils::is_admin;
use lemmy_db_schema::CommunityVisibility;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult;
pub mod create;
pub mod delete;
pub mod list;
pub mod remove;
pub mod update;
/// For now only admins can make communities private, in order to prevent abuse.
/// Need to implement admin approval for new communities to get rid of this.
fn check_community_visibility_allowed(
visibility: Option<CommunityVisibility>,
local_user_view: &LocalUserView,
) -> LemmyResult<()> {
if visibility == Some(lemmy_db_schema::CommunityVisibility::Private) {
is_admin(local_user_view)?;
}
Ok(())
}

View file

@ -23,9 +23,10 @@ pub async fn remove_community(
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<CommunityResponse>> {
let community = Community::read(&mut context.pool(), data.community_id).await?;
check_community_mod_action(
&local_user_view.person,
data.community_id,
&community,
true,
&mut context.pool(),
)
@ -65,8 +66,7 @@ pub async fn remove_community(
removed: data.removed,
},
&context,
)
.await?;
)?;
build_community_response(&context, local_user_view, community_id).await
}

View file

@ -1,3 +1,4 @@
use super::check_community_visibility_allowed;
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{
@ -51,6 +52,7 @@ pub async fn update_community(
is_valid_body_field(sidebar, false)?;
}
check_community_visibility_allowed(data.visibility, &local_user_view)?;
let description = diesel_string_update(data.description.as_deref());
let old_community = Community::read(&mut context.pool(), data.community_id).await?;
@ -66,7 +68,7 @@ pub async fn update_community(
// Verify its a mod (only mods can edit it)
check_community_mod_action(
&local_user_view.person,
data.community_id,
&old_community,
false,
&mut context.pool(),
)
@ -105,8 +107,7 @@ pub async fn update_community(
ActivityChannel::submit_activity(
SendActivityData::UpdateCommunity(local_user_view.person.clone(), community),
&context,
)
.await?;
)?;
build_community_response(&context, local_user_view, community_id).await
}

View file

@ -85,15 +85,9 @@ pub async fn create_post(
is_valid_body_field(body, true)?;
}
check_community_user_action(
&local_user_view.person,
data.community_id,
&mut context.pool(),
)
.await?;
let community = Community::read(&mut context.pool(), data.community_id).await?;
check_community_user_action(&local_user_view.person, &community, &mut context.pool()).await?;
let community_id = data.community_id;
let community = Community::read(&mut context.pool(), community_id).await?;
if community.posting_restricted_to_mods {
let community_id = data.community_id;
CommunityModeratorView::check_is_community_moderator(
@ -110,7 +104,7 @@ pub async fn create_post(
None => {
default_post_language(
&mut context.pool(),
community_id,
community.id,
local_user_view.local_user.id,
)
.await?
@ -119,7 +113,7 @@ pub async fn create_post(
// Only need to check if language is allowed in case user set it explicitly. When using default
// language, it already only returns allowed languages.
CommunityLanguage::is_allowed_community_language(&mut context.pool(), language_id, community_id)
CommunityLanguage::is_allowed_community_language(&mut context.pool(), language_id, community.id)
.await?;
let scheduled_publish_time =
@ -142,6 +136,7 @@ pub async fn create_post(
.await
.with_lemmy_type(LemmyErrorType::CouldntCreatePost)?;
let community_id = community.id;
let federate_post = if scheduled_publish_time.is_none() {
send_webmention(inserted_post.clone(), community);
|post| Some(SendActivityData::CreatePost(post))

View file

@ -8,7 +8,10 @@ use lemmy_api_common::{
utils::check_community_user_action,
};
use lemmy_db_schema::{
source::post::{Post, PostUpdateForm},
source::{
community::Community,
post::{Post, PostUpdateForm},
},
traits::Crud,
};
use lemmy_db_views::structs::LocalUserView;
@ -28,12 +31,8 @@ pub async fn delete_post(
Err(LemmyErrorType::CouldntUpdatePost)?
}
check_community_user_action(
&local_user_view.person,
orig_post.community_id,
&mut context.pool(),
)
.await?;
let community = Community::read(&mut context.pool(), orig_post.community_id).await?;
check_community_user_action(&local_user_view.person, &community, &mut context.pool()).await?;
// Verify that only the creator can delete
if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) {
@ -54,8 +53,7 @@ pub async fn delete_post(
ActivityChannel::submit_activity(
SendActivityData::DeletePost(post, local_user_view.person.clone(), data.0),
&context,
)
.await?;
)?;
build_post_response(
&context,

View file

@ -9,6 +9,7 @@ use lemmy_api_common::{
};
use lemmy_db_schema::{
source::{
community::Community,
local_user::LocalUser,
moderator::{ModRemovePost, ModRemovePostForm},
post::{Post, PostUpdateForm},
@ -26,11 +27,16 @@ pub async fn remove_post(
local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> {
let post_id = data.post_id;
// We cannot use PostView to avoid a database read here, as it doesn't return removed items
// by default. So we would have to pass in `is_mod_or_admin`, but that is impossible without
// knowing which community the post belongs to.
let orig_post = Post::read(&mut context.pool(), post_id).await?;
let community = Community::read(&mut context.pool(), orig_post.community_id).await?;
check_community_mod_action(
&local_user_view.person,
orig_post.community_id,
&community,
false,
&mut context.pool(),
)
@ -77,8 +83,7 @@ pub async fn remove_post(
removed: data.removed,
},
&context,
)
.await?;
)?;
build_post_response(&context, orig_post.community_id, local_user_view, post_id).await
}

View file

@ -24,7 +24,7 @@ use lemmy_db_schema::{
traits::Crud,
utils::{diesel_string_update, diesel_url_update, naive_now},
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views::structs::{LocalUserView, PostView};
use lemmy_utils::{
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
utils::{
@ -87,17 +87,17 @@ pub async fn update_post(
}
let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id).await?;
let orig_post = PostView::read(&mut context.pool(), post_id, None, false).await?;
check_community_user_action(
&local_user_view.person,
orig_post.community_id,
&orig_post.community,
&mut context.pool(),
)
.await?;
// Verify that only the creator can edit
if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) {
if !Post::is_post_creator(local_user_view.person.id, orig_post.post.creator_id) {
Err(LemmyErrorType::NoPostEditAllowed)?
}
@ -105,14 +105,14 @@ pub async fn update_post(
CommunityLanguage::is_allowed_community_language(
&mut context.pool(),
language_id,
orig_post.community_id,
orig_post.community.id,
)
.await?;
}
// handle changes to scheduled_publish_time
let scheduled_publish_time = match (
orig_post.scheduled_publish_time,
orig_post.post.scheduled_publish_time,
data.scheduled_publish_time,
) {
// schedule time can be changed if post is still scheduled (and not published yet)
@ -144,12 +144,12 @@ pub async fn update_post(
// send out federation/webmention if necessary
match (
orig_post.scheduled_publish_time,
orig_post.post.scheduled_publish_time,
data.scheduled_publish_time,
) {
// schedule was removed, send create activity and webmention
(Some(_), None) => {
let community = Community::read(&mut context.pool(), orig_post.community_id).await?;
let community = Community::read(&mut context.pool(), orig_post.community.id).await?;
send_webmention(updated_post.clone(), community);
generate_post_link_metadata(
updated_post.clone(),
@ -175,7 +175,7 @@ pub async fn update_post(
build_post_response(
context.deref(),
orig_post.community_id,
orig_post.community.id,
local_user_view,
post_id,
)

View file

@ -78,8 +78,7 @@ pub async fn create_private_message(
ActivityChannel::submit_activity(
SendActivityData::CreatePrivateMessage(view.clone()),
&context,
)
.await?;
)?;
Ok(Json(PrivateMessageResponse {
private_message_view: view,

View file

@ -42,8 +42,7 @@ pub async fn delete_private_message(
ActivityChannel::submit_activity(
SendActivityData::DeletePrivateMessage(local_user_view.person, private_message, data.deleted),
&context,
)
.await?;
)?;
let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?;
Ok(Json(PrivateMessageResponse {

View file

@ -59,8 +59,7 @@ pub async fn update_private_message(
ActivityChannel::submit_activity(
SendActivityData::UpdatePrivateMessage(view.clone()),
&context,
)
.await?;
)?;
Ok(Json(PrivateMessageResponse {
private_message_view: view,

View file

@ -45,8 +45,7 @@ pub async fn delete_account(
ActivityChannel::submit_activity(
SendActivityData::DeleteUser(local_user_view.person, data.delete_content),
&context,
)
.await?;
)?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -1,3 +1,4 @@
use super::to_and_audience;
use crate::{
activities::{
block::{generate_cc, SiteOrCommunity},
@ -7,6 +8,7 @@ use crate::{
verify_is_public,
verify_mod_action,
verify_person_in_community,
verify_visibility,
},
activity_lists::AnnouncableActivities,
insert_received_activity,
@ -15,7 +17,7 @@ use crate::{
};
use activitypub_federation::{
config::Data,
kinds::{activity::BlockType, public},
kinds::activity::BlockType,
protocol::verification::verify_domains_match,
traits::{ActivityHandler, Actor},
};
@ -52,14 +54,10 @@ impl BlockUser {
expires: Option<DateTime<Utc>>,
context: &Data<LemmyContext>,
) -> LemmyResult<BlockUser> {
let audience = if let SiteOrCommunity::Community(c) = target {
Some(c.id().into())
} else {
None
};
let (to, audience) = to_and_audience(target)?;
Ok(BlockUser {
actor: mod_.id().into(),
to: vec![public()],
to,
object: user.id().into(),
cc: generate_cc(target, &mut context.pool()).await?,
target: target.id(),
@ -125,9 +123,9 @@ impl ActivityHandler for BlockUser {
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<LemmyContext>) -> LemmyResult<()> {
verify_is_public(&self.to, &self.cc)?;
match self.target.dereference(context).await? {
SiteOrCommunity::Site(site) => {
verify_is_public(&self.to, &self.cc)?;
let domain = self
.object
.inner()
@ -143,6 +141,7 @@ impl ActivityHandler for BlockUser {
verify_domains_match(&site.id(), self.object.inner())?;
}
SiteOrCommunity::Community(community) => {
verify_visibility(&self.to, &self.cc, &community)?;
verify_person_in_community(&self.actor, &community, context).await?;
verify_mod_action(&self.actor, &community, context).await?;
}
@ -194,11 +193,7 @@ impl ActivityHandler for BlockUser {
CommunityPersonBan::ban(&mut context.pool(), &community_user_ban_form).await?;
// Also unsubscribe them from the community, if they are subscribed
let community_follower_form = CommunityFollowerForm {
community_id: community.id,
person_id: blocked_person.id,
pending: false,
};
let community_follower_form = CommunityFollowerForm::new(community.id, blocked_person.id);
CommunityFollower::unfollow(&mut context.pool(), &community_follower_form)
.await
.ok();

View file

@ -1,3 +1,4 @@
use super::generate_to;
use crate::{
objects::{community::ApubCommunity, instance::ApubSite},
protocol::{
@ -8,6 +9,7 @@ use crate::{
use activitypub_federation::{
config::Data,
fetch::object_id::ObjectId,
kinds::public,
traits::{Actor, Object},
};
use chrono::{DateTime, Utc};
@ -205,3 +207,13 @@ pub(crate) async fn send_ban_from_community(
.await
}
}
fn to_and_audience(
target: &SiteOrCommunity,
) -> LemmyResult<(Vec<Url>, Option<ObjectId<ApubCommunity>>)> {
Ok(if let SiteOrCommunity::Community(c) = target {
(vec![generate_to(c)?], Some(c.id().into()))
} else {
(vec![public()], None)
})
}

View file

@ -1,3 +1,4 @@
use super::to_and_audience;
use crate::{
activities::{
block::{generate_cc, SiteOrCommunity},
@ -5,6 +6,7 @@ use crate::{
generate_activity_id,
send_lemmy_activity,
verify_is_public,
verify_visibility,
},
activity_lists::AnnouncableActivities,
insert_received_activity,
@ -13,7 +15,7 @@ use crate::{
};
use activitypub_federation::{
config::Data,
kinds::{activity::UndoType, public},
kinds::activity::UndoType,
protocol::verification::verify_domains_match,
traits::{ActivityHandler, Actor},
};
@ -44,11 +46,7 @@ impl UndoBlockUser {
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
let block = BlockUser::new(target, user, mod_, None, reason, None, context).await?;
let audience = if let SiteOrCommunity::Community(c) = target {
Some(c.id().into())
} else {
None
};
let (to, audience) = to_and_audience(target)?;
let id = generate_activity_id(
UndoType::Undo,
@ -56,7 +54,7 @@ impl UndoBlockUser {
)?;
let undo = UndoBlockUser {
actor: mod_.id().into(),
to: vec![public()],
to,
object: block,
cc: generate_cc(target, &mut context.pool()).await?,
kind: UndoType::Undo,
@ -94,7 +92,6 @@ impl ActivityHandler for UndoBlockUser {
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<LemmyContext>) -> LemmyResult<()> {
verify_is_public(&self.to, &self.cc)?;
verify_domains_match(self.actor.inner(), self.object.actor.inner())?;
self.object.verify(context).await?;
Ok(())
@ -108,6 +105,7 @@ impl ActivityHandler for UndoBlockUser {
let blocked_person = self.object.object.dereference(context).await?;
match self.object.target.dereference(context).await? {
SiteOrCommunity::Site(_site) => {
verify_is_public(&self.to, &self.cc)?;
let blocked_person = Person::update(
&mut context.pool(),
blocked_person.id,
@ -135,6 +133,7 @@ impl ActivityHandler for UndoBlockUser {
ModBan::create(&mut context.pool(), &form).await?;
}
SiteOrCommunity::Community(community) => {
verify_visibility(&self.to, &self.cc, &community)?;
let community_user_ban_form = CommunityPersonBanForm {
community_id: community.id,
person_id: blocked_person.id,

View file

@ -2,9 +2,10 @@ use crate::{
activities::{
generate_activity_id,
generate_announce_activity_id,
generate_to,
send_lemmy_activity,
verify_is_public,
verify_person_in_community,
verify_visibility,
},
activity_lists::AnnouncableActivities,
insert_received_activity,
@ -18,7 +19,7 @@ use crate::{
};
use activitypub_federation::{
config::Data,
kinds::{activity::AnnounceType, public},
kinds::activity::AnnounceType,
traits::{ActivityHandler, Actor},
};
use lemmy_api_common::context::LemmyContext;
@ -92,7 +93,7 @@ impl AnnounceActivity {
generate_announce_activity_id(inner_kind, &context.settings().get_protocol_and_hostname())?;
Ok(AnnounceActivity {
actor: community.id().into(),
to: vec![public()],
to: vec![generate_to(community)?],
object: IdOrNestedObject::NestedObject(object),
cc: community
.followers_url
@ -154,7 +155,6 @@ impl ActivityHandler for AnnounceActivity {
#[tracing::instrument(skip_all)]
async fn verify(&self, _context: &Data<Self::DataType>) -> LemmyResult<()> {
verify_is_public(&self.to, &self.cc)?;
Ok(())
}
@ -169,6 +169,7 @@ impl ActivityHandler for AnnounceActivity {
}
let community = object.community(context).await?;
verify_visibility(&self.to, &self.cc, &community)?;
can_accept_activity_in_community(&Some(community), context).await?;
// verify here in order to avoid fetching the object twice over http

View file

@ -2,9 +2,10 @@ use crate::{
activities::{
community::send_activity_in_community,
generate_activity_id,
verify_is_public,
generate_to,
verify_mod_action,
verify_person_in_community,
verify_visibility,
},
activity_lists::AnnouncableActivities,
insert_received_activity,
@ -17,7 +18,7 @@ use crate::{
use activitypub_federation::{
config::Data,
fetch::object_id::ObjectId,
kinds::{activity::AddType, public},
kinds::activity::AddType,
traits::{ActivityHandler, Actor},
};
use lemmy_api_common::{
@ -53,7 +54,7 @@ impl CollectionAdd {
)?;
let add = CollectionAdd {
actor: actor.id().into(),
to: vec![public()],
to: vec![generate_to(community)?],
object: added_mod.id(),
target: generate_moderators_url(&community.actor_id)?.into(),
cc: vec![community.id()],
@ -79,7 +80,7 @@ impl CollectionAdd {
)?;
let add = CollectionAdd {
actor: actor.id().into(),
to: vec![public()],
to: vec![generate_to(community)?],
object: featured_post.ap_id.clone().into(),
target: generate_featured_url(&community.actor_id)?.into(),
cc: vec![community.id()],
@ -115,8 +116,8 @@ impl ActivityHandler for CollectionAdd {
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<Self::DataType>) -> LemmyResult<()> {
verify_is_public(&self.to, &self.cc)?;
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?;
Ok(())

View file

@ -2,9 +2,10 @@ use crate::{
activities::{
community::send_activity_in_community,
generate_activity_id,
verify_is_public,
generate_to,
verify_mod_action,
verify_person_in_community,
verify_visibility,
},
activity_lists::AnnouncableActivities,
insert_received_activity,
@ -14,7 +15,7 @@ use crate::{
use activitypub_federation::{
config::Data,
fetch::object_id::ObjectId,
kinds::{activity::RemoveType, public},
kinds::activity::RemoveType,
traits::{ActivityHandler, Actor},
};
use lemmy_api_common::{
@ -48,7 +49,7 @@ impl CollectionRemove {
)?;
let remove = CollectionRemove {
actor: actor.id().into(),
to: vec![public()],
to: vec![generate_to(community)?],
object: removed_mod.id(),
target: generate_moderators_url(&community.actor_id)?.into(),
id: id.clone(),
@ -74,7 +75,7 @@ impl CollectionRemove {
)?;
let remove = CollectionRemove {
actor: actor.id().into(),
to: vec![public()],
to: vec![generate_to(community)?],
object: featured_post.ap_id.clone().into(),
target: generate_featured_url(&community.actor_id)?.into(),
cc: vec![community.id()],
@ -110,8 +111,8 @@ impl ActivityHandler for CollectionRemove {
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<Self::DataType>) -> LemmyResult<()> {
verify_is_public(&self.to, &self.cc)?;
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?;
Ok(())

View file

@ -3,9 +3,10 @@ use crate::{
check_community_deleted_or_removed,
community::send_activity_in_community,
generate_activity_id,
verify_is_public,
generate_to,
verify_mod_action,
verify_person_in_community,
verify_visibility,
},
activity_lists::AnnouncableActivities,
insert_received_activity,
@ -18,7 +19,7 @@ use crate::{
use activitypub_federation::{
config::Data,
fetch::object_id::ObjectId,
kinds::{activity::UndoType, public},
kinds::activity::UndoType,
traits::ActivityHandler,
};
use lemmy_api_common::context::LemmyContext;
@ -49,8 +50,8 @@ impl ActivityHandler for LockPage {
}
async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), Self::Error> {
verify_is_public(&self.to, &self.cc)?;
let community = self.community(context).await?;
verify_visibility(&self.to, &self.cc, &community)?;
verify_person_in_community(&self.actor, &community, context).await?;
check_community_deleted_or_removed(&community)?;
verify_mod_action(&self.actor, &community, context).await?;
@ -92,8 +93,8 @@ impl ActivityHandler for UndoLockPage {
}
async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), Self::Error> {
verify_is_public(&self.to, &self.cc)?;
let community = self.community(context).await?;
verify_visibility(&self.to, &self.cc, &community)?;
verify_person_in_community(&self.actor, &community, context).await?;
check_community_deleted_or_removed(&community)?;
verify_mod_action(&self.actor, &community, context).await?;
@ -137,7 +138,7 @@ pub(crate) async fn send_lock_post(
let community_id = community.actor_id.inner().clone();
let lock = LockPage {
actor: actor.actor_id.clone().into(),
to: vec![public()],
to: vec![generate_to(&community)?],
object: ObjectId::from(post.ap_id),
cc: vec![community_id.clone()],
kind: LockType::Lock,
@ -153,7 +154,7 @@ pub(crate) async fn send_lock_post(
)?;
let undo = UndoLockPage {
actor: lock.actor.clone(),
to: vec![public()],
to: vec![generate_to(&community)?],
cc: lock.cc.clone(),
kind: UndoType::Undo,
id,

View file

@ -2,9 +2,10 @@ use crate::{
activities::{
community::send_activity_in_community,
generate_activity_id,
verify_is_public,
generate_to,
verify_mod_action,
verify_person_in_community,
verify_visibility,
},
activity_lists::AnnouncableActivities,
insert_received_activity,
@ -13,7 +14,7 @@ use crate::{
};
use activitypub_federation::{
config::Data,
kinds::{activity::UpdateType, public},
kinds::activity::UpdateType,
traits::{ActivityHandler, Actor, Object},
};
use lemmy_api_common::context::LemmyContext;
@ -42,7 +43,7 @@ pub(crate) async fn send_update_community(
)?;
let update = UpdateCommunity {
actor: actor.id().into(),
to: vec![public()],
to: vec![generate_to(&community)?],
object: Box::new(community.clone().into_json(&context).await?),
cc: vec![community.id()],
kind: UpdateType::Update,
@ -77,8 +78,8 @@ impl ActivityHandler for UpdateCommunity {
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<Self::DataType>) -> LemmyResult<()> {
verify_is_public(&self.to, &self.cc)?;
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.actor_id.clone().into(), context).await?;

View file

@ -3,8 +3,9 @@ use crate::{
check_community_deleted_or_removed,
community::send_activity_in_community,
generate_activity_id,
verify_is_public,
generate_to,
verify_person_in_community,
verify_visibility,
},
activity_lists::AnnouncableActivities,
insert_received_activity,
@ -18,7 +19,6 @@ use crate::{
use activitypub_federation::{
config::Data,
fetch::object_id::ObjectId,
kinds::public,
protocol::verification::{verify_domains_match, verify_urls_match},
traits::{ActivityHandler, Actor, Object},
};
@ -70,7 +70,7 @@ impl CreateOrUpdateNote {
let create_or_update = CreateOrUpdateNote {
actor: person.id().into(),
to: vec![public()],
to: vec![generate_to(&community)?],
cc: note.cc.clone(),
tag: note.tag.clone(),
object: note,
@ -118,9 +118,9 @@ impl ActivityHandler for CreateOrUpdateNote {
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<Self::DataType>) -> LemmyResult<()> {
verify_is_public(&self.to, &self.cc)?;
let post = self.object.get_parents(context).await?.0;
let community = self.community(context).await?;
verify_visibility(&self.to, &self.cc, &community)?;
verify_person_in_community(&self.actor, &community, context).await?;
verify_domains_match(self.actor.inner(), self.object.id.inner())?;

View file

@ -3,8 +3,9 @@ use crate::{
check_community_deleted_or_removed,
community::send_activity_in_community,
generate_activity_id,
verify_is_public,
generate_to,
verify_person_in_community,
verify_visibility,
},
activity_lists::AnnouncableActivities,
insert_received_activity,
@ -16,7 +17,6 @@ use crate::{
};
use activitypub_federation::{
config::Data,
kinds::public,
protocol::verification::{verify_domains_match, verify_urls_match},
traits::{ActivityHandler, Actor, Object},
};
@ -49,7 +49,7 @@ impl CreateOrUpdatePage {
)?;
Ok(CreateOrUpdatePage {
actor: actor.id().into(),
to: vec![public()],
to: vec![generate_to(community)?],
object: post.into_json(context).await?,
cc: vec![community.id()],
kind,
@ -102,8 +102,8 @@ impl ActivityHandler for CreateOrUpdatePage {
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<LemmyContext>) -> LemmyResult<()> {
verify_is_public(&self.to, &self.cc)?;
let community = self.community(context).await?;
verify_visibility(&self.to, &self.cc, &community)?;
verify_person_in_community(&self.actor, &community, context).await?;
check_community_deleted_or_removed(&community)?;
verify_domains_match(self.actor.inner(), self.object.id.inner())?;

View file

@ -84,7 +84,7 @@ impl Delete {
pub(in crate::activities::deletion) fn new(
actor: &ApubPerson,
object: DeletableObjects,
to: Url,
to: Vec<Url>,
community: Option<&Community>,
summary: Option<String>,
context: &Data<LemmyContext>,
@ -96,7 +96,7 @@ impl Delete {
let cc: Option<Url> = community.map(|c| c.actor_id.clone().into());
Ok(Delete {
actor: actor.actor_id.clone().into(),
to: vec![to],
to,
object: IdOrNestedObject::Id(object.id()),
cc: cc.into_iter().collect(),
kind: DeleteType::Delete,

View file

@ -1,11 +1,12 @@
use super::{generate_to, verify_is_public};
use crate::{
activities::{
community::send_activity_in_community,
send_lemmy_activity,
verify_is_public,
verify_mod_action,
verify_person,
verify_person_in_community,
verify_visibility,
},
activity_lists::AnnouncableActivities,
objects::{
@ -59,11 +60,12 @@ pub(crate) async fn send_apub_delete_in_community(
) -> LemmyResult<()> {
let actor = ApubPerson::from(actor);
let is_mod_action = reason.is_some();
let to = vec![generate_to(&community)?];
let activity = if deleted {
let delete = Delete::new(&actor, object, public(), Some(&community), reason, context)?;
let delete = Delete::new(&actor, object, to, Some(&community), reason, context)?;
AnnouncableActivities::Delete(delete)
} else {
let undo = UndoDelete::new(&actor, object, public(), Some(&community), reason, context)?;
let undo = UndoDelete::new(&actor, object, to, Some(&community), reason, context)?;
AnnouncableActivities::UndoDelete(undo)
};
send_activity_in_community(
@ -92,10 +94,10 @@ pub(crate) async fn send_apub_delete_private_message(
let deletable = DeletableObjects::PrivateMessage(pm.into());
let inbox = ActivitySendTargets::to_inbox(recipient.shared_inbox_or_inbox());
if deleted {
let delete: Delete = Delete::new(actor, deletable, recipient.id(), None, None, &context)?;
let delete: Delete = Delete::new(actor, deletable, vec![recipient.id()], None, None, &context)?;
send_lemmy_activity(&context, delete, actor, inbox, true).await?;
} else {
let undo = UndoDelete::new(actor, deletable, recipient.id(), None, None, &context)?;
let undo = UndoDelete::new(actor, deletable, vec![recipient.id()], None, None, &context)?;
send_lemmy_activity(&context, undo, actor, inbox, true).await?;
};
Ok(())
@ -109,7 +111,7 @@ pub async fn send_apub_delete_user(
let person: ApubPerson = person.into();
let deletable = DeletableObjects::Person(person.clone());
let mut delete: Delete = Delete::new(&person, deletable, public(), None, None, &context)?;
let mut delete: Delete = Delete::new(&person, deletable, vec![public()], None, None, &context)?;
delete.remove_data = Some(remove_data);
let inboxes = ActivitySendTargets::to_all_instances();
@ -170,7 +172,7 @@ pub(in crate::activities) async fn verify_delete_activity(
let object = DeletableObjects::read_from_db(activity.object.id(), context).await?;
match object {
DeletableObjects::Community(community) => {
verify_is_public(&activity.to, &[])?;
verify_visibility(&activity.to, &[], &community)?;
if community.local {
// can only do this check for local community, in remote case it would try to fetch the
// deleted community (which fails)
@ -185,22 +187,24 @@ pub(in crate::activities) async fn verify_delete_activity(
verify_urls_match(person.actor_id.inner(), activity.object.id())?;
}
DeletableObjects::Post(p) => {
verify_is_public(&activity.to, &[])?;
let community = activity.community(context).await?;
verify_visibility(&activity.to, &[], &community)?;
verify_delete_post_or_comment(
&activity.actor,
&p.ap_id.clone().into(),
&activity.community(context).await?,
&community,
is_mod_action,
context,
)
.await?;
}
DeletableObjects::Comment(c) => {
verify_is_public(&activity.to, &[])?;
let community = activity.community(context).await?;
verify_visibility(&activity.to, &[], &community)?;
verify_delete_post_or_comment(
&activity.actor,
&c.ap_id.clone().into(),
&activity.community(context).await?,
&community,
is_mod_action,
context,
)

View file

@ -68,7 +68,7 @@ impl UndoDelete {
pub(in crate::activities::deletion) fn new(
actor: &ApubPerson,
object: DeletableObjects,
to: Url,
to: Vec<Url>,
community: Option<&Community>,
summary: Option<String>,
context: &Data<LemmyContext>,
@ -82,7 +82,7 @@ impl UndoDelete {
let cc: Option<Url> = community.map(|c| c.actor_id.clone().into());
Ok(UndoDelete {
actor: actor.actor_id.clone().into(),
to: vec![to],
to,
object,
cc: cc.into_iter().collect(),
kind: UndoType::Undo,

View file

@ -20,7 +20,7 @@ use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
source::{
activity::ActivitySendTargets,
community::{CommunityFollower, CommunityFollowerForm},
community::{CommunityFollower, CommunityFollowerForm, CommunityFollowerState},
person::{PersonFollower, PersonFollowerForm},
},
traits::Followable,
@ -102,21 +102,25 @@ impl ActivityHandler for Follow {
pending: false,
};
PersonFollower::follow(&mut context.pool(), &form).await?;
AcceptFollow::send(self, context).await?;
}
UserOrCommunity::Community(c) => {
// Dont allow following local-only community via federation.
if c.visibility != CommunityVisibility::Public {
return Err(LemmyErrorType::NotFound.into());
}
let state = Some(match c.visibility {
CommunityVisibility::Public => CommunityFollowerState::Accepted,
CommunityVisibility::Private => CommunityFollowerState::ApprovalRequired,
// Dont allow following local-only community via federation.
CommunityVisibility::LocalOnly => return Err(LemmyErrorType::NotFound.into()),
});
let form = CommunityFollowerForm {
community_id: c.id,
person_id: actor.id,
pending: false,
state,
..CommunityFollowerForm::new(c.id, actor.id)
};
CommunityFollower::follow(&mut context.pool(), &form).await?;
if c.visibility == CommunityVisibility::Public {
AcceptFollow::send(self, context).await?;
}
}
}
AcceptFollow::send(self, context).await
Ok(())
}
}

View file

@ -1,15 +1,26 @@
use super::generate_activity_id;
use crate::{
objects::{community::ApubCommunity, person::ApubPerson},
protocol::activities::following::{follow::Follow, undo_follow::UndoFollow},
protocol::activities::following::{
accept::AcceptFollow,
follow::Follow,
reject::RejectFollow,
undo_follow::UndoFollow,
},
};
use activitypub_federation::config::Data;
use activitypub_federation::{config::Data, kinds::activity::FollowType};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::source::{community::Community, person::Person};
use lemmy_db_schema::{
newtypes::{CommunityId, PersonId},
source::{community::Community, person::Person},
traits::Crud,
};
use lemmy_utils::error::LemmyResult;
pub mod accept;
pub mod follow;
pub mod undo_follow;
pub(crate) mod accept;
pub(crate) mod follow;
pub(crate) mod reject;
pub(crate) mod undo_follow;
pub async fn send_follow_community(
community: Community,
@ -25,3 +36,29 @@ pub async fn send_follow_community(
UndoFollow::send(&actor, &community, context).await
}
}
pub async fn send_accept_or_reject_follow(
community_id: CommunityId,
person_id: PersonId,
accepted: bool,
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
let community = Community::read(&mut context.pool(), community_id).await?;
let person = Person::read(&mut context.pool(), person_id).await?;
let follow = Follow {
actor: person.actor_id.into(),
to: Some([community.actor_id.clone().into()]),
object: community.actor_id.into(),
kind: FollowType::Follow,
id: generate_activity_id(
FollowType::Follow,
&context.settings().get_protocol_and_hostname(),
)?,
};
if accepted {
AcceptFollow::send(follow, context).await
} else {
RejectFollow::send(follow, context).await
}
}

View file

@ -0,0 +1,79 @@
use crate::{
activities::{generate_activity_id, send_lemmy_activity},
insert_received_activity,
protocol::activities::following::{follow::Follow, reject::RejectFollow},
};
use activitypub_federation::{
config::Data,
kinds::activity::RejectType,
protocol::verification::verify_urls_match,
traits::{ActivityHandler, Actor},
};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
source::{
activity::ActivitySendTargets,
community::{CommunityFollower, CommunityFollowerForm},
},
traits::Followable,
};
use lemmy_utils::error::{LemmyError, LemmyResult};
use url::Url;
impl RejectFollow {
#[tracing::instrument(skip_all)]
pub async fn send(follow: Follow, context: &Data<LemmyContext>) -> LemmyResult<()> {
let user_or_community = follow.object.dereference_local(context).await?;
let person = follow.actor.clone().dereference(context).await?;
let reject = RejectFollow {
actor: user_or_community.id().into(),
to: Some([person.id().into()]),
object: follow,
kind: RejectType::Reject,
id: generate_activity_id(
RejectType::Reject,
&context.settings().get_protocol_and_hostname(),
)?,
};
let inbox = ActivitySendTargets::to_inbox(person.shared_inbox_or_inbox());
send_lemmy_activity(context, reject, &user_or_community, inbox, true).await
}
}
/// Handle rejected follows
#[async_trait::async_trait]
impl ActivityHandler for RejectFollow {
type DataType = LemmyContext;
type Error = LemmyError;
fn id(&self) -> &Url {
&self.id
}
fn actor(&self) -> &Url {
self.actor.inner()
}
#[tracing::instrument(skip_all)]
async fn verify(&self, context: &Data<LemmyContext>) -> LemmyResult<()> {
verify_urls_match(self.actor.inner(), self.object.object.inner())?;
self.object.verify(context).await?;
if let Some(to) = &self.to {
verify_urls_match(to[0].inner(), self.object.actor.inner())?;
}
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(self, context: &Data<LemmyContext>) -> LemmyResult<()> {
insert_received_activity(&self.id, context).await?;
let community = self.actor.dereference(context).await?;
let person = self.object.actor.dereference(context).await?;
// remove the follow
let form = CommunityFollowerForm::new(community.id, person.id);
CommunityFollower::unfollow(&mut context.pool(), &form).await?;
Ok(())
}
}

View file

@ -90,11 +90,7 @@ impl ActivityHandler for UndoFollow {
PersonFollower::unfollow(&mut context.pool(), &form).await?;
}
UserOrCommunity::Community(c) => {
let form = CommunityFollowerForm {
community_id: c.id,
person_id: person.id,
pending: false,
};
let form = CommunityFollowerForm::new(c.id, person.id);
CommunityFollower::unfollow(&mut context.pool(), &form).await?;
}
}

View file

@ -30,6 +30,7 @@ use activitypub_federation::{
traits::{ActivityHandler, Actor},
};
use anyhow::anyhow;
use following::send_accept_or_reject_follow;
use lemmy_api_common::{
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
@ -40,6 +41,7 @@ use lemmy_db_schema::{
community::Community,
},
traits::Crud,
CommunityVisibility,
};
use lemmy_db_views_actor::structs::{CommunityPersonBanView, CommunityView};
use lemmy_utils::error::{FederationError, LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult};
@ -120,6 +122,28 @@ pub(crate) fn verify_is_public(to: &[Url], cc: &[Url]) -> LemmyResult<()> {
}
}
/// Returns an error if object visibility doesnt match community visibility
/// (ie content in private community must also be private).
pub(crate) fn verify_visibility(to: &[Url], cc: &[Url], community: &Community) -> LemmyResult<()> {
use CommunityVisibility::*;
let object_is_public = [to, cc].iter().any(|set| set.contains(&public()));
match community.visibility {
Public if !object_is_public => Err(FederationError::ObjectIsNotPublic)?,
Private if object_is_public => Err(FederationError::ObjectIsNotPrivate)?,
LocalOnly => Err(LemmyErrorType::NotFound.into()),
_ => Ok(()),
}
}
/// Marks object as public only if the community is public
pub(crate) fn generate_to(community: &Community) -> LemmyResult<Url> {
if community.visibility == CommunityVisibility::Public {
Ok(public())
} else {
Ok(Url::parse(&format!("{}/followers", community.actor_id))?)
}
}
pub(crate) fn verify_community_matches<T>(a: &ObjectId<ApubCommunity>, b: T) -> LemmyResult<()>
where
T: Into<ObjectId<ApubCommunity>>,
@ -367,6 +391,12 @@ pub async fn match_outgoing_activities(
community,
reason,
} => Report::send(ObjectId::from(object_id), actor, community, reason, context).await,
AcceptFollower(community_id, person_id) => {
send_accept_or_reject_follow(community_id, person_id, true, &context).await
}
RejectFollower(community_id, person_id) => {
send_accept_or_reject_follow(community_id, person_id, false, &context).await
}
}
};
fed_task.await?;

View file

@ -17,7 +17,12 @@ use crate::{
page::CreateOrUpdatePage,
},
deletion::{delete::Delete, undo_delete::UndoDelete},
following::{accept::AcceptFollow, follow::Follow, undo_follow::UndoFollow},
following::{
accept::AcceptFollow,
follow::Follow,
reject::RejectFollow,
undo_follow::UndoFollow,
},
voting::{undo_vote::UndoVote, vote::Vote},
},
objects::page::Page,
@ -41,6 +46,7 @@ use url::Url;
pub enum SharedInboxActivities {
Follow(Follow),
AcceptFollow(AcceptFollow),
RejectFollow(RejectFollow),
UndoFollow(UndoFollow),
CreateOrUpdatePrivateMessage(CreateOrUpdateChatMessage),
Report(Report),
@ -68,6 +74,7 @@ pub enum GroupInboxActivities {
pub enum PersonInboxActivities {
Follow(Follow),
AcceptFollow(AcceptFollow),
RejectFollow(RejectFollow),
UndoFollow(UndoFollow),
CreateOrUpdatePrivateMessage(CreateOrUpdateChatMessage),
Delete(Delete),

View file

@ -13,7 +13,7 @@ use lemmy_db_schema::{
newtypes::DbUrl,
source::{
comment::{CommentSaved, CommentSavedForm},
community::{CommunityFollower, CommunityFollowerForm},
community::{CommunityFollower, CommunityFollowerForm, CommunityFollowerState},
community_block::{CommunityBlock, CommunityBlockForm},
instance::Instance,
instance_block::{InstanceBlock, InstanceBlockForm},
@ -186,9 +186,8 @@ pub async fn import_settings(
|(followed, context)| async move {
let community = followed.dereference(&context).await?;
let form = CommunityFollowerForm {
person_id,
community_id: community.id,
pending: true,
state: Some(CommunityFollowerState::Pending),
..CommunityFollowerForm::new(community.id, person_id)
};
CommunityFollower::follow(&mut context.pool(), &form).await?;
LemmyResult::Ok(())
@ -319,7 +318,13 @@ pub(crate) mod tests {
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
source::{
community::{Community, CommunityFollower, CommunityFollowerForm, CommunityInsertForm},
community::{
Community,
CommunityFollower,
CommunityFollowerForm,
CommunityFollowerState,
CommunityInsertForm,
},
local_user::LocalUser,
},
traits::{Crud, Followable},
@ -327,7 +332,6 @@ pub(crate) mod tests {
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::CommunityFollowerView;
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
use pretty_assertions::assert_eq;
use serial_test::serial;
use std::time::Duration;
use tokio::time::sleep;
@ -348,9 +352,8 @@ pub(crate) mod tests {
);
let community = Community::create(pool, &community_form).await?;
let follower_form = CommunityFollowerForm {
community_id: community.id,
person_id: export_user.person.id,
pending: false,
state: Some(CommunityFollowerState::Accepted),
..CommunityFollowerForm::new(community.id, export_user.person.id)
};
CommunityFollower::follow(pool, &follower_form).await?;

View file

@ -9,6 +9,7 @@ use activitypub_federation::{
};
use chrono::{DateTime, Utc};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::newtypes::InstanceId;
use lemmy_utils::error::{LemmyError, LemmyResult};
use reqwest::Url;
use serde::{Deserialize, Serialize};
@ -127,3 +128,13 @@ impl Actor for SiteOrCommunityOrUser {
}
}
}
impl SiteOrCommunityOrUser {
pub fn instance_id(&self) -> InstanceId {
match self {
SiteOrCommunityOrUser::Site(s) => s.instance_id,
SiteOrCommunityOrUser::UserOrCommunity(UserOrCommunity::User(u)) => u.instance_id,
SiteOrCommunityOrUser::UserOrCommunity(UserOrCommunity::Community(c)) => c.instance_id,
}
}
}

View file

@ -1,14 +1,10 @@
use super::check_community_content_fetchable;
use crate::{
http::{
check_community_public,
create_apub_response,
create_apub_tombstone_response,
redirect_remote_object,
},
http::{create_apub_response, create_apub_tombstone_response, redirect_remote_object},
objects::comment::ApubComment,
};
use activitypub_federation::{config::Data, traits::Object};
use actix_web::{web::Path, HttpResponse};
use actix_web::{web::Path, HttpRequest, HttpResponse};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
newtypes::CommentId,
@ -28,13 +24,14 @@ pub(crate) struct CommentQuery {
pub(crate) async fn get_apub_comment(
info: Path<CommentQuery>,
context: Data<LemmyContext>,
request: HttpRequest,
) -> LemmyResult<HttpResponse> {
let id = CommentId(info.comment_id.parse::<i32>()?);
// Can't use CommentView here because it excludes deleted/removed/local-only items
let comment: ApubComment = Comment::read(&mut context.pool(), id).await?.into();
let post = Post::read(&mut context.pool(), comment.post_id).await?;
let community = Community::read(&mut context.pool(), post.community_id).await?;
check_community_public(&community)?;
check_community_content_fetchable(&community, &request, &context).await?;
if !comment.local {
Ok(redirect_remote_object(&comment.ap_id))

View file

@ -1,3 +1,4 @@
use super::check_community_content_fetchable;
use crate::{
collections::{
community_featured::ApubCommunityFeatured,
@ -5,14 +6,14 @@ use crate::{
community_moderators::ApubCommunityModerators,
community_outbox::ApubCommunityOutbox,
},
http::{check_community_public, create_apub_response, create_apub_tombstone_response},
http::{check_community_fetchable, create_apub_response, create_apub_tombstone_response},
objects::community::ApubCommunity,
};
use activitypub_federation::{
config::Data,
traits::{Collection, Object},
};
use actix_web::{web, HttpResponse};
use actix_web::{web, HttpRequest, HttpResponse};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{source::community::Community, traits::ApubActor};
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
@ -38,7 +39,7 @@ pub(crate) async fn get_apub_community_http(
if community.deleted || community.removed {
return create_apub_tombstone_response(community.actor_id.clone());
}
check_community_public(&community)?;
check_community_fetchable(&community)?;
let apub = community.into_json(&context).await?;
create_apub_response(&apub)
@ -52,7 +53,7 @@ pub(crate) async fn get_apub_community_followers(
let community = Community::read_from_name(&mut context.pool(), &info.community_name, false)
.await?
.ok_or(LemmyErrorType::NotFound)?;
check_community_public(&community)?;
check_community_fetchable(&community)?;
let followers = ApubCommunityFollower::read_local(&community.into(), &context).await?;
create_apub_response(&followers)
}
@ -62,13 +63,14 @@ pub(crate) async fn get_apub_community_followers(
pub(crate) async fn get_apub_community_outbox(
info: web::Path<CommunityQuery>,
context: Data<LemmyContext>,
request: HttpRequest,
) -> LemmyResult<HttpResponse> {
let community: ApubCommunity =
Community::read_from_name(&mut context.pool(), &info.community_name, false)
.await?
.ok_or(LemmyErrorType::NotFound)?
.into();
check_community_public(&community)?;
check_community_content_fetchable(&community, &request, &context).await?;
let outbox = ApubCommunityOutbox::read_local(&community, &context).await?;
create_apub_response(&outbox)
}
@ -83,7 +85,7 @@ pub(crate) async fn get_apub_community_moderators(
.await?
.ok_or(LemmyErrorType::NotFound)?
.into();
check_community_public(&community)?;
check_community_fetchable(&community)?;
let moderators = ApubCommunityModerators::read_local(&community, &context).await?;
create_apub_response(&moderators)
}
@ -92,13 +94,14 @@ pub(crate) async fn get_apub_community_moderators(
pub(crate) async fn get_apub_community_featured(
info: web::Path<CommunityQuery>,
context: Data<LemmyContext>,
request: HttpRequest,
) -> LemmyResult<HttpResponse> {
let community: ApubCommunity =
Community::read_from_name(&mut context.pool(), &info.community_name, false)
.await?
.ok_or(LemmyErrorType::NotFound)?
.into();
check_community_public(&community)?;
check_community_content_fetchable(&community, &request, &context).await?;
let featured = ApubCommunityFeatured::read_local(&community, &context).await?;
create_apub_response(&featured)
}
@ -108,7 +111,7 @@ pub(crate) mod tests {
use super::*;
use crate::protocol::objects::{group::Group, tombstone::Tombstone};
use actix_web::body::to_bytes;
use actix_web::{body::to_bytes, test::TestRequest};
use lemmy_db_schema::{
newtypes::InstanceId,
source::{
@ -175,6 +178,7 @@ pub(crate) mod tests {
async fn test_get_community() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let (instance, community) = init(false, CommunityVisibility::Public, &context).await?;
let request = TestRequest::default().to_http_request();
// fetch invalid community
let query = CommunityQuery {
@ -194,8 +198,12 @@ pub(crate) mod tests {
let group = community.clone().into_json(&context).await?;
assert_eq!(group, res_group);
let res =
get_apub_community_featured(query.clone().into(), context.reset_request_count()).await?;
let res = get_apub_community_featured(
query.clone().into(),
context.reset_request_count(),
request.clone(),
)
.await?;
assert_eq!(200, res.status());
let res =
get_apub_community_followers(query.clone().into(), context.reset_request_count()).await?;
@ -203,7 +211,8 @@ pub(crate) mod tests {
let res =
get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await?;
assert_eq!(200, res.status());
let res = get_apub_community_outbox(query.into(), context.reset_request_count()).await?;
let res =
get_apub_community_outbox(query.into(), context.reset_request_count(), request).await?;
assert_eq!(200, res.status());
Instance::delete(&mut context.pool(), instance.id).await?;
@ -215,6 +224,7 @@ pub(crate) mod tests {
async fn test_get_deleted_community() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let (instance, community) = init(true, CommunityVisibility::LocalOnly, &context).await?;
let request = TestRequest::default().to_http_request();
// should return tombstone
let query = CommunityQuery {
@ -225,8 +235,12 @@ pub(crate) mod tests {
let res_tombstone = decode_response::<Tombstone>(res).await;
assert!(res_tombstone.is_ok());
let res =
get_apub_community_featured(query.clone().into(), context.reset_request_count()).await;
let res = get_apub_community_featured(
query.clone().into(),
context.reset_request_count(),
request.clone(),
)
.await;
assert!(res.is_err());
let res =
get_apub_community_followers(query.clone().into(), context.reset_request_count()).await;
@ -234,7 +248,7 @@ pub(crate) mod tests {
let res =
get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await;
assert!(res.is_err());
let res = get_apub_community_outbox(query.into(), context.reset_request_count()).await;
let res = get_apub_community_outbox(query.into(), context.reset_request_count(), request).await;
assert!(res.is_err());
//Community::delete(&mut context.pool(), community.id).await?;
@ -247,14 +261,19 @@ pub(crate) mod tests {
async fn test_get_local_only_community() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let (instance, community) = init(false, CommunityVisibility::LocalOnly, &context).await?;
let request = TestRequest::default().to_http_request();
let query = CommunityQuery {
community_name: community.name.clone(),
};
let res = get_apub_community_http(query.clone().into(), context.reset_request_count()).await;
assert!(res.is_err());
let res =
get_apub_community_featured(query.clone().into(), context.reset_request_count()).await;
let res = get_apub_community_featured(
query.clone().into(),
context.reset_request_count(),
request.clone(),
)
.await;
assert!(res.is_err());
let res =
get_apub_community_followers(query.clone().into(), context.reset_request_count()).await;
@ -262,7 +281,7 @@ pub(crate) mod tests {
let res =
get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await;
assert!(res.is_err());
let res = get_apub_community_outbox(query.into(), context.reset_request_count()).await;
let res = get_apub_community_outbox(query.into(), context.reset_request_count(), request).await;
assert!(res.is_err());
Instance::delete(&mut context.pool(), instance.id).await?;

View file

@ -1,11 +1,11 @@
use crate::{
activity_lists::SharedInboxActivities,
fetcher::user_or_community::UserOrCommunity,
fetcher::{site_or_community_or_user::SiteOrCommunityOrUser, user_or_community::UserOrCommunity},
protocol::objects::tombstone::Tombstone,
FEDERATION_CONTEXT,
};
use activitypub_federation::{
actix_web::inbox::receive_activity,
actix_web::{inbox::receive_activity, signing_actor},
config::Data,
protocol::context::WithContext,
FEDERATION_CONTENT_TYPE,
@ -17,6 +17,7 @@ use lemmy_db_schema::{
source::{activity::SentActivity, community::Community},
CommunityVisibility,
};
use lemmy_db_views_actor::structs::CommunityFollowerView;
use lemmy_utils::error::{FederationError, LemmyErrorType, LemmyResult};
use serde::{Deserialize, Serialize};
use std::{ops::Deref, time::Duration};
@ -119,12 +120,46 @@ pub(crate) async fn get_activity(
}
/// Ensure that the community is public and not removed/deleted.
fn check_community_public(community: &Community) -> LemmyResult<()> {
if community.deleted || community.removed {
Err(LemmyErrorType::Deleted)?
}
if community.visibility != CommunityVisibility::Public {
fn check_community_fetchable(community: &Community) -> LemmyResult<()> {
check_community_removed_or_deleted(community)?;
if community.visibility == CommunityVisibility::LocalOnly {
return Err(LemmyErrorType::NotFound.into());
}
Ok(())
}
/// Check if posts or comments in the community are allowed to be fetched
async fn check_community_content_fetchable(
community: &Community,
request: &HttpRequest,
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
use CommunityVisibility::*;
check_community_removed_or_deleted(community)?;
match community.visibility {
// content in public community can always be fetched
Public => Ok(()),
// no federation for local only community
LocalOnly => Err(LemmyErrorType::NotFound.into()),
// for private community check http signature of request, if there is any approved follower
// from the fetching instance then fetching is allowed
Private => {
let signing_actor = signing_actor::<SiteOrCommunityOrUser>(request, None, context).await?;
Ok(
CommunityFollowerView::check_has_followers_from_instance(
community.id,
signing_actor.instance_id(),
&mut context.pool(),
)
.await?,
)
}
}
}
fn check_community_removed_or_deleted(community: &Community) -> LemmyResult<()> {
if community.deleted || community.removed {
Err(LemmyErrorType::Deleted)?
}
Ok(())
}

View file

@ -1,14 +1,10 @@
use super::check_community_content_fetchable;
use crate::{
http::{
check_community_public,
create_apub_response,
create_apub_tombstone_response,
redirect_remote_object,
},
http::{create_apub_response, create_apub_tombstone_response, redirect_remote_object},
objects::post::ApubPost,
};
use activitypub_federation::{config::Data, traits::Object};
use actix_web::{web, HttpResponse};
use actix_web::{web, HttpRequest, HttpResponse};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
newtypes::PostId,
@ -28,12 +24,14 @@ pub(crate) struct PostQuery {
pub(crate) async fn get_apub_post(
info: web::Path<PostQuery>,
context: Data<LemmyContext>,
request: HttpRequest,
) -> LemmyResult<HttpResponse> {
let id = PostId(info.post_id.parse::<i32>()?);
// Can't use PostView here because it excludes deleted/removed/local-only items
let post: ApubPost = Post::read(&mut context.pool(), id).await?.into();
let community = Community::read(&mut context.pool(), post.community_id).await?;
check_community_public(&community)?;
check_community_content_fetchable(&community, &request, &context).await?;
if !post.local {
Ok(redirect_remote_object(&post.ap_id))

View file

@ -1,5 +1,5 @@
use crate::{
activities::{verify_is_public, verify_person_in_community},
activities::{generate_to, verify_person_in_community, verify_visibility},
check_apub_id_valid_with_strictness,
fetcher::markdown_links::markdown_rewrite_remote_links,
mentions::collect_non_local_mentions,
@ -12,7 +12,7 @@ use crate::{
};
use activitypub_federation::{
config::Data,
kinds::{object::NoteType, public},
kinds::object::NoteType,
protocol::{values::MediaTypeMarkdownOrHtml, verification::verify_domains_match},
traits::Object,
};
@ -112,7 +112,7 @@ impl Object for ApubComment {
r#type: NoteType::Note,
id: self.ap_id.clone().into(),
attributed_to: creator.actor_id.into(),
to: vec![public()],
to: vec![generate_to(&community)?],
cc: maa.ccs,
content: markdown_to_html(&self.content),
media_type: Some(MediaTypeMarkdownOrHtml::Html),
@ -140,8 +140,8 @@ impl Object for ApubComment {
) -> LemmyResult<()> {
verify_domains_match(note.id.inner(), expected_domain)?;
verify_domains_match(note.attributed_to.inner(), note.id.inner())?;
verify_is_public(&note.to, &note.cc)?;
let community = Box::pin(note.community(context)).await?;
verify_visibility(&note.to, &note.cc, &community)?;
Box::pin(check_apub_id_valid_with_strictness(
note.id.inner(),

View file

@ -39,6 +39,7 @@ use lemmy_db_schema::{
},
traits::{ApubActor, Crud},
utils::naive_now,
CommunityVisibility,
};
use lemmy_db_views_actor::structs::CommunityFollowerView;
use lemmy_utils::{
@ -126,6 +127,7 @@ impl Object for ApubCommunity {
updated: self.updated,
posting_restricted_to_mods: Some(self.posting_restricted_to_mods),
attributed_to: Some(generate_moderators_url(&self.actor_id)?.into()),
manually_approves_followers: Some(self.visibility == CommunityVisibility::Private),
};
Ok(group)
}
@ -152,7 +154,11 @@ impl Object for ApubCommunity {
let sidebar = markdown_rewrite_remote_links_opt(sidebar, context).await;
let icon = proxy_image_link_opt_apub(group.icon.map(|i| i.url), context).await?;
let banner = proxy_image_link_opt_apub(group.image.map(|i| i.url), context).await?;
let visibility = Some(if group.manually_approves_followers.unwrap_or_default() {
CommunityVisibility::Private
} else {
CommunityVisibility::Public
});
let form = CommunityInsertForm {
published: group.published,
updated: group.updated,
@ -176,6 +182,7 @@ impl Object for ApubCommunity {
moderators_url: group.attributed_to.clone().map(Into::into),
posting_restricted_to_mods: group.posting_restricted_to_mods,
featured_url: group.featured.clone().map(Into::into),
visibility,
..CommunityInsertForm::new(
instance_id,
group.preferred_username.clone(),

View file

@ -1,5 +1,5 @@
use crate::{
activities::{verify_is_public, verify_person_in_community},
activities::{generate_to, verify_person_in_community, verify_visibility},
check_apub_id_valid_with_strictness,
fetcher::markdown_links::{markdown_rewrite_remote_links_opt, to_local_url},
local_site_data_cached,
@ -16,7 +16,6 @@ use crate::{
};
use activitypub_federation::{
config::Data,
kinds::public,
protocol::{values::MediaTypeMarkdownOrHtml, verification::verify_domains_match},
traits::Object,
};
@ -135,7 +134,7 @@ impl Object for ApubPost {
kind: PageType::Page,
id: self.ap_id.clone().into(),
attributed_to: AttributedTo::Lemmy(creator.actor_id.into()),
to: vec![community.actor_id.clone().into(), public()],
to: vec![generate_to(&community)?],
cc: vec![],
name: Some(self.name.clone()),
content: self.body.as_ref().map(|b| markdown_to_html(b)),
@ -172,7 +171,7 @@ impl Object for ApubPost {
check_slurs_opt(&page.name, slur_regex)?;
verify_domains_match(page.creator()?.inner(), page.id.inner())?;
verify_is_public(&page.to, &page.cc)?;
verify_visibility(&page.to, &page.cc, &community)?;
Ok(())
}

View file

@ -1,5 +1,6 @@
pub(crate) mod accept;
pub mod follow;
pub(crate) mod reject;
pub mod undo_follow;
#[cfg(test)]

View file

@ -0,0 +1,24 @@
use crate::{
objects::{community::ApubCommunity, person::ApubPerson},
protocol::activities::following::follow::Follow,
};
use activitypub_federation::{
fetch::object_id::ObjectId,
kinds::activity::RejectType,
protocol::helpers::deserialize_skip_error,
};
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RejectFollow {
pub(crate) actor: ObjectId<ApubCommunity>,
/// Optional, for compatibility with platforms that always expect recipient field
#[serde(deserialize_with = "deserialize_skip_error", default)]
pub(crate) to: Option<[ObjectId<ApubPerson>; 1]>,
pub(crate) object: Follow,
#[serde(rename = "type")]
pub(crate) kind: RejectType,
pub(crate) id: Url,
}

View file

@ -73,6 +73,8 @@ pub struct Group {
pub(crate) featured: Option<CollectionId<ApubCommunityFeatured>>,
#[serde(default)]
pub(crate) language: Vec<LanguageTag>,
/// True if this is a private community
pub(crate) manually_approves_followers: Option<bool>,
pub(crate) published: Option<DateTime<Utc>>,
pub(crate) updated: Option<DateTime<Utc>>,
}

View file

@ -54,7 +54,7 @@ async fn main() -> anyhow::Result<()> {
async fn try_main() -> LemmyResult<()> {
let args = CmdArgs::parse();
let pool = &build_db_pool().await?;
let pool = &build_db_pool()?;
let pool = &mut pool.into();
let conn = &mut get_conn(pool).await?;

View file

@ -51,7 +51,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_crud() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;

View file

@ -37,7 +37,13 @@ mod tests {
aggregates::community_aggregates::CommunityAggregates,
source::{
comment::{Comment, CommentInsertForm},
community::{Community, CommunityFollower, CommunityFollowerForm, CommunityInsertForm},
community::{
Community,
CommunityFollower,
CommunityFollowerForm,
CommunityFollowerState,
CommunityInsertForm,
},
instance::Instance,
person::{Person, PersonInsertForm},
post::{Post, PostInsertForm},
@ -52,7 +58,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_crud() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
@ -84,7 +90,8 @@ mod tests {
let first_person_follow = CommunityFollowerForm {
community_id: inserted_community.id,
person_id: inserted_person.id,
pending: false,
state: Some(CommunityFollowerState::Accepted),
approver_id: None,
};
CommunityFollower::follow(pool, &first_person_follow).await?;
@ -92,7 +99,8 @@ mod tests {
let second_person_follow = CommunityFollowerForm {
community_id: inserted_community.id,
person_id: another_inserted_person.id,
pending: false,
state: Some(CommunityFollowerState::Accepted),
approver_id: None,
};
CommunityFollower::follow(pool, &second_person_follow).await?;
@ -100,7 +108,8 @@ mod tests {
let another_community_follow = CommunityFollowerForm {
community_id: another_inserted_community.id,
person_id: inserted_person.id,
pending: false,
state: Some(CommunityFollowerState::Accepted),
approver_id: None,
};
CommunityFollower::follow(pool, &another_community_follow).await?;

View file

@ -36,7 +36,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_crud() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;

View file

@ -70,7 +70,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_crud() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
@ -182,7 +182,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_soft_delete() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;

View file

@ -65,7 +65,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_crud() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let (inserted_instance, inserted_person, inserted_site, inserted_community) =
@ -136,7 +136,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_soft_delete() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let (inserted_instance, inserted_person, inserted_site, inserted_community) =

View file

@ -71,7 +71,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn receive_activity_duplicate() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let ap_id: DbUrl = Url::parse("http://example.com/activity/531")?.into();
@ -86,7 +86,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn sent_activity_write_read() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let ap_id: DbUrl = Url::parse("http://example.com/activity/412")?.into();
let data = json!({

View file

@ -438,7 +438,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_convert_update_languages() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
// call with empty vec, returns all languages
@ -457,7 +457,7 @@ mod tests {
#[serial]
async fn test_convert_read_languages() -> Result<(), Error> {
use crate::schema::language::dsl::{id, language};
let pool = &build_db_pool_for_tests().await;
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
// call with all languages, returns empty vec
@ -477,7 +477,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_site_languages() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let (site, instance) = create_test_site(pool).await?;
@ -502,7 +502,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_user_languages() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let (site, instance) = create_test_site(pool).await?;
@ -535,7 +535,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_community_languages() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let (site, instance) = create_test_site(pool).await?;
let test_langs = test_langs1(pool).await?;
@ -591,7 +591,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_default_post_language() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let (site, instance) = create_test_site(pool).await?;
let test_langs = test_langs1(pool).await?;

View file

@ -62,7 +62,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_captcha_happy_path() {
let pool = &build_db_pool_for_tests().await;
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let inserted = CaptchaAnswer::insert(
@ -89,7 +89,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_captcha_repeat_answer_fails() {
let pool = &build_db_pool_for_tests().await;
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let inserted = CaptchaAnswer::insert(

View file

@ -227,7 +227,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_crud() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;

View file

@ -15,6 +15,7 @@ use crate::{
Community,
CommunityFollower,
CommunityFollowerForm,
CommunityFollowerState,
CommunityInsertForm,
CommunityModerator,
CommunityModeratorForm,
@ -324,22 +325,8 @@ impl Bannable for CommunityPersonBan {
}
impl CommunityFollower {
pub fn to_subscribed_type(follower: &Option<Self>) -> SubscribedType {
match follower {
Some(f) => {
if f.pending {
SubscribedType::Pending
} else {
SubscribedType::Subscribed
}
}
// If the row doesn't exist, the person isn't a follower.
None => SubscribedType::NotSubscribed,
}
}
pub fn select_subscribed_type() -> dsl::Nullable<community_follower::pending> {
community_follower::pending.nullable()
pub fn select_subscribed_type() -> dsl::Nullable<community_follower::state> {
community_follower::state.nullable()
}
/// Check if a remote instance has any followers on local instance. For this it is enough to check
@ -357,14 +344,34 @@ impl CommunityFollower {
.then_some(())
.ok_or(LemmyErrorType::CommunityHasNoFollowers.into())
}
pub async fn approve(
pool: &mut DbPool<'_>,
community_id: CommunityId,
follower_id: PersonId,
approver_id: PersonId,
) -> LemmyResult<()> {
let conn = &mut get_conn(pool).await?;
diesel::update(community_follower::table.find((follower_id, community_id)))
.set((
community_follower::state.eq(CommunityFollowerState::Accepted),
community_follower::approver_id.eq(approver_id),
))
.get_result::<Self>(conn)
.await?;
Ok(())
}
}
impl Queryable<sql_types::Nullable<sql_types::Bool>, Pg> for SubscribedType {
type Row = Option<bool>;
impl Queryable<sql_types::Nullable<crate::schema::sql_types::CommunityFollowerState>, Pg>
for SubscribedType
{
type Row = Option<CommunityFollowerState>;
fn build(row: Self::Row) -> deserialize::Result<Self> {
Ok(match row {
Some(true) => SubscribedType::Pending,
Some(false) => SubscribedType::Subscribed,
Some(CommunityFollowerState::Pending) => SubscribedType::Pending,
Some(CommunityFollowerState::Accepted) => SubscribedType::Subscribed,
Some(CommunityFollowerState::ApprovalRequired) => SubscribedType::ApprovalRequired,
None => SubscribedType::NotSubscribed,
})
}
@ -393,7 +400,7 @@ impl Followable for CommunityFollower {
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
diesel::update(community_follower::table.find((person_id, community_id)))
.set(community_follower::pending.eq(false))
.set(community_follower::state.eq(CommunityFollowerState::Accepted))
.get_result::<Self>(conn)
.await
}
@ -463,6 +470,7 @@ mod tests {
Community,
CommunityFollower,
CommunityFollowerForm,
CommunityFollowerState,
CommunityInsertForm,
CommunityModerator,
CommunityModeratorForm,
@ -485,7 +493,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_crud() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
@ -535,7 +543,8 @@ mod tests {
let community_follower_form = CommunityFollowerForm {
community_id: inserted_community.id,
person_id: inserted_bobby.id,
pending: false,
state: Some(CommunityFollowerState::Accepted),
approver_id: None,
};
let inserted_community_follower =
@ -544,8 +553,9 @@ mod tests {
let expected_community_follower = CommunityFollower {
community_id: inserted_community.id,
person_id: inserted_bobby.id,
pending: false,
state: CommunityFollowerState::Accepted,
published: inserted_community_follower.published,
approver_id: None,
};
let bobby_moderator_form = CommunityModeratorForm {

View file

@ -61,7 +61,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_allowlist_insert_and_clear() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let domains = vec![
"tld1.xyz".to_string(),

View file

@ -46,7 +46,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_languages() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let all = Language::read_all(pool).await?;

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