[#468] Merged `upstream/develop`, resolved conflicts.
authorIvan Tashkinov <ivantashkinov@gmail.com>
Sun, 17 Feb 2019 11:07:04 +0000 (14:07 +0300)
committerIvan Tashkinov <ivantashkinov@gmail.com>
Sun, 17 Feb 2019 11:07:04 +0000 (14:07 +0300)
1  2 
lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
lib/pleroma/web/oauth/app.ex
lib/pleroma/web/oauth/authorization.ex
lib/pleroma/web/oauth/oauth_controller.ex
lib/pleroma/web/oauth/token.ex
lib/pleroma/web/router.ex
lib/pleroma/web/templates/layout/app.html.eex
lib/pleroma/web/templates/o_auth/o_auth/show.html.eex
test/web/oauth/authorization_test.exs
test/web/oauth/oauth_controller_test.exs
test/web/oauth/token_test.exs

index 5d51e913d57621638026a20df864955b7979f443,dcaeccac698d0bee14dee817dcc48a11b8dc06fe..942bb4338d6dede9ac08850433f385a77c27404b
@@@ -4,27 -4,33 +4,35 @@@
  
  defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
    use Pleroma.Web, :controller
-   alias Pleroma.{Repo, Object, Activity, User, Notification, Stats}
+   alias Pleroma.Activity
+   alias Pleroma.Config
+   alias Pleroma.Filter
+   alias Pleroma.Notification
+   alias Pleroma.Object
+   alias Pleroma.Repo
+   alias Pleroma.Stats
+   alias Pleroma.User
    alias Pleroma.Web
-   alias Pleroma.Web.MastodonAPI.{
-     StatusView,
-     AccountView,
-     MastodonView,
-     ListView,
-     FilterView,
-     PushSubscriptionView
-   }
-   alias Pleroma.Web.ActivityPub.ActivityPub
-   alias Pleroma.Web.ActivityPub.Utils
    alias Pleroma.Web.CommonAPI
-   alias Pleroma.Web.OAuth.{Authorization, Token, App}
    alias Pleroma.Web.MediaProxy
+   alias Pleroma.Web.Push
+   alias Push.Subscription
+   alias Pleroma.Web.MastodonAPI.AccountView
+   alias Pleroma.Web.MastodonAPI.FilterView
+   alias Pleroma.Web.MastodonAPI.ListView
+   alias Pleroma.Web.MastodonAPI.MastodonView
+   alias Pleroma.Web.MastodonAPI.PushSubscriptionView
+   alias Pleroma.Web.MastodonAPI.StatusView
+   alias Pleroma.Web.ActivityPub.ActivityPub
+   alias Pleroma.Web.ActivityPub.Utils
+   alias Pleroma.Web.OAuth.App
+   alias Pleroma.Web.OAuth.Authorization
+   alias Pleroma.Web.OAuth.Token
  
 +  import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
    import Ecto.Query
 +
    require Logger
  
    @httpoison Application.get_env(:pleroma, :httpoison)
    action_fallback(:errors)
  
    def create_app(conn, params) do
 -    with cs <- App.register_changeset(%App{}, params),
 +    scopes = oauth_scopes(params, ["read"])
 +
 +    app_attrs =
 +      params
 +      |> Map.drop(["scope", "scopes"])
 +      |> Map.put("scopes", scopes)
 +
 +    with cs <- App.register_changeset(%App{}, app_attrs),
           false <- cs.changes[:client_name] == @local_mastodon_name,
           {:ok, app} <- Repo.insert(cs) do
        res = %{
    @mastodon_api_level "2.5.0"
  
    def masto_instance(conn, _params) do
-     instance = Pleroma.Config.get(:instance)
+     instance = Config.get(:instance)
  
      response = %{
        uri: Web.base_url(),
        |> Map.put("user", user)
  
      activities =
-       ActivityPub.fetch_activities([user.ap_id | user.following], params)
+       [user.ap_id | user.following]
+       |> ActivityPub.fetch_activities(params)
        |> ActivityPub.contain_timeline(user)
        |> Enum.reverse()
  
    def public_timeline(%{assigns: %{user: user}} = conn, params) do
      local_only = params["local"] in [true, "True", "true", "1"]
  
-     params =
+     activities =
        params
        |> Map.put("type", ["Create", "Announce"])
        |> Map.put("local_only", local_only)
        |> Map.put("blocking_user", user)
-     activities =
-       ActivityPub.fetch_public_activities(params)
+       |> ActivityPub.fetch_public_activities()
        |> Enum.reverse()
  
      conn
              as: :activity
            )
            |> Enum.reverse(),
+         # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
          descendants:
            StatusView.render(
              "index.json",
              as: :activity
            )
            |> Enum.reverse()
+         # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
        }
  
        json(conn, result)
      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})
+     else
+       {:error, reason} ->
+         conn
+         |> put_resp_content_type("application/json")
+         |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
+     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 = Notification.for_user(user, params)
  
      result =
-       Enum.map(notifications, fn x ->
-         render_notification(user, x)
-       end)
+       notifications
+       |> Enum.map(fn x -> render_notification(user, x) end)
        |> Enum.filter(& &1)
  
      conn
          []
          |> Enum.map(&String.downcase(&1))
  
-     query_params =
+     activities =
        params
        |> Map.put("type", "Create")
        |> Map.put("local_only", local_only)
        |> Map.put("tag", tags)
        |> Map.put("tag_all", tag_all)
        |> Map.put("tag_reject", tag_reject)
-     activities =
-       ActivityPub.fetch_public_activities(query_params)
+       |> ActivityPub.fetch_public_activities()
        |> Enum.reverse()
  
      conn
           {:ok, _activity} <- ActivityPub.follow(follower, followed),
           {:ok, follower, followed} <-
             User.wait_and_refresh(
-              Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
+              Config.get([:activitypub, :follow_handshake_timeout]),
               follower,
               followed
             ) do
      tags_path = Web.base_url() <> "/tag/"
  
      tags =
-       String.split(query)
+       query
+       |> String.split()
        |> Enum.uniq()
        |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
        |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
      statuses = status_search(user, query)
  
      tags =
-       String.split(query)
+       query
+       |> String.split()
        |> Enum.uniq()
        |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
        |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
    end
  
    def favourites(%{assigns: %{user: user}} = conn, params) do
-     params =
+     activities =
        params
        |> Map.put("type", "Create")
        |> Map.put("favorited_by", user.ap_id)
        |> Map.put("blocking_user", user)
-     activities =
-       ActivityPub.fetch_public_activities(params)
+       |> ActivityPub.fetch_public_activities()
        |> Enum.reverse()
  
      conn
  
        # 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).
-       following_to =
+       activities =
          following
          |> Enum.filter(fn x -> x in user.following end)
-       activities =
-         ActivityPub.fetch_activities_bounded(following_to, following, params)
+         |> ActivityPub.fetch_activities_bounded(following, params)
          |> Enum.reverse()
  
        conn
      if user && token do
        mastodon_emoji = mastodonized_emoji()
  
-       limit = Pleroma.Config.get([:instance, :limit])
+       limit = Config.get([:instance, :limit])
  
        accounts =
          Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
              max_toot_chars: limit
            },
            rights: %{
-             delete_others_notice: !!user.info.is_moderator,
-             admin: !!user.info.is_admin
+             delete_others_notice: present?(user.info.is_moderator),
+             admin: present?(user.info.is_admin)
            },
            compose: %{
              me: "#{user.id}",
            response_type: "code",
            client_id: app.client_id,
            redirect_uri: ".",
 -          scope: app.scopes
 +          scope: Enum.join(app.scopes, " ")
          )
  
        conn
        {:ok, app}
      else
        _e ->
 -        cs = App.register_changeset(%App{}, Map.put(find_attrs, :scopes, "read,write,follow"))
 +        cs =
 +          App.register_changeset(
 +            %App{},
 +            Map.put(find_attrs, :scopes, ["read", "write", "follow"])
 +          )
  
          Repo.insert(cs)
      end
    end
  
    def get_filters(%{assigns: %{user: user}} = conn, _) do
-     filters = Pleroma.Filter.get_filters(user)
+     filters = Filter.get_filters(user)
      res = FilterView.render("filters.json", filters: filters)
      json(conn, res)
    end
          %{assigns: %{user: user}} = conn,
          %{"phrase" => phrase, "context" => context} = params
        ) do
-     query = %Pleroma.Filter{
+     query = %Filter{
        user_id: user.id,
        phrase: phrase,
        context: context,
        # expires_at
      }
  
-     {:ok, response} = Pleroma.Filter.create(query)
+     {: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 = Pleroma.Filter.get(filter_id, user)
+     filter = Filter.get(filter_id, user)
      res = FilterView.render("filter.json", filter: filter)
      json(conn, res)
    end
          %{assigns: %{user: user}} = conn,
          %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
        ) do
-     query = %Pleroma.Filter{
+     query = %Filter{
        user_id: user.id,
        filter_id: filter_id,
        phrase: phrase,
        # expires_at
      }
  
-     {:ok, response} = Pleroma.Filter.update(query)
+     {: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 = %Pleroma.Filter{
+     query = %Filter{
        user_id: user.id,
        filter_id: filter_id
      }
  
-     {:ok, _} = Pleroma.Filter.delete(query)
+     {:ok, _} = Filter.delete(query)
      json(conn, %{})
    end
  
    def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
-     true = Pleroma.Web.Push.enabled()
-     Pleroma.Web.Push.Subscription.delete_if_exists(user, token)
-     {:ok, subscription} = Pleroma.Web.Push.Subscription.create(user, token, params)
+     true = Push.enabled()
+     Subscription.delete_if_exists(user, token)
+     {:ok, subscription} = Subscription.create(user, token, params)
      view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
      json(conn, view)
    end
  
    def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
-     true = Pleroma.Web.Push.enabled()
-     subscription = Pleroma.Web.Push.Subscription.get(user, token)
+     true = Push.enabled()
+     subscription = Subscription.get(user, token)
      view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
      json(conn, view)
    end
          %{assigns: %{user: user, token: token}} = conn,
          params
        ) do
-     true = Pleroma.Web.Push.enabled()
-     {:ok, subscription} = Pleroma.Web.Push.Subscription.update(user, token, params)
+     true = Push.enabled()
+     {:ok, subscription} = Subscription.update(user, token, params)
      view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
      json(conn, view)
    end
  
    def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
-     true = Pleroma.Web.Push.enabled()
-     {:ok, _response} = Pleroma.Web.Push.Subscription.delete(user, token)
+     true = Push.enabled()
+     {:ok, _response} = Subscription.delete(user, token)
      json(conn, %{})
    end
  
    end
  
    def suggestions(%{assigns: %{user: user}} = conn, _) do
-     suggestions = Pleroma.Config.get(:suggestions)
+     suggestions = Config.get(:suggestions)
  
      if Keyword.get(suggestions, :enabled, false) do
        api = Keyword.get(suggestions, :third_party_engine, "")
        timeout = Keyword.get(suggestions, :timeout, 5000)
        limit = Keyword.get(suggestions, :limit, 23)
  
-       host = Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
+       host = Config.get([Pleroma.Web.Endpoint, :url, :host])
  
        user = user.nickname
-       url = String.replace(api, "{{host}}", host) |> String.replace("{{user}}", user)
+       url =
+         api
+         |> String.replace("{{host}}", host)
+         |> String.replace("{{user}}", user)
  
        with {:ok, %{status: 200, body: body}} <-
               @httpoison.get(
                 ]
               ),
             {:ok, data} <- Jason.decode(body) do
-         data2 =
-           Enum.slice(data, 0, limit)
+         data =
+           data
+           |> Enum.slice(0, limit)
            |> Enum.map(fn x ->
              Map.put(
                x,
            end)
  
          conn
-         |> json(data2)
+         |> json(data)
        else
          e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
        end
      |> put_status(501)
      |> json(%{error: "Can't display this activity"})
    end
+   defp present?(nil), do: false
+   defp present?(false), do: false
+   defp present?(_), do: true
  end
index c04626a73835c1b4e38cf91312fc010db57128f8,8b61bf3a4e964e1c2837d926aa100ef40ed54119..3476da484b817cc8a3a27676f9ab03d02e3e532c
@@@ -4,12 -4,12 +4,12 @@@
  
  defmodule Pleroma.Web.OAuth.App do
    use Ecto.Schema
-   import Ecto.{Changeset}
+   import Ecto.Changeset
  
    schema "apps" do
      field(:client_name, :string)
      field(:redirect_uris, :string)
 -    field(:scopes, :string)
 +    field(:scopes, {:array, :string}, default: [])
      field(:website, :string)
      field(:client_id, :string)
      field(:client_secret, :string)
  
      if changeset.valid? do
        changeset
-       |> put_change(:client_id, :crypto.strong_rand_bytes(32) |> Base.url_encode64())
-       |> put_change(:client_secret, :crypto.strong_rand_bytes(32) |> Base.url_encode64())
+       |> put_change(
+         :client_id,
+         :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
+       )
+       |> put_change(
+         :client_secret,
+         :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
+       )
      else
        changeset
      end
index c5b7ec9a5628499719103909f7f81aa7b2e8b034,9039b8b4564d5d21bc56283d1f2c2cadc943c90b..d37c2cb83b5c70fe67866e97290627f0352b6156
@@@ -5,14 -5,16 +5,17 @@@
  defmodule Pleroma.Web.OAuth.Authorization do
    use Ecto.Schema
  
-   alias Pleroma.{User, Repo}
-   alias Pleroma.Web.OAuth.{Authorization, App}
+   alias Pleroma.User
+   alias Pleroma.Repo
+   alias Pleroma.Web.OAuth.Authorization
+   alias Pleroma.Web.OAuth.App
  
-   import Ecto.{Changeset, Query}
+   import Ecto.Changeset
+   import Ecto.Query
  
    schema "oauth_authorizations" do
      field(:token, :string)
 +    field(:scopes, {:array, :string}, default: [])
      field(:valid_until, :naive_datetime)
      field(:used, :boolean, default: false)
      belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId)
      timestamps()
    end
  
 -  def create_authorization(%App{} = app, %User{} = user) do
 +  def create_authorization(%App{} = app, %User{} = user, scopes \\ nil) do
 +    scopes = scopes || app.scopes
-     token = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
+     token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
  
      authorization = %Authorization{
        token: token,
        used: false,
        user_id: user.id,
        app_id: app.id,
 +      scopes: scopes,
        valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)
      }
  
index fe2c958c91939dfa9610458758677c7d1f6ad712,dddfcf29981aa1164d33a88be96a9e961cc76320..7c1a3adbd363046ff3606922c113d4518039c41d
@@@ -5,27 -5,23 +5,30 @@@
  defmodule Pleroma.Web.OAuth.OAuthController do
    use Pleroma.Web, :controller
  
-   alias Pleroma.Web.OAuth.{Authorization, Token, App}
-   alias Pleroma.{Repo, User}
+   alias Pleroma.Web.OAuth.Authorization
+   alias Pleroma.Web.OAuth.Token
+   alias Pleroma.Web.OAuth.App
+   alias Pleroma.Repo
+   alias Pleroma.User
    alias Comeonin.Pbkdf2
  
 +  import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
 +
    plug(:fetch_session)
    plug(:fetch_flash)
  
    action_fallback(Pleroma.Web.OAuth.FallbackController)
  
    def authorize(conn, params) do
 +    app = Repo.get_by(App, client_id: params["client_id"])
 +    available_scopes = (app && app.scopes) || []
 +    scopes = oauth_scopes(params, nil) || available_scopes
 +
      render(conn, "show.html", %{
        response_type: params["response_type"],
        client_id: params["client_id"],
 -      scope: params["scope"],
 +      available_scopes: available_scopes,
 +      scopes: scopes,
        redirect_uri: params["redirect_uri"],
        state: params["state"]
      })
              "password" => password,
              "client_id" => client_id,
              "redirect_uri" => redirect_uri
 -          } = params
 +          } = auth_params
        }) do
      with %User{} = user <- User.get_by_nickname_or_email(name),
           true <- Pbkdf2.checkpw(password, user.password_hash),
 -         {:auth_active, true} <- {:auth_active, User.auth_active?(user)},
           %App{} = app <- Repo.get_by(App, client_id: client_id),
           true <- redirect_uri in String.split(app.redirect_uris),
 -         {:ok, auth} <- Authorization.create_authorization(app, user) do
 +         scopes <- oauth_scopes(auth_params, []),
 +         {:unsupported_scopes, []} <- {:unsupported_scopes, scopes -- app.scopes},
 +         # Note: `scope` param is intentionally not optional in this context
 +         {:missing_scopes, false} <- {:missing_scopes, scopes == []},
 +         {:auth_active, true} <- {:auth_active, User.auth_active?(user)},
 +         {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
        # Special case: Local MastodonFE.
        redirect_uri =
          if redirect_uri == "." do
@@@ -70,8 -62,8 +73,8 @@@
            url_params = %{:code => auth.token}
  
            url_params =
 -            if params["state"] do
 -              Map.put(url_params, :state, params["state"])
 +            if auth_params["state"] do
 +              Map.put(url_params, :state, auth_params["state"])
              else
                url_params
              end
            redirect(conn, external: url)
        end
      else
 +      {scopes_issue, _} when scopes_issue in [:unsupported_scopes, :missing_scopes] ->
 +        conn
 +        |> put_flash(:error, "Permissions not specified.")
 +        |> put_status(:unauthorized)
 +        |> authorize(auth_params)
 +
        {:auth_active, false} ->
          conn
 -        |> put_flash(:error, "Account confirmation pending")
 +        |> put_flash(:error, "Account confirmation pending.")
          |> put_status(:forbidden)
 -        |> authorize(params)
 +        |> authorize(auth_params)
  
        error ->
          error
      end
    end
  
 -  # TODO
 -  # - proper scope handling
    def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
      with %App{} = app <- get_app_from_request(conn, params),
           fixed_token = fix_padding(params["code"]),
          refresh_token: token.refresh_token,
          created_at: DateTime.to_unix(inserted_at),
          expires_in: 60 * 10,
 -        scope: "read write follow"
 +        scope: Enum.join(token.scopes)
        }
  
        json(conn, response)
      end
    end
  
 -  # TODO
 -  # - investigate a way to verify the user wants to grant read/write/follow once scope handling is done
    def token_exchange(
          conn,
          %{"grant_type" => "password", "username" => name, "password" => password} = params
           %User{} = user <- User.get_by_nickname_or_email(name),
           true <- Pbkdf2.checkpw(password, user.password_hash),
           {:auth_active, true} <- {:auth_active, User.auth_active?(user)},
 -         {:ok, auth} <- Authorization.create_authorization(app, user),
 +         scopes <- oauth_scopes(params, app.scopes),
 +         [] <- scopes -- app.scopes,
 +         true <- Enum.any?(scopes),
 +         {:ok, auth} <- Authorization.create_authorization(app, user, scopes),
           {:ok, token} <- Token.exchange_token(app, auth) do
        response = %{
          token_type: "Bearer",
          access_token: token.token,
          refresh_token: token.refresh_token,
          expires_in: 60 * 10,
 -        scope: "read write follow"
 +        scope: Enum.join(token.scopes, " ")
        }
  
        json(conn, response)
      token
      |> URI.decode()
      |> Base.url_decode64!(padding: false)
-     |> Base.url_encode64()
+     |> Base.url_encode64(padding: false)
    end
  
    defp get_app_from_request(conn, params) do
index 1fae5ed3a6018b222cae78ae6f768024bdd4051b,ca9e718ac8a27b562c4de9936500f50bddc51b23..ea4d56a291ae08f5b37cadc60e4d7ef97986ebcb
@@@ -7,13 -7,15 +7,16 @@@ defmodule Pleroma.Web.OAuth.Token d
  
    import Ecto.Query
  
-   alias Pleroma.{User, Repo}
-   alias Pleroma.Web.OAuth.{Token, App, Authorization}
+   alias Pleroma.User
+   alias Pleroma.Repo
+   alias Pleroma.Web.OAuth.Token
+   alias Pleroma.Web.OAuth.App
+   alias Pleroma.Web.OAuth.Authorization
  
    schema "oauth_tokens" do
      field(:token, :string)
      field(:refresh_token, :string)
 +    field(:scopes, {:array, :string}, default: [])
      field(:valid_until, :naive_datetime)
      belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId)
      belongs_to(:app, App)
    def exchange_token(app, auth) do
      with {:ok, auth} <- Authorization.use_token(auth),
           true <- auth.app_id == app.id do
 -      create_token(app, Repo.get(User, auth.user_id))
 +      create_token(app, Repo.get(User, auth.user_id), auth.scopes)
      end
    end
  
 -  def create_token(%App{} = app, %User{} = user) do
 +  def create_token(%App{} = app, %User{} = user, scopes \\ nil) do
 +    scopes = scopes || app.scopes
-     token = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
-     refresh_token = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
+     token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
+     refresh_token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
  
      token = %Token{
        token: token,
        refresh_token: refresh_token,
 +      scopes: scopes,
        user_id: user.id,
        app_id: app.id,
        valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)
index 6f17de1cafb7d64d96335c0dffc712c45ad8820b,d66a1c2a136b2d60eec02d900e747a5d90b99790..e09164a779a3848610c3b72cc7e40df374b32cfb
@@@ -74,25 -74,6 +74,25 @@@ defmodule Pleroma.Web.Router d
      plug(Pleroma.Plugs.EnsureUserKeyPlug)
    end
  
 +  pipeline :oauth_read_or_unauthenticated do
 +    plug(Pleroma.Plugs.OAuthScopesPlug, %{
 +      scopes: ["read"],
 +      fallback: :proceed_unauthenticated
 +    })
 +  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 :well_known do
      plug(:accepts, ["json", "jrd+json", "xml", "xrd+xml"])
    end
  
    scope "/api/pleroma", Pleroma.Web.TwitterAPI do
      pipe_through(:pleroma_api)
 +
      get("/password_reset/:token", UtilController, :show_password_reset)
      post("/password_reset", UtilController, :password_reset)
      get("/emoji", UtilController, :emoji)
    end
  
    scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do
 -    pipe_through(:admin_api)
 +    pipe_through([:admin_api, :oauth_write])
 +
      delete("/user", AdminAPIController, :user_delete)
      post("/user", AdminAPIController, :user_create)
      put("/users/tag", AdminAPIController, :tag_users)
  
    scope "/", Pleroma.Web.TwitterAPI do
      pipe_through(:pleroma_html)
 -    get("/ostatus_subscribe", UtilController, :remote_follow)
 -    post("/ostatus_subscribe", UtilController, :do_remote_follow)
 +
      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
    end
  
    scope "/api/pleroma", Pleroma.Web.TwitterAPI do
      pipe_through(:authenticated_api)
 -    post("/blocks_import", UtilController, :blocks_import)
 -    post("/follow_import", UtilController, :follow_import)
 -    post("/change_password", UtilController, :change_password)
 -    post("/delete_account", UtilController, :delete_account)
 +
 +    scope [] do
 +      pipe_through(:oauth_write)
 +
 +      post("/change_password", UtilController, :change_password)
 +      post("/delete_account", UtilController, :delete_account)
 +    end
 +
 +    scope [] do
 +      pipe_through(:oauth_follow)
 +
 +      post("/blocks_import", UtilController, :blocks_import)
 +      post("/follow_import", UtilController, :follow_import)
 +    end
    end
  
    scope "/oauth", Pleroma.Web.OAuth do
    scope "/api/v1", Pleroma.Web.MastodonAPI do
      pipe_through(:authenticated_api)
  
 -    patch("/accounts/update_credentials", MastodonAPIController, :update_credentials)
 -    get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials)
 -    get("/accounts/relationships", MastodonAPIController, :relationships)
 -    get("/accounts/search", MastodonAPIController, :account_search)
 -    post("/accounts/:id/follow", MastodonAPIController, :follow)
 -    post("/accounts/:id/unfollow", MastodonAPIController, :unfollow)
 -    post("/accounts/:id/block", MastodonAPIController, :block)
 -    post("/accounts/:id/unblock", MastodonAPIController, :unblock)
 -    post("/accounts/:id/mute", MastodonAPIController, :relationship_noop)
 -    post("/accounts/:id/unmute", MastodonAPIController, :relationship_noop)
 -    get("/accounts/:id/lists", MastodonAPIController, :account_lists)
 +    scope [] do
 +      pipe_through(:oauth_read)
 +
 +      get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials)
  
 -    get("/follow_requests", MastodonAPIController, :follow_requests)
 -    post("/follow_requests/:id/authorize", MastodonAPIController, :authorize_follow_request)
 -    post("/follow_requests/:id/reject", MastodonAPIController, :reject_follow_request)
 +      get("/accounts/relationships", MastodonAPIController, :relationships)
 +      get("/accounts/search", MastodonAPIController, :account_search)
  
 -    post("/follows", MastodonAPIController, :follow)
 +      get("/accounts/:id/lists", MastodonAPIController, :account_lists)
  
 -    get("/blocks", MastodonAPIController, :blocks)
 +      get("/follow_requests", MastodonAPIController, :follow_requests)
 +      get("/blocks", MastodonAPIController, :blocks)
 +      get("/mutes", MastodonAPIController, :empty_array)
  
 -    get("/mutes", MastodonAPIController, :empty_array)
 +      get("/timelines/home", MastodonAPIController, :home_timeline)
 +      get("/timelines/direct", MastodonAPIController, :dm_timeline)
  
 -    get("/timelines/home", MastodonAPIController, :home_timeline)
 +      get("/favourites", MastodonAPIController, :favourites)
 +      get("/bookmarks", MastodonAPIController, :bookmarks)
  
 -    get("/timelines/direct", MastodonAPIController, :dm_timeline)
 +      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("/lists", MastodonAPIController, :get_lists)
 +      get("/lists/:id", MastodonAPIController, :get_list)
 +      get("/lists/:id/accounts", MastodonAPIController, :list_accounts)
  
 -    post("/statuses", MastodonAPIController, :post_status)
 -    delete("/statuses/:id", MastodonAPIController, :delete_status)
 +      get("/domain_blocks", MastodonAPIController, :domain_blocks)
 +
 +      get("/filters", MastodonAPIController, :get_filters)
 +
 +      get("/suggestions", MastodonAPIController, :suggestions)
 +
 +      get("/endorsements", MastodonAPIController, :empty_array)
 +    end
 +
 +    scope [] do
 +      pipe_through(:oauth_write)
 +
 +      patch("/accounts/update_credentials", MastodonAPIController, :update_credentials)
 +
 +      post("/statuses", MastodonAPIController, :post_status)
 +      delete("/statuses/:id", MastodonAPIController, :delete_status)
  
-       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)
-       post("/statuses/:id/bookmark", MastodonAPIController, :bookmark_status)
-       post("/statuses/:id/unbookmark", MastodonAPIController, :unbookmark_status)
+     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)
+     post("/statuses/:id/bookmark", MastodonAPIController, :bookmark_status)
+     post("/statuses/:id/unbookmark", MastodonAPIController, :unbookmark_status)
+     post("/statuses/:id/mute", MastodonAPIController, :mute_conversation)
+     post("/statuses/:id/unmute", MastodonAPIController, :unmute_conversation)
  
 -    post("/notifications/clear", MastodonAPIController, :clear_notifications)
 -    post("/notifications/dismiss", MastodonAPIController, :dismiss_notification)
 -    get("/notifications", MastodonAPIController, :notifications)
 -    get("/notifications/:id", MastodonAPIController, :get_notification)
 +      post("/media", MastodonAPIController, :upload)
 +      put("/media/:id", MastodonAPIController, :update_media)
 +
 +      delete("/lists/:id", MastodonAPIController, :delete_list)
 +      post("/lists", MastodonAPIController, :create_list)
 +      put("/lists/:id", MastodonAPIController, :rename_list)
  
 -    post("/media", MastodonAPIController, :upload)
 -    put("/media/:id", MastodonAPIController, :update_media)
 +      post("/lists/:id/accounts", MastodonAPIController, :add_to_list)
 +      delete("/lists/:id/accounts", MastodonAPIController, :remove_from_list)
  
 -    get("/lists", MastodonAPIController, :get_lists)
 -    get("/lists/:id", MastodonAPIController, :get_list)
 -    delete("/lists/:id", MastodonAPIController, :delete_list)
 -    post("/lists", MastodonAPIController, :create_list)
 -    put("/lists/:id", MastodonAPIController, :rename_list)
 -    get("/lists/:id/accounts", MastodonAPIController, :list_accounts)
 -    post("/lists/:id/accounts", MastodonAPIController, :add_to_list)
 -    delete("/lists/:id/accounts", MastodonAPIController, :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)
 +    end
 +
 +    scope [] do
 +      pipe_through(:oauth_follow)
  
 -    get("/domain_blocks", MastodonAPIController, :domain_blocks)
 -    post("/domain_blocks", MastodonAPIController, :block_domain)
 -    delete("/domain_blocks", MastodonAPIController, :unblock_domain)
 +      post("/follows", MastodonAPIController, :follow)
 +      post("/accounts/:id/follow", MastodonAPIController, :follow)
  
 -    get("/filters", MastodonAPIController, :get_filters)
 -    post("/filters", MastodonAPIController, :create_filter)
 -    get("/filters/:id", MastodonAPIController, :get_filter)
 -    put("/filters/:id", MastodonAPIController, :update_filter)
 -    delete("/filters/:id", MastodonAPIController, :delete_filter)
 +      post("/accounts/:id/unfollow", MastodonAPIController, :unfollow)
 +      post("/accounts/:id/block", MastodonAPIController, :block)
 +      post("/accounts/:id/unblock", MastodonAPIController, :unblock)
 +      post("/accounts/:id/mute", MastodonAPIController, :relationship_noop)
 +      post("/accounts/:id/unmute", MastodonAPIController, :relationship_noop)
  
 -    post("/push/subscription", MastodonAPIController, :create_push_subscription)
 -    get("/push/subscription", MastodonAPIController, :get_push_subscription)
 -    put("/push/subscription", MastodonAPIController, :update_push_subscription)
 -    delete("/push/subscription", MastodonAPIController, :delete_push_subscription)
 +      post("/follow_requests/:id/authorize", MastodonAPIController, :authorize_follow_request)
 +      post("/follow_requests/:id/reject", MastodonAPIController, :reject_follow_request)
  
 -    get("/suggestions", MastodonAPIController, :suggestions)
 +      post("/domain_blocks", MastodonAPIController, :block_domain)
 +      delete("/domain_blocks", MastodonAPIController, :unblock_domain)
  
 -    get("/endorsements", MastodonAPIController, :empty_array)
 +      post("/push/subscription", MastodonAPIController, :create_push_subscription)
 +      get("/push/subscription", MastodonAPIController, :get_push_subscription)
 +      put("/push/subscription", MastodonAPIController, :update_push_subscription)
 +      delete("/push/subscription", MastodonAPIController, :delete_push_subscription)
 +    end
    end
  
    scope "/api/web", Pleroma.Web.MastodonAPI do
 -    pipe_through(:authenticated_api)
 +    pipe_through([:authenticated_api, :oauth_write])
  
      put("/settings", MastodonAPIController, :put_settings)
    end
  
    scope "/api/v1", Pleroma.Web.MastodonAPI do
      pipe_through(:api)
 +
      get("/instance", MastodonAPIController, :masto_instance)
      get("/instance/peers", MastodonAPIController, :peers)
      post("/apps", MastodonAPIController, :create_app)
      get("/custom_emojis", MastodonAPIController, :custom_emojis)
  
 -    get("/timelines/public", MastodonAPIController, :public_timeline)
 -    get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline)
 -    get("/timelines/list/:list_id", MastodonAPIController, :list_timeline)
 -
 -    get("/statuses/:id", MastodonAPIController, :get_status)
 -    get("/statuses/:id/context", MastodonAPIController, :get_context)
      get("/statuses/:id/card", MastodonAPIController, :status_card)
 +
      get("/statuses/:id/favourited_by", MastodonAPIController, :favourited_by)
      get("/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by)
  
 -    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("/trends", MastodonAPIController, :empty_array)
  
 -    get("/search", MastodonAPIController, :search)
 +    scope [] do
 +      pipe_through(:oauth_read_or_unauthenticated)
 +
 +      get("/timelines/public", MastodonAPIController, :public_timeline)
 +      get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline)
 +      get("/timelines/list/:list_id", MastodonAPIController, :list_timeline)
 +
 +      get("/statuses/:id", MastodonAPIController, :get_status)
 +      get("/statuses/:id/context", MastodonAPIController, :get_context)
 +
 +      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("/search", MastodonAPIController, :search)
 +    end
    end
  
    scope "/api/v2", Pleroma.Web.MastodonAPI do
 -    pipe_through(:api)
 +    pipe_through([:api, :oauth_read_or_unauthenticated])
      get("/search", MastodonAPIController, :search2)
    end
  
    scope "/api", Pleroma.Web do
      pipe_through(:api)
  
 -    get("/statuses/user_timeline", TwitterAPI.Controller, :user_timeline)
 -    get("/qvitter/statuses/user_timeline", TwitterAPI.Controller, :user_timeline)
 -    get("/users/show", TwitterAPI.Controller, :show_user)
 -
 -    get("/statuses/followers", TwitterAPI.Controller, :followers)
 -    get("/statuses/friends", TwitterAPI.Controller, :friends)
 -    get("/statuses/blocks", TwitterAPI.Controller, :blocks)
 -    get("/statuses/show/:id", TwitterAPI.Controller, :fetch_status)
 -    get("/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation)
 -
      post("/account/register", TwitterAPI.Controller, :register)
      post("/account/password_reset", TwitterAPI.Controller, :password_reset)
  
 +    post("/account/resend_confirmation_email", TwitterAPI.Controller, :resend_confirmation_email)
 +
      get(
        "/account/confirm_email/:user_id/:token",
        TwitterAPI.Controller,
        as: :confirm_email
      )
  
 -    post("/account/resend_confirmation_email", TwitterAPI.Controller, :resend_confirmation_email)
 +    scope [] do
 +      pipe_through(:oauth_read_or_unauthenticated)
 +
 +      get("/statuses/user_timeline", TwitterAPI.Controller, :user_timeline)
 +      get("/qvitter/statuses/user_timeline", TwitterAPI.Controller, :user_timeline)
 +      get("/users/show", TwitterAPI.Controller, :show_user)
 +
 +      get("/statuses/followers", TwitterAPI.Controller, :followers)
 +      get("/statuses/friends", TwitterAPI.Controller, :friends)
 +      get("/statuses/blocks", TwitterAPI.Controller, :blocks)
 +      get("/statuses/show/:id", TwitterAPI.Controller, :fetch_status)
 +      get("/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation)
  
 -    get("/search", TwitterAPI.Controller, :search)
 -    get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline)
 +      get("/search", TwitterAPI.Controller, :search)
 +      get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline)
 +    end
    end
  
    scope "/api", Pleroma.Web do
 -    pipe_through(:api)
 +    pipe_through([:api, :oauth_read_or_unauthenticated])
  
      get("/statuses/public_timeline", TwitterAPI.Controller, :public_timeline)
  
    end
  
    scope "/api", Pleroma.Web, as: :twitter_api_search do
 -    pipe_through(:api)
 +    pipe_through([:api, :oauth_read_or_unauthenticated])
      get("/pleroma/search_user", TwitterAPI.Controller, :search_user)
    end
  
    scope "/api", Pleroma.Web, as: :authenticated_twitter_api do
      pipe_through(:authenticated_api)
  
 -    get("/account/verify_credentials", TwitterAPI.Controller, :verify_credentials)
 -    post("/account/verify_credentials", TwitterAPI.Controller, :verify_credentials)
 +    scope [] do
 +      pipe_through(:oauth_read)
  
 -    post("/account/update_profile", TwitterAPI.Controller, :update_profile)
 -    post("/account/update_profile_banner", TwitterAPI.Controller, :update_banner)
 -    post("/qvitter/update_background_image", TwitterAPI.Controller, :update_background)
 +      get("/account/verify_credentials", TwitterAPI.Controller, :verify_credentials)
 +      post("/account/verify_credentials", TwitterAPI.Controller, :verify_credentials)
  
 -    get("/statuses/home_timeline", TwitterAPI.Controller, :friends_timeline)
 -    get("/statuses/friends_timeline", TwitterAPI.Controller, :friends_timeline)
 -    get("/statuses/mentions", TwitterAPI.Controller, :mentions_timeline)
 -    get("/statuses/mentions_timeline", TwitterAPI.Controller, :mentions_timeline)
 -    get("/statuses/dm_timeline", TwitterAPI.Controller, :dm_timeline)
 -    get("/qvitter/statuses/notifications", TwitterAPI.Controller, :notifications)
 +      get("/statuses/home_timeline", TwitterAPI.Controller, :friends_timeline)
 +      get("/statuses/friends_timeline", TwitterAPI.Controller, :friends_timeline)
 +      get("/statuses/mentions", TwitterAPI.Controller, :mentions_timeline)
 +      get("/statuses/mentions_timeline", TwitterAPI.Controller, :mentions_timeline)
 +      get("/statuses/dm_timeline", TwitterAPI.Controller, :dm_timeline)
 +      get("/qvitter/statuses/notifications", TwitterAPI.Controller, :notifications)
  
 -    # XXX: this is really a pleroma API, but we want to keep the pleroma namespace clean
 -    #      for now.
 -    post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
 +      get("/pleroma/friend_requests", TwitterAPI.Controller, :friend_requests)
  
 -    post("/statuses/update", TwitterAPI.Controller, :status_update)
 -    post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet)
 -    post("/statuses/unretweet/:id", TwitterAPI.Controller, :unretweet)
 -    post("/statuses/destroy/:id", TwitterAPI.Controller, :delete_post)
 +      get("/friends/ids", TwitterAPI.Controller, :friends_ids)
 +      get("/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array)
  
 -    post("/statuses/pin/:id", TwitterAPI.Controller, :pin)
 -    post("/statuses/unpin/:id", TwitterAPI.Controller, :unpin)
 +      get("/mutes/users/ids", TwitterAPI.Controller, :empty_array)
 +      get("/qvitter/mutes", TwitterAPI.Controller, :raw_empty_array)
  
 -    get("/pleroma/friend_requests", TwitterAPI.Controller, :friend_requests)
 -    post("/pleroma/friendships/approve", TwitterAPI.Controller, :approve_friend_request)
 -    post("/pleroma/friendships/deny", TwitterAPI.Controller, :deny_friend_request)
 +      get("/externalprofile/show", TwitterAPI.Controller, :external_profile)
 +
 +      post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
 +    end
  
 -    post("/friendships/create", TwitterAPI.Controller, :follow)
 -    post("/friendships/destroy", TwitterAPI.Controller, :unfollow)
 -    post("/blocks/create", TwitterAPI.Controller, :block)
 -    post("/blocks/destroy", TwitterAPI.Controller, :unblock)
 +    scope [] do
 +      pipe_through(:oauth_write)
  
 -    post("/statusnet/media/upload", TwitterAPI.Controller, :upload)
 -    post("/media/upload", TwitterAPI.Controller, :upload_json)
 -    post("/media/metadata/create", TwitterAPI.Controller, :update_media)
 +      post("/account/update_profile", TwitterAPI.Controller, :update_profile)
 +      post("/account/update_profile_banner", TwitterAPI.Controller, :update_banner)
 +      post("/qvitter/update_background_image", TwitterAPI.Controller, :update_background)
  
 -    post("/favorites/create/:id", TwitterAPI.Controller, :favorite)
 -    post("/favorites/create", TwitterAPI.Controller, :favorite)
 -    post("/favorites/destroy/:id", TwitterAPI.Controller, :unfavorite)
 +      post("/statuses/update", TwitterAPI.Controller, :status_update)
 +      post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet)
 +      post("/statuses/unretweet/:id", TwitterAPI.Controller, :unretweet)
 +      post("/statuses/destroy/:id", TwitterAPI.Controller, :delete_post)
  
 -    post("/qvitter/update_avatar", TwitterAPI.Controller, :update_avatar)
 +      post("/statuses/pin/:id", TwitterAPI.Controller, :pin)
 +      post("/statuses/unpin/:id", TwitterAPI.Controller, :unpin)
 +
 +      post("/statusnet/media/upload", TwitterAPI.Controller, :upload)
 +      post("/media/upload", TwitterAPI.Controller, :upload_json)
 +      post("/media/metadata/create", TwitterAPI.Controller, :update_media)
 +
 +      post("/favorites/create/:id", TwitterAPI.Controller, :favorite)
 +      post("/favorites/create", TwitterAPI.Controller, :favorite)
 +      post("/favorites/destroy/:id", TwitterAPI.Controller, :unfavorite)
 +
 +      post("/qvitter/update_avatar", TwitterAPI.Controller, :update_avatar)
 +    end
  
 -    get("/friends/ids", TwitterAPI.Controller, :friends_ids)
 -    get("/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array)
 +    scope [] do
 +      pipe_through(:oauth_follow)
  
 -    get("/mutes/users/ids", TwitterAPI.Controller, :empty_array)
 -    get("/qvitter/mutes", TwitterAPI.Controller, :raw_empty_array)
 +      post("/pleroma/friendships/approve", TwitterAPI.Controller, :approve_friend_request)
 +      post("/pleroma/friendships/deny", TwitterAPI.Controller, :deny_friend_request)
  
 -    get("/externalprofile/show", TwitterAPI.Controller, :external_profile)
 +      post("/friendships/create", TwitterAPI.Controller, :follow)
 +      post("/friendships/destroy", TwitterAPI.Controller, :unfollow)
 +
 +      post("/blocks/create", TwitterAPI.Controller, :block)
 +      post("/blocks/destroy", TwitterAPI.Controller, :unblock)
 +    end
    end
  
    pipeline :ap_relay do
    scope "/", Pleroma.Web.ActivityPub do
      pipe_through([:activitypub_client])
  
 -    get("/api/ap/whoami", ActivityPubController, :whoami)
 -    get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
 -    post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
 +    scope [] do
 +      pipe_through(:oauth_read)
 +      get("/api/ap/whoami", ActivityPubController, :whoami)
 +      get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
 +    end
 +
 +    scope [] do
 +      pipe_through(:oauth_write)
 +      post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
 +    end
    end
  
    scope "/relay", Pleroma.Web.ActivityPub do
  
    scope "/", Pleroma.Web.ActivityPub do
      pipe_through(:activitypub)
-     post("/users/:nickname/inbox", ActivityPubController, :inbox)
      post("/inbox", ActivityPubController, :inbox)
+     post("/users/:nickname/inbox", ActivityPubController, :inbox)
    end
  
    scope "/.well-known", Pleroma.Web do
      pipe_through(:mastodon_html)
  
      get("/web/login", MastodonAPIController, :login)
 -    post("/web/login", MastodonAPIController, :login_post)
 -    get("/web/*path", MastodonAPIController, :index)
      delete("/auth/sign_out", MastodonAPIController, :logout)
 +
 +    scope [] do
 +      pipe_through(:oauth_read)
 +      get("/web/*path", MastodonAPIController, :index)
 +    end
    end
  
    pipeline :remote_media do
  
    scope "/proxy/", Pleroma.Web.MediaProxy do
      pipe_through(:remote_media)
 +
      get("/:sig/:url", MediaProxyController, :remote)
      get("/:sig/:url/:filename", MediaProxyController, :remote)
    end
index f944cbc262aa2d8859c158036fb3cac77dda3808,520e4b3d5ceb09c49f06b21e17fd5c9e7dbc492d..db97ccac21445183448f066ea2f2e6229d361921
          border-bottom: 2px solid #4b8ed8;
        }
  
 +      input[type="checkbox"] {
 +        width: auto;
 +      }
 +
        button {
          box-sizing: border-box;
          width: 100%;
          font-weight: 500;
          font-size: 16px;
        }
+       .alert-danger {
+         box-sizing: border-box;
+         width: 100%;
+         color: #D8000C;
+         background-color: #FFD2D2;
+         border-radius: 4px;
+         border: none;
+         padding: 10px;
+         margin-top: 20px;
+         font-weight: 500;
+         font-size: 16px;
+       }
+       .alert-info {
+         box-sizing: border-box;
+         width: 100%;
+         color: #00529B;
+         background-color: #BDE5F8;
+         border-radius: 4px;
+         border: none;
+         padding: 10px;
+         margin-top: 20px;
+         font-weight: 500;
+         font-size: 16px;
+       }
      </style>
    </head>
    <body>
index 6e88efe11f622572a8f736567342f21cdb3ec9b4,32c458f0c4676e8d4986d40c69797c8d406b344b..f50599bdbdab7c0b77dbc4c4fdf81da0f79ffb6f
@@@ -1,28 -1,21 +1,32 @@@
+ <%= if get_flash(@conn, :info) do %>
  <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
+ <% end %>
+ <%= if get_flash(@conn, :error) do %>
  <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
+ <% end %>
  <h2>OAuth Authorization</h2>
  <%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %>
  <%= label f, :name, "Name or email" %>
  <%= text_input f, :name %>
  <br>
 +<br>
  <%= label f, :password, "Password" %>
  <%= password_input f, :password %>
  <br>
 +<br>
 +
 +<%= label f, :scope, "Permissions" %>
 +<br>
 +<%= for scope <- @available_scopes do %>
 +  <%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %>
 +  <%= checkbox f, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: "authorization[scope][]" %>
 +  <%= label f, :"scope_#{scope}", String.capitalize(scope) %>
 +  <br>
 +<% end %>
 +
  <%= hidden_input f, :client_id, value: @client_id %>
  <%= hidden_input f, :response_type, value: @response_type %>
  <%= hidden_input f, :redirect_uri, value: @redirect_uri %>
 -<%= hidden_input f, :scope, value: @scope %>
  <%= hidden_input f, :state, value: @state%>
  <%= submit "Authorize" %>
  <% end %>
index 68db1ceb0ede5200ac1780654268acc6e3b39e8d,81618e9350c2d2c4cacabd3a3514a373fe8ca2c4..b1a51e30ec9b4e680285b70130add9c8dd9841f0
@@@ -4,7 -4,8 +4,8 @@@
  
  defmodule Pleroma.Web.OAuth.AuthorizationTest do
    use Pleroma.DataCase
-   alias Pleroma.Web.OAuth.{Authorization, App}
+   alias Pleroma.Web.OAuth.Authorization
+   alias Pleroma.Web.OAuth.App
    import Pleroma.Factory
  
    test "create an authorization token for a valid app" do
@@@ -12,7 -13,7 +13,7 @@@
        Repo.insert(
          App.register_changeset(%App{}, %{
            client_name: "client",
 -          scopes: "scope",
 +          scopes: ["scope"],
            redirect_uris: "url"
          })
        )
@@@ -32,7 -33,7 +33,7 @@@
        Repo.insert(
          App.register_changeset(%App{}, %{
            client_name: "client",
 -          scopes: "scope",
 +          scopes: ["scope"],
            redirect_uris: "url"
          })
        )
@@@ -65,7 -66,7 +66,7 @@@
        Repo.insert(
          App.register_changeset(%App{}, %{
            client_name: "client",
 -          scopes: "scope",
 +          scopes: ["scope"],
            redirect_uris: "url"
          })
        )
index 74d512c7f7565ecab92a7ea3ddf4d0823ee0c97d,2315f9a34b6186e19da2a5a4650936827b9f7612..ca1c043194b63dc1ff11a61cd7a897d97b483373
@@@ -7,7 -7,8 +7,8 @@@ defmodule Pleroma.Web.OAuth.OAuthContro
    import Pleroma.Factory
  
    alias Pleroma.Repo
-   alias Pleroma.Web.OAuth.{Authorization, Token}
+   alias Pleroma.Web.OAuth.Authorization
+   alias Pleroma.Web.OAuth.Token
  
    test "redirects with oauth authorization" do
      user = insert(:user)
@@@ -21,7 -22,6 +22,7 @@@
            "password" => "test",
            "client_id" => app.client_id,
            "redirect_uri" => app.redirect_uris,
 +          "scope" => Enum.join(app.scopes, " "),
            "state" => "statepassed"
          }
        })
index 63a7eb3ae75366218f1d303887e1ee979131bda0,4dab4a30807d7a1ab80895a7d5c429f47c0b05a9..a708e4991775fcfe51bee17b503045f69c29df98
@@@ -4,7 -4,9 +4,9 @@@
  
  defmodule Pleroma.Web.OAuth.TokenTest do
    use Pleroma.DataCase
-   alias Pleroma.Web.OAuth.{App, Token, Authorization}
+   alias Pleroma.Web.OAuth.App
+   alias Pleroma.Web.OAuth.Authorization
+   alias Pleroma.Web.OAuth.Token
    alias Pleroma.Repo
  
    import Pleroma.Factory
@@@ -14,7 -16,7 +16,7 @@@
        Repo.insert(
          App.register_changeset(%App{}, %{
            client_name: "client",
 -          scopes: "scope",
 +          scopes: ["scope"],
            redirect_uris: "url"
          })
        )
@@@ -39,7 -41,7 +41,7 @@@
        Repo.insert(
          App.register_changeset(%App{}, %{
            client_name: "client1",
 -          scopes: "scope",
 +          scopes: ["scope"],
            redirect_uris: "url"
          })
        )
@@@ -48,7 -50,7 +50,7 @@@
        Repo.insert(
          App.register_changeset(%App{}, %{
            client_name: "client2",
 -          scopes: "scope",
 +          scopes: ["scope"],
            redirect_uris: "url"
          })
        )