[#1234] Merge remote-tracking branch 'remotes/upstream/develop' into 1234-mastodon...
authorIvan Tashkinov <ivantashkinov@gmail.com>
Wed, 2 Oct 2019 17:42:40 +0000 (20:42 +0300)
committerIvan Tashkinov <ivantashkinov@gmail.com>
Wed, 2 Oct 2019 17:42:40 +0000 (20:42 +0300)
# Conflicts:
# CHANGELOG.md
# lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
# lib/pleroma/web/router.ex

27 files changed:
1  2 
CHANGELOG.md
lib/pleroma/web/activity_pub/activity_pub_controller.ex
lib/pleroma/web/admin_api/admin_api_controller.ex
lib/pleroma/web/mastodon_api/controllers/account_controller.ex
lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
lib/pleroma/web/mastodon_api/controllers/list_controller.ex
lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
lib/pleroma/web/mastodon_api/controllers/report_controller.ex
lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
lib/pleroma/web/mastodon_api/controllers/search_controller.ex
lib/pleroma/web/mastodon_api/controllers/status_controller.ex
lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
lib/pleroma/web/oauth/oauth_controller.ex
lib/pleroma/web/pleroma_api/controllers/account_controller.ex
lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
lib/pleroma/web/router.ex
lib/pleroma/web/twitter_api/controllers/util_controller.ex
lib/pleroma/web/twitter_api/twitter_api_controller.ex
test/support/factory.ex
test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs
test/web/oauth/oauth_controller_test.exs

diff --combined CHANGELOG.md
index e3806166fa0d2a7f6230c6067c6cdbd849ef0994,3d9424c8fdb977788031a92b85276bade90bd328..87b6d1180d2b7997af1ef8195654e93de1536c09
@@@ -6,11 -6,22 +6,22 @@@ The format is based on [Keep a Changelo
  ## [Unreleased]
  ### Added
  - Refreshing poll results for remote polls
+ - Admin API: Add ability to require password reset
+ - Mastodon API: Account entities now include `follow_requests_count` (planned Mastodon 3.x addition)
+ - Pleroma API: `GET /api/v1/pleroma/accounts/:id/scrobbles` to get a list of recently scrobbled items
+ - Pleroma API: `POST /api/v1/pleroma/scrobble` to scrobble a media item
  ### Changed
  - **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
+ - **Breaking:** Admin API: Return link alongside with token on password reset
  - Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
  - Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
  - Admin API: Return `total` when querying for reports
+ - Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
+ - Admin API: Return link alongside with token on password reset
+ ### Fixed
+ - Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
  
  ## [1.1.0] - 2019-??-??
  ### Security
@@@ -38,6 -49,7 +49,7 @@@
  - AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses)
  - Improve digest email template
  – Pagination: (optional) return `total` alongside with `items` when paginating
+ - Add `rel="ugc"` to all links in statuses, to prevent SEO spam
  
  ### Fixed
  - Following from Osada
  - Pleroma API: Add `/api/v1/pleroma/accounts/confirmation_resend?email=<email>` for resending account confirmation.
  - Pleroma API: Email change endpoint.
  - Admin API: Added moderation log
+ - Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
  - Web response cache (currently, enabled for ActivityPub)
  - Mastodon API: Added an endpoint to get multiple statuses by IDs (`GET /api/v1/statuses/?ids[]=1&ids[]=2`)
+ - ActivityPub: Add ActivityPub actor's `discoverable` parameter.
+ - Admin API: Added moderation log filters (user/start date/end date/search/pagination)
 +- OAuth: support for hierarchical permissions / [Mastodon 2.4.3 OAuth permissions](https://docs.joinmastodon.org/api/permissions/)
  
  ### Changed
  - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
  - RichMedia: parsers and their order are configured in `rich_media` config.
  - RichMedia: add the rich media ttl based on image expiration time.
  
+ ## [1.0.7] - 2019-09-26
+ ### Fixed
+ - Broken federation on Erlang 22 (previous versions of hackney http client were using an option that got deprecated)
+ ### Changed
+ - ActivityPub: The first page in inboxes/outboxes is no longer embedded.
  ## [1.0.6] - 2019-08-14
  ### Fixed
  - MRF: fix use of unserializable keyword lists in describe() implementations
index 5ea749141f60664428efdfe2ee1fea4a84f30548,7cd13b4b8f3d48e54c4ebbe0aedf1ed121be3427..c349a704816572a9907ce2ce886b7b0d64e2dc6c
@@@ -30,11 -30,6 +30,11 @@@ defmodule Pleroma.Web.ActivityPub.Activ
      when action in [:activity, :object]
    )
  
 +  plug(
 +    Pleroma.Plugs.OAuthScopesPlug,
 +    %{scopes: ["read:accounts"]} when action in [:followers, :following]
 +  )
 +
    plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay])
    plug(:set_requester_reachable when action in [:inbox])
    plug(:relay_active? when action in [:relay])
@@@ -54,7 -49,8 +54,8 @@@
           {:ok, user} <- User.ensure_keys_present(user) do
        conn
        |> put_resp_content_type("application/activity+json")
-       |> json(UserView.render("user.json", %{user: user}))
+       |> put_view(UserView)
+       |> render("user.json", %{user: user})
      else
        nil -> {:error, :not_found}
      end
@@@ -95,7 -91,8 +96,8 @@@
  
        conn
        |> put_resp_content_type("application/activity+json")
-       |> json(ObjectView.render("likes.json", ap_id, likes, page))
+       |> put_view(ObjectView)
+       |> render("likes.json", %{ap_id: ap_id, likes: likes, page: page})
      else
        {:public?, false} ->
          {:error, :not_found}
           likes <- Utils.get_object_likes(object) do
        conn
        |> put_resp_content_type("application/activity+json")
-       |> json(ObjectView.render("likes.json", ap_id, likes))
+       |> put_view(ObjectView)
+       |> render("likes.json", %{ap_id: ap_id, likes: likes})
      else
        {:public?, false} ->
          {:error, :not_found}
    def following(%{assigns: %{relay: true}} = conn, _params) do
      conn
      |> put_resp_content_type("application/activity+json")
-     |> json(UserView.render("following.json", %{user: Relay.get_actor()}))
+     |> put_view(UserView)
+     |> render("following.json", %{user: Relay.get_actor()})
    end
  
    def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
  
        conn
        |> put_resp_content_type("application/activity+json")
-       |> json(UserView.render("following.json", %{user: user, page: page, for: for_user}))
+       |> put_view(UserView)
+       |> render("following.json", %{user: user, page: page, for: for_user})
      else
        {:show_follows, _} ->
          conn
           {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
        conn
        |> put_resp_content_type("application/activity+json")
-       |> json(UserView.render("following.json", %{user: user, for: for_user}))
+       |> put_view(UserView)
+       |> render("following.json", %{user: user, for: for_user})
      end
    end
  
    def followers(%{assigns: %{relay: true}} = conn, _params) do
      conn
      |> put_resp_content_type("application/activity+json")
-     |> json(UserView.render("followers.json", %{user: Relay.get_actor()}))
+     |> put_view(UserView)
+     |> render("followers.json", %{user: Relay.get_actor()})
    end
  
    def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
  
        conn
        |> put_resp_content_type("application/activity+json")
-       |> json(UserView.render("followers.json", %{user: user, page: page, for: for_user}))
+       |> put_view(UserView)
+       |> render("followers.json", %{user: user, page: page, for: for_user})
      else
        {:show_followers, _} ->
          conn
           {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
        conn
        |> put_resp_content_type("application/activity+json")
-       |> json(UserView.render("followers.json", %{user: user, for: for_user}))
+       |> put_view(UserView)
+       |> render("followers.json", %{user: user, for: for_user})
+     end
+   end
+   def outbox(conn, %{"nickname" => nickname, "page" => page?} = params)
+       when page? in [true, "true"] do
+     with %User{} = user <- User.get_cached_by_nickname(nickname),
+          {:ok, user} <- User.ensure_keys_present(user) do
+       activities =
+         if params["max_id"] do
+           ActivityPub.fetch_user_activities(user, nil, %{
+             "max_id" => params["max_id"],
+             # This is a hack because postgres generates inefficient queries when filtering by
+             # 'Answer', poll votes will be hidden by the visibility filter in this case anyway
+             "include_poll_votes" => true,
+             "limit" => 10
+           })
+         else
+           ActivityPub.fetch_user_activities(user, nil, %{
+             "limit" => 10,
+             "include_poll_votes" => true
+           })
+         end
+       conn
+       |> put_resp_content_type("application/activity+json")
+       |> put_view(UserView)
+       |> render("activity_collection_page.json", %{
+         activities: activities,
+         iri: "#{user.ap_id}/outbox"
+       })
      end
    end
  
-   def outbox(conn, %{"nickname" => nickname} = params) do
+   def outbox(conn, %{"nickname" => nickname}) do
      with %User{} = user <- User.get_cached_by_nickname(nickname),
           {:ok, user} <- User.ensure_keys_present(user) do
        conn
        |> put_resp_content_type("application/activity+json")
-       |> json(UserView.render("outbox.json", %{user: user, max_id: params["max_id"]}))
+       |> put_view(UserView)
+       |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
      end
    end
  
      with {:ok, user} <- User.ensure_keys_present(user) do
        conn
        |> put_resp_content_type("application/activity+json")
-       |> json(UserView.render("user.json", %{user: user}))
+       |> put_view(UserView)
+       |> render("user.json", %{user: user})
      else
        nil -> {:error, :not_found}
      end
      |> represent_service_actor(conn)
    end
  
+   @doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
    def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
      conn
      |> put_resp_content_type("application/activity+json")
-     |> json(UserView.render("user.json", %{user: user}))
+     |> put_view(UserView)
+     |> render("user.json", %{user: user})
    end
  
    def whoami(_conn, _params), do: {:error, :not_found}
  
    def read_inbox(
          %{assigns: %{user: %{nickname: nickname} = user}} = conn,
-         %{"nickname" => nickname} = params
-       ) do
+         %{"nickname" => nickname, "page" => page?} = params
+       )
+       when page? in [true, "true"] do
+     activities =
+       if params["max_id"] do
+         ActivityPub.fetch_activities([user.ap_id | user.following], %{
+           "max_id" => params["max_id"],
+           "limit" => 10
+         })
+       else
+         ActivityPub.fetch_activities([user.ap_id | user.following], %{"limit" => 10})
+       end
      conn
      |> put_resp_content_type("application/activity+json")
      |> put_view(UserView)
-     |> render("inbox.json", user: user, max_id: params["max_id"])
+     |> render("activity_collection_page.json", %{
+       activities: activities,
+       iri: "#{user.ap_id}/inbox"
+     })
+   end
+   def read_inbox(%{assigns: %{user: %{nickname: nickname} = user}} = conn, %{
+         "nickname" => nickname
+       }) do
+     with {:ok, user} <- User.ensure_keys_present(user) do
+       conn
+       |> put_resp_content_type("application/activity+json")
+       |> put_view(UserView)
+       |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
+     end
    end
  
    def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do
  
      {new_user, for_user}
    end
+   # TODO: Add support for "object" field
+   @doc """
+   Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
+   Parameters:
+   - (required) `file`: data of the media
+   - (optionnal) `description`: description of the media, intended for accessibility
+   Response:
+   - HTTP Code: 201 Created
+   - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
+   """
+   def upload_media(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
+     with {:ok, object} <-
+            ActivityPub.upload(
+              file,
+              actor: User.ap_id(user),
+              description: Map.get(data, "description")
+            ) do
+       Logger.debug(inspect(object))
+       conn
+       |> put_status(:created)
+       |> json(object.data)
+     end
+   end
  end
index 5bb5e67cd5deb3f9a85b2085c7a8bf65113ad95e,21da8a7ff1e933adc24b71ca196eeeb3face5bc1..513bae80060bc5506a1708bee93b99f357bb6ce6
@@@ -6,7 -6,6 +6,7 @@@ defmodule Pleroma.Web.AdminAPI.AdminAPI
    use Pleroma.Web, :controller
    alias Pleroma.Activity
    alias Pleroma.ModerationLog
 +  alias Pleroma.Plugs.OAuthScopesPlug
    alias Pleroma.User
    alias Pleroma.UserInviteToken
    alias Pleroma.Web.ActivityPub.ActivityPub
    alias Pleroma.Web.AdminAPI.Config
    alias Pleroma.Web.AdminAPI.ConfigView
    alias Pleroma.Web.AdminAPI.ModerationLogView
+   alias Pleroma.Web.AdminAPI.Report
    alias Pleroma.Web.AdminAPI.ReportView
    alias Pleroma.Web.AdminAPI.Search
    alias Pleroma.Web.CommonAPI
+   alias Pleroma.Web.Endpoint
    alias Pleroma.Web.MastodonAPI.StatusView
+   alias Pleroma.Web.Router
  
    import Pleroma.Web.ControllerHelper, only: [json_response: 3]
  
    require Logger
  
 +  plug(
 +    OAuthScopesPlug,
 +    %{scopes: ["read:accounts"]}
 +    when action in [:list_users, :user_show, :right_get, :invites]
 +  )
 +
 +  plug(
 +    OAuthScopesPlug,
 +    %{scopes: ["write:accounts"]}
 +    when action in [
 +           :get_invite_token,
 +           :revoke_invite,
 +           :email_invite,
 +           :get_password_reset,
 +           :user_follow,
 +           :user_unfollow,
 +           :user_delete,
 +           :users_create,
 +           :user_toggle_activation,
 +           :tag_users,
 +           :untag_users,
 +           :right_add,
 +           :right_delete,
 +           :set_activation_status
 +         ]
 +  )
 +
 +  plug(
 +    OAuthScopesPlug,
 +    %{scopes: ["read:reports"]} when action in [:list_reports, :report_show]
 +  )
 +
 +  plug(
 +    OAuthScopesPlug,
 +    %{scopes: ["write:reports"]}
 +    when action in [:report_update_state, :report_respond]
 +  )
 +
 +  plug(
 +    OAuthScopesPlug,
 +    %{scopes: ["read:statuses"]} when action == :list_user_statuses
 +  )
 +
 +  plug(
 +    OAuthScopesPlug,
 +    %{scopes: ["write:statuses"]}
 +    when action in [:status_update, :status_delete]
 +  )
 +
 +  plug(
 +    OAuthScopesPlug,
 +    %{scopes: ["read"]}
 +    when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log]
 +  )
 +
 +  plug(
 +    OAuthScopesPlug,
 +    %{scopes: ["write"]}
 +    when action in [:relay_follow, :relay_unfollow, :config_update]
 +  )
 +
    @users_page_size 50
  
    action_fallback(:errors)
    def user_show(conn, %{"nickname" => nickname}) do
      with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
        conn
-       |> json(AccountView.render("show.json", %{user: user}))
+       |> put_view(AccountView)
+       |> render("show.json", %{user: user})
      else
        _ -> {:error, :not_found}
      end
          })
  
        conn
-       |> json(StatusView.render("index.json", %{activities: activities, as: :activity}))
+       |> put_view(StatusView)
+       |> render("index.json", %{activities: activities, as: :activity})
      else
        _ -> {:error, :not_found}
      end
      })
  
      conn
-     |> json(AccountView.render("show.json", %{user: updated_user}))
+     |> put_view(AccountView)
+     |> render("show.json", %{user: updated_user})
    end
  
    def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
          "nickname" => nickname
        })
        when permission_group in ["moderator", "admin"] do
-     user = User.get_cached_by_nickname(nickname)
-     info =
-       %{}
-       |> Map.put("is_" <> permission_group, true)
+     info = Map.put(%{}, "is_" <> permission_group, true)
  
-     info_cng = User.Info.admin_api_update(user.info, info)
-     cng =
-       user
-       |> Ecto.Changeset.change()
-       |> Ecto.Changeset.put_embed(:info, info_cng)
+     {:ok, user} =
+       nickname
+       |> User.get_cached_by_nickname()
+       |> User.update_info(&User.Info.admin_api_update(&1, info))
  
      ModerationLog.insert_log(%{
        action: "grant",
        permission: permission_group
      })
  
-     {:ok, _user} = User.update_and_set_cache(cng)
      json(conn, info)
    end
  
      })
    end
  
+   def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
+     render_error(conn, :forbidden, "You can't revoke your own admin status.")
+   end
    def right_delete(
-         %{assigns: %{user: %User{:nickname => admin_nickname} = admin}} = conn,
+         %{assigns: %{user: admin}} = conn,
          %{
            "permission_group" => permission_group,
            "nickname" => nickname
          }
        )
        when permission_group in ["moderator", "admin"] do
-     if admin_nickname == nickname do
-       render_error(conn, :forbidden, "You can't revoke your own admin status.")
-     else
-       user = User.get_cached_by_nickname(nickname)
-       info =
-         %{}
-         |> Map.put("is_" <> permission_group, false)
+     info = Map.put(%{}, "is_" <> permission_group, false)
  
-       info_cng = User.Info.admin_api_update(user.info, info)
+     {:ok, user} =
+       nickname
+       |> User.get_cached_by_nickname()
+       |> User.update_info(&User.Info.admin_api_update(&1, info))
  
-       cng =
-         Ecto.Changeset.change(user)
-         |> Ecto.Changeset.put_embed(:info, info_cng)
-       {:ok, _user} = User.update_and_set_cache(cng)
-       ModerationLog.insert_log(%{
-         action: "revoke",
-         actor: admin,
-         subject: user,
-         permission: permission_group
-       })
+     ModerationLog.insert_log(%{
+       action: "revoke",
+       actor: admin,
+       subject: user,
+       permission: permission_group
+     })
  
-       json(conn, info)
-     end
+     json(conn, info)
    end
  
    def right_delete(conn, _) do
      invites = UserInviteToken.list_invites()
  
      conn
-     |> json(AccountView.render("invites.json", %{invites: invites}))
+     |> put_view(AccountView)
+     |> render("invites.json", %{invites: invites})
    end
  
    @doc "Revokes invite by token"
      with {:ok, invite} <- UserInviteToken.find_by_token(token),
           {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
        conn
-       |> json(AccountView.render("invite.json", %{invite: updated_invite}))
+       |> put_view(AccountView)
+       |> render("invite.json", %{invite: updated_invite})
      else
        nil -> {:error, :not_found}
      end
      {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
  
      conn
-     |> json(token.token)
+     |> json(%{
+       token: token.token,
+       link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
+     })
+   end
+   @doc "Force password reset for a given user"
+   def force_password_reset(conn, %{"nickname" => nickname}) do
+     (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
+     User.force_password_reset_async(user)
+     json_response(conn, :no_content, "")
    end
  
    def list_reports(conn, params) do
+     {page, page_size} = page_params(params)
      params =
        params
        |> Map.put("type", "Flag")
        |> Map.put("skip_preload", true)
        |> Map.put("total", true)
+       |> Map.put("limit", page_size)
+       |> Map.put("offset", (page - 1) * page_size)
  
-     reports = ActivityPub.fetch_activities([], params)
+     reports = ActivityPub.fetch_activities([], params, :offset)
  
      conn
      |> put_view(ReportView)
      with %Activity{} = report <- Activity.get_by_id(id) do
        conn
        |> put_view(ReportView)
-       |> render("show.json", %{report: report})
+       |> render("show.json", Report.extract_report_info(report))
      else
        _ -> {:error, :not_found}
      end
  
        conn
        |> put_view(ReportView)
-       |> render("show.json", %{report: report})
+       |> render("show.json", Report.extract_report_info(report))
      end
    end
  
  
        conn
        |> put_view(StatusView)
-       |> render("status.json", %{activity: activity})
+       |> render("show.json", %{activity: activity})
      else
        true ->
          {:param_cast, nil}
  
        conn
        |> put_view(StatusView)
-       |> render("status.json", %{activity: activity})
+       |> render("show.json", %{activity: activity})
      end
    end
  
    def list_log(conn, params) do
      {page, page_size} = page_params(params)
  
-     log = ModerationLog.get_all(page, page_size)
+     log =
+       ModerationLog.get_all(%{
+         page: page,
+         page_size: page_size,
+         start_date: params["start_date"],
+         end_date: params["end_date"],
+         user_id: params["user_id"],
+         search: params["search"]
+       })
  
      conn
      |> put_view(ModerationLogView)
      |> render("index.json", %{configs: updated})
    end
  
+   def reload_emoji(conn, _params) do
+     Pleroma.Emoji.reload()
+     conn |> json("ok")
+   end
    def errors(conn, {:error, :not_found}) do
      conn
      |> put_status(:not_found)
index 0000000000000000000000000000000000000000,df14ad66f9d426456cef27fd47e99157e0498eec..3bc9ed8ae4a26ab464f95d31470e3f580c770538
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,304 +1,344 @@@
+ # 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.AccountController do
+   use Pleroma.Web, :controller
+   import Pleroma.Web.ControllerHelper,
+     only: [add_link_headers: 2, truthy_param?: 1, assign_account_by_id: 2, json_response: 3]
+   alias Pleroma.Emoji
++  alias Pleroma.Plugs.OAuthScopesPlug
+   alias Pleroma.Plugs.RateLimiter
+   alias Pleroma.User
+   alias Pleroma.Web.ActivityPub.ActivityPub
+   alias Pleroma.Web.CommonAPI
+   alias Pleroma.Web.MastodonAPI.ListView
+   alias Pleroma.Web.MastodonAPI.MastodonAPI
+   alias Pleroma.Web.MastodonAPI.StatusView
+   alias Pleroma.Web.OAuth.Token
+   alias Pleroma.Web.TwitterAPI.TwitterAPI
++  plug(
++    OAuthScopesPlug,
++    %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
++    when action == :show
++  )
++
++  plug(
++    OAuthScopesPlug,
++    %{scopes: ["read:accounts"]}
++    when action in [:endorsements, :verify_credentials, :followers, :following]
++  )
++
++  plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
++
++  plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
++
++  plug(
++    OAuthScopesPlug,
++    %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
++  )
++
++  plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
++
++  plug(
++    OAuthScopesPlug,
++    %{scopes: ["follow", "write:follows"]} when action in [:follow, :unfollow]
++  )
++
++  plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
++
++  plug(
++    Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
++    when action != :create
++  )
++
+   @relations [:follow, :unfollow]
+   @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
+   plug(RateLimiter, {:relations_id_action, params: ["id", "uri"]} when action in @relations)
+   plug(RateLimiter, :relations_actions when action in @relations)
+   plug(RateLimiter, :app_account_creation when action == :create)
+   plug(:assign_account_by_id when action in @needs_account)
+   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+   @doc "POST /api/v1/accounts"
+   def create(
+         %{assigns: %{app: app}} = conn,
+         %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
+       ) do
+     params =
+       params
+       |> Map.take([
+         "email",
+         "captcha_solution",
+         "captcha_token",
+         "captcha_answer_data",
+         "token",
+         "password"
+       ])
+       |> Map.put("nickname", nickname)
+       |> Map.put("fullname", params["fullname"] || nickname)
+       |> Map.put("bio", params["bio"] || "")
+       |> Map.put("confirm", params["password"])
+     with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
+          {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
+       json(conn, %{
+         token_type: "Bearer",
+         access_token: token.token,
+         scope: app.scopes,
+         created_at: Token.Utils.format_created_at(token)
+       })
+     else
+       {:error, errors} -> json_response(conn, :bad_request, errors)
+     end
+   end
+   def create(%{assigns: %{app: _app}} = conn, _) do
+     render_error(conn, :bad_request, "Missing parameters")
+   end
+   def create(conn, _) do
+     render_error(conn, :forbidden, "Invalid credentials")
+   end
+   @doc "GET /api/v1/accounts/verify_credentials"
+   def verify_credentials(%{assigns: %{user: user}} = conn, _) do
+     chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
+     render(conn, "show.json",
+       user: user,
+       for: user,
+       with_pleroma_settings: true,
+       with_chat_token: chat_token
+     )
+   end
+   @doc "PATCH /api/v1/accounts/update_credentials"
+   def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
+     user = original_user
+     user_params =
+       %{}
+       |> add_if_present(params, "display_name", :name)
+       |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
+       |> add_if_present(params, "avatar", :avatar, fn value ->
+         with %Plug.Upload{} <- value,
+              {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
+           {:ok, object.data}
+         end
+       end)
+     emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
+     user_info_emojis =
+       user.info
+       |> Map.get(:emoji, [])
+       |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
+       |> Enum.dedup()
+     info_params =
+       [
+         :no_rich_text,
+         :locked,
+         :hide_followers_count,
+         :hide_follows_count,
+         :hide_followers,
+         :hide_follows,
+         :hide_favorites,
+         :show_role,
+         :skip_thread_containment,
+         :discoverable
+       ]
+       |> Enum.reduce(%{}, fn key, acc ->
+         add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
+       end)
+       |> add_if_present(params, "default_scope", :default_scope)
+       |> add_if_present(params, "fields", :fields, fn fields ->
+         fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
+         {:ok, fields}
+       end)
+       |> add_if_present(params, "fields", :raw_fields)
+       |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
+         {:ok, Map.merge(user.info.pleroma_settings_store, value)}
+       end)
+       |> add_if_present(params, "header", :banner, fn value ->
+         with %Plug.Upload{} <- value,
+              {:ok, object} <- ActivityPub.upload(value, type: :banner) do
+           {:ok, object.data}
+         end
+       end)
+       |> add_if_present(params, "pleroma_background_image", :background, fn value ->
+         with %Plug.Upload{} <- value,
+              {:ok, object} <- ActivityPub.upload(value, type: :background) do
+           {:ok, object.data}
+         end
+       end)
+       |> Map.put(:emoji, user_info_emojis)
+     changeset =
+       user
+       |> User.update_changeset(user_params)
+       |> User.change_info(&User.Info.profile_update(&1, info_params))
+     with {:ok, user} <- User.update_and_set_cache(changeset) do
+       if original_user != user, do: CommonAPI.update(user)
+       render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
+     else
+       _e -> render_error(conn, :forbidden, "Invalid request")
+     end
+   end
+   defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
+     with true <- Map.has_key?(params, params_field),
+          {:ok, new_value} <- value_function.(params[params_field]) do
+       Map.put(map, map_field, new_value)
+     else
+       _ -> map
+     end
+   end
+   @doc "GET /api/v1/accounts/relationships"
+   def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+     targets = User.get_all_by_ids(List.wrap(id))
+     render(conn, "relationships.json", user: user, targets: targets)
+   end
+   # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
+   def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
+   @doc "GET /api/v1/accounts/:id"
+   def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
+     with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
+          true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
+       render(conn, "show.json", user: user, for: for_user)
+     else
+       _e -> render_error(conn, :not_found, "Can't find user")
+     end
+   end
+   @doc "GET /api/v1/accounts/:id/statuses"
+   def statuses(%{assigns: %{user: reading_user}} = conn, params) do
+     with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
+       params = Map.put(params, "tag", params["tagged"])
+       activities = ActivityPub.fetch_user_activities(user, reading_user, params)
+       conn
+       |> add_link_headers(activities)
+       |> put_view(StatusView)
+       |> render("index.json", activities: activities, for: reading_user, as: :activity)
+     end
+   end
+   @doc "GET /api/v1/accounts/:id/followers"
+   def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
+     followers =
+       cond do
+         for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
+         user.info.hide_followers -> []
+         true -> MastodonAPI.get_followers(user, params)
+       end
+     conn
+     |> add_link_headers(followers)
+     |> render("index.json", for: for_user, users: followers, as: :user)
+   end
+   @doc "GET /api/v1/accounts/:id/following"
+   def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
+     followers =
+       cond do
+         for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
+         user.info.hide_follows -> []
+         true -> MastodonAPI.get_friends(user, params)
+       end
+     conn
+     |> add_link_headers(followers)
+     |> render("index.json", for: for_user, users: followers, as: :user)
+   end
+   @doc "GET /api/v1/accounts/:id/lists"
+   def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
+     lists = Pleroma.List.get_lists_account_belongs(user, account)
+     conn
+     |> put_view(ListView)
+     |> render("index.json", lists: lists)
+   end
+   @doc "POST /api/v1/accounts/:id/follow"
+   def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
+     {:error, :not_found}
+   end
+   def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
+     with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
+       render(conn, "relationship.json", user: follower, target: followed)
+     else
+       {:error, message} -> json_response(conn, :forbidden, %{error: message})
+     end
+   end
+   @doc "POST /api/v1/accounts/:id/unfollow"
+   def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
+     {:error, :not_found}
+   end
+   def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
+     with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
+       render(conn, "relationship.json", user: follower, target: followed)
+     end
+   end
+   @doc "POST /api/v1/accounts/:id/mute"
+   def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
+     notifications? = params |> Map.get("notifications", true) |> truthy_param?()
+     with {:ok, muter} <- User.mute(muter, muted, notifications?) do
+       render(conn, "relationship.json", user: muter, target: muted)
+     else
+       {:error, message} -> json_response(conn, :forbidden, %{error: message})
+     end
+   end
+   @doc "POST /api/v1/accounts/:id/unmute"
+   def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
+     with {:ok, muter} <- User.unmute(muter, muted) do
+       render(conn, "relationship.json", user: muter, target: muted)
+     else
+       {:error, message} -> json_response(conn, :forbidden, %{error: message})
+     end
+   end
+   @doc "POST /api/v1/accounts/:id/block"
+   def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
+     with {:ok, blocker} <- User.block(blocker, blocked),
+          {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
+       render(conn, "relationship.json", user: blocker, target: blocked)
+     else
+       {:error, message} -> json_response(conn, :forbidden, %{error: message})
+     end
+   end
+   @doc "POST /api/v1/accounts/:id/unblock"
+   def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
+     with {:ok, blocker} <- User.unblock(blocker, blocked),
+          {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
+       render(conn, "relationship.json", user: blocker, target: blocked)
+     else
+       {:error, message} -> json_response(conn, :forbidden, %{error: message})
+     end
+   end
++
++  @doc "GET /api/v1/endorsements"
++  def endorsements(conn, params),
++    do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
+ end
index 0000000000000000000000000000000000000000,ea1e36a12bfc254f72e374a2a4f9e8d867e68391..6c0584c54882956084f31a2fb0d199de3a8e0956
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,32 +1,38 @@@
+ # 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.ConversationController do
+   use Pleroma.Web, :controller
+   import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
+   alias Pleroma.Conversation.Participation
++  alias Pleroma.Plugs.OAuthScopesPlug
+   alias Pleroma.Repo
+   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
++  plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :index)
++  plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action == :read)
++
++  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
++
+   @doc "GET /api/v1/conversations"
+   def index(%{assigns: %{user: user}} = conn, params) do
+     participations = Participation.for_user_with_last_activity_id(user, params)
+     conn
+     |> add_link_headers(participations)
+     |> render("participations.json", participations: participations, for: user)
+   end
+   @doc "POST /api/v1/conversations/:id/read"
+   def 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
+       render(conn, "participation.json", participation: participation, for: user)
+     end
+   end
+ end
index 0000000000000000000000000000000000000000,03db6c9b838d82b1569a85bb1e485dc2a027f0e4..45c5ef8a44a70047e712adefbb0a2d3c66eb665e
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,26 +1,37 @@@
+ # 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.DomainBlockController do
+   use Pleroma.Web, :controller
++  alias Pleroma.Plugs.OAuthScopesPlug
+   alias Pleroma.User
++  plug(
++    OAuthScopesPlug,
++    %{scopes: ["follow", "read:blocks"]} when action == :index
++  )
++
++  plug(
++    OAuthScopesPlug,
++    %{scopes: ["follow", "write:blocks"]} when action != :index
++  )
++
+   @doc "GET /api/v1/domain_blocks"
+   def index(%{assigns: %{user: %{info: info}}} = conn, _) do
+     json(conn, Map.get(info, :domain_blocks, []))
+   end
+   @doc "POST /api/v1/domain_blocks"
+   def create(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
+     User.block_domain(blocker, domain)
+     json(conn, %{})
+   end
+   @doc "DELETE /api/v1/domain_blocks"
+   def delete(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
+     User.unblock_domain(blocker, domain)
+     json(conn, %{})
+   end
+ end
index 0000000000000000000000000000000000000000,19041304eea7a58b7b0d896abab425ec95a4e772..cadef72e15fa24e5be7dc6dedba074e3466561ed
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,72 +1,84 @@@
+ # 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.FilterController do
+   use Pleroma.Web, :controller
+   alias Pleroma.Filter
++  alias Pleroma.Plugs.OAuthScopesPlug
++
++  @oauth_read_actions [:show, :index]
++
++  plug(OAuthScopesPlug, %{scopes: ["read:filters"]} when action in @oauth_read_actions)
++
++  plug(
++    OAuthScopesPlug,
++    %{scopes: ["write:filters"]} when action not in @oauth_read_actions
++  )
++
++  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+   @doc "GET /api/v1/filters"
+   def index(%{assigns: %{user: user}} = conn, _) do
+     filters = Filter.get_filters(user)
+     render(conn, "filters.json", filters: filters)
+   end
+   @doc "POST /api/v1/filters"
+   def create(
+         %{assigns: %{user: user}} = conn,
+         %{"phrase" => phrase, "context" => context} = params
+       ) do
+     query = %Filter{
+       user_id: user.id,
+       phrase: phrase,
+       context: context,
+       hide: Map.get(params, "irreversible", false),
+       whole_word: Map.get(params, "boolean", true)
+       # expires_at
+     }
+     {:ok, response} = Filter.create(query)
+     render(conn, "filter.json", filter: response)
+   end
+   @doc "GET /api/v1/filters/:id"
+   def show(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
+     filter = Filter.get(filter_id, user)
+     render(conn, "filter.json", filter: filter)
+   end
+   @doc "PUT /api/v1/filters/:id"
+   def update(
+         %{assigns: %{user: user}} = conn,
+         %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
+       ) do
+     query = %Filter{
+       user_id: user.id,
+       filter_id: filter_id,
+       phrase: phrase,
+       context: context,
+       hide: Map.get(params, "irreversible", nil),
+       whole_word: Map.get(params, "boolean", true)
+       # expires_at
+     }
+     {:ok, response} = Filter.update(query)
+     render(conn, "filter.json", filter: response)
+   end
+   @doc "DELETE /api/v1/filters/:id"
+   def delete(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
+     query = %Filter{
+       user_id: user.id,
+       filter_id: filter_id
+     }
+     {:ok, _} = Filter.delete(query)
+     json(conn, %{})
+   end
+ end
index 0000000000000000000000000000000000000000,ce7b625eeeb5ac583f6ae31cdf66662f6ff91f47..06672e2bb2153b995789fde26ee94bf81a849ead
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,49 +1,57 @@@
+ # 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.FollowRequestController do
+   use Pleroma.Web, :controller
++  alias Pleroma.Plugs.OAuthScopesPlug
+   alias Pleroma.User
+   alias Pleroma.Web.CommonAPI
+   plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
+   plug(:assign_follower when action != :index)
+   action_fallback(:errors)
++  plug(OAuthScopesPlug, %{scopes: ["follow", "read:follows"]} when action == :index)
++
++  plug(
++    OAuthScopesPlug,
++    %{scopes: ["follow", "write:follows"]} when action != :index
++  )
++
+   @doc "GET /api/v1/follow_requests"
+   def index(%{assigns: %{user: followed}} = conn, _params) do
+     follow_requests = User.get_follow_requests(followed)
+     render(conn, "index.json", for: followed, users: follow_requests, as: :user)
+   end
+   @doc "POST /api/v1/follow_requests/:id/authorize"
+   def authorize(%{assigns: %{user: followed, follower: follower}} = conn, _params) do
+     with {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
+       render(conn, "relationship.json", user: followed, target: follower)
+     end
+   end
+   @doc "POST /api/v1/follow_requests/:id/reject"
+   def reject(%{assigns: %{user: followed, follower: follower}} = conn, _params) do
+     with {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
+       render(conn, "relationship.json", user: followed, target: follower)
+     end
+   end
+   defp assign_follower(%{params: %{"id" => id}} = conn, _) do
+     case User.get_cached_by_id(id) do
+       %User{} = follower -> assign(conn, :follower, follower)
+       nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
+     end
+   end
+   defp errors(conn, {:error, message}) do
+     conn
+     |> put_status(:forbidden)
+     |> json(%{error: message})
+   end
+ end
index be70896305f165a81a84190db0b49f9f828198de,50f42bee514a1ed31577ac1909144f607d8744c7..e0ffdba213713457cbbc86e3bb70c43e01f33f35
@@@ -5,20 -5,11 +5,22 @@@
  defmodule Pleroma.Web.MastodonAPI.ListController do
    use Pleroma.Web, :controller
  
 +  alias Pleroma.Plugs.OAuthScopesPlug
    alias Pleroma.User
    alias Pleroma.Web.MastodonAPI.AccountView
  
    plug(:list_by_id_and_user when action not in [:index, :create])
  
 +  plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in [:index, :show, :list_accounts])
 +
 +  plug(
 +    OAuthScopesPlug,
 +    %{scopes: ["write:lists"]}
 +    when action in [:create, :update, :delete, :add_to_list, :remove_from_list]
 +  )
 +
++  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
++
    action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
  
    # GET /api/v1/lists
@@@ -58,7 -49,7 +60,7 @@@
      with {:ok, users} <- Pleroma.List.get_following(list) do
        conn
        |> put_view(AccountView)
-       |> render("accounts.json", for: user, users: users, as: :user)
+       |> render("index.json", for: user, users: users, as: :user)
      end
    end
  
index b1e9dee3d03f70726c371ac19c41b686ab2ff23e,1484a017472d55432721a91b0f33d0cbbe2cf03e..ee644abe30da46dbac18c9e76a153e7d6fd487f5
@@@ -5,24 -5,16 +5,17 @@@
  defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
    use Pleroma.Web, :controller
  
-   import Pleroma.Web.ControllerHelper,
-     only: [json_response: 3, add_link_headers: 2, add_link_headers: 3]
+   import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
  
-   alias Ecto.Changeset
    alias Pleroma.Activity
    alias Pleroma.Bookmark
    alias Pleroma.Config
-   alias Pleroma.Conversation.Participation
-   alias Pleroma.Filter
-   alias Pleroma.Formatter
    alias Pleroma.HTTP
-   alias Pleroma.Notification
    alias Pleroma.Object
    alias Pleroma.Pagination
 +  alias Pleroma.Plugs.OAuthScopesPlug
    alias Pleroma.Plugs.RateLimiter
    alias Pleroma.Repo
-   alias Pleroma.ScheduledActivity
    alias Pleroma.Stats
    alias Pleroma.User
    alias Pleroma.Web
    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
    alias Pleroma.Web.MastodonAPI.MastodonView
-   alias Pleroma.Web.MastodonAPI.NotificationView
-   alias Pleroma.Web.MastodonAPI.ReportView
-   alias Pleroma.Web.MastodonAPI.ScheduledActivityView
    alias Pleroma.Web.MastodonAPI.StatusView
    alias Pleroma.Web.MediaProxy
    alias Pleroma.Web.OAuth.App
    alias Pleroma.Web.OAuth.Token
    alias Pleroma.Web.TwitterAPI.TwitterAPI
  
-   alias Pleroma.Web.ControllerHelper
-   import Ecto.Query
    require Logger
-   require Pleroma.Constants
  
-   plug(
-     OAuthScopesPlug,
-     %{scopes: ["write:accounts"]}
-     # Note: the following actions are not permission-secured in Mastodon:
-     when action in [
-            :put_settings,
-            :update_avatar,
-            :update_banner,
-            :update_background,
-            :set_mascot
-          ]
-   )
-   plug(
-     OAuthScopesPlug,
-     %{scopes: ["write:accounts"]}
-     when action in [:pin_status, :unpin_status, :update_credentials]
-   )
-   plug(
-     OAuthScopesPlug,
-     %{scopes: ["read:statuses"]}
-     when action in [
-            :conversations,
-            :scheduled_statuses,
-            :show_scheduled_status,
-            :home_timeline,
-            :dm_timeline
-          ]
-   )
-   plug(
-     OAuthScopesPlug,
-     %{@unauthenticated_access | scopes: ["read:statuses"]}
-     when action in [
-            :user_statuses,
-            :get_statuses,
-            :get_status,
-            :get_context,
-            :status_card,
-            :get_poll
-          ]
-   )
-   plug(
-     OAuthScopesPlug,
-     %{scopes: ["write:statuses"]}
-     when action in [
-            :update_scheduled_status,
-            :delete_scheduled_status,
-            :post_status,
-            :delete_status,
-            :reblog_status,
-            :unreblog_status,
-            :poll_vote
-          ]
-   )
-   plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action == :conversation_read)
-   plug(
-     OAuthScopesPlug,
-     %{scopes: ["read:accounts"]}
-     when action in [:endorsements, :verify_credentials, :followers, :following, :get_mascot]
-   )
-   plug(
-     OAuthScopesPlug,
-     %{@unauthenticated_access | scopes: ["read:accounts"]}
-     when action in [:user, :favourited_by, :reblogged_by]
-   )
-   plug(
-     OAuthScopesPlug,
-     %{scopes: ["read:favourites"]} when action in [:favourites, :user_favourites]
-   )
 +  @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
 +
 +  # Note: :index action handles attempt of unauthenticated access to private instance with redirect
 +  plug(
 +    OAuthScopesPlug,
 +    Map.merge(@unauthenticated_access, %{scopes: ["read"], skip_instance_privacy_check: true})
 +    when action == :index
 +  )
 +
 +  plug(
 +    OAuthScopesPlug,
 +    %{scopes: ["read"]} when action in [:suggestions, :verify_app_credentials]
 +  )
 +
-     %{scopes: ["write:favourites"]} when action in [:fav_status, :unfav_status]
++  plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :put_settings)
 +
 +  plug(
 +    OAuthScopesPlug,
-   plug(OAuthScopesPlug, %{scopes: ["read:filters"]} when action in [:get_filters, :get_filter])
++    %{@unauthenticated_access | scopes: ["read:statuses"]} when action == :get_poll
 +  )
 +
-   plug(
-     OAuthScopesPlug,
-     %{scopes: ["write:filters"]} when action in [:create_filter, :update_filter, :delete_filter]
-   )
-   plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in [:account_lists, :list_timeline])
++  plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :poll_vote)
 +
-     %{scopes: ["read:notifications"]} when action in [:notifications, :get_notification]
-   )
-   plug(
-     OAuthScopesPlug,
-     %{scopes: ["write:notifications"]}
-     when action in [:clear_notifications, :dismiss_notification, :destroy_multiple_notifications]
-   )
-   plug(
-     OAuthScopesPlug,
-     %{scopes: ["write:reports"]}
-     when action in [:create_report, :report_update_state, :report_respond]
++  plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
 +
 +  plug(OAuthScopesPlug, %{scopes: ["write:media"]} when action in [:upload, :update_media])
 +
 +  plug(
 +    OAuthScopesPlug,
-     %{scopes: ["follow", "read:blocks"]} when action in [:blocks, :domain_blocks]
-   )
-   plug(
-     OAuthScopesPlug,
-     %{scopes: ["follow", "write:blocks"]}
-     when action in [:block, :unblock, :block_domain, :unblock_domain]
-   )
-   plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
-   plug(OAuthScopesPlug, %{scopes: ["follow", "read:follows"]} when action == :follow_requests)
-   plug(
-     OAuthScopesPlug,
-     %{scopes: ["follow", "write:follows"]}
-     when action in [
-            :follow,
-            :unfollow,
-            :subscribe,
-            :unsubscribe,
-            :authorize_follow_request,
-            :reject_follow_request
-          ]
++    %{scopes: ["follow", "read:blocks"]} when action == :blocks
 +  )
 +
++  # To do: POST /api/v1/follows is not present in Mastodon; consider removing the action
 +  plug(
 +    OAuthScopesPlug,
-   plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
-   plug(
-     OAuthScopesPlug,
-     %{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
-   )
++    %{scopes: ["follow", "write:follows"]} when action == :follows
 +  )
 +
 +  plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
-   # Note: scopes not present in Mastodon: read:bookmarks, write:bookmarks
 +
-   plug(
-     OAuthScopesPlug,
-     %{scopes: ["write:bookmarks"]} when action in [:bookmark_status, :unbookmark_status]
-   )
++  # Note: scope not present in Mastodon: read:bookmarks
 +  plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks)
 +
-            :account_register,
 +  # An extra safety measure for possible actions not guarded by OAuth permissions specification
 +  plug(
 +    Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
 +    when action not in [
-            :account_confirmation_resend,
 +           :create_app,
 +           :index,
 +           :login,
 +           :logout,
 +           :password_reset,
-   @rate_limited_relations_actions ~w(follow unfollow)a
-   @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
-     post_status delete_status)a
-   plug(
-     RateLimiter,
-     {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
-     when action in ~w(reblog_status unreblog_status)a
-   )
-   plug(
-     RateLimiter,
-     {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
-     when action in ~w(fav_status unfav_status)a
-   )
-   plug(
-     RateLimiter,
-     {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
-   )
-   plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
-   plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
-   plug(RateLimiter, :app_account_creation when action == :account_register)
-   plug(RateLimiter, :search when action in [:search, :search2, :account_search])
 +           :masto_instance,
 +           :peers,
 +           :custom_emojis
 +         ]
 +  )
 +
    plug(RateLimiter, :password_reset when action == :password_reset)
-   plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
  
    @local_mastodon_name "Mastodon-Local"
  
      end
    end
  
-   defp add_if_present(
-          map,
-          params,
-          params_field,
-          map_field,
-          value_function \\ fn x -> {:ok, x} end
-        ) do
-     if Map.has_key?(params, params_field) do
-       case value_function.(params[params_field]) do
-         {:ok, new_value} -> Map.put(map, map_field, new_value)
-         :error -> map
-       end
-     else
-       map
-     end
-   end
-   def update_credentials(%{assigns: %{user: user}} = conn, params) do
-     original_user = user
-     user_params =
-       %{}
-       |> add_if_present(params, "display_name", :name)
-       |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
-       |> add_if_present(params, "avatar", :avatar, fn value ->
-         with %Plug.Upload{} <- value,
-              {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
-           {:ok, object.data}
-         else
-           _ -> :error
-         end
-       end)
-     emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
-     user_info_emojis =
-       user.info
-       |> Map.get(:emoji, [])
-       |> Enum.concat(Formatter.get_emoji_map(emojis_text))
-       |> Enum.dedup()
-     info_params =
-       [
-         :no_rich_text,
-         :locked,
-         :hide_followers_count,
-         :hide_follows_count,
-         :hide_followers,
-         :hide_follows,
-         :hide_favorites,
-         :show_role,
-         :skip_thread_containment
-       ]
-       |> 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, "fields", :fields, fn fields ->
-         fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
-         {:ok, fields}
-       end)
-       |> add_if_present(params, "fields", :raw_fields)
-       |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
-         {:ok, Map.merge(user.info.pleroma_settings_store, value)}
-       end)
-       |> add_if_present(params, "header", :banner, fn value ->
-         with %Plug.Upload{} <- value,
-              {:ok, object} <- ActivityPub.upload(value, type: :banner) do
-           {:ok, object.data}
-         else
-           _ -> :error
-         end
-       end)
-       |> add_if_present(params, "pleroma_background_image", :background, fn value ->
-         with %Plug.Upload{} <- value,
-              {:ok, object} <- ActivityPub.upload(value, type: :background) do
-           {:ok, object.data}
-         else
-           _ -> :error
-         end
-       end)
-       |> Map.put(:emoji, user_info_emojis)
-     info_cng = User.Info.profile_update(user.info, info_params)
-     with changeset <- User.update_changeset(user, user_params),
-          changeset <- Changeset.put_embed(changeset, :info, info_cng),
-          {:ok, user} <- User.update_and_set_cache(changeset) do
-       if original_user != user do
-         CommonAPI.update(user)
-       end
-       json(
-         conn,
-         AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
-       )
-     else
-       _e -> render_error(conn, :forbidden, "Invalid request")
-     end
-   end
-   def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
-     change = Changeset.change(user, %{avatar: nil})
-     {:ok, user} = User.update_and_set_cache(change)
-     CommonAPI.update(user)
-     json(conn, %{url: nil})
-   end
-   def update_avatar(%{assigns: %{user: user}} = conn, params) do
-     {:ok, object} = ActivityPub.upload(params, type: :avatar)
-     change = Changeset.change(user, %{avatar: object.data})
-     {:ok, user} = User.update_and_set_cache(change)
-     CommonAPI.update(user)
-     %{"url" => [%{"href" => href} | _]} = object.data
-     json(conn, %{url: href})
-   end
-   def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
-     with new_info <- %{"banner" => %{}},
-          info_cng <- User.Info.profile_update(user.info, new_info),
-          changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
-          {:ok, user} <- User.update_and_set_cache(changeset) do
-       CommonAPI.update(user)
-       json(conn, %{url: nil})
-     end
-   end
-   def update_banner(%{assigns: %{user: user}} = conn, params) do
-     with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
-          new_info <- %{"banner" => object.data},
-          info_cng <- User.Info.profile_update(user.info, new_info),
-          changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
-          {:ok, user} <- User.update_and_set_cache(changeset) do
-       CommonAPI.update(user)
-       %{"url" => [%{"href" => href} | _]} = object.data
-       json(conn, %{url: href})
-     end
-   end
-   def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
-     with new_info <- %{"background" => %{}},
-          info_cng <- User.Info.profile_update(user.info, new_info),
-          changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
-          {:ok, _user} <- User.update_and_set_cache(changeset) do
-       json(conn, %{url: nil})
-     end
-   end
-   def update_background(%{assigns: %{user: user}} = conn, params) do
-     with {:ok, object} <- ActivityPub.upload(params, type: :background),
-          new_info <- %{"background" => object.data},
-          info_cng <- User.Info.profile_update(user.info, new_info),
-          changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
-          {:ok, _user} <- User.update_and_set_cache(changeset) do
-       %{"url" => [%{"href" => href} | _]} = object.data
-       json(conn, %{url: href})
-     end
-   end
-   def verify_credentials(%{assigns: %{user: user}} = conn, _) do
-     chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
-     account =
-       AccountView.render("account.json", %{
-         user: user,
-         for: user,
-         with_pleroma_settings: true,
-         with_chat_token: chat_token
-       })
-     json(conn, account)
-   end
    def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
      with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
        conn
      end
    end
  
-   def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
-     with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
-          true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
-       account = AccountView.render("account.json", %{user: user, for: for_user})
-       json(conn, account)
-     else
-       _e -> render_error(conn, :not_found, "Can't find user")
-     end
-   end
    @mastodon_api_level "2.7.2"
  
    def masto_instance(conn, _params) do
  
    defp mastodonized_emoji do
      Pleroma.Emoji.get_all()
-     |> Enum.map(fn {shortcode, relative_url, tags} ->
+     |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
        url = to_string(URI.merge(Web.base_url(), relative_url))
  
        %{
          "url" => url,
          "tags" => tags,
          # Assuming that a comma is authorized in the category name
-         "category" => (tags -- ["Custom"]) |> Enum.join(",")
-       }
-     end)
-   end
-   def custom_emojis(conn, _params) do
-     mastodon_emoji = mastodonized_emoji()
-     json(conn, mastodon_emoji)
-   end
-   def home_timeline(%{assigns: %{user: user}} = conn, params) do
-     params =
-       params
-       |> Map.put("type", ["Create", "Announce"])
-       |> Map.put("blocking_user", user)
-       |> Map.put("muting_user", user)
-       |> Map.put("user", user)
-     activities =
-       [user.ap_id | user.following]
-       |> ActivityPub.fetch_activities(params)
-       |> Enum.reverse()
-     conn
-     |> add_link_headers(activities)
-     |> put_view(StatusView)
-     |> render("index.json", %{activities: activities, for: user, as: :activity})
-   end
-   def public_timeline(%{assigns: %{user: user}} = conn, params) do
-     local_only = params["local"] in [true, "True", "true", "1"]
-     activities =
-       params
-       |> Map.put("type", ["Create", "Announce"])
-       |> Map.put("local_only", local_only)
-       |> Map.put("blocking_user", user)
-       |> Map.put("muting_user", user)
-       |> Map.put("user", user)
-       |> 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 user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
-     with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
-       params =
-         params
-         |> Map.put("tag", params["tagged"])
-       activities = ActivityPub.fetch_user_activities(user, reading_user, params)
-       conn
-       |> add_link_headers(activities)
-       |> put_view(StatusView)
-       |> render("index.json", %{
-         activities: activities,
-         for: reading_user,
-         as: :activity
-       })
-     end
-   end
-   def dm_timeline(%{assigns: %{user: user}} = conn, params) do
-     params =
-       params
-       |> Map.put("type", "Create")
-       |> Map.put("blocking_user", user)
-       |> Map.put("user", user)
-       |> Map.put(:visibility, "direct")
-     activities =
-       [user.ap_id]
-       |> ActivityPub.fetch_activities_query(params)
-       |> Pagination.fetch_paginated(params)
-     conn
-     |> add_link_headers(activities)
-     |> put_view(StatusView)
-     |> render("index.json", %{activities: activities, for: user, as: :activity})
-   end
-   def get_statuses(%{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))
-     conn
-     |> put_view(StatusView)
-     |> render("index.json", activities: activities, for: user, as: :activity)
-   end
-   def get_status(%{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
-       conn
-       |> put_view(StatusView)
-       |> try_render("status.json", %{activity: activity, for: user})
-     end
-   end
-   def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-     with %Activity{} = activity <- Activity.get_by_id(id),
-          activities <-
-            ActivityPub.fetch_activities_for_context(activity.data["context"], %{
-              "blocking_user" => user,
-              "user" => user,
-              "exclude_id" => activity.id
-            }),
-          grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
-       result = %{
-         ancestors:
-           StatusView.render(
-             "index.json",
-             for: user,
-             activities: grouped_activities[true] || [],
-             as: :activity
-           )
-           |> Enum.reverse(),
-         # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
-         descendants:
-           StatusView.render(
-             "index.json",
-             for: user,
-             activities: grouped_activities[false] || [],
-             as: :activity
-           )
-           |> Enum.reverse()
-         # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
-       }
-       json(conn, result)
-     end
-   end
-   def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-     with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
-          %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
-          true <- Visibility.visible_for_user?(activity, user) do
-       conn
-       |> put_view(StatusView)
-       |> try_render("poll.json", %{object: object, for: user})
-     else
-       error when is_nil(error) or error == false ->
-         render_error(conn, :not_found, "Record not found")
-     end
-   end
-   defp get_cached_vote_or_vote(user, object, choices) do
-     idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
-     {_, res} =
-       Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
-         case CommonAPI.vote(user, object, choices) do
-           {:error, _message} = res -> {:ignore, res}
-           res -> {:commit, res}
-         end
-       end)
-     res
-   end
-   def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
-     with %Object{} = object <- Object.get_by_id(id),
-          true <- object.data["type"] == "Question",
-          %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
-          true <- Visibility.visible_for_user?(activity, user),
-          {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
-       conn
-       |> put_view(StatusView)
-       |> try_render("poll.json", %{object: object, for: user})
-     else
-       nil ->
-         render_error(conn, :not_found, "Record not found")
-       false ->
-         render_error(conn, :not_found, "Record not found")
-       {:error, message} ->
-         conn
-         |> put_status(:unprocessable_entity)
-         |> json(%{error: message})
-     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})
-       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 notifications(%{assigns: %{user: user}} = conn, params) do
-     notifications = MastodonAPI.get_notifications(user, params)
-     conn
-     |> add_link_headers(notifications)
-     |> put_view(NotificationView)
-     |> render("index.json", %{notifications: notifications, for: user})
-   end
-   def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
-     with {:ok, notification} <- Notification.get(user, id) do
-       conn
-       |> put_view(NotificationView)
-       |> render("show.json", %{notification: notification, for: user})
-     else
-       {:error, reason} ->
-         conn
-         |> put_status(:forbidden)
-         |> json(%{"error" => reason})
-     end
-   end
-   def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
-     Notification.clear(user)
-     json(conn, %{})
-   end
-   def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
-     with {:ok, _notif} <- Notification.dismiss(user, id) do
-       json(conn, %{})
-     else
-       {:error, reason} ->
-         conn
-         |> put_status(:forbidden)
-         |> json(%{"error" => reason})
-     end
-   end
-   def destroy_multiple_notifications(%{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)
-     targets = Repo.all(q)
-     conn
-     |> put_view(AccountView)
-     |> render("relationships.json", %{user: user, targets: targets})
-   end
-   # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
-   def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
-   def update_media(%{assigns: %{user: user}} = conn, data) do
-     with %Object{} = object <- Repo.get(Object, data["id"]),
-          true <- Object.authorize_mutation(object, user),
-          true <- is_binary(data["description"]),
-          description <- data["description"] do
-       new_data = %{object.data | "name" => description}
-       {:ok, _} =
-         object
-         |> Object.change(%{data: new_data})
-         |> Repo.update()
-       attachment_data = Map.put(new_data, "id", object.id)
-       conn
-       |> put_view(StatusView)
-       |> render("attachment.json", %{attachment: attachment_data})
-     end
-   end
-   def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
-     with {:ok, object} <-
-            ActivityPub.upload(
-              file,
-              actor: User.ap_id(user),
-              description: Map.get(data, "description")
-            ) do
-       attachment_data = Map.put(object.data, "id", object.id)
-       conn
-       |> put_view(StatusView)
-       |> render("attachment.json", %{attachment: attachment_data})
-     end
-   end
-   def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
-     with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
-          %{} = attachment_data <- Map.put(object.data, "id", object.id),
-          %{type: type} = rendered <-
-            StatusView.render("attachment.json", %{attachment: attachment_data}) do
-       # Reject if not an image
-       if type == "image" do
-         # Sure!
-         # Save to the user's info
-         info_changeset = User.Info.mascot_update(user.info, rendered)
-         user_changeset =
-           user
-           |> Changeset.change()
-           |> Changeset.put_embed(:info, info_changeset)
-         {:ok, _user} = User.update_and_set_cache(user_changeset)
-         conn
-         |> json(rendered)
-       else
-         render_error(conn, :unsupported_media_type, "mascots can only be images")
-       end
-     end
-   end
-   def get_mascot(%{assigns: %{user: user}} = conn, _params) do
-     mascot = User.get_mascot(user)
-     conn
-     |> json(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})
+         "category" => (tags -- ["Custom"]) |> Enum.join(",")
+       }
+     end)
    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
-       followers =
-         cond do
-           for_user && user.id == for_user.id -> followers
-           user.info.hide_followers -> []
-           true -> followers
-         end
+   def custom_emojis(conn, _params) do
+     mastodon_emoji = mastodonized_emoji()
+     json(conn, mastodon_emoji)
+   end
  
+   def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+     with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
+          %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
+          true <- Visibility.visible_for_user?(activity, user) do
        conn
-       |> add_link_headers(followers)
-       |> put_view(AccountView)
-       |> render("accounts.json", %{for: for_user, users: followers, as: :user})
+       |> put_view(StatusView)
+       |> try_render("poll.json", %{object: object, for: user})
+     else
+       error when is_nil(error) or error == false ->
+         render_error(conn, :not_found, "Record not found")
      end
    end
  
-   def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
-     with %User{} = user <- User.get_cached_by_id(id),
-          followers <- MastodonAPI.get_friends(user, params) do
-       followers =
-         cond do
-           for_user && user.id == for_user.id -> followers
-           user.info.hide_follows -> []
-           true -> followers
-         end
+   defp get_cached_vote_or_vote(user, object, choices) do
+     idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
  
-       conn
-       |> add_link_headers(followers)
-       |> put_view(AccountView)
-       |> render("accounts.json", %{for: for_user, users: followers, as: :user})
-     end
-   end
+     {_, res} =
+       Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
+         case CommonAPI.vote(user, object, choices) do
+           {:error, _message} = res -> {:ignore, res}
+           res -> {:commit, res}
+         end
+       end)
  
-   def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
-     with {:ok, follow_requests} <- User.get_follow_requests(followed) do
-       conn
-       |> put_view(AccountView)
-       |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
-     end
+     res
    end
  
-   def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
-     with %User{} = follower <- User.get_cached_by_id(id),
-          {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
+   def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
+     with %Object{} = object <- Object.get_by_id(id),
+          true <- object.data["type"] == "Question",
+          %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
+          true <- Visibility.visible_for_user?(activity, user),
+          {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
        conn
-       |> put_view(AccountView)
-       |> render("relationship.json", %{user: followed, target: follower})
+       |> put_view(StatusView)
+       |> try_render("poll.json", %{object: object, for: user})
      else
+       nil ->
+         render_error(conn, :not_found, "Record not found")
+       false ->
+         render_error(conn, :not_found, "Record not found")
        {:error, message} ->
          conn
-         |> put_status(:forbidden)
+         |> put_status(:unprocessable_entity)
          |> json(%{error: message})
      end
    end
  
-   def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
-     with %User{} = follower <- User.get_cached_by_id(id),
-          {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
+   def update_media(
+         %{assigns: %{user: user}} = conn,
+         %{"id" => id, "description" => description} = _
+       )
+       when is_binary(description) do
+     with %Object{} = object <- Repo.get(Object, id),
+          true <- Object.authorize_mutation(object, user),
+          {:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
+       attachment_data = Map.put(data, "id", object.id)
        conn
-       |> put_view(AccountView)
-       |> render("relationship.json", %{user: followed, target: follower})
-     else
-       {:error, message} ->
-         conn
-         |> put_status(:forbidden)
-         |> json(%{error: message})
+       |> put_view(StatusView)
+       |> render("attachment.json", %{attachment: attachment_data})
      end
    end
  
-   def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) 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
-       {:followed, _} ->
-         {:error, :not_found}
+   def update_media(_conn, _data), do: {:error, :bad_request}
  
-       {:error, message} ->
-         conn
-         |> put_status(:forbidden)
-         |> json(%{error: message})
-     end
-   end
+   def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
+     with {:ok, object} <-
+            ActivityPub.upload(
+              file,
+              actor: User.ap_id(user),
+              description: Map.get(data, "description")
+            ) do
+       attachment_data = Map.put(object.data, "id", object.id)
  
-   def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
-     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_status(:forbidden)
-         |> json(%{error: message})
+       |> put_view(StatusView)
+       |> render("attachment.json", %{attachment: attachment_data})
      end
    end
  
-   def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
-     with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
+   def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
+     with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
           {_, true} <- {:followed, follower.id != followed.id},
-          {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
+          {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
        conn
        |> put_view(AccountView)
-       |> render("relationship.json", %{user: follower, target: followed})
+       |> render("show.json", %{user: followed, for: follower})
      else
        {:followed, _} ->
          {:error, :not_found}
  
-       error ->
-         error
-     end
-   end
-   def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
-     notifications =
-       if Map.has_key?(params, "notifications"),
-         do: params["notifications"] in [true, "True", "true", "1"],
-         else: true
-     with %User{} = muted <- User.get_cached_by_id(id),
-          {:ok, muter} <- User.mute(muter, muted, notifications) do
-       conn
-       |> put_view(AccountView)
-       |> render("relationship.json", %{user: muter, target: muted})
-     else
-       {:error, message} ->
-         conn
-         |> put_status(:forbidden)
-         |> json(%{error: message})
-     end
-   end
-   def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
-     with %User{} = muted <- User.get_cached_by_id(id),
-          {:ok, muter} <- User.unmute(muter, muted) do
-       conn
-       |> put_view(AccountView)
-       |> render("relationship.json", %{user: muter, target: muted})
-     else
        {:error, message} ->
          conn
          |> put_status(:forbidden)
  
    def mutes(%{assigns: %{user: user}} = conn, _) do
      with muted_accounts <- User.muted_users(user) do
-       res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
+       res = AccountView.render("index.json", users: muted_accounts, for: user, as: :user)
        json(conn, res)
      end
    end
  
-   def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
-     with %User{} = blocked <- User.get_cached_by_id(id),
-          {:ok, blocker} <- User.block(blocker, blocked),
-          {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
-       conn
-       |> put_view(AccountView)
-       |> render("relationship.json", %{user: blocker, target: blocked})
-     else
-       {:error, message} ->
-         conn
-         |> put_status(:forbidden)
-         |> json(%{error: message})
-     end
-   end
-   def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
-     with %User{} = blocked <- User.get_cached_by_id(id),
-          {:ok, blocker} <- User.unblock(blocker, blocked),
-          {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
-       conn
-       |> put_view(AccountView)
-       |> render("relationship.json", %{user: blocker, target: blocked})
-     else
-       {:error, message} ->
-         conn
-         |> put_status(:forbidden)
-         |> json(%{error: message})
-     end
-   end
    def blocks(%{assigns: %{user: user}} = conn, _) do
      with blocked_accounts <- User.blocked_users(user) do
-       res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
+       res = AccountView.render("index.json", users: blocked_accounts, for: user, as: :user)
        json(conn, res)
      end
    end
  
-   def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
-     json(conn, info.domain_blocks || [])
-   end
-   def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
-     User.block_domain(blocker, domain)
-     json(conn, %{})
-   end
-   def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
-     User.unblock_domain(blocker, domain)
-     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_status(:forbidden)
-         |> json(%{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_status(:forbidden)
-         |> json(%{error: message})
-     end
-   end
    def favourites(%{assigns: %{user: user}} = conn, params) do
      params =
        params
      |> render("index.json", %{activities: activities, for: user, as: :activity})
    end
  
-   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
-           [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
-         else
-           [Pleroma.Constants.as_public()]
-         end
-       activities =
-         recipients
-         |> ActivityPub.fetch_activities(params)
-         |> Enum.reverse()
-       conn
-       |> add_link_headers(activities)
-       |> put_view(StatusView)
-       |> render("index.json", %{activities: activities, for: for_user, as: :activity})
-     else
-       nil -> {:error, :not_found}
-       true -> render_error(conn, :forbidden, "Can't get favorites")
-     end
-   end
    def bookmarks(%{assigns: %{user: user}} = conn, params) do
      user = User.get_cached_by_id(user.id)
  
      |> render("index.json", %{activities: activities, for: user, as: :activity})
    end
  
-   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)
-   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)
  
  
        limit = Config.get([:instance, :limit])
  
-       accounts =
-         Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
+       accounts = Map.put(%{}, user.id, AccountView.render("show.json", %{user: user, for: user}))
  
        initial_state =
          %{
    end
  
    def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
-     info_cng = User.Info.mastodon_settings_update(user.info, settings)
-     with changeset <- Changeset.change(user),
-          changeset <- Changeset.put_embed(changeset, :info, info_cng),
-          {:ok, _user} <- User.update_and_set_cache(changeset) do
+     with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
        json(conn, %{})
      else
        e ->
    @doc "Local Mastodon FE login init action"
    def login(conn, %{"code" => auth_token}) do
      with {:ok, app} <- get_or_make_app(),
-          %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
+          {:ok, auth} <- Authorization.get_by_token(app, auth_token),
           {:ok, token} <- Token.exchange_token(app, auth) do
        conn
        |> put_session(:oauth_token, token.token)
    def login(conn, _) do
      with {:ok, app} <- get_or_make_app() do
        path =
-         o_auth_path(
-           conn,
-           :authorize,
+         o_auth_path(conn, :authorize,
            response_type: "code",
            client_id: app.client_id,
            redirect_uri: ".",
      end
    end
  
+   @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
    defp get_or_make_app do
-     find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
-     scopes = ["read", "write", "follow", "push"]
-     with %App{} = app <- Repo.get_by(App, find_attrs) do
-       {:ok, app} =
-         if app.scopes == scopes do
-           {:ok, app}
-         else
-           app
-           |> Changeset.change(%{scopes: scopes})
-           |> Repo.update()
-         end
-       {:ok, app}
-     else
-       _e ->
-         cs =
-           App.register_changeset(
-             %App{},
-             Map.put(find_attrs, :scopes, scopes)
-           )
-         Repo.insert(cs)
-     end
+     App.get_or_make(
+       %{client_name: @local_mastodon_name, redirect_uris: "."},
+       ["read", "write", "follow", "push"]
+     )
    end
  
    def logout(conn, _) do
      |> redirect(to: "/")
    end
  
-   def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-     Logger.debug("Unimplemented, returning unmodified relationship")
-     with %User{} = target <- User.get_cached_by_id(id) do
-       conn
-       |> put_view(AccountView)
-       |> render("relationship.json", %{user: user, target: target})
-     end
-   end
+   # Stubs for unimplemented mastodon api
+   #
    def empty_array(conn, _) do
      Logger.debug("Unimplemented, returning an empty array")
      json(conn, [])
      json(conn, %{})
    end
  
-   def endorsements(conn, params), do: empty_array(conn, params)
-   def get_filters(%{assigns: %{user: user}} = conn, _) do
-     filters = Filter.get_filters(user)
-     res = FilterView.render("filters.json", filters: filters)
-     json(conn, res)
-   end
-   def create_filter(
-         %{assigns: %{user: user}} = conn,
-         %{"phrase" => phrase, "context" => context} = params
-       ) do
-     query = %Filter{
-       user_id: user.id,
-       phrase: phrase,
-       context: context,
-       hide: Map.get(params, "irreversible", false),
-       whole_word: Map.get(params, "boolean", true)
-       # expires_at
-     }
-     {:ok, response} = Filter.create(query)
-     res = FilterView.render("filter.json", filter: response)
-     json(conn, res)
-   end
-   def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
-     filter = Filter.get(filter_id, user)
-     res = FilterView.render("filter.json", filter: filter)
-     json(conn, res)
-   end
-   def update_filter(
-         %{assigns: %{user: user}} = conn,
-         %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
-       ) do
-     query = %Filter{
-       user_id: user.id,
-       filter_id: filter_id,
-       phrase: phrase,
-       context: context,
-       hide: Map.get(params, "irreversible", nil),
-       whole_word: Map.get(params, "boolean", true)
-       # expires_at
-     }
-     {:ok, response} = Filter.update(query)
-     res = FilterView.render("filter.json", filter: response)
-     json(conn, res)
-   end
-   def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
-     query = %Filter{
-       user_id: user.id,
-       filter_id: filter_id
-     }
-     {:ok, _} = Filter.delete(query)
-     json(conn, %{})
-   end
    def suggestions(%{assigns: %{user: user}} = conn, _) do
      suggestions = Config.get(:suggestions)
  
      end
    end
  
-   def status_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 =
-         StatusView.render(
-           "card.json",
-           Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
-         )
-       json(conn, data)
-     else
-       _e ->
-         %{}
-     end
-   end
-   def create_report(%{assigns: %{user: user}} = conn, params) do
-     case CommonAPI.report(user, params) do
-       {:ok, activity} ->
-         conn
-         |> put_view(ReportView)
-         |> try_render("report.json", %{activity: activity})
-       {:error, err} ->
-         conn
-         |> put_status(:bad_request)
-         |> json(%{error: err})
-     end
-   end
-   def account_register(
-         %{assigns: %{app: app}} = conn,
-         %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
-       ) do
-     params =
-       params
-       |> Map.take([
-         "email",
-         "captcha_solution",
-         "captcha_token",
-         "captcha_answer_data",
-         "token",
-         "password"
-       ])
-       |> Map.put("nickname", nickname)
-       |> Map.put("fullname", params["fullname"] || nickname)
-       |> Map.put("bio", params["bio"] || "")
-       |> Map.put("confirm", params["password"])
-     with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
-          {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
-       json(conn, %{
-         token_type: "Bearer",
-         access_token: token.token,
-         scope: app.scopes,
-         created_at: Token.Utils.format_created_at(token)
-       })
-     else
-       {:error, errors} ->
-         conn
-         |> put_status(:bad_request)
-         |> json(errors)
-     end
-   end
-   def account_register(%{assigns: %{app: _app}} = conn, _params) do
-     render_error(conn, :bad_request, "Missing parameters")
-   end
-   def account_register(conn, _) do
-     render_error(conn, :forbidden, "Invalid credentials")
-   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, for: user})
-       end)
-     conn
-     |> add_link_headers(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, for: user})
-       conn
-       |> json(participation_view)
-     end
-   end
    def password_reset(conn, params) do
      nickname_or_email = params["email"] || params["nickname"]
  
      end
    end
  
-   def account_confirmation_resend(conn, params) do
-     nickname_or_email = params["email"] || params["nickname"]
-     with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
-          {:ok, _} <- User.try_send_confirmation_email(user) do
-       conn
-       |> json_response(:no_content, "")
-     end
-   end
    def try_render(conn, target, params)
        when is_binary(target) do
      case render(conn, target, params) do
index 0000000000000000000000000000000000000000,7e4d7297c2598f0bdc4bac55022fbbfb1c6c0a2e..36c6defc2361edc5e6bebe10a98a84768e01aedc
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,57 +1,67 @@@
+ # 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.NotificationController do
+   use Pleroma.Web, :controller
+   import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
+   alias Pleroma.Notification
++  alias Pleroma.Plugs.OAuthScopesPlug
+   alias Pleroma.Web.MastodonAPI.MastodonAPI
++  @oauth_read_actions [:show, :index]
++
++  plug(
++    OAuthScopesPlug,
++    %{scopes: ["read:notifications"]} when action in @oauth_read_actions
++  )
++
++  plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action not in @oauth_read_actions)
++
+   # GET /api/v1/notifications
+   def index(%{assigns: %{user: user}} = conn, params) do
+     notifications = MastodonAPI.get_notifications(user, params)
+     conn
+     |> add_link_headers(notifications)
+     |> render("index.json", notifications: notifications, for: user)
+   end
+   # GET /api/v1/notifications/:id
+   def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+     with {:ok, notification} <- Notification.get(user, id) do
+       render(conn, "show.json", notification: notification, for: user)
+     else
+       {:error, reason} ->
+         conn
+         |> put_status(:forbidden)
+         |> json(%{"error" => reason})
+     end
+   end
+   # POST /api/v1/notifications/clear
+   def clear(%{assigns: %{user: user}} = conn, _params) do
+     Notification.clear(user)
+     json(conn, %{})
+   end
+   # POST /api/v1/notifications/dismiss
+   def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
+     with {:ok, _notif} <- Notification.dismiss(user, id) do
+       json(conn, %{})
+     else
+       {:error, reason} ->
+         conn
+         |> put_status(:forbidden)
+         |> json(%{"error" => reason})
+     end
+   end
+   # DELETE /api/v1/notifications/destroy_multiple
+   def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
+     Notification.destroy_multiple(user, ids)
+     json(conn, %{})
+   end
+ end
index 0000000000000000000000000000000000000000,1c084b74010cb28faa12fd126017dfd59347afba..313f885a66752ced31294e825adbcbd5535ef8ea
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,16 +1,20 @@@
+ # 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.ReportController do
++  alias Pleroma.Plugs.OAuthScopesPlug
++
+   use Pleroma.Web, :controller
+   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
++  plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
++
+   @doc "POST /api/v1/reports"
+   def create(%{assigns: %{user: user}} = conn, params) do
+     with {:ok, activity} <- Pleroma.Web.CommonAPI.report(user, params) do
+       render(conn, "show.json", activity: activity)
+     end
+   end
+ end
index 0000000000000000000000000000000000000000,0a56b10b6342ac4e82beb13e70a063736d565d7e..ff9276541614f675491ae644e32678aabd63f627
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,51 +1,59 @@@
+ # 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.ScheduledActivityController do
+   use Pleroma.Web, :controller
+   import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
++  alias Pleroma.Plugs.OAuthScopesPlug
+   alias Pleroma.ScheduledActivity
+   alias Pleroma.Web.MastodonAPI.MastodonAPI
+   plug(:assign_scheduled_activity when action != :index)
++  @oauth_read_actions [:show, :index]
++
++  plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in @oauth_read_actions)
++  plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action not in @oauth_read_actions)
++
++  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
++
+   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+   @doc "GET /api/v1/scheduled_statuses"
+   def index(%{assigns: %{user: user}} = conn, params) do
+     with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
+       conn
+       |> add_link_headers(scheduled_activities)
+       |> render("index.json", scheduled_activities: scheduled_activities)
+     end
+   end
+   @doc "GET /api/v1/scheduled_statuses/:id"
+   def show(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, _params) do
+     render(conn, "show.json", scheduled_activity: scheduled_activity)
+   end
+   @doc "PUT /api/v1/scheduled_statuses/:id"
+   def update(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, params) do
+     with {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
+       render(conn, "show.json", scheduled_activity: scheduled_activity)
+     end
+   end
+   @doc "DELETE /api/v1/scheduled_statuses/:id"
+   def delete(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, _params) do
+     with {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
+       render(conn, "show.json", scheduled_activity: scheduled_activity)
+     end
+   end
+   defp assign_scheduled_activity(%{assigns: %{user: user}, params: %{"id" => id}} = conn, _) do
+     case ScheduledActivity.get(user, id) do
+       %ScheduledActivity{} = activity -> assign(conn, :scheduled_activity, activity)
+       nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
+     end
+   end
+ end
index f49ca89edfe603fed6f031178a1e05c7d833a983,3fc89d645b8fb76413c9126c88c45ace6218e820..9f39b00f83af3684b822d8137169e992876d4025
@@@ -6,7 -6,6 +6,7 @@@ defmodule Pleroma.Web.MastodonAPI.Searc
    use Pleroma.Web, :controller
  
    alias Pleroma.Activity
 +  alias Pleroma.Plugs.OAuthScopesPlug
    alias Pleroma.Plugs.RateLimiter
    alias Pleroma.Repo
    alias Pleroma.User
    alias Pleroma.Web.MastodonAPI.StatusView
  
    require Logger
 +
 +  # Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search)
 +  plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated})
 +
    plug(RateLimiter, :search when action in [:search, :search2, :account_search])
  
    def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
      accounts = User.search(query, search_options(params, user))
-     res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
  
-     json(conn, res)
+     conn
+     |> put_view(AccountView)
+     |> render("index.json", users: accounts, for: user, as: :user)
    end
  
    def search2(conn, params), do: do_search(:v2, conn, params)
@@@ -76,7 -72,7 +77,7 @@@
  
    defp resource_search(_, "accounts", query, options) do
      accounts = with_fallback(fn -> User.search(query, options) end)
-     AccountView.render("accounts.json", users: accounts, for: options[:for_user], as: :user)
+     AccountView.render("index.json", users: accounts, for: options[:for_user], as: :user)
    end
  
    defp resource_search(_, "statuses", query, options) do
index 0000000000000000000000000000000000000000,3c6987a5ff6a7a0b1233cc28c2ecd515b6899bc4..ee9047d1c9977e912738e06eda9b14d363ba8a1e
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,274 +1,325 @@@
+ # 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.OAuthScopesPlug
+   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
++  @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
++
++  plug(
++    OAuthScopesPlug,
++    %{@unauthenticated_access | scopes: ["read:statuses"]}
++    when action in [
++           :index,
++           :show,
++           :card,
++           :context
++         ]
++  )
++
++  plug(
++    OAuthScopesPlug,
++    %{scopes: ["write:statuses"]}
++    when action in [
++           :create,
++           :delete,
++           :reblog,
++           :unreblog
++         ]
++  )
++
++  plug(
++    OAuthScopesPlug,
++    %{scopes: ["write:favourites"]} when action in [:favourite, :unfavourite]
++  )
++
++  plug(
++    OAuthScopesPlug,
++    %{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
++  )
++
++  plug(
++    OAuthScopesPlug,
++    %{@unauthenticated_access | scopes: ["read:accounts"]}
++    when action in [:favourited_by, :reblogged_by]
++  )
++
++  plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin])
++
++  # Note: scope not present in Mastodon: write:bookmarks
++  plug(
++    OAuthScopesPlug,
++    %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
++  )
++
++  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
++
+   @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("index.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("index.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 0000000000000000000000000000000000000000,bb8b0eb328bad899415df0d3fcb3ced0d19c8bbd..9f086a8c2f40d31a2f22acab8ada32dc97c585a0
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,136 +1,142 @@@
+ # 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.TimelineController do
+   use Pleroma.Web, :controller
+   import Pleroma.Web.ControllerHelper,
+     only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1]
+   alias Pleroma.Pagination
++  alias Pleroma.Plugs.OAuthScopesPlug
+   alias Pleroma.Web.ActivityPub.ActivityPub
++  plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct])
++  plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :list)
++
++  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
++
+   plug(:put_view, Pleroma.Web.MastodonAPI.StatusView)
+   # GET /api/v1/timelines/home
+   def home(%{assigns: %{user: user}} = conn, params) do
+     params =
+       params
+       |> Map.put("type", ["Create", "Announce"])
+       |> Map.put("blocking_user", user)
+       |> Map.put("muting_user", user)
+       |> Map.put("user", user)
+     recipients = [user.ap_id | user.following]
+     activities =
+       recipients
+       |> ActivityPub.fetch_activities(params)
+       |> Enum.reverse()
+     conn
+     |> add_link_headers(activities)
+     |> render("index.json", activities: activities, for: user, as: :activity)
+   end
+   # GET /api/v1/timelines/direct
+   def direct(%{assigns: %{user: user}} = conn, params) do
+     params =
+       params
+       |> Map.put("type", "Create")
+       |> Map.put("blocking_user", user)
+       |> Map.put("user", user)
+       |> Map.put(:visibility, "direct")
+     activities =
+       [user.ap_id]
+       |> ActivityPub.fetch_activities_query(params)
+       |> Pagination.fetch_paginated(params)
+     conn
+     |> add_link_headers(activities)
+     |> render("index.json", activities: activities, for: user, as: :activity)
+   end
+   # GET /api/v1/timelines/public
+   def public(%{assigns: %{user: user}} = conn, params) do
+     local_only = truthy_param?(params["local"])
+     activities =
+       params
+       |> Map.put("type", ["Create", "Announce"])
+       |> Map.put("local_only", local_only)
+       |> Map.put("blocking_user", user)
+       |> Map.put("muting_user", user)
+       |> ActivityPub.fetch_public_activities()
+       |> Enum.reverse()
+     conn
+     |> add_link_headers(activities, %{"local" => local_only})
+     |> render("index.json", activities: activities, for: user, as: :activity)
+   end
+   # GET /api/v1/timelines/tag/:tag
+   def hashtag(%{assigns: %{user: user}} = conn, params) do
+     local_only = truthy_param?(params["local"])
+     tags =
+       [params["tag"], params["any"]]
+       |> List.flatten()
+       |> Enum.uniq()
+       |> Enum.filter(& &1)
+       |> Enum.map(&String.downcase(&1))
+     tag_all =
+       params
+       |> Map.get("all", [])
+       |> Enum.map(&String.downcase(&1))
+     tag_reject =
+       params
+       |> Map.get("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})
+     |> render("index.json", activities: activities, for: user, as: :activity)
+   end
+   # GET /api/v1/timelines/list/:list_id
+   def list(%{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()
+       render(conn, "index.json", activities: activities, for: user, as: :activity)
+     else
+       _e -> render_error(conn, :forbidden, "Error.")
+     end
+   end
+ end
index 130ec78959055eac988225e77c1572dfb01334fa,a57670e025f734aa6d6db3ec2cb62cd14a773472..2d7b10e731130961d2b4c27caec0f53a90683edd
@@@ -202,6 -202,8 +202,8 @@@ defmodule Pleroma.Web.OAuth.OAuthContro
           {:ok, app} <- Token.Utils.fetch_app(conn),
           {:auth_active, true} <- {:auth_active, User.auth_active?(user)},
           {:user_active, true} <- {:user_active, !user.info.deactivated},
+          {:password_reset_pending, false} <-
+            {:password_reset_pending, user.info.password_reset_pending},
           {:ok, scopes} <- validate_scopes(app, params),
           {:ok, auth} <- Authorization.create_authorization(app, user, scopes),
           {:ok, token} <- Token.exchange_token(app, auth) do
        {:user_active, false} ->
          render_error(conn, :forbidden, "Your account is currently disabled")
  
+       {:password_reset_pending, true} ->
+         render_error(conn, :forbidden, "Password reset is required")
        _error ->
          render_invalid_credentials_error(conn)
      end
    defp validate_scopes(app, params) do
      params
      |> Scopes.fetch_scopes(app.scopes)
 -    |> Scopes.validates(app.scopes)
 +    |> Scopes.validate(app.scopes)
    end
  
    def default_redirect_uri(%App{} = app) do
index 0000000000000000000000000000000000000000,63c44086c4c5ef21967f0f23df26c216da328580..9012e2175e5ef7231e626b4c3f560d22fd761b45
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,143 +1,168 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
+ defmodule Pleroma.Web.PleromaAPI.AccountController do
+   use Pleroma.Web, :controller
+   import Pleroma.Web.ControllerHelper,
+     only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2]
+   alias Ecto.Changeset
++  alias Pleroma.Plugs.OAuthScopesPlug
+   alias Pleroma.Plugs.RateLimiter
+   alias Pleroma.User
+   alias Pleroma.Web.ActivityPub.ActivityPub
+   alias Pleroma.Web.CommonAPI
+   alias Pleroma.Web.MastodonAPI.StatusView
+   require Pleroma.Constants
++  plug(
++    OAuthScopesPlug,
++    %{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe]
++  )
++
++  plug(
++    OAuthScopesPlug,
++    %{scopes: ["write:accounts"]}
++    # Note: the following actions are not permission-secured in Mastodon:
++    when action in [
++           :update_avatar,
++           :update_banner,
++           :update_background
++         ]
++  )
++
++  plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
++
++  # An extra safety measure for possible actions not guarded by OAuth permissions specification
++  plug(
++    Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
++    when action != :confirmation_resend
++  )
++
+   plug(RateLimiter, :account_confirmation_resend when action == :confirmation_resend)
+   plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe])
+   plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
+   @doc "POST /api/v1/pleroma/accounts/confirmation_resend"
+   def confirmation_resend(conn, params) do
+     nickname_or_email = params["email"] || params["nickname"]
+     with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
+          {:ok, _} <- User.try_send_confirmation_email(user) do
+       json_response(conn, :no_content, "")
+     end
+   end
+   @doc "PATCH /api/v1/pleroma/accounts/update_avatar"
+   def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
+     {:ok, user} =
+       user
+       |> Changeset.change(%{avatar: nil})
+       |> User.update_and_set_cache()
+     CommonAPI.update(user)
+     json(conn, %{url: nil})
+   end
+   def update_avatar(%{assigns: %{user: user}} = conn, params) do
+     {:ok, %{data: data}} = ActivityPub.upload(params, type: :avatar)
+     {:ok, user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache()
+     %{"url" => [%{"href" => href} | _]} = data
+     CommonAPI.update(user)
+     json(conn, %{url: href})
+   end
+   @doc "PATCH /api/v1/pleroma/accounts/update_banner"
+   def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
+     new_info = %{"banner" => %{}}
+     with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
+       CommonAPI.update(user)
+       json(conn, %{url: nil})
+     end
+   end
+   def update_banner(%{assigns: %{user: user}} = conn, params) do
+     with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
+          new_info <- %{"banner" => object.data},
+          {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
+       CommonAPI.update(user)
+       %{"url" => [%{"href" => href} | _]} = object.data
+       json(conn, %{url: href})
+     end
+   end
+   @doc "PATCH /api/v1/pleroma/accounts/update_background"
+   def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
+     new_info = %{"background" => %{}}
+     with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
+       json(conn, %{url: nil})
+     end
+   end
+   def update_background(%{assigns: %{user: user}} = conn, params) do
+     with {:ok, object} <- ActivityPub.upload(params, type: :background),
+          new_info <- %{"background" => object.data},
+          {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
+       %{"url" => [%{"href" => href} | _]} = object.data
+       json(conn, %{url: href})
+     end
+   end
+   @doc "GET /api/v1/pleroma/accounts/:id/favourites"
+   def favourites(%{assigns: %{account: %{info: %{hide_favorites: true}}}} = conn, _params) do
+     render_error(conn, :forbidden, "Can't get favorites")
+   end
+   def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) 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
+         [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
+       else
+         [Pleroma.Constants.as_public()]
+       end
+     activities =
+       recipients
+       |> ActivityPub.fetch_activities(params)
+       |> Enum.reverse()
+     conn
+     |> add_link_headers(activities)
+     |> put_view(StatusView)
+     |> render("index.json", activities: activities, for: for_user, as: :activity)
+   end
+   @doc "POST /api/v1/pleroma/accounts/:id/subscribe"
+   def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do
+     with {:ok, subscription_target} <- User.subscribe(user, subscription_target) do
+       render(conn, "relationship.json", user: user, target: subscription_target)
+     else
+       {:error, message} -> json_response(conn, :forbidden, %{error: message})
+     end
+   end
+   @doc "POST /api/v1/pleroma/accounts/:id/unsubscribe"
+   def unsubscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do
+     with {:ok, subscription_target} <- User.unsubscribe(user, subscription_target) do
+       render(conn, "relationship.json", user: user, target: subscription_target)
+     else
+       {:error, message} -> json_response(conn, :forbidden, %{error: message})
+     end
+   end
+ end
index 0000000000000000000000000000000000000000,7f6a76c0e24b56906f30eec440e5256af7db02a7..d71d72dd5a1b6da7e3e3248cae09f4ce77924338
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,35 +1,41 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
+ defmodule Pleroma.Web.PleromaAPI.MascotController do
+   use Pleroma.Web, :controller
++  alias Pleroma.Plugs.OAuthScopesPlug
+   alias Pleroma.User
+   alias Pleroma.Web.ActivityPub.ActivityPub
++  plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show)
++  plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show)
++
++  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
++
+   @doc "GET /api/v1/pleroma/mascot"
+   def show(%{assigns: %{user: user}} = conn, _params) do
+     json(conn, User.get_mascot(user))
+   end
+   @doc "PUT /api/v1/pleroma/mascot"
+   def update(%{assigns: %{user: user}} = conn, %{"file" => file}) do
+     with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
+          # Reject if not an image
+          %{type: "image"} = attachment <- render_attachment(object) do
+       # Sure!
+       # Save to the user's info
+       {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, attachment))
+       json(conn, attachment)
+     else
+       %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
+     end
+   end
+   defp render_attachment(object) do
+     attachment_data = Map.put(object.data, "id", object.id)
+     Pleroma.Web.MastodonAPI.StatusView.render("attachment.json", %{attachment: attachment_data})
+   end
+ end
index f3dc4616cd7a05b8bd193ff4b28639c7380b5d66,d17ccf84d0778ff5de3972f365a8a5df12e12f76..9d50a7ca996c72f61061fa2e708917d1877eb8f6
@@@ -9,24 -9,11 +9,26 @@@ defmodule Pleroma.Web.PleromaAPI.Plerom
  
    alias Pleroma.Conversation.Participation
    alias Pleroma.Notification
 +  alias Pleroma.Plugs.OAuthScopesPlug
    alias Pleroma.Web.ActivityPub.ActivityPub
    alias Pleroma.Web.MastodonAPI.ConversationView
    alias Pleroma.Web.MastodonAPI.NotificationView
    alias Pleroma.Web.MastodonAPI.StatusView
  
-     %{scopes: ["write:conversations"]} when action in [:conversations, :conversation_read]
 +  plug(
 +    OAuthScopesPlug,
 +    %{scopes: ["read:statuses"]} when action in [:conversation, :conversation_statuses]
 +  )
 +
 +  plug(
 +    OAuthScopesPlug,
++    %{scopes: ["write:conversations"]} when action == :update_conversation
 +  )
 +
 +  plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :read_notification)
 +
++  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
++
    def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
      with %Participation{} = participation <- Participation.get(participation_id),
           true <- user.id == participation.user_id do
index 0000000000000000000000000000000000000000,0fb978c5dee4c5bd795d7f0c053426dabf84b49e..b74b3debc0aeb18eb907df76deb94b975b41287c
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,52 +1,58 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
+ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do
+   use Pleroma.Web, :controller
+   import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, fetch_integer_param: 2]
++  alias Pleroma.Plugs.OAuthScopesPlug
+   alias Pleroma.User
+   alias Pleroma.Web.ActivityPub.ActivityPub
+   alias Pleroma.Web.CommonAPI
+   alias Pleroma.Web.MastodonAPI.StatusView
++  plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :user_scrobbles)
++  plug(OAuthScopesPlug, %{scopes: ["write"]} when action != :user_scrobbles)
++
++  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
++
+   def new_scrobble(%{assigns: %{user: user}} = conn, %{"title" => _} = params) do
+     params =
+       if !params["length"] do
+         params
+       else
+         params
+         |> Map.put("length", fetch_integer_param(params, "length"))
+       end
+     with {:ok, activity} <- CommonAPI.listen(user, params) do
+       conn
+       |> put_view(StatusView)
+       |> render("listen.json", %{activity: activity, for: user})
+     else
+       {:error, message} ->
+         conn
+         |> put_status(:bad_request)
+         |> json(%{"error" => message})
+     end
+   end
+   def user_scrobbles(%{assigns: %{user: reading_user}} = conn, params) do
+     with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
+       params = Map.put(params, "type", ["Listen"])
+       activities = ActivityPub.fetch_user_abstract_activities(user, reading_user, params)
+       conn
+       |> add_link_headers(activities)
+       |> put_view(StatusView)
+       |> render("listens.json", %{
+         activities: activities,
+         for: reading_user,
+         as: :activity
+       })
+     end
+   end
+ end
index 3002d0738d4d5e74e043b15d54846c5002ad3ccf,eab55a27c09d06b2c5c2def59640773573bf84d6..cc6bcfa1a368a66e94da63094460758137c204c6
@@@ -87,6 -87,31 +87,6 @@@ defmodule Pleroma.Web.Router d
      plug(Pleroma.Plugs.EnsureUserKeyPlug)
    end
  
 -  pipeline :oauth_read_or_public do
 -    plug(Pleroma.Plugs.OAuthScopesPlug, %{
 -      scopes: ["read"],
 -      fallback: :proceed_unauthenticated
 -    })
 -
 -    plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
 -  end
 -
 -  pipeline :oauth_read do
 -    plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["read"]})
 -  end
 -
 -  pipeline :oauth_write do
 -    plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["write"]})
 -  end
 -
 -  pipeline :oauth_follow do
 -    plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["follow"]})
 -  end
 -
 -  pipeline :oauth_push do
 -    plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]})
 -  end
 -
    pipeline :well_known do
      plug(:accepts, ["json", "jrd+json", "xml", "xrd+xml"])
    end
    end
  
    scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do
 -    pipe_through([:admin_api, :oauth_write])
 +    pipe_through(:admin_api)
  
      post("/users/follow", AdminAPIController, :user_follow)
      post("/users/unfollow", AdminAPIController, :user_unfollow)
      post("/users/email_invite", AdminAPIController, :email_invite)
  
      get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
+     patch("/users/:nickname/force_password_reset", AdminAPIController, :force_password_reset)
  
      get("/users", AdminAPIController, :list_users)
      get("/users/:nickname", AdminAPIController, :user_show)
      get("/config/migrate_from_db", AdminAPIController, :migrate_from_db)
  
      get("/moderation_log", AdminAPIController, :list_log)
 -      pipe_through([:admin_api, :oauth_write])
+     post("/reload_emoji", AdminAPIController, :reload_emoji)
+   end
+   scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
+     scope "/packs" do
+       # Modifying packs
++      pipe_through(:admin_api)
+       post("/import_from_fs", EmojiAPIController, :import_from_fs)
+       post("/:pack_name/update_file", EmojiAPIController, :update_file)
+       post("/:pack_name/update_metadata", EmojiAPIController, :update_metadata)
+       put("/:name", EmojiAPIController, :create)
+       delete("/:name", EmojiAPIController, :delete)
+       post("/download_from", EmojiAPIController, :download_from)
+       post("/list_from", EmojiAPIController, :list_from)
+     end
+     scope "/packs" do
+       # Pack info / downloading
+       get("/", EmojiAPIController, :list_packs)
+       get("/:name/download_shared/", EmojiAPIController, :download_shared)
+     end
    end
  
    scope "/", Pleroma.Web.TwitterAPI do
  
      post("/main/ostatus", UtilController, :remote_subscribe)
      get("/ostatus_subscribe", UtilController, :remote_follow)
 -
 -    scope [] do
 -      pipe_through(:oauth_follow)
 -      post("/ostatus_subscribe", UtilController, :do_remote_follow)
 -    end
 +    post("/ostatus_subscribe", UtilController, :do_remote_follow)
    end
  
    scope "/api/pleroma", Pleroma.Web.TwitterAPI do
      pipe_through(:authenticated_api)
  
 -    scope [] do
 -      pipe_through(:oauth_write)
 -
 -      post("/change_email", UtilController, :change_email)
 -      post("/change_password", UtilController, :change_password)
 -      post("/delete_account", UtilController, :delete_account)
 -      put("/notification_settings", UtilController, :update_notificaton_settings)
 -      post("/disable_account", UtilController, :disable_account)
 -    end
 -
 -    scope [] do
 -      pipe_through(:oauth_follow)
 +    post("/change_email", UtilController, :change_email)
 +    post("/change_password", UtilController, :change_password)
 +    post("/delete_account", UtilController, :delete_account)
 +    put("/notification_settings", UtilController, :update_notificaton_settings)
 +    post("/disable_account", UtilController, :disable_account)
  
 -      post("/blocks_import", UtilController, :blocks_import)
 -      post("/follow_import", UtilController, :follow_import)
 -    end
 +    post("/blocks_import", UtilController, :blocks_import)
 +    post("/follow_import", UtilController, :follow_import)
    end
  
    scope "/oauth", Pleroma.Web.OAuth do
    end
  
    scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
-     pipe_through(:authenticated_api)
+     scope [] do
+       pipe_through(:authenticated_api)
 -      pipe_through(:oauth_read)
++
+       get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
+       get("/conversations/:id", PleromaAPIController, :conversation)
+     end
+     scope [] do
+       pipe_through(:authenticated_api)
 -      pipe_through(:oauth_write)
++
+       patch("/conversations/:id", PleromaAPIController, :update_conversation)
+       post("/notifications/read", PleromaAPIController, :read_notification)
+       patch("/accounts/update_avatar", AccountController, :update_avatar)
+       patch("/accounts/update_banner", AccountController, :update_banner)
+       patch("/accounts/update_background", AccountController, :update_background)
+       get("/mascot", MascotController, :show)
+       put("/mascot", MascotController, :update)
+       post("/scrobble", ScrobbleController, :new_scrobble)
+     end
+     scope [] do
+       pipe_through(:api)
 -      pipe_through(:oauth_read_or_public)
+       get("/accounts/:id/favourites", AccountController, :favourites)
+     end
+     scope [] do
+       pipe_through(:authenticated_api)
 -      pipe_through(:oauth_follow)
  
-     get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
-     get("/conversations/:id", PleromaAPIController, :conversation)
+       post("/accounts/:id/subscribe", AccountController, :subscribe)
+       post("/accounts/:id/unsubscribe", AccountController, :unsubscribe)
+     end
  
-     patch("/conversations/:id", PleromaAPIController, :update_conversation)
-     post("/notifications/read", PleromaAPIController, :read_notification)
+     post("/accounts/confirmation_resend", AccountController, :confirmation_resend)
+   end
+   scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
 -    pipe_through([:api, :oauth_read_or_public])
++    pipe_through(:api)
+     get("/accounts/:id/scrobbles", ScrobbleController, :user_scrobbles)
    end
  
    scope "/api/v1", Pleroma.Web.MastodonAPI do
      pipe_through(:authenticated_api)
  
-     get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials)
 -    scope [] do
 -      pipe_through(:oauth_read)
++    get("/accounts/verify_credentials", AccountController, :verify_credentials)
  
-     get("/accounts/relationships", MastodonAPIController, :relationships)
 -      get("/accounts/verify_credentials", AccountController, :verify_credentials)
++    get("/accounts/relationships", AccountController, :relationships)
  
-     get("/accounts/:id/lists", MastodonAPIController, :account_lists)
 -      get("/accounts/relationships", AccountController, :relationships)
++    get("/accounts/:id/lists", AccountController, :lists)
 +    get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array)
  
-     get("/follow_requests", MastodonAPIController, :follow_requests)
 -      get("/accounts/:id/lists", AccountController, :lists)
 -      get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array)
++    get("/follow_requests", FollowRequestController, :index)
 +    get("/blocks", MastodonAPIController, :blocks)
 +    get("/mutes", MastodonAPIController, :mutes)
  
-     get("/timelines/home", MastodonAPIController, :home_timeline)
-     get("/timelines/direct", MastodonAPIController, :dm_timeline)
 -      get("/follow_requests", FollowRequestController, :index)
 -      get("/blocks", MastodonAPIController, :blocks)
 -      get("/mutes", MastodonAPIController, :mutes)
++    get("/timelines/home", TimelineController, :home)
++    get("/timelines/direct", TimelineController, :direct)
  
 -      get("/timelines/home", TimelineController, :home)
 -      get("/timelines/direct", TimelineController, :direct)
 +    get("/favourites", MastodonAPIController, :favourites)
-     # Note: not present in Mastodon: bookmarks
 +    get("/bookmarks", MastodonAPIController, :bookmarks)
  
-     post("/notifications/clear", MastodonAPIController, :clear_notifications)
-     post("/notifications/dismiss", MastodonAPIController, :dismiss_notification)
-     get("/notifications", MastodonAPIController, :notifications)
-     get("/notifications/:id", MastodonAPIController, :get_notification)
 -      get("/favourites", MastodonAPIController, :favourites)
 -      get("/bookmarks", MastodonAPIController, :bookmarks)
++    get("/notifications", NotificationController, :index)
++    get("/notifications/:id", NotificationController, :show)
++    post("/notifications/clear", NotificationController, :clear)
++    post("/notifications/dismiss", NotificationController, :dismiss)
++    delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
  
-     delete(
-       "/notifications/destroy_multiple",
-       MastodonAPIController,
-       :destroy_multiple_notifications
-     )
-     get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses)
-     get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status)
 -      get("/notifications", NotificationController, :index)
 -      get("/notifications/:id", NotificationController, :show)
 -      post("/notifications/clear", NotificationController, :clear)
 -      post("/notifications/dismiss", NotificationController, :dismiss)
 -      delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
++    get("/scheduled_statuses", ScheduledActivityController, :index)
++    get("/scheduled_statuses/:id", ScheduledActivityController, :show)
  
 -      get("/scheduled_statuses", ScheduledActivityController, :index)
 -      get("/scheduled_statuses/:id", ScheduledActivityController, :show)
 +    get("/lists", ListController, :index)
 +    get("/lists/:id", ListController, :show)
 +    get("/lists/:id/accounts", ListController, :list_accounts)
  
-     get("/domain_blocks", MastodonAPIController, :domain_blocks)
 -      get("/lists", ListController, :index)
 -      get("/lists/:id", ListController, :show)
 -      get("/lists/:id/accounts", ListController, :list_accounts)
++    get("/domain_blocks", DomainBlockController, :index)
  
-     get("/filters", MastodonAPIController, :get_filters)
 -      get("/domain_blocks", DomainBlockController, :index)
++    get("/filters", FilterController, :index)
  
 -      get("/filters", FilterController, :index)
 +    get("/suggestions", MastodonAPIController, :suggestions)
  
-     get("/conversations", MastodonAPIController, :conversations)
-     post("/conversations/:id/read", MastodonAPIController, :conversation_read)
 -      get("/suggestions", MastodonAPIController, :suggestions)
++    get("/conversations", ConversationController, :index)
++    post("/conversations/:id/read", ConversationController, :read)
  
-     get("/endorsements", MastodonAPIController, :endorsements)
 -      get("/conversations", ConversationController, :index)
 -      post("/conversations/:id/read", ConversationController, :read)
++    get("/endorsements", AccountController, :endorsements)
  
-     patch("/accounts/update_credentials", MastodonAPIController, :update_credentials)
 -      get("/endorsements", MastodonAPIController, :empty_array)
 -    end
++    patch("/accounts/update_credentials", AccountController, :update_credentials)
  
-     post("/statuses", MastodonAPIController, :post_status)
-     delete("/statuses/:id", MastodonAPIController, :delete_status)
 -    scope [] do
 -      pipe_through(:oauth_write)
 -
 -      patch("/accounts/update_credentials", AccountController, :update_credentials)
++    post("/statuses", StatusController, :create)
++    delete("/statuses/:id", StatusController, :delete)
  
-     post("/statuses/:id/reblog", MastodonAPIController, :reblog_status)
-     post("/statuses/:id/unreblog", MastodonAPIController, :unreblog_status)
-     post("/statuses/:id/favourite", MastodonAPIController, :fav_status)
-     post("/statuses/:id/unfavourite", MastodonAPIController, :unfav_status)
-     post("/statuses/:id/pin", MastodonAPIController, :pin_status)
-     post("/statuses/:id/unpin", MastodonAPIController, :unpin_status)
-     # Note: not present in Mastodon: bookmark
-     post("/statuses/:id/bookmark", MastodonAPIController, :bookmark_status)
-     # Note: not present in Mastodon: unbookmark
-     post("/statuses/:id/unbookmark", MastodonAPIController, :unbookmark_status)
-     post("/statuses/:id/mute", MastodonAPIController, :mute_conversation)
-     post("/statuses/:id/unmute", MastodonAPIController, :unmute_conversation)
 -      post("/statuses", StatusController, :create)
 -      delete("/statuses/:id", StatusController, :delete)
++    post("/statuses/:id/reblog", StatusController, :reblog)
++    post("/statuses/:id/unreblog", StatusController, :unreblog)
++    post("/statuses/:id/favourite", StatusController, :favourite)
++    post("/statuses/:id/unfavourite", StatusController, :unfavourite)
++    post("/statuses/:id/pin", StatusController, :pin)
++    post("/statuses/:id/unpin", StatusController, :unpin)
++    post("/statuses/:id/bookmark", StatusController, :bookmark)
++    post("/statuses/:id/unbookmark", StatusController, :unbookmark)
++    post("/statuses/:id/mute", StatusController, :mute_conversation)
++    post("/statuses/:id/unmute", StatusController, :unmute_conversation)
  
-     put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status)
-     delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status)
 -      post("/statuses/:id/reblog", StatusController, :reblog)
 -      post("/statuses/:id/unreblog", StatusController, :unreblog)
 -      post("/statuses/:id/favourite", StatusController, :favourite)
 -      post("/statuses/:id/unfavourite", StatusController, :unfavourite)
 -      post("/statuses/:id/pin", StatusController, :pin)
 -      post("/statuses/:id/unpin", StatusController, :unpin)
 -      post("/statuses/:id/bookmark", StatusController, :bookmark)
 -      post("/statuses/:id/unbookmark", StatusController, :unbookmark)
 -      post("/statuses/:id/mute", StatusController, :mute_conversation)
 -      post("/statuses/:id/unmute", StatusController, :unmute_conversation)
++    put("/scheduled_statuses/:id", ScheduledActivityController, :update)
++    delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
  
 -      put("/scheduled_statuses/:id", ScheduledActivityController, :update)
 -      delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
 +    post("/polls/:id/votes", MastodonAPIController, :poll_vote)
  
 -      post("/polls/:id/votes", MastodonAPIController, :poll_vote)
 +    post("/media", MastodonAPIController, :upload)
 +    put("/media/:id", MastodonAPIController, :update_media)
  
 -      post("/media", MastodonAPIController, :upload)
 -      put("/media/:id", MastodonAPIController, :update_media)
 +    delete("/lists/:id", ListController, :delete)
 +    post("/lists", ListController, :create)
 +    put("/lists/:id", ListController, :update)
  
 -      delete("/lists/:id", ListController, :delete)
 -      post("/lists", ListController, :create)
 -      put("/lists/:id", ListController, :update)
 +    post("/lists/:id/accounts", ListController, :add_to_list)
 +    delete("/lists/:id/accounts", ListController, :remove_from_list)
  
-     post("/filters", MastodonAPIController, :create_filter)
-     get("/filters/:id", MastodonAPIController, :get_filter)
-     put("/filters/:id", MastodonAPIController, :update_filter)
-     delete("/filters/:id", MastodonAPIController, :delete_filter)
-     patch("/pleroma/accounts/update_avatar", MastodonAPIController, :update_avatar)
-     patch("/pleroma/accounts/update_banner", MastodonAPIController, :update_banner)
-     patch("/pleroma/accounts/update_background", MastodonAPIController, :update_background)
-     get("/pleroma/mascot", MastodonAPIController, :get_mascot)
-     put("/pleroma/mascot", MastodonAPIController, :set_mascot)
 -      post("/lists/:id/accounts", ListController, :add_to_list)
 -      delete("/lists/:id/accounts", ListController, :remove_from_list)
++    post("/filters", FilterController, :create)
++    get("/filters/:id", FilterController, :show)
++    put("/filters/:id", FilterController, :update)
++    delete("/filters/:id", FilterController, :delete)
  
-     post("/reports", MastodonAPIController, :create_report)
 -      post("/filters", FilterController, :create)
 -      get("/filters/:id", FilterController, :show)
 -      put("/filters/:id", FilterController, :update)
 -      delete("/filters/:id", FilterController, :delete)
++    post("/reports", ReportController, :create)
  
-     post("/follows", MastodonAPIController, :follow)
-     post("/accounts/:id/follow", MastodonAPIController, :follow)
 -      post("/reports", ReportController, :create)
 -    end
++    # To do: POST /api/v1/follows is not present in Mastodon - consider removing
++    post("/follows", MastodonAPIController, :follows)
  
-     post("/accounts/:id/unfollow", MastodonAPIController, :unfollow)
-     post("/accounts/:id/block", MastodonAPIController, :block)
-     post("/accounts/:id/unblock", MastodonAPIController, :unblock)
-     post("/accounts/:id/mute", MastodonAPIController, :mute)
-     post("/accounts/:id/unmute", MastodonAPIController, :unmute)
 -    scope [] do
 -      pipe_through(:oauth_follow)
++    post("/accounts/:id/follow", AccountController, :follow)
++    post("/accounts/:id/unfollow", AccountController, :unfollow)
++    post("/accounts/:id/block", AccountController, :block)
++    post("/accounts/:id/unblock", AccountController, :unblock)
++    post("/accounts/:id/mute", AccountController, :mute)
++    post("/accounts/:id/unmute", AccountController, :unmute)
  
-     post("/follow_requests/:id/authorize", MastodonAPIController, :authorize_follow_request)
-     post("/follow_requests/:id/reject", MastodonAPIController, :reject_follow_request)
 -      post("/follows", MastodonAPIController, :follows)
 -      post("/accounts/:id/follow", AccountController, :follow)
 -      post("/accounts/:id/unfollow", AccountController, :unfollow)
 -      post("/accounts/:id/block", AccountController, :block)
 -      post("/accounts/:id/unblock", AccountController, :unblock)
 -      post("/accounts/:id/mute", AccountController, :mute)
 -      post("/accounts/:id/unmute", AccountController, :unmute)
++    post("/follow_requests/:id/authorize", FollowRequestController, :authorize)
++    post("/follow_requests/:id/reject", FollowRequestController, :reject)
  
-     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)
 -      post("/follow_requests/:id/authorize", FollowRequestController, :authorize)
 -      post("/follow_requests/:id/reject", FollowRequestController, :reject)
++    post("/domain_blocks", DomainBlockController, :create)
++    delete("/domain_blocks", DomainBlockController, :delete)
  
 -      post("/domain_blocks", DomainBlockController, :create)
 -      delete("/domain_blocks", DomainBlockController, :delete)
 -    end
 -
 -    scope [] do
 -      pipe_through(:oauth_push)
 -
 -      post("/push/subscription", SubscriptionController, :create)
 -      get("/push/subscription", SubscriptionController, :get)
 -      put("/push/subscription", SubscriptionController, :update)
 -      delete("/push/subscription", SubscriptionController, :delete)
 -    end
 +    post("/push/subscription", SubscriptionController, :create)
 +    get("/push/subscription", SubscriptionController, :get)
 +    put("/push/subscription", SubscriptionController, :update)
 +    delete("/push/subscription", SubscriptionController, :delete)
    end
  
    scope "/api/web", Pleroma.Web.MastodonAPI do
 -    pipe_through([:authenticated_api, :oauth_write])
 +    pipe_through(:authenticated_api)
  
      put("/settings", MastodonAPIController, :put_settings)
    end
    scope "/api/v1", Pleroma.Web.MastodonAPI do
      pipe_through(:api)
  
-     post("/accounts", MastodonAPIController, :account_register)
+     post("/accounts", AccountController, :create)
  
      get("/instance", MastodonAPIController, :masto_instance)
      get("/instance/peers", MastodonAPIController, :peers)
      get("/apps/verify_credentials", MastodonAPIController, :verify_app_credentials)
      get("/custom_emojis", MastodonAPIController, :custom_emojis)
  
-     get("/statuses/:id/card", MastodonAPIController, :status_card)
+     get("/statuses/:id/card", StatusController, :card)
  
-     get("/statuses/:id/favourited_by", MastodonAPIController, :favourited_by)
-     get("/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by)
+     get("/statuses/:id/favourited_by", StatusController, :favourited_by)
+     get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
  
      get("/trends", MastodonAPIController, :empty_array)
  
      get("/accounts/search", SearchController, :account_search)
  
-     post(
-       "/pleroma/accounts/confirmation_resend",
-       MastodonAPIController,
-       :account_confirmation_resend
-     )
 -    scope [] do
 -      pipe_through(:oauth_read_or_public)
--
-     get("/timelines/public", MastodonAPIController, :public_timeline)
-     get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline)
-     get("/timelines/list/:list_id", MastodonAPIController, :list_timeline)
 -      get("/timelines/public", TimelineController, :public)
 -      get("/timelines/tag/:tag", TimelineController, :hashtag)
 -      get("/timelines/list/:list_id", TimelineController, :list)
++    get("/timelines/public", TimelineController, :public)
++    get("/timelines/tag/:tag", TimelineController, :hashtag)
++    get("/timelines/list/:list_id", TimelineController, :list)
  
-     get("/statuses", MastodonAPIController, :get_statuses)
-     get("/statuses/:id", MastodonAPIController, :get_status)
-     get("/statuses/:id/context", MastodonAPIController, :get_context)
 -      get("/statuses", StatusController, :index)
 -      get("/statuses/:id", StatusController, :show)
 -      get("/statuses/:id/context", StatusController, :context)
++    get("/statuses", StatusController, :index)
++    get("/statuses/:id", StatusController, :show)
++    get("/statuses/:id/context", StatusController, :context)
  
 -      get("/polls/:id", MastodonAPIController, :get_poll)
 +    get("/polls/:id", MastodonAPIController, :get_poll)
  
-     get("/accounts/:id/statuses", MastodonAPIController, :user_statuses)
-     get("/accounts/:id/followers", MastodonAPIController, :followers)
-     get("/accounts/:id/following", MastodonAPIController, :following)
-     get("/accounts/:id", MastodonAPIController, :user)
 -      get("/accounts/:id/statuses", AccountController, :statuses)
 -      get("/accounts/:id/followers", AccountController, :followers)
 -      get("/accounts/:id/following", AccountController, :following)
 -      get("/accounts/:id", AccountController, :show)
++    get("/accounts/:id/statuses", AccountController, :statuses)
++    get("/accounts/:id/followers", AccountController, :followers)
++    get("/accounts/:id/following", AccountController, :following)
++    get("/accounts/:id", AccountController, :show)
  
 -      get("/search", SearchController, :search)
 -    end
 +    get("/search", SearchController, :search)
-     get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites)
    end
  
    scope "/api/v2", Pleroma.Web.MastodonAPI do
 -    pipe_through([:api, :oauth_read_or_public])
 +    pipe_through(:api)
      get("/search", SearchController, :search2)
    end
  
      get("/oauth_tokens", TwitterAPI.Controller, :oauth_tokens)
      delete("/oauth_tokens/:id", TwitterAPI.Controller, :revoke_token)
  
 -    scope [] do
 -      pipe_through(:oauth_read)
 -
 -      post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
 -    end
 +    post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
    end
  
    pipeline :ap_service_actor do
    scope "/", Pleroma.Web.ActivityPub do
      pipe_through([:activitypub_client])
  
 -    scope [] do
 -      pipe_through(:oauth_read)
 -      get("/api/ap/whoami", ActivityPubController, :whoami)
 -      get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
 -    end
 +    get("/api/ap/whoami", ActivityPubController, :whoami)
 +    get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
 -    scope [] do
 -      pipe_through(:oauth_write)
 -      post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
 -      post("/api/ap/upload_media", ActivityPubController, :upload_media)
 -    end
 +    post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
++    post("/api/ap/upload_media", ActivityPubController, :upload_media)
 -    scope [] do
 -      pipe_through(:oauth_read_or_public)
 -      get("/users/:nickname/followers", ActivityPubController, :followers)
 -      get("/users/:nickname/following", ActivityPubController, :following)
 -    end
 +    get("/users/:nickname/followers", ActivityPubController, :followers)
 +    get("/users/:nickname/following", ActivityPubController, :following)
    end
  
    scope "/", Pleroma.Web.ActivityPub do
      pipe_through(:activitypub)
      post("/inbox", ActivityPubController, :inbox)
      post("/users/:nickname/inbox", ActivityPubController, :inbox)
    end
  
      post("/auth/password", MastodonAPIController, :password_reset)
  
 -    scope [] do
 -      pipe_through(:oauth_read)
 -      get("/web/*path", MastodonAPIController, :index)
 -    end
 +    get("/web/*path", MastodonAPIController, :index)
    end
  
    pipeline :remote_media do
index 54f0280c9b096c2e25b2e795327230f29d1fb9b0,f05a84c7f0419201188865ab3aae680831999002..c84359ddba8a9ee275106421b3a9017460c3c9fe
@@@ -13,32 -13,11 +13,32 @@@ defmodule Pleroma.Web.TwitterAPI.UtilCo
    alias Pleroma.Healthcheck
    alias Pleroma.Notification
    alias Pleroma.Plugs.AuthenticationPlug
 +  alias Pleroma.Plugs.OAuthScopesPlug
    alias Pleroma.User
    alias Pleroma.Web
    alias Pleroma.Web.CommonAPI
    alias Pleroma.Web.WebFinger
  
 +  plug(
 +    OAuthScopesPlug,
 +    %{scopes: ["follow", "write:follows"]}
 +    when action in [:do_remote_follow, :follow_import]
 +  )
 +
 +  plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks_import)
 +
 +  plug(
 +    OAuthScopesPlug,
 +    %{scopes: ["write:accounts"]}
 +    when action in [
 +           :change_email,
 +           :change_password,
 +           :delete_account,
 +           :update_notificaton_settings,
 +           :disable_account
 +         ]
 +  )
 +
    plug(Pleroma.Plugs.SetFormatPlug when action in [:config, :version])
  
    def help_test(conn, _params) do
  
    def emoji(conn, _params) do
      emoji =
-       Emoji.get_all()
-       |> Enum.map(fn {short_code, path, tags} ->
-         {short_code, %{image_url: path, tags: tags}}
+       Enum.reduce(Emoji.get_all(), %{}, fn {code, %Emoji{file: file, tags: tags}}, acc ->
+         Map.put(acc, code, %{image_url: file, tags: tags})
        end)
-       |> Enum.into(%{})
  
      json(conn, emoji)
    end
index 42bd74eb5c6d22d71de9644c90b78a9d6cbbb228,5024ac70d821ebdf1528910d15f40bff56382936..bf5a6ae42d87dd7c7f7cb2b09d722f7cab89368c
@@@ -5,29 -5,22 +5,27 @@@
  defmodule Pleroma.Web.TwitterAPI.Controller do
    use Pleroma.Web, :controller
  
-   alias Ecto.Changeset
    alias Pleroma.Notification
 +  alias Pleroma.Plugs.OAuthScopesPlug
    alias Pleroma.User
    alias Pleroma.Web.OAuth.Token
    alias Pleroma.Web.TwitterAPI.TokenView
  
    require Logger
  
 +  plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read)
 +
++  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
++
    action_fallback(:errors)
  
    def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
-     with %User{} = user <- User.get_cached_by_id(uid),
-          true <- user.local,
-          true <- user.info.confirmation_pending,
-          true <- user.info.confirmation_token == token,
-          info_change <- User.Info.confirmation_changeset(user.info, need_confirmation: false),
-          changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_change),
-          {:ok, _} <- User.update_and_set_cache(changeset) do
-       conn
-       |> redirect(to: "/")
+     new_info = [need_confirmation: false]
+     with %User{info: info} = user <- User.get_cached_by_id(uid),
+          true <- user.local and info.confirmation_pending and info.confirmation_token == token,
+          {:ok, _} <- User.update_info(user, &User.Info.confirmation_changeset(&1, new_info)) do
+       redirect(conn, to: "/")
      end
    end
  
diff --combined test/support/factory.ex
index c14c8ddb34c756bfa636a8fe00747809da6616a6,4f3244025fcd31620851d121fa23facad57638ad..b180844cd90acc7fde68ee3d95543bc89c2a1529
@@@ -71,6 -71,47 +71,47 @@@ defmodule Pleroma.Factory d
      }
    end
  
+   def audio_factory(attrs \\ %{}) do
+     text = sequence(:text, &"lain radio episode #{&1}")
+     user = attrs[:user] || insert(:user)
+     data = %{
+       "type" => "Audio",
+       "id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(),
+       "artist" => "lain",
+       "title" => text,
+       "album" => "lain radio",
+       "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+       "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
+       "actor" => user.ap_id,
+       "length" => 180_000
+     }
+     %Pleroma.Object{
+       data: merge_attributes(data, Map.get(attrs, :data, %{}))
+     }
+   end
+   def listen_factory do
+     audio = insert(:audio)
+     data = %{
+       "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
+       "type" => "Listen",
+       "actor" => audio.data["actor"],
+       "to" => audio.data["to"],
+       "object" => audio.data,
+       "published" => audio.data["published"]
+     }
+     %Pleroma.Activity{
+       data: data,
+       actor: data["actor"],
+       recipients: data["to"]
+     }
+   end
    def direct_note_factory do
      user2 = insert(:user)
  
  
      %Pleroma.Web.OAuth.Token{
        token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(),
 +      scopes: ["read"],
        refresh_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(),
        user: build(:user),
        app_id: oauth_app.id,
index 99d534348ee6596170c831275f901560b155e27e,560f5513757c2831b87260ed09088ae4a6804d09..f6c9f5028a74f239d1e12cd790a0e1882b2cab2b
@@@ -86,10 -86,9 +86,9 @@@ defmodule Pleroma.Web.MastodonAPI.Masto
        assert user = json_response(conn, 200)
  
        assert user["note"] ==
-                ~s(I drink <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe" rel="tag">#cofe</a> with <span class="h-card"><a data-user=") <>
-                  user2.id <>
-                  ~s(" class="u-url mention" href=") <>
-                  user2.ap_id <> ~s(">@<span>) <> user2.nickname <> ~s(</span></a></span>)
+                ~s(I drink <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe">#cofe</a> with <span class="h-card"><a data-user="#{
+                  user2.id
+                }" class="u-url mention" href="#{user2.ap_id}" rel="ugc">@<span>#{user2.nickname}</span></a></span>)
      end
  
      test "updates the user's locking status", %{conn: conn} do
        assert user_response["pleroma"]["background_image"]
      end
  
 -    test "requires 'write' permission", %{conn: conn} do
 +    test "requires 'write:accounts' permission", %{conn: conn} do
        token1 = insert(:oauth_token, scopes: ["read"])
        token2 = insert(:oauth_token, scopes: ["write", "follow"])
  
            |> patch("/api/v1/accounts/update_credentials", %{})
  
          if token == token1 do
 -          assert %{"error" => "Insufficient permissions: write."} == json_response(conn, 403)
 +          assert %{"error" => "Insufficient permissions: write:accounts."} ==
 +                   json_response(conn, 403)
          else
            assert json_response(conn, 200)
          end
  
        assert account["fields"] == [
                 %{"name" => "foo", "value" => "bar"},
-                %{"name" => "link", "value" => "<a href=\"http://cofe.io\">cofe.io</a>"}
+                %{"name" => "link", "value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>)}
               ]
  
        assert account["source"]["fields"] == [
index c73c500d935d0983fd34fb1edca82ba4b613f804,0cf755806ab318db09bb06b41a476385d944f17c..9a251b7edb3f82a6a0eb1287836207263d5f4d8a
@@@ -7,6 -7,7 +7,7 @@@ defmodule Pleroma.Web.OAuth.OAuthContro
    import Pleroma.Factory
  
    alias Pleroma.Repo
+   alias Pleroma.User
    alias Pleroma.Web.OAuth.Authorization
    alias Pleroma.Web.OAuth.OAuthController
    alias Pleroma.Web.OAuth.Token
              "password" => "test",
              "client_id" => app.client_id,
              "redirect_uri" => redirect_uri,
 -            "scope" => "read write",
 +            "scope" => "read:subscope write",
              "state" => "statepassed"
            }
          })
        assert %{"state" => "statepassed", "code" => code} = query
        auth = Repo.get_by(Authorization, token: code)
        assert auth
 -      assert auth.scopes == ["read", "write"]
 +      assert auth.scopes == ["read:subscope", "write"]
      end
  
      test "returns 401 for wrong credentials", %{conn: conn} do
        assert result =~ "This action is outside the authorized scopes"
      end
  
 -    test "returns 401 for scopes beyond app scopes", %{conn: conn} do
 +    test "returns 401 for scopes beyond app scopes hierarchy", %{conn: conn} do
        user = insert(:user)
        app = insert(:oauth_app, scopes: ["read", "write"])
        redirect_uri = OAuthController.default_redirect_uri(app)
  
      test "rejects token exchange for valid credentials belonging to unconfirmed user and confirmation is required" do
        Pleroma.Config.put([:instance, :account_activation_required], true)
        password = "testpassword"
-       user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password))
-       info_change = Pleroma.User.Info.confirmation_changeset(user.info, need_confirmation: true)
  
        {:ok, user} =
-         user
-         |> Ecto.Changeset.change()
-         |> Ecto.Changeset.put_embed(:info, info_change)
+         insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password))
+         |> User.change_info(&User.Info.confirmation_changeset(&1, need_confirmation: true))
          |> Repo.update()
  
        refute Pleroma.User.auth_active?(user)
        refute Map.has_key?(resp, "access_token")
      end
  
+     test "rejects token exchange for user with password_reset_pending set to true" do
+       password = "testpassword"
+       user =
+         insert(:user,
+           password_hash: Comeonin.Pbkdf2.hashpwsalt(password),
+           info: %{password_reset_pending: true}
+         )
+       app = insert(:oauth_app, scopes: ["read", "write"])
+       conn =
+         build_conn()
+         |> post("/oauth/token", %{
+           "grant_type" => "password",
+           "username" => user.nickname,
+           "password" => password,
+           "client_id" => app.client_id,
+           "client_secret" => app.client_secret
+         })
+       assert resp = json_response(conn, 403)
+       assert resp["error"] == "Password reset is required"
+       refute Map.has_key?(resp, "access_token")
+     end
      test "rejects an invalid authorization code" do
        app = insert(:oauth_app)