fedipub 0.0.1 → 0.10.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 (135) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +76 -2
  3. data/Rakefile +5 -9
  4. data/app/assets/config/fedipub_manifest.js +1 -0
  5. data/app/assets/stylesheets/fedipub/application.css +15 -0
  6. data/app/controllers/concerns/fedipub/server/render_collections.rb +19 -0
  7. data/app/controllers/fedipub/client/activities_controller.rb +27 -0
  8. data/app/controllers/fedipub/client/actors_controller.rb +54 -0
  9. data/app/controllers/fedipub/client/followings_controller.rb +103 -0
  10. data/app/controllers/fedipub/client_controller.rb +9 -0
  11. data/app/controllers/fedipub/server/activities_controller.rb +78 -0
  12. data/app/controllers/fedipub/server/actors_controller.rb +50 -0
  13. data/app/controllers/fedipub/server/followings_controller.rb +19 -0
  14. data/app/controllers/fedipub/server/nodeinfo_controller.rb +28 -0
  15. data/app/controllers/fedipub/server/published_controller.rb +30 -0
  16. data/app/controllers/fedipub/server/web_finger_controller.rb +44 -0
  17. data/app/controllers/fedipub/server_controller.rb +48 -0
  18. data/app/helpers/fedipub/server_helper.rb +18 -0
  19. data/app/jobs/fedipub/application_job.rb +10 -0
  20. data/app/jobs/fedipub/fetch_nodeinfo_job.rb +10 -0
  21. data/app/jobs/fedipub/notify_inbox_job.rb +10 -0
  22. data/app/mailers/fedipub/application_mailer.rb +6 -0
  23. data/app/models/concerns/fedipub/actor_entity.rb +165 -0
  24. data/app/models/concerns/fedipub/announceable.rb +18 -0
  25. data/app/models/concerns/fedipub/application_actor.rb +25 -0
  26. data/app/models/concerns/fedipub/data_entity.rb +278 -0
  27. data/app/models/concerns/fedipub/handles_delete_requests.rb +36 -0
  28. data/app/models/concerns/fedipub/has_uuid.rb +61 -0
  29. data/app/models/concerns/fedipub/likeable.rb +29 -0
  30. data/app/models/fedipub/activity.rb +62 -0
  31. data/app/models/fedipub/actor.rb +306 -0
  32. data/app/models/fedipub/application_record.rb +5 -0
  33. data/app/models/fedipub/following.rb +72 -0
  34. data/app/models/fedipub/host.rb +48 -0
  35. data/app/policies/fedipub/client/activity_policy.rb +9 -0
  36. data/app/policies/fedipub/client/actor_policy.rb +15 -0
  37. data/app/policies/fedipub/client/following_policy.rb +35 -0
  38. data/app/policies/fedipub/fedipub_policy.rb +63 -0
  39. data/app/policies/fedipub/server/activity_policy.rb +9 -0
  40. data/app/policies/fedipub/server/actor_policy.rb +23 -0
  41. data/app/policies/fedipub/server/following_policy.rb +6 -0
  42. data/app/policies/fedipub/server/publishable_policy.rb +15 -0
  43. data/app/views/fedipub/client/activities/_activity.html.erb +5 -0
  44. data/app/views/fedipub/client/activities/_activity.json.jbuilder +1 -0
  45. data/app/views/fedipub/client/activities/_index.json.jbuilder +1 -0
  46. data/app/views/fedipub/client/activities/feed.html.erb +11 -0
  47. data/app/views/fedipub/client/activities/feed.json.jbuilder +1 -0
  48. data/app/views/fedipub/client/activities/index.html.erb +5 -0
  49. data/app/views/fedipub/client/activities/index.json.jbuilder +1 -0
  50. data/app/views/fedipub/client/actors/_actor.json.jbuilder +17 -0
  51. data/app/views/fedipub/client/actors/_lookup_form.html.erb +5 -0
  52. data/app/views/fedipub/client/actors/gone.html.erb +1 -0
  53. data/app/views/fedipub/client/actors/index.html.erb +44 -0
  54. data/app/views/fedipub/client/actors/index.json.jbuilder +1 -0
  55. data/app/views/fedipub/client/actors/show.html.erb +112 -0
  56. data/app/views/fedipub/client/actors/show.json.jbuilder +1 -0
  57. data/app/views/fedipub/client/common/_client_links.html.erb +26 -0
  58. data/app/views/fedipub/client/followings/_follow.html.erb +4 -0
  59. data/app/views/fedipub/client/followings/_follow_actions.html.erb +36 -0
  60. data/app/views/fedipub/client/followings/_follower.html.erb +7 -0
  61. data/app/views/fedipub/client/followings/_following.json.jbuilder +1 -0
  62. data/app/views/fedipub/client/followings/_form.html.erb +22 -0
  63. data/app/views/fedipub/client/followings/index.html.erb +29 -0
  64. data/app/views/fedipub/client/followings/index.json.jbuilder +1 -0
  65. data/app/views/fedipub/client/followings/show.html.erb +21 -0
  66. data/app/views/fedipub/client/followings/show.json.jbuilder +1 -0
  67. data/app/views/fedipub/server/activities/_activity.activitypub.jbuilder +25 -0
  68. data/app/views/fedipub/server/activities/show.activitypub.jbuilder +1 -0
  69. data/app/views/fedipub/server/actors/_actor.activitypub.jbuilder +41 -0
  70. data/app/views/fedipub/server/actors/_tombstone.activitypub.jbuilder +6 -0
  71. data/app/views/fedipub/server/actors/show.activitypub.jbuilder +5 -0
  72. data/app/views/fedipub/server/followings/_following.activitypub.jbuilder +7 -0
  73. data/app/views/fedipub/server/followings/show.activitypub.jbuilder +1 -0
  74. data/app/views/fedipub/server/nodeinfo/index.nodeinfo.jbuilder +10 -0
  75. data/app/views/fedipub/server/nodeinfo/show.nodeinfo.jbuilder +19 -0
  76. data/app/views/fedipub/server/published/_publishable.activitypub.jbuilder +11 -0
  77. data/app/views/fedipub/server/published/_tombstone.activitypub.jbuilder +6 -0
  78. data/app/views/fedipub/server/published/show.activitypub.jbuilder +5 -0
  79. data/app/views/fedipub/server/shared/ordered_collection.activitypub.jbuilder +6 -0
  80. data/app/views/fedipub/server/shared/ordered_collection_page.activitypub.jbuilder +12 -0
  81. data/app/views/fedipub/server/web_finger/find.jrd.jbuilder +32 -0
  82. data/app/views/fedipub/server/web_finger/host_meta.xrd.erb +5 -0
  83. data/app/views/users/show.html.erb +4 -0
  84. data/config/initializers/mime_types.rb +21 -0
  85. data/config/initializers/security_vocab.rb +53 -0
  86. data/config/routes.rb +53 -0
  87. data/db/migrate/20200712133150_create_federails_actors.rb +22 -0
  88. data/db/migrate/20200712143127_create_federails_followings.rb +14 -0
  89. data/db/migrate/20200712174938_create_federails_activities.rb +11 -0
  90. data/db/migrate/20241002094500_add_uuids.rb +13 -0
  91. data/db/migrate/20241002094501_add_keypair_to_actors.rb +8 -0
  92. data/db/migrate/20250122160618_add_extensions_to_federails_actors.rb +5 -0
  93. data/db/migrate/20250301082500_add_local_to_actors.rb +11 -0
  94. data/db/migrate/20250329123939_add_actor_type_to_actors.rb +5 -0
  95. data/db/migrate/20250329123940_add_tombstoned_at_to_actors.rb +5 -0
  96. data/db/migrate/20250426061729_create_federails_hosts.rb +22 -0
  97. data/db/migrate/20251121160720_add_to_and_cc_to_federails_activities.rb +6 -0
  98. data/db/migrate/20260421170640_add_result_and_instrument_to_federails_activities.rb +6 -0
  99. data/db/migrate/20260723113716_rename_federails_to_fedipub.rb +15 -0
  100. data/lib/fedipub/configuration.rb +119 -0
  101. data/lib/fedipub/data_transformer/note.rb +36 -0
  102. data/lib/fedipub/engine.rb +11 -0
  103. data/lib/fedipub/maintenance/actors_updater.rb +66 -0
  104. data/lib/fedipub/maintenance/hosts_updater.rb +19 -0
  105. data/lib/fedipub/utils/actor.rb +85 -0
  106. data/lib/fedipub/utils/context.rb +12 -0
  107. data/lib/fedipub/utils/host.rb +54 -0
  108. data/lib/fedipub/utils/json_request.rb +89 -0
  109. data/lib/fedipub/utils/object.rb +131 -0
  110. data/lib/fedipub/utils/response_codes.rb +11 -0
  111. data/lib/fedipub/version.rb +1 -3
  112. data/lib/fedipub.rb +127 -4
  113. data/lib/fediverse/collection.rb +31 -0
  114. data/lib/fediverse/inbox.rb +110 -0
  115. data/lib/fediverse/node_info.rb +38 -0
  116. data/lib/fediverse/notifier.rb +63 -0
  117. data/lib/fediverse/request.rb +29 -0
  118. data/lib/fediverse/signature/draft_cavage12.rb +109 -0
  119. data/lib/fediverse/signature/rfc9421.rb +69 -0
  120. data/lib/fediverse/signature.rb +26 -0
  121. data/lib/fediverse/webfinger.rb +140 -0
  122. data/lib/fediverse.rb +5 -0
  123. data/lib/generators/fedipub/copy_client_policies/USAGE +8 -0
  124. data/lib/generators/fedipub/copy_client_policies/copy_client_policies_generator.rb +9 -0
  125. data/lib/generators/fedipub/copy_client_views/USAGE +8 -0
  126. data/lib/generators/fedipub/copy_client_views/copy_client_views_generator.rb +9 -0
  127. data/lib/generators/fedipub/copy_factories/USAGE +8 -0
  128. data/lib/generators/fedipub/copy_factories/copy_factories_generator.rb +43 -0
  129. data/lib/generators/fedipub/install/USAGE +9 -0
  130. data/lib/generators/fedipub/install/install_generator.rb +10 -0
  131. data/lib/generators/fedipub/install/templates/fedipub.rb +1 -0
  132. data/lib/generators/fedipub/install/templates/fedipub.yml +27 -0
  133. data/lib/tasks/factory_bot.rake +15 -0
  134. data/lib/tasks/fedipub_tasks.rake +13 -0
  135. metadata +276 -10
@@ -0,0 +1,62 @@
1
+ module Fedipub
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
11
+ class Activity < ApplicationRecord
12
+ include Fedipub::HasUuid
13
+
14
+ belongs_to :entity, polymorphic: true
15
+ belongs_to :actor
16
+
17
+ scope :feed_for, lambda { |actor|
18
+ actor_ids = []
19
+ Following.accepted.where(actor: actor).find_each do |following|
20
+ actor_ids << following.target_actor_id
21
+ end
22
+ where(actor_id: actor_ids)
23
+ }
24
+
25
+ after_create_commit :post_to_inboxes
26
+
27
+ before_validation :set_default_addressing, on: :create
28
+
29
+ serialize :cc, coder: YAML
30
+ serialize :to, coder: YAML
31
+ serialize :result, coder: YAML
32
+ serialize :instrument, coder: YAML
33
+
34
+ # Sends an Undo activity to reverse this one.
35
+ #
36
+ # Used for unfollowing, unliking, unboosting, etc.
37
+ #
38
+ # @return [Fedipub::Activity] the new Undo activity
39
+ def undo!
40
+ Activity.create! actor: actor, action: 'Undo', entity: self, to: to, cc: cc
41
+ end
42
+
43
+ private
44
+
45
+ # Sets up default public-and-followers addressing unless to and cc are already set
46
+ #
47
+ # This retains compatibility with previous behaviour
48
+ def set_default_addressing
49
+ return if to.present? || cc.present?
50
+
51
+ self.to = [Fediverse::Collection::PUBLIC]
52
+ self.cc = [
53
+ actor.followers_url,
54
+ (entity.try(:followers_url) if entity.try(:local?)),
55
+ ].compact.uniq
56
+ end
57
+
58
+ def post_to_inboxes
59
+ NotifyInboxJob.perform_later(self)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,306 @@
1
+ require 'fedipub/utils/host'
2
+ require 'fedipub/utils/actor'
3
+ require 'fediverse/webfinger'
4
+
5
+ module Fedipub
6
+ # Model storing _distant_ actors and links to local ones.
7
+ #
8
+ # To make a model act as an actor, use the `Fedipub::ActorEntity` concern
9
+ #
10
+ # See also:
11
+ # - https://www.w3.org/TR/activitypub/#actor-objects
12
+ class Actor < ApplicationRecord # rubocop:disable Metrics/ClassLength
13
+ class TombstonedError < StandardError; end
14
+
15
+ include Fedipub::HasUuid
16
+ include Fedipub::HandlesDeleteRequests
17
+ include Fedipub::Likeable
18
+ include Fedipub::Announceable
19
+ include Fedipub::ApplicationActor
20
+
21
+ validates :federated_url, presence: { unless: -> { entity || application_actor? } }, uniqueness: { unless: :local? }
22
+ validates :username, presence: { unless: :local? }
23
+ validates :server, presence: { unless: :local? }
24
+ validates :inbox_url, presence: { unless: :local? }
25
+ validates :outbox_url, presence: { unless: :local? }
26
+ validates :profile_url, presence: { unless: :local? }
27
+ validates :actor_type, presence: { unless: -> { local? && !application_actor? } }
28
+ validates :entity_id, uniqueness: { scope: :entity_type }, if: :entity_type
29
+ validates :entity, presence: true, if: -> { local? && !tombstoned? && !application_actor? }
30
+
31
+ belongs_to :entity, polymorphic: true, optional: true
32
+ # FIXME: Handle this with something like undelete
33
+ has_many :activities, dependent: :destroy
34
+ has_many :activities_as_entity, class_name: 'Fedipub::Activity', as: :entity, dependent: :destroy
35
+ has_many :following_followers, class_name: 'Fedipub::Following', foreign_key: :target_actor_id, dependent: :destroy, inverse_of: :target_actor
36
+ has_many :following_follows, class_name: 'Fedipub::Following', dependent: :destroy, inverse_of: :actor
37
+ # Actors following actor
38
+ has_many :followers, source: :actor, through: :following_followers
39
+ # Actors followed by actor
40
+ has_many :follows, source: :target_actor, through: :following_follows
41
+ belongs_to :host, class_name: 'Fedipub::Host', foreign_key: :server, primary_key: :domain, inverse_of: :actors, optional: true
42
+
43
+ # Explicitly explain serialization for MariaDB
44
+ attribute :extensions, :json
45
+
46
+ scope :local, -> { where(local: true) }
47
+ scope :distant, -> { where(local: false) }
48
+ scope :tombstoned, -> { where.not(tombstoned_at: nil) }
49
+ scope :not_tombstoned, -> { where(tombstoned_at: nil) }
50
+
51
+ after_create -> { FetchNodeinfoJob.perform_later(server) }, unless: :local?
52
+
53
+ on_fedipub_delete_requested -> { tombstone! }
54
+ on_fedipub_undelete_requested -> { untombstone! }
55
+
56
+ def distant?
57
+ !local?
58
+ end
59
+
60
+ def federated_url
61
+ use_entity_attributes? || application_actor? ? Fedipub::Engine.routes.url_helpers.server_actor_url(self) : attributes['federated_url'].presence
62
+ end
63
+
64
+ def username
65
+ return attributes['username'] unless use_entity_attributes?
66
+
67
+ entity.send(entity_configuration[:username_field]).to_s
68
+ end
69
+
70
+ def name
71
+ value = (entity.send(entity_configuration[:name_field]).to_s if use_entity_attributes?)
72
+
73
+ value || attributes['name'] || username
74
+ end
75
+
76
+ def server
77
+ use_entity_attributes? || application_actor? ? Utils::Host.localhost : attributes['server']
78
+ end
79
+
80
+ def actor_type
81
+ use_entity_attributes? ? entity_configuration[:actor_type] : attributes['actor_type']
82
+ end
83
+
84
+ def inbox_url
85
+ use_entity_attributes? || application_actor? ? Fedipub::Engine.routes.url_helpers.server_actor_inbox_url(self) : attributes['inbox_url']
86
+ end
87
+
88
+ def outbox_url
89
+ use_entity_attributes? || application_actor? ? Fedipub::Engine.routes.url_helpers.server_actor_outbox_url(self) : attributes['outbox_url']
90
+ end
91
+
92
+ def followers_url
93
+ use_entity_attributes? || application_actor? ? Fedipub::Engine.routes.url_helpers.followers_server_actor_url(self) : attributes['followers_url']
94
+ end
95
+
96
+ def followings_url
97
+ use_entity_attributes? || application_actor? ? Fedipub::Engine.routes.url_helpers.following_server_actor_url(self) : attributes['followings_url']
98
+ end
99
+
100
+ def profile_url
101
+ return attributes['profile_url'].presence unless use_entity_attributes?
102
+
103
+ method = entity_configuration[:profile_url_method]
104
+ return Fedipub::Engine.routes.url_helpers.server_actor_url self unless method
105
+
106
+ Rails.application.routes.url_helpers.send method, [entity]
107
+ end
108
+
109
+ def at_address(prefix: '@')
110
+ "#{prefix}#{username}@#{server}"
111
+ end
112
+
113
+ def short_at_address
114
+ use_entity_attributes? ? "@#{username}" : at_address
115
+ end
116
+
117
+ def acct_uri
118
+ "acct:#{username}@#{server}"
119
+ end
120
+
121
+ # Checks if a given actor follows the current actor
122
+ #
123
+ # @return [Fedipub::Following, false]
124
+ def follows?(actor)
125
+ list = following_follows.where target_actor: actor
126
+ return list.first if list.one?
127
+
128
+ false
129
+ end
130
+
131
+ # Checks if current actor is followed by the given actor
132
+ #
133
+ # @return [Fedipub::Following, false]
134
+ def followed_by?(actor)
135
+ list = following_followers.where actor: actor
136
+ return list.first if list.one?
137
+
138
+ false
139
+ end
140
+
141
+ def entity_configuration
142
+ raise("Entity not configured for #{entity_type}. Did you use \"acts_as_fedipub_actor\"?") unless Fedipub.actor_entity? entity_type
143
+
144
+ Fedipub.actor_entity entity_type
145
+ end
146
+
147
+ # Synchronizes actor with distant data
148
+ #
149
+ # @raise [ActiveRecord::RecordNotFound] when distant data was not found
150
+ def sync!
151
+ if local?
152
+ Rails.logger.info 'Ignored attempt to sync a local actor'
153
+ return false
154
+ end
155
+
156
+ response = Fediverse::Webfinger.fetch_actor_url(federated_url)
157
+ new_attributes = response.attributes.except 'id', 'uuid', 'created_at', 'updated_at', 'local', 'entity_id', 'entity_type'
158
+
159
+ update! new_attributes
160
+ end
161
+
162
+ def tombstoned?
163
+ tombstoned_at.present?
164
+ end
165
+
166
+ def tombstone!
167
+ Fedipub::Utils::Actor.tombstone! self
168
+ end
169
+
170
+ def untombstone!
171
+ Fedipub::Utils::Actor.untombstone! self
172
+ end
173
+
174
+ class << self
175
+ # Searches for an actor from account URI
176
+ #
177
+ # @param account [String] Account URI (username@host)
178
+ # @return [Fedipub::Actor, nil]
179
+ def find_by_account(account)
180
+ parts = Fediverse::Webfinger.split_account account
181
+
182
+ if Fediverse::Webfinger.local_user? parts
183
+ actor = find_local_by_username! parts[:username]
184
+ else
185
+ actor = find_by username: parts[:username], server: parts[:domain]
186
+ actor ||= Fediverse::Webfinger.fetch_actor(parts[:username], parts[:domain])
187
+ end
188
+
189
+ actor
190
+ end
191
+
192
+ def find_by_federation_url(federated_url)
193
+ local_route = Utils::Host.local_route federated_url
194
+ return find_param(local_route[:id]) if local_route && local_route[:controller] == 'fedipub/server/actors' && local_route[:action] == 'show'
195
+
196
+ actor = find_by federated_url: federated_url
197
+ return actor if actor
198
+
199
+ Fediverse::Webfinger.fetch_actor_url(federated_url)
200
+ end
201
+
202
+ def find_by_federation_url!(federated_url)
203
+ find_by_federation_url(federated_url).tap do |actor|
204
+ raise Fedipub::Actor::TombstonedError if actor.tombstoned?
205
+ raise ActiveRecord::RecordNotFound if actor.nil?
206
+ end
207
+ end
208
+
209
+ def find_or_create_by_account(account)
210
+ actor = find_by_account account
211
+ # Create/update distant actors
212
+ actor.save! unless actor.local?
213
+
214
+ actor
215
+ end
216
+
217
+ def find_or_create_by_federation_url(url)
218
+ actor = find_by_federation_url url
219
+ # Create/update distant actors
220
+ actor.save! unless actor.local?
221
+
222
+ actor
223
+ end
224
+
225
+ # Find or create actor from a given actor hash or actor id (actor's URL)
226
+ def find_or_create_by_object(object)
227
+ case object
228
+ when String
229
+ find_or_create_by_federation_url object
230
+ when Hash
231
+ find_or_create_by_federation_url object['id']
232
+ else
233
+ raise "Unsupported object type for actor (#{object.class})"
234
+ end
235
+ end
236
+
237
+ def find_local_by_username(username)
238
+ actor = nil
239
+ Fedipub::Configuration.actor_types.each_value do |entity|
240
+ break if actor.present?
241
+
242
+ actor = entity[:class].find_by(entity[:username_field] => username)&.fedipub_actor
243
+ end
244
+ return actor if actor
245
+
246
+ # Look for pure actors (e.g. the application actor)
247
+ actor = Fedipub::Actor.local.find_by username: username
248
+ return actor if actor
249
+
250
+ # Last hope: Search for tombstoned actors
251
+ Fedipub::Actor.local.tombstoned.find_by username: username
252
+ end
253
+
254
+ def find_local_by_username!(username)
255
+ find_local_by_username(username).tap do |actor|
256
+ raise ActiveRecord::RecordNotFound if actor.nil?
257
+ end
258
+ end
259
+ end
260
+
261
+ def public_key
262
+ ensure_key_pair_exists!
263
+ self[:public_key]
264
+ end
265
+
266
+ def private_key
267
+ ensure_key_pair_exists!
268
+ self[:private_key]
269
+ end
270
+
271
+ def key_id
272
+ "#{federated_url}#main-key"
273
+ end
274
+
275
+ private
276
+
277
+ def ensure_key_pair_exists!
278
+ return if self[:private_key].present? || !local?
279
+
280
+ update!(generate_key_pair)
281
+ end
282
+
283
+ def generate_key_pair
284
+ rsa_key = OpenSSL::PKey::RSA.new 2048
285
+ cipher = OpenSSL::Cipher.new('AES-128-CBC')
286
+ {
287
+ private_key: if Rails.application.credentials.secret_key_base
288
+ rsa_key.to_pem(cipher, Rails.application.credentials.secret_key_base)
289
+ else
290
+ rsa_key.to_pem
291
+ end,
292
+ public_key: rsa_key.public_key.to_pem,
293
+ }
294
+ end
295
+
296
+ def use_entity_attributes?
297
+ local? && !tombstoned? && entity.present?
298
+ end
299
+
300
+ def create_fedipub_activity(action, actor: self, to: nil, cc: nil)
301
+ return unless local
302
+
303
+ Activity.create! actor: actor, action: action, entity: self, to: to, cc: cc
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,5 @@
1
+ module Fedipub
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,72 @@
1
+ module Fedipub
2
+ # Stores following data between actors
3
+ class Following < ApplicationRecord
4
+ include Fedipub::HasUuid
5
+
6
+ enum :status, pending: 0, accepted: 1
7
+
8
+ validates :target_actor_id, uniqueness: { scope: [:actor_id, :target_actor_id] }
9
+
10
+ belongs_to :actor
11
+ belongs_to :target_actor, class_name: 'Fedipub::Actor'
12
+ has_many :activities, as: :entity, dependent: :destroy
13
+
14
+ after_create :after_follow
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_fedipub_delete_requested
20
+
21
+ set_callback :on_fedipub_delete_requested, -> { destroy! unless locally_instigated? }
22
+
23
+ scope :with_actor, ->(actor) { where(actor_id: actor.id).or(where(target_actor_id: actor.id)) }
24
+
25
+ def federated_url
26
+ attributes['federated_url'].presence || Fedipub::Engine.routes.url_helpers.server_actor_following_url(actor_id: actor.to_param, id: to_param)
27
+ end
28
+
29
+ def accept!
30
+ update! status: :accepted
31
+ Activity.create! actor: target_actor, action: 'Accept', entity: self, to: [actor.federated_url]
32
+ end
33
+
34
+ def follow_activity
35
+ Activity.find_by actor: actor, action: 'Follow', entity: target_actor
36
+ end
37
+
38
+ class << self
39
+ def new_from_account(account, actor:)
40
+ target_actor = Actor.find_or_create_by_account account
41
+ new actor: actor, target_actor: target_actor
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def locally_instigated?
48
+ actor.local?
49
+ end
50
+
51
+ def after_follow
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)
62
+ end
63
+
64
+ def create_activity
65
+ Activity.create! actor: actor, action: 'Follow', entity: target_actor, to: [target_actor.federated_url]
66
+ end
67
+
68
+ def destroy_activity
69
+ follow_activity&.undo!
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,48 @@
1
+ require 'fediverse/node_info'
2
+
3
+ module Fedipub
4
+ class Host < ApplicationRecord
5
+ attribute :protocols, :json
6
+ attribute :services, :json
7
+
8
+ validates :domain, presence: true, allow_blank: false, uniqueness: true
9
+
10
+ # No "dependent" option here as this is not a hard reference, and we want to keep the actors if the host gets deleted
11
+ has_many :actors, class_name: 'Fedipub::Actor', primary_key: :domain, foreign_key: :server, inverse_of: :host # rubocop:disable Rails/HasManyOrHasOneDependent
12
+
13
+ scope :same_app, -> { where software_name: Configuration.app_name }
14
+ scope :same_app_and_version, -> { same_app.where app_version: Configuration.app_version }
15
+
16
+ def same_app?
17
+ software_name == Configuration.app_name
18
+ end
19
+
20
+ def same_app_and_version?
21
+ software_name == Configuration.app_name && app_version == Configuration.app_version
22
+ end
23
+
24
+ # Update from remote data
25
+ def sync!
26
+ update! Fediverse::NodeInfo.fetch(domain)
27
+ end
28
+
29
+ class << self
30
+ # Creates or update a Host
31
+ #
32
+ # @param domain [String] Domain to check
33
+ # @param min_update_interval [Integer, ActiveSupport::Duration] Minimum amount of seconds since the last update to fetch fresh data
34
+ def create_or_update(domain, min_update_interval: 0)
35
+ entry = find_or_initialize_by domain: domain
36
+ return if min_update_interval && entry.persisted? && (entry.updated_at + min_update_interval) > Time.current
37
+
38
+ entry.sync!
39
+
40
+ entry
41
+ rescue Fediverse::NodeInfo::NoActivityPubError
42
+ Rails.logger.info { "#{domain} does not provide ActivityPub service" }
43
+ rescue Fedipub::Utils::JsonRequest::UnhandledResponseStatus, Faraday::SSLError => e
44
+ Rails.logger.info { "Error connecting to #{domain}: '#{e.message}'" }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,9 @@
1
+ module Fedipub
2
+ module Client
3
+ class ActivityPolicy < Fedipub::FedipubPolicy
4
+ def feed?
5
+ user_with_actor?
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ module Fedipub
2
+ module Client
3
+ class ActorPolicy < Fedipub::FedipubPolicy
4
+ def lookup?
5
+ true
6
+ end
7
+
8
+ class Scope < Scope
9
+ def resolve
10
+ scope
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,35 @@
1
+ module Fedipub
2
+ module Client
3
+ class FollowingPolicy < Fedipub::FedipubPolicy
4
+ def show?
5
+ in_following?
6
+ end
7
+
8
+ def destroy?
9
+ in_following?
10
+ end
11
+
12
+ def accept?
13
+ in_following? && @record.target_actor_id == @user.fedipub_actor.id
14
+ end
15
+
16
+ def follow?
17
+ create?
18
+ end
19
+
20
+ class Scope < Scope
21
+ def resolve
22
+ scope.with_actor(@user.fedipub_actor)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def in_following?
29
+ return false unless user_with_actor?
30
+
31
+ @record.actor_id == @user.fedipub_actor&.id || @record.target_actor_id == @user.fedipub_actor&.id
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,63 @@
1
+ module Fedipub
2
+ class FedipubPolicy
3
+ attr_reader :user, :record
4
+
5
+ def initialize(user, record)
6
+ @user = user
7
+ @record = record
8
+ end
9
+
10
+ def index?
11
+ true
12
+ end
13
+
14
+ def show?
15
+ true
16
+ end
17
+
18
+ def create?
19
+ @user.present?
20
+ end
21
+
22
+ def new?
23
+ create?
24
+ end
25
+
26
+ def update?
27
+ owner?
28
+ end
29
+
30
+ def edit?
31
+ update?
32
+ end
33
+
34
+ def destroy?
35
+ owner?
36
+ end
37
+
38
+ class Scope
39
+ attr_reader :user, :scope
40
+
41
+ def initialize(user, scope)
42
+ @user = user
43
+ @scope = scope
44
+ end
45
+
46
+ def resolve
47
+ scope.all
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def owner?
54
+ return false unless @user
55
+
56
+ @record.actor_id == @user.fedipub_actor.id
57
+ end
58
+
59
+ def user_with_actor?
60
+ @user && Fedipub.actor_entity?(@user) && !!@user.fedipub_actor
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,9 @@
1
+ module Fedipub
2
+ module Server
3
+ class ActivityPolicy < Fedipub::FedipubPolicy
4
+ def outbox?
5
+ true
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,23 @@
1
+ module Fedipub
2
+ module Server
3
+ class ActorPolicy < Fedipub::FedipubPolicy
4
+ def show?
5
+ @record.local?
6
+ end
7
+
8
+ def following?
9
+ true
10
+ end
11
+
12
+ def followers?
13
+ true
14
+ end
15
+
16
+ class Scope < Scope
17
+ def resolve
18
+ scope.local
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,6 @@
1
+ module Fedipub
2
+ module Server
3
+ class FollowingPolicy < Fedipub::FedipubPolicy
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,15 @@
1
+ module Fedipub
2
+ module Server
3
+ class PublishablePolicy < Fedipub::FedipubPolicy
4
+ def show?
5
+ @record.send(@record.fedipub_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,5 @@
1
+ <div>
2
+ <b><%= link_to activity.actor.name, fedipub.client_actor_url(activity.actor) %></b>
3
+ <code><%= activity.action %></code>
4
+ <!-- < %= link_to activity.entity_type, activity.entity %>-->
5
+ </div>
@@ -0,0 +1 @@
1
+ json.extract! activity, :id, :entity_id, :entity_type, :action, :actor_id, :created_at
@@ -0,0 +1 @@
1
+ json.array! @activities, partial: 'fedipub/client/activities/activity', as: :activity