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/activity_tracker.rb6
-rw-r--r--app/lib/activitypub/activity.rb51
-rw-r--r--app/lib/activitypub/activity/announce.rb15
-rw-r--r--app/lib/activitypub/activity/create.rb37
-rw-r--r--app/lib/feed_manager.rb9
-rw-r--r--app/lib/formatter.rb49
-rw-r--r--app/lib/ostatus/activity/base.rb6
-rw-r--r--app/lib/potential_friendship_tracker.rb8
8 files changed, 138 insertions, 43 deletions
diff --git a/app/lib/activity_tracker.rb b/app/lib/activity_tracker.rb
index 5b4972674..ae3c11b6a 100644
--- a/app/lib/activity_tracker.rb
+++ b/app/lib/activity_tracker.rb
@@ -4,6 +4,8 @@ class ActivityTracker
   EXPIRE_AFTER = 90.days.seconds
 
   class << self
+    include Redisable
+
     def increment(prefix)
       key = [prefix, current_week].join(':')
 
@@ -20,10 +22,6 @@ class ActivityTracker
 
     private
 
-    def redis
-      Redis.current
-    end
-
     def current_week
       Time.zone.today.cweek
     end
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index 87318fb1c..11fa3363a 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -2,6 +2,10 @@
 
 class ActivityPub::Activity
   include JsonLdHelper
+  include Redisable
+
+  SUPPORTED_TYPES = %w(Note).freeze
+  CONVERTED_TYPES = %w(Image Video Article Page).freeze
 
   def initialize(json, account, **options)
     @json    = json
@@ -70,8 +74,16 @@ class ActivityPub::Activity
     @object_uri ||= value_or_id(@object)
   end
 
-  def redis
-    Redis.current
+  def unsupported_object_type?
+    @object.is_a?(String) || !(supported_object_type? || converted_object_type?)
+  end
+
+  def supported_object_type?
+    equals_or_includes_any?(@object['type'], SUPPORTED_TYPES)
+  end
+
+  def converted_object_type?
+    equals_or_includes_any?(@object['type'], CONVERTED_TYPES)
   end
 
   def distribute(status)
@@ -123,6 +135,24 @@ class ActivityPub::Activity
     redis.setex("delete_upon_arrival:#{@account.id}:#{uri}", 6.hours.seconds, uri)
   end
 
+  def status_from_object
+    # If the status is already known, return it
+    status = status_from_uri(object_uri)
+
+    return status unless status.nil?
+
+    # If the boosted toot is embedded and it is a self-boost, handle it like a Create
+    unless unsupported_object_type?
+      actor_id = value_or_id(first_of_value(@object['attributedTo'])) || @account.uri
+
+      if actor_id == @account.uri
+        return ActivityPub::Activity.factory({ 'type' => 'Create', 'actor' => actor_id, 'object' => @object }, @account).perform
+      end
+    end
+
+    fetch_remote_original_status
+  end
+
   def fetch_remote_original_status
     if object_uri.start_with?('http')
       return if ActivityPub::TagManager.instance.local_uri?(object_uri)
@@ -137,4 +167,21 @@ class ActivityPub::Activity
   ensure
     redis.del(key)
   end
+
+  def fetch?
+    !@options[:delivery]
+  end
+
+  def followed_by_local_accounts?
+    @account.passive_relationships.exists?
+  end
+
+  def requested_through_relay?
+    @options[:relayed_through_account] && Relay.find_by(inbox_url: @options[:relayed_through_account].inbox_url)&.enabled?
+  end
+
+  def reject_payload!
+    Rails.logger.info("Rejected #{@json['type']} activity #{@json['id']} from #{@account.uri}#{@options[:relayed_through_account] && "via #{@options[:relayed_through_account].uri}"}")
+    nil
+  end
 end
diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb
index 34d1b7cbd..9f8ffd9fb 100644
--- a/app/lib/activitypub/activity/announce.rb
+++ b/app/lib/activitypub/activity/announce.rb
@@ -2,10 +2,11 @@
 
 class ActivityPub::Activity::Announce < ActivityPub::Activity
   def perform
-    original_status   = status_from_uri(object_uri)
-    original_status ||= fetch_remote_original_status
+    return reject_payload! if delete_arrived_first?(@json['id']) || !related_to_local_activity?
 
-    return if original_status.nil? || delete_arrived_first?(@json['id']) || !announceable?(original_status)
+    original_status = status_from_object
+
+    return reject_payload! if original_status.nil? || !announceable?(original_status)
 
     status = Status.find_by(account: @account, reblog: original_status)
 
@@ -41,4 +42,12 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
   def announceable?(status)
     status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility?
   end
+
+  def related_to_local_activity?
+    followed_by_local_accounts? || requested_through_relay? || reblog_of_local_status?
+  end
+
+  def reblog_of_local_status?
+    status_from_uri(object_uri)&.account&.local?
+  end
 end
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index b49657d4b..d7bd65c80 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -1,12 +1,8 @@
 # frozen_string_literal: true
 
 class ActivityPub::Activity::Create < ActivityPub::Activity
-  SUPPORTED_TYPES = %w(Note).freeze
-  CONVERTED_TYPES = %w(Image Video Article Page).freeze
-
   def perform
-    return if unsupported_object_type? || invalid_origin?(@object['id'])
-    return if Tombstone.exists?(uri: @object['id'])
+    return reject_payload! if unsupported_object_type? || invalid_origin?(@object['id']) || Tombstone.exists?(uri: @object['id']) || !related_to_local_activity?
 
     RedisLock.acquire(lock_options) do |lock|
       if lock.acquired?
@@ -318,22 +314,10 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     @object['nameMap'].is_a?(Hash) && !@object['nameMap'].empty?
   end
 
-  def unsupported_object_type?
-    @object.is_a?(String) || !(supported_object_type? || converted_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 supported_object_type?
-    equals_or_includes_any?(@object['type'], SUPPORTED_TYPES)
-  end
-
-  def converted_object_type?
-    equals_or_includes_any?(@object['type'], CONVERTED_TYPES)
-  end
-
   def skip_download?
     return @skip_download if defined?(@skip_download)
     @skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?
@@ -352,6 +336,25 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     !replied_to_status.nil? && replied_to_status.account.local?
   end
 
+  def related_to_local_activity?
+    fetch? || followed_by_local_accounts? || requested_through_relay? ||
+      responds_to_followed_account? || addresses_local_accounts?
+  end
+
+  def responds_to_followed_account?
+    !replied_to_status.nil? && (replied_to_status.account.local? || replied_to_status.account.passive_relationships.exists?)
+  end
+
+  def addresses_local_accounts?
+    return true if @options[:delivered_to_account_id]
+
+    local_usernames = (as_array(@object['to']) + as_array(@object['cc'])).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) }
+
+    return false if local_usernames.empty?
+
+    Account.local.where(username: local_usernames).exists?
+  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, [@account.preferred_inbox_url])
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index a1b186f1c..4bc75dae8 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -4,6 +4,7 @@ require 'singleton'
 
 class FeedManager
   include Singleton
+  include Redisable
 
   MAX_ITEMS = 400
 
@@ -35,7 +36,7 @@ class FeedManager
 
   def unpush_from_home(account, status)
     return false unless remove_from_feed(:home, account.id, status)
-    Redis.current.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s))
+    redis.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s))
     true
   end
 
@@ -54,7 +55,7 @@ class FeedManager
 
   def unpush_from_list(list, status)
     return false unless remove_from_feed(:list, list.id, status)
-    Redis.current.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s))
+    redis.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s))
     true
   end
 
@@ -143,10 +144,6 @@ class FeedManager
 
   private
 
-  def redis
-    Redis.current
-  end
-
   def push_update_required?(timeline_id)
     redis.exists("subscribed:#{timeline_id}")
   end
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index 05fd9eeb1..0653214f5 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -99,7 +99,7 @@ class Formatter
   end
 
   def encode_and_link_urls(html, accounts = nil, options = {})
-    entities = Extractor.extract_entities_with_indices(html, extract_url_without_protocol: false)
+    entities = utf8_friendly_extractor(html, extract_url_without_protocol: false)
 
     if accounts.is_a?(Hash)
       options  = accounts
@@ -199,6 +199,53 @@ class Formatter
     result.flatten.join
   end
 
+  UNICODE_ESCAPE_BLACKLIST_RE = /\p{Z}|\p{P}/
+
+  def utf8_friendly_extractor(text, options = {})
+    old_to_new_index = [0]
+
+    escaped = text.chars.map do |c|
+      output = begin
+        if c.ord.to_s(16).length > 2 && UNICODE_ESCAPE_BLACKLIST_RE.match(c).nil?
+          CGI.escape(c)
+        else
+          c
+        end
+      end
+
+      old_to_new_index << old_to_new_index.last + output.length
+
+      output
+    end.join
+
+    # Note: I couldn't obtain list_slug with @user/list-name format
+    # for mention so this requires additional check
+    special = Extractor.extract_urls_with_indices(escaped, options).map do |extract|
+      # exactly one of :url, :hashtag, :screen_name, :cashtag keys is present
+      key = (extract.keys & [:url, :hashtag, :screen_name, :cashtag]).first
+
+      new_indices = [
+        old_to_new_index.find_index(extract[:indices].first),
+        old_to_new_index.find_index(extract[:indices].last),
+      ]
+
+      has_prefix_char = [:hashtag, :screen_name, :cashtag].include?(key)
+      value_indices = [
+        new_indices.first + (has_prefix_char ? 1 : 0), # account for #, @ or $
+        new_indices.last - 1,
+      ]
+
+      next extract.merge(
+        :indices => new_indices,
+        key => text[value_indices.first..value_indices.last]
+      )
+    end
+
+    standard = Extractor.extract_entities_with_indices(text, options)
+
+    Extractor.remove_overlapping_entities(special + standard)
+  end
+
   def link_to_url(entity, options = {})
     url        = Addressable::URI.parse(entity[:url])
     html_attrs = { target: '_blank', rel: 'nofollow noopener' }
diff --git a/app/lib/ostatus/activity/base.rb b/app/lib/ostatus/activity/base.rb
index c5933f3ad..db70f1998 100644
--- a/app/lib/ostatus/activity/base.rb
+++ b/app/lib/ostatus/activity/base.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class OStatus::Activity::Base
+  include Redisable
+
   def initialize(xml, account = nil, **options)
     @xml     = xml
     @account = account
@@ -66,8 +68,4 @@ class OStatus::Activity::Base
       Status.find_by(uri: uri)
     end
   end
-
-  def redis
-    Redis.current
-  end
 end
diff --git a/app/lib/potential_friendship_tracker.rb b/app/lib/potential_friendship_tracker.rb
index 017a9748d..188aa4a27 100644
--- a/app/lib/potential_friendship_tracker.rb
+++ b/app/lib/potential_friendship_tracker.rb
@@ -11,6 +11,8 @@ class PotentialFriendshipTracker
   }.freeze
 
   class << self
+    include Redisable
+
     def record(account_id, target_account_id, action)
       return if account_id == target_account_id
 
@@ -31,11 +33,5 @@ class PotentialFriendshipTracker
       return [] if account_ids.empty?
       Account.searchable.where(id: account_ids)
     end
-
-    private
-
-    def redis
-      Redis.current
-    end
   end
 end