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.rb17
-rw-r--r--app/models/account_migration.rb13
-rw-r--r--app/models/account_stat.rb12
-rw-r--r--app/models/admin/account_action.rb8
-rw-r--r--app/models/admin/import.rb29
-rw-r--r--app/models/admin/status_batch_action.rb6
-rw-r--r--app/models/concerns/lockable.rb19
-rw-r--r--app/models/concerns/redisable.rb8
-rw-r--r--app/models/domain_allow.rb4
-rw-r--r--app/models/form/domain_block_batch.rb35
-rw-r--r--app/models/mute.rb1
-rw-r--r--app/models/poll.rb5
-rw-r--r--app/models/status.rb6
-rw-r--r--app/models/status_stat.rb12
-rw-r--r--app/models/trends.rb4
-rw-r--r--app/models/trends/history.rb20
-rw-r--r--app/models/user.rb3
17 files changed, 158 insertions, 44 deletions
diff --git a/app/models/account.rb b/app/models/account.rb
index 068ee7ae9..7c81e07d9 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -79,7 +79,7 @@ class Account < ApplicationRecord
 
   MAX_DISPLAY_NAME_LENGTH = (ENV['MAX_DISPLAY_NAME_CHARS'] || 30).to_i
   MAX_NOTE_LENGTH = (ENV['MAX_BIO_CHARS'] || 500).to_i
-  MAX_FIELDS = (ENV['MAX_PROFILE_FIELDS'] || 4).to_i
+  DEFAULT_FIELDS_SIZE = (ENV['MAX_PROFILE_FIELDS'] || 4).to_i
 
   enum protocol: [:ostatus, :activitypub]
   enum suspension_origin: [:local, :remote], _prefix: true
@@ -95,7 +95,7 @@ class Account < ApplicationRecord
   validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? }
   validates :display_name, length: { maximum: MAX_DISPLAY_NAME_LENGTH }, if: -> { local? && will_save_change_to_display_name? }
   validates :note, note_length: { maximum: MAX_NOTE_LENGTH }, if: -> { local? && will_save_change_to_note? }
-  validates :fields, length: { maximum: MAX_FIELDS }, if: -> { local? && will_save_change_to_fields? }
+  validates :fields, length: { maximum: DEFAULT_FIELDS_SIZE }, if: -> { local? && will_save_change_to_fields? }
 
   scope :remote, -> { where.not(domain: nil) }
   scope :local, -> { where(domain: nil) }
@@ -113,7 +113,8 @@ class Account < ApplicationRecord
   scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
   scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
   scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
-  scope :searchable, -> { without_suspended.where(moved_to_account_id: nil) }
+  scope :without_unapproved, -> { left_outer_joins(:user).remote.or(left_outer_joins(:user).merge(User.approved.confirmed)) }
+  scope :searchable, -> { without_unapproved.without_suspended.where(moved_to_account_id: nil) }
   scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) }
   scope :followable_by, ->(account) { joins(arel_table.join(Follow.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(Follow.arel_table[:target_account_id]).and(Follow.arel_table[:account_id].eq(account.id))).join_sources).where(Follow.arel_table[:id].eq(nil)).joins(arel_table.join(FollowRequest.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(FollowRequest.arel_table[:target_account_id]).and(FollowRequest.arel_table[:account_id].eq(account.id))).join_sources).where(FollowRequest.arel_table[:id].eq(nil)) }
   scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc, accounts.id desc')) }
@@ -197,7 +198,7 @@ class Account < ApplicationRecord
   end
 
   def searchable?
-    !(suspended? || moved?)
+    !(suspended? || moved?) && (!local? || (approved? && confirmed?))
   end
 
   def possibly_stale?
@@ -329,12 +330,12 @@ class Account < ApplicationRecord
   end
 
   def build_fields
-    return if fields.size >= MAX_FIELDS
+    return if fields.size >= DEFAULT_FIELDS_SIZE
 
     tmp = self[:fields] || []
     tmp = [] if tmp.is_a?(Hash)
 
-    (MAX_FIELDS - tmp.size).times do
+    (DEFAULT_FIELDS_SIZE - tmp.size).times do
       tmp << { name: '', value: '' }
     end
 
@@ -463,9 +464,11 @@ class Account < ApplicationRecord
           accounts.*,
           ts_rank_cd(#{TEXTSEARCH}, to_tsquery('simple', :tsquery), 32) AS rank
         FROM accounts
+        LEFT JOIN users ON accounts.id = users.account_id
         WHERE to_tsquery('simple', :tsquery) @@ #{TEXTSEARCH}
           AND accounts.suspended_at IS NULL
           AND accounts.moved_to_account_id IS NULL
+          AND (accounts.domain IS NOT NULL OR (users.approved = TRUE AND users.confirmed_at IS NOT NULL))
         ORDER BY rank DESC
         LIMIT :limit OFFSET :offset
       SQL
@@ -541,9 +544,11 @@ class Account < ApplicationRecord
             (count(f.id) + 1) * ts_rank_cd(#{TEXTSEARCH}, to_tsquery('simple', :tsquery), 32) AS rank
           FROM accounts
           LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = :id) OR (accounts.id = f.target_account_id AND f.account_id = :id)
+          LEFT JOIN users ON accounts.id = users.account_id
           WHERE to_tsquery('simple', :tsquery) @@ #{TEXTSEARCH}
             AND accounts.suspended_at IS NULL
             AND accounts.moved_to_account_id IS NULL
+            AND (accounts.domain IS NOT NULL OR (users.approved = TRUE AND users.confirmed_at IS NOT NULL))
           GROUP BY accounts.id
           ORDER BY rank DESC
           LIMIT :limit OFFSET :offset
diff --git a/app/models/account_migration.rb b/app/models/account_migration.rb
index ded32c9c6..06291c9f3 100644
--- a/app/models/account_migration.rb
+++ b/app/models/account_migration.rb
@@ -15,6 +15,7 @@
 
 class AccountMigration < ApplicationRecord
   include Redisable
+  include Lockable
 
   COOLDOWN_PERIOD = 30.days.freeze
 
@@ -41,12 +42,8 @@ class AccountMigration < ApplicationRecord
 
     return false unless errors.empty?
 
-    RedisLock.acquire(lock_options) do |lock|
-      if lock.acquired?
-        save
-      else
-        raise Mastodon::RaceConditionError
-      end
+    with_lock("account_migration:#{account.id}") do
+      save
     end
   end
 
@@ -83,8 +80,4 @@ class AccountMigration < ApplicationRecord
   def validate_migration_cooldown
     errors.add(:base, I18n.t('migrations.errors.on_cooldown')) if account.migrations.within_cooldown.exists?
   end
-
-  def lock_options
-    { redis: redis, key: "account_migration:#{account.id}" }
-  end
 end
diff --git a/app/models/account_stat.rb b/app/models/account_stat.rb
index b49827267..a5d71a5b8 100644
--- a/app/models/account_stat.rb
+++ b/app/models/account_stat.rb
@@ -20,4 +20,16 @@ class AccountStat < ApplicationRecord
   belongs_to :account, inverse_of: :account_stat
 
   update_index('accounts', :account)
+
+  def following_count
+    [attributes['following_count'], 0].max
+  end
+
+  def followers_count
+    [attributes['followers_count'], 0].max
+  end
+
+  def statuses_count
+    [attributes['statuses_count'], 0].max
+  end
 end
diff --git a/app/models/admin/account_action.rb b/app/models/admin/account_action.rb
index 850ea6d82..aed3bc0c7 100644
--- a/app/models/admin/account_action.rb
+++ b/app/models/admin/account_action.rb
@@ -92,6 +92,10 @@ class Admin::AccountAction
       text: text_for_warning,
       status_ids: status_ids
     )
+
+    # A log entry is only interesting if the warning contains
+    # custom text from someone. Otherwise it's just noise.
+    log_action(:create, @warning) if @warning.text.present? && type == 'none'
   end
 
   def process_reports!
@@ -160,8 +164,8 @@ class Admin::AccountAction
 
   def reports
     @reports ||= begin
-      if type == 'none' && with_report?
-        [report]
+      if type == 'none'
+        with_report? ? [report] : []
       else
         Report.where(target_account: target_account).unresolved
       end
diff --git a/app/models/admin/import.rb b/app/models/admin/import.rb
new file mode 100644
index 000000000..c305be237
--- /dev/null
+++ b/app/models/admin/import.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+# A non-activerecord helper class for csv upload
+class Admin::Import
+  extend ActiveModel::Callbacks
+  include ActiveModel::Model
+  include Paperclip::Glue
+
+  FILE_TYPES = %w(text/plain text/csv application/csv).freeze
+
+  # Paperclip required callbacks
+  define_model_callbacks :save, only: [:after]
+  define_model_callbacks :destroy, only: [:before, :after]
+
+  attr_accessor :data_file_name, :data_content_type
+
+  has_attached_file :data
+  validates_attachment_content_type :data, content_type: FILE_TYPES
+  validates_attachment_presence :data
+  validates_with AdminImportValidator, on: :create
+
+  def save
+    run_callbacks :save
+  end
+
+  def destroy
+    run_callbacks :destroy
+  end
+end
diff --git a/app/models/admin/status_batch_action.rb b/app/models/admin/status_batch_action.rb
index 631af183c..7bf6fa6da 100644
--- a/app/models/admin/status_batch_action.rb
+++ b/app/models/admin/status_batch_action.rb
@@ -103,7 +103,7 @@ class Admin::StatusBatchAction
 
   def handle_report!
     @report = Report.new(report_params) unless with_report?
-    @report.status_ids = (@report.status_ids + status_ids.map(&:to_i)).uniq
+    @report.status_ids = (@report.status_ids + allowed_status_ids).uniq
     @report.save!
 
     @report_id = @report.id
@@ -135,4 +135,8 @@ class Admin::StatusBatchAction
   def report_params
     { account: current_account, target_account: target_account }
   end
+
+  def allowed_status_ids
+    AccountStatusesFilter.new(@report.target_account, current_account).results.with_discarded.where(id: status_ids).pluck(:id)
+  end
 end
diff --git a/app/models/concerns/lockable.rb b/app/models/concerns/lockable.rb
new file mode 100644
index 000000000..55a9714ca
--- /dev/null
+++ b/app/models/concerns/lockable.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Lockable
+  # @param [String] lock_name
+  # @param [ActiveSupport::Duration] autorelease Automatically release the lock after this time
+  # @param [Boolean] raise_on_failure Raise an error if a lock cannot be acquired, or fail silently
+  # @raise [Mastodon::RaceConditionError]
+  def with_lock(lock_name, autorelease: 15.minutes, raise_on_failure: true)
+    with_redis do |redis|
+      RedisLock.acquire(redis: redis, key: "lock:#{lock_name}", autorelease: autorelease.seconds) do |lock|
+        if lock.acquired?
+          yield
+        elsif raise_on_failure
+          raise Mastodon::RaceConditionError, "Could not acquire lock for #{lock_name}, try again later"
+        end
+      end
+    end
+  end
+end
diff --git a/app/models/concerns/redisable.rb b/app/models/concerns/redisable.rb
index 8d76b6b82..0dad3abb2 100644
--- a/app/models/concerns/redisable.rb
+++ b/app/models/concerns/redisable.rb
@@ -1,11 +1,11 @@
 # frozen_string_literal: true
 
 module Redisable
-  extend ActiveSupport::Concern
-
-  private
-
   def redis
     Thread.current[:redis] ||= RedisConfiguration.pool.checkout
   end
+
+  def with_redis(&block)
+    RedisConfiguration.with(&block)
+  end
 end
diff --git a/app/models/domain_allow.rb b/app/models/domain_allow.rb
index 4b0a89c18..2e14fce25 100644
--- a/app/models/domain_allow.rb
+++ b/app/models/domain_allow.rb
@@ -23,6 +23,10 @@ class DomainAllow < ApplicationRecord
       !rule_for(domain).nil?
     end
 
+    def allowed_domains
+      select(:domain)
+    end
+
     def rule_for(domain)
       return if domain.blank?
 
diff --git a/app/models/form/domain_block_batch.rb b/app/models/form/domain_block_batch.rb
new file mode 100644
index 000000000..39012df51
--- /dev/null
+++ b/app/models/form/domain_block_batch.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+class Form::DomainBlockBatch
+  include ActiveModel::Model
+  include Authorization
+  include AccountableConcern
+
+  attr_accessor :domain_blocks_attributes, :action, :current_account
+
+  def save
+    case action
+    when 'save'
+      save!
+    end
+  end
+
+  private
+
+  def domain_blocks
+    @domain_blocks ||= domain_blocks_attributes.values.filter_map do |attributes|
+      DomainBlock.new(attributes.without('enabled')) if ActiveModel::Type::Boolean.new.cast(attributes['enabled'])
+    end
+  end
+
+  def save!
+    domain_blocks.each do |domain_block|
+      authorize(domain_block, :create?)
+      next if DomainBlock.rule_for(domain_block.domain).present?
+
+      domain_block.save!
+      DomainBlockWorker.perform_async(domain_block.id)
+      log_action :create, domain_block
+    end
+  end
+end
diff --git a/app/models/mute.rb b/app/models/mute.rb
index fe8b6f42c..578345ef6 100644
--- a/app/models/mute.rb
+++ b/app/models/mute.rb
@@ -6,7 +6,6 @@
 #  id                 :bigint(8)        not null, primary key
 #  created_at         :datetime         not null
 #  updated_at         :datetime         not null
-#  hide_notifications :boolean          default(TRUE), not null
 #  account_id         :bigint(8)        not null
 #  target_account_id  :bigint(8)        not null
 #  hide_notifications :boolean          default(TRUE), not null
diff --git a/app/models/poll.rb b/app/models/poll.rb
index ba08309a1..1a326e452 100644
--- a/app/models/poll.rb
+++ b/app/models/poll.rb
@@ -39,13 +39,12 @@ class Poll < ApplicationRecord
 
   before_validation :prepare_options, if: :local?
   before_validation :prepare_votes_count
-
-  after_initialize :prepare_cached_tallies
+  before_validation :prepare_cached_tallies
 
   after_commit :reset_parent_cache, on: :update
 
   def loaded_options
-    options.map.with_index { |title, key| Option.new(self, key.to_s, title, show_totals_now? ? cached_tallies[key] : nil) }
+    options.map.with_index { |title, key| Option.new(self, key.to_s, title, show_totals_now? ? (cached_tallies[key] || 0) : nil) }
   end
 
   def possibly_stale?
diff --git a/app/models/status.rb b/app/models/status.rb
index 75b464a70..3efa23ae2 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -322,10 +322,6 @@ class Status < ApplicationRecord
       visibilities.keys - %w(direct limited)
     end
 
-    def in_chosen_languages(account)
-      where(language: nil).or where(language: account.chosen_languages)
-    end
-
     def as_direct_timeline(account, limit = 20, max_id = nil, since_id = nil, cache_ids = false)
       # direct timeline is mix of direct message from_me and to_me.
       # 2 queries are executed with pagination.
@@ -515,7 +511,7 @@ class Status < ApplicationRecord
   end
 
   def set_poll_id
-    update_column(:poll_id, poll.id) unless poll.nil?
+    update_column(:poll_id, poll.id) if association(:poll).loaded? && poll.present?
   end
 
   def set_visibility
diff --git a/app/models/status_stat.rb b/app/models/status_stat.rb
index 024c467e7..437861d1c 100644
--- a/app/models/status_stat.rb
+++ b/app/models/status_stat.rb
@@ -17,6 +17,18 @@ class StatusStat < ApplicationRecord
 
   after_commit :reset_parent_cache
 
+  def replies_count
+    [attributes['replies_count'], 0].max
+  end
+
+  def reblogs_count
+    [attributes['reblogs_count'], 0].max
+  end
+
+  def favourites_count
+    [attributes['favourites_count'], 0].max
+  end
+
   private
 
   def reset_parent_cache
diff --git a/app/models/trends.rb b/app/models/trends.rb
index 0be900b04..0fff66a9f 100644
--- a/app/models/trends.rb
+++ b/app/models/trends.rb
@@ -33,8 +33,8 @@ module Trends
     statuses_requiring_review = statuses.request_review
 
     User.staff.includes(:account).find_each do |user|
-      links    = user.allows_trending_tags_review_emails? ? links_requiring_review : []
-      tags     = user.allows_trending_links_review_emails? ? tags_requiring_review : []
+      links    = user.allows_trending_links_review_emails? ? links_requiring_review : []
+      tags     = user.allows_trending_tags_review_emails? ? tags_requiring_review : []
       statuses = user.allows_trending_statuses_review_emails? ? statuses_requiring_review : []
       next if links.empty? && tags.empty? && statuses.empty?
 
diff --git a/app/models/trends/history.rb b/app/models/trends/history.rb
index 608e33792..74723e35c 100644
--- a/app/models/trends/history.rb
+++ b/app/models/trends/history.rb
@@ -11,11 +11,11 @@ class Trends::History
     end
 
     def uses
-      redis.mget(*@days.map { |day| day.key_for(:uses) }).map(&:to_i).sum
+      with_redis { |redis| redis.mget(*@days.map { |day| day.key_for(:uses) }).map(&:to_i).sum }
     end
 
     def accounts
-      redis.pfcount(*@days.map { |day| day.key_for(:accounts) })
+      with_redis { |redis| redis.pfcount(*@days.map { |day| day.key_for(:accounts) }) }
     end
   end
 
@@ -33,19 +33,21 @@ class Trends::History
     attr_reader :day
 
     def accounts
-      redis.pfcount(key_for(:accounts))
+      with_redis { |redis| redis.pfcount(key_for(:accounts)) }
     end
 
     def uses
-      redis.get(key_for(:uses))&.to_i || 0
+      with_redis { |redis| redis.get(key_for(:uses))&.to_i || 0 }
     end
 
     def add(account_id)
-      redis.pipelined do
-        redis.incrby(key_for(:uses), 1)
-        redis.pfadd(key_for(:accounts), account_id)
-        redis.expire(key_for(:uses), EXPIRE_AFTER)
-        redis.expire(key_for(:accounts), EXPIRE_AFTER)
+      with_redis do |redis|
+        redis.pipelined do |pipeline|
+          pipeline.incrby(key_for(:uses), 1)
+          pipeline.pfadd(key_for(:accounts), account_id)
+          pipeline.expire(key_for(:uses), EXPIRE_AFTER)
+          pipeline.expire(key_for(:accounts), EXPIRE_AFTER)
+        end
       end
     end
 
diff --git a/app/models/user.rb b/app/models/user.rb
index b38de74b8..f7a35eeb5 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -53,6 +53,7 @@ class User < ApplicationRecord
   include Settings::Extend
   include UserRoles
   include Redisable
+  include LanguagesHelper
 
   # The home and list feeds will be stored in Redis for this amount
   # of time, and status fan-out to followers will include only people
@@ -248,7 +249,7 @@ class User < ApplicationRecord
   end
 
   def preferred_posting_language
-    settings.default_language || locale
+    valid_locale_cascade(settings.default_language, locale)
   end
 
   def setting_default_privacy