diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb index 567b10ade2..572d8fd059 100644 --- a/app/lib/search_query_transformer.rb +++ b/app/lib/search_query_transformer.rb @@ -199,10 +199,7 @@ class SearchQueryTransformer < Parslet::Transform def following_account_ids return @following_account_ids if defined?(@following_account_ids) - account_exists_sql = Account.where('accounts.id = follows.target_account_id').where(searchability: %w(public public_unlisted private)).reorder(nil).select(1).to_sql - status_exists_sql = Status.where('statuses.account_id = follows.target_account_id').where(reblog_of_id: nil).where(searchability: %w(public public_unlisted private)).reorder(nil).select(1).to_sql - following_accounts = Follow.where(account_id: @options[:current_account].id).merge(Account.where("EXISTS (#{account_exists_sql})").or(Account.where("EXISTS (#{status_exists_sql})"))) - @following_account_ids = following_accounts.pluck(:target_account_id) + @following_account_ids = @options[:current_account].following.includes(:account_stat).where(account_stat: { searchable_by_follower: true }).select(:id).pluck(:id) end end diff --git a/app/models/account_stat.rb b/app/models/account_stat.rb index 622edb9c22..a680079c92 100644 --- a/app/models/account_stat.rb +++ b/app/models/account_stat.rb @@ -13,6 +13,7 @@ # updated_at :datetime not null # last_status_at :datetime # group_activitypub_count :integer +# searchable_by_follower :boolean default(FALSE), not null # class AccountStat < ApplicationRecord diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb index b05fa19476..a8cdcdccf6 100644 --- a/app/models/domain_block.rb +++ b/app/models/domain_block.rb @@ -28,6 +28,7 @@ # hidden_anonymous :boolean default(FALSE), not null # detect_invalid_subscription :boolean default(FALSE), not null # reject_reply_exclude_followers :boolean default(FALSE), not null +# reject_friend :boolean default(FALSE), not null # class DomainBlock < ApplicationRecord diff --git a/app/models/status.rb b/app/models/status.rb index 5a33411aba..4aa490a0a0 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -157,6 +157,9 @@ class Status < ApplicationRecord after_create_commit :store_uri, if: :local? after_create_commit :update_statistics, if: :local? + after_create_commit :set_searchable_follow_on_create + after_destroy_commit :set_searchable_follow_on_destroy + before_validation :prepare_contents, if: :local? before_validation :set_reblog before_validation :set_visibility @@ -670,6 +673,21 @@ class Status < ApplicationRecord ActivityTracker.increment('activity:statuses:local') end + def set_searchable_follow_on_create + return unless public_searchability? || public_unlisted_searchability? || private_searchability? + return if account.account_stat.nil? || account.account_stat.searchable_by_follower + + account.account_stat.update(searchable_by_follower: true) + end + + def set_searchable_follow_on_destroy + return unless public_searchability? || public_unlisted_searchability? || private_searchability? + return if account.account_stat.nil? || !account.account_stat.searchable_by_follower + return if account.statuses.exists?(searchability: %i(public public_unlisted unlisted private)) + + account.account_stat.update(searchable_by_follower: false) + end + def increment_counter_caches return if direct_visibility? diff --git a/db/migrate/20231007090807_add_searchable_follow_to_account_stats.rb b/db/migrate/20231007090807_add_searchable_follow_to_account_stats.rb new file mode 100644 index 0000000000..ab55a41483 --- /dev/null +++ b/db/migrate/20231007090807_add_searchable_follow_to_account_stats.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require Rails.root.join('lib', 'mastodon', 'migration_helpers') + +class AddSearchableFollowToAccountStats < ActiveRecord::Migration[7.0] + include Mastodon::MigrationHelpers + + disable_ddl_transaction! + + class AccountStat < ApplicationRecord; end + + def change + safety_assured do + add_column_with_default :account_stats, :searchable_by_follower, :boolean, default: false, allow_null: false + + AccountStat.where('EXISTS (SELECT 1 FROM statuses WHERE searchability IN (0, 10, 1) AND account_id = account_stats.account_id)') + .update_all(searchable_by_follower: true) # rubocop:disable Rails/SkipsModelValidations + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 8ee0450744..8e0b1a96c4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_10_01_050733) do +ActiveRecord::Schema[7.0].define(version: 2023_10_07_090807) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -108,6 +108,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_01_050733) do t.datetime "updated_at", precision: nil, null: false t.datetime "last_status_at", precision: nil t.integer "group_activitypub_count" + t.boolean "searchable_by_follower", default: false, null: false t.index ["account_id"], name: "index_account_stats_on_account_id", unique: true t.index ["last_status_at", "account_id"], name: "index_account_stats_on_last_status_at_and_account_id", order: { last_status_at: "DESC NULLS LAST" } end @@ -584,6 +585,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_01_050733) do t.boolean "hidden_anonymous", default: false, null: false t.boolean "detect_invalid_subscription", default: false, null: false t.boolean "reject_reply_exclude_followers", default: false, null: false + t.boolean "reject_friend", default: false, null: false t.index ["domain"], name: "index_domain_blocks_on_domain", unique: true end @@ -676,6 +678,23 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_01_050733) do t.index ["target_account_id"], name: "index_follows_on_target_account_id" end + create_table "friend_domains", force: :cascade do |t| + t.string "domain", default: "", null: false + t.string "inbox_url", default: "", null: false + t.integer "active_state", default: 0, null: false + t.integer "passive_state", default: 0, null: false + t.string "active_follow_activity_id" + t.string "passive_follow_activity_id" + t.boolean "available", default: true, null: false + t.boolean "public_unlisted", default: true, null: false + t.boolean "pseudo_relay", default: false, null: false + t.boolean "unlocked", default: false, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["domain"], name: "index_friend_domains_on_domain", unique: true + t.index ["inbox_url"], name: "index_friend_domains_on_inbox_url", unique: true + end + create_table "identities", force: :cascade do |t| t.string "provider", default: "", null: false t.string "uid", default: "", null: false diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb index 96f6bb56c9..7996a4fea5 100644 --- a/spec/models/status_spec.rb +++ b/spec/models/status_spec.rb @@ -571,5 +571,34 @@ RSpec.describe Status do status.reload expect(status.uri).to start_with('https://') end + + it 'saves as searchable by followers' do + status = described_class.create(account: alice, text: 'foo', searchability: :public) + expect(status.account.account_stat.searchable_by_follower).to be true + end + end + + describe 'after destroy' do + it 'saves as not searchable by followers' do + status = described_class.create(account: alice, text: 'foo', searchability: :public) + status.destroy + expect(status.account.account_stat.searchable_by_follower).to be false + end + + it 'on multiple posts' do + status1 = described_class.create(account: alice, text: 'foo', searchability: :public) + status2 = described_class.create(account: alice, text: 'foo', searchability: :public) + status3 = described_class.create(account: alice, text: 'foo', searchability: :public) + status4 = described_class.create(account: alice, text: 'foo', searchability: :public) + status5 = described_class.create(account: alice, text: 'foo', searchability: :public) + status1.destroy + status2.destroy + status3.destroy + expect(alice.account_stat.searchable_by_follower).to be true + status4.destroy + expect(alice.account_stat.searchable_by_follower).to be true + status5.destroy + expect(alice.account_stat.searchable_by_follower).to be false + end end end