diff --git a/app/controllers/severed_relationships_controller.rb b/app/controllers/severed_relationships_controller.rb
new file mode 100644
index 0000000000..8994fff0ac
--- /dev/null
+++ b/app/controllers/severed_relationships_controller.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+class SeveredRelationshipsController < ApplicationController
+ layout 'admin'
+
+ before_action :authenticate_user!
+ before_action :set_body_classes
+ before_action :set_cache_headers
+
+ before_action :set_event, only: [:following, :followers]
+
+ def index
+ @events = AccountRelationshipSeveranceEvent.where(account: current_account)
+ end
+
+ def following
+ respond_to do |format|
+ format.csv { send_data following_data, filename: "following-#{@event.target_name}-#{@event.created_at.to_date.iso8601}.csv" }
+ end
+ end
+
+ def followers
+ respond_to do |format|
+ format.csv { send_data followers_data, filename: "followers-#{@event.target_name}-#{@event.created_at.to_date.iso8601}.csv" }
+ end
+ end
+
+ private
+
+ def set_event
+ @event = AccountRelationshipSeveranceEvent.find(params[:id])
+ end
+
+ def following_data
+ CSV.generate(headers: ['Account address', 'Show boosts', 'Notify on new posts', 'Languages'], write_headers: true) do |csv|
+ @event.severed_relationships.active.where(local_account: current_account).includes(:remote_account).reorder(id: :desc).each do |follow|
+ csv << [acct(follow.target_account), follow.show_reblogs, follow.notify, follow.languages&.join(', ')]
+ end
+ end
+ end
+
+ def followers_data
+ CSV.generate(headers: ['Account address'], write_headers: true) do |csv|
+ @event.severed_relationships.passive.where(local_account: current_account).includes(:remote_account).reorder(id: :desc).each do |follow|
+ csv << [acct(follow.account)]
+ end
+ end
+ end
+
+ def acct(account)
+ account.local? ? account.local_username_and_domain : account.acct
+ end
+
+ def set_body_classes
+ @body_classes = 'admin'
+ end
+
+ def set_cache_headers
+ response.cache_control.replace(private: true, no_store: true)
+ end
+end
diff --git a/app/javascript/mastodon/features/notifications/components/notification.jsx b/app/javascript/mastodon/features/notifications/components/notification.jsx
index d7101f8384..1af1eb78d9 100644
--- a/app/javascript/mastodon/features/notifications/components/notification.jsx
+++ b/app/javascript/mastodon/features/notifications/components/notification.jsx
@@ -14,6 +14,7 @@ import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import FlagIcon from '@/material-icons/400-24px/flag-fill.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
+import LinkOffIcon from '@/material-icons/400-24px/link_off.svg?react';
import PersonIcon from '@/material-icons/400-24px/person-fill.svg?react';
import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
@@ -26,6 +27,7 @@ import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import FollowRequestContainer from '../containers/follow_request_container';
+import RelationshipsSeveranceEvent from './relationships_severance_event';
import Report from './report';
const messages = defineMessages({
@@ -36,6 +38,7 @@ const messages = defineMessages({
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
status: { id: 'notification.status', defaultMessage: '{name} just posted' },
update: { id: 'notification.update', defaultMessage: '{name} edited a post' },
+ severedRelationships: { id: 'notification.severed_relationships', defaultMessage: 'Relationships with {name} severed' },
adminSignUp: { id: 'notification.admin.sign_up', defaultMessage: '{name} signed up' },
adminReport: { id: 'notification.admin.report', defaultMessage: '{name} reported {target}' },
});
@@ -358,6 +361,30 @@ class Notification extends ImmutablePureComponent {
);
}
+ renderRelationshipsSevered (notification) {
+ const { intl, unread } = this.props;
+
+ if (!notification.get('event')) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+ }
+
renderAdminSignUp (notification, account, link) {
const { intl, unread } = this.props;
@@ -429,6 +456,8 @@ class Notification extends ImmutablePureComponent {
return this.renderUpdate(notification, link);
case 'poll':
return this.renderPoll(notification, account);
+ case 'severed_relationships':
+ return this.renderRelationshipsSevered(notification);
case 'admin.sign_up':
return this.renderAdminSignUp(notification, account, link);
case 'admin.report':
diff --git a/app/javascript/mastodon/features/notifications/components/relationships_severance_event.jsx b/app/javascript/mastodon/features/notifications/components/relationships_severance_event.jsx
new file mode 100644
index 0000000000..12bc5f130d
--- /dev/null
+++ b/app/javascript/mastodon/features/notifications/components/relationships_severance_event.jsx
@@ -0,0 +1,61 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
+
+// This needs to be kept in sync with app/models/relationship_severance_event.rb
+const messages = defineMessages({
+ account_suspension: { id: 'relationship_severance_notification.types.account_suspension', defaultMessage: 'Account has been suspended' },
+ domain_block: { id: 'relationship_severance_notification.types.domain_block', defaultMessage: 'Domain has been suspended' },
+ user_domain_block: { id: 'relationship_severance_notification.types.user_domain_block', defaultMessage: 'You blocked this domain' },
+});
+
+const RelationshipsSeveranceEvent = ({ event, hidden }) => {
+ const intl = useIntl();
+
+ if (hidden || !event) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ {' · '}
+ { event.get('purged') ? (
+
+ ) : (
+
+ )}
+
+ {intl.formatMessage(messages[event.get('type')])}
+
+
+
+
+
+ );
+
+};
+
+RelationshipsSeveranceEvent.propTypes = {
+ event: ImmutablePropTypes.map.isRequired,
+ hidden: PropTypes.bool,
+};
+
+export default RelationshipsSeveranceEvent;
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 376dfb7e4b..ca69853366 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -471,6 +471,8 @@
"notification.own_poll": "Your poll has ended",
"notification.poll": "A poll you have voted in has ended",
"notification.reblog": "{name} boosted your post",
+ "notification.severedRelationships": "Relationships with {name} severed",
+ "notification.severed_relationships": "Relationships with {name} severed",
"notification.status": "{name} just posted",
"notification.update": "{name} edited a post",
"notification_requests.accept": "Accept",
@@ -587,6 +589,12 @@
"refresh": "Refresh",
"regeneration_indicator.label": "Loading…",
"regeneration_indicator.sublabel": "Your home feed is being prepared!",
+ "relationship_severance_notification.purged_data": "purged by administrators",
+ "relationship_severance_notification.relationships": "{count, plural, one {# relationship} other {# relationships}}",
+ "relationship_severance_notification.types.account_suspension": "Account has been suspended",
+ "relationship_severance_notification.types.domain_block": "Domain has been suspended",
+ "relationship_severance_notification.types.user_domain_block": "You blocked this domain",
+ "relationship_severance_notification.view": "View",
"relative_time.days": "{number}d",
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js
index b1c80b3d4f..bc85936439 100644
--- a/app/javascript/mastodon/reducers/notifications.js
+++ b/app/javascript/mastodon/reducers/notifications.js
@@ -55,6 +55,7 @@ export const notificationToMap = notification => ImmutableMap({
created_at: notification.created_at,
status: notification.status ? notification.status.id : null,
report: notification.report ? fromJS(notification.report) : null,
+ event: notification.event ? fromJS(notification.event) : null,
});
const normalizeNotification = (state, notification, usePendingItems) => {
diff --git a/app/javascript/material-icons/400-24px/link_off-fill.svg b/app/javascript/material-icons/400-24px/link_off-fill.svg
new file mode 100644
index 0000000000..618e775347
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/link_off-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/link_off.svg b/app/javascript/material-icons/400-24px/link_off.svg
new file mode 100644
index 0000000000..618e775347
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/link_off.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/models/account_relationship_severance_event.rb b/app/models/account_relationship_severance_event.rb
new file mode 100644
index 0000000000..32b185e3af
--- /dev/null
+++ b/app/models/account_relationship_severance_event.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+#
+# == Schema Information
+#
+# Table name: account_relationship_severance_events
+#
+# id :bigint(8) not null, primary key
+# account_id :bigint(8) not null
+# relationship_severance_event_id :bigint(8) not null
+# relationships_count :integer default(0), not null
+# created_at :datetime not null
+# updated_at :datetime not null
+#
+class AccountRelationshipSeveranceEvent < ApplicationRecord
+ belongs_to :account
+ belongs_to :relationship_severance_event
+
+ delegate :severed_relationships, :type, :target_name, :purged, to: :relationship_severance_event, prefix: false
+
+ before_create :set_relationships_count!
+
+ private
+
+ def set_relationships_count!
+ self.relationships_count = severed_relationships.where(local_account: account).count
+ end
+end
diff --git a/app/models/concerns/account/interactions.rb b/app/models/concerns/account/interactions.rb
index 85363febfb..a32697b66e 100644
--- a/app/models/concerns/account/interactions.rb
+++ b/app/models/concerns/account/interactions.rb
@@ -83,6 +83,11 @@ module Account::Interactions
has_many :following, -> { order('follows.id desc') }, through: :active_relationships, source: :target_account
has_many :followers, -> { order('follows.id desc') }, through: :passive_relationships, source: :account
+ with_options class_name: 'SeveredRelationship', dependent: :destroy do
+ has_many :severed_relationships, foreign_key: 'local_account_id', inverse_of: :local_account
+ has_many :remote_severed_relationships, foreign_key: 'remote_account_id', inverse_of: :remote_account
+ end
+
# Account notes
has_many :account_notes, dependent: :destroy
diff --git a/app/models/concerns/account/merging.rb b/app/models/concerns/account/merging.rb
index 960ee1819f..ebc57a1221 100644
--- a/app/models/concerns/account/merging.rb
+++ b/app/models/concerns/account/merging.rb
@@ -48,6 +48,18 @@ module Account::Merging
record.update_attribute(:account_warning_id, id)
end
+ SeveredRelationship.where(local_account_id: other_account.id).reorder(nil).find_each do |record|
+ record.update_attribute(:local_account_id, id)
+ rescue ActiveRecord::RecordNotUnique
+ next
+ end
+
+ SeveredRelationship.where(remote_account_id: other_account.id).reorder(nil).find_each do |record|
+ record.update_attribute(:remote_account_id, id)
+ rescue ActiveRecord::RecordNotUnique
+ next
+ end
+
# Some follow relationships have moved, so the cache is stale
Rails.cache.delete_matched("followers_hash:#{id}:*")
Rails.cache.delete_matched("relationships:#{id}:*")
diff --git a/app/models/notification.rb b/app/models/notification.rb
index 861a154369..8ee7e77258 100644
--- a/app/models/notification.rb
+++ b/app/models/notification.rb
@@ -54,6 +54,9 @@ class Notification < ApplicationRecord
update: {
filterable: false,
}.freeze,
+ severed_relationships: {
+ filterable: false,
+ }.freeze,
'admin.sign_up': {
filterable: false,
}.freeze,
@@ -86,6 +89,7 @@ class Notification < ApplicationRecord
belongs_to :favourite, inverse_of: :notification
belongs_to :poll, inverse_of: false
belongs_to :report, inverse_of: false
+ belongs_to :relationship_severance_event, inverse_of: false
end
validates :type, inclusion: { in: TYPES }
@@ -182,6 +186,11 @@ class Notification < ApplicationRecord
self.from_account_id = activity&.status&.account_id
when 'Account'
self.from_account_id = activity&.id
+ when 'AccountRelationshipSeveranceEvent'
+ # These do not really have an originating account, but this is mandatory
+ # in the data model, and the recipient's account will by definition
+ # always exist
+ self.from_account_id = account_id
end
end
diff --git a/app/models/relationship_severance_event.rb b/app/models/relationship_severance_event.rb
new file mode 100644
index 0000000000..d9775150e8
--- /dev/null
+++ b/app/models/relationship_severance_event.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: relationship_severance_events
+#
+# id :bigint(8) not null, primary key
+# type :integer not null
+# target_name :string not null
+# purged :boolean default(FALSE), not null
+# created_at :datetime not null
+# updated_at :datetime not null
+#
+class RelationshipSeveranceEvent < ApplicationRecord
+ self.inheritance_column = nil
+
+ has_many :severed_relationships, inverse_of: :relationship_severance_event, dependent: :delete_all
+
+ enum type: {
+ domain_block: 0,
+ user_domain_block: 1,
+ account_suspension: 2,
+ }
+
+ scope :about_local_account, ->(account) { where(id: SeveredRelationship.about_local_account(account).select(:relationship_severance_event_id)) }
+
+ def import_from_active_follows!(follows)
+ import_from_follows!(follows, true)
+ end
+
+ def import_from_passive_follows!(follows)
+ import_from_follows!(follows, false)
+ end
+
+ def affected_local_accounts
+ Account.where(id: severed_relationships.select(:local_account_id))
+ end
+
+ private
+
+ def import_from_follows!(follows, active)
+ SeveredRelationship.insert_all(
+ follows.pluck(:account_id, :target_account_id, :show_reblogs, :notify, :languages).map do |account_id, target_account_id, show_reblogs, notify, languages|
+ {
+ local_account_id: active ? account_id : target_account_id,
+ remote_account_id: active ? target_account_id : account_id,
+ show_reblogs: show_reblogs,
+ notify: notify,
+ languages: languages,
+ relationship_severance_event_id: id,
+ direction: active ? :active : :passive,
+ }
+ end
+ )
+ end
+end
diff --git a/app/models/severed_relationship.rb b/app/models/severed_relationship.rb
new file mode 100644
index 0000000000..00a913f7fc
--- /dev/null
+++ b/app/models/severed_relationship.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: severed_relationships
+#
+# id :bigint(8) not null, primary key
+# relationship_severance_event_id :bigint(8) not null
+# local_account_id :bigint(8) not null
+# remote_account_id :bigint(8) not null
+# direction :integer not null
+# show_reblogs :boolean
+# notify :boolean
+# languages :string is an Array
+# created_at :datetime not null
+# updated_at :datetime not null
+#
+class SeveredRelationship < ApplicationRecord
+ belongs_to :relationship_severance_event
+ belongs_to :local_account, class_name: 'Account'
+ belongs_to :remote_account, class_name: 'Account'
+
+ enum direction: {
+ passive: 0, # analogous to `local_account.passive_relationships`
+ active: 1, # analogous to `local_account.active_relationships`
+ }
+
+ scope :about_local_account, ->(account) { where(local_account: account) }
+
+ scope :active, -> { where(direction: :active) }
+ scope :passive, -> { where(direction: :passive) }
+
+ def account
+ active? ? local_account : remote_account
+ end
+
+ def target_account
+ active? ? remote_account : local_account
+ end
+end
diff --git a/app/serializers/rest/account_relationship_severance_event_serializer.rb b/app/serializers/rest/account_relationship_severance_event_serializer.rb
new file mode 100644
index 0000000000..2578e3a20f
--- /dev/null
+++ b/app/serializers/rest/account_relationship_severance_event_serializer.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class REST::AccountRelationshipSeveranceEventSerializer < ActiveModel::Serializer
+ attributes :id, :type, :purged, :target_name, :created_at
+
+ def id
+ object.id.to_s
+ end
+end
diff --git a/app/serializers/rest/notification_serializer.rb b/app/serializers/rest/notification_serializer.rb
index 137fc53dda..d7b88b7c37 100644
--- a/app/serializers/rest/notification_serializer.rb
+++ b/app/serializers/rest/notification_serializer.rb
@@ -6,6 +6,7 @@ class REST::NotificationSerializer < ActiveModel::Serializer
belongs_to :from_account, key: :account, serializer: REST::AccountSerializer
belongs_to :target_status, key: :status, if: :status_type?, serializer: REST::StatusSerializer
belongs_to :report, if: :report_type?, serializer: REST::ReportSerializer
+ belongs_to :relationship_severance_event, key: :event, if: :relationship_severance_event?, serializer: REST::AccountRelationshipSeveranceEventSerializer
def id
object.id.to_s
@@ -18,4 +19,8 @@ class REST::NotificationSerializer < ActiveModel::Serializer
def report_type?
object.type == :'admin.report'
end
+
+ def relationship_severance_event?
+ object.type == :severed_relationships
+ end
end
diff --git a/app/services/after_block_domain_from_account_service.rb b/app/services/after_block_domain_from_account_service.rb
index 89d007c1cd..adb17845cc 100644
--- a/app/services/after_block_domain_from_account_service.rb
+++ b/app/services/after_block_domain_from_account_service.rb
@@ -9,18 +9,21 @@ class AfterBlockDomainFromAccountService < BaseService
def call(account, domain)
@account = account
@domain = domain
+ @domain_block_event = nil
clear_notifications!
remove_follows!
reject_existing_followers!
reject_pending_follow_requests!
+ notify_of_severed_relationships!
end
private
def remove_follows!
- @account.active_relationships.where(target_account: Account.where(domain: @domain)).includes(:target_account).reorder(nil).find_each do |follow|
- UnfollowService.new.call(@account, follow.target_account)
+ @account.active_relationships.where(target_account: Account.where(domain: @domain)).includes(:target_account).reorder(nil).in_batches do |follows|
+ domain_block_event.import_from_active_follows!(follows)
+ follows.each { |follow| UnfollowService.new.call(@account, follow.target_account) }
end
end
@@ -29,8 +32,9 @@ class AfterBlockDomainFromAccountService < BaseService
end
def reject_existing_followers!
- @account.passive_relationships.where(account: Account.where(domain: @domain)).includes(:account).reorder(nil).find_each do |follow|
- reject_follow!(follow)
+ @account.passive_relationships.where(account: Account.where(domain: @domain)).includes(:account).reorder(nil).in_batches do |follows|
+ domain_block_event.import_from_passive_follows!(follows)
+ follows.each { |follow| reject_follow!(follow) }
end
end
@@ -47,4 +51,15 @@ class AfterBlockDomainFromAccountService < BaseService
ActivityPub::DeliveryWorker.perform_async(Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), @account.id, follow.account.inbox_url)
end
+
+ def notify_of_severed_relationships!
+ return if @domain_block_event.nil?
+
+ event = AccountRelationshipSeveranceEvent.create!(account: @account, relationship_severance_event: @domain_block_event)
+ LocalNotificationWorker.perform_async(@account.id, event.id, 'AccountRelationshipSeveranceEvent', 'severed_relationships')
+ end
+
+ def domain_block_event
+ @domain_block_event ||= RelationshipSeveranceEvent.create!(type: :user_domain_block, target_name: @domain)
+ end
end
diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb
index 76cc36ff6b..00d020d2b3 100644
--- a/app/services/block_domain_service.rb
+++ b/app/services/block_domain_service.rb
@@ -5,8 +5,11 @@ class BlockDomainService < BaseService
def call(domain_block, update = false)
@domain_block = domain_block
+ @domain_block_event = nil
+
process_domain_block!
process_retroactive_updates! if update
+ notify_of_severed_relationships!
end
private
@@ -37,7 +40,17 @@ class BlockDomainService < BaseService
blocked_domain_accounts.without_suspended.in_batches.update_all(suspended_at: @domain_block.created_at, suspension_origin: :local)
blocked_domain_accounts.where(suspended_at: @domain_block.created_at).reorder(nil).find_each do |account|
- DeleteAccountService.new.call(account, reserve_username: true, suspended_at: @domain_block.created_at)
+ DeleteAccountService.new.call(account, reserve_username: true, suspended_at: @domain_block.created_at, relationship_severance_event: domain_block_event)
+ end
+ end
+
+ def notify_of_severed_relationships!
+ return if @domain_block_event.nil?
+
+ # TODO: check how efficient that query is, also check `push_bulk`/`perform_bulk`
+ @domain_block_event.affected_local_accounts.reorder(nil).find_each do |account|
+ event = AccountRelationshipSeveranceEvent.create!(account: account, relationship_severance_event: @domain_block_event)
+ LocalNotificationWorker.perform_async(account.id, event.id, 'AccountRelationshipSeveranceEvent', 'severed_relationships')
end
end
@@ -45,6 +58,10 @@ class BlockDomainService < BaseService
domain_block.domain
end
+ def domain_block_event
+ @domain_block_event ||= RelationshipSeveranceEvent.create!(type: :domain_block, target_name: blocked_domain)
+ end
+
def blocked_domain_accounts
Account.by_domain_and_subdomains(blocked_domain)
end
diff --git a/app/services/delete_account_service.rb b/app/services/delete_account_service.rb
index 7c7cb97df2..bdfe0e7167 100644
--- a/app/services/delete_account_service.rb
+++ b/app/services/delete_account_service.rb
@@ -58,6 +58,8 @@ class DeleteAccountService < BaseService
reports
targeted_moderation_notes
targeted_reports
+ severed_relationships
+ remote_severed_relationships
).freeze
# Suspend or remove an account and remove as much of its data
@@ -72,6 +74,7 @@ class DeleteAccountService < BaseService
# @option [Boolean] :skip_side_effects Side effects are ActivityPub and streaming API payloads
# @option [Boolean] :skip_activitypub Skip sending ActivityPub payloads. Implied by :skip_side_effects
# @option [Time] :suspended_at Only applicable when :reserve_username is true
+ # @option [RelationshipSeveranceEvent] :relationship_severance_event Event used to record severed relationships not initiated by the user
def call(account, **options)
@account = account
@options = { reserve_username: true, reserve_email: true }.merge(options)
@@ -84,6 +87,7 @@ class DeleteAccountService < BaseService
@options[:skip_activitypub] = true if @options[:skip_side_effects]
+ record_severed_relationships!
distribute_activities!
purge_content!
fulfill_deletion_request!
@@ -266,6 +270,20 @@ class DeleteAccountService < BaseService
end
end
+ def record_severed_relationships!
+ return if relationship_severance_event.nil?
+
+ @account.active_relationships.in_batches do |follows|
+ # NOTE: these follows are passive with regards to the local accounts
+ relationship_severance_event.import_from_passive_follows!(follows)
+ end
+
+ @account.passive_relationships.in_batches do |follows|
+ # NOTE: these follows are active with regards to the local accounts
+ relationship_severance_event.import_from_active_follows!(follows)
+ end
+ end
+
def delete_actor_json
@delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account, always_sign: true))
end
@@ -305,4 +323,8 @@ class DeleteAccountService < BaseService
def skip_activitypub?
@options[:skip_activitypub]
end
+
+ def relationship_severance_event
+ @options[:relationship_severance_event]
+ end
end
diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb
index f3d16f1be7..c83e4c017f 100644
--- a/app/services/notify_service.rb
+++ b/app/services/notify_service.rb
@@ -9,6 +9,8 @@ class NotifyService < BaseService
update
poll
status
+ # TODO: this probably warrants an email notification
+ severed_relationships
).freeze
class DismissCondition
@@ -20,7 +22,7 @@ class NotifyService < BaseService
def dismiss?
blocked = @recipient.unavailable?
- blocked ||= from_self? && @notification.type != :poll
+ blocked ||= from_self? && @notification.type != :poll && @notification.type != :severed_relationships
return blocked if message? && from_staff?
diff --git a/app/services/purge_domain_service.rb b/app/services/purge_domain_service.rb
index 9df81f13e6..ca0f0d441f 100644
--- a/app/services/purge_domain_service.rb
+++ b/app/services/purge_domain_service.rb
@@ -2,10 +2,26 @@
class PurgeDomainService < BaseService
def call(domain)
- Account.remote.where(domain: domain).reorder(nil).find_each do |account|
- DeleteAccountService.new.call(account, reserve_username: false, skip_side_effects: true)
- end
- CustomEmoji.remote.where(domain: domain).reorder(nil).find_each(&:destroy)
+ @domain = domain
+
+ purge_relationship_severance_events!
+ purge_accounts!
+ purge_emojis!
+
Instance.refresh
end
+
+ def purge_relationship_severance_events!
+ RelationshipSeveranceEvent.where(type: [:domain_block, :user_domain_block], target_name: @domain).in_batches.update_all(purged: true)
+ end
+
+ def purge_accounts!
+ Account.remote.where(domain: @domain).reorder(nil).find_each do |account|
+ DeleteAccountService.new.call(account, reserve_username: false, skip_side_effects: true)
+ end
+ end
+
+ def purge_emojis!
+ CustomEmoji.remote.where(domain: @domain).reorder(nil).find_each(&:destroy)
+ end
end
diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb
index 8d5446f1a8..86c4ff6416 100644
--- a/app/services/suspend_account_service.rb
+++ b/app/services/suspend_account_service.rb
@@ -8,6 +8,7 @@ class SuspendAccountService < BaseService
def call(account)
return unless account.suspended?
+ @relationship_severance_event = nil
@account = account
reject_remote_follows!
@@ -15,6 +16,7 @@ class SuspendAccountService < BaseService
unmerge_from_home_timelines!
unmerge_from_list_timelines!
privatize_media_attachments!
+ notify_of_severed_relationships!
end
private
@@ -36,6 +38,8 @@ class SuspendAccountService < BaseService
[Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), follow.target_account_id, @account.inbox_url]
end
+ relationship_severance_event.import_from_passive_follows!(follows)
+
follows.each(&:destroy)
end
end
@@ -101,7 +105,21 @@ class SuspendAccountService < BaseService
end
end
+ def notify_of_severed_relationships!
+ return if @relationship_severance_event.nil?
+
+ # TODO: check how efficient that query is, also check `push_bulk`/`perform_bulk`
+ @relationship_severance_event.affected_local_accounts.reorder(nil).find_each do |account|
+ event = AccountRelationshipSeveranceEvent.create!(account: account, relationship_severance_event: @relationship_severance_event)
+ LocalNotificationWorker.perform_async(account.id, event.id, 'AccountRelationshipSeveranceEvent', 'severed_relationships')
+ end
+ end
+
def signed_activity_json
@signed_activity_json ||= Oj.dump(serialize_payload(@account, ActivityPub::UpdateSerializer, signer: @account))
end
+
+ def relationship_severance_event
+ @relationship_severance_event ||= RelationshipSeveranceEvent.create!(type: :account_suspension, target_name: @account.acct)
+ end
end
diff --git a/app/views/severed_relationships/index.html.haml b/app/views/severed_relationships/index.html.haml
new file mode 100644
index 0000000000..97bef87929
--- /dev/null
+++ b/app/views/severed_relationships/index.html.haml
@@ -0,0 +1,34 @@
+- content_for :page_title do
+ = t('settings.severed_relationships')
+
+%p.muted-hint= t('severed_relationships.preamble')
+
+- unless @events.empty?
+ .table-wrapper
+ %table.table
+ %thead
+ %tr
+ %th= t('exports.archive_takeout.date')
+ %th= t('severed_relationships.type')
+ %th= t('severed_relationships.lost_follows')
+ %th= t('severed_relationships.lost_followers')
+ %tbody
+ - @events.each do |event|
+ %tr
+ %td= l event.created_at
+ %td= t("severed_relationships.event_type.#{event.type}", target_name: event.target_name)
+ - if event.purged?
+ %td{ rowspan: 2 }= t('severed_relationships.purged')
+ - else
+ %td
+ - count = event.severed_relationships.active.where(local_account: current_account).count
+ - if count.zero?
+ = t('generic.none')
+ - else
+ = table_link_to 'download', t('severed_relationships.download', count: count), following_severed_relationship_path(event, format: :csv)
+ %td
+ - count = event.severed_relationships.passive.where(local_account: current_account).count
+ - if count.zero?
+ = t('generic.none')
+ - else
+ = table_link_to 'download', t('severed_relationships.download', count: count), followers_severed_relationship_path(event, format: :csv)
diff --git a/config/locales/en.yml b/config/locales/en.yml
index f45175e3fd..823e720ea7 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1660,10 +1660,22 @@ en:
preferences: Preferences
profile: Public profile
relationships: Follows and followers
+ severed_relationships: Severed relationships
statuses_cleanup: Automated post deletion
strikes: Moderation strikes
two_factor_authentication: Two-factor Auth
webauthn_authentication: Security keys
+ severed_relationships:
+ download: Download (%{count})
+ event_type:
+ account_suspension: Account suspension (%{target_name})
+ domain_block: Server suspension (%{target_name})
+ user_domain_block: You blocked %{target_name}
+ lost_followers: Lost followers
+ lost_follows: Lost follows
+ preamble: You may lose follows and followers when you block a domain or when your moderators decide to suspend a remote server. When that happens, you will be able to download lists of severed relationships, to be inspected and possibly imported on another server.
+ purged: Information about this server has been purged by your server's administrators.
+ type: Event
statuses:
attached:
audio:
diff --git a/config/navigation.rb b/config/navigation.rb
index 30593b3ab9..de5f28ce96 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -22,7 +22,11 @@ SimpleNavigation::Configuration.run do |navigation|
end
end
- n.item :relationships, safe_join([fa_icon('users fw'), t('settings.relationships')]), relationships_path, if: -> { current_user.functional? && !self_destruct }
+ n.item :relationships, safe_join([fa_icon('users fw'), t('settings.relationships')]), relationships_path, if: -> { current_user.functional? && !self_destruct } do |s|
+ s.item :current, safe_join([fa_icon('users fw'), t('settings.relationships')]), relationships_path
+ s.item :severed_relationships, safe_join([fa_icon('unlink fw'), t('settings.severed_relationships')]), severed_relationships_path
+ end
+
n.item :filters, safe_join([fa_icon('filter fw'), t('filters.index.title')]), filters_path, highlights_on: %r{/filters}, if: -> { current_user.functional? && !self_destruct }
n.item :statuses_cleanup, safe_join([fa_icon('history fw'), t('settings.statuses_cleanup')]), statuses_cleanup_path, if: -> { current_user.functional_or_moved? && !self_destruct }
diff --git a/config/routes.rb b/config/routes.rb
index 35580e4182..e198a527d1 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -190,6 +190,14 @@ Rails.application.routes.draw do
end
resource :relationships, only: [:show, :update]
+ resources :severed_relationships, only: [:index] do
+ member do
+ constraints(format: :csv) do
+ get :followers
+ get :following
+ end
+ end
+ end
resource :statuses_cleanup, controller: :statuses_cleanup, only: [:show, :update]
get '/media_proxy/:id/(*any)', to: 'media_proxy#show', as: :media_proxy, format: false
diff --git a/db/migrate/20240312100644_create_relationship_severance_events.rb b/db/migrate/20240312100644_create_relationship_severance_events.rb
new file mode 100644
index 0000000000..8c55fe330f
--- /dev/null
+++ b/db/migrate/20240312100644_create_relationship_severance_events.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class CreateRelationshipSeveranceEvents < ActiveRecord::Migration[7.0]
+ def change
+ create_table :relationship_severance_events do |t|
+ t.integer :type, null: false
+ t.string :target_name, null: false
+ t.boolean :purged, null: false, default: false
+
+ t.timestamps
+
+ t.index [:type, :target_name]
+ end
+ end
+end
diff --git a/db/migrate/20240312105620_create_severed_relationships.rb b/db/migrate/20240312105620_create_severed_relationships.rb
new file mode 100644
index 0000000000..1ed911cd55
--- /dev/null
+++ b/db/migrate/20240312105620_create_severed_relationships.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class CreateSeveredRelationships < ActiveRecord::Migration[7.0]
+ def change
+ create_table :severed_relationships do |t|
+ # No need to have an index on this foreign key as it is covered by `index_severed_relationships_on_unique_tuples`
+ t.references :relationship_severance_event, null: false, foreign_key: { on_delete: :cascade }, index: false
+
+ # No need to have an index on this foregin key as it is covered by `index_severed_relationships_on_local_account_and_event`
+ t.references :local_account, null: false, foreign_key: { to_table: :accounts, on_delete: :cascade }, index: false
+ t.references :remote_account, null: false, foreign_key: { to_table: :accounts, on_delete: :cascade }
+
+ # Used to describe whether `local_account` is the active (follower) or passive (followed) part of the relationship
+ t.integer :direction, null: false
+
+ # Those attributes are carried over from the `follows` table
+ t.boolean :show_reblogs
+ t.boolean :notify
+ t.string :languages, array: true
+
+ t.timestamps
+
+ t.index [:relationship_severance_event_id, :local_account_id, :direction, :remote_account_id], name: 'index_severed_relationships_on_unique_tuples', unique: true
+ t.index [:local_account_id, :relationship_severance_event_id], name: 'index_severed_relationships_on_local_account_and_event'
+ end
+ end
+end
diff --git a/db/migrate/20240320140159_create_account_relationship_severance_events.rb b/db/migrate/20240320140159_create_account_relationship_severance_events.rb
new file mode 100644
index 0000000000..5262c508fe
--- /dev/null
+++ b/db/migrate/20240320140159_create_account_relationship_severance_events.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class CreateAccountRelationshipSeveranceEvents < ActiveRecord::Migration[7.1]
+ def change
+ create_table :account_relationship_severance_events do |t|
+ t.belongs_to :account, foreign_key: { on_delete: :cascade }, null: false
+ t.belongs_to :relationship_severance_event, foreign_key: { on_delete: :cascade }, null: false
+
+ t.integer :relationships_count, default: 0, null: false
+
+ t.index [:account_id, :relationship_severance_event_id], unique: true
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 795a81c5ce..1fdf5d4823 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -90,6 +90,17 @@ ActiveRecord::Schema[7.1].define(version: 2024_03_20_163441) do
t.index ["target_account_id"], name: "index_account_pins_on_target_account_id"
end
+ create_table "account_relationship_severance_events", force: :cascade do |t|
+ t.bigint "account_id", null: false
+ t.bigint "relationship_severance_event_id", null: false
+ t.integer "relationships_count", default: 0, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["account_id", "relationship_severance_event_id"], name: "idx_on_account_id_relationship_severance_event_id_7bd82bf20e", unique: true
+ t.index ["account_id"], name: "index_account_relationship_severance_events_on_account_id"
+ t.index ["relationship_severance_event_id"], name: "idx_on_relationship_severance_event_id_403f53e707"
+ end
+
create_table "account_stats", force: :cascade do |t|
t.bigint "account_id", null: false
t.bigint "statuses_count", default: 0, null: false
@@ -871,6 +882,15 @@ ActiveRecord::Schema[7.1].define(version: 2024_03_20_163441) do
t.string "url"
end
+ create_table "relationship_severance_events", force: :cascade do |t|
+ t.integer "type", null: false
+ t.string "target_name", null: false
+ t.boolean "purged", default: false, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["type", "target_name"], name: "index_relationship_severance_events_on_type_and_target_name"
+ end
+
create_table "relays", force: :cascade do |t|
t.string "inbox_url", default: "", null: false
t.string "follow_activity_id"
@@ -950,6 +970,21 @@ ActiveRecord::Schema[7.1].define(version: 2024_03_20_163441) do
t.index ["thing_type", "thing_id", "var"], name: "index_settings_on_thing_type_and_thing_id_and_var", unique: true
end
+ create_table "severed_relationships", force: :cascade do |t|
+ t.bigint "relationship_severance_event_id", null: false
+ t.bigint "local_account_id", null: false
+ t.bigint "remote_account_id", null: false
+ t.integer "direction", null: false
+ t.boolean "show_reblogs"
+ t.boolean "notify"
+ t.string "languages", array: true
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["local_account_id", "relationship_severance_event_id"], name: "index_severed_relationships_on_local_account_and_event"
+ t.index ["relationship_severance_event_id", "local_account_id", "direction", "remote_account_id"], name: "index_severed_relationships_on_unique_tuples", unique: true
+ t.index ["remote_account_id"], name: "index_severed_relationships_on_remote_account_id"
+ end
+
create_table "site_uploads", force: :cascade do |t|
t.string "var", default: "", null: false
t.string "file_file_name"
@@ -1231,6 +1266,8 @@ ActiveRecord::Schema[7.1].define(version: 2024_03_20_163441) do
add_foreign_key "account_notes", "accounts", on_delete: :cascade
add_foreign_key "account_pins", "accounts", column: "target_account_id", on_delete: :cascade
add_foreign_key "account_pins", "accounts", on_delete: :cascade
+ add_foreign_key "account_relationship_severance_events", "accounts", on_delete: :cascade
+ add_foreign_key "account_relationship_severance_events", "relationship_severance_events", on_delete: :cascade
add_foreign_key "account_stats", "accounts", on_delete: :cascade
add_foreign_key "account_statuses_cleanup_policies", "accounts", on_delete: :cascade
add_foreign_key "account_warnings", "accounts", column: "target_account_id", on_delete: :cascade
@@ -1323,6 +1360,9 @@ ActiveRecord::Schema[7.1].define(version: 2024_03_20_163441) do
add_foreign_key "scheduled_statuses", "accounts", on_delete: :cascade
add_foreign_key "session_activations", "oauth_access_tokens", column: "access_token_id", name: "fk_957e5bda89", on_delete: :cascade
add_foreign_key "session_activations", "users", name: "fk_e5fda67334", on_delete: :cascade
+ add_foreign_key "severed_relationships", "accounts", column: "local_account_id", on_delete: :cascade
+ add_foreign_key "severed_relationships", "accounts", column: "remote_account_id", on_delete: :cascade
+ add_foreign_key "severed_relationships", "relationship_severance_events", on_delete: :cascade
add_foreign_key "status_edits", "accounts", on_delete: :nullify
add_foreign_key "status_edits", "statuses", on_delete: :cascade
add_foreign_key "status_pins", "accounts", name: "fk_d4cb435b62", on_delete: :cascade
diff --git a/lib/mastodon/cli/maintenance.rb b/lib/mastodon/cli/maintenance.rb
index 9f234e3860..e6a346ae26 100644
--- a/lib/mastodon/cli/maintenance.rb
+++ b/lib/mastodon/cli/maintenance.rb
@@ -5,7 +5,7 @@ require_relative 'base'
module Mastodon::CLI
class Maintenance < Base
MIN_SUPPORTED_VERSION = 2019_10_01_213028
- MAX_SUPPORTED_VERSION = 2023_09_07_150100
+ MAX_SUPPORTED_VERSION = 2023_10_23_105620
# Stubs to enjoy ActiveRecord queries while not depending on a particular
# version of the code/database
@@ -39,6 +39,7 @@ module Mastodon::CLI
class Webhook < ApplicationRecord; end
class BulkImport < ApplicationRecord; end
class SoftwareUpdate < ApplicationRecord; end
+ class SeveredRelationship < ApplicationRecord; end
class DomainBlock < ApplicationRecord
enum severity: { silence: 0, suspend: 1, noop: 2 }
@@ -129,6 +130,20 @@ module Mastodon::CLI
record.update_attribute(:account_warning_id, id)
end
end
+
+ if ActiveRecord::Base.connection.table_exists?(:severed_relationships)
+ SeveredRelationship.where(local_account_id: other_account.id).reorder(nil).find_each do |record|
+ record.update_attribute(:local_account_id, id)
+ rescue ActiveRecord::RecordNotUnique
+ next
+ end
+
+ SeveredRelationship.where(remote_account_id: other_account.id).reorder(nil).find_each do |record|
+ record.update_attribute(:remote_account_id, id)
+ rescue ActiveRecord::RecordNotUnique
+ next
+ end
+ end
end
end
diff --git a/spec/fabricators/account_relationship_severance_event_fabricator.rb b/spec/fabricators/account_relationship_severance_event_fabricator.rb
new file mode 100644
index 0000000000..5580d52092
--- /dev/null
+++ b/spec/fabricators/account_relationship_severance_event_fabricator.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+Fabricator(:account_relationship_severance_event) do
+ account
+ relationship_severance_event
+end
diff --git a/spec/fabricators/relationship_severance_event_fabricator.rb b/spec/fabricators/relationship_severance_event_fabricator.rb
new file mode 100644
index 0000000000..7fec14e9f2
--- /dev/null
+++ b/spec/fabricators/relationship_severance_event_fabricator.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+Fabricator(:relationship_severance_event) do
+ type { :domain_block }
+ target_name { 'example.com' }
+end
diff --git a/spec/fabricators/severed_relationship_fabricator.rb b/spec/fabricators/severed_relationship_fabricator.rb
new file mode 100644
index 0000000000..6600b72cdf
--- /dev/null
+++ b/spec/fabricators/severed_relationship_fabricator.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+Fabricator(:severed_relationship) do
+ local_account { Fabricate.build(:account) }
+ remote_account { Fabricate.build(:account) }
+ relationship_severance_event { Fabricate.build(:relationship_severance_event) }
+ direction { :active }
+end
diff --git a/spec/models/relationship_severance_event_spec.rb b/spec/models/relationship_severance_event_spec.rb
new file mode 100644
index 0000000000..93c0f1a26d
--- /dev/null
+++ b/spec/models/relationship_severance_event_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe RelationshipSeveranceEvent do
+ let(:local_account) { Fabricate(:account) }
+ let(:remote_account) { Fabricate(:account, domain: 'example.com') }
+ let(:event) { Fabricate(:relationship_severance_event) }
+
+ describe '#import_from_active_follows!' do
+ before do
+ local_account.follow!(remote_account)
+ end
+
+ it 'imports the follow relationships with the expected direction' do
+ event.import_from_active_follows!(local_account.active_relationships)
+
+ relationships = event.severed_relationships.to_a
+ expect(relationships.size).to eq 1
+ expect(relationships[0].account).to eq local_account
+ expect(relationships[0].target_account).to eq remote_account
+ end
+ end
+
+ describe '#import_from_passive_follows!' do
+ before do
+ remote_account.follow!(local_account)
+ end
+
+ it 'imports the follow relationships with the expected direction' do
+ event.import_from_passive_follows!(local_account.passive_relationships)
+
+ relationships = event.severed_relationships.to_a
+ expect(relationships.size).to eq 1
+ expect(relationships[0].account).to eq remote_account
+ expect(relationships[0].target_account).to eq local_account
+ end
+ end
+
+ describe '#affected_local_accounts' do
+ before do
+ event.severed_relationships.create!(local_account: local_account, remote_account: remote_account, direction: :active)
+ end
+
+ it 'correctly lists local accounts' do
+ expect(event.affected_local_accounts.to_a).to contain_exactly(local_account)
+ end
+ end
+end
diff --git a/spec/models/severed_relationship_spec.rb b/spec/models/severed_relationship_spec.rb
new file mode 100644
index 0000000000..0f922d7983
--- /dev/null
+++ b/spec/models/severed_relationship_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe SeveredRelationship do
+ let(:local_account) { Fabricate(:account) }
+ let(:remote_account) { Fabricate(:account, domain: 'example.com') }
+ let(:event) { Fabricate(:relationship_severance_event) }
+
+ describe '#account' do
+ context 'when the local account is the follower' do
+ let(:severed_relationship) { Fabricate(:severed_relationship, relationship_severance_event: event, local_account: local_account, remote_account: remote_account, direction: :active) }
+
+ it 'returns the local account' do
+ expect(severed_relationship.account).to eq local_account
+ end
+ end
+
+ context 'when the local account is being followed' do
+ let(:severed_relationship) { Fabricate(:severed_relationship, relationship_severance_event: event, local_account: local_account, remote_account: remote_account, direction: :passive) }
+
+ it 'returns the remote account' do
+ expect(severed_relationship.account).to eq remote_account
+ end
+ end
+ end
+
+ describe '#target_account' do
+ context 'when the local account is the follower' do
+ let(:severed_relationship) { Fabricate(:severed_relationship, relationship_severance_event: event, local_account: local_account, remote_account: remote_account, direction: :active) }
+
+ it 'returns the remote account' do
+ expect(severed_relationship.target_account).to eq remote_account
+ end
+ end
+
+ context 'when the local account is being followed' do
+ let(:severed_relationship) { Fabricate(:severed_relationship, relationship_severance_event: event, local_account: local_account, remote_account: remote_account, direction: :passive) }
+
+ it 'returns the local account' do
+ expect(severed_relationship.target_account).to eq local_account
+ end
+ end
+ end
+end
diff --git a/spec/requests/severed_relationships_spec.rb b/spec/requests/severed_relationships_spec.rb
new file mode 100644
index 0000000000..4063026d79
--- /dev/null
+++ b/spec/requests/severed_relationships_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'Severed relationships page' do
+ include RoutingHelper
+
+ describe 'GET severed_relationships#index' do
+ let(:user) { Fabricate(:user) }
+
+ before do
+ sign_in user
+
+ Fabricate(:severed_relationship, local_account: user.account)
+ end
+
+ it 'returns http success' do
+ get severed_relationships_path
+
+ expect(response).to have_http_status(200)
+ end
+ end
+end
diff --git a/spec/services/after_block_domain_from_account_service_spec.rb b/spec/services/after_block_domain_from_account_service_spec.rb
index 2fce424b1a..12e780357d 100644
--- a/spec/services/after_block_domain_from_account_service_spec.rb
+++ b/spec/services/after_block_domain_from_account_service_spec.rb
@@ -5,22 +5,33 @@ require 'rails_helper'
RSpec.describe AfterBlockDomainFromAccountService do
subject { described_class.new }
- let!(:wolf) { Fabricate(:account, username: 'wolf', domain: 'evil.org', inbox_url: 'https://evil.org/inbox', protocol: :activitypub) }
- let!(:alice) { Fabricate(:account, username: 'alice') }
+ let(:wolf) { Fabricate(:account, username: 'wolf', domain: 'evil.org', inbox_url: 'https://evil.org/wolf/inbox', protocol: :activitypub) }
+ let(:dog) { Fabricate(:account, username: 'dog', domain: 'evil.org', inbox_url: 'https://evil.org/dog/inbox', protocol: :activitypub) }
+ let(:alice) { Fabricate(:account, username: 'alice') }
before do
- allow(ActivityPub::DeliveryWorker).to receive(:perform_async)
+ wolf.follow!(alice)
+ alice.follow!(dog)
end
- it 'purge followers from blocked domain' do
- wolf.follow!(alice)
+ around do |example|
+ Sidekiq::Testing.fake! do
+ example.run
+ end
+ end
+
+ it 'purges followers from blocked domain, sends them Reject->Follow, and records severed relationships', :aggregate_failures do
subject.call(alice, 'evil.org')
+
expect(wolf.following?(alice)).to be false
- end
+ expect(ActivityPub::DeliveryWorker.jobs.pluck('args')).to contain_exactly(
+ [a_string_including('"type":"Reject"'), alice.id, wolf.inbox_url],
+ [a_string_including('"type":"Undo"'), alice.id, dog.inbox_url]
+ )
- it 'sends Reject->Follow to followers from blocked domain' do
- wolf.follow!(alice)
- subject.call(alice, 'evil.org')
- expect(ActivityPub::DeliveryWorker).to have_received(:perform_async).once
+ severed_relationships = alice.severed_relationships.to_a
+ expect(severed_relationships.count).to eq 2
+ expect(severed_relationships[0].relationship_severance_event).to eq severed_relationships[1].relationship_severance_event
+ expect(severed_relationships.map { |rel| [rel.account, rel.target_account] }).to contain_exactly([wolf, alice], [alice, dog])
end
end
diff --git a/spec/services/block_domain_service_spec.rb b/spec/services/block_domain_service_spec.rb
index 0f278293a8..26f80eaf62 100644
--- a/spec/services/block_domain_service_spec.rb
+++ b/spec/services/block_domain_service_spec.rb
@@ -5,6 +5,8 @@ require 'rails_helper'
RSpec.describe BlockDomainService do
subject { described_class.new }
+ let(:local_account) { Fabricate(:account) }
+ let(:bystander) { Fabricate(:account, domain: 'evil.org') }
let!(:bad_account) { Fabricate(:account, username: 'badguy666', domain: 'evil.org') }
let!(:bad_status_plain) { Fabricate(:status, account: bad_account, text: 'You suck') }
let!(:bad_status_with_attachment) { Fabricate(:status, account: bad_account, text: 'Hahaha') }
@@ -13,62 +15,51 @@ RSpec.describe BlockDomainService do
describe 'for a suspension' do
before do
+ local_account.follow!(bad_account)
+ bystander.follow!(local_account)
+ end
+
+ it 'creates a domain block, suspends remote accounts with appropriate suspension date, records severed relationships', :aggregate_failures do
subject.call(DomainBlock.create!(domain: 'evil.org', severity: :suspend))
- end
- it 'creates a domain block' do
expect(DomainBlock.blocked?('evil.org')).to be true
- end
- it 'removes remote accounts from that domain' do
+ # Suspends account with appropriate suspension date
expect(bad_account.reload.suspended?).to be true
- end
-
- it 'records suspension date appropriately' do
expect(bad_account.reload.suspended_at).to eq DomainBlock.find_by(domain: 'evil.org').created_at
- end
- it 'keeps already-banned accounts banned' do
+ # Keep already-suspended account without updating the suspension date
expect(already_banned_account.reload.suspended?).to be true
- end
-
- it 'does not overwrite suspension date of already-banned accounts' do
expect(already_banned_account.reload.suspended_at).to_not eq DomainBlock.find_by(domain: 'evil.org').created_at
- end
- it 'removes the remote accounts\'s statuses and media attachments' do
+ # Removes content
expect { bad_status_plain.reload }.to raise_exception ActiveRecord::RecordNotFound
expect { bad_status_with_attachment.reload }.to raise_exception ActiveRecord::RecordNotFound
expect { bad_attachment.reload }.to raise_exception ActiveRecord::RecordNotFound
+
+ # Records severed relationships
+ severed_relationships = local_account.severed_relationships.to_a
+ expect(severed_relationships.count).to eq 2
+ expect(severed_relationships[0].relationship_severance_event).to eq severed_relationships[1].relationship_severance_event
+ expect(severed_relationships.map { |rel| [rel.account, rel.target_account] }).to contain_exactly([bystander, local_account], [local_account, bad_account])
end
end
describe 'for a silence with reject media' do
- before do
+ it 'does not mark the domain as blocked, but silences accounts with an appropriate silencing date, clears media', :aggregate_failures, :sidekiq_inline do
subject.call(DomainBlock.create!(domain: 'evil.org', severity: :silence, reject_media: true))
- end
- it 'does not create a domain block' do
expect(DomainBlock.blocked?('evil.org')).to be false
- end
- it 'silences remote accounts from that domain' do
+ # Silences account with appropriate silecing date
expect(bad_account.reload.silenced?).to be true
- end
-
- it 'records suspension date appropriately' do
expect(bad_account.reload.silenced_at).to eq DomainBlock.find_by(domain: 'evil.org').created_at
- end
- it 'keeps already-banned accounts banned' do
+ # Keeps already-silenced accounts without updating the silecing date
expect(already_banned_account.reload.silenced?).to be true
- end
-
- it 'does not overwrite suspension date of already-banned accounts' do
expect(already_banned_account.reload.silenced_at).to_not eq DomainBlock.find_by(domain: 'evil.org').created_at
- end
- it 'leaves the domains status and attachments, but clears media', :sidekiq_inline do
+ # Leaves posts but clears media
expect { bad_status_plain.reload }.to_not raise_error
expect { bad_status_with_attachment.reload }.to_not raise_error
expect { bad_attachment.reload }.to_not raise_error
diff --git a/spec/services/suspend_account_service_spec.rb b/spec/services/suspend_account_service_spec.rb
index d62f7ef0d6..74ef8f60e2 100644
--- a/spec/services/suspend_account_service_spec.rb
+++ b/spec/services/suspend_account_service_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe SuspendAccountService, :sidekiq_inline do
remote_follower.follow!(account)
end
- it 'sends an update actor to followers and reporters' do
+ it 'sends an Update actor activity to followers and reporters' do
subject
expect(a_request(:post, remote_follower.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once
expect(a_request(:post, remote_reporter.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once
@@ -85,9 +85,14 @@ RSpec.describe SuspendAccountService, :sidekiq_inline do
account.follow!(local_followee)
end
- it 'sends a reject follow' do
+ it 'sends a Reject Follow activity, and records severed relationships', :aggregate_failures do
subject
+
expect(a_request(:post, account.inbox_url).with { |req| match_reject_follow_request(req, account, local_followee) }).to have_been_made.once
+
+ severed_relationships = local_followee.severed_relationships.to_a
+ expect(severed_relationships.count).to eq 1
+ expect(severed_relationships.map { |rel| [rel.account, rel.target_account] }).to contain_exactly([account, local_followee])
end
end
end
diff --git a/yarn.lock b/yarn.lock
index 700c3f3e8d..f7d4d176a3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -42,13 +42,13 @@ __metadata:
languageName: node
linkType: hard
-"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.23.5, @babel/code-frame@npm:^7.24.1":
- version: 7.24.1
- resolution: "@babel/code-frame@npm:7.24.1"
+"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.23.5, @babel/code-frame@npm:^7.24.1, @babel/code-frame@npm:^7.24.2":
+ version: 7.24.2
+ resolution: "@babel/code-frame@npm:7.24.2"
dependencies:
- "@babel/highlight": "npm:^7.24.1"
+ "@babel/highlight": "npm:^7.24.2"
picocolors: "npm:^1.0.0"
- checksum: 10c0/c92f4244538089c95f0322c5e8f6c2287529a576ae9ab254366a073c8720ecb537e7009d5d79a6a5698d3b658b298fc77691c05608cafbe4957cab03033ada15
+ checksum: 10c0/d1d4cba89475ab6aab7a88242e1fd73b15ecb9f30c109b69752956434d10a26a52cbd37727c4eca104b6d45227bd1dfce39a6a6f4a14c9b2f07f871e968cf406
languageName: node
linkType: hard
@@ -60,11 +60,11 @@ __metadata:
linkType: hard
"@babel/core@npm:^7.10.4, @babel/core@npm:^7.11.1, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.22.1":
- version: 7.24.1
- resolution: "@babel/core@npm:7.24.1"
+ version: 7.24.3
+ resolution: "@babel/core@npm:7.24.3"
dependencies:
"@ampproject/remapping": "npm:^2.2.0"
- "@babel/code-frame": "npm:^7.24.1"
+ "@babel/code-frame": "npm:^7.24.2"
"@babel/generator": "npm:^7.24.1"
"@babel/helper-compilation-targets": "npm:^7.23.6"
"@babel/helper-module-transforms": "npm:^7.23.3"
@@ -78,7 +78,7 @@ __metadata:
gensync: "npm:^1.0.0-beta.2"
json5: "npm:^2.2.3"
semver: "npm:^6.3.1"
- checksum: 10c0/b085b0bc65c225f20b9d5f7b05c8b127c005a73c355d4a7480f099de5d6757abafa7f60786eb95e6d098a6b5c34618e7b0950d60ef55139db04d8767d410e0a9
+ checksum: 10c0/e6e756b6de27d0312514a005688fa1915c521ad4269a388913eff2120a546538078f8488d6d16e86f851872f263cb45a6bbae08738297afb9382600d2ac342a9
languageName: node
linkType: hard
@@ -217,12 +217,12 @@ __metadata:
languageName: node
linkType: hard
-"@babel/helper-module-imports@npm:^7.0.0-beta.49, @babel/helper-module-imports@npm:^7.10.4, @babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.24.1":
- version: 7.24.1
- resolution: "@babel/helper-module-imports@npm:7.24.1"
+"@babel/helper-module-imports@npm:^7.0.0-beta.49, @babel/helper-module-imports@npm:^7.10.4, @babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.24.1, @babel/helper-module-imports@npm:^7.24.3":
+ version: 7.24.3
+ resolution: "@babel/helper-module-imports@npm:7.24.3"
dependencies:
"@babel/types": "npm:^7.24.0"
- checksum: 10c0/9242a9af73e7eb3fe106d7e55c157149f7f38df6494980744e99fc608103c2ee20726df9596fae722c57ae89c9c304200673b733c3c1c5312385ff26ebd2a4fa
+ checksum: 10c0/052c188adcd100f5e8b6ff0c9643ddaabc58b6700d3bbbc26804141ad68375a9f97d9d173658d373d31853019e65f62610239e3295cdd58e573bdcb2fded188d
languageName: node
linkType: hard
@@ -353,15 +353,15 @@ __metadata:
languageName: node
linkType: hard
-"@babel/highlight@npm:^7.24.1":
- version: 7.24.1
- resolution: "@babel/highlight@npm:7.24.1"
+"@babel/highlight@npm:^7.24.2":
+ version: 7.24.2
+ resolution: "@babel/highlight@npm:7.24.2"
dependencies:
"@babel/helper-validator-identifier": "npm:^7.22.20"
chalk: "npm:^2.4.2"
js-tokens: "npm:^4.0.0"
picocolors: "npm:^1.0.0"
- checksum: 10c0/39520f655101245efd44a6e5997e73e2b977f8e2011897022ec81fa5b0366dbfef5313bdebadbd08186f52b7e9c21b38ba265cc78caa0c6a8c894461e7a70430
+ checksum: 10c0/98ce00321daedeed33a4ed9362dc089a70375ff1b3b91228b9f05e6591d387a81a8cba68886e207861b8871efa0bc997ceabdd9c90f6cce3ee1b2f7f941b42db
languageName: node
linkType: hard
@@ -662,9 +662,9 @@ __metadata:
languageName: node
linkType: hard
-"@babel/plugin-transform-async-generator-functions@npm:^7.24.1":
- version: 7.24.1
- resolution: "@babel/plugin-transform-async-generator-functions@npm:7.24.1"
+"@babel/plugin-transform-async-generator-functions@npm:^7.24.3":
+ version: 7.24.3
+ resolution: "@babel/plugin-transform-async-generator-functions@npm:7.24.3"
dependencies:
"@babel/helper-environment-visitor": "npm:^7.22.20"
"@babel/helper-plugin-utils": "npm:^7.24.0"
@@ -672,7 +672,7 @@ __metadata:
"@babel/plugin-syntax-async-generators": "npm:^7.8.4"
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 10c0/c207cb6b3b7dcb5cd9b22ce5a063493ec7534a21375cb987ad079eee6cc5cae573bd3c97af91fbf0ee47cae1d3f8632c4387885aeab0eedece5652dc4e6b9f1c
+ checksum: 10c0/55ceed059f819dcccbfe69600bfa1c055ada466bd54eda117cfdd2cf773dd85799e2f6556e4a559b076e93b9704abcca2aef9d72aad7dc8a5d3d17886052f1d3
languageName: node
linkType: hard
@@ -1200,10 +1200,10 @@ __metadata:
linkType: hard
"@babel/plugin-transform-runtime@npm:^7.22.4":
- version: 7.24.1
- resolution: "@babel/plugin-transform-runtime@npm:7.24.1"
+ version: 7.24.3
+ resolution: "@babel/plugin-transform-runtime@npm:7.24.3"
dependencies:
- "@babel/helper-module-imports": "npm:^7.24.1"
+ "@babel/helper-module-imports": "npm:^7.24.3"
"@babel/helper-plugin-utils": "npm:^7.24.0"
babel-plugin-polyfill-corejs2: "npm:^0.4.10"
babel-plugin-polyfill-corejs3: "npm:^0.10.1"
@@ -1211,7 +1211,7 @@ __metadata:
semver: "npm:^6.3.1"
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 10c0/54e4dbc7aa6fd6c338c657108418f10301e88bbc4c00675f901d529d3bbf578e6a438076bc57c1efdb433853dea1d7c1a6ebaa18f936debc1fd9e25f7553c33a
+ checksum: 10c0/ee01967bf405d84bd95ca4089166a18fb23fe9851a6da53dcf712a7f8ba003319996f21f320d568ec76126e18adfaee978206ccda86eef7652d47cc9a052e75e
languageName: node
linkType: hard
@@ -1333,8 +1333,8 @@ __metadata:
linkType: hard
"@babel/preset-env@npm:^7.11.0, @babel/preset-env@npm:^7.12.1, @babel/preset-env@npm:^7.22.4":
- version: 7.24.1
- resolution: "@babel/preset-env@npm:7.24.1"
+ version: 7.24.3
+ resolution: "@babel/preset-env@npm:7.24.3"
dependencies:
"@babel/compat-data": "npm:^7.24.1"
"@babel/helper-compilation-targets": "npm:^7.23.6"
@@ -1363,7 +1363,7 @@ __metadata:
"@babel/plugin-syntax-top-level-await": "npm:^7.14.5"
"@babel/plugin-syntax-unicode-sets-regex": "npm:^7.18.6"
"@babel/plugin-transform-arrow-functions": "npm:^7.24.1"
- "@babel/plugin-transform-async-generator-functions": "npm:^7.24.1"
+ "@babel/plugin-transform-async-generator-functions": "npm:^7.24.3"
"@babel/plugin-transform-async-to-generator": "npm:^7.24.1"
"@babel/plugin-transform-block-scoped-functions": "npm:^7.24.1"
"@babel/plugin-transform-block-scoping": "npm:^7.24.1"
@@ -1412,13 +1412,13 @@ __metadata:
"@babel/plugin-transform-unicode-sets-regex": "npm:^7.24.1"
"@babel/preset-modules": "npm:0.1.6-no-external-plugins"
babel-plugin-polyfill-corejs2: "npm:^0.4.10"
- babel-plugin-polyfill-corejs3: "npm:^0.10.1"
+ babel-plugin-polyfill-corejs3: "npm:^0.10.4"
babel-plugin-polyfill-regenerator: "npm:^0.6.1"
core-js-compat: "npm:^3.31.0"
semver: "npm:^6.3.1"
peerDependencies:
"@babel/core": ^7.0.0-0
- checksum: 10c0/c9fd96ed8561ac3f8d2e490face5b763ea05a7908770a52fe2ffa0e72581c5cd8ea5c016a85f47775fc3584c6d748eea62044c19ae3f4edf11ffc1e1e9c8bffd
+ checksum: 10c0/abd6f3b6c6a71d4ff766cda5b51467677a811240d022492e651065e26ce1a8eb2067eabe5653fce80dda9c5c204fb7b89b419578d7e86eaaf7970929ee7b4885
languageName: node
linkType: hard
@@ -1578,16 +1578,7 @@ __metadata:
languageName: node
linkType: hard
-"@csstools/css-parser-algorithms@npm:^2.5.0":
- version: 2.5.0
- resolution: "@csstools/css-parser-algorithms@npm:2.5.0"
- peerDependencies:
- "@csstools/css-tokenizer": ^2.2.3
- checksum: 10c0/31b4a523d956e204af9842183678cca5a88ad76551d54dcb6083f8a6f2dfd8fdec6c09bca5410842af54b90997308bebee7593c17dbc1a4e951453b54bd3f024
- languageName: node
- linkType: hard
-
-"@csstools/css-parser-algorithms@npm:^2.6.1":
+"@csstools/css-parser-algorithms@npm:^2.5.0, @csstools/css-parser-algorithms@npm:^2.6.1":
version: 2.6.1
resolution: "@csstools/css-parser-algorithms@npm:2.6.1"
peerDependencies:
@@ -1596,31 +1587,14 @@ __metadata:
languageName: node
linkType: hard
-"@csstools/css-tokenizer@npm:^2.2.3":
- version: 2.2.3
- resolution: "@csstools/css-tokenizer@npm:2.2.3"
- checksum: 10c0/557266ec52e8b36c19008a5bbd7151effba085cdd6d68270c01afebf914981caac698eda754b2a530a8a9947a3dd70e3f3a39a5e037c4170bb2a055a92754acb
- languageName: node
- linkType: hard
-
-"@csstools/css-tokenizer@npm:^2.2.4":
+"@csstools/css-tokenizer@npm:^2.2.3, @csstools/css-tokenizer@npm:^2.2.4":
version: 2.2.4
resolution: "@csstools/css-tokenizer@npm:2.2.4"
checksum: 10c0/23997db5874514f4b951ebd215e1e6cc8baf03adf9a35fc6fd028b84cb52aa2dc053860722108c09859a9b37b455f62b84181fe15539cd37797ea699b9ff85f0
languageName: node
linkType: hard
-"@csstools/media-query-list-parser@npm:^2.1.7":
- version: 2.1.7
- resolution: "@csstools/media-query-list-parser@npm:2.1.7"
- peerDependencies:
- "@csstools/css-parser-algorithms": ^2.5.0
- "@csstools/css-tokenizer": ^2.2.3
- checksum: 10c0/433aef06b00f1d402fd24074a1919b8e2de94245a3b780da6466c8cc9e0f3cc93d2db930f0fce36c7d6908cd50b626cd61e803d3f62dddad79eeb742858028ef
- languageName: node
- linkType: hard
-
-"@csstools/media-query-list-parser@npm:^2.1.9":
+"@csstools/media-query-list-parser@npm:^2.1.7, @csstools/media-query-list-parser@npm:^2.1.9":
version: 2.1.9
resolution: "@csstools/media-query-list-parser@npm:2.1.9"
peerDependencies:
@@ -1996,16 +1970,7 @@ __metadata:
languageName: node
linkType: hard
-"@csstools/selector-specificity@npm:^3.0.1":
- version: 3.0.1
- resolution: "@csstools/selector-specificity@npm:3.0.1"
- peerDependencies:
- postcss-selector-parser: ^6.0.13
- checksum: 10c0/4280f494726d5e38de74e28dee2ff74ec86244560dff4edeec3ddff3ac73c774c19535bd1bb70cad77949bfb359cf87e977d0ec3264591e3b7260342a20dd84f
- languageName: node
- linkType: hard
-
-"@csstools/selector-specificity@npm:^3.0.2":
+"@csstools/selector-specificity@npm:^3.0.1, @csstools/selector-specificity@npm:^3.0.2":
version: 3.0.2
resolution: "@csstools/selector-specificity@npm:3.0.2"
peerDependencies:
@@ -5263,15 +5228,15 @@ __metadata:
languageName: node
linkType: hard
-"babel-plugin-polyfill-corejs3@npm:^0.10.1":
- version: 0.10.1
- resolution: "babel-plugin-polyfill-corejs3@npm:0.10.1"
+"babel-plugin-polyfill-corejs3@npm:^0.10.1, babel-plugin-polyfill-corejs3@npm:^0.10.4":
+ version: 0.10.4
+ resolution: "babel-plugin-polyfill-corejs3@npm:0.10.4"
dependencies:
"@babel/helper-define-polyfill-provider": "npm:^0.6.1"
- core-js-compat: "npm:^3.36.0"
+ core-js-compat: "npm:^3.36.1"
peerDependencies:
"@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0
- checksum: 10c0/54ad0b2d2eb44ae8cc20485ef3be6097e262e02ee76003f39734af5e421743dcff478bfc3cc8fef7cda298e1b507c618ff161c47672fd1ae598df0a2ca48b4e1
+ checksum: 10c0/31b92cd3dfb5b417da8dfcf0deaa4b8b032b476d7bb31ca51c66127cf25d41e89260e89d17bc004b2520faa38aa9515fafabf81d89f9d4976e9dc1163e4a7c41
languageName: node
linkType: hard
@@ -6344,7 +6309,7 @@ __metadata:
languageName: node
linkType: hard
-"core-js-compat@npm:^3.31.0, core-js-compat@npm:^3.36.0":
+"core-js-compat@npm:^3.31.0, core-js-compat@npm:^3.36.1":
version: 3.36.1
resolution: "core-js-compat@npm:3.36.1"
dependencies:
@@ -6708,9 +6673,9 @@ __metadata:
languageName: node
linkType: hard
-"cssnano-preset-default@npm:^6.1.0":
- version: 6.1.0
- resolution: "cssnano-preset-default@npm:6.1.0"
+"cssnano-preset-default@npm:^6.1.1":
+ version: 6.1.1
+ resolution: "cssnano-preset-default@npm:6.1.1"
dependencies:
browserslist: "npm:^4.23.0"
css-declaration-sorter: "npm:^7.1.1"
@@ -6722,12 +6687,12 @@ __metadata:
postcss-discard-duplicates: "npm:^6.0.3"
postcss-discard-empty: "npm:^6.0.3"
postcss-discard-overridden: "npm:^6.0.2"
- postcss-merge-longhand: "npm:^6.0.4"
- postcss-merge-rules: "npm:^6.1.0"
- postcss-minify-font-values: "npm:^6.0.3"
+ postcss-merge-longhand: "npm:^6.0.5"
+ postcss-merge-rules: "npm:^6.1.1"
+ postcss-minify-font-values: "npm:^6.1.0"
postcss-minify-gradients: "npm:^6.0.3"
postcss-minify-params: "npm:^6.1.0"
- postcss-minify-selectors: "npm:^6.0.3"
+ postcss-minify-selectors: "npm:^6.0.4"
postcss-normalize-charset: "npm:^6.0.2"
postcss-normalize-display-values: "npm:^6.0.2"
postcss-normalize-positions: "npm:^6.0.2"
@@ -6741,10 +6706,10 @@ __metadata:
postcss-reduce-initial: "npm:^6.1.0"
postcss-reduce-transforms: "npm:^6.0.2"
postcss-svgo: "npm:^6.0.3"
- postcss-unique-selectors: "npm:^6.0.3"
+ postcss-unique-selectors: "npm:^6.0.4"
peerDependencies:
postcss: ^8.4.31
- checksum: 10c0/47b7026b66b80a03f043929f825f48a13ed3a4086a6f335f25312c77fe73977a74cf718a486f91d9513b652e7d34312394380141c3bf6b8c8027ebc96710b6f6
+ checksum: 10c0/dc5927c8538778f859b781dc1fb10f0082cd7b3afdba30e835e472dbf51724d4f18e5170587761150142284a371d6c9d2c3b51d29826c08d2b9dd86a929597ee
languageName: node
linkType: hard
@@ -6758,14 +6723,14 @@ __metadata:
linkType: hard
"cssnano@npm:^6.0.1":
- version: 6.1.0
- resolution: "cssnano@npm:6.1.0"
+ version: 6.1.1
+ resolution: "cssnano@npm:6.1.1"
dependencies:
- cssnano-preset-default: "npm:^6.1.0"
+ cssnano-preset-default: "npm:^6.1.1"
lilconfig: "npm:^3.1.1"
peerDependencies:
postcss: ^8.4.31
- checksum: 10c0/ffe0d8c9110cce01692f51d21ae2fe6d319f2329989d28ef0dddb67a6fba2780c525f00682f0788bdbba380f37893d27ee870b3e99fb97c1fb8edccbd68a1d92
+ checksum: 10c0/2db52c2f5e314f05efec8977de392886ef0e7e08568ac45446f2303218180e317cee64c6c0c6d2c1d70a7f339fcead75384e09e187b88ccacd7f9fd51919fdf1
languageName: node
linkType: hard
@@ -13446,40 +13411,40 @@ __metadata:
languageName: node
linkType: hard
-"postcss-merge-longhand@npm:^6.0.4":
- version: 6.0.4
- resolution: "postcss-merge-longhand@npm:6.0.4"
+"postcss-merge-longhand@npm:^6.0.5":
+ version: 6.0.5
+ resolution: "postcss-merge-longhand@npm:6.0.5"
dependencies:
postcss-value-parser: "npm:^4.2.0"
- stylehacks: "npm:^6.1.0"
+ stylehacks: "npm:^6.1.1"
peerDependencies:
postcss: ^8.4.31
- checksum: 10c0/6c05cfe60d86cb0b6f40abe4649e1c0c21cf416fbf17aa15f04c315fcef4887827db5ef2593eca27b2b14127f5338ab179b147940c22315b5a9bcb0bdbbfa768
+ checksum: 10c0/5a223a7f698c05ab42e9997108a7ff27ea1e0c33a11a353d65a04fc89c3b5b750b9e749550d76b6406329117a055adfc79dde7fee48dca5c8e167a2854ae3fea
languageName: node
linkType: hard
-"postcss-merge-rules@npm:^6.1.0":
- version: 6.1.0
- resolution: "postcss-merge-rules@npm:6.1.0"
+"postcss-merge-rules@npm:^6.1.1":
+ version: 6.1.1
+ resolution: "postcss-merge-rules@npm:6.1.1"
dependencies:
browserslist: "npm:^4.23.0"
caniuse-api: "npm:^3.0.0"
cssnano-utils: "npm:^4.0.2"
- postcss-selector-parser: "npm:^6.0.15"
+ postcss-selector-parser: "npm:^6.0.16"
peerDependencies:
postcss: ^8.4.31
- checksum: 10c0/3ce76c87e29003fe46fbeba64348ed61d50d8966cfd56ec59b70b6fbf2e2ea8866b8399eec09e036fc636c84207ba12037a1dbc1374fd313a885511947699cad
+ checksum: 10c0/6d8952dbb19b1e59bf5affe0871fa1be6515103466857cff5af879d6cf619659f8642ec7a931cabb7cdbd393d8c1e91748bf70bee70fa3edea010d4e25786d04
languageName: node
linkType: hard
-"postcss-minify-font-values@npm:^6.0.3":
- version: 6.0.3
- resolution: "postcss-minify-font-values@npm:6.0.3"
+"postcss-minify-font-values@npm:^6.1.0":
+ version: 6.1.0
+ resolution: "postcss-minify-font-values@npm:6.1.0"
dependencies:
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.31
- checksum: 10c0/c1ae31099e3ae79169405d3d46cd49cff35c70c63d1f36f24b16fcce43999c130db396e1fde071a375bd5b4853b14058111034a8da278a3a31f9ca12e091116e
+ checksum: 10c0/0d6567170c22a7db42096b5eac298f041614890fbe01759a9fa5ccda432f2bb09efd399d92c11bf6675ae13ccd259db4602fad3c358317dee421df5f7ab0a003
languageName: node
linkType: hard
@@ -13509,14 +13474,14 @@ __metadata:
languageName: node
linkType: hard
-"postcss-minify-selectors@npm:^6.0.3":
- version: 6.0.3
- resolution: "postcss-minify-selectors@npm:6.0.3"
+"postcss-minify-selectors@npm:^6.0.4":
+ version: 6.0.4
+ resolution: "postcss-minify-selectors@npm:6.0.4"
dependencies:
- postcss-selector-parser: "npm:^6.0.15"
+ postcss-selector-parser: "npm:^6.0.16"
peerDependencies:
postcss: ^8.4.31
- checksum: 10c0/6abc83edf3fd746979ef709182fd613a764c5c2f68ae20aaa1b38940153a1078c0b270d657fe3bcfe6cda44b61f5af762fe9b31b8b62f63b897bfc2d2bc02b88
+ checksum: 10c0/695ec2e1e3a7812b0cabe1105d0ed491760be3d8e9433914fb5af1fc30a84e6dc24089cd31b7e300de620b8e7adf806526c1acf8dd14077a7d1d2820c60a327c
languageName: node
linkType: hard
@@ -13876,13 +13841,13 @@ __metadata:
languageName: node
linkType: hard
-"postcss-selector-parser@npm:^6.0.11, postcss-selector-parser@npm:^6.0.13, postcss-selector-parser@npm:^6.0.15, postcss-selector-parser@npm:^6.0.2, postcss-selector-parser@npm:^6.0.4":
- version: 6.0.15
- resolution: "postcss-selector-parser@npm:6.0.15"
+"postcss-selector-parser@npm:^6.0.11, postcss-selector-parser@npm:^6.0.13, postcss-selector-parser@npm:^6.0.15, postcss-selector-parser@npm:^6.0.16, postcss-selector-parser@npm:^6.0.2, postcss-selector-parser@npm:^6.0.4":
+ version: 6.0.16
+ resolution: "postcss-selector-parser@npm:6.0.16"
dependencies:
cssesc: "npm:^3.0.0"
util-deprecate: "npm:^1.0.2"
- checksum: 10c0/48b425d6cef497bcf6b7d136f6fd95cfca43026955e07ec9290d3c15457de3a862dbf251dd36f42c07a0d5b5ab6f31e41acefeff02528995a989b955505e440b
+ checksum: 10c0/0e11657cb3181aaf9ff67c2e59427c4df496b4a1b6a17063fae579813f80af79d444bf38f82eeb8b15b4679653fd3089e66ef0283f9aab01874d885e6cf1d2cf
languageName: node
linkType: hard
@@ -13898,14 +13863,14 @@ __metadata:
languageName: node
linkType: hard
-"postcss-unique-selectors@npm:^6.0.3":
- version: 6.0.3
- resolution: "postcss-unique-selectors@npm:6.0.3"
+"postcss-unique-selectors@npm:^6.0.4":
+ version: 6.0.4
+ resolution: "postcss-unique-selectors@npm:6.0.4"
dependencies:
- postcss-selector-parser: "npm:^6.0.15"
+ postcss-selector-parser: "npm:^6.0.16"
peerDependencies:
postcss: ^8.4.31
- checksum: 10c0/884c5da4c3bfdacf6a61bb3bd23d212e61d2b3e99ba5099d4d646d18970d2c72d8f6bd8f2ab244ee68d7214e576dc3fd9004fc946ff872e745a965da29f7b18b
+ checksum: 10c0/bfb99d8a7c675c93f2e65c9d9d563477bfd46fdce9e2727d42d57982b31ccbaaf944e8034bfbefe48b3119e77fba7eb1b181c19b91cb3a5448058fa66a7c9ae9
languageName: node
linkType: hard
@@ -16406,15 +16371,15 @@ __metadata:
languageName: node
linkType: hard
-"stylehacks@npm:^6.1.0":
- version: 6.1.0
- resolution: "stylehacks@npm:6.1.0"
+"stylehacks@npm:^6.1.1":
+ version: 6.1.1
+ resolution: "stylehacks@npm:6.1.1"
dependencies:
browserslist: "npm:^4.23.0"
- postcss-selector-parser: "npm:^6.0.15"
+ postcss-selector-parser: "npm:^6.0.16"
peerDependencies:
postcss: ^8.4.31
- checksum: 10c0/0e9624d2b12d00d5593e3ef9ef8ed1f4c2029087b4862567cfab9ea3d3fc21efeb9aa00251c13defdfff4481abff8d6e0c48e3e27fac967c959acc7dcb0d5b67
+ checksum: 10c0/2dd2bccfd8311ff71492e63a7b8b86c3d7b1fff55d4ba5a2357aff97743e633d351cdc2f5ae3c0057637d00dab4ef5fc5b218a1b370e4585a41df22b5a5128be
languageName: node
linkType: hard