fedipub 0.0.1 → 0.10.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 (135) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +76 -2
  3. data/Rakefile +5 -9
  4. data/app/assets/config/fedipub_manifest.js +1 -0
  5. data/app/assets/stylesheets/fedipub/application.css +15 -0
  6. data/app/controllers/concerns/fedipub/server/render_collections.rb +19 -0
  7. data/app/controllers/fedipub/client/activities_controller.rb +27 -0
  8. data/app/controllers/fedipub/client/actors_controller.rb +54 -0
  9. data/app/controllers/fedipub/client/followings_controller.rb +103 -0
  10. data/app/controllers/fedipub/client_controller.rb +9 -0
  11. data/app/controllers/fedipub/server/activities_controller.rb +78 -0
  12. data/app/controllers/fedipub/server/actors_controller.rb +50 -0
  13. data/app/controllers/fedipub/server/followings_controller.rb +19 -0
  14. data/app/controllers/fedipub/server/nodeinfo_controller.rb +28 -0
  15. data/app/controllers/fedipub/server/published_controller.rb +30 -0
  16. data/app/controllers/fedipub/server/web_finger_controller.rb +44 -0
  17. data/app/controllers/fedipub/server_controller.rb +48 -0
  18. data/app/helpers/fedipub/server_helper.rb +18 -0
  19. data/app/jobs/fedipub/application_job.rb +10 -0
  20. data/app/jobs/fedipub/fetch_nodeinfo_job.rb +10 -0
  21. data/app/jobs/fedipub/notify_inbox_job.rb +10 -0
  22. data/app/mailers/fedipub/application_mailer.rb +6 -0
  23. data/app/models/concerns/fedipub/actor_entity.rb +165 -0
  24. data/app/models/concerns/fedipub/announceable.rb +18 -0
  25. data/app/models/concerns/fedipub/application_actor.rb +25 -0
  26. data/app/models/concerns/fedipub/data_entity.rb +278 -0
  27. data/app/models/concerns/fedipub/handles_delete_requests.rb +36 -0
  28. data/app/models/concerns/fedipub/has_uuid.rb +61 -0
  29. data/app/models/concerns/fedipub/likeable.rb +29 -0
  30. data/app/models/fedipub/activity.rb +62 -0
  31. data/app/models/fedipub/actor.rb +306 -0
  32. data/app/models/fedipub/application_record.rb +5 -0
  33. data/app/models/fedipub/following.rb +72 -0
  34. data/app/models/fedipub/host.rb +48 -0
  35. data/app/policies/fedipub/client/activity_policy.rb +9 -0
  36. data/app/policies/fedipub/client/actor_policy.rb +15 -0
  37. data/app/policies/fedipub/client/following_policy.rb +35 -0
  38. data/app/policies/fedipub/fedipub_policy.rb +63 -0
  39. data/app/policies/fedipub/server/activity_policy.rb +9 -0
  40. data/app/policies/fedipub/server/actor_policy.rb +23 -0
  41. data/app/policies/fedipub/server/following_policy.rb +6 -0
  42. data/app/policies/fedipub/server/publishable_policy.rb +15 -0
  43. data/app/views/fedipub/client/activities/_activity.html.erb +5 -0
  44. data/app/views/fedipub/client/activities/_activity.json.jbuilder +1 -0
  45. data/app/views/fedipub/client/activities/_index.json.jbuilder +1 -0
  46. data/app/views/fedipub/client/activities/feed.html.erb +11 -0
  47. data/app/views/fedipub/client/activities/feed.json.jbuilder +1 -0
  48. data/app/views/fedipub/client/activities/index.html.erb +5 -0
  49. data/app/views/fedipub/client/activities/index.json.jbuilder +1 -0
  50. data/app/views/fedipub/client/actors/_actor.json.jbuilder +17 -0
  51. data/app/views/fedipub/client/actors/_lookup_form.html.erb +5 -0
  52. data/app/views/fedipub/client/actors/gone.html.erb +1 -0
  53. data/app/views/fedipub/client/actors/index.html.erb +44 -0
  54. data/app/views/fedipub/client/actors/index.json.jbuilder +1 -0
  55. data/app/views/fedipub/client/actors/show.html.erb +112 -0
  56. data/app/views/fedipub/client/actors/show.json.jbuilder +1 -0
  57. data/app/views/fedipub/client/common/_client_links.html.erb +26 -0
  58. data/app/views/fedipub/client/followings/_follow.html.erb +4 -0
  59. data/app/views/fedipub/client/followings/_follow_actions.html.erb +36 -0
  60. data/app/views/fedipub/client/followings/_follower.html.erb +7 -0
  61. data/app/views/fedipub/client/followings/_following.json.jbuilder +1 -0
  62. data/app/views/fedipub/client/followings/_form.html.erb +22 -0
  63. data/app/views/fedipub/client/followings/index.html.erb +29 -0
  64. data/app/views/fedipub/client/followings/index.json.jbuilder +1 -0
  65. data/app/views/fedipub/client/followings/show.html.erb +21 -0
  66. data/app/views/fedipub/client/followings/show.json.jbuilder +1 -0
  67. data/app/views/fedipub/server/activities/_activity.activitypub.jbuilder +25 -0
  68. data/app/views/fedipub/server/activities/show.activitypub.jbuilder +1 -0
  69. data/app/views/fedipub/server/actors/_actor.activitypub.jbuilder +41 -0
  70. data/app/views/fedipub/server/actors/_tombstone.activitypub.jbuilder +6 -0
  71. data/app/views/fedipub/server/actors/show.activitypub.jbuilder +5 -0
  72. data/app/views/fedipub/server/followings/_following.activitypub.jbuilder +7 -0
  73. data/app/views/fedipub/server/followings/show.activitypub.jbuilder +1 -0
  74. data/app/views/fedipub/server/nodeinfo/index.nodeinfo.jbuilder +10 -0
  75. data/app/views/fedipub/server/nodeinfo/show.nodeinfo.jbuilder +19 -0
  76. data/app/views/fedipub/server/published/_publishable.activitypub.jbuilder +11 -0
  77. data/app/views/fedipub/server/published/_tombstone.activitypub.jbuilder +6 -0
  78. data/app/views/fedipub/server/published/show.activitypub.jbuilder +5 -0
  79. data/app/views/fedipub/server/shared/ordered_collection.activitypub.jbuilder +6 -0
  80. data/app/views/fedipub/server/shared/ordered_collection_page.activitypub.jbuilder +12 -0
  81. data/app/views/fedipub/server/web_finger/find.jrd.jbuilder +32 -0
  82. data/app/views/fedipub/server/web_finger/host_meta.xrd.erb +5 -0
  83. data/app/views/users/show.html.erb +4 -0
  84. data/config/initializers/mime_types.rb +21 -0
  85. data/config/initializers/security_vocab.rb +53 -0
  86. data/config/routes.rb +53 -0
  87. data/db/migrate/20200712133150_create_federails_actors.rb +22 -0
  88. data/db/migrate/20200712143127_create_federails_followings.rb +14 -0
  89. data/db/migrate/20200712174938_create_federails_activities.rb +11 -0
  90. data/db/migrate/20241002094500_add_uuids.rb +13 -0
  91. data/db/migrate/20241002094501_add_keypair_to_actors.rb +8 -0
  92. data/db/migrate/20250122160618_add_extensions_to_federails_actors.rb +5 -0
  93. data/db/migrate/20250301082500_add_local_to_actors.rb +11 -0
  94. data/db/migrate/20250329123939_add_actor_type_to_actors.rb +5 -0
  95. data/db/migrate/20250329123940_add_tombstoned_at_to_actors.rb +5 -0
  96. data/db/migrate/20250426061729_create_federails_hosts.rb +22 -0
  97. data/db/migrate/20251121160720_add_to_and_cc_to_federails_activities.rb +6 -0
  98. data/db/migrate/20260421170640_add_result_and_instrument_to_federails_activities.rb +6 -0
  99. data/db/migrate/20260723113716_rename_federails_to_fedipub.rb +15 -0
  100. data/lib/fedipub/configuration.rb +119 -0
  101. data/lib/fedipub/data_transformer/note.rb +36 -0
  102. data/lib/fedipub/engine.rb +11 -0
  103. data/lib/fedipub/maintenance/actors_updater.rb +66 -0
  104. data/lib/fedipub/maintenance/hosts_updater.rb +19 -0
  105. data/lib/fedipub/utils/actor.rb +85 -0
  106. data/lib/fedipub/utils/context.rb +12 -0
  107. data/lib/fedipub/utils/host.rb +54 -0
  108. data/lib/fedipub/utils/json_request.rb +89 -0
  109. data/lib/fedipub/utils/object.rb +131 -0
  110. data/lib/fedipub/utils/response_codes.rb +11 -0
  111. data/lib/fedipub/version.rb +1 -3
  112. data/lib/fedipub.rb +127 -4
  113. data/lib/fediverse/collection.rb +31 -0
  114. data/lib/fediverse/inbox.rb +110 -0
  115. data/lib/fediverse/node_info.rb +38 -0
  116. data/lib/fediverse/notifier.rb +63 -0
  117. data/lib/fediverse/request.rb +29 -0
  118. data/lib/fediverse/signature/draft_cavage12.rb +109 -0
  119. data/lib/fediverse/signature/rfc9421.rb +69 -0
  120. data/lib/fediverse/signature.rb +26 -0
  121. data/lib/fediverse/webfinger.rb +140 -0
  122. data/lib/fediverse.rb +5 -0
  123. data/lib/generators/fedipub/copy_client_policies/USAGE +8 -0
  124. data/lib/generators/fedipub/copy_client_policies/copy_client_policies_generator.rb +9 -0
  125. data/lib/generators/fedipub/copy_client_views/USAGE +8 -0
  126. data/lib/generators/fedipub/copy_client_views/copy_client_views_generator.rb +9 -0
  127. data/lib/generators/fedipub/copy_factories/USAGE +8 -0
  128. data/lib/generators/fedipub/copy_factories/copy_factories_generator.rb +43 -0
  129. data/lib/generators/fedipub/install/USAGE +9 -0
  130. data/lib/generators/fedipub/install/install_generator.rb +10 -0
  131. data/lib/generators/fedipub/install/templates/fedipub.rb +1 -0
  132. data/lib/generators/fedipub/install/templates/fedipub.yml +27 -0
  133. data/lib/tasks/factory_bot.rake +15 -0
  134. data/lib/tasks/fedipub_tasks.rake +13 -0
  135. metadata +276 -10
@@ -0,0 +1,54 @@
1
+ module Fedipub
2
+ module Utils
3
+ class Host
4
+ class << self
5
+ COMMON_PORTS = [80, 443].freeze
6
+
7
+ ##
8
+ # @return [String] Host and port of the current instance
9
+ def localhost
10
+ uri = URI.parse Fedipub.configuration.site_host
11
+ host_and_port (uri.host || 'localhost'), Fedipub.configuration.site_port
12
+ end
13
+
14
+ ##
15
+ # Checks if the given URL points somewhere on current instance
16
+ #
17
+ # @param url [String] URL to check
18
+ #
19
+ # @return [true, false]
20
+ def local_url?(url)
21
+ uri = URI.parse url
22
+ host = host_and_port uri.host, uri.port
23
+ localhost == host
24
+ end
25
+
26
+ ##
27
+ # Gets the route on the current instance, or nil
28
+ #
29
+ # @param url [String] URL to check
30
+ #
31
+ # @return [ActionDispatch::Routing::RouteSet, nil] nil when URL do not match a route
32
+ def local_route(url)
33
+ return nil unless local_url? url
34
+
35
+ Rails.application.routes.recognize_path(url)
36
+ rescue ActionController::RoutingError
37
+ nil
38
+ end
39
+
40
+ private
41
+
42
+ def host_and_port(host, port)
43
+ port_string = if port.present? && COMMON_PORTS.exclude?(port)
44
+ ":#{port}"
45
+ else
46
+ ''
47
+ end
48
+
49
+ "#{host}#{port_string}"
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,89 @@
1
+ require 'faraday'
2
+ require 'faraday/follow_redirects'
3
+ require 'fediverse/signature'
4
+
5
+ module Fedipub
6
+ module Utils
7
+ # Wrapper around HTTP calls which ensures signatures etc are applied.
8
+ class JsonRequest
9
+ include Singleton
10
+
11
+ class << self
12
+ extend Forwardable
13
+
14
+ def_delegators :instance, :get_json, :get, :post
15
+ end
16
+
17
+ class UnhandledResponseStatus < StandardError; end
18
+
19
+ # Makes a GET request and returns a +Hash+ from the parsed body
20
+ #
21
+ # @param url [String] Target URL
22
+ # @param params [Hash] Querystring parameters
23
+ # @param headers [Hash] Additional headers
24
+ # @param expected_status [Integer] Expected response status. Will raise a +UnhandledResponseStatus+ when status is different
25
+ #
26
+ # @return The parsed JSON object
27
+ #
28
+ # @raise [UnhandledResponseStatus] when response status is not the expected_status
29
+ def get_json(url, params: {}, headers: {}, expected_status: 200, from: nil)
30
+ response = get url: url, params: params, headers: headers, from: from
31
+ raise UnhandledResponseStatus, "Unhandled status code #{response.status} for GET #{url}" if expected_status && response.status != expected_status
32
+
33
+ JSON.parse(response.body)
34
+ end
35
+
36
+ def get(url:, params: {}, headers: {}, from: nil)
37
+ execute_request method: :get, url: url, params: params, headers: headers, from: from || Fedipub::Actor.application_actor
38
+ end
39
+
40
+ def post(url:, message:, headers: {}, from: nil)
41
+ execute_request method: :post, url: url, headers: headers, message: message, from: from
42
+ end
43
+
44
+ private
45
+
46
+ # Send to remote server with RFC9421 signature and double-knocking for draft-cavage-12 if that fails
47
+ def execute_request(method:, url:, params: {}, headers: {}, message: nil, from: nil) # rubocop:disable Metrics/ParameterLists
48
+ req = build_request(method: method, url: url, params: params, headers: headers, message: message)
49
+ response = connection.builder.build_response(
50
+ connection,
51
+ from ? Fediverse::Signature.sign(sender: from, request: req.dup) : req
52
+ )
53
+ # If signature was present and rejected, try double-knocking
54
+ return response unless from && response.status.in?([400, 401])
55
+
56
+ connection.builder.build_response(
57
+ connection,
58
+ Fediverse::Signature.sign(sender: from, request: req, legacy_signature: true)
59
+ )
60
+ end
61
+
62
+ def build_request(method:, url:, params: {}, headers: {}, message: nil) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
63
+ # Extract params from URL string if they're in there instead of the hash
64
+ uri = URI(url)
65
+ params.merge! Rack::Utils.parse_nested_query(uri.query)
66
+ uri.query = nil
67
+ # Build the request
68
+ connection.build_request(method) do |req|
69
+ req.url uri
70
+ req.body = message
71
+ req.params = params
72
+ req.headers = {
73
+ 'Content-Type' => Mime[:activitypub].to_s,
74
+ 'Accept' => [Mime[:activitypub].to_s, Mime[:activitypub].send(:synonyms), "#{Mime[:json]};q=0.5"].flatten.join(', '),
75
+ 'User-Agent' => req.headers['User-Agent'] || "Fedipub/#{Fedipub::VERSION}",
76
+ 'Accept-Signature' => 'sig1=()',
77
+ }.compact.merge(headers)
78
+ end
79
+ end
80
+
81
+ def connection
82
+ @connection ||= Faraday.new do |faraday|
83
+ faraday.response :follow_redirects
84
+ faraday.adapter Faraday.default_adapter
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,131 @@
1
+ module Fedipub
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 [Fedipub::Actor, Fedipub::DataEntity, Fedipub::Following, nil]
29
+ def find_distant_object_in_all(federated_url)
30
+ # Search in actors
31
+ object = Fedipub::Actor.find_by federated_url: federated_url
32
+ return object if object.present?
33
+
34
+ # Search in followings
35
+ object = Fedipub::Following.find_by federated_url: federated_url
36
+ return object if object.present?
37
+
38
+ # Search in data entities
39
+ Fedipub.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] == 'fedipub/server/published' && route[:action] == 'show'
98
+
99
+ route
100
+ end
101
+
102
+ def from_local_route(route)
103
+ config = Fedipub.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 = Fedipub.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.fedipub_actor = Fedipub::Actor.find_by_federation_url hash['attributedTo'] if entity && !entity.fedipub_actor
125
+
126
+ entity
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,11 @@
1
+ module Fedipub
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,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Fedipub
4
- VERSION = "0.0.1"
2
+ VERSION = '0.10.0'.freeze
5
3
  end
data/lib/fedipub.rb CHANGED
@@ -1,8 +1,131 @@
1
- # frozen_string_literal: true
1
+ require 'jbuilder'
2
+ require 'kaminari'
3
+ require 'pundit'
2
4
 
3
- require_relative "fedipub/version"
5
+ require 'fedipub/version'
6
+ require 'fedipub/engine'
7
+ require 'fedipub/configuration'
8
+ require 'fedipub/utils/object'
9
+ require 'fedipub/utils/json_request'
10
+ require 'fedipub/utils/response_codes'
4
11
 
12
+ require 'fediverse'
13
+
14
+ # rubocop:disable Style/ClassVars
15
+
16
+ # This module includes classes and methods related to Ruby on Rails: engine configuration, models, controllers, etc.
5
17
  module Fedipub
6
- class Error < StandardError; end
7
- # Your code goes here...
18
+ DEFAULT_DATA_FILTER_METHOD = :handle_federated_object?
19
+
20
+ mattr_reader :configuration
21
+ @@configuration = Configuration
22
+
23
+ class << self
24
+ def configure
25
+ yield @@configuration
26
+ end
27
+
28
+ def config_from(name) # rubocop:disable Metrics/MethodLength
29
+ config = Rails.application.config_for name
30
+ [
31
+ :app_name,
32
+ :app_version,
33
+ :force_ssl,
34
+ :site_host,
35
+ :site_port,
36
+ :enable_discovery,
37
+ :open_registrations,
38
+ :nodeinfo_metadata,
39
+ :app_layout,
40
+ :server_routes_path,
41
+ :client_routes_path,
42
+ :remote_follow_url_method,
43
+ :base_client_controller,
44
+ :remote_entities_cache_duration,
45
+ :job_queue,
46
+ ].each { |key| Configuration.send :"#{key}=", config[key] if config.key?(key) }
47
+ end
48
+
49
+ # @return [Boolean] True if the given model is a possible actor
50
+ #
51
+ # @example
52
+ # puts "Follow #{some_actor.name}" if actor_entity? current_user
53
+ def actor_entity?(class_or_instance)
54
+ Configuration.actor_types.key? class_or_instance_name(class_or_instance)
55
+ end
56
+
57
+ # @return [Hash] The configuration for the given actor entity
58
+ def actor_entity(class_or_instance)
59
+ klass = class_or_instance_name(class_or_instance)
60
+ raise "#{klass} is not a configured actor entity" unless Configuration.actor_types.key?(klass)
61
+
62
+ Configuration.actor_types[klass]
63
+ end
64
+
65
+ # @return [Boolean] True if the given model is a possible data entity
66
+ def data_entity?(class_or_instance)
67
+ Configuration.data_types.key? class_or_instance_name(class_or_instance)
68
+ end
69
+
70
+ # Finds configured data types from ActivityPub type
71
+ #
72
+ # @param type [String] ActivityPub object type, as configured with `:handles`
73
+ # @return [Array] List of data entity configurations
74
+ #
75
+ # @example
76
+ # data_entity_handlers_for 'Note'
77
+ def data_entity_handlers_for(type)
78
+ Fedipub::Configuration.data_types.select { |_, v| v[:handles] == type }.map(&:last)
79
+ end
80
+
81
+ # Finds the configured handler for a given ActivityPub object
82
+ #
83
+ # @param hash [Hash] ActivityPub object hash
84
+ #
85
+ # @return [Hash, nil] Data entity configuration
86
+ def data_entity_handler_for(hash)
87
+ data_entity_handlers_for(hash['type']).find do |handler|
88
+ if handler[:filter_method] || handler[:class].respond_to?(DEFAULT_DATA_FILTER_METHOD)
89
+ handler[:class].send(handler[:filter_method] || DEFAULT_DATA_FILTER_METHOD, hash)
90
+ else
91
+ true
92
+ end
93
+ end
94
+ end
95
+
96
+ # Finds configured data type from route path segment
97
+ #
98
+ # @param route_path_segment [Symbol, String] Route path segment, as configured with `:route_path_segment`
99
+ # @return [Hash, nil] Entity configuration
100
+ #
101
+ # @example
102
+ # data_entity_handled_on :articles
103
+ def data_entity_handled_on(route_path_segment)
104
+ route_path_segment = route_path_segment.to_sym
105
+ Fedipub::Configuration.data_types.find { |_, v| v[:route_path_segment] == route_path_segment }&.last
106
+ end
107
+
108
+ # @return [Hash] The configuration for the given data entity
109
+ def data_entity_configuration(class_or_instance)
110
+ klass = class_or_instance_name(class_or_instance)
111
+ raise "#{klass} is not a configured data entity" unless Configuration.data_types.key?(klass)
112
+
113
+ Configuration.data_types[klass]
114
+ end
115
+
116
+ private
117
+
118
+ # @return [String] Class name of the provided class or instance
119
+ def class_or_instance_name(class_or_instance)
120
+ case class_or_instance
121
+ when String
122
+ class_or_instance
123
+ when Class
124
+ class_or_instance.name
125
+ else
126
+ class_or_instance.class.name
127
+ end
128
+ end
129
+ end
8
130
  end
131
+ # rubocop:enable Style/ClassVars
@@ -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
@@ -0,0 +1,110 @@
1
+ require 'fediverse/request'
2
+
3
+ module Fediverse
4
+ class Inbox
5
+ @@handlers = {} # rubocop:todo Style/ClassVars
6
+ class << self
7
+ # Registers a handler for incoming data
8
+ #
9
+ # Unless a specific type is not implemented in Fedipub, you should leave the 'Delete' activity to Fedipub:
10
+ # it will dispatch a `on_fedipub_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
18
+ def register_handler(activity_type, object_type, klass, method)
19
+ @@handlers[activity_type] ||= {}
20
+ @@handlers[activity_type][object_type] ||= {}
21
+ @@handlers[activity_type][object_type][klass] = method
22
+ end
23
+
24
+ # Executes the registered handler for an incoming object
25
+ #
26
+ # @param payload [Hash] Dereferenced activity
27
+ def dispatch_request(payload)
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'))
33
+ handlers.each_pair do |klass, method|
34
+ klass.send method, payload
35
+ end
36
+ return true unless handlers.empty?
37
+
38
+ Rails.logger.debug { "Unhandled activity type: #{payload['type']}" }
39
+ false
40
+ end
41
+
42
+ private
43
+
44
+ def dispatch_delete_request(payload)
45
+ payload['object'] = payload['object']['id'] unless payload['object'].is_a? String
46
+ object = Fedipub::Utils::Object.find_distant_object_in_all payload['object']
47
+ return if object.blank?
48
+
49
+ object.run_callbacks :on_fedipub_delete_requested
50
+ end
51
+
52
+ def get_handlers(activity_type, object_type)
53
+ {}.merge(@@handlers.dig(activity_type, object_type) || {})
54
+ .merge(@@handlers.dig(activity_type, '*') || {})
55
+ .merge(@@handlers.dig('*', '*') || {})
56
+ .merge(@@handlers.dig('*', object_type) || {})
57
+ end
58
+
59
+ def handle_create_follow_request(activity)
60
+ actor = Fedipub::Actor.find_or_create_by_object activity['actor']
61
+ target_actor = Fedipub::Actor.find_or_create_by_object activity['object']
62
+
63
+ Fedipub::Following.create! actor: actor, target_actor: target_actor, federated_url: activity['id']
64
+ end
65
+
66
+ def handle_accept_follow_request(activity)
67
+ original_activity = Request.dereference(activity['object'])
68
+
69
+ actor = Fedipub::Actor.find_or_create_by_object original_activity['actor']
70
+ target_actor = Fedipub::Actor.find_or_create_by_object original_activity['object']
71
+ raise 'Follow not accepted by target actor but by someone else' if activity['actor'] != target_actor.federated_url
72
+
73
+ follow = Fedipub::Following.find_by actor: actor, target_actor: target_actor
74
+ follow.accept!
75
+ end
76
+
77
+ def handle_undo_follow_request(activity)
78
+ original_activity = activity['object']
79
+
80
+ actor = Fedipub::Actor.find_or_create_by_object original_activity['actor']
81
+ target_actor = Fedipub::Actor.find_or_create_by_object original_activity['object']
82
+
83
+ follow = Fedipub::Following.find_by actor: actor, target_actor: target_actor
84
+ follow&.destroy
85
+ end
86
+
87
+ def handle_delete_request(activity)
88
+ object = Fedipub::Utils::Object.find_distant_object_in_all(activity['object'])
89
+ return if object.blank?
90
+
91
+ object.run_callbacks :on_fedipub_delete_requested
92
+ end
93
+
94
+ def handle_undelete_request(activity)
95
+ # Get to original object
96
+ delete_activity = Request.dereference(activity['object'])
97
+ object = Fedipub::Utils::Object.find_distant_object_in_all(delete_activity['object'])
98
+ return if object.blank?
99
+
100
+ object.run_callbacks :on_fedipub_undelete_requested
101
+ end
102
+ end
103
+
104
+ register_handler 'Follow', '*', self, :handle_create_follow_request
105
+ register_handler 'Accept', 'Follow', self, :handle_accept_follow_request
106
+ register_handler 'Undo', 'Follow', self, :handle_undo_follow_request
107
+ register_handler 'Delete', '*', self, :handle_delete_request
108
+ register_handler 'Undo', 'Delete', self, :handle_undelete_request
109
+ end
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 = Fedipub::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 = Fedipub::Configuration.force_ssl ? 'https' : 'http'
27
+ @base_url = "#{scheme}://#{domain}"
28
+ end
29
+
30
+ def nodeinfo_url(domain)
31
+ response = Fedipub::Utils::JsonRequest.get_json "#{base_url(domain)}/.well-known/nodeinfo"
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
@@ -0,0 +1,63 @@
1
+ require 'fediverse/signature'
2
+
3
+ module Fediverse
4
+ class Notifier
5
+ class << self
6
+ # Posts an activity to its recipients
7
+ #
8
+ # @param activity [Fedipub::Activity]
9
+ def post_to_inboxes(activity)
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?
13
+
14
+ # Deliver to each inbox
15
+ message = payload(activity)
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)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ # Overridden by Fedipub::Moderation for filtering
25
+ def post_to_inbox(inbox_url:, message:, from: nil)
26
+ Fedipub::Utils::JsonRequest.post(url: inbox_url, message: message, from: from)
27
+ end
28
+
29
+ # Determines the list of inboxes that the activity should be delivered to
30
+ #
31
+ # @return [Array<Fedipub::Actor>]
32
+ def inboxes_for(activity)
33
+ return [] unless activity.actor.local?
34
+
35
+ [activity.to, activity.cc].flatten.compact.reject { |x| x == Fediverse::Collection::PUBLIC }.map do |url|
36
+ actor = Fedipub::Actor.find_or_create_by_federation_url(url)
37
+ [actor.inbox_url]
38
+ rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid
39
+ collection_to_actors(url).map(&:inbox_url)
40
+ end.flatten.compact
41
+ end
42
+
43
+ def collection_to_actors(url)
44
+ collection = Collection.fetch(url)
45
+ collection.filter_map do |actor_url|
46
+ Fedipub::Actor.find_or_create_by_federation_url(actor_url)
47
+ rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid
48
+ nil
49
+ end
50
+ rescue Errors::NotACollection
51
+ []
52
+ end
53
+
54
+ def payload(activity)
55
+ Fedipub::ServerController.renderer.new.render(
56
+ template: 'fedipub/server/activities/show',
57
+ assigns: { activity: activity },
58
+ format: :json
59
+ )
60
+ end
61
+ end
62
+ end
63
+ end