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.
- checksums.yaml +4 -4
- data/LICENSE +21 -0
- data/README.md +19 -189
- data/app/controllers/federails/client/actors_controller.rb +18 -4
- data/app/controllers/federails/server/actors_controller.rb +4 -1
- data/app/controllers/federails/server/published_controller.rb +30 -0
- data/app/controllers/federails/server/web_finger_controller.rb +9 -6
- data/app/controllers/federails/server_controller.rb +7 -0
- data/app/models/concerns/federails/actor_entity.rb +120 -56
- data/app/models/concerns/federails/data_entity.rb +205 -0
- data/app/models/concerns/federails/handles_delete_requests.rb +31 -0
- data/app/models/concerns/federails/has_uuid.rb +27 -1
- data/app/models/federails/activity.rb +27 -4
- data/app/models/federails/actor.rb +93 -30
- data/app/models/federails/following.rb +29 -9
- data/app/policies/federails/server/publishable_policy.rb +15 -0
- data/app/views/federails/client/actors/_actor.json.jbuilder +4 -1
- data/app/views/federails/client/actors/gone.html.erb +1 -0
- data/app/views/federails/client/actors/index.html.erb +7 -1
- data/app/views/federails/server/activities/_activity.activitypub.jbuilder +8 -3
- data/app/views/federails/server/actors/_actor.activitypub.jbuilder +2 -2
- data/app/views/federails/server/actors/_tombstone.activitypub.jbuilder +9 -0
- data/app/views/federails/server/actors/show.activitypub.jbuilder +5 -1
- data/app/views/federails/server/published/_publishable.activitypub.jbuilder +11 -0
- data/app/views/federails/server/published/_tombstone.activitypub.jbuilder +9 -0
- data/app/views/federails/server/published/show.activitypub.jbuilder +5 -0
- data/config/routes.rb +4 -0
- data/db/migrate/20250122160618_add_extensions_to_federails_actors.rb +5 -0
- data/db/migrate/20250301082500_add_local_to_actors.rb +11 -0
- data/db/migrate/20250329123939_add_actor_type_to_actors.rb +5 -0
- data/db/migrate/20250329123940_add_tombstoned_at_to_actors.rb +5 -0
- data/lib/federails/configuration.rb +24 -1
- data/lib/federails/data_transformer/note.rb +31 -0
- data/lib/federails/maintenance/actors_updater.rb +67 -0
- data/lib/federails/utils/actor.rb +53 -0
- data/lib/federails/utils/object.rb +131 -0
- data/lib/federails/version.rb +1 -1
- data/lib/federails.rb +54 -0
- data/lib/fediverse/inbox.rb +40 -8
- data/lib/fediverse/notifier.rb +3 -0
- data/lib/fediverse/request.rb +13 -0
- data/lib/fediverse/webfinger.rb +72 -26
- data/lib/fediverse.rb +3 -0
- data/lib/tasks/federails_tasks.rake +8 -4
- 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
|
-
#
|
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
|
22
|
-
when '
|
23
|
-
[
|
33
|
+
case action
|
34
|
+
when 'Follow'
|
35
|
+
[entity]
|
36
|
+
when 'Undo'
|
37
|
+
[entity.entity]
|
38
|
+
when 'Accept'
|
39
|
+
[entity.actor]
|
24
40
|
else
|
25
|
-
|
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
|
-
|
13
|
+
class TombstonedError < StandardError; end
|
7
14
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
validates :
|
12
|
-
validates :
|
13
|
-
validates :
|
14
|
-
validates :
|
15
|
-
validates :
|
16
|
-
validates :
|
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
|
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
|
-
|
30
|
-
|
44
|
+
on_federails_delete_requested -> { tombstone! }
|
45
|
+
|
46
|
+
def distant?
|
47
|
+
!local?
|
31
48
|
end
|
32
49
|
|
33
50
|
def federated_url
|
34
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
"
|
100
|
+
"@#{username}@#{server}"
|
80
101
|
end
|
81
102
|
|
82
103
|
def short_at_address
|
83
|
-
|
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
|
-
|
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 =
|
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
|
-
|
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:
|
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
|
40
|
-
|
41
|
-
|
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: '
|
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:
|
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
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= t('.gone') %>
|
@@ -27,7 +27,13 @@
|
|
27
27
|
<% end %>
|
28
28
|
<% @actors.each do |actor| %>
|
29
29
|
<tr>
|
30
|
-
<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>
|