about summary refs log tree commit diff
path: root/app/lib/activitypub/activity
diff options
context:
space:
mode:
Diffstat (limited to 'app/lib/activitypub/activity')
-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.rb185
-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
10 files changed, 444 insertions, 0 deletions
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..9a34484f5
--- /dev/null
+++ b/app/lib/activitypub/activity/create.rb
@@ -0,0 +1,185 @@
+# 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 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
+
+  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