Merge branch 'develop' into tests/mastodon_api_controller.ex
authorMaksim Pechnikov <parallel588@gmail.com>
Sat, 28 Sep 2019 07:32:03 +0000 (10:32 +0300)
committerMaksim Pechnikov <parallel588@gmail.com>
Sat, 28 Sep 2019 07:36:04 +0000 (10:36 +0300)
1  2 
lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
lib/pleroma/web/mastodon_api/controllers/status_controller.ex
lib/pleroma/web/mastodon_api/views/status_view.ex
test/web/mastodon_api/mastodon_api_controller_test.exs

index 8f6b3456a30f8ceed80e31a46d2263d6f17a77a4,a839a93c2b0373f804a4a9fb542cc73f58e0c424..0878f7ba64a27d387cb9554b768b09390ad2c465
@@@ -42,12 -38,10 +38,8 @@@ defmodule Pleroma.Web.MastodonAPI.Masto
    alias Pleroma.Web.OAuth.Authorization
    alias Pleroma.Web.OAuth.Scopes
    alias Pleroma.Web.OAuth.Token
-   alias Pleroma.Web.RichMedia
    alias Pleroma.Web.TwitterAPI.TwitterAPI
  
-   alias Pleroma.Web.ControllerHelper
--  import Ecto.Query
--
    require Logger
    require Pleroma.Constants
  
      end
    end
  
-   def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
-     with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
-       conn
-       |> add_link_headers(scheduled_activities)
-       |> put_view(ScheduledActivityView)
-       |> render("index.json", %{scheduled_activities: scheduled_activities})
-     end
-   end
-   def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
-     with %ScheduledActivity{} = scheduled_activity <-
-            ScheduledActivity.get(user, scheduled_activity_id) do
-       conn
-       |> put_view(ScheduledActivityView)
-       |> render("show.json", %{scheduled_activity: scheduled_activity})
-     else
-       _ -> {:error, :not_found}
-     end
-   end
-   def update_scheduled_status(
-         %{assigns: %{user: user}} = conn,
-         %{"id" => scheduled_activity_id} = params
-       ) do
-     with %ScheduledActivity{} = scheduled_activity <-
-            ScheduledActivity.get(user, scheduled_activity_id),
-          {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
-       conn
-       |> put_view(ScheduledActivityView)
-       |> render("show.json", %{scheduled_activity: scheduled_activity})
-     else
-       nil -> {:error, :not_found}
-       error -> error
-     end
-   end
-   def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
-     with %ScheduledActivity{} = scheduled_activity <-
-            ScheduledActivity.get(user, scheduled_activity_id),
-          {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
-       conn
-       |> put_view(ScheduledActivityView)
-       |> render("show.json", %{scheduled_activity: scheduled_activity})
-     else
-       nil -> {:error, :not_found}
-       error -> error
-     end
-   end
-   def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
-     params =
-       params
-       |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
-     scheduled_at = params["scheduled_at"]
-     if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
-       with {:ok, scheduled_activity} <-
-              ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
-         conn
-         |> put_view(ScheduledActivityView)
-         |> render("show.json", %{scheduled_activity: scheduled_activity})
-       end
-     else
-       params = Map.drop(params, ["scheduled_at"])
-       case CommonAPI.post(user, params) do
-         {:error, message} ->
-           conn
-           |> put_status(:unprocessable_entity)
-           |> json(%{error: message})
-         {:ok, activity} ->
-           conn
-           |> put_view(StatusView)
-           |> try_render("status.json", %{
-             activity: activity,
-             for: user,
-             as: :activity,
-             with_direct_conversation_id: true
-           })
-       end
-     end
-   end
-   def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-     with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
-       json(conn, %{})
-     else
-       _e -> render_error(conn, :forbidden, "Can't delete this post")
-     end
-   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),
-          %Activity{} = announce <- Activity.normalize(announce.data) do
-       conn
-       |> put_view(StatusView)
-       |> try_render("status.json", %{activity: announce, for: user, as: :activity})
-     end
-   end
-   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_with_object(id) do
-       conn
-       |> put_view(StatusView)
-       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
-     end
-   end
-   def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
-     with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
-          %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
-       conn
-       |> put_view(StatusView)
-       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
-     end
-   end
-   def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
-     with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
-          %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
-       conn
-       |> put_view(StatusView)
-       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
-     end
-   end
-   def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
-     with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
-       conn
-       |> put_view(StatusView)
-       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
-     end
-   end
-   def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
-     with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
-       conn
-       |> put_view(StatusView)
-       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
-     end
-   end
-   def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-     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, _bookmark} <- Bookmark.create(user.id, activity.id) do
-       conn
-       |> put_view(StatusView)
-       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
-     end
-   end
-   def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-     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, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
-       conn
-       |> put_view(StatusView)
-       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
-     end
-   end
-   def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-     activity = Activity.get_by_id(id)
-     with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
-       conn
-       |> put_view(StatusView)
-       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
-     end
-   end
-   def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-     activity = Activity.get_by_id(id)
-     with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
-       conn
-       |> put_view(StatusView)
-       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
-     end
-   end
    def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
 -    id = List.wrap(id)
 -    q = from(u in User, where: u.id in ^id)
 -    targets = Repo.all(q)
 +    targets = User.get_all_by_ids(List.wrap(id))
  
      conn
      |> put_view(AccountView)
    def get_mascot(%{assigns: %{user: user}} = conn, _params) do
      mascot = User.get_mascot(user)
  
 -    conn
 -    |> json(mascot)
 +    json(conn, mascot)
    end
  
-   def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-     with %Activity{} = activity <- Activity.get_by_id_with_object(id),
-          {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
-          %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
-       q = from(u in User, where: u.ap_id in ^likes)
-       users =
-         Repo.all(q)
-         |> Enum.filter(&(not User.blocks?(user, &1)))
-       conn
-       |> put_view(AccountView)
-       |> render("accounts.json", %{for: user, users: users, as: :user})
-     else
-       {:visible, false} -> {:error, :not_found}
-       _ -> json(conn, [])
-     end
-   end
-   def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-     with %Activity{} = activity <- Activity.get_by_id_with_object(id),
-          {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
-          %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
-       q = from(u in User, where: u.ap_id in ^announces)
-       users =
-         Repo.all(q)
-         |> Enum.filter(&(not User.blocks?(user, &1)))
-       conn
-       |> put_view(AccountView)
-       |> render("accounts.json", %{for: user, users: users, as: :user})
-     else
-       {:visible, false} -> {:error, :not_found}
-       _ -> json(conn, [])
-     end
-   end
-   def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
-     local_only = params["local"] in [true, "True", "true", "1"]
-     tags =
-       [params["tag"], params["any"]]
-       |> List.flatten()
-       |> Enum.uniq()
-       |> Enum.filter(& &1)
-       |> Enum.map(&String.downcase(&1))
-     tag_all =
-       params["all"] ||
-         []
-         |> Enum.map(&String.downcase(&1))
-     tag_reject =
-       params["none"] ||
-         []
-         |> Enum.map(&String.downcase(&1))
-     activities =
-       params
-       |> Map.put("type", "Create")
-       |> Map.put("local_only", local_only)
-       |> Map.put("blocking_user", user)
-       |> Map.put("muting_user", user)
-       |> Map.put("user", user)
-       |> Map.put("tag", tags)
-       |> Map.put("tag_all", tag_all)
-       |> Map.put("tag_reject", tag_reject)
-       |> ActivityPub.fetch_public_activities()
-       |> Enum.reverse()
-     conn
-     |> add_link_headers(activities, %{"local" => local_only})
-     |> put_view(StatusView)
-     |> render("index.json", %{activities: activities, for: user, as: :activity})
-   end
    def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
      with %User{} = user <- User.get_cached_by_id(id),
           followers <- MastodonAPI.get_followers(user, params) do
  
    def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
      lists = Pleroma.List.get_lists_account_belongs(user, account_id)
 -    res = ListView.render("lists.json", lists: lists)
 -    json(conn, res)
 +
 +    conn
 +    |> put_view(ListView)
 +    |> render("index.json", %{lists: lists})
    end
  
-   def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
-     with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
-       params =
-         params
-         |> Map.put("type", "Create")
-         |> Map.put("blocking_user", user)
-         |> Map.put("user", user)
-         |> Map.put("muting_user", user)
-       # we must filter the following list for the user to avoid leaking statuses the user
-       # does not actually have permission to see (for more info, peruse security issue #270).
-       activities =
-         following
-         |> Enum.filter(fn x -> x in user.following end)
-         |> ActivityPub.fetch_activities_bounded(following, params)
-         |> Enum.reverse()
-       conn
-       |> put_view(StatusView)
-       |> render("index.json", %{activities: activities, for: user, as: :activity})
-     else
-       _e -> render_error(conn, :forbidden, "Error.")
-     end
-   end
    def index(%{assigns: %{user: user}} = conn, _params) do
      token = get_session(conn, :oauth_token)
  
index 0000000000000000000000000000000000000000,ae3d515755148e24c7f301165a6d479dc3c7f742..f4de9285b49807f1ee31ea597b5a0e036a3a22b2
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,273 +1,274 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
+ defmodule Pleroma.Web.MastodonAPI.StatusController do
+   use Pleroma.Web, :controller
+   import Pleroma.Web.MastodonAPI.MastodonAPIController, only: [try_render: 3]
+   require Ecto.Query
+   alias Pleroma.Activity
+   alias Pleroma.Bookmark
+   alias Pleroma.Object
+   alias Pleroma.Plugs.RateLimiter
+   alias Pleroma.Repo
+   alias Pleroma.ScheduledActivity
+   alias Pleroma.User
+   alias Pleroma.Web.ActivityPub.ActivityPub
+   alias Pleroma.Web.ActivityPub.Visibility
+   alias Pleroma.Web.CommonAPI
+   alias Pleroma.Web.MastodonAPI.AccountView
+   alias Pleroma.Web.MastodonAPI.ScheduledActivityView
+   @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
+   plug(
+     RateLimiter,
+     {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
+     when action in ~w(reblog unreblog)a
+   )
+   plug(
+     RateLimiter,
+     {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
+     when action in ~w(favourite unfavourite)a
+   )
+   plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
+   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+   @doc """
+   GET `/api/v1/statuses?ids[]=1&ids[]=2`
+   `ids` query param is required
+   """
+   def index(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
+     limit = 100
+     activities =
+       ids
+       |> Enum.take(limit)
+       |> Activity.all_by_ids_with_object()
+       |> Enum.filter(&Visibility.visible_for_user?(&1, user))
+     render(conn, "index.json", activities: activities, for: user, as: :activity)
+   end
+   @doc """
+   POST /api/v1/statuses
+   Creates a scheduled status when `scheduled_at` param is present and it's far enough
+   """
+   def create(
+         %{assigns: %{user: user}} = conn,
+         %{"status" => _, "scheduled_at" => scheduled_at} = params
+       ) do
+     params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
+     if ScheduledActivity.far_enough?(scheduled_at) do
+       with {:ok, scheduled_activity} <-
+              ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
+         conn
+         |> put_view(ScheduledActivityView)
+         |> render("show.json", scheduled_activity: scheduled_activity)
+       end
+     else
+       create(conn, Map.drop(params, ["scheduled_at"]))
+     end
+   end
+   @doc """
+   POST /api/v1/statuses
+   Creates a regular status
+   """
+   def create(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
+     params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
+     with {:ok, activity} <- CommonAPI.post(user, params) do
+       try_render(conn, "show.json",
+         activity: activity,
+         for: user,
+         as: :activity,
+         with_direct_conversation_id: true
+       )
+     else
+       {:error, message} ->
+         conn
+         |> put_status(:unprocessable_entity)
+         |> json(%{error: message})
+     end
+   end
+   def create(%{assigns: %{user: _user}} = conn, %{"media_ids" => _} = params) do
+     create(conn, Map.put(params, "status", ""))
+   end
+   @doc "GET /api/v1/statuses/:id"
+   def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+     with %Activity{} = activity <- Activity.get_by_id_with_object(id),
+          true <- Visibility.visible_for_user?(activity, user) do
+       try_render(conn, "show.json", activity: activity, for: user)
+     end
+   end
+   @doc "DELETE /api/v1/statuses/:id"
+   def delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+     with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
+       json(conn, %{})
+     else
+       _e -> render_error(conn, :forbidden, "Can't delete this post")
+     end
+   end
+   @doc "POST /api/v1/statuses/:id/reblog"
+   def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+     with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
+          %Activity{} = announce <- Activity.normalize(announce.data) do
+       try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
+     end
+   end
+   @doc "POST /api/v1/statuses/:id/unreblog"
+   def unreblog(%{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_with_object(id) do
+       try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
+     end
+   end
+   @doc "POST /api/v1/statuses/:id/favourite"
+   def favourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+     with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
+          %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
+       try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+     end
+   end
+   @doc "POST /api/v1/statuses/:id/unfavourite"
+   def unfavourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+     with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
+          %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
+       try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+     end
+   end
+   @doc "POST /api/v1/statuses/:id/pin"
+   def pin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+     with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
+       try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+     end
+   end
+   @doc "POST /api/v1/statuses/:id/unpin"
+   def unpin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+     with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
+       try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+     end
+   end
+   @doc "POST /api/v1/statuses/:id/bookmark"
+   def bookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+     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, _bookmark} <- Bookmark.create(user.id, activity.id) do
+       try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+     end
+   end
+   @doc "POST /api/v1/statuses/:id/unbookmark"
+   def unbookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+     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, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
+       try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+     end
+   end
+   @doc "POST /api/v1/statuses/:id/mute"
+   def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+     with %Activity{} = activity <- Activity.get_by_id(id),
+          {:ok, activity} <- CommonAPI.add_mute(user, activity) do
+       try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+     end
+   end
+   @doc "POST /api/v1/statuses/:id/unmute"
+   def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+     with %Activity{} = activity <- Activity.get_by_id(id),
+          {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
+       try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+     end
+   end
+   @doc "GET /api/v1/statuses/:id/card"
++  @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
+   def card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
+     with %Activity{} = activity <- Activity.get_by_id(status_id),
+          true <- Visibility.visible_for_user?(activity, user) do
+       data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
+       render(conn, "card.json", data)
+     else
+       _ -> render_error(conn, :not_found, "Record not found")
+     end
+   end
+   @doc "GET /api/v1/statuses/:id/favourited_by"
+   def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+     with %Activity{} = activity <- Activity.get_by_id_with_object(id),
+          {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
+          %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
+       users =
+         User
+         |> Ecto.Query.where([u], u.ap_id in ^likes)
+         |> Repo.all()
+         |> Enum.filter(&(not User.blocks?(user, &1)))
+       conn
+       |> put_view(AccountView)
+       |> render("accounts.json", for: user, users: users, as: :user)
+     else
+       {:visible, false} -> {:error, :not_found}
+       _ -> json(conn, [])
+     end
+   end
+   @doc "GET /api/v1/statuses/:id/reblogged_by"
+   def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+     with %Activity{} = activity <- Activity.get_by_id_with_object(id),
+          {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
+          %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
+       users =
+         User
+         |> Ecto.Query.where([u], u.ap_id in ^announces)
+         |> Repo.all()
+         |> Enum.filter(&(not User.blocks?(user, &1)))
+       conn
+       |> put_view(AccountView)
+       |> render("accounts.json", for: user, users: users, as: :user)
+     else
+       {:visible, false} -> {:error, :not_found}
+       _ -> json(conn, [])
+     end
+   end
+   @doc "GET /api/v1/statuses/:id/context"
+   def context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+     with %Activity{} = activity <- Activity.get_by_id(id) do
+       activities =
+         ActivityPub.fetch_activities_for_context(activity.data["context"], %{
+           "blocking_user" => user,
+           "user" => user,
+           "exclude_id" => activity.id
+         })
+       render(conn, "context.json", activity: activity, activities: activities, user: user)
+     end
+   end
+ end
index 1e9829886546fd13375aea70cebd14ca0c430042,da9f1e9b81428b48a5a6b5f10fc6153947ed79a5..b3acb7a228b66415d7c22ea6fe5fab61cab8a2d0
@@@ -1499,130 -533,72 +544,74 @@@ defmodule Pleroma.Web.MastodonAPI.Masto
      end
    end
  
 -  test "mascot upload", %{conn: conn} do
 -    user = insert(:user)
 +  describe "/api/v1/pleroma/mascot" do
 +    test "mascot upload", %{conn: conn} do
 +      user = insert(:user)
  
 -    non_image_file = %Plug.Upload{
 -      content_type: "audio/mpeg",
 -      path: Path.absname("test/fixtures/sound.mp3"),
 -      filename: "sound.mp3"
 -    }
 +      non_image_file = %Plug.Upload{
 +        content_type: "audio/mpeg",
 +        path: Path.absname("test/fixtures/sound.mp3"),
 +        filename: "sound.mp3"
 +      }
  
 -    conn =
 -      conn
 -      |> assign(:user, user)
 -      |> put("/api/v1/pleroma/mascot", %{"file" => non_image_file})
 +      conn =
 +        conn
 +        |> assign(:user, user)
 +        |> put("/api/v1/pleroma/mascot", %{"file" => non_image_file})
  
 -    assert json_response(conn, 415)
 +      assert json_response(conn, 415)
  
 -    file = %Plug.Upload{
 -      content_type: "image/jpg",
 -      path: Path.absname("test/fixtures/image.jpg"),
 -      filename: "an_image.jpg"
 -    }
 +      file = %Plug.Upload{
 +        content_type: "image/jpg",
 +        path: Path.absname("test/fixtures/image.jpg"),
 +        filename: "an_image.jpg"
 +      }
  
 -    conn =
 -      build_conn()
 -      |> assign(:user, user)
 -      |> put("/api/v1/pleroma/mascot", %{"file" => file})
 +      conn =
 +        build_conn()
 +        |> assign(:user, user)
 +        |> put("/api/v1/pleroma/mascot", %{"file" => file})
  
 -    assert %{"id" => _, "type" => image} = json_response(conn, 200)
 -  end
 +      assert %{"id" => _, "type" => image} = json_response(conn, 200)
 +    end
  
 -  test "mascot retrieving", %{conn: conn} do
 -    user = insert(:user)
 -    # When user hasn't set a mascot, we should just get pleroma tan back
 -    conn =
 -      conn
 -      |> assign(:user, user)
 -      |> get("/api/v1/pleroma/mascot")
 +    test "mascot retrieving", %{conn: conn} do
 +      user = insert(:user)
 +      # When user hasn't set a mascot, we should just get pleroma tan back
 +      conn =
 +        conn
 +        |> assign(:user, user)
 +        |> get("/api/v1/pleroma/mascot")
  
 -    assert %{"url" => url} = json_response(conn, 200)
 -    assert url =~ "pleroma-fox-tan-smol"
 +      assert %{"url" => url} = json_response(conn, 200)
 +      assert url =~ "pleroma-fox-tan-smol"
  
 -    # When a user sets their mascot, we should get that back
 -    file = %Plug.Upload{
 -      content_type: "image/jpg",
 -      path: Path.absname("test/fixtures/image.jpg"),
 -      filename: "an_image.jpg"
 -    }
 +      # When a user sets their mascot, we should get that back
 +      file = %Plug.Upload{
 +        content_type: "image/jpg",
 +        path: Path.absname("test/fixtures/image.jpg"),
 +        filename: "an_image.jpg"
 +      }
  
 -    conn =
 -      build_conn()
 -      |> assign(:user, user)
 -      |> put("/api/v1/pleroma/mascot", %{"file" => file})
 +      conn =
 +        build_conn()
 +        |> assign(:user, user)
 +        |> put("/api/v1/pleroma/mascot", %{"file" => file})
  
 -    assert json_response(conn, 200)
 +      assert json_response(conn, 200)
  
 -    user = User.get_cached_by_id(user.id)
 +      user = User.get_cached_by_id(user.id)
  
 -    conn =
 -      build_conn()
 -      |> assign(:user, user)
 -      |> get("/api/v1/pleroma/mascot")
 +      conn =
 +        build_conn()
 +        |> assign(:user, user)
 +        |> get("/api/v1/pleroma/mascot")
  
 -    assert %{"url" => url, "type" => "image"} = json_response(conn, 200)
 -    assert url =~ "an_image"
 +      assert %{"url" => url, "type" => "image"} = json_response(conn, 200)
 +      assert url =~ "an_image"
 +    end
    end
  
-   test "hashtag timeline", %{conn: conn} do
-     following = insert(:user)
-     capture_log(fn ->
-       {:ok, activity} = CommonAPI.post(following, %{"status" => "test #2hu"})
-       {:ok, [_activity]} =
-         OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
-       nconn =
-         conn
-         |> get("/api/v1/timelines/tag/2hu")
-       assert [%{"id" => id}] = json_response(nconn, 200)
-       assert id == to_string(activity.id)
-       # works for different capitalization too
-       nconn =
-         conn
-         |> get("/api/v1/timelines/tag/2HU")
-       assert [%{"id" => id}] = json_response(nconn, 200)
-       assert id == to_string(activity.id)
-     end)
-   end
-   test "multi-hashtag timeline", %{conn: conn} do
-     user = insert(:user)
-     {:ok, activity_test} = CommonAPI.post(user, %{"status" => "#test"})
-     {:ok, activity_test1} = CommonAPI.post(user, %{"status" => "#test #test1"})
-     {:ok, activity_none} = CommonAPI.post(user, %{"status" => "#test #none"})
-     any_test =
-       conn
-       |> get("/api/v1/timelines/tag/test", %{"any" => ["test1"]})
-     [status_none, status_test1, status_test] = json_response(any_test, 200)
-     assert to_string(activity_test.id) == status_test["id"]
-     assert to_string(activity_test1.id) == status_test1["id"]
-     assert to_string(activity_none.id) == status_none["id"]
-     restricted_test =
-       conn
-       |> get("/api/v1/timelines/tag/test", %{"all" => ["test1"], "none" => ["none"]})
-     assert [status_test1] == json_response(restricted_test, 200)
-     all_test = conn |> get("/api/v1/timelines/tag/test", %{"all" => ["none"]})
-     assert [status_none] == json_response(all_test, 200)
-   end
    test "getting followers", %{conn: conn} do
      user = insert(:user)
      other_user = insert(:user)