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 acb1d2ff25..25ea958329 100644 --- a/app/lib/fasp/request.rb +++ b/app/lib/fasp/request.rb @@ -26,6 +26,7 @@ class Fasp::Request def 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 a3dc58de49..eff4e826c5 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 before_create :create_keypair 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 5a81b00773..122f57df57 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -34,6 +34,7 @@ class Status < ApplicationRecord include Discard::Model include Paginable include RateLimitable + include Status::FaspConcern include Status::SafeReblogInsert include Status::SearchConcern include Status::SnapshotConcern 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 adc0b0e7e6..8f2c46b740 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -470,6 +470,20 @@ ActiveRecord::Schema[7.2].define(version: 2024_12_13_170053) 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 @@ -1310,6 +1324,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_12_13_170053) 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