Merge branch 'fix/tusky-dm' into 'develop'
authorkaniini <nenolod@gmail.com>
Fri, 25 Jan 2019 05:19:32 +0000 (05:19 +0000)
committerkaniini <nenolod@gmail.com>
Fri, 25 Jan 2019 05:19:32 +0000 (05:19 +0000)
Add actor to recipients list

Closes #390

See merge request pleroma/pleroma!683

1  2 
lib/pleroma/notification.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/common_api/utils.ex
lib/pleroma/web/twitter_api/views/activity_view.ex
test/user_test.exs
test/web/activity_pub/activity_pub_test.exs
test/web/mastodon_api/mastodon_api_controller_test.exs
test/web/mastodon_api/status_view_test.exs

index 2c8f60f1941178fd9dd489644f3dfb5e82008d45,4659e14ef8bd667f50d4b06ea3757b6d4d1f73d3..e47145601e8cbd3d57af9eccc1d0c6dad409914e
@@@ -4,13 -4,14 +4,14 @@@
  
  defmodule Pleroma.Notification do
    use Ecto.Schema
-   alias Pleroma.{User, Activity, Notification, Repo, Object}
+   alias Pleroma.{User, Activity, Notification, Repo}
+   alias Pleroma.Web.CommonAPI.Utils
    import Ecto.Query
  
    schema "notifications" do
      field(:seen, :boolean, default: false)
 -    belongs_to(:user, Pleroma.User)
 -    belongs_to(:activity, Pleroma.Activity)
 +    belongs_to(:user, User, type: Pleroma.FlakeId)
 +    belongs_to(:activity, Activity, type: Pleroma.FlakeId)
  
      timestamps()
    end
@@@ -96,7 -97,7 +97,7 @@@
      end
    end
  
 -  def create_notifications(%Activity{id: _, data: %{"to" => _, "type" => type}} = activity)
 +  def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)
        when type in ["Create", "Like", "Announce", "Follow"] do
      users = get_notified_from_activity(activity)
  
        when type in ["Create", "Like", "Announce", "Follow"] do
      recipients =
        []
-       |> maybe_notify_to_recipients(activity)
-       |> maybe_notify_mentioned_recipients(activity)
+       |> Utils.maybe_notify_to_recipients(activity)
+       |> Utils.maybe_notify_mentioned_recipients(activity)
        |> Enum.uniq()
  
      User.get_users_from_set(recipients, local_only)
    end
  
    def get_notified_from_activity(_, _local_only), do: []
-   defp maybe_notify_to_recipients(
-          recipients,
-          %Activity{data: %{"to" => to, "type" => _type}} = _activity
-        ) do
-     recipients ++ to
-   end
-   defp maybe_notify_mentioned_recipients(
-          recipients,
-          %Activity{data: %{"to" => _to, "type" => type} = data} = _activity
-        )
-        when type == "Create" do
-     object = Object.normalize(data["object"])
-     object_data =
-       cond do
-         !is_nil(object) ->
-           object.data
-         is_map(data["object"]) ->
-           data["object"]
-         true ->
-           %{}
-       end
-     tagged_mentions = maybe_extract_mentions(object_data)
-     recipients ++ tagged_mentions
-   end
-   defp maybe_notify_mentioned_recipients(recipients, _), do: recipients
-   defp maybe_extract_mentions(%{"tag" => tag}) do
-     tag
-     |> Enum.filter(fn x -> is_map(x) end)
-     |> Enum.filter(fn x -> x["type"] == "Mention" end)
-     |> Enum.map(fn x -> x["href"] end)
-   end
-   defp maybe_extract_mentions(_), do: []
  end
index 85fa83e2b55c0b716dc56ebf44057bdef0fc5bf7,487d4c84a892fee848697c28bc5fc15b2b17dd26..6b4682e35837ca04ee02ac471cd98795915b9045
@@@ -36,6 -36,14 +36,14 @@@ defmodule Pleroma.Web.ActivityPub.Activ
      {recipients, to, cc}
    end
  
+   defp get_recipients(%{"type" => "Create"} = data) do
+     to = data["to"] || []
+     cc = data["cc"] || []
+     actor = data["actor"] || []
+     recipients = (to ++ cc ++ [actor]) |> Enum.uniq()
+     {recipients, to, cc}
+   end
    defp get_recipients(data) do
      to = data["to"] || []
      cc = data["cc"] || []
    def stream_out(activity) do
      public = "https://www.w3.org/ns/activitystreams#Public"
  
 -    if activity.data["type"] in ["Create", "Announce"] do
 +    if activity.data["type"] in ["Create", "Announce", "Delete"] do
        Pleroma.Web.Streamer.stream("user", activity)
        Pleroma.Web.Streamer.stream("list", activity)
  
            Pleroma.Web.Streamer.stream("public:local", activity)
          end
  
 -        activity.data["object"]
 -        |> Map.get("tag", [])
 -        |> Enum.filter(fn tag -> is_bitstring(tag) end)
 -        |> Enum.map(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end)
 +        if activity.data["type"] in ["Create"] do
 +          activity.data["object"]
 +          |> Map.get("tag", [])
 +          |> Enum.filter(fn tag -> is_bitstring(tag) end)
 +          |> Enum.map(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end)
  
 -        if activity.data["object"]["attachment"] != [] do
 -          Pleroma.Web.Streamer.stream("public:media", activity)
 +          if activity.data["object"]["attachment"] != [] do
 +            Pleroma.Web.Streamer.stream("public:media", activity)
  
 -          if activity.local do
 -            Pleroma.Web.Streamer.stream("public:local:media", activity)
 +            if activity.local do
 +              Pleroma.Web.Streamer.stream("public:local:media", activity)
 +            end
            end
          end
        else
               additional
             ),
           {:ok, activity} <- insert(create_data, local),
 -         :ok <- maybe_federate(activity),
 -         {:ok, _actor} <- User.increase_note_count(actor) do
 +         # Changing note count prior to enqueuing federation task in order to avoid race conditions on updating user.info
 +         {:ok, _actor} <- User.increase_note_count(actor),
 +         :ok <- maybe_federate(activity) do
        {:ok, activity}
      end
    end
          %User{ap_id: _} = user,
          %Object{data: %{"id" => _}} = object,
          activity_id \\ nil,
 -        local \\ true
 +        local \\ true,
 +        public \\ true
        ) do
      with true <- is_public?(object),
 -         announce_data <- make_announce_data(user, object, activity_id),
 +         announce_data <- make_announce_data(user, object, activity_id, public),
           {:ok, activity} <- insert(announce_data, local),
           {:ok, object} <- add_announce_to_object(activity, object),
           :ok <- maybe_federate(activity) do
  
      with {:ok, _} <- Object.delete(object),
           {:ok, activity} <- insert(data, local),
 -         :ok <- maybe_federate(activity),
 -         {:ok, _actor} <- User.decrease_note_count(user) do
 +         # Changing note count prior to enqueuing federation task in order to avoid race conditions on updating user.info
 +         {:ok, _actor} <- User.decrease_note_count(user),
 +         :ok <- maybe_federate(activity) do
        {:ok, activity}
      end
    end
      |> Enum.reverse()
    end
  
 +  defp restrict_since(query, %{"since_id" => ""}), do: query
 +
    defp restrict_since(query, %{"since_id" => since_id}) do
      from(activity in query, where: activity.id > ^since_id)
    end
  
    defp restrict_local(query, _), do: query
  
 +  defp restrict_max(query, %{"max_id" => ""}), do: query
 +
    defp restrict_max(query, %{"max_id" => max_id}) do
      from(activity in query, where: activity.id < ^max_id)
    end
      end
    end
  
 -  def is_public?(%Object{data: %{"type" => "Tombstone"}}) do
 -    false
 +  def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false
 +  def is_public?(%Object{data: data}), do: is_public?(data)
 +  def is_public?(%Activity{data: data}), do: is_public?(data)
 +  def is_public?(%{"directMessage" => true}), do: false
 +
 +  def is_public?(data) do
 +    "https://www.w3.org/ns/activitystreams#Public" in (data["to"] ++ (data["cc"] || []))
 +  end
 +
 +  def is_private?(activity) do
 +    !is_public?(activity) && Enum.any?(activity.data["to"], &String.contains?(&1, "/followers"))
    end
  
 -  def is_public?(activity) do
 -    "https://www.w3.org/ns/activitystreams#Public" in (activity.data["to"] ++
 -                                                         (activity.data["cc"] || []))
 +  def is_direct?(%Activity{data: %{"directMessage" => true}}), do: true
 +  def is_direct?(%Object{data: %{"directMessage" => true}}), do: true
 +
 +  def is_direct?(activity) do
 +    !is_public?(activity) && !is_private?(activity)
    end
  
    def visible_for_user?(activity, nil) do
index a0f59d9004eb103961a3dc1c9bfb41e81d687193,d36875705727f56a1a9e7662515aafc6d1b658b7..208677bd755857f7b31af1dc113123a02be10839
@@@ -14,13 -14,13 +14,13 @@@ defmodule Pleroma.Web.CommonAPI.Utils d
  
    # This is a hack for twidere.
    def get_by_id_or_ap_id(id) do
 -    activity = Repo.get(Activity, id) || Activity.get_create_activity_by_object_ap_id(id)
 +    activity = Repo.get(Activity, id) || Activity.get_create_by_object_ap_id(id)
  
      activity &&
        if activity.data["type"] == "Create" do
          activity
        else
 -        Activity.get_create_activity_by_object_ap_id(activity.data["object"])
 +        Activity.get_create_by_object_ap_id(activity.data["object"])
        end
    end
  
      Enum.join([text | attachment_text], "<br>")
    end
  
 +  def format_input(text, mentions, tags, format, options \\ [])
 +
    @doc """
    Formatting text to plain text.
    """
 -  def format_input(text, mentions, tags, "text/plain") do
 +  def format_input(text, mentions, tags, "text/plain", options) do
      text
      |> Formatter.html_escape("text/plain")
      |> String.replace(~r/\r?\n/, "<br>")
      |> (&{[], &1}).()
      |> Formatter.add_links()
 -    |> Formatter.add_user_links(mentions)
 +    |> Formatter.add_user_links(mentions, options[:user_links] || [])
      |> Formatter.add_hashtag_links(tags)
      |> Formatter.finalize()
    end
    @doc """
    Formatting text to html.
    """
 -  def format_input(text, mentions, _tags, "text/html") do
 +  def format_input(text, mentions, _tags, "text/html", options) do
      text
      |> Formatter.html_escape("text/html")
      |> (&{[], &1}).()
 -    |> Formatter.add_user_links(mentions)
 +    |> Formatter.add_user_links(mentions, options[:user_links] || [])
      |> Formatter.finalize()
    end
  
    @doc """
    Formatting text to markdown.
    """
 -  def format_input(text, mentions, tags, "text/markdown") do
 +  def format_input(text, mentions, tags, "text/markdown", options) do
      text
      |> Formatter.mentions_escape(mentions)
      |> Earmark.as_html!()
      |> Formatter.html_escape("text/html")
      |> (&{[], &1}).()
 -    |> Formatter.add_user_links(mentions)
 +    |> Formatter.add_user_links(mentions, options[:user_links] || [])
      |> Formatter.add_hashtag_links(tags)
      |> Formatter.finalize()
    end
        }
      end)
    end
+   def maybe_notify_to_recipients(
+         recipients,
+         %Activity{data: %{"to" => to, "type" => _type}} = _activity
+       ) do
+     recipients ++ to
+   end
+   def maybe_notify_mentioned_recipients(
+         recipients,
+         %Activity{data: %{"to" => _to, "type" => type} = data} = _activity
+       )
+       when type == "Create" do
+     object = Object.normalize(data["object"])
+     object_data =
+       cond do
+         !is_nil(object) ->
+           object.data
+         is_map(data["object"]) ->
+           data["object"]
+         true ->
+           %{}
+       end
+     tagged_mentions = maybe_extract_mentions(object_data)
+     recipients ++ tagged_mentions
+   end
+   def maybe_notify_mentioned_recipients(recipients, _), do: recipients
+   def maybe_extract_mentions(%{"tag" => tag}) do
+     tag
+     |> Enum.filter(fn x -> is_map(x) end)
+     |> Enum.filter(fn x -> x["type"] == "Mention" end)
+     |> Enum.map(fn x -> x["href"] end)
+   end
+   def maybe_extract_mentions(_), do: []
  end
index 5eb06a26e5fd5592d9f0f2819ef3f9c050b5990b,9ae7846c072a837872a5fa4bf1b63869d6dcf092..e0a52d94abcf5128bb56bc78413fb777206de4a4
@@@ -101,10 -101,20 +101,10 @@@ defmodule Pleroma.Web.TwitterAPI.Activi
          user
  
        true ->
 -        error_user(ap_id)
 +        User.error_user(ap_id)
      end
    end
  
 -  defp error_user(ap_id) do
 -    %User{
 -      name: ap_id,
 -      ap_id: ap_id,
 -      info: %User.Info{},
 -      nickname: "erroruser@example.com",
 -      inserted_at: NaiveDateTime.utc_now()
 -    }
 -  end
 -
    def render("index.json", opts) do
      context_ids = collect_context_ids(opts.activities)
      users = collect_users(opts.activities)
    def render("activity.json", %{activity: %{data: %{"type" => "Announce"}} = activity} = opts) do
      user = get_user(activity.data["actor"], opts)
      created_at = activity.data["published"] |> Utils.date_to_asctime()
 -    announced_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
 +    announced_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
  
      text = "#{user.nickname} retweeted a status."
  
  
    def render("activity.json", %{activity: %{data: %{"type" => "Like"}} = activity} = opts) do
      user = get_user(activity.data["actor"], opts)
 -    liked_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
 +    liked_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
      liked_activity_id = if liked_activity, do: liked_activity.id, else: nil
  
      created_at =
      pinned = activity.id in user.info.pinned_activities
  
      attentions =
-       activity.recipients
+       []
+       |> Utils.maybe_notify_to_recipients(activity)
+       |> Utils.maybe_notify_mentioned_recipients(activity)
        |> Enum.map(fn ap_id -> get_user(ap_id, opts) end)
        |> Enum.filter(& &1)
        |> Enum.map(fn user -> UserView.render("show.json", %{user: user, for: opts[:for]}) end)
diff --combined test/user_test.exs
index 092cfc5dc74080ff908e87d42a29c67ce5b6308e,935c9c5b120de7ede8487d40e82c96e6265f3110..a0657c7b63736d6d047fc18c17b1f0f84f76ead6
@@@ -672,12 -672,13 +672,13 @@@ defmodule Pleroma.UserTest d
          "status" => "hey @#{addressed.nickname} @#{addressed_remote.nickname}"
        })
  
-     assert [addressed] == User.get_recipients_from_activity(activity)
+     assert Enum.map([actor, addressed], & &1.ap_id) --
+              Enum.map(User.get_recipients_from_activity(activity), & &1.ap_id) == []
  
      {:ok, user} = User.follow(user, actor)
      {:ok, _user_two} = User.follow(user_two, actor)
      recipients = User.get_recipients_from_activity(activity)
-     assert length(recipients) == 2
+     assert length(recipients) == 3
      assert user in recipients
      assert addressed in recipients
    end
    end
  
    describe "User.search" do
 -    test "finds a user, ranking by similarity" do
 -      _user = insert(:user, %{name: "lain"})
 -      _user_two = insert(:user, %{name: "ean"})
 -      _user_three = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social"})
 -      user_four = insert(:user, %{nickname: "lain@pleroma.soykaf.com"})
 +    test "finds a user by full or partial nickname" do
 +      user = insert(:user, %{nickname: "john"})
  
 -      assert user_four ==
 -               User.search("lain@ple") |> List.first() |> Map.put(:search_distance, nil)
 +      Enum.each(["john", "jo", "j"], fn query ->
 +        assert user == User.search(query) |> List.first() |> Map.put(:search_rank, nil)
 +      end)
 +    end
 +
 +    test "finds a user by full or partial name" do
 +      user = insert(:user, %{name: "John Doe"})
 +
 +      Enum.each(["John Doe", "JOHN", "doe", "j d", "j", "d"], fn query ->
 +        assert user == User.search(query) |> List.first() |> Map.put(:search_rank, nil)
 +      end)
 +    end
 +
 +    test "finds users, preferring nickname matches over name matches" do
 +      u1 = insert(:user, %{name: "lain", nickname: "nick1"})
 +      u2 = insert(:user, %{nickname: "lain", name: "nick1"})
 +
 +      assert [u2.id, u1.id] == Enum.map(User.search("lain"), & &1.id)
 +    end
 +
 +    test "finds users, considering density of matched tokens" do
 +      u1 = insert(:user, %{name: "Bar Bar plus Word Word"})
 +      u2 = insert(:user, %{name: "Word Word Bar Bar Bar"})
 +
 +      assert [u2.id, u1.id] == Enum.map(User.search("bar word"), & &1.id)
 +    end
 +
 +    test "finds users, ranking by similarity" do
 +      u1 = insert(:user, %{name: "lain"})
 +      _u2 = insert(:user, %{name: "ean"})
 +      u3 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social"})
 +      u4 = insert(:user, %{nickname: "lain@pleroma.soykaf.com"})
 +
 +      assert [u4.id, u3.id, u1.id] == Enum.map(User.search("lain@ple"), & &1.id)
 +    end
 +
 +    test "finds users, handling misspelled requests" do
 +      u1 = insert(:user, %{name: "lain"})
 +
 +      assert [u1.id] == Enum.map(User.search("laiin"), & &1.id)
 +    end
 +
 +    test "finds users, boosting ranks of friends and followers" do
 +      u1 = insert(:user)
 +      u2 = insert(:user, %{name: "Doe"})
 +      follower = insert(:user, %{name: "Doe"})
 +      friend = insert(:user, %{name: "Doe"})
 +
 +      {:ok, follower} = User.follow(follower, u1)
 +      {:ok, u1} = User.follow(u1, friend)
 +
 +      assert [friend.id, follower.id, u2.id] == Enum.map(User.search("doe", false, u1), & &1.id)
      end
  
      test "finds a user whose name is nil" do
        assert user_two ==
                 User.search("lain@pleroma.soykaf.com")
                 |> List.first()
 -               |> Map.put(:search_distance, nil)
 +               |> Map.put(:search_rank, nil)
 +    end
 +
 +    test "does not yield false-positive matches" do
 +      insert(:user, %{name: "John Doe"})
 +
 +      Enum.each(["mary", "a", ""], fn query ->
 +        assert [] == User.search(query)
 +      end)
      end
    end
  
        Pleroma.Config.put([:instance, :account_activation_required], false)
      end
    end
 +
 +  describe "parse_bio/2" do
 +    test "preserves hosts in user links text" do
 +      remote_user = insert(:user, local: false, nickname: "nick@domain.com")
 +      user = insert(:user)
 +      bio = "A.k.a. @nick@domain.com"
 +
 +      expected_text =
 +        "A.k.a. <span class='h-card'><a data-user='#{remote_user.id}' class='u-url mention' href='#{
 +          remote_user.ap_id
 +        }'>" <> "@<span>nick@domain.com</span></a></span>"
 +
 +      assert expected_text == User.parse_bio(bio, user)
 +    end
 +  end
  end
index 18f0943794dad676e502737390cd44a8a0d61e8c,6b1debc611e04d06a1cdbc011e6db436d4a7b47e..d2e54d8049a80e25b50ac400fc435693918ad6a9
@@@ -160,7 -160,7 +160,7 @@@ defmodule Pleroma.Web.ActivityPub.Activ
  
        assert activity.data["to"] == ["user1", "user2"]
        assert activity.actor == user.ap_id
-       assert activity.recipients == ["user1", "user2"]
+       assert activity.recipients == ["user1", "user2", user.ap_id]
      end
    end
  
  
      {:ok, user} = User.block(user, %{ap_id: activity_three.data["actor"]})
      {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster)
 -    %Activity{} = boost_activity = Activity.get_create_activity_by_object_ap_id(id)
 +    %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id)
      activity_three = Repo.get(Activity, activity_three.id)
  
      activities = ActivityPub.fetch_activities([], %{"blocking_user" => user})
        assert like_activity == same_like_activity
        assert object.data["likes"] == [user.ap_id]
  
 -      [note_activity] = Activity.all_by_object_ap_id(object.data["id"])
 +      [note_activity] = Activity.get_all_create_by_object_ap_id(object.data["id"])
        assert note_activity.data["object"]["like_count"] == 1
  
        {:ok, _like_activity, object} = ActivityPub.like(user_two, object)
        {:ok, object} =
          ActivityPub.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
  
 -      assert activity = Activity.get_create_activity_by_object_ap_id(object.data["id"])
 +      assert activity = Activity.get_create_by_object_ap_id(object.data["id"])
        assert activity.data["id"]
  
        {:ok, object_again} =
  
      test "it works with objects only available via Ostatus" do
        {:ok, object} = ActivityPub.fetch_object_from_id("https://shitposter.club/notice/2827873")
 -      assert activity = Activity.get_create_activity_by_object_ap_id(object.data["id"])
 +      assert activity = Activity.get_create_by_object_ap_id(object.data["id"])
        assert activity.data["id"]
  
        {:ok, object_again} =
index 8443dc856ba420d592f4279e083e4b05fbb3bf62,fdd3f12130e6f16d336827bcd3c062a5e2965080..6004285d6b6f9aa7c3bbd0358ef82a59ae8de5f1
@@@ -10,7 -10,6 +10,7 @@@ defmodule Pleroma.Web.MastodonAPI.Masto
    alias Pleroma.Web.{OStatus, CommonAPI}
    alias Pleroma.Web.ActivityPub.ActivityPub
    alias Pleroma.Web.MastodonAPI.FilterView
 +  alias Ecto.Changeset
    import Pleroma.Factory
    import ExUnit.CaptureLog
    import Tesla.Mock
  
      assert %{"id" => id, "visibility" => "direct"} = json_response(conn, 200)
      assert activity = Repo.get(Activity, id)
-     assert activity.recipients == [user2.ap_id]
+     assert activity.recipients == [user2.ap_id, user1.ap_id]
      assert activity.data["to"] == [user2.ap_id]
      assert activity.data["cc"] == []
    end
      assert %{"visibility" => "direct"} = status
      assert status["url"] != direct.data["id"]
  
+     # User should be able to see his own direct message
+     res_conn =
+       build_conn()
+       |> assign(:user, user_one)
+       |> get("api/v1/timelines/direct")
+     [status] = json_response(res_conn, 200)
+     assert %{"visibility" => "direct"} = status
      # Both should be visible here
      res_conn =
        conn
          |> assign(:user, user)
          |> get("/api/v1/filters/#{filter.filter_id}")
  
 -      assert response = json_response(conn, 200)
 +      assert _response = json_response(conn, 200)
      end
  
      test "update a filter", %{conn: conn} do
          |> get("/api/v1/notifications")
  
        expected_response =
 -        "hi <span><a data-user=\"#{user.id}\" href=\"#{user.ap_id}\">@<span>#{user.nickname}</span></a></span>"
 +        "hi <span class=\"h-card\"><a data-user=\"#{user.id}\" class=\"u-url mention\" href=\"#{
 +          user.ap_id
 +        }\">@<span>#{user.nickname}</span></a></span>"
  
        assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200)
        assert response == expected_response
          |> get("/api/v1/notifications/#{notification.id}")
  
        expected_response =
 -        "hi <span><a data-user=\"#{user.id}\" href=\"#{user.ap_id}\">@<span>#{user.nickname}</span></a></span>"
 +        "hi <span class=\"h-card\"><a data-user=\"#{user.id}\" class=\"u-url mention\" href=\"#{
 +          user.ap_id
 +        }\">@<span>#{user.nickname}</span></a></span>"
  
        assert %{"status" => %{"content" => response}} = json_response(conn, 200)
        assert response == expected_response
      assert [status] = json_response(first_conn, 200)
      assert status["id"] == to_string(activity.id)
  
 -    assert [{"link", link_header}] =
 +    assert [{"link", _link_header}] =
               Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
  
      # Honours query params
        assert user = json_response(conn, 200)
  
        assert user["note"] ==
 -               "I drink <a data-tag=\"cofe\" href=\"http://localhost:4001/tag/cofe\">#cofe</a> with <span><a data-user=\"#{
 +               "I drink <a class=\"hashtag\" data-tag=\"cofe\" href=\"http://localhost:4001/tag/cofe\">#cofe</a> with <span class=\"h-card\"><a data-user=\"#{
                   user2.id
 -               }\" href=\"#{user2.ap_id}\">@<span>#{user2.nickname}</span></a></span>"
 +               }\" class=\"u-url mention\" href=\"#{user2.ap_id}\">@<span>#{user2.nickname}</span></a></span>"
      end
  
      test "updates the user's locking status", %{conn: conn} do
    end
  
    test "get instance information", %{conn: conn} do
 -    insert(:user, %{local: true})
      user = insert(:user, %{local: true})
 -    insert(:user, %{local: false})
 +
 +    user2 = insert(:user, %{local: true})
 +    {:ok, _user2} = User.deactivate(user2, !user2.info.deactivated)
 +
 +    insert(:user, %{local: false, nickname: "u@peer1.com"})
 +    insert(:user, %{local: false, nickname: "u@peer2.com"})
  
      {:ok, _} = TwitterAPI.create_status(user, %{"status" => "cofe"})
  
 +    # Stats should count users with missing or nil `info.deactivated` value
 +    user = Repo.get(User, user.id)
 +    info_change = Changeset.change(user.info, %{deactivated: nil})
 +
 +    {:ok, _user} =
 +      user
 +      |> Changeset.change()
 +      |> Changeset.put_embed(:info, info_change)
 +      |> User.update_and_set_cache()
 +
      Pleroma.Stats.update_stats()
  
 -    conn =
 -      conn
 -      |> get("/api/v1/instance")
 +    conn = get(conn, "/api/v1/instance")
 +
 +    assert result = json_response(conn, 200)
 +
 +    stats = result["stats"]
 +
 +    assert stats
 +    assert stats["user_count"] == 1
 +    assert stats["status_count"] == 1
 +    assert stats["domain_count"] == 2
 +  end
 +
 +  test "get peers", %{conn: conn} do
 +    insert(:user, %{local: false, nickname: "u@peer1.com"})
 +    insert(:user, %{local: false, nickname: "u@peer2.com"})
 +
 +    Pleroma.Stats.update_stats()
 +
 +    conn = get(conn, "/api/v1/instance/peers")
  
      assert result = json_response(conn, 200)
  
 -    assert result["stats"]["user_count"] == 2
 -    assert result["stats"]["status_count"] == 1
 +    assert ["peer1.com", "peer2.com"] == Enum.sort(result)
    end
  
    test "put settings", %{conn: conn} do
        |> assign(:user, user)
        |> put("/api/web/settings", %{"data" => %{"programming" => "socks"}})
  
 -    assert result = json_response(conn, 200)
 +    assert _result = json_response(conn, 200)
  
      user = User.get_cached_by_ap_id(user.ap_id)
      assert user.info.settings == %{"programming" => "socks"}
index e33479368d9ed5cec9c925656d5e7e605a069da2,82bf71c70407fe836b90623f126b512d17981b9c..ebf6273e8985dd85212cdbc2662f06cf36fbafa9
@@@ -19,36 -19,6 +19,36 @@@ defmodule Pleroma.Web.MastodonAPI.Statu
      :ok
    end
  
 +  test "returns a temporary ap_id based user for activities missing db users" do
 +    user = insert(:user)
 +
 +    {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
 +
 +    Repo.delete(user)
 +    Cachex.clear(:user_cache)
 +
 +    %{account: ms_user} = StatusView.render("status.json", activity: activity)
 +
 +    assert ms_user.acct == "erroruser@example.com"
 +  end
 +
 +  test "tries to get a user by nickname if fetching by ap_id doesn't work" do
 +    user = insert(:user)
 +
 +    {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
 +
 +    {:ok, user} =
 +      user
 +      |> Ecto.Changeset.change(%{ap_id: "#{user.ap_id}/extension/#{user.nickname}"})
 +      |> Repo.update()
 +
 +    Cachex.clear(:user_cache)
 +
 +    result = StatusView.render("status.json", activity: activity)
 +
 +    assert result[:account][:id] == to_string(user.id)
 +  end
 +
    test "a note with null content" do
      note = insert(:note_activity)
  
  
      status = StatusView.render("status.json", %{activity: activity})
  
-     assert status.mentions == [AccountView.render("mention.json", %{user: user})]
+     actor = Repo.get_by(User, ap_id: activity.actor)
+     assert status.mentions ==
+              Enum.map([user, actor], fn u -> AccountView.render("mention.json", %{user: u}) end)
    end
  
    test "attachments" do
          "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
        )
  
 -    %Activity{} = activity = Activity.get_create_activity_by_object_ap_id(object.data["id"])
 +    %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
  
      represented = StatusView.render("status.json", %{for: user, activity: activity})