Merge remote-tracking branch 'origin/develop' into conversations_three
authorlain <lain@soykaf.club>
Wed, 1 May 2019 16:40:41 +0000 (18:40 +0200)
committerlain <lain@soykaf.club>
Wed, 1 May 2019 16:40:41 +0000 (18:40 +0200)
1  2 
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
lib/pleroma/web/router.ex
test/web/activity_pub/activity_pub_test.exs
test/web/mastodon_api/mastodon_api_controller_test.exs

index 577e6a59eea7527888b6e4a74733dce2556b9219,483a2153fb31db107b200b6a18215c151d50ab6f..28754e864e75f37a111285a4c6f80c052eefa1fb
@@@ -4,17 -4,17 +4,18 @@@
  
  defmodule Pleroma.Web.ActivityPub.ActivityPub do
    alias Pleroma.Activity
 +  alias Pleroma.Conversation
    alias Pleroma.Instances
    alias Pleroma.Notification
    alias Pleroma.Object
+   alias Pleroma.Object.Fetcher
+   alias Pleroma.Pagination
    alias Pleroma.Repo
    alias Pleroma.Upload
    alias Pleroma.User
    alias Pleroma.Web.ActivityPub.MRF
    alias Pleroma.Web.ActivityPub.Transmogrifier
    alias Pleroma.Web.Federator
-   alias Pleroma.Web.OStatus
    alias Pleroma.Web.WebFinger
  
    import Ecto.Query
    end
  
    def increase_replies_count_if_reply(%{
-         "object" =>
-           %{"inReplyTo" => reply_ap_id, "inReplyToStatusId" => reply_status_id} = object,
+         "object" => %{"inReplyTo" => reply_ap_id} = object,
          "type" => "Create"
        }) do
      if is_public?(object) do
-       Activity.increase_replies_count(reply_status_id)
        Object.increase_replies_count(reply_ap_id)
      end
    end
    def increase_replies_count_if_reply(_create_data), do: :noop
  
    def decrease_replies_count_if_reply(%Object{
-         data: %{"inReplyTo" => reply_ap_id, "inReplyToStatusId" => reply_status_id} = object
+         data: %{"inReplyTo" => reply_ap_id} = object
        }) do
      if is_public?(object) do
-       Activity.decrease_replies_count(reply_status_id)
        Object.decrease_replies_count(reply_ap_id)
      end
    end
           {:ok, map} <- MRF.filter(map),
           {recipients, _, _} = get_recipients(map),
           {:fake, false, map, recipients} <- {:fake, fake, map, recipients},
-          {:ok, object} <- insert_full_object(map) do
+          {:ok, map, object} <- insert_full_object(map) do
        {:ok, activity} =
          Repo.insert(%Activity{
            data: map,
        end)
  
        Notification.create_notifications(activity)
 +      Conversation.create_or_bump_for(activity)
        stream_out(activity)
        {:ok, activity}
      else
          end
  
          if activity.data["type"] in ["Create"] do
-           activity.data["object"]
+           object = Object.normalize(activity)
+           object.data
            |> Map.get("tag", [])
            |> Enum.filter(fn tag -> is_bitstring(tag) end)
            |> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end)
  
-           if activity.data["object"]["attachment"] != [] do
+           if object.data["attachment"] != [] do
              Pleroma.Web.Streamer.stream("public:media", activity)
  
              if activity.local do
          if !Enum.member?(activity.data["cc"] || [], public) &&
               !Enum.member?(
                 activity.data["to"],
-                User.get_by_ap_id(activity.data["actor"]).follower_address
+                User.get_cached_by_ap_id(activity.data["actor"]).follower_address
               ),
             do: Pleroma.Web.Streamer.stream("direct", activity)
        end
           :ok <- maybe_federate(activity) do
        Enum.each(User.all_superusers(), fn superuser ->
          superuser
-         |> Pleroma.AdminEmail.report(actor, account, statuses, content)
-         |> Pleroma.Mailer.deliver_async()
+         |> Pleroma.Emails.AdminEmail.report(actor, account, statuses, content)
+         |> Pleroma.Emails.Mailer.deliver_async()
        end)
  
        {:ok, activity}
      end
    end
  
 -  def fetch_activities_for_context(context, opts \\ %{}) do
 +  defp fetch_activities_for_context_query(context, opts) do
      public = ["https://www.w3.org/ns/activitystreams#Public"]
  
      recipients =
        if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public
  
 -    query = from(activity in Activity)
 -
 -    query =
 -      query
 -      |> restrict_blocked(opts)
 -      |> restrict_recipients(recipients, opts["user"])
 -
 -    query =
 -      from(
 -        activity in query,
 -        where:
 -          fragment(
 -            "?->>'type' = ? and ?->>'context' = ?",
 -            activity.data,
 -            "Create",
 -            activity.data,
 -            ^context
 -          ),
 -        order_by: [desc: :id]
 +    from(activity in Activity)
 +    |> restrict_blocked(opts)
 +    |> restrict_recipients(recipients, opts["user"])
 +    |> where(
 +      [activity],
 +      fragment(
 +        "?->>'type' = ? and ?->>'context' = ?",
 +        activity.data,
 +        "Create",
 +        activity.data,
 +        ^context
        )
 -      |> Activity.with_preloaded_object()
 +    )
 +    |> order_by([activity], desc: activity.id)
 +  end
 +
 +  @spec fetch_activities_for_context(String.t(), keyword() | map()) :: [Activity.t()]
 +  def fetch_activities_for_context(context, opts \\ %{}) do
 +    context
 +    |> fetch_activities_for_context_query(opts)
 +    |> Activity.with_preloaded_object()
 +    |> Repo.all()
 +  end
  
 -    Repo.all(query)
 +  @spec fetch_latest_activity_id_for_context(String.t(), keyword() | map()) ::
 +          Pleroma.FlakeId.t() | nil
 +  def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
 +    context
 +    |> fetch_activities_for_context_query(opts)
 +    |> limit(1)
 +    |> select([a], a.id)
 +    |> Repo.one()
    end
  
    def fetch_public_activities(opts \\ %{}) do
  
      q
      |> restrict_unlisted()
-     |> Repo.all()
+     |> Pagination.fetch_paginated(opts)
      |> Enum.reverse()
    end
  
  
    defp restrict_since(query, _), do: query
  
+   defp restrict_tag_reject(_query, %{"tag_reject" => _tag_reject, "skip_preload" => true}) do
+     raise "Can't use the child object without preloading!"
+   end
    defp restrict_tag_reject(query, %{"tag_reject" => tag_reject})
         when is_list(tag_reject) and tag_reject != [] do
      from(
-       activity in query,
-       where: fragment(~s(\(not \(? #> '{"object","tag"}'\) \\?| ?\)), activity.data, ^tag_reject)
+       [_activity, object] in query,
+       where: fragment("not (?)->'tag' \\?| (?)", object.data, ^tag_reject)
      )
    end
  
    defp restrict_tag_reject(query, _), do: query
  
+   defp restrict_tag_all(_query, %{"tag_all" => _tag_all, "skip_preload" => true}) do
+     raise "Can't use the child object without preloading!"
+   end
    defp restrict_tag_all(query, %{"tag_all" => tag_all})
         when is_list(tag_all) and tag_all != [] do
      from(
-       activity in query,
-       where: fragment(~s(\(? #> '{"object","tag"}'\) \\?& ?), activity.data, ^tag_all)
+       [_activity, object] in query,
+       where: fragment("(?)->'tag' \\?& (?)", object.data, ^tag_all)
      )
    end
  
    defp restrict_tag_all(query, _), do: query
  
+   defp restrict_tag(_query, %{"tag" => _tag, "skip_preload" => true}) do
+     raise "Can't use the child object without preloading!"
+   end
    defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do
      from(
-       activity in query,
-       where: fragment(~s(\(? #> '{"object","tag"}'\) \\?| ?), activity.data, ^tag)
+       [_activity, object] in query,
+       where: fragment("(?)->'tag' \\?| (?)", object.data, ^tag)
      )
    end
  
    defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do
      from(
-       activity in query,
-       where: fragment(~s(? <@ (? #> '{"object","tag"}'\)), ^tag, activity.data)
+       [_activity, object] in query,
+       where: fragment("(?)->'tag' \\? (?)", object.data, ^tag)
      )
    end
  
      )
    end
  
-   defp restrict_limit(query, %{"limit" => limit}) do
-     from(activity in query, limit: ^limit)
-   end
-   defp restrict_limit(query, _), do: query
    defp restrict_local(query, %{"local_only" => true}) do
      from(activity in query, where: activity.local == true)
    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
-   defp restrict_max(query, _), do: query
    defp restrict_actor(query, %{"actor_id" => actor_id}) do
      from(activity in query, where: activity.actor == ^actor_id)
    end
  
    defp restrict_favorited_by(query, _), do: query
  
+   defp restrict_media(_query, %{"only_media" => _val, "skip_preload" => true}) do
+     raise "Can't use the child object without preloading!"
+   end
    defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do
      from(
-       activity in query,
-       where: fragment(~s(not (? #> '{"object","attachment"}' = ?\)), activity.data, ^[])
+       [_activity, object] in query,
+       where: fragment("not (?)->'attachment' = (?)", object.data, ^[])
      )
    end
  
      from(
        activity in query,
        where: fragment("not (? = ANY(?))", activity.actor, ^blocks),
-       where: fragment("not (?->'to' \\?| ?)", activity.data, ^blocks),
+       where: fragment("not (? && ?)", activity.recipients, ^blocks),
+       where:
+         fragment(
+           "not (?->>'type' = 'Announce' and ?->'to' \\?| ?)",
+           activity.data,
+           activity.data,
+           ^blocks
+         ),
        where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks)
      )
    end
    end
  
    def fetch_activities_query(recipients, opts \\ %{}) do
-     base_query =
-       from(
-         activity in Activity,
-         limit: 20,
-         order_by: [fragment("? desc nulls last", activity.id)]
-       )
+     base_query = from(activity in Activity)
  
      base_query
      |> maybe_preload_objects(opts)
      |> restrict_tag_all(opts)
      |> restrict_since(opts)
      |> restrict_local(opts)
-     |> restrict_limit(opts)
-     |> restrict_max(opts)
      |> restrict_actor(opts)
      |> restrict_type(opts)
      |> restrict_favorited_by(opts)
  
    def fetch_activities(recipients, opts \\ %{}) do
      fetch_activities_query(recipients, opts)
-     |> Repo.all()
+     |> Pagination.fetch_paginated(opts)
      |> Enum.reverse()
    end
  
    def fetch_activities_bounded(recipients_to, recipients_cc, opts \\ %{}) do
      fetch_activities_query([], opts)
      |> restrict_to_cc(recipients_to, recipients_cc)
-     |> Repo.all()
+     |> Pagination.fetch_paginated(opts)
      |> Enum.reverse()
    end
  
    end
  
    def fetch_and_prepare_user_from_ap_id(ap_id) do
-     with {:ok, data} <- fetch_and_contain_remote_object_from_id(ap_id) do
+     with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id) do
        user_data_from_user_object(data)
      else
        e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
    end
  
    def make_user_from_ap_id(ap_id) do
-     if _user = User.get_by_ap_id(ap_id) do
+     if _user = User.get_cached_by_ap_id(ap_id) do
        Transmogrifier.upgrade_user_from_ap_id(ap_id)
      else
        with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do
      end
    end
  
-   # TODO:
-   # This will create a Create activity, which we need internally at the moment.
-   def fetch_object_from_id(id) do
-     if object = Object.get_cached_by_ap_id(id) do
-       {:ok, object}
-     else
-       with {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
-            nil <- Object.normalize(data),
-            params <- %{
-              "type" => "Create",
-              "to" => data["to"],
-              "cc" => data["cc"],
-              "actor" => data["actor"] || data["attributedTo"],
-              "object" => data
-            },
-            :ok <- Transmogrifier.contain_origin(id, params),
-            {:ok, activity} <- Transmogrifier.handle_incoming(params) do
-         {:ok, Object.normalize(activity)}
-       else
-         {:error, {:reject, nil}} ->
-           {:reject, nil}
-         object = %Object{} ->
-           {:ok, object}
-         _e ->
-           Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
-           case OStatus.fetch_activity_from_url(id) do
-             {:ok, [activity | _]} -> {:ok, Object.normalize(activity)}
-             e -> e
-           end
-       end
-     end
-   end
-   def fetch_and_contain_remote_object_from_id(id) do
-     Logger.info("Fetching object #{id} via AP")
-     with true <- String.starts_with?(id, "http"),
-          {:ok, %{body: body, status: code}} when code in 200..299 <-
-            @httpoison.get(
-              id,
-              [{:Accept, "application/activity+json"}]
-            ),
-          {:ok, data} <- Jason.decode(body),
-          :ok <- Transmogrifier.contain_origin_from_id(id, data) do
-       {:ok, data}
-     else
-       e ->
-         {:error, e}
-     end
-   end
    # filter out broken threads
    def contain_broken_threads(%Activity{} = activity, %User{} = user) do
      entire_thread_visible_for_user?(activity, user)
index d5b6a943f24bee28441d0ee5f89760c54e25e42b,ed585098a7b520253e1ce8a258e3670d6d261d31..aa3f46482a5e0ff00bb8ca128099bb2de4c2f9ce
@@@ -4,14 -4,15 +4,16 @@@
  
  defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
    use Pleroma.Web, :controller
    alias Ecto.Changeset
    alias Pleroma.Activity
+   alias Pleroma.Bookmark
    alias Pleroma.Config
 +  alias Pleroma.Conversation.Participation
    alias Pleroma.Filter
    alias Pleroma.Notification
    alias Pleroma.Object
+   alias Pleroma.Object.Fetcher
+   alias Pleroma.Pagination
    alias Pleroma.Repo
    alias Pleroma.ScheduledActivity
    alias Pleroma.Stats
@@@ -22,7 -23,6 +24,7 @@@
    alias Pleroma.Web.CommonAPI
    alias Pleroma.Web.MastodonAPI.AccountView
    alias Pleroma.Web.MastodonAPI.AppView
 +  alias Pleroma.Web.MastodonAPI.ConversationView
    alias Pleroma.Web.MastodonAPI.FilterView
    alias Pleroma.Web.MastodonAPI.ListView
    alias Pleroma.Web.MastodonAPI.MastodonAPI
@@@ -36,7 -36,7 +38,7 @@@
    alias Pleroma.Web.OAuth.Authorization
    alias Pleroma.Web.OAuth.Token
  
-   import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
+   alias Pleroma.Web.ControllerHelper
    import Ecto.Query
  
    require Logger
@@@ -47,7 -47,7 +49,7 @@@
    action_fallback(:errors)
  
    def create_app(conn, params) do
-     scopes = oauth_scopes(params, ["read"])
+     scopes = ControllerHelper.oauth_scopes(params, ["read"])
  
      app_attrs =
        params
        end)
  
      info_params =
-       %{}
-       |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
+       [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
+       |> Enum.reduce(%{}, fn key, acc ->
+         add_if_present(acc, params, to_string(key), key, fn value ->
+           {:ok, ControllerHelper.truthy_param?(value)}
+         end)
+       end)
+       |> add_if_present(params, "default_scope", :default_scope)
        |> add_if_present(params, "header", :banner, fn value ->
          with %Plug.Upload{} <- value,
               {:ok, object} <- ActivityPub.upload(value, type: :banner) do
          end
        end)
  
-     info_cng = User.Info.mastodon_profile_update(user.info, info_params)
+     info_cng = User.Info.profile_update(user.info, info_params)
  
      with changeset <- User.update_changeset(user, user_params),
           changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
          "static_url" => url,
          "visible_in_picker" => true,
          "url" => url,
-         "tags" => String.split(tags, ",")
+         "tags" => tags
        }
      end)
    end
    defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
      params =
        conn.params
-       |> Map.drop(["since_id", "max_id"])
+       |> Map.drop(["since_id", "max_id", "min_id"])
        |> Map.merge(params)
  
      last = List.last(activities)
-     first = List.first(activities)
  
      if last do
-       min = last.id
-       max = first.id
+       max_id = last.id
+       limit =
+         params
+         |> Map.get("limit", "20")
+         |> String.to_integer()
+       min_id =
+         if length(activities) <= limit do
+           activities
+           |> List.first()
+           |> Map.get(:id)
+         else
+           activities
+           |> Enum.at(limit * -1)
+           |> Map.get(:id)
+         end
  
        {next_url, prev_url} =
          if param do
                Pleroma.Web.Endpoint,
                method,
                param,
-               Map.merge(params, %{max_id: min})
+               Map.merge(params, %{max_id: max_id})
              ),
              mastodon_api_url(
                Pleroma.Web.Endpoint,
                method,
                param,
-               Map.merge(params, %{since_id: max})
+               Map.merge(params, %{min_id: min_id})
              )
            }
          else
              mastodon_api_url(
                Pleroma.Web.Endpoint,
                method,
-               Map.merge(params, %{max_id: min})
+               Map.merge(params, %{max_id: max_id})
              ),
              mastodon_api_url(
                Pleroma.Web.Endpoint,
                method,
-               Map.merge(params, %{since_id: max})
+               Map.merge(params, %{min_id: min_id})
              )
            }
          end
        |> ActivityPub.contain_timeline(user)
        |> Enum.reverse()
  
+     user = Repo.preload(user, bookmarks: :activity)
      conn
      |> add_link_headers(:home_timeline, activities)
      |> put_view(StatusView)
        |> ActivityPub.fetch_public_activities()
        |> Enum.reverse()
  
+     user = Repo.preload(user, bookmarks: :activity)
      conn
      |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
      |> put_view(StatusView)
    end
  
    def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
-     with %User{} = user <- User.get_by_id(params["id"]) do
+     with %User{} = user <- User.get_cached_by_id(params["id"]),
+          reading_user <- Repo.preload(reading_user, :bookmarks) do
        activities = ActivityPub.fetch_user_activities(user, reading_user, params)
  
        conn
      activities =
        [user.ap_id]
        |> ActivityPub.fetch_activities_query(params)
-       |> Repo.all()
+       |> Pagination.fetch_paginated(params)
+     user = Repo.preload(user, bookmarks: :activity)
  
      conn
      |> add_link_headers(:dm_timeline, activities)
    end
  
    def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-     with %Activity{} = activity <- Activity.get_by_id(id),
+     with %Activity{} = activity <- Activity.get_by_id_with_object(id),
           true <- Visibility.visible_for_user?(activity, user) do
+       user = Repo.preload(user, bookmarks: :activity)
        conn
        |> put_view(StatusView)
        |> try_render("status.json", %{activity: activity, for: user})
    end
  
    def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
-     with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
+     with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
+          %Activity{} = announce <- Activity.normalize(announce.data) do
+       user = Repo.preload(user, bookmarks: :activity)
        conn
        |> put_view(StatusView)
        |> try_render("status.json", %{activity: announce, for: user, as: :activity})
  
    def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
      with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
-          %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
+          %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
+       user = Repo.preload(user, bookmarks: :activity)
        conn
        |> put_view(StatusView)
        |> try_render("status.json", %{activity: activity, for: user, as: :activity})
    end
  
    def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-     with %Activity{} = activity <- Activity.get_by_id(id),
-          %User{} = user <- User.get_by_nickname(user.nickname),
+     with %Activity{} = activity <- Activity.get_by_id_with_object(id),
+          %User{} = user <- User.get_cached_by_nickname(user.nickname),
           true <- Visibility.visible_for_user?(activity, user),
-          {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
+          {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
+       user = Repo.preload(user, bookmarks: :activity)
        conn
        |> put_view(StatusView)
        |> try_render("status.json", %{activity: activity, for: user, as: :activity})
    end
  
    def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-     with %Activity{} = activity <- Activity.get_by_id(id),
-          %User{} = user <- User.get_by_nickname(user.nickname),
+     with %Activity{} = activity <- Activity.get_by_id_with_object(id),
+          %User{} = user <- User.get_cached_by_nickname(user.nickname),
           true <- Visibility.visible_for_user?(activity, user),
-          {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
+          {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
+       user = Repo.preload(user, bookmarks: :activity)
        conn
        |> put_view(StatusView)
        |> try_render("status.json", %{activity: activity, for: user, as: :activity})
      end
    end
  
+   def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
+     Notification.destroy_multiple(user, ids)
+     json(conn, %{})
+   end
    def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
      id = List.wrap(id)
      q = from(u in User, where: u.id in ^id)
    end
  
    def favourited_by(conn, %{"id" => id}) do
-     with %Activity{data: %{"object" => %{"likes" => likes}}} <- Activity.get_by_id(id) do
+     with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
+          %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
        q = from(u in User, where: u.ap_id in ^likes)
        users = Repo.all(q)
  
    end
  
    def reblogged_by(conn, %{"id" => id}) do
-     with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Activity.get_by_id(id) do
+     with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
+          %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
        q = from(u in User, where: u.ap_id in ^announces)
        users = Repo.all(q)
  
    end
  
    def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
-     with %User{} = user <- User.get_by_id(id),
+     with %User{} = user <- User.get_cached_by_id(id),
           followers <- MastodonAPI.get_followers(user, params) do
        followers =
          cond do
    end
  
    def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
-     with %User{} = user <- User.get_by_id(id),
+     with %User{} = user <- User.get_cached_by_id(id),
           followers <- MastodonAPI.get_friends(user, params) do
        followers =
          cond do
    end
  
    def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
-     with %User{} = follower <- User.get_by_id(id),
+     with %User{} = follower <- User.get_cached_by_id(id),
           {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
        conn
        |> put_view(AccountView)
    end
  
    def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
-     with %User{} = follower <- User.get_by_id(id),
+     with %User{} = follower <- User.get_cached_by_id(id),
           {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
        conn
        |> put_view(AccountView)
    end
  
    def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
-     with %User{} = followed <- User.get_by_id(id),
-          false <- User.following?(follower, followed),
-          {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
+     with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
+          {_, true} <- {:followed, follower.id != followed.id},
+          {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
        conn
        |> put_view(AccountView)
        |> render("relationship.json", %{user: follower, target: followed})
      else
-       true ->
-         followed = User.get_cached_by_id(id)
-         {:ok, follower} =
-           case conn.params["reblogs"] do
-             true -> CommonAPI.show_reblogs(follower, followed)
-             false -> CommonAPI.hide_reblogs(follower, followed)
-           end
-         conn
-         |> put_view(AccountView)
-         |> render("relationship.json", %{user: follower, target: followed})
+       {:followed, _} ->
+         {:error, :not_found}
  
        {:error, message} ->
          conn
    end
  
    def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
-     with %User{} = followed <- User.get_by_nickname(uri),
+     with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
+          {_, true} <- {:followed, follower.id != followed.id},
           {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
        conn
        |> put_view(AccountView)
        |> render("account.json", %{user: followed, for: follower})
      else
+       {:followed, _} ->
+         {:error, :not_found}
        {:error, message} ->
          conn
          |> put_resp_content_type("application/json")
    end
  
    def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
-     with %User{} = followed <- User.get_by_id(id),
+     with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
+          {_, true} <- {:followed, follower.id != followed.id},
           {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
        conn
        |> put_view(AccountView)
        |> render("relationship.json", %{user: follower, target: followed})
+     else
+       {:followed, _} ->
+         {:error, :not_found}
+       error ->
+         error
      end
    end
  
    def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
-     with %User{} = muted <- User.get_by_id(id),
+     with %User{} = muted <- User.get_cached_by_id(id),
           {:ok, muter} <- User.mute(muter, muted) do
        conn
        |> put_view(AccountView)
    end
  
    def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
-     with %User{} = muted <- User.get_by_id(id),
+     with %User{} = muted <- User.get_cached_by_id(id),
           {:ok, muter} <- User.unmute(muter, muted) do
        conn
        |> put_view(AccountView)
    end
  
    def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
-     with %User{} = blocked <- User.get_by_id(id),
+     with %User{} = blocked <- User.get_cached_by_id(id),
           {:ok, blocker} <- User.block(blocker, blocked),
           {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
        conn
    end
  
    def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
-     with %User{} = blocked <- User.get_by_id(id),
+     with %User{} = blocked <- User.get_cached_by_id(id),
           {:ok, blocker} <- User.unblock(blocker, blocked),
           {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
        conn
      json(conn, %{})
    end
  
+   def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+     with %User{} = subscription_target <- User.get_cached_by_id(id),
+          {:ok, subscription_target} = User.subscribe(user, subscription_target) do
+       conn
+       |> put_view(AccountView)
+       |> render("relationship.json", %{user: user, target: subscription_target})
+     else
+       {:error, message} ->
+         conn
+         |> put_resp_content_type("application/json")
+         |> send_resp(403, Jason.encode!(%{"error" => message}))
+     end
+   end
+   def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+     with %User{} = subscription_target <- User.get_cached_by_id(id),
+          {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
+       conn
+       |> put_view(AccountView)
+       |> render("relationship.json", %{user: user, target: subscription_target})
+     else
+       {:error, message} ->
+         conn
+         |> put_resp_content_type("application/json")
+         |> send_resp(403, Jason.encode!(%{"error" => message}))
+     end
+   end
    def status_search(user, query) do
      fetched =
        if Regex.match?(~r/https?:/, query) do
-         with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
+         with {:ok, object} <- Fetcher.fetch_object_from_id(query),
               %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
               true <- Visibility.visible_for_user?(activity, user) do
            [activity]
  
      q =
        from(
-         a in Activity,
+         [a, o] in Activity.with_preloaded_object(Activity),
          where: fragment("?->>'type' = 'Create'", a.data),
          where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
          where:
            fragment(
-             "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
-             a.data,
+             "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
+             o.data,
              ^query
            ),
          limit: 20,
        ActivityPub.fetch_activities([], params)
        |> Enum.reverse()
  
+     user = Repo.preload(user, bookmarks: :activity)
      conn
      |> add_link_headers(:favourites, activities)
      |> put_view(StatusView)
      |> render("index.json", %{activities: activities, for: user, as: :activity})
    end
  
-   def bookmarks(%{assigns: %{user: user}} = conn, _) do
-     user = User.get_by_id(user.id)
+   def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
+     with %User{} = user <- User.get_by_id(id),
+          false <- user.info.hide_favorites do
+       params =
+         params
+         |> Map.put("type", "Create")
+         |> Map.put("favorited_by", user.ap_id)
+         |> Map.put("blocking_user", for_user)
+       recipients =
+         if for_user do
+           ["https://www.w3.org/ns/activitystreams#Public"] ++
+             [for_user.ap_id | for_user.following]
+         else
+           ["https://www.w3.org/ns/activitystreams#Public"]
+         end
+       activities =
+         recipients
+         |> ActivityPub.fetch_activities(params)
+         |> Enum.reverse()
+       conn
+       |> add_link_headers(:favourites, activities)
+       |> put_view(StatusView)
+       |> render("index.json", %{activities: activities, for: for_user, as: :activity})
+     else
+       nil ->
+         {:error, :not_found}
+       true ->
+         conn
+         |> put_status(403)
+         |> json(%{error: "Can't get favorites"})
+     end
+   end
+   def bookmarks(%{assigns: %{user: user}} = conn, params) do
+     user = User.get_cached_by_id(user.id)
+     user = Repo.preload(user, bookmarks: :activity)
+     bookmarks =
+       Bookmark.for_user_query(user.id)
+       |> Pagination.fetch_paginated(params)
  
      activities =
-       user.bookmarks
-       |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
-       |> Enum.reverse()
+       bookmarks
+       |> Enum.map(fn b -> b.activity end)
  
      conn
+     |> add_link_headers(:bookmarks, bookmarks)
      |> put_view(StatusView)
      |> render("index.json", %{activities: activities, for: user, as: :activity})
    end
      accounts
      |> Enum.each(fn account_id ->
        with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
-            %User{} = followed <- User.get_by_id(account_id) do
+            %User{} = followed <- User.get_cached_by_id(account_id) do
          Pleroma.List.follow(list, followed)
        end
      end)
      accounts
      |> Enum.each(fn account_id ->
        with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
-            %User{} = followed <- Pleroma.User.get_by_id(account_id) do
+            %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
          Pleroma.List.unfollow(list, followed)
        end
      end)
          |> ActivityPub.fetch_activities_bounded(following, params)
          |> Enum.reverse()
  
+       user = Repo.preload(user, bookmarks: :activity)
        conn
        |> put_view(StatusView)
        |> render("index.json", %{activities: activities, for: user, as: :activity})
    def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
      Logger.debug("Unimplemented, returning unmodified relationship")
  
-     with %User{} = target <- User.get_by_id(id) do
+     with %User{} = target <- User.get_cached_by_id(id) do
        conn
        |> put_view(AccountView)
        |> render("relationship.json", %{user: user, target: target})
                x,
                "id",
                case User.get_or_fetch(x["acct"]) do
-                 %{id: id} -> id
+                 {:ok, %User{id: id}} -> id
                  _ -> 0
                end
              )
      end
    end
  
 +  def conversations(%{assigns: %{user: user}} = conn, params) do
 +    participations = Participation.for_user_with_last_activity_id(user, params)
 +
 +    conversations =
 +      Enum.map(participations, fn participation ->
 +        ConversationView.render("participation.json", %{participation: participation, user: user})
 +      end)
 +
 +    conn
 +    |> add_link_headers(:conversations, participations)
 +    |> json(conversations)
 +  end
 +
 +  def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
 +    with %Participation{} = participation <-
 +           Repo.get_by(Participation, id: participation_id, user_id: user.id),
 +         {:ok, participation} <- Participation.mark_as_read(participation) do
 +      participation_view =
 +        ConversationView.render("participation.json", %{participation: participation, user: user})
 +
 +      conn
 +      |> json(participation_view)
 +    end
 +  end
 +
    def try_render(conn, target, params)
        when is_binary(target) do
      res = render(conn, target, params)
index dc5119c50a7b263bab7d1510061ad9f191d1f48f,ff4f08af57a58a16cf7db7d033577bece2349649..6d9c77c1aa62646d793d31f8760ec442c0a09e9d
@@@ -135,6 -135,7 +135,7 @@@ defmodule Pleroma.Web.Router d
      post("/password_reset", UtilController, :password_reset)
      get("/emoji", UtilController, :emoji)
      get("/captcha", UtilController, :captcha)
+     get("/healthcheck", UtilController, :healthcheck)
    end
  
    scope "/api/pleroma", Pleroma.Web do
      delete("/relay", AdminAPIController, :relay_unfollow)
  
      get("/invite_token", AdminAPIController, :get_invite_token)
+     get("/invites", AdminAPIController, :invites)
+     post("/revoke_invite", AdminAPIController, :revoke_invite)
      post("/email_invite", AdminAPIController, :email_invite)
  
      get("/password_reset", AdminAPIController, :get_password_reset)
  
        post("/change_password", UtilController, :change_password)
        post("/delete_account", UtilController, :delete_account)
+       put("/notification_settings", UtilController, :update_notificaton_settings)
      end
  
      scope [] do
        get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials)
  
        get("/accounts/relationships", MastodonAPIController, :relationships)
-       get("/accounts/search", MastodonAPIController, :account_search)
  
        get("/accounts/:id/lists", MastodonAPIController, :account_lists)
        get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array)
        post("/notifications/dismiss", MastodonAPIController, :dismiss_notification)
        get("/notifications", MastodonAPIController, :notifications)
        get("/notifications/:id", MastodonAPIController, :get_notification)
+       delete("/notifications/destroy_multiple", MastodonAPIController, :destroy_multiple)
  
        get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses)
        get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status)
  
        get("/suggestions", MastodonAPIController, :suggestions)
  
 +      get("/conversations", MastodonAPIController, :conversations)
 +      post("/conversations/:id/read", MastodonAPIController, :conversation_read)
 +
        get("/endorsements", MastodonAPIController, :empty_array)
  
        get("/pleroma/flavour", MastodonAPIController, :get_flavour)
  
        post("/domain_blocks", MastodonAPIController, :block_domain)
        delete("/domain_blocks", MastodonAPIController, :unblock_domain)
+       post("/pleroma/accounts/:id/subscribe", MastodonAPIController, :subscribe)
+       post("/pleroma/accounts/:id/unsubscribe", MastodonAPIController, :unsubscribe)
      end
  
      scope [] do
  
      get("/trends", MastodonAPIController, :empty_array)
  
+     get("/accounts/search", MastodonAPIController, :account_search)
      scope [] do
        pipe_through(:oauth_read_or_unauthenticated)
  
        get("/accounts/:id", MastodonAPIController, :user)
  
        get("/search", MastodonAPIController, :search)
+       get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites)
      end
    end
  
index c3911500e66e77d30305a9336d24e95aff7f6992,f8e987e5827499040a7187a2ce335a0f08eb2a4d..15276ba7b193f0c02010b52f76c3842d06ba16e0
@@@ -84,17 -84,21 +84,21 @@@ defmodule Pleroma.Web.ActivityPub.Activ
        {:ok, status_two} = CommonAPI.post(user, %{"status" => ". #essais"})
        {:ok, status_three} = CommonAPI.post(user, %{"status" => ". #test #reject"})
  
-       fetch_one = ActivityPub.fetch_activities([], %{"tag" => "test"})
-       fetch_two = ActivityPub.fetch_activities([], %{"tag" => ["test", "essais"]})
+       fetch_one = ActivityPub.fetch_activities([], %{"type" => "Create", "tag" => "test"})
+       fetch_two =
+         ActivityPub.fetch_activities([], %{"type" => "Create", "tag" => ["test", "essais"]})
  
        fetch_three =
          ActivityPub.fetch_activities([], %{
+           "type" => "Create",
            "tag" => ["test", "essais"],
            "tag_reject" => ["reject"]
          })
  
        fetch_four =
          ActivityPub.fetch_activities([], %{
+           "type" => "Create",
            "tag" => ["test"],
            "tag_all" => ["test", "reject"]
          })
      end
  
      test "doesn't drop activities with content being null" do
 +      user = insert(:user)
 +
        data = %{
 -        "ok" => true,
 +        "actor" => user.ap_id,
 +        "to" => [],
          "object" => %{
 +          "actor" => user.ap_id,
 +          "to" => [],
 +          "type" => "Note",
            "content" => nil
          }
        }
      end
  
      test "inserts a given map into the activity database, giving it an id if it has none." do
 +      user = insert(:user)
 +
        data = %{
 -        "ok" => true
 +        "actor" => user.ap_id,
 +        "to" => [],
 +        "object" => %{
 +          "actor" => user.ap_id,
 +          "to" => [],
 +          "type" => "Note",
 +          "content" => "hey"
 +        }
        }
  
        {:ok, %Activity{} = activity} = ActivityPub.insert(data)
        given_id = "bla"
  
        data = %{
 -        "ok" => true,
          "id" => given_id,
 -        "context" => "blabla"
 +        "actor" => user.ap_id,
 +        "to" => [],
 +        "context" => "blabla",
 +        "object" => %{
 +          "actor" => user.ap_id,
 +          "to" => [],
 +          "type" => "Note",
 +          "content" => "hey"
 +        }
        }
  
        {:ok, %Activity{} = activity} = ActivityPub.insert(data)
      end
  
      test "adds a context when none is there" do
 +      user = insert(:user)
 +
        data = %{
 -        "id" => "some_id",
 +        "actor" => user.ap_id,
 +        "to" => [],
          "object" => %{
 -          "id" => "object_id"
 +          "actor" => user.ap_id,
 +          "to" => [],
 +          "type" => "Note",
 +          "content" => "hey"
          }
        }
  
      end
  
      test "adds an id to a given object if it lacks one and is a note and inserts it to the object database" do
 +      user = insert(:user)
 +
        data = %{
 +        "actor" => user.ap_id,
 +        "to" => [],
          "object" => %{
 +          "actor" => user.ap_id,
 +          "to" => [],
            "type" => "Note",
 -          "ok" => true
 +          "content" => "hey"
          }
        }
  
        {:ok, %Activity{} = activity} = ActivityPub.insert(data)
-       assert is_binary(activity.data["object"]["id"])
-       assert %Object{} = Object.get_by_ap_id(activity.data["object"]["id"])
+       object = Object.normalize(activity.data["object"])
+       assert is_binary(object.data["id"])
+       assert %Object{} = Object.get_by_ap_id(activity.data["object"])
      end
    end
  
            to: ["user1", "user1", "user2"],
            actor: user,
            context: "",
-           object: %{}
+           object: %{
+             "to" => ["user1", "user1", "user2"],
+             "type" => "Note",
+             "content" => "testing"
+           }
          })
  
        assert activity.data["to"] == ["user1", "user2"]
        user = insert(:user)
  
        {:ok, _} =
-         CommonAPI.post(User.get_by_id(user.id), %{"status" => "1", "visibility" => "public"})
+         CommonAPI.post(User.get_cached_by_id(user.id), %{
+           "status" => "1",
+           "visibility" => "public"
+         })
  
        {:ok, _} =
-         CommonAPI.post(User.get_by_id(user.id), %{"status" => "2", "visibility" => "unlisted"})
+         CommonAPI.post(User.get_cached_by_id(user.id), %{
+           "status" => "2",
+           "visibility" => "unlisted"
+         })
  
        {:ok, _} =
-         CommonAPI.post(User.get_by_id(user.id), %{"status" => "2", "visibility" => "private"})
+         CommonAPI.post(User.get_cached_by_id(user.id), %{
+           "status" => "2",
+           "visibility" => "private"
+         })
  
        {:ok, _} =
-         CommonAPI.post(User.get_by_id(user.id), %{"status" => "3", "visibility" => "direct"})
+         CommonAPI.post(User.get_cached_by_id(user.id), %{
+           "status" => "3",
+           "visibility" => "direct"
+         })
  
-       user = User.get_by_id(user.id)
+       user = User.get_cached_by_id(user.id)
        assert user.info.note_count == 2
      end
  
        # public
        {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "public"))
        assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
-       assert data["object"]["repliesCount"] == 1
        assert object.data["repliesCount"] == 1
  
        # unlisted
        {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "unlisted"))
        assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
-       assert data["object"]["repliesCount"] == 2
        assert object.data["repliesCount"] == 2
  
        # private
        {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "private"))
        assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
-       assert data["object"]["repliesCount"] == 2
        assert object.data["repliesCount"] == 2
  
        # direct
        {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "direct"))
        assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
-       assert data["object"]["repliesCount"] == 2
        assert object.data["repliesCount"] == 2
      end
    end
      assert Enum.member?(activities, activity_one)
    end
  
+   test "doesn't return transitive interactions concerning blocked users" do
+     blocker = insert(:user)
+     blockee = insert(:user)
+     friend = insert(:user)
+     {:ok, blocker} = User.block(blocker, blockee)
+     {:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"})
+     {:ok, activity_two} = CommonAPI.post(friend, %{"status" => "hey! @#{blockee.nickname}"})
+     {:ok, activity_three} = CommonAPI.post(blockee, %{"status" => "hey! @#{friend.nickname}"})
+     {:ok, activity_four} = CommonAPI.post(blockee, %{"status" => "hey! @#{blocker.nickname}"})
+     activities = ActivityPub.fetch_activities([], %{"blocking_user" => blocker})
+     assert Enum.member?(activities, activity_one)
+     refute Enum.member?(activities, activity_two)
+     refute Enum.member?(activities, activity_three)
+     refute Enum.member?(activities, activity_four)
+   end
+   test "doesn't return announce activities concerning blocked users" do
+     blocker = insert(:user)
+     blockee = insert(:user)
+     friend = insert(:user)
+     {:ok, blocker} = User.block(blocker, blockee)
+     {:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"})
+     {:ok, activity_two} = CommonAPI.post(blockee, %{"status" => "hey! @#{friend.nickname}"})
+     {:ok, activity_three, _} = CommonAPI.repeat(activity_two.id, friend)
+     activities =
+       ActivityPub.fetch_activities([], %{"blocking_user" => blocker})
+       |> Enum.map(fn act -> act.id end)
+     assert Enum.member?(activities, activity_one.id)
+     refute Enum.member?(activities, activity_two.id)
+     refute Enum.member?(activities, activity_three.id)
+   end
    test "doesn't return muted activities" do
      activity_one = insert(:note_activity)
      activity_two = insert(:note_activity)
      end
    end
  
-   describe "fetching an object" do
-     test "it fetches an object" do
-       {:ok, object} =
-         ActivityPub.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
-       assert activity = Activity.get_create_by_object_ap_id(object.data["id"])
-       assert activity.data["id"]
-       {:ok, object_again} =
-         ActivityPub.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
-       assert [attachment] = object.data["attachment"]
-       assert is_list(attachment["url"])
-       assert object == object_again
-     end
-     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_by_object_ap_id(object.data["id"])
-       assert activity.data["id"]
-       {:ok, object_again} =
-         ActivityPub.fetch_object_from_id("https://shitposter.club/notice/2827873")
-       assert object == object_again
-     end
-     test "it correctly stitches up conversations between ostatus and ap" do
-       last = "https://mstdn.io/users/mayuutann/statuses/99568293732299394"
-       {:ok, object} = ActivityPub.fetch_object_from_id(last)
+   describe "fetch the latest Follow" do
+     test "fetches the latest Follow activity" do
+       %Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity)
+       follower = Repo.get_by(User, ap_id: activity.data["actor"])
+       followed = Repo.get_by(User, ap_id: activity.data["object"])
  
-       object = Object.get_by_ap_id(object.data["inReplyTo"])
-       assert object
+       assert activity == Utils.fetch_latest_follow(follower, followed)
      end
    end
  
        user = insert(:user, info: %{note_count: 10})
  
        {:ok, a1} =
-         CommonAPI.post(User.get_by_id(user.id), %{"status" => "yeah", "visibility" => "public"})
+         CommonAPI.post(User.get_cached_by_id(user.id), %{
+           "status" => "yeah",
+           "visibility" => "public"
+         })
  
        {:ok, a2} =
-         CommonAPI.post(User.get_by_id(user.id), %{"status" => "yeah", "visibility" => "unlisted"})
+         CommonAPI.post(User.get_cached_by_id(user.id), %{
+           "status" => "yeah",
+           "visibility" => "unlisted"
+         })
  
        {:ok, a3} =
-         CommonAPI.post(User.get_by_id(user.id), %{"status" => "yeah", "visibility" => "private"})
+         CommonAPI.post(User.get_cached_by_id(user.id), %{
+           "status" => "yeah",
+           "visibility" => "private"
+         })
  
        {:ok, a4} =
-         CommonAPI.post(User.get_by_id(user.id), %{"status" => "yeah", "visibility" => "direct"})
+         CommonAPI.post(User.get_cached_by_id(user.id), %{
+           "status" => "yeah",
+           "visibility" => "direct"
+         })
  
-       {:ok, _} = a1.data["object"]["id"] |> Object.get_by_ap_id() |> ActivityPub.delete()
-       {:ok, _} = a2.data["object"]["id"] |> Object.get_by_ap_id() |> ActivityPub.delete()
-       {:ok, _} = a3.data["object"]["id"] |> Object.get_by_ap_id() |> ActivityPub.delete()
-       {:ok, _} = a4.data["object"]["id"] |> Object.get_by_ap_id() |> ActivityPub.delete()
+       {:ok, _} = Object.normalize(a1) |> ActivityPub.delete()
+       {:ok, _} = Object.normalize(a2) |> ActivityPub.delete()
+       {:ok, _} = Object.normalize(a3) |> ActivityPub.delete()
+       {:ok, _} = Object.normalize(a4) |> ActivityPub.delete()
  
-       user = User.get_by_id(user.id)
+       user = User.get_cached_by_id(user.id)
        assert user.info.note_count == 10
      end
  
  
        _ = CommonAPI.delete(direct_reply.id, user2)
        assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
-       assert data["object"]["repliesCount"] == 2
        assert object.data["repliesCount"] == 2
  
        _ = CommonAPI.delete(private_reply.id, user2)
        assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
-       assert data["object"]["repliesCount"] == 2
        assert object.data["repliesCount"] == 2
  
        _ = CommonAPI.delete(public_reply.id, user2)
        assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
-       assert data["object"]["repliesCount"] == 1
        assert object.data["repliesCount"] == 1
  
        _ = CommonAPI.delete(unlisted_reply.id, user2)
        assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
-       assert data["object"]["repliesCount"] == 0
        assert object.data["repliesCount"] == 0
      end
    end
        activities = ActivityPub.fetch_activities([user1.ap_id | user1.following])
  
        private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"])
-       assert [public_activity, private_activity_1, private_activity_3] == activities
+       assert [public_activity, private_activity_1, private_activity_3] ==
+                activities
        assert length(activities) == 3
  
        activities = ActivityPub.contain_timeline(activities, user1)
      end
    end
  
-   test "it can fetch plume articles" do
-     {:ok, object} =
-       ActivityPub.fetch_object_from_id(
-         "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/"
-       )
-     assert object
-   end
    describe "update" do
      test "it creates an update activity with the new user data" do
        user = insert(:user)
      end
    end
  
-   test "it can fetch peertube videos" do
-     {:ok, object} =
-       ActivityPub.fetch_object_from_id(
-         "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
-       )
-     assert object
-   end
    test "returned pinned statuses" do
      Pleroma.Config.put([:instance, :max_pinned_statuses], 3)
      user = insert(:user)
index cf77dff78de7be1d80c8ef381bf6ac64fdf0354f,c2a12d3c7a9fca1d4114ef148359024e7e9a1461..0eed9b5d721f7f5822130562c0f372862152728c
@@@ -300,65 -300,6 +300,65 @@@ defmodule Pleroma.Web.MastodonAPI.Masto
      assert status["url"] != direct.data["id"]
    end
  
 +  test "Conversations", %{conn: conn} do
 +    user_one = insert(:user)
 +    user_two = insert(:user)
 +
 +    {:ok, user_two} = User.follow(user_two, user_one)
 +
 +    {:ok, direct} =
 +      CommonAPI.post(user_one, %{
 +        "status" => "Hi @#{user_two.nickname}!",
 +        "visibility" => "direct"
 +      })
 +
 +    {:ok, _follower_only} =
 +      CommonAPI.post(user_one, %{
 +        "status" => "Hi @#{user_two.nickname}!",
 +        "visibility" => "private"
 +      })
 +
 +    res_conn =
 +      conn
 +      |> assign(:user, user_one)
 +      |> get("/api/v1/conversations")
 +
 +    assert response = json_response(res_conn, 200)
 +
 +    assert [
 +             %{
 +               "id" => res_id,
 +               "accounts" => res_accounts,
 +               "last_status" => res_last_status,
 +               "unread" => unread
 +             }
 +           ] = response
 +
 +    assert length(res_accounts) == 2
 +    assert is_binary(res_id)
 +    assert unread == true
 +    assert res_last_status["id"] == direct.id
 +
 +    # Apparently undocumented API endpoint
 +    res_conn =
 +      conn
 +      |> assign(:user, user_one)
 +      |> post("/api/v1/conversations/#{res_id}/read")
 +
 +    assert response = json_response(res_conn, 200)
 +    assert length(response["accounts"]) == 2
 +    assert response["last_status"]["id"] == direct.id
 +    assert response["unread"] == false
 +
 +    # (vanilla) Mastodon frontend behaviour
 +    res_conn =
 +      conn
 +      |> assign(:user, user_one)
 +      |> get("/api/v1/statuses/#{res_last_status["id"]}/context")
 +
 +    assert %{"ancestors" => [], "descendants" => []} == json_response(res_conn, 200)
 +  end
 +
    test "doesn't include DMs from blocked users", %{conn: conn} do
      blocker = insert(:user)
      blocked = insert(:user)
      activity = Activity.get_by_id(id)
  
      assert activity.data["context"] == replied_to.data["context"]
-     assert activity.data["object"]["inReplyToStatusId"] == replied_to.id
+     assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
    end
  
    test "posting a status with an invalid in_reply_to_id", %{conn: conn} do
    describe "deleting a status" do
      test "when you created it", %{conn: conn} do
        activity = insert(:note_activity)
-       author = User.get_by_ap_id(activity.data["actor"])
+       author = User.get_cached_by_ap_id(activity.data["actor"])
  
        conn =
          conn
  
        assert [%{"id" => ^reblog_notification_id}] = json_response(conn_res, 200)
      end
+     test "destroy multiple", %{conn: conn} do
+       user = insert(:user)
+       other_user = insert(:user)
+       {:ok, activity1} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
+       {:ok, activity2} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
+       {:ok, activity3} = CommonAPI.post(user, %{"status" => "hi @#{other_user.nickname}"})
+       {:ok, activity4} = CommonAPI.post(user, %{"status" => "hi @#{other_user.nickname}"})
+       notification1_id = Repo.get_by(Notification, activity_id: activity1.id).id |> to_string()
+       notification2_id = Repo.get_by(Notification, activity_id: activity2.id).id |> to_string()
+       notification3_id = Repo.get_by(Notification, activity_id: activity3.id).id |> to_string()
+       notification4_id = Repo.get_by(Notification, activity_id: activity4.id).id |> to_string()
+       conn =
+         conn
+         |> assign(:user, user)
+       conn_res =
+         conn
+         |> get("/api/v1/notifications")
+       result = json_response(conn_res, 200)
+       assert [%{"id" => ^notification2_id}, %{"id" => ^notification1_id}] = result
+       conn2 =
+         conn
+         |> assign(:user, other_user)
+       conn_res =
+         conn2
+         |> get("/api/v1/notifications")
+       result = json_response(conn_res, 200)
+       assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
+       conn_destroy =
+         conn
+         |> delete("/api/v1/notifications/destroy_multiple", %{
+           "ids" => [notification1_id, notification2_id]
+         })
+       assert json_response(conn_destroy, 200) == %{}
+       conn_res =
+         conn2
+         |> get("/api/v1/notifications")
+       result = json_response(conn_res, 200)
+       assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
+     end
    end
  
    describe "reblogging" do
          |> assign(:user, user)
          |> post("/api/v1/statuses/#{activity.id}/reblog")
  
-       assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
-                json_response(conn, 200)
+       assert %{
+                "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
+                "reblogged" => true
+              } = json_response(conn, 200)
+       assert to_string(activity.id) == id
+     end
+     test "reblogged status for another user", %{conn: conn} do
+       activity = insert(:note_activity)
+       user1 = insert(:user)
+       user2 = insert(:user)
+       user3 = insert(:user)
+       CommonAPI.favorite(activity.id, user2)
+       {:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id)
+       {:ok, reblog_activity1, _object} = CommonAPI.repeat(activity.id, user1)
+       {:ok, _, _object} = CommonAPI.repeat(activity.id, user2)
+       conn_res =
+         conn
+         |> assign(:user, user3)
+         |> get("/api/v1/statuses/#{reblog_activity1.id}")
+       assert %{
+                "reblog" => %{"id" => id, "reblogged" => false, "reblogs_count" => 2},
+                "reblogged" => false,
+                "favourited" => false,
+                "bookmarked" => false
+              } = json_response(conn_res, 200)
+       conn_res =
+         conn
+         |> assign(:user, user2)
+         |> get("/api/v1/statuses/#{reblog_activity1.id}")
+       assert %{
+                "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2},
+                "reblogged" => true,
+                "favourited" => true,
+                "bookmarked" => true
+              } = json_response(conn_res, 200)
  
        assert to_string(activity.id) == id
      end
  
      test "unimplemented pinned statuses feature", %{conn: conn} do
        note = insert(:note_activity)
-       user = User.get_by_ap_id(note.data["actor"])
+       user = User.get_cached_by_ap_id(note.data["actor"])
  
        conn =
          conn
  
      test "gets an users media", %{conn: conn} do
        note = insert(:note_activity)
-       user = User.get_by_ap_id(note.data["actor"])
+       user = User.get_cached_by_ap_id(note.data["actor"])
  
        file = %Plug.Upload{
          content_type: "image/jpg",
  
        {:ok, _activity} = ActivityPub.follow(other_user, user)
  
-       user = User.get_by_id(user.id)
-       other_user = User.get_by_id(other_user.id)
+       user = User.get_cached_by_id(user.id)
+       other_user = User.get_cached_by_id(other_user.id)
  
        assert User.following?(other_user, user) == false
  
  
        {:ok, _activity} = ActivityPub.follow(other_user, user)
  
-       user = User.get_by_id(user.id)
-       other_user = User.get_by_id(other_user.id)
+       user = User.get_cached_by_id(user.id)
+       other_user = User.get_cached_by_id(other_user.id)
  
        assert User.following?(other_user, user) == false
  
        assert relationship = json_response(conn, 200)
        assert to_string(other_user.id) == relationship["id"]
  
-       user = User.get_by_id(user.id)
-       other_user = User.get_by_id(other_user.id)
+       user = User.get_cached_by_id(user.id)
+       other_user = User.get_cached_by_id(other_user.id)
  
        assert User.following?(other_user, user) == true
      end
  
        {:ok, _activity} = ActivityPub.follow(other_user, user)
  
-       user = User.get_by_id(user.id)
+       user = User.get_cached_by_id(user.id)
  
        conn =
          build_conn()
        assert relationship = json_response(conn, 200)
        assert to_string(other_user.id) == relationship["id"]
  
-       user = User.get_by_id(user.id)
-       other_user = User.get_by_id(other_user.id)
+       user = User.get_cached_by_id(user.id)
+       other_user = User.get_cached_by_id(other_user.id)
  
        assert User.following?(other_user, user) == false
      end
      assert id2 == follower2.id
  
      assert [link_header] = get_resp_header(res_conn, "link")
-     assert link_header =~ ~r/since_id=#{follower2.id}/
+     assert link_header =~ ~r/min_id=#{follower2.id}/
      assert link_header =~ ~r/max_id=#{follower2.id}/
    end
  
      assert id2 == following2.id
  
      assert [link_header] = get_resp_header(res_conn, "link")
-     assert link_header =~ ~r/since_id=#{following2.id}/
+     assert link_header =~ ~r/min_id=#{following2.id}/
      assert link_header =~ ~r/max_id=#{following2.id}/
    end
  
  
      assert %{"id" => _id, "following" => true} = json_response(conn, 200)
  
-     user = User.get_by_id(user.id)
+     user = User.get_cached_by_id(user.id)
  
      conn =
        build_conn()
  
      assert %{"id" => _id, "following" => false} = json_response(conn, 200)
  
-     user = User.get_by_id(user.id)
+     user = User.get_cached_by_id(user.id)
  
      conn =
        build_conn()
      assert id == to_string(other_user.id)
    end
  
+   test "following without reblogs" do
+     follower = insert(:user)
+     followed = insert(:user)
+     other_user = insert(:user)
+     conn =
+       build_conn()
+       |> assign(:user, follower)
+       |> post("/api/v1/accounts/#{followed.id}/follow?reblogs=false")
+     assert %{"showing_reblogs" => false} = json_response(conn, 200)
+     {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hey"})
+     {:ok, reblog, _} = CommonAPI.repeat(activity.id, followed)
+     conn =
+       build_conn()
+       |> assign(:user, User.get_cached_by_id(follower.id))
+       |> get("/api/v1/timelines/home")
+     assert [] == json_response(conn, 200)
+     conn =
+       build_conn()
+       |> assign(:user, follower)
+       |> post("/api/v1/accounts/#{followed.id}/follow?reblogs=true")
+     assert %{"showing_reblogs" => true} = json_response(conn, 200)
+     conn =
+       build_conn()
+       |> assign(:user, User.get_cached_by_id(follower.id))
+       |> get("/api/v1/timelines/home")
+     expected_activity_id = reblog.id
+     assert [%{"id" => ^expected_activity_id}] = json_response(conn, 200)
+   end
+   test "following / unfollowing errors" do
+     user = insert(:user)
+     conn =
+       build_conn()
+       |> assign(:user, user)
+     # self follow
+     conn_res = post(conn, "/api/v1/accounts/#{user.id}/follow")
+     assert %{"error" => "Record not found"} = json_response(conn_res, 404)
+     # self unfollow
+     user = User.get_cached_by_id(user.id)
+     conn_res = post(conn, "/api/v1/accounts/#{user.id}/unfollow")
+     assert %{"error" => "Record not found"} = json_response(conn_res, 404)
+     # self follow via uri
+     user = User.get_cached_by_id(user.id)
+     conn_res = post(conn, "/api/v1/follows", %{"uri" => user.nickname})
+     assert %{"error" => "Record not found"} = json_response(conn_res, 404)
+     # follow non existing user
+     conn_res = post(conn, "/api/v1/accounts/doesntexist/follow")
+     assert %{"error" => "Record not found"} = json_response(conn_res, 404)
+     # follow non existing user via uri
+     conn_res = post(conn, "/api/v1/follows", %{"uri" => "doesntexist"})
+     assert %{"error" => "Record not found"} = json_response(conn_res, 404)
+     # unfollow non existing user
+     conn_res = post(conn, "/api/v1/accounts/doesntexist/unfollow")
+     assert %{"error" => "Record not found"} = json_response(conn_res, 404)
+   end
    test "muting / unmuting a user", %{conn: conn} do
      user = insert(:user)
      other_user = insert(:user)
  
      assert %{"id" => _id, "muting" => true} = json_response(conn, 200)
  
-     user = User.get_by_id(user.id)
+     user = User.get_cached_by_id(user.id)
  
      conn =
        build_conn()
      assert %{"id" => _id, "muting" => false} = json_response(conn, 200)
    end
  
+   test "subscribing / unsubscribing to a user", %{conn: conn} do
+     user = insert(:user)
+     subscription_target = insert(:user)
+     conn =
+       conn
+       |> assign(:user, user)
+       |> post("/api/v1/pleroma/accounts/#{subscription_target.id}/subscribe")
+     assert %{"id" => _id, "subscribing" => true} = json_response(conn, 200)
+     conn =
+       build_conn()
+       |> assign(:user, user)
+       |> post("/api/v1/pleroma/accounts/#{subscription_target.id}/unsubscribe")
+     assert %{"id" => _id, "subscribing" => false} = json_response(conn, 200)
+   end
    test "getting a list of mutes", %{conn: conn} do
      user = insert(:user)
      other_user = insert(:user)
  
      assert %{"id" => _id, "blocking" => true} = json_response(conn, 200)
  
-     user = User.get_by_id(user.id)
+     user = User.get_cached_by_id(user.id)
  
      conn =
        build_conn()
      capture_log(fn ->
        conn =
          conn
-         |> get("/api/v1/search", %{"q" => activity.data["object"]["id"]})
+         |> get("/api/v1/search", %{"q" => Object.normalize(activity).data["id"]})
  
        assert results = json_response(conn, 200)
  
      assert [] = json_response(third_conn, 200)
    end
  
+   describe "getting favorites timeline of specified user" do
+     setup do
+       [current_user, user] = insert_pair(:user, %{info: %{hide_favorites: false}})
+       [current_user: current_user, user: user]
+     end
+     test "returns list of statuses favorited by specified user", %{
+       conn: conn,
+       current_user: current_user,
+       user: user
+     } do
+       [activity | _] = insert_pair(:note_activity)
+       CommonAPI.favorite(activity.id, user)
+       response =
+         conn
+         |> assign(:user, current_user)
+         |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+         |> json_response(:ok)
+       [like] = response
+       assert length(response) == 1
+       assert like["id"] == activity.id
+     end
+     test "returns favorites for specified user_id when user is not logged in", %{
+       conn: conn,
+       user: user
+     } do
+       activity = insert(:note_activity)
+       CommonAPI.favorite(activity.id, user)
+       response =
+         conn
+         |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+         |> json_response(:ok)
+       assert length(response) == 1
+     end
+     test "returns favorited DM only when user is logged in and he is one of recipients", %{
+       conn: conn,
+       current_user: current_user,
+       user: user
+     } do
+       {:ok, direct} =
+         CommonAPI.post(current_user, %{
+           "status" => "Hi @#{user.nickname}!",
+           "visibility" => "direct"
+         })
+       CommonAPI.favorite(direct.id, user)
+       response =
+         conn
+         |> assign(:user, current_user)
+         |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+         |> json_response(:ok)
+       assert length(response) == 1
+       anonymous_response =
+         conn
+         |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+         |> json_response(:ok)
+       assert length(anonymous_response) == 0
+     end
+     test "does not return others' favorited DM when user is not one of recipients", %{
+       conn: conn,
+       current_user: current_user,
+       user: user
+     } do
+       user_two = insert(:user)
+       {:ok, direct} =
+         CommonAPI.post(user_two, %{
+           "status" => "Hi @#{user.nickname}!",
+           "visibility" => "direct"
+         })
+       CommonAPI.favorite(direct.id, user)
+       response =
+         conn
+         |> assign(:user, current_user)
+         |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+         |> json_response(:ok)
+       assert length(response) == 0
+     end
+     test "paginates favorites using since_id and max_id", %{
+       conn: conn,
+       current_user: current_user,
+       user: user
+     } do
+       activities = insert_list(10, :note_activity)
+       Enum.each(activities, fn activity ->
+         CommonAPI.favorite(activity.id, user)
+       end)
+       third_activity = Enum.at(activities, 2)
+       seventh_activity = Enum.at(activities, 6)
+       response =
+         conn
+         |> assign(:user, current_user)
+         |> get("/api/v1/pleroma/accounts/#{user.id}/favourites", %{
+           since_id: third_activity.id,
+           max_id: seventh_activity.id
+         })
+         |> json_response(:ok)
+       assert length(response) == 3
+       refute third_activity in response
+       refute seventh_activity in response
+     end
+     test "limits favorites using limit parameter", %{
+       conn: conn,
+       current_user: current_user,
+       user: user
+     } do
+       7
+       |> insert_list(:note_activity)
+       |> Enum.each(fn activity ->
+         CommonAPI.favorite(activity.id, user)
+       end)
+       response =
+         conn
+         |> assign(:user, current_user)
+         |> get("/api/v1/pleroma/accounts/#{user.id}/favourites", %{limit: "3"})
+         |> json_response(:ok)
+       assert length(response) == 3
+     end
+     test "returns empty response when user does not have any favorited statuses", %{
+       conn: conn,
+       current_user: current_user,
+       user: user
+     } do
+       response =
+         conn
+         |> assign(:user, current_user)
+         |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+         |> json_response(:ok)
+       assert Enum.empty?(response)
+     end
+     test "returns 404 error when specified user is not exist", %{conn: conn} do
+       conn = get(conn, "/api/v1/pleroma/accounts/test/favourites")
+       assert json_response(conn, 404) == %{"error" => "Record not found"}
+     end
+     test "returns 403 error when user has hidden own favorites", %{
+       conn: conn,
+       current_user: current_user
+     } do
+       user = insert(:user, %{info: %{hide_favorites: true}})
+       activity = insert(:note_activity)
+       CommonAPI.favorite(activity.id, user)
+       conn =
+         conn
+         |> assign(:user, current_user)
+         |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+       assert json_response(conn, 403) == %{"error" => "Can't get favorites"}
+     end
+     test "hides favorites for new users by default", %{conn: conn, current_user: current_user} do
+       user = insert(:user)
+       activity = insert(:note_activity)
+       CommonAPI.favorite(activity.id, user)
+       conn =
+         conn
+         |> assign(:user, current_user)
+         |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+       assert user.info.hide_favorites
+       assert json_response(conn, 403) == %{"error" => "Can't get favorites"}
+     end
+   end
    describe "updating credentials" do
      test "updates the user's bio", %{conn: conn} do
        user = insert(:user)
        assert user["locked"] == true
      end
  
+     test "updates the user's default scope", %{conn: conn} do
+       user = insert(:user)
+       conn =
+         conn
+         |> assign(:user, user)
+         |> patch("/api/v1/accounts/update_credentials", %{default_scope: "cofe"})
+       assert user = json_response(conn, 200)
+       assert user["source"]["privacy"] == "cofe"
+     end
+     test "updates the user's hide_followers status", %{conn: conn} do
+       user = insert(:user)
+       conn =
+         conn
+         |> assign(:user, user)
+         |> patch("/api/v1/accounts/update_credentials", %{hide_followers: "true"})
+       assert user = json_response(conn, 200)
+       assert user["pleroma"]["hide_followers"] == true
+     end
+     test "updates the user's hide_follows status", %{conn: conn} do
+       user = insert(:user)
+       conn =
+         conn
+         |> assign(:user, user)
+         |> patch("/api/v1/accounts/update_credentials", %{hide_follows: "true"})
+       assert user = json_response(conn, 200)
+       assert user["pleroma"]["hide_follows"] == true
+     end
+     test "updates the user's hide_favorites status", %{conn: conn} do
+       user = insert(:user)
+       conn =
+         conn
+         |> assign(:user, user)
+         |> patch("/api/v1/accounts/update_credentials", %{hide_favorites: "true"})
+       assert user = json_response(conn, 200)
+       assert user["pleroma"]["hide_favorites"] == true
+     end
+     test "updates the user's show_role status", %{conn: conn} do
+       user = insert(:user)
+       conn =
+         conn
+         |> assign(:user, user)
+         |> patch("/api/v1/accounts/update_credentials", %{show_role: "false"})
+       assert user = json_response(conn, 200)
+       assert user["source"]["pleroma"]["show_role"] == false
+     end
+     test "updates the user's no_rich_text status", %{conn: conn} do
+       user = insert(:user)
+       conn =
+         conn
+         |> assign(:user, user)
+         |> patch("/api/v1/accounts/update_credentials", %{no_rich_text: "true"})
+       assert user = json_response(conn, 200)
+       assert user["source"]["pleroma"]["no_rich_text"] == true
+     end
      test "updates the user's name", %{conn: conn} do
        user = insert(:user)
  
      conn = get(conn, "/api/v1/instance")
      assert result = json_response(conn, 200)
  
+     email = Pleroma.Config.get([:instance, :email])
      # Note: not checking for "max_toot_chars" since it's optional
      assert %{
               "uri" => _,
               "title" => _,
               "description" => _,
               "version" => _,
-              "email" => _,
+              "email" => from_config_email,
               "urls" => %{
                 "streaming_api" => _
               },
               "languages" => _,
               "registrations" => _
             } = result
+     assert email == from_config_email
    end
  
    test "get instance stats", %{conn: conn} do
      {:ok, _} = TwitterAPI.create_status(user, %{"status" => "cofe"})
  
      # Stats should count users with missing or nil `info.deactivated` value
-     user = User.get_by_id(user.id)
+     user = User.get_cached_by_id(user.id)
      info_change = Changeset.change(user.info, %{deactivated: nil})
  
      {:ok, _user} =
  
        assert [link_header] = get_resp_header(conn, "link")
        assert link_header =~ ~r/media_only=true/
-       assert link_header =~ ~r/since_id=#{notification2.id}/
+       assert link_header =~ ~r/min_id=#{notification2.id}/
        assert link_header =~ ~r/max_id=#{notification1.id}/
      end
    end
        assert %{"error" => "Record not found"} = json_response(res_conn, 404)
      end
    end
+   test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do
+     user1 = insert(:user)
+     user2 = insert(:user)
+     user3 = insert(:user)
+     {:ok, replied_to} = TwitterAPI.create_status(user1, %{"status" => "cofe"})
+     # Reply to status from another user
+     conn1 =
+       conn
+       |> assign(:user, user2)
+       |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
+     assert %{"content" => "xD", "id" => id} = json_response(conn1, 200)
+     activity = Activity.get_by_id_with_object(id)
+     assert Object.normalize(activity).data["inReplyTo"] == Object.normalize(replied_to).data["id"]
+     assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
+     # Reblog from the third user
+     conn2 =
+       conn
+       |> assign(:user, user3)
+       |> post("/api/v1/statuses/#{activity.id}/reblog")
+     assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
+              json_response(conn2, 200)
+     assert to_string(activity.id) == id
+     # Getting third user status
+     conn3 =
+       conn
+       |> assign(:user, user3)
+       |> get("api/v1/timelines/home")
+     [reblogged_activity] = json_response(conn3, 200)
+     assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id
+     replied_to_user = User.get_by_ap_id(replied_to.data["actor"])
+     assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id
+   end
  end