fedipub 0.0.1 → 0.9.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 (132) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +70 -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 +45 -0
  17. data/app/controllers/fedipub/server_controller.rb +37 -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 +166 -0
  24. data/app/models/concerns/fedipub/announceable.rb +18 -0
  25. data/app/models/concerns/fedipub/data_entity.rb +279 -0
  26. data/app/models/concerns/fedipub/handles_delete_requests.rb +36 -0
  27. data/app/models/concerns/fedipub/has_uuid.rb +61 -0
  28. data/app/models/concerns/fedipub/likeable.rb +29 -0
  29. data/app/models/fedipub/activity.rb +62 -0
  30. data/app/models/fedipub/actor.rb +303 -0
  31. data/app/models/fedipub/application_record.rb +5 -0
  32. data/app/models/fedipub/following.rb +72 -0
  33. data/app/models/fedipub/host.rb +48 -0
  34. data/app/policies/fedipub/client/activity_policy.rb +9 -0
  35. data/app/policies/fedipub/client/actor_policy.rb +15 -0
  36. data/app/policies/fedipub/client/following_policy.rb +35 -0
  37. data/app/policies/fedipub/fedipub_policy.rb +63 -0
  38. data/app/policies/fedipub/server/activity_policy.rb +9 -0
  39. data/app/policies/fedipub/server/actor_policy.rb +23 -0
  40. data/app/policies/fedipub/server/following_policy.rb +6 -0
  41. data/app/policies/fedipub/server/publishable_policy.rb +15 -0
  42. data/app/views/fedipub/client/activities/_activity.html.erb +5 -0
  43. data/app/views/fedipub/client/activities/_activity.json.jbuilder +1 -0
  44. data/app/views/fedipub/client/activities/_index.json.jbuilder +1 -0
  45. data/app/views/fedipub/client/activities/feed.html.erb +11 -0
  46. data/app/views/fedipub/client/activities/feed.json.jbuilder +1 -0
  47. data/app/views/fedipub/client/activities/index.html.erb +5 -0
  48. data/app/views/fedipub/client/activities/index.json.jbuilder +1 -0
  49. data/app/views/fedipub/client/actors/_actor.json.jbuilder +17 -0
  50. data/app/views/fedipub/client/actors/_lookup_form.html.erb +5 -0
  51. data/app/views/fedipub/client/actors/gone.html.erb +1 -0
  52. data/app/views/fedipub/client/actors/index.html.erb +44 -0
  53. data/app/views/fedipub/client/actors/index.json.jbuilder +1 -0
  54. data/app/views/fedipub/client/actors/show.html.erb +112 -0
  55. data/app/views/fedipub/client/actors/show.json.jbuilder +1 -0
  56. data/app/views/fedipub/client/common/_client_links.html.erb +26 -0
  57. data/app/views/fedipub/client/followings/_follow.html.erb +4 -0
  58. data/app/views/fedipub/client/followings/_follow_actions.html.erb +36 -0
  59. data/app/views/fedipub/client/followings/_follower.html.erb +7 -0
  60. data/app/views/fedipub/client/followings/_following.json.jbuilder +1 -0
  61. data/app/views/fedipub/client/followings/_form.html.erb +22 -0
  62. data/app/views/fedipub/client/followings/index.html.erb +29 -0
  63. data/app/views/fedipub/client/followings/index.json.jbuilder +1 -0
  64. data/app/views/fedipub/client/followings/show.html.erb +21 -0
  65. data/app/views/fedipub/client/followings/show.json.jbuilder +1 -0
  66. data/app/views/fedipub/server/activities/_activity.activitypub.jbuilder +25 -0
  67. data/app/views/fedipub/server/activities/show.activitypub.jbuilder +1 -0
  68. data/app/views/fedipub/server/actors/_actor.activitypub.jbuilder +27 -0
  69. data/app/views/fedipub/server/actors/_tombstone.activitypub.jbuilder +6 -0
  70. data/app/views/fedipub/server/actors/show.activitypub.jbuilder +5 -0
  71. data/app/views/fedipub/server/followings/_following.activitypub.jbuilder +7 -0
  72. data/app/views/fedipub/server/followings/show.activitypub.jbuilder +1 -0
  73. data/app/views/fedipub/server/nodeinfo/index.nodeinfo.jbuilder +6 -0
  74. data/app/views/fedipub/server/nodeinfo/show.nodeinfo.jbuilder +19 -0
  75. data/app/views/fedipub/server/published/_publishable.activitypub.jbuilder +11 -0
  76. data/app/views/fedipub/server/published/_tombstone.activitypub.jbuilder +6 -0
  77. data/app/views/fedipub/server/published/show.activitypub.jbuilder +5 -0
  78. data/app/views/fedipub/server/shared/ordered_collection.activitypub.jbuilder +6 -0
  79. data/app/views/fedipub/server/shared/ordered_collection_page.activitypub.jbuilder +12 -0
  80. data/app/views/fedipub/server/web_finger/find.jrd.jbuilder +24 -0
  81. data/app/views/fedipub/server/web_finger/host_meta.xrd.erb +5 -0
  82. data/app/views/users/show.html.erb +4 -0
  83. data/config/initializers/mime_types.rb +21 -0
  84. data/config/initializers/security_vocab.rb +53 -0
  85. data/config/routes.rb +53 -0
  86. data/db/migrate/20200712133150_create_federails_actors.rb +22 -0
  87. data/db/migrate/20200712143127_create_federails_followings.rb +14 -0
  88. data/db/migrate/20200712174938_create_federails_activities.rb +11 -0
  89. data/db/migrate/20241002094500_add_uuids.rb +13 -0
  90. data/db/migrate/20241002094501_add_keypair_to_actors.rb +8 -0
  91. data/db/migrate/20250122160618_add_extensions_to_federails_actors.rb +5 -0
  92. data/db/migrate/20250301082500_add_local_to_actors.rb +11 -0
  93. data/db/migrate/20250329123939_add_actor_type_to_actors.rb +5 -0
  94. data/db/migrate/20250329123940_add_tombstoned_at_to_actors.rb +5 -0
  95. data/db/migrate/20250426061729_create_federails_hosts.rb +22 -0
  96. data/db/migrate/20251121160720_add_to_and_cc_to_federails_activities.rb +6 -0
  97. data/db/migrate/20260421170640_add_result_and_instrument_to_federails_activities.rb +6 -0
  98. data/db/migrate/20260723113716_rename_federails_to_fedipub.rb +15 -0
  99. data/lib/fedipub/configuration.rb +119 -0
  100. data/lib/fedipub/data_transformer/note.rb +36 -0
  101. data/lib/fedipub/engine.rb +11 -0
  102. data/lib/fedipub/maintenance/actors_updater.rb +66 -0
  103. data/lib/fedipub/maintenance/hosts_updater.rb +19 -0
  104. data/lib/fedipub/utils/actor.rb +85 -0
  105. data/lib/fedipub/utils/context.rb +12 -0
  106. data/lib/fedipub/utils/host.rb +54 -0
  107. data/lib/fedipub/utils/json_request.rb +41 -0
  108. data/lib/fedipub/utils/object.rb +131 -0
  109. data/lib/fedipub/utils/response_codes.rb +11 -0
  110. data/lib/fedipub/version.rb +1 -3
  111. data/lib/fedipub.rb +127 -4
  112. data/lib/fediverse/collection.rb +31 -0
  113. data/lib/fediverse/inbox.rb +110 -0
  114. data/lib/fediverse/node_info.rb +38 -0
  115. data/lib/fediverse/notifier.rb +90 -0
  116. data/lib/fediverse/request.rb +29 -0
  117. data/lib/fediverse/signature.rb +49 -0
  118. data/lib/fediverse/webfinger.rb +140 -0
  119. data/lib/fediverse.rb +5 -0
  120. data/lib/generators/fedipub/copy_client_policies/USAGE +8 -0
  121. data/lib/generators/fedipub/copy_client_policies/copy_client_policies_generator.rb +9 -0
  122. data/lib/generators/fedipub/copy_client_views/USAGE +8 -0
  123. data/lib/generators/fedipub/copy_client_views/copy_client_views_generator.rb +9 -0
  124. data/lib/generators/fedipub/copy_factories/USAGE +8 -0
  125. data/lib/generators/fedipub/copy_factories/copy_factories_generator.rb +43 -0
  126. data/lib/generators/fedipub/install/USAGE +9 -0
  127. data/lib/generators/fedipub/install/install_generator.rb +10 -0
  128. data/lib/generators/fedipub/install/templates/fedipub.rb +1 -0
  129. data/lib/generators/fedipub/install/templates/fedipub.yml +27 -0
  130. data/lib/tasks/factory_bot.rake +15 -0
  131. data/lib/tasks/fedipub_tasks.rake +13 -0
  132. metadata +259 -10
@@ -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,166 @@
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 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
+ # rubocop:enable Metrics/ParameterLists
61
+
62
+ # Define a method that will be called after the entity receives a follow request.
63
+ # The follow request will be passed as an argument to the method.
64
+ #
65
+ # @param method_name [Symbol] The name of the method to call, or a block that will be called directly
66
+ #
67
+ # @example
68
+ # after_followed :accept_follow
69
+ def after_followed(method_name)
70
+ @after_followed = method_name
71
+ end
72
+
73
+ # Define a method that will be called after a follow request made by the entity is accepted
74
+ # The accepted follow request will be passed as an argument to the method.
75
+ #
76
+ # @param method_name [Symbol] The name of the method to call, or a block that will be called directly
77
+ #
78
+ # @example
79
+ # after_follow_accepted :follow_accepted
80
+ def after_follow_accepted(method_name)
81
+ @after_follow_accepted = method_name
82
+ end
83
+
84
+ # Define a method that will be called after an activity has been received
85
+ #
86
+ # @param activity_type [String] The activity action to handle, e.g. 'Create'. If you specify '*', the handler will be called for any activity type.
87
+ # @param object_type [String] The object type to handle, e.g. 'Note'. If you specify '*', the handler will be called for any object type.
88
+ # @param method_name [Symbol] The name of the class method to call. The method will receive the complete activity payload as a parameter.
89
+ #
90
+ # @example
91
+ # after_activity_received 'Create', 'Note', :create_note
92
+ def after_activity_received(activity_type, object_type, method_name)
93
+ Fediverse::Inbox.register_handler(activity_type, object_type, self, method_name)
94
+ end
95
+
96
+ private
97
+
98
+ def dispatch_callback(name, instance, *)
99
+ case name
100
+ when :after_followed
101
+ instance.send(@after_followed, *) if @after_followed
102
+ when :after_follow_accepted
103
+ instance.send(@after_follow_accepted, *) if @after_follow_accepted
104
+ end
105
+ end
106
+ end
107
+
108
+ included do
109
+ # No "dependent: :xyz" as the "before_destroy" hook should have nullified the actor
110
+ has_one :fedipub_actor, class_name: 'Fedipub::Actor', as: :entity # rubocop:disable Rails/HasManyOrHasOneDependent
111
+
112
+ after_create :create_fedipub_actor, if: lambda {
113
+ raise("Entity not configured for #{self.class.name}. Did you use \"acts_as_fedipub_actor\"?") unless Fedipub.actor_entity? self
114
+
115
+ Fedipub.actor_entity(self)[:auto_create_actors]
116
+ }
117
+ before_destroy :tombstone_fedipub_actor!
118
+ end
119
+
120
+ # Add custom data to actor responses.
121
+ #
122
+ # Override in your own model to add extra data, which will be merged into the actor response
123
+ # generated by Fedipub. You can include extra `@context` for activitypub extensions and it will
124
+ # be merged with the main response context.
125
+ #
126
+ # @example
127
+ # def to_activitypub_object
128
+ # {
129
+ # "@context": {
130
+ # toot: "http://joinmastodon.org/ns#",
131
+ # attributionDomains: {
132
+ # "@id": "toot:attributionDomains",
133
+ # "@type": "@id"
134
+ # }
135
+ # },
136
+ # attributionDomains: [
137
+ # "example.com"
138
+ # ]
139
+ # }
140
+ # end
141
+ def to_activitypub_object
142
+ {}
143
+ end
144
+
145
+ private
146
+
147
+ # Result is used to determine if an actor related to this entity should be created as local actor or not
148
+ #
149
+ # Override it in your models if you need distant actors to be related to another entity.
150
+ def create_fedipub_actor_as_local?
151
+ true
152
+ end
153
+
154
+ def create_fedipub_actor
155
+ Fedipub::Actor.create_with(local: create_fedipub_actor_as_local?).find_or_create_by!(entity: self)
156
+ end
157
+
158
+ def tombstone_fedipub_actor!
159
+ fedipub_actor.presence&.tombstone!
160
+ end
161
+
162
+ def untombstone_fedipub_actor!
163
+ fedipub_actor.presence&.untombstone!
164
+ end
165
+ end
166
+ 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,279 @@
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 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
+ # rubocop:enable Metrics/ParameterLists, Metrics/MethodLength
159
+
160
+ # Instantiates a new instance from an ActivityPub object
161
+ #
162
+ # @param activitypub_object [Hash]
163
+ #
164
+ # @return [self]
165
+ def new_from_activitypub_object(activitypub_object)
166
+ new from_activitypub_object(activitypub_object)
167
+ end
168
+
169
+ # Creates or updates entity based on the ActivityPub activity
170
+ #
171
+ # @param activity_hash_or_id [Hash, String] Dereferenced activity hash or ID
172
+ #
173
+ # @return [self]
174
+ def handle_incoming_fediverse_data(activity_hash_or_id)
175
+ activity = Fediverse::Request.dereference(activity_hash_or_id)
176
+ object = Fediverse::Request.dereference(activity['object'])
177
+
178
+ entity = Fedipub::Utils::Object.find_or_create!(object)
179
+
180
+ if activity['type'] == 'Update'
181
+ entity.assign_attributes from_activitypub_object(object)
182
+
183
+ # Use timestamps from attributes
184
+ entity.save! touch: false
185
+ end
186
+
187
+ entity
188
+ end
189
+
190
+ def find_untombstoned_by!(**params)
191
+ configuration = Fedipub.data_entity_configuration(self)
192
+ entity = find_by!(**params)
193
+
194
+ raise Fedipub::DataEntity::TombstonedError if configuration[:soft_deleted_method] && entity.send(configuration[:soft_deleted_method])
195
+
196
+ entity
197
+ end
198
+ end
199
+
200
+ included do
201
+ belongs_to :fedipub_actor, class_name: 'Fedipub::Actor'
202
+
203
+ scope :local_fedipub_entities, -> { where federated_url: nil }
204
+ scope :distant_fedipub_entities, -> { where.not(federated_url: nil) }
205
+
206
+ before_validation :set_fedipub_actor
207
+ after_create -> { create_fedipub_activity 'Create' }
208
+ after_update -> { create_fedipub_activity 'Update' }, unless: :fedipub_tombstoned?
209
+ after_destroy -> { create_fedipub_activity 'Delete' }
210
+ end
211
+
212
+ # Computed value for the federated URL
213
+ #
214
+ # @return [String]
215
+ def federated_url
216
+ return nil unless send(fedipub_data_configuration[:should_federate_method])
217
+ return attributes['federated_url'] if attributes['federated_url'].present?
218
+
219
+ path_segment = Fedipub.data_entity_configuration(self)[:route_path_segment]
220
+ url_param = Fedipub.data_entity_configuration(self)[:url_param]
221
+ Fedipub::Engine.routes.url_helpers.server_published_url(publishable_type: path_segment, id: send(url_param))
222
+ end
223
+
224
+ # Check whether the entity was created locally or comes from the Fediverse
225
+ #
226
+ # @return [Boolean]
227
+ def local_fedipub_entity?
228
+ attributes['federated_url'].blank?
229
+ end
230
+
231
+ def fedipub_tombstoned?
232
+ fedipub_data_configuration[:soft_deleted_method] ? send(fedipub_data_configuration[:soft_deleted_method]) : false
233
+ end
234
+
235
+ def fedipub_tombstoned_at
236
+ fedipub_data_configuration[:soft_delete_date_method] ? send(fedipub_data_configuration[:soft_delete_date_method]) : nil
237
+ end
238
+
239
+ def fedipub_data_configuration
240
+ Fedipub.data_entity_configuration(self)
241
+ end
242
+
243
+ def fedipub_sync!
244
+ if local_fedipub_entity?
245
+ Rails.logger.info { "Ignored attempt to sync a local #{self.class.name}" }
246
+ return false
247
+ end
248
+
249
+ object = Fediverse::Request.dereference(federated_url)
250
+
251
+ update! self.class.from_activitypub_object(object)
252
+ end
253
+
254
+ private
255
+
256
+ def set_fedipub_actor
257
+ return fedipub_actor if fedipub_actor.present?
258
+
259
+ self.fedipub_actor = send(fedipub_data_configuration[:actor_entity_method])&.fedipub_actor if fedipub_data_configuration[:actor_entity_method]
260
+
261
+ raise 'Cannot determine actor from configuration' unless fedipub_actor
262
+ end
263
+
264
+ def create_fedipub_activity(action, actor: fedipub_actor, to: nil, cc: nil)
265
+ ensure_fedipub_configuration!
266
+ return unless local_fedipub_entity? && send(fedipub_data_configuration[:should_federate_method])
267
+
268
+ Activity.create! actor: actor, action: action, entity: self, to: to, cc: cc
269
+ end
270
+
271
+ def ensure_fedipub_configuration!
272
+ raise("Entity not configured for #{self.class.name}. Did you use \"acts_as_fedipub_data\"?") unless Fedipub.data_entity? self
273
+ end
274
+
275
+ def default_should_federate?
276
+ true
277
+ end
278
+ end
279
+ 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
@@ -0,0 +1,62 @@
1
+ module Fedipub
2
+ # Activities can be compared to a log of what happened in the Fediverse.
3
+ #
4
+ # Activities from local actors ends in the actors _outboxes_.
5
+ # Activities form distant actors comes from the actor's _inbox_.
6
+ # We try to only keep activities _from_ local actors, and external activities _targetting_ local actors.
7
+ #
8
+ # See also:
9
+ # - https://www.w3.org/TR/activitypub/#outbox
10
+ # - https://www.w3.org/TR/activitypub/#inbox
11
+ class Activity < ApplicationRecord
12
+ include Fedipub::HasUuid
13
+
14
+ belongs_to :entity, polymorphic: true
15
+ belongs_to :actor
16
+
17
+ scope :feed_for, lambda { |actor|
18
+ actor_ids = []
19
+ Following.accepted.where(actor: actor).find_each do |following|
20
+ actor_ids << following.target_actor_id
21
+ end
22
+ where(actor_id: actor_ids)
23
+ }
24
+
25
+ after_create_commit :post_to_inboxes
26
+
27
+ before_validation :set_default_addressing, on: :create
28
+
29
+ serialize :cc, coder: YAML
30
+ serialize :to, coder: YAML
31
+ serialize :result, coder: YAML
32
+ serialize :instrument, coder: YAML
33
+
34
+ # Sends an Undo activity to reverse this one.
35
+ #
36
+ # Used for unfollowing, unliking, unboosting, etc.
37
+ #
38
+ # @return [Fedipub::Activity] the new Undo activity
39
+ def undo!
40
+ Activity.create! actor: actor, action: 'Undo', entity: self, to: to, cc: cc
41
+ end
42
+
43
+ private
44
+
45
+ # Sets up default public-and-followers addressing unless to and cc are already set
46
+ #
47
+ # This retains compatibility with previous behaviour
48
+ def set_default_addressing
49
+ return if to.present? || cc.present?
50
+
51
+ self.to = [Fediverse::Collection::PUBLIC]
52
+ self.cc = [
53
+ actor.followers_url,
54
+ (entity.try(:followers_url) if entity.try(:local?)),
55
+ ].compact.uniq
56
+ end
57
+
58
+ def post_to_inboxes
59
+ NotifyInboxJob.perform_later(self)
60
+ end
61
+ end
62
+ end