diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index b5918d231c7..748c5de5add 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -3,7 +3,8 @@ module Admin class InstancesController < BaseController 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 authorize :instance, :index? @@ -13,14 +14,55 @@ module Admin authorize :instance, :show? 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 def set_instance @instance = Instance.find(params[:id]) end + def set_exhausted_deliveries_days + @exhausted_deliveries_days = @instance.delivery_failure_tracker.exhausted_deliveries_days + end + def set_instances @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 def filtered_instances diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index 0f3ca36e2df..e9a298a248e 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -21,7 +21,7 @@ module Admin::ActionLogsHelper record.shortcode when 'Report' link_to "##{record.id}", admin_report_path(record) - when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock' + when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain' link_to record.domain, "https://#{record.domain}" when 'Status' link_to record.account.acct, ActivityPub::TagManager.instance.url_for(record) @@ -38,7 +38,7 @@ module Admin::ActionLogsHelper case type when 'CustomEmoji' attributes['shortcode'] - when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock' + when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain' link_to attributes['domain'], "https://#{attributes['domain']}" when 'Status' tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count')) diff --git a/app/lib/delivery_failure_tracker.rb b/app/lib/delivery_failure_tracker.rb index 2cd6ef7adf8..8907ade4c18 100644 --- a/app/lib/delivery_failure_tracker.rb +++ b/app/lib/delivery_failure_tracker.rb @@ -17,6 +17,10 @@ class DeliveryFailureTracker UnavailableDomain.find_by(domain: @host)&.destroy end + def clear_failures! + Redis.current.del(exhausted_deliveries_key) + end + def days Redis.current.scard(exhausted_deliveries_key) || 0 end @@ -25,6 +29,10 @@ class DeliveryFailureTracker !UnavailableDomain.where(domain: @host).exists? 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! class << self @@ -44,6 +52,24 @@ class DeliveryFailureTracker def reset!(url) new(url).reset! 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 private diff --git a/app/models/admin/action_log_filter.rb b/app/models/admin/action_log_filter.rb index 3a1b67e0675..a1c156a8bf8 100644 --- a/app/models/admin/action_log_filter.rb +++ b/app/models/admin/action_log_filter.rb @@ -17,12 +17,14 @@ 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_unavailable_domain: { target_type: 'UnavailableDomain', 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_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, disable_2fa_user: { target_type: 'User', action: 'disable' }.freeze, disable_custom_emoji: { target_type: 'CustomEmoji', action: 'disable' }.freeze, diff --git a/app/models/instance.rb b/app/models/instance.rb index 29be036626e..8949be054a5 100644 --- a/app/models/instance.rb +++ b/app/models/instance.rb @@ -10,10 +10,13 @@ class Instance < ApplicationRecord self.primary_key = :domain + attr_accessor :failure_days + has_many :accounts, 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 :unavailable_domain, foreign_key: :domain, primary_key: :domain # skipcq: RB-RL1031 scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } diff --git a/app/models/instance_filter.rb b/app/models/instance_filter.rb index 0598d8fead6..9e533c4aa94 100644 --- a/app/models/instance_filter.rb +++ b/app/models/instance_filter.rb @@ -4,6 +4,8 @@ class InstanceFilter KEYS = %i( limited by_domain + warning + unavailable ).freeze attr_reader :params @@ -13,7 +15,7 @@ class InstanceFilter end 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| 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')) when 'by_domain' Instance.matches_domain(value) + when 'warning' + Instance.where(domain: DeliveryFailureTracker.warning_domains) + when 'unavailable' + Instance.joins(:unavailable_domain) else raise "Unknown filter: #{key}" end diff --git a/app/policies/delivery_policy.rb b/app/policies/delivery_policy.rb new file mode 100644 index 00000000000..24d06c1689f --- /dev/null +++ b/app/policies/delivery_policy.rb @@ -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 diff --git a/app/views/admin/instances/_exhausted_deliveries_days.haml b/app/views/admin/instances/_exhausted_deliveries_days.haml new file mode 100644 index 00000000000..e581f542dda --- /dev/null +++ b/app/views/admin/instances/_exhausted_deliveries_days.haml @@ -0,0 +1,2 @@ +%li.negative-hint + = l(exhausted_deliveries_days) diff --git a/app/views/admin/instances/_instance.html.haml b/app/views/admin/instances/_instance.html.haml index 188d0d9841d..990cf9ec88d 100644 --- a/app/views/admin/instances/_instance.html.haml +++ b/app/views/admin/instances/_instance.html.haml @@ -22,4 +22,12 @@ = t('admin.accounts.whitelisted') - else = 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 diff --git a/app/views/admin/instances/index.html.haml b/app/views/admin/instances/index.html.haml index 7c7958786d2..797948d9482 100644 --- a/app/views/admin/instances/index.html.haml +++ b/app/views/admin/instances/index.html.haml @@ -16,6 +16,24 @@ - unless whitelist_mode? %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? = form_tag admin_instances_url, method: 'GET', class: 'simple_form' do .fields-group diff --git a/app/views/admin/instances/show.html.haml b/app/views/admin/instances/show.html.haml index 0b9382771bb..46252933892 100644 --- a/app/views/admin/instances/show.html.haml +++ b/app/views/admin/instances/show.html.haml @@ -1,6 +1,18 @@ - content_for :page_title do = @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 %div = link_to admin_accounts_path(remote: '1', by_domain: @instance.domain) do @@ -48,6 +60,13 @@ = simple_format(h(@instance.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/ %div.action-buttons @@ -59,3 +78,9 @@ = link_to t('admin.domain_blocks.undo'), admin_domain_block_path(@instance.domain_block), class: 'button' - else = 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' diff --git a/config/locales/en.yml b/config/locales/en.yml index 1b41ee0632b..bfa4898174d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -230,6 +230,7 @@ en: 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 demote_user: Demote User destroy_announcement: Delete Announcement destroy_custom_emoji: Delete Custom Emoji @@ -238,6 +239,7 @@ en: destroy_email_domain_block: Delete e-mail domain block destroy_ip_block: Delete IP rule destroy_status: Delete Post + destroy_unavailable_domain: Delete Unavailable Domain disable_2fa_user: Disable 2FA disable_custom_emoji: Disable Custom Emoji disable_user: Disable User @@ -271,6 +273,7 @@ en: 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}" demote_user_html: "%{name} demoted user %{target}" destroy_announcement_html: "%{name} deleted announcement %{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_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}" disable_2fa_user_html: "%{name} disabled two factor requirement for user %{target}" disable_custom_emoji_html: "%{name} disabled emoji %{target}" disable_user_html: "%{name} disabled login for user %{target}" @@ -451,8 +455,25 @@ en: title: Follow recommendations unsuppress: Restore follow recommendation instances: + back_to_all: All + back_to_limited: Limited + back_to_warning: Warning 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_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. known_accounts: one: "%{count} known account" diff --git a/config/routes.rb b/config/routes.rb index 4661a7c114b..8ca7fccdd93 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -217,7 +217,14 @@ Rails.application.routes.draw do 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 :reports, only: [:index, :show] do