federails 0.4.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/README.md +19 -189
  4. data/app/controllers/federails/client/actors_controller.rb +18 -4
  5. data/app/controllers/federails/server/actors_controller.rb +4 -1
  6. data/app/controllers/federails/server/published_controller.rb +30 -0
  7. data/app/controllers/federails/server/web_finger_controller.rb +9 -6
  8. data/app/controllers/federails/server_controller.rb +7 -0
  9. data/app/models/concerns/federails/actor_entity.rb +120 -56
  10. data/app/models/concerns/federails/data_entity.rb +205 -0
  11. data/app/models/concerns/federails/handles_delete_requests.rb +31 -0
  12. data/app/models/concerns/federails/has_uuid.rb +27 -1
  13. data/app/models/federails/activity.rb +27 -4
  14. data/app/models/federails/actor.rb +93 -30
  15. data/app/models/federails/following.rb +29 -9
  16. data/app/policies/federails/server/publishable_policy.rb +15 -0
  17. data/app/views/federails/client/actors/_actor.json.jbuilder +4 -1
  18. data/app/views/federails/client/actors/gone.html.erb +1 -0
  19. data/app/views/federails/client/actors/index.html.erb +7 -1
  20. data/app/views/federails/server/activities/_activity.activitypub.jbuilder +8 -3
  21. data/app/views/federails/server/actors/_actor.activitypub.jbuilder +2 -2
  22. data/app/views/federails/server/actors/_tombstone.activitypub.jbuilder +9 -0
  23. data/app/views/federails/server/actors/show.activitypub.jbuilder +5 -1
  24. data/app/views/federails/server/published/_publishable.activitypub.jbuilder +11 -0
  25. data/app/views/federails/server/published/_tombstone.activitypub.jbuilder +9 -0
  26. data/app/views/federails/server/published/show.activitypub.jbuilder +5 -0
  27. data/config/routes.rb +4 -0
  28. data/db/migrate/20250122160618_add_extensions_to_federails_actors.rb +5 -0
  29. data/db/migrate/20250301082500_add_local_to_actors.rb +11 -0
  30. data/db/migrate/20250329123939_add_actor_type_to_actors.rb +5 -0
  31. data/db/migrate/20250329123940_add_tombstoned_at_to_actors.rb +5 -0
  32. data/lib/federails/configuration.rb +24 -1
  33. data/lib/federails/data_transformer/note.rb +31 -0
  34. data/lib/federails/maintenance/actors_updater.rb +67 -0
  35. data/lib/federails/utils/actor.rb +53 -0
  36. data/lib/federails/utils/object.rb +131 -0
  37. data/lib/federails/version.rb +1 -1
  38. data/lib/federails.rb +54 -0
  39. data/lib/fediverse/inbox.rb +40 -8
  40. data/lib/fediverse/notifier.rb +3 -0
  41. data/lib/fediverse/request.rb +13 -0
  42. data/lib/fediverse/webfinger.rb +72 -26
  43. data/lib/fediverse.rb +3 -0
  44. data/lib/tasks/federails_tasks.rake +8 -4
  45. metadata +22 -6
@@ -0,0 +1,205 @@
1
+ require 'fediverse/inbox'
2
+
3
+ module Federails
4
+ # Model concern to include in models for which data is pushed to the Fediverse and comes from the Fediverse.
5
+ #
6
+ # Once included, an activity will automatically be created upon
7
+ # - entity creation
8
+ # - entity updates
9
+ #
10
+ # Also, when properly configured, a handler is registered to transform incoming objects and create/update entities
11
+ # accordingly.
12
+ #
13
+ # ## Pre-requisites
14
+ #
15
+ # Model must have a `federated_url` attribute:
16
+ # ```rb
17
+ # add_column :posts, :federated_url, :string, null: true, default: nil
18
+ # ```
19
+ #
20
+ # ## Usage
21
+ #
22
+ # Include the concern in an existing model:
23
+ #
24
+ # ```rb
25
+ # class Post < ApplicationRecord
26
+ # include Federails::DataEntity
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
32
+ # end
33
+ # ```
34
+ module DataEntity
35
+ class TombstonedError < StandardError; end
36
+
37
+ extend ActiveSupport::Concern
38
+ include Federails::HandlesDeleteRequests
39
+
40
+ # Class methods automatically included in the concern.
41
+ module ClassMethods
42
+ # Configures the mapping between entity and Fediverse
43
+ #
44
+ # Model should have the following methods:
45
+ # - `to_activitypub_object`, returning a valid ActivityPub object
46
+ #
47
+ # @param actor_entity_method [Symbol] Method returning an object responding to 'federails_actor', for local content
48
+ # @param url_param [Symbol] Column name of the object ID that should be used in URLs. Defaults to +:id+
49
+ # @param route_path_segment [Symbol] Segment used in Federails routes to display the ActivityPub representation.
50
+ # Defaults to the pluralized, underscored class name
51
+ # @param handles [String] Type of ActivityPub object handled by this entity type
52
+ # @param with [Symbol] Self class method that will handle incoming objects. Defaults to +:handle_incoming_fediverse_data+
53
+ # @param filter_method [Symbol] Self class method that determines if an incoming object should be handled. Note
54
+ # that the first model for which this method returns true will be used. If left empty, the model CAN be selected,
55
+ # so define them if many models handle the same data type.
56
+ # @param should_federate_method [Symbol] method to determine if an object should be federated. If the method returns false,
57
+ # no create/update activities will happen, and object will not be accessible at federated_url. Defaults to a method
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
63
+ #
64
+ # @example
65
+ # acts_as_federails_data handles: 'Note', with: :note_handler, route_path_segment: :articles, actor_entity_method: :user
66
+ # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
67
+ def acts_as_federails_data(
68
+ handles:,
69
+ with: :handle_incoming_fediverse_data,
70
+ route_path_segment: nil,
71
+ actor_entity_method: nil,
72
+ url_param: :id,
73
+ filter_method: nil,
74
+ should_federate_method: :default_should_federate?,
75
+ soft_deleted_method: nil,
76
+ soft_delete_date_method: nil
77
+ )
78
+ route_path_segment ||= name.pluralize.underscore
79
+
80
+ Federails::Configuration.register_data_type self,
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
92
+ Fediverse::Inbox.register_handler 'Create', handles, self, with
93
+ Fediverse::Inbox.register_handler 'Update', handles, self, with
94
+ end
95
+ # rubocop:enable Metrics/ParameterLists, Metrics/MethodLength
96
+
97
+ # Instantiates a new instance from an ActivityPub object
98
+ #
99
+ # @param activitypub_object [Hash]
100
+ #
101
+ # @return [self]
102
+ def new_from_activitypub_object(activitypub_object)
103
+ new from_activitypub_object(activitypub_object)
104
+ end
105
+
106
+ # Creates or updates entity based on the ActivityPub activity
107
+ #
108
+ # @param activity_hash_or_id [Hash, String] Dereferenced activity hash or ID
109
+ #
110
+ # @return [self]
111
+ def handle_incoming_fediverse_data(activity_hash_or_id)
112
+ activity = Fediverse::Request.dereference(activity_hash_or_id)
113
+ object = Fediverse::Request.dereference(activity['object'])
114
+
115
+ entity = Federails::Utils::Object.find_or_create!(object)
116
+
117
+ if activity['type'] == 'Update'
118
+ entity.assign_attributes from_activitypub_object(object)
119
+
120
+ # Use timestamps from attributes
121
+ entity.save! touch: false
122
+ end
123
+
124
+ entity
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
135
+ end
136
+
137
+ included do
138
+ belongs_to :federails_actor, class_name: 'Federails::Actor'
139
+
140
+ scope :local_federails_entities, -> { where federated_url: nil }
141
+ scope :distant_federails_entities, -> { where.not(federated_url: nil) }
142
+
143
+ before_validation :set_federails_actor
144
+ after_create -> { create_federails_activity 'Create' }
145
+ after_update -> { create_federails_activity 'Update' }, :federails_tombstoned?
146
+ after_destroy -> { create_federails_activity 'Delete' }
147
+ end
148
+
149
+ # Computed value for the federated URL
150
+ #
151
+ # @return [String]
152
+ def federated_url
153
+ return nil unless send(federails_data_configuration[:should_federate_method])
154
+ return attributes['federated_url'] if attributes['federated_url'].present?
155
+
156
+ path_segment = Federails.data_entity_configuration(self)[:route_path_segment]
157
+ url_param = Federails.data_entity_configuration(self)[:url_param]
158
+ Federails::Engine.routes.url_helpers.server_published_url(publishable_type: path_segment, id: send(url_param))
159
+ end
160
+
161
+ # Check whether the entity was created locally or comes from the Fediverse
162
+ #
163
+ # @return [Boolean]
164
+ def local_federails_entity?
165
+ attributes['federated_url'].blank?
166
+ end
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
+
176
+ def federails_data_configuration
177
+ Federails.data_entity_configuration(self)
178
+ end
179
+
180
+ private
181
+
182
+ def set_federails_actor
183
+ return federails_actor if federails_actor.present?
184
+
185
+ self.federails_actor = send(federails_data_configuration[:actor_entity_method])&.federails_actor if federails_data_configuration[:actor_entity_method]
186
+
187
+ raise 'Cannot determine actor from configuration' unless federails_actor
188
+ end
189
+
190
+ def create_federails_activity(action)
191
+ ensure_federails_configuration!
192
+ return unless local_federails_entity? && send(federails_data_configuration[:should_federate_method])
193
+
194
+ Activity.create! actor: federails_actor, action: action, entity: self
195
+ end
196
+
197
+ def ensure_federails_configuration!
198
+ raise("Entity not configured for #{self.class.name}. Did you use \"acts_as_federails_data\"?") unless Federails.data_entity? self
199
+ end
200
+
201
+ def default_should_federate?
202
+ true
203
+ end
204
+ end
205
+ end
@@ -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
@@ -1,4 +1,28 @@
1
1
  module Federails
2
+ # Model concern providing UUIDs as model parameter (instead of IDs).
3
+ #
4
+ #
5
+ # A _required_, `uuid` field is required on the model's table for this concern to work:
6
+ #
7
+ # ```rb
8
+ # # Example migration
9
+ # add_column :my_table, :uuid, :text, default: nil, index: { unique: true }
10
+ # ```
11
+ #
12
+ # Usage:
13
+ #
14
+ # ```rb
15
+ # class MyModel < ApplicationRecord
16
+ # include Federails::HasUuid
17
+ # end
18
+ #
19
+ # # And now:
20
+ # instance = MyModel.find_param 'aaaa_bbbb_cccc_dddd_....'
21
+ # instance.to_param
22
+ # # => 'aaaa_bbbb_cccc_dddd_....'
23
+ # ```
24
+ #
25
+ # It can be added on existing tables without data migration as the `uuid` accessor will generate the value when missing.
2
26
  module HasUuid
3
27
  extend ActiveSupport::Concern
4
28
 
@@ -11,12 +35,14 @@ module Federails
11
35
  end
12
36
  end
13
37
 
38
+ # @return [String] The UUID
14
39
  def to_param
15
40
  uuid
16
41
  end
17
42
 
18
- # Override UUID accessor to provide lazy initialization of UUIDs for old data
43
+ # @return [String]
19
44
  def uuid
45
+ # Override UUID accessor to provide lazy initialization of UUIDs for old data
20
46
  if self[:uuid].blank?
21
47
  generate_uuid
22
48
  save!
@@ -1,4 +1,13 @@
1
1
  module Federails
2
+ # Activities can be compared to a log of what happened in the Fediverse.
3
+ #
4
+ # Activities from local actors ends in the actors _outboxes_.
5
+ # Activities form distant actors comes from the actor's _inbox_.
6
+ # We try to only keep activities _from_ local actors, and external activities _targetting_ local actors.
7
+ #
8
+ # See also:
9
+ # - https://www.w3.org/TR/activitypub/#outbox
10
+ # - https://www.w3.org/TR/activitypub/#inbox
2
11
  class Activity < ApplicationRecord
3
12
  include Federails::HasUuid
4
13
 
@@ -15,19 +24,33 @@ module Federails
15
24
 
16
25
  after_create_commit :post_to_inboxes
17
26
 
27
+ # Determines the list of actors targeted by the activity
28
+ #
29
+ # @return [Array<Federails::Actor>]
18
30
  def recipients
19
31
  return [] unless actor.local?
20
32
 
21
- case entity_type
22
- when 'Federails::Following'
23
- [(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]
24
40
  else
25
- actor.followers
41
+ default_recipient_list
26
42
  end
27
43
  end
28
44
 
29
45
  private
30
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
+
31
54
  def post_to_inboxes
32
55
  NotifyInboxJob.perform_later(self)
33
56
  end
@@ -1,19 +1,31 @@
1
1
  require 'federails/utils/host'
2
+ require 'federails/utils/actor'
2
3
  require 'fediverse/webfinger'
3
4
 
4
5
  module Federails
6
+ # Model storing _distant_ actors and links to local ones.
7
+ #
8
+ # To make a model act as an actor, use the `Federails::ActorEntity` concern
9
+ #
10
+ # See also:
11
+ # - https://www.w3.org/TR/activitypub/#actor-objects
5
12
  class Actor < ApplicationRecord # rubocop:disable Metrics/ClassLength
6
- include Federails::HasUuid
13
+ class TombstonedError < StandardError; end
7
14
 
8
- validates :federated_url, presence: { unless: :entity }, uniqueness: { unless: :entity }
9
- validates :username, presence: { unless: :entity }
10
- validates :server, presence: { unless: :entity }
11
- validates :inbox_url, presence: { unless: :entity }
12
- validates :outbox_url, presence: { unless: :entity }
13
- validates :followers_url, presence: { unless: :entity }
14
- validates :followings_url, presence: { unless: :entity }
15
- validates :profile_url, presence: { unless: :entity }
16
- 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? }
17
29
 
18
30
  belongs_to :entity, polymorphic: true, optional: true
19
31
  # FIXME: Handle this with something like undelete
@@ -24,50 +36,59 @@ module Federails
24
36
  has_many :followers, source: :actor, through: :following_followers
25
37
  has_many :follows, source: :target_actor, through: :following_follows
26
38
 
27
- scope :local, -> { where.not(entity: 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) }
28
43
 
29
- def local?
30
- entity.present?
44
+ on_federails_delete_requested -> { tombstone! }
45
+
46
+ def distant?
47
+ !local?
31
48
  end
32
49
 
33
50
  def federated_url
34
- 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
35
52
  end
36
53
 
37
54
  def username
38
- return attributes['username'] unless local?
55
+ return attributes['username'] unless use_entity_attributes?
39
56
 
40
57
  entity.send(entity_configuration[:username_field]).to_s
41
58
  end
42
59
 
43
60
  def name
44
- 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?)
45
62
 
46
63
  value || attributes['name'] || username
47
64
  end
48
65
 
49
66
  def server
50
- 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']
51
72
  end
52
73
 
53
74
  def inbox_url
54
- 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']
55
76
  end
56
77
 
57
78
  def outbox_url
58
- 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']
59
80
  end
60
81
 
61
82
  def followers_url
62
- 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']
63
84
  end
64
85
 
65
86
  def followings_url
66
- 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']
67
88
  end
68
89
 
69
90
  def profile_url
70
- return attributes['profile_url'].presence unless local?
91
+ return attributes['profile_url'].presence unless use_entity_attributes?
71
92
 
72
93
  method = entity_configuration[:profile_url_method]
73
94
  return Federails::Engine.routes.url_helpers.server_actor_url self unless method
@@ -76,11 +97,15 @@ module Federails
76
97
  end
77
98
 
78
99
  def at_address
79
- "#{username}@#{server}"
100
+ "@#{username}@#{server}"
80
101
  end
81
102
 
82
103
  def short_at_address
83
- local? ? "@#{username}" : at_address
104
+ use_entity_attributes? ? "@#{username}" : at_address
105
+ end
106
+
107
+ def acct_uri
108
+ "acct:#{username}@#{server}"
84
109
  end
85
110
 
86
111
  def follows?(actor)
@@ -96,16 +121,24 @@ module Federails
96
121
  Federails.actor_entity entity_type
97
122
  end
98
123
 
124
+ def tombstoned?
125
+ tombstoned_at.present?
126
+ end
127
+
128
+ def tombstone!
129
+ Federails::Utils::Actor.tombstone! self
130
+ end
131
+
99
132
  class << self
100
- 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)
101
138
  parts = Fediverse::Webfinger.split_account account
102
139
 
103
140
  if Fediverse::Webfinger.local_user? parts
104
- actor = nil
105
- Federails::Configuration.actor_types.each_value do |entity|
106
- actor ||= entity[:class].find_by(entity[:username_field] => parts[:username])&.federails_actor
107
- end
108
- raise ActiveRecord::RecordNotFound if actor.nil?
141
+ actor = find_local_by_username! parts[:username]
109
142
  else
110
143
  actor = find_by username: parts[:username], server: parts[:domain]
111
144
  actor ||= Fediverse::Webfinger.fetch_actor(parts[:username], parts[:domain])
@@ -124,6 +157,13 @@ module Federails
124
157
  Fediverse::Webfinger.fetch_actor_url(federated_url)
125
158
  end
126
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
+
127
167
  def find_or_create_by_account(account)
128
168
  actor = find_by_account account
129
169
  # Create/update distant actors
@@ -151,6 +191,25 @@ module Federails
151
191
  raise "Unsupported object type for actor (#{object.class})"
152
192
  end
153
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
154
213
  end
155
214
 
156
215
  def public_key
@@ -187,5 +246,9 @@ module Federails
187
246
  public_key: rsa_key.public_key.to_pem,
188
247
  }
189
248
  end
249
+
250
+ def use_entity_attributes?
251
+ local? && !tombstoned?
252
+ end
190
253
  end
191
254
  end
@@ -1,4 +1,5 @@
1
1
  module Federails
2
+ # Stores following data between actors
2
3
  class Following < ApplicationRecord
3
4
  include Federails::HasUuid
4
5
 
@@ -8,17 +9,21 @@ module Federails
8
9
 
9
10
  belongs_to :actor
10
11
  belongs_to :target_actor, class_name: 'Federails::Actor'
11
- # FIXME: Handle this with something like undelete
12
12
  has_many :activities, as: :entity, dependent: :destroy
13
13
 
14
14
  after_create :after_follow
15
- after_create :create_activity
16
- after_destroy :destroy_activity
15
+ after_create :create_activity, if: :locally_instigated?
16
+ after_update :after_follow_accepted
17
+ after_destroy :destroy_activity, if: :locally_instigated?
18
+
19
+ define_callbacks :on_federails_delete_requested
20
+
21
+ set_callback :on_federails_delete_requested, -> { destroy! unless locally_instigated? }
17
22
 
18
23
  scope :with_actor, ->(actor) { where(actor_id: actor.id).or(where(target_actor_id: actor.id)) }
19
24
 
20
25
  def federated_url
21
- attributes['federated_url'].presence || Federails::Engine.routes.url_helpers.server_actor_following_url(actor_id: actor_id, id: id)
26
+ attributes['federated_url'].presence || Federails::Engine.routes.url_helpers.server_actor_following_url(actor_id: actor.to_param, id: to_param)
22
27
  end
23
28
 
24
29
  def accept!
@@ -26,6 +31,10 @@ module Federails
26
31
  Activity.create! actor: target_actor, action: 'Accept', entity: self
27
32
  end
28
33
 
34
+ def follow_activity
35
+ Activity.find_by actor: actor, action: 'Follow', entity: target_actor
36
+ end
37
+
29
38
  class << self
30
39
  def new_from_account(account, actor:)
31
40
  target_actor = Actor.find_or_create_by_account account
@@ -35,18 +44,29 @@ module Federails
35
44
 
36
45
  private
37
46
 
47
+ def locally_instigated?
48
+ actor.local?
49
+ end
50
+
38
51
  def after_follow
39
- target_actor&.entity&.run_callbacks :followed, :after do
40
- self
41
- end
52
+ return unless target_actor&.entity
53
+
54
+ target_actor.entity.class.send(:dispatch_callback, :after_followed, target_actor.entity, self)
55
+ end
56
+
57
+ def after_follow_accepted
58
+ return unless status_previously_changed? && status == 'accepted'
59
+ return unless actor&.entity
60
+
61
+ actor.entity.class.send(:dispatch_callback, :after_follow_accepted, actor.entity, self)
42
62
  end
43
63
 
44
64
  def create_activity
45
- Activity.create! actor: actor, action: 'Create', entity: self
65
+ Activity.create! actor: actor, action: 'Follow', entity: target_actor
46
66
  end
47
67
 
48
68
  def destroy_activity
49
- Activity.create! actor: actor, action: 'Undo', entity: self
69
+ Activity.create! actor: actor, action: 'Undo', entity: follow_activity
50
70
  end
51
71
  end
52
72
  end
@@ -0,0 +1,15 @@
1
+ module Federails
2
+ module Server
3
+ class PublishablePolicy < Federails::FederailsPolicy
4
+ def show?
5
+ @record.send(@record.federails_data_configuration[:should_federate_method])
6
+ end
7
+
8
+ class Scope < Scope
9
+ def resolve
10
+ raise NotImplementedError
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -9,6 +9,9 @@ json.extract! actor,
9
9
  :followings_url,
10
10
  :profile_url,
11
11
  :at_address,
12
- :user_id,
12
+ :local,
13
+ :entity_id,
14
+ :entity_type,
15
+ :tombstoned_at,
13
16
  :created_at,
14
17
  :updated_at
@@ -0,0 +1 @@
1
+ <%= t('.gone') %>
@@ -27,7 +27,13 @@
27
27
  <% end %>
28
28
  <% @actors.each do |actor| %>
29
29
  <tr>
30
- <td><%= actor.name %></td>
30
+ <td>
31
+ <% if actor.tombstoned? %>
32
+ <del><%= actor.name %></del>
33
+ <% else %>
34
+ <%= actor.name %>
35
+ <% end %>
36
+ </td>
31
37
  <td><%= actor.username %></td>
32
38
  <td><%= actor.at_address %></td>
33
39
  <td><%= actor.local? %></td>