about summary refs log tree commit diff
path: root/app/lib
diff options
context:
space:
mode:
Diffstat (limited to 'app/lib')
-rw-r--r--app/lib/activitypub/activity.rb28
-rw-r--r--app/lib/activitypub/activity/announce.rb39
-rw-r--r--app/lib/activitypub/activity/block.rb7
-rw-r--r--app/lib/activitypub/activity/create.rb2
-rw-r--r--app/lib/activitypub/activity/undo.rb51
-rw-r--r--app/lib/activitypub/activity/update.rb2
-rw-r--r--app/lib/exceptions.rb1
-rw-r--r--app/lib/feed_manager.rb8
8 files changed, 122 insertions, 16 deletions
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index 0ce279d28..ab946470b 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -157,6 +157,34 @@ class ActivityPub::Activity
     fetch_remote_original_status
   end
 
+  def dereference_object!
+    return unless @object.is_a?(String)
+    return if invalid_origin?(@object)
+
+    object = fetch_resource(@object, true, signed_fetch_account)
+    return unless object.present? && object.is_a?(Hash) && supported_context?(object)
+
+    @object = object
+  end
+
+  def signed_fetch_account
+    first_mentioned_local_account || first_local_follower
+  end
+
+  def first_mentioned_local_account
+    audience = (as_array(@json['to']) + as_array(@json['cc'])).uniq
+    local_usernames = audience.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }
+                              .map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) }
+
+    return if local_usernames.empty?
+
+    Account.local.where(username: local_usernames).first
+  end
+
+  def first_local_follower
+    @account.followers.local.first
+  end
+
   def follow_request_from_object
     @follow_request ||= FollowRequest.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
   end
diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb
index 34c646668..9e108985a 100644
--- a/app/lib/activitypub/activity/announce.rb
+++ b/app/lib/activitypub/activity/announce.rb
@@ -4,25 +4,32 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
   def perform
     return reject_payload! if delete_arrived_first?(@json['id']) || !related_to_local_activity?
 
-    original_status = status_from_object
+    RedisLock.acquire(lock_options) do |lock|
+      if lock.acquired?
+        original_status = status_from_object
 
-    return reject_payload! if original_status.nil? || !announceable?(original_status)
+        return reject_payload! if original_status.nil? || !announceable?(original_status)
 
-    status = Status.find_by(account: @account, reblog: original_status)
+        @status = Status.find_by(account: @account, reblog: original_status)
 
-    return status unless status.nil?
+        return @status unless @status.nil?
 
-    status = Status.create!(
-      account: @account,
-      reblog: original_status,
-      uri: @json['id'],
-      created_at: @json['published'],
-      override_timestamps: @options[:override_timestamps],
-      visibility: visibility_from_audience
-    )
+        @status = Status.create!(
+          account: @account,
+          reblog: original_status,
+          uri: @json['id'],
+          created_at: @json['published'],
+          override_timestamps: @options[:override_timestamps],
+          visibility: visibility_from_audience
+        )
 
-    distribute(status)
-    status
+        distribute(@status)
+      else
+        raise Mastodon::RaceConditionError
+      end
+    end
+
+    @status
   end
 
   private
@@ -54,4 +61,8 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
   def reblog_of_local_status?
     status_from_uri(object_uri)&.account&.local?
   end
+
+  def lock_options
+    { redis: Redis.current, key: "announce:#{@object['id']}" }
+  end
 end
diff --git a/app/lib/activitypub/activity/block.rb b/app/lib/activitypub/activity/block.rb
index a17a2d50a..90477bf33 100644
--- a/app/lib/activitypub/activity/block.rb
+++ b/app/lib/activitypub/activity/block.rb
@@ -4,7 +4,12 @@ class ActivityPub::Activity::Block < ActivityPub::Activity
   def perform
     target_account = account_from_uri(object_uri)
 
-    return if target_account.nil? || !target_account.local? || @account.blocking?(target_account)
+    return if target_account.nil? || !target_account.local?
+
+    if @account.blocking?(target_account)
+      @account.block_relationships.find_by(target_account: target_account).update(uri: @json['id']) if @json['id'].present?
+      return
+    end
 
     UnfollowService.new.call(target_account, @account) if target_account.following?(@account)
 
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index e81452e3c..08dd98e94 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -2,6 +2,8 @@
 
 class ActivityPub::Activity::Create < ActivityPub::Activity
   def perform
+    dereference_object!
+
     case @object['type']
     when 'EncryptedMessage'
       create_encrypted_message
diff --git a/app/lib/activitypub/activity/undo.rb b/app/lib/activitypub/activity/undo.rb
index 599823c6e..9eff1b71c 100644
--- a/app/lib/activitypub/activity/undo.rb
+++ b/app/lib/activitypub/activity/undo.rb
@@ -13,11 +13,62 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity
       undo_like
     when 'Block'
       undo_block
+    when nil
+      handle_reference
     end
   end
 
   private
 
+  def handle_reference
+    # Some implementations do not inline the object, and as we don't have a
+    # global index, we have to guess what object it is.
+    return if object_uri.nil?
+
+    try_undo_announce || try_undo_accept || try_undo_follow || try_undo_like || try_undo_block || delete_later!(object_uri)
+  end
+
+  def try_undo_announce
+    status = Status.where.not(reblog_of_id: nil).find_by(uri: object_uri, account: @account)
+    if status.present?
+      RemoveStatusService.new.call(status)
+      true
+    else
+      false
+    end
+  end
+
+  def try_undo_accept
+    # We can't currently handle `Undo Accept` as we don't record `Accept`'s uri
+    false
+  end
+
+  def try_undo_follow
+    follow = @account.follow_requests.find_by(uri: object_uri) || @account.active_relationships.find_by(uri: object_uri)
+
+    if follow.present?
+      follow.destroy
+      true
+    else
+      false
+    end
+  end
+
+  def try_undo_like
+    # There is an index on accounts, but an account may have *many* favs, so this may be too costly
+    false
+  end
+
+  def try_undo_block
+    block = @account.block_relationships.find_by(uri: object_uri)
+    if block.present?
+      UnblockService.new.call(@account, block.target_account)
+      true
+    else
+      false
+    end
+  end
+
   def undo_announce
     return if object_uri.nil?
 
diff --git a/app/lib/activitypub/activity/update.rb b/app/lib/activitypub/activity/update.rb
index 70035325b..018e2df54 100644
--- a/app/lib/activitypub/activity/update.rb
+++ b/app/lib/activitypub/activity/update.rb
@@ -4,6 +4,8 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
   SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
 
   def perform
+    dereference_object!
+
     if equals_or_includes_any?(@object['type'], SUPPORTED_TYPES)
       update_account
     elsif equals_or_includes_any?(@object['type'], %w(Question))
diff --git a/app/lib/exceptions.rb b/app/lib/exceptions.rb
index 3362576b0..7c8e77871 100644
--- a/app/lib/exceptions.rb
+++ b/app/lib/exceptions.rb
@@ -7,6 +7,7 @@ module Mastodon
   class HostValidationError < ValidationError; end
   class LengthValidationError < ValidationError; end
   class DimensionsValidationError < ValidationError; end
+  class StreamValidationError < ValidationError; end
   class RaceConditionError < Error; end
   class RateLimitExceededError < Error; end
 
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index 6afdd3b08..2dc60092c 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -139,9 +139,15 @@ class FeedManager
   end
 
   def clear_from_timeline(account, target_account)
+    # Clear from timeline all statuses from or mentionning target_account
     timeline_key        = key(:home, account.id)
     timeline_status_ids = redis.zrange(timeline_key, 0, -1)
-    target_statuses     = Status.where(id: timeline_status_ids, account: target_account)
+    statuses            = Status.where(id: timeline_status_ids).select(:id, :reblog_of_id, :account_id).to_a
+    reblogged_ids       = Status.where(id: statuses.map(&:reblog_of_id).compact, account: target_account).pluck(:id)
+    with_mentions_ids   = Mention.active.where(status_id: statuses.flat_map { |s| [s.id, s.reblog_of_id] }.compact, account: target_account).pluck(:status_id)
+    target_statuses     = statuses.filter do |status|
+      status.account_id == target_account.id || reblogged_ids.include?(status.reblog_of_id) || with_mentions_ids.include?(status.id) || with_mentions_ids.include?(status.reblog_of_id)
+    end
 
     target_statuses.each do |status|
       unpush_from_home(account, status)