about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--app/controllers/admin/dashboard_controller.rb4
-rw-r--r--app/controllers/admin/domain_blocks_controller.rb28
-rw-r--r--app/controllers/admin/instances_controller.rb2
-rw-r--r--app/controllers/admin/tags_controller.rb32
-rw-r--r--app/controllers/remote_interaction_controller.rb1
-rw-r--r--app/models/domain_block.rb16
-rw-r--r--app/models/remote_follow.rb36
-rw-r--r--app/models/tag.rb3
-rw-r--r--app/models/trending_tags.rb9
-rw-r--r--app/services/block_domain_service.rb11
-rw-r--r--app/services/resolve_account_service.rb14
-rw-r--r--app/services/unblock_domain_service.rb19
-rw-r--r--app/views/admin/accounts/show.html.haml2
-rw-r--r--app/views/admin/dashboard/index.html.haml14
-rw-r--r--app/views/admin/domain_blocks/edit.html.haml30
-rw-r--r--app/views/admin/domain_blocks/new.html.haml6
-rw-r--r--app/views/admin/domain_blocks/show.html.haml12
-rw-r--r--app/views/admin/instances/show.html.haml13
-rw-r--r--app/views/admin/tags/index.html.haml1
-rw-r--r--app/views/admin/tags/show.html.haml33
-rw-r--r--app/workers/domain_block_worker.rb4
-rw-r--r--config/locales/en.yml14
-rw-r--r--config/routes.rb6
-rw-r--r--db/migrate/20190807135426_add_comments_to_domain_blocks.rb7
-rw-r--r--db/schema.rb4
-rw-r--r--spec/services/unblock_domain_service_spec.rb2
-rw-r--r--spec/workers/domain_block_worker_spec.rb2
27 files changed, 266 insertions, 59 deletions
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index 92bf7fbb9..4116f99f4 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -5,6 +5,7 @@ module Admin
   class DashboardController < BaseController
     def index
       @users_count           = User.count
+      @pending_users_count   = User.pending.count
       @registrations_week    = Redis.current.get("activity:accounts:local:#{current_week}") || 0
       @logins_week           = Redis.current.pfcount("activity:logins:#{current_week}")
       @interactions_week     = Redis.current.get("activity:interactions:#{current_week}") || 0
@@ -19,7 +20,7 @@ module Admin
       @redis_version         = redis_info['redis_version']
       @reports_count         = Report.unresolved.count
       @queue_backlog         = Sidekiq::Stats.new.enqueued
-      @recent_users          = User.confirmed.recent.includes(:account).limit(4)
+      @recent_users          = User.confirmed.recent.includes(:account).limit(8)
       @database_size         = ActiveRecord::Base.connection.execute('SELECT pg_database_size(current_database())').first['pg_database_size']
       @redis_size            = redis_info['used_memory']
       @ldap_enabled          = ENV['LDAP_ENABLED'] == 'true'
@@ -28,6 +29,7 @@ module Admin
       @pam_enabled           = ENV['PAM_ENABLED'] == 'true'
       @hidden_service        = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true'
       @trending_hashtags     = TrendingTags.get(10, filtered: false)
+      @pending_tags_count    = Tag.pending_review.count
       @authorized_fetch      = authorized_fetch_mode?
       @whitelist_enabled     = whitelist_mode?
       @profile_directory     = Setting.profile_directory
diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb
index 7129656da..74a36b79c 100644
--- a/app/controllers/admin/domain_blocks_controller.rb
+++ b/app/controllers/admin/domain_blocks_controller.rb
@@ -2,13 +2,17 @@
 
 module Admin
   class DomainBlocksController < BaseController
-    before_action :set_domain_block, only: [:show, :destroy]
+    before_action :set_domain_block, only: [:show, :destroy, :edit, :update]
 
     def new
       authorize :domain_block, :create?
       @domain_block = DomainBlock.new(domain: params[:_domain])
     end
 
+    def edit
+      authorize :domain_block, :create?
+    end
+
     def create
       authorize :domain_block, :create?
 
@@ -35,6 +39,22 @@ module Admin
       end
     end
 
+    def update
+      authorize :domain_block, :create?
+
+      @domain_block.update(update_params)
+
+      severity_changed = @domain_block.severity_changed?
+
+      if @domain_block.save
+        DomainBlockWorker.perform_async(@domain_block.id, severity_changed)
+        log_action :create, @domain_block
+        redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
+      else
+        render :edit
+      end
+    end
+
     def show
       authorize @domain_block, :show?
     end
@@ -52,8 +72,12 @@ module Admin
       @domain_block = DomainBlock.find(params[:id])
     end
 
+    def update_params
+      params.require(:domain_block).permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment)
+    end
+
     def resource_params
-      params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports)
+      params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment)
     end
   end
 end
diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb
index d4f201807..b47b18f8e 100644
--- a/app/controllers/admin/instances_controller.rb
+++ b/app/controllers/admin/instances_controller.rb
@@ -21,6 +21,8 @@ module Admin
       @blocks_count    = Block.where(target_account: Account.where(domain: params[:id])).count
       @available       = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url)
       @media_storage   = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size)
+      @private_comment = @domain_block&.private_comment
+      @public_comment  = @domain_block&.public_comment
     end
 
     private
diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb
index ed271aedc..d62361eaa 100644
--- a/app/controllers/admin/tags_controller.rb
+++ b/app/controllers/admin/tags_controller.rb
@@ -4,6 +4,8 @@ module Admin
   class TagsController < BaseController
     before_action :set_tags, only: :index
     before_action :set_tag, except: :index
+    before_action :set_usage_by_domain, except: :index
+    before_action :set_counters, except: :index
 
     def index
       authorize :tag, :index?
@@ -33,12 +35,28 @@ module Admin
       @tag = Tag.find(params[:id])
     end
 
+    def set_usage_by_domain
+      @usage_by_domain = @tag.statuses
+                             .where(visibility: :public)
+                             .where(Status.arel_table[:id].gteq(Mastodon::Snowflake.id_at(Time.now.utc.beginning_of_day)))
+                             .joins(:account)
+                             .group('accounts.domain')
+                             .reorder('statuses_count desc')
+                             .pluck('accounts.domain, count(*) AS statuses_count')
+    end
+
+    def set_counters
+      @accounts_today = @tag.history.first[:accounts]
+      @accounts_week  = Redis.current.pfcount(*current_week_days.map { |day| "activity:tags:#{@tag.id}:#{day}:accounts" })
+    end
+
     def filtered_tags
       scope = Tag
       scope = scope.discoverable if filter_params[:context] == 'directory'
-      scope = scope.reviewed if filter_params[:review] == 'reviewed'
-      scope = scope.pending_review if filter_params[:review] == 'pending_review'
-      scope.reorder(score: :desc)
+      scope = scope.unreviewed if filter_params[:review] == 'unreviewed'
+      scope = scope.reviewed.order(reviewed_at: :desc) if filter_params[:review] == 'reviewed'
+      scope = scope.pending_review.order(requested_review_at: :desc) if filter_params[:review] == 'pending_review'
+      scope.order(score: :desc)
     end
 
     def filter_params
@@ -48,5 +66,13 @@ module Admin
     def tag_params
       params.require(:tag).permit(:name, :trendable, :usable, :listable)
     end
+
+    def current_week_days
+      now = Time.now.utc.beginning_of_day.to_date
+
+      (Date.commercial(now.cwyear, now.cweek)..now).map do |date|
+        date.to_time.utc.beginning_of_day.to_i
+      end
+    end
   end
 end
diff --git a/app/controllers/remote_interaction_controller.rb b/app/controllers/remote_interaction_controller.rb
index e76f248b1..5ae72989b 100644
--- a/app/controllers/remote_interaction_controller.rb
+++ b/app/controllers/remote_interaction_controller.rb
@@ -40,7 +40,6 @@ class RemoteInteractionController < ApplicationController
     @status = Status.find(params[:id])
     authorize @status, :show?
   rescue Mastodon::NotPermittedError
-    # Reraise in order to get a 404
     raise ActiveRecord::RecordNotFound
   end
 
diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb
index 25d3b87ef..3f5b9f23e 100644
--- a/app/models/domain_block.rb
+++ b/app/models/domain_block.rb
@@ -3,13 +3,15 @@
 #
 # Table name: domain_blocks
 #
-#  id             :bigint(8)        not null, primary key
-#  domain         :string           default(""), not null
-#  created_at     :datetime         not null
-#  updated_at     :datetime         not null
-#  severity       :integer          default("silence")
-#  reject_media   :boolean          default(FALSE), not null
-#  reject_reports :boolean          default(FALSE), not null
+#  id              :bigint(8)        not null, primary key
+#  domain          :string           default(""), not null
+#  created_at      :datetime         not null
+#  updated_at      :datetime         not null
+#  severity        :integer          default("silence")
+#  reject_media    :boolean          default(FALSE), not null
+#  reject_reports  :boolean          default(FALSE), not null
+#  private_comment :text
+#  public_comment  :text
 #
 
 class DomainBlock < ApplicationRecord
diff --git a/app/models/remote_follow.rb b/app/models/remote_follow.rb
index 2537de36c..93df11724 100644
--- a/app/models/remote_follow.rb
+++ b/app/models/remote_follow.rb
@@ -2,19 +2,21 @@
 
 class RemoteFollow
   include ActiveModel::Validations
+  include RoutingHelper
 
   attr_accessor :acct, :addressable_template
 
   validates :acct, presence: true
 
-  def initialize(attrs = nil)
-    @acct = attrs[:acct].gsub(/\A@/, '').strip if !attrs.nil? && !attrs[:acct].nil?
+  def initialize(attrs = {})
+    @acct = normalize_acct(attrs[:acct])
   end
 
   def valid?
     return false unless super
 
-    populate_template
+    fetch_template!
+
     errors.empty?
   end
 
@@ -28,8 +30,30 @@ class RemoteFollow
 
   private
 
-  def populate_template
-    if acct.blank? || redirect_url_link.nil? || redirect_url_link.template.nil?
+  def normalize_acct(value)
+    return if value.blank?
+
+    username, domain = value.strip.gsub(/\A@/, '').split('@')
+
+    domain = begin
+      if TagManager.instance.local_domain?(domain)
+        nil
+      else
+        TagManager.instance.normalize_domain(domain)
+      end
+    end
+
+    [username, domain].compact.join('@')
+  end
+
+  def fetch_template!
+    return missing_resource if acct.blank?
+
+    _, domain = acct.split('@')
+
+    if domain.nil?
+      @addressable_template = Addressable::Template.new("#{authorize_interaction_url}?uri={uri}")
+    elsif redirect_url_link.nil? || redirect_url_link.template.nil?
       missing_resource_error
     else
       @addressable_template = Addressable::Template.new(redirect_uri_template)
@@ -45,7 +69,7 @@ class RemoteFollow
   end
 
   def acct_resource
-    @_acct_resource ||= Goldfinger.finger("acct:#{acct}")
+    @acct_resource ||= Goldfinger.finger("acct:#{acct}")
   rescue Goldfinger::Error, HTTP::ConnectionError
     nil
   end
diff --git a/app/models/tag.rb b/app/models/tag.rb
index e2fe91da1..1364d1dba 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -30,7 +30,8 @@ class Tag < ApplicationRecord
   validate :validate_name_change, if: -> { !new_record? && name_changed? }
 
   scope :reviewed, -> { where.not(reviewed_at: nil) }
-  scope :pending_review, -> { where(reviewed_at: nil).where.not(requested_review_at: nil) }
+  scope :unreviewed, -> { where(reviewed_at: nil) }
+  scope :pending_review, -> { unreviewed.where.not(requested_review_at: nil) }
   scope :usable, -> { where(usable: [true, nil]) }
   scope :discoverable, -> { where(listable: [true, nil]).joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).order(Arel.sql('account_tag_stats.accounts_count desc')) }
   scope :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('count(*) desc')) }
diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb
index 0a7e2feac..594ae9520 100644
--- a/app/models/trending_tags.rb
+++ b/app/models/trending_tags.rb
@@ -5,6 +5,7 @@ class TrendingTags
   EXPIRE_HISTORY_AFTER = 7.days.seconds
   EXPIRE_TRENDS_AFTER  = 1.day.seconds
   THRESHOLD            = 5
+  LIMIT                = 10
 
   class << self
     include Redisable
@@ -18,18 +19,18 @@ class TrendingTags
     end
 
     def get(limit, filtered: true)
-      tag_ids = redis.zrevrange("#{KEY}:#{Time.now.utc.beginning_of_day.to_i}", 0, limit - 1).map(&:to_i)
+      tag_ids = redis.zrevrange("#{KEY}:#{Time.now.utc.beginning_of_day.to_i}", 0, LIMIT - 1).map(&:to_i)
 
       tags = Tag.where(id: tag_ids)
       tags = tags.where(trendable: true) if filtered
       tags = tags.each_with_object({}) { |tag, h| h[tag.id] = tag }
 
-      tag_ids.map { |tag_id| tags[tag_id] }.compact
+      tag_ids.map { |tag_id| tags[tag_id] }.compact.take(limit)
     end
 
     def trending?(tag)
       rank = redis.zrevrank("#{KEY}:#{Time.now.utc.beginning_of_day.to_i}", tag.id)
-      rank.present? && rank <= 10
+      rank.present? && rank <= LIMIT
     end
 
     private
@@ -59,7 +60,7 @@ class TrendingTags
         old_rank = redis.zrevrank(key, tag.id)
 
         redis.zadd(key, score, tag.id)
-        request_review!(tag) if (old_rank.nil? || old_rank > 10) && redis.zrevrank(key, tag.id) <= 10 && !tag.trendable? && tag.requires_review? && !tag.requested_review?
+        request_review!(tag) if (old_rank.nil? || old_rank > LIMIT) && redis.zrevrank(key, tag.id) <= LIMIT && !tag.trendable? && tag.requires_review? && !tag.requested_review?
       end
 
       redis.expire(key, EXPIRE_TRENDS_AFTER)
diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb
index c5e5e5761..0ec6be503 100644
--- a/app/services/block_domain_service.rb
+++ b/app/services/block_domain_service.rb
@@ -3,13 +3,22 @@
 class BlockDomainService < BaseService
   attr_reader :domain_block
 
-  def call(domain_block)
+  def call(domain_block, update = false)
     @domain_block = domain_block
     process_domain_block!
+    process_retroactive_updates! if update
   end
 
   private
 
+  def process_retroactive_updates!
+    # If the domain block severity has been changed, undo the appropriate limitations
+    scope = Account.by_domain_and_subdomains(domain_block.domain)
+
+    scope.where(silenced_at: domain_block.created_at).in_batches.update_all(silenced_at: nil) unless domain_block.silence?
+    scope.where(suspended_at: domain_block.created_at).in_batches.update_all(suspended_at: nil) unless domain_block.suspend?
+  end
+
   def process_domain_block!
     clear_media! if domain_block.reject_media?
 
diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb
index 7864c4bcd..12e6544a0 100644
--- a/app/services/resolve_account_service.rb
+++ b/app/services/resolve_account_service.rb
@@ -60,17 +60,23 @@ class ResolveAccountService < BaseService
       @account  = uri
       @username = @account.username
       @domain   = @account.domain
-      @uri      = [@username, @domain].compact.join('@')
     else
-      @uri               = uri
       @username, @domain = uri.split('@')
     end
 
-    @domain = nil if TagManager.instance.local_domain?(@domain)
+    @domain = begin
+      if TagManager.instance.local_domain?(@domain)
+        nil
+      else
+        TagManager.instance.normalize_domain(@domain)
+      end
+    end
+
+    @uri = [@username, @domain].compact.join('@')
   end
 
   def process_webfinger!(uri, redirected = false)
-    @webfinger                           = Goldfinger.finger("acct:#{@uri}")
+    @webfinger                           = Goldfinger.finger("acct:#{uri}")
     confirmed_username, confirmed_domain = @webfinger.subject.gsub(/\Aacct:/, '').split('@')
 
     if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
diff --git a/app/services/unblock_domain_service.rb b/app/services/unblock_domain_service.rb
index fc262a50a..d502d9e49 100644
--- a/app/services/unblock_domain_service.rb
+++ b/app/services/unblock_domain_service.rb
@@ -10,24 +10,9 @@ class UnblockDomainService < BaseService
   end
 
   def process_retroactive_updates
-    blocked_accounts.in_batches.update_all(update_options) unless domain_block.noop?
-  end
-
-  def blocked_accounts
     scope = Account.by_domain_and_subdomains(domain_block.domain)
 
-    if domain_block.silence?
-      scope.where(silenced_at: @domain_block.created_at)
-    else
-      scope.where(suspended_at: @domain_block.created_at)
-    end
-  end
-
-  def update_options
-    { domain_block_impact => nil }
-  end
-
-  def domain_block_impact
-    domain_block.silence? ? :silenced_at : :suspended_at
+    scope.where(silenced_at: domain_block.created_at).in_batches.update_all(silenced_at: nil) unless domain_block.noop?
+    scope.where(suspended_at: domain_block.created_at).in_batches.update_all(suspended_at: nil) if domain_block.suspend?
   end
 end
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index 7494c9fa2..59babd3b0 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -174,7 +174,7 @@
 
       - unless @account.local?
         - if DomainBlock.where(domain: @account.domain).exists?
-          = link_to t('admin.domain_blocks.undo'), admin_instance_path(@account.domain), class: 'button'
+          = link_to t('admin.domain_blocks.view'), admin_instance_path(@account.domain), class: 'button'
         - else
           = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @account.domain), class: 'button button--destructive'
 
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 3c98da35f..408d515ca 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -15,14 +15,22 @@
       .dashboard__counters__num= number_with_delimiter @logins_week
       .dashboard__counters__label= t 'admin.dashboard.week_users_active'
   %div
-    %div
-      .dashboard__counters__num= number_with_delimiter @interactions_week
-      .dashboard__counters__label= t 'admin.dashboard.week_interactions'
+    = link_to admin_pending_accounts_path do
+      .dashboard__counters__num= number_with_delimiter @pending_users_count
+      .dashboard__counters__label= t 'admin.dashboard.pending_users'
   %div
     = link_to admin_reports_url do
       .dashboard__counters__num= number_with_delimiter @reports_count
       .dashboard__counters__label= t 'admin.dashboard.open_reports'
   %div
+    = link_to admin_tags_path(review: 'pending_review') do
+      .dashboard__counters__num= number_with_delimiter @pending_tags_count
+      .dashboard__counters__label= t 'admin.dashboard.pending_tags'
+  %div
+    %div
+      .dashboard__counters__num= number_with_delimiter @interactions_week
+      .dashboard__counters__label= t 'admin.dashboard.week_interactions'
+  %div
     = link_to sidekiq_url do
       .dashboard__counters__num= number_with_delimiter @queue_backlog
       .dashboard__counters__label= t 'admin.dashboard.backlog'
diff --git a/app/views/admin/domain_blocks/edit.html.haml b/app/views/admin/domain_blocks/edit.html.haml
new file mode 100644
index 000000000..29e47ef3b
--- /dev/null
+++ b/app/views/admin/domain_blocks/edit.html.haml
@@ -0,0 +1,30 @@
+- content_for :header_tags do
+  = javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous'
+
+- content_for :page_title do
+  = t('admin.domain_blocks.edit')
+
+= simple_form_for @domain_block, url: admin_domain_block_path(@domain_block), method: :put do |f|
+  = render 'shared/error_messages', object: @domain_block
+
+  .fields-row
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :domain, wrapper: :with_label, label: t('admin.domain_blocks.domain'), hint: t('admin.domain_blocks.new.hint'), required: true, readonly: true, disabled: true
+
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| t("admin.domain_blocks.new.severity.#{type}") }, hint: t('admin.domain_blocks.new.severity.desc_html')
+
+  .fields-group
+    = f.input :reject_media, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_media'), hint: I18n.t('admin.domain_blocks.reject_media_hint')
+
+  .fields-group
+    = f.input :reject_reports, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_reports'), hint: I18n.t('admin.domain_blocks.reject_reports_hint')
+
+  .field-group
+    = f.input :private_comment, wrapper: :with_label, label: I18n.t('admin.domain_blocks.private_comment'), hint: t('admin.domain_blocks.private_comment_hint'), rows: 6
+
+  .field-group
+    = f.input :public_comment, wrapper: :with_label, label: I18n.t('admin.domain_blocks.public_comment'), hint: t('admin.domain_blocks.public_comment_hint'), rows: 6
+
+  .actions
+    = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/admin/domain_blocks/new.html.haml b/app/views/admin/domain_blocks/new.html.haml
index 3a4963489..a643825df 100644
--- a/app/views/admin/domain_blocks/new.html.haml
+++ b/app/views/admin/domain_blocks/new.html.haml
@@ -17,5 +17,11 @@
   .fields-group
     = f.input :reject_reports, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_reports'), hint: I18n.t('admin.domain_blocks.reject_reports_hint')
 
+  .field-group
+    = f.input :private_comment, wrapper: :with_label, label: I18n.t('admin.domain_blocks.private_comment'), hint: t('admin.domain_blocks.private_comment_hint'), rows: 6
+
+  .field-group
+    = f.input :public_comment, wrapper: :with_label, label: I18n.t('admin.domain_blocks.public_comment'), hint: t('admin.domain_blocks.public_comment_hint'), rows: 6
+
   .actions
     = f.button :button, t('.create'), type: :submit
diff --git a/app/views/admin/domain_blocks/show.html.haml b/app/views/admin/domain_blocks/show.html.haml
index dca4dbac7..e64aaa629 100644
--- a/app/views/admin/domain_blocks/show.html.haml
+++ b/app/views/admin/domain_blocks/show.html.haml
@@ -1,6 +1,18 @@
 - content_for :page_title do
   = t('admin.domain_blocks.show.title', domain: @domain_block.domain)
 
+- if @domain_block.private_comment.present?
+  .speech-bubble
+    .speech-bubble__bubble
+      = simple_format(h(@domain_block.private_comment))
+    .speech-bubble__owner= t 'admin.instances.private_comment'
+
+- if @domain_block.public_comment.present?
+  .speech-bubble
+    .speech-bubble__bubble
+      = simple_format(h(@domain_block.public_comment))
+    .speech-bubble__owner= t 'admin.instances.public_comment'
+
 = simple_form_for @domain_block, url: admin_domain_block_path(@domain_block), method: :delete do |f|
 
   - unless (@domain_block.noop?)
diff --git a/app/views/admin/instances/show.html.haml b/app/views/admin/instances/show.html.haml
index fbb49ba02..294c9495d 100644
--- a/app/views/admin/instances/show.html.haml
+++ b/app/views/admin/instances/show.html.haml
@@ -31,6 +31,18 @@
           = fa_icon 'times'
       .dashboard__counters__label= t 'admin.instances.delivery_available'
 
+- if @private_comment.present?
+  .speech-bubble
+    .speech-bubble__bubble
+      = simple_format(h(@private_comment))
+    .speech-bubble__owner= t 'admin.instances.private_comment'
+
+- if @public_comment.present?
+  .speech-bubble
+    .speech-bubble__bubble
+      = simple_format(h(@public_comment))
+    .speech-bubble__owner= t 'admin.instances.public_comment'
+
 %hr.spacer/
 
 %div{ style: 'overflow: hidden' }
@@ -41,6 +53,7 @@
     - if @domain_allow
       = link_to t('admin.domain_allows.undo'), admin_domain_allow_path(@domain_allow), class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure'), method: :delete }
     - elsif @domain_block
+      = link_to t('admin.domain_blocks.edit'), edit_admin_domain_block_path(@domain_block), class: 'button'
       = link_to t('admin.domain_blocks.undo'), admin_domain_block_path(@domain_block), class: 'button'
     - else
       = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @instance.domain), class: 'button'
diff --git a/app/views/admin/tags/index.html.haml b/app/views/admin/tags/index.html.haml
index 5e4ee21f5..d994955ef 100644
--- a/app/views/admin/tags/index.html.haml
+++ b/app/views/admin/tags/index.html.haml
@@ -12,6 +12,7 @@
     %strong= t('admin.tags.review')
     %ul
       %li= filter_link_to t('generic.all'), review: nil
+      %li= filter_link_to t('admin.tags.unreviewed'), review: 'unreviewed'
       %li= filter_link_to t('admin.tags.reviewed'), review: 'reviewed'
       %li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{Tag.pending_review.count})"], ' '), review: 'pending_review'
 
diff --git a/app/views/admin/tags/show.html.haml b/app/views/admin/tags/show.html.haml
index 27c8dc92b..6a1e03065 100644
--- a/app/views/admin/tags/show.html.haml
+++ b/app/views/admin/tags/show.html.haml
@@ -1,6 +1,22 @@
 - content_for :page_title do
   = "##{@tag.name}"
 
+.dashboard__counters
+  %div
+    = link_to web_url("timelines/tag/#{@tag.name}") do
+      .dashboard__counters__num= number_with_delimiter @accounts_today
+      .dashboard__counters__label= t 'admin.tags.accounts_today'
+  %div
+    %div
+      .dashboard__counters__num= number_with_delimiter @accounts_week
+      .dashboard__counters__label= t 'admin.tags.accounts_week'
+  %div
+    = link_to explore_hashtag_path(@tag) do
+      .dashboard__counters__num= number_with_delimiter @tag.accounts_count
+      .dashboard__counters__label= t 'admin.tags.directory'
+
+%hr.spacer/
+
 = simple_form_for @tag, url: admin_tag_path(@tag.id) do |f|
   = render 'shared/error_messages', object: @tag
 
@@ -9,8 +25,21 @@
 
   .fields-group
     = f.input :usable, as: :boolean, wrapper: :with_label
-    = f.input :trendable, as: :boolean, wrapper: :with_label
-    = f.input :listable, as: :boolean, wrapper: :with_label
+    = f.input :trendable, as: :boolean, wrapper: :with_label, disabled: !Setting.trends
+    = f.input :listable, as: :boolean, wrapper: :with_label, disabled: !Setting.profile_directory
 
   .actions
     = f.button :button, t('generic.save_changes'), type: :submit
+
+%hr.spacer/
+
+%h3= t 'admin.tags.breakdown'
+
+.table-wrapper
+  %table.table
+    %tbody
+      - @usage_by_domain.each do |(domain, count)|
+        %tr
+          %th= domain || site_hostname
+          %td= "#{number_with_delimiter((count.to_f / @tag.history[0][:uses].to_f) * 100)}%"
+          %td= number_with_delimiter count
diff --git a/app/workers/domain_block_worker.rb b/app/workers/domain_block_worker.rb
index 884477829..35518d6b5 100644
--- a/app/workers/domain_block_worker.rb
+++ b/app/workers/domain_block_worker.rb
@@ -3,8 +3,8 @@
 class DomainBlockWorker
   include Sidekiq::Worker
 
-  def perform(domain_block_id)
-    BlockDomainService.new.call(DomainBlock.find(domain_block_id))
+  def perform(domain_block_id, update = false)
+    BlockDomainService.new.call(DomainBlock.find(domain_block_id), update)
   rescue ActiveRecord::RecordNotFound
     true
   end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 6b6b99683..5fc166f4c 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -261,6 +261,8 @@ en:
       hidden_service: Federation with hidden services
       keybase: Keybase integration
       open_reports: open reports
+      pending_tags: hashtags waiting for review
+      pending_users: users waiting for review
       recent_users: Recent users
       search: Full-text search
       single_user_mode: Single user mode
@@ -283,6 +285,7 @@ en:
       created_msg: Domain block is now being processed
       destroyed_msg: Domain block has been undone
       domain: Domain
+      edit: Edit domain block
       existing_domain_block_html: You have already imposed stricter limits on %{name}, you need to <a href="%{unblock_url}">unblock it</a> first.
       new:
         create: Create block
@@ -293,6 +296,10 @@ en:
           silence: Silence
           suspend: Suspend
         title: New domain block
+      private_comment: Private comment
+      private_comment_hint: Comment about this domain limitation for internal use by the moderators.
+      public_comment: Public comment
+      public_comment_hint: Comment about this domain limitation for the general public, if advertising the list of domain limitations is enabled.
       reject_media: Reject media files
       reject_media_hint: Removes locally stored media files and refuses to download any in the future. Irrelevant for suspensions
       reject_reports: Reject reports
@@ -312,6 +319,7 @@ en:
         title: Undo domain block for %{domain}
         undo: Undo
       undo: Undo domain block
+      view: View domain block
     email_domain_blocks:
       add_new: Add new
       created_msg: Successfully added e-mail domain to blacklist
@@ -335,6 +343,8 @@ en:
         all: All
         limited: Limited
         title: Moderation
+      private_comment: Private comment
+      public_comment: Public comment
       title: Federation
       total_blocked_by_us: Blocked by us
       total_followed_by_them: Followed by them
@@ -501,6 +511,9 @@ en:
       title: Account statuses
       with_media: With media
     tags:
+      accounts_today: Unique uses today
+      accounts_week: Unique uses this week
+      breakdown: Breakdown of today's usage by source
       context: Context
       directory: In directory
       in_directory: "%{count} in directory"
@@ -509,6 +522,7 @@ en:
       title: Hashtags
       trending_right_now: Trending right now
       unique_uses_today: "%{count} posting today"
+      unreviewed: Not reviewed
       updated_msg: Hashtag settings updated successfully
     title: Administration
     warning_presets:
diff --git a/config/routes.rb b/config/routes.rb
index 92271b00f..160640e0e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -157,7 +157,11 @@ Rails.application.routes.draw do
     get '/dashboard', to: 'dashboard#index'
 
     resources :domain_allows, only: [:new, :create, :show, :destroy]
-    resources :domain_blocks, only: [:new, :create, :show, :destroy]
+    resources :domain_blocks, only: [:new, :create, :show, :destroy, :update] do
+      member do
+        get :edit
+      end
+    end
     resources :email_domain_blocks, only: [:index, :new, :create, :destroy]
     resources :action_logs, only: [:index]
     resources :warning_presets, except: [:new]
diff --git a/db/migrate/20190807135426_add_comments_to_domain_blocks.rb b/db/migrate/20190807135426_add_comments_to_domain_blocks.rb
new file mode 100644
index 000000000..b660a71ad
--- /dev/null
+++ b/db/migrate/20190807135426_add_comments_to_domain_blocks.rb
@@ -0,0 +1,7 @@
+class AddCommentsToDomainBlocks < ActiveRecord::Migration[5.2]
+  def change
+    add_column :domain_blocks, :private_comment, :text
+    add_column :domain_blocks, :public_comment, :text
+  end
+end
+
diff --git a/db/schema.rb b/db/schema.rb
index 2b3056007..e87c6831f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2019_08_05_123746) do
+ActiveRecord::Schema.define(version: 2019_08_07_135426) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -269,6 +269,8 @@ ActiveRecord::Schema.define(version: 2019_08_05_123746) do
     t.integer "severity", default: 0
     t.boolean "reject_media", default: false, null: false
     t.boolean "reject_reports", default: false, null: false
+    t.text "private_comment"
+    t.text "public_comment"
     t.index ["domain"], name: "index_domain_blocks_on_domain", unique: true
   end
 
diff --git a/spec/services/unblock_domain_service_spec.rb b/spec/services/unblock_domain_service_spec.rb
index 619aefb5c..27dbc92ad 100644
--- a/spec/services/unblock_domain_service_spec.rb
+++ b/spec/services/unblock_domain_service_spec.rb
@@ -31,7 +31,7 @@ describe UnblockDomainService, type: :service do
       subject.call(@domain_block)
       expect_deleted_domain_block
       expect(@suspended.reload.suspended?).to be false
-      expect(@silenced.reload.silenced?).to be true
+      expect(@silenced.reload.silenced?).to be false
       expect(@independently_suspended.reload.suspended?).to be true
       expect(@independently_silenced.reload.silenced?).to be true
     end
diff --git a/spec/workers/domain_block_worker_spec.rb b/spec/workers/domain_block_worker_spec.rb
index c4138501f..48b3e38c4 100644
--- a/spec/workers/domain_block_worker_spec.rb
+++ b/spec/workers/domain_block_worker_spec.rb
@@ -14,7 +14,7 @@ describe DomainBlockWorker do
       result = subject.perform(domain_block.id)
 
       expect(result).to be_nil
-      expect(service).to have_received(:call).with(domain_block)
+      expect(service).to have_received(:call).with(domain_block, false)
     end
 
     it 'calls domain block service for relevant domain block' do