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,10 @@
1
+ require 'fediverse/notifier'
2
+
3
+ module Fedipub
4
+ class FetchNodeinfoJob < ApplicationJob
5
+ # @param domain [String] Domain to create/update
6
+ def perform(domain)
7
+ Fedipub::Host.create_or_update domain, min_update_interval: Fedipub::Configuration.remote_entities_cache_duration
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ require 'fediverse/notifier'
2
+
3
+ module Fedipub
4
+ class NotifyInboxJob < ApplicationJob
5
+ def perform(activity)
6
+ activity.reload
7
+ Fediverse::Notifier.post_to_inboxes(activity)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,6 @@
1
+ module Fedipub
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ end
@@ -0,0 +1,165 @@
1
+ module Fedipub
2
+ # Concern to include in models that acts as actors.
3
+ #
4
+ # Actors can be anything; they authors content _via_ their _outbox_ and receive content in their _inbox_.
5
+ # Actors can follow and be followed by each other
6
+ #
7
+ # By default, when an entry is created on models using this concern, a _local_ `Fedipub::Actor` will be created.
8
+ #
9
+ # See also:
10
+ # - https://www.w3.org/TR/activitypub/#actor-objects
11
+ #
12
+ # ## Usage
13
+ #
14
+ # Include the concern in an existing model:
15
+ #
16
+ # ```rb
17
+ # class User < ApplicationRecord
18
+ # include Fedipub::ActorEntity
19
+ # acts_as_fedipub_actor options
20
+ # end
21
+ # ```
22
+ module ActorEntity
23
+ extend ActiveSupport::Concern
24
+
25
+ # Class methods automatically included in the concern.
26
+ module ClassMethods
27
+ # Configures the mapping between entity and actor
28
+ #
29
+ # @param username_field [Symbol] The method or attribute name that returns the preferred username for ActivityPub
30
+ # @param name_field [Symbol] The method or attribute name that returns the preferred name for ActivityPub
31
+ # @param profile_url_method [Symbol] The route method name that will generate the profile URL for ActivityPub
32
+ # @param actor_type [String] The ActivityStreams Actor type for this entity; defaults to 'Person'
33
+ # @param user_count_method [Symbol] A class method to call to count active users. Leave unspecified to leave this
34
+ # entity out of user counts. Method signature should accept a single parameter which will specify a date range
35
+ # If parameter is nil, the total user count should be returned. If the parameter is specified, the number of users
36
+ # active during the time period should be returned.
37
+ # @param auto_create_actors [Boolean] Whether to automatically create an actor when the entity is created
38
+ #
39
+ # @example
40
+ # acts_as_fedipub_actor username_field: :username, name_field: :display_name, profile_url_method: :url_for, actor_type: 'Person'
41
+ # rubocop:disable-next Metrics/ParameterLists
42
+ def acts_as_fedipub_actor(
43
+ name_field:,
44
+ username_field:,
45
+ profile_url_method: nil,
46
+ actor_type: 'Person',
47
+ user_count_method: nil,
48
+ auto_create_actors: true
49
+ )
50
+ Fedipub::Configuration.register_actor_class(
51
+ self,
52
+ username_field: username_field,
53
+ name_field: name_field,
54
+ profile_url_method: profile_url_method,
55
+ actor_type: actor_type,
56
+ user_count_method: user_count_method,
57
+ auto_create_actors: auto_create_actors
58
+ )
59
+ end
60
+
61
+ # Define a method that will be called after the entity receives a follow request.
62
+ # The follow request will be passed as an argument to the method.
63
+ #
64
+ # @param method_name [Symbol] The name of the method to call, or a block that will be called directly
65
+ #
66
+ # @example
67
+ # after_followed :accept_follow
68
+ def after_followed(method_name)
69
+ @after_followed = method_name
70
+ end
71
+
72
+ # Define a method that will be called after a follow request made by the entity is accepted
73
+ # The accepted follow request will be passed as an argument to the method.
74
+ #
75
+ # @param method_name [Symbol] The name of the method to call, or a block that will be called directly
76
+ #
77
+ # @example
78
+ # after_follow_accepted :follow_accepted
79
+ def after_follow_accepted(method_name)
80
+ @after_follow_accepted = method_name
81
+ end
82
+
83
+ # Define a method that will be called after an activity has been received
84
+ #
85
+ # @param activity_type [String] The activity action to handle, e.g. 'Create'. If you specify '*', the handler will be called for any activity type.
86
+ # @param object_type [String] The object type to handle, e.g. 'Note'. If you specify '*', the handler will be called for any object type.
87
+ # @param method_name [Symbol] The name of the class method to call. The method will receive the complete activity payload as a parameter.
88
+ #
89
+ # @example
90
+ # after_activity_received 'Create', 'Note', :create_note
91
+ def after_activity_received(activity_type, object_type, method_name)
92
+ Fediverse::Inbox.register_handler(activity_type, object_type, self, method_name)
93
+ end
94
+
95
+ private
96
+
97
+ def dispatch_callback(name, instance, *)
98
+ case name
99
+ when :after_followed
100
+ instance.send(@after_followed, *) if @after_followed
101
+ when :after_follow_accepted
102
+ instance.send(@after_follow_accepted, *) if @after_follow_accepted
103
+ end
104
+ end
105
+ end
106
+
107
+ included do
108
+ # No "dependent: :xyz" as the "before_destroy" hook should have nullified the actor
109
+ has_one :fedipub_actor, class_name: 'Fedipub::Actor', as: :entity # rubocop:disable Rails/HasManyOrHasOneDependent
110
+
111
+ after_create :create_fedipub_actor, if: lambda {
112
+ raise("Entity not configured for #{self.class.name}. Did you use \"acts_as_fedipub_actor\"?") unless Fedipub.actor_entity? self
113
+
114
+ Fedipub.actor_entity(self)[:auto_create_actors]
115
+ }
116
+ before_destroy :tombstone_fedipub_actor!
117
+ end
118
+
119
+ # Add custom data to actor responses.
120
+ #
121
+ # Override in your own model to add extra data, which will be merged into the actor response
122
+ # generated by Fedipub. You can include extra `@context` for activitypub extensions and it will
123
+ # be merged with the main response context.
124
+ #
125
+ # @example
126
+ # def to_activitypub_object
127
+ # {
128
+ # "@context": {
129
+ # toot: "http://joinmastodon.org/ns#",
130
+ # attributionDomains: {
131
+ # "@id": "toot:attributionDomains",
132
+ # "@type": "@id"
133
+ # }
134
+ # },
135
+ # attributionDomains: [
136
+ # "example.com"
137
+ # ]
138
+ # }
139
+ # end
140
+ def to_activitypub_object
141
+ {}
142
+ end
143
+
144
+ private
145
+
146
+ # Result is used to determine if an actor related to this entity should be created as local actor or not
147
+ #
148
+ # Override it in your models if you need distant actors to be related to another entity.
149
+ def create_fedipub_actor_as_local?
150
+ true
151
+ end
152
+
153
+ def create_fedipub_actor
154
+ Fedipub::Actor.create_with(local: create_fedipub_actor_as_local?).find_or_create_by!(entity: self)
155
+ end
156
+
157
+ def tombstone_fedipub_actor!
158
+ fedipub_actor.presence&.tombstone!
159
+ end
160
+
161
+ def untombstone_fedipub_actor!
162
+ fedipub_actor.presence&.untombstone!
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,18 @@
1
+ module Fedipub
2
+ module Announceable
3
+ extend ActiveSupport::Concern
4
+
5
+ # Announces (boosts) this entity by creating a new Activity.
6
+ #
7
+ # @param actor [Fedipub::Actor] The actor doing the announce; defaults to the entity's own actor.
8
+ #
9
+ # @return [Fedipub::Activity] the newly-created Announce activity
10
+ def announce!(actor: nil)
11
+ actor ||= try(:fedipub_actor) || self
12
+ create_fedipub_activity('Announce',
13
+ actor: actor,
14
+ to: [Fediverse::Collection::PUBLIC],
15
+ cc: [actor.followers_url])
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,25 @@
1
+ module Fedipub
2
+ # Definition for a single local Application Actor, which represents the instance/server itself.
3
+ #
4
+ # Keys from this actor are used to sign outgoing GET requests.
5
+ module ApplicationActor
6
+ extend ActiveSupport::Concern
7
+
8
+ APPLICATION_ACTOR_ATTRIBUTES = {
9
+ username: '__application',
10
+ actor_type: 'Application',
11
+ entity: nil,
12
+ local: true,
13
+ }.freeze
14
+
15
+ included do
16
+ def self.application_actor
17
+ find_or_create_by!(APPLICATION_ACTOR_ATTRIBUTES)
18
+ end
19
+
20
+ def application_actor?
21
+ APPLICATION_ACTOR_ATTRIBUTES.all? { |attribute, value| attributes[attribute.to_s] == value }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,278 @@
1
+ require 'fediverse/inbox'
2
+
3
+ module Fedipub
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 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 :fedipub_actor, foreign_key: true, null: true, default: nil
22
+ #
23
+ # Model must have the following attributes:
24
+ # ```rb
25
+ # add_column :posts, :federated_url, :string, null: true, default: nil
26
+ # add_reference :posts, :fedipub_actor, foreign_key: true, null: true, default: nil
27
+ # ```
28
+ #
29
+ # ## Usage
30
+ #
31
+ # Include the concern in an existing model:
32
+ #
33
+ # ```rb
34
+ # class Post < ApplicationRecord
35
+ # include Fedipub::DataEntity
36
+ # acts_as_fedipub_data options
37
+ #
38
+ # # This will be called when a Delete activity comes for the entry. As we don't know how you want to handle it,
39
+ # # you'll have to implement the behavior yourself.
40
+ # on_fedipub_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_fedipub_undelete_requested :do_something_else
45
+ #
46
+ # def to_activitypub_object
47
+ # Fedipub::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 Fedipub responses:
64
+ # - you will need to send the delete activity yourself
65
+ #
66
+ # ```rb
67
+ # acts_as_fedipub_data handles: 'Note',
68
+ # ...,
69
+ # soft_deleted_method: :deleted?
70
+ # soft_delete_date_method: :deleted_at
71
+ #
72
+ # on_fedipub_delete_requested :soft_delete!
73
+ # on_fedipub_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_fedipub_activity 'Delete' unless local_fedipub_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_fedipub_entity?
87
+ # delete_activity = Activity.find_by action: 'Delete', entity: self
88
+ # send_fedipub_activity 'Undo', entity: delete_activity, actor: fedipub_actor if delete_activity.present?
89
+ # end
90
+ # end
91
+ #
92
+ # def restore_remote_entity!
93
+ # self.deleted_at: nil
94
+ # fedipub_sync!
95
+ # save!
96
+ # end
97
+ # ```
98
+ module DataEntity
99
+ class TombstonedError < StandardError; end
100
+
101
+ extend ActiveSupport::Concern
102
+ include Fedipub::HandlesDeleteRequests
103
+ include Fedipub::Likeable
104
+ include Fedipub::Announceable
105
+
106
+ # Class methods automatically included in the concern.
107
+ module ClassMethods
108
+ # Configures the mapping between entity and Fediverse
109
+ #
110
+ # @param actor_entity_method [Symbol] Method returning an object responding to 'fedipub_actor', for local content
111
+ # @param url_param [Symbol] Column name of the object ID that should be used in URLs. Defaults to +:id+
112
+ # @param route_path_segment [Symbol] Segment used in Fedipub routes to display the ActivityPub representation.
113
+ # Defaults to the pluralized, underscored class name
114
+ # @param handles [String] Type of ActivityPub object handled by this entity type
115
+ # @param with [Symbol] Self class method that will handle incoming objects. Defaults to +:handle_incoming_fediverse_data+
116
+ # @param filter_method [Symbol] Self class method that determines if an incoming object should be handled. Note
117
+ # that the first model for which this method returns true will be used. If left empty, the model CAN be selected,
118
+ # so define them if many models handle the same data type.
119
+ # @param should_federate_method [Symbol] method to determine if an object should be federated. If the method returns false,
120
+ # no create/update activities will happen, and object will not be accessible at federated_url. Defaults to a method
121
+ # that always returns true.
122
+ # @param soft_deleted_method [Symbol, nil] If the model uses a soft-delete mechanism, this is the method to check
123
+ # if entity is soft-deleted. This is not required by the spec but greatly encouraged as the app will return a 410
124
+ # response with a Tombstone object instead of an 404 error.
125
+ # @param soft_delete_date_method [Symbol, nil] Method to get the date of the soft-deletion
126
+ #
127
+ # @example
128
+ # acts_as_fedipub_data handles: 'Note', with: :note_handler, route_path_segment: :articles, actor_entity_method: :user
129
+ # rubocop:disable-next Metrics/ParameterLists, Metrics/MethodLength
130
+ def acts_as_fedipub_data(
131
+ handles:,
132
+ with: :handle_incoming_fediverse_data,
133
+ route_path_segment: nil,
134
+ actor_entity_method: nil,
135
+ url_param: :id,
136
+ filter_method: nil,
137
+ should_federate_method: :default_should_federate?,
138
+ soft_deleted_method: nil,
139
+ soft_delete_date_method: nil
140
+ )
141
+ route_path_segment ||= name.pluralize.underscore
142
+
143
+ Fedipub::Configuration.register_data_type self,
144
+ route_path_segment: route_path_segment,
145
+ actor_entity_method: actor_entity_method,
146
+ url_param: url_param,
147
+ handles: handles,
148
+ with: with,
149
+ filter_method: filter_method,
150
+ should_federate_method: should_federate_method,
151
+ soft_deleted_method: soft_deleted_method,
152
+ soft_delete_date_method: soft_delete_date_method
153
+
154
+ # NOTE: Delete activities cannot be handled like this as we can't be sure to have the object's type
155
+ Fediverse::Inbox.register_handler 'Create', handles, self, with
156
+ Fediverse::Inbox.register_handler 'Update', handles, self, with
157
+ end
158
+
159
+ # Instantiates a new instance from an ActivityPub object
160
+ #
161
+ # @param activitypub_object [Hash]
162
+ #
163
+ # @return [self]
164
+ def new_from_activitypub_object(activitypub_object)
165
+ new from_activitypub_object(activitypub_object)
166
+ end
167
+
168
+ # Creates or updates entity based on the ActivityPub activity
169
+ #
170
+ # @param activity_hash_or_id [Hash, String] Dereferenced activity hash or ID
171
+ #
172
+ # @return [self]
173
+ def handle_incoming_fediverse_data(activity_hash_or_id)
174
+ activity = Fediverse::Request.dereference(activity_hash_or_id)
175
+ object = Fediverse::Request.dereference(activity['object'])
176
+
177
+ entity = Fedipub::Utils::Object.find_or_create!(object)
178
+
179
+ if activity['type'] == 'Update'
180
+ entity.assign_attributes from_activitypub_object(object)
181
+
182
+ # Use timestamps from attributes
183
+ entity.save! touch: false
184
+ end
185
+
186
+ entity
187
+ end
188
+
189
+ def find_untombstoned_by!(**params)
190
+ configuration = Fedipub.data_entity_configuration(self)
191
+ entity = find_by!(**params)
192
+
193
+ raise Fedipub::DataEntity::TombstonedError if configuration[:soft_deleted_method] && entity.send(configuration[:soft_deleted_method])
194
+
195
+ entity
196
+ end
197
+ end
198
+
199
+ included do
200
+ belongs_to :fedipub_actor, class_name: 'Fedipub::Actor'
201
+
202
+ scope :local_fedipub_entities, -> { where federated_url: nil }
203
+ scope :distant_fedipub_entities, -> { where.not(federated_url: nil) }
204
+
205
+ before_validation :set_fedipub_actor
206
+ after_create -> { create_fedipub_activity 'Create' }
207
+ after_update -> { create_fedipub_activity 'Update' }, unless: :fedipub_tombstoned?
208
+ after_destroy -> { create_fedipub_activity 'Delete' }
209
+ end
210
+
211
+ # Computed value for the federated URL
212
+ #
213
+ # @return [String]
214
+ def federated_url
215
+ return nil unless send(fedipub_data_configuration[:should_federate_method])
216
+ return attributes['federated_url'] if attributes['federated_url'].present?
217
+
218
+ path_segment = Fedipub.data_entity_configuration(self)[:route_path_segment]
219
+ url_param = Fedipub.data_entity_configuration(self)[:url_param]
220
+ Fedipub::Engine.routes.url_helpers.server_published_url(publishable_type: path_segment, id: send(url_param))
221
+ end
222
+
223
+ # Check whether the entity was created locally or comes from the Fediverse
224
+ #
225
+ # @return [Boolean]
226
+ def local_fedipub_entity?
227
+ attributes['federated_url'].blank?
228
+ end
229
+
230
+ def fedipub_tombstoned?
231
+ fedipub_data_configuration[:soft_deleted_method] ? send(fedipub_data_configuration[:soft_deleted_method]) : false
232
+ end
233
+
234
+ def fedipub_tombstoned_at
235
+ fedipub_data_configuration[:soft_delete_date_method] ? send(fedipub_data_configuration[:soft_delete_date_method]) : nil
236
+ end
237
+
238
+ def fedipub_data_configuration
239
+ Fedipub.data_entity_configuration(self)
240
+ end
241
+
242
+ def fedipub_sync!
243
+ if local_fedipub_entity?
244
+ Rails.logger.info { "Ignored attempt to sync a local #{self.class.name}" }
245
+ return false
246
+ end
247
+
248
+ object = Fediverse::Request.dereference(federated_url)
249
+
250
+ update! self.class.from_activitypub_object(object)
251
+ end
252
+
253
+ private
254
+
255
+ def set_fedipub_actor
256
+ return fedipub_actor if fedipub_actor.present?
257
+
258
+ self.fedipub_actor = send(fedipub_data_configuration[:actor_entity_method])&.fedipub_actor if fedipub_data_configuration[:actor_entity_method]
259
+
260
+ raise 'Cannot determine actor from configuration' unless fedipub_actor
261
+ end
262
+
263
+ def create_fedipub_activity(action, actor: fedipub_actor, to: nil, cc: nil)
264
+ ensure_fedipub_configuration!
265
+ return unless local_fedipub_entity? && send(fedipub_data_configuration[:should_federate_method])
266
+
267
+ Activity.create! actor: actor, action: action, entity: self, to: to, cc: cc
268
+ end
269
+
270
+ def ensure_fedipub_configuration!
271
+ raise("Entity not configured for #{self.class.name}. Did you use \"acts_as_fedipub_data\"?") unless Fedipub.data_entity? self
272
+ end
273
+
274
+ def default_should_federate?
275
+ true
276
+ end
277
+ end
278
+ end
@@ -0,0 +1,36 @@
1
+ module Fedipub
2
+ # Model concern providing hooks for on_fedipub_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 Fedipub::HandlesDeleteRequests
14
+ #
15
+ # on_fedipub_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_fedipub_delete_requested(*)
23
+ set_callback(:on_fedipub_delete_requested, *)
24
+ end
25
+
26
+ def on_fedipub_undelete_requested(*)
27
+ set_callback(:on_fedipub_undelete_requested, *)
28
+ end
29
+ end
30
+
31
+ included do
32
+ define_callbacks :on_fedipub_delete_requested
33
+ define_callbacks :on_fedipub_undelete_requested
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,61 @@
1
+ module Fedipub
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 Fedipub::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.
26
+ module HasUuid
27
+ extend ActiveSupport::Concern
28
+
29
+ included do
30
+ before_validation :generate_uuid
31
+ validates :uuid, presence: true, uniqueness: true
32
+
33
+ def self.find_param(param)
34
+ find_by!(uuid: param)
35
+ end
36
+ end
37
+
38
+ # @return [String] The UUID
39
+ def to_param
40
+ uuid
41
+ end
42
+
43
+ # @return [String]
44
+ def uuid
45
+ # Override UUID accessor to provide lazy initialization of UUIDs for old data
46
+ if self[:uuid].blank?
47
+ generate_uuid
48
+ save!
49
+ end
50
+ self[:uuid]
51
+ end
52
+
53
+ private
54
+
55
+ def generate_uuid
56
+ return if self[:uuid].present?
57
+
58
+ (self.uuid = SecureRandom.uuid) while self[:uuid].blank? || self.class.exists?(uuid: self[:uuid])
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,29 @@
1
+ module Fedipub
2
+ module Likeable
3
+ extend ActiveSupport::Concern
4
+
5
+ # Likes this entity by creating a new Activity.
6
+ #
7
+ # @param actor [Fedipub::Actor] The actor that is doing the liking.
8
+ #
9
+ # @return [Fedipub::Activity] the newly-created Like activity
10
+ def like!(actor:)
11
+ create_fedipub_activity('Like',
12
+ actor: actor,
13
+ to: [Fediverse::Collection::PUBLIC],
14
+ cc: [actor.followers_url])
15
+ end
16
+
17
+ # Dislikes this entity by creating a new Activity.
18
+ #
19
+ # @param actor [Fedipub::Actor] The actor that is doing the disliking.
20
+ #
21
+ # @return [Fedipub::Activity] the newly-created Dislike activity
22
+ def dislike!(actor:)
23
+ create_fedipub_activity('Dislike',
24
+ actor: actor,
25
+ to: [Fediverse::Collection::PUBLIC],
26
+ cc: [actor.followers_url])
27
+ end
28
+ end
29
+ end