From 28dc6e5f094c1afa570730ab3964ee2605d160ea Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Mon, 16 Dec 2024 10:50:01 +0100 Subject: [PATCH 01/15] Humble beginnings of fasp data sharing. Subscriptions can be made and new statuses will be announced to subscribed fasp. --- .../v0/backfill_requests_controller.rb | 4 ++ .../v0/event_subscriptions_controller.rb | 25 ++++++++++++ app/lib/fasp/request.rb | 1 + app/models/concerns/status/fasp_concern.rb | 16 ++++++++ app/models/fasp/provider.rb | 1 + app/models/fasp/subscription.rb | 40 +++++++++++++++++++ app/models/status.rb | 1 + .../fasp/announce_new_content_worker.rb | 28 +++++++++++++ config/routes/fasp.rb | 8 ++++ config/sidekiq.yml | 1 + ...0241213130230_create_fasp_subscriptions.rb | 18 +++++++++ db/schema.rb | 15 +++++++ .../fasp/subscription_fabricator.rb | 12 ++++++ spec/models/fasp/subscription_spec.rb | 7 ++++ .../data_sharing/v0/backfill_requests_spec.rb | 9 +++++ .../v0/event_subscriptions_spec.rb | 9 +++++ 16 files changed, 195 insertions(+) create mode 100644 app/controllers/api/fasp/data_sharing/v0/backfill_requests_controller.rb create mode 100644 app/controllers/api/fasp/data_sharing/v0/event_subscriptions_controller.rb create mode 100644 app/models/concerns/status/fasp_concern.rb create mode 100644 app/models/fasp/subscription.rb create mode 100644 app/workers/fasp/announce_new_content_worker.rb create mode 100644 db/migrate/20241213130230_create_fasp_subscriptions.rb create mode 100644 spec/fabricators/fasp/subscription_fabricator.rb create mode 100644 spec/models/fasp/subscription_spec.rb create mode 100644 spec/requests/api/fasp/data_sharing/v0/backfill_requests_spec.rb create mode 100644 spec/requests/api/fasp/data_sharing/v0/event_subscriptions_spec.rb diff --git a/app/controllers/api/fasp/data_sharing/v0/backfill_requests_controller.rb b/app/controllers/api/fasp/data_sharing/v0/backfill_requests_controller.rb new file mode 100644 index 0000000000..a5d5d8b791 --- /dev/null +++ b/app/controllers/api/fasp/data_sharing/v0/backfill_requests_controller.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class Api::Fasp::DataSharing::V0::BackfillRequestsController < ApplicationController +end diff --git a/app/controllers/api/fasp/data_sharing/v0/event_subscriptions_controller.rb b/app/controllers/api/fasp/data_sharing/v0/event_subscriptions_controller.rb new file mode 100644 index 0000000000..29e03d5836 --- /dev/null +++ b/app/controllers/api/fasp/data_sharing/v0/event_subscriptions_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class Api::Fasp::DataSharing::V0::EventSubscriptionsController < Api::Fasp::BaseController + def create + subscription = current_provider.fasp_subscriptions.create!(subscription_params) + + render json: { subscription: { id: subscription.id } }, status: 201 + end + + def destroy + subscription = current_provider.fasp_subscriptions.find(params[:id]) + subscription.destroy + + head 204 + end + + private + + def subscription_params + params + .permit(:category, :subscriptionType, :maxBatchSize, threshold: {}) + .to_unsafe_h + .transform_keys { |k| k.to_s.underscore } + end +end diff --git a/app/lib/fasp/request.rb b/app/lib/fasp/request.rb index 2addbe8502..7d8c05d406 100644 --- a/app/lib/fasp/request.rb +++ b/app/lib/fasp/request.rb @@ -32,6 +32,7 @@ class Fasp::Request def request_headers(verb, url, body = '') result = { 'accept' => 'application/json', + 'content-type' => 'application/json', 'content-digest' => content_digest(body), } result.merge(signature_headers(verb, url, result)) diff --git a/app/models/concerns/status/fasp_concern.rb b/app/models/concerns/status/fasp_concern.rb new file mode 100644 index 0000000000..748f22fb0f --- /dev/null +++ b/app/models/concerns/status/fasp_concern.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Status::FaspConcern + extend ActiveSupport::Concern + + included do + after_commit :announce_new_content_to_subscribed_fasp, on: :create + end + + private + + def announce_new_content_to_subscribed_fasp + store_uri unless uri # TODO: solve this more elegantly + Fasp::AnnounceNewContentWorker.perform_async(uri) + end +end diff --git a/app/models/fasp/provider.rb b/app/models/fasp/provider.rb index cd1b3008c7..e0457266e8 100644 --- a/app/models/fasp/provider.rb +++ b/app/models/fasp/provider.rb @@ -23,6 +23,7 @@ class Fasp::Provider < ApplicationRecord include DebugConcern has_many :fasp_debug_callbacks, inverse_of: :fasp_provider, class_name: 'Fasp::DebugCallback', dependent: :delete_all + has_many :fasp_subscriptions, inverse_of: :fasp_provider, class_name: 'Fasp::Subscription', dependent: :delete_all validates :name, presence: true validates :base_url, presence: true, url: true diff --git a/app/models/fasp/subscription.rb b/app/models/fasp/subscription.rb new file mode 100644 index 0000000000..4c98db27ab --- /dev/null +++ b/app/models/fasp/subscription.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: fasp_subscriptions +# +# id :bigint(8) not null, primary key +# category :string not null +# max_batch_size :integer not null +# subscription_type :string not null +# threshold_likes :integer +# threshold_replies :integer +# threshold_shares :integer +# threshold_timeframe :integer +# created_at :datetime not null +# updated_at :datetime not null +# fasp_provider_id :bigint(8) not null +# +class Fasp::Subscription < ApplicationRecord + CATEGORIES = %w(account content).freeze + TYPES = %w(lifecycle trends).freeze + + belongs_to :fasp_provider, class_name: 'Fasp::Provider' + + validates :category, presence: true, inclusion: CATEGORIES + validates :subscription_type, presence: true, + inclusion: TYPES + + scope :content, -> { where(category: 'content') } + scope :account, -> { where(category: 'account') } + scope :lifecycle, -> { where(subscription_type: 'lifecycle') } + scope :trends, -> { where(subscription_type: 'trends') } + + def threshold=(threshold) + self.threshold_timeframe = threshold['timeframe'] || 15 + self.threshold_shares = threshold['shares'] || 3 + self.threshold_likes = threshold['likes'] || 3 + self.threshold_replies = threshold['replies'] || 3 + end +end diff --git a/app/models/status.rb b/app/models/status.rb index cdff5a2ac3..13560628b2 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -35,6 +35,7 @@ class Status < ApplicationRecord include Discard::Model include Paginable include RateLimitable + include Status::FaspConcern include Status::FetchRepliesConcern include Status::SafeReblogInsert include Status::SearchConcern diff --git a/app/workers/fasp/announce_new_content_worker.rb b/app/workers/fasp/announce_new_content_worker.rb new file mode 100644 index 0000000000..cc7b3d3b68 --- /dev/null +++ b/app/workers/fasp/announce_new_content_worker.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class Fasp::AnnounceNewContentWorker + include Sidekiq::Worker + + sidekiq_options queue: 'fasp', retry: 5 + + def perform(uri) + Fasp::Subscription.includes(:fasp_provider).content.lifecycle.each do |subscription| + announce(subscription, uri) + end + end + + private + + def announce(subscription, uri) + Fasp::Request.new(subscription.fasp_provider).post('/data_sharing/v0/announcements', body: { + source: { + subscription: { + id: subscription.id.to_s, + }, + }, + category: 'content', + eventType: 'new', + objectUris: [uri], + }) + end +end diff --git a/config/routes/fasp.rb b/config/routes/fasp.rb index 9d052526de..cb9b894290 100644 --- a/config/routes/fasp.rb +++ b/config/routes/fasp.rb @@ -10,6 +10,14 @@ namespace :api, format: false do end end + namespace :data_sharing do + namespace :v0 do + resources :backfill_requests, only: [:create] + + resources :event_subscriptions, only: [:create, :destroy] + end + end + resource :registration, only: [:create] end end diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 488c2f2ab3..9bfc7e9984 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -7,6 +7,7 @@ - [mailers, 2] - [pull] - [scheduler] + - [fasp] :scheduler: :listened_queues_only: true diff --git a/db/migrate/20241213130230_create_fasp_subscriptions.rb b/db/migrate/20241213130230_create_fasp_subscriptions.rb new file mode 100644 index 0000000000..7037022303 --- /dev/null +++ b/db/migrate/20241213130230_create_fasp_subscriptions.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class CreateFaspSubscriptions < ActiveRecord::Migration[7.2] + def change + create_table :fasp_subscriptions do |t| + t.string :category, null: false + t.string :subscription_type, null: false + t.integer :max_batch_size, null: false + t.integer :threshold_timeframe + t.integer :threshold_shares + t.integer :threshold_likes + t.integer :threshold_replies + t.references :fasp_provider, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 26db259464..253a1a1f97 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -471,6 +471,20 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_13_123400) do t.index ["base_url"], name: "index_fasp_providers_on_base_url", unique: true end + create_table "fasp_subscriptions", force: :cascade do |t| + t.string "category", null: false + t.string "subscription_type", null: false + t.integer "max_batch_size", null: false + t.integer "threshold_timeframe" + t.integer "threshold_shares" + t.integer "threshold_likes" + t.integer "threshold_replies" + t.bigint "fasp_provider_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["fasp_provider_id"], name: "index_fasp_subscriptions_on_fasp_provider_id" + end + create_table "favourites", force: :cascade do |t| t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false @@ -1316,6 +1330,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_13_123400) do add_foreign_key "custom_filters", "accounts", on_delete: :cascade add_foreign_key "email_domain_blocks", "email_domain_blocks", column: "parent_id", on_delete: :cascade add_foreign_key "fasp_debug_callbacks", "fasp_providers" + 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 add_foreign_key "featured_tags", "accounts", on_delete: :cascade diff --git a/spec/fabricators/fasp/subscription_fabricator.rb b/spec/fabricators/fasp/subscription_fabricator.rb new file mode 100644 index 0000000000..d560a0efce --- /dev/null +++ b/spec/fabricators/fasp/subscription_fabricator.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +Fabricator('Fasp::Subscription') do + category 'MyString' + subscription_type 'MyString' + max_batch_size 1 + threshold_timeframe 1 + threshold_shares 1 + threshold_likes 1 + threshold_replies 1 + fasp_provider nil +end diff --git a/spec/models/fasp/subscription_spec.rb b/spec/models/fasp/subscription_spec.rb new file mode 100644 index 0000000000..493fe3e345 --- /dev/null +++ b/spec/models/fasp/subscription_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Fasp::Subscription do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/requests/api/fasp/data_sharing/v0/backfill_requests_spec.rb b/spec/requests/api/fasp/data_sharing/v0/backfill_requests_spec.rb new file mode 100644 index 0000000000..d4987261b2 --- /dev/null +++ b/spec/requests/api/fasp/data_sharing/v0/backfill_requests_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Api::Fasp::DataSharing::V0::BackfillRequests' do + describe 'GET /index' do + pending "add some examples (or delete) #{__FILE__}" + end +end diff --git a/spec/requests/api/fasp/data_sharing/v0/event_subscriptions_spec.rb b/spec/requests/api/fasp/data_sharing/v0/event_subscriptions_spec.rb new file mode 100644 index 0000000000..8f6b8873db --- /dev/null +++ b/spec/requests/api/fasp/data_sharing/v0/event_subscriptions_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Api::Fasp::DataSharing::V0::EventSubscriptions' do + describe 'GET /index' do + pending "add some examples (or delete) #{__FILE__}" + end +end From efc18aee68a824811ff630419af8f52e643d9485 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Mon, 16 Dec 2024 15:50:59 +0100 Subject: [PATCH 02/15] Handle full content lifecycle for subscribed fasps --- app/models/concerns/status/fasp_concern.rb | 12 +++++++- ...announce_content_lifecycle_event_worker.rb | 28 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 app/workers/fasp/announce_content_lifecycle_event_worker.rb diff --git a/app/models/concerns/status/fasp_concern.rb b/app/models/concerns/status/fasp_concern.rb index 748f22fb0f..84b95dcc5b 100644 --- a/app/models/concerns/status/fasp_concern.rb +++ b/app/models/concerns/status/fasp_concern.rb @@ -5,12 +5,22 @@ module Status::FaspConcern included do after_commit :announce_new_content_to_subscribed_fasp, on: :create + after_commit :announce_updated_content_to_subscribed_fasp, on: :update + after_commit :announce_deleted_content_to_subscribed_fasp, on: :destroy end private def announce_new_content_to_subscribed_fasp store_uri unless uri # TODO: solve this more elegantly - Fasp::AnnounceNewContentWorker.perform_async(uri) + Fasp::AnnounceContentLifecycleEventWorker.perform_async(uri, 'new') + end + + def announce_updated_content_to_subscribed_fasp + Fasp::AnnounceContentLifecycleEventWorker.perform_async(uri, 'update') + end + + def announce_deleted_content_to_subscribed_fasp + Fasp::AnnounceContentLifecycleEventWorker.perform_async(uri, 'delete') end end diff --git a/app/workers/fasp/announce_content_lifecycle_event_worker.rb b/app/workers/fasp/announce_content_lifecycle_event_worker.rb new file mode 100644 index 0000000000..f18b45fae0 --- /dev/null +++ b/app/workers/fasp/announce_content_lifecycle_event_worker.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class Fasp::AnnounceContentLifecycleEventWorker + include Sidekiq::Worker + + sidekiq_options queue: 'fasp', retry: 5 + + def perform(uri, event_type) + Fasp::Subscription.includes(:fasp_provider).content.lifecycle.each do |subscription| + announce(subscription, uri, event_type) + end + end + + private + + def announce(subscription, uri, event_type) + Fasp::Request.new(subscription.fasp_provider).post('/data_sharing/v0/announcements', body: { + source: { + subscription: { + id: subscription.id.to_s, + }, + }, + category: 'content', + eventType: event_type, + objectUris: [uri], + }) + end +end From 75266ec2c11a3afe29e28b3438db3be5bba5b178 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Wed, 18 Dec 2024 09:51:44 +0100 Subject: [PATCH 03/15] Add account and trend data sharing --- app/models/account.rb | 1 + app/models/concerns/account/fasp_concern.rb | 28 +++++++++ app/models/concerns/favourite/fasp_concern.rb | 15 +++++ app/models/concerns/status/fasp_concern.rb | 11 ++++ app/models/fasp/subscription.rb | 4 ++ app/models/favourite.rb | 1 + ...nnounce_account_lifecycle_event_worker.rb} | 14 ++--- app/workers/fasp/announce_trend_worker.rb | 59 +++++++++++++++++++ 8 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 app/models/concerns/account/fasp_concern.rb create mode 100644 app/models/concerns/favourite/fasp_concern.rb rename app/workers/fasp/{announce_new_content_worker.rb => announce_account_lifecycle_event_worker.rb} (57%) create mode 100644 app/workers/fasp/announce_trend_worker.rb diff --git a/app/models/account.rb b/app/models/account.rb index 53bf2407e8..22b8bce601 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -85,6 +85,7 @@ class Account < ApplicationRecord include Account::Associations include Account::Avatar include Account::Counters + include Account::FaspConcern include Account::FinderConcern include Account::Header include Account::Interactions diff --git a/app/models/concerns/account/fasp_concern.rb b/app/models/concerns/account/fasp_concern.rb new file mode 100644 index 0000000000..01717d396c --- /dev/null +++ b/app/models/concerns/account/fasp_concern.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Account::FaspConcern + extend ActiveSupport::Concern + + included do + after_commit :announce_new_account_to_subscribed_fasp, on: :create + after_commit :announce_updated_account_to_subscribed_fasp, on: :update + after_commit :announce_deleted_account_to_subscribed_fasp, on: :destroy + end + + private + + def announce_new_account_to_subscribed_fasp + uri = ActivityPub::TagManager.instance.uri_for(self) + Fasp::AnnounceAccountLifecycleEventWorker.perform_async(uri, 'new') + end + + def announce_updated_account_to_subscribed_fasp + uri = ActivityPub::TagManager.instance.uri_for(self) + Fasp::AnnounceAccountLifecycleEventWorker.perform_async(uri, 'update') + end + + def announce_deleted_account_to_subscribed_fasp + uri = ActivityPub::TagManager.instance.uri_for(self) + Fasp::AnnounceAccountLifecycleEventWorker.perform_async(uri, 'delete') + end +end diff --git a/app/models/concerns/favourite/fasp_concern.rb b/app/models/concerns/favourite/fasp_concern.rb new file mode 100644 index 0000000000..2293411f3d --- /dev/null +++ b/app/models/concerns/favourite/fasp_concern.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Favourite::FaspConcern + extend ActiveSupport::Concern + + included do + after_commit :announce_trends_to_subscribed_fasp, on: :create + end + + private + + def announce_trends_to_subscribed_fasp + Fasp::AnnounceTrendWorker.perform_async(status_id, 'favourite') + end +end diff --git a/app/models/concerns/status/fasp_concern.rb b/app/models/concerns/status/fasp_concern.rb index 84b95dcc5b..30c7898324 100644 --- a/app/models/concerns/status/fasp_concern.rb +++ b/app/models/concerns/status/fasp_concern.rb @@ -7,6 +7,7 @@ module Status::FaspConcern after_commit :announce_new_content_to_subscribed_fasp, on: :create after_commit :announce_updated_content_to_subscribed_fasp, on: :update after_commit :announce_deleted_content_to_subscribed_fasp, on: :destroy + after_commit :announce_trends_to_subscribed_fasp, on: :create end private @@ -23,4 +24,14 @@ module Status::FaspConcern def announce_deleted_content_to_subscribed_fasp Fasp::AnnounceContentLifecycleEventWorker.perform_async(uri, 'delete') end + + def announce_trends_to_subscribed_fasp + candidate_id, trend_source = + if reblog_of_id + [reblog_of_id, 'reblog'] + elsif in_reply_to_id + [in_reply_to_id, 'reply'] + end + Fasp::AnnounceTrendWorker.perform_async(candidate_id, trend_source) if candidate_id + end end diff --git a/app/models/fasp/subscription.rb b/app/models/fasp/subscription.rb index 4c98db27ab..1523613450 100644 --- a/app/models/fasp/subscription.rb +++ b/app/models/fasp/subscription.rb @@ -37,4 +37,8 @@ class Fasp::Subscription < ApplicationRecord self.threshold_likes = threshold['likes'] || 3 self.threshold_replies = threshold['replies'] || 3 end + + def timeframe_start + threshold_timeframe.minutes.ago + end end diff --git a/app/models/favourite.rb b/app/models/favourite.rb index 042f72beae..7bf793e2a1 100644 --- a/app/models/favourite.rb +++ b/app/models/favourite.rb @@ -13,6 +13,7 @@ class Favourite < ApplicationRecord include Paginable + include Favourite::FaspConcern update_index('statuses', :status) diff --git a/app/workers/fasp/announce_new_content_worker.rb b/app/workers/fasp/announce_account_lifecycle_event_worker.rb similarity index 57% rename from app/workers/fasp/announce_new_content_worker.rb rename to app/workers/fasp/announce_account_lifecycle_event_worker.rb index cc7b3d3b68..9a19892e73 100644 --- a/app/workers/fasp/announce_new_content_worker.rb +++ b/app/workers/fasp/announce_account_lifecycle_event_worker.rb @@ -1,27 +1,27 @@ # frozen_string_literal: true -class Fasp::AnnounceNewContentWorker +class Fasp::AnnounceAccountLifecycleEventWorker include Sidekiq::Worker sidekiq_options queue: 'fasp', retry: 5 - def perform(uri) - Fasp::Subscription.includes(:fasp_provider).content.lifecycle.each do |subscription| - announce(subscription, uri) + def perform(uri, event_type) + Fasp::Subscription.includes(:fasp_provider).account.lifecycle.each do |subscription| + announce(subscription, uri, event_type) end end private - def announce(subscription, uri) + def announce(subscription, uri, event_type) Fasp::Request.new(subscription.fasp_provider).post('/data_sharing/v0/announcements', body: { source: { subscription: { id: subscription.id.to_s, }, }, - category: 'content', - eventType: 'new', + category: 'account', + eventType: event_type, objectUris: [uri], }) end diff --git a/app/workers/fasp/announce_trend_worker.rb b/app/workers/fasp/announce_trend_worker.rb new file mode 100644 index 0000000000..0d5fc6db48 --- /dev/null +++ b/app/workers/fasp/announce_trend_worker.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +class Fasp::AnnounceTrendWorker + include Sidekiq::Worker + + sidekiq_options queue: 'fasp', retry: 5 + + def perform(status_id, trend_source) + status = ::Status.find(status_id) + Fasp::Subscription.includes(:fasp_provider).content.trends.each do |subscription| + announce(subscription, status.uri) if trending?(subscription, status, trend_source) + end + rescue ActiveRecord::RecordNotFound + # status might not exist anymore, in which case there is nothing to do + end + + private + + def trending?(subscription, status, trend_source) + scope = scope_for(status, trend_source) + threshold = threshold_for(subscription, trend_source) + scope.where(created_at: subscription.timeframe_start..).count >= threshold + end + + def scope_for(status, trend_source) + case trend_source + when 'favourite' + status.favourites + when 'reblog' + status.reblogs + when 'reply' + status.replies + end + end + + def threshold_for(subscription, trend_source) + case trend_source + when 'favourite' + subscription.threshold_likes + when 'reblog' + subscription.threshold_shares + when 'reply' + subscription.threshold_replies + end + end + + def announce(subscription, uri) + Fasp::Request.new(subscription.fasp_provider).post('/data_sharing/v0/announcements', body: { + source: { + subscription: { + id: subscription.id.to_s, + }, + }, + category: 'content', + eventType: 'trending', + objectUris: [uri], + }) + end +end From 69cc06bf5b677a591da521b4c90a5c2d82e6d469 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Thu, 19 Dec 2024 14:03:30 +0100 Subject: [PATCH 04/15] Select what to share with fasp Only share statuses where the account has `#indexable` set to `true`. Only share accounts where `#discoverable` is set to `true`, with one exception: If `#discoverable` has just been set to `false` this is an important information for the fasp. --- app/models/concerns/account/fasp_concern.rb | 6 ++++++ app/models/concerns/status/fasp_concern.rb | 8 ++++++++ app/models/status.rb | 2 +- app/workers/fasp/announce_trend_worker.rb | 4 +++- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/models/concerns/account/fasp_concern.rb b/app/models/concerns/account/fasp_concern.rb index 01717d396c..6c1d6cc710 100644 --- a/app/models/concerns/account/fasp_concern.rb +++ b/app/models/concerns/account/fasp_concern.rb @@ -12,16 +12,22 @@ module Account::FaspConcern private def announce_new_account_to_subscribed_fasp + return unless discoverable? + uri = ActivityPub::TagManager.instance.uri_for(self) Fasp::AnnounceAccountLifecycleEventWorker.perform_async(uri, 'new') end def announce_updated_account_to_subscribed_fasp + return unless discoverable? || saved_change_to_discoverable? + uri = ActivityPub::TagManager.instance.uri_for(self) Fasp::AnnounceAccountLifecycleEventWorker.perform_async(uri, 'update') end def announce_deleted_account_to_subscribed_fasp + return unless discoverable? + uri = ActivityPub::TagManager.instance.uri_for(self) Fasp::AnnounceAccountLifecycleEventWorker.perform_async(uri, 'delete') end diff --git a/app/models/concerns/status/fasp_concern.rb b/app/models/concerns/status/fasp_concern.rb index 30c7898324..bc0779e39c 100644 --- a/app/models/concerns/status/fasp_concern.rb +++ b/app/models/concerns/status/fasp_concern.rb @@ -13,19 +13,27 @@ module Status::FaspConcern private def announce_new_content_to_subscribed_fasp + return unless account_indexable? + store_uri unless uri # TODO: solve this more elegantly Fasp::AnnounceContentLifecycleEventWorker.perform_async(uri, 'new') end def announce_updated_content_to_subscribed_fasp + return unless account_indexable? + Fasp::AnnounceContentLifecycleEventWorker.perform_async(uri, 'update') end def announce_deleted_content_to_subscribed_fasp + return unless account_indexable? + Fasp::AnnounceContentLifecycleEventWorker.perform_async(uri, 'delete') end def announce_trends_to_subscribed_fasp + return unless account_indexable? + candidate_id, trend_source = if reblog_of_id [reblog_of_id, 'reblog'] diff --git a/app/models/status.rb b/app/models/status.rb index 13560628b2..86a99ba360 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -171,7 +171,7 @@ class Status < ApplicationRecord ], thread: :account - delegate :domain, to: :account, prefix: true + delegate :domain, :indexable, to: :account, prefix: true REAL_TIME_WINDOW = 6.hours diff --git a/app/workers/fasp/announce_trend_worker.rb b/app/workers/fasp/announce_trend_worker.rb index 0d5fc6db48..687871ff2e 100644 --- a/app/workers/fasp/announce_trend_worker.rb +++ b/app/workers/fasp/announce_trend_worker.rb @@ -6,7 +6,9 @@ class Fasp::AnnounceTrendWorker sidekiq_options queue: 'fasp', retry: 5 def perform(status_id, trend_source) - status = ::Status.find(status_id) + status = ::Status.includes(:account).find(status_id) + return unless status.account.indexable? + Fasp::Subscription.includes(:fasp_provider).content.trends.each do |subscription| announce(subscription, status.uri) if trending?(subscription, status, trend_source) end From a09ce4f6d822229c4ae0f106f6f8ef44a81c5cd8 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Thu, 19 Dec 2024 17:07:41 +0100 Subject: [PATCH 05/15] Fix typo --- app/models/status.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/status.rb b/app/models/status.rb index 86a99ba360..bd5267a7d5 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -171,7 +171,7 @@ class Status < ApplicationRecord ], thread: :account - delegate :domain, :indexable, to: :account, prefix: true + delegate :domain, :indexable?, to: :account, prefix: true REAL_TIME_WINDOW = 6.hours From 1ad47008848be042d047c46d4cced092901ed410 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Fri, 20 Dec 2024 16:19:51 +0100 Subject: [PATCH 06/15] Only share public statuses. Only actually public, i.e. not "unlisted". One exception: Share an update if the update changed the visibility from "public" to something else. The fasp can act on this information then. --- app/models/concerns/status/fasp_concern.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/models/concerns/status/fasp_concern.rb b/app/models/concerns/status/fasp_concern.rb index bc0779e39c..82c6e8a9b6 100644 --- a/app/models/concerns/status/fasp_concern.rb +++ b/app/models/concerns/status/fasp_concern.rb @@ -13,20 +13,20 @@ module Status::FaspConcern private def announce_new_content_to_subscribed_fasp - return unless account_indexable? + return unless account_indexable? && public_visibility? store_uri unless uri # TODO: solve this more elegantly Fasp::AnnounceContentLifecycleEventWorker.perform_async(uri, 'new') end def announce_updated_content_to_subscribed_fasp - return unless account_indexable? + return unless account_indexable? && public_visibility_or_just_changed? Fasp::AnnounceContentLifecycleEventWorker.perform_async(uri, 'update') end def announce_deleted_content_to_subscribed_fasp - return unless account_indexable? + return unless account_indexable? && public_visibility? Fasp::AnnounceContentLifecycleEventWorker.perform_async(uri, 'delete') end @@ -42,4 +42,8 @@ module Status::FaspConcern end Fasp::AnnounceTrendWorker.perform_async(candidate_id, trend_source) if candidate_id end + + def public_visibility_or_just_changed? + public_visibility? || visibility_previously_was == 'public' + end end From 1490d3939ea76c0af409c481e6b149a525965543 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Mon, 6 Jan 2025 14:39:09 +0100 Subject: [PATCH 07/15] Implement backfill requests --- .../v0/backfill_requests_controller.rb | 24 ++++++- .../v0/continuations_controller.rb | 10 +++ app/models/fasp.rb | 2 + app/models/fasp/backfill_request.rb | 67 +++++++++++++++++++ app/models/fasp/provider.rb | 1 + app/models/fasp/subscription.rb | 3 +- app/workers/fasp/backfill_worker.rb | 32 +++++++++ config/routes/fasp.rb | 4 +- ...103131909_create_fasp_backfill_requests.rb | 15 +++++ db/schema.rb | 12 ++++ .../fasp/backfill_request_fabricator.rb | 9 +++ spec/models/fasp/backfill_request_spec.rb | 7 ++ .../data_sharing/v0/continuations_spec.rb | 9 +++ 13 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 app/controllers/api/fasp/data_sharing/v0/continuations_controller.rb create mode 100644 app/models/fasp/backfill_request.rb create mode 100644 app/workers/fasp/backfill_worker.rb create mode 100644 db/migrate/20250103131909_create_fasp_backfill_requests.rb create mode 100644 spec/fabricators/fasp/backfill_request_fabricator.rb create mode 100644 spec/models/fasp/backfill_request_spec.rb create mode 100644 spec/requests/api/fasp/data_sharing/v0/continuations_spec.rb diff --git a/app/controllers/api/fasp/data_sharing/v0/backfill_requests_controller.rb b/app/controllers/api/fasp/data_sharing/v0/backfill_requests_controller.rb index a5d5d8b791..c37a94f251 100644 --- a/app/controllers/api/fasp/data_sharing/v0/backfill_requests_controller.rb +++ b/app/controllers/api/fasp/data_sharing/v0/backfill_requests_controller.rb @@ -1,4 +1,26 @@ # frozen_string_literal: true -class Api::Fasp::DataSharing::V0::BackfillRequestsController < ApplicationController +class Api::Fasp::DataSharing::V0::BackfillRequestsController < Api::Fasp::BaseController + def create + backfill_request = current_provider.fasp_backfill_requests.new(backfill_request_params) + + respond_to do |format| + format.json do + if backfill_request.save + render json: { backfillRequest: { id: backfill_request.id } }, status: 201 + else + head 422 + end + end + end + end + + private + + def backfill_request_params + params + .permit(:category, :maxCount) + .to_unsafe_h + .transform_keys { |k| k.to_s.underscore } + end end diff --git a/app/controllers/api/fasp/data_sharing/v0/continuations_controller.rb b/app/controllers/api/fasp/data_sharing/v0/continuations_controller.rb new file mode 100644 index 0000000000..eff2ac0e21 --- /dev/null +++ b/app/controllers/api/fasp/data_sharing/v0/continuations_controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class Api::Fasp::DataSharing::V0::ContinuationsController < Api::Fasp::BaseController + def create + backfill_request = current_provider.fasp_backfill_requests.find(params[:backfill_request_id]) + Fasp::BackfillWorker.perform_async(backfill_request.id) + + head 204 + end +end diff --git a/app/models/fasp.rb b/app/models/fasp.rb index cb33937715..e4e73a2312 100644 --- a/app/models/fasp.rb +++ b/app/models/fasp.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module Fasp + DATA_CATEGORIES = %w(account content).freeze + def self.table_name_prefix 'fasp_' end diff --git a/app/models/fasp/backfill_request.rb b/app/models/fasp/backfill_request.rb new file mode 100644 index 0000000000..e1be611097 --- /dev/null +++ b/app/models/fasp/backfill_request.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: fasp_backfill_requests +# +# id :bigint(8) not null, primary key +# category :string not null +# cursor :string +# fulfilled :boolean default(FALSE), not null +# max_count :integer default(100), not null +# created_at :datetime not null +# updated_at :datetime not null +# fasp_provider_id :bigint(8) not null +# +class Fasp::BackfillRequest < ApplicationRecord + belongs_to :fasp_provider, class_name: 'Fasp::Provider' + + validates :category, presence: true, inclusion: Fasp::DATA_CATEGORIES + validates :max_count, presence: true, + numericality: { only_integer: true } + + after_commit :queue_fulfillment_job, on: :create + + def next_objects + @next_objects ||= base_scope.to_a + end + + def next_uris + next_objects.map { |o| ActivityPub::TagManager.instance.uri_for(o) } + end + + def more_objects_available? + return false if next_objects.empty? + + base_scope.where(id: ...(next_objects.last.id)).any? + end + + def advance! + if more_objects_available? + update!(cursor: next_objects.last.id) + else + update!(fulfilled: true) + end + end + + private + + def base_scope + result = category_scope.limit(max_count).order(id: :desc) + result = result.where(id: ...cursor) if cursor.present? + result + end + + def category_scope + case category + when 'account' + Account.discoverable.without_instance_actor + when 'content' + Status.indexable + end + end + + def queue_fulfillment_job + Fasp::BackfillWorker.perform_async(id) + end +end diff --git a/app/models/fasp/provider.rb b/app/models/fasp/provider.rb index e0457266e8..7926953e6c 100644 --- a/app/models/fasp/provider.rb +++ b/app/models/fasp/provider.rb @@ -22,6 +22,7 @@ class Fasp::Provider < ApplicationRecord include DebugConcern + has_many :fasp_backfill_requests, inverse_of: :fasp_provider, class_name: 'Fasp::BackfillRequest', dependent: :delete_all has_many :fasp_debug_callbacks, inverse_of: :fasp_provider, class_name: 'Fasp::DebugCallback', dependent: :delete_all has_many :fasp_subscriptions, inverse_of: :fasp_provider, class_name: 'Fasp::Subscription', dependent: :delete_all diff --git a/app/models/fasp/subscription.rb b/app/models/fasp/subscription.rb index 1523613450..80e8ed691c 100644 --- a/app/models/fasp/subscription.rb +++ b/app/models/fasp/subscription.rb @@ -17,12 +17,11 @@ # fasp_provider_id :bigint(8) not null # class Fasp::Subscription < ApplicationRecord - CATEGORIES = %w(account content).freeze TYPES = %w(lifecycle trends).freeze belongs_to :fasp_provider, class_name: 'Fasp::Provider' - validates :category, presence: true, inclusion: CATEGORIES + validates :category, presence: true, inclusion: Fasp::DATA_CATEGORIES validates :subscription_type, presence: true, inclusion: TYPES diff --git a/app/workers/fasp/backfill_worker.rb b/app/workers/fasp/backfill_worker.rb new file mode 100644 index 0000000000..4e30b71a7d --- /dev/null +++ b/app/workers/fasp/backfill_worker.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class Fasp::BackfillWorker + include Sidekiq::Worker + + sidekiq_options queue: 'fasp', retry: 5 + + def perform(backfill_request_id) + backfill_request = Fasp::BackfillRequest.find(backfill_request_id) + + announce(backfill_request) + + backfill_request.advance! + rescue ActiveRecord::RecordNotFound + # ignore missing backfill requests + end + + private + + def announce(backfill_request) + Fasp::Request.new(backfill_request.fasp_provider).post('/data_sharing/v0/announcements', body: { + source: { + backfillRequest: { + id: backfill_request.id.to_s, + }, + }, + category: backfill_request.category, + objectUris: backfill_request.next_uris, + moreObjectsAvailable: backfill_request.more_objects_available?, + }) + end +end diff --git a/config/routes/fasp.rb b/config/routes/fasp.rb index cb9b894290..bd2bb4b520 100644 --- a/config/routes/fasp.rb +++ b/config/routes/fasp.rb @@ -12,7 +12,9 @@ namespace :api, format: false do namespace :data_sharing do namespace :v0 do - resources :backfill_requests, only: [:create] + resources :backfill_requests, only: [:create] do + resource :continuation, only: [:create] + end resources :event_subscriptions, only: [:create, :destroy] end diff --git a/db/migrate/20250103131909_create_fasp_backfill_requests.rb b/db/migrate/20250103131909_create_fasp_backfill_requests.rb new file mode 100644 index 0000000000..31dcaaa469 --- /dev/null +++ b/db/migrate/20250103131909_create_fasp_backfill_requests.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class CreateFaspBackfillRequests < ActiveRecord::Migration[7.2] + def change + create_table :fasp_backfill_requests do |t| + t.string :category, null: false + t.integer :max_count, null: false, default: 100 + t.string :cursor + t.boolean :fulfilled, null: false, default: false + t.references :fasp_provider, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 253a1a1f97..c4c8f80801 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -445,6 +445,17 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_13_123400) do t.index ["domain"], name: "index_email_domain_blocks_on_domain", unique: true end + create_table "fasp_backfill_requests", force: :cascade do |t| + t.string "category", null: false + t.integer "max_count", default: 100, null: false + t.string "cursor" + t.boolean "fulfilled", default: false, null: false + t.bigint "fasp_provider_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["fasp_provider_id"], name: "index_fasp_backfill_requests_on_fasp_provider_id" + end + create_table "fasp_debug_callbacks", force: :cascade do |t| t.bigint "fasp_provider_id", null: false t.string "ip", null: false @@ -1329,6 +1340,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_13_123400) do add_foreign_key "custom_filter_statuses", "statuses", on_delete: :cascade add_foreign_key "custom_filters", "accounts", 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_debug_callbacks", "fasp_providers" add_foreign_key "fasp_subscriptions", "fasp_providers" add_foreign_key "favourites", "accounts", name: "fk_5eb6c2b873", on_delete: :cascade diff --git a/spec/fabricators/fasp/backfill_request_fabricator.rb b/spec/fabricators/fasp/backfill_request_fabricator.rb new file mode 100644 index 0000000000..fc9461be27 --- /dev/null +++ b/spec/fabricators/fasp/backfill_request_fabricator.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +Fabricator('Fasp::BackfillRequest') do + category 'MyString' + max_count 1 + cursor 'MyString' + fulfilled false + fasp_provider nil +end diff --git a/spec/models/fasp/backfill_request_spec.rb b/spec/models/fasp/backfill_request_spec.rb new file mode 100644 index 0000000000..397a9e942b --- /dev/null +++ b/spec/models/fasp/backfill_request_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Fasp::BackfillRequest do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/requests/api/fasp/data_sharing/v0/continuations_spec.rb b/spec/requests/api/fasp/data_sharing/v0/continuations_spec.rb new file mode 100644 index 0000000000..67265fc96d --- /dev/null +++ b/spec/requests/api/fasp/data_sharing/v0/continuations_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Api::Fasp::DataSharing::V0::Continuations' do + describe 'GET /index' do + pending "add some examples (or delete) #{__FILE__}" + end +end From 716dec82bcd9a5138244397d26c85a24b1700aab Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Fri, 21 Mar 2025 15:26:34 +0100 Subject: [PATCH 08/15] Fix autogenerated fabricators --- .../fasp/backfill_request_fabricator.rb | 10 +++++----- spec/fabricators/fasp/subscription_fabricator.rb | 14 +++++--------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/spec/fabricators/fasp/backfill_request_fabricator.rb b/spec/fabricators/fasp/backfill_request_fabricator.rb index fc9461be27..1dd58b0081 100644 --- a/spec/fabricators/fasp/backfill_request_fabricator.rb +++ b/spec/fabricators/fasp/backfill_request_fabricator.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -Fabricator('Fasp::BackfillRequest') do - category 'MyString' - max_count 1 - cursor 'MyString' +Fabricator(:fasp_backfill_request, from: 'Fasp::BackfillRequest') do + category 'content' + max_count 10 + cursor nil fulfilled false - fasp_provider nil + fasp_provider end diff --git a/spec/fabricators/fasp/subscription_fabricator.rb b/spec/fabricators/fasp/subscription_fabricator.rb index d560a0efce..6b5fdaaefb 100644 --- a/spec/fabricators/fasp/subscription_fabricator.rb +++ b/spec/fabricators/fasp/subscription_fabricator.rb @@ -1,12 +1,8 @@ # frozen_string_literal: true -Fabricator('Fasp::Subscription') do - category 'MyString' - subscription_type 'MyString' - max_batch_size 1 - threshold_timeframe 1 - threshold_shares 1 - threshold_likes 1 - threshold_replies 1 - fasp_provider nil +Fabricator(:fasp_subscription, from: 'Fasp::Subscription') do + category 'content' + subscription_type 'lifecycle' + max_batch_size 10 + fasp_provider end From c94e29874639dd916ca3eb2945076c4bdd1cc290 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Fri, 21 Mar 2025 15:28:03 +0100 Subject: [PATCH 09/15] Put FASP support behind a feature flag --- app/models/account.rb | 2 +- app/models/favourite.rb | 2 +- app/models/status.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/account.rb b/app/models/account.rb index 22b8bce601..d80ce39a3a 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -85,7 +85,7 @@ class Account < ApplicationRecord include Account::Associations include Account::Avatar include Account::Counters - include Account::FaspConcern + include Account::FaspConcern if Mastodon::Feature.fasp_enabled? include Account::FinderConcern include Account::Header include Account::Interactions diff --git a/app/models/favourite.rb b/app/models/favourite.rb index 7bf793e2a1..1731ac0d03 100644 --- a/app/models/favourite.rb +++ b/app/models/favourite.rb @@ -13,7 +13,7 @@ class Favourite < ApplicationRecord include Paginable - include Favourite::FaspConcern + include Favourite::FaspConcern if Mastodon::Feature.fasp_enabled? update_index('statuses', :status) diff --git a/app/models/status.rb b/app/models/status.rb index bd5267a7d5..327a21dc1b 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -35,7 +35,7 @@ class Status < ApplicationRecord include Discard::Model include Paginable include RateLimitable - include Status::FaspConcern + include Status::FaspConcern if Mastodon::Feature.fasp_enabled? include Status::FetchRepliesConcern include Status::SafeReblogInsert include Status::SearchConcern From cee1378231256320747a6a1f6229006bc756b021 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Mon, 7 Apr 2025 16:43:34 +0200 Subject: [PATCH 10/15] Add model specs --- spec/fabricators/account_fabricator.rb | 1 + spec/models/fasp/backfill_request_spec.rb | 89 ++++++++++++++++++++++- spec/models/fasp/subscription_spec.rb | 28 ++++++- 3 files changed, 116 insertions(+), 2 deletions(-) diff --git a/spec/fabricators/account_fabricator.rb b/spec/fabricators/account_fabricator.rb index 534b8ae843..6ec89a1cb6 100644 --- a/spec/fabricators/account_fabricator.rb +++ b/spec/fabricators/account_fabricator.rb @@ -15,4 +15,5 @@ Fabricator(:account) do user { |attrs| attrs[:domain].nil? ? Fabricate.build(:user, account: nil) : nil } uri { |attrs| attrs[:domain].nil? ? '' : "https://#{attrs[:domain]}/users/#{attrs[:username]}" } discoverable true + indexable true end diff --git a/spec/models/fasp/backfill_request_spec.rb b/spec/models/fasp/backfill_request_spec.rb index 397a9e942b..56ea70d5a7 100644 --- a/spec/models/fasp/backfill_request_spec.rb +++ b/spec/models/fasp/backfill_request_spec.rb @@ -3,5 +3,92 @@ require 'rails_helper' RSpec.describe Fasp::BackfillRequest do - pending "add some examples to (or delete) #{__FILE__}" + describe '#next_objects' do + let(:account) { Fabricate(:account) } + + before { Fabricate.times(3, :status, account:).sort_by(&:id) } + + context 'with a new backfill request' do + subject { Fabricate(:fasp_backfill_request, max_count: 2) } + + it 'returns the newest two statuses' do + expect(subject.next_objects).to eq [statuses[2], statuses[1]] + end + end + + context 'with cursor set to second newest status' do + subject do + Fabricate(:fasp_backfill_request, max_count: 2, cursor: statuses[1].id) + end + + it 'returns the oldest status' do + expect(subject.next_objects).to eq [statuses[0]] + end + end + + context 'when all statuses are not `indexable`' do + subject { Fabricate(:fasp_backfill_request) } + + let(:account) { Fabricate(:account, indexable: false) } + + it 'returns no statuses' do + expect(subject.next_objects).to be_empty + end + end + end + + describe '#next_uris' do + subject { Fabricate(:fasp_backfill_request) } + + let(:statuses) { Fabricate.times(2, :status) } + + it 'returns uris of the next objects' do + uris = statuses.map(&:uri) + + expect(subject.next_uris).to match_array(uris) + end + end + + describe '#more_objects_available?' do + subject { Fabricate(:fasp_backfill_request, max_count: 2) } + + context 'when more objects are available' do + before { Fabricate.times(3, :status) } + + it 'returns `true`' do + expect(subject.more_objects_available?).to be true + end + end + + context 'when no more objects are available' do + before { Fabricate.times(2, :status) } + + it 'returns `false`' do + expect(subject.more_objects_available?).to be false + end + end + end + + describe '#advance!' do + subject { Fabricate(:fasp_backfill_request, max_count: 2) } + + context 'when more objects are available' do + before { Fabricate.times(3, :status) } + + it 'updates `cursor`' do + expect { subject.advance! }.to change(subject, :cursor) + expect(subject).to be_persisted + end + end + + context 'when no more objects are available' do + before { Fabricate.times(2, :status) } + + it 'sets `fulfilled` to `true`' do + expect { subject.advance! }.to change(subject, :fulfilled) + .from(false).to(true) + expect(subject).to be_persisted + end + end + end end diff --git a/spec/models/fasp/subscription_spec.rb b/spec/models/fasp/subscription_spec.rb index 493fe3e345..d51759d48f 100644 --- a/spec/models/fasp/subscription_spec.rb +++ b/spec/models/fasp/subscription_spec.rb @@ -3,5 +3,31 @@ require 'rails_helper' RSpec.describe Fasp::Subscription do - pending "add some examples to (or delete) #{__FILE__}" + describe '#threshold=' do + subject { described_class.new } + + it 'allows setting all threshold values at once' do + subject.threshold = { + 'timeframe' => 30, + 'shares' => 5, + 'likes' => 8, + 'replies' => 7, + } + + expect(subject.threshold_timeframe).to eq 30 + expect(subject.threshold_shares).to eq 5 + expect(subject.threshold_likes).to eq 8 + expect(subject.threshold_replies).to eq 7 + end + end + + describe '#timeframe_start' do + subject { described_class.new(threshold_timeframe: 45) } + + it 'returns a Time representing the beginning of the timeframe' do + travel_to Time.zone.local(2025, 4, 7, 16, 40) do + expect(subject.timeframe_start).to eq Time.zone.local(2025, 4, 7, 15, 55) + end + end + end end From ddceb88367fe112d662887947b6c6b167fe14e8d Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Tue, 8 Apr 2025 16:43:13 +0200 Subject: [PATCH 11/15] Add request specs --- .../data_sharing/v0/backfill_requests_spec.rb | 38 +++++++++++-- .../data_sharing/v0/continuations_spec.rb | 19 +++++-- .../v0/event_subscriptions_spec.rb | 54 +++++++++++++++++-- 3 files changed, 102 insertions(+), 9 deletions(-) diff --git a/spec/requests/api/fasp/data_sharing/v0/backfill_requests_spec.rb b/spec/requests/api/fasp/data_sharing/v0/backfill_requests_spec.rb index d4987261b2..2d1f1d6417 100644 --- a/spec/requests/api/fasp/data_sharing/v0/backfill_requests_spec.rb +++ b/spec/requests/api/fasp/data_sharing/v0/backfill_requests_spec.rb @@ -2,8 +2,40 @@ require 'rails_helper' -RSpec.describe 'Api::Fasp::DataSharing::V0::BackfillRequests' do - describe 'GET /index' do - pending "add some examples (or delete) #{__FILE__}" +RSpec.describe 'Api::Fasp::DataSharing::V0::BackfillRequests', feature: :fasp do + include ProviderRequestHelper + + describe 'POST /api/fasp/data_sharing/v0/backfill_requests' do + let(:provider) { Fabricate(:fasp_provider) } + + context 'with valid parameters' do + it 'creates a new backfill request' do + params = { category: 'content', maxCount: 10 } + headers = request_authentication_headers(provider, + url: api_fasp_data_sharing_v0_backfill_requests_url, + method: :post, + body: params) + + expect do + post api_fasp_data_sharing_v0_backfill_requests_path, headers:, params:, as: :json + end.to change(Fasp::BackfillRequest, :count).by(1) + expect(response).to have_http_status(201) + end + end + + context 'with invalid parameters' do + it 'does not create a backfill request' do + params = { category: 'unknown', maxCount: 10 } + headers = request_authentication_headers(provider, + url: api_fasp_data_sharing_v0_backfill_requests_url, + method: :post, + body: params) + + expect do + post api_fasp_data_sharing_v0_backfill_requests_path, headers:, params:, as: :json + end.to_not change(Fasp::BackfillRequest, :count) + expect(response).to have_http_status(422) + end + end end end diff --git a/spec/requests/api/fasp/data_sharing/v0/continuations_spec.rb b/spec/requests/api/fasp/data_sharing/v0/continuations_spec.rb index 67265fc96d..59ab44d0c4 100644 --- a/spec/requests/api/fasp/data_sharing/v0/continuations_spec.rb +++ b/spec/requests/api/fasp/data_sharing/v0/continuations_spec.rb @@ -2,8 +2,21 @@ require 'rails_helper' -RSpec.describe 'Api::Fasp::DataSharing::V0::Continuations' do - describe 'GET /index' do - pending "add some examples (or delete) #{__FILE__}" +RSpec.describe 'Api::Fasp::DataSharing::V0::Continuations', feature: :fasp do + include ProviderRequestHelper + + describe 'POST /api/fasp/data_sharing/v0/backfill_requests/:id/continuations' do + let(:backfill_request) { Fabricate(:fasp_backfill_request) } + let(:provider) { backfill_request.fasp_provider } + + it 'queues a job to continue the given backfill request' do + headers = request_authentication_headers(provider, + url: api_fasp_data_sharing_v0_backfill_request_continuation_url(backfill_request), + method: :post) + + post api_fasp_data_sharing_v0_backfill_request_continuation_path(backfill_request), headers:, as: :json + expect(response).to have_http_status(204) + expect(Fasp::BackfillWorker).to have_enqueued_sidekiq_job(backfill_request.id) + end end end diff --git a/spec/requests/api/fasp/data_sharing/v0/event_subscriptions_spec.rb b/spec/requests/api/fasp/data_sharing/v0/event_subscriptions_spec.rb index 8f6b8873db..beab9e326f 100644 --- a/spec/requests/api/fasp/data_sharing/v0/event_subscriptions_spec.rb +++ b/spec/requests/api/fasp/data_sharing/v0/event_subscriptions_spec.rb @@ -2,8 +2,56 @@ require 'rails_helper' -RSpec.describe 'Api::Fasp::DataSharing::V0::EventSubscriptions' do - describe 'GET /index' do - pending "add some examples (or delete) #{__FILE__}" +RSpec.describe 'Api::Fasp::DataSharing::V0::EventSubscriptions', feature: :fasp do + include ProviderRequestHelper + + describe 'POST /api/fasp/data_sharing/v0/event_subscriptions' do + let(:provider) { Fabricate(:fasp_provider) } + + context 'with valid parameters' do + it 'creates a new subscription' do + params = { category: 'content', subscriptionType: 'lifecycle', maxBatchSize: 10 } + headers = request_authentication_headers(provider, + url: api_fasp_data_sharing_v0_event_subscriptions_url, + method: :post, + body: params) + + expect do + post api_fasp_data_sharing_v0_event_subscriptions_path, headers:, params:, as: :json + end.to change(Fasp::Subscription, :count).by(1) + expect(response).to have_http_status(201) + end + end + + context 'with invalid parameters' do + it 'does not create a subscription' do + params = { category: 'unknown' } + headers = request_authentication_headers(provider, + url: api_fasp_data_sharing_v0_event_subscriptions_url, + method: :post, + body: params) + + expect do + post api_fasp_data_sharing_v0_event_subscriptions_path, headers:, params:, as: :json + end.to_not change(Fasp::Subscription, :count) + expect(response).to have_http_status(422) + end + end + end + + describe 'DELETE /api/fasp/data_sharing/v0/event_subscriptions/:id' do + let(:subscription) { Fabricate(:fasp_subscription) } + let(:provider) { subscription.fasp_provider } + + it 'deletes the subscription' do + headers = request_authentication_headers(provider, + url: api_fasp_data_sharing_v0_event_subscription_url(subscription), + method: :delete) + + expect do + delete api_fasp_data_sharing_v0_event_subscription_path(subscription), headers:, as: :json + end.to change(Fasp::Subscription, :count).by(-1) + expect(response).to have_http_status(204) + end end end From f106686f470da485eb3567f0ea1bbde0fde1eef0 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Thu, 10 Apr 2025 11:05:06 +0200 Subject: [PATCH 12/15] Add specs for model hooks --- app/models/account.rb | 2 +- app/models/concerns/account/fasp_concern.rb | 3 + app/models/concerns/favourite/fasp_concern.rb | 2 + app/models/concerns/status/fasp_concern.rb | 4 + app/models/favourite.rb | 2 +- app/models/status.rb | 2 +- .../concerns/account/fasp_concern_spec.rb | 83 +++++++++++ .../concerns/favourite/fasp_concern_spec.rb | 11 ++ .../concerns/status/fasp_concern_spec.rb | 133 ++++++++++++++++++ 9 files changed, 239 insertions(+), 3 deletions(-) create mode 100644 spec/models/concerns/account/fasp_concern_spec.rb create mode 100644 spec/models/concerns/favourite/fasp_concern_spec.rb create mode 100644 spec/models/concerns/status/fasp_concern_spec.rb diff --git a/app/models/account.rb b/app/models/account.rb index d80ce39a3a..22b8bce601 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -85,7 +85,7 @@ class Account < ApplicationRecord include Account::Associations include Account::Avatar include Account::Counters - include Account::FaspConcern if Mastodon::Feature.fasp_enabled? + include Account::FaspConcern include Account::FinderConcern include Account::Header include Account::Interactions diff --git a/app/models/concerns/account/fasp_concern.rb b/app/models/concerns/account/fasp_concern.rb index 6c1d6cc710..b18529a3e9 100644 --- a/app/models/concerns/account/fasp_concern.rb +++ b/app/models/concerns/account/fasp_concern.rb @@ -12,6 +12,7 @@ module Account::FaspConcern private def announce_new_account_to_subscribed_fasp + return unless Mastodon::Feature.fasp_enabled? return unless discoverable? uri = ActivityPub::TagManager.instance.uri_for(self) @@ -19,6 +20,7 @@ module Account::FaspConcern end def announce_updated_account_to_subscribed_fasp + return unless Mastodon::Feature.fasp_enabled? return unless discoverable? || saved_change_to_discoverable? uri = ActivityPub::TagManager.instance.uri_for(self) @@ -26,6 +28,7 @@ module Account::FaspConcern end def announce_deleted_account_to_subscribed_fasp + return unless Mastodon::Feature.fasp_enabled? return unless discoverable? uri = ActivityPub::TagManager.instance.uri_for(self) diff --git a/app/models/concerns/favourite/fasp_concern.rb b/app/models/concerns/favourite/fasp_concern.rb index 2293411f3d..c72e7c3792 100644 --- a/app/models/concerns/favourite/fasp_concern.rb +++ b/app/models/concerns/favourite/fasp_concern.rb @@ -10,6 +10,8 @@ module Favourite::FaspConcern private def announce_trends_to_subscribed_fasp + return unless Mastodon::Feature.fasp_enabled? + Fasp::AnnounceTrendWorker.perform_async(status_id, 'favourite') end end diff --git a/app/models/concerns/status/fasp_concern.rb b/app/models/concerns/status/fasp_concern.rb index 82c6e8a9b6..d09dd3ca10 100644 --- a/app/models/concerns/status/fasp_concern.rb +++ b/app/models/concerns/status/fasp_concern.rb @@ -13,6 +13,7 @@ module Status::FaspConcern private def announce_new_content_to_subscribed_fasp + return unless Mastodon::Feature.fasp_enabled? return unless account_indexable? && public_visibility? store_uri unless uri # TODO: solve this more elegantly @@ -20,18 +21,21 @@ module Status::FaspConcern end def announce_updated_content_to_subscribed_fasp + return unless Mastodon::Feature.fasp_enabled? return unless account_indexable? && public_visibility_or_just_changed? Fasp::AnnounceContentLifecycleEventWorker.perform_async(uri, 'update') end def announce_deleted_content_to_subscribed_fasp + return unless Mastodon::Feature.fasp_enabled? return unless account_indexable? && public_visibility? Fasp::AnnounceContentLifecycleEventWorker.perform_async(uri, 'delete') end def announce_trends_to_subscribed_fasp + return unless Mastodon::Feature.fasp_enabled? return unless account_indexable? candidate_id, trend_source = diff --git a/app/models/favourite.rb b/app/models/favourite.rb index 1731ac0d03..7bf793e2a1 100644 --- a/app/models/favourite.rb +++ b/app/models/favourite.rb @@ -13,7 +13,7 @@ class Favourite < ApplicationRecord include Paginable - include Favourite::FaspConcern if Mastodon::Feature.fasp_enabled? + include Favourite::FaspConcern update_index('statuses', :status) diff --git a/app/models/status.rb b/app/models/status.rb index 327a21dc1b..bd5267a7d5 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -35,7 +35,7 @@ class Status < ApplicationRecord include Discard::Model include Paginable include RateLimitable - include Status::FaspConcern if Mastodon::Feature.fasp_enabled? + include Status::FaspConcern include Status::FetchRepliesConcern include Status::SafeReblogInsert include Status::SearchConcern diff --git a/spec/models/concerns/account/fasp_concern_spec.rb b/spec/models/concerns/account/fasp_concern_spec.rb new file mode 100644 index 0000000000..0434689bff --- /dev/null +++ b/spec/models/concerns/account/fasp_concern_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Account::FaspConcern, feature: :fasp do + describe '#create' do + let(:discoverable_attributes) do + Fabricate.attributes_for(:account).except('user_id') + end + let(:undiscoverable_attributes) do + discoverable_attributes.merge('discoverable' => false) + end + + context 'when account is discoverable' do + it 'queues a job to notify provider' do + Account.create(discoverable_attributes) + + expect(Fasp::AnnounceAccountLifecycleEventWorker).to have_enqueued_sidekiq_job + end + end + + context 'when account is not discoverable' do + it 'does not queue a job' do + Account.create(undiscoverable_attributes) + + expect(Fasp::AnnounceAccountLifecycleEventWorker).to_not have_enqueued_sidekiq_job + end + end + end + + describe '#update' do + before do + # Create account and clear sidekiq queue so we only catch + # jobs queued as part of the update + account + Sidekiq::Worker.clear_all + end + + context 'when account is discoverable' do + let(:account) { Fabricate(:account, domain: 'example.com') } + + it 'queues a job to notify provider' do + expect { account.touch }.to enqueue_sidekiq_job(Fasp::AnnounceAccountLifecycleEventWorker) + end + end + + context 'when account was discoverable before' do + let(:account) { Fabricate(:account, domain: 'example.com') } + + it 'queues a job to notify provider' do + expect do + account.update(discoverable: false) + end.to enqueue_sidekiq_job(Fasp::AnnounceAccountLifecycleEventWorker) + end + end + + context 'when account has not been discoverable' do + let(:account) { Fabricate(:account, domain: 'example.com', discoverable: false) } + + it 'does not queue a job' do + expect { account.touch }.to_not enqueue_sidekiq_job(Fasp::AnnounceAccountLifecycleEventWorker) + end + end + end + + describe '#destroy' do + context 'when account is discoverable' do + let(:account) { Fabricate(:account, domain: 'example.com') } + + it 'queues a job to notify provider' do + expect { account.destroy }.to enqueue_sidekiq_job(Fasp::AnnounceAccountLifecycleEventWorker) + end + end + + context 'when account is not discoverable' do + let(:account) { Fabricate(:account, domain: 'example.com', discoverable: false) } + + it 'does not queue a job' do + expect { account.destroy }.to_not enqueue_sidekiq_job(Fasp::AnnounceAccountLifecycleEventWorker) + end + end + end +end diff --git a/spec/models/concerns/favourite/fasp_concern_spec.rb b/spec/models/concerns/favourite/fasp_concern_spec.rb new file mode 100644 index 0000000000..a56618f1f2 --- /dev/null +++ b/spec/models/concerns/favourite/fasp_concern_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Favourite::FaspConcern, feature: :fasp do + describe '#create' do + it 'queues a job to notify provider' do + expect { Fabricate(:favourite) }.to enqueue_sidekiq_job(Fasp::AnnounceTrendWorker) + end + end +end diff --git a/spec/models/concerns/status/fasp_concern_spec.rb b/spec/models/concerns/status/fasp_concern_spec.rb new file mode 100644 index 0000000000..f904a833fc --- /dev/null +++ b/spec/models/concerns/status/fasp_concern_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Status::FaspConcern, feature: :fasp do + describe '#create' do + context 'when account is indexable' do + let(:account) { Fabricate(:account, domain: 'example.com') } + + context 'when status is public' do + it 'queues a job to notify provider of new status' do + expect do + Fabricate(:status, account:) + end.to enqueue_sidekiq_job(Fasp::AnnounceContentLifecycleEventWorker) + end + end + + context 'when status is not public' do + it 'does not queue a job' do + expect do + Fabricate(:status, account:, visibility: :unlisted) + end.to_not enqueue_sidekiq_job(Fasp::AnnounceContentLifecycleEventWorker) + end + end + + context 'when status is in reply to another' do + it 'queues a job to notify provider of possible trend' do + parent = Fabricate(:status) + expect do + Fabricate(:status, account:, thread: parent) + end.to enqueue_sidekiq_job(Fasp::AnnounceTrendWorker) + end + end + + context 'when status is a reblog of another' do + it 'queues a job to notify provider of possible trend' do + original = Fabricate(:status, account:) + expect do + Fabricate(:status, account:, reblog: original) + end.to enqueue_sidekiq_job(Fasp::AnnounceTrendWorker) + end + end + end + + context 'when account is not indexable' do + let(:account) { Fabricate(:account, indexable: false) } + + it 'does not queue a job' do + expect do + Fabricate(:status, account:) + end.to_not enqueue_sidekiq_job(Fasp::AnnounceContentLifecycleEventWorker) + end + end + end + + describe '#update' do + before do + # Create status and clear sidekiq queues to only catch + # jobs queued due to the update + status + Sidekiq::Worker.clear_all + end + + context 'when account is indexable' do + let(:account) { Fabricate(:account, domain: 'example.com') } + let(:status) { Fabricate(:status, account:, visibility:) } + + context 'when status is public' do + let(:visibility) { :public } + + it 'queues a job to notify provider' do + expect { status.touch }.to enqueue_sidekiq_job(Fasp::AnnounceContentLifecycleEventWorker) + end + end + + context 'when status just switched to non-public' do + let(:visibility) { :public } + + it 'queues a job to notify provider' do + expect do + status.update(visibility: :unlisted) + end.to enqueue_sidekiq_job(Fasp::AnnounceContentLifecycleEventWorker) + end + end + + context 'when status has not been public' do + let(:visibility) { :unlisted } + + it 'does not queue a job' do + expect do + status.touch + end.to_not enqueue_sidekiq_job(Fasp::AnnounceContentLifecycleEventWorker) + end + end + end + + context 'when account is not indexable' do + let(:account) { Fabricate(:account, domain: 'example.com', indexable: false) } + let(:status) { Fabricate(:status, account:) } + + it 'does not queue a job' do + expect { status.touch }.to_not enqueue_sidekiq_job(Fasp::AnnounceContentLifecycleEventWorker) + end + end + end + + describe '#destroy' do + let(:status) { Fabricate(:status, account:) } + + before do + # Create status and clear sidekiq queues to only catch + # jobs queued due to the update + status + Sidekiq::Worker.clear_all + end + + context 'when account is indexable' do + let(:account) { Fabricate(:account, domain: 'example.com') } + + it 'queues a job to notify provider' do + expect { status.destroy }.to enqueue_sidekiq_job(Fasp::AnnounceContentLifecycleEventWorker) + end + end + + context 'when account is not indexable' do + let(:account) { Fabricate(:account, domain: 'example.com', indexable: false) } + + it 'does not queue a job' do + expect { status.destroy }.to_not enqueue_sidekiq_job(Fasp::AnnounceContentLifecycleEventWorker) + end + end + end +end From 77488e1dab58ef9d09da2ec40beaeea2bfefbefc Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Thu, 10 Apr 2025 11:06:29 +0200 Subject: [PATCH 13/15] Fix broken specs Rubocop fooled me into breaking this. --- spec/models/fasp/backfill_request_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/models/fasp/backfill_request_spec.rb b/spec/models/fasp/backfill_request_spec.rb index 56ea70d5a7..5ea820db1e 100644 --- a/spec/models/fasp/backfill_request_spec.rb +++ b/spec/models/fasp/backfill_request_spec.rb @@ -5,8 +5,7 @@ require 'rails_helper' RSpec.describe Fasp::BackfillRequest do describe '#next_objects' do let(:account) { Fabricate(:account) } - - before { Fabricate.times(3, :status, account:).sort_by(&:id) } + let!(:statuses) { Fabricate.times(3, :status, account:).sort_by(&:id) } context 'with a new backfill request' do subject { Fabricate(:fasp_backfill_request, max_count: 2) } From 95f02467c9f4ae1f3ef91b700367a424def4894f Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Thu, 10 Apr 2025 12:30:19 +0200 Subject: [PATCH 14/15] Add worker specs --- ...nce_account_lifecycle_event_worker_spec.rb | 34 ++++++++++++ ...nce_content_lifecycle_event_worker_spec.rb | 34 ++++++++++++ .../fasp/announce_trend_worker_spec.rb | 52 +++++++++++++++++++ spec/workers/fasp/backfill_worker_spec.rb | 32 ++++++++++++ 4 files changed, 152 insertions(+) create mode 100644 spec/workers/fasp/announce_account_lifecycle_event_worker_spec.rb create mode 100644 spec/workers/fasp/announce_content_lifecycle_event_worker_spec.rb create mode 100644 spec/workers/fasp/announce_trend_worker_spec.rb create mode 100644 spec/workers/fasp/backfill_worker_spec.rb diff --git a/spec/workers/fasp/announce_account_lifecycle_event_worker_spec.rb b/spec/workers/fasp/announce_account_lifecycle_event_worker_spec.rb new file mode 100644 index 0000000000..0d4a870875 --- /dev/null +++ b/spec/workers/fasp/announce_account_lifecycle_event_worker_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Fasp::AnnounceAccountLifecycleEventWorker do + include ProviderRequestHelper + + let(:account_uri) { 'https://masto.example.com/accounts/1' } + let(:subscription) do + Fabricate(:fasp_subscription, category: 'account') + end + let(:provider) { subscription.fasp_provider } + let!(:stubbed_request) do + stub_provider_request(provider, + method: :post, + path: '/data_sharing/v0/announcements', + response_body: { + source: { + subscription: { + id: subscription.id.to_s, + }, + }, + category: 'account', + eventType: 'new', + objectUris: [account_uri], + }) + end + + it 'sends the account uri to subscribed providers' do + described_class.new.perform(account_uri, 'new') + + expect(stubbed_request).to have_been_made + end +end diff --git a/spec/workers/fasp/announce_content_lifecycle_event_worker_spec.rb b/spec/workers/fasp/announce_content_lifecycle_event_worker_spec.rb new file mode 100644 index 0000000000..60618607c9 --- /dev/null +++ b/spec/workers/fasp/announce_content_lifecycle_event_worker_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Fasp::AnnounceContentLifecycleEventWorker do + include ProviderRequestHelper + + let(:status_uri) { 'https://masto.example.com/status/1' } + let(:subscription) do + Fabricate(:fasp_subscription) + end + let(:provider) { subscription.fasp_provider } + let!(:stubbed_request) do + stub_provider_request(provider, + method: :post, + path: '/data_sharing/v0/announcements', + response_body: { + source: { + subscription: { + id: subscription.id.to_s, + }, + }, + category: 'content', + eventType: 'new', + objectUris: [status_uri], + }) + end + + it 'sends the status uri to subscribed providers' do + described_class.new.perform(status_uri, 'new') + + expect(stubbed_request).to have_been_made + end +end diff --git a/spec/workers/fasp/announce_trend_worker_spec.rb b/spec/workers/fasp/announce_trend_worker_spec.rb new file mode 100644 index 0000000000..799d8a8f48 --- /dev/null +++ b/spec/workers/fasp/announce_trend_worker_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Fasp::AnnounceTrendWorker do + include ProviderRequestHelper + + let(:status) { Fabricate(:status) } + let(:subscription) do + Fabricate(:fasp_subscription, + category: 'content', + subscription_type: 'trends', + threshold_timeframe: 15, + threshold_likes: 2) + end + let(:provider) { subscription.fasp_provider } + let!(:stubbed_request) do + stub_provider_request(provider, + method: :post, + path: '/data_sharing/v0/announcements', + response_body: { + source: { + subscription: { + id: subscription.id.to_s, + }, + }, + category: 'content', + eventType: 'trending', + objectUris: [status.uri], + }) + end + + context 'when the configured threshold is met' do + before do + Fabricate.times(2, :favourite, status:) + end + + it 'sends the account uri to subscribed providers' do + described_class.new.perform(status.id, 'favourite') + + expect(stubbed_request).to have_been_made + end + end + + context 'when the configured threshold is not met' do + it 'does not notify any provider' do + described_class.new.perform(status.id, 'favourite') + + expect(stubbed_request).to_not have_been_made + end + end +end diff --git a/spec/workers/fasp/backfill_worker_spec.rb b/spec/workers/fasp/backfill_worker_spec.rb new file mode 100644 index 0000000000..43734e02ba --- /dev/null +++ b/spec/workers/fasp/backfill_worker_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Fasp::BackfillWorker do + include ProviderRequestHelper + + let(:backfill_request) { Fabricate(:fasp_backfill_request) } + let(:provider) { backfill_request.fasp_provider } + let(:status) { Fabricate(:status) } + let!(:stubbed_request) do + stub_provider_request(provider, + method: :post, + path: '/data_sharing/v0/announcements', + response_body: { + source: { + backfillRequest: { + id: backfill_request.id.to_s, + }, + }, + category: 'content', + objectUris: [status.uri], + moreObjectsAvailable: false, + }) + end + + it 'sends status uri to provider that requested backfill' do + described_class.new.perform(backfill_request.id) + + expect(stubbed_request).to have_been_made + end +end From 8968d3a4104b028c1b39e1bbaab11a63f85e95f7 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Thu, 10 Apr 2025 15:03:09 +0200 Subject: [PATCH 15/15] Improve comment to explain redundant call --- app/models/concerns/status/fasp_concern.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/models/concerns/status/fasp_concern.rb b/app/models/concerns/status/fasp_concern.rb index d09dd3ca10..5e7cf45379 100644 --- a/app/models/concerns/status/fasp_concern.rb +++ b/app/models/concerns/status/fasp_concern.rb @@ -16,7 +16,11 @@ module Status::FaspConcern return unless Mastodon::Feature.fasp_enabled? return unless account_indexable? && public_visibility? - store_uri unless uri # TODO: solve this more elegantly + # We need the uri here, but it is set in another `after_commit` + # callback. Hooks included from modules are run before the ones + # in the class itself and can neither be reordered nor is there + # a way to declare dependencies. + store_uri if uri.nil? Fasp::AnnounceContentLifecycleEventWorker.perform_async(uri, 'new') end