2024-02-20 20:13:42 +00:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
module Api
|
|
|
|
module V1
|
|
|
|
module Webhooks
|
|
|
|
# Recibe webhooks de la Social Inbox
|
|
|
|
#
|
2024-02-28 20:52:24 +00:00
|
|
|
# @todo Mover todo a un Job que obtenga el objeto remoto antes de
|
|
|
|
# instanciar el objeto localmente en lugar de arreglarlo después y
|
|
|
|
# poder responder lo más rápido posible el webhook.
|
2024-02-20 20:13:42 +00:00
|
|
|
# @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
|
2024-02-20 20:15:43 +00:00
|
|
|
# Devuelve un error si el token no es válido
|
|
|
|
usuarie.present?
|
|
|
|
|
2024-03-01 19:47:00 +00:00
|
|
|
::ActivityPub.transaction do
|
|
|
|
|
2024-02-20 20:15:43 +00:00
|
|
|
# Crea todos los registros necesarios y actualiza el estado
|
2024-02-21 19:42:14 +00:00
|
|
|
actor.present?
|
|
|
|
instance.present?
|
|
|
|
object.present?
|
|
|
|
activity_pub.present?
|
2024-02-28 20:52:24 +00:00
|
|
|
|
2024-02-20 20:15:43 +00:00
|
|
|
activity.update_activity_pub_state!
|
|
|
|
end
|
|
|
|
rescue ActiveRecord::RecordInvalid => e
|
2024-02-21 15:46:38 +00:00
|
|
|
ExceptionNotifier.notify_exception(e,
|
|
|
|
data: { site: site.name, usuarie: usuarie.email,
|
|
|
|
activity: original_activity })
|
2024-02-20 20:15:43 +00:00
|
|
|
ensure
|
2024-02-20 20:13:42 +00:00
|
|
|
head :accepted
|
|
|
|
end
|
|
|
|
|
2024-02-22 21:42:20 +00:00
|
|
|
# Cuando la Social Inbox acepta una actividad, la recibimos
|
|
|
|
# igual y la guardamos por si cambiamos de idea.
|
|
|
|
#
|
|
|
|
# @todo DRY
|
2024-02-20 20:13:42 +00:00
|
|
|
def onapproved
|
2024-03-01 19:47:00 +00:00
|
|
|
::ActivityPub.transaction do
|
2024-02-22 21:42:20 +00:00
|
|
|
actor.present?
|
|
|
|
instance.present?
|
|
|
|
object.present?
|
|
|
|
activity.present?
|
|
|
|
activity_pub.approve!
|
2024-02-21 16:04:15 +00:00
|
|
|
end
|
|
|
|
|
2024-02-20 20:13:42 +00:00
|
|
|
head :accepted
|
|
|
|
end
|
|
|
|
|
2024-02-22 21:42:20 +00:00
|
|
|
# Cuando la Social Inbox rechaza una actividad, la recibimos
|
|
|
|
# igual y la guardamos por si cambiamos de idea.
|
|
|
|
#
|
|
|
|
# @todo DRY
|
2024-02-20 20:13:42 +00:00
|
|
|
def onrejected
|
2024-03-01 19:47:00 +00:00
|
|
|
::ActivityPub.transaction do
|
2024-02-22 21:42:20 +00:00
|
|
|
actor.present?
|
|
|
|
instance.present?
|
|
|
|
object.present?
|
|
|
|
activity.present?
|
|
|
|
activity_pub.reject!
|
2024-02-21 16:06:06 +00:00
|
|
|
end
|
|
|
|
|
2024-02-20 20:13:42 +00:00
|
|
|
head :accepted
|
|
|
|
end
|
2024-02-20 20:15:43 +00:00
|
|
|
|
|
|
|
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 ||=
|
2024-02-21 15:46:38 +00:00
|
|
|
case original_activity[:object]
|
|
|
|
when String then original_activity[:object]
|
|
|
|
when Hash then original_activity.dig(:object, :id)
|
2024-02-20 20:15:43 +00:00
|
|
|
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
|
2024-02-21 15:30:01 +00:00
|
|
|
# existe, actualiza su contenido. Si el objeto no viene
|
|
|
|
# incorporado, obtenemos el contenido más tarde.
|
2024-02-20 20:15:43 +00:00
|
|
|
#
|
|
|
|
# @return [ActivityPub::Object]
|
|
|
|
def object
|
2024-03-01 19:47:00 +00:00
|
|
|
@object ||= ::ActivityPub::Object.find_or_initialize_by(uri: object_uri).tap do |o|
|
2024-02-21 20:50:06 +00:00
|
|
|
# XXX: Si el objeto es una actividad, esto siempre va a ser
|
|
|
|
# Generic
|
|
|
|
o.type ||= 'ActivityPub::Object::Generic'
|
2024-02-21 18:44:16 +00:00
|
|
|
o.content = original_object if object_embedded?
|
2024-02-21 16:13:24 +00:00
|
|
|
|
2024-02-20 20:15:43 +00:00
|
|
|
o.save!
|
2024-02-21 18:44:16 +00:00
|
|
|
|
|
|
|
# XXX: el objeto necesita ser guardado antes de poder
|
|
|
|
# procesarlo
|
2024-03-01 19:47:00 +00:00
|
|
|
::ActivityPub::FetchJob.perform_later(site: site, object: o) unless object_embedded?
|
2024-02-20 20:15:43 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Genera el seguimiento del estado del objeto con respecto al
|
|
|
|
# sitio.
|
|
|
|
#
|
|
|
|
# @return [ActivityPub]
|
|
|
|
def activity_pub
|
2024-02-28 20:52:24 +00:00
|
|
|
@activity_pub ||= site.activity_pubs.find_or_create_by!(site: site, instance: instance, object: object)
|
2024-02-20 20:15:43 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# Crea la actividad y la vincula con el estado
|
|
|
|
#
|
|
|
|
# @return [ActivityPub::Activity]
|
|
|
|
def activity
|
2024-02-23 15:26:33 +00:00
|
|
|
@activity ||=
|
2024-03-01 19:47:00 +00:00
|
|
|
::ActivityPub::Activity
|
2024-02-23 15:26:33 +00:00
|
|
|
.type_from(original_activity)
|
2024-02-28 20:52:24 +00:00
|
|
|
.find_or_initialize_by(uri: original_activity[:id], activity_pub: activity_pub, actor: actor).tap do |a|
|
2024-02-23 15:26:33 +00:00
|
|
|
a.content = original_activity.dup
|
|
|
|
a.content[:object] = object.uri
|
|
|
|
a.save!
|
|
|
|
end
|
2024-02-20 20:15:43 +00:00
|
|
|
end
|
|
|
|
|
2024-02-26 15:27:56 +00:00
|
|
|
# Actor, si no hay instancia, la crea en el momento, junto con
|
|
|
|
# su estado de moderación.
|
2024-02-20 20:15:43 +00:00
|
|
|
#
|
|
|
|
# @return [Actor]
|
|
|
|
def actor
|
2024-03-01 19:47:00 +00:00
|
|
|
@actor ||= ::ActivityPub::Actor.find_or_initialize_by(uri: original_activity[:actor]).tap do |a|
|
2024-02-24 15:47:11 +00:00
|
|
|
unless a.instance
|
2024-03-01 19:47:00 +00:00
|
|
|
a.instance = ::ActivityPub::Instance.find_or_create_by(hostname: URI.parse(a.uri).hostname)
|
2024-02-20 20:15:43 +00:00
|
|
|
|
2024-02-26 15:27:56 +00:00
|
|
|
site.instance_moderations.find_or_create_by(instance: a.instance)
|
|
|
|
|
2024-03-01 19:47:00 +00:00
|
|
|
::ActivityPub::InstanceFetchJob.perform_later(site: site, instance: a.instance)
|
2024-02-24 15:47:11 +00:00
|
|
|
end
|
2024-02-24 14:09:57 +00:00
|
|
|
|
2024-02-20 20:15:43 +00:00
|
|
|
a.save!
|
2024-02-24 15:46:55 +00:00
|
|
|
|
2024-02-28 20:44:21 +00:00
|
|
|
site.actor_moderations.find_or_create_by(actor: a)
|
|
|
|
|
2024-03-01 19:47:00 +00:00
|
|
|
::ActivityPub::ActorFetchJob.perform_later(site: site, actor: a)
|
2024-02-20 20:15:43 +00:00
|
|
|
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
|
|
|
|
rescue RuntimeError => e
|
|
|
|
raise ActiveRecord::RecordNotFound, e.message
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# @return [Hash,String]
|
|
|
|
def original_object
|
2024-02-23 15:36:03 +00:00
|
|
|
@original_object ||= original_activity[:object].dup.tap do |o|
|
|
|
|
o[:@context] = original_activity[:@context].dup
|
|
|
|
end
|
2024-02-20 20:15:43 +00:00
|
|
|
end
|
2024-02-20 20:13:42 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|