federails 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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