0
0
Fork 0

Change admin UI for hashtags and add back whitelisted trends (#11490)

Fix #271

Add back the `GET /api/v1/trends` API with the caveat that it does
not return tags that have not been allowed to trend by the staff.

When a hashtag begins to trend (internally) and that hashtag has
not been previously reviewed by the staff, the staff is notified.

The new admin UI for hashtags allows filtering hashtags by where
they are used (e.g. in the profile directory), whether they have
been reviewed or are pending reviewal, they show by how many people
the hashtag is used in the directory, how many people used it
today, how many statuses with it have been created today, and it
allows fixing the name of the hashtag to make it more readable.

The disallowed hashtags feature has been reworked. It is now
controlled from the admin UI for hashtags instead of from
the file `config/settings.yml`
This commit is contained in:
Eugen Rochko 2019-08-05 19:54:29 +02:00 committed by GitHub
parent 6201bfdfba
commit 115dab78f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 258 additions and 173 deletions

View file

@ -2,5 +2,16 @@
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
include Remotable
def boolean_with_default(key, default_value)
value = attributes[key]
if value.nil?
default_value
else
value
end
end
end

View file

@ -3,11 +3,16 @@
#
# Table name: tags
#
# id :bigint(8) not null, primary key
# name :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# score :integer
# id :bigint(8) not null, primary key
# name :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# score :integer
# usable :boolean
# trendable :boolean
# listable :boolean
# reviewed_at :datetime
# requested_review_at :datetime
#
class Tag < ApplicationRecord
@ -22,16 +27,17 @@ class Tag < ApplicationRecord
HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_RE})/i
validates :name, presence: true, format: { with: /\A(#{HASHTAG_NAME_RE})\z/i }
validate :validate_name_change, if: -> { !new_record? && name_changed? }
scope :discoverable, -> { joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).where(account_tag_stats: { hidden: false }).order(Arel.sql('account_tag_stats.accounts_count desc')) }
scope :hidden, -> { where(account_tag_stats: { hidden: true }) }
scope :reviewed, -> { where.not(reviewed_at: nil) }
scope :pending_review, -> { where(reviewed_at: nil).where.not(requested_review_at: nil) }
scope :discoverable, -> { where.not(listable: false).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')) }
delegate :accounts_count,
:accounts_count=,
:increment_count!,
:decrement_count!,
:hidden?,
to: :account_tag_stat
after_save :save_account_tag_stat
@ -48,6 +54,40 @@ class Tag < ApplicationRecord
name
end
def usable
boolean_with_default('usable', true)
end
alias usable? usable
def listable
boolean_with_default('listable', true)
end
alias listable? listable
def trendable
boolean_with_default('trendable', false)
end
alias trendable? trendable
def requires_review?
reviewed_at.nil?
end
def reviewed?
reviewed_at.present?
end
def requested_review?
requested_review_at.present?
end
def trending?
TrendingTags.trending?(self)
end
def history
days = []
@ -117,4 +157,8 @@ class Tag < ApplicationRecord
return unless account_tag_stat&.changed?
account_tag_stat.save
end
def validate_name_change
errors.add(:name, I18n.t('tags.does_not_match_previous_name')) unless name_was.mb_chars.casecmp(name.mb_chars).zero?
end
end

View file

@ -10,20 +10,28 @@ class TrendingTags
include Redisable
def record_use!(tag, account, at_time = Time.now.utc)
return if disallowed_hashtags.include?(tag.name) || account.silenced? || account.bot?
return if account.silenced? || account.bot? || !tag.usable? || !(tag.trendable? || tag.requires_review?)
increment_historical_use!(tag.id, at_time)
increment_unique_use!(tag.id, account.id, at_time)
increment_vote!(tag.id, at_time)
increment_vote!(tag, at_time)
end
def get(limit)
key = "#{KEY}:#{Time.now.utc.beginning_of_day.to_i}"
tag_ids = redis.zrevrange(key, 0, limit - 1).map(&:to_i)
tags = Tag.where(id: tag_ids).to_a.each_with_object({}) { |tag, h| h[tag.id] = tag }
def get(limit, filtered: true)
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
end
def trending?(tag)
rank = redis.zrevrank("#{KEY}:#{Time.now.utc.beginning_of_day.to_i}", tag.id)
rank.present? && rank <= 10
end
private
def increment_historical_use!(tag_id, at_time)
@ -38,33 +46,27 @@ class TrendingTags
redis.expire(key, EXPIRE_HISTORY_AFTER)
end
def increment_vote!(tag_id, at_time)
def increment_vote!(tag, at_time)
key = "#{KEY}:#{at_time.beginning_of_day.to_i}"
expected = redis.pfcount("activity:tags:#{tag_id}:#{(at_time - 1.day).beginning_of_day.to_i}:accounts").to_f
expected = redis.pfcount("activity:tags:#{tag.id}:#{(at_time - 1.day).beginning_of_day.to_i}:accounts").to_f
expected = 1.0 if expected.zero?
observed = redis.pfcount("activity:tags:#{tag_id}:#{at_time.beginning_of_day.to_i}:accounts").to_f
observed = redis.pfcount("activity:tags:#{tag.id}:#{at_time.beginning_of_day.to_i}:accounts").to_f
if expected > observed || observed < THRESHOLD
redis.zrem(key, tag_id.to_s)
redis.zrem(key, tag.id)
else
score = ((observed - expected)**2) / expected
added = redis.zadd(key, score, tag_id.to_s)
bump_tag_score!(tag_id) if added
score = ((observed - expected)**2) / expected
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?
end
redis.expire(key, EXPIRE_TRENDS_AFTER)
end
def bump_tag_score!(tag_id)
Tag.where(id: tag_id).update_all('score = COALESCE(score, 0) + 1')
end
def disallowed_hashtags
return @disallowed_hashtags if defined?(@disallowed_hashtags)
@disallowed_hashtags = Setting.disallowed_hashtags.nil? ? [] : Setting.disallowed_hashtags
@disallowed_hashtags = @disallowed_hashtags.split(' ') if @disallowed_hashtags.is_a? String
@disallowed_hashtags = @disallowed_hashtags.map(&:downcase)
def request_review!(tag)
User.staff.includes(:account).find_each { |u| AdminMailer.new_trending_tag(u.account, tag).deliver_later! if u.allows_trending_tag_emails? }
end
end
end

View file

@ -207,6 +207,10 @@ class User < ApplicationRecord
settings.notification_emails['pending_account']
end
def allows_trending_tag_emails?
settings.notification_emails['trending_tag']
end
def hides_network?
@hides_network ||= settings.hide_network
end