mirror of
https://github.com/mastodon/mastodon.git
synced 2024-11-30 16:41:55 +00:00
74081433d0
If a status with a hashtag becomes very popular, it stands to reason that the hashtag should have a chance at trending Fix no stats being recorded for hashtags that are not allowed to trend, and stop ignoring bots Remove references to hashtags in profile directory from the code and the admin UI
153 lines
4.3 KiB
Ruby
153 lines
4.3 KiB
Ruby
# frozen_string_literal: true
|
|
# == Schema Information
|
|
#
|
|
# 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
|
|
# usable :boolean
|
|
# trendable :boolean
|
|
# listable :boolean
|
|
# reviewed_at :datetime
|
|
# requested_review_at :datetime
|
|
# last_status_at :datetime
|
|
# max_score :float
|
|
# max_score_at :datetime
|
|
#
|
|
|
|
class Tag < ApplicationRecord
|
|
has_and_belongs_to_many :statuses
|
|
has_and_belongs_to_many :accounts
|
|
|
|
has_many :featured_tags, dependent: :destroy, inverse_of: :tag
|
|
|
|
HASHTAG_SEPARATORS = "_\u00B7\u200c"
|
|
HASHTAG_NAME_RE = "([[:word:]_][[:word:]#{HASHTAG_SEPARATORS}]*[[:alpha:]#{HASHTAG_SEPARATORS}][[:word:]#{HASHTAG_SEPARATORS}]*[[:word:]_])|([[:word:]_]*[[:alpha:]][[:word:]_]*)"
|
|
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 :reviewed, -> { where.not(reviewed_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 :listable, -> { where(listable: [true, nil]) }
|
|
scope :trendable, -> { Setting.trendable_by_default ? where(trendable: [true, nil]) : where(trendable: true) }
|
|
scope :recently_used, ->(account) { joins(:statuses).where(statuses: { id: account.statuses.select(:id).limit(1000) }).group(:id).order(Arel.sql('count(*) desc')) }
|
|
scope :matches_name, ->(term) { where(arel_table[:name].lower.matches(arel_table.lower("#{sanitize_sql_like(Tag.normalize(term))}%"), nil, true)) } # Search with case-sensitive to use B-tree index
|
|
|
|
update_index('tags#tag', :self)
|
|
|
|
def to_param
|
|
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', Setting.trendable_by_default)
|
|
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 use!(account, status: nil, at_time: Time.now.utc)
|
|
TrendingTags.record_use!(self, account, status: status, at_time: at_time)
|
|
end
|
|
|
|
def trending?
|
|
TrendingTags.trending?(self)
|
|
end
|
|
|
|
def history
|
|
days = []
|
|
|
|
7.times do |i|
|
|
day = i.days.ago.beginning_of_day.to_i
|
|
|
|
days << {
|
|
day: day.to_s,
|
|
uses: Redis.current.get("activity:tags:#{id}:#{day}") || '0',
|
|
accounts: Redis.current.pfcount("activity:tags:#{id}:#{day}:accounts").to_s,
|
|
}
|
|
end
|
|
|
|
days
|
|
end
|
|
|
|
class << self
|
|
def find_or_create_by_names(name_or_names)
|
|
Array(name_or_names).map(&method(:normalize)).uniq { |str| str.mb_chars.downcase.to_s }.map do |normalized_name|
|
|
tag = matching_name(normalized_name).first || create(name: normalized_name)
|
|
|
|
yield tag if block_given?
|
|
|
|
tag
|
|
end
|
|
end
|
|
|
|
def search_for(term, limit = 5, offset = 0, options = {})
|
|
stripped_term = term.strip
|
|
|
|
query = Tag.listable.matches_name(stripped_term)
|
|
query = query.merge(matching_name(stripped_term).or(where.not(reviewed_at: nil))) if options[:exclude_unreviewed]
|
|
|
|
query.order(Arel.sql('length(name) ASC, name ASC'))
|
|
.limit(limit)
|
|
.offset(offset)
|
|
end
|
|
|
|
def find_normalized(name)
|
|
matching_name(name).first
|
|
end
|
|
|
|
def find_normalized!(name)
|
|
find_normalized(name) || raise(ActiveRecord::RecordNotFound)
|
|
end
|
|
|
|
def matching_name(name_or_names)
|
|
names = Array(name_or_names).map { |name| arel_table.lower(normalize(name)) }
|
|
|
|
if names.size == 1
|
|
where(arel_table[:name].lower.eq(names.first))
|
|
else
|
|
where(arel_table[:name].lower.in(names))
|
|
end
|
|
end
|
|
|
|
def normalize(str)
|
|
str.gsub(/\A#/, '')
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
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
|