forked from mirrors/catstodon
Add account migration UI (#11846)
Fix #10736 - Change data export to be available for non-functional accounts - Change non-functional accounts to include redirecting accounts
This commit is contained in:
parent
b6df9c1067
commit
3ed94dcc1a
31 changed files with 542 additions and 73 deletions
|
@ -5,7 +5,10 @@ module ExportControllerConcern
|
|||
|
||||
included do
|
||||
before_action :authenticate_user!
|
||||
before_action :require_not_suspended!
|
||||
before_action :load_export
|
||||
|
||||
skip_before_action :require_functional!
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -27,4 +30,8 @@ module ExportControllerConcern
|
|||
def export_filename
|
||||
"#{controller_name}.csv"
|
||||
end
|
||||
|
||||
def require_not_suspended!
|
||||
forbidden if current_account.suspended?
|
||||
end
|
||||
end
|
||||
|
|
42
app/controllers/settings/aliases_controller.rb
Normal file
42
app/controllers/settings/aliases_controller.rb
Normal file
|
@ -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
|
|
@ -6,6 +6,9 @@ class Settings::ExportsController < Settings::BaseController
|
|||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :require_not_suspended!
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def show
|
||||
@export = Export.new(current_account)
|
||||
|
@ -34,4 +37,8 @@ class Settings::ExportsController < Settings::BaseController
|
|||
def lock_options
|
||||
{ redis: Redis.current, key: "backup:#{current_user.id}" }
|
||||
end
|
||||
|
||||
def require_not_suspended!
|
||||
forbidden if current_account.suspended?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,31 +4,59 @@ class Settings::MigrationsController < Settings::BaseController
|
|||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :require_not_suspended!
|
||||
before_action :set_migrations
|
||||
before_action :set_cooldown
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def show
|
||||
@migration = Form::Migration.new(account: current_account.moved_to_account)
|
||||
@migration = current_account.migrations.build
|
||||
end
|
||||
|
||||
def update
|
||||
@migration = Form::Migration.new(resource_params)
|
||||
def create
|
||||
@migration = current_account.migrations.build(resource_params)
|
||||
|
||||
if @migration.valid? && migration_account_changed?
|
||||
current_account.update!(moved_to_account: @migration.account)
|
||||
if @migration.save_with_challenge(current_user)
|
||||
current_account.update!(moved_to_account: @migration.target_account)
|
||||
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
|
||||
redirect_to settings_migration_path, notice: I18n.t('migrations.updated_msg')
|
||||
ActivityPub::MoveDistributionWorker.perform_async(@migration.id)
|
||||
redirect_to settings_migration_path, notice: I18n.t('migrations.moved_msg', acct: current_account.moved_to_account.acct)
|
||||
else
|
||||
render :show
|
||||
end
|
||||
end
|
||||
|
||||
def cancel
|
||||
if current_account.moved_to_account_id.present?
|
||||
current_account.update!(moved_to_account: nil)
|
||||
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
|
||||
end
|
||||
|
||||
redirect_to settings_migration_path, notice: I18n.t('migrations.cancelled_msg')
|
||||
end
|
||||
|
||||
helper_method :on_cooldown?
|
||||
|
||||
private
|
||||
|
||||
def resource_params
|
||||
params.require(:migration).permit(:acct)
|
||||
params.require(:account_migration).permit(:acct, :current_password, :current_username)
|
||||
end
|
||||
|
||||
def migration_account_changed?
|
||||
current_account.moved_to_account_id != @migration.account&.id &&
|
||||
current_account.id != @migration.account&.id
|
||||
def set_migrations
|
||||
@migrations = current_account.migrations.includes(:target_account).order(id: :desc).reject(&:new_record?)
|
||||
end
|
||||
|
||||
def set_cooldown
|
||||
@cooldown = current_account.migrations.within_cooldown.first
|
||||
end
|
||||
|
||||
def on_cooldown?
|
||||
@cooldown.present?
|
||||
end
|
||||
|
||||
def require_not_suspended!
|
||||
forbidden if current_account.suspended?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -87,4 +87,12 @@ module SettingsHelper
|
|||
'desktop'
|
||||
end
|
||||
end
|
||||
|
||||
def compact_account_link_to(account)
|
||||
return if account.nil?
|
||||
|
||||
link_to ActivityPub::TagManager.instance.url_for(account), class: 'name-tag', title: account.acct do
|
||||
safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
41
app/models/account_alias.rb
Normal file
41
app/models/account_alias.rb
Normal file
|
@ -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
|
74
app/models/account_migration.rb
Normal file
74
app/models/account_migration.rb
Normal file
|
@ -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
|
|
@ -52,6 +52,8 @@ module AccountAssociations
|
|||
|
||||
# Account migrations
|
||||
belongs_to :moved_to_account, class_name: 'Account', optional: true
|
||||
has_many :migrations, class_name: 'AccountMigration', dependent: :destroy, inverse_of: :account
|
||||
has_many :aliases, class_name: 'AccountAlias', dependent: :destroy, inverse_of: :account
|
||||
|
||||
# Hashtags
|
||||
has_and_belongs_to_many :tags
|
||||
|
|
|
@ -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
|
|
@ -49,7 +49,7 @@ class RemoteFollow
|
|||
end
|
||||
|
||||
def fetch_template!
|
||||
return missing_resource if acct.blank?
|
||||
return missing_resource_error if acct.blank?
|
||||
|
||||
_, domain = acct.split('@')
|
||||
|
||||
|
|
|
@ -168,7 +168,7 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
def functional?
|
||||
confirmed? && approved? && !disabled? && !account.suspended?
|
||||
confirmed? && approved? && !disabled? && !account.suspended? && account.moved_to_account_id.nil?
|
||||
end
|
||||
|
||||
def unconfirmed_or_pending?
|
||||
|
|
26
app/serializers/activitypub/move_serializer.rb
Normal file
26
app/serializers/activitypub/move_serializer.rb
Normal file
|
@ -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/
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
.fields-row__column.fields-group.fields-row__column-6
|
||||
= f.input :email, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, required: true, disabled: current_account.suspended?
|
||||
.fields-row__column.fields-group.fields-row__column-6
|
||||
= f.input :current_password, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }, required: true, disabled: current_account.suspended?
|
||||
= f.input :current_password, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }, required: true, disabled: current_account.suspended?, hint: false
|
||||
|
||||
.fields-row
|
||||
.fields-row__column.fields-group.fields-row__column-6
|
||||
|
|
29
app/views/settings/aliases/index.html.haml
Normal file
29
app/views/settings/aliases/index.html.haml
Normal file
|
@ -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 }
|
|
@ -37,12 +37,16 @@
|
|||
%td= number_with_delimiter @export.total_domain_blocks
|
||||
%td= table_link_to 'download', t('exports.csv'), settings_exports_domain_blocks_path(format: :csv)
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
%p.muted-hint= t('exports.archive_takeout.hint_html')
|
||||
|
||||
- if policy(:backup).create?
|
||||
%p= link_to t('exports.archive_takeout.request'), settings_export_path, class: 'button', method: :post
|
||||
|
||||
- unless @backups.empty?
|
||||
%hr.spacer/
|
||||
|
||||
.table-wrapper
|
||||
%table.table
|
||||
%thead
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -60,6 +60,11 @@
|
|||
%h6= t('auth.migrate_account')
|
||||
%p.muted-hint= t('auth.migrate_account_html', path: settings_migration_path)
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
%h6= t 'migrations.incoming_migrations'
|
||||
%p.muted-hint= t('migrations.incoming_migrations_html', path: settings_aliases_path)
|
||||
|
||||
- if open_deletion?
|
||||
%hr.spacer/
|
||||
|
||||
|
|
32
app/workers/activitypub/move_distribution_worker.rb
Normal file
32
app/workers/activitypub/move_distribution_worker.rb
Normal file
|
@ -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
|
|
@ -554,6 +554,12 @@ en:
|
|||
new_trending_tag:
|
||||
body: 'The hashtag #%{name} is trending today, but has not been previously reviewed. It will not be displayed publicly unless you allow it to, or just save the form as it is to never hear about it again.'
|
||||
subject: New hashtag up for review on %{instance} (#%{name})
|
||||
aliases:
|
||||
add_new: Create alias
|
||||
created_msg: Successfully created a new alias. You can now initiate the move from the old account.
|
||||
deleted_msg: Successfully remove the alias. Moving from that account to this one will no longer be possible.
|
||||
hint_html: If you want to move from another account to this one, here you can create an alias, which is required before you can proceed with moving followers from the old account to this one. This action by itself is <strong>harmless and reversible</strong>. <strong>The account migration is initiated from the old account</strong>.
|
||||
remove: Unlink alias
|
||||
appearance:
|
||||
advanced_web_interface: Advanced web interface
|
||||
advanced_web_interface_hint: 'If you want to make use of your entire screen width, the advanced web interface allows you to configure many different columns to see as much information at the same time as you want: Home, notifications, federated timeline, any number of lists and hashtags.'
|
||||
|
@ -613,6 +619,7 @@ en:
|
|||
confirming: Waiting for e-mail confirmation to be completed.
|
||||
functional: Your account is fully operational.
|
||||
pending: Your application is pending review by our staff. This may take some time. You will receive an e-mail if your application is approved.
|
||||
redirecting_to: Your account is inactive because it is currently redirecting to %{acct}.
|
||||
trouble_logging_in: Trouble logging in?
|
||||
authorize_follow:
|
||||
already_following: You are already following this account
|
||||
|
@ -801,10 +808,32 @@ en:
|
|||
images_and_video: Cannot attach a video to a status that already contains images
|
||||
too_many: Cannot attach more than 4 files
|
||||
migrations:
|
||||
acct: username@domain of the new account
|
||||
currently_redirecting: 'Your profile is set to redirect to:'
|
||||
proceed: Save
|
||||
updated_msg: Your account migration setting successfully updated!
|
||||
acct: Moved to
|
||||
cancel: Cancel redirect
|
||||
cancel_explanation: Cancelling the redirect will re-activate your current account, but will not bring back followers that have been moved to that account.
|
||||
cancelled_msg: Successfully cancelled the redirect.
|
||||
errors:
|
||||
already_moved: is the same account you have already moved to
|
||||
missing_also_known_as: is not back-referencing this account
|
||||
move_to_self: cannot be current account
|
||||
not_found: could not be found
|
||||
on_cooldown: You are on cooldown
|
||||
followers_count: Followers at time of move
|
||||
incoming_migrations: Moving from a different account
|
||||
incoming_migrations_html: To move from another account to this one, first you need to <a href="%{path}">create an account alias</a>.
|
||||
moved_msg: Your account is now redirecting to %{acct} and your followers are being moved over.
|
||||
not_redirecting: Your account is not redirecting to any other account currently.
|
||||
on_cooldown: You have recently migrated your account. This function will become available again in %{count} days.
|
||||
past_migrations: Past migrations
|
||||
proceed_with_move: Move followers
|
||||
redirecting_to: Your account is redirecting to %{acct}.
|
||||
warning:
|
||||
backreference_required: The new account must first be configured to back-reference this one
|
||||
before: 'Before proceeding, please read these notes carefully:'
|
||||
cooldown: After moving there is a cooldown period during which you will not be able to move again
|
||||
disabled_account: Your current account will not be fully usable afterwards. However, you will have access to data export as well as re-activation.
|
||||
followers: This action will move all followers from the current account to the new account
|
||||
other_data: No other data will be moved automatically
|
||||
moderation:
|
||||
title: Moderation
|
||||
notification_mailer:
|
||||
|
@ -950,6 +979,7 @@ en:
|
|||
settings:
|
||||
account: Account
|
||||
account_settings: Account settings
|
||||
aliases: Account aliases
|
||||
appearance: Appearance
|
||||
authorized_apps: Authorized apps
|
||||
back: Back to Mastodon
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
en:
|
||||
simple_form:
|
||||
hints:
|
||||
account_alias:
|
||||
acct: Specify the username@domain of the account you want to move from
|
||||
account_migration:
|
||||
acct: Specify the username@domain of the account you want to move to
|
||||
account_warning_preset:
|
||||
text: You can use toot syntax, such as URLs, hashtags and mentions
|
||||
admin_account_action:
|
||||
|
@ -15,6 +19,8 @@ en:
|
|||
avatar: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px
|
||||
bot: This account mainly performs automated actions and might not be monitored
|
||||
context: One or multiple contexts where the filter should apply
|
||||
current_password: For security purposes please enter the password of the current account
|
||||
current_username: To confirm, please enter the username of the current account
|
||||
digest: Only sent after a long period of inactivity and only if you have received any personal messages in your absence
|
||||
discoverable: The profile directory is another way by which your account can reach a wider audience
|
||||
email: You will be sent a confirmation e-mail
|
||||
|
@ -60,6 +66,10 @@ en:
|
|||
fields:
|
||||
name: Label
|
||||
value: Content
|
||||
account_alias:
|
||||
acct: Handle of the old account
|
||||
account_migration:
|
||||
acct: Handle of the new account
|
||||
account_warning_preset:
|
||||
text: Preset text
|
||||
admin_account_action:
|
||||
|
|
|
@ -5,7 +5,7 @@ SimpleNavigation::Configuration.run do |navigation|
|
|||
n.item :web, safe_join([fa_icon('chevron-left fw'), t('settings.back')]), root_url
|
||||
|
||||
n.item :profile, safe_join([fa_icon('user fw'), t('settings.profile')]), settings_profile_url, if: -> { current_user.functional? } do |s|
|
||||
s.item :profile, safe_join([fa_icon('pencil fw'), t('settings.appearance')]), settings_profile_url, highlights_on: %r{/settings/profile|/settings/migration}
|
||||
s.item :profile, safe_join([fa_icon('pencil fw'), t('settings.appearance')]), settings_profile_url
|
||||
s.item :featured_tags, safe_join([fa_icon('hashtag fw'), t('settings.featured_tags')]), settings_featured_tags_url
|
||||
s.item :identity_proofs, safe_join([fa_icon('key fw'), t('settings.identity_proofs')]), settings_identity_proofs_path, highlights_on: %r{/settings/identity_proofs*}, if: proc { current_account.identity_proofs.exists? }
|
||||
end
|
||||
|
@ -20,13 +20,13 @@ SimpleNavigation::Configuration.run do |navigation|
|
|||
n.item :filters, safe_join([fa_icon('filter fw'), t('filters.index.title')]), filters_path, highlights_on: %r{/filters}, if: -> { current_user.functional? }
|
||||
|
||||
n.item :security, safe_join([fa_icon('lock fw'), t('settings.account')]), edit_user_registration_url do |s|
|
||||
s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete}
|
||||
s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete|/settings/migration|/settings/aliases}
|
||||
s.item :two_factor_authentication, safe_join([fa_icon('mobile fw'), t('settings.two_factor_authentication')]), settings_two_factor_authentication_url, highlights_on: %r{/settings/two_factor_authentication}
|
||||
s.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url
|
||||
end
|
||||
|
||||
n.item :data, safe_join([fa_icon('cloud-download fw'), t('settings.import_and_export')]), settings_export_url, if: -> { current_user.functional? } do |s|
|
||||
s.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url
|
||||
n.item :data, safe_join([fa_icon('cloud-download fw'), t('settings.import_and_export')]), settings_export_url do |s|
|
||||
s.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url, if: -> { current_user.functional? }
|
||||
s.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url
|
||||
end
|
||||
|
||||
|
|
|
@ -134,8 +134,14 @@ Rails.application.routes.draw do
|
|||
end
|
||||
|
||||
resource :delete, only: [:show, :destroy]
|
||||
resource :migration, only: [:show, :update]
|
||||
|
||||
resource :migration, only: [:show, :create] do
|
||||
collection do
|
||||
post :cancel
|
||||
end
|
||||
end
|
||||
|
||||
resources :aliases, only: [:index, :create, :destroy]
|
||||
resources :sessions, only: [:destroy]
|
||||
resources :featured_tags, only: [:index, :create, :destroy]
|
||||
end
|
||||
|
|
12
db/migrate/20190914202517_create_account_migrations.rb
Normal file
12
db/migrate/20190914202517_create_account_migrations.rb
Normal file
|
@ -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
|
11
db/migrate/20190915194355_create_account_aliases.rb
Normal file
11
db/migrate/20190915194355_create_account_aliases.rb
Normal file
|
@ -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
|
23
db/schema.rb
23
db/schema.rb
|
@ -15,6 +15,15 @@ ActiveRecord::Schema.define(version: 2019_09_17_213523) do
|
|||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
create_table "account_aliases", force: :cascade do |t|
|
||||
t.bigint "account_id"
|
||||
t.string "acct", default: "", null: false
|
||||
t.string "uri", default: "", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["account_id"], name: "index_account_aliases_on_account_id"
|
||||
end
|
||||
|
||||
create_table "account_conversations", force: :cascade do |t|
|
||||
t.bigint "account_id"
|
||||
t.bigint "conversation_id"
|
||||
|
@ -49,6 +58,17 @@ ActiveRecord::Schema.define(version: 2019_09_17_213523) do
|
|||
t.index ["account_id"], name: "index_account_identity_proofs_on_account_id"
|
||||
end
|
||||
|
||||
create_table "account_migrations", force: :cascade do |t|
|
||||
t.bigint "account_id"
|
||||
t.string "acct", default: "", null: false
|
||||
t.bigint "followers_count", default: 0, null: false
|
||||
t.bigint "target_account_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["account_id"], name: "index_account_migrations_on_account_id"
|
||||
t.index ["target_account_id"], name: "index_account_migrations_on_target_account_id"
|
||||
end
|
||||
|
||||
create_table "account_moderation_notes", force: :cascade do |t|
|
||||
t.text "content", null: false
|
||||
t.bigint "account_id", null: false
|
||||
|
@ -768,10 +788,13 @@ ActiveRecord::Schema.define(version: 2019_09_17_213523) do
|
|||
t.index ["user_id"], name: "index_web_settings_on_user_id", unique: true
|
||||
end
|
||||
|
||||
add_foreign_key "account_aliases", "accounts", on_delete: :cascade
|
||||
add_foreign_key "account_conversations", "accounts", on_delete: :cascade
|
||||
add_foreign_key "account_conversations", "conversations", on_delete: :cascade
|
||||
add_foreign_key "account_domain_blocks", "accounts", name: "fk_206c6029bd", on_delete: :cascade
|
||||
add_foreign_key "account_identity_proofs", "accounts", on_delete: :cascade
|
||||
add_foreign_key "account_migrations", "accounts", column: "target_account_id", on_delete: :nullify
|
||||
add_foreign_key "account_migrations", "accounts", on_delete: :cascade
|
||||
add_foreign_key "account_moderation_notes", "accounts"
|
||||
add_foreign_key "account_moderation_notes", "accounts", column: "target_account_id"
|
||||
add_foreign_key "account_pins", "accounts", column: "target_account_id", on_delete: :cascade
|
||||
|
|
|
@ -21,6 +21,7 @@ describe Settings::MigrationsController do
|
|||
|
||||
let(:user) { Fabricate(:user, account: account) }
|
||||
let(:account) { Fabricate(:account, moved_to_account: moved_to_account) }
|
||||
|
||||
before { sign_in user, scope: :user }
|
||||
|
||||
context 'when user does not have moved to account' do
|
||||
|
@ -32,7 +33,7 @@ describe Settings::MigrationsController do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when user does not have moved to account' do
|
||||
context 'when user has a moved to account' do
|
||||
let(:moved_to_account) { Fabricate(:account) }
|
||||
|
||||
it 'renders show page' do
|
||||
|
@ -43,21 +44,22 @@ describe Settings::MigrationsController do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'PUT #update' do
|
||||
describe 'POST #create' do
|
||||
context 'when user is not sign in' do
|
||||
subject { put :update }
|
||||
subject { post :create }
|
||||
|
||||
it_behaves_like 'authenticate user'
|
||||
end
|
||||
|
||||
context 'when user is sign in' do
|
||||
subject { put :update, params: { migration: { acct: acct } } }
|
||||
subject { post :create, params: { account_migration: { acct: acct, current_password: '12345678' } } }
|
||||
|
||||
let(:user) { Fabricate(:user, password: '12345678') }
|
||||
|
||||
let(:user) { Fabricate(:user) }
|
||||
before { sign_in user, scope: :user }
|
||||
|
||||
context 'when migration account is changed' do
|
||||
let(:acct) { Fabricate(:account) }
|
||||
let(:acct) { Fabricate(:account, also_known_as: [ActivityPub::TagManager.instance.uri_for(user.account)]) }
|
||||
|
||||
it 'updates moved to account' do
|
||||
is_expected.to redirect_to settings_migration_path
|
||||
|
|
5
spec/fabricators/account_alias_fabricator.rb
Normal file
5
spec/fabricators/account_alias_fabricator.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
Fabricator(:account_alias) do
|
||||
account
|
||||
acct 'test@example.com'
|
||||
uri 'https://example.com/users/test'
|
||||
end
|
6
spec/fabricators/account_migration_fabricator.rb
Normal file
6
spec/fabricators/account_migration_fabricator.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
Fabricator(:account_migration) do
|
||||
account
|
||||
target_account
|
||||
followers_count 1234
|
||||
acct 'test@example.com'
|
||||
end
|
5
spec/models/account_alias_spec.rb
Normal file
5
spec/models/account_alias_spec.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AccountAlias, type: :model do
|
||||
|
||||
end
|
5
spec/models/account_migration_spec.rb
Normal file
5
spec/models/account_migration_spec.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AccountMigration, type: :model do
|
||||
|
||||
end
|
Loading…
Reference in a new issue