diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index c4cf4a77b..5eedbe8c8 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 8a216ce0d..11661ce08 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -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 diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 62b8986a4..d52ec0e86 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -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) diff --git a/app/models/status.rb b/app/models/status.rb index 0baa5c98b..6c16046a5 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -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) diff --git a/app/models/tag.rb b/app/models/tag.rb index ecfe4f7e8..04b902885 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -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 diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 0fe7b0524..d05c9c4f8 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -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 diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 2efd51445..4db3d4cf4 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -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 diff --git a/app/services/hashtag_query_service.rb b/app/services/hashtag_query_service.rb index 5773d78c6..5a1464805 100644 --- a/app/services/hashtag_query_service.rb +++ b/app/services/hashtag_query_service.rb @@ -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 diff --git a/app/services/process_hashtags_service.rb b/app/services/process_hashtags_service.rb index fb89f6c5b..4257d9231 100644 --- a/app/services/process_hashtags_service.rb +++ b/app/services/process_hashtags_service.rb @@ -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) diff --git a/db/migrate/20190505072439_add_private_to_tags.rb b/db/migrate/20190505072439_add_private_to_tags.rb new file mode 100644 index 000000000..b9a29fe9e --- /dev/null +++ b/db/migrate/20190505072439_add_private_to_tags.rb @@ -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