From dd7ef0dc41584089a97444d8192bc61505108e6c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 8 Aug 2017 21:52:15 +0200 Subject: Add ActivityPub inbox (#4216) * Add ActivityPub inbox * Handle ActivityPub deletes * Handle ActivityPub creates * Handle ActivityPub announces * Stubs for handling all activities that need to be handled * Add ActivityPub actor resolving * Handle conversation URI passing in ActivityPub * Handle content language in ActivityPub * Send accept header when fetching actor, handle JSON parse errors * Test for ActivityPub::FetchRemoteAccountService * Handle public key and icon/image when embedded/as array/as resolvable URI * Implement ActivityPub::FetchRemoteStatusService * Add stubs for more interactions * Undo activities implemented * Handle out of order activities * Hook up ActivityPub to ResolveRemoteAccountService, handle Update Account activities * Add fragment IDs to all transient activity serializers * Add tests and fixes * Add stubs for missing tests * Add more tests * Add more tests --- .../activitypub/process_account_service.rb | 84 ++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 app/services/activitypub/process_account_service.rb (limited to 'app/services/activitypub/process_account_service.rb') diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb new file mode 100644 index 000000000..92e2dbb30 --- /dev/null +++ b/app/services/activitypub/process_account_service.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +class ActivityPub::ProcessAccountService < BaseService + include JsonLdHelper + + # Should be called with confirmed valid JSON + # and WebFinger-resolved username and domain + def call(username, domain, json) + @json = json + @uri = @json['id'] + @username = username + @domain = domain + @account = Account.find_by(uri: @uri) + + create_account if @account.nil? + update_account + + @account + rescue Oj::ParseError + nil + end + + private + + def create_account + @account = Account.new + @account.username = @username + @account.domain = @domain + @account.uri = @uri + @account.suspended = true if auto_suspend? + @account.silenced = true if auto_silence? + @account.private_key = nil + @account.save! + end + + def update_account + @account.last_webfingered_at = Time.now.utc + @account.protocol = :activitypub + @account.inbox_url = @json['inbox'] || '' + @account.outbox_url = @json['outbox'] || '' + @account.shared_inbox_url = @json['sharedInbox'] || '' + @account.followers_url = @json['followers'] || '' + @account.url = @json['url'] || @uri + @account.display_name = @json['name'] || '' + @account.note = @json['summary'] || '' + @account.avatar_remote_url = image_url('icon') + @account.header_remote_url = image_url('image') + @account.public_key = public_key || '' + @account.save! + end + + def image_url(key) + value = first_of_value(@json[key]) + + return if value.nil? + return @json[key]['url'] if @json[key].is_a?(Hash) + + image = fetch_resource(value) + image['url'] if image + end + + def public_key + value = first_of_value(@json['publicKey']) + + return if value.nil? + return value['publicKeyPem'] if value.is_a?(Hash) + + key = fetch_resource(value) + key['publicKeyPem'] if key + end + + def auto_suspend? + domain_block && domain_block.suspend? + end + + def auto_silence? + domain_block && domain_block.silence? + end + + def domain_block + return @domain_block if defined?(@domain_block) + @domain_block = DomainBlock.find_by(domain: @domain) + end +end -- cgit From ccdd5a9576819cdc95946d98fea0e3c8bbd1d626 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 12 Aug 2017 17:41:03 +0200 Subject: Add serializing/unserializing of "locked" actor attribute (#4585) --- app/lib/activitypub/adapter.rb | 4 ++++ app/lib/activitypub/case_transform.rb | 24 ++++++++++++++++++++++ app/serializers/activitypub/actor_serializer.rb | 2 ++ .../activitypub/process_account_service.rb | 1 + 4 files changed, 31 insertions(+) create mode 100644 app/lib/activitypub/case_transform.rb (limited to 'app/services/activitypub/process_account_service.rb') diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb index e038136c0..df132f019 100644 --- a/app/lib/activitypub/adapter.rb +++ b/app/lib/activitypub/adapter.rb @@ -5,6 +5,10 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base :camel_lower end + def self.transform_key_casing!(value, _options) + ActivityPub::CaseTransform.camel_lower(value) + end + def serializable_hash(options = nil) options = serialization_options(options) serialized_hash = { '@context': ActivityPub::TagManager::CONTEXT }.merge(ActiveModelSerializers::Adapter::Attributes.new(serializer, instance_options).serializable_hash(options)) diff --git a/app/lib/activitypub/case_transform.rb b/app/lib/activitypub/case_transform.rb new file mode 100644 index 000000000..7f716f862 --- /dev/null +++ b/app/lib/activitypub/case_transform.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module ActivityPub::CaseTransform + class << self + def camel_lower_cache + @camel_lower_cache ||= {} + end + + def camel_lower(value) + case value + when Array then value.map { |item| camel_lower(item) } + when Hash then value.deep_transform_keys! { |key| camel_lower(key) } + when Symbol then camel_lower(value.to_s).to_sym + when String + camel_lower_cache[value] ||= if value.start_with?('_:') + '_:' + value.gsub(/\A_:/, '').underscore.camelize(:lower) + else + value.underscore.camelize(:lower) + end + else value + end + end + end +end diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index 8a119603d..b15736868 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -9,6 +9,8 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer has_one :public_key, serializer: ActivityPub::PublicKeySerializer + attribute :locked, key: '_:locked' + class ImageSerializer < ActiveModel::Serializer include RoutingHelper diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 92e2dbb30..9fb7ebf9e 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -46,6 +46,7 @@ class ActivityPub::ProcessAccountService < BaseService @account.avatar_remote_url = image_url('icon') @account.header_remote_url = image_url('image') @account.public_key = public_key || '' + @account.locked = @json['_:locked'] || false @account.save! end -- cgit From 6e9eda53319bc970b085c7c55277981320b2a835 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 21 Aug 2017 01:14:40 +0200 Subject: ActivityPub migration procedure (#4617) * ActivityPub migration procedure Once one account is detected as going from OStatus to ActivityPub, invalidate WebFinger cache for other accounts from the same domain * Unsubscribe from PuSH updates once we receive an ActivityPub payload * Re-subscribe to PuSH unless already unsubscribed, regardless of protocol --- app/controllers/activitypub/inboxes_controller.rb | 6 ++++++ app/controllers/admin/accounts_controller.rb | 2 +- app/services/activitypub/process_account_service.rb | 8 +++++++- app/services/unsubscribe_service.rb | 2 +- app/workers/activitypub/post_upgrade_worker.rb | 15 +++++++++++++++ app/workers/pubsubhubbub/unsubscribe_worker.rb | 15 +++++++++++++++ app/workers/scheduler/subscriptions_scheduler.rb | 2 +- lib/tasks/mastodon.rake | 5 +---- 8 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 app/workers/activitypub/post_upgrade_worker.rb create mode 100644 app/workers/pubsubhubbub/unsubscribe_worker.rb (limited to 'app/services/activitypub/process_account_service.rb') diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index 0f49b4399..078494c20 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -7,6 +7,7 @@ class ActivityPub::InboxesController < Api::BaseController def create if signed_request_account + upgrade_account process_payload head 201 else @@ -24,6 +25,11 @@ class ActivityPub::InboxesController < Api::BaseController @body ||= request.body.read end + def upgrade_account + return unless signed_request_account.subscribed? + Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) + end + def process_payload ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body.force_encoding('UTF-8')) end diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 7bceee2cd..54c659e1b 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -17,7 +17,7 @@ module Admin end def unsubscribe - UnsubscribeService.new.call(@account) + Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id) redirect_to admin_account_path(@account.id) end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 9fb7ebf9e..2f2dfd330 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -12,7 +12,8 @@ class ActivityPub::ProcessAccountService < BaseService @domain = domain @account = Account.find_by(uri: @uri) - create_account if @account.nil? + create_account if @account.nil? + upgrade_account if @account.ostatus? update_account @account @@ -24,6 +25,7 @@ class ActivityPub::ProcessAccountService < BaseService def create_account @account = Account.new + @account.protocol = :activitypub @account.username = @username @account.domain = @domain @account.uri = @uri @@ -50,6 +52,10 @@ class ActivityPub::ProcessAccountService < BaseService @account.save! end + def upgrade_account + ActivityPub::PostUpgradeWorker.perform_async(@account.domain) + end + def image_url(key) value = first_of_value(@json[key]) diff --git a/app/services/unsubscribe_service.rb b/app/services/unsubscribe_service.rb index c5e0e73fe..865f783bc 100644 --- a/app/services/unsubscribe_service.rb +++ b/app/services/unsubscribe_service.rb @@ -2,7 +2,7 @@ class UnsubscribeService < BaseService def call(account) - return unless account.ostatus? + return if account.hub_url.blank? @account = account @response = build_request.perform diff --git a/app/workers/activitypub/post_upgrade_worker.rb b/app/workers/activitypub/post_upgrade_worker.rb new file mode 100644 index 000000000..4154b8582 --- /dev/null +++ b/app/workers/activitypub/post_upgrade_worker.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class ActivityPub::PostUpgradeWorker + include Sidekiq::Worker + + sidekiq_options queue: 'pull' + + def perform(domain) + Account.where(domain: domain) + .where(protocol: :ostatus) + .where.not(last_webfingered_at: nil) + .in_batches + .update_all(last_webfingered_at: nil) + end +end diff --git a/app/workers/pubsubhubbub/unsubscribe_worker.rb b/app/workers/pubsubhubbub/unsubscribe_worker.rb new file mode 100644 index 000000000..a271715b7 --- /dev/null +++ b/app/workers/pubsubhubbub/unsubscribe_worker.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class Pubsubhubbub::UnsubscribeWorker + include Sidekiq::Worker + + sidekiq_options queue: 'push', retry: false, unique: :until_executed, dead: false + + def perform(account_id) + account = Account.find(account_id) + logger.debug "PuSH unsubscribing from #{account.acct}" + ::UnsubscribeService.new.call(account) + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/app/workers/scheduler/subscriptions_scheduler.rb b/app/workers/scheduler/subscriptions_scheduler.rb index 5ddfaed18..35ecda2db 100644 --- a/app/workers/scheduler/subscriptions_scheduler.rb +++ b/app/workers/scheduler/subscriptions_scheduler.rb @@ -14,6 +14,6 @@ class Scheduler::SubscriptionsScheduler private def expiring_accounts - Account.where(protocol: :ostatus).expiring(1.day.from_now).partitioned + Account.expiring(1.day.from_now).partitioned end end diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index 226523554..870fd7e4e 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -111,10 +111,7 @@ namespace :mastodon do namespace :push do desc 'Unsubscribes from PuSH updates of feeds nobody follows locally' task clear: :environment do - Account.remote.without_followers.where.not(subscription_expires_at: nil).find_each do |a| - Rails.logger.debug "PuSH unsubscribing from #{a.acct}" - UnsubscribeService.new.call(a) - end + Pubsubhubbub::UnsubscribeWorker.push_bulk(Account.remote.without_followers.where.not(subscription_expires_at: nil).pluck(:id)) end desc 'Re-subscribes to soon expiring PuSH subscriptions (deprecated)' -- cgit From d63de55ef84eea883b72a121d680b8841af8e2c0 Mon Sep 17 00:00:00 2001 From: unarist Date: Wed, 23 Aug 2017 01:30:15 +0900 Subject: Fix bugs which OStatus accounts may detected as ActivityPub ready (#4662) * Fallback to OStatus in FetchAtomService * Skip activity+json link if that activity is Person without inbox * If unsupported activity was detected and all other URLs failed, retry with ActivityPub-less Accept header * Allow mention to OStatus account in ActivityPub * Don't update profile with inbox-less Person object --- app/lib/activitypub/activity/create.rb | 2 +- .../activitypub/process_account_service.rb | 2 + app/services/fetch_atom_service.rb | 60 +++++++++++++--------- .../fetch_remote_account_service_spec.rb | 27 ++++++++++ 4 files changed, 67 insertions(+), 24 deletions(-) (limited to 'app/services/activitypub/process_account_service.rb') diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 154125759..5c59c4b24 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -68,7 +68,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def process_mention(tag, status) account = account_from_uri(tag['href']) - account = ActivityPub::FetchRemoteAccountService.new.call(tag['href']) if account.nil? + account = FetchRemoteAccountService.new.call(tag['href']) if account.nil? return if account.nil? account.mentions.create(status: status) end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 2f2dfd330..99f9dbdc2 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -6,6 +6,8 @@ class ActivityPub::ProcessAccountService < BaseService # Should be called with confirmed valid JSON # and WebFinger-resolved username and domain def call(username, domain, json) + return unless json['inbox'].present? + @json = json @uri = @json['id'] @username = username diff --git a/app/services/fetch_atom_service.rb b/app/services/fetch_atom_service.rb index c6a4dc2e9..3cf39e006 100644 --- a/app/services/fetch_atom_service.rb +++ b/app/services/fetch_atom_service.rb @@ -1,13 +1,17 @@ # frozen_string_literal: true class FetchAtomService < BaseService + include JsonLdHelper + def call(url) return if url.blank? - @url = url + result = process(url) - perform_request - process_response + # retry without ActivityPub + result ||= process(url) if @unsupported_activity + + result rescue OpenSSL::SSL::SSLError => e Rails.logger.debug "SSL error: #{e}" nil @@ -18,9 +22,18 @@ class FetchAtomService < BaseService private + def process(url, terminal = false) + @url = url + perform_request + process_response(terminal) + end + def perform_request + accept = 'text/html' + accept = 'application/activity+json, application/ld+json, application/atom+xml, ' + accept unless @unsupported_activity + @response = Request.new(:get, @url) - .add_headers('Accept' => 'application/activity+json, application/ld+json, application/atom+xml, text/html') + .add_headers('Accept' => accept) .perform end @@ -30,7 +43,12 @@ class FetchAtomService < BaseService if @response.mime_type == 'application/atom+xml' [@url, @response.to_s, :ostatus] elsif ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@response.mime_type) - [@url, @response.to_s, :activitypub] + if supported_activity?(@response.to_s) + [@url, @response.to_s, :activitypub] + else + @unsupported_activity = true + nil + end elsif @response['Link'] && !terminal process_headers elsif @response.mime_type == 'text/html' && !terminal @@ -44,15 +62,10 @@ class FetchAtomService < BaseService json_link = page.xpath('//link[@rel="alternate"]').find { |link| ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(link['type']) } atom_link = page.xpath('//link[@rel="alternate"]').find { |link| link['type'] == 'application/atom+xml' } - if !json_link.nil? - @url = json_link['href'] - perform_request - process_response(true) - elsif !atom_link.nil? - @url = atom_link['href'] - perform_request - process_response(true) - end + result ||= process(json_link.href, terminal: true) unless json_link.nil? || @unsupported_activity + result ||= process(atom_link.href, terminal: true) unless atom_link.nil? + + result end def process_headers @@ -61,14 +74,15 @@ class FetchAtomService < BaseService json_link = link_header.find_link(%w(rel alternate), %w(type application/activity+json)) || link_header.find_link(%w(rel alternate), ['type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"']) atom_link = link_header.find_link(%w(rel alternate), %w(type application/atom+xml)) - if !json_link.nil? - @url = json_link.href - perform_request - process_response(true) - elsif !atom_link.nil? - @url = atom_link.href - perform_request - process_response(true) - end + result ||= process(json_link.href, terminal: true) unless json_link.nil? || @unsupported_activity + result ||= process(atom_link.href, terminal: true) unless atom_link.nil? + + result + end + + def supported_activity?(body) + json = body_to_json(body) + return false if json.nil? || !supported_context?(json) + json['type'] == 'Person' ? json['inbox'].present? : true end end diff --git a/spec/services/activitypub/fetch_remote_account_service_spec.rb b/spec/services/activitypub/fetch_remote_account_service_spec.rb index 786d7f7f2..391d051c1 100644 --- a/spec/services/activitypub/fetch_remote_account_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_account_service_spec.rb @@ -11,6 +11,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService do preferredUsername: 'alice', name: 'Alice', summary: 'Foo bar', + inbox: 'http://example.com/alice/inbox', } end @@ -35,6 +36,32 @@ RSpec.describe ActivityPub::FetchRemoteAccountService do end end + context 'when the account does not have a inbox' do + let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } + + before do + actor[:inbox] = nil + + stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + end + + it 'fetches resource' do + account + expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once + end + + it 'looks up webfinger' do + account + expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once + end + + it 'returns nil' do + expect(account).to be_nil + end + + end + context 'when URI and WebFinger share the same host' do let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } -- cgit From 1b5806b74475213ae45b483d08a29daa40988f84 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 2 Sep 2017 14:01:23 +0200 Subject: Define missing JSON-LD properties (#4767) Using _: property names is discouraged, as in the future, canonicalization may throw an error when encountering that instead of discarding it silently like it does now. We are defining some ActivityStreams properties which we expect to land in ActivityStreams eventually, to ensure that future versions of Mastodon will remain compatible with this even once that happens. Those would be `locked`, `sensitive` and `Hashtag` We are defining a custom context inline for some properties which we do not expect to land in any other context. `atomUri`, `inReplyToAtomUri` and `conversation` are part of the custom defined OStatus context. --- app/lib/activitypub/activity/create.rb | 6 +++--- app/lib/activitypub/activity/delete.rb | 2 +- app/lib/activitypub/adapter.rb | 20 +++++++++++++++++++- app/serializers/activitypub/actor_serializer.rb | 4 +--- app/serializers/activitypub/delete_serializer.rb | 3 +-- app/serializers/activitypub/note_serializer.rb | 17 +++++++++++++---- .../activitypub/fetch_remote_status_service.rb | 4 ++-- app/services/activitypub/process_account_service.rb | 2 +- 8 files changed, 41 insertions(+), 17 deletions(-) (limited to 'app/services/activitypub/process_account_service.rb') diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index a6f1e60d9..081e80570 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -26,7 +26,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def find_existing_status status = status_from_uri(object_uri) - status ||= Status.find_by(uri: @object['_:atomUri']) if @object['_:atomUri'].present? + status ||= Status.find_by(uri: @object['atomUri']) if @object['atomUri'].present? status end @@ -43,7 +43,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity sensitive: @object['sensitive'] || false, visibility: visibility_from_audience, thread: replied_to_status, - conversation: conversation_from_uri(@object['_:conversation']), + conversation: conversation_from_uri(@object['conversation']), } end @@ -125,7 +125,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity @replied_to_status = nil else @replied_to_status = status_from_uri(in_reply_to_uri) - @replied_to_status ||= status_from_uri(@object['_:inReplyToAtomUri']) if @object['_:inReplyToAtomUri'].present? + @replied_to_status ||= status_from_uri(@object['inReplyToAtomUri']) if @object['inReplyToAtomUri'].present? @replied_to_status end end diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb index 9d804c86d..4c6afb090 100644 --- a/app/lib/activitypub/activity/delete.rb +++ b/app/lib/activitypub/activity/delete.rb @@ -17,7 +17,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity def delete_note status = Status.find_by(uri: object_uri, account: @account) - status ||= Status.find_by(uri: @object['_:atomUri'], account: @account) if @object.is_a?(Hash) && @object['_:atomUri'].present? + status ||= Status.find_by(uri: @object['atomUri'], account: @account) if @object.is_a?(Hash) && @object['atomUri'].present? delete_later!(object_uri) diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb index 92210579e..fe4dddd38 100644 --- a/app/lib/activitypub/adapter.rb +++ b/app/lib/activitypub/adapter.rb @@ -1,6 +1,24 @@ # frozen_string_literal: true class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base + CONTEXT = { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + + { + 'locked' => 'as:locked', + 'sensitive' => 'as:sensitive', + 'Hashtag' => 'as:Hashtag', + + 'ostatus' => 'http://ostatus.org#', + 'atomUri' => 'ostatus:atomUri', + 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', + 'conversation' => 'ostatus:conversation', + }, + ], + }.freeze + def self.default_key_transform :camel_lower end @@ -11,7 +29,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base def serializable_hash(options = nil) options = serialization_options(options) - serialized_hash = { '@context': [ActivityPub::TagManager::CONTEXT, 'https://w3id.org/security/v1'] }.merge(ActiveModelSerializers::Adapter::Attributes.new(serializer, instance_options).serializable_hash(options)) + serialized_hash = CONTEXT.merge(ActiveModelSerializers::Adapter::Attributes.new(serializer, instance_options).serializable_hash(options)) self.class.transform_key_casing!(serialized_hash, instance_options) end end diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index a72ecee24..f004dc326 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -6,12 +6,10 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer attributes :id, :type, :following, :followers, :inbox, :outbox, :shared_inbox, :preferred_username, :name, :summary, - :url + :url, :locked has_one :public_key, serializer: ActivityPub::PublicKeySerializer - attribute :locked, key: '_:locked' - class ImageSerializer < ActiveModel::Serializer include RoutingHelper diff --git a/app/serializers/activitypub/delete_serializer.rb b/app/serializers/activitypub/delete_serializer.rb index a041c577b..87a43b95d 100644 --- a/app/serializers/activitypub/delete_serializer.rb +++ b/app/serializers/activitypub/delete_serializer.rb @@ -2,8 +2,7 @@ class ActivityPub::DeleteSerializer < ActiveModel::Serializer class TombstoneSerializer < ActiveModel::Serializer - attributes :id, :type - attribute :atom_uri, key: '_:atomUri' + attributes :id, :type, :atom_uri def id ActivityPub::TagManager.instance.uri_for(object) diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 15031dfdc..d42f54263 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -3,14 +3,13 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer attributes :id, :type, :summary, :content, :in_reply_to, :published, :url, - :attributed_to, :to, :cc, :sensitive + :attributed_to, :to, :cc, :sensitive, + :atom_uri, :in_reply_to_atom_uri, + :conversation has_many :media_attachments, key: :attachment has_many :virtual_tags, key: :tag - attribute :atom_uri, key: '_:atomUri', if: :local? - attribute :in_reply_to_atom_uri, key: '_:inReplyToAtomUri' - def id ActivityPub::TagManager.instance.uri_for(object) end @@ -62,6 +61,8 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer end def atom_uri + return unless object.local? + ::TagManager.instance.uri_for(object) end @@ -71,6 +72,14 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer ::TagManager.instance.uri_for(object.thread) end + def conversation + if object.conversation.uri? + object.conversation.uri + else + TagManager.instance.unique_tag(object.conversation.created_at, object.conversation.id, 'Conversation') + end + end + def local? object.account.local? end diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb index c114515cd..68ca58d62 100644 --- a/app/services/activitypub/fetch_remote_status_service.rb +++ b/app/services/activitypub/fetch_remote_status_service.rb @@ -25,8 +25,8 @@ class ActivityPub::FetchRemoteStatusService < BaseService def activity_json if %w(Note Article).include? @json['type'] { - 'type' => 'Create', - 'actor' => first_of_value(@json['attributedTo']), + 'type' => 'Create', + 'actor' => first_of_value(@json['attributedTo']), 'object' => @json, } else diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 99f9dbdc2..44798d051 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -50,7 +50,7 @@ class ActivityPub::ProcessAccountService < BaseService @account.avatar_remote_url = image_url('icon') @account.header_remote_url = image_url('image') @account.public_key = public_key || '' - @account.locked = @json['_:locked'] || false + @account.locked = @json['locked'] || false @account.save! end -- cgit From 37fdddd927614d550aa47f142e3adc32cf70810b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 2 Sep 2017 23:13:35 +0200 Subject: Rename "locked" to "manuallyApprovesFollowers" in ActivityPub (#4779) See: --- app/lib/activitypub/adapter.rb | 15 +++++++-------- app/serializers/activitypub/actor_serializer.rb | 6 +++++- app/services/activitypub/process_account_service.rb | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) (limited to 'app/services/activitypub/process_account_service.rb') diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb index fe4dddd38..6ed66a239 100644 --- a/app/lib/activitypub/adapter.rb +++ b/app/lib/activitypub/adapter.rb @@ -7,14 +7,13 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base 'https://w3id.org/security/v1', { - 'locked' => 'as:locked', - 'sensitive' => 'as:sensitive', - 'Hashtag' => 'as:Hashtag', - - 'ostatus' => 'http://ostatus.org#', - 'atomUri' => 'ostatus:atomUri', - 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', - 'conversation' => 'ostatus:conversation', + 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', + 'sensitive' => 'as:sensitive', + 'Hashtag' => 'as:Hashtag', + 'ostatus' => 'http://ostatus.org#', + 'atomUri' => 'ostatus:atomUri', + 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', + 'conversation' => 'ostatus:conversation', }, ], }.freeze diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index f004dc326..25521eca9 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -6,7 +6,7 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer attributes :id, :type, :following, :followers, :inbox, :outbox, :shared_inbox, :preferred_username, :name, :summary, - :url, :locked + :url, :manually_approves_followers has_one :public_key, serializer: ActivityPub::PublicKeySerializer @@ -90,4 +90,8 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer def header_exists? object.header.exists? end + + def manually_approves_followers + object.locked + end end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 44798d051..a26b39cb5 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -50,7 +50,7 @@ class ActivityPub::ProcessAccountService < BaseService @account.avatar_remote_url = image_url('icon') @account.header_remote_url = image_url('image') @account.public_key = public_key || '' - @account.locked = @json['locked'] || false + @account.locked = @json['manuallyApprovesFollowers'] || false @account.save! end -- cgit From 9b50a9dd835c3a08effc86a6ef3e29e3a16e3d27 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 4 Sep 2017 18:26:33 +0200 Subject: Fix some ActivityPub JSON bugs (#4796) - Fix assumption that `url` is always a string. Handle it if it's an array of strings, array of objects, object, or string, both for accounts and for objects - `sharedInbox` is actually supposed to be under `endpoints`, handle both cases and adjust the serializer --- app/lib/activitypub/activity/create.rb | 12 +++++++++++- app/serializers/activitypub/actor_serializer.rb | 18 +++++++++++++++--- app/services/activitypub/process_account_service.rb | 18 ++++++++++++++---- 3 files changed, 40 insertions(+), 8 deletions(-) (limited to 'app/services/activitypub/process_account_service.rb') diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 081e80570..9a34484f5 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -33,7 +33,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def status_params { uri: @object['id'], - url: @object['url'] || @object['id'], + url: object_url || @object['id'], account: @account, text: text_from_content || '', language: language_from_content, @@ -147,6 +147,16 @@ class ActivityPub::Activity::Create < ActivityPub::Activity @object['contentMap'].keys.first end + def object_url + return if @object['url'].blank? + + value = first_of_value(@object['url']) + + return value if value.is_a?(String) + + value['href'] + end + def language_map? @object['contentMap'].is_a?(Hash) && !@object['contentMap'].empty? end diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index 25521eca9..a11178f5b 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -4,7 +4,7 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer include RoutingHelper attributes :id, :type, :following, :followers, - :inbox, :outbox, :shared_inbox, + :inbox, :outbox, :preferred_username, :name, :summary, :url, :manually_approves_followers @@ -24,6 +24,18 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer end end + class EndpointsSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :shared_inbox + + def shared_inbox + inbox_url + end + end + + has_one :endpoints, serializer: EndpointsSerializer + has_one :icon, serializer: ImageSerializer, if: :avatar_exists? has_one :image, serializer: ImageSerializer, if: :header_exists? @@ -51,8 +63,8 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer account_outbox_url(object) end - def shared_inbox - inbox_url + def endpoints + object end def preferred_username diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index a26b39cb5..29eb1c2e1 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -6,7 +6,7 @@ class ActivityPub::ProcessAccountService < BaseService # Should be called with confirmed valid JSON # and WebFinger-resolved username and domain def call(username, domain, json) - return unless json['inbox'].present? + return if json['inbox'].blank? @json = json @uri = @json['id'] @@ -42,9 +42,9 @@ class ActivityPub::ProcessAccountService < BaseService @account.protocol = :activitypub @account.inbox_url = @json['inbox'] || '' @account.outbox_url = @json['outbox'] || '' - @account.shared_inbox_url = @json['sharedInbox'] || '' + @account.shared_inbox_url = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || '' @account.followers_url = @json['followers'] || '' - @account.url = @json['url'] || @uri + @account.url = url || @uri @account.display_name = @json['name'] || '' @account.note = @json['summary'] || '' @account.avatar_remote_url = image_url('icon') @@ -62,7 +62,7 @@ class ActivityPub::ProcessAccountService < BaseService value = first_of_value(@json[key]) return if value.nil? - return @json[key]['url'] if @json[key].is_a?(Hash) + return value['url'] if value.is_a?(Hash) image = fetch_resource(value) image['url'] if image @@ -78,6 +78,16 @@ class ActivityPub::ProcessAccountService < BaseService key['publicKeyPem'] if key end + def url + return if @json['url'].blank? + + value = first_of_value(@json['url']) + + return value if value.is_a?(String) + + value['href'] + end + def auto_suspend? domain_block && domain_block.suspend? end -- cgit