federails 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +11 -0
- data/app/controllers/federails/application_controller.rb +4 -0
- data/app/controllers/federails/client/activities_controller.rb +1 -1
- data/app/controllers/federails/client/actors_controller.rb +1 -1
- data/app/controllers/federails/client/followings_controller.rb +9 -1
- data/app/controllers/federails/server/activities_controller.rb +5 -5
- data/app/controllers/federails/server/actors_controller.rb +1 -1
- data/app/controllers/federails/server/followings_controller.rb +2 -1
- data/app/controllers/federails/server/nodeinfo_controller.rb +2 -2
- data/app/controllers/federails/server/web_finger_controller.rb +2 -2
- data/app/helpers/federails/application_helper.rb +8 -0
- data/app/models/concerns/federails/entity.rb +12 -1
- data/app/models/concerns/federails/has_uuid.rb +35 -0
- data/app/models/federails/activity.rb +8 -13
- data/app/models/federails/actor.rb +38 -1
- data/app/models/federails/following.rb +9 -0
- data/app/views/federails/client/actors/show.html.erb +1 -1
- data/app/views/federails/server/activities/{_activity.json.jbuilder → _activity.activitypub.jbuilder} +6 -1
- data/app/views/federails/server/actors/{_actor.json.jbuilder → _actor.activitypub.jbuilder} +11 -1
- data/app/views/federails/server/web_finger/{find.json.jbuilder → find.jrd.jbuilder} +5 -1
- data/config/initializers/mime_types.rb +21 -0
- data/config/routes.rb +1 -1
- data/db/migrate/20241002094500_add_uuids.rb +13 -0
- data/db/migrate/20241002094501_add_keypair_to_actors.rb +8 -0
- data/lib/federails/configuration.rb +4 -0
- data/lib/federails/version.rb +1 -1
- data/lib/federails.rb +2 -1
- data/lib/fediverse/notifier.rb +44 -5
- data/lib/fediverse/signature.rb +49 -0
- data/lib/fediverse/webfinger.rb +31 -7
- metadata +20 -15
- /data/app/views/federails/server/activities/{outbox.json.jbuilder → outbox.activitypub.jbuilder} +0 -0
- /data/app/views/federails/server/activities/{show.json.jbuilder → show.activitypub.jbuilder} +0 -0
- /data/app/views/federails/server/actors/{followers.json.jbuilder → followers.activitypub.jbuilder} +0 -0
- /data/app/views/federails/server/actors/{following.json.jbuilder → following.activitypub.jbuilder} +0 -0
- /data/app/views/federails/server/actors/{show.json.jbuilder → show.activitypub.jbuilder} +0 -0
- /data/app/views/federails/server/followings/{_following.json.jbuilder → _following.activitypub.jbuilder} +0 -0
- /data/app/views/federails/server/followings/{show.json.jbuilder → show.activitypub.jbuilder} +0 -0
- /data/app/views/federails/server/nodeinfo/{index.json.jbuilder → index.nodeinfo.jbuilder} +0 -0
- /data/app/views/federails/server/nodeinfo/{show.json.jbuilder → show.nodeinfo.jbuilder} +0 -0
- /data/app/views/federails/server/web_finger/{host_meta.xml.erb → host_meta.xrd.erb} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 070da5d2cc3d016475a69f797c6280595618d5f60de95cf7506bb2a1e9cbce4c
|
4
|
+
data.tar.gz: a954535a092fa71fdb911f973363d8372b6d56e42b32190394e92026567c6913
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 69fba1438beef26a150079c2a39eb185903025dcad3c24f5dfee941d844710d12938cf5546f0dc488f725096a2c4e6f32dfba4d1d3c825b9ba9418f063bb4ec9
|
7
|
+
data.tar.gz: 01ef6821f7b1aa9422f013e23ed20b1225f0e630d4925ca81c7490f43d790a1d77a3b93e8d72783d93aee59f1760b48bbdd71a59feca353bca0a1277a6bd03c0
|
data/README.md
CHANGED
@@ -102,6 +102,17 @@ Federails.configure do |config|
|
|
102
102
|
end
|
103
103
|
```
|
104
104
|
|
105
|
+
#### Remote following
|
106
|
+
|
107
|
+
By default, remote follow requests (where you press a follow button on another server and get redirected home to complete the follow)
|
108
|
+
will use the built-in client paths. If you're not using the client, or want to provide your own user interface, you can set the path like this, assuming that `new_follow_url` is a valid route in your app. A `uri` query parameter template will be automatically appended, you don't need to specify that.
|
109
|
+
|
110
|
+
```
|
111
|
+
Federails.configure do |config|
|
112
|
+
config.remote_follow_url_method = :new_follow_url
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
105
116
|
### Migrations
|
106
117
|
|
107
118
|
Copy the migrations:
|
@@ -11,6 +11,10 @@ module Federails
|
|
11
11
|
def error_fallback(exception, fallback_message, status)
|
12
12
|
message = exception&.message || fallback_message
|
13
13
|
respond_to do |format|
|
14
|
+
format.jrd { head status }
|
15
|
+
format.xrd { head status }
|
16
|
+
format.activitypub { head status }
|
17
|
+
format.nodeinfo { head status }
|
14
18
|
format.json { render json: { error: message }, status: status }
|
15
19
|
format.html { raise exception }
|
16
20
|
end
|
@@ -8,7 +8,7 @@ module Federails
|
|
8
8
|
# GET /app/activities.json
|
9
9
|
def index
|
10
10
|
@activities = policy_scope(Federails::Activity, policy_scope_class: Federails::Client::ActivityPolicy::Scope).all
|
11
|
-
@activities = @activities.where
|
11
|
+
@activities = @activities.where actor: Actor.find_param(params[:actor_id]) if params[:actor_id]
|
12
12
|
end
|
13
13
|
|
14
14
|
# GET /app/feed
|
@@ -25,7 +25,7 @@ module Federails
|
|
25
25
|
|
26
26
|
# Use callbacks to share common setup or constraints between actions.
|
27
27
|
def set_actor
|
28
|
-
@actor = Federails::Actor.
|
28
|
+
@actor = Federails::Actor.find_param(params[:id])
|
29
29
|
authorize @actor, policy_class: Federails::Client::ActorPolicy
|
30
30
|
end
|
31
31
|
|
@@ -4,6 +4,14 @@ module Federails
|
|
4
4
|
before_action :authenticate_user!
|
5
5
|
before_action :set_following, only: [:accept, :destroy]
|
6
6
|
|
7
|
+
# GET /app/followings/new?uri={uri}
|
8
|
+
def new
|
9
|
+
# Find actor (and fetch if necessary)
|
10
|
+
actor = Actor.find_or_create_by_federation_url(params[:uri])
|
11
|
+
# Redirect to local profile page which will have a follow button on it
|
12
|
+
redirect_to federails.client_actor_url(actor)
|
13
|
+
end
|
14
|
+
|
7
15
|
# PUT /app/followings/:id/accept
|
8
16
|
# PUT /app/followings/:id/accept.json
|
9
17
|
def accept
|
@@ -62,7 +70,7 @@ module Federails
|
|
62
70
|
|
63
71
|
# Use callbacks to share common setup or constraints between actions.
|
64
72
|
def set_following
|
65
|
-
@following = Following.
|
73
|
+
@following = Following.find_param(params[:id])
|
66
74
|
authorize @following, policy_class: Federails::Client::FollowingPolicy
|
67
75
|
end
|
68
76
|
|
@@ -8,7 +8,7 @@ module Federails
|
|
8
8
|
# GET /federation/activities
|
9
9
|
# GET /federation/actors/1/outbox.json
|
10
10
|
def outbox
|
11
|
-
@actor = Actor.
|
11
|
+
@actor = Actor.find_param(params[:actor_id])
|
12
12
|
@activities = policy_scope(Federails::Activity, policy_scope_class: Federails::Server::ActivityPolicy::Scope).where(actor: @actor).order(created_at: :desc)
|
13
13
|
@total_activities = @activities.count
|
14
14
|
@activities = @activities.page(params[:page])
|
@@ -20,12 +20,12 @@ module Federails
|
|
20
20
|
# POST /federation/actors/1/inbox
|
21
21
|
def create
|
22
22
|
payload = payload_from_params
|
23
|
-
return
|
23
|
+
return head :unprocessable_entity unless payload
|
24
24
|
|
25
25
|
if Fediverse::Inbox.dispatch_request(payload)
|
26
|
-
|
26
|
+
head :created
|
27
27
|
else
|
28
|
-
|
28
|
+
head :unprocessable_entity
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
@@ -33,7 +33,7 @@ module Federails
|
|
33
33
|
|
34
34
|
# Use callbacks to share common setup or constraints between actions.
|
35
35
|
def set_activity
|
36
|
-
@activity =
|
36
|
+
@activity = Actor.find_param(params[:actor_id]).activities.find_param(params[:id])
|
37
37
|
end
|
38
38
|
|
39
39
|
# Only allow a list of trusted parameters through.
|
@@ -21,7 +21,7 @@ module Federails
|
|
21
21
|
|
22
22
|
# Use callbacks to share common setup or constraints between actions.
|
23
23
|
def set_actor
|
24
|
-
@actor = Actor.
|
24
|
+
@actor = Actor.find_param(params[:id])
|
25
25
|
authorize @actor, policy_class: Federails::Server::ActorPolicy
|
26
26
|
end
|
27
27
|
|
@@ -10,7 +10,8 @@ module Federails
|
|
10
10
|
|
11
11
|
# Use callbacks to share common setup or constraints between actions.
|
12
12
|
def set_following
|
13
|
-
|
13
|
+
actor = Actor.find_param(params[:actor_id])
|
14
|
+
@following = Following.find_by!(actor: actor, uuid: params[:id])
|
14
15
|
authorize @following, policy_class: Federails::Server::FollowingPolicy
|
15
16
|
end
|
16
17
|
end
|
@@ -2,7 +2,7 @@ module Federails
|
|
2
2
|
module Server
|
3
3
|
class NodeinfoController < ServerController
|
4
4
|
def index
|
5
|
-
render formats: [:
|
5
|
+
render formats: [:nodeinfo]
|
6
6
|
end
|
7
7
|
|
8
8
|
def show # rubocop:todo Metrics/AbcSize
|
@@ -15,7 +15,7 @@ module Federails
|
|
15
15
|
@active_month += model.where(created_at: ((30.days.ago)...Time.current)).count
|
16
16
|
@active_halfyear += model.where(created_at: ((180.days.ago)...Time.current)).count
|
17
17
|
end
|
18
|
-
render formats: [:
|
18
|
+
render formats: [:nodeinfo]
|
19
19
|
end
|
20
20
|
end
|
21
21
|
end
|
@@ -15,11 +15,11 @@ module Federails
|
|
15
15
|
end
|
16
16
|
raise ActiveRecord::RecordNotFound if @user.nil?
|
17
17
|
|
18
|
-
render formats: [:
|
18
|
+
render formats: [:jrd]
|
19
19
|
end
|
20
20
|
|
21
21
|
def host_meta
|
22
|
-
render
|
22
|
+
render formats: [:xrd]
|
23
23
|
end
|
24
24
|
|
25
25
|
# TODO: complete missing endpoints
|
@@ -1,4 +1,12 @@
|
|
1
1
|
module Federails
|
2
2
|
module ApplicationHelper
|
3
|
+
def remote_follow_url
|
4
|
+
method_name = Federails.configuration.remote_follow_url_method.to_s
|
5
|
+
if method_name.starts_with? 'federails.'
|
6
|
+
send(method_name.gsub('federails.', ''))
|
7
|
+
else
|
8
|
+
Rails.application.routes.url_helpers.send(method_name)
|
9
|
+
end
|
10
|
+
end
|
3
11
|
end
|
4
12
|
end
|
@@ -2,7 +2,18 @@ module Federails
|
|
2
2
|
module Entity
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
|
-
included do
|
5
|
+
included do # rubocop:todo Metrics/BlockLength
|
6
|
+
include ActiveSupport::Callbacks
|
7
|
+
define_callbacks :followed
|
8
|
+
|
9
|
+
# Define a method that will be called after the entity receives a follow request
|
10
|
+
# @param method [Symbol] The name of the method to call, or a block that will be called directly
|
11
|
+
# @example
|
12
|
+
# after_followed :accept_follow
|
13
|
+
def self.after_followed(method)
|
14
|
+
set_callback :followed, :after, method
|
15
|
+
end
|
16
|
+
|
6
17
|
has_one :actor, class_name: 'Federails::Actor', as: :entity, dependent: :destroy
|
7
18
|
|
8
19
|
after_create :create_actor
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Federails
|
2
|
+
module HasUuid
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
before_validation :generate_uuid
|
7
|
+
validates :uuid, presence: true, uniqueness: true
|
8
|
+
|
9
|
+
def self.find_param(param)
|
10
|
+
find_by!(uuid: param)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_param
|
15
|
+
uuid
|
16
|
+
end
|
17
|
+
|
18
|
+
# Override UUID accessor to provide lazy initialization of UUIDs for old data
|
19
|
+
def uuid
|
20
|
+
if self[:uuid].blank?
|
21
|
+
generate_uuid
|
22
|
+
save!
|
23
|
+
end
|
24
|
+
self[:uuid]
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def generate_uuid
|
30
|
+
return if self[:uuid].present?
|
31
|
+
|
32
|
+
(self.uuid = SecureRandom.uuid) while self[:uuid].blank? || self.class.exists?(uuid: self[:uuid])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
module Federails
|
2
2
|
class Activity < ApplicationRecord
|
3
|
+
include Federails::HasUuid
|
4
|
+
|
3
5
|
belongs_to :entity, polymorphic: true
|
4
6
|
belongs_to :actor
|
5
7
|
|
@@ -13,22 +15,15 @@ module Federails
|
|
13
15
|
|
14
16
|
after_create_commit :post_to_inboxes
|
15
17
|
|
16
|
-
def recipients
|
18
|
+
def recipients
|
17
19
|
return [] unless actor.local?
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
actors.push(*actor.followers) if entity_type == 'Note'
|
25
|
-
when 'Accept'
|
26
|
-
actors.push(entity.actor) if entity_type == 'Federails::Following'
|
27
|
-
when 'Undo'
|
28
|
-
actors.push(entity.target_actor) if entity_type == 'Federails::Following'
|
21
|
+
case entity_type
|
22
|
+
when 'Federails::Following'
|
23
|
+
[(action == 'Accept' ? entity.actor : entity.target_actor)]
|
24
|
+
else
|
25
|
+
actor.followers
|
29
26
|
end
|
30
|
-
|
31
|
-
actors
|
32
27
|
end
|
33
28
|
|
34
29
|
private
|
@@ -3,6 +3,8 @@ require 'fediverse/webfinger'
|
|
3
3
|
|
4
4
|
module Federails
|
5
5
|
class Actor < ApplicationRecord # rubocop:disable Metrics/ClassLength
|
6
|
+
include Federails::HasUuid
|
7
|
+
|
6
8
|
validates :federated_url, presence: { unless: :entity }, uniqueness: { unless: :entity }
|
7
9
|
validates :username, presence: { unless: :entity }
|
8
10
|
validates :server, presence: { unless: :entity }
|
@@ -112,7 +114,7 @@ module Federails
|
|
112
114
|
|
113
115
|
def find_by_federation_url(federated_url)
|
114
116
|
local_route = Utils::Host.local_route federated_url
|
115
|
-
return
|
117
|
+
return find_param(local_route[:id]) if local_route && local_route[:controller] == 'federails/server/actors' && local_route[:action] == 'show'
|
116
118
|
|
117
119
|
actor = find_by federated_url: federated_url
|
118
120
|
return actor if actor
|
@@ -148,5 +150,40 @@ module Federails
|
|
148
150
|
end
|
149
151
|
end
|
150
152
|
end
|
153
|
+
|
154
|
+
def public_key
|
155
|
+
ensure_key_pair_exists!
|
156
|
+
self[:public_key]
|
157
|
+
end
|
158
|
+
|
159
|
+
def private_key
|
160
|
+
ensure_key_pair_exists!
|
161
|
+
self[:private_key]
|
162
|
+
end
|
163
|
+
|
164
|
+
def key_id
|
165
|
+
"#{federated_url}#main-key"
|
166
|
+
end
|
167
|
+
|
168
|
+
private
|
169
|
+
|
170
|
+
def ensure_key_pair_exists!
|
171
|
+
return if self[:private_key].present? || !local?
|
172
|
+
|
173
|
+
update!(generate_key_pair)
|
174
|
+
end
|
175
|
+
|
176
|
+
def generate_key_pair
|
177
|
+
rsa_key = OpenSSL::PKey::RSA.new 2048
|
178
|
+
cipher = OpenSSL::Cipher.new('AES-128-CBC')
|
179
|
+
{
|
180
|
+
private_key: if Rails.application.credentials.secret_key_base
|
181
|
+
rsa_key.to_pem(cipher, Rails.application.credentials.secret_key_base)
|
182
|
+
else
|
183
|
+
rsa_key.to_pem
|
184
|
+
end,
|
185
|
+
public_key: rsa_key.public_key.to_pem,
|
186
|
+
}
|
187
|
+
end
|
151
188
|
end
|
152
189
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
module Federails
|
2
2
|
class Following < ApplicationRecord
|
3
|
+
include Federails::HasUuid
|
4
|
+
|
3
5
|
enum status: { pending: 0, accepted: 1 }
|
4
6
|
|
5
7
|
validates :target_actor_id, uniqueness: { scope: [:actor_id, :target_actor_id] }
|
@@ -9,6 +11,7 @@ module Federails
|
|
9
11
|
# FIXME: Handle this with something like undelete
|
10
12
|
has_many :activities, as: :entity, dependent: :destroy
|
11
13
|
|
14
|
+
after_create :after_follow
|
12
15
|
after_create :create_activity
|
13
16
|
after_destroy :destroy_activity
|
14
17
|
|
@@ -32,6 +35,12 @@ module Federails
|
|
32
35
|
|
33
36
|
private
|
34
37
|
|
38
|
+
def after_follow
|
39
|
+
target_actor&.entity&.run_callbacks :followed, :after do
|
40
|
+
self
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
35
44
|
def create_activity
|
36
45
|
Activity.create! actor: actor, action: 'Create', entity: self
|
37
46
|
end
|
@@ -61,7 +61,7 @@
|
|
61
61
|
<p>
|
62
62
|
<b>Home page:</b>
|
63
63
|
<% if @actor.local? && Federails::Configuration.user_profile_url_method %>
|
64
|
-
<%= link_to @actor.send(Federails::Configuration.user_username_field), send(Federails::Configuration.user_profile_url_method, @actor.user) %>
|
64
|
+
<%= link_to @actor.send(Federails::Configuration.user_username_field), Rails.application.routes.url_helpers.send(Federails::Configuration.user_profile_url_method, @actor.user) %>
|
65
65
|
<% elsif @actor.profile_url %>
|
66
66
|
<%= link_to @actor.name, @actor.profile_url %>
|
67
67
|
<% else %>
|
@@ -6,4 +6,9 @@ json.type activity.action
|
|
6
6
|
json.actor activity.actor.federated_url
|
7
7
|
json.to ['https://www.w3.org/ns/activitystreams#Public']
|
8
8
|
json.cc [activity.actor.followers_url]
|
9
|
-
|
9
|
+
|
10
|
+
if activity.entity.respond_to? :to_activitypub_object
|
11
|
+
json.object activity.entity.to_activitypub_object
|
12
|
+
elsif activity.entity.respond_to? :federated_url
|
13
|
+
json.object activity.entity.federated_url
|
14
|
+
end
|
@@ -1,4 +1,7 @@
|
|
1
|
-
json.set! '@context',
|
1
|
+
json.set! '@context', [
|
2
|
+
'https://www.w3.org/ns/activitystreams',
|
3
|
+
'https://w3id.org/security/v1',
|
4
|
+
]
|
2
5
|
|
3
6
|
json.id actor.federated_url
|
4
7
|
json.name actor.name
|
@@ -9,3 +12,10 @@ json.outbox actor.outbox_url
|
|
9
12
|
json.followers actor.followers_url
|
10
13
|
json.following actor.followings_url
|
11
14
|
json.url actor.profile_url
|
15
|
+
if actor.public_key
|
16
|
+
json.publicKey do
|
17
|
+
json.id actor.key_id
|
18
|
+
json.owner actor.federated_url
|
19
|
+
json.publicKeyPem actor.public_key
|
20
|
+
end
|
21
|
+
end
|
@@ -4,7 +4,7 @@ links = [
|
|
4
4
|
# Federation actor URL
|
5
5
|
{
|
6
6
|
rel: 'self',
|
7
|
-
type:
|
7
|
+
type: Mime[:activitypub].to_s,
|
8
8
|
href: @user.actor.federated_url,
|
9
9
|
},
|
10
10
|
]
|
@@ -17,4 +17,8 @@ if @user.actor.profile_url
|
|
17
17
|
href: @user.actor.profile_url
|
18
18
|
end
|
19
19
|
|
20
|
+
# Remote following
|
21
|
+
links.push rel: 'http://ostatus.org/schema/1.0/subscribe',
|
22
|
+
template: "#{remote_follow_url}?uri={uri}"
|
23
|
+
|
20
24
|
json.links links
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Webfinger: https://datatracker.ietf.org/doc/html/rfc7033
|
2
|
+
Mime::Type.register 'application/jrd+json', :jrd
|
3
|
+
Mime::Type.register 'application/xrd+xml', :xrd
|
4
|
+
|
5
|
+
# ActivityPub: https://www.w3.org/TR/activitypub/#retrieving-objects
|
6
|
+
Mime::Type.register 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', :activitypub, ['application/activity+json', 'application/json']
|
7
|
+
|
8
|
+
# Nodeinfo: https://github.com/jhass/nodeinfo/blob/main/PROTOCOL.md#retrieval
|
9
|
+
Mime::Type.register 'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"', :nodeinfo
|
10
|
+
|
11
|
+
# Get current request parsers. Apparently we need to do it this way and can't add in-place, see
|
12
|
+
# https://api.rubyonrails.org/classes/ActionDispatch/Http/Parameters/ClassMethods.html#method-i-parameter_parsers-3D
|
13
|
+
parsers = ActionDispatch::Request.parameter_parsers
|
14
|
+
# Copy the default JSON parsing for JSON types
|
15
|
+
[:jrd, :activitypub, :nodeinfo].each do |mime_type|
|
16
|
+
parsers[Mime[mime_type].symbol] = parsers[:json]
|
17
|
+
end
|
18
|
+
# XRD just needs a simple XML parser
|
19
|
+
parsers[Mime[:xrd].symbol] = ->(raw_post) { Hash.from_xml(raw_post) || {} }
|
20
|
+
# Store updated parsers
|
21
|
+
ActionDispatch::Request.parameter_parsers = parsers
|
data/config/routes.rb
CHANGED
@@ -18,7 +18,7 @@ Federails::Engine.routes.draw do
|
|
18
18
|
resources :activities, only: [:index]
|
19
19
|
end
|
20
20
|
get :feed, to: 'activities#feed'
|
21
|
-
resources :followings, only: [:create, :destroy] do
|
21
|
+
resources :followings, only: [:new, :create, :destroy] do
|
22
22
|
collection do
|
23
23
|
post :follow, to: 'followings#follow'
|
24
24
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class AddUuids < ActiveRecord::Migration[7.0]
|
2
|
+
def change
|
3
|
+
[
|
4
|
+
:federails_actors,
|
5
|
+
:federails_activities,
|
6
|
+
:federails_followings,
|
7
|
+
].each do |table|
|
8
|
+
change_table table do |t|
|
9
|
+
t.string :uuid, default: nil, index: { unique: true }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -42,6 +42,10 @@ module Federails
|
|
42
42
|
mattr_accessor :client_routes_path
|
43
43
|
@@client_routes_path = :app
|
44
44
|
|
45
|
+
# Route method for remote-following requests
|
46
|
+
mattr_accessor :remote_follow_url_method
|
47
|
+
@@remote_follow_url_method = 'federails.new_client_following_url'
|
48
|
+
|
45
49
|
# Method to use for links to user profiles
|
46
50
|
# @deprecated Set profile_url_method option on acts_as_federails_actor instead
|
47
51
|
mattr_accessor :user_profile_url_method
|
data/lib/federails/version.rb
CHANGED
data/lib/federails.rb
CHANGED
@@ -27,10 +27,11 @@ module Federails
|
|
27
27
|
:user_class, # @deprecated
|
28
28
|
:server_routes_path,
|
29
29
|
:client_routes_path,
|
30
|
+
:remote_follow_url_method,
|
30
31
|
:user_profile_url_method, # @deprecated
|
31
32
|
:user_name_field, # @deprecated
|
32
33
|
:user_username_field, # @deprecated
|
33
|
-
].each { |key| Configuration.send :"#{key}=", config[key] }
|
34
|
+
].each { |key| Configuration.send :"#{key}=", config[key] if config.key?(key) }
|
34
35
|
end
|
35
36
|
end
|
36
37
|
# rubocop:enable Style/ClassVars
|
data/lib/fediverse/notifier.rb
CHANGED
@@ -1,21 +1,60 @@
|
|
1
|
+
require 'fediverse/signature'
|
2
|
+
|
1
3
|
module Fediverse
|
2
4
|
class Notifier
|
3
5
|
class << self
|
4
6
|
def post_to_inboxes(activity)
|
5
7
|
actors = activity.recipients
|
6
|
-
|
7
8
|
Rails.logger.debug('Nobody to notice') && return if actors.count.zero?
|
8
9
|
|
9
|
-
message =
|
10
|
+
message = payload(activity)
|
11
|
+
actors.each do |recipient|
|
12
|
+
Rails.logger.debug { "Sending activity ##{activity.id} to #{recipient.inbox_url}" }
|
13
|
+
post_to_inbox(to: recipient, message: message, from: activity.actor)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def payload(activity)
|
20
|
+
Federails::ApplicationController.renderer.new.render(
|
10
21
|
template: 'federails/server/activities/show',
|
11
22
|
assigns: { activity: activity },
|
12
23
|
format: :json
|
13
24
|
)
|
14
|
-
|
15
|
-
|
16
|
-
|
25
|
+
end
|
26
|
+
|
27
|
+
def post_to_inbox(to:, message:, from: nil)
|
28
|
+
conn = Faraday.default_connection
|
29
|
+
conn.builder.build_response(
|
30
|
+
conn,
|
31
|
+
signed_request(to: to, message: message, from: from)
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def signed_request(to:, message:, from:)
|
36
|
+
req = request(to: to, message: message)
|
37
|
+
req.headers['Signature'] = Fediverse::Signature.sign(sender: from, request: req) if from
|
38
|
+
req
|
39
|
+
end
|
40
|
+
|
41
|
+
def request(to:, message:) # rubocop:todo Metrics/AbcSize
|
42
|
+
Faraday.default_connection.build_request(:post) do |req|
|
43
|
+
req.url to.inbox_url
|
44
|
+
req.body = message
|
45
|
+
req.headers['Content-Type'] = Mime[:activitypub].to_s
|
46
|
+
req.headers['Accept'] = Mime[:activitypub].to_s
|
47
|
+
req.headers['Host'] = URI.parse(to.inbox_url).host
|
48
|
+
req.headers['Date'] = Time.now.utc.httpdate
|
49
|
+
req.headers['Digest'] = digest(message)
|
17
50
|
end
|
18
51
|
end
|
52
|
+
|
53
|
+
def digest(message)
|
54
|
+
"SHA-256=#{Base64.strict_encode64(
|
55
|
+
OpenSSL::Digest.new('SHA256').digest(message)
|
56
|
+
)}"
|
57
|
+
end
|
19
58
|
end
|
20
59
|
end
|
21
60
|
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
|
data/lib/fediverse/webfinger.rb
CHANGED
@@ -30,28 +30,52 @@ module Fediverse
|
|
30
30
|
|
31
31
|
# Returns actor id
|
32
32
|
def webfinger(username, domain)
|
33
|
-
|
34
|
-
json = get_json "#{scheme}://#{domain}/.well-known/webfinger", resource: "acct:#{username}@#{domain}"
|
33
|
+
json = webfinger_response(username, domain)
|
35
34
|
link = json['links'].find { |l| l['type'] == 'application/activity+json' }
|
36
35
|
|
37
36
|
link['href'] if link
|
38
37
|
end
|
39
38
|
|
39
|
+
# Returns remote follow link template, or complete link if actor_url is provided
|
40
|
+
def remote_follow_url(username, domain, actor_url: nil)
|
41
|
+
json = webfinger_response(username, domain)
|
42
|
+
link = json['links'].find { |l| l['rel'] == 'http://ostatus.org/schema/1.0/subscribe' }
|
43
|
+
return nil if link&.dig('template').nil?
|
44
|
+
|
45
|
+
if actor_url
|
46
|
+
link['template'].gsub('{uri}', CGI.escape(actor_url))
|
47
|
+
else
|
48
|
+
link['template']
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
40
52
|
private
|
41
53
|
|
54
|
+
def webfinger_response(username, domain)
|
55
|
+
scheme = Federails.configuration.force_ssl ? 'https' : 'http'
|
56
|
+
get_json "#{scheme}://#{domain}/.well-known/webfinger", resource: "acct:#{username}@#{domain}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def server_and_port(id)
|
60
|
+
uri = URI.parse id
|
61
|
+
if uri.port && [80, 443].exclude?(uri.port)
|
62
|
+
"#{uri.host}:#{uri.port}"
|
63
|
+
else
|
64
|
+
uri.host
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
42
68
|
def webfinger_to_actor(data)
|
43
|
-
uri = URI.parse data['id']
|
44
|
-
server = uri.host
|
45
|
-
server += ":#{uri.port}" if uri.port && [80, 443].exclude?(uri.port)
|
46
69
|
Federails::Actor.new federated_url: data['id'],
|
47
70
|
username: data['preferredUsername'],
|
48
71
|
name: data['name'],
|
49
|
-
server:
|
72
|
+
server: server_and_port(data['id']),
|
50
73
|
inbox_url: data['inbox'],
|
51
74
|
outbox_url: data['outbox'],
|
52
75
|
followers_url: data['followers'],
|
53
76
|
followings_url: data['following'],
|
54
|
-
profile_url: data['url']
|
77
|
+
profile_url: data['url'],
|
78
|
+
public_key: data.dig('publicKey', 'publicKeyPem')
|
55
79
|
end
|
56
80
|
|
57
81
|
def get_json(url, payload = {})
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: federails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Manuel Tancoigne
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-10-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -134,6 +134,7 @@ files:
|
|
134
134
|
- app/jobs/federails/notify_inbox_job.rb
|
135
135
|
- app/mailers/federails/application_mailer.rb
|
136
136
|
- app/models/concerns/federails/entity.rb
|
137
|
+
- app/models/concerns/federails/has_uuid.rb
|
137
138
|
- app/models/federails/activity.rb
|
138
139
|
- app/models/federails/actor.rb
|
139
140
|
- app/models/federails/application_record.rb
|
@@ -166,24 +167,27 @@ files:
|
|
166
167
|
- app/views/federails/client/followings/index.json.jbuilder
|
167
168
|
- app/views/federails/client/followings/show.html.erb
|
168
169
|
- app/views/federails/client/followings/show.json.jbuilder
|
169
|
-
- app/views/federails/server/activities/_activity.
|
170
|
-
- app/views/federails/server/activities/outbox.
|
171
|
-
- app/views/federails/server/activities/show.
|
172
|
-
- app/views/federails/server/actors/_actor.
|
173
|
-
- app/views/federails/server/actors/followers.
|
174
|
-
- app/views/federails/server/actors/following.
|
175
|
-
- app/views/federails/server/actors/show.
|
176
|
-
- app/views/federails/server/followings/_following.
|
177
|
-
- app/views/federails/server/followings/show.
|
178
|
-
- app/views/federails/server/nodeinfo/index.
|
179
|
-
- app/views/federails/server/nodeinfo/show.
|
180
|
-
- app/views/federails/server/web_finger/find.
|
181
|
-
- app/views/federails/server/web_finger/host_meta.
|
170
|
+
- app/views/federails/server/activities/_activity.activitypub.jbuilder
|
171
|
+
- app/views/federails/server/activities/outbox.activitypub.jbuilder
|
172
|
+
- app/views/federails/server/activities/show.activitypub.jbuilder
|
173
|
+
- app/views/federails/server/actors/_actor.activitypub.jbuilder
|
174
|
+
- app/views/federails/server/actors/followers.activitypub.jbuilder
|
175
|
+
- app/views/federails/server/actors/following.activitypub.jbuilder
|
176
|
+
- app/views/federails/server/actors/show.activitypub.jbuilder
|
177
|
+
- app/views/federails/server/followings/_following.activitypub.jbuilder
|
178
|
+
- app/views/federails/server/followings/show.activitypub.jbuilder
|
179
|
+
- app/views/federails/server/nodeinfo/index.nodeinfo.jbuilder
|
180
|
+
- app/views/federails/server/nodeinfo/show.nodeinfo.jbuilder
|
181
|
+
- app/views/federails/server/web_finger/find.jrd.jbuilder
|
182
|
+
- app/views/federails/server/web_finger/host_meta.xrd.erb
|
183
|
+
- config/initializers/mime_types.rb
|
182
184
|
- config/routes.rb
|
183
185
|
- db/migrate/20200712133150_create_federails_actors.rb
|
184
186
|
- db/migrate/20200712143127_create_federails_followings.rb
|
185
187
|
- db/migrate/20200712174938_create_federails_activities.rb
|
186
188
|
- db/migrate/20240731145400_change_actor_entity_rel_to_polymorphic.rb
|
189
|
+
- db/migrate/20241002094500_add_uuids.rb
|
190
|
+
- db/migrate/20241002094501_add_keypair_to_actors.rb
|
187
191
|
- lib/federails.rb
|
188
192
|
- lib/federails/configuration.rb
|
189
193
|
- lib/federails/engine.rb
|
@@ -192,6 +196,7 @@ files:
|
|
192
196
|
- lib/fediverse/inbox.rb
|
193
197
|
- lib/fediverse/notifier.rb
|
194
198
|
- lib/fediverse/request.rb
|
199
|
+
- lib/fediverse/signature.rb
|
195
200
|
- lib/fediverse/webfinger.rb
|
196
201
|
- lib/generators/federails/install/USAGE
|
197
202
|
- lib/generators/federails/install/install_generator.rb
|
/data/app/views/federails/server/activities/{outbox.json.jbuilder → outbox.activitypub.jbuilder}
RENAMED
File without changes
|
/data/app/views/federails/server/activities/{show.json.jbuilder → show.activitypub.jbuilder}
RENAMED
File without changes
|
/data/app/views/federails/server/actors/{followers.json.jbuilder → followers.activitypub.jbuilder}
RENAMED
File without changes
|
/data/app/views/federails/server/actors/{following.json.jbuilder → following.activitypub.jbuilder}
RENAMED
File without changes
|
File without changes
|
File without changes
|
/data/app/views/federails/server/followings/{show.json.jbuilder → show.activitypub.jbuilder}
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|