Implement instance actor (#1798)
* Implement instance actor * wip: make site bans federate * finish implementation and unit tests for federated bans * start adding api tests * fix api test * remve site from GetCommunityResponse * only federate site bans originating from user's home instance * dont expose site.private_key in api
This commit is contained in:
parent
f1aef63149
commit
dd865c5af5
55 changed files with 1480 additions and 505 deletions
|
@ -27,6 +27,9 @@ import {
|
||||||
reportPost,
|
reportPost,
|
||||||
listPostReports,
|
listPostReports,
|
||||||
randomString,
|
randomString,
|
||||||
|
registerUser,
|
||||||
|
API,
|
||||||
|
getSite
|
||||||
} from './shared';
|
} from './shared';
|
||||||
import { PostView, CommunityView } from 'lemmy-js-client';
|
import { PostView, CommunityView } from 'lemmy-js-client';
|
||||||
|
|
||||||
|
@ -297,29 +300,46 @@ test('A and G subscribe to B (center) A posts, it gets announced to G', async ()
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Enforce site ban for federated user', async () => {
|
test('Enforce site ban for federated user', async () => {
|
||||||
let alphaShortname = `@lemmy_alpha@lemmy-alpha:8541`;
|
// create a test user
|
||||||
let alphaPerson = (await resolvePerson(beta, alphaShortname)).person;
|
let alphaUserJwt = await registerUser(alpha);
|
||||||
|
expect(alphaUserJwt).toBeDefined();
|
||||||
|
let alphaUser: API = {
|
||||||
|
client: alpha.client,
|
||||||
|
auth: alphaUserJwt.jwt,
|
||||||
|
};
|
||||||
|
let alphaUserActorId = (await getSite(alphaUser)).my_user.local_user_view.person.actor_id;
|
||||||
|
expect(alphaUserActorId).toBeDefined();
|
||||||
|
let alphaPerson = (await resolvePerson(alphaUser, alphaUserActorId)).person;
|
||||||
expect(alphaPerson).toBeDefined();
|
expect(alphaPerson).toBeDefined();
|
||||||
|
|
||||||
// ban alpha from beta site
|
// alpha makes post in beta community, it federates to beta instance
|
||||||
let banAlpha = await banPersonFromSite(beta, alphaPerson.person.id, true);
|
let postRes1 = await createPost(alphaUser, betaCommunity.community.id);
|
||||||
|
let searchBeta1 = await searchPostLocal(beta, postRes1.post_view.post);
|
||||||
|
expect(searchBeta1.posts[0]).toBeDefined();
|
||||||
|
|
||||||
|
// ban alpha from its instance
|
||||||
|
let banAlpha = await banPersonFromSite(alpha, alphaPerson.person.id, true, true);
|
||||||
expect(banAlpha.banned).toBe(true);
|
expect(banAlpha.banned).toBe(true);
|
||||||
|
|
||||||
// Alpha makes post on beta
|
// alpha ban should be federated to beta
|
||||||
let postRes = await createPost(alpha, betaCommunity.community.id);
|
let alphaUserOnBeta1 = await resolvePerson(beta, alphaUserActorId);
|
||||||
expect(postRes.post_view.post).toBeDefined();
|
expect(alphaUserOnBeta1.person.person.banned).toBe(true);
|
||||||
expect(postRes.post_view.community.local).toBe(false);
|
|
||||||
expect(postRes.post_view.creator.local).toBe(true);
|
|
||||||
expect(postRes.post_view.counts.score).toBe(1);
|
|
||||||
|
|
||||||
// Make sure that post doesn't make it to beta
|
// existing alpha post should be removed on beta
|
||||||
let searchBeta = await searchPostLocal(beta, postRes.post_view.post);
|
let searchBeta2 = await searchPostLocal(beta, postRes1.post_view.post);
|
||||||
let betaPost = searchBeta.posts[0];
|
expect(searchBeta2.posts[0]).toBeUndefined();
|
||||||
expect(betaPost).toBeUndefined();
|
|
||||||
|
|
||||||
// Unban alpha
|
// Unban alpha
|
||||||
let unBanAlpha = await banPersonFromSite(beta, alphaPerson.person.id, false);
|
let unBanAlpha = await banPersonFromSite(alpha, alphaPerson.person.id, false, false);
|
||||||
expect(unBanAlpha.banned).toBe(false);
|
expect(unBanAlpha.banned).toBe(false);
|
||||||
|
|
||||||
|
// alpha makes new post in beta community, it federates
|
||||||
|
let postRes2 = await createPost(alphaUser, betaCommunity.community.id);
|
||||||
|
let searchBeta3 = await searchPostLocal(beta, postRes2.post_view.post);
|
||||||
|
expect(searchBeta3.posts[0]).toBeDefined();
|
||||||
|
|
||||||
|
let alphaUserOnBeta2 = await resolvePerson(beta, alphaUserActorId)
|
||||||
|
expect(alphaUserOnBeta2.person.person.banned).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Enforce community ban for federated user', async () => {
|
test('Enforce community ban for federated user', async () => {
|
||||||
|
@ -327,33 +347,41 @@ test('Enforce community ban for federated user', async () => {
|
||||||
let alphaPerson = (await resolvePerson(beta, alphaShortname)).person;
|
let alphaPerson = (await resolvePerson(beta, alphaShortname)).person;
|
||||||
expect(alphaPerson).toBeDefined();
|
expect(alphaPerson).toBeDefined();
|
||||||
|
|
||||||
// ban alpha from beta site
|
// make a post in beta, it goes through
|
||||||
await banPersonFromCommunity(beta, alphaPerson.person.id, 2, false);
|
let postRes1 = await createPost(alpha, betaCommunity.community.id);
|
||||||
let banAlpha = await banPersonFromCommunity(beta, alphaPerson.person.id, 2, true);
|
let searchBeta1 = await searchPostLocal(beta, postRes1.post_view.post);
|
||||||
|
expect(searchBeta1.posts[0]).toBeDefined();
|
||||||
|
|
||||||
|
// ban alpha from beta community
|
||||||
|
let banAlpha = await banPersonFromCommunity(beta, alphaPerson.person.id, 2, true, true);
|
||||||
expect(banAlpha.banned).toBe(true);
|
expect(banAlpha.banned).toBe(true);
|
||||||
|
|
||||||
|
// ensure that the post by alpha got removed
|
||||||
|
let searchAlpha1 = await searchPostLocal(alpha, postRes1.post_view.post);
|
||||||
|
expect(searchAlpha1.posts[0]).toBeUndefined();
|
||||||
|
|
||||||
// Alpha tries to make post on beta, but it fails because of ban
|
// Alpha tries to make post on beta, but it fails because of ban
|
||||||
let postRes = await createPost(alpha, betaCommunity.community.id);
|
let postRes2 = await createPost(alpha, betaCommunity.community.id);
|
||||||
expect(postRes.post_view).toBeUndefined();
|
expect(postRes2.post_view).toBeUndefined();
|
||||||
|
|
||||||
// Unban alpha
|
// Unban alpha
|
||||||
let unBanAlpha = await banPersonFromCommunity(
|
let unBanAlpha = await banPersonFromCommunity(
|
||||||
beta,
|
beta,
|
||||||
alphaPerson.person.id,
|
alphaPerson.person.id,
|
||||||
2,
|
2,
|
||||||
|
false,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
expect(unBanAlpha.banned).toBe(false);
|
expect(unBanAlpha.banned).toBe(false);
|
||||||
let postRes2 = await createPost(alpha, betaCommunity.community.id);
|
let postRes3 = await createPost(alpha, betaCommunity.community.id);
|
||||||
expect(postRes2.post_view.post).toBeDefined();
|
expect(postRes3.post_view.post).toBeDefined();
|
||||||
expect(postRes2.post_view.community.local).toBe(false);
|
expect(postRes3.post_view.community.local).toBe(false);
|
||||||
expect(postRes2.post_view.creator.local).toBe(true);
|
expect(postRes3.post_view.creator.local).toBe(true);
|
||||||
expect(postRes2.post_view.counts.score).toBe(1);
|
expect(postRes3.post_view.counts.score).toBe(1);
|
||||||
|
|
||||||
// Make sure that post makes it to beta community
|
// Make sure that post makes it to beta community
|
||||||
let searchBeta = await searchPostLocal(beta, postRes2.post_view.post);
|
let searchBeta2 = await searchPostLocal(beta, postRes3.post_view.post);
|
||||||
let betaPost = searchBeta.posts[0];
|
expect(searchBeta2.posts[0]).toBeDefined();
|
||||||
expect(betaPost).toBeDefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Report a post', async () => {
|
test('Report a post', async () => {
|
||||||
|
|
|
@ -66,23 +66,23 @@ export interface API {
|
||||||
}
|
}
|
||||||
|
|
||||||
export let alpha: API = {
|
export let alpha: API = {
|
||||||
client: new LemmyHttp('http://localhost:8541'),
|
client: new LemmyHttp('http://127.0.0.1:8541'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export let beta: API = {
|
export let beta: API = {
|
||||||
client: new LemmyHttp('http://localhost:8551'),
|
client: new LemmyHttp('http://127.0.0.1:8551'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export let gamma: API = {
|
export let gamma: API = {
|
||||||
client: new LemmyHttp('http://localhost:8561'),
|
client: new LemmyHttp('http://127.0.0.1:8561'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export let delta: API = {
|
export let delta: API = {
|
||||||
client: new LemmyHttp('http://localhost:8571'),
|
client: new LemmyHttp('http://127.0.0.1:8571'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export let epsilon: API = {
|
export let epsilon: API = {
|
||||||
client: new LemmyHttp('http://localhost:8581'),
|
client: new LemmyHttp('http://127.0.0.1:8581'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const password = 'lemmylemmy'
|
const password = 'lemmylemmy'
|
||||||
|
@ -289,13 +289,14 @@ export async function resolvePerson(
|
||||||
export async function banPersonFromSite(
|
export async function banPersonFromSite(
|
||||||
api: API,
|
api: API,
|
||||||
person_id: number,
|
person_id: number,
|
||||||
ban: boolean
|
ban: boolean,
|
||||||
|
remove_data: boolean,
|
||||||
): Promise<BanPersonResponse> {
|
): Promise<BanPersonResponse> {
|
||||||
// Make sure lemmy-beta/c/main is cached on lemmy_alpha
|
// Make sure lemmy-beta/c/main is cached on lemmy_alpha
|
||||||
let form: BanPerson = {
|
let form: BanPerson = {
|
||||||
person_id,
|
person_id,
|
||||||
ban,
|
ban,
|
||||||
remove_data: false,
|
remove_data,
|
||||||
auth: api.auth,
|
auth: api.auth,
|
||||||
};
|
};
|
||||||
return api.client.banPerson(form);
|
return api.client.banPerson(form);
|
||||||
|
@ -305,13 +306,13 @@ export async function banPersonFromCommunity(
|
||||||
api: API,
|
api: API,
|
||||||
person_id: number,
|
person_id: number,
|
||||||
community_id: number,
|
community_id: number,
|
||||||
|
remove_data: boolean,
|
||||||
ban: boolean
|
ban: boolean
|
||||||
): Promise<BanFromCommunityResponse> {
|
): Promise<BanFromCommunityResponse> {
|
||||||
// Make sure lemmy-beta/c/main is cached on lemmy_alpha
|
|
||||||
let form: BanFromCommunity = {
|
let form: BanFromCommunity = {
|
||||||
person_id,
|
person_id,
|
||||||
community_id,
|
community_id,
|
||||||
remove_data: false,
|
remove_data,
|
||||||
ban,
|
ban,
|
||||||
auth: api.auth,
|
auth: api.auth,
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,22 +8,19 @@ use lemmy_api_common::{
|
||||||
community::*,
|
community::*,
|
||||||
get_local_user_view_from_jwt,
|
get_local_user_view_from_jwt,
|
||||||
is_mod_or_admin,
|
is_mod_or_admin,
|
||||||
|
remove_user_data_in_community,
|
||||||
};
|
};
|
||||||
use lemmy_apub::{
|
use lemmy_apub::{
|
||||||
|
activities::block::SiteOrCommunity,
|
||||||
objects::{community::ApubCommunity, person::ApubPerson},
|
objects::{community::ApubCommunity, person::ApubPerson},
|
||||||
protocol::activities::{
|
protocol::activities::{
|
||||||
community::{
|
block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
|
||||||
add_mod::AddMod,
|
community::{add_mod::AddMod, remove_mod::RemoveMod},
|
||||||
block_user::BlockUserFromCommunity,
|
|
||||||
remove_mod::RemoveMod,
|
|
||||||
undo_block_user::UndoBlockUserFromCommunity,
|
|
||||||
},
|
|
||||||
following::{follow::FollowCommunity as FollowCommunityApub, undo_follow::UndoFollowCommunity},
|
following::{follow::FollowCommunity as FollowCommunityApub, undo_follow::UndoFollowCommunity},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
source::{
|
source::{
|
||||||
comment::Comment,
|
|
||||||
community::{
|
community::{
|
||||||
Community,
|
Community,
|
||||||
CommunityFollower,
|
CommunityFollower,
|
||||||
|
@ -43,11 +40,9 @@ use lemmy_db_schema::{
|
||||||
ModTransferCommunityForm,
|
ModTransferCommunityForm,
|
||||||
},
|
},
|
||||||
person::Person,
|
person::Person,
|
||||||
post::Post,
|
|
||||||
},
|
},
|
||||||
traits::{Bannable, Blockable, Crud, Followable, Joinable},
|
traits::{Bannable, Blockable, Crud, Followable, Joinable},
|
||||||
};
|
};
|
||||||
use lemmy_db_views::comment_view::CommentQueryBuilder;
|
|
||||||
use lemmy_db_views_actor::{
|
use lemmy_db_views_actor::{
|
||||||
community_moderator_view::CommunityModeratorView,
|
community_moderator_view::CommunityModeratorView,
|
||||||
community_view::CommunityView,
|
community_view::CommunityView,
|
||||||
|
@ -213,6 +208,7 @@ impl Perform for BanFromCommunity {
|
||||||
|
|
||||||
let community_id = data.community_id;
|
let community_id = data.community_id;
|
||||||
let banned_person_id = data.person_id;
|
let banned_person_id = data.person_id;
|
||||||
|
let remove_data = data.remove_data.unwrap_or(false);
|
||||||
let expires = data.expires.map(naive_from_unix);
|
let expires = data.expires.map(naive_from_unix);
|
||||||
|
|
||||||
// Verify that only mods or admins can ban
|
// Verify that only mods or admins can ban
|
||||||
|
@ -254,10 +250,12 @@ impl Perform for BanFromCommunity {
|
||||||
.await?
|
.await?
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
BlockUserFromCommunity::send(
|
BlockUser::send(
|
||||||
&community,
|
&SiteOrCommunity::Community(community),
|
||||||
&banned_person,
|
&banned_person,
|
||||||
&local_user_view.person.clone().into(),
|
&local_user_view.person.clone().into(),
|
||||||
|
remove_data,
|
||||||
|
data.reason.clone(),
|
||||||
expires,
|
expires,
|
||||||
context,
|
context,
|
||||||
)
|
)
|
||||||
|
@ -268,41 +266,19 @@ impl Perform for BanFromCommunity {
|
||||||
.await?
|
.await?
|
||||||
.map_err(LemmyError::from)
|
.map_err(LemmyError::from)
|
||||||
.map_err(|e| e.with_message("community_user_already_banned"))?;
|
.map_err(|e| e.with_message("community_user_already_banned"))?;
|
||||||
UndoBlockUserFromCommunity::send(
|
UndoBlockUser::send(
|
||||||
&community,
|
&SiteOrCommunity::Community(community),
|
||||||
&banned_person,
|
&banned_person,
|
||||||
&local_user_view.person.clone().into(),
|
&local_user_view.person.clone().into(),
|
||||||
|
data.reason.clone(),
|
||||||
context,
|
context,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove/Restore their data if that's desired
|
// Remove/Restore their data if that's desired
|
||||||
if data.remove_data.unwrap_or(false) {
|
if remove_data {
|
||||||
// Posts
|
remove_user_data_in_community(community_id, banned_person_id, context.pool()).await?;
|
||||||
blocking(context.pool(), move |conn: &'_ _| {
|
|
||||||
Post::update_removed_for_creator(conn, banned_person_id, Some(community_id), true)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// Comments
|
|
||||||
// TODO Diesel doesn't allow updates with joins, so this has to be a loop
|
|
||||||
let comments = blocking(context.pool(), move |conn| {
|
|
||||||
CommentQueryBuilder::create(conn)
|
|
||||||
.creator_id(banned_person_id)
|
|
||||||
.community_id(community_id)
|
|
||||||
.limit(std::i64::MAX)
|
|
||||||
.list()
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
for comment_view in &comments {
|
|
||||||
let comment_id = comment_view.comment.id;
|
|
||||||
blocking(context.pool(), move |conn: &'_ _| {
|
|
||||||
Comment::update_removed(conn, comment_id, true)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
|
|
|
@ -10,10 +10,15 @@ use lemmy_api_common::{
|
||||||
is_admin,
|
is_admin,
|
||||||
password_length_check,
|
password_length_check,
|
||||||
person::*,
|
person::*,
|
||||||
|
remove_user_data,
|
||||||
send_email_verification_success,
|
send_email_verification_success,
|
||||||
send_password_reset_email,
|
send_password_reset_email,
|
||||||
send_verification_email,
|
send_verification_email,
|
||||||
};
|
};
|
||||||
|
use lemmy_apub::{
|
||||||
|
activities::block::SiteOrCommunity,
|
||||||
|
protocol::activities::block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
|
||||||
|
};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
diesel_option_overwrite,
|
diesel_option_overwrite,
|
||||||
diesel_option_overwrite_to_url,
|
diesel_option_overwrite_to_url,
|
||||||
|
@ -21,7 +26,6 @@ use lemmy_db_schema::{
|
||||||
naive_now,
|
naive_now,
|
||||||
source::{
|
source::{
|
||||||
comment::Comment,
|
comment::Comment,
|
||||||
community::Community,
|
|
||||||
email_verification::EmailVerification,
|
email_verification::EmailVerification,
|
||||||
local_user::{LocalUser, LocalUserForm},
|
local_user::{LocalUser, LocalUserForm},
|
||||||
moderator::*,
|
moderator::*,
|
||||||
|
@ -29,7 +33,6 @@ use lemmy_db_schema::{
|
||||||
person::*,
|
person::*,
|
||||||
person_block::{PersonBlock, PersonBlockForm},
|
person_block::{PersonBlock, PersonBlockForm},
|
||||||
person_mention::*,
|
person_mention::*,
|
||||||
post::Post,
|
|
||||||
private_message::PrivateMessage,
|
private_message::PrivateMessage,
|
||||||
site::*,
|
site::*,
|
||||||
},
|
},
|
||||||
|
@ -44,7 +47,6 @@ use lemmy_db_views::{
|
||||||
private_message_view::PrivateMessageView,
|
private_message_view::PrivateMessageView,
|
||||||
};
|
};
|
||||||
use lemmy_db_views_actor::{
|
use lemmy_db_views_actor::{
|
||||||
community_moderator_view::CommunityModeratorView,
|
|
||||||
person_mention_view::{PersonMentionQueryBuilder, PersonMentionView},
|
person_mention_view::{PersonMentionQueryBuilder, PersonMentionView},
|
||||||
person_view::PersonViewSafe,
|
person_view::PersonViewSafe,
|
||||||
};
|
};
|
||||||
|
@ -91,7 +93,7 @@ impl Perform for Login {
|
||||||
return Err(LemmyError::from_message("password_incorrect"));
|
return Err(LemmyError::from_message("password_incorrect"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let site = blocking(context.pool(), Site::read_simple).await??;
|
let site = blocking(context.pool(), Site::read_local_site).await??;
|
||||||
if site.require_email_verification && !local_user_view.local_user.email_verified {
|
if site.require_email_verification && !local_user_view.local_user.email_verified {
|
||||||
return Err(LemmyError::from_message("email_not_verified"));
|
return Err(LemmyError::from_message("email_not_verified"));
|
||||||
}
|
}
|
||||||
|
@ -200,7 +202,7 @@ impl Perform for SaveUserSettings {
|
||||||
|
|
||||||
// When the site requires email, make sure email is not Some(None). IE, an overwrite to a None value
|
// When the site requires email, make sure email is not Some(None). IE, an overwrite to a None value
|
||||||
if let Some(email) = &email {
|
if let Some(email) = &email {
|
||||||
let site_fut = blocking(context.pool(), Site::read_simple);
|
let site_fut = blocking(context.pool(), Site::read_local_site);
|
||||||
if email.is_none() && site_fut.await??.require_email_verification {
|
if email.is_none() && site_fut.await??.require_email_verification {
|
||||||
return Err(LemmyError::from_message("email_required"));
|
return Err(LemmyError::from_message("email_required"));
|
||||||
}
|
}
|
||||||
|
@ -443,45 +445,15 @@ impl Perform for BanPerson {
|
||||||
let expires = data.expires.map(naive_from_unix);
|
let expires = data.expires.map(naive_from_unix);
|
||||||
|
|
||||||
let ban_person = move |conn: &'_ _| Person::ban_person(conn, banned_person_id, ban, expires);
|
let ban_person = move |conn: &'_ _| Person::ban_person(conn, banned_person_id, ban, expires);
|
||||||
blocking(context.pool(), ban_person)
|
let person = blocking(context.pool(), ban_person)
|
||||||
.await?
|
.await?
|
||||||
.map_err(LemmyError::from)
|
.map_err(LemmyError::from)
|
||||||
.map_err(|e| e.with_message("couldnt_update_user"))?;
|
.map_err(|e| e.with_message("couldnt_update_user"))?;
|
||||||
|
|
||||||
// Remove their data if that's desired
|
// Remove their data if that's desired
|
||||||
if data.remove_data.unwrap_or(false) {
|
let remove_data = data.remove_data.unwrap_or(false);
|
||||||
// Posts
|
if remove_data {
|
||||||
blocking(context.pool(), move |conn: &'_ _| {
|
remove_user_data(person.id, context.pool()).await?;
|
||||||
Post::update_removed_for_creator(conn, banned_person_id, None, true)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// Communities
|
|
||||||
// Remove all communities where they're the top mod
|
|
||||||
// for now, remove the communities manually
|
|
||||||
let first_mod_communities = blocking(context.pool(), move |conn: &'_ _| {
|
|
||||||
CommunityModeratorView::get_community_first_mods(conn)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// Filter to only this banned users top communities
|
|
||||||
let banned_user_first_communities: Vec<CommunityModeratorView> = first_mod_communities
|
|
||||||
.into_iter()
|
|
||||||
.filter(|fmc| fmc.moderator.id == banned_person_id)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for first_mod_community in banned_user_first_communities {
|
|
||||||
blocking(context.pool(), move |conn: &'_ _| {
|
|
||||||
Community::update_removed(conn, first_mod_community.community.id, true)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comments
|
|
||||||
blocking(context.pool(), move |conn: &'_ _| {
|
|
||||||
Comment::update_removed_for_creator(conn, banned_person_id, true)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
|
@ -501,6 +473,36 @@ impl Perform for BanPerson {
|
||||||
})
|
})
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
|
let site = SiteOrCommunity::Site(
|
||||||
|
blocking(context.pool(), Site::read_local_site)
|
||||||
|
.await??
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
// if the action affects a local user, federate to other instances
|
||||||
|
if person.local {
|
||||||
|
if ban {
|
||||||
|
BlockUser::send(
|
||||||
|
&site,
|
||||||
|
&person.into(),
|
||||||
|
&local_user_view.person.into(),
|
||||||
|
remove_data,
|
||||||
|
data.reason.clone(),
|
||||||
|
expires,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
UndoBlockUser::send(
|
||||||
|
&site,
|
||||||
|
&person.into(),
|
||||||
|
&local_user_view.person.into(),
|
||||||
|
data.reason.clone(),
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let res = BanPersonResponse {
|
let res = BanPersonResponse {
|
||||||
person_view,
|
person_view,
|
||||||
banned: data.ban,
|
banned: data.ban,
|
||||||
|
|
|
@ -583,7 +583,7 @@ impl Perform for ListRegistrationApplications {
|
||||||
is_admin(&local_user_view)?;
|
is_admin(&local_user_view)?;
|
||||||
|
|
||||||
let unread_only = data.unread_only;
|
let unread_only = data.unread_only;
|
||||||
let verified_email_only = blocking(context.pool(), Site::read_simple)
|
let verified_email_only = blocking(context.pool(), Site::read_local_site)
|
||||||
.await??
|
.await??
|
||||||
.require_email_verification;
|
.require_email_verification;
|
||||||
|
|
||||||
|
@ -689,7 +689,7 @@ impl Perform for GetUnreadRegistrationApplicationCount {
|
||||||
// Only let admins do this
|
// Only let admins do this
|
||||||
is_admin(&local_user_view)?;
|
is_admin(&local_user_view)?;
|
||||||
|
|
||||||
let verified_email_only = blocking(context.pool(), Site::read_simple)
|
let verified_email_only = blocking(context.pool(), Site::read_local_site)
|
||||||
.await??
|
.await??
|
||||||
.require_email_verification;
|
.require_email_verification;
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ use itertools::Itertools;
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
newtypes::{CommunityId, LocalUserId, PersonId, PostId},
|
newtypes::{CommunityId, LocalUserId, PersonId, PostId},
|
||||||
source::{
|
source::{
|
||||||
|
comment::Comment,
|
||||||
community::Community,
|
community::Community,
|
||||||
email_verification::{EmailVerification, EmailVerificationForm},
|
email_verification::{EmailVerification, EmailVerificationForm},
|
||||||
password_reset_request::PasswordResetRequest,
|
password_reset_request::PasswordResetRequest,
|
||||||
|
@ -22,8 +23,12 @@ use lemmy_db_schema::{
|
||||||
traits::{ApubActor, Crud, Readable},
|
traits::{ApubActor, Crud, Readable},
|
||||||
DbPool,
|
DbPool,
|
||||||
};
|
};
|
||||||
use lemmy_db_views::local_user_view::{LocalUserSettingsView, LocalUserView};
|
use lemmy_db_views::{
|
||||||
|
comment_view::CommentQueryBuilder,
|
||||||
|
local_user_view::{LocalUserSettingsView, LocalUserView},
|
||||||
|
};
|
||||||
use lemmy_db_views_actor::{
|
use lemmy_db_views_actor::{
|
||||||
|
community_moderator_view::CommunityModeratorView,
|
||||||
community_person_ban_view::CommunityPersonBanView,
|
community_person_ban_view::CommunityPersonBanView,
|
||||||
community_view::CommunityView,
|
community_view::CommunityView,
|
||||||
};
|
};
|
||||||
|
@ -35,7 +40,6 @@ use lemmy_utils::{
|
||||||
LemmyError,
|
LemmyError,
|
||||||
Sensitive,
|
Sensitive,
|
||||||
};
|
};
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
|
pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
|
||||||
where
|
where
|
||||||
|
@ -267,7 +271,7 @@ pub async fn check_person_block(
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), LemmyError> {
|
pub async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), LemmyError> {
|
||||||
if score == -1 {
|
if score == -1 {
|
||||||
let site = blocking(pool, Site::read_simple).await??;
|
let site = blocking(pool, Site::read_local_site).await??;
|
||||||
if !site.enable_downvotes {
|
if !site.enable_downvotes {
|
||||||
return Err(LemmyError::from_message("downvotes_disabled"));
|
return Err(LemmyError::from_message("downvotes_disabled"));
|
||||||
}
|
}
|
||||||
|
@ -281,7 +285,7 @@ pub async fn check_private_instance(
|
||||||
pool: &DbPool,
|
pool: &DbPool,
|
||||||
) -> Result<(), LemmyError> {
|
) -> Result<(), LemmyError> {
|
||||||
if local_user_view.is_none() {
|
if local_user_view.is_none() {
|
||||||
let site = blocking(pool, Site::read_simple).await?;
|
let site = blocking(pool, Site::read_local_site).await?;
|
||||||
|
|
||||||
// The site might not be set up yet
|
// The site might not be set up yet
|
||||||
if let Ok(site) = site {
|
if let Ok(site) = site {
|
||||||
|
@ -311,7 +315,7 @@ pub async fn build_federated_instances(
|
||||||
|
|
||||||
let mut linked = distinct_communities
|
let mut linked = distinct_communities
|
||||||
.iter()
|
.iter()
|
||||||
.map(|actor_id| Ok(Url::parse(actor_id)?.host_str().unwrap_or("").to_string()))
|
.map(|actor_id| Ok(actor_id.host_str().unwrap_or("").to_string()))
|
||||||
.collect::<Result<Vec<String>, LemmyError>>()?;
|
.collect::<Result<Vec<String>, LemmyError>>()?;
|
||||||
|
|
||||||
if let Some(allowed) = allowed.as_ref() {
|
if let Some(allowed) = allowed.as_ref() {
|
||||||
|
@ -511,7 +515,7 @@ pub async fn check_private_instance_and_federation_enabled(
|
||||||
pool: &DbPool,
|
pool: &DbPool,
|
||||||
settings: &Settings,
|
settings: &Settings,
|
||||||
) -> Result<(), LemmyError> {
|
) -> Result<(), LemmyError> {
|
||||||
let site_opt = blocking(pool, Site::read_simple).await?;
|
let site_opt = blocking(pool, Site::read_local_site).await?;
|
||||||
|
|
||||||
if let Ok(site) = site_opt {
|
if let Ok(site) = site_opt {
|
||||||
if site.private_instance && settings.federation.enabled {
|
if site.private_instance && settings.federation.enabled {
|
||||||
|
@ -555,3 +559,73 @@ where
|
||||||
Ok(blocking(pool, move |conn| Actor::read_from_name(conn, &identifier)).await??)
|
Ok(blocking(pool, move |conn| Actor::read_from_name(conn, &identifier)).await??)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn remove_user_data(banned_person_id: PersonId, pool: &DbPool) -> Result<(), LemmyError> {
|
||||||
|
// Posts
|
||||||
|
blocking(pool, move |conn: &'_ _| {
|
||||||
|
Post::update_removed_for_creator(conn, banned_person_id, None, true)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
// Communities
|
||||||
|
// Remove all communities where they're the top mod
|
||||||
|
// for now, remove the communities manually
|
||||||
|
let first_mod_communities = blocking(pool, move |conn: &'_ _| {
|
||||||
|
CommunityModeratorView::get_community_first_mods(conn)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
// Filter to only this banned users top communities
|
||||||
|
let banned_user_first_communities: Vec<CommunityModeratorView> = first_mod_communities
|
||||||
|
.into_iter()
|
||||||
|
.filter(|fmc| fmc.moderator.id == banned_person_id)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for first_mod_community in banned_user_first_communities {
|
||||||
|
blocking(pool, move |conn: &'_ _| {
|
||||||
|
Community::update_removed(conn, first_mod_community.community.id, true)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comments
|
||||||
|
blocking(pool, move |conn: &'_ _| {
|
||||||
|
Comment::update_removed_for_creator(conn, banned_person_id, true)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_user_data_in_community(
|
||||||
|
community_id: CommunityId,
|
||||||
|
banned_person_id: PersonId,
|
||||||
|
pool: &DbPool,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
// Posts
|
||||||
|
blocking(pool, move |conn| {
|
||||||
|
Post::update_removed_for_creator(conn, banned_person_id, Some(community_id), true)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
// Comments
|
||||||
|
// TODO Diesel doesn't allow updates with joins, so this has to be a loop
|
||||||
|
let comments = blocking(pool, move |conn| {
|
||||||
|
CommentQueryBuilder::create(conn)
|
||||||
|
.creator_id(banned_person_id)
|
||||||
|
.community_id(community_id)
|
||||||
|
.limit(std::i64::MAX)
|
||||||
|
.list()
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
for comment_view in &comments {
|
||||||
|
let comment_id = comment_view.comment.id;
|
||||||
|
blocking(pool, move |conn| {
|
||||||
|
Comment::update_removed(conn, comment_id, true)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ impl PerformCrud for CreateCommunity {
|
||||||
let local_user_view =
|
let local_user_view =
|
||||||
get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
|
get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
|
||||||
|
|
||||||
let site = blocking(context.pool(), move |conn| Site::read(conn, 0)).await??;
|
let site = blocking(context.pool(), Site::read_local_site).await??;
|
||||||
if site.community_creation_admin_only && is_admin(&local_user_view).is_err() {
|
if site.community_creation_admin_only && is_admin(&local_user_view).is_err() {
|
||||||
return Err(LemmyError::from_message(
|
return Err(LemmyError::from_message(
|
||||||
"only_admins_can_create_communities",
|
"only_admins_can_create_communities",
|
||||||
|
|
|
@ -7,19 +7,25 @@ use lemmy_api_common::{
|
||||||
site::*,
|
site::*,
|
||||||
site_description_length_check,
|
site_description_length_check,
|
||||||
};
|
};
|
||||||
|
use lemmy_apub::generate_site_inbox_url;
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
diesel_option_overwrite,
|
diesel_option_overwrite,
|
||||||
diesel_option_overwrite_to_url,
|
diesel_option_overwrite_to_url,
|
||||||
|
naive_now,
|
||||||
|
newtypes::DbUrl,
|
||||||
source::site::{Site, SiteForm},
|
source::site::{Site, SiteForm},
|
||||||
traits::Crud,
|
traits::Crud,
|
||||||
};
|
};
|
||||||
use lemmy_db_views::site_view::SiteView;
|
use lemmy_db_views::site_view::SiteView;
|
||||||
use lemmy_utils::{
|
use lemmy_utils::{
|
||||||
|
apub::generate_actor_keypair,
|
||||||
|
settings::structs::Settings,
|
||||||
utils::{check_slurs, check_slurs_opt},
|
utils::{check_slurs, check_slurs_opt},
|
||||||
ConnectionId,
|
ConnectionId,
|
||||||
LemmyError,
|
LemmyError,
|
||||||
};
|
};
|
||||||
use lemmy_websocket::LemmyContext;
|
use lemmy_websocket::LemmyContext;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl PerformCrud for CreateSite {
|
impl PerformCrud for CreateSite {
|
||||||
|
@ -33,7 +39,7 @@ impl PerformCrud for CreateSite {
|
||||||
) -> Result<SiteResponse, LemmyError> {
|
) -> Result<SiteResponse, LemmyError> {
|
||||||
let data: &CreateSite = self;
|
let data: &CreateSite = self;
|
||||||
|
|
||||||
let read_site = Site::read_simple;
|
let read_site = Site::read_local_site;
|
||||||
if blocking(context.pool(), read_site).await?.is_ok() {
|
if blocking(context.pool(), read_site).await?.is_ok() {
|
||||||
return Err(LemmyError::from_message("site_already_exists"));
|
return Err(LemmyError::from_message("site_already_exists"));
|
||||||
};
|
};
|
||||||
|
@ -56,6 +62,9 @@ impl PerformCrud for CreateSite {
|
||||||
site_description_length_check(desc)?;
|
site_description_length_check(desc)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let actor_id: DbUrl = Url::parse(&Settings::get().get_protocol_and_hostname())?.into();
|
||||||
|
let inbox_url = Some(generate_site_inbox_url(&actor_id)?);
|
||||||
|
let keypair = generate_actor_keypair()?;
|
||||||
let site_form = SiteForm {
|
let site_form = SiteForm {
|
||||||
name: data.name.to_owned(),
|
name: data.name.to_owned(),
|
||||||
sidebar,
|
sidebar,
|
||||||
|
@ -66,6 +75,11 @@ impl PerformCrud for CreateSite {
|
||||||
open_registration: data.open_registration,
|
open_registration: data.open_registration,
|
||||||
enable_nsfw: data.enable_nsfw,
|
enable_nsfw: data.enable_nsfw,
|
||||||
community_creation_admin_only: data.community_creation_admin_only,
|
community_creation_admin_only: data.community_creation_admin_only,
|
||||||
|
actor_id: Some(actor_id),
|
||||||
|
last_refreshed_at: Some(naive_now()),
|
||||||
|
inbox_url,
|
||||||
|
private_key: Some(Some(keypair.private_key)),
|
||||||
|
public_key: Some(keypair.public_key),
|
||||||
..SiteForm::default()
|
..SiteForm::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ use lemmy_db_schema::{
|
||||||
use lemmy_db_views::site_view::SiteView;
|
use lemmy_db_views::site_view::SiteView;
|
||||||
use lemmy_utils::{utils::check_slurs_opt, ConnectionId, LemmyError};
|
use lemmy_utils::{utils::check_slurs_opt, ConnectionId, LemmyError};
|
||||||
use lemmy_websocket::{messages::SendAllMessage, LemmyContext, UserOperationCrud};
|
use lemmy_websocket::{messages::SendAllMessage, LemmyContext, UserOperationCrud};
|
||||||
|
use std::default::Default;
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl PerformCrud for EditSite {
|
impl PerformCrud for EditSite {
|
||||||
|
@ -41,7 +42,7 @@ impl PerformCrud for EditSite {
|
||||||
// Make sure user is an admin
|
// Make sure user is an admin
|
||||||
is_admin(&local_user_view)?;
|
is_admin(&local_user_view)?;
|
||||||
|
|
||||||
let found_site = blocking(context.pool(), Site::read_simple).await??;
|
let found_site = blocking(context.pool(), Site::read_local_site).await??;
|
||||||
|
|
||||||
let sidebar = diesel_option_overwrite(&data.sidebar);
|
let sidebar = diesel_option_overwrite(&data.sidebar);
|
||||||
let description = diesel_option_overwrite(&data.description);
|
let description = diesel_option_overwrite(&data.description);
|
||||||
|
@ -68,6 +69,7 @@ impl PerformCrud for EditSite {
|
||||||
require_application: data.require_application,
|
require_application: data.require_application,
|
||||||
application_question,
|
application_question,
|
||||||
private_instance: data.private_instance,
|
private_instance: data.private_instance,
|
||||||
|
..SiteForm::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let update_site = blocking(context.pool(), move |conn| {
|
let update_site = blocking(context.pool(), move |conn| {
|
||||||
|
|
|
@ -58,7 +58,7 @@ impl PerformCrud for Register {
|
||||||
let (mut email_verification, mut require_application) = (false, false);
|
let (mut email_verification, mut require_application) = (false, false);
|
||||||
|
|
||||||
// Make sure site has open registration
|
// Make sure site has open registration
|
||||||
if let Ok(site) = blocking(context.pool(), Site::read_simple).await? {
|
if let Ok(site) = blocking(context.pool(), Site::read_local_site).await? {
|
||||||
if !site.open_registration {
|
if !site.open_registration {
|
||||||
return Err(LemmyError::from_message("registration_closed"));
|
return Err(LemmyError::from_message("registration_closed"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
],
|
],
|
||||||
"target": "http://enterprise.lemmy.ml/c/main",
|
"target": "http://enterprise.lemmy.ml/c/main",
|
||||||
"type": "Block",
|
"type": "Block",
|
||||||
|
"remove_data": "true",
|
||||||
|
"summary": "spam post",
|
||||||
"expires": "2021-11-01T12:23:50.151874+00:00",
|
"expires": "2021-11-01T12:23:50.151874+00:00",
|
||||||
"id": "http://enterprise.lemmy.ml/activities/block/5d42fffb-0903-4625-86d4-0b39bb344fc2"
|
"id": "http://enterprise.lemmy.ml/activities/block/5d42fffb-0903-4625-86d4-0b39bb344fc2"
|
||||||
}
|
}
|
|
@ -14,6 +14,9 @@
|
||||||
],
|
],
|
||||||
"target": "http://enterprise.lemmy.ml/c/main",
|
"target": "http://enterprise.lemmy.ml/c/main",
|
||||||
"type": "Block",
|
"type": "Block",
|
||||||
|
"remove_data": "true",
|
||||||
|
"summary": "spam post",
|
||||||
|
"expires": "2021-11-01T12:23:50.151874+00:00",
|
||||||
"id": "http://enterprise.lemmy.ml/activities/block/726f43ab-bd0e-4ab3-89c8-627e976f553c"
|
"id": "http://enterprise.lemmy.ml/activities/block/726f43ab-bd0e-4ab3-89c8-627e976f553c"
|
||||||
},
|
},
|
||||||
"cc": [
|
"cc": [
|
39
crates/apub/assets/lemmy/objects/instance.json
Normal file
39
crates/apub/assets/lemmy/objects/instance.json
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
{
|
||||||
|
"stickied": "as:stickied",
|
||||||
|
"pt": "https://join-lemmy.org#",
|
||||||
|
"sc": "http://schema.org#",
|
||||||
|
"matrixUserId": {
|
||||||
|
"type": "sc:Text",
|
||||||
|
"id": "as:alsoKnownAs"
|
||||||
|
},
|
||||||
|
"sensitive": "as:sensitive",
|
||||||
|
"comments_enabled": {
|
||||||
|
"type": "sc:Boolean",
|
||||||
|
"id": "pt:commentsEnabled"
|
||||||
|
},
|
||||||
|
"moderators": "as:moderators"
|
||||||
|
},
|
||||||
|
"https://w3id.org/security/v1"
|
||||||
|
],
|
||||||
|
"type": "Service",
|
||||||
|
"id": "https://enterprise.lemmy.ml/",
|
||||||
|
"name": "Enterprise",
|
||||||
|
"summary": "A test instance",
|
||||||
|
"content": "<p>Enterprise sidebar</p>\\n",
|
||||||
|
"mediaType": "text/html",
|
||||||
|
"source": {
|
||||||
|
"content": "Enterprise sidebar",
|
||||||
|
"mediaType": "text/markdown"
|
||||||
|
},
|
||||||
|
"inbox": "https://enterprise.lemmy.ml/inbox",
|
||||||
|
"outbox": "https://enterprise.lemmy.ml/outbox",
|
||||||
|
"publicKey": {
|
||||||
|
"id": "https://enterprise.lemmy.ml/#main-key",
|
||||||
|
"owner": "https://enterprise.lemmy.ml/",
|
||||||
|
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAupcK0xTw5yQb/fnztAmb\n9LfPbhJJP1+1GwUaOXGYiDJD6uYJhl9CLmgztLl3RyV9ltOYoN8/NLNDfOMmgOjd\nrsNWEjDI9IcVPmiZnhU7hsi6KgQvJzzv8O5/xYjAGhDfrGmtdpL+lyG0B5fQod8J\n/V5VWvTQ0B0qFrLSBBuhOrp8/fTtDskdtElDPtnNfH2jn6FgtLOijidWwf9ekFo4\n0I1JeuEw6LuD/CzKVJTPoztzabUV1DQF/DnFJm+8y7SCJa9jEO56Uf9eVfa1jF6f\ndH6ZvNJMiafstVuLMAw7C/eNJy3ufXgtZ4403oOKA0aRSYf1cc9pHSZ9gDE/mevH\nLwIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||||
|
},
|
||||||
|
"published": "2022-01-19T21:52:11.110741+00:00"
|
||||||
|
}
|
241
crates/apub/src/activities/block/block_user.rs
Normal file
241
crates/apub/src/activities/block/block_user.rs
Normal file
|
@ -0,0 +1,241 @@
|
||||||
|
use crate::{
|
||||||
|
activities::{
|
||||||
|
block::{generate_cc, generate_instance_inboxes, SiteOrCommunity},
|
||||||
|
community::{announce::GetCommunity, send_activity_in_community},
|
||||||
|
generate_activity_id,
|
||||||
|
send_lemmy_activity,
|
||||||
|
verify_activity,
|
||||||
|
verify_is_public,
|
||||||
|
verify_mod_action,
|
||||||
|
verify_person_in_community,
|
||||||
|
},
|
||||||
|
activity_lists::AnnouncableActivities,
|
||||||
|
objects::{community::ApubCommunity, person::ApubPerson},
|
||||||
|
protocol::activities::block::block_user::BlockUser,
|
||||||
|
};
|
||||||
|
use activitystreams_kinds::{activity::BlockType, public};
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use lemmy_api_common::{blocking, remove_user_data, remove_user_data_in_community};
|
||||||
|
use lemmy_apub_lib::{
|
||||||
|
data::Data,
|
||||||
|
object_id::ObjectId,
|
||||||
|
traits::{ActivityHandler, ActorType},
|
||||||
|
verify::verify_domains_match,
|
||||||
|
};
|
||||||
|
use lemmy_db_schema::{
|
||||||
|
source::{
|
||||||
|
community::{
|
||||||
|
CommunityFollower,
|
||||||
|
CommunityFollowerForm,
|
||||||
|
CommunityPersonBan,
|
||||||
|
CommunityPersonBanForm,
|
||||||
|
},
|
||||||
|
moderator::{ModBan, ModBanForm},
|
||||||
|
person::Person,
|
||||||
|
},
|
||||||
|
traits::{Bannable, Crud, Followable},
|
||||||
|
};
|
||||||
|
use lemmy_utils::{settings::structs::Settings, utils::convert_datetime, LemmyError};
|
||||||
|
use lemmy_websocket::LemmyContext;
|
||||||
|
|
||||||
|
impl BlockUser {
|
||||||
|
pub(in crate::activities::block) async fn new(
|
||||||
|
target: &SiteOrCommunity,
|
||||||
|
user: &ApubPerson,
|
||||||
|
mod_: &ApubPerson,
|
||||||
|
remove_data: Option<bool>,
|
||||||
|
reason: Option<String>,
|
||||||
|
expires: Option<NaiveDateTime>,
|
||||||
|
context: &LemmyContext,
|
||||||
|
) -> Result<BlockUser, LemmyError> {
|
||||||
|
Ok(BlockUser {
|
||||||
|
actor: ObjectId::new(mod_.actor_id()),
|
||||||
|
to: vec![public()],
|
||||||
|
object: ObjectId::new(user.actor_id()),
|
||||||
|
cc: generate_cc(target, context.pool()).await?,
|
||||||
|
target: target.id(),
|
||||||
|
kind: BlockType::Block,
|
||||||
|
remove_data,
|
||||||
|
summary: reason,
|
||||||
|
id: generate_activity_id(
|
||||||
|
BlockType::Block,
|
||||||
|
&context.settings().get_protocol_and_hostname(),
|
||||||
|
)?,
|
||||||
|
expires: expires.map(convert_datetime),
|
||||||
|
unparsed: Default::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
pub async fn send(
|
||||||
|
target: &SiteOrCommunity,
|
||||||
|
user: &ApubPerson,
|
||||||
|
mod_: &ApubPerson,
|
||||||
|
remove_data: bool,
|
||||||
|
reason: Option<String>,
|
||||||
|
expires: Option<NaiveDateTime>,
|
||||||
|
context: &LemmyContext,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let block = BlockUser::new(
|
||||||
|
target,
|
||||||
|
user,
|
||||||
|
mod_,
|
||||||
|
Some(remove_data),
|
||||||
|
reason,
|
||||||
|
expires,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let block_id = block.id.clone();
|
||||||
|
|
||||||
|
match target {
|
||||||
|
SiteOrCommunity::Site(_) => {
|
||||||
|
let inboxes = generate_instance_inboxes(user, context.pool()).await?;
|
||||||
|
send_lemmy_activity(context, &block, &block_id, mod_, inboxes, false).await
|
||||||
|
}
|
||||||
|
SiteOrCommunity::Community(c) => {
|
||||||
|
let activity = AnnouncableActivities::BlockUser(block);
|
||||||
|
let inboxes = vec![user.shared_inbox_or_inbox_url()];
|
||||||
|
send_activity_in_community(activity, &block_id, mod_, c, inboxes, context).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for BlockUser {
|
||||||
|
type DataType = LemmyContext;
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &Data<LemmyContext>,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_is_public(&self.to, &self.cc)?;
|
||||||
|
verify_activity(&self.id, self.actor.inner(), &context.settings())?;
|
||||||
|
match self
|
||||||
|
.target
|
||||||
|
.dereference(context, context.client(), request_counter)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
SiteOrCommunity::Site(site) => {
|
||||||
|
let domain = self.object.inner().domain().expect("url needs domain");
|
||||||
|
if Settings::get().hostname == domain {
|
||||||
|
return Err(
|
||||||
|
anyhow!("Site bans from remote instance can't affect user's home instance").into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// site ban can only target a user who is on the same instance as the actor (admin)
|
||||||
|
verify_domains_match(&site.actor_id(), self.actor.inner())?;
|
||||||
|
verify_domains_match(&site.actor_id(), self.object.inner())?;
|
||||||
|
}
|
||||||
|
SiteOrCommunity::Community(community) => {
|
||||||
|
verify_person_in_community(&self.actor, &community, context, request_counter).await?;
|
||||||
|
verify_mod_action(&self.actor, &community, context, request_counter).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn receive(
|
||||||
|
self,
|
||||||
|
context: &Data<LemmyContext>,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let expires = self.expires.map(|u| u.naive_local());
|
||||||
|
let mod_person = self
|
||||||
|
.actor
|
||||||
|
.dereference(context, context.client(), request_counter)
|
||||||
|
.await?;
|
||||||
|
let blocked_person = self
|
||||||
|
.object
|
||||||
|
.dereference(context, context.client(), request_counter)
|
||||||
|
.await?;
|
||||||
|
let target = self
|
||||||
|
.target
|
||||||
|
.dereference(context, context.client(), request_counter)
|
||||||
|
.await?;
|
||||||
|
match target {
|
||||||
|
SiteOrCommunity::Site(_site) => {
|
||||||
|
let blocked_person = blocking(context.pool(), move |conn| {
|
||||||
|
Person::ban_person(conn, blocked_person.id, true, expires)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
if self.remove_data.unwrap_or(false) {
|
||||||
|
remove_user_data(blocked_person.id, context.pool()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// write mod log
|
||||||
|
let form = ModBanForm {
|
||||||
|
mod_person_id: mod_person.id,
|
||||||
|
other_person_id: blocked_person.id,
|
||||||
|
reason: self.summary,
|
||||||
|
banned: Some(true),
|
||||||
|
expires,
|
||||||
|
};
|
||||||
|
blocking(context.pool(), move |conn| ModBan::create(conn, &form)).await??;
|
||||||
|
}
|
||||||
|
SiteOrCommunity::Community(community) => {
|
||||||
|
let community_user_ban_form = CommunityPersonBanForm {
|
||||||
|
community_id: community.id,
|
||||||
|
person_id: blocked_person.id,
|
||||||
|
expires: Some(expires),
|
||||||
|
};
|
||||||
|
blocking(context.pool(), move |conn| {
|
||||||
|
CommunityPersonBan::ban(conn, &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,
|
||||||
|
};
|
||||||
|
blocking(context.pool(), move |conn: &'_ _| {
|
||||||
|
CommunityFollower::unfollow(conn, &community_follower_form)
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
if self.remove_data.unwrap_or(false) {
|
||||||
|
remove_user_data_in_community(community.id, blocked_person.id, context.pool()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// write to mod log
|
||||||
|
let form = ModBanForm {
|
||||||
|
mod_person_id: mod_person.id,
|
||||||
|
other_person_id: blocked_person.id,
|
||||||
|
reason: self.summary,
|
||||||
|
banned: Some(true),
|
||||||
|
expires,
|
||||||
|
};
|
||||||
|
blocking(context.pool(), move |conn| ModBan::create(conn, &form)).await??;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl GetCommunity for BlockUser {
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn get_community(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<ApubCommunity, LemmyError> {
|
||||||
|
let target = self
|
||||||
|
.target
|
||||||
|
.dereference(context, context.client(), request_counter)
|
||||||
|
.await?;
|
||||||
|
match target {
|
||||||
|
SiteOrCommunity::Community(c) => Ok(c),
|
||||||
|
SiteOrCommunity::Site(_) => Err(anyhow!("Calling get_community() on site activity").into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
144
crates/apub/src/activities/block/mod.rs
Normal file
144
crates/apub/src/activities/block/mod.rs
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
use crate::{
|
||||||
|
objects::{community::ApubCommunity, instance::ApubSite, person::ApubPerson},
|
||||||
|
protocol::objects::{group::Group, instance::Instance},
|
||||||
|
};
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_apub_lib::{
|
||||||
|
object_id::ObjectId,
|
||||||
|
traits::{ActorType, ApubObject},
|
||||||
|
};
|
||||||
|
use lemmy_db_schema::{source::site::Site, DbPool};
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::LemmyContext;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub mod block_user;
|
||||||
|
pub mod undo_block_user;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum SiteOrCommunity {
|
||||||
|
Site(ApubSite),
|
||||||
|
Community(ApubCommunity),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum InstanceOrGroup {
|
||||||
|
Instance(Instance),
|
||||||
|
Group(Group),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ApubObject for SiteOrCommunity {
|
||||||
|
type DataType = LemmyContext;
|
||||||
|
type ApubType = InstanceOrGroup;
|
||||||
|
type TombstoneType = ();
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
|
||||||
|
Some(match self {
|
||||||
|
SiteOrCommunity::Site(i) => i.last_refreshed_at,
|
||||||
|
SiteOrCommunity::Community(c) => c.last_refreshed_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn read_from_apub_id(
|
||||||
|
object_id: Url,
|
||||||
|
data: &Self::DataType,
|
||||||
|
) -> Result<Option<Self>, LemmyError>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let site = ApubSite::read_from_apub_id(object_id.clone(), data).await?;
|
||||||
|
Ok(match site {
|
||||||
|
Some(o) => Some(SiteOrCommunity::Site(o)),
|
||||||
|
None => ApubCommunity::read_from_apub_id(object_id, data)
|
||||||
|
.await?
|
||||||
|
.map(SiteOrCommunity::Community),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(self, _data: &Self::DataType) -> Result<(), LemmyError> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn into_apub(self, _data: &Self::DataType) -> Result<Self::ApubType, LemmyError> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_tombstone(&self) -> Result<Self::TombstoneType, LemmyError> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn verify(
|
||||||
|
apub: &Self::ApubType,
|
||||||
|
expected_domain: &Url,
|
||||||
|
data: &Self::DataType,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
match apub {
|
||||||
|
InstanceOrGroup::Instance(i) => {
|
||||||
|
ApubSite::verify(i, expected_domain, data, request_counter).await
|
||||||
|
}
|
||||||
|
InstanceOrGroup::Group(g) => {
|
||||||
|
ApubCommunity::verify(g, expected_domain, data, request_counter).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn from_apub(
|
||||||
|
apub: Self::ApubType,
|
||||||
|
data: &Self::DataType,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<Self, LemmyError>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
Ok(match apub {
|
||||||
|
InstanceOrGroup::Instance(p) => {
|
||||||
|
SiteOrCommunity::Site(ApubSite::from_apub(p, data, request_counter).await?)
|
||||||
|
}
|
||||||
|
InstanceOrGroup::Group(n) => {
|
||||||
|
SiteOrCommunity::Community(ApubCommunity::from_apub(n, data, request_counter).await?)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SiteOrCommunity {
|
||||||
|
fn id(&self) -> ObjectId<SiteOrCommunity> {
|
||||||
|
match self {
|
||||||
|
SiteOrCommunity::Site(s) => ObjectId::new(s.actor_id.clone()),
|
||||||
|
SiteOrCommunity::Community(c) => ObjectId::new(c.actor_id.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn generate_cc(target: &SiteOrCommunity, pool: &DbPool) -> Result<Vec<Url>, LemmyError> {
|
||||||
|
Ok(match target {
|
||||||
|
SiteOrCommunity::Site(_) => blocking(pool, Site::read_remote_sites)
|
||||||
|
.await??
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.actor_id.into())
|
||||||
|
.collect(),
|
||||||
|
SiteOrCommunity::Community(c) => vec![c.actor_id()],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn generate_instance_inboxes(
|
||||||
|
blocked_user: &ApubPerson,
|
||||||
|
pool: &DbPool,
|
||||||
|
) -> Result<Vec<Url>, LemmyError> {
|
||||||
|
let mut inboxes: Vec<Url> = blocking(pool, Site::read_remote_sites)
|
||||||
|
.await??
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.inbox_url.into())
|
||||||
|
.collect();
|
||||||
|
inboxes.push(blocked_user.shared_inbox_or_inbox_url());
|
||||||
|
Ok(inboxes)
|
||||||
|
}
|
164
crates/apub/src/activities/block/undo_block_user.rs
Normal file
164
crates/apub/src/activities/block/undo_block_user.rs
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
use crate::{
|
||||||
|
activities::{
|
||||||
|
block::{generate_cc, generate_instance_inboxes, SiteOrCommunity},
|
||||||
|
community::{announce::GetCommunity, send_activity_in_community},
|
||||||
|
generate_activity_id,
|
||||||
|
send_lemmy_activity,
|
||||||
|
verify_activity,
|
||||||
|
verify_is_public,
|
||||||
|
},
|
||||||
|
activity_lists::AnnouncableActivities,
|
||||||
|
objects::{community::ApubCommunity, person::ApubPerson},
|
||||||
|
protocol::activities::block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
|
||||||
|
};
|
||||||
|
use activitystreams_kinds::{activity::UndoType, public};
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_apub_lib::{
|
||||||
|
data::Data,
|
||||||
|
object_id::ObjectId,
|
||||||
|
traits::{ActivityHandler, ActorType},
|
||||||
|
verify::verify_domains_match,
|
||||||
|
};
|
||||||
|
use lemmy_db_schema::{
|
||||||
|
source::{
|
||||||
|
community::{CommunityPersonBan, CommunityPersonBanForm},
|
||||||
|
moderator::{ModBan, ModBanForm},
|
||||||
|
person::Person,
|
||||||
|
},
|
||||||
|
traits::{Bannable, Crud},
|
||||||
|
};
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::LemmyContext;
|
||||||
|
|
||||||
|
impl UndoBlockUser {
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
pub async fn send(
|
||||||
|
target: &SiteOrCommunity,
|
||||||
|
user: &ApubPerson,
|
||||||
|
mod_: &ApubPerson,
|
||||||
|
reason: Option<String>,
|
||||||
|
context: &LemmyContext,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let block = BlockUser::new(target, user, mod_, None, reason, None, context).await?;
|
||||||
|
|
||||||
|
let id = generate_activity_id(
|
||||||
|
UndoType::Undo,
|
||||||
|
&context.settings().get_protocol_and_hostname(),
|
||||||
|
)?;
|
||||||
|
let undo = UndoBlockUser {
|
||||||
|
actor: ObjectId::new(mod_.actor_id()),
|
||||||
|
to: vec![public()],
|
||||||
|
object: block,
|
||||||
|
cc: generate_cc(target, context.pool()).await?,
|
||||||
|
kind: UndoType::Undo,
|
||||||
|
id: id.clone(),
|
||||||
|
unparsed: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let inboxes = vec![user.shared_inbox_or_inbox_url()];
|
||||||
|
match target {
|
||||||
|
SiteOrCommunity::Site(_) => {
|
||||||
|
let inboxes = generate_instance_inboxes(user, context.pool()).await?;
|
||||||
|
send_lemmy_activity(context, &undo, &id, mod_, inboxes, false).await
|
||||||
|
}
|
||||||
|
SiteOrCommunity::Community(c) => {
|
||||||
|
let activity = AnnouncableActivities::UndoBlockUser(undo);
|
||||||
|
send_activity_in_community(activity, &id, mod_, c, inboxes, context).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ActivityHandler for UndoBlockUser {
|
||||||
|
type DataType = LemmyContext;
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
context: &Data<LemmyContext>,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
verify_is_public(&self.to, &self.cc)?;
|
||||||
|
verify_activity(&self.id, self.actor.inner(), &context.settings())?;
|
||||||
|
verify_domains_match(self.actor.inner(), self.object.actor.inner())?;
|
||||||
|
self.object.verify(context, request_counter).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn receive(
|
||||||
|
self,
|
||||||
|
context: &Data<LemmyContext>,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let expires = self.object.expires.map(|u| u.naive_local());
|
||||||
|
let mod_person = self
|
||||||
|
.actor
|
||||||
|
.dereference(context, context.client(), request_counter)
|
||||||
|
.await?;
|
||||||
|
let blocked_person = self
|
||||||
|
.object
|
||||||
|
.object
|
||||||
|
.dereference(context, context.client(), request_counter)
|
||||||
|
.await?;
|
||||||
|
match self
|
||||||
|
.object
|
||||||
|
.target
|
||||||
|
.dereference(context, context.client(), request_counter)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
SiteOrCommunity::Site(_site) => {
|
||||||
|
let blocked_person = blocking(context.pool(), move |conn| {
|
||||||
|
Person::ban_person(conn, blocked_person.id, false, expires)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
// write mod log
|
||||||
|
let form = ModBanForm {
|
||||||
|
mod_person_id: mod_person.id,
|
||||||
|
other_person_id: blocked_person.id,
|
||||||
|
reason: self.object.summary,
|
||||||
|
banned: Some(false),
|
||||||
|
expires,
|
||||||
|
};
|
||||||
|
blocking(context.pool(), move |conn| ModBan::create(conn, &form)).await??;
|
||||||
|
}
|
||||||
|
SiteOrCommunity::Community(community) => {
|
||||||
|
let community_user_ban_form = CommunityPersonBanForm {
|
||||||
|
community_id: community.id,
|
||||||
|
person_id: blocked_person.id,
|
||||||
|
expires: None,
|
||||||
|
};
|
||||||
|
blocking(context.pool(), move |conn: &'_ _| {
|
||||||
|
CommunityPersonBan::unban(conn, &community_user_ban_form)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
// write to mod log
|
||||||
|
let form = ModBanForm {
|
||||||
|
mod_person_id: mod_person.id,
|
||||||
|
other_person_id: blocked_person.id,
|
||||||
|
reason: self.object.summary,
|
||||||
|
banned: Some(false),
|
||||||
|
expires,
|
||||||
|
};
|
||||||
|
blocking(context.pool(), move |conn| ModBan::create(conn, &form)).await??;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl GetCommunity for UndoBlockUser {
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn get_community(
|
||||||
|
&self,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<ApubCommunity, LemmyError> {
|
||||||
|
self.object.get_community(context, request_counter).await
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,145 +0,0 @@
|
||||||
use crate::{
|
|
||||||
activities::{
|
|
||||||
community::{announce::GetCommunity, send_activity_in_community},
|
|
||||||
generate_activity_id,
|
|
||||||
verify_activity,
|
|
||||||
verify_is_public,
|
|
||||||
verify_mod_action,
|
|
||||||
verify_person_in_community,
|
|
||||||
},
|
|
||||||
activity_lists::AnnouncableActivities,
|
|
||||||
objects::{community::ApubCommunity, person::ApubPerson},
|
|
||||||
protocol::activities::community::block_user::BlockUserFromCommunity,
|
|
||||||
};
|
|
||||||
use activitystreams_kinds::{activity::BlockType, public};
|
|
||||||
use chrono::NaiveDateTime;
|
|
||||||
use lemmy_api_common::blocking;
|
|
||||||
use lemmy_apub_lib::{
|
|
||||||
data::Data,
|
|
||||||
object_id::ObjectId,
|
|
||||||
traits::{ActivityHandler, ActorType},
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::community::{
|
|
||||||
CommunityFollower,
|
|
||||||
CommunityFollowerForm,
|
|
||||||
CommunityPersonBan,
|
|
||||||
CommunityPersonBanForm,
|
|
||||||
},
|
|
||||||
traits::{Bannable, Followable},
|
|
||||||
};
|
|
||||||
use lemmy_utils::{utils::convert_datetime, LemmyError};
|
|
||||||
use lemmy_websocket::LemmyContext;
|
|
||||||
|
|
||||||
impl BlockUserFromCommunity {
|
|
||||||
pub(in crate::activities::community) fn new(
|
|
||||||
community: &ApubCommunity,
|
|
||||||
target: &ApubPerson,
|
|
||||||
actor: &ApubPerson,
|
|
||||||
expires: Option<NaiveDateTime>,
|
|
||||||
context: &LemmyContext,
|
|
||||||
) -> Result<BlockUserFromCommunity, LemmyError> {
|
|
||||||
Ok(BlockUserFromCommunity {
|
|
||||||
actor: ObjectId::new(actor.actor_id()),
|
|
||||||
to: vec![public()],
|
|
||||||
object: ObjectId::new(target.actor_id()),
|
|
||||||
cc: vec![community.actor_id()],
|
|
||||||
target: ObjectId::new(community.actor_id()),
|
|
||||||
kind: BlockType::Block,
|
|
||||||
id: generate_activity_id(
|
|
||||||
BlockType::Block,
|
|
||||||
&context.settings().get_protocol_and_hostname(),
|
|
||||||
)?,
|
|
||||||
expires: expires.map(convert_datetime),
|
|
||||||
unparsed: Default::default(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
|
||||||
pub async fn send(
|
|
||||||
community: &ApubCommunity,
|
|
||||||
target: &ApubPerson,
|
|
||||||
actor: &ApubPerson,
|
|
||||||
expires: Option<NaiveDateTime>,
|
|
||||||
context: &LemmyContext,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let block = BlockUserFromCommunity::new(community, target, actor, expires, context)?;
|
|
||||||
let block_id = block.id.clone();
|
|
||||||
|
|
||||||
let activity = AnnouncableActivities::BlockUserFromCommunity(block);
|
|
||||||
let inboxes = vec![target.shared_inbox_or_inbox_url()];
|
|
||||||
send_activity_in_community(activity, &block_id, actor, community, inboxes, context).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl ActivityHandler for BlockUserFromCommunity {
|
|
||||||
type DataType = LemmyContext;
|
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
|
||||||
async fn verify(
|
|
||||||
&self,
|
|
||||||
context: &Data<LemmyContext>,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
verify_is_public(&self.to, &self.cc)?;
|
|
||||||
verify_activity(&self.id, self.actor.inner(), &context.settings())?;
|
|
||||||
let community = self.get_community(context, request_counter).await?;
|
|
||||||
verify_person_in_community(&self.actor, &community, context, request_counter).await?;
|
|
||||||
verify_mod_action(&self.actor, &community, context, request_counter).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
|
||||||
async fn receive(
|
|
||||||
self,
|
|
||||||
context: &Data<LemmyContext>,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let community = self.get_community(context, request_counter).await?;
|
|
||||||
let blocked_user = self
|
|
||||||
.object
|
|
||||||
.dereference(context, context.client(), request_counter)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let community_user_ban_form = CommunityPersonBanForm {
|
|
||||||
community_id: community.id,
|
|
||||||
person_id: blocked_user.id,
|
|
||||||
expires: Some(self.expires.map(|u| u.naive_local())),
|
|
||||||
};
|
|
||||||
|
|
||||||
blocking(context.pool(), move |conn: &'_ _| {
|
|
||||||
CommunityPersonBan::ban(conn, &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_user.id,
|
|
||||||
pending: false,
|
|
||||||
};
|
|
||||||
blocking(context.pool(), move |conn: &'_ _| {
|
|
||||||
CommunityFollower::unfollow(conn, &community_follower_form)
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl GetCommunity for BlockUserFromCommunity {
|
|
||||||
#[tracing::instrument(skip_all)]
|
|
||||||
async fn get_community(
|
|
||||||
&self,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<ApubCommunity, LemmyError> {
|
|
||||||
self
|
|
||||||
.target
|
|
||||||
.dereference(context, context.client(), request_counter)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -11,10 +11,8 @@ use url::Url;
|
||||||
|
|
||||||
pub mod add_mod;
|
pub mod add_mod;
|
||||||
pub mod announce;
|
pub mod announce;
|
||||||
pub mod block_user;
|
|
||||||
pub mod remove_mod;
|
pub mod remove_mod;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
pub mod undo_block_user;
|
|
||||||
pub mod update;
|
pub mod update;
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
|
|
|
@ -1,118 +0,0 @@
|
||||||
use crate::{
|
|
||||||
activities::{
|
|
||||||
community::{announce::GetCommunity, send_activity_in_community},
|
|
||||||
generate_activity_id,
|
|
||||||
verify_activity,
|
|
||||||
verify_is_public,
|
|
||||||
verify_mod_action,
|
|
||||||
verify_person_in_community,
|
|
||||||
},
|
|
||||||
activity_lists::AnnouncableActivities,
|
|
||||||
objects::{community::ApubCommunity, person::ApubPerson},
|
|
||||||
protocol::activities::community::{
|
|
||||||
block_user::BlockUserFromCommunity,
|
|
||||||
undo_block_user::UndoBlockUserFromCommunity,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use activitystreams_kinds::{activity::UndoType, public};
|
|
||||||
use lemmy_api_common::blocking;
|
|
||||||
use lemmy_apub_lib::{
|
|
||||||
data::Data,
|
|
||||||
object_id::ObjectId,
|
|
||||||
traits::{ActivityHandler, ActorType},
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::community::{CommunityPersonBan, CommunityPersonBanForm},
|
|
||||||
traits::Bannable,
|
|
||||||
};
|
|
||||||
use lemmy_utils::LemmyError;
|
|
||||||
use lemmy_websocket::LemmyContext;
|
|
||||||
|
|
||||||
impl UndoBlockUserFromCommunity {
|
|
||||||
#[tracing::instrument(skip_all)]
|
|
||||||
pub async fn send(
|
|
||||||
community: &ApubCommunity,
|
|
||||||
target: &ApubPerson,
|
|
||||||
actor: &ApubPerson,
|
|
||||||
context: &LemmyContext,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let block = BlockUserFromCommunity::new(community, target, actor, None, context)?;
|
|
||||||
|
|
||||||
let id = generate_activity_id(
|
|
||||||
UndoType::Undo,
|
|
||||||
&context.settings().get_protocol_and_hostname(),
|
|
||||||
)?;
|
|
||||||
let undo = UndoBlockUserFromCommunity {
|
|
||||||
actor: ObjectId::new(actor.actor_id()),
|
|
||||||
to: vec![public()],
|
|
||||||
object: block,
|
|
||||||
cc: vec![community.actor_id()],
|
|
||||||
kind: UndoType::Undo,
|
|
||||||
id: id.clone(),
|
|
||||||
unparsed: Default::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let activity = AnnouncableActivities::UndoBlockUserFromCommunity(undo);
|
|
||||||
let inboxes = vec![target.shared_inbox_or_inbox_url()];
|
|
||||||
send_activity_in_community(activity, &id, actor, community, inboxes, context).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl ActivityHandler for UndoBlockUserFromCommunity {
|
|
||||||
type DataType = LemmyContext;
|
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
|
||||||
async fn verify(
|
|
||||||
&self,
|
|
||||||
context: &Data<LemmyContext>,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
verify_is_public(&self.to, &self.cc)?;
|
|
||||||
verify_activity(&self.id, self.actor.inner(), &context.settings())?;
|
|
||||||
let community = self.get_community(context, request_counter).await?;
|
|
||||||
verify_person_in_community(&self.actor, &community, context, request_counter).await?;
|
|
||||||
verify_mod_action(&self.actor, &community, context, request_counter).await?;
|
|
||||||
self.object.verify(context, request_counter).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
|
||||||
async fn receive(
|
|
||||||
self,
|
|
||||||
context: &Data<LemmyContext>,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let community = self.get_community(context, request_counter).await?;
|
|
||||||
let blocked_user = self
|
|
||||||
.object
|
|
||||||
.object
|
|
||||||
.dereference(context, context.client(), request_counter)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let community_user_ban_form = CommunityPersonBanForm {
|
|
||||||
community_id: community.id,
|
|
||||||
person_id: blocked_user.id,
|
|
||||||
expires: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
blocking(context.pool(), move |conn: &'_ _| {
|
|
||||||
CommunityPersonBan::unban(conn, &community_user_ban_form)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl GetCommunity for UndoBlockUserFromCommunity {
|
|
||||||
#[tracing::instrument(skip_all)]
|
|
||||||
async fn get_community(
|
|
||||||
&self,
|
|
||||||
context: &LemmyContext,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<ApubCommunity, LemmyError> {
|
|
||||||
self.object.get_community(context, request_counter).await
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -25,6 +25,7 @@ use tracing::info;
|
||||||
use url::{ParseError, Url};
|
use url::{ParseError, Url};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub mod block;
|
||||||
pub mod comment;
|
pub mod comment;
|
||||||
pub mod community;
|
pub mod community;
|
||||||
pub mod deletion;
|
pub mod deletion;
|
||||||
|
|
|
@ -3,13 +3,12 @@ use crate::{
|
||||||
objects::community::ApubCommunity,
|
objects::community::ApubCommunity,
|
||||||
protocol::{
|
protocol::{
|
||||||
activities::{
|
activities::{
|
||||||
|
block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
|
||||||
community::{
|
community::{
|
||||||
add_mod::AddMod,
|
add_mod::AddMod,
|
||||||
announce::AnnounceActivity,
|
announce::AnnounceActivity,
|
||||||
block_user::BlockUserFromCommunity,
|
|
||||||
remove_mod::RemoveMod,
|
remove_mod::RemoveMod,
|
||||||
report::Report,
|
report::Report,
|
||||||
undo_block_user::UndoBlockUserFromCommunity,
|
|
||||||
update::UpdateCommunity,
|
update::UpdateCommunity,
|
||||||
},
|
},
|
||||||
create_or_update::{comment::CreateOrUpdateComment, post::CreateOrUpdatePost},
|
create_or_update::{comment::CreateOrUpdateComment, post::CreateOrUpdatePost},
|
||||||
|
@ -78,14 +77,22 @@ pub enum AnnouncableActivities {
|
||||||
Delete(Delete),
|
Delete(Delete),
|
||||||
UndoDelete(UndoDelete),
|
UndoDelete(UndoDelete),
|
||||||
UpdateCommunity(UpdateCommunity),
|
UpdateCommunity(UpdateCommunity),
|
||||||
BlockUserFromCommunity(BlockUserFromCommunity),
|
BlockUser(BlockUser),
|
||||||
UndoBlockUserFromCommunity(UndoBlockUserFromCommunity),
|
UndoBlockUser(UndoBlockUser),
|
||||||
AddMod(AddMod),
|
AddMod(AddMod),
|
||||||
RemoveMod(RemoveMod),
|
RemoveMod(RemoveMod),
|
||||||
// For compatibility with Pleroma/Mastodon (send only)
|
// For compatibility with Pleroma/Mastodon (send only)
|
||||||
Page(Page),
|
Page(Page),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
#[activity_handler(LemmyContext)]
|
||||||
|
pub enum SiteInboxActivities {
|
||||||
|
BlockUser(BlockUser),
|
||||||
|
UndoBlockUser(UndoBlockUser),
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl GetCommunity for AnnouncableActivities {
|
impl GetCommunity for AnnouncableActivities {
|
||||||
#[tracing::instrument(skip(self, context))]
|
#[tracing::instrument(skip(self, context))]
|
||||||
|
@ -103,8 +110,8 @@ impl GetCommunity for AnnouncableActivities {
|
||||||
Delete(a) => a.get_community(context, request_counter).await?,
|
Delete(a) => a.get_community(context, request_counter).await?,
|
||||||
UndoDelete(a) => a.get_community(context, request_counter).await?,
|
UndoDelete(a) => a.get_community(context, request_counter).await?,
|
||||||
UpdateCommunity(a) => a.get_community(context, request_counter).await?,
|
UpdateCommunity(a) => a.get_community(context, request_counter).await?,
|
||||||
BlockUserFromCommunity(a) => a.get_community(context, request_counter).await?,
|
BlockUser(a) => a.get_community(context, request_counter).await?,
|
||||||
UndoBlockUserFromCommunity(a) => a.get_community(context, request_counter).await?,
|
UndoBlockUser(a) => a.get_community(context, request_counter).await?,
|
||||||
AddMod(a) => a.get_community(context, request_counter).await?,
|
AddMod(a) => a.get_community(context, request_counter).await?,
|
||||||
RemoveMod(a) => a.get_community(context, request_counter).await?,
|
RemoveMod(a) => a.get_community(context, request_counter).await?,
|
||||||
Page(_) => unimplemented!(),
|
Page(_) => unimplemented!(),
|
||||||
|
|
|
@ -148,6 +148,7 @@ mod tests {
|
||||||
source::{
|
source::{
|
||||||
community::Community,
|
community::Community,
|
||||||
person::{Person, PersonForm},
|
person::{Person, PersonForm},
|
||||||
|
site::Site,
|
||||||
},
|
},
|
||||||
traits::Crud,
|
traits::Crud,
|
||||||
};
|
};
|
||||||
|
@ -159,6 +160,7 @@ mod tests {
|
||||||
let client = reqwest::Client::new().into();
|
let client = reqwest::Client::new().into();
|
||||||
let manager = create_activity_queue(client);
|
let manager = create_activity_queue(client);
|
||||||
let context = init_context(manager.queue_handle().clone());
|
let context = init_context(manager.queue_handle().clone());
|
||||||
|
let (new_mod, site) = parse_lemmy_person(&context).await;
|
||||||
let community = parse_lemmy_community(&context).await;
|
let community = parse_lemmy_community(&context).await;
|
||||||
let community_id = community.id;
|
let community_id = community.id;
|
||||||
|
|
||||||
|
@ -174,7 +176,7 @@ mod tests {
|
||||||
|
|
||||||
CommunityModerator::join(&context.pool().get().unwrap(), &community_moderator_form).unwrap();
|
CommunityModerator::join(&context.pool().get().unwrap(), &community_moderator_form).unwrap();
|
||||||
|
|
||||||
let new_mod = parse_lemmy_person(&context).await;
|
assert_eq!(site.actor_id.to_string(), "https://enterprise.lemmy.ml/");
|
||||||
|
|
||||||
let json: GroupModerators =
|
let json: GroupModerators =
|
||||||
file_to_json_object("assets/lemmy/collections/group_moderators.json").unwrap();
|
file_to_json_object("assets/lemmy/collections/group_moderators.json").unwrap();
|
||||||
|
@ -209,5 +211,6 @@ mod tests {
|
||||||
community_context.0.id,
|
community_context.0.id,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
Site::delete(&*community_context.1.pool().get().unwrap(), site.id).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
99
crates/apub/src/fetcher/deletable_apub_object.rs
Normal file
99
crates/apub/src/fetcher/deletable_apub_object.rs
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
use crate::fetcher::post_or_comment::PostOrComment;
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_db_queries::source::{
|
||||||
|
comment::Comment_,
|
||||||
|
community::Community_,
|
||||||
|
person::Person_,
|
||||||
|
post::Post_,
|
||||||
|
};
|
||||||
|
use lemmy_db_schema::source::{
|
||||||
|
comment::Comment,
|
||||||
|
community::Community,
|
||||||
|
person::Person,
|
||||||
|
post::Post,
|
||||||
|
site::Site,
|
||||||
|
};
|
||||||
|
use lemmy_utils::LemmyError;
|
||||||
|
use lemmy_websocket::LemmyContext;
|
||||||
|
|
||||||
|
// TODO: merge this trait with ApubObject (means that db_schema needs to depend on apub_lib)
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
pub trait DeletableApubObject {
|
||||||
|
// TODO: pass in tombstone with summary field, to decide between remove/delete
|
||||||
|
async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl DeletableApubObject for Community {
|
||||||
|
async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
|
||||||
|
let id = self.id;
|
||||||
|
blocking(context.pool(), move |conn| {
|
||||||
|
Community::update_deleted(conn, id, true)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl DeletableApubObject for Person {
|
||||||
|
async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
|
||||||
|
let id = self.id;
|
||||||
|
blocking(context.pool(), move |conn| Person::delete_account(conn, id)).await??;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl DeletableApubObject for Post {
|
||||||
|
async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
|
||||||
|
let id = self.id;
|
||||||
|
blocking(context.pool(), move |conn| {
|
||||||
|
Post::update_deleted(conn, id, true)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl DeletableApubObject for Comment {
|
||||||
|
async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
|
||||||
|
let id = self.id;
|
||||||
|
blocking(context.pool(), move |conn| {
|
||||||
|
Comment::update_deleted(conn, id, true)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl DeletableApubObject for PostOrComment {
|
||||||
|
async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
|
||||||
|
match self {
|
||||||
|
PostOrComment::Comment(c) => {
|
||||||
|
blocking(context.pool(), move |conn| {
|
||||||
|
Comment::update_deleted(conn, c.id, true)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
}
|
||||||
|
PostOrComment::Post(p) => {
|
||||||
|
blocking(context.pool(), move |conn| {
|
||||||
|
Post::update_deleted(conn, p.id, true)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl DeletableApubObject for Site {
|
||||||
|
async fn delete(self, _context: &LemmyContext) -> Result<(), LemmyError> {
|
||||||
|
// not implemented, ignore
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,6 +36,7 @@ mod community;
|
||||||
mod person;
|
mod person;
|
||||||
mod post;
|
mod post;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
|
pub mod site;
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub async fn shared_inbox(
|
pub async fn shared_inbox(
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
activity_lists::PersonInboxActivities,
|
activity_lists::PersonInboxActivities,
|
||||||
context::WithContext,
|
context::WithContext,
|
||||||
|
generate_outbox_url,
|
||||||
http::{
|
http::{
|
||||||
create_apub_response,
|
create_apub_response,
|
||||||
create_apub_tombstone_response,
|
create_apub_tombstone_response,
|
||||||
|
@ -9,7 +10,7 @@ use crate::{
|
||||||
ActivityCommonFields,
|
ActivityCommonFields,
|
||||||
},
|
},
|
||||||
objects::person::ApubPerson,
|
objects::person::ApubPerson,
|
||||||
protocol::collections::person_outbox::PersonOutbox,
|
protocol::collections::empty_outbox::EmptyOutbox,
|
||||||
};
|
};
|
||||||
use actix_web::{web, web::Payload, HttpRequest, HttpResponse};
|
use actix_web::{web, web::Payload, HttpRequest, HttpResponse};
|
||||||
use lemmy_api_common::blocking;
|
use lemmy_api_common::blocking;
|
||||||
|
@ -80,6 +81,7 @@ pub(crate) async fn get_apub_person_outbox(
|
||||||
Person::read_from_name(conn, &info.user_name)
|
Person::read_from_name(conn, &info.user_name)
|
||||||
})
|
})
|
||||||
.await??;
|
.await??;
|
||||||
let outbox = PersonOutbox::new(person).await?;
|
let outbox_id = generate_outbox_url(&person.actor_id)?.into();
|
||||||
|
let outbox = EmptyOutbox::new(outbox_id).await?;
|
||||||
Ok(create_apub_response(&outbox))
|
Ok(create_apub_response(&outbox))
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ use crate::http::{
|
||||||
person::{get_apub_person_http, get_apub_person_outbox, person_inbox},
|
person::{get_apub_person_http, get_apub_person_outbox, person_inbox},
|
||||||
post::get_apub_post,
|
post::get_apub_post,
|
||||||
shared_inbox,
|
shared_inbox,
|
||||||
|
site::{get_apub_site_http, get_apub_site_inbox, get_apub_site_outbox},
|
||||||
};
|
};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
guard::{Guard, GuardContext},
|
guard::{Guard, GuardContext},
|
||||||
|
@ -26,6 +27,8 @@ pub fn config(cfg: &mut web::ServiceConfig, settings: &Settings) {
|
||||||
println!("federation enabled, host is {}", settings.hostname);
|
println!("federation enabled, host is {}", settings.hostname);
|
||||||
|
|
||||||
cfg
|
cfg
|
||||||
|
.route("/", web::get().to(get_apub_site_http))
|
||||||
|
.route("/site_outbox", web::get().to(get_apub_site_outbox))
|
||||||
.route(
|
.route(
|
||||||
"/c/{community_name}",
|
"/c/{community_name}",
|
||||||
web::get().to(get_apub_community_http),
|
web::get().to(get_apub_community_http),
|
||||||
|
@ -57,7 +60,8 @@ pub fn config(cfg: &mut web::ServiceConfig, settings: &Settings) {
|
||||||
.guard(InboxRequestGuard)
|
.guard(InboxRequestGuard)
|
||||||
.route("/c/{community_name}/inbox", web::post().to(community_inbox))
|
.route("/c/{community_name}/inbox", web::post().to(community_inbox))
|
||||||
.route("/u/{user_name}/inbox", web::post().to(person_inbox))
|
.route("/u/{user_name}/inbox", web::post().to(person_inbox))
|
||||||
.route("/inbox", web::post().to(shared_inbox)),
|
.route("/inbox", web::post().to(shared_inbox))
|
||||||
|
.route("/site_inbox", web::post().to(get_apub_site_inbox)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
49
crates/apub/src/http/site.rs
Normal file
49
crates/apub/src/http/site.rs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
use crate::{
|
||||||
|
activity_lists::SiteInboxActivities,
|
||||||
|
context::WithContext,
|
||||||
|
http::{create_apub_response, payload_to_string, receive_activity, ActivityCommonFields},
|
||||||
|
objects::instance::ApubSite,
|
||||||
|
protocol::collections::empty_outbox::EmptyOutbox,
|
||||||
|
};
|
||||||
|
use actix_web::{web, web::Payload, HttpRequest, HttpResponse};
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_apub_lib::traits::ApubObject;
|
||||||
|
use lemmy_db_schema::source::site::Site;
|
||||||
|
use lemmy_utils::{settings::structs::Settings, LemmyError};
|
||||||
|
use lemmy_websocket::LemmyContext;
|
||||||
|
use tracing::info;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub(crate) async fn get_apub_site_http(
|
||||||
|
context: web::Data<LemmyContext>,
|
||||||
|
) -> Result<HttpResponse, LemmyError> {
|
||||||
|
let site: ApubSite = blocking(context.pool(), Site::read_local_site)
|
||||||
|
.await??
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let apub = site.into_apub(&context).await?;
|
||||||
|
Ok(create_apub_response(&apub))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
pub(crate) async fn get_apub_site_outbox() -> Result<HttpResponse, LemmyError> {
|
||||||
|
let outbox_id = format!(
|
||||||
|
"{}/site_outbox",
|
||||||
|
Settings::get().get_protocol_and_hostname()
|
||||||
|
);
|
||||||
|
let outbox = EmptyOutbox::new(Url::parse(&outbox_id)?).await?;
|
||||||
|
Ok(create_apub_response(&outbox))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
pub async fn get_apub_site_inbox(
|
||||||
|
request: HttpRequest,
|
||||||
|
payload: Payload,
|
||||||
|
context: web::Data<LemmyContext>,
|
||||||
|
) -> Result<HttpResponse, LemmyError> {
|
||||||
|
let unparsed = payload_to_string(payload).await?;
|
||||||
|
info!("Received site inbox activity {}", unparsed);
|
||||||
|
let activity_data: ActivityCommonFields = serde_json::from_str(&unparsed)?;
|
||||||
|
let activity = serde_json::from_str::<WithContext<SiteInboxActivities>>(&unparsed)?;
|
||||||
|
receive_activity(request, activity.inner(), activity_data, &context).await
|
||||||
|
}
|
|
@ -164,6 +164,12 @@ pub fn generate_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
|
||||||
Ok(Url::parse(&format!("{}/inbox", actor_id))?.into())
|
Ok(Url::parse(&format!("{}/inbox", actor_id))?.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn generate_site_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
|
||||||
|
let mut actor_id: Url = actor_id.clone().into();
|
||||||
|
actor_id.set_path("site_inbox");
|
||||||
|
Ok(actor_id.into())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn generate_shared_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, LemmyError> {
|
pub fn generate_shared_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, LemmyError> {
|
||||||
let actor_id: Url = actor_id.clone().into();
|
let actor_id: Url = actor_id.clone().into();
|
||||||
let url = format!(
|
let url = format!(
|
||||||
|
|
|
@ -18,7 +18,7 @@ use lemmy_api_common::blocking;
|
||||||
use lemmy_apub_lib::{
|
use lemmy_apub_lib::{
|
||||||
object_id::ObjectId,
|
object_id::ObjectId,
|
||||||
traits::ApubObject,
|
traits::ApubObject,
|
||||||
values::{MediaTypeHtml, MediaTypeMarkdown},
|
values::MediaTypeHtml,
|
||||||
verify::verify_domains_match,
|
verify::verify_domains_match,
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
|
@ -120,10 +120,7 @@ impl ApubObject for ApubComment {
|
||||||
cc: maa.ccs,
|
cc: maa.ccs,
|
||||||
content: markdown_to_html(&self.content),
|
content: markdown_to_html(&self.content),
|
||||||
media_type: Some(MediaTypeHtml::Html),
|
media_type: Some(MediaTypeHtml::Html),
|
||||||
source: SourceCompat::Lemmy(Source {
|
source: SourceCompat::Lemmy(Source::new(self.content.clone())),
|
||||||
content: self.content.clone(),
|
|
||||||
media_type: MediaTypeMarkdown::Markdown,
|
|
||||||
}),
|
|
||||||
in_reply_to,
|
in_reply_to,
|
||||||
published: Some(convert_datetime(self.published)),
|
published: Some(convert_datetime(self.published)),
|
||||||
updated: self.updated.map(convert_datetime),
|
updated: self.updated.map(convert_datetime),
|
||||||
|
@ -213,19 +210,21 @@ pub(crate) mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::objects::{
|
use crate::objects::{
|
||||||
community::{tests::parse_lemmy_community, ApubCommunity},
|
community::{tests::parse_lemmy_community, ApubCommunity},
|
||||||
|
instance::ApubSite,
|
||||||
person::{tests::parse_lemmy_person, ApubPerson},
|
person::{tests::parse_lemmy_person, ApubPerson},
|
||||||
post::ApubPost,
|
post::ApubPost,
|
||||||
tests::{file_to_json_object, init_context},
|
tests::{file_to_json_object, init_context},
|
||||||
};
|
};
|
||||||
use assert_json_diff::assert_json_include;
|
use assert_json_diff::assert_json_include;
|
||||||
use lemmy_apub_lib::activity_queue::create_activity_queue;
|
use lemmy_apub_lib::activity_queue::create_activity_queue;
|
||||||
|
use lemmy_db_schema::source::site::Site;
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
|
|
||||||
async fn prepare_comment_test(
|
async fn prepare_comment_test(
|
||||||
url: &Url,
|
url: &Url,
|
||||||
context: &LemmyContext,
|
context: &LemmyContext,
|
||||||
) -> (ApubPerson, ApubCommunity, ApubPost) {
|
) -> (ApubPerson, ApubCommunity, ApubPost, ApubSite) {
|
||||||
let person = parse_lemmy_person(context).await;
|
let (person, site) = parse_lemmy_person(context).await;
|
||||||
let community = parse_lemmy_community(context).await;
|
let community = parse_lemmy_community(context).await;
|
||||||
let post_json = file_to_json_object("assets/lemmy/objects/page.json").unwrap();
|
let post_json = file_to_json_object("assets/lemmy/objects/page.json").unwrap();
|
||||||
ApubPost::verify(&post_json, url, context, &mut 0)
|
ApubPost::verify(&post_json, url, context, &mut 0)
|
||||||
|
@ -234,13 +233,14 @@ pub(crate) mod tests {
|
||||||
let post = ApubPost::from_apub(post_json, context, &mut 0)
|
let post = ApubPost::from_apub(post_json, context, &mut 0)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
(person, community, post)
|
(person, community, post, site)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cleanup(data: (ApubPerson, ApubCommunity, ApubPost), context: &LemmyContext) {
|
fn cleanup(data: (ApubPerson, ApubCommunity, ApubPost, ApubSite), context: &LemmyContext) {
|
||||||
Post::delete(&*context.pool().get().unwrap(), data.2.id).unwrap();
|
Post::delete(&*context.pool().get().unwrap(), data.2.id).unwrap();
|
||||||
Community::delete(&*context.pool().get().unwrap(), data.1.id).unwrap();
|
Community::delete(&*context.pool().get().unwrap(), data.1.id).unwrap();
|
||||||
Person::delete(&*context.pool().get().unwrap(), data.0.id).unwrap();
|
Person::delete(&*context.pool().get().unwrap(), data.0.id).unwrap();
|
||||||
|
Site::delete(&*context.pool().get().unwrap(), data.3.id).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
|
|
|
@ -3,6 +3,7 @@ use crate::{
|
||||||
collections::{community_moderators::ApubCommunityModerators, CommunityContext},
|
collections::{community_moderators::ApubCommunityModerators, CommunityContext},
|
||||||
generate_moderators_url,
|
generate_moderators_url,
|
||||||
generate_outbox_url,
|
generate_outbox_url,
|
||||||
|
objects::instance::fetch_instance_actor_for_object,
|
||||||
protocol::{
|
protocol::{
|
||||||
objects::{group::Group, tombstone::Tombstone, Endpoints},
|
objects::{group::Group, tombstone::Tombstone, Endpoints},
|
||||||
ImageObject,
|
ImageObject,
|
||||||
|
@ -16,7 +17,6 @@ use lemmy_api_common::blocking;
|
||||||
use lemmy_apub_lib::{
|
use lemmy_apub_lib::{
|
||||||
object_id::ObjectId,
|
object_id::ObjectId,
|
||||||
traits::{ActorType, ApubObject},
|
traits::{ActorType, ApubObject},
|
||||||
values::MediaTypeMarkdown,
|
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{source::community::Community, traits::ApubActor};
|
use lemmy_db_schema::{source::community::Community, traits::ApubActor};
|
||||||
use lemmy_db_views_actor::community_follower_view::CommunityFollowerView;
|
use lemmy_db_views_actor::community_follower_view::CommunityFollowerView;
|
||||||
|
@ -80,22 +80,15 @@ impl ApubObject for ApubCommunity {
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
async fn into_apub(self, _context: &LemmyContext) -> Result<Group, LemmyError> {
|
async fn into_apub(self, _context: &LemmyContext) -> Result<Group, LemmyError> {
|
||||||
let source = self.description.clone().map(|bio| Source {
|
|
||||||
content: bio,
|
|
||||||
media_type: MediaTypeMarkdown::Markdown,
|
|
||||||
});
|
|
||||||
let icon = self.icon.clone().map(ImageObject::new);
|
|
||||||
let image = self.banner.clone().map(ImageObject::new);
|
|
||||||
|
|
||||||
let group = Group {
|
let group = Group {
|
||||||
kind: GroupType::Group,
|
kind: GroupType::Group,
|
||||||
id: ObjectId::new(self.actor_id()),
|
id: ObjectId::new(self.actor_id()),
|
||||||
preferred_username: self.name.clone(),
|
preferred_username: self.name.clone(),
|
||||||
name: self.title.clone(),
|
name: self.title.clone(),
|
||||||
summary: self.description.as_ref().map(|b| markdown_to_html(b)),
|
summary: self.description.as_ref().map(|b| markdown_to_html(b)),
|
||||||
source,
|
source: self.description.clone().map(Source::new),
|
||||||
icon,
|
icon: self.icon.clone().map(ImageObject::new),
|
||||||
image,
|
image: self.banner.clone().map(ImageObject::new),
|
||||||
sensitive: Some(self.nsfw),
|
sensitive: Some(self.nsfw),
|
||||||
moderators: Some(ObjectId::<ApubCommunityModerators>::new(
|
moderators: Some(ObjectId::<ApubCommunityModerators>::new(
|
||||||
generate_moderators_url(&self.actor_id)?,
|
generate_moderators_url(&self.actor_id)?,
|
||||||
|
@ -160,6 +153,8 @@ impl ApubObject for ApubCommunity {
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetch_instance_actor_for_object(community.actor_id(), context, request_counter).await;
|
||||||
|
|
||||||
Ok(community)
|
Ok(community)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -219,9 +214,12 @@ impl ApubCommunity {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::objects::tests::{file_to_json_object, init_context};
|
use crate::objects::{
|
||||||
|
instance::tests::parse_lemmy_instance,
|
||||||
|
tests::{file_to_json_object, init_context},
|
||||||
|
};
|
||||||
use lemmy_apub_lib::activity_queue::create_activity_queue;
|
use lemmy_apub_lib::activity_queue::create_activity_queue;
|
||||||
use lemmy_db_schema::traits::Crud;
|
use lemmy_db_schema::{source::site::Site, traits::Crud};
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
|
|
||||||
pub(crate) async fn parse_lemmy_community(context: &LemmyContext) -> ApubCommunity {
|
pub(crate) async fn parse_lemmy_community(context: &LemmyContext) -> ApubCommunity {
|
||||||
|
@ -239,7 +237,7 @@ pub(crate) mod tests {
|
||||||
let community = ApubCommunity::from_apub(json, context, &mut request_counter)
|
let community = ApubCommunity::from_apub(json, context, &mut request_counter)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// this makes two requests to the (intentionally) broken outbox/moderators collections
|
// this makes one requests to the (intentionally broken) outbox collection
|
||||||
assert_eq!(request_counter, 1);
|
assert_eq!(request_counter, 1);
|
||||||
community
|
community
|
||||||
}
|
}
|
||||||
|
@ -250,6 +248,7 @@ pub(crate) mod tests {
|
||||||
let client = reqwest::Client::new().into();
|
let client = reqwest::Client::new().into();
|
||||||
let manager = create_activity_queue(client);
|
let manager = create_activity_queue(client);
|
||||||
let context = init_context(manager.queue_handle().clone());
|
let context = init_context(manager.queue_handle().clone());
|
||||||
|
let site = parse_lemmy_instance(&context).await;
|
||||||
let community = parse_lemmy_community(&context).await;
|
let community = parse_lemmy_community(&context).await;
|
||||||
|
|
||||||
assert_eq!(community.title, "Ten Forward");
|
assert_eq!(community.title, "Ten Forward");
|
||||||
|
@ -257,5 +256,6 @@ pub(crate) mod tests {
|
||||||
assert_eq!(community.description.as_ref().unwrap().len(), 132);
|
assert_eq!(community.description.as_ref().unwrap().len(), 132);
|
||||||
|
|
||||||
Community::delete(&*context.pool().get().unwrap(), community.id).unwrap();
|
Community::delete(&*context.pool().get().unwrap(), community.id).unwrap();
|
||||||
|
Site::delete(&*context.pool().get().unwrap(), site.id).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
221
crates/apub/src/objects/instance.rs
Normal file
221
crates/apub/src/objects/instance.rs
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
use crate::{
|
||||||
|
check_is_apub_id_valid,
|
||||||
|
objects::get_summary_from_string_or_source,
|
||||||
|
protocol::{objects::instance::Instance, ImageObject, Source, Unparsed},
|
||||||
|
};
|
||||||
|
use activitystreams_kinds::actor::ServiceType;
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use lemmy_api_common::blocking;
|
||||||
|
use lemmy_apub_lib::{
|
||||||
|
object_id::ObjectId,
|
||||||
|
traits::{ActorType, ApubObject},
|
||||||
|
values::MediaTypeHtml,
|
||||||
|
verify::verify_domains_match,
|
||||||
|
};
|
||||||
|
use lemmy_db_schema::{
|
||||||
|
naive_now,
|
||||||
|
source::site::{Site, SiteForm},
|
||||||
|
};
|
||||||
|
use lemmy_utils::{
|
||||||
|
utils::{check_slurs, check_slurs_opt, convert_datetime, markdown_to_html},
|
||||||
|
LemmyError,
|
||||||
|
};
|
||||||
|
use lemmy_websocket::LemmyContext;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use tracing::debug;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ApubSite(Site);
|
||||||
|
|
||||||
|
impl Deref for ApubSite {
|
||||||
|
type Target = Site;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Site> for ApubSite {
|
||||||
|
fn from(s: Site) -> Self {
|
||||||
|
ApubSite { 0: s }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ApubObject for ApubSite {
|
||||||
|
type DataType = LemmyContext;
|
||||||
|
type ApubType = Instance;
|
||||||
|
type TombstoneType = ();
|
||||||
|
|
||||||
|
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
|
||||||
|
Some(self.last_refreshed_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn read_from_apub_id(
|
||||||
|
object_id: Url,
|
||||||
|
data: &Self::DataType,
|
||||||
|
) -> Result<Option<Self>, LemmyError> {
|
||||||
|
Ok(
|
||||||
|
blocking(data.pool(), move |conn| {
|
||||||
|
Site::read_from_apub_id(conn, object_id)
|
||||||
|
})
|
||||||
|
.await??
|
||||||
|
.map(Into::into),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(self, _data: &Self::DataType) -> Result<(), LemmyError> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn into_apub(self, _data: &Self::DataType) -> Result<Self::ApubType, LemmyError> {
|
||||||
|
let instance = Instance {
|
||||||
|
kind: ServiceType::Service,
|
||||||
|
id: ObjectId::new(self.actor_id()),
|
||||||
|
name: self.name.clone(),
|
||||||
|
content: self.sidebar.as_ref().map(|d| markdown_to_html(d)),
|
||||||
|
source: self.sidebar.clone().map(Source::new),
|
||||||
|
summary: self.description.clone(),
|
||||||
|
media_type: self.sidebar.as_ref().map(|_| MediaTypeHtml::Html),
|
||||||
|
icon: self.icon.clone().map(ImageObject::new),
|
||||||
|
image: self.banner.clone().map(ImageObject::new),
|
||||||
|
inbox: self.inbox_url.clone().into(),
|
||||||
|
outbox: Url::parse(&format!("{}/site_outbox", self.actor_id))?,
|
||||||
|
public_key: self.get_public_key()?,
|
||||||
|
published: convert_datetime(self.published),
|
||||||
|
updated: self.updated.map(convert_datetime),
|
||||||
|
unparsed: Unparsed::default(),
|
||||||
|
};
|
||||||
|
Ok(instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_tombstone(&self) -> Result<Self::TombstoneType, LemmyError> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn verify(
|
||||||
|
apub: &Self::ApubType,
|
||||||
|
expected_domain: &Url,
|
||||||
|
data: &Self::DataType,
|
||||||
|
_request_counter: &mut i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
check_is_apub_id_valid(apub.id.inner(), true, &data.settings())?;
|
||||||
|
verify_domains_match(expected_domain, apub.id.inner())?;
|
||||||
|
|
||||||
|
let slur_regex = &data.settings().slur_regex();
|
||||||
|
check_slurs(&apub.name, slur_regex)?;
|
||||||
|
check_slurs_opt(&apub.summary, slur_regex)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn from_apub(
|
||||||
|
apub: Self::ApubType,
|
||||||
|
data: &Self::DataType,
|
||||||
|
_request_counter: &mut i32,
|
||||||
|
) -> Result<Self, LemmyError> {
|
||||||
|
let site_form = SiteForm {
|
||||||
|
name: apub.name.clone(),
|
||||||
|
sidebar: Some(get_summary_from_string_or_source(
|
||||||
|
&apub.content,
|
||||||
|
&apub.source,
|
||||||
|
)),
|
||||||
|
updated: apub.updated.map(|u| u.clone().naive_local()),
|
||||||
|
icon: Some(apub.icon.clone().map(|i| i.url.into())),
|
||||||
|
banner: Some(apub.image.clone().map(|i| i.url.into())),
|
||||||
|
description: Some(apub.summary.clone()),
|
||||||
|
actor_id: Some(apub.id.clone().into()),
|
||||||
|
last_refreshed_at: Some(naive_now()),
|
||||||
|
inbox_url: Some(apub.inbox.clone().into()),
|
||||||
|
public_key: Some(apub.public_key.public_key_pem.clone()),
|
||||||
|
..SiteForm::default()
|
||||||
|
};
|
||||||
|
let site = blocking(data.pool(), move |conn| Site::upsert(conn, &site_form)).await??;
|
||||||
|
Ok(site.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActorType for ApubSite {
|
||||||
|
fn actor_id(&self) -> Url {
|
||||||
|
self.actor_id.to_owned().into()
|
||||||
|
}
|
||||||
|
fn public_key(&self) -> String {
|
||||||
|
self.public_key.to_owned()
|
||||||
|
}
|
||||||
|
fn private_key(&self) -> Option<String> {
|
||||||
|
self.private_key.to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inbox_url(&self) -> Url {
|
||||||
|
self.inbox_url.clone().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shared_inbox_url(&self) -> Option<Url> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Instance actor is at the root path, so we simply need to clear the path and other unnecessary
|
||||||
|
/// parts of the url.
|
||||||
|
pub fn instance_actor_id_from_url(mut url: Url) -> Url {
|
||||||
|
url.set_fragment(None);
|
||||||
|
url.set_path("");
|
||||||
|
url.set_query(None);
|
||||||
|
url
|
||||||
|
}
|
||||||
|
|
||||||
|
/// try to fetch the instance actor (to make things like instance rules available)
|
||||||
|
pub(in crate::objects) async fn fetch_instance_actor_for_object(
|
||||||
|
object_id: Url,
|
||||||
|
context: &LemmyContext,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) {
|
||||||
|
// try to fetch the instance actor (to make things like instance rules available)
|
||||||
|
let instance_id = instance_actor_id_from_url(object_id);
|
||||||
|
let site = ObjectId::<ApubSite>::new(instance_id.clone())
|
||||||
|
.dereference(context, context.client(), request_counter)
|
||||||
|
.await;
|
||||||
|
if let Err(e) = site {
|
||||||
|
debug!("Failed to dereference site for {}: {}", instance_id, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::objects::tests::{file_to_json_object, init_context};
|
||||||
|
use lemmy_apub_lib::activity_queue::create_activity_queue;
|
||||||
|
use lemmy_db_schema::traits::Crud;
|
||||||
|
use serial_test::serial;
|
||||||
|
|
||||||
|
pub(crate) async fn parse_lemmy_instance(context: &LemmyContext) -> ApubSite {
|
||||||
|
let json: Instance = file_to_json_object("assets/lemmy/objects/instance.json").unwrap();
|
||||||
|
let id = Url::parse("https://enterprise.lemmy.ml/").unwrap();
|
||||||
|
let mut request_counter = 0;
|
||||||
|
ApubSite::verify(&json, &id, context, &mut request_counter)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let site = ApubSite::from_apub(json, context, &mut request_counter)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(request_counter, 0);
|
||||||
|
site
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_parse_lemmy_instance() {
|
||||||
|
let client = reqwest::Client::new().into();
|
||||||
|
let manager = create_activity_queue(client);
|
||||||
|
let context = init_context(manager.queue_handle().clone());
|
||||||
|
let site = parse_lemmy_instance(&context).await;
|
||||||
|
|
||||||
|
assert_eq!(site.name, "Enterprise");
|
||||||
|
assert_eq!(site.description.as_ref().unwrap().len(), 15);
|
||||||
|
|
||||||
|
Site::delete(&*context.pool().get().unwrap(), site.id).unwrap();
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ use html2md::parse_html;
|
||||||
|
|
||||||
pub mod comment;
|
pub mod comment;
|
||||||
pub mod community;
|
pub mod community;
|
||||||
|
pub mod instance;
|
||||||
pub mod person;
|
pub mod person;
|
||||||
pub mod post;
|
pub mod post;
|
||||||
pub mod private_message;
|
pub mod private_message;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
check_is_apub_id_valid,
|
check_is_apub_id_valid,
|
||||||
generate_outbox_url,
|
generate_outbox_url,
|
||||||
objects::get_summary_from_string_or_source,
|
objects::{get_summary_from_string_or_source, instance::fetch_instance_actor_for_object},
|
||||||
protocol::{
|
protocol::{
|
||||||
objects::{
|
objects::{
|
||||||
person::{Person, UserTypes},
|
person::{Person, UserTypes},
|
||||||
|
@ -16,7 +16,6 @@ use lemmy_api_common::blocking;
|
||||||
use lemmy_apub_lib::{
|
use lemmy_apub_lib::{
|
||||||
object_id::ObjectId,
|
object_id::ObjectId,
|
||||||
traits::{ActorType, ApubObject},
|
traits::{ActorType, ApubObject},
|
||||||
values::MediaTypeMarkdown,
|
|
||||||
verify::verify_domains_match,
|
verify::verify_domains_match,
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
|
@ -88,12 +87,6 @@ impl ApubObject for ApubPerson {
|
||||||
} else {
|
} else {
|
||||||
UserTypes::Person
|
UserTypes::Person
|
||||||
};
|
};
|
||||||
let source = self.bio.clone().map(|bio| Source {
|
|
||||||
content: bio,
|
|
||||||
media_type: MediaTypeMarkdown::Markdown,
|
|
||||||
});
|
|
||||||
let icon = self.avatar.clone().map(ImageObject::new);
|
|
||||||
let image = self.banner.clone().map(ImageObject::new);
|
|
||||||
|
|
||||||
let person = Person {
|
let person = Person {
|
||||||
kind,
|
kind,
|
||||||
|
@ -101,9 +94,9 @@ impl ApubObject for ApubPerson {
|
||||||
preferred_username: self.name.clone(),
|
preferred_username: self.name.clone(),
|
||||||
name: self.display_name.clone(),
|
name: self.display_name.clone(),
|
||||||
summary: self.bio.as_ref().map(|b| markdown_to_html(b)),
|
summary: self.bio.as_ref().map(|b| markdown_to_html(b)),
|
||||||
source,
|
source: self.bio.clone().map(Source::new),
|
||||||
icon,
|
icon: self.avatar.clone().map(ImageObject::new),
|
||||||
image,
|
image: self.banner.clone().map(ImageObject::new),
|
||||||
matrix_user_id: self.matrix_user_id.clone(),
|
matrix_user_id: self.matrix_user_id.clone(),
|
||||||
published: Some(convert_datetime(self.published)),
|
published: Some(convert_datetime(self.published)),
|
||||||
outbox: generate_outbox_url(&self.actor_id)?.into(),
|
outbox: generate_outbox_url(&self.actor_id)?.into(),
|
||||||
|
@ -144,7 +137,7 @@ impl ApubObject for ApubPerson {
|
||||||
async fn from_apub(
|
async fn from_apub(
|
||||||
person: Person,
|
person: Person,
|
||||||
context: &LemmyContext,
|
context: &LemmyContext,
|
||||||
_request_counter: &mut i32,
|
request_counter: &mut i32,
|
||||||
) -> Result<ApubPerson, LemmyError> {
|
) -> Result<ApubPerson, LemmyError> {
|
||||||
let person_form = PersonForm {
|
let person_form = PersonForm {
|
||||||
name: person.preferred_username,
|
name: person.preferred_username,
|
||||||
|
@ -175,6 +168,10 @@ impl ApubObject for ApubPerson {
|
||||||
DbPerson::upsert(conn, &person_form)
|
DbPerson::upsert(conn, &person_form)
|
||||||
})
|
})
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
|
let actor_id = person.actor_id.clone().into();
|
||||||
|
fetch_instance_actor_for_object(actor_id, context, request_counter).await;
|
||||||
|
|
||||||
Ok(person.into())
|
Ok(person.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -204,12 +201,19 @@ impl ActorType for ApubPerson {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::objects::tests::{file_to_json_object, init_context};
|
use crate::{
|
||||||
|
objects::{
|
||||||
|
instance::{tests::parse_lemmy_instance, ApubSite},
|
||||||
|
tests::{file_to_json_object, init_context},
|
||||||
|
},
|
||||||
|
protocol::objects::instance::Instance,
|
||||||
|
};
|
||||||
use lemmy_apub_lib::activity_queue::create_activity_queue;
|
use lemmy_apub_lib::activity_queue::create_activity_queue;
|
||||||
use lemmy_db_schema::traits::Crud;
|
use lemmy_db_schema::{source::site::Site, traits::Crud};
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
|
|
||||||
pub(crate) async fn parse_lemmy_person(context: &LemmyContext) -> ApubPerson {
|
pub(crate) async fn parse_lemmy_person(context: &LemmyContext) -> (ApubPerson, ApubSite) {
|
||||||
|
let site = parse_lemmy_instance(context).await;
|
||||||
let json = file_to_json_object("assets/lemmy/objects/person.json").unwrap();
|
let json = file_to_json_object("assets/lemmy/objects/person.json").unwrap();
|
||||||
let url = Url::parse("https://enterprise.lemmy.ml/u/picard").unwrap();
|
let url = Url::parse("https://enterprise.lemmy.ml/u/picard").unwrap();
|
||||||
let mut request_counter = 0;
|
let mut request_counter = 0;
|
||||||
|
@ -220,7 +224,7 @@ pub(crate) mod tests {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(request_counter, 0);
|
assert_eq!(request_counter, 0);
|
||||||
person
|
(person, site)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
|
@ -229,13 +233,14 @@ pub(crate) mod tests {
|
||||||
let client = reqwest::Client::new().into();
|
let client = reqwest::Client::new().into();
|
||||||
let manager = create_activity_queue(client);
|
let manager = create_activity_queue(client);
|
||||||
let context = init_context(manager.queue_handle().clone());
|
let context = init_context(manager.queue_handle().clone());
|
||||||
let person = parse_lemmy_person(&context).await;
|
let (person, site) = parse_lemmy_person(&context).await;
|
||||||
|
|
||||||
assert_eq!(person.display_name, Some("Jean-Luc Picard".to_string()));
|
assert_eq!(person.display_name, Some("Jean-Luc Picard".to_string()));
|
||||||
assert!(!person.local);
|
assert!(!person.local);
|
||||||
assert_eq!(person.bio.as_ref().unwrap().len(), 39);
|
assert_eq!(person.bio.as_ref().unwrap().len(), 39);
|
||||||
|
|
||||||
DbPerson::delete(&*context.pool().get().unwrap(), person.id).unwrap();
|
DbPerson::delete(&*context.pool().get().unwrap(), person.id).unwrap();
|
||||||
|
Site::delete(&*context.pool().get().unwrap(), site.id).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
|
@ -244,6 +249,16 @@ pub(crate) mod tests {
|
||||||
let client = reqwest::Client::new().into();
|
let client = reqwest::Client::new().into();
|
||||||
let manager = create_activity_queue(client);
|
let manager = create_activity_queue(client);
|
||||||
let context = init_context(manager.queue_handle().clone());
|
let context = init_context(manager.queue_handle().clone());
|
||||||
|
|
||||||
|
// create and parse a fake pleroma instance actor, to avoid network request during test
|
||||||
|
let mut json: Instance = file_to_json_object("assets/lemmy/objects/instance.json").unwrap();
|
||||||
|
let id = Url::parse("https://queer.hacktivis.me/").unwrap();
|
||||||
|
json.id = ObjectId::new(id);
|
||||||
|
let mut request_counter = 0;
|
||||||
|
let site = ApubSite::from_apub(json, &context, &mut request_counter)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let json = file_to_json_object("assets/pleroma/objects/person.json").unwrap();
|
let json = file_to_json_object("assets/pleroma/objects/person.json").unwrap();
|
||||||
let url = Url::parse("https://queer.hacktivis.me/users/lanodan").unwrap();
|
let url = Url::parse("https://queer.hacktivis.me/users/lanodan").unwrap();
|
||||||
let mut request_counter = 0;
|
let mut request_counter = 0;
|
||||||
|
@ -261,5 +276,6 @@ pub(crate) mod tests {
|
||||||
assert_eq!(person.bio.as_ref().unwrap().len(), 873);
|
assert_eq!(person.bio.as_ref().unwrap().len(), 873);
|
||||||
|
|
||||||
DbPerson::delete(&*context.pool().get().unwrap(), person.id).unwrap();
|
DbPerson::delete(&*context.pool().get().unwrap(), person.id).unwrap();
|
||||||
|
Site::delete(&*context.pool().get().unwrap(), site.id).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -214,6 +214,7 @@ mod tests {
|
||||||
tests::{file_to_json_object, init_context},
|
tests::{file_to_json_object, init_context},
|
||||||
};
|
};
|
||||||
use lemmy_apub_lib::activity_queue::create_activity_queue;
|
use lemmy_apub_lib::activity_queue::create_activity_queue;
|
||||||
|
use lemmy_db_schema::source::site::Site;
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
|
@ -222,8 +223,8 @@ mod tests {
|
||||||
let client = reqwest::Client::new().into();
|
let client = reqwest::Client::new().into();
|
||||||
let manager = create_activity_queue(client);
|
let manager = create_activity_queue(client);
|
||||||
let context = init_context(manager.queue_handle().clone());
|
let context = init_context(manager.queue_handle().clone());
|
||||||
|
let (person, site) = parse_lemmy_person(&context).await;
|
||||||
let community = parse_lemmy_community(&context).await;
|
let community = parse_lemmy_community(&context).await;
|
||||||
let person = parse_lemmy_person(&context).await;
|
|
||||||
|
|
||||||
let json = file_to_json_object("assets/lemmy/objects/page.json").unwrap();
|
let json = file_to_json_object("assets/lemmy/objects/page.json").unwrap();
|
||||||
let url = Url::parse("https://enterprise.lemmy.ml/post/55143").unwrap();
|
let url = Url::parse("https://enterprise.lemmy.ml/post/55143").unwrap();
|
||||||
|
@ -246,5 +247,6 @@ mod tests {
|
||||||
Post::delete(&*context.pool().get().unwrap(), post.id).unwrap();
|
Post::delete(&*context.pool().get().unwrap(), post.id).unwrap();
|
||||||
Person::delete(&*context.pool().get().unwrap(), person.id).unwrap();
|
Person::delete(&*context.pool().get().unwrap(), person.id).unwrap();
|
||||||
Community::delete(&*context.pool().get().unwrap(), community.id).unwrap();
|
Community::delete(&*context.pool().get().unwrap(), community.id).unwrap();
|
||||||
|
Site::delete(&*context.pool().get().unwrap(), site.id).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ use lemmy_api_common::blocking;
|
||||||
use lemmy_apub_lib::{
|
use lemmy_apub_lib::{
|
||||||
object_id::ObjectId,
|
object_id::ObjectId,
|
||||||
traits::ApubObject,
|
traits::ApubObject,
|
||||||
values::{MediaTypeHtml, MediaTypeMarkdown},
|
values::MediaTypeHtml,
|
||||||
verify::verify_domains_match,
|
verify::verify_domains_match,
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
|
@ -87,10 +87,7 @@ impl ApubObject for ApubPrivateMessage {
|
||||||
to: [ObjectId::new(recipient.actor_id)],
|
to: [ObjectId::new(recipient.actor_id)],
|
||||||
content: markdown_to_html(&self.content),
|
content: markdown_to_html(&self.content),
|
||||||
media_type: Some(MediaTypeHtml::Html),
|
media_type: Some(MediaTypeHtml::Html),
|
||||||
source: Some(Source {
|
source: Some(Source::new(self.content.clone())),
|
||||||
content: self.content.clone(),
|
|
||||||
media_type: MediaTypeMarkdown::Markdown,
|
|
||||||
}),
|
|
||||||
published: Some(convert_datetime(self.published)),
|
published: Some(convert_datetime(self.published)),
|
||||||
updated: self.updated.map(convert_datetime),
|
updated: self.updated.map(convert_datetime),
|
||||||
unparsed: Default::default(),
|
unparsed: Default::default(),
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
use crate::{
|
use crate::{activities::block::SiteOrCommunity, objects::person::ApubPerson, protocol::Unparsed};
|
||||||
objects::{community::ApubCommunity, person::ApubPerson},
|
|
||||||
protocol::Unparsed,
|
|
||||||
};
|
|
||||||
use activitystreams_kinds::activity::BlockType;
|
use activitystreams_kinds::activity::BlockType;
|
||||||
use chrono::{DateTime, FixedOffset};
|
use chrono::{DateTime, FixedOffset};
|
||||||
use lemmy_apub_lib::object_id::ObjectId;
|
use lemmy_apub_lib::object_id::ObjectId;
|
||||||
|
@ -10,16 +7,21 @@ use url::Url;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct BlockUserFromCommunity {
|
pub struct BlockUser {
|
||||||
pub(crate) actor: ObjectId<ApubPerson>,
|
pub(crate) actor: ObjectId<ApubPerson>,
|
||||||
#[serde(deserialize_with = "crate::deserialize_one_or_many")]
|
#[serde(deserialize_with = "crate::deserialize_one_or_many")]
|
||||||
pub(crate) to: Vec<Url>,
|
pub(crate) to: Vec<Url>,
|
||||||
pub(crate) object: ObjectId<ApubPerson>,
|
pub(crate) object: ObjectId<ApubPerson>,
|
||||||
#[serde(deserialize_with = "crate::deserialize_one_or_many")]
|
#[serde(deserialize_with = "crate::deserialize_one_or_many")]
|
||||||
pub(crate) cc: Vec<Url>,
|
pub(crate) cc: Vec<Url>,
|
||||||
pub(crate) target: ObjectId<ApubCommunity>,
|
pub(crate) target: ObjectId<SiteOrCommunity>,
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
pub(crate) kind: BlockType,
|
pub(crate) kind: BlockType,
|
||||||
|
/// Quick and dirty solution.
|
||||||
|
/// TODO: send a separate Delete activity instead
|
||||||
|
pub(crate) remove_data: Option<bool>,
|
||||||
|
/// block reason, written to mod log
|
||||||
|
pub(crate) summary: Option<String>,
|
||||||
pub(crate) id: Url,
|
pub(crate) id: Url,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub(crate) unparsed: Unparsed,
|
pub(crate) unparsed: Unparsed,
|
17
crates/apub/src/protocol/activities/block/mod.rs
Normal file
17
crates/apub/src/protocol/activities/block/mod.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
pub mod block_user;
|
||||||
|
pub mod undo_block_user;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::protocol::{
|
||||||
|
activities::block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
|
||||||
|
tests::test_parse_lemmy_item,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_parse_lemmy_block() {
|
||||||
|
test_parse_lemmy_item::<BlockUser>("assets/lemmy/activities/block/block_user.json").unwrap();
|
||||||
|
test_parse_lemmy_item::<UndoBlockUser>("assets/lemmy/activities/block/undo_block_user.json")
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
objects::person::ApubPerson,
|
objects::person::ApubPerson,
|
||||||
protocol::{activities::community::block_user::BlockUserFromCommunity, Unparsed},
|
protocol::{activities::block::block_user::BlockUser, Unparsed},
|
||||||
};
|
};
|
||||||
use activitystreams_kinds::activity::UndoType;
|
use activitystreams_kinds::activity::UndoType;
|
||||||
use lemmy_apub_lib::object_id::ObjectId;
|
use lemmy_apub_lib::object_id::ObjectId;
|
||||||
|
@ -9,11 +9,11 @@ use url::Url;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct UndoBlockUserFromCommunity {
|
pub struct UndoBlockUser {
|
||||||
pub(crate) actor: ObjectId<ApubPerson>,
|
pub(crate) actor: ObjectId<ApubPerson>,
|
||||||
#[serde(deserialize_with = "crate::deserialize_one_or_many")]
|
#[serde(deserialize_with = "crate::deserialize_one_or_many")]
|
||||||
pub(crate) to: Vec<Url>,
|
pub(crate) to: Vec<Url>,
|
||||||
pub(crate) object: BlockUserFromCommunity,
|
pub(crate) object: BlockUser,
|
||||||
#[serde(deserialize_with = "crate::deserialize_one_or_many")]
|
#[serde(deserialize_with = "crate::deserialize_one_or_many")]
|
||||||
pub(crate) cc: Vec<Url>,
|
pub(crate) cc: Vec<Url>,
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
|
@ -1,9 +1,7 @@
|
||||||
pub mod add_mod;
|
pub mod add_mod;
|
||||||
pub mod announce;
|
pub mod announce;
|
||||||
pub mod block_user;
|
|
||||||
pub mod remove_mod;
|
pub mod remove_mod;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
pub mod undo_block_user;
|
|
||||||
pub mod update;
|
pub mod update;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -12,10 +10,8 @@ mod tests {
|
||||||
activities::community::{
|
activities::community::{
|
||||||
add_mod::AddMod,
|
add_mod::AddMod,
|
||||||
announce::AnnounceActivity,
|
announce::AnnounceActivity,
|
||||||
block_user::BlockUserFromCommunity,
|
|
||||||
remove_mod::RemoveMod,
|
remove_mod::RemoveMod,
|
||||||
report::Report,
|
report::Report,
|
||||||
undo_block_user::UndoBlockUserFromCommunity,
|
|
||||||
update::UpdateCommunity,
|
update::UpdateCommunity,
|
||||||
},
|
},
|
||||||
tests::test_parse_lemmy_item,
|
tests::test_parse_lemmy_item,
|
||||||
|
@ -32,15 +28,6 @@ mod tests {
|
||||||
test_parse_lemmy_item::<RemoveMod>("assets/lemmy/activities/community/remove_mod.json")
|
test_parse_lemmy_item::<RemoveMod>("assets/lemmy/activities/community/remove_mod.json")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
test_parse_lemmy_item::<BlockUserFromCommunity>(
|
|
||||||
"assets/lemmy/activities/community/block_user.json",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
test_parse_lemmy_item::<UndoBlockUserFromCommunity>(
|
|
||||||
"assets/lemmy/activities/community/undo_block_user.json",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
test_parse_lemmy_item::<UpdateCommunity>(
|
test_parse_lemmy_item::<UpdateCommunity>(
|
||||||
"assets/lemmy/activities/community/update_community.json",
|
"assets/lemmy/activities/community/update_community.json",
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use strum_macros::Display;
|
use strum_macros::Display;
|
||||||
|
|
||||||
|
pub mod block;
|
||||||
pub mod community;
|
pub mod community;
|
||||||
pub mod create_or_update;
|
pub mod create_or_update;
|
||||||
pub mod deletion;
|
pub mod deletion;
|
||||||
|
|
|
@ -1,24 +1,23 @@
|
||||||
use crate::generate_outbox_url;
|
|
||||||
use activitystreams_kinds::collection::OrderedCollectionType;
|
use activitystreams_kinds::collection::OrderedCollectionType;
|
||||||
use lemmy_db_schema::source::person::Person;
|
|
||||||
use lemmy_utils::LemmyError;
|
use lemmy_utils::LemmyError;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
/// Empty placeholder outbox used for Person, Instance, which dont implement a proper outbox yet.
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct PersonOutbox {
|
pub(crate) struct EmptyOutbox {
|
||||||
r#type: OrderedCollectionType,
|
r#type: OrderedCollectionType,
|
||||||
id: Url,
|
id: Url,
|
||||||
ordered_items: Vec<()>,
|
ordered_items: Vec<()>,
|
||||||
total_items: i32,
|
total_items: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PersonOutbox {
|
impl EmptyOutbox {
|
||||||
pub(crate) async fn new(user: Person) -> Result<PersonOutbox, LemmyError> {
|
pub(crate) async fn new(outbox_id: Url) -> Result<EmptyOutbox, LemmyError> {
|
||||||
Ok(PersonOutbox {
|
Ok(EmptyOutbox {
|
||||||
r#type: OrderedCollectionType::OrderedCollection,
|
r#type: OrderedCollectionType::OrderedCollection,
|
||||||
id: generate_outbox_url(&user.actor_id)?.into(),
|
id: outbox_id,
|
||||||
ordered_items: vec![],
|
ordered_items: vec![],
|
||||||
total_items: 0,
|
total_items: 0,
|
||||||
})
|
})
|
|
@ -1,16 +1,16 @@
|
||||||
|
pub(crate) mod empty_outbox;
|
||||||
pub(crate) mod group_followers;
|
pub(crate) mod group_followers;
|
||||||
pub(crate) mod group_moderators;
|
pub(crate) mod group_moderators;
|
||||||
pub(crate) mod group_outbox;
|
pub(crate) mod group_outbox;
|
||||||
pub(crate) mod person_outbox;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::protocol::{
|
use crate::protocol::{
|
||||||
collections::{
|
collections::{
|
||||||
|
empty_outbox::EmptyOutbox,
|
||||||
group_followers::GroupFollowers,
|
group_followers::GroupFollowers,
|
||||||
group_moderators::GroupModerators,
|
group_moderators::GroupModerators,
|
||||||
group_outbox::GroupOutbox,
|
group_outbox::GroupOutbox,
|
||||||
person_outbox::PersonOutbox,
|
|
||||||
},
|
},
|
||||||
tests::test_parse_lemmy_item,
|
tests::test_parse_lemmy_item,
|
||||||
};
|
};
|
||||||
|
@ -24,6 +24,6 @@ mod tests {
|
||||||
assert_eq!(outbox.ordered_items.len() as i32, outbox.total_items);
|
assert_eq!(outbox.ordered_items.len() as i32, outbox.total_items);
|
||||||
test_parse_lemmy_item::<GroupModerators>("assets/lemmy/collections/group_moderators.json")
|
test_parse_lemmy_item::<GroupModerators>("assets/lemmy/collections/group_moderators.json")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
test_parse_lemmy_item::<PersonOutbox>("assets/lemmy/collections/person_outbox.json").unwrap();
|
test_parse_lemmy_item::<EmptyOutbox>("assets/lemmy/collections/person_outbox.json").unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,15 @@ pub struct Source {
|
||||||
pub(crate) media_type: MediaTypeMarkdown,
|
pub(crate) media_type: MediaTypeMarkdown,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Source {
|
||||||
|
pub(crate) fn new(content: String) -> Self {
|
||||||
|
Source {
|
||||||
|
content,
|
||||||
|
media_type: MediaTypeMarkdown::Markdown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ImageObject {
|
pub struct ImageObject {
|
||||||
|
|
39
crates/apub/src/protocol/objects/instance.rs
Normal file
39
crates/apub/src/protocol/objects/instance.rs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
use crate::{
|
||||||
|
objects::instance::ApubSite,
|
||||||
|
protocol::{ImageObject, Source, Unparsed},
|
||||||
|
};
|
||||||
|
use activitystreams_kinds::actor::ServiceType;
|
||||||
|
use chrono::{DateTime, FixedOffset};
|
||||||
|
use lemmy_apub_lib::{object_id::ObjectId, signatures::PublicKey, values::MediaTypeHtml};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_with::skip_serializing_none;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Instance {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub(crate) kind: ServiceType,
|
||||||
|
pub(crate) id: ObjectId<ApubSite>,
|
||||||
|
// site name
|
||||||
|
pub(crate) name: String,
|
||||||
|
// sidebar
|
||||||
|
pub(crate) content: Option<String>,
|
||||||
|
pub(crate) source: Option<Source>,
|
||||||
|
// short instance description
|
||||||
|
pub(crate) summary: Option<String>,
|
||||||
|
pub(crate) media_type: Option<MediaTypeHtml>,
|
||||||
|
/// instance icon
|
||||||
|
pub(crate) icon: Option<ImageObject>,
|
||||||
|
/// instance banner
|
||||||
|
pub(crate) image: Option<ImageObject>,
|
||||||
|
pub(crate) inbox: Url,
|
||||||
|
/// mandatory field in activitypub, currently empty in lemmy
|
||||||
|
pub(crate) outbox: Url,
|
||||||
|
pub(crate) public_key: PublicKey,
|
||||||
|
pub(crate) published: DateTime<FixedOffset>,
|
||||||
|
pub(crate) updated: Option<DateTime<FixedOffset>>,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub(crate) unparsed: Unparsed,
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ use url::Url;
|
||||||
|
|
||||||
pub(crate) mod chat_message;
|
pub(crate) mod chat_message;
|
||||||
pub(crate) mod group;
|
pub(crate) mod group;
|
||||||
|
pub(crate) mod instance;
|
||||||
pub(crate) mod note;
|
pub(crate) mod note;
|
||||||
pub(crate) mod page;
|
pub(crate) mod page;
|
||||||
pub(crate) mod person;
|
pub(crate) mod person;
|
||||||
|
@ -23,6 +24,7 @@ mod tests {
|
||||||
objects::{
|
objects::{
|
||||||
chat_message::ChatMessage,
|
chat_message::ChatMessage,
|
||||||
group::Group,
|
group::Group,
|
||||||
|
instance::Instance,
|
||||||
note::Note,
|
note::Note,
|
||||||
page::Page,
|
page::Page,
|
||||||
person::Person,
|
person::Person,
|
||||||
|
@ -33,9 +35,10 @@ mod tests {
|
||||||
};
|
};
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_parse_object_lemmy() {
|
async fn test_parse_objects_lemmy() {
|
||||||
test_parse_lemmy_item::<Person>("assets/lemmy/objects/person.json").unwrap();
|
test_parse_lemmy_item::<Instance>("assets/lemmy/objects/instance.json").unwrap();
|
||||||
test_parse_lemmy_item::<Group>("assets/lemmy/objects/group.json").unwrap();
|
test_parse_lemmy_item::<Group>("assets/lemmy/objects/group.json").unwrap();
|
||||||
|
test_parse_lemmy_item::<Person>("assets/lemmy/objects/person.json").unwrap();
|
||||||
test_parse_lemmy_item::<Page>("assets/lemmy/objects/page.json").unwrap();
|
test_parse_lemmy_item::<Page>("assets/lemmy/objects/page.json").unwrap();
|
||||||
test_parse_lemmy_item::<Note>("assets/lemmy/objects/note.json").unwrap();
|
test_parse_lemmy_item::<Note>("assets/lemmy/objects/note.json").unwrap();
|
||||||
test_parse_lemmy_item::<ChatMessage>("assets/lemmy/objects/chat_message.json").unwrap();
|
test_parse_lemmy_item::<ChatMessage>("assets/lemmy/objects/chat_message.json").unwrap();
|
||||||
|
@ -43,7 +46,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_parse_object_pleroma() {
|
async fn test_parse_objects_pleroma() {
|
||||||
file_to_json_object::<WithContext<Person>>("assets/pleroma/objects/person.json").unwrap();
|
file_to_json_object::<WithContext<Person>>("assets/pleroma/objects/person.json").unwrap();
|
||||||
file_to_json_object::<WithContext<Note>>("assets/pleroma/objects/note.json").unwrap();
|
file_to_json_object::<WithContext<Note>>("assets/pleroma/objects/note.json").unwrap();
|
||||||
file_to_json_object::<WithContext<ChatMessage>>("assets/pleroma/objects/chat_message.json")
|
file_to_json_object::<WithContext<ChatMessage>>("assets/pleroma/objects/chat_message.json")
|
||||||
|
@ -51,19 +54,19 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_parse_object_smithereen() {
|
async fn test_parse_objects_smithereen() {
|
||||||
file_to_json_object::<WithContext<Person>>("assets/smithereen/objects/person.json").unwrap();
|
file_to_json_object::<WithContext<Person>>("assets/smithereen/objects/person.json").unwrap();
|
||||||
file_to_json_object::<Note>("assets/smithereen/objects/note.json").unwrap();
|
file_to_json_object::<Note>("assets/smithereen/objects/note.json").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_parse_object_mastodon() {
|
async fn test_parse_objects_mastodon() {
|
||||||
file_to_json_object::<WithContext<Person>>("assets/mastodon/objects/person.json").unwrap();
|
file_to_json_object::<WithContext<Person>>("assets/mastodon/objects/person.json").unwrap();
|
||||||
file_to_json_object::<WithContext<Note>>("assets/mastodon/objects/note.json").unwrap();
|
file_to_json_object::<WithContext<Note>>("assets/mastodon/objects/note.json").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_parse_object_lotide() {
|
async fn test_parse_objects_lotide() {
|
||||||
file_to_json_object::<WithContext<Group>>("assets/lotide/objects/group.json").unwrap();
|
file_to_json_object::<WithContext<Group>>("assets/lotide/objects/group.json").unwrap();
|
||||||
file_to_json_object::<WithContext<Person>>("assets/lotide/objects/person.json").unwrap();
|
file_to_json_object::<WithContext<Person>>("assets/lotide/objects/person.json").unwrap();
|
||||||
file_to_json_object::<WithContext<Note>>("assets/lotide/objects/note.json").unwrap();
|
file_to_json_object::<WithContext<Note>>("assets/lotide/objects/note.json").unwrap();
|
||||||
|
|
|
@ -55,19 +55,7 @@ mod tests {
|
||||||
|
|
||||||
let site_form = SiteForm {
|
let site_form = SiteForm {
|
||||||
name: "test_site".into(),
|
name: "test_site".into(),
|
||||||
sidebar: None,
|
..Default::default()
|
||||||
description: None,
|
|
||||||
icon: None,
|
|
||||||
banner: None,
|
|
||||||
enable_downvotes: None,
|
|
||||||
open_registration: None,
|
|
||||||
enable_nsfw: None,
|
|
||||||
updated: None,
|
|
||||||
community_creation_admin_only: Some(false),
|
|
||||||
require_email_verification: None,
|
|
||||||
require_application: None,
|
|
||||||
application_question: None,
|
|
||||||
private_instance: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Site::create(&conn, &site_form).unwrap();
|
Site::create(&conn, &site_form).unwrap();
|
||||||
|
@ -136,7 +124,8 @@ mod tests {
|
||||||
let after_delete_creator = SiteAggregates::read(&conn);
|
let after_delete_creator = SiteAggregates::read(&conn);
|
||||||
assert!(after_delete_creator.is_ok());
|
assert!(after_delete_creator.is_ok());
|
||||||
|
|
||||||
Site::delete(&conn, 1).unwrap();
|
let site_id = after_delete_creator.unwrap().id;
|
||||||
|
Site::delete(&conn, site_id).unwrap();
|
||||||
let after_delete_site = SiteAggregates::read(&conn);
|
let after_delete_site = SiteAggregates::read(&conn);
|
||||||
assert!(after_delete_site.is_err());
|
assert!(after_delete_site.is_err());
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,9 +122,9 @@ impl Community {
|
||||||
.get_result::<Self>(conn)
|
.get_result::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn distinct_federated_communities(conn: &PgConnection) -> Result<Vec<String>, Error> {
|
pub fn distinct_federated_communities(conn: &PgConnection) -> Result<Vec<DbUrl>, Error> {
|
||||||
use crate::schema::community::dsl::*;
|
use crate::schema::community::dsl::*;
|
||||||
community.select(actor_id).distinct().load::<String>(conn)
|
community.select(actor_id).distinct().load::<DbUrl>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn upsert(conn: &PgConnection, community_form: &CommunityForm) -> Result<Community, Error> {
|
pub fn upsert(conn: &PgConnection, community_form: &CommunityForm) -> Result<Community, Error> {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::{source::site::*, traits::Crud};
|
use crate::{source::site::*, traits::Crud, DbUrl};
|
||||||
use diesel::{dsl::*, result::Error, *};
|
use diesel::{dsl::*, result::Error, *};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
impl Crud for Site {
|
impl Crud for Site {
|
||||||
type Form = SiteForm;
|
type Form = SiteForm;
|
||||||
|
@ -27,8 +28,35 @@ impl Crud for Site {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Site {
|
impl Site {
|
||||||
pub fn read_simple(conn: &PgConnection) -> Result<Self, Error> {
|
pub fn read_local_site(conn: &PgConnection) -> Result<Self, Error> {
|
||||||
use crate::schema::site::dsl::*;
|
use crate::schema::site::dsl::*;
|
||||||
site.first::<Self>(conn)
|
site.order_by(id).first::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn upsert(conn: &PgConnection, site_form: &SiteForm) -> Result<Site, Error> {
|
||||||
|
use crate::schema::site::dsl::*;
|
||||||
|
insert_into(site)
|
||||||
|
.values(site_form)
|
||||||
|
.on_conflict(actor_id)
|
||||||
|
.do_update()
|
||||||
|
.set(site_form)
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_from_apub_id(conn: &PgConnection, object_id: Url) -> Result<Option<Self>, Error> {
|
||||||
|
use crate::schema::site::dsl::*;
|
||||||
|
let object_id: DbUrl = object_id.into();
|
||||||
|
Ok(
|
||||||
|
site
|
||||||
|
.filter(actor_id.eq(object_id))
|
||||||
|
.first::<Site>(conn)
|
||||||
|
.ok()
|
||||||
|
.map(Into::into),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_remote_sites(conn: &PgConnection) -> Result<Vec<Self>, Error> {
|
||||||
|
use crate::schema::site::dsl::*;
|
||||||
|
site.order_by(id).offset(1).get_results::<Self>(conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ use std::{
|
||||||
fmt,
|
fmt,
|
||||||
fmt::{Display, Formatter},
|
fmt::{Display, Formatter},
|
||||||
io::Write,
|
io::Write,
|
||||||
|
ops::Deref,
|
||||||
};
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
@ -125,3 +126,11 @@ where
|
||||||
DbUrl(id.into())
|
DbUrl(id.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Deref for DbUrl {
|
||||||
|
type Target = Url;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -454,6 +454,11 @@ table! {
|
||||||
require_application -> Bool,
|
require_application -> Bool,
|
||||||
application_question -> Nullable<Text>,
|
application_question -> Nullable<Text>,
|
||||||
private_instance -> Bool,
|
private_instance -> Bool,
|
||||||
|
actor_id -> Text,
|
||||||
|
last_refreshed_at -> Timestamp,
|
||||||
|
inbox_url -> Text,
|
||||||
|
private_key -> Nullable<Text>,
|
||||||
|
public_key -> Text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,11 @@ pub struct Site {
|
||||||
pub require_application: bool,
|
pub require_application: bool,
|
||||||
pub application_question: Option<String>,
|
pub application_question: Option<String>,
|
||||||
pub private_instance: bool,
|
pub private_instance: bool,
|
||||||
|
pub actor_id: DbUrl,
|
||||||
|
pub last_refreshed_at: chrono::NaiveDateTime,
|
||||||
|
pub inbox_url: DbUrl,
|
||||||
|
pub private_key: Option<String>,
|
||||||
|
pub public_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable, AsChangeset, Default)]
|
#[derive(Insertable, AsChangeset, Default)]
|
||||||
|
@ -40,4 +45,9 @@ pub struct SiteForm {
|
||||||
pub require_application: Option<bool>,
|
pub require_application: Option<bool>,
|
||||||
pub application_question: Option<Option<String>>,
|
pub application_question: Option<Option<String>>,
|
||||||
pub private_instance: Option<bool>,
|
pub private_instance: Option<bool>,
|
||||||
|
pub actor_id: Option<DbUrl>,
|
||||||
|
pub last_refreshed_at: Option<chrono::NaiveDateTime>,
|
||||||
|
pub inbox_url: Option<DbUrl>,
|
||||||
|
pub private_key: Option<Option<String>>,
|
||||||
|
pub public_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,11 +14,12 @@ pub struct SiteView {
|
||||||
|
|
||||||
impl SiteView {
|
impl SiteView {
|
||||||
pub fn read(conn: &PgConnection) -> Result<Self, Error> {
|
pub fn read(conn: &PgConnection) -> Result<Self, Error> {
|
||||||
let (site, counts) = site::table
|
let (mut site, counts) = site::table
|
||||||
.inner_join(site_aggregates::table)
|
.inner_join(site_aggregates::table)
|
||||||
.select((site::all_columns, site_aggregates::all_columns))
|
.select((site::all_columns, site_aggregates::all_columns))
|
||||||
.first::<(Site, SiteAggregates)>(conn)?;
|
.first::<(Site, SiteAggregates)>(conn)?;
|
||||||
|
|
||||||
|
site.private_key = None;
|
||||||
Ok(SiteView { site, counts })
|
Ok(SiteView { site, counts })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
6
migrations/2022-01-28-104106_instance-actor/down.sql
Normal file
6
migrations/2022-01-28-104106_instance-actor/down.sql
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
alter table site
|
||||||
|
drop column actor_id,
|
||||||
|
drop column last_refreshed_at,
|
||||||
|
drop column inbox_url,
|
||||||
|
drop column private_key,
|
||||||
|
drop column public_key;
|
6
migrations/2022-01-28-104106_instance-actor/up.sql
Normal file
6
migrations/2022-01-28-104106_instance-actor/up.sql
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
alter table site
|
||||||
|
add column actor_id varchar(255) not null unique default generate_unique_changeme(),
|
||||||
|
add column last_refreshed_at Timestamp not null default now(),
|
||||||
|
add column inbox_url varchar(255) not null default generate_unique_changeme(),
|
||||||
|
add column private_key text,
|
||||||
|
add column public_key text not null default generate_unique_changeme();
|
|
@ -8,6 +8,7 @@ use lemmy_apub::{
|
||||||
generate_inbox_url,
|
generate_inbox_url,
|
||||||
generate_local_apub_endpoint,
|
generate_local_apub_endpoint,
|
||||||
generate_shared_inbox_url,
|
generate_shared_inbox_url,
|
||||||
|
generate_site_inbox_url,
|
||||||
EndpointType,
|
EndpointType,
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
|
@ -18,11 +19,13 @@ use lemmy_db_schema::{
|
||||||
person::{Person, PersonForm},
|
person::{Person, PersonForm},
|
||||||
post::Post,
|
post::Post,
|
||||||
private_message::PrivateMessage,
|
private_message::PrivateMessage,
|
||||||
|
site::{Site, SiteForm},
|
||||||
},
|
},
|
||||||
traits::Crud,
|
traits::Crud,
|
||||||
};
|
};
|
||||||
use lemmy_utils::{apub::generate_actor_keypair, LemmyError};
|
use lemmy_utils::{apub::generate_actor_keypair, LemmyError};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
pub fn run_advanced_migrations(
|
pub fn run_advanced_migrations(
|
||||||
conn: &PgConnection,
|
conn: &PgConnection,
|
||||||
|
@ -35,6 +38,7 @@ pub fn run_advanced_migrations(
|
||||||
private_message_updates_2020_05_05(conn, protocol_and_hostname)?;
|
private_message_updates_2020_05_05(conn, protocol_and_hostname)?;
|
||||||
post_thumbnail_url_updates_2020_07_27(conn, protocol_and_hostname)?;
|
post_thumbnail_url_updates_2020_07_27(conn, protocol_and_hostname)?;
|
||||||
apub_columns_2021_02_02(conn)?;
|
apub_columns_2021_02_02(conn)?;
|
||||||
|
instance_actor_2022_01_28(conn, protocol_and_hostname)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -284,3 +288,29 @@ fn apub_columns_2021_02_02(conn: &PgConnection) -> Result<(), LemmyError> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Site object turns into an actor, so that things like instance description can be federated. This
|
||||||
|
/// means we need to add actor columns to the site table, and initialize them with correct values.
|
||||||
|
/// Before this point, there is only a single value in the site table which refers to the local
|
||||||
|
/// Lemmy instance, so thats all we need to update.
|
||||||
|
fn instance_actor_2022_01_28(
|
||||||
|
conn: &PgConnection,
|
||||||
|
protocol_and_hostname: &str,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
info!("Running instance_actor_2021_09_29");
|
||||||
|
if let Ok(site) = Site::read_local_site(conn) {
|
||||||
|
let key_pair = generate_actor_keypair()?;
|
||||||
|
let actor_id = Url::parse(protocol_and_hostname)?;
|
||||||
|
let site_form = SiteForm {
|
||||||
|
name: site.name,
|
||||||
|
actor_id: Some(actor_id.clone().into()),
|
||||||
|
last_refreshed_at: Some(naive_now()),
|
||||||
|
inbox_url: Some(generate_site_inbox_url(&actor_id.into())?),
|
||||||
|
private_key: Some(Some(key_pair.private_key)),
|
||||||
|
public_key: Some(key_pair.public_key),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
Site::update(conn, site.id, &site_form)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue