diff options
Diffstat (limited to 'app/services')
30 files changed, 509 insertions, 111 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/authorize_follow_service.rb b/app/services/authorize_follow_service.rb new file mode 100644 index 000000000..ac465bdb2 --- /dev/null +++ b/app/services/authorize_follow_service.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class AuthorizeFollowService < BaseService + def call(source_account, target_account) + follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account) + follow_request.authorize! + NotificationWorker.perform_async(build_xml(follow_request), target_account.id, source_account.id) unless source_account.local? + end + + private + + def build_xml(follow_request) + Nokogiri::XML::Builder.new do |xml| + entry(xml, true) do + unique_id xml, Time.now.utc, follow_request.id, 'FollowRequest' + title xml, "#{follow_request.target_account.acct} authorizes follow request by #{follow_request.account.acct}" + + author(xml) do + include_author xml, follow_request.target_account + end + + object_type xml, :activity + verb xml, :authorize + + target(xml) do + author(xml) do + include_author xml, follow_request.account + end + + object_type xml, :activity + verb xml, :request_friend + + target(xml) do + include_author xml, follow_request.target_account + end + end + end + end.to_xml + end +end diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb index 9518b1fcf..6c131bd34 100644 --- a/app/services/block_domain_service.rb +++ b/app/services/block_domain_service.rb @@ -1,13 +1,11 @@ # frozen_string_literal: true class BlockDomainService < BaseService - def call(domain, severity) - DomainBlock.where(domain: domain).first_or_create!(domain: domain, severity: severity) - - if severity == :silence - Account.where(domain: domain).update_all(silenced: true) + def call(domain_block) + if domain_block.silence? + Account.where(domain: domain_block.domain).update_all(silenced: true) else - Account.where(domain: domain).find_each do |account| + Account.where(domain: domain_block.domain).find_each do |account| account.subscription(api_subscription_url(account.id)).unsubscribe if account.subscribed? SuspendAccountService.new.call(account) end diff --git a/app/services/block_service.rb b/app/services/block_service.rb index e04b6cc39..bd914d8be 100644 --- a/app/services/block_service.rb +++ b/app/services/block_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class BlockService < BaseService + include StreamEntryRenderer + def call(account, target_account) return if account.id == target_account.id @@ -10,6 +12,28 @@ class BlockService < BaseService block = account.block!(target_account) BlockWorker.perform_async(account.id, target_account.id) - NotificationWorker.perform_async(block.stream_entry.id, target_account.id) unless target_account.local? + NotificationWorker.perform_async(build_xml(block), account.id, target_account.id) unless target_account.local? + end + + private + + def build_xml(block) + Nokogiri::XML::Builder.new do |xml| + entry(xml, true) do + unique_id xml, block.created_at, block.id, 'Block' + title xml, "#{block.account.acct} no longer wishes to interact with #{block.target_account.acct}" + + author(xml) do + include_author xml, block.account + end + + object_type xml, :activity + verb xml, :block + + target(xml) do + include_author xml, block.target_account + end + end + end.to_xml end end diff --git a/app/services/concerns/stream_entry_renderer.rb b/app/services/concerns/stream_entry_renderer.rb new file mode 100644 index 000000000..a4255daea --- /dev/null +++ b/app/services/concerns/stream_entry_renderer.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module StreamEntryRenderer + def stream_entry_to_xml(stream_entry) + renderer = StreamEntriesController.renderer.new(method: 'get', http_host: Rails.configuration.x.local_domain, https: Rails.configuration.x.use_https) + renderer.render(:show, assigns: { stream_entry: stream_entry }, formats: [:atom]) + end +end diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 71f6cbca1..42222c25b 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -4,8 +4,15 @@ class FanOutOnWriteService < BaseService # Push a status into home and mentions feeds # @param [Status] status def call(status) + raise Mastodon::RaceConditionError if status.visibility.nil? + 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? @@ -26,9 +33,18 @@ class FanOutOnWriteService < BaseService def deliver_to_followers(status) Rails.logger.debug "Delivering status #{status.id} to followers" - status.account.followers.where(domain: nil).joins(:user).where('users.current_sign_in_at > ?', 14.days.ago).find_each do |follower| - next if FeedManager.instance.filter?(:home, status, follower) - FeedManager.instance.push(:home, follower, status) + status.account.followers.where(domain: nil).joins(:user).where('users.current_sign_in_at > ?', 14.days.ago).select(:id).find_each do |follower| + FeedInsertWorker.perform_async(status.id, follower.id) + 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, mention.account_id) + FeedManager.instance.push(:home, mentioned_account, status) end end @@ -37,9 +53,9 @@ class FanOutOnWriteService < BaseService payload = FeedManager.instance.inline_render(nil, 'api/v1/statuses/show', status) - status.tags.find_each do |tag| - FeedManager.instance.broadcast("hashtag:#{tag.name}", event: 'update', payload: payload) - FeedManager.instance.broadcast("hashtag:#{tag.name}:local", event: 'update', payload: payload) if status.account.local? + status.tags.pluck(:name).each do |hashtag| + FeedManager.instance.broadcast("hashtag:#{hashtag}", event: 'update', payload: payload) + FeedManager.instance.broadcast("hashtag:#{hashtag}:local", event: 'update', payload: payload) if status.account.local? end end diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb index d5fbd29e9..5cc96403c 100644 --- a/app/services/favourite_service.rb +++ b/app/services/favourite_service.rb @@ -6,18 +6,42 @@ 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) - Pubsubhubbub::DistributionWorker.perform_async(favourite.stream_entry.id) - if status.local? NotifyService.new.call(favourite.status.account, favourite) else - NotificationWorker.perform_async(favourite.stream_entry.id, status.account_id) + NotificationWorker.perform_async(build_xml(favourite), account.id, status.account_id) end favourite end + + 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, description + content xml, description + + author(xml) do + include_author xml, favourite.account + end + + object_type xml, :activity + verb xml, :favorite + in_reply_to xml, TagManager.instance.uri_for(favourite.status), TagManager.instance.url_for(favourite.status) + + target(xml) do + include_target xml, favourite.status + end + end + end.to_xml + end end diff --git a/app/services/fetch_atom_service.rb b/app/services/fetch_atom_service.rb index 98ee1db84..c3dad1eb9 100644 --- a/app/services/fetch_atom_service.rb +++ b/app/services/fetch_atom_service.rb @@ -2,6 +2,8 @@ class FetchAtomService < BaseService def call(url) + return if url.blank? + response = http_client.head(url) Rails.logger.debug "Remote status HEAD request returned code #{response.code}" @@ -45,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 3c3694a65..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) @@ -22,7 +27,9 @@ class FetchRemoteAccountService < BaseService Rails.logger.debug "Going to webfinger #{username}@#{domain}" - return FollowRemoteAccountService.new.call("#{username}@#{domain}") + account = FollowRemoteAccountService.new.call("#{username}@#{domain}") + UpdateRemoteProfileService.new.call(xml, account) unless account.nil? + account rescue TypeError Rails.logger.debug "Unparseable URL given: #{url}" nil 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 9f34cb6ac..17b3b2542 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -1,14 +1,16 @@ # frozen_string_literal: true class FollowService < BaseService + include StreamEntryRenderer + # Follow a remote user, notify remote user about the follow # @param [Account] source_account From which to follow # @param [String] uri User URI to follow in the form of username@domain def call(source_account, uri) - target_account = follow_remote_account_service.call(uri) + 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) @@ -20,10 +22,14 @@ class FollowService < BaseService private def request_follow(source_account, target_account) - return unless target_account.local? - follow_request = FollowRequest.create!(account: source_account, target_account: target_account) - NotifyService.new.call(target_account, follow_request) + + if target_account.local? + NotifyService.new.call(target_account, follow_request) + else + NotificationWorker.perform_async(build_follow_request_xml(follow_request), source_account.id, target_account.id) + AfterRemoteFollowRequestWorker.perform_async(follow_request.id) + end follow_request end @@ -34,12 +40,12 @@ class FollowService < BaseService if target_account.local? NotifyService.new.call(target_account, follow) else - subscribe_service.call(target_account) - NotificationWorker.perform_async(follow.stream_entry.id, target_account.id) + SubscribeService.new.call(target_account) unless target_account.subscribed? + NotificationWorker.perform_async(build_follow_xml(follow), source_account.id, target_account.id) + AfterRemoteFollowWorker.perform_async(follow.id) end MergeWorker.perform_async(target_account.id, source_account.id) - Pubsubhubbub::DistributionWorker.perform_async(follow.stream_entry.id) follow end @@ -48,11 +54,49 @@ class FollowService < BaseService Redis.current end - def follow_remote_account_service - @follow_remote_account_service ||= FollowRemoteAccountService.new + 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, description + content xml, description + + author(xml) do + include_author xml, follow_request.account + end + + object_type xml, :activity + verb xml, :request_friend + + target(xml) do + include_author xml, follow_request.target_account + end + end + end.to_xml end - def subscribe_service - @subscribe_service ||= SubscribeService.new + 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, description + content xml, description + + author(xml) do + include_author xml, follow.account + end + + object_type xml, :activity + verb xml, :follow + + target(xml) do + include_author xml, follow.target_account + end + end + end.to_xml end end 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/notify_service.rb b/app/services/notify_service.rb index 942cd9d21..24486f220 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -17,7 +17,7 @@ class NotifyService < BaseService private def blocked_mention? - FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient) + FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient.id) end def blocked_favourite? 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..07dcb81da 100644 --- a/app/services/precompute_feed_service.rb +++ b/app/services/precompute_feed_service.rb @@ -4,10 +4,12 @@ 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) + redis.pipelined do + Status.as_home_timeline(account).limit(FeedManager::MAX_ITEMS / 4).each do |status| + next if status.direct_visibility? || FeedManager.instance.filter?(:home, status, account.id) + redis.zadd(FeedManager.instance.key(:home, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id) + end end end diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb index c411e3e82..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) @@ -106,7 +119,8 @@ class ProcessFeedService < BaseService text: content(entry), spoiler_text: content_warning(entry), created_at: published(entry), - reply: thread?(entry) + reply: thread?(entry), + visibility: visibility_scope(entry) ) if thread?(entry) @@ -144,15 +158,9 @@ class ProcessFeedService < BaseService def mentions_from_xml(parent, xml) processed_account_ids = [] - public_visibility = false xml.xpath('./xmlns:link[@rel="mentioned"]', xmlns: TagManager::XMLNS).each do |link| - if link['ostatus:object-type'] == TagManager::TYPES[:collection] && link['href'] == TagManager::COLLECTIONS[:public] - public_visibility = true - next - elsif link['ostatus:object-type'] == TagManager::TYPES[:group] - next - end + next if [TagManager::TYPES[:group], TagManager::TYPES[:collection]].include? link['ostatus:object-type'] url = Addressable::URI.parse(link['href']) @@ -164,17 +172,11 @@ 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 end - - parent.visibility = public_visibility ? :public : :unlisted - parent.save! end def hashtags_from_xml(parent, xml) @@ -189,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'] @@ -230,6 +235,10 @@ class ProcessFeedService < BaseService xml.at_xpath('./xmlns:summary', xmlns: TagManager::XMLNS)&.content || '' end + def visibility_scope(xml = @xml) + xml.at_xpath('./mastodon:scope', mastodon: TagManager::MTDN_XMLNS)&.content&.to_sym || :public + end + def published(xml = @xml) xml.at_xpath('./xmlns:published', xmlns: TagManager::XMLNS).content end diff --git a/app/services/process_interaction_service.rb b/app/services/process_interaction_service.rb index 5f91e3127..d5f7b4b3c 100644 --- a/app/services/process_interaction_service.rb +++ b/app/services/process_interaction_service.rb @@ -29,10 +29,18 @@ class ProcessInteractionService < BaseService case verb(xml) when :follow follow!(account, target_account) unless target_account.locked? || target_account.blocking?(account) + when :request_friend + follow_request!(account, target_account) unless !target_account.locked? || target_account.blocking?(account) + when :authorize + authorize_follow_request!(account, target_account) + when :reject + reject_follow_request!(account, target_account) when :unfollow unfollow!(account, target_account) when :favorite favourite!(xml, account) + when :unfavorite + unfavourite!(xml, account) when :post add_post!(body, account) if mentions_account?(xml, target_account) when :share @@ -56,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 @@ -72,6 +80,22 @@ class ProcessInteractionService < BaseService NotifyService.new.call(target_account, follow) end + def follow_request!(account, target_account) + follow_request = FollowRequest.create!(account: account, target_account: target_account) + NotifyService.new.call(target_account, follow_request) + end + + def authorize_follow_request!(account, target_account) + follow_request = FollowRequest.find_by(account: target_account, target_account: account) + follow_request&.authorize! + SubscribeService.new.call(account) unless account.subscribed? + end + + def reject_follow_request!(account, target_account) + follow_request = FollowRequest.find_by(account: target_account, target_account: account) + follow_request&.reject! + end + def unfollow!(account, target_account) account.unfollow!(target_account) end @@ -99,6 +123,12 @@ class ProcessInteractionService < BaseService NotifyService.new.call(current_status.account, favourite) end + def unfavourite!(xml, from_account) + current_status = status(xml) + favourite = current_status.favourites.where(account: from_account).first + favourite&.destroy + end + def add_post!(body, account) process_feed_service.call(body, account) end diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index 72568e702..aa0a4d71b 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ProcessMentionsService < BaseService + include StreamEntryRenderer + # Scan status for mentions and fetch remote mentioned users, create # local mention pointers, send Salmon notifications to mentioned # remote users @@ -25,15 +27,13 @@ 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 - next if status.private_visibility? && (!mentioned_account.following?(status.account) || !mentioned_account.local?) - if mentioned_account.local? NotifyService.new.call(mentioned_account, mention) else - NotificationWorker.perform_async(status.stream_entry.id, mentioned_account.id) + NotificationWorker.perform_async(stream_entry_to_xml(status.stream_entry), status.account_id, mentioned_account.id) end end end diff --git a/app/services/pubsubhubbub/subscribe_service.rb b/app/services/pubsubhubbub/subscribe_service.rb index 343376d77..bf36e3fa6 100644 --- a/app/services/pubsubhubbub/subscribe_service.rb +++ b/app/services/pubsubhubbub/subscribe_service.rb @@ -2,8 +2,9 @@ class Pubsubhubbub::SubscribeService < BaseService def call(account, callback, secret, lease_seconds) - return ['Invalid topic URL', 422] if account.nil? - return ['Invalid callback URL', 422] unless !callback.blank? && callback =~ /\A#{URI.regexp(%w(http https))}\z/ + return ['Invalid topic URL', 422] if account.nil? + return ['Invalid callback URL', 422] unless !callback.blank? && callback =~ /\A#{URI.regexp(%w(http https))}\z/ + return ['Callback URL not allowed', 403] if DomainBlock.blocked?(Addressable::URI.parse(callback).host) subscription = Subscription.where(account: account, callback_url: callback).first_or_create!(account: account, callback_url: callback) Pubsubhubbub::ConfirmationWorker.perform_async(subscription.id, 'subscribe', secret, lease_seconds) diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb index 4ea0dbf6c..11446ce28 100644 --- a/app/services/reblog_service.rb +++ b/app/services/reblog_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ReblogService < BaseService + include StreamEntryRenderer + # Reblog a status and notify its remote author # @param [Account] account Account to reblog from # @param [Status] reblogged_status Status to be reblogged @@ -8,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: '') @@ -18,15 +20,9 @@ class ReblogService < BaseService if reblogged_status.local? NotifyService.new.call(reblog.reblog.account, reblog) else - NotificationWorker.perform_async(reblog.stream_entry.id, reblog.reblog.account_id) + NotificationWorker.perform_async(stream_entry_to_xml(reblog.stream_entry), account.id, reblog.reblog.account_id) end reblog end - - private - - def send_interaction_service - @send_interaction_service ||= SendInteractionService.new - end end diff --git a/app/services/reject_follow_service.rb b/app/services/reject_follow_service.rb new file mode 100644 index 000000000..1b03d62e6 --- /dev/null +++ b/app/services/reject_follow_service.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class RejectFollowService < BaseService + def call(source_account, target_account) + follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account) + follow_request.reject! + NotificationWorker.perform_async(build_xml(follow_request), target_account.id, source_account.id) unless source_account.local? + end + + private + + def build_xml(follow_request) + Nokogiri::XML::Builder.new do |xml| + entry(xml, true) do + unique_id xml, Time.now.utc, follow_request.id, 'FollowRequest' + title xml, "#{follow_request.target_account.acct} rejects follow request by #{follow_request.account.acct}" + + author(xml) do + include_author xml, follow_request.target_account + end + + object_type xml, :activity + verb xml, :reject + + target(xml) do + author(xml) do + include_author xml, follow_request.account + end + + object_type xml, :activity + verb xml, :request_friend + + target(xml) do + include_author xml, follow_request.target_account + end + end + end + end.to_xml + end +end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 48e8dd3b8..cf1f432e4 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class RemoveStatusService < BaseService + include StreamEntryRenderer + def call(status) remove_from_self(status) if status.account.local? remove_from_followers(status) @@ -30,12 +32,16 @@ class RemoveStatusService < BaseService end def remove_from_mentioned(status) + notified_domains = [] + status.mentions.each do |mention| mentioned_account = mention.account if mentioned_account.local? unpush(:mentions, mentioned_account, status) else + next if notified_domains.include?(mentioned_account.domain) + notified_domains << mentioned_account.domain send_delete_salmon(mentioned_account, status) end end @@ -43,7 +49,7 @@ class RemoveStatusService < BaseService def send_delete_salmon(account, status) return unless status.local? - NotificationWorker.perform_async(status.stream_entry.id, account.id) + NotificationWorker.perform_async(stream_entry_to_xml(status.stream_entry), status.account_id, account.id) end def remove_reblogs(status) @@ -53,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/send_interaction_service.rb b/app/services/send_interaction_service.rb index 05a1e77e3..99113eeca 100644 --- a/app/services/send_interaction_service.rb +++ b/app/services/send_interaction_service.rb @@ -2,27 +2,16 @@ class SendInteractionService < BaseService # Send an Atom representation of an interaction to a remote Salmon endpoint - # @param [StreamEntry] stream_entry + # @param [String] Entry XML + # @param [Account] source_account # @param [Account] target_account - def call(stream_entry, target_account) - envelope = salmon.pack(entry_xml(stream_entry), stream_entry.account.keypair) + def call(xml, source_account, target_account) + envelope = salmon.pack(xml, source_account.keypair) salmon.post(target_account.salmon_url, envelope) end private - def entry_xml(stream_entry) - Nokogiri::XML::Builder.new do |xml| - entry(xml, true) do - author(xml) do - include_author xml, stream_entry.account - end - - include_entry xml, stream_entry - end - end.to_xml - end - def salmon @salmon ||= OStatus2::Salmon.new end diff --git a/app/services/unblock_service.rb b/app/services/unblock_service.rb index f389364f9..c4f789f74 100644 --- a/app/services/unblock_service.rb +++ b/app/services/unblock_service.rb @@ -5,6 +5,28 @@ class UnblockService < BaseService return unless account.blocking?(target_account) unblock = account.unblock!(target_account) - NotificationWorker.perform_async(unblock.stream_entry.id, target_account.id) unless target_account.local? + NotificationWorker.perform_async(build_xml(unblock), account.id, target_account.id) unless target_account.local? + end + + private + + def build_xml(block) + Nokogiri::XML::Builder.new do |xml| + entry(xml, true) do + unique_id xml, Time.now.utc, block.id, 'Block' + title xml, "#{block.account.acct} no longer blocks #{block.target_account.acct}" + + author(xml) do + include_author xml, block.account + end + + object_type xml, :activity + verb xml, :unblock + + target(xml) do + include_author xml, block.target_account + end + end + end.to_xml end end diff --git a/app/services/unfavourite_service.rb b/app/services/unfavourite_service.rb index de6e84e7d..5f0ba4254 100644 --- a/app/services/unfavourite_service.rb +++ b/app/services/unfavourite_service.rb @@ -5,10 +5,34 @@ class UnfavouriteService < BaseService favourite = Favourite.find_by!(account: account, status: status) favourite.destroy! - unless status.local? - NotificationWorker.perform_async(favourite.stream_entry.id, status.account_id) - end + NotificationWorker.perform_async(build_xml(favourite), account.id, status.account_id) unless status.local? favourite end + + 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, description + content xml, description + + author(xml) do + include_author xml, favourite.account + end + + object_type xml, :activity + verb xml, :unfavorite + in_reply_to xml, TagManager.instance.uri_for(favourite.status), TagManager.instance.url_for(favourite.status) + + target(xml) do + include_target xml, favourite.status + end + end + end.to_xml + end end diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb index f469793c1..3440da364 100644 --- a/app/services/unfollow_service.rb +++ b/app/services/unfollow_service.rb @@ -6,7 +6,32 @@ class UnfollowService < BaseService # @param [Account] target_account Which to unfollow def call(source_account, target_account) follow = source_account.unfollow!(target_account) - NotificationWorker.perform_async(follow.stream_entry.id, target_account.id) unless target_account.local? + NotificationWorker.perform_async(build_xml(follow), source_account.id, target_account.id) unless target_account.local? UnmergeWorker.perform_async(target_account.id, source_account.id) end + + 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, description + content xml, description + + author(xml) do + include_author xml, follow.account + end + + object_type xml, :activity + verb xml, :unfollow + + target(xml) do + include_author xml, follow.target_account + end + end + end.to_xml + end end 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 ad9c56540..74baa1cc5 100644 --- a/app/services/update_remote_profile_service.rb +++ b/app/services/update_remote_profile_service.rb @@ -10,9 +10,11 @@ class UpdateRemoteProfileService < BaseService unless author_xml.nil? account.display_name = author_xml.at_xpath('./poco:displayName', poco: TagManager::POCO_XMLNS).content unless author_xml.at_xpath('./poco:displayName', poco: TagManager::POCO_XMLNS).nil? account.note = author_xml.at_xpath('./poco:note', poco: TagManager::POCO_XMLNS).content unless author_xml.at_xpath('./poco:note', poco: TagManager::POCO_XMLNS).nil? + account.locked = author_xml.at_xpath('./mastodon:scope', mastodon: TagManager::MTDN_XMLNS)&.content == 'private' 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 |