Implement separate mod activities for feature, lock post (#2716)

* Implement separate mod activities for feature, lock post

Also includes collection for featured posts. Later we also need
to do the same for Comment.distinguished

* some changes

---------

Co-authored-by: Dessalines <dessalines@users.noreply.github.com>
This commit is contained in:
Nutomic 2023-02-18 23:50:28 +09:00 committed by GitHub
parent 8409e50f8c
commit 62663a9f2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1294 additions and 679 deletions

View file

@ -840,6 +840,10 @@ pub fn generate_outbox_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
Ok(Url::parse(&format!("{actor_id}/outbox"))?.into())
}
pub fn generate_featured_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
Ok(Url::parse(&format!("{actor_id}/featured"))?.into())
}
pub fn generate_moderators_url(community_id: &DbUrl) -> Result<DbUrl, LemmyError> {
Ok(Url::parse(&format!("{community_id}/moderators"))?.into())
}

View file

@ -0,0 +1,14 @@
{
"cc": [
"https://ds9.lemmy.ml/c/main"
],
"id": "https://ds9.lemmy.ml/activities/add/47d911f5-52c5-4659-b2fd-0e58c451a427",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"type": "Add",
"actor": "https://ds9.lemmy.ml/u/lemmy_alpha",
"object": "https://ds9.lemmy.ml/post/2",
"target": "https://ds9.lemmy.ml/c/main/featured",
"audience": "https://ds9.lemmy.ml/c/main"
}

View file

@ -0,0 +1,13 @@
{
"id": "http://lemmy-alpha:8541/activities/lock/cb48761d-9e8c-42ce-aacb-b4bbe6408db2",
"actor": "http://lemmy-alpha:8541/u/lemmy_alpha",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"object": "http://lemmy-alpha:8541/post/2",
"cc": [
"http://lemmy-alpha:8541/c/main"
],
"type": "Lock",
"audience": "http://lemmy-alpha:8541/c/main"
}

View file

@ -0,0 +1,14 @@
{
"cc": [
"https://ds9.lemmy.ml/c/main"
],
"id": "https://ds9.lemmy.ml/activities/add/47d911f5-52c5-4659-b2fd-0e58c451a427",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"type": "Remove",
"actor": "https://ds9.lemmy.ml/u/lemmy_alpha",
"object": "https://ds9.lemmy.ml/post/2",
"target": "https://ds9.lemmy.ml/c/main/featured",
"audience": "https://ds9.lemmy.ml/c/main"
}

View file

@ -0,0 +1,25 @@
{
"id": "http://lemmy-alpha:8541/activities/undo/d6066719-d277-4964-9190-4d6faffac286",
"actor": "http://lemmy-alpha:8541/u/lemmy_alpha",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"object": {
"actor": "http://lemmy-alpha:8541/u/lemmy_alpha",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"object": "http://lemmy-alpha:8541/post/2",
"cc": [
"http://lemmy-alpha:8541/c/main"
],
"type": "Lock",
"id": "http://lemmy-alpha:8541/activities/lock/08b6fd3e-9ef3-4358-a987-8bb641f3e2c3",
"audience": "http://lemmy-alpha:8541/c/main"
},
"cc": [
"http://lemmy-alpha:8541/c/main"
],
"type": "Undo",
"audience": "http://lemmy-alpha:8541/c/main"
}

View file

@ -0,0 +1,51 @@
{
"type": "OrderedCollection",
"id": "https://ds9.lemmy.ml/c/main/featured",
"totalItems": 2,
"orderedItems": [
{
"type": "Page",
"id": "https://ds9.lemmy.ml/post/2",
"attributedTo": "https://ds9.lemmy.ml/u/lemmy_alpha",
"to": [
"https://ds9.lemmy.ml/c/main",
"https://www.w3.org/ns/activitystreams#Public"
],
"name": "test 2",
"cc": [],
"mediaType": "text/html",
"attachment": [],
"commentsEnabled": true,
"sensitive": false,
"stickied": true,
"published": "2023-02-06T06:42:41.939437+00:00",
"language": {
"identifier": "de",
"name": "Deutsch"
},
"audience": "https://ds9.lemmy.ml/c/main"
},
{
"type": "Page",
"id": "https://ds9.lemmy.ml/post/1",
"attributedTo": "https://ds9.lemmy.ml/u/lemmy_alpha",
"to": [
"https://ds9.lemmy.ml/c/main",
"https://www.w3.org/ns/activitystreams#Public"
],
"name": "test 1",
"cc": [],
"mediaType": "text/html",
"attachment": [],
"commentsEnabled": true,
"sensitive": false,
"stickied": true,
"published": "2023-02-06T06:42:37.119567+00:00",
"language": {
"identifier": "de",
"name": "Deutsch"
},
"audience": "https://ds9.lemmy.ml/c/main"
}
]
}

View file

@ -21,6 +21,7 @@
"followers": "https://enterprise.lemmy.ml/c/tenforward/followers",
"moderators": "https://enterprise.lemmy.ml/c/tenforward/moderators",
"attributedTo": "https://enterprise.lemmy.ml/c/tenforward/moderators",
"featured": "https://enterprise.lemmy.ml/c/tenforward//featured",
"postingRestrictedToMods": false,
"endpoints": {
"sharedInbox": "https://enterprise.lemmy.ml/inbox"

View file

@ -0,0 +1,73 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
{
"ostatus": "http://ostatus.org#",
"atomUri": "ostatus:atomUri",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"sensitive": "as:sensitive",
"toot": "http://joinmastodon.org/ns#",
"votersCount": "toot:votersCount",
"Hashtag": "as:Hashtag"
}
],
"id": "https://mastodon.social/users/LemmyDev/collections/featured",
"type": "OrderedCollection",
"totalItems": 1,
"orderedItems": [
{
"id": "https://mastodon.social/users/LemmyDev/statuses/104246642906910728",
"type": "Note",
"summary": null,
"inReplyTo": null,
"published": "2020-05-28T14:52:14Z",
"url": "https://mastodon.social/@LemmyDev/104246642906910728",
"attributedTo": "https://mastodon.social/users/LemmyDev",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://mastodon.social/users/LemmyDev/followers"
],
"sensitive": false,
"atomUri": "https://mastodon.social/users/LemmyDev/statuses/104246642906910728",
"inReplyToAtomUri": null,
"conversation": "tag:mastodon.social,2020-05-28:objectId=175451535:objectType=Conversation",
"content": "<p>Inaugural Post for Lemmy, a decentralized, easily self-hostable <a href=\"https://mastodon.social/tags/reddit\" class=\"mention hashtag\" rel=\"tag\">#<span>reddit</span></a> / link aggregator alternative,intended to work in the <a href=\"https://mastodon.social/tags/fediverse\" class=\"mention hashtag\" rel=\"tag\">#<span>fediverse</span></a>: </p><p><a href=\"https://github.com/LemmyNet/lemmy/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"><span class=\"invisible\">https://</span><span class=\"\">github.com/LemmyNet/lemmy/</span><span class=\"invisible\"></span></a></p><p><a href=\"https://mastodon.social/tags/activitypub\" class=\"mention hashtag\" rel=\"tag\">#<span>activitypub</span></a></p>",
"contentMap": {
"en": "<p>Inaugural Post for Lemmy, a decentralized, easily self-hostable <a href=\"https://mastodon.social/tags/reddit\" class=\"mention hashtag\" rel=\"tag\">#<span>reddit</span></a> / link aggregator alternative, intended to work in the <a href=\"https://mastodon.social/tags/fediverse\" class=\"mention hashtag\" rel=\"tag\">#<span>fediverse</span></a>: </p><p><a href=\"https://github.com/LemmyNet/lemmy/\" target=\"_blank\" rel=\"nofollownoopener noreferrer\"><span class=\"invisible\">https://</span><span class=\"\">github.com/LemmyNet/lemmy/</span><span class=\"invisible\"></span></a></p><p><a href=\"https://mastodon.social/tags/activitypub\" class=\"mentionhashtag\" rel=\"tag\">#<span>activitypub</span></a></p>"
},
"attachment": [],
"tag": [
{
"type": "Hashtag",
"href": "https://mastodon.social/tags/reddit",
"name": "#reddit"
},
{
"type": "Hashtag",
"href": "https://mastodon.social/tags/fediverse",
"name": "#fediverse"
},
{
"type": "Hashtag",
"href": "https://mastodon.social/tags/activitypub",
"name": "#activitypub"
}
],
"replies": {
"id": "https://mastodon.social/users/LemmyDev/statuses/104246642906910728/replies",
"type": "Collection",
"first": {
"type": "CollectionPage",
"next": "https://mastodon.social/users/LemmyDev/statuses/104246642906910728/replies?min_id=104246644059085152&page=true",
"partOf": "https://mastodon.social/users/LemmyDev/statuses/104246642906910728/replies",
"items": [
"https://mastodon.social/users/LemmyDev/statuses/104246644059085152"
]
}
}
}
]
}

View file

@ -1,182 +0,0 @@
use crate::{
activities::{
community::send_activity_in_community,
generate_activity_id,
verify_add_remove_moderator_target,
verify_is_public,
verify_mod_action,
verify_person_in_community,
},
activity_lists::AnnouncableActivities,
local_instance,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{
activities::community::{add_mod::AddMod, remove_mod::RemoveMod},
InCommunity,
},
ActorType,
SendActivity,
};
use activitypub_federation::{
core::object_id::ObjectId,
data::Data,
traits::{ActivityHandler, Actor},
};
use activitystreams_kinds::{activity::AddType, public};
use lemmy_api_common::{
community::{AddModToCommunity, AddModToCommunityResponse},
context::LemmyContext,
utils::{generate_moderators_url, get_local_user_view_from_jwt},
};
use lemmy_db_schema::{
source::{
community::{Community, CommunityModerator, CommunityModeratorForm},
moderator::{ModAddCommunity, ModAddCommunityForm},
person::Person,
},
traits::{Crud, Joinable},
};
use lemmy_utils::error::LemmyError;
use url::Url;
impl AddMod {
#[tracing::instrument(skip_all)]
pub async fn send(
community: &ApubCommunity,
added_mod: &ApubPerson,
actor: &ApubPerson,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let id = generate_activity_id(
AddType::Add,
&context.settings().get_protocol_and_hostname(),
)?;
let add = AddMod {
actor: ObjectId::new(actor.actor_id()),
to: vec![public()],
object: ObjectId::new(added_mod.actor_id()),
target: generate_moderators_url(&community.actor_id)?.into(),
cc: vec![community.actor_id()],
kind: AddType::Add,
id: id.clone(),
audience: Some(ObjectId::new(community.actor_id())),
};
let activity = AnnouncableActivities::AddMod(add);
let inboxes = vec![added_mod.shared_inbox_or_inbox()];
send_activity_in_community(activity, actor, community, inboxes, true, context).await
}
}
#[async_trait::async_trait(?Send)]
impl ActivityHandler for AddMod {
type DataType = LemmyContext;
type Error = LemmyError;
fn id(&self) -> &Url {
&self.id
}
fn actor(&self) -> &Url {
self.actor.inner()
}
#[tracing::instrument(skip_all)]
async fn verify(
&self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_is_public(&self.to, &self.cc)?;
let community = self.community(context, request_counter).await?;
verify_person_in_community(&self.actor, &community, context, request_counter).await?;
verify_mod_action(
&self.actor,
self.object.inner(),
community.id,
context,
request_counter,
)
.await?;
verify_add_remove_moderator_target(&self.target, &community)?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(
self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let community = self.community(context, request_counter).await?;
let new_mod = self
.object
.dereference(context, local_instance(context).await, request_counter)
.await?;
// If we had to refetch the community while parsing the activity, then the new mod has already
// been added. Skip it here as it would result in a duplicate key error.
let new_mod_id = new_mod.id;
let moderated_communities =
CommunityModerator::get_person_moderated_communities(context.pool(), new_mod_id).await?;
if !moderated_communities.contains(&community.id) {
let form = CommunityModeratorForm {
community_id: community.id,
person_id: new_mod.id,
};
CommunityModerator::join(context.pool(), &form).await?;
// write mod log
let actor = self
.actor
.dereference(context, local_instance(context).await, request_counter)
.await?;
let form = ModAddCommunityForm {
mod_person_id: actor.id,
other_person_id: new_mod.id,
community_id: community.id,
removed: Some(false),
};
ModAddCommunity::create(context.pool(), &form).await?;
}
// TODO: send websocket notification about added mod
Ok(())
}
}
#[async_trait::async_trait(?Send)]
impl SendActivity for AddModToCommunity {
type Response = AddModToCommunityResponse;
async fn send_activity(
request: &Self,
_response: &Self::Response,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
let community: ApubCommunity = Community::read(context.pool(), request.community_id)
.await?
.into();
let updated_mod: ApubPerson = Person::read(context.pool(), request.person_id)
.await?
.into();
if request.added {
AddMod::send(
&community,
&updated_mod,
&local_user_view.person.into(),
context,
)
.await
} else {
RemoveMod::send(
&community,
&updated_mod,
&local_user_view.person.into(),
context,
)
.await
}
}
}

View file

@ -0,0 +1,256 @@
use crate::{
activities::{
community::send_activity_in_community,
generate_activity_id,
verify_is_public,
verify_mod_action,
verify_person_in_community,
},
activity_lists::AnnouncableActivities,
local_instance,
objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost},
protocol::{
activities::{
community::{collection_add::CollectionAdd, collection_remove::CollectionRemove},
create_or_update::page::CreateOrUpdatePage,
CreateOrUpdateType,
},
InCommunity,
},
ActorType,
SendActivity,
};
use activitypub_federation::{
core::object_id::ObjectId,
data::Data,
traits::{ActivityHandler, Actor},
};
use activitystreams_kinds::{activity::AddType, public};
use lemmy_api_common::{
community::{AddModToCommunity, AddModToCommunityResponse},
context::LemmyContext,
post::{FeaturePost, PostResponse},
utils::{generate_featured_url, generate_moderators_url, get_local_user_view_from_jwt},
};
use lemmy_db_schema::{
impls::community::CollectionType,
source::{
community::{Community, CommunityModerator, CommunityModeratorForm},
moderator::{ModAddCommunity, ModAddCommunityForm},
person::Person,
post::{Post, PostUpdateForm},
},
traits::{Crud, Joinable},
};
use lemmy_utils::error::LemmyError;
use url::Url;
impl CollectionAdd {
#[tracing::instrument(skip_all)]
pub async fn send_add_mod(
community: &ApubCommunity,
added_mod: &ApubPerson,
actor: &ApubPerson,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let id = generate_activity_id(
AddType::Add,
&context.settings().get_protocol_and_hostname(),
)?;
let add = CollectionAdd {
actor: ObjectId::new(actor.actor_id()),
to: vec![public()],
object: added_mod.actor_id(),
target: generate_moderators_url(&community.actor_id)?.into(),
cc: vec![community.actor_id()],
kind: AddType::Add,
id: id.clone(),
audience: Some(ObjectId::new(community.actor_id())),
};
let activity = AnnouncableActivities::CollectionAdd(add);
let inboxes = vec![added_mod.shared_inbox_or_inbox()];
send_activity_in_community(activity, actor, community, inboxes, true, context).await
}
pub async fn send_add_featured_post(
community: &ApubCommunity,
featured_post: &ApubPost,
actor: &ApubPerson,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let id = generate_activity_id(
AddType::Add,
&context.settings().get_protocol_and_hostname(),
)?;
let add = CollectionAdd {
actor: ObjectId::new(actor.actor_id()),
to: vec![public()],
object: featured_post.ap_id.clone().into(),
target: generate_featured_url(&community.actor_id)?.into(),
cc: vec![community.actor_id()],
kind: AddType::Add,
id: id.clone(),
audience: Some(ObjectId::new(community.actor_id())),
};
let activity = AnnouncableActivities::CollectionAdd(add);
send_activity_in_community(activity, actor, community, vec![], true, context).await
}
}
#[async_trait::async_trait(?Send)]
impl ActivityHandler for CollectionAdd {
type DataType = LemmyContext;
type Error = LemmyError;
fn id(&self) -> &Url {
&self.id
}
fn actor(&self) -> &Url {
self.actor.inner()
}
#[tracing::instrument(skip_all)]
async fn verify(
&self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_is_public(&self.to, &self.cc)?;
let community = self.community(context, request_counter).await?;
verify_person_in_community(&self.actor, &community, context, request_counter).await?;
verify_mod_action(
&self.actor,
&self.object,
community.id,
context,
request_counter,
)
.await?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(
self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let (community, collection_type) =
Community::get_by_collection_url(context.pool(), &self.target.into()).await?;
match collection_type {
CollectionType::Moderators => {
let new_mod = ObjectId::<ApubPerson>::new(self.object)
.dereference(context, local_instance(context).await, request_counter)
.await?;
// If we had to refetch the community while parsing the activity, then the new mod has already
// been added. Skip it here as it would result in a duplicate key error.
let new_mod_id = new_mod.id;
let moderated_communities =
CommunityModerator::get_person_moderated_communities(context.pool(), new_mod_id).await?;
if !moderated_communities.contains(&community.id) {
let form = CommunityModeratorForm {
community_id: community.id,
person_id: new_mod.id,
};
CommunityModerator::join(context.pool(), &form).await?;
// write mod log
let actor = self
.actor
.dereference(context, local_instance(context).await, request_counter)
.await?;
let form = ModAddCommunityForm {
mod_person_id: actor.id,
other_person_id: new_mod.id,
community_id: community.id,
removed: Some(false),
};
ModAddCommunity::create(context.pool(), &form).await?;
}
// TODO: send websocket notification about added mod
}
CollectionType::Featured => {
let post = ObjectId::<ApubPost>::new(self.object)
.dereference(context, local_instance(context).await, request_counter)
.await?;
let form = PostUpdateForm::builder()
.featured_community(Some(true))
.build();
Post::update(context.pool(), post.id, &form).await?;
}
}
Ok(())
}
}
#[async_trait::async_trait(?Send)]
impl SendActivity for AddModToCommunity {
type Response = AddModToCommunityResponse;
async fn send_activity(
request: &Self,
_response: &Self::Response,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
let community: ApubCommunity = Community::read(context.pool(), request.community_id)
.await?
.into();
let updated_mod: ApubPerson = Person::read(context.pool(), request.person_id)
.await?
.into();
if request.added {
CollectionAdd::send_add_mod(
&community,
&updated_mod,
&local_user_view.person.into(),
context,
)
.await
} else {
CollectionRemove::send_remove_mod(
&community,
&updated_mod,
&local_user_view.person.into(),
context,
)
.await
}
}
}
#[async_trait::async_trait(?Send)]
impl SendActivity for FeaturePost {
type Response = PostResponse;
async fn send_activity(
request: &Self,
response: &Self::Response,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
// Deprecated, for backwards compatibility with 0.17
CreateOrUpdatePage::send(
&response.post_view.post,
local_user_view.person.id,
CreateOrUpdateType::Update,
context,
)
.await?;
let community = Community::read(context.pool(), response.post_view.community.id)
.await?
.into();
let post = response.post_view.post.clone().into();
let person = local_user_view.person.into();
if request.featured {
CollectionAdd::send_add_featured_post(&community, &post, &person, context).await
} else {
CollectionRemove::send_remove_featured_post(&community, &post, &person, context).await
}
}
}

View file

@ -0,0 +1,171 @@
use crate::{
activities::{
community::send_activity_in_community,
generate_activity_id,
verify_is_public,
verify_mod_action,
verify_person_in_community,
},
activity_lists::AnnouncableActivities,
local_instance,
objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost},
protocol::{activities::community::collection_remove::CollectionRemove, InCommunity},
ActorType,
};
use activitypub_federation::{
core::object_id::ObjectId,
data::Data,
traits::{ActivityHandler, Actor},
};
use activitystreams_kinds::{activity::RemoveType, public};
use lemmy_api_common::{
context::LemmyContext,
utils::{generate_featured_url, generate_moderators_url},
};
use lemmy_db_schema::{
impls::community::CollectionType,
source::{
community::{Community, CommunityModerator, CommunityModeratorForm},
moderator::{ModAddCommunity, ModAddCommunityForm},
post::{Post, PostUpdateForm},
},
traits::{Crud, Joinable},
};
use lemmy_utils::error::LemmyError;
use url::Url;
impl CollectionRemove {
#[tracing::instrument(skip_all)]
pub async fn send_remove_mod(
community: &ApubCommunity,
removed_mod: &ApubPerson,
actor: &ApubPerson,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let id = generate_activity_id(
RemoveType::Remove,
&context.settings().get_protocol_and_hostname(),
)?;
let remove = CollectionRemove {
actor: ObjectId::new(actor.actor_id()),
to: vec![public()],
object: ObjectId::new(removed_mod.actor_id()),
target: generate_moderators_url(&community.actor_id)?.into(),
id: id.clone(),
cc: vec![community.actor_id()],
kind: RemoveType::Remove,
audience: Some(ObjectId::new(community.actor_id())),
};
let activity = AnnouncableActivities::CollectionRemove(remove);
let inboxes = vec![removed_mod.shared_inbox_or_inbox()];
send_activity_in_community(activity, actor, community, inboxes, true, context).await
}
pub async fn send_remove_featured_post(
community: &ApubCommunity,
featured_post: &ApubPost,
actor: &ApubPerson,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let id = generate_activity_id(
RemoveType::Remove,
&context.settings().get_protocol_and_hostname(),
)?;
let remove = CollectionRemove {
actor: ObjectId::new(actor.actor_id()),
to: vec![public()],
object: featured_post.ap_id.clone().into(),
target: generate_featured_url(&community.actor_id)?.into(),
cc: vec![community.actor_id()],
kind: RemoveType::Remove,
id: id.clone(),
audience: Some(ObjectId::new(community.actor_id())),
};
let activity = AnnouncableActivities::CollectionRemove(remove);
send_activity_in_community(activity, actor, community, vec![], true, context).await
}
}
#[async_trait::async_trait(?Send)]
impl ActivityHandler for CollectionRemove {
type DataType = LemmyContext;
type Error = LemmyError;
fn id(&self) -> &Url {
&self.id
}
fn actor(&self) -> &Url {
self.actor.inner()
}
#[tracing::instrument(skip_all)]
async fn verify(
&self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_is_public(&self.to, &self.cc)?;
let community = self.community(context, request_counter).await?;
verify_person_in_community(&self.actor, &community, context, request_counter).await?;
verify_mod_action(
&self.actor,
self.object.inner(),
community.id,
context,
request_counter,
)
.await?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(
self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let (community, collection_type) =
Community::get_by_collection_url(context.pool(), &self.target.into()).await?;
match collection_type {
CollectionType::Moderators => {
let remove_mod = self
.object
.dereference(context, local_instance(context).await, request_counter)
.await?;
let form = CommunityModeratorForm {
community_id: community.id,
person_id: remove_mod.id,
};
CommunityModerator::leave(context.pool(), &form).await?;
// write mod log
let actor = self
.actor
.dereference(context, local_instance(context).await, request_counter)
.await?;
let form = ModAddCommunityForm {
mod_person_id: actor.id,
other_person_id: remove_mod.id,
community_id: community.id,
removed: Some(true),
};
ModAddCommunity::create(context.pool(), &form).await?;
// TODO: send websocket notification about removed mod
}
CollectionType::Featured => {
let post = ObjectId::<ApubPost>::new(self.object)
.dereference(context, local_instance(context).await, request_counter)
.await?;
let form = PostUpdateForm::builder()
.featured_community(Some(false))
.build();
Post::update(context.pool(), post.id, &form).await?;
}
}
Ok(())
}
}

View file

@ -0,0 +1,200 @@
use crate::{
activities::{
check_community_deleted_or_removed,
community::send_activity_in_community,
generate_activity_id,
verify_is_public,
verify_mod_action,
verify_person_in_community,
},
activity_lists::AnnouncableActivities,
local_instance,
protocol::{
activities::{
community::lock_page::{LockPage, LockType, UndoLockPage},
create_or_update::page::CreateOrUpdatePage,
CreateOrUpdateType,
},
InCommunity,
},
SendActivity,
};
use activitypub_federation::{core::object_id::ObjectId, data::Data, traits::ActivityHandler};
use activitystreams_kinds::{activity::UndoType, public};
use lemmy_api_common::{
context::LemmyContext,
post::{LockPost, PostResponse},
utils::get_local_user_view_from_jwt,
};
use lemmy_db_schema::{
source::{
community::Community,
post::{Post, PostUpdateForm},
},
traits::Crud,
};
use lemmy_utils::error::LemmyError;
use url::Url;
#[async_trait::async_trait(?Send)]
impl ActivityHandler for LockPage {
type DataType = LemmyContext;
type Error = LemmyError;
fn id(&self) -> &Url {
&self.id
}
fn actor(&self) -> &Url {
self.actor.inner()
}
async fn verify(
&self,
context: &Data<Self::DataType>,
request_counter: &mut i32,
) -> Result<(), Self::Error> {
verify_is_public(&self.to, &self.cc)?;
let community = self.community(context, request_counter).await?;
verify_person_in_community(&self.actor, &community, context, request_counter).await?;
check_community_deleted_or_removed(&community)?;
verify_mod_action(
&self.actor,
self.object.inner(),
community.id,
context,
request_counter,
)
.await?;
Ok(())
}
async fn receive(
self,
context: &Data<Self::DataType>,
request_counter: &mut i32,
) -> Result<(), Self::Error> {
let form = PostUpdateForm::builder().locked(Some(true)).build();
let post = self
.object
.dereference(context, local_instance(context).await, request_counter)
.await?;
Post::update(context.pool(), post.id, &form).await?;
Ok(())
}
}
#[async_trait::async_trait(?Send)]
impl ActivityHandler for UndoLockPage {
type DataType = LemmyContext;
type Error = LemmyError;
fn id(&self) -> &Url {
&self.id
}
fn actor(&self) -> &Url {
self.actor.inner()
}
async fn verify(
&self,
context: &Data<Self::DataType>,
request_counter: &mut i32,
) -> Result<(), Self::Error> {
verify_is_public(&self.to, &self.cc)?;
let community = self.community(context, request_counter).await?;
verify_person_in_community(&self.actor, &community, context, request_counter).await?;
check_community_deleted_or_removed(&community)?;
verify_mod_action(
&self.actor,
self.object.object.inner(),
community.id,
context,
request_counter,
)
.await?;
Ok(())
}
async fn receive(
self,
context: &Data<Self::DataType>,
request_counter: &mut i32,
) -> Result<(), Self::Error> {
let form = PostUpdateForm::builder().locked(Some(false)).build();
let post = self
.object
.object
.dereference(context, local_instance(context).await, request_counter)
.await?;
Post::update(context.pool(), post.id, &form).await?;
Ok(())
}
}
#[async_trait::async_trait(?Send)]
impl SendActivity for LockPost {
type Response = PostResponse;
async fn send_activity(
request: &Self,
response: &Self::Response,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
// For backwards compat with 0.17
CreateOrUpdatePage::send(
&response.post_view.post,
local_user_view.person.id,
CreateOrUpdateType::Update,
context,
)
.await?;
let id = generate_activity_id(
LockType::Lock,
&context.settings().get_protocol_and_hostname(),
)?;
let community_id: Url = response.post_view.community.actor_id.clone().into();
let actor = ObjectId::new(local_user_view.person.actor_id.clone());
let lock = LockPage {
actor,
to: vec![public()],
object: ObjectId::new(response.post_view.post.ap_id.clone()),
cc: vec![community_id.clone()],
kind: LockType::Lock,
id,
audience: Some(ObjectId::new(community_id)),
};
let activity = if request.locked {
AnnouncableActivities::LockPost(lock)
} else {
let id = generate_activity_id(
UndoType::Undo,
&context.settings().get_protocol_and_hostname(),
)?;
let undo = UndoLockPage {
actor: lock.actor.clone(),
to: vec![public()],
cc: lock.cc.clone(),
kind: UndoType::Undo,
id,
audience: lock.audience.clone(),
object: lock,
};
AnnouncableActivities::UndoLockPost(undo)
};
let community = Community::read(context.pool(), response.post_view.community.id).await?;
send_activity_in_community(
activity,
&local_user_view.person.into(),
&community.into(),
vec![],
true,
context,
)
.await?;
Ok(())
}
}

View file

@ -1,19 +1,19 @@
use crate::{
activities::send_lemmy_activity,
activity_lists::AnnouncableActivities,
local_instance,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::activities::community::announce::AnnounceActivity,
};
use activitypub_federation::{core::object_id::ObjectId, traits::Actor};
use activitypub_federation::traits::Actor;
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::source::person::PersonFollower;
use lemmy_utils::error::LemmyError;
use url::Url;
pub mod add_mod;
pub mod announce;
pub mod remove_mod;
pub mod collection_add;
pub mod collection_remove;
pub mod lock_page;
pub mod report;
pub mod update;
@ -62,15 +62,3 @@ pub(crate) async fn send_activity_in_community(
Ok(())
}
#[tracing::instrument(skip_all)]
pub(crate) async fn get_community_from_moderators_url(
moderators: &Url,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let community_id = Url::parse(&moderators.to_string().replace("/moderators", ""))?;
ObjectId::new(community_id)
.dereference(context, local_instance(context).await, request_counter)
.await
}

View file

@ -1,130 +0,0 @@
use crate::{
activities::{
community::send_activity_in_community,
generate_activity_id,
verify_add_remove_moderator_target,
verify_is_public,
verify_mod_action,
verify_person_in_community,
},
activity_lists::AnnouncableActivities,
local_instance,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::community::remove_mod::RemoveMod, InCommunity},
ActorType,
};
use activitypub_federation::{
core::object_id::ObjectId,
data::Data,
traits::{ActivityHandler, Actor},
};
use activitystreams_kinds::{activity::RemoveType, public};
use lemmy_api_common::{context::LemmyContext, utils::generate_moderators_url};
use lemmy_db_schema::{
source::{
community::{CommunityModerator, CommunityModeratorForm},
moderator::{ModAddCommunity, ModAddCommunityForm},
},
traits::{Crud, Joinable},
};
use lemmy_utils::error::LemmyError;
use url::Url;
impl RemoveMod {
#[tracing::instrument(skip_all)]
pub async fn send(
community: &ApubCommunity,
removed_mod: &ApubPerson,
actor: &ApubPerson,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let id = generate_activity_id(
RemoveType::Remove,
&context.settings().get_protocol_and_hostname(),
)?;
let remove = RemoveMod {
actor: ObjectId::new(actor.actor_id()),
to: vec![public()],
object: ObjectId::new(removed_mod.actor_id()),
target: generate_moderators_url(&community.actor_id)?.into(),
id: id.clone(),
cc: vec![community.actor_id()],
kind: RemoveType::Remove,
audience: Some(ObjectId::new(community.actor_id())),
};
let activity = AnnouncableActivities::RemoveMod(remove);
let inboxes = vec![removed_mod.shared_inbox_or_inbox()];
send_activity_in_community(activity, actor, community, inboxes, true, context).await
}
}
#[async_trait::async_trait(?Send)]
impl ActivityHandler for RemoveMod {
type DataType = LemmyContext;
type Error = LemmyError;
fn id(&self) -> &Url {
&self.id
}
fn actor(&self) -> &Url {
self.actor.inner()
}
#[tracing::instrument(skip_all)]
async fn verify(
&self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_is_public(&self.to, &self.cc)?;
let community = self.community(context, request_counter).await?;
verify_person_in_community(&self.actor, &community, context, request_counter).await?;
verify_mod_action(
&self.actor,
self.object.inner(),
community.id,
context,
request_counter,
)
.await?;
verify_add_remove_moderator_target(&self.target, &community)?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(
self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let community = self.community(context, request_counter).await?;
let remove_mod = self
.object
.dereference(context, local_instance(context).await, request_counter)
.await?;
let form = CommunityModeratorForm {
community_id: community.id,
person_id: remove_mod.id,
};
CommunityModerator::leave(context.pool(), &form).await?;
// write mod log
let actor = self
.actor
.dereference(context, local_instance(context).await, request_counter)
.await?;
let form = ModAddCommunityForm {
mod_person_id: actor.id,
other_person_id: remove_mod.id,
community_id: community.id,
removed: Some(true),
};
ModAddCommunity::create(context.pool(), &form).await?;
// TODO: send websocket notification about removed mod
Ok(())
}
}

View file

@ -25,8 +25,7 @@ use activitypub_federation::{
use activitystreams_kinds::public;
use lemmy_api_common::{
context::LemmyContext,
post::{CreatePost, EditPost, FeaturePost, LockPost, PostResponse},
utils::get_local_user_view_from_jwt,
post::{CreatePost, EditPost, PostResponse},
websocket::{send::send_post_ws_message, UserOperationCrud},
};
use lemmy_db_schema::{
@ -79,48 +78,6 @@ impl SendActivity for EditPost {
}
}
#[async_trait::async_trait(?Send)]
impl SendActivity for LockPost {
type Response = PostResponse;
async fn send_activity(
request: &Self,
response: &Self::Response,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
CreateOrUpdatePage::send(
&response.post_view.post,
local_user_view.person.id,
CreateOrUpdateType::Update,
context,
)
.await
}
}
#[async_trait::async_trait(?Send)]
impl SendActivity for FeaturePost {
type Response = PostResponse;
async fn send_activity(
request: &Self,
response: &Self::Response,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
CreateOrUpdatePage::send(
&response.post_view.post,
local_user_view.person.id,
CreateOrUpdateType::Update,
context,
)
.await
}
}
impl CreateOrUpdatePage {
pub(crate) async fn new(
post: ApubPost,
@ -145,7 +102,7 @@ impl CreateOrUpdatePage {
}
#[tracing::instrument(skip_all)]
async fn send(
pub(crate) async fn send(
post: &Post,
person_id: PersonId,
kind: CreateOrUpdateType,

View file

@ -76,7 +76,7 @@ impl SendActivity for DeletePost {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
let community = Community::read(context.pool(), response.post_view.community.id).await?;
let deletable = DeletableObjects::Post(Box::new(response.post_view.post.clone().into()));
let deletable = DeletableObjects::Post(response.post_view.post.clone().into());
send_apub_delete_in_community(
local_user_view.person,
community,
@ -101,7 +101,7 @@ impl SendActivity for RemovePost {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
let community = Community::read(context.pool(), response.post_view.community.id).await?;
let deletable = DeletableObjects::Post(Box::new(response.post_view.post.clone().into()));
let deletable = DeletableObjects::Post(response.post_view.post.clone().into());
send_apub_delete_in_community(
local_user_view.person,
community,
@ -126,8 +126,7 @@ impl SendActivity for DeleteComment {
let community_id = response.comment_view.community.id;
let community = Community::read(context.pool(), community_id).await?;
let person = Person::read(context.pool(), response.comment_view.creator.id).await?;
let deletable =
DeletableObjects::Comment(Box::new(response.comment_view.comment.clone().into()));
let deletable = DeletableObjects::Comment(response.comment_view.comment.clone().into());
send_apub_delete_in_community(person, community, deletable, None, request.deleted, context)
.await
}
@ -146,7 +145,7 @@ impl SendActivity for RemoveComment {
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
let comment = Comment::read(context.pool(), request.comment_id).await?;
let community = Community::read(context.pool(), response.comment_view.community.id).await?;
let deletable = DeletableObjects::Comment(Box::new(comment.into()));
let deletable = DeletableObjects::Comment(comment.into());
send_apub_delete_in_community(
local_user_view.person,
community,
@ -192,7 +191,7 @@ impl SendActivity for DeleteCommunity {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
let community = Community::read(context.pool(), request.community_id).await?;
let deletable = DeletableObjects::Community(Box::new(community.clone().into()));
let deletable = DeletableObjects::Community(community.clone().into());
send_apub_delete_in_community(
local_user_view.person,
community,
@ -217,7 +216,7 @@ impl SendActivity for RemoveCommunity {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
let community = Community::read(context.pool(), request.community_id).await?;
let deletable = DeletableObjects::Community(Box::new(community.clone().into()));
let deletable = DeletableObjects::Community(community.clone().into());
send_apub_delete_in_community(
local_user_view.person,
community,
@ -271,7 +270,7 @@ async fn send_apub_delete_private_message(
let recipient_id = pm.recipient_id;
let recipient: ApubPerson = Person::read(context.pool(), recipient_id).await?.into();
let deletable = DeletableObjects::PrivateMessage(Box::new(pm.into()));
let deletable = DeletableObjects::PrivateMessage(pm.into());
let inbox = vec![recipient.shared_inbox_or_inbox()];
if deleted {
let delete = Delete::new(actor, deletable, recipient.actor_id(), None, None, context)?;
@ -284,10 +283,10 @@ async fn send_apub_delete_private_message(
}
pub enum DeletableObjects {
Community(Box<ApubCommunity>),
Comment(Box<ApubComment>),
Post(Box<ApubPost>),
PrivateMessage(Box<ApubPrivateMessage>),
Community(ApubCommunity),
Comment(ApubComment),
Post(ApubPost),
PrivateMessage(ApubPrivateMessage),
}
impl DeletableObjects {
@ -297,16 +296,16 @@ impl DeletableObjects {
context: &LemmyContext,
) -> Result<DeletableObjects, LemmyError> {
if let Some(c) = ApubCommunity::read_from_apub_id(ap_id.clone(), context).await? {
return Ok(DeletableObjects::Community(Box::new(c)));
return Ok(DeletableObjects::Community(c));
}
if let Some(p) = ApubPost::read_from_apub_id(ap_id.clone(), context).await? {
return Ok(DeletableObjects::Post(Box::new(p)));
return Ok(DeletableObjects::Post(p));
}
if let Some(c) = ApubComment::read_from_apub_id(ap_id.clone(), context).await? {
return Ok(DeletableObjects::Comment(Box::new(c)));
return Ok(DeletableObjects::Comment(c));
}
if let Some(p) = ApubPrivateMessage::read_from_apub_id(ap_id.clone(), context).await? {
return Ok(DeletableObjects::PrivateMessage(Box::new(p)));
return Ok(DeletableObjects::PrivateMessage(p));
}
Err(diesel::NotFound.into())
}

View file

@ -12,7 +12,7 @@ use activitypub_federation::{
};
use activitystreams_kinds::public;
use anyhow::anyhow;
use lemmy_api_common::{context::LemmyContext, utils::generate_moderators_url};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
newtypes::CommunityId,
source::{community::Community, local_site::LocalSite},
@ -111,18 +111,6 @@ pub(crate) async fn verify_mod_action(
Err(LemmyError::from_message("Not a mod"))
}
/// For Add/Remove community moderator activities, check that the target field actually contains
/// /c/community/moderators. Any different values are unsupported.
fn verify_add_remove_moderator_target(
target: &Url,
community: &ApubCommunity,
) -> Result<(), LemmyError> {
if target != &generate_moderators_url(&community.actor_id)?.into() {
return Err(LemmyError::from_message("Unkown target url"));
}
Ok(())
}
pub(crate) fn verify_is_public(to: &[Url], cc: &[Url]) -> Result<(), LemmyError> {
if ![to, cc].iter().any(|set| set.contains(&public())) {
return Err(LemmyError::from_message("Object is not public"));
@ -130,11 +118,15 @@ pub(crate) fn verify_is_public(to: &[Url], cc: &[Url]) -> Result<(), LemmyError>
Ok(())
}
pub(crate) fn verify_community_matches(
a: &ApubCommunity,
b: CommunityId,
) -> Result<(), LemmyError> {
if a.id != b {
pub(crate) fn verify_community_matches<T>(
a: &ObjectId<ApubCommunity>,
b: T,
) -> Result<(), LemmyError>
where
T: Into<ObjectId<ApubCommunity>>,
{
let b: ObjectId<ApubCommunity> = b.into();
if a != &b {
return Err(LemmyError::from_message("Invalid community"));
}
Ok(())

View file

@ -4,9 +4,10 @@ use crate::{
activities::{
block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
community::{
add_mod::AddMod,
announce::{AnnounceActivity, RawAnnouncableActivities},
remove_mod::RemoveMod,
collection_add::CollectionAdd,
collection_remove::CollectionRemove,
lock_page::{LockPage, UndoLockPage},
report::Report,
update::UpdateCommunity,
},
@ -85,8 +86,10 @@ pub enum AnnouncableActivities {
UpdateCommunity(UpdateCommunity),
BlockUser(BlockUser),
UndoBlockUser(UndoBlockUser),
AddMod(AddMod),
RemoveMod(RemoveMod),
CollectionAdd(CollectionAdd),
CollectionRemove(CollectionRemove),
LockPost(LockPage),
UndoLockPost(UndoLockPage),
// For compatibility with Pleroma/Mastodon (send only)
Page(Page),
}
@ -120,8 +123,10 @@ impl InCommunity for AnnouncableActivities {
UpdateCommunity(a) => a.community(context, request_counter).await,
BlockUser(a) => a.community(context, request_counter).await,
UndoBlockUser(a) => a.community(context, request_counter).await,
AddMod(a) => a.community(context, request_counter).await,
RemoveMod(a) => a.community(context, request_counter).await,
CollectionAdd(a) => a.community(context, request_counter).await,
CollectionRemove(a) => a.community(context, request_counter).await,
LockPost(a) => a.community(context, request_counter).await,
UndoLockPost(a) => a.community(context, request_counter).await,
Page(_) => unimplemented!(),
}
}

View file

@ -0,0 +1,103 @@
use crate::{
collections::CommunityContext,
objects::post::ApubPost,
protocol::collections::group_featured::GroupFeatured,
};
use activitypub_federation::{
data::Data,
traits::{ActivityHandler, ApubObject},
utils::verify_domains_match,
};
use activitystreams_kinds::collection::OrderedCollectionType;
use futures::future::{join_all, try_join_all};
use lemmy_api_common::utils::generate_featured_url;
use lemmy_db_schema::{source::post::Post, utils::FETCH_LIMIT_MAX};
use lemmy_utils::error::LemmyError;
use url::Url;
#[derive(Clone, Debug)]
pub(crate) struct ApubCommunityFeatured(Vec<ApubPost>);
#[async_trait::async_trait(?Send)]
impl ApubObject for ApubCommunityFeatured {
type DataType = CommunityContext;
type ApubType = GroupFeatured;
type DbType = ();
type Error = LemmyError;
async fn read_from_apub_id(
_object_id: Url,
data: &Self::DataType,
) -> Result<Option<Self>, Self::Error>
where
Self: Sized,
{
// Only read from database if its a local community, otherwise fetch over http
if data.0.local {
let community_id = data.0.id;
let post_list: Vec<ApubPost> = Post::list_featured_for_community(data.1.pool(), community_id)
.await?
.into_iter()
.map(Into::into)
.collect();
Ok(Some(ApubCommunityFeatured(post_list)))
} else {
Ok(None)
}
}
async fn into_apub(self, data: &Self::DataType) -> Result<Self::ApubType, Self::Error> {
let ordered_items = try_join_all(self.0.into_iter().map(|p| p.into_apub(&data.1))).await?;
Ok(GroupFeatured {
r#type: OrderedCollectionType::OrderedCollection,
id: generate_featured_url(&data.0.actor_id)?.into(),
total_items: ordered_items.len() as i32,
ordered_items,
})
}
async fn verify(
apub: &Self::ApubType,
expected_domain: &Url,
_data: &Self::DataType,
_request_counter: &mut i32,
) -> Result<(), Self::Error> {
verify_domains_match(expected_domain, &apub.id)?;
Ok(())
}
async fn from_apub(
apub: Self::ApubType,
data: &Self::DataType,
_request_counter: &mut i32,
) -> Result<Self, Self::Error>
where
Self: Sized,
{
let mut posts = apub.ordered_items;
if posts.len() as i64 > FETCH_LIMIT_MAX {
posts = posts[0..(FETCH_LIMIT_MAX as usize)].to_vec();
}
// We intentionally ignore errors here. This is because the outbox might contain posts from old
// Lemmy versions, or from other software which we cant parse. In that case, we simply skip the
// item and only parse the ones that work.
let data = Data::new(data.1.clone());
// process items in parallel, to avoid long delay from fetch_site_metadata() and other processing
join_all(posts.into_iter().map(|post| {
async {
// use separate request counter for each item, otherwise there will be problems with
// parallel processing
let request_counter = &mut 0;
let verify = post.verify(&data, request_counter).await;
if verify.is_ok() {
post.receive(&data, request_counter).await.ok();
}
}
}))
.await;
// This return value is unused, so just set an empty vec
Ok(ApubCommunityFeatured(Vec::new()))
}
}

View file

@ -23,6 +23,7 @@ use lemmy_api_common::utils::generate_outbox_url;
use lemmy_db_schema::{
source::{person::Person, post::Post},
traits::Crud,
utils::FETCH_LIMIT_MAX,
};
use lemmy_utils::error::LemmyError;
use url::Url;
@ -35,6 +36,7 @@ impl ApubObject for ApubCommunityOutbox {
type DataType = CommunityContext;
type ApubType = GroupOutbox;
type Error = LemmyError;
type DbType = ();
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
None
@ -59,11 +61,6 @@ impl ApubObject for ApubCommunityOutbox {
}
}
async fn delete(self, _data: &Self::DataType) -> Result<(), LemmyError> {
// do nothing (it gets deleted automatically with the community)
Ok(())
}
#[tracing::instrument(skip_all)]
async fn into_apub(self, data: &Self::DataType) -> Result<Self::ApubType, LemmyError> {
let mut ordered_items = vec![];
@ -103,8 +100,8 @@ impl ApubObject for ApubCommunityOutbox {
_request_counter: &mut i32,
) -> Result<Self, LemmyError> {
let mut outbox_activities = apub.ordered_items;
if outbox_activities.len() > 20 {
outbox_activities = outbox_activities[0..20].to_vec();
if outbox_activities.len() as i64 > FETCH_LIMIT_MAX {
outbox_activities = outbox_activities[0..(FETCH_LIMIT_MAX as usize)].to_vec();
}
// We intentionally ignore errors here. This is because the outbox might contain posts from old
@ -128,6 +125,4 @@ impl ApubObject for ApubCommunityOutbox {
// This return value is unused, so just set an empty vec
Ok(ApubCommunityOutbox(Vec::new()))
}
type DbType = ();
}

View file

@ -1,6 +1,7 @@
use crate::objects::community::ApubCommunity;
use lemmy_api_common::context::LemmyContext;
pub(crate) mod community_featured;
pub(crate) mod community_moderators;
pub(crate) mod community_outbox;

View file

@ -1,88 +0,0 @@
use crate::fetcher::post_or_comment::PostOrComment;
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_api_common::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;
Community::update_deleted(context.pool(), 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;
Person::delete_account(context.pool(), id).await?;
Ok(())
}
}
#[async_trait::async_trait(?Send)]
impl DeletableApubObject for Post {
async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
let id = self.id;
Post::update_deleted(context.pool(), 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;
Comment::update_deleted(context.pool(), 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) => {
Comment::update_deleted(context.pool(), c.id, true)
.await?;
}
PostOrComment::Post(p) => {
Post::update_deleted(context.pool(), 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(())
}
}

View file

@ -18,8 +18,8 @@ use url::Url;
#[derive(Clone, Debug)]
pub enum PostOrComment {
Post(Box<ApubPost>),
Comment(Box<ApubComment>),
Post(ApubPost),
Comment(ApubComment),
}
#[derive(Deserialize)]
@ -40,7 +40,6 @@ impl ApubObject for PostOrComment {
None
}
// TODO: this can probably be implemented using a single sql query
#[tracing::instrument(skip_all)]
async fn read_from_apub_id(
object_id: Url,
@ -48,10 +47,10 @@ impl ApubObject for PostOrComment {
) -> Result<Option<Self>, LemmyError> {
let post = ApubPost::read_from_apub_id(object_id.clone(), data).await?;
Ok(match post {
Some(o) => Some(PostOrComment::Post(Box::new(o))),
Some(o) => Some(PostOrComment::Post(o)),
None => ApubComment::read_from_apub_id(object_id, data)
.await?
.map(|c| PostOrComment::Comment(Box::new(c))),
.map(PostOrComment::Comment),
})
}
@ -87,12 +86,12 @@ impl ApubObject for PostOrComment {
request_counter: &mut i32,
) -> Result<Self, LemmyError> {
Ok(match apub {
PageOrNote::Page(p) => PostOrComment::Post(Box::new(
ApubPost::from_apub(*p, context, request_counter).await?,
)),
PageOrNote::Note(n) => PostOrComment::Comment(Box::new(
ApubComment::from_apub(n, context, request_counter).await?,
)),
PageOrNote::Page(p) => {
PostOrComment::Post(ApubPost::from_apub(*p, context, request_counter).await?)
}
PageOrNote::Note(n) => {
PostOrComment::Comment(ApubComment::from_apub(n, context, request_counter).await?)
}
})
}
}

View file

@ -1,6 +1,7 @@
use crate::{
activity_lists::GroupInboxActivities,
collections::{
community_featured::ApubCommunityFeatured,
community_moderators::ApubCommunityModerators,
community_outbox::ApubCommunityOutbox,
CommunityContext,
@ -16,7 +17,10 @@ use activitypub_federation::{
traits::ApubObject,
};
use actix_web::{web, HttpRequest, HttpResponse};
use lemmy_api_common::{context::LemmyContext, utils::generate_outbox_url};
use lemmy_api_common::{
context::LemmyContext,
utils::{generate_featured_url, generate_outbox_url},
};
use lemmy_db_schema::{source::community::Community, traits::ApubActor};
use lemmy_utils::error::LemmyError;
use serde::Deserialize;
@ -106,3 +110,20 @@ pub(crate) async fn get_apub_community_moderators(
&moderators.into_apub(&outbox_data).await?,
))
}
/// Returns collection of featured (stickied) posts.
pub(crate) async fn get_apub_community_featured(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
let community = Community::read_from_name(context.pool(), &info.community_name, false).await?;
if community.deleted || community.removed {
return Err(LemmyError::from_message("deleted"));
}
let id = ObjectId::new(generate_featured_url(&community.actor_id)?);
let data = CommunityContext(community.into(), context.get_ref().clone());
let featured: ApubCommunityFeatured = id
.dereference(&data, local_instance(&context).await, &mut 0)
.await?;
Ok(create_apub_response(&featured.into_apub(&data).await?))
}

View file

@ -2,6 +2,7 @@ use crate::http::{
comment::get_apub_comment,
community::{
community_inbox,
get_apub_community_featured,
get_apub_community_followers,
get_apub_community_http,
get_apub_community_moderators,
@ -37,6 +38,10 @@ pub fn config(cfg: &mut web::ServiceConfig) {
"/c/{community_name}/outbox",
web::get().to(get_apub_community_outbox),
)
.route(
"/c/{community_name}/featured",
web::get().to(get_apub_community_featured),
)
.route(
"/c/{community_name}/moderators",
web::get().to(get_apub_community_moderators),

View file

@ -1,6 +1,6 @@
use crate::{
check_apub_id_valid_with_strictness,
collections::{community_moderators::ApubCommunityModerators, CommunityContext},
collections::CommunityContext,
fetch_local_site_data,
local_instance,
objects::instance::fetch_instance_actor_for_object,
@ -20,7 +20,7 @@ use chrono::NaiveDateTime;
use itertools::Itertools;
use lemmy_api_common::{
context::LemmyContext,
utils::{generate_moderators_url, generate_outbox_url},
utils::{generate_featured_url, generate_moderators_url, generate_outbox_url},
};
use lemmy_db_schema::{
source::{
@ -90,9 +90,6 @@ impl ApubObject for ApubCommunity {
let community_id = self.id;
let langs = CommunityLanguage::read(data.pool(), community_id).await?;
let language = LanguageTag::new_multiple(langs, data.pool()).await?;
let attributed_to = Some(ObjectId::<ApubCommunityModerators>::new(
generate_moderators_url(&self.actor_id)?,
));
let group = Group {
kind: GroupType::Group,
@ -104,7 +101,8 @@ impl ApubObject for ApubCommunity {
icon: self.icon.clone().map(ImageObject::new),
image: self.banner.clone().map(ImageObject::new),
sensitive: Some(self.nsfw),
moderators: attributed_to.clone(),
moderators: Some(generate_moderators_url(&self.actor_id)?.into()),
featured: Some(generate_featured_url(&self.actor_id)?.into()),
inbox: self.inbox_url.clone().into(),
outbox: ObjectId::new(generate_outbox_url(&self.actor_id)?),
followers: self.followers_url.clone().into(),
@ -116,7 +114,7 @@ impl ApubObject for ApubCommunity {
published: Some(convert_datetime(self.published)),
updated: self.updated.map(convert_datetime),
posting_restricted_to_mods: Some(self.posting_restricted_to_mods),
attributed_to,
attributed_to: Some(generate_moderators_url(&self.actor_id)?.into()),
};
Ok(group)
}

View file

@ -49,18 +49,13 @@ impl InCommunity for BlockUser {
.target
.dereference(context, local_instance(context).await, request_counter)
.await?;
let target_community = match target {
let community = match target {
SiteOrCommunity::Community(c) => c,
SiteOrCommunity::Site(_) => return Err(anyhow!("activity is not in community").into()),
};
if let Some(audience) = &self.audience {
let audience = audience
.dereference(context, local_instance(context).await, request_counter)
.await?;
verify_community_matches(&audience, target_community.id)?;
Ok(audience)
} else {
Ok(target_community)
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community)
}
}

View file

@ -1,6 +1,5 @@
use crate::{
activities::verify_community_matches,
local_instance,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::block::block_user::BlockUser, InCommunity},
};
@ -35,15 +34,10 @@ impl InCommunity for UndoBlockUser {
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let object_community = self.object.community(context, request_counter).await?;
let community = self.object.community(context, request_counter).await?;
if let Some(audience) = &self.audience {
let audience = audience
.dereference(context, local_instance(context).await, request_counter)
.await?;
verify_community_matches(&audience, object_community.id)?;
Ok(audience)
} else {
Ok(object_community)
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community)
}
}

View file

@ -1,23 +1,23 @@
use crate::{
activities::{community::get_community_from_moderators_url, verify_community_matches},
local_instance,
activities::verify_community_matches,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::InCommunity,
};
use activitypub_federation::{core::object_id::ObjectId, deser::helpers::deserialize_one_or_many};
use activitystreams_kinds::activity::AddType;
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::source::community::Community;
use lemmy_utils::error::LemmyError;
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AddMod {
pub struct CollectionAdd {
pub(crate) actor: ObjectId<ApubPerson>,
#[serde(deserialize_with = "deserialize_one_or_many")]
pub(crate) to: Vec<Url>,
pub(crate) object: ObjectId<ApubPerson>,
pub(crate) object: Url,
pub(crate) target: Url,
#[serde(deserialize_with = "deserialize_one_or_many")]
pub(crate) cc: Vec<Url>,
@ -28,22 +28,17 @@ pub struct AddMod {
}
#[async_trait::async_trait(?Send)]
impl InCommunity for AddMod {
impl InCommunity for CollectionAdd {
async fn community(
&self,
context: &LemmyContext,
request_counter: &mut i32,
_request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let mod_community =
get_community_from_moderators_url(&self.target, context, request_counter).await?;
let (community, _) =
Community::get_by_collection_url(context.pool(), &self.clone().target.into()).await?;
if let Some(audience) = &self.audience {
let audience = audience
.dereference(context, local_instance(context).await, request_counter)
.await?;
verify_community_matches(&audience, mod_community.id)?;
Ok(audience)
} else {
Ok(mod_community)
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community.into())
}
}

View file

@ -1,19 +1,19 @@
use crate::{
activities::{community::get_community_from_moderators_url, verify_community_matches},
local_instance,
activities::verify_community_matches,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::InCommunity,
};
use activitypub_federation::{core::object_id::ObjectId, deser::helpers::deserialize_one_or_many};
use activitystreams_kinds::activity::RemoveType;
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::source::community::Community;
use lemmy_utils::error::LemmyError;
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RemoveMod {
pub struct CollectionRemove {
pub(crate) actor: ObjectId<ApubPerson>,
#[serde(deserialize_with = "deserialize_one_or_many")]
pub(crate) to: Vec<Url>,
@ -28,22 +28,17 @@ pub struct RemoveMod {
}
#[async_trait::async_trait(?Send)]
impl InCommunity for RemoveMod {
impl InCommunity for CollectionRemove {
async fn community(
&self,
context: &LemmyContext,
request_counter: &mut i32,
_request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let mod_community =
get_community_from_moderators_url(&self.target, context, request_counter).await?;
let (community, _) =
Community::get_by_collection_url(context.pool(), &self.clone().target.into()).await?;
if let Some(audience) = &self.audience {
let audience = audience
.dereference(context, local_instance(context).await, request_counter)
.await?;
verify_community_matches(&audience, mod_community.id)?;
Ok(audience)
} else {
Ok(mod_community)
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community.into())
}
}

View file

@ -0,0 +1,83 @@
use crate::{
activities::verify_community_matches,
local_instance,
objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost},
protocol::InCommunity,
};
use activitypub_federation::{core::object_id::ObjectId, deser::helpers::deserialize_one_or_many};
use activitystreams_kinds::activity::UndoType;
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{source::community::Community, traits::Crud};
use lemmy_utils::error::LemmyError;
use serde::{Deserialize, Serialize};
use strum_macros::Display;
use url::Url;
#[derive(Clone, Debug, Deserialize, Serialize, Display)]
pub enum LockType {
Lock,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LockPage {
pub(crate) actor: ObjectId<ApubPerson>,
#[serde(deserialize_with = "deserialize_one_or_many")]
pub(crate) to: Vec<Url>,
pub(crate) object: ObjectId<ApubPost>,
#[serde(deserialize_with = "deserialize_one_or_many")]
pub(crate) cc: Vec<Url>,
#[serde(rename = "type")]
pub(crate) kind: LockType,
pub(crate) id: Url,
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UndoLockPage {
pub(crate) actor: ObjectId<ApubPerson>,
#[serde(deserialize_with = "deserialize_one_or_many")]
pub(crate) to: Vec<Url>,
pub(crate) object: LockPage,
#[serde(deserialize_with = "deserialize_one_or_many")]
pub(crate) cc: Vec<Url>,
#[serde(rename = "type")]
pub(crate) kind: UndoType,
pub(crate) id: Url,
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
}
#[async_trait::async_trait(?Send)]
impl InCommunity for LockPage {
async fn community(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let post = self
.object
.dereference(context, local_instance(context).await, request_counter)
.await?;
let community = Community::read(context.pool(), post.community_id).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community.into())
}
}
#[async_trait::async_trait(?Send)]
impl InCommunity for UndoLockPage {
async fn community(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let community = self.object.community(context, request_counter).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community)
}
}

View file

@ -1,6 +1,7 @@
pub mod add_mod;
pub mod announce;
pub mod remove_mod;
pub mod collection_add;
pub mod collection_remove;
pub mod lock_page;
pub mod report;
pub mod update;
@ -8,9 +9,10 @@ pub mod update;
mod tests {
use crate::protocol::{
activities::community::{
add_mod::AddMod,
announce::AnnounceActivity,
remove_mod::RemoveMod,
collection_add::CollectionAdd,
collection_remove::CollectionRemove,
lock_page::{LockPage, UndoLockPage},
report::Report,
update::UpdateCommunity,
},
@ -24,8 +26,22 @@ mod tests {
)
.unwrap();
test_parse_lemmy_item::<AddMod>("assets/lemmy/activities/community/add_mod.json").unwrap();
test_parse_lemmy_item::<RemoveMod>("assets/lemmy/activities/community/remove_mod.json")
test_parse_lemmy_item::<CollectionAdd>("assets/lemmy/activities/community/add_mod.json")
.unwrap();
test_parse_lemmy_item::<CollectionRemove>("assets/lemmy/activities/community/remove_mod.json")
.unwrap();
test_parse_lemmy_item::<CollectionAdd>(
"assets/lemmy/activities/community/add_featured_post.json",
)
.unwrap();
test_parse_lemmy_item::<CollectionRemove>(
"assets/lemmy/activities/community/remove_featured_post.json",
)
.unwrap();
test_parse_lemmy_item::<LockPage>("assets/lemmy/activities/community/lock_page.json").unwrap();
test_parse_lemmy_item::<UndoLockPage>("assets/lemmy/activities/community/undo_lock_page.json")
.unwrap();
test_parse_lemmy_item::<UpdateCommunity>(

View file

@ -33,17 +33,12 @@ impl InCommunity for Report {
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let to_community = self.to[0]
let community = self.to[0]
.dereference(context, local_instance(context).await, request_counter)
.await?;
if let Some(audience) = &self.audience {
let audience = audience
.dereference(context, local_instance(context).await, request_counter)
.await?;
verify_community_matches(&audience, to_community.id)?;
Ok(audience)
} else {
Ok(to_community)
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community)
}
}

View file

@ -36,17 +36,12 @@ impl InCommunity for UpdateCommunity {
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let object_community: ApubCommunity = ObjectId::new(self.object.id.clone())
let community: ApubCommunity = ObjectId::new(self.object.id.clone())
.dereference(context, local_instance(context).await, request_counter)
.await?;
if let Some(audience) = &self.audience {
let audience = audience
.dereference(context, local_instance(context).await, request_counter)
.await?;
verify_community_matches(&audience, object_community.id)?;
Ok(audience)
} else {
Ok(object_community)
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community)
}
}

View file

@ -1,6 +1,5 @@
use crate::{
activities::verify_community_matches,
local_instance,
mentions::MentionOrValue,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::CreateOrUpdateType, objects::note::Note, InCommunity},
@ -37,15 +36,10 @@ impl InCommunity for CreateOrUpdateNote {
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let post = self.object.get_parents(context, request_counter).await?.0;
let community = Community::read(context.pool(), post.community_id).await?;
if let Some(audience) = &self.audience {
let audience = audience
.dereference(context, local_instance(context).await, request_counter)
.await?;
verify_community_matches(&audience, post.community_id)?;
Ok(audience)
} else {
let community = Community::read(context.pool(), post.community_id).await?;
Ok(community.into())
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community.into())
}
}

View file

@ -1,6 +1,5 @@
use crate::{
activities::verify_community_matches,
local_instance,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::CreateOrUpdateType, objects::page::Page, InCommunity},
};
@ -32,15 +31,10 @@ impl InCommunity for CreateOrUpdatePage {
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let object_community = self.object.community(context, request_counter).await?;
let community = self.object.community(context, request_counter).await?;
if let Some(audience) = &self.audience {
let audience = audience
.dereference(context, local_instance(context).await, request_counter)
.await?;
verify_community_matches(&audience, object_community.id)?;
Ok(audience)
} else {
Ok(object_community)
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community)
}
}

View file

@ -1,6 +1,5 @@
use crate::{
activities::{deletion::DeletableObjects, verify_community_matches},
local_instance,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{objects::tombstone::Tombstone, IdOrNestedObject, InCommunity},
};
@ -44,7 +43,7 @@ impl InCommunity for Delete {
async fn community(
&self,
context: &LemmyContext,
request_counter: &mut i32,
_request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let community_id = match DeletableObjects::read_from_db(self.object.id(), context).await? {
DeletableObjects::Community(c) => c.id,
@ -57,15 +56,10 @@ impl InCommunity for Delete {
return Err(anyhow!("Private message is not part of community").into())
}
};
let community = Community::read(context.pool(), community_id).await?;
if let Some(audience) = &self.audience {
let audience = audience
.dereference(context, local_instance(context).await, request_counter)
.await?;
verify_community_matches(&audience, community_id)?;
Ok(audience)
} else {
let community = Community::read(context.pool(), community_id).await?;
Ok(community.into())
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community.into())
}
}

View file

@ -1,6 +1,5 @@
use crate::{
activities::verify_community_matches,
local_instance,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::deletion::delete::Delete, InCommunity},
};
@ -37,15 +36,10 @@ impl InCommunity for UndoDelete {
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let object_community = self.object.community(context, request_counter).await?;
let community = self.object.community(context, request_counter).await?;
if let Some(audience) = &self.audience {
let audience = audience
.dereference(context, local_instance(context).await, request_counter)
.await?;
verify_community_matches(&audience, object_community.id)?;
Ok(audience)
} else {
Ok(object_community)
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community)
}
}

View file

@ -1,6 +1,5 @@
use crate::{
activities::verify_community_matches,
local_instance,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::voting::vote::Vote, InCommunity},
};
@ -29,16 +28,10 @@ impl InCommunity for UndoVote {
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let local_instance = local_instance(context).await;
let object_community = self.object.community(context, request_counter).await?;
let community = self.object.community(context, request_counter).await?;
if let Some(audience) = &self.audience {
let audience = audience
.dereference(context, local_instance, request_counter)
.await?;
verify_community_matches(&audience, object_community.id)?;
Ok(audience)
} else {
Ok(object_community)
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community)
}
}

View file

@ -59,20 +59,15 @@ impl InCommunity for Vote {
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let local_instance = local_instance(context).await;
let object_community = self
let community = self
.object
.dereference(context, local_instance, request_counter)
.await?
.community(context, request_counter)
.await?;
if let Some(audience) = &self.audience {
let audience = audience
.dereference(context, local_instance, request_counter)
.await?;
verify_community_matches(&audience, object_community.id)?;
Ok(audience)
} else {
Ok(object_community)
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community)
}
}

View file

@ -0,0 +1,13 @@
use crate::protocol::objects::page::Page;
use activitystreams_kinds::collection::OrderedCollectionType;
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GroupFeatured {
pub(crate) r#type: OrderedCollectionType,
pub(crate) id: Url,
pub(crate) total_items: i32,
pub(crate) ordered_items: Vec<Page>,
}

View file

@ -1,4 +1,5 @@
pub(crate) mod empty_outbox;
pub(crate) mod group_featured;
pub(crate) mod group_followers;
pub(crate) mod group_moderators;
pub(crate) mod group_outbox;
@ -8,11 +9,12 @@ mod tests {
use crate::protocol::{
collections::{
empty_outbox::EmptyOutbox,
group_featured::GroupFeatured,
group_followers::GroupFollowers,
group_moderators::GroupModerators,
group_outbox::GroupOutbox,
},
tests::test_parse_lemmy_item,
tests::{test_json, test_parse_lemmy_item},
};
#[test]
@ -22,8 +24,15 @@ mod tests {
let outbox =
test_parse_lemmy_item::<GroupOutbox>("assets/lemmy/collections/group_outbox.json").unwrap();
assert_eq!(outbox.ordered_items.len() as i32, outbox.total_items);
test_parse_lemmy_item::<GroupFeatured>("assets/lemmy/collections/group_featured_posts.json")
.unwrap();
test_parse_lemmy_item::<GroupModerators>("assets/lemmy/collections/group_moderators.json")
.unwrap();
test_parse_lemmy_item::<EmptyOutbox>("assets/lemmy/collections/person_outbox.json").unwrap();
}
#[test]
fn test_parse_mastodon_collections() {
test_json::<GroupFeatured>("assets/mastodon/collections/featured.json").unwrap();
}
}

View file

@ -1,6 +1,7 @@
use crate::{
check_apub_id_valid_with_strictness,
collections::{
community_featured::ApubCommunityFeatured,
community_moderators::ApubCommunityModerators,
community_outbox::ApubCommunityOutbox,
},
@ -65,6 +66,7 @@ pub struct Group {
pub(crate) posting_restricted_to_mods: Option<bool>,
pub(crate) outbox: ObjectId<ApubCommunityOutbox>,
pub(crate) endpoints: Option<Endpoints>,
pub(crate) featured: Option<ObjectId<ApubCommunityFeatured>>,
#[serde(default)]
pub(crate) language: Vec<LanguageTag>,
pub(crate) published: Option<DateTime<FixedOffset>>,
@ -117,8 +119,10 @@ impl Group {
followers_url: Some(self.followers.into()),
inbox_url: Some(self.inbox.into()),
shared_inbox_url: self.endpoints.map(|e| e.shared_inbox.into()),
moderators_url: self.moderators.map(Into::into),
posting_restricted_to_mods: self.posting_restricted_to_mods,
instance_id,
featured_url: self.featured.map(Into::into),
}
}
@ -146,7 +150,9 @@ impl Group {
followers_url: Some(self.followers.into()),
inbox_url: Some(self.inbox.into()),
shared_inbox_url: Some(self.endpoints.map(|e| e.shared_inbox.into())),
moderators_url: self.moderators.map(Into::into),
posting_restricted_to_mods: self.posting_restricted_to_mods,
featured_url: self.featured.map(Into::into),
}
}
}

View file

@ -67,15 +67,11 @@ impl Note {
.await?,
);
match parent.deref() {
PostOrComment::Post(p) => {
let post = p.deref().clone();
Ok((post, None))
}
PostOrComment::Post(p) => Ok((p.clone(), None)),
PostOrComment::Comment(c) => {
let post_id = c.post_id;
let post = Post::read(context.pool(), post_id).await?;
let comment = c.deref().clone();
Ok((post.into(), Some(comment)))
Ok((post.into(), Some(c.clone())))
}
}
}
@ -89,15 +85,10 @@ impl InCommunity for Note {
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let (post, _) = self.get_parents(context, request_counter).await?;
let community_id = post.community_id;
let community = Community::read(context.pool(), post.community_id).await?;
if let Some(audience) = &self.audience {
let audience = audience
.dereference(context, local_instance(context).await, request_counter)
.await?;
verify_community_matches(&audience, community_id)?;
Ok(audience)
} else {
Ok(Community::read(context.pool(), community_id).await?.into())
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community.into())
}
}

View file

@ -64,6 +64,7 @@ pub struct Page {
pub(crate) image: Option<ImageObject>,
pub(crate) comments_enabled: Option<bool>,
pub(crate) sensitive: Option<bool>,
/// Deprecated, for compatibility with Lemmy 0.17
pub(crate) stickied: Option<bool>,
pub(crate) published: Option<DateTime<FixedOffset>>,
pub(crate) updated: Option<DateTime<FixedOffset>>,
@ -252,14 +253,9 @@ impl InCommunity for Page {
}
};
if let Some(audience) = &self.audience {
let audience = audience
.dereference(context, instance, request_counter)
.await?;
verify_community_matches(&audience, community.id)?;
Ok(audience)
} else {
Ok(community)
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community)
}
}

View file

@ -194,6 +194,38 @@ impl DeleteableOrRemoveable for Community {
}
}
pub enum CollectionType {
Moderators,
Featured,
}
impl Community {
/// Get the community which has a given moderators or featured url, also return the collection type
pub async fn get_by_collection_url(
pool: &DbPool,
url: &DbUrl,
) -> Result<(Community, CollectionType), Error> {
use crate::schema::community::dsl::{featured_url, moderators_url};
use CollectionType::*;
let conn = &mut get_conn(pool).await?;
let res = community
.filter(moderators_url.eq(url))
.first::<Self>(conn)
.await;
if let Ok(c) = res {
return Ok((c, Moderators));
}
let res = community
.filter(featured_url.eq(url))
.first::<Self>(conn)
.await;
if let Ok(c) = res {
return Ok((c, Featured));
}
Err(diesel::NotFound)
}
}
impl CommunityModerator {
pub async fn delete_for_community(
pool: &DbPool,
@ -430,6 +462,8 @@ mod tests {
followers_url: inserted_community.followers_url.clone(),
inbox_url: inserted_community.inbox_url.clone(),
shared_inbox_url: None,
moderators_url: None,
featured_url: None,
hidden: false,
posting_restricted_to_mods: false,
instance_id: inserted_instance.id,

View file

@ -89,6 +89,22 @@ impl Post {
.await
}
pub async fn list_featured_for_community(
pool: &DbPool,
the_community_id: CommunityId,
) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?;
post
.filter(community_id.eq(the_community_id))
.filter(deleted.eq(false))
.filter(removed.eq(false))
.filter(featured_community.eq(true))
.then_order_by(published.desc())
.limit(FETCH_LIMIT_MAX)
.load::<Self>(conn)
.await
}
pub async fn permadelete_for_creator(
pool: &DbPool,
for_creator_id: PersonId,

View file

@ -1,4 +1,6 @@
#[cfg(feature = "full")]
use activitypub_federation::{core::object_id::ObjectId, traits::ApubObject};
#[cfg(feature = "full")]
use diesel_ltree::Ltree;
use serde::{Deserialize, Serialize};
use std::{
@ -110,7 +112,7 @@ pub struct LocalSiteId(i32);
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
#[cfg_attr(feature = "full", derive(AsExpression, FromSqlRow))]
#[cfg_attr(feature = "full", diesel(sql_type = diesel::sql_types::Text))]
pub struct DbUrl(pub(crate) Url);
pub struct DbUrl(pub(crate) Box<Url>);
#[cfg(feature = "full")]
#[derive(Serialize, Deserialize)]
@ -128,13 +130,23 @@ impl Display for DbUrl {
#[allow(clippy::from_over_into)]
impl Into<DbUrl> for Url {
fn into(self) -> DbUrl {
DbUrl(self)
DbUrl(Box::new(self))
}
}
#[allow(clippy::from_over_into)]
impl Into<Url> for DbUrl {
fn into(self) -> Url {
self.0
*self.0
}
}
#[cfg(feature = "full")]
impl<T> From<DbUrl> for ObjectId<T>
where
T: ApubObject + Send,
for<'de2> <T as ApubObject>::ApubType: Deserialize<'de2>,
{
fn from(value: DbUrl) -> Self {
ObjectId::new(value)
}
}

View file

@ -98,6 +98,8 @@ table! {
followers_url -> Varchar,
inbox_url -> Varchar,
shared_inbox_url -> Nullable<Varchar>,
moderators_url -> Nullable<Varchar>,
featured_url -> Nullable<Varchar>,
hidden -> Bool,
posting_restricted_to_mods -> Bool,
instance_id -> Int4,

View file

@ -27,6 +27,12 @@ pub struct Community {
pub followers_url: DbUrl,
pub inbox_url: DbUrl,
pub shared_inbox_url: Option<DbUrl>,
/// Url where moderators collection is served over Activitypub
#[serde(skip)]
pub moderators_url: Option<DbUrl>,
/// Url where featured posts collection is served over Activitypub
#[serde(skip)]
pub featured_url: Option<DbUrl>,
pub hidden: bool,
pub posting_restricted_to_mods: bool,
pub instance_id: InstanceId,
@ -80,6 +86,8 @@ pub struct CommunityInsertForm {
pub followers_url: Option<DbUrl>,
pub inbox_url: Option<DbUrl>,
pub shared_inbox_url: Option<DbUrl>,
pub moderators_url: Option<DbUrl>,
pub featured_url: Option<DbUrl>,
pub hidden: Option<bool>,
pub posting_restricted_to_mods: Option<bool>,
#[builder(!default)]
@ -108,6 +116,8 @@ pub struct CommunityUpdateForm {
pub followers_url: Option<DbUrl>,
pub inbox_url: Option<DbUrl>,
pub shared_inbox_url: Option<Option<DbUrl>>,
pub moderators_url: Option<DbUrl>,
pub featured_url: Option<DbUrl>,
pub hidden: Option<bool>,
pub posting_restricted_to_mods: Option<bool>,
}

View file

@ -227,7 +227,7 @@ where
{
fn from_sql(value: diesel::backend::RawValue<'_, DB>) -> diesel::deserialize::Result<Self> {
let str = String::from_sql(value)?;
Ok(DbUrl(Url::parse(&str)?))
Ok(DbUrl(Box::new(Url::parse(&str)?)))
}
}
@ -237,7 +237,7 @@ where
for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>,
{
fn from(id: ObjectId<Kind>) -> Self {
DbUrl(id.into())
DbUrl(Box::new(id.into()))
}
}

View file

@ -44,6 +44,7 @@ services:
- RUST_LOG="warn,lemmy_server=debug,lemmy_api=debug,lemmy_api_common=debug,lemmy_api_crud=debug,lemmy_apub=debug,lemmy_db_schema=debug,lemmy_db_views=debug,lemmy_db_views_actor=debug,lemmy_db_views_moderator=debug,lemmy_routes=debug,lemmy_utils=debug,lemmy_websocket=debug"
depends_on:
- postgres_alpha
restart: always
ports:
- "8541:8541"
postgres_alpha:
@ -73,6 +74,7 @@ services:
- RUST_LOG="warn,lemmy_server=debug,lemmy_api=debug,lemmy_api_common=debug,lemmy_api_crud=debug,lemmy_apub=debug,lemmy_db_schema=debug,lemmy_db_views=debug,lemmy_db_views_actor=debug,lemmy_db_views_moderator=debug,lemmy_routes=debug,lemmy_utils=debug,lemmy_websocket=debug"
depends_on:
- postgres_beta
restart: always
ports:
- "8551:8551"
postgres_beta:
@ -102,6 +104,7 @@ services:
- RUST_LOG="warn,lemmy_server=debug,lemmy_api=debug,lemmy_api_common=debug,lemmy_api_crud=debug,lemmy_apub=debug,lemmy_db_schema=debug,lemmy_db_views=debug,lemmy_db_views_actor=debug,lemmy_db_views_moderator=debug,lemmy_routes=debug,lemmy_utils=debug,lemmy_websocket=debug"
depends_on:
- postgres_gamma
restart: always
ports:
- "8561:8561"
postgres_gamma:
@ -132,6 +135,7 @@ services:
- RUST_LOG="warn,lemmy_server=debug,lemmy_api=debug,lemmy_api_common=debug,lemmy_api_crud=debug,lemmy_apub=debug,lemmy_db_schema=debug,lemmy_db_views=debug,lemmy_db_views_actor=debug,lemmy_db_views_moderator=debug,lemmy_routes=debug,lemmy_utils=debug,lemmy_websocket=debug"
depends_on:
- postgres_delta
restart: always
ports:
- "8571:8571"
postgres_delta:
@ -162,6 +166,7 @@ services:
- RUST_LOG="warn,lemmy_server=debug,lemmy_api=debug,lemmy_api_common=debug,lemmy_api_crud=debug,lemmy_apub=debug,lemmy_db_schema=debug,lemmy_db_views=debug,lemmy_db_views_actor=debug,lemmy_db_views_moderator=debug,lemmy_routes=debug,lemmy_utils=debug,lemmy_websocket=debug"
depends_on:
- postgres_epsilon
restart: always
ports:
- "8581:8581"
postgres_epsilon:

View file

@ -0,0 +1,2 @@
alter table community drop column moderators_url;
alter table community drop column featured_url;

View file

@ -0,0 +1,2 @@
alter table community add column moderators_url varchar(255) unique;
alter table community add column featured_url varchar(255) unique;