reportPost,
listPostReports,
randomString,
+ registerUser,
+ API,
+ getSite
} from './shared';
import { PostView, CommunityView } from 'lemmy-js-client';
});
test('Enforce site ban for federated user', async () => {
- let alphaShortname = `@lemmy_alpha@lemmy-alpha:8541`;
- let alphaPerson = (await resolvePerson(beta, alphaShortname)).person;
+ // create a test user
+ 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();
- // ban alpha from beta site
- let banAlpha = await banPersonFromSite(beta, alphaPerson.person.id, true);
+ // alpha makes post in beta community, it federates to beta instance
+ 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);
- // Alpha makes post on beta
- let postRes = await createPost(alpha, betaCommunity.community.id);
- expect(postRes.post_view.post).toBeDefined();
- expect(postRes.post_view.community.local).toBe(false);
- expect(postRes.post_view.creator.local).toBe(true);
- expect(postRes.post_view.counts.score).toBe(1);
+ // alpha ban should be federated to beta
+ let alphaUserOnBeta1 = await resolvePerson(beta, alphaUserActorId);
+ expect(alphaUserOnBeta1.person.person.banned).toBe(true);
- // Make sure that post doesn't make it to beta
- let searchBeta = await searchPostLocal(beta, postRes.post_view.post);
- let betaPost = searchBeta.posts[0];
- expect(betaPost).toBeUndefined();
+ // existing alpha post should be removed on beta
+ let searchBeta2 = await searchPostLocal(beta, postRes1.post_view.post);
+ expect(searchBeta2.posts[0]).toBeUndefined();
// 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);
+
+ // 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 () => {
let alphaPerson = (await resolvePerson(beta, alphaShortname)).person;
expect(alphaPerson).toBeDefined();
- // ban alpha from beta site
- await banPersonFromCommunity(beta, alphaPerson.person.id, 2, false);
- let banAlpha = await banPersonFromCommunity(beta, alphaPerson.person.id, 2, true);
+ // make a post in beta, it goes through
+ let postRes1 = await createPost(alpha, betaCommunity.community.id);
+ 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);
+ // 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
- let postRes = await createPost(alpha, betaCommunity.community.id);
- expect(postRes.post_view).toBeUndefined();
+ let postRes2 = await createPost(alpha, betaCommunity.community.id);
+ expect(postRes2.post_view).toBeUndefined();
// Unban alpha
let unBanAlpha = await banPersonFromCommunity(
beta,
alphaPerson.person.id,
2,
+ false,
false
);
expect(unBanAlpha.banned).toBe(false);
- let postRes2 = await createPost(alpha, betaCommunity.community.id);
- expect(postRes2.post_view.post).toBeDefined();
- expect(postRes2.post_view.community.local).toBe(false);
- expect(postRes2.post_view.creator.local).toBe(true);
- expect(postRes2.post_view.counts.score).toBe(1);
+ let postRes3 = await createPost(alpha, betaCommunity.community.id);
+ expect(postRes3.post_view.post).toBeDefined();
+ expect(postRes3.post_view.community.local).toBe(false);
+ expect(postRes3.post_view.creator.local).toBe(true);
+ expect(postRes3.post_view.counts.score).toBe(1);
// Make sure that post makes it to beta community
- let searchBeta = await searchPostLocal(beta, postRes2.post_view.post);
- let betaPost = searchBeta.posts[0];
- expect(betaPost).toBeDefined();
+ let searchBeta2 = await searchPostLocal(beta, postRes3.post_view.post);
+ expect(searchBeta2.posts[0]).toBeDefined();
});
test('Report a post', async () => {
}
export let alpha: API = {
- client: new LemmyHttp('http://localhost:8541'),
+ client: new LemmyHttp('http://127.0.0.1:8541'),
};
export let beta: API = {
- client: new LemmyHttp('http://localhost:8551'),
+ client: new LemmyHttp('http://127.0.0.1:8551'),
};
export let gamma: API = {
- client: new LemmyHttp('http://localhost:8561'),
+ client: new LemmyHttp('http://127.0.0.1:8561'),
};
export let delta: API = {
- client: new LemmyHttp('http://localhost:8571'),
+ client: new LemmyHttp('http://127.0.0.1:8571'),
};
export let epsilon: API = {
- client: new LemmyHttp('http://localhost:8581'),
+ client: new LemmyHttp('http://127.0.0.1:8581'),
};
const password = 'lemmylemmy'
export async function banPersonFromSite(
api: API,
person_id: number,
- ban: boolean
+ ban: boolean,
+ remove_data: boolean,
): Promise<BanPersonResponse> {
// Make sure lemmy-beta/c/main is cached on lemmy_alpha
let form: BanPerson = {
person_id,
ban,
- remove_data: false,
+ remove_data,
auth: api.auth,
};
return api.client.banPerson(form);
api: API,
person_id: number,
community_id: number,
+ remove_data: boolean,
ban: boolean
): Promise<BanFromCommunityResponse> {
- // Make sure lemmy-beta/c/main is cached on lemmy_alpha
let form: BanFromCommunity = {
person_id,
community_id,
- remove_data: false,
+ remove_data,
ban,
auth: api.auth,
};
community::*,
get_local_user_view_from_jwt,
is_mod_or_admin,
+ remove_user_data_in_community,
};
use lemmy_apub::{
+ activities::block::SiteOrCommunity,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::activities::{
- community::{
- add_mod::AddMod,
- block_user::BlockUserFromCommunity,
- remove_mod::RemoveMod,
- undo_block_user::UndoBlockUserFromCommunity,
- },
+ block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
+ community::{add_mod::AddMod, remove_mod::RemoveMod},
following::{follow::FollowCommunity as FollowCommunityApub, undo_follow::UndoFollowCommunity},
},
};
use lemmy_db_schema::{
source::{
- comment::Comment,
community::{
Community,
CommunityFollower,
ModTransferCommunityForm,
},
person::Person,
- post::Post,
},
traits::{Bannable, Blockable, Crud, Followable, Joinable},
};
-use lemmy_db_views::comment_view::CommentQueryBuilder;
use lemmy_db_views_actor::{
community_moderator_view::CommunityModeratorView,
community_view::CommunityView,
let community_id = data.community_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);
// Verify that only mods or admins can ban
.await?
.ok();
- BlockUserFromCommunity::send(
- &community,
+ BlockUser::send(
+ &SiteOrCommunity::Community(community),
&banned_person,
&local_user_view.person.clone().into(),
+ remove_data,
+ data.reason.clone(),
expires,
context,
)
.await?
.map_err(LemmyError::from)
.map_err(|e| e.with_message("community_user_already_banned"))?;
- UndoBlockUserFromCommunity::send(
- &community,
+ UndoBlockUser::send(
+ &SiteOrCommunity::Community(community),
&banned_person,
&local_user_view.person.clone().into(),
+ data.reason.clone(),
context,
)
.await?;
}
// Remove/Restore their data if that's desired
- if data.remove_data.unwrap_or(false) {
- // Posts
- 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??;
- }
+ if remove_data {
+ remove_user_data_in_community(community_id, banned_person_id, context.pool()).await?;
}
// Mod tables
is_admin,
password_length_check,
person::*,
+ remove_user_data,
send_email_verification_success,
send_password_reset_email,
send_verification_email,
};
+use lemmy_apub::{
+ activities::block::SiteOrCommunity,
+ protocol::activities::block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
+};
use lemmy_db_schema::{
diesel_option_overwrite,
diesel_option_overwrite_to_url,
naive_now,
source::{
comment::Comment,
- community::Community,
email_verification::EmailVerification,
local_user::{LocalUser, LocalUserForm},
moderator::*,
person::*,
person_block::{PersonBlock, PersonBlockForm},
person_mention::*,
- post::Post,
private_message::PrivateMessage,
site::*,
},
private_message_view::PrivateMessageView,
};
use lemmy_db_views_actor::{
- community_moderator_view::CommunityModeratorView,
person_mention_view::{PersonMentionQueryBuilder, PersonMentionView},
person_view::PersonViewSafe,
};
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 {
return Err(LemmyError::from_message("email_not_verified"));
}
// When the site requires email, make sure email is not Some(None). IE, an overwrite to a None value
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 {
return Err(LemmyError::from_message("email_required"));
}
let expires = data.expires.map(naive_from_unix);
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?
.map_err(LemmyError::from)
.map_err(|e| e.with_message("couldnt_update_user"))?;
// Remove their data if that's desired
- if data.remove_data.unwrap_or(false) {
- // Posts
- blocking(context.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(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??;
+ let remove_data = data.remove_data.unwrap_or(false);
+ if remove_data {
+ remove_user_data(person.id, context.pool()).await?;
}
// Mod tables
})
.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 {
person_view,
banned: data.ban,
is_admin(&local_user_view)?;
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??
.require_email_verification;
// Only let admins do this
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??
.require_email_verification;
use lemmy_db_schema::{
newtypes::{CommunityId, LocalUserId, PersonId, PostId},
source::{
+ comment::Comment,
community::Community,
email_verification::{EmailVerification, EmailVerificationForm},
password_reset_request::PasswordResetRequest,
traits::{ApubActor, Crud, Readable},
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::{
+ community_moderator_view::CommunityModeratorView,
community_person_ban_view::CommunityPersonBanView,
community_view::CommunityView,
};
LemmyError,
Sensitive,
};
-use url::Url;
pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
where
#[tracing::instrument(skip_all)]
pub async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), LemmyError> {
if score == -1 {
- let site = blocking(pool, Site::read_simple).await??;
+ let site = blocking(pool, Site::read_local_site).await??;
if !site.enable_downvotes {
return Err(LemmyError::from_message("downvotes_disabled"));
}
pool: &DbPool,
) -> Result<(), LemmyError> {
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
if let Ok(site) = site {
let mut linked = distinct_communities
.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>>()?;
if let Some(allowed) = allowed.as_ref() {
pool: &DbPool,
settings: &Settings,
) -> 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 site.private_instance && settings.federation.enabled {
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(())
+}
let local_user_view =
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() {
return Err(LemmyError::from_message(
"only_admins_can_create_communities",
site::*,
site_description_length_check,
};
+use lemmy_apub::generate_site_inbox_url;
use lemmy_db_schema::{
diesel_option_overwrite,
diesel_option_overwrite_to_url,
+ naive_now,
+ newtypes::DbUrl,
source::site::{Site, SiteForm},
traits::Crud,
};
use lemmy_db_views::site_view::SiteView;
use lemmy_utils::{
+ apub::generate_actor_keypair,
+ settings::structs::Settings,
utils::{check_slurs, check_slurs_opt},
ConnectionId,
LemmyError,
};
use lemmy_websocket::LemmyContext;
+use url::Url;
#[async_trait::async_trait(?Send)]
impl PerformCrud for CreateSite {
) -> Result<SiteResponse, LemmyError> {
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() {
return Err(LemmyError::from_message("site_already_exists"));
};
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 {
name: data.name.to_owned(),
sidebar,
open_registration: data.open_registration,
enable_nsfw: data.enable_nsfw,
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()
};
use lemmy_db_views::site_view::SiteView;
use lemmy_utils::{utils::check_slurs_opt, ConnectionId, LemmyError};
use lemmy_websocket::{messages::SendAllMessage, LemmyContext, UserOperationCrud};
+use std::default::Default;
#[async_trait::async_trait(?Send)]
impl PerformCrud for EditSite {
// Make sure user is an admin
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 description = diesel_option_overwrite(&data.description);
require_application: data.require_application,
application_question,
private_instance: data.private_instance,
+ ..SiteForm::default()
};
let update_site = blocking(context.pool(), move |conn| {
let (mut email_verification, mut require_application) = (false, false);
// 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 {
return Err(LemmyError::from_message("registration_closed"));
}
],
"target": "http://enterprise.lemmy.ml/c/main",
"type": "Block",
+ "remove_data": "true",
+ "summary": "spam post",
"expires": "2021-11-01T12:23:50.151874+00:00",
"id": "http://enterprise.lemmy.ml/activities/block/5d42fffb-0903-4625-86d4-0b39bb344fc2"
}
],
"target": "http://enterprise.lemmy.ml/c/main",
"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"
},
"cc": [
--- /dev/null
+{
+ "@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"
+}
\ No newline at end of file
--- /dev/null
+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()),
+ }
+ }
+}
--- /dev/null
+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)
+}
--- /dev/null
+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
+ }
+}
+++ /dev/null
-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
- }
-}
pub mod add_mod;
pub mod announce;
-pub mod block_user;
pub mod remove_mod;
pub mod report;
-pub mod undo_block_user;
pub mod update;
#[tracing::instrument(skip_all)]
+++ /dev/null
-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
- }
-}
use url::{ParseError, Url};
use uuid::Uuid;
+pub mod block;
pub mod comment;
pub mod community;
pub mod deletion;
objects::community::ApubCommunity,
protocol::{
activities::{
+ block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
community::{
add_mod::AddMod,
announce::AnnounceActivity,
- block_user::BlockUserFromCommunity,
remove_mod::RemoveMod,
report::Report,
- undo_block_user::UndoBlockUserFromCommunity,
update::UpdateCommunity,
},
create_or_update::{comment::CreateOrUpdateComment, post::CreateOrUpdatePost},
Delete(Delete),
UndoDelete(UndoDelete),
UpdateCommunity(UpdateCommunity),
- BlockUserFromCommunity(BlockUserFromCommunity),
- UndoBlockUserFromCommunity(UndoBlockUserFromCommunity),
+ BlockUser(BlockUser),
+ UndoBlockUser(UndoBlockUser),
AddMod(AddMod),
RemoveMod(RemoveMod),
// For compatibility with Pleroma/Mastodon (send only)
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)]
impl GetCommunity for AnnouncableActivities {
#[tracing::instrument(skip(self, context))]
Delete(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?,
- BlockUserFromCommunity(a) => a.get_community(context, request_counter).await?,
- UndoBlockUserFromCommunity(a) => a.get_community(context, request_counter).await?,
+ BlockUser(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?,
RemoveMod(a) => a.get_community(context, request_counter).await?,
Page(_) => unimplemented!(),
source::{
community::Community,
person::{Person, PersonForm},
+ site::Site,
},
traits::Crud,
};
let client = reqwest::Client::new().into();
let manager = create_activity_queue(client);
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_id = community.id;
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 =
file_to_json_object("assets/lemmy/collections/group_moderators.json").unwrap();
community_context.0.id,
)
.unwrap();
+ Site::delete(&*community_context.1.pool().get().unwrap(), site.id).unwrap();
}
}
--- /dev/null
+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(())
+ }
+}
mod person;
mod post;
pub mod routes;
+pub mod site;
#[tracing::instrument(skip_all)]
pub async fn shared_inbox(
use crate::{
activity_lists::PersonInboxActivities,
context::WithContext,
+ generate_outbox_url,
http::{
create_apub_response,
create_apub_tombstone_response,
ActivityCommonFields,
},
objects::person::ApubPerson,
- protocol::collections::person_outbox::PersonOutbox,
+ protocol::collections::empty_outbox::EmptyOutbox,
};
use actix_web::{web, web::Payload, HttpRequest, HttpResponse};
use lemmy_api_common::blocking;
Person::read_from_name(conn, &info.user_name)
})
.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))
}
person::{get_apub_person_http, get_apub_person_outbox, person_inbox},
post::get_apub_post,
shared_inbox,
+ site::{get_apub_site_http, get_apub_site_inbox, get_apub_site_outbox},
};
use actix_web::{
guard::{Guard, GuardContext},
println!("federation enabled, host is {}", settings.hostname);
cfg
+ .route("/", web::get().to(get_apub_site_http))
+ .route("/site_outbox", web::get().to(get_apub_site_outbox))
.route(
"/c/{community_name}",
web::get().to(get_apub_community_http),
.guard(InboxRequestGuard)
.route("/c/{community_name}/inbox", web::post().to(community_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)),
);
}
}
--- /dev/null
+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
+}
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> {
let actor_id: Url = actor_id.clone().into();
let url = format!(
use lemmy_apub_lib::{
object_id::ObjectId,
traits::ApubObject,
- values::{MediaTypeHtml, MediaTypeMarkdown},
+ values::MediaTypeHtml,
verify::verify_domains_match,
};
use lemmy_db_schema::{
cc: maa.ccs,
content: markdown_to_html(&self.content),
media_type: Some(MediaTypeHtml::Html),
- source: SourceCompat::Lemmy(Source {
- content: self.content.clone(),
- media_type: MediaTypeMarkdown::Markdown,
- }),
+ source: SourceCompat::Lemmy(Source::new(self.content.clone())),
in_reply_to,
published: Some(convert_datetime(self.published)),
updated: self.updated.map(convert_datetime),
use super::*;
use crate::objects::{
community::{tests::parse_lemmy_community, ApubCommunity},
+ instance::ApubSite,
person::{tests::parse_lemmy_person, ApubPerson},
post::ApubPost,
tests::{file_to_json_object, init_context},
};
use assert_json_diff::assert_json_include;
use lemmy_apub_lib::activity_queue::create_activity_queue;
+ use lemmy_db_schema::source::site::Site;
use serial_test::serial;
async fn prepare_comment_test(
url: &Url,
context: &LemmyContext,
- ) -> (ApubPerson, ApubCommunity, ApubPost) {
- let person = parse_lemmy_person(context).await;
+ ) -> (ApubPerson, ApubCommunity, ApubPost, ApubSite) {
+ let (person, site) = parse_lemmy_person(context).await;
let community = parse_lemmy_community(context).await;
let post_json = file_to_json_object("assets/lemmy/objects/page.json").unwrap();
ApubPost::verify(&post_json, url, context, &mut 0)
let post = ApubPost::from_apub(post_json, context, &mut 0)
.await
.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();
Community::delete(&*context.pool().get().unwrap(), data.1.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]
collections::{community_moderators::ApubCommunityModerators, CommunityContext},
generate_moderators_url,
generate_outbox_url,
+ objects::instance::fetch_instance_actor_for_object,
protocol::{
objects::{group::Group, tombstone::Tombstone, Endpoints},
ImageObject,
use lemmy_apub_lib::{
object_id::ObjectId,
traits::{ActorType, ApubObject},
- values::MediaTypeMarkdown,
};
use lemmy_db_schema::{source::community::Community, traits::ApubActor};
use lemmy_db_views_actor::community_follower_view::CommunityFollowerView;
#[tracing::instrument(skip_all)]
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 {
kind: GroupType::Group,
id: ObjectId::new(self.actor_id()),
preferred_username: self.name.clone(),
name: self.title.clone(),
summary: self.description.as_ref().map(|b| markdown_to_html(b)),
- source,
- icon,
- image,
+ source: self.description.clone().map(Source::new),
+ icon: self.icon.clone().map(ImageObject::new),
+ image: self.banner.clone().map(ImageObject::new),
sensitive: Some(self.nsfw),
moderators: Some(ObjectId::<ApubCommunityModerators>::new(
generate_moderators_url(&self.actor_id)?,
.ok();
}
+ fetch_instance_actor_for_object(community.actor_id(), context, request_counter).await;
+
Ok(community)
}
}
#[cfg(test)]
pub(crate) mod tests {
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_db_schema::traits::Crud;
+ use lemmy_db_schema::{source::site::Site, traits::Crud};
use serial_test::serial;
pub(crate) async fn parse_lemmy_community(context: &LemmyContext) -> ApubCommunity {
let community = ApubCommunity::from_apub(json, context, &mut request_counter)
.await
.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);
community
}
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;
let community = parse_lemmy_community(&context).await;
assert_eq!(community.title, "Ten Forward");
assert_eq!(community.description.as_ref().unwrap().len(), 132);
Community::delete(&*context.pool().get().unwrap(), community.id).unwrap();
+ Site::delete(&*context.pool().get().unwrap(), site.id).unwrap();
}
}
--- /dev/null
+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();
+ }
+}
pub mod comment;
pub mod community;
+pub mod instance;
pub mod person;
pub mod post;
pub mod private_message;
use crate::{
check_is_apub_id_valid,
generate_outbox_url,
- objects::get_summary_from_string_or_source,
+ objects::{get_summary_from_string_or_source, instance::fetch_instance_actor_for_object},
protocol::{
objects::{
person::{Person, UserTypes},
use lemmy_apub_lib::{
object_id::ObjectId,
traits::{ActorType, ApubObject},
- values::MediaTypeMarkdown,
verify::verify_domains_match,
};
use lemmy_db_schema::{
} else {
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 {
kind,
preferred_username: self.name.clone(),
name: self.display_name.clone(),
summary: self.bio.as_ref().map(|b| markdown_to_html(b)),
- source,
- icon,
- image,
+ source: self.bio.clone().map(Source::new),
+ icon: self.avatar.clone().map(ImageObject::new),
+ image: self.banner.clone().map(ImageObject::new),
matrix_user_id: self.matrix_user_id.clone(),
published: Some(convert_datetime(self.published)),
outbox: generate_outbox_url(&self.actor_id)?.into(),
async fn from_apub(
person: Person,
context: &LemmyContext,
- _request_counter: &mut i32,
+ request_counter: &mut i32,
) -> Result<ApubPerson, LemmyError> {
let person_form = PersonForm {
name: person.preferred_username,
DbPerson::upsert(conn, &person_form)
})
.await??;
+
+ let actor_id = person.actor_id.clone().into();
+ fetch_instance_actor_for_object(actor_id, context, request_counter).await;
+
Ok(person.into())
}
}
#[cfg(test)]
pub(crate) mod tests {
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_db_schema::traits::Crud;
+ use lemmy_db_schema::{source::site::Site, traits::Crud};
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 url = Url::parse("https://enterprise.lemmy.ml/u/picard").unwrap();
let mut request_counter = 0;
.await
.unwrap();
assert_eq!(request_counter, 0);
- person
+ (person, site)
}
#[actix_rt::test]
let client = reqwest::Client::new().into();
let manager = create_activity_queue(client);
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!(!person.local);
assert_eq!(person.bio.as_ref().unwrap().len(), 39);
DbPerson::delete(&*context.pool().get().unwrap(), person.id).unwrap();
+ Site::delete(&*context.pool().get().unwrap(), site.id).unwrap();
}
#[actix_rt::test]
let client = reqwest::Client::new().into();
let manager = create_activity_queue(client);
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 url = Url::parse("https://queer.hacktivis.me/users/lanodan").unwrap();
let mut request_counter = 0;
assert_eq!(person.bio.as_ref().unwrap().len(), 873);
DbPerson::delete(&*context.pool().get().unwrap(), person.id).unwrap();
+ Site::delete(&*context.pool().get().unwrap(), site.id).unwrap();
}
}
tests::{file_to_json_object, init_context},
};
use lemmy_apub_lib::activity_queue::create_activity_queue;
+ use lemmy_db_schema::source::site::Site;
use serial_test::serial;
#[actix_rt::test]
let client = reqwest::Client::new().into();
let manager = create_activity_queue(client);
let context = init_context(manager.queue_handle().clone());
+ let (person, site) = parse_lemmy_person(&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 url = Url::parse("https://enterprise.lemmy.ml/post/55143").unwrap();
Post::delete(&*context.pool().get().unwrap(), post.id).unwrap();
Person::delete(&*context.pool().get().unwrap(), person.id).unwrap();
Community::delete(&*context.pool().get().unwrap(), community.id).unwrap();
+ Site::delete(&*context.pool().get().unwrap(), site.id).unwrap();
}
}
use lemmy_apub_lib::{
object_id::ObjectId,
traits::ApubObject,
- values::{MediaTypeHtml, MediaTypeMarkdown},
+ values::MediaTypeHtml,
verify::verify_domains_match,
};
use lemmy_db_schema::{
to: [ObjectId::new(recipient.actor_id)],
content: markdown_to_html(&self.content),
media_type: Some(MediaTypeHtml::Html),
- source: Some(Source {
- content: self.content.clone(),
- media_type: MediaTypeMarkdown::Markdown,
- }),
+ source: Some(Source::new(self.content.clone())),
published: Some(convert_datetime(self.published)),
updated: self.updated.map(convert_datetime),
unparsed: Default::default(),
-use crate::{
- objects::{community::ApubCommunity, person::ApubPerson},
- protocol::Unparsed,
-};
+use crate::{activities::block::SiteOrCommunity, objects::person::ApubPerson, protocol::Unparsed};
use activitystreams_kinds::activity::BlockType;
use chrono::{DateTime, FixedOffset};
use lemmy_apub_lib::object_id::ObjectId;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
-pub struct BlockUserFromCommunity {
+pub struct BlockUser {
pub(crate) actor: ObjectId<ApubPerson>,
#[serde(deserialize_with = "crate::deserialize_one_or_many")]
pub(crate) to: Vec<Url>,
pub(crate) object: ObjectId<ApubPerson>,
#[serde(deserialize_with = "crate::deserialize_one_or_many")]
pub(crate) cc: Vec<Url>,
- pub(crate) target: ObjectId<ApubCommunity>,
+ pub(crate) target: ObjectId<SiteOrCommunity>,
#[serde(rename = "type")]
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,
#[serde(flatten)]
pub(crate) unparsed: Unparsed,
--- /dev/null
+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();
+ }
+}
use crate::{
objects::person::ApubPerson,
- protocol::{activities::community::block_user::BlockUserFromCommunity, Unparsed},
+ protocol::{activities::block::block_user::BlockUser, Unparsed},
};
use activitystreams_kinds::activity::UndoType;
use lemmy_apub_lib::object_id::ObjectId;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
-pub struct UndoBlockUserFromCommunity {
+pub struct UndoBlockUser {
pub(crate) actor: ObjectId<ApubPerson>,
#[serde(deserialize_with = "crate::deserialize_one_or_many")]
pub(crate) to: Vec<Url>,
- pub(crate) object: BlockUserFromCommunity,
+ pub(crate) object: BlockUser,
#[serde(deserialize_with = "crate::deserialize_one_or_many")]
pub(crate) cc: Vec<Url>,
#[serde(rename = "type")]
pub mod add_mod;
pub mod announce;
-pub mod block_user;
pub mod remove_mod;
pub mod report;
-pub mod undo_block_user;
pub mod update;
#[cfg(test)]
activities::community::{
add_mod::AddMod,
announce::AnnounceActivity,
- block_user::BlockUserFromCommunity,
remove_mod::RemoveMod,
report::Report,
- undo_block_user::UndoBlockUserFromCommunity,
update::UpdateCommunity,
},
tests::test_parse_lemmy_item,
test_parse_lemmy_item::<RemoveMod>("assets/lemmy/activities/community/remove_mod.json")
.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>(
"assets/lemmy/activities/community/update_community.json",
)
use serde::{Deserialize, Serialize};
use strum_macros::Display;
+pub mod block;
pub mod community;
pub mod create_or_update;
pub mod deletion;
-use crate::generate_outbox_url;
use activitystreams_kinds::collection::OrderedCollectionType;
-use lemmy_db_schema::source::person::Person;
use lemmy_utils::LemmyError;
use serde::{Deserialize, Serialize};
use url::Url;
+/// Empty placeholder outbox used for Person, Instance, which dont implement a proper outbox yet.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
-pub(crate) struct PersonOutbox {
+pub(crate) struct EmptyOutbox {
r#type: OrderedCollectionType,
id: Url,
ordered_items: Vec<()>,
total_items: i32,
}
-impl PersonOutbox {
- pub(crate) async fn new(user: Person) -> Result<PersonOutbox, LemmyError> {
- Ok(PersonOutbox {
+impl EmptyOutbox {
+ pub(crate) async fn new(outbox_id: Url) -> Result<EmptyOutbox, LemmyError> {
+ Ok(EmptyOutbox {
r#type: OrderedCollectionType::OrderedCollection,
- id: generate_outbox_url(&user.actor_id)?.into(),
+ id: outbox_id,
ordered_items: vec![],
total_items: 0,
})
+pub(crate) mod empty_outbox;
pub(crate) mod group_followers;
pub(crate) mod group_moderators;
pub(crate) mod group_outbox;
-pub(crate) mod person_outbox;
#[cfg(test)]
mod tests {
use crate::protocol::{
collections::{
+ empty_outbox::EmptyOutbox,
group_followers::GroupFollowers,
group_moderators::GroupModerators,
group_outbox::GroupOutbox,
- person_outbox::PersonOutbox,
},
tests::test_parse_lemmy_item,
};
assert_eq!(outbox.ordered_items.len() as i32, outbox.total_items);
test_parse_lemmy_item::<GroupModerators>("assets/lemmy/collections/group_moderators.json")
.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();
}
}
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)]
#[serde(rename_all = "camelCase")]
pub struct ImageObject {
--- /dev/null
+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,
+}
pub(crate) mod chat_message;
pub(crate) mod group;
+pub(crate) mod instance;
pub(crate) mod note;
pub(crate) mod page;
pub(crate) mod person;
objects::{
chat_message::ChatMessage,
group::Group,
+ instance::Instance,
note::Note,
page::Page,
person::Person,
};
#[actix_rt::test]
- async fn test_parse_object_lemmy() {
- test_parse_lemmy_item::<Person>("assets/lemmy/objects/person.json").unwrap();
+ async fn test_parse_objects_lemmy() {
+ 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::<Person>("assets/lemmy/objects/person.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::<ChatMessage>("assets/lemmy/objects/chat_message.json").unwrap();
}
#[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<Note>>("assets/pleroma/objects/note.json").unwrap();
file_to_json_object::<WithContext<ChatMessage>>("assets/pleroma/objects/chat_message.json")
}
#[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::<Note>("assets/smithereen/objects/note.json").unwrap();
}
#[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<Note>>("assets/mastodon/objects/note.json").unwrap();
}
#[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<Person>>("assets/lotide/objects/person.json").unwrap();
file_to_json_object::<WithContext<Note>>("assets/lotide/objects/note.json").unwrap();
let site_form = SiteForm {
name: "test_site".into(),
- sidebar: None,
- 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,
+ ..Default::default()
};
Site::create(&conn, &site_form).unwrap();
let after_delete_creator = SiteAggregates::read(&conn);
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);
assert!(after_delete_site.is_err());
}
.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::*;
- 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> {
-use crate::{source::site::*, traits::Crud};
+use crate::{source::site::*, traits::Crud, DbUrl};
use diesel::{dsl::*, result::Error, *};
+use url::Url;
impl Crud for Site {
type Form = SiteForm;
}
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::*;
- 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)
}
}
fmt,
fmt::{Display, Formatter},
io::Write,
+ ops::Deref,
};
use url::Url;
DbUrl(id.into())
}
}
+
+impl Deref for DbUrl {
+ type Target = Url;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
require_application -> Bool,
application_question -> Nullable<Text>,
private_instance -> Bool,
+ actor_id -> Text,
+ last_refreshed_at -> Timestamp,
+ inbox_url -> Text,
+ private_key -> Nullable<Text>,
+ public_key -> Text,
}
}
pub require_application: bool,
pub application_question: Option<String>,
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)]
pub require_application: Option<bool>,
pub application_question: Option<Option<String>>,
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>,
}
impl SiteView {
pub fn read(conn: &PgConnection) -> Result<Self, Error> {
- let (site, counts) = site::table
+ let (mut site, counts) = site::table
.inner_join(site_aggregates::table)
.select((site::all_columns, site_aggregates::all_columns))
.first::<(Site, SiteAggregates)>(conn)?;
+ site.private_key = None;
Ok(SiteView { site, counts })
}
}
--- /dev/null
+alter table site
+ drop column actor_id,
+ drop column last_refreshed_at,
+ drop column inbox_url,
+ drop column private_key,
+ drop column public_key;
--- /dev/null
+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();
generate_inbox_url,
generate_local_apub_endpoint,
generate_shared_inbox_url,
+ generate_site_inbox_url,
EndpointType,
};
use lemmy_db_schema::{
person::{Person, PersonForm},
post::Post,
private_message::PrivateMessage,
+ site::{Site, SiteForm},
},
traits::Crud,
};
use lemmy_utils::{apub::generate_actor_keypair, LemmyError};
use tracing::info;
+use url::Url;
pub fn run_advanced_migrations(
conn: &PgConnection,
private_message_updates_2020_05_05(conn, protocol_and_hostname)?;
post_thumbnail_url_updates_2020_07_27(conn, protocol_and_hostname)?;
apub_columns_2021_02_02(conn)?;
+ instance_actor_2022_01_28(conn, protocol_and_hostname)?;
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(())
+}