diff --git a/app/controllers/actor_moderations_controller.rb b/app/controllers/actor_moderations_controller.rb index cb7a63a9..cd81e441 100644 --- a/app/controllers/actor_moderations_controller.rb +++ b/app/controllers/actor_moderations_controller.rb @@ -64,7 +64,7 @@ class ActorModerationsController < ApplicationController end end - message = actor_moderation.public_send(method) ? :success : :error + message = actor_moderations.public_send(method) ? :success : :error flash[message] = I18n.t("actor_moderations.action_on_several.#{message}") end diff --git a/app/jobs/activity_pub/inbox_job.rb b/app/jobs/activity_pub/inbox_job.rb new file mode 100644 index 00000000..93216d44 --- /dev/null +++ b/app/jobs/activity_pub/inbox_job.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class ActivityPub + class InboxJob < ApplicationJob + self.priority = 10 + + # @param :site [Site] + # @param :activity [String] + # @param :action [Symbol] + def perform(site:, activity:, action:) + response = site.social_inbox.inbox.public_send(action, id: activity) + + raise response.body unless response.ok? + end + end +end diff --git a/app/jobs/activity_pub/instance_moderation_job.rb b/app/jobs/activity_pub/instance_moderation_job.rb index 214f8dd4..00100abf 100644 --- a/app/jobs/activity_pub/instance_moderation_job.rb +++ b/app/jobs/activity_pub/instance_moderation_job.rb @@ -30,6 +30,8 @@ class ActivityPub else scope.block_all_without_callbacks! end + + ActivityPub::SyncListsJob.perform_later(site: site) end end end diff --git a/app/jobs/activity_pub/sync_lists_job.rb b/app/jobs/activity_pub/sync_lists_job.rb new file mode 100644 index 00000000..39f6bdc9 --- /dev/null +++ b/app/jobs/activity_pub/sync_lists_job.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +class ActivityPub + # Sincroniza las listas de bloqueo y permitidas con el estado actual + # de la base de datos. + class SyncListsJob < ApplicationJob + # Siempre correr al final + self.priority = 100 + + attr_reader :logs + + # Ejecuta todas las requests y consolida los posibles errores. + # + # @param site [Site] + def run(site:) + @logs = {} + + instance_scope = site.instance_moderations.joins(:instance) + actor_scope = site.actor_moderations.joins(:actor) + + blocklist = wildcardize(instance_scope.blocked.pluck(:hostname)) + actor_scope.blocked.distinct.pluck(:mention).compact + allowlist = wildcardize(instance_scope.allowed.pluck(:hostname)) + actor_scope.allowed.distinct.pluck(:mention).compact + pauselist = wildcardize(instance_scope.paused.pluck(:hostname)) + actor_scope.paused.distinct.pluck(:mention).compact + + if blocklist.present? + Rails.logger.info "Bloqueando: #{blocklist.join(', ')}" + process(:blocked) { site.social_inbox.allowlist.delete(list: blocklist) } + process(:blocked) { site.social_inbox.blocklist.post(list: blocklist) } + end + + if allowlist.present? + Rails.logger.info "Permitiendo: #{allowlist.join(', ')}" + process(:allowed) { site.social_inbox.blocklist.delete(list: allowlist) } + process(:allowed) { site.social_inbox.allowlist.post(list: allowlist) } + end + + if pauselist.present? + Rails.logger.info "Pausando: #{pauselist.join(', ')}" + process(:paused) { site.social_inbox.blocklist.delete(list: pauselist) } + process(:paused) { site.social_inbox.allowlist.delete(list: pauselist) } + end + + # Si alguna falló, reintentar + raise if logs.present? + rescue Exception => e + ExceptionNotifier.notify_exception(e, data: { site: site.name, logs: logs, blocklist: blocklist, allowlist: allowlist, pauselist: pauselist }) + + raise + end + + private + + def process(stage) + response = yield + + return if response.ok? + + logs[stage] ||= [] + logs[stage] << { body: response.body, code: response.code } + end + + # @params hostnames [Array] + # @return [Array] + def wildcardize(hostnames) + hostnames.map do |hostname| + "@*@#{hostname}" + end + end + end +end diff --git a/app/models/activity_pub.rb b/app/models/activity_pub.rb index 8142ecfb..55b48cd7 100644 --- a/app/models/activity_pub.rb +++ b/app/models/activity_pub.rb @@ -47,9 +47,9 @@ class ActivityPub < ApplicationRecord # Todavía no hay una decisión sobre el objeto state :paused, initial: true # Le usuarie aprobó el objeto - state :approved, before_enter: :allow_remotely! + state :approved # Le usuarie rechazó el objeto - state :rejected, before_enter: :reject_remotely! + state :rejected # Le usuarie reportó el objeto state :reported # Le actore eliminó el objeto @@ -84,11 +84,19 @@ class ActivityPub < ApplicationRecord # webhook a modo de confirmación. event :approve do transitions from: %i[paused], to: :approved + + after do + ActivityPub::InboxJob.perform_later(site: site, activity: activities.first.uri, action: :accept) + end end # La actividad fue rechazada event :reject do transitions from: %i[paused], to: :rejected + + after do + ActivityPub::InboxJob.perform_later(site: site, activity: activities.first.uri, action: :reject) + end end # Solo podemos reportarla luego de rechazarla @@ -103,15 +111,4 @@ class ActivityPub < ApplicationRecord # Definir eventos en masa include AasmEventsConcern - - # Lo que tenemos que aprobar o rechazar es la última actividad - # disponible, que según el scope por defecto, va a ser la primera de - # la lista. - def reject_remotely! - raise unless site.social_inbox.inbox.reject(id: activities.first.uri).ok? - end - - def allow_remotely! - raise unless site.social_inbox.inbox.accept(id: activities.first.uri).ok? - end end diff --git a/app/models/actor_moderation.rb b/app/models/actor_moderation.rb index c566b844..18149be4 100644 --- a/app/models/actor_moderation.rb +++ b/app/models/actor_moderation.rb @@ -14,9 +14,9 @@ class ActorModeration < ApplicationRecord accepts_nested_attributes_for :remote_flag aasm do - state :paused, initial: true, before_enter: :pause_remotely! - state :allowed, before_enter: :allow_remotely! - state :blocked, before_enter: :block_remotely! + state :paused, initial: true + state :allowed + state :blocked state :reported state :removed @@ -25,19 +25,19 @@ class ActorModeration < ApplicationRecord end event :pause do - transitions from: %i[allowed blocked reported], to: :paused + transitions from: %i[allowed blocked reported], to: :paused, after: :synchronize! end # Al permitir una cuenta no se permiten todos los comentarios # pendientes de moderación que ya hizo. event :allow do - transitions from: %i[paused blocked reported], to: :allowed + transitions from: %i[paused blocked reported], to: :allowed, after: :synchronize! end # Al bloquear una cuenta no se bloquean todos los comentarios # pendientes de moderación que hizo. event :block do - transitions from: %i[paused allowed], to: :blocked + transitions from: %i[paused allowed], to: :blocked, after: :synchronize! end # Al reportar, necesitamos asociar una RemoteFlag para poder @@ -64,24 +64,7 @@ class ActorModeration < ApplicationRecord # Definir eventos en masa include AasmEventsConcern - def pause_remotely! - raise unless - actor.mention && - site.social_inbox.allowlist.delete(list: [actor.mention]).ok? && - site.social_inbox.blocklist.delete(list: [actor.mention]).ok? - end - - def allow_remotely! - raise unless - actor.mention && - site.social_inbox.allowlist.post(list: [actor.mention]).ok? && - site.social_inbox.blocklist.delete(list: [actor.mention]).ok? - end - - def block_remotely! - raise unless - actor.mention && - site.social_inbox.allowlist.delete(list: [actor.mention]).ok? && - site.social_inbox.blocklist.post(list: [actor.mention]).ok? + def synchronize! + ActivityPub::SyncListsJob.perform_later(site: site) end end diff --git a/app/models/fediblock_state.rb b/app/models/fediblock_state.rb index a6faa14f..82912f76 100644 --- a/app/models/fediblock_state.rb +++ b/app/models/fediblock_state.rb @@ -20,8 +20,8 @@ class FediblockState < ApplicationRecord # Aunque queramos las listas habilitadas por defecto, tenemos que # habilitarlas luego de crearlas para poder generar la lista de # bloqueo en la Social Inbox. - state :disabled, initial: true, before_enter: :disable_remotely_and_pause_instances! - state :enabled, before_enter: :enable_remotely_and_block_instances! + state :disabled, initial: true, before_enter: :pause_unique_instances! + state :enabled, before_enter: :block_instances! error_on_all_events do |e| ExceptionNotifier.notify_exception(e, data: { site: site.name, fediblock: id }) @@ -38,32 +38,27 @@ class FediblockState < ApplicationRecord # pero esto implica que tenemos que encontrar las que sí y quitarlas # de list_names event :disable do - transitions from: :enabled, to: :disabled + transitions from: :enabled, to: :disabled, after: :synchronize! end end private - def enable_remotely_and_block_instances! - # Bloquear todos las instancias de este Fediblock - enable_remotely! list_names(fediblock.hostnames) - - # Luego esta tarea crea las que falten e ignora las que ya se - # bloquearon. - ActivityPub::InstanceModerationJob.perform_now(site: site, hostnames: fediblock.hostnames, perform_remotely: false) + def block_instances! + ActivityPub::InstanceModerationJob.perform_later(site: site, hostnames: fediblock.hostnames, perform_remotely: false) end - def disable_remotely_and_pause_instances! - # Deshabilitar todas las instancias que no estén habilitadas por - # otros fediblocks - disable_remotely! list_names(unique_hostnames) - - # Pausar todas las moderaciones de las instancias que no estén - # bloqueadas por otros fediblocks. + # Pausar todas las moderaciones de las instancias que no estén + # bloqueadas por otros fediblocks. + def pause_unique_instances! instance_ids = ActivityPub::Instance.where(hostname: unique_hostnames).ids site.instance_moderations.where(instance_id: instance_ids).pause_all_without_callbacks! end + def synchronize! + ActivityPub::SyncListsJob.perform_later(site: site) + end + # Devuelve los hostnames únicos a esta instancia. # # @return [Array] @@ -82,30 +77,4 @@ class FediblockState < ApplicationRecord fediblock.hostnames - other_enabled_hostnames end end - - # @param hostnames [Array] - # @return [Array] - def list_names(hostnames) - hostnames.map do |hostname| - "@*@#{hostname}" - end - end - - # Al deshabilitar, las instancias pasan a ser analizadas caso por caso - # - # @param list [Array] - def disable_remotely!(list) - raise unless - site.social_inbox.blocklist.delete(list: list).ok? && - site.social_inbox.allowlist.delete(list: list).ok? - end - - # Al habilitar, se bloquean todas las instancias de la lista - # - # @param list [Array] - def enable_remotely!(list) - raise unless - site.social_inbox.blocklist.post(list: list).ok? && - site.social_inbox.allowlist.delete(list: list).ok? - end end diff --git a/app/models/instance_moderation.rb b/app/models/instance_moderation.rb index 7c76262e..5a1a5ed6 100644 --- a/app/models/instance_moderation.rb +++ b/app/models/instance_moderation.rb @@ -11,14 +11,18 @@ class InstanceModeration < ApplicationRecord belongs_to :instance, class_name: 'ActivityPub::Instance' aasm do - state :paused, initial: true, before_enter: :pause_remotely! - state :allowed, before_enter: :allow_remotely! - state :blocked, before_enter: :block_remotely! + state :paused, initial: true + state :allowed + state :blocked error_on_all_events do |e| ExceptionNotifier.notify_exception(e, data: { site: site.name, instance: instance.hostname, instance_moderation: id }) end + after_all_events do + ActivityPub::SyncListsJob.perform_later(site: site) + end + # Al volver la instancia a pausa no cambiamos el estado de # moderación de actores pre-existente. event :pause do @@ -40,36 +44,4 @@ class InstanceModeration < ApplicationRecord # Definir eventos en masa include AasmEventsConcern - - # @return [Array] - def actor_ids - ActivityPub::Actor.where(instance_id: instance_id).ids - end - - # Elimina la instancia de todas las listas - # - # @return [Boolean] - def pause_remotely! - raise unless - site.social_inbox.blocklist.delete(list: [instance.list_name]).ok? && - site.social_inbox.allowlist.delete(list: [instance.list_name]).ok? - end - - # Deja de permitir la instancia - # - # @return [Boolean] - def block_remotely! - raise unless - site.social_inbox.allowlist.delete(list: [instance.list_name]).ok? && - site.social_inbox.blocklist.post(list: [instance.list_name]).ok? - end - - # Permite la instancia - # - # @return [Boolean] - def allow_remotely! - raise unless - site.social_inbox.blocklist.delete(list: [instance.list_name]).ok? && - site.social_inbox.allowlist.post(list: [instance.list_name]).ok? - end end diff --git a/config/locales/es.yml b/config/locales/es.yml index dd85c79d..75f024b8 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -109,7 +109,7 @@ es: instances_btn_box: text_pause: Moderar caso por caso text_allow: Permitir todo - text_deny: Bloquear instancia + text_block: Bloquear instancia profiles_btn_box: text_pause: Revisar siempre text_allow: Aprobar siempre