about summary refs log tree commit diff
path: root/app/services
diff options
context:
space:
mode:
Diffstat (limited to 'app/services')
-rw-r--r--app/services/account_search_service.rb26
-rw-r--r--app/services/fan_out_on_write_service.rb17
-rw-r--r--app/services/favourite_service.rb7
-rw-r--r--app/services/fetch_atom_service.rb2
-rw-r--r--app/services/fetch_remote_account_service.rb9
-rw-r--r--app/services/fetch_remote_resource_service.rb18
-rw-r--r--app/services/fetch_remote_status_service.rb9
-rw-r--r--app/services/follow_service.rb12
-rw-r--r--app/services/mute_service.rb23
-rw-r--r--app/services/post_status_service.rb15
-rw-r--r--app/services/precompute_feed_service.rb8
-rw-r--r--app/services/process_feed_service.rb23
-rw-r--r--app/services/process_interaction_service.rb2
-rw-r--r--app/services/process_mentions_service.rb2
-rw-r--r--app/services/reblog_service.rb2
-rw-r--r--app/services/remove_status_service.rb2
-rw-r--r--app/services/search_service.rb25
-rw-r--r--app/services/unfavourite_service.rb5
-rw-r--r--app/services/unfollow_service.rb5
-rw-r--r--app/services/unmute_service.rb11
-rw-r--r--app/services/update_remote_profile_service.rb1
21 files changed, 181 insertions, 43 deletions
diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb
new file mode 100644
index 000000000..f55439dcb
--- /dev/null
+++ b/app/services/account_search_service.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class AccountSearchService < BaseService
+  def call(query, limit, resolve = false, account = nil)
+    return [] if query.blank? || query.start_with?('#')
+
+    username, domain = query.gsub(/\A@/, '').split('@')
+    domain = nil if TagManager.instance.local_domain?(domain)
+
+    if domain.nil?
+      exact_match = Account.find_local(username)
+      results     = account.nil? ? Account.search_for(username, limit) : Account.advanced_search_for(username, account, limit)
+    else
+      exact_match = Account.find_remote(username, domain)
+      results     = account.nil? ? Account.search_for("#{username} #{domain}", limit) : Account.advanced_search_for("#{username} #{domain}", account, limit)
+    end
+
+    results = [exact_match] + results.reject { |a| a.id == exact_match.id } if exact_match
+
+    if resolve && !exact_match && !domain.nil?
+      results = [FollowRemoteAccountService.new.call("#{username}@#{domain}")]
+    end
+
+    results
+  end
+end
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index 71f6cbca1..402b84b2f 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -5,7 +5,12 @@ class FanOutOnWriteService < BaseService
   # @param [Status] status
   def call(status)
     deliver_to_self(status) if status.account.local?
-    deliver_to_followers(status)
+
+    if status.direct_visibility?
+      deliver_to_mentioned_followers(status)
+    else
+      deliver_to_followers(status)
+    end
 
     return if status.account.silenced? || !status.public_visibility? || status.reblog?
 
@@ -32,6 +37,16 @@ class FanOutOnWriteService < BaseService
     end
   end
 
+  def deliver_to_mentioned_followers(status)
+    Rails.logger.debug "Delivering status #{status.id} to mentioned followers"
+
+    status.mentions.includes(:account).each do |mention|
+      mentioned_account = mention.account
+      next if !mentioned_account.local? || !mentioned_account.following?(status.account) || FeedManager.instance.filter?(:home, status, mentioned_account)
+      FeedManager.instance.push(:home, mentioned_account, status)
+    end
+  end
+
   def deliver_to_hashtags(status)
     Rails.logger.debug "Delivering status #{status.id} to hashtags"
 
diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb
index 824729ed6..5cc96403c 100644
--- a/app/services/favourite_service.rb
+++ b/app/services/favourite_service.rb
@@ -6,7 +6,7 @@ class FavouriteService < BaseService
   # @param [Status] status
   # @return [Favourite]
   def call(account, status)
-    raise Mastodon::NotPermitted unless status.permitted?(account)
+    raise Mastodon::NotPermittedError unless status.permitted?(account)
 
     favourite = Favourite.create!(account: account, status: status)
 
@@ -22,10 +22,13 @@ class FavouriteService < BaseService
   private
 
   def build_xml(favourite)
+    description = "#{favourite.account.acct} favourited a status by #{favourite.status.account.acct}"
+
     Nokogiri::XML::Builder.new do |xml|
       entry(xml, true) do
         unique_id xml, favourite.created_at, favourite.id, 'Favourite'
-        title xml, "#{favourite.account.acct} favourited a status by #{favourite.status.account.acct}"
+        title xml, description
+        content xml, description
 
         author(xml) do
           include_author xml, favourite.account
diff --git a/app/services/fetch_atom_service.rb b/app/services/fetch_atom_service.rb
index f7e9c150a..c3dad1eb9 100644
--- a/app/services/fetch_atom_service.rb
+++ b/app/services/fetch_atom_service.rb
@@ -47,6 +47,6 @@ class FetchAtomService < BaseService
   end
 
   def http_client
-    HTTP.timeout(:per_operation, write: 20, connect: 20, read: 50).follow
+    HTTP.timeout(:per_operation, write: 10, connect: 10, read: 10).follow
   end
 end
diff --git a/app/services/fetch_remote_account_service.rb b/app/services/fetch_remote_account_service.rb
index baefa3a86..6a6a696d6 100644
--- a/app/services/fetch_remote_account_service.rb
+++ b/app/services/fetch_remote_account_service.rb
@@ -1,8 +1,13 @@
 # frozen_string_literal: true
 
 class FetchRemoteAccountService < BaseService
-  def call(url)
-    atom_url, body = FetchAtomService.new.call(url)
+  def call(url, prefetched_body = nil)
+    if prefetched_body.nil?
+      atom_url, body = FetchAtomService.new.call(url)
+    else
+      atom_url = url
+      body     = prefetched_body
+    end
 
     return nil if atom_url.nil?
     process_atom(atom_url, body)
diff --git a/app/services/fetch_remote_resource_service.rb b/app/services/fetch_remote_resource_service.rb
new file mode 100644
index 000000000..2185ceb20
--- /dev/null
+++ b/app/services/fetch_remote_resource_service.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class FetchRemoteResourceService < BaseService
+  def call(url)
+    atom_url, body = FetchAtomService.new.call(url)
+
+    return nil if atom_url.nil?
+
+    xml = Nokogiri::XML(body)
+    xml.encoding = 'utf-8'
+
+    if xml.root.name == 'feed'
+      FetchRemoteAccountService.new.call(atom_url, body)
+    elsif xml.root.name == 'entry'
+      FetchRemoteStatusService.new.call(atom_url, body)
+    end
+  end
+end
diff --git a/app/services/fetch_remote_status_service.rb b/app/services/fetch_remote_status_service.rb
index 7063231e4..e2d185723 100644
--- a/app/services/fetch_remote_status_service.rb
+++ b/app/services/fetch_remote_status_service.rb
@@ -1,8 +1,13 @@
 # frozen_string_literal: true
 
 class FetchRemoteStatusService < BaseService
-  def call(url)
-    atom_url, body = FetchAtomService.new.call(url)
+  def call(url, prefetched_body = nil)
+    if prefetched_body.nil?
+      atom_url, body = FetchAtomService.new.call(url)
+    else
+      atom_url = url
+      body     = prefetched_body
+    end
 
     return nil if atom_url.nil?
     process_atom(atom_url, body)
diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb
index d67b1bf2d..17b3b2542 100644
--- a/app/services/follow_service.rb
+++ b/app/services/follow_service.rb
@@ -10,7 +10,7 @@ class FollowService < BaseService
     target_account = FollowRemoteAccountService.new.call(uri)
 
     raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended?
-    raise Mastodon::NotPermitted       if target_account.blocking?(source_account) || source_account.blocking?(target_account)
+    raise Mastodon::NotPermittedError       if target_account.blocking?(source_account) || source_account.blocking?(target_account)
 
     if target_account.locked?
       request_follow(source_account, target_account)
@@ -55,10 +55,13 @@ class FollowService < BaseService
   end
 
   def build_follow_request_xml(follow_request)
+    description = "#{follow_request.account.acct} requested to follow #{follow_request.target_account.acct}"
+
     Nokogiri::XML::Builder.new do |xml|
       entry(xml, true) do
         unique_id xml, follow_request.created_at, follow_request.id, 'FollowRequest'
-        title xml, "#{follow_request.account.acct} requested to follow #{follow_request.target_account.acct}"
+        title xml, description
+        content xml, description
 
         author(xml) do
           include_author xml, follow_request.account
@@ -75,10 +78,13 @@ class FollowService < BaseService
   end
 
   def build_follow_xml(follow)
+    description = "#{follow.account.acct} started following #{follow.target_account.acct}"
+
     Nokogiri::XML::Builder.new do |xml|
       entry(xml, true) do
         unique_id xml, follow.created_at, follow.id, 'Follow'
-        title xml, "#{follow.account.acct} started following #{follow.target_account.acct}"
+        title xml, description
+        content xml, description
 
         author(xml) do
           include_author xml, follow.account
diff --git a/app/services/mute_service.rb b/app/services/mute_service.rb
new file mode 100644
index 000000000..0050cfc8d
--- /dev/null
+++ b/app/services/mute_service.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class MuteService < BaseService
+  def call(account, target_account)
+    return if account.id == target_account.id
+    clear_home_timeline(account, target_account)
+    account.mute!(target_account)
+  end
+
+  private
+
+  def clear_home_timeline(account, target_account)
+    home_key = FeedManager.instance.key(:home, account.id)
+
+    target_account.statuses.select('id').find_each do |status|
+      redis.zrem(home_key, status.id)
+    end
+  end
+
+  def redis
+    Redis.current
+  end
+end
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index 979941c84..b8179f7dc 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -13,6 +13,7 @@ class PostStatusService < BaseService
   # @option [Doorkeeper::Application] :application
   # @return [Status]
   def call(account, text, in_reply_to = nil, options = {})
+    media  = validate_media!(options[:media_ids])
     status = account.statuses.create!(text: text,
                                       thread: in_reply_to,
                                       sensitive: options[:sensitive],
@@ -20,7 +21,7 @@ class PostStatusService < BaseService
                                       visibility: options[:visibility],
                                       application: options[:application])
 
-    attach_media(status, options[:media_ids])
+    attach_media(status, media)
     process_mentions_service.call(status)
     process_hashtags_service.call(status)
 
@@ -33,10 +34,20 @@ class PostStatusService < BaseService
 
   private
 
-  def attach_media(status, media_ids)
+  def validate_media!(media_ids)
     return if media_ids.nil? || !media_ids.is_a?(Enumerable)
 
+    raise Mastodon::ValidationError, 'Cannot attach more than 4 files' if media_ids.size > 4
+
     media = MediaAttachment.where(status_id: nil).where(id: media_ids.take(4).map(&:to_i))
+
+    raise Mastodon::ValidationError, 'Cannot attach a video to a toot that already contains images' if media.size > 1 && media.find(&:video?)
+
+    media
+  end
+
+  def attach_media(status, media)
+    return if media.nil?
     media.update(status_id: status.id)
   end
 
diff --git a/app/services/precompute_feed_service.rb b/app/services/precompute_feed_service.rb
index 54d11b631..e1ec56e8d 100644
--- a/app/services/precompute_feed_service.rb
+++ b/app/services/precompute_feed_service.rb
@@ -4,10 +4,10 @@ class PrecomputeFeedService < BaseService
   # Fill up a user's home/mentions feed from DB and return a subset
   # @param [Symbol] type :home or :mentions
   # @param [Account] account
-  def call(type, account)
-    Status.send("as_#{type}_timeline", account).limit(FeedManager::MAX_ITEMS).each do |status|
-      next if FeedManager.instance.filter?(type, status, account)
-      redis.zadd(FeedManager.instance.key(type, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id)
+  def call(_, account)
+    Status.as_home_timeline(account).limit(FeedManager::MAX_ITEMS).each do |status|
+      next if status.direct_visibility? || FeedManager.instance.filter?(:home, status, account)
+      redis.zadd(FeedManager.instance.key(:home, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id)
     end
   end
 
diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb
index f0a62aa14..69911abc5 100644
--- a/app/services/process_feed_service.rb
+++ b/app/services/process_feed_service.rb
@@ -61,12 +61,25 @@ class ProcessFeedService < BaseService
 
       status.save!
 
-      NotifyService.new.call(status.reblog.account, status) if status.reblog? && status.reblog.account.local?
+      notify_about_mentions!(status) unless status.reblog?
+      notify_about_reblog!(status) if status.reblog? && status.reblog.account.local?
       Rails.logger.debug "Queuing remote status #{status.id} (#{id}) for distribution"
       DistributionWorker.perform_async(status.id)
       status
     end
 
+    def notify_about_mentions!(status)
+      status.mentions.includes(:account).each do |mention|
+        mentioned_account = mention.account
+        next unless mentioned_account.local?
+        NotifyService.new.call(mentioned_account, mention)
+      end
+    end
+
+    def notify_about_reblog!(status)
+      NotifyService.new.call(status.reblog.account, status)
+    end
+
     def delete_status
       Rails.logger.debug "Deleting remote status #{id}"
       status = Status.find_by(uri: id)
@@ -159,10 +172,7 @@ class ProcessFeedService < BaseService
 
         next if mentioned_account.nil? || processed_account_ids.include?(mentioned_account.id)
 
-        mention = mentioned_account.mentions.where(status: parent).first_or_create(status: parent)
-
-        # Notify local user
-        NotifyService.new.call(mentioned_account, mention) if mentioned_account.local?
+        mentioned_account.mentions.where(status: parent).first_or_create(status: parent)
 
         # So we can skip duplicate mentions
         processed_account_ids << mentioned_account.id
@@ -181,6 +191,9 @@ class ProcessFeedService < BaseService
         next unless link['href']
 
         media = MediaAttachment.where(status: parent, remote_url: link['href']).first_or_initialize(account: parent.account, status: parent, remote_url: link['href'])
+        parsed_url = URI.parse(link['href'])
+
+        next if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.empty?
 
         begin
           media.file_remote_url = link['href']
diff --git a/app/services/process_interaction_service.rb b/app/services/process_interaction_service.rb
index c74ff9e22..d5f7b4b3c 100644
--- a/app/services/process_interaction_service.rb
+++ b/app/services/process_interaction_service.rb
@@ -64,7 +64,7 @@ class ProcessInteractionService < BaseService
   end
 
   def mentions_account?(xml, account)
-    xml.xpath('/xmlns:entry/xmlns:link[@rel="mentioned"]', xmlns: TagManager::XMLNS).each { |mention_link| return true if mention_link.attribute('href').value == TagManager.instance.url_for(account) }
+    xml.xpath('/xmlns:entry/xmlns:link[@rel="mentioned"]', xmlns: TagManager::XMLNS).each { |mention_link| return true if [TagManager.instance.uri_for(account), TagManager.instance.url_for(account)].include?(mention_link.attribute('href').value) }
     false
   end
 
diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb
index d3d3af8af..aa0a4d71b 100644
--- a/app/services/process_mentions_service.rb
+++ b/app/services/process_mentions_service.rb
@@ -27,7 +27,7 @@ class ProcessMentionsService < BaseService
       mentioned_account.mentions.where(status: status).first_or_create(status: status)
     end
 
-    status.mentions.each do |mention|
+    status.mentions.includes(:account).each do |mention|
       mentioned_account = mention.account
 
       if mentioned_account.local?
diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb
index 7a52f041f..11446ce28 100644
--- a/app/services/reblog_service.rb
+++ b/app/services/reblog_service.rb
@@ -10,7 +10,7 @@ class ReblogService < BaseService
   def call(account, reblogged_status)
     reblogged_status = reblogged_status.reblog if reblogged_status.reblog?
 
-    raise Mastodon::NotPermitted if reblogged_status.private_visibility? || !reblogged_status.permitted?(account)
+    raise Mastodon::NotPermittedError if reblogged_status.direct_visibility? || reblogged_status.private_visibility? || !reblogged_status.permitted?(account)
 
     reblog = account.statuses.create!(reblog: reblogged_status, text: '')
 
diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb
index 73b545f17..cf1f432e4 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -59,7 +59,7 @@ class RemoveStatusService < BaseService
   end
 
   def unpush(type, receiver, status)
-    if status.reblog?
+    if status.reblog? && !redis.zscore(FeedManager.instance.key(type, receiver.id), status.reblog_of_id).nil?
       redis.zadd(FeedManager.instance.key(type, receiver.id), status.reblog_of_id, status.reblog_of_id)
     else
       redis.zremrangebyscore(FeedManager.instance.key(type, receiver.id), status.id, status.id)
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index 04de8a134..e9745010b 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -1,24 +1,19 @@
 # frozen_string_literal: true
 
 class SearchService < BaseService
-  def call(query, limit, resolve = false)
-    return if query.blank? || query.start_with?('#')
+  def call(query, limit, resolve = false, account = nil)
+    results = { accounts: [], hashtags: [], statuses: [] }
 
-    username, domain = query.gsub(/\A@/, '').split('@')
+    return results if query.blank?
 
-    if domain.nil?
-      exact_match = Account.find_local(username)
-      results     = Account.search_for(username)
-    else
-      exact_match = Account.find_remote(username, domain)
-      results     = Account.search_for("#{username} #{domain}")
-    end
+    if query =~ /\Ahttps?:\/\//
+      resource = FetchRemoteResourceService.new.call(query)
 
-    results = results.limit(limit).to_a
-    results = [exact_match] + results.reject { |a| a.id == exact_match.id } if exact_match
-
-    if resolve && !exact_match && !domain.nil?
-      results = [FollowRemoteAccountService.new.call("#{username}@#{domain}")]
+      results[:accounts] << resource if resource.is_a?(Account)
+      results[:statuses] << resource if resource.is_a?(Status)
+    else
+      results[:accounts] = AccountSearchService.new.call(query, limit, resolve, account)
+      results[:hashtags] = Tag.search_for(query.gsub(/\A#/, ''), limit) unless query.start_with?('@')
     end
 
     results
diff --git a/app/services/unfavourite_service.rb b/app/services/unfavourite_service.rb
index 1d3e6f06d..5f0ba4254 100644
--- a/app/services/unfavourite_service.rb
+++ b/app/services/unfavourite_service.rb
@@ -13,10 +13,13 @@ class UnfavouriteService < BaseService
   private
 
   def build_xml(favourite)
+    description = "#{favourite.account.acct} no longer favourites a status by #{favourite.status.account.acct}"
+
     Nokogiri::XML::Builder.new do |xml|
       entry(xml, true) do
         unique_id xml, Time.now.utc, favourite.id, 'Favourite'
-        title xml, "#{favourite.account.acct} no longer favourites a status by #{favourite.status.account.acct}"
+        title xml, description
+        content xml, description
 
         author(xml) do
           include_author xml, favourite.account
diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb
index 07f9b93dd..3440da364 100644
--- a/app/services/unfollow_service.rb
+++ b/app/services/unfollow_service.rb
@@ -13,10 +13,13 @@ class UnfollowService < BaseService
   private
 
   def build_xml(follow)
+    description = "#{follow.account.acct} is no longer following #{follow.target_account.acct}"
+
     Nokogiri::XML::Builder.new do |xml|
       entry(xml, true) do
         unique_id xml, Time.now.utc, follow.id, 'Follow'
-        title xml, "#{follow.account.acct} is no longer following #{follow.target_account.acct}"
+        title xml, description
+        content xml, description
 
         author(xml) do
           include_author xml, follow.account
diff --git a/app/services/unmute_service.rb b/app/services/unmute_service.rb
new file mode 100644
index 000000000..6aeea358f
--- /dev/null
+++ b/app/services/unmute_service.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class UnmuteService < BaseService
+  def call(account, target_account)
+    return unless account.muting?(target_account)
+
+    account.unmute!(target_account)
+
+    MergeWorker.perform_async(target_account.id, account.id) if account.following?(target_account)
+  end
+end
diff --git a/app/services/update_remote_profile_service.rb b/app/services/update_remote_profile_service.rb
index dc315db19..74baa1cc5 100644
--- a/app/services/update_remote_profile_service.rb
+++ b/app/services/update_remote_profile_service.rb
@@ -14,6 +14,7 @@ class UpdateRemoteProfileService < BaseService
 
       unless account.suspended? || DomainBlock.find_by(domain: account.domain)&.reject_media?
         account.avatar_remote_url = author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS)['href'] unless author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS).nil? || author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS)['href'].blank?
+        account.header_remote_url = author_xml.at_xpath('./xmlns:link[@rel="header"]', xmlns: TagManager::XMLNS)['href'] unless author_xml.at_xpath('./xmlns:link[@rel="header"]', xmlns: TagManager::XMLNS).nil? || author_xml.at_xpath('./xmlns:link[@rel="header"]', xmlns: TagManager::XMLNS)['href'].blank?
       end
     end