diff --git a/app/controllers/api/v1/webhooks/social_inbox_controller.rb b/app/controllers/api/v1/webhooks/social_inbox_controller.rb index a1ff9677..12545915 100644 --- a/app/controllers/api/v1/webhooks/social_inbox_controller.rb +++ b/app/controllers/api/v1/webhooks/social_inbox_controller.rb @@ -142,7 +142,8 @@ module Api end end - # Actor, si no hay instancia, la crea en el momento + # Actor, si no hay instancia, la crea en el momento, junto con + # su estado de moderación. # # @return [Actor] def actor @@ -150,6 +151,8 @@ module Api 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 diff --git a/app/controllers/instance_moderations_controller.rb b/app/controllers/instance_moderations_controller.rb new file mode 100644 index 00000000..55f3c51b --- /dev/null +++ b/app/controllers/instance_moderations_controller.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# Actualiza la relación entre un sitio y una instancia +class InstanceModerationsController < ApplicationController + before_action :authorize_policy + + def pause + instance_moderation.pause! + + redirect_to site_moderation_queue_path + end + + def allow + instance_moderation.allow! + + redirect_to site_moderation_queue_path + end + + def block + instance_moderation.block! + + redirect_to site_moderation_queue_path + end + + private + + # @return [InstanceModeration] + def instance_moderation + @instance_moderation ||= site.instance_moderations.find(params[:instance_moderation_id]) + end + + def authorize_policy + authorize instance_moderation + end +end diff --git a/app/controllers/moderation_queue_controller.rb b/app/controllers/moderation_queue_controller.rb index 8920c717..d2123234 100644 --- a/app/controllers/moderation_queue_controller.rb +++ b/app/controllers/moderation_queue_controller.rb @@ -8,7 +8,7 @@ class ModerationQueueController < ApplicationController # @todo cambiar el estado por query @activity_pubs = site.activity_pubs - @instances = ActivityPub::Instance.where(id: @activity_pubs.distinct.pluck(:instance_id)) + @instance_moderations = site.instance_moderations end # Perfil remoto de usuarie diff --git a/app/models/activity_pub/instance.rb b/app/models/activity_pub/instance.rb index 17bf183d..627ccb10 100644 --- a/app/models/activity_pub/instance.rb +++ b/app/models/activity_pub/instance.rb @@ -13,6 +13,7 @@ class ActivityPub has_many :activity_pubs has_many :actors + has_many :instance_moderations aasm do state :paused, initial: true diff --git a/app/models/instance_moderation.rb b/app/models/instance_moderation.rb new file mode 100644 index 00000000..3c092958 --- /dev/null +++ b/app/models/instance_moderation.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Mantiene el registro de relaciones entre sitios e instancias +class InstanceModeration < ApplicationRecord + include AASM + + belongs_to :site + belongs_to :instance, class_name: 'ActivityPub::Instance' + + aasm do + state :paused, initial: true + state :allowed + state :blocked + + event :pause do + transitions from: %i[allowed blocked], to: :paused + end + + event :allow do + transitions from: %i[paused blocked], to: :allowed + end + + event :block do + transitions from: %i[paused allowed], to: :blocked + end + end +end diff --git a/app/models/site/social_distributed_press.rb b/app/models/site/social_distributed_press.rb index 7ebdcb36..73b284bf 100644 --- a/app/models/site/social_distributed_press.rb +++ b/app/models/site/social_distributed_press.rb @@ -11,6 +11,7 @@ class Site has_encrypted :private_key_pem has_many :activity_pubs + has_many :instance_moderations before_save :generate_private_key_pem!, unless: :private_key_pem? diff --git a/app/policies/instance_moderation_policy.rb b/app/policies/instance_moderation_policy.rb new file mode 100644 index 00000000..6b3157e8 --- /dev/null +++ b/app/policies/instance_moderation_policy.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# Solo les usuaries pueden moderar instancias +InstanceModerationPolicy = Struct.new(:usuarie, :instance_moderation) do + def pause? + instance_moderation.site.usuarie? usuarie + end + + def allow? + pause? + end + + def block? + pause? + end +end diff --git a/app/views/components/_btn_base.haml b/app/views/components/_btn_base.haml index 7fa507ca..677b88f1 100644 --- a/app/views/components/_btn_base.haml +++ b/app/views/components/_btn_base.haml @@ -1,3 +1,7 @@ -# Componente Botón general Moderación -%button.btn{ href: href, class: local_assigns[:class] }= text +- local_assigns[:method] ||= 'patch' +- local_assigns[:class] = "btn #{local_assigns[:class]}" + +-# @todo path es obligatorio += button_to text, local_assigns[:path], **local_assigns diff --git a/app/views/components/_instances_btn_box.haml b/app/views/components/_instances_btn_box.haml index 854262c0..74cad4a4 100644 --- a/app/views/components/_instances_btn_box.haml +++ b/app/views/components/_instances_btn_box.haml @@ -1,6 +1,6 @@ -# Componente botonera de moderación de Instancias - btn_class = 'btn btn-secondary' -= render 'components/btn_base', text: t('.text_check'), class: btn_class, href: '' -= render 'components/btn_base', text: t('.text_allow'), class: btn_class, href: '' -= render 'components/btn_base', text: t('.text_deny'), class: btn_class, href: '' \ No newline at end of file += render 'components/btn_base', path: site_instance_moderation_pause_path(instance_moderation_id: instance_moderation), text: t('.text_check'), class: btn_class, disabled: !instance_moderation.may_pause? += render 'components/btn_base', path: site_instance_moderation_allow_path(instance_moderation_id: instance_moderation), text: t('.text_allow'), class: btn_class, disabled: !instance_moderation.may_allow? += render 'components/btn_base', path: site_instance_moderation_block_path(instance_moderation_id: instance_moderation), text: t('.text_deny'), class: btn_class, disabled: !instance_moderation.may_block? diff --git a/app/views/moderation_queue/_instances.haml b/app/views/moderation_queue/_instances.haml index e836d7e0..318cddef 100644 --- a/app/views/moderation_queue/_instances.haml +++ b/app/views/moderation_queue/_instances.haml @@ -1,13 +1,13 @@ -# Filtros = render 'components/instances_filters' -- @instances.each do |instance| +- instance_moderations.each do |instance_moderation| %hr - = render 'moderation_queue/instance', instance: instance + = render 'moderation_queue/instance', instance: instance_moderation.instance -# Botones moderación .d-flex.pb-4 - = render 'components/instances_btn_box', instance: instance + = render 'components/instances_btn_box', site: site, instance_moderation: instance_moderation %hr %h3.mt-5= t('moderation_queue.instances.title') diff --git a/app/views/moderation_queue/index.haml b/app/views/moderation_queue/index.haml index ab98ee30..0c937758 100644 --- a/app/views/moderation_queue/index.haml +++ b/app/views/moderation_queue/index.haml @@ -5,7 +5,7 @@ .col - summary = t('.instances') = render 'layouts/details', summary: summary do - = render 'moderation_queue/instances', site: @site, post: @post, moderation_queue: @moderation_queue + = render 'moderation_queue/instances', site: @site, instance_moderations: @instance_moderations %hr - summary = t('.accounts') = render 'layouts/details', summary: summary do diff --git a/config/routes.rb b/config/routes.rb index ddc29994..b6250902 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -62,6 +62,12 @@ Rails.application.routes.draw do get 'remote_profile', to: 'moderation_queue#remote_profile' get 'instances', to: 'moderation_queue#instances' + resources :instance_moderations, only: [] do + patch :pause, to: 'instance_moderations#pause' + patch :allow, to: 'instance_moderations#allow' + patch :block, to: 'instance_moderations#block' + end + # Gestionar artículos según idioma nested do scope '/(:locale)', constraint: /[a-z]{2}(-[A-Z]{2})?/ do diff --git a/db/migrate/20240226134335_create_instance_moderation.rb b/db/migrate/20240226134335_create_instance_moderation.rb new file mode 100644 index 00000000..8b08e14e --- /dev/null +++ b/db/migrate/20240226134335_create_instance_moderation.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# Como la instancia es única para todo el panel, necesitamos llevar +# registro de su relación con cada sitio por separado. +class CreateInstanceModeration < ActiveRecord::Migration[6.1] + def up + create_table :instance_moderations do |t| + t.timestamps + + t.belongs_to :site + t.uuid :instance_id, index: true + + t.string :aasm_state, null: false, default: 'paused' + + t.index %i[site_id instance_id], unique: true + end + + ActivityPub.all.find_each do |activity_pub| + InstanceModeration.find_or_create_by(site: activity_pub.site, instance: activity_pub.instance) + end + end + + def down + drop_table :instance_moderations + end +end diff --git a/db/structure.sql b/db/structure.sql index 4fb11e5a..c3896060 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -950,6 +950,39 @@ CREATE TABLE public.indexed_posts ( ); +-- +-- Name: instance_moderations; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.instance_moderations ( + id bigint NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + site_id bigint, + instance_id uuid, + aasm_state character varying DEFAULT 'paused'::character varying NOT NULL +); + + +-- +-- Name: instance_moderations_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.instance_moderations_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: instance_moderations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.instance_moderations_id_seq OWNED BY public.instance_moderations.id; + + -- -- Name: licencias; Type: TABLE; Schema: public; Owner: - -- @@ -1514,6 +1547,13 @@ ALTER TABLE ONLY public.designs ALTER COLUMN id SET DEFAULT nextval('public.desi ALTER TABLE ONLY public.distributed_press_publishers ALTER COLUMN id SET DEFAULT nextval('public.distributed_press_publishers_id_seq'::regclass); +-- +-- Name: instance_moderations id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.instance_moderations ALTER COLUMN id SET DEFAULT nextval('public.instance_moderations_id_seq'::regclass); + + -- -- Name: licencias id; Type: DEFAULT; Schema: public; Owner: - -- @@ -1774,6 +1814,14 @@ ALTER TABLE ONLY public.indexed_posts ADD CONSTRAINT indexed_posts_pkey PRIMARY KEY (id); +-- +-- Name: instance_moderations instance_moderations_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.instance_moderations + ADD CONSTRAINT instance_moderations_pkey PRIMARY KEY (id); + + -- -- Name: licencias licencias_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2139,6 +2187,27 @@ CREATE INDEX index_indexed_posts_on_locale ON public.indexed_posts USING btree ( CREATE INDEX index_indexed_posts_on_site_id ON public.indexed_posts USING btree (site_id); +-- +-- Name: index_instance_moderations_on_instance_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_instance_moderations_on_instance_id ON public.instance_moderations USING btree (instance_id); + + +-- +-- Name: index_instance_moderations_on_site_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_instance_moderations_on_site_id ON public.instance_moderations USING btree (site_id); + + +-- +-- Name: index_instance_moderations_on_site_id_and_instance_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_instance_moderations_on_site_id_and_instance_id ON public.instance_moderations USING btree (site_id, instance_id); + + -- -- Name: index_licencias_on_name; Type: INDEX; Schema: public; Owner: - -- @@ -2476,6 +2545,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20240220161414'), ('20240221184007'), ('20240223170317'), -('20240226133022'); +('20240226133022'), +('20240226134335');