diff --git a/js/src/views/Posts/Edit.vue b/js/src/views/Posts/Edit.vue
index 7d6428e74..957ac2dc0 100644
--- a/js/src/views/Posts/Edit.vue
+++ b/js/src/views/Posts/Edit.vue
@@ -97,7 +97,7 @@ import { FETCH_POST, CREATE_POST, UPDATE_POST, DELETE_POST } from "../../graphql
 
 import { IPost, PostVisibility } from "../../types/post.model";
 import Editor from "../../components/Editor.vue";
-import { IActor, IGroup } from "../../types/actor";
+import { IActor, IGroup, usernameWithDomain } from "../../types/actor";
 import TagInput from "../../components/Event/TagInput.vue";
 import RouteName from "../../router/name";
 import Subtitle from "../../components/Utils/Subtitle.vue";
@@ -233,7 +233,7 @@ export default class EditPost extends Vue {
     if (data && this.post.attributedTo) {
       this.$router.push({
         name: RouteName.POSTS,
-        params: { preferredUsername: this.post.attributedTo.preferredUsername },
+        params: { preferredUsername: usernameWithDomain(this.post.attributedTo) },
       });
     }
   }
diff --git a/js/src/views/Posts/List.vue b/js/src/views/Posts/List.vue
index 30d515820..214704117 100644
--- a/js/src/views/Posts/List.vue
+++ b/js/src/views/Posts/List.vue
@@ -120,6 +120,16 @@ const POSTS_PAGE_LIMIT = 10;
   components: {
     PostElementItem,
   },
+  metaInfo() {
+    return {
+      // if no subcomponents specify a metaInfo.title, this title will be used
+      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+      // @ts-ignore
+      title: this.$t("My groups") as string,
+      // all titles will be injected into this template
+      titleTemplate: "%s | Mobilizon",
+    };
+  },
 })
 export default class PostList extends Vue {
   @Prop({ required: true, type: String }) preferredUsername!: string;
diff --git a/js/src/views/Resources/ResourceFolder.vue b/js/src/views/Resources/ResourceFolder.vue
index 1dcb85952..8def62dc9 100644
--- a/js/src/views/Resources/ResourceFolder.vue
+++ b/js/src/views/Resources/ResourceFolder.vue
@@ -114,6 +114,7 @@
                 :resource="localResource"
                 :group="resource.actor"
                 @delete="deleteResource"
+                @rename="handleRename"
                 @move="handleMove"
                 v-else
               />
@@ -143,7 +144,7 @@
         <section class="modal-card-body">
           <resource-selector
             :initialResource="updatedResource"
-            :username="resource.actor.preferredUsername"
+            :username="usernameWithDomain(resource.actor)"
             @updateResource="moveResource"
             @closeMoveModal="moveModal = false"
           />
@@ -200,6 +201,7 @@ import { Component, Mixins, Prop, Watch } from "vue-property-decorator";
 import ResourceItem from "@/components/Resource/ResourceItem.vue";
 import FolderItem from "@/components/Resource/FolderItem.vue";
 import Draggable from "vuedraggable";
+import { RefetchQueryDescription } from "apollo-client/core/watchQueryOptions";
 import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
 import { IActor, usernameWithDomain } from "../../types/actor";
 import RouteName from "../../router/name";
@@ -232,6 +234,9 @@ import ResourceSelector from "../../components/Resource/ResourceSelector.vue";
           username: this.$route.params.preferredUsername,
         };
       },
+      error({ graphQLErrors }) {
+        this.handleErrors(graphQLErrors);
+      },
     },
     config: CONFIG,
     currentActor: CURRENT_ACTOR_CLIENT,
@@ -291,6 +296,13 @@ export default class Resources extends Mixins(ResourceMixin) {
 
   mapServiceTypeToIcon = mapServiceTypeToIcon;
 
+  get actualPath(): string {
+    const path = Array.isArray(this.$route.params.path)
+      ? this.$route.params.path.join("/")
+      : this.$route.params.path || this.path;
+    return path[0] !== "/" ? `/${path}` : path;
+  }
+
   async createResource(): Promise<void> {
     if (!this.resource.actor) return;
     try {
@@ -305,34 +317,7 @@ export default class Resources extends Mixins(ResourceMixin) {
             this.resource.id && this.resource.id.startsWith("root_") ? null : this.resource.id,
           type: this.newResource.type,
         },
-        update: (store, { data: { createResource } }) => {
-          if (createResource == null) return;
-          if (!this.resource.actor) return;
-          const cachedData = store.readQuery<{ resource: IResource }>({
-            query: GET_RESOURCE,
-            variables: {
-              path: this.resource.path,
-              username: this.resource.actor.preferredUsername,
-            },
-          });
-          if (cachedData == null) return;
-          const { resource } = cachedData;
-          if (resource == null) {
-            console.error("Cannot update resource cache, because of null value.");
-            return;
-          }
-          const newResource: IResource = createResource;
-          resource.children.elements = resource.children.elements.concat([newResource]);
-
-          store.writeQuery({
-            query: GET_RESOURCE,
-            variables: {
-              path: this.resource.path,
-              username: this.resource.actor.preferredUsername,
-            },
-            data: { resource },
-          });
-        },
+        refetchQueries: () => this.postRefreshQueries(),
       });
       this.createLinkResourceModal = false;
       this.createResourceModal = false;
@@ -429,6 +414,19 @@ export default class Resources extends Mixins(ResourceMixin) {
     });
   }
 
+  // eslint-disable-next-line class-methods-use-this
+  private postRefreshQueries(): RefetchQueryDescription {
+    return [
+      {
+        query: GET_RESOURCE,
+        variables: {
+          path: this.actualPath,
+          username: this.$route.params.preferredUsername,
+        },
+      },
+    ];
+  }
+
   async deleteResource(resourceID: string): Promise<void> {
     try {
       await this.$apollo.mutate({
@@ -436,37 +434,7 @@ export default class Resources extends Mixins(ResourceMixin) {
         variables: {
           id: resourceID,
         },
-        update: (store, { data: { deleteResource } }) => {
-          if (deleteResource == null) return;
-          if (!this.resource.actor) return;
-          const cachedData = store.readQuery<{ resource: IResource }>({
-            query: GET_RESOURCE,
-            variables: {
-              path: this.resource.path,
-              username: this.resource.actor.preferredUsername,
-            },
-          });
-          if (cachedData == null) return;
-          const { resource } = cachedData;
-          if (resource == null) {
-            console.error("Cannot update resource cache, because of null value.");
-            return;
-          }
-          const oldResource: IResource = deleteResource;
-
-          resource.children.elements = resource.children.elements.filter(
-            (resourceElement) => resourceElement.id !== oldResource.id
-          );
-
-          store.writeQuery({
-            query: GET_RESOURCE,
-            variables: {
-              path: this.resource.path,
-              username: this.resource.actor.preferredUsername,
-            },
-            data: { resource },
-          });
-        },
+        refetchQueries: () => this.postRefreshQueries(),
       });
       this.validCheckedResources = this.validCheckedResources.filter((id) => id !== resourceID);
       delete this.checkedResources[resourceID];
@@ -476,6 +444,7 @@ export default class Resources extends Mixins(ResourceMixin) {
   }
 
   handleRename(resource: IResource): void {
+    console.log("handleRename");
     this.renameModal = true;
     this.updatedResource = { ...resource };
   }
@@ -506,6 +475,7 @@ export default class Resources extends Mixins(ResourceMixin) {
           parentId: resource.parent ? resource.parent.id : null,
           path: resource.path,
         },
+        refetchQueries: () => this.postRefreshQueries(),
         update: (store, { data }) => {
           if (!data || data.updateResource == null || parentPath == null) return;
           if (!this.resource.actor) return;
@@ -577,9 +547,19 @@ export default class Resources extends Mixins(ResourceMixin) {
       console.error(e);
     }
   }
+
+  handleErrors(errors: any[]): void {
+    if (errors.some((error) => error.status_code === 404)) {
+      this.$router.replace({ name: RouteName.PAGE_NOT_FOUND });
+    }
+  }
 }
 </script>
 <style lang="scss" scoped>
+.container.section {
+  background: $white;
+}
+
 nav.breadcrumb ul {
   align-items: center;
 
diff --git a/lib/federation/activity_pub/activity_pub.ex b/lib/federation/activity_pub/activity_pub.ex
index 5791a47c4..4332bdcb1 100644
--- a/lib/federation/activity_pub/activity_pub.ex
+++ b/lib/federation/activity_pub/activity_pub.ex
@@ -15,6 +15,7 @@ defmodule Mobilizon.Federation.ActivityPub do
     Config,
     Discussions,
     Events,
+    Posts,
     Resources,
     Share,
     Users
@@ -88,6 +89,7 @@ defmodule Mobilizon.Federation.ActivityPub do
            {:existing, Discussions.get_discussion_by_url(url)},
          {:existing, nil} <- {:existing, Discussions.get_comment_from_url(url)},
          {:existing, nil} <- {:existing, Resources.get_resource_by_url(url)},
+         {:existing, nil} <- {:existing, Posts.get_post_by_url(url)},
          {:existing, nil} <-
            {:existing, Actors.get_actor_by_url_2(url)},
          {:existing, nil} <- {:existing, Actors.get_member_by_url(url)},
@@ -109,6 +111,9 @@ defmodule Mobilizon.Federation.ActivityPub do
 
               {:error, "Gone"} ->
                 {:error, "Gone", entity}
+
+              {:error, "Not found"} ->
+                {:error, "Not found", entity}
             end
           else
             {:ok, entity}
diff --git a/lib/federation/activity_pub/federator.ex b/lib/federation/activity_pub/federator.ex
index a419834e8..74b947afe 100644
--- a/lib/federation/activity_pub/federator.ex
+++ b/lib/federation/activity_pub/federator.ex
@@ -50,7 +50,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
 
   def handle(:incoming_ap_doc, params) do
     Logger.info("Handling incoming AP activity")
-    Logger.debug(inspect(params))
+    Logger.debug(inspect(Map.drop(params, ["@context"])))
 
     case Transmogrifier.handle_incoming(params) do
       {:ok, activity, _data} ->
diff --git a/lib/federation/activity_pub/fetcher.ex b/lib/federation/activity_pub/fetcher.ex
index 97ff31fd9..a6ece6a36 100644
--- a/lib/federation/activity_pub/fetcher.ex
+++ b/lib/federation/activity_pub/fetcher.ex
@@ -32,6 +32,10 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
         Logger.warn("Resource at #{url} is 410 Gone")
         {:error, "Gone"}
 
+      {:ok, %Tesla.Env{status: 404}} ->
+        Logger.warn("Resource at #{url} is 404 Gone")
+        {:error, "Not found"}
+
       {:ok, %Tesla.Env{} = res} ->
         {:error, res}
     end
diff --git a/lib/federation/activity_pub/preloader.ex b/lib/federation/activity_pub/preloader.ex
index dedcb0971..79db7a65a 100644
--- a/lib/federation/activity_pub/preloader.ex
+++ b/lib/federation/activity_pub/preloader.ex
@@ -4,10 +4,11 @@ defmodule Mobilizon.Federation.ActivityPub.Preloader do
   """
 
   # TODO: Move me in a more appropriate place
-  alias Mobilizon.{Actors, Discussions, Events, Resources}
+  alias Mobilizon.{Actors, Discussions, Events, Posts, Resources}
   alias Mobilizon.Actors.{Actor, Member}
   alias Mobilizon.Discussions.{Comment, Discussion}
   alias Mobilizon.Events.Event
+  alias Mobilizon.Posts.Post
   alias Mobilizon.Resources.Resource
   alias Mobilizon.Tombstone
 
@@ -23,6 +24,9 @@ defmodule Mobilizon.Federation.ActivityPub.Preloader do
   def maybe_preload(%Resource{url: url}),
     do: {:ok, Resources.get_resource_by_url_with_preloads(url)}
 
+  def maybe_preload(%Post{url: url}),
+    do: {:ok, Posts.get_post_by_url_with_preloads(url)}
+
   def maybe_preload(%Actor{url: url}), do: {:ok, Actors.get_actor_by_url!(url, true)}
 
   def maybe_preload(%Member{} = member), do: {:ok, member}
diff --git a/lib/federation/activity_pub/refresher.ex b/lib/federation/activity_pub/refresher.ex
index d825199da..2169e5212 100644
--- a/lib/federation/activity_pub/refresher.ex
+++ b/lib/federation/activity_pub/refresher.ex
@@ -118,7 +118,9 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
 
   defp process_collection(_, _), do: :error
 
-  defp handling_element(data) when is_map(data) do
+  # If we're handling an activity
+  defp handling_element(%{"type" => activity_type} = data)
+       when activity_type in ["Create", "Update", "Delete"] do
     object = get_in(data, ["object"])
 
     if object do
@@ -128,6 +130,26 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
     Transmogrifier.handle_incoming(data)
   end
 
+  # If we're handling directly an object
+  defp handling_element(data) when is_map(data) do
+    object = get_in(data, ["object"])
+
+    if object do
+      object |> Utils.get_url() |> Mobilizon.Tombstone.delete_uri_tombstone()
+    end
+
+    activity = %{
+      "type" => "Create",
+      "to" => data["to"],
+      "cc" => data["cc"],
+      "actor" => data["actor"] || data["attributedTo"],
+      "attributedTo" => data["attributedTo"] || data["actor"],
+      "object" => data
+    }
+
+    Transmogrifier.handle_incoming(activity)
+  end
+
   defp handling_element(uri) when is_binary(uri) do
     ActivityPub.fetch_object_from_url(uri)
   end
diff --git a/lib/federation/activity_pub/transmogrifier.ex b/lib/federation/activity_pub/transmogrifier.ex
index fb0830890..09e4559ae 100644
--- a/lib/federation/activity_pub/transmogrifier.ex
+++ b/lib/federation/activity_pub/transmogrifier.ex
@@ -381,12 +381,15 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
           update_data
       ) do
     with actor <- Utils.get_actor(update_data),
-         {:ok, %Actor{url: actor_url, suspended: false}} <-
+         {:ok, %Actor{url: actor_url, suspended: false} = actor} <-
            ActivityPub.get_or_fetch_actor_by_url(actor),
          {:ok, %Event{} = old_event} <-
            object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
          object_data <- Converter.Event.as_to_model_data(object),
-         {:origin_check, true} <- {:origin_check, Utils.origin_check?(actor_url, update_data)},
+         {:origin_check, true} <-
+           {:origin_check,
+            Utils.origin_check?(actor_url, update_data) ||
+              Utils.can_update_group_object?(actor, old_event)},
          {:ok, %Activity{} = activity, %Event{} = new_event} <-
            ActivityPub.update(old_event, object_data, false) do
       {:ok, activity, new_event}
@@ -418,6 +421,57 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
     end
   end
 
+  def handle_incoming(
+        %{"type" => "Update", "object" => %{"type" => "Article"} = object, "actor" => _actor} =
+          update_data
+      ) do
+    with actor <- Utils.get_actor(update_data),
+         {:ok, %Actor{url: actor_url, suspended: false} = actor} <-
+           ActivityPub.get_or_fetch_actor_by_url(actor),
+         {:ok, %Post{} = old_post} <-
+           object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
+         object_data <- Converter.Post.as_to_model_data(object),
+         {:origin_check, true} <-
+           {:origin_check,
+            Utils.origin_check?(actor_url, update_data["object"]) ||
+              Utils.can_update_group_object?(actor, old_post)},
+         {:ok, %Activity{} = activity, %Post{} = new_post} <-
+           ActivityPub.update(old_post, object_data, false) do
+      {:ok, activity, new_post}
+    else
+      {:origin_check, _} ->
+        Logger.warn("Actor tried to update a post but doesn't has the required role")
+        :error
+
+      _e ->
+        :error
+    end
+  end
+
+  def handle_incoming(
+        %{"type" => "Update", "object" => %{"type" => type} = object, "actor" => _actor} =
+          update_data
+      )
+      when type in ["ResourceCollection", "Document"] do
+    with actor <- Utils.get_actor(update_data),
+         {:ok, %Actor{url: actor_url, suspended: false}} <-
+           ActivityPub.get_or_fetch_actor_by_url(actor),
+         {:ok, %Resource{} = old_resource} <-
+           object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
+         object_data <- Converter.Resource.as_to_model_data(object),
+         {:origin_check, true} <-
+           {:origin_check,
+            Utils.origin_check?(actor_url, update_data) ||
+              Utils.can_update_group_object?(actor, old_resource)},
+         {:ok, %Activity{} = activity, %Resource{} = new_resource} <-
+           ActivityPub.update(old_resource, object_data, false) do
+      {:ok, activity, new_resource}
+    else
+      _e ->
+        :error
+    end
+  end
+
   def handle_incoming(
         %{"type" => "Update", "object" => %{"type" => "Member"} = object, "actor" => _actor} =
           update_data
@@ -505,11 +559,11 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
     with actor_url <- Utils.get_actor(data),
          {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor_url),
          object_id <- Utils.get_url(object),
-         {:error, "Gone", object} <- ActivityPub.fetch_object_from_url(object_id, force: true),
+         {:ok, object} <- is_group_object_gone(object_id),
          {:origin_check, true} <-
            {:origin_check,
             Utils.origin_check_from_id?(actor_url, object_id) ||
-              Utils.activity_actor_is_group_member?(actor, object)},
+              Utils.can_delete_group_object?(actor, object)},
          {:ok, activity, object} <- ActivityPub.delete(object, actor, false) do
       {:ok, activity, object}
     else
@@ -523,6 +577,29 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
     end
   end
 
+  def handle_incoming(
+        %{"type" => "Move", "object" => %{"type" => type} = object, "actor" => _actor} = data
+      )
+      when type in ["ResourceCollection", "Document"] do
+    with actor <- Utils.get_actor(data),
+         {:ok, %Actor{url: actor_url, suspended: false} = actor} <-
+           ActivityPub.get_or_fetch_actor_by_url(actor),
+         {:ok, %Resource{} = old_resource} <-
+           object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
+         object_data <- Converter.Resource.as_to_model_data(object),
+         {:origin_check, true} <-
+           {:origin_check,
+            Utils.origin_check?(actor_url, data) ||
+              Utils.can_update_group_object?(actor, old_resource)},
+         {:ok, activity, new_resource} <- ActivityPub.move(:resource, old_resource, object_data) do
+      {:ok, activity, new_resource}
+    else
+      e ->
+        Logger.error(inspect(e))
+        :error
+    end
+  end
+
   def handle_incoming(
         %{
           "type" => "Join",
@@ -975,4 +1052,25 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
       fetch_object_optionnally_authenticated(url, actor)
     end
   end
+
+  defp is_group_object_gone(object_id) do
+    case ActivityPub.fetch_object_from_url(object_id, force: true) do
+      {:error, error_message, object} when error_message in ["Gone", "Not found"] ->
+        {:ok, object}
+
+      {:ok, %{url: url} = object} ->
+        if Utils.are_same_origin?(url, Endpoint.url()),
+          do: {:ok, object},
+          else: {:error, "Group object URL remote"}
+
+      {:error, {:error, err}} ->
+        {:error, err}
+
+      {:error, err} ->
+        {:error, err}
+
+      err ->
+        err
+    end
+  end
 end
diff --git a/lib/federation/activity_pub/types/actors.ex b/lib/federation/activity_pub/types/actors.ex
index 77dfc8871..bc147b9d8 100644
--- a/lib/federation/activity_pub/types/actors.ex
+++ b/lib/federation/activity_pub/types/actors.ex
@@ -88,6 +88,9 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
 
   def group_actor(%Actor{} = actor), do: actor
 
+  def role_needed_to_update(%Actor{} = _group), do: :administrator
+  def role_needed_to_delete(%Actor{} = _group), do: :administrator
+
   defp prepare_args_for_actor(args) do
     args
     |> maybe_sanitize_username()
diff --git a/lib/federation/activity_pub/types/comments.ex b/lib/federation/activity_pub/types/comments.ex
index 1c1f04f6d..019a2958a 100644
--- a/lib/federation/activity_pub/types/comments.ex
+++ b/lib/federation/activity_pub/types/comments.ex
@@ -98,6 +98,9 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
 
   def group_actor(_), do: nil
 
+  def role_needed_to_update(%Comment{attributed_to: %Actor{} = _group}), do: :administrator
+  def role_needed_to_delete(%Comment{attributed_to_id: _attributed_to_id}), do: :administrator
+
   # Prepare and sanitize arguments for comments
   defp prepare_args_for_comment(args) do
     with in_reply_to_comment <-
diff --git a/lib/federation/activity_pub/types/discussions.ex b/lib/federation/activity_pub/types/discussions.ex
index 0d5248f0e..57eb77033 100644
--- a/lib/federation/activity_pub/types/discussions.ex
+++ b/lib/federation/activity_pub/types/discussions.ex
@@ -89,6 +89,9 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
 
   def group_actor(%Discussion{actor_id: actor_id}), do: Actors.get_actor(actor_id)
 
+  def role_needed_to_update(%Discussion{}), do: :moderator
+  def role_needed_to_delete(%Discussion{}), do: :moderator
+
   @spec maybe_publish_graphql_subscription(Discussion.t()) :: :ok
   defp maybe_publish_graphql_subscription(%Discussion{} = discussion) do
     Absinthe.Subscription.publish(Endpoint, discussion,
diff --git a/lib/federation/activity_pub/types/entity.ex b/lib/federation/activity_pub/types/entity.ex
index ffb3974c4..5aa3253aa 100644
--- a/lib/federation/activity_pub/types/entity.ex
+++ b/lib/federation/activity_pub/types/entity.ex
@@ -57,6 +57,8 @@ defprotocol Mobilizon.Federation.ActivityPub.Types.Managable do
 end
 
 defprotocol Mobilizon.Federation.ActivityPub.Types.Ownable do
+  @type group_role :: :member | :moderator | :administrator | nil
+
   @spec group_actor(Entity.t()) :: Actor.t() | nil
   @doc "Returns an eventual group for the entity"
   def group_actor(entity)
@@ -64,6 +66,12 @@ defprotocol Mobilizon.Federation.ActivityPub.Types.Ownable do
   @spec actor(Entity.t()) :: Actor.t() | nil
   @doc "Returns the actor for the entity"
   def actor(entity)
+
+  @spec role_needed_to_update(Entity.t()) :: group_role()
+  def role_needed_to_update(entity)
+
+  @spec role_needed_to_delete(Entity.t()) :: group_role()
+  def role_needed_to_delete(entity)
 end
 
 defimpl Managable, for: Event do
@@ -74,6 +82,8 @@ end
 defimpl Ownable, for: Event do
   defdelegate group_actor(entity), to: Events
   defdelegate actor(entity), to: Events
+  defdelegate role_needed_to_update(entity), to: Events
+  defdelegate role_needed_to_delete(entity), to: Events
 end
 
 defimpl Managable, for: Comment do
@@ -84,6 +94,8 @@ end
 defimpl Ownable, for: Comment do
   defdelegate group_actor(entity), to: Comments
   defdelegate actor(entity), to: Comments
+  defdelegate role_needed_to_update(entity), to: Comments
+  defdelegate role_needed_to_delete(entity), to: Comments
 end
 
 defimpl Managable, for: Post do
@@ -94,6 +106,8 @@ end
 defimpl Ownable, for: Post do
   defdelegate group_actor(entity), to: Posts
   defdelegate actor(entity), to: Posts
+  defdelegate role_needed_to_update(entity), to: Posts
+  defdelegate role_needed_to_delete(entity), to: Posts
 end
 
 defimpl Managable, for: Actor do
@@ -104,6 +118,8 @@ end
 defimpl Ownable, for: Actor do
   defdelegate group_actor(entity), to: Actors
   defdelegate actor(entity), to: Actors
+  defdelegate role_needed_to_update(entity), to: Actors
+  defdelegate role_needed_to_delete(entity), to: Actors
 end
 
 defimpl Managable, for: TodoList do
@@ -114,6 +130,8 @@ end
 defimpl Ownable, for: TodoList do
   defdelegate group_actor(entity), to: TodoLists
   defdelegate actor(entity), to: TodoLists
+  defdelegate role_needed_to_update(entity), to: TodoLists
+  defdelegate role_needed_to_delete(entity), to: TodoLists
 end
 
 defimpl Managable, for: Todo do
@@ -124,6 +142,8 @@ end
 defimpl Ownable, for: Todo do
   defdelegate group_actor(entity), to: Todos
   defdelegate actor(entity), to: Todos
+  defdelegate role_needed_to_update(entity), to: Todos
+  defdelegate role_needed_to_delete(entity), to: Todos
 end
 
 defimpl Managable, for: Resource do
@@ -134,6 +154,8 @@ end
 defimpl Ownable, for: Resource do
   defdelegate group_actor(entity), to: Resources
   defdelegate actor(entity), to: Resources
+  defdelegate role_needed_to_update(entity), to: Resources
+  defdelegate role_needed_to_delete(entity), to: Resources
 end
 
 defimpl Managable, for: Discussion do
@@ -144,11 +166,15 @@ end
 defimpl Ownable, for: Discussion do
   defdelegate group_actor(entity), to: Discussions
   defdelegate actor(entity), to: Discussions
+  defdelegate role_needed_to_update(entity), to: Discussions
+  defdelegate role_needed_to_delete(entity), to: Discussions
 end
 
 defimpl Ownable, for: Tombstone do
   defdelegate group_actor(entity), to: Tombstones
   defdelegate actor(entity), to: Tombstones
+  defdelegate role_needed_to_update(entity), to: Tombstones
+  defdelegate role_needed_to_delete(entity), to: Tombstones
 end
 
 defimpl Managable, for: Member do
diff --git a/lib/federation/activity_pub/types/events.ex b/lib/federation/activity_pub/types/events.ex
index febfd2a25..2cd0a9d5e 100644
--- a/lib/federation/activity_pub/types/events.ex
+++ b/lib/federation/activity_pub/types/events.ex
@@ -88,6 +88,10 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
 
   def group_actor(_), do: nil
 
+  def role_needed_to_update(%Event{attributed_to: %Actor{} = _group}), do: :moderator
+  def role_needed_to_delete(%Event{attributed_to_id: _attributed_to_id}), do: :moderator
+  def role_needed_to_delete(_), do: nil
+
   def join(%Event{} = event, %Actor{} = actor, _local, additional) do
     with {:maximum_attendee_capacity, true} <-
            {:maximum_attendee_capacity, check_attendee_capacity(event)},
diff --git a/lib/federation/activity_pub/types/posts.ex b/lib/federation/activity_pub/types/posts.ex
index f7cd1104a..bddfaf475 100644
--- a/lib/federation/activity_pub/types/posts.ex
+++ b/lib/federation/activity_pub/types/posts.ex
@@ -1,6 +1,6 @@
 defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
   @moduledoc false
-  alias Mobilizon.{Actors, Posts}
+  alias Mobilizon.{Actors, Posts, Tombstone}
   alias Mobilizon.Actors.Actor
   alias Mobilizon.Federation.ActivityPub.Types.Entity
   alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
@@ -11,6 +11,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
 
   @behaviour Entity
 
+  @public_ap "https://www.w3.org/ns/activitystreams#Public"
+
   @impl Entity
   def create(args, additional) do
     with args <- Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1),
@@ -42,7 +44,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
          {:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <-
            Posts.update_post(post, args),
          {:ok, true} <- Cachex.del(:activity_pub, "post_#{post.slug}"),
-         {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
+         {:ok, %Actor{url: group_url} = group} <- Actors.get_group_by_actor_id(group_id),
          %Actor{url: creator_url} = creator <- Actors.get_actor(creator_id),
          post_as_data <-
            Convertible.model_to_as(%{post | attributed_to: group, author: creator}),
@@ -50,7 +52,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
            "to" => [group.members_url],
            "cc" => [],
            "actor" => creator_url,
-           "attributedTo" => [creator_url]
+           "attributedTo" => [group_url]
          } do
       update_data = make_update_data(post_as_data, Map.merge(audience, additional))
 
@@ -66,7 +68,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
   def delete(
         %Post{
           url: url,
-          attributed_to: %Actor{url: group_url}
+          attributed_to: %Actor{url: group_url, members_url: members_url}
         } = post,
         %Actor{url: actor_url} = actor,
         _local,
@@ -77,11 +79,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
       "type" => "Delete",
       "object" => Convertible.model_to_as(post),
       "id" => url <> "/delete",
-      "to" => [group_url]
+      "to" => [group_url, @public_ap, members_url]
     }
 
-    with {:ok, _post} <- Posts.delete_post(post),
-         {:ok, true} <- Cachex.del(:activity_pub, "post_#{post.slug}") do
+    with {:ok, %Post{} = post} <- Posts.delete_post(post),
+         {:ok, true} <- Cachex.del(:activity_pub, "post_#{post.slug}"),
+         {:ok, %Tombstone{} = _tombstone} <-
+           Tombstone.create_tombstone(%{uri: post.url, actor_id: actor.id}) do
       {:ok, activity_data, actor, post}
     end
   end
@@ -91,4 +95,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
 
   def group_actor(%Post{attributed_to_id: attributed_to_id}),
     do: Actors.get_actor(attributed_to_id)
+
+  def role_needed_to_update(%Post{}), do: :moderator
+  def role_needed_to_delete(%Post{}), do: :moderator
 end
diff --git a/lib/federation/activity_pub/types/resources.ex b/lib/federation/activity_pub/types/resources.ex
index 025b8af21..c57afcb86 100644
--- a/lib/federation/activity_pub/types/resources.ex
+++ b/lib/federation/activity_pub/types/resources.ex
@@ -155,4 +155,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
     do: Actors.get_actor(creator_id)
 
   def group_actor(%Resource{actor_id: actor_id}), do: Actors.get_actor(actor_id)
+
+  def role_needed_to_update(%Resource{}), do: :member
+  def role_needed_to_delete(%Resource{}), do: :member
 end
diff --git a/lib/federation/activity_pub/types/todo_lists.ex b/lib/federation/activity_pub/types/todo_lists.ex
index b2b170f16..1b9bc590b 100644
--- a/lib/federation/activity_pub/types/todo_lists.ex
+++ b/lib/federation/activity_pub/types/todo_lists.ex
@@ -67,4 +67,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do
   def actor(%TodoList{}), do: nil
 
   def group_actor(%TodoList{actor_id: actor_id}), do: Actors.get_actor(actor_id)
+
+  def role_needed_to_update(%TodoList{}), do: :member
+  def role_needed_to_delete(%TodoList{}), do: :member
 end
diff --git a/lib/federation/activity_pub/types/todos.ex b/lib/federation/activity_pub/types/todos.ex
index 50e573eb9..dab23ca95 100644
--- a/lib/federation/activity_pub/types/todos.ex
+++ b/lib/federation/activity_pub/types/todos.ex
@@ -79,4 +79,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
         nil
     end
   end
+
+  def role_needed_to_update(%Todo{}), do: :member
+  def role_needed_to_delete(%Todo{}), do: :member
 end
diff --git a/lib/federation/activity_pub/types/tombstones.ex b/lib/federation/activity_pub/types/tombstones.ex
index b3f36cb6e..0787d47be 100644
--- a/lib/federation/activity_pub/types/tombstones.ex
+++ b/lib/federation/activity_pub/types/tombstones.ex
@@ -11,4 +11,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Tombstones do
   def actor(_), do: nil
 
   def group_actor(_), do: nil
+
+  def role_needed_to_update(%Actor{}), do: nil
+  def role_needed_to_delete(%Actor{}), do: nil
 end
diff --git a/lib/federation/activity_pub/utils.ex b/lib/federation/activity_pub/utils.ex
index ee763bb32..6f9a64413 100644
--- a/lib/federation/activity_pub/utils.ex
+++ b/lib/federation/activity_pub/utils.ex
@@ -287,10 +287,41 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
   def origin_check_from_id?(id, %{"id" => other_id} = _params) when is_binary(other_id),
     do: origin_check_from_id?(id, other_id)
 
-  def activity_actor_is_group_member?(%Actor{id: actor_id}, object) do
+  def activity_actor_is_group_member?(
+        %Actor{id: actor_id, url: actor_url},
+        object,
+        role \\ :member
+      ) do
     case Ownable.group_actor(object) do
-      %Actor{type: :Group, id: group_id} ->
-        Actors.is_member?(actor_id, group_id)
+      %Actor{type: :Group, id: group_id, url: group_url} ->
+        Logger.debug("Group object url is #{group_url}")
+
+        case role do
+          :moderator ->
+            Logger.debug(
+              "Checking if activity actor #{actor_url} is a moderator from group from #{
+                object.url
+              }"
+            )
+
+            Actors.is_moderator?(actor_id, group_id)
+
+          :administrator ->
+            Logger.debug(
+              "Checking if activity actor #{actor_url} is an administrator from group from #{
+                object.url
+              }"
+            )
+
+            Actors.is_administrator?(actor_id, group_id)
+
+          _ ->
+            Logger.debug(
+              "Checking if activity actor #{actor_url} is a member from group from #{object.url}"
+            )
+
+            Actors.is_member?(actor_id, group_id)
+        end
 
       _ ->
         false
@@ -628,4 +659,39 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
 
     :ok
   end
+
+  def can_update_group_object?(%Actor{} = actor, object) do
+    can_manage_group_object?(:role_needed_to_update, actor, object)
+  end
+
+  def can_delete_group_object?(%Actor{} = actor, object) do
+    can_manage_group_object?(:role_needed_to_delete, actor, object)
+  end
+
+  @spec can_manage_group_object?(
+          :role_needed_to_update | :role_needed_to_delete,
+          Actor.t(),
+          any()
+        ) :: boolean()
+  defp can_manage_group_object?(action_function, %Actor{url: actor_url} = actor, object) do
+    if Ownable.group_actor(object) != nil do
+      case apply(Ownable, action_function, [object]) do
+        role when role in [:member, :moderator, :administrator] ->
+          activity_actor_is_group_member?(actor, object, role)
+
+        _ ->
+          case action_function do
+            :role_needed_to_update ->
+              Logger.warn("Actor #{actor_url} can't update #{object.url}")
+
+            :role_needed_to_delete ->
+              Logger.warn("Actor #{actor_url} can't delete #{object.url}")
+          end
+
+          false
+      end
+    else
+      true
+    end
+  end
 end
diff --git a/lib/graphql/error.ex b/lib/graphql/error.ex
index 2e20a228b..6d5def169 100644
--- a/lib/graphql/error.ex
+++ b/lib/graphql/error.ex
@@ -88,6 +88,7 @@ defmodule Mobilizon.GraphQL.Error do
   defp metadata(:post_not_found), do: {404, dgettext("errors", "Post not found")}
   defp metadata(:event_not_found), do: {404, dgettext("errors", "Event not found")}
   defp metadata(:group_not_found), do: {404, dgettext("errors", "Group not found")}
+  defp metadata(:resource_not_found), do: {404, dgettext("errors", "Resource not found")}
   defp metadata(:unknown), do: {500, dgettext("errors", "Something went wrong")}
 
   defp metadata(code) do
diff --git a/lib/graphql/resolvers/post.ex b/lib/graphql/resolvers/post.ex
index 5f98a72e0..abd9666fc 100644
--- a/lib/graphql/resolvers/post.ex
+++ b/lib/graphql/resolvers/post.ex
@@ -149,7 +149,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
         } = _resolution
       ) do
     with {:uuid, {:ok, _uuid}} <- {:uuid, Ecto.UUID.cast(id)},
-         %Actor{id: actor_id} <- Users.get_actor_for_user(user),
+         %Actor{id: actor_id, url: actor_url} <- Users.get_actor_for_user(user),
          {:post, %Post{attributed_to: %Actor{id: group_id} = group} = post} <-
            {:post, Posts.get_post_with_preloads(id)},
          args <-
@@ -158,7 +158,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
            end),
          {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
          {:ok, _, %Post{} = post} <-
-           ActivityPub.update(post, args, true, %{}) do
+           ActivityPub.update(post, args, true, %{"actor" => actor_url}) do
       {:ok, post}
     else
       {:uuid, :error} ->
diff --git a/lib/graphql/resolvers/resource.ex b/lib/graphql/resolvers/resource.ex
index 29011a526..d68439f58 100644
--- a/lib/graphql/resolvers/resource.ex
+++ b/lib/graphql/resolvers/resource.ex
@@ -83,8 +83,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
            {:resource, Resources.get_resource_by_group_and_path_with_preloads(group_id, path)} do
       {:ok, resource}
     else
+      {:group, _} -> {:error, :group_not_found}
       {:member, false} -> {:error, dgettext("errors", "Profile is not member of group")}
-      {:resource, _} -> {:error, dgettext("errors", "No such resource")}
+      {:resource, _} -> {:error, :resource_not_found}
     end
   end
 
@@ -137,12 +138,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
           }
         } = _resolution
       ) do
-    with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
+    with %Actor{id: actor_id, url: actor_url} <- Users.get_actor_for_user(user),
          {:resource, %Resource{actor_id: group_id} = resource} <-
            {:resource, Resources.get_resource_with_preloads(resource_id)},
          {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
          {:ok, _, %Resource{} = resource} <-
-           ActivityPub.update(resource, args, true, %{}) do
+           ActivityPub.update(resource, args, true, %{"actor" => actor_url}) do
       {:ok, resource}
     else
       {:resource, _} ->
@@ -195,8 +196,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
           }
         } = _resolution
       ) do
-    with {:ok, data} when is_map(data) <- Parser.parse(resource_url) do
-      {:ok, struct(Metadata, data)}
+    case Parser.parse(resource_url) do
+      {:ok, data} when is_map(data) ->
+        {:ok, struct(Metadata, data)}
+
+      {:error, _err} ->
+        Logger.warn("Error while fetching preview from #{inspect(resource_url)}")
+        {:error, :unknown_resource}
     end
   end
 
diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex
index 92d40e9b5..7270c6d61 100644
--- a/lib/mobilizon/events/events.ex
+++ b/lib/mobilizon/events/events.ex
@@ -406,7 +406,7 @@ defmodule Mobilizon.Events do
   def list_public_events_for_actor(actor, page \\ nil, limit \\ nil)
 
   def list_public_events_for_actor(%Actor{type: :Group} = group, page, limit),
-    do: list_organized_events_for_group(group, :public, nil, page, limit)
+    do: list_organized_events_for_group(group, :public, nil, nil, page, limit)
 
   def list_public_events_for_actor(%Actor{id: actor_id}, page, limit) do
     actor_id
diff --git a/lib/service/http/rich_media_preview_client.ex b/lib/service/http/rich_media_preview_client.ex
new file mode 100644
index 000000000..edd190818
--- /dev/null
+++ b/lib/service/http/rich_media_preview_client.ex
@@ -0,0 +1,23 @@
+defmodule Mobilizon.Service.HTTP.RichMediaPreviewClient do
+  @moduledoc """
+  Tesla HTTP Basic Client
+  with JSON middleware
+  """
+
+  use Tesla
+  alias Mobilizon.Config
+
+  @default_opts [
+    recv_timeout: 20_000
+  ]
+
+  adapter(Tesla.Adapter.Hackney, @default_opts)
+
+  @user_agent Config.instance_user_agent()
+
+  plug(Tesla.Middleware.FollowRedirects)
+
+  plug(Tesla.Middleware.Timeout, timeout: 10_000)
+
+  plug(Tesla.Middleware.Headers, [{"User-Agent", @user_agent}])
+end
diff --git a/lib/service/rich_media/parser.ex b/lib/service/rich_media/parser.ex
index fa16a328c..12face84b 100644
--- a/lib/service/rich_media/parser.ex
+++ b/lib/service/rich_media/parser.ex
@@ -17,6 +17,7 @@ defmodule Mobilizon.Service.RichMedia.Parser do
   ]
 
   alias Mobilizon.Config
+  alias Mobilizon.Service.HTTP.RichMediaPreviewClient
   alias Mobilizon.Service.RichMedia.Favicon
   alias Plug.Conn.Utils
   require Logger
@@ -56,7 +57,7 @@ defmodule Mobilizon.Service.RichMedia.Parser do
       with {:ok, _} <- prevent_local_address(url),
            {:ok, %{body: body, status: code, headers: response_headers}}
            when code in 200..299 <-
-             Tesla.get(
+             RichMediaPreviewClient.get(
                url,
                headers: headers,
                opts: @options
@@ -188,8 +189,11 @@ defmodule Mobilizon.Service.RichMedia.Parser do
   defp maybe_parse(html) do
     Enum.reduce_while(parsers(), %{}, fn parser, acc ->
       case parser.parse(html, acc) do
-        {:ok, data} -> {:halt, data}
-        {:error, _msg} -> {:cont, acc}
+        {:ok, data} ->
+          {:halt, data}
+
+        {:error, _msg} ->
+          {:cont, acc}
       end
     end)
   end
@@ -237,7 +241,7 @@ defmodule Mobilizon.Service.RichMedia.Parser do
         !String.ends_with?(hostname, ".localhost")
 
   defp validate_hostname_only(hostname),
-    do: hostname |> String.graphemes() |> Enum.count(&(&1 == "o")) > 0
+    do: hostname |> String.graphemes() |> Enum.count(&(&1 == ".")) > 0
 
   defp validate_ip(hostname) do
     case hostname |> String.to_charlist() |> :inet.parse_address() do
diff --git a/lib/service/rich_media/parsers/oembed_parser.ex b/lib/service/rich_media/parsers/oembed_parser.ex
index e1e3c19f8..dcc2d3e0b 100644
--- a/lib/service/rich_media/parsers/oembed_parser.ex
+++ b/lib/service/rich_media/parsers/oembed_parser.ex
@@ -21,7 +21,7 @@ defmodule Mobilizon.Service.RichMedia.Parsers.OEmbed do
     with elements = [_ | _] <- get_discovery_data(html),
          {:ok, oembed_url} <- get_oembed_url(elements),
          {:ok, oembed_data} <- get_oembed_data(oembed_url),
-         oembed_data <- filter_oembed_data(oembed_data) do
+         {:ok, oembed_data} <- filter_oembed_data(oembed_data) do
       Logger.debug("Data found with OEmbed parser")
       Logger.debug(inspect(oembed_data))
       {:ok, oembed_data}
@@ -55,7 +55,9 @@ defmodule Mobilizon.Service.RichMedia.Parsers.OEmbed do
         {:error, "No type declared for OEmbed data"}
 
       "link" ->
-        Map.put(data, :image_remote_url, Map.get(data, :thumbnail_url))
+        data
+        |> Map.put(:image_remote_url, Map.get(data, :thumbnail_url))
+        |> (&{:ok, &1}).()
 
       "photo" ->
         if Map.get(data, :url, "") == "" do
@@ -65,6 +67,7 @@ defmodule Mobilizon.Service.RichMedia.Parsers.OEmbed do
           |> Map.put(:image_remote_url, Map.get(data, :url))
           |> Map.put(:width, Map.get(data, :width, 0))
           |> Map.put(:height, Map.get(data, :height, 0))
+          |> (&{:ok, &1}).()
         end
 
       "video" ->
@@ -75,6 +78,7 @@ defmodule Mobilizon.Service.RichMedia.Parsers.OEmbed do
         |> Map.put(:width, Map.get(data, :width, 0))
         |> Map.put(:height, Map.get(data, :height, 0))
         |> Map.put(:image_remote_url, Map.get(data, :thumbnail_url))
+        |> (&{:ok, &1}).()
 
       "rich" ->
         {:error, "OEmbed data has rich type, which we don't support"}
diff --git a/test/federation/activity_pub/transmogrifier/delete_test.exs b/test/federation/activity_pub/transmogrifier/delete_test.exs
new file mode 100644
index 000000000..30717266a
--- /dev/null
+++ b/test/federation/activity_pub/transmogrifier/delete_test.exs
@@ -0,0 +1,317 @@
+defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.DeleteTest do
+  use Mobilizon.DataCase
+  use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
+  use Oban.Testing, repo: Mobilizon.Storage.Repo
+  import Mobilizon.Factory
+  import ExUnit.CaptureLog
+  import Mox
+
+  alias Mobilizon.{Actors, Discussions, Events, Posts, Resources}
+  alias Mobilizon.Actors.Actor
+  alias Mobilizon.Discussions.Comment
+  alias Mobilizon.Events.Event
+  alias Mobilizon.Posts.Post
+  alias Mobilizon.Resources.Resource
+  alias Mobilizon.Federation.ActivityPub.{Activity, Transmogrifier}
+  alias Mobilizon.Federation.ActivityStream.Convertible
+  alias Mobilizon.Service.HTTP.ActivityPub.Mock
+
+  describe "handle incoming delete activities" do
+    test "it works for incoming deletes" do
+      %Actor{url: actor_url} =
+        actor = insert(:actor, url: "http://mobilizon.tld/@remote", domain: "mobilizon.tld")
+
+      %Comment{url: comment_url} =
+        insert(:comment,
+          actor: nil,
+          actor_id: actor.id,
+          url: "http://mobilizon.tld/comments/9f3794b8-11a0-4a98-8cb7-475ab332c701"
+        )
+
+      Mock
+      |> expect(:call, fn
+        %{method: :get, url: "http://mobilizon.tld/comments/9f3794b8-11a0-4a98-8cb7-475ab332c701"},
+        _opts ->
+          {:ok, %Tesla.Env{status: 410, body: "Gone"}}
+      end)
+
+      data =
+        File.read!("test/fixtures/mastodon-delete.json")
+        |> Jason.decode!()
+
+      object =
+        data["object"]
+        |> Map.put("id", comment_url)
+
+      data =
+        data
+        |> Map.put("object", object)
+        |> Map.put("actor", actor_url)
+
+      assert Discussions.get_comment_from_url(comment_url)
+      assert is_nil(Discussions.get_comment_from_url(comment_url).deleted_at)
+
+      {:ok, %Activity{local: false}, _} = Transmogrifier.handle_incoming(data)
+
+      refute is_nil(Discussions.get_comment_from_url(comment_url).deleted_at)
+    end
+
+    test "it fails for incoming deletes with spoofed origin" do
+      comment = insert(:comment)
+
+      announce_data =
+        File.read!("test/fixtures/mastodon-announce.json")
+        |> Jason.decode!()
+        |> Map.put("object", comment.url)
+
+      {:ok, _, _} = Transmogrifier.handle_incoming(announce_data)
+
+      data =
+        File.read!("test/fixtures/mastodon-delete.json")
+        |> Jason.decode!()
+
+      object =
+        data["object"]
+        |> Map.put("id", comment.url)
+
+      data =
+        data
+        |> Map.put("object", object)
+
+      :error = Transmogrifier.handle_incoming(data)
+
+      assert Discussions.get_comment_from_url(comment.url)
+    end
+
+    setup :set_mox_from_context
+
+    test "it works for incoming actor deletes" do
+      %Actor{url: url} =
+        actor = insert(:actor, url: "https://framapiaf.org/users/admin", domain: "framapiaf.org")
+
+      %Event{url: event1_url} = event1 = insert(:event, organizer_actor: actor)
+      insert(:event, organizer_actor: actor)
+
+      %Comment{url: comment1_url} = comment1 = insert(:comment, actor: actor)
+      insert(:comment, actor: actor)
+
+      data =
+        File.read!("test/fixtures/mastodon-delete-user.json")
+        |> Jason.decode!()
+
+      Mock
+      |> expect(:call, fn
+        %{method: :get, url: "https://framapiaf.org/users/admin"}, _opts ->
+          {:ok, %Tesla.Env{status: 410, body: "Gone"}}
+      end)
+
+      {:ok, _activity, _actor} = Transmogrifier.handle_incoming(data)
+      assert %{success: 1, failure: 0} == Oban.drain_queue(queue: :background)
+
+      assert {:error, :actor_not_found} = Actors.get_actor_by_url(url)
+      assert {:error, :event_not_found} = Events.get_event(event1.id)
+      # Tombstone are cascade deleted, seems correct for now
+      # assert %Tombstone{} = Tombstone.find_tombstone(event1_url)
+      assert %Comment{deleted_at: deleted_at} = Discussions.get_comment(comment1.id)
+      refute is_nil(deleted_at)
+      # assert %Tombstone{} = Tombstone.find_tombstone(comment1_url)
+    end
+
+    test "it fails for incoming actor deletes with spoofed origin" do
+      %{url: url} = insert(:actor)
+      deleted_actor_url = "https://framapiaf.org/users/admin"
+
+      data =
+        File.read!("test/fixtures/mastodon-delete-user.json")
+        |> Jason.decode!()
+        |> Map.put("actor", url)
+
+      deleted_actor_data =
+        File.read!("test/fixtures/mastodon-actor.json")
+        |> Jason.decode!()
+        |> Map.put("id", deleted_actor_url)
+
+      Mock
+      |> expect(:call, fn
+        %{url: ^deleted_actor_url}, _opts ->
+          {:ok, %Tesla.Env{status: 200, body: deleted_actor_data}}
+      end)
+
+      assert capture_log(fn ->
+               assert :error == Transmogrifier.handle_incoming(data)
+             end) =~ "Object origin check failed"
+
+      assert Actors.get_actor_by_url(url)
+    end
+  end
+
+  describe "handle incoming delete activities for group posts" do
+    test "works for remote deletions by moderators" do
+      %Actor{url: remote_actor_url} =
+        remote_actor =
+        insert(:actor,
+          domain: "remote.domain",
+          url: "https://remote.domain/@remote",
+          preferred_username: "remote"
+        )
+
+      group = insert(:group)
+      insert(:member, actor: remote_actor, parent: group, role: :moderator)
+      %Post{} = post = insert(:post, attributed_to: group)
+
+      data = Convertible.model_to_as(post)
+      refute is_nil(Posts.get_post_by_url(data["id"]))
+
+      delete_data =
+        File.read!("test/fixtures/mastodon-delete.json")
+        |> Jason.decode!()
+
+      object =
+        data
+        |> Map.put("type", "Article")
+
+      delete_data =
+        delete_data
+        |> Map.put("actor", remote_actor_url)
+        |> Map.put("object", object)
+
+      {:ok, _activity, _actor} = Transmogrifier.handle_incoming(delete_data)
+
+      assert is_nil(Posts.get_post_by_url(data["id"]))
+    end
+
+    test "doesn't work for remote deletions if the actor is just a group member" do
+      %Actor{url: remote_actor_url} =
+        remote_actor =
+        insert(:actor,
+          domain: "remote.domain",
+          url: "https://remote.domain/@remote",
+          preferred_username: "remote"
+        )
+
+      group = insert(:group)
+      insert(:member, actor: remote_actor, parent: group, role: :member)
+      %Post{} = post = insert(:post, attributed_to: group)
+
+      data = Convertible.model_to_as(post)
+      refute is_nil(Posts.get_post_by_url(data["id"]))
+
+      delete_data =
+        File.read!("test/fixtures/mastodon-delete.json")
+        |> Jason.decode!()
+
+      object =
+        data
+        |> Map.put("type", "Article")
+
+      delete_data =
+        delete_data
+        |> Map.put("actor", remote_actor_url)
+        |> Map.put("object", object)
+
+      :error = Transmogrifier.handle_incoming(delete_data)
+
+      refute is_nil(Posts.get_post_by_url(data["id"]))
+    end
+
+    test "doesn't work for remote deletions if the actor is not a group member" do
+      %Actor{url: remote_actor_url} =
+        insert(:actor,
+          domain: "remote.domain",
+          url: "https://remote.domain/@remote",
+          preferred_username: "remote"
+        )
+
+      group = insert(:group)
+      %Post{} = post = insert(:post, attributed_to: group)
+
+      data = Convertible.model_to_as(post)
+      refute is_nil(Posts.get_post_by_url(data["id"]))
+
+      delete_data =
+        File.read!("test/fixtures/mastodon-delete.json")
+        |> Jason.decode!()
+
+      object =
+        data
+        |> Map.put("type", "Article")
+
+      delete_data =
+        delete_data
+        |> Map.put("actor", remote_actor_url)
+        |> Map.put("object", object)
+
+      :error = Transmogrifier.handle_incoming(delete_data)
+
+      refute is_nil(Posts.get_post_by_url(data["id"]))
+    end
+  end
+
+  describe "handle incoming delete activities for resources" do
+    test "works for remote deletions" do
+      %Actor{url: remote_actor_url} =
+        remote_actor =
+        insert(:actor,
+          domain: "remote.domain",
+          url: "http://remote.domain/@remote",
+          preferred_username: "remote"
+        )
+
+      group = insert(:group)
+      insert(:member, actor: remote_actor, parent: group, role: :member)
+      %Resource{} = resource = insert(:resource, actor: group)
+
+      data = Convertible.model_to_as(resource)
+      refute is_nil(Resources.get_resource_by_url(data["id"]))
+
+      delete_data =
+        File.read!("test/fixtures/mastodon-delete.json")
+        |> Jason.decode!()
+
+      object =
+        data
+        |> Map.put("type", "Document")
+
+      delete_data =
+        delete_data
+        |> Map.put("actor", remote_actor_url)
+        |> Map.put("object", object)
+
+      {:ok, _activity, _actor} = Transmogrifier.handle_incoming(delete_data)
+
+      assert is_nil(Resources.get_resource_by_url(data["id"]))
+    end
+
+    test "doesn't work for remote deletions if the actor is not a group member" do
+      %Actor{url: remote_actor_url} =
+        insert(:actor,
+          domain: "remote.domain",
+          url: "http://remote.domain/@remote",
+          preferred_username: "remote"
+        )
+
+      group = insert(:group)
+      %Post{} = post = insert(:post, attributed_to: group)
+
+      data = Convertible.model_to_as(post)
+      refute is_nil(Posts.get_post_by_url(data["id"]))
+
+      delete_data =
+        File.read!("test/fixtures/mastodon-delete.json")
+        |> Jason.decode!()
+
+      object =
+        data
+        |> Map.put("type", "Article")
+
+      delete_data =
+        delete_data
+        |> Map.put("actor", remote_actor_url)
+        |> Map.put("object", object)
+
+      :error = Transmogrifier.handle_incoming(delete_data)
+
+      refute is_nil(Posts.get_post_by_url(data["id"]))
+    end
+  end
+end
diff --git a/test/federation/activity_pub/transmogrifier/update_test.exs b/test/federation/activity_pub/transmogrifier/update_test.exs
new file mode 100644
index 000000000..be9786855
--- /dev/null
+++ b/test/federation/activity_pub/transmogrifier/update_test.exs
@@ -0,0 +1,227 @@
+defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.UpdateTest do
+  use Mobilizon.DataCase
+  use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
+  use Oban.Testing, repo: Mobilizon.Storage.Repo
+  import Mobilizon.Factory
+
+  alias Mobilizon.{Actors, Events, Posts}
+  alias Mobilizon.Actors.{Actor, Member}
+  alias Mobilizon.Events.Event
+  alias Mobilizon.Posts.Post
+  alias Mobilizon.Federation.ActivityPub.{Activity, Transmogrifier}
+  alias Mobilizon.Federation.ActivityStream.Convertible
+
+  describe "handle incoming update activities" do
+    test "it works for incoming update activities on actors" do
+      use_cassette "activity_pub/update_actor_activity" do
+        data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
+
+        {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
+        update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
+
+        object =
+          update_data["object"]
+          |> Map.put("actor", data["actor"])
+          |> Map.put("id", data["actor"])
+
+        update_data =
+          update_data
+          |> Map.put("actor", data["actor"])
+          |> Map.put("object", object)
+
+        {:ok, %Activity{data: _data, local: false}, _} =
+          Transmogrifier.handle_incoming(update_data)
+
+        {:ok, %Actor{} = actor} = Actors.get_actor_by_url(update_data["actor"])
+        assert actor.name == "nextsoft"
+
+        assert actor.summary == "<p>Some bio</p>"
+      end
+    end
+
+    test "it works for incoming update activities on events" do
+      use_cassette "activity_pub/event_update_activities" do
+        data = File.read!("test/fixtures/mobilizon-post-activity.json") |> Jason.decode!()
+
+        {:ok, %Activity{data: data, local: false}, %Event{id: event_id}} =
+          Transmogrifier.handle_incoming(data)
+
+        assert_enqueued(
+          worker: Mobilizon.Service.Workers.BuildSearch,
+          args: %{event_id: event_id, op: :insert_search_event}
+        )
+
+        assert %{success: 1, failure: 0} == Oban.drain_queue(queue: :search)
+
+        update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
+
+        object =
+          data["object"]
+          |> Map.put("actor", data["actor"])
+          |> Map.put("name", "My updated event")
+          |> Map.put("id", data["object"]["id"])
+          |> Map.put("type", "Event")
+
+        update_data =
+          update_data
+          |> Map.put("actor", data["actor"])
+          |> Map.put("object", object)
+
+        {:ok, %Activity{data: data, local: false}, _} =
+          Transmogrifier.handle_incoming(update_data)
+
+        %Event{} = event = Events.get_event_by_url(data["object"]["id"])
+
+        assert_enqueued(
+          worker: Mobilizon.Service.Workers.BuildSearch,
+          args: %{event_id: event_id, op: :update_search_event}
+        )
+
+        assert %{success: 1, failure: 0} == Oban.drain_queue(queue: :search)
+
+        assert event.title == "My updated event"
+
+        assert event.description == data["object"]["content"]
+      end
+    end
+
+    #     test "it works for incoming update activities which lock the account" do
+    #       data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
+
+    #       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+    #       update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
+
+    #       object =
+    #         update_data["object"]
+    #         |> Map.put("actor", data["actor"])
+    #         |> Map.put("id", data["actor"])
+    #         |> Map.put("manuallyApprovesFollowers", true)
+
+    #       update_data =
+    #         update_data
+    #         |> Map.put("actor", data["actor"])
+    #         |> Map.put("object", object)
+
+    #       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data)
+
+    #       user = User.get_cached_by_ap_id(data["actor"])
+    #       assert user.info["locked"] == true
+    #     end
+  end
+
+  describe "handle incoming updates activities for group posts" do
+    test "it works for incoming update activities on group posts when remote actor is a moderator" do
+      use_cassette "activity_pub/group_post_update_activities" do
+        %Actor{url: remote_actor_url} =
+          remote_actor =
+          insert(:actor,
+            domain: "remote.domain",
+            url: "https://remote.domain/@remote",
+            preferred_username: "remote"
+          )
+
+        group = insert(:group)
+        %Member{} = member = insert(:member, actor: remote_actor, parent: group, role: :moderator)
+        %Post{} = post = insert(:post, attributed_to: group)
+
+        data = Convertible.model_to_as(post)
+        refute is_nil(Posts.get_post_by_url(data["id"]))
+
+        update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
+
+        object =
+          data
+          |> Map.put("name", "My updated post")
+          |> Map.put("type", "Article")
+
+        update_data =
+          update_data
+          |> Map.put("actor", remote_actor_url)
+          |> Map.put("object", object)
+
+        {:ok, %Activity{data: data, local: false}, _} =
+          Transmogrifier.handle_incoming(update_data)
+
+        %Post{id: updated_post_id, title: updated_post_title} =
+          Posts.get_post_by_url(data["object"]["id"])
+
+        assert updated_post_id == post.id
+        assert updated_post_title == "My updated post"
+      end
+    end
+
+    test "it works for incoming update activities on group posts" do
+      use_cassette "activity_pub/group_post_update_activities" do
+        %Actor{url: remote_actor_url} =
+          remote_actor =
+          insert(:actor,
+            domain: "remote.domain",
+            url: "https://remote.domain/@remote",
+            preferred_username: "remote"
+          )
+
+        group = insert(:group)
+        %Member{} = member = insert(:member, actor: remote_actor, parent: group)
+        %Post{} = post = insert(:post, attributed_to: group)
+
+        data = Convertible.model_to_as(post)
+        refute is_nil(Posts.get_post_by_url(data["id"]))
+
+        update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
+
+        object =
+          data
+          |> Map.put("name", "My updated post")
+          |> Map.put("type", "Article")
+
+        update_data =
+          update_data
+          |> Map.put("actor", remote_actor_url)
+          |> Map.put("object", object)
+
+        :error = Transmogrifier.handle_incoming(update_data)
+
+        %Post{id: updated_post_id, title: updated_post_title} = Posts.get_post_by_url(data["id"])
+
+        assert updated_post_id == post.id
+        refute updated_post_title == "My updated post"
+      end
+    end
+
+    test "it fails for incoming update activities on group posts when the actor is not a member from the group" do
+      use_cassette "activity_pub/group_post_update_activities" do
+        %Actor{url: remote_actor_url} =
+          insert(:actor,
+            domain: "remote.domain",
+            url: "https://remote.domain/@remote",
+            preferred_username: "remote"
+          )
+
+        group = insert(:group)
+        %Post{} = post = insert(:post, attributed_to: group)
+
+        data = Convertible.model_to_as(post)
+        refute is_nil(Posts.get_post_by_url(data["id"]))
+
+        update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
+
+        object =
+          data
+          |> Map.put("name", "My updated post")
+          |> Map.put("type", "Article")
+
+        update_data =
+          update_data
+          |> Map.put("actor", remote_actor_url)
+          |> Map.put("object", object)
+
+        :error = Transmogrifier.handle_incoming(update_data)
+
+        %Post{id: updated_post_id, title: updated_post_title} = Posts.get_post_by_url(data["id"])
+
+        assert updated_post_id == post.id
+        refute updated_post_title == "My updated post"
+      end
+    end
+  end
+end
diff --git a/test/federation/activity_pub/transmogrifier_test.exs b/test/federation/activity_pub/transmogrifier_test.exs
index cfee647c7..a6d7c808f 100644
--- a/test/federation/activity_pub/transmogrifier_test.exs
+++ b/test/federation/activity_pub/transmogrifier_test.exs
@@ -10,11 +10,10 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
   use Oban.Testing, repo: Mobilizon.Storage.Repo
 
   import Mobilizon.Factory
-  import ExUnit.CaptureLog
   import Mock
   import Mox
 
-  alias Mobilizon.{Actors, Discussions, Events}
+  alias Mobilizon.{Actors, Discussions}
   alias Mobilizon.Actors.Actor
   alias Mobilizon.Discussions.Comment
   alias Mobilizon.Events.Event
@@ -707,233 +706,6 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
     end
   end
 
-  describe "handle incoming update activities" do
-    test "it works for incoming update activities on actors" do
-      use_cassette "activity_pub/update_actor_activity" do
-        data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
-
-        {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
-        update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
-
-        object =
-          update_data["object"]
-          |> Map.put("actor", data["actor"])
-          |> Map.put("id", data["actor"])
-
-        update_data =
-          update_data
-          |> Map.put("actor", data["actor"])
-          |> Map.put("object", object)
-
-        {:ok, %Activity{data: _data, local: false}, _} =
-          Transmogrifier.handle_incoming(update_data)
-
-        {:ok, %Actor{} = actor} = Actors.get_actor_by_url(update_data["actor"])
-        assert actor.name == "nextsoft"
-
-        assert actor.summary == "<p>Some bio</p>"
-      end
-    end
-
-    test "it works for incoming update activities on events" do
-      use_cassette "activity_pub/event_update_activities" do
-        data = File.read!("test/fixtures/mobilizon-post-activity.json") |> Jason.decode!()
-
-        {:ok, %Activity{data: data, local: false}, %Event{id: event_id}} =
-          Transmogrifier.handle_incoming(data)
-
-        assert_enqueued(
-          worker: Mobilizon.Service.Workers.BuildSearch,
-          args: %{event_id: event_id, op: :insert_search_event}
-        )
-
-        assert %{success: 1, failure: 0} == Oban.drain_queue(queue: :search)
-
-        update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
-
-        object =
-          data["object"]
-          |> Map.put("actor", data["actor"])
-          |> Map.put("name", "My updated event")
-          |> Map.put("id", data["object"]["id"])
-          |> Map.put("type", "Event")
-
-        update_data =
-          update_data
-          |> Map.put("actor", data["actor"])
-          |> Map.put("object", object)
-
-        {:ok, %Activity{data: data, local: false}, _} =
-          Transmogrifier.handle_incoming(update_data)
-
-        %Event{} = event = Events.get_event_by_url(data["object"]["id"])
-
-        assert_enqueued(
-          worker: Mobilizon.Service.Workers.BuildSearch,
-          args: %{event_id: event_id, op: :update_search_event}
-        )
-
-        assert %{success: 1, failure: 0} == Oban.drain_queue(queue: :search)
-
-        assert event.title == "My updated event"
-
-        assert event.description == data["object"]["content"]
-      end
-    end
-
-    #     test "it works for incoming update activities which lock the account" do
-    #       data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
-
-    #       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
-    #       update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
-
-    #       object =
-    #         update_data["object"]
-    #         |> Map.put("actor", data["actor"])
-    #         |> Map.put("id", data["actor"])
-    #         |> Map.put("manuallyApprovesFollowers", true)
-
-    #       update_data =
-    #         update_data
-    #         |> Map.put("actor", data["actor"])
-    #         |> Map.put("object", object)
-
-    #       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data)
-
-    #       user = User.get_cached_by_ap_id(data["actor"])
-    #       assert user.info["locked"] == true
-    #     end
-  end
-
-  describe "handle incoming delete activities" do
-    test "it works for incoming deletes" do
-      %Actor{url: actor_url} =
-        actor = insert(:actor, url: "http://mobilizon.tld/@remote", domain: "mobilizon.tld")
-
-      %Comment{url: comment_url} =
-        insert(:comment,
-          actor: nil,
-          actor_id: actor.id,
-          url: "http://mobilizon.tld/comments/9f3794b8-11a0-4a98-8cb7-475ab332c701"
-        )
-
-      Mock
-      |> expect(:call, fn
-        %{method: :get, url: "http://mobilizon.tld/comments/9f3794b8-11a0-4a98-8cb7-475ab332c701"},
-        _opts ->
-          {:ok, %Tesla.Env{status: 410, body: "Gone"}}
-      end)
-
-      data =
-        File.read!("test/fixtures/mastodon-delete.json")
-        |> Jason.decode!()
-
-      object =
-        data["object"]
-        |> Map.put("id", comment_url)
-
-      data =
-        data
-        |> Map.put("object", object)
-        |> Map.put("actor", actor_url)
-
-      assert Discussions.get_comment_from_url(comment_url)
-      assert is_nil(Discussions.get_comment_from_url(comment_url).deleted_at)
-
-      {:ok, %Activity{local: false}, _} = Transmogrifier.handle_incoming(data)
-
-      refute is_nil(Discussions.get_comment_from_url(comment_url).deleted_at)
-    end
-
-    test "it fails for incoming deletes with spoofed origin" do
-      comment = insert(:comment)
-
-      announce_data =
-        File.read!("test/fixtures/mastodon-announce.json")
-        |> Jason.decode!()
-        |> Map.put("object", comment.url)
-
-      {:ok, _, _} = Transmogrifier.handle_incoming(announce_data)
-
-      data =
-        File.read!("test/fixtures/mastodon-delete.json")
-        |> Jason.decode!()
-
-      object =
-        data["object"]
-        |> Map.put("id", comment.url)
-
-      data =
-        data
-        |> Map.put("object", object)
-
-      :error = Transmogrifier.handle_incoming(data)
-
-      assert Discussions.get_comment_from_url(comment.url)
-    end
-
-    setup :set_mox_from_context
-
-    test "it works for incoming actor deletes" do
-      %Actor{url: url} =
-        actor = insert(:actor, url: "https://framapiaf.org/users/admin", domain: "framapiaf.org")
-
-      %Event{url: event1_url} = event1 = insert(:event, organizer_actor: actor)
-      insert(:event, organizer_actor: actor)
-
-      %Comment{url: comment1_url} = comment1 = insert(:comment, actor: actor)
-      insert(:comment, actor: actor)
-
-      data =
-        File.read!("test/fixtures/mastodon-delete-user.json")
-        |> Jason.decode!()
-
-      Mock
-      |> expect(:call, fn
-        %{method: :get, url: "https://framapiaf.org/users/admin"}, _opts ->
-          {:ok, %Tesla.Env{status: 410, body: "Gone"}}
-      end)
-
-      {:ok, _activity, _actor} = Transmogrifier.handle_incoming(data)
-      assert %{success: 1, failure: 0} == Oban.drain_queue(queue: :background)
-
-      assert {:error, :actor_not_found} = Actors.get_actor_by_url(url)
-      assert {:error, :event_not_found} = Events.get_event(event1.id)
-      # Tombstone are cascade deleted, seems correct for now
-      # assert %Tombstone{} = Tombstone.find_tombstone(event1_url)
-      assert %Comment{deleted_at: deleted_at} = Discussions.get_comment(comment1.id)
-      refute is_nil(deleted_at)
-      # assert %Tombstone{} = Tombstone.find_tombstone(comment1_url)
-    end
-
-    test "it fails for incoming actor deletes with spoofed origin" do
-      %{url: url} = insert(:actor)
-      deleted_actor_url = "https://framapiaf.org/users/admin"
-
-      data =
-        File.read!("test/fixtures/mastodon-delete-user.json")
-        |> Jason.decode!()
-        |> Map.put("actor", url)
-
-      deleted_actor_data =
-        File.read!("test/fixtures/mastodon-actor.json")
-        |> Jason.decode!()
-        |> Map.put("id", deleted_actor_url)
-
-      Mock
-      |> expect(:call, fn
-        %{url: ^deleted_actor_url}, _opts ->
-          {:ok, %Tesla.Env{status: 200, body: deleted_actor_data}}
-      end)
-
-      assert capture_log(fn ->
-               assert :error == Transmogrifier.handle_incoming(data)
-             end) =~ "Object origin check failed"
-
-      assert Actors.get_actor_by_url(url)
-    end
-  end
-
   describe "handle tombstones" do
     setup :verify_on_exit!
 
diff --git a/test/fixtures/vcr_cassettes/activity_pub/group_post_update_activities.json b/test/fixtures/vcr_cassettes/activity_pub/group_post_update_activities.json
new file mode 100644
index 000000000..35e46a0b9
--- /dev/null
+++ b/test/fixtures/vcr_cassettes/activity_pub/group_post_update_activities.json
@@ -0,0 +1,24 @@
+[
+  {
+    "request": {
+      "body": "{\"@context\":[\"https://www.w3.org/ns/activitystreams\",\"https://litepub.social/litepub/context.jsonld\",{\"Hashtag\":\"as:Hashtag\",\"PostalAddress\":\"sc:PostalAddress\",\"address\":{\"@id\":\"sc:address\",\"@type\":\"sc:PostalAddress\"},\"addressCountry\":\"sc:addressCountry\",\"addressLocality\":\"sc:addressLocality\",\"addressRegion\":\"sc:addressRegion\",\"anonymousParticipationEnabled\":{\"@id\":\"mz:anonymousParticipationEnabled\",\"@type\":\"sc:Boolean\"},\"category\":\"sc:category\",\"commentsEnabled\":{\"@id\":\"pt:commentsEnabled\",\"@type\":\"sc:Boolean\"},\"discoverable\":\"toot:discoverable\",\"ical\":\"http://www.w3.org/2002/12/cal/ical#\",\"joinMode\":{\"@id\":\"mz:joinMode\",\"@type\":\"mz:joinModeType\"},\"joinModeType\":{\"@id\":\"mz:joinModeType\",\"@type\":\"rdfs:Class\"},\"location\":{\"@id\":\"sc:location\",\"@type\":\"sc:Place\"},\"manuallyApprovesFollowers\":\"as:manuallyApprovesFollowers\",\"maximumAttendeeCapacity\":\"sc:maximumAttendeeCapacity\",\"mz\":\"https://joinmobilizon.org/ns#\",\"participationMessage\":{\"@id\":\"mz:participationMessage\",\"@type\":\"sc:Text\"},\"postalCode\":\"sc:postalCode\",\"pt\":\"https://joinpeertube.org/ns#\",\"repliesModerationOption\":{\"@id\":\"mz:repliesModerationOption\",\"@type\":\"mz:repliesModerationOptionType\"},\"repliesModerationOptionType\":{\"@id\":\"mz:repliesModerationOptionType\",\"@type\":\"rdfs:Class\"},\"sc\":\"http://schema.org#\",\"streetAddress\":\"sc:streetAddress\",\"toot\":\"http://joinmastodon.org/ns#\",\"uuid\":\"sc:identifier\"}],\"actor\":\"http://mobilizon.test/@myGroup0\",\"cc\":[],\"id\":\"http://mobilizon.test/announces/839e0ffc-f437-48db-afba-9ce1e971e938\",\"object\":{\"actor\":\"http://mobilizon.test/@thomas0\",\"attributedTo\":\"http://mobilizon.test/@myGroup0\",\"content\":\"The <b>HTML</b>body for my Article\",\"id\":\"http://mobilizon.test/p/6a482d5f-94fc-446b-84bb-d4d386d5dd45\",\"name\":\"My updated post\",\"published\":\"2020-10-19T08:37:52Z\",\"type\":\"Article\"},\"to\":[\"http://mobilizon.test/@myGroup0/members\"],\"type\":\"Announce\"}",
+      "headers": {
+        "Content-Type": "application/activity+json",
+        "signature": "keyId=\"http://mobilizon.test/@myGroup0#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) content-length date digest host\",signature=\"P+7rSSUeUBdX74wbvSEe4roG7yh7MfpF6s4tjv5q1kbeVKtXZRyfC1LqgVNCADZYXFqYlMvfF7DiaRQRiMznGWawM/QXK08eXiAVihYK28Pa56BfI68OUakd+FptlwfB4WJ4Jc7xi1z+iarv+EvlFxjkG5pgwL4mW49rvNnigELzypGtp2bj/2BhiBItHutvOju1MwLR1EBQFJBSZDVZZKbHTcV4KbGtbYvkWUbH8fZbe3fgctKlvO/z9kw+yBTTIEE1O18F4HiJ17nYtaaxv3/vl5RxcjYLpf+QQzkaPOsSLZs8zpIZZp3BbLtPh+OGwkyK9PBQsaI0N1ZSLQ5gaQ==\"",
+        "digest": "SHA-256=EyZ+uZ/Vv2lUK8ozgOHBpnoUWUM5WQHATQb1tEMldNU=",
+        "date": "Mon, 19 Oct 2020 08:37:52 GMT"
+      },
+      "method": "post",
+      "options": [],
+      "request_body": "",
+      "url": "http://mobilizon.test/inbox"
+    },
+    "response": {
+      "binary": false,
+      "body": "nxdomain",
+      "headers": [],
+      "status_code": null,
+      "type": "error"
+    }
+  }
+]
\ No newline at end of file
diff --git a/test/graphql/resolvers/resource_test.exs b/test/graphql/resolvers/resource_test.exs
index 628914ee0..293b2fe31 100644
--- a/test/graphql/resolvers/resource_test.exs
+++ b/test/graphql/resolvers/resource_test.exs
@@ -341,7 +341,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ResourceTest do
           }
         )
 
-      assert hd(res["errors"])["message"] == "No such resource"
+      assert hd(res["errors"])["message"] == "Resource not found"
     end
 
     test "get_resource/3 for a non-existing group", %{
@@ -778,7 +778,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ResourceTest do
           }
         )
 
-      assert hd(res["errors"])["message"] == "No such resource"
+      assert hd(res["errors"])["message"] == "Resource not found"
     end
 
     test "delete_resource/3 deletes a folder and children", %{
@@ -828,7 +828,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ResourceTest do
           }
         )
 
-      assert hd(res["errors"])["message"] == "No such resource"
+      assert hd(res["errors"])["message"] == "Resource not found"
 
       res =
         conn
@@ -841,7 +841,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ResourceTest do
           }
         )
 
-      assert hd(res["errors"])["message"] == "No such resource"
+      assert hd(res["errors"])["message"] == "Resource not found"
     end
 
     test "delete_resource/3 deletes a resource not found", %{