Merge pull request #2579 from ClearlyClaire/glitch-soc/merge-upstream

Merge upstream changes up to f866413e72
This commit is contained in:
Claire 2024-01-19 12:09:01 +01:00 committed by GitHub
commit 915cd36ac1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 226 additions and 300 deletions

View file

@ -82,12 +82,9 @@ Rails/WhereExists:
- 'app/lib/feed_manager.rb' - 'app/lib/feed_manager.rb'
- 'app/lib/status_cache_hydrator.rb' - 'app/lib/status_cache_hydrator.rb'
- 'app/lib/suspicious_sign_in_detector.rb' - 'app/lib/suspicious_sign_in_detector.rb'
- 'app/models/concerns/account/interactions.rb'
- 'app/models/featured_tag.rb'
- 'app/models/poll.rb' - 'app/models/poll.rb'
- 'app/models/session_activation.rb' - 'app/models/session_activation.rb'
- 'app/models/status.rb' - 'app/models/status.rb'
- 'app/models/user.rb'
- 'app/policies/status_policy.rb' - 'app/policies/status_policy.rb'
- 'app/serializers/rest/announcement_serializer.rb' - 'app/serializers/rest/announcement_serializer.rb'
- 'app/serializers/rest/tag_serializer.rb' - 'app/serializers/rest/tag_serializer.rb'

View file

@ -27,7 +27,7 @@ class Api::V1::Peers::SearchController < Api::BaseController
@domains = InstancesIndex.query(function_score: { @domains = InstancesIndex.query(function_score: {
query: { query: {
prefix: { prefix: {
domain: TagManager.instance.normalize_domain(params[:q].strip), domain: normalized_domain,
}, },
}, },
@ -37,11 +37,18 @@ class Api::V1::Peers::SearchController < Api::BaseController
}, },
}).limit(10).pluck(:domain) }).limit(10).pluck(:domain)
else else
domain = params[:q].strip domain = normalized_domain
domain = TagManager.instance.normalize_domain(domain) @domains = Instance.searchable.domain_starts_with(domain).limit(10).pluck(:domain)
@domains = Instance.searchable.where(Instance.arel_table[:domain].matches("#{Instance.sanitize_sql_like(domain)}%", false, true)).limit(10).pluck(:domain)
end end
rescue Addressable::URI::InvalidURIError rescue Addressable::URI::InvalidURIError
@domains = [] @domains = []
end end
def normalized_domain
TagManager.instance.normalize_domain(query_value)
end
def query_value
params[:q].strip
end
end end

View file

@ -129,7 +129,6 @@ class Account < ApplicationRecord
scope :alphabetic, -> { order(domain: :asc, username: :asc) } scope :alphabetic, -> { order(domain: :asc, username: :asc) }
scope :matches_username, ->(value) { where('lower((username)::text) LIKE lower(?)', "#{value}%") } scope :matches_username, ->(value) { where('lower((username)::text) LIKE lower(?)', "#{value}%") }
scope :matches_display_name, ->(value) { where(arel_table[:display_name].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 :without_unapproved, -> { left_outer_joins(:user).merge(User.approved.confirmed).or(remote) } scope :without_unapproved, -> { left_outer_joins(:user).merge(User.approved.confirmed).or(remote) }
scope :searchable, -> { without_unapproved.without_suspended.where(moved_to_account_id: nil) } scope :searchable, -> { without_unapproved.without_suspended.where(moved_to_account_id: nil) }
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).joins(:account_stat) } scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).joins(:account_stat) }

View file

@ -20,8 +20,11 @@ class Appeal < ApplicationRecord
belongs_to :account belongs_to :account
belongs_to :strike, class_name: 'AccountWarning', foreign_key: 'account_warning_id', inverse_of: :appeal belongs_to :strike, class_name: 'AccountWarning', foreign_key: 'account_warning_id', inverse_of: :appeal
belongs_to :approved_by_account, class_name: 'Account', optional: true
belongs_to :rejected_by_account, class_name: 'Account', optional: true with_options class_name: 'Account', optional: true do
belongs_to :approved_by_account
belongs_to :rejected_by_account
end
validates :text, presence: true, length: { maximum: 2_000 } validates :text, presence: true, length: { maximum: 2_000 }
validates :account_warning_id, uniqueness: true validates :account_warning_id, uniqueness: true

View file

@ -183,7 +183,7 @@ module Account::Interactions
end end
def following?(other_account) def following?(other_account)
active_relationships.where(target_account: other_account).exists? active_relationships.exists?(target_account: other_account)
end end
def following_anyone? def following_anyone?
@ -199,51 +199,51 @@ module Account::Interactions
end end
def blocking?(other_account) def blocking?(other_account)
block_relationships.where(target_account: other_account).exists? block_relationships.exists?(target_account: other_account)
end end
def domain_blocking?(other_domain) def domain_blocking?(other_domain)
domain_blocks.where(domain: other_domain).exists? domain_blocks.exists?(domain: other_domain)
end end
def muting?(other_account) def muting?(other_account)
mute_relationships.where(target_account: other_account).exists? mute_relationships.exists?(target_account: other_account)
end end
def muting_conversation?(conversation) def muting_conversation?(conversation)
conversation_mutes.where(conversation: conversation).exists? conversation_mutes.exists?(conversation: conversation)
end end
def muting_notifications?(other_account) def muting_notifications?(other_account)
mute_relationships.where(target_account: other_account, hide_notifications: true).exists? mute_relationships.exists?(target_account: other_account, hide_notifications: true)
end end
def muting_reblogs?(other_account) def muting_reblogs?(other_account)
active_relationships.where(target_account: other_account, show_reblogs: false).exists? active_relationships.exists?(target_account: other_account, show_reblogs: false)
end end
def requested?(other_account) def requested?(other_account)
follow_requests.where(target_account: other_account).exists? follow_requests.exists?(target_account: other_account)
end end
def favourited?(status) def favourited?(status)
status.proper.favourites.where(account: self).exists? status.proper.favourites.exists?(account: self)
end end
def bookmarked?(status) def bookmarked?(status)
status.proper.bookmarks.where(account: self).exists? status.proper.bookmarks.exists?(account: self)
end end
def reblogged?(status) def reblogged?(status)
status.proper.reblogs.where(account: self).exists? status.proper.reblogs.exists?(account: self)
end end
def pinned?(status) def pinned?(status)
status_pins.where(status: status).exists? status_pins.exists?(status: status)
end end
def endorsed?(account) def endorsed?(account)
account_pins.where(target_account: account).exists? account_pins.exists?(target_account: account)
end end
def status_matches_filters(status) def status_matches_filters(status)

View file

@ -17,8 +17,6 @@ class DomainAllow < ApplicationRecord
validates :domain, presence: true, uniqueness: true, domain: true validates :domain, presence: true, uniqueness: true, domain: true
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
def to_log_human_identifier def to_log_human_identifier
domain domain
end end

View file

@ -28,7 +28,6 @@ class DomainBlock < ApplicationRecord
has_many :accounts, foreign_key: :domain, primary_key: :domain, inverse_of: false, dependent: nil has_many :accounts, foreign_key: :domain, primary_key: :domain, inverse_of: false, dependent: nil
delegate :count, to: :accounts, prefix: true delegate :count, to: :accounts, prefix: true
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
scope :with_user_facing_limitations, -> { where(severity: [:silence, :suspend]) } scope :with_user_facing_limitations, -> { where(severity: [:silence, :suspend]) }
scope :with_limitations, -> { where(severity: [:silence, :suspend]).or(where(reject_media: true)) } scope :with_limitations, -> { where(severity: [:silence, :suspend]).or(where(reject_media: true)) }
scope :by_severity, -> { in_order_of(:severity, %w(noop silence suspend)).order(:domain) } scope :by_severity, -> { in_order_of(:severity, %w(noop silence suspend)).order(:domain) }

View file

@ -21,8 +21,10 @@ class EmailDomainBlock < ApplicationRecord
include DomainNormalizable include DomainNormalizable
include Paginable include Paginable
belongs_to :parent, class_name: 'EmailDomainBlock', optional: true with_options class_name: 'EmailDomainBlock' do
has_many :children, class_name: 'EmailDomainBlock', foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy belongs_to :parent, optional: true
has_many :children, foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy
end
validates :domain, presence: true, uniqueness: true, domain: true validates :domain, presence: true, uniqueness: true, domain: true

View file

@ -45,7 +45,7 @@ class FeaturedTag < ApplicationRecord
end end
def decrement(deleted_status_id) def decrement(deleted_status_id)
update(statuses_count: [0, statuses_count - 1].max, last_status_at: account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).where.not(id: deleted_status_id).select(:created_at).first&.created_at) update(statuses_count: [0, statuses_count - 1].max, last_status_at: visible_tagged_account_statuses.where.not(id: deleted_status_id).select(:created_at).first&.created_at)
end end
private private
@ -55,8 +55,8 @@ class FeaturedTag < ApplicationRecord
end end
def reset_data def reset_data
self.statuses_count = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).count self.statuses_count = visible_tagged_account_statuses.count
self.last_status_at = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).select(:created_at).first&.created_at self.last_status_at = visible_tagged_account_statuses.select(:created_at).first&.created_at
end end
def validate_featured_tags_limit def validate_featured_tags_limit
@ -66,6 +66,14 @@ class FeaturedTag < ApplicationRecord
end end
def validate_tag_uniqueness def validate_tag_uniqueness
errors.add(:name, :taken) if FeaturedTag.by_name(name).where(account_id: account_id).exists? errors.add(:name, :taken) if tag_already_featured_for_account?
end
def tag_already_featured_for_account?
FeaturedTag.by_name(name).exists?(account_id: account_id)
end
def visible_tagged_account_statuses
account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag)
end end
end end

View file

@ -23,6 +23,7 @@ class Instance < ApplicationRecord
scope :searchable, -> { where.not(domain: DomainBlock.select(:domain)) } scope :searchable, -> { where.not(domain: DomainBlock.select(:domain)) }
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
scope :domain_starts_with, ->(value) { where(arel_table[:domain].matches("#{sanitize_sql_like(value)}%", false, true)) }
scope :by_domain_and_subdomains, ->(domain) { where("reverse('.' || domain) LIKE reverse(?)", "%.#{domain}") } scope :by_domain_and_subdomains, ->(domain) { where("reverse('.' || domain) LIKE reverse(?)", "%.#{domain}") }
def self.refresh def self.refresh

View file

@ -27,8 +27,11 @@ class Poll < ApplicationRecord
belongs_to :status belongs_to :status
has_many :votes, class_name: 'PollVote', inverse_of: :poll, dependent: :delete_all has_many :votes, class_name: 'PollVote', inverse_of: :poll, dependent: :delete_all
has_many :voters, -> { group('accounts.id') }, through: :votes, class_name: 'Account', source: :account
has_many :local_voters, -> { group('accounts.id').merge(Account.local) }, through: :votes, class_name: 'Account', source: :account with_options class_name: 'Account', source: :account, through: :votes do
has_many :voters, -> { group('accounts.id') }
has_many :local_voters, -> { group('accounts.id').merge(Account.local) }
end
has_many :notifications, as: :activity, dependent: :destroy has_many :notifications, as: :activity, dependent: :destroy

View file

@ -29,9 +29,12 @@ class Report < ApplicationRecord
rate_limit by: :account, family: :reports rate_limit by: :account, family: :reports
belongs_to :account belongs_to :account
belongs_to :target_account, class_name: 'Account'
belongs_to :action_taken_by_account, class_name: 'Account', optional: true with_options class_name: 'Account' do
belongs_to :assigned_account, class_name: 'Account', optional: true belongs_to :target_account
belongs_to :action_taken_by_account, optional: true
belongs_to :assigned_account, optional: true
end
has_many :notes, class_name: 'ReportNote', inverse_of: :report, dependent: :destroy has_many :notes, class_name: 'ReportNote', inverse_of: :report, dependent: :destroy
has_many :notifications, as: :activity, dependent: :destroy has_many :notifications, as: :activity, dependent: :destroy

View file

@ -61,8 +61,10 @@ class Status < ApplicationRecord
belongs_to :conversation, optional: true belongs_to :conversation, optional: true
belongs_to :preloadable_poll, class_name: 'Poll', foreign_key: 'poll_id', optional: true, inverse_of: false belongs_to :preloadable_poll, class_name: 'Poll', foreign_key: 'poll_id', optional: true, inverse_of: false
belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true with_options class_name: 'Status', optional: true do
belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true belongs_to :thread, foreign_key: 'in_reply_to_id', inverse_of: :replies
belongs_to :reblog, foreign_key: 'reblog_of_id', inverse_of: :reblogs
end
has_many :favourites, inverse_of: :status, dependent: :destroy has_many :favourites, inverse_of: :status, dependent: :destroy
has_many :bookmarks, inverse_of: :status, dependent: :destroy has_many :bookmarks, inverse_of: :status, dependent: :destroy

View file

@ -434,7 +434,7 @@ class User < ApplicationRecord
end end
def sign_up_from_ip_requires_approval? 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? sign_up_ip.present? && IpBlock.sign_up_requires_approval.exists?(['ip >>= ?', sign_up_ip.to_s])
end end
def sign_up_email_requires_approval? def sign_up_email_requires_approval?

View file

@ -20,8 +20,7 @@ class CopyStatusStats < ActiveRecord::Migration[5.2]
private private
def supports_upsert? def supports_upsert?
version = select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i ActiveRecord::Base.connection.database_version >= 90_500
version >= 90_500
end end
def up_fast def up_fast

View file

@ -24,8 +24,7 @@ class CopyAccountStats < ActiveRecord::Migration[5.2]
private private
def supports_upsert? def supports_upsert?
version = select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i ActiveRecord::Base.connection.database_version >= 90_500
version >= 90_500
end end
def up_fast def up_fast

View file

@ -17,8 +17,7 @@ class AddUniqueIndexOnPreviewCardsStatuses < ActiveRecord::Migration[6.1]
def supports_concurrent_reindex? def supports_concurrent_reindex?
@supports_concurrent_reindex ||= begin @supports_concurrent_reindex ||= begin
version = select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i ActiveRecord::Base.connection.database_version >= 120_000
version >= 120_000
end end
end end

View file

@ -223,7 +223,7 @@ module Mastodon::CLI
say 'Deduplicating accounts… for local accounts, you will be asked to chose which account to keep unchanged.' say 'Deduplicating accounts… for local accounts, you will be asked to chose which account to keep unchanged.'
find_duplicate_accounts.each do |row| find_duplicate_accounts.each do |row|
accounts = Account.where(id: row['ids'].split(',')).to_a accounts = Account.where(id: row['ids'].split(','))
if accounts.first.local? if accounts.first.local?
deduplicate_local_accounts!(accounts) deduplicate_local_accounts!(accounts)
@ -275,7 +275,7 @@ module Mastodon::CLI
def deduplicate_users_process_email def deduplicate_users_process_email
ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users GROUP BY email HAVING count(*) > 1").each do |row| ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users GROUP BY email HAVING count(*) > 1").each do |row|
users = User.where(id: row['ids'].split(',')).sort_by(&:updated_at).reverse users = User.where(id: row['ids'].split(',')).order(updated_at: :desc).to_a
ref_user = users.shift ref_user = users.shift
say "Multiple users registered with e-mail address #{ref_user.email}.", :yellow say "Multiple users registered with e-mail address #{ref_user.email}.", :yellow
say "e-mail will be disabled for the following accounts: #{users.map { |user| user.account.acct }.join(', ')}", :yellow say "e-mail will be disabled for the following accounts: #{users.map { |user| user.account.acct }.join(', ')}", :yellow
@ -289,7 +289,7 @@ module Mastodon::CLI
def deduplicate_users_process_confirmation_token def deduplicate_users_process_confirmation_token
ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE confirmation_token IS NOT NULL GROUP BY confirmation_token HAVING count(*) > 1").each do |row| ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE confirmation_token IS NOT NULL GROUP BY confirmation_token HAVING count(*) > 1").each do |row|
users = User.where(id: row['ids'].split(',')).sort_by(&:created_at).reverse.drop(1) users = User.where(id: row['ids'].split(',')).order(created_at: :desc).to_a.drop(1)
say "Unsetting confirmation token for those accounts: #{users.map { |user| user.account.acct }.join(', ')}", :yellow say "Unsetting confirmation token for those accounts: #{users.map { |user| user.account.acct }.join(', ')}", :yellow
users.each do |user| users.each do |user|
@ -301,7 +301,7 @@ module Mastodon::CLI
def deduplicate_users_process_remember_token def deduplicate_users_process_remember_token
if migrator_version < 2022_01_18_183010 if migrator_version < 2022_01_18_183010
ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE remember_token IS NOT NULL GROUP BY remember_token HAVING count(*) > 1").each do |row| ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE remember_token IS NOT NULL GROUP BY remember_token HAVING count(*) > 1").each do |row|
users = User.where(id: row['ids'].split(',')).sort_by(&:updated_at).reverse.drop(1) users = User.where(id: row['ids'].split(',')).order(updated_at: :desc).to_a.drop(1)
say "Unsetting remember token for those accounts: #{users.map { |user| user.account.acct }.join(', ')}", :yellow say "Unsetting remember token for those accounts: #{users.map { |user| user.account.acct }.join(', ')}", :yellow
users.each do |user| users.each do |user|
@ -313,7 +313,7 @@ module Mastodon::CLI
def deduplicate_users_process_password_token def deduplicate_users_process_password_token
ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE reset_password_token IS NOT NULL GROUP BY reset_password_token HAVING count(*) > 1").each do |row| ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE reset_password_token IS NOT NULL GROUP BY reset_password_token HAVING count(*) > 1").each do |row|
users = User.where(id: row['ids'].split(',')).sort_by(&:updated_at).reverse.drop(1) users = User.where(id: row['ids'].split(',')).order(updated_at: :desc).to_a.drop(1)
say "Unsetting password reset token for those accounts: #{users.map { |user| user.account.acct }.join(', ')}", :yellow say "Unsetting password reset token for those accounts: #{users.map { |user| user.account.acct }.join(', ')}", :yellow
users.each do |user| users.each do |user|
@ -341,7 +341,7 @@ module Mastodon::CLI
say 'Removing duplicate account identity proofs…' say 'Removing duplicate account identity proofs…'
ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM account_identity_proofs GROUP BY account_id, provider, provider_username HAVING count(*) > 1").each do |row| ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM account_identity_proofs GROUP BY account_id, provider, provider_username HAVING count(*) > 1").each do |row|
AccountIdentityProof.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy) AccountIdentityProof.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy)
end end
say 'Restoring account identity proofs indexes…' say 'Restoring account identity proofs indexes…'
@ -355,7 +355,7 @@ module Mastodon::CLI
say 'Removing duplicate announcement reactions…' say 'Removing duplicate announcement reactions…'
ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM announcement_reactions GROUP BY account_id, announcement_id, name HAVING count(*) > 1").each do |row| ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM announcement_reactions GROUP BY account_id, announcement_id, name HAVING count(*) > 1").each do |row|
AnnouncementReaction.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy) AnnouncementReaction.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy)
end end
say 'Restoring announcement_reactions indexes…' say 'Restoring announcement_reactions indexes…'
@ -367,7 +367,7 @@ module Mastodon::CLI
say 'Deduplicating conversations…' say 'Deduplicating conversations…'
ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM conversations WHERE uri IS NOT NULL GROUP BY uri HAVING count(*) > 1").each do |row| ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM conversations WHERE uri IS NOT NULL GROUP BY uri HAVING count(*) > 1").each do |row|
conversations = Conversation.where(id: row['ids'].split(',')).sort_by(&:id).reverse conversations = Conversation.where(id: row['ids'].split(',')).order(id: :desc).to_a
ref_conversation = conversations.shift ref_conversation = conversations.shift
@ -390,7 +390,7 @@ module Mastodon::CLI
say 'Deduplicating custom_emojis…' say 'Deduplicating custom_emojis…'
ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM custom_emojis GROUP BY shortcode, domain HAVING count(*) > 1").each do |row| ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM custom_emojis GROUP BY shortcode, domain HAVING count(*) > 1").each do |row|
emojis = CustomEmoji.where(id: row['ids'].split(',')).sort_by(&:id).reverse emojis = CustomEmoji.where(id: row['ids'].split(',')).order(id: :desc).to_a
ref_emoji = emojis.shift ref_emoji = emojis.shift
@ -409,7 +409,7 @@ module Mastodon::CLI
say 'Deduplicating custom_emoji_categories…' say 'Deduplicating custom_emoji_categories…'
ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM custom_emoji_categories GROUP BY name HAVING count(*) > 1").each do |row| ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM custom_emoji_categories GROUP BY name HAVING count(*) > 1").each do |row|
categories = CustomEmojiCategory.where(id: row['ids'].split(',')).sort_by(&:id).reverse categories = CustomEmojiCategory.where(id: row['ids'].split(',')).order(id: :desc).to_a
ref_category = categories.shift ref_category = categories.shift
@ -428,7 +428,7 @@ module Mastodon::CLI
say 'Deduplicating domain_allows…' say 'Deduplicating domain_allows…'
ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM domain_allows GROUP BY domain HAVING count(*) > 1").each do |row| ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM domain_allows GROUP BY domain HAVING count(*) > 1").each do |row|
DomainAllow.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy) DomainAllow.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy)
end end
say 'Restoring domain_allows indexes…' say 'Restoring domain_allows indexes…'
@ -466,7 +466,7 @@ module Mastodon::CLI
say 'Deduplicating unavailable_domains…' say 'Deduplicating unavailable_domains…'
ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM unavailable_domains GROUP BY domain HAVING count(*) > 1").each do |row| ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM unavailable_domains GROUP BY domain HAVING count(*) > 1").each do |row|
UnavailableDomain.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy) UnavailableDomain.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy)
end end
say 'Restoring unavailable_domains indexes…' say 'Restoring unavailable_domains indexes…'
@ -478,7 +478,7 @@ module Mastodon::CLI
say 'Deduplicating email_domain_blocks…' say 'Deduplicating email_domain_blocks…'
ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM email_domain_blocks GROUP BY domain HAVING count(*) > 1").each do |row| ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM email_domain_blocks GROUP BY domain HAVING count(*) > 1").each do |row|
domain_blocks = EmailDomainBlock.where(id: row['ids'].split(',')).sort_by { |b| b.parent.nil? ? 1 : 0 }.to_a domain_blocks = EmailDomainBlock.where(id: row['ids'].split(',')).order(EmailDomainBlock.arel_table[:parent_id].asc.nulls_first).to_a
domain_blocks.drop(1).each(&:destroy) domain_blocks.drop(1).each(&:destroy)
end end
@ -507,7 +507,7 @@ module Mastodon::CLI
say 'Deduplicating preview_cards…' say 'Deduplicating preview_cards…'
ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM preview_cards GROUP BY url HAVING count(*) > 1").each do |row| ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM preview_cards GROUP BY url HAVING count(*) > 1").each do |row|
PreviewCard.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy) PreviewCard.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy)
end end
say 'Restoring preview_cards indexes…' say 'Restoring preview_cards indexes…'
@ -519,7 +519,7 @@ module Mastodon::CLI
say 'Deduplicating statuses…' say 'Deduplicating statuses…'
ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM statuses WHERE uri IS NOT NULL GROUP BY uri HAVING count(*) > 1").each do |row| ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM statuses WHERE uri IS NOT NULL GROUP BY uri HAVING count(*) > 1").each do |row|
statuses = Status.where(id: row['ids'].split(',')).sort_by(&:id) statuses = Status.where(id: row['ids'].split(',')).order(id: :asc).to_a
ref_status = statuses.shift ref_status = statuses.shift
statuses.each do |status| statuses.each do |status|
merge_statuses!(ref_status, status) if status.account_id == ref_status.account_id merge_statuses!(ref_status, status) if status.account_id == ref_status.account_id
@ -541,7 +541,7 @@ module Mastodon::CLI
say 'Deduplicating tags…' say 'Deduplicating tags…'
ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM tags GROUP BY lower((name)::text) HAVING count(*) > 1").each do |row| ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM tags GROUP BY lower((name)::text) HAVING count(*) > 1").each do |row|
tags = Tag.where(id: row['ids'].split(',')).sort_by { |t| [t.usable?, t.trendable?, t.listable?].count(false) } tags = Tag.where(id: row['ids'].split(',')).order(Arel.sql('(usable::int + trendable::int + listable::int) desc')).to_a
ref_tag = tags.shift ref_tag = tags.shift
tags.each do |tag| tags.each do |tag|
merge_tags!(ref_tag, tag) merge_tags!(ref_tag, tag)
@ -564,7 +564,7 @@ module Mastodon::CLI
say 'Deduplicating webauthn_credentials…' say 'Deduplicating webauthn_credentials…'
ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM webauthn_credentials GROUP BY external_id HAVING count(*) > 1").each do |row| ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM webauthn_credentials GROUP BY external_id HAVING count(*) > 1").each do |row|
WebauthnCredential.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy) WebauthnCredential.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy)
end end
say 'Restoring webauthn_credentials indexes…' say 'Restoring webauthn_credentials indexes…'
@ -578,7 +578,7 @@ module Mastodon::CLI
say 'Deduplicating webhooks…' say 'Deduplicating webhooks…'
ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM webhooks GROUP BY url HAVING count(*) > 1").each do |row| ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM webhooks GROUP BY url HAVING count(*) > 1").each do |row|
Webhook.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy) Webhook.where(id: row['ids'].split(',')).order(id: :desc).drop(1).each(&:destroy)
end end
say 'Restoring webhooks indexes…' say 'Restoring webhooks indexes…'
@ -590,8 +590,8 @@ module Mastodon::CLI
SoftwareUpdate.delete_all SoftwareUpdate.delete_all
end end
def deduplicate_local_accounts!(accounts) def deduplicate_local_accounts!(scope)
accounts = accounts.sort_by(&:id).reverse accounts = scope.order(id: :desc).to_a
say "Multiple local accounts were found for username '#{accounts.first.username}'.", :yellow say "Multiple local accounts were found for username '#{accounts.first.username}'.", :yellow
say 'All those accounts are distinct accounts but only the most recently-created one is fully-functional.', :yellow say 'All those accounts are distinct accounts but only the most recently-created one is fully-functional.', :yellow
@ -629,8 +629,8 @@ module Mastodon::CLI
end end
end end
def deduplicate_remote_accounts!(accounts) def deduplicate_remote_accounts!(scope)
accounts = accounts.sort_by(&:updated_at).reverse accounts = scope.order(updated_at: :desc).to_a
reference_account = accounts.shift reference_account = accounts.shift

View file

@ -8,15 +8,15 @@
# shorten temporary column names. # shorten temporary column names.
# Documentation on using these functions (and why one might do so): # Documentation on using these functions (and why one might do so):
# https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/what_requires_downtime.md # https://gitlab.com/gitlab-org/gitlab-foss/-/blob/master/doc/development/database/avoiding_downtime_in_migrations.md
# The file itself: # The original file (since updated):
# https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/database/migration_helpers.rb # https://gitlab.com/gitlab-org/gitlab-foss/-/blob/master/lib/gitlab/database/migration_helpers.rb
# It is licensed as follows: # It is licensed as follows:
# Copyright (c) 2011-2017 GitLab B.V. # Copyright (c) 2011-present GitLab B.V.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights # in the Software without restriction, including without limitation the rights
@ -24,16 +24,16 @@
# copies of the Software, and to permit persons to whom the Software is # copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions: # furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in # The above copyright notice and this permission notice shall be included in all
# all copies or substantial portions of the Software. # copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# THE SOFTWARE. # SOFTWARE.
# This is bad form, but there are enough differences that it's impractical to do # This is bad form, but there are enough differences that it's impractical to do
# otherwise: # otherwise:
@ -77,37 +77,12 @@ module Mastodon
end end
end end
BACKGROUND_MIGRATION_BATCH_SIZE = 1000 # Number of rows to process per job
BACKGROUND_MIGRATION_JOB_BUFFER_SIZE = 1000 # Number of jobs to bulk queue at a time
# Gets an estimated number of rows for a table # Gets an estimated number of rows for a table
def estimate_rows_in_table(table_name) def estimate_rows_in_table(table_name)
exec_query('SELECT reltuples FROM pg_class WHERE relname = ' + exec_query('SELECT reltuples FROM pg_class WHERE relname = ' +
"'#{table_name}'").to_a.first['reltuples'] "'#{table_name}'").to_a.first['reltuples']
end end
# Adds `created_at` and `updated_at` columns with timezone information.
#
# This method is an improved version of Rails' built-in method `add_timestamps`.
#
# Available options are:
# default - The default value for the column.
# null - When set to `true` the column will allow NULL values.
# The default is to not allow NULL values.
def add_timestamps_with_timezone(table_name, **options)
options[:null] = false if options[:null].nil?
[:created_at, :updated_at].each do |column_name|
if options[:default] && transaction_open?
raise '`add_timestamps_with_timezone` with default value cannot be run inside a transaction. ' \
'You can disable transactions by calling `disable_ddl_transaction!` ' \
'in the body of your migration class'
end
add_column(table_name, column_name, :datetime_with_timezone, **options)
end
end
# Creates a new index, concurrently when supported # Creates a new index, concurrently when supported
# #
# On PostgreSQL this method creates an index concurrently, on MySQL this # On PostgreSQL this method creates an index concurrently, on MySQL this
@ -746,39 +721,6 @@ module Mastodon
rename_index table_name, "#{index_name}_new", index_name rename_index table_name, "#{index_name}_new", index_name
end end
# This will replace the first occurrence of a string in a column with
# the replacement
# On postgresql we can use `regexp_replace` for that.
# On mysql we find the location of the pattern, and overwrite it
# with the replacement
def replace_sql(column, pattern, replacement)
quoted_pattern = Arel::Nodes::Quoted.new(pattern.to_s)
quoted_replacement = Arel::Nodes::Quoted.new(replacement.to_s)
replace = Arel::Nodes::NamedFunction
.new("regexp_replace", [column, quoted_pattern, quoted_replacement])
Arel::Nodes::SqlLiteral.new(replace.to_sql)
end
def remove_foreign_key_without_error(*args)
remove_foreign_key(*args)
rescue ArgumentError
end
def sidekiq_queue_migrate(queue_from, to:)
while sidekiq_queue_length(queue_from) > 0
Sidekiq.redis do |conn|
conn.rpoplpush "queue:#{queue_from}", "queue:#{to}"
end
end
end
def sidekiq_queue_length(queue_name)
Sidekiq.redis do |conn|
conn.llen("queue:#{queue_name}")
end
end
def check_trigger_permissions!(table) def check_trigger_permissions!(table)
unless Grant.create_and_execute_trigger?(table) unless Grant.create_and_execute_trigger?(table)
dbname = ActiveRecord::Base.configurations[Rails.env]['database'] dbname = ActiveRecord::Base.configurations[Rails.env]['database']
@ -799,91 +741,6 @@ into similar problems in the future (e.g. when new tables are created).
end end
end end
# Bulk queues background migration jobs for an entire table, batched by ID range.
# "Bulk" meaning many jobs will be pushed at a time for efficiency.
# If you need a delay interval per job, then use `queue_background_migration_jobs_by_range_at_intervals`.
#
# model_class - The table being iterated over
# job_class_name - The background migration job class as a string
# batch_size - The maximum number of rows per job
#
# Example:
#
# class Route < ActiveRecord::Base
# include EachBatch
# self.table_name = 'routes'
# end
#
# bulk_queue_background_migration_jobs_by_range(Route, 'ProcessRoutes')
#
# Where the model_class includes EachBatch, and the background migration exists:
#
# class Gitlab::BackgroundMigration::ProcessRoutes
# def perform(start_id, end_id)
# # do something
# end
# end
def bulk_queue_background_migration_jobs_by_range(model_class, job_class_name, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE)
raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id')
jobs = []
model_class.each_batch(of: batch_size) do |relation|
start_id, end_id = relation.pluck('MIN(id), MAX(id)').first
if jobs.length >= BACKGROUND_MIGRATION_JOB_BUFFER_SIZE
# Note: This code path generally only helps with many millions of rows
# We push multiple jobs at a time to reduce the time spent in
# Sidekiq/Redis operations. We're using this buffer based approach so we
# don't need to run additional queries for every range.
BackgroundMigrationWorker.perform_bulk(jobs)
jobs.clear
end
jobs << [job_class_name, [start_id, end_id]]
end
BackgroundMigrationWorker.perform_bulk(jobs) unless jobs.empty?
end
# Queues background migration jobs for an entire table, batched by ID range.
# Each job is scheduled with a `delay_interval` in between.
# If you use a small interval, then some jobs may run at the same time.
#
# model_class - The table being iterated over
# job_class_name - The background migration job class as a string
# delay_interval - The duration between each job's scheduled time (must respond to `to_f`)
# batch_size - The maximum number of rows per job
#
# Example:
#
# class Route < ActiveRecord::Base
# include EachBatch
# self.table_name = 'routes'
# end
#
# queue_background_migration_jobs_by_range_at_intervals(Route, 'ProcessRoutes', 1.minute)
#
# Where the model_class includes EachBatch, and the background migration exists:
#
# class Gitlab::BackgroundMigration::ProcessRoutes
# def perform(start_id, end_id)
# # do something
# end
# end
def queue_background_migration_jobs_by_range_at_intervals(model_class, job_class_name, delay_interval, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE)
raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id')
model_class.each_batch(of: batch_size) do |relation, index|
start_id, end_id = relation.pluck('MIN(id), MAX(id)').first
# `BackgroundMigrationWorker.bulk_perform_in` schedules all jobs for
# the same time, which is not helpful in most cases where we wish to
# spread the work over time.
BackgroundMigrationWorker.perform_in(delay_interval * index, job_class_name, [start_id, end_id])
end
end
private private
# https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb#L678-L684 # https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb#L678-L684

View file

@ -9,14 +9,10 @@ RSpec.describe Account do
let(:bob) { Fabricate(:account, username: 'bob') } let(:bob) { Fabricate(:account, username: 'bob') }
describe '#suspend!' do describe '#suspend!' do
it 'marks the account as suspended' do it 'marks the account as suspended and creates a deletion request' do
subject.suspend! expect { subject.suspend! }
expect(subject.suspended?).to be true .to change(subject, :suspended?).from(false).to(true)
end .and(change { AccountDeletionRequest.exists?(account: subject) }.from(false).to(true))
it 'creates a deletion request' do
subject.suspend!
expect(AccountDeletionRequest.where(account: subject).exists?).to be true
end end
context 'when the account is of a local user' do context 'when the account is of a local user' do

View file

@ -3,16 +3,18 @@
require 'rails_helper' require 'rails_helper'
describe DomainAllow do describe DomainAllow do
describe 'scopes' do describe 'Validations' do
describe 'matches_domain' do it 'is invalid without a domain' do
let(:domain) { Fabricate(:domain_allow, domain: 'example.com') } domain_allow = Fabricate.build(:domain_allow, domain: nil)
let(:other_domain) { Fabricate(:domain_allow, domain: 'example.biz') } domain_allow.valid?
expect(domain_allow).to model_have_error_on_field(:domain)
end
it 'returns the correct records' do it 'is invalid if the same normalized domain already exists' do
results = described_class.matches_domain('example.com') _domain_allow = Fabricate(:domain_allow, domain: 'にゃん')
domain_allow_with_normalized_value = Fabricate.build(:domain_allow, domain: 'xn--r9j5b5b')
expect(results).to eq([domain]) domain_allow_with_normalized_value.valid?
end expect(domain_allow_with_normalized_value).to model_have_error_on_field(:domain)
end end
end end
end end

View file

@ -0,0 +1,59 @@
# frozen_string_literal: true
require 'rails_helper'
describe 'API Peers Search' do
describe 'GET /api/v1/peers/search' do
context 'when peers api is disabled' do
before do
Setting.peers_api_enabled = false
end
it 'returns http not found response' do
get '/api/v1/peers/search'
expect(response)
.to have_http_status(404)
end
end
context 'with no search param' do
it 'returns http success and empty response' do
get '/api/v1/peers/search'
expect(response)
.to have_http_status(200)
expect(body_as_json)
.to be_blank
end
end
context 'with invalid search param' do
it 'returns http success and empty response' do
get '/api/v1/peers/search', params: { q: 'ftp://Invalid-Host!!.valüe' }
expect(response)
.to have_http_status(200)
expect(body_as_json)
.to be_blank
end
end
context 'with search param' do
let!(:account) { Fabricate(:account, domain: 'host.example') }
before { Instance.refresh }
it 'returns http success and json with known domains' do
get '/api/v1/peers/search', params: { q: 'host.example' }
expect(response)
.to have_http_status(200)
expect(body_as_json.size)
.to eq(1)
expect(body_as_json.first)
.to eq(account.domain)
end
end
end
end

View file

@ -5,25 +5,25 @@ require 'rails_helper'
RSpec.describe PurgeDomainService, type: :service do RSpec.describe PurgeDomainService, type: :service do
subject { described_class.new } subject { described_class.new }
let!(:old_account) { Fabricate(:account, domain: 'obsolete.org') } let(:domain) { 'obsolete.org' }
let!(:old_status_plain) { Fabricate(:status, account: old_account) } let!(:account) { Fabricate(:account, domain: domain) }
let!(:old_status_with_attachment) { Fabricate(:status, account: old_account) } let!(:status_plain) { Fabricate(:status, account: account) }
let!(:old_attachment) { Fabricate(:media_attachment, account: old_account, status: old_status_with_attachment, file: attachment_fixture('attachment.jpg')) } let!(:status_with_attachment) { Fabricate(:status, account: account) }
let!(:attachment) { Fabricate(:media_attachment, account: account, status: status_with_attachment, file: attachment_fixture('attachment.jpg')) }
describe 'for a suspension' do describe 'for a suspension' do
before do it 'refreshes instance view and removes associated records' do
subject.call('obsolete.org') expect { subject.call(domain) }
.to change { domain_instance_exists }.from(true).to(false)
expect { account.reload }.to raise_exception ActiveRecord::RecordNotFound
expect { status_plain.reload }.to raise_exception ActiveRecord::RecordNotFound
expect { status_with_attachment.reload }.to raise_exception ActiveRecord::RecordNotFound
expect { attachment.reload }.to raise_exception ActiveRecord::RecordNotFound
end end
it 'removes the remote accounts\'s statuses and media attachments' do def domain_instance_exists
expect { old_account.reload }.to raise_exception ActiveRecord::RecordNotFound Instance.exists?(domain: domain)
expect { old_status_plain.reload }.to raise_exception ActiveRecord::RecordNotFound
expect { old_status_with_attachment.reload }.to raise_exception ActiveRecord::RecordNotFound
expect { old_attachment.reload }.to raise_exception ActiveRecord::RecordNotFound
end
it 'refreshes instances view' do
expect(Instance.where(domain: 'obsolete.org').exists?).to be false
end end
end end
end end

View file

@ -5,12 +5,13 @@ require 'rails_helper'
RSpec.describe UnallowDomainService, type: :service do RSpec.describe UnallowDomainService, type: :service do
subject { described_class.new } subject { described_class.new }
let!(:bad_account) { Fabricate(:account, username: 'badguy666', domain: 'evil.org') } let(:bad_domain) { 'evil.org' }
let!(:bad_account) { Fabricate(:account, username: 'badguy666', domain: bad_domain) }
let!(:bad_status_harassment) { Fabricate(:status, account: bad_account, text: 'You suck') } let!(:bad_status_harassment) { Fabricate(:status, account: bad_account, text: 'You suck') }
let!(:bad_status_mean) { Fabricate(:status, account: bad_account, text: 'Hahaha') } let!(:bad_status_mean) { Fabricate(:status, account: bad_account, text: 'Hahaha') }
let!(:bad_attachment) { Fabricate(:media_attachment, account: bad_account, status: bad_status_mean, file: attachment_fixture('attachment.jpg')) } let!(:bad_attachment) { Fabricate(:media_attachment, account: bad_account, status: bad_status_mean, file: attachment_fixture('attachment.jpg')) }
let!(:already_banned_account) { Fabricate(:account, username: 'badguy', domain: 'evil.org', suspended: true, silenced: true) } let!(:already_banned_account) { Fabricate(:account, username: 'badguy', domain: bad_domain, suspended: true, silenced: true) }
let!(:domain_allow) { Fabricate(:domain_allow, domain: 'evil.org') } let!(:domain_allow) { Fabricate(:domain_allow, domain: bad_domain) }
context 'with limited federation mode', :sidekiq_inline do context 'with limited federation mode', :sidekiq_inline do
before do before do
@ -18,23 +19,15 @@ RSpec.describe UnallowDomainService, type: :service do
end end
describe '#call' do describe '#call' do
before do it 'makes the domain not allowed and removes accounts from that domain' do
subject.call(domain_allow) expect { subject.call(domain_allow) }
end .to change { bad_domain_allowed }.from(true).to(false)
.and change { bad_domain_account_exists }.from(true).to(false)
it 'removes the allowed domain' do
expect(DomainAllow.allowed?('evil.org')).to be false
end
it 'removes remote accounts from that domain' do
expect { already_banned_account.reload }.to raise_error(ActiveRecord::RecordNotFound) expect { already_banned_account.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect(Account.where(domain: 'evil.org').exists?).to be false expect { bad_status_harassment.reload }.to raise_error(ActiveRecord::RecordNotFound)
end expect { bad_status_mean.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { bad_attachment.reload }.to raise_error(ActiveRecord::RecordNotFound)
it 'removes the remote accounts\'s statuses and media attachments' do
expect { bad_status_harassment.reload }.to raise_exception ActiveRecord::RecordNotFound
expect { bad_status_mean.reload }.to raise_exception ActiveRecord::RecordNotFound
expect { bad_attachment.reload }.to raise_exception ActiveRecord::RecordNotFound
end end
end end
end end
@ -45,23 +38,23 @@ RSpec.describe UnallowDomainService, type: :service do
end end
describe '#call' do describe '#call' do
before do it 'makes the domain not allowed but preserves accounts from the domain' do
subject.call(domain_allow) expect { subject.call(domain_allow) }
end .to change { bad_domain_allowed }.from(true).to(false)
.and not_change { bad_domain_account_exists }.from(true)
it 'removes the allowed domain' do
expect(DomainAllow.allowed?('evil.org')).to be false
end
it 'does not remove accounts from that domain' do
expect(Account.where(domain: 'evil.org').exists?).to be true
end
it 'removes the remote accounts\'s statuses and media attachments' do
expect { bad_status_harassment.reload }.to_not raise_error expect { bad_status_harassment.reload }.to_not raise_error
expect { bad_status_mean.reload }.to_not raise_error expect { bad_status_mean.reload }.to_not raise_error
expect { bad_attachment.reload }.to_not raise_error expect { bad_attachment.reload }.to_not raise_error
end end
end end
end end
def bad_domain_allowed
DomainAllow.allowed?(bad_domain)
end
def bad_domain_account_exists
Account.exists?(domain: bad_domain)
end
end end

View file

@ -4616,11 +4616,11 @@ __metadata:
linkType: hard linkType: hard
"async-mutex@npm:^0.4.0": "async-mutex@npm:^0.4.0":
version: 0.4.0 version: 0.4.1
resolution: "async-mutex@npm:0.4.0" resolution: "async-mutex@npm:0.4.1"
dependencies: dependencies:
tslib: "npm:^2.4.0" tslib: "npm:^2.4.0"
checksum: 6541695f80c1d6c5acbf3f7f04e8ff0733b3e029312c48d77bb95243fbe21fc5319f45ac3d72ce08551e6df83dc32440285ce9a3ac17bfc5d385ff0cc8ccd62a checksum: 3c412736c0bc4a9a2cfd948276a8caab8686aa615866a5bd20986e616f8945320acb310058a17afa1b31b8de6f634a78b7ec2217a33d7559b38f68bb85a95854
languageName: node languageName: node
linkType: hard linkType: hard
@ -4680,12 +4680,12 @@ __metadata:
linkType: hard linkType: hard
"autoprefixer@npm:^10.4.14": "autoprefixer@npm:^10.4.14":
version: 10.4.16 version: 10.4.17
resolution: "autoprefixer@npm:10.4.16" resolution: "autoprefixer@npm:10.4.17"
dependencies: dependencies:
browserslist: "npm:^4.21.10" browserslist: "npm:^4.22.2"
caniuse-lite: "npm:^1.0.30001538" caniuse-lite: "npm:^1.0.30001578"
fraction.js: "npm:^4.3.6" fraction.js: "npm:^4.3.7"
normalize-range: "npm:^0.1.2" normalize-range: "npm:^0.1.2"
picocolors: "npm:^1.0.0" picocolors: "npm:^1.0.0"
postcss-value-parser: "npm:^4.2.0" postcss-value-parser: "npm:^4.2.0"
@ -4693,7 +4693,7 @@ __metadata:
postcss: ^8.1.0 postcss: ^8.1.0
bin: bin:
autoprefixer: bin/autoprefixer autoprefixer: bin/autoprefixer
checksum: e00256e754d481a026d928bca729b25954074dd142dbec022f0a7db0d3bbc0dc2e2dc7542e94fec22eff81e21fe140e6856448e2d9a002660cb1e2ad434daee0 checksum: 1d21cc8edb7bf993682094ceed03a32c18f5293f071182a64c2c6defb44bbe91d576ad775d2347469a81997b80cea0bbc4ad3eeb5b12710f9feacf2e6c04bb51
languageName: node languageName: node
linkType: hard linkType: hard
@ -5240,7 +5240,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"browserslist@npm:^4.0.0, browserslist@npm:^4.21.10, browserslist@npm:^4.22.2": "browserslist@npm:^4.0.0, browserslist@npm:^4.22.2":
version: 4.22.2 version: 4.22.2
resolution: "browserslist@npm:4.22.2" resolution: "browserslist@npm:4.22.2"
dependencies: dependencies:
@ -5466,10 +5466,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001538, caniuse-lite@npm:^1.0.30001565": "caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001565, caniuse-lite@npm:^1.0.30001578":
version: 1.0.30001568 version: 1.0.30001578
resolution: "caniuse-lite@npm:1.0.30001568" resolution: "caniuse-lite@npm:1.0.30001578"
checksum: 13f01e5a2481134bd61cf565ce9fecbd8e107902927a0dcf534230a92191a81f1715792170f5f39719c767c3a96aa6df9917a8d5601f15bbd5e4041a8cfecc99 checksum: c3bd9c08a945cee4f0cc284a217ebe9c2613e04d5aef4b48f1871a779b1875c34286469eb8d7d94bd028b5a354613e676ad503b6bf8db20a2f154574bd5fde48
languageName: node languageName: node
linkType: hard linkType: hard
@ -8298,10 +8298,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"fraction.js@npm:^4.3.6": "fraction.js@npm:^4.3.7":
version: 4.3.6 version: 4.3.7
resolution: "fraction.js@npm:4.3.6" resolution: "fraction.js@npm:4.3.7"
checksum: d224bf62e350c4dbe66c6ac5ad9c4ec6d3c8e64c13323686dbebe7c8cc118491c297dca4961d3c93f847670794cb05e6d8b706f0e870846ab66a9c4491d0e914 checksum: df291391beea9ab4c263487ffd9d17fed162dbb736982dee1379b2a8cc94e4e24e46ed508c6d278aded9080ba51872f1bc5f3a5fd8d7c74e5f105b508ac28711
languageName: node languageName: node
linkType: hard linkType: hard