Implement scoped tags; use `local:` and `self:` scopes for community and personal tags, respectively.

staging
multiple creatures 2019-05-05 23:04:23 -05:00
parent 992218f05f
commit a47b1daaeb
10 changed files with 80 additions and 20 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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