federails 0.6.2 → 0.8.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -0
  3. data/app/controllers/concerns/federails/server/render_collections.rb +19 -0
  4. data/app/controllers/federails/client/actors_controller.rb +1 -1
  5. data/app/controllers/federails/client/followings_controller.rb +4 -4
  6. data/app/controllers/federails/server/activities_controller.rb +14 -6
  7. data/app/controllers/federails/server/actors_controller.rb +16 -7
  8. data/app/controllers/federails/server/nodeinfo_controller.rb +2 -2
  9. data/app/helpers/federails/server_helper.rb +6 -0
  10. data/app/jobs/federails/application_job.rb +2 -0
  11. data/app/jobs/federails/fetch_nodeinfo_job.rb +10 -0
  12. data/app/jobs/federails/notify_inbox_job.rb +0 -2
  13. data/app/models/concerns/federails/actor_entity.rb +5 -1
  14. data/app/models/concerns/federails/data_entity.rb +80 -8
  15. data/app/models/concerns/federails/handles_delete_requests.rb +5 -0
  16. data/app/models/federails/activity.rb +15 -22
  17. data/app/models/federails/actor.rb +44 -3
  18. data/app/models/federails/following.rb +3 -3
  19. data/app/models/federails/host.rb +48 -0
  20. data/app/views/federails/client/common/_client_links.html.erb +3 -1
  21. data/app/views/federails/client/followings/_follow_actions.html.erb +5 -1
  22. data/app/views/federails/client/followings/_follower.html.erb +1 -1
  23. data/app/views/federails/server/activities/_activity.activitypub.jbuilder +7 -3
  24. data/app/views/federails/server/actors/_actor.activitypub.jbuilder +7 -6
  25. data/app/views/federails/server/actors/_tombstone.activitypub.jbuilder +1 -4
  26. data/app/views/federails/server/followings/_following.activitypub.jbuilder +1 -1
  27. data/app/views/federails/server/nodeinfo/index.nodeinfo.jbuilder +1 -1
  28. data/app/views/federails/server/nodeinfo/show.nodeinfo.jbuilder +2 -3
  29. data/app/views/federails/server/published/_publishable.activitypub.jbuilder +2 -2
  30. data/app/views/federails/server/published/_tombstone.activitypub.jbuilder +1 -4
  31. data/app/views/federails/server/shared/ordered_collection.activitypub.jbuilder +6 -0
  32. data/app/views/federails/server/shared/ordered_collection_page.activitypub.jbuilder +12 -0
  33. data/app/views/users/show.html.erb +4 -0
  34. data/db/migrate/20250426061729_create_federails_hosts.rb +22 -0
  35. data/db/migrate/20251121160720_add_to_and_cc_to_federails_activities.rb +6 -0
  36. data/lib/federails/configuration.rb +25 -0
  37. data/lib/federails/data_transformer/note.rb +6 -1
  38. data/lib/federails/maintenance/actors_updater.rb +3 -4
  39. data/lib/federails/maintenance/hosts_updater.rb +19 -0
  40. data/lib/federails/utils/actor.rb +33 -1
  41. data/lib/federails/utils/context.rb +12 -0
  42. data/lib/federails/utils/json_request.rb +41 -0
  43. data/lib/federails/utils/object.rb +1 -1
  44. data/lib/federails/utils/response_codes.rb +11 -0
  45. data/lib/federails/version.rb +1 -1
  46. data/lib/federails.rb +16 -3
  47. data/lib/fediverse/collection.rb +31 -0
  48. data/lib/fediverse/inbox.rb +10 -0
  49. data/lib/fediverse/node_info.rb +38 -0
  50. data/lib/fediverse/notifier.rb +39 -12
  51. data/lib/fediverse/request.rb +6 -28
  52. data/lib/fediverse/signature.rb +1 -1
  53. data/lib/fediverse/webfinger.rb +10 -33
  54. data/lib/fediverse.rb +2 -0
  55. data/lib/generators/federails/copy_client_policies/USAGE +8 -0
  56. data/lib/generators/federails/copy_client_policies/copy_client_policies_generator.rb +9 -0
  57. data/lib/generators/federails/copy_factories/USAGE +8 -0
  58. data/lib/generators/federails/copy_factories/copy_factories_generator.rb +43 -0
  59. data/lib/generators/federails/install/templates/federails.yml +2 -0
  60. data/lib/tasks/federails_tasks.rake +5 -0
  61. metadata +38 -6
  62. data/app/views/federails/server/activities/outbox.activitypub.jbuilder +0 -18
  63. data/app/views/federails/server/actors/followers.activitypub.jbuilder +0 -18
  64. data/app/views/federails/server/actors/following.activitypub.jbuilder +0 -18
@@ -1,7 +1,7 @@
1
1
  <div>
2
2
  <b><%= link_to following.actor.name, federails.client_actor_url(following.actor) %></b>
3
3
  (<%= following.actor.at_address %>) (<%= following.status %>)
4
- <% if following.pending? && following.target_actor == current_user.federails_actor %>
4
+ <% if following.pending? && following.target_actor == current_user&.federails_actor %>
5
5
  <%= button_to 'Accept', federails.accept_client_following_path(following), method: :put %>
6
6
  <% end %>
7
7
  </div>
@@ -1,13 +1,17 @@
1
1
  context = true unless context == false
2
2
  addressing = true unless addressing == false
3
- json.set! '@context', 'https://www.w3.org/ns/activitystreams' if context
3
+ set_json_ld_context(json) if context
4
4
 
5
5
  json.id Federails::Engine.routes.url_helpers.server_actor_activity_url activity.actor, activity
6
6
  json.type activity.action
7
7
  json.actor activity.actor.federated_url
8
8
  if addressing
9
- json.to ['https://www.w3.org/ns/activitystreams#Public']
10
- json.cc [activity.actor.followers_url]
9
+ json.merge!(
10
+ {
11
+ to: activity.to,
12
+ cc: activity.cc,
13
+ }.compact
14
+ )
11
15
  end
12
16
 
13
17
  if activity.entity.is_a? Federails::Activity
@@ -1,11 +1,12 @@
1
1
  actor_data = actor.entity&.to_activitypub_object || {}
2
2
 
3
- json.set! '@context', ([
4
- 'https://www.w3.org/ns/activitystreams',
5
- 'https://w3id.org/security/v1',
6
- ] + [
7
- actor_data&.delete(:@context),
8
- ].flatten).compact
3
+ set_json_ld_context(
4
+ json,
5
+ additional: [
6
+ 'https://w3id.org/security/v1',
7
+ actor_data.delete(:@context),
8
+ ]
9
+ )
9
10
 
10
11
  json.id actor.federated_url
11
12
  json.name actor.name
@@ -1,7 +1,4 @@
1
- json.set! '@context', [
2
- 'https://www.w3.org/ns/activitystreams',
3
- 'https://w3id.org/security/v1',
4
- ]
1
+ set_json_ld_context(json)
5
2
 
6
3
  json.id actor.federated_url
7
4
  json.type 'Tombstone'
@@ -1,5 +1,5 @@
1
1
  context = true unless context == false
2
- json.set! '@context', 'https://www.w3.org/ns/activitystreams' if context
2
+ set_json_ld_context(json) if context
3
3
 
4
4
  json.id following.federated_url
5
5
  json.type 'Follow'
@@ -1,6 +1,6 @@
1
1
  json.links [
2
2
  {
3
- rel: 'https://nodeinfo.diaspora.software/ns/schema/2.0',
3
+ rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
4
4
  href: show_node_info_url,
5
5
  },
6
6
  ]
@@ -1,6 +1,5 @@
1
1
  json.version '2.0'
2
- # FIXME: Use configuration values when created
3
- json.software name: Federails::Configuration.app_name,
2
+ json.software name: Federails::Configuration.app_name&.parameterize,
4
3
  version: Federails::Configuration.app_version
5
4
  json.protocols [
6
5
  'activitypub',
@@ -17,4 +16,4 @@ if @has_user_counts
17
16
  activeHalfyear: @active_halfyear,
18
17
  }
19
18
  end
20
- json.metadata({})
19
+ json.metadata(Federails::Configuration.nodeinfo_metadata || {})
@@ -1,5 +1,5 @@
1
1
  context = true unless context == false
2
- json.set! '@context', 'https://www.w3.org/ns/activitystreams' if context
2
+ set_json_ld_context(json) if context
3
3
 
4
4
  publishable.to_activitypub_object.each_pair do |key, value|
5
5
  json.set! key, value
@@ -7,5 +7,5 @@ end
7
7
 
8
8
  json.id publishable.federated_url
9
9
  json.actor publishable.federails_actor.federated_url
10
- json.to ['https://www.w3.org/ns/activitystreams#Public']
10
+ json.to [Fediverse::Collection::PUBLIC]
11
11
  json.cc [publishable.federails_actor.followers_url]
@@ -1,7 +1,4 @@
1
- json.set! '@context', [
2
- 'https://www.w3.org/ns/activitystreams',
3
- 'https://w3id.org/security/v1',
4
- ]
1
+ set_json_ld_context(json)
5
2
 
6
3
  json.id publishable.federated_url
7
4
  json.type 'Tombstone'
@@ -0,0 +1,6 @@
1
+ set_json_ld_context(json)
2
+ json.id send(url_helper, actor)
3
+ json.type 'OrderedCollection'
4
+ json.totalItems collection.total_count
5
+ json.first send(url_helper, actor, page: 1)
6
+ json.last send(url_helper, actor, page: collection.total_pages)
@@ -0,0 +1,12 @@
1
+ set_json_ld_context(json)
2
+ json.type 'OrderedCollectionPage'
3
+ json.id send(url_helper, actor, page: collection.current_page)
4
+ json.partOf send(url_helper, actor)
5
+ json.first send(url_helper, actor, page: 1)
6
+ json.last send(url_helper, actor, page: collection.total_pages)
7
+ json.next send(url_helper, actor, page: collection.next_page) if collection.next_page
8
+ json.prev send(url_helper, actor, page: collection.prev_page) if collection.prev_page
9
+ json.totalItems collection.total_count
10
+ json.orderedItems do
11
+ items_block.call(json, collection)
12
+ end
@@ -0,0 +1,4 @@
1
+ This page renders a users profile; its route (<code>user_url</code>) is used as <code>:profile_url_method</code>
2
+ in Federails integration (<code>acts_as_actor_entity</code> in <code>User</code> model).
3
+
4
+ <pre><%= JSON.pretty_generate @user.as_json %></pre>
@@ -0,0 +1,22 @@
1
+ class CreateFederailsHosts < ActiveRecord::Migration[7.2]
2
+ def change
3
+ create_table :federails_hosts do |t|
4
+ t.string :domain, null: false, default: nil
5
+ t.string :nodeinfo_url
6
+ t.string :software_name
7
+ t.string :software_version
8
+
9
+ # Uncomment the lines below if you use PostgreSQL
10
+ # t.jsonb :protocols, default: []
11
+ # t.jsonb :services, default: {}
12
+ #
13
+ # Other databases
14
+ t.text :protocols, default: '[]'
15
+ t.text :services, default: '{}'
16
+
17
+ t.timestamps
18
+
19
+ t.index :domain, unique: true
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,6 @@
1
+ class AddToAndCcToFederailsActivities < ActiveRecord::Migration[7.2]
2
+ def change
3
+ add_column :federails_activities, :to, :string
4
+ add_column :federails_activities, :cc, :string
5
+ end
6
+ end
@@ -35,6 +35,14 @@ module Federails
35
35
  @@open_registrations.is_a?(Proc) ? @@open_registrations.call : @@open_registrations
36
36
  end
37
37
 
38
+ # Custom metadata for nodeinfo
39
+ # Can either be a static hash, or a Proc which will be called to get the state.
40
+ mattr_writer :nodeinfo_metadata
41
+ @@nodeinfo_metadata = {}
42
+ def self.nodeinfo_metadata
43
+ @@nodeinfo_metadata.is_a?(Proc) ? @@nodeinfo_metadata.call : @@nodeinfo_metadata
44
+ end
45
+
38
46
  # Application layout
39
47
  mattr_accessor :app_layout
40
48
  @@app_layout = nil
@@ -58,6 +66,13 @@ module Federails
58
66
  # @!method self.remote_follow_url_method=(value)
59
67
  #
60
68
  # Sets the route method for remote-following requests
69
+ #
70
+ # The route should lead to a page displaying the remote actor and a button to follow it.
71
+ # Remote actor is specified in the `uri` query parameter.
72
+ #
73
+ # Its value defaults to a route of the Federails client, so your application will break
74
+ # if you don't use the client routes and don't override this value.
75
+ #
61
76
  # @param value [String] Route method name as used in links
62
77
  # @example
63
78
  # remote_follow_url_method 'main_app.my_custom_route_helper'
@@ -74,6 +89,16 @@ module Federails
74
89
  Federails::Engine.routes.default_url_options[:port] = value
75
90
  end
76
91
 
92
+ # Default amount of seconds to consider that a remote entity could be updated
93
+ #
94
+ # This setting is used for hosts information only, for now.
95
+ mattr_accessor :remote_entities_cache_duration
96
+ @@remote_entities_cache_duration = 1.day
97
+
98
+ # Job queue name
99
+ mattr_accessor :job_queue
100
+ @@job_queue = :default
101
+
77
102
  # List of actor types (classes using Federails::ActorEntity)
78
103
  mattr_reader :actor_types
79
104
  @@actor_types = {}
@@ -1,3 +1,5 @@
1
+ require 'federails/utils/context'
2
+
1
3
  module Federails
2
4
  module DataTransformer
3
5
  module Note
@@ -17,7 +19,10 @@ module Federails
17
19
  # - https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object
18
20
  # - https://www.w3.org/TR/activitystreams-vocabulary/#dfn-note
19
21
  def self.to_federation(entity, content:, name: nil, custom: {})
20
- custom.merge '@context' => 'https://www.w3.org/ns/activitystreams',
22
+ # Merge default and custom contexts
23
+ context = Utils::Context.generate(additional: custom.delete('@context'))
24
+ # Merge in standard Note fields
25
+ custom.merge '@context' => context,
21
26
  'id' => entity.federated_url,
22
27
  'type' => 'Note',
23
28
  'name' => name,
@@ -54,12 +54,11 @@ module Federails
54
54
  def update(actor)
55
55
  return :ignored_local if actor.local?
56
56
 
57
- response = Fediverse::Webfinger.fetch_actor_url(actor.federated_url)
58
- new_attributes = response.attributes.except 'id', 'uuid', 'created_at', 'updated_at', 'local', 'entity_id', 'entity_type'
59
-
60
- actor.update(new_attributes) ? :updated : :failed
57
+ actor.sync! ? :updated : :failed
61
58
  rescue ActiveRecord::RecordNotFound
62
59
  :not_found
60
+ rescue StandardError
61
+ :failed
63
62
  end
64
63
  end
65
64
  end
@@ -0,0 +1,19 @@
1
+ module Federails
2
+ module Maintenance
3
+ class HostsUpdater
4
+ class << self
5
+ # Update information for all known hosts, and complete if some are missing
6
+ def run(cache_interval: nil)
7
+ cache_interval ||= Federails::Configuration.remote_entities_cache_duration
8
+
9
+ domains = Federails::Actor.distant.distinct(:server).pluck(:server) + Federails::Host.pluck(:domain)
10
+ domains.uniq!
11
+
12
+ domains.each do |domain|
13
+ Federails::Host.create_or_update domain, min_update_interval: cache_interval
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -27,13 +27,21 @@ module Federails
27
27
  actor
28
28
  end
29
29
 
30
+ def untombstone!(actor)
31
+ if actor.local?
32
+ untombstone_local_actor actor
33
+ else
34
+ untombstone_distant_actor actor
35
+ end
36
+ end
37
+
30
38
  private
31
39
 
32
40
  def tombstone_local_actor(actor)
33
41
  Federails::Actor.transaction do
34
42
  hash = {
35
43
  tombstoned_at: Time.current,
36
- entity: nil,
44
+ entity: actor.entity || nil,
37
45
  }
38
46
  # Hardcode attributes depending on the actor's entity
39
47
  COMPUTED_ATTRIBUTES.each { |attribute| hash[attribute] = actor.send(attribute) }
@@ -44,9 +52,33 @@ module Federails
44
52
  end
45
53
  end
46
54
 
55
+ def untombstone_local_actor(actor)
56
+ return unless actor.tombstoned?
57
+ raise 'Cannot restore a local actor without an entity' if actor.entity.blank?
58
+
59
+ Federails::Actor.transaction do
60
+ # Reset hardcoded attributes depending on the actor's entity
61
+ hash = { tombstoned_at: nil }
62
+ COMPUTED_ATTRIBUTES.each { |attribute| hash[attribute] = nil }
63
+
64
+ actor.update! hash
65
+
66
+ delete_activity = Activity.find_by actor: actor, action: 'Delete', entity: actor
67
+ return unless delete_activity
68
+
69
+ Activity.create! actor: actor, action: 'Undo', entity: delete_activity
70
+ end
71
+ end
72
+
47
73
  def tombstone_distant_actor(actor)
48
74
  actor.update! tombstoned_at: Time.current
49
75
  end
76
+
77
+ def untombstone_distant_actor(actor)
78
+ actor.tombstoned_at = nil
79
+ actor.sync!
80
+ actor.save
81
+ end
50
82
  end
51
83
  end
52
84
  end
@@ -0,0 +1,12 @@
1
+ module Federails
2
+ module Utils
3
+ module Context
4
+ class << self
5
+ def generate(additional: nil)
6
+ activity_streams = 'https://www.w3.org/ns/activitystreams'
7
+ additional.nil? ? activity_streams : [activity_streams, additional].flatten.compact
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,41 @@
1
+ require 'faraday'
2
+ require 'faraday/follow_redirects'
3
+
4
+ module Federails
5
+ module Utils
6
+ # Simple wrapper to make requests expecting JSON
7
+ module JsonRequest
8
+ class UnhandledResponseStatus < StandardError; end
9
+
10
+ BASE_HEADERS = {
11
+ 'Content-Type' => 'application/json',
12
+ 'Accept' => 'application/json',
13
+ }.freeze
14
+
15
+ # Makes a GET request and returns a +Hash+ from the parsed body
16
+ #
17
+ # @param url [String] Target URL
18
+ # @param params [Hash] Querystring parameters
19
+ # @param headers [Hash] Additional headers
20
+ # @param follow_redirects [Boolean] Whether to follow redirections
21
+ # @param expected_status [Integer] Expected response status. Will raise a +UnhandledResponseStatus+ when status is different
22
+ #
23
+ # @return The parsed JSON object
24
+ #
25
+ # @raise [UnhandledResponseStatus] when response status is not the expected_status
26
+ def self.get_json(url, params: {}, headers: {}, follow_redirects: false, expected_status: 200)
27
+ headers = BASE_HEADERS.merge headers
28
+
29
+ connection = Faraday.new url: url, params: params, headers: headers do |faraday|
30
+ faraday.response :follow_redirects if follow_redirects
31
+ faraday.adapter Faraday.default_adapter
32
+ end
33
+
34
+ response = connection.get
35
+ raise UnhandledResponseStatus, "Unhandled status code #{response.status} for GET #{url}" if expected_status && response.status != expected_status
36
+
37
+ JSON.parse(response.body)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -121,7 +121,7 @@ module Federails
121
121
  entity = handler[:class].new_from_activitypub_object(hash)
122
122
  return unless entity
123
123
 
124
- entity.federails_actor = Federails::Actor.find_or_create_by_object hash['attributedTo'] if entity && !entity.federails_actor
124
+ entity.federails_actor = Federails::Actor.find_by_federation_url hash['attributedTo'] if entity && !entity.federails_actor
125
125
 
126
126
  entity
127
127
  end
@@ -0,0 +1,11 @@
1
+ module Federails
2
+ module Utils
3
+ module ResponseCodes
4
+ UNPROCESSABLE_CONTENT = if Gem::Version.new(Rack::RELEASE) < Gem::Version.new('3.1')
5
+ :unprocessable_entity
6
+ else
7
+ :unprocessable_content
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,3 +1,3 @@
1
1
  module Federails
2
- VERSION = '0.6.2'.freeze
2
+ VERSION = '0.8.0'.freeze
3
3
  end
data/lib/federails.rb CHANGED
@@ -1,7 +1,15 @@
1
+ require 'jbuilder'
2
+ require 'kaminari'
3
+ require 'pundit'
4
+
1
5
  require 'federails/version'
2
6
  require 'federails/engine'
3
7
  require 'federails/configuration'
4
8
  require 'federails/utils/object'
9
+ require 'federails/utils/json_request'
10
+ require 'federails/utils/response_codes'
11
+
12
+ require 'fediverse'
5
13
 
6
14
  # rubocop:disable Style/ClassVars
7
15
 
@@ -30,11 +38,14 @@ module Federails
30
38
  :site_port,
31
39
  :enable_discovery,
32
40
  :open_registrations,
41
+ :nodeinfo_metadata,
33
42
  :app_layout,
34
43
  :server_routes_path,
35
44
  :client_routes_path,
36
45
  :remote_follow_url_method,
37
46
  :base_client_controller,
47
+ :remote_entities_cache_duration,
48
+ :job_queue,
38
49
  ].each { |key| Configuration.send :"#{key}=", config[key] if config.key?(key) }
39
50
  end
40
51
 
@@ -77,9 +88,11 @@ module Federails
77
88
  # @return [Hash, nil] Data entity configuration
78
89
  def data_entity_handler_for(hash)
79
90
  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)
91
+ if handler[:filter_method] || handler[:class].respond_to?(DEFAULT_DATA_FILTER_METHOD)
92
+ handler[:class].send(handler[:filter_method] || DEFAULT_DATA_FILTER_METHOD, hash)
93
+ else
94
+ true
95
+ end
83
96
  end
84
97
  end
85
98
 
@@ -0,0 +1,31 @@
1
+ module Fediverse
2
+ class Collection < Array
3
+ PUBLIC = 'https://www.w3.org/ns/activitystreams#Public'.freeze
4
+
5
+ attr_reader :total_items, :id, :type
6
+
7
+ def self.fetch(url)
8
+ new.fetch(url)
9
+ end
10
+
11
+ def fetch(url)
12
+ json = Fediverse::Request.dereference(url)
13
+ @total_items = json['totalItems']
14
+ @id = json['id']
15
+ @type = json['type']
16
+ raise Errors::NotACollection unless %w[OrderedCollection Collection].include?(@type)
17
+
18
+ next_url = json['first']
19
+ while next_url
20
+ page = Fediverse::Request.dereference(next_url)
21
+ concat(page['orderedItems'] || page['items'])
22
+ next_url = page['next']
23
+ end
24
+ self
25
+ end
26
+ end
27
+
28
+ module Errors
29
+ class NotACollection < StandardError; end
30
+ end
31
+ end
@@ -90,11 +90,21 @@ module Fediverse
90
90
 
91
91
  object.run_callbacks :on_federails_delete_requested
92
92
  end
93
+
94
+ def handle_undelete_request(activity)
95
+ # Get to original object
96
+ delete_activity = Request.dereference(activity['object'])
97
+ object = Federails::Utils::Object.find_distant_object_in_all(delete_activity['object'])
98
+ return if object.blank?
99
+
100
+ object.run_callbacks :on_federails_undelete_requested
101
+ end
93
102
  end
94
103
 
95
104
  register_handler 'Follow', '*', self, :handle_create_follow_request
96
105
  register_handler 'Accept', 'Follow', self, :handle_accept_follow_request
97
106
  register_handler 'Undo', 'Follow', self, :handle_undo_follow_request
98
107
  register_handler 'Delete', '*', self, :handle_delete_request
108
+ register_handler 'Undo', 'Delete', self, :handle_undelete_request
99
109
  end
100
110
  end
@@ -0,0 +1,38 @@
1
+ module Fediverse
2
+ class NodeInfo
3
+ class NotFoundError < StandardError; end
4
+ class NoActivityPubError < StandardError; end
5
+
6
+ class << self
7
+ def fetch(domain)
8
+ url = nodeinfo_url(domain)
9
+
10
+ hash = Federails::Utils::JsonRequest.get_json url
11
+ raise NoActivityPubError, "#{domain} does not handle activitypub protocol" unless hash['protocols'].include? 'activitypub'
12
+
13
+ {
14
+ domain: domain,
15
+ nodeinfo_url: url,
16
+ software_version: hash.dig('software', 'version'),
17
+ software_name: hash.dig('software', 'name'),
18
+ protocols: hash['protocols'],
19
+ services: hash['services'],
20
+ }
21
+ end
22
+
23
+ private
24
+
25
+ def base_url(domain)
26
+ scheme = Federails::Configuration.force_ssl ? 'https' : 'http'
27
+ @base_url = "#{scheme}://#{domain}"
28
+ end
29
+
30
+ def nodeinfo_url(domain)
31
+ response = Federails::Utils::JsonRequest.get_json "#{base_url(domain)}/.well-known/nodeinfo", follow_redirects: true
32
+ entry = response['links']&.find { |link| link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/2.0' }
33
+
34
+ entry['href']
35
+ end
36
+ end
37
+ end
38
+ end
@@ -7,18 +7,45 @@ module Fediverse
7
7
  #
8
8
  # @param activity [Federails::Activity]
9
9
  def post_to_inboxes(activity)
10
- actors = activity.recipients
11
- Rails.logger.debug('Nobody to notice') && return if actors.count.zero?
10
+ # Get the list of actors we need to send the activity to
11
+ inboxes = inboxes_for(activity)
12
+ Rails.logger.debug('Nobody to notice') && return if inboxes.none?
12
13
 
14
+ # Deliver to each inbox
13
15
  message = payload(activity)
14
- actors.each do |recipient|
15
- Rails.logger.debug { "Sending activity ##{activity.id} to #{recipient.inbox_url}" }
16
- post_to_inbox(to: recipient, message: message, from: activity.actor)
16
+ inboxes.each do |url|
17
+ Rails.logger.debug { "Sending activity ##{activity.id} to inbox at #{url}" }
18
+ post_to_inbox(inbox_url: url, message: message, from: activity.actor)
17
19
  end
18
20
  end
19
21
 
20
22
  private
21
23
 
24
+ # Determines the list of inboxes that the activity should be delivered to
25
+ #
26
+ # @return [Array<Federails::Actor>]
27
+ def inboxes_for(activity)
28
+ return [] unless activity.actor.local?
29
+
30
+ [activity.to, activity.cc].flatten.compact.reject { |x| x == Fediverse::Collection::PUBLIC }.map do |url|
31
+ actor = Federails::Actor.find_or_create_by_federation_url(url)
32
+ [actor.inbox_url]
33
+ rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid
34
+ collection_to_actors(url).map(&:inbox_url)
35
+ end.flatten.compact
36
+ end
37
+
38
+ def collection_to_actors(url)
39
+ collection = Collection.fetch(url)
40
+ collection.filter_map do |actor_url|
41
+ Federails::Actor.find_or_create_by_federation_url(actor_url)
42
+ rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid
43
+ nil
44
+ end
45
+ rescue Errors::NotACollection
46
+ []
47
+ end
48
+
22
49
  def payload(activity)
23
50
  Federails::ServerController.renderer.new.render(
24
51
  template: 'federails/server/activities/show',
@@ -27,27 +54,27 @@ module Fediverse
27
54
  )
28
55
  end
29
56
 
30
- def post_to_inbox(to:, message:, from: nil)
57
+ def post_to_inbox(inbox_url:, message:, from: nil)
31
58
  conn = Faraday.default_connection
32
59
  conn.builder.build_response(
33
60
  conn,
34
- signed_request(to: to, message: message, from: from)
61
+ signed_request(url: inbox_url, message: message, from: from)
35
62
  )
36
63
  end
37
64
 
38
- def signed_request(to:, message:, from:)
39
- req = request(to: to, message: message)
65
+ def signed_request(url:, message:, from:)
66
+ req = request(url: url, message: message)
40
67
  req.headers['Signature'] = Fediverse::Signature.sign(sender: from, request: req) if from
41
68
  req
42
69
  end
43
70
 
44
- def request(to:, message:) # rubocop:todo Metrics/AbcSize
71
+ def request(url:, message:) # rubocop:todo Metrics/AbcSize
45
72
  Faraday.default_connection.build_request(:post) do |req|
46
- req.url to.inbox_url
73
+ req.url url
47
74
  req.body = message
48
75
  req.headers['Content-Type'] = Mime[:activitypub].to_s
49
76
  req.headers['Accept'] = Mime[:activitypub].to_s
50
- req.headers['Host'] = URI.parse(to.inbox_url).host
77
+ req.headers['Host'] = URI.parse(url).host
51
78
  req.headers['Date'] = Time.now.utc.httpdate
52
79
  req.headers['Digest'] = digest(message)
53
80
  end