Merge tag 'v3.3.0' into instance_only_statuses
This commit is contained in:
@ -50,6 +50,8 @@
|
||||
# avatar_storage_schema_version :integer
|
||||
# header_storage_schema_version :integer
|
||||
# devices_url :string
|
||||
# sensitized_at :datetime
|
||||
# suspension_origin :integer
|
||||
#
|
||||
|
||||
class Account < ApplicationRecord
|
||||
@ -65,6 +67,8 @@ class Account < ApplicationRecord
|
||||
include Paginable
|
||||
include AccountCounters
|
||||
include DomainNormalizable
|
||||
include DomainMaterializable
|
||||
include AccountMerging
|
||||
|
||||
TRUST_LEVELS = {
|
||||
untrusted: 0,
|
||||
@ -72,6 +76,7 @@ class Account < ApplicationRecord
|
||||
}.freeze
|
||||
|
||||
enum protocol: [:ostatus, :activitypub]
|
||||
enum suspension_origin: [:local, :remote], _prefix: true
|
||||
|
||||
validates :username, presence: true
|
||||
validates_with UniqueUsernameValidator, if: -> { will_save_change_to_username? }
|
||||
@ -92,13 +97,14 @@ class Account < ApplicationRecord
|
||||
scope :partitioned, -> { order(Arel.sql('row_number() over (partition by domain)')) }
|
||||
scope :silenced, -> { where.not(silenced_at: nil) }
|
||||
scope :suspended, -> { where.not(suspended_at: nil) }
|
||||
scope :sensitized, -> { where.not(sensitized_at: nil) }
|
||||
scope :without_suspended, -> { where(suspended_at: nil) }
|
||||
scope :without_silenced, -> { where(silenced_at: nil) }
|
||||
scope :without_instance_actor, -> { where.not(id: -99) }
|
||||
scope :recent, -> { reorder(id: :desc) }
|
||||
scope :bots, -> { where(actor_type: %w(Application Service)) }
|
||||
scope :groups, -> { where(actor_type: 'Group') }
|
||||
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}%")) }
|
||||
@ -217,28 +223,45 @@ class Account < ApplicationRecord
|
||||
end
|
||||
|
||||
def suspended?
|
||||
suspended_at.present?
|
||||
suspended_at.present? && !instance_actor?
|
||||
end
|
||||
|
||||
def suspend!(date = Time.now.utc)
|
||||
def suspended_permanently?
|
||||
suspended? && deletion_request.nil?
|
||||
end
|
||||
|
||||
def suspended_temporarily?
|
||||
suspended? && deletion_request.present?
|
||||
end
|
||||
|
||||
def suspend!(date: Time.now.utc, origin: :local)
|
||||
transaction do
|
||||
user&.disable! if local?
|
||||
update!(suspended_at: date)
|
||||
create_deletion_request!
|
||||
update!(suspended_at: date, suspension_origin: origin)
|
||||
end
|
||||
end
|
||||
|
||||
def unsuspend!
|
||||
transaction do
|
||||
user&.enable! if local?
|
||||
update!(suspended_at: nil)
|
||||
deletion_request&.destroy!
|
||||
update!(suspended_at: nil, suspension_origin: nil)
|
||||
end
|
||||
end
|
||||
|
||||
def sensitized?
|
||||
sensitized_at.present?
|
||||
end
|
||||
|
||||
def sensitize!(date = Time.now.utc)
|
||||
update!(sensitized_at: date)
|
||||
end
|
||||
|
||||
def unsensitize!
|
||||
update!(sensitized_at: nil)
|
||||
end
|
||||
|
||||
def memorialize!
|
||||
transaction do
|
||||
user&.disable! if local?
|
||||
update!(memorial: true)
|
||||
end
|
||||
update!(memorial: true)
|
||||
end
|
||||
|
||||
def sign?
|
||||
@ -355,6 +378,12 @@ class Account < ApplicationRecord
|
||||
shared_inbox_url.presence || inbox_url
|
||||
end
|
||||
|
||||
def synchronization_uri_prefix
|
||||
return 'local' if local?
|
||||
|
||||
@synchronization_uri_prefix ||= uri[/http(s?):\/\/[^\/]+\//]
|
||||
end
|
||||
|
||||
class Field < ActiveModelSerializers::Model
|
||||
attributes :name, :value, :verified_at, :account, :errors
|
||||
|
||||
@ -410,12 +439,8 @@ class Account < ApplicationRecord
|
||||
super - %w(statuses_count following_count followers_count)
|
||||
end
|
||||
|
||||
def domains
|
||||
reorder(nil).pluck(Arel.sql('distinct accounts.domain'))
|
||||
end
|
||||
|
||||
def inboxes
|
||||
urls = reorder(nil).where(protocol: :activitypub).pluck(Arel.sql("distinct coalesce(nullif(accounts.shared_inbox_url, ''), accounts.inbox_url)"))
|
||||
urls = reorder(nil).where(protocol: :activitypub).group(:preferred_inbox_url).pluck(Arel.sql("coalesce(nullif(accounts.shared_inbox_url, ''), accounts.inbox_url) AS preferred_inbox_url"))
|
||||
DeliveryFailureTracker.without_unavailable(urls)
|
||||
end
|
||||
|
||||
@ -553,17 +578,6 @@ class Account < ApplicationRecord
|
||||
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
|
||||
FeedManager.instance.clean_feeds!(:home, [id])
|
||||
end
|
||||
end
|
||||
|
@ -36,17 +36,18 @@ class AccountConversation < ApplicationRecord
|
||||
end
|
||||
|
||||
class << self
|
||||
def paginate_by_id(limit, options = {})
|
||||
def to_a_paginated_by_id(limit, options = {})
|
||||
if options[:min_id]
|
||||
paginate_by_min_id(limit, options[:min_id]).reverse
|
||||
paginate_by_min_id(limit, options[:min_id], options[:max_id]).reverse
|
||||
else
|
||||
paginate_by_max_id(limit, options[:max_id], options[:since_id])
|
||||
paginate_by_max_id(limit, options[:max_id], options[:since_id]).to_a
|
||||
end
|
||||
end
|
||||
|
||||
def paginate_by_min_id(limit, min_id = nil)
|
||||
def paginate_by_min_id(limit, min_id = nil, max_id = nil)
|
||||
query = order(arel_table[:last_status_id].asc).limit(limit)
|
||||
query = query.where(arel_table[:last_status_id].gt(min_id)) if min_id.present?
|
||||
query = query.where(arel_table[:last_status_id].lt(max_id)) if max_id.present?
|
||||
query
|
||||
end
|
||||
|
||||
|
20
app/models/account_deletion_request.rb
Normal file
20
app/models/account_deletion_request.rb
Normal file
@ -0,0 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: account_deletion_requests
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# account_id :bigint(8)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
class AccountDeletionRequest < ApplicationRecord
|
||||
DELAY_TO_DELETION = 30.days.freeze
|
||||
|
||||
belongs_to :account
|
||||
|
||||
def due_at
|
||||
created_at + DELAY_TO_DELETION
|
||||
end
|
||||
end
|
@ -45,7 +45,7 @@ class AccountFilter
|
||||
def scope_for(key, value)
|
||||
case key.to_s
|
||||
when 'local'
|
||||
Account.local
|
||||
Account.local.without_instance_actor
|
||||
when 'remote'
|
||||
Account.remote
|
||||
when 'by_domain'
|
||||
|
@ -21,26 +21,26 @@ class AccountStat < ApplicationRecord
|
||||
|
||||
def increment_count!(key)
|
||||
update(attributes_for_increment(key))
|
||||
rescue ActiveRecord::StaleObjectError
|
||||
rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotUnique
|
||||
begin
|
||||
reload_with_id
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
# Nothing to do
|
||||
else
|
||||
retry
|
||||
return
|
||||
end
|
||||
|
||||
retry
|
||||
end
|
||||
|
||||
def decrement_count!(key)
|
||||
update(key => [public_send(key) - 1, 0].max)
|
||||
rescue ActiveRecord::StaleObjectError
|
||||
update(attributes_for_decrement(key))
|
||||
rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotUnique
|
||||
begin
|
||||
reload_with_id
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
# Nothing to do
|
||||
else
|
||||
retry
|
||||
return
|
||||
end
|
||||
|
||||
retry
|
||||
end
|
||||
|
||||
private
|
||||
@ -51,8 +51,13 @@ class AccountStat < ApplicationRecord
|
||||
attrs
|
||||
end
|
||||
|
||||
def attributes_for_decrement(key)
|
||||
attrs = { key => [public_send(key) - 1, 0].max }
|
||||
attrs
|
||||
end
|
||||
|
||||
def reload_with_id
|
||||
self.id = find_by!(account: account).id if new_record?
|
||||
self.id = self.class.find_by!(account: account).id if new_record?
|
||||
reload
|
||||
end
|
||||
end
|
||||
|
@ -13,7 +13,7 @@
|
||||
#
|
||||
|
||||
class AccountWarning < ApplicationRecord
|
||||
enum action: %i(none disable silence suspend), _suffix: :action
|
||||
enum action: %i(none disable sensitive silence suspend), _suffix: :action
|
||||
|
||||
belongs_to :account, inverse_of: :account_warnings
|
||||
belongs_to :target_account, class_name: 'Account', inverse_of: :targeted_account_warnings
|
||||
|
@ -8,6 +8,7 @@ class Admin::AccountAction
|
||||
TYPES = %w(
|
||||
none
|
||||
disable
|
||||
sensitive
|
||||
silence
|
||||
suspend
|
||||
).freeze
|
||||
@ -64,6 +65,8 @@ class Admin::AccountAction
|
||||
case type
|
||||
when 'disable'
|
||||
handle_disable!
|
||||
when 'sensitive'
|
||||
handle_sensitive!
|
||||
when 'silence'
|
||||
handle_silence!
|
||||
when 'suspend'
|
||||
@ -109,6 +112,12 @@ class Admin::AccountAction
|
||||
target_account.user&.disable!
|
||||
end
|
||||
|
||||
def handle_sensitive!
|
||||
authorize(target_account, :sensitive?)
|
||||
log_action(:sensitive, target_account)
|
||||
target_account.sensitize!
|
||||
end
|
||||
|
||||
def handle_silence!
|
||||
authorize(target_account, :silence?)
|
||||
log_action(:silence, target_account)
|
||||
@ -118,7 +127,7 @@ class Admin::AccountAction
|
||||
def handle_suspend!
|
||||
authorize(target_account, :suspend?)
|
||||
log_action(:suspend, target_account)
|
||||
target_account.suspend!
|
||||
target_account.suspend!(origin: :local)
|
||||
end
|
||||
|
||||
def text_for_warning
|
||||
@ -134,7 +143,7 @@ class Admin::AccountAction
|
||||
end
|
||||
|
||||
def process_email!
|
||||
UserMailer.warning(target_account.user, warning, status_ids).deliver_now! if warnable?
|
||||
UserMailer.warning(target_account.user, warning, status_ids).deliver_later! if warnable?
|
||||
end
|
||||
|
||||
def warnable?
|
||||
|
@ -35,9 +35,11 @@ class Admin::ActionLogFilter
|
||||
reopen_report: { target_type: 'Report', action: 'reopen' }.freeze,
|
||||
reset_password_user: { target_type: 'User', action: 'reset_password' }.freeze,
|
||||
resolve_report: { target_type: 'Report', action: 'resolve' }.freeze,
|
||||
sensitive_account: { target_type: 'Account', action: 'sensitive' }.freeze,
|
||||
silence_account: { target_type: 'Account', action: 'silence' }.freeze,
|
||||
suspend_account: { target_type: 'Account', action: 'suspend' }.freeze,
|
||||
unassigned_report: { target_type: 'Report', action: 'unassigned' }.freeze,
|
||||
unsensitive_account: { target_type: 'Account', action: 'unsensitive' }.freeze,
|
||||
unsilence_account: { target_type: 'Account', action: 'unsilence' }.freeze,
|
||||
unsuspend_account: { target_type: 'Account', action: 'unsuspend' }.freeze,
|
||||
update_announcement: { target_type: 'Announcement', action: 'update' }.freeze,
|
||||
|
@ -22,6 +22,7 @@ class Announcement < ApplicationRecord
|
||||
scope :published, -> { where(published: true) }
|
||||
scope :without_muted, ->(account) { joins("LEFT OUTER JOIN announcement_mutes ON announcement_mutes.announcement_id = announcements.id AND announcement_mutes.account_id = #{account.id}").where('announcement_mutes.id IS NULL') }
|
||||
scope :chronological, -> { order(Arel.sql('COALESCE(announcements.starts_at, announcements.scheduled_at, announcements.published_at, announcements.created_at) ASC')) }
|
||||
scope :reverse_chronological, -> { order(Arel.sql('COALESCE(announcements.starts_at, announcements.scheduled_at, announcements.published_at, announcements.created_at) DESC')) }
|
||||
|
||||
has_many :announcement_mutes, dependent: :destroy
|
||||
has_many :announcement_reactions, dependent: :destroy
|
||||
|
@ -60,5 +60,8 @@ module AccountAssociations
|
||||
# Hashtags
|
||||
has_and_belongs_to_many :tags
|
||||
has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account
|
||||
|
||||
# Account deletion requests
|
||||
has_one :deletion_request, class_name: 'AccountDeletionRequest', inverse_of: :account, dependent: :destroy
|
||||
end
|
||||
end
|
||||
|
@ -8,6 +8,7 @@ module AccountInteractions
|
||||
Follow.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow, mapping|
|
||||
mapping[follow.target_account_id] = {
|
||||
reblogs: follow.show_reblogs?,
|
||||
notify: follow.notify?,
|
||||
}
|
||||
end
|
||||
end
|
||||
@ -36,6 +37,7 @@ module AccountInteractions
|
||||
FollowRequest.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow_request, mapping|
|
||||
mapping[follow_request.target_account_id] = {
|
||||
reblogs: follow_request.show_reblogs?,
|
||||
notify: follow_request.notify?,
|
||||
}
|
||||
end
|
||||
end
|
||||
@ -95,25 +97,29 @@ module AccountInteractions
|
||||
has_many :announcement_mutes, dependent: :destroy
|
||||
end
|
||||
|
||||
def follow!(other_account, reblogs: nil, uri: nil, rate_limit: false)
|
||||
reblogs = true if reblogs.nil?
|
||||
|
||||
rel = active_relationships.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit)
|
||||
def follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false, bypass_limit: false)
|
||||
rel = active_relationships.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit, bypass_follow_limit: bypass_limit)
|
||||
.find_or_create_by!(target_account: other_account)
|
||||
|
||||
rel.update!(show_reblogs: reblogs)
|
||||
rel.show_reblogs = reblogs unless reblogs.nil?
|
||||
rel.notify = notify unless notify.nil?
|
||||
|
||||
rel.save! if rel.changed?
|
||||
|
||||
remove_potential_friendship(other_account)
|
||||
|
||||
rel
|
||||
end
|
||||
|
||||
def request_follow!(other_account, reblogs: nil, uri: nil, rate_limit: false)
|
||||
reblogs = true if reblogs.nil?
|
||||
|
||||
rel = follow_requests.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit)
|
||||
def request_follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false, bypass_limit: false)
|
||||
rel = follow_requests.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit, bypass_follow_limit: bypass_limit)
|
||||
.find_or_create_by!(target_account: other_account)
|
||||
|
||||
rel.update!(show_reblogs: reblogs)
|
||||
rel.show_reblogs = reblogs unless reblogs.nil?
|
||||
rel.notify = notify unless notify.nil?
|
||||
|
||||
rel.save! if rel.changed?
|
||||
|
||||
remove_potential_friendship(other_account)
|
||||
|
||||
rel
|
||||
@ -125,9 +131,12 @@ module AccountInteractions
|
||||
.find_or_create_by!(target_account: other_account)
|
||||
end
|
||||
|
||||
def mute!(other_account, notifications: nil)
|
||||
def mute!(other_account, notifications: nil, duration: 0)
|
||||
notifications = true if notifications.nil?
|
||||
mute = mute_relationships.create_with(hide_notifications: notifications).find_or_create_by!(target_account: other_account)
|
||||
mute = mute_relationships.create_with(hide_notifications: notifications).find_or_initialize_by(target_account: other_account)
|
||||
mute.expires_in = duration.zero? ? nil : duration
|
||||
mute.save!
|
||||
|
||||
remove_potential_friendship(other_account)
|
||||
|
||||
# When toggling a mute between hiding and allowing notifications, the mute will already exist, so the find_or_create_by! call will return the existing Mute without updating the hide_notifications attribute. Therefore, we check that hide_notifications? is what we want and set it if it isn't.
|
||||
@ -234,6 +243,26 @@ module AccountInteractions
|
||||
.where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
|
||||
end
|
||||
|
||||
def remote_followers_hash(url_prefix)
|
||||
Rails.cache.fetch("followers_hash:#{id}:#{url_prefix}") do
|
||||
digest = "\x00" * 32
|
||||
followers.where(Account.arel_table[:uri].matches(url_prefix + '%', false, true)).pluck_each(:uri) do |uri|
|
||||
Xorcist.xor!(digest, Digest::SHA256.digest(uri))
|
||||
end
|
||||
digest.unpack('H*')[0]
|
||||
end
|
||||
end
|
||||
|
||||
def local_followers_hash
|
||||
Rails.cache.fetch("followers_hash:#{id}:local") do
|
||||
digest = "\x00" * 32
|
||||
followers.where(domain: nil).pluck_each(:username) do |username|
|
||||
Xorcist.xor!(digest, Digest::SHA256.digest(ActivityPub::TagManager.instance.uri_for_username(username)))
|
||||
end
|
||||
digest.unpack('H*')[0]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def remove_potential_friendship(other_account, mutual = false)
|
||||
|
51
app/models/concerns/account_merging.rb
Normal file
51
app/models/concerns/account_merging.rb
Normal file
@ -0,0 +1,51 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module AccountMerging
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def merge_with!(other_account)
|
||||
# Since it's the same remote resource, the remote resource likely
|
||||
# already believes we are following/blocking, so it's safe to
|
||||
# re-attribute the relationships too. However, during the presence
|
||||
# of the index bug users could have *also* followed the reference
|
||||
# account already, therefore mass update will not work and we need
|
||||
# to check for (and skip past) uniqueness errors
|
||||
|
||||
owned_classes = [
|
||||
Status, StatusPin, MediaAttachment, Poll, Report, Tombstone, Favourite,
|
||||
Follow, FollowRequest, Block, Mute, AccountIdentityProof,
|
||||
AccountModerationNote, AccountPin, AccountStat, ListAccount,
|
||||
PollVote, Mention, AccountDeletionRequest, AccountNote
|
||||
]
|
||||
|
||||
owned_classes.each do |klass|
|
||||
klass.where(account_id: other_account.id).find_each do |record|
|
||||
begin
|
||||
record.update_attribute(:account_id, id)
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
next
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
target_classes = [
|
||||
Follow, FollowRequest, Block, Mute, AccountModerationNote, AccountPin,
|
||||
AccountNote
|
||||
]
|
||||
|
||||
target_classes.each do |klass|
|
||||
klass.where(target_account_id: other_account.id).find_each do |record|
|
||||
begin
|
||||
record.update_attribute(:target_account_id, id)
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
next
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Some follow relationships have moved, so the cache is stale
|
||||
Rails.cache.delete_matched("followers_hash:#{id}:*")
|
||||
Rails.cache.delete_matched("relationships:#{id}:*")
|
||||
Rails.cache.delete_matched("relationships:*:#{id}")
|
||||
end
|
||||
end
|
13
app/models/concerns/domain_materializable.rb
Normal file
13
app/models/concerns/domain_materializable.rb
Normal file
@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DomainMaterializable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
after_create_commit :refresh_instances_view
|
||||
end
|
||||
|
||||
def refresh_instances_view
|
||||
Instance.refresh unless domain.nil? || Instance.where(domain: domain).exists?
|
||||
end
|
||||
end
|
@ -6,7 +6,15 @@ module Expireable
|
||||
included do
|
||||
scope :expired, -> { where.not(expires_at: nil).where('expires_at < ?', Time.now.utc) }
|
||||
|
||||
attr_reader :expires_in
|
||||
def expires_in
|
||||
return @expires_in if defined?(@expires_in)
|
||||
|
||||
if expires_at.nil?
|
||||
nil
|
||||
else
|
||||
(expires_at - created_at).to_i
|
||||
end
|
||||
end
|
||||
|
||||
def expires_in=(interval)
|
||||
self.expires_at = interval.to_i.seconds.from_now if interval.present?
|
||||
|
17
app/models/concerns/follow_limitable.rb
Normal file
17
app/models/concerns/follow_limitable.rb
Normal file
@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module FollowLimitable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
validates_with FollowLimitValidator, on: :create, unless: :bypass_follow_limit?
|
||||
end
|
||||
|
||||
def bypass_follow_limit=(value)
|
||||
@bypass_follow_limit = value
|
||||
end
|
||||
|
||||
def bypass_follow_limit?
|
||||
@bypass_follow_limit
|
||||
end
|
||||
end
|
@ -14,18 +14,19 @@ module Paginable
|
||||
# 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) {
|
||||
scope :paginate_by_min_id, ->(limit, min_id = nil, max_id = nil) {
|
||||
query = reorder(arel_table[:id]).limit(limit)
|
||||
query = query.where(arel_table[:id].gt(min_id)) if min_id.present?
|
||||
query = query.where(arel_table[:id].lt(max_id)) if max_id.present?
|
||||
query
|
||||
}
|
||||
|
||||
scope :paginate_by_id, ->(limit, options = {}) {
|
||||
def self.to_a_paginated_by_id(limit, options = {})
|
||||
if options[:min_id].present?
|
||||
paginate_by_min_id(limit, options[:min_id]).reverse
|
||||
paginate_by_min_id(limit, options[:min_id], options[:max_id]).reverse
|
||||
else
|
||||
paginate_by_max_id(limit, options[:max_id], options[:since_id])
|
||||
paginate_by_max_id(limit, options[:max_id], options[:since_id]).to_a
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -12,6 +12,7 @@
|
||||
|
||||
class DomainAllow < ApplicationRecord
|
||||
include DomainNormalizable
|
||||
include DomainMaterializable
|
||||
|
||||
validates :domain, presence: true, uniqueness: true, domain: true
|
||||
|
||||
|
@ -12,10 +12,12 @@
|
||||
# reject_reports :boolean default(FALSE), not null
|
||||
# private_comment :text
|
||||
# public_comment :text
|
||||
# obfuscate :boolean default(FALSE), not null
|
||||
#
|
||||
|
||||
class DomainBlock < ApplicationRecord
|
||||
include DomainNormalizable
|
||||
include DomainMaterializable
|
||||
|
||||
enum severity: [:silence, :suspend, :noop]
|
||||
|
||||
@ -72,4 +74,23 @@ class DomainBlock < ApplicationRecord
|
||||
scope = suspend? ? accounts.where(suspended_at: created_at) : accounts.where(silenced_at: created_at)
|
||||
scope.count
|
||||
end
|
||||
|
||||
def public_domain
|
||||
return domain unless obfuscate?
|
||||
|
||||
length = domain.size
|
||||
visible_ratio = length / 4
|
||||
|
||||
domain.chars.map.with_index do |chr, i|
|
||||
if i > visible_ratio && i < length - visible_ratio && chr != '.'
|
||||
'*'
|
||||
else
|
||||
chr
|
||||
end
|
||||
end.join
|
||||
end
|
||||
|
||||
def domain_digest
|
||||
Digest::SHA256.hexdigest(domain)
|
||||
end
|
||||
end
|
||||
|
@ -9,6 +9,14 @@ class Export
|
||||
@account = account
|
||||
end
|
||||
|
||||
def to_bookmarks_csv
|
||||
CSV.generate do |csv|
|
||||
account.bookmarks.includes(:status).reorder(id: :desc).each do |bookmark|
|
||||
csv << [ActivityPub::TagManager.instance.uri_for(bookmark.status)]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def to_blocked_accounts_csv
|
||||
to_csv account.blocking.select(:username, :domain)
|
||||
end
|
||||
@ -55,6 +63,10 @@ class Export
|
||||
account.statuses_count
|
||||
end
|
||||
|
||||
def total_bookmarks
|
||||
account.bookmarks.count
|
||||
end
|
||||
|
||||
def total_follows
|
||||
account.following_count
|
||||
end
|
||||
|
@ -36,7 +36,7 @@ class Favourite < ApplicationRecord
|
||||
end
|
||||
|
||||
def decrement_cache_counters
|
||||
return if association(:status).loaded? && (status.marked_for_destruction? || status.marked_for_mass_destruction?)
|
||||
return if association(:status).loaded? && status.marked_for_destruction?
|
||||
status&.decrement_count!(:favourites_count)
|
||||
end
|
||||
end
|
||||
|
@ -20,12 +20,12 @@ class Feed
|
||||
protected
|
||||
|
||||
def from_redis(limit, max_id, since_id, min_id)
|
||||
max_id = '+inf' if max_id.blank?
|
||||
if min_id.blank?
|
||||
max_id = '+inf' if max_id.blank?
|
||||
since_id = '-inf' if since_id.blank?
|
||||
unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).map(&:first).map(&:to_i)
|
||||
else
|
||||
unhydrated = redis.zrangebyscore(key, "(#{min_id}", '+inf', limit: [0, limit], with_scores: true).map(&:first).map(&:to_i)
|
||||
unhydrated = redis.zrangebyscore(key, "(#{min_id}", "(#{max_id}", limit: [0, limit], with_scores: true).map(&:first).map(&:to_i)
|
||||
end
|
||||
|
||||
Status.where(id: unhydrated).cache_ids
|
||||
|
@ -10,12 +10,14 @@
|
||||
# target_account_id :bigint(8) not null
|
||||
# show_reblogs :boolean default(TRUE), not null
|
||||
# uri :string
|
||||
# notify :boolean default(FALSE), not null
|
||||
#
|
||||
|
||||
class Follow < ApplicationRecord
|
||||
include Paginable
|
||||
include RelationshipCacheable
|
||||
include RateLimitable
|
||||
include FollowLimitable
|
||||
|
||||
rate_limit by: :account, family: :follows
|
||||
|
||||
@ -25,7 +27,6 @@ class Follow < ApplicationRecord
|
||||
has_one :notification, as: :activity, dependent: :destroy
|
||||
|
||||
validates :account_id, uniqueness: { scope: :target_account_id }
|
||||
validates_with FollowLimitValidator, on: :create
|
||||
|
||||
scope :recent, -> { reorder(id: :desc) }
|
||||
|
||||
@ -34,14 +35,16 @@ class Follow < ApplicationRecord
|
||||
end
|
||||
|
||||
def revoke_request!
|
||||
FollowRequest.create!(account: account, target_account: target_account, show_reblogs: show_reblogs, uri: uri)
|
||||
FollowRequest.create!(account: account, target_account: target_account, show_reblogs: show_reblogs, notify: notify, uri: uri)
|
||||
destroy!
|
||||
end
|
||||
|
||||
before_validation :set_uri, only: :create
|
||||
after_create :increment_cache_counters
|
||||
after_create :invalidate_hash_cache
|
||||
after_destroy :remove_endorsements
|
||||
after_destroy :decrement_cache_counters
|
||||
after_destroy :invalidate_hash_cache
|
||||
|
||||
private
|
||||
|
||||
@ -62,4 +65,10 @@ class Follow < ApplicationRecord
|
||||
account&.decrement_count!(:following_count)
|
||||
target_account&.decrement_count!(:followers_count)
|
||||
end
|
||||
|
||||
def invalidate_hash_cache
|
||||
return if account.local? && target_account.local?
|
||||
|
||||
Rails.cache.delete("followers_hash:#{target_account_id}:#{account.synchronization_uri_prefix}")
|
||||
end
|
||||
end
|
||||
|
@ -10,12 +10,14 @@
|
||||
# target_account_id :bigint(8) not null
|
||||
# show_reblogs :boolean default(TRUE), not null
|
||||
# uri :string
|
||||
# notify :boolean default(FALSE), not null
|
||||
#
|
||||
|
||||
class FollowRequest < ApplicationRecord
|
||||
include Paginable
|
||||
include RelationshipCacheable
|
||||
include RateLimitable
|
||||
include FollowLimitable
|
||||
|
||||
rate_limit by: :account, family: :follows
|
||||
|
||||
@ -25,10 +27,9 @@ class FollowRequest < ApplicationRecord
|
||||
has_one :notification, as: :activity, dependent: :destroy
|
||||
|
||||
validates :account_id, uniqueness: { scope: :target_account_id }
|
||||
validates_with FollowLimitValidator, on: :create
|
||||
|
||||
def authorize!
|
||||
account.follow!(target_account, reblogs: show_reblogs, uri: uri)
|
||||
account.follow!(target_account, reblogs: show_reblogs, notify: notify, uri: uri)
|
||||
MergeWorker.perform_async(target_account.id, account.id) if account.local?
|
||||
destroy!
|
||||
end
|
||||
|
@ -9,6 +9,8 @@ class Form::AccountBatch
|
||||
|
||||
def save
|
||||
case action
|
||||
when 'follow'
|
||||
follow!
|
||||
when 'unfollow'
|
||||
unfollow!
|
||||
when 'remove_from_followers'
|
||||
@ -24,6 +26,12 @@ class Form::AccountBatch
|
||||
|
||||
private
|
||||
|
||||
def follow!
|
||||
accounts.find_each do |target_account|
|
||||
FollowService.new.call(current_account, target_account)
|
||||
end
|
||||
end
|
||||
|
||||
def unfollow!
|
||||
accounts.find_each do |target_account|
|
||||
UnfollowService.new.call(current_account, target_account)
|
||||
@ -43,7 +51,7 @@ class Form::AccountBatch
|
||||
end
|
||||
|
||||
def account_domains
|
||||
accounts.pluck(Arel.sql('distinct domain')).compact
|
||||
accounts.group(:domain).pluck(:domain).compact
|
||||
end
|
||||
|
||||
def accounts
|
||||
@ -69,6 +77,6 @@ class Form::AccountBatch
|
||||
records = accounts.includes(:user)
|
||||
|
||||
records.each { |account| authorize(account.user, :reject?) }
|
||||
.each { |account| SuspendAccountService.new.call(account, reserve_email: false, reserve_username: false) }
|
||||
.each { |account| DeleteAccountService.new.call(account, reserve_email: false, reserve_username: false) }
|
||||
end
|
||||
end
|
||||
|
@ -35,6 +35,7 @@ class Form::AdminSettings
|
||||
show_domain_blocks
|
||||
show_domain_blocks_rationale
|
||||
noindex
|
||||
require_invite_text
|
||||
).freeze
|
||||
|
||||
BOOLEAN_KEYS = %i(
|
||||
@ -51,6 +52,7 @@ class Form::AdminSettings
|
||||
trends
|
||||
trendable_by_default
|
||||
noindex
|
||||
require_invite_text
|
||||
).freeze
|
||||
|
||||
UPLOAD_KEYS = %i(
|
||||
|
@ -30,7 +30,7 @@ class Form::CustomEmojiBatch
|
||||
private
|
||||
|
||||
def custom_emojis
|
||||
CustomEmoji.where(id: custom_emoji_ids)
|
||||
@custom_emojis ||= CustomEmoji.where(id: custom_emoji_ids)
|
||||
end
|
||||
|
||||
def update!
|
||||
|
31
app/models/form/ip_block_batch.rb
Normal file
31
app/models/form/ip_block_batch.rb
Normal file
@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Form::IpBlockBatch
|
||||
include ActiveModel::Model
|
||||
include Authorization
|
||||
include AccountableConcern
|
||||
|
||||
attr_accessor :ip_block_ids, :action, :current_account
|
||||
|
||||
def save
|
||||
case action
|
||||
when 'delete'
|
||||
delete!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ip_blocks
|
||||
@ip_blocks ||= IpBlock.where(id: ip_block_ids)
|
||||
end
|
||||
|
||||
def delete!
|
||||
ip_blocks.each { |ip_block| authorize(ip_block, :destroy?) }
|
||||
|
||||
ip_blocks.each do |ip_block|
|
||||
ip_block.destroy
|
||||
log_action :destroy, ip_block
|
||||
end
|
||||
end
|
||||
end
|
@ -24,9 +24,10 @@ class Import < ApplicationRecord
|
||||
|
||||
belongs_to :account
|
||||
|
||||
enum type: [:following, :blocking, :muting, :domain_blocking]
|
||||
enum type: [:following, :blocking, :muting, :domain_blocking, :bookmarks]
|
||||
|
||||
validates :type, presence: true
|
||||
validates_with ImportValidator, on: :create
|
||||
|
||||
has_attached_file :data
|
||||
validates_attachment_content_type :data, content_type: FILE_TYPES
|
||||
|
@ -1,26 +1,63 @@
|
||||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: instances
|
||||
#
|
||||
# domain :string primary key
|
||||
# accounts_count :bigint(8)
|
||||
#
|
||||
|
||||
class Instance
|
||||
include ActiveModel::Model
|
||||
class Instance < ApplicationRecord
|
||||
self.primary_key = :domain
|
||||
|
||||
attr_accessor :domain, :accounts_count, :domain_block
|
||||
has_many :accounts, foreign_key: :domain, primary_key: :domain
|
||||
|
||||
def initialize(resource)
|
||||
@domain = resource.domain
|
||||
@accounts_count = resource.respond_to?(:accounts_count) ? resource.accounts_count : nil
|
||||
@domain_block = resource.is_a?(DomainBlock) ? resource : DomainBlock.rule_for(domain)
|
||||
@domain_allow = resource.is_a?(DomainAllow) ? resource : DomainAllow.rule_for(domain)
|
||||
belongs_to :domain_block, foreign_key: :domain, primary_key: :domain
|
||||
belongs_to :domain_allow, foreign_key: :domain, primary_key: :domain
|
||||
|
||||
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
||||
|
||||
def self.refresh
|
||||
Scenic.database.refresh_materialized_view(table_name, concurrently: true, cascade: false)
|
||||
end
|
||||
|
||||
def countable?
|
||||
@accounts_count.present?
|
||||
def readonly?
|
||||
true
|
||||
end
|
||||
|
||||
def delivery_failure_tracker
|
||||
@delivery_failure_tracker ||= DeliveryFailureTracker.new(domain)
|
||||
end
|
||||
|
||||
def following_count
|
||||
@following_count ||= Follow.where(account: accounts).count
|
||||
end
|
||||
|
||||
def followers_count
|
||||
@followers_count ||= Follow.where(target_account: accounts).count
|
||||
end
|
||||
|
||||
def reports_count
|
||||
@reports_count ||= Report.where(target_account: accounts).count
|
||||
end
|
||||
|
||||
def blocks_count
|
||||
@blocks_count ||= Block.where(target_account: accounts).count
|
||||
end
|
||||
|
||||
def public_comment
|
||||
domain_block&.public_comment
|
||||
end
|
||||
|
||||
def private_comment
|
||||
domain_block&.private_comment
|
||||
end
|
||||
|
||||
def media_storage
|
||||
@media_storage ||= MediaAttachment.where(account: accounts).sum(:file_file_size)
|
||||
end
|
||||
|
||||
def to_param
|
||||
domain
|
||||
end
|
||||
|
||||
def cache_key
|
||||
domain
|
||||
end
|
||||
end
|
||||
|
@ -13,18 +13,27 @@ class InstanceFilter
|
||||
end
|
||||
|
||||
def results
|
||||
if params[:limited].present?
|
||||
scope = DomainBlock
|
||||
scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present?
|
||||
scope.order(id: :desc)
|
||||
elsif params[:allowed].present?
|
||||
scope = DomainAllow
|
||||
scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present?
|
||||
scope.order(id: :desc)
|
||||
scope = Instance.includes(:domain_block, :domain_allow).order(accounts_count: :desc)
|
||||
|
||||
params.each do |key, value|
|
||||
scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
|
||||
end
|
||||
|
||||
scope
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def scope_for(key, value)
|
||||
case key.to_s
|
||||
when 'limited'
|
||||
Instance.joins(:domain_block).reorder(Arel.sql('domain_blocks.id desc'))
|
||||
when 'allowed'
|
||||
Instance.joins(:domain_allow).reorder(Arel.sql('domain_allows.id desc'))
|
||||
when 'by_domain'
|
||||
Instance.matches_domain(value)
|
||||
else
|
||||
scope = Account.remote
|
||||
scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present?
|
||||
scope.by_domain_accounts
|
||||
raise "Unknown filter: #{key}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -28,7 +28,7 @@ class Invite < ApplicationRecord
|
||||
before_validation :set_code
|
||||
|
||||
def valid_for_use?
|
||||
(max_uses.nil? || uses < max_uses) && !expired? && !(user.nil? || user.disabled?)
|
||||
(max_uses.nil? || uses < max_uses) && !expired? && user&.functional?
|
||||
end
|
||||
|
||||
private
|
||||
|
41
app/models/ip_block.rb
Normal file
41
app/models/ip_block.rb
Normal file
@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: ip_blocks
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# expires_at :datetime
|
||||
# ip :inet default(#<IPAddr: IPv4:0.0.0.0/255.255.255.255>), not null
|
||||
# severity :integer default(NULL), not null
|
||||
# comment :text default(""), not null
|
||||
#
|
||||
|
||||
class IpBlock < ApplicationRecord
|
||||
CACHE_KEY = 'blocked_ips'
|
||||
|
||||
include Expireable
|
||||
|
||||
enum severity: {
|
||||
sign_up_requires_approval: 5000,
|
||||
no_access: 9999,
|
||||
}
|
||||
|
||||
validates :ip, :severity, presence: true
|
||||
|
||||
after_commit :reset_cache
|
||||
|
||||
class << self
|
||||
def blocked?(remote_ip)
|
||||
blocked_ips_map = Rails.cache.fetch(CACHE_KEY) { FastIpMap.new(IpBlock.where(severity: :no_access).pluck(:ip)) }
|
||||
blocked_ips_map.include?(remote_ip)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reset_cache
|
||||
Rails.cache.delete(CACHE_KEY)
|
||||
end
|
||||
end
|
@ -3,11 +3,12 @@
|
||||
#
|
||||
# Table name: lists
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# account_id :bigint(8) not null
|
||||
# title :string default(""), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# id :bigint(8) not null, primary key
|
||||
# account_id :bigint(8) not null
|
||||
# title :string default(""), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# replies_policy :integer default("list"), not null
|
||||
#
|
||||
|
||||
class List < ApplicationRecord
|
||||
@ -15,6 +16,8 @@ class List < ApplicationRecord
|
||||
|
||||
PER_ACCOUNT_LIMIT = 50
|
||||
|
||||
enum replies_policy: [:list, :followed, :none], _prefix: :show
|
||||
|
||||
belongs_to :account, optional: true
|
||||
|
||||
has_many :list_accounts, inverse_of: :list, dependent: :destroy
|
||||
@ -31,17 +34,6 @@ class List < ApplicationRecord
|
||||
private
|
||||
|
||||
def clean_feed_manager
|
||||
reblog_key = FeedManager.instance.key(:list, id, 'reblogs')
|
||||
reblogged_id_set = Redis.current.zrange(reblog_key, 0, -1)
|
||||
|
||||
Redis.current.pipelined do
|
||||
Redis.current.del(FeedManager.instance.key(:list, id))
|
||||
Redis.current.del(reblog_key)
|
||||
|
||||
reblogged_id_set.each do |reblogged_id|
|
||||
reblog_set_key = FeedManager.instance.key(:list, id, "reblogs:#{reblogged_id}")
|
||||
Redis.current.del(reblog_set_key)
|
||||
end
|
||||
end
|
||||
FeedManager.instance.clean_feeds!(:list, [id])
|
||||
end
|
||||
end
|
||||
|
@ -9,11 +9,13 @@
|
||||
# account_id :bigint(8) not null
|
||||
# target_account_id :bigint(8) not null
|
||||
# hide_notifications :boolean default(TRUE), not null
|
||||
# expires_at :datetime
|
||||
#
|
||||
|
||||
class Mute < ApplicationRecord
|
||||
include Paginable
|
||||
include RelationshipCacheable
|
||||
include Expireable
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :target_account, class_name: 'Account'
|
||||
|
@ -10,21 +10,34 @@
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint(8) not null
|
||||
# from_account_id :bigint(8) not null
|
||||
# type :string
|
||||
#
|
||||
|
||||
class Notification < ApplicationRecord
|
||||
self.inheritance_column = nil
|
||||
|
||||
include Paginable
|
||||
include Cacheable
|
||||
|
||||
TYPE_CLASS_MAP = {
|
||||
mention: 'Mention',
|
||||
reblog: 'Status',
|
||||
follow: 'Follow',
|
||||
follow_request: 'FollowRequest',
|
||||
favourite: 'Favourite',
|
||||
poll: 'Poll',
|
||||
LEGACY_TYPE_CLASS_MAP = {
|
||||
'Mention' => :mention,
|
||||
'Status' => :reblog,
|
||||
'Follow' => :follow,
|
||||
'FollowRequest' => :follow_request,
|
||||
'Favourite' => :favourite,
|
||||
'Poll' => :poll,
|
||||
}.freeze
|
||||
|
||||
TYPES = %i(
|
||||
mention
|
||||
status
|
||||
reblog
|
||||
follow
|
||||
follow_request
|
||||
favourite
|
||||
poll
|
||||
).freeze
|
||||
|
||||
STATUS_INCLUDES = [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account]].freeze
|
||||
|
||||
belongs_to :account, optional: true
|
||||
@ -38,26 +51,30 @@ class Notification < ApplicationRecord
|
||||
belongs_to :favourite, foreign_type: 'Favourite', foreign_key: 'activity_id', optional: true
|
||||
belongs_to :poll, foreign_type: 'Poll', foreign_key: 'activity_id', optional: true
|
||||
|
||||
validates :account_id, uniqueness: { scope: [:activity_type, :activity_id] }
|
||||
validates :activity_type, inclusion: { in: TYPE_CLASS_MAP.values }
|
||||
validates :type, inclusion: { in: TYPES }
|
||||
|
||||
scope :without_suspended, -> { joins(:from_account).merge(Account.without_suspended) }
|
||||
|
||||
scope :browserable, ->(exclude_types = [], account_id = nil) {
|
||||
types = TYPE_CLASS_MAP.values - activity_types_from_types(exclude_types)
|
||||
types = TYPES - exclude_types.map(&:to_sym)
|
||||
|
||||
if account_id.nil?
|
||||
where(activity_type: types)
|
||||
where(type: types)
|
||||
else
|
||||
where(activity_type: types, from_account_id: account_id)
|
||||
where(type: types, from_account_id: account_id)
|
||||
end
|
||||
}
|
||||
|
||||
cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account, follow_request: :account, poll: [status: STATUS_INCLUDES]
|
||||
|
||||
def type
|
||||
@type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym
|
||||
@type ||= (super || LEGACY_TYPE_CLASS_MAP[activity_type]).to_sym
|
||||
end
|
||||
|
||||
def target_status
|
||||
case type
|
||||
when :status
|
||||
status
|
||||
when :reblog
|
||||
status&.reblog
|
||||
when :favourite
|
||||
@ -86,10 +103,6 @@ class Notification < ApplicationRecord
|
||||
item.target_status.account = accounts[item.target_status.account_id] if item.target_status
|
||||
end
|
||||
end
|
||||
|
||||
def activity_types_from_types(types)
|
||||
types.map { |type| TYPE_CLASS_MAP[type.to_sym] }.compact
|
||||
end
|
||||
end
|
||||
|
||||
after_initialize :set_from_account
|
||||
|
@ -25,7 +25,7 @@ class Poll < ApplicationRecord
|
||||
belongs_to :account
|
||||
belongs_to :status
|
||||
|
||||
has_many :votes, class_name: 'PollVote', inverse_of: :poll, dependent: :destroy
|
||||
has_many :votes, class_name: 'PollVote', inverse_of: :poll, dependent: :delete_all
|
||||
|
||||
has_many :notifications, as: :activity, dependent: :destroy
|
||||
|
||||
|
@ -72,6 +72,7 @@ class PreviewCard < ApplicationRecord
|
||||
class << self
|
||||
private
|
||||
|
||||
# rubocop:disable Naming/MethodParameterName
|
||||
def image_styles(f)
|
||||
styles = {
|
||||
original: {
|
||||
@ -85,6 +86,7 @@ class PreviewCard < ApplicationRecord
|
||||
styles[:original][:format] = 'jpg' if f.instance.image_content_type == 'image/gif'
|
||||
styles
|
||||
end
|
||||
# rubocop:enable Naming/MethodParameterName
|
||||
end
|
||||
|
||||
private
|
||||
|
98
app/models/public_feed.rb
Normal file
98
app/models/public_feed.rb
Normal file
@ -0,0 +1,98 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class PublicFeed < Feed
|
||||
# @param [Account] account
|
||||
# @param [Hash] options
|
||||
# @option [Boolean] :with_replies
|
||||
# @option [Boolean] :with_reblogs
|
||||
# @option [Boolean] :local
|
||||
# @option [Boolean] :remote
|
||||
# @option [Boolean] :only_media
|
||||
def initialize(account, options = {})
|
||||
@account = account
|
||||
@options = options
|
||||
end
|
||||
|
||||
# @param [Integer] limit
|
||||
# @param [Integer] max_id
|
||||
# @param [Integer] since_id
|
||||
# @param [Integer] min_id
|
||||
# @return [Array<Status>]
|
||||
def get(limit, max_id = nil, since_id = nil, min_id = nil)
|
||||
scope = public_scope
|
||||
|
||||
scope.merge!(without_replies_scope) unless with_replies?
|
||||
scope.merge!(without_reblogs_scope) unless with_reblogs?
|
||||
scope.merge!(local_only_scope) if local_only?
|
||||
scope.merge!(remote_only_scope) if remote_only?
|
||||
if account?
|
||||
scope.merge!(account_filters_scope)
|
||||
else
|
||||
scope.merge!(instance_only_statuses_scope)
|
||||
end
|
||||
scope.merge!(media_only_scope) if media_only?
|
||||
|
||||
scope.cache_ids.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def with_reblogs?
|
||||
@options[:with_reblogs]
|
||||
end
|
||||
|
||||
def with_replies?
|
||||
@options[:with_replies]
|
||||
end
|
||||
|
||||
def local_only?
|
||||
@options[:local]
|
||||
end
|
||||
|
||||
def remote_only?
|
||||
@options[:remote]
|
||||
end
|
||||
|
||||
def account?
|
||||
@account.present?
|
||||
end
|
||||
|
||||
def media_only?
|
||||
@options[:only_media]
|
||||
end
|
||||
|
||||
def public_scope
|
||||
Status.with_public_visibility.joins(:account).merge(Account.without_suspended.without_silenced)
|
||||
end
|
||||
|
||||
def local_only_scope
|
||||
Status.local
|
||||
end
|
||||
|
||||
def remote_only_scope
|
||||
Status.remote
|
||||
end
|
||||
|
||||
def without_replies_scope
|
||||
Status.without_replies
|
||||
end
|
||||
|
||||
def without_reblogs_scope
|
||||
Status.without_reblogs
|
||||
end
|
||||
|
||||
def media_only_scope
|
||||
Status.joins(:media_attachments).group(:id)
|
||||
end
|
||||
|
||||
def instance_only_statuses_scope
|
||||
Status.where(local_only: [false, nil])
|
||||
end
|
||||
|
||||
def account_filters_scope
|
||||
Status.not_excluded_by_account(@account).tap do |scope|
|
||||
scope.merge!(Status.not_domain_blocked_by_account(@account)) unless local_only?
|
||||
scope.merge!(Status.in_chosen_languages(@account)) if @account.chosen_languages.present?
|
||||
end
|
||||
end
|
||||
end
|
@ -14,6 +14,7 @@
|
||||
# target_account_id :bigint(8) not null
|
||||
# assigned_account_id :bigint(8)
|
||||
# uri :string
|
||||
# forwarded :boolean
|
||||
#
|
||||
|
||||
class Report < ApplicationRecord
|
||||
|
@ -70,12 +70,16 @@ class SessionActivation < ApplicationRecord
|
||||
end
|
||||
|
||||
def assign_access_token
|
||||
superapp = Doorkeeper::Application.find_by(superapp: true)
|
||||
self.access_token = Doorkeeper::AccessToken.create!(access_token_attributes)
|
||||
end
|
||||
|
||||
self.access_token = Doorkeeper::AccessToken.create!(application_id: superapp&.id,
|
||||
resource_owner_id: user_id,
|
||||
scopes: 'read write follow',
|
||||
expires_in: Doorkeeper.configuration.access_token_expires_in,
|
||||
use_refresh_token: Doorkeeper.configuration.refresh_token_enabled?)
|
||||
def access_token_attributes
|
||||
{
|
||||
application_id: Doorkeeper::Application.find_by(superapp: true)&.id,
|
||||
resource_owner_id: user_id,
|
||||
scopes: 'read write follow',
|
||||
expires_in: Doorkeeper.configuration.access_token_expires_in,
|
||||
use_refresh_token: Doorkeeper.configuration.refresh_token_enabled?,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
@ -86,24 +86,24 @@ class Status < ApplicationRecord
|
||||
scope :recent, -> { reorder(id: :desc) }
|
||||
scope :remote, -> { where(local: false).where.not(uri: nil) }
|
||||
scope :local, -> { where(local: true).or(where(uri: nil)) }
|
||||
|
||||
scope :with_accounts, ->(ids) { where(id: ids).includes(:account) }
|
||||
scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') }
|
||||
scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') }
|
||||
scope :without_local_only, -> { where(local_only: [false, nil]) }
|
||||
scope :with_public_visibility, -> { where(visibility: :public) }
|
||||
scope :tagged_with, ->(tag) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag }) }
|
||||
scope :tagged_with, ->(tag_ids) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag_ids }) }
|
||||
scope :in_chosen_languages, ->(account) { where(language: nil).or where(language: account.chosen_languages) }
|
||||
scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced_at: nil }) }
|
||||
scope :including_silenced_accounts, -> { left_outer_joins(:account).where.not(accounts: { silenced_at: nil }) }
|
||||
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|
|
||||
scope :tagged_with_all, ->(tag_ids) {
|
||||
Array(tag_ids).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|
|
||||
scope :tagged_with_none, ->(tag_ids) {
|
||||
Array(tag_ids).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
|
||||
@ -234,14 +234,6 @@ class Status < ApplicationRecord
|
||||
@emojis = CustomEmoji.from_text(fields.join(' '), account.domain)
|
||||
end
|
||||
|
||||
def mark_for_mass_destruction!
|
||||
@marked_for_mass_destruction = true
|
||||
end
|
||||
|
||||
def marked_for_mass_destruction?
|
||||
@marked_for_mass_destruction
|
||||
end
|
||||
|
||||
def replies_count
|
||||
status_stat&.replies_count || 0
|
||||
end
|
||||
@ -285,26 +277,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_public_timeline(account = nil, local_only = false)
|
||||
query = timeline_scope(local_only).without_replies
|
||||
|
||||
apply_timeline_filters(query, account, [:local, true].include?(local_only))
|
||||
end
|
||||
|
||||
def as_tag_timeline(tag, account = nil, local_only = false)
|
||||
query = timeline_scope(local_only).tagged_with(tag)
|
||||
|
||||
apply_timeline_filters(query, account, local_only)
|
||||
end
|
||||
|
||||
def as_outbox_timeline(account)
|
||||
where(account: account, visibility: :public)
|
||||
end
|
||||
|
||||
def favourites_map(status_ids, account_id)
|
||||
Favourite.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |f, h| h[f.status_id] = true }
|
||||
end
|
||||
@ -381,51 +353,6 @@ class Status < ApplicationRecord
|
||||
status&.distributable? ? status : nil
|
||||
end.compact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def timeline_scope(scope = false)
|
||||
starting_scope = case scope
|
||||
when :local, true
|
||||
Status.local
|
||||
when :remote
|
||||
Status.remote
|
||||
else
|
||||
Status
|
||||
end
|
||||
|
||||
starting_scope
|
||||
.with_public_visibility
|
||||
.without_reblogs
|
||||
end
|
||||
|
||||
def apply_timeline_filters(query, account, local_only)
|
||||
if account.nil?
|
||||
filter_timeline_default(query)
|
||||
else
|
||||
filter_timeline_for_account(query, account, local_only)
|
||||
end
|
||||
end
|
||||
|
||||
def filter_timeline_for_account(query, account, local_only)
|
||||
query = query.not_excluded_by_account(account)
|
||||
query = query.not_domain_blocked_by_account(account) unless local_only
|
||||
query = query.in_chosen_languages(account) if account.chosen_languages.present?
|
||||
query.merge(account_silencing_filter(account))
|
||||
end
|
||||
|
||||
def filter_timeline_default(query)
|
||||
query.without_local_only.excluding_silenced_accounts
|
||||
end
|
||||
|
||||
def account_silencing_filter(account)
|
||||
if account.silenced?
|
||||
including_myself = left_outer_joins(:account).where(account_id: account.id).references(:accounts)
|
||||
excluding_silenced_accounts.or(including_myself)
|
||||
else
|
||||
excluding_silenced_accounts
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def status_stat
|
||||
@ -507,7 +434,7 @@ class Status < ApplicationRecord
|
||||
end
|
||||
|
||||
def decrement_counter_caches
|
||||
return if direct_visibility? || marked_for_mass_destruction?
|
||||
return if direct_visibility?
|
||||
|
||||
account&.decrement_count!(:statuses_count)
|
||||
reblog&.decrement_count!(:reblogs_count) if reblog?
|
||||
@ -517,7 +444,7 @@ class Status < ApplicationRecord
|
||||
def unlink_from_conversations
|
||||
return unless direct_visibility?
|
||||
|
||||
mentioned_accounts = mentions.includes(:account).map(&:account)
|
||||
mentioned_accounts = (association(:mentions).loaded? ? mentions : mentions.includes(:account)).map(&:account)
|
||||
inbox_owners = mentioned_accounts.select(&:local?) + (account.local? ? [account] : [])
|
||||
|
||||
inbox_owners.each do |inbox_owner|
|
||||
|
@ -39,7 +39,7 @@ class Tag < ApplicationRecord
|
||||
scope :listable, -> { where(listable: [true, nil]) }
|
||||
scope :trendable, -> { Setting.trendable_by_default ? where(trendable: [true, nil]) : where(trendable: true) }
|
||||
scope :discoverable, -> { listable.joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).order(Arel.sql('account_tag_stats.accounts_count desc')) }
|
||||
scope :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('count(*) desc')) }
|
||||
scope :recently_used, ->(account) { joins(:statuses).where(statuses: { id: account.statuses.select(:id).limit(1000) }).group(:id).order(Arel.sql('count(*) desc')) }
|
||||
scope :matches_name, ->(value) { where(arel_table[:name].matches("#{value}%")) }
|
||||
|
||||
delegate :accounts_count,
|
||||
@ -126,7 +126,7 @@ class Tag < ApplicationRecord
|
||||
end
|
||||
|
||||
def search_for(term, limit = 5, offset = 0, options = {})
|
||||
normalized_term = normalize(term.strip).mb_chars.downcase.to_s
|
||||
normalized_term = normalize(term.strip)
|
||||
pattern = sanitize_sql_like(normalized_term) + '%'
|
||||
query = Tag.listable.where(arel_table[:name].lower.matches(pattern))
|
||||
query = query.where(arel_table[:name].lower.eq(normalized_term).or(arel_table[:reviewed_at].not_eq(nil))) if options[:exclude_unreviewed]
|
||||
|
57
app/models/tag_feed.rb
Normal file
57
app/models/tag_feed.rb
Normal file
@ -0,0 +1,57 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TagFeed < PublicFeed
|
||||
LIMIT_PER_MODE = 4
|
||||
|
||||
# @param [Tag] tag
|
||||
# @param [Account] account
|
||||
# @param [Hash] options
|
||||
# @option [Enumerable<String>] :any
|
||||
# @option [Enumerable<String>] :all
|
||||
# @option [Enumerable<String>] :none
|
||||
# @option [Boolean] :local
|
||||
# @option [Boolean] :remote
|
||||
# @option [Boolean] :only_media
|
||||
def initialize(tag, account, options = {})
|
||||
@tag = tag
|
||||
@account = account
|
||||
@options = options
|
||||
end
|
||||
|
||||
# @param [Integer] limit
|
||||
# @param [Integer] max_id
|
||||
# @param [Integer] since_id
|
||||
# @param [Integer] min_id
|
||||
# @return [Array<Status>]
|
||||
def get(limit, max_id = nil, since_id = nil, min_id = nil)
|
||||
scope = public_scope
|
||||
|
||||
scope.merge!(tagged_with_any_scope)
|
||||
scope.merge!(tagged_with_all_scope)
|
||||
scope.merge!(tagged_with_none_scope)
|
||||
scope.merge!(local_only_scope) if local_only?
|
||||
scope.merge!(remote_only_scope) if remote_only?
|
||||
scope.merge!(account_filters_scope) if account?
|
||||
scope.merge!(media_only_scope) if media_only?
|
||||
|
||||
scope.cache_ids.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tagged_with_any_scope
|
||||
Status.group(:id).tagged_with(tags_for(Array(@tag.name) | Array(@options[:any])))
|
||||
end
|
||||
|
||||
def tagged_with_all_scope
|
||||
Status.group(:id).tagged_with_all(tags_for(@options[:all]))
|
||||
end
|
||||
|
||||
def tagged_with_none_scope
|
||||
Status.group(:id).tagged_with_none(tags_for(@options[:none]))
|
||||
end
|
||||
|
||||
def tags_for(names)
|
||||
Tag.matching_name(Array(names).take(LIMIT_PER_MODE)).pluck(:id) if names.present?
|
||||
end
|
||||
end
|
@ -12,6 +12,8 @@
|
||||
class UnavailableDomain < ApplicationRecord
|
||||
include DomainNormalizable
|
||||
|
||||
validates :domain, presence: true, uniqueness: true
|
||||
|
||||
after_commit :reset_cache!
|
||||
|
||||
private
|
||||
|
@ -40,6 +40,8 @@
|
||||
# approved :boolean default(TRUE), not null
|
||||
# sign_in_token :string
|
||||
# sign_in_token_sent_at :datetime
|
||||
# webauthn_id :string
|
||||
# sign_up_ip :inet
|
||||
#
|
||||
|
||||
class User < ApplicationRecord
|
||||
@ -61,7 +63,7 @@ class User < ApplicationRecord
|
||||
devise :two_factor_backupable,
|
||||
otp_number_of_backup_codes: 10
|
||||
|
||||
devise :registerable, :recoverable, :rememberable, :trackable, :validatable,
|
||||
devise :registerable, :recoverable, :rememberable, :validatable,
|
||||
:confirmable
|
||||
|
||||
include Omniauthable
|
||||
@ -77,15 +79,24 @@ class User < ApplicationRecord
|
||||
has_many :backups, inverse_of: :user
|
||||
has_many :invites, inverse_of: :user
|
||||
has_many :markers, inverse_of: :user, dependent: :destroy
|
||||
has_many :webauthn_credentials, dependent: :destroy
|
||||
|
||||
has_one :invite_request, class_name: 'UserInviteRequest', inverse_of: :user, dependent: :destroy
|
||||
accepts_nested_attributes_for :invite_request, reject_if: ->(attributes) { attributes['text'].blank? }
|
||||
accepts_nested_attributes_for :invite_request, reject_if: ->(attributes) { attributes['text'].blank? && !Setting.require_invite_text }
|
||||
validates :invite_request, presence: true, on: :create, if: :invite_text_required?
|
||||
|
||||
validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale?
|
||||
validates_with BlacklistedEmailValidator, on: :create
|
||||
validates_with EmailMxValidator, if: :validate_email_dns?
|
||||
validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create
|
||||
|
||||
# Those are honeypot/antispam fields
|
||||
attr_accessor :registration_form_time, :website, :confirm_password
|
||||
|
||||
validates_with RegistrationFormTimeValidator, on: :create
|
||||
validates :website, absence: true, on: :create
|
||||
validates :confirm_password, absence: true, on: :create
|
||||
|
||||
scope :recent, -> { order(id: :desc) }
|
||||
scope :pending, -> { where(approved: false) }
|
||||
scope :approved, -> { where(approved: true) }
|
||||
@ -95,7 +106,7 @@ class User < ApplicationRecord
|
||||
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_at: nil }) }
|
||||
scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) }
|
||||
scope :matches_ip, ->(value) { left_joins(:session_activations).where('users.current_sign_in_ip <<= ?', value).or(left_joins(:session_activations).where('users.last_sign_in_ip <<= ?', value)).or(left_joins(:session_activations).where('session_activations.ip <<= ?', value)) }
|
||||
scope :matches_ip, ->(value) { left_joins(:session_activations).where('users.current_sign_in_ip <<= ?', value).or(left_joins(:session_activations).where('users.sign_up_ip <<= ?', value)).or(left_joins(:session_activations).where('users.last_sign_in_ip <<= ?', value)).or(left_joins(:session_activations).where('session_activations.ip <<= ?', value)) }
|
||||
scope :emailable, -> { confirmed.enabled.joins(:account).merge(Account.searchable) }
|
||||
|
||||
before_validation :sanitize_languages
|
||||
@ -112,11 +123,12 @@ 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, :aggregate_reblogs, :show_application,
|
||||
:advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images, :default_federation,
|
||||
:advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images,
|
||||
:disable_swiping, :default_federation,
|
||||
to: :settings, prefix: :setting, allow_nil: false
|
||||
|
||||
attr_reader :invite_code, :sign_in_token_attempt
|
||||
attr_writer :external
|
||||
attr_writer :external, :bypass_invite_request_check
|
||||
|
||||
def confirmed?
|
||||
confirmed_at.present?
|
||||
@ -161,12 +173,30 @@ class User < ApplicationRecord
|
||||
prepare_new_user! if new_user && approved?
|
||||
end
|
||||
|
||||
def update_sign_in!(request, new_sign_in: false)
|
||||
old_current, new_current = current_sign_in_at, Time.now.utc
|
||||
self.last_sign_in_at = old_current || new_current
|
||||
self.current_sign_in_at = new_current
|
||||
|
||||
old_current, new_current = current_sign_in_ip, request.remote_ip
|
||||
self.last_sign_in_ip = old_current || new_current
|
||||
self.current_sign_in_ip = new_current
|
||||
|
||||
if new_sign_in
|
||||
self.sign_in_count ||= 0
|
||||
self.sign_in_count += 1
|
||||
end
|
||||
|
||||
save(validate: false) unless new_record?
|
||||
prepare_returning_user!
|
||||
end
|
||||
|
||||
def pending?
|
||||
!approved?
|
||||
end
|
||||
|
||||
def active_for_authentication?
|
||||
true
|
||||
!account.memorial?
|
||||
end
|
||||
|
||||
def suspicious_sign_in?(ip)
|
||||
@ -174,7 +204,7 @@ class User < ApplicationRecord
|
||||
end
|
||||
|
||||
def functional?
|
||||
confirmed? && approved? && !disabled? && !account.suspended? && account.moved_to_account_id.nil?
|
||||
confirmed? && approved? && !disabled? && !account.suspended? && !account.memorial? && account.moved_to_account_id.nil?
|
||||
end
|
||||
|
||||
def unconfirmed_or_pending?
|
||||
@ -192,14 +222,25 @@ class User < ApplicationRecord
|
||||
prepare_new_user!
|
||||
end
|
||||
|
||||
def update_tracked_fields!(request)
|
||||
super
|
||||
prepare_returning_user!
|
||||
def otp_enabled?
|
||||
otp_required_for_login
|
||||
end
|
||||
|
||||
def webauthn_enabled?
|
||||
webauthn_credentials.any?
|
||||
end
|
||||
|
||||
def two_factor_enabled?
|
||||
otp_required_for_login? || webauthn_credentials.any?
|
||||
end
|
||||
|
||||
def disable_two_factor!
|
||||
self.otp_required_for_login = false
|
||||
self.otp_secret = nil
|
||||
otp_backup_codes&.clear
|
||||
|
||||
webauthn_credentials.destroy_all if webauthn_enabled?
|
||||
|
||||
save!
|
||||
end
|
||||
|
||||
@ -235,16 +276,16 @@ class User < ApplicationRecord
|
||||
@shows_application ||= settings.show_application
|
||||
end
|
||||
|
||||
# rubocop:disable Naming/MethodParameterName
|
||||
def token_for_app(a)
|
||||
return nil if a.nil? || a.owner != self
|
||||
Doorkeeper::AccessToken
|
||||
.find_or_create_by(application_id: a.id, resource_owner_id: id) do |t|
|
||||
|
||||
Doorkeeper::AccessToken.find_or_create_by(application_id: a.id, resource_owner_id: id) do |t|
|
||||
t.scopes = a.scopes
|
||||
t.expires_in = Doorkeeper.configuration.access_token_expires_in
|
||||
t.use_refresh_token = Doorkeeper.configuration.refresh_token_enabled?
|
||||
end
|
||||
end
|
||||
# rubocop:enable Naming/MethodParameterName
|
||||
|
||||
def activate_session(request)
|
||||
session_activations.activate(session_id: SecureRandom.hex,
|
||||
@ -312,6 +353,7 @@ class User < ApplicationRecord
|
||||
|
||||
arr << [current_sign_in_at, current_sign_in_ip] if current_sign_in_ip.present?
|
||||
arr << [last_sign_in_at, last_sign_in_ip] if last_sign_in_ip.present?
|
||||
arr << [created_at, sign_up_ip] if sign_up_ip.present?
|
||||
|
||||
arr.sort_by { |pair| pair.first || Time.now.utc }.uniq(&:last).reverse!
|
||||
end
|
||||
@ -366,7 +408,17 @@ class User < ApplicationRecord
|
||||
end
|
||||
|
||||
def set_approved
|
||||
self.approved = open_registrations? || valid_invitation? || external?
|
||||
self.approved = begin
|
||||
if sign_up_from_ip_requires_approval?
|
||||
false
|
||||
else
|
||||
open_registrations? || valid_invitation? || external?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def sign_up_from_ip_requires_approval?
|
||||
!sign_up_ip.nil? && IpBlock.where(severity: :sign_up_requires_approval).where('ip >>= ?', sign_up_ip.to_s).exists?
|
||||
end
|
||||
|
||||
def open_registrations?
|
||||
@ -377,6 +429,10 @@ class User < ApplicationRecord
|
||||
!!@external
|
||||
end
|
||||
|
||||
def bypass_invite_request_check?
|
||||
@bypass_invite_request_check
|
||||
end
|
||||
|
||||
def sanitize_languages
|
||||
return if chosen_languages.nil?
|
||||
chosen_languages.reject!(&:blank?)
|
||||
@ -395,7 +451,7 @@ class User < ApplicationRecord
|
||||
end
|
||||
|
||||
def notify_staff_about_pending_account!
|
||||
User.staff.includes(:account).each do |u|
|
||||
User.staff.includes(:account).find_each do |u|
|
||||
next unless u.allows_pending_account_emails?
|
||||
AdminMailer.new_pending_account(u.account, self).deliver_later
|
||||
end
|
||||
@ -414,4 +470,8 @@ class User < ApplicationRecord
|
||||
def validate_email_dns?
|
||||
email_changed? && !(Rails.env.test? || Rails.env.development?)
|
||||
end
|
||||
|
||||
def invite_text_required?
|
||||
Setting.require_invite_text && !invited? && !external? && !bypass_invite_request_check?
|
||||
end
|
||||
end
|
||||
|
22
app/models/webauthn_credential.rb
Normal file
22
app/models/webauthn_credential.rb
Normal file
@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: webauthn_credentials
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# external_id :string not null
|
||||
# public_key :string not null
|
||||
# nickname :string not null
|
||||
# sign_count :bigint(8) default(0), not null
|
||||
# user_id :bigint(8)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class WebauthnCredential < ApplicationRecord
|
||||
validates :external_id, :public_key, :nickname, :sign_count, presence: true
|
||||
validates :external_id, uniqueness: true
|
||||
validates :nickname, uniqueness: { scope: :user_id }
|
||||
validates :sign_count,
|
||||
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**63 - 1 }
|
||||
end
|
Reference in New Issue
Block a user