Merge tag 'v2.7.0rc1' into instance_only_statuses
This commit is contained in:
@ -32,9 +32,6 @@
|
||||
# suspended :boolean default(FALSE), not null
|
||||
# locked :boolean default(FALSE), not null
|
||||
# header_remote_url :string default(""), not null
|
||||
# statuses_count :integer default(0), not null
|
||||
# followers_count :integer default(0), not null
|
||||
# following_count :integer default(0), not null
|
||||
# last_webfingered_at :datetime
|
||||
# inbox_url :string default(""), not null
|
||||
# outbox_url :string default(""), not null
|
||||
@ -46,24 +43,27 @@
|
||||
# featured_collection_url :string
|
||||
# fields :jsonb
|
||||
# actor_type :string
|
||||
# discoverable :boolean
|
||||
# also_known_as :string is an Array
|
||||
#
|
||||
|
||||
class Account < ApplicationRecord
|
||||
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
|
||||
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i
|
||||
MIN_FOLLOWERS_DISCOVERY = 10
|
||||
|
||||
include AccountAssociations
|
||||
include AccountAvatar
|
||||
include AccountFinderConcern
|
||||
include AccountHeader
|
||||
include AccountInteractions
|
||||
include Attachmentable
|
||||
include Paginable
|
||||
include AccountCounters
|
||||
include DomainNormalizable
|
||||
|
||||
enum protocol: [:ostatus, :activitypub]
|
||||
|
||||
# Local users
|
||||
has_one :user, inverse_of: :account
|
||||
|
||||
validates :username, presence: true
|
||||
|
||||
# Remote user validations
|
||||
@ -75,64 +75,29 @@ class Account < ApplicationRecord
|
||||
validates_with UniqueUsernameValidator, if: -> { local? && will_save_change_to_username? }
|
||||
validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? }
|
||||
validates :display_name, length: { maximum: 30 }, if: -> { local? && will_save_change_to_display_name? }
|
||||
validates :note, length: { maximum: 160 }, if: -> { local? && will_save_change_to_note? }
|
||||
validates :note, note_length: { maximum: 160 }, if: -> { local? && will_save_change_to_note? }
|
||||
validates :fields, length: { maximum: 4 }, if: -> { local? && will_save_change_to_fields? }
|
||||
|
||||
# Timelines
|
||||
has_many :stream_entries, inverse_of: :account, dependent: :destroy
|
||||
has_many :statuses, inverse_of: :account, dependent: :destroy
|
||||
has_many :favourites, inverse_of: :account, dependent: :destroy
|
||||
has_many :mentions, inverse_of: :account, dependent: :destroy
|
||||
has_many :notifications, inverse_of: :account, dependent: :destroy
|
||||
|
||||
# Pinned statuses
|
||||
has_many :status_pins, inverse_of: :account, dependent: :destroy
|
||||
has_many :pinned_statuses, -> { reorder('status_pins.created_at DESC') }, through: :status_pins, class_name: 'Status', source: :status
|
||||
|
||||
# Endorsements
|
||||
has_many :account_pins, inverse_of: :account, dependent: :destroy
|
||||
has_many :endorsed_accounts, through: :account_pins, class_name: 'Account', source: :target_account
|
||||
|
||||
# Media
|
||||
has_many :media_attachments, dependent: :destroy
|
||||
|
||||
# PuSH subscriptions
|
||||
has_many :subscriptions, dependent: :destroy
|
||||
|
||||
# Report relationships
|
||||
has_many :reports
|
||||
has_many :targeted_reports, class_name: 'Report', foreign_key: :target_account_id
|
||||
|
||||
has_many :report_notes, dependent: :destroy
|
||||
has_many :custom_filters, inverse_of: :account, dependent: :destroy
|
||||
|
||||
# Moderation notes
|
||||
has_many :account_moderation_notes, dependent: :destroy
|
||||
has_many :targeted_moderation_notes, class_name: 'AccountModerationNote', foreign_key: :target_account_id, dependent: :destroy
|
||||
|
||||
# Lists
|
||||
has_many :list_accounts, inverse_of: :account, dependent: :destroy
|
||||
has_many :lists, through: :list_accounts
|
||||
|
||||
# Account migrations
|
||||
belongs_to :moved_to_account, class_name: 'Account', optional: true
|
||||
|
||||
scope :remote, -> { where.not(domain: nil) }
|
||||
scope :local, -> { where(domain: nil) }
|
||||
scope :without_followers, -> { where(followers_count: 0) }
|
||||
scope :with_followers, -> { where('followers_count > 0') }
|
||||
scope :expiring, ->(time) { remote.where.not(subscription_expires_at: nil).where('subscription_expires_at < ?', time) }
|
||||
scope :partitioned, -> { order(Arel.sql('row_number() over (partition by domain)')) }
|
||||
scope :silenced, -> { where(silenced: true) }
|
||||
scope :suspended, -> { where(suspended: true) }
|
||||
scope :without_suspended, -> { where(suspended: false) }
|
||||
scope :without_silenced, -> { where(silenced: false) }
|
||||
scope :recent, -> { reorder(id: :desc) }
|
||||
scope :bots, -> { where(actor_type: %w(Application Service)) }
|
||||
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
|
||||
scope :by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') }
|
||||
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, -> { where(suspended: false).where(moved_to_account_id: nil) }
|
||||
scope :searchable, -> { without_suspended.where(moved_to_account_id: nil) }
|
||||
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).joins(:account_stat).where(AccountStat.arel_table[:followers_count].gteq(MIN_FOLLOWERS_DISCOVERY)).by_recent_status }
|
||||
scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) }
|
||||
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')) }
|
||||
scope :popular, -> { order('account_stats.followers_count desc') }
|
||||
|
||||
delegate :email,
|
||||
:unconfirmed_email,
|
||||
@ -176,6 +141,10 @@ class Account < ApplicationRecord
|
||||
"#{username}@#{Rails.configuration.x.local_domain}"
|
||||
end
|
||||
|
||||
def local_followers_count
|
||||
Follow.where(target_account_id: id).count
|
||||
end
|
||||
|
||||
def to_webfinger_s
|
||||
"acct:#{local_username_and_domain}"
|
||||
end
|
||||
@ -193,6 +162,14 @@ class Account < ApplicationRecord
|
||||
ResolveAccountService.new.call(acct)
|
||||
end
|
||||
|
||||
def silence!
|
||||
update!(silenced: true)
|
||||
end
|
||||
|
||||
def unsilence!
|
||||
update!(silenced: false)
|
||||
end
|
||||
|
||||
def suspend!
|
||||
transaction do
|
||||
user&.disable! if local?
|
||||
@ -218,6 +195,44 @@ class Account < ApplicationRecord
|
||||
@keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key)
|
||||
end
|
||||
|
||||
def tags_as_strings=(tag_names)
|
||||
tag_names.map! { |name| name.mb_chars.downcase.to_s }
|
||||
tag_names.uniq!
|
||||
|
||||
# Existing hashtags
|
||||
hashtags_map = Tag.where(name: tag_names).each_with_object({}) { |tag, h| h[tag.name] = tag }
|
||||
|
||||
# Initialize not yet existing hashtags
|
||||
tag_names.each do |name|
|
||||
next if hashtags_map.key?(name)
|
||||
hashtags_map[name] = Tag.new(name: name)
|
||||
end
|
||||
|
||||
# Remove hashtags that are to be deleted
|
||||
tags.each do |tag|
|
||||
if hashtags_map.key?(tag.name)
|
||||
hashtags_map.delete(tag.name)
|
||||
else
|
||||
transaction do
|
||||
tags.delete(tag)
|
||||
tag.decrement_count!(:accounts_count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Add hashtags that were so far missing
|
||||
hashtags_map.each_value do |tag|
|
||||
transaction do
|
||||
tags << tag
|
||||
tag.increment_count!(:accounts_count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def also_known_as
|
||||
self[:also_known_as] || []
|
||||
end
|
||||
|
||||
def fields
|
||||
(self[:fields] || []).map { |f| Field.new(self, f) }
|
||||
end
|
||||
@ -385,7 +400,9 @@ class Account < ApplicationRecord
|
||||
LIMIT ?
|
||||
SQL
|
||||
|
||||
find_by_sql([sql, limit])
|
||||
records = find_by_sql([sql, limit])
|
||||
ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
|
||||
records
|
||||
end
|
||||
|
||||
def advanced_search_for(terms, account, limit = 10, following = false)
|
||||
@ -412,7 +429,7 @@ class Account < ApplicationRecord
|
||||
LIMIT ?
|
||||
SQL
|
||||
|
||||
find_by_sql([sql, account.id, account.id, account.id, limit])
|
||||
records = find_by_sql([sql, account.id, account.id, account.id, limit])
|
||||
else
|
||||
sql = <<-SQL.squish
|
||||
SELECT
|
||||
@ -428,8 +445,11 @@ class Account < ApplicationRecord
|
||||
LIMIT ?
|
||||
SQL
|
||||
|
||||
find_by_sql([sql, account.id, account.id, limit])
|
||||
records = find_by_sql([sql, account.id, account.id, limit])
|
||||
end
|
||||
|
||||
ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
|
||||
records
|
||||
end
|
||||
|
||||
private
|
||||
@ -448,8 +468,8 @@ class Account < ApplicationRecord
|
||||
end
|
||||
|
||||
before_create :generate_keys
|
||||
before_validation :normalize_domain
|
||||
before_validation :prepare_contents, if: :local?
|
||||
before_destroy :clean_feed_manager
|
||||
|
||||
private
|
||||
|
||||
@ -469,10 +489,25 @@ class Account < ApplicationRecord
|
||||
def normalize_domain
|
||||
return if local?
|
||||
|
||||
self.domain = TagManager.instance.normalize_domain(domain)
|
||||
super
|
||||
end
|
||||
|
||||
def emojifiable_text
|
||||
[note, display_name, fields.map(&:value)].join(' ')
|
||||
end
|
||||
|
||||
def clean_feed_manager
|
||||
reblog_key = FeedManager.instance.key(:home, id, 'reblogs')
|
||||
reblogged_id_set = Redis.current.zrange(reblog_key, 0, -1)
|
||||
|
||||
Redis.current.pipelined do
|
||||
Redis.current.del(FeedManager.instance.key(:home, id))
|
||||
Redis.current.del(reblog_key)
|
||||
|
||||
reblogged_id_set.each do |reblogged_id|
|
||||
reblog_set_key = FeedManager.instance.key(:home, id, "reblogs:#{reblogged_id}")
|
||||
Redis.current.del(reblog_set_key)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -5,13 +5,14 @@ class AccountFilter
|
||||
|
||||
def initialize(params)
|
||||
@params = params
|
||||
set_defaults!
|
||||
end
|
||||
|
||||
def results
|
||||
scope = Account.recent
|
||||
scope = Account.recent.includes(:user)
|
||||
|
||||
params.each do |key, value|
|
||||
scope.merge!(scope_for(key, value)) if value.present?
|
||||
scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
|
||||
end
|
||||
|
||||
scope
|
||||
@ -19,6 +20,11 @@ class AccountFilter
|
||||
|
||||
private
|
||||
|
||||
def set_defaults!
|
||||
params['local'] = '1' if params['remote'].blank?
|
||||
params['active'] = '1' if params['suspended'].blank? && params['silenced'].blank?
|
||||
end
|
||||
|
||||
def scope_for(key, value)
|
||||
case key.to_s
|
||||
when 'local'
|
||||
@ -27,10 +33,10 @@ class AccountFilter
|
||||
Account.remote
|
||||
when 'by_domain'
|
||||
Account.where(domain: value)
|
||||
when 'active'
|
||||
Account.without_suspended
|
||||
when 'silenced'
|
||||
Account.silenced
|
||||
when 'alphabetic'
|
||||
Account.reorder(nil).alphabetic
|
||||
when 'suspended'
|
||||
Account.suspended
|
||||
when 'username'
|
||||
@ -40,11 +46,7 @@ class AccountFilter
|
||||
when 'email'
|
||||
accounts_with_users.merge User.matches_email(value)
|
||||
when 'ip'
|
||||
if valid_ip?(value)
|
||||
accounts_with_users.merge User.with_recent_ip_address(value)
|
||||
else
|
||||
Account.default_scoped
|
||||
end
|
||||
valid_ip?(value) ? accounts_with_users.where('users.current_sign_in_ip <<= ?', value) : Account.none
|
||||
when 'staff'
|
||||
accounts_with_users.merge User.staff
|
||||
else
|
||||
@ -57,8 +59,7 @@ class AccountFilter
|
||||
end
|
||||
|
||||
def valid_ip?(value)
|
||||
IPAddr.new(value)
|
||||
true
|
||||
IPAddr.new(value) && true
|
||||
rescue IPAddr::InvalidAddressError
|
||||
false
|
||||
end
|
||||
|
34
app/models/account_stat.rb
Normal file
34
app/models/account_stat.rb
Normal file
@ -0,0 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: account_stats
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# account_id :bigint(8) not null
|
||||
# statuses_count :bigint(8) default(0), not null
|
||||
# following_count :bigint(8) default(0), not null
|
||||
# followers_count :bigint(8) default(0), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# last_status_at :datetime
|
||||
#
|
||||
|
||||
class AccountStat < ApplicationRecord
|
||||
belongs_to :account, inverse_of: :account_stat
|
||||
|
||||
def increment_count!(key)
|
||||
update(attributes_for_increment(key))
|
||||
end
|
||||
|
||||
def decrement_count!(key)
|
||||
update(key => [public_send(key) - 1, 0].max)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def attributes_for_increment(key)
|
||||
attrs = { key => public_send(key) + 1 }
|
||||
attrs[:last_status_at] = Time.now.utc if key == :statuses_count
|
||||
attrs
|
||||
end
|
||||
end
|
24
app/models/account_tag_stat.rb
Normal file
24
app/models/account_tag_stat.rb
Normal file
@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: account_tag_stats
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# tag_id :bigint(8) not null
|
||||
# accounts_count :bigint(8) default(0), not null
|
||||
# hidden :boolean default(FALSE), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class AccountTagStat < ApplicationRecord
|
||||
belongs_to :tag, inverse_of: :account_tag_stat
|
||||
|
||||
def increment_count!(key)
|
||||
update(key => public_send(key) + 1)
|
||||
end
|
||||
|
||||
def decrement_count!(key)
|
||||
update(key => [public_send(key) - 1, 0].max)
|
||||
end
|
||||
end
|
23
app/models/account_warning.rb
Normal file
23
app/models/account_warning.rb
Normal file
@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: account_warnings
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# account_id :bigint(8)
|
||||
# target_account_id :bigint(8)
|
||||
# action :integer default("none"), not null
|
||||
# text :text default(""), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class AccountWarning < ApplicationRecord
|
||||
enum action: %i(none disable silence suspend), _suffix: :action
|
||||
|
||||
belongs_to :account, inverse_of: :account_warnings
|
||||
belongs_to :target_account, class_name: 'Account', inverse_of: :targeted_account_warnings
|
||||
|
||||
scope :latest, -> { order(created_at: :desc) }
|
||||
scope :custom, -> { where.not(text: '') }
|
||||
end
|
15
app/models/account_warning_preset.rb
Normal file
15
app/models/account_warning_preset.rb
Normal file
@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: account_warning_presets
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# text :text default(""), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class AccountWarningPreset < ApplicationRecord
|
||||
validates :text, presence: true
|
||||
end
|
134
app/models/admin/account_action.rb
Normal file
134
app/models/admin/account_action.rb
Normal file
@ -0,0 +1,134 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Admin::AccountAction
|
||||
include ActiveModel::Model
|
||||
include AccountableConcern
|
||||
include Authorization
|
||||
|
||||
TYPES = %w(
|
||||
none
|
||||
disable
|
||||
silence
|
||||
suspend
|
||||
).freeze
|
||||
|
||||
attr_accessor :target_account,
|
||||
:current_account,
|
||||
:type,
|
||||
:text,
|
||||
:report_id,
|
||||
:warning_preset_id,
|
||||
:send_email_notification
|
||||
|
||||
attr_reader :warning
|
||||
|
||||
def save!
|
||||
ApplicationRecord.transaction do
|
||||
process_action!
|
||||
process_warning!
|
||||
end
|
||||
|
||||
queue_email!
|
||||
process_reports!
|
||||
end
|
||||
|
||||
def report
|
||||
@report ||= Report.find(report_id) if report_id.present?
|
||||
end
|
||||
|
||||
def with_report?
|
||||
!report.nil?
|
||||
end
|
||||
|
||||
class << self
|
||||
def types_for_account(account)
|
||||
if account.local?
|
||||
TYPES
|
||||
else
|
||||
TYPES - %w(none disable)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_action!
|
||||
case type
|
||||
when 'disable'
|
||||
handle_disable!
|
||||
when 'silence'
|
||||
handle_silence!
|
||||
when 'suspend'
|
||||
handle_suspend!
|
||||
end
|
||||
end
|
||||
|
||||
def process_warning!
|
||||
return unless warnable?
|
||||
|
||||
authorize(target_account, :warn?)
|
||||
|
||||
@warning = AccountWarning.create!(target_account: target_account,
|
||||
account: current_account,
|
||||
action: type,
|
||||
text: text_for_warning)
|
||||
|
||||
# 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?
|
||||
end
|
||||
|
||||
def process_reports!
|
||||
return if report_id.blank?
|
||||
|
||||
authorize(report, :update?)
|
||||
|
||||
if type == 'none'
|
||||
log_action(:resolve, report)
|
||||
report.resolve!(current_account)
|
||||
else
|
||||
Report.where(target_account: target_account).unresolved.update_all(action_taken: true, action_taken_by_account_id: current_account.id)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_disable!
|
||||
authorize(target_account.user, :disable?)
|
||||
log_action(:disable, target_account.user)
|
||||
target_account.user&.disable!
|
||||
end
|
||||
|
||||
def handle_silence!
|
||||
authorize(target_account, :silence?)
|
||||
log_action(:silence, target_account)
|
||||
target_account.silence!
|
||||
end
|
||||
|
||||
def handle_suspend!
|
||||
authorize(target_account, :suspend?)
|
||||
log_action(:suspend, target_account)
|
||||
target_account.suspend!
|
||||
queue_suspension_worker!
|
||||
end
|
||||
|
||||
def text_for_warning
|
||||
[warning_preset&.text, text].compact.join("\n\n")
|
||||
end
|
||||
|
||||
def queue_suspension_worker!
|
||||
Admin::SuspensionWorker.perform_async(target_account.id)
|
||||
end
|
||||
|
||||
def queue_email!
|
||||
return unless warnable?
|
||||
|
||||
UserMailer.warning(target_account.user, warning).deliver_later!
|
||||
end
|
||||
|
||||
def warnable?
|
||||
send_email_notification && target_account.local?
|
||||
end
|
||||
|
||||
def warning_preset
|
||||
@warning_preset ||= AccountWarningPreset.find(warning_preset_id) if warning_preset_id.present?
|
||||
end
|
||||
end
|
59
app/models/concerns/account_associations.rb
Normal file
59
app/models/concerns/account_associations.rb
Normal file
@ -0,0 +1,59 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module AccountAssociations
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
# Local users
|
||||
has_one :user, inverse_of: :account, dependent: :destroy
|
||||
|
||||
# Timelines
|
||||
has_many :stream_entries, inverse_of: :account, dependent: :destroy
|
||||
has_many :statuses, inverse_of: :account, dependent: :destroy
|
||||
has_many :favourites, inverse_of: :account, dependent: :destroy
|
||||
has_many :mentions, inverse_of: :account, dependent: :destroy
|
||||
has_many :notifications, inverse_of: :account, dependent: :destroy
|
||||
has_many :conversations, class_name: 'AccountConversation', dependent: :destroy, inverse_of: :account
|
||||
has_many :scheduled_statuses, inverse_of: :account, dependent: :destroy
|
||||
|
||||
# Pinned statuses
|
||||
has_many :status_pins, inverse_of: :account, dependent: :destroy
|
||||
has_many :pinned_statuses, -> { reorder('status_pins.created_at DESC') }, through: :status_pins, class_name: 'Status', source: :status
|
||||
|
||||
# Endorsements
|
||||
has_many :account_pins, inverse_of: :account, dependent: :destroy
|
||||
has_many :endorsed_accounts, through: :account_pins, class_name: 'Account', source: :target_account
|
||||
|
||||
# Media
|
||||
has_many :media_attachments, dependent: :destroy
|
||||
|
||||
# PuSH subscriptions
|
||||
has_many :subscriptions, dependent: :destroy
|
||||
|
||||
# Report relationships
|
||||
has_many :reports, dependent: :destroy, inverse_of: :account
|
||||
has_many :targeted_reports, class_name: 'Report', foreign_key: :target_account_id, dependent: :destroy, inverse_of: :target_account
|
||||
|
||||
has_many :report_notes, dependent: :destroy
|
||||
has_many :custom_filters, inverse_of: :account, dependent: :destroy
|
||||
|
||||
# Moderation notes
|
||||
has_many :account_moderation_notes, dependent: :destroy, inverse_of: :account
|
||||
has_many :targeted_moderation_notes, class_name: 'AccountModerationNote', foreign_key: :target_account_id, dependent: :destroy, inverse_of: :target_account
|
||||
has_many :account_warnings, dependent: :destroy, inverse_of: :account
|
||||
has_many :targeted_account_warnings, class_name: 'AccountWarning', foreign_key: :target_account_id, dependent: :destroy, inverse_of: :target_account
|
||||
|
||||
# Lists (that the account is on, not owned by the account)
|
||||
has_many :list_accounts, inverse_of: :account, dependent: :destroy
|
||||
has_many :lists, through: :list_accounts
|
||||
|
||||
# Lists (owned by the account)
|
||||
has_many :owned_lists, class_name: 'List', dependent: :destroy, inverse_of: :account
|
||||
|
||||
# Account migrations
|
||||
belongs_to :moved_to_account, class_name: 'Account', optional: true
|
||||
|
||||
# Hashtags
|
||||
has_and_belongs_to_many :tags
|
||||
end
|
||||
end
|
32
app/models/concerns/account_counters.rb
Normal file
32
app/models/concerns/account_counters.rb
Normal file
@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module AccountCounters
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_one :account_stat, inverse_of: :account
|
||||
after_save :save_account_stat
|
||||
end
|
||||
|
||||
delegate :statuses_count,
|
||||
:statuses_count=,
|
||||
:following_count,
|
||||
:following_count=,
|
||||
:followers_count,
|
||||
:followers_count=,
|
||||
:increment_count!,
|
||||
:decrement_count!,
|
||||
:last_status_at,
|
||||
to: :account_stat
|
||||
|
||||
def account_stat
|
||||
super || build_account_stat
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def save_account_stat
|
||||
return unless account_stat&.changed?
|
||||
account_stat.save
|
||||
end
|
||||
end
|
@ -12,6 +12,10 @@ module AccountFinderConcern
|
||||
find_remote(username, domain) || raise(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
def representative
|
||||
find_local(Setting.site_contact_username.gsub(/\A@/, '')) || Account.local.find_by(suspended: false)
|
||||
end
|
||||
|
||||
def find_local(username)
|
||||
find_remote(username, nil)
|
||||
end
|
||||
|
15
app/models/concerns/domain_normalizable.rb
Normal file
15
app/models/concerns/domain_normalizable.rb
Normal file
@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DomainNormalizable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_validation :normalize_domain
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def normalize_domain
|
||||
self.domain = TagManager.instance.normalize_domain(domain)
|
||||
end
|
||||
end
|
@ -8,7 +8,7 @@ module StatusThreadingConcern
|
||||
end
|
||||
|
||||
def descendants(limit, account = nil, max_child_id = nil, since_child_id = nil, depth = nil)
|
||||
find_statuses_from_tree_path(descendant_ids(limit, max_child_id, since_child_id, depth), account)
|
||||
find_statuses_from_tree_path(descendant_ids(limit, max_child_id, since_child_id, depth), account, promote: true)
|
||||
end
|
||||
|
||||
private
|
||||
@ -76,7 +76,7 @@ module StatusThreadingConcern
|
||||
descendants_with_self - [self]
|
||||
end
|
||||
|
||||
def find_statuses_from_tree_path(ids, account)
|
||||
def find_statuses_from_tree_path(ids, account, promote: false)
|
||||
statuses = statuses_with_accounts(ids).to_a
|
||||
account_ids = statuses.map(&:account_id).uniq
|
||||
domains = statuses.map(&:account_domain).compact.uniq
|
||||
@ -86,6 +86,28 @@ module StatusThreadingConcern
|
||||
|
||||
# Order ancestors/descendants by tree path
|
||||
statuses.sort_by! { |status| ids.index(status.id) }
|
||||
|
||||
# Bring self-replies to the top
|
||||
if promote
|
||||
promote_by!(statuses) { |status| status.in_reply_to_account_id == status.account_id }
|
||||
else
|
||||
statuses
|
||||
end
|
||||
end
|
||||
|
||||
def promote_by!(arr)
|
||||
insert_at = arr.find_index { |item| !yield(item) }
|
||||
|
||||
return arr if insert_at.nil?
|
||||
|
||||
arr.each_with_index do |item, index|
|
||||
next if index <= insert_at || !yield(item)
|
||||
|
||||
arr.insert(insert_at, arr.delete_at(index))
|
||||
insert_at += 1
|
||||
end
|
||||
|
||||
arr
|
||||
end
|
||||
|
||||
def relations_map_for_account(account, account_ids, domains)
|
||||
|
@ -31,6 +31,8 @@ class CustomEmoji < ApplicationRecord
|
||||
|
||||
has_attached_file :image, styles: { static: { format: 'png', convert_options: '-coalesce -strip' } }
|
||||
|
||||
before_validation :downcase_domain
|
||||
|
||||
validates_attachment :image, content_type: { content_type: 'image/png' }, presence: true, size: { less_than: LIMIT }
|
||||
validates :shortcode, uniqueness: { scope: :domain }, format: { with: /\A#{SHORTCODE_RE_FRAGMENT}\z/ }, length: { minimum: 2 }
|
||||
|
||||
@ -73,4 +75,8 @@ class CustomEmoji < ApplicationRecord
|
||||
def remove_entity_cache
|
||||
Rails.cache.delete(EntityCache.instance.to_key(:emoji, shortcode, domain))
|
||||
end
|
||||
|
||||
def downcase_domain
|
||||
self.domain = domain.downcase unless domain.nil?
|
||||
end
|
||||
end
|
||||
|
@ -26,7 +26,7 @@ class CustomEmojiFilter
|
||||
when 'remote'
|
||||
CustomEmoji.remote
|
||||
when 'by_domain'
|
||||
CustomEmoji.where(domain: value)
|
||||
CustomEmoji.where(domain: value.downcase)
|
||||
when 'shortcode'
|
||||
CustomEmoji.search(value)
|
||||
else
|
||||
|
@ -13,6 +13,8 @@
|
||||
#
|
||||
|
||||
class DomainBlock < ApplicationRecord
|
||||
include DomainNormalizable
|
||||
|
||||
enum severity: [:silence, :suspend, :noop]
|
||||
|
||||
attr_accessor :retroactive
|
||||
@ -25,12 +27,4 @@ class DomainBlock < ApplicationRecord
|
||||
def self.blocked?(domain)
|
||||
where(domain: domain, severity: :suspend).exists?
|
||||
end
|
||||
|
||||
before_validation :normalize_domain
|
||||
|
||||
private
|
||||
|
||||
def normalize_domain
|
||||
self.domain = TagManager.instance.normalize_domain(domain)
|
||||
end
|
||||
end
|
||||
|
@ -10,7 +10,7 @@
|
||||
#
|
||||
|
||||
class EmailDomainBlock < ApplicationRecord
|
||||
before_validation :normalize_domain
|
||||
include DomainNormalizable
|
||||
|
||||
validates :domain, presence: true, uniqueness: true
|
||||
|
||||
@ -27,10 +27,4 @@ class EmailDomainBlock < ApplicationRecord
|
||||
|
||||
where(domain: domain).exists?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def normalize_domain
|
||||
self.domain = TagManager.instance.normalize_domain(domain)
|
||||
end
|
||||
end
|
||||
|
@ -9,15 +9,33 @@ class Export
|
||||
end
|
||||
|
||||
def to_blocked_accounts_csv
|
||||
to_csv account.blocking
|
||||
to_csv account.blocking.select(:username, :domain)
|
||||
end
|
||||
|
||||
def to_muted_accounts_csv
|
||||
to_csv account.muting
|
||||
to_csv account.muting.select(:username, :domain)
|
||||
end
|
||||
|
||||
def to_following_accounts_csv
|
||||
to_csv account.following
|
||||
to_csv account.following.select(:username, :domain)
|
||||
end
|
||||
|
||||
def to_lists_csv
|
||||
CSV.generate do |csv|
|
||||
account.owned_lists.select(:title).each do |list|
|
||||
list.accounts.select(:username, :domain).each do |account|
|
||||
csv << [list.title, acct(account)]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def to_blocked_domains_csv
|
||||
CSV.generate do |csv|
|
||||
account.domain_blocks.pluck(:domain).each do |domain|
|
||||
csv << [domain]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def total_storage
|
||||
@ -32,6 +50,10 @@ class Export
|
||||
account.following_count
|
||||
end
|
||||
|
||||
def total_lists
|
||||
account.owned_lists.count
|
||||
end
|
||||
|
||||
def total_followers
|
||||
account.followers_count
|
||||
end
|
||||
@ -44,13 +66,21 @@ class Export
|
||||
account.muting.count
|
||||
end
|
||||
|
||||
def total_domain_blocks
|
||||
account.domain_blocks.count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def to_csv(accounts)
|
||||
CSV.generate do |csv|
|
||||
accounts.each do |account|
|
||||
csv << [(account.local? ? account.local_username_and_domain : account.acct)]
|
||||
csv << [acct(account)]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def acct(account)
|
||||
account.local? ? account.local_username_and_domain : account.acct
|
||||
end
|
||||
end
|
||||
|
@ -16,11 +16,8 @@ class Follow < ApplicationRecord
|
||||
include Paginable
|
||||
include RelationshipCacheable
|
||||
|
||||
belongs_to :account, counter_cache: :following_count
|
||||
|
||||
belongs_to :target_account,
|
||||
class_name: 'Account',
|
||||
counter_cache: :followers_count
|
||||
belongs_to :account
|
||||
belongs_to :target_account, class_name: 'Account'
|
||||
|
||||
has_one :notification, as: :activity, dependent: :destroy
|
||||
|
||||
@ -39,7 +36,9 @@ class Follow < ApplicationRecord
|
||||
end
|
||||
|
||||
before_validation :set_uri, only: :create
|
||||
after_create :increment_cache_counters
|
||||
after_destroy :remove_endorsements
|
||||
after_destroy :decrement_cache_counters
|
||||
|
||||
private
|
||||
|
||||
@ -50,4 +49,14 @@ class Follow < ApplicationRecord
|
||||
def remove_endorsements
|
||||
AccountPin.where(target_account_id: target_account_id, account_id: account_id).delete_all
|
||||
end
|
||||
|
||||
def increment_cache_counters
|
||||
account&.increment_count!(:following_count)
|
||||
target_account&.increment_count!(:followers_count)
|
||||
end
|
||||
|
||||
def decrement_cache_counters
|
||||
account&.decrement_count!(:following_count)
|
||||
target_account&.decrement_count!(:followers_count)
|
||||
end
|
||||
end
|
||||
|
@ -44,6 +44,8 @@ class Form::AdminSettings
|
||||
:preview_sensitive_media=,
|
||||
:custom_css,
|
||||
:custom_css=,
|
||||
:profile_directory,
|
||||
:profile_directory=,
|
||||
to: Setting
|
||||
)
|
||||
end
|
||||
|
@ -1,7 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Form::AdminSuspensionConfirmation
|
||||
include ActiveModel::Model
|
||||
|
||||
attr_accessor :acct, :report_id
|
||||
end
|
@ -6,8 +6,6 @@ class Form::StatusBatch
|
||||
|
||||
attr_accessor :status_ids, :action, :current_account
|
||||
|
||||
ACTION_TYPE = %w(nsfw_on nsfw_off delete).freeze
|
||||
|
||||
def save
|
||||
case action
|
||||
when 'nsfw_on', 'nsfw_off'
|
||||
|
@ -3,12 +3,12 @@
|
||||
#
|
||||
# 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
|
||||
# id :bigint(8) not null, primary key
|
||||
# user_id :bigint(8)
|
||||
#
|
||||
|
||||
class Identity < ApplicationRecord
|
||||
|
@ -3,10 +3,23 @@
|
||||
class Instance
|
||||
include ActiveModel::Model
|
||||
|
||||
attr_accessor :domain, :accounts_count
|
||||
attr_accessor :domain, :accounts_count, :domain_block
|
||||
|
||||
def initialize(account)
|
||||
@domain = account.domain
|
||||
@accounts_count = account.accounts_count
|
||||
def initialize(resource)
|
||||
@domain = resource.domain
|
||||
@accounts_count = resource.accounts_count
|
||||
@domain_block = resource.is_a?(DomainBlock) ? resource : DomainBlock.find_by(domain: domain)
|
||||
end
|
||||
|
||||
def cached_sample_accounts
|
||||
Rails.cache.fetch("#{cache_key}/sample_accounts", expires_in: 12.hours) { Account.where(domain: domain).searchable.joins(:account_stat).popular.limit(3) }
|
||||
end
|
||||
|
||||
def to_param
|
||||
domain
|
||||
end
|
||||
|
||||
def cache_key
|
||||
domain
|
||||
end
|
||||
end
|
||||
|
@ -8,21 +8,10 @@ class InstanceFilter
|
||||
end
|
||||
|
||||
def results
|
||||
scope = Account.remote.by_domain_accounts
|
||||
params.each do |key, value|
|
||||
scope.merge!(scope_for(key, value)) if value.present?
|
||||
end
|
||||
scope
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def scope_for(key, value)
|
||||
case key.to_s
|
||||
when 'domain_name'
|
||||
Account.matches_domain(value)
|
||||
if params[:limited].present?
|
||||
DomainBlock.order(id: :desc)
|
||||
else
|
||||
raise "Unknown filter: #{key}"
|
||||
Account.remote.by_domain_accounts
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -3,20 +3,21 @@
|
||||
#
|
||||
# Table name: media_attachments
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# status_id :bigint(8)
|
||||
# file_file_name :string
|
||||
# file_content_type :string
|
||||
# file_file_size :integer
|
||||
# file_updated_at :datetime
|
||||
# remote_url :string default(""), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# shortcode :string
|
||||
# type :integer default("image"), not null
|
||||
# file_meta :json
|
||||
# account_id :bigint(8)
|
||||
# description :text
|
||||
# id :bigint(8) not null, primary key
|
||||
# status_id :bigint(8)
|
||||
# file_file_name :string
|
||||
# file_content_type :string
|
||||
# file_file_size :integer
|
||||
# file_updated_at :datetime
|
||||
# remote_url :string default(""), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# shortcode :string
|
||||
# type :integer default("image"), not null
|
||||
# file_meta :json
|
||||
# account_id :bigint(8)
|
||||
# description :text
|
||||
# scheduled_status_id :bigint(8)
|
||||
#
|
||||
|
||||
class MediaAttachment < ApplicationRecord
|
||||
@ -76,8 +77,9 @@ class MediaAttachment < ApplicationRecord
|
||||
IMAGE_LIMIT = 8.megabytes
|
||||
VIDEO_LIMIT = 40.megabytes
|
||||
|
||||
belongs_to :account, inverse_of: :media_attachments, optional: true
|
||||
belongs_to :status, inverse_of: :media_attachments, optional: true
|
||||
belongs_to :account, inverse_of: :media_attachments, optional: true
|
||||
belongs_to :status, inverse_of: :media_attachments, optional: true
|
||||
belongs_to :scheduled_status, inverse_of: :media_attachments, optional: true
|
||||
|
||||
has_attached_file :file,
|
||||
styles: ->(f) { file_styles f },
|
||||
@ -94,8 +96,8 @@ class MediaAttachment < ApplicationRecord
|
||||
validates :account, presence: true
|
||||
validates :description, length: { maximum: 420 }, if: :local?
|
||||
|
||||
scope :attached, -> { where.not(status_id: nil) }
|
||||
scope :unattached, -> { where(status_id: nil) }
|
||||
scope :attached, -> { where.not(status_id: nil).or(where.not(scheduled_status_id: nil)) }
|
||||
scope :unattached, -> { where(status_id: nil, scheduled_status_id: nil) }
|
||||
scope :local, -> { where(remote_url: '') }
|
||||
scope :remote, -> { where.not(remote_url: '') }
|
||||
|
||||
|
@ -75,7 +75,7 @@ class Notification < ApplicationRecord
|
||||
|
||||
return if account_ids.empty?
|
||||
|
||||
accounts = Account.where(id: account_ids).each_with_object({}) { |a, h| h[a.id] = a }
|
||||
accounts = Account.where(id: account_ids).includes(:account_stat).each_with_object({}) { |a, h| h[a.id] = a }
|
||||
|
||||
cached_items.each do |item|
|
||||
item.from_account = accounts[item.from_account_id]
|
||||
|
@ -68,7 +68,7 @@ class Relay < ApplicationRecord
|
||||
end
|
||||
|
||||
def some_local_account
|
||||
@some_local_account ||= Account.local.find_by(suspended: false)
|
||||
@some_local_account ||= Account.representative
|
||||
end
|
||||
|
||||
def ensure_disabled
|
||||
|
@ -15,7 +15,7 @@ class ReportNote < ApplicationRecord
|
||||
belongs_to :account
|
||||
belongs_to :report, inverse_of: :notes, touch: true
|
||||
|
||||
scope :latest, -> { reorder('created_at ASC') }
|
||||
scope :latest, -> { reorder(created_at: :desc) }
|
||||
|
||||
validates :content, presence: true, length: { maximum: 500 }
|
||||
end
|
||||
|
39
app/models/scheduled_status.rb
Normal file
39
app/models/scheduled_status.rb
Normal file
@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: scheduled_statuses
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# account_id :bigint(8)
|
||||
# scheduled_at :datetime
|
||||
# params :jsonb
|
||||
#
|
||||
|
||||
class ScheduledStatus < ApplicationRecord
|
||||
include Paginable
|
||||
|
||||
TOTAL_LIMIT = 300
|
||||
DAILY_LIMIT = 25
|
||||
|
||||
belongs_to :account, inverse_of: :scheduled_statuses
|
||||
has_many :media_attachments, inverse_of: :scheduled_status, dependent: :nullify
|
||||
|
||||
validate :validate_future_date
|
||||
validate :validate_total_limit
|
||||
validate :validate_daily_limit
|
||||
|
||||
private
|
||||
|
||||
def validate_future_date
|
||||
errors.add(:scheduled_at, I18n.t('scheduled_statuses.too_soon')) if scheduled_at.present? && scheduled_at <= Time.now.utc + PostStatusService::MIN_SCHEDULE_OFFSET
|
||||
end
|
||||
|
||||
def validate_total_limit
|
||||
errors.add(:base, I18n.t('scheduled_statuses.over_total_limit', limit: TOTAL_LIMIT)) if account.scheduled_statuses.count >= TOTAL_LIMIT
|
||||
end
|
||||
|
||||
def validate_daily_limit
|
||||
errors.add(:base, I18n.t('scheduled_statuses.over_daily_limit', limit: DAILY_LIMIT)) if account.scheduled_statuses.where('scheduled_at::date = ?::date', scheduled_at).count >= DAILY_LIMIT
|
||||
end
|
||||
end
|
@ -84,18 +84,28 @@ class Status < ApplicationRecord
|
||||
scope :including_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced: true }) }
|
||||
scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
|
||||
scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) }
|
||||
scope :tagged_with_all, ->(tags) {
|
||||
Array(tags).map(&:id).map(&:to_i).reduce(self) do |result, id|
|
||||
result.joins("INNER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}")
|
||||
end
|
||||
}
|
||||
scope :tagged_with_none, ->(tags) {
|
||||
Array(tags).map(&:id).map(&:to_i).reduce(self) do |result, id|
|
||||
result.joins("LEFT OUTER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}")
|
||||
.where("t#{id}.tag_id IS NULL")
|
||||
end
|
||||
}
|
||||
|
||||
cache_associated :account,
|
||||
:application,
|
||||
cache_associated :application,
|
||||
:media_attachments,
|
||||
:conversation,
|
||||
:status_stat,
|
||||
:tags,
|
||||
:preview_cards,
|
||||
:stream_entry,
|
||||
active_mentions: :account,
|
||||
account: :account_stat,
|
||||
active_mentions: { account: :account_stat },
|
||||
reblog: [
|
||||
:account,
|
||||
:application,
|
||||
:stream_entry,
|
||||
:tags,
|
||||
@ -103,9 +113,10 @@ class Status < ApplicationRecord
|
||||
:media_attachments,
|
||||
:conversation,
|
||||
:status_stat,
|
||||
active_mentions: :account,
|
||||
account: :account_stat,
|
||||
active_mentions: { account: :account_stat },
|
||||
],
|
||||
thread: :account
|
||||
thread: { account: :account_stat }
|
||||
|
||||
delegate :domain, to: :account, prefix: true
|
||||
|
||||
@ -231,8 +242,8 @@ class Status < ApplicationRecord
|
||||
update_status_stat!(key => [public_send(key) - 1, 0].max)
|
||||
end
|
||||
|
||||
after_create :increment_counter_caches
|
||||
after_destroy :decrement_counter_caches
|
||||
after_create_commit :increment_counter_caches
|
||||
after_destroy_commit :decrement_counter_caches
|
||||
|
||||
after_create_commit :store_uri, if: :local?
|
||||
after_create_commit :update_statistics, if: :local?
|
||||
@ -345,7 +356,7 @@ class Status < ApplicationRecord
|
||||
|
||||
return if account_ids.empty?
|
||||
|
||||
accounts = Account.where(id: account_ids).each_with_object({}) { |a, h| h[a.id] = a }
|
||||
accounts = Account.where(id: account_ids).includes(:account_stat).each_with_object({}) { |a, h| h[a.id] = a }
|
||||
|
||||
cached_items.each do |item|
|
||||
item.account = accounts[item.account_id]
|
||||
@ -423,7 +434,7 @@ class Status < ApplicationRecord
|
||||
end
|
||||
|
||||
def store_uri
|
||||
update_attribute(:uri, ActivityPub::TagManager.instance.uri_for(self)) if uri.nil?
|
||||
update_column(:uri, ActivityPub::TagManager.instance.uri_for(self)) if uri.nil?
|
||||
end
|
||||
|
||||
def prepare_contents
|
||||
@ -442,6 +453,8 @@ class Status < ApplicationRecord
|
||||
end
|
||||
|
||||
def set_conversation
|
||||
self.thread = thread.reblog if thread&.reblog?
|
||||
|
||||
self.reply = !(in_reply_to_id.nil? && thread.nil?) unless reply
|
||||
|
||||
if reply? && !thread.nil?
|
||||
@ -476,12 +489,7 @@ class Status < ApplicationRecord
|
||||
def increment_counter_caches
|
||||
return if direct_visibility?
|
||||
|
||||
if association(:account).loaded?
|
||||
account.update_attribute(:statuses_count, account.statuses_count + 1)
|
||||
else
|
||||
Account.where(id: account_id).update_all('statuses_count = COALESCE(statuses_count, 0) + 1')
|
||||
end
|
||||
|
||||
account&.increment_count!(:statuses_count)
|
||||
reblog&.increment_count!(:reblogs_count) if reblog?
|
||||
thread&.increment_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?)
|
||||
end
|
||||
@ -489,12 +497,7 @@ class Status < ApplicationRecord
|
||||
def decrement_counter_caches
|
||||
return if direct_visibility? || marked_for_mass_destruction?
|
||||
|
||||
if association(:account).loaded?
|
||||
account.update_attribute(:statuses_count, [account.statuses_count - 1, 0].max)
|
||||
else
|
||||
Account.where(id: account_id).update_all('statuses_count = GREATEST(COALESCE(statuses_count, 0) - 1, 0)')
|
||||
end
|
||||
|
||||
account&.decrement_count!(:statuses_count)
|
||||
reblog&.decrement_count!(:reblogs_count) if reblog?
|
||||
thread&.decrement_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?)
|
||||
end
|
||||
|
@ -11,12 +11,36 @@
|
||||
|
||||
class Tag < ApplicationRecord
|
||||
has_and_belongs_to_many :statuses
|
||||
has_and_belongs_to_many :accounts
|
||||
has_and_belongs_to_many :sample_accounts, -> { searchable.discoverable.popular.limit(3) }, class_name: 'Account'
|
||||
|
||||
has_one :account_tag_stat, dependent: :destroy
|
||||
|
||||
HASHTAG_NAME_RE = '[[:word:]_]*[[:alpha:]_·][[:word:]_]*'
|
||||
HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_RE})/i
|
||||
|
||||
validates :name, presence: true, uniqueness: true, format: { with: /\A#{HASHTAG_NAME_RE}\z/i }
|
||||
|
||||
scope :discoverable, -> { joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).where(account_tag_stats: { hidden: false }).order(Arel.sql('account_tag_stats.accounts_count desc')) }
|
||||
scope :hidden, -> { where(account_tag_stats: { hidden: true }) }
|
||||
|
||||
delegate :accounts_count,
|
||||
:accounts_count=,
|
||||
:increment_count!,
|
||||
:decrement_count!,
|
||||
:hidden?,
|
||||
to: :account_tag_stat
|
||||
|
||||
after_save :save_account_tag_stat
|
||||
|
||||
def account_tag_stat
|
||||
super || build_account_tag_stat
|
||||
end
|
||||
|
||||
def cached_sample_accounts
|
||||
Rails.cache.fetch("#{cache_key}/sample_accounts", expires_in: 12.hours) { sample_accounts }
|
||||
end
|
||||
|
||||
def to_param
|
||||
name
|
||||
end
|
||||
@ -43,4 +67,11 @@ class Tag < ApplicationRecord
|
||||
Tag.where('lower(name) like lower(?)', pattern).order(:name).limit(limit)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def save_account_tag_stat
|
||||
return unless account_tag_stat&.changed?
|
||||
account_tag_stat.save
|
||||
end
|
||||
end
|
||||
|
@ -36,6 +36,7 @@
|
||||
# invite_id :bigint(8)
|
||||
# remember_token :string
|
||||
# chosen_languages :string is an Array
|
||||
# created_by_application_id :bigint(8)
|
||||
#
|
||||
|
||||
class User < ApplicationRecord
|
||||
@ -49,7 +50,7 @@ class User < ApplicationRecord
|
||||
# every day. Raising the duration reduces the amount of expensive
|
||||
# RegenerationWorker jobs that need to be run when those people come
|
||||
# to check their feed
|
||||
ACTIVE_DURATION = ENV.fetch('USER_ACTIVE_DAYS', 7).to_i.days
|
||||
ACTIVE_DURATION = ENV.fetch('USER_ACTIVE_DAYS', 7).to_i.days.freeze
|
||||
|
||||
devise :two_factor_authenticatable,
|
||||
otp_secret_encryption_key: Rails.configuration.x.otp_secret
|
||||
@ -66,6 +67,7 @@ class User < ApplicationRecord
|
||||
|
||||
belongs_to :account, inverse_of: :user
|
||||
belongs_to :invite, counter_cache: :uses, optional: true
|
||||
belongs_to :created_by_application, class_name: 'Doorkeeper::Application', optional: true
|
||||
accepts_nested_attributes_for :account
|
||||
|
||||
has_many :applications, class_name: 'Doorkeeper::Application', as: :owner
|
||||
@ -73,17 +75,19 @@ class User < ApplicationRecord
|
||||
|
||||
validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale?
|
||||
validates_with BlacklistedEmailValidator, if: :email_changed?
|
||||
validates_with EmailMxValidator, if: :email_changed?
|
||||
validates_with EmailMxValidator, if: :validate_email_dns?
|
||||
validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create
|
||||
|
||||
scope :recent, -> { order(id: :desc) }
|
||||
scope :admins, -> { where(admin: true) }
|
||||
scope :moderators, -> { where(moderator: true) }
|
||||
scope :staff, -> { admins.or(moderators) }
|
||||
scope :confirmed, -> { where.not(confirmed_at: nil) }
|
||||
scope :enabled, -> { where(disabled: false) }
|
||||
scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) }
|
||||
scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended: false }) }
|
||||
scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) }
|
||||
scope :with_recent_ip_address, ->(value) { where(arel_table[:current_sign_in_ip].eq(value).or(arel_table[:last_sign_in_ip].eq(value))) }
|
||||
scope :emailable, -> { confirmed.enabled.joins(:account).merge(Account.searchable) }
|
||||
|
||||
before_validation :sanitize_languages
|
||||
|
||||
@ -96,7 +100,7 @@ class User < ApplicationRecord
|
||||
|
||||
delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal,
|
||||
:reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network,
|
||||
:expand_spoilers, :default_language, :default_federation, to: :settings, prefix: :setting, allow_nil: false
|
||||
:expand_spoilers, :default_language, :aggregate_reblogs, :default_federation, to: :settings, prefix: :setting, allow_nil: false
|
||||
|
||||
attr_reader :invite_code
|
||||
|
||||
@ -137,6 +141,10 @@ class User < ApplicationRecord
|
||||
confirmed_at.present?
|
||||
end
|
||||
|
||||
def invited?
|
||||
invite_id.present?
|
||||
end
|
||||
|
||||
def staff?
|
||||
admin? || moderator?
|
||||
end
|
||||
@ -232,6 +240,10 @@ class User < ApplicationRecord
|
||||
@hides_network ||= settings.hide_network
|
||||
end
|
||||
|
||||
def aggregates_reblogs?
|
||||
@aggregates_reblogs ||= settings.aggregate_reblogs
|
||||
end
|
||||
|
||||
def token_for_app(a)
|
||||
return nil if a.nil? || a.owner != self
|
||||
Doorkeeper::AccessToken
|
||||
@ -291,7 +303,7 @@ class User < ApplicationRecord
|
||||
end
|
||||
|
||||
if resource.blank?
|
||||
resource = new(email: attributes[:email])
|
||||
resource = new(email: attributes[:email], agreement: true)
|
||||
if Devise.check_at_sign && !resource[:email].index('@')
|
||||
resource[:email] = Rpam2.getenv(resource.find_pam_service, attributes[:email], attributes[:password], 'email', false)
|
||||
resource[:email] = "#{attributes[:email]}@#{resource.find_pam_suffix}" unless resource[:email]
|
||||
@ -304,7 +316,7 @@ class User < ApplicationRecord
|
||||
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 = new(email: attributes[:mail].first, agreement: true, account_attributes: { username: attributes[Devise.ldap_uid.to_sym].first })
|
||||
resource.ldap_setup(attributes)
|
||||
end
|
||||
|
||||
@ -357,4 +369,8 @@ class User < ApplicationRecord
|
||||
def needs_feed_update?
|
||||
last_sign_in_at < ACTIVE_DURATION.ago
|
||||
end
|
||||
|
||||
def validate_email_dns?
|
||||
email_changed? && !(Rails.env.test? || Rails.env.development?)
|
||||
end
|
||||
end
|
||||
|
@ -68,6 +68,9 @@ class Web::PushSubscription < ApplicationRecord
|
||||
p256dh: key_p256dh,
|
||||
auth: key_auth,
|
||||
ttl: ttl,
|
||||
ssl_timeout: 10,
|
||||
open_timeout: 10,
|
||||
read_timeout: 10,
|
||||
vapid: {
|
||||
subject: "mailto:#{::Setting.site_contact_email}",
|
||||
private_key: Rails.configuration.x.vapid_private_key,
|
||||
|
Reference in New Issue
Block a user