Make follow requests federate
This commit is contained in:
		
							parent
							
								
									d551e43a9b
								
							
						
					
					
						commit
						149887a0ff
					
				
					 25 changed files with 148 additions and 61 deletions
				
			
		|  | @ -18,12 +18,12 @@ class Api::V1::FollowRequestsController < ApiController | |||
|   end | ||||
| 
 | ||||
|   def authorize | ||||
|     FollowRequest.find_by!(account_id: params[:id], target_account: current_account).authorize! | ||||
|     AuthorizeFollowService.new.call(Account.find(params[:id]), current_account) | ||||
|     render_empty | ||||
|   end | ||||
| 
 | ||||
|   def reject | ||||
|     FollowRequest.find_by!(account_id: params[:id], target_account: current_account).reject! | ||||
|     RejectFollowService.new.call(Account.find(params[:id]), current_account) | ||||
|     render_empty | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module ObfuscateFilename | ||||
|   extend ActiveSupport::Concern | ||||
| 
 | ||||
|  |  | |||
|  | @ -143,6 +143,10 @@ module AtomBuilderHelper | |||
|     xml.link(:rel => 'mentioned', :href => TagManager::COLLECTIONS[:public], 'ostatus:object-type' => TagManager::TYPES[:collection]) | ||||
|   end | ||||
| 
 | ||||
|   def privacy_scope(xml, level) | ||||
|     xml['mastodon'].scope(level) | ||||
|   end | ||||
| 
 | ||||
|   def include_author(xml, account) | ||||
|     object_type      xml, :person | ||||
|     uri              xml, TagManager.instance.uri_for(account) | ||||
|  | @ -152,6 +156,7 @@ module AtomBuilderHelper | |||
|     link_alternate   xml, TagManager.instance.url_for(account) | ||||
|     link_avatar      xml, account | ||||
|     portable_contact xml, account | ||||
|     privacy_scope    xml, account.locked? ? :private : :public | ||||
|   end | ||||
| 
 | ||||
|   def rich_content(xml, activity) | ||||
|  | @ -216,6 +221,7 @@ module AtomBuilderHelper | |||
|           end | ||||
| 
 | ||||
|           category(xml, 'nsfw') if stream_entry.target.sensitive? | ||||
|           privacy_scope(xml, stream_entry.target.visibility) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|  | @ -237,6 +243,7 @@ module AtomBuilderHelper | |||
|     end | ||||
| 
 | ||||
|     category(xml, 'nsfw') if stream_entry.activity.sensitive? | ||||
|     privacy_scope(xml, stream_entry.activity.visibility) | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
|  | @ -249,6 +256,7 @@ module AtomBuilderHelper | |||
|                'xmlns:poco'     => TagManager::POCO_XMLNS, | ||||
|                'xmlns:media'    => TagManager::MEDIA_XMLNS, | ||||
|                'xmlns:ostatus'  => TagManager::OS_XMLNS, | ||||
|                'xmlns:mastodon' => TagManager::MTDN_XMLNS, | ||||
|              }, &block) | ||||
|   end | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,15 +7,18 @@ class TagManager | |||
|   include RoutingHelper | ||||
| 
 | ||||
|   VERBS = { | ||||
|     post:       'http://activitystrea.ms/schema/1.0/post', | ||||
|     share:      'http://activitystrea.ms/schema/1.0/share', | ||||
|     favorite:   'http://activitystrea.ms/schema/1.0/favorite', | ||||
|     unfavorite: 'http://activitystrea.ms/schema/1.0/unfavorite', | ||||
|     delete:     'http://activitystrea.ms/schema/1.0/delete', | ||||
|     follow:     'http://activitystrea.ms/schema/1.0/follow', | ||||
|     unfollow:   'http://ostatus.org/schema/1.0/unfollow', | ||||
|     block:      'http://mastodon.social/schema/1.0/block', | ||||
|     unblock:    'http://mastodon.social/schema/1.0/unblock', | ||||
|     post:           'http://activitystrea.ms/schema/1.0/post', | ||||
|     share:          'http://activitystrea.ms/schema/1.0/share', | ||||
|     favorite:       'http://activitystrea.ms/schema/1.0/favorite', | ||||
|     unfavorite:     'http://activitystrea.ms/schema/1.0/unfavorite', | ||||
|     delete:         'http://activitystrea.ms/schema/1.0/delete', | ||||
|     follow:         'http://activitystrea.ms/schema/1.0/follow', | ||||
|     request_friend: 'http://activitystrea.ms/schema/1.0/request-friend', | ||||
|     authorize:      'http://activitystrea.ms/schema/1.0/authorize', | ||||
|     reject:         'http://activitystrea.ms/schema/1.0/reject', | ||||
|     unfollow:       'http://ostatus.org/schema/1.0/unfollow', | ||||
|     block:          'http://mastodon.social/schema/1.0/block', | ||||
|     unblock:        'http://mastodon.social/schema/1.0/unblock', | ||||
|   }.freeze | ||||
| 
 | ||||
|   TYPES = { | ||||
|  | @ -38,6 +41,7 @@ class TagManager | |||
|   POCO_XMLNS  = 'http://portablecontacts.net/spec/1.0' | ||||
|   DFRN_XMLNS  = 'http://purl.org/macgirvin/dfrn/1.0' | ||||
|   OS_XMLNS    = 'http://ostatus.org/schema/1.0' | ||||
|   MTDN_XMLNS  = 'http://mastodon.social/schema/1.0' | ||||
| 
 | ||||
|   def unique_tag(date, id, type) | ||||
|     "tag:#{Rails.configuration.x.local_domain},#{date.strftime('%Y-%m-%d')}:objectId=#{id}:objectType=#{type}" | ||||
|  |  | |||
|  | @ -12,11 +12,11 @@ class Favourite < ApplicationRecord | |||
|   validates :status_id, uniqueness: { scope: :account_id } | ||||
| 
 | ||||
|   def verb | ||||
|     :favorite | ||||
|     destroyed? ? :unfavorite : :favorite | ||||
|   end | ||||
| 
 | ||||
|   def title | ||||
|     "#{account.acct} favourited a status by #{status.account.acct}" | ||||
|     destroyed? ? "#{account.acct} no longer favourites a status by #{status.account.acct}" : "#{account.acct} favourited a status by #{status.account.acct}" | ||||
|   end | ||||
| 
 | ||||
|   delegate :object_type, to: :target | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ | |||
| 
 | ||||
| class FollowRequest < ApplicationRecord | ||||
|   include Paginable | ||||
|   include Streamable | ||||
| 
 | ||||
|   belongs_to :account | ||||
|   belongs_to :target_account, class_name: 'Account' | ||||
|  | @ -12,12 +13,47 @@ class FollowRequest < ApplicationRecord | |||
|   validates :account_id, uniqueness: { scope: :target_account_id } | ||||
| 
 | ||||
|   def authorize! | ||||
|     @verb = :authorize | ||||
| 
 | ||||
|     account.follow!(target_account) | ||||
|     MergeWorker.perform_async(target_account.id, account.id) | ||||
| 
 | ||||
|     destroy! | ||||
|   end | ||||
| 
 | ||||
|   def reject! | ||||
|     @verb = :reject | ||||
|     destroy! | ||||
|   end | ||||
| 
 | ||||
|   def verb | ||||
|     destroyed? ? (@verb || :delete) : :request_friend | ||||
|   end | ||||
| 
 | ||||
|   def target | ||||
|     target_account | ||||
|   end | ||||
| 
 | ||||
|   def object_type | ||||
|     :person | ||||
|   end | ||||
| 
 | ||||
|   def hidden? | ||||
|     true | ||||
|   end | ||||
| 
 | ||||
|   def title | ||||
|     if destroyed? | ||||
|       case @verb | ||||
|       when :authorize | ||||
|         "#{target_account.acct} authorized #{account.acct}'s request to follow" | ||||
|       when :reject | ||||
|         "#{target_account.acct} rejected #{account.acct}'s request to follow" | ||||
|       else | ||||
|         "#{account.acct} withdrew the request to follow #{target_account.acct}" | ||||
|       end | ||||
|     else | ||||
|       "#{account.acct} requested to follow #{target_account.acct}" | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -30,7 +30,7 @@ class StreamEntry < ApplicationRecord | |||
|   end | ||||
| 
 | ||||
|   def targeted? | ||||
|     [:follow, :unfollow, :block, :unblock, :share, :favorite].include? verb | ||||
|     [:follow, :request_friend, :authorize, :unfollow, :block, :unblock, :share, :favorite].include? verb | ||||
|   end | ||||
| 
 | ||||
|   def target | ||||
|  |  | |||
							
								
								
									
										11
									
								
								app/services/authorize_follow_service.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/services/authorize_follow_service.rb
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class AuthorizeFollowService < BaseService | ||||
|   include StreamEntryRenderer | ||||
| 
 | ||||
|   def call(source_account, target_account) | ||||
|     follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account) | ||||
|     follow_request.authorize! | ||||
|     NotificationWorker.perform_async(stream_entry_to_xml(follow_request.stream_entry), target_account.id, source_account.id) unless source_account.local? | ||||
|   end | ||||
| end | ||||
|  | @ -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,6 @@ 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(stream_entry_to_xml(block.stream_entry), account.id, target_account.id) unless target_account.local? | ||||
|   end | ||||
| end | ||||
|  |  | |||
							
								
								
									
										8
									
								
								app/services/concerns/stream_entry_renderer.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								app/services/concerns/stream_entry_renderer.rb
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||
|  | @ -1,6 +1,8 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class FavouriteService < BaseService | ||||
|   include StreamEntryRenderer | ||||
| 
 | ||||
|   # Favourite a status and notify remote user | ||||
|   # @param [Account] account | ||||
|   # @param [Status] status | ||||
|  | @ -15,7 +17,7 @@ class FavouriteService < BaseService | |||
|     if status.local? | ||||
|       NotifyService.new.call(favourite.status.account, favourite) | ||||
|     else | ||||
|       NotificationWorker.perform_async(favourite.stream_entry.id, status.account_id) | ||||
|       NotificationWorker.perform_async(stream_entry_to_xml(favourite.stream_entry), account.id, status.account_id) | ||||
|     end | ||||
| 
 | ||||
|     favourite | ||||
|  |  | |||
|  | @ -1,6 +1,8 @@ | |||
| # 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 | ||||
|  | @ -20,10 +22,13 @@ 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(stream_entry_to_xml(follow_request.stream_entry), source_account.id, target_account.id) | ||||
|     end | ||||
| 
 | ||||
|     follow_request | ||||
|   end | ||||
|  | @ -35,7 +40,7 @@ class FollowService < BaseService | |||
|       NotifyService.new.call(target_account, follow) | ||||
|     else | ||||
|       subscribe_service.call(target_account) | ||||
|       NotificationWorker.perform_async(follow.stream_entry.id, target_account.id) | ||||
|       NotificationWorker.perform_async(stream_entry_to_xml(follow.stream_entry), source_account.id, target_account.id) | ||||
|     end | ||||
| 
 | ||||
|     MergeWorker.perform_async(target_account.id, source_account.id) | ||||
|  |  | |||
|  | @ -29,6 +29,10 @@ 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 :unfollow | ||||
|         unfollow!(account, target_account) | ||||
|       when :favorite | ||||
|  | @ -72,6 +76,16 @@ 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_target_account!(account, target_account) | ||||
|     follow_request = FollowRequest.find_by(account: target_account, target_account: account) | ||||
|     follow_request&.authorize! | ||||
|   end | ||||
| 
 | ||||
|   def unfollow!(account, target_account) | ||||
|     account.unfollow!(target_account) | ||||
|   end | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -33,7 +35,7 @@ class ProcessMentionsService < BaseService | |||
|       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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  |  | |||
							
								
								
									
										11
									
								
								app/services/reject_follow_service.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/services/reject_follow_service.rb
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class RejectFollowService < BaseService | ||||
|   include StreamEntryRenderer | ||||
| 
 | ||||
|   def call(source_account, target_account) | ||||
|     follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account) | ||||
|     follow_request.reject! | ||||
|     NotificationWorker.perform_async(stream_entry_to_xml(follow_request.stream_entry), target_account.id, source_account.id) unless source_account.local? | ||||
|   end | ||||
| end | ||||
|  | @ -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) | ||||
|  | @ -43,7 +45,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) | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -1,10 +1,12 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class UnblockService < BaseService | ||||
|   include StreamEntryRenderer | ||||
| 
 | ||||
|   def call(account, target_account) | ||||
|     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(stream_entry_to_xml(unblock.stream_entry), account.id, target_account.id) unless target_account.local? | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -1,12 +1,14 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class UnfavouriteService < BaseService | ||||
|   include StreamEntryRenderer | ||||
| 
 | ||||
|   def call(account, status) | ||||
|     favourite = Favourite.find_by!(account: account, status: status) | ||||
|     favourite.destroy! | ||||
| 
 | ||||
|     unless status.local? | ||||
|       NotificationWorker.perform_async(favourite.stream_entry.id, status.account_id) | ||||
|       NotificationWorker.perform_async(stream_entry_to_xml(favourite.stream_entry), account.id, status.account_id) | ||||
|     end | ||||
| 
 | ||||
|     favourite | ||||
|  |  | |||
|  | @ -1,12 +1,14 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class UnfollowService < BaseService | ||||
|   include StreamEntryRenderer | ||||
| 
 | ||||
|   # Unfollow and notify the remote user | ||||
|   # @param [Account] source_account Where to unfollow from | ||||
|   # @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(stream_entry_to_xml(follow.stream_entry), source_account.id, target_account.id) unless target_account.local? | ||||
|     UnmergeWorker.perform_async(target_account.id, source_account.id) | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ 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? | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ class NotificationWorker | |||
| 
 | ||||
|   sidekiq_options retry: 5 | ||||
| 
 | ||||
|   def perform(stream_entry_id, target_account_id) | ||||
|     SendInteractionService.new.call(StreamEntry.find(stream_entry_id), Account.find(target_account_id)) | ||||
|   def perform(xml, source_account_id, target_account_id) | ||||
|     SendInteractionService.new.call(xml, Account.find(source_account_id), Account.find(target_account_id)) | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -1,11 +0,0 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class PushNotificationWorker | ||||
|   include Sidekiq::Worker | ||||
| 
 | ||||
|   def perform(notification_id) | ||||
|     SendPushNotificationService.new.call(Notification.find(notification_id)) | ||||
|   rescue ActiveRecord::RecordNotFound | ||||
|     true | ||||
|   end | ||||
| end | ||||
|  | @ -13,7 +13,7 @@ RSpec.describe AtomBuilderHelper, type: :helper do | |||
| 
 | ||||
|   describe '#feed' do | ||||
|     it 'creates a feed' do | ||||
|       expect(used_in_builder { |xml| helper.feed(xml) }).to match '<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0"/>' | ||||
|       expect(used_in_builder { |xml| helper.feed(xml) }).to match '<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:mastodon="http://mastodon.social/schema/1.0"/>' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue