Add public blocks to /about/blocks (#11298)
* Add automatic blocklist display in /about/blocks Inspired by https://github.com/Gargron/mastodon.social-misc * Add admin option to set who can see instance blocks * Normalize locales files * Rename “Sandbox” to “Silence” for consistency * Disable /about/blocks when in whitelist mode * Optionally display rationale for domain blocks * Only display domain blocks that have user-facing limitations, and order them * Redesign table of blocked domains to better handle long domain names and rationales * Change domain blocks ordering now that rationales aren't displayed right away * Only show explanation for block severities actually in use * Reword instance block explanations and add disclaimer for public fetch mode
This commit is contained in:
parent
9e1d28f48e
commit
9b6a5ed109
10 changed files with 197 additions and 5 deletions
|
@ -3,10 +3,12 @@
|
||||||
class AboutController < ApplicationController
|
class AboutController < ApplicationController
|
||||||
layout 'public'
|
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_body_classes, only: :show
|
||||||
before_action :set_instance_presenter
|
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]
|
skip_before_action :require_functional!, only: [:more, :terms]
|
||||||
|
|
||||||
|
@ -18,12 +20,40 @@ class AboutController < ApplicationController
|
||||||
|
|
||||||
def terms; end
|
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
|
private
|
||||||
|
|
||||||
def require_open_federation!
|
def require_open_federation!
|
||||||
not_found if whitelist_mode?
|
not_found if whitelist_mode?
|
||||||
end
|
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
|
def new_user
|
||||||
User.new.tap do |user|
|
User.new.tap do |user|
|
||||||
user.build_account
|
user.build_account
|
||||||
|
|
|
@ -141,6 +141,15 @@ function main() {
|
||||||
return false;
|
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 => {
|
delegate(document, '.modal-button', 'click', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ class DomainBlock < ApplicationRecord
|
||||||
delegate :count, to: :accounts, prefix: true
|
delegate :count, to: :accounts, prefix: true
|
||||||
|
|
||||||
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
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
|
class << self
|
||||||
def suspend?(domain)
|
def suspend?(domain)
|
||||||
|
|
|
@ -30,6 +30,8 @@ class Form::AdminSettings
|
||||||
mascot
|
mascot
|
||||||
spam_check_enabled
|
spam_check_enabled
|
||||||
trends
|
trends
|
||||||
|
show_domain_blocks
|
||||||
|
show_domain_blocks_rationale
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
BOOLEAN_KEYS = %i(
|
BOOLEAN_KEYS = %i(
|
||||||
|
@ -60,6 +62,8 @@ class Form::AdminSettings
|
||||||
validates :site_contact_email, :site_contact_username, presence: true
|
validates :site_contact_email, :site_contact_username, presence: true
|
||||||
validates :site_contact_username, existing_username: true
|
validates :site_contact_username, existing_username: true
|
||||||
validates :bootstrap_timeline_accounts, existing_username: { multiple: 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 = {})
|
def initialize(_attributes = {})
|
||||||
super
|
super
|
||||||
|
|
48
app/views/about/blocks.html.haml
Normal file
48
app/views/about/blocks.html.haml
Normal file
|
@ -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'
|
|
@ -79,6 +79,12 @@
|
||||||
.fields-group
|
.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'
|
= 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
|
.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 :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?
|
= 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?
|
||||||
|
|
|
@ -423,6 +423,13 @@ en:
|
||||||
custom_css:
|
custom_css:
|
||||||
desc_html: Modify the look with CSS loaded on every page
|
desc_html: Modify the look with CSS loaded on every page
|
||||||
title: Custom CSS
|
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:
|
hero:
|
||||||
desc_html: Displayed on the frontpage. At least 600x100px recommended. When not set, falls back to server thumbnail
|
desc_html: Displayed on the frontpage. At least 600x100px recommended. When not set, falls back to server thumbnail
|
||||||
title: Hero image
|
title: Hero image
|
||||||
|
@ -630,6 +637,23 @@ en:
|
||||||
people:
|
people:
|
||||||
one: "%{count} person"
|
one: "%{count} person"
|
||||||
other: "%{count} people"
|
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:
|
domain_validator:
|
||||||
invalid_domain: is not a valid domain name
|
invalid_domain: is not a valid domain name
|
||||||
errors:
|
errors:
|
||||||
|
|
|
@ -425,6 +425,7 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
get '/about', to: 'about#show'
|
get '/about', to: 'about#show'
|
||||||
get '/about/more', to: 'about#more'
|
get '/about/more', to: 'about#more'
|
||||||
|
get '/about/blocks', to: 'about#blocks'
|
||||||
get '/terms', to: 'about#terms'
|
get '/terms', to: 'about#terms'
|
||||||
|
|
||||||
root 'home#index'
|
root 'home#index'
|
||||||
|
|
|
@ -64,6 +64,8 @@ defaults: &defaults
|
||||||
peers_api_enabled: true
|
peers_api_enabled: true
|
||||||
show_known_fediverse_at_about_page: true
|
show_known_fediverse_at_about_page: true
|
||||||
spam_check_enabled: true
|
spam_check_enabled: true
|
||||||
|
show_domain_blocks: 'disabled'
|
||||||
|
show_domain_blocks_rationale: 'disabled'
|
||||||
|
|
||||||
development:
|
development:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
|
|
Loading…
Reference in a new issue