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.
- checksums.yaml +4 -4
- data/README.md +70 -2
- data/Rakefile +5 -9
- data/app/assets/config/fedipub_manifest.js +1 -0
- data/app/assets/stylesheets/fedipub/application.css +15 -0
- data/app/controllers/concerns/fedipub/server/render_collections.rb +19 -0
- data/app/controllers/fedipub/client/activities_controller.rb +27 -0
- data/app/controllers/fedipub/client/actors_controller.rb +54 -0
- data/app/controllers/fedipub/client/followings_controller.rb +103 -0
- data/app/controllers/fedipub/client_controller.rb +9 -0
- data/app/controllers/fedipub/server/activities_controller.rb +78 -0
- data/app/controllers/fedipub/server/actors_controller.rb +50 -0
- data/app/controllers/fedipub/server/followings_controller.rb +19 -0
- data/app/controllers/fedipub/server/nodeinfo_controller.rb +28 -0
- data/app/controllers/fedipub/server/published_controller.rb +30 -0
- data/app/controllers/fedipub/server/web_finger_controller.rb +45 -0
- data/app/controllers/fedipub/server_controller.rb +37 -0
- data/app/helpers/fedipub/server_helper.rb +18 -0
- data/app/jobs/fedipub/application_job.rb +10 -0
- data/app/jobs/fedipub/fetch_nodeinfo_job.rb +10 -0
- data/app/jobs/fedipub/notify_inbox_job.rb +10 -0
- data/app/mailers/fedipub/application_mailer.rb +6 -0
- data/app/models/concerns/fedipub/actor_entity.rb +166 -0
- data/app/models/concerns/fedipub/announceable.rb +18 -0
- data/app/models/concerns/fedipub/data_entity.rb +279 -0
- data/app/models/concerns/fedipub/handles_delete_requests.rb +36 -0
- data/app/models/concerns/fedipub/has_uuid.rb +61 -0
- data/app/models/concerns/fedipub/likeable.rb +29 -0
- data/app/models/fedipub/activity.rb +62 -0
- data/app/models/fedipub/actor.rb +303 -0
- data/app/models/fedipub/application_record.rb +5 -0
- data/app/models/fedipub/following.rb +72 -0
- data/app/models/fedipub/host.rb +48 -0
- data/app/policies/fedipub/client/activity_policy.rb +9 -0
- data/app/policies/fedipub/client/actor_policy.rb +15 -0
- data/app/policies/fedipub/client/following_policy.rb +35 -0
- data/app/policies/fedipub/fedipub_policy.rb +63 -0
- data/app/policies/fedipub/server/activity_policy.rb +9 -0
- data/app/policies/fedipub/server/actor_policy.rb +23 -0
- data/app/policies/fedipub/server/following_policy.rb +6 -0
- data/app/policies/fedipub/server/publishable_policy.rb +15 -0
- data/app/views/fedipub/client/activities/_activity.html.erb +5 -0
- data/app/views/fedipub/client/activities/_activity.json.jbuilder +1 -0
- data/app/views/fedipub/client/activities/_index.json.jbuilder +1 -0
- data/app/views/fedipub/client/activities/feed.html.erb +11 -0
- data/app/views/fedipub/client/activities/feed.json.jbuilder +1 -0
- data/app/views/fedipub/client/activities/index.html.erb +5 -0
- data/app/views/fedipub/client/activities/index.json.jbuilder +1 -0
- data/app/views/fedipub/client/actors/_actor.json.jbuilder +17 -0
- data/app/views/fedipub/client/actors/_lookup_form.html.erb +5 -0
- data/app/views/fedipub/client/actors/gone.html.erb +1 -0
- data/app/views/fedipub/client/actors/index.html.erb +44 -0
- data/app/views/fedipub/client/actors/index.json.jbuilder +1 -0
- data/app/views/fedipub/client/actors/show.html.erb +112 -0
- data/app/views/fedipub/client/actors/show.json.jbuilder +1 -0
- data/app/views/fedipub/client/common/_client_links.html.erb +26 -0
- data/app/views/fedipub/client/followings/_follow.html.erb +4 -0
- data/app/views/fedipub/client/followings/_follow_actions.html.erb +36 -0
- data/app/views/fedipub/client/followings/_follower.html.erb +7 -0
- data/app/views/fedipub/client/followings/_following.json.jbuilder +1 -0
- data/app/views/fedipub/client/followings/_form.html.erb +22 -0
- data/app/views/fedipub/client/followings/index.html.erb +29 -0
- data/app/views/fedipub/client/followings/index.json.jbuilder +1 -0
- data/app/views/fedipub/client/followings/show.html.erb +21 -0
- data/app/views/fedipub/client/followings/show.json.jbuilder +1 -0
- data/app/views/fedipub/server/activities/_activity.activitypub.jbuilder +25 -0
- data/app/views/fedipub/server/activities/show.activitypub.jbuilder +1 -0
- data/app/views/fedipub/server/actors/_actor.activitypub.jbuilder +27 -0
- data/app/views/fedipub/server/actors/_tombstone.activitypub.jbuilder +6 -0
- data/app/views/fedipub/server/actors/show.activitypub.jbuilder +5 -0
- data/app/views/fedipub/server/followings/_following.activitypub.jbuilder +7 -0
- data/app/views/fedipub/server/followings/show.activitypub.jbuilder +1 -0
- data/app/views/fedipub/server/nodeinfo/index.nodeinfo.jbuilder +6 -0
- data/app/views/fedipub/server/nodeinfo/show.nodeinfo.jbuilder +19 -0
- data/app/views/fedipub/server/published/_publishable.activitypub.jbuilder +11 -0
- data/app/views/fedipub/server/published/_tombstone.activitypub.jbuilder +6 -0
- data/app/views/fedipub/server/published/show.activitypub.jbuilder +5 -0
- data/app/views/fedipub/server/shared/ordered_collection.activitypub.jbuilder +6 -0
- data/app/views/fedipub/server/shared/ordered_collection_page.activitypub.jbuilder +12 -0
- data/app/views/fedipub/server/web_finger/find.jrd.jbuilder +24 -0
- data/app/views/fedipub/server/web_finger/host_meta.xrd.erb +5 -0
- data/app/views/users/show.html.erb +4 -0
- data/config/initializers/mime_types.rb +21 -0
- data/config/initializers/security_vocab.rb +53 -0
- data/config/routes.rb +53 -0
- data/db/migrate/20200712133150_create_federails_actors.rb +22 -0
- data/db/migrate/20200712143127_create_federails_followings.rb +14 -0
- data/db/migrate/20200712174938_create_federails_activities.rb +11 -0
- data/db/migrate/20241002094500_add_uuids.rb +13 -0
- data/db/migrate/20241002094501_add_keypair_to_actors.rb +8 -0
- data/db/migrate/20250122160618_add_extensions_to_federails_actors.rb +5 -0
- data/db/migrate/20250301082500_add_local_to_actors.rb +11 -0
- data/db/migrate/20250329123939_add_actor_type_to_actors.rb +5 -0
- data/db/migrate/20250329123940_add_tombstoned_at_to_actors.rb +5 -0
- data/db/migrate/20250426061729_create_federails_hosts.rb +22 -0
- data/db/migrate/20251121160720_add_to_and_cc_to_federails_activities.rb +6 -0
- data/db/migrate/20260421170640_add_result_and_instrument_to_federails_activities.rb +6 -0
- data/db/migrate/20260723113716_rename_federails_to_fedipub.rb +15 -0
- data/lib/fedipub/configuration.rb +119 -0
- data/lib/fedipub/data_transformer/note.rb +36 -0
- data/lib/fedipub/engine.rb +11 -0
- data/lib/fedipub/maintenance/actors_updater.rb +66 -0
- data/lib/fedipub/maintenance/hosts_updater.rb +19 -0
- data/lib/fedipub/utils/actor.rb +85 -0
- data/lib/fedipub/utils/context.rb +12 -0
- data/lib/fedipub/utils/host.rb +54 -0
- data/lib/fedipub/utils/json_request.rb +41 -0
- data/lib/fedipub/utils/object.rb +131 -0
- data/lib/fedipub/utils/response_codes.rb +11 -0
- data/lib/fedipub/version.rb +1 -3
- data/lib/fedipub.rb +127 -4
- data/lib/fediverse/collection.rb +31 -0
- data/lib/fediverse/inbox.rb +110 -0
- data/lib/fediverse/node_info.rb +38 -0
- data/lib/fediverse/notifier.rb +90 -0
- data/lib/fediverse/request.rb +29 -0
- data/lib/fediverse/signature.rb +49 -0
- data/lib/fediverse/webfinger.rb +140 -0
- data/lib/fediverse.rb +5 -0
- data/lib/generators/fedipub/copy_client_policies/USAGE +8 -0
- data/lib/generators/fedipub/copy_client_policies/copy_client_policies_generator.rb +9 -0
- data/lib/generators/fedipub/copy_client_views/USAGE +8 -0
- data/lib/generators/fedipub/copy_client_views/copy_client_views_generator.rb +9 -0
- data/lib/generators/fedipub/copy_factories/USAGE +8 -0
- data/lib/generators/fedipub/copy_factories/copy_factories_generator.rb +43 -0
- data/lib/generators/fedipub/install/USAGE +9 -0
- data/lib/generators/fedipub/install/install_generator.rb +10 -0
- data/lib/generators/fedipub/install/templates/fedipub.rb +1 -0
- data/lib/generators/fedipub/install/templates/fedipub.yml +27 -0
- data/lib/tasks/factory_bot.rake +15 -0
- data/lib/tasks/fedipub_tasks.rake +13 -0
- metadata +259 -10
|
@@ -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
|