Persist follow recommendations from FASP (#35218)

This commit is contained in:
David Roetzel 2025-06-30 15:39:36 +02:00 committed by GitHub
parent e8a603b18f
commit bae258925c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 151 additions and 8 deletions

View File

@ -8,6 +8,7 @@ class AccountSuggestions
AccountSuggestions::FriendsOfFriendsSource, AccountSuggestions::FriendsOfFriendsSource,
AccountSuggestions::SimilarProfilesSource, AccountSuggestions::SimilarProfilesSource,
AccountSuggestions::GlobalSource, AccountSuggestions::GlobalSource,
AccountSuggestions::FaspSource,
].freeze ].freeze
BATCH_SIZE = 40 BATCH_SIZE = 40

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AccountSuggestions::FaspSource < AccountSuggestions::Source
def get(account, limit: DEFAULT_LIMIT)
return [] unless Mastodon::Feature.fasp_enabled?
base_account_scope(account).where(id: fasp_follow_recommendations_for(account)).limit(limit).pluck(:id).map do |account_id|
[account_id, :fasp]
end
end
private
def fasp_follow_recommendations_for(account)
Fasp::FollowRecommendation.for_account(account).newest_first.select(:recommended_account_id)
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: fasp_follow_recommendations
#
# id :bigint(8) not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
# recommended_account_id :bigint(8) not null
# requesting_account_id :bigint(8) not null
#
class Fasp::FollowRecommendation < ApplicationRecord
MAX_AGE = 1.day.freeze
belongs_to :requesting_account, class_name: 'Account'
belongs_to :recommended_account, class_name: 'Account'
scope :outdated, -> { where(created_at: ...(MAX_AGE.ago)) }
scope :for_account, ->(account) { where(requesting_account: account) }
scope :newest_first, -> { order(created_at: :desc) }
end

View File

@ -23,10 +23,19 @@ class Fasp::FollowRecommendationWorker
Fasp::Request.new(provider).get("/follow_recommendation/v0/accounts?#{params}").each do |uri| Fasp::Request.new(provider).get("/follow_recommendation/v0/accounts?#{params}").each do |uri|
next if Account.where(uri:).any? next if Account.where(uri:).any?
account = fetch_service.call(uri) new_account = fetch_service.call(uri)
async_refresh.increment_result_count(by: 1) if account.present?
if new_account.present?
Fasp::FollowRecommendation.find_or_create_by(requesting_account: account, recommended_account: new_account)
async_refresh.increment_result_count(by: 1)
end
end end
end end
# Invalidate follow recommendation cache so it does not
# take up to 15 minutes for the new recommendations to
# show up
Rails.cache.delete("follow_recommendations/#{account.id}")
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
# Nothing to be done # Nothing to be done
ensure ensure

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class Scheduler::Fasp::FollowRecommendationCleanupScheduler
include Sidekiq::Worker
sidekiq_options retry: 0, lock: :until_executed, lock_ttl: 1.day.to_i
def perform
return unless Mastodon::Feature.fasp_enabled?
Fasp::FollowRecommendation.outdated.delete_all
end
end

View File

@ -68,3 +68,7 @@
interval: 1 hour interval: 1 hour
class: Scheduler::AutoCloseRegistrationsScheduler class: Scheduler::AutoCloseRegistrationsScheduler
queue: scheduler queue: scheduler
fasp_follow_recommendation_cleanup_scheduler:
interval: 1 day
class: Scheduler::Fasp::FollowRecommendationsScheduler
queue: scheduler

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
class CreateFaspFollowRecommendations < ActiveRecord::Migration[8.0]
def change
create_table :fasp_follow_recommendations do |t|
t.references :requesting_account, null: false, foreign_key: { to_table: :accounts }
t.references :recommended_account, null: false, foreign_key: { to_table: :accounts }
t.timestamps
end
end
end

View File

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_06_05_110215) do ActiveRecord::Schema[8.0].define(version: 2025_06_27_132728) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql" enable_extension "pg_catalog.plpgsql"
@ -191,8 +191,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_05_110215) do
t.boolean "hide_collections" t.boolean "hide_collections"
t.integer "avatar_storage_schema_version" t.integer "avatar_storage_schema_version"
t.integer "header_storage_schema_version" t.integer "header_storage_schema_version"
t.integer "suspension_origin"
t.datetime "sensitized_at", precision: nil t.datetime "sensitized_at", precision: nil
t.integer "suspension_origin"
t.boolean "trendable" t.boolean "trendable"
t.datetime "reviewed_at", precision: nil t.datetime "reviewed_at", precision: nil
t.datetime "requested_review_at", precision: nil t.datetime "requested_review_at", precision: nil
@ -465,6 +465,15 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_05_110215) do
t.index ["fasp_provider_id"], name: "index_fasp_debug_callbacks_on_fasp_provider_id" t.index ["fasp_provider_id"], name: "index_fasp_debug_callbacks_on_fasp_provider_id"
end end
create_table "fasp_follow_recommendations", force: :cascade do |t|
t.bigint "requesting_account_id", null: false
t.bigint "recommended_account_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["recommended_account_id"], name: "index_fasp_follow_recommendations_on_recommended_account_id"
t.index ["requesting_account_id"], name: "index_fasp_follow_recommendations_on_requesting_account_id"
end
create_table "fasp_providers", force: :cascade do |t| create_table "fasp_providers", force: :cascade do |t|
t.boolean "confirmed", default: false, null: false t.boolean "confirmed", default: false, null: false
t.string "name", null: false t.string "name", null: false
@ -604,12 +613,12 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_05_110215) do
end end
create_table "ip_blocks", force: :cascade do |t| create_table "ip_blocks", force: :cascade do |t|
t.datetime "created_at", precision: nil, null: false
t.datetime "updated_at", precision: nil, null: false
t.datetime "expires_at", precision: nil
t.inet "ip", default: "0.0.0.0", null: false t.inet "ip", default: "0.0.0.0", null: false
t.integer "severity", default: 0, null: false t.integer "severity", default: 0, null: false
t.datetime "expires_at", precision: nil
t.text "comment", default: "", null: false t.text "comment", default: "", null: false
t.datetime "created_at", precision: nil, null: false
t.datetime "updated_at", precision: nil, null: false
t.index ["ip"], name: "index_ip_blocks_on_ip", unique: true t.index ["ip"], name: "index_ip_blocks_on_ip", unique: true
end end
@ -1367,6 +1376,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_05_110215) do
add_foreign_key "email_domain_blocks", "email_domain_blocks", column: "parent_id", on_delete: :cascade add_foreign_key "email_domain_blocks", "email_domain_blocks", column: "parent_id", on_delete: :cascade
add_foreign_key "fasp_backfill_requests", "fasp_providers" add_foreign_key "fasp_backfill_requests", "fasp_providers"
add_foreign_key "fasp_debug_callbacks", "fasp_providers" add_foreign_key "fasp_debug_callbacks", "fasp_providers"
add_foreign_key "fasp_follow_recommendations", "accounts", column: "recommended_account_id"
add_foreign_key "fasp_follow_recommendations", "accounts", column: "requesting_account_id"
add_foreign_key "fasp_subscriptions", "fasp_providers" add_foreign_key "fasp_subscriptions", "fasp_providers"
add_foreign_key "favourites", "accounts", name: "fk_5eb6c2b873", on_delete: :cascade add_foreign_key "favourites", "accounts", name: "fk_5eb6c2b873", on_delete: :cascade
add_foreign_key "favourites", "statuses", name: "fk_b0e856845e", on_delete: :cascade add_foreign_key "favourites", "statuses", name: "fk_b0e856845e", on_delete: :cascade

View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
Fabricator(:fasp_follow_recommendation, from: 'Fasp::FollowRecommendation') do
requesting_account { Fabricate.build(:account) }
recommended_account { Fabricate.build(:account, domain: 'fedi.example.com') }
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AccountSuggestions::FaspSource do
describe '#get', feature: :fasp do
subject { described_class.new }
let(:bob) { Fabricate(:account) }
let(:alice) { Fabricate(:account, domain: 'fedi.example.com') }
let(:eve) { Fabricate(:account, domain: 'fedi.example.com') }
before do
[alice, eve].each do |recommended_account|
Fasp::FollowRecommendation.create!(requesting_account: bob, recommended_account:)
end
end
it 'returns recommendations obtained by FASP' do
expect(subject.get(bob)).to contain_exactly([alice.id, :fasp], [eve.id, :fasp])
end
end
end

View File

@ -7,7 +7,7 @@ RSpec.describe Fasp::FollowRecommendationWorker, feature: :fasp do
let(:provider) { Fabricate(:follow_recommendation_fasp) } let(:provider) { Fabricate(:follow_recommendation_fasp) }
let(:account) { Fabricate(:account) } let(:account) { Fabricate(:account) }
let(:fetch_service) { instance_double(ActivityPub::FetchRemoteActorService, call: true) } let(:fetch_service) { instance_double(ActivityPub::FetchRemoteActorService) }
let!(:stubbed_request) do let!(:stubbed_request) do
account_uri = ActivityPub::TagManager.instance.uri_for(account) account_uri = ActivityPub::TagManager.instance.uri_for(account)
@ -23,6 +23,8 @@ RSpec.describe Fasp::FollowRecommendationWorker, feature: :fasp do
before do before do
allow(ActivityPub::FetchRemoteActorService).to receive(:new).and_return(fetch_service) allow(ActivityPub::FetchRemoteActorService).to receive(:new).and_return(fetch_service)
allow(fetch_service).to receive(:call).and_invoke(->(_) { Fabricate(:account, domain: 'fedi.example.com') })
end end
it "sends the requesting account's uri to provider and fetches received account uris" do it "sends the requesting account's uri to provider and fetches received account uris" do
@ -48,4 +50,10 @@ RSpec.describe Fasp::FollowRecommendationWorker, feature: :fasp do
expect(async_refresh.reload.result_count).to eq 2 expect(async_refresh.reload.result_count).to eq 2
end end
it 'persists the results' do
expect do
described_class.new.perform(account.id)
end.to change(Fasp::FollowRecommendation, :count).by(2)
end
end end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Scheduler::Fasp::FollowRecommendationCleanupScheduler do
let(:worker) { described_class.new }
describe '#perform', feature: :fasp do
before do
Fabricate(:fasp_follow_recommendation, created_at: 2.days.ago)
end
it 'deletes outdated recommendations' do
expect { worker.perform }.to change(Fasp::FollowRecommendation, :count).by(-1)
end
end
end