diff options
Diffstat (limited to 'app/models')
-rw-r--r-- | app/models/account.rb | 17 | ||||
-rw-r--r-- | app/models/account_migration.rb | 13 | ||||
-rw-r--r-- | app/models/account_stat.rb | 12 | ||||
-rw-r--r-- | app/models/admin/account_action.rb | 8 | ||||
-rw-r--r-- | app/models/admin/import.rb | 29 | ||||
-rw-r--r-- | app/models/admin/status_batch_action.rb | 6 | ||||
-rw-r--r-- | app/models/concerns/lockable.rb | 19 | ||||
-rw-r--r-- | app/models/concerns/redisable.rb | 8 | ||||
-rw-r--r-- | app/models/domain_allow.rb | 4 | ||||
-rw-r--r-- | app/models/form/domain_block_batch.rb | 35 | ||||
-rw-r--r-- | app/models/mute.rb | 1 | ||||
-rw-r--r-- | app/models/poll.rb | 5 | ||||
-rw-r--r-- | app/models/status.rb | 6 | ||||
-rw-r--r-- | app/models/status_stat.rb | 12 | ||||
-rw-r--r-- | app/models/trends.rb | 4 | ||||
-rw-r--r-- | app/models/trends/history.rb | 20 | ||||
-rw-r--r-- | app/models/user.rb | 3 |
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 |