diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index d276e8fe5..5e942e5c0 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -3,10 +3,12 @@ class AboutController < ApplicationController layout 'public' - before_action :require_open_federation!, only: [:show, :more] + before_action :require_open_federation!, only: [:show, :more, :blocks] + before_action :check_blocklist_enabled, only: [:blocks] + before_action :authenticate_user!, only: [:blocks], if: :blocklist_account_required? before_action :set_body_classes, only: :show before_action :set_instance_presenter - before_action :set_expires_in + before_action :set_expires_in, only: [:show, :more, :terms] skip_before_action :require_functional!, only: [:more, :terms] @@ -18,12 +20,40 @@ class AboutController < ApplicationController def terms; end + def blocks + @show_rationale = Setting.show_domain_blocks_rationale == 'all' + @show_rationale |= Setting.show_domain_blocks_rationale == 'users' && !current_user.nil? && current_user.functional? + @blocks = DomainBlock.with_user_facing_limitations.order('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), reject_media, domain').to_a + end + private def require_open_federation! not_found if whitelist_mode? end + def check_blocklist_enabled + not_found if Setting.show_domain_blocks == 'disabled' + end + + def blocklist_account_required? + Setting.show_domain_blocks == 'users' + end + + def block_severity_text(block) + if block.severity == 'suspend' + I18n.t('domain_blocks.suspension') + else + limitations = [] + limitations << I18n.t('domain_blocks.media_block') if block.reject_media? + limitations << I18n.t('domain_blocks.silence') if block.severity == 'silence' + limitations.join(', ') + end + end + + helper_method :block_severity_text + helper_method :public_fetch_mode? + def new_user User.new.tap do |user| user.build_account diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index b58622a8d..c5cd7129f 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -141,6 +141,15 @@ function main() { return false; }); + delegate(document, '.blocks-table button.icon-button', 'click', function(e) { + e.preventDefault(); + + const classList = this.firstElementChild.classList; + classList.toggle('fa-chevron-down'); + classList.toggle('fa-chevron-up'); + this.parentElement.parentElement.nextElementSibling.classList.toggle('hidden'); + }); + delegate(document, '.modal-button', 'click', e => { e.preventDefault(); diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss index 11ac6dfeb..fe6beba5d 100644 --- a/app/javascript/styles/mastodon/tables.scss +++ b/app/javascript/styles/mastodon/tables.scss @@ -241,3 +241,70 @@ a.table-action-link { } } } + +.blocks-table { + width: 100%; + max-width: 100%; + border-spacing: 0; + border-collapse: collapse; + table-layout: fixed; + border: 1px solid darken($ui-base-color, 8%); + + thead { + border: 1px solid darken($ui-base-color, 8%); + background: darken($ui-base-color, 4%); + font-weight: 500; + + th.severity-column { + width: 120px; + } + + th.button-column { + width: 23px; + } + } + + tbody > tr { + border: 1px solid darken($ui-base-color, 8%); + border-bottom: 0; + background: darken($ui-base-color, 4%); + + &:hover { + background: darken($ui-base-color, 2%); + } + + &.even { + background: $ui-base-color; + + &:hover { + background: lighten($ui-base-color, 2%); + } + } + + &.rationale { + background: lighten($ui-base-color, 4%); + border-top: 0; + + &:hover { + background: lighten($ui-base-color, 6%); + } + + &.hidden { + display: none; + } + } + + td:first-child { + overflow: hidden; + text-overflow: ellipsis; + } + } + + th, + td { + padding: 8px; + line-height: 18px; + vertical-align: top; + text-align: left; + } +} diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb index 37b8d98c6..4383cbd05 100644 --- a/app/models/domain_block.rb +++ b/app/models/domain_block.rb @@ -25,6 +25,7 @@ class DomainBlock < ApplicationRecord delegate :count, to: :accounts, prefix: true scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } + scope :with_user_facing_limitations, -> { where(severity: [:silence, :suspend]).or(where(reject_media: true)) } class << self def suspend?(domain) diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 051268375..6bc3ca9f5 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -30,6 +30,8 @@ class Form::AdminSettings mascot spam_check_enabled trends + show_domain_blocks + show_domain_blocks_rationale ).freeze BOOLEAN_KEYS = %i( @@ -60,6 +62,8 @@ class Form::AdminSettings validates :site_contact_email, :site_contact_username, presence: true validates :site_contact_username, existing_username: true validates :bootstrap_timeline_accounts, existing_username: { multiple: true } + validates :show_domain_blocks, inclusion: { in: %w(disabled users all) } + validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) } def initialize(_attributes = {}) super diff --git a/app/views/about/blocks.html.haml b/app/views/about/blocks.html.haml new file mode 100644 index 000000000..a81a4d1eb --- /dev/null +++ b/app/views/about/blocks.html.haml @@ -0,0 +1,48 @@ +- content_for :page_title do + = t('domain_blocks.title', instance: site_hostname) + +.grid + .column-0 + .box-widget.rich-formatting + %h2= t('domain_blocks.blocked_domains') + %p= t('domain_blocks.description', instance: site_hostname) + .table-wrapper + %table.blocks-table + %thead + %tr + %th= t('domain_blocks.domain') + %th.severity-column= t('domain_blocks.severity') + - if @show_rationale + %th.button-column + %tbody + - if @blocks.empty? + %tr + %td{ colspan: @show_rationale ? 3 : 2 }= t('domain_blocks.no_domain_blocks') + - else + - @blocks.each_with_index do |block, i| + %tr{ class: i % 2 == 0 ? 'even': nil } + %td{ title: block.domain }= block.domain + %td= block_severity_text(block) + - if @show_rationale + %td + - if block.public_comment.present? + %button.icon-button{ title: t('domain_blocks.show_rationale'), 'aria-label' => t('domain_blocks.show_rationale') } + = fa_icon 'chevron-down fw', 'aria-hidden' => true + - if @show_rationale + - if block.public_comment.present? + %tr.rationale.hidden + %td{ colspan: 3 }= block.public_comment.presence + %h2= t('domain_blocks.severity_legend.title') + - if @blocks.any? { |block| block.reject_media? } + %h3= t('domain_blocks.media_block') + %p= t('domain_blocks.severity_legend.media_block') + - if @blocks.any? { |block| block.severity == 'silence' } + %h3= t('domain_blocks.silence') + %p= t('domain_blocks.severity_legend.silence') + - if @blocks.any? { |block| block.severity == 'suspend' } + %h3= t('domain_blocks.suspension') + %p= t('domain_blocks.severity_legend.suspension') + - if public_fetch_mode? + %p= t('domain_blocks.severity_legend.suspension_disclaimer') + .column-1 + = render 'application/sidebar' diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index 28c0ece15..28880c087 100644 --- a/app/views/admin/settings/edit.html.haml +++ b/app/views/admin/settings/edit.html.haml @@ -79,6 +79,12 @@ .fields-group = f.input :min_invite_role, wrapper: :with_label, collection: %i(disabled user moderator admin), label: t('admin.settings.registrations.min_invite_role.title'), label_method: lambda { |role| role == :disabled ? t('admin.settings.registrations.min_invite_role.disabled') : t("admin.accounts.roles.#{role}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' + .fields-row + .fields-row__column.fields-row__column-6.fields-group + = f.input :show_domain_blocks, wrapper: :with_label, collection: %i(disabled users all), label: t('admin.settings.domain_blocks.title'), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' + .fields-row__column.fields-row__column-6.fields-group + = f.input :show_domain_blocks_rationale, wrapper: :with_label, collection: %i(disabled users all), label: t('admin.settings.domain_blocks_rationale.title'), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' + .fields-group = f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, label: t('admin.settings.registrations.closed_message.title'), hint: t('admin.settings.registrations.closed_message.desc_html'), input_html: { rows: 8 } = f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 } unless whitelist_mode? diff --git a/config/locales/en.yml b/config/locales/en.yml index 4696dc11b..8d267065c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -423,6 +423,13 @@ en: custom_css: desc_html: Modify the look with CSS loaded on every page title: Custom CSS + domain_blocks: + all: To everyone + disabled: To no one + title: Show domain blocks + users: To logged-in local users + domain_blocks_rationale: + title: Show rationale hero: desc_html: Displayed on the frontpage. At least 600x100px recommended. When not set, falls back to server thumbnail title: Hero image @@ -630,6 +637,23 @@ en: people: one: "%{count} person" other: "%{count} people" + domain_blocks: + blocked_domains: List of limited and blocked domains + description: This is the list of servers that %{instance} limits or reject federation with. + domain: Domain + media_block: Media block + no_domain_blocks: "(No domain blocks)" + severity: Severity + severity_legend: + media_block: Media files coming from the server are neither fetched, stored, or displayed to the user. + silence: Accounts from silenced servers can be found, followed and interacted with, but their toots will not appear in the public timelines, and notifications from them will not reach local users who are not following them. + suspension: No content from suspended servers is stored or displayed, nor is any content sent to them. Interactions from suspended servers are ignored. + suspension_disclaimer: Suspended servers may occasionally retrieve public content from this server. + title: Severities + show_rationale: Show rationale + silence: Silence + suspension: Suspension + title: "%{instance} List of blocked instances" domain_validator: invalid_domain: is not a valid domain name errors: diff --git a/config/routes.rb b/config/routes.rb index 9c33b8190..9ae24b0cd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -423,9 +423,10 @@ Rails.application.routes.draw do get '/web/(*any)', to: 'home#index', as: :web - get '/about', to: 'about#show' - get '/about/more', to: 'about#more' - get '/terms', to: 'about#terms' + get '/about', to: 'about#show' + get '/about/more', to: 'about#more' + get '/about/blocks', to: 'about#blocks' + get '/terms', to: 'about#terms' root 'home#index' diff --git a/config/settings.yml b/config/settings.yml index 4e5eefb59..6dbc46706 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -64,6 +64,8 @@ defaults: &defaults peers_api_enabled: true show_known_fediverse_at_about_page: true spam_check_enabled: true + show_domain_blocks: 'disabled' + show_domain_blocks_rationale: 'disabled' development: <<: *defaults