federails 0.6.2 → 0.8.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -0
  3. data/app/controllers/concerns/federails/server/render_collections.rb +19 -0
  4. data/app/controllers/federails/client/actors_controller.rb +1 -1
  5. data/app/controllers/federails/client/followings_controller.rb +4 -4
  6. data/app/controllers/federails/server/activities_controller.rb +14 -6
  7. data/app/controllers/federails/server/actors_controller.rb +16 -7
  8. data/app/controllers/federails/server/nodeinfo_controller.rb +2 -2
  9. data/app/helpers/federails/server_helper.rb +6 -0
  10. data/app/jobs/federails/application_job.rb +2 -0
  11. data/app/jobs/federails/fetch_nodeinfo_job.rb +10 -0
  12. data/app/jobs/federails/notify_inbox_job.rb +0 -2
  13. data/app/models/concerns/federails/actor_entity.rb +5 -1
  14. data/app/models/concerns/federails/data_entity.rb +80 -8
  15. data/app/models/concerns/federails/handles_delete_requests.rb +5 -0
  16. data/app/models/federails/activity.rb +15 -22
  17. data/app/models/federails/actor.rb +44 -3
  18. data/app/models/federails/following.rb +3 -3
  19. data/app/models/federails/host.rb +48 -0
  20. data/app/views/federails/client/common/_client_links.html.erb +3 -1
  21. data/app/views/federails/client/followings/_follow_actions.html.erb +5 -1
  22. data/app/views/federails/client/followings/_follower.html.erb +1 -1
  23. data/app/views/federails/server/activities/_activity.activitypub.jbuilder +7 -3
  24. data/app/views/federails/server/actors/_actor.activitypub.jbuilder +7 -6
  25. data/app/views/federails/server/actors/_tombstone.activitypub.jbuilder +1 -4
  26. data/app/views/federails/server/followings/_following.activitypub.jbuilder +1 -1
  27. data/app/views/federails/server/nodeinfo/index.nodeinfo.jbuilder +1 -1
  28. data/app/views/federails/server/nodeinfo/show.nodeinfo.jbuilder +2 -3
  29. data/app/views/federails/server/published/_publishable.activitypub.jbuilder +2 -2
  30. data/app/views/federails/server/published/_tombstone.activitypub.jbuilder +1 -4
  31. data/app/views/federails/server/shared/ordered_collection.activitypub.jbuilder +6 -0
  32. data/app/views/federails/server/shared/ordered_collection_page.activitypub.jbuilder +12 -0
  33. data/app/views/users/show.html.erb +4 -0
  34. data/db/migrate/20250426061729_create_federails_hosts.rb +22 -0
  35. data/db/migrate/20251121160720_add_to_and_cc_to_federails_activities.rb +6 -0
  36. data/lib/federails/configuration.rb +25 -0
  37. data/lib/federails/data_transformer/note.rb +6 -1
  38. data/lib/federails/maintenance/actors_updater.rb +3 -4
  39. data/lib/federails/maintenance/hosts_updater.rb +19 -0
  40. data/lib/federails/utils/actor.rb +33 -1
  41. data/lib/federails/utils/context.rb +12 -0
  42. data/lib/federails/utils/json_request.rb +41 -0
  43. data/lib/federails/utils/object.rb +1 -1
  44. data/lib/federails/utils/response_codes.rb +11 -0
  45. data/lib/federails/version.rb +1 -1
  46. data/lib/federails.rb +16 -3
  47. data/lib/fediverse/collection.rb +31 -0
  48. data/lib/fediverse/inbox.rb +10 -0
  49. data/lib/fediverse/node_info.rb +38 -0
  50. data/lib/fediverse/notifier.rb +39 -12
  51. data/lib/fediverse/request.rb +6 -28
  52. data/lib/fediverse/signature.rb +1 -1
  53. data/lib/fediverse/webfinger.rb +10 -33
  54. data/lib/fediverse.rb +2 -0
  55. data/lib/generators/federails/copy_client_policies/USAGE +8 -0
  56. data/lib/generators/federails/copy_client_policies/copy_client_policies_generator.rb +9 -0
  57. data/lib/generators/federails/copy_factories/USAGE +8 -0
  58. data/lib/generators/federails/copy_factories/copy_factories_generator.rb +43 -0
  59. data/lib/generators/federails/install/templates/federails.yml +2 -0
  60. data/lib/tasks/federails_tasks.rake +5 -0
  61. metadata +38 -6
  62. data/app/views/federails/server/activities/outbox.activitypub.jbuilder +0 -18
  63. data/app/views/federails/server/actors/followers.activitypub.jbuilder +0 -18
  64. data/app/views/federails/server/actors/following.activitypub.jbuilder +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 28392d4e043a74e77e376374a27a4ee2c86c604b90d0fa0ac7e9ecc337a21630
4
- data.tar.gz: cebe905ce0a273bff88ca969a8b0bf72d2f9bc287210dcaf7fd48042a182edce
3
+ metadata.gz: 36117bc1ea548be65aebe4ee505850ecac3233851f07aaae18136188ec1c1077
4
+ data.tar.gz: '0958b68fce09e2775504b28f5227e098470c0cb3c89eb4ca3dec7f5ece871d78'
5
5
  SHA512:
6
- metadata.gz: 660950645cf4a9af0d3d8dd34337a3857d038f487cc22a3199d78d9cdcdfc17cad165955aba8163c1e2c084be39fdbd639c571b7a9bd88658fed96f2d73b1615
7
- data.tar.gz: cfcf8fc68460ef4eb7e66e9d4a459cf29863f2330f903d5967ef5d4bb43f3817963e355eb3690d84798674664dce0267ccb0ab05772274e2cb06a97aa152bf2b
6
+ metadata.gz: 2f6e4dc1e3554b81a02710b9d893c88ffa77ff7c33e623574378cd7b87a13165297dcd4b22a4ce38e90073a545708d7d82e17e9867b1f345a1ebcad2c1cdf237
7
+ data.tar.gz: a8fb6f9811a9369ee7cf706c64517f47c8a66b335de715b8325e508ca6790dc4e1f7db6ae67b62651920cf3fd467d1c2e23263fb5b4f38f2d40a2c1d5a9cdfc2
data/README.md CHANGED
@@ -40,6 +40,13 @@ It _may_ work on other versions, but we won't provide support.
40
40
  - [Common questions](docs/faq.md)
41
41
  - [Contributing](CONTRIBUTING.md)
42
42
 
43
+ ## Extensions
44
+
45
+ Extensions extends the features of Federails.
46
+
47
+ - [Federails Moderation](https://github.com/manyfold3d/federails-moderation/)
48
+ > A gem that provides moderation capabilities for Federails
49
+
43
50
  ## License
44
51
 
45
52
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -53,6 +60,7 @@ See [CONTRIBUTING](CONTRIBUTING.md) to have an overview of the process and the t
53
60
  - [echarp](https://gitlab.com/echarp)
54
61
  - [James Smith](https://gitlab.com/floppy.uk)
55
62
  - [Manuel Tancoigne](https://gitlab.com/mtancoigne)
63
+ - [pessi-v](https://github.com/pessi-v)
56
64
 
57
65
  ### Indirect contributions
58
66
 
@@ -0,0 +1,19 @@
1
+ module Federails
2
+ module Server
3
+ module RenderCollections
4
+ extend ActiveSupport::Concern
5
+
6
+ def render_collection(actor:, collection:, url_helper:, &items_block)
7
+ if params[:page].present?
8
+ render_collection_page(actor: actor, collection: collection, url_helper: url_helper, items_block: items_block)
9
+ else
10
+ render 'federails/server/shared/ordered_collection', locals: { collection: collection, url_helper: url_helper, actor: actor }
11
+ end
12
+ end
13
+
14
+ def render_collection_page(collection:, actor:, url_helper:, items_block:)
15
+ render 'federails/server/shared/ordered_collection_page', locals: { collection: collection, url_helper: url_helper, actor: actor, items_block: items_block }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -35,7 +35,7 @@ module Federails
35
35
  end
36
36
 
37
37
  def account_param
38
- params.require('account')
38
+ params.require('account').strip
39
39
  end
40
40
 
41
41
  def render_show
@@ -8,7 +8,7 @@ module Federails
8
8
  # GET /app/followings/new?uri={uri}
9
9
  def new
10
10
  # Find actor (and fetch if necessary)
11
- actor = Actor.find_or_create_by_federation_url(params[:uri])
11
+ actor = Actor.find_or_create_by_federation_url(params.require(:uri))
12
12
  # Redirect to local profile page which will have a follow button on it
13
13
  redirect_to federails.client_actor_url(actor)
14
14
  end
@@ -23,7 +23,7 @@ module Federails
23
23
  format.json { render :show, status: :ok, location: @following }
24
24
  else
25
25
  format.html { redirect_to url, alert: I18n.t('controller.followings.accept.error') }
26
- format.json { render json: @following.errors, status: :unprocessable_entity }
26
+ format.json { render json: @following.errors, status: Federails::Utils::ResponseCodes::UNPROCESSABLE_CONTENT }
27
27
  end
28
28
  end
29
29
  end
@@ -49,7 +49,7 @@ module Federails
49
49
  # Renders a 422 instead of a 404
50
50
  respond_to do |format|
51
51
  format.html { redirect_to federails.client_actors_url, alert: I18n.t('controller.followings.follow.error') }
52
- format.json { render json: { target_actor: ['does not exist'] }, status: :unprocessable_entity }
52
+ format.json { render json: { target_actor: ['does not exist'] }, status: Federails::Utils::ResponseCodes::UNPROCESSABLE_CONTENT }
53
53
  end
54
54
 
55
55
  return
@@ -94,7 +94,7 @@ module Federails
94
94
  format.json { render :show, status: :created, location: @following }
95
95
  else
96
96
  format.html { redirect_to url, alert: I18n.t('controller.followings.save_and_render.error') }
97
- format.json { render json: @following.errors, status: :unprocessable_entity }
97
+ format.json { render json: @following.errors, status: Federails::Utils::ResponseCodes::UNPROCESSABLE_CONTENT }
98
98
  end
99
99
  end
100
100
  end
@@ -3,6 +3,8 @@ require 'fediverse/inbox'
3
3
  module Federails
4
4
  module Server
5
5
  class ActivitiesController < Federails::ServerController
6
+ include Federails::Server::RenderCollections
7
+
6
8
  before_action :set_activity, only: [:show]
7
9
 
8
10
  # GET /federation/activities
@@ -10,10 +12,16 @@ module Federails
10
12
  def outbox
11
13
  authorize Federails::Activity, policy_class: Federails::Server::ActivityPolicy
12
14
 
13
- @actor = Actor.find_param(params[:actor_id])
14
- @activities = policy_scope(Federails::Activity, policy_scope_class: Federails::Server::ActivityPolicy::Scope).where(actor: @actor).order(created_at: :desc)
15
- @total_activities = @activities.count
16
- @activities = @activities.page(params[:page])
15
+ actor = Actor.find_param(params[:actor_id])
16
+ activities = policy_scope(Federails::Activity, policy_scope_class: Federails::Server::ActivityPolicy::Scope).where(actor: actor).order(created_at: :desc)
17
+
18
+ render_collection(
19
+ collection: activities.page(params[:page]),
20
+ actor: actor,
21
+ url_helper: :server_actor_outbox_url
22
+ ) do |builder, items|
23
+ builder.array! items, partial: 'federails/server/activities/activity', as: :activity, context: false
24
+ end
17
25
  end
18
26
 
19
27
  # GET /federation/actors/1/activities/1.json
@@ -24,12 +32,12 @@ module Federails
24
32
  skip_authorization
25
33
 
26
34
  payload = payload_from_params
27
- return head :unprocessable_entity unless payload
35
+ return head Federails::Utils::ResponseCodes::UNPROCESSABLE_CONTENT unless payload
28
36
 
29
37
  if Fediverse::Inbox.dispatch_request(payload)
30
38
  head :created
31
39
  else
32
- head :unprocessable_entity
40
+ head Federails::Utils::ResponseCodes::UNPROCESSABLE_CONTENT
33
41
  end
34
42
  end
35
43
 
@@ -1,6 +1,8 @@
1
1
  module Federails
2
2
  module Server
3
3
  class ActorsController < Federails::ServerController
4
+ include Federails::Server::RenderCollections
5
+
4
6
  before_action :set_actor, only: [:show, :followers, :following]
5
7
 
6
8
  # GET /federation/actors/1
@@ -14,14 +16,26 @@ module Federails
14
16
  # GET /federation/actors/:id/followers.json
15
17
  def followers
16
18
  @actors = @actor.followers.order(created_at: :desc)
17
- followings_queries
19
+ render_collection(
20
+ collection: @actors.page(params[:page]),
21
+ actor: @actor,
22
+ url_helper: :followers_server_actor_url
23
+ ) do |builder, items|
24
+ builder.array! items.map(&:federated_url)
25
+ end
18
26
  end
19
27
 
20
28
  # GET /federation/actors/:id/followers
21
29
  # GET /federation/actors/:id/followers.json
22
30
  def following
23
31
  @actors = @actor.follows.order(created_at: :desc)
24
- followings_queries
32
+ render_collection(
33
+ collection: @actors.page(params[:page]),
34
+ actor: @actor,
35
+ url_helper: :following_server_actor_url
36
+ ) do |builder, items|
37
+ builder.array! items.map(&:federated_url)
38
+ end
25
39
  end
26
40
 
27
41
  private
@@ -31,11 +45,6 @@ module Federails
31
45
  @actor = Actor.find_param(params[:id])
32
46
  authorize @actor, policy_class: Federails::Server::ActorPolicy
33
47
  end
34
-
35
- def followings_queries
36
- @total_actors = @actors.count
37
- @actors = @actors.page(params[:page])
38
- end
39
48
  end
40
49
  end
41
50
  end
@@ -18,8 +18,8 @@ module Federails
18
18
  @has_user_counts = true
19
19
  model = config[:class]
20
20
  @total += model.send(method, nil)
21
- @active_month += model.send(method, ((30.days.ago)...Time.current))
22
- @active_halfyear += model.send(method, ((180.days.ago)...Time.current))
21
+ @active_month += model.send(method, (30.days.ago)...Time.current)
22
+ @active_halfyear += model.send(method, (180.days.ago)...Time.current)
23
23
  end
24
24
  render formats: [:nodeinfo]
25
25
  end
@@ -1,3 +1,5 @@
1
+ require 'federails/utils/context'
2
+
1
3
  module Federails
2
4
  module ServerHelper
3
5
  def remote_follow_url
@@ -8,5 +10,9 @@ module Federails
8
10
  Rails.application.routes.url_helpers.send(method_name)
9
11
  end
10
12
  end
13
+
14
+ def set_json_ld_context(json, additional: nil)
15
+ json.set! '@context', Federails::Utils::Context.generate(additional: additional)
16
+ end
11
17
  end
12
18
  end
@@ -1,5 +1,7 @@
1
1
  module Federails
2
2
  class ApplicationJob < ActiveJob::Base
3
+ queue_as { Configuration.job_queue.to_sym }
4
+
3
5
  discard_on ActiveJob::DeserializationError
4
6
  after_discard do |_job, exception|
5
7
  Rails.logger.info exception.to_s
@@ -0,0 +1,10 @@
1
+ require 'fediverse/notifier'
2
+
3
+ module Federails
4
+ class FetchNodeinfoJob < ApplicationJob
5
+ # @param domain [String] Domain to create/update
6
+ def perform(domain)
7
+ Federails::Host.create_or_update domain, min_update_interval: Federails::Configuration.remote_entities_cache_duration
8
+ end
9
+ end
10
+ end
@@ -2,8 +2,6 @@ require 'fediverse/notifier'
2
2
 
3
3
  module Federails
4
4
  class NotifyInboxJob < ApplicationJob
5
- queue_as :default
6
-
7
5
  def perform(activity)
8
6
  activity.reload
9
7
  Fediverse::Notifier.post_to_inboxes(activity)
@@ -156,7 +156,11 @@ module Federails
156
156
  end
157
157
 
158
158
  def tombstone_federails_actor!
159
- federails_actor.tombstone! if federails_actor.present?
159
+ federails_actor.presence&.tombstone!
160
+ end
161
+
162
+ def untombstone_federails_actor!
163
+ federails_actor.presence&.untombstone!
160
164
  end
161
165
  end
162
166
  end
@@ -12,9 +12,18 @@ module Federails
12
12
  #
13
13
  # ## Pre-requisites
14
14
  #
15
- # Model must have a `federated_url` attribute:
15
+ # Model should have the following methods:
16
+ # - `to_activitypub_object`, returning a valid ActivityPub object
17
+ # - `self.from_activitypub_object`, returning a hash of valid attributes from a hash of incoming data
18
+ #
19
+ # Table needs at least:
20
+ # - `t.string :federated_url, null: true, default: nil`
21
+ # - `t.references :federails_actor, foreign_key: true, null: true, default: nil
22
+ #
23
+ # Model must have the following attributes:
16
24
  # ```rb
17
25
  # add_column :posts, :federated_url, :string, null: true, default: nil
26
+ # add_reference :posts, :federails_actor, foreign_key: true, null: true, default: nil
18
27
  # ```
19
28
  #
20
29
  # ## Usage
@@ -29,6 +38,61 @@ module Federails
29
38
  # # This will be called when a Delete activity comes for the entry. As we don't know how you want to handle it,
30
39
  # # you'll have to implement the behavior yourself.
31
40
  # on_federails_delete_requested :do_something
41
+ #
42
+ # # This will be called when a Undo activity comes for the entry. The easiest way to handle this case is to re-fetch
43
+ # # the entity
44
+ # on_federails_undelete_requested :do_something_else
45
+ #
46
+ # def to_activitypub_object
47
+ # Federails::DataTransformer::Note.to_federation self,
48
+ # content: content,
49
+ # name: title
50
+ # end
51
+ #
52
+ # # Creates a hash of attributes from incoming Note
53
+ # def self.from_activitypub_object(hash)
54
+ # {
55
+ # title: hash['name'] || 'A post',
56
+ # content: hash['content'],
57
+ # }
58
+ # end
59
+ # end
60
+ # ```
61
+ #
62
+ # **If your model has a mechanism for soft deletion:**
63
+ # - you can specify some methods names to handle it in Federails responses:
64
+ # - you will need to send the delete activity yourself
65
+ #
66
+ # ```rb
67
+ # acts_as_federails_data handles: 'Note',
68
+ # ...,
69
+ # soft_deleted_method: :deleted?
70
+ # soft_delete_date_method: :deleted_at
71
+ #
72
+ # on_federails_delete_requested :soft_delete!
73
+ # on_federails_undelete_requested :restore_remote_entity!
74
+ #
75
+ # # Method you use to soft-delete entities
76
+ # def soft_delete!
77
+ # update deleted_at: time.current
78
+ #
79
+ # send_federails_activity 'Delete' unless local_federails_entity?
80
+ # end
81
+ #
82
+ # # Method you use to restore soft-deleted entities
83
+ # def restore!
84
+ # update deleted_at: nil
85
+ #
86
+ # if local_federails_entity?
87
+ # delete_activity = Activity.find_by action: 'Delete', entity: self
88
+ # send_federails_activity 'Undo', entity: delete_activity, actor: federails_actor if delete_activity.present?
89
+ # end
90
+ # end
91
+ #
92
+ # def restore_remote_entity!
93
+ # self.deleted_at: nil
94
+ # federails_sync!
95
+ # save!
32
96
  # end
33
97
  # ```
34
98
  module DataEntity
@@ -41,9 +105,6 @@ module Federails
41
105
  module ClassMethods
42
106
  # Configures the mapping between entity and Fediverse
43
107
  #
44
- # Model should have the following methods:
45
- # - `to_activitypub_object`, returning a valid ActivityPub object
46
- #
47
108
  # @param actor_entity_method [Symbol] Method returning an object responding to 'federails_actor', for local content
48
109
  # @param url_param [Symbol] Column name of the object ID that should be used in URLs. Defaults to +:id+
49
110
  # @param route_path_segment [Symbol] Segment used in Federails routes to display the ActivityPub representation.
@@ -56,9 +117,9 @@ module Federails
56
117
  # @param should_federate_method [Symbol] method to determine if an object should be federated. If the method returns false,
57
118
  # no create/update activities will happen, and object will not be accessible at federated_url. Defaults to a method
58
119
  # 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.
120
+ # @param soft_deleted_method [Symbol, nil] If the model uses a soft-delete mechanism, this is the method to check
121
+ # if entity is soft-deleted. This is not required by the spec but greatly encouraged as the app will return a 410
122
+ # response with a Tombstone object instead of an 404 error.
62
123
  # @param soft_delete_date_method [Symbol, nil] Method to get the date of the soft-deletion
63
124
  #
64
125
  # @example
@@ -142,7 +203,7 @@ module Federails
142
203
 
143
204
  before_validation :set_federails_actor
144
205
  after_create -> { create_federails_activity 'Create' }
145
- after_update -> { create_federails_activity 'Update' }, :federails_tombstoned?
206
+ after_update -> { create_federails_activity 'Update' }, unless: :federails_tombstoned?
146
207
  after_destroy -> { create_federails_activity 'Delete' }
147
208
  end
148
209
 
@@ -177,6 +238,17 @@ module Federails
177
238
  Federails.data_entity_configuration(self)
178
239
  end
179
240
 
241
+ def federails_sync!
242
+ if local_federails_entity?
243
+ Rails.logger.info { "Ignored attempt to sync a local #{self.class.name}" }
244
+ return false
245
+ end
246
+
247
+ object = Fediverse::Request.dereference(federated_url)
248
+
249
+ update! self.class.from_activitypub_object(object)
250
+ end
251
+
180
252
  private
181
253
 
182
254
  def set_federails_actor
@@ -22,10 +22,15 @@ module Federails
22
22
  def on_federails_delete_requested(*args)
23
23
  set_callback :on_federails_delete_requested, *args
24
24
  end
25
+
26
+ def on_federails_undelete_requested(*args)
27
+ set_callback :on_federails_undelete_requested, *args
28
+ end
25
29
  end
26
30
 
27
31
  included do
28
32
  define_callbacks :on_federails_delete_requested
33
+ define_callbacks :on_federails_undelete_requested
29
34
  end
30
35
  end
31
36
  end
@@ -24,31 +24,24 @@ module Federails
24
24
 
25
25
  after_create_commit :post_to_inboxes
26
26
 
27
- # Determines the list of actors targeted by the activity
28
- #
29
- # @return [Array<Federails::Actor>]
30
- def recipients
31
- return [] unless actor.local?
32
-
33
- case action
34
- when 'Follow'
35
- [entity]
36
- when 'Undo'
37
- [entity.entity]
38
- when 'Accept'
39
- [entity.actor]
40
- else
41
- default_recipient_list
42
- end
43
- end
27
+ before_validation :set_default_addressing, on: :create
28
+
29
+ serialize :cc, coder: YAML
30
+ serialize :to, coder: YAML
44
31
 
45
32
  private
46
33
 
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
34
+ # Sets up default public-and-followers addressing unless to and cc are already set
35
+ #
36
+ # This retains compatibility with previous behaviour
37
+ def set_default_addressing
38
+ return if to.present? || cc.present?
39
+
40
+ self.to = [Fediverse::Collection::PUBLIC]
41
+ self.cc = [
42
+ actor.followers_url,
43
+ (entity.try(:followers_url) if entity.try(:local?)),
44
+ ].compact.uniq
52
45
  end
53
46
 
54
47
  def post_to_inboxes
@@ -33,15 +33,24 @@ module Federails
33
33
  has_many :activities_as_entity, class_name: 'Federails::Activity', as: :entity, dependent: :destroy
34
34
  has_many :following_followers, class_name: 'Federails::Following', foreign_key: :target_actor_id, dependent: :destroy, inverse_of: :target_actor
35
35
  has_many :following_follows, class_name: 'Federails::Following', dependent: :destroy, inverse_of: :actor
36
+ # Actors following actor
36
37
  has_many :followers, source: :actor, through: :following_followers
38
+ # Actors followed by actor
37
39
  has_many :follows, source: :target_actor, through: :following_follows
40
+ belongs_to :host, class_name: 'Federails::Host', foreign_key: :server, primary_key: :domain, inverse_of: :actors, optional: true
41
+
42
+ # Explicitly explain serialization for MariaDB
43
+ attribute :extensions, :json
38
44
 
39
45
  scope :local, -> { where(local: true) }
40
46
  scope :distant, -> { where(local: false) }
41
47
  scope :tombstoned, -> { where.not(tombstoned_at: nil) }
42
48
  scope :not_tombstoned, -> { where(tombstoned_at: nil) }
43
49
 
50
+ after_create -> { FetchNodeinfoJob.perform_later(server) }, unless: :local?
51
+
44
52
  on_federails_delete_requested -> { tombstone! }
53
+ on_federails_undelete_requested -> { untombstone! }
45
54
 
46
55
  def distant?
47
56
  !local?
@@ -96,8 +105,8 @@ module Federails
96
105
  Rails.application.routes.url_helpers.send method, [entity]
97
106
  end
98
107
 
99
- def at_address
100
- "@#{username}@#{server}"
108
+ def at_address(prefix: '@')
109
+ "#{prefix}#{username}@#{server}"
101
110
  end
102
111
 
103
112
  def short_at_address
@@ -108,9 +117,22 @@ module Federails
108
117
  "acct:#{username}@#{server}"
109
118
  end
110
119
 
120
+ # Checks if a given actor follows the current actor
121
+ #
122
+ # @return [Federails::Following, false]
111
123
  def follows?(actor)
112
124
  list = following_follows.where target_actor: actor
113
- return list.first if list.count == 1
125
+ return list.first if list.one?
126
+
127
+ false
128
+ end
129
+
130
+ # Checks if current actor is followed by the given actor
131
+ #
132
+ # @return [Federails::Following, false]
133
+ def followed_by?(actor)
134
+ list = following_followers.where actor: actor
135
+ return list.first if list.one?
114
136
 
115
137
  false
116
138
  end
@@ -121,6 +143,21 @@ module Federails
121
143
  Federails.actor_entity entity_type
122
144
  end
123
145
 
146
+ # Synchronizes actor with distant data
147
+ #
148
+ # @raise [ActiveRecord::RecordNotFound] when distant data was not found
149
+ def sync!
150
+ if local?
151
+ Rails.logger.info 'Ignored attempt to sync a local actor'
152
+ return false
153
+ end
154
+
155
+ response = Fediverse::Webfinger.fetch_actor_url(federated_url)
156
+ new_attributes = response.attributes.except 'id', 'uuid', 'created_at', 'updated_at', 'local', 'entity_id', 'entity_type'
157
+
158
+ update! new_attributes
159
+ end
160
+
124
161
  def tombstoned?
125
162
  tombstoned_at.present?
126
163
  end
@@ -129,6 +166,10 @@ module Federails
129
166
  Federails::Utils::Actor.tombstone! self
130
167
  end
131
168
 
169
+ def untombstone!
170
+ Federails::Utils::Actor.untombstone! self
171
+ end
172
+
132
173
  class << self
133
174
  # Searches for an actor from account URI
134
175
  #
@@ -28,7 +28,7 @@ module Federails
28
28
 
29
29
  def accept!
30
30
  update! status: :accepted
31
- Activity.create! actor: target_actor, action: 'Accept', entity: self
31
+ Activity.create! actor: target_actor, action: 'Accept', entity: self, to: [actor.federated_url]
32
32
  end
33
33
 
34
34
  def follow_activity
@@ -62,11 +62,11 @@ module Federails
62
62
  end
63
63
 
64
64
  def create_activity
65
- Activity.create! actor: actor, action: 'Follow', entity: target_actor
65
+ Activity.create! actor: actor, action: 'Follow', entity: target_actor, to: [target_actor.federated_url]
66
66
  end
67
67
 
68
68
  def destroy_activity
69
- Activity.create! actor: actor, action: 'Undo', entity: follow_activity
69
+ Activity.create! actor: actor, action: 'Undo', entity: follow_activity, to: [target_actor.federated_url]
70
70
  end
71
71
  end
72
72
  end
@@ -0,0 +1,48 @@
1
+ require 'fediverse/node_info'
2
+
3
+ module Federails
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: 'Federails::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 Federails::Utils::JsonRequest::UnhandledResponseStatus, Faraday::SSLError => e
44
+ Rails.logger.info { "Error connecting to #{domain}: '#{e.message}'" }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -17,7 +17,9 @@ Federation:
17
17
  </ul>
18
18
 
19
19
  <%# Debug information%>
20
- <% if !Federails::actor_entity?(user) %>
20
+ <% if !user %>
21
+ Register or login to view your actor's information
22
+ <% elsif !Federails::actor_entity?(user) %>
21
23
  <p><%= user.class.name %> is not configured to have an associated actor; you won't be allowed to follow or be followed</p>
22
24
  <% elsif !user.federails_actor.present? %>
23
25
  <p>Your account does not have an associated actor; you won't be allowed to follow or be followed</p>
@@ -10,7 +10,7 @@
10
10
  <% if actor.entity == user %>
11
11
  <button type="button" role="button" disabled="disabled">That's you</button>
12
12
  <% elsif follow %>
13
- Already following (<%= follow.status %>)
13
+ You are already following (<%= follow.status %>)
14
14
  <%= button_to 'Cancel', federails.client_following_path(follow), method: :delete %>
15
15
  <% else %>
16
16
  <%= button_to "Follow #{actor.username}", federails.follow_client_followings_path, params: { account: actor.at_address }, method: :post %>
@@ -26,6 +26,10 @@
26
26
  <%= button_to 'Revoke', federails.client_following_path(followed), method: :delete %>
27
27
  <% end %>
28
28
  <% end %>
29
+ <% elsif !user %>
30
+ Register or login to be able to follow this actor. Alternatively, search for this address on Fediverse server where
31
+ you already have an account:
32
+ <input type="text" readonly="readonly" value="<%= actor.at_address(prefix: '') %>">
29
33
  <% else %>
30
34
  <%= user.class.name %> is not configured to follow/be followed, or has no associated actor.
31
35
  <% end %>