]> Untitled Git - lemmy.git/commitdiff
Implement separate mod activities for feature, lock post (#2716)
authorNutomic <me@nutomic.com>
Sat, 18 Feb 2023 14:50:28 +0000 (23:50 +0900)
committerGitHub <noreply@github.com>
Sat, 18 Feb 2023 14:50:28 +0000 (09:50 -0500)
* 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>
54 files changed:
crates/api_common/src/utils.rs
crates/apub/assets/lemmy/activities/community/add_featured_post.json [new file with mode: 0644]
crates/apub/assets/lemmy/activities/community/lock_page.json [new file with mode: 0644]
crates/apub/assets/lemmy/activities/community/remove_featured_post.json [new file with mode: 0644]
crates/apub/assets/lemmy/activities/community/undo_lock_page.json [new file with mode: 0644]
crates/apub/assets/lemmy/collections/group_featured_posts.json [new file with mode: 0644]
crates/apub/assets/lemmy/objects/group.json
crates/apub/assets/mastodon/collections/featured.json [new file with mode: 0644]
crates/apub/src/activities/community/add_mod.rs [deleted file]
crates/apub/src/activities/community/collection_add.rs [new file with mode: 0644]
crates/apub/src/activities/community/collection_remove.rs [new file with mode: 0644]
crates/apub/src/activities/community/lock_page.rs [new file with mode: 0644]
crates/apub/src/activities/community/mod.rs
crates/apub/src/activities/community/remove_mod.rs [deleted file]
crates/apub/src/activities/create_or_update/post.rs
crates/apub/src/activities/deletion/mod.rs
crates/apub/src/activities/mod.rs
crates/apub/src/activity_lists.rs
crates/apub/src/collections/community_featured.rs [new file with mode: 0644]
crates/apub/src/collections/community_outbox.rs
crates/apub/src/collections/mod.rs
crates/apub/src/fetcher/deletable_apub_object.rs [deleted file]
crates/apub/src/fetcher/post_or_comment.rs
crates/apub/src/http/community.rs
crates/apub/src/http/routes.rs
crates/apub/src/objects/community.rs
crates/apub/src/protocol/activities/block/block_user.rs
crates/apub/src/protocol/activities/block/undo_block_user.rs
crates/apub/src/protocol/activities/community/collection_add.rs [moved from crates/apub/src/protocol/activities/community/add_mod.rs with 63% similarity]
crates/apub/src/protocol/activities/community/collection_remove.rs [moved from crates/apub/src/protocol/activities/community/remove_mod.rs with 66% similarity]
crates/apub/src/protocol/activities/community/lock_page.rs [new file with mode: 0644]
crates/apub/src/protocol/activities/community/mod.rs
crates/apub/src/protocol/activities/community/report.rs
crates/apub/src/protocol/activities/community/update.rs
crates/apub/src/protocol/activities/create_or_update/note.rs
crates/apub/src/protocol/activities/create_or_update/page.rs
crates/apub/src/protocol/activities/deletion/delete.rs
crates/apub/src/protocol/activities/deletion/undo_delete.rs
crates/apub/src/protocol/activities/voting/undo_vote.rs
crates/apub/src/protocol/activities/voting/vote.rs
crates/apub/src/protocol/collections/group_featured.rs [new file with mode: 0644]
crates/apub/src/protocol/collections/mod.rs
crates/apub/src/protocol/objects/group.rs
crates/apub/src/protocol/objects/note.rs
crates/apub/src/protocol/objects/page.rs
crates/db_schema/src/impls/community.rs
crates/db_schema/src/impls/post.rs
crates/db_schema/src/newtypes.rs
crates/db_schema/src/schema.rs
crates/db_schema/src/source/community.rs
crates/db_schema/src/utils.rs
docker/federation/docker-compose.yml
migrations/2023-02-07-030958_community-collections/down.sql [new file with mode: 0644]
migrations/2023-02-07-030958_community-collections/up.sql [new file with mode: 0644]

index 130f6d635781eddf74fff6917b0b725844c297a1..919027636e264f68def860b8468a27b6e67a5db1 100644 (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())
 }
diff --git a/crates/apub/assets/lemmy/activities/community/add_featured_post.json b/crates/apub/assets/lemmy/activities/community/add_featured_post.json
new file mode 100644 (file)
index 0000000..f7e46ef
--- /dev/null
@@ -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"
+}
\ No newline at end of file
diff --git a/crates/apub/assets/lemmy/activities/community/lock_page.json b/crates/apub/assets/lemmy/activities/community/lock_page.json
new file mode 100644 (file)
index 0000000..887d0f7
--- /dev/null
@@ -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"
+}
\ No newline at end of file
diff --git a/crates/apub/assets/lemmy/activities/community/remove_featured_post.json b/crates/apub/assets/lemmy/activities/community/remove_featured_post.json
new file mode 100644 (file)
index 0000000..7622582
--- /dev/null
@@ -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"
+}
diff --git a/crates/apub/assets/lemmy/activities/community/undo_lock_page.json b/crates/apub/assets/lemmy/activities/community/undo_lock_page.json
new file mode 100644 (file)
index 0000000..91e6a46
--- /dev/null
@@ -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"
+}
diff --git a/crates/apub/assets/lemmy/collections/group_featured_posts.json b/crates/apub/assets/lemmy/collections/group_featured_posts.json
new file mode 100644 (file)
index 0000000..c5bc5fd
--- /dev/null
@@ -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"
+    }
+  ]
+}
index d44cc79bd7d17c40c57c3292291ebe85e009bbc3..10c46640c5f4c7ae5be9c3197fcc961b221898f4 100644 (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"
diff --git a/crates/apub/assets/mastodon/collections/featured.json b/crates/apub/assets/mastodon/collections/featured.json
new file mode 100644 (file)
index 0000000..fecc7bb
--- /dev/null
@@ -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"
+          ]
+        }
+      }
+    }
+  ]
+}
diff --git a/crates/apub/src/activities/community/add_mod.rs b/crates/apub/src/activities/community/add_mod.rs
deleted file mode 100644 (file)
index 1bc31b4..0000000
+++ /dev/null
@@ -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
-    }
-  }
-}
diff --git a/crates/apub/src/activities/community/collection_add.rs b/crates/apub/src/activities/community/collection_add.rs
new file mode 100644 (file)
index 0000000..b45fa27
--- /dev/null
@@ -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
+    }
+  }
+}
diff --git a/crates/apub/src/activities/community/collection_remove.rs b/crates/apub/src/activities/community/collection_remove.rs
new file mode 100644 (file)
index 0000000..d804e65
--- /dev/null
@@ -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(())
+  }
+}
diff --git a/crates/apub/src/activities/community/lock_page.rs b/crates/apub/src/activities/community/lock_page.rs
new file mode 100644 (file)
index 0000000..8caf3bf
--- /dev/null
@@ -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(())
+  }
+}
index 4d35c9d56a29efd9f99e3df685c919ed95c7d56f..226d5d64310b927652f5d95a46c1e0f5f5b08085 100644 (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
-}
diff --git a/crates/apub/src/activities/community/remove_mod.rs b/crates/apub/src/activities/community/remove_mod.rs
deleted file mode 100644 (file)
index 9bd8c4e..0000000
+++ /dev/null
@@ -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(())
-  }
-}
index 69eae583c80f833d7d2d985bbeb546247d00a01c..d2e8e76c74f582049146755c0503b79bceb4e3e0 100644 (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,
index dae70dc3d5aa01752c742b16df442ebf41ac21b7..c17dcbfc2c41be13a5253e2287339cfafa473089 100644 (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())
   }
index 2fb5808b7869af7a085536965846914e498ef190..2896959fc60654e31898094b0564e67fc5e9b8de 100644 (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(())
index 037717d844b4465f053f5e393dcb80f7ade96d52..70ae1bb8ff4c0fc47e4ba8ef5a07e0d3dbf47ddc 100644 (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!(),
     }
   }
diff --git a/crates/apub/src/collections/community_featured.rs b/crates/apub/src/collections/community_featured.rs
new file mode 100644 (file)
index 0000000..7e1941f
--- /dev/null
@@ -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()))
+  }
+}
index a16fbd02bba8ad75d3b3b1769600b34e167c1ffd..49140aab8d7a29887ff9b9815b9a518ba3517cad 100644 (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 = ();
 }
index 40bdf1206aef77cde5060885d12dbd88b1d8010f..a8d5e136d384b10b312f07a02fd83f7200157825 100644 (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;
 
diff --git a/crates/apub/src/fetcher/deletable_apub_object.rs b/crates/apub/src/fetcher/deletable_apub_object.rs
deleted file mode 100644 (file)
index 0eae410..0000000
+++ /dev/null
@@ -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(())
-  }
-}
index f8a260b9823b784c39dc8c50f44988902f563ff9..c0a42301cb9be7f7ce31d277933c02cf6300112e 100644 (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?)
+      }
     })
   }
 }
index 74809509fb4adad01f0559b5084b339f4db4a87e..9a0d1f02bf1e96fba0eddeb7c3491c6e27e5c1c3 100644 (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?))
+}
index a588b3127b03295cebabd31bc44ce6a4f22c173c..4d4941f534d83def72239d750e30a34a87b8e02c 100644 (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),
index e23e0743cb0e7d23fa389ee44439b63bbfade9dd..9f8cfae60b4eacb51c0e6da404244a7e6bc9f4b6 100644 (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)
   }
index 3ac040ced0a7b44b3c5b1445e5d86c6de76ce230..0812489b6635f27aa5f60087488cf23219be013d 100644 (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)
   }
 }
index d818af9de8212dbaaa4e6e38873abeb1bf8b6282..9466fedc9f6a432ac163f79adafcf2b5c7f31806 100644 (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)
   }
 }
similarity index 63%
rename from crates/apub/src/protocol/activities/community/add_mod.rs
rename to crates/apub/src/protocol/activities/community/collection_add.rs
index 22fc07fcbaf4fd85a130772aeb6f562786ca3ceb..a91244189895ba18086c6289783f37cf970bcff8 100644 (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())
   }
 }
similarity index 66%
rename from crates/apub/src/protocol/activities/community/remove_mod.rs
rename to crates/apub/src/protocol/activities/community/collection_remove.rs
index ce46fb920f6cba3b8aaf22d0992514ee8a3955a7..6ca8a2392439f4992cdab2f119c849506146d3b3 100644 (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())
   }
 }
diff --git a/crates/apub/src/protocol/activities/community/lock_page.rs b/crates/apub/src/protocol/activities/community/lock_page.rs
new file mode 100644 (file)
index 0000000..f7b5554
--- /dev/null
@@ -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)
+  }
+}
index 47771891ffed0b071a6e38e377231457bb19da76..d43e111e0b989b814c609e7990578ca806f33f33 100644 (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>(
index 0a1ef650fc264c06d20d7e1ba407de3578f2ee0b..4d786b3954c901af36cb062d1fa101ae33b9738c 100644 (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)
   }
 }
index 9a2f1f48165e7c7c47f09f743ded9016b0f11cd5..a8934717f5da54c016488e7e33a20f064f69081b 100644 (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)
   }
 }
index dfa1dbe9ff5d5d5683e5241299ebe73e541ae57e..2cfcd2d8a05170abbd03a1d30ce97ae277eddf65 100644 (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())
   }
 }
index 2c15d9f90d98c8a282d18f275e4fbdf9c291159e..ad17d7383bd145244fb420ed4c49fe4ba3412485 100644 (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)
   }
 }
index d92ac2456c044d774a0c8eb1fe79d8682e706c8d..162e595f1f28f696accf9b2cc76676f86d7ea990 100644 (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())
   }
 }
index d5249ba9aaa438d2e6156f83f1d67c361613da70..6c584ccf5299862b7a45d6ab5de782ef80d2b02c 100644 (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)
   }
 }
index 0973c76a86059d2472b31869c7f7f4e071b171d8..a8432c8275752c19f68de822333e67d89f0909f5 100644 (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)
   }
 }
index 2a09a45eaa4a4022a48f6abd3679cb36f3e54654..cf79fc9adab41af4a1efde2cbdc20d1e1172ffa1 100644 (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)
   }
 }
diff --git a/crates/apub/src/protocol/collections/group_featured.rs b/crates/apub/src/protocol/collections/group_featured.rs
new file mode 100644 (file)
index 0000000..cbd7dce
--- /dev/null
@@ -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>,
+}
index 0e251a1bbf4c5da4ef5aabd88b1ba3664b328ba0..41b4a9f584cc9bda5af83f4785ad00b421d54b77 100644 (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();
+  }
 }
index a09f9b118a68bf7591a2cf8f2c977839a81beb84..9c4aa35d0c085f3ee1ce3fcb4e56ee460399b6de 100644 (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),
     }
   }
 }
index f561c313df9e624ad2ec692bbefad7237707aa50..f93a41ef884a8ed5edb54bd1a90be9375afb1504 100644 (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())
   }
 }
index 9055b1fcc3201c570ec2ae99f44f5270cbcc7cbe..65a164ecdd473e3b588df4f6ba0057e801f68c7f 100644 (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)
   }
 }
 
index 935bfa05eaffa9585ccd288370abebddcffd056c..d1a4f2b6c34fa415f54105222ada626f769a948b 100644 (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,
index 91e7bc1e316e5456a36887afd5e86dec42ee32da..9f5c64389b5f735343bbd932ce766cf7e3599d7c 100644 (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,
index b8e85a1b3311d8c3d01c6897a92a50c6286a1a7d..7e2465bb70b0eb18fd9ef0d927894fcd4dccb766 100644 (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)
   }
 }
 
index 8c893cf9222ce04c665cb8de25c89ee3c06148fb..a177139ceafa297d372d54dd311b42674c32b93f 100644 (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,
index 2630737a2df9dc19073bc9424e90b7958a3ac422..664e29b4c89b51d7a4aed5a7cf693f6f10abdcdc 100644 (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>,
 }
index 76d1f795007747784bfb944ed698d497a2d9088b..71a0875d34d2c60e272f626acedcf34e26d2760d 100644 (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()))
   }
 }
 
index f581aa4f46c088c03a55c16bee7179274a101163..ab512f49f05bdb6c4561b9c9b1fe22a8d6ffe094 100644 (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:
diff --git a/migrations/2023-02-07-030958_community-collections/down.sql b/migrations/2023-02-07-030958_community-collections/down.sql
new file mode 100644 (file)
index 0000000..8f7b531
--- /dev/null
@@ -0,0 +1,2 @@
+alter table community drop column moderators_url;
+alter table community drop column featured_url;
\ No newline at end of file
diff --git a/migrations/2023-02-07-030958_community-collections/up.sql b/migrations/2023-02-07-030958_community-collections/up.sql
new file mode 100644 (file)
index 0000000..78e7e52
--- /dev/null
@@ -0,0 +1,2 @@
+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