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.
- checksums.yaml +4 -4
- data/README.md +76 -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 +44 -0
- data/app/controllers/fedipub/server_controller.rb +48 -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 +165 -0
- data/app/models/concerns/fedipub/announceable.rb +18 -0
- data/app/models/concerns/fedipub/application_actor.rb +25 -0
- data/app/models/concerns/fedipub/data_entity.rb +278 -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 +306 -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 +41 -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 +10 -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 +32 -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 +89 -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 +63 -0
- data/lib/fediverse/request.rb +29 -0
- data/lib/fediverse/signature/draft_cavage12.rb +109 -0
- data/lib/fediverse/signature/rfc9421.rb +69 -0
- data/lib/fediverse/signature.rb +26 -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 +276 -10
|
@@ -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,109 @@
|
|
|
1
|
+
require 'fediverse/signature'
|
|
2
|
+
|
|
3
|
+
module Fediverse
|
|
4
|
+
module Signature
|
|
5
|
+
class DraftCavage12
|
|
6
|
+
class << self
|
|
7
|
+
def sign(sender:, request:)
|
|
8
|
+
request = set_headers(request)
|
|
9
|
+
|
|
10
|
+
parts = {
|
|
11
|
+
keyId: sender.key_id,
|
|
12
|
+
headers: signature_headers(request).join(' '),
|
|
13
|
+
signature: signature(sender: sender, request: request),
|
|
14
|
+
}
|
|
15
|
+
request.headers['Signature'] = parts.map { |k, v| "#{k}=\"#{v}\"" }.join(',')
|
|
16
|
+
request
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def verify!(request:)
|
|
20
|
+
# Do we have a signature to verify?
|
|
21
|
+
return false unless request.headers.key?('Signature')
|
|
22
|
+
|
|
23
|
+
# Have we got what we need?
|
|
24
|
+
components = signature_components(request)
|
|
25
|
+
raise Fediverse::Signature::BadSignature, 'Malformed signature' unless components['signature'] && components['headers']
|
|
26
|
+
|
|
27
|
+
# Find the sender
|
|
28
|
+
sender = find_sender_by_key_id(components['keyId'])
|
|
29
|
+
raise Fediverse::Signature::BadSignature, "Couldn't find sender" unless sender
|
|
30
|
+
|
|
31
|
+
# Build the expected payload
|
|
32
|
+
comparison_string = signature_payload(request: request, headers: components['headers'])
|
|
33
|
+
|
|
34
|
+
# Verify the payload against the signature
|
|
35
|
+
result = do_verification(components['signature'], sender, comparison_string)
|
|
36
|
+
raise Fediverse::Signature::BadSignature unless result
|
|
37
|
+
|
|
38
|
+
result
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def do_verification(signature, sender, comparison_string)
|
|
44
|
+
signature = Base64.decode64(signature)
|
|
45
|
+
key = OpenSSL::PKey::RSA.new(sender.public_key)
|
|
46
|
+
key.verify(OpenSSL::Digest.new('SHA256'), signature, comparison_string)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def find_sender_by_key_id(key_id)
|
|
50
|
+
return unless key_id
|
|
51
|
+
|
|
52
|
+
Fedipub::Actor.find_or_create_by_federation_url(
|
|
53
|
+
key_id.split('#', 2).first
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def set_headers(request) # rubocop:disable Naming/AccessorMethodName
|
|
58
|
+
request.headers['Digest'] = digest(request.body) if request.body
|
|
59
|
+
request.headers['Host'] = URI.parse(request.path).host
|
|
60
|
+
request.headers['Date'] = Time.now.utc.httpdate
|
|
61
|
+
request
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def signature_components(request)
|
|
65
|
+
request.headers['Signature'].split(',').to_h do |pair|
|
|
66
|
+
/\A(?<key>\w+)="(?<value>.*)"\z/ =~ pair
|
|
67
|
+
[key, value]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def digest(message)
|
|
72
|
+
"SHA-256=#{Base64.strict_encode64(
|
|
73
|
+
OpenSSL::Digest.new('SHA256').digest(message)
|
|
74
|
+
)}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def signature_payload(request:, headers: signature_headers(request))
|
|
78
|
+
headers = headers.split if headers.is_a?(String)
|
|
79
|
+
headers.map do |header|
|
|
80
|
+
case header
|
|
81
|
+
when '(request-target)'
|
|
82
|
+
"(request-target): #{(request.try(:http_method) || request.try(:method)).downcase} #{URI.parse(request.path).path}"
|
|
83
|
+
else
|
|
84
|
+
"#{header}: #{request.headers[header.capitalize]}"
|
|
85
|
+
end
|
|
86
|
+
end.join("\n")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def signature_headers(request)
|
|
90
|
+
if request.body
|
|
91
|
+
%w[(request-target) host date digest]
|
|
92
|
+
else
|
|
93
|
+
%w[(request-target) host date]
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def signature(sender:, request:)
|
|
98
|
+
private_key = OpenSSL::PKey::RSA.new sender.private_key, Rails.application.credentials.secret_key_base
|
|
99
|
+
Base64.strict_encode64(
|
|
100
|
+
private_key.sign(
|
|
101
|
+
OpenSSL::Digest.new('SHA256'),
|
|
102
|
+
signature_payload(request: request)
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require 'linzer'
|
|
2
|
+
require 'linzer/faraday'
|
|
3
|
+
require 'linzer/rack'
|
|
4
|
+
|
|
5
|
+
Linzer::Message.register_adapter ActionDispatch::Request, Linzer::Message::Adapter::Rack::Request
|
|
6
|
+
|
|
7
|
+
module Fediverse
|
|
8
|
+
module Signature
|
|
9
|
+
class Rfc9421
|
|
10
|
+
class << self
|
|
11
|
+
def sign(sender:, request:)
|
|
12
|
+
request.headers['Content-Digest'] = digest(request.body) if request.body
|
|
13
|
+
Linzer.sign!(
|
|
14
|
+
request,
|
|
15
|
+
key: linzer_private_key(sender),
|
|
16
|
+
components: components(request),
|
|
17
|
+
params: {
|
|
18
|
+
created: Time.now.to_i,
|
|
19
|
+
alg: 'rsa-v1_5-sha256',
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
request
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def verify!(request:)
|
|
26
|
+
# Do we have a signature to verify?
|
|
27
|
+
return false if !request.headers.key?('Signature-Input') || !request.headers.key?('Signature')
|
|
28
|
+
|
|
29
|
+
# Verify the signature
|
|
30
|
+
Linzer.verify!(request) do |key_id|
|
|
31
|
+
sender = Fedipub::Actor.find_or_create_by_federation_url(key_id.split('#', 2).first)
|
|
32
|
+
raise Fediverse::Signature::BadSignature if sender.nil?
|
|
33
|
+
|
|
34
|
+
linzer_public_key(sender)
|
|
35
|
+
end
|
|
36
|
+
rescue Linzer::Error
|
|
37
|
+
raise Fediverse::Signature::BadSignature
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# Converts key to right structure for Linzer to use
|
|
43
|
+
def linzer_private_key(sender)
|
|
44
|
+
private_key = OpenSSL::PKey::RSA.new sender.private_key, Rails.application.credentials.secret_key_base
|
|
45
|
+
Linzer.new_rsa_v1_5_sha256_key(private_key.to_pem, sender.key_id)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def linzer_public_key(sender)
|
|
49
|
+
public_key = OpenSSL::PKey::RSA.new(sender.public_key)
|
|
50
|
+
Linzer.new_rsa_v1_5_sha256_key(public_key.to_pem, sender.key_id)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def components(request)
|
|
54
|
+
if request.body
|
|
55
|
+
%w[@method @target-uri content-digest]
|
|
56
|
+
else
|
|
57
|
+
%w[@method @target-uri]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def digest(message)
|
|
62
|
+
"sha-256=:#{Base64.strict_encode64(
|
|
63
|
+
OpenSSL::Digest.new('SHA256').digest(message)
|
|
64
|
+
)}:"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Fediverse
|
|
2
|
+
module Signature
|
|
3
|
+
class BadSignature < StandardError; end
|
|
4
|
+
|
|
5
|
+
class << self
|
|
6
|
+
def sign(sender:, request:, legacy_signature: false)
|
|
7
|
+
if legacy_signature
|
|
8
|
+
Fediverse::Signature::DraftCavage12.sign(sender: sender, request: request)
|
|
9
|
+
else
|
|
10
|
+
Fediverse::Signature::Rfc9421.sign(sender: sender, request: request)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def verify!(request:, require_signature: false) # rubocop:disable Naming/PredicateMethod
|
|
15
|
+
success = Fediverse::Signature::Rfc9421.verify!(request: request) ||
|
|
16
|
+
Fediverse::Signature::DraftCavage12.verify!(request: request)
|
|
17
|
+
raise(BadSignature) if require_signature && !success
|
|
18
|
+
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
require 'fediverse/signature/draft_cavage12'
|
|
26
|
+
require 'fediverse/signature/rfc9421'
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
require 'fedipub/utils/host'
|
|
2
|
+
require 'fedipub/utils/json_request'
|
|
3
|
+
|
|
4
|
+
module Fediverse
|
|
5
|
+
# Methods related to Webfinger: find accounts, fetch actors,...
|
|
6
|
+
class Webfinger
|
|
7
|
+
class << self
|
|
8
|
+
ACCOUNT_REGEX = /(?<username>[a-z0-9\-_.]+)(?:@(?<domain>.*))?/
|
|
9
|
+
|
|
10
|
+
# Extracts username and domain from an account string.
|
|
11
|
+
# Accepts forms "user@domain", "@user@domain" and "acct:user@domain"
|
|
12
|
+
#
|
|
13
|
+
# @param account [String] Account string
|
|
14
|
+
#
|
|
15
|
+
# @return [MatchData, nil] Matches with +:username+ and +:domain+ or +nil+
|
|
16
|
+
def split_account(account)
|
|
17
|
+
/\A(acct:|@)?#{ACCOUNT_REGEX}\z/io.match account
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Determines if a given account string should be a local account (same host as configured one)
|
|
21
|
+
#
|
|
22
|
+
# @param hash [Hash, MatchData] Object with +:username+ and +:domain+ keys
|
|
23
|
+
#
|
|
24
|
+
# @return [Boolean]
|
|
25
|
+
def local_user?(hash)
|
|
26
|
+
hash[:username] && (hash[:domain].nil? || (hash[:domain] == Fedipub::Utils::Host.localhost))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Fetches a distant actor
|
|
30
|
+
#
|
|
31
|
+
# @param username [String]
|
|
32
|
+
# @param domain [String]
|
|
33
|
+
#
|
|
34
|
+
# @return [Fedipub::Actor, nil] Fedipub actor or nothing when not found
|
|
35
|
+
def fetch_actor(username, domain)
|
|
36
|
+
fetch_actor_url webfinger(username, domain)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Fetches an actor given its URL
|
|
40
|
+
#
|
|
41
|
+
# @param url [String] Actor's federation URL
|
|
42
|
+
#
|
|
43
|
+
# @return [Fedipub::Actor, nil] Fedipub actor or nothing when not found
|
|
44
|
+
def fetch_actor_url(url)
|
|
45
|
+
webfinger_to_actor get_json url
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Gets the real actor's federation URL from its username and domain
|
|
49
|
+
#
|
|
50
|
+
# @param username [String]
|
|
51
|
+
# @param domain [String]
|
|
52
|
+
#
|
|
53
|
+
# @return [String, nil] Federation URL if found
|
|
54
|
+
def webfinger(username, domain)
|
|
55
|
+
json = webfinger_response(username, domain)
|
|
56
|
+
link = json['links'].find { |l| Mime::Type.lookup(l['type']).to_sym == :activitypub }
|
|
57
|
+
|
|
58
|
+
link['href'] if link
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns remote follow link template, or complete link if actor_url is provided
|
|
62
|
+
#
|
|
63
|
+
# @param username [String]
|
|
64
|
+
# @param domain [String]
|
|
65
|
+
# @param actor_url [String] Optional Federation URL to provide when known
|
|
66
|
+
#
|
|
67
|
+
# @return [String] The URL to use as follow URL
|
|
68
|
+
def remote_follow_url(username, domain, actor_url: nil)
|
|
69
|
+
json = webfinger_response(username, domain)
|
|
70
|
+
link = json['links'].find { |l| l['rel'] == 'http://ostatus.org/schema/1.0/subscribe' }
|
|
71
|
+
return nil if link&.dig('template').nil?
|
|
72
|
+
|
|
73
|
+
if actor_url
|
|
74
|
+
link['template'].gsub('{uri}', CGI.escape(actor_url))
|
|
75
|
+
else
|
|
76
|
+
link['template']
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# Makes a webfinger request for a given username/domain
|
|
83
|
+
# @return [Hash] Webfinger response's content
|
|
84
|
+
def webfinger_response(username, domain)
|
|
85
|
+
scheme = Fedipub.configuration.force_ssl ? 'https' : 'http'
|
|
86
|
+
get_json "#{scheme}://#{domain}/.well-known/webfinger", resource: "acct:#{username}@#{domain}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Extracts the server and port from a string, omitting common ports
|
|
90
|
+
# @return [String] Server and port
|
|
91
|
+
def server_and_port(string)
|
|
92
|
+
uri = URI.parse string
|
|
93
|
+
if uri.port && [80, 443].exclude?(uri.port)
|
|
94
|
+
"#{uri.host}:#{uri.port}"
|
|
95
|
+
else
|
|
96
|
+
uri.host
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Builds a +Fedipub::Actor+ from a Webfinger response
|
|
101
|
+
# @param data [Hash] Webfinger response
|
|
102
|
+
# @return [Fedipub::Actor]
|
|
103
|
+
def webfinger_to_actor(data) # rubocop:disable Metrics/MethodLength
|
|
104
|
+
data = data.clone
|
|
105
|
+
id = data.delete('id')
|
|
106
|
+
Fedipub::Actor.new federated_url: id,
|
|
107
|
+
username: data.delete('preferredUsername'),
|
|
108
|
+
actor_type: data.delete('type'),
|
|
109
|
+
name: data.delete('name'),
|
|
110
|
+
server: server_and_port(id),
|
|
111
|
+
inbox_url: data.delete('inbox'),
|
|
112
|
+
outbox_url: data.delete('outbox'),
|
|
113
|
+
followers_url: data.delete('followers'),
|
|
114
|
+
followings_url: data.delete('following'),
|
|
115
|
+
profile_url: data.delete('url'),
|
|
116
|
+
public_key: data.delete('publicKey')&.dig('publicKeyPem'),
|
|
117
|
+
extensions: data.except('@context')
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Makes a simple GET request and returns a +Hash+ from the parsed body
|
|
121
|
+
# @return [Hash]
|
|
122
|
+
# @raise [ActiveRecord::RecordNotFound] when the response is invalid
|
|
123
|
+
def get_json(url, params = {})
|
|
124
|
+
Fedipub::Utils::JsonRequest.get_json(url, params: params, headers: { accept: 'application/json' })
|
|
125
|
+
rescue Fedipub::Utils::JsonRequest::UnhandledResponseStatus => e
|
|
126
|
+
Rails.logger.debug { e.message }
|
|
127
|
+
|
|
128
|
+
raise ActiveRecord::RecordNotFound
|
|
129
|
+
rescue Faraday::ConnectionFailed
|
|
130
|
+
Rails.logger.debug { "Failed to reach server for GET #{url}" }
|
|
131
|
+
|
|
132
|
+
raise ActiveRecord::RecordNotFound
|
|
133
|
+
rescue JSON::ParserError
|
|
134
|
+
Rails.logger.debug { "Invalid JSON response for GET #{url}" }
|
|
135
|
+
|
|
136
|
+
raise ActiveRecord::RecordNotFound
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
data/lib/fediverse.rb
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module Fedipub
|
|
2
|
+
class CopyClientPoliciesGenerator < Rails::Generators::Base
|
|
3
|
+
source_root File.expand_path('../../../../app/policies/fedipub', __dir__)
|
|
4
|
+
|
|
5
|
+
def copy_policies
|
|
6
|
+
directory 'client', Rails.root.join('app', 'policies', 'fedipub', 'client')
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Fedipub
|
|
2
|
+
class CopyFactoriesGenerator < Rails::Generators::Base
|
|
3
|
+
SOURCE_DIRECTORY = File.expand_path('../../../../spec/factories/fedipub', __dir__)
|
|
4
|
+
FACTORY_DEFINITION_REGEX = /(FactoryBot.define do\n\s+factory) :(\w+),/
|
|
5
|
+
|
|
6
|
+
source_root SOURCE_DIRECTORY
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
super
|
|
10
|
+
@files = []
|
|
11
|
+
@factories = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def copy_factories
|
|
15
|
+
dest = Rails.root.join('spec', 'factories')
|
|
16
|
+
|
|
17
|
+
Dir.entries(SOURCE_DIRECTORY).each do |node|
|
|
18
|
+
source_path = File.join(SOURCE_DIRECTORY, node)
|
|
19
|
+
next unless File.file?(source_path) && node.match?(/\.rb\Z/)
|
|
20
|
+
|
|
21
|
+
file_path = File.join(dest, "fedipub_#{node}")
|
|
22
|
+
copy_file node, file_path
|
|
23
|
+
@files << file_path
|
|
24
|
+
@factories << File.read(file_path).match(FACTORY_DEFINITION_REGEX)&.[](2)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
substitute_values!
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def substitute_values!
|
|
33
|
+
@factories.compact!
|
|
34
|
+
@files.each do |file|
|
|
35
|
+
gsub_file file, FACTORY_DEFINITION_REGEX, '\1 :fedipub_\2,'
|
|
36
|
+
|
|
37
|
+
@factories.each do |factory|
|
|
38
|
+
gsub_file file, ":#{factory}", ":fedipub_#{factory}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Fedipub
|
|
2
|
+
class InstallGenerator < Rails::Generators::Base
|
|
3
|
+
source_root File.expand_path('templates', __dir__)
|
|
4
|
+
|
|
5
|
+
def copy_files
|
|
6
|
+
copy_file 'fedipub.yml', Rails.root.join('config', 'fedipub.yml')
|
|
7
|
+
copy_file 'fedipub.rb', Rails.root.join('config', 'initializers', 'fedipub.rb')
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Fedipub.config_from 'fedipub'
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
defaults: &defaults
|
|
3
|
+
app_name:
|
|
4
|
+
app_version:
|
|
5
|
+
force_ssl: false
|
|
6
|
+
site_host: http://localhost
|
|
7
|
+
site_port: 3000
|
|
8
|
+
enable_discovery: true
|
|
9
|
+
open_registrations: false
|
|
10
|
+
app_layout: 'layouts/application'
|
|
11
|
+
server_routes_path: federation
|
|
12
|
+
client_routes_path: app
|
|
13
|
+
job_queue: default
|
|
14
|
+
#remote_entities_cache_duration: 86400 # 1 day in seconds
|
|
15
|
+
#base_client_controller: ::ActionController::Base
|
|
16
|
+
|
|
17
|
+
development:
|
|
18
|
+
<<: *defaults
|
|
19
|
+
|
|
20
|
+
test:
|
|
21
|
+
<<: *defaults
|
|
22
|
+
site_port: null
|
|
23
|
+
|
|
24
|
+
production:
|
|
25
|
+
<<: *defaults
|
|
26
|
+
force_ssl: true
|
|
27
|
+
site_port: 443
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
namespace :factory_bot do
|
|
2
|
+
desc 'Verify that all FactoryBot factories are valid'
|
|
3
|
+
task lint: :environment do
|
|
4
|
+
if Rails.env.test?
|
|
5
|
+
conn = ActiveRecord::Base.connection
|
|
6
|
+
conn.transaction do
|
|
7
|
+
FactoryBot.lint traits: true
|
|
8
|
+
raise ActiveRecord::Rollback
|
|
9
|
+
end
|
|
10
|
+
else
|
|
11
|
+
system("bundle exec rake app:factory_bot:lint RAILS_ENV='test'")
|
|
12
|
+
raise if $CHILD_STATUS.exitstatus.nonzero?
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
namespace :fedipub do
|
|
2
|
+
desc 'Re-fetches every remote actors to update database'
|
|
3
|
+
task sync_actors: :environment do
|
|
4
|
+
Fedipub::Maintenance::ActorUpdater.run do |actor, status|
|
|
5
|
+
puts "#{actor.federated_url}: #{status}"
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
desc 'Re-fetches every host and completes missing ones'
|
|
10
|
+
task sync_hosts: :environment do
|
|
11
|
+
Fedipub::Maintenance::HostsUpdater.run
|
|
12
|
+
end
|
|
13
|
+
end
|