mirror of https://github.com/mastodon/mastodon
Add account migration UI (#11846)
Fix #10736 - Change data export to be available for non-functional accounts - Change non-functional accounts to include redirecting accountspull/11894/head
parent
b6df9c1067
commit
3ed94dcc1a
@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Settings::AliasesController < Settings::BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_aliases, except: :destroy
|
||||
before_action :set_alias, only: :destroy
|
||||
|
||||
def index
|
||||
@alias = current_account.aliases.build
|
||||
end
|
||||
|
||||
def create
|
||||
@alias = current_account.aliases.build(resource_params)
|
||||
|
||||
if @alias.save
|
||||
redirect_to settings_aliases_path, notice: I18n.t('aliases.created_msg')
|
||||
else
|
||||
render :show
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@alias.destroy!
|
||||
redirect_to settings_aliases_path, notice: I18n.t('aliases.deleted_msg')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resource_params
|
||||
params.require(:account_alias).permit(:acct)
|
||||
end
|
||||
|
||||
def set_alias
|
||||
@alias = current_account.aliases.find(params[:id])
|
||||
end
|
||||
|
||||
def set_aliases
|
||||
@aliases = current_account.aliases.order(id: :desc).reject(&:new_record?)
|
||||
end
|
||||
end
|
@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: account_aliases
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# account_id :bigint(8)
|
||||
# acct :string default(""), not null
|
||||
# uri :string default(""), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class AccountAlias < ApplicationRecord
|
||||
belongs_to :account
|
||||
|
||||
validates :acct, presence: true, domain: { acct: true }
|
||||
validates :uri, presence: true
|
||||
|
||||
before_validation :set_uri
|
||||
after_create :add_to_account
|
||||
after_destroy :remove_from_account
|
||||
|
||||
private
|
||||
|
||||
def set_uri
|
||||
target_account = ResolveAccountService.new.call(acct)
|
||||
self.uri = ActivityPub::TagManager.instance.uri_for(target_account) unless target_account.nil?
|
||||
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error
|
||||
# Validation will take care of it
|
||||
end
|
||||
|
||||
def add_to_account
|
||||
account.update(also_known_as: account.also_known_as + [uri])
|
||||
end
|
||||
|
||||
def remove_from_account
|
||||
account.update(also_known_as: account.also_known_as.reject { |x| x == uri })
|
||||
end
|
||||
end
|
@ -0,0 +1,74 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: account_migrations
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# account_id :bigint(8)
|
||||
# acct :string default(""), not null
|
||||
# followers_count :bigint(8) default(0), not null
|
||||
# target_account_id :bigint(8)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class AccountMigration < ApplicationRecord
|
||||
COOLDOWN_PERIOD = 30.days.freeze
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :target_account, class_name: 'Account'
|
||||
|
||||
before_validation :set_target_account
|
||||
before_validation :set_followers_count
|
||||
|
||||
validates :acct, presence: true, domain: { acct: true }
|
||||
validate :validate_migration_cooldown
|
||||
validate :validate_target_account
|
||||
|
||||
scope :within_cooldown, ->(now = Time.now.utc) { where(arel_table[:created_at].gteq(now - COOLDOWN_PERIOD)) }
|
||||
|
||||
attr_accessor :current_password, :current_username
|
||||
|
||||
def save_with_challenge(current_user)
|
||||
if current_user.encrypted_password.present?
|
||||
errors.add(:current_password, :invalid) unless current_user.valid_password?(current_password)
|
||||
else
|
||||
errors.add(:current_username, :invalid) unless account.username == current_username
|
||||
end
|
||||
|
||||
return false unless errors.empty?
|
||||
|
||||
save
|
||||
end
|
||||
|
||||
def cooldown_at
|
||||
created_at + COOLDOWN_PERIOD
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_target_account
|
||||
self.target_account = ResolveAccountService.new.call(acct)
|
||||
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error
|
||||
# Validation will take care of it
|
||||
end
|
||||
|
||||
def set_followers_count
|
||||
self.followers_count = account.followers_count
|
||||
end
|
||||
|
||||
def validate_target_account
|
||||
if target_account.nil?
|
||||
errors.add(:acct, I18n.t('migrations.errors.not_found'))
|
||||
else
|
||||
errors.add(:acct, I18n.t('migrations.errors.missing_also_known_as')) unless target_account.also_known_as.include?(ActivityPub::TagManager.instance.uri_for(account))
|
||||
errors.add(:acct, I18n.t('migrations.errors.already_moved')) if account.moved_to_account_id.present? && account.moved_to_account_id == target_account.id
|
||||
errors.add(:acct, I18n.t('migrations.errors.move_to_self')) if account.id == target_account.id
|
||||
end
|
||||
end
|
||||
|
||||
def validate_migration_cooldown
|
||||
errors.add(:base, I18n.t('migrations.errors.on_cooldown')) if account.migrations.within_cooldown.exists?
|
||||
end
|
||||
end
|
@ -1,25 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Form::Migration
|
||||
include ActiveModel::Validations
|
||||
|
||||
attr_accessor :acct, :account
|
||||
|
||||
def initialize(attrs = {})
|
||||
@account = attrs[:account]
|
||||
@acct = attrs[:account].acct unless @account.nil?
|
||||
@acct = attrs[:acct].gsub(/\A@/, '').strip unless attrs[:acct].nil?
|
||||
end
|
||||
|
||||
def valid?
|
||||
return false unless super
|
||||
set_account
|
||||
errors.empty?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
self.account = (ResolveAccountService.new.call(acct) if account.nil? && acct.present?)
|
||||
end
|
||||
end
|
@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::MoveSerializer < ActivityPub::Serializer
|
||||
attributes :id, :type, :target, :actor
|
||||
attribute :virtual_object, key: :object
|
||||
|
||||
def id
|
||||
[ActivityPub::TagManager.instance.uri_for(object.account), '#moves/', object.id].join
|
||||
end
|
||||
|
||||
def type
|
||||
'Move'
|
||||
end
|
||||
|
||||
def target
|
||||
ActivityPub::TagManager.instance.uri_for(object.target_account)
|
||||
end
|
||||
|
||||
def virtual_object
|
||||
ActivityPub::TagManager.instance.uri_for(object.account)
|
||||
end
|
||||
|
||||
def actor
|
||||
ActivityPub::TagManager.instance.uri_for(object.account)
|
||||
end
|
||||
end
|
@ -1,16 +1,22 @@
|
||||
%h3= t('auth.status.account_status')
|
||||
|
||||
- if @user.account.suspended?
|
||||
%span.negative-hint= t('user_mailer.warning.explanation.suspend')
|
||||
- elsif @user.disabled?
|
||||
%span.negative-hint= t('user_mailer.warning.explanation.disable')
|
||||
- elsif @user.account.silenced?
|
||||
%span.warning-hint= t('user_mailer.warning.explanation.silence')
|
||||
- elsif !@user.confirmed?
|
||||
%span.warning-hint= t('auth.status.confirming')
|
||||
- elsif !@user.approved?
|
||||
%span.warning-hint= t('auth.status.pending')
|
||||
- else
|
||||
%span.positive-hint= t('auth.status.functional')
|
||||
.simple_form
|
||||
%p.hint
|
||||
- if @user.account.suspended?
|
||||
%span.negative-hint= t('user_mailer.warning.explanation.suspend')
|
||||
- elsif @user.disabled?
|
||||
%span.negative-hint= t('user_mailer.warning.explanation.disable')
|
||||
- elsif @user.account.silenced?
|
||||
%span.warning-hint= t('user_mailer.warning.explanation.silence')
|
||||
- elsif !@user.confirmed?
|
||||
%span.warning-hint= t('auth.status.confirming')
|
||||
= link_to t('auth.didnt_get_confirmation'), new_user_confirmation_path
|
||||
- elsif !@user.approved?
|
||||
%span.warning-hint= t('auth.status.pending')
|
||||
- elsif @user.account.moved_to_account_id.present?
|
||||
%span.positive-hint= t('auth.status.redirecting_to', acct: @user.account.moved_to_account.acct)
|
||||
= link_to t('migrations.cancel'), settings_migration_path
|
||||
- else
|
||||
%span.positive-hint= t('auth.status.functional')
|
||||
|
||||
%hr.spacer/
|
||||
|
@ -0,0 +1,29 @@
|
||||
- content_for :page_title do
|
||||
= t('settings.aliases')
|
||||
|
||||
= simple_form_for @alias, url: settings_aliases_path do |f|
|
||||
= render 'shared/error_messages', object: @alias
|
||||
|
||||
%p.hint= t('aliases.hint_html')
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
.fields-group
|
||||
= f.input :acct, wrapper: :with_block_label, input_html: { autocapitalize: 'none', autocorrect: 'off' }
|
||||
|
||||
.actions
|
||||
= f.button :button, t('aliases.add_new'), type: :submit, class: 'button'
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
.table-wrapper
|
||||
%table.table.inline-table
|
||||
%thead
|
||||
%tr
|
||||
%th= t('simple_form.labels.account_alias.acct')
|
||||
%th
|
||||
%tbody
|
||||
- @aliases.each do |account_alias|
|
||||
%tr
|
||||
%td= account_alias.acct
|
||||
%td= table_link_to 'trash', t('aliases.remove'), settings_alias_path(account_alias), data: { method: :delete }
|
@ -1,17 +1,85 @@
|
||||
- content_for :page_title do
|
||||
= t('settings.migrate')
|
||||
|
||||
= simple_form_for @migration, as: :migration, url: settings_migration_path, html: { method: :put } do |f|
|
||||
- if @migration.account
|
||||
%p.hint= t('migrations.currently_redirecting')
|
||||
.simple_form
|
||||
- if current_account.moved_to_account.present?
|
||||
.fields-row
|
||||
.fields-row__column.fields-group.fields-row__column-6
|
||||
= render 'application/card', account: current_account.moved_to_account
|
||||
.fields-row__column.fields-group.fields-row__column-6
|
||||
%p.hint
|
||||
%span.positive-hint= t('migrations.redirecting_to', acct: current_account.moved_to_account.acct)
|
||||
|
||||
.fields-group
|
||||
= render partial: 'application/card', locals: { account: @migration.account }
|
||||
%p.hint= t('migrations.cancel_explanation')
|
||||
|
||||
%p.hint= link_to t('migrations.cancel'), cancel_settings_migration_path, data: { method: :post }
|
||||
- else
|
||||
%p.hint
|
||||
%span.positive-hint= t('migrations.not_redirecting')
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
%h3= t 'migrations.proceed_with_move'
|
||||
|
||||
= simple_form_for @migration, url: settings_migration_path do |f|
|
||||
- if on_cooldown?
|
||||
%span.warning-hint= t('migrations.on_cooldown', count: ((@cooldown.cooldown_at - Time.now.utc) / 1.day.seconds).ceil)
|
||||
- else
|
||||
%p.hint= t('migrations.warning.before')
|
||||
|
||||
%ul.hint
|
||||
%li.warning-hint= t('migrations.warning.followers')
|
||||
%li.warning-hint= t('migrations.warning.other_data')
|
||||
%li.warning-hint= t('migrations.warning.backreference_required')
|
||||
%li.warning-hint= t('migrations.warning.cooldown')
|
||||
%li.warning-hint= t('migrations.warning.disabled_account')
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
= render 'shared/error_messages', object: @migration
|
||||
|
||||
.fields-group
|
||||
= f.input :acct, placeholder: t('migrations.acct')
|
||||
.fields-row
|
||||
.fields-row__column.fields-group.fields-row__column-6
|
||||
= f.input :acct, wrapper: :with_block_label, input_html: { autocapitalize: 'none', autocorrect: 'off' }, disabled: on_cooldown?
|
||||
|
||||
.fields-row__column.fields-group.fields-row__column-6
|
||||
- if current_user.encrypted_password.present?
|
||||
= f.input :current_password, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, required: true, disabled: on_cooldown?
|
||||
- else
|
||||
= f.input :current_username, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, required: true, disabled: on_cooldown?
|
||||
|
||||
.actions
|
||||
= f.button :button, t('migrations.proceed'), type: :submit, class: 'negative'
|
||||
= f.button :button, t('migrations.proceed_with_move'), type: :submit, class: 'button button--destructive', disabled: on_cooldown?
|
||||
|
||||
- unless @migrations.empty?
|
||||
%hr.spacer/
|
||||
|
||||
%h3= t 'migrations.past_migrations'
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
.table-wrapper
|
||||
%table.table.inline-table
|
||||
%thead
|
||||
%tr
|
||||
%th= t('migrations.acct')
|
||||
%th= t('migrations.followers_count')
|
||||
%th
|
||||
%tbody
|
||||
- @migrations.each do |migration|
|
||||
%tr
|
||||
%td
|
||||
- if migration.target_account.present?
|
||||
= compact_account_link_to migration.target_account
|
||||
- else
|
||||
= migration.acct
|
||||
|
||||
%td= number_with_delimiter migration.followers_count
|
||||
|
||||
%td
|
||||
%time.time-ago{ datetime: migration.created_at.iso8601, title: l(migration.created_at) }= l(migration.created_at)
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
%h3= t 'migrations.incoming_migrations'
|
||||
%p.muted-hint= t('migrations.incoming_migrations_html', path: settings_aliases_path)
|
||||
|
@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::MoveDistributionWorker
|
||||
include Sidekiq::Worker
|
||||
include Payloadable
|
||||
|
||||
sidekiq_options queue: 'push'
|
||||
|
||||
def perform(migration_id)
|
||||
@migration = AccountMigration.find(migration_id)
|
||||
|
||||
ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url|
|
||||
[signed_payload, @account.id, inbox_url]
|
||||
end
|
||||
|
||||
ActivityPub::DeliveryWorker.push_bulk(Relay.enabled.pluck(:inbox_url)) do |inbox_url|
|
||||
[signed_payload, @account.id, inbox_url]
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def inboxes
|
||||
@inboxes ||= @migration.account.followers.inboxes
|
||||
end
|
||||
|
||||
def signed_payload
|
||||
@signed_payload ||= Oj.dump(serialize_payload(@migration, ActivityPub::MoveSerializer, signer: @account))
|
||||
end
|
||||
end
|
@ -0,0 +1,12 @@
|
||||
class CreateAccountMigrations < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :account_migrations do |t|
|
||||
t.belongs_to :account, foreign_key: { on_delete: :cascade }
|
||||
t.string :acct, null: false, default: ''
|
||||
t.bigint :followers_count, null: false, default: 0
|
||||
t.belongs_to :target_account, foreign_key: { to_table: :accounts, on_delete: :nullify }
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,11 @@
|
||||
class CreateAccountAliases < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :account_aliases do |t|
|
||||
t.belongs_to :account, foreign_key: { on_delete: :cascade }
|
||||
t.string :acct, null: false, default: ''
|
||||
t.string :uri, null: false, default: ''
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,5 @@
|
||||
Fabricator(:account_alias) do
|
||||
account
|
||||
acct 'test@example.com'
|
||||
uri 'https://example.com/users/test'
|
||||
end
|
@ -0,0 +1,6 @@
|
||||
Fabricator(:account_migration) do
|
||||
account
|
||||
target_account
|
||||
followers_count 1234
|
||||
acct 'test@example.com'
|
||||
end
|
@ -0,0 +1,5 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AccountAlias, type: :model do
|
||||
|
||||
end
|
@ -0,0 +1,5 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AccountMigration, type: :model do
|
||||
|
||||
end
|
Loading…
Reference in New Issue