5
0
Fork 0
mirror of https://0xacab.org/sutty/sutty synced 2024-10-10 04:16:57 +00:00

Merge branch 'issue-15109-1' of https://0xacab.org/sutty/sutty into production.panel.sutty.nl

This commit is contained in:
Sutty 2024-03-16 17:28:08 +00:00
commit 9147045505
8 changed files with 198 additions and 162 deletions

View file

@ -83,6 +83,7 @@ gem 'rubanok'
gem 'after_commit_everywhere', '~> 1.0' gem 'after_commit_everywhere', '~> 1.0'
gem 'aasm' gem 'aasm'
gem 'que-web'
# database # database
gem 'hairtrigger' gem 'hairtrigger'

View file

@ -97,6 +97,7 @@ GEM
ast (2.4.2) ast (2.4.2)
autoprefixer-rails (10.4.13.0) autoprefixer-rails (10.4.13.0)
execjs (~> 2) execjs (~> 2)
base64 (0.2.0)
bcrypt (3.1.20-x86_64-linux-musl) bcrypt (3.1.20-x86_64-linux-musl)
bcrypt_pbkdf (1.1.0-x86_64-linux-musl) bcrypt_pbkdf (1.1.0-x86_64-linux-musl)
benchmark-ips (2.12.0) benchmark-ips (2.12.0)
@ -371,6 +372,8 @@ GEM
i18n (>= 0.6.10, < 2) i18n (>= 0.6.10, < 2)
request_store (~> 1.0) request_store (~> 1.0)
multi_xml (0.6.0) multi_xml (0.6.0)
mustermann (3.0.0)
ruby2_keywords (~> 0.0.1)
net-imap (0.4.9) net-imap (0.4.9)
date date
net-protocol net-protocol
@ -410,12 +413,18 @@ GEM
pundit (2.3.1) pundit (2.3.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
que (2.2.1) que (2.2.1)
que-web (0.10.0)
que (>= 1)
sinatra
racc (1.7.3-x86_64-linux-musl) racc (1.7.3-x86_64-linux-musl)
rack (2.2.8) rack (2.2.8)
rack-cors (2.0.1) rack-cors (2.0.1)
rack (>= 2.0.0) rack (>= 2.0.0)
rack-mini-profiler (3.1.0) rack-mini-profiler (3.1.0)
rack (>= 1.2.0) rack (>= 1.2.0)
rack-protection (3.2.0)
base64 (>= 0.1.0)
rack (~> 2.2, >= 2.2.4)
rack-proxy (0.7.7) rack-proxy (0.7.7)
rack rack
rack-test (2.1.0) rack-test (2.1.0)
@ -514,6 +523,7 @@ GEM
ruby-statistics (3.0.2) ruby-statistics (3.0.2)
ruby-vips (2.2.0) ruby-vips (2.2.0)
ffi (~> 1.12) ffi (~> 1.12)
ruby2_keywords (0.0.5)
ruby2ruby (2.5.0) ruby2ruby (2.5.0)
ruby_parser (~> 3.1) ruby_parser (~> 3.1)
sexp_processor (~> 4.6) sexp_processor (~> 4.6)
@ -540,6 +550,11 @@ GEM
sexp_processor (4.17.0) sexp_processor (4.17.0)
simpleidn (0.2.1) simpleidn (0.2.1)
unf (~> 0.1.4) unf (~> 0.1.4)
sinatra (3.2.0)
mustermann (~> 3.0)
rack (~> 2.2, >= 2.2.4)
rack-protection (= 3.2.0)
tilt (~> 2.0)
sourcemap (0.1.1) sourcemap (0.1.1)
spring (4.1.1) spring (4.1.1)
spring-watcher-listen (2.1.0) spring-watcher-listen (2.1.0)
@ -627,7 +642,7 @@ DEPENDENCIES
devise devise
devise-i18n devise-i18n
devise_invitable devise_invitable
distributed-press-api-client (~> 0.4.0) distributed-press-api-client (~> 0.4.1)
dotenv-rails dotenv-rails
down down
ed25519 ed25519
@ -671,6 +686,7 @@ DEPENDENCIES
puma puma
pundit pundit
que que
que-web
rack-cors rack-cors
rack-mini-profiler rack-mini-profiler
rails (~> 6.1.0) rails (~> 6.1.0)

View file

@ -5,196 +5,51 @@ module Api
module Webhooks module Webhooks
# Recibe webhooks de la Social Inbox # Recibe webhooks de la Social Inbox
# #
# @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.
# @see {https://www.w3.org/TR/activitypub/} # @see {https://www.w3.org/TR/activitypub/}
class SocialInboxController < BaseController class SocialInboxController < BaseController
include Api::V1::Webhooks::Concerns::WebhookConcern include Api::V1::Webhooks::Concerns::WebhookConcern
# Validar que el token sea correcto
before_action :usuarie
# Cuando una actividad ingresa en la cola de moderación, la # Cuando una actividad ingresa en la cola de moderación, la
# recibimos por acá # recibimos por acá
# #
# Vamos a recibir Create, Update, Delete, Follow, Undo y obtener # Vamos a recibir Create, Update, Delete, Follow, Undo,
# el objeto dentro de cada una para guardar un estado asociado # Announce, Like y obtener el objeto dentro de cada una para
# al sitio. # guardar un estado asociado al sitio.
# #
# El objeto del estado puede ser un objeto o une actore, # El objeto del estado puede ser un objeto o une actore,
# dependiendo de la actividad. # dependiendo de la actividad.
def moderationqueued def moderationqueued
# Devuelve un error si el token no es válido process! :paused
usuarie.present?
::ActivityPub.transaction do
# Crea todos los registros necesarios y actualiza el estado
actor.present?
instance.present?
object.present?
activity_pub.present?
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 head :accepted
end end
# Cuando la Social Inbox acepta una actividad, la recibimos # Cuando la Social Inbox acepta una actividad, la recibimos
# igual y la guardamos por si cambiamos de idea. # igual y la guardamos por si cambiamos de idea.
#
# @todo DRY
def onapproved def onapproved
::ActivityPub.transaction do process! :approved
actor.present?
instance.present?
object.present?
activity.present?
activity_pub.update(aasm_state: 'approved')
activity.update_activity_pub_state!
end
head :accepted head :accepted
end end
# Cuando la Social Inbox rechaza una actividad, la recibimos # Cuando la Social Inbox rechaza una actividad, la recibimos
# igual y la guardamos por si cambiamos de idea. # igual y la guardamos por si cambiamos de idea.
#
# @todo DRY
def onrejected def onrejected
::ActivityPub.transaction do process! :rejected
actor.present?
instance.present?
object.present?
activity.present?
activity_pub.update(aasm_state: 'rejected')
end
head :accepted head :accepted
end end
private private
# Si el objeto ya viene incorporado en la actividad o lo tenemos # Envía la actividad para procesamiento por separado.
# que traer remotamente.
# #
# @return [Bool] # @param initial_state [Symbol]
def object_embedded? def process!(initial_state)
@object_embedded ||= original_activity[:object].is_a?(Hash) ::ActivityPub::ProcessJob.perform_later(site: site, body: request.raw_post, initial_state: initial_state)
end
# Encuentra la URI del objeto o falla si no la encuentra.
#
# @return [String]
def object_uri
@object_uri ||= ::ActivityPub.uri_from_object(original_activity[:object])
ensure
raise ActiveRecord::RecordNotFound, 'object id missing' if @object_uri.blank?
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. Si el objeto no viene
# incorporado, obtenemos el contenido más tarde.
#
# @return [ActivityPub::Object]
def object
@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'
if object_embedded?
o.content = original_object
begin
type = original_object[:type].presence
o.type = "ActivityPub::Object::#{type}".constantize if type
rescue NameError
end
end
o.save!
# XXX: el objeto necesita ser guardado antes de poder
# procesarlo. No usamos GlobalID porque el tipo de objeto
# cambia y produce un error de deserialización.
::ActivityPub::FetchJob.perform_later(site: site, object_id: o.id) unless object_embedded?
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, actor: actor, instance: instance, object_id: object.id, object_type: object.type)
end
# Crea la actividad y la vincula con el estado
#
# @return [ActivityPub::Activity]
def activity
@activity ||=
::ActivityPub::Activity
.type_from(original_activity)
.find_or_initialize_by(uri: original_activity[:id], activity_pub: activity_pub, actor: actor).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, junto con
# su estado de moderación.
#
# @return [Actor]
def actor
@actor ||= ::ActivityPub::Actor.find_or_initialize_by(uri: original_activity[:actor]).tap do |a|
unless a.instance
a.instance = ::ActivityPub::Instance.find_or_create_by(hostname: URI.parse(a.uri).hostname)
::ActivityPub::InstanceFetchJob.perform_later(site: site, instance: a.instance)
end
site.instance_moderations.find_or_create_by(instance: a.instance)
a.save!
site.actor_moderations.find_or_create_by(actor: a)
::ActivityPub::ActorFetchJob.perform_later(site: site, actor: a)
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
@original_object ||= original_activity[:object].dup.tap do |o|
o[:@context] = original_activity[:@context].dup
end
end end
end end
end end

View file

@ -0,0 +1,154 @@
# frozen_string_literal: true
class ActivityPub
# Procesar las actividades a medida que llegan
class ProcessJob < ApplicationJob
attr_reader :body
# Procesa la actividad en segundo plano
#
# @param :body [String]
# @param :initial_state [Symbol,String]
def perform(site:, body:, initial_state: :paused)
@body = body
@site = site
ActiveRecord::Base.connection_pool.with_connection do
::ActivityPub.transaction do
# Crea todos los registros necesarios y actualiza el estado
actor.present?
instance.present?
object.present?
activity_pub.present?
activity_pub.update(aasm_state: initial_state)
activity.update_activity_pub_state!
end
end
# Al generar una excepción, en lugar de seguir intentando, enviamos
# el reporte.
rescue Exception => e
ExceptionNotifier.notify_exception(e, data: { site: site.name, activity: original_activity })
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 ||= ::ActivityPub.uri_from_object(original_activity[:object])
ensure
raise ActiveRecord::RecordNotFound, 'object id missing' if @object_uri.blank?
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. Si el objeto no viene
# incorporado, obtenemos el contenido más tarde.
#
# @return [ActivityPub::Object]
def object
@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'
if object_embedded?
o.content = original_object
begin
type = original_object[:type].presence
o.type = "ActivityPub::Object::#{type}".constantize if type
rescue NameError
end
end
o.save!
# XXX: el objeto necesita ser guardado antes de poder
# procesarlo. No usamos GlobalID porque el tipo de objeto
# cambia y produce un error de deserialización.
::ActivityPub::FetchJob.perform_later(site: site, object_id: o.id) unless object_embedded?
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, actor: actor, instance: instance,
object_id: object.id, object_type: object.type)
end
# Crea la actividad y la vincula con el estado
#
# @return [ActivityPub::Activity]
def activity
@activity ||=
::ActivityPub::Activity
.type_from(original_activity)
.find_or_initialize_by(uri: original_activity[:id], activity_pub: activity_pub, actor: actor).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, junto con
# su estado de moderación.
#
# @return [Actor]
def actor
@actor ||= ::ActivityPub::Actor.find_or_initialize_by(uri: original_activity[:actor]).tap do |a|
unless a.instance
a.instance = ::ActivityPub::Instance.find_or_create_by(hostname: URI.parse(a.uri).hostname)
::ActivityPub::InstanceFetchJob.perform_later(site: site, instance: a.instance)
end
site.instance_moderations.find_or_create_by(instance: a.instance)
a.save!
site.actor_moderations.find_or_create_by(actor: a)
::ActivityPub::ActorFetchJob.perform_later(site: site, actor: a)
end
end
# @return [Hash,String]
def original_object
@original_object ||= original_activity[:object].dup.tap do |o|
o[:@context] = original_activity[:@context].dup
end
end
# Descubre la actividad recibida, generando un error si la
# actividad no está dirigida a nosotres.
#
# @todo Validar formato con Dry::Schema
# @return [Hash]
def original_activity
@original_activity ||= FastJsonparser.parse(body).tap do |activity|
raise '@context missing' unless activity[:@context].present?
raise 'id missing' unless activity[:id].present?
raise 'object missing' unless activity[:object].present?
end
end
end
end

View file

@ -3,7 +3,7 @@
- local_assigns[:method] ||= 'patch' - local_assigns[:method] ||= 'patch'
- local_assigns[:class] ||= 'btn-secondary' - local_assigns[:class] ||= 'btn-secondary'
- local_assigns[:class] = "btn #{local_assigns[:class]}" - local_assigns[:class] = "btn #{local_assigns[:class]}"
- local_assigns.delete(:text)
-# @todo path es obligatorio = button_to(path, **local_assigns.compact) do
= button_to local_assigns[:path], **local_assigns.compact do
= text = text

View file

@ -1,4 +1,6 @@
-# -#
@param name [String] @param name [String]
@param value [String] @param value [String]
%button.dropdown-item{type: 'submit', data: { target: 'dropdown.item' }, name: name, value: value, **local_assigns.compact } @param text [String]
- local_assigns.delete(:text)
%button.dropdown-item{type: 'submit', data: { target: 'dropdown.item' }, name: name, value: value, **local_assigns.compact }= text

View file

@ -0,0 +1,5 @@
# frozen_string_literal: true
Que::Web.use(Rack::Auth::Basic) do |user, password|
[user, password] == [ENV['HTTP_BASIC_USER'], ENV['HTTP_BASIC_PASSWORD']]
end

View file

@ -4,6 +4,9 @@ Rails.application.routes.draw do
devise_for :usuaries devise_for :usuaries
get '/.well-known/change-password', to: redirect('/usuaries/edit') get '/.well-known/change-password', to: redirect('/usuaries/edit')
require 'que/web'
mount Que::Web => '/que'
root 'application#index' root 'application#index'
constraints(Constraints::ApiSubdomain.new) do constraints(Constraints::ApiSubdomain.new) do