diff --git a/.rubocop.yml b/.rubocop.yml index 0ca5405170..74f79611fb 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -109,6 +109,7 @@ Metrics/ParameterLists: Metrics/PerceivedComplexity: Exclude: - 'app/policies/status_policy.rb' + - 'app/services/post_status_service.rb' # Reason: Prevailing style is argument file paths # https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsfilepath diff --git a/app/javascript/mastodon/features/compose/containers/warning_container.jsx b/app/javascript/mastodon/features/compose/containers/warning_container.jsx index 1c27b74f8b..3348e91b1f 100644 --- a/app/javascript/mastodon/features/compose/containers/warning_container.jsx +++ b/app/javascript/mastodon/features/compose/containers/warning_container.jsx @@ -11,7 +11,7 @@ import Warning from '../components/warning'; const mapStateToProps = state => ({ needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']), - hashtagWarning: ['public', 'public_unlisted', 'login'].indexOf(state.getIn(['compose', 'privacy'])) < 0 && HASHTAG_PATTERN_REGEX.test(state.getIn(['compose', 'text'])), + hashtagWarning: ['public', 'public_unlisted', 'login'].indexOf(state.getIn(['compose', 'privacy'])) < 0 && state.getIn(['compose', 'searchability']) !== 'public' && HASHTAG_PATTERN_REGEX.test(state.getIn(['compose', 'text'])), directMessageWarning: state.getIn(['compose', 'privacy']) === 'direct', searchabilityWarning: state.getIn(['compose', 'searchability']) === 'limited', }); diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb index aef05e9d9d..8ca14de08f 100644 --- a/app/lib/search_query_transformer.rb +++ b/app/lib/search_query_transformer.rb @@ -107,7 +107,7 @@ class SearchQueryTransformer < Parslet::Transform if clause[:prefix] PrefixClause.new(prefix, clause[:term].to_s) elsif clause[:term] - TermClause.new(prefix, operator, clause[:term].to_s) + PhraseClause.new(prefix, operator, clause[:term].to_s) elsif clause[:shortcode] TermClause.new(prefix, operator, ":#{clause[:term]}:") elsif clause[:phrase] diff --git a/app/models/concerns/has_user_settings.rb b/app/models/concerns/has_user_settings.rb index 8f8657fcd7..9a8a6378a9 100644 --- a/app/models/concerns/has_user_settings.rb +++ b/app/models/concerns/has_user_settings.rb @@ -171,6 +171,10 @@ module HasUserSettings settings['default_searchability'] || 'private' end + def setting_disallow_unlisted_public_searchability + settings['disallow_unlisted_public_searchability'] + end + def allows_report_emails? settings['notification_emails.report'] end diff --git a/app/models/public_feed.rb b/app/models/public_feed.rb index f4d27e53d3..929f062562 100644 --- a/app/models/public_feed.rb +++ b/app/models/public_feed.rb @@ -70,6 +70,10 @@ class PublicFeed Status.with_public_visibility.joins(:account).merge(Account.without_suspended.without_silenced) end + def public_search_scope + Status.with_public_search_visibility.joins(:account).merge(Account.without_suspended.without_silenced) + end + def local_only_scope Status.local end diff --git a/app/models/status.rb b/app/models/status.rb index ee20b42a99..fa4b4c5644 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -103,6 +103,7 @@ class Status < ApplicationRecord scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') } scope :without_reblogs, -> { where(statuses: { reblog_of_id: nil }) } scope :with_public_visibility, -> { where(visibility: [:public, :public_unlisted, :login]) } + scope :with_public_search_visibility, -> { merge(where(visibility: [:public, :public_unlisted, :login]).or(Status.where(searchability: :public))) } scope :with_global_timeline_visibility, -> { where(visibility: [:public, :login]) } scope :tagged_with, ->(tag_ids) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag_ids }) } scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced_at: nil }) } @@ -384,11 +385,15 @@ class Status < ApplicationRecord end def compute_searchability - return 'direct' if searchability && unsupported_searchability? - return searchability if account.local? && !searchability.nil? - return 'direct' if account.local? || [:public, :private, :direct, :limited].exclude?(account.searchability.to_sym) + local = account.local? - Status.searchabilities[[Account.searchabilities[account.searchability] || 3, Status.searchabilities[searchability.nil? ? 'direct' : searchability] || 3].max] || 'direct' + return 'direct' if unsupported_searchability? + return searchability if local && !searchability.nil? + return 'direct' if local || [:public, :private, :direct, :limited].exclude?(account.searchability.to_sym) + + account_searchability = Status.searchabilities[account.searchability] + status_searchability = Status.searchabilities[searchability.nil? ? 'direct' : searchability] + Status.searchabilities.invert.fetch([account_searchability, status_searchability].max) || 'direct' end def compute_searchability_activitypub @@ -407,7 +412,7 @@ class Status < ApplicationRecord end def selectable_searchabilities - searchabilities.keys - %w(public_unlisted) + searchabilities.keys - %w(public_unlisted unsupported) end def favourites_map(status_ids, account_id) @@ -524,7 +529,7 @@ class Status < ApplicationRecord def set_searchability return if searchability.nil? - if visibility == 'public' || visibility == 'public_unlisted' || visibility == 'login' + if visibility == 'public' || visibility == 'public_unlisted' || visibility == 'login' || (visibility == 'unlisted' && account.local?) self.searchability = [Status.searchabilities[searchability], Status.visibilities['public']].max elsif visibility == 'limited' self.searchability = Status.searchabilities['limited'] diff --git a/app/models/tag_feed.rb b/app/models/tag_feed.rb index 4f9c9c6d64..fe805ed0dc 100644 --- a/app/models/tag_feed.rb +++ b/app/models/tag_feed.rb @@ -23,7 +23,7 @@ class TagFeed < PublicFeed # @param [Integer] min_id # @return [Array] def get(limit, max_id = nil, since_id = nil, min_id = nil) - scope = public_scope + scope = public_search_scope scope.merge!(tagged_with_any_scope) scope.merge!(tagged_with_all_scope) diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb index a3b0e37c6c..e1552f3cb1 100644 --- a/app/models/user_settings.rb +++ b/app/models/user_settings.rb @@ -21,6 +21,7 @@ class UserSettings setting :default_privacy, default: nil, in: %w(public public_unlisted login unlisted private) setting :default_reblog_privacy, default: nil setting :default_searchability, default: :direct, in: %w(public private direct limited) + setting :disallow_unlisted_public_searchability, default: false setting :public_post_to_unlisted, default: false setting :reject_public_unlisted_subscription, default: false setting :reject_unlisted_subscription, default: false diff --git a/app/serializers/rest/custom_emoji_slim_serializer.rb b/app/serializers/rest/custom_emoji_slim_serializer.rb index 38add019f8..c5542fbd27 100644 --- a/app/serializers/rest/custom_emoji_slim_serializer.rb +++ b/app/serializers/rest/custom_emoji_slim_serializer.rb @@ -9,6 +9,7 @@ class REST::CustomEmojiSlimSerializer < ActiveModel::Serializer attribute :width, if: :width? attribute :height, if: :height? attribute :sensitive, if: :sensitive? + attribute :is_sensitive, if: :sensitive? def url full_asset_url(object.image.url) @@ -49,4 +50,8 @@ class REST::CustomEmojiSlimSerializer < ActiveModel::Serializer def sensitive object.is_sensitive end + + def is_sensitive # rubocop:disable Naming/PredicateName + sensitive + end end diff --git a/app/services/concerns/account_scope.rb b/app/services/concerns/account_scope.rb index 45a11c911e..1d617fd731 100644 --- a/app/services/concerns/account_scope.rb +++ b/app/services/concerns/account_scope.rb @@ -21,7 +21,7 @@ module AccountScope end def scope_status_mentioned(status) - status.active_mentions.joins(:account).merge(Account.local).select('account_id AS id').reorder(nil) + Account.local.where(id: status.active_mentions.select(:account_id)).reorder(nil) end # TODO: not work diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 951b766ff7..1b5a00f3d6 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -23,6 +23,8 @@ class FanOutOnWriteService < BaseService elsif broadcastable_unlisted? fan_out_to_public_recipients! fan_out_to_public_unlisted_streams! + elsif broadcastable_unlisted2? + fan_out_to_unlisted_streams! end end @@ -73,6 +75,10 @@ class FanOutOnWriteService < BaseService broadcast_to_public_unlisted_streams! end + def fan_out_to_unlisted_streams! + broadcast_to_hashtag_streams! + end + def deliver_to_self! FeedManager.instance.push_to_home(@account, @status, update: update?) if @account.local? end @@ -242,6 +248,10 @@ class FanOutOnWriteService < BaseService @status.public_unlisted_visibility? && !@status.reblog? && !@account.silenced? end + def broadcastable_unlisted2? + @status.unlisted_visibility? && @status.public_searchability? && !@status.reblog? && !@account.silenced? + end + class AntennaCollection def initialize(status, update, stl_home = false) # rubocop:disable Style/OptionalBooleanParameter @status = status diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 9859787d90..bacb9579f5 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -83,9 +83,11 @@ class PostStatusService < BaseService end def searchability + return :private if @options[:searchability]&.to_sym == :public && @visibility&.to_sym == :unlisted && @account.user&.setting_disallow_unlisted_public_searchability + case @options[:searchability]&.to_sym when :public - case @visibility&.to_sym when :public, :public_unlisted, :login then :public when :unlisted, :private then :private else :direct end + case @visibility&.to_sym when :public, :public_unlisted, :login, :unlisted then :public when :private then :private else :direct end when :private case @visibility&.to_sym when :public, :public_unlisted, :login, :unlisted, :private then :private else :direct end when :direct diff --git a/app/services/search_service.rb b/app/services/search_service.rb index 683315f7c2..ae65520b7a 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -56,7 +56,7 @@ class SearchService < BaseService privacy_definition = privacy_definition.or(StatusesIndex.filter(term: { searchability: 'limited' }).filter(term: { account_id: @account.id }).track_scores(true).min_score(@min_score)) end - definition = parsed_query.apply(StatusesIndex.min_score(@min_score).track_scores(true)) + definition = parsed_query.apply(StatusesIndex.min_score(@min_score).track_scores(true)).order(id: :desc) definition = definition.filter(term: { account_id: @options[:account_id] }) if @options[:account_id].present? definition = definition.and(privacy_definition) diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index 38bf483f95..8a89efff98 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -36,6 +36,9 @@ .fields-group = ff.input :'web.enable_login_privacy', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_enable_login_privacy'), hint: false + .fields-group + = ff.input :disallow_unlisted_public_searchability, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_disallow_unlisted_public_searchability'), hint: I18n.t('simple_form.hints.defaults.setting_disallow_unlisted_public_searchability') + .fields-group = ff.input :public_post_to_unlisted, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_public_post_to_unlisted'), hint: I18n.t('simple_form.hints.defaults.setting_public_post_to_unlisted') diff --git a/app/workers/delivery_emoji_reaction_worker.rb b/app/workers/delivery_emoji_reaction_worker.rb index 1f18ea5add..d25e5e844a 100644 --- a/app/workers/delivery_emoji_reaction_worker.rb +++ b/app/workers/delivery_emoji_reaction_worker.rb @@ -11,7 +11,7 @@ class DeliveryEmojiReactionWorker if status.present? scope_status(status).includes(:user).find_each do |account| - redis.publish("timeline:#{account.id}", payload_json) if (!account.respond_to?(:user) || !account.user&.setting_stop_emoji_reaction_streaming) && redis.exists?("subscribed:timeline:#{account.id}") + redis.publish("timeline:#{account.id}", payload_json) if (account.user.nil? || !account.user&.setting_stop_emoji_reaction_streaming) && redis.exists?("subscribed:timeline:#{account.id}") end end diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 80d210f3df..648e6a1cd7 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1645,7 +1645,7 @@ ja: limited_long: この投稿はあなたしか検索できません private: フォロワーのみ private_long: この投稿はフォロワーのみが検索できます - public: 公開 + public: 全て public_long: この投稿は誰でも検索できます show_more: もっと見る show_newer: 新しいものを表示 diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 6b981ceff5..bfafb618b6 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -218,6 +218,7 @@ en: setting_default_sensitive: Always mark media as sensitive setting_delete_modal: Show confirmation dialog before deleting a post setting_disable_swiping: Disable swiping motions + setting_disallow_unlisted_public_searchability: Disallow public searchability when unlisted visibility setting_display_media: Media display setting_display_media_default: Default setting_display_media_expand: Show more medias diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index 2663b487fc..c7b6e781c9 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -56,6 +56,7 @@ ja: setting_always_send_emails: 通常、Mastodon からメール通知は行われません。 setting_boost_modal: ブーストの公開範囲が指定できるようになります setting_default_sensitive: 閲覧注意状態のメディアはデフォルトでは内容が伏せられ、クリックして初めて閲覧できるようになります + setting_disallow_unlisted_public_searchability: この設定を有効にすると、未収載投稿と検索範囲「全て」は両立できず不特定多数からの検索が不可になります。Fedibirdと同じ挙動になります setting_display_media_default: 閲覧注意としてマークされたメディアは隠す setting_display_media_hide_all: メディアを常に隠す setting_display_media_show_all: メディアを常に表示する @@ -221,6 +222,7 @@ ja: setting_default_sensitive: メディアを常に閲覧注意としてマークする setting_delete_modal: 投稿を削除する前に確認ダイアログを表示する setting_disable_swiping: スワイプでの切り替えを無効にする + setting_disallow_unlisted_public_searchability: 未収載投稿の検索許可が「全て」だった場合、「フォロワーのみ」に変更する setting_display_media: メディアの表示 setting_display_media_default: 標準 setting_display_media_expand: 5個目以降のメディアも表示する (最大16)