mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-22 09:56:21 +00:00
Merge branch 'issue-15109-1' into panel.testing.sutty.nl
This commit is contained in:
commit
f2a48f6f7d
115 changed files with 2594 additions and 96 deletions
|
@ -90,7 +90,7 @@ rubocop:
|
||||||
- *apk-add
|
- *apk-add
|
||||||
- *disable-hainish
|
- *disable-hainish
|
||||||
script:
|
script:
|
||||||
- "./bin/modified_files | ./bin/with_extension rb | xargs -r go-task bundle -- exec rubocop"
|
- "go-task rubocop"
|
||||||
haml:
|
haml:
|
||||||
stage: "test"
|
stage: "test"
|
||||||
cache:
|
cache:
|
||||||
|
@ -101,4 +101,4 @@ haml:
|
||||||
- *apk-add
|
- *apk-add
|
||||||
- *disable-hainish
|
- *disable-hainish
|
||||||
script:
|
script:
|
||||||
- "./bin/modified_files | ./bin/with_extension haml | xargs -r go-task bundle -- exec haml-lint"
|
- "go-task haml-lint"
|
||||||
|
|
3
Gemfile
3
Gemfile
|
@ -39,7 +39,7 @@ gem 'devise-i18n'
|
||||||
gem 'devise_invitable'
|
gem 'devise_invitable'
|
||||||
gem 'redis-client'
|
gem 'redis-client'
|
||||||
gem 'hiredis-client'
|
gem 'hiredis-client'
|
||||||
gem 'distributed-press-api-client', '~> 0.4.0rc2'
|
gem 'distributed-press-api-client', '~> 0.4.0rc3'
|
||||||
gem 'email_address', git: 'https://github.com/fauno/email_address', branch: 'i18n'
|
gem 'email_address', git: 'https://github.com/fauno/email_address', branch: 'i18n'
|
||||||
gem 'exception_notification'
|
gem 'exception_notification'
|
||||||
gem 'fast_blank'
|
gem 'fast_blank'
|
||||||
|
@ -79,6 +79,7 @@ gem 'webpacker'
|
||||||
gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git'
|
gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git'
|
||||||
gem 'kaminari'
|
gem 'kaminari'
|
||||||
gem 'device_detector'
|
gem 'device_detector'
|
||||||
|
gem 'rubanok'
|
||||||
|
|
||||||
gem 'after_commit_everywhere', '~> 1.0'
|
gem 'after_commit_everywhere', '~> 1.0'
|
||||||
gem 'aasm'
|
gem 'aasm'
|
||||||
|
|
10
Gemfile.lock
10
Gemfile.lock
|
@ -167,12 +167,12 @@ GEM
|
||||||
devise_invitable (2.0.9)
|
devise_invitable (2.0.9)
|
||||||
actionmailer (>= 5.0)
|
actionmailer (>= 5.0)
|
||||||
devise (>= 4.6)
|
devise (>= 4.6)
|
||||||
distributed-press-api-client (0.4.0rc2)
|
distributed-press-api-client (0.4.0rc3)
|
||||||
addressable (~> 2.3, >= 2.3.0)
|
addressable (~> 2.3, >= 2.3.0)
|
||||||
climate_control
|
climate_control
|
||||||
dry-schema
|
dry-schema
|
||||||
httparty (~> 0.18)
|
httparty (~> 0.18)
|
||||||
httparty-cache (~> 0.0.4)
|
httparty-cache (~> 0.0.6)
|
||||||
json (~> 2.1, >= 2.1.0)
|
json (~> 2.1, >= 2.1.0)
|
||||||
jwt (~> 2.6.0)
|
jwt (~> 2.6.0)
|
||||||
dotenv (2.8.1)
|
dotenv (2.8.1)
|
||||||
|
@ -273,7 +273,7 @@ GEM
|
||||||
httparty (0.21.0)
|
httparty (0.21.0)
|
||||||
mini_mime (>= 1.0.0)
|
mini_mime (>= 1.0.0)
|
||||||
multi_xml (>= 0.5.2)
|
multi_xml (>= 0.5.2)
|
||||||
httparty-cache (0.0.5)
|
httparty-cache (0.0.6)
|
||||||
httparty (~> 0.18)
|
httparty (~> 0.18)
|
||||||
i18n (1.14.1)
|
i18n (1.14.1)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
|
@ -489,6 +489,7 @@ GEM
|
||||||
rexml (~> 3.2, >= 3.2.4)
|
rexml (~> 3.2, >= 3.2.4)
|
||||||
stream (~> 0.5.3)
|
stream (~> 0.5.3)
|
||||||
rouge (3.30.0)
|
rouge (3.30.0)
|
||||||
|
rubanok (0.5.0)
|
||||||
rubocop (1.42.0)
|
rubocop (1.42.0)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
|
@ -626,7 +627,7 @@ DEPENDENCIES
|
||||||
devise
|
devise
|
||||||
devise-i18n
|
devise-i18n
|
||||||
devise_invitable
|
devise_invitable
|
||||||
distributed-press-api-client (~> 0.4.0rc2)
|
distributed-press-api-client (~> 0.4.0rc3)
|
||||||
dotenv-rails
|
dotenv-rails
|
||||||
down
|
down
|
||||||
ed25519
|
ed25519
|
||||||
|
@ -680,6 +681,7 @@ DEPENDENCIES
|
||||||
redis-rails
|
redis-rails
|
||||||
rgl
|
rgl
|
||||||
rollups!
|
rollups!
|
||||||
|
rubanok
|
||||||
rubocop-rails
|
rubocop-rails
|
||||||
ruby-brs
|
ruby-brs
|
||||||
rubyzip
|
rubyzip
|
||||||
|
|
1
Procfile
1
Procfile
|
@ -3,3 +3,4 @@ distributed_press_tokens_renew: bundle exec rake distributed_press:tokens:renew
|
||||||
emergency_cleanup: bundle exec rake cleanup:everything BEFORE=7
|
emergency_cleanup: bundle exec rake cleanup:everything BEFORE=7
|
||||||
que: daemonize -c /srv/ -p /srv/tmp/que.pid -u rails /usr/local/bin/syslogize bundle exec que
|
que: daemonize -c /srv/ -p /srv/tmp/que.pid -u rails /usr/local/bin/syslogize bundle exec que
|
||||||
stats: bundle exec rake stats:process_all
|
stats: bundle exec rake stats:process_all
|
||||||
|
fediblock: bundle exec rails activity_pub:fediblocks
|
||||||
|
|
|
@ -183,3 +183,11 @@ tasks:
|
||||||
- "{{.HAINISH}} gem install bundler-audit"
|
- "{{.HAINISH}} gem install bundler-audit"
|
||||||
status:
|
status:
|
||||||
- "test -f ../hain/usr/bin/bundler-audit"
|
- "test -f ../hain/usr/bin/bundler-audit"
|
||||||
|
rubocop:
|
||||||
|
desc: "Ruby linting"
|
||||||
|
cmds:
|
||||||
|
- "./bin/modified_files | ./bin/with_extension rb | xargs -r {{.HAINISH}} bundle exec rubocop {{.CLI_ARGS}}"
|
||||||
|
haml-lint:
|
||||||
|
desc: "HAML linting"
|
||||||
|
cmds:
|
||||||
|
- "./bin/modified_files | ./bin/with_extension haml | xargs -r {{.HAINISH}} bundle exec haml-lint {{.CLI_ARGS}}"
|
||||||
|
|
|
@ -587,3 +587,31 @@ $bezier: cubic-bezier(0.75, 0, 0.25, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// details styles
|
||||||
|
|
||||||
|
.details {
|
||||||
|
& > summary {
|
||||||
|
list-style: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.hide-when-open {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-when-open {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[open] {
|
||||||
|
& > summary {
|
||||||
|
.hide-when-open {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-when-open {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,10 @@ $cyan: #13fefe;
|
||||||
--color: #{$cyan};
|
--color: #{$cyan};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background-color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
color: $black;
|
color: $black;
|
||||||
|
@ -26,3 +30,5 @@ $cyan: #13fefe;
|
||||||
box-shadow: 0 0 0 0.2rem $cyan;
|
box-shadow: 0 0 0 0.2rem $cyan;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
61
app/controllers/activity_pubs_controller.rb
Normal file
61
app/controllers/activity_pubs_controller.rb
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Gestiona acciones de moderación
|
||||||
|
class ActivityPubsController < ApplicationController
|
||||||
|
include ModerationConcern
|
||||||
|
|
||||||
|
ActivityPub.events.each do |event|
|
||||||
|
define_method(event) do
|
||||||
|
authorize activity_pub
|
||||||
|
|
||||||
|
activity_pub.update(remote_flag_params(activity_pub)) if event == :report
|
||||||
|
activity_pub.public_send(:"#{event}!") if activity_pub.public_send(:"may_#{event}?")
|
||||||
|
|
||||||
|
redirect_to_moderation_queue!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def action_on_several
|
||||||
|
activity_pubs = site.activity_pubs.where(id: params[:activity_pub])
|
||||||
|
|
||||||
|
authorize activity_pubs
|
||||||
|
|
||||||
|
action = params[:activity_pub_action].to_sym
|
||||||
|
method = :"#{action}!"
|
||||||
|
may = :"may_#{action}?"
|
||||||
|
|
||||||
|
redirect_to_moderation_queue!
|
||||||
|
|
||||||
|
return unless ActivityPub.events.include? action
|
||||||
|
|
||||||
|
# Crear una sola remote flag por autore
|
||||||
|
if action == :report
|
||||||
|
message = remote_flag_params(activity_pubs.first).dig(:remote_flag_attributes, :message)
|
||||||
|
|
||||||
|
activity_pubs.distinct.pluck(:actor_id).each do |actor_id|
|
||||||
|
remote_flag = ActivityPub::RemoteFlag.find_or_initialize_by(actor_id: actor_id, site_id: site.id)
|
||||||
|
remote_flag.message = message
|
||||||
|
# Lo estamos actualizando, con lo que lo vamos a volver a enviar
|
||||||
|
remote_flag.requeue if remote_flag.persisted?
|
||||||
|
remote_flag.save
|
||||||
|
# XXX: Idealmente todas las ActivityPub que enviamos pueden
|
||||||
|
# cambiar de estado, pero chequeamos de todas formas.
|
||||||
|
remote_flag.activity_pubs << (activity_pubs.where(actor_id: actor_id).to_a.select { |a| a.public_send(may) })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ActivityPub.transaction do
|
||||||
|
activity_pubs.find_each do |activity_pub|
|
||||||
|
next unless activity_pub.public_send(may)
|
||||||
|
|
||||||
|
activity_pub.public_send(method)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def activity_pub
|
||||||
|
@activity_pub ||= site.activity_pubs.find(params[:activity_pub_id])
|
||||||
|
end
|
||||||
|
end
|
56
app/controllers/actor_moderations_controller.rb
Normal file
56
app/controllers/actor_moderations_controller.rb
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Gestiona la cola de moderación de actores
|
||||||
|
class ActorModerationsController < ApplicationController
|
||||||
|
include ModerationConcern
|
||||||
|
include ModerationFiltersConcern
|
||||||
|
|
||||||
|
ActorModeration.events.each do |actor_event|
|
||||||
|
define_method(actor_event) do
|
||||||
|
authorize actor_moderation
|
||||||
|
|
||||||
|
# Crea una RemoteFlag si se envían los parámetros adecuados
|
||||||
|
actor_moderation.update(remote_flag_params(actor_moderation)) if actor_event == :report
|
||||||
|
|
||||||
|
actor_moderation.public_send(:"#{actor_event}!") if actor_moderation.public_send(:"may_#{actor_event}?")
|
||||||
|
|
||||||
|
redirect_to_moderation_queue!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Ver el perfil remoto
|
||||||
|
def show
|
||||||
|
@remote_profile = actor_moderation.actor.content
|
||||||
|
@moderation_queue = rubanok_process(site.activity_pubs.where(actor_id: actor_moderation.actor_id), with: ActivityPubProcessor)
|
||||||
|
end
|
||||||
|
|
||||||
|
def action_on_several
|
||||||
|
actor_moderations = site.actor_moderations.where(id: params[:actor_moderation])
|
||||||
|
|
||||||
|
authorize actor_moderations
|
||||||
|
|
||||||
|
action = params[:actor_moderation_action].to_sym
|
||||||
|
method = :"#{action}!"
|
||||||
|
may = :"may_#{action}?"
|
||||||
|
|
||||||
|
redirect_to_moderation_queue!
|
||||||
|
|
||||||
|
return unless ActorModeration.events.include? action
|
||||||
|
|
||||||
|
ActorModeration.transaction do
|
||||||
|
actor_moderations.find_each do |actor_moderation|
|
||||||
|
next unless actor_moderation.public_send(may)
|
||||||
|
|
||||||
|
actor_moderation.update(actor_moderation_params(actor_moderation)) if action == :report
|
||||||
|
|
||||||
|
actor_moderation.public_send(method)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def actor_moderation
|
||||||
|
@actor_moderation ||= site.actor_moderations.find(params[:actor_moderation_id] || params[:id])
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,27 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
module V1
|
||||||
|
module ActivityPub
|
||||||
|
# Devuelve los reportes remotos hechos
|
||||||
|
#
|
||||||
|
# @todo Verificar la firma. Por ahora no es necesario porque no es
|
||||||
|
# posible obtener remotamente todos los reportes y se identifican por
|
||||||
|
# UUIDv4.
|
||||||
|
class RemoteFlagsController < BaseController
|
||||||
|
skip_forgery_protection
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: (remote_flag&.content || {}), content_type: 'application/activity+json'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# @return [ActivityPub::RemoteFlag,nil]
|
||||||
|
def remote_flag
|
||||||
|
@remote_flag ||= ::ActivityPub::RemoteFlag.find(params[:id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,6 +5,9 @@ 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
|
||||||
|
@ -22,12 +25,14 @@ module Api
|
||||||
# Devuelve un error si el token no es válido
|
# Devuelve un error si el token no es válido
|
||||||
usuarie.present?
|
usuarie.present?
|
||||||
|
|
||||||
ActivityPub.transaction do
|
::ActivityPub.transaction do
|
||||||
|
|
||||||
# Crea todos los registros necesarios y actualiza el estado
|
# Crea todos los registros necesarios y actualiza el estado
|
||||||
actor.present?
|
actor.present?
|
||||||
instance.present?
|
instance.present?
|
||||||
object.present?
|
object.present?
|
||||||
activity_pub.present?
|
activity_pub.present?
|
||||||
|
|
||||||
activity.update_activity_pub_state!
|
activity.update_activity_pub_state!
|
||||||
end
|
end
|
||||||
rescue ActiveRecord::RecordInvalid => e
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
|
@ -43,12 +48,12 @@ module Api
|
||||||
#
|
#
|
||||||
# @todo DRY
|
# @todo DRY
|
||||||
def onapproved
|
def onapproved
|
||||||
ActivityPub.transaction do
|
::ActivityPub.transaction do
|
||||||
actor.present?
|
actor.present?
|
||||||
instance.present?
|
instance.present?
|
||||||
object.present?
|
object.present?
|
||||||
activity.present?
|
activity.present?
|
||||||
activity_pub.approve!
|
activity_pub.approve! if activity_pub.may_approve?
|
||||||
end
|
end
|
||||||
|
|
||||||
head :accepted
|
head :accepted
|
||||||
|
@ -59,12 +64,12 @@ module Api
|
||||||
#
|
#
|
||||||
# @todo DRY
|
# @todo DRY
|
||||||
def onrejected
|
def onrejected
|
||||||
ActivityPub.transaction do
|
::ActivityPub.transaction do
|
||||||
actor.present?
|
actor.present?
|
||||||
instance.present?
|
instance.present?
|
||||||
object.present?
|
object.present?
|
||||||
activity.present?
|
activity.present?
|
||||||
activity_pub.reject!
|
activity_pub.reject! if activity_pub.may_reject?
|
||||||
end
|
end
|
||||||
|
|
||||||
head :accepted
|
head :accepted
|
||||||
|
@ -84,13 +89,9 @@ module Api
|
||||||
#
|
#
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def object_uri
|
def object_uri
|
||||||
@object_uri ||=
|
@object_uri ||= ::ActivityPub.uri_from_object(original_activity[:object])
|
||||||
case original_activity[:object]
|
|
||||||
when String then original_activity[:object]
|
|
||||||
when Hash then original_activity.dig(:object, :id)
|
|
||||||
end
|
|
||||||
ensure
|
ensure
|
||||||
raise ActiveRecord::RecordNotFound, 'object id missing' unless @object_uri
|
raise ActiveRecord::RecordNotFound, 'object id missing' if @object_uri.blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Atajo a la instancia
|
# Atajo a la instancia
|
||||||
|
@ -106,17 +107,25 @@ module Api
|
||||||
#
|
#
|
||||||
# @return [ActivityPub::Object]
|
# @return [ActivityPub::Object]
|
||||||
def object
|
def object
|
||||||
@object ||= ActivityPub::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
|
# XXX: Si el objeto es una actividad, esto siempre va a ser
|
||||||
# Generic
|
# Generic
|
||||||
o.type ||= 'ActivityPub::Object::Generic'
|
o.type ||= 'ActivityPub::Object::Generic'
|
||||||
o.content = original_object if object_embedded?
|
|
||||||
|
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!
|
o.save!
|
||||||
|
|
||||||
# XXX: el objeto necesita ser guardado antes de poder
|
# XXX: el objeto necesita ser guardado antes de poder
|
||||||
# procesarlo
|
# procesarlo
|
||||||
ActivityPub::FetchJob.perform_later(site: site, object: o) unless object_embedded?
|
::ActivityPub::FetchJob.perform_later(site: site, object: o) unless object_embedded?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -125,30 +134,42 @@ module Api
|
||||||
#
|
#
|
||||||
# @return [ActivityPub]
|
# @return [ActivityPub]
|
||||||
def activity_pub
|
def activity_pub
|
||||||
@activity_pub ||= site.activity_pubs.find_or_create_by!(site: site, object: object)
|
@activity_pub ||= site.activity_pubs.find_or_create_by!(site: site, actor: actor, instance: instance, object_id: object.id, object_type: object.type)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Crea la actividad y la vincula con el estado
|
# Crea la actividad y la vincula con el estado
|
||||||
#
|
#
|
||||||
# @return [ActivityPub::Activity]
|
# @return [ActivityPub::Activity]
|
||||||
def activity
|
def activity
|
||||||
@activity ||= ActivityPub::Activity.type_from(original_activity).new(uri: original_activity[:id],
|
@activity ||=
|
||||||
activity_pub: activity_pub).tap do |a|
|
::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 = original_activity.dup
|
||||||
a.content[:object] = object.uri
|
a.content[:object] = object.uri
|
||||||
a.save!
|
a.save!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Actor, si no hay instancia, la crea en el momento
|
# Actor, si no hay instancia, la crea en el momento, junto con
|
||||||
|
# su estado de moderación.
|
||||||
#
|
#
|
||||||
# @return [Actor]
|
# @return [Actor]
|
||||||
def actor
|
def actor
|
||||||
@actor ||= ActivityPub::Actor.find_or_initialize_by(uri: original_activity[:actor]).tap do |a|
|
@actor ||= ::ActivityPub::Actor.find_or_initialize_by(uri: original_activity[:actor]).tap do |a|
|
||||||
next if a.instance
|
unless a.instance
|
||||||
|
a.instance = ::ActivityPub::Instance.find_or_create_by(hostname: URI.parse(a.uri).hostname)
|
||||||
|
|
||||||
|
site.instance_moderations.find_or_create_by(instance: a.instance)
|
||||||
|
|
||||||
|
::ActivityPub::InstanceFetchJob.perform_later(site: site, instance: a.instance)
|
||||||
|
end
|
||||||
|
|
||||||
a.instance = ActivityPub::Instance.find_or_create_by(hostname: URI.parse(a.uri).hostname)
|
|
||||||
a.save!
|
a.save!
|
||||||
|
|
||||||
|
site.actor_moderations.find_or_create_by(actor: a)
|
||||||
|
|
||||||
|
::ActivityPub::ActorFetchJob.perform_later(site: site, actor: a)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -169,7 +190,9 @@ module Api
|
||||||
|
|
||||||
# @return [Hash,String]
|
# @return [Hash,String]
|
||||||
def original_object
|
def original_object
|
||||||
@original_object ||= original_activity[:object].dup
|
@original_object ||= original_activity[:object].dup.tap do |o|
|
||||||
|
o[:@context] = original_activity[:@context].dup
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
# Forma de ingreso a Sutty
|
# Forma de ingreso a Sutty
|
||||||
class ApplicationController < ActionController::Base
|
class ApplicationController < ActionController::Base
|
||||||
include ExceptionHandler
|
include ExceptionHandler
|
||||||
include Pundit
|
include Pundit::Authorization
|
||||||
|
|
||||||
protect_from_forgery with: :null_session, prepend: true
|
protect_from_forgery with: :null_session, prepend: true
|
||||||
|
|
||||||
|
@ -119,4 +119,5 @@ class ApplicationController < ActionController::Base
|
||||||
|
|
||||||
sites_path
|
sites_path
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
34
app/controllers/concerns/moderation_concern.rb
Normal file
34
app/controllers/concerns/moderation_concern.rb
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ModerationConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
private
|
||||||
|
|
||||||
|
def redirect_to_moderation_queue!
|
||||||
|
redirect_back fallback_location: site_moderation_queue_path(**(session[:moderation_queue_filters] || {}))
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def panel_actor_mention
|
||||||
|
@panel_actor_mention ||= ENV.fetch('PANEL_ACTOR_MENTION', '@sutty@sutty.nl')
|
||||||
|
end
|
||||||
|
|
||||||
|
def remote_flag_params(model)
|
||||||
|
{ remote_flag_attributes: { id: model.remote_flag_id, message: ''.dup } }.tap do |p|
|
||||||
|
p[:remote_flag_attributes][:site_id] = model.site_id
|
||||||
|
p[:remote_flag_attributes][:actor_id] = model.actor_id
|
||||||
|
|
||||||
|
I18n.available_locales.each do |locale|
|
||||||
|
p[:remote_flag_attributes][:message].tap do |m|
|
||||||
|
m << I18n.t(locale)
|
||||||
|
m << ': '
|
||||||
|
m << I18n.t('remote_flags.report_message', locale: locale, panel_actor_mention: panel_actor_mention)
|
||||||
|
m << '\n\n'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
15
app/controllers/concerns/moderation_filters_concern.rb
Normal file
15
app/controllers/concerns/moderation_filters_concern.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ModerationFiltersConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
before_action :store_filters_in_session!, only: %i[index show]
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def store_filters_in_session!
|
||||||
|
session[:moderation_queue_filters] = params.permit(:instance_state, :actor_state, :activity_pub_state)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
38
app/controllers/fediblock_states_controller.rb
Normal file
38
app/controllers/fediblock_states_controller.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Estado de las listas de bloqueo en cada sitio
|
||||||
|
class FediblockStatesController < ApplicationController
|
||||||
|
# Realiza cambios en las listas de bloqueo
|
||||||
|
def action_on_several
|
||||||
|
# Encontrar todas y deshabilitar las que no se enviaron
|
||||||
|
site.fediblock_states.all.find_each do |fediblock_state|
|
||||||
|
if fediblock_states_ids.include? fediblock_state.id
|
||||||
|
fediblock_state.enable! if fediblock_state.may_enable?
|
||||||
|
elsif fediblock_state.may_disable?
|
||||||
|
fediblock_state.disable!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Bloquear otras instancias
|
||||||
|
if custom_blocklist.present?
|
||||||
|
ActivityPub::InstanceModerationJob.perform_later(site: site, hostnames: custom_blocklist)
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_to site_moderation_queue_path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fediblock_states_ids
|
||||||
|
params[:fediblock_states_ids] || []
|
||||||
|
end
|
||||||
|
|
||||||
|
# La lista de hostnames
|
||||||
|
def custom_blocklist
|
||||||
|
@custom_blocklist ||= fediblocks_states_params[:custom_blocklist].split("\n").map(&:strip).select(&:present?)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fediblocks_states_params
|
||||||
|
@fediblocks_states_params ||= params.permit(:custom_blocklist, fediblock_states_ids: [])
|
||||||
|
end
|
||||||
|
end
|
43
app/controllers/instance_moderations_controller.rb
Normal file
43
app/controllers/instance_moderations_controller.rb
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Actualiza la relación entre un sitio y una instancia
|
||||||
|
class InstanceModerationsController < ApplicationController
|
||||||
|
include ModerationConcern
|
||||||
|
|
||||||
|
InstanceModeration.events.each do |event|
|
||||||
|
define_method(event) do
|
||||||
|
authorize instance_moderation
|
||||||
|
|
||||||
|
instance_moderation.public_send(:"#{event}!") if instance_moderation.public_send(:"may_#{event}?")
|
||||||
|
|
||||||
|
redirect_to_moderation_queue!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def action_on_several
|
||||||
|
instance_moderations = site.instance_moderations.where(id: params[:instance_moderation])
|
||||||
|
|
||||||
|
authorize instance_moderations
|
||||||
|
|
||||||
|
action = params[:instance_moderation_action].to_sym
|
||||||
|
method = :"#{action}!"
|
||||||
|
may = :"may_#{action}?"
|
||||||
|
|
||||||
|
redirect_to_moderation_queue!
|
||||||
|
|
||||||
|
return unless InstanceModeration.events.include? action
|
||||||
|
|
||||||
|
InstanceModeration.transaction do
|
||||||
|
instance_moderations.find_each do |instance_moderation|
|
||||||
|
instance_moderation.public_send(method) if instance_moderation.public_send(may)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# @return [InstanceModeration]
|
||||||
|
def instance_moderation
|
||||||
|
@instance_moderation ||= site.instance_moderations.find(params[:instance_moderation_id])
|
||||||
|
end
|
||||||
|
end
|
15
app/controllers/moderation_queue_controller.rb
Normal file
15
app/controllers/moderation_queue_controller.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Cola de moderación de ActivityPub
|
||||||
|
class ModerationQueueController < ApplicationController
|
||||||
|
include ModerationFiltersConcern
|
||||||
|
|
||||||
|
# Cola de moderación viendo todo el sitio
|
||||||
|
def index
|
||||||
|
# @todo cambiar el estado por query
|
||||||
|
@activity_pubs = site.activity_pubs
|
||||||
|
@instance_moderations = rubanok_process(site.instance_moderations, with: InstanceModerationProcessor)
|
||||||
|
@actor_moderations = rubanok_process(site.actor_moderations, with: ActorModerationProcessor)
|
||||||
|
@moderation_queue = rubanok_process(site.activity_pubs, with: ActivityPubProcessor)
|
||||||
|
end
|
||||||
|
end
|
|
@ -38,6 +38,7 @@ class PostsController < ApplicationController
|
||||||
@usuarie = site.usuarie? current_usuarie
|
@usuarie = site.usuarie? current_usuarie
|
||||||
|
|
||||||
@site_stat = SiteStat.new(site)
|
@site_stat = SiteStat.new(site)
|
||||||
|
dummy_data
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
@ -83,6 +84,7 @@ class PostsController < ApplicationController
|
||||||
authorize post
|
authorize post
|
||||||
breadcrumb post.title.value, site_post_path(site, post, locale: locale), match: :exact
|
breadcrumb post.title.value, site_post_path(site, post, locale: locale), match: :exact
|
||||||
breadcrumb 'posts.edit', ''
|
breadcrumb 'posts.edit', ''
|
||||||
|
dummy_data
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
|
|
@ -33,10 +33,24 @@ module ApplicationHelper
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Devuelve todas las etiquetas HTML que queremos mantener
|
# Sanitizador que elimina todo
|
||||||
def all_html_tags
|
#
|
||||||
%w[h1 h2 h3 h4 h5 h6 p a ul ol li table tr td th tbody thead
|
# @param html [String]
|
||||||
tfoot em strong sup blockquote cite pre section article]
|
# @return [String]
|
||||||
|
def text_plain(html)
|
||||||
|
sanitize(html, tags: [], attributes: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sanitizador con etiquetas y atributos por defecto
|
||||||
|
#
|
||||||
|
# @param html [String]
|
||||||
|
# @param options [Hash]
|
||||||
|
# @return [String]
|
||||||
|
def sanitize(html, options = {})
|
||||||
|
options[:tags] ||= Sutty::ALLOWED_TAGS
|
||||||
|
options[:attributes] ||= Sutty::ALLOWED_ATTRIBUTES
|
||||||
|
|
||||||
|
super(html, options)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Genera HTML y limpia etiquetas innecesarias
|
# Genera HTML y limpia etiquetas innecesarias
|
||||||
|
|
7
app/helpers/moderation_queue_helper.rb
Normal file
7
app/helpers/moderation_queue_helper.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ModerationQueueHelper
|
||||||
|
def filter_states(**args)
|
||||||
|
params.permit(:state, :actor_state, :activity_pub_state).merge(**args)
|
||||||
|
end
|
||||||
|
end
|
17
app/javascript/controllers/details_controller.js
Normal file
17
app/javascript/controllers/details_controller.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { Controller } from "stimulus";
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = [];
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
const state = window.sessionStorage.getItem(this.element.id);
|
||||||
|
|
||||||
|
if (state === "open") {
|
||||||
|
this.element.setAttribute("open", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
store(event = undefined) {
|
||||||
|
window.sessionStorage.setItem(this.element.id, event.newState);
|
||||||
|
}
|
||||||
|
}
|
106
app/javascript/controllers/dropdown_controller.js
Normal file
106
app/javascript/controllers/dropdown_controller.js
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import { Controller } from "stimulus";
|
||||||
|
|
||||||
|
// https://getbootstrap.com/docs/4.6/components/dropdowns/#single-button
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ["dropdown", "button", "item"];
|
||||||
|
|
||||||
|
// Al iniciar el controlador
|
||||||
|
connect() {
|
||||||
|
// Llevar la cuenta del item con foco
|
||||||
|
this.data.set("item", -1);
|
||||||
|
|
||||||
|
// Gestionar las teclas
|
||||||
|
this.keydownEvent = this.keydown.bind(this);
|
||||||
|
this.element.addEventListener("keydown", this.keydownEvent);
|
||||||
|
|
||||||
|
// Gestionar el foco
|
||||||
|
this.focusinEvent = this.focusin.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Al eliminar el controlador (al pasar a otra página)
|
||||||
|
disconnect() {
|
||||||
|
// Eliminar la gestión de teclas
|
||||||
|
this.element.removeEventListener("keydown", this.keydownEvent);
|
||||||
|
// Eliminar la gestión del foco
|
||||||
|
document.removeEventListener("focusin", this.focusinEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mostrar u ocultar
|
||||||
|
toggle(event) {
|
||||||
|
(this.buttonTarget.ariaExpanded === "false") ? this.show() : this.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mostrar
|
||||||
|
show() {
|
||||||
|
this.buttonTarget.ariaExpanded = "true";
|
||||||
|
this.element.classList.add("show");
|
||||||
|
this.dropdownTarget.classList.add("show");
|
||||||
|
|
||||||
|
// Activar la gestión del foco
|
||||||
|
document.addEventListener("focusin", this.focusinEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ocultar
|
||||||
|
hide() {
|
||||||
|
this.buttonTarget.ariaExpanded = "false";
|
||||||
|
this.element.classList.remove("show");
|
||||||
|
this.dropdownTarget.classList.remove("show");
|
||||||
|
// Volver al inicio el foco de items
|
||||||
|
this.data.set("item", -1);
|
||||||
|
|
||||||
|
// Desactivar la gestión del foco
|
||||||
|
document.removeEventListener("focusin", this.focusinEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestionar el foco
|
||||||
|
focusin(event) {
|
||||||
|
const item = this.itemTargets.find(x => x === event.target);
|
||||||
|
|
||||||
|
// Si el foco se coloca sobre elementos del controlador, no hacer
|
||||||
|
// nada
|
||||||
|
if (event.target === this.buttonTarget || item) {
|
||||||
|
// Si es un item, el comportamiento de las flechas verticales y el
|
||||||
|
// Tab tiene que ser igual
|
||||||
|
if (item) this.data.set("item", this.itemTargets.indexOf(item));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// De lo contrario, ocultar
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestionar las teclas
|
||||||
|
keydown(event) {
|
||||||
|
const initial = parseInt(this.data.get("item"));
|
||||||
|
let item = initial;
|
||||||
|
|
||||||
|
switch (event.keyCode) {
|
||||||
|
case 27:
|
||||||
|
// Esc cierra el menú y devuelve el foco
|
||||||
|
this.hide();
|
||||||
|
this.buttonTarget.focus();
|
||||||
|
break;
|
||||||
|
case 38:
|
||||||
|
// Moverse hacia arriba con tope en el primer item
|
||||||
|
if (item > -1) item--;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 40:
|
||||||
|
// Moverse hacia abajo con tope en el último ítem, si el
|
||||||
|
// dropdown estaba cerrado, abrirlo.
|
||||||
|
if (item === -1) this.show();
|
||||||
|
if (item <= this.itemTargets.length) item++;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si cambió la posición del ítem, darle foco y actualizar el
|
||||||
|
// contador.
|
||||||
|
if (initial !== item) {
|
||||||
|
this.itemTargets[item]?.focus();
|
||||||
|
|
||||||
|
this.data.set("item", item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
app/javascript/controllers/select_all_controller.js
Normal file
11
app/javascript/controllers/select_all_controller.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { Controller } from "stimulus";
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ["toggle", "input"];
|
||||||
|
|
||||||
|
toggle(event = undefined) {
|
||||||
|
this.inputTargets.forEach(input => {
|
||||||
|
input.checked = this.toggleTarget.checked;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
26
app/jobs/activity_pub/actor_fetch_job.rb
Normal file
26
app/jobs/activity_pub/actor_fetch_job.rb
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# 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
|
||||||
|
class ActorFetchJob < ApplicationJob
|
||||||
|
self.priority = 50
|
||||||
|
|
||||||
|
def perform(site:, actor:)
|
||||||
|
ActivityPub::Actor.transaction do
|
||||||
|
response = site.social_inbox.dereferencer.get(uri: actor.uri)
|
||||||
|
|
||||||
|
# @todo Fallar cuando la respuesta no funcione?
|
||||||
|
return unless response.ok?
|
||||||
|
return if response.miss? && actor.content.present?
|
||||||
|
|
||||||
|
actor.update(content: FastJsonparser.parse(response.body))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
25
app/jobs/activity_pub/fediblock_fetch_job.rb
Normal file
25
app/jobs/activity_pub/fediblock_fetch_job.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub
|
||||||
|
# Se encarga de mantener las listas de bloqueo actualizadas. Luego de
|
||||||
|
# actualizar el listado de instancias, bloquea las instancias en cada
|
||||||
|
# sitio que tenga el fediblock habilitado.
|
||||||
|
class FediblockFetchJob < ApplicationJob
|
||||||
|
self.priority = 50
|
||||||
|
|
||||||
|
def perform
|
||||||
|
ActivityPub::Fediblock.find_each do |fediblock|
|
||||||
|
fediblock.process!
|
||||||
|
|
||||||
|
hostnames_added = fediblock.hostnames - fediblock.hostnames_was
|
||||||
|
|
||||||
|
# No hacer nada si no cambió con respecto a la versión anterior
|
||||||
|
next if hostnames_added.empty?
|
||||||
|
|
||||||
|
ActivityPub::FediblockUpdatedJob.perform_later(fediblock: fediblock, hostnames: hostnames_added)
|
||||||
|
rescue ActivityPub::Fediblock::FediblockDownloadError => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { fediblock: fediblock.title })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
29
app/jobs/activity_pub/fediblock_updated_job.rb
Normal file
29
app/jobs/activity_pub/fediblock_updated_job.rb
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Se encarga de mantener sincronizadas las listas de instancias
|
||||||
|
# de los fediblocks con los sitios que las tengan activadas.
|
||||||
|
#
|
||||||
|
# También va a asociar las listas con todos los sitios que tengan la
|
||||||
|
# Social Inbox habilitada.
|
||||||
|
class ActivityPub
|
||||||
|
class FediblockUpdatedJob < ApplicationJob
|
||||||
|
self.priority = 50
|
||||||
|
|
||||||
|
# @param :fediblock [ActivityPub::Fediblock]
|
||||||
|
# @param :hostnames [Array<String>]
|
||||||
|
def perform(fediblock:, hostnames:)
|
||||||
|
instances = ActivityPub::Instance.where(hostname: hostnames)
|
||||||
|
|
||||||
|
# Todos los sitios con la Social Inbox habilitada
|
||||||
|
Site.where(id: DeploySocialDistributedPress.pluck(:site_id)).find_each do |site|
|
||||||
|
# Crea el estado si no existía
|
||||||
|
fediblock_state = site.fediblock_states.find_or_create_by(fediblock: fediblock)
|
||||||
|
|
||||||
|
# No hace nada con los deshabilitados
|
||||||
|
next unless fediblock_state.enabled?
|
||||||
|
|
||||||
|
ActivityPub::InstanceModerationJob.perform_later(site: site, hostnames: hostnames)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -9,6 +9,8 @@
|
||||||
# autenticación.
|
# autenticación.
|
||||||
class ActivityPub
|
class ActivityPub
|
||||||
class FetchJob < ApplicationJob
|
class FetchJob < ApplicationJob
|
||||||
|
self.priority = 50
|
||||||
|
|
||||||
def perform(site:, object:)
|
def perform(site:, object:)
|
||||||
ActivityPub::Object.transaction do
|
ActivityPub::Object.transaction do
|
||||||
return if object.activity_pubs.where(aasm_state: 'removed').count.positive?
|
return if object.activity_pubs.where(aasm_state: 'removed').count.positive?
|
||||||
|
|
38
app/jobs/activity_pub/instance_fetch_job.rb
Normal file
38
app/jobs/activity_pub/instance_fetch_job.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub
|
||||||
|
# Obtiene o actualiza los datos de una instancia. Usamos un cliente
|
||||||
|
# de ActivityPub porque la instancia podría estar en federación
|
||||||
|
# limitada.
|
||||||
|
class InstanceFetchJob < ApplicationJob
|
||||||
|
self.priority = 100
|
||||||
|
|
||||||
|
def perform(site:, instance:)
|
||||||
|
%w[/api/v2/instance /api/v1/instance].each do |api|
|
||||||
|
uri = SocialInbox.generate_uri(instance.hostname) do |u|
|
||||||
|
u.path = api
|
||||||
|
end
|
||||||
|
|
||||||
|
response = site.social_inbox.dereferencer.get(uri: uri)
|
||||||
|
|
||||||
|
next unless response.ok?
|
||||||
|
# @todo Validate schema
|
||||||
|
next unless response.parsed_response.is_a?(DistributedPress::V1::Social::ReferencedObject)
|
||||||
|
|
||||||
|
instance.update(content: response.parsed_response.object)
|
||||||
|
|
||||||
|
break
|
||||||
|
rescue BRS::BaseError,
|
||||||
|
Errno::ECONNREFUSED,
|
||||||
|
HTTParty::Error,
|
||||||
|
JSON::JSONError,
|
||||||
|
Net::OpenTimeout,
|
||||||
|
OpenSSL::OpenSSLError,
|
||||||
|
SocketError,
|
||||||
|
Errno::ENETUNREACH => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { instance: uri })
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
31
app/jobs/activity_pub/instance_moderation_job.rb
Normal file
31
app/jobs/activity_pub/instance_moderation_job.rb
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub
|
||||||
|
# Bloquea varias instancias de una sola vez
|
||||||
|
class InstanceModerationJob < ApplicationJob
|
||||||
|
self.priority = 50
|
||||||
|
|
||||||
|
# @param :site [Site]
|
||||||
|
# @param :hostnames [Array<String>]
|
||||||
|
def perform(site:, hostnames:)
|
||||||
|
# Crear las instancias que no existan todavía
|
||||||
|
hostnames.each do |hostname|
|
||||||
|
ActivityPub::Instance.find_or_create_by(hostname: hostname)
|
||||||
|
end
|
||||||
|
|
||||||
|
instances = ActivityPub::Instance.where(hostname: hostnames)
|
||||||
|
|
||||||
|
Site.transaction do
|
||||||
|
# Crea todas las moderaciones de instancia con un estado por
|
||||||
|
# defecto si no existen
|
||||||
|
instances.find_each do |instance|
|
||||||
|
# Esto bloquea cada una individualmente en la Social Inbox,
|
||||||
|
# idealmente son pocas instancias las que aparecen.
|
||||||
|
site.instance_moderations.find_or_create_by(instance: instance).tap do |instance_moderation|
|
||||||
|
instance_moderation.block! if instance_moderation.may_block?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
31
app/jobs/activity_pub/remote_flag_job.rb
Normal file
31
app/jobs/activity_pub/remote_flag_job.rb
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Envía un reporte directamente a la instancia remota
|
||||||
|
#
|
||||||
|
# @todo El panel debería ser su propia instancia y firmar sus propios
|
||||||
|
# mensajes.
|
||||||
|
# @todo Como la Social Inbox no soporta enviar actividades
|
||||||
|
# a destinataries que no sean seguidores, enviamos el reporte
|
||||||
|
# directamente a la instancia.
|
||||||
|
# @see {https://github.com/hyphacoop/social.distributed.press/issues/14}
|
||||||
|
class ActivityPub
|
||||||
|
class RemoteFlagJob < ApplicationJob
|
||||||
|
self.priority = 30
|
||||||
|
|
||||||
|
def perform(remote_flag:)
|
||||||
|
return if remote_flag.can_queue?
|
||||||
|
|
||||||
|
remote_flag.queue!
|
||||||
|
|
||||||
|
client = remote_flag.site.social_inbox.client_for(remote_flag.actor&.content['inbox'])
|
||||||
|
response = client.post(endpoint: '', body: remote_flag.content)
|
||||||
|
|
||||||
|
raise 'No se pudo enviar el reporte' unless response.ok?
|
||||||
|
|
||||||
|
remote_flag.send!
|
||||||
|
rescue Exception => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { remote_flag: remote_flag.id, response: response.parsed_response })
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -9,15 +9,35 @@
|
||||||
# @see {https://www.w3.org/TR/activitypub/#client-to-server-interactions}
|
# @see {https://www.w3.org/TR/activitypub/#client-to-server-interactions}
|
||||||
class ActivityPub < ApplicationRecord
|
class ActivityPub < ApplicationRecord
|
||||||
include AASM
|
include AASM
|
||||||
|
include AasmEventsConcern
|
||||||
|
|
||||||
|
IGNORED_EVENTS = %i[remove]
|
||||||
|
IGNORED_STATES = %i[removed]
|
||||||
|
|
||||||
|
belongs_to :instance
|
||||||
belongs_to :site
|
belongs_to :site
|
||||||
belongs_to :object, polymorphic: true
|
belongs_to :object, polymorphic: true
|
||||||
|
belongs_to :actor
|
||||||
|
belongs_to :remote_flag, class_name: 'ActivityPub::RemoteFlag'
|
||||||
has_many :activities
|
has_many :activities
|
||||||
|
|
||||||
validates :site_id, presence: true
|
validates :site_id, presence: true
|
||||||
validates :object_id, presence: true
|
validates :object_id, presence: true
|
||||||
validates :aasm_state, presence: true, inclusion: { in: %w[paused approved rejected reported removed] }
|
validates :aasm_state, presence: true, inclusion: { in: %w[paused approved rejected reported removed] }
|
||||||
|
|
||||||
|
accepts_nested_attributes_for :remote_flag
|
||||||
|
|
||||||
|
# Encuentra la URI de un objeto
|
||||||
|
#
|
||||||
|
# @return [String, nil]
|
||||||
|
def self.uri_from_object(object)
|
||||||
|
case object
|
||||||
|
when Array then uri_from_object(object.first)
|
||||||
|
when String then object
|
||||||
|
when Hash then (object['id'] || object[:id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
aasm do
|
aasm do
|
||||||
# Todavía no hay una decisión sobre el objeto
|
# Todavía no hay una decisión sobre el objeto
|
||||||
state :paused, initial: true
|
state :paused, initial: true
|
||||||
|
@ -41,25 +61,35 @@ class ActivityPub < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Si un objeto previamente aprobado fue actualizado, volvemos a
|
# La actividad se aprueba, informándole a la Social Inbox que está
|
||||||
# pausarlo.
|
# aprobada. También recibimos la aprobación via
|
||||||
event :pause do
|
# webhook a modo de confirmación.
|
||||||
transitions from: %i[approved rejected], to: :paused
|
|
||||||
end
|
|
||||||
|
|
||||||
# La actividad se aprueba
|
|
||||||
event :approve do
|
event :approve do
|
||||||
transitions from: %i[paused rejected], to: :approved
|
transitions from: %i[paused], to: :approved
|
||||||
|
|
||||||
|
before do
|
||||||
|
raise AASM::InvalidTransition unless
|
||||||
|
site.social_inbox.inbox.accept(id: object.uri).ok?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# La actividad fue rechazada
|
# La actividad fue rechazada
|
||||||
event :reject do
|
event :reject do
|
||||||
transitions from: %i[paused approved], to: :rejected
|
transitions from: %i[paused approved], to: :rejected
|
||||||
|
|
||||||
|
before do
|
||||||
|
raise AASM::InvalidTransition unless
|
||||||
|
site.social_inbox.inbox.reject(id: object.uri).ok?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Solo podemos reportarla luego de rechazarla
|
# Solo podemos reportarla luego de rechazarla
|
||||||
event :report do
|
event :report do
|
||||||
transitions from: :rejected, to: :reported
|
transitions from: :rejected, to: :reported
|
||||||
|
|
||||||
|
before do
|
||||||
|
ActivityPub::RemoteFlagJob.perform_later(remote_flag: remote_flag) if remote_flag.waiting?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,6 +16,7 @@ class ActivityPub
|
||||||
include ActivityPub::Concerns::JsonLdConcern
|
include ActivityPub::Concerns::JsonLdConcern
|
||||||
|
|
||||||
belongs_to :activity_pub
|
belongs_to :activity_pub
|
||||||
|
belongs_to :actor, touch: true
|
||||||
has_one :object, through: :activity_pub
|
has_one :object, through: :activity_pub
|
||||||
|
|
||||||
validates :activity_pub_id, presence: true
|
validates :activity_pub_id, presence: true
|
||||||
|
|
|
@ -3,11 +3,22 @@
|
||||||
class ActivityPub
|
class ActivityPub
|
||||||
class Activity
|
class Activity
|
||||||
class Delete < ActivityPub::Activity
|
class Delete < ActivityPub::Activity
|
||||||
# Si estamos eliminando el objeto, tenemos que vaciar su contenido y
|
# Los Delete se refieren a objetos. Al eliminar un objeto,
|
||||||
# cambiar el estado a borrado.
|
# cancelamos todas las actividades que tienen relacionadas.
|
||||||
|
#
|
||||||
|
# XXX: La actividad tiene una firma, pero la implementación no
|
||||||
|
# está recomendada
|
||||||
|
#
|
||||||
|
# @todo Validar que le Actor corresponda con los objetos. Esto ya
|
||||||
|
# lo haría la Social Inbox por nosotres.
|
||||||
|
# @see {https://docs.joinmastodon.org/spec/security/#ld}
|
||||||
def update_activity_pub_state!
|
def update_activity_pub_state!
|
||||||
|
ActivityPub.transaction do
|
||||||
|
ActivityPub::Object.find_by(uri: ActivityPub.uri_from_object(content['object']))&.activity_pubs&.find_each(&:remove!)
|
||||||
|
|
||||||
activity_pub.remove!
|
activity_pub.remove!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -15,6 +15,8 @@ class ActivityPub
|
||||||
# Sin embargo, estas acciones nunca deberían llegar a nuestra
|
# Sin embargo, estas acciones nunca deberían llegar a nuestra
|
||||||
# Inbox.
|
# Inbox.
|
||||||
#
|
#
|
||||||
|
# @todo Validar que le Actor corresponda con los objetos. Esto ya
|
||||||
|
# lo haría la Social Inbox por nosotres.
|
||||||
# @see {https://github.com/hyphacoop/social.distributed.press/issues/43}
|
# @see {https://github.com/hyphacoop/social.distributed.press/issues/43}
|
||||||
def update_activity_pub_state!
|
def update_activity_pub_state!
|
||||||
ActivityPub.transaction do
|
ActivityPub.transaction do
|
||||||
|
|
|
@ -10,6 +10,20 @@ class ActivityPub
|
||||||
include ActivityPub::Concerns::JsonLdConcern
|
include ActivityPub::Concerns::JsonLdConcern
|
||||||
|
|
||||||
belongs_to :instance
|
belongs_to :instance
|
||||||
|
has_many :actor_moderation
|
||||||
has_many :activity_pubs, as: :object
|
has_many :activity_pubs, as: :object
|
||||||
|
has_many :activities
|
||||||
|
has_many :remote_flags
|
||||||
|
|
||||||
|
# Obtiene el nombre de la Actor como mención, solo si obtuvimos el
|
||||||
|
# contenido de antemano.
|
||||||
|
#
|
||||||
|
# @return [String, nil]
|
||||||
|
def mention
|
||||||
|
return if content['preferredUsername'].blank?
|
||||||
|
return if instance.blank?
|
||||||
|
|
||||||
|
@mention ||= "@#{content['preferredUsername']}@#{instance.hostname}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
82
app/models/activity_pub/fediblock.rb
Normal file
82
app/models/activity_pub/fediblock.rb
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'httparty'
|
||||||
|
|
||||||
|
# Listas de bloqueo y sus URLs de descarga
|
||||||
|
class ActivityPub
|
||||||
|
class Fediblock < ApplicationRecord
|
||||||
|
class Client
|
||||||
|
include ::HTTParty
|
||||||
|
|
||||||
|
# @param url [String]
|
||||||
|
# @return [HTTParty::Response]
|
||||||
|
def get(url)
|
||||||
|
self.class.get(url, parser: csv_parser)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Procesa el CSV
|
||||||
|
#
|
||||||
|
# @return [Proc]
|
||||||
|
def csv_parser
|
||||||
|
@csv_parser ||=
|
||||||
|
begin
|
||||||
|
require 'csv'
|
||||||
|
|
||||||
|
proc do |body, _|
|
||||||
|
CSV.parse(body, headers: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class FediblockDownloadError < ::StandardError; end
|
||||||
|
|
||||||
|
validates_presence_of :title, :url, :download_url, :format
|
||||||
|
validates_inclusion_of :format, in: %w[mastodon fediblock]
|
||||||
|
|
||||||
|
HOSTNAME_HEADERS = {
|
||||||
|
'mastodon' => '#domain',
|
||||||
|
'fediblock' => 'domain'
|
||||||
|
}
|
||||||
|
|
||||||
|
def client
|
||||||
|
@client ||= Client.new
|
||||||
|
end
|
||||||
|
|
||||||
|
# Todas las instancias de este fediblock
|
||||||
|
def instances
|
||||||
|
ActivityPub::Instance.where(hostname: hostnames)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Descarga la lista y crea las instancias con el estado necesario
|
||||||
|
def process!
|
||||||
|
response = client.get(download_url)
|
||||||
|
|
||||||
|
raise FediblockDownloadError unless response.ok?
|
||||||
|
|
||||||
|
Fediblock.transaction do
|
||||||
|
csv = response.parsed_response
|
||||||
|
process_csv! csv
|
||||||
|
|
||||||
|
update(hostnames: csv.map { |r| r[hostname_header] })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def hostname_header
|
||||||
|
HOSTNAME_HEADERS[format]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Crea o encuentra instancias que ya existían y las bloquea
|
||||||
|
#
|
||||||
|
# @param csv [CSV::Table]
|
||||||
|
def process_csv!(csv)
|
||||||
|
csv.each do |row|
|
||||||
|
ActivityPub::Instance.find_or_create_by(hostname: row[hostname_header]).tap do |i|
|
||||||
|
i.block! if i.may_block?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -13,11 +13,28 @@ class ActivityPub
|
||||||
|
|
||||||
has_many :activity_pubs
|
has_many :activity_pubs
|
||||||
has_many :actors
|
has_many :actors
|
||||||
|
has_many :instance_moderations
|
||||||
|
|
||||||
|
# XXX: Mantenemos esto por si queremos bloquear una instancia a
|
||||||
|
# nivel general
|
||||||
aasm do
|
aasm do
|
||||||
state :paused, initial: true
|
state :paused, initial: true
|
||||||
state :allowed
|
state :allowed
|
||||||
state :blocked
|
state :blocked
|
||||||
|
|
||||||
|
# Al pasar una instancia a bloqueo, quiere decir que todos los
|
||||||
|
# sitios adoptan esta lista
|
||||||
|
event :block do
|
||||||
|
transitions from: %i[paused allowed], to: :blocked
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_name
|
||||||
|
"@*@#{hostname}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def uri
|
||||||
|
@uri ||= "https://#{hostname}/"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,5 +6,12 @@ class ActivityPub
|
||||||
include ActivityPub::Concerns::JsonLdConcern
|
include ActivityPub::Concerns::JsonLdConcern
|
||||||
|
|
||||||
has_many :activity_pubs, as: :object
|
has_many :activity_pubs, as: :object
|
||||||
|
|
||||||
|
# Encontrar le Actor por su relación con el objeto
|
||||||
|
#
|
||||||
|
# @return [ActivityPub::Actor,nil]
|
||||||
|
def actor
|
||||||
|
ActivityPub::Actor.find_by(uri: content['actor'])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
46
app/models/activity_pub/remote_flag.rb
Normal file
46
app/models/activity_pub/remote_flag.rb
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub
|
||||||
|
class RemoteFlag < ApplicationRecord
|
||||||
|
include AASM
|
||||||
|
include AasmEventsConcern
|
||||||
|
|
||||||
|
aasm do
|
||||||
|
state :waiting, initial: true
|
||||||
|
state :queued
|
||||||
|
state :sent
|
||||||
|
|
||||||
|
event :queue do
|
||||||
|
transitions from: :waiting, to: :queued
|
||||||
|
end
|
||||||
|
|
||||||
|
event :send do
|
||||||
|
transitions from: :queued, to: :sent
|
||||||
|
end
|
||||||
|
|
||||||
|
event :resend do
|
||||||
|
transitions from: :sent, to: :waiting
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
belongs_to :actor
|
||||||
|
belongs_to :site
|
||||||
|
|
||||||
|
has_one :actor_moderation
|
||||||
|
has_many :activity_pubs
|
||||||
|
# XXX: source_type es obligatorio para el `through`
|
||||||
|
has_many :objects, through: :activity_pubs, source_type: 'ActivityPub::Object::Note'
|
||||||
|
|
||||||
|
# Genera la actividad a enviar
|
||||||
|
def content
|
||||||
|
{
|
||||||
|
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'id' => Rails.application.routes.url_helpers.v1_activity_pub_remote_flag_url(self, host: site.social_inbox_hostname),
|
||||||
|
'type' => 'Flag',
|
||||||
|
'actor' => ENV.fetch('PANEL_ACTOR_ID') { "https://#{ENV['SUTTY']}/about.jsonld" },
|
||||||
|
'content' => message.to_s,
|
||||||
|
'object' => [ actor.uri ] + objects.pluck(:uri)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
87
app/models/actor_moderation.rb
Normal file
87
app/models/actor_moderation.rb
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Mantiene la relación entre Site y Actor
|
||||||
|
class ActorModeration < ApplicationRecord
|
||||||
|
include AASM
|
||||||
|
include AasmEventsConcern
|
||||||
|
|
||||||
|
IGNORED_EVENTS = []
|
||||||
|
IGNORED_STATES = []
|
||||||
|
|
||||||
|
belongs_to :site
|
||||||
|
belongs_to :remote_flag, class_name: 'ActivityPub::RemoteFlag'
|
||||||
|
belongs_to :actor, class_name: 'ActivityPub::Actor'
|
||||||
|
|
||||||
|
accepts_nested_attributes_for :remote_flag
|
||||||
|
|
||||||
|
# Bloquea todes les Actores bloqueables
|
||||||
|
def self.block_all!
|
||||||
|
self.update_all(aasm_state: 'blocked', updated_at: Time.now)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.pause_all!
|
||||||
|
self.update_all(aasm_state: 'paused', updated_at: Time.now)
|
||||||
|
end
|
||||||
|
|
||||||
|
aasm do
|
||||||
|
state :paused, initial: true
|
||||||
|
state :allowed
|
||||||
|
state :blocked
|
||||||
|
state :reported
|
||||||
|
|
||||||
|
event :pause do
|
||||||
|
transitions from: %i[allowed blocked reported], to: :paused
|
||||||
|
|
||||||
|
before do
|
||||||
|
pause_remotely!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
event :allow do
|
||||||
|
transitions from: %i[paused blocked reported], to: :allowed
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow_remotely!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
event :block do
|
||||||
|
transitions from: %i[paused allowed], to: :blocked
|
||||||
|
|
||||||
|
before do
|
||||||
|
block_remotely!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Al reportar, necesitamos asociar una RemoteFlag para poder
|
||||||
|
# enviarla.
|
||||||
|
event :report do
|
||||||
|
transitions from: %i[blocked], to: :reported
|
||||||
|
|
||||||
|
before do
|
||||||
|
ActivityPub::RemoteFlagJob.perform_later(remote_flag: remote_flag) if remote_flag.waiting?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def pause_remotely!
|
||||||
|
raise AASM::InvalidTransition 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 AASM::InvalidTransition 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 AASM::InvalidTransition unless
|
||||||
|
actor.mention &&
|
||||||
|
site.social_inbox.allowlist.delete(list: [actor.mention]).ok? &&
|
||||||
|
site.social_inbox.blocklist.post(list: [actor.mention]).ok?
|
||||||
|
end
|
||||||
|
end
|
31
app/models/concerns/aasm_events_concern.rb
Normal file
31
app/models/concerns/aasm_events_concern.rb
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module AasmEventsConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
# Todos los eventos de la máquina de estados
|
||||||
|
#
|
||||||
|
# @return [Array<Symbol>]
|
||||||
|
def self.events
|
||||||
|
aasm.events.map(&:name) - self::IGNORED_EVENTS
|
||||||
|
end
|
||||||
|
|
||||||
|
# Encuentra todos los eventos que se pueden ejecutar con el filtro
|
||||||
|
# actual.
|
||||||
|
#
|
||||||
|
# @return [Array<Symbol>]
|
||||||
|
def self.transitionable_events(current_state)
|
||||||
|
self.events.select do |event|
|
||||||
|
aasm.events.find { |x| x.name == event }.transitions_from_state? current_state
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Todos los estados de la máquina de estados
|
||||||
|
#
|
||||||
|
# @return [Array<Symbol>]
|
||||||
|
def self.states
|
||||||
|
aasm.states.map(&:name) - self::IGNORED_STATES
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,7 +5,7 @@ module Tienda
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
encrypts :tienda_api_key
|
has_encrypted :tienda_api_key
|
||||||
|
|
||||||
def tienda?
|
def tienda?
|
||||||
tienda_api_key.present? && tienda_url.present?
|
tienda_api_key.present? && tienda_url.present?
|
||||||
|
|
|
@ -8,6 +8,7 @@ class DeploySocialDistributedPress < Deploy
|
||||||
DEPENDENCIES = %i[deploy_distributed_press deploy_rsync deploy_full_rsync].freeze
|
DEPENDENCIES = %i[deploy_distributed_press deploy_rsync deploy_full_rsync].freeze
|
||||||
|
|
||||||
after_save :create_hooks!
|
after_save :create_hooks!
|
||||||
|
after_create :enable_fediblocks!
|
||||||
|
|
||||||
# Envía las notificaciones
|
# Envía las notificaciones
|
||||||
def deploy(output: false)
|
def deploy(output: false)
|
||||||
|
@ -57,13 +58,6 @@ class DeploySocialDistributedPress < Deploy
|
||||||
|
|
||||||
private
|
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
|
# Crea los hooks en la Social Inbox para que nos avise de actividades
|
||||||
# nuevas
|
# nuevas
|
||||||
#
|
#
|
||||||
|
@ -79,7 +73,7 @@ class DeploySocialDistributedPress < Deploy
|
||||||
webhook_class.new.call({
|
webhook_class.new.call({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: Rails.application.routes.url_helpers.public_send(
|
url: Rails.application.routes.url_helpers.public_send(
|
||||||
event_url, site_id: site.name, host: api_hostname
|
event_url, site_id: site.name, host: site.social_inbox_hostname
|
||||||
),
|
),
|
||||||
headers: {
|
headers: {
|
||||||
'X-Social-Inbox': rol.token
|
'X-Social-Inbox': rol.token
|
||||||
|
@ -95,4 +89,16 @@ class DeploySocialDistributedPress < Deploy
|
||||||
ExceptionNotifier.notify_exception(e, data: { site_id: site.name, usuarie_id: rol.usuarie_id })
|
ExceptionNotifier.notify_exception(e, data: { site_id: site.name, usuarie_id: rol.usuarie_id })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Habilita todos los fediblocks disponibles.
|
||||||
|
#
|
||||||
|
# @todo Hacer que algunos sean opcionales
|
||||||
|
# @todo Mover a un Job
|
||||||
|
def enable_fediblocks!
|
||||||
|
ActivityPub::Fediblock.find_each do |fediblock|
|
||||||
|
site.fediblock_states.find_or_create_by(fediblock: fediblock).tap do |state|
|
||||||
|
state.enable! if state.may_enable?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
103
app/models/fediblock_state.rb
Normal file
103
app/models/fediblock_state.rb
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Relación entre Fediblocks y Sites.
|
||||||
|
#
|
||||||
|
# Cuando se habilita un Fediblock, tenemos que asociar todas sus
|
||||||
|
# instancias con el sitio y bloquearlas. Cuando se deshabilita, la
|
||||||
|
# relación ya está creada y se va actualizando.
|
||||||
|
#
|
||||||
|
# @see ActivityPub::FediblockUpdatedJob
|
||||||
|
class FediblockState < ApplicationRecord
|
||||||
|
include AASM
|
||||||
|
|
||||||
|
belongs_to :site
|
||||||
|
belongs_to :fediblock, class_name: 'ActivityPub::Fediblock'
|
||||||
|
|
||||||
|
# El efecto secundario de esta máquina de estados es modificar el
|
||||||
|
# estado de moderación de cada instancia en el sitio. Nos salteamos
|
||||||
|
# los hooks de los eventos individuales.
|
||||||
|
aasm do
|
||||||
|
# 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
|
||||||
|
state :enabled
|
||||||
|
|
||||||
|
event :enable do
|
||||||
|
transitions from: :disabled, to: :enabled
|
||||||
|
|
||||||
|
before do
|
||||||
|
enable_remotely!
|
||||||
|
|
||||||
|
# Al actualizar el estado en masa garantizamos que las
|
||||||
|
# instancias que ya existen queden sincronizadas con el bloqueo
|
||||||
|
# en masa que acabamos de hacer.
|
||||||
|
instance_moderations.block_all!
|
||||||
|
|
||||||
|
# Luego esta tarea crea las que falten e ignora las que ya se
|
||||||
|
# bloquearon.
|
||||||
|
ActivityPub::InstanceModerationJob.perform_now(site: site, hostnames: fediblock.hostnames)
|
||||||
|
|
||||||
|
# Bloquear a todes les Actores de las instancias bloqueadas para
|
||||||
|
# indicarle a le usuarie que les tiene que desbloquear
|
||||||
|
# manualmente.
|
||||||
|
ActorModeration.where(actor_id: actor_ids).paused.block_all!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Al deshabilitar, las listas pasan a modo pausa.
|
||||||
|
#
|
||||||
|
# @todo No cambiar el estado si se habían habilitado manualmente,
|
||||||
|
# pero esto implica que tenemos que encontrar las que sí y quitarlas
|
||||||
|
# de list_names
|
||||||
|
event :disable do
|
||||||
|
transitions from: :enabled, to: :disabled
|
||||||
|
|
||||||
|
before do
|
||||||
|
disable_remotely!
|
||||||
|
|
||||||
|
instance_moderations.pause_all!
|
||||||
|
|
||||||
|
# Volver a pausar todes les actores de esta instancia que fueron
|
||||||
|
# bloqueades.
|
||||||
|
ActorModeration.where(actor_id: actor_ids).blocked.pause_all!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def actor_ids
|
||||||
|
ActivityPub::Actor.where(instance_id: instance_ids).pluck(:id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def instance_ids
|
||||||
|
fediblock.instances.pluck(:id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Todas las instancias de moderación de este sitio
|
||||||
|
def instance_moderations
|
||||||
|
site.instance_moderations.where(instance_id: instance_ids)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Array<String>]
|
||||||
|
def list_names
|
||||||
|
@list_names ||= fediblock.instances.map do |instance|
|
||||||
|
"@*@#{instance}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Al deshabilitar, las instancias pasan a ser analizadas caso por caso
|
||||||
|
def disable_remotely!
|
||||||
|
raise AASM::InvalidTransition unless
|
||||||
|
site.social_inbox.blocklist.delete(list: list_names).ok? &&
|
||||||
|
site.social_inbox.allowlist.delete(list: list_names).ok?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Al habilitar, se bloquean todas las instancias de la lista
|
||||||
|
def enable_remotely!
|
||||||
|
raise AASM::InvalidTransition unless
|
||||||
|
site.social_inbox.blocklist.post(list: list_names).ok? &&
|
||||||
|
site.social_inbox.allowlist.delete(list: list_names).ok?
|
||||||
|
end
|
||||||
|
end
|
85
app/models/instance_moderation.rb
Normal file
85
app/models/instance_moderation.rb
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Mantiene el registro de relaciones entre sitios e instancias
|
||||||
|
class InstanceModeration < ApplicationRecord
|
||||||
|
include AASM
|
||||||
|
include AasmEventsConcern
|
||||||
|
|
||||||
|
IGNORED_EVENTS = []
|
||||||
|
IGNORED_STATES = []
|
||||||
|
|
||||||
|
belongs_to :site
|
||||||
|
belongs_to :instance, class_name: 'ActivityPub::Instance'
|
||||||
|
|
||||||
|
# Traer todas las instancias bloqueables, según la máquina de estados,
|
||||||
|
# todas las que no estén bloqueadas ya.
|
||||||
|
scope :may_block, -> { where.not(aasm_state: 'blocked') }
|
||||||
|
scope :may_pause, -> { where.not(aasm_state: 'paused') }
|
||||||
|
|
||||||
|
# Bloquear instancias en masa
|
||||||
|
def self.block_all!
|
||||||
|
self.may_block.update_all(aasm_state: 'blocked', updated_at: Time.now)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Pausar instancias en masa
|
||||||
|
def self.pause_all!
|
||||||
|
self.may_pause.update_all(aasm_state: 'paused', updated_at: Time.now)
|
||||||
|
end
|
||||||
|
|
||||||
|
aasm do
|
||||||
|
state :paused, initial: true
|
||||||
|
state :allowed
|
||||||
|
state :blocked
|
||||||
|
|
||||||
|
event :pause do
|
||||||
|
transitions from: %i[allowed blocked], to: :paused
|
||||||
|
|
||||||
|
before do
|
||||||
|
pause_remotely!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
event :allow do
|
||||||
|
transitions from: %i[paused blocked], to: :allowed
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow_remotely!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
event :block do
|
||||||
|
transitions from: %i[paused allowed], to: :blocked
|
||||||
|
|
||||||
|
before do
|
||||||
|
block_remotely!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Elimina la instancia de todas las listas
|
||||||
|
#
|
||||||
|
# @return [Boolean]
|
||||||
|
def pause_remotely!
|
||||||
|
raise AASM::InvalidTransition 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 AASM::InvalidTransition 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 AASM::InvalidTransition unless
|
||||||
|
site.social_inbox.blocklist.delete(list: [instance.list_name]).ok? &&
|
||||||
|
site.social_inbox.allowlist.post(list: [instance.list_name]).ok?
|
||||||
|
end
|
||||||
|
end
|
|
@ -198,8 +198,8 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||||
|
|
||||||
sanitizer
|
sanitizer
|
||||||
.sanitize(string.tr("\r", '').unicode_normalize,
|
.sanitize(string.tr("\r", '').unicode_normalize,
|
||||||
tags: allowed_tags,
|
tags: Sutty::ALLOWED_TAGS,
|
||||||
attributes: allowed_attributes)
|
attributes: Sutty::ALLOWED_ATTRIBUTES)
|
||||||
.strip
|
.strip
|
||||||
.html_safe
|
.html_safe
|
||||||
end
|
end
|
||||||
|
@ -208,16 +208,6 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||||
@sanitizer ||= Rails::Html::Sanitizer.safe_list_sanitizer.new
|
@sanitizer ||= Rails::Html::Sanitizer.safe_list_sanitizer.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def allowed_attributes
|
|
||||||
@allowed_attributes ||= %w[style href src alt controls data-align data-multimedia data-multimedia-inner id
|
|
||||||
name rel target referrerpolicy class colspan rowspan role data-turbo start type reversed].freeze
|
|
||||||
end
|
|
||||||
|
|
||||||
def allowed_tags
|
|
||||||
@allowed_tags ||= %w[strong em del u mark p h1 h2 h3 h4 h5 h6 ul ol li img iframe audio video div figure blockquote
|
|
||||||
figcaption a sub sup small table thead tbody tfoot tr th td br code].freeze
|
|
||||||
end
|
|
||||||
|
|
||||||
# Decifra el valor
|
# Decifra el valor
|
||||||
#
|
#
|
||||||
# XXX: Otros tipos de valores necesitan implementar su propio método
|
# XXX: Otros tipos de valores necesitan implementar su propio método
|
||||||
|
|
|
@ -17,7 +17,7 @@ class Site < ApplicationRecord
|
||||||
# tiene acceso pero los datos se guardan cifrados en el sitio. Esto
|
# tiene acceso pero los datos se guardan cifrados en el sitio. Esto
|
||||||
# protege información privada en repositorios públicos, pero no la
|
# protege información privada en repositorios públicos, pero no la
|
||||||
# protege de acceso al panel de Sutty!
|
# protege de acceso al panel de Sutty!
|
||||||
encrypts :private_key
|
has_encrypted :private_key
|
||||||
|
|
||||||
validates :name, uniqueness: true, hostname: {
|
validates :name, uniqueness: true, hostname: {
|
||||||
allow_root_label: true
|
allow_root_label: true
|
||||||
|
|
|
@ -5,7 +5,7 @@ class Site
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
encrypts :api_key
|
has_encrypted :api_key
|
||||||
before_save :add_api_key_if_missing!
|
before_save :add_api_key_if_missing!
|
||||||
|
|
||||||
# Genera mensajes secretos que podemos usar para la API de cada
|
# Genera mensajes secretos que podemos usar para la API de cada
|
||||||
|
|
|
@ -8,9 +8,14 @@ class Site
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
encrypts :private_key_pem
|
has_encrypted :private_key_pem
|
||||||
|
|
||||||
has_many :activity_pubs
|
has_many :activity_pubs
|
||||||
|
has_many :instance_moderations
|
||||||
|
has_many :actor_moderations
|
||||||
|
has_many :fediblock_states
|
||||||
|
has_many :instances, through: :instance_moderations
|
||||||
|
has_many :remote_flags, class_name: 'ActivityPub::RemoteFlag'
|
||||||
|
|
||||||
before_save :generate_private_key_pem!, unless: :private_key_pem?
|
before_save :generate_private_key_pem!, unless: :private_key_pem?
|
||||||
|
|
||||||
|
@ -19,6 +24,13 @@ class Site
|
||||||
@social_inbox ||= SocialInbox.new(site: self)
|
@social_inbox ||= SocialInbox.new(site: self)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Obtiene el hostname de la API de Sutty
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
|
def social_inbox_hostname
|
||||||
|
Rails.application.routes.default_url_options[:host].sub('panel', 'api')
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Genera la llave privada y la almacena
|
# Genera la llave privada y la almacena
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'distributed_press/v1/social/client'
|
require 'distributed_press/v1/social/client'
|
||||||
|
require 'distributed_press/v1/social/allowlist'
|
||||||
|
require 'distributed_press/v1/social/blocklist'
|
||||||
require 'distributed_press/v1/social/hook'
|
require 'distributed_press/v1/social/hook'
|
||||||
require 'distributed_press/v1/social/inbox'
|
require 'distributed_press/v1/social/inbox'
|
||||||
require 'distributed_press/v1/social/dereferencer'
|
require 'distributed_press/v1/social/dereferencer'
|
||||||
|
@ -28,15 +30,27 @@ class SocialInbox
|
||||||
end
|
end
|
||||||
|
|
||||||
def actor_id
|
def actor_id
|
||||||
@actor_id ||= generate_uri do |uri|
|
@actor_id ||= SocialInbox.generate_uri(hostname) do |uri|
|
||||||
uri.path = '/about.jsonld'
|
uri.path = '/about.jsonld'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# @return [DistributedPress::V1::Social::Client]
|
# @return [DistributedPress::V1::Social::Client]
|
||||||
def client
|
def client
|
||||||
@client ||= DistributedPress::V1::Social::Client.new(
|
@client ||= client_for site.config.dig('activity_pub', 'url')
|
||||||
url: site.config.dig('activity_pub', 'url'),
|
end
|
||||||
|
|
||||||
|
# Permite enviar mensajes directo a otro servidor
|
||||||
|
#
|
||||||
|
# @param url [String]
|
||||||
|
# @return [DistributedPress::V1::Social::Client]
|
||||||
|
def client_for(url)
|
||||||
|
raise "Falló generar un cliente" if url.blank?
|
||||||
|
|
||||||
|
@client_for ||= {}
|
||||||
|
@client_for[url] ||=
|
||||||
|
DistributedPress::V1::Social::Client.new(
|
||||||
|
url: url,
|
||||||
public_key_url: public_key_url,
|
public_key_url: public_key_url,
|
||||||
private_key_pem: site.private_key_pem,
|
private_key_pem: site.private_key_pem,
|
||||||
logger: Rails.logger,
|
logger: Rails.logger,
|
||||||
|
@ -59,9 +73,19 @@ class SocialInbox
|
||||||
@hook ||= DistributedPress::V1::Social::Hook.new(client: client, actor: actor)
|
@hook ||= DistributedPress::V1::Social::Hook.new(client: client, actor: actor)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [DistributedPress::V1::Social::Allowlist]
|
||||||
|
def allowlist
|
||||||
|
@allowlist ||= DistributedPress::V1::Social::Allowlist.new(client: client, actor: actor)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [DistributedPress::V1::Social::Blocklist]
|
||||||
|
def blocklist
|
||||||
|
@blocklist ||= DistributedPress::V1::Social::Blocklist.new(client: client, actor: actor)
|
||||||
|
end
|
||||||
|
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def public_key_url
|
def public_key_url
|
||||||
@public_key_url ||= generate_uri do |uri|
|
@public_key_url ||= SocialInbox.generate_uri(hostname) do |uri|
|
||||||
uri.path = '/about.jsonld'
|
uri.path = '/about.jsonld'
|
||||||
uri.fragment = 'main-key'
|
uri.fragment = 'main-key'
|
||||||
end
|
end
|
||||||
|
@ -78,7 +102,7 @@ class SocialInbox
|
||||||
# Genera una URI dentro de este sitio
|
# Genera una URI dentro de este sitio
|
||||||
#
|
#
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def generate_uri(&block)
|
def self.generate_uri(hostname, &block)
|
||||||
@public_key_url ||= URI("https://#{hostname}").tap(&block).to_s
|
URI("https://#{hostname}").tap(&block).to_s
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
16
app/policies/activity_pub_policy.rb
Normal file
16
app/policies/activity_pub_policy.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Solo les usuaries pueden moderar comentarios
|
||||||
|
ActivityPubPolicy = Struct.new(:usuarie, :activity_pub) do
|
||||||
|
ActivityPub.events.each do |event|
|
||||||
|
define_method(:"#{event}?") do
|
||||||
|
activity_pub.site.usuarie? usuarie
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# En este paso tenemos varias instancias por moderar pero todas son
|
||||||
|
# del mismo sitio.
|
||||||
|
def action_on_several?
|
||||||
|
activity_pub.first.site.usuarie? usuarie
|
||||||
|
end
|
||||||
|
end
|
16
app/policies/actor_moderation_policy.rb
Normal file
16
app/policies/actor_moderation_policy.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Solo les usuaries pueden moderar actores
|
||||||
|
ActorModerationPolicy = Struct.new(:usuarie, :actor_moderation) do
|
||||||
|
ActorModeration.events.each do |actor_event|
|
||||||
|
define_method(:"#{actor_event}?") do
|
||||||
|
actor_moderation.site.usuarie? usuarie
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# En este paso tenemos varias cuentas por moderar pero todas son
|
||||||
|
# del mismo sitio.
|
||||||
|
def action_on_several?
|
||||||
|
actor_moderation.first.site.usuarie? usuarie
|
||||||
|
end
|
||||||
|
end
|
16
app/policies/instance_moderation_policy.rb
Normal file
16
app/policies/instance_moderation_policy.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Solo les usuaries pueden moderar instancias
|
||||||
|
InstanceModerationPolicy = Struct.new(:usuarie, :instance_moderation) do
|
||||||
|
InstanceModeration.events.each do |event|
|
||||||
|
define_method(:"#{event}?") do
|
||||||
|
instance_moderation.site.usuarie? usuarie
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# En este paso tenemos varias instancias por moderar pero todas son
|
||||||
|
# del mismo sitio.
|
||||||
|
def action_on_several?
|
||||||
|
instance_moderation.first.site.usuarie? usuarie
|
||||||
|
end
|
||||||
|
end
|
15
app/processors/activity_pub_processor.rb
Normal file
15
app/processors/activity_pub_processor.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Gestiona los filtros de ActivityPub
|
||||||
|
class ActivityPubProcessor < Rubanok::Processor
|
||||||
|
# En orden descendiente para encontrar la última actividad
|
||||||
|
#
|
||||||
|
# Por ahora solo queremos moderar comentarios.
|
||||||
|
prepare do
|
||||||
|
raw.where(object_type: %w[ActivityPub::Object::Note ActivityPub::Object::Article]).order(updated_at: :desc)
|
||||||
|
end
|
||||||
|
|
||||||
|
map :activity_pub_state, activate_always: true do |activity_pub_state: 'paused'|
|
||||||
|
raw.where(aasm_state: activity_pub_state)
|
||||||
|
end
|
||||||
|
end
|
13
app/processors/actor_moderation_processor.rb
Normal file
13
app/processors/actor_moderation_processor.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Gestiona los filtros de ActorModeration
|
||||||
|
class ActorModerationProcessor < Rubanok::Processor
|
||||||
|
# En orden descendiente para encontrar le últime Actor
|
||||||
|
prepare do
|
||||||
|
raw.order(updated_at: :desc)
|
||||||
|
end
|
||||||
|
|
||||||
|
map :actor_state, activate_always: true do |actor_state: 'paused'|
|
||||||
|
raw.where(aasm_state: actor_state)
|
||||||
|
end
|
||||||
|
end
|
12
app/processors/instance_moderation_processor.rb
Normal file
12
app/processors/instance_moderation_processor.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Gestiona los filtros de InstanceModeration
|
||||||
|
class InstanceModerationProcessor < Rubanok::Processor
|
||||||
|
prepare do
|
||||||
|
raw.includes(:instance).order('activity_pub_instances.hostname')
|
||||||
|
end
|
||||||
|
|
||||||
|
map :instance_state, activate_always: true do |instance_state: 'paused'|
|
||||||
|
raw.where(aasm_state: instance_state)
|
||||||
|
end
|
||||||
|
end
|
|
@ -28,6 +28,8 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
||||||
|
|
||||||
add_role_to_deploys! role
|
add_role_to_deploys! role
|
||||||
|
|
||||||
|
add_role_to_deploys! role
|
||||||
|
|
||||||
site.save &&
|
site.save &&
|
||||||
site.config.write &&
|
site.config.write &&
|
||||||
commit_config(action: :create) &&
|
commit_config(action: :create) &&
|
||||||
|
|
8
app/views/actor_moderations/show.haml
Normal file
8
app/views/actor_moderations/show.haml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
.row.justify-content-center
|
||||||
|
.col-12.col-md-8
|
||||||
|
%h1= t('.profile')
|
||||||
|
= render 'components/actor', remote_profile: @remote_profile
|
||||||
|
.col-12.col-md-8
|
||||||
|
= render 'components/profiles_btn_box', actor_moderation: @actor_moderation
|
||||||
|
.col-12.col-md-8
|
||||||
|
= render 'moderation_queue/comments', moderation_queue: @moderation_queue
|
20
app/views/components/_actor.haml
Normal file
20
app/views/components/_actor.haml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
-# Componente Remote_Profile
|
||||||
|
|
||||||
|
.py-2
|
||||||
|
%dl
|
||||||
|
%dt= t('.profile_name')
|
||||||
|
%dd= text_plain remote_profile['name']
|
||||||
|
|
||||||
|
%dt= t('.preferred_name')
|
||||||
|
%dd= text_plain remote_profile['preferredUsername']
|
||||||
|
|
||||||
|
%dt= t('.profile_id')
|
||||||
|
%dd
|
||||||
|
= link_to text_plain(remote_profile['id'])
|
||||||
|
|
||||||
|
- if remote_profile['published'].present?
|
||||||
|
%dt= t('.profile_published')
|
||||||
|
%dd
|
||||||
|
= render 'layouts/time', time: text_plain(remote_profile['published'])
|
||||||
|
%dt= t('.profile_summary')
|
||||||
|
%dd= sanitize remote_profile['summary']
|
13
app/views/components/_block_list.haml
Normal file
13
app/views/components/_block_list.haml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
-# Componente Listas de bloqueo de Instancias
|
||||||
|
- know_more = t('.know_more')
|
||||||
|
- instances_blocked = t('.instances_blocked')
|
||||||
|
.card.mt-3.mb-3
|
||||||
|
.card-body
|
||||||
|
= render 'components/checkbox', id: state.id, name: 'fediblock_states_ids[]', value: state.id, checked: state.enabled? do
|
||||||
|
%span.h4.mb-0= blocklist.title
|
||||||
|
|
||||||
|
%dl.mb-0
|
||||||
|
%dt.d-inline= instances_blocked
|
||||||
|
%dd.d-inline.font-weight-normal= blocklist.hostnames.count
|
||||||
|
%p.mb-0.font-weight-normal
|
||||||
|
%a{ href: blocklist.url }= know_more
|
2
app/views/components/_block_lists.haml
Normal file
2
app/views/components/_block_lists.haml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
- blocklists.each do |blocklist|
|
||||||
|
= render 'components/block_list', blocklist: blocklist.fediblock, state: blocklist
|
9
app/views/components/_btn_base.haml
Normal file
9
app/views/components/_btn_base.haml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
-# Componente Botón general Moderación
|
||||||
|
|
||||||
|
- local_assigns[:method] ||= 'patch'
|
||||||
|
- local_assigns[:class] ||= 'btn-secondary'
|
||||||
|
- local_assigns[:class] = "btn #{local_assigns[:class]}"
|
||||||
|
|
||||||
|
-# @todo path es obligatorio
|
||||||
|
= button_to local_assigns[:path], **local_assigns do
|
||||||
|
= text
|
5
app/views/components/_checkbox.haml
Normal file
5
app/views/components/_checkbox.haml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
-# Componente Checkbox
|
||||||
|
- local_assigns[:name] ||= id
|
||||||
|
.custom-control.custom-checkbox
|
||||||
|
%input.custom-control-input{ form: local_assigns[:form_id], type: 'checkbox', id: id, **local_assigns }
|
||||||
|
%label.custom-control-label{ for: id }= yield
|
8
app/views/components/_comments_btn_box.haml
Normal file
8
app/views/components/_comments_btn_box.haml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
-# Componente Botonera de Comentarios
|
||||||
|
|
||||||
|
.d-flex.flex-row
|
||||||
|
- ActivityPub.events.each do |event|
|
||||||
|
= render 'components/btn_base',
|
||||||
|
text: t(".text_#{event}"),
|
||||||
|
path: public_send(:"site_activity_pub_#{event}_path", activity_pub_id: activity_pub),
|
||||||
|
disabled: !activity_pub.public_send(:"may_#{event}?")
|
6
app/views/components/_comments_checked_submenu.haml
Normal file
6
app/views/components/_comments_checked_submenu.haml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
- current_state = params[:activity_pub_state]&.to_sym || ActivityPub.states.first
|
||||||
|
|
||||||
|
- ActivityPub.aasm.events.each do |event|
|
||||||
|
- next if ActivityPub::IGNORED_EVENTS.include? event.name
|
||||||
|
- next unless event.transitions_from_state?(current_state)
|
||||||
|
= render 'components/dropdown_button', form_id: form_id, text: t(".submenu_#{event.name}"), name: 'activity_pub_action', value: event.name
|
9
app/views/components/_comments_filters.haml
Normal file
9
app/views/components/_comments_filters.haml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
- current_state = params[:activity_pub_state]&.to_sym || ActivityPub.states.first
|
||||||
|
|
||||||
|
.d-flex.py-2
|
||||||
|
- if ActivityPub.transitionable_events(current_state).present?
|
||||||
|
= render 'components/dropdown', text: t('.text_checked') do
|
||||||
|
= render 'components/comments_checked_submenu', form_id: form_id
|
||||||
|
|
||||||
|
= render 'components/dropdown', text: t('.text_show') do
|
||||||
|
= render 'components/comments_show_submenu', activity_pubs: activity_pubs
|
4
app/views/components/_comments_show_submenu.haml
Normal file
4
app/views/components/_comments_show_submenu.haml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
- ActivityPub.states.each do |state|
|
||||||
|
= render 'components/dropdown_item',
|
||||||
|
text: t(".submenu_#{state}", count: activity_pubs.unscope(where: :aasm_state).public_send(state).count),
|
||||||
|
path: filter_states(activity_pub_state: state)
|
34
app/views/components/_dropdown.haml
Normal file
34
app/views/components/_dropdown.haml
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
-#
|
||||||
|
@param :text [String] Contenido del botón
|
||||||
|
@param :button_classes [Array] Clases para el botón
|
||||||
|
@param :dropdown_classes [Array] Clases para el listado
|
||||||
|
@yield Un bloque que renderiza components/dropdown_item
|
||||||
|
- button_classes = local_assigns[:button_classes]&.join(' ')
|
||||||
|
- dropdown_classes = local_assigns[:dropdown_classes]&.join(' ')
|
||||||
|
|
||||||
|
.btn-group{
|
||||||
|
data: {
|
||||||
|
controller: 'dropdown'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
%button.btn.dropdown-toggle{
|
||||||
|
type: 'button',
|
||||||
|
class: button_classes,
|
||||||
|
data: {
|
||||||
|
toggle: 'true',
|
||||||
|
display: 'static',
|
||||||
|
action: 'dropdown#toggle',
|
||||||
|
target: 'dropdown.button'
|
||||||
|
},
|
||||||
|
aria: {
|
||||||
|
expanded: 'false'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
= text
|
||||||
|
.dropdown-menu{
|
||||||
|
class: dropdown_classes,
|
||||||
|
data: {
|
||||||
|
target: 'dropdown.dropdown'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
= yield
|
4
app/views/components/_dropdown_button.haml
Normal file
4
app/views/components/_dropdown_button.haml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
-#
|
||||||
|
@param name [String]
|
||||||
|
@param value [String]
|
||||||
|
%button.dropdown-item{type: 'submit', data: { target: 'dropdown.item' }, name: name, value: value, form: local_assigns[:form_id] }= text
|
4
app/views/components/_dropdown_item.haml
Normal file
4
app/views/components/_dropdown_item.haml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
-#
|
||||||
|
@param :text [String] Contenido del link
|
||||||
|
@param :path [String,Hash] Link
|
||||||
|
= link_to text, path, class: 'dropdown-item', data: { target: 'dropdown.item' }
|
6
app/views/components/_instances_btn_box.haml
Normal file
6
app/views/components/_instances_btn_box.haml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
-# Componente botonera de moderación de Instancias
|
||||||
|
|
||||||
|
- btn_class = 'btn btn-secondary'
|
||||||
|
= render 'components/btn_base', path: site_instance_moderation_pause_path(instance_moderation_id: instance_moderation), text: t('.text_check'), class: btn_class, disabled: !instance_moderation.may_pause?
|
||||||
|
= render 'components/btn_base', path: site_instance_moderation_allow_path(instance_moderation_id: instance_moderation), text: t('.text_allow'), class: btn_class, disabled: !instance_moderation.may_allow?
|
||||||
|
= render 'components/btn_base', path: site_instance_moderation_block_path(instance_moderation_id: instance_moderation), text: t('.text_deny'), class: btn_class, disabled: !instance_moderation.may_block?
|
2
app/views/components/_instances_checked_submenu.haml
Normal file
2
app/views/components/_instances_checked_submenu.haml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
- InstanceModeration.transitionable_events(current_state).each do |event|
|
||||||
|
= render 'components/dropdown_button', text: t(".submenu_#{event}"), name: 'instance_moderation_action', value: event, form_id: form_id
|
9
app/views/components/_instances_filters.haml
Normal file
9
app/views/components/_instances_filters.haml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
- current_state = params[:state]&.to_sym || InstanceModeration.states.first
|
||||||
|
|
||||||
|
.d-flex.py-2
|
||||||
|
- if InstanceModeration.transitionable_events(current_state).present?
|
||||||
|
= render 'components/dropdown', text: t('.text_checked') do
|
||||||
|
= render 'components/instances_checked_submenu', form_id: form_id, current_state: current_state
|
||||||
|
|
||||||
|
= render 'components/dropdown', text: t('.text_show') do
|
||||||
|
= render 'components/instances_show_submenu', instance_moderations: instance_moderations
|
4
app/views/components/_instances_show_submenu.haml
Normal file
4
app/views/components/_instances_show_submenu.haml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
- InstanceModeration.states.each do |state|
|
||||||
|
= render 'components/dropdown_item',
|
||||||
|
text: t(".submenu_#{state}", count: instance_moderations.unscope(where: :aasm_state).public_send(state).count),
|
||||||
|
path: filter_states(instance_state: state)
|
9
app/views/components/_profiles_btn_box.haml
Normal file
9
app/views/components/_profiles_btn_box.haml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
-# Componente Botonera de Moderación de Cuentas (Remote_profile)
|
||||||
|
.d-flex.flex-row
|
||||||
|
- btn_class = 'btn-secondary'
|
||||||
|
- ActorModeration.events.each do |actor_event|
|
||||||
|
= render 'components/btn_base',
|
||||||
|
text: t(".text_#{actor_event}"),
|
||||||
|
path: public_send(:"site_actor_moderation_#{actor_event}_path", actor_moderation_id: actor_moderation),
|
||||||
|
class: btn_class,
|
||||||
|
disabled: !actor_moderation.public_send(:"may_#{actor_event}?")
|
2
app/views/components/_profiles_checked_submenu.haml
Normal file
2
app/views/components/_profiles_checked_submenu.haml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
- ActorModeration.transitionable_events(current_state).each do |actor_event|
|
||||||
|
= render 'components/dropdown_button', text: t(".submenu_#{actor_event}"), name: 'actor_moderation_action', value: actor_event, form_id: form_id
|
9
app/views/components/_profiles_filters.haml
Normal file
9
app/views/components/_profiles_filters.haml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
- current_state = params[:actor_state]&.to_sym || ActorModeration.states.first
|
||||||
|
|
||||||
|
.d-flex.py-2
|
||||||
|
- if ActorModeration.transitionable_events(current_state).present?
|
||||||
|
= render 'components/dropdown', text: t('.text_checked') do
|
||||||
|
= render 'components/profiles_checked_submenu', form_id: form_id, current_state: current_state
|
||||||
|
|
||||||
|
= render 'components/dropdown', text: t('.text_show') do
|
||||||
|
= render 'components/profiles_show_submenu', actor_moderations: actor_moderations
|
4
app/views/components/_profiles_show_submenu.haml
Normal file
4
app/views/components/_profiles_show_submenu.haml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
- ActorModeration.states.each do |actor_state|
|
||||||
|
= render 'components/dropdown_item',
|
||||||
|
text: t(".submenu_#{actor_state}", count: actor_moderations.unscope(where: :aasm_state).public_send(actor_state).count),
|
||||||
|
path: filter_states(actor_state: actor_state)
|
4
app/views/components/_select_all.haml
Normal file
4
app/views/components/_select_all.haml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
-#
|
||||||
|
@param id [String]
|
||||||
|
= render 'components/checkbox', id: id, form: local_assigns[:form_id], data: { action: 'select-all#toggle', target: 'select-all.toggle' } do
|
||||||
|
%span.sr-only= t('.label')
|
13
app/views/components/_select_all_container.haml
Normal file
13
app/views/components/_select_all_container.haml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
-#
|
||||||
|
Contenedor para las acciones en masa.
|
||||||
|
|
||||||
|
Es un formulario auto-contenido, que permite colocar los elementos
|
||||||
|
fuera del formulario para evitar anidarlos. Mientras los elementos
|
||||||
|
tengan el atributo `form` con el mismo parámetro `form_id`, el
|
||||||
|
navegador los va a asignar a este formulario.
|
||||||
|
|
||||||
|
@param path [String]
|
||||||
|
@param form_id [String]
|
||||||
|
|
||||||
|
= form_tag path, id: form_id, method: :patch do
|
||||||
|
-# nada
|
16
app/views/layouts/_details.haml
Normal file
16
app/views/layouts/_details.haml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
-#
|
||||||
|
Detail Cola de Moderación
|
||||||
|
|
||||||
|
@param :id [String] El ID opcional sirve para mantener el historial de
|
||||||
|
cuál estaba abierto y recuperarlo al cargar la página
|
||||||
|
@param :summary [String] El resumen
|
||||||
|
@param :summary_class [String] Clases para el summary
|
||||||
|
|
||||||
|
- local_assigns[:summary_class] ||= 'h3'
|
||||||
|
|
||||||
|
%details.details.py-2{ id: local_assigns[:id], data: { controller: 'details', action: 'toggle->details#store' } }
|
||||||
|
%summary.d-flex.flex-row.align-items-center.justify-content-between{ class: local_assigns[:summary_class] }
|
||||||
|
%span= summary
|
||||||
|
%span.hide-when-open ▶
|
||||||
|
%span.show-when-open ▼
|
||||||
|
= yield
|
13
app/views/moderation_queue/_account.haml
Normal file
13
app/views/moderation_queue/_account.haml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
.row.no-gutters.pt-2
|
||||||
|
.col-1
|
||||||
|
= render 'components/checkbox', id: actor_moderation.id, form_id: form_id, name: 'actor_moderation[]', value: actor_moderation.id, data: { target: 'select-all.input' }
|
||||||
|
.col-11
|
||||||
|
%h4
|
||||||
|
= link_to text_plain(profile['name']), site_actor_moderation_path(id: actor_moderation)
|
||||||
|
.mb-3
|
||||||
|
= sanitize profile['summary']
|
||||||
|
|
||||||
|
-# Botones de Moderación
|
||||||
|
- cache actor_moderation do
|
||||||
|
.d-flex.pb-4
|
||||||
|
= render 'components/profiles_btn_box', actor_moderation: actor_moderation
|
17
app/views/moderation_queue/_accounts.haml
Normal file
17
app/views/moderation_queue/_accounts.haml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
- form_id = 'actor_moderations_action_on_several'
|
||||||
|
|
||||||
|
= render 'components/select_all_container', path: site_actor_moderations_action_on_several_path, form_id: form_id
|
||||||
|
|
||||||
|
.row.no-gutters.pt-2{ data: { controller: 'select-all' } }
|
||||||
|
.col-1.d-flex.align-items-center
|
||||||
|
= render 'components/select_all', id: 'actors', form_id: form_id
|
||||||
|
.col-11
|
||||||
|
-# Filtros
|
||||||
|
= render 'components/profiles_filters', actor_moderations: actor_moderations, form_id: form_id
|
||||||
|
.col-12
|
||||||
|
- if actor_moderations.count.zero?
|
||||||
|
%h4= t('moderation_queue.nothing')
|
||||||
|
- actor_moderations.find_each do |actor_moderation|
|
||||||
|
- cache [actor_moderation, actor_moderation.actor] do
|
||||||
|
%hr
|
||||||
|
= render 'account', actor_moderation: actor_moderation, profile: actor_moderation.actor.content, form_id: form_id
|
|
@ -0,0 +1,3 @@
|
||||||
|
.form-group
|
||||||
|
= label_tag 'custom_blocklist', t('moderation_queue.instances.custom_block')
|
||||||
|
= text_area_tag 'custom_blocklist', nil, class: 'form-control'
|
33
app/views/moderation_queue/_comment.haml
Normal file
33
app/views/moderation_queue/_comment.haml
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
-#
|
||||||
|
Componente Comentario
|
||||||
|
|
||||||
|
@param profile [Hash]
|
||||||
|
@param comment [Hash]
|
||||||
|
@param activity_pub [ActivityPub]
|
||||||
|
|
||||||
|
- in_reply_to = text_plain comment['inReplyTo']
|
||||||
|
- summary = text_plain comment['summary']
|
||||||
|
|
||||||
|
.row.no-gutters
|
||||||
|
.col-1
|
||||||
|
= render 'components/checkbox', id: activity_pub.id, name: 'activity_pub[]', value: activity_pub.id, data: { target: 'select-all.input' }, form: form_id
|
||||||
|
.col-11
|
||||||
|
.d-flex.flex-row.align-items-center.justify-content-between
|
||||||
|
%h4.mb-0
|
||||||
|
%a{ href: text_plain(comment['attributedTo']) }= text_plain profile['preferredUsername']
|
||||||
|
%small
|
||||||
|
= render 'layouts/time', time: text_plain(comment['published'])
|
||||||
|
- if in_reply_to.present?
|
||||||
|
%dl
|
||||||
|
%dt.d-inline
|
||||||
|
%small= t('.reply_to')
|
||||||
|
%dd.d-inline
|
||||||
|
%small
|
||||||
|
%a{ href: in_reply_to }= in_reply_to
|
||||||
|
.content
|
||||||
|
- if summary.present?
|
||||||
|
= render 'layouts/details', summary: summary, summary_class: 'h5' do
|
||||||
|
= sanitize comment['content']
|
||||||
|
- else
|
||||||
|
= sanitize comment['content']
|
||||||
|
= render 'components/comments_btn_box', activity_pub: activity_pub
|
17
app/views/moderation_queue/_comments.haml
Normal file
17
app/views/moderation_queue/_comments.haml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
- form_id = 'activity_pub_action_on_several'
|
||||||
|
|
||||||
|
= render 'components/select_all_container', path: site_activity_pubs_action_on_several_path, form_id: form_id
|
||||||
|
|
||||||
|
.row.no-gutters.pt-2{ data: { controller: 'select-all' } }
|
||||||
|
.col-1.d-flex.align-items-center
|
||||||
|
= render 'components/select_all', id: 'select-all-comments', form_id: form_id
|
||||||
|
.col-11
|
||||||
|
-# Filtros
|
||||||
|
= render 'components/comments_filters', activity_pubs: moderation_queue, form_id: form_id
|
||||||
|
.col-12
|
||||||
|
- if moderation_queue.count.zero?
|
||||||
|
%h4= t('moderation_queue.nothing')
|
||||||
|
- moderation_queue.each do |activity_pub|
|
||||||
|
-# cache [activity_pub, activity_pub.object, activity_pub.actor] do
|
||||||
|
%hr
|
||||||
|
= render 'comment', comment: activity_pub.object.content, profile: activity_pub.actor.content, activity_pub: activity_pub, form_id: form_id
|
19
app/views/moderation_queue/_instance.haml
Normal file
19
app/views/moderation_queue/_instance.haml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
- usuaries = instance.content.dig('usage', 'users', 'active_month')
|
||||||
|
- usuaries ||= instance.content.dig('stats', 'user_count')
|
||||||
|
|
||||||
|
.row.no-gutters.pt-2
|
||||||
|
.col-1
|
||||||
|
= render 'components/checkbox', id: instance.hostname, form_id: form_id, name: 'instance_moderation[]', value: instance_moderation.id, data: { target: 'select-all.input' }
|
||||||
|
.col-11
|
||||||
|
%h4
|
||||||
|
%a{ href: instance.uri }= sanitize(instance.content['title']) || instance.hostname
|
||||||
|
.content
|
||||||
|
= sanitize instance.content['description']
|
||||||
|
- if usuaries.present?
|
||||||
|
%dl
|
||||||
|
%dt.d-inline= t('.users')
|
||||||
|
%dd.d-inline= text_plain usuaries.to_s
|
||||||
|
|
||||||
|
-# Botones moderación
|
||||||
|
.d-flex.pb-4
|
||||||
|
= render 'components/instances_btn_box', instance_moderation: instance_moderation
|
31
app/views/moderation_queue/_instances.haml
Normal file
31
app/views/moderation_queue/_instances.haml
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
- form_id = 'instance_moderation_action_on_several'
|
||||||
|
|
||||||
|
%section
|
||||||
|
= render 'components/select_all_container', path: site_instance_moderations_action_on_several_path, form_id: form_id
|
||||||
|
.row.no-gutters.pt-2{ data: { controller: 'select-all' } }
|
||||||
|
.col-1.d-flex.align-items-center
|
||||||
|
= render 'components/select_all', id: 'instances', form_id: form_id
|
||||||
|
.col-11
|
||||||
|
-# Filtros
|
||||||
|
= render 'components/instances_filters', instance_moderations: instance_moderations, form_id: form_id
|
||||||
|
|
||||||
|
.col-12
|
||||||
|
- if instance_moderations.count.zero?
|
||||||
|
%h4= t('moderation_queue.nothing')
|
||||||
|
|
||||||
|
- instance_moderations.each do |instance_moderation|
|
||||||
|
- cache [instance_moderation.aasm_state, instance_moderation.instance] do
|
||||||
|
%hr
|
||||||
|
= render 'moderation_queue/instance', instance_moderation: instance_moderation, instance: instance_moderation.instance, form_id: form_id
|
||||||
|
|
||||||
|
%hr
|
||||||
|
%div
|
||||||
|
%h3.mt-5= t('moderation_queue.instances.title')
|
||||||
|
%lead= t('moderation_queue.instances.description')
|
||||||
|
|
||||||
|
= form_tag site_fediblock_states_action_on_several_path, method: :patch do
|
||||||
|
= render 'components/block_lists', blocklists: fediblock_states
|
||||||
|
= render 'moderation_queue/block_instances_textarea'
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
%button.btn.btn-secondary.mt-3{ type: 'submit' }= t('moderation_queue.instances.submit')
|
13
app/views/moderation_queue/index.haml
Normal file
13
app/views/moderation_queue/index.haml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
.row.justify-content-center
|
||||||
|
.col-md-8
|
||||||
|
%h1= t('.title')
|
||||||
|
.row
|
||||||
|
.col
|
||||||
|
= render 'layouts/details', id: 'summary', summary: t('.instances') do
|
||||||
|
= render 'moderation_queue/instances', site: @site, instance_moderations: @instance_moderations, fediblock_states: @site.fediblock_states
|
||||||
|
%hr
|
||||||
|
= render 'layouts/details', id: 'accounts', summary: t('.accounts') do
|
||||||
|
= render 'moderation_queue/accounts', site: @site, actor_moderations: @actor_moderations
|
||||||
|
%hr
|
||||||
|
= render 'layouts/details', id: 'comments', summary: t('.comments') do
|
||||||
|
= render 'moderation_queue/comments', site: @site, moderation_queue: @moderation_queue
|
14
app/views/posts/_moderation_queue.haml
Normal file
14
app/views/posts/_moderation_queue.haml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
.row.no-gutters.pt-2
|
||||||
|
.col-1
|
||||||
|
= render 'components/checkbox', id: moderation_queue.first['id']
|
||||||
|
.col-11
|
||||||
|
-# Filtros
|
||||||
|
= render 'components/comments_filters'
|
||||||
|
|
||||||
|
- moderation_queue.each do |comment|
|
||||||
|
%hr
|
||||||
|
= render 'moderation_queue/comment', comment: comment, profile: comment['attributedTo']
|
||||||
|
|
||||||
|
-# Botones moderación
|
||||||
|
.d-flex
|
||||||
|
= render 'components/comments_btn_box'
|
|
@ -1,3 +1,9 @@
|
||||||
.row.justify-content-center
|
.row.justify-content-center
|
||||||
.col-12.col-lg-8
|
.col-12.col-lg-8
|
||||||
|
- if policy(@site).edit?
|
||||||
|
= render 'layouts/details', summary: t('posts.edit.post') do
|
||||||
|
= render 'posts/form', site: @site, post: @post
|
||||||
|
= render 'layouts/details', summary: t('posts.edit.moderation_queue') do
|
||||||
|
= render 'posts/moderation_queue', site: @site, post: @post, moderation_queue: @moderation_queue
|
||||||
|
- else
|
||||||
= render 'posts/form', site: @site, post: @post
|
= render 'posts/form', site: @site, post: @post
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
.row.justify-content-center
|
.row.justify-content-center
|
||||||
.col-12.col-lg-8
|
.col-12.col-lg-8
|
||||||
%article.content.table-responsive-md
|
%article.content.table-responsive-md
|
||||||
= link_to t('posts.edit'),
|
= link_to t('posts.edit_post'),
|
||||||
edit_site_post_path(@site, @post.id),
|
edit_site_post_path(@site, @post.id),
|
||||||
class: 'btn btn-secondary btn-block'
|
class: 'btn btn-secondary btn-block'
|
||||||
|
|
||||||
|
@ -20,7 +20,6 @@
|
||||||
post: @post, attribute: attr,
|
post: @post, attribute: attr,
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
site: @site,
|
site: @site,
|
||||||
tags: all_html_tags,
|
|
||||||
locale: @locale,
|
locale: @locale,
|
||||||
dir: dir)
|
dir: dir)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
test -n "${CI_MERGE_REQUEST_DIFF_BASE_SHA}"
|
CI_MERGE_REQUEST_DIFF_BASE_SHA="${CI_MERGE_REQUEST_DIFF_BASE_SHA:-origin/rails}"
|
||||||
|
|
||||||
git diff --name-status ${CI_MERGE_REQUEST_DIFF_BASE_SHA} \
|
git diff --name-status ${CI_MERGE_REQUEST_DIFF_BASE_SHA} \
|
||||||
| grep -v "^D" \
|
| grep -v "^D" \
|
||||||
|
|
|
@ -37,6 +37,11 @@ if %w[development test].include? ENV['RAILS_ENV']
|
||||||
end
|
end
|
||||||
|
|
||||||
module Sutty
|
module Sutty
|
||||||
|
ALLOWED_ATTRIBUTES = %w[style href src alt controls data-align data-multimedia data-multimedia-inner id name rel
|
||||||
|
target referrerpolicy class colspan rowspan role data-turbo start type reversed].freeze
|
||||||
|
ALLOWED_TAGS = %w[strong em del u mark p h1 h2 h3 h4 h5 h6 ul ol li img iframe audio video div figure blockquote
|
||||||
|
figcaption a sub sup small table thead tbody tfoot tr th td br code].freeze
|
||||||
|
|
||||||
# Sutty!
|
# Sutty!
|
||||||
class Application < Rails::Application
|
class Application < Rails::Application
|
||||||
# Initialize configuration defaults for originally generated Rails
|
# Initialize configuration defaults for originally generated Rails
|
||||||
|
|
|
@ -1,4 +1,141 @@
|
||||||
en:
|
en:
|
||||||
|
date:
|
||||||
|
format: '%m/%d/%Y'
|
||||||
|
published_at: "Published at"
|
||||||
|
last_modified_at: "Last modification"
|
||||||
|
abbr_day_names:
|
||||||
|
- Mon
|
||||||
|
- Tue
|
||||||
|
- Wed
|
||||||
|
- Thu
|
||||||
|
- Fri
|
||||||
|
- Sat
|
||||||
|
- Sun
|
||||||
|
day_names:
|
||||||
|
- Monday
|
||||||
|
- Tuesday
|
||||||
|
- Wednesday
|
||||||
|
- Thursday
|
||||||
|
- Friday
|
||||||
|
- Saturday
|
||||||
|
- Sunday
|
||||||
|
abbr_month_names:
|
||||||
|
- Jan
|
||||||
|
- Feb
|
||||||
|
- Mar
|
||||||
|
- Apr
|
||||||
|
- May
|
||||||
|
- Jun
|
||||||
|
- Jul
|
||||||
|
- Aug
|
||||||
|
- Sep
|
||||||
|
- Oct
|
||||||
|
- Nov
|
||||||
|
- Dec
|
||||||
|
month_names:
|
||||||
|
- January
|
||||||
|
- February
|
||||||
|
- March
|
||||||
|
- April
|
||||||
|
- May
|
||||||
|
- June
|
||||||
|
- July
|
||||||
|
- August
|
||||||
|
- September
|
||||||
|
- October
|
||||||
|
- November
|
||||||
|
- December
|
||||||
|
time:
|
||||||
|
am: am
|
||||||
|
pm: pm
|
||||||
|
format: '%-I:%M %p'
|
||||||
|
components:
|
||||||
|
block_list:
|
||||||
|
know_more: Know more
|
||||||
|
instances_blocked: Instances blocked
|
||||||
|
instances_filters:
|
||||||
|
text_show: Show
|
||||||
|
text_checked: With selected
|
||||||
|
instances_checked_submenu:
|
||||||
|
submenu_pause: Moderate
|
||||||
|
submenu_allow: Allow
|
||||||
|
submenu_block: Block
|
||||||
|
instances_show_submenu:
|
||||||
|
submenu_paused: "Moderated (%{count})"
|
||||||
|
submenu_allowed: "Allowed (%{count})"
|
||||||
|
submenu_blocked: "Blocked (%{count})"
|
||||||
|
comments_filters:
|
||||||
|
text_show: Show
|
||||||
|
text_checked: With selected
|
||||||
|
comments_checked_submenu:
|
||||||
|
submenu_pause: Pause
|
||||||
|
submenu_approve: Approve
|
||||||
|
submenu_reject: Reject
|
||||||
|
submenu_report: Report
|
||||||
|
comments_show_submenu:
|
||||||
|
submenu_paused: "Paused (%{count})"
|
||||||
|
submenu_approved: "Approved (%{count})"
|
||||||
|
submenu_rejected: "Rejected (%{count})"
|
||||||
|
submenu_reported: "Reported (%{count})"
|
||||||
|
profiles_filters:
|
||||||
|
text_show: Show
|
||||||
|
text_checked: With selected
|
||||||
|
profiles_checked_submenu:
|
||||||
|
submenu_pause: Pause
|
||||||
|
submenu_allow: Allow
|
||||||
|
submenu_block: Block
|
||||||
|
submenu_report: Report
|
||||||
|
profiles_show_submenu:
|
||||||
|
submenu_paused: "Paused (%{count})"
|
||||||
|
submenu_allowed: "Allowed (%{count})"
|
||||||
|
submenu_blocked: "Blocked (%{count})"
|
||||||
|
submenu_reported: "Reported (%{count})"
|
||||||
|
block_lists:
|
||||||
|
title: Block lists
|
||||||
|
comments_btn_box:
|
||||||
|
text_pause: Pause
|
||||||
|
text_approve: Approve
|
||||||
|
text_reject: Reject
|
||||||
|
text_reply: Reply
|
||||||
|
text_report: Report
|
||||||
|
instances_btn_box:
|
||||||
|
text_check: Check case by case
|
||||||
|
text_allow: Allow everything
|
||||||
|
text_deny: Block instance
|
||||||
|
profiles_btn_box:
|
||||||
|
text_pause: Always check
|
||||||
|
text_allow: Always approve
|
||||||
|
text_block: Block
|
||||||
|
text_report: Report
|
||||||
|
actor_moderations:
|
||||||
|
show:
|
||||||
|
user: Username
|
||||||
|
profile: Profile
|
||||||
|
profile_name: Profile name
|
||||||
|
preferred_name: Name in Fediverse
|
||||||
|
profile_id: ID
|
||||||
|
profile_published: Published
|
||||||
|
profile_summary: Summary
|
||||||
|
remote_flags:
|
||||||
|
report_message: "Hi! Someone using Sutty CMS reported this account on your instance. We don't have support for customized report messages yet, but we will soon. You can reach us at %{panel_actor_mention}."
|
||||||
|
moderation_queue:
|
||||||
|
everything: 'Select all'
|
||||||
|
nothing: "There's nothing for this filter"
|
||||||
|
index:
|
||||||
|
title: Moderation
|
||||||
|
instances: Instances
|
||||||
|
accounts: Accounts
|
||||||
|
comments: Comments
|
||||||
|
comment:
|
||||||
|
source_profile: Source Profile
|
||||||
|
reply_to: Reply to
|
||||||
|
instances:
|
||||||
|
title: My block lists
|
||||||
|
description: Description
|
||||||
|
custom_block: Custom block lists
|
||||||
|
submit: Save block lists
|
||||||
|
instance:
|
||||||
|
users: "Users:"
|
||||||
dark: Dark
|
dark: Dark
|
||||||
dir: ltr
|
dir: ltr
|
||||||
en: English
|
en: English
|
||||||
|
@ -574,7 +711,10 @@ en:
|
||||||
categories: 'Everything'
|
categories: 'Everything'
|
||||||
index:
|
index:
|
||||||
search: 'Search'
|
search: 'Search'
|
||||||
edit: 'Edit'
|
edit_post: 'Edit'
|
||||||
|
edit:
|
||||||
|
moderation_queue: Moderation Queue
|
||||||
|
post: Post
|
||||||
preview:
|
preview:
|
||||||
btn: 'Preliminary version'
|
btn: 'Preliminary version'
|
||||||
alert: 'Not every article type has a preliminary version'
|
alert: 'Not every article type has a preliminary version'
|
||||||
|
|
|
@ -1,4 +1,140 @@
|
||||||
es:
|
es:
|
||||||
|
date:
|
||||||
|
format: '%d/%m/%Y'
|
||||||
|
published_at: "Publicado en"
|
||||||
|
last_modified_at: "Última modificación"
|
||||||
|
abbr_day_names:
|
||||||
|
- Lun
|
||||||
|
- Mar
|
||||||
|
- Mié
|
||||||
|
- Jue
|
||||||
|
- Vie
|
||||||
|
- Sáb
|
||||||
|
- Dom
|
||||||
|
day_names:
|
||||||
|
- Lunes
|
||||||
|
- Martes
|
||||||
|
- Miércoles
|
||||||
|
- Jueves
|
||||||
|
- Viernes
|
||||||
|
- Sábado
|
||||||
|
- Domingo
|
||||||
|
abbr_month_names:
|
||||||
|
- Ene
|
||||||
|
- Feb
|
||||||
|
- Mar
|
||||||
|
- Abr
|
||||||
|
- May
|
||||||
|
- Jun
|
||||||
|
- Jul
|
||||||
|
- Ago
|
||||||
|
- Sep
|
||||||
|
- Oct
|
||||||
|
- Nov
|
||||||
|
- Dic
|
||||||
|
month_names:
|
||||||
|
- Enero
|
||||||
|
- Febrero
|
||||||
|
- Marzo
|
||||||
|
- Abril
|
||||||
|
- Mayo
|
||||||
|
- Junio
|
||||||
|
- Julio
|
||||||
|
- Agosto
|
||||||
|
- Septiembre
|
||||||
|
- Octubre
|
||||||
|
- Noviembre
|
||||||
|
- Diciembre
|
||||||
|
time:
|
||||||
|
am: am
|
||||||
|
pm: pm
|
||||||
|
format: '%-H:%M'
|
||||||
|
components:
|
||||||
|
block_list:
|
||||||
|
know_more: Saber más (en inglés)
|
||||||
|
instances_blocked: Instancias bloqueadas
|
||||||
|
instances_filters:
|
||||||
|
text_show: Ver
|
||||||
|
text_checked: Con los marcados
|
||||||
|
instances_checked_submenu:
|
||||||
|
submenu_pause: Moderar caso por caso
|
||||||
|
submenu_allow: Permitir todo
|
||||||
|
submenu_block: Rechazar todo
|
||||||
|
instances_show_submenu:
|
||||||
|
submenu_paused: "Pausadas (%{count})"
|
||||||
|
submenu_allowed: "Permitidas (%{count})"
|
||||||
|
submenu_blocked: "Bloqueadas (%{count})"
|
||||||
|
comments_filters:
|
||||||
|
text_show: Ver
|
||||||
|
text_checked: Con los marcados
|
||||||
|
comments_checked_submenu:
|
||||||
|
submenu_pause: Pausar
|
||||||
|
submenu_approve: Aprobar
|
||||||
|
submenu_reject: Rechazar
|
||||||
|
submenu_report: Reportar
|
||||||
|
comments_show_submenu:
|
||||||
|
submenu_paused: "Pausados (%{count})"
|
||||||
|
submenu_approved: "Aprobados (%{count})"
|
||||||
|
submenu_rejected: "Rechazados (%{count})"
|
||||||
|
submenu_reported: "Reportados (%{count})"
|
||||||
|
profiles_filters:
|
||||||
|
text_show: Ver
|
||||||
|
text_checked: Con los marcados
|
||||||
|
profiles_checked_submenu:
|
||||||
|
submenu_pause: Pausar
|
||||||
|
submenu_allow: Aceptar
|
||||||
|
submenu_block: Bloquear
|
||||||
|
submenu_report: Reportar
|
||||||
|
profiles_show_submenu:
|
||||||
|
submenu_paused: "Pausadas (%{count})"
|
||||||
|
submenu_allowed: "Permitidas (%{count})"
|
||||||
|
submenu_blocked: "Bloqueadas (%{count})"
|
||||||
|
submenu_reported: "Reportadas (%{count})"
|
||||||
|
block_lists:
|
||||||
|
title: Listas de bloqueo
|
||||||
|
comments_btn_box:
|
||||||
|
text_pause: Pausar
|
||||||
|
text_approve: Aceptar
|
||||||
|
text_reject: Rechazar
|
||||||
|
text_report: Reportar
|
||||||
|
instances_btn_box:
|
||||||
|
text_check: Moderar caso por caso
|
||||||
|
text_allow: Permitir todo
|
||||||
|
text_deny: Bloquear instancia
|
||||||
|
profiles_btn_box:
|
||||||
|
text_pause: Revisar siempre
|
||||||
|
text_allow: Aprobar siempre
|
||||||
|
text_block: Bloquear
|
||||||
|
text_report: Reportar
|
||||||
|
actor_moderations:
|
||||||
|
show:
|
||||||
|
user: Nombre de usuarie
|
||||||
|
profile: Cuenta de Origen
|
||||||
|
profile_name: Nombre de la cuenta
|
||||||
|
preferred_name: Nombre en el Fediverso
|
||||||
|
profile_id: ID
|
||||||
|
profile_published: Publicada
|
||||||
|
profile_summary: Presentación
|
||||||
|
remote_flags:
|
||||||
|
report_message: "¡Hola! Une usuarie de Sutty CMS reportó esta cuenta en tu instancia. Todavía no tenemos soporte para mensajes personalizados. Podés contactarnos en %{panel_actor_mention}."
|
||||||
|
moderation_queue:
|
||||||
|
everything: 'Seleccionar todo'
|
||||||
|
nothing: 'No hay nada para este filtro'
|
||||||
|
index:
|
||||||
|
title: Actividades de moderación
|
||||||
|
instances: Instancias
|
||||||
|
accounts: Cuentas
|
||||||
|
comments: Comentarios
|
||||||
|
comment:
|
||||||
|
source_profile: Cuenta de Origen
|
||||||
|
reply_to: En respuesta a
|
||||||
|
instances:
|
||||||
|
title: Mis listas de bloqueo
|
||||||
|
description: Descripción de listas de bloqueo
|
||||||
|
custom_block: Lista personalizada de bloqueo
|
||||||
|
submit: Guardar listas de bloqueo
|
||||||
|
instance:
|
||||||
|
users: "Usuaries:"
|
||||||
dark: Oscuro
|
dark: Oscuro
|
||||||
es: Castellano
|
es: Castellano
|
||||||
en: English
|
en: English
|
||||||
|
@ -516,6 +652,9 @@ es:
|
||||||
en: 'inglés'
|
en: 'inglés'
|
||||||
ar: 'árabe'
|
ar: 'árabe'
|
||||||
posts:
|
posts:
|
||||||
|
edit:
|
||||||
|
moderation_queue: Comentarios
|
||||||
|
post: Contenido
|
||||||
prev: Página anterior
|
prev: Página anterior
|
||||||
next: Página siguiente
|
next: Página siguiente
|
||||||
empty: No hay artículos con estos parámetros de búsqueda.
|
empty: No hay artículos con estos parámetros de búsqueda.
|
||||||
|
@ -582,7 +721,7 @@ es:
|
||||||
remove_filter_help: 'Quitar este filtro: %{filter}'
|
remove_filter_help: 'Quitar este filtro: %{filter}'
|
||||||
index:
|
index:
|
||||||
search: 'Buscar'
|
search: 'Buscar'
|
||||||
edit: 'Editar'
|
edit_post: 'Editar'
|
||||||
preview:
|
preview:
|
||||||
btn: 'Versión preliminar'
|
btn: 'Versión preliminar'
|
||||||
alert: 'No todos los tipos de artículos poseen vista preliminar :)'
|
alert: 'No todos los tipos de artículos poseen vista preliminar :)'
|
||||||
|
|
|
@ -11,6 +11,10 @@ Rails.application.routes.draw do
|
||||||
namespace :v1 do
|
namespace :v1 do
|
||||||
resources :csp_reports, only: %i[create]
|
resources :csp_reports, only: %i[create]
|
||||||
|
|
||||||
|
namespace :activity_pub do
|
||||||
|
resources :remote_flags, only: %i[show]
|
||||||
|
end
|
||||||
|
|
||||||
resources :sites, only: %i[index], constraints: { site_id: /[a-z0-9\-.]+/, id: /[a-z0-9\-.]+/ } do
|
resources :sites, only: %i[index], constraints: { site_id: /[a-z0-9\-.]+/, id: /[a-z0-9\-.]+/ } do
|
||||||
get :'invitades/cookie', to: 'invitades#cookie'
|
get :'invitades/cookie', to: 'invitades#cookie'
|
||||||
post :'posts/:layout', to: 'posts#create', as: :posts
|
post :'posts/:layout', to: 'posts#create', as: :posts
|
||||||
|
@ -58,6 +62,33 @@ Rails.application.routes.draw do
|
||||||
get 'collaborate', to: 'collaborations#collaborate'
|
get 'collaborate', to: 'collaborations#collaborate'
|
||||||
post 'collaborate', to: 'collaborations#accept_collaboration'
|
post 'collaborate', to: 'collaborations#accept_collaboration'
|
||||||
|
|
||||||
|
get 'moderation_queue', to: 'moderation_queue#index'
|
||||||
|
|
||||||
|
resources :instance_moderations, only: [] do
|
||||||
|
patch :pause, to: 'instance_moderations#pause'
|
||||||
|
patch :allow, to: 'instance_moderations#allow'
|
||||||
|
patch :block, to: 'instance_moderations#block'
|
||||||
|
end
|
||||||
|
|
||||||
|
patch :instance_moderations_action_on_several, to: 'instance_moderations#action_on_several'
|
||||||
|
patch :fediblock_states_action_on_several, to: 'fediblock_states#action_on_several'
|
||||||
|
|
||||||
|
resources :actor_moderations, only: %i[show] do
|
||||||
|
ActorModeration.events.each do |actor_event|
|
||||||
|
patch actor_event, to: "actor_moderations##{actor_event}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
patch :actor_moderations_action_on_several, to: 'actor_moderations#action_on_several'
|
||||||
|
|
||||||
|
resources :activity_pub, only: [] do
|
||||||
|
ActivityPub.events.each do |event|
|
||||||
|
patch event, to: "activity_pubs##{event}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
patch :activity_pubs_action_on_several, to: 'activity_pubs#action_on_several'
|
||||||
|
|
||||||
# Gestionar artículos según idioma
|
# Gestionar artículos según idioma
|
||||||
nested do
|
nested do
|
||||||
scope '/(:locale)', constraint: /[a-z]{2}(-[A-Z]{2})?/ do
|
scope '/(:locale)', constraint: /[a-z]{2}(-[A-Z]{2})?/ do
|
||||||
|
|
18
db/migrate/20240223170317_add_actor_to_activities.rb
Normal file
18
db/migrate/20240223170317_add_actor_to_activities.rb
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Relaciona Actor con Activity
|
||||||
|
class AddActorToActivities < ActiveRecord::Migration[6.1]
|
||||||
|
def up
|
||||||
|
add_column :activity_pub_activities, :actor_id, :uuid, index: true
|
||||||
|
|
||||||
|
ActivityPub::Activity.find_each do |activity|
|
||||||
|
actor = ActivityPub::Actor.find_by(uri: activity.content['actor'])
|
||||||
|
|
||||||
|
activity.update(actor: actor) if actor.present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_column :activity_pub_activities, :actor_id, :uuid, index: true
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,16 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Relaciona instancias con sus actividades
|
||||||
|
class AddInstanceIdToActivityPubs < ActiveRecord::Migration[6.1]
|
||||||
|
def up
|
||||||
|
add_column :activity_pubs, :instance_id, :uuid, index: true
|
||||||
|
|
||||||
|
ActivityPub.all.find_each do |activity_pub|
|
||||||
|
activity_pub.update(instance: activity_pub&.object&.actor&.instance)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_column :activity_pubs, :instance_id, :uuid, index: true
|
||||||
|
end
|
||||||
|
end
|
26
db/migrate/20240226134335_create_instance_moderation.rb
Normal file
26
db/migrate/20240226134335_create_instance_moderation.rb
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Como la instancia es única para todo el panel, necesitamos llevar
|
||||||
|
# registro de su relación con cada sitio por separado.
|
||||||
|
class CreateInstanceModeration < ActiveRecord::Migration[6.1]
|
||||||
|
def up
|
||||||
|
create_table :instance_moderations do |t|
|
||||||
|
t.timestamps
|
||||||
|
|
||||||
|
t.belongs_to :site
|
||||||
|
t.uuid :instance_id, index: true
|
||||||
|
|
||||||
|
t.string :aasm_state, null: false, default: 'paused'
|
||||||
|
|
||||||
|
t.index %i[site_id instance_id], unique: true
|
||||||
|
end
|
||||||
|
|
||||||
|
ActivityPub.all.find_each do |activity_pub|
|
||||||
|
InstanceModeration.find_or_create_by(site: activity_pub.site, instance: activity_pub.instance)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
drop_table :instance_moderations
|
||||||
|
end
|
||||||
|
end
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue