Implement scoped tags; use `local:` and `self:` scopes for community and personal tags, respectively.
parent
992218f05f
commit
a47b1daaeb
|
@ -84,7 +84,9 @@ class AccountsController < ApplicationController
|
|||
tag = Tag.find_normalized(params[:tag])
|
||||
|
||||
if tag
|
||||
Status.tagged_with(tag.id)
|
||||
return Status.none if !user_signed_in && (tag.local || tag.private) || tag.private && current_account.id != @account.id
|
||||
scope = tag.private ? current_account.statuses : tag.local ? Status.local : Status
|
||||
scope.tagged_with(tag.id)
|
||||
else
|
||||
Status.none
|
||||
end
|
||||
|
|
|
@ -72,7 +72,9 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
|||
tag = Tag.find_normalized(params[:tagged])
|
||||
|
||||
if tag
|
||||
Status.tagged_with(tag.id)
|
||||
return Status.none if !user_signed_in && (tag.local || tag.private) || tag.private && current_account.id != @account.id
|
||||
scope = tag.private ? current_account.statuses : tag.local ? Status.local : Status
|
||||
scope.tagged_with(tag.id)
|
||||
else
|
||||
Status.none
|
||||
end
|
||||
|
|
|
@ -121,7 +121,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
def attach_tags(status)
|
||||
@tags.each do |tag|
|
||||
status.tags << tag
|
||||
TrendingTags.record_use!(tag, status.account, status.created_at) if status.public_visibility?
|
||||
TrendingTags.record_use!(tag, status.account, status.created_at) if status.distributable?
|
||||
end
|
||||
|
||||
@mentions.each do |mention|
|
||||
|
@ -148,6 +148,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
return if tag['name'].blank?
|
||||
|
||||
hashtag = tag['name'].gsub(/\A#/, '').mb_chars.downcase
|
||||
|
||||
return if hashtag.starts_with?('self:', '_self:', 'local:', '_local:')
|
||||
|
||||
hashtag = Tag.where(name: hashtag).first_or_create!(name: hashtag)
|
||||
|
||||
return if @tags.include?(hashtag)
|
||||
|
|
|
@ -379,8 +379,8 @@ class Status < ApplicationRecord
|
|||
apply_timeline_filters(query, account, local_only)
|
||||
end
|
||||
|
||||
def as_tag_timeline(tag, account = nil, local_only = false)
|
||||
query = browsable_timeline_scope(local_only).tagged_with(tag)
|
||||
def as_tag_timeline(tag, account = nil, local_only = false, priv = false)
|
||||
query = tag_timeline_scope(account, local_only, priv).tagged_with(tag)
|
||||
apply_timeline_filters(query, account, local_only)
|
||||
end
|
||||
|
||||
|
@ -465,9 +465,28 @@ class Status < ApplicationRecord
|
|||
|
||||
def browsable_timeline_scope(local_only = false)
|
||||
starting_scope = local_only ? Status.network : Status
|
||||
starting_scope
|
||||
.public_browsable
|
||||
.without_reblogs
|
||||
starting_scope = starting_scope.public_browsable
|
||||
if Setting.show_reblogs_in_public_timelines
|
||||
starting_scope
|
||||
else
|
||||
starting_scope.without_reblogs
|
||||
end
|
||||
end
|
||||
|
||||
def tag_timeline_scope(account = nil, local_only = false, priv = false)
|
||||
if priv
|
||||
return Status.none if account.nil?
|
||||
starting_scope = account.statuses
|
||||
else
|
||||
starting_scope = local_only ? Status.network : Status
|
||||
starting_scope = scope.public_browsable
|
||||
end
|
||||
|
||||
if Setting.show_reblogs_in_public_timelines
|
||||
starting_scope
|
||||
else
|
||||
starting_scope.without_reblogs
|
||||
end
|
||||
end
|
||||
|
||||
def apply_timeline_filters(query, account, local_only)
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
# name :string default(""), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# local :boolean default(FALSE), not null
|
||||
# private :boolean default(FALSE), not null
|
||||
#
|
||||
|
||||
class Tag < ApplicationRecord
|
||||
|
@ -17,7 +19,7 @@ class Tag < ApplicationRecord
|
|||
has_many :featured_tags, dependent: :destroy, inverse_of: :tag
|
||||
has_one :account_tag_stat, dependent: :destroy
|
||||
|
||||
HASHTAG_NAME_RE = '[[:word:]_\-]*[[:alpha:]_·\-][[:word:]_\-]*'
|
||||
HASHTAG_NAME_RE = '[[:word:]:_\-]*[[:alpha:]:_·\-][[:word:]:_\-]*'
|
||||
HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_RE})/i
|
||||
|
||||
validates :name, presence: true, uniqueness: true, format: { with: /\A#{HASHTAG_NAME_RE}\z/i }
|
||||
|
@ -26,6 +28,11 @@ class Tag < ApplicationRecord
|
|||
scope :hidden, -> { where(account_tag_stats: { hidden: true }) }
|
||||
scope :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('count(*) desc')) }
|
||||
|
||||
scope :only_local, -> { where(local: true) }
|
||||
scope :only_global, -> { where(local: false) }
|
||||
scope :only_private, -> { where(private: true) }
|
||||
scope :only_public, -> { where(private: false) }
|
||||
|
||||
delegate :accounts_count,
|
||||
:accounts_count=,
|
||||
:increment_count!,
|
||||
|
@ -33,6 +40,7 @@ class Tag < ApplicationRecord
|
|||
:hidden?,
|
||||
to: :account_tag_stat
|
||||
|
||||
before_create :set_scope
|
||||
after_save :save_account_tag_stat
|
||||
|
||||
def account_tag_stat
|
||||
|
@ -88,4 +96,9 @@ class Tag < ApplicationRecord
|
|||
return unless account_tag_stat&.changed?
|
||||
account_tag_stat.save
|
||||
end
|
||||
|
||||
def set_scope
|
||||
self.private = true if name.starts_with?('self', '_self')
|
||||
self.local = true if self.private || name.starts_with?('local', '_local')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -99,7 +99,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
|||
end
|
||||
|
||||
def virtual_tags
|
||||
object.active_mentions.to_a.sort_by(&:id) + object.tags + object.emojis
|
||||
object.active_mentions.to_a.sort_by(&:id) + object.tags.reject { |t| t.local || t.private } + object.emojis
|
||||
end
|
||||
|
||||
def atom_uri
|
||||
|
|
|
@ -93,7 +93,7 @@ class FanOutOnWriteService < BaseService
|
|||
def deliver_to_hashtags(status)
|
||||
Rails.logger.debug "Delivering status #{status.id} to hashtags"
|
||||
|
||||
status.tags.pluck(:name).each do |hashtag|
|
||||
status.tags.reject { |t| t.private }.pluck(:name).each do |hashtag|
|
||||
Redis.current.publish("timeline:hashtag:#{hashtag}", @payload)
|
||||
Redis.current.publish("timeline:hashtag:#{hashtag}:local", @payload) if status.local?
|
||||
end
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class HashtagQueryService < BaseService
|
||||
def call(tag, params, account = nil, local = false)
|
||||
tags = tags_for(Array(tag.name) | Array(params[:any])).pluck(:id)
|
||||
def call(tag, params, account = nil, local = false, priv = false)
|
||||
tags = tags_for(Array(tag.name) | Array(params[:any]))
|
||||
all = tags_for(params[:all])
|
||||
none = tags_for(params[:none])
|
||||
|
||||
all_tags = Array(tags) | Array(all) | Array(none)
|
||||
local = all_tags.any? { |t| t.local } unless local
|
||||
priv = all_tags.any? { |t| t.private } unless priv
|
||||
|
||||
tags = tags.pluck(:id)
|
||||
|
||||
Status.distinct
|
||||
.as_tag_timeline(tags, account, local)
|
||||
.as_tag_timeline(tags, account, local, priv)
|
||||
.tagged_with_all(all)
|
||||
.tagged_with_none(none)
|
||||
end
|
||||
|
|
|
@ -2,19 +2,26 @@
|
|||
|
||||
class ProcessHashtagsService < BaseService
|
||||
def call(status, tags = [])
|
||||
tags = Extractor.extract_hashtags(status.text) if status.network?
|
||||
tags = Extractor.extract_hashtags(status.text) if tags.blank? && status.local?
|
||||
records = []
|
||||
|
||||
tags.map { |str| str.mb_chars.downcase }.uniq(&:to_s).each do |name|
|
||||
tag = Tag.where(name: name).first_or_create(name: name)
|
||||
component_indices = name.size.times.select {|i| name[i] == ':'}
|
||||
component_indices << name.size - 1
|
||||
component_indices.each do |i|
|
||||
frag = name[0..i]
|
||||
tag = Tag.where(name: frag).first_or_create(name: frag)
|
||||
|
||||
status.tags << tag
|
||||
records << tag
|
||||
status.tags << tag
|
||||
|
||||
TrendingTags.record_use!(tag, status.account, status.created_at) if status.distributable?
|
||||
next if tag.local || tag.private
|
||||
|
||||
records << tag
|
||||
TrendingTags.record_use!(tag, status.account, status.created_at) if status.distributable?
|
||||
end
|
||||
end
|
||||
|
||||
return unless status.public_visibility? || status.unlisted_visibility?
|
||||
return unless status.distributable?
|
||||
|
||||
status.account.featured_tags.where(tag_id: records.map(&:id)).each do |featured_tag|
|
||||
featured_tag.increment(status.created_at)
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
class AddPrivateToTags < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
safety_assured {
|
||||
add_column :tags, :local, :boolean, default: false, null: false
|
||||
add_column :tags, :private, :boolean, default: false, null: false
|
||||
}
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue