[#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 --cc CHANGELOG.md
index e3806166fa0d2a7f6230c6067c6cdbd849ef0994,3d9424c8fdb977788031a92b85276bade90bd328..87b6d1180d2b7997af1ef8195654e93de1536c09
@@@ -100,9 -112,11 +112,12 @@@ The format is based on [Keep a Changelo
  - 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
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
@@@ -11,14 -10,6 +11,16 @@@ defmodule Pleroma.Web.MastodonAPI.ListC
  
    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
index b1e9dee3d03f70726c371ac19c41b686ab2ff23e,1484a017472d55432721a91b0f33d0cbbe2cf03e..ee644abe30da46dbac18c9e76a153e7d6fd487f5
@@@ -11,18 -10,11 +10,12 @@@ defmodule Pleroma.Web.MastodonAPI.Masto
    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.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"
  
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 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 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
@@@ -15,18 -14,6 +15,20 @@@ defmodule Pleroma.Web.PleromaAPI.Plerom
    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
@@@ -180,6 -206,30 +181,30 @@@ defmodule Pleroma.Web.Router d
      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
    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
  
      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
    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
index 42bd74eb5c6d22d71de9644c90b78a9d6cbbb228,5024ac70d821ebdf1528910d15f40bff56382936..bf5a6ae42d87dd7c7f7cb2b09d722f7cab89368c
@@@ -5,17 -5,13 +5,18 @@@
  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
Simple merge