mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-22 16:16:21 +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=
|
GITLAB_TOKEN=
|
||||||
PGVER=15
|
PGVER=15
|
||||||
PGPID=/run/postgresql.pid
|
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 'devise_invitable'
|
||||||
gem 'redis-client'
|
gem 'redis-client'
|
||||||
gem 'hiredis-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 'email_address', git: 'https://github.com/fauno/email_address', branch: 'i18n'
|
||||||
gem 'exception_notification'
|
gem 'exception_notification'
|
||||||
gem 'fast_blank'
|
gem 'fast_blank'
|
||||||
|
@ -83,6 +83,7 @@ gem 'rubanok'
|
||||||
|
|
||||||
gem 'after_commit_everywhere', '~> 1.0'
|
gem 'after_commit_everywhere', '~> 1.0'
|
||||||
gem 'aasm'
|
gem 'aasm'
|
||||||
|
gem 'que-web'
|
||||||
|
|
||||||
# database
|
# database
|
||||||
gem 'hairtrigger'
|
gem 'hairtrigger'
|
||||||
|
|
20
Gemfile.lock
20
Gemfile.lock
|
@ -97,6 +97,7 @@ GEM
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
autoprefixer-rails (10.4.13.0)
|
autoprefixer-rails (10.4.13.0)
|
||||||
execjs (~> 2)
|
execjs (~> 2)
|
||||||
|
base64 (0.2.0)
|
||||||
bcrypt (3.1.20-x86_64-linux-musl)
|
bcrypt (3.1.20-x86_64-linux-musl)
|
||||||
bcrypt_pbkdf (1.1.0-x86_64-linux-musl)
|
bcrypt_pbkdf (1.1.0-x86_64-linux-musl)
|
||||||
benchmark-ips (2.12.0)
|
benchmark-ips (2.12.0)
|
||||||
|
@ -167,7 +168,7 @@ GEM
|
||||||
devise_invitable (2.0.9)
|
devise_invitable (2.0.9)
|
||||||
actionmailer (>= 5.0)
|
actionmailer (>= 5.0)
|
||||||
devise (>= 4.6)
|
devise (>= 4.6)
|
||||||
distributed-press-api-client (0.4.0rc3)
|
distributed-press-api-client (0.4.1)
|
||||||
addressable (~> 2.3, >= 2.3.0)
|
addressable (~> 2.3, >= 2.3.0)
|
||||||
climate_control
|
climate_control
|
||||||
dry-schema
|
dry-schema
|
||||||
|
@ -371,6 +372,8 @@ GEM
|
||||||
i18n (>= 0.6.10, < 2)
|
i18n (>= 0.6.10, < 2)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
multi_xml (0.6.0)
|
multi_xml (0.6.0)
|
||||||
|
mustermann (3.0.0)
|
||||||
|
ruby2_keywords (~> 0.0.1)
|
||||||
net-imap (0.4.9)
|
net-imap (0.4.9)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
|
@ -410,12 +413,18 @@ GEM
|
||||||
pundit (2.3.1)
|
pundit (2.3.1)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
que (2.2.1)
|
que (2.2.1)
|
||||||
|
que-web (0.10.0)
|
||||||
|
que (>= 1)
|
||||||
|
sinatra
|
||||||
racc (1.7.3-x86_64-linux-musl)
|
racc (1.7.3-x86_64-linux-musl)
|
||||||
rack (2.2.8)
|
rack (2.2.8)
|
||||||
rack-cors (2.0.1)
|
rack-cors (2.0.1)
|
||||||
rack (>= 2.0.0)
|
rack (>= 2.0.0)
|
||||||
rack-mini-profiler (3.1.0)
|
rack-mini-profiler (3.1.0)
|
||||||
rack (>= 1.2.0)
|
rack (>= 1.2.0)
|
||||||
|
rack-protection (3.2.0)
|
||||||
|
base64 (>= 0.1.0)
|
||||||
|
rack (~> 2.2, >= 2.2.4)
|
||||||
rack-proxy (0.7.7)
|
rack-proxy (0.7.7)
|
||||||
rack
|
rack
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
|
@ -514,6 +523,7 @@ GEM
|
||||||
ruby-statistics (3.0.2)
|
ruby-statistics (3.0.2)
|
||||||
ruby-vips (2.2.0)
|
ruby-vips (2.2.0)
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
|
ruby2_keywords (0.0.5)
|
||||||
ruby2ruby (2.5.0)
|
ruby2ruby (2.5.0)
|
||||||
ruby_parser (~> 3.1)
|
ruby_parser (~> 3.1)
|
||||||
sexp_processor (~> 4.6)
|
sexp_processor (~> 4.6)
|
||||||
|
@ -540,6 +550,11 @@ GEM
|
||||||
sexp_processor (4.17.0)
|
sexp_processor (4.17.0)
|
||||||
simpleidn (0.2.1)
|
simpleidn (0.2.1)
|
||||||
unf (~> 0.1.4)
|
unf (~> 0.1.4)
|
||||||
|
sinatra (3.2.0)
|
||||||
|
mustermann (~> 3.0)
|
||||||
|
rack (~> 2.2, >= 2.2.4)
|
||||||
|
rack-protection (= 3.2.0)
|
||||||
|
tilt (~> 2.0)
|
||||||
sourcemap (0.1.1)
|
sourcemap (0.1.1)
|
||||||
spring (4.1.1)
|
spring (4.1.1)
|
||||||
spring-watcher-listen (2.1.0)
|
spring-watcher-listen (2.1.0)
|
||||||
|
@ -627,7 +642,7 @@ DEPENDENCIES
|
||||||
devise
|
devise
|
||||||
devise-i18n
|
devise-i18n
|
||||||
devise_invitable
|
devise_invitable
|
||||||
distributed-press-api-client (~> 0.4.0rc3)
|
distributed-press-api-client (~> 0.4.1)
|
||||||
dotenv-rails
|
dotenv-rails
|
||||||
down
|
down
|
||||||
ed25519
|
ed25519
|
||||||
|
@ -671,6 +686,7 @@ DEPENDENCIES
|
||||||
puma
|
puma
|
||||||
pundit
|
pundit
|
||||||
que
|
que
|
||||||
|
que-web
|
||||||
rack-cors
|
rack-cors
|
||||||
rack-mini-profiler
|
rack-mini-profiler
|
||||||
rails (~> 6.1.0)
|
rails (~> 6.1.0)
|
||||||
|
|
|
@ -34,6 +34,22 @@ $sizes: (
|
||||||
@import "bootstrap";
|
@import "bootstrap";
|
||||||
@import "editor";
|
@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 {
|
||||||
.editor-content {
|
.editor-content {
|
||||||
figure {
|
figure {
|
||||||
|
|
|
@ -8,27 +8,43 @@ class ActivityPubsController < ApplicationController
|
||||||
define_method(event) do
|
define_method(event) do
|
||||||
authorize activity_pub
|
authorize activity_pub
|
||||||
|
|
||||||
activity_pub.update(remote_flag_params(activity_pub)) if event == :report
|
if event == :report
|
||||||
activity_pub.public_send(:"#{event}!") if activity_pub.public_send(:"may_#{event}?")
|
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!
|
redirect_to_moderation_queue!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def action_on_several
|
def action_on_several
|
||||||
|
redirect_to_moderation_queue!
|
||||||
|
|
||||||
activity_pubs = site.activity_pubs.where(id: params[:activity_pub])
|
activity_pubs = site.activity_pubs.where(id: params[:activity_pub])
|
||||||
|
|
||||||
|
return if activity_pubs.count.zero?
|
||||||
|
|
||||||
authorize activity_pubs
|
authorize activity_pubs
|
||||||
|
|
||||||
action = params[:activity_pub_action].to_sym
|
action = params[:activity_pub_action].to_sym
|
||||||
method = :"#{action}!"
|
method = :"#{action}_all!"
|
||||||
may = :"may_#{action}?"
|
may = :"may_#{action}?"
|
||||||
|
|
||||||
redirect_to_moderation_queue!
|
|
||||||
|
|
||||||
return unless ActivityPub.events.include? action
|
return unless ActivityPub.events.include? action
|
||||||
|
|
||||||
# Crear una sola remote flag por autore
|
# Crear una sola remote flag por autore
|
||||||
|
ActivityPub.transaction do
|
||||||
if action == :report
|
if action == :report
|
||||||
message = remote_flag_params(activity_pubs.first).dig(:remote_flag_attributes, :message)
|
message = remote_flag_params(activity_pubs.first).dig(:remote_flag_attributes, :message)
|
||||||
|
|
||||||
|
@ -44,12 +60,9 @@ class ActivityPubsController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
ActivityPub.transaction do
|
message = activity_pubs.public_send(method) ? :success : :error
|
||||||
activity_pubs.find_each do |activity_pub|
|
|
||||||
next unless activity_pub.public_send(may)
|
|
||||||
|
|
||||||
activity_pub.public_send(method)
|
flash[message] = I18n.t("activity_pubs.action_on_several.#{message}")
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -5,14 +5,31 @@ class ActorModerationsController < ApplicationController
|
||||||
include ModerationConcern
|
include ModerationConcern
|
||||||
include ModerationFiltersConcern
|
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|
|
ActorModeration.events.each do |actor_event|
|
||||||
define_method(actor_event) do
|
define_method(actor_event) do
|
||||||
authorize actor_moderation
|
authorize actor_moderation
|
||||||
|
|
||||||
# Crea una RemoteFlag si se envían los parámetros adecuados
|
# 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!
|
redirect_to_moderation_queue!
|
||||||
end
|
end
|
||||||
|
@ -20,32 +37,44 @@ class ActorModerationsController < ApplicationController
|
||||||
|
|
||||||
# Ver el perfil remoto
|
# Ver el perfil remoto
|
||||||
def show
|
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
|
@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
|
end
|
||||||
|
|
||||||
def action_on_several
|
def action_on_several
|
||||||
|
redirect_to_moderation_queue!
|
||||||
|
|
||||||
actor_moderations = site.actor_moderations.where(id: params[:actor_moderation])
|
actor_moderations = site.actor_moderations.where(id: params[:actor_moderation])
|
||||||
|
|
||||||
|
return if actor_moderations.count.zero?
|
||||||
|
|
||||||
authorize actor_moderations
|
authorize actor_moderations
|
||||||
|
|
||||||
action = params[:actor_moderation_action].to_sym
|
action = params[:actor_moderation_action].to_sym
|
||||||
method = :"#{action}!"
|
method = :"#{action}_all!"
|
||||||
may = :"may_#{action}?"
|
may = :"may_#{action}?"
|
||||||
|
|
||||||
redirect_to_moderation_queue!
|
|
||||||
|
|
||||||
return unless ActorModeration.events.include? action
|
return unless ActorModeration.events.include? action
|
||||||
|
|
||||||
ActorModeration.transaction do
|
ActorModeration.transaction do
|
||||||
|
if action == :report
|
||||||
actor_moderations.find_each do |actor_moderation|
|
actor_moderations.find_each do |actor_moderation|
|
||||||
next unless actor_moderation.public_send(may)
|
next unless actor_moderation.public_send(may)
|
||||||
|
|
||||||
actor_moderation.update(actor_moderation_params(actor_moderation)) if action == :report
|
actor_moderation.update(actor_moderation_params(actor_moderation))
|
||||||
|
|
||||||
actor_moderation.public_send(method)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
message = actor_moderations.public_send(method) ? :success : :error
|
||||||
|
|
||||||
|
flash[message] = I18n.t("actor_moderations.action_on_several.#{message}")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -5,194 +5,51 @@ module Api
|
||||||
module Webhooks
|
module Webhooks
|
||||||
# Recibe webhooks de la Social Inbox
|
# Recibe webhooks de la Social Inbox
|
||||||
#
|
#
|
||||||
# @todo Mover todo a un Job que obtenga el objeto remoto antes de
|
|
||||||
# instanciar el objeto localmente en lugar de arreglarlo después y
|
|
||||||
# poder responder lo más rápido posible el webhook.
|
|
||||||
# @see {https://www.w3.org/TR/activitypub/}
|
# @see {https://www.w3.org/TR/activitypub/}
|
||||||
class SocialInboxController < BaseController
|
class SocialInboxController < BaseController
|
||||||
include Api::V1::Webhooks::Concerns::WebhookConcern
|
include Api::V1::Webhooks::Concerns::WebhookConcern
|
||||||
|
|
||||||
|
# Validar que el token sea correcto
|
||||||
|
before_action :usuarie
|
||||||
|
|
||||||
# Cuando una actividad ingresa en la cola de moderación, la
|
# Cuando una actividad ingresa en la cola de moderación, la
|
||||||
# recibimos por acá
|
# recibimos por acá
|
||||||
#
|
#
|
||||||
# Vamos a recibir Create, Update, Delete, Follow, Undo y obtener
|
# Vamos a recibir Create, Update, Delete, Follow, Undo,
|
||||||
# el objeto dentro de cada una para guardar un estado asociado
|
# Announce, Like y obtener el objeto dentro de cada una para
|
||||||
# al sitio.
|
# guardar un estado asociado al sitio.
|
||||||
#
|
#
|
||||||
# El objeto del estado puede ser un objeto o une actore,
|
# El objeto del estado puede ser un objeto o une actore,
|
||||||
# dependiendo de la actividad.
|
# dependiendo de la actividad.
|
||||||
def moderationqueued
|
def moderationqueued
|
||||||
# Devuelve un error si el token no es válido
|
process! :paused
|
||||||
usuarie.present?
|
|
||||||
|
|
||||||
::ActivityPub.transaction do
|
|
||||||
|
|
||||||
# Crea todos los registros necesarios y actualiza el estado
|
|
||||||
actor.present?
|
|
||||||
instance.present?
|
|
||||||
object.present?
|
|
||||||
activity_pub.present?
|
|
||||||
|
|
||||||
activity.update_activity_pub_state!
|
|
||||||
end
|
|
||||||
rescue ActiveRecord::RecordInvalid => e
|
|
||||||
ExceptionNotifier.notify_exception(e,
|
|
||||||
data: { site: site.name, usuarie: usuarie.email,
|
|
||||||
activity: original_activity })
|
|
||||||
ensure
|
|
||||||
head :accepted
|
head :accepted
|
||||||
end
|
end
|
||||||
|
|
||||||
# Cuando la Social Inbox acepta una actividad, la recibimos
|
# Cuando la Social Inbox acepta una actividad, la recibimos
|
||||||
# igual y la guardamos por si cambiamos de idea.
|
# igual y la guardamos por si cambiamos de idea.
|
||||||
#
|
|
||||||
# @todo DRY
|
|
||||||
def onapproved
|
def onapproved
|
||||||
::ActivityPub.transaction do
|
process! :approved
|
||||||
actor.present?
|
|
||||||
instance.present?
|
|
||||||
object.present?
|
|
||||||
activity.present?
|
|
||||||
activity_pub.approve! if activity_pub.may_approve?
|
|
||||||
end
|
|
||||||
|
|
||||||
head :accepted
|
head :accepted
|
||||||
end
|
end
|
||||||
|
|
||||||
# Cuando la Social Inbox rechaza una actividad, la recibimos
|
# Cuando la Social Inbox rechaza una actividad, la recibimos
|
||||||
# igual y la guardamos por si cambiamos de idea.
|
# igual y la guardamos por si cambiamos de idea.
|
||||||
#
|
|
||||||
# @todo DRY
|
|
||||||
def onrejected
|
def onrejected
|
||||||
::ActivityPub.transaction do
|
process! :rejected
|
||||||
actor.present?
|
|
||||||
instance.present?
|
|
||||||
object.present?
|
|
||||||
activity.present?
|
|
||||||
activity_pub.reject! if activity_pub.may_reject?
|
|
||||||
end
|
|
||||||
|
|
||||||
head :accepted
|
head :accepted
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Si el objeto ya viene incorporado en la actividad o lo tenemos
|
# Envía la actividad para procesamiento por separado.
|
||||||
# que traer remotamente.
|
|
||||||
#
|
#
|
||||||
# @return [Bool]
|
# @param initial_state [Symbol]
|
||||||
def object_embedded?
|
def process!(initial_state)
|
||||||
@object_embedded ||= original_activity[:object].is_a?(Hash)
|
::ActivityPub::ProcessJob.perform_later(site: site, body: request.raw_post, initial_state: initial_state)
|
||||||
end
|
|
||||||
|
|
||||||
# Encuentra la URI del objeto o falla si no la encuentra.
|
|
||||||
#
|
|
||||||
# @return [String]
|
|
||||||
def object_uri
|
|
||||||
@object_uri ||= ::ActivityPub.uri_from_object(original_activity[:object])
|
|
||||||
ensure
|
|
||||||
raise ActiveRecord::RecordNotFound, 'object id missing' if @object_uri.blank?
|
|
||||||
end
|
|
||||||
|
|
||||||
# Atajo a la instancia
|
|
||||||
#
|
|
||||||
# @return [ActivityPub::Instance]
|
|
||||||
def instance
|
|
||||||
actor.instance
|
|
||||||
end
|
|
||||||
|
|
||||||
# Genera un objeto a partir de la actividad. Si el objeto ya
|
|
||||||
# existe, actualiza su contenido. Si el objeto no viene
|
|
||||||
# incorporado, obtenemos el contenido más tarde.
|
|
||||||
#
|
|
||||||
# @return [ActivityPub::Object]
|
|
||||||
def object
|
|
||||||
@object ||= ::ActivityPub::Object.find_or_initialize_by(uri: object_uri).tap do |o|
|
|
||||||
# XXX: Si el objeto es una actividad, esto siempre va a ser
|
|
||||||
# Generic
|
|
||||||
o.type ||= 'ActivityPub::Object::Generic'
|
|
||||||
|
|
||||||
if object_embedded?
|
|
||||||
o.content = original_object
|
|
||||||
begin
|
|
||||||
type = original_object[:type].presence
|
|
||||||
o.type = "ActivityPub::Object::#{type}".constantize if type
|
|
||||||
rescue NameError
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
o.save!
|
|
||||||
|
|
||||||
# XXX: el objeto necesita ser guardado antes de poder
|
|
||||||
# procesarlo
|
|
||||||
::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
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,10 +12,6 @@ class ApplicationController < ActionController::Base
|
||||||
before_action :notify_unconfirmed_email, unless: :devise_controller?
|
before_action :notify_unconfirmed_email, unless: :devise_controller?
|
||||||
around_action :set_locale
|
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
|
before_action do
|
||||||
Rack::MiniProfiler.authorize_request if current_usuarie&.email&.ends_with?('@' + ENV.fetch('SUTTY', 'sutty.nl'))
|
Rack::MiniProfiler.authorize_request if current_usuarie&.email&.ends_with?('@' + ENV.fetch('SUTTY', 'sutty.nl'))
|
||||||
end
|
end
|
||||||
|
@ -75,11 +71,6 @@ class ApplicationController < ActionController::Base
|
||||||
I18n.with_locale(current_locale, &action)
|
I18n.with_locale(current_locale, &action)
|
||||||
end
|
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
|
# Necesario para poder acceder a Blazer. Solo les usuaries de este
|
||||||
# sitio pueden acceder al panel.
|
# sitio pueden acceder al panel.
|
||||||
def require_usuarie
|
def require_usuarie
|
||||||
|
|
|
@ -12,13 +12,31 @@ module ExceptionHandler
|
||||||
rescue_from PageNotFound, with: :page_not_found
|
rescue_from PageNotFound, with: :page_not_found
|
||||||
rescue_from ActionController::RoutingError, 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 Pundit::NilPolicyError, with: :page_not_found
|
||||||
|
rescue_from ActionController::RoutingError, with: :page_not_found
|
||||||
|
rescue_from ActionController::ParameterMissing, with: :page_not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
def site_not_found
|
def site_not_found
|
||||||
|
reset_response!
|
||||||
|
|
||||||
|
flash[:error] = I18n.t('errors.site_not_found')
|
||||||
|
|
||||||
redirect_to sites_path
|
redirect_to sites_path
|
||||||
end
|
end
|
||||||
|
|
||||||
def page_not_found
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,7 +16,9 @@ module ModerationConcern
|
||||||
end
|
end
|
||||||
|
|
||||||
def remote_flag_params(model)
|
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][:site_id] = model.site_id
|
||||||
p[:remote_flag_attributes][:actor_id] = model.actor_id
|
p[:remote_flag_attributes][:actor_id] = model.actor_id
|
||||||
|
|
||||||
|
|
|
@ -11,11 +11,22 @@ class FediblockStatesController < ApplicationController
|
||||||
elsif fediblock_state.may_disable?
|
elsif fediblock_state.may_disable?
|
||||||
fediblock_state.disable!
|
fediblock_state.disable!
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
# Bloquear otras instancias
|
# Bloquear otras instancias
|
||||||
if custom_blocklist.present?
|
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
|
end
|
||||||
|
|
||||||
redirect_to site_moderation_queue_path
|
redirect_to site_moderation_queue_path
|
||||||
|
|
|
@ -8,29 +8,37 @@ class InstanceModerationsController < ApplicationController
|
||||||
define_method(event) do
|
define_method(event) do
|
||||||
authorize instance_moderation
|
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!
|
redirect_to_moderation_queue!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def action_on_several
|
def action_on_several
|
||||||
|
redirect_to_moderation_queue!
|
||||||
|
|
||||||
instance_moderations = site.instance_moderations.where(id: params[:instance_moderation])
|
instance_moderations = site.instance_moderations.where(id: params[:instance_moderation])
|
||||||
|
|
||||||
|
return if instance_moderations.count.zero?
|
||||||
|
|
||||||
authorize instance_moderations
|
authorize instance_moderations
|
||||||
|
|
||||||
action = params[:instance_moderation_action].to_sym
|
action = params[:instance_moderation_action].to_sym
|
||||||
method = :"#{action}!"
|
method = :"#{action}_all!"
|
||||||
may = :"may_#{action}?"
|
|
||||||
|
|
||||||
redirect_to_moderation_queue!
|
|
||||||
|
|
||||||
return unless InstanceModeration.events.include? action
|
return unless InstanceModeration.events.include? action
|
||||||
|
|
||||||
InstanceModeration.transaction do
|
InstanceModeration.transaction do
|
||||||
instance_moderations.find_each do |instance_moderation|
|
message = instance_moderations.public_send(method) ? :success : :error
|
||||||
instance_moderation.public_send(method) if instance_moderation.public_send(may)
|
|
||||||
end
|
flash[:message] = I18n.t("instance_moderations.action_on_several.#{message}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,19 @@
|
||||||
class ModerationQueueController < ApplicationController
|
class ModerationQueueController < ApplicationController
|
||||||
include ModerationFiltersConcern
|
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
|
# Cola de moderación viendo todo el sitio
|
||||||
def index
|
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
|
# @todo cambiar el estado por query
|
||||||
@activity_pubs = site.activity_pubs
|
@activity_pubs = site.activity_pubs
|
||||||
@instance_moderations = rubanok_process(site.instance_moderations, with: InstanceModerationProcessor)
|
@instance_moderations = rubanok_process(site.instance_moderations, with: InstanceModerationProcessor)
|
||||||
|
|
|
@ -38,7 +38,6 @@ class PostsController < ApplicationController
|
||||||
@usuarie = site.usuarie? current_usuarie
|
@usuarie = site.usuarie? current_usuarie
|
||||||
|
|
||||||
@site_stat = SiteStat.new(site)
|
@site_stat = SiteStat.new(site)
|
||||||
dummy_data
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
@ -84,7 +83,6 @@ class PostsController < ApplicationController
|
||||||
authorize post
|
authorize post
|
||||||
breadcrumb post.title.value, site_post_path(site, post, locale: locale), match: :exact
|
breadcrumb post.title.value, site_post_path(site, post, locale: locale), match: :exact
|
||||||
breadcrumb 'posts.edit', ''
|
breadcrumb 'posts.edit', ''
|
||||||
dummy_data
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
|
|
@ -2,6 +2,14 @@
|
||||||
|
|
||||||
module ModerationQueueHelper
|
module ModerationQueueHelper
|
||||||
def filter_states(**args)
|
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
|
||||||
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
|
class FetchJob < ApplicationJob
|
||||||
self.priority = 50
|
self.priority = 50
|
||||||
|
|
||||||
def perform(site:, object:)
|
def perform(site:, object_id:)
|
||||||
ActivityPub::Object.transaction do
|
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?
|
return if object.activity_pubs.where(aasm_state: 'removed').count.positive?
|
||||||
|
|
||||||
response = site.social_inbox.dereferencer.get(uri: object.uri)
|
response = site.social_inbox.dereferencer.get(uri: object.uri)
|
||||||
|
|
||||||
# @todo Fallar cuando la respuesta no funcione?
|
# @todo Fallar cuando la respuesta no funcione?
|
||||||
return unless response.ok?
|
# @todo Eliminar en 410 Gone
|
||||||
return if response.miss? && object.content.present?
|
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)
|
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
|
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)
|
response = site.social_inbox.dereferencer.get(uri: uri)
|
||||||
|
|
||||||
next unless response.ok?
|
next unless response.success?
|
||||||
# @todo Validate schema
|
# @todo Validate schema
|
||||||
next unless response.parsed_response.is_a?(DistributedPress::V1::Social::ReferencedObject)
|
next unless response.parsed_response.is_a?(DistributedPress::V1::Social::ReferencedObject)
|
||||||
|
|
||||||
|
|
|
@ -3,14 +3,13 @@
|
||||||
class ActivityPub
|
class ActivityPub
|
||||||
# Bloquea varias instancias de una sola vez
|
# Bloquea varias instancias de una sola vez
|
||||||
class InstanceModerationJob < ApplicationJob
|
class InstanceModerationJob < ApplicationJob
|
||||||
self.priority = 50
|
|
||||||
|
|
||||||
# @param :site [Site]
|
# @param :site [Site]
|
||||||
# @param :hostnames [Array<String>]
|
# @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
|
# Crear las instancias que no existan todavía
|
||||||
hostnames.each do |hostname|
|
hostnames.each do |hostname|
|
||||||
ActivityPub::Instance.find_or_create_by(hostname: hostname)
|
ActivityPub::Instance.lock.find_or_create_by(hostname: hostname)
|
||||||
end
|
end
|
||||||
|
|
||||||
instances = ActivityPub::Instance.where(hostname: hostnames)
|
instances = ActivityPub::Instance.where(hostname: hostnames)
|
||||||
|
@ -21,10 +20,18 @@ class ActivityPub
|
||||||
instances.find_each do |instance|
|
instances.find_each do |instance|
|
||||||
# Esto bloquea cada una individualmente en la Social Inbox,
|
# Esto bloquea cada una individualmente en la Social Inbox,
|
||||||
# idealmente son pocas instancias las que aparecen.
|
# idealmente son pocas instancias las que aparecen.
|
||||||
site.instance_moderations.find_or_create_by(instance: instance).tap do |instance_moderation|
|
site.instance_moderations.lock.find_or_create_by(instance: instance)
|
||||||
instance_moderation.block! if instance_moderation.may_block?
|
end
|
||||||
end
|
|
||||||
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
|
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
|
self.priority = 30
|
||||||
|
|
||||||
def perform(remote_flag:)
|
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!
|
remote_flag.queue!
|
||||||
|
|
||||||
client = remote_flag.site.social_inbox.client_for(remote_flag.actor&.content['inbox'])
|
uri = URI.parse(inbox)
|
||||||
response = client.post(endpoint: '', body: remote_flag.content)
|
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
|
rescue Exception => e
|
||||||
ExceptionNotifier.notify_exception(e, data: { remote_flag: remote_flag.id, response: response.parsed_response })
|
ExceptionNotifier.notify_exception(e, data: { remote_flag: remote_flag.id, response: response.parsed_response })
|
||||||
|
remote_flag.resend!
|
||||||
raise
|
raise
|
||||||
end
|
end
|
||||||
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,
|
# una actividad, puede estar destinada a varies actores dentro de Sutty,
|
||||||
# con lo que generamos una cola para cada une.
|
# 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}
|
# @see {https://www.w3.org/TR/activitypub/#client-to-server-interactions}
|
||||||
class ActivityPub < ApplicationRecord
|
class ActivityPub < ApplicationRecord
|
||||||
include AASM
|
IGNORED_EVENTS = %i[pause remove].freeze
|
||||||
include AasmEventsConcern
|
IGNORED_STATES = %i[removed].freeze
|
||||||
|
|
||||||
IGNORED_EVENTS = %i[remove]
|
include AASM
|
||||||
IGNORED_STATES = %i[removed]
|
|
||||||
|
|
||||||
belongs_to :instance
|
belongs_to :instance
|
||||||
belongs_to :site
|
belongs_to :site
|
||||||
belongs_to :object, polymorphic: true
|
belongs_to :object, polymorphic: true
|
||||||
belongs_to :actor
|
belongs_to :actor
|
||||||
belongs_to :remote_flag, class_name: 'ActivityPub::RemoteFlag'
|
belongs_to :remote_flag, optional: true, class_name: 'ActivityPub::RemoteFlag'
|
||||||
has_many :activities
|
has_many :activities
|
||||||
|
|
||||||
validates :site_id, presence: true
|
validates :site_id, presence: true
|
||||||
|
@ -38,6 +43,42 @@ class ActivityPub < ApplicationRecord
|
||||||
end
|
end
|
||||||
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
|
aasm do
|
||||||
# Todavía no hay una decisión sobre el objeto
|
# Todavía no hay una decisión sobre el objeto
|
||||||
state :paused, initial: true
|
state :paused, initial: true
|
||||||
|
@ -50,13 +91,26 @@ class ActivityPub < ApplicationRecord
|
||||||
# Le actore eliminó el objeto
|
# Le actore eliminó el objeto
|
||||||
state :removed
|
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
|
# Recibir una acción de eliminación, eliminar el contenido de la
|
||||||
# base de datos. Esto elimina el contenido para todos los sitios
|
# base de datos. Esto elimina el contenido para todos los sitios
|
||||||
# porque estamos respetando lo que pidió le actore.
|
# porque estamos respetando lo que pidió le actore.
|
||||||
event :remove do
|
event :remove do
|
||||||
transitions to: :removed
|
transitions to: :removed
|
||||||
|
|
||||||
before do
|
after do
|
||||||
|
next if object.blank?
|
||||||
|
|
||||||
object.update(content: {}) unless object.content.empty?
|
object.update(content: {}) unless object.content.empty?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -67,9 +121,8 @@ class ActivityPub < ApplicationRecord
|
||||||
event :approve do
|
event :approve do
|
||||||
transitions from: %i[paused], to: :approved
|
transitions from: %i[paused], to: :approved
|
||||||
|
|
||||||
before do
|
after do
|
||||||
raise AASM::InvalidTransition unless
|
ActivityPub::InboxJob.perform_later(site: site, activity: activities.first.uri, action: :accept)
|
||||||
site.social_inbox.inbox.accept(id: object.uri).ok?
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -77,19 +130,22 @@ class ActivityPub < ApplicationRecord
|
||||||
event :reject do
|
event :reject do
|
||||||
transitions from: %i[paused approved], to: :rejected
|
transitions from: %i[paused approved], to: :rejected
|
||||||
|
|
||||||
before do
|
after do
|
||||||
raise AASM::InvalidTransition unless
|
ActivityPub::InboxJob.perform_later(site: site, activity: activities.first.uri, action: :reject)
|
||||||
site.social_inbox.inbox.reject(id: object.uri).ok?
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Solo podemos reportarla luego de rechazarla
|
# Reportarla implica rechazarla
|
||||||
event :report do
|
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?
|
ActivityPub::RemoteFlagJob.perform_later(remote_flag: remote_flag) if remote_flag.waiting?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Definir eventos en masa
|
||||||
|
include AasmEventsConcern
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,6 +20,8 @@ class ActivityPub
|
||||||
has_one :object, through: :activity_pub
|
has_one :object, through: :activity_pub
|
||||||
|
|
||||||
validates :activity_pub_id, presence: true
|
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
|
# Siempre en orden descendiente para saber el último estado
|
||||||
default_scope -> { order(created_at: :desc) }
|
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.
|
# lo haría la Social Inbox por nosotres.
|
||||||
# @see {https://docs.joinmastodon.org/spec/security/#ld}
|
# @see {https://docs.joinmastodon.org/spec/security/#ld}
|
||||||
def update_activity_pub_state!
|
def update_activity_pub_state!
|
||||||
|
ActiveRecord::Base.connection_pool.with_connection do
|
||||||
ActivityPub.transaction do
|
ActivityPub.transaction do
|
||||||
ActivityPub::Object.find_by(uri: ActivityPub.uri_from_object(content['object']))&.activity_pubs&.find_each(&:remove!)
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,8 +4,15 @@
|
||||||
#
|
#
|
||||||
# Una actividad de seguimiento se refiere siempre a une actore (el
|
# Una actividad de seguimiento se refiere siempre a une actore (el
|
||||||
# sitio) y proviene de otre actore.
|
# sitio) y proviene de otre actore.
|
||||||
|
#
|
||||||
|
# Por ahora las solicitudes de seguimiento se auto-aprueban.
|
||||||
class ActivityPub
|
class ActivityPub
|
||||||
class Activity
|
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
|
||||||
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 :activities
|
||||||
has_many :remote_flags
|
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
|
# Obtiene el nombre de la Actor como mención, solo si obtuvimos el
|
||||||
# contenido de antemano.
|
# contenido de antemano.
|
||||||
#
|
#
|
||||||
# @return [String, nil]
|
# @return [String, nil]
|
||||||
def mention
|
def mentionize!
|
||||||
|
return if mention.present?
|
||||||
return if content['preferredUsername'].blank?
|
return if content['preferredUsername'].blank?
|
||||||
return if instance.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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,8 +6,6 @@ class ActivityPub
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
validates :uri, presence: true, uniqueness: true
|
|
||||||
|
|
||||||
# Cuando asignamos contenido, obtener la URI si no lo hicimos ya
|
# Cuando asignamos contenido, obtener la URI si no lo hicimos ya
|
||||||
before_save :uri_from_content!, unless: :uri?
|
before_save :uri_from_content!, unless: :uri?
|
||||||
|
|
||||||
|
|
|
@ -31,8 +31,8 @@ class ActivityPub
|
||||||
|
|
||||||
class FediblockDownloadError < ::StandardError; end
|
class FediblockDownloadError < ::StandardError; end
|
||||||
|
|
||||||
validates_presence_of :title, :url, :download_url, :format
|
validates_presence_of :title, :url, :format
|
||||||
validates_inclusion_of :format, in: %w[mastodon fediblock]
|
validates_inclusion_of :format, in: %w[mastodon fediblock none]
|
||||||
|
|
||||||
HOSTNAME_HEADERS = {
|
HOSTNAME_HEADERS = {
|
||||||
'mastodon' => '#domain',
|
'mastodon' => '#domain',
|
||||||
|
@ -52,7 +52,7 @@ class ActivityPub
|
||||||
def process!
|
def process!
|
||||||
response = client.get(download_url)
|
response = client.get(download_url)
|
||||||
|
|
||||||
raise FediblockDownloadError unless response.ok?
|
raise FediblockDownloadError unless response.success?
|
||||||
|
|
||||||
Fediblock.transaction do
|
Fediblock.transaction do
|
||||||
csv = response.parsed_response
|
csv = response.parsed_response
|
||||||
|
|
|
@ -9,7 +9,7 @@ class ActivityPub
|
||||||
include AASM
|
include AASM
|
||||||
|
|
||||||
validates :aasm_state, presence: true, inclusion: { in: %w[paused allowed blocked] }
|
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 :activity_pubs
|
||||||
has_many :actors
|
has_many :actors
|
||||||
|
|
|
@ -5,13 +5,62 @@ class ActivityPub
|
||||||
class Object < ApplicationRecord
|
class Object < ApplicationRecord
|
||||||
include ActivityPub::Concerns::JsonLdConcern
|
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
|
has_many :activity_pubs, as: :object
|
||||||
|
|
||||||
# Encontrar le Actor por su relación con el objeto
|
# Encontrar le Actor por su relación con el objeto
|
||||||
#
|
#
|
||||||
# @return [ActivityPub::Actor,nil]
|
# @return [ActivityPub::Actor,nil]
|
||||||
def actor
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
# Una aplicación o instancia
|
# Una aplicación o instancia
|
||||||
class ActivityPub
|
class ActivityPub
|
||||||
class Object
|
class Object
|
||||||
class Application < ActivityPub::Object; end
|
class Application < ActivityPub::Object
|
||||||
|
include Concerns::ActorTypeConcern
|
||||||
|
end
|
||||||
end
|
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
|
# Una organización
|
||||||
class ActivityPub
|
class ActivityPub
|
||||||
class Object
|
class Object
|
||||||
class Organization < ActivityPub::Object; end
|
class Organization < ActivityPub::Object
|
||||||
|
include Concerns::ActorTypeConcern
|
||||||
|
end
|
||||||
end
|
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
|
# Una persona, el perfil de une actore
|
||||||
class ActivityPub
|
class ActivityPub
|
||||||
class Object
|
class Object
|
||||||
class Person < ActivityPub::Object; end
|
class Person < ActivityPub::Object
|
||||||
|
include Concerns::ActorTypeConcern
|
||||||
|
end
|
||||||
end
|
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 ActivityPub
|
||||||
class RemoteFlag < ApplicationRecord
|
class RemoteFlag < ApplicationRecord
|
||||||
|
IGNORED_EVENTS = [].freeze
|
||||||
|
IGNORED_STATES = [].freeze
|
||||||
|
|
||||||
include AASM
|
include AASM
|
||||||
include AasmEventsConcern
|
|
||||||
|
|
||||||
aasm do
|
aasm do
|
||||||
state :waiting, initial: true
|
state :waiting, initial: true
|
||||||
|
@ -14,7 +16,7 @@ class ActivityPub
|
||||||
transitions from: :waiting, to: :queued
|
transitions from: :waiting, to: :queued
|
||||||
end
|
end
|
||||||
|
|
||||||
event :send do
|
event :report do
|
||||||
transitions from: :queued, to: :sent
|
transitions from: :queued, to: :sent
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -23,6 +25,9 @@ class ActivityPub
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Definir eventos en masa
|
||||||
|
include AasmEventsConcern
|
||||||
|
|
||||||
belongs_to :actor
|
belongs_to :actor
|
||||||
belongs_to :site
|
belongs_to :site
|
||||||
|
|
||||||
|
@ -37,10 +42,18 @@ class ActivityPub
|
||||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
'@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),
|
'id' => Rails.application.routes.url_helpers.v1_activity_pub_remote_flag_url(self, host: site.social_inbox_hostname),
|
||||||
'type' => 'Flag',
|
'type' => 'Flag',
|
||||||
'actor' => ENV.fetch('PANEL_ACTOR_ID') { "https://#{ENV['SUTTY']}/about.jsonld" },
|
'actor' => main_site.social_inbox.actor_id,
|
||||||
'content' => message.to_s,
|
'content' => message.to_s,
|
||||||
'object' => [actor.uri] + objects.pluck(:uri)
|
'object' => [actor.uri] + objects.pluck(:uri)
|
||||||
}
|
}
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,86 +2,69 @@
|
||||||
|
|
||||||
# Mantiene la relación entre Site y Actor
|
# Mantiene la relación entre Site y Actor
|
||||||
class ActorModeration < ApplicationRecord
|
class ActorModeration < ApplicationRecord
|
||||||
include AASM
|
IGNORED_EVENTS = %i[remove].freeze
|
||||||
include AasmEventsConcern
|
IGNORED_STATES = %i[removed].freeze
|
||||||
|
|
||||||
IGNORED_EVENTS = []
|
include AASM
|
||||||
IGNORED_STATES = []
|
|
||||||
|
|
||||||
belongs_to :site
|
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'
|
belongs_to :actor, class_name: 'ActivityPub::Actor'
|
||||||
|
|
||||||
accepts_nested_attributes_for :remote_flag
|
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
|
aasm do
|
||||||
state :paused, initial: true
|
state :paused, initial: true
|
||||||
state :allowed
|
state :allowed
|
||||||
state :blocked
|
state :blocked
|
||||||
state :reported
|
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
|
event :pause do
|
||||||
transitions from: %i[allowed blocked reported], to: :paused
|
transitions from: %i[allowed blocked reported], to: :paused, after: :synchronize!
|
||||||
|
|
||||||
before do
|
|
||||||
pause_remotely!
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Al permitir una cuenta no se permiten todos los comentarios
|
||||||
|
# pendientes de moderación que ya hizo.
|
||||||
event :allow do
|
event :allow do
|
||||||
transitions from: %i[paused blocked reported], to: :allowed
|
transitions from: %i[paused blocked reported], to: :allowed, after: :synchronize!
|
||||||
|
|
||||||
before do
|
|
||||||
allow_remotely!
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Al bloquear una cuenta no se bloquean todos los comentarios
|
||||||
|
# pendientes de moderación que hizo.
|
||||||
event :block do
|
event :block do
|
||||||
transitions from: %i[paused allowed], to: :blocked
|
transitions from: %i[paused allowed], to: :blocked, after: :synchronize!
|
||||||
|
|
||||||
before do
|
|
||||||
block_remotely!
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Al reportar, necesitamos asociar una RemoteFlag para poder
|
# Al reportar, necesitamos asociar una RemoteFlag para poder
|
||||||
# enviarla.
|
# enviarla.
|
||||||
event :report do
|
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?
|
ActivityPub::RemoteFlagJob.perform_later(remote_flag: remote_flag) if remote_flag.waiting?
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
def pause_remotely!
|
# Definir eventos en masa
|
||||||
raise AASM::InvalidTransition unless
|
include AasmEventsConcern
|
||||||
actor.mention &&
|
|
||||||
site.social_inbox.allowlist.delete(list: [actor.mention]).ok? &&
|
|
||||||
site.social_inbox.blocklist.delete(list: [actor.mention]).ok?
|
|
||||||
end
|
|
||||||
|
|
||||||
def allow_remotely!
|
def synchronize!
|
||||||
raise AASM::InvalidTransition unless
|
ActivityPub::SyncListsJob.perform_later(site: site)
|
||||||
actor.mention &&
|
|
||||||
site.social_inbox.allowlist.post(list: [actor.mention]).ok? &&
|
|
||||||
site.social_inbox.blocklist.delete(list: [actor.mention]).ok?
|
|
||||||
end
|
|
||||||
|
|
||||||
def block_remotely!
|
|
||||||
raise AASM::InvalidTransition unless
|
|
||||||
actor.mention &&
|
|
||||||
site.social_inbox.allowlist.delete(list: [actor.mention]).ok? &&
|
|
||||||
site.social_inbox.blocklist.post(list: [actor.mention]).ok?
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,7 +16,7 @@ module AasmEventsConcern
|
||||||
#
|
#
|
||||||
# @return [Array<Symbol>]
|
# @return [Array<Symbol>]
|
||||||
def self.transitionable_events(current_state)
|
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
|
aasm.events.find { |x| x.name == event }.transitions_from_state? current_state
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -27,5 +27,35 @@ module AasmEventsConcern
|
||||||
def self.states
|
def self.states
|
||||||
aasm.states.map(&:name) - self::IGNORED_STATES
|
aasm.states.map(&:name) - self::IGNORED_STATES
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,16 +7,16 @@ class DeploySocialDistributedPress < Deploy
|
||||||
# Solo luego de publicar remotamente
|
# Solo luego de publicar remotamente
|
||||||
DEPENDENCIES = %i[deploy_distributed_press deploy_rsync deploy_full_rsync].freeze
|
DEPENDENCIES = %i[deploy_distributed_press deploy_rsync deploy_full_rsync].freeze
|
||||||
|
|
||||||
after_save :create_hooks!
|
|
||||||
after_create :enable_fediblocks!
|
|
||||||
|
|
||||||
# Envía las notificaciones
|
# Envía las notificaciones
|
||||||
def deploy(output: false)
|
def deploy(output: false)
|
||||||
with_tempfile(site.private_key_pem) do |file|
|
with_tempfile(site.private_key_pem) do |file|
|
||||||
key = Shellwords.escape file.path
|
key = Shellwords.escape file.path
|
||||||
dest = Shellwords.escape destination
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ class DeploySocialDistributedPress < Deploy
|
||||||
|
|
||||||
response = hook_client.put(event: event, hook: webhook)
|
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
|
rescue ArgumentError => e
|
||||||
ExceptionNotifier.notify_exception(e, data: { site_id: site.name, usuarie_id: rol.usuarie_id })
|
ExceptionNotifier.notify_exception(e, data: { site_id: site.name, usuarie_id: rol.usuarie_id })
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,84 +20,61 @@ class FediblockState < ApplicationRecord
|
||||||
# Aunque queramos las listas habilitadas por defecto, tenemos que
|
# Aunque queramos las listas habilitadas por defecto, tenemos que
|
||||||
# habilitarlas luego de crearlas para poder generar la lista de
|
# habilitarlas luego de crearlas para poder generar la lista de
|
||||||
# bloqueo en la Social Inbox.
|
# bloqueo en la Social Inbox.
|
||||||
state :disabled, initial: true
|
state :disabled, initial: true, before_enter: :pause_unique_instances!
|
||||||
state :enabled
|
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
|
event :enable do
|
||||||
transitions from: :disabled, to: :enabled
|
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
|
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,
|
# @todo No cambiar el estado si se habían habilitado manualmente,
|
||||||
# pero esto implica que tenemos que encontrar las que sí y quitarlas
|
# pero esto implica que tenemos que encontrar las que sí y quitarlas
|
||||||
# de list_names
|
# de list_names
|
||||||
event :disable do
|
event :disable do
|
||||||
transitions from: :enabled, to: :disabled
|
transitions from: :enabled, to: :disabled, after: :synchronize!
|
||||||
|
|
||||||
before do
|
|
||||||
disable_remotely!
|
|
||||||
|
|
||||||
instance_moderations.pause_all!
|
|
||||||
|
|
||||||
# Volver a pausar todes les actores de esta instancia que fueron
|
|
||||||
# bloqueades.
|
|
||||||
ActorModeration.where(actor_id: actor_ids).blocked.pause_all!
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def actor_ids
|
def block_instances!
|
||||||
ActivityPub::Actor.where(instance_id: instance_ids).pluck(:id)
|
ActivityPub::InstanceModerationJob.perform_later(site: site, hostnames: fediblock.hostnames, perform_remotely: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def instance_ids
|
# Pausar todas las moderaciones de las instancias que no estén
|
||||||
fediblock.instances.pluck(:id)
|
# 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
|
end
|
||||||
|
|
||||||
# Todas las instancias de moderación de este sitio
|
def synchronize!
|
||||||
def instance_moderations
|
ActivityPub::SyncListsJob.perform_later(site: site)
|
||||||
site.instance_moderations.where(instance_id: instance_ids)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Devuelve los hostnames únicos a esta instancia.
|
||||||
|
#
|
||||||
# @return [Array<String>]
|
# @return [Array<String>]
|
||||||
def list_names
|
def unique_hostnames
|
||||||
@list_names ||= fediblock.instances.map do |instance|
|
@unique_hostnames ||=
|
||||||
"@*@#{instance}"
|
begin
|
||||||
end
|
other_enabled_fediblock_ids =
|
||||||
end
|
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
|
fediblock.hostnames - other_enabled_hostnames
|
||||||
def disable_remotely!
|
end
|
||||||
raise AASM::InvalidTransition unless
|
|
||||||
site.social_inbox.blocklist.delete(list: list_names).ok? &&
|
|
||||||
site.social_inbox.allowlist.delete(list: list_names).ok?
|
|
||||||
end
|
|
||||||
|
|
||||||
# Al habilitar, se bloquean todas las instancias de la lista
|
|
||||||
def enable_remotely!
|
|
||||||
raise AASM::InvalidTransition unless
|
|
||||||
site.social_inbox.blocklist.post(list: list_names).ok? &&
|
|
||||||
site.social_inbox.allowlist.delete(list: list_names).ok?
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,84 +2,46 @@
|
||||||
|
|
||||||
# Mantiene el registro de relaciones entre sitios e instancias
|
# Mantiene el registro de relaciones entre sitios e instancias
|
||||||
class InstanceModeration < ApplicationRecord
|
class InstanceModeration < ApplicationRecord
|
||||||
include AASM
|
IGNORED_EVENTS = [].freeze
|
||||||
include AasmEventsConcern
|
IGNORED_STATES = [].freeze
|
||||||
|
|
||||||
IGNORED_EVENTS = []
|
include AASM
|
||||||
IGNORED_STATES = []
|
|
||||||
|
|
||||||
belongs_to :site
|
belongs_to :site
|
||||||
belongs_to :instance, class_name: 'ActivityPub::Instance'
|
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
|
aasm do
|
||||||
state :paused, initial: true
|
state :paused, initial: true
|
||||||
state :allowed
|
state :allowed
|
||||||
state :blocked
|
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
|
event :pause do
|
||||||
transitions from: %i[allowed blocked], to: :paused
|
transitions from: %i[allowed blocked], to: :paused
|
||||||
|
|
||||||
before do
|
|
||||||
pause_remotely!
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Al permitir, solo bloqueamos la instancia, sin modificar el estado
|
||||||
|
# de les actores y comentarios retroactivamente.
|
||||||
event :allow do
|
event :allow do
|
||||||
transitions from: %i[paused blocked], to: :allowed
|
transitions from: %i[paused blocked], to: :allowed
|
||||||
|
|
||||||
before do
|
|
||||||
allow_remotely!
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Al bloquear, solo bloqueamos la instancia, sin modificar el estado
|
||||||
|
# de les actores y comentarios retroactivamente.
|
||||||
event :block do
|
event :block do
|
||||||
transitions from: %i[paused allowed], to: :blocked
|
transitions from: %i[paused allowed], to: :blocked
|
||||||
|
|
||||||
before do
|
|
||||||
block_remotely!
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Elimina la instancia de todas las listas
|
# Definir eventos en masa
|
||||||
#
|
include AasmEventsConcern
|
||||||
# @return [Boolean]
|
|
||||||
def pause_remotely!
|
|
||||||
raise AASM::InvalidTransition unless
|
|
||||||
site.social_inbox.blocklist.delete(list: [instance.list_name]).ok? &&
|
|
||||||
site.social_inbox.allowlist.delete(list: [instance.list_name]).ok?
|
|
||||||
end
|
|
||||||
|
|
||||||
# Deja de permitir la instancia
|
|
||||||
#
|
|
||||||
# @return [Boolean]
|
|
||||||
def block_remotely!
|
|
||||||
raise AASM::InvalidTransition unless
|
|
||||||
site.social_inbox.allowlist.delete(list: [instance.list_name]).ok? &&
|
|
||||||
site.social_inbox.blocklist.post(list: [instance.list_name]).ok?
|
|
||||||
end
|
|
||||||
|
|
||||||
# Permite la instancia
|
|
||||||
#
|
|
||||||
# @return [Boolean]
|
|
||||||
def allow_remotely!
|
|
||||||
raise AASM::InvalidTransition unless
|
|
||||||
site.social_inbox.blocklist.delete(list: [instance.list_name]).ok? &&
|
|
||||||
site.social_inbox.allowlist.post(list: [instance.list_name]).ok?
|
|
||||||
end
|
|
||||||
end
|
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?
|
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]
|
# @return [SocialInbox]
|
||||||
def social_inbox
|
def social_inbox
|
||||||
@social_inbox ||= SocialInbox.new(site: self)
|
@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
|
# En este paso tenemos varias instancias por moderar pero todas son
|
||||||
# del mismo sitio.
|
# del mismo sitio.
|
||||||
def action_on_several?
|
def action_on_several?
|
||||||
instance_moderation.first.site.usuarie? usuarie
|
instance_moderation.first.presence && instance_moderation.first.site.usuarie?(usuarie)
|
||||||
end
|
end
|
||||||
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.
|
# Por ahora solo queremos moderar comentarios.
|
||||||
prepare do
|
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
|
end
|
||||||
|
|
||||||
map :activity_pub_state, activate_always: true do |activity_pub_state: 'paused'|
|
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
|
.col-12.col-md-8
|
||||||
= render 'components/profiles_btn_box', actor_moderation: @actor_moderation
|
= render 'components/profiles_btn_box', actor_moderation: @actor_moderation
|
||||||
.col-12.col-md-8
|
.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
|
-# Componente Remote_Profile
|
||||||
|
|
||||||
|
- uri = text_plain(remote_profile['id'])
|
||||||
|
|
||||||
.py-2
|
.py-2
|
||||||
%dl
|
%dl
|
||||||
%dt= t('.profile_name')
|
%dt= t('.profile_name')
|
||||||
|
@ -10,7 +12,7 @@
|
||||||
|
|
||||||
%dt= t('.profile_id')
|
%dt= t('.profile_id')
|
||||||
%dd
|
%dd
|
||||||
= link_to text_plain(remote_profile['id'])
|
= link_to uri, uri
|
||||||
|
|
||||||
- if remote_profile['published'].present?
|
- if remote_profile['published'].present?
|
||||||
%dt= t('.profile_published')
|
%dt= t('.profile_published')
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
-# Componente Botón general Moderación
|
-# Componente Botón general Moderación
|
||||||
|
|
||||||
- local_assigns[:method] ||= 'patch'
|
- local_assigns[:method] ||= 'patch'
|
||||||
- local_assigns[:class] ||= 'btn-secondary'
|
|
||||||
- local_assigns[:class] = "btn #{local_assigns[:class]}"
|
- local_assigns[:class] = "btn #{local_assigns[:class]}"
|
||||||
|
- local_assigns.delete(:text)
|
||||||
|
|
||||||
-# @todo path es obligatorio
|
= button_to(path, **local_assigns.compact) do
|
||||||
= button_to local_assigns[:path], **local_assigns do
|
|
||||||
= text
|
= text
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
-# Componente Checkbox
|
-# Componente Checkbox
|
||||||
- local_assigns[:name] ||= id
|
- local_assigns[:name] ||= id
|
||||||
|
|
||||||
.custom-control.custom-checkbox
|
.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
|
%label.custom-control-label{ for: id }= yield
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
-# Componente Botonera de Comentarios
|
-# Componente Botonera de Comentarios
|
||||||
|
|
||||||
|
- local = { reject: { data: { confirm: t('.confirm_reject') } }, report: { class: 'ml-auto', data: { confirm: t('.confirm_report') } } }
|
||||||
|
|
||||||
.d-flex.flex-row
|
.d-flex.flex-row
|
||||||
- ActivityPub.events.each do |event|
|
- ActivityPub.events.each do |event|
|
||||||
|
- possible = activity_pub.public_send(:"may_#{event}?")
|
||||||
|
%div{ class: local.dig(event, :class) }
|
||||||
= render 'components/btn_base',
|
= render 'components/btn_base',
|
||||||
text: t(".text_#{event}"),
|
text: t(".text_#{event}"),
|
||||||
path: public_send(:"site_activity_pub_#{event}_path", activity_pub_id: activity_pub),
|
path: public_send(:"site_activity_pub_#{event}_path", activity_pub_id: activity_pub),
|
||||||
disabled: !activity_pub.public_send(:"may_#{event}?")
|
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
|
- current_state = params[:activity_pub_state]&.to_sym || ActivityPub.states.first
|
||||||
|
|
||||||
- ActivityPub.aasm.events.each do |event|
|
- ActivityPub.aasm.events.each do |event|
|
||||||
- next if ActivityPub::IGNORED_EVENTS.include? event.name
|
- next if ActivityPub::IGNORED_EVENTS.include? event.name
|
||||||
- next unless event.transitions_from_state?(current_state)
|
- 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
|
- 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?
|
- if ActivityPub.transitionable_events(current_state).present?
|
||||||
= render 'components/dropdown', text: t('.text_checked') do
|
= 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/dropdown', text: t('.text_show') do
|
||||||
= render 'components/comments_show_submenu', activity_pubs: activity_pubs
|
= render 'components/comments_show_submenu', activity_pubs: activity_pubs
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
- ActivityPub.states.each do |state|
|
- ActivityPub.states.each do |state|
|
||||||
= render 'components/dropdown_item',
|
= render 'components/dropdown_item',
|
||||||
text: t(".submenu_#{state}", count: activity_pubs.unscope(where: :aasm_state).public_send(state).count),
|
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'
|
controller: 'dropdown'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
%button.btn.dropdown-toggle{
|
%button.btn.btn-outline-secondary.dropdown-toggle{
|
||||||
type: 'button',
|
type: 'button',
|
||||||
class: button_classes,
|
class: button_classes,
|
||||||
data: {
|
data: {
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
-#
|
-#
|
||||||
@param name [String]
|
@param name [String]
|
||||||
@param value [String]
|
@param value [String]
|
||||||
%button.dropdown-item{type: 'submit', data: { target: 'dropdown.item' }, name: name, value: value, 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 :text [String] Contenido del link
|
||||||
@param :path [String,Hash] 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
|
-# Componente botonera de moderación de Instancias
|
||||||
|
|
||||||
- btn_class = 'btn btn-secondary'
|
- local_data = {}
|
||||||
= 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?
|
- InstanceModeration.events.each do |event|
|
||||||
= 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?
|
- possible = instance_moderation.public_send(:"may_#{event}?")
|
||||||
= 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?
|
= 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|
|
- 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
|
- 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?
|
- if InstanceModeration.transitionable_events(current_state).present?
|
||||||
= render 'components/dropdown', text: t('.text_checked') do
|
= 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/dropdown', text: t('.text_show') do
|
||||||
= render 'components/instances_show_submenu', instance_moderations: instance_moderations
|
= render 'components/instances_show_submenu', instance_moderations: instance_moderations
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
- InstanceModeration.states.each do |state|
|
- InstanceModeration.states.each do |state|
|
||||||
= render 'components/dropdown_item',
|
= render 'components/dropdown_item',
|
||||||
text: t(".submenu_#{state}", count: instance_moderations.unscope(where: :aasm_state).public_send(state).count),
|
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)
|
-# Componente Botonera de Moderación de Cuentas (Remote_profile)
|
||||||
.d-flex.flex-row
|
.d-flex.flex-row.w-100
|
||||||
- btn_class = 'btn-secondary'
|
- local = { report: { class: 'ml-auto', data: { confirm: t('.confirm_report') } } }
|
||||||
- ActorModeration.events.each do |actor_event|
|
- ActorModeration.events.each do |actor_event|
|
||||||
|
- possible = !actor_moderation.public_send(:"may_#{actor_event}?")
|
||||||
|
%div{ class: local.dig(actor_event, :class) }
|
||||||
= render 'components/btn_base',
|
= render 'components/btn_base',
|
||||||
text: t(".text_#{actor_event}"),
|
text: t(".text_#{actor_event}"),
|
||||||
path: public_send(:"site_actor_moderation_#{actor_event}_path", actor_moderation_id: actor_moderation),
|
path: public_send(:"site_actor_moderation_#{actor_event}_path", actor_moderation_id: actor_moderation),
|
||||||
class: btn_class,
|
class: ('btn-secondary' if possible),
|
||||||
disabled: !actor_moderation.public_send(:"may_#{actor_event}?")
|
disabled: !possible,
|
||||||
|
data: local.dig(actor_event, :data)
|
||||||
|
|
|
@ -1,2 +1,5 @@
|
||||||
|
-#
|
||||||
|
@params form [String]
|
||||||
|
|
||||||
- ActorModeration.transitionable_events(current_state).each do |actor_event|
|
- 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
|
- 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?
|
- if ActorModeration.transitionable_events(current_state).present?
|
||||||
= render 'components/dropdown', text: t('.text_checked') do
|
= 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/dropdown', text: t('.text_show') do
|
||||||
= render 'components/profiles_show_submenu', actor_moderations: actor_moderations
|
= render 'components/profiles_show_submenu', actor_moderations: actor_moderations
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
- ActorModeration.states.each do |actor_state|
|
- ActorModeration.states.each do |actor_state|
|
||||||
= render 'components/dropdown_item',
|
= render 'components/dropdown_item',
|
||||||
text: t(".submenu_#{actor_state}", count: actor_moderations.unscope(where: :aasm_state).public_send(actor_state).count),
|
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]
|
@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')
|
%span.sr-only= t('.label')
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
navegador los va a asignar a este formulario.
|
navegador los va a asignar a este formulario.
|
||||||
|
|
||||||
@param path [String]
|
@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
|
-# nada
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
|
-#
|
||||||
|
@params form [String]
|
||||||
|
|
||||||
.row.no-gutters.pt-2
|
.row.no-gutters.pt-2
|
||||||
.col-1
|
.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
|
.col-11
|
||||||
|
- cache [actor_moderation, profile] do
|
||||||
%h4
|
%h4
|
||||||
= link_to text_plain(profile['name']), site_actor_moderation_path(id: actor_moderation)
|
= link_to text_plain(profile['name']), site_actor_moderation_path(id: actor_moderation)
|
||||||
.mb-3
|
.mb-3
|
||||||
= sanitize profile['summary']
|
= sanitize profile['summary']
|
||||||
|
|
||||||
-# Botones de Moderación
|
-# Botones de Moderación
|
||||||
- cache actor_moderation do
|
|
||||||
.d-flex.pb-4
|
.d-flex.pb-4
|
||||||
= render 'components/profiles_btn_box', actor_moderation: actor_moderation
|
= render 'components/profiles_btn_box', actor_moderation: actor_moderation
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
- form_id = 'actor_moderations_action_on_several'
|
- 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' } }
|
.row.no-gutters.pt-2{ data: { controller: 'select-all' } }
|
||||||
.col-1.d-flex.align-items-center
|
.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
|
.col-11
|
||||||
-# Filtros
|
-# 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
|
.col-12
|
||||||
- if actor_moderations.count.zero?
|
- if actor_moderations.count.zero?
|
||||||
%h4= t('moderation_queue.nothing')
|
%h4= t('moderation_queue.nothing')
|
||||||
- actor_moderations.find_each do |actor_moderation|
|
- actor_moderations.find_each do |actor_moderation|
|
||||||
- cache [actor_moderation, actor_moderation.actor] do
|
- next if actor_moderation.actor.content.empty?
|
||||||
%hr
|
%hr
|
||||||
= render 'account', actor_moderation: actor_moderation, profile: actor_moderation.actor.content, form_id: form_id
|
= render 'account', actor_moderation: actor_moderation, profile: actor_moderation.actor.content, form: form_id
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
.form-group
|
.form-group
|
||||||
= label_tag 'custom_blocklist', t('moderation_queue.instances.custom_block')
|
= 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,20 +1,34 @@
|
||||||
-#
|
-#
|
||||||
Componente Comentario
|
Componente Comentario
|
||||||
|
|
||||||
|
@param site [Site]
|
||||||
|
@param form [String]
|
||||||
@param profile [Hash]
|
@param profile [Hash]
|
||||||
@param comment [Hash]
|
@param comment [Hash]
|
||||||
@param activity_pub [ActivityPub]
|
@param activity_pub [ActivityPub]
|
||||||
|
|
||||||
- in_reply_to = text_plain comment['inReplyTo']
|
- 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']
|
- summary = text_plain comment['summary']
|
||||||
|
-# @todo Generar un desplegable con todas las opciones
|
||||||
|
- url = text_plain ActivityPub.url_from_object(comment)
|
||||||
|
|
||||||
.row.no-gutters
|
.row.no-gutters
|
||||||
.col-1
|
.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
|
.col-11
|
||||||
|
- cache [activity_pub, comment] do
|
||||||
.d-flex.flex-row.align-items-center.justify-content-between
|
.d-flex.flex-row.align-items-center.justify-content-between
|
||||||
%h4.mb-0
|
%h4.mb-0
|
||||||
%a{ href: text_plain(comment['attributedTo']) }= text_plain profile['preferredUsername']
|
%a{ href: text_plain(comment['attributedTo']) }= text_plain profile['preferredUsername']
|
||||||
|
%a{ href: url }
|
||||||
%small
|
%small
|
||||||
= render 'layouts/time', time: text_plain(comment['published'])
|
= render 'layouts/time', time: text_plain(comment['published'])
|
||||||
- if in_reply_to.present?
|
- if in_reply_to.present?
|
||||||
|
@ -24,7 +38,7 @@
|
||||||
%dd.d-inline
|
%dd.d-inline
|
||||||
%small
|
%small
|
||||||
%a{ href: in_reply_to }= in_reply_to
|
%a{ href: in_reply_to }= in_reply_to
|
||||||
.content
|
.content.mb-3
|
||||||
- if summary.present?
|
- if summary.present?
|
||||||
= render 'layouts/details', summary: summary, summary_class: 'h5' do
|
= render 'layouts/details', summary: summary, summary_class: 'h5' do
|
||||||
= sanitize comment['content']
|
= sanitize comment['content']
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
- form_id = 'activity_pub_action_on_several'
|
- 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' } }
|
.row.no-gutters.pt-2{ data: { controller: 'select-all' } }
|
||||||
.col-1.d-flex.align-items-center
|
.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
|
.col-11
|
||||||
-# Filtros
|
-# 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
|
.col-12
|
||||||
- if moderation_queue.count.zero?
|
- if moderation_queue.count.zero?
|
||||||
%h4= t('moderation_queue.nothing')
|
%h4= t('moderation_queue.nothing')
|
||||||
- moderation_queue.each do |activity_pub|
|
- 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
|
%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,12 +1,16 @@
|
||||||
- usuaries = instance.content.dig('usage', 'users', 'active_month')
|
- usuaries = instance.content.dig('usage', 'users', 'active_month')
|
||||||
- usuaries ||= instance.content.dig('stats', 'user_count')
|
- usuaries ||= instance.content.dig('stats', 'user_count')
|
||||||
|
- title = sanitize(instance.content['title'])
|
||||||
|
|
||||||
.row.no-gutters.pt-2
|
.row.no-gutters.pt-2
|
||||||
.col-1
|
.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
|
.col-11
|
||||||
|
- cache [instance_moderation, instance] do
|
||||||
%h4
|
%h4
|
||||||
%a{ href: instance.uri }= sanitize(instance.content['title']) || instance.hostname
|
%a{ href: instance.uri }= title || instance.hostname
|
||||||
|
- if title.present?
|
||||||
|
= " (#{instance.hostname})".html_safe
|
||||||
.content
|
.content
|
||||||
= sanitize instance.content['description']
|
= sanitize instance.content['description']
|
||||||
- if usuaries.present?
|
- if usuaries.present?
|
||||||
|
|
|
@ -1,22 +1,21 @@
|
||||||
- form_id = 'instance_moderation_action_on_several'
|
- form_id = 'instance_moderation_action_on_several'
|
||||||
|
|
||||||
%section
|
%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' } }
|
.row.no-gutters.pt-2{ data: { controller: 'select-all' } }
|
||||||
.col-1.d-flex.align-items-center
|
.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
|
.col-11
|
||||||
-# Filtros
|
-# 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
|
.col-12
|
||||||
- if instance_moderations.count.zero?
|
- if instance_moderations.count.zero?
|
||||||
%h4= t('moderation_queue.nothing')
|
%h4= t('moderation_queue.nothing')
|
||||||
|
|
||||||
- instance_moderations.each do |instance_moderation|
|
- instance_moderations.each do |instance_moderation|
|
||||||
- cache [instance_moderation.aasm_state, instance_moderation.instance] do
|
|
||||||
%hr
|
%hr
|
||||||
= render 'moderation_queue/instance', instance_moderation: instance_moderation, instance: instance_moderation.instance, form_id: form_id
|
= render 'moderation_queue/instance', instance_moderation: instance_moderation, instance: instance_moderation.instance, form: form_id
|
||||||
|
|
||||||
%hr
|
%hr
|
||||||
%div
|
%div
|
||||||
|
|
|
@ -7,11 +7,11 @@
|
||||||
|
|
||||||
%main.row
|
%main.row
|
||||||
%aside.menu.col-lg-3
|
%aside.menu.col-lg-3
|
||||||
|
.mb-3
|
||||||
= render 'sites/header', site: @site
|
= render 'sites/header', site: @site
|
||||||
|
|
||||||
= render 'sites/status', site: @site
|
= render 'sites/status', site: @site
|
||||||
|
= render 'sites/build', site: @site, class: 'btn-block'
|
||||||
= render 'sites/build', site: @site, class: 'btn-block mb-3'
|
= render 'sites/moderation_queue', site: @site, class: 'btn-block'
|
||||||
|
|
||||||
%h3= t('posts.new')
|
%h3= t('posts.new')
|
||||||
%table.table.table-sm.mb-3
|
%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,11 +15,6 @@
|
||||||
%tbody
|
%tbody
|
||||||
- @sites.each do |site|
|
- @sites.each do |site|
|
||||||
- next unless site.jekyll?
|
- 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
|
%tr
|
||||||
%td
|
%td
|
||||||
%h2
|
%h2
|
||||||
|
@ -30,17 +25,17 @@
|
||||||
%p.lead= site.description
|
%p.lead= site.description
|
||||||
%br
|
%br
|
||||||
= link_to t('.visit'), site.url, class: 'btn btn-secondary'
|
= link_to t('.visit'), site.url, class: 'btn btn-secondary'
|
||||||
- if rol.temporal
|
- if current_usuarie.rol_for_site(site).temporal?
|
||||||
= button_to t('sites.invitations.accept'),
|
= render 'components/btn_base',
|
||||||
site_usuaries_accept_invitation_path(site),
|
text: t('sites.invitations.accept'),
|
||||||
method: :patch,
|
path: site_usuaries_accept_invitation_path(site),
|
||||||
title: t('help.sites.invitations.accept'),
|
title: t('help.sites.invitations.accept'),
|
||||||
class: 'btn btn-secondary'
|
class: 'btn-secondary'
|
||||||
= button_to t('sites.invitations.reject'),
|
= render 'components/btn_base',
|
||||||
site_usuaries_reject_invitation_path(site),
|
text: t('sites.invitations.reject'),
|
||||||
method: :patch,
|
path: site_usuaries_reject_invitation_path(site),
|
||||||
title: t('help.sites.invitations.reject'),
|
title: t('help.sites.invitations.reject'),
|
||||||
class: 'btn btn-secondary'
|
class: 'btn-secondary'
|
||||||
- else
|
- else
|
||||||
- if policy(site).show?
|
- if policy(site).show?
|
||||||
= render 'layouts/btn_with_tooltip',
|
= render 'layouts/btn_with_tooltip',
|
||||||
|
@ -48,10 +43,11 @@
|
||||||
type: 'success',
|
type: 'success',
|
||||||
link: site_path(site),
|
link: site_path(site),
|
||||||
text: t('sites.posts')
|
text: t('sites.posts')
|
||||||
|
= render 'sites/build', site: site
|
||||||
|
= render 'sites/moderation_queue', site: site
|
||||||
- if policy(SiteUsuarie.new(site, current_usuarie)).index?
|
- if policy(SiteUsuarie.new(site, current_usuarie)).index?
|
||||||
= render 'layouts/btn_with_tooltip',
|
= render 'layouts/btn_with_tooltip',
|
||||||
tooltip: t('usuaries.index.help.self'),
|
tooltip: t('usuaries.index.help.self'),
|
||||||
text: t('usuaries.index.title'),
|
text: t('usuaries.index.title'),
|
||||||
type: 'info',
|
type: 'info',
|
||||||
link: site_usuaries_path(site)
|
link: site_usuaries_path(site)
|
||||||
= render 'sites/build', site: site
|
|
||||||
|
|
|
@ -142,7 +142,7 @@ Rails.application.configure do
|
||||||
}
|
}
|
||||||
config.action_mailer.default_options = { from: ENV.fetch('DEFAULT_FROM', "noreply@sutty.nl") }
|
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[:host] = "panel.#{ENV.fetch('SUTTY', 'sutty.nl')}"
|
||||||
Rails.application.routes.default_url_options[:protocol] = 'https'
|
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
|
pm: pm
|
||||||
format: '%-I:%M %p'
|
format: '%-I:%M %p'
|
||||||
components:
|
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:
|
block_list:
|
||||||
know_more: Know more
|
know_more: Know more
|
||||||
instances_blocked: Instances blocked
|
instances_blocked: Instances blocked
|
||||||
instances_filters:
|
instances_filters:
|
||||||
text_show: Show
|
text_show: Show
|
||||||
text_checked: With selected
|
text_checked: With selected...
|
||||||
instances_checked_submenu:
|
instances_checked_submenu:
|
||||||
submenu_pause: Moderate
|
submenu_pause: Moderate
|
||||||
submenu_allow: Allow
|
submenu_allow: Allow
|
||||||
|
@ -66,7 +74,7 @@ en:
|
||||||
submenu_blocked: "Blocked (%{count})"
|
submenu_blocked: "Blocked (%{count})"
|
||||||
comments_filters:
|
comments_filters:
|
||||||
text_show: Show
|
text_show: Show
|
||||||
text_checked: With selected
|
text_checked: With selected...
|
||||||
comments_checked_submenu:
|
comments_checked_submenu:
|
||||||
submenu_pause: Pause
|
submenu_pause: Pause
|
||||||
submenu_approve: Approve
|
submenu_approve: Approve
|
||||||
|
@ -79,7 +87,7 @@ en:
|
||||||
submenu_reported: "Reported (%{count})"
|
submenu_reported: "Reported (%{count})"
|
||||||
profiles_filters:
|
profiles_filters:
|
||||||
text_show: Show
|
text_show: Show
|
||||||
text_checked: With selected
|
text_checked: With selected...
|
||||||
profiles_checked_submenu:
|
profiles_checked_submenu:
|
||||||
submenu_pause: Pause
|
submenu_pause: Pause
|
||||||
submenu_allow: Allow
|
submenu_allow: Allow
|
||||||
|
@ -98,26 +106,68 @@ en:
|
||||||
text_reject: Reject
|
text_reject: Reject
|
||||||
text_reply: Reply
|
text_reply: Reply
|
||||||
text_report: Report
|
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:
|
instances_btn_box:
|
||||||
text_check: Check case by case
|
text_pause: Check case by case
|
||||||
text_allow: Allow everything
|
text_allow: Allow everything
|
||||||
text_deny: Block instance
|
text_block: Block instance
|
||||||
profiles_btn_box:
|
profiles_btn_box:
|
||||||
text_pause: Always check
|
text_pause: Always check
|
||||||
text_allow: Always approve
|
text_allow: Always approve
|
||||||
text_block: Block
|
text_block: Block
|
||||||
text_report: Report
|
text_report: Report
|
||||||
actor_moderations:
|
confirm_report: "Send report to the remote instance? This action will also block the account."
|
||||||
show:
|
|
||||||
user: Username
|
|
||||||
profile: Profile
|
|
||||||
profile_name: Profile name
|
|
||||||
preferred_name: Name in Fediverse
|
|
||||||
profile_id: ID
|
|
||||||
profile_published: Published
|
|
||||||
profile_summary: Summary
|
|
||||||
remote_flags:
|
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}."
|
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:
|
moderation_queue:
|
||||||
everything: 'Select all'
|
everything: 'Select all'
|
||||||
nothing: "There's nothing for this filter"
|
nothing: "There's nothing for this filter"
|
||||||
|
@ -131,8 +181,11 @@ en:
|
||||||
reply_to: Reply to
|
reply_to: Reply to
|
||||||
instances:
|
instances:
|
||||||
title: My block lists
|
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: Custom block lists
|
||||||
|
custom_block_placeholder: |
|
||||||
|
a.doma.in
|
||||||
|
per.li.ne
|
||||||
submit: Save block lists
|
submit: Save block lists
|
||||||
instance:
|
instance:
|
||||||
users: "Users:"
|
users: "Users:"
|
||||||
|
@ -339,6 +392,7 @@ en:
|
||||||
lang:
|
lang:
|
||||||
not_available: "This language is not yet available, would you help us by translating Sutty into it?"
|
not_available: "This language is not yet available, would you help us by translating Sutty into it?"
|
||||||
errors:
|
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}'
|
argument_error: 'Argument `%{argument}` must be an instance of %{class}'
|
||||||
unknown_locale: 'Unknown %{locale} locale'
|
unknown_locale: 'Unknown %{locale} locale'
|
||||||
posts:
|
posts:
|
||||||
|
@ -515,6 +569,8 @@ en:
|
||||||
column: "Country"
|
column: "Country"
|
||||||
empty: "(couldn't detect country)"
|
empty: "(couldn't detect country)"
|
||||||
sites:
|
sites:
|
||||||
|
moderation_queue:
|
||||||
|
moderation_needed: "There are new activities pending revision since the last time you moderated."
|
||||||
donations:
|
donations:
|
||||||
url: 'https://donaciones.sutty.nl/en/'
|
url: 'https://donaciones.sutty.nl/en/'
|
||||||
text: 'Support us'
|
text: 'Support us'
|
||||||
|
|
|
@ -50,12 +50,20 @@ es:
|
||||||
pm: pm
|
pm: pm
|
||||||
format: '%-H:%M'
|
format: '%-H:%M'
|
||||||
components:
|
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:
|
block_list:
|
||||||
know_more: Saber más (en inglés)
|
know_more: Saber más (en inglés)
|
||||||
instances_blocked: Instancias bloqueadas
|
instances_blocked: Instancias bloqueadas
|
||||||
instances_filters:
|
instances_filters:
|
||||||
text_show: Ver
|
text_show: Ver
|
||||||
text_checked: Con los marcados
|
text_checked: Con los marcados...
|
||||||
instances_checked_submenu:
|
instances_checked_submenu:
|
||||||
submenu_pause: Moderar caso por caso
|
submenu_pause: Moderar caso por caso
|
||||||
submenu_allow: Permitir todo
|
submenu_allow: Permitir todo
|
||||||
|
@ -66,7 +74,7 @@ es:
|
||||||
submenu_blocked: "Bloqueadas (%{count})"
|
submenu_blocked: "Bloqueadas (%{count})"
|
||||||
comments_filters:
|
comments_filters:
|
||||||
text_show: Ver
|
text_show: Ver
|
||||||
text_checked: Con los marcados
|
text_checked: Con los marcados...
|
||||||
comments_checked_submenu:
|
comments_checked_submenu:
|
||||||
submenu_pause: Pausar
|
submenu_pause: Pausar
|
||||||
submenu_approve: Aprobar
|
submenu_approve: Aprobar
|
||||||
|
@ -79,7 +87,7 @@ es:
|
||||||
submenu_reported: "Reportados (%{count})"
|
submenu_reported: "Reportados (%{count})"
|
||||||
profiles_filters:
|
profiles_filters:
|
||||||
text_show: Ver
|
text_show: Ver
|
||||||
text_checked: Con los marcados
|
text_checked: Con los marcados...
|
||||||
profiles_checked_submenu:
|
profiles_checked_submenu:
|
||||||
submenu_pause: Pausar
|
submenu_pause: Pausar
|
||||||
submenu_allow: Aceptar
|
submenu_allow: Aceptar
|
||||||
|
@ -97,26 +105,68 @@ es:
|
||||||
text_approve: Aceptar
|
text_approve: Aceptar
|
||||||
text_reject: Rechazar
|
text_reject: Rechazar
|
||||||
text_report: Reportar
|
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:
|
instances_btn_box:
|
||||||
text_check: Moderar caso por caso
|
text_pause: Moderar caso por caso
|
||||||
text_allow: Permitir todo
|
text_allow: Permitir todo
|
||||||
text_deny: Bloquear instancia
|
text_block: Bloquear instancia
|
||||||
profiles_btn_box:
|
profiles_btn_box:
|
||||||
text_pause: Revisar siempre
|
text_pause: Revisar siempre
|
||||||
text_allow: Aprobar siempre
|
text_allow: Aprobar siempre
|
||||||
text_block: Bloquear
|
text_block: Bloquear
|
||||||
text_report: Reportar
|
text_report: Reportar
|
||||||
actor_moderations:
|
confirm_report: "¿Enviar el reporte a la instancia remota? Esta acción también bloqueará la cuenta."
|
||||||
show:
|
|
||||||
user: Nombre de usuarie
|
|
||||||
profile: Cuenta de Origen
|
|
||||||
profile_name: Nombre de la cuenta
|
|
||||||
preferred_name: Nombre en el Fediverso
|
|
||||||
profile_id: ID
|
|
||||||
profile_published: Publicada
|
|
||||||
profile_summary: Presentación
|
|
||||||
remote_flags:
|
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}."
|
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:
|
moderation_queue:
|
||||||
everything: 'Seleccionar todo'
|
everything: 'Seleccionar todo'
|
||||||
nothing: 'No hay nada para este filtro'
|
nothing: 'No hay nada para este filtro'
|
||||||
|
@ -130,8 +180,11 @@ es:
|
||||||
reply_to: En respuesta a
|
reply_to: En respuesta a
|
||||||
instances:
|
instances:
|
||||||
title: Mis listas de bloqueo
|
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: Lista personalizada de bloqueo
|
||||||
|
custom_block_placeholder: |
|
||||||
|
un.domin.io
|
||||||
|
por.lin.ea
|
||||||
submit: Guardar listas de bloqueo
|
submit: Guardar listas de bloqueo
|
||||||
instance:
|
instance:
|
||||||
users: "Usuaries:"
|
users: "Usuaries:"
|
||||||
|
@ -338,6 +391,7 @@ es:
|
||||||
lang:
|
lang:
|
||||||
not_available: "Este idioma todavía no está disponible, ¿nos ayudas a agregarlo y mantenerlo?"
|
not_available: "Este idioma todavía no está disponible, ¿nos ayudas a agregarlo y mantenerlo?"
|
||||||
errors:
|
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}'
|
argument_error: 'El argumento `%{argument}` debe ser una instancia de %{class}'
|
||||||
unknown_locale: 'El idioma %{locale} es desconocido'
|
unknown_locale: 'El idioma %{locale} es desconocido'
|
||||||
posts:
|
posts:
|
||||||
|
@ -519,6 +573,8 @@ es:
|
||||||
column: "País"
|
column: "País"
|
||||||
empty: "(no se pudo detectar el país)"
|
empty: "(no se pudo detectar el país)"
|
||||||
sites:
|
sites:
|
||||||
|
moderation_queue:
|
||||||
|
moderation_needed: "Hay actividades pendientes de revisión desde la última vez que moderaste."
|
||||||
donations:
|
donations:
|
||||||
url: 'https://donaciones.sutty.nl/'
|
url: 'https://donaciones.sutty.nl/'
|
||||||
text: 'Apoyá nuestro trabajo'
|
text: 'Apoyá nuestro trabajo'
|
||||||
|
|
|
@ -4,6 +4,9 @@ Rails.application.routes.draw do
|
||||||
devise_for :usuaries
|
devise_for :usuaries
|
||||||
get '/.well-known/change-password', to: redirect('/usuaries/edit')
|
get '/.well-known/change-password', to: redirect('/usuaries/edit')
|
||||||
|
|
||||||
|
require 'que/web'
|
||||||
|
mount Que::Web => '/que'
|
||||||
|
|
||||||
root 'application#index'
|
root 'application#index'
|
||||||
|
|
||||||
constraints(Constraints::ApiSubdomain.new) do
|
constraints(Constraints::ApiSubdomain.new) do
|
||||||
|
|
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