federails 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|