summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--app/controllers/concerns/export_controller_concern.rb7
-rw-r--r--app/controllers/settings/aliases_controller.rb42
-rw-r--r--app/controllers/settings/exports_controller.rb7
-rw-r--r--app/controllers/settings/migrations_controller.rb48
-rw-r--r--app/helpers/settings_helper.rb8
-rw-r--r--app/models/account_alias.rb41
-rw-r--r--app/models/account_migration.rb74
-rw-r--r--app/models/concerns/account_associations.rb2
-rw-r--r--app/models/form/migration.rb25
-rw-r--r--app/models/remote_follow.rb2
-rw-r--r--app/models/user.rb2
-rw-r--r--app/serializers/activitypub/move_serializer.rb26
-rw-r--r--app/views/auth/registrations/_status.html.haml30
-rw-r--r--app/views/auth/registrations/edit.html.haml2
-rw-r--r--app/views/settings/aliases/index.html.haml29
-rw-r--r--app/views/settings/exports/show.html.haml4
-rw-r--r--app/views/settings/migrations/show.html.haml84
-rw-r--r--app/views/settings/profiles/show.html.haml5
-rw-r--r--app/workers/activitypub/move_distribution_worker.rb32
-rw-r--r--config/locales/en.yml38
-rw-r--r--config/locales/simple_form.en.yml10
-rw-r--r--config/navigation.rb8
-rw-r--r--config/routes.rb8
-rw-r--r--db/migrate/20190914202517_create_account_migrations.rb12
-rw-r--r--db/migrate/20190915194355_create_account_aliases.rb11
-rw-r--r--db/schema.rb23
-rw-r--r--spec/controllers/settings/migrations_controller_spec.rb14
-rw-r--r--spec/fabricators/account_alias_fabricator.rb5
-rw-r--r--spec/fabricators/account_migration_fabricator.rb6
-rw-r--r--spec/models/account_alias_spec.rb5
-rw-r--r--spec/models/account_migration_spec.rb5
31 files changed, 542 insertions, 73 deletions
diff --git a/app/controllers/concerns/export_controller_concern.rb b/app/controllers/concerns/export_controller_concern.rb
index e20b71a3039..bfe990c827d 100644
--- a/app/controllers/concerns/export_controller_concern.rb
+++ b/app/controllers/concerns/export_controller_concern.rb
@@ -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
diff --git a/app/controllers/settings/aliases_controller.rb b/app/controllers/settings/aliases_controller.rb
new file mode 100644
index 00000000000..2b675f065a5
--- /dev/null
+++ b/app/controllers/settings/aliases_controller.rb
@@ -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
diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb
index 3012fbf7756..0e93d07a9b2 100644
--- a/app/controllers/settings/exports_controller.rb
+++ b/app/controllers/settings/exports_controller.rb
@@ -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
diff --git a/app/controllers/settings/migrations_controller.rb b/app/controllers/settings/migrations_controller.rb
index 59eb48779c1..90092c69256 100644
--- a/app/controllers/settings/migrations_controller.rb
+++ b/app/controllers/settings/migrations_controller.rb
@@ -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
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index 2b3fd1263a8..ecc73baf579 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -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
diff --git a/app/models/account_alias.rb b/app/models/account_alias.rb
new file mode 100644
index 00000000000..e9a0dd79e03
--- /dev/null
+++ b/app/models/account_alias.rb
@@ -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
diff --git a/app/models/account_migration.rb b/app/models/account_migration.rb
new file mode 100644
index 00000000000..15830bffbd0
--- /dev/null
+++ b/app/models/account_migration.rb
@@ -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
diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb
index 1db7771c721..c9cc5c610b8 100644
--- a/app/models/concerns/account_associations.rb
+++ b/app/models/concerns/account_associations.rb
@@ -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
diff --git a/app/models/form/migration.rb b/app/models/form/migration.rb
deleted file mode 100644
index c2a8655e132..00000000000
--- a/app/models/form/migration.rb
+++ /dev/null
@@ -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
diff --git a/app/models/remote_follow.rb b/app/models/remote_follow.rb
index 52dd3f67bac..5ea53528727 100644
--- a/app/models/remote_follow.rb
+++ b/app/models/remote_follow.rb
@@ -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('@')
diff --git a/app/models/user.rb b/app/models/user.rb
index b48455802d4..9a19a53b32d 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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?
diff --git a/app/serializers/activitypub/move_serializer.rb b/app/serializers/activitypub/move_serializer.rb
new file mode 100644
index 00000000000..5675875fa52
--- /dev/null
+++ b/app/serializers/activitypub/move_serializer.rb
@@ -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
diff --git a/app/views/auth/registrations/_status.html.haml b/app/views/auth/registrations/_status.html.haml
index b38a83d67de..47112dae07c 100644
--- a/app/views/auth/registrations/_status.html.haml
+++ b/app/views/auth/registrations/_status.html.haml
@@ -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/
diff --git a/app/views/auth/registrations/edit.html.haml b/app/views/auth/registrations/edit.html.haml
index 710ee5c6895..885171c58b9 100644
--- a/app/views/auth/registrations/edit.html.haml
+++ b/app/views/auth/registrations/edit.html.haml
@@ -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
diff --git a/app/views/settings/aliases/index.html.haml b/app/views/settings/aliases/index.html.haml
new file mode 100644
index 00000000000..5b698636815
--- /dev/null
+++ b/app/views/settings/aliases/index.html.haml
@@ -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 }
diff --git a/app/views/settings/exports/show.html.haml b/app/views/settings/exports/show.html.haml
index b13cea976a1..76ff76bd9c6 100644
--- a/app/views/settings/exports/show.html.haml
+++ b/app/views/settings/exports/show.html.haml
@@ -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/
+