activitypub-rails 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +22 -0
- data/app/controllers/activitypub/rails/application_controller.rb +7 -0
- data/app/helpers/activitypub/rails/application_helper.rb +6 -0
- data/app/jobs/activitypub/application_job.rb +4 -0
- data/app/jobs/activitypub/delivery_job.rb +38 -0
- data/app/jobs/activitypub/distribution_job.rb +42 -0
- data/app/jobs/activitypub/processing_job.rb +11 -0
- data/app/jobs/activitypub/undo_vote_distribution_job.rb +47 -0
- data/app/jobs/activitypub/update_distribution_job.rb +37 -0
- data/app/jobs/activitypub/vote_distribution_job.rb +38 -0
- data/app/models/activitypub/application_record.rb +5 -0
- data/app/models/activitypub/dict.rb +30 -0
- data/app/models/activitypub/rails/actor.rb +7 -0
- data/app/models/concerns/activitypub/rails/actor_concern.rb +69 -0
- data/app/models/concerns/activitypub/rails/object_concern.rb +23 -0
- data/app/serializers/activitypub/accept_follow_serializer.rb +20 -0
- data/app/serializers/activitypub/activity_serializer.rb +24 -0
- data/app/serializers/activitypub/actor_serializer.rb +37 -0
- data/app/serializers/activitypub/base_serializer.rb +39 -0
- data/app/serializers/activitypub/comment_serializer.rb +43 -0
- data/app/serializers/activitypub/follow_serializer.rb +17 -0
- data/app/serializers/activitypub/image_serializer.rb +11 -0
- data/app/serializers/activitypub/like_serializer.rb +25 -0
- data/app/serializers/activitypub/outbox_serializer.rb +49 -0
- data/app/serializers/activitypub/story_serializer.rb +56 -0
- data/app/serializers/activitypub/undo_like_serializer.rb +16 -0
- data/app/serializers/activitypub/update_serializer.rb +32 -0
- data/app/views/layouts/activitypub/application.html.erb +16 -0
- data/config/initializers/inflections.rb +3 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20181214172927_create_activitypub_rails_actors.rb +29 -0
- data/lib/activitypub/rails.rb +17 -0
- data/lib/activitypub/rails/engine.rb +7 -0
- data/lib/activitypub/rails/version.rb +5 -0
- data/lib/tasks/activitypub/rails_tasks.rake +4 -0
- metadata +109 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3f23f289b5c2543accc61329e0bd31a9c92effce1e5f93d06b926f28727ac7d5
|
4
|
+
data.tar.gz: 541c426383ddaa992346a67126a9ea9bc7fc804792a29789dab17c1d9a89ff17
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e4543b00629ccfb172a61ba1b929750f075eda8055663631256cfbfeef2d460b0c96e045914893c2e4999c04dcc2f9860b766749256eb2079dd9e5e3d83814e9
|
7
|
+
data.tar.gz: 2e19e853af10e0978f28f6ebde659ee42204a25c54f61764ce469f95dabee5b9799aca464658620fa4f775792a7ad9648c221be1a08493b70b2a099fbaeccc49
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2018 mbajur
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# ActivityPub
|
2
|
+
Short description and motivation.
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
How to use my plugin.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'activity_pub'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
```bash
|
16
|
+
$ bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
```bash
|
21
|
+
$ gem install activity_pub
|
22
|
+
```
|
23
|
+
|
24
|
+
## Contributing
|
25
|
+
Contribution directions go here.
|
26
|
+
|
27
|
+
## License
|
28
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'ActivityPub'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
load 'rails/tasks/statistics.rake'
|
21
|
+
|
22
|
+
require 'bundler/gem_tasks'
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class ActivityPub::DeliveryJob < ApplicationJob
|
2
|
+
queue_as :push
|
3
|
+
|
4
|
+
HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze
|
5
|
+
|
6
|
+
def perform(json, source_account_id, inbox_url, options = {})
|
7
|
+
@json = json
|
8
|
+
@source_account = ::Account.find(source_account_id)
|
9
|
+
@inbox_url = inbox_url
|
10
|
+
@options = options.with_indifferent_access
|
11
|
+
|
12
|
+
perform_request
|
13
|
+
rescue => e
|
14
|
+
raise e.class, "Delivery failed for #{inbox_url}: #{e.message}", e.backtrace[0]
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def build_request
|
20
|
+
request = Request.new(:post, @inbox_url, body: @json)
|
21
|
+
request.on_behalf_of(@source_account, :uri)
|
22
|
+
request.add_headers(HEADERS)
|
23
|
+
end
|
24
|
+
|
25
|
+
def perform_request
|
26
|
+
build_request.perform do |response|
|
27
|
+
raise Prismo::Exceptions::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def response_successful?(response)
|
32
|
+
(200...300).cover?(response.code)
|
33
|
+
end
|
34
|
+
|
35
|
+
def response_error_unsalvageable?(response)
|
36
|
+
(400...500).cover?(response.code) && response.code != 429
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class ActivityPub::DistributionJob < ApplicationJob
|
2
|
+
queue_as :push
|
3
|
+
|
4
|
+
def self.call(resource)
|
5
|
+
ActivityPub::DistributionJob.perform_later(resource.class.name, resource.id)
|
6
|
+
end
|
7
|
+
|
8
|
+
def perform(resource_type, resource_id)
|
9
|
+
@resource = resource_type.constantize.find(resource_id)
|
10
|
+
@account = @resource.account
|
11
|
+
|
12
|
+
inboxes.each do |inbox_url|
|
13
|
+
ActivityPub::DeliveryJob.perform_later(
|
14
|
+
signed_payload, @account.id, inbox_url
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
rescue ActiveRecord::RecordNotFound
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def inboxes
|
25
|
+
case @resource
|
26
|
+
when Comment
|
27
|
+
@resource.story.comments.remote.map { |c| c.account.inbox_url }.reject(&:blank?)
|
28
|
+
when Story
|
29
|
+
@resource.account.followers.inboxes
|
30
|
+
else
|
31
|
+
[]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def signed_payload
|
36
|
+
Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account))
|
37
|
+
end
|
38
|
+
|
39
|
+
def payload
|
40
|
+
ActivityPub::ActivitySerializer.new(@resource, with_context: true).as_json
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class ActivityPub::UndoVoteDistributionJob < ApplicationJob
|
2
|
+
queue_as :push
|
3
|
+
|
4
|
+
def self.call(vote)
|
5
|
+
ActivityPub::UndoVoteDistributionJob.perform_later(
|
6
|
+
vote.id,
|
7
|
+
vote.voteable_type,
|
8
|
+
vote.voteable_id,
|
9
|
+
vote.account_id
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
def perform(vote_id, voteable_type, voteable_id, account_id)
|
14
|
+
@vote_id = vote_id
|
15
|
+
@voteable = voteable_type.constantize.find(voteable_id)
|
16
|
+
@account = Account.find(account_id)
|
17
|
+
|
18
|
+
inboxes.each do |inbox_url|
|
19
|
+
ActivityPub::DeliveryJob.perform_later(
|
20
|
+
signed_payload, account.id, inbox_url
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
rescue ActiveRecord::RecordNotFound
|
25
|
+
true
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :voteable, :account
|
31
|
+
|
32
|
+
def inboxes
|
33
|
+
if voteable.is_a?(Comment)
|
34
|
+
[voteable.account.inbox_url]
|
35
|
+
else
|
36
|
+
[]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def signed_payload
|
41
|
+
@signed_payload ||= Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(account))
|
42
|
+
end
|
43
|
+
|
44
|
+
def payload
|
45
|
+
@payload ||= ActivityPub::UndoLikeSerializer.new(voteable, with_context: true).as_json
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class ActivityPub::UpdateDistributionJob < ApplicationJob
|
2
|
+
queue_as :push
|
3
|
+
|
4
|
+
def self.call_later(resource)
|
5
|
+
perform_later(resource.id, resource.class.name)
|
6
|
+
end
|
7
|
+
|
8
|
+
def perform(resource_id, resource_class_name)
|
9
|
+
@resource = resource_class_name.constantize.find(resource_id)
|
10
|
+
|
11
|
+
inboxes.each do |inbox_url|
|
12
|
+
ActivityPub::DeliveryJob.perform_later(signed_payload, account.id, inbox_url)
|
13
|
+
end
|
14
|
+
rescue ActiveRecord::RecordNotFound
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :resource
|
21
|
+
|
22
|
+
def account
|
23
|
+
@account ||= resource.is_a?(Account) ? resource : resource.account
|
24
|
+
end
|
25
|
+
|
26
|
+
def inboxes
|
27
|
+
@inboxes ||= account.followers.inboxes
|
28
|
+
end
|
29
|
+
|
30
|
+
def signed_payload
|
31
|
+
@signed_payload ||= Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(account))
|
32
|
+
end
|
33
|
+
|
34
|
+
def payload
|
35
|
+
ActivityPub::UpdateSerializer.new(resource, with_context: true).as_json
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class ActivityPub::VoteDistributionJob < ApplicationJob
|
2
|
+
queue_as :push
|
3
|
+
|
4
|
+
def perform(vote_id)
|
5
|
+
@vote = Vote.find(vote_id)
|
6
|
+
@voteable = vote.voteable
|
7
|
+
@account = vote.account
|
8
|
+
|
9
|
+
inboxes.each do |inbox_url|
|
10
|
+
ActivityPub::DeliveryJob.perform_later(
|
11
|
+
signed_payload, account.id, inbox_url
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
rescue ActiveRecord::RecordNotFound
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_reader :vote, :account, :voteable
|
22
|
+
|
23
|
+
def inboxes
|
24
|
+
if voteable.is_a?(Comment)
|
25
|
+
[voteable.account.inbox_url]
|
26
|
+
else
|
27
|
+
[]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def signed_payload
|
32
|
+
@signed_payload ||= Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(account))
|
33
|
+
end
|
34
|
+
|
35
|
+
def payload
|
36
|
+
@payload ||= ActivityPub::LikeSerializer.new(voteable, with_context: true).as_json
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module ActivityPub
|
2
|
+
class Dict
|
3
|
+
ACTOR_TYPES = {
|
4
|
+
application: 'application',
|
5
|
+
group: 'group',
|
6
|
+
organization: 'organization',
|
7
|
+
person: 'person',
|
8
|
+
service: 'service'
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
OBJECT_TYPES = {
|
12
|
+
relationship: 'relationship',
|
13
|
+
article: 'article',
|
14
|
+
document: 'document',
|
15
|
+
audio: 'audio',
|
16
|
+
image: 'image',
|
17
|
+
video: 'video',
|
18
|
+
note: 'note',
|
19
|
+
page: 'page',
|
20
|
+
event: 'event',
|
21
|
+
place: 'place',
|
22
|
+
profile: 'profile',
|
23
|
+
tombstone: 'tombstone',
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
LINK_TYPES = {
|
27
|
+
mention: 'mention',
|
28
|
+
}.freeze
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module ActivityPub
|
2
|
+
module Rails::ActorConcern
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
USERNAME_REGEXP = /[a-z0-9_]+([a-z0-9_\.]+[a-z0-9_]+)?/i
|
6
|
+
MENTION_REGEXP = /(?<=^|[^\/[:word:]])@((#{USERNAME_REGEXP})(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i
|
7
|
+
|
8
|
+
included do
|
9
|
+
include ActivityPub::Rails::ObjectConcern
|
10
|
+
|
11
|
+
scope :matches_slug, ->(value) { where(arel_table[:slug].matches("#{value}%")) }
|
12
|
+
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
|
13
|
+
|
14
|
+
before_create :generate_keys
|
15
|
+
|
16
|
+
validates :slug, presence: true
|
17
|
+
validates :slug, uniqueness: { scope: :domain, case_sensitive: true },
|
18
|
+
if: -> { !local? }
|
19
|
+
validates :slug, format: { with: /\A[a-z0-9_]+\z/i },
|
20
|
+
length: { maximum: 30 }, if: -> { local? },
|
21
|
+
uniqueness: { scope: :domain, case_sensitive: true }
|
22
|
+
end
|
23
|
+
|
24
|
+
def acct
|
25
|
+
local? ? slug : "#{slug}@#{domain}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def possibly_stale?
|
29
|
+
last_webfingered_at.nil? || last_webfingered_at <= 1.day.ago
|
30
|
+
end
|
31
|
+
|
32
|
+
def local_slug_and_domain
|
33
|
+
"#{slug}@#{ActivityPub.local_domain}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_webfinger_s
|
37
|
+
"acct:#{local_slug_and_domain}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def keypair
|
41
|
+
@keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key)
|
42
|
+
end
|
43
|
+
|
44
|
+
def magic_key
|
45
|
+
modulus, exponent = [keypair.public_key.n, keypair.public_key.e].map do |component|
|
46
|
+
result = []
|
47
|
+
|
48
|
+
until component.zero?
|
49
|
+
result << [component % 256].pack('C')
|
50
|
+
component >>= 8
|
51
|
+
end
|
52
|
+
|
53
|
+
result.reverse.join
|
54
|
+
end
|
55
|
+
|
56
|
+
(['RSA'] + [modulus, exponent].map { |n| Base64.urlsafe_encode64(n) }).join('.')
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def generate_keys
|
62
|
+
return unless local? && !Rails.env.test?
|
63
|
+
|
64
|
+
keypair = OpenSSL::PKey::RSA.new(2048)
|
65
|
+
self.private_key = keypair.to_pem
|
66
|
+
self.public_key = keypair.public_key.to_pem
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ActivityPub
|
2
|
+
module Rails::ObjectConcern
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
scope :remote, -> { where.not(domain: nil) }
|
7
|
+
scope :local, -> { where(domain: nil) }
|
8
|
+
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
9
|
+
|
10
|
+
def self.find_local(id)
|
11
|
+
local.find_by(id: id)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def local?
|
16
|
+
domain.nil?
|
17
|
+
end
|
18
|
+
|
19
|
+
def remote?
|
20
|
+
!local?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ActivityPub
|
2
|
+
class AcceptFollowSerializer < ActivityPub::BaseSerializer
|
3
|
+
def data
|
4
|
+
{
|
5
|
+
id: id,
|
6
|
+
type: 'Accept',
|
7
|
+
actor: ActivityPub::TagManager.instance.uri_for(object.target_account),
|
8
|
+
object: ActivityPub::FollowSerializer.new(object).data
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
def id
|
13
|
+
[
|
14
|
+
ActivityPub::TagManager.instance.uri_for(object.target_account),
|
15
|
+
'#accepts/follows/',
|
16
|
+
object.id
|
17
|
+
].join
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ActivityPub
|
2
|
+
class ActivitySerializer < ActivityPub::BaseSerializer
|
3
|
+
def data
|
4
|
+
{
|
5
|
+
id: ActivityPub::TagManager.instance.activity_uri_for(object),
|
6
|
+
object: parent_object,
|
7
|
+
type: 'Create',
|
8
|
+
actor: ActivityPub::TagManager.instance.uri_for(object.account),
|
9
|
+
published: object.created_at.iso8601,
|
10
|
+
to: ActivityPub::TagManager.instance.to(object),
|
11
|
+
cc: ActivityPub::TagManager.instance.cc(object)
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def parent_object
|
18
|
+
case object
|
19
|
+
when Story then ActivityPub::StorySerializer.new(object).data
|
20
|
+
when Comment then ActivityPub::CommentSerializer.new(object).data
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module ActivityPub
|
2
|
+
class ActorSerializer < ActivityPub::BaseSerializer
|
3
|
+
def data
|
4
|
+
result = {
|
5
|
+
id: account_url(object.username),
|
6
|
+
type: 'Person',
|
7
|
+
name: object.display_name,
|
8
|
+
preferredUsername: object.username,
|
9
|
+
summary: object.bio,
|
10
|
+
inbox: activitypub_account_inbox_url(object.username),
|
11
|
+
outbox: outbox_activitypub_account_url(object.username),
|
12
|
+
url: account_url(object.username),
|
13
|
+
publicKey: public_key,
|
14
|
+
}
|
15
|
+
|
16
|
+
result[:icon] = icon
|
17
|
+
|
18
|
+
result
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def public_key
|
24
|
+
{
|
25
|
+
id: ActivityPub::TagManager.instance.uri_for(object) + '#main-key',
|
26
|
+
owner: account_url(object.username),
|
27
|
+
publicKeyPem: object.public_key
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def icon
|
32
|
+
return nil if !object.avatar_data? || !object.avatar_attacher.stored?
|
33
|
+
|
34
|
+
ActivityPub::ImageSerializer.new(object.avatar).data
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module ActivityPub
|
2
|
+
class BaseSerializer < Granola::Serializer
|
3
|
+
include RoutingHelper
|
4
|
+
|
5
|
+
CONTEXT = [
|
6
|
+
'https://www.w3.org/ns/activitystreams',
|
7
|
+
'https://w3id.org/security/v1',
|
8
|
+
|
9
|
+
{
|
10
|
+
'Hashtag' => 'as:Hashtag'
|
11
|
+
}, {
|
12
|
+
'votes' => {
|
13
|
+
'@id' => 'as:votes',
|
14
|
+
'@type' => '@id'
|
15
|
+
}
|
16
|
+
}
|
17
|
+
].freeze
|
18
|
+
|
19
|
+
attr_reader :options
|
20
|
+
|
21
|
+
def initialize(object, options = {})
|
22
|
+
@options = options
|
23
|
+
super(object)
|
24
|
+
end
|
25
|
+
|
26
|
+
def as_json(opts = {})
|
27
|
+
options.merge!(opts)
|
28
|
+
|
29
|
+
json = data
|
30
|
+
json['@context'] = CONTEXT if options[:with_context]
|
31
|
+
|
32
|
+
json
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_json(opts = {})
|
36
|
+
Oj.dump(as_json(opts))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module ActivityPub
|
2
|
+
class CommentSerializer < ActivityPub::BaseSerializer
|
3
|
+
def data
|
4
|
+
{
|
5
|
+
id: ActivityPub::TagManager.instance.uri_for(object),
|
6
|
+
type: object.object_type.to_s.capitalize,
|
7
|
+
published: object.created_at.iso8601,
|
8
|
+
url: url,
|
9
|
+
votes: object.votes_count,
|
10
|
+
attributedTo: attributed_to,
|
11
|
+
to: [ActivityPub::TagManager::COLLECTIONS[:public]],
|
12
|
+
inReplyTo: in_reply_to,
|
13
|
+
content: object.body_html,
|
14
|
+
source: source,
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def url
|
19
|
+
{ type: 'Link', mediaType: 'text/html', href: comment_url(object) }
|
20
|
+
end
|
21
|
+
|
22
|
+
def attributed_to
|
23
|
+
[
|
24
|
+
{ type: 'Person', id: account_url(object.account.username) }
|
25
|
+
]
|
26
|
+
end
|
27
|
+
|
28
|
+
def in_reply_to
|
29
|
+
if object.parent_id.present?
|
30
|
+
ActivityPub::TagManager.instance.uri_for(object.parent)
|
31
|
+
else
|
32
|
+
ActivityPub::TagManager.instance.uri_for(object.story)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def source
|
37
|
+
{
|
38
|
+
content: object.body,
|
39
|
+
mediaType: 'text/markdown'
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ActivityPub
|
2
|
+
class FollowSerializer < ActivityPub::BaseSerializer
|
3
|
+
def data
|
4
|
+
{
|
5
|
+
id: id,
|
6
|
+
type: 'Follow',
|
7
|
+
actor: ActivityPub::TagManager.instance.uri_for(object.account),
|
8
|
+
object: ActivityPub::TagManager.instance.uri_for(object.target_account)
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
def id
|
13
|
+
ActivityPub::TagManager.instance.uri_for(object) ||
|
14
|
+
[ActivityPub::TagManager.instance.uri_for(object.account), '#follows/', object.id].join
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module ActivityPub
|
2
|
+
class ImageSerializer < ActivityPub::BaseSerializer
|
3
|
+
def data
|
4
|
+
{
|
5
|
+
type: 'Image',
|
6
|
+
url: image_url(object[:size_400].url, host: ActionController::Base.asset_host),
|
7
|
+
media_type: object[object.keys.first].mime_type
|
8
|
+
}
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ActivityPub
|
2
|
+
class LikeSerializer < ActivityPub::BaseSerializer
|
3
|
+
def data
|
4
|
+
{
|
5
|
+
id: id,
|
6
|
+
object: parent_object,
|
7
|
+
type: 'Like',
|
8
|
+
actor: ActivityPub::TagManager.instance.uri_for(object.account),
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
def id
|
13
|
+
[ActivityPub::TagManager.instance.uri_for(object.account), '#likes/', object.id].join
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def parent_object
|
19
|
+
case object
|
20
|
+
when Story then ActivityPub::StorySerializer.new(object).data
|
21
|
+
when Comment then ActivityPub::CommentSerializer.new(object).data
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module ActivityPub
|
2
|
+
class OutboxSerializer < ActivityPub::BaseSerializer
|
3
|
+
def data
|
4
|
+
result = {
|
5
|
+
id: object.id,
|
6
|
+
type: type
|
7
|
+
}
|
8
|
+
|
9
|
+
result[:totalItems] = total_items if object.size.present?
|
10
|
+
result[:next] = object.next if object.next.present?
|
11
|
+
result[:prev] = object.prev if object.prev.present?
|
12
|
+
result[:part_of] = object.part_of if object.part_of.present?
|
13
|
+
|
14
|
+
result[:first] = object.first if object.first.present?
|
15
|
+
result[:last] = object.last if object.last.present?
|
16
|
+
|
17
|
+
result[:items] = items if (!object.items.nil? || page?) && !ordered?
|
18
|
+
result[:ordered_items] = items if (!object.items.nil? || page?) && ordered?
|
19
|
+
|
20
|
+
result
|
21
|
+
end
|
22
|
+
|
23
|
+
def type
|
24
|
+
if page?
|
25
|
+
ordered? ? 'OrderedCollectionPage' : 'CollectionPage'
|
26
|
+
else
|
27
|
+
ordered? ? 'OrderedCollection' : 'Collection'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def total_items
|
32
|
+
object.size
|
33
|
+
end
|
34
|
+
|
35
|
+
def items
|
36
|
+
ActivityPub::StorySerializer.list(object.items).data
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def ordered?
|
42
|
+
object.type == :ordered
|
43
|
+
end
|
44
|
+
|
45
|
+
def page?
|
46
|
+
object.part_of.present?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module ActivityPub
|
2
|
+
class StorySerializer < ActivityPub::BaseSerializer
|
3
|
+
def data
|
4
|
+
{
|
5
|
+
id: ActivityPub::TagManager.instance.uri_for(object),
|
6
|
+
type: object.object_type.to_s.capitalize,
|
7
|
+
name: object.title,
|
8
|
+
published: object.created_at.iso8601,
|
9
|
+
content: object.decorate.description_html,
|
10
|
+
source: source,
|
11
|
+
url: url,
|
12
|
+
votes: object.votes_count,
|
13
|
+
attributedTo: attributed_to,
|
14
|
+
to: [ActivityPub::TagManager::COLLECTIONS[:public]],
|
15
|
+
tag: tags,
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def url
|
22
|
+
{
|
23
|
+
type: 'Link',
|
24
|
+
mediaType: 'text/html',
|
25
|
+
href: object.article? ? story_url(object) : object.url
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def attributed_to
|
30
|
+
[
|
31
|
+
{ type: 'Person', id: account_url(object.account.username) }
|
32
|
+
]
|
33
|
+
end
|
34
|
+
|
35
|
+
def tags
|
36
|
+
TagSerializer.list(object.tags).data
|
37
|
+
end
|
38
|
+
|
39
|
+
def source
|
40
|
+
{
|
41
|
+
content: object.description,
|
42
|
+
mediaType: 'text/markdown'
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
class TagSerializer < ActivityPub::BaseSerializer
|
47
|
+
def data
|
48
|
+
{
|
49
|
+
type: 'Hashtag',
|
50
|
+
href: tag_stories_url(object.name),
|
51
|
+
name: "##{object.name}"
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ActivityPub
|
2
|
+
class UndoLikeSerializer < ActivityPub::BaseSerializer
|
3
|
+
def data
|
4
|
+
{
|
5
|
+
id: id,
|
6
|
+
type: 'Undo',
|
7
|
+
object: ActivityPub::LikeSerializer.new(object).data,
|
8
|
+
actor: ActivityPub::TagManager.instance.uri_for(object.account)
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
def id
|
13
|
+
[ActivityPub::TagManager.instance.uri_for(object.account), '#likes/', object.id, '/undo'].join
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module ActivityPub
|
2
|
+
class UpdateSerializer < ActivityPub::BaseSerializer
|
3
|
+
def data
|
4
|
+
{
|
5
|
+
id: id,
|
6
|
+
type: 'Update',
|
7
|
+
object: object_serialized,
|
8
|
+
actor: actor
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
def id
|
13
|
+
[ActivityPub::TagManager.instance.uri_for(object), '#updates/', object.updated_at.to_i].join
|
14
|
+
end
|
15
|
+
|
16
|
+
def actor
|
17
|
+
resource = object.is_a?(Account) ? object : object.account
|
18
|
+
ActivityPub::TagManager.instance.uri_for(resource)
|
19
|
+
end
|
20
|
+
|
21
|
+
def object_serialized
|
22
|
+
case object
|
23
|
+
when Account
|
24
|
+
ActivityPub::ActorSerializer.new(object).data
|
25
|
+
when Story
|
26
|
+
ActivityPub::StorySerializer.new(object).data
|
27
|
+
when Comment
|
28
|
+
ActivityPub::CommentSerializer.new(object).data
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Activity pub</title>
|
5
|
+
<%= csrf_meta_tags %>
|
6
|
+
<%= csp_meta_tag %>
|
7
|
+
|
8
|
+
<%= stylesheet_link_tag "activity_pub/application", media: "all" %>
|
9
|
+
<%= javascript_include_tag "activity_pub/application" %>
|
10
|
+
</head>
|
11
|
+
<body>
|
12
|
+
|
13
|
+
<%= yield %>
|
14
|
+
|
15
|
+
</body>
|
16
|
+
</html>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
class CreateActivityPubRailsActors < ActiveRecord::Migration[5.2]
|
2
|
+
def change
|
3
|
+
create_table :activitypub_rails_actors, id: false do |t|
|
4
|
+
t.bigint :id
|
5
|
+
t.string :slug, null: false, default: ''
|
6
|
+
t.string :domain
|
7
|
+
t.text :private_key
|
8
|
+
t.text :public_key, null: false, default: ''
|
9
|
+
t.string :remote_url, null: false, default: ''
|
10
|
+
t.string :name, null: false, default: ''
|
11
|
+
t.text :summary
|
12
|
+
t.string :type
|
13
|
+
t.string :uri, null: false, default: ''
|
14
|
+
t.string :url
|
15
|
+
t.datetime :subscription_expires_at
|
16
|
+
t.datetime :last_webfingered_at
|
17
|
+
t.string :inbox_url, null: false, default: ''
|
18
|
+
t.string :outbox_url, null: false, default: ''
|
19
|
+
t.string :shared_inbox_url, null: false, default: ''
|
20
|
+
t.string :actor_type
|
21
|
+
t.string :followers_url
|
22
|
+
t.integer :following_count, default: 0
|
23
|
+
t.integer :followers_count, default: 0
|
24
|
+
|
25
|
+
t.timestamps
|
26
|
+
end
|
27
|
+
add_index :activitypub_rails_actors, %i[type slug domain], unique: true
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "activitypub/rails/engine"
|
2
|
+
|
3
|
+
module ActivityPub
|
4
|
+
module Rails
|
5
|
+
class << self
|
6
|
+
mattr_accessor :local_domain
|
7
|
+
|
8
|
+
# add default values of more config vars here
|
9
|
+
# self.site_name = "Site Name"
|
10
|
+
end
|
11
|
+
|
12
|
+
# this function maps the vars from your app into your engine
|
13
|
+
def self.setup(&block)
|
14
|
+
yield self
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activitypub-rails
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- mbajur
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-12-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 5.2.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sqlite3
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: ActivityPub engine for Ruby on Rails
|
42
|
+
email:
|
43
|
+
- mbajur@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- MIT-LICENSE
|
49
|
+
- README.md
|
50
|
+
- Rakefile
|
51
|
+
- app/controllers/activitypub/rails/application_controller.rb
|
52
|
+
- app/helpers/activitypub/rails/application_helper.rb
|
53
|
+
- app/jobs/activitypub/application_job.rb
|
54
|
+
- app/jobs/activitypub/delivery_job.rb
|
55
|
+
- app/jobs/activitypub/distribution_job.rb
|
56
|
+
- app/jobs/activitypub/processing_job.rb
|
57
|
+
- app/jobs/activitypub/undo_vote_distribution_job.rb
|
58
|
+
- app/jobs/activitypub/update_distribution_job.rb
|
59
|
+
- app/jobs/activitypub/vote_distribution_job.rb
|
60
|
+
- app/models/activitypub/application_record.rb
|
61
|
+
- app/models/activitypub/dict.rb
|
62
|
+
- app/models/activitypub/rails/actor.rb
|
63
|
+
- app/models/concerns/activitypub/rails/actor_concern.rb
|
64
|
+
- app/models/concerns/activitypub/rails/object_concern.rb
|
65
|
+
- app/serializers/activitypub/accept_follow_serializer.rb
|
66
|
+
- app/serializers/activitypub/activity_serializer.rb
|
67
|
+
- app/serializers/activitypub/actor_serializer.rb
|
68
|
+
- app/serializers/activitypub/base_serializer.rb
|
69
|
+
- app/serializers/activitypub/comment_serializer.rb
|
70
|
+
- app/serializers/activitypub/follow_serializer.rb
|
71
|
+
- app/serializers/activitypub/image_serializer.rb
|
72
|
+
- app/serializers/activitypub/like_serializer.rb
|
73
|
+
- app/serializers/activitypub/outbox_serializer.rb
|
74
|
+
- app/serializers/activitypub/story_serializer.rb
|
75
|
+
- app/serializers/activitypub/undo_like_serializer.rb
|
76
|
+
- app/serializers/activitypub/update_serializer.rb
|
77
|
+
- app/views/layouts/activitypub/application.html.erb
|
78
|
+
- config/initializers/inflections.rb
|
79
|
+
- config/routes.rb
|
80
|
+
- db/migrate/20181214172927_create_activitypub_rails_actors.rb
|
81
|
+
- lib/activitypub/rails.rb
|
82
|
+
- lib/activitypub/rails/engine.rb
|
83
|
+
- lib/activitypub/rails/version.rb
|
84
|
+
- lib/tasks/activitypub/rails_tasks.rake
|
85
|
+
homepage: https://gitlab.com/prismosuite/activitypub-rails
|
86
|
+
licenses:
|
87
|
+
- MIT
|
88
|
+
metadata: {}
|
89
|
+
post_install_message:
|
90
|
+
rdoc_options: []
|
91
|
+
require_paths:
|
92
|
+
- lib
|
93
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
requirements: []
|
104
|
+
rubyforge_project:
|
105
|
+
rubygems_version: 2.7.7
|
106
|
+
signing_key:
|
107
|
+
specification_version: 4
|
108
|
+
summary: ActivityPub engine for Ruby on Rails
|
109
|
+
test_files: []
|