federails 0.1.0 → 0.2.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 +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
|