federails 0.4.0 → 0.5.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/LICENSE +21 -0
- data/README.md +4 -199
- data/app/controllers/federails/server/published_controller.rb +30 -0
- data/app/models/concerns/federails/actor_entity.rb +90 -58
- data/app/models/concerns/federails/data_entity.rb +178 -0
- data/app/models/concerns/federails/has_uuid.rb +27 -1
- data/app/models/federails/activity.rb +12 -0
- data/app/models/federails/actor.rb +7 -0
- data/app/models/federails/following.rb +1 -0
- data/app/policies/federails/server/publishable_policy.rb +15 -0
- data/app/views/federails/server/published/_publishable.activitypub.jbuilder +11 -0
- data/app/views/federails/server/published/show.activitypub.jbuilder +1 -0
- data/config/routes.rb +4 -0
- data/lib/federails/configuration.rb +19 -0
- data/lib/federails/data_transformer/note.rb +31 -0
- data/lib/federails/utils/object.rb +106 -0
- data/lib/federails/version.rb +1 -1
- data/lib/federails.rb +54 -0
- data/lib/fediverse/inbox.rb +15 -4
- data/lib/fediverse/notifier.rb +3 -0
- data/lib/fediverse/request.rb +13 -0
- data/lib/fediverse/webfinger.rb +59 -9
- data/lib/fediverse.rb +3 -0
- metadata +12 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9e6d475999161590317e4a038b44829fdae2a44ccb007b03f24c240fccfa0771
|
4
|
+
data.tar.gz: b2266e7c4e9ac8c68177e2b17fc2e74afab52fc0e93607943716e06b39c10f6d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 14c482129bac20d5dfdaa2fa498070ab215819a0fe6af6499e268d631f40a7c15ee3341e999e6b0bfee5fd997b7a41f879c35537aeebbfbbdfc6182289d5edf4
|
7
|
+
data.tar.gz: 0c6fe55888c7b31f93958e258ad4b137aa305269056b2b512af2a137e2a15ac6de36a0a83f7fbec436c6657fca3c4ad9c18c08bb46a93e48601ef226c9a0011c
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2020 Experiments Labs / Experimentations
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
CHANGED
@@ -23,206 +23,11 @@ The general direction is to be able to:
|
|
23
23
|
- implement some or all the parts of the RFC labelled with **SHOULD** and **SHOULD NOT**
|
24
24
|
- maybe implement the parts of the RFC labelled with **MAY**
|
25
25
|
|
26
|
-
##
|
26
|
+
## Documentation
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
gem "federails"
|
32
|
-
```
|
33
|
-
|
34
|
-
And then execute:
|
35
|
-
|
36
|
-
```bash
|
37
|
-
$ bundle
|
38
|
-
```
|
39
|
-
|
40
|
-
### Configuration
|
41
|
-
|
42
|
-
Generate configuration files:
|
43
|
-
|
44
|
-
```sh
|
45
|
-
bundle exec rails generate federails:install
|
46
|
-
```
|
47
|
-
|
48
|
-
It creates an initializer and a configuration file:
|
49
|
-
- `config/initializers/federails.rb`
|
50
|
-
- `config/federails.yml`
|
51
|
-
|
52
|
-
By default, Federails is configured using `config_from` method, that loads the appropriate YAML file, but you may want
|
53
|
-
to configure it differently:
|
54
|
-
|
55
|
-
```rb
|
56
|
-
# config/initializers/federails.rb
|
57
|
-
Federails.configure do |config|
|
58
|
-
config.host = 'localhost'
|
59
|
-
# ...
|
60
|
-
end
|
61
|
-
```
|
62
|
-
|
63
|
-
For now, refer to [the source code](lib/federails/configuration.rb) for the full list of options.
|
64
|
-
|
65
|
-
### Routes
|
66
|
-
|
67
|
-
Mount the engine on `/`: routes to `/.well-known/*` and `/nodeinfo/*` must be at the root of the site.
|
68
|
-
Federails routes are then available under the configured path (`routes_path`):
|
69
|
-
|
70
|
-
```rb
|
71
|
-
# config/routes.rb
|
72
|
-
mount Federails::Engine => '/'
|
73
|
-
```
|
74
|
-
|
75
|
-
With `routes_path = 'federation'`, routes will be:
|
76
|
-
|
77
|
-
```txt
|
78
|
-
/.well-known/webfinger(.:format)
|
79
|
-
/.well-known/host-meta(.:format)
|
80
|
-
/.well-known/nodeinfo(.:format)
|
81
|
-
/nodeinfo/2.0(.:format)
|
82
|
-
/federation/actors/:id/followers(.:format)
|
83
|
-
/federation/actors/:id/following(.:format)
|
84
|
-
/federation/actors/:actor_id/outbox(.:format)
|
85
|
-
/federation/actors/:actor_id/inbox(.:format)
|
86
|
-
/federation/actors/:actor_id/activities/:id(.:format)
|
87
|
-
/federation/actors/:actor_id/followings/:id(.:format)
|
88
|
-
/federation/actors/:actor_id/notes/:id(.:format)
|
89
|
-
/federation/actors/:id(.:format)
|
90
|
-
...
|
91
|
-
```
|
92
|
-
|
93
|
-
Some routes can be disabled in configuration if you don't want to expose particular features:
|
94
|
-
|
95
|
-
```rb
|
96
|
-
Federails.configure do |config|
|
97
|
-
# Disable routing for .well-known and nodeinfo
|
98
|
-
config.enable_discovery = false
|
99
|
-
|
100
|
-
# Disable web client UI routes
|
101
|
-
config.client_routes_path = nil
|
102
|
-
end
|
103
|
-
```
|
104
|
-
|
105
|
-
#### Remote following
|
106
|
-
|
107
|
-
By default, remote follow requests (where you press a follow button on another server and get redirected home to complete the follow)
|
108
|
-
will use the built-in client paths. If you're not using the client, or want to provide your own user interface, you can set the path like this, assuming that `new_follow_url` is a valid route in your app. A `uri` query parameter template will be automatically appended, you don't need to specify that.
|
109
|
-
|
110
|
-
```rb
|
111
|
-
Federails.configure do |config|
|
112
|
-
config.remote_follow_url_method = :new_follow_url
|
113
|
-
end
|
114
|
-
```
|
115
|
-
|
116
|
-
### Migrations
|
117
|
-
|
118
|
-
Copy the migrations:
|
119
|
-
|
120
|
-
```sh
|
121
|
-
bundle exec rails federails:install:migrations
|
122
|
-
```
|
123
|
-
|
124
|
-
### User model
|
125
|
-
|
126
|
-
In the ActivityPub world, we refer to _actors_ to represent the thing that publishes or subscribe to _other actors_.
|
127
|
-
|
128
|
-
Federails provides a concern to include in your "user" model or whatever will publish data:
|
129
|
-
|
130
|
-
```rb
|
131
|
-
# app/models/user.rb
|
132
|
-
|
133
|
-
class User < ApplicationRecord
|
134
|
-
# Include the concern here:
|
135
|
-
include Federails::ActorEntity
|
136
|
-
|
137
|
-
# Configure field names
|
138
|
-
acts_as_federails_actor username_field: :username, name_field: :name, profile_url_method: :user_url
|
139
|
-
end
|
140
|
-
```
|
141
|
-
|
142
|
-
This concern automatically create a `Federails::Actor` after a user creation, as well as the `actor` reference. When adding it to
|
143
|
-
an existing model with existing data, you will need to generate the corresponding actors yourself in a migration.
|
144
|
-
|
145
|
-
Usage example:
|
146
|
-
|
147
|
-
```rb
|
148
|
-
actor = User.find(1).federails_actor
|
149
|
-
|
150
|
-
actor.inbox
|
151
|
-
actor.outbox
|
152
|
-
actor.followers
|
153
|
-
actor.following
|
154
|
-
#...
|
155
|
-
```
|
156
|
-
|
157
|
-
### Using the Federails client
|
158
|
-
|
159
|
-
Federails comes with a client, enabled by default, that provides basic views to display and interact with Federails data,
|
160
|
-
accessible on `/app` by default (changeable with the configuration option `client_routes_path`)
|
161
|
-
|
162
|
-
If it's a good starting point, it might be disabled once you made your own integration by setting `client_routes_path`
|
163
|
-
to a `nil` value.
|
164
|
-
|
165
|
-
If you want to override the client's views, copy them in your application:
|
166
|
-
|
167
|
-
```sh
|
168
|
-
rails generate federails:copy_client_views
|
169
|
-
```
|
170
|
-
|
171
|
-
## Common questions
|
172
|
-
|
173
|
-
- **I override the base controller and the links breaks in my layout**
|
174
|
-
|
175
|
-
Use `main_app.<url_helper>` for links to your application; `federails.<federails_url_helper>` for links to the Federails client.
|
176
|
-
- **I specified a custom layout and the links breaks in it**
|
177
|
-
|
178
|
-
Use `main_app.<url_helper>` for links to your application; `federails.<federails_url_helper>` for links to the Federails client.
|
179
|
-
- **I specified a custom layout and my helpers are not available**
|
180
|
-
|
181
|
-
You will have better results if you specify a `base_controller` from your application as Federails base controller is isolated from the main app and does not have access to its helpers.
|
182
|
-
|
183
|
-
## Contributing
|
184
|
-
|
185
|
-
Contributions are welcome, may it be issues, ideas, code or whatever you want to share. Please note:
|
186
|
-
|
187
|
-
- This project is _fast forward_ only: we don't do merge commits
|
188
|
-
- We adhere to [semantic versioning](). Please update the changelog in your commits
|
189
|
-
- We try to adhere to [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) principles
|
190
|
-
- We _may_ rename your commits before merging them
|
191
|
-
- We _may_ split your commits before merging them
|
192
|
-
|
193
|
-
To contribute:
|
194
|
-
|
195
|
-
1. Fork this repository
|
196
|
-
2. Create small commits
|
197
|
-
3. Ideally create small pull requests. Don't hesitate to open them early so we all can follow how it's going
|
198
|
-
4. Get congratulated
|
199
|
-
|
200
|
-
### Tooling
|
201
|
-
|
202
|
-
#### RSpec
|
203
|
-
|
204
|
-
RSpec is the test suite. Start it with
|
205
|
-
|
206
|
-
```sh
|
207
|
-
bundle exec rspec
|
208
|
-
```
|
209
|
-
|
210
|
-
#### Rubocop
|
211
|
-
|
212
|
-
Rubocop is a linter. Start it with
|
213
|
-
|
214
|
-
```sh
|
215
|
-
bundle exec rubocop
|
216
|
-
```
|
217
|
-
|
218
|
-
#### FactoryBot
|
219
|
-
|
220
|
-
FactoryBot is a factory generator used in tests and development.
|
221
|
-
A rake task checks the replayability of the factories and traits:
|
222
|
-
|
223
|
-
```sh
|
224
|
-
bundle exec app:factory_bot:lint
|
225
|
-
```
|
28
|
+
- [Usage](docs/usage.md)
|
29
|
+
- [Common questions](docs/faq.md)
|
30
|
+
- [Contributing](docs/contributing.md)
|
226
31
|
|
227
32
|
## License
|
228
33
|
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Federails
|
2
|
+
module Server
|
3
|
+
# Controller to render ActivityPub representation of entities configured with Federails::DataEntity
|
4
|
+
class PublishedController < Federails::ServerController
|
5
|
+
def show
|
6
|
+
@publishable = type_scope.find_by!(url_param => params[:id])
|
7
|
+
authorize @publishable, policy_class: Federails::Server::PublishablePolicy
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def publishable_config
|
13
|
+
return @publishable_config if instance_variable_defined? :@publishable_config
|
14
|
+
|
15
|
+
_, @publishable_config = Federails.configuration.data_types.find { |_, v| v[:route_path_segment].to_s == params[:publishable_type] }
|
16
|
+
raise ActiveRecord::RecordNotFound, "Invalid #{params[:publishable_type]} type" unless @publishable_config
|
17
|
+
|
18
|
+
@publishable_config
|
19
|
+
end
|
20
|
+
|
21
|
+
def url_param
|
22
|
+
publishable_config[:url_param]
|
23
|
+
end
|
24
|
+
|
25
|
+
def type_scope
|
26
|
+
publishable_config[:class].all
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -1,51 +1,45 @@
|
|
1
1
|
module Federails
|
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_ `Federails::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 Federails::ActorEntity
|
19
|
+
# acts_as_federails_actor options
|
20
|
+
# end
|
21
|
+
# ```
|
2
22
|
module ActorEntity
|
3
23
|
extend ActiveSupport::Concern
|
4
24
|
|
5
|
-
included
|
6
|
-
|
7
|
-
define_callbacks :followed
|
8
|
-
|
9
|
-
# Define a method that will be called after the entity receives a follow request
|
10
|
-
# @param method [Symbol] The name of the method to call, or a block that will be called directly
|
11
|
-
# @example
|
12
|
-
# after_followed :accept_follow
|
13
|
-
def self.after_followed(method)
|
14
|
-
set_callback :followed, :after, method
|
15
|
-
end
|
16
|
-
|
17
|
-
# Define a method that will be called after an activity has been received
|
18
|
-
# @param activity_type [String] The activity action to handle, e.g. 'Create'. If you specify '*', the handler will be called for any activity type.
|
19
|
-
# @param object_type [String] The object type to handle, e.g. 'Note'. If you specify '*', the handler will be called for any object type.
|
20
|
-
# @param method [Symbol] The name of the class method to call. The method will receive the complete activity payload as a parameter.
|
21
|
-
# @example
|
22
|
-
# after_activity_received 'Create', 'Note', :create_note
|
23
|
-
def self.after_activity_received(activity_type, object_type, method)
|
24
|
-
Fediverse::Inbox.register_handler(activity_type, object_type, self, method)
|
25
|
-
end
|
26
|
-
|
27
|
-
has_one :federails_actor, class_name: 'Federails::Actor', as: :entity, dependent: :destroy
|
28
|
-
|
29
|
-
after_create :create_federails_actor, if: lambda {
|
30
|
-
raise("Entity not configured for #{self.class.name}. Did you use \"acts_as_federails_actor\"?") unless Federails.actor_entity? self
|
31
|
-
|
32
|
-
Federails.actor_entity(self)[:auto_create_actors]
|
33
|
-
}
|
34
|
-
|
25
|
+
# Class methods automatically included in the concern.
|
26
|
+
module ClassMethods
|
35
27
|
# Configures the mapping between entity and actor
|
28
|
+
#
|
36
29
|
# @param username_field [Symbol] The method or attribute name that returns the preferred username for ActivityPub
|
37
30
|
# @param name_field [Symbol] The method or attribute name that returns the preferred name for ActivityPub
|
38
31
|
# @param profile_url_method [Symbol] The route method name that will generate the profile URL for ActivityPub
|
39
32
|
# @param actor_type [String] The ActivityStreams Actor type for this entity; defaults to 'Person'
|
40
33
|
# @param user_count_method [Symbol] A class method to call to count active users. Leave unspecified to leave this
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
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.
|
44
37
|
# @param auto_create_actors [Boolean] Whether to automatically create an actor when the entity is created
|
38
|
+
#
|
45
39
|
# @example
|
46
40
|
# acts_as_federails_actor username_field: :username, name_field: :display_name, profile_url_method: :url_for, actor_type: 'Person'
|
47
41
|
# rubocop:disable Metrics/ParameterLists
|
48
|
-
def
|
42
|
+
def acts_as_federails_actor(
|
49
43
|
name_field:,
|
50
44
|
username_field:,
|
51
45
|
profile_url_method: nil,
|
@@ -65,34 +59,72 @@ module Federails
|
|
65
59
|
end
|
66
60
|
# rubocop:enable Metrics/ParameterLists
|
67
61
|
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
71
|
-
#
|
62
|
+
# Define a method that will be called after the entity receives a follow request
|
63
|
+
#
|
64
|
+
# @param method_name [Symbol] The name of the method to call, or a block that will be called directly
|
65
|
+
#
|
72
66
|
# @example
|
73
|
-
#
|
74
|
-
|
75
|
-
|
76
|
-
# toot: "http://joinmastodon.org/ns#",
|
77
|
-
# attributionDomains: {
|
78
|
-
# "@id": "toot:attributionDomains",
|
79
|
-
# "@type": "@id"
|
80
|
-
# }
|
81
|
-
# },
|
82
|
-
# attributionDomains: [
|
83
|
-
# "example.com"
|
84
|
-
# ]
|
85
|
-
# }
|
86
|
-
# end
|
87
|
-
def to_activitypub_object
|
88
|
-
{}
|
67
|
+
# after_followed :accept_follow
|
68
|
+
def after_followed(method_name)
|
69
|
+
set_callback :followed, :after, method_name
|
89
70
|
end
|
90
71
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
72
|
+
# Define a method that will be called after an activity has been received
|
73
|
+
#
|
74
|
+
# @param activity_type [String] The activity action to handle, e.g. 'Create'. If you specify '*', the handler will be called for any activity type.
|
75
|
+
# @param object_type [String] The object type to handle, e.g. 'Note'. If you specify '*', the handler will be called for any object type.
|
76
|
+
# @param method_name [Symbol] The name of the class method to call. The method will receive the complete activity payload as a parameter.
|
77
|
+
#
|
78
|
+
# @example
|
79
|
+
# after_activity_received 'Create', 'Note', :create_note
|
80
|
+
def after_activity_received(activity_type, object_type, method_name)
|
81
|
+
Fediverse::Inbox.register_handler(activity_type, object_type, self, method_name)
|
95
82
|
end
|
96
83
|
end
|
84
|
+
|
85
|
+
included do
|
86
|
+
include ActiveSupport::Callbacks
|
87
|
+
|
88
|
+
define_callbacks :followed
|
89
|
+
|
90
|
+
has_one :federails_actor, class_name: 'Federails::Actor', as: :entity, dependent: :destroy
|
91
|
+
|
92
|
+
after_create :create_federails_actor, if: lambda {
|
93
|
+
raise("Entity not configured for #{self.class.name}. Did you use \"acts_as_federails_actor\"?") unless Federails.actor_entity? self
|
94
|
+
|
95
|
+
Federails.actor_entity(self)[:auto_create_actors]
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
# Add custom data to actor responses.
|
100
|
+
#
|
101
|
+
# Override in your own model to add extra data, which will be merged into the actor response
|
102
|
+
# generated by Federails. You can include extra `@context` for activitypub extensions and it will
|
103
|
+
# be merged with the main response context.
|
104
|
+
#
|
105
|
+
# @example
|
106
|
+
# def to_activitypub_object
|
107
|
+
# {
|
108
|
+
# "@context": {
|
109
|
+
# toot: "http://joinmastodon.org/ns#",
|
110
|
+
# attributionDomains: {
|
111
|
+
# "@id": "toot:attributionDomains",
|
112
|
+
# "@type": "@id"
|
113
|
+
# }
|
114
|
+
# },
|
115
|
+
# attributionDomains: [
|
116
|
+
# "example.com"
|
117
|
+
# ]
|
118
|
+
# }
|
119
|
+
# end
|
120
|
+
def to_activitypub_object
|
121
|
+
{}
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def create_federails_actor
|
127
|
+
Federails::Actor.create! entity: self
|
128
|
+
end
|
97
129
|
end
|
98
130
|
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
require 'fediverse/inbox'
|
2
|
+
|
3
|
+
module Federails
|
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 must have a `federated_url` attribute:
|
16
|
+
# ```rb
|
17
|
+
# add_column :posts, :federated_url, :string, null: true, default: nil
|
18
|
+
# ```
|
19
|
+
#
|
20
|
+
# ## Usage
|
21
|
+
#
|
22
|
+
# Include the concern in an existing model:
|
23
|
+
#
|
24
|
+
# ```rb
|
25
|
+
# class Post < ApplicationRecord
|
26
|
+
# include Federails::DataEntity
|
27
|
+
# acts_as_federails_data options
|
28
|
+
# end
|
29
|
+
# ```
|
30
|
+
module DataEntity
|
31
|
+
extend ActiveSupport::Concern
|
32
|
+
|
33
|
+
# Class methods automatically included in the concern.
|
34
|
+
module ClassMethods
|
35
|
+
# Configures the mapping between entity and Fediverse
|
36
|
+
#
|
37
|
+
# Model should have the following methods:
|
38
|
+
# - `to_activitypub_object`, returning a valid ActivityPub object
|
39
|
+
#
|
40
|
+
# @param actor_entity_method [Symbol] Method returning an object responding to 'federails_actor', for local content
|
41
|
+
# @param url_param [Symbol] Column name of the object ID that should be used in URLs. Defaults to +:id+
|
42
|
+
# @param route_path_segment [Symbol] Segment used in Federails routes to display the ActivityPub representation.
|
43
|
+
# Defaults to the pluralized, underscored class name
|
44
|
+
# @param handles [String] Type of ActivityPub object handled by this entity type
|
45
|
+
# @param with [Symbol] Self class method that will handle incoming objects. Defaults to +:handle_incoming_fediverse_data+
|
46
|
+
# @param filter_method [Symbol] Self class method that determines if an incoming object should be handled. Note
|
47
|
+
# that the first model for which this method returns true will be used. If left empty, the model CAN be selected,
|
48
|
+
# so define them if many models handle the same data type.
|
49
|
+
# @param should_federate_method [Symbol] method to determine if an object should be federated. If the method returns false,
|
50
|
+
# no create/update activities will happen, and object will not be accessible at federated_url. Defaults to a method
|
51
|
+
# that always returns true.
|
52
|
+
#
|
53
|
+
# @example
|
54
|
+
# acts_as_federails_data handles: 'Note', with: :note_handler, route_path_segment: :articles, actor_entity_method: :user
|
55
|
+
# rubocop:disable Metrics/ParameterLists
|
56
|
+
def acts_as_federails_data(
|
57
|
+
handles:,
|
58
|
+
with: :handle_incoming_fediverse_data,
|
59
|
+
route_path_segment: nil,
|
60
|
+
actor_entity_method: nil,
|
61
|
+
url_param: :id,
|
62
|
+
filter_method: nil,
|
63
|
+
should_federate_method: :default_should_federate?
|
64
|
+
)
|
65
|
+
route_path_segment ||= name.pluralize.underscore
|
66
|
+
|
67
|
+
Federails::Configuration.register_data_type self,
|
68
|
+
route_path_segment: route_path_segment,
|
69
|
+
actor_entity_method: actor_entity_method,
|
70
|
+
url_param: url_param,
|
71
|
+
handles: handles,
|
72
|
+
with: with,
|
73
|
+
filter_method: filter_method,
|
74
|
+
should_federate_method: should_federate_method
|
75
|
+
|
76
|
+
Fediverse::Inbox.register_handler 'Create', handles, self, with
|
77
|
+
Fediverse::Inbox.register_handler 'Update', handles, self, with
|
78
|
+
end
|
79
|
+
# rubocop:enable Metrics/ParameterLists
|
80
|
+
|
81
|
+
# Instantiates a new instance from an ActivityPub object
|
82
|
+
#
|
83
|
+
# @param activitypub_object [Hash]
|
84
|
+
#
|
85
|
+
# @return [self]
|
86
|
+
def new_from_activitypub_object(activitypub_object)
|
87
|
+
new from_activitypub_object(activitypub_object)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Creates or updates entity based on the ActivityPub activity
|
91
|
+
#
|
92
|
+
# @param activity_hash_or_id [Hash, String] Dereferenced activity hash or ID
|
93
|
+
#
|
94
|
+
# @return [self]
|
95
|
+
def handle_incoming_fediverse_data(activity_hash_or_id)
|
96
|
+
activity = Fediverse::Request.dereference(activity_hash_or_id)
|
97
|
+
object = Fediverse::Request.dereference(activity['object'])
|
98
|
+
|
99
|
+
entity = Federails::Utils::Object.find_or_create!(object)
|
100
|
+
|
101
|
+
if activity['type'] == 'Update'
|
102
|
+
entity.assign_attributes from_activitypub_object(object)
|
103
|
+
|
104
|
+
# Use timestamps from attributes
|
105
|
+
entity.save! touch: false
|
106
|
+
end
|
107
|
+
|
108
|
+
entity
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
included do
|
113
|
+
belongs_to :federails_actor, class_name: 'Federails::Actor'
|
114
|
+
|
115
|
+
scope :local_federails_entities, -> { where federated_url: nil }
|
116
|
+
scope :distant_federails_entities, -> { where.not(federated_url: nil) }
|
117
|
+
|
118
|
+
before_validation :set_federails_actor
|
119
|
+
after_create :create_federails_activity
|
120
|
+
after_update :update_federails_activity
|
121
|
+
end
|
122
|
+
|
123
|
+
# Computed value for the federated URL
|
124
|
+
#
|
125
|
+
# @return [String]
|
126
|
+
def federated_url
|
127
|
+
return nil unless send(federails_data_configuration[:should_federate_method])
|
128
|
+
return attributes['federated_url'] if attributes['federated_url'].present?
|
129
|
+
|
130
|
+
path_segment = Federails.data_entity_configuration(self)[:route_path_segment]
|
131
|
+
url_param = Federails.data_entity_configuration(self)[:url_param]
|
132
|
+
Federails::Engine.routes.url_helpers.server_published_url(publishable_type: path_segment, id: send(url_param))
|
133
|
+
end
|
134
|
+
|
135
|
+
# Check whether the entity was created locally or comes from the Fediverse
|
136
|
+
#
|
137
|
+
# @return [Boolean]
|
138
|
+
def local_federails_entity?
|
139
|
+
attributes['federated_url'].blank?
|
140
|
+
end
|
141
|
+
|
142
|
+
def federails_data_configuration
|
143
|
+
Federails.data_entity_configuration(self)
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
def set_federails_actor
|
149
|
+
return federails_actor if federails_actor.present?
|
150
|
+
|
151
|
+
self.federails_actor = send(federails_data_configuration[:actor_entity_method])&.federails_actor if federails_data_configuration[:actor_entity_method]
|
152
|
+
|
153
|
+
raise 'Cannot determine actor from configuration' unless federails_actor
|
154
|
+
end
|
155
|
+
|
156
|
+
def create_federails_activity
|
157
|
+
ensure_federails_configuration!
|
158
|
+
return unless local_federails_entity? && send(federails_data_configuration[:should_federate_method])
|
159
|
+
|
160
|
+
Activity.create! actor: federails_actor, action: 'Create', entity: self
|
161
|
+
end
|
162
|
+
|
163
|
+
def update_federails_activity
|
164
|
+
ensure_federails_configuration!
|
165
|
+
return unless local_federails_entity? && send(federails_data_configuration[:should_federate_method])
|
166
|
+
|
167
|
+
Activity.create! actor: federails_actor, action: 'Update', entity: self
|
168
|
+
end
|
169
|
+
|
170
|
+
def ensure_federails_configuration!
|
171
|
+
raise("Entity not configured for #{self.class.name}. Did you use \"acts_as_federails_data\"?") unless Federails.data_entity? self
|
172
|
+
end
|
173
|
+
|
174
|
+
def default_should_federate?
|
175
|
+
true
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -1,4 +1,28 @@
|
|
1
1
|
module Federails
|
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 Federails::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.
|
2
26
|
module HasUuid
|
3
27
|
extend ActiveSupport::Concern
|
4
28
|
|
@@ -11,12 +35,14 @@ module Federails
|
|
11
35
|
end
|
12
36
|
end
|
13
37
|
|
38
|
+
# @return [String] The UUID
|
14
39
|
def to_param
|
15
40
|
uuid
|
16
41
|
end
|
17
42
|
|
18
|
-
#
|
43
|
+
# @return [String]
|
19
44
|
def uuid
|
45
|
+
# Override UUID accessor to provide lazy initialization of UUIDs for old data
|
20
46
|
if self[:uuid].blank?
|
21
47
|
generate_uuid
|
22
48
|
save!
|
@@ -1,4 +1,13 @@
|
|
1
1
|
module Federails
|
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
|
2
11
|
class Activity < ApplicationRecord
|
3
12
|
include Federails::HasUuid
|
4
13
|
|
@@ -15,6 +24,9 @@ module Federails
|
|
15
24
|
|
16
25
|
after_create_commit :post_to_inboxes
|
17
26
|
|
27
|
+
# Determines the list of actors targeted by the activity
|
28
|
+
#
|
29
|
+
# @return [Array<Federails::Actor>]
|
18
30
|
def recipients
|
19
31
|
return [] unless actor.local?
|
20
32
|
|
@@ -2,6 +2,12 @@ require 'federails/utils/host'
|
|
2
2
|
require 'fediverse/webfinger'
|
3
3
|
|
4
4
|
module Federails
|
5
|
+
# Model storing _distant_ actors and links to local ones.
|
6
|
+
#
|
7
|
+
# To make a model act as an actor, use the `Federails::ActorEntity` concern
|
8
|
+
#
|
9
|
+
# See also:
|
10
|
+
# - https://www.w3.org/TR/activitypub/#actor-objects
|
5
11
|
class Actor < ApplicationRecord # rubocop:disable Metrics/ClassLength
|
6
12
|
include Federails::HasUuid
|
7
13
|
|
@@ -25,6 +31,7 @@ module Federails
|
|
25
31
|
has_many :follows, source: :target_actor, through: :following_follows
|
26
32
|
|
27
33
|
scope :local, -> { where.not(entity: nil) }
|
34
|
+
scope :distant, -> { where.not(federated_url: nil) }
|
28
35
|
|
29
36
|
def local?
|
30
37
|
entity.present?
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Federails
|
2
|
+
module Server
|
3
|
+
class PublishablePolicy < Federails::FederailsPolicy
|
4
|
+
def show?
|
5
|
+
@record.send(@record.federails_data_configuration[:should_federate_method])
|
6
|
+
end
|
7
|
+
|
8
|
+
class Scope < Scope
|
9
|
+
def resolve
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
context = true unless context == false
|
2
|
+
json.set! '@context', 'https://www.w3.org/ns/activitystreams' if context
|
3
|
+
|
4
|
+
publishable.to_activitypub_object.each_pair do |key, value|
|
5
|
+
json.set! key, value
|
6
|
+
end
|
7
|
+
|
8
|
+
json.id publishable.federated_url
|
9
|
+
json.actor publishable.federails_actor.federated_url
|
10
|
+
json.to ['https://www.w3.org/ns/activitystreams#Public']
|
11
|
+
json.cc [publishable.federails_actor.followers_url]
|
@@ -0,0 +1 @@
|
|
1
|
+
json.partial! 'federails/server/published/publishable', publishable: @publishable
|
data/config/routes.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
module Federails
|
2
2
|
# rubocop:disable Style/ClassVars
|
3
|
+
|
4
|
+
# Stores the Federails configuration in a _singleton_.
|
3
5
|
module Configuration
|
4
6
|
# Application name, used in well-known and nodeinfo endpoints
|
5
7
|
mattr_accessor :app_name
|
@@ -45,7 +47,16 @@ module Federails
|
|
45
47
|
mattr_accessor :base_client_controller
|
46
48
|
@@base_client_controller = 'ActionController::Base'
|
47
49
|
|
50
|
+
# @!method self.remote_follow_url_method
|
51
|
+
#
|
48
52
|
# Route method for remote-following requests
|
53
|
+
|
54
|
+
# @!method self.remote_follow_url_method=(value)
|
55
|
+
#
|
56
|
+
# Sets the route method for remote-following requests
|
57
|
+
# @param value [String] Route method name as used in links
|
58
|
+
# @example
|
59
|
+
# remote_follow_url_method 'main_app.my_custom_route_helper'
|
49
60
|
mattr_accessor :remote_follow_url_method
|
50
61
|
@@remote_follow_url_method = 'federails.new_client_following_url'
|
51
62
|
|
@@ -66,6 +77,14 @@ module Federails
|
|
66
77
|
def self.register_actor_class(klass, config = {})
|
67
78
|
@@actor_types[klass.name] = config.merge(class: klass)
|
68
79
|
end
|
80
|
+
|
81
|
+
# List of data types (classes using Federails::DataEntity)
|
82
|
+
mattr_reader :data_types
|
83
|
+
@@data_types = {}
|
84
|
+
|
85
|
+
def self.register_data_type(klass, config = {})
|
86
|
+
@@data_types[klass.name] = config.merge(class: klass)
|
87
|
+
end
|
69
88
|
end
|
70
89
|
# rubocop:enable Style/ClassVars
|
71
90
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Federails
|
2
|
+
module DataTransformer
|
3
|
+
module Note
|
4
|
+
# Renders a Note. The entity is used to determine actor and generic fields data
|
5
|
+
#
|
6
|
+
# @param entity [#federail_actor] A model instance
|
7
|
+
# @param content [String] Note content
|
8
|
+
# @param name [String, nil] Optional name/title
|
9
|
+
# @param custom [Hash] Optional additional keys (e.g.: attachment, icon, ...). Defaults will override these.
|
10
|
+
#
|
11
|
+
# @return [Hash]
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# Federails::DataTransformer::Note.to_federation(comment, content: comment.content, custom: { 'inReplyTo' => comment.parent.federated_url })
|
15
|
+
#
|
16
|
+
# See:
|
17
|
+
# - https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object
|
18
|
+
# - https://www.w3.org/TR/activitystreams-vocabulary/#dfn-note
|
19
|
+
def self.to_federation(entity, content:, name: nil, custom: {})
|
20
|
+
custom.merge '@context' => 'https://www.w3.org/ns/activitystreams',
|
21
|
+
'id' => entity.federated_url,
|
22
|
+
'type' => 'Note',
|
23
|
+
'name' => name,
|
24
|
+
'content' => content,
|
25
|
+
'attributedTo' => entity.federails_actor.federated_url,
|
26
|
+
'published' => entity.created_at,
|
27
|
+
'updated' => entity.updated_at
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Federails
|
2
|
+
module Utils
|
3
|
+
# Methods to manipulate incoming objects
|
4
|
+
class Object
|
5
|
+
class << self
|
6
|
+
# Finds data from an object or its ID.
|
7
|
+
#
|
8
|
+
# When data exists locally, the entity is returned.
|
9
|
+
# For distant data, a new instance is returned unless the target does not exist.
|
10
|
+
#
|
11
|
+
# @param object_or_id [String, Hash] String identifier or incoming object
|
12
|
+
#
|
13
|
+
# @return [ApplicationRecord, nil] Entity or nil when invalid/not found
|
14
|
+
def find_or_initialize(object_or_id)
|
15
|
+
federated_url = object_or_id.is_a?(Hash) ? object_or_id['id'] : object_or_id
|
16
|
+
|
17
|
+
route = local_route(federated_url)
|
18
|
+
return from_local_route(route) if route
|
19
|
+
|
20
|
+
from_distant_server(object_or_id)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Finds or initializes an entity from an ActivityPub object or id
|
24
|
+
#
|
25
|
+
# @see .find_or_initialize
|
26
|
+
#
|
27
|
+
# @param object_or_id [String, Hash] String identifier or incoming object
|
28
|
+
#
|
29
|
+
# @return [ApplicationRecord, nil] Entity or nil when invalid/not found
|
30
|
+
def find_or_initialize!(object_or_id)
|
31
|
+
entity = find_or_initialize object_or_id
|
32
|
+
raise ActiveRecord::RecordNotFound unless entity
|
33
|
+
|
34
|
+
entity
|
35
|
+
end
|
36
|
+
|
37
|
+
# Finds or create an entity from an ActivityPub object or id
|
38
|
+
#
|
39
|
+
# Note that the data transformer MUST return timestamps from the ActivityPub object if used on the model,
|
40
|
+
# as they won't be set automatically.
|
41
|
+
#
|
42
|
+
# @see .find_or_initialize!
|
43
|
+
#
|
44
|
+
# @param object_or_id [String, Hash] String identifier or incoming object
|
45
|
+
#
|
46
|
+
# @return [ApplicationRecord, nil] Entity or nil when invalid/not found
|
47
|
+
def find_or_create!(object_or_id)
|
48
|
+
entity = find_or_initialize! object_or_id
|
49
|
+
return entity if entity.persisted?
|
50
|
+
|
51
|
+
entity.save!(touch: false)
|
52
|
+
entity
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns the timestamps to use from an ActivityPub object
|
56
|
+
#
|
57
|
+
# @param hash [Hash] ActivityPub object
|
58
|
+
#
|
59
|
+
# @return [Hash] Hash with timestamps
|
60
|
+
def timestamp_attributes(hash)
|
61
|
+
{
|
62
|
+
created_at: hash['published'] ||= Time.current,
|
63
|
+
updated_at: hash['updated'].presence || hash['published'],
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def local_route(url)
|
70
|
+
route = Utils::Host.local_route(url)
|
71
|
+
|
72
|
+
return nil unless route && route[:controller] == 'federails/server/published' && route[:action] == 'show'
|
73
|
+
|
74
|
+
route
|
75
|
+
end
|
76
|
+
|
77
|
+
def from_local_route(route)
|
78
|
+
config = Federails.data_entity_handled_on route[:publishable_type]
|
79
|
+
return unless config
|
80
|
+
|
81
|
+
config[:class]&.find_by(config[:url_param] => route[:id])
|
82
|
+
rescue ActiveRecord::RecordNotFound
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
|
86
|
+
def from_distant_server(federated_url)
|
87
|
+
hash = Fediverse::Request.dereference(federated_url)
|
88
|
+
return unless hash
|
89
|
+
|
90
|
+
handler = Federails.data_entity_handler_for hash
|
91
|
+
return unless handler
|
92
|
+
|
93
|
+
entity = handler[:class].find_by federated_url: hash['id']
|
94
|
+
return entity if entity
|
95
|
+
|
96
|
+
entity = handler[:class].new_from_activitypub_object(hash)
|
97
|
+
return unless entity
|
98
|
+
|
99
|
+
entity.federails_actor = Federails::Actor.find_or_create_by_object hash['attributedTo'] if entity && !entity.federails_actor
|
100
|
+
|
101
|
+
entity
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
data/lib/federails/version.rb
CHANGED
data/lib/federails.rb
CHANGED
@@ -1,9 +1,14 @@
|
|
1
1
|
require 'federails/version'
|
2
2
|
require 'federails/engine'
|
3
3
|
require 'federails/configuration'
|
4
|
+
require 'federails/utils/object'
|
4
5
|
|
5
6
|
# rubocop:disable Style/ClassVars
|
7
|
+
|
8
|
+
# This module includes classes and methods related to Ruby on Rails: engine configuration, models, controllers, etc.
|
6
9
|
module Federails
|
10
|
+
DEFAULT_DATA_FILTER_METHOD = :handle_federated_object?
|
11
|
+
|
7
12
|
mattr_reader :configuration
|
8
13
|
@@configuration = Configuration
|
9
14
|
|
@@ -49,6 +54,55 @@ module Federails
|
|
49
54
|
Configuration.actor_types[klass]
|
50
55
|
end
|
51
56
|
|
57
|
+
# @return [Boolean] True if the given model is a possible data entity
|
58
|
+
def data_entity?(class_or_instance)
|
59
|
+
Configuration.data_types.key? class_or_instance_name(class_or_instance)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Finds configured data types from ActivityPub type
|
63
|
+
#
|
64
|
+
# @param type [String] ActivityPub object type, as configured with `:handles`
|
65
|
+
# @return [Array] List of data entity configurations
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# data_entity_handlers_for 'Note'
|
69
|
+
def data_entity_handlers_for(type)
|
70
|
+
Federails::Configuration.data_types.select { |_, v| v[:handles] == type }.map(&:last)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Finds the configured handler for a given ActivityPub object
|
74
|
+
#
|
75
|
+
# @param hash [Hash] ActivityPub object hash
|
76
|
+
#
|
77
|
+
# @return [Hash, nil] Data entity configuration
|
78
|
+
def data_entity_handler_for(hash)
|
79
|
+
data_entity_handlers_for(hash['type']).find do |handler|
|
80
|
+
return true if !handler[:filter_method] && !handler[:class].respond_to?(DEFAULT_DATA_FILTER_METHOD)
|
81
|
+
|
82
|
+
handler[:class].send(handler[:filter_method] || DEFAULT_DATA_FILTER_METHOD, hash)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Finds configured data type from route path segment
|
87
|
+
#
|
88
|
+
# @param route_path_segment [Symbol, String] Route path segment, as configured with `:route_path_segment`
|
89
|
+
# @return [Hash, nil] Entity configuration
|
90
|
+
#
|
91
|
+
# @example
|
92
|
+
# data_entity_handled_on :articles
|
93
|
+
def data_entity_handled_on(route_path_segment)
|
94
|
+
route_path_segment = route_path_segment.to_sym
|
95
|
+
Federails::Configuration.data_types.find { |_, v| v[:route_path_segment] == route_path_segment }&.last
|
96
|
+
end
|
97
|
+
|
98
|
+
# @return [Hash] The configuration for the given data entity
|
99
|
+
def data_entity_configuration(class_or_instance)
|
100
|
+
klass = class_or_instance_name(class_or_instance)
|
101
|
+
raise "#{klass} is not a configured data entity" unless Configuration.data_types.key?(klass)
|
102
|
+
|
103
|
+
Configuration.data_types[klass]
|
104
|
+
end
|
105
|
+
|
52
106
|
private
|
53
107
|
|
54
108
|
# @return [String] Class name of the provided class or instance
|
data/lib/fediverse/inbox.rb
CHANGED
@@ -4,21 +4,32 @@ module Fediverse
|
|
4
4
|
class Inbox
|
5
5
|
@@handlers = {} # rubocop:todo Style/ClassVars
|
6
6
|
class << self
|
7
|
+
# Registers a handler for incoming data
|
8
|
+
#
|
9
|
+
# @param activity_type [String] Target activity type ('Create', 'Follow', 'Like', ...)
|
10
|
+
# See https://www.w3.org/TR/activitystreams-vocabulary/#activity-types for a list of common ones
|
11
|
+
# @param object_type [String] Type of the related object ('Article', 'Note', ...)
|
12
|
+
# See https://www.w3.org/TR/activitystreams-vocabulary/#object-types for a list of common object types
|
13
|
+
# @param klass [String] Class handling the incoming object
|
14
|
+
# @param method [Symbol] Method in the class that will handle the object
|
7
15
|
def register_handler(activity_type, object_type, klass, method)
|
8
16
|
@@handlers[activity_type] ||= {}
|
9
17
|
@@handlers[activity_type][object_type] ||= {}
|
10
18
|
@@handlers[activity_type][object_type][klass] = method
|
11
19
|
end
|
12
20
|
|
21
|
+
# Executes the registered handler for an incoming object
|
22
|
+
#
|
23
|
+
# @param payload [Hash] Dereferenced activity
|
13
24
|
def dispatch_request(payload)
|
14
|
-
|
25
|
+
payload['object'] = Fediverse::Request.dereference(payload['object']) if payload.key? 'object'
|
26
|
+
|
27
|
+
handlers = get_handlers(payload['type'], payload.dig('object', 'type'))
|
15
28
|
handlers.each_pair do |klass, method|
|
16
29
|
klass.send method, payload
|
17
30
|
end
|
18
31
|
return true unless handlers.empty?
|
19
32
|
|
20
|
-
# FIXME: Fails silently
|
21
|
-
# raise NotImplementedError
|
22
33
|
Rails.logger.debug { "Unhandled activity type: #{payload['type']}" }
|
23
34
|
false
|
24
35
|
end
|
@@ -40,7 +51,7 @@ module Fediverse
|
|
40
51
|
end
|
41
52
|
|
42
53
|
def handle_accept_request(activity)
|
43
|
-
original_activity = Request.
|
54
|
+
original_activity = Request.dereference(activity['object'])
|
44
55
|
|
45
56
|
actor = Federails::Actor.find_or_create_by_object original_activity['actor']
|
46
57
|
target_actor = Federails::Actor.find_or_create_by_object original_activity['object']
|
data/lib/fediverse/notifier.rb
CHANGED
@@ -3,6 +3,9 @@ require 'fediverse/signature'
|
|
3
3
|
module Fediverse
|
4
4
|
class Notifier
|
5
5
|
class << self
|
6
|
+
# Posts an activity to its recipients
|
7
|
+
#
|
8
|
+
# @param activity [Federails::Activity]
|
6
9
|
def post_to_inboxes(activity)
|
7
10
|
actors = activity.recipients
|
8
11
|
Rails.logger.debug('Nobody to notice') && return if actors.count.zero?
|
data/lib/fediverse/request.rb
CHANGED
@@ -11,6 +11,7 @@ module Fediverse
|
|
11
11
|
@id = id
|
12
12
|
end
|
13
13
|
|
14
|
+
# FIXME: Replace by `Webfinger.get_json` (move other method here as class method)
|
14
15
|
def get
|
15
16
|
Rails.logger.debug { "GET #{@id}" }
|
16
17
|
@response = Faraday.get(@id, nil, BASE_HEADERS)
|
@@ -21,6 +22,18 @@ module Fediverse
|
|
21
22
|
def get(id)
|
22
23
|
new(id).get
|
23
24
|
end
|
25
|
+
|
26
|
+
# Dereferences a value
|
27
|
+
#
|
28
|
+
# @param value [String, Hash]
|
29
|
+
#
|
30
|
+
# @return [Hash, nil]
|
31
|
+
def dereference(value)
|
32
|
+
return value if value.is_a? Hash
|
33
|
+
return get(value) if value.is_a? String
|
34
|
+
|
35
|
+
raise "Unhandled object type #{value.class}"
|
36
|
+
end
|
24
37
|
end
|
25
38
|
|
26
39
|
private
|
data/lib/fediverse/webfinger.rb
CHANGED
@@ -4,31 +4,63 @@ require 'faraday/follow_redirects'
|
|
4
4
|
require 'federails/utils/host'
|
5
5
|
|
6
6
|
module Fediverse
|
7
|
+
# Methods related to Webfinger: find accounts, fetch actors,...
|
7
8
|
class Webfinger
|
8
9
|
class << self
|
9
10
|
ACCOUNT_REGEX = /(?<username>[a-z0-9\-_.]+)(?:@(?<domain>.*))?/
|
10
11
|
|
12
|
+
# Extracts username and domain from a "acct:username@domain" string
|
13
|
+
#
|
14
|
+
# @param account [String] Account string
|
15
|
+
#
|
16
|
+
# @return [MatchData, nil] Matches with +:username+ and +:domain+ or +nil+
|
11
17
|
def split_resource_account(account)
|
12
18
|
/\Aacct:#{ACCOUNT_REGEX}\z/io.match account
|
13
19
|
end
|
14
20
|
|
21
|
+
# Extracts username and domain from a "username@domain" string
|
22
|
+
#
|
23
|
+
# @param account [String] Account string
|
24
|
+
#
|
25
|
+
# @return [MatchData, nil] Matches with +:username+ and +:domain+ or +nil+
|
15
26
|
def split_account(account)
|
16
27
|
/\A#{ACCOUNT_REGEX}\z/io.match account
|
17
28
|
end
|
18
29
|
|
19
|
-
|
20
|
-
|
30
|
+
# Determines if a given account string should be a local account (same host as configured one)
|
31
|
+
#
|
32
|
+
# @param hash [Hash, MatchData] Object with +:username+ and +:domain+ keys
|
33
|
+
#
|
34
|
+
# @return [Boolean]
|
35
|
+
def local_user?(hash)
|
36
|
+
hash[:username] && (hash[:domain].nil? || (hash[:domain] == Federails::Utils::Host.localhost))
|
21
37
|
end
|
22
38
|
|
39
|
+
# Fetches a distant actor
|
40
|
+
#
|
41
|
+
# @param username [String]
|
42
|
+
# @param domain [String]
|
43
|
+
#
|
44
|
+
# @return [Federails::Actor, nil] Federails actor or nothing when not found
|
23
45
|
def fetch_actor(username, domain)
|
24
46
|
fetch_actor_url webfinger(username, domain)
|
25
47
|
end
|
26
48
|
|
49
|
+
# Fetches an actor given its URL
|
50
|
+
#
|
51
|
+
# @param url [String] Actor's federation URL
|
52
|
+
#
|
53
|
+
# @return [Federails::Actor, nil] Federails actor or nothing when not found
|
27
54
|
def fetch_actor_url(url)
|
28
55
|
webfinger_to_actor get_json url
|
29
56
|
end
|
30
57
|
|
31
|
-
#
|
58
|
+
# Gets the real actor's federation URL from its username and domain
|
59
|
+
#
|
60
|
+
# @param username [String]
|
61
|
+
# @param domain [String]
|
62
|
+
#
|
63
|
+
# @return [String, nil] Federation URL if found
|
32
64
|
def webfinger(username, domain)
|
33
65
|
json = webfinger_response(username, domain)
|
34
66
|
link = json['links'].find { |l| l['type'] == 'application/activity+json' }
|
@@ -37,6 +69,12 @@ module Fediverse
|
|
37
69
|
end
|
38
70
|
|
39
71
|
# Returns remote follow link template, or complete link if actor_url is provided
|
72
|
+
#
|
73
|
+
# @param username [String]
|
74
|
+
# @param domain [String]
|
75
|
+
# @param actor_url [String] Optional Federation URL to provide when known
|
76
|
+
#
|
77
|
+
# @return [String] The URL to use as follow URL
|
40
78
|
def remote_follow_url(username, domain, actor_url: nil)
|
41
79
|
json = webfinger_response(username, domain)
|
42
80
|
link = json['links'].find { |l| l['rel'] == 'http://ostatus.org/schema/1.0/subscribe' }
|
@@ -51,13 +89,17 @@ module Fediverse
|
|
51
89
|
|
52
90
|
private
|
53
91
|
|
92
|
+
# Makes a webfinger request for a given username/domain
|
93
|
+
# @return [Hash] Webfinger response's content
|
54
94
|
def webfinger_response(username, domain)
|
55
95
|
scheme = Federails.configuration.force_ssl ? 'https' : 'http'
|
56
96
|
get_json "#{scheme}://#{domain}/.well-known/webfinger", resource: "acct:#{username}@#{domain}"
|
57
97
|
end
|
58
98
|
|
59
|
-
|
60
|
-
|
99
|
+
# Extracts the server and port from a string, omitting common ports
|
100
|
+
# @return [String] Server and port
|
101
|
+
def server_and_port(string)
|
102
|
+
uri = URI.parse string
|
61
103
|
if uri.port && [80, 443].exclude?(uri.port)
|
62
104
|
"#{uri.host}:#{uri.port}"
|
63
105
|
else
|
@@ -65,6 +107,9 @@ module Fediverse
|
|
65
107
|
end
|
66
108
|
end
|
67
109
|
|
110
|
+
# Builds a +Federails::Actor+ from a Webfinger response
|
111
|
+
# @param data [Hash] Webfinger response
|
112
|
+
# @return [Federails::Actor]
|
68
113
|
def webfinger_to_actor(data)
|
69
114
|
Federails::Actor.new federated_url: data['id'],
|
70
115
|
username: data['preferredUsername'],
|
@@ -78,6 +123,9 @@ module Fediverse
|
|
78
123
|
public_key: data.dig('publicKey', 'publicKeyPem')
|
79
124
|
end
|
80
125
|
|
126
|
+
# Makes a simple GET request and returns a +Hash+ from the parsed body
|
127
|
+
# @return [Hash]
|
128
|
+
# @raise [ActiveRecord::RecordNotFound] when the response is invalid
|
81
129
|
def get_json(url, payload = {})
|
82
130
|
response = get(url, payload: payload, headers: { accept: 'application/json' })
|
83
131
|
|
@@ -93,10 +141,12 @@ module Fediverse
|
|
93
141
|
raise ActiveRecord::RecordNotFound
|
94
142
|
end
|
95
143
|
|
96
|
-
# Only perform a GET request and throws an ActiveRecord::RecordNotFound
|
97
|
-
#
|
98
|
-
# That's "ok-ish"; when an actor is unavailable, whatever the reason is, it's
|
99
|
-
#
|
144
|
+
# Only perform a GET request and throws an ActiveRecord::RecordNotFound on error.
|
145
|
+
#
|
146
|
+
# That's "ok-ish"; when an actor is unavailable, whatever the reason is, it's not found...
|
147
|
+
#
|
148
|
+
# @return [Faraday::Response]
|
149
|
+
# @raise [ActiveRecord::RecordNotFound] when the response is invalid
|
100
150
|
def get(url, payload: {}, headers: {})
|
101
151
|
connection = Faraday.new url: url, params: payload, headers: headers do |faraday|
|
102
152
|
faraday.response :follow_redirects # use Faraday::FollowRedirects::Middleware
|
data/lib/fediverse.rb
ADDED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: federails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Manuel Tancoigne
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-01-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -115,6 +115,7 @@ executables: []
|
|
115
115
|
extensions: []
|
116
116
|
extra_rdoc_files: []
|
117
117
|
files:
|
118
|
+
- LICENSE
|
118
119
|
- README.md
|
119
120
|
- Rakefile
|
120
121
|
- app/assets/config/federails_manifest.js
|
@@ -127,6 +128,7 @@ files:
|
|
127
128
|
- app/controllers/federails/server/actors_controller.rb
|
128
129
|
- app/controllers/federails/server/followings_controller.rb
|
129
130
|
- app/controllers/federails/server/nodeinfo_controller.rb
|
131
|
+
- app/controllers/federails/server/published_controller.rb
|
130
132
|
- app/controllers/federails/server/web_finger_controller.rb
|
131
133
|
- app/controllers/federails/server_controller.rb
|
132
134
|
- app/helpers/federails/server_helper.rb
|
@@ -134,6 +136,7 @@ files:
|
|
134
136
|
- app/jobs/federails/notify_inbox_job.rb
|
135
137
|
- app/mailers/federails/application_mailer.rb
|
136
138
|
- app/models/concerns/federails/actor_entity.rb
|
139
|
+
- app/models/concerns/federails/data_entity.rb
|
137
140
|
- app/models/concerns/federails/has_uuid.rb
|
138
141
|
- app/models/federails/activity.rb
|
139
142
|
- app/models/federails/actor.rb
|
@@ -146,6 +149,7 @@ files:
|
|
146
149
|
- app/policies/federails/server/activity_policy.rb
|
147
150
|
- app/policies/federails/server/actor_policy.rb
|
148
151
|
- app/policies/federails/server/following_policy.rb
|
152
|
+
- app/policies/federails/server/publishable_policy.rb
|
149
153
|
- app/views/federails/client/activities/_activity.html.erb
|
150
154
|
- app/views/federails/client/activities/_activity.json.jbuilder
|
151
155
|
- app/views/federails/client/activities/_index.json.jbuilder
|
@@ -180,6 +184,8 @@ files:
|
|
180
184
|
- app/views/federails/server/followings/show.activitypub.jbuilder
|
181
185
|
- app/views/federails/server/nodeinfo/index.nodeinfo.jbuilder
|
182
186
|
- app/views/federails/server/nodeinfo/show.nodeinfo.jbuilder
|
187
|
+
- app/views/federails/server/published/_publishable.activitypub.jbuilder
|
188
|
+
- app/views/federails/server/published/show.activitypub.jbuilder
|
183
189
|
- app/views/federails/server/web_finger/find.jrd.jbuilder
|
184
190
|
- app/views/federails/server/web_finger/host_meta.xrd.erb
|
185
191
|
- config/initializers/mime_types.rb
|
@@ -191,9 +197,12 @@ files:
|
|
191
197
|
- db/migrate/20241002094501_add_keypair_to_actors.rb
|
192
198
|
- lib/federails.rb
|
193
199
|
- lib/federails/configuration.rb
|
200
|
+
- lib/federails/data_transformer/note.rb
|
194
201
|
- lib/federails/engine.rb
|
195
202
|
- lib/federails/utils/host.rb
|
203
|
+
- lib/federails/utils/object.rb
|
196
204
|
- lib/federails/version.rb
|
205
|
+
- lib/fediverse.rb
|
197
206
|
- lib/fediverse/inbox.rb
|
198
207
|
- lib/fediverse/notifier.rb
|
199
208
|
- lib/fediverse/request.rb
|
@@ -230,7 +239,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
230
239
|
- !ruby/object:Gem::Version
|
231
240
|
version: '0'
|
232
241
|
requirements: []
|
233
|
-
rubygems_version: 3.
|
242
|
+
rubygems_version: 3.5.23
|
234
243
|
signing_key:
|
235
244
|
specification_version: 4
|
236
245
|
summary: An ActivityPub engine for Ruby on Rails
|