about summary refs log tree commit diff
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/account.rb3
-rw-r--r--app/models/account_domain_block.rb8
-rw-r--r--app/models/backup.rb22
-rw-r--r--app/models/block.rb4
-rw-r--r--app/models/concerns/account_avatar.rb6
-rw-r--r--app/models/concerns/account_header.rb6
-rw-r--r--app/models/concerns/omniauthable.rb85
-rw-r--r--app/models/concerns/paginable.rb9
-rw-r--r--app/models/concerns/relationship_cacheable.rb16
-rw-r--r--app/models/concerns/remotable.rb6
-rw-r--r--app/models/favourite.rb2
-rw-r--r--app/models/follow.rb1
-rw-r--r--app/models/follow_request.rb1
-rw-r--r--app/models/form/admin_settings.rb2
-rw-r--r--app/models/form/migration.rb2
-rw-r--r--app/models/glitch/keyword_mute_helper.rb27
-rw-r--r--app/models/identity.rb22
-rw-r--r--app/models/import.rb2
-rw-r--r--app/models/invite.rb2
-rw-r--r--app/models/media_attachment.rb74
-rw-r--r--app/models/mute.rb4
-rw-r--r--app/models/notification.rb3
-rw-r--r--app/models/preview_card.rb13
-rw-r--r--app/models/report.rb4
-rw-r--r--app/models/site_upload.rb4
-rw-r--r--app/models/status.rb34
-rw-r--r--app/models/user.rb117
-rw-r--r--app/models/web/setting.rb2
28 files changed, 432 insertions, 49 deletions
diff --git a/app/models/account.rb b/app/models/account.rb
index 6df9668d5..61f81ab70 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -43,6 +43,7 @@
 #  protocol                :integer          default("ostatus"), not null
 #  memorial                :boolean          default(FALSE), not null
 #  moved_to_account_id     :integer
+#  featured_collection_url :string
 #
 
 class Account < ApplicationRecord
@@ -165,7 +166,7 @@ class Account < ApplicationRecord
 
   def refresh!
     return if local?
-    ResolveRemoteAccountService.new.call(acct)
+    ResolveAccountService.new.call(acct)
   end
 
   def unsuspend!
diff --git a/app/models/account_domain_block.rb b/app/models/account_domain_block.rb
index abcc923b3..bc00b4f32 100644
--- a/app/models/account_domain_block.rb
+++ b/app/models/account_domain_block.rb
@@ -16,12 +16,16 @@ class AccountDomainBlock < ApplicationRecord
   belongs_to :account
   validates :domain, presence: true, uniqueness: { scope: :account_id }
 
-  after_create  :remove_blocking_cache
-  after_destroy :remove_blocking_cache
+  after_commit :remove_blocking_cache
+  after_commit :remove_relationship_cache
 
   private
 
   def remove_blocking_cache
     Rails.cache.delete("exclude_domains_for:#{account_id}")
   end
+
+  def remove_relationship_cache
+    Rails.cache.delete_matched("relationship:#{account_id}:*")
+  end
 end
diff --git a/app/models/backup.rb b/app/models/backup.rb
new file mode 100644
index 000000000..5a7e6a14d
--- /dev/null
+++ b/app/models/backup.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: backups
+#
+#  id                :integer          not null, primary key
+#  user_id           :integer
+#  dump_file_name    :string
+#  dump_content_type :string
+#  dump_file_size    :integer
+#  dump_updated_at   :datetime
+#  processed         :boolean          default(FALSE), not null
+#  created_at        :datetime         not null
+#  updated_at        :datetime         not null
+#
+
+class Backup < ApplicationRecord
+  belongs_to :user, inverse_of: :backups
+
+  has_attached_file :dump
+  do_not_validate_attachment_file_type :dump
+end
diff --git a/app/models/block.rb b/app/models/block.rb
index 441e6bca3..d6ecabd3b 100644
--- a/app/models/block.rb
+++ b/app/models/block.rb
@@ -12,14 +12,14 @@
 
 class Block < ApplicationRecord
   include Paginable
+  include RelationshipCacheable
 
   belongs_to :account
   belongs_to :target_account, class_name: 'Account'
 
   validates :account_id, uniqueness: { scope: :target_account_id }
 
-  after_create  :remove_blocking_cache
-  after_destroy :remove_blocking_cache
+  after_commit :remove_blocking_cache
 
   private
 
diff --git a/app/models/concerns/account_avatar.rb b/app/models/concerns/account_avatar.rb
index 8a5c9a22c..9e34a9461 100644
--- a/app/models/concerns/account_avatar.rb
+++ b/app/models/concerns/account_avatar.rb
@@ -7,8 +7,8 @@ module AccountAvatar
 
   class_methods do
     def avatar_styles(file)
-      styles = { original: '120x120#' }
-      styles[:static] = { format: 'png', convert_options: '-coalesce' } if file.content_type == 'image/gif'
+      styles = { original: { geometry: '400x400#', file_geometry_parser: FastGeometryParser } }
+      styles[:static] = { geometry: '400x400#', format: 'png', convert_options: '-coalesce', file_geometry_parser: FastGeometryParser } if file.content_type == 'image/gif'
       styles
     end
 
@@ -17,7 +17,7 @@ module AccountAvatar
 
   included do
     # Avatar upload
-    has_attached_file :avatar, styles: ->(f) { avatar_styles(f) }, convert_options: { all: '-quality 80 -strip' }
+    has_attached_file :avatar, styles: ->(f) { avatar_styles(f) }, convert_options: { all: '-strip' }, processors: [:lazy_thumbnail]
     validates_attachment_content_type :avatar, content_type: IMAGE_MIME_TYPES
     validates_attachment_size :avatar, less_than: 2.megabytes
   end
diff --git a/app/models/concerns/account_header.rb b/app/models/concerns/account_header.rb
index aff2aa3f9..04c576b28 100644
--- a/app/models/concerns/account_header.rb
+++ b/app/models/concerns/account_header.rb
@@ -7,8 +7,8 @@ module AccountHeader
 
   class_methods do
     def header_styles(file)
-      styles = { original: '700x335#' }
-      styles[:static] = { format: 'png', convert_options: '-coalesce' } if file.content_type == 'image/gif'
+      styles = { original: { geometry: '700x335#', file_geometry_parser: FastGeometryParser } }
+      styles[:static] = { geometry: '700x335#', format: 'png', convert_options: '-coalesce', file_geometry_parser: FastGeometryParser } if file.content_type == 'image/gif'
       styles
     end
 
@@ -17,7 +17,7 @@ module AccountHeader
 
   included do
     # Header upload
-    has_attached_file :header, styles: ->(f) { header_styles(f) }, convert_options: { all: '-quality 80 -strip' }
+    has_attached_file :header, styles: ->(f) { header_styles(f) }, convert_options: { all: '-strip' }, processors: [:lazy_thumbnail]
     validates_attachment_content_type :header, content_type: IMAGE_MIME_TYPES
     validates_attachment_size :header, less_than: 2.megabytes
   end
diff --git a/app/models/concerns/omniauthable.rb b/app/models/concerns/omniauthable.rb
new file mode 100644
index 000000000..50288e700
--- /dev/null
+++ b/app/models/concerns/omniauthable.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+module Omniauthable
+  extend ActiveSupport::Concern
+
+  TEMP_EMAIL_PREFIX = 'change@me'
+  TEMP_EMAIL_REGEX = /\Achange@me/
+
+  included do
+    def omniauth_providers
+      Devise.omniauth_configs.keys
+    end
+
+    def email_verified?
+      email && email !~ TEMP_EMAIL_REGEX
+    end
+  end
+
+  class_methods do
+    def find_for_oauth(auth, signed_in_resource = nil)
+      # EOLE-SSO Patch
+      auth.uid = (auth.uid[0][:uid] || auth.uid[0][:user]) if auth.uid.is_a? Hashie::Array
+      identity = Identity.find_for_oauth(auth)
+
+      # If a signed_in_resource is provided it always overrides the existing user
+      # to prevent the identity being locked with accidentally created accounts.
+      # Note that this may leave zombie accounts (with no associated identity) which
+      # can be cleaned up at a later date.
+      user = signed_in_resource ? signed_in_resource : identity.user
+      user = create_for_oauth(auth) if user.nil?
+
+      if identity.user.nil?
+        identity.user = user
+        identity.save!
+      end
+
+      user
+    end
+
+    def create_for_oauth(auth)
+      # Check if the user exists with provided email if the provider gives us a
+      # verified email.  If no verified email was provided or the user already
+      # exists, we assign a temporary email and ask the user to verify it on
+      # the next step via Auth::ConfirmationsController.finish_signup
+
+      user = User.new(user_params_from_auth(auth))
+      user.account.avatar_remote_url = auth.info.image if auth.info.image =~ /\A#{URI.regexp(%w(http https))}\z/
+      user.skip_confirmation!
+      user.save!
+      user
+    end
+
+    private
+
+    def user_params_from_auth(auth)
+      strategy          = Devise.omniauth_configs[auth.provider.to_sym].strategy
+      assume_verified   = strategy.try(:security).try(:assume_email_is_verified)
+      email_is_verified = auth.info.verified || auth.info.verified_email || assume_verified
+      email             = auth.info.verified_email || auth.info.email
+      email             = email_is_verified && !User.exists?(email: auth.info.email) && email
+      display_name      = auth.info.full_name || [auth.info.first_name, auth.info.last_name].join(' ')
+
+      {
+        email: email ? email : "#{TEMP_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com",
+        password: Devise.friendly_token[0, 20],
+        account_attributes: {
+          username: ensure_unique_username(auth.uid),
+          display_name: display_name,
+        },
+      }
+    end
+
+    def ensure_unique_username(starting_username)
+      username = starting_username
+      i        = 0
+
+      while Account.exists?(username: username)
+        i       += 1
+        username = "#{starting_username}_#{i}"
+      end
+
+      username
+    end
+  end
+end
diff --git a/app/models/concerns/paginable.rb b/app/models/concerns/paginable.rb
index 6061bf9bd..66695677e 100644
--- a/app/models/concerns/paginable.rb
+++ b/app/models/concerns/paginable.rb
@@ -10,5 +10,14 @@ module Paginable
       query = query.where(arel_table[:id].gt(since_id)) if since_id.present?
       query
     }
+
+    # Differs from :paginate_by_max_id in that it gives the results immediately following min_id,
+    # whereas since_id gives the items with largest id, but with since_id as a cutoff.
+    # Results will be in ascending order by id.
+    scope :paginate_by_min_id, ->(limit, min_id = nil) {
+      query = reorder(arel_table[:id]).limit(limit)
+      query = query.where(arel_table[:id].gt(min_id)) if min_id.present?
+      query
+    }
   end
 end
diff --git a/app/models/concerns/relationship_cacheable.rb b/app/models/concerns/relationship_cacheable.rb
new file mode 100644
index 000000000..0d9359f7e
--- /dev/null
+++ b/app/models/concerns/relationship_cacheable.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module RelationshipCacheable
+  extend ActiveSupport::Concern
+
+  included do
+    after_commit :remove_relationship_cache
+  end
+
+  private
+
+  def remove_relationship_cache
+    Rails.cache.delete("relationship:#{account_id}:#{target_account_id}")
+    Rails.cache.delete("relationship:#{target_account_id}:#{account_id}")
+  end
+end
diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb
index 990035b34..020303a2f 100644
--- a/app/models/concerns/remotable.rb
+++ b/app/models/concerns/remotable.rb
@@ -28,7 +28,11 @@ module Remotable
           matches  = response.headers['content-disposition']&.match(/filename="([^"]*)"/)
           filename = matches.nil? ? parsed_url.path.split('/').last : matches[1]
           basename = SecureRandom.hex(8)
-          extname  = File.extname(filename)
+          extname = if filename.nil?
+                      ''
+                    else
+                      File.extname(filename)
+                    end
 
           send("#{attachment_name}=", StringIO.new(response.to_s))
           send("#{attachment_name}_file_name=", basename + extname)
diff --git a/app/models/favourite.rb b/app/models/favourite.rb
index 2b1271f31..fa1884b86 100644
--- a/app/models/favourite.rb
+++ b/app/models/favourite.rb
@@ -13,6 +13,8 @@
 class Favourite < ApplicationRecord
   include Paginable
 
+  update_index('statuses#status', :status) if Chewy.enabled?
+
   belongs_to :account, inverse_of: :favourites
   belongs_to :status,  inverse_of: :favourites, counter_cache: true
 
diff --git a/app/models/follow.rb b/app/models/follow.rb
index f953b8e3e..8e6fe537a 100644
--- a/app/models/follow.rb
+++ b/app/models/follow.rb
@@ -13,6 +13,7 @@
 
 class Follow < ApplicationRecord
   include Paginable
+  include RelationshipCacheable
 
   belongs_to :account, counter_cache: :following_count
 
diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb
index bd6c4a0b9..cde26ceed 100644
--- a/app/models/follow_request.rb
+++ b/app/models/follow_request.rb
@@ -13,6 +13,7 @@
 
 class FollowRequest < ApplicationRecord
   include Paginable
+  include RelationshipCacheable
 
   belongs_to :account
   belongs_to :target_account, class_name: 'Account'
diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb
index dd629279c..32922e7f1 100644
--- a/app/models/form/admin_settings.rb
+++ b/app/models/form/admin_settings.rb
@@ -34,6 +34,8 @@ class Form::AdminSettings
     :activity_api_enabled=,
     :peers_api_enabled,
     :peers_api_enabled=,
+    :show_known_fediverse_at_about_page,
+    :show_known_fediverse_at_about_page=,
     to: Setting
   )
 end
diff --git a/app/models/form/migration.rb b/app/models/form/migration.rb
index b74987337..c2a8655e1 100644
--- a/app/models/form/migration.rb
+++ b/app/models/form/migration.rb
@@ -20,6 +20,6 @@ class Form::Migration
   private
 
   def set_account
-    self.account = (ResolveRemoteAccountService.new.call(acct) if account.nil? && acct.present?)
+    self.account = (ResolveAccountService.new.call(acct) if account.nil? && acct.present?)
   end
 end
diff --git a/app/models/glitch/keyword_mute_helper.rb b/app/models/glitch/keyword_mute_helper.rb
new file mode 100644
index 000000000..6d067947f
--- /dev/null
+++ b/app/models/glitch/keyword_mute_helper.rb
@@ -0,0 +1,27 @@
+require 'html2text'
+
+class Glitch::KeywordMuteHelper
+  attr_reader :text_matcher
+  attr_reader :tag_matcher
+
+  def initialize(receiver_id)
+    @text_matcher   = Glitch::KeywordMute.text_matcher_for(receiver_id)
+    @tag_matcher    = Glitch::KeywordMute.tag_matcher_for(receiver_id)
+  end
+
+  def matches?(status)
+    matchers_match?(status) || (status.reblog? && matchers_match?(status.reblog))
+  end
+
+  private
+
+  def matchers_match?(status)
+    text_matcher.matches?(prepare_text(status.text)) ||
+      text_matcher.matches?(prepare_text(status.spoiler_text)) ||
+      tag_matcher.matches?(status.tags)
+  end
+
+  def prepare_text(text)
+    Html2Text.convert(text)
+  end
+end
diff --git a/app/models/identity.rb b/app/models/identity.rb
new file mode 100644
index 000000000..a5e0c09ec
--- /dev/null
+++ b/app/models/identity.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: identities
+#
+#  id         :integer          not null, primary key
+#  user_id    :integer
+#  provider   :string           default(""), not null
+#  uid        :string           default(""), not null
+#  created_at :datetime         not null
+#  updated_at :datetime         not null
+#
+
+class Identity < ApplicationRecord
+  belongs_to :user, dependent: :destroy
+  validates :uid, presence: true, uniqueness: { scope: :provider }
+  validates :provider, presence: true
+
+  def self.find_for_oauth(auth)
+    find_or_create_by(uid: auth.uid, provider: auth.provider)
+  end
+end
diff --git a/app/models/import.rb b/app/models/import.rb
index ba88435bf..fdb4c6b80 100644
--- a/app/models/import.rb
+++ b/app/models/import.rb
@@ -26,7 +26,7 @@ class Import < ApplicationRecord
 
   validates :type, presence: true
 
-  has_attached_file :data, url: '/system/:hash.:extension', hash_secret: ENV['PAPERCLIP_SECRET']
+  has_attached_file :data
   validates_attachment_content_type :data, content_type: FILE_TYPES
   validates_attachment_presence :data
 end
diff --git a/app/models/invite.rb b/app/models/invite.rb
index b87a3b722..4ba5432d2 100644
--- a/app/models/invite.rb
+++ b/app/models/invite.rb
@@ -4,7 +4,7 @@
 # Table name: invites
 #
 #  id         :integer          not null, primary key
-#  user_id    :integer
+#  user_id    :integer          not null
 #  code       :string           default(""), not null
 #  expires_at :datetime
 #  max_uses   :integer
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index 25b7fd085..283d0e714 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -34,7 +34,18 @@ class MediaAttachment < ApplicationRecord
   VIDEO_MIME_TYPES = ['video/webm', 'video/mp4'].freeze
   AUDIO_MIME_TYPES = ['audio/mpeg', 'audio/mp4', 'audio/vnd.wav', 'audio/wav', 'audio/x-wav', 'audio/x-wave', 'audio/ogg',].freeze
 
-  IMAGE_STYLES = { original: '1280x1280>', small: '400x400>' }.freeze
+  IMAGE_STYLES = {
+    original: {
+      geometry: '1280x1280>',
+      file_geometry_parser: FastGeometryParser,
+    },
+
+    small: {
+      geometry: '400x400>',
+      file_geometry_parser: FastGeometryParser,
+    },
+  }.freeze
+
   AUDIO_STYLES = {
     original: {
       format: 'mp4',
@@ -50,6 +61,7 @@ class MediaAttachment < ApplicationRecord
       },
     },
   }.freeze
+
   VIDEO_STYLES = {
     small: {
       convert_options: {
@@ -97,6 +109,24 @@ class MediaAttachment < ApplicationRecord
     shortcode
   end
 
+  def focus=(point)
+    return if point.blank?
+
+    x, y = (point.is_a?(Enumerable) ? point : point.split(',')).map(&:to_f)
+
+    meta = file.instance_read(:meta) || {}
+    meta['focus'] = { 'x' => x, 'y' => y }
+
+    file.instance_write(:meta, meta)
+  end
+
+  def focus
+    x = file.meta['focus']['x']
+    y = file.meta['focus']['y']
+
+    "#{x},#{y}"
+  end
+
   before_create :prepare_description, unless: :local?
   before_create :set_shortcode
   before_post_process :set_type_and_extension
@@ -178,26 +208,42 @@ class MediaAttachment < ApplicationRecord
   end
 
   def populate_meta
-    meta = {}
+    meta = file.instance_read(:meta) || {}
 
     file.queued_for_write.each do |style, file|
-      begin
-        geo = Paperclip::Geometry.from_file file
-
-        meta[style] = {
-          width: geo.width.to_i,
-          height: geo.height.to_i,
-          size: "#{geo.width.to_i}x#{geo.height.to_i}",
-          aspect: geo.width.to_f / geo.height.to_f,
-        }
-      rescue Paperclip::Errors::NotIdentifiedByImageMagickError
-        meta[style] = {}
-      end
+      meta[style] = style == :small || image? ? image_geometry(file) : video_metadata(file)
     end
 
     meta
   end
 
+  def image_geometry(file)
+    width, height = FastImage.size(file.path)
+
+    return {} if width.nil?
+
+    {
+      width:  width,
+      height: height,
+      size: "#{width}x#{height}",
+      aspect: width.to_f / height.to_f,
+    }
+  end
+
+  def video_metadata(file)
+    movie = FFMPEG::Movie.new(file.path)
+
+    return {} unless movie.valid?
+
+    {
+      width: movie.width,
+      height: movie.height,
+      frame_rate: movie.frame_rate,
+      duration: movie.duration,
+      bitrate: movie.bitrate,
+    }
+  end
+
   def appropriate_extension
     mime_type = MIME::Types[file.content_type]
 
diff --git a/app/models/mute.rb b/app/models/mute.rb
index da4787179..ebb3818c7 100644
--- a/app/models/mute.rb
+++ b/app/models/mute.rb
@@ -13,14 +13,14 @@
 
 class Mute < ApplicationRecord
   include Paginable
+  include RelationshipCacheable
 
   belongs_to :account
   belongs_to :target_account, class_name: 'Account'
 
   validates :account_id, uniqueness: { scope: :target_account_id }
 
-  after_create  :remove_blocking_cache
-  after_destroy :remove_blocking_cache
+  after_commit :remove_blocking_cache
 
   private
 
diff --git a/app/models/notification.rb b/app/models/notification.rb
index 733f89cf7..7f8dae5ec 100644
--- a/app/models/notification.rb
+++ b/app/models/notification.rb
@@ -69,7 +69,7 @@ class Notification < ApplicationRecord
 
   class << self
     def reload_stale_associations!(cached_items)
-      account_ids = cached_items.map(&:from_account_id).uniq
+      account_ids = (cached_items.map(&:from_account_id) + cached_items.map { |item| item.target_status&.account_id }.compact).uniq
 
       return if account_ids.empty?
 
@@ -77,6 +77,7 @@ class Notification < ApplicationRecord
 
       cached_items.each do |item|
         item.from_account = accounts[item.from_account_id]
+        item.target_status.account = accounts[item.target_status.account_id] if item.target_status
       end
     end
 
diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb
index 716b82243..86eecdfe5 100644
--- a/app/models/preview_card.rb
+++ b/app/models/preview_card.rb
@@ -33,7 +33,7 @@ class PreviewCard < ApplicationRecord
 
   has_and_belongs_to_many :statuses
 
-  has_attached_file :image, styles: { original: '400x400>' }, convert_options: { all: '-quality 80 -strip' }
+  has_attached_file :image, styles: { original: { geometry: '400x400>', file_geometry_parser: FastGeometryParser } }, convert_options: { all: '-quality 80 -strip' }
 
   include Attachmentable
   include Remotable
@@ -58,10 +58,11 @@ class PreviewCard < ApplicationRecord
 
     return if file.nil?
 
-    geo         = Paperclip::Geometry.from_file(file)
-    self.width  = geo.width.to_i
-    self.height = geo.height.to_i
-  rescue Paperclip::Errors::NotIdentifiedByImageMagickError
-    nil
+    width, height = FastImage.size(file.path)
+
+    return nil if width.nil?
+
+    self.width  = width
+    self.height = height
   end
 end
diff --git a/app/models/report.rb b/app/models/report.rb
index f55fb6d3e..dd123fc15 100644
--- a/app/models/report.rb
+++ b/app/models/report.rb
@@ -24,6 +24,10 @@ class Report < ApplicationRecord
 
   validates :comment, length: { maximum: 1000 }
 
+  def object_type
+    :flag
+  end
+
   def statuses
     Status.where(id: status_ids).includes(:account, :media_attachments, :mentions)
   end
diff --git a/app/models/site_upload.rb b/app/models/site_upload.rb
index 8ffdc8313..641128adf 100644
--- a/app/models/site_upload.rb
+++ b/app/models/site_upload.rb
@@ -34,8 +34,8 @@ class SiteUpload < ApplicationRecord
 
     return if tempfile.nil?
 
-    geometry  = Paperclip::Geometry.from_file(tempfile)
-    self.meta = { width: geometry.width.to_i, height: geometry.height.to_i }
+    width, height = FastImage.size(tempfile.path)
+    self.meta = { width: width, height: height }
   end
 
   def clear_cache
diff --git a/app/models/status.rb b/app/models/status.rb
index e927fb9dd..7e5ca09e4 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -32,6 +32,8 @@ class Status < ApplicationRecord
   include Cacheable
   include StatusThreadingConcern
 
+  update_index('statuses#status', :proper) if Chewy.enabled?
+
   enum visibility: [:public, :unlisted, :private, :direct], _suffix: :visibility
 
   belongs_to :application, class_name: 'Doorkeeper::Application', optional: true
@@ -56,7 +58,7 @@ class Status < ApplicationRecord
   has_one :stream_entry, as: :activity, inverse_of: :status
 
   validates :uri, uniqueness: true, presence: true, unless: :local?
-  validates :text, presence: true, unless: :reblog?
+  validates :text, presence: true, unless: -> { with_media? || reblog? }
   validates_with StatusLengthValidator
   validates :reblog, uniqueness: { scope: :account }, if: :reblog?
 
@@ -77,10 +79,28 @@ class Status < ApplicationRecord
 
   scope :not_local_only, -> { where(local_only: [false, nil]) }
 
-  cache_associated :account, :application, :media_attachments, :tags, :stream_entry, mentions: :account, reblog: [:account, :application, :stream_entry, :tags, :media_attachments, mentions: :account], thread: :account
+  cache_associated :account, :application, :media_attachments, :conversation, :tags, :stream_entry, mentions: :account, reblog: [:account, :application, :stream_entry, :tags, :media_attachments, :conversation, mentions: :account], thread: :account
 
   delegate :domain, to: :account, prefix: true
 
+  REAL_TIME_WINDOW = 6.hours
+
+  def searchable_by(preloaded = nil)
+    ids = [account_id]
+
+    if preloaded.nil?
+      ids += mentions.pluck(:account_id)
+      ids += favourites.pluck(:account_id)
+      ids += reblogs.pluck(:account_id)
+    else
+      ids += preloaded.mentions[id] || []
+      ids += preloaded.favourites[id] || []
+      ids += preloaded.reblogs[id] || []
+    end
+
+    ids.uniq
+  end
+
   def reply?
     !in_reply_to_id.nil? || attributes['reply']
   end
@@ -93,6 +113,10 @@ class Status < ApplicationRecord
     !reblog_of_id.nil?
   end
 
+  def within_realtime_window?
+    created_at >= REAL_TIME_WINDOW.ago
+  end
+
   def verb
     if destroyed?
       :delete
@@ -129,8 +153,12 @@ class Status < ApplicationRecord
     private_visibility? || direct_visibility?
   end
 
+  def with_media?
+    media_attachments.any?
+  end
+
   def non_sensitive_with_media?
-    !sensitive? && media_attachments.any?
+    !sensitive? && with_media?
   end
 
   def emojis
diff --git a/app/models/user.rb b/app/models/user.rb
index 3cf9900bd..0346cf8ae 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -34,15 +34,17 @@
 #  disabled                  :boolean          default(FALSE), not null
 #  moderator                 :boolean          default(FALSE), not null
 #  invite_id                 :integer
+#  remember_token            :string
 #
 
 class User < ApplicationRecord
   include Settings::Extend
+  include Omniauthable
 
   ACTIVE_DURATION = 14.days
 
   devise :two_factor_authenticatable,
-         otp_secret_encryption_key: ENV['OTP_SECRET']
+         otp_secret_encryption_key: Rails.configuration.x.otp_secret
 
   devise :two_factor_backupable,
          otp_number_of_backup_codes: 10
@@ -50,11 +52,14 @@ class User < ApplicationRecord
   devise :registerable, :recoverable, :rememberable, :trackable, :validatable,
          :confirmable
 
+  devise :omniauthable
+
   belongs_to :account, inverse_of: :user
   belongs_to :invite, counter_cache: :uses, optional: true
   accepts_nested_attributes_for :account
 
   has_many :applications, class_name: 'Doorkeeper::Application', as: :owner
+  has_many :backups, inverse_of: :user
 
   validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale?
   validates_with BlacklistedEmailValidator, if: :email_changed?
@@ -79,11 +84,44 @@ class User < ApplicationRecord
   has_many :session_activations, dependent: :destroy
 
   delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal,
-           :reduce_motion, :system_font_ui, :noindex, :flavour, :skin,
+           :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_sensitive_media,
            to: :settings, prefix: :setting, allow_nil: false
 
   attr_accessor :invite_code
 
+  def pam_conflict(_)
+    # block pam login tries on traditional account
+    nil
+  end
+
+  def pam_conflict?
+    return false unless Devise.pam_authentication
+    encrypted_password.present? && is_pam_account?
+  end
+
+  def pam_get_name
+    return account.username if account.present?
+    super
+  end
+
+  def pam_setup(_attributes)
+    acc = Account.new(username: pam_get_name)
+    acc.save!(validate: false)
+
+    self.email = "#{acc.username}@#{find_pam_suffix}" if email.nil? && find_pam_suffix
+    self.confirmed_at = Time.now.utc
+    self.admin = false
+    self.account = acc
+
+    acc.destroy! unless save
+  end
+
+  def ldap_setup(_attributes)
+    self.confirmed_at = Time.now.utc
+    self.admin = false
+    save!
+  end
+
   def confirmed?
     confirmed_at.present?
   end
@@ -129,7 +167,7 @@ class User < ApplicationRecord
     new_user = !confirmed?
 
     super
-    update_statistics! if new_user
+    prepare_new_user! if new_user
   end
 
   def confirm!
@@ -137,7 +175,12 @@ class User < ApplicationRecord
 
     skip_confirmation!
     save!
-    update_statistics! if new_user
+    prepare_new_user! if new_user
+  end
+
+  def update_tracked_fields!(request)
+    super
+    prepare_returning_user!
   end
 
   def promote!
@@ -208,6 +251,56 @@ class User < ApplicationRecord
     @invite_code = code
   end
 
+  def password_required?
+    return false if Devise.pam_authentication || Devise.ldap_authentication
+    super
+  end
+
+  def send_reset_password_instructions
+    return false if encrypted_password.blank? && (Devise.pam_authentication || Devise.ldap_authentication)
+    super
+  end
+
+  def reset_password!(new_password, new_password_confirmation)
+    return false if encrypted_password.blank? && (Devise.pam_authentication || Devise.ldap_authentication)
+    super
+  end
+
+  def self.pam_get_user(attributes = {})
+    if attributes[:email]
+      resource =
+        if Devise.check_at_sign && !attributes[:email].index('@')
+          joins(:account).find_by(accounts: { username: attributes[:email] })
+        else
+          find_by(email: attributes[:email])
+        end
+
+      if resource.blank?
+        resource = new(email: attributes[:email])
+        if Devise.check_at_sign && !resource[:email].index('@')
+          resource[:email] = "#{attributes[:email]}@#{resource.find_pam_suffix}"
+        end
+      end
+      resource
+    end
+  end
+
+  def self.ldap_get_user(attributes = {})
+    resource = joins(:account).find_by(accounts: { username: attributes[Devise.ldap_uid.to_sym].first })
+
+    if resource.blank?
+      resource = new(email: attributes[:mail].first, account_attributes: { username: attributes[Devise.ldap_uid.to_sym].first })
+      resource.ldap_setup(attributes)
+    end
+
+    resource
+  end
+
+  def self.authenticate_with_pam(attributes = {})
+    return nil unless Devise.pam_authentication
+    super
+  end
+
   protected
 
   def send_devise_notification(notification, *args)
@@ -220,9 +313,23 @@ class User < ApplicationRecord
     filtered_languages.reject!(&:blank?)
   end
 
-  def update_statistics!
+  def prepare_new_user!
     BootstrapTimelineWorker.perform_async(account_id)
     ActivityTracker.increment('activity:accounts:local')
     UserMailer.welcome(self).deliver_later
   end
+
+  def prepare_returning_user!
+    ActivityTracker.record('activity:logins', id)
+    regenerate_feed! if needs_feed_update?
+  end
+
+  def regenerate_feed!
+    Redis.current.setnx("account:#{account_id}:regeneration", true) && Redis.current.expire("account:#{account_id}:regeneration", 1.day.seconds)
+    RegenerationWorker.perform_async(account_id)
+  end
+
+  def needs_feed_update?
+    last_sign_in_at < ACTIVE_DURATION.ago
+  end
 end
diff --git a/app/models/web/setting.rb b/app/models/web/setting.rb
index 12b9d1226..0a5129d17 100644
--- a/app/models/web/setting.rb
+++ b/app/models/web/setting.rb
@@ -7,7 +7,7 @@
 #  data       :json
 #  created_at :datetime         not null
 #  updated_at :datetime         not null
-#  user_id    :integer
+#  user_id    :integer          not null
 #
 
 class Web::Setting < ApplicationRecord