Add notifications of severed relationships (#27511)
This commit is contained in:
parent
8a1423a474
commit
44bf7b8128
39 changed files with 781 additions and 54 deletions
61
app/controllers/severed_relationships_controller.rb
Normal file
61
app/controllers/severed_relationships_controller.rb
Normal file
|
@ -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
|
|
@ -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 (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<div className={classNames('notification notification-severed-relationships focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.adminReport, { name: notification.getIn(['event', 'target_name']) }), notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<Icon id='unlink' icon={LinkOffIcon} />
|
||||
|
||||
<span title={notification.get('created_at')}>
|
||||
<FormattedMessage id='notification.severedRelationships' defaultMessage='Relationships with {name} severed' values={{ name: notification.getIn(['event', 'target_name']) }} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<RelationshipsSeveranceEvent event={notification.get('event')} />
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
|
||||
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':
|
||||
|
|
|
@ -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 (
|
||||
<div className='notification__report'>
|
||||
<div className='notification__report__details'>
|
||||
<div>
|
||||
<RelativeTimestamp timestamp={event.get('created_at')} short={false} />
|
||||
{' · '}
|
||||
{ event.get('purged') ? (
|
||||
<FormattedMessage
|
||||
id='relationship_severance_notification.purged_data'
|
||||
defaultMessage='purged by administrators'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='relationship_severance_notification.relationships'
|
||||
defaultMessage='{count, plural, one {# relationship} other {# relationships}}'
|
||||
values={{ count: event.get('relationships_count', 0) }}
|
||||
/>
|
||||
)}
|
||||
<br />
|
||||
<strong>{intl.formatMessage(messages[event.get('type')])}</strong>
|
||||
</div>
|
||||
|
||||
<div className='notification__report__actions'>
|
||||
<a href='/severed_relationships' className='button' target='_blank' rel='noopener noreferrer'>
|
||||
<FormattedMessage id='relationship_severance_notification.view' defaultMessage='View' />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
RelationshipsSeveranceEvent.propTypes = {
|
||||
event: ImmutablePropTypes.map.isRequired,
|
||||
hidden: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default RelationshipsSeveranceEvent;
|
|
@ -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",
|
||||
|
|
|
@ -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) => {
|
||||
|
|
1
app/javascript/material-icons/400-24px/link_off-fill.svg
Normal file
1
app/javascript/material-icons/400-24px/link_off-fill.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m770-302-60-62q40-11 65-42.5t25-73.5q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 57-29.5 105T770-302ZM634-440l-80-80h86v80h-6ZM792-56 56-792l56-56 736 736-56 56ZM440-280H280q-83 0-141.5-58.5T80-480q0-69 42-123t108-71l74 74h-24q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h65l79 80H320Z"/></svg>
|
After Width: | Height: | Size: 414 B |
1
app/javascript/material-icons/400-24px/link_off.svg
Normal file
1
app/javascript/material-icons/400-24px/link_off.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m770-302-60-62q40-11 65-42.5t25-73.5q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 57-29.5 105T770-302ZM634-440l-80-80h86v80h-6ZM792-56 56-792l56-56 736 736-56 56ZM440-280H280q-83 0-141.5-58.5T80-480q0-69 42-123t108-71l74 74h-24q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h65l79 80H320Z"/></svg>
|
After Width: | Height: | Size: 414 B |
27
app/models/account_relationship_severance_event.rb
Normal file
27
app/models/account_relationship_severance_event.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
# 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
|
||||
# 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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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}:*")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
56
app/models/relationship_severance_event.rb
Normal file
56
app/models/relationship_severance_event.rb
Normal file
|
@ -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
|
40
app/models/severed_relationship.rb
Normal file
40
app/models/severed_relationship.rb
Normal file
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
34
app/views/severed_relationships/index.html.haml
Normal file
34
app/views/severed_relationships/index.html.haml
Normal file
|
@ -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)
|
Loading…
Add table
Add a link
Reference in a new issue