federails 0.6.2 → 0.7.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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -0
  3. data/app/controllers/federails/client/actors_controller.rb +1 -1
  4. data/app/controllers/federails/client/followings_controller.rb +1 -1
  5. data/app/controllers/federails/server/nodeinfo_controller.rb +2 -2
  6. data/app/models/concerns/federails/actor_entity.rb +4 -0
  7. data/app/models/concerns/federails/data_entity.rb +80 -8
  8. data/app/models/concerns/federails/handles_delete_requests.rb +5 -0
  9. data/app/models/federails/actor.rb +37 -2
  10. data/app/views/federails/client/common/_client_links.html.erb +3 -1
  11. data/app/views/federails/client/followings/_follow_actions.html.erb +5 -1
  12. data/app/views/federails/client/followings/_follower.html.erb +1 -1
  13. data/app/views/federails/server/nodeinfo/index.nodeinfo.jbuilder +1 -1
  14. data/lib/federails/configuration.rb +7 -0
  15. data/lib/federails/maintenance/actors_updater.rb +3 -4
  16. data/lib/federails/utils/actor.rb +33 -1
  17. data/lib/federails/utils/json_request.rb +41 -0
  18. data/lib/federails/utils/object.rb +1 -1
  19. data/lib/federails/version.rb +1 -1
  20. data/lib/federails.rb +5 -0
  21. data/lib/fediverse/inbox.rb +10 -0
  22. data/lib/fediverse/node_info.rb +38 -0
  23. data/lib/fediverse/request.rb +6 -28
  24. data/lib/fediverse/webfinger.rb +10 -33
  25. data/lib/generators/federails/copy_client_policies/USAGE +8 -0
  26. data/lib/generators/federails/copy_client_policies/copy_client_policies_generator.rb +9 -0
  27. data/lib/generators/federails/copy_factories/USAGE +8 -0
  28. data/lib/generators/federails/copy_factories/copy_factories_generator.rb +21 -0
  29. metadata +22 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 28392d4e043a74e77e376374a27a4ee2c86c604b90d0fa0ac7e9ecc337a21630
4
- data.tar.gz: cebe905ce0a273bff88ca969a8b0bf72d2f9bc287210dcaf7fd48042a182edce
3
+ metadata.gz: 07b0025f15cc58379219f20956c53c0c3e6810e68ac60367b9728c9eff03220b
4
+ data.tar.gz: 22957563450ab52509a350e6e50f397cb564396c0927c510439ea1ad041106d9
5
5
  SHA512:
6
- metadata.gz: 660950645cf4a9af0d3d8dd34337a3857d038f487cc22a3199d78d9cdcdfc17cad165955aba8163c1e2c084be39fdbd639c571b7a9bd88658fed96f2d73b1615
7
- data.tar.gz: cfcf8fc68460ef4eb7e66e9d4a459cf29863f2330f903d5967ef5d4bb43f3817963e355eb3690d84798674664dce0267ccb0ab05772274e2cb06a97aa152bf2b
6
+ metadata.gz: 72e09779f3cdd23101c29e659f568f5dfddb5c85453c3853d49d1d7408b8017ea6c836b1b06afd6e3d62a6a476b8ce6746ec78b10e7f0d06ab32e825b947c258
7
+ data.tar.gz: 2b6bd12fb24cb50925e637c10f5f16453afa799a438695e36a77c3478c7394d3b3204ca8f6cbcddad49dfa1a031dd5a3ffaefb42c100b946f5c6ca8ae4550128
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
 
@@ -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
@@ -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
@@ -158,5 +158,9 @@ module Federails
158
158
  def tombstone_federails_actor!
159
159
  federails_actor.tombstone! if federails_actor.present?
160
160
  end
161
+
162
+ def untombstone_federails_actor!
163
+ federails_actor.untombstone! if federails_actor.present?
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
@@ -33,7 +33,9 @@ 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
38
40
 
39
41
  scope :local, -> { where(local: true) }
@@ -42,6 +44,7 @@ module Federails
42
44
  scope :not_tombstoned, -> { where(tombstoned_at: nil) }
43
45
 
44
46
  on_federails_delete_requested -> { tombstone! }
47
+ on_federails_undelete_requested -> { untombstone! }
45
48
 
46
49
  def distant?
47
50
  !local?
@@ -96,8 +99,8 @@ module Federails
96
99
  Rails.application.routes.url_helpers.send method, [entity]
97
100
  end
98
101
 
99
- def at_address
100
- "@#{username}@#{server}"
102
+ def at_address(prefix: '@')
103
+ "#{prefix}#{username}@#{server}"
101
104
  end
102
105
 
103
106
  def short_at_address
@@ -108,6 +111,9 @@ module Federails
108
111
  "acct:#{username}@#{server}"
109
112
  end
110
113
 
114
+ # Checks if a given actor follows the current actor
115
+ #
116
+ # @return [Federails::Following, false]
111
117
  def follows?(actor)
112
118
  list = following_follows.where target_actor: actor
113
119
  return list.first if list.count == 1
@@ -115,12 +121,37 @@ module Federails
115
121
  false
116
122
  end
117
123
 
124
+ # Checks if current actor is followed by the given actor
125
+ #
126
+ # @return [Federails::Following, false]
127
+ def followed_by?(actor)
128
+ list = following_followers.where actor: actor
129
+ return list.first if list.count == 1
130
+
131
+ false
132
+ end
133
+
118
134
  def entity_configuration
119
135
  raise("Entity not configured for #{entity_type}. Did you use \"acts_as_federails_actor\"?") unless Federails.actor_entity? entity_type
120
136
 
121
137
  Federails.actor_entity entity_type
122
138
  end
123
139
 
140
+ # Synchronizes actor with distant data
141
+ #
142
+ # @raise [ActiveRecord::RecordNotFound] when distant data was not found
143
+ def sync!
144
+ if local?
145
+ Rails.logger.info 'Ignored attempt to sync a local actor'
146
+ return false
147
+ end
148
+
149
+ response = Fediverse::Webfinger.fetch_actor_url(federated_url)
150
+ new_attributes = response.attributes.except 'id', 'uuid', 'created_at', 'updated_at', 'local', 'entity_id', 'entity_type'
151
+
152
+ update! new_attributes
153
+ end
154
+
124
155
  def tombstoned?
125
156
  tombstoned_at.present?
126
157
  end
@@ -129,6 +160,10 @@ module Federails
129
160
  Federails::Utils::Actor.tombstone! self
130
161
  end
131
162
 
163
+ def untombstone!
164
+ Federails::Utils::Actor.untombstone! self
165
+ end
166
+
132
167
  class << self
133
168
  # Searches for an actor from account URI
134
169
  #
@@ -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 %>
@@ -1,7 +1,7 @@
1
1
  <div>
2
2
  <b><%= link_to following.actor.name, federails.client_actor_url(following.actor) %></b>
3
3
  (<%= following.actor.at_address %>) (<%= following.status %>)
4
- <% if following.pending? && following.target_actor == current_user.federails_actor %>
4
+ <% if following.pending? && following.target_actor == current_user&.federails_actor %>
5
5
  <%= button_to 'Accept', federails.accept_client_following_path(following), method: :put %>
6
6
  <% end %>
7
7
  </div>
@@ -1,6 +1,6 @@
1
1
  json.links [
2
2
  {
3
- rel: 'https://nodeinfo.diaspora.software/ns/schema/2.0',
3
+ rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
4
4
  href: show_node_info_url,
5
5
  },
6
6
  ]
@@ -58,6 +58,13 @@ module Federails
58
58
  # @!method self.remote_follow_url_method=(value)
59
59
  #
60
60
  # Sets the route method for remote-following requests
61
+ #
62
+ # The route should lead to a page displaying the remote actor and a button to follow it.
63
+ # Remote actor is specified in the `uri` query parameter.
64
+ #
65
+ # Its value defaults to a route of the Federails client, so your application will break
66
+ # if you don't use the client routes and don't override this value.
67
+ #
61
68
  # @param value [String] Route method name as used in links
62
69
  # @example
63
70
  # remote_follow_url_method 'main_app.my_custom_route_helper'
@@ -54,12 +54,11 @@ module Federails
54
54
  def update(actor)
55
55
  return :ignored_local if actor.local?
56
56
 
57
- response = Fediverse::Webfinger.fetch_actor_url(actor.federated_url)
58
- new_attributes = response.attributes.except 'id', 'uuid', 'created_at', 'updated_at', 'local', 'entity_id', 'entity_type'
59
-
60
- actor.update(new_attributes) ? :updated : :failed
57
+ actor.sync! ? :updated : :failed
61
58
  rescue ActiveRecord::RecordNotFound
62
59
  :not_found
60
+ rescue StandardError
61
+ :failed
63
62
  end
64
63
  end
65
64
  end
@@ -27,13 +27,21 @@ module Federails
27
27
  actor
28
28
  end
29
29
 
30
+ def untombstone!(actor)
31
+ if actor.local?
32
+ untombstone_local_actor actor
33
+ else
34
+ untombstone_distant_actor actor
35
+ end
36
+ end
37
+
30
38
  private
31
39
 
32
40
  def tombstone_local_actor(actor)
33
41
  Federails::Actor.transaction do
34
42
  hash = {
35
43
  tombstoned_at: Time.current,
36
- entity: nil,
44
+ entity: actor.entity || nil,
37
45
  }
38
46
  # Hardcode attributes depending on the actor's entity
39
47
  COMPUTED_ATTRIBUTES.each { |attribute| hash[attribute] = actor.send(attribute) }
@@ -44,9 +52,33 @@ module Federails
44
52
  end
45
53
  end
46
54
 
55
+ def untombstone_local_actor(actor)
56
+ return unless actor.tombstoned?
57
+ raise 'Cannot restore a local actor without an entity' if actor.entity.blank?
58
+
59
+ Federails::Actor.transaction do
60
+ # Reset hardcoded attributes depending on the actor's entity
61
+ hash = { tombstoned_at: nil }
62
+ COMPUTED_ATTRIBUTES.each { |attribute| hash[attribute] = nil }
63
+
64
+ actor.update! hash
65
+
66
+ delete_activity = Activity.find_by actor: actor, action: 'Delete', entity: actor
67
+ return unless delete_activity
68
+
69
+ Activity.create! actor: actor, action: 'Undo', entity: delete_activity
70
+ end
71
+ end
72
+
47
73
  def tombstone_distant_actor(actor)
48
74
  actor.update! tombstoned_at: Time.current
49
75
  end
76
+
77
+ def untombstone_distant_actor(actor)
78
+ actor.tombstoned_at = nil
79
+ actor.sync!
80
+ actor.save
81
+ end
50
82
  end
51
83
  end
52
84
  end
@@ -0,0 +1,41 @@
1
+ require 'faraday'
2
+ require 'faraday/follow_redirects'
3
+
4
+ module Federails
5
+ module Utils
6
+ # Simple wrapper to make requests expecting JSON
7
+ module JsonRequest
8
+ class UnhandledResponseStatus < StandardError; end
9
+
10
+ BASE_HEADERS = {
11
+ 'Content-Type' => 'application/json',
12
+ 'Accept' => 'application/json',
13
+ }.freeze
14
+
15
+ # Makes a GET request and returns a +Hash+ from the parsed body
16
+ #
17
+ # @param url [String] Target URL
18
+ # @param params [Hash] Querystring parameters
19
+ # @param headers [Hash] Additional headers
20
+ # @param follow_redirects [Boolean] Whether to follow redirections
21
+ # @param expected_status [Integer] Expected response status. Will raise a +UnhandledResponseStatus+ when status is different
22
+ #
23
+ # @return The parsed JSON object
24
+ #
25
+ # @raise [UnhandledResponseStatus] when response status is not the expected_status
26
+ def self.get_json(url, params: {}, headers: {}, follow_redirects: false, expected_status: 200)
27
+ headers = BASE_HEADERS.merge headers
28
+
29
+ connection = Faraday.new url: url, params: params, headers: headers do |faraday|
30
+ faraday.response :follow_redirects if follow_redirects
31
+ faraday.adapter Faraday.default_adapter
32
+ end
33
+
34
+ response = connection.get
35
+ raise UnhandledResponseStatus, "Unhandled status code #{response.status} for GET #{url}" if expected_status && response.status != expected_status
36
+
37
+ JSON.parse(response.body)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -121,7 +121,7 @@ module Federails
121
121
  entity = handler[:class].new_from_activitypub_object(hash)
122
122
  return unless entity
123
123
 
124
- entity.federails_actor = Federails::Actor.find_or_create_by_object hash['attributedTo'] if entity && !entity.federails_actor
124
+ entity.federails_actor = Federails::Actor.find_by_federation_url hash['attributedTo'] if entity && !entity.federails_actor
125
125
 
126
126
  entity
127
127
  end
@@ -1,3 +1,3 @@
1
1
  module Federails
2
- VERSION = '0.6.2'.freeze
2
+ VERSION = '0.7.0'.freeze
3
3
  end
data/lib/federails.rb CHANGED
@@ -1,7 +1,12 @@
1
+ require 'jbuilder'
2
+ require 'kaminari'
3
+ require 'pundit'
4
+
1
5
  require 'federails/version'
2
6
  require 'federails/engine'
3
7
  require 'federails/configuration'
4
8
  require 'federails/utils/object'
9
+ require 'federails/utils/json_request'
5
10
 
6
11
  # rubocop:disable Style/ClassVars
7
12
 
@@ -90,11 +90,21 @@ module Fediverse
90
90
 
91
91
  object.run_callbacks :on_federails_delete_requested
92
92
  end
93
+
94
+ def handle_undelete_request(activity)
95
+ # Get to original object
96
+ delete_activity = Request.dereference(activity['object'])
97
+ object = Federails::Utils::Object.find_distant_object_in_all(delete_activity['object'])
98
+ return if object.blank?
99
+
100
+ object.run_callbacks :on_federails_undelete_requested
101
+ end
93
102
  end
94
103
 
95
104
  register_handler 'Follow', '*', self, :handle_create_follow_request
96
105
  register_handler 'Accept', 'Follow', self, :handle_accept_follow_request
97
106
  register_handler 'Undo', 'Follow', self, :handle_undo_follow_request
98
107
  register_handler 'Delete', '*', self, :handle_delete_request
108
+ register_handler 'Undo', 'Delete', self, :handle_undelete_request
99
109
  end
100
110
  end
@@ -0,0 +1,38 @@
1
+ module Fediverse
2
+ class NodeInfo
3
+ class NotFoundError < StandardError; end
4
+ class NoActivityPubError < StandardError; end
5
+
6
+ class << self
7
+ def fetch(domain)
8
+ url = nodeinfo_url(domain)
9
+
10
+ hash = Federails::Utils::JsonRequest.get_json url
11
+ raise NoActivityPubError, "#{domain} does not handle activitypub protocol" unless hash['protocols'].include? 'activitypub'
12
+
13
+ {
14
+ domain: domain,
15
+ nodeinfo_url: url,
16
+ software_version: hash.dig('software', 'version'),
17
+ software_name: hash.dig('software', 'name'),
18
+ protocols: hash['protocols'],
19
+ services: hash['services'],
20
+ }
21
+ end
22
+
23
+ private
24
+
25
+ def base_url(domain)
26
+ scheme = Federails::Configuration.force_ssl ? 'https' : 'http'
27
+ @base_url = "#{scheme}://#{domain}"
28
+ end
29
+
30
+ def nodeinfo_url(domain)
31
+ response = Federails::Utils::JsonRequest.get_json "#{base_url(domain)}/.well-known/nodeinfo", follow_redirects: true
32
+ entry = response['links']&.find { |link| link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/2.0' }
33
+
34
+ entry['href']
35
+ end
36
+ end
37
+ end
38
+ end
@@ -2,27 +2,7 @@ require 'json/ld'
2
2
 
3
3
  module Fediverse
4
4
  class Request
5
- BASE_HEADERS = {
6
- 'Content-Type' => 'application/json',
7
- 'Accept' => 'application/json',
8
- }.freeze
9
-
10
- def initialize(id)
11
- @id = id
12
- end
13
-
14
- # FIXME: Replace by `Webfinger.get_json` (move other method here as class method)
15
- def get
16
- Rails.logger.debug { "GET #{@id}" }
17
- @response = Faraday.get(@id, nil, BASE_HEADERS)
18
- response_to_json
19
- end
20
-
21
5
  class << self
22
- def get(id)
23
- new(id).get
24
- end
25
-
26
6
  # Dereferences a value
27
7
  #
28
8
  # @param value [String, Hash]
@@ -34,18 +14,16 @@ module Fediverse
34
14
 
35
15
  raise "Unhandled object type #{value.class}"
36
16
  end
37
- end
38
17
 
39
- private
18
+ private
40
19
 
41
- def response_to_json
42
- begin
43
- body = JSON.parse @response.body
20
+ def get(id)
21
+ json = Federails::Utils::JsonRequest.get_json(id)
22
+
23
+ JSON::LD::API.compact json, json['@context']
44
24
  rescue JSON::ParserError
45
- return
25
+ nil
46
26
  end
47
-
48
- JSON::LD::API.compact body, body['@context']
49
27
  end
50
28
  end
51
29
  end
@@ -1,7 +1,5 @@
1
- require 'faraday'
2
- require 'faraday/follow_redirects'
3
-
4
1
  require 'federails/utils/host'
2
+ require 'federails/utils/json_request'
5
3
 
6
4
  module Fediverse
7
5
  # Methods related to Webfinger: find accounts, fetch actors,...
@@ -122,42 +120,21 @@ module Fediverse
122
120
  # Makes a simple GET request and returns a +Hash+ from the parsed body
123
121
  # @return [Hash]
124
122
  # @raise [ActiveRecord::RecordNotFound] when the response is invalid
125
- def get_json(url, payload = {})
126
- response = get(url, payload: payload, headers: { accept: 'application/json' })
123
+ def get_json(url, params = {})
124
+ Federails::Utils::JsonRequest.get_json(url, params: params, follow_redirects: true, headers: { accept: 'application/json' })
125
+ rescue Federails::Utils::JsonRequest::UnhandledResponseStatus => e
126
+ Rails.logger.debug { e.message }
127
127
 
128
- if response.status != 200
129
- Rails.logger.debug { "Unhandled status code #{response.status} for GET #{url}" }
130
- raise ActiveRecord::RecordNotFound
131
- end
128
+ raise ActiveRecord::RecordNotFound
129
+ rescue Faraday::ConnectionFailed
130
+ Rails.logger.debug { "Failed to reach server for GET #{url}" }
132
131
 
133
- JSON.parse(response.body)
132
+ raise ActiveRecord::RecordNotFound
134
133
  rescue JSON::ParserError
135
- Rails.logger.debug { "Invalid JSON response GET #{url}" }
134
+ Rails.logger.debug { "Invalid JSON response for GET #{url}" }
136
135
 
137
136
  raise ActiveRecord::RecordNotFound
138
137
  end
139
-
140
- # Only perform a GET request and throws an ActiveRecord::RecordNotFound on error.
141
- #
142
- # That's "ok-ish"; when an actor is unavailable, whatever the reason is, it's not found...
143
- #
144
- # @return [Faraday::Response]
145
- # @raise [ActiveRecord::RecordNotFound] when the response is invalid
146
- def get(url, payload: {}, headers: {})
147
- connection = Faraday.new url: url, params: payload, headers: headers do |faraday|
148
- faraday.response :follow_redirects # use Faraday::FollowRedirects::Middleware
149
- faraday.adapter Faraday.default_adapter
150
- end
151
-
152
- begin
153
- response = connection.get
154
- rescue Faraday::ConnectionFailed
155
- Rails.logger.debug { "Failed to reach server for GET #{url}" }
156
- raise ActiveRecord::RecordNotFound
157
- end
158
-
159
- response
160
- end
161
138
  end
162
139
  end
163
140
  end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Copies the Pundit client policies to be overridden in main application.
3
+
4
+ Example:
5
+ bin/rails generate copy_client_policies
6
+
7
+ This will create:
8
+ app/policies/federails/client/*
@@ -0,0 +1,9 @@
1
+ module Federails
2
+ class CopyClientPoliciesGenerator < Rails::Generators::Base
3
+ source_root File.expand_path('../../../../app/policies/federails', __dir__)
4
+
5
+ def copy_policies
6
+ directory 'client', Rails.root.join('app', 'policies', 'federails', 'client')
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Copies FactoryBot factories in the "spec" directory
3
+
4
+ Example:
5
+ bin/rails generate copy_factories
6
+
7
+ This will create:
8
+ spec/factories/*
@@ -0,0 +1,21 @@
1
+ module Federails
2
+ class CopyFactoriesGenerator < Rails::Generators::Base
3
+ SOURCE_DIRECTORY = File.expand_path('../../../../spec/factories/federails', __dir__)
4
+
5
+ source_root SOURCE_DIRECTORY
6
+
7
+ def copy_factories
8
+ dest = Rails.root.join('spec', 'factories')
9
+
10
+ Dir.entries(SOURCE_DIRECTORY)
11
+ .each do |node|
12
+ source_path = File.join(SOURCE_DIRECTORY, node)
13
+ next unless File.file?(source_path) && node.match?(/\.rb\Z/)
14
+
15
+ file_path = File.join(dest, "federails_#{node}")
16
+ copy_file node, file_path
17
+ gsub_file file_path, /(FactoryBot.define do\n\s+factory) :(\w+),/, '\1 :federails_\2,'
18
+ end
19
+ end
20
+ end
21
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: federails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Manuel Tancoigne
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-04-18 00:00:00.000000000 Z
10
+ date: 2025-05-10 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: faraday
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: jbuilder
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.7'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.7'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: json-ld
42
56
  requirement: !ruby/object:Gem::Requirement
@@ -209,16 +223,22 @@ files:
209
223
  - lib/federails/maintenance/actors_updater.rb
210
224
  - lib/federails/utils/actor.rb
211
225
  - lib/federails/utils/host.rb
226
+ - lib/federails/utils/json_request.rb
212
227
  - lib/federails/utils/object.rb
213
228
  - lib/federails/version.rb
214
229
  - lib/fediverse.rb
215
230
  - lib/fediverse/inbox.rb
231
+ - lib/fediverse/node_info.rb
216
232
  - lib/fediverse/notifier.rb
217
233
  - lib/fediverse/request.rb
218
234
  - lib/fediverse/signature.rb
219
235
  - lib/fediverse/webfinger.rb
236
+ - lib/generators/federails/copy_client_policies/USAGE
237
+ - lib/generators/federails/copy_client_policies/copy_client_policies_generator.rb
220
238
  - lib/generators/federails/copy_client_views/USAGE
221
239
  - lib/generators/federails/copy_client_views/copy_client_views_generator.rb
240
+ - lib/generators/federails/copy_factories/USAGE
241
+ - lib/generators/federails/copy_factories/copy_factories_generator.rb
222
242
  - lib/generators/federails/install/USAGE
223
243
  - lib/generators/federails/install/install_generator.rb
224
244
  - lib/generators/federails/install/templates/federails.rb