fedipub 0.0.1 → 0.9.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.
- checksums.yaml +4 -4
- data/README.md +70 -2
- data/Rakefile +5 -9
- data/app/assets/config/fedipub_manifest.js +1 -0
- data/app/assets/stylesheets/fedipub/application.css +15 -0
- data/app/controllers/concerns/fedipub/server/render_collections.rb +19 -0
- data/app/controllers/fedipub/client/activities_controller.rb +27 -0
- data/app/controllers/fedipub/client/actors_controller.rb +54 -0
- data/app/controllers/fedipub/client/followings_controller.rb +103 -0
- data/app/controllers/fedipub/client_controller.rb +9 -0
- data/app/controllers/fedipub/server/activities_controller.rb +78 -0
- data/app/controllers/fedipub/server/actors_controller.rb +50 -0
- data/app/controllers/fedipub/server/followings_controller.rb +19 -0
- data/app/controllers/fedipub/server/nodeinfo_controller.rb +28 -0
- data/app/controllers/fedipub/server/published_controller.rb +30 -0
- data/app/controllers/fedipub/server/web_finger_controller.rb +45 -0
- data/app/controllers/fedipub/server_controller.rb +37 -0
- data/app/helpers/fedipub/server_helper.rb +18 -0
- data/app/jobs/fedipub/application_job.rb +10 -0
- data/app/jobs/fedipub/fetch_nodeinfo_job.rb +10 -0
- data/app/jobs/fedipub/notify_inbox_job.rb +10 -0
- data/app/mailers/fedipub/application_mailer.rb +6 -0
- data/app/models/concerns/fedipub/actor_entity.rb +166 -0
- data/app/models/concerns/fedipub/announceable.rb +18 -0
- data/app/models/concerns/fedipub/data_entity.rb +279 -0
- data/app/models/concerns/fedipub/handles_delete_requests.rb +36 -0
- data/app/models/concerns/fedipub/has_uuid.rb +61 -0
- data/app/models/concerns/fedipub/likeable.rb +29 -0
- data/app/models/fedipub/activity.rb +62 -0
- data/app/models/fedipub/actor.rb +303 -0
- data/app/models/fedipub/application_record.rb +5 -0
- data/app/models/fedipub/following.rb +72 -0
- data/app/models/fedipub/host.rb +48 -0
- data/app/policies/fedipub/client/activity_policy.rb +9 -0
- data/app/policies/fedipub/client/actor_policy.rb +15 -0
- data/app/policies/fedipub/client/following_policy.rb +35 -0
- data/app/policies/fedipub/fedipub_policy.rb +63 -0
- data/app/policies/fedipub/server/activity_policy.rb +9 -0
- data/app/policies/fedipub/server/actor_policy.rb +23 -0
- data/app/policies/fedipub/server/following_policy.rb +6 -0
- data/app/policies/fedipub/server/publishable_policy.rb +15 -0
- data/app/views/fedipub/client/activities/_activity.html.erb +5 -0
- data/app/views/fedipub/client/activities/_activity.json.jbuilder +1 -0
- data/app/views/fedipub/client/activities/_index.json.jbuilder +1 -0
- data/app/views/fedipub/client/activities/feed.html.erb +11 -0
- data/app/views/fedipub/client/activities/feed.json.jbuilder +1 -0
- data/app/views/fedipub/client/activities/index.html.erb +5 -0
- data/app/views/fedipub/client/activities/index.json.jbuilder +1 -0
- data/app/views/fedipub/client/actors/_actor.json.jbuilder +17 -0
- data/app/views/fedipub/client/actors/_lookup_form.html.erb +5 -0
- data/app/views/fedipub/client/actors/gone.html.erb +1 -0
- data/app/views/fedipub/client/actors/index.html.erb +44 -0
- data/app/views/fedipub/client/actors/index.json.jbuilder +1 -0
- data/app/views/fedipub/client/actors/show.html.erb +112 -0
- data/app/views/fedipub/client/actors/show.json.jbuilder +1 -0
- data/app/views/fedipub/client/common/_client_links.html.erb +26 -0
- data/app/views/fedipub/client/followings/_follow.html.erb +4 -0
- data/app/views/fedipub/client/followings/_follow_actions.html.erb +36 -0
- data/app/views/fedipub/client/followings/_follower.html.erb +7 -0
- data/app/views/fedipub/client/followings/_following.json.jbuilder +1 -0
- data/app/views/fedipub/client/followings/_form.html.erb +22 -0
- data/app/views/fedipub/client/followings/index.html.erb +29 -0
- data/app/views/fedipub/client/followings/index.json.jbuilder +1 -0
- data/app/views/fedipub/client/followings/show.html.erb +21 -0
- data/app/views/fedipub/client/followings/show.json.jbuilder +1 -0
- data/app/views/fedipub/server/activities/_activity.activitypub.jbuilder +25 -0
- data/app/views/fedipub/server/activities/show.activitypub.jbuilder +1 -0
- data/app/views/fedipub/server/actors/_actor.activitypub.jbuilder +27 -0
- data/app/views/fedipub/server/actors/_tombstone.activitypub.jbuilder +6 -0
- data/app/views/fedipub/server/actors/show.activitypub.jbuilder +5 -0
- data/app/views/fedipub/server/followings/_following.activitypub.jbuilder +7 -0
- data/app/views/fedipub/server/followings/show.activitypub.jbuilder +1 -0
- data/app/views/fedipub/server/nodeinfo/index.nodeinfo.jbuilder +6 -0
- data/app/views/fedipub/server/nodeinfo/show.nodeinfo.jbuilder +19 -0
- data/app/views/fedipub/server/published/_publishable.activitypub.jbuilder +11 -0
- data/app/views/fedipub/server/published/_tombstone.activitypub.jbuilder +6 -0
- data/app/views/fedipub/server/published/show.activitypub.jbuilder +5 -0
- data/app/views/fedipub/server/shared/ordered_collection.activitypub.jbuilder +6 -0
- data/app/views/fedipub/server/shared/ordered_collection_page.activitypub.jbuilder +12 -0
- data/app/views/fedipub/server/web_finger/find.jrd.jbuilder +24 -0
- data/app/views/fedipub/server/web_finger/host_meta.xrd.erb +5 -0
- data/app/views/users/show.html.erb +4 -0
- data/config/initializers/mime_types.rb +21 -0
- data/config/initializers/security_vocab.rb +53 -0
- data/config/routes.rb +53 -0
- data/db/migrate/20200712133150_create_federails_actors.rb +22 -0
- data/db/migrate/20200712143127_create_federails_followings.rb +14 -0
- data/db/migrate/20200712174938_create_federails_activities.rb +11 -0
- data/db/migrate/20241002094500_add_uuids.rb +13 -0
- data/db/migrate/20241002094501_add_keypair_to_actors.rb +8 -0
- data/db/migrate/20250122160618_add_extensions_to_federails_actors.rb +5 -0
- data/db/migrate/20250301082500_add_local_to_actors.rb +11 -0
- data/db/migrate/20250329123939_add_actor_type_to_actors.rb +5 -0
- data/db/migrate/20250329123940_add_tombstoned_at_to_actors.rb +5 -0
- data/db/migrate/20250426061729_create_federails_hosts.rb +22 -0
- data/db/migrate/20251121160720_add_to_and_cc_to_federails_activities.rb +6 -0
- data/db/migrate/20260421170640_add_result_and_instrument_to_federails_activities.rb +6 -0
- data/db/migrate/20260723113716_rename_federails_to_fedipub.rb +15 -0
- data/lib/fedipub/configuration.rb +119 -0
- data/lib/fedipub/data_transformer/note.rb +36 -0
- data/lib/fedipub/engine.rb +11 -0
- data/lib/fedipub/maintenance/actors_updater.rb +66 -0
- data/lib/fedipub/maintenance/hosts_updater.rb +19 -0
- data/lib/fedipub/utils/actor.rb +85 -0
- data/lib/fedipub/utils/context.rb +12 -0
- data/lib/fedipub/utils/host.rb +54 -0
- data/lib/fedipub/utils/json_request.rb +41 -0
- data/lib/fedipub/utils/object.rb +131 -0
- data/lib/fedipub/utils/response_codes.rb +11 -0
- data/lib/fedipub/version.rb +1 -3
- data/lib/fedipub.rb +127 -4
- data/lib/fediverse/collection.rb +31 -0
- data/lib/fediverse/inbox.rb +110 -0
- data/lib/fediverse/node_info.rb +38 -0
- data/lib/fediverse/notifier.rb +90 -0
- data/lib/fediverse/request.rb +29 -0
- data/lib/fediverse/signature.rb +49 -0
- data/lib/fediverse/webfinger.rb +140 -0
- data/lib/fediverse.rb +5 -0
- data/lib/generators/fedipub/copy_client_policies/USAGE +8 -0
- data/lib/generators/fedipub/copy_client_policies/copy_client_policies_generator.rb +9 -0
- data/lib/generators/fedipub/copy_client_views/USAGE +8 -0
- data/lib/generators/fedipub/copy_client_views/copy_client_views_generator.rb +9 -0
- data/lib/generators/fedipub/copy_factories/USAGE +8 -0
- data/lib/generators/fedipub/copy_factories/copy_factories_generator.rb +43 -0
- data/lib/generators/fedipub/install/USAGE +9 -0
- data/lib/generators/fedipub/install/install_generator.rb +10 -0
- data/lib/generators/fedipub/install/templates/fedipub.rb +1 -0
- data/lib/generators/fedipub/install/templates/fedipub.yml +27 -0
- data/lib/tasks/factory_bot.rake +15 -0
- data/lib/tasks/fedipub_tasks.rake +13 -0
- metadata +259 -10
|
@@ -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
|
data/lib/fedipub/version.rb
CHANGED
data/lib/fedipub.rb
CHANGED
|
@@ -1,8 +1,131 @@
|
|
|
1
|
-
|
|
1
|
+
require 'jbuilder'
|
|
2
|
+
require 'kaminari'
|
|
3
|
+
require 'pundit'
|
|
2
4
|
|
|
3
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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", 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
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
# Determines the list of inboxes that the activity should be delivered to
|
|
25
|
+
#
|
|
26
|
+
# @return [Array<Fedipub::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 = Fedipub::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
|
+
Fedipub::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
|
+
|
|
49
|
+
def payload(activity)
|
|
50
|
+
Fedipub::ServerController.renderer.new.render(
|
|
51
|
+
template: 'fedipub/server/activities/show',
|
|
52
|
+
assigns: { activity: activity },
|
|
53
|
+
format: :json
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def post_to_inbox(inbox_url:, message:, from: nil)
|
|
58
|
+
conn = Faraday.default_connection
|
|
59
|
+
conn.builder.build_response(
|
|
60
|
+
conn,
|
|
61
|
+
signed_request(url: inbox_url, message: message, from: from)
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def signed_request(url:, message:, from:)
|
|
66
|
+
req = request(url: url, message: message)
|
|
67
|
+
req.headers['Signature'] = Fediverse::Signature.sign(sender: from, request: req) if from
|
|
68
|
+
req
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def request(url:, message:) # rubocop:todo Metrics/AbcSize
|
|
72
|
+
Faraday.default_connection.build_request(:post) do |req|
|
|
73
|
+
req.url url
|
|
74
|
+
req.body = message
|
|
75
|
+
req.headers['Content-Type'] = Mime[:activitypub].to_s
|
|
76
|
+
req.headers['Accept'] = Mime[:activitypub].to_s
|
|
77
|
+
req.headers['Host'] = URI.parse(url).host
|
|
78
|
+
req.headers['Date'] = Time.now.utc.httpdate
|
|
79
|
+
req.headers['Digest'] = digest(message)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def digest(message)
|
|
84
|
+
"SHA-256=#{Base64.strict_encode64(
|
|
85
|
+
OpenSSL::Digest.new('SHA256').digest(message)
|
|
86
|
+
)}"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'json/ld'
|
|
2
|
+
|
|
3
|
+
module Fediverse
|
|
4
|
+
class Request
|
|
5
|
+
class << self
|
|
6
|
+
# Dereferences a value
|
|
7
|
+
#
|
|
8
|
+
# @param value [String, Hash]
|
|
9
|
+
#
|
|
10
|
+
# @return [Hash, nil]
|
|
11
|
+
def dereference(value)
|
|
12
|
+
return value if value.is_a? Hash
|
|
13
|
+
return get(value) if value.is_a? String
|
|
14
|
+
|
|
15
|
+
raise "Unhandled object type #{value.class}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def get(id)
|
|
21
|
+
json = Fedipub::Utils::JsonRequest.get_json(id)
|
|
22
|
+
|
|
23
|
+
JSON::LD::API.compact json, json['@context']
|
|
24
|
+
rescue JSON::ParserError
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Fediverse
|
|
2
|
+
class Signature
|
|
3
|
+
class << self
|
|
4
|
+
def sign(sender:, request:)
|
|
5
|
+
private_key = OpenSSL::PKey::RSA.new sender.private_key, Rails.application.credentials.secret_key_base
|
|
6
|
+
headers = '(request-target) host date digest'
|
|
7
|
+
sig = Base64.strict_encode64(
|
|
8
|
+
private_key.sign(
|
|
9
|
+
OpenSSL::Digest.new('SHA256'), signature_payload(request: request, headers: headers)
|
|
10
|
+
)
|
|
11
|
+
)
|
|
12
|
+
{
|
|
13
|
+
keyId: sender.key_id,
|
|
14
|
+
headers: headers,
|
|
15
|
+
signature: sig,
|
|
16
|
+
}.map { |k, v| "#{k}=\"#{v}\"" }.join(',')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def verify(sender:, request:)
|
|
20
|
+
raise 'Unsigned headers' unless request.headers['Signature']
|
|
21
|
+
|
|
22
|
+
signature_header = request.headers['Signature'].split(',').to_h do |pair|
|
|
23
|
+
/\A(?<key>\w+)="(?<value>.*)"\z/ =~ pair
|
|
24
|
+
[key, value]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
headers = signature_header['headers']
|
|
28
|
+
signature = Base64.decode64(signature_header['signature'])
|
|
29
|
+
key = OpenSSL::PKey::RSA.new(sender.public_key)
|
|
30
|
+
|
|
31
|
+
comparison_string = signature_payload(request: request, headers: headers)
|
|
32
|
+
|
|
33
|
+
key.verify(OpenSSL::Digest.new('SHA256'), signature, comparison_string)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def signature_payload(request:, headers:)
|
|
39
|
+
headers.split.map do |signed_header_name|
|
|
40
|
+
if signed_header_name == '(request-target)'
|
|
41
|
+
"(request-target): #{request.http_method} #{URI.parse(request.path).path}"
|
|
42
|
+
else
|
|
43
|
+
"#{signed_header_name}: #{request.headers[signed_header_name.capitalize]}"
|
|
44
|
+
end
|
|
45
|
+
end.join("\n")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|