Fix #10736 - Change data export to be available for non-functional accounts - Change non-functional accounts to include redirecting accountsmaster
@ -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') | %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/ | %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 | - content_for :page_title do | ||||
= t('settings.migrate') | = 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 | = 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 | .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 |