Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into remake-remodel
authorlain <lain@soykaf.club>
Wed, 4 Dec 2019 15:35:59 +0000 (16:35 +0100)
committerlain <lain@soykaf.club>
Wed, 4 Dec 2019 15:35:59 +0000 (16:35 +0100)
13 files changed:
1  2 
lib/pleroma/object/containment.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/activity_pub/transmogrifier.ex
lib/pleroma/web/common_api/common_api.ex
lib/pleroma/web/mastodon_api/controllers/status_controller.ex
test/notification_test.exs
test/object_test.exs
test/user_test.exs
test/web/activity_pub/transmogrifier_test.exs
test/web/common_api/common_api_test.exs
test/web/mastodon_api/views/notification_view_test.exs
test/web/ostatus/ostatus_controller_test.exs
test/web/streamer/streamer_test.exs

index c53f29cd68b5f5c956b6422a1620797a17c152d7,25aa32f60743dc21fef01e1184539b3b70a77638..00b68190a09b0e5186f342befdff9712c6285704
@@@ -32,18 -32,6 +32,18 @@@ defmodule Pleroma.Object.Containment d
      get_actor(%{"actor" => actor})
    end
  
 +  def get_object(%{"object" => id}) when is_binary(id) do
 +    id
 +  end
 +
 +  def get_object(%{"object" => %{"id" => id}}) when is_binary(id) do
 +    id
 +  end
 +
 +  def get_object(_) do
 +    nil
 +  end
 +
    # TODO: We explicitly allow 'tag' URIs through, due to references to legacy OStatus
    # objects being present in the test suite environment.  Once these objects are
    # removed, please also remove this.
    def contain_origin(id, %{"attributedTo" => actor} = params),
      do: contain_origin(id, Map.put(params, "actor", actor))
  
-   def contain_origin_from_id(_id, %{"id" => nil}), do: :error
+   def contain_origin(_id, _data), do: :error
  
-   def contain_origin_from_id(id, %{"id" => other_id} = _params) do
+   def contain_origin_from_id(id, %{"id" => other_id} = _params) when is_binary(other_id) do
      id_uri = URI.parse(id)
      other_uri = URI.parse(other_id)
  
      compare_uris(id_uri, other_uri)
    end
  
+   def contain_origin_from_id(_id, _data), do: :error
    def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}),
      do: contain_origin(id, object)
  
index 72a29e50ffc5460043a4b6410c8d1d7b9bced1c8,d6a425d8bd0f6628e4a1ec606402806c4fea89d3..673fc8a9979be478ec7d4052151addad6af437f5
@@@ -124,23 -124,6 +124,23 @@@ defmodule Pleroma.Web.ActivityPub.Activ
  
    def increase_poll_votes_if_vote(_create_data), do: :noop
  
 +  # TODO rewrite in with style
 +  @spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()}
 +  def persist(object, meta) do
 +    local = Keyword.fetch!(meta, :local)
 +    {recipients, _, _} = get_recipients(object)
 +
 +    {:ok, activity} =
 +      Repo.insert(%Activity{
 +        data: object,
 +        local: local,
 +        recipients: recipients,
 +        actor: object["actor"]
 +      })
 +
 +    {:ok, activity, meta}
 +  end
 +
    def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do
      with nil <- Activity.normalize(map),
           map <- lazy_put_activity_defaults(map, fake),
           {_, true} <- {:remote_limit_error, check_remote_limit(map)},
           {:ok, map} <- MRF.filter(map),
           {recipients, _, _} = get_recipients(map),
 +         # ???
           {:fake, false, map, recipients} <- {:fake, fake, map, recipients},
           {:containment, :ok} <- {:containment, Containment.contain_child(map)},
           {:ok, map, object} <- insert_full_object(map) do
      end
    end
  
+   def react_with_emoji(user, object, emoji, options \\ []) do
+     with local <- Keyword.get(options, :local, true),
+          activity_id <- Keyword.get(options, :activity_id, nil),
+          Pleroma.Emoji.is_unicode_emoji?(emoji),
+          reaction_data <- make_emoji_reaction_data(user, object, emoji, activity_id),
+          {:ok, activity} <- insert(reaction_data, local),
+          {:ok, object} <- add_emoji_reaction_to_object(activity, object),
+          :ok <- maybe_federate(activity) do
+       {:ok, activity, object}
+     end
+   end
+   def unreact_with_emoji(user, reaction_id, options \\ []) do
+     with local <- Keyword.get(options, :local, true),
+          activity_id <- Keyword.get(options, :activity_id, nil),
+          user_ap_id <- user.ap_id,
+          %Activity{actor: ^user_ap_id} = reaction_activity <- Activity.get_by_ap_id(reaction_id),
+          object <- Object.normalize(reaction_activity),
+          unreact_data <- make_undo_data(user, reaction_activity, activity_id),
+          {:ok, activity} <- insert(unreact_data, local),
+          {:ok, object} <- remove_emoji_reaction_from_object(reaction_activity, object),
+          :ok <- maybe_federate(activity) do
+       {:ok, activity, object}
+     end
+   end
    # TODO: This is weird, maybe we shouldn't check here if we can make the activity.
    def like(
          %User{ap_id: ap_id} = user,
      end
    end
  
+   def move(%User{} = origin, %User{} = target, local \\ true) do
+     params = %{
+       "type" => "Move",
+       "actor" => origin.ap_id,
+       "object" => origin.ap_id,
+       "target" => target.ap_id
+     }
+     with true <- origin.ap_id in target.also_known_as,
+          {:ok, activity} <- insert(params, local) do
+       maybe_federate(activity)
+       BackgroundWorker.enqueue("move_following", %{
+         "origin_id" => origin.id,
+         "target_id" => target.id
+       })
+       {:ok, activity}
+     else
+       false -> {:error, "Target account must have the origin in `alsoKnownAs`"}
+       err -> err
+     end
+   end
    defp fetch_activities_for_context_query(context, opts) do
      public = [Pleroma.Constants.as_public()]
  
      |> fetch_activities_query(opts)
      |> restrict_unlisted()
      |> Pagination.fetch_paginated(opts, pagination)
-     |> Enum.reverse()
    end
  
    @valid_visibilities ~w[direct unlisted public private]
      |> Enum.reverse()
    end
  
+   def fetch_instance_activities(params) do
+     params =
+       params
+       |> Map.put("type", ["Create", "Announce"])
+       |> Map.put("instance", params["instance"])
+       |> Map.put("whole_db", true)
+     fetch_activities([Pleroma.Constants.as_public()], params, :offset)
+     |> Enum.reverse()
+   end
    defp user_activities_recipients(%{"godmode" => true}) do
      []
    end
  
    defp restrict_muted_reblogs(query, _), do: query
  
+   defp restrict_instance(query, %{"instance" => instance}) do
+     users =
+       from(
+         u in User,
+         select: u.ap_id,
+         where: fragment("? LIKE ?", u.nickname, ^"%@#{instance}")
+       )
+       |> Repo.all()
+     from(activity in query, where: activity.actor in ^users)
+   end
+   defp restrict_instance(query, _), do: query
    defp exclude_poll_votes(query, %{"include_poll_votes" => true}), do: query
  
    defp exclude_poll_votes(query, _) do
      |> restrict_reblogs(opts)
      |> restrict_pinned(opts)
      |> restrict_muted_reblogs(opts)
+     |> restrict_instance(opts)
      |> Activity.restrict_deactivated_users()
      |> exclude_poll_votes(opts)
      |> exclude_visibility(opts)
        name: data["name"],
        follower_address: data["followers"],
        following_address: data["following"],
-       bio: data["summary"]
+       bio: data["summary"],
+       also_known_as: Map.get(data, "alsoKnownAs", [])
      }
  
      # nickname can be nil because of virtual actors
      end
    end
  
-   defp collection_private(data) do
-     if is_map(data["first"]) and
-          data["first"]["type"] in ["CollectionPage", "OrderedCollectionPage"] do
+   defp collection_private(%{"first" => first}) do
+     if is_map(first) and
+          first["type"] in ["CollectionPage", "OrderedCollectionPage"] do
        {:ok, false}
      else
        with {:ok, %{"type" => type}} when type in ["CollectionPage", "OrderedCollectionPage"] <-
-              Fetcher.fetch_and_contain_remote_object_from_id(data["first"]) do
+              Fetcher.fetch_and_contain_remote_object_from_id(first) do
          {:ok, false}
        else
          {:error, {:ok, %{status: code}}} when code in [401, 403] ->
      end
    end
  
+   defp collection_private(_data), do: {:ok, true}
    def user_data_from_user_object(data) do
      with {:ok, data} <- MRF.filter(data),
           {:ok, data} <- object_to_user_data(data) do
index a25bb1978b58de793f6b50a92efd7ad84a49dc1e,ce95fb6babf84a0c7ac38359c7bae335f2dea5aa..6603e4929d50c384f2ee122426515f7433f3e3d8
@@@ -13,9 -13,6 +13,9 @@@ defmodule Pleroma.Web.ActivityPub.Trans
    alias Pleroma.Repo
    alias Pleroma.User
    alias Pleroma.Web.ActivityPub.ActivityPub
 +  alias Pleroma.Web.ActivityPub.ObjectValidator
 +  alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
 +  alias Pleroma.Web.ActivityPub.Pipeline
    alias Pleroma.Web.ActivityPub.Utils
    alias Pleroma.Web.ActivityPub.Visibility
    alias Pleroma.Web.Federator
      end
    end
  
 -  def handle_incoming(
 -        %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
 -        _options
 -      ) do
 -    with actor <- Containment.get_actor(data),
 -         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
 -         {:ok, object} <- get_obj_helper(object_id),
 -         {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
+   @misskey_reactions %{
+     "like" => "👍",
+     "love" => "❤️",
+     "laugh" => "😆",
+     "hmm" => "🤔",
+     "surprise" => "😮",
+     "congrats" => "🎉",
+     "angry" => "💢",
+     "confused" => "😥",
+     "rip" => "😇",
+     "pudding" => "🍮",
+     "star" => "⭐"
+   }
+   @doc "Rewrite misskey likes into EmojiReactions"
+   def handle_incoming(
+         %{
+           "type" => "Like",
+           "_misskey_reaction" => reaction
+         } = data,
+         options
+       ) do
+     data
+     |> Map.put("type", "EmojiReaction")
+     |> Map.put("content", @misskey_reactions[reaction] || reaction)
+     |> handle_incoming(options)
+   end
 +  def handle_incoming(%{"type" => "Like"} = data, _options) do
 +    with {_, {:ok, cast_data_sym}} <-
 +           {:casting_data,
 +            data |> LikeValidator.cast_data() |> Ecto.Changeset.apply_action(:insert)},
 +         {_, cast_data} <-
 +           {:stringify_keys, ObjectValidator.stringify_keys(cast_data_sym |> Map.from_struct())},
 +         :ok <- ObjectValidator.fetch_actor_and_object(cast_data),
 +         {_, {:ok, cast_data}} <- {:maybe_add_context, maybe_add_context_from_object(cast_data)},
 +         {_, {:ok, cast_data}} <-
 +           {:maybe_add_recipients, maybe_add_recipients_from_object(cast_data)},
 +         {_, {:ok, activity, _meta}} <-
 +           {:common_pipeline, Pipeline.common_pipeline(cast_data, local: false)} do
        {:ok, activity}
      else
 -      _e -> :error
 +      e -> {:error, e}
      end
    end
  
+   def handle_incoming(
+         %{
+           "type" => "EmojiReaction",
+           "object" => object_id,
+           "actor" => _actor,
+           "id" => id,
+           "content" => emoji
+         } = data,
+         _options
+       ) do
+     with actor <- Containment.get_actor(data),
+          {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
+          {:ok, object} <- get_obj_helper(object_id),
+          {:ok, activity, _object} <-
+            ActivityPub.react_with_emoji(actor, object, emoji, activity_id: id, local: false) do
+       {:ok, activity}
+     else
+       _e -> :error
+     end
+   end
    def handle_incoming(
          %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
          _options
  
        update_data =
          new_user_data
-         |> Map.take([:avatar, :banner, :bio, :name])
+         |> Map.take([:avatar, :banner, :bio, :name, :also_known_as])
          |> Map.put(:fields, fields)
          |> Map.put(:locked, locked)
          |> Map.put(:invisible, invisible)
      end
    end
  
+   def handle_incoming(
+         %{
+           "type" => "Undo",
+           "object" => %{"type" => "EmojiReaction", "id" => reaction_activity_id},
+           "actor" => _actor,
+           "id" => id
+         } = data,
+         _options
+       ) do
+     with actor <- Containment.get_actor(data),
+          {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
+          {:ok, activity, _} <-
+            ActivityPub.unreact_with_emoji(actor, reaction_activity_id,
+              activity_id: id,
+              local: false
+            ) do
+       {:ok, activity}
+     else
+       _e -> :error
+     end
+   end
    def handle_incoming(
          %{
            "type" => "Undo",
      end
    end
  
+   def handle_incoming(
+         %{
+           "type" => "Move",
+           "actor" => origin_actor,
+           "object" => origin_actor,
+           "target" => target_actor
+         },
+         _options
+       ) do
+     with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
+          {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
+          true <- origin_actor in target_user.also_known_as do
+       ActivityPub.move(origin_user, target_user, false)
+     else
+       _e -> :error
+     end
+   end
    def handle_incoming(_, _), do: :error
  
    @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
      Map.put(object, "attachment", attachments)
    end
  
-   defp strip_internal_fields(object) do
+   def strip_internal_fields(object) do
      object
      |> Map.drop(Pleroma.Constants.object_internal_fields())
    end
    def maybe_fix_user_url(data), do: data
  
    def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
 +
 +  defp maybe_add_context_from_object(%{"context" => context} = data) when is_binary(context),
 +    do: {:ok, data}
 +
 +  defp maybe_add_context_from_object(%{"object" => object} = data) when is_binary(object) do
 +    if object = Object.normalize(object) do
 +      data =
 +        data
 +        |> Map.put("context", object.data["context"])
 +
 +      {:ok, data}
 +    else
 +      {:error, "No context on referenced object"}
 +    end
 +  end
 +
 +  defp maybe_add_context_from_object(_) do
 +    {:error, "No referenced object"}
 +  end
 +
 +  defp maybe_add_recipients_from_object(%{"object" => object} = data) do
 +    to = data["to"] || []
 +    cc = data["cc"] || []
 +
 +    if to == [] && cc == [] do
 +      if object = Object.normalize(object) do
 +        data =
 +          data
 +          |> Map.put("to", [object.data["actor"]])
 +          |> Map.put("cc", cc)
 +
 +        {:ok, data}
 +      else
 +        {:error, "No actor on referenced object"}
 +      end
 +    else
 +      {:ok, data}
 +    end
 +  end
 +
 +  defp maybe_add_recipients_from_object(_) do
 +    {:error, "No referenced object"}
 +  end
  end
index b649d1c62c703d2038fe7cacbf6b7d2d8066296b,fe6e26a90ab6dee48c35df65acfe1f16670638fa..a95fffcfcc6f68050823275ba5a8c0dfa00842bb
@@@ -11,8 -11,6 +11,8 @@@ defmodule Pleroma.Web.CommonAPI d
    alias Pleroma.ThreadMute
    alias Pleroma.User
    alias Pleroma.Web.ActivityPub.ActivityPub
 +  alias Pleroma.Web.ActivityPub.Builder
 +  alias Pleroma.Web.ActivityPub.Pipeline
    alias Pleroma.Web.ActivityPub.Utils
    alias Pleroma.Web.ActivityPub.Visibility
  
@@@ -20,7 -18,6 +20,7 @@@
    import Pleroma.Web.CommonAPI.Utils
  
    require Pleroma.Constants
 +  require Logger
  
    def follow(follower, followed) do
      timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
      end
    end
  
 -  def favorite(id_or_ap_id, user) do
 -    with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
 -         object <- Object.normalize(activity),
 -         nil <- Utils.get_existing_like(user.ap_id, object) do
 -      ActivityPub.like(user, object)
 +  @spec favorite(User.t(), binary()) :: {:ok, Activity.t()} | {:error, any()}
 +  def favorite(%User{} = user, id) do
 +    with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
 +         {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
 +         {_, {:ok, %Activity{} = activity, _meta}} <-
 +           {:common_pipeline,
 +            Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
 +      {:ok, activity}
      else
 -      _ -> {:error, dgettext("errors", "Could not favorite")}
 +      e ->
 +        Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
 +        {:error, dgettext("errors", "Could not favorite")}
      end
    end
  
      end
    end
  
+   def react_with_emoji(id, user, emoji) do
+     with %Activity{} = activity <- Activity.get_by_id(id),
+          object <- Object.normalize(activity) do
+       ActivityPub.react_with_emoji(user, object, emoji)
+     else
+       _ ->
+         {:error, dgettext("errors", "Could not add reaction emoji")}
+     end
+   end
+   def unreact_with_emoji(id, user, emoji) do
+     with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
+       ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
+     else
+       _ ->
+         {:error, dgettext("errors", "Could not remove reaction emoji")}
+     end
+   end
    def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
      with :ok <- validate_not_author(object, user),
           :ok <- validate_existing_votes(user, object),
      end
    end
  
+   def update_report_state(activity_ids, state) when is_list(activity_ids) do
+     case Utils.update_report_state(activity_ids, state) do
+       :ok -> {:ok, activity_ids}
+       _ -> {:error, dgettext("errors", "Could not update state")}
+     end
+   end
    def update_report_state(activity_id, state) do
      with %Activity{} = activity <- Activity.get_by_id(activity_id) do
        Utils.update_report_state(activity, state)
index 4b4482aa8a2b32321e76dcc4f3c54f996ace5e05,74b223cf4efcfa1771c999c364bf80bcc808aae0..160e039af2d7e621497994c93326d73e54de2905
@@@ -82,17 -82,17 +82,17 @@@ defmodule Pleroma.Web.MastodonAPI.Statu
  
    plug(
      RateLimiter,
-     {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
+     [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]]
      when action in ~w(reblog unreblog)a
    )
  
    plug(
      RateLimiter,
-     {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
+     [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]]
      when action in ~w(favourite unfavourite)a
    )
  
-   plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
+   plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
  
    action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
  
    end
  
    @doc "POST /api/v1/statuses/:id/favourite"
 -  def favourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
 -    with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
 -         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
 +  def favourite(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
 +    with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
 +         %Activity{} = activity <- Activity.get_by_id(activity_id) do
        try_render(conn, "show.json", activity: activity, for: user, as: :activity)
      end
    end
index b2ced6c9c7f07050c153edae617017fcbf93d092,dcbffeafe39b16c1afdf7505fc4744cf2119ce25..2200d03ea76da3843e279dd2fb7ad9c6a6093258
@@@ -14,8 -14,6 +14,8 @@@ defmodule Pleroma.NotificationTest d
    alias Pleroma.Web.CommonAPI
    alias Pleroma.Web.Streamer
  
 +  import ExUnit.CaptureLog
 +
    describe "create_notifications" do
      test "notifies someone when they are directly addressed" do
        user = insert(:user)
            "status" => "hey @#{other_user.nickname}!"
          })
  
 -      {:ok, activity_two, _} = CommonAPI.favorite(activity_one.id, third_user)
 +      {:ok, activity_two} = CommonAPI.favorite(third_user, activity_one.id)
  
        assert other_user not in Notification.get_notified_from_activity(activity_two)
      end
  
        assert Enum.empty?(Notification.for_user(user))
  
 -      {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
 +      {:ok, _} = CommonAPI.favorite(other_user, activity.id)
  
        assert length(Notification.for_user(user)) == 1
  
  
        assert Enum.empty?(Notification.for_user(user))
  
 -      {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
 +      {:ok, _} = CommonAPI.favorite(other_user, activity.id)
  
        assert length(Notification.for_user(user)) == 1
  
  
        assert Enum.empty?(Notification.for_user(user))
  
 -      {:error, _} = CommonAPI.favorite(activity.id, other_user)
 +      assert capture_log(fn ->
 +               {:error, _} = CommonAPI.favorite(other_user, activity.id)
 +             end) =~ "[error]"
  
        assert Enum.empty?(Notification.for_user(user))
      end
  
        assert Enum.empty?(Notification.for_user(local_user))
      end
+     test "move activity generates a notification" do
+       %{ap_id: old_ap_id} = old_user = insert(:user)
+       %{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id])
+       follower = insert(:user)
+       other_follower = insert(:user, %{allow_following_move: false})
+       User.follow(follower, old_user)
+       User.follow(other_follower, old_user)
+       Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user)
+       ObanHelpers.perform_all()
+       assert [
+                %{
+                  activity: %{
+                    data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id}
+                  }
+                }
+              ] = Notification.for_user(follower)
+       assert [
+                %{
+                  activity: %{
+                    data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id}
+                  }
+                }
+              ] = Notification.for_user(other_follower)
+     end
    end
  
    describe "for_user" do
diff --combined test/object_test.exs
index 353bc388d24f7d915713ca1b2ef9db5548c4838e,9247a6d841a06276b23e2bf3104aafd5502ed2f6..643b50ae6b427650fe6e27f451116e6a2b09d2a7
@@@ -124,6 -124,8 +124,8 @@@ defmodule Pleroma.ObjectTest d
        %Object{} =
          object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
  
+       Object.set_cache(object)
        assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
        assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
  
        })
  
        updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
+       object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
+       assert updated_object == object_in_cache
        assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
        assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
      end
        %Object{} =
          object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
  
+       Object.set_cache(object)
        assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
        assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
  
                 mock_modified.(%Tesla.Env{status: 404, body: ""})
  
                 updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
+                object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
+                assert updated_object == object_in_cache
                 assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
                 assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
               end) =~
        %Object{} =
          object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
  
+       Object.set_cache(object)
        assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
        assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
  
        })
  
        updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: 100)
+       object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
+       assert updated_object == object_in_cache
        assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
        assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
      end
        %Object{} =
          object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
  
+       Object.set_cache(object)
        assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
        assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
  
        user = insert(:user)
        activity = Activity.get_create_by_object_ap_id(object.data["id"])
 -      {:ok, _activity, object} = CommonAPI.favorite(activity.id, user)
 +      {:ok, activity} = CommonAPI.favorite(user, activity.id)
 +      object = Object.get_by_ap_id(activity.data["object"])
  
        assert object.data["like_count"] == 1
  
        })
  
        updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
+       object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
+       assert updated_object == object_in_cache
        assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
        assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
  
diff --combined test/user_test.exs
index 4aeddd88ded0ddc7470f0749fd8823c075965c7b,c345e43e9168a475bb291013208a16b48bf3f1b1..e1e57f07be5272eb10f6b80a894e13945cf9e86f
@@@ -25,6 -25,25 +25,25 @@@ defmodule Pleroma.UserTest d
  
    clear_config([:instance, :account_activation_required])
  
+   describe "service actors" do
+     test "returns invisible actor" do
+       uri = "#{Pleroma.Web.Endpoint.url()}/internal/fetch-test"
+       followers_uri = "#{uri}/followers"
+       user = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test")
+       assert %User{
+                nickname: "internal.fetch-test",
+                invisible: true,
+                local: true,
+                ap_id: ^uri,
+                follower_address: ^followers_uri
+              } = user
+       user2 = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test")
+       assert user.id == user2.id
+     end
+   end
    describe "when tags are nil" do
      test "tagging a user" do
        user = insert(:user, %{tags: nil})
      {:ok, user} = User.follow(user, followed)
  
      user = User.get_cached_by_id(user.id)
      followed = User.get_cached_by_ap_id(followed.ap_id)
      assert followed.follower_count == 1
+     assert user.following_count == 1
  
      assert User.ap_followers(followed) in User.following(user)
    end
  
        assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers"
      end
-     test "it ensures info is not nil" do
-       changeset = User.register_changeset(%User{}, @full_user_data)
-       assert changeset.valid?
-       {:ok, user} =
-         changeset
-         |> Repo.insert()
-       refute is_nil(user.info)
-     end
    end
  
    describe "user registration, with :account_activation_required" do
            :user,
            local: false,
            nickname: "admin@mastodon.example.org",
-           ap_id: ap_id,
-           info: %{}
+           ap_id: ap_id
          )
  
        {:ok, fetched_user} = User.get_or_fetch(ap_id)
            local: false,
            nickname: "admin@mastodon.example.org",
            ap_id: "http://mastodon.example.org/users/admin",
-           last_refreshed_at: a_week_ago,
-           info: %{}
+           last_refreshed_at: a_week_ago
          )
  
        assert orig_user.last_refreshed_at == a_week_ago
        name: "Someone",
        nickname: "a@b.de",
        ap_id: "http...",
-       info: %{some: "info"},
        avatar: %{some: "avatar"}
      }
  
        {:ok, user} = User.follow(user, user2)
        {:ok, _user} = User.deactivate(user)
  
-       info = User.get_cached_user_info(user2)
+       user2 = User.get_cached_by_id(user2.id)
  
-       assert info.follower_count == 0
+       assert user2.follower_count == 0
        assert [] = User.get_followers(user2)
      end
  
        user2 = insert(:user)
  
        {:ok, user2} = User.follow(user2, user)
+       assert user2.following_count == 1
        assert User.following_count(user2) == 1
  
        {:ok, _user} = User.deactivate(user)
  
-       info = User.get_cached_user_info(user2)
+       user2 = User.get_cached_by_id(user2.id)
  
-       assert info.following_count == 0
+       assert refresh_record(user2).following_count == 0
+       assert user2.following_count == 0
        assert User.following_count(user2) == 0
        assert [] = User.get_friends(user2)
      end
        object_two = insert(:note, user: follower)
        activity_two = insert(:note_activity, user: follower, note: object_two)
  
 -      {:ok, like, _} = CommonAPI.favorite(activity_two.id, user)
 -      {:ok, like_two, _} = CommonAPI.favorite(activity.id, follower)
 +      {:ok, like} = CommonAPI.favorite(user, activity_two.id)
 +      {:ok, like_two} = CommonAPI.favorite(follower, activity.id)
        {:ok, repeat, _} = CommonAPI.repeat(activity_two.id, user)
  
        {:ok, job} = User.delete(user)
          ap_id: user.ap_id,
          name: user.name,
          nickname: user.nickname,
-         bio: String.duplicate("h", current_max_length + 1),
-         info: %{}
+         bio: String.duplicate("h", current_max_length + 1)
        }
  
        assert {:ok, %User{}} = User.insert_or_update_user(data)
        data = %{
          ap_id: user.ap_id,
          name: String.duplicate("h", current_max_length + 1),
-         nickname: user.nickname,
-         info: %{}
+         nickname: user.nickname
        }
  
        assert {:ok, %User{}} = User.insert_or_update_user(data)
    describe "caching" do
      test "invalidate_cache works" do
        user = insert(:user)
-       _user_info = User.get_cached_user_info(user)
  
+       User.set_cache(user)
        User.invalidate_cache(user)
  
        {:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
        {:ok, nil} = Cachex.get(:user_cache, "nickname:#{user.nickname}")
-       {:ok, nil} = Cachex.get(:user_cache, "user_info:#{user.id}")
      end
  
      test "User.delete() plugs any possible zombie objects" do
      refute User.auth_active?(local_user)
      assert User.auth_active?(confirmed_user)
      assert User.auth_active?(remote_user)
+     # also shows unactive for deactivated users
+     deactivated_but_confirmed =
+       insert(:user, local: true, confirmation_pending: false, deactivated: true)
+     refute User.auth_active?(deactivated_but_confirmed)
    end
  
    describe "superuser?/1" do
  
      {:ok, user} = User.block(user, follower)
  
-     assert User.user_info(user).follower_count == 2
+     assert user.follower_count == 2
    end
  
    describe "list_inactive_users_query/1" do
      end
    end
  
-   describe "set_info_cache/2" do
-     setup do
-       user = insert(:user)
-       {:ok, user: user}
-     end
-     test "update from args", %{user: user} do
-       User.set_info_cache(user, %{following_count: 15, follower_count: 18})
-       %{follower_count: followers, following_count: following} = User.get_cached_user_info(user)
-       assert followers == 18
-       assert following == 15
-     end
-     test "without args", %{user: user} do
-       User.set_info_cache(user, %{})
-       %{follower_count: followers, following_count: following} = User.get_cached_user_info(user)
-       assert followers == 0
-       assert following == 0
-     end
-   end
-   describe "user_info/2" do
-     setup do
-       user = insert(:user)
-       {:ok, user: user}
-     end
-     test "update from args", %{user: user} do
-       %{follower_count: followers, following_count: following} =
-         User.user_info(user, %{following_count: 15, follower_count: 18})
-       assert followers == 18
-       assert following == 15
-     end
-     test "without args", %{user: user} do
-       %{follower_count: followers, following_count: following} = User.user_info(user)
-       assert followers == 0
-       assert following == 0
-     end
-   end
    describe "is_internal_user?/1" do
      test "non-internal user returns false" do
        user = insert(:user)
            ap_enabled: true
          )
  
-       assert User.user_info(other_user).following_count == 0
-       assert User.user_info(other_user).follower_count == 0
+       assert other_user.following_count == 0
+       assert other_user.follower_count == 0
  
        {:ok, user} = Pleroma.User.follow(user, other_user)
        other_user = Pleroma.User.get_by_id(other_user.id)
  
-       assert User.user_info(user).following_count == 1
-       assert User.user_info(other_user).follower_count == 1
+       assert user.following_count == 1
+       assert other_user.follower_count == 1
      end
  
      test "syncronizes the counters with the remote instance for the followed when enabled" do
            ap_enabled: true
          )
  
-       assert User.user_info(other_user).following_count == 0
-       assert User.user_info(other_user).follower_count == 0
+       assert other_user.following_count == 0
+       assert other_user.follower_count == 0
  
        Pleroma.Config.put([:instance, :external_user_synchronization], true)
        {:ok, _user} = User.follow(user, other_user)
        other_user = User.get_by_id(other_user.id)
  
-       assert User.user_info(other_user).follower_count == 437
+       assert other_user.follower_count == 437
      end
  
      test "syncronizes the counters with the remote instance for the follower when enabled" do
            ap_enabled: true
          )
  
-       assert User.user_info(other_user).following_count == 0
-       assert User.user_info(other_user).follower_count == 0
+       assert other_user.following_count == 0
+       assert other_user.follower_count == 0
  
        Pleroma.Config.put([:instance, :external_user_synchronization], true)
        {:ok, other_user} = User.follow(other_user, user)
  
-       assert User.user_info(other_user).following_count == 152
+       assert other_user.following_count == 152
      end
    end
  
index 5e72f33b25d1a816298d63228a357ec301e6ac54,5da358c43636e10fa0556dfbb3d4b72393c43bfe..1910de6e0bdb02f022ec3ad52f6d20ac50218d60
@@@ -39,6 -39,7 +39,7 @@@ defmodule Pleroma.Web.ActivityPub.Trans
        assert activity == returned_activity
      end
  
+     @tag capture_log: true
      test "it fetches replied-to activities if we don't have them" do
        data =
          File.read!("test/fixtures/mastodon-post-activity.json")
          |> Poison.decode!()
          |> Map.put("object", activity.data["object"])
  
 -      {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
 +      {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data)
 +
 +      refute Enum.empty?(activity.recipients)
  
        assert data["actor"] == "http://mastodon.example.org/users/admin"
        assert data["type"] == "Like"
        assert data["object"] == activity.data["object"]
      end
  
+     test "it works for incoming misskey likes, turning them into EmojiReactions" do
+       user = insert(:user)
+       {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
+       data =
+         File.read!("test/fixtures/misskey-like.json")
+         |> Poison.decode!()
+         |> Map.put("object", activity.data["object"])
+       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+       assert data["actor"] == data["actor"]
+       assert data["type"] == "EmojiReaction"
+       assert data["id"] == data["id"]
+       assert data["object"] == activity.data["object"]
+       assert data["content"] == "🍮"
+     end
+     test "it works for incoming misskey likes that contain unicode emojis, turning them into EmojiReactions" do
+       user = insert(:user)
+       {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
+       data =
+         File.read!("test/fixtures/misskey-like.json")
+         |> Poison.decode!()
+         |> Map.put("object", activity.data["object"])
+         |> Map.put("_misskey_reaction", "⭐")
+       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+       assert data["actor"] == data["actor"]
+       assert data["type"] == "EmojiReaction"
+       assert data["id"] == data["id"]
+       assert data["object"] == activity.data["object"]
+       assert data["content"] == "⭐"
+     end
+     test "it works for incoming emoji reactions" do
+       user = insert(:user)
+       {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
+       data =
+         File.read!("test/fixtures/emoji-reaction.json")
+         |> Poison.decode!()
+         |> Map.put("object", activity.data["object"])
+       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+       assert data["actor"] == "http://mastodon.example.org/users/admin"
+       assert data["type"] == "EmojiReaction"
+       assert data["id"] == "http://mastodon.example.org/users/admin#reactions/2"
+       assert data["object"] == activity.data["object"]
+       assert data["content"] == "👌"
+     end
+     test "it works for incoming emoji reaction undos" do
+       user = insert(:user)
+       {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
+       {:ok, reaction_activity, _object} = CommonAPI.react_with_emoji(activity.id, user, "👌")
+       data =
+         File.read!("test/fixtures/mastodon-undo-like.json")
+         |> Poison.decode!()
+         |> Map.put("object", reaction_activity.data["id"])
+         |> Map.put("actor", user.ap_id)
+       {:ok, activity} = Transmogrifier.handle_incoming(data)
+       assert activity.actor == user.ap_id
+       assert activity.data["id"] == data["id"]
+       assert activity.data["type"] == "Undo"
+     end
      test "it returns an error for incoming unlikes wihout a like activity" do
        user = insert(:user)
        {:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
        assert object.data["content"] == "this is a private toot"
      end
  
+     @tag capture_log: true
      test "it rejects incoming announces with an inlined activity from another origin" do
        data =
          File.read!("test/fixtures/bogus-mastodon-announce.json")
        refute Map.has_key?(object.data, "likes")
      end
  
+     test "it strips internal reactions" do
+       user = insert(:user)
+       {:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
+       {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, user, "📢")
+       %{object: object} = Activity.get_by_id_with_object(activity.id)
+       assert Map.has_key?(object.data, "reactions")
+       assert Map.has_key?(object.data, "reaction_count")
+       object_data = Transmogrifier.strip_internal_fields(object.data)
+       refute Map.has_key?(object_data, "reactions")
+       refute Map.has_key?(object_data, "reaction_count")
+     end
      test "it works for incoming update activities" do
        data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
  
        assert user.bio == "<p>Some bio</p>"
      end
  
+     test "it works with alsoKnownAs" do
+       {:ok, %Activity{data: %{"actor" => actor}}} =
+         "test/fixtures/mastodon-post-activity.json"
+         |> File.read!()
+         |> Poison.decode!()
+         |> Transmogrifier.handle_incoming()
+       assert User.get_cached_by_ap_id(actor).also_known_as == ["http://example.org/users/foo"]
+       {:ok, _activity} =
+         "test/fixtures/mastodon-update.json"
+         |> File.read!()
+         |> Poison.decode!()
+         |> Map.put("actor", actor)
+         |> Map.update!("object", fn object ->
+           object
+           |> Map.put("actor", actor)
+           |> Map.put("id", actor)
+           |> Map.put("alsoKnownAs", [
+             "http://mastodon.example.org/users/foo",
+             "http://example.org/users/bar"
+           ])
+         end)
+         |> Transmogrifier.handle_incoming()
+       assert User.get_cached_by_ap_id(actor).also_known_as == [
+                "http://mastodon.example.org/users/foo",
+                "http://example.org/users/bar"
+              ]
+     end
      test "it works with custom profile fields" do
        {:ok, activity} =
          "test/fixtures/mastodon-post-activity.json"
        assert Activity.get_by_id(activity.id)
      end
  
+     @tag capture_log: true
      test "it works for incoming user deletes" do
        %{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin")
  
        assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["cc"]
        assert [user.follower_address] == activity.data["to"]
      end
+     test "it accepts Move activities" do
+       old_user = insert(:user)
+       new_user = insert(:user)
+       message = %{
+         "@context" => "https://www.w3.org/ns/activitystreams",
+         "type" => "Move",
+         "actor" => old_user.ap_id,
+         "object" => old_user.ap_id,
+         "target" => new_user.ap_id
+       }
+       assert :error = Transmogrifier.handle_incoming(message)
+       {:ok, _new_user} = User.update_and_set_cache(new_user, %{also_known_as: [old_user.ap_id]})
+       assert {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(message)
+       assert activity.actor == old_user.ap_id
+       assert activity.data["actor"] == old_user.ap_id
+       assert activity.data["object"] == old_user.ap_id
+       assert activity.data["target"] == new_user.ap_id
+       assert activity.data["type"] == "Move"
+     end
    end
  
    describe "prepare outgoing" do
        assert modified_object["inReplyToAtomUri"] == ""
      end
  
+     @tag capture_log: true
      test "returns modified object when allowed incoming reply", %{data: data} do
        object_with_reply =
          Map.put(
               end) =~ "Unsupported URI scheme"
      end
  
+     @tag capture_log: true
      test "returns {:ok, %Object{}} for success case" do
        assert {:ok, %Object{}} =
                 Transmogrifier.get_obj_helper("https://shitposter.club/notice/2827873")
index 5e5d468474aaa7082204177b69a7a3d019f4c18b,138488d44b1626f8ea7fe5ec5ce140794a5d7c71..d641f74787aa8f50eadbafbff9c1a7123d907cc9
@@@ -14,7 -14,6 +14,7 @@@ defmodule Pleroma.Web.CommonAPITest d
    alias Pleroma.Web.CommonAPI
  
    import Pleroma.Factory
 +  import ExUnit.CaptureLog
  
    require Pleroma.Constants
  
    end
  
    describe "reactions" do
+     test "reacting to a status with an emoji" do
+       user = insert(:user)
+       other_user = insert(:user)
+       {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
+       {:ok, reaction, _} = CommonAPI.react_with_emoji(activity.id, user, "👍")
+       assert reaction.data["actor"] == user.ap_id
+       assert reaction.data["content"] == "👍"
+       # TODO: test error case.
+     end
+     test "unreacting to a status with an emoji" do
+       user = insert(:user)
+       other_user = insert(:user)
+       {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
+       {:ok, reaction, _} = CommonAPI.react_with_emoji(activity.id, user, "👍")
+       {:ok, unreaction, _} = CommonAPI.unreact_with_emoji(activity.id, user, "👍")
+       assert unreaction.data["type"] == "Undo"
+       assert unreaction.data["object"] == reaction.data["id"]
+     end
      test "repeating a status" do
        user = insert(:user)
        other_user = insert(:user)
        user = insert(:user)
        other_user = insert(:user)
  
 -      {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
 +      {:ok, post_activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
  
 -      {:ok, %Activity{}, _} = CommonAPI.favorite(activity.id, user)
 +      {:ok, %Activity{data: data}} = CommonAPI.favorite(user, post_activity.id)
 +      assert data["type"] == "Like"
 +      assert data["actor"] == user.ap_id
 +      assert data["object"] == post_activity.data["object"]
      end
  
      test "retweeting a status twice returns an error" do
        other_user = insert(:user)
  
        {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
 -      {:ok, %Activity{}, _object} = CommonAPI.favorite(activity.id, user)
 -      {:error, _} = CommonAPI.favorite(activity.id, user)
 +      {:ok, %Activity{}} = CommonAPI.favorite(user, activity.id)
 +
 +      assert capture_log(fn ->
 +               assert {:error, _} = CommonAPI.favorite(user, activity.id)
 +             end) =~ "[error]"
      end
    end
  
  
        assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"}
      end
+     test "updates state of multiple reports" do
+       [reporter, target_user] = insert_pair(:user)
+       activity = insert(:note_activity, user: target_user)
+       {:ok, %Activity{id: first_report_id}} =
+         CommonAPI.report(reporter, %{
+           "account_id" => target_user.id,
+           "comment" => "I feel offended",
+           "status_ids" => [activity.id]
+         })
+       {:ok, %Activity{id: second_report_id}} =
+         CommonAPI.report(reporter, %{
+           "account_id" => target_user.id,
+           "comment" => "I feel very offended!",
+           "status_ids" => [activity.id]
+         })
+       {:ok, report_ids} =
+         CommonAPI.update_report_state([first_report_id, second_report_id], "resolved")
+       first_report = Activity.get_by_id(first_report_id)
+       second_report = Activity.get_by_id(second_report_id)
+       assert report_ids -- [first_report_id, second_report_id] == []
+       assert first_report.data["state"] == "resolved"
+       assert second_report.data["state"] == "resolved"
+     end
    end
  
    describe "reblog muting" do
index d0680926806b5d54e19e3018e05c8c909fbc2b43,26e1afc857d5fc8f2ae5f68f65ba3f9cdc4f5920..a741cc014aa3c15e84f6a2e7983216f330cbd2a3
@@@ -42,7 -42,7 +42,7 @@@ defmodule Pleroma.Web.MastodonAPI.Notif
      user = insert(:user)
      another_user = insert(:user)
      {:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"})
 -    {:ok, favorite_activity, _object} = CommonAPI.favorite(create_activity.id, another_user)
 +    {:ok, favorite_activity} = CommonAPI.favorite(another_user, create_activity.id)
      {:ok, [notification]} = Notification.create_notifications(favorite_activity)
      create_activity = Activity.get_by_id(create_activity.id)
  
      assert [] ==
               NotificationView.render("index.json", %{notifications: [notification], for: followed})
    end
+   test "Move notification" do
+     %{ap_id: old_ap_id} = old_user = insert(:user)
+     %{ap_id: _new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id])
+     follower = insert(:user)
+     User.follow(follower, old_user)
+     Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user)
+     Pleroma.Tests.ObanHelpers.perform_all()
+     old_user = refresh_record(old_user)
+     new_user = refresh_record(new_user)
+     [notification] = Notification.for_user(follower)
+     expected = %{
+       id: to_string(notification.id),
+       pleroma: %{is_seen: false},
+       type: "move",
+       account: AccountView.render("show.json", %{user: old_user, for: follower}),
+       target: AccountView.render("show.json", %{user: new_user, for: follower}),
+       created_at: Utils.to_masto_date(notification.inserted_at)
+     }
+     assert [expected] ==
+              NotificationView.render("index.json", %{notifications: [notification], for: follower})
+   end
  end
index 60090c1ebca3bee9acf11d7143208da1a7eecaa6,50235dfef85583e9b0c62a8ee2dd23fdc6e047c8..567aabbf13d771e4953cfc72ae09d02d8a42c1b2
@@@ -35,23 -35,6 +35,6 @@@ defmodule Pleroma.Web.OStatus.OStatusCo
        assert redirected_to(conn) == "/notice/#{note_activity.id}"
      end
  
-     test "500s when user not found", %{conn: conn} do
-       note_activity = insert(:note_activity)
-       object = Object.normalize(note_activity)
-       user = User.get_cached_by_ap_id(note_activity.data["actor"])
-       User.invalidate_cache(user)
-       Pleroma.Repo.delete(user)
-       [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"]))
-       url = "/objects/#{uuid}"
-       conn =
-         conn
-         |> put_req_header("accept", "application/xml")
-         |> get(url)
-       assert response(conn, 500) == ~S({"error":"Something went wrong"})
-     end
      test "404s on private objects", %{conn: conn} do
        note_activity = insert(:direct_note_activity)
        object = Object.normalize(note_activity)
        assert redirected_to(conn) == "/notice/#{note_activity.id}"
      end
  
-     test "505s when user not found", %{conn: conn} do
-       note_activity = insert(:note_activity)
-       [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
-       user = User.get_cached_by_ap_id(note_activity.data["actor"])
-       User.invalidate_cache(user)
-       Pleroma.Repo.delete(user)
-       conn =
-         conn
-         |> put_req_header("accept", "text/html")
-         |> get("/activities/#{uuid}")
-       assert response(conn, 500) == ~S({"error":"Something went wrong"})
-     end
      test "404s on private activities", %{conn: conn} do
        note_activity = insert(:direct_note_activity)
        [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
    end
  
    describe "GET notice/2" do
-     test "gets a notice in xml format", %{conn: conn} do
+     test "redirects to a proper object URL when json requested and the object is local", %{
+       conn: conn
+     } do
        note_activity = insert(:note_activity)
+       expected_redirect_url = Object.normalize(note_activity).data["id"]
  
-       conn
-       |> get("/notice/#{note_activity.id}")
-       |> response(200)
+       redirect_url =
+         conn
+         |> put_req_header("accept", "application/activity+json")
+         |> get("/notice/#{note_activity.id}")
+         |> redirected_to()
+       assert redirect_url == expected_redirect_url
      end
  
-     test "gets a notice in AS2 format", %{conn: conn} do
-       note_activity = insert(:note_activity)
+     test "returns a 404 on remote notice when json requested", %{conn: conn} do
+       note_activity = insert(:note_activity, local: false)
  
        conn
        |> put_req_header("accept", "application/activity+json")
        |> get("/notice/#{note_activity.id}")
-       |> json_response(200)
+       |> response(404)
      end
  
      test "500s when actor not found", %{conn: conn} do
        assert response(conn, 500) == ~S({"error":"Something went wrong"})
      end
  
-     test "only gets a notice in AS2 format for Create messages", %{conn: conn} do
-       note_activity = insert(:note_activity)
-       url = "/notice/#{note_activity.id}"
-       conn =
-         conn
-         |> put_req_header("accept", "application/activity+json")
-         |> get(url)
-       assert json_response(conn, 200)
-       user = insert(:user)
-       {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)
-       url = "/notice/#{like_activity.id}"
-       assert like_activity.data["type"] == "Like"
-       conn =
-         build_conn()
-         |> put_req_header("accept", "application/activity+json")
-         |> get(url)
-       assert response(conn, 404)
-     end
      test "render html for redirect for html format", %{conn: conn} do
        note_activity = insert(:note_activity)
  
  
        user = insert(:user)
  
 -      {:ok, like_activity, _} = CommonAPI.favorite(note_activity.id, user)
 +      {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)
  
        assert like_activity.data["type"] == "Like"
  
index 3a14d12f0ec63d78af19753b1174db96a2571d75,8265f18dd28e1939d186f5264204aae65d2deac7..5a5b3514793f423b97b93f6e07eece6a4fbf9612
@@@ -15,7 -15,7 +15,7 @@@ defmodule Pleroma.Web.StreamerTest d
    alias Pleroma.Web.Streamer.StreamerSocket
    alias Pleroma.Web.Streamer.Worker
  
-   @moduletag needs_streamer: true
+   @moduletag needs_streamer: true, capture_log: true
    clear_config_all([:instance, :skip_thread_containment])
  
    describe "user streams" do
@@@ -69,7 -69,7 +69,7 @@@
        )
  
        {:ok, activity} = CommonAPI.post(user, %{"status" => ":("})
 -      {:ok, notif, _} = CommonAPI.favorite(activity.id, blocked)
 +      {:ok, notif} = CommonAPI.favorite(blocked, activity.id)
  
        Streamer.stream("user:notification", notif)
        Task.await(task)
@@@ -88,7 -88,7 +88,7 @@@
  
        {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"})
        {:ok, activity} = CommonAPI.add_mute(user, activity)
 -      {:ok, notif, _} = CommonAPI.favorite(activity.id, user2)
 +      {:ok, notif} = CommonAPI.favorite(user2, activity.id)
        Streamer.stream("user:notification", notif)
        Task.await(task)
      end
  
        {:ok, user} = User.block_domain(user, "hecking-lewd-place.com")
        {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"})
 -      {:ok, notif, _} = CommonAPI.favorite(activity.id, user2)
 +      {:ok, notif} = CommonAPI.favorite(user2, activity.id)
  
        Streamer.stream("user:notification", notif)
        Task.await(task)