Merge branch 'main' into glitch-soc/merge-upstream
Conflicts: - `app/views/admin/pending_accounts/index.html.haml`: Removed upstream, while it had glitch-soc-specific changes to accomodate for glitch-soc's theming system. Removed the file. Additional changes: - `app/views/admin/accounts/index.html.haml': Accomodate for glitch-soc's theming system.
This commit is contained in:
		
						commit
						179f1d3a78
					
				
					 41 changed files with 1046 additions and 773 deletions
				
			
		| 
						 | 
				
			
			@ -15,6 +15,7 @@ vendor/bundle
 | 
			
		|||
*.swp
 | 
			
		||||
*~
 | 
			
		||||
postgres
 | 
			
		||||
postgres14
 | 
			
		||||
redis
 | 
			
		||||
elasticsearch
 | 
			
		||||
chart
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -40,6 +40,7 @@
 | 
			
		|||
 | 
			
		||||
# Ignore postgres + redis + elasticsearch volume optionally created by docker-compose
 | 
			
		||||
/postgres
 | 
			
		||||
/postgres14
 | 
			
		||||
/redis
 | 
			
		||||
/elasticsearch
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,13 +2,24 @@
 | 
			
		|||
 | 
			
		||||
module Admin
 | 
			
		||||
  class AccountsController < BaseController
 | 
			
		||||
    before_action :set_account, except: [:index]
 | 
			
		||||
    before_action :set_account, except: [:index, :batch]
 | 
			
		||||
    before_action :require_remote_account!, only: [:redownload]
 | 
			
		||||
    before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
 | 
			
		||||
 | 
			
		||||
    def index
 | 
			
		||||
      authorize :account, :index?
 | 
			
		||||
 | 
			
		||||
      @accounts = filtered_accounts.page(params[:page])
 | 
			
		||||
      @form     = Form::AccountBatch.new
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def batch
 | 
			
		||||
      @form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
 | 
			
		||||
      @form.save
 | 
			
		||||
    rescue ActionController::ParameterMissing
 | 
			
		||||
      flash[:alert] = I18n.t('admin.accounts.no_account_selected')
 | 
			
		||||
    ensure
 | 
			
		||||
      redirect_to admin_accounts_path(filter_params)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def show
 | 
			
		||||
| 
						 | 
				
			
			@ -38,13 +49,13 @@ module Admin
 | 
			
		|||
    def approve
 | 
			
		||||
      authorize @account.user, :approve?
 | 
			
		||||
      @account.user.approve!
 | 
			
		||||
      redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.approved_msg', username: @account.acct)
 | 
			
		||||
      redirect_to admin_accounts_path(status: 'pending'), notice: I18n.t('admin.accounts.approved_msg', username: @account.acct)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def reject
 | 
			
		||||
      authorize @account.user, :reject?
 | 
			
		||||
      DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
 | 
			
		||||
      redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct)
 | 
			
		||||
      redirect_to admin_accounts_path(status: 'pending'), notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def destroy
 | 
			
		||||
| 
						 | 
				
			
			@ -121,11 +132,25 @@ module Admin
 | 
			
		|||
    end
 | 
			
		||||
 | 
			
		||||
    def filtered_accounts
 | 
			
		||||
      AccountFilter.new(filter_params).results
 | 
			
		||||
      AccountFilter.new(filter_params.with_defaults(order: 'recent')).results
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def filter_params
 | 
			
		||||
      params.slice(*AccountFilter::KEYS).permit(*AccountFilter::KEYS)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def form_account_batch_params
 | 
			
		||||
      params.require(:form_account_batch).permit(:action, account_ids: [])
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def action_from_button
 | 
			
		||||
      if params[:suspend]
 | 
			
		||||
        'suspend'
 | 
			
		||||
      elsif params[:approve]
 | 
			
		||||
        'approve'
 | 
			
		||||
      elsif params[:reject]
 | 
			
		||||
        'reject'
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,52 +0,0 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
module Admin
 | 
			
		||||
  class PendingAccountsController < BaseController
 | 
			
		||||
    before_action :set_accounts, only: :index
 | 
			
		||||
 | 
			
		||||
    def index
 | 
			
		||||
      @form = Form::AccountBatch.new
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def batch
 | 
			
		||||
      @form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
 | 
			
		||||
      @form.save
 | 
			
		||||
    rescue ActionController::ParameterMissing
 | 
			
		||||
      flash[:alert] = I18n.t('admin.accounts.no_account_selected')
 | 
			
		||||
    ensure
 | 
			
		||||
      redirect_to admin_pending_accounts_path(current_params)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def approve_all
 | 
			
		||||
      Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'approve').save
 | 
			
		||||
      redirect_to admin_pending_accounts_path(current_params)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def reject_all
 | 
			
		||||
      Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'reject').save
 | 
			
		||||
      redirect_to admin_pending_accounts_path(current_params)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    private
 | 
			
		||||
 | 
			
		||||
    def set_accounts
 | 
			
		||||
      @accounts = Account.joins(:user).merge(User.pending.recent).includes(user: :invite_request).page(params[:page])
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def form_account_batch_params
 | 
			
		||||
      params.require(:form_account_batch).permit(:action, account_ids: [])
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def action_from_button
 | 
			
		||||
      if params[:approve]
 | 
			
		||||
        'approve'
 | 
			
		||||
      elsif params[:reject]
 | 
			
		||||
        'reject'
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def current_params
 | 
			
		||||
      params.slice(:page).permit(:page)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
module AccountableConcern
 | 
			
		||||
  extend ActiveSupport::Concern
 | 
			
		||||
 | 
			
		||||
  def log_action(action, target)
 | 
			
		||||
    Admin::ActionLog.create(account: current_account, action: action, target: target)
 | 
			
		||||
  def log_action(action, target, options = {})
 | 
			
		||||
    Admin::ActionLog.create(account: current_account, action: action, target: target, recorded_changes: options.stringify_keys)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -57,7 +57,7 @@ module TwoFactorAuthenticationConcern
 | 
			
		|||
 | 
			
		||||
    if valid_webauthn_credential?(user, webauthn_credential)
 | 
			
		||||
      on_authentication_success(user, :webauthn)
 | 
			
		||||
      render json: { redirect_path: root_path }, status: :ok
 | 
			
		||||
      render json: { redirect_path: after_sign_in_path_for(user) }, status: :ok
 | 
			
		||||
    else
 | 
			
		||||
      on_authentication_failure(user, :webauthn, :invalid_credential)
 | 
			
		||||
      render json: { error: t('webauthn_credentials.invalid_credential') }, status: :unprocessable_entity
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,6 +36,8 @@ module Admin::ActionLogsHelper
 | 
			
		|||
 | 
			
		||||
  def log_target_from_history(type, attributes)
 | 
			
		||||
    case type
 | 
			
		||||
    when 'User'
 | 
			
		||||
      attributes['username']
 | 
			
		||||
    when 'CustomEmoji'
 | 
			
		||||
      attributes['shortcode']
 | 
			
		||||
    when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,41 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
module Admin::DashboardHelper
 | 
			
		||||
  def feature_hint(feature, enabled)
 | 
			
		||||
    indicator   = safe_join([enabled ? t('simple_form.yes') : t('simple_form.no'), fa_icon('power-off fw')], ' ')
 | 
			
		||||
    class_names = enabled ? 'pull-right positive-hint' : 'pull-right neutral-hint'
 | 
			
		||||
  def relevant_account_ip(account, ip_query)
 | 
			
		||||
    default_ip = [account.user_current_sign_in_ip || account.user_sign_up_ip]
 | 
			
		||||
 | 
			
		||||
    safe_join([feature, content_tag(:span, indicator, class: class_names)])
 | 
			
		||||
    matched_ip = begin
 | 
			
		||||
      ip_query_addr = IPAddr.new(ip_query)
 | 
			
		||||
      account.user.recent_ips.find { |(_, ip)| ip_query_addr.include?(ip) } || default_ip
 | 
			
		||||
    rescue IPAddr::Error
 | 
			
		||||
      default_ip
 | 
			
		||||
    end.last
 | 
			
		||||
 | 
			
		||||
    if matched_ip
 | 
			
		||||
      link_to matched_ip, admin_accounts_path(ip: matched_ip)
 | 
			
		||||
    else
 | 
			
		||||
      '-'
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def relevant_account_timestamp(account)
 | 
			
		||||
    timestamp, exact = begin
 | 
			
		||||
      if account.user_current_sign_in_at && account.user_current_sign_in_at < 24.hours.ago
 | 
			
		||||
        [account.user_current_sign_in_at, true]
 | 
			
		||||
      elsif account.user_current_sign_in_at
 | 
			
		||||
        [account.user_current_sign_in_at, false]
 | 
			
		||||
      elsif account.user_pending?
 | 
			
		||||
        [account.user_created_at, true]
 | 
			
		||||
      elsif account.last_status_at.present?
 | 
			
		||||
        [account.last_status_at, true]
 | 
			
		||||
      else
 | 
			
		||||
        [nil, false]
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    return '-' if timestamp.nil?
 | 
			
		||||
    return t('generic.today') unless exact
 | 
			
		||||
 | 
			
		||||
    content_tag(:time, l(timestamp), class: 'time-ago', datetime: timestamp.iso8601, title: l(timestamp))
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,6 +37,7 @@ export const THUMBNAIL_UPLOAD_PROGRESS = 'THUMBNAIL_UPLOAD_PROGRESS';
 | 
			
		|||
export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
 | 
			
		||||
export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
 | 
			
		||||
export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
 | 
			
		||||
export const COMPOSE_SUGGESTION_IGNORE = 'COMPOSE_SUGGESTION_IGNORE';
 | 
			
		||||
export const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE';
 | 
			
		||||
 | 
			
		||||
export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
 | 
			
		||||
| 
						 | 
				
			
			@ -536,13 +537,25 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
 | 
			
		|||
      startPosition = position;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dispatch({
 | 
			
		||||
      type: COMPOSE_SUGGESTION_SELECT,
 | 
			
		||||
      position: startPosition,
 | 
			
		||||
      token,
 | 
			
		||||
      completion,
 | 
			
		||||
      path,
 | 
			
		||||
    });
 | 
			
		||||
    // We don't want to replace hashtags that vary only in case due to accessibility, but we need to fire off an event so that
 | 
			
		||||
    // the suggestions are dismissed and the cursor moves forward.
 | 
			
		||||
    if (suggestion.type !== 'hashtag' || token.slice(1).localeCompare(suggestion.name, undefined, { sensitivity: 'accent' }) !== 0) {
 | 
			
		||||
      dispatch({
 | 
			
		||||
        type: COMPOSE_SUGGESTION_SELECT,
 | 
			
		||||
        position: startPosition,
 | 
			
		||||
        token,
 | 
			
		||||
        completion,
 | 
			
		||||
        path,
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      dispatch({
 | 
			
		||||
        type: COMPOSE_SUGGESTION_IGNORE,
 | 
			
		||||
        position: startPosition,
 | 
			
		||||
        token,
 | 
			
		||||
        completion,
 | 
			
		||||
        path,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,6 +21,7 @@ import {
 | 
			
		|||
  COMPOSE_SUGGESTIONS_CLEAR,
 | 
			
		||||
  COMPOSE_SUGGESTIONS_READY,
 | 
			
		||||
  COMPOSE_SUGGESTION_SELECT,
 | 
			
		||||
  COMPOSE_SUGGESTION_IGNORE,
 | 
			
		||||
  COMPOSE_SUGGESTION_TAGS_UPDATE,
 | 
			
		||||
  COMPOSE_TAG_HISTORY_UPDATE,
 | 
			
		||||
  COMPOSE_SENSITIVITY_CHANGE,
 | 
			
		||||
| 
						 | 
				
			
			@ -165,6 +166,17 @@ const insertSuggestion = (state, position, token, completion, path) => {
 | 
			
		|||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const ignoreSuggestion = (state, position, token, completion, path) => {
 | 
			
		||||
  return state.withMutations(map => {
 | 
			
		||||
    map.updateIn(path, oldText => `${oldText.slice(0, position + token.length)} ${oldText.slice(position + token.length)}`);
 | 
			
		||||
    map.set('suggestion_token', null);
 | 
			
		||||
    map.set('suggestions', ImmutableList());
 | 
			
		||||
    map.set('focusDate', new Date());
 | 
			
		||||
    map.set('caretPosition', position + token.length + 1);
 | 
			
		||||
    map.set('idempotencyKey', uuid());
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const sortHashtagsByUse = (state, tags) => {
 | 
			
		||||
  const personalHistory = state.get('tagHistory');
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -398,6 +410,8 @@ export default function compose(state = initialState, action) {
 | 
			
		|||
    return state.set('suggestions', ImmutableList(normalizeSuggestions(state, action))).set('suggestion_token', action.token);
 | 
			
		||||
  case COMPOSE_SUGGESTION_SELECT:
 | 
			
		||||
    return insertSuggestion(state, action.position, action.token, action.completion, action.path);
 | 
			
		||||
  case COMPOSE_SUGGESTION_IGNORE:
 | 
			
		||||
    return ignoreSuggestion(state, action.position, action.token, action.completion, action.path);
 | 
			
		||||
  case COMPOSE_SUGGESTION_TAGS_UPDATE:
 | 
			
		||||
    return updateSuggestionTags(state, action.token);
 | 
			
		||||
  case COMPOSE_TAG_HISTORY_UPDATE:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -103,7 +103,9 @@ function main() {
 | 
			
		|||
    delegate(document, '#registration_user_password_confirmation,#registration_user_password', 'input', () => {
 | 
			
		||||
      const password = document.getElementById('registration_user_password');
 | 
			
		||||
      const confirmation = document.getElementById('registration_user_password_confirmation');
 | 
			
		||||
      if (password.value && password.value !== confirmation.value) {
 | 
			
		||||
      if (confirmation.value && confirmation.value.length > password.maxLength) {
 | 
			
		||||
        confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.exceeds_maxlength'] || 'Password confirmation exceeds the maximum password length', locale)).format());
 | 
			
		||||
      } else if (password.value && password.value !== confirmation.value) {
 | 
			
		||||
        confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.mismatching'] || 'Password confirmation does not match', locale)).format());
 | 
			
		||||
      } else {
 | 
			
		||||
        confirmation.setCustomValidity('');
 | 
			
		||||
| 
						 | 
				
			
			@ -115,7 +117,9 @@ function main() {
 | 
			
		|||
      const confirmation = document.getElementById('user_password_confirmation');
 | 
			
		||||
      if (!confirmation) return;
 | 
			
		||||
 | 
			
		||||
      if (password.value && password.value !== confirmation.value) {
 | 
			
		||||
      if (confirmation.value && confirmation.value.length > password.maxLength) {
 | 
			
		||||
        confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.exceeds_maxlength'] || 'Password confirmation exceeds the maximum password length', locale)).format());
 | 
			
		||||
      } else if (password.value && password.value !== confirmation.value) {
 | 
			
		||||
        confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.mismatching'] || 'Password confirmation does not match', locale)).format());
 | 
			
		||||
      } else {
 | 
			
		||||
        confirmation.setCustomValidity('');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -326,7 +326,12 @@
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.batch-table__row--muted .pending-account__header {
 | 
			
		||||
.batch-table__row--muted {
 | 
			
		||||
  color: lighten($ui-base-color, 26%);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.batch-table__row--muted .pending-account__header,
 | 
			
		||||
.batch-table__row--muted .accounts-table {
 | 
			
		||||
  &,
 | 
			
		||||
  a,
 | 
			
		||||
  strong {
 | 
			
		||||
| 
						 | 
				
			
			@ -334,10 +339,31 @@
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.batch-table__row--attention .pending-account__header {
 | 
			
		||||
.batch-table__row--muted .accounts-table {
 | 
			
		||||
  tbody td.accounts-table__extra,
 | 
			
		||||
  &__count,
 | 
			
		||||
  &__count small {
 | 
			
		||||
    color: lighten($ui-base-color, 26%);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.batch-table__row--attention {
 | 
			
		||||
  color: $gold-star;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.batch-table__row--attention .pending-account__header,
 | 
			
		||||
.batch-table__row--attention .accounts-table {
 | 
			
		||||
  &,
 | 
			
		||||
  a,
 | 
			
		||||
  strong {
 | 
			
		||||
    color: $gold-star;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.batch-table__row--attention .accounts-table {
 | 
			
		||||
  tbody td.accounts-table__extra,
 | 
			
		||||
  &__count,
 | 
			
		||||
  &__count small {
 | 
			
		||||
    color: $gold-star;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -237,6 +237,11 @@ a.table-action-link {
 | 
			
		|||
        flex: 1 1 auto;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &__quote {
 | 
			
		||||
        padding: 12px;
 | 
			
		||||
        padding-top: 0;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &__extra {
 | 
			
		||||
        flex: 0 0 auto;
 | 
			
		||||
        text-align: right;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -443,6 +443,24 @@
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  tbody td.accounts-table__extra {
 | 
			
		||||
    width: 120px;
 | 
			
		||||
    text-align: right;
 | 
			
		||||
    color: $darker-text-color;
 | 
			
		||||
    padding-right: 16px;
 | 
			
		||||
 | 
			
		||||
    a {
 | 
			
		||||
      text-decoration: none;
 | 
			
		||||
      color: inherit;
 | 
			
		||||
 | 
			
		||||
      &:focus,
 | 
			
		||||
      &:hover,
 | 
			
		||||
      &:active {
 | 
			
		||||
        text-decoration: underline;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__comment {
 | 
			
		||||
    width: 50%;
 | 
			
		||||
    vertical-align: initial !important;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -129,6 +129,8 @@ class Account < ApplicationRecord
 | 
			
		|||
           :unconfirmed_email,
 | 
			
		||||
           :current_sign_in_ip,
 | 
			
		||||
           :current_sign_in_at,
 | 
			
		||||
           :created_at,
 | 
			
		||||
           :sign_up_ip,
 | 
			
		||||
           :confirmed?,
 | 
			
		||||
           :approved?,
 | 
			
		||||
           :pending?,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,18 +2,15 @@
 | 
			
		|||
 | 
			
		||||
class AccountFilter
 | 
			
		||||
  KEYS = %i(
 | 
			
		||||
    local
 | 
			
		||||
    remote
 | 
			
		||||
    by_domain
 | 
			
		||||
    active
 | 
			
		||||
    pending
 | 
			
		||||
    silenced
 | 
			
		||||
    suspended
 | 
			
		||||
    origin
 | 
			
		||||
    status
 | 
			
		||||
    permissions
 | 
			
		||||
    username
 | 
			
		||||
    by_domain
 | 
			
		||||
    display_name
 | 
			
		||||
    email
 | 
			
		||||
    ip
 | 
			
		||||
    staff
 | 
			
		||||
    invited_by
 | 
			
		||||
    order
 | 
			
		||||
  ).freeze
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -21,11 +18,10 @@ class AccountFilter
 | 
			
		|||
 | 
			
		||||
  def initialize(params)
 | 
			
		||||
    @params = params
 | 
			
		||||
    set_defaults!
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def results
 | 
			
		||||
    scope = Account.includes(:user).reorder(nil)
 | 
			
		||||
    scope = Account.includes(:account_stat, user: [:session_activations, :invite_request]).without_instance_actor.reorder(nil)
 | 
			
		||||
 | 
			
		||||
    params.each do |key, value|
 | 
			
		||||
      scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
 | 
			
		||||
| 
						 | 
				
			
			@ -36,30 +32,16 @@ class AccountFilter
 | 
			
		|||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def set_defaults!
 | 
			
		||||
    params['local']  = '1' if params['remote'].blank?
 | 
			
		||||
    params['active'] = '1' if params['suspended'].blank? && params['silenced'].blank? && params['pending'].blank?
 | 
			
		||||
    params['order']  = 'recent' if params['order'].blank?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def scope_for(key, value)
 | 
			
		||||
    case key.to_s
 | 
			
		||||
    when 'local'
 | 
			
		||||
      Account.local.without_instance_actor
 | 
			
		||||
    when 'remote'
 | 
			
		||||
      Account.remote
 | 
			
		||||
    when 'origin'
 | 
			
		||||
      origin_scope(value)
 | 
			
		||||
    when 'permissions'
 | 
			
		||||
      permissions_scope(value)
 | 
			
		||||
    when 'status'
 | 
			
		||||
      status_scope(value)
 | 
			
		||||
    when 'by_domain'
 | 
			
		||||
      Account.where(domain: value)
 | 
			
		||||
    when 'active'
 | 
			
		||||
      Account.without_suspended
 | 
			
		||||
    when 'pending'
 | 
			
		||||
      accounts_with_users.merge(User.pending)
 | 
			
		||||
    when 'disabled'
 | 
			
		||||
      accounts_with_users.merge(User.disabled)
 | 
			
		||||
    when 'silenced'
 | 
			
		||||
      Account.silenced
 | 
			
		||||
    when 'suspended'
 | 
			
		||||
      Account.suspended
 | 
			
		||||
    when 'username'
 | 
			
		||||
      Account.matches_username(value)
 | 
			
		||||
    when 'display_name'
 | 
			
		||||
| 
						 | 
				
			
			@ -68,8 +50,8 @@ class AccountFilter
 | 
			
		|||
      accounts_with_users.merge(User.matches_email(value))
 | 
			
		||||
    when 'ip'
 | 
			
		||||
      valid_ip?(value) ? accounts_with_users.merge(User.matches_ip(value)) : Account.none
 | 
			
		||||
    when 'staff'
 | 
			
		||||
      accounts_with_users.merge(User.staff)
 | 
			
		||||
    when 'invited_by'
 | 
			
		||||
      invited_by_scope(value)
 | 
			
		||||
    when 'order'
 | 
			
		||||
      order_scope(value)
 | 
			
		||||
    else
 | 
			
		||||
| 
						 | 
				
			
			@ -77,21 +59,56 @@ class AccountFilter
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def order_scope(value)
 | 
			
		||||
    case value
 | 
			
		||||
  def origin_scope(value)
 | 
			
		||||
    case value.to_s
 | 
			
		||||
    when 'local'
 | 
			
		||||
      Account.local
 | 
			
		||||
    when 'remote'
 | 
			
		||||
      Account.remote
 | 
			
		||||
    else
 | 
			
		||||
      raise "Unknown origin: #{value}"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def status_scope(value)
 | 
			
		||||
    case value.to_s
 | 
			
		||||
    when 'active'
 | 
			
		||||
      params['remote'] ? Account.joins(:account_stat).by_recent_status : Account.joins(:user).by_recent_sign_in
 | 
			
		||||
      Account.without_suspended
 | 
			
		||||
    when 'pending'
 | 
			
		||||
      accounts_with_users.merge(User.pending)
 | 
			
		||||
    when 'suspended'
 | 
			
		||||
      Account.suspended
 | 
			
		||||
    else
 | 
			
		||||
      raise "Unknown status: #{value}"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def order_scope(value)
 | 
			
		||||
    case value.to_s
 | 
			
		||||
    when 'active'
 | 
			
		||||
      accounts_with_users.left_joins(:account_stat).order(Arel.sql('coalesce(users.current_sign_in_at, account_stats.last_status_at, to_timestamp(0)) desc, accounts.id desc'))
 | 
			
		||||
    when 'recent'
 | 
			
		||||
      Account.recent
 | 
			
		||||
    when 'alphabetic'
 | 
			
		||||
      Account.alphabetic
 | 
			
		||||
    else
 | 
			
		||||
      raise "Unknown order: #{value}"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def invited_by_scope(value)
 | 
			
		||||
    Account.left_joins(user: :invite).merge(Invite.where(user_id: value.to_s))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def permissions_scope(value)
 | 
			
		||||
    case value.to_s
 | 
			
		||||
    when 'staff'
 | 
			
		||||
      accounts_with_users.merge(User.staff)
 | 
			
		||||
    else
 | 
			
		||||
      raise "Unknown permissions: #{value}"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def accounts_with_users
 | 
			
		||||
    Account.joins(:user)
 | 
			
		||||
    Account.left_joins(:user)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def valid_ip?(value)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,7 +17,7 @@ class Admin::ActionLog < ApplicationRecord
 | 
			
		|||
  serialize :recorded_changes
 | 
			
		||||
 | 
			
		||||
  belongs_to :account
 | 
			
		||||
  belongs_to :target, polymorphic: true
 | 
			
		||||
  belongs_to :target, polymorphic: true, optional: true
 | 
			
		||||
 | 
			
		||||
  default_scope -> { order('id desc') }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,6 +11,8 @@ class Admin::ActionLogFilter
 | 
			
		|||
    assigned_to_self_report: { target_type: 'Report', action: 'assigned_to_self' }.freeze,
 | 
			
		||||
    change_email_user: { target_type: 'User', action: 'change_email' }.freeze,
 | 
			
		||||
    confirm_user: { target_type: 'User', action: 'confirm' }.freeze,
 | 
			
		||||
    approve_user: { target_type: 'User', action: 'approve' }.freeze,
 | 
			
		||||
    reject_user: { target_type: 'User', action: 'reject' }.freeze,
 | 
			
		||||
    create_account_warning: { target_type: 'AccountWarning', action: 'create' }.freeze,
 | 
			
		||||
    create_announcement: { target_type: 'Announcement', action: 'create' }.freeze,
 | 
			
		||||
    create_custom_emoji: { target_type: 'CustomEmoji', action: 'create' }.freeze,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@
 | 
			
		|||
class Form::AccountBatch
 | 
			
		||||
  include ActiveModel::Model
 | 
			
		||||
  include Authorization
 | 
			
		||||
  include AccountableConcern
 | 
			
		||||
  include Payloadable
 | 
			
		||||
 | 
			
		||||
  attr_accessor :account_ids, :action, :current_account
 | 
			
		||||
| 
						 | 
				
			
			@ -25,19 +26,21 @@ class Form::AccountBatch
 | 
			
		|||
      suppress_follow_recommendation!
 | 
			
		||||
    when 'unsuppress_follow_recommendation'
 | 
			
		||||
      unsuppress_follow_recommendation!
 | 
			
		||||
    when 'suspend'
 | 
			
		||||
      suspend!
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def follow!
 | 
			
		||||
    accounts.find_each do |target_account|
 | 
			
		||||
    accounts.each do |target_account|
 | 
			
		||||
      FollowService.new.call(current_account, target_account)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def unfollow!
 | 
			
		||||
    accounts.find_each do |target_account|
 | 
			
		||||
    accounts.each do |target_account|
 | 
			
		||||
      UnfollowService.new.call(current_account, target_account)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -61,23 +64,31 @@ class Form::AccountBatch
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def approve!
 | 
			
		||||
    users = accounts.includes(:user).map(&:user)
 | 
			
		||||
 | 
			
		||||
    users.each { |user| authorize(user, :approve?) }
 | 
			
		||||
         .each(&:approve!)
 | 
			
		||||
    accounts.includes(:user).find_each do |account|
 | 
			
		||||
      approve_account(account)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def reject!
 | 
			
		||||
    records = accounts.includes(:user)
 | 
			
		||||
    accounts.includes(:user).find_each do |account|
 | 
			
		||||
      reject_account(account)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
    records.each { |account| authorize(account.user, :reject?) }
 | 
			
		||||
           .each { |account| DeleteAccountService.new.call(account, reserve_email: false, reserve_username: false) }
 | 
			
		||||
  def suspend!
 | 
			
		||||
    accounts.find_each do |account|
 | 
			
		||||
      if account.user_pending?
 | 
			
		||||
        reject_account(account)
 | 
			
		||||
      else
 | 
			
		||||
        suspend_account(account)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def suppress_follow_recommendation!
 | 
			
		||||
    authorize(:follow_recommendation, :suppress?)
 | 
			
		||||
 | 
			
		||||
    accounts.each do |account|
 | 
			
		||||
    accounts.find_each do |account|
 | 
			
		||||
      FollowRecommendationSuppression.create(account: account)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -87,4 +98,24 @@ class Form::AccountBatch
 | 
			
		|||
 | 
			
		||||
    FollowRecommendationSuppression.where(account_id: account_ids).destroy_all
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def reject_account(account)
 | 
			
		||||
    authorize(account.user, :reject?)
 | 
			
		||||
    log_action(:reject, account.user, username: account.username)
 | 
			
		||||
    account.suspend!(origin: :local)
 | 
			
		||||
    AccountDeletionWorker.perform_async(account.id, reserve_username: false)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def suspend_account(account)
 | 
			
		||||
    authorize(account, :suspend?)
 | 
			
		||||
    log_action(:suspend, account)
 | 
			
		||||
    account.suspend!(origin: :local)
 | 
			
		||||
    Admin::SuspensionWorker.perform_async(account.id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def approve_account(account)
 | 
			
		||||
    authorize(account.user, :approve?)
 | 
			
		||||
    log_action(:approve, account.user)
 | 
			
		||||
    account.user.approve!
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@ class Trends::Tags < Trends::Base
 | 
			
		|||
  PREFIX = 'trending_tags'
 | 
			
		||||
 | 
			
		||||
  self.default_options = {
 | 
			
		||||
    threshold: 15,
 | 
			
		||||
    threshold: 5,
 | 
			
		||||
    review_threshold: 10,
 | 
			
		||||
    max_score_cooldown: 2.days.freeze,
 | 
			
		||||
    max_score_halflife: 4.hours.freeze,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,24 +1,35 @@
 | 
			
		|||
%tr
 | 
			
		||||
  %td
 | 
			
		||||
    = admin_account_link_to(account)
 | 
			
		||||
  %td
 | 
			
		||||
    %div.account-badges= account_badge(account, all: true)
 | 
			
		||||
  %td
 | 
			
		||||
    - if account.user_current_sign_in_ip
 | 
			
		||||
      %samp.ellipsized-ip{ title: account.user_current_sign_in_ip }= account.user_current_sign_in_ip
 | 
			
		||||
    - else
 | 
			
		||||
      \-
 | 
			
		||||
  %td
 | 
			
		||||
    - if account.user_current_sign_in_at
 | 
			
		||||
      %time.time-ago{ datetime: account.user_current_sign_in_at.iso8601, title: l(account.user_current_sign_in_at) }= l account.user_current_sign_in_at
 | 
			
		||||
    - elsif account.last_status_at.present?
 | 
			
		||||
      %time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at
 | 
			
		||||
    - else
 | 
			
		||||
      \-
 | 
			
		||||
  %td
 | 
			
		||||
    - if account.local? && account.user_pending?
 | 
			
		||||
      = table_link_to 'check', t('admin.accounts.approve'), approve_admin_account_path(account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:approve, account.user)
 | 
			
		||||
      = table_link_to 'times', t('admin.accounts.reject'), reject_admin_account_path(account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:reject, account.user)
 | 
			
		||||
    - else
 | 
			
		||||
      = table_link_to 'circle', t('admin.accounts.web'), web_path("accounts/#{account.id}")
 | 
			
		||||
      = table_link_to 'globe', t('admin.accounts.public'), ActivityPub::TagManager.instance.url_for(account)
 | 
			
		||||
.batch-table__row{ class: [!account.suspended? && account.user_pending? && 'batch-table__row--attention', account.suspended? && 'batch-table__row--muted'] }
 | 
			
		||||
  %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
 | 
			
		||||
    = f.check_box :account_ids, { multiple: true, include_hidden: false }, account.id
 | 
			
		||||
  .batch-table__row__content.batch-table__row__content--unpadded
 | 
			
		||||
    %table.accounts-table
 | 
			
		||||
      %tbody
 | 
			
		||||
        %tr
 | 
			
		||||
          %td
 | 
			
		||||
            = account_link_to account, path: admin_account_path(account.id)
 | 
			
		||||
          %td.accounts-table__count.optional
 | 
			
		||||
            - if account.suspended? || account.user_pending?
 | 
			
		||||
              \-
 | 
			
		||||
            - else
 | 
			
		||||
              = friendly_number_to_human account.statuses_count
 | 
			
		||||
            %small= t('accounts.posts', count: account.statuses_count).downcase
 | 
			
		||||
          %td.accounts-table__count.optional
 | 
			
		||||
            - if account.suspended? || account.user_pending?
 | 
			
		||||
              \-
 | 
			
		||||
            - else
 | 
			
		||||
              = friendly_number_to_human account.followers_count
 | 
			
		||||
            %small= t('accounts.followers', count: account.followers_count).downcase
 | 
			
		||||
          %td.accounts-table__count
 | 
			
		||||
            = relevant_account_timestamp(account)
 | 
			
		||||
            %small= t('accounts.last_active')
 | 
			
		||||
          %td.accounts-table__extra
 | 
			
		||||
            - if account.local?
 | 
			
		||||
              - if account.user_email
 | 
			
		||||
                = link_to account.user_email.split('@').last, admin_accounts_path(email: "%@#{account.user_email.split('@').last}"), title: account.user_email
 | 
			
		||||
              - else
 | 
			
		||||
                \-
 | 
			
		||||
              %br/
 | 
			
		||||
              %samp.ellipsized-ip= relevant_account_ip(account, params[:ip])
 | 
			
		||||
    - if !account.suspended? && account.user_pending? && account.user&.invite_request&.text&.present?
 | 
			
		||||
      .batch-table__row__content__quote
 | 
			
		||||
        %p= account.user&.invite_request&.text
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,30 +5,30 @@
 | 
			
		|||
  .filter-subset
 | 
			
		||||
    %strong= t('admin.accounts.location.title')
 | 
			
		||||
    %ul
 | 
			
		||||
      %li= filter_link_to t('admin.accounts.location.local'), remote: nil
 | 
			
		||||
      %li= filter_link_to t('admin.accounts.location.remote'), remote: '1'
 | 
			
		||||
      %li= filter_link_to t('generic.all'), origin: nil
 | 
			
		||||
      %li= filter_link_to t('admin.accounts.location.local'), origin: 'local'
 | 
			
		||||
      %li= filter_link_to t('admin.accounts.location.remote'), origin: 'remote'
 | 
			
		||||
  .filter-subset
 | 
			
		||||
    %strong= t('admin.accounts.moderation.title')
 | 
			
		||||
    %ul
 | 
			
		||||
      %li= link_to safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), admin_pending_accounts_path
 | 
			
		||||
      %li= filter_link_to t('admin.accounts.moderation.active'), silenced: nil, suspended: nil, pending: nil
 | 
			
		||||
      %li= filter_link_to t('admin.accounts.moderation.silenced'), silenced: '1', suspended: nil, pending: nil
 | 
			
		||||
      %li= filter_link_to t('admin.accounts.moderation.suspended'), suspended: '1', silenced: nil, pending: nil
 | 
			
		||||
      %li= filter_link_to t('generic.all'), status: nil
 | 
			
		||||
      %li= filter_link_to t('admin.accounts.moderation.active'), status: 'active'
 | 
			
		||||
      %li= filter_link_to t('admin.accounts.moderation.suspended'), status: 'suspended'
 | 
			
		||||
      %li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), status: 'pending'
 | 
			
		||||
  .filter-subset
 | 
			
		||||
    %strong= t('admin.accounts.role')
 | 
			
		||||
    %ul
 | 
			
		||||
      %li= filter_link_to t('admin.accounts.moderation.all'), staff: nil
 | 
			
		||||
      %li= filter_link_to t('admin.accounts.roles.staff'), staff: '1'
 | 
			
		||||
      %li= filter_link_to t('admin.accounts.moderation.all'), permissions: nil
 | 
			
		||||
      %li= filter_link_to t('admin.accounts.roles.staff'), permissions: 'staff'
 | 
			
		||||
  .filter-subset
 | 
			
		||||
    %strong= t 'generic.order_by'
 | 
			
		||||
    %ul
 | 
			
		||||
      %li= filter_link_to t('relationships.most_recent'), order: nil
 | 
			
		||||
      %li= filter_link_to t('admin.accounts.username'), order: 'alphabetic'
 | 
			
		||||
      %li= filter_link_to t('relationships.last_active'), order: 'active'
 | 
			
		||||
 | 
			
		||||
= form_tag admin_accounts_url, method: 'GET', class: 'simple_form' do
 | 
			
		||||
  .fields-group
 | 
			
		||||
    - AccountFilter::KEYS.each do |key|
 | 
			
		||||
    - (AccountFilter::KEYS - %i(origin status permissions)).each do |key|
 | 
			
		||||
      - if params[key].present?
 | 
			
		||||
        = hidden_field_tag key, params[key]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -41,16 +41,27 @@
 | 
			
		|||
      %button.button= t('admin.accounts.search')
 | 
			
		||||
      = link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative'
 | 
			
		||||
 | 
			
		||||
.table-wrapper
 | 
			
		||||
  %table.table
 | 
			
		||||
    %thead
 | 
			
		||||
      %tr
 | 
			
		||||
        %th= t('admin.accounts.username')
 | 
			
		||||
        %th= t('admin.accounts.role')
 | 
			
		||||
        %th= t('admin.accounts.most_recent_ip')
 | 
			
		||||
        %th= t('admin.accounts.most_recent_activity')
 | 
			
		||||
        %th
 | 
			
		||||
    %tbody
 | 
			
		||||
      = render partial: 'account', collection: @accounts
 | 
			
		||||
= form_for(@form, url: batch_admin_accounts_path) do |f|
 | 
			
		||||
  = hidden_field_tag :page, params[:page] || 1
 | 
			
		||||
 | 
			
		||||
  - AccountFilter::KEYS.each do |key|
 | 
			
		||||
    = hidden_field_tag key, params[key] if params[key].present?
 | 
			
		||||
 | 
			
		||||
  .batch-table
 | 
			
		||||
    .batch-table__toolbar
 | 
			
		||||
      %label.batch-table__toolbar__select.batch-checkbox-all
 | 
			
		||||
        = check_box_tag :batch_checkbox_all, nil, false
 | 
			
		||||
      .batch-table__toolbar__actions
 | 
			
		||||
        - if @accounts.any? { |account| account.user_pending? }
 | 
			
		||||
          = f.button safe_join([fa_icon('check'), t('admin.accounts.approve')]), name: :approve, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
 | 
			
		||||
 | 
			
		||||
          = f.button safe_join([fa_icon('times'), t('admin.accounts.reject')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
 | 
			
		||||
 | 
			
		||||
        = f.button safe_join([fa_icon('lock'), t('admin.accounts.perform_full_suspension')]), name: :suspend, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
 | 
			
		||||
    .batch-table__body
 | 
			
		||||
      - if @accounts.empty?
 | 
			
		||||
        = nothing_here 'nothing-here--under-tabs'
 | 
			
		||||
      - else
 | 
			
		||||
        = render partial: 'account', collection: @accounts, locals: { f: f }
 | 
			
		||||
 | 
			
		||||
= paginate @accounts
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,7 +35,7 @@
 | 
			
		|||
      %span= t('admin.dashboard.pending_reports_html', count: @pending_reports_count)
 | 
			
		||||
      = fa_icon 'chevron-right fw'
 | 
			
		||||
 | 
			
		||||
    = link_to admin_pending_accounts_path, class: 'dashboard__quick-access' do
 | 
			
		||||
    = link_to admin_accounts_path(status: 'pending'), class: 'dashboard__quick-access' do
 | 
			
		||||
      %span= t('admin.dashboard.pending_users_html', count: @pending_users_count)
 | 
			
		||||
      = fa_icon 'chevron-right fw'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,7 +15,7 @@
 | 
			
		|||
 | 
			
		||||
.dashboard__counters
 | 
			
		||||
  %div
 | 
			
		||||
    = link_to admin_accounts_path(remote: '1', by_domain: @instance.domain) do
 | 
			
		||||
    = link_to admin_accounts_path(origin: 'remote', by_domain: @instance.domain) do
 | 
			
		||||
      .dashboard__counters__num= number_with_delimiter @instance.accounts_count
 | 
			
		||||
      .dashboard__counters__label= t 'admin.accounts.title'
 | 
			
		||||
  %div
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,9 @@
 | 
			
		|||
.batch-table__row
 | 
			
		||||
  %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
 | 
			
		||||
    = f.check_box :ip_block_ids, { multiple: true, include_hidden: false }, ip_block.id
 | 
			
		||||
  .batch-table__row__content
 | 
			
		||||
    .batch-table__row__content__text
 | 
			
		||||
      %samp= "#{ip_block.ip}/#{ip_block.ip.prefix}"
 | 
			
		||||
  .batch-table__row__content.pending-account
 | 
			
		||||
    .pending-account__header
 | 
			
		||||
      %samp= link_to "#{ip_block.ip}/#{ip_block.ip.prefix}", admin_accounts_path(ip: "#{ip_block.ip}/#{ip_block.ip.prefix}")
 | 
			
		||||
      - if ip_block.comment.present?
 | 
			
		||||
        •
 | 
			
		||||
        = ip_block.comment
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,16 +0,0 @@
 | 
			
		|||
.batch-table__row
 | 
			
		||||
  %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
 | 
			
		||||
    = f.check_box :account_ids, { multiple: true, include_hidden: false }, account.id
 | 
			
		||||
  .batch-table__row__content.pending-account
 | 
			
		||||
    .pending-account__header
 | 
			
		||||
      = link_to admin_account_path(account.id) do
 | 
			
		||||
        %strong= account.user_email
 | 
			
		||||
        = "(@#{account.username})"
 | 
			
		||||
      %br/
 | 
			
		||||
      %samp= account.user_current_sign_in_ip
 | 
			
		||||
      •
 | 
			
		||||
      = t 'admin.accounts.time_in_queue', time: time_ago_in_words(account.user&.created_at)
 | 
			
		||||
 | 
			
		||||
    - if account.user&.invite_request&.text&.present?
 | 
			
		||||
      .pending-account__body
 | 
			
		||||
        %p= account.user&.invite_request&.text
 | 
			
		||||
| 
						 | 
				
			
			@ -1,30 +0,0 @@
 | 
			
		|||
- content_for :page_title do
 | 
			
		||||
  = t('admin.pending_accounts.title', count: User.pending.count)
 | 
			
		||||
 | 
			
		||||
= form_for(@form, url: batch_admin_pending_accounts_path) do |f|
 | 
			
		||||
  = hidden_field_tag :page, params[:page] || 1
 | 
			
		||||
 | 
			
		||||
  .batch-table
 | 
			
		||||
    .batch-table__toolbar
 | 
			
		||||
      %label.batch-table__toolbar__select.batch-checkbox-all
 | 
			
		||||
        = check_box_tag :batch_checkbox_all, nil, false
 | 
			
		||||
      .batch-table__toolbar__actions
 | 
			
		||||
        = f.button safe_join([fa_icon('check'), t('admin.accounts.approve')]), name: :approve, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
 | 
			
		||||
 | 
			
		||||
        = f.button safe_join([fa_icon('times'), t('admin.accounts.reject')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
 | 
			
		||||
    .batch-table__body
 | 
			
		||||
      - if @accounts.empty?
 | 
			
		||||
        = nothing_here 'nothing-here--under-tabs'
 | 
			
		||||
      - else
 | 
			
		||||
        = render partial: 'account', collection: @accounts, locals: { f: f }
 | 
			
		||||
 | 
			
		||||
= paginate @accounts
 | 
			
		||||
 | 
			
		||||
%hr.spacer/
 | 
			
		||||
 | 
			
		||||
%div.action-buttons
 | 
			
		||||
  %div
 | 
			
		||||
    = link_to t('admin.accounts.approve_all'), approve_all_admin_pending_accounts_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button'
 | 
			
		||||
 | 
			
		||||
  %div
 | 
			
		||||
    = link_to t('admin.accounts.reject_all'), reject_all_admin_pending_accounts_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive'
 | 
			
		||||
| 
						 | 
				
			
			@ -9,4 +9,4 @@
 | 
			
		|||
<%= quote_wrap(@account.user&.invite_request&.text) %>
 | 
			
		||||
<% end %>
 | 
			
		||||
 | 
			
		||||
<%= raw t('application_mailer.view')%> <%= admin_pending_accounts_url %>
 | 
			
		||||
<%= raw t('application_mailer.view')%> <%= admin_accounts_url(status: 'pending') %>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,12 +16,12 @@ class Scheduler::FollowRecommendationsScheduler
 | 
			
		|||
    AccountSummary.refresh
 | 
			
		||||
    FollowRecommendation.refresh
 | 
			
		||||
 | 
			
		||||
    fallback_recommendations = FollowRecommendation.limit(SET_SIZE).index_by(&:account_id)
 | 
			
		||||
    fallback_recommendations = FollowRecommendation.order(rank: :desc).limit(SET_SIZE).index_by(&:account_id)
 | 
			
		||||
 | 
			
		||||
    I18n.available_locales.each do |locale|
 | 
			
		||||
      recommendations = begin
 | 
			
		||||
        if AccountSummary.safe.filtered.localized(locale).exists? # We can skip the work if no accounts with that language exist
 | 
			
		||||
          FollowRecommendation.localized(locale).limit(SET_SIZE).index_by(&:account_id)
 | 
			
		||||
          FollowRecommendation.localized(locale).order(rank: :desc).limit(SET_SIZE).index_by(&:account_id)
 | 
			
		||||
        else
 | 
			
		||||
          {}
 | 
			
		||||
        end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -99,7 +99,6 @@ en:
 | 
			
		|||
    accounts:
 | 
			
		||||
      add_email_domain_block: Block e-mail domain
 | 
			
		||||
      approve: Approve
 | 
			
		||||
      approve_all: Approve all
 | 
			
		||||
      approved_msg: Successfully approved %{username}'s sign-up application
 | 
			
		||||
      are_you_sure: Are you sure?
 | 
			
		||||
      avatar: Avatar
 | 
			
		||||
| 
						 | 
				
			
			@ -153,7 +152,6 @@ en:
 | 
			
		|||
        active: Active
 | 
			
		||||
        all: All
 | 
			
		||||
        pending: Pending
 | 
			
		||||
        silenced: Limited
 | 
			
		||||
        suspended: Suspended
 | 
			
		||||
        title: Moderation
 | 
			
		||||
      moderation_notes: Moderation notes
 | 
			
		||||
| 
						 | 
				
			
			@ -171,7 +169,6 @@ en:
 | 
			
		|||
      redownload: Refresh profile
 | 
			
		||||
      redownloaded_msg: Successfully refreshed %{username}'s profile from origin
 | 
			
		||||
      reject: Reject
 | 
			
		||||
      reject_all: Reject all
 | 
			
		||||
      rejected_msg: Successfully rejected %{username}'s sign-up application
 | 
			
		||||
      remove_avatar: Remove avatar
 | 
			
		||||
      remove_header: Remove header
 | 
			
		||||
| 
						 | 
				
			
			@ -210,7 +207,6 @@ en:
 | 
			
		|||
      suspended: Suspended
 | 
			
		||||
      suspension_irreversible: The data of this account has been irreversibly deleted. You can unsuspend the account to make it usable but it will not recover any data it previously had.
 | 
			
		||||
      suspension_reversible_hint_html: The account has been suspended, and the data will be fully removed on %{date}. Until then, the account can be restored without any ill effects. If you wish to remove all of the account's data immediately, you can do so below.
 | 
			
		||||
      time_in_queue: Waiting in queue %{time}
 | 
			
		||||
      title: Accounts
 | 
			
		||||
      unconfirmed_email: Unconfirmed email
 | 
			
		||||
      undo_sensitized: Undo force-sensitive
 | 
			
		||||
| 
						 | 
				
			
			@ -226,6 +222,7 @@ en:
 | 
			
		|||
      whitelisted: Allowed for federation
 | 
			
		||||
    action_logs:
 | 
			
		||||
      action_types:
 | 
			
		||||
        approve_user: Approve User
 | 
			
		||||
        assigned_to_self_report: Assign Report
 | 
			
		||||
        change_email_user: Change E-mail for User
 | 
			
		||||
        confirm_user: Confirm User
 | 
			
		||||
| 
						 | 
				
			
			@ -255,6 +252,7 @@ en:
 | 
			
		|||
        enable_user: Enable User
 | 
			
		||||
        memorialize_account: Memorialize Account
 | 
			
		||||
        promote_user: Promote User
 | 
			
		||||
        reject_user: Reject User
 | 
			
		||||
        remove_avatar_user: Remove Avatar
 | 
			
		||||
        reopen_report: Reopen Report
 | 
			
		||||
        reset_password_user: Reset Password
 | 
			
		||||
| 
						 | 
				
			
			@ -271,6 +269,7 @@ en:
 | 
			
		|||
        update_domain_block: Update Domain Block
 | 
			
		||||
        update_status: Update Post
 | 
			
		||||
      actions:
 | 
			
		||||
        approve_user_html: "%{name} approved sign-up from %{target}"
 | 
			
		||||
        assigned_to_self_report_html: "%{name} assigned report %{target} to themselves"
 | 
			
		||||
        change_email_user_html: "%{name} changed the e-mail address of user %{target}"
 | 
			
		||||
        confirm_user_html: "%{name} confirmed e-mail address of user %{target}"
 | 
			
		||||
| 
						 | 
				
			
			@ -300,6 +299,7 @@ en:
 | 
			
		|||
        enable_user_html: "%{name} enabled login for user %{target}"
 | 
			
		||||
        memorialize_account_html: "%{name} turned %{target}'s account into a memoriam page"
 | 
			
		||||
        promote_user_html: "%{name} promoted user %{target}"
 | 
			
		||||
        reject_user_html: "%{name} rejected sign-up from %{target}"
 | 
			
		||||
        remove_avatar_user_html: "%{name} removed %{target}'s avatar"
 | 
			
		||||
        reopen_report_html: "%{name} reopened report %{target}"
 | 
			
		||||
        reset_password_user_html: "%{name} reset password of user %{target}"
 | 
			
		||||
| 
						 | 
				
			
			@ -377,13 +377,13 @@ en:
 | 
			
		|||
      new_users: new users
 | 
			
		||||
      opened_reports: reports opened
 | 
			
		||||
      pending_reports_html:
 | 
			
		||||
        one: "<strong>1</strong> pending reports"
 | 
			
		||||
        one: "<strong>1</strong> pending report"
 | 
			
		||||
        other: "<strong>%{count}</strong> pending reports"
 | 
			
		||||
      pending_tags_html:
 | 
			
		||||
        one: "<strong>1</strong> pending hashtags"
 | 
			
		||||
        one: "<strong>1</strong> pending hashtag"
 | 
			
		||||
        other: "<strong>%{count}</strong> pending hashtags"
 | 
			
		||||
      pending_users_html:
 | 
			
		||||
        one: "<strong>1</strong> pending users"
 | 
			
		||||
        one: "<strong>1</strong> pending user"
 | 
			
		||||
        other: "<strong>%{count}</strong> pending users"
 | 
			
		||||
      resolved_reports: reports resolved
 | 
			
		||||
      software: Software
 | 
			
		||||
| 
						 | 
				
			
			@ -519,8 +519,6 @@ en:
 | 
			
		|||
        title: Create new IP rule
 | 
			
		||||
      no_ip_block_selected: No IP rules were changed as none were selected
 | 
			
		||||
      title: IP rules
 | 
			
		||||
    pending_accounts:
 | 
			
		||||
      title: Pending accounts (%{count})
 | 
			
		||||
    relationships:
 | 
			
		||||
      title: "%{acct}'s relationships"
 | 
			
		||||
    relays:
 | 
			
		||||
| 
						 | 
				
			
			@ -980,6 +978,7 @@ en:
 | 
			
		|||
    none: None
 | 
			
		||||
    order_by: Order by
 | 
			
		||||
    save_changes: Save changes
 | 
			
		||||
    today: today
 | 
			
		||||
    validation_errors:
 | 
			
		||||
      one: Something isn't quite right yet! Please review the error below
 | 
			
		||||
      other: Something isn't quite right yet! Please review %{count} errors below
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,7 +47,7 @@ SimpleNavigation::Configuration.run do |navigation|
 | 
			
		|||
    n.item :moderation, safe_join([fa_icon('gavel fw'), t('moderation.title')]), admin_reports_url, if: proc { current_user.staff? } do |s|
 | 
			
		||||
      s.item :action_logs, safe_join([fa_icon('bars fw'), t('admin.action_logs.title')]), admin_action_logs_url
 | 
			
		||||
      s.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports}
 | 
			
		||||
      s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url, highlights_on: %r{/admin/accounts|/admin/pending_accounts}
 | 
			
		||||
      s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url(origin: 'local'), highlights_on: %r{/admin/accounts|/admin/pending_accounts}
 | 
			
		||||
      s.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path
 | 
			
		||||
      s.item :follow_recommendations, safe_join([fa_icon('user-plus fw'), t('admin.follow_recommendations.title')]), admin_follow_recommendations_path, highlights_on: %r{/admin/follow_recommendations}
 | 
			
		||||
      s.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url(limited: whitelist_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows}, if: -> { current_user.admin? }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -253,6 +253,10 @@ Rails.application.routes.draw do
 | 
			
		|||
        post :reject
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      collection do
 | 
			
		||||
        post :batch
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      resource :change_email, only: [:show, :update]
 | 
			
		||||
      resource :reset, only: [:create]
 | 
			
		||||
      resource :action, only: [:new, :create], controller: 'account_actions'
 | 
			
		||||
| 
						 | 
				
			
			@ -273,14 +277,6 @@ Rails.application.routes.draw do
 | 
			
		|||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    resources :pending_accounts, only: [:index] do
 | 
			
		||||
      collection do
 | 
			
		||||
        post :approve_all
 | 
			
		||||
        post :reject_all
 | 
			
		||||
        post :batch
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    resources :users, only: [] do
 | 
			
		||||
      resource :two_factor_authentication, only: [:destroy]
 | 
			
		||||
      resource :sign_in_token_authentication, only: [:create, :destroy]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,19 +2,13 @@ commit_message: '[ci skip]'
 | 
			
		|||
files:
 | 
			
		||||
  - source: /app/javascript/mastodon/locales/en.json
 | 
			
		||||
    translation: /app/javascript/mastodon/locales/%two_letters_code%.json
 | 
			
		||||
    update_option: update_as_unapproved
 | 
			
		||||
  - source: /config/locales/en.yml
 | 
			
		||||
    translation: /config/locales/%two_letters_code%.yml
 | 
			
		||||
    update_option: update_as_unapproved
 | 
			
		||||
  - source: /config/locales/simple_form.en.yml
 | 
			
		||||
    translation: /config/locales/simple_form.%two_letters_code%.yml
 | 
			
		||||
    update_option: update_as_unapproved
 | 
			
		||||
  - source: /config/locales/activerecord.en.yml
 | 
			
		||||
    translation: /config/locales/activerecord.%two_letters_code%.yml
 | 
			
		||||
    update_option: update_as_unapproved
 | 
			
		||||
  - source: /config/locales/devise.en.yml
 | 
			
		||||
    translation: /config/locales/devise.%two_letters_code%.yml
 | 
			
		||||
    update_option: update_as_unapproved
 | 
			
		||||
  - source: /config/locales/doorkeeper.en.yml
 | 
			
		||||
    translation: /config/locales/doorkeeper.%two_letters_code%.yml
 | 
			
		||||
    update_option: update_as_unapproved
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
class UpdateAccountSummariesToVersion2 < ActiveRecord::Migration[6.1]
 | 
			
		||||
  def up
 | 
			
		||||
    reapplication_follow_recommendations_v2 do
 | 
			
		||||
      drop_view :account_summaries, materialized: true
 | 
			
		||||
      create_view :account_summaries, version: 2, materialized: { no_data: true }
 | 
			
		||||
      safety_assured { add_index :account_summaries, :account_id, unique: true }
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def down
 | 
			
		||||
    reapplication_follow_recommendations_v2 do
 | 
			
		||||
      drop_view :account_summaries, materialized: true
 | 
			
		||||
      create_view :account_summaries, version: 1, materialized: { no_data: true }
 | 
			
		||||
      safety_assured { add_index :account_summaries, :account_id, unique: true }
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def reapplication_follow_recommendations_v2
 | 
			
		||||
    drop_view :follow_recommendations, materialized: true
 | 
			
		||||
    yield
 | 
			
		||||
    create_view :follow_recommendations, version: 2, materialized: { no_data: true }
 | 
			
		||||
    safety_assured { add_index :follow_recommendations, :account_id, unique: true }
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -10,7 +10,7 @@
 | 
			
		|||
#
 | 
			
		||||
# It's strongly recommended that you check this file into your version control system.
 | 
			
		||||
 | 
			
		||||
ActiveRecord::Schema.define(version: 2021_11_26_000907) do
 | 
			
		||||
ActiveRecord::Schema.define(version: 2021_12_13_040746) do
 | 
			
		||||
 | 
			
		||||
  # These are extensions that must be enabled in order to support this database
 | 
			
		||||
  enable_extension "plpgsql"
 | 
			
		||||
| 
						 | 
				
			
			@ -1131,7 +1131,7 @@ ActiveRecord::Schema.define(version: 2021_11_26_000907) do
 | 
			
		|||
              statuses.language,
 | 
			
		||||
              statuses.sensitive
 | 
			
		||||
             FROM statuses
 | 
			
		||||
            WHERE ((statuses.account_id = accounts.id) AND (statuses.deleted_at IS NULL))
 | 
			
		||||
            WHERE ((statuses.account_id = accounts.id) AND (statuses.deleted_at IS NULL) AND (statuses.reblog_of_id IS NULL))
 | 
			
		||||
            ORDER BY statuses.id DESC
 | 
			
		||||
           LIMIT 20) t0)
 | 
			
		||||
    WHERE ((accounts.suspended_at IS NULL) AND (accounts.silenced_at IS NULL) AND (accounts.moved_to_account_id IS NULL) AND (accounts.discoverable = true) AND (accounts.locked = false))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										23
									
								
								db/views/account_summaries_v02.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								db/views/account_summaries_v02.sql
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
SELECT
 | 
			
		||||
  accounts.id AS account_id,
 | 
			
		||||
  mode() WITHIN GROUP (ORDER BY language ASC) AS language,
 | 
			
		||||
  mode() WITHIN GROUP (ORDER BY sensitive ASC) AS sensitive
 | 
			
		||||
FROM accounts
 | 
			
		||||
CROSS JOIN LATERAL (
 | 
			
		||||
  SELECT
 | 
			
		||||
    statuses.account_id,
 | 
			
		||||
    statuses.language,
 | 
			
		||||
    statuses.sensitive
 | 
			
		||||
  FROM statuses
 | 
			
		||||
  WHERE statuses.account_id = accounts.id
 | 
			
		||||
    AND statuses.deleted_at IS NULL
 | 
			
		||||
    AND statuses.reblog_of_id IS NULL
 | 
			
		||||
  ORDER BY statuses.id DESC
 | 
			
		||||
  LIMIT 20
 | 
			
		||||
) t0
 | 
			
		||||
WHERE accounts.suspended_at IS NULL
 | 
			
		||||
  AND accounts.silenced_at IS NULL
 | 
			
		||||
  AND accounts.moved_to_account_id IS NULL
 | 
			
		||||
  AND accounts.discoverable = 't'
 | 
			
		||||
  AND accounts.locked = 'f'
 | 
			
		||||
GROUP BY accounts.id
 | 
			
		||||
| 
						 | 
				
			
			@ -14,16 +14,21 @@ module Mastodon
 | 
			
		|||
    end
 | 
			
		||||
 | 
			
		||||
    option :days, type: :numeric, default: 90
 | 
			
		||||
    option :clean_followed, type: :boolean
 | 
			
		||||
    option :skip_media_remove, type: :boolean
 | 
			
		||||
    option :vacuum, type: :boolean, default: false, desc: 'Reduce the file size and update the statistics. This option locks the table for a long time, so run it offline'
 | 
			
		||||
    option :batch_size, type: :numeric, default: 1_000, aliases: [:b], desc: 'Number of records in each batch'
 | 
			
		||||
    option :continue, type: :boolean, default: false, desc: 'If remove is not completed, execute from the previous continuation'
 | 
			
		||||
    option :clean_followed, type: :boolean, default: false, desc: 'Include the status of remote accounts that are followed by local accounts as candidates for remove'
 | 
			
		||||
    option :skip_status_remove, type: :boolean, default: false, desc: 'Skip status remove (run only cleanup tasks)'
 | 
			
		||||
    option :skip_media_remove, type: :boolean, default: false, desc: 'Skip remove orphaned media attachments'
 | 
			
		||||
    option :compress_database, type: :boolean, default: false, desc: 'Compress database and update the statistics. This option locks the table for a long time, so run it offline'
 | 
			
		||||
    desc 'remove', 'Remove unreferenced statuses'
 | 
			
		||||
    long_desc <<~LONG_DESC
 | 
			
		||||
      Remove statuses that are not referenced by local user activity, such as
 | 
			
		||||
      ones that came from relays, or belonging to users that were once followed
 | 
			
		||||
      by someone locally but no longer are.
 | 
			
		||||
 | 
			
		||||
      It also removes orphaned records and performs additional cleanup tasks
 | 
			
		||||
      such as updating statistics and recovering disk space.
 | 
			
		||||
 | 
			
		||||
      This is a computationally heavy procedure that creates extra database
 | 
			
		||||
      indices before commencing, and removes them afterward.
 | 
			
		||||
    LONG_DESC
 | 
			
		||||
| 
						 | 
				
			
			@ -33,41 +38,56 @@ module Mastodon
 | 
			
		|||
        exit(1)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      remove_statuses
 | 
			
		||||
      vacuum_and_analyze_statuses
 | 
			
		||||
      remove_orphans_media_attachments
 | 
			
		||||
      remove_orphans_conversations
 | 
			
		||||
      vacuum_and_analyze_conversations
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    private
 | 
			
		||||
 | 
			
		||||
    def remove_statuses
 | 
			
		||||
      return if options[:skip_status_remove]
 | 
			
		||||
 | 
			
		||||
      say('Creating temporary database indices...')
 | 
			
		||||
 | 
			
		||||
      ActiveRecord::Base.connection.add_index(:accounts, :id, name: :index_accounts_local, where: 'domain is null', algorithm: :concurrently, if_not_exists: true)
 | 
			
		||||
      ActiveRecord::Base.connection.add_index(:status_pins, :status_id, name: :index_status_pins_status_id, algorithm: :concurrently, if_not_exists: true)
 | 
			
		||||
      ActiveRecord::Base.connection.add_index(:media_attachments, :remote_url, name: :index_media_attachments_remote_url, where: 'remote_url is not null', algorithm: :concurrently, if_not_exists: true)
 | 
			
		||||
 | 
			
		||||
      max_id   = Mastodon::Snowflake.id_at(options[:days].days.ago)
 | 
			
		||||
      start_at = Time.now.to_f
 | 
			
		||||
 | 
			
		||||
      say('Extract the deletion target... This might take a while...')
 | 
			
		||||
      unless options[:continue] && ActiveRecord::Base.connection.table_exists?('statuses_to_be_deleted')
 | 
			
		||||
        ActiveRecord::Base.connection.add_index(:accounts, :id, name: :index_accounts_local, where: 'domain is null', algorithm: :concurrently, if_not_exists: true)
 | 
			
		||||
        ActiveRecord::Base.connection.add_index(:status_pins, :status_id, name: :index_status_pins_status_id, algorithm: :concurrently, if_not_exists: true)
 | 
			
		||||
 | 
			
		||||
      ActiveRecord::Base.connection.create_table('statuses_to_be_deleted', temporary: true)
 | 
			
		||||
        say('Extract the deletion target from statuses... This might take a while...')
 | 
			
		||||
 | 
			
		||||
      # Skip accounts followed by local accounts
 | 
			
		||||
      clean_followed_sql = 'AND NOT EXISTS (SELECT 1 FROM follows WHERE statuses.account_id = follows.target_account_id)' unless options[:clean_followed]
 | 
			
		||||
        ActiveRecord::Base.connection.create_table('statuses_to_be_deleted', force: true)
 | 
			
		||||
 | 
			
		||||
      ActiveRecord::Base.connection.exec_insert(<<-SQL.squish, 'SQL', [[nil, max_id]])
 | 
			
		||||
        INSERT INTO statuses_to_be_deleted (id)
 | 
			
		||||
        SELECT statuses.id FROM statuses WHERE deleted_at IS NULL AND NOT local AND uri IS NOT NULL AND (id < $1)
 | 
			
		||||
        AND NOT EXISTS (SELECT 1 FROM statuses AS statuses1 WHERE statuses.id = statuses1.in_reply_to_id)
 | 
			
		||||
        AND NOT EXISTS (SELECT 1 FROM statuses AS statuses1 WHERE statuses1.id = statuses.reblog_of_id AND (statuses1.uri IS NULL OR statuses1.local))
 | 
			
		||||
        AND NOT EXISTS (SELECT 1 FROM statuses AS statuses1 WHERE statuses.id = statuses1.reblog_of_id AND (statuses1.uri IS NULL OR statuses1.local OR statuses1.id >= $1))
 | 
			
		||||
        AND NOT EXISTS (SELECT 1 FROM status_pins WHERE statuses.id = status_id)
 | 
			
		||||
        AND NOT EXISTS (SELECT 1 FROM mentions WHERE statuses.id = mentions.status_id AND mentions.account_id IN (SELECT accounts.id FROM accounts WHERE domain IS NULL))
 | 
			
		||||
        AND NOT EXISTS (SELECT 1 FROM favourites WHERE statuses.id = favourites.status_id AND favourites.account_id IN (SELECT accounts.id FROM accounts WHERE domain IS NULL))
 | 
			
		||||
        AND NOT EXISTS (SELECT 1 FROM bookmarks WHERE statuses.id = bookmarks.status_id AND bookmarks.account_id IN (SELECT accounts.id FROM accounts WHERE domain IS NULL))
 | 
			
		||||
        #{clean_followed_sql}
 | 
			
		||||
      SQL
 | 
			
		||||
        # Skip accounts followed by local accounts
 | 
			
		||||
        clean_followed_sql = 'AND NOT EXISTS (SELECT 1 FROM follows WHERE statuses.account_id = follows.target_account_id)' unless options[:clean_followed]
 | 
			
		||||
 | 
			
		||||
      say('Removing temporary database indices to restore write performance...')
 | 
			
		||||
        ActiveRecord::Base.connection.exec_insert(<<-SQL.squish, 'SQL', [[nil, max_id]])
 | 
			
		||||
          INSERT INTO statuses_to_be_deleted (id)
 | 
			
		||||
          SELECT statuses.id FROM statuses WHERE deleted_at IS NULL AND NOT local AND uri IS NOT NULL AND (id < $1)
 | 
			
		||||
          AND NOT EXISTS (SELECT 1 FROM statuses AS statuses1 WHERE statuses.id = statuses1.in_reply_to_id)
 | 
			
		||||
          AND NOT EXISTS (SELECT 1 FROM statuses AS statuses1 WHERE statuses1.id = statuses.reblog_of_id AND (statuses1.uri IS NULL OR statuses1.local))
 | 
			
		||||
          AND NOT EXISTS (SELECT 1 FROM statuses AS statuses1 WHERE statuses.id = statuses1.reblog_of_id AND (statuses1.uri IS NULL OR statuses1.local OR statuses1.id >= $1))
 | 
			
		||||
          AND NOT EXISTS (SELECT 1 FROM status_pins WHERE statuses.id = status_id)
 | 
			
		||||
          AND NOT EXISTS (SELECT 1 FROM mentions WHERE statuses.id = mentions.status_id AND mentions.account_id IN (SELECT accounts.id FROM accounts WHERE domain IS NULL))
 | 
			
		||||
          AND NOT EXISTS (SELECT 1 FROM favourites WHERE statuses.id = favourites.status_id AND favourites.account_id IN (SELECT accounts.id FROM accounts WHERE domain IS NULL))
 | 
			
		||||
          AND NOT EXISTS (SELECT 1 FROM bookmarks WHERE statuses.id = bookmarks.status_id AND bookmarks.account_id IN (SELECT accounts.id FROM accounts WHERE domain IS NULL))
 | 
			
		||||
          #{clean_followed_sql}
 | 
			
		||||
        SQL
 | 
			
		||||
 | 
			
		||||
      ActiveRecord::Base.connection.remove_index(:accounts, name: :index_accounts_local, if_exists: true)
 | 
			
		||||
      ActiveRecord::Base.connection.remove_index(:status_pins, name: :index_status_pins_status_id, if_exists: true)
 | 
			
		||||
        say('Removing temporary database indices to restore write performance...')
 | 
			
		||||
 | 
			
		||||
      say('Beginning removal... This might take a while...')
 | 
			
		||||
        ActiveRecord::Base.connection.remove_index(:accounts, name: :index_accounts_local, if_exists: true)
 | 
			
		||||
        ActiveRecord::Base.connection.remove_index(:status_pins, name: :index_status_pins_status_id, if_exists: true)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      say('Beginning statuses removal... This might take a while...')
 | 
			
		||||
 | 
			
		||||
      klass = Class.new(ApplicationRecord) do |c|
 | 
			
		||||
        c.table_name = 'statuses_to_be_deleted'
 | 
			
		||||
| 
						 | 
				
			
			@ -89,20 +109,7 @@ module Mastodon
 | 
			
		|||
 | 
			
		||||
      progress.stop
 | 
			
		||||
 | 
			
		||||
      if options[:vacuum]
 | 
			
		||||
        say('Run VACUUM and ANALYZE to statuses...')
 | 
			
		||||
 | 
			
		||||
        ActiveRecord::Base.connection.execute('VACUUM FULL ANALYZE statuses')
 | 
			
		||||
      else
 | 
			
		||||
        say('Run ANALYZE to statuses...')
 | 
			
		||||
 | 
			
		||||
        ActiveRecord::Base.connection.execute('ANALYZE statuses')
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      unless options[:skip_media_remove]
 | 
			
		||||
        say('Beginning removal of now-orphaned media attachments to free up disk space...')
 | 
			
		||||
        Scheduler::MediaCleanupScheduler.new.perform
 | 
			
		||||
      end
 | 
			
		||||
      ActiveRecord::Base.connection.drop_table('statuses_to_be_deleted')
 | 
			
		||||
 | 
			
		||||
      say("Done after #{Time.now.to_f - start_at}s, removed #{removed} out of #{processed} statuses.", :green)
 | 
			
		||||
    ensure
 | 
			
		||||
| 
						 | 
				
			
			@ -112,5 +119,108 @@ module Mastodon
 | 
			
		|||
      ActiveRecord::Base.connection.remove_index(:status_pins, name: :index_status_pins_status_id, if_exists: true)
 | 
			
		||||
      ActiveRecord::Base.connection.remove_index(:media_attachments, name: :index_media_attachments_remote_url, if_exists: true)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def remove_orphans_media_attachments
 | 
			
		||||
      return if options[:skip_media_remove]
 | 
			
		||||
 | 
			
		||||
      start_at = Time.now.to_f
 | 
			
		||||
 | 
			
		||||
      say('Beginning removal of now-orphaned media attachments to free up disk space...')
 | 
			
		||||
 | 
			
		||||
      scope     = MediaAttachment.reorder(nil).unattached.where('created_at < ?', options[:days].pred.days.ago)
 | 
			
		||||
      processed = 0
 | 
			
		||||
      removed   = 0
 | 
			
		||||
      progress  = create_progress_bar(scope.count)
 | 
			
		||||
 | 
			
		||||
      scope.find_each do |media_attachment|
 | 
			
		||||
        media_attachment.destroy!
 | 
			
		||||
 | 
			
		||||
        removed += 1
 | 
			
		||||
      rescue => e
 | 
			
		||||
        progress.log pastel.red("Error processing #{media_attachment.id}: #{e}")
 | 
			
		||||
      ensure
 | 
			
		||||
        progress.increment
 | 
			
		||||
        processed += 1
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      progress.stop
 | 
			
		||||
 | 
			
		||||
      say("Done after #{Time.now.to_f - start_at}s, removed #{removed} out of #{processed} media_attachments.", :green)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def remove_orphans_conversations
 | 
			
		||||
      start_at = Time.now.to_f
 | 
			
		||||
 | 
			
		||||
      unless options[:continue] && ActiveRecord::Base.connection.table_exists?('conversations_to_be_deleted')
 | 
			
		||||
        say('Creating temporary database indices...')
 | 
			
		||||
 | 
			
		||||
        ActiveRecord::Base.connection.add_index(:statuses, :conversation_id, name: :index_statuses_conversation_id, algorithm: :concurrently, if_not_exists: true)
 | 
			
		||||
 | 
			
		||||
        say('Extract the deletion target from coversations... This might take a while...')
 | 
			
		||||
 | 
			
		||||
        ActiveRecord::Base.connection.create_table('conversations_to_be_deleted', force: true)
 | 
			
		||||
 | 
			
		||||
        ActiveRecord::Base.connection.exec_insert(<<-SQL.squish, 'SQL')
 | 
			
		||||
          INSERT INTO conversations_to_be_deleted (id)
 | 
			
		||||
          SELECT id FROM conversations WHERE NOT EXISTS (SELECT 1 FROM statuses WHERE statuses.conversation_id = conversations.id)
 | 
			
		||||
        SQL
 | 
			
		||||
 | 
			
		||||
        say('Removing temporary database indices to restore write performance...')
 | 
			
		||||
        ActiveRecord::Base.connection.remove_index(:statuses, name: :index_statuses_conversation_id, if_exists: true)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      say('Beginning orphans removal... This might take a while...')
 | 
			
		||||
 | 
			
		||||
      klass = Class.new(ApplicationRecord) do |c|
 | 
			
		||||
        c.table_name = 'conversations_to_be_deleted'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      Object.const_set('ConversationsToBeDeleted', klass)
 | 
			
		||||
 | 
			
		||||
      scope     = ConversationsToBeDeleted
 | 
			
		||||
      processed = 0
 | 
			
		||||
      removed   = 0
 | 
			
		||||
      progress  = create_progress_bar(scope.count.fdiv(options[:batch_size]).ceil)
 | 
			
		||||
 | 
			
		||||
      scope.in_batches(of: options[:batch_size]) do |relation|
 | 
			
		||||
        ids        = relation.pluck(:id)
 | 
			
		||||
        processed += ids.count
 | 
			
		||||
        removed   += Conversation.unscoped.where(id: ids).delete_all
 | 
			
		||||
        progress.increment
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      progress.stop
 | 
			
		||||
 | 
			
		||||
      ActiveRecord::Base.connection.drop_table('conversations_to_be_deleted')
 | 
			
		||||
 | 
			
		||||
      say("Done after #{Time.now.to_f - start_at}s, removed #{removed} out of #{processed} conversations.", :green)
 | 
			
		||||
    ensure
 | 
			
		||||
      say('Removing temporary database indices to restore write performance...')
 | 
			
		||||
      ActiveRecord::Base.connection.remove_index(:statuses, name: :index_statuses_conversation_id, if_exists: true)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def vacuum_and_analyze_statuses
 | 
			
		||||
      if options[:compress_database]
 | 
			
		||||
        say('Run VACUUM FULL ANALYZE to statuses...')
 | 
			
		||||
        ActiveRecord::Base.connection.execute('VACUUM FULL ANALYZE statuses')
 | 
			
		||||
        say('Run REINDEX to statuses...')
 | 
			
		||||
        ActiveRecord::Base.connection.execute('REINDEX TABLE statuses')
 | 
			
		||||
      else
 | 
			
		||||
        say('Run ANALYZE to statuses...')
 | 
			
		||||
        ActiveRecord::Base.connection.execute('ANALYZE statuses')
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def vacuum_and_analyze_conversations
 | 
			
		||||
      if options[:compress_database]
 | 
			
		||||
        say('Run VACUUM FULL ANALYZE to conversations...')
 | 
			
		||||
        ActiveRecord::Base.connection.execute('VACUUM FULL ANALYZE conversations')
 | 
			
		||||
        say('Run REINDEX to conversations...')
 | 
			
		||||
        ActiveRecord::Base.connection.execute('REINDEX TABLE conversations')
 | 
			
		||||
      else
 | 
			
		||||
        say('Run ANALYZE to conversations...')
 | 
			
		||||
        ActiveRecord::Base.connection.execute('ANALYZE conversations')
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										14
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								package.json
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -149,13 +149,13 @@
 | 
			
		|||
    "redis": "^3.1.2",
 | 
			
		||||
    "redux": "^4.1.2",
 | 
			
		||||
    "redux-immutable": "^4.0.0",
 | 
			
		||||
    "redux-thunk": "^2.4.0",
 | 
			
		||||
    "redux-thunk": "^2.4.1",
 | 
			
		||||
    "regenerator-runtime": "^0.13.9",
 | 
			
		||||
    "rellax": "^1.12.1",
 | 
			
		||||
    "requestidlecallback": "^0.3.0",
 | 
			
		||||
    "reselect": "^4.1.4",
 | 
			
		||||
    "reselect": "^4.1.5",
 | 
			
		||||
    "rimraf": "^3.0.2",
 | 
			
		||||
    "sass": "^1.43.4",
 | 
			
		||||
    "sass": "^1.43.5",
 | 
			
		||||
    "sass-loader": "^10.2.0",
 | 
			
		||||
    "stacktrace-js": "^2.0.2",
 | 
			
		||||
    "stringz": "^2.1.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -172,19 +172,19 @@
 | 
			
		|||
    "webpack-cli": "^3.3.12",
 | 
			
		||||
    "webpack-merge": "^5.8.0",
 | 
			
		||||
    "wicg-inert": "^3.1.1",
 | 
			
		||||
    "ws": "^8.2.3"
 | 
			
		||||
    "ws": "^8.3.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@testing-library/jest-dom": "^5.15.0",
 | 
			
		||||
    "@testing-library/jest-dom": "^5.16.0",
 | 
			
		||||
    "@testing-library/react": "^12.1.2",
 | 
			
		||||
    "babel-eslint": "^10.1.0",
 | 
			
		||||
    "babel-jest": "^27.3.1",
 | 
			
		||||
    "babel-jest": "^27.4.0",
 | 
			
		||||
    "eslint": "^7.32.0",
 | 
			
		||||
    "eslint-plugin-import": "~2.25.3",
 | 
			
		||||
    "eslint-plugin-jsx-a11y": "~6.5.1",
 | 
			
		||||
    "eslint-plugin-promise": "~5.1.1",
 | 
			
		||||
    "eslint-plugin-react": "~7.27.1",
 | 
			
		||||
    "jest": "^27.3.1",
 | 
			
		||||
    "jest": "^27.4.3",
 | 
			
		||||
    "raf": "^3.4.1",
 | 
			
		||||
    "react-intl-translations-manager": "^5.0.3",
 | 
			
		||||
    "react-test-renderer": "^16.14.0",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,12 +21,9 @@ RSpec.describe Admin::AccountsController, type: :controller do
 | 
			
		|||
      expect(AccountFilter).to receive(:new) do |params|
 | 
			
		||||
        h = params.to_h
 | 
			
		||||
 | 
			
		||||
        expect(h[:local]).to eq '1'
 | 
			
		||||
        expect(h[:remote]).to eq '1'
 | 
			
		||||
        expect(h[:origin]).to eq 'local'
 | 
			
		||||
        expect(h[:by_domain]).to eq 'domain'
 | 
			
		||||
        expect(h[:active]).to eq '1'
 | 
			
		||||
        expect(h[:silenced]).to eq '1'
 | 
			
		||||
        expect(h[:suspended]).to eq '1'
 | 
			
		||||
        expect(h[:status]).to eq 'active'
 | 
			
		||||
        expect(h[:username]).to eq 'username'
 | 
			
		||||
        expect(h[:display_name]).to eq 'display name'
 | 
			
		||||
        expect(h[:email]).to eq 'local-part@domain'
 | 
			
		||||
| 
						 | 
				
			
			@ -36,12 +33,9 @@ RSpec.describe Admin::AccountsController, type: :controller do
 | 
			
		|||
      end
 | 
			
		||||
 | 
			
		||||
      get :index, params: {
 | 
			
		||||
        local: '1',
 | 
			
		||||
        remote: '1',
 | 
			
		||||
        origin: 'local',
 | 
			
		||||
        by_domain: 'domain',
 | 
			
		||||
        active: '1',
 | 
			
		||||
        silenced: '1',
 | 
			
		||||
        suspended: '1',
 | 
			
		||||
        status: 'active',
 | 
			
		||||
        username: 'username',
 | 
			
		||||
        display_name: 'display name',
 | 
			
		||||
        email: 'local-part@domain',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,10 +2,10 @@ require 'rails_helper'
 | 
			
		|||
 | 
			
		||||
describe AccountFilter do
 | 
			
		||||
  describe 'with empty params' do
 | 
			
		||||
    it 'defaults to recent local not-suspended account list' do
 | 
			
		||||
    it 'excludes instance actor by default' do
 | 
			
		||||
      filter = described_class.new({})
 | 
			
		||||
 | 
			
		||||
      expect(filter.results).to eq Account.local.without_instance_actor.recent.without_suspended
 | 
			
		||||
      expect(filter.results).to eq Account.without_instance_actor
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -16,42 +16,4 @@ describe AccountFilter do
 | 
			
		|||
      expect { filter.results }.to raise_error(/wrong/)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe 'with valid params' do
 | 
			
		||||
    it 'combines filters on Account' do
 | 
			
		||||
      filter = described_class.new(
 | 
			
		||||
        by_domain: 'test.com',
 | 
			
		||||
        silenced: true,
 | 
			
		||||
        username: 'test',
 | 
			
		||||
        display_name: 'name',
 | 
			
		||||
        email: 'user@example.com',
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      allow(Account).to receive(:where).and_return(Account.none)
 | 
			
		||||
      allow(Account).to receive(:silenced).and_return(Account.none)
 | 
			
		||||
      allow(Account).to receive(:matches_display_name).and_return(Account.none)
 | 
			
		||||
      allow(Account).to receive(:matches_username).and_return(Account.none)
 | 
			
		||||
      allow(User).to receive(:matches_email).and_return(User.none)
 | 
			
		||||
 | 
			
		||||
      filter.results
 | 
			
		||||
 | 
			
		||||
      expect(Account).to have_received(:where).with(domain: 'test.com')
 | 
			
		||||
      expect(Account).to have_received(:silenced)
 | 
			
		||||
      expect(Account).to have_received(:matches_username).with('test')
 | 
			
		||||
      expect(Account).to have_received(:matches_display_name).with('name')
 | 
			
		||||
      expect(User).to have_received(:matches_email).with('user@example.com')
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    describe 'that call account methods' do
 | 
			
		||||
      %i(local remote silenced suspended).each do |option|
 | 
			
		||||
        it "delegates the #{option} option" do
 | 
			
		||||
          allow(Account).to receive(option).and_return(Account.none)
 | 
			
		||||
          filter = described_class.new({ option => true })
 | 
			
		||||
          filter.results
 | 
			
		||||
 | 
			
		||||
          expect(Account).to have_received(option).at_least(1)
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue