diff --git a/app/models/activity_pub.rb b/app/models/activity_pub.rb new file mode 100644 index 00000000..c0474b89 --- /dev/null +++ b/app/models/activity_pub.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +# = ActivityPub = +# +# El registro de actividades recibidas y su estado. Cuando recibimos +# una actividad, puede estar destinada a varies actores dentro de Sutty, +# con lo que generamos una cola para cada une. +# +# @see {https://www.w3.org/TR/activitypub/#client-to-server-interactions} +class ActivityPub < ApplicationRecord + include AASM + + belongs_to :site + belongs_to :object, polymorphic: true + has_many :activities + + validates :site_id, presence: true + validates :object_id, presence: true + validates :aasm_state, presence: true, inclusion: { in: %w[paused approved rejected reported deleted] } + + aasm do + # Todavía no hay una decisión sobre el objeto + state :paused, initial: true + # Le usuarie aprobó el objeto + state :approved + # Le usuarie rechazó el objeto + state :rejected + # Le usuarie reportó el objeto + state :reported + # Le actore eliminó el objeto + state :deleted + end +end diff --git a/app/models/activity_pub/activity.rb b/app/models/activity_pub/activity.rb new file mode 100644 index 00000000..4a88c1f3 --- /dev/null +++ b/app/models/activity_pub/activity.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# = Activity = +# +# Lleva un registro de las actividades que nos piden hacer remotamente. +# +# Las actividades pueden tener distintos destinataries (sitios/actores). +# +# @todo Obtener el contenido del objeto dinámicamente si no existe +# localmente, por ejemplo cuando la actividad crea un objeto pero lo +# envía como referencia en lugar de anidarlo. +# +# @see {https://www.w3.org/TR/activitypub/#client-to-server-interactions} +class ActivityPub::Activity < ApplicationRecord + include ActivityPub::Concerns::JsonLdConcern + + belongs_to :activity_pub + has_one :object, through: :activity_pub + + validates :activity_pub_id, presence: true + + # Siempre en orden descendiente para saber el último estado + default_scope -> { order(created_at: :desc) } +end diff --git a/app/models/activity_pub/activity/create.rb b/app/models/activity_pub/activity/create.rb new file mode 100644 index 00000000..3dcba5c2 --- /dev/null +++ b/app/models/activity_pub/activity/create.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +class ActivityPub::Activity::Create < ActivityPub::Activity; end diff --git a/app/models/activity_pub/activity/delete.rb b/app/models/activity_pub/activity/delete.rb new file mode 100644 index 00000000..f3684a0f --- /dev/null +++ b/app/models/activity_pub/activity/delete.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +class ActivityPub::Activity::Delete < ActivityPub::Activity; end diff --git a/app/models/activity_pub/activity/flag.rb b/app/models/activity_pub/activity/flag.rb new file mode 100644 index 00000000..2911911e --- /dev/null +++ b/app/models/activity_pub/activity/flag.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +class ActivityPub::Activity::Flag < ActivityPub::Activity; end diff --git a/app/models/activity_pub/activity/follow.rb b/app/models/activity_pub/activity/follow.rb new file mode 100644 index 00000000..c22dfd51 --- /dev/null +++ b/app/models/activity_pub/activity/follow.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# = Follow = +# +# Una actividad de seguimiento se refiere siempre a une actore (el +# sitio) y proviene de otre actore. +class ActivityPub::Activity::Follow < ActivityPub::Activity; end diff --git a/app/models/activity_pub/activity/generic.rb b/app/models/activity_pub/activity/generic.rb new file mode 100644 index 00000000..8bf76471 --- /dev/null +++ b/app/models/activity_pub/activity/generic.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +class ActivityPub::Activity::Generic < ActivityPub::Activity; end diff --git a/app/models/activity_pub/activity/undo.rb b/app/models/activity_pub/activity/undo.rb new file mode 100644 index 00000000..a4915394 --- /dev/null +++ b/app/models/activity_pub/activity/undo.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# = Undo = +# +# Deshace una actividad, dependiendo de la actividad a la que se +# refiere. +class ActivityPub::Activity::Undo < ActivityPub::Activity +end diff --git a/app/models/activity_pub/activity/update.rb b/app/models/activity_pub/activity/update.rb new file mode 100644 index 00000000..8089cdcf --- /dev/null +++ b/app/models/activity_pub/activity/update.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +class ActivityPub::Activity::Update < ActivityPub::Activity; end diff --git a/app/models/activity_pub/actor.rb b/app/models/activity_pub/actor.rb new file mode 100644 index 00000000..f29c382a --- /dev/null +++ b/app/models/activity_pub/actor.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# = Actor = +# +# Actor es la entidad que realiza acciones en ActivityPub +# +# @todo Obtener el perfil dinámicamente +class ActivityPub::Actor < ApplicationRecord + include ActivityPub::Concerns::JsonLdConcern + + belongs_to :instance + has_many :activity_pubs, as: :object + has_many :objects +end diff --git a/app/models/activity_pub/concerns/json_ld_concern.rb b/app/models/activity_pub/concerns/json_ld_concern.rb new file mode 100644 index 00000000..b0899606 --- /dev/null +++ b/app/models/activity_pub/concerns/json_ld_concern.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class ActivityPub + module Concerns + module JsonLdConcern + extend ActiveSupport::Concern + + included do + validates :uri, presence: true, uniqueness: true + + # Cuando asignamos contenido, obtener la URI si no lo hicimos ya + before_save :uri_from_content!, unless: :uri? + + # Obtiene un tipo de actividad a partir del tipo informado + # + # @param object [Hash] + # @return [Activity] + def self.type_from(object) + "#{self.class.name}::#{object[:type].presence || 'Generic'}".constantize + rescue NameError + self.class::Generic + end + + private + + def uri_from_content! + self.uri = content[:id] + end + end + end + end +end diff --git a/app/models/activity_pub/instance.rb b/app/models/activity_pub/instance.rb new file mode 100644 index 00000000..fe4a777b --- /dev/null +++ b/app/models/activity_pub/instance.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# = Instance = +# +# Representa cada instancia del fediverso que interactúa con la Social +# Inbox. +class ActivityPub::Instance < ApplicationRecord + include AASM + + validates :aasm_state, presence: true, inclusion: { in: %w[paused allowed blocked] } + validates :hostname, uniqueness: true, hostname: true + + has_many :activity_pubs + has_many :actors + + aasm do + state :paused, initial: true + state :allowed + state :blocked + end +end diff --git a/app/models/activity_pub/object.rb b/app/models/activity_pub/object.rb new file mode 100644 index 00000000..519749ef --- /dev/null +++ b/app/models/activity_pub/object.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# Almacena objetos de ActivityPub, como Note, Article, etc. +class ActivityPub::Object < ApplicationRecord + include ActivityPub::Concerns::JsonLdConcern + + belongs_to :actor + has_many :activity_pubs, as: :object + + validates :actor_id, presence: true +end diff --git a/app/models/activity_pub/object/application.rb b/app/models/activity_pub/object/application.rb new file mode 100644 index 00000000..e8a6f97c --- /dev/null +++ b/app/models/activity_pub/object/application.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# = Application = +# +# Una aplicación o instancia +class ActivityPub::Object::Application < ActivityPub::Object; end diff --git a/app/models/activity_pub/object/article.rb b/app/models/activity_pub/object/article.rb new file mode 100644 index 00000000..ad1a6131 --- /dev/null +++ b/app/models/activity_pub/object/article.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# = Article = +# +# Representa artículos +class ActivityPub::Object::Article < ActivityPub::Object; end diff --git a/app/models/activity_pub/object/generic.rb b/app/models/activity_pub/object/generic.rb new file mode 100644 index 00000000..f345e7a9 --- /dev/null +++ b/app/models/activity_pub/object/generic.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +# = Generic = +class ActivityPub::Object::Generic < ActivityPub::Object; end diff --git a/app/models/activity_pub/object/note.rb b/app/models/activity_pub/object/note.rb new file mode 100644 index 00000000..0f84c747 --- /dev/null +++ b/app/models/activity_pub/object/note.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# = Note = +# +# Representa notas, el tipo más común de objeto del Fediverso. +class ActivityPub::Object::Note < ActivityPub::Object; end diff --git a/app/models/activity_pub/object/organization.rb b/app/models/activity_pub/object/organization.rb new file mode 100644 index 00000000..a5327d10 --- /dev/null +++ b/app/models/activity_pub/object/organization.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# = Organization = +# +# Una organización +class ActivityPub::Object::Organization < ActivityPub::Object; end diff --git a/app/models/activity_pub/object/person.rb b/app/models/activity_pub/object/person.rb new file mode 100644 index 00000000..98a1568d --- /dev/null +++ b/app/models/activity_pub/object/person.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# = Person = +# +# Una persona, el perfil de une actore +class ActivityPub::Object::Person < ActivityPub::Object; end diff --git a/db/migrate/20240219153919_create_activity_pub_activities.rb b/db/migrate/20240219153919_create_activity_pub_activities.rb new file mode 100644 index 00000000..555656ad --- /dev/null +++ b/db/migrate/20240219153919_create_activity_pub_activities.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# Actividades. Se asocian a un objeto y a una cola de moderación +class CreateActivityPubActivities < ActiveRecord::Migration[6.1] + def change + create_table :activity_pub_activities, id: :uuid do |t| + t.timestamps + + t.uuid :activity_pub_id, index: true, null: false + + t.string :type, null: false + t.string :uri, null: false + t.jsonb :content, default: {} + end + end +end diff --git a/db/migrate/20240219175839_create_activity_pub_actors.rb b/db/migrate/20240219175839_create_activity_pub_actors.rb new file mode 100644 index 00000000..656b3f63 --- /dev/null +++ b/db/migrate/20240219175839_create_activity_pub_actors.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# Almacena actores de ActivityPub y los relaciona con actividades +class CreateActivityPubActors < ActiveRecord::Migration[6.1] + def change + create_table :activity_pub_actors, id: :uuid do |t| + t.timestamps + t.uuid :instance_id, index: true, null: false + t.string :uri, index: true, unique: true, null: false + end + end +end diff --git a/db/migrate/20240219204011_create_activity_pubs.rb b/db/migrate/20240219204011_create_activity_pubs.rb new file mode 100644 index 00000000..cf797fc8 --- /dev/null +++ b/db/migrate/20240219204011_create_activity_pubs.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# Registro de actividades. +class CreateActivityPubs < ActiveRecord::Migration[6.1] + def change + create_table :activity_pubs, id: :uuid do |t| + t.timestamps + + t.bigint :site_id, null: false + t.uuid :object_id, null: false + t.string :object_type, null: false + + t.string :aasm_state, null: false + + t.index %i[site_id object_id object_type], unique: true + end + end +end diff --git a/db/migrate/20240219204224_create_activity_pub_objects.rb b/db/migrate/20240219204224_create_activity_pub_objects.rb new file mode 100644 index 00000000..865589ab --- /dev/null +++ b/db/migrate/20240219204224_create_activity_pub_objects.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# Almacena objetos de ActivityPub. Los objetos pueden estar compartidos +# por toda la instancia. +class CreateActivityPubObjects < ActiveRecord::Migration[6.1] + def change + create_table :activity_pub_objects, id: :uuid do |t| + t.timestamps + + t.uuid :actor_id, index: true, null: false + + t.string :type, null: false + t.string :uri, null: false, unique: true + t.jsonb :content, default: {} + end + end +end diff --git a/db/migrate/20240220161414_create_activity_pub_instances.rb b/db/migrate/20240220161414_create_activity_pub_instances.rb new file mode 100644 index 00000000..feb9351d --- /dev/null +++ b/db/migrate/20240220161414_create_activity_pub_instances.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Almacena las instancias +class CreateActivityPubInstances < ActiveRecord::Migration[6.1] + def change + create_table :activity_pub_instances, id: :uuid do |t| + t.timestamps + t.string :hostname, index: true, unique: true, null: false + t.string :aasm_state, null: false + t.jsonb :content, default: {} + end + end +end diff --git a/db/structure.sql b/db/structure.sql index dede286d..723c9e99 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -473,6 +473,78 @@ CREATE SEQUENCE public.active_storage_variant_records_id_seq ALTER SEQUENCE public.active_storage_variant_records_id_seq OWNED BY public.active_storage_variant_records.id; +-- +-- Name: activity_pub_activities; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.activity_pub_activities ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + activity_pub_id uuid NOT NULL, + type character varying NOT NULL, + uri character varying NOT NULL, + content jsonb DEFAULT '{}'::jsonb +); + + +-- +-- Name: activity_pub_actors; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.activity_pub_actors ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + instance_id uuid NOT NULL, + uri character varying NOT NULL +); + + +-- +-- Name: activity_pub_instances; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.activity_pub_instances ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + hostname character varying NOT NULL, + aasm_state character varying NOT NULL, + content jsonb DEFAULT '{}'::jsonb +); + + +-- +-- Name: activity_pub_objects; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.activity_pub_objects ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + actor_id uuid NOT NULL, + type character varying NOT NULL, + uri character varying NOT NULL, + content jsonb DEFAULT '{}'::jsonb +); + + +-- +-- Name: activity_pubs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.activity_pubs ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + site_id bigint NOT NULL, + object_id uuid NOT NULL, + object_type character varying NOT NULL, + aasm_state character varying NOT NULL +); + + -- -- Name: ar_internal_metadata; Type: TABLE; Schema: public; Owner: - -- @@ -1565,6 +1637,46 @@ ALTER TABLE ONLY public.active_storage_variant_records ADD CONSTRAINT active_storage_variant_records_pkey PRIMARY KEY (id); +-- +-- Name: activity_pub_activities activity_pub_activities_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.activity_pub_activities + ADD CONSTRAINT activity_pub_activities_pkey PRIMARY KEY (id); + + +-- +-- Name: activity_pub_actors activity_pub_actors_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.activity_pub_actors + ADD CONSTRAINT activity_pub_actors_pkey PRIMARY KEY (id); + + +-- +-- Name: activity_pub_instances activity_pub_instances_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.activity_pub_instances + ADD CONSTRAINT activity_pub_instances_pkey PRIMARY KEY (id); + + +-- +-- Name: activity_pub_objects activity_pub_objects_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.activity_pub_objects + ADD CONSTRAINT activity_pub_objects_pkey PRIMARY KEY (id); + + +-- +-- Name: activity_pubs activity_pubs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.activity_pubs + ADD CONSTRAINT activity_pubs_pkey PRIMARY KEY (id); + + -- -- Name: blazer_audits blazer_audits_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -1865,6 +1977,48 @@ CREATE UNIQUE INDEX index_active_storage_blobs_on_key_and_service_name ON public CREATE UNIQUE INDEX index_active_storage_variant_records_uniqueness ON public.active_storage_variant_records USING btree (blob_id, variation_digest); +-- +-- Name: index_activity_pub_activities_on_activity_pub_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_activity_pub_activities_on_activity_pub_id ON public.activity_pub_activities USING btree (activity_pub_id); + + +-- +-- Name: index_activity_pub_actors_on_instance_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_activity_pub_actors_on_instance_id ON public.activity_pub_actors USING btree (instance_id); + + +-- +-- Name: index_activity_pub_actors_on_uri; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_activity_pub_actors_on_uri ON public.activity_pub_actors USING btree (uri); + + +-- +-- Name: index_activity_pub_instances_on_hostname; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_activity_pub_instances_on_hostname ON public.activity_pub_instances USING btree (hostname); + + +-- +-- Name: index_activity_pub_objects_on_actor_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_activity_pub_objects_on_actor_id ON public.activity_pub_objects USING btree (actor_id); + + +-- +-- Name: index_activity_pubs_on_site_id_and_object_id_and_object_type; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_activity_pubs_on_site_id_and_object_id_and_object_type ON public.activity_pubs USING btree (site_id, object_id, object_type); + + -- -- Name: index_blazer_audits_on_query_id; Type: INDEX; Schema: public; Owner: - -- @@ -2320,6 +2474,11 @@ INSERT INTO "schema_migrations" (version) VALUES ('20230829204127'), ('20230921155401'), ('20230927153926'), -('20240216170202'); +('20240216170202'), +('20240219153919'), +('20240219175839'), +('20240219204011'), +('20240219204224'), +('20240220161414');