mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-22 10:16:22 +00:00
Merge branch 'issue-15109-1' into panel.testing.sutty.nl
This commit is contained in:
commit
bead769943
111 changed files with 1621 additions and 635 deletions
2
.env
2
.env
|
@ -39,3 +39,5 @@ GITLAB_PROJECT=
|
|||
GITLAB_TOKEN=
|
||||
PGVER=15
|
||||
PGPID=/run/postgresql.pid
|
||||
PANEL_ACTOR_MENTION=@sutty@sutty.nl
|
||||
PANEL_ACTOR_SITE_ID=1
|
||||
|
|
3
Gemfile
3
Gemfile
|
@ -39,7 +39,7 @@ gem 'devise-i18n'
|
|||
gem 'devise_invitable'
|
||||
gem 'redis-client'
|
||||
gem 'hiredis-client'
|
||||
gem 'distributed-press-api-client', '~> 0.4.0rc3'
|
||||
gem 'distributed-press-api-client', '~> 0.4.1'
|
||||
gem 'email_address', git: 'https://github.com/fauno/email_address', branch: 'i18n'
|
||||
gem 'exception_notification'
|
||||
gem 'fast_blank'
|
||||
|
@ -83,6 +83,7 @@ gem 'rubanok'
|
|||
|
||||
gem 'after_commit_everywhere', '~> 1.0'
|
||||
gem 'aasm'
|
||||
gem 'que-web'
|
||||
|
||||
# database
|
||||
gem 'hairtrigger'
|
||||
|
|
20
Gemfile.lock
20
Gemfile.lock
|
@ -97,6 +97,7 @@ GEM
|
|||
ast (2.4.2)
|
||||
autoprefixer-rails (10.4.13.0)
|
||||
execjs (~> 2)
|
||||
base64 (0.2.0)
|
||||
bcrypt (3.1.20-x86_64-linux-musl)
|
||||
bcrypt_pbkdf (1.1.0-x86_64-linux-musl)
|
||||
benchmark-ips (2.12.0)
|
||||
|
@ -167,7 +168,7 @@ GEM
|
|||
devise_invitable (2.0.9)
|
||||
actionmailer (>= 5.0)
|
||||
devise (>= 4.6)
|
||||
distributed-press-api-client (0.4.0rc3)
|
||||
distributed-press-api-client (0.4.1)
|
||||
addressable (~> 2.3, >= 2.3.0)
|
||||
climate_control
|
||||
dry-schema
|
||||
|
@ -371,6 +372,8 @@ GEM
|
|||
i18n (>= 0.6.10, < 2)
|
||||
request_store (~> 1.0)
|
||||
multi_xml (0.6.0)
|
||||
mustermann (3.0.0)
|
||||
ruby2_keywords (~> 0.0.1)
|
||||
net-imap (0.4.9)
|
||||
date
|
||||
net-protocol
|
||||
|
@ -410,12 +413,18 @@ GEM
|
|||
pundit (2.3.1)
|
||||
activesupport (>= 3.0.0)
|
||||
que (2.2.1)
|
||||
que-web (0.10.0)
|
||||
que (>= 1)
|
||||
sinatra
|
||||
racc (1.7.3-x86_64-linux-musl)
|
||||
rack (2.2.8)
|
||||
rack-cors (2.0.1)
|
||||
rack (>= 2.0.0)
|
||||
rack-mini-profiler (3.1.0)
|
||||
rack (>= 1.2.0)
|
||||
rack-protection (3.2.0)
|
||||
base64 (>= 0.1.0)
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
rack-proxy (0.7.7)
|
||||
rack
|
||||
rack-test (2.1.0)
|
||||
|
@ -514,6 +523,7 @@ GEM
|
|||
ruby-statistics (3.0.2)
|
||||
ruby-vips (2.2.0)
|
||||
ffi (~> 1.12)
|
||||
ruby2_keywords (0.0.5)
|
||||
ruby2ruby (2.5.0)
|
||||
ruby_parser (~> 3.1)
|
||||
sexp_processor (~> 4.6)
|
||||
|
@ -540,6 +550,11 @@ GEM
|
|||
sexp_processor (4.17.0)
|
||||
simpleidn (0.2.1)
|
||||
unf (~> 0.1.4)
|
||||
sinatra (3.2.0)
|
||||
mustermann (~> 3.0)
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
rack-protection (= 3.2.0)
|
||||
tilt (~> 2.0)
|
||||
sourcemap (0.1.1)
|
||||
spring (4.1.1)
|
||||
spring-watcher-listen (2.1.0)
|
||||
|
@ -627,7 +642,7 @@ DEPENDENCIES
|
|||
devise
|
||||
devise-i18n
|
||||
devise_invitable
|
||||
distributed-press-api-client (~> 0.4.0rc3)
|
||||
distributed-press-api-client (~> 0.4.1)
|
||||
dotenv-rails
|
||||
down
|
||||
ed25519
|
||||
|
@ -671,6 +686,7 @@ DEPENDENCIES
|
|||
puma
|
||||
pundit
|
||||
que
|
||||
que-web
|
||||
rack-cors
|
||||
rack-mini-profiler
|
||||
rails (~> 6.1.0)
|
||||
|
|
|
@ -34,6 +34,22 @@ $sizes: (
|
|||
@import "bootstrap";
|
||||
@import "editor";
|
||||
|
||||
@each $color, $rgb in $theme-colors {
|
||||
.#{$color} {
|
||||
color: var(--#{$color});
|
||||
|
||||
&:focus {
|
||||
color: var(--#{$color});
|
||||
}
|
||||
|
||||
::-moz-selection,
|
||||
::selection {
|
||||
background: var(--#{$color});
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editor {
|
||||
.editor-content {
|
||||
figure {
|
||||
|
|
|
@ -8,48 +8,61 @@ class ActivityPubsController < ApplicationController
|
|||
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}?")
|
||||
if event == :report
|
||||
remote_flag_params(activity_pub).tap do |p|
|
||||
activity_pub.remote_flag_id = p[:remote_flag_attributes][:id]
|
||||
activity_pub.update(p)
|
||||
end
|
||||
end
|
||||
|
||||
message =
|
||||
if activity_pub.public_send(:"may_#{event}?") && activity_pub.public_send(:"#{event}!")
|
||||
:success
|
||||
else
|
||||
:error
|
||||
end
|
||||
|
||||
flash[message] = I18n.t("activity_pubs.#{event}.#{message}")
|
||||
|
||||
redirect_to_moderation_queue!
|
||||
end
|
||||
end
|
||||
|
||||
def action_on_several
|
||||
redirect_to_moderation_queue!
|
||||
|
||||
activity_pubs = site.activity_pubs.where(id: params[:activity_pub])
|
||||
|
||||
return if activity_pubs.count.zero?
|
||||
|
||||
authorize activity_pubs
|
||||
|
||||
action = params[:activity_pub_action].to_sym
|
||||
method = :"#{action}!"
|
||||
method = :"#{action}_all!"
|
||||
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)
|
||||
if action == :report
|
||||
message = remote_flag_params(activity_pubs.first).dig(:remote_flag_attributes, :message)
|
||||
|
||||
activity_pub.public_send(method)
|
||||
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
|
||||
|
||||
message = activity_pubs.public_send(method) ? :success : :error
|
||||
|
||||
flash[message] = I18n.t("activity_pubs.action_on_several.#{message}")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -5,14 +5,31 @@ class ActorModerationsController < ApplicationController
|
|||
include ModerationConcern
|
||||
include ModerationFiltersConcern
|
||||
|
||||
before_action :authenticate_usuarie!
|
||||
|
||||
breadcrumb -> { current_usuarie.email }, :edit_usuarie_registration_path
|
||||
breadcrumb 'sites.index', :sites_path, match: :exact
|
||||
|
||||
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
|
||||
if actor_event == :report
|
||||
remote_flag_params(actor_moderation).tap do |p|
|
||||
actor_moderation.remote_flag_id = p[:remote_flag_attributes][:id]
|
||||
actor_moderation.update(p)
|
||||
end
|
||||
end
|
||||
|
||||
actor_moderation.public_send(:"#{actor_event}!") if actor_moderation.public_send(:"may_#{actor_event}?")
|
||||
message =
|
||||
if actor_moderation.public_send(:"may_#{actor_event}?") && actor_moderation.public_send(:"#{actor_event}!")
|
||||
:success
|
||||
else
|
||||
:error
|
||||
end
|
||||
|
||||
flash[message] = I18n.t("actor_moderations.#{actor_event}.#{message}")
|
||||
|
||||
redirect_to_moderation_queue!
|
||||
end
|
||||
|
@ -20,31 +37,43 @@ class ActorModerationsController < ApplicationController
|
|||
|
||||
# Ver el perfil remoto
|
||||
def show
|
||||
breadcrumb site.title, site_posts_path(site)
|
||||
breadcrumb I18n.t('moderation_queue.index.title'), site_moderation_queue_path(site)
|
||||
|
||||
@remote_profile = actor_moderation.actor.content
|
||||
@moderation_queue = rubanok_process(site.activity_pubs.where(actor_id: actor_moderation.actor_id), with: ActivityPubProcessor)
|
||||
@moderation_queue = rubanok_process(site.activity_pubs.where(actor_id: actor_moderation.actor_id),
|
||||
with: ActivityPubProcessor)
|
||||
|
||||
breadcrumb @remote_profile['name'] || actor_moderation.actor.mention || actor_moderation.actor.uri, ''
|
||||
end
|
||||
|
||||
def action_on_several
|
||||
redirect_to_moderation_queue!
|
||||
|
||||
actor_moderations = site.actor_moderations.where(id: params[:actor_moderation])
|
||||
|
||||
return if actor_moderations.count.zero?
|
||||
|
||||
authorize actor_moderations
|
||||
|
||||
action = params[:actor_moderation_action].to_sym
|
||||
method = :"#{action}!"
|
||||
method = :"#{action}_all!"
|
||||
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)
|
||||
if action == :report
|
||||
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)
|
||||
actor_moderation.update(actor_moderation_params(actor_moderation))
|
||||
end
|
||||
end
|
||||
|
||||
message = actor_moderations.public_send(method) ? :success : :error
|
||||
|
||||
flash[message] = I18n.t("actor_moderations.action_on_several.#{message}")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -5,194 +5,51 @@ module Api
|
|||
module Webhooks
|
||||
# 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/}
|
||||
class SocialInboxController < BaseController
|
||||
include Api::V1::Webhooks::Concerns::WebhookConcern
|
||||
|
||||
# Validar que el token sea correcto
|
||||
before_action :usuarie
|
||||
|
||||
# Cuando una actividad ingresa en la cola de moderación, la
|
||||
# recibimos por acá
|
||||
#
|
||||
# Vamos a recibir Create, Update, Delete, Follow, Undo y obtener
|
||||
# el objeto dentro de cada una para guardar un estado asociado
|
||||
# al sitio.
|
||||
# Vamos a recibir Create, Update, Delete, Follow, Undo,
|
||||
# Announce, Like y obtener el objeto dentro de cada una para
|
||||
# guardar un estado asociado al sitio.
|
||||
#
|
||||
# El objeto del estado puede ser un objeto o une actore,
|
||||
# dependiendo de la actividad.
|
||||
def moderationqueued
|
||||
# Devuelve un error si el token no es válido
|
||||
usuarie.present?
|
||||
process! :paused
|
||||
|
||||
::ActivityPub.transaction do
|
||||
|
||||
# Crea todos los registros necesarios y actualiza el estado
|
||||
actor.present?
|
||||
instance.present?
|
||||
object.present?
|
||||
activity_pub.present?
|
||||
|
||||
activity.update_activity_pub_state!
|
||||
end
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
ExceptionNotifier.notify_exception(e,
|
||||
data: { site: site.name, usuarie: usuarie.email,
|
||||
activity: original_activity })
|
||||
ensure
|
||||
head :accepted
|
||||
end
|
||||
|
||||
# Cuando la Social Inbox acepta una actividad, la recibimos
|
||||
# igual y la guardamos por si cambiamos de idea.
|
||||
#
|
||||
# @todo DRY
|
||||
def onapproved
|
||||
::ActivityPub.transaction do
|
||||
actor.present?
|
||||
instance.present?
|
||||
object.present?
|
||||
activity.present?
|
||||
activity_pub.approve! if activity_pub.may_approve?
|
||||
end
|
||||
process! :approved
|
||||
|
||||
head :accepted
|
||||
end
|
||||
|
||||
# Cuando la Social Inbox rechaza una actividad, la recibimos
|
||||
# igual y la guardamos por si cambiamos de idea.
|
||||
#
|
||||
# @todo DRY
|
||||
def onrejected
|
||||
::ActivityPub.transaction do
|
||||
actor.present?
|
||||
instance.present?
|
||||
object.present?
|
||||
activity.present?
|
||||
activity_pub.reject! if activity_pub.may_reject?
|
||||
end
|
||||
process! :rejected
|
||||
|
||||
head :accepted
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Si el objeto ya viene incorporado en la actividad o lo tenemos
|
||||
# que traer remotamente.
|
||||
# Envía la actividad para procesamiento por separado.
|
||||
#
|
||||
# @return [Bool]
|
||||
def object_embedded?
|
||||
@object_embedded ||= original_activity[:object].is_a?(Hash)
|
||||
end
|
||||
|
||||
# Encuentra la URI del objeto o falla si no la encuentra.
|
||||
#
|
||||
# @return [String]
|
||||
def object_uri
|
||||
@object_uri ||= ::ActivityPub.uri_from_object(original_activity[:object])
|
||||
ensure
|
||||
raise ActiveRecord::RecordNotFound, 'object id missing' if @object_uri.blank?
|
||||
end
|
||||
|
||||
# Atajo a la instancia
|
||||
#
|
||||
# @return [ActivityPub::Instance]
|
||||
def instance
|
||||
actor.instance
|
||||
end
|
||||
|
||||
# Genera un objeto a partir de la actividad. Si el objeto ya
|
||||
# existe, actualiza su contenido. Si el objeto no viene
|
||||
# incorporado, obtenemos el contenido más tarde.
|
||||
#
|
||||
# @return [ActivityPub::Object]
|
||||
def object
|
||||
@object ||= ::ActivityPub::Object.find_or_initialize_by(uri: object_uri).tap do |o|
|
||||
# XXX: Si el objeto es una actividad, esto siempre va a ser
|
||||
# Generic
|
||||
o.type ||= 'ActivityPub::Object::Generic'
|
||||
|
||||
if object_embedded?
|
||||
o.content = original_object
|
||||
begin
|
||||
type = original_object[:type].presence
|
||||
o.type = "ActivityPub::Object::#{type}".constantize if type
|
||||
rescue NameError
|
||||
end
|
||||
end
|
||||
|
||||
o.save!
|
||||
|
||||
# XXX: el objeto necesita ser guardado antes de poder
|
||||
# procesarlo
|
||||
::ActivityPub::FetchJob.perform_later(site: site, object: o) unless object_embedded?
|
||||
end
|
||||
end
|
||||
|
||||
# Genera el seguimiento del estado del objeto con respecto al
|
||||
# sitio.
|
||||
#
|
||||
# @return [ActivityPub]
|
||||
def activity_pub
|
||||
@activity_pub ||= site.activity_pubs.find_or_create_by!(site: site, actor: actor, instance: instance, object_id: object.id, object_type: object.type)
|
||||
end
|
||||
|
||||
# Crea la actividad y la vincula con el estado
|
||||
#
|
||||
# @return [ActivityPub::Activity]
|
||||
def activity
|
||||
@activity ||=
|
||||
::ActivityPub::Activity
|
||||
.type_from(original_activity)
|
||||
.find_or_initialize_by(uri: original_activity[:id], activity_pub: activity_pub, actor: actor).tap do |a|
|
||||
a.content = original_activity.dup
|
||||
a.content[:object] = object.uri
|
||||
a.save!
|
||||
end
|
||||
end
|
||||
|
||||
# Actor, si no hay instancia, la crea en el momento, junto con
|
||||
# su estado de moderación.
|
||||
#
|
||||
# @return [Actor]
|
||||
def actor
|
||||
@actor ||= ::ActivityPub::Actor.find_or_initialize_by(uri: original_activity[:actor]).tap do |a|
|
||||
unless a.instance
|
||||
a.instance = ::ActivityPub::Instance.find_or_create_by(hostname: URI.parse(a.uri).hostname)
|
||||
|
||||
site.instance_moderations.find_or_create_by(instance: a.instance)
|
||||
|
||||
::ActivityPub::InstanceFetchJob.perform_later(site: site, instance: a.instance)
|
||||
end
|
||||
|
||||
a.save!
|
||||
|
||||
site.actor_moderations.find_or_create_by(actor: a)
|
||||
|
||||
::ActivityPub::ActorFetchJob.perform_later(site: site, actor: a)
|
||||
end
|
||||
end
|
||||
|
||||
# Descubre la actividad recibida, generando un error si la
|
||||
# actividad no está dirigida a nosotres.
|
||||
#
|
||||
# @todo Validar formato
|
||||
# @return [Hash]
|
||||
def original_activity
|
||||
@original_activity ||= FastJsonparser.parse(request.raw_post).tap do |activity|
|
||||
raise '@context missing' unless activity[:@context].presence
|
||||
raise 'id missing' unless activity[:id].presence
|
||||
raise 'object missing' unless activity[:object].presence
|
||||
rescue RuntimeError => e
|
||||
raise ActiveRecord::RecordNotFound, e.message
|
||||
end
|
||||
end
|
||||
|
||||
# @return [Hash,String]
|
||||
def original_object
|
||||
@original_object ||= original_activity[:object].dup.tap do |o|
|
||||
o[:@context] = original_activity[:@context].dup
|
||||
end
|
||||
# @param initial_state [Symbol]
|
||||
def process!(initial_state)
|
||||
::ActivityPub::ProcessJob.perform_later(site: site, body: request.raw_post, initial_state: initial_state)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,10 +12,6 @@ class ApplicationController < ActionController::Base
|
|||
before_action :notify_unconfirmed_email, unless: :devise_controller?
|
||||
around_action :set_locale
|
||||
|
||||
rescue_from Pundit::NilPolicyError, with: :page_not_found
|
||||
rescue_from ActionController::RoutingError, with: :page_not_found
|
||||
rescue_from ActionController::ParameterMissing, with: :page_not_found
|
||||
|
||||
before_action do
|
||||
Rack::MiniProfiler.authorize_request if current_usuarie&.email&.ends_with?('@' + ENV.fetch('SUTTY', 'sutty.nl'))
|
||||
end
|
||||
|
@ -75,11 +71,6 @@ class ApplicationController < ActionController::Base
|
|||
I18n.with_locale(current_locale, &action)
|
||||
end
|
||||
|
||||
# Muestra una página 404
|
||||
def page_not_found
|
||||
render 'application/page_not_found', status: :not_found
|
||||
end
|
||||
|
||||
# Necesario para poder acceder a Blazer. Solo les usuaries de este
|
||||
# sitio pueden acceder al panel.
|
||||
def require_usuarie
|
||||
|
|
|
@ -12,13 +12,31 @@ module ExceptionHandler
|
|||
rescue_from PageNotFound, with: :page_not_found
|
||||
rescue_from ActionController::RoutingError, with: :page_not_found
|
||||
rescue_from Pundit::NilPolicyError, with: :page_not_found
|
||||
rescue_from Pundit::NilPolicyError, with: :page_not_found
|
||||
rescue_from ActionController::RoutingError, with: :page_not_found
|
||||
rescue_from ActionController::ParameterMissing, with: :page_not_found
|
||||
end
|
||||
|
||||
def site_not_found
|
||||
reset_response!
|
||||
|
||||
flash[:error] = I18n.t('errors.site_not_found')
|
||||
|
||||
redirect_to sites_path
|
||||
end
|
||||
|
||||
def page_not_found
|
||||
send_file Rails.root.join('public', '404.html')
|
||||
reset_response!
|
||||
|
||||
render 'application/page_not_found', status: :not_found
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reset_response!
|
||||
self.response_body = nil
|
||||
@_response_body = nil
|
||||
|
||||
headers.delete('Location')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,7 +16,9 @@ module ModerationConcern
|
|||
end
|
||||
|
||||
def remote_flag_params(model)
|
||||
{ remote_flag_attributes: { id: model.remote_flag_id, message: ''.dup } }.tap do |p|
|
||||
remote_flag = ActivityPub::RemoteFlag.find_by(actor_id: model.actor_id)
|
||||
|
||||
{ remote_flag_attributes: { id: 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
|
||||
|
||||
|
|
|
@ -11,11 +11,22 @@ class FediblockStatesController < ApplicationController
|
|||
elsif fediblock_state.may_disable?
|
||||
fediblock_state.disable!
|
||||
end
|
||||
|
||||
flash[:success] = I18n.t('fediblock_states.action_on_several.success')
|
||||
rescue Exception => e
|
||||
ExceptionNotifier.notify_exception(e, data: { site: site.name })
|
||||
|
||||
flash.delete(:success)
|
||||
flash[:error] = I18n.t('fediblock_states.action_on_several.error')
|
||||
end
|
||||
|
||||
# Bloquear otras instancias
|
||||
if custom_blocklist.present?
|
||||
ActivityPub::InstanceModerationJob.perform_later(site: site, hostnames: custom_blocklist)
|
||||
if ActivityPub::InstanceModerationJob.perform_now(site: site, hostnames: custom_blocklist)
|
||||
flash[:success] = I18n.t('fediblock_states.action_on_several.custom_blocklist_success')
|
||||
else
|
||||
flash[:error] = I18n.t('fediblock_states.action_on_several.custom_blocklist_error')
|
||||
end
|
||||
end
|
||||
|
||||
redirect_to site_moderation_queue_path
|
||||
|
|
|
@ -8,29 +8,37 @@ class InstanceModerationsController < ApplicationController
|
|||
define_method(event) do
|
||||
authorize instance_moderation
|
||||
|
||||
instance_moderation.public_send(:"#{event}!") if instance_moderation.public_send(:"may_#{event}?")
|
||||
message =
|
||||
if instance_moderation.public_send(:"may_#{event}?") && instance_moderation.public_send(:"#{event}!")
|
||||
:success
|
||||
else
|
||||
:error
|
||||
end
|
||||
|
||||
flash[message] = I18n.t("instance_moderations.#{event}.#{message}")
|
||||
|
||||
redirect_to_moderation_queue!
|
||||
end
|
||||
end
|
||||
|
||||
def action_on_several
|
||||
redirect_to_moderation_queue!
|
||||
|
||||
instance_moderations = site.instance_moderations.where(id: params[:instance_moderation])
|
||||
|
||||
return if instance_moderations.count.zero?
|
||||
|
||||
authorize instance_moderations
|
||||
|
||||
action = params[:instance_moderation_action].to_sym
|
||||
method = :"#{action}!"
|
||||
may = :"may_#{action}?"
|
||||
|
||||
redirect_to_moderation_queue!
|
||||
method = :"#{action}_all!"
|
||||
|
||||
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
|
||||
message = instance_moderations.public_send(method) ? :success : :error
|
||||
|
||||
flash[:message] = I18n.t("instance_moderations.action_on_several.#{message}")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -4,8 +4,19 @@
|
|||
class ModerationQueueController < ApplicationController
|
||||
include ModerationFiltersConcern
|
||||
|
||||
before_action :authenticate_usuarie!
|
||||
|
||||
breadcrumb -> { current_usuarie.email }, :edit_usuarie_registration_path
|
||||
breadcrumb 'sites.index', :sites_path, match: :exact
|
||||
|
||||
# Cola de moderación viendo todo el sitio
|
||||
def index
|
||||
authorize ModerationQueue.new(site)
|
||||
breadcrumb site.title, site_posts_path(site)
|
||||
breadcrumb I18n.t('moderation_queue.index.title'), ''
|
||||
|
||||
site.moderation_checked!
|
||||
|
||||
# @todo cambiar el estado por query
|
||||
@activity_pubs = site.activity_pubs
|
||||
@instance_moderations = rubanok_process(site.instance_moderations, with: InstanceModerationProcessor)
|
||||
|
|
|
@ -38,7 +38,6 @@ class PostsController < ApplicationController
|
|||
@usuarie = site.usuarie? current_usuarie
|
||||
|
||||
@site_stat = SiteStat.new(site)
|
||||
dummy_data
|
||||
end
|
||||
|
||||
def show
|
||||
|
@ -84,7 +83,6 @@ class PostsController < ApplicationController
|
|||
authorize post
|
||||
breadcrumb post.title.value, site_post_path(site, post, locale: locale), match: :exact
|
||||
breadcrumb 'posts.edit', ''
|
||||
dummy_data
|
||||
end
|
||||
|
||||
def update
|
||||
|
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
module ModerationQueueHelper
|
||||
def filter_states(**args)
|
||||
params.permit(:state, :actor_state, :activity_pub_state).merge(**args)
|
||||
params.permit(:instance_state, :actor_state, :activity_pub_state).merge(**args)
|
||||
end
|
||||
|
||||
def active?(states, state_name, state)
|
||||
if params[state_name].present?
|
||||
params[state_name] == state.to_s
|
||||
else
|
||||
states.first == state
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
# 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
|
|
@ -11,19 +11,36 @@ class ActivityPub
|
|||
class FetchJob < ApplicationJob
|
||||
self.priority = 50
|
||||
|
||||
def perform(site:, object:)
|
||||
def perform(site:, object_id:)
|
||||
ActivityPub::Object.transaction do
|
||||
object = ::ActivityPub::Object.find(object_id)
|
||||
|
||||
return if object.blank?
|
||||
return if object.activity_pubs.where(aasm_state: 'removed').count.positive?
|
||||
|
||||
response = site.social_inbox.dereferencer.get(uri: object.uri)
|
||||
|
||||
# @todo Fallar cuando la respuesta no funcione?
|
||||
return unless response.ok?
|
||||
return if response.miss? && object.content.present?
|
||||
# @todo Eliminar en 410 Gone
|
||||
return unless response.success?
|
||||
# Ignorar si ya la caché fue revalidada y ya teníamos el
|
||||
# contenido
|
||||
return if response.hit? && object.content.present?
|
||||
|
||||
current_type = object.type
|
||||
content = FastJsonparser.parse(response.body)
|
||||
|
||||
object.update(content: content, type: ActivityPub::Object.type_from(content).name)
|
||||
# Modificar atómicamente
|
||||
::ActivityPub::Object.lock.find(object_id).update!(content: content, type: ActivityPub::Object.type_from(content).name)
|
||||
|
||||
object = ::ActivityPub::Object.find(object_id)
|
||||
# Actualiza la mención
|
||||
object.actor&.save! if object.actor_type?
|
||||
|
||||
# Arreglar las relaciones con actividades también
|
||||
ActivityPub.where(object_id: object.id).update_all(object_type: object.type, updated_at: Time.now)
|
||||
rescue FastJsonparser::ParseError => e
|
||||
ExceptionNotifier.notify_exception(e, data: { site: site.name, object: object.uri, body: response.body })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
16
app/jobs/activity_pub/inbox_job.rb
Normal file
16
app/jobs/activity_pub/inbox_job.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub
|
||||
class InboxJob < ApplicationJob
|
||||
self.priority = 10
|
||||
|
||||
# @param :site [Site]
|
||||
# @param :activity [String]
|
||||
# @param :action [Symbol]
|
||||
def perform(site:, activity:, action:)
|
||||
response = site.social_inbox.inbox.public_send(action, id: activity)
|
||||
|
||||
raise response.body unless response.success?
|
||||
end
|
||||
end
|
||||
end
|
|
@ -15,7 +15,7 @@ class ActivityPub
|
|||
|
||||
response = site.social_inbox.dereferencer.get(uri: uri)
|
||||
|
||||
next unless response.ok?
|
||||
next unless response.success?
|
||||
# @todo Validate schema
|
||||
next unless response.parsed_response.is_a?(DistributedPress::V1::Social::ReferencedObject)
|
||||
|
||||
|
|
|
@ -3,14 +3,13 @@
|
|||
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:)
|
||||
# @param :perform_remotely [Bool]
|
||||
def perform(site:, hostnames:, perform_remotely: true)
|
||||
# Crear las instancias que no existan todavía
|
||||
hostnames.each do |hostname|
|
||||
ActivityPub::Instance.find_or_create_by(hostname: hostname)
|
||||
ActivityPub::Instance.lock.find_or_create_by(hostname: hostname)
|
||||
end
|
||||
|
||||
instances = ActivityPub::Instance.where(hostname: hostnames)
|
||||
|
@ -21,10 +20,18 @@ class ActivityPub
|
|||
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
|
||||
site.instance_moderations.lock.find_or_create_by(instance: instance)
|
||||
end
|
||||
|
||||
scope = site.instance_moderations.where(instance_id: instances.ids)
|
||||
|
||||
if perform_remotely
|
||||
scope.block_all!
|
||||
else
|
||||
scope.block_all_without_callbacks!
|
||||
end
|
||||
|
||||
ActivityPub::SyncListsJob.perform_later(site: site)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
147
app/jobs/activity_pub/process_job.rb
Normal file
147
app/jobs/activity_pub/process_job.rb
Normal file
|
@ -0,0 +1,147 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub
|
||||
# Procesar las actividades a medida que llegan
|
||||
class ProcessJob < ApplicationJob
|
||||
attr_reader :body
|
||||
|
||||
# Procesa la actividad en segundo plano
|
||||
#
|
||||
# @param :body [String]
|
||||
# @param :initial_state [Symbol,String]
|
||||
def perform(site:, body:, initial_state: :paused)
|
||||
@body = body
|
||||
@site = site
|
||||
|
||||
ActiveRecord::Base.connection_pool.with_connection do
|
||||
::ActivityPub.transaction do
|
||||
# Crea todos los registros necesarios y actualiza el estado
|
||||
actor.present?
|
||||
instance.present?
|
||||
object.present?
|
||||
activity_pub.present?
|
||||
activity_pub.update(aasm_state: initial_state)
|
||||
|
||||
activity.update_activity_pub_state!
|
||||
end
|
||||
end
|
||||
# Al generar una excepción, en lugar de seguir intentando, enviamos
|
||||
# el reporte.
|
||||
rescue Exception => e
|
||||
ExceptionNotifier.notify_exception(e, data: { site: site.name, body: body, initial_state: initial_state, activity: original_activity, message: 'Esta acción se canceló automáticamente, para regenerarla, volver a correr el proceso con los mismos parámetros.' })
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Si el objeto ya viene incorporado en la actividad o lo tenemos
|
||||
# que traer remotamente.
|
||||
#
|
||||
# @return [Bool]
|
||||
def object_embedded?
|
||||
@object_embedded ||= original_activity[:object].is_a?(Hash)
|
||||
end
|
||||
|
||||
# Encuentra la URI del objeto o falla si no la encuentra.
|
||||
#
|
||||
# @return [String]
|
||||
def object_uri
|
||||
@object_uri ||= ::ActivityPub.uri_from_object(original_activity[:object])
|
||||
ensure
|
||||
raise ActiveRecord::RecordNotFound, 'object id missing' if @object_uri.blank?
|
||||
end
|
||||
|
||||
# Atajo a la instancia
|
||||
#
|
||||
# @return [ActivityPub::Instance]
|
||||
def instance
|
||||
actor.instance
|
||||
end
|
||||
|
||||
# Genera un objeto a partir de la actividad. Si el objeto ya
|
||||
# existe, actualiza su contenido. Si el objeto no viene
|
||||
# incorporado, obtenemos el contenido más tarde.
|
||||
#
|
||||
# @return [ActivityPub::Object]
|
||||
def object
|
||||
@object ||= ::ActivityPub::Object.lock.find_or_initialize_by(uri: object_uri).tap do |o|
|
||||
o.lock! if o.persisted?
|
||||
o.content = original_object if object_embedded?
|
||||
|
||||
o.save!
|
||||
|
||||
# XXX: el objeto necesita ser guardado antes de poder
|
||||
# procesarlo. No usamos GlobalID porque el tipo de objeto
|
||||
# cambia y produce un error de deserialización.
|
||||
::ActivityPub::FetchJob.perform_later(site: site, object_id: o.id) unless object_embedded?
|
||||
end
|
||||
end
|
||||
|
||||
# Genera el seguimiento del estado del objeto con respecto al
|
||||
# sitio.
|
||||
#
|
||||
# @return [ActivityPub]
|
||||
def activity_pub
|
||||
@activity_pub ||= site.activity_pubs.lock.find_or_create_by!(site: site, actor: actor, instance: instance,
|
||||
object_id: object.id, object_type: object.type)
|
||||
end
|
||||
|
||||
# Crea la actividad y la vincula con el estado
|
||||
#
|
||||
# @return [ActivityPub::Activity]
|
||||
def activity
|
||||
@activity ||=
|
||||
::ActivityPub::Activity
|
||||
.type_from(original_activity)
|
||||
.lock
|
||||
.find_or_initialize_by(uri: original_activity[:id], activity_pub: activity_pub, actor: actor).tap do |a|
|
||||
a.lock! if a.persisted?
|
||||
a.content = original_activity.dup
|
||||
a.content[:object] = object.uri
|
||||
a.save!
|
||||
end
|
||||
end
|
||||
|
||||
# Actor, si no hay instancia, la crea en el momento, junto con
|
||||
# su estado de moderación.
|
||||
#
|
||||
# @return [Actor]
|
||||
def actor
|
||||
@actor ||= ::ActivityPub::Actor.lock.find_or_initialize_by(uri: original_activity[:actor]).tap do |a|
|
||||
a.lock! if a.persisted?
|
||||
|
||||
unless a.instance
|
||||
a.instance = ::ActivityPub::Instance.lock.find_or_create_by(hostname: URI.parse(a.uri).hostname)
|
||||
|
||||
::ActivityPub::InstanceFetchJob.perform_later(site: site, instance: a.instance)
|
||||
end
|
||||
|
||||
site.instance_moderations.lock.find_or_create_by(instance: a.instance)
|
||||
a.save!
|
||||
|
||||
site.actor_moderations.lock.find_or_create_by(actor: a)
|
||||
|
||||
::ActivityPub::FetchJob.perform_later(site: site, object_id: a.object.id)
|
||||
end
|
||||
end
|
||||
|
||||
# @return [Hash,String]
|
||||
def original_object
|
||||
@original_object ||= original_activity[:object].dup.tap do |o|
|
||||
o[:@context] = original_activity[:@context].dup
|
||||
end
|
||||
end
|
||||
|
||||
# Descubre la actividad recibida, generando un error si la
|
||||
# actividad no está dirigida a nosotres.
|
||||
#
|
||||
# @todo Validar formato con Dry::Schema
|
||||
# @return [Hash]
|
||||
def original_activity
|
||||
@original_activity ||= FastJsonparser.parse(body).tap do |activity|
|
||||
raise '@context missing' unless activity[:@context].present?
|
||||
raise 'id missing' unless activity[:id].present?
|
||||
raise 'object missing' unless activity[:object].present?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -13,18 +13,24 @@ class ActivityPub
|
|||
self.priority = 30
|
||||
|
||||
def perform(remote_flag:)
|
||||
return if remote_flag.can_queue?
|
||||
return unless remote_flag.may_queue?
|
||||
|
||||
inbox = remote_flag.actor&.content&.[]('inbox')
|
||||
|
||||
raise 'Inbox is missing for actor' if inbox.blank?
|
||||
|
||||
remote_flag.queue!
|
||||
|
||||
client = remote_flag.site.social_inbox.client_for(remote_flag.actor&.content['inbox'])
|
||||
response = client.post(endpoint: '', body: remote_flag.content)
|
||||
uri = URI.parse(inbox)
|
||||
client = remote_flag.main_site.social_inbox.client_for(uri.origin)
|
||||
response = client.post(endpoint: uri.path, body: remote_flag.content)
|
||||
|
||||
raise 'No se pudo enviar el reporte' unless response.ok?
|
||||
raise 'No se pudo enviar el reporte' unless response.success?
|
||||
|
||||
remote_flag.send!
|
||||
remote_flag.report!
|
||||
rescue Exception => e
|
||||
ExceptionNotifier.notify_exception(e, data: { remote_flag: remote_flag.id, response: response.parsed_response })
|
||||
remote_flag.resend!
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
|
70
app/jobs/activity_pub/sync_lists_job.rb
Normal file
70
app/jobs/activity_pub/sync_lists_job.rb
Normal file
|
@ -0,0 +1,70 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub
|
||||
# Sincroniza las listas de bloqueo y permitidas con el estado actual
|
||||
# de la base de datos.
|
||||
class SyncListsJob < ApplicationJob
|
||||
# Siempre correr al final
|
||||
self.priority = 100
|
||||
|
||||
attr_reader :logs
|
||||
|
||||
# Ejecuta todas las requests y consolida los posibles errores.
|
||||
#
|
||||
# @param site [Site]
|
||||
def run(site:)
|
||||
@logs = {}
|
||||
|
||||
instance_scope = site.instance_moderations.joins(:instance)
|
||||
actor_scope = site.actor_moderations.joins(:actor)
|
||||
|
||||
blocklist = wildcardize(instance_scope.blocked.pluck(:hostname)) + actor_scope.blocked.distinct.pluck(:mention).compact + actor_scope.reported.distinct.pluck(:mention).compact
|
||||
allowlist = wildcardize(instance_scope.allowed.pluck(:hostname)) + actor_scope.allowed.distinct.pluck(:mention).compact
|
||||
pauselist = wildcardize(instance_scope.paused.pluck(:hostname)) + actor_scope.paused.distinct.pluck(:mention).compact
|
||||
|
||||
if blocklist.present?
|
||||
Rails.logger.info "Bloqueando: #{blocklist.join(', ')}"
|
||||
process(:blocked) { site.social_inbox.allowlist.delete(list: blocklist) }
|
||||
process(:blocked) { site.social_inbox.blocklist.post(list: blocklist) }
|
||||
end
|
||||
|
||||
if allowlist.present?
|
||||
Rails.logger.info "Permitiendo: #{allowlist.join(', ')}"
|
||||
process(:allowed) { site.social_inbox.blocklist.delete(list: allowlist) }
|
||||
process(:allowed) { site.social_inbox.allowlist.post(list: allowlist) }
|
||||
end
|
||||
|
||||
if pauselist.present?
|
||||
Rails.logger.info "Pausando: #{pauselist.join(', ')}"
|
||||
process(:paused) { site.social_inbox.blocklist.delete(list: pauselist) }
|
||||
process(:paused) { site.social_inbox.allowlist.delete(list: pauselist) }
|
||||
end
|
||||
|
||||
# Si alguna falló, reintentar
|
||||
raise if logs.present?
|
||||
rescue Exception => e
|
||||
ExceptionNotifier.notify_exception(e, data: { site: site.name, logs: logs, blocklist: blocklist, allowlist: allowlist, pauselist: pauselist })
|
||||
|
||||
raise
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process(stage)
|
||||
response = yield
|
||||
|
||||
return if response.success?
|
||||
|
||||
logs[stage] ||= []
|
||||
logs[stage] << { body: response.body, code: response.code }
|
||||
end
|
||||
|
||||
# @params hostnames [Array<String>]
|
||||
# @return [Array<String>]
|
||||
def wildcardize(hostnames)
|
||||
hostnames.map do |hostname|
|
||||
"@*@#{hostname}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,19 +6,24 @@
|
|||
# una actividad, puede estar destinada a varies actores dentro de Sutty,
|
||||
# con lo que generamos una cola para cada une.
|
||||
#
|
||||
#
|
||||
# @todo Ya que une actore puede hacer varias actividades sobre el mismo
|
||||
# objeto, lo correcto sería que la actividad a moderar sea una sola en
|
||||
# lugar de una lista acumulativa. Es decir cada ActivityPub representa
|
||||
# el estado del conjunto (Actor, Object, Activity)
|
||||
#
|
||||
# @see {https://www.w3.org/TR/activitypub/#client-to-server-interactions}
|
||||
class ActivityPub < ApplicationRecord
|
||||
include AASM
|
||||
include AasmEventsConcern
|
||||
IGNORED_EVENTS = %i[pause remove].freeze
|
||||
IGNORED_STATES = %i[removed].freeze
|
||||
|
||||
IGNORED_EVENTS = %i[remove]
|
||||
IGNORED_STATES = %i[removed]
|
||||
include AASM
|
||||
|
||||
belongs_to :instance
|
||||
belongs_to :site
|
||||
belongs_to :object, polymorphic: true
|
||||
belongs_to :actor
|
||||
belongs_to :remote_flag, class_name: 'ActivityPub::RemoteFlag'
|
||||
belongs_to :remote_flag, optional: true, class_name: 'ActivityPub::RemoteFlag'
|
||||
has_many :activities
|
||||
|
||||
validates :site_id, presence: true
|
||||
|
@ -38,6 +43,42 @@ class ActivityPub < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
# Obtiene el campo `url` de diversas formas. Si es una String, asumir
|
||||
# que es una URL, si es un Hash, asumir que es un Link, si es un
|
||||
# Array de Strings, obtener la primera, si es de Hash, obtener el
|
||||
# primer link con rel=canonical y mediaType=text/html
|
||||
#
|
||||
# De lo contrario devolver el ID.
|
||||
#
|
||||
# @todo Refactorizar
|
||||
# @param object [Hash]
|
||||
# @return [String]
|
||||
def self.url_from_object(object)
|
||||
raise unless object.respond_to?(:[])
|
||||
|
||||
url =
|
||||
case object['url']
|
||||
when String then object['url']
|
||||
when Hash then object['href']
|
||||
# Esto es un lío porque queremos saber si es un Array<Hash> o
|
||||
# Array<String> o mezcla y obtener el que más nos convenga o
|
||||
# adivinar uno.
|
||||
when Array
|
||||
links = object['url'].map.with_index do |link, i|
|
||||
case link
|
||||
when Hash then link
|
||||
else { 'href' => link.to_s }
|
||||
end
|
||||
end
|
||||
|
||||
links.find do |link|
|
||||
link['rel'] == 'canonical' && link['mediaType'] == 'text/html'
|
||||
end&.[]('href') || links.first&.[]('href')
|
||||
end
|
||||
|
||||
url || object['id']
|
||||
end
|
||||
|
||||
aasm do
|
||||
# Todavía no hay una decisión sobre el objeto
|
||||
state :paused, initial: true
|
||||
|
@ -50,13 +91,26 @@ class ActivityPub < ApplicationRecord
|
|||
# Le actore eliminó el objeto
|
||||
state :removed
|
||||
|
||||
# Gestionar todos los errores
|
||||
error_on_all_events do |e|
|
||||
ExceptionNotifier.notify_exception(e, data: { site: site.name, activity_pub: self.id, activity: activities.first.uri })
|
||||
end
|
||||
|
||||
# Se puede volver a pausa en caso de actualización remota, para
|
||||
# revisar los cambios.
|
||||
event :pause do
|
||||
transitions to: :paused
|
||||
end
|
||||
|
||||
# Recibir una acción de eliminación, eliminar el contenido de la
|
||||
# base de datos. Esto elimina el contenido para todos los sitios
|
||||
# porque estamos respetando lo que pidió le actore.
|
||||
event :remove do
|
||||
transitions to: :removed
|
||||
|
||||
before do
|
||||
after do
|
||||
next if object.blank?
|
||||
|
||||
object.update(content: {}) unless object.content.empty?
|
||||
end
|
||||
end
|
||||
|
@ -67,9 +121,8 @@ class ActivityPub < ApplicationRecord
|
|||
event :approve do
|
||||
transitions from: %i[paused], to: :approved
|
||||
|
||||
before do
|
||||
raise AASM::InvalidTransition unless
|
||||
site.social_inbox.inbox.accept(id: object.uri).ok?
|
||||
after do
|
||||
ActivityPub::InboxJob.perform_later(site: site, activity: activities.first.uri, action: :accept)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -77,19 +130,22 @@ class ActivityPub < ApplicationRecord
|
|||
event :reject do
|
||||
transitions from: %i[paused approved], to: :rejected
|
||||
|
||||
before do
|
||||
raise AASM::InvalidTransition unless
|
||||
site.social_inbox.inbox.reject(id: object.uri).ok?
|
||||
after do
|
||||
ActivityPub::InboxJob.perform_later(site: site, activity: activities.first.uri, action: :reject)
|
||||
end
|
||||
end
|
||||
|
||||
# Solo podemos reportarla luego de rechazarla
|
||||
# Reportarla implica rechazarla
|
||||
event :report do
|
||||
transitions from: :rejected, to: :reported
|
||||
transitions from: %i[paused approved rejected], to: :reported
|
||||
|
||||
before do
|
||||
after do
|
||||
ActivityPub::InboxJob.perform_later(site: site, activity: activities.first.uri, action: :reject)
|
||||
ActivityPub::RemoteFlagJob.perform_later(remote_flag: remote_flag) if remote_flag.waiting?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Definir eventos en masa
|
||||
include AasmEventsConcern
|
||||
end
|
||||
|
|
|
@ -20,6 +20,8 @@ class ActivityPub
|
|||
has_one :object, through: :activity_pub
|
||||
|
||||
validates :activity_pub_id, presence: true
|
||||
# Las actividades son únicas con respecto a su estado
|
||||
validates :uri, presence: true, url: true, uniqueness: { scope: :activity_pub_id, message: 'estado duplicado' }
|
||||
|
||||
# Siempre en orden descendiente para saber el último estado
|
||||
default_scope -> { order(created_at: :desc) }
|
||||
|
|
8
app/models/activity_pub/activity/announce.rb
Normal file
8
app/models/activity_pub/activity/announce.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub
|
||||
class Activity
|
||||
# Boost
|
||||
class Announce < ActivityPub::Activity; end
|
||||
end
|
||||
end
|
|
@ -13,10 +13,24 @@ class ActivityPub
|
|||
# lo haría la Social Inbox por nosotres.
|
||||
# @see {https://docs.joinmastodon.org/spec/security/#ld}
|
||||
def update_activity_pub_state!
|
||||
ActivityPub.transaction do
|
||||
ActivityPub::Object.find_by(uri: ActivityPub.uri_from_object(content['object']))&.activity_pubs&.find_each(&:remove!)
|
||||
ActiveRecord::Base.connection_pool.with_connection do
|
||||
ActivityPub.transaction do
|
||||
object = ActivityPub::Object.find_by(uri: ActivityPub.uri_from_object(content['object']))
|
||||
|
||||
activity_pub.remove!
|
||||
if object.present?
|
||||
object.activity_pubs.find_each do |activity_pub|
|
||||
activity_pub.remove! if activity_pub.may_remove?
|
||||
end
|
||||
|
||||
# Encontrar todas las acciones de moderación de le actore
|
||||
# eliminade y moverlas a eliminar.
|
||||
if (actor = ActivityPub::Actor.find_by(uri: object.uri)).present?
|
||||
ActorModeration.where(actor_id: actor.id).remove_all!
|
||||
end
|
||||
end
|
||||
|
||||
activity_pub.remove! if activity_pub.may_remove?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,8 +4,15 @@
|
|||
#
|
||||
# Una actividad de seguimiento se refiere siempre a une actore (el
|
||||
# sitio) y proviene de otre actore.
|
||||
#
|
||||
# Por ahora las solicitudes de seguimiento se auto-aprueban.
|
||||
class ActivityPub
|
||||
class Activity
|
||||
class Follow < ActivityPub::Activity; end
|
||||
class Follow < ActivityPub::Activity
|
||||
# Auto-aprobar la solicitud de seguimiento
|
||||
def update_activity_pub_state!
|
||||
activity_pub.approve! if activity_pub.may_approve?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
8
app/models/activity_pub/activity/like.rb
Normal file
8
app/models/activity_pub/activity/like.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub
|
||||
class Activity
|
||||
# Like
|
||||
class Like < ActivityPub::Activity; end
|
||||
end
|
||||
end
|
|
@ -15,15 +15,29 @@ class ActivityPub
|
|||
has_many :activities
|
||||
has_many :remote_flags
|
||||
|
||||
# Les actores son únicxs a toda la base de datos
|
||||
validates :uri, presence: true, url: true, uniqueness: true
|
||||
|
||||
before_save :mentionize!
|
||||
|
||||
# Obtiene el nombre de la Actor como mención, solo si obtuvimos el
|
||||
# contenido de antemano.
|
||||
#
|
||||
# @return [String, nil]
|
||||
def mention
|
||||
def mentionize!
|
||||
return if mention.present?
|
||||
return if content['preferredUsername'].blank?
|
||||
return if instance.blank?
|
||||
|
||||
@mention ||= "@#{content['preferredUsername']}@#{instance.hostname}"
|
||||
self.mention ||= "@#{content['preferredUsername']}@#{instance.hostname}"
|
||||
end
|
||||
|
||||
def object
|
||||
@object ||= ActivityPub::Object.lock.find_or_create_by(uri: uri)
|
||||
end
|
||||
|
||||
def content
|
||||
object.content
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,8 +6,6 @@ class ActivityPub
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
validates :uri, presence: true, uniqueness: true
|
||||
|
||||
# Cuando asignamos contenido, obtener la URI si no lo hicimos ya
|
||||
before_save :uri_from_content!, unless: :uri?
|
||||
|
||||
|
|
|
@ -31,8 +31,8 @@ class ActivityPub
|
|||
|
||||
class FediblockDownloadError < ::StandardError; end
|
||||
|
||||
validates_presence_of :title, :url, :download_url, :format
|
||||
validates_inclusion_of :format, in: %w[mastodon fediblock]
|
||||
validates_presence_of :title, :url, :format
|
||||
validates_inclusion_of :format, in: %w[mastodon fediblock none]
|
||||
|
||||
HOSTNAME_HEADERS = {
|
||||
'mastodon' => '#domain',
|
||||
|
@ -52,7 +52,7 @@ class ActivityPub
|
|||
def process!
|
||||
response = client.get(download_url)
|
||||
|
||||
raise FediblockDownloadError unless response.ok?
|
||||
raise FediblockDownloadError unless response.success?
|
||||
|
||||
Fediblock.transaction do
|
||||
csv = response.parsed_response
|
||||
|
|
|
@ -9,7 +9,7 @@ class ActivityPub
|
|||
include AASM
|
||||
|
||||
validates :aasm_state, presence: true, inclusion: { in: %w[paused allowed blocked] }
|
||||
validates :hostname, uniqueness: true, hostname: true
|
||||
validates :hostname, uniqueness: true, hostname: { allow_numeric_hostname: true }
|
||||
|
||||
has_many :activity_pubs
|
||||
has_many :actors
|
||||
|
|
|
@ -5,13 +5,62 @@ class ActivityPub
|
|||
class Object < ApplicationRecord
|
||||
include ActivityPub::Concerns::JsonLdConcern
|
||||
|
||||
before_validation :type_from_content!, unless: :type?
|
||||
|
||||
# Los objetos son únicos a toda la base de datos
|
||||
validates :uri, presence: true, url: true, uniqueness: true
|
||||
validate :uri_is_content_id?, if: :content?
|
||||
|
||||
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'])
|
||||
ActivityPub::Actor.find_by(uri: actor_uri)
|
||||
end
|
||||
|
||||
# @return [String]
|
||||
def actor_uri
|
||||
content['attributedTo']
|
||||
end
|
||||
|
||||
def actor_type?
|
||||
false
|
||||
end
|
||||
|
||||
def object_type?
|
||||
true
|
||||
end
|
||||
|
||||
# Poder explorar propiedades remotas
|
||||
#
|
||||
# @return [DistributedPress::V1::Social::ReferencedObject]
|
||||
def referenced(site)
|
||||
require 'distributed_press/v1/social/referenced_object'
|
||||
|
||||
@referenced ||= DistributedPress::V1::Social::ReferencedObject.new(object: content, dereferencer: site.social_inbox.dereferencer)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def uri_is_content_id?
|
||||
return if self.uri == content['id']
|
||||
|
||||
errors.add(:activity_pub_objects, 'El ID del objeto no coincide con su URI')
|
||||
end
|
||||
|
||||
# Encuentra el tipo a partir del contenido, si existe.
|
||||
#
|
||||
# XXX: Si el objeto es una actividad, esto siempre va a ser
|
||||
# Generic
|
||||
def type_from_content!
|
||||
self.type =
|
||||
begin
|
||||
"ActivityPub::Object::#{content['type'].presence || 'Generic'}".constantize
|
||||
rescue NameError
|
||||
ActivityPub::Object::Generic
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
# Una aplicación o instancia
|
||||
class ActivityPub
|
||||
class Object
|
||||
class Application < ActivityPub::Object; end
|
||||
class Application < ActivityPub::Object
|
||||
include Concerns::ActorTypeConcern
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
10
app/models/activity_pub/object/audio.rb
Normal file
10
app/models/activity_pub/object/audio.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# = Audio =
|
||||
#
|
||||
# Representa artículos
|
||||
class ActivityPub
|
||||
class Object
|
||||
class Audio < ActivityPub::Object; end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub
|
||||
class Object
|
||||
module Concerns
|
||||
module ActorTypeConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
# La URI de le Actor en este caso es la misma id
|
||||
#
|
||||
# @return [String]
|
||||
def actor_uri
|
||||
uri
|
||||
end
|
||||
|
||||
# El objeto referencia a une Actor
|
||||
#
|
||||
# @see {https://www.w3.org/TR/activitystreams-vocabulary/#actor-types}
|
||||
def actor_type?
|
||||
true
|
||||
end
|
||||
|
||||
# El objeto es un objeto
|
||||
#
|
||||
# @see {https://www.w3.org/TR/activitystreams-vocabulary/#object-types}
|
||||
def object_type?
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
10
app/models/activity_pub/object/document.rb
Normal file
10
app/models/activity_pub/object/document.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# = Document =
|
||||
#
|
||||
# Representa artículos
|
||||
class ActivityPub
|
||||
class Object
|
||||
class Document < ActivityPub::Object; end
|
||||
end
|
||||
end
|
10
app/models/activity_pub/object/event.rb
Normal file
10
app/models/activity_pub/object/event.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# = Event =
|
||||
#
|
||||
# Representa artículos
|
||||
class ActivityPub
|
||||
class Object
|
||||
class Event < ActivityPub::Object; end
|
||||
end
|
||||
end
|
10
app/models/activity_pub/object/group.rb
Normal file
10
app/models/activity_pub/object/group.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# = Group =
|
||||
class ActivityPub
|
||||
class Object
|
||||
class Group < ActivityPub::Object
|
||||
include Concerns::ActorTypeConcern
|
||||
end
|
||||
end
|
||||
end
|
10
app/models/activity_pub/object/image.rb
Normal file
10
app/models/activity_pub/object/image.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# = Image =
|
||||
#
|
||||
# Representa artículos
|
||||
class ActivityPub
|
||||
class Object
|
||||
class Image < ActivityPub::Object; end
|
||||
end
|
||||
end
|
|
@ -5,6 +5,8 @@
|
|||
# Una organización
|
||||
class ActivityPub
|
||||
class Object
|
||||
class Organization < ActivityPub::Object; end
|
||||
class Organization < ActivityPub::Object
|
||||
include Concerns::ActorTypeConcern
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
10
app/models/activity_pub/object/page.rb
Normal file
10
app/models/activity_pub/object/page.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# = Page =
|
||||
#
|
||||
# Representa artículos
|
||||
class ActivityPub
|
||||
class Object
|
||||
class Page < ActivityPub::Object; end
|
||||
end
|
||||
end
|
|
@ -5,6 +5,8 @@
|
|||
# Una persona, el perfil de une actore
|
||||
class ActivityPub
|
||||
class Object
|
||||
class Person < ActivityPub::Object; end
|
||||
class Person < ActivityPub::Object
|
||||
include Concerns::ActorTypeConcern
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
10
app/models/activity_pub/object/place.rb
Normal file
10
app/models/activity_pub/object/place.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# = Place =
|
||||
#
|
||||
# Representa artículos
|
||||
class ActivityPub
|
||||
class Object
|
||||
class Place < ActivityPub::Object; end
|
||||
end
|
||||
end
|
10
app/models/activity_pub/object/profile.rb
Normal file
10
app/models/activity_pub/object/profile.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# = Profile =
|
||||
#
|
||||
# Representa artículos
|
||||
class ActivityPub
|
||||
class Object
|
||||
class Profile < ActivityPub::Object; end
|
||||
end
|
||||
end
|
10
app/models/activity_pub/object/relationship.rb
Normal file
10
app/models/activity_pub/object/relationship.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# = Relationship =
|
||||
#
|
||||
# Representa artículos
|
||||
class ActivityPub
|
||||
class Object
|
||||
class Relationship < ActivityPub::Object; end
|
||||
end
|
||||
end
|
10
app/models/activity_pub/object/service.rb
Normal file
10
app/models/activity_pub/object/service.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# = Service =
|
||||
class ActivityPub
|
||||
class Object
|
||||
class Service < ActivityPub::Object
|
||||
include Concerns::ActorTypeConcern
|
||||
end
|
||||
end
|
||||
end
|
10
app/models/activity_pub/object/tombstone.rb
Normal file
10
app/models/activity_pub/object/tombstone.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# = Tombstone =
|
||||
#
|
||||
# Representa artículos
|
||||
class ActivityPub
|
||||
class Object
|
||||
class Tombstone < ActivityPub::Object; end
|
||||
end
|
||||
end
|
10
app/models/activity_pub/object/video.rb
Normal file
10
app/models/activity_pub/object/video.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# = Video =
|
||||
#
|
||||
# Representa artículos
|
||||
class ActivityPub
|
||||
class Object
|
||||
class Video < ActivityPub::Object; end
|
||||
end
|
||||
end
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
class ActivityPub
|
||||
class RemoteFlag < ApplicationRecord
|
||||
IGNORED_EVENTS = [].freeze
|
||||
IGNORED_STATES = [].freeze
|
||||
|
||||
include AASM
|
||||
include AasmEventsConcern
|
||||
|
||||
aasm do
|
||||
state :waiting, initial: true
|
||||
|
@ -14,7 +16,7 @@ class ActivityPub
|
|||
transitions from: :waiting, to: :queued
|
||||
end
|
||||
|
||||
event :send do
|
||||
event :report do
|
||||
transitions from: :queued, to: :sent
|
||||
end
|
||||
|
||||
|
@ -23,6 +25,9 @@ class ActivityPub
|
|||
end
|
||||
end
|
||||
|
||||
# Definir eventos en masa
|
||||
include AasmEventsConcern
|
||||
|
||||
belongs_to :actor
|
||||
belongs_to :site
|
||||
|
||||
|
@ -37,10 +42,18 @@ class ActivityPub
|
|||
'@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" },
|
||||
'actor' => main_site.social_inbox.actor_id,
|
||||
'content' => message.to_s,
|
||||
'object' => [ actor.uri ] + objects.pluck(:uri)
|
||||
'object' => [actor.uri] + objects.pluck(:uri)
|
||||
}
|
||||
end
|
||||
|
||||
# Este es el sitio principal que actúa como origen del reporte.
|
||||
# Tiene que tener la Social Inbox habilitada al mismo tiempo.
|
||||
#
|
||||
# @return [Site]
|
||||
def main_site
|
||||
@main_site ||= Site.find(ENV.fetch('PANEL_ACTOR_SITE_ID') { 1 })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,86 +2,69 @@
|
|||
|
||||
# Mantiene la relación entre Site y Actor
|
||||
class ActorModeration < ApplicationRecord
|
||||
include AASM
|
||||
include AasmEventsConcern
|
||||
IGNORED_EVENTS = %i[remove].freeze
|
||||
IGNORED_STATES = %i[removed].freeze
|
||||
|
||||
IGNORED_EVENTS = []
|
||||
IGNORED_STATES = []
|
||||
include AASM
|
||||
|
||||
belongs_to :site
|
||||
belongs_to :remote_flag, class_name: 'ActivityPub::RemoteFlag'
|
||||
belongs_to :remote_flag, optional: true, 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
|
||||
state :removed
|
||||
|
||||
error_on_all_events do |e|
|
||||
ExceptionNotifier.notify_exception(e, data: { site: site.name, actor: actor.uri, actor_moderation: id })
|
||||
end
|
||||
|
||||
event :pause do
|
||||
transitions from: %i[allowed blocked reported], to: :paused
|
||||
|
||||
before do
|
||||
pause_remotely!
|
||||
end
|
||||
transitions from: %i[allowed blocked reported], to: :paused, after: :synchronize!
|
||||
end
|
||||
|
||||
# Al permitir una cuenta no se permiten todos los comentarios
|
||||
# pendientes de moderación que ya hizo.
|
||||
event :allow do
|
||||
transitions from: %i[paused blocked reported], to: :allowed
|
||||
|
||||
before do
|
||||
allow_remotely!
|
||||
end
|
||||
transitions from: %i[paused blocked reported], to: :allowed, after: :synchronize!
|
||||
end
|
||||
|
||||
# Al bloquear una cuenta no se bloquean todos los comentarios
|
||||
# pendientes de moderación que hizo.
|
||||
event :block do
|
||||
transitions from: %i[paused allowed], to: :blocked
|
||||
|
||||
before do
|
||||
block_remotely!
|
||||
end
|
||||
transitions from: %i[paused allowed], to: :blocked, after: :synchronize!
|
||||
end
|
||||
|
||||
# Al reportar, necesitamos asociar una RemoteFlag para poder
|
||||
# enviarla.
|
||||
event :report do
|
||||
transitions from: %i[blocked], to: :reported
|
||||
transitions from: %i[pause allowed blocked], to: :reported, after: :synchronize!
|
||||
|
||||
before do
|
||||
after do
|
||||
ActivityPub::RemoteFlagJob.perform_later(remote_flag: remote_flag) if remote_flag.waiting?
|
||||
end
|
||||
end
|
||||
|
||||
# Si un perfil es eliminado remotamente, tenemos que dejar de
|
||||
# mostrarlo y todas sus actividades.
|
||||
event :remove do
|
||||
transitions to: :removed
|
||||
|
||||
after do
|
||||
site.activity_pubs.where(actor_id: actor_id).remove_all!
|
||||
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
|
||||
# Definir eventos en masa
|
||||
include AasmEventsConcern
|
||||
|
||||
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?
|
||||
def synchronize!
|
||||
ActivityPub::SyncListsJob.perform_later(site: site)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,7 +16,7 @@ module AasmEventsConcern
|
|||
#
|
||||
# @return [Array<Symbol>]
|
||||
def self.transitionable_events(current_state)
|
||||
self.events.select do |event|
|
||||
events.select do |event|
|
||||
aasm.events.find { |x| x.name == event }.transitions_from_state? current_state
|
||||
end
|
||||
end
|
||||
|
@ -27,5 +27,35 @@ module AasmEventsConcern
|
|||
def self.states
|
||||
aasm.states.map(&:name) - self::IGNORED_STATES
|
||||
end
|
||||
|
||||
# Define un método que cambia el estado para todos los objetos del
|
||||
# scope actual.
|
||||
#
|
||||
# @return [Bool] Si hubo al menos un error, devuelve false.
|
||||
aasm.events.map(&:name).each do |event|
|
||||
define_singleton_method(:"#{event}_all!") do
|
||||
successes = []
|
||||
|
||||
find_each do |object|
|
||||
successes << (object.public_send(:"may_#{event}?") && object.public_send(:"#{event}!"))
|
||||
end
|
||||
|
||||
successes.all?
|
||||
end
|
||||
|
||||
# Ejecuta la transición del evento en la base de datos sin
|
||||
# ejecutar los callbacks, sin modificar los items del scope que no
|
||||
# pueden transicionar.
|
||||
#
|
||||
# @return [Integer] Registros modificados
|
||||
define_singleton_method(:"#{event}_all_without_callbacks!") do
|
||||
aasm_event = aasm.events.find { |e| e.name == event }
|
||||
to_state = aasm_event.transitions.map(&:to).first
|
||||
from_states = aasm_event.transitions.map(&:from)
|
||||
|
||||
unscope(where: :aasm_state).where(aasm_state: from_states).update_all(aasm_state: to_state,
|
||||
updated_at: Time.now)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,16 +7,16 @@ class DeploySocialDistributedPress < Deploy
|
|||
# Solo luego de publicar remotamente
|
||||
DEPENDENCIES = %i[deploy_distributed_press deploy_rsync deploy_full_rsync].freeze
|
||||
|
||||
after_save :create_hooks!
|
||||
after_create :enable_fediblocks!
|
||||
|
||||
# Envía las notificaciones
|
||||
def deploy(output: false)
|
||||
with_tempfile(site.private_key_pem) do |file|
|
||||
key = Shellwords.escape file.path
|
||||
dest = Shellwords.escape destination
|
||||
|
||||
run %(bundle exec jekyll notify --trace --key #{key} --destination "#{dest}"), output: output
|
||||
run(%(bundle exec jekyll notify --trace --key #{key} --destination "#{dest}"), output: output).tap do |_|
|
||||
create_hooks!
|
||||
enable_fediblocks!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -84,7 +84,7 @@ class DeploySocialDistributedPress < Deploy
|
|||
|
||||
response = hook_client.put(event: event, hook: webhook)
|
||||
|
||||
raise ArgumentError, response.body unless response.ok?
|
||||
raise ArgumentError, response.body unless response.success?
|
||||
rescue ArgumentError => e
|
||||
ExceptionNotifier.notify_exception(e, data: { site_id: site.name, usuarie_id: rol.usuarie_id })
|
||||
end
|
||||
|
|
|
@ -20,84 +20,61 @@ class FediblockState < ApplicationRecord
|
|||
# Aunque queramos las listas habilitadas por defecto, tenemos que
|
||||
# habilitarlas luego de crearlas para poder generar la lista de
|
||||
# bloqueo en la Social Inbox.
|
||||
state :disabled, initial: true
|
||||
state :enabled
|
||||
state :disabled, initial: true, before_enter: :pause_unique_instances!
|
||||
state :enabled, before_enter: :block_instances!
|
||||
|
||||
error_on_all_events do |e|
|
||||
ExceptionNotifier.notify_exception(e, data: { site: site.name, fediblock: id })
|
||||
end
|
||||
|
||||
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.
|
||||
# Al deshabilitar, las listas pasan a modo pausa, a menos que estén
|
||||
# activas en otros listados.
|
||||
#
|
||||
# @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
|
||||
transitions from: :enabled, to: :disabled, after: :synchronize!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def actor_ids
|
||||
ActivityPub::Actor.where(instance_id: instance_ids).pluck(:id)
|
||||
def block_instances!
|
||||
ActivityPub::InstanceModerationJob.perform_later(site: site, hostnames: fediblock.hostnames, perform_remotely: false)
|
||||
end
|
||||
|
||||
def instance_ids
|
||||
fediblock.instances.pluck(:id)
|
||||
# Pausar todas las moderaciones de las instancias que no estén
|
||||
# bloqueadas por otros fediblocks.
|
||||
def pause_unique_instances!
|
||||
instance_ids = ActivityPub::Instance.where(hostname: unique_hostnames).ids
|
||||
site.instance_moderations.where(instance_id: instance_ids).pause_all_without_callbacks!
|
||||
end
|
||||
|
||||
# Todas las instancias de moderación de este sitio
|
||||
def instance_moderations
|
||||
site.instance_moderations.where(instance_id: instance_ids)
|
||||
def synchronize!
|
||||
ActivityPub::SyncListsJob.perform_later(site: site)
|
||||
end
|
||||
|
||||
# Devuelve los hostnames únicos a esta instancia.
|
||||
#
|
||||
# @return [Array<String>]
|
||||
def list_names
|
||||
@list_names ||= fediblock.instances.map do |instance|
|
||||
"@*@#{instance}"
|
||||
end
|
||||
end
|
||||
def unique_hostnames
|
||||
@unique_hostnames ||=
|
||||
begin
|
||||
other_enabled_fediblock_ids =
|
||||
site.fediblock_states.enabled.where.not(id: id).pluck(:fediblock_id)
|
||||
other_enabled_hostnames =
|
||||
ActivityPub::Fediblock
|
||||
.where(id: other_enabled_fediblock_ids)
|
||||
.pluck(:hostnames)
|
||||
.flatten
|
||||
.uniq
|
||||
|
||||
# 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?
|
||||
fediblock.hostnames - other_enabled_hostnames
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,84 +2,46 @@
|
|||
|
||||
# Mantiene el registro de relaciones entre sitios e instancias
|
||||
class InstanceModeration < ApplicationRecord
|
||||
include AASM
|
||||
include AasmEventsConcern
|
||||
IGNORED_EVENTS = [].freeze
|
||||
IGNORED_STATES = [].freeze
|
||||
|
||||
IGNORED_EVENTS = []
|
||||
IGNORED_STATES = []
|
||||
include AASM
|
||||
|
||||
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
|
||||
|
||||
error_on_all_events do |e|
|
||||
ExceptionNotifier.notify_exception(e, data: { site: site.name, instance: instance.hostname, instance_moderation: id })
|
||||
end
|
||||
|
||||
after_all_events do
|
||||
ActivityPub::SyncListsJob.perform_later(site: site)
|
||||
end
|
||||
|
||||
# Al volver la instancia a pausa no cambiamos el estado de
|
||||
# moderación de actores pre-existente.
|
||||
event :pause do
|
||||
transitions from: %i[allowed blocked], to: :paused
|
||||
|
||||
before do
|
||||
pause_remotely!
|
||||
end
|
||||
end
|
||||
|
||||
# Al permitir, solo bloqueamos la instancia, sin modificar el estado
|
||||
# de les actores y comentarios retroactivamente.
|
||||
event :allow do
|
||||
transitions from: %i[paused blocked], to: :allowed
|
||||
|
||||
before do
|
||||
allow_remotely!
|
||||
end
|
||||
end
|
||||
|
||||
# Al bloquear, solo bloqueamos la instancia, sin modificar el estado
|
||||
# de les actores y comentarios retroactivamente.
|
||||
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
|
||||
# Definir eventos en masa
|
||||
include AasmEventsConcern
|
||||
end
|
||||
|
|
3
app/models/moderation_queue.rb
Normal file
3
app/models/moderation_queue.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
ModerationQueue = Struct.new(:site)
|
5
app/models/que_job.rb
Normal file
5
app/models/que_job.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'que/active_record/model'
|
||||
|
||||
class QueJob < Que::ActiveRecord::Model; end
|
|
@ -19,6 +19,29 @@ class Site
|
|||
|
||||
before_save :generate_private_key_pem!, unless: :private_key_pem?
|
||||
|
||||
def moderation_enabled?
|
||||
deploy_social_inbox.present?
|
||||
end
|
||||
|
||||
def deploy_social_inbox
|
||||
@deploy_social_inbox ||= deploys.find_by(type: 'DeploySocialDistributedPress')
|
||||
end
|
||||
|
||||
def moderation_checked!
|
||||
deploy_social_inbox.touch
|
||||
end
|
||||
|
||||
# @return [Bool]
|
||||
def moderation_needed?
|
||||
return false unless moderation_enabled?
|
||||
|
||||
last_activity_pub = activity_pubs.order(updated_at: :desc).first&.updated_at
|
||||
|
||||
return false if last_activity_pub.blank?
|
||||
|
||||
last_activity_pub > deploy_social_inbox.updated_at
|
||||
end
|
||||
|
||||
# @return [SocialInbox]
|
||||
def social_inbox
|
||||
@social_inbox ||= SocialInbox.new(site: self)
|
||||
|
|
|
@ -11,6 +11,6 @@ InstanceModerationPolicy = Struct.new(:usuarie, :instance_moderation) do
|
|||
# En este paso tenemos varias instancias por moderar pero todas son
|
||||
# del mismo sitio.
|
||||
def action_on_several?
|
||||
instance_moderation.first.site.usuarie? usuarie
|
||||
instance_moderation.first.presence && instance_moderation.first.site.usuarie?(usuarie)
|
||||
end
|
||||
end
|
||||
|
|
9
app/policies/moderation_queue_policy.rb
Normal file
9
app/policies/moderation_queue_policy.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Si la cola de moderación está activada y le usuarie tiene permisos de
|
||||
# usuarie.
|
||||
ModerationQueuePolicy = Struct.new(:usuarie, :moderation_queue) do
|
||||
def index?
|
||||
moderation_queue.site.moderation_enabled? && moderation_queue.site.usuarie?(usuarie)
|
||||
end
|
||||
end
|
|
@ -6,7 +6,14 @@ class ActivityPubProcessor < Rubanok::Processor
|
|||
#
|
||||
# Por ahora solo queremos moderar comentarios.
|
||||
prepare do
|
||||
raw.where(object_type: %w[ActivityPub::Object::Note ActivityPub::Object::Article]).order(updated_at: :desc)
|
||||
raw
|
||||
.joins(:activities)
|
||||
.where(
|
||||
activity_pub_activities: {
|
||||
type: %w[ActivityPub::Activity::Create ActivityPub::Activity::Update]
|
||||
},
|
||||
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'|
|
||||
|
|
21
app/validators/url_validator.rb
Normal file
21
app/validators/url_validator.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Valida URLs
|
||||
#
|
||||
# @see {https://storck.io/posts/better-http-url-validation-in-ruby-on-rails/}
|
||||
class UrlValidator < ActiveModel::EachValidator
|
||||
def validate_each(record, attribute, value)
|
||||
if value.blank?
|
||||
record.errors.add(attribute, :url_missing)
|
||||
return
|
||||
end
|
||||
|
||||
uri = URI.parse(value)
|
||||
|
||||
record.errors.add(attribute, :scheme_missing) if uri.scheme.blank?
|
||||
record.errors.add(attribute, :host_missing) if uri.host.blank?
|
||||
record.errors.add(attribute, :path_missing) if uri.path.blank?
|
||||
rescue URI::Error
|
||||
record.errors.add(attribute, :invalid)
|
||||
end
|
||||
end
|
|
@ -5,4 +5,4 @@
|
|||
.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
|
||||
= render 'moderation_queue/comments', site: @site, moderation_queue: @moderation_queue
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
-# Componente Remote_Profile
|
||||
|
||||
- uri = text_plain(remote_profile['id'])
|
||||
|
||||
.py-2
|
||||
%dl
|
||||
%dt= t('.profile_name')
|
||||
|
@ -10,7 +12,7 @@
|
|||
|
||||
%dt= t('.profile_id')
|
||||
%dd
|
||||
= link_to text_plain(remote_profile['id'])
|
||||
= link_to uri, uri
|
||||
|
||||
- if remote_profile['published'].present?
|
||||
%dt= t('.profile_published')
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
-# Componente Botón general Moderación
|
||||
|
||||
- local_assigns[:method] ||= 'patch'
|
||||
- local_assigns[:class] ||= 'btn-secondary'
|
||||
- local_assigns[:class] = "btn #{local_assigns[:class]}"
|
||||
- local_assigns.delete(:text)
|
||||
|
||||
-# @todo path es obligatorio
|
||||
= button_to local_assigns[:path], **local_assigns do
|
||||
= button_to(path, **local_assigns.compact) do
|
||||
= text
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
-# 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 }
|
||||
%input.custom-control-input{ type: 'checkbox', id: id, **local_assigns.compact }
|
||||
%label.custom-control-label{ for: id }= yield
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
-# Componente Botonera de Comentarios
|
||||
|
||||
- local = { reject: { data: { confirm: t('.confirm_reject') } }, report: { class: 'ml-auto', data: { confirm: t('.confirm_report') } } }
|
||||
|
||||
.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}?")
|
||||
- possible = activity_pub.public_send(:"may_#{event}?")
|
||||
%div{ class: local.dig(event, :class) }
|
||||
= render 'components/btn_base',
|
||||
text: t(".text_#{event}"),
|
||||
path: public_send(:"site_activity_pub_#{event}_path", activity_pub_id: activity_pub),
|
||||
class: ('btn-secondary' if possible),
|
||||
disabled: !possible,
|
||||
data: local.dig(event, :data)
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
-#
|
||||
@param form [String]
|
||||
|
||||
- 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
|
||||
= render 'components/dropdown_button', form: form, text: t(".submenu_#{event.name}"), name: 'activity_pub_action', value: event.name
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
-#
|
||||
@params form [String]
|
||||
|
||||
- current_state = params[:activity_pub_state]&.to_sym || ActivityPub.states.first
|
||||
|
||||
.d-flex.py-2
|
||||
.d-flex.flex-row.justify-content-between.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/comments_checked_submenu', form: form
|
||||
|
||||
= render 'components/dropdown', text: t('.text_show') do
|
||||
= render 'components/comments_show_submenu', activity_pubs: activity_pubs
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
- 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)
|
||||
path: filter_states(activity_pub_state: state),
|
||||
class: ('active' if active?(ActivityPub.states, :activity_pub_state, state))
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
controller: 'dropdown'
|
||||
}
|
||||
}
|
||||
%button.btn.dropdown-toggle{
|
||||
%button.btn.btn-outline-secondary.dropdown-toggle{
|
||||
type: 'button',
|
||||
class: button_classes,
|
||||
data: {
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
-#
|
||||
@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
|
||||
@param text [String]
|
||||
- local_assigns.delete(:text)
|
||||
%button.dropdown-item{type: 'submit', data: { target: 'dropdown.item' }, name: name, value: value, **local_assigns.compact }= text
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
-#
|
||||
@param :text [String] Contenido del link
|
||||
@param :path [String,Hash] Link
|
||||
= link_to text, path, class: 'dropdown-item', data: { target: 'dropdown.item' }
|
||||
- local_assigns[:class] = "dropdown-item #{local_assigns[:class]}"
|
||||
= link_to text, path, class: local_assigns[:class], data: { target: 'dropdown.item' }
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
-# 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?
|
||||
- local_data = {}
|
||||
- InstanceModeration.events.each do |event|
|
||||
- possible = instance_moderation.public_send(:"may_#{event}?")
|
||||
= render 'components/btn_base',
|
||||
path: public_send(:"site_instance_moderation_#{event}_path", instance_moderation_id: instance_moderation),
|
||||
text: t(".text_#{event}"),
|
||||
class: ('btn-secondary' if possible),
|
||||
disabled: !possible,
|
||||
data: local_data[event]
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
-#
|
||||
@params form [String]
|
||||
|
||||
- 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
|
||||
= render 'components/dropdown_button', text: t(".submenu_#{event}"), name: 'instance_moderation_action', value: event, form: form
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
-#
|
||||
@params form [String]
|
||||
|
||||
- current_state = params[:state]&.to_sym || InstanceModeration.states.first
|
||||
|
||||
.d-flex.py-2
|
||||
.d-flex.flex-row.justify-content-between.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/instances_checked_submenu', form: form, current_state: current_state
|
||||
|
||||
= render 'components/dropdown', text: t('.text_show') do
|
||||
= render 'components/instances_show_submenu', instance_moderations: instance_moderations
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
- 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)
|
||||
path: filter_states(instance_state: state),
|
||||
class: ('active' if active?(InstanceModeration.states, :instance_state, state))
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
-# Componente Botonera de Moderación de Cuentas (Remote_profile)
|
||||
.d-flex.flex-row
|
||||
- btn_class = 'btn-secondary'
|
||||
.d-flex.flex-row.w-100
|
||||
- local = { report: { class: 'ml-auto', data: { confirm: t('.confirm_report') } } }
|
||||
- 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}?")
|
||||
- possible = !actor_moderation.public_send(:"may_#{actor_event}?")
|
||||
%div{ class: local.dig(actor_event, :class) }
|
||||
= 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-secondary' if possible),
|
||||
disabled: !possible,
|
||||
data: local.dig(actor_event, :data)
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
-#
|
||||
@params form [String]
|
||||
|
||||
- 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
|
||||
= render 'components/dropdown_button', text: t(".submenu_#{actor_event}"), name: 'actor_moderation_action', value: actor_event, form: form
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
-#
|
||||
@params form [String]
|
||||
|
||||
- current_state = params[:actor_state]&.to_sym || ActorModeration.states.first
|
||||
|
||||
.d-flex.py-2
|
||||
.d-flex.flex-row.justify-content-between.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/profiles_checked_submenu', form: form, current_state: current_state
|
||||
|
||||
= render 'components/dropdown', text: t('.text_show') do
|
||||
= render 'components/profiles_show_submenu', actor_moderations: actor_moderations
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
- 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)
|
||||
path: filter_states(actor_state: actor_state),
|
||||
class: ('active' if active?(ActorModeration.states, :actor_state, actor_state))
|
||||
|
|
|
@ -1,4 +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
|
||||
= render 'components/checkbox', id: id, data: { action: 'select-all#toggle', target: 'select-all.toggle', **local_assigns.compact } do
|
||||
%span.sr-only= t('.label')
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
navegador los va a asignar a este formulario.
|
||||
|
||||
@param path [String]
|
||||
@param form_id [String]
|
||||
@param form [String]
|
||||
|
||||
= form_tag path, id: form_id, method: :patch do
|
||||
= form_tag path, id: form, method: :patch do
|
||||
-# nada
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
-#
|
||||
@params form [String]
|
||||
|
||||
.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' }
|
||||
= render 'components/checkbox', id: actor_moderation.id, form: form, 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']
|
||||
- cache [actor_moderation, profile] do
|
||||
%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
|
||||
.d-flex.pb-4
|
||||
= render 'components/profiles_btn_box', actor_moderation: actor_moderation
|
||||
|
|
|
@ -1,17 +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
|
||||
= render 'components/select_all_container', path: site_actor_moderations_action_on_several_path, form: 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
|
||||
= render 'components/select_all', id: 'actors', form: form_id
|
||||
.col-11
|
||||
-# Filtros
|
||||
= render 'components/profiles_filters', actor_moderations: actor_moderations, form_id: form_id
|
||||
= render 'components/profiles_filters', actor_moderations: actor_moderations, form: 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
|
||||
- next if actor_moderation.actor.content.empty?
|
||||
%hr
|
||||
= render 'account', actor_moderation: actor_moderation, profile: actor_moderation.actor.content, form: form_id
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
.form-group
|
||||
= label_tag 'custom_blocklist', t('moderation_queue.instances.custom_block')
|
||||
= text_area_tag 'custom_blocklist', nil, class: 'form-control'
|
||||
= text_area_tag 'custom_blocklist', nil, class: 'form-control', placeholder: t('moderation_queue.instances.custom_block_placeholder')
|
||||
|
|
|
@ -1,33 +1,47 @@
|
|||
-#
|
||||
Componente Comentario
|
||||
|
||||
@param site [Site]
|
||||
@param form [String]
|
||||
@param profile [Hash]
|
||||
@param comment [Hash]
|
||||
@param activity_pub [ActivityPub]
|
||||
|
||||
- in_reply_to = text_plain comment['inReplyTo']
|
||||
:ruby
|
||||
begin
|
||||
if in_reply_to && (remote_object = object.referenced(site)['inReplyTo'])
|
||||
in_reply_to = ActivityPub.url_from_object(remote_object)
|
||||
end
|
||||
rescue Exception => e
|
||||
ExceptionNotifier.notify_exception(e, data: { site: site.name, object: comment })
|
||||
end
|
||||
- summary = text_plain comment['summary']
|
||||
-# @todo Generar un desplegable con todas las opciones
|
||||
- url = text_plain ActivityPub.url_from_object(comment)
|
||||
|
||||
.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
|
||||
= render 'components/checkbox', id: activity_pub.id, name: 'activity_pub[]', value: activity_pub.id, data: { target: 'select-all.input' }, form: form
|
||||
.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
|
||||
- cache [activity_pub, comment] do
|
||||
.d-flex.flex-row.align-items-center.justify-content-between
|
||||
%h4.mb-0
|
||||
%a{ href: text_plain(comment['attributedTo']) }= text_plain profile['preferredUsername']
|
||||
%a{ href: url }
|
||||
%small
|
||||
%a{ href: in_reply_to }= in_reply_to
|
||||
.content
|
||||
- if summary.present?
|
||||
= render 'layouts/details', summary: summary, summary_class: 'h5' do
|
||||
= 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.mb-3
|
||||
- if summary.present?
|
||||
= render 'layouts/details', summary: summary, summary_class: 'h5' do
|
||||
= sanitize comment['content']
|
||||
- else
|
||||
= sanitize comment['content']
|
||||
- else
|
||||
= sanitize comment['content']
|
||||
= render 'components/comments_btn_box', activity_pub: activity_pub
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
- form_id = 'activity_pub_action_on_several'
|
||||
|
||||
= render 'components/select_all_container', path: site_activity_pubs_action_on_several_path, form_id: form_id
|
||||
= render 'components/select_all_container', path: site_activity_pubs_action_on_several_path, form: 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
|
||||
= render 'components/select_all', id: 'select-all-comments', form: form_id
|
||||
.col-11
|
||||
-# Filtros
|
||||
= render 'components/comments_filters', activity_pubs: moderation_queue, form_id: form_id
|
||||
= render 'components/comments_filters', activity_pubs: moderation_queue, form: 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
|
||||
- next if activity_pub.object.content.empty?
|
||||
- next if activity_pub.actor.content.empty?
|
||||
%hr
|
||||
= render 'comment', comment: activity_pub.object.content, profile: activity_pub.actor.content, activity_pub: activity_pub, form_id: form_id
|
||||
= render 'moderation_queue/comment', comment: activity_pub.object.content, profile: activity_pub.actor.content, activity_pub: activity_pub, form: form_id, site: site, object: activity_pub.object
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
- usuaries = instance.content.dig('usage', 'users', 'active_month')
|
||||
- usuaries ||= instance.content.dig('stats', 'user_count')
|
||||
- title = sanitize(instance.content['title'])
|
||||
|
||||
.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' }
|
||||
= render 'components/checkbox', id: instance.hostname, form: form, 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
|
||||
- cache [instance_moderation, instance] do
|
||||
%h4
|
||||
%a{ href: instance.uri }= title || instance.hostname
|
||||
- if title.present?
|
||||
= " (#{instance.hostname})".html_safe
|
||||
.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
|
||||
|
|
|
@ -1,22 +1,21 @@
|
|||
- 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
|
||||
= render 'components/select_all_container', path: site_instance_moderations_action_on_several_path, form: 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
|
||||
= render 'components/select_all', id: 'instances', form: form_id
|
||||
.col-11
|
||||
-# Filtros
|
||||
= render 'components/instances_filters', instance_moderations: instance_moderations, form_id: form_id
|
||||
= render 'components/instances_filters', instance_moderations: instance_moderations, form: 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
|
||||
= render 'moderation_queue/instance', instance_moderation: instance_moderation, instance: instance_moderation.instance, form: form_id
|
||||
|
||||
%hr
|
||||
%div
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
|
||||
%main.row
|
||||
%aside.menu.col-lg-3
|
||||
= render 'sites/header', site: @site
|
||||
|
||||
= render 'sites/status', site: @site
|
||||
|
||||
= render 'sites/build', site: @site, class: 'btn-block mb-3'
|
||||
.mb-3
|
||||
= render 'sites/header', site: @site
|
||||
= render 'sites/status', site: @site
|
||||
= render 'sites/build', site: @site, class: 'btn-block'
|
||||
= render 'sites/moderation_queue', site: @site, class: 'btn-block'
|
||||
|
||||
%h3= t('posts.new')
|
||||
%table.table.table-sm.mb-3
|
||||
|
|
9
app/views/sites/_moderation_queue.haml
Normal file
9
app/views/sites/_moderation_queue.haml
Normal file
|
@ -0,0 +1,9 @@
|
|||
- if policy(ModerationQueue.new(site)).index?
|
||||
- moderation_needed = site.moderation_needed?
|
||||
|
||||
- local_assigns[:class] = "btn btn-secondary #{local_assigns[:class]}"
|
||||
= link_to site_moderation_queue_path(site), class: local_assigns[:class], title: (t('.moderation_needed') if moderation_needed) do
|
||||
= t('moderation_queue.index.title')
|
||||
- if moderation_needed
|
||||
%span.primary ⏺
|
||||
%span.sr-only= t('.moderation_needed')
|
|
@ -15,43 +15,39 @@
|
|||
%tbody
|
||||
- @sites.each do |site|
|
||||
- next unless site.jekyll?
|
||||
- rol = current_usuarie.rol_for_site(site)
|
||||
-#
|
||||
TODO: Solo les usuaries cachean porque tenemos que separar
|
||||
les botones por permisos.
|
||||
- cache_if (rol.usuarie? && !rol.temporal), [site, I18n.locale] do
|
||||
%tr
|
||||
%td
|
||||
%h2
|
||||
- if policy(site).show?
|
||||
= link_to site.title, site_posts_path(site, locale: site.default_locale)
|
||||
- else
|
||||
= site.title
|
||||
%p.lead= site.description
|
||||
%br
|
||||
= link_to t('.visit'), site.url, class: 'btn btn-secondary'
|
||||
- if rol.temporal
|
||||
= button_to t('sites.invitations.accept'),
|
||||
site_usuaries_accept_invitation_path(site),
|
||||
method: :patch,
|
||||
title: t('help.sites.invitations.accept'),
|
||||
class: 'btn btn-secondary'
|
||||
= button_to t('sites.invitations.reject'),
|
||||
site_usuaries_reject_invitation_path(site),
|
||||
method: :patch,
|
||||
title: t('help.sites.invitations.reject'),
|
||||
class: 'btn btn-secondary'
|
||||
%tr
|
||||
%td
|
||||
%h2
|
||||
- if policy(site).show?
|
||||
= link_to site.title, site_posts_path(site, locale: site.default_locale)
|
||||
- else
|
||||
- if policy(site).show?
|
||||
= render 'layouts/btn_with_tooltip',
|
||||
tooltip: t('help.sites.edit_posts'),
|
||||
type: 'success',
|
||||
link: site_path(site),
|
||||
text: t('sites.posts')
|
||||
- if policy(SiteUsuarie.new(site, current_usuarie)).index?
|
||||
= render 'layouts/btn_with_tooltip',
|
||||
tooltip: t('usuaries.index.help.self'),
|
||||
text: t('usuaries.index.title'),
|
||||
type: 'info',
|
||||
link: site_usuaries_path(site)
|
||||
= render 'sites/build', site: site
|
||||
= site.title
|
||||
%p.lead= site.description
|
||||
%br
|
||||
= link_to t('.visit'), site.url, class: 'btn btn-secondary'
|
||||
- if current_usuarie.rol_for_site(site).temporal?
|
||||
= render 'components/btn_base',
|
||||
text: t('sites.invitations.accept'),
|
||||
path: site_usuaries_accept_invitation_path(site),
|
||||
title: t('help.sites.invitations.accept'),
|
||||
class: 'btn-secondary'
|
||||
= render 'components/btn_base',
|
||||
text: t('sites.invitations.reject'),
|
||||
path: site_usuaries_reject_invitation_path(site),
|
||||
title: t('help.sites.invitations.reject'),
|
||||
class: 'btn-secondary'
|
||||
- else
|
||||
- if policy(site).show?
|
||||
= render 'layouts/btn_with_tooltip',
|
||||
tooltip: t('help.sites.edit_posts'),
|
||||
type: 'success',
|
||||
link: site_path(site),
|
||||
text: t('sites.posts')
|
||||
= render 'sites/build', site: site
|
||||
= render 'sites/moderation_queue', site: site
|
||||
- if policy(SiteUsuarie.new(site, current_usuarie)).index?
|
||||
= render 'layouts/btn_with_tooltip',
|
||||
tooltip: t('usuaries.index.help.self'),
|
||||
text: t('usuaries.index.title'),
|
||||
type: 'info',
|
||||
link: site_usuaries_path(site)
|
||||
|
|
|
@ -142,7 +142,7 @@ Rails.application.configure do
|
|||
}
|
||||
config.action_mailer.default_options = { from: ENV.fetch('DEFAULT_FROM', "noreply@sutty.nl") }
|
||||
|
||||
config.middleware.use ExceptionNotification::Rack, gitlab: {}, ignore_exceptions: ['DeployJob::DeployAlreadyRunningException']
|
||||
config.middleware.use ExceptionNotification::Rack, gitlab: {}, error_grouping: true, ignore_exceptions: ['DeployJob::DeployAlreadyRunningException']
|
||||
|
||||
Rails.application.routes.default_url_options[:host] = "panel.#{ENV.fetch('SUTTY', 'sutty.nl')}"
|
||||
Rails.application.routes.default_url_options[:protocol] = 'https'
|
||||
|
|
5
config/initializers/que_web.rb
Normal file
5
config/initializers/que_web.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Que::Web.use(Rack::Auth::Basic) do |user, password|
|
||||
[user, password] == [ENV['HTTP_BASIC_USER'], ENV['HTTP_BASIC_PASSWORD']]
|
||||
end
|
|
@ -50,12 +50,20 @@ en:
|
|||
pm: pm
|
||||
format: '%-I:%M %p'
|
||||
components:
|
||||
actor:
|
||||
user: Username
|
||||
profile: Profile
|
||||
profile_name: Profile name
|
||||
preferred_name: Name in Fediverse
|
||||
profile_id: ID
|
||||
profile_published: Published
|
||||
profile_summary: Summary
|
||||
block_list:
|
||||
know_more: Know more
|
||||
instances_blocked: Instances blocked
|
||||
instances_filters:
|
||||
text_show: Show
|
||||
text_checked: With selected
|
||||
text_checked: With selected...
|
||||
instances_checked_submenu:
|
||||
submenu_pause: Moderate
|
||||
submenu_allow: Allow
|
||||
|
@ -66,7 +74,7 @@ en:
|
|||
submenu_blocked: "Blocked (%{count})"
|
||||
comments_filters:
|
||||
text_show: Show
|
||||
text_checked: With selected
|
||||
text_checked: With selected...
|
||||
comments_checked_submenu:
|
||||
submenu_pause: Pause
|
||||
submenu_approve: Approve
|
||||
|
@ -79,7 +87,7 @@ en:
|
|||
submenu_reported: "Reported (%{count})"
|
||||
profiles_filters:
|
||||
text_show: Show
|
||||
text_checked: With selected
|
||||
text_checked: With selected...
|
||||
profiles_checked_submenu:
|
||||
submenu_pause: Pause
|
||||
submenu_allow: Allow
|
||||
|
@ -98,26 +106,68 @@ en:
|
|||
text_reject: Reject
|
||||
text_reply: Reply
|
||||
text_report: Report
|
||||
confirm_report: "Send report to the remote instance? This action will also reject the comment."
|
||||
confirm_reject: "Reject this comment? Please notice we can't undo this action at this moment."
|
||||
instances_btn_box:
|
||||
text_check: Check case by case
|
||||
text_pause: Check case by case
|
||||
text_allow: Allow everything
|
||||
text_deny: Block instance
|
||||
text_block: 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
|
||||
confirm_report: "Send report to the remote instance? This action will also block the account."
|
||||
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}."
|
||||
activity_pubs:
|
||||
action_on_several:
|
||||
success: "Several comments have changed moderation state. You can find them using the filters on the Comments section."
|
||||
error: "There was an error while changing moderation state. We received a report and will be acting on it soon."
|
||||
approve:
|
||||
success: "Comment approved."
|
||||
error: "There was an error while approving the comment. We received a report and will be acting on it soon."
|
||||
reject:
|
||||
success: "Comment rejected. You can report it using the Report button."
|
||||
error: "There was an error while rejecting the comment. We received a report and will be acting on it soon."
|
||||
report:
|
||||
success: "Comment reported."
|
||||
error: "There was an error while reporting the comment. We received a report and will be acting on it soon."
|
||||
actor_moderations:
|
||||
action_on_several:
|
||||
success: "Several accounts have changed moderation state. You can find them using the filters on the Accounts section. No action was performed over existing Comments."
|
||||
error: "There was an error while changing moderation state. We received a report and will be acting on it soon."
|
||||
pause:
|
||||
success: "Account paused. No action was performed on existing Comments."
|
||||
error: "There was an error while pausing the account. We received a report and will be acting on it soon."
|
||||
allow:
|
||||
success: "Account allowed. All of their comments from now on will be approved automatically. No action was performed over existing Comments."
|
||||
error: "There was an error while allowing the account. We received a report and will be acting on it soon."
|
||||
block:
|
||||
success: "Account blocked. All of their comments from now on will be rejected automatically. No action was performed over existing Comments. If you want to report it to their instance, please use the Report button."
|
||||
error: "There was an error while blocking the account. We received a report and will be acting on it soon."
|
||||
report:
|
||||
success: "Account reported."
|
||||
error: "There was an error while reporting the account. We received a report and will be acting on it soon."
|
||||
instance_moderations:
|
||||
action_on_several:
|
||||
success: "Several instances have changed moderation state. You can find them using the filters on the Instances section. No action was performed over existing Accounts and Comments."
|
||||
error: "There was an error while changing moderation state. We received a report and will be acting on it soon."
|
||||
pause:
|
||||
success: "Instance paused. All of their comments and accounts from now on will need to be moderated individually. No action was performed over existing Accounts and Comments."
|
||||
error: "There was an error while pausing the instance. We received a report and will be acting on it soon."
|
||||
allow:
|
||||
success: "Instance allowed. All of their comments and accounts from now on will be approved automatically. No action was performed over existing Accounts and Comments."
|
||||
error: "There was an error while allowing the instance. We received a report and will be acting on it soon."
|
||||
block:
|
||||
success: "Instance blocked. All of their comments and accounts from now on will be rejected automatically. No action was performed over existing Accounts and Comments."
|
||||
error: "There was an error while blocking the instance. We received a report and will be acting on it soon."
|
||||
fediblock_states:
|
||||
action_on_several:
|
||||
success: "Blocklists have been enabled, you can find their instances by filtering by Blocked. You can approve them individually on the Accounts section. No action was performed over existing Accounts and Comments."
|
||||
error: "There was an error while enabling or disabling blocklists. We received a report and will be acting on it soon."
|
||||
custom_blocklist_success: "Custom blocklist has been added, you can find the instances by filtering by Blocked. No action was performed over existing Accounts and Comments."
|
||||
custom_blocklist_error: "There was an error while adding a custom blocklist. We received a report and will be acting on it soon."
|
||||
moderation_queue:
|
||||
everything: 'Select all'
|
||||
nothing: "There's nothing for this filter"
|
||||
|
@ -131,8 +181,11 @@ en:
|
|||
reply_to: Reply to
|
||||
instances:
|
||||
title: My block lists
|
||||
description: Description
|
||||
description: "Blocklists contain instances known for hosting hate speech, promote fascism, violence, sexual/gendered abuse and/or misinformation."
|
||||
custom_block: Custom block lists
|
||||
custom_block_placeholder: |
|
||||
a.doma.in
|
||||
per.li.ne
|
||||
submit: Save block lists
|
||||
instance:
|
||||
users: "Users:"
|
||||
|
@ -339,6 +392,7 @@ en:
|
|||
lang:
|
||||
not_available: "This language is not yet available, would you help us by translating Sutty into it?"
|
||||
errors:
|
||||
site_not_found: "Site not found, or maybe you don't have access to it."
|
||||
argument_error: 'Argument `%{argument}` must be an instance of %{class}'
|
||||
unknown_locale: 'Unknown %{locale} locale'
|
||||
posts:
|
||||
|
@ -515,6 +569,8 @@ en:
|
|||
column: "Country"
|
||||
empty: "(couldn't detect country)"
|
||||
sites:
|
||||
moderation_queue:
|
||||
moderation_needed: "There are new activities pending revision since the last time you moderated."
|
||||
donations:
|
||||
url: 'https://donaciones.sutty.nl/en/'
|
||||
text: 'Support us'
|
||||
|
|
|
@ -50,12 +50,20 @@ es:
|
|||
pm: pm
|
||||
format: '%-H:%M'
|
||||
components:
|
||||
actor:
|
||||
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
|
||||
block_list:
|
||||
know_more: Saber más (en inglés)
|
||||
instances_blocked: Instancias bloqueadas
|
||||
instances_filters:
|
||||
text_show: Ver
|
||||
text_checked: Con los marcados
|
||||
text_checked: Con los marcados...
|
||||
instances_checked_submenu:
|
||||
submenu_pause: Moderar caso por caso
|
||||
submenu_allow: Permitir todo
|
||||
|
@ -66,7 +74,7 @@ es:
|
|||
submenu_blocked: "Bloqueadas (%{count})"
|
||||
comments_filters:
|
||||
text_show: Ver
|
||||
text_checked: Con los marcados
|
||||
text_checked: Con los marcados...
|
||||
comments_checked_submenu:
|
||||
submenu_pause: Pausar
|
||||
submenu_approve: Aprobar
|
||||
|
@ -79,7 +87,7 @@ es:
|
|||
submenu_reported: "Reportados (%{count})"
|
||||
profiles_filters:
|
||||
text_show: Ver
|
||||
text_checked: Con los marcados
|
||||
text_checked: Con los marcados...
|
||||
profiles_checked_submenu:
|
||||
submenu_pause: Pausar
|
||||
submenu_allow: Aceptar
|
||||
|
@ -97,26 +105,68 @@ es:
|
|||
text_approve: Aceptar
|
||||
text_reject: Rechazar
|
||||
text_report: Reportar
|
||||
confirm_report: "¿Enviar el reporte a la instancia remota? Esta acción también rechazará el comentario."
|
||||
confirm_reject: "¿Rechazar este comentario? Tené en cuenta que por el momento no es posible deshacer esta acción."
|
||||
instances_btn_box:
|
||||
text_check: Moderar caso por caso
|
||||
text_pause: Moderar caso por caso
|
||||
text_allow: Permitir todo
|
||||
text_deny: Bloquear instancia
|
||||
text_block: Bloquear instancia
|
||||
profiles_btn_box:
|
||||
text_pause: Revisar siempre
|
||||
text_allow: Aprobar siempre
|
||||
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
|
||||
confirm_report: "¿Enviar el reporte a la instancia remota? Esta acción también bloqueará la cuenta."
|
||||
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}."
|
||||
activity_pubs:
|
||||
action_on_several:
|
||||
success: "Se ha modificado el estado de moderación de varios comentarios. Podés encontrarlos usando los filtros en la sección Comentarios."
|
||||
error: "Hubo un error al modificar el estado de moderación de varios comentarios. Hemos recibido el reporte y lo estaremos verificando."
|
||||
approve:
|
||||
success: "Comentario aprobado."
|
||||
error: "No se puedo aprobar el comentario. Hemos recibido el reporte y lo estaremos verificando."
|
||||
reject:
|
||||
success: "Comentario rechazado. Podés reportarlo usando el botón Reportar."
|
||||
error: "No se puedo rechazar el comentario. Hemos recibido el reporte y lo estaremos verificando."
|
||||
report:
|
||||
success: "Comentario reportado."
|
||||
error: "No se puedo reportar el comentario. Hemos recibido el reporte y lo estaremos verificando."
|
||||
actor_moderations:
|
||||
action_on_several:
|
||||
success: "Se ha modificado el estado de moderación de varias cuentas. Podés encontrarlas usando los filtros en la sección Cuentas. No se modificaron comentarios pre-existentes."
|
||||
error: "Hubo un error al modificar el estado de moderación de varias cuentas. Hemos recibido el reporte y lo estaremos verificando."
|
||||
pause:
|
||||
success: "Cuenta pausada. Todos los comentarios que haga necesitan ser aprobados manualmente en la sección Comentarios. No se modificaron comentarios pre-existentes."
|
||||
error: "No se pudo pausar la cuenta. Hemos recibido el reporte y lo estaremos verificando."
|
||||
allow:
|
||||
success: "Cuenta permitida. Todos los comentarios que haga serán aprobados inmediatamente. No se modificaron comentarios pre-existentes."
|
||||
error: "No se pudo permitir la cuenta. Hemos recibido el reporte y lo estaremos verificando."
|
||||
block:
|
||||
success: "Cuenta bloqueada. Todos los comentarios que haga serán rechazados inmediatamente. Si querés reportarla a su instancia, podés usar el botón Reportar. No se modificaron comentarios pre-existentes."
|
||||
error: "No se pudo bloquear la cuenta. Hemos recibido el reporte y lo estaremos verificando."
|
||||
report:
|
||||
success: "Cuenta reportada a su instancia."
|
||||
error: "No se pudo reportar la cuenta. Hemos recibido el reporte y lo estaremos verificando."
|
||||
instance_moderations:
|
||||
action_on_several:
|
||||
success: "Se ha modificado el estado de moderación de varias instancias. Podés encontrarlas usando los filtros en la sección Instancias. No se modificaron cuentas y comentarios pre-existentes."
|
||||
error: "Hubo un error al modificar el estado de moderación de varias instancias. Hemos recibido el reporte y lo estaremos verificando."
|
||||
pause:
|
||||
success: "Instancia pausada. A partir de ahora, todos los comentarios y cuentas de esta instancia necesitan ser aprobados manualmente. No se ha modificado el estado de moderación de cuentas ni comentarios pre-existentes."
|
||||
error: "No se pudo pausar la instancia. Hemos recibido el reporte y lo estaremos verificando."
|
||||
allow:
|
||||
success: "Instancia permitida. A partir de ahora, todos los comentarios y cuentas pendientes serán aprobados inmediatamente. No se modificaron cuentas ni comentarios pre-existentes."
|
||||
error: "No se pudo permitir la instancia. Hemos recibido el reporte y lo estaremos verificando."
|
||||
block:
|
||||
success: "Instancia bloqueada. A partir de ahora, todos los comentarios y cuentas serán rechazados inmediatamente. No se modificaron cuentas ni comentarios pre-existentes."
|
||||
error: "No se pudo bloquear la instancia. Hemos recibido el reporte y lo estaremos verificando."
|
||||
fediblock_states:
|
||||
action_on_several:
|
||||
success: "Se habilitaron las listas de bloqueo, podés encontrar las instancias filtrando por Bloqueadas. Podés activarlas individualmente en la sección Cuentas. No se modificaron cuentas ni comentarios pre-existentes."
|
||||
error: "Hubo un error al activar o desactivar listas de bloqueo, ya recibimos el reporte y lo estaremos verificando."
|
||||
custom_blocklist_success: "Se agregaron las instancias personalizadas a la lista de bloqueo, podés encontrarlas filtrando por Bloqueadas. Podés aprobarlas individualmente en la sección Cuentas. No se modificaron cuentas ni comentarios pre-existentes."
|
||||
custom_blocklist_error: "Hubo un error al agregar instancias personalizadas a la lista de bloqueo, ya recibimos el reporte y lo estaremos verificando."
|
||||
moderation_queue:
|
||||
everything: 'Seleccionar todo'
|
||||
nothing: 'No hay nada para este filtro'
|
||||
|
@ -130,8 +180,11 @@ es:
|
|||
reply_to: En respuesta a
|
||||
instances:
|
||||
title: Mis listas de bloqueo
|
||||
description: Descripción de listas de bloqueo
|
||||
description: "Las listas de bloqueo contienen instancias conocidas por alojar discurso de odio, promover el fascismo, la violencia, abuso sexual y/o desinformación."
|
||||
custom_block: Lista personalizada de bloqueo
|
||||
custom_block_placeholder: |
|
||||
un.domin.io
|
||||
por.lin.ea
|
||||
submit: Guardar listas de bloqueo
|
||||
instance:
|
||||
users: "Usuaries:"
|
||||
|
@ -338,6 +391,7 @@ es:
|
|||
lang:
|
||||
not_available: "Este idioma todavía no está disponible, ¿nos ayudas a agregarlo y mantenerlo?"
|
||||
errors:
|
||||
site_not_found: "No encontramos ese sitio o quizás no tengas acceso."
|
||||
argument_error: 'El argumento `%{argument}` debe ser una instancia de %{class}'
|
||||
unknown_locale: 'El idioma %{locale} es desconocido'
|
||||
posts:
|
||||
|
@ -519,6 +573,8 @@ es:
|
|||
column: "País"
|
||||
empty: "(no se pudo detectar el país)"
|
||||
sites:
|
||||
moderation_queue:
|
||||
moderation_needed: "Hay actividades pendientes de revisión desde la última vez que moderaste."
|
||||
donations:
|
||||
url: 'https://donaciones.sutty.nl/'
|
||||
text: 'Apoyá nuestro trabajo'
|
||||
|
|
|
@ -4,6 +4,9 @@ Rails.application.routes.draw do
|
|||
devise_for :usuaries
|
||||
get '/.well-known/change-password', to: redirect('/usuaries/edit')
|
||||
|
||||
require 'que/web'
|
||||
mount Que::Web => '/que'
|
||||
|
||||
root 'application#index'
|
||||
|
||||
constraints(Constraints::ApiSubdomain.new) do
|
||||
|
|
13
db/migrate/20240307201510_remove_actor_moderations.rb
Normal file
13
db/migrate/20240307201510_remove_actor_moderations.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Mover todes les actores eliminades
|
||||
class RemoveActorModerations < ActiveRecord::Migration[6.1]
|
||||
def up
|
||||
actor_ids =
|
||||
ActivityPub.where(aasm_state: 'removed', object_type: 'ActivityPub::Object::Person').distinct.pluck(:actor_id)
|
||||
|
||||
ActorModeration.where(actor_id: actor_ids).remove_all!
|
||||
end
|
||||
|
||||
def down; end
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue