Merge branch 'main' into glitch-soc/merge-upstream
Conflicts: - `app/controllers/settings/preferences_controller.rb`: Upstream dropping `digest` from notifications emails while we have more notification emails settings. Removed `digest` from our list while keeping our extra settings. - `app/javascript/packs/admin.js`: Conflicts caused by glitch-soc's theming system. Applied the changes to `app/javascript/core/admin.js`. - `app/views/settings/preferences/other/show.html.haml`: Upstream removed a setting close to a glitch-soc-only setting. Applied upstream's change.
This commit is contained in:
		
						commit
						2a7538aeed
					
				
					 72 changed files with 1796 additions and 830 deletions
				
			
		
							
								
								
									
										10
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								Gemfile.lock
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -75,8 +75,8 @@ GEM
 | 
			
		|||
      minitest (>= 5.1)
 | 
			
		||||
      tzinfo (~> 2.0)
 | 
			
		||||
      zeitwerk (~> 2.3)
 | 
			
		||||
    addressable (2.8.0)
 | 
			
		||||
      public_suffix (>= 2.0.2, < 5.0)
 | 
			
		||||
    addressable (2.8.1)
 | 
			
		||||
      public_suffix (>= 2.0.2, < 6.0)
 | 
			
		||||
    aes_key_wrap (1.1.0)
 | 
			
		||||
    airbrussh (1.4.1)
 | 
			
		||||
      sshkit (>= 1.6.1, != 1.7.0)
 | 
			
		||||
| 
						 | 
				
			
			@ -424,7 +424,7 @@ GEM
 | 
			
		|||
      concurrent-ruby (~> 1.0, >= 1.0.2)
 | 
			
		||||
      sidekiq (>= 3.5)
 | 
			
		||||
      statsd-ruby (~> 1.4, >= 1.4.0)
 | 
			
		||||
    oj (3.13.20)
 | 
			
		||||
    oj (3.13.21)
 | 
			
		||||
    omniauth (1.9.2)
 | 
			
		||||
      hashie (>= 3.4.6)
 | 
			
		||||
      rack (>= 1.6.2, < 3)
 | 
			
		||||
| 
						 | 
				
			
			@ -480,8 +480,8 @@ GEM
 | 
			
		|||
      pry (>= 0.13, < 0.15)
 | 
			
		||||
    pry-rails (0.3.9)
 | 
			
		||||
      pry (>= 0.10.4)
 | 
			
		||||
    public_suffix (4.0.7)
 | 
			
		||||
    puma (5.6.4)
 | 
			
		||||
    public_suffix (5.0.0)
 | 
			
		||||
    puma (5.6.5)
 | 
			
		||||
      nio4r (~> 2.0)
 | 
			
		||||
    pundit (2.2.0)
 | 
			
		||||
      activesupport (>= 3.0.0)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,7 +16,11 @@ module Admin
 | 
			
		|||
    def batch
 | 
			
		||||
      authorize :account, :index?
 | 
			
		||||
 | 
			
		||||
      @form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
 | 
			
		||||
      @form = Form::AccountBatch.new(form_account_batch_params)
 | 
			
		||||
      @form.current_account = current_account
 | 
			
		||||
      @form.action = action_from_button
 | 
			
		||||
      @form.select_all_matching = params[:select_all_matching]
 | 
			
		||||
      @form.query = filtered_accounts
 | 
			
		||||
      @form.save
 | 
			
		||||
    rescue ActionController::ParameterMissing
 | 
			
		||||
      flash[:alert] = I18n.t('admin.accounts.no_account_selected')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,6 +23,7 @@ module Admin
 | 
			
		|||
      @role.current_account = current_account
 | 
			
		||||
 | 
			
		||||
      if @role.save
 | 
			
		||||
        log_action :create, @role
 | 
			
		||||
        redirect_to admin_roles_path
 | 
			
		||||
      else
 | 
			
		||||
        render :new
 | 
			
		||||
| 
						 | 
				
			
			@ -39,6 +40,7 @@ module Admin
 | 
			
		|||
      @role.current_account = current_account
 | 
			
		||||
 | 
			
		||||
      if @role.update(resource_params)
 | 
			
		||||
        log_action :update, @role
 | 
			
		||||
        redirect_to admin_roles_path
 | 
			
		||||
      else
 | 
			
		||||
        render :edit
 | 
			
		||||
| 
						 | 
				
			
			@ -48,6 +50,7 @@ module Admin
 | 
			
		|||
    def destroy
 | 
			
		||||
      authorize @role, :destroy?
 | 
			
		||||
      @role.destroy!
 | 
			
		||||
      log_action :destroy, @role
 | 
			
		||||
      redirect_to admin_roles_path
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,6 +14,7 @@ module Admin
 | 
			
		|||
      @user.current_account = current_account
 | 
			
		||||
 | 
			
		||||
      if @user.update(resource_params)
 | 
			
		||||
        log_action :change_role, @user
 | 
			
		||||
        redirect_to admin_account_path(@user.account_id), notice: I18n.t('admin.accounts.change_role.changed_msg')
 | 
			
		||||
      else
 | 
			
		||||
        render :show
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,99 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController
 | 
			
		||||
  include Authorization
 | 
			
		||||
  include AccountableConcern
 | 
			
		||||
 | 
			
		||||
  LIMIT = 100
 | 
			
		||||
 | 
			
		||||
  before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:canonical_email_blocks' }, only: [:index, :show, :test]
 | 
			
		||||
  before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:canonical_email_blocks' }, except: [:index, :show, :test]
 | 
			
		||||
 | 
			
		||||
  before_action :set_canonical_email_blocks, only: :index
 | 
			
		||||
  before_action :set_canonical_email_blocks_from_test, only: [:test]
 | 
			
		||||
  before_action :set_canonical_email_block, only: [:show, :destroy]
 | 
			
		||||
 | 
			
		||||
  after_action :verify_authorized
 | 
			
		||||
  after_action :insert_pagination_headers, only: :index
 | 
			
		||||
 | 
			
		||||
  PAGINATION_PARAMS = %i(limit).freeze
 | 
			
		||||
 | 
			
		||||
  def index
 | 
			
		||||
    authorize :canonical_email_block, :index?
 | 
			
		||||
    render json: @canonical_email_blocks, each_serializer: REST::Admin::CanonicalEmailBlockSerializer
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def show
 | 
			
		||||
    authorize @canonical_email_block, :show?
 | 
			
		||||
    render json: @canonical_email_block, serializer: REST::Admin::CanonicalEmailBlockSerializer
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def test
 | 
			
		||||
    authorize :canonical_email_block, :test?
 | 
			
		||||
    render json: @canonical_email_blocks, each_serializer: REST::Admin::CanonicalEmailBlockSerializer
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def create
 | 
			
		||||
    authorize :canonical_email_block, :create?
 | 
			
		||||
 | 
			
		||||
    @canonical_email_block = CanonicalEmailBlock.create!(resource_params)
 | 
			
		||||
    log_action :create, @canonical_email_block
 | 
			
		||||
 | 
			
		||||
    render json: @canonical_email_block, serializer: REST::Admin::CanonicalEmailBlockSerializer
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def destroy
 | 
			
		||||
    authorize @canonical_email_block, :destroy?
 | 
			
		||||
 | 
			
		||||
    @canonical_email_block.destroy!
 | 
			
		||||
    log_action :destroy, @canonical_email_block
 | 
			
		||||
 | 
			
		||||
    render json: @canonical_email_block, serializer: REST::Admin::CanonicalEmailBlockSerializer
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def resource_params
 | 
			
		||||
    params.permit(:canonical_email_hash, :email)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def set_canonical_email_blocks
 | 
			
		||||
    @canonical_email_blocks = CanonicalEmailBlock.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def set_canonical_email_blocks_from_test
 | 
			
		||||
    @canonical_email_blocks = CanonicalEmailBlock.matching_email(params[:email])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def set_canonical_email_block
 | 
			
		||||
    @canonical_email_block = CanonicalEmailBlock.find(params[:id])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def insert_pagination_headers
 | 
			
		||||
    set_pagination_headers(next_path, prev_path)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def next_path
 | 
			
		||||
    api_v1_admin_canonical_email_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def prev_path
 | 
			
		||||
    api_v1_admin_canonical_email_blocks_url(pagination_params(min_id: pagination_since_id)) unless @canonical_email_blocks.empty?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def pagination_max_id
 | 
			
		||||
    @canonical_email_blocks.last.id
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def pagination_since_id
 | 
			
		||||
    @canonical_email_blocks.first.id
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def records_continue?
 | 
			
		||||
    @canonical_email_blocks.size == limit_param(LIMIT)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def pagination_params(core_params)
 | 
			
		||||
    params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,90 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController
 | 
			
		||||
  include Authorization
 | 
			
		||||
  include AccountableConcern
 | 
			
		||||
 | 
			
		||||
  LIMIT = 100
 | 
			
		||||
 | 
			
		||||
  before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:email_domain_blocks' }, only: [:index, :show]
 | 
			
		||||
  before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:email_domain_blocks' }, except: [:index, :show]
 | 
			
		||||
  before_action :set_email_domain_blocks, only: :index
 | 
			
		||||
  before_action :set_email_domain_block, only: [:show, :destroy]
 | 
			
		||||
 | 
			
		||||
  after_action :verify_authorized
 | 
			
		||||
  after_action :insert_pagination_headers, only: :index
 | 
			
		||||
 | 
			
		||||
  PAGINATION_PARAMS = %i(
 | 
			
		||||
    limit
 | 
			
		||||
  ).freeze
 | 
			
		||||
 | 
			
		||||
  def create
 | 
			
		||||
    authorize :email_domain_block, :create?
 | 
			
		||||
 | 
			
		||||
    @email_domain_block = EmailDomainBlock.create!(resource_params)
 | 
			
		||||
    log_action :create, @email_domain_block
 | 
			
		||||
 | 
			
		||||
    render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def index
 | 
			
		||||
    authorize :email_domain_block, :index?
 | 
			
		||||
    render json: @email_domain_blocks, each_serializer: REST::Admin::EmailDomainBlockSerializer
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def show
 | 
			
		||||
    authorize @email_domain_block, :show?
 | 
			
		||||
    render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def destroy
 | 
			
		||||
    authorize @email_domain_block, :destroy?
 | 
			
		||||
 | 
			
		||||
    @email_domain_block.destroy!
 | 
			
		||||
    log_action :destroy, @email_domain_block
 | 
			
		||||
 | 
			
		||||
    render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def set_email_domain_blocks
 | 
			
		||||
    @email_domain_blocks = EmailDomainBlock.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def set_email_domain_block
 | 
			
		||||
    @email_domain_block = EmailDomainBlock.find(params[:id])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def resource_params
 | 
			
		||||
    params.permit(:domain)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def insert_pagination_headers
 | 
			
		||||
    set_pagination_headers(next_path, prev_path)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def next_path
 | 
			
		||||
    api_v1_admin_email_domain_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def prev_path
 | 
			
		||||
    api_v1_admin_email_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @email_domain_blocks.empty?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def pagination_max_id
 | 
			
		||||
    @email_domain_blocks.last.id
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def pagination_since_id
 | 
			
		||||
    @email_domain_blocks.first.id
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def records_continue?
 | 
			
		||||
    @email_domain_blocks.size == limit_param(LIMIT)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def pagination_params(core_params)
 | 
			
		||||
    params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										99
									
								
								app/controllers/api/v1/admin/ip_blocks_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								app/controllers/api/v1/admin/ip_blocks_controller.rb
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,99 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class Api::V1::Admin::IpBlocksController < Api::BaseController
 | 
			
		||||
  include Authorization
 | 
			
		||||
  include AccountableConcern
 | 
			
		||||
 | 
			
		||||
  LIMIT = 100
 | 
			
		||||
 | 
			
		||||
  before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:ip_blocks' }, only: [:index, :show]
 | 
			
		||||
  before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:ip_blocks' }, except: [:index, :show]
 | 
			
		||||
  before_action :set_ip_blocks, only: :index
 | 
			
		||||
  before_action :set_ip_block, only: [:show, :update, :destroy]
 | 
			
		||||
 | 
			
		||||
  after_action :verify_authorized
 | 
			
		||||
  after_action :insert_pagination_headers, only: :index
 | 
			
		||||
 | 
			
		||||
  PAGINATION_PARAMS = %i(
 | 
			
		||||
    limit
 | 
			
		||||
  ).freeze
 | 
			
		||||
 | 
			
		||||
  def create
 | 
			
		||||
    authorize :ip_block, :create?
 | 
			
		||||
 | 
			
		||||
    @ip_block = IpBlock.create!(resource_params)
 | 
			
		||||
    log_action :create, @ip_block
 | 
			
		||||
 | 
			
		||||
    render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def index
 | 
			
		||||
    authorize :ip_block, :index?
 | 
			
		||||
    render json: @ip_blocks, each_serializer: REST::Admin::IpBlockSerializer
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def show
 | 
			
		||||
    authorize @ip_block, :show?
 | 
			
		||||
    render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def update
 | 
			
		||||
    authorize @ip_block, :update?
 | 
			
		||||
 | 
			
		||||
    @ip_block.update(resource_params)
 | 
			
		||||
    log_action :update, @ip_block
 | 
			
		||||
 | 
			
		||||
    render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def destroy
 | 
			
		||||
    authorize @ip_block, :destroy?
 | 
			
		||||
 | 
			
		||||
    @ip_block.destroy!
 | 
			
		||||
    log_action :destroy, @ip_block
 | 
			
		||||
 | 
			
		||||
    render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def set_ip_blocks
 | 
			
		||||
    @ip_blocks = IpBlock.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def set_ip_block
 | 
			
		||||
    @ip_block = IpBlock.find(params[:id])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def resource_params
 | 
			
		||||
    params.permit(:ip, :severity, :comment, :expires_in)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def insert_pagination_headers
 | 
			
		||||
    set_pagination_headers(next_path, prev_path)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def next_path
 | 
			
		||||
    api_v1_admin_ip_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def prev_path
 | 
			
		||||
    api_v1_admin_ip_blocks_url(pagination_params(min_id: pagination_since_id)) unless @ip_blocks.empty?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def pagination_max_id
 | 
			
		||||
    @ip_blocks.last.id
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def pagination_since_id
 | 
			
		||||
    @ip_blocks.first.id
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def records_continue?
 | 
			
		||||
    @ip_blocks.size == limit_param(LIMIT)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def pagination_params(core_params)
 | 
			
		||||
    params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -3,7 +3,11 @@
 | 
			
		|||
module AccountableConcern
 | 
			
		||||
  extend ActiveSupport::Concern
 | 
			
		||||
 | 
			
		||||
  def log_action(action, target, options = {})
 | 
			
		||||
    Admin::ActionLog.create(account: current_account, action: action, target: target, recorded_changes: options.stringify_keys)
 | 
			
		||||
  def log_action(action, target)
 | 
			
		||||
    Admin::ActionLog.create(
 | 
			
		||||
      account: current_account,
 | 
			
		||||
      action: action,
 | 
			
		||||
      target: target
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -58,7 +58,7 @@ class Settings::PreferencesController < Settings::BaseController
 | 
			
		|||
      :setting_trends,
 | 
			
		||||
      :setting_crop_images,
 | 
			
		||||
      :setting_always_send_emails,
 | 
			
		||||
      notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag trending_link trending_status appeal),
 | 
			
		||||
      notification_emails: %i(follow follow_request reblog favourite mention report pending_account trending_tag trending_link trending_status appeal),
 | 
			
		||||
      interactions: %i(must_be_follower must_be_following must_be_following_dm)
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,64 +2,29 @@
 | 
			
		|||
 | 
			
		||||
module Admin::ActionLogsHelper
 | 
			
		||||
  def log_target(log)
 | 
			
		||||
    if log.target
 | 
			
		||||
      linkable_log_target(log.target)
 | 
			
		||||
    else
 | 
			
		||||
      log_target_from_history(log.target_type, log.recorded_changes)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def linkable_log_target(record)
 | 
			
		||||
    case record.class.name
 | 
			
		||||
    case log.target_type
 | 
			
		||||
    when 'Account'
 | 
			
		||||
      link_to record.acct, admin_account_path(record.id)
 | 
			
		||||
      link_to log.human_identifier, admin_account_path(log.target_id)
 | 
			
		||||
    when 'User'
 | 
			
		||||
      link_to record.account.acct, admin_account_path(record.account_id)
 | 
			
		||||
    when 'CustomEmoji'
 | 
			
		||||
      record.shortcode
 | 
			
		||||
      link_to log.human_identifier, admin_account_path(log.route_param)
 | 
			
		||||
    when 'UserRole'
 | 
			
		||||
      link_to log.human_identifier, admin_roles_path(log.target_id)
 | 
			
		||||
    when 'Report'
 | 
			
		||||
      link_to "##{record.id}", admin_report_path(record)
 | 
			
		||||
      link_to "##{log.human_identifier}", admin_report_path(log.target_id)
 | 
			
		||||
    when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'
 | 
			
		||||
      link_to record.domain, "https://#{record.domain}"
 | 
			
		||||
      link_to log.human_identifier, "https://#{log.human_identifier}"
 | 
			
		||||
    when 'Status'
 | 
			
		||||
      link_to record.account.acct, ActivityPub::TagManager.instance.url_for(record)
 | 
			
		||||
      link_to log.human_identifier, log.permalink
 | 
			
		||||
    when 'AccountWarning'
 | 
			
		||||
      link_to record.target_account.acct, admin_account_path(record.target_account_id)
 | 
			
		||||
      link_to log.human_identifier, admin_account_path(log.target_id)
 | 
			
		||||
    when 'Announcement'
 | 
			
		||||
      link_to truncate(record.text), edit_admin_announcement_path(record.id)
 | 
			
		||||
    when 'IpBlock'
 | 
			
		||||
      "#{record.ip}/#{record.ip.prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{record.severity}")})"
 | 
			
		||||
    when 'Instance'
 | 
			
		||||
      record.domain
 | 
			
		||||
      link_to truncate(log.human_identifier), edit_admin_announcement_path(log.target_id)
 | 
			
		||||
    when 'IpBlock', 'Instance', 'CustomEmoji'
 | 
			
		||||
      log.human_identifier
 | 
			
		||||
    when 'CanonicalEmailBlock'
 | 
			
		||||
      content_tag(:samp, log.human_identifier[0...7], title: log.human_identifier)
 | 
			
		||||
    when 'Appeal'
 | 
			
		||||
      link_to record.account.acct, disputes_strike_path(record.strike)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def log_target_from_history(type, attributes)
 | 
			
		||||
    case type
 | 
			
		||||
    when 'User'
 | 
			
		||||
      attributes['username']
 | 
			
		||||
    when 'CustomEmoji'
 | 
			
		||||
      attributes['shortcode']
 | 
			
		||||
    when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'
 | 
			
		||||
      link_to attributes['domain'], "https://#{attributes['domain']}"
 | 
			
		||||
    when 'Status'
 | 
			
		||||
      tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count'))
 | 
			
		||||
 | 
			
		||||
      if tmp_status.account
 | 
			
		||||
        link_to tmp_status.account&.acct || "##{tmp_status.account_id}", admin_account_path(tmp_status.account_id)
 | 
			
		||||
      else
 | 
			
		||||
        I18n.t('admin.action_logs.deleted_status')
 | 
			
		||||
      end
 | 
			
		||||
    when 'Announcement'
 | 
			
		||||
      truncate(attributes['text'].is_a?(Array) ? attributes['text'].last : attributes['text'])
 | 
			
		||||
    when 'IpBlock'
 | 
			
		||||
      "#{attributes['ip']}/#{attributes['ip'].prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{attributes['severity']}")})"
 | 
			
		||||
    when 'Instance'
 | 
			
		||||
      attributes['domain']
 | 
			
		||||
      link_to log.human_identifier, disputes_strike_path(log.route_param)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,18 +6,71 @@ import ready from '../mastodon/ready';
 | 
			
		|||
 | 
			
		||||
const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
 | 
			
		||||
 | 
			
		||||
const showSelectAll = () => {
 | 
			
		||||
  const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
 | 
			
		||||
  selectAllMatchingElement.classList.add('active');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const hideSelectAll = () => {
 | 
			
		||||
  const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
 | 
			
		||||
  const hiddenField = document.querySelector('#select_all_matching');
 | 
			
		||||
  const selectedMsg = document.querySelector('.batch-table__select-all .selected');
 | 
			
		||||
  const notSelectedMsg = document.querySelector('.batch-table__select-all .not-selected');
 | 
			
		||||
 | 
			
		||||
  selectAllMatchingElement.classList.remove('active');
 | 
			
		||||
  selectedMsg.classList.remove('active');
 | 
			
		||||
  notSelectedMsg.classList.add('active');
 | 
			
		||||
  hiddenField.value = '0';
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
 | 
			
		||||
  const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
 | 
			
		||||
 | 
			
		||||
  [].forEach.call(document.querySelectorAll(batchCheckboxClassName), (content) => {
 | 
			
		||||
    content.checked = target.checked;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  if (selectAllMatchingElement) {
 | 
			
		||||
    if (target.checked) {
 | 
			
		||||
      showSelectAll();
 | 
			
		||||
    } else {
 | 
			
		||||
      hideSelectAll();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
delegate(document, '.batch-table__select-all button', 'click', () => {
 | 
			
		||||
  const hiddenField = document.querySelector('#select_all_matching');
 | 
			
		||||
  const active = hiddenField.value === '1';
 | 
			
		||||
  const selectedMsg = document.querySelector('.batch-table__select-all .selected');
 | 
			
		||||
  const notSelectedMsg = document.querySelector('.batch-table__select-all .not-selected');
 | 
			
		||||
 | 
			
		||||
  if (active) {
 | 
			
		||||
    hiddenField.value = '0';
 | 
			
		||||
    selectedMsg.classList.remove('active');
 | 
			
		||||
    notSelectedMsg.classList.add('active');
 | 
			
		||||
  } else {
 | 
			
		||||
    hiddenField.value = '1';
 | 
			
		||||
    notSelectedMsg.classList.remove('active');
 | 
			
		||||
    selectedMsg.classList.add('active');
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
delegate(document, batchCheckboxClassName, 'change', () => {
 | 
			
		||||
  const checkAllElement = document.querySelector('#batch_checkbox_all');
 | 
			
		||||
  const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
 | 
			
		||||
 | 
			
		||||
  if (checkAllElement) {
 | 
			
		||||
    checkAllElement.checked = [].every.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
 | 
			
		||||
    checkAllElement.indeterminate = !checkAllElement.checked && [].some.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
 | 
			
		||||
 | 
			
		||||
    if (selectAllMatchingElement) {
 | 
			
		||||
      if (checkAllElement.checked) {
 | 
			
		||||
        showSelectAll();
 | 
			
		||||
      } else {
 | 
			
		||||
        hideSelectAll();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@ const TimelineHint = ({ resource, url }) => (
 | 
			
		|||
  <div className='timeline-hint'>
 | 
			
		||||
    <strong><FormattedMessage id='timeline_hint.remote_resource_not_displayed' defaultMessage='{resource} from other servers are not displayed.' values={{ resource }} /></strong>
 | 
			
		||||
    <br />
 | 
			
		||||
    <a href={url} target='_blank'><FormattedMessage id='account.browse_more_on_origin_server' defaultMessage='Browse more on the original profile' /></a>
 | 
			
		||||
    <a href={url} target='_blank' rel='noopener'><FormattedMessage id='account.browse_more_on_origin_server' defaultMessage='Browse more on the original profile' /></a>
 | 
			
		||||
  </div>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,9 @@
 | 
			
		|||
import * as registerPushNotifications from './actions/push_notifications';
 | 
			
		||||
import { setupBrowserNotifications } from './actions/notifications';
 | 
			
		||||
import { default as Mastodon, store } from './containers/mastodon';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import ReactDOM from 'react-dom';
 | 
			
		||||
import ready from './ready';
 | 
			
		||||
import * as registerPushNotifications from 'mastodon/actions/push_notifications';
 | 
			
		||||
import { setupBrowserNotifications } from 'mastodon/actions/notifications';
 | 
			
		||||
import Mastodon, { store } from 'mastodon/containers/mastodon';
 | 
			
		||||
import ready from 'mastodon/ready';
 | 
			
		||||
 | 
			
		||||
const perf = require('./performance');
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -24,10 +24,20 @@ function main() {
 | 
			
		|||
 | 
			
		||||
    ReactDOM.render(<Mastodon {...props} />, mountNode);
 | 
			
		||||
    store.dispatch(setupBrowserNotifications());
 | 
			
		||||
    if (process.env.NODE_ENV === 'production') {
 | 
			
		||||
      // avoid offline in dev mode because it's harder to debug
 | 
			
		||||
      require('offline-plugin/runtime').install();
 | 
			
		||||
      store.dispatch(registerPushNotifications.register());
 | 
			
		||||
 | 
			
		||||
    if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
 | 
			
		||||
      import('workbox-window')
 | 
			
		||||
        .then(({ Workbox }) => {
 | 
			
		||||
          const wb = new Workbox('/sw.js');
 | 
			
		||||
 | 
			
		||||
          return wb.register();
 | 
			
		||||
        })
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          store.dispatch(registerPushNotifications.register());
 | 
			
		||||
        })
 | 
			
		||||
        .catch(err => {
 | 
			
		||||
          console.error(err);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    perf.stop('main()');
 | 
			
		||||
  });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,20 +1,59 @@
 | 
			
		|||
// import { freeStorage, storageFreeable } from '../storage/modifier';
 | 
			
		||||
import './web_push_notifications';
 | 
			
		||||
import { ExpirationPlugin } from 'workbox-expiration';
 | 
			
		||||
import { precacheAndRoute } from 'workbox-precaching';
 | 
			
		||||
import { registerRoute } from 'workbox-routing';
 | 
			
		||||
import { CacheFirst } from 'workbox-strategies';
 | 
			
		||||
import { handleNotificationClick, handlePush } from './web_push_notifications';
 | 
			
		||||
 | 
			
		||||
// function openSystemCache() {
 | 
			
		||||
//   return caches.open('mastodon-system');
 | 
			
		||||
// }
 | 
			
		||||
const CACHE_NAME_PREFIX = 'mastodon-';
 | 
			
		||||
 | 
			
		||||
function openWebCache() {
 | 
			
		||||
  return caches.open('mastodon-web');
 | 
			
		||||
  return caches.open(`${CACHE_NAME_PREFIX}web`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function fetchRoot() {
 | 
			
		||||
  return fetch('/', { credentials: 'include', redirect: 'manual' });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// const firefox = navigator.userAgent.match(/Firefox\/(\d+)/);
 | 
			
		||||
// const invalidOnlyIfCached = firefox && firefox[1] < 60;
 | 
			
		||||
precacheAndRoute(self.__WB_MANIFEST);
 | 
			
		||||
 | 
			
		||||
registerRoute(
 | 
			
		||||
  /locale_.*\.js$/,
 | 
			
		||||
  new CacheFirst({
 | 
			
		||||
    cacheName: `${CACHE_NAME_PREFIX}locales`,
 | 
			
		||||
    plugins: [
 | 
			
		||||
      new ExpirationPlugin({
 | 
			
		||||
        maxAgeSeconds: 30 * 24 * 60 * 60, // 1 month
 | 
			
		||||
        maxEntries: 5,
 | 
			
		||||
      }),
 | 
			
		||||
    ],
 | 
			
		||||
  }),
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
registerRoute(
 | 
			
		||||
  ({ request }) => request.destination === 'font',
 | 
			
		||||
  new CacheFirst({
 | 
			
		||||
    cacheName: `${CACHE_NAME_PREFIX}fonts`,
 | 
			
		||||
    plugins: [
 | 
			
		||||
      new ExpirationPlugin({
 | 
			
		||||
        maxAgeSeconds: 30 * 24 * 60 * 60, // 1 month
 | 
			
		||||
        maxEntries: 5,
 | 
			
		||||
      }),
 | 
			
		||||
    ],
 | 
			
		||||
  }),
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
registerRoute(
 | 
			
		||||
  ({ request }) => ['audio', 'image', 'track', 'video'].includes(request.destination),
 | 
			
		||||
  new CacheFirst({
 | 
			
		||||
    cacheName: `m${CACHE_NAME_PREFIX}media`,
 | 
			
		||||
    plugins: [
 | 
			
		||||
      new ExpirationPlugin({
 | 
			
		||||
        maxAgeSeconds: 7 * 24 * 60 * 60, // 1 week
 | 
			
		||||
        maxEntries: 256,
 | 
			
		||||
      }),
 | 
			
		||||
    ],
 | 
			
		||||
  }),
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
// Cause a new version of a registered Service Worker to replace an existing one
 | 
			
		||||
// that is already installed, and replace the currently active worker on open pages.
 | 
			
		||||
| 
						 | 
				
			
			@ -52,26 +91,8 @@ self.addEventListener('fetch', function(event) {
 | 
			
		|||
 | 
			
		||||
      return response;
 | 
			
		||||
    }));
 | 
			
		||||
  } /* else if (storageFreeable && (ATTACHMENT_HOST ? url.host === ATTACHMENT_HOST : url.pathname.startsWith('/system/'))) {
 | 
			
		||||
    event.respondWith(openSystemCache().then(cache => {
 | 
			
		||||
      return cache.match(event.request.url).then(cached => {
 | 
			
		||||
        if (cached === undefined) {
 | 
			
		||||
          const asyncResponse = invalidOnlyIfCached && event.request.cache === 'only-if-cached' ?
 | 
			
		||||
            fetch(event.request, { cache: 'no-cache' }) : fetch(event.request);
 | 
			
		||||
 | 
			
		||||
          return asyncResponse.then(response => {
 | 
			
		||||
            if (response.ok) {
 | 
			
		||||
              cache
 | 
			
		||||
                .put(event.request.url, response.clone())
 | 
			
		||||
                .catch(()=>{}).then(freeStorage()).catch();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return response;
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return cached;
 | 
			
		||||
      });
 | 
			
		||||
    }));
 | 
			
		||||
  } */
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
self.addEventListener('push', handlePush);
 | 
			
		||||
self.addEventListener('notificationclick', handleNotificationClick);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -75,7 +75,7 @@ const formatMessage = (messageId, locale, values = {}) =>
 | 
			
		|||
const htmlToPlainText = html =>
 | 
			
		||||
  unescape(html.replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n').replace(/<[^>]*>/g, ''));
 | 
			
		||||
 | 
			
		||||
const handlePush = (event) => {
 | 
			
		||||
export const handlePush = (event) => {
 | 
			
		||||
  const { access_token, notification_id, preferred_locale, title, body, icon } = event.data.json();
 | 
			
		||||
 | 
			
		||||
  // Placeholder until more information can be loaded
 | 
			
		||||
| 
						 | 
				
			
			@ -189,7 +189,7 @@ const openUrl = url =>
 | 
			
		|||
    return self.clients.openWindow(url);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
const handleNotificationClick = (event) => {
 | 
			
		||||
export const handleNotificationClick = (event) => {
 | 
			
		||||
  const reactToNotificationClick = new Promise((resolve, reject) => {
 | 
			
		||||
    if (event.action) {
 | 
			
		||||
      if (event.action === 'expand') {
 | 
			
		||||
| 
						 | 
				
			
			@ -211,6 +211,3 @@ const handleNotificationClick = (event) => {
 | 
			
		|||
 | 
			
		||||
  event.waitUntil(reactToNotificationClick);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
self.addEventListener('push', handlePush);
 | 
			
		||||
self.addEventListener('notificationclick', handleNotificationClick);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,27 +0,0 @@
 | 
			
		|||
export default () => new Promise((resolve, reject) => {
 | 
			
		||||
  // ServiceWorker is required to synchronize the login state.
 | 
			
		||||
  // Microsoft Edge 17 does not support getAll according to:
 | 
			
		||||
  // Catalog of standard and vendor APIs across browsers - Microsoft Edge Development
 | 
			
		||||
  // https://developer.microsoft.com/en-us/microsoft-edge/platform/catalog/?q=specName%3Aindexeddb
 | 
			
		||||
  if (!('caches' in self && 'getAll' in IDBObjectStore.prototype)) {
 | 
			
		||||
    reject();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const request = indexedDB.open('mastodon');
 | 
			
		||||
 | 
			
		||||
  request.onerror = reject;
 | 
			
		||||
  request.onsuccess = ({ target }) => resolve(target.result);
 | 
			
		||||
 | 
			
		||||
  request.onupgradeneeded = ({ target }) => {
 | 
			
		||||
    const accounts = target.result.createObjectStore('accounts', { autoIncrement: true });
 | 
			
		||||
    const statuses = target.result.createObjectStore('statuses', { autoIncrement: true });
 | 
			
		||||
 | 
			
		||||
    accounts.createIndex('id', 'id', { unique: true });
 | 
			
		||||
    accounts.createIndex('moved', 'moved');
 | 
			
		||||
 | 
			
		||||
    statuses.createIndex('id', 'id', { unique: true });
 | 
			
		||||
    statuses.createIndex('account', 'account');
 | 
			
		||||
    statuses.createIndex('reblog', 'reblog');
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,211 +0,0 @@
 | 
			
		|||
import openDB from './db';
 | 
			
		||||
 | 
			
		||||
const accountAssetKeys = ['avatar', 'avatar_static', 'header', 'header_static'];
 | 
			
		||||
const storageMargin = 8388608;
 | 
			
		||||
const storeLimit = 1024;
 | 
			
		||||
 | 
			
		||||
// navigator.storage is not present on:
 | 
			
		||||
// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.100 Safari/537.36 Edge/16.16299
 | 
			
		||||
// estimate method is not present on Chrome 57.0.2987.98 on Linux.
 | 
			
		||||
export const storageFreeable = 'storage' in navigator && 'estimate' in navigator.storage;
 | 
			
		||||
 | 
			
		||||
function openCache() {
 | 
			
		||||
  // ServiceWorker and Cache API is not available on iOS 11
 | 
			
		||||
  // https://webkit.org/status/#specification-service-workers
 | 
			
		||||
  return self.caches ? caches.open('mastodon-system') : Promise.reject();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function printErrorIfAvailable(error) {
 | 
			
		||||
  if (error) {
 | 
			
		||||
    console.warn(error);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function put(name, objects, onupdate, oncreate) {
 | 
			
		||||
  return openDB().then(db => (new Promise((resolve, reject) => {
 | 
			
		||||
    const putTransaction = db.transaction(name, 'readwrite');
 | 
			
		||||
    const putStore = putTransaction.objectStore(name);
 | 
			
		||||
    const putIndex = putStore.index('id');
 | 
			
		||||
 | 
			
		||||
    objects.forEach(object => {
 | 
			
		||||
      putIndex.getKey(object.id).onsuccess = retrieval => {
 | 
			
		||||
        function addObject() {
 | 
			
		||||
          putStore.add(object);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function deleteObject() {
 | 
			
		||||
          putStore.delete(retrieval.target.result).onsuccess = addObject;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (retrieval.target.result) {
 | 
			
		||||
          if (onupdate) {
 | 
			
		||||
            onupdate(object, retrieval.target.result, putStore, deleteObject);
 | 
			
		||||
          } else {
 | 
			
		||||
            deleteObject();
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          if (oncreate) {
 | 
			
		||||
            oncreate(object, addObject);
 | 
			
		||||
          } else {
 | 
			
		||||
            addObject();
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    putTransaction.oncomplete = () => {
 | 
			
		||||
      const readTransaction = db.transaction(name, 'readonly');
 | 
			
		||||
      const readStore = readTransaction.objectStore(name);
 | 
			
		||||
      const count = readStore.count();
 | 
			
		||||
 | 
			
		||||
      count.onsuccess = () => {
 | 
			
		||||
        const excess = count.result - storeLimit;
 | 
			
		||||
 | 
			
		||||
        if (excess > 0) {
 | 
			
		||||
          const retrieval = readStore.getAll(null, excess);
 | 
			
		||||
 | 
			
		||||
          retrieval.onsuccess = () => resolve(retrieval.result);
 | 
			
		||||
          retrieval.onerror = reject;
 | 
			
		||||
        } else {
 | 
			
		||||
          resolve([]);
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      count.onerror = reject;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    putTransaction.onerror = reject;
 | 
			
		||||
  })).then(resolved => {
 | 
			
		||||
    db.close();
 | 
			
		||||
    return resolved;
 | 
			
		||||
  }, error => {
 | 
			
		||||
    db.close();
 | 
			
		||||
    throw error;
 | 
			
		||||
  }));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function evictAccountsByRecords(records) {
 | 
			
		||||
  return openDB().then(db => {
 | 
			
		||||
    const transaction = db.transaction(['accounts', 'statuses'], 'readwrite');
 | 
			
		||||
    const accounts = transaction.objectStore('accounts');
 | 
			
		||||
    const accountsIdIndex = accounts.index('id');
 | 
			
		||||
    const accountsMovedIndex = accounts.index('moved');
 | 
			
		||||
    const statuses = transaction.objectStore('statuses');
 | 
			
		||||
    const statusesIndex = statuses.index('account');
 | 
			
		||||
 | 
			
		||||
    function evict(toEvict) {
 | 
			
		||||
      toEvict.forEach(record => {
 | 
			
		||||
        openCache()
 | 
			
		||||
          .then(cache => accountAssetKeys.forEach(key => cache.delete(records[key])))
 | 
			
		||||
          .catch(printErrorIfAvailable);
 | 
			
		||||
 | 
			
		||||
        accountsMovedIndex.getAll(record.id).onsuccess = ({ target }) => evict(target.result);
 | 
			
		||||
 | 
			
		||||
        statusesIndex.getAll(record.id).onsuccess =
 | 
			
		||||
          ({ target }) => evictStatusesByRecords(target.result);
 | 
			
		||||
 | 
			
		||||
        accountsIdIndex.getKey(record.id).onsuccess =
 | 
			
		||||
          ({ target }) => target.result && accounts.delete(target.result);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    evict(records);
 | 
			
		||||
 | 
			
		||||
    db.close();
 | 
			
		||||
  }).catch(printErrorIfAvailable);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function evictStatus(id) {
 | 
			
		||||
  evictStatuses([id]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function evictStatuses(ids) {
 | 
			
		||||
  return openDB().then(db => {
 | 
			
		||||
    const transaction = db.transaction('statuses', 'readwrite');
 | 
			
		||||
    const store = transaction.objectStore('statuses');
 | 
			
		||||
    const idIndex = store.index('id');
 | 
			
		||||
    const reblogIndex = store.index('reblog');
 | 
			
		||||
 | 
			
		||||
    ids.forEach(id => {
 | 
			
		||||
      reblogIndex.getAllKeys(id).onsuccess =
 | 
			
		||||
        ({ target }) => target.result.forEach(reblogKey => store.delete(reblogKey));
 | 
			
		||||
 | 
			
		||||
      idIndex.getKey(id).onsuccess =
 | 
			
		||||
        ({ target }) => target.result && store.delete(target.result);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    db.close();
 | 
			
		||||
  }).catch(printErrorIfAvailable);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function evictStatusesByRecords(records) {
 | 
			
		||||
  return evictStatuses(records.map(({ id }) => id));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function putAccounts(records, avatarStatic) {
 | 
			
		||||
  const avatarKey = avatarStatic ? 'avatar_static' : 'avatar';
 | 
			
		||||
  const newURLs = [];
 | 
			
		||||
 | 
			
		||||
  put('accounts', records, (newRecord, oldKey, store, oncomplete) => {
 | 
			
		||||
    store.get(oldKey).onsuccess = ({ target }) => {
 | 
			
		||||
      accountAssetKeys.forEach(key => {
 | 
			
		||||
        const newURL = newRecord[key];
 | 
			
		||||
        const oldURL = target.result[key];
 | 
			
		||||
 | 
			
		||||
        if (newURL !== oldURL) {
 | 
			
		||||
          openCache()
 | 
			
		||||
            .then(cache => cache.delete(oldURL))
 | 
			
		||||
            .catch(printErrorIfAvailable);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const newURL = newRecord[avatarKey];
 | 
			
		||||
      const oldURL = target.result[avatarKey];
 | 
			
		||||
 | 
			
		||||
      if (newURL !== oldURL) {
 | 
			
		||||
        newURLs.push(newURL);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      oncomplete();
 | 
			
		||||
    };
 | 
			
		||||
  }, (newRecord, oncomplete) => {
 | 
			
		||||
    newURLs.push(newRecord[avatarKey]);
 | 
			
		||||
    oncomplete();
 | 
			
		||||
  }).then(records => Promise.all([
 | 
			
		||||
    evictAccountsByRecords(records),
 | 
			
		||||
    openCache().then(cache => cache.addAll(newURLs)),
 | 
			
		||||
  ])).then(freeStorage, error => {
 | 
			
		||||
    freeStorage();
 | 
			
		||||
    throw error;
 | 
			
		||||
  }).catch(printErrorIfAvailable);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function putStatuses(records) {
 | 
			
		||||
  put('statuses', records)
 | 
			
		||||
    .then(evictStatusesByRecords)
 | 
			
		||||
    .catch(printErrorIfAvailable);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function freeStorage() {
 | 
			
		||||
  return storageFreeable && navigator.storage.estimate().then(({ quota, usage }) => {
 | 
			
		||||
    if (usage + storageMargin < quota) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return openDB().then(db => new Promise((resolve, reject) => {
 | 
			
		||||
      const retrieval = db.transaction('accounts', 'readonly').objectStore('accounts').getAll(null, 1);
 | 
			
		||||
 | 
			
		||||
      retrieval.onsuccess = () => {
 | 
			
		||||
        if (retrieval.result.length > 0) {
 | 
			
		||||
          resolve(evictAccountsByRecords(retrieval.result).then(freeStorage));
 | 
			
		||||
        } else {
 | 
			
		||||
          resolve(caches.delete('mastodon-system'));
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      retrieval.onerror = reject;
 | 
			
		||||
 | 
			
		||||
      db.close();
 | 
			
		||||
    }));
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -190,6 +190,55 @@ a.table-action-link {
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__select-all {
 | 
			
		||||
    background: $ui-base-color;
 | 
			
		||||
    height: 47px;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    border: 1px solid darken($ui-base-color, 8%);
 | 
			
		||||
    border-top: 0;
 | 
			
		||||
    color: $secondary-text-color;
 | 
			
		||||
    display: none;
 | 
			
		||||
 | 
			
		||||
    &.active {
 | 
			
		||||
      display: flex;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .selected,
 | 
			
		||||
    .not-selected {
 | 
			
		||||
      display: none;
 | 
			
		||||
 | 
			
		||||
      &.active {
 | 
			
		||||
        display: block;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    strong {
 | 
			
		||||
      font-weight: 700;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    span {
 | 
			
		||||
      padding: 8px;
 | 
			
		||||
      display: inline-block;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    button {
 | 
			
		||||
      background: transparent;
 | 
			
		||||
      border: 0;
 | 
			
		||||
      font: inherit;
 | 
			
		||||
      color: $highlight-text-color;
 | 
			
		||||
      border-radius: 4px;
 | 
			
		||||
      font-weight: 700;
 | 
			
		||||
      padding: 8px;
 | 
			
		||||
 | 
			
		||||
      &:hover,
 | 
			
		||||
      &:focus,
 | 
			
		||||
      &:active {
 | 
			
		||||
        background: lighten($ui-base-color, 8%);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__form {
 | 
			
		||||
    padding: 16px;
 | 
			
		||||
    border: 1px solid darken($ui-base-color, 8%);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -60,7 +60,7 @@ class FeedManager
 | 
			
		|||
  # @param [Boolean] update
 | 
			
		||||
  # @return [Boolean]
 | 
			
		||||
  def push_to_home(account, status, update: false)
 | 
			
		||||
    return false unless add_to_feed(:home, account.id, status, aggregate_reblogs: account.user&.aggregates_reblogs?)
 | 
			
		||||
    return false unless add_to_feed(:home, account.id, status, aggregate_reblogs: true)
 | 
			
		||||
 | 
			
		||||
    trim(:home, account.id)
 | 
			
		||||
    PushUpdateWorker.perform_async(account.id, status.id, "timeline:#{account.id}", { 'update' => update }) if push_update_required?("timeline:#{account.id}")
 | 
			
		||||
| 
						 | 
				
			
			@ -73,7 +73,7 @@ class FeedManager
 | 
			
		|||
  # @param [Boolean] update
 | 
			
		||||
  # @return [Boolean]
 | 
			
		||||
  def unpush_from_home(account, status, update: false)
 | 
			
		||||
    return false unless remove_from_feed(:home, account.id, status, aggregate_reblogs: account.user&.aggregates_reblogs?)
 | 
			
		||||
    return false unless remove_from_feed(:home, account.id, status, aggregate_reblogs: true)
 | 
			
		||||
 | 
			
		||||
    redis.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s)) unless update
 | 
			
		||||
    true
 | 
			
		||||
| 
						 | 
				
			
			@ -85,7 +85,7 @@ class FeedManager
 | 
			
		|||
  # @param [Boolean] update
 | 
			
		||||
  # @return [Boolean]
 | 
			
		||||
  def push_to_list(list, status, update: false)
 | 
			
		||||
    return false if filter_from_list?(status, list) || !add_to_feed(:list, list.id, status, aggregate_reblogs: list.account.user&.aggregates_reblogs?)
 | 
			
		||||
    return false if filter_from_list?(status, list) || !add_to_feed(:list, list.id, status, aggregate_reblogs: true)
 | 
			
		||||
 | 
			
		||||
    trim(:list, list.id)
 | 
			
		||||
    PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}", { 'update' => update }) if push_update_required?("timeline:list:#{list.id}")
 | 
			
		||||
| 
						 | 
				
			
			@ -98,7 +98,7 @@ class FeedManager
 | 
			
		|||
  # @param [Boolean] update
 | 
			
		||||
  # @return [Boolean]
 | 
			
		||||
  def unpush_from_list(list, status, update: false)
 | 
			
		||||
    return false unless remove_from_feed(:list, list.id, status, aggregate_reblogs: list.account.user&.aggregates_reblogs?)
 | 
			
		||||
    return false unless remove_from_feed(:list, list.id, status, aggregate_reblogs: true)
 | 
			
		||||
 | 
			
		||||
    redis.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s)) unless update
 | 
			
		||||
    true
 | 
			
		||||
| 
						 | 
				
			
			@ -133,7 +133,7 @@ class FeedManager
 | 
			
		|||
  # @return [void]
 | 
			
		||||
  def merge_into_home(from_account, into_account)
 | 
			
		||||
    timeline_key = key(:home, into_account.id)
 | 
			
		||||
    aggregate    = into_account.user&.aggregates_reblogs?
 | 
			
		||||
    aggregate    = true
 | 
			
		||||
    query        = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
 | 
			
		||||
 | 
			
		||||
    if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4
 | 
			
		||||
| 
						 | 
				
			
			@ -159,7 +159,7 @@ class FeedManager
 | 
			
		|||
  # @return [void]
 | 
			
		||||
  def merge_into_list(from_account, list)
 | 
			
		||||
    timeline_key = key(:list, list.id)
 | 
			
		||||
    aggregate    = list.account.user&.aggregates_reblogs?
 | 
			
		||||
    aggregate    = true
 | 
			
		||||
    query        = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
 | 
			
		||||
 | 
			
		||||
    if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4
 | 
			
		||||
| 
						 | 
				
			
			@ -188,7 +188,7 @@ class FeedManager
 | 
			
		|||
    timeline_status_ids = redis.zrange(timeline_key, 0, -1)
 | 
			
		||||
 | 
			
		||||
    from_account.statuses.select('id, reblog_of_id').where(id: timeline_status_ids).reorder(nil).find_each do |status|
 | 
			
		||||
      remove_from_feed(:home, into_account.id, status, aggregate_reblogs: into_account.user&.aggregates_reblogs?)
 | 
			
		||||
      remove_from_feed(:home, into_account.id, status, aggregate_reblogs: true)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -201,7 +201,7 @@ class FeedManager
 | 
			
		|||
    timeline_status_ids = redis.zrange(timeline_key, 0, -1)
 | 
			
		||||
 | 
			
		||||
    from_account.statuses.select('id, reblog_of_id').where(id: timeline_status_ids).reorder(nil).find_each do |status|
 | 
			
		||||
      remove_from_feed(:list, list.id, status, aggregate_reblogs: list.account.user&.aggregates_reblogs?)
 | 
			
		||||
      remove_from_feed(:list, list.id, status, aggregate_reblogs: true)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -260,7 +260,7 @@ class FeedManager
 | 
			
		|||
  # @return [void]
 | 
			
		||||
  def populate_home(account)
 | 
			
		||||
    limit        = FeedManager::MAX_ITEMS / 2
 | 
			
		||||
    aggregate    = account.user&.aggregates_reblogs?
 | 
			
		||||
    aggregate    = true
 | 
			
		||||
    timeline_key = key(:home, account.id)
 | 
			
		||||
 | 
			
		||||
    account.statuses.limit(limit).each do |status|
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -66,24 +66,6 @@ class NotificationMailer < ApplicationMailer
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def digest(recipient, **opts)
 | 
			
		||||
    return unless recipient.user.functional?
 | 
			
		||||
 | 
			
		||||
    @me                  = recipient
 | 
			
		||||
    @since               = opts[:since] || [@me.user.last_emailed_at, (@me.user.current_sign_in_at + 1.day)].compact.max
 | 
			
		||||
    @notifications_count = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since).count
 | 
			
		||||
 | 
			
		||||
    return if @notifications_count.zero?
 | 
			
		||||
 | 
			
		||||
    @notifications = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since).limit(40)
 | 
			
		||||
    @follows_since = Notification.where(account: @me, activity_type: 'Follow').where('created_at > ?', @since).count
 | 
			
		||||
 | 
			
		||||
    locale_for_account(@me) do
 | 
			
		||||
      mail to: @me.user.email,
 | 
			
		||||
           subject: I18n.t(:subject, scope: [:notification_mailer, :digest], count: @notifications_count)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def thread_by_conversation(conversation)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -364,6 +364,10 @@ class Account < ApplicationRecord
 | 
			
		|||
    username
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def to_log_human_identifier
 | 
			
		||||
    acct
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def excluded_from_timeline_account_ids
 | 
			
		||||
    Rails.cache.fetch("exclude_account_ids_for:#{id}") { block_relationships.pluck(:target_account_id) + blocked_by_relationships.pluck(:account_id) + mute_relationships.pluck(:target_account_id) }
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,4 +43,8 @@ class AccountWarning < ApplicationRecord
 | 
			
		|||
  def overruled?
 | 
			
		||||
    overruled_at.present?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def to_log_human_identifier
 | 
			
		||||
    target_account.acct
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,38 +9,42 @@
 | 
			
		|||
#  action           :string           default(""), not null
 | 
			
		||||
#  target_type      :string
 | 
			
		||||
#  target_id        :bigint(8)
 | 
			
		||||
#  recorded_changes :text             default(""), not null
 | 
			
		||||
#  created_at       :datetime         not null
 | 
			
		||||
#  updated_at       :datetime         not null
 | 
			
		||||
#  human_identifier :string
 | 
			
		||||
#  route_param      :string
 | 
			
		||||
#  permalink        :string
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
class Admin::ActionLog < ApplicationRecord
 | 
			
		||||
  serialize :recorded_changes
 | 
			
		||||
  self.ignored_columns = %w(
 | 
			
		||||
    recorded_changes
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  belongs_to :account
 | 
			
		||||
  belongs_to :target, polymorphic: true, optional: true
 | 
			
		||||
 | 
			
		||||
  default_scope -> { order('id desc') }
 | 
			
		||||
 | 
			
		||||
  before_validation :set_human_identifier
 | 
			
		||||
  before_validation :set_route_param
 | 
			
		||||
  before_validation :set_permalink
 | 
			
		||||
 | 
			
		||||
  def action
 | 
			
		||||
    super.to_sym
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  before_validation :set_changes
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def set_changes
 | 
			
		||||
    case action
 | 
			
		||||
    when :destroy, :create
 | 
			
		||||
      self.recorded_changes = target.attributes
 | 
			
		||||
    when :update, :promote, :demote
 | 
			
		||||
      self.recorded_changes = target.previous_changes
 | 
			
		||||
    when :change_email
 | 
			
		||||
      self.recorded_changes = ActiveSupport::HashWithIndifferentAccess.new(
 | 
			
		||||
        email: [target.email, nil],
 | 
			
		||||
        unconfirmed_email: [nil, target.unconfirmed_email]
 | 
			
		||||
      )
 | 
			
		||||
    end
 | 
			
		||||
  def set_human_identifier
 | 
			
		||||
    self.human_identifier = target.to_log_human_identifier if target.respond_to?(:to_log_human_identifier)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def set_route_param
 | 
			
		||||
    self.route_param = target.to_log_route_param if target.respond_to?(:to_log_route_param)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def set_permalink
 | 
			
		||||
    self.permalink = target.to_log_permalink if target.respond_to?(:to_log_permalink)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ class Admin::ActionLogFilter
 | 
			
		|||
    reject_appeal: { target_type: 'Appeal', action: 'reject' }.freeze,
 | 
			
		||||
    assigned_to_self_report: { target_type: 'Report', action: 'assigned_to_self' }.freeze,
 | 
			
		||||
    change_email_user: { target_type: 'User', action: 'change_email' }.freeze,
 | 
			
		||||
    change_role_user: { target_type: 'User', action: 'change_role' }.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,
 | 
			
		||||
| 
						 | 
				
			
			@ -21,16 +22,22 @@ class Admin::ActionLogFilter
 | 
			
		|||
    create_domain_allow: { target_type: 'DomainAllow', action: 'create' }.freeze,
 | 
			
		||||
    create_domain_block: { target_type: 'DomainBlock', action: 'create' }.freeze,
 | 
			
		||||
    create_email_domain_block: { target_type: 'EmailDomainBlock', action: 'create' }.freeze,
 | 
			
		||||
    create_ip_block: { target_type: 'IpBlock', action: 'create' }.freeze,
 | 
			
		||||
    create_unavailable_domain: { target_type: 'UnavailableDomain', action: 'create' }.freeze,
 | 
			
		||||
    create_user_role: { target_type: 'UserRole', action: 'create' }.freeze,
 | 
			
		||||
    create_canonical_email_block: { target_type: 'CanonicalEmailBlock', action: 'create' }.freeze,
 | 
			
		||||
    demote_user: { target_type: 'User', action: 'demote' }.freeze,
 | 
			
		||||
    destroy_announcement: { target_type: 'Announcement', action: 'destroy' }.freeze,
 | 
			
		||||
    destroy_custom_emoji: { target_type: 'CustomEmoji', action: 'destroy' }.freeze,
 | 
			
		||||
    destroy_domain_allow: { target_type: 'DomainAllow', action: 'destroy' }.freeze,
 | 
			
		||||
    destroy_domain_block: { target_type: 'DomainBlock', action: 'destroy' }.freeze,
 | 
			
		||||
    destroy_ip_block: { target_type: 'IpBlock', action: 'destroy' }.freeze,
 | 
			
		||||
    destroy_email_domain_block: { target_type: 'EmailDomainBlock', action: 'destroy' }.freeze,
 | 
			
		||||
    destroy_instance: { target_type: 'Instance', action: 'destroy' }.freeze,
 | 
			
		||||
    destroy_unavailable_domain: { target_type: 'UnavailableDomain', action: 'destroy' }.freeze,
 | 
			
		||||
    destroy_status: { target_type: 'Status', action: 'destroy' }.freeze,
 | 
			
		||||
    destroy_user_role: { target_type: 'UserRole', action: 'destroy' }.freeze,
 | 
			
		||||
    destroy_canonical_email_block: { target_type: 'CanonicalEmailBlock', action: 'destroy' }.freeze,
 | 
			
		||||
    disable_2fa_user: { target_type: 'User', action: 'disable' }.freeze,
 | 
			
		||||
    disable_custom_emoji: { target_type: 'CustomEmoji', action: 'disable' }.freeze,
 | 
			
		||||
    disable_user: { target_type: 'User', action: 'disable' }.freeze,
 | 
			
		||||
| 
						 | 
				
			
			@ -52,6 +59,8 @@ class Admin::ActionLogFilter
 | 
			
		|||
    update_announcement: { target_type: 'Announcement', action: 'update' }.freeze,
 | 
			
		||||
    update_custom_emoji: { target_type: 'CustomEmoji', action: 'update' }.freeze,
 | 
			
		||||
    update_status: { target_type: 'Status', action: 'update' }.freeze,
 | 
			
		||||
    update_user_role: { target_type: 'UserRole', action: 'update' }.freeze,
 | 
			
		||||
    update_ip_block: { target_type: 'IpBlock', action: 'update' }.freeze,
 | 
			
		||||
    unblock_email_account: { target_type: 'Account', action: 'unblock_email' }.freeze,
 | 
			
		||||
  }.freeze
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,6 +34,10 @@ class Announcement < ApplicationRecord
 | 
			
		|||
  before_validation :set_all_day
 | 
			
		||||
  before_validation :set_published, on: :create
 | 
			
		||||
 | 
			
		||||
  def to_log_human_identifier
 | 
			
		||||
    text
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def publish!
 | 
			
		||||
    update!(published: true, published_at: Time.now.utc, scheduled_at: nil)
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -52,6 +52,14 @@ class Appeal < ApplicationRecord
 | 
			
		|||
    update!(rejected_at: Time.now.utc, rejected_by_account: current_account)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def to_log_human_identifier
 | 
			
		||||
    account.acct
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def to_log_route_param
 | 
			
		||||
    account_warning_id
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def validate_time_frame
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,27 +5,30 @@
 | 
			
		|||
#
 | 
			
		||||
#  id                   :bigint(8)        not null, primary key
 | 
			
		||||
#  canonical_email_hash :string           default(""), not null
 | 
			
		||||
#  reference_account_id :bigint(8)        not null
 | 
			
		||||
#  reference_account_id :bigint(8)
 | 
			
		||||
#  created_at           :datetime         not null
 | 
			
		||||
#  updated_at           :datetime         not null
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
class CanonicalEmailBlock < ApplicationRecord
 | 
			
		||||
  include EmailHelper
 | 
			
		||||
  include Paginable
 | 
			
		||||
 | 
			
		||||
  belongs_to :reference_account, class_name: 'Account'
 | 
			
		||||
  belongs_to :reference_account, class_name: 'Account', optional: true
 | 
			
		||||
 | 
			
		||||
  validates :canonical_email_hash, presence: true, uniqueness: true
 | 
			
		||||
 | 
			
		||||
  scope :matching_email, ->(email) { where(canonical_email_hash: email_to_canonical_email_hash(email)) }
 | 
			
		||||
 | 
			
		||||
  def to_log_human_identifier
 | 
			
		||||
    canonical_email_hash
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def email=(email)
 | 
			
		||||
    self.canonical_email_hash = email_to_canonical_email_hash(email)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def self.block?(email)
 | 
			
		||||
    where(canonical_email_hash: email_to_canonical_email_hash(email)).exists?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def self.find_blocks(email)
 | 
			
		||||
    where(canonical_email_hash: email_to_canonical_email_hash(email))
 | 
			
		||||
    matching_email(email).exists?
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -49,7 +49,7 @@ class CustomEmoji < ApplicationRecord
 | 
			
		|||
  scope :local, -> { where(domain: nil) }
 | 
			
		||||
  scope :remote, -> { where.not(domain: nil) }
 | 
			
		||||
  scope :alphabetic, -> { order(domain: :asc, shortcode: :asc) }
 | 
			
		||||
  scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) }
 | 
			
		||||
  scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches("%.#{domain}"))) }
 | 
			
		||||
  scope :listed, -> { local.where(disabled: false).where(visible_in_picker: true) }
 | 
			
		||||
 | 
			
		||||
  remotable_attachment :image, LIMIT
 | 
			
		||||
| 
						 | 
				
			
			@ -70,6 +70,10 @@ class CustomEmoji < ApplicationRecord
 | 
			
		|||
    copy.tap(&:save!)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def to_log_human_identifier
 | 
			
		||||
    shortcode
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  class << self
 | 
			
		||||
    def from_text(text, domain = nil)
 | 
			
		||||
      return [] if text.blank?
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,7 @@
 | 
			
		|||
#
 | 
			
		||||
#  id               :bigint(8)        not null, primary key
 | 
			
		||||
#  custom_filter_id :bigint(8)        not null
 | 
			
		||||
#  status_id        :bigint(8)        default(""), not null
 | 
			
		||||
#  status_id        :bigint(8)        not null
 | 
			
		||||
#  created_at       :datetime         not null
 | 
			
		||||
#  updated_at       :datetime         not null
 | 
			
		||||
#
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,6 +19,10 @@ class DomainAllow < ApplicationRecord
 | 
			
		|||
 | 
			
		||||
  scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
 | 
			
		||||
 | 
			
		||||
  def to_log_human_identifier
 | 
			
		||||
    domain
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  class << self
 | 
			
		||||
    def allowed?(domain)
 | 
			
		||||
      !rule_for(domain).nil?
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,6 +31,10 @@ class DomainBlock < ApplicationRecord
 | 
			
		|||
  scope :with_user_facing_limitations, -> { where(severity: [:silence, :suspend]).or(where(reject_media: true)) }
 | 
			
		||||
  scope :by_severity, -> { order(Arel.sql('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), reject_media, domain')) }
 | 
			
		||||
 | 
			
		||||
  def to_log_human_identifier
 | 
			
		||||
    domain
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def policies
 | 
			
		||||
    if suspend?
 | 
			
		||||
      [:suspend]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,6 +17,7 @@ class EmailDomainBlock < ApplicationRecord
 | 
			
		|||
  )
 | 
			
		||||
 | 
			
		||||
  include DomainNormalizable
 | 
			
		||||
  include Paginable
 | 
			
		||||
 | 
			
		||||
  belongs_to :parent, class_name: 'EmailDomainBlock', optional: true
 | 
			
		||||
  has_many :children, class_name: 'EmailDomainBlock', foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy
 | 
			
		||||
| 
						 | 
				
			
			@ -26,6 +27,10 @@ class EmailDomainBlock < ApplicationRecord
 | 
			
		|||
  # Used for adding multiple blocks at once
 | 
			
		||||
  attr_accessor :other_domains
 | 
			
		||||
 | 
			
		||||
  def to_log_human_identifier
 | 
			
		||||
    domain
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def history
 | 
			
		||||
    @history ||= Trends::History.new('email_domain_blocks', id)
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,8 @@ class Form::AccountBatch
 | 
			
		|||
  include AccountableConcern
 | 
			
		||||
  include Payloadable
 | 
			
		||||
 | 
			
		||||
  attr_accessor :account_ids, :action, :current_account
 | 
			
		||||
  attr_accessor :account_ids, :action, :current_account,
 | 
			
		||||
                :select_all_matching, :query
 | 
			
		||||
 | 
			
		||||
  def save
 | 
			
		||||
    case action
 | 
			
		||||
| 
						 | 
				
			
			@ -60,7 +61,11 @@ class Form::AccountBatch
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def accounts
 | 
			
		||||
    Account.where(id: account_ids)
 | 
			
		||||
    if select_all_matching?
 | 
			
		||||
      query
 | 
			
		||||
    else
 | 
			
		||||
      Account.where(id: account_ids)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def approve!
 | 
			
		||||
| 
						 | 
				
			
			@ -101,7 +106,7 @@ class Form::AccountBatch
 | 
			
		|||
 | 
			
		||||
  def reject_account(account)
 | 
			
		||||
    authorize(account.user, :reject?)
 | 
			
		||||
    log_action(:reject, account.user, username: account.username)
 | 
			
		||||
    log_action(:reject, account.user)
 | 
			
		||||
    account.suspend!(origin: :local)
 | 
			
		||||
    AccountDeletionWorker.perform_async(account.id, { 'reserve_username' => false })
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -118,4 +123,8 @@ class Form::AccountBatch
 | 
			
		|||
    log_action(:approve, account.user)
 | 
			
		||||
    account.user.approve!
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def select_all_matching?
 | 
			
		||||
    select_all_matching == '1'
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -48,6 +48,8 @@ class Instance < ApplicationRecord
 | 
			
		|||
    domain
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  alias to_log_human_identifier to_param
 | 
			
		||||
 | 
			
		||||
  delegate :exhausted_deliveries_days, to: :delivery_failure_tracker
 | 
			
		||||
 | 
			
		||||
  def availability_over_days(num_days, end_date = Time.now.utc.to_date)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,6 +16,7 @@ class IpBlock < ApplicationRecord
 | 
			
		|||
  CACHE_KEY = 'blocked_ips'
 | 
			
		||||
 | 
			
		||||
  include Expireable
 | 
			
		||||
  include Paginable
 | 
			
		||||
 | 
			
		||||
  enum severity: {
 | 
			
		||||
    sign_up_requires_approval: 5000,
 | 
			
		||||
| 
						 | 
				
			
			@ -27,6 +28,10 @@ class IpBlock < ApplicationRecord
 | 
			
		|||
 | 
			
		||||
  after_commit :reset_cache
 | 
			
		||||
 | 
			
		||||
  def to_log_human_identifier
 | 
			
		||||
    "#{ip}/#{ip.prefix}"
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  class << self
 | 
			
		||||
    def blocked?(remote_ip)
 | 
			
		||||
      blocked_ips_map = Rails.cache.fetch(CACHE_KEY) { FastIpMap.new(IpBlock.where(severity: :no_access).pluck(:ip)) }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -115,6 +115,10 @@ class Report < ApplicationRecord
 | 
			
		|||
    Report.where.not(id: id).where(target_account_id: target_account_id).unresolved.exists?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def to_log_human_identifier
 | 
			
		||||
    id
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def history
 | 
			
		||||
    subquery = [
 | 
			
		||||
      Admin::ActionLog.where(
 | 
			
		||||
| 
						 | 
				
			
			@ -136,6 +140,8 @@ class Report < ApplicationRecord
 | 
			
		|||
    Admin::ActionLog.from(Arel::Nodes::As.new(subquery, Admin::ActionLog.arel_table))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def set_uri
 | 
			
		||||
    self.uri = ActivityPub::TagManager.instance.generate_uri_for(self) if uri.nil? && account.local?
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -171,6 +171,14 @@ class Status < ApplicationRecord
 | 
			
		|||
    ].compact.join("\n\n")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def to_log_human_identifier
 | 
			
		||||
    account.acct
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def to_log_permalink
 | 
			
		||||
    ActivityPub::TagManager.instance.uri_for(self)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def reply?
 | 
			
		||||
    !in_reply_to_id.nil? || attributes['reply']
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,6 +16,10 @@ class UnavailableDomain < ApplicationRecord
 | 
			
		|||
 | 
			
		||||
  after_commit :reset_cache!
 | 
			
		||||
 | 
			
		||||
  def to_log_human_identifier
 | 
			
		||||
    domain
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def reset_cache!
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -181,6 +181,14 @@ class User < ApplicationRecord
 | 
			
		|||
    update!(disabled: false)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def to_log_human_identifier
 | 
			
		||||
    account.acct
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def to_log_route_param
 | 
			
		||||
    account_id
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def confirm
 | 
			
		||||
    new_user      = !confirmed?
 | 
			
		||||
    self.approved = true if open_registrations? && !sign_up_from_ip_requires_approval?
 | 
			
		||||
| 
						 | 
				
			
			@ -281,10 +289,6 @@ class User < ApplicationRecord
 | 
			
		|||
    settings.default_privacy || (account.locked? ? 'private' : 'public')
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def allows_digest_emails?
 | 
			
		||||
    settings.notification_emails['digest']
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def allows_report_emails?
 | 
			
		||||
    settings.notification_emails['report']
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -155,6 +155,10 @@ class UserRole < ApplicationRecord
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def to_log_human_identifier
 | 
			
		||||
    name
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def in_permissions?(privilege)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										23
									
								
								app/policies/canonical_email_block_policy.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								app/policies/canonical_email_block_policy.rb
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class CanonicalEmailBlockPolicy < ApplicationPolicy
 | 
			
		||||
  def index?
 | 
			
		||||
    role.can?(:manage_blocks)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def show?
 | 
			
		||||
    role.can?(:manage_blocks)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def test?
 | 
			
		||||
    role.can?(:manage_blocks)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def create?
 | 
			
		||||
    role.can?(:manage_blocks)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def destroy?
 | 
			
		||||
    role.can?(:manage_blocks)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -9,6 +9,10 @@ class IpBlockPolicy < ApplicationPolicy
 | 
			
		|||
    role.can?(:manage_blocks)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def update?
 | 
			
		||||
    role.can?(:manage_blocks)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def destroy?
 | 
			
		||||
    role.can?(:manage_blocks)
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class REST::Admin::CanonicalEmailBlockSerializer < ActiveModel::Serializer
 | 
			
		||||
  attributes :id, :canonical_email_hash
 | 
			
		||||
 | 
			
		||||
  def id
 | 
			
		||||
    object.id.to_s
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class REST::Admin::EmailDomainBlockSerializer < ActiveModel::Serializer
 | 
			
		||||
  attributes :id, :domain, :created_at, :history
 | 
			
		||||
 | 
			
		||||
  def id
 | 
			
		||||
    object.id.to_s
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										14
									
								
								app/serializers/rest/admin/ip_block_serializer.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								app/serializers/rest/admin/ip_block_serializer.rb
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class REST::Admin::IpBlockSerializer < ActiveModel::Serializer
 | 
			
		||||
  attributes :id, :ip, :severity, :comment,
 | 
			
		||||
             :created_at, :expires_at
 | 
			
		||||
 | 
			
		||||
  def id
 | 
			
		||||
    object.id.to_s
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def ip
 | 
			
		||||
    "#{object.ip}/#{object.ip.prefix}"
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -10,24 +10,18 @@ class ClearDomainMediaService < BaseService
 | 
			
		|||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def invalidate_association_caches!
 | 
			
		||||
  def invalidate_association_caches!(status_ids)
 | 
			
		||||
    # Normally, associated models of a status are immutable (except for accounts)
 | 
			
		||||
    # so they are aggressively cached. After updating the media attachments to no
 | 
			
		||||
    # longer point to a local file, we need to clear the cache to make those
 | 
			
		||||
    # changes appear in the API and UI
 | 
			
		||||
    @affected_status_ids.each { |id| Rails.cache.delete_matched("statuses/#{id}-*") }
 | 
			
		||||
    Rails.cache.delete_multi(status_ids.map { |id| "statuses/#{id}" })
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def clear_media!
 | 
			
		||||
    @affected_status_ids = []
 | 
			
		||||
 | 
			
		||||
    begin
 | 
			
		||||
      clear_account_images!
 | 
			
		||||
      clear_account_attachments!
 | 
			
		||||
      clear_emojos!
 | 
			
		||||
    ensure
 | 
			
		||||
      invalidate_association_caches!
 | 
			
		||||
    end
 | 
			
		||||
    clear_account_images!
 | 
			
		||||
    clear_account_attachments!
 | 
			
		||||
    clear_emojos!
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def clear_account_images!
 | 
			
		||||
| 
						 | 
				
			
			@ -39,12 +33,18 @@ class ClearDomainMediaService < BaseService
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def clear_account_attachments!
 | 
			
		||||
    media_from_blocked_domain.reorder(nil).find_each do |attachment|
 | 
			
		||||
      @affected_status_ids << attachment.status_id if attachment.status_id.present?
 | 
			
		||||
    media_from_blocked_domain.reorder(nil).find_in_batches do |attachments|
 | 
			
		||||
      affected_status_ids = []
 | 
			
		||||
 | 
			
		||||
      attachment.file.destroy if attachment.file&.exists?
 | 
			
		||||
      attachment.type = :unknown
 | 
			
		||||
      attachment.save
 | 
			
		||||
      attachments.each do |attachment|
 | 
			
		||||
        affected_status_ids << attachment.status_id if attachment.status_id.present?
 | 
			
		||||
 | 
			
		||||
        attachment.file.destroy if attachment.file&.exists?
 | 
			
		||||
        attachment.type = :unknown
 | 
			
		||||
        attachment.save
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      invalidate_association_caches!(affected_status_ids) unless affected_status_ids.empty?
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,6 +34,7 @@
 | 
			
		|||
 | 
			
		||||
= form_for(@form, url: batch_admin_accounts_path) do |f|
 | 
			
		||||
  = hidden_field_tag :page, params[:page] || 1
 | 
			
		||||
  = hidden_field_tag :select_all_matching, '0'
 | 
			
		||||
 | 
			
		||||
  - AccountFilter::KEYS.each do |key|
 | 
			
		||||
    = hidden_field_tag key, params[key] if params[key].present?
 | 
			
		||||
| 
						 | 
				
			
			@ -49,6 +50,14 @@
 | 
			
		|||
          = 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') }
 | 
			
		||||
    - if @accounts.total_count > @accounts.size
 | 
			
		||||
      .batch-table__select-all
 | 
			
		||||
        .not-selected.active
 | 
			
		||||
          %span= t('generic.all_items_on_page_selected_html', count: @accounts.size)
 | 
			
		||||
          %button{ type: 'button' }= t('generic.select_all_matching_items', count: @accounts.total_count)
 | 
			
		||||
        .selected
 | 
			
		||||
          %span= t('generic.all_matching_items_selected_html', count: @accounts.total_count)
 | 
			
		||||
          %button{ type: 'button' }= t('generic.deselect')
 | 
			
		||||
    .batch-table__body
 | 
			
		||||
      - if @accounts.empty?
 | 
			
		||||
        = nothing_here 'nothing-here--under-tabs'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,6 +23,7 @@
 | 
			
		|||
    %link{ rel: 'manifest', href: manifest_path(format: :json) }/
 | 
			
		||||
    %meta{ name: 'theme-color', content: '#6364FF' }/
 | 
			
		||||
    %meta{ name: 'apple-mobile-web-app-capable', content: 'yes' }/
 | 
			
		||||
    %meta{ name: 'apple-itunes-app', content: 'app-id=1571998974' }/
 | 
			
		||||
 | 
			
		||||
    %title= content_for?(:page_title) ? safe_join([yield(:page_title).chomp.html_safe, title], ' - ') : title
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,44 +0,0 @@
 | 
			
		|||
%table.email-table{ cellspacing: 0, cellpadding: 0 }
 | 
			
		||||
  %tbody
 | 
			
		||||
    %tr
 | 
			
		||||
      %td.email-body
 | 
			
		||||
        .email-container
 | 
			
		||||
          %table.content-section{ cellspacing: 0, cellpadding: 0 }
 | 
			
		||||
            %tbody
 | 
			
		||||
              %tr
 | 
			
		||||
                %td.content-cell.darker.hero-with-button
 | 
			
		||||
                  .email-row
 | 
			
		||||
                    .col-6
 | 
			
		||||
                      %table.column{ cellspacing: 0, cellpadding: 0 }
 | 
			
		||||
                        %tbody
 | 
			
		||||
                          %tr
 | 
			
		||||
                            %td.column-cell.text-center.padded
 | 
			
		||||
                              %h1= t 'notification_mailer.digest.title'
 | 
			
		||||
                              %p.lead= t('notification_mailer.digest.body', since: l((@me.user_current_sign_in_at || @since).to_date, format: :short), instance: site_hostname)
 | 
			
		||||
                              %table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
 | 
			
		||||
                                %tbody
 | 
			
		||||
                                  %tr
 | 
			
		||||
                                    %td.button-primary
 | 
			
		||||
                                      = link_to web_url do
 | 
			
		||||
                                        %span= t 'notification_mailer.digest.action'
 | 
			
		||||
 | 
			
		||||
- @notifications.each_with_index do |n, i|
 | 
			
		||||
  = render 'status', status: n.target_status, i: i
 | 
			
		||||
 | 
			
		||||
- unless @follows_since.zero?
 | 
			
		||||
  %table.email-table{ cellspacing: 0, cellpadding: 0 }
 | 
			
		||||
    %tbody
 | 
			
		||||
      %tr
 | 
			
		||||
        %td.email-body
 | 
			
		||||
          .email-container
 | 
			
		||||
            %table.content-section{ cellspacing: 0, cellpadding: 0 }
 | 
			
		||||
              %tbody
 | 
			
		||||
                %tr
 | 
			
		||||
                  %td.content-cell.content-start.border-top
 | 
			
		||||
                    .email-row
 | 
			
		||||
                      .col-6
 | 
			
		||||
                        %table.column{ cellspacing: 0, cellpadding: 0 }
 | 
			
		||||
                          %tbody
 | 
			
		||||
                            %tr
 | 
			
		||||
                              %td.column-cell.text-center
 | 
			
		||||
                                %p= t('notification_mailer.digest.new_followers_summary', count: @follows_since)
 | 
			
		||||
| 
						 | 
				
			
			@ -1,15 +0,0 @@
 | 
			
		|||
<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
 | 
			
		||||
 | 
			
		||||
<%= raw t('notification_mailer.digest.body', since: l(@me.user_current_sign_in_at || @since), instance: root_url) %>
 | 
			
		||||
<% @notifications.each do |notification| %>
 | 
			
		||||
 | 
			
		||||
* <%= raw t('notification_mailer.digest.mention', name: notification.from_account.pretty_acct) %>
 | 
			
		||||
 | 
			
		||||
  <%= raw extract_status_plain_text(notification.target_status) %>
 | 
			
		||||
 | 
			
		||||
  <%= raw t('application_mailer.view')%> <%= web_url("statuses/#{notification.target_status.id}") %>
 | 
			
		||||
<% end %>
 | 
			
		||||
<% if @follows_since > 0 %>
 | 
			
		||||
 | 
			
		||||
<%= raw t('notification_mailer.digest.new_followers_summary', count: @follows_since) %>
 | 
			
		||||
<% end %>
 | 
			
		||||
| 
						 | 
				
			
			@ -28,10 +28,6 @@
 | 
			
		|||
  .fields-group
 | 
			
		||||
    = f.input :setting_always_send_emails, as: :boolean, wrapper: :with_label
 | 
			
		||||
 | 
			
		||||
  .fields-group
 | 
			
		||||
    = f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff|
 | 
			
		||||
      = ff.input :digest, as: :boolean, wrapper: :with_label
 | 
			
		||||
 | 
			
		||||
  %h4= t 'notifications.other_settings'
 | 
			
		||||
 | 
			
		||||
  .fields-group
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,9 +10,6 @@
 | 
			
		|||
  .fields-group
 | 
			
		||||
    = f.input :setting_noindex, as: :boolean, wrapper: :with_label
 | 
			
		||||
 | 
			
		||||
  .fields-group
 | 
			
		||||
    = f.input :setting_aggregate_reblogs, as: :boolean, wrapper: :with_label, recommended: true
 | 
			
		||||
 | 
			
		||||
  - unless Setting.hide_followers_count
 | 
			
		||||
    .fields-group
 | 
			
		||||
      = f.input :setting_hide_followers_count, as: :boolean, wrapper: :with_label
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,21 +0,0 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class DigestMailerWorker
 | 
			
		||||
  include Sidekiq::Worker
 | 
			
		||||
 | 
			
		||||
  sidekiq_options queue: 'mailers'
 | 
			
		||||
 | 
			
		||||
  attr_reader :user
 | 
			
		||||
 | 
			
		||||
  def perform(user_id)
 | 
			
		||||
    @user = User.find(user_id)
 | 
			
		||||
    deliver_digest if @user.allows_digest_emails?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def deliver_digest
 | 
			
		||||
    NotificationMailer.digest(user.account).deliver_now!
 | 
			
		||||
    user.touch(:last_emailed_at)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -1,25 +0,0 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class Scheduler::EmailScheduler
 | 
			
		||||
  include Sidekiq::Worker
 | 
			
		||||
 | 
			
		||||
  sidekiq_options retry: 0
 | 
			
		||||
 | 
			
		||||
  FREQUENCY      = 7.days.freeze
 | 
			
		||||
  SIGN_IN_OFFSET = 1.day.freeze
 | 
			
		||||
 | 
			
		||||
  def perform
 | 
			
		||||
    eligible_users.reorder(nil).find_each do |user|
 | 
			
		||||
      next unless user.allows_digest_emails?
 | 
			
		||||
      DigestMailerWorker.perform_async(user.id)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def eligible_users
 | 
			
		||||
    User.emailable
 | 
			
		||||
        .where('current_sign_in_at < ?', (FREQUENCY + SIGN_IN_OFFSET).ago)
 | 
			
		||||
        .where('last_emailed_at IS NULL OR last_emailed_at < ?', FREQUENCY.ago)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -235,17 +235,21 @@ en:
 | 
			
		|||
        approve_user: Approve User
 | 
			
		||||
        assigned_to_self_report: Assign Report
 | 
			
		||||
        change_email_user: Change E-mail for User
 | 
			
		||||
        change_role_user: Change Role of User
 | 
			
		||||
        confirm_user: Confirm User
 | 
			
		||||
        create_account_warning: Create Warning
 | 
			
		||||
        create_announcement: Create Announcement
 | 
			
		||||
        create_canonical_email_block: Create E-mail Block
 | 
			
		||||
        create_custom_emoji: Create Custom Emoji
 | 
			
		||||
        create_domain_allow: Create Domain Allow
 | 
			
		||||
        create_domain_block: Create Domain Block
 | 
			
		||||
        create_email_domain_block: Create E-mail Domain Block
 | 
			
		||||
        create_ip_block: Create IP rule
 | 
			
		||||
        create_unavailable_domain: Create Unavailable Domain
 | 
			
		||||
        create_user_role: Create Role
 | 
			
		||||
        demote_user: Demote User
 | 
			
		||||
        destroy_announcement: Delete Announcement
 | 
			
		||||
        destroy_canonical_email_block: Delete E-mail Block
 | 
			
		||||
        destroy_custom_emoji: Delete Custom Emoji
 | 
			
		||||
        destroy_domain_allow: Delete Domain Allow
 | 
			
		||||
        destroy_domain_block: Delete Domain Block
 | 
			
		||||
| 
						 | 
				
			
			@ -254,6 +258,7 @@ en:
 | 
			
		|||
        destroy_ip_block: Delete IP rule
 | 
			
		||||
        destroy_status: Delete Post
 | 
			
		||||
        destroy_unavailable_domain: Delete Unavailable Domain
 | 
			
		||||
        destroy_user_role: Destroy Role
 | 
			
		||||
        disable_2fa_user: Disable 2FA
 | 
			
		||||
        disable_custom_emoji: Disable Custom Emoji
 | 
			
		||||
        disable_sign_in_token_auth_user: Disable E-mail Token Authentication for User
 | 
			
		||||
| 
						 | 
				
			
			@ -280,24 +285,30 @@ en:
 | 
			
		|||
        update_announcement: Update Announcement
 | 
			
		||||
        update_custom_emoji: Update Custom Emoji
 | 
			
		||||
        update_domain_block: Update Domain Block
 | 
			
		||||
        update_ip_block: Update IP rule
 | 
			
		||||
        update_status: Update Post
 | 
			
		||||
        update_user_role: Update Role
 | 
			
		||||
      actions:
 | 
			
		||||
        approve_appeal_html: "%{name} approved moderation decision appeal from %{target}"
 | 
			
		||||
        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}"
 | 
			
		||||
        change_role_user_html: "%{name} changed role of %{target}"
 | 
			
		||||
        confirm_user_html: "%{name} confirmed e-mail address of user %{target}"
 | 
			
		||||
        create_account_warning_html: "%{name} sent a warning to %{target}"
 | 
			
		||||
        create_announcement_html: "%{name} created new announcement %{target}"
 | 
			
		||||
        create_canonical_email_block_html: "%{name} blocked e-mail with the hash %{target}"
 | 
			
		||||
        create_custom_emoji_html: "%{name} uploaded new emoji %{target}"
 | 
			
		||||
        create_domain_allow_html: "%{name} allowed federation with domain %{target}"
 | 
			
		||||
        create_domain_block_html: "%{name} blocked domain %{target}"
 | 
			
		||||
        create_email_domain_block_html: "%{name} blocked e-mail domain %{target}"
 | 
			
		||||
        create_ip_block_html: "%{name} created rule for IP %{target}"
 | 
			
		||||
        create_unavailable_domain_html: "%{name} stopped delivery to domain %{target}"
 | 
			
		||||
        create_user_role_html: "%{name} created %{target} role"
 | 
			
		||||
        demote_user_html: "%{name} demoted user %{target}"
 | 
			
		||||
        destroy_announcement_html: "%{name} deleted announcement %{target}"
 | 
			
		||||
        destroy_custom_emoji_html: "%{name} destroyed emoji %{target}"
 | 
			
		||||
        destroy_canonical_email_block_html: "%{name} unblocked e-mail with the hash %{target}"
 | 
			
		||||
        destroy_custom_emoji_html: "%{name} deleted emoji %{target}"
 | 
			
		||||
        destroy_domain_allow_html: "%{name} disallowed federation with domain %{target}"
 | 
			
		||||
        destroy_domain_block_html: "%{name} unblocked domain %{target}"
 | 
			
		||||
        destroy_email_domain_block_html: "%{name} unblocked e-mail domain %{target}"
 | 
			
		||||
| 
						 | 
				
			
			@ -305,6 +316,7 @@ en:
 | 
			
		|||
        destroy_ip_block_html: "%{name} deleted rule for IP %{target}"
 | 
			
		||||
        destroy_status_html: "%{name} removed post by %{target}"
 | 
			
		||||
        destroy_unavailable_domain_html: "%{name} resumed delivery to domain %{target}"
 | 
			
		||||
        destroy_user_role_html: "%{name} deleted %{target} role"
 | 
			
		||||
        disable_2fa_user_html: "%{name} disabled two factor requirement for user %{target}"
 | 
			
		||||
        disable_custom_emoji_html: "%{name} disabled emoji %{target}"
 | 
			
		||||
        disable_sign_in_token_auth_user_html: "%{name} disabled e-mail token authentication for %{target}"
 | 
			
		||||
| 
						 | 
				
			
			@ -331,8 +343,9 @@ en:
 | 
			
		|||
        update_announcement_html: "%{name} updated announcement %{target}"
 | 
			
		||||
        update_custom_emoji_html: "%{name} updated emoji %{target}"
 | 
			
		||||
        update_domain_block_html: "%{name} updated domain block for %{target}"
 | 
			
		||||
        update_ip_block_html: "%{name} changed rule for IP %{target}"
 | 
			
		||||
        update_status_html: "%{name} updated post by %{target}"
 | 
			
		||||
      deleted_status: "(deleted post)"
 | 
			
		||||
        update_user_role_html: "%{name} changed %{target} role"
 | 
			
		||||
      empty: No logs found.
 | 
			
		||||
      filter_by_action: Filter by action
 | 
			
		||||
      filter_by_user: Filter by user
 | 
			
		||||
| 
						 | 
				
			
			@ -1220,12 +1233,22 @@ en:
 | 
			
		|||
    trending_now: Trending now
 | 
			
		||||
  generic:
 | 
			
		||||
    all: All
 | 
			
		||||
    all_items_on_page_selected_html:
 | 
			
		||||
      one: "<strong>%{count}</strong> item on this page is selected."
 | 
			
		||||
      other: All <strong>%{count}</strong> items on this page are selected.
 | 
			
		||||
    all_matching_items_selected_html:
 | 
			
		||||
      one: "<strong>%{count}</strong> item matching your search is selected."
 | 
			
		||||
      other: All <strong>%{count}</strong> items matching your search are selected.
 | 
			
		||||
    changes_saved_msg: Changes successfully saved!
 | 
			
		||||
    copy: Copy
 | 
			
		||||
    delete: Delete
 | 
			
		||||
    deselect: Deselect all
 | 
			
		||||
    none: None
 | 
			
		||||
    order_by: Order by
 | 
			
		||||
    save_changes: Save changes
 | 
			
		||||
    select_all_matching_items:
 | 
			
		||||
      one: Select %{count} item matching your search.
 | 
			
		||||
      other: Select all %{count} items matching your search.
 | 
			
		||||
    today: today
 | 
			
		||||
    validation_errors:
 | 
			
		||||
      one: Something isn't quite right yet! Please review the error below
 | 
			
		||||
| 
						 | 
				
			
			@ -1334,17 +1357,6 @@ en:
 | 
			
		|||
        subject: "%{name} submitted a report"
 | 
			
		||||
      sign_up:
 | 
			
		||||
        subject: "%{name} signed up"
 | 
			
		||||
    digest:
 | 
			
		||||
      action: View all notifications
 | 
			
		||||
      body: Here is a brief summary of the messages you missed since your last visit on %{since}
 | 
			
		||||
      mention: "%{name} mentioned you in:"
 | 
			
		||||
      new_followers_summary:
 | 
			
		||||
        one: Also, you have acquired one new follower while being away! Yay!
 | 
			
		||||
        other: Also, you have acquired %{count} new followers while being away! Amazing!
 | 
			
		||||
      subject:
 | 
			
		||||
        one: "1 new notification since your last visit 🐘"
 | 
			
		||||
        other: "%{count} new notifications since your last visit 🐘"
 | 
			
		||||
      title: In your absence...
 | 
			
		||||
    favourite:
 | 
			
		||||
      body: 'Your post was favourited by %{name}:'
 | 
			
		||||
      subject: "%{name} favourited your post"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -615,6 +615,8 @@ Rails.application.routes.draw do
 | 
			
		|||
 | 
			
		||||
        resources :domain_allows, only: [:index, :show, :create, :destroy]
 | 
			
		||||
        resources :domain_blocks, only: [:index, :show, :update, :create, :destroy]
 | 
			
		||||
        resources :email_domain_blocks, only: [:index, :show, :create, :destroy]
 | 
			
		||||
        resources :ip_blocks, only: [:index, :show, :update, :create, :destroy]
 | 
			
		||||
 | 
			
		||||
        namespace :trends do
 | 
			
		||||
          resources :tags, only: [:index]
 | 
			
		||||
| 
						 | 
				
			
			@ -625,6 +627,12 @@ Rails.application.routes.draw do
 | 
			
		|||
        post :measures, to: 'measures#create'
 | 
			
		||||
        post :dimensions, to: 'dimensions#create'
 | 
			
		||||
        post :retention, to: 'retention#create'
 | 
			
		||||
 | 
			
		||||
        resources :canonical_email_blocks, only: [:index, :create, :show, :destroy] do
 | 
			
		||||
          collection do
 | 
			
		||||
            post :test
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -49,10 +49,6 @@
 | 
			
		|||
    cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
 | 
			
		||||
    class: Scheduler::IpCleanupScheduler
 | 
			
		||||
    queue: scheduler
 | 
			
		||||
  email_scheduler:
 | 
			
		||||
    cron: '0 10 * * 2'
 | 
			
		||||
    class: Scheduler::EmailScheduler
 | 
			
		||||
    queue: scheduler
 | 
			
		||||
  backup_cleanup_scheduler:
 | 
			
		||||
    cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
 | 
			
		||||
    class: Scheduler::BackupCleanupScheduler
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,29 +1,16 @@
 | 
			
		|||
// Note: You must restart bin/webpack-dev-server for changes to take effect
 | 
			
		||||
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const { URL } = require('url');
 | 
			
		||||
const { createHash } = require('crypto');
 | 
			
		||||
const { readFileSync } = require('fs');
 | 
			
		||||
const { resolve } = require('path');
 | 
			
		||||
const { merge } = require('webpack-merge');
 | 
			
		||||
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
 | 
			
		||||
const OfflinePlugin = require('offline-plugin');
 | 
			
		||||
const TerserPlugin = require('terser-webpack-plugin');
 | 
			
		||||
const CompressionPlugin = require('compression-webpack-plugin');
 | 
			
		||||
const { output } = require('./configuration');
 | 
			
		||||
const { InjectManifest } = require('workbox-webpack-plugin');
 | 
			
		||||
const sharedConfig = require('./shared');
 | 
			
		||||
 | 
			
		||||
let attachmentHost;
 | 
			
		||||
 | 
			
		||||
if (process.env.S3_ENABLED === 'true') {
 | 
			
		||||
  if (process.env.S3_ALIAS_HOST || process.env.S3_CLOUDFRONT_HOST) {
 | 
			
		||||
    attachmentHost = process.env.S3_ALIAS_HOST || process.env.S3_CLOUDFRONT_HOST;
 | 
			
		||||
  } else {
 | 
			
		||||
    attachmentHost = process.env.S3_HOSTNAME || `s3-${process.env.S3_REGION || 'us-east-1'}.amazonaws.com`;
 | 
			
		||||
  }
 | 
			
		||||
} else if (process.env.SWIFT_ENABLED === 'true') {
 | 
			
		||||
  const { host } = new URL(process.env.SWIFT_OBJECT_URL);
 | 
			
		||||
  attachmentHost = host;
 | 
			
		||||
} else {
 | 
			
		||||
  attachmentHost = null;
 | 
			
		||||
}
 | 
			
		||||
const root = resolve(__dirname, '..', '..');
 | 
			
		||||
 | 
			
		||||
module.exports = merge(sharedConfig, {
 | 
			
		||||
  mode: 'production',
 | 
			
		||||
| 
						 | 
				
			
			@ -52,47 +39,28 @@ module.exports = merge(sharedConfig, {
 | 
			
		|||
      openAnalyzer: false,
 | 
			
		||||
      logLevel: 'silent', // do not bother Webpacker, who runs with --json and parses stdout
 | 
			
		||||
    }),
 | 
			
		||||
    new OfflinePlugin({
 | 
			
		||||
      publicPath: output.publicPath, // sw.js must be served from the root to avoid scope issues
 | 
			
		||||
      safeToUseOptionalCaches: true,
 | 
			
		||||
      caches: {
 | 
			
		||||
        main: [':rest:'],
 | 
			
		||||
        additional: [':externals:'],
 | 
			
		||||
        optional: [
 | 
			
		||||
          '**/locale_*.js', // don't fetch every locale; the user only needs one
 | 
			
		||||
          '**/*_polyfills-*.js', // the user may not need polyfills
 | 
			
		||||
          '**/*.woff2', // the user may have system-fonts enabled
 | 
			
		||||
          // images/audio can be cached on-demand
 | 
			
		||||
          '**/*.png',
 | 
			
		||||
          '**/*.jpg',
 | 
			
		||||
          '**/*.jpeg',
 | 
			
		||||
          '**/*.svg',
 | 
			
		||||
          '**/*.mp3',
 | 
			
		||||
          '**/*.ogg',
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      externals: [
 | 
			
		||||
        '/emoji/1f602.svg', // used for emoji picker dropdown
 | 
			
		||||
        '/emoji/sheet_10.png', // used in emoji-mart
 | 
			
		||||
    new InjectManifest({
 | 
			
		||||
      additionalManifestEntries: ['1f602.svg', 'sheet_13.png'].map((filename) => {
 | 
			
		||||
        const path = resolve(root, 'public', 'emoji', filename);
 | 
			
		||||
        const body = readFileSync(path);
 | 
			
		||||
        const md5  = createHash('md5');
 | 
			
		||||
 | 
			
		||||
        md5.update(body);
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
          revision: md5.digest('hex'),
 | 
			
		||||
          url: `/emoji/${filename}`,
 | 
			
		||||
        };
 | 
			
		||||
      }),
 | 
			
		||||
      exclude: [
 | 
			
		||||
        /(?:base|extra)_polyfills-.*\.js$/,
 | 
			
		||||
        /locale_.*\.js$/,
 | 
			
		||||
        /mailer-.*\.(?:css|js)$/,
 | 
			
		||||
      ],
 | 
			
		||||
      excludes: [
 | 
			
		||||
        '**/*.gz',
 | 
			
		||||
        '**/*.map',
 | 
			
		||||
        'stats.json',
 | 
			
		||||
        'report.html',
 | 
			
		||||
        // any browser that supports ServiceWorker will support woff2
 | 
			
		||||
        '**/*.eot',
 | 
			
		||||
        '**/*.ttf',
 | 
			
		||||
        '**/*-webfont-*.svg',
 | 
			
		||||
        '**/*.woff',
 | 
			
		||||
      ],
 | 
			
		||||
      ServiceWorker: {
 | 
			
		||||
        entry: `imports-loader?additionalCode=${encodeURIComponent(`var ATTACHMENT_HOST=${JSON.stringify(attachmentHost)};`)}!${encodeURI(path.join(__dirname, '../../app/javascript/mastodon/service_worker/entry.js'))}`,
 | 
			
		||||
        cacheName: 'mastodon',
 | 
			
		||||
        output: '../assets/sw.js',
 | 
			
		||||
        publicPath: '/sw.js',
 | 
			
		||||
        minify: true,
 | 
			
		||||
      },
 | 
			
		||||
      include: [/\.js$/, /\.css$/],
 | 
			
		||||
      maximumFileSizeToCacheInBytes: 2 * 1_024 * 1_024, // 2 MiB
 | 
			
		||||
      swDest: resolve(root, 'public', 'packs', 'sw.js'),
 | 
			
		||||
      swSrc: resolve(root, 'app', 'javascript', 'mastodon', 'service_worker', 'entry.js'),
 | 
			
		||||
    }),
 | 
			
		||||
  ],
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
class AddHumanIdentifierToAdminActionLogs < ActiveRecord::Migration[6.1]
 | 
			
		||||
  def change
 | 
			
		||||
    add_column :admin_action_logs, :human_identifier, :string
 | 
			
		||||
    add_column :admin_action_logs, :route_param, :string
 | 
			
		||||
    add_column :admin_action_logs, :permalink, :string
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
class ChangeCanonicalEmailBlocksNullable < ActiveRecord::Migration[6.1]
 | 
			
		||||
  def change
 | 
			
		||||
    safety_assured { change_column :canonical_email_blocks, :reference_account_id, :bigint, null: true, default: nil }
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class FixCustomFilterKeywordsIdSeq < ActiveRecord::Migration[6.1]
 | 
			
		||||
  disable_ddl_transaction!
 | 
			
		||||
 | 
			
		||||
  def up
 | 
			
		||||
    # 20220613110711 manually inserts items with set `id` in the database, but
 | 
			
		||||
    # we also need to bump the sequence number, otherwise 
 | 
			
		||||
    safety_assured do
 | 
			
		||||
      execute <<-SQL.squish
 | 
			
		||||
        BEGIN;
 | 
			
		||||
        LOCK TABLE custom_filter_keywords IN EXCLUSIVE MODE;
 | 
			
		||||
        SELECT setval('custom_filter_keywords_id_seq'::regclass, id) FROM custom_filter_keywords ORDER BY id DESC LIMIT 1;
 | 
			
		||||
        COMMIT;
 | 
			
		||||
      SQL
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def down; end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class RemoveRecordedChangesFromAdminActionLogs < ActiveRecord::Migration[5.2]
 | 
			
		||||
  disable_ddl_transaction!
 | 
			
		||||
 | 
			
		||||
  def change
 | 
			
		||||
    safety_assured { remove_column :admin_action_logs, :recorded_changes, :text }
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -10,7 +10,7 @@
 | 
			
		|||
#
 | 
			
		||||
# It's strongly recommended that you check this file into your version control system.
 | 
			
		||||
 | 
			
		||||
ActiveRecord::Schema.define(version: 2022_08_08_101323) do
 | 
			
		||||
ActiveRecord::Schema.define(version: 2022_08_27_195229) do
 | 
			
		||||
 | 
			
		||||
  # These are extensions that must be enabled in order to support this database
 | 
			
		||||
  enable_extension "plpgsql"
 | 
			
		||||
| 
						 | 
				
			
			@ -205,9 +205,11 @@ ActiveRecord::Schema.define(version: 2022_08_08_101323) do
 | 
			
		|||
    t.string "action", default: "", null: false
 | 
			
		||||
    t.string "target_type"
 | 
			
		||||
    t.bigint "target_id"
 | 
			
		||||
    t.text "recorded_changes", default: "", null: false
 | 
			
		||||
    t.datetime "created_at", null: false
 | 
			
		||||
    t.datetime "updated_at", null: false
 | 
			
		||||
    t.string "human_identifier"
 | 
			
		||||
    t.string "route_param"
 | 
			
		||||
    t.string "permalink"
 | 
			
		||||
    t.index ["account_id"], name: "index_admin_action_logs_on_account_id"
 | 
			
		||||
    t.index ["target_type", "target_id"], name: "index_admin_action_logs_on_target_type_and_target_id"
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -294,7 +296,7 @@ ActiveRecord::Schema.define(version: 2022_08_08_101323) do
 | 
			
		|||
 | 
			
		||||
  create_table "canonical_email_blocks", force: :cascade do |t|
 | 
			
		||||
    t.string "canonical_email_hash", default: "", null: false
 | 
			
		||||
    t.bigint "reference_account_id", null: false
 | 
			
		||||
    t.bigint "reference_account_id"
 | 
			
		||||
    t.datetime "created_at", precision: 6, null: false
 | 
			
		||||
    t.datetime "updated_at", precision: 6, null: false
 | 
			
		||||
    t.index ["canonical_email_hash"], name: "index_canonical_email_blocks_on_canonical_email_hash", unique: true
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,7 +15,7 @@ services:
 | 
			
		|||
 | 
			
		||||
  redis:
 | 
			
		||||
    restart: always
 | 
			
		||||
    image: redis:6-alpine
 | 
			
		||||
    image: redis:7-alpine
 | 
			
		||||
    networks:
 | 
			
		||||
      - internal_network
 | 
			
		||||
    healthcheck:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,17 +18,15 @@ module Mastodon
 | 
			
		|||
      When suspending a local user, a hash of a "canonical" version of their e-mail
 | 
			
		||||
      address is stored to prevent them from signing up again.
 | 
			
		||||
 | 
			
		||||
      This command can be used to find whether a known email address is blocked,
 | 
			
		||||
      and if so, which account it was attached to.
 | 
			
		||||
      This command can be used to find whether a known email address is blocked.
 | 
			
		||||
    LONG_DESC
 | 
			
		||||
    def find(email)
 | 
			
		||||
      accts = CanonicalEmailBlock.find_blocks(email).map(&:reference_account).map(&:acct).to_a
 | 
			
		||||
      accts = CanonicalEmailBlock.matching_email(email)
 | 
			
		||||
 | 
			
		||||
      if accts.empty?
 | 
			
		||||
        say("#{email} is not blocked", :yellow)
 | 
			
		||||
        say("#{email} is not blocked", :green)
 | 
			
		||||
      else
 | 
			
		||||
        accts.each do |acct|
 | 
			
		||||
          say(acct, :white)
 | 
			
		||||
        end
 | 
			
		||||
        say("#{email} is blocked", :red)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -40,24 +38,13 @@ module Mastodon
 | 
			
		|||
      This command allows removing a canonical email block.
 | 
			
		||||
    LONG_DESC
 | 
			
		||||
    def remove(email)
 | 
			
		||||
      blocks = CanonicalEmailBlock.find_blocks(email)
 | 
			
		||||
      blocks = CanonicalEmailBlock.matching_email(email)
 | 
			
		||||
 | 
			
		||||
      if blocks.empty?
 | 
			
		||||
        say("#{email} is not blocked", :yellow)
 | 
			
		||||
        say("#{email} is not blocked", :green)
 | 
			
		||||
      else
 | 
			
		||||
        blocks.destroy_all
 | 
			
		||||
        say("Removed canonical email block for #{email}", :green)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    private
 | 
			
		||||
 | 
			
		||||
    def color(processed, failed)
 | 
			
		||||
      if !processed.zero? && failed.zero?
 | 
			
		||||
        :green
 | 
			
		||||
      elsif failed.zero?
 | 
			
		||||
        :yellow
 | 
			
		||||
      else
 | 
			
		||||
        :red
 | 
			
		||||
        say("Unblocked #{email}", :green)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -84,7 +84,6 @@
 | 
			
		|||
    "object-assign": "^4.1.1",
 | 
			
		||||
    "object-fit-images": "^3.2.3",
 | 
			
		||||
    "object.values": "^1.1.5",
 | 
			
		||||
    "offline-plugin": "^5.0.7",
 | 
			
		||||
    "path-complete-extname": "^1.0.0",
 | 
			
		||||
    "pg": "^8.5.0",
 | 
			
		||||
    "postcss": "^8.4.16",
 | 
			
		||||
| 
						 | 
				
			
			@ -138,6 +137,12 @@
 | 
			
		|||
    "webpack-cli": "^3.3.12",
 | 
			
		||||
    "webpack-merge": "^5.8.0",
 | 
			
		||||
    "wicg-inert": "^3.1.2",
 | 
			
		||||
    "workbox-expiration": "^6.5.3",
 | 
			
		||||
    "workbox-precaching": "^6.5.3",
 | 
			
		||||
    "workbox-routing": "^6.5.3",
 | 
			
		||||
    "workbox-strategies": "^6.5.3",
 | 
			
		||||
    "workbox-webpack-plugin": "^6.5.3",
 | 
			
		||||
    "workbox-window": "^6.5.3",
 | 
			
		||||
    "ws": "^8.8.1"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
assets/sw.js
 | 
			
		||||
packs/sw.js
 | 
			
		||||
							
								
								
									
										1
									
								
								public/sw.js.map
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								public/sw.js.map
									
									
									
									
									
										Symbolic link
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
packs/sw.js.map
 | 
			
		||||
| 
						 | 
				
			
			@ -3,32 +3,4 @@
 | 
			
		|||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
RSpec.describe Admin::ActionLogsHelper, type: :helper do
 | 
			
		||||
  klass = Class.new do
 | 
			
		||||
    include ActionView::Helpers
 | 
			
		||||
    include Admin::ActionLogsHelper
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  let(:hoge) { klass.new }
 | 
			
		||||
 | 
			
		||||
  describe '#log_target' do
 | 
			
		||||
    after do
 | 
			
		||||
      hoge.log_target(log)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'log.target' do
 | 
			
		||||
      let(:log) { double(target: true) }
 | 
			
		||||
 | 
			
		||||
      it 'calls linkable_log_target' do
 | 
			
		||||
        expect(hoge).to receive(:linkable_log_target).with(log.target)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context '!log.target' do
 | 
			
		||||
      let(:log) { double(target: false, target_type: :type, recorded_changes: :change) }
 | 
			
		||||
 | 
			
		||||
      it 'calls log_target_from_history' do
 | 
			
		||||
        expect(hoge).to receive(:log_target_from_history).with(log.target_type, log.recorded_changes)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -101,35 +101,4 @@ RSpec.describe NotificationMailer, type: :mailer do
 | 
			
		|||
      expect(mail.body.encoded).to match("bob has requested to follow you")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe 'digest' do
 | 
			
		||||
    before do
 | 
			
		||||
      mention = Fabricate(:mention, account: receiver.account, status: foreign_status)
 | 
			
		||||
      Fabricate(:notification, account: receiver.account, activity: mention)
 | 
			
		||||
      sender.follow!(receiver.account)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context do
 | 
			
		||||
      let!(:mail) { NotificationMailer.digest(receiver.account, since: 5.days.ago) }
 | 
			
		||||
 | 
			
		||||
      include_examples 'localized subject', 'notification_mailer.digest.subject', count: 1, name: 'bob'
 | 
			
		||||
 | 
			
		||||
      it 'renders the headers' do
 | 
			
		||||
        expect(mail.subject).to match('notification since your last')
 | 
			
		||||
        expect(mail.to).to eq([receiver.email])
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'renders the body' do
 | 
			
		||||
        expect(mail.body.encoded).to match('brief summary')
 | 
			
		||||
        expect(mail.body.encoded).to include 'The body of the foreign status'
 | 
			
		||||
        expect(mail.body.encoded).to include sender.username
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'includes activities since the receiver last signed in' do
 | 
			
		||||
      receiver.update!(last_emailed_at: nil, current_sign_in_at: '2000-03-01T00:00:00Z')
 | 
			
		||||
      mail = NotificationMailer.digest(receiver.account)
 | 
			
		||||
      expect(mail.body.encoded).to include 'Mar 01, 2000, 00:00'
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,36 +0,0 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
describe DigestMailerWorker do
 | 
			
		||||
  describe 'perform' do
 | 
			
		||||
    let(:user) { Fabricate(:user, last_emailed_at: 3.days.ago) }
 | 
			
		||||
 | 
			
		||||
    context 'for a user who receives digests' do
 | 
			
		||||
      it 'sends the email' do
 | 
			
		||||
        service = double(deliver_now!: nil)
 | 
			
		||||
        allow(NotificationMailer).to receive(:digest).and_return(service)
 | 
			
		||||
        update_user_digest_setting(true)
 | 
			
		||||
        described_class.perform_async(user.id)
 | 
			
		||||
 | 
			
		||||
        expect(NotificationMailer).to have_received(:digest)
 | 
			
		||||
        expect(user.reload.last_emailed_at).to be_within(1).of(Time.now.utc)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'for a user who does not receive digests' do
 | 
			
		||||
      it 'does not send the email' do
 | 
			
		||||
        allow(NotificationMailer).to receive(:digest)
 | 
			
		||||
        update_user_digest_setting(false)
 | 
			
		||||
        described_class.perform_async(user.id)
 | 
			
		||||
 | 
			
		||||
        expect(NotificationMailer).not_to have_received(:digest)
 | 
			
		||||
        expect(user.last_emailed_at).to be_within(1).of(3.days.ago)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def update_user_digest_setting(value)
 | 
			
		||||
      user.settings['notification_emails'] = user.settings['notification_emails'].merge('digest' => value)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
		Loading…
	
		Reference in a new issue