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())
}
--- /dev/null
+{
+ "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"
+}
\ No newline at end of file
--- /dev/null
+{
+ "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"
+}
\ No newline at end of file
--- /dev/null
+{
+ "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"
+}
--- /dev/null
+{
+ "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"
+}
--- /dev/null
+{
+ "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"
+ }
+ ]
+}
"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"
--- /dev/null
+{
+ "@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"
+ ]
+ }
+ }
+ }
+ ]
+}
+++ /dev/null
-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
- }
- }
-}
--- /dev/null
+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
+ }
+ }
+}
--- /dev/null
+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(())
+ }
+}
--- /dev/null
+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(())
+ }
+}
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;
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
-}
+++ /dev/null
-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(())
- }
-}
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::{
}
}
-#[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,
}
#[tracing::instrument(skip_all)]
- async fn send(
+ pub(crate) async fn send(
post: &Post,
person_id: PersonId,
kind: CreateOrUpdateType,
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,
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,
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
}
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,
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,
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,
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)?;
}
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 {
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())
}
};
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},
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"));
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(())
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,
},
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),
}
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!(),
}
}
--- /dev/null
+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()))
+ }
+}
use lemmy_db_schema::{
source::{person::Person, post::Post},
traits::Crud,
+ utils::FETCH_LIMIT_MAX,
};
use lemmy_utils::error::LemmyError;
use url::Url;
type DataType = CommunityContext;
type ApubType = GroupOutbox;
type Error = LemmyError;
+ type DbType = ();
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
None
}
}
- 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![];
_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
// This return value is unused, so just set an empty vec
Ok(ApubCommunityOutbox(Vec::new()))
}
-
- type DbType = ();
}
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;
+++ /dev/null
-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(())
- }
-}
#[derive(Clone, Debug)]
pub enum PostOrComment {
- Post(Box<ApubPost>),
- Comment(Box<ApubComment>),
+ Post(ApubPost),
+ Comment(ApubComment),
}
#[derive(Deserialize)]
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,
) -> 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),
})
}
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?)
+ }
})
}
}
use crate::{
activity_lists::GroupInboxActivities,
collections::{
+ community_featured::ApubCommunityFeatured,
community_moderators::ApubCommunityModerators,
community_outbox::ApubCommunityOutbox,
CommunityContext,
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;
&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?))
+}
comment::get_apub_comment,
community::{
community_inbox,
+ get_apub_community_featured,
get_apub_community_followers,
get_apub_community_http,
get_apub_community_moderators,
"/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),
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,
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::{
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,
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(),
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)
}
.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)
}
}
use crate::{
activities::verify_community_matches,
- local_instance,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::block::block_user::BlockUser, InCommunity},
};
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)
}
}
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>,
}
#[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())
}
}
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>,
}
#[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())
}
}
--- /dev/null
+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)
+ }
+}
-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;
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,
},
)
.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>(
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)
}
}
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)
}
}
use crate::{
activities::verify_community_matches,
- local_instance,
mentions::MentionOrValue,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::CreateOrUpdateType, objects::note::Note, InCommunity},
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())
}
}
use crate::{
activities::verify_community_matches,
- local_instance,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::CreateOrUpdateType, objects::page::Page, InCommunity},
};
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)
}
}
use crate::{
activities::{deletion::DeletableObjects, verify_community_matches},
- local_instance,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{objects::tombstone::Tombstone, IdOrNestedObject, InCommunity},
};
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,
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())
}
}
use crate::{
activities::verify_community_matches,
- local_instance,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::deletion::delete::Delete, InCommunity},
};
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)
}
}
use crate::{
activities::verify_community_matches,
- local_instance,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::voting::vote::Vote, InCommunity},
};
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)
}
}
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)
}
}
--- /dev/null
+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>,
+}
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;
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]
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();
+ }
}
use crate::{
check_apub_id_valid_with_strictness,
collections::{
+ community_featured::ApubCommunityFeatured,
community_moderators::ApubCommunityModerators,
community_outbox::ApubCommunityOutbox,
},
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>>,
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),
}
}
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),
}
}
}
.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())))
}
}
}
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())
}
}
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>>,
}
};
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)
}
}
}
}
+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,
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,
.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,
#[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::{
#[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)]
#[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)
}
}
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,
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,
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)]
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>,
}
{
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)?)))
}
}
for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>,
{
fn from(id: ObjectId<Kind>) -> Self {
- DbUrl(id.into())
+ DbUrl(Box::new(id.into()))
}
}
- 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:
- 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:
- 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:
- 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:
- 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:
--- /dev/null
+alter table community drop column moderators_url;
+alter table community drop column featured_url;
\ No newline at end of file
--- /dev/null
+alter table community add column moderators_url varchar(255) unique;
+alter table community add column featured_url varchar(255) unique;
\ No newline at end of file