federails 0.5.0 → 0.6.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +26 -1
  3. data/app/controllers/federails/client/actors_controller.rb +18 -4
  4. data/app/controllers/federails/server/actors_controller.rb +4 -1
  5. data/app/controllers/federails/server/published_controller.rb +1 -1
  6. data/app/controllers/federails/server/web_finger_controller.rb +9 -6
  7. data/app/controllers/federails/server_controller.rb +7 -0
  8. data/app/models/concerns/federails/actor_entity.rb +40 -8
  9. data/app/models/concerns/federails/data_entity.rb +49 -22
  10. data/app/models/concerns/federails/handles_delete_requests.rb +31 -0
  11. data/app/models/federails/activity.rb +15 -4
  12. data/app/models/federails/actor.rb +87 -31
  13. data/app/models/federails/following.rb +28 -9
  14. data/app/views/federails/client/actors/_actor.json.jbuilder +4 -1
  15. data/app/views/federails/client/actors/gone.html.erb +1 -0
  16. data/app/views/federails/client/actors/index.html.erb +7 -1
  17. data/app/views/federails/server/activities/_activity.activitypub.jbuilder +8 -3
  18. data/app/views/federails/server/actors/_actor.activitypub.jbuilder +2 -2
  19. data/app/views/federails/server/actors/_tombstone.activitypub.jbuilder +9 -0
  20. data/app/views/federails/server/actors/show.activitypub.jbuilder +5 -1
  21. data/app/views/federails/server/published/_tombstone.activitypub.jbuilder +9 -0
  22. data/app/views/federails/server/published/show.activitypub.jbuilder +5 -1
  23. data/db/migrate/20250122160618_add_extensions_to_federails_actors.rb +5 -0
  24. data/db/migrate/20250301082500_add_local_to_actors.rb +11 -0
  25. data/db/migrate/20250329123939_add_actor_type_to_actors.rb +5 -0
  26. data/db/migrate/20250329123940_add_tombstoned_at_to_actors.rb +5 -0
  27. data/lib/federails/configuration.rb +5 -1
  28. data/lib/federails/data_transformer/note.rb +1 -1
  29. data/lib/federails/maintenance/actors_updater.rb +67 -0
  30. data/lib/federails/utils/actor.rb +53 -0
  31. data/lib/federails/utils/object.rb +25 -0
  32. data/lib/federails/version.rb +1 -1
  33. data/lib/fediverse/inbox.rb +25 -4
  34. data/lib/fediverse/webfinger.rb +19 -23
  35. data/lib/tasks/federails_tasks.rake +8 -4
  36. metadata +13 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e6d475999161590317e4a038b44829fdae2a44ccb007b03f24c240fccfa0771
4
- data.tar.gz: b2266e7c4e9ac8c68177e2b17fc2e74afab52fc0e93607943716e06b39c10f6d
3
+ metadata.gz: '0538a0c2d7f7e3845a5d8ae304ac2b319b3cf1e76b10520645773f8f763c3431'
4
+ data.tar.gz: e97c559ecc5aba1b1c05c9be5f39b982a22e92cb8f73ae8c8a08741a3efb3d01
5
5
  SHA512:
6
- metadata.gz: 14c482129bac20d5dfdaa2fa498070ab215819a0fe6af6499e268d631f40a7c15ee3341e999e6b0bfee5fd997b7a41f879c35537aeebbfbbdfc6182289d5edf4
7
- data.tar.gz: 0c6fe55888c7b31f93958e258ad4b137aa305269056b2b512af2a137e2a15ac6de36a0a83f7fbec436c6657fca3c4ad9c18c08bb46a93e48601ef226c9a0011c
6
+ metadata.gz: 560377c5b698b87274d1df302595c33467f010402096227583161087aaf124b0a9e2820f44234b2932c158e84b0ab8ccb79ea8501557840509a416c7b8205295
7
+ data.tar.gz: 6380fc79556446afbd4a81e3e5e2e6b49bdb5877576dd1038208c1c4471cd3986bb35bbfaae1a9f37e618ddf5ab14dcf6974f512f6a609c70c2de0b7c251803f
data/README.md CHANGED
@@ -23,12 +23,37 @@ The general direction is to be able to:
23
23
  - implement some or all the parts of the RFC labelled with **SHOULD** and **SHOULD NOT**
24
24
  - maybe implement the parts of the RFC labelled with **MAY**
25
25
 
26
+ ## Supported Ruby on Rails versions
27
+
28
+ This gem is tested against non end-of-life versions of Ruby and Rails:
29
+
30
+ - Ruby versions 3.1 to 3.4
31
+ - Rails 7.1 to 8.0.x.
32
+
33
+ Feel free to open an issue if we missed something
34
+
35
+ It _may_ work on other versions, but we won't provide support.
36
+
26
37
  ## Documentation
27
38
 
28
39
  - [Usage](docs/usage.md)
29
40
  - [Common questions](docs/faq.md)
30
- - [Contributing](docs/contributing.md)
41
+ - [Contributing](CONTRIBUTING.md)
31
42
 
32
43
  ## License
33
44
 
34
45
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
46
+
47
+ ## Contributing
48
+
49
+ See [CONTRIBUTING](CONTRIBUTING.md) to have an overview of the process and the tools we use.
50
+
51
+ ### Contributors
52
+
53
+ - [echarp](https://gitlab.com/echarp)
54
+ - [James Smith](https://gitlab.com/floppy.uk)
55
+ - [Manuel Tancoigne](https://gitlab.com/mtancoigne)
56
+
57
+ ### Indirect contributions
58
+
59
+ - Gitlab runners are graciously provided by [Coopaname](https://coopaname.coop), a French cooperative.
@@ -14,14 +14,16 @@ module Federails
14
14
 
15
15
  # GET /app/actors/1
16
16
  # GET /app/actors/1.json
17
- def show; end
17
+ def show
18
+ render_show
19
+ end
18
20
 
19
- # GET /app/explorer/lookup
20
- # GET /app/explorer/lookup.json
21
+ # GET /app/actors/lookup
22
+ # GET /app/actors/lookup.json
21
23
  def lookup
22
24
  @actor = Federails::Actor.find_by_account account_param
23
25
  authorize @actor, policy_class: Federails::Client::ActorPolicy
24
- render :show
26
+ render_show
25
27
  end
26
28
 
27
29
  private
@@ -35,6 +37,18 @@ module Federails
35
37
  def account_param
36
38
  params.require('account')
37
39
  end
40
+
41
+ def render_show
42
+ respond_to do |format|
43
+ if @actor.tombstoned?
44
+ format.html { render :gone, status: :gone }
45
+ format.json { render json: { error: I18n.t('controller.actors.gone') }, status: :gone }
46
+ else
47
+ format.html { render :show }
48
+ format.json { render :show }
49
+ end
50
+ end
51
+ end
38
52
  end
39
53
  end
40
54
  end
@@ -5,7 +5,10 @@ module Federails
5
5
 
6
6
  # GET /federation/actors/1
7
7
  # GET /federation/actors/1.json
8
- def show; end
8
+ def show
9
+ status = @actor.tombstoned? ? :gone : :ok
10
+ render :show, status: status
11
+ end
9
12
 
10
13
  # GET /federation/actors/:id/followers
11
14
  # GET /federation/actors/:id/followers.json
@@ -3,7 +3,7 @@ module Federails
3
3
  # Controller to render ActivityPub representation of entities configured with Federails::DataEntity
4
4
  class PublishedController < Federails::ServerController
5
5
  def show
6
- @publishable = type_scope.find_by!(url_param => params[:id])
6
+ @publishable = type_scope.find_untombstoned_by!(url_param => params[:id])
7
7
  authorize @publishable, policy_class: Federails::Server::PublishablePolicy
8
8
  end
9
9
 
@@ -9,11 +9,12 @@ module Federails
9
9
  resource = params.require(:resource)
10
10
  case resource
11
11
  when %r{^https?://.+}
12
- @user = Federails::Actor.find_by_federation_url(resource)&.entity
12
+ @user = Federails::Actor.find_by_federation_url!(resource).entity # rubocop:disable Rails/DynamicFindBy
13
13
  when /^acct:.+/
14
- Federails::Configuration.actor_types.each_value do |entity|
15
- @user ||= entity[:class].find_by(entity[:username_field] => username)
16
- end
14
+ actor = Federails::Actor.find_local_by_username(username)
15
+ raise Federails::Actor::TombstonedError if actor&.tombstoned?
16
+
17
+ @user = actor&.entity
17
18
  end
18
19
  raise ActiveRecord::RecordNotFound if @user.nil?
19
20
 
@@ -31,11 +32,13 @@ module Federails
31
32
  private
32
33
 
33
34
  def username
34
- account = Fediverse::Webfinger.split_resource_account params.require(:resource)
35
+ return @username if instance_variable_defined? :@username
36
+
37
+ account = Fediverse::Webfinger.split_account params.require(:resource)
35
38
  # Fail early if user don't _seems_ local
36
39
  raise ActiveRecord::RecordNotFound unless account && Fediverse::Webfinger.local_user?(account)
37
40
 
38
- account[:username]
41
+ @username = account[:username]
39
42
  end
40
43
  end
41
44
  end
@@ -8,6 +8,9 @@ module Federails
8
8
  helper Federails::ServerHelper
9
9
 
10
10
  rescue_from ActiveRecord::RecordNotFound, with: :error_not_found
11
+ rescue_from Federails::Actor::TombstonedError,
12
+ Federails::DataEntity::TombstonedError,
13
+ with: :error_gone
11
14
 
12
15
  private
13
16
 
@@ -26,5 +29,9 @@ module Federails
26
29
  def error_not_found(exception = nil)
27
30
  error_fallback(exception, 'Resource not found', :not_found)
28
31
  end
32
+
33
+ def error_gone(exception = nil)
34
+ error_fallback(exception, 'Resource is gone', :gone)
35
+ end
29
36
  end
30
37
  end
@@ -59,14 +59,26 @@ module Federails
59
59
  end
60
60
  # rubocop:enable Metrics/ParameterLists
61
61
 
62
- # Define a method that will be called after the entity receives a follow request
62
+ # Define a method that will be called after the entity receives a follow request.
63
+ # The follow request will be passed as an argument to the method.
63
64
  #
64
65
  # @param method_name [Symbol] The name of the method to call, or a block that will be called directly
65
66
  #
66
67
  # @example
67
68
  # after_followed :accept_follow
68
69
  def after_followed(method_name)
69
- set_callback :followed, :after, method_name
70
+ @after_followed = method_name
71
+ end
72
+
73
+ # Define a method that will be called after a follow request made by the entity is accepted
74
+ # The accepted follow request will be passed as an argument to the method.
75
+ #
76
+ # @param method_name [Symbol] The name of the method to call, or a block that will be called directly
77
+ #
78
+ # @example
79
+ # after_follow_accepted :follow_accepted
80
+ def after_follow_accepted(method_name)
81
+ @after_follow_accepted = method_name
70
82
  end
71
83
 
72
84
  # Define a method that will be called after an activity has been received
@@ -80,20 +92,29 @@ module Federails
80
92
  def after_activity_received(activity_type, object_type, method_name)
81
93
  Fediverse::Inbox.register_handler(activity_type, object_type, self, method_name)
82
94
  end
83
- end
84
95
 
85
- included do
86
- include ActiveSupport::Callbacks
96
+ private
87
97
 
88
- define_callbacks :followed
98
+ def dispatch_callback(name, instance, *args)
99
+ case name
100
+ when :after_followed
101
+ instance.send(@after_followed, *args) if @after_followed
102
+ when :after_follow_accepted
103
+ instance.send(@after_follow_accepted, *args) if @after_follow_accepted
104
+ end
105
+ end
106
+ end
89
107
 
90
- has_one :federails_actor, class_name: 'Federails::Actor', as: :entity, dependent: :destroy
108
+ included do
109
+ # No "dependent: :xyz" as the "before_destroy" hook should have nullified the actor
110
+ has_one :federails_actor, class_name: 'Federails::Actor', as: :entity # rubocop:disable Rails/HasManyOrHasOneDependent
91
111
 
92
112
  after_create :create_federails_actor, if: lambda {
93
113
  raise("Entity not configured for #{self.class.name}. Did you use \"acts_as_federails_actor\"?") unless Federails.actor_entity? self
94
114
 
95
115
  Federails.actor_entity(self)[:auto_create_actors]
96
116
  }
117
+ before_destroy :tombstone_federails_actor!
97
118
  end
98
119
 
99
120
  # Add custom data to actor responses.
@@ -123,8 +144,19 @@ module Federails
123
144
 
124
145
  private
125
146
 
147
+ # Result is used to determine if an actor related to this entity should be created as local actor or not
148
+ #
149
+ # Override it in your models if you need distant actors to be related to another entity.
150
+ def create_federails_actor_as_local?
151
+ true
152
+ end
153
+
126
154
  def create_federails_actor
127
- Federails::Actor.create! entity: self
155
+ Federails::Actor.create_with(local: create_federails_actor_as_local?).find_or_create_by!(entity: self)
156
+ end
157
+
158
+ def tombstone_federails_actor!
159
+ federails_actor.tombstone!
128
160
  end
129
161
  end
130
162
  end
@@ -25,10 +25,17 @@ module Federails
25
25
  # class Post < ApplicationRecord
26
26
  # include Federails::DataEntity
27
27
  # acts_as_federails_data options
28
+ #
29
+ # # This will be called when a Delete activity comes for the entry. As we don't know how you want to handle it,
30
+ # # you'll have to implement the behavior yourself.
31
+ # on_federails_delete_requested :do_something
28
32
  # end
29
33
  # ```
30
34
  module DataEntity
35
+ class TombstonedError < StandardError; end
36
+
31
37
  extend ActiveSupport::Concern
38
+ include Federails::HandlesDeleteRequests
32
39
 
33
40
  # Class methods automatically included in the concern.
34
41
  module ClassMethods
@@ -49,10 +56,14 @@ module Federails
49
56
  # @param should_federate_method [Symbol] method to determine if an object should be federated. If the method returns false,
50
57
  # no create/update activities will happen, and object will not be accessible at federated_url. Defaults to a method
51
58
  # that always returns true.
59
+ # @param soft_deleted_method [Symbol, nil] Method to soft-delete the object when receiving a Delete request. This
60
+ # is not required by the spec but greatly encouraged as the app will return a 410 response with a Tombstone object
61
+ # instead of an 404 error.
62
+ # @param soft_delete_date_method [Symbol, nil] Method to get the date of the soft-deletion
52
63
  #
53
64
  # @example
54
65
  # acts_as_federails_data handles: 'Note', with: :note_handler, route_path_segment: :articles, actor_entity_method: :user
55
- # rubocop:disable Metrics/ParameterLists
66
+ # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
56
67
  def acts_as_federails_data(
57
68
  handles:,
58
69
  with: :handle_incoming_fediverse_data,
@@ -60,23 +71,28 @@ module Federails
60
71
  actor_entity_method: nil,
61
72
  url_param: :id,
62
73
  filter_method: nil,
63
- should_federate_method: :default_should_federate?
74
+ should_federate_method: :default_should_federate?,
75
+ soft_deleted_method: nil,
76
+ soft_delete_date_method: nil
64
77
  )
65
78
  route_path_segment ||= name.pluralize.underscore
66
79
 
67
80
  Federails::Configuration.register_data_type self,
68
- route_path_segment: route_path_segment,
69
- actor_entity_method: actor_entity_method,
70
- url_param: url_param,
71
- handles: handles,
72
- with: with,
73
- filter_method: filter_method,
74
- should_federate_method: should_federate_method
75
-
81
+ route_path_segment: route_path_segment,
82
+ actor_entity_method: actor_entity_method,
83
+ url_param: url_param,
84
+ handles: handles,
85
+ with: with,
86
+ filter_method: filter_method,
87
+ should_federate_method: should_federate_method,
88
+ soft_deleted_method: soft_deleted_method,
89
+ soft_delete_date_method: soft_delete_date_method
90
+
91
+ # NOTE: Delete activities cannot be handled like this as we can't be sure to have the object's type
76
92
  Fediverse::Inbox.register_handler 'Create', handles, self, with
77
93
  Fediverse::Inbox.register_handler 'Update', handles, self, with
78
94
  end
79
- # rubocop:enable Metrics/ParameterLists
95
+ # rubocop:enable Metrics/ParameterLists, Metrics/MethodLength
80
96
 
81
97
  # Instantiates a new instance from an ActivityPub object
82
98
  #
@@ -107,6 +123,15 @@ module Federails
107
123
 
108
124
  entity
109
125
  end
126
+
127
+ def find_untombstoned_by!(**params)
128
+ configuration = Federails.data_entity_configuration(self)
129
+ entity = find_by!(**params)
130
+
131
+ raise Federails::DataEntity::TombstonedError if configuration[:soft_deleted_method] && entity.send(configuration[:soft_deleted_method])
132
+
133
+ entity
134
+ end
110
135
  end
111
136
 
112
137
  included do
@@ -116,8 +141,9 @@ module Federails
116
141
  scope :distant_federails_entities, -> { where.not(federated_url: nil) }
117
142
 
118
143
  before_validation :set_federails_actor
119
- after_create :create_federails_activity
120
- after_update :update_federails_activity
144
+ after_create -> { create_federails_activity 'Create' }
145
+ after_update -> { create_federails_activity 'Update' }, :federails_tombstoned?
146
+ after_destroy -> { create_federails_activity 'Delete' }
121
147
  end
122
148
 
123
149
  # Computed value for the federated URL
@@ -139,6 +165,14 @@ module Federails
139
165
  attributes['federated_url'].blank?
140
166
  end
141
167
 
168
+ def federails_tombstoned?
169
+ federails_data_configuration[:soft_deleted_method] ? send(federails_data_configuration[:soft_deleted_method]) : false
170
+ end
171
+
172
+ def federails_tombstoned_at
173
+ federails_data_configuration[:soft_delete_date_method] ? send(federails_data_configuration[:soft_delete_date_method]) : nil
174
+ end
175
+
142
176
  def federails_data_configuration
143
177
  Federails.data_entity_configuration(self)
144
178
  end
@@ -153,18 +187,11 @@ module Federails
153
187
  raise 'Cannot determine actor from configuration' unless federails_actor
154
188
  end
155
189
 
156
- def create_federails_activity
157
- ensure_federails_configuration!
158
- return unless local_federails_entity? && send(federails_data_configuration[:should_federate_method])
159
-
160
- Activity.create! actor: federails_actor, action: 'Create', entity: self
161
- end
162
-
163
- def update_federails_activity
190
+ def create_federails_activity(action)
164
191
  ensure_federails_configuration!
165
192
  return unless local_federails_entity? && send(federails_data_configuration[:should_federate_method])
166
193
 
167
- Activity.create! actor: federails_actor, action: 'Update', entity: self
194
+ Activity.create! actor: federails_actor, action: action, entity: self
168
195
  end
169
196
 
170
197
  def ensure_federails_configuration!
@@ -0,0 +1,31 @@
1
+ module Federails
2
+ # Model concern providing hooks for on_federails_delete_requested callback
3
+ #
4
+ # ```rb
5
+ # # Example migration
6
+ # add_column :my_table, :uuid, :text, default: nil, index: { unique: true }
7
+ # ```
8
+ #
9
+ # Usage:
10
+ #
11
+ # ```rb
12
+ # class MyModel < ApplicationRecord
13
+ # include Federails::HandlesDeleteRequests
14
+ #
15
+ # on_federails_delete_requested -> { delete! }
16
+ # end
17
+ module HandlesDeleteRequests
18
+ extend ActiveSupport::Concern
19
+
20
+ # Class methods automatically included in the concern.
21
+ module ClassMethods
22
+ def on_federails_delete_requested(*args)
23
+ set_callback :on_federails_delete_requested, *args
24
+ end
25
+ end
26
+
27
+ included do
28
+ define_callbacks :on_federails_delete_requested
29
+ end
30
+ end
31
+ end
@@ -30,16 +30,27 @@ module Federails
30
30
  def recipients
31
31
  return [] unless actor.local?
32
32
 
33
- case entity_type
34
- when 'Federails::Following'
35
- [(action == 'Accept' ? entity.actor : entity.target_actor)]
33
+ case action
34
+ when 'Follow'
35
+ [entity]
36
+ when 'Undo'
37
+ [entity.entity]
38
+ when 'Accept'
39
+ [entity.actor]
36
40
  else
37
- actor.followers
41
+ default_recipient_list
38
42
  end
39
43
  end
40
44
 
41
45
  private
42
46
 
47
+ def default_recipient_list
48
+ list = actor.followers
49
+ # If local actor is the subject, notify that actor's followers as well
50
+ list += entity.followers if entity.is_a?(Federails::Actor) && entity.local?
51
+ list.uniq
52
+ end
53
+
43
54
  def post_to_inboxes
44
55
  NotifyInboxJob.perform_later(self)
45
56
  end
@@ -1,4 +1,5 @@
1
1
  require 'federails/utils/host'
2
+ require 'federails/utils/actor'
2
3
  require 'fediverse/webfinger'
3
4
 
4
5
  module Federails
@@ -9,17 +10,22 @@ module Federails
9
10
  # See also:
10
11
  # - https://www.w3.org/TR/activitypub/#actor-objects
11
12
  class Actor < ApplicationRecord # rubocop:disable Metrics/ClassLength
12
- include Federails::HasUuid
13
+ class TombstonedError < StandardError; end
13
14
 
14
- validates :federated_url, presence: { unless: :entity }, uniqueness: { unless: :entity }
15
- validates :username, presence: { unless: :entity }
16
- validates :server, presence: { unless: :entity }
17
- validates :inbox_url, presence: { unless: :entity }
18
- validates :outbox_url, presence: { unless: :entity }
19
- validates :followers_url, presence: { unless: :entity }
20
- validates :followings_url, presence: { unless: :entity }
21
- validates :profile_url, presence: { unless: :entity }
22
- validates :entity_id, uniqueness: { scope: :entity_type }, if: :local?
15
+ include Federails::HasUuid
16
+ include Federails::HandlesDeleteRequests
17
+
18
+ validates :federated_url, presence: { unless: :entity }, uniqueness: { unless: :local? }
19
+ validates :username, presence: { unless: :local? }
20
+ validates :server, presence: { unless: :local? }
21
+ validates :inbox_url, presence: { unless: :local? }
22
+ validates :outbox_url, presence: { unless: :local? }
23
+ validates :followers_url, presence: { unless: :local? }
24
+ validates :followings_url, presence: { unless: :local? }
25
+ validates :profile_url, presence: { unless: :local? }
26
+ validates :actor_type, presence: { unless: :local? }
27
+ validates :entity_id, uniqueness: { scope: :entity_type }, if: :entity_type
28
+ validates :entity, presence: true, if: -> { local? && !tombstoned? }
23
29
 
24
30
  belongs_to :entity, polymorphic: true, optional: true
25
31
  # FIXME: Handle this with something like undelete
@@ -30,51 +36,59 @@ module Federails
30
36
  has_many :followers, source: :actor, through: :following_followers
31
37
  has_many :follows, source: :target_actor, through: :following_follows
32
38
 
33
- scope :local, -> { where.not(entity: nil) }
34
- scope :distant, -> { where.not(federated_url: nil) }
39
+ scope :local, -> { where(local: true) }
40
+ scope :distant, -> { where(local: false) }
41
+ scope :tombstoned, -> { where.not(tombstoned_at: nil) }
42
+ scope :not_tombstoned, -> { where(tombstoned_at: nil) }
35
43
 
36
- def local?
37
- entity.present?
44
+ on_federails_delete_requested -> { tombstone! }
45
+
46
+ def distant?
47
+ !local?
38
48
  end
39
49
 
40
50
  def federated_url
41
- local? ? Federails::Engine.routes.url_helpers.server_actor_url(self) : attributes['federated_url'].presence
51
+ use_entity_attributes? ? Federails::Engine.routes.url_helpers.server_actor_url(self) : attributes['federated_url'].presence
42
52
  end
43
53
 
44
54
  def username
45
- return attributes['username'] unless local?
55
+ return attributes['username'] unless use_entity_attributes?
46
56
 
47
57
  entity.send(entity_configuration[:username_field]).to_s
48
58
  end
49
59
 
50
60
  def name
51
- value = (entity.send(entity_configuration[:name_field]).to_s if local?)
61
+ value = (entity.send(entity_configuration[:name_field]).to_s if use_entity_attributes?)
52
62
 
53
63
  value || attributes['name'] || username
54
64
  end
55
65
 
56
66
  def server
57
- local? ? Utils::Host.localhost : attributes['server']
67
+ use_entity_attributes? ? Utils::Host.localhost : attributes['server']
68
+ end
69
+
70
+ def actor_type
71
+ use_entity_attributes? ? entity_configuration[:actor_type] : attributes['actor_type']
58
72
  end
59
73
 
60
74
  def inbox_url
61
- local? ? Federails::Engine.routes.url_helpers.server_actor_inbox_url(self) : attributes['inbox_url']
75
+ use_entity_attributes? ? Federails::Engine.routes.url_helpers.server_actor_inbox_url(self) : attributes['inbox_url']
62
76
  end
63
77
 
64
78
  def outbox_url
65
- local? ? Federails::Engine.routes.url_helpers.server_actor_outbox_url(self) : attributes['outbox_url']
79
+ use_entity_attributes? ? Federails::Engine.routes.url_helpers.server_actor_outbox_url(self) : attributes['outbox_url']
66
80
  end
67
81
 
68
82
  def followers_url
69
- local? ? Federails::Engine.routes.url_helpers.followers_server_actor_url(self) : attributes['followers_url']
83
+ use_entity_attributes? ? Federails::Engine.routes.url_helpers.followers_server_actor_url(self) : attributes['followers_url']
70
84
  end
71
85
 
72
86
  def followings_url
73
- local? ? Federails::Engine.routes.url_helpers.following_server_actor_url(self) : attributes['followings_url']
87
+ use_entity_attributes? ? Federails::Engine.routes.url_helpers.following_server_actor_url(self) : attributes['followings_url']
74
88
  end
75
89
 
76
90
  def profile_url
77
- return attributes['profile_url'].presence unless local?
91
+ return attributes['profile_url'].presence unless use_entity_attributes?
78
92
 
79
93
  method = entity_configuration[:profile_url_method]
80
94
  return Federails::Engine.routes.url_helpers.server_actor_url self unless method
@@ -83,11 +97,15 @@ module Federails
83
97
  end
84
98
 
85
99
  def at_address
86
- "#{username}@#{server}"
100
+ "@#{username}@#{server}"
87
101
  end
88
102
 
89
103
  def short_at_address
90
- local? ? "@#{username}" : at_address
104
+ use_entity_attributes? ? "@#{username}" : at_address
105
+ end
106
+
107
+ def acct_uri
108
+ "acct:#{username}@#{server}"
91
109
  end
92
110
 
93
111
  def follows?(actor)
@@ -103,16 +121,24 @@ module Federails
103
121
  Federails.actor_entity entity_type
104
122
  end
105
123
 
124
+ def tombstoned?
125
+ tombstoned_at.present?
126
+ end
127
+
128
+ def tombstone!
129
+ Federails::Utils::Actor.tombstone! self
130
+ end
131
+
106
132
  class << self
107
- def find_by_account(account) # rubocop:todo Metrics/AbcSize
133
+ # Searches for an actor from account URI
134
+ #
135
+ # @param account [String] Account URI (username@host)
136
+ # @return [Federails::Actor, nil]
137
+ def find_by_account(account)
108
138
  parts = Fediverse::Webfinger.split_account account
109
139
 
110
140
  if Fediverse::Webfinger.local_user? parts
111
- actor = nil
112
- Federails::Configuration.actor_types.each_value do |entity|
113
- actor ||= entity[:class].find_by(entity[:username_field] => parts[:username])&.federails_actor
114
- end
115
- raise ActiveRecord::RecordNotFound if actor.nil?
141
+ actor = find_local_by_username! parts[:username]
116
142
  else
117
143
  actor = find_by username: parts[:username], server: parts[:domain]
118
144
  actor ||= Fediverse::Webfinger.fetch_actor(parts[:username], parts[:domain])
@@ -131,6 +157,13 @@ module Federails
131
157
  Fediverse::Webfinger.fetch_actor_url(federated_url)
132
158
  end
133
159
 
160
+ def find_by_federation_url!(federated_url)
161
+ find_by_federation_url(federated_url).tap do |actor|
162
+ raise Federails::Actor::TombstonedError if actor.tombstoned?
163
+ raise ActiveRecord::RecordNotFound if actor.nil?
164
+ end
165
+ end
166
+
134
167
  def find_or_create_by_account(account)
135
168
  actor = find_by_account account
136
169
  # Create/update distant actors
@@ -158,6 +191,25 @@ module Federails
158
191
  raise "Unsupported object type for actor (#{object.class})"
159
192
  end
160
193
  end
194
+
195
+ def find_local_by_username(username)
196
+ actor = nil
197
+ Federails::Configuration.actor_types.each_value do |entity|
198
+ break if actor.present?
199
+
200
+ actor = entity[:class].find_by(entity[:username_field] => username)&.federails_actor
201
+ end
202
+ return actor if actor
203
+
204
+ # Last hope: Search for tombstoned actors
205
+ Federails::Actor.local.tombstoned.find_by username: username
206
+ end
207
+
208
+ def find_local_by_username!(username)
209
+ find_local_by_username(username).tap do |actor|
210
+ raise ActiveRecord::RecordNotFound if actor.nil?
211
+ end
212
+ end
161
213
  end
162
214
 
163
215
  def public_key
@@ -194,5 +246,9 @@ module Federails
194
246
  public_key: rsa_key.public_key.to_pem,
195
247
  }
196
248
  end
249
+
250
+ def use_entity_attributes?
251
+ local? && !tombstoned?
252
+ end
197
253
  end
198
254
  end