Skip to content

Commit

Permalink
Add account migration UI (mastodon#11846)
Browse files Browse the repository at this point in the history
Fix mastodon#10736

- Change data export to be available for non-functional accounts
- Change non-functional accounts to include redirecting accounts
  • Loading branch information
Gargron authored and hiyuki2578 committed Oct 2, 2019
1 parent aa1ac2d commit ac3fd84
Show file tree
Hide file tree
Showing 31 changed files with 542 additions and 73 deletions.
7 changes: 7 additions & 0 deletions app/controllers/concerns/export_controller_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,4 +30,8 @@ def export_data
def export_filename
"#{controller_name}.csv"
end

def require_not_suspended!
forbidden if current_account.suspended?
end
end
42 changes: 42 additions & 0 deletions app/controllers/settings/aliases_controller.rb
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions app/controllers/settings/exports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -34,4 +37,8 @@ def create
def lock_options
{ redis: Redis.current, key: "backup:#{current_user.id}" }
end

def require_not_suspended!
forbidden if current_account.suspended?
end
end
48 changes: 38 additions & 10 deletions app/controllers/settings/migrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 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 migration_account_changed?
current_account.moved_to_account_id != @migration.account&.id &&
current_account.id != @migration.account&.id
def require_not_suspended!
forbidden if current_account.suspended?
end
end
8 changes: 8 additions & 0 deletions app/helpers/settings_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,12 @@ def session_device_icon(session)
'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 changes: 41 additions & 0 deletions app/models/account_alias.rb
Original file line number Diff line number Diff line change
@@ -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 changes: 74 additions & 0 deletions app/models/account_migration.rb
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions app/models/concerns/account_associations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 0 additions & 25 deletions app/models/form/migration.rb

This file was deleted.

2 changes: 1 addition & 1 deletion app/models/remote_follow.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def normalize_acct(value)
end

def fetch_template!
return missing_resource if acct.blank?
return missing_resource_error if acct.blank?

_, domain = acct.split('@')

Expand Down
2 changes: 1 addition & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def active_for_authentication?
end

def functional?
confirmed? && approved? && !disabled? && !account.suspended?
confirmed? && approved? && !disabled? && !account.suspended? && account.moved_to_account_id.nil?
end

def unconfirmed_or_pending?
Expand Down
26 changes: 26 additions & 0 deletions app/serializers/activitypub/move_serializer.rb
Original file line number Diff line number Diff line change
@@ -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
30 changes: 18 additions & 12 deletions app/views/auth/registrations/_status.html.haml
Original file line number Diff line number Diff line change
@@ -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/
Loading

0 comments on commit ac3fd84

Please sign in to comment.