federails 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +38 -1
  3. data/app/controllers/federails/client/activities_controller.rb +9 -3
  4. data/app/controllers/federails/client/actors_controller.rb +5 -2
  5. data/app/controllers/federails/client/followings_controller.rb +13 -3
  6. data/app/controllers/federails/client_controller.rb +9 -0
  7. data/app/controllers/federails/server/activities_controller.rb +11 -6
  8. data/app/controllers/federails/server/actors_controller.rb +6 -2
  9. data/app/controllers/federails/server/followings_controller.rb +3 -2
  10. data/app/controllers/federails/server/nodeinfo_controller.rb +13 -7
  11. data/app/controllers/federails/server/web_finger_controller.rb +7 -3
  12. data/app/controllers/federails/{application_controller.rb → server_controller.rb} +10 -3
  13. data/app/helpers/federails/server_helper.rb +12 -0
  14. data/app/models/concerns/federails/entity.rb +67 -15
  15. data/app/models/concerns/federails/has_uuid.rb +35 -0
  16. data/app/models/federails/activity.rb +8 -13
  17. data/app/models/federails/actor.rb +41 -2
  18. data/app/models/federails/following.rb +9 -0
  19. data/app/policies/federails/client/activity_policy.rb +3 -0
  20. data/app/policies/federails/client/actor_policy.rb +1 -1
  21. data/app/policies/federails/client/following_policy.rb +2 -2
  22. data/app/policies/federails/federails_policy.rb +4 -0
  23. data/app/policies/federails/server/activity_policy.rb +3 -0
  24. data/app/views/federails/client/activities/_activity.html.erb +1 -1
  25. data/app/views/federails/client/activities/feed.html.erb +8 -1
  26. data/app/views/federails/client/activities/index.html.erb +1 -1
  27. data/app/views/federails/client/actors/index.html.erb +14 -0
  28. data/app/views/federails/client/actors/show.html.erb +101 -89
  29. data/app/views/federails/client/common/_client_links.html.erb +24 -0
  30. data/app/views/federails/client/followings/_follow_actions.html.erb +32 -0
  31. data/app/views/federails/client/followings/_form.html.erb +1 -0
  32. data/app/views/federails/server/activities/{_activity.json.jbuilder → _activity.activitypub.jbuilder} +6 -1
  33. data/app/views/federails/server/actors/_actor.activitypub.jbuilder +26 -0
  34. data/app/views/federails/server/nodeinfo/{show.json.jbuilder → show.nodeinfo.jbuilder} +8 -7
  35. data/app/views/federails/server/web_finger/{find.json.jbuilder → find.jrd.jbuilder} +5 -1
  36. data/config/initializers/mime_types.rb +21 -0
  37. data/config/routes.rb +7 -3
  38. data/db/migrate/20200712133150_create_federails_actors.rb +3 -5
  39. data/db/migrate/20241002094500_add_uuids.rb +13 -0
  40. data/db/migrate/20241002094501_add_keypair_to_actors.rb +8 -0
  41. data/lib/federails/configuration.rb +11 -28
  42. data/lib/federails/version.rb +1 -1
  43. data/lib/federails.rb +13 -5
  44. data/lib/fediverse/inbox.rb +33 -37
  45. data/lib/fediverse/notifier.rb +44 -5
  46. data/lib/fediverse/signature.rb +49 -0
  47. data/lib/fediverse/webfinger.rb +31 -7
  48. data/lib/generators/federails/copy_client_views/USAGE +8 -0
  49. data/lib/generators/federails/copy_client_views/copy_client_views_generator.rb +9 -0
  50. data/lib/generators/federails/install/install_generator.rb +2 -2
  51. data/lib/generators/federails/install/templates/federails.yml +2 -0
  52. metadata +28 -20
  53. data/app/controllers/federails/server/server_controller.rb +0 -17
  54. data/app/helpers/federails/application_helper.rb +0 -4
  55. data/app/views/federails/server/actors/_actor.json.jbuilder +0 -11
  56. data/db/migrate/20240731145400_change_actor_entity_rel_to_polymorphic.rb +0 -11
  57. /data/app/views/federails/server/activities/{outbox.json.jbuilder → outbox.activitypub.jbuilder} +0 -0
  58. /data/app/views/federails/server/activities/{show.json.jbuilder → show.activitypub.jbuilder} +0 -0
  59. /data/app/views/federails/server/actors/{followers.json.jbuilder → followers.activitypub.jbuilder} +0 -0
  60. /data/app/views/federails/server/actors/{following.json.jbuilder → following.activitypub.jbuilder} +0 -0
  61. /data/app/views/federails/server/actors/{show.json.jbuilder → show.activitypub.jbuilder} +0 -0
  62. /data/app/views/federails/server/followings/{_following.json.jbuilder → _following.activitypub.jbuilder} +0 -0
  63. /data/app/views/federails/server/followings/{show.json.jbuilder → show.activitypub.jbuilder} +0 -0
  64. /data/app/views/federails/server/nodeinfo/{index.json.jbuilder → index.nodeinfo.jbuilder} +0 -0
  65. /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: bc485556cd2ae749668f8f6a6ffec973361bdef6970e421845b9c2de64e8cecf
4
- data.tar.gz: 34630a4b116e47b3d4c3ec4fd7599ba0264f4dd76fdff5db795d2be90c50c2cc
3
+ metadata.gz: e489a0088b05ef80d9cc112164f054a030f0cb007406cb14789372a5a52c490f
4
+ data.tar.gz: b6832c622e5c33bf85f520839604a9e6e516821097d70bb6c6acb7df92f0d4b3
5
5
  SHA512:
6
- metadata.gz: b296b6db88b1077326722a187f6074fc8f9bbeb4f43d0f90a2c7811fb7bfd14b0e247f64149ce41c0e53128ce45d3e7e61ecfb0da8d47e64b6b7f188dfa33c8c
7
- data.tar.gz: 948085a88295cf6c6699ed1dbea2a64a30d8695bb1a9d596cb83d66753eb4e26b5129d0d00574d21f230544b75a3f859411e64c785878abc2ce85021ef41a2b5
6
+ metadata.gz: 59e24e51b83545daabfa7dc0c878183d8b0bc14276b559b529fb5ee48183739c49b1a1987eb4a5e8d0d795dcfbe6970d6a3478691f9a13cd12c399903df61695
7
+ data.tar.gz: 01f6e7bef60bd2d15e142b7b55fecf25dd76ab67571c994c3c8aae1eba23fbcc75d02fb03775131a070b3cc6c18021d33ce91bb8645e937d94c1425a0a7433a3
data/README.md CHANGED
@@ -92,7 +92,7 @@ With `routes_path = 'federation'`, routes will be:
92
92
 
93
93
  Some routes can be disabled in configuration if you don't want to expose particular features:
94
94
 
95
- ```
95
+ ```rb
96
96
  Federails.configure do |config|
97
97
  # Disable routing for .well-known and nodeinfo
98
98
  config.enable_discovery = false
@@ -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
+ ```rb
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:
@@ -143,6 +154,32 @@ actor.following
143
154
  #...
144
155
  ```
145
156
 
157
+ ### Using the Federails client
158
+
159
+ Federails comes with a client, enabled by default, that provides basic views to display and interact with Federails data,
160
+ accessible on `/app` by default (changeable with the configuration option `client_routes_path`)
161
+
162
+ If it's a good starting point, it might be disabled once you made your own integration by setting `client_routes_path`
163
+ to a `nil` value.
164
+
165
+ If you want to override the client's views, copy them in your application:
166
+
167
+ ```sh
168
+ rails generate federails:copy_client_views
169
+ ```
170
+
171
+ ## Common questions
172
+
173
+ - **I override the base controller and the links breaks in my layout**
174
+
175
+ Use `main_app.<url_helper>` for links to your application; `federails.<federails_url_helper>` for links to the Federails client.
176
+ - **I specified a custom layout and the links breaks in it**
177
+
178
+ Use `main_app.<url_helper>` for links to your application; `federails.<federails_url_helper>` for links to the Federails client.
179
+ - **I specified a custom layout and my helpers are not available**
180
+
181
+ You will have better results if you specify a `base_controller` from your application as Federails base controller is isolated from the main app and does not have access to its helpers.
182
+
146
183
  ## Contributing
147
184
 
148
185
  Contributions are welcome, may it be issues, ideas, code or whatever you want to share. Please note:
@@ -1,14 +1,14 @@
1
1
  module Federails
2
2
  module Client
3
- class ActivitiesController < Federails::ApplicationController
3
+ class ActivitiesController < Federails::ClientController
4
4
  before_action :authenticate_user!, only: [:feed]
5
- # layout 'layouts/application'
5
+ before_action :authorize_action!
6
6
 
7
7
  # GET /app/activities
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 actor_id: params[:actor_id] if params[:actor_id]
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
@@ -16,6 +16,12 @@ module Federails
16
16
  def feed
17
17
  @activities = Activity.feed_for(current_user.actor)
18
18
  end
19
+
20
+ private
21
+
22
+ def authorize_action!
23
+ authorize(Federails::Activity, policy_class: Federails::Client::ActivityPolicy)
24
+ end
19
25
  end
20
26
  end
21
27
  end
@@ -1,12 +1,15 @@
1
1
  module Federails
2
2
  module Client
3
- class ActorsController < Federails::ApplicationController
3
+ class ActorsController < Federails::ClientController
4
4
  before_action :set_actor, only: [:show]
5
5
 
6
6
  # GET /app/actors
7
7
  # GET /app/actors.json
8
8
  def index
9
+ authorize Federails::Actor, policy_class: Federails::Client::ActorPolicy
10
+
9
11
  @actors = policy_scope(Federails::Actor, policy_scope_class: Federails::Client::ActorPolicy::Scope).all
12
+ @actors = @actors.local if params[:local_only]
10
13
  end
11
14
 
12
15
  # GET /app/actors/1
@@ -25,7 +28,7 @@ module Federails
25
28
 
26
29
  # Use callbacks to share common setup or constraints between actions.
27
30
  def set_actor
28
- @actor = Federails::Actor.find(params[:id])
31
+ @actor = Federails::Actor.find_param(params[:id])
29
32
  authorize @actor, policy_class: Federails::Client::ActorPolicy
30
33
  end
31
34
 
@@ -1,9 +1,18 @@
1
1
  module Federails
2
2
  module Client
3
- class FollowingsController < Federails::ApplicationController
3
+ class FollowingsController < Federails::ClientController
4
4
  before_action :authenticate_user!
5
+ before_action :skip_authorization, only: [:new, :create]
5
6
  before_action :set_following, only: [:accept, :destroy]
6
7
 
8
+ # GET /app/followings/new?uri={uri}
9
+ def new
10
+ # Find actor (and fetch if necessary)
11
+ actor = Actor.find_or_create_by_federation_url(params[:uri])
12
+ # Redirect to local profile page which will have a follow button on it
13
+ redirect_to federails.client_actor_url(actor)
14
+ end
15
+
7
16
  # PUT /app/followings/:id/accept
8
17
  # PUT /app/followings/:id/accept.json
9
18
  def accept
@@ -32,9 +41,10 @@ module Federails
32
41
  # POST /app/followings/follow
33
42
  # POST /app/followings/follow.json
34
43
  def follow
44
+ authorize Federails::Following, policy_class: Federails::Client::FollowingPolicy
45
+
35
46
  begin
36
47
  @following = Following.new_from_account following_account_params, actor: current_user.actor
37
- authorize @following, policy_class: Federails::Client::FollowingPolicy
38
48
  rescue ::ActiveRecord::RecordNotFound
39
49
  # Renders a 422 instead of a 404
40
50
  respond_to do |format|
@@ -62,7 +72,7 @@ module Federails
62
72
 
63
73
  # Use callbacks to share common setup or constraints between actions.
64
74
  def set_following
65
- @following = Following.find(params[:id])
75
+ @following = Following.find_param(params[:id])
66
76
  authorize @following, policy_class: Federails::Client::FollowingPolicy
67
77
  end
68
78
 
@@ -0,0 +1,9 @@
1
+ module Federails
2
+ class ClientController < Federails.configuration.base_client_controller.constantize
3
+ include Pundit::Authorization
4
+
5
+ after_action :verify_authorized
6
+
7
+ layout Federails.configuration.app_layout if Federails.configuration.app_layout
8
+ end
9
+ end
@@ -2,13 +2,15 @@ require 'fediverse/inbox'
2
2
 
3
3
  module Federails
4
4
  module Server
5
- class ActivitiesController < ServerController
5
+ class ActivitiesController < Federails::ServerController
6
6
  before_action :set_activity, only: [:show]
7
7
 
8
8
  # GET /federation/activities
9
9
  # GET /federation/actors/1/outbox.json
10
10
  def outbox
11
- @actor = Actor.find(params[:actor_id])
11
+ authorize Federails::Activity, policy_class: Federails::Server::ActivityPolicy
12
+
13
+ @actor = Actor.find_param(params[:actor_id])
12
14
  @activities = policy_scope(Federails::Activity, policy_scope_class: Federails::Server::ActivityPolicy::Scope).where(actor: @actor).order(created_at: :desc)
13
15
  @total_activities = @activities.count
14
16
  @activities = @activities.page(params[:page])
@@ -19,13 +21,15 @@ module Federails
19
21
 
20
22
  # POST /federation/actors/1/inbox
21
23
  def create
24
+ skip_authorization
25
+
22
26
  payload = payload_from_params
23
- return render json: {}, status: :unprocessable_entity unless payload
27
+ return head :unprocessable_entity unless payload
24
28
 
25
29
  if Fediverse::Inbox.dispatch_request(payload)
26
- render json: {}, status: :created
30
+ head :created
27
31
  else
28
- render json: {}, status: :unprocessable_entity
32
+ head :unprocessable_entity
29
33
  end
30
34
  end
31
35
 
@@ -33,7 +37,8 @@ module Federails
33
37
 
34
38
  # Use callbacks to share common setup or constraints between actions.
35
39
  def set_activity
36
- @activity = Activity.find_by!(actor_id: params[:actor_id], id: params[:id])
40
+ @activity = Actor.find_param(params[:actor_id]).activities.find_param(params[:id])
41
+ authorize @activity, policy_class: Federails::Server::ActivityPolicy
37
42
  end
38
43
 
39
44
  # Only allow a list of trusted parameters through.
@@ -1,17 +1,21 @@
1
1
  module Federails
2
2
  module Server
3
- class ActorsController < ServerController
3
+ class ActorsController < Federails::ServerController
4
4
  before_action :set_actor, only: [:show, :followers, :following]
5
5
 
6
6
  # GET /federation/actors/1
7
7
  # GET /federation/actors/1.json
8
8
  def show; end
9
9
 
10
+ # GET /federation/actors/:id/followers
11
+ # GET /federation/actors/:id/followers.json
10
12
  def followers
11
13
  @actors = @actor.followers.order(created_at: :desc)
12
14
  followings_queries
13
15
  end
14
16
 
17
+ # GET /federation/actors/:id/followers
18
+ # GET /federation/actors/:id/followers.json
15
19
  def following
16
20
  @actors = @actor.follows.order(created_at: :desc)
17
21
  followings_queries
@@ -21,7 +25,7 @@ module Federails
21
25
 
22
26
  # Use callbacks to share common setup or constraints between actions.
23
27
  def set_actor
24
- @actor = Actor.find(params[:id])
28
+ @actor = Actor.find_param(params[:id])
25
29
  authorize @actor, policy_class: Federails::Server::ActorPolicy
26
30
  end
27
31
 
@@ -1,6 +1,6 @@
1
1
  module Federails
2
2
  module Server
3
- class FollowingsController < ServerController
3
+ class FollowingsController < Federails::ServerController
4
4
  before_action :set_following, only: [:show]
5
5
 
6
6
  # GET /federation/actors/1/followings/1.json
@@ -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
- @following = Following.find_by!(actor_id: params[:actor_id], id: params[:id])
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
@@ -1,21 +1,27 @@
1
1
  module Federails
2
2
  module Server
3
- class NodeinfoController < ServerController
3
+ class NodeinfoController < Federails::ServerController
4
4
  def index
5
- render formats: [:json]
5
+ skip_authorization
6
+
7
+ render formats: [:nodeinfo]
6
8
  end
7
9
 
8
10
  def show # rubocop:todo Metrics/AbcSize
11
+ skip_authorization
12
+
9
13
  @total = @active_halfyear = @active_month = 0
14
+ @has_user_counts = false
10
15
  Federails::Configuration.entity_types.each_value do |config|
11
- next unless config[:include_in_user_count]
16
+ next unless (method = config[:user_count_method]&.to_sym)
12
17
 
18
+ @has_user_counts = true
13
19
  model = config[:class]
14
- @total += model.count
15
- @active_month += model.where(created_at: ((30.days.ago)...Time.current)).count
16
- @active_halfyear += model.where(created_at: ((180.days.ago)...Time.current)).count
20
+ @total += model.send(method, nil)
21
+ @active_month += model.send(method, ((30.days.ago)...Time.current))
22
+ @active_halfyear += model.send(method, ((180.days.ago)...Time.current))
17
23
  end
18
- render formats: [:json]
24
+ render formats: [:nodeinfo]
19
25
  end
20
26
  end
21
27
  end
@@ -2,8 +2,10 @@ require 'fediverse/webfinger'
2
2
 
3
3
  module Federails
4
4
  module Server
5
- class WebFingerController < ServerController
5
+ class WebFingerController < Federails::ServerController
6
6
  def find
7
+ skip_authorization
8
+
7
9
  resource = params.require(:resource)
8
10
  case resource
9
11
  when %r{^https?://.+}
@@ -15,11 +17,13 @@ module Federails
15
17
  end
16
18
  raise ActiveRecord::RecordNotFound if @user.nil?
17
19
 
18
- render formats: [:json]
20
+ render formats: [:jrd]
19
21
  end
20
22
 
21
23
  def host_meta
22
- render content_type: 'application/xrd+xml', formats: [:xml]
24
+ skip_authorization
25
+
26
+ render formats: [:xrd]
23
27
  end
24
28
 
25
29
  # TODO: complete missing endpoints
@@ -1,16 +1,23 @@
1
1
  module Federails
2
- class ApplicationController < ActionController::Base
2
+ class ServerController < ::ActionController::Base # rubocop:disable Rails/ApplicationController
3
3
  include Pundit::Authorization
4
4
 
5
- rescue_from ActiveRecord::RecordNotFound, with: :error_not_found
5
+ after_action :verify_authorized
6
+
7
+ protect_from_forgery with: :null_session
8
+ helper Federails::ServerHelper
6
9
 
7
- layout Federails.configuration.app_layout if Federails.configuration.app_layout
10
+ rescue_from ActiveRecord::RecordNotFound, with: :error_not_found
8
11
 
9
12
  private
10
13
 
11
14
  def error_fallback(exception, fallback_message, status)
12
15
  message = exception&.message || fallback_message
13
16
  respond_to do |format|
17
+ format.jrd { head status }
18
+ format.xrd { head status }
19
+ format.activitypub { head status }
20
+ format.nodeinfo { head status }
14
21
  format.json { render json: { error: message }, status: status }
15
22
  format.html { raise exception }
16
23
  end
@@ -0,0 +1,12 @@
1
+ module Federails
2
+ module ServerHelper
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
11
+ end
12
+ end
@@ -2,39 +2,91 @@ 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
+
17
+ # Define a method that will be called after an activity has been received
18
+ # @param activity_type [String] The activity action to handle, e.g. 'Create'. If you specify '*', the handler will be called for any activity type.
19
+ # @param object_type [String] The object type to handle, e.g. 'Note'. If you specify '*', the handler will be called for any object type.
20
+ # @param method [Symbol] The name of the class method to call. The method will receive the complete activity payload as a parameter.
21
+ # @example
22
+ # after_activity_received 'Create', 'Note', :create_note
23
+ def self.after_activity_received(activity_type, object_type, method)
24
+ Fediverse::Inbox.register_handler(activity_type, object_type, self, method)
25
+ end
26
+
6
27
  has_one :actor, class_name: 'Federails::Actor', as: :entity, dependent: :destroy
7
28
 
8
- after_create :create_actor
29
+ after_create :create_actor, if: lambda {
30
+ raise("Entity not configured for #{self.class.name}. Did you use \"acts_as_federails_actor\"?") unless Federails::Configuration.entity_types.key? self.class.name
31
+
32
+ Federails::Configuration.entity_types[self.class.name][:auto_create_actors]
33
+ }
9
34
 
10
35
  # Configures the mapping between entity and actor
11
36
  # @param username_field [Symbol] The method or attribute name that returns the preferred username for ActivityPub
12
37
  # @param name_field [Symbol] The method or attribute name that returns the preferred name for ActivityPub
13
38
  # @param profile_url_method [Symbol] The route method name that will generate the profile URL for ActivityPub
14
39
  # @param actor_type [String] The ActivityStreams Actor type for this entity; defaults to 'Person'
15
- # @param include_in_user_count [boolean] Should this entity be included in the nodeinfo user count? Defaults to true
40
+ # @param user_count_method [Symbol] A class method to call to count active users. Leave unspecified to leave this
41
+ # entity out of user counts. Method signature should accept a single parameter which will specify a date range
42
+ # If parameter is nil, the total user count should be returned. If the parameter is specified, the number of users
43
+ # active during the time period should be returned.
44
+ # @param auto_create_actors [Boolean] Whether to automatically create an actor when the entity is created
16
45
  # @example
17
46
  # acts_as_federails_actor username_field: :username, name_field: :display_name, profile_url_method: :url_for, actor_type: 'Person'
47
+ # rubocop:disable Metrics/ParameterLists
18
48
  def self.acts_as_federails_actor(
19
- username_field: Federails::Configuration.user_username_field,
20
- name_field: Federails::Configuration.user_name_field,
21
- profile_url_method: Federails.configuration.user_profile_url_method,
49
+ name_field:,
50
+ username_field:,
51
+ profile_url_method: nil,
22
52
  actor_type: 'Person',
23
- include_in_user_count: true
53
+ user_count_method: nil,
54
+ auto_create_actors: true
24
55
  )
25
56
  Federails::Configuration.register_entity(
26
57
  self,
27
- username_field: username_field,
28
- name_field: name_field,
29
- profile_url_method: profile_url_method,
30
- actor_type: actor_type,
31
- include_in_user_count: include_in_user_count
58
+ username_field: username_field,
59
+ name_field: name_field,
60
+ profile_url_method: profile_url_method,
61
+ actor_type: actor_type,
62
+ user_count_method: user_count_method,
63
+ auto_create_actors: auto_create_actors
32
64
  )
33
65
  end
66
+ # rubocop:enable Metrics/ParameterLists
34
67
 
35
- # Automatically run default acts_as_federails_actor
36
- # this can be optionally called again with different configuration in the entity
37
- acts_as_federails_actor
68
+ # Add custom data to actor responses.
69
+ # Override in your own model to add extra data, which will be merged into the actor response
70
+ # generated by Federails. You can include extra `@context` for activitypub extensions and it will
71
+ # be merged with the main response context.
72
+ # @example
73
+ # def to_activitypub_object
74
+ # {
75
+ # "@context": {
76
+ # toot: "http://joinmastodon.org/ns#",
77
+ # attributionDomains: {
78
+ # "@id": "toot:attributionDomains",
79
+ # "@type": "@id"
80
+ # }
81
+ # },
82
+ # attributionDomains: [
83
+ # "example.com"
84
+ # ]
85
+ # }
86
+ # end
87
+ def to_activitypub_object
88
+ {}
89
+ end
38
90
 
39
91
  private
40
92
 
@@ -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 # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
18
+ def recipients
17
19
  return [] unless actor.local?
18
20
 
19
- actors = []
20
- case action
21
- when 'Create'
22
- actors.push(entity.target_actor) if entity_type == 'Federails::Following'
23
- # FIXME: Move this to dummy, somehow
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 }
@@ -89,7 +91,9 @@ module Federails
89
91
  end
90
92
 
91
93
  def entity_configuration
92
- Federails::Configuration.entity_types[entity.class.name]
94
+ raise("Entity not configured for #{entity_type}. Did you use \"acts_as_federails_actor\"?") unless Federails::Configuration.entity_types.key? entity_type
95
+
96
+ Federails::Configuration.entity_types[entity_type]
93
97
  end
94
98
 
95
99
  class << self
@@ -112,7 +116,7 @@ module Federails
112
116
 
113
117
  def find_by_federation_url(federated_url)
114
118
  local_route = Utils::Host.local_route federated_url
115
- return find local_route[:id] if local_route && local_route[:controller] == 'federails/server/actors' && local_route[:action] == 'show'
119
+ return find_param(local_route[:id]) if local_route && local_route[:controller] == 'federails/server/actors' && local_route[:action] == 'show'
116
120
 
117
121
  actor = find_by federated_url: federated_url
118
122
  return actor if actor
@@ -148,5 +152,40 @@ module Federails
148
152
  end
149
153
  end
150
154
  end
155
+
156
+ def public_key
157
+ ensure_key_pair_exists!
158
+ self[:public_key]
159
+ end
160
+
161
+ def private_key
162
+ ensure_key_pair_exists!
163
+ self[:private_key]
164
+ end
165
+
166
+ def key_id
167
+ "#{federated_url}#main-key"
168
+ end
169
+
170
+ private
171
+
172
+ def ensure_key_pair_exists!
173
+ return if self[:private_key].present? || !local?
174
+
175
+ update!(generate_key_pair)
176
+ end
177
+
178
+ def generate_key_pair
179
+ rsa_key = OpenSSL::PKey::RSA.new 2048
180
+ cipher = OpenSSL::Cipher.new('AES-128-CBC')
181
+ {
182
+ private_key: if Rails.application.credentials.secret_key_base
183
+ rsa_key.to_pem(cipher, Rails.application.credentials.secret_key_base)
184
+ else
185
+ rsa_key.to_pem
186
+ end,
187
+ public_key: rsa_key.public_key.to_pem,
188
+ }
189
+ end
151
190
  end
152
191
  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