mirror of
https://github.com/mastodon/mastodon.git
synced 2025-07-15 08:48:15 +00:00
Persist follow recommendations from FASP (#35218)
This commit is contained in:
parent
e8a603b18f
commit
bae258925c
|
@ -8,6 +8,7 @@ class AccountSuggestions
|
|||
AccountSuggestions::FriendsOfFriendsSource,
|
||||
AccountSuggestions::SimilarProfilesSource,
|
||||
AccountSuggestions::GlobalSource,
|
||||
AccountSuggestions::FaspSource,
|
||||
].freeze
|
||||
|
||||
BATCH_SIZE = 40
|
||||
|
|
17
app/models/account_suggestions/fasp_source.rb
Normal file
17
app/models/account_suggestions/fasp_source.rb
Normal 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
|
22
app/models/fasp/follow_recommendation.rb
Normal file
22
app/models/fasp/follow_recommendation.rb
Normal 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
|
|
@ -23,10 +23,19 @@ class Fasp::FollowRecommendationWorker
|
|||
Fasp::Request.new(provider).get("/follow_recommendation/v0/accounts?#{params}").each do |uri|
|
||||
next if Account.where(uri:).any?
|
||||
|
||||
account = fetch_service.call(uri)
|
||||
async_refresh.increment_result_count(by: 1) if account.present?
|
||||
new_account = fetch_service.call(uri)
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
# Nothing to be done
|
||||
ensure
|
||||
|
|
|
@ -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
|
|
@ -68,3 +68,7 @@
|
|||
interval: 1 hour
|
||||
class: Scheduler::AutoCloseRegistrationsScheduler
|
||||
queue: scheduler
|
||||
fasp_follow_recommendation_cleanup_scheduler:
|
||||
interval: 1 day
|
||||
class: Scheduler::Fasp::FollowRecommendationsScheduler
|
||||
queue: scheduler
|
||||
|
|
|
@ -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
|
21
db/schema.rb
21
db/schema.rb
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# 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
|
||||
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.integer "avatar_storage_schema_version"
|
||||
t.integer "header_storage_schema_version"
|
||||
t.integer "suspension_origin"
|
||||
t.datetime "sensitized_at", precision: nil
|
||||
t.integer "suspension_origin"
|
||||
t.boolean "trendable"
|
||||
t.datetime "reviewed_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"
|
||||
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|
|
||||
t.boolean "confirmed", default: false, null: false
|
||||
t.string "name", null: false
|
||||
|
@ -604,12 +613,12 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_05_110215) do
|
|||
end
|
||||
|
||||
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.integer "severity", default: 0, null: false
|
||||
t.datetime "expires_at", precision: nil
|
||||
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
|
||||
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 "fasp_backfill_requests", "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 "favourites", "accounts", name: "fk_5eb6c2b873", on_delete: :cascade
|
||||
add_foreign_key "favourites", "statuses", name: "fk_b0e856845e", on_delete: :cascade
|
||||
|
|
|
@ -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
|
23
spec/models/account_suggestions/fasp_source_spec.rb
Normal file
23
spec/models/account_suggestions/fasp_source_spec.rb
Normal 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
|
|
@ -7,7 +7,7 @@ RSpec.describe Fasp::FollowRecommendationWorker, feature: :fasp do
|
|||
|
||||
let(:provider) { Fabricate(:follow_recommendation_fasp) }
|
||||
let(:account) { Fabricate(:account) }
|
||||
let(:fetch_service) { instance_double(ActivityPub::FetchRemoteActorService, call: true) }
|
||||
let(:fetch_service) { instance_double(ActivityPub::FetchRemoteActorService) }
|
||||
|
||||
let!(:stubbed_request) do
|
||||
account_uri = ActivityPub::TagManager.instance.uri_for(account)
|
||||
|
@ -23,6 +23,8 @@ RSpec.describe Fasp::FollowRecommendationWorker, feature: :fasp do
|
|||
|
||||
before do
|
||||
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
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
it 'persists the results' do
|
||||
expect do
|
||||
described_class.new.perform(account.id)
|
||||
end.to change(Fasp::FollowRecommendation, :count).by(2)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue
Block a user