about summary refs log tree commit diff
path: root/app/lib
diff options
context:
space:
mode:
authorDavid Yip <yipdw@member.fsf.org>2017-09-09 14:27:47 -0500
committerDavid Yip <yipdw@member.fsf.org>2017-09-09 14:27:47 -0500
commitb9f7bc149b2a6abfbdaee83e6992b617b8bdb18e (patch)
tree355225f4424a6ea1b40c66c5540ccab42096e3bf /app/lib
parente18ed4bbc7ab4e258d05a3e2a5db0790f67a8f37 (diff)
parent5d170587e3b6c1a3b3ebe0910b62a4c526e2900d (diff)
Merge branch 'origin/master' into sync/upstream
 Conflicts:
	app/javascript/mastodon/components/status_list.js
	app/javascript/mastodon/features/notifications/index.js
	app/javascript/mastodon/features/ui/components/modal_root.js
	app/javascript/mastodon/features/ui/components/onboarding_modal.js
	app/javascript/mastodon/features/ui/index.js
	app/javascript/styles/about.scss
	app/javascript/styles/accounts.scss
	app/javascript/styles/components.scss
	app/presenters/instance_presenter.rb
	app/services/post_status_service.rb
	app/services/reblog_service.rb
	app/views/about/more.html.haml
	app/views/about/show.html.haml
	app/views/accounts/_header.html.haml
	config/webpack/loaders/babel.js
	spec/controllers/api/v1/accounts/credentials_controller_spec.rb
Diffstat (limited to 'app/lib')
-rw-r--r--app/lib/activitypub/activity.rb106
-rw-r--r--app/lib/activitypub/activity/accept.rb25
-rw-r--r--app/lib/activitypub/activity/announce.rb28
-rw-r--r--app/lib/activitypub/activity/block.rb12
-rw-r--r--app/lib/activitypub/activity/create.rb175
-rw-r--r--app/lib/activitypub/activity/delete.rb45
-rw-r--r--app/lib/activitypub/activity/follow.rb24
-rw-r--r--app/lib/activitypub/activity/like.rb12
-rw-r--r--app/lib/activitypub/activity/reject.rb25
-rw-r--r--app/lib/activitypub/activity/undo.rb71
-rw-r--r--app/lib/activitypub/activity/update.rb17
-rw-r--r--app/lib/activitypub/adapter.rb23
-rw-r--r--app/lib/activitypub/case_transform.rb24
-rw-r--r--app/lib/activitypub/linked_data_signature.rb56
-rw-r--r--app/lib/activitypub/tag_manager.rb38
-rw-r--r--app/lib/ostatus/activity/base.rb24
-rw-r--r--app/lib/ostatus/activity/creation.rb49
-rw-r--r--app/lib/ostatus/activity/deletion.rb4
-rw-r--r--app/lib/ostatus/activity/remote.rb6
-rw-r--r--app/lib/ostatus/atom_serializer.rb5
-rw-r--r--app/lib/request.rb24
-rw-r--r--app/lib/status_finder.rb (renamed from app/lib/stream_entry_finder.rb)10
22 files changed, 774 insertions, 29 deletions
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
new file mode 100644
index 000000000..b06dd6194
--- /dev/null
+++ b/app/lib/activitypub/activity.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+class ActivityPub::Activity
+  include JsonLdHelper
+
+  def initialize(json, account)
+    @json    = json
+    @account = account
+    @object  = @json['object']
+  end
+
+  def perform
+    raise NotImplementedError
+  end
+
+  class << self
+    def factory(json, account)
+      @json = json
+      klass&.new(json, account)
+    end
+
+    private
+
+    def klass
+      case @json['type']
+      when 'Create'
+        ActivityPub::Activity::Create
+      when 'Announce'
+        ActivityPub::Activity::Announce
+      when 'Delete'
+        ActivityPub::Activity::Delete
+      when 'Follow'
+        ActivityPub::Activity::Follow
+      when 'Like'
+        ActivityPub::Activity::Like
+      when 'Block'
+        ActivityPub::Activity::Block
+      when 'Update'
+        ActivityPub::Activity::Update
+      when 'Undo'
+        ActivityPub::Activity::Undo
+      when 'Accept'
+        ActivityPub::Activity::Accept
+      when 'Reject'
+        ActivityPub::Activity::Reject
+      end
+    end
+  end
+
+  protected
+
+  def status_from_uri(uri)
+    ActivityPub::TagManager.instance.uri_to_resource(uri, Status)
+  end
+
+  def account_from_uri(uri)
+    ActivityPub::TagManager.instance.uri_to_resource(uri, Account)
+  end
+
+  def object_uri
+    @object_uri ||= value_or_id(@object)
+  end
+
+  def redis
+    Redis.current
+  end
+
+  def distribute(status)
+    notify_about_reblog(status) if reblog_of_local_account?(status)
+    notify_about_mentions(status)
+    crawl_links(status)
+    distribute_to_followers(status)
+  end
+
+  def reblog_of_local_account?(status)
+    status.reblog? && status.reblog.account.local?
+  end
+
+  def notify_about_reblog(status)
+    NotifyService.new.call(status.reblog.account, status)
+  end
+
+  def notify_about_mentions(status)
+    status.mentions.includes(:account).each do |mention|
+      next unless mention.account.local? && audience_includes?(mention.account)
+      NotifyService.new.call(mention.account, mention)
+    end
+  end
+
+  def crawl_links(status)
+    return if status.spoiler_text?
+    LinkCrawlWorker.perform_async(status.id)
+  end
+
+  def distribute_to_followers(status)
+    ::DistributionWorker.perform_async(status.id)
+  end
+
+  def delete_arrived_first?(uri)
+    redis.exists("delete_upon_arrival:#{@account.id}:#{uri}")
+  end
+
+  def delete_later!(uri)
+    redis.setex("delete_upon_arrival:#{@account.id}:#{uri}", 6.hours.seconds, uri)
+  end
+end
diff --git a/app/lib/activitypub/activity/accept.rb b/app/lib/activitypub/activity/accept.rb
new file mode 100644
index 000000000..bd90c9019
--- /dev/null
+++ b/app/lib/activitypub/activity/accept.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class ActivityPub::Activity::Accept < ActivityPub::Activity
+  def perform
+    case @object['type']
+    when 'Follow'
+      accept_follow
+    end
+  end
+
+  private
+
+  def accept_follow
+    target_account = account_from_uri(target_uri)
+
+    return if target_account.nil? || !target_account.local?
+
+    follow_request = FollowRequest.find_by(account: target_account, target_account: @account)
+    follow_request&.authorize!
+  end
+
+  def target_uri
+    @target_uri ||= value_or_id(@object['actor'])
+  end
+end
diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb
new file mode 100644
index 000000000..c4da405c7
--- /dev/null
+++ b/app/lib/activitypub/activity/announce.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class ActivityPub::Activity::Announce < ActivityPub::Activity
+  def perform
+    original_status   = status_from_uri(object_uri)
+    original_status ||= fetch_remote_original_status
+
+    return if original_status.nil? || delete_arrived_first?(@json['id'])
+
+    status = Status.find_by(account: @account, reblog: original_status)
+
+    return status unless status.nil?
+
+    status = Status.create!(account: @account, reblog: original_status, uri: @json['id'])
+    distribute(status)
+    status
+  end
+
+  private
+
+  def fetch_remote_original_status
+    if object_uri.start_with?('http')
+      ActivityPub::FetchRemoteStatusService.new.call(object_uri)
+    elsif @object['url'].present?
+      ::FetchRemoteStatusService.new.call(@object['url'])
+    end
+  end
+end
diff --git a/app/lib/activitypub/activity/block.rb b/app/lib/activitypub/activity/block.rb
new file mode 100644
index 000000000..f630d5db2
--- /dev/null
+++ b/app/lib/activitypub/activity/block.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class ActivityPub::Activity::Block < ActivityPub::Activity
+  def perform
+    target_account = account_from_uri(object_uri)
+
+    return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id']) || @account.blocking?(target_account)
+
+    UnfollowService.new.call(target_account, @account) if target_account.following?(@account)
+    @account.block!(target_account)
+  end
+end
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
new file mode 100644
index 000000000..081e80570
--- /dev/null
+++ b/app/lib/activitypub/activity/create.rb
@@ -0,0 +1,175 @@
+# frozen_string_literal: true
+
+class ActivityPub::Activity::Create < ActivityPub::Activity
+  def perform
+    return if delete_arrived_first?(object_uri) || unsupported_object_type?
+
+    status = find_existing_status
+
+    return status unless status.nil?
+
+    ApplicationRecord.transaction do
+      status = Status.create!(status_params)
+
+      process_tags(status)
+      process_attachments(status)
+    end
+
+    resolve_thread(status)
+    distribute(status)
+    forward_for_reply if status.public_visibility? || status.unlisted_visibility?
+
+    status
+  end
+
+  private
+
+  def find_existing_status
+    status   = status_from_uri(object_uri)
+    status ||= Status.find_by(uri: @object['atomUri']) if @object['atomUri'].present?
+    status
+  end
+
+  def status_params
+    {
+      uri: @object['id'],
+      url: @object['url'] || @object['id'],
+      account: @account,
+      text: text_from_content || '',
+      language: language_from_content,
+      spoiler_text: @object['summary'] || '',
+      created_at: @object['published'] || Time.now.utc,
+      reply: @object['inReplyTo'].present?,
+      sensitive: @object['sensitive'] || false,
+      visibility: visibility_from_audience,
+      thread: replied_to_status,
+      conversation: conversation_from_uri(@object['conversation']),
+    }
+  end
+
+  def process_tags(status)
+    return unless @object['tag'].is_a?(Array)
+
+    @object['tag'].each do |tag|
+      case tag['type']
+      when 'Hashtag'
+        process_hashtag tag, status
+      when 'Mention'
+        process_mention tag, status
+      end
+    end
+  end
+
+  def process_hashtag(tag, status)
+    hashtag = tag['name'].gsub(/\A#/, '').mb_chars.downcase
+    hashtag = Tag.where(name: hashtag).first_or_initialize(name: hashtag)
+
+    status.tags << hashtag
+  end
+
+  def process_mention(tag, status)
+    account = account_from_uri(tag['href'])
+    account = FetchRemoteAccountService.new.call(tag['href']) if account.nil?
+    return if account.nil?
+    account.mentions.create(status: status)
+  end
+
+  def process_attachments(status)
+    return unless @object['attachment'].is_a?(Array)
+
+    @object['attachment'].each do |attachment|
+      next if unsupported_media_type?(attachment['mediaType'])
+
+      href             = Addressable::URI.parse(attachment['url']).normalize.to_s
+      media_attachment = MediaAttachment.create(status: status, account: status.account, remote_url: href)
+
+      next if skip_download?
+
+      media_attachment.file_remote_url = href
+      media_attachment.save
+    end
+  end
+
+  def resolve_thread(status)
+    return unless status.reply? && status.thread.nil?
+    ThreadResolveWorker.perform_async(status.id, in_reply_to_uri)
+  end
+
+  def conversation_from_uri(uri)
+    return nil if uri.nil?
+    return Conversation.find_by(id: TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if TagManager.instance.local_id?(uri)
+    Conversation.find_by(uri: uri) || Conversation.create!(uri: uri)
+  end
+
+  def visibility_from_audience
+    if equals_or_includes?(@object['to'], ActivityPub::TagManager::COLLECTIONS[:public])
+      :public
+    elsif equals_or_includes?(@object['cc'], ActivityPub::TagManager::COLLECTIONS[:public])
+      :unlisted
+    elsif equals_or_includes?(@object['to'], @account.followers_url)
+      :private
+    else
+      :direct
+    end
+  end
+
+  def audience_includes?(account)
+    uri = ActivityPub::TagManager.instance.uri_for(account)
+    equals_or_includes?(@object['to'], uri) || equals_or_includes?(@object['cc'], uri)
+  end
+
+  def replied_to_status
+    return @replied_to_status if defined?(@replied_to_status)
+
+    if in_reply_to_uri.blank?
+      @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
+    end
+  end
+
+  def in_reply_to_uri
+    value_or_id(@object['inReplyTo'])
+  end
+
+  def text_from_content
+    if @object['content'].present?
+      @object['content']
+    elsif language_map?
+      @object['contentMap'].values.first
+    end
+  end
+
+  def language_from_content
+    return nil unless language_map?
+    @object['contentMap'].keys.first
+  end
+
+  def language_map?
+    @object['contentMap'].is_a?(Hash) && !@object['contentMap'].empty?
+  end
+
+  def unsupported_object_type?
+    @object.is_a?(String) || !%w(Article Note).include?(@object['type'])
+  end
+
+  def unsupported_media_type?(mime_type)
+    mime_type.present? && !(MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES).include?(mime_type)
+  end
+
+  def skip_download?
+    return @skip_download if defined?(@skip_download)
+    @skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?
+  end
+
+  def reply_to_local?
+    !replied_to_status.nil? && replied_to_status.account.local?
+  end
+
+  def forward_for_reply
+    return unless @json['signature'].present? && reply_to_local?
+    ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id)
+  end
+end
diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb
new file mode 100644
index 000000000..4c6afb090
--- /dev/null
+++ b/app/lib/activitypub/activity/delete.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+class ActivityPub::Activity::Delete < ActivityPub::Activity
+  def perform
+    if @account.uri == object_uri
+      delete_person
+    else
+      delete_note
+    end
+  end
+
+  private
+
+  def delete_person
+    SuspendAccountService.new.call(@account)
+  end
+
+  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?
+
+    delete_later!(object_uri)
+
+    return if status.nil?
+
+    forward_for_reblogs(status)
+    delete_now!(status)
+  end
+
+  def forward_for_reblogs(status)
+    return if @json['signature'].blank?
+
+    ActivityPub::RawDistributionWorker.push_bulk(status.reblogs.includes(:account).references(:account).merge(Account.local).pluck(:account_id)) do |account_id|
+      [payload, account_id]
+    end
+  end
+
+  def delete_now!(status)
+    RemoveStatusService.new.call(status)
+  end
+
+  def payload
+    @payload ||= Oj.dump(@json)
+  end
+end
diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb
new file mode 100644
index 000000000..8adbbb9c3
--- /dev/null
+++ b/app/lib/activitypub/activity/follow.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class ActivityPub::Activity::Follow < ActivityPub::Activity
+  def perform
+    target_account = account_from_uri(object_uri)
+
+    return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id']) || @account.requested?(target_account)
+
+    # Fast-forward repeat follow requests
+    if @account.following?(target_account)
+      AuthorizeFollowService.new.call(@account, target_account, skip_follow_request: true)
+      return
+    end
+
+    follow_request = FollowRequest.create!(account: @account, target_account: target_account)
+
+    if target_account.locked?
+      NotifyService.new.call(target_account, follow_request)
+    else
+      AuthorizeFollowService.new.call(@account, target_account)
+      NotifyService.new.call(target_account, ::Follow.find_by(account: @account, target_account: target_account))
+    end
+  end
+end
diff --git a/app/lib/activitypub/activity/like.rb b/app/lib/activitypub/activity/like.rb
new file mode 100644
index 000000000..674d5fe47
--- /dev/null
+++ b/app/lib/activitypub/activity/like.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class ActivityPub::Activity::Like < ActivityPub::Activity
+  def perform
+    original_status = status_from_uri(object_uri)
+
+    return if original_status.nil? || !original_status.account.local? || delete_arrived_first?(@json['id']) || @account.favourited?(original_status)
+
+    favourite = original_status.favourites.create!(account: @account)
+    NotifyService.new.call(original_status.account, favourite)
+  end
+end
diff --git a/app/lib/activitypub/activity/reject.rb b/app/lib/activitypub/activity/reject.rb
new file mode 100644
index 000000000..d815feeb6
--- /dev/null
+++ b/app/lib/activitypub/activity/reject.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class ActivityPub::Activity::Reject < ActivityPub::Activity
+  def perform
+    case @object['type']
+    when 'Follow'
+      reject_follow
+    end
+  end
+
+  private
+
+  def reject_follow
+    target_account = account_from_uri(target_uri)
+
+    return if target_account.nil? || !target_account.local?
+
+    follow_request = FollowRequest.find_by(account: target_account, target_account: @account)
+    follow_request&.reject!
+  end
+
+  def target_uri
+    @target_uri ||= value_or_id(@object['actor'])
+  end
+end
diff --git a/app/lib/activitypub/activity/undo.rb b/app/lib/activitypub/activity/undo.rb
new file mode 100644
index 000000000..4b0905de2
--- /dev/null
+++ b/app/lib/activitypub/activity/undo.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+class ActivityPub::Activity::Undo < ActivityPub::Activity
+  def perform
+    case @object['type']
+    when 'Announce'
+      undo_announce
+    when 'Follow'
+      undo_follow
+    when 'Like'
+      undo_like
+    when 'Block'
+      undo_block
+    end
+  end
+
+  private
+
+  def undo_announce
+    status = Status.find_by(uri: object_uri, account: @account)
+
+    if status.nil?
+      delete_later!(object_uri)
+    else
+      RemoveStatusService.new.call(status)
+    end
+  end
+
+  def undo_follow
+    target_account = account_from_uri(target_uri)
+
+    return if target_account.nil? || !target_account.local?
+
+    if @account.following?(target_account)
+      @account.unfollow!(target_account)
+    elsif @account.requested?(target_account)
+      FollowRequest.find_by(account: @account, target_account: target_account)&.destroy
+    else
+      delete_later!(object_uri)
+    end
+  end
+
+  def undo_like
+    status = status_from_uri(target_uri)
+
+    return if status.nil? || !status.account.local?
+
+    if @account.favourited?(status)
+      favourite = status.favourites.where(account: @account).first
+      favourite&.destroy
+    else
+      delete_later!(object_uri)
+    end
+  end
+
+  def undo_block
+    target_account = account_from_uri(target_uri)
+
+    return if target_account.nil? || !target_account.local?
+
+    if @account.blocking?(target_account)
+      UnblockService.new.call(@account, target_account)
+    else
+      delete_later!(object_uri)
+    end
+  end
+
+  def target_uri
+    @target_uri ||= value_or_id(@object['object'])
+  end
+end
diff --git a/app/lib/activitypub/activity/update.rb b/app/lib/activitypub/activity/update.rb
new file mode 100644
index 000000000..0134b4015
--- /dev/null
+++ b/app/lib/activitypub/activity/update.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class ActivityPub::Activity::Update < ActivityPub::Activity
+  def perform
+    case @object['type']
+    when 'Person'
+      update_account
+    end
+  end
+
+  private
+
+  def update_account
+    return if @account.uri != object_uri
+    ActivityPub::ProcessAccountService.new.call(@account.username, @account.domain, @object)
+  end
+end
diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb
index 0a70207bc..6ed66a239 100644
--- a/app/lib/activitypub/adapter.rb
+++ b/app/lib/activitypub/adapter.rb
@@ -1,13 +1,34 @@
 # frozen_string_literal: true
 
 class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
+  CONTEXT = {
+    '@context': [
+      'https://www.w3.org/ns/activitystreams',
+      'https://w3id.org/security/v1',
+
+      {
+        'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
+        '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
 
+  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': 'https://www.w3.org/ns/activitystreams' }.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/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/lib/activitypub/linked_data_signature.rb b/app/lib/activitypub/linked_data_signature.rb
new file mode 100644
index 000000000..adb8b6cdf
--- /dev/null
+++ b/app/lib/activitypub/linked_data_signature.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+class ActivityPub::LinkedDataSignature
+  include JsonLdHelper
+
+  CONTEXT = 'https://w3id.org/identity/v1'
+
+  def initialize(json)
+    @json = json.with_indifferent_access
+  end
+
+  def verify_account!
+    return unless @json['signature'].is_a?(Hash)
+
+    type        = @json['signature']['type']
+    creator_uri = @json['signature']['creator']
+    signature   = @json['signature']['signatureValue']
+
+    return unless type == 'RsaSignature2017'
+
+    creator   = ActivityPub::TagManager.instance.uri_to_resource(creator_uri, Account)
+    creator ||= ActivityPub::FetchRemoteKeyService.new.call(creator_uri)
+
+    return if creator.nil?
+
+    options_hash   = hash(@json['signature'].without('type', 'id', 'signatureValue').merge('@context' => CONTEXT))
+    document_hash  = hash(@json.without('signature'))
+    to_be_verified = options_hash + document_hash
+
+    if creator.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, Base64.decode64(signature), to_be_verified)
+      creator
+    end
+  end
+
+  def sign!(creator)
+    options = {
+      'type'    => 'RsaSignature2017',
+      'creator' => [ActivityPub::TagManager.instance.uri_for(creator), '#main-key'].join,
+      'created' => Time.now.utc.iso8601,
+    }
+
+    options_hash  = hash(options.without('type', 'id', 'signatureValue').merge('@context' => CONTEXT))
+    document_hash = hash(@json.without('signature'))
+    to_be_signed  = options_hash + document_hash
+
+    signature = Base64.strict_encode64(creator.keypair.sign(OpenSSL::Digest::SHA256.new, to_be_signed))
+
+    @json.merge('signature' => options.merge('signatureValue' => signature))
+  end
+
+  private
+
+  def hash(obj)
+    Digest::SHA256.hexdigest(canonicalize(obj))
+  end
+end
diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb
index ec42bcad3..de575d9e6 100644
--- a/app/lib/activitypub/tag_manager.rb
+++ b/app/lib/activitypub/tag_manager.rb
@@ -6,6 +6,8 @@ class ActivityPub::TagManager
   include Singleton
   include RoutingHelper
 
+  CONTEXT = 'https://www.w3.org/ns/activitystreams'
+
   COLLECTIONS = {
     public: 'https://www.w3.org/ns/activitystreams#Public',
   }.freeze
@@ -17,6 +19,7 @@ class ActivityPub::TagManager
     when :person
       short_account_url(target)
     when :note, :comment, :activity
+      return activity_account_status_url(target.account, target) if target.reblog?
       short_account_status_url(target.account, target)
     end
   end
@@ -28,10 +31,17 @@ class ActivityPub::TagManager
     when :person
       account_url(target)
     when :note, :comment, :activity
+      return activity_account_status_url(target.account, target) if target.reblog?
       account_status_url(target.account, target)
     end
   end
 
+  def activity_uri_for(target)
+    return nil unless %i(note comment activity).include?(target.object_type) && target.local?
+
+    activity_account_status_url(target.account, target)
+  end
+
   # Primary audience of a status
   # Public statuses go out to primarily the public collection
   # Unlisted and private statuses go out primarily to the followers collection
@@ -66,4 +76,32 @@ class ActivityPub::TagManager
 
     cc
   end
+
+  def local_uri?(uri)
+    uri  = Addressable::URI.parse(uri)
+    host = uri.normalized_host
+    host = "#{host}:#{uri.port}" if uri.port
+
+    !host.nil? && (::TagManager.instance.local_domain?(host) || ::TagManager.instance.web_domain?(host))
+  end
+
+  def uri_to_local_id(uri, param = :id)
+    path_params = Rails.application.routes.recognize_path(uri)
+    path_params[param]
+  end
+
+  def uri_to_resource(uri, klass)
+    if local_uri?(uri)
+      case klass.name
+      when 'Account'
+        klass.find_local(uri_to_local_id(uri, :username))
+      else
+        klass.find_by(id: uri_to_local_id(uri))
+      end
+    elsif ::TagManager.instance.local_id?(uri)
+      klass.find_by(id: ::TagManager.instance.unique_tag_to_local_id(uri, klass.to_s))
+    else
+      klass.find_by(uri: uri.split('#').first)
+    end
+  end
 end
diff --git a/app/lib/ostatus/activity/base.rb b/app/lib/ostatus/activity/base.rb
index e1477f0eb..1dc7abee3 100644
--- a/app/lib/ostatus/activity/base.rb
+++ b/app/lib/ostatus/activity/base.rb
@@ -29,21 +29,43 @@ class OStatus::Activity::Base
   end
 
   def url
-    link = @xml.at_xpath('./xmlns:link[@rel="alternate"]', xmlns: TagManager::XMLNS)
+    link = @xml.xpath('./xmlns:link[@rel="alternate"]', xmlns: TagManager::XMLNS).find { |link_candidate| link_candidate['type'] == 'text/html' }
     link.nil? ? nil : link['href']
   end
 
+  def activitypub_uri
+    link = @xml.xpath('./xmlns:link[@rel="alternate"]', xmlns: TagManager::XMLNS).find { |link_candidate| ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(link_candidate['type']) }
+    link.nil? ? nil : link['href']
+  end
+
+  def activitypub_uri?
+    activitypub_uri.present?
+  end
+
   private
 
   def find_status(uri)
     if TagManager.instance.local_id?(uri)
       local_id = TagManager.instance.unique_tag_to_local_id(uri, 'Status')
       return Status.find_by(id: local_id)
+    elsif ActivityPub::TagManager.instance.local_uri?(uri)
+      local_id = ActivityPub::TagManager.instance.uri_to_local_id(uri)
+      return Status.find_by(id: local_id)
     end
 
     Status.find_by(uri: uri)
   end
 
+  def find_activitypub_status(uri, href)
+    tag_matches = /tag:([^,:]+)[^:]*:objectId=([\d]+)/.match(uri)
+    href_matches = %r{/users/([^/]+)}.match(href)
+
+    unless tag_matches.nil? || href_matches.nil?
+      uri = "https://#{tag_matches[1]}/users/#{href_matches[1]}/statuses/#{tag_matches[2]}"
+      Status.find_by(uri: uri)
+    end
+  end
+
   def redis
     Redis.current
   end
diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb
index e22f746f2..1a23c9efa 100644
--- a/app/lib/ostatus/activity/creation.rb
+++ b/app/lib/ostatus/activity/creation.rb
@@ -9,6 +9,11 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
 
     return [nil, false] if @account.suspended?
 
+    if activitypub_uri? && [:public, :unlisted].include?(visibility_scope)
+      result = perform_via_activitypub
+      return result if result.first.present?
+    end
+
     Rails.logger.debug "Creating remote status #{id}"
 
     # Return early if status already exists in db
@@ -16,24 +21,28 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
 
     return [status, false] unless status.nil?
 
-    status = Status.create!(
-      uri: id,
-      url: url,
-      account: @account,
-      reblog: reblog,
-      text: content,
-      spoiler_text: content_warning,
-      created_at: published,
-      reply: thread?,
-      language: content_language,
-      visibility: visibility_scope,
-      conversation: find_or_create_conversation,
-      thread: thread? ? find_status(thread.first) : nil
-    )
-
-    save_mentions(status)
-    save_hashtags(status)
-    save_media(status)
+    cached_reblog = reblog
+
+    ApplicationRecord.transaction do
+      status = Status.create!(
+        uri: id,
+        url: url,
+        account: @account,
+        reblog: cached_reblog,
+        text: content,
+        spoiler_text: content_warning,
+        created_at: published,
+        reply: thread?,
+        language: content_language,
+        visibility: visibility_scope,
+        conversation: find_or_create_conversation,
+        thread: thread? ? find_status(thread.first) || find_activitypub_status(thread.first, thread.second) : nil
+      )
+
+      save_mentions(status)
+      save_hashtags(status)
+      save_media(status)
+    end
 
     if thread? && status.thread.nil?
       Rails.logger.debug "Trying to attach #{status.id} (#{id}) to #{thread.first}"
@@ -48,6 +57,10 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
     [status, true]
   end
 
+  def perform_via_activitypub
+    [find_status(activitypub_uri) || ActivityPub::FetchRemoteStatusService.new.call(activitypub_uri), false]
+  end
+
   def content
     @xml.at_xpath('./xmlns:content', xmlns: TagManager::XMLNS).content
   end
diff --git a/app/lib/ostatus/activity/deletion.rb b/app/lib/ostatus/activity/deletion.rb
index 860faf501..c98f5ee0a 100644
--- a/app/lib/ostatus/activity/deletion.rb
+++ b/app/lib/ostatus/activity/deletion.rb
@@ -3,7 +3,9 @@
 class OStatus::Activity::Deletion < OStatus::Activity::Base
   def perform
     Rails.logger.debug "Deleting remote status #{id}"
-    status = Status.find_by(uri: id, account: @account)
+
+    status   = Status.find_by(uri: id, account: @account)
+    status ||= Status.find_by(uri: activitypub_uri, account: @account) if activitypub_uri?
 
     if status.nil?
       redis.setex("delete_upon_arrival:#{@account.id}:#{id}", 6 * 3_600, id)
diff --git a/app/lib/ostatus/activity/remote.rb b/app/lib/ostatus/activity/remote.rb
index ecec6886c..5b204b6d8 100644
--- a/app/lib/ostatus/activity/remote.rb
+++ b/app/lib/ostatus/activity/remote.rb
@@ -2,6 +2,10 @@
 
 class OStatus::Activity::Remote < OStatus::Activity::Base
   def perform
-    find_status(id) || FetchRemoteStatusService.new.call(url)
+    if activitypub_uri?
+      find_status(activitypub_uri) || FetchRemoteStatusService.new.call(url)
+    else
+      find_status(id) || FetchRemoteStatusService.new.call(url)
+    end
   end
 end
diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb
index 0d62361be..81fae4140 100644
--- a/app/lib/ostatus/atom_serializer.rb
+++ b/app/lib/ostatus/atom_serializer.rb
@@ -79,6 +79,9 @@ class OStatus::AtomSerializer
 
     if stream_entry.status.nil?
       append_element(entry, 'content', 'Deleted status')
+    elsif stream_entry.status.destroyed?
+      append_element(entry, 'content', 'Deleted status')
+      append_element(entry, 'link', nil, rel: :alternate, type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(stream_entry.status)) if stream_entry.account.local?
     else
       serialize_status_attributes(entry, stream_entry.status)
     end
@@ -343,6 +346,8 @@ class OStatus::AtomSerializer
   end
 
   def serialize_status_attributes(entry, status)
+    append_element(entry, 'link', nil, rel: :alternate, type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(status)) if status.account.local?
+
     append_element(entry, 'summary', status.spoiler_text, 'xml:lang': status.language) if status.spoiler_text?
     append_element(entry, 'content', Formatter.instance.format(status).to_str, type: 'html', 'xml:lang': status.language)
 
diff --git a/app/lib/request.rb b/app/lib/request.rb
index e73c5ac20..c01e07925 100644
--- a/app/lib/request.rb
+++ b/app/lib/request.rb
@@ -12,15 +12,21 @@ class Request
     @headers = {}
 
     set_common_headers!
+    set_digest! if options.key?(:body)
   end
 
-  def on_behalf_of(account)
+  def on_behalf_of(account, key_id_format = :acct)
     raise ArgumentError unless account.local?
-    @account = account
+
+    @account       = account
+    @key_id_format = key_id_format
+
+    self
   end
 
   def add_headers(new_headers)
     @headers.merge!(new_headers)
+    self
   end
 
   def perform
@@ -40,8 +46,11 @@ class Request
     @headers['Date']         = Time.now.utc.httpdate
   end
 
+  def set_digest!
+    @headers['Digest'] = "SHA-256=#{Digest::SHA256.base64digest(@options[:body])}"
+  end
+
   def signature
-    key_id    = @account.to_webfinger_s
     algorithm = 'rsa-sha256'
     signature = Base64.strict_encode64(@account.keypair.sign(OpenSSL::Digest::SHA256.new, signed_string))
 
@@ -60,6 +69,15 @@ class Request
     @user_agent ||= "#{HTTP::Request::USER_AGENT} (Mastodon/#{Mastodon::Version}; +#{root_url})"
   end
 
+  def key_id
+    case @key_id_format
+    when :acct
+      @account.to_webfinger_s
+    when :uri
+      [ActivityPub::TagManager.instance.uri_for(@account), '#main-key'].join
+    end
+  end
+
   def timeout
     { write: 10, connect: 10, read: 10 }
   end
diff --git a/app/lib/stream_entry_finder.rb b/app/lib/status_finder.rb
index 0ea33229c..4d1aed297 100644
--- a/app/lib/stream_entry_finder.rb
+++ b/app/lib/status_finder.rb
@@ -1,20 +1,22 @@
 # frozen_string_literal: true
 
-class StreamEntryFinder
+class StatusFinder
   attr_reader :url
 
   def initialize(url)
     @url = url
   end
 
-  def stream_entry
+  def status
     verify_action!
 
+    raise ActiveRecord::RecordNotFound unless TagManager.instance.local_url?(url)
+
     case recognized_params[:controller]
     when 'stream_entries'
-      StreamEntry.find(recognized_params[:id])
+      StreamEntry.find(recognized_params[:id]).status
     when 'statuses'
-      Status.find(recognized_params[:id]).stream_entry
+      Status.find(recognized_params[:id])
     else
       raise ActiveRecord::RecordNotFound
     end