about summary refs log tree commit diff
path: root/app/controllers
diff options
context:
space:
mode:
Diffstat (limited to 'app/controllers')
-rw-r--r--app/controllers/admin/account_moderation_notes_controller.rb56
-rw-r--r--app/controllers/admin/accounts_controller.rb31
-rw-r--r--app/controllers/admin/base_controller.rb4
-rw-r--r--app/controllers/admin/confirmations_controller.rb9
-rw-r--r--app/controllers/admin/custom_emojis_controller.rb28
-rw-r--r--app/controllers/admin/domain_blocks_controller.rb9
-rw-r--r--app/controllers/admin/email_domain_blocks_controller.rb5
-rw-r--r--app/controllers/admin/instances_controller.rb2
-rw-r--r--app/controllers/admin/reported_statuses_controller.rb9
-rw-r--r--app/controllers/admin/reports_controller.rb3
-rw-r--r--app/controllers/admin/resets_controller.rb9
-rw-r--r--app/controllers/admin/roles_controller.rb25
-rw-r--r--app/controllers/admin/settings_controller.rb3
-rw-r--r--app/controllers/admin/silences_controller.rb2
-rw-r--r--app/controllers/admin/statuses_controller.rb17
-rw-r--r--app/controllers/admin/subscriptions_controller.rb1
-rw-r--r--app/controllers/admin/suspensions_controller.rb4
-rw-r--r--app/controllers/admin/two_factor_authentications_controller.rb1
-rw-r--r--app/controllers/api/v1/accounts_controller.rb8
-rw-r--r--app/controllers/api/v1/lists/accounts_controller.rb81
-rw-r--r--app/controllers/api/v1/lists_controller.rb79
-rw-r--r--app/controllers/api/v1/mutes_controller.rb31
-rw-r--r--app/controllers/api/v1/notifications_controller.rb9
-rw-r--r--app/controllers/api/v1/reports_controller.rb2
-rw-r--r--app/controllers/api/v1/search_controller.rb18
-rw-r--r--app/controllers/api/v1/timelines/direct_controller.rb60
-rw-r--r--app/controllers/api/v1/timelines/home_controller.rb2
-rw-r--r--app/controllers/api/v1/timelines/list_controller.rb66
-rw-r--r--app/controllers/application_controller.rb12
-rw-r--r--app/controllers/auth/sessions_controller.rb2
-rw-r--r--app/controllers/concerns/authorization.rb1
-rw-r--r--app/controllers/settings/keyword_mutes_controller.rb64
-rw-r--r--app/controllers/settings/notifications_controller.rb2
-rw-r--r--app/controllers/stream_entries_controller.rb2
34 files changed, 591 insertions, 66 deletions
diff --git a/app/controllers/admin/account_moderation_notes_controller.rb b/app/controllers/admin/account_moderation_notes_controller.rb
index 414a875d0..7f69a3363 100644
--- a/app/controllers/admin/account_moderation_notes_controller.rb
+++ b/app/controllers/admin/account_moderation_notes_controller.rb
@@ -1,31 +1,41 @@
 # frozen_string_literal: true
 
-class Admin::AccountModerationNotesController < Admin::BaseController
-  def create
-    @account_moderation_note = current_account.account_moderation_notes.new(resource_params)
-    if @account_moderation_note.save
-      @target_account = @account_moderation_note.target_account
-      redirect_to admin_account_path(@target_account.id), notice: I18n.t('admin.account_moderation_notes.created_msg')
-    else
-      @account = @account_moderation_note.target_account
-      @moderation_notes = @account.targeted_moderation_notes.latest
-      render template: 'admin/accounts/show'
+module Admin
+  class AccountModerationNotesController < BaseController
+    before_action :set_account_moderation_note, only: [:destroy]
+
+    def create
+      authorize AccountModerationNote, :create?
+
+      @account_moderation_note = current_account.account_moderation_notes.new(resource_params)
+
+      if @account_moderation_note.save
+        redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.created_msg')
+      else
+        @account          = @account_moderation_note.target_account
+        @moderation_notes = @account.targeted_moderation_notes.latest
+
+        render template: 'admin/accounts/show'
+      end
     end
-  end
 
-  def destroy
-    @account_moderation_note = AccountModerationNote.find(params[:id])
-    @target_account = @account_moderation_note.target_account
-    @account_moderation_note.destroy
-    redirect_to admin_account_path(@target_account.id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg')
-  end
+    def destroy
+      authorize @account_moderation_note, :destroy?
+      @account_moderation_note.destroy
+      redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg')
+    end
 
-  private
+    private
 
-  def resource_params
-    params.require(:account_moderation_note).permit(
-      :content,
-      :target_account_id
-    )
+    def resource_params
+      params.require(:account_moderation_note).permit(
+        :content,
+        :target_account_id
+      )
+    end
+
+    def set_account_moderation_note
+      @account_moderation_note = AccountModerationNote.find(params[:id])
+    end
   end
 end
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index ffa4dc850..0829bc769 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -2,29 +2,54 @@
 
 module Admin
   class AccountsController < BaseController
-    before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload]
+    before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :enable, :disable, :memorialize]
     before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
+    before_action :require_local_account!, only: [:enable, :disable, :memorialize]
 
     def index
+      authorize :account, :index?
       @accounts = filtered_accounts.page(params[:page])
     end
 
     def show
+      authorize @account, :show?
       @account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
       @moderation_notes = @account.targeted_moderation_notes.latest
     end
 
     def subscribe
+      authorize @account, :subscribe?
       Pubsubhubbub::SubscribeWorker.perform_async(@account.id)
       redirect_to admin_account_path(@account.id)
     end
 
     def unsubscribe
+      authorize @account, :unsubscribe?
       Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id)
       redirect_to admin_account_path(@account.id)
     end
 
+    def memorialize
+      authorize @account, :memorialize?
+      @account.memorialize!
+      redirect_to admin_account_path(@account.id)
+    end
+
+    def enable
+      authorize @account.user, :enable?
+      @account.user.enable!
+      redirect_to admin_account_path(@account.id)
+    end
+
+    def disable
+      authorize @account.user, :disable?
+      @account.user.disable!
+      redirect_to admin_account_path(@account.id)
+    end
+
     def redownload
+      authorize @account, :redownload?
+
       @account.reset_avatar!
       @account.reset_header!
       @account.save!
@@ -42,6 +67,10 @@ module Admin
       redirect_to admin_account_path(@account.id) if @account.local?
     end
 
+    def require_local_account!
+      redirect_to admin_account_path(@account.id) unless @account.local? && @account.user.present?
+    end
+
     def filtered_accounts
       AccountFilter.new(filter_params).results
     end
diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb
index 11fe326bc..db4839a8f 100644
--- a/app/controllers/admin/base_controller.rb
+++ b/app/controllers/admin/base_controller.rb
@@ -2,7 +2,9 @@
 
 module Admin
   class BaseController < ApplicationController
-    before_action :require_admin!
+    include Authorization
+
+    before_action :require_staff!
 
     layout 'admin'
   end
diff --git a/app/controllers/admin/confirmations_controller.rb b/app/controllers/admin/confirmations_controller.rb
index 2542e21ee..c10b0ebee 100644
--- a/app/controllers/admin/confirmations_controller.rb
+++ b/app/controllers/admin/confirmations_controller.rb
@@ -2,15 +2,18 @@
 
 module Admin
   class ConfirmationsController < BaseController
+    before_action :set_user
+
     def create
-      account_user.confirm
+      authorize @user, :confirm?
+      @user.confirm!
       redirect_to admin_accounts_path
     end
 
     private
 
-    def account_user
-      Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
+    def set_user
+      @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
     end
   end
 end
diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb
index 5cce5bce4..509f7a48f 100644
--- a/app/controllers/admin/custom_emojis_controller.rb
+++ b/app/controllers/admin/custom_emojis_controller.rb
@@ -5,14 +5,18 @@ module Admin
     before_action :set_custom_emoji, except: [:index, :new, :create]
 
     def index
-      @custom_emojis = filtered_custom_emojis.page(params[:page])
+      authorize :custom_emoji, :index?
+      @custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page])
     end
 
     def new
+      authorize :custom_emoji, :create?
       @custom_emoji = CustomEmoji.new
     end
 
     def create
+      authorize :custom_emoji, :create?
+
       @custom_emoji = CustomEmoji.new(resource_params)
 
       if @custom_emoji.save
@@ -22,13 +26,27 @@ module Admin
       end
     end
 
+    def update
+      authorize @custom_emoji, :update?
+
+      if @custom_emoji.update(resource_params)
+        redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg')
+      else
+        redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.update_failed_msg')
+      end
+    end
+
     def destroy
+      authorize @custom_emoji, :destroy?
       @custom_emoji.destroy
       redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
     end
 
     def copy
-      emoji = CustomEmoji.new(domain: nil, shortcode: @custom_emoji.shortcode, image: @custom_emoji.image)
+      authorize @custom_emoji, :copy?
+
+      emoji = CustomEmoji.find_or_initialize_by(domain: nil, shortcode: @custom_emoji.shortcode)
+      emoji.image = @custom_emoji.image
 
       if emoji.save
         flash[:notice] = I18n.t('admin.custom_emojis.copied_msg')
@@ -36,15 +54,17 @@ module Admin
         flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
       end
 
-      redirect_to admin_custom_emojis_path(params[:page])
+      redirect_to admin_custom_emojis_path(page: params[:page])
     end
 
     def enable
+      authorize @custom_emoji, :enable?
       @custom_emoji.update!(disabled: false)
       redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.enabled_msg')
     end
 
     def disable
+      authorize @custom_emoji, :disable?
       @custom_emoji.update!(disabled: true)
       redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.disabled_msg')
     end
@@ -56,7 +76,7 @@ module Admin
     end
 
     def resource_params
-      params.require(:custom_emoji).permit(:shortcode, :image)
+      params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker)
     end
 
     def filtered_custom_emojis
diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb
index 1ab620e03..e383dc831 100644
--- a/app/controllers/admin/domain_blocks_controller.rb
+++ b/app/controllers/admin/domain_blocks_controller.rb
@@ -5,14 +5,18 @@ module Admin
     before_action :set_domain_block, only: [:show, :destroy]
 
     def index
+      authorize :domain_block, :index?
       @domain_blocks = DomainBlock.page(params[:page])
     end
 
     def new
+      authorize :domain_block, :create?
       @domain_block = DomainBlock.new
     end
 
     def create
+      authorize :domain_block, :create?
+
       @domain_block = DomainBlock.new(resource_params)
 
       if @domain_block.save
@@ -23,9 +27,12 @@ module Admin
       end
     end
 
-    def show; end
+    def show
+      authorize @domain_block, :show?
+    end
 
     def destroy
+      authorize @domain_block, :destroy?
       UnblockDomainService.new.call(@domain_block, retroactive_unblock?)
       redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg')
     end
diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb
index 09275d5dc..01058bf46 100644
--- a/app/controllers/admin/email_domain_blocks_controller.rb
+++ b/app/controllers/admin/email_domain_blocks_controller.rb
@@ -5,14 +5,18 @@ module Admin
     before_action :set_email_domain_block, only: [:show, :destroy]
 
     def index
+      authorize :email_domain_block, :index?
       @email_domain_blocks = EmailDomainBlock.page(params[:page])
     end
 
     def new
+      authorize :email_domain_block, :create?
       @email_domain_block = EmailDomainBlock.new
     end
 
     def create
+      authorize :email_domain_block, :create?
+
       @email_domain_block = EmailDomainBlock.new(resource_params)
 
       if @email_domain_block.save
@@ -23,6 +27,7 @@ module Admin
     end
 
     def destroy
+      authorize @email_domain_block, :destroy?
       @email_domain_block.destroy
       redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg')
     end
diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb
index 22f02e5d0..8ed0ea421 100644
--- a/app/controllers/admin/instances_controller.rb
+++ b/app/controllers/admin/instances_controller.rb
@@ -3,10 +3,12 @@
 module Admin
   class InstancesController < BaseController
     def index
+      authorize :instance, :index?
       @instances = ordered_instances
     end
 
     def resubscribe
+      authorize :instance, :resubscribe?
       params.require(:by_domain)
       Pubsubhubbub::SubscribeWorker.push_bulk(subscribeable_accounts.pluck(:id))
       redirect_to admin_instances_path
diff --git a/app/controllers/admin/reported_statuses_controller.rb b/app/controllers/admin/reported_statuses_controller.rb
index 5a31adecf..4f66ce708 100644
--- a/app/controllers/admin/reported_statuses_controller.rb
+++ b/app/controllers/admin/reported_statuses_controller.rb
@@ -2,19 +2,20 @@
 
 module Admin
   class ReportedStatusesController < BaseController
-    include Authorization
-
     before_action :set_report
     before_action :set_status, only: [:update, :destroy]
 
     def create
-      @form = Form::StatusBatch.new(form_status_batch_params)
-      flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
+      authorize :status, :update?
+
+      @form         = Form::StatusBatch.new(form_status_batch_params)
+      flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
 
       redirect_to admin_report_path(@report)
     end
 
     def update
+      authorize @status, :update?
       @status.update(status_params)
       redirect_to admin_report_path(@report)
     end
diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb
index 226467739..745757ee8 100644
--- a/app/controllers/admin/reports_controller.rb
+++ b/app/controllers/admin/reports_controller.rb
@@ -5,14 +5,17 @@ module Admin
     before_action :set_report, except: [:index]
 
     def index
+      authorize :report, :index?
       @reports = filtered_reports.page(params[:page])
     end
 
     def show
+      authorize @report, :show?
       @form = Form::StatusBatch.new
     end
 
     def update
+      authorize @report, :update?
       process_report
       redirect_to admin_report_path(@report)
     end
diff --git a/app/controllers/admin/resets_controller.rb b/app/controllers/admin/resets_controller.rb
index 6db648403..00b590bf6 100644
--- a/app/controllers/admin/resets_controller.rb
+++ b/app/controllers/admin/resets_controller.rb
@@ -2,17 +2,18 @@
 
 module Admin
   class ResetsController < BaseController
-    before_action :set_account
+    before_action :set_user
 
     def create
-      @account.user.send_reset_password_instructions
+      authorize @user, :reset_password?
+      @user.send_reset_password_instructions
       redirect_to admin_accounts_path
     end
 
     private
 
-    def set_account
-      @account = Account.find(params[:account_id])
+    def set_user
+      @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
     end
   end
 end
diff --git a/app/controllers/admin/roles_controller.rb b/app/controllers/admin/roles_controller.rb
new file mode 100644
index 000000000..8f8685827
--- /dev/null
+++ b/app/controllers/admin/roles_controller.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Admin
+  class RolesController < BaseController
+    before_action :set_user
+
+    def promote
+      authorize @user, :promote?
+      @user.promote!
+      redirect_to admin_account_path(@user.account_id)
+    end
+
+    def demote
+      authorize @user, :demote?
+      @user.demote!
+      redirect_to admin_account_path(@user.account_id)
+    end
+
+    private
+
+    def set_user
+      @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
+    end
+  end
+end
diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb
index a2f86b8a9..e81290228 100644
--- a/app/controllers/admin/settings_controller.rb
+++ b/app/controllers/admin/settings_controller.rb
@@ -28,10 +28,13 @@ module Admin
     ).freeze
 
     def edit
+      authorize :settings, :show?
       @admin_settings = Form::AdminSettings.new
     end
 
     def update
+      authorize :settings, :update?
+
       settings_params.each do |key, value|
         if UPLOAD_SETTINGS.include?(key)
           upload = SiteUpload.where(var: key).first_or_initialize(var: key)
diff --git a/app/controllers/admin/silences_controller.rb b/app/controllers/admin/silences_controller.rb
index 81a3008b9..01fb292de 100644
--- a/app/controllers/admin/silences_controller.rb
+++ b/app/controllers/admin/silences_controller.rb
@@ -5,11 +5,13 @@ module Admin
     before_action :set_account
 
     def create
+      authorize @account, :silence?
       @account.update(silenced: true)
       redirect_to admin_accounts_path
     end
 
     def destroy
+      authorize @account, :unsilence?
       @account.update(silenced: false)
       redirect_to admin_accounts_path
     end
diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb
index b05000b16..b54a9b824 100644
--- a/app/controllers/admin/statuses_controller.rb
+++ b/app/controllers/admin/statuses_controller.rb
@@ -2,8 +2,6 @@
 
 module Admin
   class StatusesController < BaseController
-    include Authorization
-
     helper_method :current_params
 
     before_action :set_account
@@ -12,24 +10,30 @@ module Admin
     PER_PAGE = 20
 
     def index
+      authorize :status, :index?
+
       @statuses = @account.statuses
+
       if params[:media]
         account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
         @statuses.merge!(Status.where(id: account_media_status_ids))
       end
-      @statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
 
-      @form = Form::StatusBatch.new
+      @statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
+      @form     = Form::StatusBatch.new
     end
 
     def create
-      @form = Form::StatusBatch.new(form_status_batch_params)
-      flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
+      authorize :status, :update?
+
+      @form         = Form::StatusBatch.new(form_status_batch_params)
+      flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
 
       redirect_to admin_account_statuses_path(@account.id, current_params)
     end
 
     def update
+      authorize @status, :update?
       @status.update(status_params)
       redirect_to admin_account_statuses_path(@account.id, current_params)
     end
@@ -60,6 +64,7 @@ module Admin
 
     def current_params
       page = (params[:page] || 1).to_i
+
       {
         media: params[:media],
         page: page > 1 && page,
diff --git a/app/controllers/admin/subscriptions_controller.rb b/app/controllers/admin/subscriptions_controller.rb
index 624a475a3..40500ef43 100644
--- a/app/controllers/admin/subscriptions_controller.rb
+++ b/app/controllers/admin/subscriptions_controller.rb
@@ -3,6 +3,7 @@
 module Admin
   class SubscriptionsController < BaseController
     def index
+      authorize :subscription, :index?
       @subscriptions = ordered_subscriptions.page(requested_page)
     end
 
diff --git a/app/controllers/admin/suspensions_controller.rb b/app/controllers/admin/suspensions_controller.rb
index 5d9048d94..778feea5e 100644
--- a/app/controllers/admin/suspensions_controller.rb
+++ b/app/controllers/admin/suspensions_controller.rb
@@ -5,12 +5,14 @@ module Admin
     before_action :set_account
 
     def create
+      authorize @account, :suspend?
       Admin::SuspensionWorker.perform_async(@account.id)
       redirect_to admin_accounts_path
     end
 
     def destroy
-      @account.update(suspended: false)
+      authorize @account, :unsuspend?
+      @account.unsuspend!
       redirect_to admin_accounts_path
     end
 
diff --git a/app/controllers/admin/two_factor_authentications_controller.rb b/app/controllers/admin/two_factor_authentications_controller.rb
index 69c08f605..5a45d25cd 100644
--- a/app/controllers/admin/two_factor_authentications_controller.rb
+++ b/app/controllers/admin/two_factor_authentications_controller.rb
@@ -5,6 +5,7 @@ module Admin
     before_action :set_user
 
     def destroy
+      authorize @user, :disable_2fa?
       @user.disable_two_factor!
       redirect_to admin_accounts_path
     end
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index b3fc4e561..85eb2d60e 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -13,9 +13,11 @@ class Api::V1::AccountsController < Api::BaseController
   end
 
   def follow
-    FollowService.new.call(current_user.account, @account.acct)
+    reblogs_arg = { reblogs: params[:reblogs] }
+    
+    FollowService.new.call(current_user.account, @account.acct, reblogs_arg)
 
-    options = @account.locked? ? {} : { following_map: { @account.id => true }, requested_map: { @account.id => false } }
+    options = @account.locked? ? {} : { following_map: { @account.id => reblogs_arg }, requested_map: { @account.id => false } }
 
     render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
   end
@@ -26,7 +28,7 @@ class Api::V1::AccountsController < Api::BaseController
   end
 
   def mute
-    MuteService.new.call(current_user.account, @account)
+    MuteService.new.call(current_user.account, @account, notifications: params[:notifications])
     render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
   end
 
diff --git a/app/controllers/api/v1/lists/accounts_controller.rb b/app/controllers/api/v1/lists/accounts_controller.rb
new file mode 100644
index 000000000..40c485e8d
--- /dev/null
+++ b/app/controllers/api/v1/lists/accounts_controller.rb
@@ -0,0 +1,81 @@
+# 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 :require_user!
+  before_action :set_list
+
+  after_action :insert_pagination_headers, only: :show
+
+  def show
+    @accounts = @list.accounts.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
+    render json: @accounts, each_serializer: REST::AccountSerializer
+  end
+
+  def create
+    ApplicationRecord.transaction do
+      list_accounts.each do |account|
+        @list.accounts << account
+      end
+    end
+
+    render_empty
+  end
+
+  def destroy
+    ListAccount.where(list: @list, account_id: account_ids).destroy_all
+    render_empty
+  end
+
+  private
+
+  def set_list
+    @list = List.where(account: current_account).find(params[:list_id])
+  end
+
+  def list_accounts
+    Account.find(account_ids)
+  end
+
+  def account_ids
+    Array(resource_params[:account_ids])
+  end
+
+  def resource_params
+    params.permit(account_ids: [])
+  end
+
+  def insert_pagination_headers
+    set_pagination_headers(next_path, prev_path)
+  end
+
+  def next_path
+    if records_continue?
+      api_v1_list_accounts_url pagination_params(max_id: pagination_max_id)
+    end
+  end
+
+  def prev_path
+    unless @accounts.empty?
+      api_v1_list_accounts_url pagination_params(since_id: pagination_since_id)
+    end
+  end
+
+  def pagination_max_id
+    @accounts.last.id
+  end
+
+  def pagination_since_id
+    @accounts.first.id
+  end
+
+  def records_continue?
+    @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
+  end
+
+  def pagination_params(core_params)
+    params.permit(:limit).merge(core_params)
+  end
+end
diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb
new file mode 100644
index 000000000..9437373bd
--- /dev/null
+++ b/app/controllers/api/v1/lists_controller.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+class Api::V1::ListsController < Api::BaseController
+  LISTS_LIMIT = 50
+
+  before_action -> { doorkeeper_authorize! :read },    only: [:index, :show]
+  before_action -> { doorkeeper_authorize! :write }, except: [:index, :show]
+
+  before_action :require_user!
+  before_action :set_list, except: [:index, :create]
+
+  after_action :insert_pagination_headers, only: :index
+
+  def index
+    @lists = List.where(account: current_account).paginate_by_max_id(limit_param(LISTS_LIMIT), params[:max_id], params[:since_id])
+    render json: @lists, each_serializer: REST::ListSerializer
+  end
+
+  def show
+    render json: @list, serializer: REST::ListSerializer
+  end
+
+  def create
+    @list = List.create!(list_params.merge(account: current_account))
+    render json: @list, serializer: REST::ListSerializer
+  end
+
+  def update
+    @list.update!(list_params)
+    render json: @list, serializer: REST::ListSerializer
+  end
+
+  def destroy
+    @list.destroy!
+    render_empty
+  end
+
+  private
+
+  def set_list
+    @list = List.where(account: current_account).find(params[:id])
+  end
+
+  def list_params
+    params.permit(:title)
+  end
+
+  def insert_pagination_headers
+    set_pagination_headers(next_path, prev_path)
+  end
+
+  def next_path
+    if records_continue?
+      api_v1_lists_url pagination_params(max_id: pagination_max_id)
+    end
+  end
+
+  def prev_path
+    unless @lists.empty?
+      api_v1_lists_url pagination_params(since_id: pagination_since_id)
+    end
+  end
+
+  def pagination_max_id
+    @lists.last.id
+  end
+
+  def pagination_since_id
+    @lists.first.id
+  end
+
+  def records_continue?
+    @lists.size == limit_param(LISTS_LIMIT)
+  end
+
+  def pagination_params(core_params)
+    params.permit(:limit).merge(core_params)
+  end
+end
diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb
index 0c43cb943..92ad251ef 100644
--- a/app/controllers/api/v1/mutes_controller.rb
+++ b/app/controllers/api/v1/mutes_controller.rb
@@ -8,10 +8,15 @@ class Api::V1::MutesController < Api::BaseController
   respond_to :json
 
   def index
-    @accounts = load_accounts
+    @data = @accounts = load_accounts
     render json: @accounts, each_serializer: REST::AccountSerializer
   end
 
+  def details
+    @data = @mutes = load_mutes
+    render json: @mutes, each_serializer: REST::MuteSerializer
+  end 
+
   private
 
   def load_accounts
@@ -22,6 +27,10 @@ class Api::V1::MutesController < Api::BaseController
     Account.includes(:muted_by).references(:muted_by)
   end
 
+  def load_mutes
+    paginated_mutes.includes(:account, :target_account).to_a
+  end
+
   def paginated_mutes
     Mute.where(account: current_account).paginate_by_max_id(
       limit_param(DEFAULT_ACCOUNTS_LIMIT),
@@ -36,26 +45,34 @@ class Api::V1::MutesController < Api::BaseController
 
   def next_path
     if records_continue?
-      api_v1_mutes_url pagination_params(max_id: pagination_max_id)
+      url_for pagination_params(max_id: pagination_max_id)
     end
   end
 
   def prev_path
-    unless @accounts.empty?
-      api_v1_mutes_url pagination_params(since_id: pagination_since_id)
+    unless@data.empty?
+      url_for pagination_params(since_id: pagination_since_id)
     end
   end
 
   def pagination_max_id
-    @accounts.last.muted_by_ids.last
+    if params[:action] == "details"
+      @mutes.last.id
+    else
+      @accounts.last.muted_by_ids.last
+    end
   end
 
   def pagination_since_id
-    @accounts.first.muted_by_ids.first
+    if params[:action] == "details"
+      @mutes.first.id
+    else
+      @accounts.first.muted_by_ids.first
+    end
   end
 
   def records_continue?
-    @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
+    @data.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
   end
 
   def pagination_params(core_params)
diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb
index 8910b77e9..a949752fb 100644
--- a/app/controllers/api/v1/notifications_controller.rb
+++ b/app/controllers/api/v1/notifications_controller.rb
@@ -24,11 +24,20 @@ class Api::V1::NotificationsController < Api::BaseController
     render_empty
   end
 
+  def destroy
+    dismiss
+  end
+
   def dismiss
     current_account.notifications.find_by!(id: params[:id]).destroy!
     render_empty
   end
 
+  def destroy_multiple
+    current_account.notifications.where(id: params[:ids]).destroy_all
+    render_empty
+  end
+
   private
 
   def load_notifications
diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb
index 9592cd4bd..22828217d 100644
--- a/app/controllers/api/v1/reports_controller.rb
+++ b/app/controllers/api/v1/reports_controller.rb
@@ -19,7 +19,7 @@ class Api::V1::ReportsController < Api::BaseController
       comment: report_params[:comment]
     )
 
-    User.admins.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later }
+    User.staff.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later }
 
     render json: @report, serializer: REST::ReportSerializer
   end
diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb
index bc5b8e5d4..d1b4e0402 100644
--- a/app/controllers/api/v1/search_controller.rb
+++ b/app/controllers/api/v1/search_controller.rb
@@ -1,7 +1,9 @@
 # frozen_string_literal: true
 
 class Api::V1::SearchController < Api::BaseController
-  RESULTS_LIMIT = 5
+  include Authorization
+
+  RESULTS_LIMIT = 10
 
   before_action -> { doorkeeper_authorize! :read }
   before_action :require_user!
@@ -9,12 +11,24 @@ class Api::V1::SearchController < Api::BaseController
   respond_to :json
 
   def index
-    @search = Search.new(search_results)
+    @search = Search.new(search)
     render json: @search, serializer: REST::SearchSerializer
   end
 
   private
 
+  def search
+    search_results.tap do |search|
+      search[:statuses].keep_if do |status|
+        begin
+          authorize status, :show?
+        rescue Mastodon::NotPermittedError
+          false
+        end
+      end
+    end
+  end
+
   def search_results
     SearchService.new.call(
       params[:q],
diff --git a/app/controllers/api/v1/timelines/direct_controller.rb b/app/controllers/api/v1/timelines/direct_controller.rb
new file mode 100644
index 000000000..d455227eb
--- /dev/null
+++ b/app/controllers/api/v1/timelines/direct_controller.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+class Api::V1::Timelines::DirectController < Api::BaseController
+  before_action -> { doorkeeper_authorize! :read }, only: [:show]
+  before_action :require_user!, only: [:show]
+  after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
+
+  respond_to :json
+
+  def show
+    @statuses = load_statuses
+    render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
+  end
+
+  private
+
+  def load_statuses
+    cached_direct_statuses
+  end
+
+  def cached_direct_statuses
+    cache_collection direct_statuses, Status
+  end
+
+  def direct_statuses
+    direct_timeline_statuses.paginate_by_max_id(
+      limit_param(DEFAULT_STATUSES_LIMIT),
+      params[:max_id],
+      params[:since_id]
+    )
+  end
+
+  def direct_timeline_statuses
+    Status.as_direct_timeline(current_account)
+  end
+
+  def insert_pagination_headers
+    set_pagination_headers(next_path, prev_path)
+  end
+
+  def pagination_params(core_params)
+    params.permit(:local, :limit).merge(core_params)
+  end
+
+  def next_path
+    api_v1_timelines_direct_url pagination_params(max_id: pagination_max_id)
+  end
+
+  def prev_path
+    api_v1_timelines_direct_url pagination_params(since_id: pagination_since_id)
+  end
+
+  def pagination_max_id
+    @statuses.last.id
+  end
+
+  def pagination_since_id
+    @statuses.first.id
+  end
+end
diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb
index 3dd27710c..db6cd8568 100644
--- a/app/controllers/api/v1/timelines/home_controller.rb
+++ b/app/controllers/api/v1/timelines/home_controller.rb
@@ -31,7 +31,7 @@ class Api::V1::Timelines::HomeController < Api::BaseController
   end
 
   def account_home_feed
-    Feed.new(:home, current_account)
+    HomeFeed.new(current_account)
   end
 
   def insert_pagination_headers
diff --git a/app/controllers/api/v1/timelines/list_controller.rb b/app/controllers/api/v1/timelines/list_controller.rb
new file mode 100644
index 000000000..f5db71e46
--- /dev/null
+++ b/app/controllers/api/v1/timelines/list_controller.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+class Api::V1::Timelines::ListController < Api::BaseController
+  before_action -> { doorkeeper_authorize! :read }
+  before_action :require_user!
+  before_action :set_list
+  before_action :set_statuses
+
+  after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
+
+  def show
+    render json: @statuses,
+           each_serializer: REST::StatusSerializer,
+           relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id)
+  end
+
+  private
+
+  def set_list
+    @list = List.where(account: current_account).find(params[:id])
+  end
+
+  def set_statuses
+    @statuses = cached_list_statuses
+  end
+
+  def cached_list_statuses
+    cache_collection list_statuses, Status
+  end
+
+  def list_statuses
+    list_feed.get(
+      limit_param(DEFAULT_STATUSES_LIMIT),
+      params[:max_id],
+      params[:since_id]
+    )
+  end
+
+  def list_feed
+    ListFeed.new(@list)
+  end
+
+  def insert_pagination_headers
+    set_pagination_headers(next_path, prev_path)
+  end
+
+  def pagination_params(core_params)
+    params.permit(:limit).merge(core_params)
+  end
+
+  def next_path
+    api_v1_timelines_list_url params[:id], pagination_params(max_id: pagination_max_id)
+  end
+
+  def prev_path
+    api_v1_timelines_list_url params[:id], pagination_params(since_id: pagination_since_id)
+  end
+
+  def pagination_max_id
+    @statuses.last.id
+  end
+
+  def pagination_since_id
+    @statuses.first.id
+  end
+end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index d5eca6ffb..f5dbe837e 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -13,11 +13,13 @@ class ApplicationController < ActionController::Base
   helper_method :current_account
   helper_method :current_session
   helper_method :current_theme
+  helper_method :theme_data
   helper_method :single_user_mode?
 
   rescue_from ActionController::RoutingError, with: :not_found
   rescue_from ActiveRecord::RecordNotFound, with: :not_found
   rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
+  rescue_from Mastodon::NotPermittedError, with: :forbidden
 
   before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
   before_action :check_suspension, if: :user_signed_in?
@@ -40,6 +42,10 @@ class ApplicationController < ActionController::Base
     redirect_to root_path unless current_user&.admin?
   end
 
+  def require_staff!
+    redirect_to root_path unless current_user&.staff?
+  end
+
   def check_suspension
     forbidden if current_user.account.suspended?
   end
@@ -83,6 +89,10 @@ class ApplicationController < ActionController::Base
     current_user.setting_theme
   end
 
+  def theme_data
+    Themes.instance.get(current_theme)
+  end
+
   def cache_collection(raw, klass)
     return raw unless klass.respond_to?(:with_includes)
 
@@ -99,7 +109,7 @@ class ApplicationController < ActionController::Base
     unless uncached_ids.empty?
       uncached = klass.where(id: uncached_ids).with_includes.map { |item| [item.id, item] }.to_h
 
-      uncached.values.each do |item|
+      uncached.each_value do |item|
         Rails.cache.write(item.cache_key, item)
       end
     end
diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb
index 463a183e4..a5acb6c36 100644
--- a/app/controllers/auth/sessions_controller.rb
+++ b/app/controllers/auth/sessions_controller.rb
@@ -62,7 +62,7 @@ class Auth::SessionsController < Devise::SessionsController
 
     if user_params[:otp_attempt].present? && session[:otp_user_id]
       authenticate_with_two_factor_via_otp(user)
-    elsif user && user.valid_password?(user_params[:password])
+    elsif user&.valid_password?(user_params[:password])
       prompt_for_two_factor(user)
     end
   end
diff --git a/app/controllers/concerns/authorization.rb b/app/controllers/concerns/authorization.rb
index 7828fe48d..95a37e379 100644
--- a/app/controllers/concerns/authorization.rb
+++ b/app/controllers/concerns/authorization.rb
@@ -2,6 +2,7 @@
 
 module Authorization
   extend ActiveSupport::Concern
+
   include Pundit
 
   def pundit_user
diff --git a/app/controllers/settings/keyword_mutes_controller.rb b/app/controllers/settings/keyword_mutes_controller.rb
new file mode 100644
index 000000000..f79e1b320
--- /dev/null
+++ b/app/controllers/settings/keyword_mutes_controller.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+class Settings::KeywordMutesController < ApplicationController
+  layout 'admin'
+
+  before_action :authenticate_user!
+  before_action :load_keyword_mute, only: [:edit, :update, :destroy]
+
+  def index
+    @keyword_mutes = paginated_keyword_mutes_for_account
+  end
+
+  def new
+    @keyword_mute = keyword_mutes_for_account.build
+  end
+
+  def create
+    @keyword_mute = keyword_mutes_for_account.create(keyword_mute_params)
+
+    if @keyword_mute.persisted?
+      redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg')
+    else
+      render :new
+    end
+  end
+
+  def update
+    if @keyword_mute.update(keyword_mute_params)
+      redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg')
+    else
+      render :edit
+    end
+  end
+
+  def destroy
+    @keyword_mute.destroy!
+
+    redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg')
+  end
+
+  def destroy_all
+    keyword_mutes_for_account.delete_all
+
+    redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg')
+  end
+
+  private
+
+  def keyword_mutes_for_account
+    Glitch::KeywordMute.where(account: current_account)
+  end
+
+  def load_keyword_mute
+    @keyword_mute = keyword_mutes_for_account.find(params[:id])
+  end
+
+  def keyword_mute_params
+    params.require(:keyword_mute).permit(:keyword, :whole_word)
+  end
+
+  def paginated_keyword_mutes_for_account
+    keyword_mutes_for_account.order(:keyword).page params[:page]
+  end
+end
diff --git a/app/controllers/settings/notifications_controller.rb b/app/controllers/settings/notifications_controller.rb
index 09839f16e..ce2530c54 100644
--- a/app/controllers/settings/notifications_controller.rb
+++ b/app/controllers/settings/notifications_controller.rb
@@ -26,7 +26,7 @@ class Settings::NotificationsController < ApplicationController
   def user_settings_params
     params.require(:user).permit(
       notification_emails: %i(follow follow_request reblog favourite mention digest),
-      interactions: %i(must_be_follower must_be_following)
+      interactions: %i(must_be_follower must_be_following must_be_following_dm)
     )
   end
 end
diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb
index cc579dbc8..5f61e2182 100644
--- a/app/controllers/stream_entries_controller.rb
+++ b/app/controllers/stream_entries_controller.rb
@@ -48,7 +48,7 @@ class StreamEntriesController < ApplicationController
     @type         = @stream_entry.activity_type.downcase
 
     raise ActiveRecord::RecordNotFound if @stream_entry.activity.nil?
-    authorize @stream_entry.activity, :show? if @stream_entry.hidden?
+    authorize @stream_entry.activity, :show? if @stream_entry.hidden? || @stream_entry.local_only?
   rescue Mastodon::NotPermittedError
     # Reraise in order to get a 404
     raise ActiveRecord::RecordNotFound