federails 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/README.md +19 -189
  4. data/app/controllers/federails/client/actors_controller.rb +18 -4
  5. data/app/controllers/federails/server/actors_controller.rb +4 -1
  6. data/app/controllers/federails/server/published_controller.rb +30 -0
  7. data/app/controllers/federails/server/web_finger_controller.rb +9 -6
  8. data/app/controllers/federails/server_controller.rb +7 -0
  9. data/app/models/concerns/federails/actor_entity.rb +120 -56
  10. data/app/models/concerns/federails/data_entity.rb +205 -0
  11. data/app/models/concerns/federails/handles_delete_requests.rb +31 -0
  12. data/app/models/concerns/federails/has_uuid.rb +27 -1
  13. data/app/models/federails/activity.rb +27 -4
  14. data/app/models/federails/actor.rb +93 -30
  15. data/app/models/federails/following.rb +29 -9
  16. data/app/policies/federails/server/publishable_policy.rb +15 -0
  17. data/app/views/federails/client/actors/_actor.json.jbuilder +4 -1
  18. data/app/views/federails/client/actors/gone.html.erb +1 -0
  19. data/app/views/federails/client/actors/index.html.erb +7 -1
  20. data/app/views/federails/server/activities/_activity.activitypub.jbuilder +8 -3
  21. data/app/views/federails/server/actors/_actor.activitypub.jbuilder +2 -2
  22. data/app/views/federails/server/actors/_tombstone.activitypub.jbuilder +9 -0
  23. data/app/views/federails/server/actors/show.activitypub.jbuilder +5 -1
  24. data/app/views/federails/server/published/_publishable.activitypub.jbuilder +11 -0
  25. data/app/views/federails/server/published/_tombstone.activitypub.jbuilder +9 -0
  26. data/app/views/federails/server/published/show.activitypub.jbuilder +5 -0
  27. data/config/routes.rb +4 -0
  28. data/db/migrate/20250122160618_add_extensions_to_federails_actors.rb +5 -0
  29. data/db/migrate/20250301082500_add_local_to_actors.rb +11 -0
  30. data/db/migrate/20250329123939_add_actor_type_to_actors.rb +5 -0
  31. data/db/migrate/20250329123940_add_tombstoned_at_to_actors.rb +5 -0
  32. data/lib/federails/configuration.rb +24 -1
  33. data/lib/federails/data_transformer/note.rb +31 -0
  34. data/lib/federails/maintenance/actors_updater.rb +67 -0
  35. data/lib/federails/utils/actor.rb +53 -0
  36. data/lib/federails/utils/object.rb +131 -0
  37. data/lib/federails/version.rb +1 -1
  38. data/lib/federails.rb +54 -0
  39. data/lib/fediverse/inbox.rb +40 -8
  40. data/lib/fediverse/notifier.rb +3 -0
  41. data/lib/fediverse/request.rb +13 -0
  42. data/lib/fediverse/webfinger.rb +72 -26
  43. data/lib/fediverse.rb +3 -0
  44. data/lib/tasks/federails_tasks.rake +8 -4
  45. metadata +22 -6
@@ -1,13 +1,18 @@
1
1
  context = true unless context == false
2
+ addressing = true unless addressing == false
2
3
  json.set! '@context', 'https://www.w3.org/ns/activitystreams' if context
3
4
 
4
5
  json.id Federails::Engine.routes.url_helpers.server_actor_activity_url activity.actor, activity
5
6
  json.type activity.action
6
7
  json.actor activity.actor.federated_url
7
- json.to ['https://www.w3.org/ns/activitystreams#Public']
8
- json.cc [activity.actor.followers_url]
8
+ if addressing
9
+ json.to ['https://www.w3.org/ns/activitystreams#Public']
10
+ json.cc [activity.actor.followers_url]
11
+ end
9
12
 
10
- if activity.entity.respond_to? :to_activitypub_object
13
+ if activity.entity.is_a? Federails::Activity
14
+ json.object { json.partial!('federails/server/activities/activity', activity: activity.entity, context: false, addressing: false) }
15
+ elsif activity.entity.respond_to? :to_activitypub_object
11
16
  json.object activity.entity.to_activitypub_object
12
17
  elsif activity.entity.respond_to? :federated_url
13
18
  json.object activity.entity.federated_url
@@ -1,4 +1,4 @@
1
- actor_data = actor.entity.to_activitypub_object
1
+ actor_data = actor.entity&.to_activitypub_object || {}
2
2
 
3
3
  json.set! '@context', ([
4
4
  'https://www.w3.org/ns/activitystreams',
@@ -9,7 +9,7 @@ json.set! '@context', ([
9
9
 
10
10
  json.id actor.federated_url
11
11
  json.name actor.name
12
- json.type actor.entity_configuration[:actor_type]
12
+ json.type actor.actor_type
13
13
  json.preferredUsername actor.username
14
14
  json.inbox actor.inbox_url
15
15
  json.outbox actor.outbox_url
@@ -0,0 +1,9 @@
1
+ json.set! '@context', [
2
+ 'https://www.w3.org/ns/activitystreams',
3
+ 'https://w3id.org/security/v1',
4
+ ]
5
+
6
+ json.id actor.federated_url
7
+ json.type 'Tombstone'
8
+ json.deleted actor.tombstoned_at
9
+ json.formerType actor.actor_type
@@ -1 +1,5 @@
1
- json.partial! 'federails/server/actors/actor', actor: @actor
1
+ if @actor.tombstoned?
2
+ json.partial! 'federails/server/actors/tombstone', actor: @actor
3
+ else
4
+ json.partial! 'federails/server/actors/actor', actor: @actor
5
+ end
@@ -0,0 +1,11 @@
1
+ context = true unless context == false
2
+ json.set! '@context', 'https://www.w3.org/ns/activitystreams' if context
3
+
4
+ publishable.to_activitypub_object.each_pair do |key, value|
5
+ json.set! key, value
6
+ end
7
+
8
+ json.id publishable.federated_url
9
+ json.actor publishable.federails_actor.federated_url
10
+ json.to ['https://www.w3.org/ns/activitystreams#Public']
11
+ json.cc [publishable.federails_actor.followers_url]
@@ -0,0 +1,9 @@
1
+ json.set! '@context', [
2
+ 'https://www.w3.org/ns/activitystreams',
3
+ 'https://w3id.org/security/v1',
4
+ ]
5
+
6
+ json.id publishable.federated_url
7
+ json.type 'Tombstone'
8
+ json.deleted publishable.federails_tombstoned_at
9
+ json.formerType publishable.federails_data_configuration[:handles]
@@ -0,0 +1,5 @@
1
+ if @publishable.federails_tombstoned?
2
+ json.partial! 'federails/server/published/tombstone', publishable: @publishable
3
+ else
4
+ json.partial! 'federails/server/published/publishable', publishable: @publishable
5
+ end
data/config/routes.rb CHANGED
@@ -45,5 +45,9 @@ Federails::Engine.routes.draw do
45
45
  resources :activities, only: [:show]
46
46
  resources :followings, only: [:show]
47
47
  end
48
+
49
+ scope :published do
50
+ get ':publishable_type/:id', to: 'published#show', as: :published
51
+ end
48
52
  end
49
53
  end
@@ -0,0 +1,5 @@
1
+ class AddExtensionsToFederailsActors < ActiveRecord::Migration[7.1]
2
+ def change
3
+ add_column :federails_actors, :extensions, :json, default: nil, null: true
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ class AddLocalToActors < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_column :federails_actors, :local, :boolean, null: false, default: false
4
+
5
+ reversible do |dir|
6
+ dir.up do
7
+ exec_update 'UPDATE federails_actors SET local=true WHERE entity_type IS NOT NULL'
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ class AddActorTypeToActors < ActiveRecord::Migration[7.2]
2
+ def change
3
+ add_column :federails_actors, :actor_type, :string, null: true
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddTombstonedAtToActors < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_column :federails_actors, :tombstoned_at, :datetime, default: nil
4
+ end
5
+ end
@@ -1,5 +1,7 @@
1
1
  module Federails
2
2
  # rubocop:disable Style/ClassVars
3
+
4
+ # Stores the Federails configuration in a _singleton_.
3
5
  module Configuration
4
6
  # Application name, used in well-known and nodeinfo endpoints
5
7
  mattr_accessor :app_name
@@ -26,8 +28,12 @@ module Federails
26
28
  @@enable_discovery = true
27
29
 
28
30
  # Does the site allow open registrations? (only used for nodeinfo reporting)
29
- mattr_accessor :open_registrations
31
+ # Can either be a static boolean, or a Proc which will be called to get the state.
32
+ mattr_writer :open_registrations
30
33
  @@open_registrations = false
34
+ def self.open_registrations
35
+ @@open_registrations.is_a?(Proc) ? @@open_registrations.call : @@open_registrations
36
+ end
31
37
 
32
38
  # Application layout
33
39
  mattr_accessor :app_layout
@@ -45,7 +51,16 @@ module Federails
45
51
  mattr_accessor :base_client_controller
46
52
  @@base_client_controller = 'ActionController::Base'
47
53
 
54
+ # @!method self.remote_follow_url_method
55
+ #
48
56
  # Route method for remote-following requests
57
+
58
+ # @!method self.remote_follow_url_method=(value)
59
+ #
60
+ # Sets the route method for remote-following requests
61
+ # @param value [String] Route method name as used in links
62
+ # @example
63
+ # remote_follow_url_method 'main_app.my_custom_route_helper'
49
64
  mattr_accessor :remote_follow_url_method
50
65
  @@remote_follow_url_method = 'federails.new_client_following_url'
51
66
 
@@ -66,6 +81,14 @@ module Federails
66
81
  def self.register_actor_class(klass, config = {})
67
82
  @@actor_types[klass.name] = config.merge(class: klass)
68
83
  end
84
+
85
+ # List of data types (classes using Federails::DataEntity)
86
+ mattr_reader :data_types
87
+ @@data_types = {}
88
+
89
+ def self.register_data_type(klass, config = {})
90
+ @@data_types[klass.name] = config.merge(class: klass)
91
+ end
69
92
  end
70
93
  # rubocop:enable Style/ClassVars
71
94
  end
@@ -0,0 +1,31 @@
1
+ module Federails
2
+ module DataTransformer
3
+ module Note
4
+ # Renders a Note. The entity is used to determine actor and generic fields data
5
+ #
6
+ # @param entity [#federail_actor, #federated_url, #created_at, #updated_at] A model instance
7
+ # @param content [String] Note content
8
+ # @param name [String, nil] Optional name/title
9
+ # @param custom [Hash] Optional additional keys (e.g.: attachment, icon, ...). Defaults will override these.
10
+ #
11
+ # @return [Hash]
12
+ #
13
+ # @example
14
+ # Federails::DataTransformer::Note.to_federation(comment, content: comment.content, custom: { 'inReplyTo' => comment.parent.federated_url })
15
+ #
16
+ # See:
17
+ # - https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object
18
+ # - https://www.w3.org/TR/activitystreams-vocabulary/#dfn-note
19
+ def self.to_federation(entity, content:, name: nil, custom: {})
20
+ custom.merge '@context' => 'https://www.w3.org/ns/activitystreams',
21
+ 'id' => entity.federated_url,
22
+ 'type' => 'Note',
23
+ 'name' => name,
24
+ 'content' => content,
25
+ 'attributedTo' => entity.federails_actor.federated_url,
26
+ 'published' => entity.created_at,
27
+ 'updated' => entity.updated_at
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,67 @@
1
+ module Federails
2
+ module Maintenance
3
+ class ActorsUpdater
4
+ class << self
5
+ # Fetches all distant actors again and update their local copy
6
+ #
7
+ # A block can be passed with two arguments: the actor being updated and the update status
8
+ #
9
+ # @param actors [Integer, Federails::Actor, Array<Federails::Actor>, nil] Actor ID, Actor or list of actors to update.
10
+ # If nothing is passed, all distant actors are processed
11
+ # @example
12
+ # Update all distant actors
13
+ # Federails::Maintenance::ActorUpdater.run
14
+ # With an actor id:
15
+ # Federails::Maintenance::ActorUpdater.run 1
16
+ # With a federated URL:
17
+ # Federails::Maintenance::ActorUpdater.run 'https://example.com/actor'
18
+ # With a federated URL:
19
+ # Federails::Maintenance::ActorUpdater.run ['https://example.com/actors/1', 'https://example.com/actors/1']
20
+ # With actors:
21
+ # Federails::Maintenance::ActorUpdater.run Federails::Actor.last(10)
22
+ # Update all distant actors and puts status for each actor
23
+ # Federails::Maintenance::ActorUpdater.run {|actor, status| puts "#{actor.federated_url}: #{status}"}
24
+ def run(actors = nil, &block)
25
+ actors_list(actors).each do |actor|
26
+ status = update(actor)
27
+
28
+ yield(actor, status) if block
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ # Make a list of actors to update from the passed attribute
35
+ def actors_list(param) # rubocop:disable Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
36
+ if param.nil?
37
+ Federails::Actor.distant
38
+ elsif param.is_a? String
39
+ [Federails::Actor.distant.find_by!(federated_url: param)]
40
+ elsif param.is_a? Integer
41
+ [Federails::Actor.distant.find(param)]
42
+ elsif param.is_a?(Federails::Actor)
43
+ [param]
44
+ elsif param.respond_to?(:pluck) && param.first.is_a?(Federails::Actor)
45
+ param
46
+ elsif param.is_a?(Array) && param.first.is_a?(String)
47
+ Federails::Actor.distant.where(federated_url: param)
48
+ else
49
+ raise "Cannot extract actors from #{param.class}"
50
+ end
51
+ end
52
+
53
+ # @param actor [Federails::Actor]
54
+ def update(actor)
55
+ return :ignored_local if actor.local?
56
+
57
+ response = Fediverse::Webfinger.fetch_actor_url(actor.federated_url)
58
+ new_attributes = response.attributes.except 'id', 'uuid', 'created_at', 'updated_at', 'local'
59
+
60
+ actor.update(new_attributes) ? :updated : :failed
61
+ rescue ActiveRecord::RecordNotFound
62
+ :not_found
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,53 @@
1
+ module Federails
2
+ module Utils
3
+ class Actor
4
+ # List of the attributes computed for local actors
5
+ COMPUTED_ATTRIBUTES = [
6
+ :federated_url,
7
+ :username,
8
+ :name,
9
+ :server,
10
+ :inbox_url,
11
+ :outbox_url,
12
+ :followers_url,
13
+ :followings_url,
14
+ :profile_url,
15
+ ].freeze
16
+
17
+ class << self
18
+ # @param actor [Federails::Actor]
19
+ # @return [Federails::Actor]
20
+ def tombstone!(actor)
21
+ if actor.local?
22
+ tombstone_local_actor actor
23
+ else
24
+ tombstone_distant_actor actor
25
+ end
26
+
27
+ actor
28
+ end
29
+
30
+ private
31
+
32
+ def tombstone_local_actor(actor)
33
+ Federails::Actor.transaction do
34
+ hash = {
35
+ tombstoned_at: Time.current,
36
+ entity: nil,
37
+ }
38
+ # Hardcode attributes depending on the actor's entity
39
+ COMPUTED_ATTRIBUTES.each { |attribute| hash[attribute] = actor.send(attribute) }
40
+
41
+ actor.update! hash
42
+
43
+ Activity.create! actor: actor, action: 'Delete', entity: actor
44
+ end
45
+ end
46
+
47
+ def tombstone_distant_actor(actor)
48
+ actor.update! tombstoned_at: Time.current
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,131 @@
1
+ module Federails
2
+ module Utils
3
+ # Methods to manipulate incoming objects
4
+ class Object
5
+ class << self
6
+ # Finds data from an object or its ID.
7
+ #
8
+ # When data exists locally, the entity is returned.
9
+ # For distant data, a new instance is returned unless the target does not exist.
10
+ #
11
+ # @param object_or_id [String, Hash] String identifier or incoming object
12
+ #
13
+ # @return [ApplicationRecord, nil] Entity or nil when invalid/not found
14
+ def find_or_initialize(object_or_id)
15
+ federated_url = object_or_id.is_a?(Hash) ? object_or_id['id'] : object_or_id
16
+
17
+ route = local_route(federated_url)
18
+ return from_local_route(route) if route
19
+
20
+ from_distant_server(object_or_id)
21
+ end
22
+
23
+ # Search for a distant object in actors and configured data entities.
24
+ #
25
+ # This is useful to find something when the type is unknown, as an object from a Delete activity.
26
+ #
27
+ # @param federated_url [String] Object identifier
28
+ # @return [Federails::Actor, Federails::DataEntity, Federails::Following, nil]
29
+ def find_distant_object_in_all(federated_url)
30
+ # Search in actors
31
+ object = Federails::Actor.find_by federated_url: federated_url
32
+ return object if object.present?
33
+
34
+ # Search in followings
35
+ object = Federails::Following.find_by federated_url: federated_url
36
+ return object if object.present?
37
+
38
+ # Search in data entities
39
+ Federails.configuration.data_types.keys.sort.each do |klass|
40
+ object = klass.constantize.find_by federated_url: federated_url
41
+
42
+ break if object.present?
43
+ end
44
+
45
+ object
46
+ end
47
+
48
+ # Finds or initializes an entity from an ActivityPub object or id
49
+ #
50
+ # @see .find_or_initialize
51
+ #
52
+ # @param object_or_id [String, Hash] String identifier or incoming object
53
+ #
54
+ # @return [ApplicationRecord, nil] Entity or nil when invalid/not found
55
+ def find_or_initialize!(object_or_id)
56
+ entity = find_or_initialize object_or_id
57
+ raise ActiveRecord::RecordNotFound unless entity
58
+
59
+ entity
60
+ end
61
+
62
+ # Finds or create an entity from an ActivityPub object or id
63
+ #
64
+ # Note that the data transformer MUST return timestamps from the ActivityPub object if used on the model,
65
+ # as they won't be set automatically.
66
+ #
67
+ # @see .find_or_initialize!
68
+ #
69
+ # @param object_or_id [String, Hash] String identifier or incoming object
70
+ #
71
+ # @return [ApplicationRecord, nil] Entity or nil when invalid/not found
72
+ def find_or_create!(object_or_id)
73
+ entity = find_or_initialize! object_or_id
74
+ return entity if entity.persisted?
75
+
76
+ entity.save!(touch: false)
77
+ entity
78
+ end
79
+
80
+ # Returns the timestamps to use from an ActivityPub object
81
+ #
82
+ # @param hash [Hash] ActivityPub object
83
+ #
84
+ # @return [Hash] Hash with timestamps
85
+ def timestamp_attributes(hash)
86
+ {
87
+ created_at: hash['published'] ||= Time.current,
88
+ updated_at: hash['updated'].presence || hash['published'],
89
+ }
90
+ end
91
+
92
+ private
93
+
94
+ def local_route(url)
95
+ route = Utils::Host.local_route(url)
96
+
97
+ return nil unless route && route[:controller] == 'federails/server/published' && route[:action] == 'show'
98
+
99
+ route
100
+ end
101
+
102
+ def from_local_route(route)
103
+ config = Federails.data_entity_handled_on route[:publishable_type]
104
+ return unless config
105
+
106
+ config[:class]&.find_by(config[:url_param] => route[:id])
107
+ rescue ActiveRecord::RecordNotFound
108
+ nil
109
+ end
110
+
111
+ def from_distant_server(federated_url)
112
+ hash = Fediverse::Request.dereference(federated_url)
113
+ return unless hash
114
+
115
+ handler = Federails.data_entity_handler_for hash
116
+ return unless handler
117
+
118
+ entity = handler[:class].find_by federated_url: hash['id']
119
+ return entity if entity
120
+
121
+ entity = handler[:class].new_from_activitypub_object(hash)
122
+ return unless entity
123
+
124
+ entity.federails_actor = Federails::Actor.find_or_create_by_object hash['attributedTo'] if entity && !entity.federails_actor
125
+
126
+ entity
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -1,3 +1,3 @@
1
1
  module Federails
2
- VERSION = '0.4.0'.freeze
2
+ VERSION = '0.6.0'.freeze
3
3
  end
data/lib/federails.rb CHANGED
@@ -1,9 +1,14 @@
1
1
  require 'federails/version'
2
2
  require 'federails/engine'
3
3
  require 'federails/configuration'
4
+ require 'federails/utils/object'
4
5
 
5
6
  # rubocop:disable Style/ClassVars
7
+
8
+ # This module includes classes and methods related to Ruby on Rails: engine configuration, models, controllers, etc.
6
9
  module Federails
10
+ DEFAULT_DATA_FILTER_METHOD = :handle_federated_object?
11
+
7
12
  mattr_reader :configuration
8
13
  @@configuration = Configuration
9
14
 
@@ -49,6 +54,55 @@ module Federails
49
54
  Configuration.actor_types[klass]
50
55
  end
51
56
 
57
+ # @return [Boolean] True if the given model is a possible data entity
58
+ def data_entity?(class_or_instance)
59
+ Configuration.data_types.key? class_or_instance_name(class_or_instance)
60
+ end
61
+
62
+ # Finds configured data types from ActivityPub type
63
+ #
64
+ # @param type [String] ActivityPub object type, as configured with `:handles`
65
+ # @return [Array] List of data entity configurations
66
+ #
67
+ # @example
68
+ # data_entity_handlers_for 'Note'
69
+ def data_entity_handlers_for(type)
70
+ Federails::Configuration.data_types.select { |_, v| v[:handles] == type }.map(&:last)
71
+ end
72
+
73
+ # Finds the configured handler for a given ActivityPub object
74
+ #
75
+ # @param hash [Hash] ActivityPub object hash
76
+ #
77
+ # @return [Hash, nil] Data entity configuration
78
+ def data_entity_handler_for(hash)
79
+ data_entity_handlers_for(hash['type']).find do |handler|
80
+ return true if !handler[:filter_method] && !handler[:class].respond_to?(DEFAULT_DATA_FILTER_METHOD)
81
+
82
+ handler[:class].send(handler[:filter_method] || DEFAULT_DATA_FILTER_METHOD, hash)
83
+ end
84
+ end
85
+
86
+ # Finds configured data type from route path segment
87
+ #
88
+ # @param route_path_segment [Symbol, String] Route path segment, as configured with `:route_path_segment`
89
+ # @return [Hash, nil] Entity configuration
90
+ #
91
+ # @example
92
+ # data_entity_handled_on :articles
93
+ def data_entity_handled_on(route_path_segment)
94
+ route_path_segment = route_path_segment.to_sym
95
+ Federails::Configuration.data_types.find { |_, v| v[:route_path_segment] == route_path_segment }&.last
96
+ end
97
+
98
+ # @return [Hash] The configuration for the given data entity
99
+ def data_entity_configuration(class_or_instance)
100
+ klass = class_or_instance_name(class_or_instance)
101
+ raise "#{klass} is not a configured data entity" unless Configuration.data_types.key?(klass)
102
+
103
+ Configuration.data_types[klass]
104
+ end
105
+
52
106
  private
53
107
 
54
108
  # @return [String] Class name of the provided class or instance
@@ -4,27 +4,51 @@ module Fediverse
4
4
  class Inbox
5
5
  @@handlers = {} # rubocop:todo Style/ClassVars
6
6
  class << self
7
+ # Registers a handler for incoming data
8
+ #
9
+ # Unless a specific type is not implemented in Federails, you should leave the 'Delete' activity to Federails:
10
+ # it will dispatch a `on_federails_delete_requested` event on the right objects.
11
+ #
12
+ # @param activity_type [String] Target activity type ('Create', 'Follow', 'Like', ...)
13
+ # See https://www.w3.org/TR/activitystreams-vocabulary/#activity-types for a list of common ones
14
+ # @param object_type [String] Type of the related object ('Article', 'Note', ...)
15
+ # See https://www.w3.org/TR/activitystreams-vocabulary/#object-types for a list of common object types
16
+ # @param klass [String] Class handling the incoming object
17
+ # @param method [Symbol] Method in the class that will handle the object
7
18
  def register_handler(activity_type, object_type, klass, method)
8
19
  @@handlers[activity_type] ||= {}
9
20
  @@handlers[activity_type][object_type] ||= {}
10
21
  @@handlers[activity_type][object_type][klass] = method
11
22
  end
12
23
 
24
+ # Executes the registered handler for an incoming object
25
+ #
26
+ # @param payload [Hash] Dereferenced activity
13
27
  def dispatch_request(payload)
14
- handlers = get_handlers(payload['type'], payload['object'].is_a?(Hash) ? payload.dig('object', 'type') : nil)
28
+ return dispatch_delete_request(payload) if payload['type'] == 'Delete'
29
+
30
+ payload['object'] = Fediverse::Request.dereference(payload['object']) if payload.key? 'object'
31
+
32
+ handlers = get_handlers(payload['type'], payload.dig('object', 'type'))
15
33
  handlers.each_pair do |klass, method|
16
34
  klass.send method, payload
17
35
  end
18
36
  return true unless handlers.empty?
19
37
 
20
- # FIXME: Fails silently
21
- # raise NotImplementedError
22
38
  Rails.logger.debug { "Unhandled activity type: #{payload['type']}" }
23
39
  false
24
40
  end
25
41
 
26
42
  private
27
43
 
44
+ def dispatch_delete_request(payload)
45
+ payload['object'] = payload['object']['id'] unless payload['object'].is_a? String
46
+ object = Federails::Utils::Object.find_distant_object_in_all payload['object']
47
+ return if object.blank?
48
+
49
+ object.run_callbacks :on_federails_delete_requested
50
+ end
51
+
28
52
  def get_handlers(activity_type, object_type)
29
53
  {}.merge(@@handlers.dig(activity_type, object_type) || {})
30
54
  .merge(@@handlers.dig(activity_type, '*') || {})
@@ -39,8 +63,8 @@ module Fediverse
39
63
  Federails::Following.create! actor: actor, target_actor: target_actor, federated_url: activity['id']
40
64
  end
41
65
 
42
- def handle_accept_request(activity)
43
- original_activity = Request.get(activity['object'])
66
+ def handle_accept_follow_request(activity)
67
+ original_activity = Request.dereference(activity['object'])
44
68
 
45
69
  actor = Federails::Actor.find_or_create_by_object original_activity['actor']
46
70
  target_actor = Federails::Actor.find_or_create_by_object original_activity['object']
@@ -50,7 +74,7 @@ module Fediverse
50
74
  follow.accept!
51
75
  end
52
76
 
53
- def handle_undo_request(activity)
77
+ def handle_undo_follow_request(activity)
54
78
  original_activity = activity['object']
55
79
 
56
80
  actor = Federails::Actor.find_or_create_by_object original_activity['actor']
@@ -59,10 +83,18 @@ module Fediverse
59
83
  follow = Federails::Following.find_by actor: actor, target_actor: target_actor
60
84
  follow&.destroy
61
85
  end
86
+
87
+ def handle_delete_request(activity)
88
+ object = Federails::Utils::Object.find_distant_object_in_all(activity['object'])
89
+ return if object.blank?
90
+
91
+ object.run_callbacks :on_federails_delete_requested
92
+ end
62
93
  end
63
94
 
64
95
  register_handler 'Follow', '*', self, :handle_create_follow_request
65
- register_handler 'Accept', 'Follow', self, :handle_accept_request
66
- register_handler 'Undo', 'Follow', self, :handle_undo_request
96
+ register_handler 'Accept', 'Follow', self, :handle_accept_follow_request
97
+ register_handler 'Undo', 'Follow', self, :handle_undo_follow_request
98
+ register_handler 'Delete', '*', self, :handle_delete_request
67
99
  end
68
100
  end
@@ -3,6 +3,9 @@ require 'fediverse/signature'
3
3
  module Fediverse
4
4
  class Notifier
5
5
  class << self
6
+ # Posts an activity to its recipients
7
+ #
8
+ # @param activity [Federails::Activity]
6
9
  def post_to_inboxes(activity)
7
10
  actors = activity.recipients
8
11
  Rails.logger.debug('Nobody to notice') && return if actors.count.zero?