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/about_controller.rb2
-rw-r--r--app/controllers/accounts_controller.rb36
-rw-r--r--app/controllers/activitypub/collections_controller.rb57
-rw-r--r--app/controllers/activitypub/inboxes_controller.rb4
-rw-r--r--app/controllers/activitypub/outboxes_controller.rb6
-rw-r--r--app/controllers/admin/settings_controller.rb4
-rw-r--r--app/controllers/api/base_controller.rb4
-rw-r--r--app/controllers/api/salmon_controller.rb4
-rw-r--r--app/controllers/api/v1/accounts/credentials_controller.rb2
-rw-r--r--app/controllers/api/v1/accounts/relationships_controller.rb4
-rw-r--r--app/controllers/api/v1/accounts/search_controller.rb4
-rw-r--r--app/controllers/api/v1/accounts/statuses_controller.rb14
-rw-r--r--app/controllers/api/v1/accounts_controller.rb6
-rw-r--r--app/controllers/api/v1/media_controller.rb2
-rw-r--r--app/controllers/api/v1/reports_controller.rb12
-rw-r--r--app/controllers/api/v1/search_controller.rb6
-rw-r--r--app/controllers/api/v1/statuses/pins_controller.rb28
-rw-r--r--app/controllers/api/v1/timelines/public_controller.rb14
-rw-r--r--app/controllers/api/v1/timelines/tag_controller.rb14
-rw-r--r--app/controllers/application_controller.rb7
-rw-r--r--app/controllers/auth/confirmations_controller.rb23
-rw-r--r--app/controllers/auth/omniauth_callbacks_controller.rb33
-rw-r--r--app/controllers/auth/registrations_controller.rb5
-rw-r--r--app/controllers/auth/sessions_controller.rb22
-rw-r--r--app/controllers/authorize_follows_controller.rb2
-rw-r--r--app/controllers/concerns/localized.rb12
-rw-r--r--app/controllers/concerns/signature_authentication.rb11
-rw-r--r--app/controllers/concerns/signature_verification.rb2
-rw-r--r--app/controllers/concerns/user_tracking_concern.rb16
-rw-r--r--app/controllers/follower_accounts_controller.rb2
-rw-r--r--app/controllers/following_accounts_controller.rb2
-rw-r--r--app/controllers/home_controller.rb3
-rw-r--r--app/controllers/media_controller.rb18
-rw-r--r--app/controllers/settings/exports_controller.rb14
-rw-r--r--app/controllers/settings/preferences_controller.rb1
-rw-r--r--app/controllers/settings/two_factor_authentication/confirmations_controller.rb6
-rw-r--r--app/controllers/statuses_controller.rb1
-rw-r--r--app/controllers/stream_entries_controller.rb5
38 files changed, 325 insertions, 83 deletions
diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb
index 8785df14e..7b46b2228 100644
--- a/app/controllers/about_controller.rb
+++ b/app/controllers/about_controller.rb
@@ -36,7 +36,7 @@ class AboutController < ApplicationController
 
   def initial_state_params
     {
-      settings: {},
+      settings: { known_fediverse: Setting.show_known_fediverse_at_about_page },
       token: current_session&.token,
     }
   end
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index c9725ed00..1efaf619b 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class AccountsController < ApplicationController
+  PAGE_SIZE = 20
+
   include AccountControllerConcern
 
   before_action :set_cache_headers
@@ -17,13 +19,16 @@ class AccountsController < ApplicationController
         end
 
         @pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
-        @statuses        = filtered_statuses.paginate_by_max_id(20, params[:max_id], params[:since_id])
+        @statuses        = filtered_status_page(params)
         @statuses        = cache_collection(@statuses, Status)
-        @next_url        = next_url unless @statuses.empty?
+        unless @statuses.empty?
+          @older_url        = older_url if @statuses.last.id > filtered_statuses.last.id
+          @newer_url        = newer_url if @statuses.first.id < filtered_statuses.first.id
+        end
       end
 
       format.atom do
-        @entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
+        @entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id])
         render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? }))
       end
 
@@ -70,13 +75,22 @@ class AccountsController < ApplicationController
     @account = Account.find_local!(params[:username])
   end
 
-  def next_url
+  def older_url
+    ::Rails.logger.info("older: max_id #{@statuses.last.id}, url #{pagination_url(max_id: @statuses.last.id)}")
+    pagination_url(max_id: @statuses.last.id)
+  end
+
+  def newer_url
+    pagination_url(min_id: @statuses.first.id)
+  end
+
+  def pagination_url(max_id: nil, min_id: nil)
     if media_requested?
-      short_account_media_url(@account, max_id: @statuses.last.id)
+      short_account_media_url(@account, max_id: max_id, min_id: min_id)
     elsif replies_requested?
-      short_account_with_replies_url(@account, max_id: @statuses.last.id)
+      short_account_with_replies_url(@account, max_id: max_id, min_id: min_id)
     else
-      short_account_url(@account, max_id: @statuses.last.id)
+      short_account_url(@account, max_id: max_id, min_id: min_id)
     end
   end
 
@@ -87,4 +101,12 @@ class AccountsController < ApplicationController
   def replies_requested?
     request.path.ends_with?('/with_replies')
   end
+
+  def filtered_status_page(params)
+    if params[:min_id].present?
+      filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse
+    else
+      filtered_statuses.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id]).to_a
+    end
+  end
 end
diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb
new file mode 100644
index 000000000..081914016
--- /dev/null
+++ b/app/controllers/activitypub/collections_controller.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+class ActivityPub::CollectionsController < Api::BaseController
+  include SignatureVerification
+
+  before_action :set_account
+  before_action :set_size
+  before_action :set_statuses
+
+  def show
+    render json: collection_presenter,
+           serializer: ActivityPub::CollectionSerializer,
+           adapter: ActivityPub::Adapter,
+           content_type: 'application/activity+json',
+           skip_activities: true
+  end
+
+  private
+
+  def set_account
+    @account = Account.find_local!(params[:account_username])
+  end
+
+  def set_statuses
+    @statuses = scope_for_collection.paginate_by_max_id(20, params[:max_id], params[:since_id])
+    @statuses = cache_collection(@statuses, Status)
+  end
+
+  def set_size
+    case params[:id]
+    when 'featured'
+      @account.pinned_statuses.count
+    else
+      raise ActiveRecord::NotFound
+    end
+  end
+
+  def scope_for_collection
+    case params[:id]
+    when 'featured'
+      @account.statuses.permitted_for(@account, signed_request_account).tap do |scope|
+        scope.merge!(@account.pinned_statuses)
+      end
+    else
+      raise ActiveRecord::NotFound
+    end
+  end
+
+  def collection_presenter
+    ActivityPub::CollectionPresenter.new(
+      id: account_collection_url(@account, params[:id]),
+      type: :ordered,
+      size: @size,
+      items: @statuses
+    )
+  end
+end
diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb
index 76553a162..af51e32d5 100644
--- a/app/controllers/activitypub/inboxes_controller.rb
+++ b/app/controllers/activitypub/inboxes_controller.rb
@@ -11,7 +11,7 @@ class ActivityPub::InboxesController < Api::BaseController
       process_payload
       head 202
     else
-      [signature_verification_failure_reason, 401]
+      render plain: signature_verification_failure_reason, status: 401
     end
   end
 
@@ -28,7 +28,7 @@ class ActivityPub::InboxesController < Api::BaseController
   def upgrade_account
     if signed_request_account.ostatus?
       signed_request_account.update(last_webfingered_at: nil)
-      ResolveRemoteAccountWorker.perform_async(signed_request_account.acct)
+      ResolveAccountWorker.perform_async(signed_request_account.acct)
     end
 
     Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) if signed_request_account.subscribed?
diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb
index 9f97ff622..9ed700c1e 100644
--- a/app/controllers/activitypub/outboxes_controller.rb
+++ b/app/controllers/activitypub/outboxes_controller.rb
@@ -1,13 +1,15 @@
 # frozen_string_literal: true
 
 class ActivityPub::OutboxesController < Api::BaseController
+  include SignatureVerification
+
   before_action :set_account
 
   def show
-    @statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(20, params[:max_id], params[:since_id])
+    @statuses = @account.statuses.permitted_for(@account, signed_request_account).paginate_by_max_id(20, params[:max_id], params[:since_id])
     @statuses = cache_collection(@statuses, Status)
 
-    render json: outbox_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
+    render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
   end
 
   private
diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb
index 487282dc3..ce3208209 100644
--- a/app/controllers/admin/settings_controller.rb
+++ b/app/controllers/admin/settings_controller.rb
@@ -16,9 +16,11 @@ module Admin
       show_staff_badge
       bootstrap_timeline_accounts
       thumbnail
+      hero
       min_invite_role
       activity_api_enabled
       peers_api_enabled
+      show_known_fediverse_at_about_page
     ).freeze
 
     BOOLEAN_SETTINGS = %w(
@@ -28,10 +30,12 @@ module Admin
       show_staff_badge
       activity_api_enabled
       peers_api_enabled
+      show_known_fediverse_at_about_page
     ).freeze
 
     UPLOAD_SETTINGS = %w(
       thumbnail
+      hero
     ).freeze
 
     def edit
diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb
index 52e68ab35..7b5168b31 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -51,6 +51,10 @@ class Api::BaseController < ApplicationController
     [params[:limit].to_i.abs, default_limit * 2].min
   end
 
+  def truthy_param?(key)
+    ActiveModel::Type::Boolean.new.cast(params[key])
+  end
+
   def current_resource_owner
     @current_user ||= User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
   end
diff --git a/app/controllers/api/salmon_controller.rb b/app/controllers/api/salmon_controller.rb
index 143e9d3cd..ac5f3268d 100644
--- a/app/controllers/api/salmon_controller.rb
+++ b/app/controllers/api/salmon_controller.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class Api::SalmonController < Api::BaseController
+  include SignatureVerification
+
   before_action :set_account
   respond_to :txt
 
@@ -9,7 +11,7 @@ class Api::SalmonController < Api::BaseController
       process_salmon
       head 202
     elsif payload.present?
-      [signature_verification_failure_reason, 401]
+      render plain: signature_verification_failure_reason, status: 401
     else
       head 400
     end
diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb
index da534d960..68af22529 100644
--- a/app/controllers/api/v1/accounts/credentials_controller.rb
+++ b/app/controllers/api/v1/accounts/credentials_controller.rb
@@ -20,6 +20,6 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
   private
 
   def account_params
-    params.permit(:display_name, :note, :avatar, :header)
+    params.permit(:display_name, :note, :avatar, :header, :locked)
   end
 end
diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb
index 91a942d75..70236d1a8 100644
--- a/app/controllers/api/v1/accounts/relationships_controller.rb
+++ b/app/controllers/api/v1/accounts/relationships_controller.rb
@@ -10,7 +10,7 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
     accounts = Account.where(id: account_ids).select('id')
     # .where doesn't guarantee that our results are in the same order
     # we requested them, so return the "right" order to the requestor.
-    @accounts = accounts.index_by(&:id).values_at(*account_ids)
+    @accounts = accounts.index_by(&:id).values_at(*account_ids).compact
     render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships
   end
 
@@ -21,6 +21,6 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
   end
 
   def account_ids
-    @_account_ids ||= Array(params[:id]).map(&:to_i)
+    Array(params[:id]).map(&:to_i)
   end
 end
diff --git a/app/controllers/api/v1/accounts/search_controller.rb b/app/controllers/api/v1/accounts/search_controller.rb
index 11e647c3c..7649da433 100644
--- a/app/controllers/api/v1/accounts/search_controller.rb
+++ b/app/controllers/api/v1/accounts/search_controller.rb
@@ -22,8 +22,4 @@ class Api::V1::Accounts::SearchController < Api::BaseController
       following: truthy_param?(:following)
     )
   end
-
-  def truthy_param?(key)
-    params[key] == 'true'
-  end
 end
diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb
index 095f6937b..1e1511a7b 100644
--- a/app/controllers/api/v1/accounts/statuses_controller.rb
+++ b/app/controllers/api/v1/accounts/statuses_controller.rb
@@ -28,9 +28,9 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
 
   def account_statuses
     default_statuses.tap do |statuses|
-      statuses.merge!(only_media_scope) if params[:only_media]
-      statuses.merge!(pinned_scope) if params[:pinned]
-      statuses.merge!(no_replies_scope) if params[:exclude_replies]
+      statuses.merge!(only_media_scope) if truthy_param?(:only_media)
+      statuses.merge!(pinned_scope) if truthy_param?(:pinned)
+      statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
     end
   end
 
@@ -51,7 +51,13 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
   end
 
   def account_media_status_ids
-    @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
+    # `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids.
+    # Also, Avoid getting slow by not narrowing down by `statuses.account_id`.
+    # When narrowing down by `statuses.account_id`, `index_statuses_20180106` will be used
+    # and the table will be joined by `Merge Semi Join`, so the query will be slow.
+    Status.joins(:media_attachments).merge(@account.media_attachments).permitted_for(@account, current_account)
+          .paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
+          .reorder(id: :desc).distinct(:id).pluck(:id)
   end
 
   def pinned_scope
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index 4e73e9e8b..d64325944 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -13,9 +13,9 @@ class Api::V1::AccountsController < Api::BaseController
   end
 
   def follow
-    FollowService.new.call(current_user.account, @account.acct, reblogs: params[:reblogs])
+    FollowService.new.call(current_user.account, @account.acct, reblogs: truthy_param?(:reblogs))
 
-    options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: params[:reblogs] } }, requested_map: { @account.id => false } }
+    options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
 
     render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
   end
@@ -26,7 +26,7 @@ class Api::V1::AccountsController < Api::BaseController
   end
 
   def mute
-    MuteService.new.call(current_user.account, @account, notifications: params[:notifications])
+    MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications))
     render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
   end
 
diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb
index 9f330f0df..d4e6337e7 100644
--- a/app/controllers/api/v1/media_controller.rb
+++ b/app/controllers/api/v1/media_controller.rb
@@ -27,7 +27,7 @@ class Api::V1::MediaController < Api::BaseController
   private
 
   def media_params
-    params.permit(:file, :description)
+    params.permit(:file, :description, :focus)
   end
 
   def file_type_error
diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb
index 22828217d..f5095e073 100644
--- a/app/controllers/api/v1/reports_controller.rb
+++ b/app/controllers/api/v1/reports_controller.rb
@@ -13,14 +13,14 @@ class Api::V1::ReportsController < Api::BaseController
   end
 
   def create
-    @report = current_account.reports.create!(
-      target_account: reported_account,
+    @report = ReportService.new.call(
+      current_account,
+      reported_account,
       status_ids: reported_status_ids,
-      comment: report_params[:comment]
+      comment: report_params[:comment],
+      forward: report_params[:forward]
     )
 
-    User.staff.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later }
-
     render json: @report, serializer: REST::ReportSerializer
   end
 
@@ -39,6 +39,6 @@ class Api::V1::ReportsController < Api::BaseController
   end
 
   def report_params
-    params.permit(:account_id, :comment, status_ids: [])
+    params.permit(:account_id, :comment, :forward, status_ids: [])
   end
 end
diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb
index d1b4e0402..99b635ad9 100644
--- a/app/controllers/api/v1/search_controller.rb
+++ b/app/controllers/api/v1/search_controller.rb
@@ -33,12 +33,8 @@ class Api::V1::SearchController < Api::BaseController
     SearchService.new.call(
       params[:q],
       RESULTS_LIMIT,
-      resolving_search?,
+      truthy_param?(:resolve),
       current_account
     )
   end
-
-  def resolving_search?
-    params[:resolve] == 'true'
-  end
 end
diff --git a/app/controllers/api/v1/statuses/pins_controller.rb b/app/controllers/api/v1/statuses/pins_controller.rb
index 3de1009b8..bba6a6f48 100644
--- a/app/controllers/api/v1/statuses/pins_controller.rb
+++ b/app/controllers/api/v1/statuses/pins_controller.rb
@@ -11,12 +11,18 @@ class Api::V1::Statuses::PinsController < Api::BaseController
 
   def create
     StatusPin.create!(account: current_account, status: @status)
+    distribute_add_activity!
     render json: @status, serializer: REST::StatusSerializer
   end
 
   def destroy
     pin = StatusPin.find_by(account: current_account, status: @status)
-    pin&.destroy!
+
+    if pin
+      pin.destroy!
+      distribute_remove_activity!
+    end
+
     render json: @status, serializer: REST::StatusSerializer
   end
 
@@ -25,4 +31,24 @@ class Api::V1::Statuses::PinsController < Api::BaseController
   def set_status
     @status = Status.find(params[:status_id])
   end
+
+  def distribute_add_activity!
+    json = ActiveModelSerializers::SerializableResource.new(
+      @status,
+      serializer: ActivityPub::AddSerializer,
+      adapter: ActivityPub::Adapter
+    ).as_json
+
+    ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account)
+  end
+
+  def distribute_remove_activity!
+    json = ActiveModelSerializers::SerializableResource.new(
+      @status,
+      serializer: ActivityPub::RemoveSerializer,
+      adapter: ActivityPub::Adapter
+    ).as_json
+
+    ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account)
+  end
 end
diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb
index 49887778e..d7d70b94d 100644
--- a/app/controllers/api/v1/timelines/public_controller.rb
+++ b/app/controllers/api/v1/timelines/public_controller.rb
@@ -21,15 +21,23 @@ class Api::V1::Timelines::PublicController < Api::BaseController
   end
 
   def public_statuses
-    public_timeline_statuses.paginate_by_max_id(
+    statuses = public_timeline_statuses.paginate_by_max_id(
       limit_param(DEFAULT_STATUSES_LIMIT),
       params[:max_id],
       params[:since_id]
     )
+
+    if truthy_param?(:only_media)
+      # `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids.
+      status_ids = statuses.joins(:media_attachments).distinct(:id).pluck(:id)
+      statuses.where(id: status_ids)
+    else
+      statuses
+    end
   end
 
   def public_timeline_statuses
-    Status.as_public_timeline(current_account, params[:local])
+    Status.as_public_timeline(current_account, truthy_param?(:local))
   end
 
   def insert_pagination_headers
@@ -37,7 +45,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
   end
 
   def pagination_params(core_params)
-    params.permit(:local, :limit).merge(core_params)
+    params.permit(:local, :limit, :only_media).merge(core_params)
   end
 
   def next_path
diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb
index 08db04a39..eb32611ad 100644
--- a/app/controllers/api/v1/timelines/tag_controller.rb
+++ b/app/controllers/api/v1/timelines/tag_controller.rb
@@ -29,16 +29,24 @@ class Api::V1::Timelines::TagController < Api::BaseController
     if @tag.nil?
       []
     else
-      tag_timeline_statuses.paginate_by_max_id(
+      statuses = tag_timeline_statuses.paginate_by_max_id(
         limit_param(DEFAULT_STATUSES_LIMIT),
         params[:max_id],
         params[:since_id]
       )
+
+      if truthy_param?(:only_media)
+        # `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids.
+        status_ids = statuses.joins(:media_attachments).distinct(:id).pluck(:id)
+        statuses.where(id: status_ids)
+      else
+        statuses
+      end
     end
   end
 
   def tag_timeline_statuses
-    Status.as_tag_timeline(@tag, current_account, params[:local])
+    Status.as_tag_timeline(@tag, current_account, truthy_param?(:local))
   end
 
   def insert_pagination_headers
@@ -46,7 +54,7 @@ class Api::V1::Timelines::TagController < Api::BaseController
   end
 
   def pagination_params(core_params)
-    params.permit(:local, :limit).merge(core_params)
+    params.permit(:local, :limit, :only_media).merge(core_params)
   end
 
   def next_path
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 276c6b012..fc745eaec 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -15,6 +15,7 @@ class ApplicationController < ActionController::Base
   helper_method :current_flavour
   helper_method :current_skin
   helper_method :single_user_mode?
+  helper_method :use_seamless_external_login?
 
   rescue_from ActionController::RoutingError, with: :not_found
   rescue_from ActiveRecord::RecordNotFound, with: :not_found
@@ -35,7 +36,7 @@ class ApplicationController < ActionController::Base
   end
 
   def store_current_location
-    store_location_for(:user, request.url)
+    store_location_for(:user, request.url) unless request.format == :json
   end
 
   def require_admin!
@@ -145,6 +146,10 @@ class ApplicationController < ActionController::Base
     @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.exists?
   end
 
+  def use_seamless_external_login?
+    Devise.pam_authentication || Devise.ldap_authentication
+  end
+
   def current_account
     @current_account ||= current_user.try(:account)
   end
diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb
index 72b8e9dd8..f3e0ae257 100644
--- a/app/controllers/auth/confirmations_controller.rb
+++ b/app/controllers/auth/confirmations_controller.rb
@@ -3,6 +3,7 @@
 class Auth::ConfirmationsController < Devise::ConfirmationsController
   layout 'auth'
 
+  before_action :set_user, only: [:finish_signup]
   before_action :set_pack
 
   private
@@ -10,4 +11,26 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
   def set_pack
     use_pack 'auth'
   end
+
+  # GET/PATCH /users/:id/finish_signup
+  def finish_signup
+    return unless request.patch? && params[:user]
+    if @user.update(user_params)
+      @user.skip_reconfirmation!
+      sign_in(@user, bypass: true)
+      redirect_to root_path, notice: I18n.t('devise.confirmations.send_instructions')
+    else
+      @show_errors = true
+    end
+  end
+
+  private
+
+  def set_user
+    @user = current_user
+  end
+
+  def user_params
+    params.require(:user).permit(:email)
+  end
 end
diff --git a/app/controllers/auth/omniauth_callbacks_controller.rb b/app/controllers/auth/omniauth_callbacks_controller.rb
new file mode 100644
index 000000000..bbf63bed3
--- /dev/null
+++ b/app/controllers/auth/omniauth_callbacks_controller.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
+  skip_before_action :verify_authenticity_token
+
+  def self.provides_callback_for(provider)
+    provider_id = provider.to_s.chomp '_oauth2'
+
+    define_method provider do
+      @user = User.find_for_oauth(request.env['omniauth.auth'], current_user)
+
+      if @user.persisted?
+        sign_in_and_redirect @user, event: :authentication
+        set_flash_message(:notice, :success, kind: provider_id.capitalize) if is_navigational_format?
+      else
+        session["devise.#{provider}_data"] = request.env['omniauth.auth']
+        redirect_to new_user_registration_url
+      end
+    end
+  end
+
+  Devise.omniauth_configs.each_key do |provider|
+    provides_callback_for provider
+  end
+
+  def after_sign_in_path_for(resource)
+    if resource.email_verified?
+      root_path
+    else
+      finish_signup_path
+    end
+  end
+end
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
index 2b6a1bdbc..9b3ea4f27 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -15,6 +15,11 @@ class Auth::RegistrationsController < Devise::RegistrationsController
 
   protected
 
+  def update_resource(resource, params)
+    params[:password] = nil if Devise.pam_authentication && resource.encrypted_password.blank?
+    super
+  end
+
   def build_resource(hash = nil)
     super(hash)
 
diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb
index f45d77b88..62f3b2eb6 100644
--- a/app/controllers/auth/sessions_controller.rb
+++ b/app/controllers/auth/sessions_controller.rb
@@ -11,6 +11,14 @@ class Auth::SessionsController < Devise::SessionsController
   prepend_before_action :set_pack
   before_action :set_instance_presenter, only: [:new]
 
+  def new
+    Devise.omniauth_configs.each do |provider, config|
+      return redirect_to(omniauth_authorize_path(resource_name, provider)) if config.strategy.redirect_at_sign_in
+    end
+
+    super
+  end
+
   def create
     super do |resource|
       remember_me(resource)
@@ -29,7 +37,11 @@ class Auth::SessionsController < Devise::SessionsController
     if session[:otp_user_id]
       User.find(session[:otp_user_id])
     elsif user_params[:email]
-      User.find_for_authentication(email: user_params[:email])
+      if use_seamless_external_login? && Devise.check_at_sign && user_params[:email].index('@').nil?
+        User.joins(:account).find_by(accounts: { username: user_params[:email] })
+      else
+        User.find_for_authentication(email: user_params[:email])
+      end
     end
   end
 
@@ -47,6 +59,14 @@ class Auth::SessionsController < Devise::SessionsController
     end
   end
 
+  def after_sign_out_path_for(_resource_or_scope)
+    Devise.omniauth_configs.each_value do |config|
+      return root_path if config.strategy.redirect_at_sign_in
+    end
+
+    super
+  end
+
   def two_factor_enabled?
     find_user.try(:otp_required_for_login?)
   end
diff --git a/app/controllers/authorize_follows_controller.rb b/app/controllers/authorize_follows_controller.rb
index eda50e07d..95052df7c 100644
--- a/app/controllers/authorize_follows_controller.rb
+++ b/app/controllers/authorize_follows_controller.rb
@@ -46,7 +46,7 @@ class AuthorizeFollowsController < ApplicationController
   end
 
   def account_from_remote_follow
-    ResolveRemoteAccountService.new.call(acct_without_prefix)
+    ResolveAccountService.new.call(acct_without_prefix)
   end
 
   def acct_param_is_url?
diff --git a/app/controllers/concerns/localized.rb b/app/controllers/concerns/localized.rb
index a9ea60f7d..e697284a8 100644
--- a/app/controllers/concerns/localized.rb
+++ b/app/controllers/concerns/localized.rb
@@ -17,11 +17,7 @@ module Localized
   end
 
   def default_locale
-    request_locale || env_locale || I18n.default_locale
-  end
-
-  def env_locale
-    ENV['DEFAULT_LOCALE']
+    request_locale || I18n.default_locale
   end
 
   def request_locale
@@ -29,12 +25,10 @@ module Localized
   end
 
   def preferred_locale
-    http_accept_language.preferred_language_from([env_locale]) ||
-      http_accept_language.preferred_language_from(I18n.available_locales)
+    http_accept_language.preferred_language_from(I18n.available_locales)
   end
 
   def compatible_locale
-    http_accept_language.compatible_language_from([env_locale]) ||
-      http_accept_language.compatible_language_from(I18n.available_locales)
+    http_accept_language.compatible_language_from(I18n.available_locales)
   end
 end
diff --git a/app/controllers/concerns/signature_authentication.rb b/app/controllers/concerns/signature_authentication.rb
new file mode 100644
index 000000000..beec93223
--- /dev/null
+++ b/app/controllers/concerns/signature_authentication.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module SignatureAuthentication
+  extend ActiveSupport::Concern
+
+  include SignatureVerification
+
+  def current_account
+    super || signed_request_account
+  end
+end
diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb
index 2baafb5bf..f289228d3 100644
--- a/app/controllers/concerns/signature_verification.rb
+++ b/app/controllers/concerns/signature_verification.rb
@@ -114,7 +114,7 @@ module SignatureVerification
 
   def account_from_key_id(key_id)
     if key_id.start_with?('acct:')
-      ResolveRemoteAccountService.new.call(key_id.gsub(/\Aacct:/, ''))
+      ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''))
     elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
       account   = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
       account ||= ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false)
diff --git a/app/controllers/concerns/user_tracking_concern.rb b/app/controllers/concerns/user_tracking_concern.rb
index a2510e55f..be10705fc 100644
--- a/app/controllers/concerns/user_tracking_concern.rb
+++ b/app/controllers/concerns/user_tracking_concern.rb
@@ -3,7 +3,6 @@
 module UserTrackingConcern
   extend ActiveSupport::Concern
 
-  REGENERATE_FEED_DAYS = 14
   UPDATE_SIGN_IN_HOURS = 24
 
   included do
@@ -14,25 +13,10 @@ module UserTrackingConcern
 
   def set_user_activity
     return unless user_needs_sign_in_update?
-
-    # Mark as signed-in today
     current_user.update_tracked_fields!(request)
-    ActivityTracker.record('activity:logins', current_user.id)
-
-    # Regenerate feed if needed
-    regenerate_feed! if user_needs_feed_update?
   end
 
   def user_needs_sign_in_update?
     user_signed_in? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < UPDATE_SIGN_IN_HOURS.hours.ago)
   end
-
-  def user_needs_feed_update?
-    current_user.last_sign_in_at < REGENERATE_FEED_DAYS.days.ago
-  end
-
-  def regenerate_feed!
-    Redis.current.setnx("account:#{current_user.account_id}:regeneration", true) && Redis.current.expire("account:#{current_user.account_id}:regeneration", 1.day.seconds)
-    RegenerationWorker.perform_async(current_user.account_id)
-  end
 end
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index 080cbde11..c74d3f86d 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -9,6 +9,8 @@ class FollowerAccountsController < ApplicationController
     respond_to do |format|
       format.html do
         use_pack 'public'
+
+        @relationships = AccountRelationshipsPresenter.new(@follows.map(&:account_id), current_user.account_id) if user_signed_in?
       end
 
       format.json do
diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb
index 74e83ad81..4c1e3f327 100644
--- a/app/controllers/following_accounts_controller.rb
+++ b/app/controllers/following_accounts_controller.rb
@@ -9,6 +9,8 @@ class FollowingAccountsController < ApplicationController
     respond_to do |format|
       format.html do
         use_pack 'public'
+
+        @relationships = AccountRelationshipsPresenter.new(@follows.map(&:target_account_id), current_user.account_id) if user_signed_in?
       end
 
       format.json do
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 7437a647e..a8ec0dcc9 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -35,7 +35,8 @@ class HomeController < ApplicationController
       end
     end
 
-    redirect_to(default_redirect_path)
+    matches = request.path.match(%r{\A/web/timelines/tag/(?<tag>.+)\z})
+    redirect_to(matches ? tag_path(CGI.unescape(matches[:tag])) : default_redirect_path)
   end
 
   def set_pack
diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb
index f652f5ace..88c7232dd 100644
--- a/app/controllers/media_controller.rb
+++ b/app/controllers/media_controller.rb
@@ -3,20 +3,26 @@
 class MediaController < ApplicationController
   include Authorization
 
-  before_action :verify_permitted_status
+  before_action :set_media_attachment
+  before_action :verify_permitted_status!
 
   def show
-    redirect_to media_attachment.file.url(:original)
+    redirect_to @media_attachment.file.url(:original)
+  end
+
+  def player
+    @body_classes = 'player'
+    raise ActiveRecord::RecordNotFound unless @media_attachment.video? || @media_attachment.gifv?
   end
 
   private
 
-  def media_attachment
-    MediaAttachment.attached.find_by!(shortcode: params[:id])
+  def set_media_attachment
+    @media_attachment = MediaAttachment.attached.find_by!(shortcode: params[:id] || params[:medium_id])
   end
 
-  def verify_permitted_status
-    authorize media_attachment.status, :show?
+  def verify_permitted_status!
+    authorize @media_attachment.status, :show?
   rescue Mastodon::NotPermittedError
     # Reraise in order to get a 404 instead of a 403 error code
     raise ActiveRecord::RecordNotFound
diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb
index 9c03ece86..cf8745576 100644
--- a/app/controllers/settings/exports_controller.rb
+++ b/app/controllers/settings/exports_controller.rb
@@ -1,7 +1,19 @@
 # frozen_string_literal: true
 
 class Settings::ExportsController < Settings::BaseController
+  include Authorization
+
   def show
-    @export = Export.new(current_account)
+    @export  = Export.new(current_account)
+    @backups = current_user.backups
+  end
+
+  def create
+    authorize :backup, :create?
+
+    backup = current_user.backups.create!
+    BackupWorker.perform_async(backup.id)
+
+    redirect_to settings_export_path
   end
 end
diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index 7cd1abe0c..c853b5ab7 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -36,6 +36,7 @@ class Settings::PreferencesController < Settings::BaseController
       :setting_favourite_modal,
       :setting_delete_modal,
       :setting_auto_play_gif,
+      :setting_display_sensitive_media,
       :setting_reduce_motion,
       :setting_system_font_ui,
       :setting_noindex,
diff --git a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb
index f1fa03f0a..8518c61ee 100644
--- a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb
+++ b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb
@@ -3,6 +3,8 @@
 module Settings
   module TwoFactorAuthentication
     class ConfirmationsController < BaseController
+      before_action :ensure_otp_secret
+
       def new
         prepare_two_factor_form
       end
@@ -34,6 +36,10 @@ module Settings
         @provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Rails.configuration.x.local_domain)
         @qrcode = RQRCode::QRCode.new(@provision_url)
       end
+
+      def ensure_otp_secret
+        redirect_to settings_two_factor_authentication_path unless current_user.otp_secret
+      end
     end
   end
 end
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index d67fac0e5..61ffb97d9 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -1,6 +1,7 @@
 # frozen_string_literal: true
 
 class StatusesController < ApplicationController
+  include SignatureAuthentication
   include Authorization
 
   layout 'public'
diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb
index b597ba4bb..e2ea45c83 100644
--- a/app/controllers/stream_entries_controller.rb
+++ b/app/controllers/stream_entries_controller.rb
@@ -10,6 +10,7 @@ class StreamEntriesController < ApplicationController
   before_action :set_stream_entry
   before_action :set_link_headers
   before_action :check_account_suspension
+  before_action :set_cache_headers
 
   def show
     respond_to do |format|
@@ -20,6 +21,10 @@ class StreamEntriesController < ApplicationController
       end
 
       format.atom do
+        unless @stream_entry.hidden?
+          skip_session!
+          expires_in 3.minutes, public: true
+        end
         render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true))
       end
     end