From 471b82969078e24db1b1243349f57fe6438bc5f2 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 7 Feb 2024 16:24:52 -0300 Subject: [PATCH 01/42] feat: actualizar cliente de DP --- Gemfile | 2 +- Gemfile.lock | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 466ec079..80051e93 100644 --- a/Gemfile +++ b/Gemfile @@ -37,7 +37,7 @@ gem 'commonmarker' gem 'devise' gem 'devise-i18n' gem 'devise_invitable' -gem 'distributed-press-api-client', '~> 0.3.0rc0' +gem 'distributed-press-api-client', '~> 0.4.0rc0' gem 'email_address', git: 'https://github.com/fauno/email_address', branch: 'i18n' gem 'exception_notification' gem 'fast_blank' diff --git a/Gemfile.lock b/Gemfile.lock index 78563c84..90bf6d98 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -157,11 +157,12 @@ GEM devise_invitable (2.0.8) actionmailer (>= 5.0) devise (>= 4.6) - distributed-press-api-client (0.3.0rc0) + distributed-press-api-client (0.4.0rc0) addressable (~> 2.3, >= 2.3.0) climate_control dry-schema httparty (~> 0.18) + httparty-cache json (~> 2.1, >= 2.1.0) jwt (~> 2.6.0) dotenv (2.8.1) @@ -259,6 +260,8 @@ GEM httparty (0.21.0) mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) + httparty-cache (0.0.1) + httparty (~> 0.18) i18n (1.14.1) concurrent-ruby (~> 1.0) icalendar (2.8.0) @@ -600,7 +603,7 @@ DEPENDENCIES devise devise-i18n devise_invitable - distributed-press-api-client (~> 0.3.0rc0) + distributed-press-api-client (~> 0.4.0rc0) dotenv-rails down ed25519 From 6841945b398cd04a38b859715fcaedb26e51bf81 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 16 Feb 2024 14:52:37 -0300 Subject: [PATCH 02/42] feat: dependencias --- Gemfile | 5 ++++- Gemfile.lock | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 80051e93..3cf01934 100644 --- a/Gemfile +++ b/Gemfile @@ -37,7 +37,9 @@ gem 'commonmarker' gem 'devise' gem 'devise-i18n' gem 'devise_invitable' -gem 'distributed-press-api-client', '~> 0.4.0rc0' +gem 'redis-client' +gem 'hiredis-client' +gem 'distributed-press-api-client', '~> 0.4.0rc2' gem 'email_address', git: 'https://github.com/fauno/email_address', branch: 'i18n' gem 'exception_notification' gem 'fast_blank' @@ -65,6 +67,7 @@ gem 'redis', '~> 4.0', require: %w[redis redis/connection/hiredis] gem 'redis-rails' gem 'rollups', git: 'https://github.com/fauno/rollup.git', branch: 'update' gem 'rubyzip' +gem 'ruby-brs' gem 'rugged', '1.5.0.1' gem 'git_clone_url' gem 'concurrent-ruby-ext' diff --git a/Gemfile.lock b/Gemfile.lock index 90bf6d98..5b8ca619 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -88,6 +88,7 @@ GEM zeitwerk (~> 2.3) addressable (2.8.4) public_suffix (>= 2.0.2, < 6.0) + adsp (1.0.10) ast (2.4.2) autoprefixer-rails (10.4.13.0) execjs (~> 2) @@ -157,12 +158,12 @@ GEM devise_invitable (2.0.8) actionmailer (>= 5.0) devise (>= 4.6) - distributed-press-api-client (0.4.0rc0) + distributed-press-api-client (0.4.0rc2) addressable (~> 2.3, >= 2.3.0) climate_control dry-schema httparty (~> 0.18) - httparty-cache + httparty-cache (~> 0.0.4) json (~> 2.1, >= 2.1.0) jwt (~> 2.6.0) dotenv (2.8.1) @@ -256,11 +257,13 @@ GEM heapy (0.2.0) thor hiredis (0.6.3-x86_64-linux-musl) + hiredis-client (0.14.1-x86_64-linux-musl) + redis-client (= 0.14.1) http_parser.rb (0.8.0-x86_64-linux-musl) httparty (0.21.0) mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) - httparty-cache (0.0.1) + httparty-cache (0.0.4) httparty (~> 0.18) i18n (1.14.1) concurrent-ruby (~> 1.0) @@ -450,6 +453,8 @@ GEM redis-activesupport (5.3.0) activesupport (>= 3, < 8) redis-store (>= 1.3, < 2) + redis-client (0.14.1) + connection_pool redis-rack (2.1.4) rack (>= 2.0.8, < 3) redis-store (>= 1.2, < 2) @@ -487,6 +492,8 @@ GEM activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) + ruby-brs (1.3.3-x86_64-linux-musl) + adsp (~> 1.0) ruby-filemagic (0.7.3-x86_64-linux-musl) ruby-progressbar (1.13.0) ruby-statistics (3.0.2) @@ -603,7 +610,7 @@ DEPENDENCIES devise devise-i18n devise_invitable - distributed-press-api-client (~> 0.4.0rc0) + distributed-press-api-client (~> 0.4.0rc2) dotenv-rails down ed25519 @@ -619,6 +626,7 @@ DEPENDENCIES haml-lint hamlit-rails hiredis + hiredis-client httparty icalendar image_processing @@ -652,10 +660,12 @@ DEPENDENCIES rails-i18n rails_warden redis (~> 4.0) + redis-client redis-rails rgl rollups! rubocop-rails + ruby-brs rubyzip rugged (= 1.5.0.1) safe_yaml From cdf0685c67721e0f4686e205379a00883506800f Mon Sep 17 00:00:00 2001 From: f Date: Fri, 16 Feb 2024 14:53:05 -0300 Subject: [PATCH 03/42] feat: asociar rol con deploy nos permite acceder al token --- app/models/deploy.rb | 2 ++ app/models/rol.rb | 1 + .../20240216170202_add_rol_to_deploys.rb | 18 ++++++++++++++++++ db/structure.sql | 6 ++++-- 4 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20240216170202_add_rol_to_deploys.rb diff --git a/app/models/deploy.rb b/app/models/deploy.rb index 1f087eb3..8f28f214 100644 --- a/app/models/deploy.rb +++ b/app/models/deploy.rb @@ -10,6 +10,8 @@ require 'open3' # :attributes`. class Deploy < ApplicationRecord belongs_to :site + belongs_to :rol + has_many :build_stats, dependent: :destroy DEPENDENCIES = [] diff --git a/app/models/rol.rb b/app/models/rol.rb index 37332400..c9a92515 100644 --- a/app/models/rol.rb +++ b/app/models/rol.rb @@ -11,6 +11,7 @@ class Rol < ApplicationRecord belongs_to :usuarie belongs_to :site + has_many :deploys validates_inclusion_of :rol, in: ROLES diff --git a/db/migrate/20240216170202_add_rol_to_deploys.rb b/db/migrate/20240216170202_add_rol_to_deploys.rb new file mode 100644 index 00000000..5f629432 --- /dev/null +++ b/db/migrate/20240216170202_add_rol_to_deploys.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# Establece una relación entre roles y deploys +class AddRolToDeploys < ActiveRecord::Migration[6.1] + def up + add_column :deploys, :rol_id, :integer, index: true + + Deploy.find_each do |deploy| + rol_id = deploy.site.roles.find_by(rol: 'usuarie', temporal: false).id + + deploy.update_column(:rol_id, rol_id) if rol_id + end + end + + def down + remove_column :deploys, :rol_id + end +end diff --git a/db/structure.sql b/db/structure.sql index cb085f63..dede286d 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -759,7 +759,8 @@ CREATE TABLE public.deploys ( updated_at timestamp without time zone NOT NULL, site_id integer, type character varying, - "values" text + "values" text, + rol_id integer ); @@ -2318,6 +2319,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20230731195050'), ('20230829204127'), ('20230921155401'), -('20230927153926'); +('20230927153926'), +('20240216170202'); From 6d0ea6fa5d864ae9969d43fa2fef5caf54888548 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 16 Feb 2024 14:54:50 -0300 Subject: [PATCH 04/42] feat: acceder a la social inbox desde el sitio --- app/models/site/social_distributed_press.rb | 11 ++++- app/models/social_inbox.rb | 52 +++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 app/models/social_inbox.rb diff --git a/app/models/site/social_distributed_press.rb b/app/models/site/social_distributed_press.rb index 3be6404e..1193ca76 100644 --- a/app/models/site/social_distributed_press.rb +++ b/app/models/site/social_distributed_press.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'distributed_press/v1/social/client' + class Site # Agrega soporte para Social Distributed Press en los sitios module SocialDistributedPress @@ -10,13 +12,20 @@ class Site before_save :generate_private_key_pem!, unless: :private_key_pem? + # @return [SocialInbox] + def social_inbox + @social_inbox ||= SocialInbox.new(site: self) + end + private # Genera la llave privada y la almacena # # @return [nil] def generate_private_key_pem! - self.private_key_pem ||= ::DistributedPress::V1::Social::Client.new(public_key_url: nil, key_size: 2048).private_key.export + self.private_key_pem ||= DistributedPress::V1::Social::Client.new( + public_key_url: nil, + key_size: 2048).private_key.export end end end diff --git a/app/models/social_inbox.rb b/app/models/social_inbox.rb new file mode 100644 index 00000000..47c3457c --- /dev/null +++ b/app/models/social_inbox.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'distributed_press/v1/social/client' + +# Gestiona la Social Inbox de un sitio +class SocialInbox + # @return [Site] + attr_reader :site + + # @param :site [Site] + def initialize(site:) + @site = site + end + + # @return [String] + def actor + @actor ||= + begin + user = site.config.dig('activity_pub', 'username') + user ||= hostname.split('.', 2).first + + "@#{user}@#{hostname}" + end + end + + # @return [DistributedPress::V1::Social::Client] + def client + @client ||= DistributedPress::V1::Social::Client.new( + url: site.config.dig('activity_pub', 'url'), + public_key_url: public_key_url, + private_key_pem: site.private_key_pem, + logger: Rails.logger, + cache_store: :redis + ) + end + + # @return [String] + def public_key_url + @public_key_url ||= URI("https://#{hostname}").tap do |uri| + uri.path = '/about.jsonld' + uri.fragment = 'main-key' + end.to_s + end + + def hostname + @hostname ||= + begin + host = site.config.dig('activity_pub', 'hostname') + host ||= site.hostname + end + end +end From b792cb2d43a6ac145bb8a3afea6f0ccb1841eab5 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 16 Feb 2024 15:59:37 -0300 Subject: [PATCH 05/42] feat: poder reutilizar webhooks --- .../api/v1/concerns/webhook_concern.rb | 71 +++++++++++++++++++ app/controllers/api/v1/webhooks_controller.rb | 56 +-------------- config/routes.rb | 4 +- 3 files changed, 75 insertions(+), 56 deletions(-) create mode 100644 app/controllers/api/v1/concerns/webhook_concern.rb diff --git a/app/controllers/api/v1/concerns/webhook_concern.rb b/app/controllers/api/v1/concerns/webhook_concern.rb new file mode 100644 index 00000000..59d48b28 --- /dev/null +++ b/app/controllers/api/v1/concerns/webhook_concern.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module Api + module V1 + # Helpers para webhooks + module WebhookConcern + extend ActiveSupport::Concern + + included do + # Responde con forbidden si falla la validación del token + rescue_from ActiveRecord::RecordNotFound, with: :platforms_answer + + private + + # Valida el token que envía la plataforma en el webhook + # + # @return [String] + def token + @token ||= + begin + _headers = request.headers + _token ||= _headers['X-Gitlab-Token'].presence + _token ||= token_from_signature(_headers['X-Gitea-Signature'].presence) + _token ||= token_from_signature(_headers['X-Hub-Signature-256'].presence, 'sha256=') + _token + ensure + raise ActiveRecord::RecordNotFound, 'Proveedor no soportado' if _token.blank? + end + end + + # Valida token a partir de firma + # + # @param signature [String,nil] + # @param prepend [String] + # @return [String, nil] + def token_from_signature(signature, prepend = '') + return if signature.nil? + + payload = request.raw_post + + site.roles.where(temporal: false, rol: 'usuarie').pluck(:token).find do |token| + new_signature = prepend + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), token, payload) + + ActiveSupport::SecurityUtils.secure_compare(new_signature, signature.to_s) + end + end + + # Encuentra el sitio a partir de la URL + # + # @return [Site] + def site + @site ||= Site.find_by_name!(params[:site_id]) + end + + # Encuentra le usuarie + # + # @return [Site] + def usuarie + @usuarie ||= site.roles.find_by!(temporal: false, rol: 'usuarie', token: token).usuarie + end + + # Respuesta de error a plataformas + def platforms_answer(exception) + ExceptionNotifier.notify_exception(exception, data: { headers: request.headers.to_h }) + + head :forbidden + end + end + end + end +end diff --git a/app/controllers/api/v1/webhooks_controller.rb b/app/controllers/api/v1/webhooks_controller.rb index 6e7b7022..f64fa93c 100644 --- a/app/controllers/api/v1/webhooks_controller.rb +++ b/app/controllers/api/v1/webhooks_controller.rb @@ -4,8 +4,7 @@ module Api module V1 # Recibe webhooks y lanza un PullJob class WebhooksController < BaseController - # responde con forbidden si falla la validación del token - rescue_from ActiveRecord::RecordNotFound, with: :platforms_answer + include WebhookConcern # Trae los cambios a partir de un post de Webhooks: # (Gitlab, Github, Gitea, etc) @@ -19,59 +18,6 @@ module Api GitPullJob.perform_later(site, usuarie, message) head :ok end - - private - - # encuentra el sitio a partir de la url - def site - @site ||= Site.find_by_name!(params[:site_id]) - end - - # valida el token que envía la plataforma del webhook - # - # @return [String] - def token - @token ||= - begin - # Gitlab - if request.headers['X-Gitlab-Token'].present? - request.headers['X-Gitlab-Token'] - # Github - elsif request.headers['X-Hub-Signature-256'].present? - token_from_signature(request.headers['X-Hub-Signature-256'], 'sha256=') - # Gitea - elsif request.headers['X-Gitea-Signature'].present? - token_from_signature(request.headers['X-Gitea-Signature']) - else - raise ActiveRecord::RecordNotFound, 'proveedor no soportado' - end - end - end - - # valida token a partir de firma de webhook - # - # @return [String, Boolean] - def token_from_signature(signature, prepend = '') - payload = request.body.read - site.roles.where(temporal: false, rol: 'usuarie').pluck(:token).find do |token| - new_signature = prepend + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), token, payload) - ActiveSupport::SecurityUtils.secure_compare(new_signature, signature.to_s) - end.tap do |t| - raise ActiveRecord::RecordNotFound, 'token no encontrado' if t.nil? - end - end - - # encuentra le usuarie - def usuarie - @usuarie ||= site.roles.find_by!(temporal: false, rol: 'usuarie', token: token).usuarie - end - - # respuesta de error a plataformas - def platforms_answer(exception) - ExceptionNotifier.notify_exception(exception, data: { headers: request.headers.to_h }) - - head :forbidden - end end end end diff --git a/config/routes.rb b/config/routes.rb index 635be07a..8186b64e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -18,7 +18,9 @@ Rails.application.routes.draw do get :'contact/cookie', to: 'invitades#contact_cookie' post :'contact/:form', to: 'contact#receive', as: :contact - post :'webhooks/pull', to: 'webhooks#pull' + namespace :webhooks do + post :pull, to: 'webhooks#pull' + end end end end From b24f49fe2611791eb377a56d4f7e9cdbeafafbd4 Mon Sep 17 00:00:00 2001 From: f Date: Fri, 16 Feb 2024 16:01:17 -0300 Subject: [PATCH 06/42] feat: crear webhooks en la social inbox #15109 --- .../api/v1/social_inbox_controller.rb | 22 +++++++++++ app/models/deploy_social_distributed_press.rb | 39 +++++++++++++++++++ app/models/social_inbox.rb | 6 +++ config/application.rb | 3 ++ config/routes.rb | 6 +++ 5 files changed, 76 insertions(+) create mode 100644 app/controllers/api/v1/social_inbox_controller.rb diff --git a/app/controllers/api/v1/social_inbox_controller.rb b/app/controllers/api/v1/social_inbox_controller.rb new file mode 100644 index 00000000..3881b6bc --- /dev/null +++ b/app/controllers/api/v1/social_inbox_controller.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Api + module V1 + # Recibe webhooks de la Social Inbox + class SocialInboxController < BaseController + include WebhookConcern + + def moderationqueued + head :accepted + end + + def onapproved + head :accepted + end + + def onrejected + head :accepted + end + end + end +end diff --git a/app/models/deploy_social_distributed_press.rb b/app/models/deploy_social_distributed_press.rb index db555ab7..f5c38a22 100644 --- a/app/models/deploy_social_distributed_press.rb +++ b/app/models/deploy_social_distributed_press.rb @@ -7,6 +7,8 @@ class DeploySocialDistributedPress < Deploy # Solo luego de publicar remotamente DEPENDENCIES = %i[deploy_distributed_press deploy_rsync deploy_full_rsync] + after_save :create_hooks! + # Envía las notificaciones def deploy(output: false) with_tempfile(site.private_key_pem) do |file| @@ -52,4 +54,41 @@ class DeploySocialDistributedPress < Deploy def flags_for_build(**args) "--key #{Shellwords.escape args[:private_key].path}" end + + private + + # Obtiene el hostname de la API de Sutty + # + # @return [String] + def api_hostname + Rails.application.routes.default_url_options[:host].sub('panel', 'api') + end + + # Crea los hooks en la Social Inbox para que nos avise de actividades + # nuevas + # + # @return [nil] + def create_hooks! + hook_client = site.social_inbox.hook + + hook_client.class::EVENTS.each do |event| + event_url = :"v1_site_webhooks_social_inbox_#{event}_url" + + webhook = DistributedPress::V1::Social::Schemas::Webhook.new.call({ + method: 'POST', + url: Rails.application.routes.url_helpers.public_send(event_url, site_id: site.name, host: api_hostname), + headers: { + 'X-Social-Inbox': rol.token + } + }) + + raise ArgumentError, webhook.errors.messages if webhook.failure? + + response = hook_client.put(event: event, hook: webhook) + + raise ArgumentError, response.parsed_body unless response.ok? + rescue ArgumentError => e + ExceptionNotifier.notify_exception(e, data: { site_id: site.name, usuarie_id: rol.usuarie_id }) + end + end end diff --git a/app/models/social_inbox.rb b/app/models/social_inbox.rb index 47c3457c..8aa5b504 100644 --- a/app/models/social_inbox.rb +++ b/app/models/social_inbox.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'distributed_press/v1/social/client' +require 'distributed_press/v1/social/hook' # Gestiona la Social Inbox de un sitio class SocialInbox @@ -34,6 +35,11 @@ class SocialInbox ) end + # @return [DistributedPress::V1::Social::Hook] + def hook + @hook ||= DistributedPress::V1::Social::Hook.new(client: client, actor: actor) + end + # @return [String] def public_key_url @public_key_url ||= URI("https://#{hostname}").tap do |uri| diff --git a/config/application.rb b/config/application.rb index 529e341a..73a7a884 100644 --- a/config/application.rb +++ b/config/application.rb @@ -2,6 +2,9 @@ require_relative 'boot' +require 'redis-client' +require 'hiredis-client' +require 'brs' require 'rails' # Pick the frameworks you want: require 'active_model/railtie' diff --git a/config/routes.rb b/config/routes.rb index 8186b64e..f6f081f7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -20,6 +20,12 @@ Rails.application.routes.draw do namespace :webhooks do post :pull, to: 'webhooks#pull' + + namespace :social_inbox do + post :moderationqueued, to: 'social_inbox#moderationqueued' + post :onapproved, to: 'social_inbox#onapproved' + post :onrejected, to: 'social_inbox#onrejected' + end end end end From b4117d7c348b63b0af2efa86bbb3b79d582ec1ad Mon Sep 17 00:00:00 2001 From: f Date: Sat, 17 Feb 2024 14:11:17 -0300 Subject: [PATCH 07/42] chore: rubocop --- .../api/v1/concerns/webhook_concern.rb | 13 ++++++------ app/models/deploy.rb | 8 ++++---- app/models/deploy_social_distributed_press.rb | 20 +++++++++++-------- app/models/site/social_distributed_press.rb | 3 ++- app/models/social_inbox.rb | 5 +---- 5 files changed, 26 insertions(+), 23 deletions(-) diff --git a/app/controllers/api/v1/concerns/webhook_concern.rb b/app/controllers/api/v1/concerns/webhook_concern.rb index 59d48b28..9960e550 100644 --- a/app/controllers/api/v1/concerns/webhook_concern.rb +++ b/app/controllers/api/v1/concerns/webhook_concern.rb @@ -18,13 +18,14 @@ module Api def token @token ||= begin - _headers = request.headers - _token ||= _headers['X-Gitlab-Token'].presence - _token ||= token_from_signature(_headers['X-Gitea-Signature'].presence) - _token ||= token_from_signature(_headers['X-Hub-Signature-256'].presence, 'sha256=') - _token + header = request.headers + token = header['X-Social-Inbox'].presence + token ||= header['X-Gitlab-Token'].presence + token ||= token_from_signature(header['X-Gitea-Signature'].presence) + token ||= token_from_signature(header['X-Hub-Signature-256'].presence, 'sha256=') + token ensure - raise ActiveRecord::RecordNotFound, 'Proveedor no soportado' if _token.blank? + raise ActiveRecord::RecordNotFound, 'Proveedor no soportado' if token.blank? end end diff --git a/app/models/deploy.rb b/app/models/deploy.rb index 8f28f214..77646034 100644 --- a/app/models/deploy.rb +++ b/app/models/deploy.rb @@ -14,8 +14,8 @@ class Deploy < ApplicationRecord has_many :build_stats, dependent: :destroy - DEPENDENCIES = [] - SOFT_DEPENDENCIES = [] + DEPENDENCIES = [].freeze + SOFT_DEPENDENCIES = [].freeze def deploy(**) raise NotImplementedError @@ -74,7 +74,7 @@ class Deploy < ApplicationRecord 'HOME' => home_dir, 'PATH' => paths.join(':'), 'JEKYLL_ENV' => Rails.env, - 'LANG' => ENV['LANG'], + 'LANG' => ENV.fetch('LANG', nil) }) end @@ -139,7 +139,7 @@ class Deploy < ApplicationRecord # provisto con el archivo como parámetro # # @param :content [String] - def with_tempfile(content, &block) + def with_tempfile(content) Tempfile.create(SecureRandom.hex) do |file| file.write content.to_s file.rewind diff --git a/app/models/deploy_social_distributed_press.rb b/app/models/deploy_social_distributed_press.rb index f5c38a22..fc0e01d5 100644 --- a/app/models/deploy_social_distributed_press.rb +++ b/app/models/deploy_social_distributed_press.rb @@ -5,7 +5,7 @@ require 'distributed_press/v1/social/client' # Publicar novedades al Fediverso class DeploySocialDistributedPress < Deploy # Solo luego de publicar remotamente - DEPENDENCIES = %i[deploy_distributed_press deploy_rsync deploy_full_rsync] + DEPENDENCIES = %i[deploy_distributed_press deploy_rsync deploy_full_rsync].freeze after_save :create_hooks! @@ -70,17 +70,21 @@ class DeploySocialDistributedPress < Deploy # @return [nil] def create_hooks! hook_client = site.social_inbox.hook + webhook_class = DistributedPress::V1::Social::Schemas::Webhook hook_client.class::EVENTS.each do |event| event_url = :"v1_site_webhooks_social_inbox_#{event}_url" - webhook = DistributedPress::V1::Social::Schemas::Webhook.new.call({ - method: 'POST', - url: Rails.application.routes.url_helpers.public_send(event_url, site_id: site.name, host: api_hostname), - headers: { - 'X-Social-Inbox': rol.token - } - }) + webhook = + webhook_class.new.call({ + method: 'POST', + url: Rails.application.routes.url_helpers.public_send( + event_url, site_id: site.name, host: api_hostname + ), + headers: { + 'X-Social-Inbox': rol.token + } + }) raise ArgumentError, webhook.errors.messages if webhook.failure? diff --git a/app/models/site/social_distributed_press.rb b/app/models/site/social_distributed_press.rb index 1193ca76..d3ebf579 100644 --- a/app/models/site/social_distributed_press.rb +++ b/app/models/site/social_distributed_press.rb @@ -25,7 +25,8 @@ class Site def generate_private_key_pem! self.private_key_pem ||= DistributedPress::V1::Social::Client.new( public_key_url: nil, - key_size: 2048).private_key.export + key_size: 2048 + ).private_key.export end end end diff --git a/app/models/social_inbox.rb b/app/models/social_inbox.rb index 8aa5b504..24f749be 100644 --- a/app/models/social_inbox.rb +++ b/app/models/social_inbox.rb @@ -50,9 +50,6 @@ class SocialInbox def hostname @hostname ||= - begin - host = site.config.dig('activity_pub', 'hostname') - host ||= site.hostname - end + site.config.dig('activity_pub', 'hostname') || site.hostname end end From 27c0ca655eef45d61523f577dc9cc34cee0d0afe Mon Sep 17 00:00:00 2001 From: f Date: Tue, 20 Feb 2024 14:44:52 -0300 Subject: [PATCH 08/42] feat: modelo de datos de activitypub #15109 --- app/models/activity_pub.rb | 33 ++++ app/models/activity_pub/activity.rb | 24 +++ app/models/activity_pub/activity/create.rb | 3 + app/models/activity_pub/activity/delete.rb | 3 + app/models/activity_pub/activity/flag.rb | 3 + app/models/activity_pub/activity/follow.rb | 7 + app/models/activity_pub/activity/generic.rb | 3 + app/models/activity_pub/activity/undo.rb | 8 + app/models/activity_pub/activity/update.rb | 3 + app/models/activity_pub/actor.rb | 14 ++ .../activity_pub/concerns/json_ld_concern.rb | 32 ++++ app/models/activity_pub/instance.rb | 21 +++ app/models/activity_pub/object.rb | 11 ++ app/models/activity_pub/object/application.rb | 6 + app/models/activity_pub/object/article.rb | 6 + app/models/activity_pub/object/generic.rb | 4 + app/models/activity_pub/object/note.rb | 6 + .../activity_pub/object/organization.rb | 6 + app/models/activity_pub/object/person.rb | 6 + ...19153919_create_activity_pub_activities.rb | 16 ++ ...240219175839_create_activity_pub_actors.rb | 12 ++ .../20240219204011_create_activity_pubs.rb | 18 ++ ...40219204224_create_activity_pub_objects.rb | 17 ++ ...220161414_create_activity_pub_instances.rb | 13 ++ db/structure.sql | 161 +++++++++++++++++- 25 files changed, 435 insertions(+), 1 deletion(-) create mode 100644 app/models/activity_pub.rb create mode 100644 app/models/activity_pub/activity.rb create mode 100644 app/models/activity_pub/activity/create.rb create mode 100644 app/models/activity_pub/activity/delete.rb create mode 100644 app/models/activity_pub/activity/flag.rb create mode 100644 app/models/activity_pub/activity/follow.rb create mode 100644 app/models/activity_pub/activity/generic.rb create mode 100644 app/models/activity_pub/activity/undo.rb create mode 100644 app/models/activity_pub/activity/update.rb create mode 100644 app/models/activity_pub/actor.rb create mode 100644 app/models/activity_pub/concerns/json_ld_concern.rb create mode 100644 app/models/activity_pub/instance.rb create mode 100644 app/models/activity_pub/object.rb create mode 100644 app/models/activity_pub/object/application.rb create mode 100644 app/models/activity_pub/object/article.rb create mode 100644 app/models/activity_pub/object/generic.rb create mode 100644 app/models/activity_pub/object/note.rb create mode 100644 app/models/activity_pub/object/organization.rb create mode 100644 app/models/activity_pub/object/person.rb create mode 100644 db/migrate/20240219153919_create_activity_pub_activities.rb create mode 100644 db/migrate/20240219175839_create_activity_pub_actors.rb create mode 100644 db/migrate/20240219204011_create_activity_pubs.rb create mode 100644 db/migrate/20240219204224_create_activity_pub_objects.rb create mode 100644 db/migrate/20240220161414_create_activity_pub_instances.rb diff --git a/app/models/activity_pub.rb b/app/models/activity_pub.rb new file mode 100644 index 00000000..c0474b89 --- /dev/null +++ b/app/models/activity_pub.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +# = ActivityPub = +# +# El registro de actividades recibidas y su estado. Cuando recibimos +# una actividad, puede estar destinada a varies actores dentro de Sutty, +# con lo que generamos una cola para cada une. +# +# @see {https://www.w3.org/TR/activitypub/#client-to-server-interactions} +class ActivityPub < ApplicationRecord + include AASM + + belongs_to :site + belongs_to :object, polymorphic: true + has_many :activities + + validates :site_id, presence: true + validates :object_id, presence: true + validates :aasm_state, presence: true, inclusion: { in: %w[paused approved rejected reported deleted] } + + aasm do + # Todavía no hay una decisión sobre el objeto + state :paused, initial: true + # Le usuarie aprobó el objeto + state :approved + # Le usuarie rechazó el objeto + state :rejected + # Le usuarie reportó el objeto + state :reported + # Le actore eliminó el objeto + state :deleted + end +end diff --git a/app/models/activity_pub/activity.rb b/app/models/activity_pub/activity.rb new file mode 100644 index 00000000..4a88c1f3 --- /dev/null +++ b/app/models/activity_pub/activity.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# = Activity = +# +# Lleva un registro de las actividades que nos piden hacer remotamente. +# +# Las actividades pueden tener distintos destinataries (sitios/actores). +# +# @todo Obtener el contenido del objeto dinámicamente si no existe +# localmente, por ejemplo cuando la actividad crea un objeto pero lo +# envía como referencia en lugar de anidarlo. +# +# @see {https://www.w3.org/TR/activitypub/#client-to-server-interactions} +class ActivityPub::Activity < ApplicationRecord + include ActivityPub::Concerns::JsonLdConcern + + belongs_to :activity_pub + has_one :object, through: :activity_pub + + validates :activity_pub_id, presence: true + + # Siempre en orden descendiente para saber el último estado + default_scope -> { order(created_at: :desc) } +end diff --git a/app/models/activity_pub/activity/create.rb b/app/models/activity_pub/activity/create.rb new file mode 100644 index 00000000..3dcba5c2 --- /dev/null +++ b/app/models/activity_pub/activity/create.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +class ActivityPub::Activity::Create < ActivityPub::Activity; end diff --git a/app/models/activity_pub/activity/delete.rb b/app/models/activity_pub/activity/delete.rb new file mode 100644 index 00000000..f3684a0f --- /dev/null +++ b/app/models/activity_pub/activity/delete.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +class ActivityPub::Activity::Delete < ActivityPub::Activity; end diff --git a/app/models/activity_pub/activity/flag.rb b/app/models/activity_pub/activity/flag.rb new file mode 100644 index 00000000..2911911e --- /dev/null +++ b/app/models/activity_pub/activity/flag.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +class ActivityPub::Activity::Flag < ActivityPub::Activity; end diff --git a/app/models/activity_pub/activity/follow.rb b/app/models/activity_pub/activity/follow.rb new file mode 100644 index 00000000..c22dfd51 --- /dev/null +++ b/app/models/activity_pub/activity/follow.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# = Follow = +# +# Una actividad de seguimiento se refiere siempre a une actore (el +# sitio) y proviene de otre actore. +class ActivityPub::Activity::Follow < ActivityPub::Activity; end diff --git a/app/models/activity_pub/activity/generic.rb b/app/models/activity_pub/activity/generic.rb new file mode 100644 index 00000000..8bf76471 --- /dev/null +++ b/app/models/activity_pub/activity/generic.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +class ActivityPub::Activity::Generic < ActivityPub::Activity; end diff --git a/app/models/activity_pub/activity/undo.rb b/app/models/activity_pub/activity/undo.rb new file mode 100644 index 00000000..a4915394 --- /dev/null +++ b/app/models/activity_pub/activity/undo.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# = Undo = +# +# Deshace una actividad, dependiendo de la actividad a la que se +# refiere. +class ActivityPub::Activity::Undo < ActivityPub::Activity +end diff --git a/app/models/activity_pub/activity/update.rb b/app/models/activity_pub/activity/update.rb new file mode 100644 index 00000000..8089cdcf --- /dev/null +++ b/app/models/activity_pub/activity/update.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +class ActivityPub::Activity::Update < ActivityPub::Activity; end diff --git a/app/models/activity_pub/actor.rb b/app/models/activity_pub/actor.rb new file mode 100644 index 00000000..f29c382a --- /dev/null +++ b/app/models/activity_pub/actor.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# = Actor = +# +# Actor es la entidad que realiza acciones en ActivityPub +# +# @todo Obtener el perfil dinámicamente +class ActivityPub::Actor < ApplicationRecord + include ActivityPub::Concerns::JsonLdConcern + + belongs_to :instance + has_many :activity_pubs, as: :object + has_many :objects +end diff --git a/app/models/activity_pub/concerns/json_ld_concern.rb b/app/models/activity_pub/concerns/json_ld_concern.rb new file mode 100644 index 00000000..b0899606 --- /dev/null +++ b/app/models/activity_pub/concerns/json_ld_concern.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class ActivityPub + module Concerns + module JsonLdConcern + extend ActiveSupport::Concern + + included do + validates :uri, presence: true, uniqueness: true + + # Cuando asignamos contenido, obtener la URI si no lo hicimos ya + before_save :uri_from_content!, unless: :uri? + + # Obtiene un tipo de actividad a partir del tipo informado + # + # @param object [Hash] + # @return [Activity] + def self.type_from(object) + "#{self.class.name}::#{object[:type].presence || 'Generic'}".constantize + rescue NameError + self.class::Generic + end + + private + + def uri_from_content! + self.uri = content[:id] + end + end + end + end +end diff --git a/app/models/activity_pub/instance.rb b/app/models/activity_pub/instance.rb new file mode 100644 index 00000000..fe4a777b --- /dev/null +++ b/app/models/activity_pub/instance.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# = Instance = +# +# Representa cada instancia del fediverso que interactúa con la Social +# Inbox. +class ActivityPub::Instance < ApplicationRecord + include AASM + + validates :aasm_state, presence: true, inclusion: { in: %w[paused allowed blocked] } + validates :hostname, uniqueness: true, hostname: true + + has_many :activity_pubs + has_many :actors + + aasm do + state :paused, initial: true + state :allowed + state :blocked + end +end diff --git a/app/models/activity_pub/object.rb b/app/models/activity_pub/object.rb new file mode 100644 index 00000000..519749ef --- /dev/null +++ b/app/models/activity_pub/object.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# Almacena objetos de ActivityPub, como Note, Article, etc. +class ActivityPub::Object < ApplicationRecord + include ActivityPub::Concerns::JsonLdConcern + + belongs_to :actor + has_many :activity_pubs, as: :object + + validates :actor_id, presence: true +end diff --git a/app/models/activity_pub/object/application.rb b/app/models/activity_pub/object/application.rb new file mode 100644 index 00000000..e8a6f97c --- /dev/null +++ b/app/models/activity_pub/object/application.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# = Application = +# +# Una aplicación o instancia +class ActivityPub::Object::Application < ActivityPub::Object; end diff --git a/app/models/activity_pub/object/article.rb b/app/models/activity_pub/object/article.rb new file mode 100644 index 00000000..ad1a6131 --- /dev/null +++ b/app/models/activity_pub/object/article.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# = Article = +# +# Representa artículos +class ActivityPub::Object::Article < ActivityPub::Object; end diff --git a/app/models/activity_pub/object/generic.rb b/app/models/activity_pub/object/generic.rb new file mode 100644 index 00000000..f345e7a9 --- /dev/null +++ b/app/models/activity_pub/object/generic.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +# = Generic = +class ActivityPub::Object::Generic < ActivityPub::Object; end diff --git a/app/models/activity_pub/object/note.rb b/app/models/activity_pub/object/note.rb new file mode 100644 index 00000000..0f84c747 --- /dev/null +++ b/app/models/activity_pub/object/note.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# = Note = +# +# Representa notas, el tipo más común de objeto del Fediverso. +class ActivityPub::Object::Note < ActivityPub::Object; end diff --git a/app/models/activity_pub/object/organization.rb b/app/models/activity_pub/object/organization.rb new file mode 100644 index 00000000..a5327d10 --- /dev/null +++ b/app/models/activity_pub/object/organization.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# = Organization = +# +# Una organización +class ActivityPub::Object::Organization < ActivityPub::Object; end diff --git a/app/models/activity_pub/object/person.rb b/app/models/activity_pub/object/person.rb new file mode 100644 index 00000000..98a1568d --- /dev/null +++ b/app/models/activity_pub/object/person.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# = Person = +# +# Una persona, el perfil de une actore +class ActivityPub::Object::Person < ActivityPub::Object; end diff --git a/db/migrate/20240219153919_create_activity_pub_activities.rb b/db/migrate/20240219153919_create_activity_pub_activities.rb new file mode 100644 index 00000000..555656ad --- /dev/null +++ b/db/migrate/20240219153919_create_activity_pub_activities.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# Actividades. Se asocian a un objeto y a una cola de moderación +class CreateActivityPubActivities < ActiveRecord::Migration[6.1] + def change + create_table :activity_pub_activities, id: :uuid do |t| + t.timestamps + + t.uuid :activity_pub_id, index: true, null: false + + t.string :type, null: false + t.string :uri, null: false + t.jsonb :content, default: {} + end + end +end diff --git a/db/migrate/20240219175839_create_activity_pub_actors.rb b/db/migrate/20240219175839_create_activity_pub_actors.rb new file mode 100644 index 00000000..656b3f63 --- /dev/null +++ b/db/migrate/20240219175839_create_activity_pub_actors.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# Almacena actores de ActivityPub y los relaciona con actividades +class CreateActivityPubActors < ActiveRecord::Migration[6.1] + def change + create_table :activity_pub_actors, id: :uuid do |t| + t.timestamps + t.uuid :instance_id, index: true, null: false + t.string :uri, index: true, unique: true, null: false + end + end +end diff --git a/db/migrate/20240219204011_create_activity_pubs.rb b/db/migrate/20240219204011_create_activity_pubs.rb new file mode 100644 index 00000000..cf797fc8 --- /dev/null +++ b/db/migrate/20240219204011_create_activity_pubs.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# Registro de actividades. +class CreateActivityPubs < ActiveRecord::Migration[6.1] + def change + create_table :activity_pubs, id: :uuid do |t| + t.timestamps + + t.bigint :site_id, null: false + t.uuid :object_id, null: false + t.string :object_type, null: false + + t.string :aasm_state, null: false + + t.index %i[site_id object_id object_type], unique: true + end + end +end diff --git a/db/migrate/20240219204224_create_activity_pub_objects.rb b/db/migrate/20240219204224_create_activity_pub_objects.rb new file mode 100644 index 00000000..865589ab --- /dev/null +++ b/db/migrate/20240219204224_create_activity_pub_objects.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# Almacena objetos de ActivityPub. Los objetos pueden estar compartidos +# por toda la instancia. +class CreateActivityPubObjects < ActiveRecord::Migration[6.1] + def change + create_table :activity_pub_objects, id: :uuid do |t| + t.timestamps + + t.uuid :actor_id, index: true, null: false + + t.string :type, null: false + t.string :uri, null: false, unique: true + t.jsonb :content, default: {} + end + end +end diff --git a/db/migrate/20240220161414_create_activity_pub_instances.rb b/db/migrate/20240220161414_create_activity_pub_instances.rb new file mode 100644 index 00000000..feb9351d --- /dev/null +++ b/db/migrate/20240220161414_create_activity_pub_instances.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Almacena las instancias +class CreateActivityPubInstances < ActiveRecord::Migration[6.1] + def change + create_table :activity_pub_instances, id: :uuid do |t| + t.timestamps + t.string :hostname, index: true, unique: true, null: false + t.string :aasm_state, null: false + t.jsonb :content, default: {} + end + end +end diff --git a/db/structure.sql b/db/structure.sql index dede286d..723c9e99 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -473,6 +473,78 @@ CREATE SEQUENCE public.active_storage_variant_records_id_seq ALTER SEQUENCE public.active_storage_variant_records_id_seq OWNED BY public.active_storage_variant_records.id; +-- +-- Name: activity_pub_activities; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.activity_pub_activities ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + activity_pub_id uuid NOT NULL, + type character varying NOT NULL, + uri character varying NOT NULL, + content jsonb DEFAULT '{}'::jsonb +); + + +-- +-- Name: activity_pub_actors; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.activity_pub_actors ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + instance_id uuid NOT NULL, + uri character varying NOT NULL +); + + +-- +-- Name: activity_pub_instances; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.activity_pub_instances ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + hostname character varying NOT NULL, + aasm_state character varying NOT NULL, + content jsonb DEFAULT '{}'::jsonb +); + + +-- +-- Name: activity_pub_objects; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.activity_pub_objects ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + actor_id uuid NOT NULL, + type character varying NOT NULL, + uri character varying NOT NULL, + content jsonb DEFAULT '{}'::jsonb +); + + +-- +-- Name: activity_pubs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.activity_pubs ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + site_id bigint NOT NULL, + object_id uuid NOT NULL, + object_type character varying NOT NULL, + aasm_state character varying NOT NULL +); + + -- -- Name: ar_internal_metadata; Type: TABLE; Schema: public; Owner: - -- @@ -1565,6 +1637,46 @@ ALTER TABLE ONLY public.active_storage_variant_records ADD CONSTRAINT active_storage_variant_records_pkey PRIMARY KEY (id); +-- +-- Name: activity_pub_activities activity_pub_activities_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.activity_pub_activities + ADD CONSTRAINT activity_pub_activities_pkey PRIMARY KEY (id); + + +-- +-- Name: activity_pub_actors activity_pub_actors_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.activity_pub_actors + ADD CONSTRAINT activity_pub_actors_pkey PRIMARY KEY (id); + + +-- +-- Name: activity_pub_instances activity_pub_instances_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.activity_pub_instances + ADD CONSTRAINT activity_pub_instances_pkey PRIMARY KEY (id); + + +-- +-- Name: activity_pub_objects activity_pub_objects_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.activity_pub_objects + ADD CONSTRAINT activity_pub_objects_pkey PRIMARY KEY (id); + + +-- +-- Name: activity_pubs activity_pubs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.activity_pubs + ADD CONSTRAINT activity_pubs_pkey PRIMARY KEY (id); + + -- -- Name: blazer_audits blazer_audits_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -1865,6 +1977,48 @@ CREATE UNIQUE INDEX index_active_storage_blobs_on_key_and_service_name ON public CREATE UNIQUE INDEX index_active_storage_variant_records_uniqueness ON public.active_storage_variant_records USING btree (blob_id, variation_digest); +-- +-- Name: index_activity_pub_activities_on_activity_pub_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_activity_pub_activities_on_activity_pub_id ON public.activity_pub_activities USING btree (activity_pub_id); + + +-- +-- Name: index_activity_pub_actors_on_instance_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_activity_pub_actors_on_instance_id ON public.activity_pub_actors USING btree (instance_id); + + +-- +-- Name: index_activity_pub_actors_on_uri; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_activity_pub_actors_on_uri ON public.activity_pub_actors USING btree (uri); + + +-- +-- Name: index_activity_pub_instances_on_hostname; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_activity_pub_instances_on_hostname ON public.activity_pub_instances USING btree (hostname); + + +-- +-- Name: index_activity_pub_objects_on_actor_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_activity_pub_objects_on_actor_id ON public.activity_pub_objects USING btree (actor_id); + + +-- +-- Name: index_activity_pubs_on_site_id_and_object_id_and_object_type; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_activity_pubs_on_site_id_and_object_id_and_object_type ON public.activity_pubs USING btree (site_id, object_id, object_type); + + -- -- Name: index_blazer_audits_on_query_id; Type: INDEX; Schema: public; Owner: - -- @@ -2320,6 +2474,11 @@ INSERT INTO "schema_migrations" (version) VALUES ('20230829204127'), ('20230921155401'), ('20230927153926'), -('20240216170202'); +('20240216170202'), +('20240219153919'), +('20240219175839'), +('20240219204011'), +('20240219204224'), +('20240220161414'); From 8921de81aef45c3d822144fc1f2e4bbb38fe0f3e Mon Sep 17 00:00:00 2001 From: f Date: Tue, 20 Feb 2024 17:13:42 -0300 Subject: [PATCH 09/42] feat: rutas --- .../api/v1/concerns/webhook_concern.rb | 72 ------------------ .../api/v1/social_inbox_controller.rb | 22 ------ .../v1/webhooks/concerns/webhook_concern.rb | 76 +++++++++++++++++++ .../api/v1/webhooks/pull_controller.rb | 25 ++++++ .../v1/webhooks/social_inbox_controller.rb | 39 ++++++++++ app/controllers/api/v1/webhooks_controller.rb | 23 ------ config/routes.rb | 4 +- 7 files changed, 142 insertions(+), 119 deletions(-) delete mode 100644 app/controllers/api/v1/concerns/webhook_concern.rb delete mode 100644 app/controllers/api/v1/social_inbox_controller.rb create mode 100644 app/controllers/api/v1/webhooks/concerns/webhook_concern.rb create mode 100644 app/controllers/api/v1/webhooks/pull_controller.rb create mode 100644 app/controllers/api/v1/webhooks/social_inbox_controller.rb delete mode 100644 app/controllers/api/v1/webhooks_controller.rb diff --git a/app/controllers/api/v1/concerns/webhook_concern.rb b/app/controllers/api/v1/concerns/webhook_concern.rb deleted file mode 100644 index 9960e550..00000000 --- a/app/controllers/api/v1/concerns/webhook_concern.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -module Api - module V1 - # Helpers para webhooks - module WebhookConcern - extend ActiveSupport::Concern - - included do - # Responde con forbidden si falla la validación del token - rescue_from ActiveRecord::RecordNotFound, with: :platforms_answer - - private - - # Valida el token que envía la plataforma en el webhook - # - # @return [String] - def token - @token ||= - begin - header = request.headers - token = header['X-Social-Inbox'].presence - token ||= header['X-Gitlab-Token'].presence - token ||= token_from_signature(header['X-Gitea-Signature'].presence) - token ||= token_from_signature(header['X-Hub-Signature-256'].presence, 'sha256=') - token - ensure - raise ActiveRecord::RecordNotFound, 'Proveedor no soportado' if token.blank? - end - end - - # Valida token a partir de firma - # - # @param signature [String,nil] - # @param prepend [String] - # @return [String, nil] - def token_from_signature(signature, prepend = '') - return if signature.nil? - - payload = request.raw_post - - site.roles.where(temporal: false, rol: 'usuarie').pluck(:token).find do |token| - new_signature = prepend + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), token, payload) - - ActiveSupport::SecurityUtils.secure_compare(new_signature, signature.to_s) - end - end - - # Encuentra el sitio a partir de la URL - # - # @return [Site] - def site - @site ||= Site.find_by_name!(params[:site_id]) - end - - # Encuentra le usuarie - # - # @return [Site] - def usuarie - @usuarie ||= site.roles.find_by!(temporal: false, rol: 'usuarie', token: token).usuarie - end - - # Respuesta de error a plataformas - def platforms_answer(exception) - ExceptionNotifier.notify_exception(exception, data: { headers: request.headers.to_h }) - - head :forbidden - end - end - end - end -end diff --git a/app/controllers/api/v1/social_inbox_controller.rb b/app/controllers/api/v1/social_inbox_controller.rb deleted file mode 100644 index 3881b6bc..00000000 --- a/app/controllers/api/v1/social_inbox_controller.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module Api - module V1 - # Recibe webhooks de la Social Inbox - class SocialInboxController < BaseController - include WebhookConcern - - def moderationqueued - head :accepted - end - - def onapproved - head :accepted - end - - def onrejected - head :accepted - end - end - end -end diff --git a/app/controllers/api/v1/webhooks/concerns/webhook_concern.rb b/app/controllers/api/v1/webhooks/concerns/webhook_concern.rb new file mode 100644 index 00000000..a546a55c --- /dev/null +++ b/app/controllers/api/v1/webhooks/concerns/webhook_concern.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module Api + module V1 + module Webhooks + module Concerns + # Helpers para webhooks + module WebhookConcern + extend ActiveSupport::Concern + + included do + # Responde con forbidden si falla la validación del token + rescue_from ActiveRecord::RecordNotFound, with: :platforms_answer + + private + + # Valida el token que envía la plataforma en el webhook + # + # @return [String] + def token + @token ||= + begin + header = request.headers + token = header['X-Social-Inbox'].presence + token ||= header['X-Gitlab-Token'].presence + token ||= token_from_signature(header['X-Gitea-Signature'].presence) + token ||= token_from_signature(header['X-Hub-Signature-256'].presence, 'sha256=') + token + ensure + raise ActiveRecord::RecordNotFound, 'Proveedor no soportado' if token.blank? + end + end + + # Valida token a partir de firma + # + # @param signature [String,nil] + # @param prepend [String] + # @return [String, nil] + def token_from_signature(signature, prepend = '') + return if signature.nil? + + payload = request.raw_post + + site.roles.where(temporal: false, rol: 'usuarie').pluck(:token).find do |token| + new_signature = prepend + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), token, payload) + + ActiveSupport::SecurityUtils.secure_compare(new_signature, signature.to_s) + end + end + + # Encuentra el sitio a partir de la URL + # + # @return [Site] + def site + @site ||= Site.find_by_name!(params[:site_id]) + end + + # Encuentra le usuarie + # + # @return [Site] + def usuarie + @usuarie ||= site.roles.find_by!(temporal: false, rol: 'usuarie', token: token).usuarie + end + + # Respuesta de error a plataformas + def platforms_answer(exception) + ExceptionNotifier.notify_exception(exception, data: { headers: request.headers.to_h }) + + head :forbidden + end + end + end + end + end + end +end diff --git a/app/controllers/api/v1/webhooks/pull_controller.rb b/app/controllers/api/v1/webhooks/pull_controller.rb new file mode 100644 index 00000000..5f0b703b --- /dev/null +++ b/app/controllers/api/v1/webhooks/pull_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Api + module V1 + module Webhooks + # Recibe webhooks y lanza un PullJob + class PullController < BaseController + include WebhookConcern + + # Trae los cambios a partir de un post de Webhooks: + # (Gitlab, Github, Gitea, etc) + # + # @return [nil] + def pull + message = I18n.with_locale(site.default_locale) do + I18n.t('webhooks.pull.message') + end + + GitPullJob.perform_later(site, usuarie, message) + head :ok + end + end + end + end +end diff --git a/app/controllers/api/v1/webhooks/social_inbox_controller.rb b/app/controllers/api/v1/webhooks/social_inbox_controller.rb new file mode 100644 index 00000000..bc604156 --- /dev/null +++ b/app/controllers/api/v1/webhooks/social_inbox_controller.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Api + module V1 + module Webhooks + # Recibe webhooks de la Social Inbox + # + # @see {https://www.w3.org/TR/activitypub/} + class SocialInboxController < BaseController + include Api::V1::Webhooks::Concerns::WebhookConcern + + # Cuando una actividad ingresa en la cola de moderación, la + # recibimos por acá + # + # Vamos a recibir Create, Update, Delete, Follow, Undo y obtener + # el objeto dentro de cada una para guardar un estado asociado + # al sitio. + # + # El objeto del estado puede ser un objeto o une actore, + # dependiendo de la actividad. + def moderationqueued + head :accepted + end + + # Cuando aprobamos una actividad, recibimos la confirmación y + # cambiamos el estado + def onapproved + head :accepted + end + + # Cuando rechazamos una actividad, recibimos la confirmación y + # cambiamos el estado + def onrejected + head :accepted + end + end + end + end +end diff --git a/app/controllers/api/v1/webhooks_controller.rb b/app/controllers/api/v1/webhooks_controller.rb deleted file mode 100644 index f64fa93c..00000000 --- a/app/controllers/api/v1/webhooks_controller.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module Api - module V1 - # Recibe webhooks y lanza un PullJob - class WebhooksController < BaseController - include WebhookConcern - - # Trae los cambios a partir de un post de Webhooks: - # (Gitlab, Github, Gitea, etc) - # - # @return [nil] - def pull - message = I18n.with_locale(site.default_locale) do - I18n.t('webhooks.pull.message') - end - - GitPullJob.perform_later(site, usuarie, message) - head :ok - end - end - end -end diff --git a/config/routes.rb b/config/routes.rb index f6f081f7..88376dde 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -19,9 +19,9 @@ Rails.application.routes.draw do post :'contact/:form', to: 'contact#receive', as: :contact namespace :webhooks do - post :pull, to: 'webhooks#pull' + post :pull, to: 'pull#pull' - namespace :social_inbox do + scope :social_inbox do post :moderationqueued, to: 'social_inbox#moderationqueued' post :onapproved, to: 'social_inbox#onapproved' post :onrejected, to: 'social_inbox#onrejected' From 9fa43353144d01154dca75cf88bd27a4c879dc03 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 20 Feb 2024 17:15:43 -0300 Subject: [PATCH 10/42] feat: recibir y almacenar actividades --- Gemfile | 3 + Gemfile.lock | 237 ++++++++++-------- .../v1/webhooks/social_inbox_controller.rb | 105 ++++++++ app/models/activity_pub/activity.rb | 5 + .../activity_pub/concerns/json_ld_concern.rb | 6 +- app/models/site/social_distributed_press.rb | 2 + app/models/social_inbox.rb | 19 +- 7 files changed, 262 insertions(+), 115 deletions(-) diff --git a/Gemfile b/Gemfile index 3cf01934..8e955e8f 100644 --- a/Gemfile +++ b/Gemfile @@ -80,6 +80,9 @@ gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git' gem 'kaminari' gem 'device_detector' +gem 'after_commit_everywhere' +gem 'aasm' + # database gem 'hairtrigger' gem 'pg' diff --git a/Gemfile.lock b/Gemfile.lock index 5b8ca619..50745140 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -27,74 +27,80 @@ GIT GEM remote: https://17.3.alpine.gems.sutty.nl/ specs: - actioncable (6.1.7.3) - actionpack (= 6.1.7.3) - activesupport (= 6.1.7.3) + aasm (5.5.0) + concurrent-ruby (~> 1.0) + actioncable (6.1.7.4) + actionpack (= 6.1.7.4) + activesupport (= 6.1.7.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.7.3) - actionpack (= 6.1.7.3) - activejob (= 6.1.7.3) - activerecord (= 6.1.7.3) - activestorage (= 6.1.7.3) - activesupport (= 6.1.7.3) + actionmailbox (6.1.7.4) + actionpack (= 6.1.7.4) + activejob (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) mail (>= 2.7.1) - actionmailer (6.1.7.3) - actionpack (= 6.1.7.3) - actionview (= 6.1.7.3) - activejob (= 6.1.7.3) - activesupport (= 6.1.7.3) + actionmailer (6.1.7.4) + actionpack (= 6.1.7.4) + actionview (= 6.1.7.4) + activejob (= 6.1.7.4) + activesupport (= 6.1.7.4) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.7.3) - actionview (= 6.1.7.3) - activesupport (= 6.1.7.3) + actionpack (6.1.7.4) + actionview (= 6.1.7.4) + activesupport (= 6.1.7.4) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.7.3) - actionpack (= 6.1.7.3) - activerecord (= 6.1.7.3) - activestorage (= 6.1.7.3) - activesupport (= 6.1.7.3) + actiontext (6.1.7.4) + actionpack (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) nokogiri (>= 1.8.5) - actionview (6.1.7.3) - activesupport (= 6.1.7.3) + actionview (6.1.7.4) + activesupport (= 6.1.7.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.1.7.3) - activesupport (= 6.1.7.3) + activejob (6.1.7.4) + activesupport (= 6.1.7.4) globalid (>= 0.3.6) - activemodel (6.1.7.3) - activesupport (= 6.1.7.3) - activerecord (6.1.7.3) - activemodel (= 6.1.7.3) - activesupport (= 6.1.7.3) - activestorage (6.1.7.3) - actionpack (= 6.1.7.3) - activejob (= 6.1.7.3) - activerecord (= 6.1.7.3) - activesupport (= 6.1.7.3) + activemodel (6.1.7.4) + activesupport (= 6.1.7.4) + activerecord (6.1.7.4) + activemodel (= 6.1.7.4) + activesupport (= 6.1.7.4) + activestorage (6.1.7.4) + actionpack (= 6.1.7.4) + activejob (= 6.1.7.4) + activerecord (= 6.1.7.4) + activesupport (= 6.1.7.4) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.7.3) + activesupport (6.1.7.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) zeitwerk (~> 2.3) - addressable (2.8.4) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) adsp (1.0.10) + after_commit_everywhere (1.4.0) + activerecord (>= 4.2) + activesupport ast (2.4.2) autoprefixer-rails (10.4.13.0) execjs (~> 2) - bcrypt (3.1.19-x86_64-linux-musl) + bcrypt (3.1.20-x86_64-linux-musl) bcrypt_pbkdf (1.1.0-x86_64-linux-musl) benchmark-ips (2.12.0) + bigdecimal (3.1.1) bindex (0.8.1-x86_64-linux-musl) blazer (2.6.5) activerecord (>= 5) @@ -105,7 +111,8 @@ GEM autoprefixer-rails (>= 9.1.0) popper_js (>= 1.16.1, < 2) sassc-rails (>= 2.0.0) - brakeman (5.4.1) + brakeman (6.1.1) + racc builder (3.2.4) bundler-audit (0.9.1) bundler (>= 1.2.0, < 3) @@ -125,6 +132,7 @@ GEM concurrent-ruby (1.2.2) concurrent-ruby-ext (1.2.2-x86_64-linux-musl) concurrent-ruby (= 1.2.2) + connection_pool (2.4.1) crass (1.0.6) database_cleaner (2.0.2) database_cleaner-active_record (>= 2, < 3) @@ -132,7 +140,7 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - date (3.3.3-x86_64-linux-musl) + date (3.3.4-x86_64-linux-musl) dead_end (4.0.0) derailed_benchmarks (2.1.2) benchmark-ips (~> 2) @@ -146,8 +154,8 @@ GEM rake (> 10, < 14) ruby-statistics (>= 2.1) thor (>= 0.19, < 2) - device_detector (1.1.1) - devise (4.9.2) + device_detector (1.1.2) + devise (4.9.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) @@ -155,7 +163,7 @@ GEM warden (~> 1.2.3) devise-i18n (1.11.0) devise (>= 4.9.0) - devise_invitable (2.0.8) + devise_invitable (2.0.9) actionmailer (>= 5.0) devise (>= 4.6) distributed-press-api-client (0.4.0rc2) @@ -172,10 +180,10 @@ GEM railties (>= 3.2) down (5.4.1) addressable (~> 2.8) - dry-configurable (1.0.1) + dry-configurable (1.1.0) dry-core (~> 1.0, < 2) zeitwerk (~> 2.6) - dry-core (1.0.0) + dry-core (1.0.1) concurrent-ruby (~> 1.0) zeitwerk (~> 2.6) dry-inflector (1.0.0) @@ -184,7 +192,7 @@ GEM concurrent-ruby (~> 1.0) dry-core (~> 1.0, < 2) zeitwerk (~> 2.6) - dry-schema (1.13.1) + dry-schema (1.13.3) concurrent-ruby (~> 1.0) dry-configurable (~> 1.0, >= 1.0.1) dry-core (~> 1.0, < 2) @@ -192,7 +200,8 @@ GEM dry-logic (>= 1.4, < 2) dry-types (>= 1.7, < 2) zeitwerk (~> 2.6) - dry-types (1.7.1) + dry-types (1.7.2) + bigdecimal (~> 3.0) concurrent-ruby (~> 1.0) dry-core (~> 1.0) dry-inflector (~> 1.0) @@ -225,25 +234,25 @@ GEM ffi (~> 1.0) git_clone_url (2.0.0) uri-ssh_git (>= 2.0) - globalid (1.1.0) - activesupport (>= 5.0) + globalid (1.2.1) + activesupport (>= 6.1) groupdate (6.2.1) activesupport (>= 5.2) hairtrigger (1.0.0) activerecord (>= 6.0, < 8) ruby2ruby (~> 2.4) ruby_parser (~> 3.10) - haml (6.1.2-x86_64-linux-musl) + haml (6.3.0) temple (>= 0.8.2) thor tilt haml-lint (0.999.999) haml_lint - haml_lint (0.45.0) - haml (>= 4.0, < 6.2) + haml_lint (0.53.0) + haml (>= 5.0) parallel (~> 1.10) rainbow - rubocop (>= 0.50.0) + rubocop (>= 1.0) sysexits (~> 1.1) hamlit (3.0.3-x86_64-linux-musl) temple (>= 0.8.2) @@ -296,7 +305,7 @@ GEM terminal-table (~> 2.0) jekyll-commonmark (1.4.0) commonmarker (~> 0.22) - jekyll-images (0.4.1) + jekyll-images (0.4.4) jekyll (~> 4) ruby-filemagic (~> 0.7) ruby-vips (~> 2) @@ -306,7 +315,7 @@ GEM sassc (> 2.0.1, < 3.0) jekyll-watch (2.2.1) listen (~> 3.0) - json (2.6.3-x86_64-linux-musl) + json (2.7.1-x86_64-linux-musl) jwt (2.6.0) kaminari (1.2.2) activesupport (>= 4.1.0) @@ -335,12 +344,12 @@ GEM loaf (0.10.0) railties (>= 3.2) lockbox (1.2.0) - lograge (0.12.0) + lograge (0.14.0) actionpack (>= 4) activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.21.3) + loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -354,36 +363,37 @@ GEM method_source (1.0.0) mini_histogram (0.3.1) mini_magick (4.12.0) - mini_mime (1.1.2) - mini_portile2 (2.8.2) - minitest (5.18.0) + mini_mime (1.1.5) + mini_portile2 (2.8.5) + minitest (5.21.1) mobility (1.2.9) i18n (>= 0.6.10, < 2) request_store (~> 1.0) multi_xml (0.6.0) - net-imap (0.3.4) + net-imap (0.4.9) date net-protocol net-pop (0.1.2) net-protocol - net-protocol (0.2.1) + net-protocol (0.2.2) timeout - net-smtp (0.3.3) + net-smtp (0.4.0) net-protocol - net-ssh (7.1.0) + net-ssh (7.2.1) netaddr (2.0.6) - nio4r (2.5.9-x86_64-linux-musl) - nokogiri (1.15.4-x86_64-linux-musl) + nio4r (2.7.0-x86_64-linux-musl) + nokogiri (1.16.0-x86_64-linux-musl) mini_portile2 (~> 2.8.2) racc (~> 1.4) orm_adapter (0.5.0) pairing_heap (3.0.1) - parallel (1.23.0) - parser (3.2.2.1) + parallel (1.24.0) + parser (3.2.2.3) ast (~> 2.4.1) + racc pathutil (0.16.2) forwardable-extended (~> 2.6) - pg (1.5.3-x86_64-linux-musl) + pg (1.5.4-x86_64-linux-musl) pg_search (2.3.6) activerecord (>= 5.2) activesupport (>= 5.2) @@ -393,55 +403,57 @@ GEM pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) - public_suffix (5.0.3) - puma (6.3.1-x86_64-linux-musl) + public_suffix (5.0.4) + puma (6.4.2-x86_64-linux-musl) nio4r (~> 2.0) - pundit (2.3.0) + pundit (2.3.1) activesupport (>= 3.0.0) que (2.2.1) - racc (1.7.1-x86_64-linux-musl) - rack (2.2.7) + racc (1.7.3-x86_64-linux-musl) + rack (2.2.8) rack-cors (2.0.1) rack (>= 2.0.0) rack-mini-profiler (3.1.0) rack (>= 1.2.0) - rack-proxy (0.7.6) + rack-proxy (0.7.7) rack rack-test (2.1.0) rack (>= 1.3) - rails (6.1.7.3) - actioncable (= 6.1.7.3) - actionmailbox (= 6.1.7.3) - actionmailer (= 6.1.7.3) - actionpack (= 6.1.7.3) - actiontext (= 6.1.7.3) - actionview (= 6.1.7.3) - activejob (= 6.1.7.3) - activemodel (= 6.1.7.3) - activerecord (= 6.1.7.3) - activestorage (= 6.1.7.3) - activesupport (= 6.1.7.3) + rails (6.1.7.4) + actioncable (= 6.1.7.4) + actionmailbox (= 6.1.7.4) + actionmailer (= 6.1.7.4) + actionpack (= 6.1.7.4) + actiontext (= 6.1.7.4) + actionview (= 6.1.7.4) + activejob (= 6.1.7.4) + activemodel (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) bundler (>= 1.15.0) - railties (= 6.1.7.3) + railties (= 6.1.7.4) sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.5.0) - loofah (~> 2.19, >= 2.19.1) - rails-i18n (7.0.7) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + rails-i18n (7.0.8) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) rails_warden (0.6.0) warden (>= 1.2.0) - railties (6.1.7.3) - actionpack (= 6.1.7.3) - activesupport (= 6.1.7.3) + railties (6.1.7.4) + actionpack (= 6.1.7.4) + activesupport (= 6.1.7.4) method_source rake (>= 12.2) thor (~> 1.0) rainbow (3.1.1) - rake (13.0.6) + rake (13.1.0) rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) @@ -464,13 +476,13 @@ GEM redis-store (>= 1.2, < 2) redis-store (1.9.2) redis (>= 4, < 6) - regexp_parser (2.8.0) + regexp_parser (2.9.0) request_store (1.5.1) rack (>= 1.4) - responders (3.1.0) + responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) - rexml (3.2.5) + rexml (3.2.6) rgl (0.6.3) pairing_heap (>= 0.3.0) rexml (~> 3.2, >= 3.2.4) @@ -486,18 +498,19 @@ GEM rubocop-ast (>= 1.24.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.28.1) + rubocop-ast (1.30.0) parser (>= 3.2.1.0) - rubocop-rails (2.19.1) + rubocop-rails (2.23.1) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) ruby-brs (1.3.3-x86_64-linux-musl) adsp (~> 1.0) ruby-filemagic (0.7.3-x86_64-linux-musl) ruby-progressbar (1.13.0) ruby-statistics (3.0.2) - ruby-vips (2.1.4) + ruby-vips (2.2.0) ffi (~> 1.12) ruby2ruby (2.5.0) ruby_parser (~> 3.1) @@ -530,14 +543,14 @@ GEM spring-watcher-listen (2.1.0) listen (>= 2.7, < 4.0) spring (>= 4) - sprockets (4.2.0) + sprockets (4.2.1) concurrent-ruby (~> 1.0) rack (>= 2.2.4, < 4) sprockets-rails (3.4.2) actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) - sqlite3 (1.6.3-x86_64-linux-musl) + sqlite3 (1.7.0-x86_64-linux-musl) mini_portile2 (~> 2.8.0) stackprof (0.2.25-x86_64-linux-musl) stream (0.5.5) @@ -546,13 +559,13 @@ GEM jekyll (~> 4) symbol-fstring (1.0.2-x86_64-linux-musl) sysexits (1.2.0) - temple (0.10.1) + temple (0.10.3) terminal-table (2.0.0) unicode-display_width (~> 1.1, >= 1.1.1) thor (1.3.0) - tilt (2.1.0) + tilt (2.3.0) timecop (0.9.6) - timeout (0.3.2) + timeout (0.4.1) turbolinks (5.2.1) turbolinks-source (~> 5.2) turbolinks-source (5.2.0) @@ -562,7 +575,7 @@ GEM execjs (>= 0.3.0, < 3) unf (0.1.4) unf_ext - unf_ext (0.0.8.2-x86_64-linux-musl) + unf_ext (0.0.9-x86_64-linux-musl) unicode-display_width (1.8.0) uri-ssh_git (2.0.0) validates_hostname (1.0.13) @@ -588,12 +601,14 @@ GEM xpath (3.2.0) nokogiri (~> 1.8) yard (0.9.34) - zeitwerk (2.6.8) + zeitwerk (2.6.12) PLATFORMS x86_64-linux-musl DEPENDENCIES + aasm + after_commit_everywhere bcrypt (~> 3.1.7) bcrypt_pbkdf blazer diff --git a/app/controllers/api/v1/webhooks/social_inbox_controller.rb b/app/controllers/api/v1/webhooks/social_inbox_controller.rb index bc604156..c8c695c1 100644 --- a/app/controllers/api/v1/webhooks/social_inbox_controller.rb +++ b/app/controllers/api/v1/webhooks/social_inbox_controller.rb @@ -19,6 +19,16 @@ module Api # El objeto del estado puede ser un objeto o une actore, # dependiendo de la actividad. def moderationqueued + # Devuelve un error si el token no es válido + usuarie.present? + + ActivityPub.transaction do + # Crea todos los registros necesarios y actualiza el estado + activity.update_activity_pub_state! + end + rescue ActiveRecord::RecordInvalid => e + ExceptionNotifier.notify_exception(e, data: { site: site.name, usuarie: usuarie.email, activity: original_activity }) + ensure head :accepted end @@ -33,6 +43,101 @@ module Api def onrejected head :accepted end + + private + + # Si el objeto ya viene incorporado en la actividad o lo tenemos + # que traer remotamente. + # + # @return [Bool] + def object_embedded? + @object_embedded ||= original_activity[:object].is_a?(Hash) + end + + # Encuentra la URI del objeto o falla si no la encuentra. + # + # @return [String] + def object_uri + @object_uri ||= + begin + case original_activity[:object] + when String then original_activity[:object] + when Hash then original_activity.dig(:object, :id) + end + end + ensure + raise ActiveRecord::RecordNotFound, 'object id missing' unless @object_uri + end + + # Atajo a la instancia + # + # @return [ActivityPub::Instance] + def instance + actor.instance + end + + # Genera un objeto a partir de la actividad. Si el objeto ya + # existe, actualiza su contenido. + # + # @return [ActivityPub::Object] + def object + @object ||= ActivityPub::Object.type_from(original_object).find_or_initialize_by(actor: actor, uri: object_uri).tap do |o| + o.content = original_object if object_embedded? + o.save! + end + end + + # Genera el seguimiento del estado del objeto con respecto al + # sitio. + # + # @return [ActivityPub] + def activity_pub + @activity_pub ||= site.activity_pubs.find_or_create_by!(site: site, object: object) + end + + # Crea la actividad y la vincula con el estado + # + # @return [ActivityPub::Activity] + def activity + @activity ||= ActivityPub::Activity.type_from(original_activity).new(uri: original_activity[:id], activity_pub: activity_pub).tap do |a| + a.content = original_activity.dup + a.content[:object] = object.uri + a.save! + end + end + + # Actor, si no hay instancia, la crea en el momento + # + # @return [Actor] + def actor + @actor ||= ActivityPub::Actor.find_or_initialize_by(uri: original_activity[:actor]).tap do |a| + next if a.instance + + a.instance = ActivityPub::Instance.find_or_create_by(hostname: URI.parse(a.uri).hostname) + a.save! + end + end + + # Descubre la actividad recibida, generando un error si la + # actividad no está dirigida a nosotres. + # + # @todo Validar formato + # @return [Hash] + def original_activity + @original_activity ||= FastJsonparser.parse(request.raw_post).tap do |activity| + raise '@context missing' unless activity[:@context].presence + raise 'id missing' unless activity[:id].presence + raise 'object missing' unless activity[:object].presence + raise 'not for us' unless [activity[:to]].flatten.include?(site.social_inbox.actor_id) + rescue RuntimeError => e + raise ActiveRecord::RecordNotFound, e.message + end + end + + # @return [Hash,String] + def original_object + @original_object ||= original_activity[:object].dup + end end end end diff --git a/app/models/activity_pub/activity.rb b/app/models/activity_pub/activity.rb index 4a88c1f3..a1f734e0 100644 --- a/app/models/activity_pub/activity.rb +++ b/app/models/activity_pub/activity.rb @@ -21,4 +21,9 @@ class ActivityPub::Activity < ApplicationRecord # Siempre en orden descendiente para saber el último estado default_scope -> { order(created_at: :desc) } + + # Cambia la máquina de estados según el tipo de actividad + def update_activity_pub_state! + nil + end end diff --git a/app/models/activity_pub/concerns/json_ld_concern.rb b/app/models/activity_pub/concerns/json_ld_concern.rb index b0899606..bc30330c 100644 --- a/app/models/activity_pub/concerns/json_ld_concern.rb +++ b/app/models/activity_pub/concerns/json_ld_concern.rb @@ -16,9 +16,11 @@ class ActivityPub # @param object [Hash] # @return [Activity] def self.type_from(object) - "#{self.class.name}::#{object[:type].presence || 'Generic'}".constantize + raise NameError unless object.is_a?(Hash) + + "#{model_name.name}::#{object[:type].presence || 'Generic'}".constantize rescue NameError - self.class::Generic + model_name.name.constantize::Generic end private diff --git a/app/models/site/social_distributed_press.rb b/app/models/site/social_distributed_press.rb index d3ebf579..c3abe06e 100644 --- a/app/models/site/social_distributed_press.rb +++ b/app/models/site/social_distributed_press.rb @@ -10,6 +10,8 @@ class Site included do encrypts :private_key_pem + has_many :activity_pubs + before_save :generate_private_key_pem!, unless: :private_key_pem? # @return [SocialInbox] diff --git a/app/models/social_inbox.rb b/app/models/social_inbox.rb index 24f749be..78362a10 100644 --- a/app/models/social_inbox.rb +++ b/app/models/social_inbox.rb @@ -24,6 +24,12 @@ class SocialInbox end end + def actor_id + @actor_id ||= generate_uri do |uri| + uri.path = '/about.jsonld' + end + end + # @return [DistributedPress::V1::Social::Client] def client @client ||= DistributedPress::V1::Social::Client.new( @@ -42,14 +48,23 @@ class SocialInbox # @return [String] def public_key_url - @public_key_url ||= URI("https://#{hostname}").tap do |uri| + @public_key_url ||= generate_uri do |uri| uri.path = '/about.jsonld' uri.fragment = 'main-key' - end.to_s + end end def hostname @hostname ||= site.config.dig('activity_pub', 'hostname') || site.hostname end + + # Genera una URI dentro de este sitio + # + # @return [String] + def generate_uri(&block) + @public_key_url ||= URI("https://#{hostname}").tap do |uri| + yield uri + end.to_s + end end From 88e93e3b5bab7c8fff75062bfc4788f4b33e2453 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 20 Feb 2024 17:15:57 -0300 Subject: [PATCH 11/42] feat: al eliminar una actividad, vaciar su objeto --- app/models/activity_pub.rb | 11 +++++++++++ app/models/activity_pub/activity/delete.rb | 8 +++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/models/activity_pub.rb b/app/models/activity_pub.rb index c0474b89..c15998ea 100644 --- a/app/models/activity_pub.rb +++ b/app/models/activity_pub.rb @@ -29,5 +29,16 @@ class ActivityPub < ApplicationRecord state :reported # Le actore eliminó el objeto state :deleted + + # Recibir una acción de eliminación, eliminar el contenido de la + # base de datos. Esto elimina el contenido para todos los sitios + # porque estamos respetando lo que pidió le actore. + event :delete do + transitions to: :deleted + + after do + object.update(object: {}) + end + end end end diff --git a/app/models/activity_pub/activity/delete.rb b/app/models/activity_pub/activity/delete.rb index f3684a0f..1730d49d 100644 --- a/app/models/activity_pub/activity/delete.rb +++ b/app/models/activity_pub/activity/delete.rb @@ -1,3 +1,9 @@ # frozen_string_literal: true -class ActivityPub::Activity::Delete < ActivityPub::Activity; end +class ActivityPub::Activity::Delete < ActivityPub::Activity + # Si estamos eliminando el objeto, tenemos que vaciar su contenido y + # cambiar el estado a borrado + def update_activity_pub_state! + activity_pub.deleted! + end +end From 8ff556c9ed5f4f585c1148488bed5930d2db6d91 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 20 Feb 2024 17:16:08 -0300 Subject: [PATCH 12/42] fix: al actualizar un objeto, pausar la actividad --- app/models/activity_pub/activity/update.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models/activity_pub/activity/update.rb b/app/models/activity_pub/activity/update.rb index 8089cdcf..34703938 100644 --- a/app/models/activity_pub/activity/update.rb +++ b/app/models/activity_pub/activity/update.rb @@ -1,3 +1,9 @@ # frozen_string_literal: true -class ActivityPub::Activity::Update < ActivityPub::Activity; end +class ActivityPub::Activity::Update < ActivityPub::Activity + # Si estamos actualizando el objeto, tenemos que devolverlo a estado + # de moderación + def update_activity_pub_state! + activity_pub.paused! + end +end From 9a479a157b6d865cf4233c9737bf26d750934e99 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 20 Feb 2024 17:18:15 -0300 Subject: [PATCH 13/42] feat: volver a pausar un objeto aprobado cuando se lo actualiza --- app/models/activity_pub.rb | 6 ++++++ app/models/activity_pub/activity/update.rb | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models/activity_pub.rb b/app/models/activity_pub.rb index c15998ea..2a127433 100644 --- a/app/models/activity_pub.rb +++ b/app/models/activity_pub.rb @@ -40,5 +40,11 @@ class ActivityPub < ApplicationRecord object.update(object: {}) end end + + # Si un objeto previamente aprobado fue actualizado, volvemos a + # pausarlo. + event :pause do + transitions from: :approved, to: :paused + end end end diff --git a/app/models/activity_pub/activity/update.rb b/app/models/activity_pub/activity/update.rb index 34703938..e9203ba5 100644 --- a/app/models/activity_pub/activity/update.rb +++ b/app/models/activity_pub/activity/update.rb @@ -4,6 +4,6 @@ class ActivityPub::Activity::Update < ActivityPub::Activity # Si estamos actualizando el objeto, tenemos que devolverlo a estado # de moderación def update_activity_pub_state! - activity_pub.paused! + activity_pub.pause! if activity_pub.approved? end end From fc7a524a85189bd1cdfe2c86cffa938faa5962aa Mon Sep 17 00:00:00 2001 From: f Date: Tue, 20 Feb 2024 17:18:39 -0300 Subject: [PATCH 14/42] fix: aasm --- config/application.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/application.rb b/config/application.rb index 73a7a884..27a21cc6 100644 --- a/config/application.rb +++ b/config/application.rb @@ -2,6 +2,7 @@ require_relative 'boot' +require 'aasm' require 'redis-client' require 'hiredis-client' require 'brs' From 051d8c6d5624486fb85f9f625eec6bf8ec854789 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 12:30:01 -0300 Subject: [PATCH 15/42] =?UTF-8?q?feat:=20obtener=20el=20contenido=20del=20?= =?UTF-8?q?objeto=20m=C3=A1s=20adelante?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/v1/webhooks/social_inbox_controller.rb | 9 +++++++-- app/jobs/activity_pub/fetch_job.rb | 18 ++++++++++++++++++ app/models/social_inbox.rb | 6 ++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 app/jobs/activity_pub/fetch_job.rb diff --git a/app/controllers/api/v1/webhooks/social_inbox_controller.rb b/app/controllers/api/v1/webhooks/social_inbox_controller.rb index c8c695c1..e51b89dd 100644 --- a/app/controllers/api/v1/webhooks/social_inbox_controller.rb +++ b/app/controllers/api/v1/webhooks/social_inbox_controller.rb @@ -77,12 +77,17 @@ module Api end # Genera un objeto a partir de la actividad. Si el objeto ya - # existe, actualiza su contenido. + # existe, actualiza su contenido. Si el objeto no viene + # incorporado, obtenemos el contenido más tarde. # # @return [ActivityPub::Object] def object @object ||= ActivityPub::Object.type_from(original_object).find_or_initialize_by(actor: actor, uri: object_uri).tap do |o| - o.content = original_object if object_embedded? + if object_embedded? + o.content = original_object + else + ActivityPub::FetchJob.perform_later(site: site, object: object) + end o.save! end end diff --git a/app/jobs/activity_pub/fetch_job.rb b/app/jobs/activity_pub/fetch_job.rb new file mode 100644 index 00000000..526cdafb --- /dev/null +++ b/app/jobs/activity_pub/fetch_job.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# Obtiene o actualiza el contenido de un objeto, usando las credenciales +# del sitio. +# +# XXX: Esto usa las credenciales del sitio para volver el objeto +# disponible para todo el CMS. Asumimos que el objeto devuelto es el +# mismo para todo el mundo y las credenciales solo son para +# autenticación. +class ActivityPub::FetchJob < ApplicationJob + def perform(site:, object:) + ActivityPub::Object.transaction do + response = site.social_inbox.dereferencer.get(uri: object.uri) + + object.update(content: FastJsonparser.parse(response.body)) if response.ok? + end + end +end diff --git a/app/models/social_inbox.rb b/app/models/social_inbox.rb index 78362a10..624ee571 100644 --- a/app/models/social_inbox.rb +++ b/app/models/social_inbox.rb @@ -2,6 +2,7 @@ require 'distributed_press/v1/social/client' require 'distributed_press/v1/social/hook' +require 'distributed_press/v1/social/dereferencer' # Gestiona la Social Inbox de un sitio class SocialInbox @@ -41,6 +42,11 @@ class SocialInbox ) end + # @return [DistributedPress::V1::Social::Dereferencer] + def dereferencer + @dereferencer ||= DistributedPress::V1::Social::Dereferencer.new(client: client) + end + # @return [DistributedPress::V1::Social::Hook] def hook @hook ||= DistributedPress::V1::Social::Hook.new(client: client, actor: actor) From fb4401fd537cf0071ce41963334ec919a70ada63 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 12:32:39 -0300 Subject: [PATCH 16/42] feat: no actualizar si no es necesario MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cuando la respuesta viene desde la caché, no es es necesario modificar el objeto. --- app/jobs/activity_pub/fetch_job.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/jobs/activity_pub/fetch_job.rb b/app/jobs/activity_pub/fetch_job.rb index 526cdafb..097a8d32 100644 --- a/app/jobs/activity_pub/fetch_job.rb +++ b/app/jobs/activity_pub/fetch_job.rb @@ -12,7 +12,11 @@ class ActivityPub::FetchJob < ApplicationJob ActivityPub::Object.transaction do response = site.social_inbox.dereferencer.get(uri: object.uri) - object.update(content: FastJsonparser.parse(response.body)) if response.ok? + # @todo Fallar cuando la respuesta no funcione? + return unless response.ok? + return unless response.miss? + + object.update(content: FastJsonparser.parse(response.body)) end end end From 091d5ac41d5a0bfc1ff30ae4e28a6b61c74a963c Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 12:46:38 -0300 Subject: [PATCH 17/42] chore: rubocop --- .../v1/webhooks/social_inbox_controller.rb | 18 ++++++++------- app/jobs/activity_pub/fetch_job.rb | 18 ++++++++------- app/models/activity_pub/activity.rb | 22 ++++++++++--------- app/models/activity_pub/activity/create.rb | 6 ++++- app/models/activity_pub/activity/delete.rb | 14 +++++++----- app/models/activity_pub/activity/flag.rb | 6 ++++- app/models/activity_pub/activity/follow.rb | 6 ++++- app/models/activity_pub/activity/generic.rb | 6 ++++- app/models/activity_pub/activity/undo.rb | 6 ++++- app/models/activity_pub/activity/update.rb | 14 +++++++----- app/models/activity_pub/actor.rb | 12 +++++----- app/models/activity_pub/instance.rb | 22 ++++++++++--------- app/models/activity_pub/object.rb | 12 +++++----- app/models/activity_pub/object/application.rb | 6 ++++- app/models/activity_pub/object/article.rb | 6 ++++- app/models/activity_pub/object/generic.rb | 6 ++++- app/models/activity_pub/object/note.rb | 6 ++++- .../activity_pub/object/organization.rb | 6 ++++- app/models/activity_pub/object/person.rb | 6 ++++- app/models/social_inbox.rb | 4 +--- 20 files changed, 132 insertions(+), 70 deletions(-) diff --git a/app/controllers/api/v1/webhooks/social_inbox_controller.rb b/app/controllers/api/v1/webhooks/social_inbox_controller.rb index e51b89dd..3341b33d 100644 --- a/app/controllers/api/v1/webhooks/social_inbox_controller.rb +++ b/app/controllers/api/v1/webhooks/social_inbox_controller.rb @@ -27,7 +27,9 @@ module Api activity.update_activity_pub_state! end rescue ActiveRecord::RecordInvalid => e - ExceptionNotifier.notify_exception(e, data: { site: site.name, usuarie: usuarie.email, activity: original_activity }) + ExceptionNotifier.notify_exception(e, + data: { site: site.name, usuarie: usuarie.email, + activity: original_activity }) ensure head :accepted end @@ -59,11 +61,9 @@ module Api # @return [String] def object_uri @object_uri ||= - begin - case original_activity[:object] - when String then original_activity[:object] - when Hash then original_activity.dig(:object, :id) - end + case original_activity[:object] + when String then original_activity[:object] + when Hash then original_activity.dig(:object, :id) end ensure raise ActiveRecord::RecordNotFound, 'object id missing' unless @object_uri @@ -82,7 +82,8 @@ module Api # # @return [ActivityPub::Object] def object - @object ||= ActivityPub::Object.type_from(original_object).find_or_initialize_by(actor: actor, uri: object_uri).tap do |o| + @object ||= ActivityPub::Object.type_from(original_object).find_or_initialize_by(actor: actor, + uri: object_uri).tap do |o| if object_embedded? o.content = original_object else @@ -104,7 +105,8 @@ module Api # # @return [ActivityPub::Activity] def activity - @activity ||= ActivityPub::Activity.type_from(original_activity).new(uri: original_activity[:id], activity_pub: activity_pub).tap do |a| + @activity ||= ActivityPub::Activity.type_from(original_activity).new(uri: original_activity[:id], + activity_pub: activity_pub).tap do |a| a.content = original_activity.dup a.content[:object] = object.uri a.save! diff --git a/app/jobs/activity_pub/fetch_job.rb b/app/jobs/activity_pub/fetch_job.rb index 097a8d32..ff14c795 100644 --- a/app/jobs/activity_pub/fetch_job.rb +++ b/app/jobs/activity_pub/fetch_job.rb @@ -7,16 +7,18 @@ # disponible para todo el CMS. Asumimos que el objeto devuelto es el # mismo para todo el mundo y las credenciales solo son para # autenticación. -class ActivityPub::FetchJob < ApplicationJob - def perform(site:, object:) - ActivityPub::Object.transaction do - response = site.social_inbox.dereferencer.get(uri: object.uri) +class ActivityPub + class FetchJob < ApplicationJob + def perform(site:, object:) + ActivityPub::Object.transaction do + response = site.social_inbox.dereferencer.get(uri: object.uri) - # @todo Fallar cuando la respuesta no funcione? - return unless response.ok? - return unless response.miss? + # @todo Fallar cuando la respuesta no funcione? + return unless response.ok? + return unless response.miss? - object.update(content: FastJsonparser.parse(response.body)) + object.update(content: FastJsonparser.parse(response.body)) + end end end end diff --git a/app/models/activity_pub/activity.rb b/app/models/activity_pub/activity.rb index a1f734e0..5ee3d2d1 100644 --- a/app/models/activity_pub/activity.rb +++ b/app/models/activity_pub/activity.rb @@ -11,19 +11,21 @@ # envía como referencia en lugar de anidarlo. # # @see {https://www.w3.org/TR/activitypub/#client-to-server-interactions} -class ActivityPub::Activity < ApplicationRecord - include ActivityPub::Concerns::JsonLdConcern +class ActivityPub + class Activity < ApplicationRecord + include ActivityPub::Concerns::JsonLdConcern - belongs_to :activity_pub - has_one :object, through: :activity_pub + belongs_to :activity_pub + has_one :object, through: :activity_pub - validates :activity_pub_id, presence: true + validates :activity_pub_id, presence: true - # Siempre en orden descendiente para saber el último estado - default_scope -> { order(created_at: :desc) } + # Siempre en orden descendiente para saber el último estado + default_scope -> { order(created_at: :desc) } - # Cambia la máquina de estados según el tipo de actividad - def update_activity_pub_state! - nil + # Cambia la máquina de estados según el tipo de actividad + def update_activity_pub_state! + nil + end end end diff --git a/app/models/activity_pub/activity/create.rb b/app/models/activity_pub/activity/create.rb index 3dcba5c2..4acafaf2 100644 --- a/app/models/activity_pub/activity/create.rb +++ b/app/models/activity_pub/activity/create.rb @@ -1,3 +1,7 @@ # frozen_string_literal: true -class ActivityPub::Activity::Create < ActivityPub::Activity; end +class ActivityPub + module Activity + class Create < ActivityPub::Activity; end + end +end diff --git a/app/models/activity_pub/activity/delete.rb b/app/models/activity_pub/activity/delete.rb index 1730d49d..2973f730 100644 --- a/app/models/activity_pub/activity/delete.rb +++ b/app/models/activity_pub/activity/delete.rb @@ -1,9 +1,13 @@ # frozen_string_literal: true -class ActivityPub::Activity::Delete < ActivityPub::Activity - # Si estamos eliminando el objeto, tenemos que vaciar su contenido y - # cambiar el estado a borrado - def update_activity_pub_state! - activity_pub.deleted! +class ActivityPub + module Activity + class Delete < ActivityPub::Activity + # Si estamos eliminando el objeto, tenemos que vaciar su contenido y + # cambiar el estado a borrado + def update_activity_pub_state! + activity_pub.deleted! + end + end end end diff --git a/app/models/activity_pub/activity/flag.rb b/app/models/activity_pub/activity/flag.rb index 2911911e..27bbe266 100644 --- a/app/models/activity_pub/activity/flag.rb +++ b/app/models/activity_pub/activity/flag.rb @@ -1,3 +1,7 @@ # frozen_string_literal: true -class ActivityPub::Activity::Flag < ActivityPub::Activity; end +class ActivityPub + module Activity + class Flag < ActivityPub::Activity; end + end +end diff --git a/app/models/activity_pub/activity/follow.rb b/app/models/activity_pub/activity/follow.rb index c22dfd51..9e32b67d 100644 --- a/app/models/activity_pub/activity/follow.rb +++ b/app/models/activity_pub/activity/follow.rb @@ -4,4 +4,8 @@ # # Una actividad de seguimiento se refiere siempre a une actore (el # sitio) y proviene de otre actore. -class ActivityPub::Activity::Follow < ActivityPub::Activity; end +class ActivityPub + module Activity + class Follow < ActivityPub::Activity; end + end +end diff --git a/app/models/activity_pub/activity/generic.rb b/app/models/activity_pub/activity/generic.rb index 8bf76471..89c26247 100644 --- a/app/models/activity_pub/activity/generic.rb +++ b/app/models/activity_pub/activity/generic.rb @@ -1,3 +1,7 @@ # frozen_string_literal: true -class ActivityPub::Activity::Generic < ActivityPub::Activity; end +class ActivityPub + module Activity + class Generic < ActivityPub::Activity; end + end +end diff --git a/app/models/activity_pub/activity/undo.rb b/app/models/activity_pub/activity/undo.rb index a4915394..98233af9 100644 --- a/app/models/activity_pub/activity/undo.rb +++ b/app/models/activity_pub/activity/undo.rb @@ -4,5 +4,9 @@ # # Deshace una actividad, dependiendo de la actividad a la que se # refiere. -class ActivityPub::Activity::Undo < ActivityPub::Activity +class ActivityPub + module Activity + class Undo < ActivityPub::Activity + end + end end diff --git a/app/models/activity_pub/activity/update.rb b/app/models/activity_pub/activity/update.rb index e9203ba5..50622b93 100644 --- a/app/models/activity_pub/activity/update.rb +++ b/app/models/activity_pub/activity/update.rb @@ -1,9 +1,13 @@ # frozen_string_literal: true -class ActivityPub::Activity::Update < ActivityPub::Activity - # Si estamos actualizando el objeto, tenemos que devolverlo a estado - # de moderación - def update_activity_pub_state! - activity_pub.pause! if activity_pub.approved? +class ActivityPub + module Activity + class Update < ActivityPub::Activity + # Si estamos actualizando el objeto, tenemos que devolverlo a estado + # de moderación + def update_activity_pub_state! + activity_pub.pause! if activity_pub.approved? + end + end end end diff --git a/app/models/activity_pub/actor.rb b/app/models/activity_pub/actor.rb index f29c382a..7be69602 100644 --- a/app/models/activity_pub/actor.rb +++ b/app/models/activity_pub/actor.rb @@ -5,10 +5,12 @@ # Actor es la entidad que realiza acciones en ActivityPub # # @todo Obtener el perfil dinámicamente -class ActivityPub::Actor < ApplicationRecord - include ActivityPub::Concerns::JsonLdConcern +class ActivityPub + class Actor < ApplicationRecord + include ActivityPub::Concerns::JsonLdConcern - belongs_to :instance - has_many :activity_pubs, as: :object - has_many :objects + belongs_to :instance + has_many :activity_pubs, as: :object + has_many :objects + end end diff --git a/app/models/activity_pub/instance.rb b/app/models/activity_pub/instance.rb index fe4a777b..b13b8676 100644 --- a/app/models/activity_pub/instance.rb +++ b/app/models/activity_pub/instance.rb @@ -4,18 +4,20 @@ # # Representa cada instancia del fediverso que interactúa con la Social # Inbox. -class ActivityPub::Instance < ApplicationRecord - include AASM +class ActivityPub + class Instance < ApplicationRecord + include AASM - validates :aasm_state, presence: true, inclusion: { in: %w[paused allowed blocked] } - validates :hostname, uniqueness: true, hostname: true + validates :aasm_state, presence: true, inclusion: { in: %w[paused allowed blocked] } + validates :hostname, uniqueness: true, hostname: true - has_many :activity_pubs - has_many :actors + has_many :activity_pubs + has_many :actors - aasm do - state :paused, initial: true - state :allowed - state :blocked + aasm do + state :paused, initial: true + state :allowed + state :blocked + end end end diff --git a/app/models/activity_pub/object.rb b/app/models/activity_pub/object.rb index 519749ef..49a06772 100644 --- a/app/models/activity_pub/object.rb +++ b/app/models/activity_pub/object.rb @@ -1,11 +1,13 @@ # frozen_string_literal: true # Almacena objetos de ActivityPub, como Note, Article, etc. -class ActivityPub::Object < ApplicationRecord - include ActivityPub::Concerns::JsonLdConcern +class ActivityPub + class Object < ApplicationRecord + include ActivityPub::Concerns::JsonLdConcern - belongs_to :actor - has_many :activity_pubs, as: :object + belongs_to :actor + has_many :activity_pubs, as: :object - validates :actor_id, presence: true + validates :actor_id, presence: true + end end diff --git a/app/models/activity_pub/object/application.rb b/app/models/activity_pub/object/application.rb index e8a6f97c..e8d8fa0e 100644 --- a/app/models/activity_pub/object/application.rb +++ b/app/models/activity_pub/object/application.rb @@ -3,4 +3,8 @@ # = Application = # # Una aplicación o instancia -class ActivityPub::Object::Application < ActivityPub::Object; end +class ActivityPub + module Object + class Application < ActivityPub::Object; end + end +end diff --git a/app/models/activity_pub/object/article.rb b/app/models/activity_pub/object/article.rb index ad1a6131..69fe371c 100644 --- a/app/models/activity_pub/object/article.rb +++ b/app/models/activity_pub/object/article.rb @@ -3,4 +3,8 @@ # = Article = # # Representa artículos -class ActivityPub::Object::Article < ActivityPub::Object; end +class ActivityPub + module Object + class Article < ActivityPub::Object; end + end +end diff --git a/app/models/activity_pub/object/generic.rb b/app/models/activity_pub/object/generic.rb index f345e7a9..c16b25c6 100644 --- a/app/models/activity_pub/object/generic.rb +++ b/app/models/activity_pub/object/generic.rb @@ -1,4 +1,8 @@ # frozen_string_literal: true # = Generic = -class ActivityPub::Object::Generic < ActivityPub::Object; end +class ActivityPub + module Object + class Generic < ActivityPub::Object; end + end +end diff --git a/app/models/activity_pub/object/note.rb b/app/models/activity_pub/object/note.rb index 0f84c747..06b969ab 100644 --- a/app/models/activity_pub/object/note.rb +++ b/app/models/activity_pub/object/note.rb @@ -3,4 +3,8 @@ # = Note = # # Representa notas, el tipo más común de objeto del Fediverso. -class ActivityPub::Object::Note < ActivityPub::Object; end +class ActivityPub + module Object + class Note < ActivityPub::Object; end + end +end diff --git a/app/models/activity_pub/object/organization.rb b/app/models/activity_pub/object/organization.rb index a5327d10..31e5887d 100644 --- a/app/models/activity_pub/object/organization.rb +++ b/app/models/activity_pub/object/organization.rb @@ -3,4 +3,8 @@ # = Organization = # # Una organización -class ActivityPub::Object::Organization < ActivityPub::Object; end +class ActivityPub + module Object + class Organization < ActivityPub::Object; end + end +end diff --git a/app/models/activity_pub/object/person.rb b/app/models/activity_pub/object/person.rb index 98a1568d..fd01e515 100644 --- a/app/models/activity_pub/object/person.rb +++ b/app/models/activity_pub/object/person.rb @@ -3,4 +3,8 @@ # = Person = # # Una persona, el perfil de une actore -class ActivityPub::Object::Person < ActivityPub::Object; end +class ActivityPub + module Object + class Person < ActivityPub::Object; end + end +end diff --git a/app/models/social_inbox.rb b/app/models/social_inbox.rb index 624ee571..45b8afd8 100644 --- a/app/models/social_inbox.rb +++ b/app/models/social_inbox.rb @@ -69,8 +69,6 @@ class SocialInbox # # @return [String] def generate_uri(&block) - @public_key_url ||= URI("https://#{hostname}").tap do |uri| - yield uri - end.to_s + @public_key_url ||= URI("https://#{hostname}").tap(&block).to_s end end From 7b8730c34c2ca41e5f3f25bd77c15cd3cf1e572a Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 13:04:15 -0300 Subject: [PATCH 18/42] feat: las actividades se aprueban cuando las confirma la SI --- .../api/v1/webhooks/social_inbox_controller.rb | 6 +++++- app/models/activity_pub.rb | 9 ++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/v1/webhooks/social_inbox_controller.rb b/app/controllers/api/v1/webhooks/social_inbox_controller.rb index 3341b33d..76c7ec40 100644 --- a/app/controllers/api/v1/webhooks/social_inbox_controller.rb +++ b/app/controllers/api/v1/webhooks/social_inbox_controller.rb @@ -35,8 +35,12 @@ module Api end # Cuando aprobamos una actividad, recibimos la confirmación y - # cambiamos el estado + # cambiamos el estado. def onapproved + ActivityPub.transaction do + activity_pub.approve! if activity_pub.waiting? + end + head :accepted end diff --git a/app/models/activity_pub.rb b/app/models/activity_pub.rb index 2a127433..71d67722 100644 --- a/app/models/activity_pub.rb +++ b/app/models/activity_pub.rb @@ -21,6 +21,8 @@ class ActivityPub < ApplicationRecord aasm do # Todavía no hay una decisión sobre el objeto state :paused, initial: true + # Estamos esperando respuesta desde la Social Inbox + state :waiting # Le usuarie aprobó el objeto state :approved # Le usuarie rechazó el objeto @@ -44,7 +46,12 @@ class ActivityPub < ApplicationRecord # Si un objeto previamente aprobado fue actualizado, volvemos a # pausarlo. event :pause do - transitions from: :approved, to: :paused + transitions from: %i[waiting approved], to: :paused + end + + # La actividad se aprueba + event :approve do + transitions from: :waiting, to: :approved end end end From e733c45b63cfd901e82dd9565585b2cb2bcbea3b Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 13:06:06 -0300 Subject: [PATCH 19/42] fix: las actividades se pueden rechazar --- app/controllers/api/v1/webhooks/social_inbox_controller.rb | 4 ++++ app/models/activity_pub.rb | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/app/controllers/api/v1/webhooks/social_inbox_controller.rb b/app/controllers/api/v1/webhooks/social_inbox_controller.rb index 76c7ec40..20028708 100644 --- a/app/controllers/api/v1/webhooks/social_inbox_controller.rb +++ b/app/controllers/api/v1/webhooks/social_inbox_controller.rb @@ -47,6 +47,10 @@ module Api # Cuando rechazamos una actividad, recibimos la confirmación y # cambiamos el estado def onrejected + ActivityPub.transaction do + activity_pub.reject! if activity_pub.waiting? + end + head :accepted end diff --git a/app/models/activity_pub.rb b/app/models/activity_pub.rb index 71d67722..b8e8eded 100644 --- a/app/models/activity_pub.rb +++ b/app/models/activity_pub.rb @@ -53,5 +53,10 @@ class ActivityPub < ApplicationRecord event :approve do transitions from: :waiting, to: :approved end + + # La actividad fue rechazada + event :reject do + transitions from: :waiting, to: :rejected + end end end From a9bdabf409076494b97d237631d47e1bbe64e592 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 13:07:27 -0300 Subject: [PATCH 20/42] =?UTF-8?q?fix:=20se=20puede=20volver=20a=20pausar?= =?UTF-8?q?=20despu=C3=A9s=20de=20rechazarla?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/activity_pub.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/activity_pub.rb b/app/models/activity_pub.rb index b8e8eded..1c225226 100644 --- a/app/models/activity_pub.rb +++ b/app/models/activity_pub.rb @@ -46,7 +46,7 @@ class ActivityPub < ApplicationRecord # Si un objeto previamente aprobado fue actualizado, volvemos a # pausarlo. event :pause do - transitions from: %i[waiting approved], to: :paused + transitions from: %i[waiting approved rejected], to: :paused end # La actividad se aprueba From b477487b659e01e522d621584cb9b94fba676eb7 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 13:13:24 -0300 Subject: [PATCH 21/42] =?UTF-8?q?fixup!=20feat:=20obtener=20el=20contenido?= =?UTF-8?q?=20del=20objeto=20m=C3=A1s=20adelante?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/v1/webhooks/social_inbox_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/v1/webhooks/social_inbox_controller.rb b/app/controllers/api/v1/webhooks/social_inbox_controller.rb index 20028708..532cca5e 100644 --- a/app/controllers/api/v1/webhooks/social_inbox_controller.rb +++ b/app/controllers/api/v1/webhooks/social_inbox_controller.rb @@ -95,8 +95,9 @@ module Api if object_embedded? o.content = original_object else - ActivityPub::FetchJob.perform_later(site: site, object: object) + ActivityPub::FetchJob.perform_later(site: site, object: o) end + o.save! end end From 732012e14b8d54db01e274db2d9f53a223523898 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 13:13:34 -0300 Subject: [PATCH 22/42] docs: orden en que se crean los registros --- app/controllers/api/v1/webhooks/social_inbox_controller.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/controllers/api/v1/webhooks/social_inbox_controller.rb b/app/controllers/api/v1/webhooks/social_inbox_controller.rb index 532cca5e..51322990 100644 --- a/app/controllers/api/v1/webhooks/social_inbox_controller.rb +++ b/app/controllers/api/v1/webhooks/social_inbox_controller.rb @@ -24,6 +24,12 @@ module Api ActivityPub.transaction do # Crea todos los registros necesarios y actualiza el estado + # + # 1. Actor + # 2. Instance + # 3. Object + # 4. ActivityPub + # 5. Activity activity.update_activity_pub_state! end rescue ActiveRecord::RecordInvalid => e From 34e63ff2dca083306ada0023098a7937b373f19f Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 14:07:11 -0300 Subject: [PATCH 23/42] =?UTF-8?q?feat:=20actualizar=20el=20tipo=20tambi?= =?UTF-8?q?=C3=A9n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/jobs/activity_pub/fetch_job.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/jobs/activity_pub/fetch_job.rb b/app/jobs/activity_pub/fetch_job.rb index ff14c795..e5950c86 100644 --- a/app/jobs/activity_pub/fetch_job.rb +++ b/app/jobs/activity_pub/fetch_job.rb @@ -17,7 +17,9 @@ class ActivityPub return unless response.ok? return unless response.miss? - object.update(content: FastJsonparser.parse(response.body)) + content = FastJsonparser.parse(response.body) + + object.update(content: content, type: ActivityPub::Object.type_from(content).name) end end end From 03c0b71b5f36f14af1f8a9dbab1f9a97fb706d2f Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 15:42:56 -0300 Subject: [PATCH 24/42] fix: no es necesario vincular actores con objetos --- .../api/v1/webhooks/social_inbox_controller.rb | 3 +-- app/models/activity_pub/actor.rb | 1 - app/models/activity_pub/object.rb | 2 -- .../20240221184007_remove_actor_from_objects.rb | 12 ++++++++++++ 4 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 db/migrate/20240221184007_remove_actor_from_objects.rb diff --git a/app/controllers/api/v1/webhooks/social_inbox_controller.rb b/app/controllers/api/v1/webhooks/social_inbox_controller.rb index 51322990..c15a757d 100644 --- a/app/controllers/api/v1/webhooks/social_inbox_controller.rb +++ b/app/controllers/api/v1/webhooks/social_inbox_controller.rb @@ -96,8 +96,7 @@ module Api # # @return [ActivityPub::Object] def object - @object ||= ActivityPub::Object.type_from(original_object).find_or_initialize_by(actor: actor, - uri: object_uri).tap do |o| + @object ||= ActivityPub::Object.type_from(original_object).find_or_initialize_by(uri: object_uri).tap do |o| if object_embedded? o.content = original_object else diff --git a/app/models/activity_pub/actor.rb b/app/models/activity_pub/actor.rb index 7be69602..e79a596a 100644 --- a/app/models/activity_pub/actor.rb +++ b/app/models/activity_pub/actor.rb @@ -11,6 +11,5 @@ class ActivityPub belongs_to :instance has_many :activity_pubs, as: :object - has_many :objects end end diff --git a/app/models/activity_pub/object.rb b/app/models/activity_pub/object.rb index 49a06772..ec759e3e 100644 --- a/app/models/activity_pub/object.rb +++ b/app/models/activity_pub/object.rb @@ -7,7 +7,5 @@ class ActivityPub belongs_to :actor has_many :activity_pubs, as: :object - - validates :actor_id, presence: true end end diff --git a/db/migrate/20240221184007_remove_actor_from_objects.rb b/db/migrate/20240221184007_remove_actor_from_objects.rb new file mode 100644 index 00000000..6ee5822c --- /dev/null +++ b/db/migrate/20240221184007_remove_actor_from_objects.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# No es necesario vincular actores con objetos, porque la forma en que +# lo estábamos haciendo no se refiere a le actore del objeto, sino de +# acciones distintas sobre el mismo objeto, generado por une actore. +# +# Y ese valor ya lo podemos obtener desde attributedTo +class RemoveActorFromObjects < ActiveRecord::Migration[6.1] + def change + remove_column :activity_pub_objects, :actor_id, :uuid, index: true + end +end From 0443cb0fc3cce6d7e6eff11b97b9f5b5914939c1 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 15:43:10 -0300 Subject: [PATCH 25/42] fix: responder con forbidden cuando los registros no sean validos --- app/controllers/api/v1/webhooks/concerns/webhook_concern.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/api/v1/webhooks/concerns/webhook_concern.rb b/app/controllers/api/v1/webhooks/concerns/webhook_concern.rb index a546a55c..b94c91f6 100644 --- a/app/controllers/api/v1/webhooks/concerns/webhook_concern.rb +++ b/app/controllers/api/v1/webhooks/concerns/webhook_concern.rb @@ -11,6 +11,7 @@ module Api included do # Responde con forbidden si falla la validación del token rescue_from ActiveRecord::RecordNotFound, with: :platforms_answer + rescue_from ActiveRecord::RecordInvalid, with: :platforms_answer private From 5dbff20e2bf43df6833106c1f5f790a2399b38bc Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 15:44:04 -0300 Subject: [PATCH 26/42] fixup! chore: rubocop --- app/models/activity_pub/activity/create.rb | 2 +- app/models/activity_pub/activity/delete.rb | 2 +- app/models/activity_pub/activity/flag.rb | 2 +- app/models/activity_pub/activity/follow.rb | 2 +- app/models/activity_pub/activity/generic.rb | 2 +- app/models/activity_pub/activity/undo.rb | 2 +- app/models/activity_pub/activity/update.rb | 2 +- app/models/activity_pub/object/application.rb | 2 +- app/models/activity_pub/object/article.rb | 2 +- app/models/activity_pub/object/generic.rb | 2 +- app/models/activity_pub/object/note.rb | 2 +- app/models/activity_pub/object/organization.rb | 2 +- app/models/activity_pub/object/person.rb | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/models/activity_pub/activity/create.rb b/app/models/activity_pub/activity/create.rb index 4acafaf2..9cd32559 100644 --- a/app/models/activity_pub/activity/create.rb +++ b/app/models/activity_pub/activity/create.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class ActivityPub - module Activity + class Activity class Create < ActivityPub::Activity; end end end diff --git a/app/models/activity_pub/activity/delete.rb b/app/models/activity_pub/activity/delete.rb index 2973f730..7080375e 100644 --- a/app/models/activity_pub/activity/delete.rb +++ b/app/models/activity_pub/activity/delete.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class ActivityPub - module Activity + class Activity class Delete < ActivityPub::Activity # Si estamos eliminando el objeto, tenemos que vaciar su contenido y # cambiar el estado a borrado diff --git a/app/models/activity_pub/activity/flag.rb b/app/models/activity_pub/activity/flag.rb index 27bbe266..ffbc374b 100644 --- a/app/models/activity_pub/activity/flag.rb +++ b/app/models/activity_pub/activity/flag.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class ActivityPub - module Activity + class Activity class Flag < ActivityPub::Activity; end end end diff --git a/app/models/activity_pub/activity/follow.rb b/app/models/activity_pub/activity/follow.rb index 9e32b67d..e383490a 100644 --- a/app/models/activity_pub/activity/follow.rb +++ b/app/models/activity_pub/activity/follow.rb @@ -5,7 +5,7 @@ # Una actividad de seguimiento se refiere siempre a une actore (el # sitio) y proviene de otre actore. class ActivityPub - module Activity + class Activity class Follow < ActivityPub::Activity; end end end diff --git a/app/models/activity_pub/activity/generic.rb b/app/models/activity_pub/activity/generic.rb index 89c26247..95fff3eb 100644 --- a/app/models/activity_pub/activity/generic.rb +++ b/app/models/activity_pub/activity/generic.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class ActivityPub - module Activity + class Activity class Generic < ActivityPub::Activity; end end end diff --git a/app/models/activity_pub/activity/undo.rb b/app/models/activity_pub/activity/undo.rb index 98233af9..41fb5e51 100644 --- a/app/models/activity_pub/activity/undo.rb +++ b/app/models/activity_pub/activity/undo.rb @@ -5,7 +5,7 @@ # Deshace una actividad, dependiendo de la actividad a la que se # refiere. class ActivityPub - module Activity + class Activity class Undo < ActivityPub::Activity end end diff --git a/app/models/activity_pub/activity/update.rb b/app/models/activity_pub/activity/update.rb index 50622b93..19c95b68 100644 --- a/app/models/activity_pub/activity/update.rb +++ b/app/models/activity_pub/activity/update.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class ActivityPub - module Activity + class Activity class Update < ActivityPub::Activity # Si estamos actualizando el objeto, tenemos que devolverlo a estado # de moderación diff --git a/app/models/activity_pub/object/application.rb b/app/models/activity_pub/object/application.rb index e8d8fa0e..99ac935c 100644 --- a/app/models/activity_pub/object/application.rb +++ b/app/models/activity_pub/object/application.rb @@ -4,7 +4,7 @@ # # Una aplicación o instancia class ActivityPub - module Object + class Object class Application < ActivityPub::Object; end end end diff --git a/app/models/activity_pub/object/article.rb b/app/models/activity_pub/object/article.rb index 69fe371c..126ba3f1 100644 --- a/app/models/activity_pub/object/article.rb +++ b/app/models/activity_pub/object/article.rb @@ -4,7 +4,7 @@ # # Representa artículos class ActivityPub - module Object + class Object class Article < ActivityPub::Object; end end end diff --git a/app/models/activity_pub/object/generic.rb b/app/models/activity_pub/object/generic.rb index c16b25c6..3e5ff719 100644 --- a/app/models/activity_pub/object/generic.rb +++ b/app/models/activity_pub/object/generic.rb @@ -2,7 +2,7 @@ # = Generic = class ActivityPub - module Object + class Object class Generic < ActivityPub::Object; end end end diff --git a/app/models/activity_pub/object/note.rb b/app/models/activity_pub/object/note.rb index 06b969ab..ca113c15 100644 --- a/app/models/activity_pub/object/note.rb +++ b/app/models/activity_pub/object/note.rb @@ -4,7 +4,7 @@ # # Representa notas, el tipo más común de objeto del Fediverso. class ActivityPub - module Object + class Object class Note < ActivityPub::Object; end end end diff --git a/app/models/activity_pub/object/organization.rb b/app/models/activity_pub/object/organization.rb index 31e5887d..e3385232 100644 --- a/app/models/activity_pub/object/organization.rb +++ b/app/models/activity_pub/object/organization.rb @@ -4,7 +4,7 @@ # # Una organización class ActivityPub - module Object + class Object class Organization < ActivityPub::Object; end end end diff --git a/app/models/activity_pub/object/person.rb b/app/models/activity_pub/object/person.rb index fd01e515..a6a85d43 100644 --- a/app/models/activity_pub/object/person.rb +++ b/app/models/activity_pub/object/person.rb @@ -4,7 +4,7 @@ # # Una persona, el perfil de une actore class ActivityPub - module Object + class Object class Person < ActivityPub::Object; end end end From ddaee30da715981c9871728706450050ecbfd091 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 15:44:16 -0300 Subject: [PATCH 27/42] fix: primero guardar el objeto antes de obtener su contenido --- .../api/v1/webhooks/social_inbox_controller.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/controllers/api/v1/webhooks/social_inbox_controller.rb b/app/controllers/api/v1/webhooks/social_inbox_controller.rb index c15a757d..24865c92 100644 --- a/app/controllers/api/v1/webhooks/social_inbox_controller.rb +++ b/app/controllers/api/v1/webhooks/social_inbox_controller.rb @@ -97,13 +97,13 @@ module Api # @return [ActivityPub::Object] def object @object ||= ActivityPub::Object.type_from(original_object).find_or_initialize_by(uri: object_uri).tap do |o| - if object_embedded? - o.content = original_object - else - ActivityPub::FetchJob.perform_later(site: site, object: o) - end + o.content = original_object if object_embedded? o.save! + + # XXX: el objeto necesita ser guardado antes de poder + # procesarlo + ActivityPub::FetchJob.perform_later(site: site, object: o) unless object_embedded? end end From d98f893a7153d5fe546c6db5632356733e2ec4eb Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 15:44:29 -0300 Subject: [PATCH 28/42] fix: algunas actividades no tienen destinatarie --- app/controllers/api/v1/webhooks/social_inbox_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/api/v1/webhooks/social_inbox_controller.rb b/app/controllers/api/v1/webhooks/social_inbox_controller.rb index 24865c92..fb7ede50 100644 --- a/app/controllers/api/v1/webhooks/social_inbox_controller.rb +++ b/app/controllers/api/v1/webhooks/social_inbox_controller.rb @@ -149,7 +149,6 @@ module Api raise '@context missing' unless activity[:@context].presence raise 'id missing' unless activity[:id].presence raise 'object missing' unless activity[:object].presence - raise 'not for us' unless [activity[:to]].flatten.include?(site.social_inbox.actor_id) rescue RuntimeError => e raise ActiveRecord::RecordNotFound, e.message end From 39bbc3a2bd26c7240a933e4769e5c642cea7fff7 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 15:44:41 -0300 Subject: [PATCH 29/42] =?UTF-8?q?fix:=20deleted=20es=20un=20m=C3=A9todo=20?= =?UTF-8?q?reservado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/activity_pub.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/activity_pub.rb b/app/models/activity_pub.rb index 1c225226..42ed3f61 100644 --- a/app/models/activity_pub.rb +++ b/app/models/activity_pub.rb @@ -30,13 +30,13 @@ class ActivityPub < ApplicationRecord # Le usuarie reportó el objeto state :reported # Le actore eliminó el objeto - state :deleted + state :removed # Recibir una acción de eliminación, eliminar el contenido de la # base de datos. Esto elimina el contenido para todos los sitios # porque estamos respetando lo que pidió le actore. - event :delete do - transitions to: :deleted + event :remove do + transitions to: :removed after do object.update(object: {}) From f517889992e9f72fb517ab9c79afed20c80e2769 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 15:45:15 -0300 Subject: [PATCH 30/42] fixup! fix: no es necesario vincular actores con objetos --- db/structure.sql | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/db/structure.sql b/db/structure.sql index 723c9e99..ee99e791 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -523,7 +523,6 @@ CREATE TABLE public.activity_pub_objects ( id uuid DEFAULT gen_random_uuid() NOT NULL, created_at timestamp(6) without time zone NOT NULL, updated_at timestamp(6) without time zone NOT NULL, - actor_id uuid NOT NULL, type character varying NOT NULL, uri character varying NOT NULL, content jsonb DEFAULT '{}'::jsonb @@ -2005,13 +2004,6 @@ CREATE INDEX index_activity_pub_actors_on_uri ON public.activity_pub_actors USIN CREATE INDEX index_activity_pub_instances_on_hostname ON public.activity_pub_instances USING btree (hostname); --- --- Name: index_activity_pub_objects_on_actor_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX index_activity_pub_objects_on_actor_id ON public.activity_pub_objects USING btree (actor_id); - - -- -- Name: index_activity_pubs_on_site_id_and_object_id_and_object_type; Type: INDEX; Schema: public; Owner: - -- @@ -2479,6 +2471,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20240219175839'), ('20240219204011'), ('20240219204224'), -('20240220161414'); +('20240220161414'), +('20240221184007'); From f8f0eb9d3c341467ec25b9b48d242d29c36b3401 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 16:42:02 -0300 Subject: [PATCH 31/42] fix: no buscar csrf --- app/controllers/api/v1/webhooks/concerns/webhook_concern.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/api/v1/webhooks/concerns/webhook_concern.rb b/app/controllers/api/v1/webhooks/concerns/webhook_concern.rb index b94c91f6..aef2dd83 100644 --- a/app/controllers/api/v1/webhooks/concerns/webhook_concern.rb +++ b/app/controllers/api/v1/webhooks/concerns/webhook_concern.rb @@ -9,6 +9,8 @@ module Api extend ActiveSupport::Concern included do + skip_before_action :verify_authenticity_token + # Responde con forbidden si falla la validación del token rescue_from ActiveRecord::RecordNotFound, with: :platforms_answer rescue_from ActiveRecord::RecordInvalid, with: :platforms_answer From a4133c60018772af78b5bb5813050b4d1fbf5b34 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 16:42:14 -0300 Subject: [PATCH 32/42] fix: asegurarse que todos los registros existan --- .../api/v1/webhooks/social_inbox_controller.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/controllers/api/v1/webhooks/social_inbox_controller.rb b/app/controllers/api/v1/webhooks/social_inbox_controller.rb index fb7ede50..e6b80c4b 100644 --- a/app/controllers/api/v1/webhooks/social_inbox_controller.rb +++ b/app/controllers/api/v1/webhooks/social_inbox_controller.rb @@ -24,12 +24,10 @@ module Api ActivityPub.transaction do # Crea todos los registros necesarios y actualiza el estado - # - # 1. Actor - # 2. Instance - # 3. Object - # 4. ActivityPub - # 5. Activity + actor.present? + instance.present? + object.present? + activity_pub.present? activity.update_activity_pub_state! end rescue ActiveRecord::RecordInvalid => e From 3292d7ebe3fc8f82fe817bc9ccd5d0be0074a171 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 16:42:28 -0300 Subject: [PATCH 33/42] fix: vaciar el contenido antes de cambiar de estado --- app/models/activity_pub.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/activity_pub.rb b/app/models/activity_pub.rb index 42ed3f61..9445717f 100644 --- a/app/models/activity_pub.rb +++ b/app/models/activity_pub.rb @@ -38,8 +38,8 @@ class ActivityPub < ApplicationRecord event :remove do transitions to: :removed - after do - object.update(object: {}) + before do + object.update(content: {}) end end From a13f42c22f39f00835a6a5862e750a00a61be62f Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 16:43:31 -0300 Subject: [PATCH 34/42] fixup! fixup! fix: no es necesario vincular actores con objetos --- app/models/activity_pub/object.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/activity_pub/object.rb b/app/models/activity_pub/object.rb index ec759e3e..898d5375 100644 --- a/app/models/activity_pub/object.rb +++ b/app/models/activity_pub/object.rb @@ -5,7 +5,6 @@ class ActivityPub class Object < ApplicationRecord include ActivityPub::Concerns::JsonLdConcern - belongs_to :actor has_many :activity_pubs, as: :object end end From b562b006bc716200898524a027ac67efb24d9080 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 17:03:59 -0300 Subject: [PATCH 35/42] =?UTF-8?q?fix:=20versi=C3=B3n=20requerida=20por=20a?= =?UTF-8?q?asm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gemfile | 2 +- Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 8e955e8f..676b3673 100644 --- a/Gemfile +++ b/Gemfile @@ -80,7 +80,7 @@ gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git' gem 'kaminari' gem 'device_detector' -gem 'after_commit_everywhere' +gem 'after_commit_everywhere', '~> 1.0' gem 'aasm' # database diff --git a/Gemfile.lock b/Gemfile.lock index 50745140..7aac4c12 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -608,7 +608,7 @@ PLATFORMS DEPENDENCIES aasm - after_commit_everywhere + after_commit_everywhere (~> 1.0) bcrypt (~> 3.1.7) bcrypt_pbkdf blazer From e53f31f359617b90cd77921c4e67f2521daed337 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 17:04:39 -0300 Subject: [PATCH 36/42] =?UTF-8?q?fixup!=20fix:=20deleted=20es=20un=20m?= =?UTF-8?q?=C3=A9todo=20reservado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/activity_pub.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/activity_pub.rb b/app/models/activity_pub.rb index 9445717f..902924b6 100644 --- a/app/models/activity_pub.rb +++ b/app/models/activity_pub.rb @@ -16,7 +16,7 @@ class ActivityPub < ApplicationRecord validates :site_id, presence: true validates :object_id, presence: true - validates :aasm_state, presence: true, inclusion: { in: %w[paused approved rejected reported deleted] } + validates :aasm_state, presence: true, inclusion: { in: %w[paused approved rejected reported removed] } aasm do # Todavía no hay una decisión sobre el objeto From fc7c2e5b7455b8e91d526a366ac0f57bce8c6c2e Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 17:05:17 -0300 Subject: [PATCH 37/42] fix: undo auto-cancela la actividad --- app/models/activity_pub.rb | 4 +++- app/models/activity_pub/activity/undo.rb | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/models/activity_pub.rb b/app/models/activity_pub.rb index 902924b6..7e5bcabe 100644 --- a/app/models/activity_pub.rb +++ b/app/models/activity_pub.rb @@ -38,8 +38,10 @@ class ActivityPub < ApplicationRecord event :remove do transitions to: :removed + # @todo Es posible que haya un ActivityPub::FetchJob pendiente + # cuando estamos haciendo esto, que rellene el objeto después. before do - object.update(content: {}) + object.update(content: {}) unless object.content.empty? end end diff --git a/app/models/activity_pub/activity/undo.rb b/app/models/activity_pub/activity/undo.rb index 41fb5e51..4b6a5a4c 100644 --- a/app/models/activity_pub/activity/undo.rb +++ b/app/models/activity_pub/activity/undo.rb @@ -7,6 +7,18 @@ class ActivityPub class Activity class Undo < ActivityPub::Activity + # Una actividad de deshacer tiene anidada como objeto la actividad + # a deshacer. Para respetar la voluntad de le actore remote, + # tendríamos que eliminar cualquier actividad pendiente sobre el + # objeto. + # + # Sin embargo, estas acciones nunca deberían llegar a nuestra + # Inbox. + # + # @see {https://github.com/hyphacoop/social.distributed.press/issues/43} + def update_activity_pub_state! + activity_pub.remove! + end end end end From dca266714d520cfb0fbd36460c7a898573fac33a Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 17:08:11 -0300 Subject: [PATCH 38/42] fix: no completar el contenido si el objeto tiene actividades canceladas --- app/jobs/activity_pub/fetch_job.rb | 2 ++ app/models/activity_pub.rb | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/jobs/activity_pub/fetch_job.rb b/app/jobs/activity_pub/fetch_job.rb index e5950c86..ec8c29f7 100644 --- a/app/jobs/activity_pub/fetch_job.rb +++ b/app/jobs/activity_pub/fetch_job.rb @@ -11,6 +11,8 @@ class ActivityPub class FetchJob < ApplicationJob def perform(site:, object:) ActivityPub::Object.transaction do + return if object.activity_pubs.where(aasm_state: 'removed').count.positive? + response = site.social_inbox.dereferencer.get(uri: object.uri) # @todo Fallar cuando la respuesta no funcione? diff --git a/app/models/activity_pub.rb b/app/models/activity_pub.rb index 7e5bcabe..df8e5c5c 100644 --- a/app/models/activity_pub.rb +++ b/app/models/activity_pub.rb @@ -38,8 +38,6 @@ class ActivityPub < ApplicationRecord event :remove do transitions to: :removed - # @todo Es posible que haya un ActivityPub::FetchJob pendiente - # cuando estamos haciendo esto, que rellene el objeto después. before do object.update(content: {}) unless object.content.empty? end From 2ae2b8e9e87b254cc041bda6c060a7b93d171592 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 17:09:15 -0300 Subject: [PATCH 39/42] =?UTF-8?q?fix:=20las=20actividades=20de=20borrado?= =?UTF-8?q?=20eliminan=20el=20contenido=20tambi=C3=A9n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/activity_pub/activity/delete.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/activity_pub/activity/delete.rb b/app/models/activity_pub/activity/delete.rb index 7080375e..351dd3cb 100644 --- a/app/models/activity_pub/activity/delete.rb +++ b/app/models/activity_pub/activity/delete.rb @@ -4,9 +4,9 @@ class ActivityPub class Activity class Delete < ActivityPub::Activity # Si estamos eliminando el objeto, tenemos que vaciar su contenido y - # cambiar el estado a borrado + # cambiar el estado a borrado. def update_activity_pub_state! - activity_pub.deleted! + activity_pub.remove! end end end From 7a936c114314ae4fbcc5827cc1aa019dad02bd2e Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 17:22:31 -0300 Subject: [PATCH 40/42] =?UTF-8?q?fix:=20no=20actualizar=20si=20ya=20estaba?= =?UTF-8?q?=20cacheado=20y=20el=20contenido=20exist=C3=ADa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/jobs/activity_pub/fetch_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/activity_pub/fetch_job.rb b/app/jobs/activity_pub/fetch_job.rb index ec8c29f7..b6c45026 100644 --- a/app/jobs/activity_pub/fetch_job.rb +++ b/app/jobs/activity_pub/fetch_job.rb @@ -17,7 +17,7 @@ class ActivityPub # @todo Fallar cuando la respuesta no funcione? return unless response.ok? - return unless response.miss? + return if response.miss? && object.content.present? content = FastJsonparser.parse(response.body) From e62d6c6c1d5e03d3b4b6c9c47feefc5d2b186833 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 17:50:06 -0300 Subject: [PATCH 41/42] fix: asignar un tipo por defecto para el objeto --- app/controllers/api/v1/webhooks/social_inbox_controller.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/v1/webhooks/social_inbox_controller.rb b/app/controllers/api/v1/webhooks/social_inbox_controller.rb index e6b80c4b..c71c4922 100644 --- a/app/controllers/api/v1/webhooks/social_inbox_controller.rb +++ b/app/controllers/api/v1/webhooks/social_inbox_controller.rb @@ -94,7 +94,10 @@ module Api # # @return [ActivityPub::Object] def object - @object ||= ActivityPub::Object.type_from(original_object).find_or_initialize_by(uri: object_uri).tap do |o| + @object ||= ActivityPub::Object.find_or_initialize_by(uri: object_uri).tap do |o| + # XXX: Si el objeto es una actividad, esto siempre va a ser + # Generic + o.type ||= 'ActivityPub::Object::Generic' o.content = original_object if object_embedded? o.save! From 6b12e5141cd1f4004b53573d39d934f2c205d24b Mon Sep 17 00:00:00 2001 From: f Date: Wed, 21 Feb 2024 17:54:18 -0300 Subject: [PATCH 42/42] feat: al deshacer una actividad resolvemos la pendiente --- app/models/activity_pub/activity/undo.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/models/activity_pub/activity/undo.rb b/app/models/activity_pub/activity/undo.rb index 4b6a5a4c..18fbff5e 100644 --- a/app/models/activity_pub/activity/undo.rb +++ b/app/models/activity_pub/activity/undo.rb @@ -17,7 +17,10 @@ class ActivityPub # # @see {https://github.com/hyphacoop/social.distributed.press/issues/43} def update_activity_pub_state! - activity_pub.remove! + ActivityPub.transaction do + ActivityPub::Activity.find_by(uri: content['object'])&.activity_pub&.remove! + activity_pub.remove! + end end end end