Add management of delivery availability in Federation settings (#15771)

* Add management of delivery availavility in Federation settings

* fix translate

* Remove useless object creation

* Fix DeepSource issue

* Add shortcut for all

* Fix DeepSource(skipcq)

* Change 'remove' to 'clear'

* Fix style

* Change class method name (exhausted_deliveries_key_by)
This commit is contained in:
Takeshi Umeda 2021-05-06 06:39:02 +09:00 committed by GitHub
parent d9ae3db8d5
commit 7cb34b32f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 180 additions and 5 deletions

View File

@ -3,7 +3,8 @@
module Admin module Admin
class InstancesController < BaseController class InstancesController < BaseController
before_action :set_instances, only: :index before_action :set_instances, only: :index
before_action :set_instance, only: :show before_action :set_instance, except: :index
before_action :set_exhausted_deliveries_days, only: :show
def index def index
authorize :instance, :index? authorize :instance, :index?
@ -13,14 +14,55 @@ module Admin
authorize :instance, :show? authorize :instance, :show?
end end
def clear_delivery_errors
authorize :delivery, :clear_delivery_errors?
@instance.delivery_failure_tracker.clear_failures!
redirect_to admin_instance_path(@instance.domain)
end
def restart_delivery
authorize :delivery, :restart_delivery?
last_unavailable_domain = unavailable_domain
if last_unavailable_domain.present?
@instance.delivery_failure_tracker.track_success!
log_action :destroy, last_unavailable_domain
end
redirect_to admin_instance_path(@instance.domain)
end
def stop_delivery
authorize :delivery, :stop_delivery?
UnavailableDomain.create(domain: @instance.domain)
log_action :create, unavailable_domain
redirect_to admin_instance_path(@instance.domain)
end
private private
def set_instance def set_instance
@instance = Instance.find(params[:id]) @instance = Instance.find(params[:id])
end end
def set_exhausted_deliveries_days
@exhausted_deliveries_days = @instance.delivery_failure_tracker.exhausted_deliveries_days
end
def set_instances def set_instances
@instances = filtered_instances.page(params[:page]) @instances = filtered_instances.page(params[:page])
warning_domains_map = DeliveryFailureTracker.warning_domains_map
@instances.each do |instance|
instance.failure_days = warning_domains_map[instance.domain]
end
end
def unavailable_domain
UnavailableDomain.find_by(domain: @instance.domain)
end end
def filtered_instances def filtered_instances

View File

@ -21,7 +21,7 @@ module Admin::ActionLogsHelper
record.shortcode record.shortcode
when 'Report' when 'Report'
link_to "##{record.id}", admin_report_path(record) link_to "##{record.id}", admin_report_path(record)
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock' when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'
link_to record.domain, "https://#{record.domain}" link_to record.domain, "https://#{record.domain}"
when 'Status' when 'Status'
link_to record.account.acct, ActivityPub::TagManager.instance.url_for(record) link_to record.account.acct, ActivityPub::TagManager.instance.url_for(record)
@ -38,7 +38,7 @@ module Admin::ActionLogsHelper
case type case type
when 'CustomEmoji' when 'CustomEmoji'
attributes['shortcode'] attributes['shortcode']
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock' when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'
link_to attributes['domain'], "https://#{attributes['domain']}" link_to attributes['domain'], "https://#{attributes['domain']}"
when 'Status' when 'Status'
tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count')) tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count'))

View File

@ -17,6 +17,10 @@ class DeliveryFailureTracker
UnavailableDomain.find_by(domain: @host)&.destroy UnavailableDomain.find_by(domain: @host)&.destroy
end end
def clear_failures!
Redis.current.del(exhausted_deliveries_key)
end
def days def days
Redis.current.scard(exhausted_deliveries_key) || 0 Redis.current.scard(exhausted_deliveries_key) || 0
end end
@ -25,6 +29,10 @@ class DeliveryFailureTracker
!UnavailableDomain.where(domain: @host).exists? !UnavailableDomain.where(domain: @host).exists?
end end
def exhausted_deliveries_days
Redis.current.smembers(exhausted_deliveries_key).sort.map { |date| Date.new(date.slice(0, 4).to_i, date.slice(4, 2).to_i, date.slice(6, 2).to_i) }
end
alias reset! track_success! alias reset! track_success!
class << self class << self
@ -44,6 +52,24 @@ class DeliveryFailureTracker
def reset!(url) def reset!(url)
new(url).reset! new(url).reset!
end end
def warning_domains
domains = Redis.current.keys(exhausted_deliveries_key_by('*')).map do |key|
key.delete_prefix(exhausted_deliveries_key_by(''))
end
domains - UnavailableDomain.all.pluck(:domain)
end
def warning_domains_map
warning_domains.index_with { |domain| Redis.current.scard(exhausted_deliveries_key_by(domain)) }
end
private
def exhausted_deliveries_key_by(host)
"exhausted_deliveries:#{host}"
end
end end
private private

View File

@ -17,12 +17,14 @@ class Admin::ActionLogFilter
create_domain_allow: { target_type: 'DomainAllow', action: 'create' }.freeze, create_domain_allow: { target_type: 'DomainAllow', action: 'create' }.freeze,
create_domain_block: { target_type: 'DomainBlock', action: 'create' }.freeze, create_domain_block: { target_type: 'DomainBlock', action: 'create' }.freeze,
create_email_domain_block: { target_type: 'EmailDomainBlock', action: 'create' }.freeze, create_email_domain_block: { target_type: 'EmailDomainBlock', action: 'create' }.freeze,
create_unavailable_domain: { target_type: 'UnavailableDomain', action: 'create' }.freeze,
demote_user: { target_type: 'User', action: 'demote' }.freeze, demote_user: { target_type: 'User', action: 'demote' }.freeze,
destroy_announcement: { target_type: 'Announcement', action: 'destroy' }.freeze, destroy_announcement: { target_type: 'Announcement', action: 'destroy' }.freeze,
destroy_custom_emoji: { target_type: 'CustomEmoji', action: 'destroy' }.freeze, destroy_custom_emoji: { target_type: 'CustomEmoji', action: 'destroy' }.freeze,
destroy_domain_allow: { target_type: 'DomainAllow', action: 'destroy' }.freeze, destroy_domain_allow: { target_type: 'DomainAllow', action: 'destroy' }.freeze,
destroy_domain_block: { target_type: 'DomainBlock', action: 'destroy' }.freeze, destroy_domain_block: { target_type: 'DomainBlock', action: 'destroy' }.freeze,
destroy_email_domain_block: { target_type: 'EmailDomainBlock', action: 'destroy' }.freeze, destroy_email_domain_block: { target_type: 'EmailDomainBlock', action: 'destroy' }.freeze,
destroy_unavailable_domain: { target_type: 'UnavailableDomain', action: 'destroy' }.freeze,
destroy_status: { target_type: 'Status', action: 'destroy' }.freeze, destroy_status: { target_type: 'Status', action: 'destroy' }.freeze,
disable_2fa_user: { target_type: 'User', action: 'disable' }.freeze, disable_2fa_user: { target_type: 'User', action: 'disable' }.freeze,
disable_custom_emoji: { target_type: 'CustomEmoji', action: 'disable' }.freeze, disable_custom_emoji: { target_type: 'CustomEmoji', action: 'disable' }.freeze,

View File

@ -10,10 +10,13 @@
class Instance < ApplicationRecord class Instance < ApplicationRecord
self.primary_key = :domain self.primary_key = :domain
attr_accessor :failure_days
has_many :accounts, foreign_key: :domain, primary_key: :domain has_many :accounts, foreign_key: :domain, primary_key: :domain
belongs_to :domain_block, foreign_key: :domain, primary_key: :domain belongs_to :domain_block, foreign_key: :domain, primary_key: :domain
belongs_to :domain_allow, foreign_key: :domain, primary_key: :domain belongs_to :domain_allow, foreign_key: :domain, primary_key: :domain
belongs_to :unavailable_domain, foreign_key: :domain, primary_key: :domain # skipcq: RB-RL1031
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }

View File

@ -4,6 +4,8 @@ class InstanceFilter
KEYS = %i( KEYS = %i(
limited limited
by_domain by_domain
warning
unavailable
).freeze ).freeze
attr_reader :params attr_reader :params
@ -13,7 +15,7 @@ class InstanceFilter
end end
def results def results
scope = Instance.includes(:domain_block, :domain_allow).order(accounts_count: :desc) scope = Instance.includes(:domain_block, :domain_allow, :unavailable_domain).order(accounts_count: :desc)
params.each do |key, value| params.each do |key, value|
scope.merge!(scope_for(key, value.to_s.strip)) if value.present? scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
@ -32,6 +34,10 @@ class InstanceFilter
Instance.joins(:domain_allow).reorder(Arel.sql('domain_allows.id desc')) Instance.joins(:domain_allow).reorder(Arel.sql('domain_allows.id desc'))
when 'by_domain' when 'by_domain'
Instance.matches_domain(value) Instance.matches_domain(value)
when 'warning'
Instance.where(domain: DeliveryFailureTracker.warning_domains)
when 'unavailable'
Instance.joins(:unavailable_domain)
else else
raise "Unknown filter: #{key}" raise "Unknown filter: #{key}"
end end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class DeliveryPolicy < ApplicationPolicy
def clear_delivery_errors?
admin?
end
def restart_delivery?
admin?
end
def stop_delivery?
admin?
end
end

View File

@ -0,0 +1,2 @@
%li.negative-hint
= l(exhausted_deliveries_days)

View File

@ -22,4 +22,12 @@
= t('admin.accounts.whitelisted') = t('admin.accounts.whitelisted')
- else - else
= t('admin.accounts.no_limits_imposed') = t('admin.accounts.no_limits_imposed')
- if instance.failure_days
= ' / '
%span.negative-hint
= t('admin.instances.delivery.warning_message', count: instance.failure_days)
- if instance.unavailable_domain
= ' / '
%span.negative-hint
= t('admin.instances.delivery.unavailable_message')
.trends__item__current{ title: t('admin.instances.known_accounts', count: instance.accounts_count) }= number_to_human instance.accounts_count, strip_insignificant_zeros: true .trends__item__current{ title: t('admin.instances.known_accounts', count: instance.accounts_count) }= number_to_human instance.accounts_count, strip_insignificant_zeros: true

View File

@ -16,6 +16,24 @@
- unless whitelist_mode? - unless whitelist_mode?
%li= filter_link_to t('admin.instances.moderation.limited'), limited: '1' %li= filter_link_to t('admin.instances.moderation.limited'), limited: '1'
.filter-subset
%strong= t('admin.instances.delivery.title')
%ul
%li= filter_link_to t('admin.instances.delivery.all'), warning: nil, unavailable: nil
%li= filter_link_to t('admin.instances.delivery.warning'), warning: '1', unavailable: nil
%li= filter_link_to t('admin.instances.delivery.unavailable'), warning: nil, unavailable: '1'
.back-link
= link_to admin_instances_path() do
%i.fa.fa-chevron-left.fa-fw
= t('admin.instances.back_to_all')
= link_to admin_instances_path(limited: 1) do
%i.fa.fa-chevron-left.fa-fw
= t('admin.instances.back_to_limited')
= link_to admin_instances_path(warning: 1) do
%i.fa.fa-chevron-left.fa-fw
= t('admin.instances.back_to_warning')
- unless whitelist_mode? - unless whitelist_mode?
= form_tag admin_instances_url, method: 'GET', class: 'simple_form' do = form_tag admin_instances_url, method: 'GET', class: 'simple_form' do
.fields-group .fields-group

View File

@ -1,6 +1,18 @@
- content_for :page_title do - content_for :page_title do
= @instance.domain = @instance.domain
.filters
.back-link
= link_to admin_instances_path() do
%i.fa.fa-chevron-left.fa-fw
= t('admin.instances.back_to_all')
= link_to admin_instances_path(limited: 1) do
%i.fa.fa-chevron-left.fa-fw
= t('admin.instances.back_to_limited')
= link_to admin_instances_path(warning: 1) do
%i.fa.fa-chevron-left.fa-fw
= t('admin.instances.back_to_warning')
.dashboard__counters .dashboard__counters
%div %div
= link_to admin_accounts_path(remote: '1', by_domain: @instance.domain) do = link_to admin_accounts_path(remote: '1', by_domain: @instance.domain) do
@ -48,6 +60,13 @@
= simple_format(h(@instance.public_comment)) = simple_format(h(@instance.public_comment))
.speech-bubble__owner= t 'admin.instances.public_comment' .speech-bubble__owner= t 'admin.instances.public_comment'
- unless @exhausted_deliveries_days.empty?
%h4= t 'admin.instances.delivery_error_days'
%ul
= render partial: 'exhausted_deliveries_days', collection: @exhausted_deliveries_days
%p.hint
= t 'admin.instances.delivery_error_hint', count: DeliveryFailureTracker::FAILURE_DAYS_THRESHOLD
%hr.spacer/ %hr.spacer/
%div.action-buttons %div.action-buttons
@ -59,3 +78,9 @@
= link_to t('admin.domain_blocks.undo'), admin_domain_block_path(@instance.domain_block), class: 'button' = link_to t('admin.domain_blocks.undo'), admin_domain_block_path(@instance.domain_block), class: 'button'
- else - else
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @instance.domain), class: 'button' = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @instance.domain), class: 'button'
- if @instance.delivery_failure_tracker.available?
- unless @exhausted_deliveries_days.empty?
= link_to t('admin.instances.delivery.clear'), clear_delivery_errors_admin_instance_path(@instance), data: { confirm: t('admin.accounts.are_you_sure'), method: :post }, class: 'button'
= link_to t('admin.instances.delivery.stop'), stop_delivery_admin_instance_path(@instance), data: { confirm: t('admin.accounts.are_you_sure'), method: :post }, class: 'button'
- else
= link_to t('admin.instances.delivery.restart'), restart_delivery_admin_instance_path(@instance), data: { confirm: t('admin.accounts.are_you_sure'), method: :post }, class: 'button'

View File

@ -230,6 +230,7 @@ en:
create_domain_block: Create Domain Block create_domain_block: Create Domain Block
create_email_domain_block: Create E-mail Domain Block create_email_domain_block: Create E-mail Domain Block
create_ip_block: Create IP rule create_ip_block: Create IP rule
create_unavailable_domain: Create Unavailable Domain
demote_user: Demote User demote_user: Demote User
destroy_announcement: Delete Announcement destroy_announcement: Delete Announcement
destroy_custom_emoji: Delete Custom Emoji destroy_custom_emoji: Delete Custom Emoji
@ -238,6 +239,7 @@ en:
destroy_email_domain_block: Delete e-mail domain block destroy_email_domain_block: Delete e-mail domain block
destroy_ip_block: Delete IP rule destroy_ip_block: Delete IP rule
destroy_status: Delete Post destroy_status: Delete Post
destroy_unavailable_domain: Delete Unavailable Domain
disable_2fa_user: Disable 2FA disable_2fa_user: Disable 2FA
disable_custom_emoji: Disable Custom Emoji disable_custom_emoji: Disable Custom Emoji
disable_user: Disable User disable_user: Disable User
@ -271,6 +273,7 @@ en:
create_domain_block_html: "%{name} blocked domain %{target}" create_domain_block_html: "%{name} blocked domain %{target}"
create_email_domain_block_html: "%{name} blocked e-mail domain %{target}" create_email_domain_block_html: "%{name} blocked e-mail domain %{target}"
create_ip_block_html: "%{name} created rule for IP %{target}" create_ip_block_html: "%{name} created rule for IP %{target}"
create_unavailable_domain_html: "%{name} stopped delivery to domain %{target}"
demote_user_html: "%{name} demoted user %{target}" demote_user_html: "%{name} demoted user %{target}"
destroy_announcement_html: "%{name} deleted announcement %{target}" destroy_announcement_html: "%{name} deleted announcement %{target}"
destroy_custom_emoji_html: "%{name} destroyed emoji %{target}" destroy_custom_emoji_html: "%{name} destroyed emoji %{target}"
@ -279,6 +282,7 @@ en:
destroy_email_domain_block_html: "%{name} unblocked e-mail domain %{target}" destroy_email_domain_block_html: "%{name} unblocked e-mail domain %{target}"
destroy_ip_block_html: "%{name} deleted rule for IP %{target}" destroy_ip_block_html: "%{name} deleted rule for IP %{target}"
destroy_status_html: "%{name} removed post by %{target}" destroy_status_html: "%{name} removed post by %{target}"
destroy_unavailable_domain_html: "%{name} resumed delivery to domain %{target}"
disable_2fa_user_html: "%{name} disabled two factor requirement for user %{target}" disable_2fa_user_html: "%{name} disabled two factor requirement for user %{target}"
disable_custom_emoji_html: "%{name} disabled emoji %{target}" disable_custom_emoji_html: "%{name} disabled emoji %{target}"
disable_user_html: "%{name} disabled login for user %{target}" disable_user_html: "%{name} disabled login for user %{target}"
@ -451,8 +455,25 @@ en:
title: Follow recommendations title: Follow recommendations
unsuppress: Restore follow recommendation unsuppress: Restore follow recommendation
instances: instances:
back_to_all: All
back_to_limited: Limited
back_to_warning: Warning
by_domain: Domain by_domain: Domain
delivery:
all: All
clear: Clear delivery errors
restart: Restart delivery
stop: Stop delivery
title: Delivery
unavailable: Unavailable
unavailable_message: Delivery unavailable
warning: Warning
warning_message:
one: Delivery failure %{count} day
other: Delivery failure %{count} days
delivery_available: Delivery is available delivery_available: Delivery is available
delivery_error_days: Delivery error days
delivery_error_hint: If delivery is not possible for %{count} days, it will be automatically marked as undeliverable.
empty: No domains found. empty: No domains found.
known_accounts: known_accounts:
one: "%{count} known account" one: "%{count} known account"

View File

@ -217,7 +217,14 @@ Rails.application.routes.draw do
end end
end end
resources :instances, only: [:index, :show], constraints: { id: /[^\/]+/ } resources :instances, only: [:index, :show], constraints: { id: /[^\/]+/ } do
member do
post :clear_delivery_errors
post :restart_delivery
post :stop_delivery
end
end
resources :rules resources :rules
resources :reports, only: [:index, :show] do resources :reports, only: [:index, :show] do