Compare commits
4 Commits
main
...
features/s
Author | SHA1 | Date |
---|---|---|
Claire | 8622abe6aa | |
Claire | 1d2859c6e1 | |
Claire | 9fa04105ff | |
Claire | 20682d5469 |
|
@ -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 = RelationshipSeveranceEvent.about_account(current_account)
|
||||
end
|
||||
|
||||
def following
|
||||
respond_to do |format|
|
||||
format.csv { send_data following_data, filename: 'following-TODO.csv' }
|
||||
end
|
||||
end
|
||||
|
||||
def followers
|
||||
respond_to do |format|
|
||||
format.csv { send_data followers_data, filename: 'followers-TODO.csv' }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_event
|
||||
@event = RelationshipSeveranceEvent.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.where(account: current_account).includes(:target_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.where(target_account: current_account).includes(: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
|
|
@ -37,6 +37,7 @@ class Notification < ApplicationRecord
|
|||
favourite
|
||||
poll
|
||||
update
|
||||
relationships_severed
|
||||
admin.sign_up
|
||||
admin.report
|
||||
).freeze
|
||||
|
@ -63,6 +64,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 }
|
||||
|
@ -156,6 +158,11 @@ class Notification < ApplicationRecord
|
|||
self.from_account_id = activity&.status&.account_id
|
||||
when 'Account'
|
||||
self.from_account_id = activity&.id
|
||||
when 'RelationshipSeveranceEvent'
|
||||
# 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
|
||||
end
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: relationship_severance_events
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# type :integer 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,
|
||||
}
|
||||
|
||||
scope :about_account, ->(account) { where(id: SeveredRelationship.about_account(account).select(:relationship_severance_event_id)) }
|
||||
|
||||
def import_from_follows!(follows)
|
||||
SeveredRelationship.insert_all( # rubocop:disable Rails/SkipsModelValidations
|
||||
follows.pluck(:account_id, :target_account_id, :show_reblogs, :notify, :languages).map do |account_id, target_account_id, show_reblogs, notify, languages|
|
||||
{
|
||||
account_id: account_id,
|
||||
target_account_id: target_account_id,
|
||||
show_reblogs: show_reblogs,
|
||||
notify: notify,
|
||||
languages: languages,
|
||||
relationship_severance_event_id: id,
|
||||
}
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
# 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
|
||||
# account_id :bigint(8) not null
|
||||
# target_account_id :bigint(8) 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 :account
|
||||
belongs_to :target_account, class_name: 'Account'
|
||||
|
||||
scope :about_account, ->(account) { where(account: account).or(where(target_account: account)) }
|
||||
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::RelationshipSeveranceEventSerializer
|
||||
|
||||
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 == :relationship_severance_event
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class REST::RelationshipSeveranceEventSerializer < ActiveModel::Serializer
|
||||
attributes :id, :type, :domain, :created_at
|
||||
|
||||
def id
|
||||
object.id.to_s
|
||||
end
|
||||
end
|
|
@ -9,6 +9,7 @@ class AfterBlockDomainFromAccountService < BaseService
|
|||
def call(account, domain)
|
||||
@account = account
|
||||
@domain = domain
|
||||
@domain_block_event = nil
|
||||
|
||||
clear_notifications!
|
||||
remove_follows!
|
||||
|
@ -19,8 +20,9 @@ class AfterBlockDomainFromAccountService < BaseService
|
|||
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_follows!(follows)
|
||||
follows.each { |follow| UnfollowService.new.call(@account, follow.target_account) }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -29,8 +31,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_follows!(follows)
|
||||
follows.each { |follow| reject_follow!(follow) }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -47,4 +50,8 @@ class AfterBlockDomainFromAccountService < BaseService
|
|||
|
||||
ActivityPub::DeliveryWorker.perform_async(Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), @account.id, follow.account.inbox_url)
|
||||
end
|
||||
|
||||
def domain_block_event
|
||||
@domain_block_event ||= RelationshipSeveranceEvent.create!(type: :user_domain_block, domain: @domain)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,6 +5,7 @@ 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
|
||||
end
|
||||
|
@ -37,7 +38,7 @@ 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
|
||||
|
||||
|
@ -45,6 +46,10 @@ class BlockDomainService < BaseService
|
|||
domain_block.domain
|
||||
end
|
||||
|
||||
def domain_block_event
|
||||
@domain_block_event ||= RelationshipSeveranceEvent.create!(type: :domain_block, domain: blocked_domain)
|
||||
end
|
||||
|
||||
def blocked_domain_accounts
|
||||
Account.by_domain_and_subdomains(blocked_domain)
|
||||
end
|
||||
|
|
|
@ -72,6 +72,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 +85,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 +268,18 @@ class DeleteAccountService < BaseService
|
|||
end
|
||||
end
|
||||
|
||||
def record_severed_relationships!
|
||||
return if relationship_severance_event.nil?
|
||||
|
||||
Follow.where(target_account: @account).in_batches do |follows|
|
||||
relationship_severance_event.import_from_follows!(follows)
|
||||
end
|
||||
|
||||
Follow.where(account: @account).in_batches do |follows|
|
||||
relationship_severance_event.import_from_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 +319,8 @@ class DeleteAccountService < BaseService
|
|||
def skip_activitypub?
|
||||
@options[:skip_activitypub]
|
||||
end
|
||||
|
||||
def relationship_severance_event
|
||||
@options[:relationship_severance_event]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -109,7 +109,7 @@ class NotifyService < BaseService
|
|||
|
||||
def blocked?
|
||||
blocked = @recipient.suspended?
|
||||
blocked ||= from_self? && @notification.type != :poll
|
||||
blocked ||= from_self? && @notification.type != :poll && @notification.type != :relationships_severed
|
||||
|
||||
return blocked if message? && from_staff?
|
||||
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
- 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}")
|
||||
%td
|
||||
- count = event.severed_relationships.where(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.where(target_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)
|
|
@ -1648,10 +1648,19 @@ 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:
|
||||
domain_block: Server suspension
|
||||
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.
|
||||
type: Event
|
||||
statuses:
|
||||
attached:
|
||||
audio:
|
||||
|
|
|
@ -16,7 +16,11 @@ SimpleNavigation::Configuration.run do |navigation|
|
|||
s.item :other, safe_join([fa_icon('cog fw'), t('preferences.other')]), settings_preferences_other_path
|
||||
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 }
|
||||
|
||||
|
|
|
@ -161,6 +161,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
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
# 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 :domain
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateSeveredRelationships < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
create_table :severed_relationships do |t|
|
||||
t.references :relationship_severance_event, null: false, foreign_key: { on_delete: :cascade }
|
||||
t.references :account, null: false, foreign_key: { on_delete: :cascade }
|
||||
t.references :target_account, null: false, foreign_key: { to_table: :accounts, on_delete: :cascade }
|
||||
|
||||
t.boolean :show_reblogs
|
||||
t.boolean :notify
|
||||
t.string :languages, array: true
|
||||
|
||||
t.timestamps
|
||||
|
||||
t.index [:relationship_severance_event_id, :account_id, :target_account_id], name: 'index_severed_relationships_on_event_account_and_target_account', unique: true
|
||||
t.index [:account_id, :relationship_severance_event_id], name: 'index_severed_relationships_on_account_and_event'
|
||||
t.index [:target_account_id, :relationship_severance_event_id], name: 'index_severed_relationships_on_target_account_and_event'
|
||||
end
|
||||
end
|
||||
end
|
29
db/schema.rb
29
db/schema.rb
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.0].define(version: 2023_09_07_150100) do
|
||||
ActiveRecord::Schema[7.0].define(version: 2023_10_23_105620) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
|
@ -813,6 +813,13 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_07_150100) do
|
|||
t.bigint "status_id", null: false
|
||||
end
|
||||
|
||||
create_table "relationship_severance_events", force: :cascade do |t|
|
||||
t.integer "type", null: false
|
||||
t.string "domain"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "relays", force: :cascade do |t|
|
||||
t.string "inbox_url", default: "", null: false
|
||||
t.string "follow_activity_id"
|
||||
|
@ -891,6 +898,23 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_07_150100) 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 "account_id", null: false
|
||||
t.bigint "target_account_id", 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 ["account_id", "relationship_severance_event_id"], name: "index_severed_relationships_on_account_and_event"
|
||||
t.index ["account_id"], name: "index_severed_relationships_on_account_id"
|
||||
t.index ["relationship_severance_event_id", "account_id", "target_account_id"], name: "index_severed_relationships_on_event_account_and_target_account", unique: true
|
||||
t.index ["relationship_severance_event_id"], name: "index_severed_relationships_on_relationship_severance_event_id"
|
||||
t.index ["target_account_id", "relationship_severance_event_id"], name: "index_severed_relationships_on_target_account_and_event"
|
||||
t.index ["target_account_id"], name: "index_severed_relationships_on_target_account_id"
|
||||
end
|
||||
|
||||
create_table "site_uploads", force: :cascade do |t|
|
||||
t.string "var", default: "", null: false
|
||||
t.string "file_file_name"
|
||||
|
@ -1252,6 +1276,9 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_07_150100) 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: "target_account_id", on_delete: :cascade
|
||||
add_foreign_key "severed_relationships", "accounts", 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
|
||||
|
|
Loading…
Reference in New Issue