From cdb101340a20183a82889f811d9311c370c855e5 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Fri, 29 Jun 2018 15:34:36 +0200
Subject: Keyword/phrase filtering (#7905)

* Add keyword filtering

    GET|POST       /api/v1/filters
    GET|PUT|DELETE /api/v1/filters/:id

- Irreversible filters can drop toots from home or notifications
- Other filters can hide toots through the client app
- Filters use a phrase valid in particular contexts, expiration

* Make sure expired filters don't get applied client-side

* Add missing API methods

* Remove "regex filter" from column settings

* Add tests

* Add test for FeedManager

* Add CustomFilter test

* Add UI for managing filters

* Add streaming API event to allow syncing filters

* Fix tests
---
 spec/controllers/api/v1/filter_controller_spec.rb | 81 +++++++++++++++++++++++
 1 file changed, 81 insertions(+)
 create mode 100644 spec/controllers/api/v1/filter_controller_spec.rb

(limited to 'spec/controllers/api/v1')

diff --git a/spec/controllers/api/v1/filter_controller_spec.rb b/spec/controllers/api/v1/filter_controller_spec.rb
new file mode 100644
index 000000000..3ffd8f784
--- /dev/null
+++ b/spec/controllers/api/v1/filter_controller_spec.rb
@@ -0,0 +1,81 @@
+require 'rails_helper'
+
+RSpec.describe Api::V1::FiltersController, type: :controller do
+  render_views
+
+  let(:user)  { Fabricate(:user) }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'GET #index' do
+    let!(:filter) { Fabricate(:custom_filter, account: user.account) }
+
+    it 'returns http success' do
+      get :index
+      expect(response).to have_http_status(200)
+    end
+  end
+
+  describe 'POST #create' do
+    before do
+      post :create, params: { phrase: 'magic', context: %w(home), irreversible: true }
+    end
+
+    it 'returns http success' do
+      expect(response).to have_http_status(200)
+    end
+
+    it 'creates a filter' do
+      filter = user.account.custom_filters.first
+      expect(filter).to_not be_nil
+      expect(filter.phrase).to eq 'magic'
+      expect(filter.context).to eq %w(home)
+      expect(filter.irreversible?).to be true
+      expect(filter.expires_at).to be_nil
+    end
+  end
+
+  describe 'GET #show' do
+    let(:filter) { Fabricate(:custom_filter, account: user.account) }
+
+    it 'returns http success' do
+      get :show, params: { id: filter.id }
+      expect(response).to have_http_status(200)
+    end
+  end
+
+  describe 'PUT #update' do
+    let(:filter) { Fabricate(:custom_filter, account: user.account) }
+
+    before do
+      put :update, params: { id: filter.id, phrase: 'updated' }
+    end
+
+    it 'returns http success' do
+      expect(response).to have_http_status(200)
+    end
+
+    it 'updates the filter' do
+      expect(filter.reload.phrase).to eq 'updated'
+    end
+  end
+
+  describe 'DELETE #destroy' do
+    let(:filter) { Fabricate(:custom_filter, account: user.account) }
+
+    before do
+      delete :destroy, params: { id: filter.id }
+    end
+
+    it 'returns http success' do
+      expect(response).to have_http_status(200)
+    end
+
+    it 'removes the filter' do
+      expect { filter.reload }.to raise_error ActiveRecord::RecordNotFound
+    end
+  end
+end
-- 
cgit 


From da8fe8079e13758f45e5ba77cb8023c554ae193c Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Tue, 3 Jul 2018 01:47:56 +0200
Subject: Re-add follow recommendations API (#7918)

* Re-add follow recommendations API

    GET /api/v1/suggestions

Removed in 8efa081f210d72ed450c39ac4cde0fd84fb3d3fb due to Neo4J
dependency. The algorithm uses triadic closures, takes into account
suspensions, blocks, mutes, domain blocks, excludes locked and moved
accounts, and prefers more recently updated accounts.

* Track interactions with people you don't follow

Replying to, favouriting and reblogging someone you're not following
will make them show up in follow recommendations. The interactions
have different weights:

- Replying is 1
- Favouriting is 10 (decidedly positive interaction, but private)
- Reblogging is 20

Following them, muting or blocking will remove them from the list,
obviously.

* Remove triadic closures, ensure potential friendships are trimmed
---
 app/controllers/api/v1/suggestions_controller.rb   | 21 +++++++
 app/lib/potential_friendship_tracker.rb            | 39 ++++++++++++
 app/models/account.rb                              | 29 +--------
 app/models/concerns/account_interactions.rb        | 12 ++++
 app/services/favourite_service.rb                  |  8 +++
 app/services/post_status_service.rb                |  7 +++
 app/services/reblog_service.rb                     |  7 +++
 config/routes.rb                                   |  1 +
 .../api/v1/suggestions_controller_spec.rb          | 35 +++++++++++
 spec/models/account_spec.rb                        | 71 ----------------------
 10 files changed, 131 insertions(+), 99 deletions(-)
 create mode 100644 app/controllers/api/v1/suggestions_controller.rb
 create mode 100644 app/lib/potential_friendship_tracker.rb
 create mode 100644 spec/controllers/api/v1/suggestions_controller_spec.rb

(limited to 'spec/controllers/api/v1')

diff --git a/app/controllers/api/v1/suggestions_controller.rb b/app/controllers/api/v1/suggestions_controller.rb
new file mode 100644
index 000000000..3abccedd5
--- /dev/null
+++ b/app/controllers/api/v1/suggestions_controller.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class Api::V1::SuggestionsController < Api::BaseController
+  include Authorization
+
+  before_action -> { doorkeeper_authorize! :read }
+  before_action :require_user!
+  before_action :set_accounts
+
+  respond_to :json
+
+  def index
+    render json: @accounts, each_serializer: REST::AccountSerializer
+  end
+
+  private
+
+  def set_accounts
+    @accounts = PotentialFriendshipTracker.get(current_account.id, limit: limit_param(DEFAULT_ACCOUNTS_LIMIT))
+  end
+end
diff --git a/app/lib/potential_friendship_tracker.rb b/app/lib/potential_friendship_tracker.rb
new file mode 100644
index 000000000..362482669
--- /dev/null
+++ b/app/lib/potential_friendship_tracker.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+class PotentialFriendshipTracker
+  EXPIRE_AFTER = 90.days.seconds
+  MAX_ITEMS    = 80
+
+  WEIGHTS = {
+    reply: 1,
+    favourite: 10,
+    reblog: 20,
+  }.freeze
+
+  class << self
+    def record(account_id, target_account_id, action)
+      key    = "interactions:#{account_id}"
+      weight = WEIGHTS[action]
+
+      redis.zincrby(key, weight, target_account_id)
+      redis.zremrangebyrank(key, 0, -MAX_ITEMS)
+      redis.expire(key, EXPIRE_AFTER)
+    end
+
+    def remove(account_id, target_account_id)
+      redis.zrem("interactions:#{account_id}", target_account_id)
+    end
+
+    def get(account_id, limit: 20, offset: 0)
+      account_ids = redis.zrevrange("interactions:#{account_id}", offset, limit)
+      return [] if account_ids.empty?
+      Account.searchable.where(id: account_ids)
+    end
+
+    private
+
+    def redis
+      Redis.current
+    end
+  end
+end
diff --git a/app/models/account.rb b/app/models/account.rb
index 40a45b1f8..1f720bf88 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -127,6 +127,7 @@ class Account < ApplicationRecord
   scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
   scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
   scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
+  scope :searchable, -> { where(suspended: false).where(moved_to_account_id: nil) }
 
   delegate :email,
            :unconfirmed_email,
@@ -309,34 +310,6 @@ class Account < ApplicationRecord
       DeliveryFailureTracker.filter(urls)
     end
 
-    def triadic_closures(account, limit: 5, offset: 0)
-      sql = <<-SQL.squish
-        WITH first_degree AS (
-          SELECT target_account_id
-          FROM follows
-          WHERE account_id = :account_id
-        )
-        SELECT accounts.*
-        FROM follows
-        INNER JOIN accounts ON follows.target_account_id = accounts.id
-        WHERE
-          account_id IN (SELECT * FROM first_degree)
-          AND target_account_id NOT IN (SELECT * FROM first_degree)
-          AND target_account_id NOT IN (:excluded_account_ids)
-          AND accounts.suspended = false
-        GROUP BY target_account_id, accounts.id
-        ORDER BY count(account_id) DESC
-        OFFSET :offset
-        LIMIT :limit
-      SQL
-
-      excluded_account_ids = account.excluded_from_timeline_account_ids + [account.id]
-
-      find_by_sql(
-        [sql, { account_id: account.id, excluded_account_ids: excluded_account_ids, limit: limit, offset: offset }]
-      )
-    end
-
     def search_for(terms, limit = 10)
       textsearch, query = generate_query_for_search(terms)
 
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
index ef59f5d15..ee435f956 100644
--- a/app/models/concerns/account_interactions.rb
+++ b/app/models/concerns/account_interactions.rb
@@ -89,10 +89,13 @@ module AccountInteractions
                               .find_or_create_by!(target_account: other_account)
 
     rel.update!(show_reblogs: reblogs)
+    remove_potential_friendship(other_account)
+
     rel
   end
 
   def block!(other_account, uri: nil)
+    remove_potential_friendship(other_account)
     block_relationships.create_with(uri: uri)
                        .find_or_create_by!(target_account: other_account)
   end
@@ -100,10 +103,13 @@ module AccountInteractions
   def mute!(other_account, notifications: nil)
     notifications = true if notifications.nil?
     mute = mute_relationships.create_with(hide_notifications: notifications).find_or_create_by!(target_account: other_account)
+    remove_potential_friendship(other_account)
+
     # When toggling a mute between hiding and allowing notifications, the mute will already exist, so the find_or_create_by! call will return the existing Mute without updating the hide_notifications attribute. Therefore, we check that hide_notifications? is what we want and set it if it isn't.
     if mute.hide_notifications? != notifications
       mute.update!(hide_notifications: notifications)
     end
+
     mute
   end
 
@@ -194,4 +200,10 @@ module AccountInteractions
     lists.joins(account: :user)
          .where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
   end
+
+  private
+
+  def remove_potential_friendship(other_account)
+    PotentialFriendshipTracker.remove(id, other_account.id)
+  end
 end
diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb
index bc2d1547a..6e1ac3ba9 100644
--- a/app/services/favourite_service.rb
+++ b/app/services/favourite_service.rb
@@ -15,7 +15,10 @@ class FavouriteService < BaseService
     return favourite unless favourite.nil?
 
     favourite = Favourite.create!(account: account, status: status)
+
     create_notification(favourite)
+    bump_potential_friendship(account, status)
+
     favourite
   end
 
@@ -33,6 +36,11 @@ class FavouriteService < BaseService
     end
   end
 
+  def bump_potential_friendship(account, status)
+    return if account.following?(status.account_id)
+    PotentialFriendshipTracker.record(account.id, status.account_id, :favourite)
+  end
+
   def build_json(favourite)
     Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
       favourite,
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index 735985725..bad82051a 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -47,6 +47,8 @@ class PostStatusService < BaseService
       redis.setex("idempotency:status:#{account.id}:#{options[:idempotency]}", 3_600, status.id)
     end
 
+    bump_potential_friendship(account, status)
+
     status
   end
 
@@ -79,4 +81,9 @@ class PostStatusService < BaseService
   def redis
     Redis.current
   end
+
+  def bump_potential_friendship(account, status)
+    return if !status.reply? || account.following?(status.account_id)
+    PotentialFriendshipTracker.record(account.id, status.in_reply_to_account_id, :reply)
+  end
 end
diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb
index 3c4e5847f..0ee8bac2f 100644
--- a/app/services/reblog_service.rb
+++ b/app/services/reblog_service.rb
@@ -24,6 +24,8 @@ class ReblogService < BaseService
     ActivityPub::DistributionWorker.perform_async(reblog.id)
 
     create_notification(reblog)
+    bump_potential_friendship(account, reblog)
+
     reblog
   end
 
@@ -41,6 +43,11 @@ class ReblogService < BaseService
     end
   end
 
+  def bump_potential_friendship(account, reblog)
+    return if account.following?(reblog.reblog.account_id)
+    PotentialFriendshipTracker.record(account.id, reblog.reblog.account_id, :reblog)
+  end
+
   def build_json(reblog)
     Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
       reblog,
diff --git a/config/routes.rb b/config/routes.rb
index 5fdd3b390..e59325964 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -246,6 +246,7 @@ Rails.application.routes.draw do
 
       resources :streaming, only: [:index]
       resources :custom_emojis, only: [:index]
+      resources :suggestions, only: [:index]
 
       get '/search', to: 'search#index', as: :search
 
diff --git a/spec/controllers/api/v1/suggestions_controller_spec.rb b/spec/controllers/api/v1/suggestions_controller_spec.rb
new file mode 100644
index 000000000..17f10b04f
--- /dev/null
+++ b/spec/controllers/api/v1/suggestions_controller_spec.rb
@@ -0,0 +1,35 @@
+require 'rails_helper'
+
+RSpec.describe Api::V1::SuggestionsController, type: :controller do
+  render_views
+
+  let(:user)  { Fabricate(:user) }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'GET #index' do
+    let(:bob) { Fabricate(:account) }
+    let(:jeff) { Fabricate(:account) }
+
+    before do
+      PotentialFriendshipTracker.record(user.account_id, bob.id, :reblog)
+      PotentialFriendshipTracker.record(user.account_id, jeff.id, :favourite)
+
+      get :index
+    end
+
+    it 'returns http success' do
+      expect(response).to have_http_status(200)
+    end
+
+    it 'returns accounts' do
+      json = body_as_json
+
+      expect(json.size).to be >= 1
+      expect(json.map { |i| i[:id] }).to include *[bob, jeff].map { |i| i.id.to_s }
+    end
+  end
+end
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index cce659a8a..c50791bcd 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -454,77 +454,6 @@ RSpec.describe Account, type: :model do
     end
   end
 
-  describe '.triadic_closures' do
-    let!(:me) { Fabricate(:account) }
-    let!(:friend) { Fabricate(:account) }
-    let!(:friends_friend) { Fabricate(:account) }
-    let!(:both_follow) { Fabricate(:account) }
-
-    before do
-      me.follow!(friend)
-      friend.follow!(friends_friend)
-
-      me.follow!(both_follow)
-      friend.follow!(both_follow)
-    end
-
-    it 'finds accounts you dont follow which are followed by accounts you do follow' do
-      expect(described_class.triadic_closures(me)).to eq [friends_friend]
-    end
-
-    it 'limits by 5 with offset 0 by defualt' do
-      first_degree = 6.times.map { Fabricate(:account) }
-      matches = 5.times.map { Fabricate(:account) }
-      first_degree.each { |account| me.follow!(account) }
-      matches.each do |match|
-        first_degree.each { |account| account.follow!(match) }
-        first_degree.shift
-      end
-
-      expect(described_class.triadic_closures(me)).to eq matches
-    end
-
-    it 'accepts arbitrary limits' do
-      another_friend = Fabricate(:account)
-      higher_friends_friend = Fabricate(:account)
-      me.follow!(another_friend)
-      friend.follow!(higher_friends_friend)
-      another_friend.follow!(higher_friends_friend)
-
-      expect(described_class.triadic_closures(me, limit: 1)).to eq [higher_friends_friend]
-    end
-
-    it 'acceps arbitrary offset' do
-      another_friend = Fabricate(:account)
-      higher_friends_friend = Fabricate(:account)
-      me.follow!(another_friend)
-      friend.follow!(higher_friends_friend)
-      another_friend.follow!(higher_friends_friend)
-
-      expect(described_class.triadic_closures(me, offset: 1)).to eq [friends_friend]
-    end
-
-    context 'when you block account' do
-      before do
-        me.block!(friends_friend)
-      end
-
-      it 'rejects blocked accounts' do
-        expect(described_class.triadic_closures(me)).to be_empty
-      end
-    end
-
-    context 'when you mute account' do
-      before do
-        me.mute!(friends_friend)
-      end
-
-      it 'rejects muted accounts' do
-        expect(described_class.triadic_closures(me)).to be_empty
-      end
-    end
-  end
-
   describe '#statuses_count' do
     subject { Fabricate(:account) }
 
-- 
cgit 


From 1f6ed4f86ab2aa98bb271b40bf381370fab4fdf2 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Thu, 5 Jul 2018 18:31:35 +0200
Subject: Add more granular OAuth scopes (#7929)

* Add more granular OAuth scopes

* Add human-readable descriptions of the new scopes

* Ensure new scopes look good on the app UI

* Add tests

* Group scopes in screen and color-code dangerous ones

* Fix wrong extra scope
---
 app/controllers/api/base_controller.rb             |  4 ++
 .../api/v1/accounts/credentials_controller.rb      |  4 +-
 .../v1/accounts/follower_accounts_controller.rb    |  2 +-
 .../v1/accounts/following_accounts_controller.rb   |  2 +-
 .../api/v1/accounts/lists_controller.rb            |  2 +-
 .../api/v1/accounts/relationships_controller.rb    |  2 +-
 .../api/v1/accounts/search_controller.rb           |  2 +-
 .../api/v1/accounts/statuses_controller.rb         |  2 +-
 app/controllers/api/v1/accounts_controller.rb      |  7 +++-
 app/controllers/api/v1/blocks_controller.rb        |  2 +-
 app/controllers/api/v1/domain_blocks_controller.rb |  3 +-
 app/controllers/api/v1/favourites_controller.rb    |  2 +-
 app/controllers/api/v1/filters_controller.rb       |  4 +-
 .../api/v1/follow_requests_controller.rb           |  3 +-
 app/controllers/api/v1/follows_controller.rb       |  2 +-
 .../api/v1/lists/accounts_controller.rb            |  4 +-
 app/controllers/api/v1/lists_controller.rb         |  4 +-
 app/controllers/api/v1/media_controller.rb         |  2 +-
 app/controllers/api/v1/mutes_controller.rb         |  2 +-
 app/controllers/api/v1/notifications_controller.rb |  3 +-
 app/controllers/api/v1/reports_controller.rb       |  4 +-
 app/controllers/api/v1/search_controller.rb        |  2 +-
 .../statuses/favourited_by_accounts_controller.rb  |  7 +---
 .../api/v1/statuses/favourites_controller.rb       |  2 +-
 .../api/v1/statuses/mutes_controller.rb            |  2 +-
 app/controllers/api/v1/statuses/pins_controller.rb |  2 +-
 .../statuses/reblogged_by_accounts_controller.rb   |  7 +---
 .../api/v1/statuses/reblogs_controller.rb          |  2 +-
 app/controllers/api/v1/statuses_controller.rb      |  9 +----
 .../api/v1/timelines/direct_controller.rb          |  2 +-
 .../api/v1/timelines/home_controller.rb            |  2 +-
 .../api/v1/timelines/list_controller.rb            |  2 +-
 app/helpers/application_helper.rb                  | 10 +++++
 app/javascript/styles/mastodon/forms.scss          |  4 ++
 app/views/settings/applications/_fields.html.haml  | 17 +++-----
 config/initializers/doorkeeper.rb                  | 27 ++++++++++++-
 config/locales/doorkeeper.en.yml                   | 30 +++++++++++++--
 config/locales/simple_form.en.yml                  |  1 +
 .../api/v1/accounts/credentials_controller_spec.rb |  6 ++-
 .../accounts/follower_accounts_controller_spec.rb  |  2 +-
 .../accounts/following_accounts_controller_spec.rb |  2 +-
 .../api/v1/accounts/lists_controller_spec.rb       |  2 +-
 .../v1/accounts/relationships_controller_spec.rb   |  2 +-
 .../api/v1/accounts/search_controller_spec.rb      |  2 +-
 .../api/v1/accounts/statuses_controller_spec.rb    |  2 +-
 .../controllers/api/v1/accounts_controller_spec.rb | 45 ++++++++++++++++++++--
 spec/controllers/api/v1/blocks_controller_spec.rb  | 14 ++++++-
 .../api/v1/domain_blocks_controller_spec.rb        | 22 ++++++++++-
 .../api/v1/favourites_controller_spec.rb           |  2 +-
 spec/controllers/api/v1/filter_controller_spec.rb  |  8 +++-
 .../api/v1/follow_requests_controller_spec.rb      |  8 +++-
 spec/controllers/api/v1/follows_controller_spec.rb |  2 +-
 .../api/v1/lists/accounts_controller_spec.rb       |  7 +++-
 spec/controllers/api/v1/lists_controller_spec.rb   | 12 +++++-
 spec/controllers/api/v1/media_controller_spec.rb   |  2 +-
 spec/controllers/api/v1/mutes_controller_spec.rb   |  2 +-
 .../api/v1/notifications_controller_spec.rb        | 10 ++++-
 spec/controllers/api/v1/reports_controller_spec.rb |  5 ++-
 spec/controllers/api/v1/search_controller_spec.rb  |  2 +-
 .../favourited_by_accounts_controller_spec.rb      |  2 +-
 .../api/v1/statuses/favourites_controller_spec.rb  |  2 +-
 .../api/v1/statuses/mutes_controller_spec.rb       |  2 +-
 .../api/v1/statuses/pins_controller_spec.rb        |  2 +-
 .../reblogged_by_accounts_controller_spec.rb       |  2 +-
 .../api/v1/statuses/reblogs_controller_spec.rb     |  2 +-
 .../controllers/api/v1/statuses_controller_spec.rb |  7 +++-
 .../api/v1/timelines/home_controller_spec.rb       |  2 +-
 .../api/v1/timelines/list_controller_spec.rb       |  2 +-
 spec/controllers/api/v2/search_controller_spec.rb  | 22 +++++++++++
 69 files changed, 292 insertions(+), 102 deletions(-)
 create mode 100644 spec/controllers/api/v2/search_controller_spec.rb

(limited to 'spec/controllers/api/v1')

diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb
index b5c084e14..770a69921 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -78,4 +78,8 @@ class Api::BaseController < ApplicationController
   def render_empty
     render json: {}, status: 200
   end
+
+  def authorize_if_got_token!(*scopes)
+    doorkeeper_authorize!(*scopes) if doorkeeper_token
+  end
 end
diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb
index 2d0737ee4..dcd41b35c 100644
--- a/app/controllers/api/v1/accounts/credentials_controller.rb
+++ b/app/controllers/api/v1/accounts/credentials_controller.rb
@@ -1,8 +1,8 @@
 # frozen_string_literal: true
 
 class Api::V1::Accounts::CredentialsController < Api::BaseController
-  before_action -> { doorkeeper_authorize! :read }, except: [:update]
-  before_action -> { doorkeeper_authorize! :write }, only: [:update]
+  before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, except: [:update]
+  before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:update]
   before_action :require_user!
 
   def show
diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb
index 4578cf6ca..daa35769e 100644
--- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb
+++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
-  before_action -> { doorkeeper_authorize! :read }
+  before_action -> { doorkeeper_authorize! :read, :'read:accounts' }
   before_action :set_account
   after_action :insert_pagination_headers
 
diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb
index ce2bbda85..6be97b87e 100644
--- a/app/controllers/api/v1/accounts/following_accounts_controller.rb
+++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
-  before_action -> { doorkeeper_authorize! :read }
+  before_action -> { doorkeeper_authorize! :read, :'read:accounts' }
   before_action :set_account
   after_action :insert_pagination_headers
 
diff --git a/app/controllers/api/v1/accounts/lists_controller.rb b/app/controllers/api/v1/accounts/lists_controller.rb
index a7ba89ce2..72392453c 100644
--- a/app/controllers/api/v1/accounts/lists_controller.rb
+++ b/app/controllers/api/v1/accounts/lists_controller.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class Api::V1::Accounts::ListsController < Api::BaseController
-  before_action -> { doorkeeper_authorize! :read }
+  before_action -> { doorkeeper_authorize! :read, :'read:lists' }
   before_action :require_user!
   before_action :set_account
 
diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb
index 70236d1a8..ab8a0461f 100644
--- a/app/controllers/api/v1/accounts/relationships_controller.rb
+++ b/app/controllers/api/v1/accounts/relationships_controller.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class Api::V1::Accounts::RelationshipsController < Api::BaseController
-  before_action -> { doorkeeper_authorize! :read }
+  before_action -> { doorkeeper_authorize! :read, :'read:follows' }
   before_action :require_user!
 
   respond_to :json
diff --git a/app/controllers/api/v1/accounts/search_controller.rb b/app/controllers/api/v1/accounts/search_controller.rb
index 7649da433..91c9f1547 100644
--- a/app/controllers/api/v1/accounts/search_controller.rb
+++ b/app/controllers/api/v1/accounts/search_controller.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class Api::V1::Accounts::SearchController < Api::BaseController
-  before_action -> { doorkeeper_authorize! :read }
+  before_action -> { doorkeeper_authorize! :read, :'read:accounts' }
   before_action :require_user!
 
   respond_to :json
diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb
index c40155cb5..06fa6c762 100644
--- a/app/controllers/api/v1/accounts/statuses_controller.rb
+++ b/app/controllers/api/v1/accounts/statuses_controller.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class Api::V1::Accounts::StatusesController < Api::BaseController
-  before_action -> { doorkeeper_authorize! :read }
+  before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
   before_action :set_account
   after_action :insert_pagination_headers
 
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index b7133ca8e..1d5372a8c 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -1,8 +1,11 @@
 # frozen_string_literal: true
 
 class Api::V1::AccountsController < Api::BaseController
-  before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock, :mute, :unmute]
-  before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock, :mute, :unmute]
+  before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:follow, :unfollow, :block, :unblock, :mute, :unmute]
+  before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow]
+  before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute]
+  before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, only: [:block, :unblock]
+
   before_action :require_user!, except: [:show]
   before_action :set_account
   before_action :check_account_suspension, only: [:show]
diff --git a/app/controllers/api/v1/blocks_controller.rb b/app/controllers/api/v1/blocks_controller.rb
index a39701340..99c53d59a 100644
--- a/app/controllers/api/v1/blocks_controller.rb
+++ b/app/controllers/api/v1/blocks_controller.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class Api::V1::BlocksController < Api::BaseController
-  before_action -> { doorkeeper_authorize! :follow }
+  before_action -> { doorkeeper_authorize! :follow, :'read:blocks' }
   before_action :require_user!
   after_action :insert_pagination_headers
 
diff --git a/app/controllers/api/v1/domain_blocks_controller.rb b/app/controllers/api/v1/domain_blocks_controller.rb
index e55d622c3..af9e7a20f 100644
--- a/app/controllers/api/v1/domain_blocks_controller.rb
+++ b/app/controllers/api/v1/domain_blocks_controller.rb
@@ -3,7 +3,8 @@
 class Api::V1::DomainBlocksController < Api::BaseController
   BLOCK_LIMIT = 100
 
-  before_action -> { doorkeeper_authorize! :follow }
+  before_action -> { doorkeeper_authorize! :follow, :'read:blocks' }, only: :show
+  before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, except: :show
   before_action :require_user!
   after_action :insert_pagination_headers, only: :show
 
diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb
index b4265ed34..ab5204355 100644
--- a/app/controllers/api/v1/favourites_controller.rb
+++ b/app/controllers/api/v1/favourites_controller.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class Api::V1::FavouritesController < Api::BaseController
-  before_action -> { doorkeeper_authorize! :read }
+  before_action -> { doorkeeper_authorize! :read, :'read:favourites' }
   before_action :require_user!
   after_action :insert_pagination_headers
 
diff --git a/app/controllers/api/v1/filters_controller.rb b/app/controllers/api/v1/filters_controller.rb
index c89722b85..02efd323b 100644
--- a/app/controllers/api/v1/filters_controller.rb
+++ b/app/controllers/api/v1/filters_controller.rb
@@ -1,8 +1,8 @@
 # frozen_string_literal: true
 
 class Api::V1::FiltersController < Api::BaseController
-  before_action -> { doorkeeper_authorize! :read }, only: [:index, :show]
-  before_action -> { doorkeeper_authorize! :write }, except: [:index, :show]
+  before_action -> { doorkeeper_authorize! :read, :'read:filters' }, only: [:index, :show]
+  before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show]
   before_action :require_user!
   before_action :set_filters, only: :index
   before_action :set_filter, only: [:show, :update, :destroy]
diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb
index d5c7c565a..313fe2f81 100644
--- a/app/controllers/api/v1/follow_requests_controller.rb
+++ b/app/controllers/api/v1/follow_requests_controller.rb
@@ -1,7 +1,8 @@
 # frozen_string_literal: true
 
 class Api::V1::FollowRequestsController < Api::BaseController
-  before_action -> { doorkeeper_authorize! :follow }
+  before_action -> { doorkeeper_authorize! :follow, :'read:follows' }, only: :index
+  before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, except: :index
   before_action :require_user!
   after_action :insert_pagination_headers, only: :index
 
diff --git a/app/controllers/api/v1/follows_controller.rb b/app/controllers/api/v1/follows_controller.rb
index 5a2b2f32f..5420c0533 100644
--- a/app/controllers/api/v1/follows_controller.rb
+++ b/app/controllers/api/v1/follows_controller.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class Api::V1::FollowsController < Api::BaseController
-  before_action -> { doorkeeper_authorize! :follow }
+  before_action -> { doorkeeper_authorize! :follow, :'write:follows' }
   before_action :require_user!
 
   respond_to :json
diff --git a/app/controllers/api/v1/lists/accounts_controller.rb b/app/controllers/api/v1/lists/accounts_controller.rb
index f2bded851..19de56732 100644
--- a/app/controllers/api/v1/lists/accounts_controller.rb
+++ b/app/controllers/api/v1/lists/accounts_controller.rb
@@ -1,8 +1,8 @@
 # frozen_string_literal: true
 
 class Api::V1::Lists::AccountsController < Api::BaseController
-  before_action -> { doorkeeper_authorize! :read },    only: [:show]
-  before_action -> { doorkeeper_authorize! :write }, except: [:show]
+  before_action -> { doorkeeper_authorize! :read, :'read:lists' },    only:  [:show]
+  before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:show]
 
   before_action :require_user!
   before_action :set_list
diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb
index 180a91d81..b42b8b971 100644
--- a/app/controllers/api/v1/lists_controller.rb
+++ b/app/controllers/api/v1/lists_controller.rb
@@ -1,8 +1,8 @@
 # frozen_string_literal: true
 
 class Api::V1::ListsController < Api::BaseController
-  before_action -> { doorkeeper_authorize! :read },    only: [:index, :show]
-  before_action -> { doorkeeper_authorize! :write }, except: [:index, :show]
+  before_action -> { doorkeeper_authorize! :read, :'read:lists' },    only:  [:index, :show]
+  before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:index, :show]
 
   before_action :require_user!
   before_action :set_list, except: [:index, :create]
diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb
index d4e6337e7..aaa93b615 100644
--- a/app/controllers/api/v1/media_controller.rb
+++ b/app/controllers/api/v1/media_controller.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class Api::V1::MediaController < Api::BaseController
-  before_action -> { doorkeeper_authorize! :write }
+  before_action -> { doorkeeper_authorize! :write, :'write:media' }
   before_action :require_user!
 
   include ObfuscateFilename
diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb
index c457408ba..faa7d16cd 100644
--- a/app/controllers/api/v1/mutes_controller.rb
+++ b/app/controllers/api/v1/mutes_controller.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class Api::V1::MutesController < Api::BaseController
-  before_action -> { doorkeeper_authorize! :follow }
+  before_action -> { doorkeeper_authorize! :follow, :'read:mutes' }
   before_action :require_user!
   after_action :insert_pagination_headers
 
diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb
index ebbe0b292..593c8f9a9 100644
--- a/app/controllers/api/v1/notifications_controller.rb
+++ b/app/controllers/api/v1/notifications_controller.rb
@@ -1,7 +1,8 @@
 # frozen_string_literal: true
 
 class Api::V1::NotificationsController < Api::BaseController
-  before_action -> { doorkeeper_authorize! :read }
+  before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, except: [:clear, :dismiss]
+  before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: [:clear, :dismiss]
   before_action :require_user!
   after_action :insert_pagination_headers, only: :index
 
diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb
index f5095e073..a954101cb 100644
--- a/app/controllers/api/v1/reports_controller.rb
+++ b/app/controllers/api/v1/reports_controller.rb
@@ -1,8 +1,8 @@
 # frozen_string_literal: true
 
 class Api::V1::ReportsController < Api::BaseController
-  before_action -> { doorkeeper_authorize! :read }, except: [:create]
-  before_action -> { doorkeeper_authorize! :write }, only:  [:create]
+  before_action -> { doorkeeper_authorize! :read, :'read:reports' }, except: [:create]
+  before_action -> { doorkeeper_authorize! :write, :'write:reports' }, only: [:create]
   before_action :require_user!
 
   respond_to :json
diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb
index 05754d0f2..dc1a37599 100644
--- a/app/controllers/api/v1/search_controller.rb
+++ b/app/controllers/api/v1/search_controller.rb
@@ -5,7 +5,7 @@ class Api::V1::SearchController < Api::BaseController
 
   RESULTS_LIMIT = 5
 
-  before_action -> { doorkeeper_authorize! :read }
+  before_action -> { doorkeeper_authorize! :read, :'read:search' }
   before_action :require_user!
 
   respond_to :json
diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
index 3fe304153..8f4070bc7 100644
--- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
+++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
@@ -3,7 +3,7 @@
 class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
   include Authorization
 
-  before_action :authorize_if_got_token
+  before_action -> { authorize_if_got_token! :read, :'read:accounts' }
   before_action :set_status
   after_action :insert_pagination_headers
 
@@ -71,11 +71,6 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
     raise ActiveRecord::RecordNotFound
   end
 
-  def authorize_if_got_token
-    request_token = Doorkeeper::OAuth::Token.from_request(request, *Doorkeeper.configuration.access_token_methods)
-    doorkeeper_authorize! :read if request_token
-  end
-
   def pagination_params(core_params)
     params.slice(:limit).permit(:limit).merge(core_params)
   end
diff --git a/app/controllers/api/v1/statuses/favourites_controller.rb b/app/controllers/api/v1/statuses/favourites_controller.rb
index 35f8a48cd..cceee9060 100644
--- a/app/controllers/api/v1/statuses/favourites_controller.rb
+++ b/app/controllers/api/v1/statuses/favourites_controller.rb
@@ -3,7 +3,7 @@
 class Api::V1::Statuses::FavouritesController < Api::BaseController
   include Authorization
 
-  before_action -> { doorkeeper_authorize! :write }
+  before_action -> { doorkeeper_authorize! :write, :'write:favourites' }
   before_action :require_user!
 
   respond_to :json
diff --git a/app/controllers/api/v1/statuses/mutes_controller.rb b/app/controllers/api/v1/statuses/mutes_controller.rb
index a4bf0acdd..b02469b4f 100644
--- a/app/controllers/api/v1/statuses/mutes_controller.rb
+++ b/app/controllers/api/v1/statuses/mutes_controller.rb
@@ -3,7 +3,7 @@
 class Api::V1::Statuses::MutesController < Api::BaseController
   include Authorization
 
-  before_action -> { doorkeeper_authorize! :write }
+  before_action -> { doorkeeper_authorize! :write, :'write:mutes' }
   before_action :require_user!
   before_action :set_status
   before_action :set_conversation
diff --git a/app/controllers/api/v1/statuses/pins_controller.rb b/app/controllers/api/v1/statuses/pins_controller.rb
index 54f8be667..4118a8ce4 100644
--- a/app/controllers/api/v1/statuses/pins_controller.rb
+++ b/app/controllers/api/v1/statuses/pins_controller.rb
@@ -3,7 +3,7 @@
 class Api::V1::Statuses::PinsController < Api::BaseController
   include Authorization
 
-  before_action -> { doorkeeper_authorize! :write }
+  before_action -> { doorkeeper_authorize! :write, :'write:accounts' }
   before_action :require_user!
   before_action :set_status
 
diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
index b065db2c7..93b83ce48 100644
--- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
+++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
@@ -3,7 +3,7 @@
 class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
   include Authorization
 
-  before_action :authorize_if_got_token
+  before_action -> { authorize_if_got_token! :read, :'read:accounts' }
   before_action :set_status
   after_action :insert_pagination_headers
 
@@ -68,11 +68,6 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
     raise ActiveRecord::RecordNotFound
   end
 
-  def authorize_if_got_token
-    request_token = Doorkeeper::OAuth::Token.from_request(request, *Doorkeeper.configuration.access_token_methods)
-    doorkeeper_authorize! :read if request_token
-  end
-
   def pagination_params(core_params)
     params.slice(:limit).permit(:limit).merge(core_params)
   end
diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb
index 634af474f..04847a6b7 100644
--- a/app/controllers/api/v1/statuses/reblogs_controller.rb
+++ b/app/controllers/api/v1/statuses/reblogs_controller.rb
@@ -3,7 +3,7 @@
 class Api::V1::Statuses::ReblogsController < Api::BaseController
   include Authorization
 
-  before_action -> { doorkeeper_authorize! :write }
+  before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
   before_action :require_user!
 
   respond_to :json
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index 289d91045..c6925d462 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -3,8 +3,8 @@
 class Api::V1::StatusesController < Api::BaseController
   include Authorization
 
-  before_action :authorize_if_got_token, except:            [:create, :destroy]
-  before_action -> { doorkeeper_authorize! :write }, only:  [:create, :destroy]
+  before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy]
+  before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only:   [:create, :destroy]
   before_action :require_user!, except:  [:show, :context, :card]
   before_action :set_status, only:       [:show, :context, :card]
 
@@ -84,9 +84,4 @@ class Api::V1::StatusesController < Api::BaseController
   def pagination_params(core_params)
     params.slice(:limit).permit(:limit).merge(core_params)
   end
-
-  def authorize_if_got_token
-    request_token = Doorkeeper::OAuth::Token.from_request(request, *Doorkeeper.configuration.access_token_methods)
-    doorkeeper_authorize! :read if request_token
-  end
 end
diff --git a/app/controllers/api/v1/timelines/direct_controller.rb b/app/controllers/api/v1/timelines/direct_controller.rb
index ef64078be..d8a76d153 100644
--- a/app/controllers/api/v1/timelines/direct_controller.rb
+++ b/app/controllers/api/v1/timelines/direct_controller.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class Api::V1::Timelines::DirectController < Api::BaseController
-  before_action -> { doorkeeper_authorize! :read }, only: [:show]
+  before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:show]
   before_action :require_user!, only: [:show]
   after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
 
diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb
index cde4e8420..4412aaaa3 100644
--- a/app/controllers/api/v1/timelines/home_controller.rb
+++ b/app/controllers/api/v1/timelines/home_controller.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class Api::V1::Timelines::HomeController < Api::BaseController
-  before_action -> { doorkeeper_authorize! :read }, only: [:show]
+  before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:show]
   before_action :require_user!, only: [:show]
   after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
 
diff --git a/app/controllers/api/v1/timelines/list_controller.rb b/app/controllers/api/v1/timelines/list_controller.rb
index 06d596c08..cfc5f3b5e 100644
--- a/app/controllers/api/v1/timelines/list_controller.rb
+++ b/app/controllers/api/v1/timelines/list_controller.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class Api::V1::Timelines::ListController < Api::BaseController
-  before_action -> { doorkeeper_authorize! :read }
+  before_action -> { doorkeeper_authorize! :read, :'read:lists' }
   before_action :require_user!
   before_action :set_list
   before_action :set_statuses
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 95863ab1f..327901e4e 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -1,6 +1,12 @@
 # frozen_string_literal: true
 
 module ApplicationHelper
+  DANGEROUS_SCOPES = %w(
+    read
+    write
+    follow
+  ).freeze
+
   def active_nav_class(path)
     current_page?(path) ? 'active' : ''
   end
@@ -43,6 +49,10 @@ module ApplicationHelper
     Rails.env.production? ? site_title : "#{site_title} (Dev)"
   end
 
+  def class_for_scope(scope)
+    'scope-danger' if DANGEROUS_SCOPES.include?(scope.to_s)
+  end
+
   def can?(action, record)
     return false if record.nil?
     policy(record).public_send("#{action}?")
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index e4fd6c1f1..458eb86e9 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -612,3 +612,7 @@ code {
     display: block;
   }
 }
+
+.scope-danger {
+  color: $warning-red;
+}
diff --git a/app/views/settings/applications/_fields.html.haml b/app/views/settings/applications/_fields.html.haml
index b21f3cca6..db90df349 100644
--- a/app/views/settings/applications/_fields.html.haml
+++ b/app/views/settings/applications/_fields.html.haml
@@ -8,14 +8,9 @@
   %p.hint= t('doorkeeper.applications.help.native_redirect_uri', native_redirect_uri: Doorkeeper.configuration.native_redirect_uri)
 
 .field-group
-  = f.input :scopes,
-    label: t('activerecord.attributes.doorkeeper/application.scopes'),
-    collection: Doorkeeper.configuration.scopes,
-    wrapper: :with_label,
-    include_blank: false,
-    label_method: lambda { |scope| safe_join([scope, content_tag(:span, t("doorkeeper.scopes.#{scope}"), class: 'hint')]) },
-    selected: f.object.scopes.all,
-    required: false,
-    as: :check_boxes,
-    collection_wrapper_tag: 'ul',
-    item_wrapper_tag: 'li'
+  .input.with_block_label
+    %label= t('activerecord.attributes.doorkeeper/application.scopes')
+    %span.hint= t('simple_form.hints.defaults.scopes')
+
+  - Doorkeeper.configuration.scopes.group_by { |s| s.split(':').first }.each do |k, v|
+    = f.input :scopes, label: false, hint: false, collection: v.sort, wrapper: :with_block_label, include_blank: false, label_method: lambda { |scope| safe_join([content_tag(:samp, scope, class: class_for_scope(scope)), content_tag(:span, t("doorkeeper.scopes.#{scope}"), class: 'hint')]) }, selected: f.object.scopes.all, required: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index 469553803..fe2490b32 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -55,7 +55,32 @@ Doorkeeper.configure do
   # For more information go to
   # https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes
   default_scopes  :read
-  optional_scopes :write, :follow, :push
+  optional_scopes :write,
+                  :'write:accounts',
+                  :'write:blocks',
+                  :'write:favourites',
+                  :'write:filters',
+                  :'write:follows',
+                  :'write:lists',
+                  :'write:media',
+                  :'write:mutes',
+                  :'write:notifications',
+                  :'write:reports',
+                  :'write:statuses',
+                  :read,
+                  :'read:accounts',
+                  :'read:blocks',
+                  :'read:favourites',
+                  :'read:filters',
+                  :'read:follows',
+                  :'read:lists',
+                  :'read:mutes',
+                  :'read:notifications',
+                  :'read:reports',
+                  :'read:search',
+                  :'read:statuses',
+                  :follow,
+                  :push
 
   # Change the way client credentials are retrieved from the request object.
   # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml
index eca1fc675..f1fe03716 100644
--- a/config/locales/doorkeeper.en.yml
+++ b/config/locales/doorkeeper.en.yml
@@ -114,7 +114,29 @@ en:
       application:
         title: OAuth authorization required
     scopes:
-      follow: follow, block, unblock and unfollow accounts
-      push: receive push notifications for your account
-      read: read your account's data
-      write: post on your behalf
+      follow: modify account relationships
+      push: receive your push notifications
+      read: read all your account's data
+      read:accounts: see accounts information
+      read:blocks: see your blocks
+      read:favourites: see your favourites
+      read:filters: see your filters
+      read:follows: see your follows
+      read:lists: see your lists
+      read:mutes: see your mutes
+      read:notifications: see your notifications
+      read:reports: see your reports
+      read:search: search on your behalf
+      read:statuses: see all statuses
+      write: modify all your account's data
+      write:accounts: modify your profile
+      write:blocks: block accounts and domains
+      write:favourites: favourite statuses
+      write:filters: create filters
+      write:follows: follow people
+      write:lists: create lists
+      write:media: upload media files
+      write:mutes: mute people and conversations
+      write:notifications: clear your notifications
+      write:reports: report other people
+      write:statuses: publish statuses
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index 59133ea73..49d94bcde 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -20,6 +20,7 @@ en:
           one: <span class="note-counter">1</span> character left
           other: <span class="note-counter">%{count}</span> characters left
         phrase: Will be matched regardless of casing in text or content warning of a toot
+        scopes: Which APIs the application will be allowed to access. If you select a top-level scope, you don't need to select individual ones.
         setting_default_language: The language of your toots can be detected automatically, but it's not always accurate
         setting_hide_network: Who you follow and who follows you will not be shown on your profile
         setting_noindex: Affects your public profile and status pages
diff --git a/spec/controllers/api/v1/accounts/credentials_controller_spec.rb b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb
index 9a52fd14c..727669886 100644
--- a/spec/controllers/api/v1/accounts/credentials_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb
@@ -4,7 +4,7 @@ describe Api::V1::Accounts::CredentialsController do
   render_views
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
 
   context 'with an oauth token' do
     before do
@@ -12,6 +12,8 @@ describe Api::V1::Accounts::CredentialsController do
     end
 
     describe 'GET #show' do
+      let(:scopes) { 'read:accounts' }
+
       it 'returns http success' do
         get :show
         expect(response).to have_http_status(200)
@@ -19,6 +21,8 @@ describe Api::V1::Accounts::CredentialsController do
     end
 
     describe 'PATCH #update' do
+      let(:scopes) { 'write:accounts' }
+
       describe 'with valid data' do
         before do
           allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async)
diff --git a/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb b/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb
index b47af4963..75e0570e9 100644
--- a/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb
@@ -4,7 +4,7 @@ describe Api::V1::Accounts::FollowerAccountsController do
   render_views
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
 
   before do
     Fabricate(:follow, target_account: user.account)
diff --git a/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb b/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb
index 29fd7cd5b..7f7105ad3 100644
--- a/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb
@@ -4,7 +4,7 @@ describe Api::V1::Accounts::FollowingAccountsController do
   render_views
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
 
   before do
     Fabricate(:follow, account: user.account)
diff --git a/spec/controllers/api/v1/accounts/lists_controller_spec.rb b/spec/controllers/api/v1/accounts/lists_controller_spec.rb
index df9fe0e34..baafea8e6 100644
--- a/spec/controllers/api/v1/accounts/lists_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/lists_controller_spec.rb
@@ -4,7 +4,7 @@ describe Api::V1::Accounts::ListsController do
   render_views
 
   let(:user)    { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:lists') }
   let(:account) { Fabricate(:account) }
   let(:list)    { Fabricate(:list, account: user.account) }
 
diff --git a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
index 7e350da7e..fe715ff62 100644
--- a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
@@ -4,7 +4,7 @@ describe Api::V1::Accounts::RelationshipsController do
   render_views
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:follows') }
 
   before do
     allow(controller).to receive(:doorkeeper_token) { token }
diff --git a/spec/controllers/api/v1/accounts/search_controller_spec.rb b/spec/controllers/api/v1/accounts/search_controller_spec.rb
index dbc4b9f3e..8ff2b17de 100644
--- a/spec/controllers/api/v1/accounts/search_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/search_controller_spec.rb
@@ -4,7 +4,7 @@ RSpec.describe Api::V1::Accounts::SearchController, type: :controller do
   render_views
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
 
   before do
     allow(controller).to receive(:doorkeeper_token) { token }
diff --git a/spec/controllers/api/v1/accounts/statuses_controller_spec.rb b/spec/controllers/api/v1/accounts/statuses_controller_spec.rb
index 09bb46937..693cd1ac6 100644
--- a/spec/controllers/api/v1/accounts/statuses_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/statuses_controller_spec.rb
@@ -4,7 +4,7 @@ describe Api::V1::Accounts::StatusesController do
   render_views
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:statuses') }
 
   before do
     allow(controller).to receive(:doorkeeper_token) { token }
diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb
index 7a9e0f8e4..3e54e88a5 100644
--- a/spec/controllers/api/v1/accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts_controller_spec.rb
@@ -3,21 +3,38 @@ require 'rails_helper'
 RSpec.describe Api::V1::AccountsController, type: :controller do
   render_views
 
-  let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow read') }
+  let(:user)   { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+  let(:scopes) { '' }
+  let(:token)  { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
 
   before do
     allow(controller).to receive(:doorkeeper_token) { token }
   end
 
+  shared_examples 'forbidden for wrong scope' do |wrong_scope|
+    let(:scopes) { wrong_scope }
+
+    it 'returns http forbidden' do
+      expect(response).to have_http_status(403)
+    end
+  end
+
   describe 'GET #show' do
-    it 'returns http success' do
+    let(:scopes) { 'read:accounts' }
+
+    before do
       get :show, params: { id: user.account.id }
+    end
+
+    it 'returns http success' do
       expect(response).to have_http_status(200)
     end
+
+    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
   end
 
   describe 'POST #follow' do
+    let(:scopes) { 'write:follows' }
     let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', locked: locked)).account }
 
     before do
@@ -41,6 +58,8 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
       it 'creates a following relation between user and target user' do
         expect(user.account.following?(other_account)).to be true
       end
+
+      it_behaves_like 'forbidden for wrong scope', 'read:accounts'
     end
 
     context 'with locked account' do
@@ -60,10 +79,13 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
       it 'creates a follow request relation between user and target user' do
         expect(user.account.requested?(other_account)).to be true
       end
+
+      it_behaves_like 'forbidden for wrong scope', 'read:accounts'
     end
   end
 
   describe 'POST #unfollow' do
+    let(:scopes) { 'write:follows' }
     let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
 
     before do
@@ -78,9 +100,12 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
     it 'removes the following relation between user and target user' do
       expect(user.account.following?(other_account)).to be false
     end
+
+    it_behaves_like 'forbidden for wrong scope', 'read:accounts'
   end
 
   describe 'POST #block' do
+    let(:scopes) { 'write:blocks' }
     let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
 
     before do
@@ -99,9 +124,12 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
     it 'creates a blocking relation' do
       expect(user.account.blocking?(other_account)).to be true
     end
+
+    it_behaves_like 'forbidden for wrong scope', 'read:accounts'
   end
 
   describe 'POST #unblock' do
+    let(:scopes) { 'write:blocks' }
     let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
 
     before do
@@ -116,9 +144,12 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
     it 'removes the blocking relation between user and target user' do
       expect(user.account.blocking?(other_account)).to be false
     end
+
+    it_behaves_like 'forbidden for wrong scope', 'read:accounts'
   end
 
   describe 'POST #mute' do
+    let(:scopes) { 'write:mutes' }
     let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
 
     before do
@@ -141,9 +172,12 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
     it 'mutes notifications' do
       expect(user.account.muting_notifications?(other_account)).to be true
     end
+
+    it_behaves_like 'forbidden for wrong scope', 'read:accounts'
   end
 
   describe 'POST #mute with notifications set to false' do
+    let(:scopes) { 'write:mutes' }
     let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
 
     before do
@@ -166,9 +200,12 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
     it 'does not mute notifications' do
       expect(user.account.muting_notifications?(other_account)).to be false
     end
+
+    it_behaves_like 'forbidden for wrong scope', 'read:accounts'
   end
 
   describe 'POST #unmute' do
+    let(:scopes) { 'write:mutes' }
     let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
 
     before do
@@ -183,5 +220,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
     it 'removes the muting relation between user and target user' do
       expect(user.account.muting?(other_account)).to be false
     end
+
+    it_behaves_like 'forbidden for wrong scope', 'read:accounts'
   end
 end
diff --git a/spec/controllers/api/v1/blocks_controller_spec.rb b/spec/controllers/api/v1/blocks_controller_spec.rb
index eff5fb9da..818f76c92 100644
--- a/spec/controllers/api/v1/blocks_controller_spec.rb
+++ b/spec/controllers/api/v1/blocks_controller_spec.rb
@@ -3,8 +3,9 @@ require 'rails_helper'
 RSpec.describe Api::V1::BlocksController, type: :controller do
   render_views
 
-  let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') }
+  let(:user)   { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+  let(:scopes) { 'read:blocks' }
+  let(:token)  { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
 
   before { allow(controller).to receive(:doorkeeper_token) { token } }
 
@@ -49,5 +50,14 @@ RSpec.describe Api::V1::BlocksController, type: :controller do
       get :index
       expect(response).to have_http_status(200)
     end
+
+    context 'with wrong scopes' do
+      let(:scopes) { 'write:blocks' }
+
+      it 'returns http forbidden' do
+        get :index
+        expect(response).to have_http_status(403)
+      end
+    end
   end
 end
diff --git a/spec/controllers/api/v1/domain_blocks_controller_spec.rb b/spec/controllers/api/v1/domain_blocks_controller_spec.rb
index bae4612a2..6a7a35c7a 100644
--- a/spec/controllers/api/v1/domain_blocks_controller_spec.rb
+++ b/spec/controllers/api/v1/domain_blocks_controller_spec.rb
@@ -4,14 +4,24 @@ RSpec.describe Api::V1::DomainBlocksController, type: :controller do
   render_views
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
 
   before do
     user.account.block_domain!('example.com')
     allow(controller).to receive(:doorkeeper_token) { token }
   end
 
+  shared_examples 'forbidden for wrong scope' do |wrong_scope|
+    let(:scopes) { wrong_scope }
+
+    it 'returns http forbidden' do
+      expect(response).to have_http_status(403)
+    end
+  end
+
   describe 'GET #show' do
+    let(:scopes) { 'read:blocks' }
+
     before do
       get :show, params: { limit: 1 }
     end
@@ -23,9 +33,13 @@ RSpec.describe Api::V1::DomainBlocksController, type: :controller do
     it 'returns blocked domains' do
       expect(body_as_json.first).to eq 'example.com'
     end
+
+    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
   end
 
   describe 'POST #create' do
+    let(:scopes) { 'write:blocks' }
+
     before do
       post :create, params: { domain: 'example.org' }
     end
@@ -37,9 +51,13 @@ RSpec.describe Api::V1::DomainBlocksController, type: :controller do
     it 'creates a domain block' do
       expect(user.account.domain_blocking?('example.org')).to be true
     end
+
+    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
   end
 
   describe 'DELETE #destroy' do
+    let(:scopes) { 'write:blocks' }
+
     before do
       delete :destroy, params: { domain: 'example.com' }
     end
@@ -51,5 +69,7 @@ RSpec.describe Api::V1::DomainBlocksController, type: :controller do
     it 'deletes a domain block' do
       expect(user.account.domain_blocking?('example.com')).to be false
     end
+
+    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
   end
 end
diff --git a/spec/controllers/api/v1/favourites_controller_spec.rb b/spec/controllers/api/v1/favourites_controller_spec.rb
index 46cf70f4d..2bdf927f2 100644
--- a/spec/controllers/api/v1/favourites_controller_spec.rb
+++ b/spec/controllers/api/v1/favourites_controller_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe Api::V1::FavouritesController, type: :controller do
       context 'with read scope and valid resource owner' do
         before do
           allow(controller).to receive(:doorkeeper_token) do
-            Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read')
+            Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:favourites')
           end
         end
 
diff --git a/spec/controllers/api/v1/filter_controller_spec.rb b/spec/controllers/api/v1/filter_controller_spec.rb
index 3ffd8f784..5948809e3 100644
--- a/spec/controllers/api/v1/filter_controller_spec.rb
+++ b/spec/controllers/api/v1/filter_controller_spec.rb
@@ -4,13 +4,14 @@ RSpec.describe Api::V1::FiltersController, type: :controller do
   render_views
 
   let(:user)  { Fabricate(:user) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
 
   before do
     allow(controller).to receive(:doorkeeper_token) { token }
   end
 
   describe 'GET #index' do
+    let(:scopes) { 'read:filters' }
     let!(:filter) { Fabricate(:custom_filter, account: user.account) }
 
     it 'returns http success' do
@@ -20,6 +21,8 @@ RSpec.describe Api::V1::FiltersController, type: :controller do
   end
 
   describe 'POST #create' do
+    let(:scopes) { 'write:filters' }
+
     before do
       post :create, params: { phrase: 'magic', context: %w(home), irreversible: true }
     end
@@ -39,6 +42,7 @@ RSpec.describe Api::V1::FiltersController, type: :controller do
   end
 
   describe 'GET #show' do
+    let(:scopes) { 'read:filters' }
     let(:filter) { Fabricate(:custom_filter, account: user.account) }
 
     it 'returns http success' do
@@ -48,6 +52,7 @@ RSpec.describe Api::V1::FiltersController, type: :controller do
   end
 
   describe 'PUT #update' do
+    let(:scopes) { 'write:filters' }
     let(:filter) { Fabricate(:custom_filter, account: user.account) }
 
     before do
@@ -64,6 +69,7 @@ RSpec.describe Api::V1::FiltersController, type: :controller do
   end
 
   describe 'DELETE #destroy' do
+    let(:scopes) { 'write:filters' }
     let(:filter) { Fabricate(:custom_filter, account: user.account) }
 
     before do
diff --git a/spec/controllers/api/v1/follow_requests_controller_spec.rb b/spec/controllers/api/v1/follow_requests_controller_spec.rb
index 3c0b84af8..87292d9ce 100644
--- a/spec/controllers/api/v1/follow_requests_controller_spec.rb
+++ b/spec/controllers/api/v1/follow_requests_controller_spec.rb
@@ -4,7 +4,7 @@ RSpec.describe Api::V1::FollowRequestsController, type: :controller do
   render_views
 
   let(:user)     { Fabricate(:user, account: Fabricate(:account, username: 'alice', locked: true)) }
-  let(:token)    { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') }
+  let(:token)    { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
   let(:follower) { Fabricate(:account, username: 'bob') }
 
   before do
@@ -13,6 +13,8 @@ RSpec.describe Api::V1::FollowRequestsController, type: :controller do
   end
 
   describe 'GET #index' do
+    let(:scopes) { 'read:follows' }
+
     before do
       get :index, params: { limit: 1 }
     end
@@ -23,6 +25,8 @@ RSpec.describe Api::V1::FollowRequestsController, type: :controller do
   end
 
   describe 'POST #authorize' do
+    let(:scopes) { 'write:follows' }
+
     before do
       post :authorize, params: { id: follower.id }
     end
@@ -37,6 +41,8 @@ RSpec.describe Api::V1::FollowRequestsController, type: :controller do
   end
 
   describe 'POST #reject' do
+    let(:scopes) { 'write:follows' }
+
     before do
       post :reject, params: { id: follower.id }
     end
diff --git a/spec/controllers/api/v1/follows_controller_spec.rb b/spec/controllers/api/v1/follows_controller_spec.rb
index 38badb80a..089e0fe5e 100644
--- a/spec/controllers/api/v1/follows_controller_spec.rb
+++ b/spec/controllers/api/v1/follows_controller_spec.rb
@@ -4,7 +4,7 @@ RSpec.describe Api::V1::FollowsController, type: :controller do
   render_views
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:follows') }
 
   before do
     allow(controller).to receive(:doorkeeper_token) { token }
diff --git a/spec/controllers/api/v1/lists/accounts_controller_spec.rb b/spec/controllers/api/v1/lists/accounts_controller_spec.rb
index c37a481d6..08c22de56 100644
--- a/spec/controllers/api/v1/lists/accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/lists/accounts_controller_spec.rb
@@ -4,7 +4,7 @@ describe Api::V1::Lists::AccountsController do
   render_views
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
   let(:list)  { Fabricate(:list, account: user.account) }
 
   before do
@@ -14,6 +14,8 @@ describe Api::V1::Lists::AccountsController do
   end
 
   describe 'GET #index' do
+    let(:scopes) { 'read:lists' }
+
     it 'returns http success' do
       get :show, params: { list_id: list.id }
 
@@ -22,6 +24,7 @@ describe Api::V1::Lists::AccountsController do
   end
 
   describe 'POST #create' do
+    let(:scopes) { 'write:lists' }
     let(:bob) { Fabricate(:account, username: 'bob') }
 
     before do
@@ -39,6 +42,8 @@ describe Api::V1::Lists::AccountsController do
   end
 
   describe 'DELETE #destroy' do
+    let(:scopes) { 'write:lists' }
+
     before do
       delete :destroy, params: { list_id: list.id, account_ids: [list.accounts.first.id] }
     end
diff --git a/spec/controllers/api/v1/lists_controller_spec.rb b/spec/controllers/api/v1/lists_controller_spec.rb
index 213429581..e92213789 100644
--- a/spec/controllers/api/v1/lists_controller_spec.rb
+++ b/spec/controllers/api/v1/lists_controller_spec.rb
@@ -4,12 +4,14 @@ RSpec.describe Api::V1::ListsController, type: :controller do
   render_views
 
   let!(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let!(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') }
+  let!(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
   let!(:list)  { Fabricate(:list, account: user.account) }
 
   before { allow(controller).to receive(:doorkeeper_token) { token } }
 
   describe 'GET #index' do
+    let(:scopes) { 'read:lists' }
+
     it 'returns http success' do
       get :index
       expect(response).to have_http_status(200)
@@ -17,6 +19,8 @@ RSpec.describe Api::V1::ListsController, type: :controller do
   end
 
   describe 'GET #show' do
+    let(:scopes) { 'read:lists' }
+
     it 'returns http success' do
       get :show, params: { id: list.id }
       expect(response).to have_http_status(200)
@@ -24,6 +28,8 @@ RSpec.describe Api::V1::ListsController, type: :controller do
   end
 
   describe 'POST #create' do
+    let(:scopes) { 'write:lists' }
+
     before do
       post :create, params: { title: 'Foo bar' }
     end
@@ -39,6 +45,8 @@ RSpec.describe Api::V1::ListsController, type: :controller do
   end
 
   describe 'PUT #update' do
+    let(:scopes) { 'write:lists' }
+
     before do
       put :update, params: { id: list.id, title: 'Updated title' }
     end
@@ -53,6 +61,8 @@ RSpec.describe Api::V1::ListsController, type: :controller do
   end
 
   describe 'DELETE #destroy' do
+    let(:scopes) { 'write:lists' }
+
     before do
       delete :destroy, params: { id: list.id }
     end
diff --git a/spec/controllers/api/v1/media_controller_spec.rb b/spec/controllers/api/v1/media_controller_spec.rb
index ce260eb90..f01fcd942 100644
--- a/spec/controllers/api/v1/media_controller_spec.rb
+++ b/spec/controllers/api/v1/media_controller_spec.rb
@@ -4,7 +4,7 @@ RSpec.describe Api::V1::MediaController, type: :controller do
   render_views
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write') }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:media') }
 
   before do
     allow(controller).to receive(:doorkeeper_token) { token }
diff --git a/spec/controllers/api/v1/mutes_controller_spec.rb b/spec/controllers/api/v1/mutes_controller_spec.rb
index dc4a9753a..f9603b7ff 100644
--- a/spec/controllers/api/v1/mutes_controller_spec.rb
+++ b/spec/controllers/api/v1/mutes_controller_spec.rb
@@ -4,7 +4,7 @@ RSpec.describe Api::V1::MutesController, type: :controller do
   render_views
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:mutes') }
 
   before do
     Fabricate(:mute, account: user.account, hide_notifications: false)
diff --git a/spec/controllers/api/v1/notifications_controller_spec.rb b/spec/controllers/api/v1/notifications_controller_spec.rb
index 2e6163fcd..9f679cb8a 100644
--- a/spec/controllers/api/v1/notifications_controller_spec.rb
+++ b/spec/controllers/api/v1/notifications_controller_spec.rb
@@ -4,7 +4,7 @@ RSpec.describe Api::V1::NotificationsController, type: :controller do
   render_views
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
   let(:other) { Fabricate(:user, account: Fabricate(:account, username: 'bob')) }
 
   before do
@@ -12,6 +12,8 @@ RSpec.describe Api::V1::NotificationsController, type: :controller do
   end
 
   describe 'GET #show' do
+    let(:scopes) { 'read:notifications' }
+
     it 'returns http success' do
       notification = Fabricate(:notification, account: user.account)
       get :show, params: { id: notification.id }
@@ -21,6 +23,8 @@ RSpec.describe Api::V1::NotificationsController, type: :controller do
   end
 
   describe 'POST #dismiss' do
+    let(:scopes) { 'write:notifications' }
+
     it 'destroys the notification' do
       notification = Fabricate(:notification, account: user.account)
       post :dismiss, params: { id: notification.id }
@@ -31,6 +35,8 @@ RSpec.describe Api::V1::NotificationsController, type: :controller do
   end
 
   describe 'POST #clear' do
+    let(:scopes) { 'write:notifications' }
+
     it 'clears notifications for the account' do
       notification = Fabricate(:notification, account: user.account)
       post :clear
@@ -41,6 +47,8 @@ RSpec.describe Api::V1::NotificationsController, type: :controller do
   end
 
   describe 'GET #index' do
+    let(:scopes) { 'read:notifications' }
+
     before do
       first_status = PostStatusService.new.call(user.account, 'Test')
       @reblog_of_first_status = ReblogService.new.call(other.account, first_status)
diff --git a/spec/controllers/api/v1/reports_controller_spec.rb b/spec/controllers/api/v1/reports_controller_spec.rb
index 1e1ef9308..ac93998c6 100644
--- a/spec/controllers/api/v1/reports_controller_spec.rb
+++ b/spec/controllers/api/v1/reports_controller_spec.rb
@@ -6,13 +6,15 @@ RSpec.describe Api::V1::ReportsController, type: :controller do
   render_views
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
 
   before do
     allow(controller).to receive(:doorkeeper_token) { token }
   end
 
   describe 'GET #index' do
+    let(:scopes) { 'read:reports' }
+
     it 'returns http success' do
       get :index
 
@@ -21,6 +23,7 @@ RSpec.describe Api::V1::ReportsController, type: :controller do
   end
 
   describe 'POST #create' do
+    let(:scopes)  { 'write:reports' }
     let!(:status) { Fabricate(:status) }
     let!(:admin)  { Fabricate(:user, admin: true) }
 
diff --git a/spec/controllers/api/v1/search_controller_spec.rb b/spec/controllers/api/v1/search_controller_spec.rb
index 024703867..c9e544cc7 100644
--- a/spec/controllers/api/v1/search_controller_spec.rb
+++ b/spec/controllers/api/v1/search_controller_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Api::V1::SearchController, type: :controller do
   render_views
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:search') }
 
   before do
     allow(controller).to receive(:doorkeeper_token) { token }
diff --git a/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb b/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb
index c873e05dd..23b5d7de9 100644
--- a/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe Api::V1::Statuses::FavouritedByAccountsController, type: :control
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
   let(:app)   { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: app) }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: app, scopes: 'read:accounts') }
 
   context 'with an oauth token' do
     before do
diff --git a/spec/controllers/api/v1/statuses/favourites_controller_spec.rb b/spec/controllers/api/v1/statuses/favourites_controller_spec.rb
index 53f602616..24a760e20 100644
--- a/spec/controllers/api/v1/statuses/favourites_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/favourites_controller_spec.rb
@@ -7,7 +7,7 @@ describe Api::V1::Statuses::FavouritesController do
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
   let(:app)   { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write', application: app) }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:favourites', application: app) }
 
   context 'with an oauth token' do
     before do
diff --git a/spec/controllers/api/v1/statuses/mutes_controller_spec.rb b/spec/controllers/api/v1/statuses/mutes_controller_spec.rb
index 13b4625d1..966398580 100644
--- a/spec/controllers/api/v1/statuses/mutes_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/mutes_controller_spec.rb
@@ -7,7 +7,7 @@ describe Api::V1::Statuses::MutesController do
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
   let(:app)   { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write', application: app) }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:mutes', application: app) }
 
   context 'with an oauth token' do
     before do
diff --git a/spec/controllers/api/v1/statuses/pins_controller_spec.rb b/spec/controllers/api/v1/statuses/pins_controller_spec.rb
index 8f5b0800b..13405d285 100644
--- a/spec/controllers/api/v1/statuses/pins_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/pins_controller_spec.rb
@@ -7,7 +7,7 @@ describe Api::V1::Statuses::PinsController do
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
   let(:app)   { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write', application: app) }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:accounts', application: app) }
 
   context 'with an oauth token' do
     before do
diff --git a/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb b/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb
index 9c0c2b60c..d758786dc 100644
--- a/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe Api::V1::Statuses::RebloggedByAccountsController, type: :controll
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
   let(:app)   { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: app) }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: app, scopes: 'read:accounts') }
 
   context 'with an oauth token' do
     before do
diff --git a/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb b/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb
index e60f8da2a..d14ca3e8b 100644
--- a/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb
@@ -7,7 +7,7 @@ describe Api::V1::Statuses::ReblogsController do
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
   let(:app)   { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write', application: app) }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:statuses', application: app) }
 
   context 'with an oauth token' do
     before do
diff --git a/spec/controllers/api/v1/statuses_controller_spec.rb b/spec/controllers/api/v1/statuses_controller_spec.rb
index 27e4f4eb2..8bc3b0c67 100644
--- a/spec/controllers/api/v1/statuses_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses_controller_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
 
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
   let(:app)   { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: app, scopes: 'write') }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: app, scopes: scopes) }
 
   context 'with an oauth token' do
     before do
@@ -13,6 +13,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
     end
 
     describe 'GET #show' do
+      let(:scopes) { 'read:statuses' }
       let(:status) { Fabricate(:status, account: user.account) }
 
       it 'returns http success' do
@@ -22,6 +23,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
     end
 
     describe 'GET #context' do
+      let(:scopes) { 'read:statuses' }
       let(:status) { Fabricate(:status, account: user.account) }
 
       before do
@@ -35,6 +37,8 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
     end
 
     describe 'POST #create' do
+      let(:scopes) { 'write:statuses' }
+
       before do
         post :create, params: { status: 'Hello world' }
       end
@@ -45,6 +49,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
     end
 
     describe 'DELETE #destroy' do
+      let(:scopes) { 'write:statuses' }
       let(:status) { Fabricate(:status, account: user.account) }
 
       before do
diff --git a/spec/controllers/api/v1/timelines/home_controller_spec.rb b/spec/controllers/api/v1/timelines/home_controller_spec.rb
index 85b031641..a667c33fa 100644
--- a/spec/controllers/api/v1/timelines/home_controller_spec.rb
+++ b/spec/controllers/api/v1/timelines/home_controller_spec.rb
@@ -12,7 +12,7 @@ describe Api::V1::Timelines::HomeController do
   end
 
   context 'with a user context' do
-    let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
+    let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:statuses') }
 
     describe 'GET #show' do
       before do
diff --git a/spec/controllers/api/v1/timelines/list_controller_spec.rb b/spec/controllers/api/v1/timelines/list_controller_spec.rb
index 1729217c9..93a2be6e6 100644
--- a/spec/controllers/api/v1/timelines/list_controller_spec.rb
+++ b/spec/controllers/api/v1/timelines/list_controller_spec.rb
@@ -13,7 +13,7 @@ describe Api::V1::Timelines::ListController do
   end
 
   context 'with a user context' do
-    let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
+    let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:lists') }
 
     describe 'GET #show' do
       before do
diff --git a/spec/controllers/api/v2/search_controller_spec.rb b/spec/controllers/api/v2/search_controller_spec.rb
new file mode 100644
index 000000000..8ee8753de
--- /dev/null
+++ b/spec/controllers/api/v2/search_controller_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Api::V2::SearchController, type: :controller do
+  render_views
+
+  let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:search') }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      get :index, params: { q: 'test' }
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
-- 
cgit