Add ActivityPub inbox (#4216)
* Add ActivityPub inbox * Handle ActivityPub deletes * Handle ActivityPub creates * Handle ActivityPub announces * Stubs for handling all activities that need to be handled * Add ActivityPub actor resolving * Handle conversation URI passing in ActivityPub * Handle content language in ActivityPub * Send accept header when fetching actor, handle JSON parse errors * Test for ActivityPub::FetchRemoteAccountService * Handle public key and icon/image when embedded/as array/as resolvable URI * Implement ActivityPub::FetchRemoteStatusService * Add stubs for more interactions * Undo activities implemented * Handle out of order activities * Hook up ActivityPub to ResolveRemoteAccountService, handle Update Account activities * Add fragment IDs to all transient activity serializers * Add tests and fixes * Add stubs for missing tests * Add more tests * Add more tests
This commit is contained in:
		
							parent
							
								
									cbcac23679
								
							
						
					
					
						commit
						f18739fd60
					
				
					 50 changed files with 1652 additions and 21 deletions
				
			
		
							
								
								
									
										30
									
								
								app/controllers/activitypub/inboxes_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/controllers/activitypub/inboxes_controller.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class ActivityPub::InboxesController < Api::BaseController | ||||||
|  |   include SignatureVerification | ||||||
|  | 
 | ||||||
|  |   before_action :set_account | ||||||
|  | 
 | ||||||
|  |   def create | ||||||
|  |     if signed_request_account | ||||||
|  |       process_payload | ||||||
|  |       head 201 | ||||||
|  |     else | ||||||
|  |       head 202 | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   private | ||||||
|  | 
 | ||||||
|  |   def set_account | ||||||
|  |     @account = Account.find_local!(params[:account_username]) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def body | ||||||
|  |     @body ||= request.body.read | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def process_payload | ||||||
|  |     ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body.force_encoding('UTF-8')) | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										31
									
								
								app/helpers/jsonld_helper.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								app/helpers/jsonld_helper.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,31 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | module JsonLdHelper | ||||||
|  |   def equals_or_includes?(haystack, needle) | ||||||
|  |     haystack.is_a?(Array) ? haystack.include?(needle) : haystack == needle | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def first_of_value(value) | ||||||
|  |     value.is_a?(Array) ? value.first : value | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def supported_context?(json) | ||||||
|  |     equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def fetch_resource(uri) | ||||||
|  |     response = build_request(uri).perform | ||||||
|  |     return if response.code != 200 | ||||||
|  |     Oj.load(response.to_s, mode: :strict) | ||||||
|  |   rescue Oj::ParseError | ||||||
|  |     nil | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   private | ||||||
|  | 
 | ||||||
|  |   def build_request(uri) | ||||||
|  |     request = Request.new(:get, uri) | ||||||
|  |     request.add_headers('Accept' => 'application/activity+json') | ||||||
|  |     request | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										109
									
								
								app/lib/activitypub/activity.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								app/lib/activitypub/activity.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,109 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class ActivityPub::Activity | ||||||
|  |   include JsonLdHelper | ||||||
|  | 
 | ||||||
|  |   def initialize(json, account) | ||||||
|  |     @json    = json | ||||||
|  |     @account = account | ||||||
|  |     @object  = @json['object'] | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def perform | ||||||
|  |     raise NotImplementedError | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   class << self | ||||||
|  |     def factory(json, account) | ||||||
|  |       @json = json | ||||||
|  |       klass&.new(json, account) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     private | ||||||
|  | 
 | ||||||
|  |     def klass | ||||||
|  |       case @json['type'] | ||||||
|  |       when 'Create' | ||||||
|  |         ActivityPub::Activity::Create | ||||||
|  |       when 'Announce' | ||||||
|  |         ActivityPub::Activity::Announce | ||||||
|  |       when 'Delete' | ||||||
|  |         ActivityPub::Activity::Delete | ||||||
|  |       when 'Follow' | ||||||
|  |         ActivityPub::Activity::Follow | ||||||
|  |       when 'Like' | ||||||
|  |         ActivityPub::Activity::Like | ||||||
|  |       when 'Block' | ||||||
|  |         ActivityPub::Activity::Block | ||||||
|  |       when 'Update' | ||||||
|  |         ActivityPub::Activity::Update | ||||||
|  |       when 'Undo' | ||||||
|  |         ActivityPub::Activity::Undo | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   protected | ||||||
|  | 
 | ||||||
|  |   def status_from_uri(uri) | ||||||
|  |     ActivityPub::TagManager.instance.uri_to_resource(uri, Status) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def account_from_uri(uri) | ||||||
|  |     ActivityPub::TagManager.instance.uri_to_resource(uri, Account) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def object_uri | ||||||
|  |     @object_uri ||= @object.is_a?(String) ? @object : @object['id'] | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def redis | ||||||
|  |     Redis.current | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def distribute(status) | ||||||
|  |     notify_about_reblog(status) if reblog_of_local_account?(status) | ||||||
|  |     notify_about_mentions(status) | ||||||
|  |     crawl_links(status) | ||||||
|  |     distribute_to_followers(status) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def reblog_of_local_account?(status) | ||||||
|  |     status.reblog? && status.reblog.account.local? | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def notify_about_reblog(status) | ||||||
|  |     NotifyService.new.call(status.reblog.account, status) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def notify_about_mentions(status) | ||||||
|  |     status.mentions.includes(:account).each do |mention| | ||||||
|  |       next unless mention.account.local? && audience_includes?(mention.account) | ||||||
|  |       NotifyService.new.call(mention.account, mention) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def crawl_links(status) | ||||||
|  |     return if status.spoiler_text? | ||||||
|  |     LinkCrawlWorker.perform_async(status.id) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def distribute_to_followers(status) | ||||||
|  |     DistributionWorker.perform_async(status.id) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def delete_arrived_first?(uri) | ||||||
|  |     key = "delete_upon_arrival:#{@account.id}:#{uri}" | ||||||
|  | 
 | ||||||
|  |     if redis.exists(key) | ||||||
|  |       redis.del(key) | ||||||
|  |       true | ||||||
|  |     else | ||||||
|  |       false | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def delete_later!(uri) | ||||||
|  |     redis.setex("delete_upon_arrival:#{@account.id}:#{uri}", 6.hours.seconds, uri) | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										14
									
								
								app/lib/activitypub/activity/announce.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								app/lib/activitypub/activity/announce.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class ActivityPub::Activity::Announce < ActivityPub::Activity | ||||||
|  |   def perform | ||||||
|  |     original_status = status_from_uri(object_uri) | ||||||
|  |     original_status = ActivityPub::FetchRemoteStatusService.new.call(object_uri) if original_status.nil? | ||||||
|  | 
 | ||||||
|  |     return if original_status.nil? || delete_arrived_first?(@json['id']) | ||||||
|  | 
 | ||||||
|  |     status = Status.create!(account: @account, reblog: original_status, uri: @json['id']) | ||||||
|  |     distribute(status) | ||||||
|  |     status | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										12
									
								
								app/lib/activitypub/activity/block.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								app/lib/activitypub/activity/block.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class ActivityPub::Activity::Block < ActivityPub::Activity | ||||||
|  |   def perform | ||||||
|  |     target_account = account_from_uri(object_uri) | ||||||
|  | 
 | ||||||
|  |     return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id']) | ||||||
|  | 
 | ||||||
|  |     UnfollowService.new.call(target_account, @account) if target_account.following?(@account) | ||||||
|  |     @account.block!(target_account) | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										148
									
								
								app/lib/activitypub/activity/create.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								app/lib/activitypub/activity/create.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,148 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class ActivityPub::Activity::Create < ActivityPub::Activity | ||||||
|  |   def perform | ||||||
|  |     return if delete_arrived_first?(object_uri) || unsupported_object_type? | ||||||
|  | 
 | ||||||
|  |     status = Status.find_by(uri: object_uri) | ||||||
|  | 
 | ||||||
|  |     return status unless status.nil? | ||||||
|  | 
 | ||||||
|  |     ApplicationRecord.transaction do | ||||||
|  |       status = Status.create!(status_params) | ||||||
|  | 
 | ||||||
|  |       process_tags(status) | ||||||
|  |       process_attachments(status) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     resolve_thread(status) | ||||||
|  |     distribute(status) | ||||||
|  | 
 | ||||||
|  |     status | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   private | ||||||
|  | 
 | ||||||
|  |   def status_params | ||||||
|  |     { | ||||||
|  |       uri: @object['id'], | ||||||
|  |       url: @object['url'], | ||||||
|  |       account: @account, | ||||||
|  |       text: text_from_content || '', | ||||||
|  |       language: language_from_content, | ||||||
|  |       spoiler_text: @object['summary'] || '', | ||||||
|  |       created_at: @object['published'] || Time.now.utc, | ||||||
|  |       reply: @object['inReplyTo'].present?, | ||||||
|  |       sensitive: @object['sensitive'] || false, | ||||||
|  |       visibility: visibility_from_audience, | ||||||
|  |       thread: replied_to_status, | ||||||
|  |       conversation: conversation_from_uri(@object['_:conversation']), | ||||||
|  |     } | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def process_tags(status) | ||||||
|  |     return unless @object['tag'].is_a?(Array) | ||||||
|  | 
 | ||||||
|  |     @object['tag'].each do |tag| | ||||||
|  |       case tag['type'] | ||||||
|  |       when 'Hashtag' | ||||||
|  |         process_hashtag tag, status | ||||||
|  |       when 'Mention' | ||||||
|  |         process_mention tag, status | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def process_hashtag(tag, status) | ||||||
|  |     hashtag = tag['name'].gsub(/\A#/, '').mb_chars.downcase | ||||||
|  |     hashtag = Tag.where(name: hashtag).first_or_initialize(name: hashtag) | ||||||
|  | 
 | ||||||
|  |     status.tags << hashtag | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def process_mention(tag, status) | ||||||
|  |     account = account_from_uri(tag['href']) | ||||||
|  |     account = ActivityPub::FetchRemoteAccountService.new.call(tag['href']) if account.nil? | ||||||
|  |     return if account.nil? | ||||||
|  |     account.mentions.create(status: status) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def process_attachments(status) | ||||||
|  |     return unless @object['attachment'].is_a?(Array) | ||||||
|  | 
 | ||||||
|  |     @object['attachment'].each do |attachment| | ||||||
|  |       next if unsupported_media_type?(attachment['mediaType']) | ||||||
|  | 
 | ||||||
|  |       href             = Addressable::URI.parse(attachment['url']).normalize.to_s | ||||||
|  |       media_attachment = MediaAttachment.create(status: status, account: status.account, remote_url: href) | ||||||
|  | 
 | ||||||
|  |       next if skip_download? | ||||||
|  | 
 | ||||||
|  |       media_attachment.file_remote_url = href | ||||||
|  |       media_attachment.save | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def resolve_thread(status) | ||||||
|  |     return unless status.reply? && status.thread.nil? | ||||||
|  |     ActivityPub::ThreadResolveWorker.perform_async(status.id, @object['inReplyTo']) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def conversation_from_uri(uri) | ||||||
|  |     return nil if uri.nil? | ||||||
|  |     return Conversation.find_by(id: TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if TagManager.instance.local_id?(uri) | ||||||
|  |     Conversation.find_by(uri: uri) || Conversation.create!(uri: uri) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def visibility_from_audience | ||||||
|  |     if equals_or_includes?(@object['to'], ActivityPub::TagManager::COLLECTIONS[:public]) | ||||||
|  |       :public | ||||||
|  |     elsif equals_or_includes?(@object['cc'], ActivityPub::TagManager::COLLECTIONS[:public]) | ||||||
|  |       :unlisted | ||||||
|  |     elsif equals_or_includes?(@object['to'], @account.followers_url) | ||||||
|  |       :private | ||||||
|  |     else | ||||||
|  |       :direct | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def audience_includes?(account) | ||||||
|  |     uri = ActivityPub::TagManager.instance.uri_for(account) | ||||||
|  |     equals_or_includes?(@object['to'], uri) || equals_or_includes?(@object['cc'], uri) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def replied_to_status | ||||||
|  |     return if @object['inReplyTo'].blank? | ||||||
|  |     @replied_to_status ||= status_from_uri(@object['inReplyTo']) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def text_from_content | ||||||
|  |     if @object['content'].present? | ||||||
|  |       @object['content'] | ||||||
|  |     elsif language_map? | ||||||
|  |       @object['contentMap'].values.first | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def language_from_content | ||||||
|  |     return nil unless language_map? | ||||||
|  |     @object['contentMap'].keys.first | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def language_map? | ||||||
|  |     @object['contentMap'].is_a?(Hash) && !@object['contentMap'].empty? | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def unsupported_object_type? | ||||||
|  |     @object.is_a?(String) || !%w(Article Note).include?(@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 skip_download? | ||||||
|  |     return @skip_download if defined?(@skip_download) | ||||||
|  |     @skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media? | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										13
									
								
								app/lib/activitypub/activity/delete.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/lib/activitypub/activity/delete.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class ActivityPub::Activity::Delete < ActivityPub::Activity | ||||||
|  |   def perform | ||||||
|  |     status = Status.find_by(uri: object_uri, account: @account) | ||||||
|  | 
 | ||||||
|  |     if status.nil? | ||||||
|  |       delete_later!(object_uri) | ||||||
|  |     else | ||||||
|  |       RemoveStatusService.new.call(status) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										12
									
								
								app/lib/activitypub/activity/follow.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								app/lib/activitypub/activity/follow.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class ActivityPub::Activity::Follow < ActivityPub::Activity | ||||||
|  |   def perform | ||||||
|  |     target_account = account_from_uri(object_uri) | ||||||
|  | 
 | ||||||
|  |     return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id']) | ||||||
|  | 
 | ||||||
|  |     follow = @account.follow!(target_account) | ||||||
|  |     NotifyService.new.call(target_account, follow) | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										12
									
								
								app/lib/activitypub/activity/like.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								app/lib/activitypub/activity/like.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class ActivityPub::Activity::Like < ActivityPub::Activity | ||||||
|  |   def perform | ||||||
|  |     original_status = status_from_uri(object_uri) | ||||||
|  | 
 | ||||||
|  |     return if original_status.nil? || !original_status.account.local? || delete_arrived_first?(@json['id']) | ||||||
|  | 
 | ||||||
|  |     favourite = original_status.favourites.where(account: @account).first_or_create!(account: @account) | ||||||
|  |     NotifyService.new.call(original_status.account, favourite) | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										69
									
								
								app/lib/activitypub/activity/undo.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								app/lib/activitypub/activity/undo.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,69 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class ActivityPub::Activity::Undo < ActivityPub::Activity | ||||||
|  |   def perform | ||||||
|  |     case @object['type'] | ||||||
|  |     when 'Announce' | ||||||
|  |       undo_announce | ||||||
|  |     when 'Follow' | ||||||
|  |       undo_follow | ||||||
|  |     when 'Like' | ||||||
|  |       undo_like | ||||||
|  |     when 'Block' | ||||||
|  |       undo_block | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   private | ||||||
|  | 
 | ||||||
|  |   def undo_announce | ||||||
|  |     status = Status.find_by(uri: object_uri, account: @account) | ||||||
|  | 
 | ||||||
|  |     if status.nil? | ||||||
|  |       delete_later!(object_uri) | ||||||
|  |     else | ||||||
|  |       RemoveStatusService.new.call(status) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def undo_follow | ||||||
|  |     target_account = account_from_uri(target_uri) | ||||||
|  | 
 | ||||||
|  |     return if target_account.nil? || !target_account.local? | ||||||
|  | 
 | ||||||
|  |     if @account.following?(target_account) | ||||||
|  |       @account.unfollow!(target_account) | ||||||
|  |     else | ||||||
|  |       delete_later!(object_uri) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def undo_like | ||||||
|  |     status = status_from_uri(target_uri) | ||||||
|  | 
 | ||||||
|  |     return if status.nil? || !status.account.local? | ||||||
|  | 
 | ||||||
|  |     if @account.favourited?(status) | ||||||
|  |       favourite = status.favourites.where(account: @account).first | ||||||
|  |       favourite&.destroy | ||||||
|  |     else | ||||||
|  |       delete_later!(object_uri) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def undo_block | ||||||
|  |     target_account = account_from_uri(target_uri) | ||||||
|  | 
 | ||||||
|  |     return if target_account.nil? || !target_account.local? | ||||||
|  | 
 | ||||||
|  |     if @account.blocking?(target_account) | ||||||
|  |       UnblockService.new.call(@account, target_account) | ||||||
|  |     else | ||||||
|  |       delete_later!(object_uri) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def target_uri | ||||||
|  |     @target_uri ||= @object['object'].is_a?(String) ? @object['object'] : @object['object']['id'] | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										17
									
								
								app/lib/activitypub/activity/update.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/lib/activitypub/activity/update.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class ActivityPub::Activity::Update < ActivityPub::Activity | ||||||
|  |   def perform | ||||||
|  |     case @object['type'] | ||||||
|  |     when 'Person' | ||||||
|  |       update_account | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   private | ||||||
|  | 
 | ||||||
|  |   def update_account | ||||||
|  |     return if @account.uri != object_uri | ||||||
|  |     ActivityPub::ProcessAccountService.new.call(@account.username, @account.domain, @object) | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -7,7 +7,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base | ||||||
| 
 | 
 | ||||||
|   def serializable_hash(options = nil) |   def serializable_hash(options = nil) | ||||||
|     options = serialization_options(options) |     options = serialization_options(options) | ||||||
|     serialized_hash = { '@context': 'https://www.w3.org/ns/activitystreams' }.merge(ActiveModelSerializers::Adapter::Attributes.new(serializer, instance_options).serializable_hash(options)) |     serialized_hash = { '@context': ActivityPub::TagManager::CONTEXT }.merge(ActiveModelSerializers::Adapter::Attributes.new(serializer, instance_options).serializable_hash(options)) | ||||||
|     self.class.transform_key_casing!(serialized_hash, instance_options) |     self.class.transform_key_casing!(serialized_hash, instance_options) | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -6,6 +6,8 @@ class ActivityPub::TagManager | ||||||
|   include Singleton |   include Singleton | ||||||
|   include RoutingHelper |   include RoutingHelper | ||||||
| 
 | 
 | ||||||
|  |   CONTEXT = 'https://www.w3.org/ns/activitystreams' | ||||||
|  | 
 | ||||||
|   COLLECTIONS = { |   COLLECTIONS = { | ||||||
|     public: 'https://www.w3.org/ns/activitystreams#Public', |     public: 'https://www.w3.org/ns/activitystreams#Public', | ||||||
|   }.freeze |   }.freeze | ||||||
|  | @ -66,4 +68,27 @@ class ActivityPub::TagManager | ||||||
| 
 | 
 | ||||||
|     cc |     cc | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   def local_uri?(uri) | ||||||
|  |     host = Addressable::URI.parse(uri).normalized_host | ||||||
|  |     ::TagManager.instance.local_domain?(host) || ::TagManager.instance.web_domain?(host) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def uri_to_local_id(uri, param = :id) | ||||||
|  |     path_params = Rails.application.routes.recognize_path(uri) | ||||||
|  |     path_params[param] | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def uri_to_resource(uri, klass) | ||||||
|  |     if local_uri?(uri) | ||||||
|  |       case klass.name | ||||||
|  |       when 'Account' | ||||||
|  |         klass.find_local(uri_to_local_id(uri, :username)) | ||||||
|  |       else | ||||||
|  |         klass.find_by(id: uri_to_local_id(uri)) | ||||||
|  |       end | ||||||
|  |     else | ||||||
|  |       klass.find_by(uri: uri) | ||||||
|  |     end | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -10,6 +10,8 @@ module Remotable | ||||||
|       alt_method_name = "reset_#{attachment_name}!".to_sym |       alt_method_name = "reset_#{attachment_name}!".to_sym | ||||||
| 
 | 
 | ||||||
|       define_method method_name do |url| |       define_method method_name do |url| | ||||||
|  |         return if url.blank? | ||||||
|  | 
 | ||||||
|         begin |         begin | ||||||
|           parsed_url = Addressable::URI.parse(url).normalize |           parsed_url = Addressable::URI.parse(url).normalize | ||||||
|         rescue Addressable::URI::InvalidURIError |         rescue Addressable::URI::InvalidURIError | ||||||
|  |  | ||||||
|  | @ -1,10 +1,14 @@ | ||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| 
 | 
 | ||||||
| class ActivityPub::AcceptFollowSerializer < ActiveModel::Serializer | class ActivityPub::AcceptFollowSerializer < ActiveModel::Serializer | ||||||
|   attributes :type, :actor |   attributes :id, :type, :actor | ||||||
| 
 | 
 | ||||||
|   has_one :object, serializer: ActivityPub::FollowSerializer |   has_one :object, serializer: ActivityPub::FollowSerializer | ||||||
| 
 | 
 | ||||||
|  |   def id | ||||||
|  |     [ActivityPub::TagManager.instance.uri_for(object.target_account), '#accepts/follows/', object.id].join | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def type |   def type | ||||||
|     'Accept' |     'Accept' | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -5,10 +5,27 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer | ||||||
| 
 | 
 | ||||||
|   attributes :id, :type, :following, :followers, |   attributes :id, :type, :following, :followers, | ||||||
|              :inbox, :outbox, :preferred_username, |              :inbox, :outbox, :preferred_username, | ||||||
|              :name, :summary, :icon, :image |              :name, :summary, :url | ||||||
| 
 | 
 | ||||||
|   has_one :public_key, serializer: ActivityPub::PublicKeySerializer |   has_one :public_key, serializer: ActivityPub::PublicKeySerializer | ||||||
| 
 | 
 | ||||||
|  |   class ImageSerializer < ActiveModel::Serializer | ||||||
|  |     include RoutingHelper | ||||||
|  | 
 | ||||||
|  |     attributes :type, :url | ||||||
|  | 
 | ||||||
|  |     def type | ||||||
|  |       'Image' | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def url | ||||||
|  |       full_asset_url(object.url(:original)) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   has_one :icon,  serializer: ImageSerializer, if: :avatar_exists? | ||||||
|  |   has_one :image, serializer: ImageSerializer, if: :header_exists? | ||||||
|  | 
 | ||||||
|   def id |   def id | ||||||
|     account_url(object) |     account_url(object) | ||||||
|   end |   end | ||||||
|  | @ -26,7 +43,7 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def inbox |   def inbox | ||||||
|     nil |     account_inbox_url(object) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def outbox |   def outbox | ||||||
|  | @ -46,14 +63,26 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def icon |   def icon | ||||||
|     full_asset_url(object.avatar.url(:original)) |     object.avatar | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def image |   def image | ||||||
|     full_asset_url(object.header.url(:original)) |     object.header | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def public_key |   def public_key | ||||||
|     object |     object | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   def url | ||||||
|  |     short_account_url(object) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def avatar_exists? | ||||||
|  |     object.avatar.exists? | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def header_exists? | ||||||
|  |     object.header.exists? | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -1,9 +1,13 @@ | ||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| 
 | 
 | ||||||
| class ActivityPub::BlockSerializer < ActiveModel::Serializer | class ActivityPub::BlockSerializer < ActiveModel::Serializer | ||||||
|   attributes :type, :actor |   attributes :id, :type, :actor | ||||||
|   attribute :virtual_object, key: :object |   attribute :virtual_object, key: :object | ||||||
| 
 | 
 | ||||||
|  |   def id | ||||||
|  |     [ActivityPub::TagManager.instance.uri_for(object.account), '#blocks/', object.id].join | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def type |   def type | ||||||
|     'Block' |     'Block' | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -1,9 +1,13 @@ | ||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| 
 | 
 | ||||||
| class ActivityPub::DeleteSerializer < ActiveModel::Serializer | class ActivityPub::DeleteSerializer < ActiveModel::Serializer | ||||||
|   attributes :type, :actor |   attributes :id, :type, :actor | ||||||
|   attribute :virtual_object, key: :object |   attribute :virtual_object, key: :object | ||||||
| 
 | 
 | ||||||
|  |   def id | ||||||
|  |     [ActivityPub::TagManager.instance.uri_for(object), '#delete'].join | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def type |   def type | ||||||
|     'Delete' |     'Delete' | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -1,9 +1,13 @@ | ||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| 
 | 
 | ||||||
| class ActivityPub::FollowSerializer < ActiveModel::Serializer | class ActivityPub::FollowSerializer < ActiveModel::Serializer | ||||||
|   attributes :type, :actor |   attributes :id, :type, :actor | ||||||
|   attribute :virtual_object, key: :object |   attribute :virtual_object, key: :object | ||||||
| 
 | 
 | ||||||
|  |   def id | ||||||
|  |     [ActivityPub::TagManager.instance.uri_for(object.account), '#follows/', object.id].join | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def type |   def type | ||||||
|     'Follow' |     'Follow' | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -1,9 +1,13 @@ | ||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| 
 | 
 | ||||||
| class ActivityPub::LikeSerializer < ActiveModel::Serializer | class ActivityPub::LikeSerializer < ActiveModel::Serializer | ||||||
|   attributes :type, :actor |   attributes :id, :type, :actor | ||||||
|   attribute :virtual_object, key: :object |   attribute :virtual_object, key: :object | ||||||
| 
 | 
 | ||||||
|  |   def id | ||||||
|  |     [ActivityPub::TagManager.instance.uri_for(object.account), '#likes/', object.id].join | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def type |   def type | ||||||
|     'Like' |     'Like' | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -1,10 +1,14 @@ | ||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| 
 | 
 | ||||||
| class ActivityPub::RejectFollowSerializer < ActiveModel::Serializer | class ActivityPub::RejectFollowSerializer < ActiveModel::Serializer | ||||||
|   attributes :type, :actor |   attributes :id, :type, :actor | ||||||
| 
 | 
 | ||||||
|   has_one :object, serializer: ActivityPub::FollowSerializer |   has_one :object, serializer: ActivityPub::FollowSerializer | ||||||
| 
 | 
 | ||||||
|  |   def id | ||||||
|  |     [ActivityPub::TagManager.instance.uri_for(object.target_account), '#rejects/follows/', object.id].join | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def type |   def type | ||||||
|     'Reject' |     'Reject' | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -1,10 +1,14 @@ | ||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| 
 | 
 | ||||||
| class ActivityPub::UndoBlockSerializer < ActiveModel::Serializer | class ActivityPub::UndoBlockSerializer < ActiveModel::Serializer | ||||||
|   attributes :type, :actor |   attributes :id, :type, :actor | ||||||
| 
 | 
 | ||||||
|   has_one :object, serializer: ActivityPub::BlockSerializer |   has_one :object, serializer: ActivityPub::BlockSerializer | ||||||
| 
 | 
 | ||||||
|  |   def id | ||||||
|  |     [ActivityPub::TagManager.instance.uri_for(object.account), '#blocks/', object.id, '/undo'].join | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def type |   def type | ||||||
|     'Undo' |     'Undo' | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -1,10 +1,14 @@ | ||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| 
 | 
 | ||||||
| class ActivityPub::UndoFollowSerializer < ActiveModel::Serializer | class ActivityPub::UndoFollowSerializer < ActiveModel::Serializer | ||||||
|   attributes :type, :actor |   attributes :id, :type, :actor | ||||||
| 
 | 
 | ||||||
|   has_one :object, serializer: ActivityPub::FollowSerializer |   has_one :object, serializer: ActivityPub::FollowSerializer | ||||||
| 
 | 
 | ||||||
|  |   def id | ||||||
|  |     [ActivityPub::TagManager.instance.uri_for(object.account), '#follows/', object.id, '/undo'].join | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def type |   def type | ||||||
|     'Undo' |     'Undo' | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -1,10 +1,14 @@ | ||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| 
 | 
 | ||||||
| class ActivityPub::UndoLikeSerializer < ActiveModel::Serializer | class ActivityPub::UndoLikeSerializer < ActiveModel::Serializer | ||||||
|   attributes :type, :actor |   attributes :id, :type, :actor | ||||||
| 
 | 
 | ||||||
|   has_one :object, serializer: ActivityPub::LikeSerializer |   has_one :object, serializer: ActivityPub::LikeSerializer | ||||||
| 
 | 
 | ||||||
|  |   def id | ||||||
|  |     [ActivityPub::TagManager.instance.uri_for(object.account), '#likes/', object.id, '/undo'].join | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def type |   def type | ||||||
|     'Undo' |     'Undo' | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -1,10 +1,14 @@ | ||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| 
 | 
 | ||||||
| class ActivityPub::UpdateSerializer < ActiveModel::Serializer | class ActivityPub::UpdateSerializer < ActiveModel::Serializer | ||||||
|   attributes :type, :actor |   attributes :id, :type, :actor | ||||||
| 
 | 
 | ||||||
|   has_one :object, serializer: ActivityPub::ActorSerializer |   has_one :object, serializer: ActivityPub::ActorSerializer | ||||||
| 
 | 
 | ||||||
|  |   def id | ||||||
|  |     [ActivityPub::TagManager.instance.uri_for(object), '#updates/', object.updated_at.to_i].join | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def type |   def type | ||||||
|     'Update' |     'Update' | ||||||
|   end |   end | ||||||
|  |  | ||||||
							
								
								
									
										57
									
								
								app/services/activitypub/fetch_remote_account_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								app/services/activitypub/fetch_remote_account_service.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,57 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class ActivityPub::FetchRemoteAccountService < BaseService | ||||||
|  |   include JsonLdHelper | ||||||
|  | 
 | ||||||
|  |   # Should be called when uri has already been checked for locality | ||||||
|  |   # Does a WebFinger roundtrip on each call | ||||||
|  |   def call(uri) | ||||||
|  |     @json = fetch_resource(uri) | ||||||
|  | 
 | ||||||
|  |     return unless supported_context? && expected_type? | ||||||
|  | 
 | ||||||
|  |     @uri      = @json['id'] | ||||||
|  |     @username = @json['preferredUsername'] | ||||||
|  |     @domain   = Addressable::URI.parse(uri).normalized_host | ||||||
|  | 
 | ||||||
|  |     return unless verified_webfinger? | ||||||
|  | 
 | ||||||
|  |     ActivityPub::ProcessAccountService.new.call(@username, @domain, @json) | ||||||
|  |   rescue Oj::ParseError | ||||||
|  |     nil | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   private | ||||||
|  | 
 | ||||||
|  |   def verified_webfinger? | ||||||
|  |     webfinger                            = Goldfinger.finger("acct:#{@username}@#{@domain}") | ||||||
|  |     confirmed_username, confirmed_domain = split_acct(webfinger.subject) | ||||||
|  | 
 | ||||||
|  |     return true if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero? | ||||||
|  | 
 | ||||||
|  |     webfinger                            = Goldfinger.finger("acct:#{confirmed_username}@#{confirmed_domain}") | ||||||
|  |     confirmed_username, confirmed_domain = split_acct(webfinger.subject) | ||||||
|  |     self_reference                       = webfinger.link('self') | ||||||
|  | 
 | ||||||
|  |     return false if self_reference&.href != @uri | ||||||
|  | 
 | ||||||
|  |     @username = confirmed_username | ||||||
|  |     @domain   = confirmed_domain | ||||||
|  | 
 | ||||||
|  |     true | ||||||
|  |   rescue Goldfinger::Error | ||||||
|  |     false | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def split_acct(acct) | ||||||
|  |     acct.gsub(/\Aacct:/, '').split('@') | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def supported_context? | ||||||
|  |     super(@json) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def expected_type? | ||||||
|  |     @json['type'] == 'Person' | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										36
									
								
								app/services/activitypub/fetch_remote_status_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								app/services/activitypub/fetch_remote_status_service.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class ActivityPub::FetchRemoteStatusService < BaseService | ||||||
|  |   include JsonLdHelper | ||||||
|  | 
 | ||||||
|  |   # Should be called when uri has already been checked for locality | ||||||
|  |   def call(uri) | ||||||
|  |     @json = fetch_resource(uri) | ||||||
|  | 
 | ||||||
|  |     return unless supported_context? && expected_type? | ||||||
|  | 
 | ||||||
|  |     attributed_to = first_of_value(@json['attributedTo']) | ||||||
|  |     attributed_to = attributed_to['id'] if attributed_to.is_a?(Hash) | ||||||
|  | 
 | ||||||
|  |     return unless trustworthy_attribution?(uri, attributed_to) | ||||||
|  | 
 | ||||||
|  |     actor = ActivityPub::TagManager.instance.uri_to_resource(attributed_to, Account) | ||||||
|  |     actor = ActivityPub::FetchRemoteAccountService.new.call(attributed_to) if actor.nil? | ||||||
|  | 
 | ||||||
|  |     ActivityPub::Activity::Create.new({ 'object' => @json }, actor).perform | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   private | ||||||
|  | 
 | ||||||
|  |   def trustworthy_attribution?(uri, attributed_to) | ||||||
|  |     Addressable::URI.parse(uri).normalized_host.casecmp(Addressable::URI.parse(attributed_to).normalized_host).zero? | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def supported_context? | ||||||
|  |     super(@json) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def expected_type? | ||||||
|  |     %w(Note Article).include? @json['type'] | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										84
									
								
								app/services/activitypub/process_account_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								app/services/activitypub/process_account_service.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,84 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class ActivityPub::ProcessAccountService < BaseService | ||||||
|  |   include JsonLdHelper | ||||||
|  | 
 | ||||||
|  |   # Should be called with confirmed valid JSON | ||||||
|  |   # and WebFinger-resolved username and domain | ||||||
|  |   def call(username, domain, json) | ||||||
|  |     @json     = json | ||||||
|  |     @uri      = @json['id'] | ||||||
|  |     @username = username | ||||||
|  |     @domain   = domain | ||||||
|  |     @account  = Account.find_by(uri: @uri) | ||||||
|  | 
 | ||||||
|  |     create_account if @account.nil? | ||||||
|  |     update_account | ||||||
|  | 
 | ||||||
|  |     @account | ||||||
|  |   rescue Oj::ParseError | ||||||
|  |     nil | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   private | ||||||
|  | 
 | ||||||
|  |   def create_account | ||||||
|  |     @account = Account.new | ||||||
|  |     @account.username    = @username | ||||||
|  |     @account.domain      = @domain | ||||||
|  |     @account.uri         = @uri | ||||||
|  |     @account.suspended   = true if auto_suspend? | ||||||
|  |     @account.silenced    = true if auto_silence? | ||||||
|  |     @account.private_key = nil | ||||||
|  |     @account.save! | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def update_account | ||||||
|  |     @account.last_webfingered_at = Time.now.utc | ||||||
|  |     @account.protocol            = :activitypub | ||||||
|  |     @account.inbox_url           = @json['inbox'] || '' | ||||||
|  |     @account.outbox_url          = @json['outbox'] || '' | ||||||
|  |     @account.shared_inbox_url    = @json['sharedInbox'] || '' | ||||||
|  |     @account.followers_url       = @json['followers'] || '' | ||||||
|  |     @account.url                 = @json['url'] || @uri | ||||||
|  |     @account.display_name        = @json['name'] || '' | ||||||
|  |     @account.note                = @json['summary'] || '' | ||||||
|  |     @account.avatar_remote_url   = image_url('icon') | ||||||
|  |     @account.header_remote_url   = image_url('image') | ||||||
|  |     @account.public_key          = public_key || '' | ||||||
|  |     @account.save! | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def image_url(key) | ||||||
|  |     value = first_of_value(@json[key]) | ||||||
|  | 
 | ||||||
|  |     return if value.nil? | ||||||
|  |     return @json[key]['url'] if @json[key].is_a?(Hash) | ||||||
|  | 
 | ||||||
|  |     image = fetch_resource(value) | ||||||
|  |     image['url'] if image | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def public_key | ||||||
|  |     value = first_of_value(@json['publicKey']) | ||||||
|  | 
 | ||||||
|  |     return if value.nil? | ||||||
|  |     return value['publicKeyPem'] if value.is_a?(Hash) | ||||||
|  | 
 | ||||||
|  |     key = fetch_resource(value) | ||||||
|  |     key['publicKeyPem'] if key | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def auto_suspend? | ||||||
|  |     domain_block && domain_block.suspend? | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def auto_silence? | ||||||
|  |     domain_block && domain_block.silence? | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def domain_block | ||||||
|  |     return @domain_block if defined?(@domain_block) | ||||||
|  |     @domain_block = DomainBlock.find_by(domain: @domain) | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										38
									
								
								app/services/activitypub/process_collection_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								app/services/activitypub/process_collection_service.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,38 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class ActivityPub::ProcessCollectionService < BaseService | ||||||
|  |   include JsonLdHelper | ||||||
|  | 
 | ||||||
|  |   def call(body, account) | ||||||
|  |     @account = account | ||||||
|  |     @json    = Oj.load(body, mode: :strict) | ||||||
|  | 
 | ||||||
|  |     return if @account.suspended? || !supported_context? | ||||||
|  | 
 | ||||||
|  |     case @json['type'] | ||||||
|  |     when 'Collection', 'CollectionPage' | ||||||
|  |       process_items @json['items'] | ||||||
|  |     when 'OrderedCollection', 'OrderedCollectionPage' | ||||||
|  |       process_items @json['orderedItems'] | ||||||
|  |     else | ||||||
|  |       process_items [@json] | ||||||
|  |     end | ||||||
|  |   rescue Oj::ParseError | ||||||
|  |     nil | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   private | ||||||
|  | 
 | ||||||
|  |   def process_items(items) | ||||||
|  |     items.reverse_each.map { |item| process_item(item) }.compact | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def supported_context? | ||||||
|  |     super(@json) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def process_item(item) | ||||||
|  |     activity = ActivityPub::Activity.factory(item, @account) | ||||||
|  |     activity&.perform | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -2,6 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| class ResolveRemoteAccountService < BaseService | class ResolveRemoteAccountService < BaseService | ||||||
|   include OStatus2::MagicKey |   include OStatus2::MagicKey | ||||||
|  |   include JsonLdHelper | ||||||
| 
 | 
 | ||||||
|   DFRN_NS = 'http://purl.org/macgirvin/dfrn/1.0' |   DFRN_NS = 'http://purl.org/macgirvin/dfrn/1.0' | ||||||
| 
 | 
 | ||||||
|  | @ -12,6 +13,7 @@ class ResolveRemoteAccountService < BaseService | ||||||
|   # @return [Account] |   # @return [Account] | ||||||
|   def call(uri, update_profile = true, redirected = nil) |   def call(uri, update_profile = true, redirected = nil) | ||||||
|     @username, @domain = uri.split('@') |     @username, @domain = uri.split('@') | ||||||
|  |     @update_profile    = update_profile | ||||||
| 
 | 
 | ||||||
|     return Account.find_local(@username) if TagManager.instance.local_domain?(@domain) |     return Account.find_local(@username) if TagManager.instance.local_domain?(@domain) | ||||||
| 
 | 
 | ||||||
|  | @ -42,10 +44,11 @@ class ResolveRemoteAccountService < BaseService | ||||||
|       if lock.acquired? |       if lock.acquired? | ||||||
|         @account = Account.find_remote(@username, @domain) |         @account = Account.find_remote(@username, @domain) | ||||||
| 
 | 
 | ||||||
|         create_account if @account.nil? |         if activitypub_ready? | ||||||
|         update_account |           handle_activitypub | ||||||
| 
 |         else | ||||||
|         update_account_profile if update_profile |           handle_ostatus | ||||||
|  |         end | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  | @ -58,18 +61,47 @@ class ResolveRemoteAccountService < BaseService | ||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|   def links_missing? |   def links_missing? | ||||||
|     @webfinger.link('http://schemas.google.com/g/2010#updates-from').nil? || |     !(activitypub_ready? || ostatus_ready?) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def ostatus_ready? | ||||||
|  |     !(@webfinger.link('http://schemas.google.com/g/2010#updates-from').nil? || | ||||||
|       @webfinger.link('salmon').nil? || |       @webfinger.link('salmon').nil? || | ||||||
|       @webfinger.link('http://webfinger.net/rel/profile-page').nil? || |       @webfinger.link('http://webfinger.net/rel/profile-page').nil? || | ||||||
|       @webfinger.link('magic-public-key').nil? || |       @webfinger.link('magic-public-key').nil? || | ||||||
|       canonical_uri.nil? || |       canonical_uri.nil? || | ||||||
|       hub_url.nil? |       hub_url.nil?) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def webfinger_update_due? |   def webfinger_update_due? | ||||||
|     @account.nil? || @account.last_webfingered_at.nil? || @account.last_webfingered_at <= 1.day.ago |     @account.nil? || @account.last_webfingered_at.nil? || @account.last_webfingered_at <= 1.day.ago | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   def activitypub_ready? | ||||||
|  |     !@webfinger.link('self').nil? && | ||||||
|  |       ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self').type) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def handle_ostatus | ||||||
|  |     create_account if @account.nil? | ||||||
|  |     update_account | ||||||
|  |     update_account_profile if update_profile? | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def update_profile? | ||||||
|  |     @update_profile | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def handle_activitypub | ||||||
|  |     json = fetch_resource(actor_url) | ||||||
|  | 
 | ||||||
|  |     return unless supported_context?(json) && json['type'] == 'Person' | ||||||
|  | 
 | ||||||
|  |     @account = ActivityPub::ProcessAccountService.new.call(@username, @domain, json) | ||||||
|  |   rescue Oj::ParseError | ||||||
|  |     nil | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def create_account |   def create_account | ||||||
|     Rails.logger.debug "Creating new remote account for #{@username}@#{@domain}" |     Rails.logger.debug "Creating new remote account for #{@username}@#{@domain}" | ||||||
| 
 | 
 | ||||||
|  | @ -81,6 +113,7 @@ class ResolveRemoteAccountService < BaseService | ||||||
| 
 | 
 | ||||||
|   def update_account |   def update_account | ||||||
|     @account.last_webfingered_at = Time.now.utc |     @account.last_webfingered_at = Time.now.utc | ||||||
|  |     @account.protocol            = :ostatus | ||||||
|     @account.remote_url          = atom_url |     @account.remote_url          = atom_url | ||||||
|     @account.salmon_url          = salmon_url |     @account.salmon_url          = salmon_url | ||||||
|     @account.url                 = url |     @account.url                 = url | ||||||
|  | @ -111,6 +144,10 @@ class ResolveRemoteAccountService < BaseService | ||||||
|     @salmon_url ||= @webfinger.link('salmon').href |     @salmon_url ||= @webfinger.link('salmon').href | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   def actor_url | ||||||
|  |     @actor_url ||= @webfinger.link('self').href | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def url |   def url | ||||||
|     @url ||= @webfinger.link('http://webfinger.net/rel/profile-page').href |     @url ||= @webfinger.link('http://webfinger.net/rel/profile-page').href | ||||||
|   end |   end | ||||||
|  |  | ||||||
							
								
								
									
										11
									
								
								app/workers/activitypub/processing_worker.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/workers/activitypub/processing_worker.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class ActivityPub::ProcessingWorker | ||||||
|  |   include Sidekiq::Worker | ||||||
|  | 
 | ||||||
|  |   sidekiq_options backtrace: true | ||||||
|  | 
 | ||||||
|  |   def perform(account_id, body) | ||||||
|  |     ProcessCollectionService.new.call(body, Account.find(account_id)) | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										17
									
								
								app/workers/activitypub/thread_resolve_worker.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/workers/activitypub/thread_resolve_worker.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class ActivityPub::ThreadResolveWorker | ||||||
|  |   include Sidekiq::Worker | ||||||
|  | 
 | ||||||
|  |   sidekiq_options queue: 'pull', retry: false | ||||||
|  | 
 | ||||||
|  |   def perform(child_status_id, parent_uri) | ||||||
|  |     child_status  = Status.find(child_status_id) | ||||||
|  |     parent_status = ActivityPub::FetchRemoteStatusService.new.call(parent_uri) | ||||||
|  | 
 | ||||||
|  |     return if parent_status.nil? | ||||||
|  | 
 | ||||||
|  |     child_status.thread = parent_status | ||||||
|  |     child_status.save! | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -17,4 +17,5 @@ ActiveSupport::Inflector.inflections(:en) do |inflect| | ||||||
|   inflect.acronym 'ActivityPub' |   inflect.acronym 'ActivityPub' | ||||||
|   inflect.acronym 'PubSubHubbub' |   inflect.acronym 'PubSubHubbub' | ||||||
|   inflect.acronym 'ActivityStreams' |   inflect.acronym 'ActivityStreams' | ||||||
|  |   inflect.acronym 'JsonLd' | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -51,6 +51,7 @@ Rails.application.routes.draw do | ||||||
|     resource :follow, only: [:create], controller: :account_follow |     resource :follow, only: [:create], controller: :account_follow | ||||||
|     resource :unfollow, only: [:create], controller: :account_unfollow |     resource :unfollow, only: [:create], controller: :account_unfollow | ||||||
|     resource :outbox, only: [:show], module: :activitypub |     resource :outbox, only: [:show], module: :activitypub | ||||||
|  |     resource :inbox, only: [:create], module: :activitypub | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   get '/@:username', to: 'accounts#show', as: :short_account |   get '/@:username', to: 'accounts#show', as: :short_account | ||||||
|  |  | ||||||
							
								
								
									
										7
									
								
								spec/controllers/activitypub/inboxes_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								spec/controllers/activitypub/inboxes_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe ActivityPub::InboxesController, type: :controller do | ||||||
|  |   describe 'POST #create' do | ||||||
|  |     pending | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										19
									
								
								spec/controllers/activitypub/outboxes_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								spec/controllers/activitypub/outboxes_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe ActivityPub::OutboxesController, type: :controller do | ||||||
|  |   let!(:account) { Fabricate(:account) } | ||||||
|  | 
 | ||||||
|  |   before do | ||||||
|  |     Fabricate(:status, account: account) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe 'GET #show' do | ||||||
|  |     before do | ||||||
|  |       get :show, params: { account_username: account.username } | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns http success' do | ||||||
|  |       expect(response).to have_http_status(:success) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										35
									
								
								spec/helpers/jsonld_helper_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								spec/helpers/jsonld_helper_spec.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | describe JsonLdHelper do | ||||||
|  |   describe '#equals_or_includes?' do | ||||||
|  |     it 'returns true when value equals' do | ||||||
|  |       expect(helper.equals_or_includes?('foo', 'foo')).to be true | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns false when value does not equal' do | ||||||
|  |       expect(helper.equals_or_includes?('foo', 'bar')).to be false | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns true when value is included' do | ||||||
|  |       expect(helper.equals_or_includes?(%w(foo baz), 'foo')).to be true | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns false when value is not included' do | ||||||
|  |       expect(helper.equals_or_includes?(%w(foo baz), 'bar')).to be false | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#first_of_value' do | ||||||
|  |     pending | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#supported_context?' do | ||||||
|  |     pending | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#fetch_resource' do | ||||||
|  |     pending | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										29
									
								
								spec/lib/activitypub/activity/announce_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								spec/lib/activitypub/activity/announce_spec.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe ActivityPub::Activity::Announce do | ||||||
|  |   let(:sender)    { Fabricate(:account) } | ||||||
|  |   let(:recipient) { Fabricate(:account) } | ||||||
|  |   let(:status)    { Fabricate(:status, account: recipient) } | ||||||
|  | 
 | ||||||
|  |   let(:json) do | ||||||
|  |     { | ||||||
|  |       '@context': 'https://www.w3.org/ns/activitystreams', | ||||||
|  |       id: 'foo', | ||||||
|  |       type: 'Announce', | ||||||
|  |       actor: ActivityPub::TagManager.instance.uri_for(sender), | ||||||
|  |       object: ActivityPub::TagManager.instance.uri_for(status), | ||||||
|  |     }.with_indifferent_access | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#perform' do | ||||||
|  |     subject { described_class.new(json, sender) } | ||||||
|  | 
 | ||||||
|  |     before do | ||||||
|  |       subject.perform | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'creates a reblog by sender of status' do | ||||||
|  |       expect(sender.reblogged?(status)).to be true | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										28
									
								
								spec/lib/activitypub/activity/block_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								spec/lib/activitypub/activity/block_spec.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe ActivityPub::Activity::Block do | ||||||
|  |   let(:sender)    { Fabricate(:account) } | ||||||
|  |   let(:recipient) { Fabricate(:account) } | ||||||
|  | 
 | ||||||
|  |   let(:json) do | ||||||
|  |     { | ||||||
|  |       '@context': 'https://www.w3.org/ns/activitystreams', | ||||||
|  |       id: 'foo', | ||||||
|  |       type: 'Block', | ||||||
|  |       actor: ActivityPub::TagManager.instance.uri_for(sender), | ||||||
|  |       object: ActivityPub::TagManager.instance.uri_for(recipient), | ||||||
|  |     }.with_indifferent_access | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#perform' do | ||||||
|  |     subject { described_class.new(json, sender) } | ||||||
|  | 
 | ||||||
|  |     before do | ||||||
|  |       subject.perform | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'creates a block from sender to recipient' do | ||||||
|  |       expect(sender.blocking?(recipient)).to be true | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										221
									
								
								spec/lib/activitypub/activity/create_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								spec/lib/activitypub/activity/create_spec.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,221 @@ | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe ActivityPub::Activity::Create do | ||||||
|  |   let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers') } | ||||||
|  | 
 | ||||||
|  |   let(:json) do | ||||||
|  |     { | ||||||
|  |       '@context': 'https://www.w3.org/ns/activitystreams', | ||||||
|  |       id: 'foo', | ||||||
|  |       type: 'Create', | ||||||
|  |       actor: ActivityPub::TagManager.instance.uri_for(sender), | ||||||
|  |       object: object_json, | ||||||
|  |     }.with_indifferent_access | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   subject { described_class.new(json, sender) } | ||||||
|  | 
 | ||||||
|  |   before do | ||||||
|  |     stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt')) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#perform' do | ||||||
|  |     before do | ||||||
|  |       subject.perform | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'standalone' do | ||||||
|  |       let(:object_json) do | ||||||
|  |         { | ||||||
|  |           id: 'bar', | ||||||
|  |           type: 'Note', | ||||||
|  |           content: 'Lorem ipsum', | ||||||
|  |         } | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'creates status' do | ||||||
|  |         status = sender.statuses.first | ||||||
|  | 
 | ||||||
|  |         expect(status).to_not be_nil | ||||||
|  |         expect(status.text).to eq 'Lorem ipsum' | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'missing to/cc defaults to direct privacy' do | ||||||
|  |         status = sender.statuses.first | ||||||
|  | 
 | ||||||
|  |         expect(status).to_not be_nil | ||||||
|  |         expect(status.visibility).to eq 'direct' | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'public' do | ||||||
|  |       let(:object_json) do | ||||||
|  |         { | ||||||
|  |           id: 'bar', | ||||||
|  |           type: 'Note', | ||||||
|  |           content: 'Lorem ipsum', | ||||||
|  |           to: 'https://www.w3.org/ns/activitystreams#Public', | ||||||
|  |         } | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'creates status' do | ||||||
|  |         status = sender.statuses.first | ||||||
|  | 
 | ||||||
|  |         expect(status).to_not be_nil | ||||||
|  |         expect(status.visibility).to eq 'public' | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'unlisted' do | ||||||
|  |       let(:object_json) do | ||||||
|  |         { | ||||||
|  |           id: 'bar', | ||||||
|  |           type: 'Note', | ||||||
|  |           content: 'Lorem ipsum', | ||||||
|  |           cc: 'https://www.w3.org/ns/activitystreams#Public', | ||||||
|  |         } | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'creates status' do | ||||||
|  |         status = sender.statuses.first | ||||||
|  | 
 | ||||||
|  |         expect(status).to_not be_nil | ||||||
|  |         expect(status.visibility).to eq 'unlisted' | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'private' do | ||||||
|  |       let(:object_json) do | ||||||
|  |         { | ||||||
|  |           id: 'bar', | ||||||
|  |           type: 'Note', | ||||||
|  |           content: 'Lorem ipsum', | ||||||
|  |           to: 'http://example.com/followers', | ||||||
|  |         } | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'creates status' do | ||||||
|  |         status = sender.statuses.first | ||||||
|  | 
 | ||||||
|  |         expect(status).to_not be_nil | ||||||
|  |         expect(status.visibility).to eq 'private' | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'direct' do | ||||||
|  |       let(:recipient) { Fabricate(:account) } | ||||||
|  | 
 | ||||||
|  |       let(:object_json) do | ||||||
|  |         { | ||||||
|  |           id: 'bar', | ||||||
|  |           type: 'Note', | ||||||
|  |           content: 'Lorem ipsum', | ||||||
|  |           to: ActivityPub::TagManager.instance.uri_for(recipient), | ||||||
|  |         } | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'creates status' do | ||||||
|  |         status = sender.statuses.first | ||||||
|  | 
 | ||||||
|  |         expect(status).to_not be_nil | ||||||
|  |         expect(status.visibility).to eq 'direct' | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'as a reply' do | ||||||
|  |       let(:original_status) { Fabricate(:status) } | ||||||
|  | 
 | ||||||
|  |       let(:object_json) do | ||||||
|  |         { | ||||||
|  |           id: 'bar', | ||||||
|  |           type: 'Note', | ||||||
|  |           content: 'Lorem ipsum', | ||||||
|  |           inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status), | ||||||
|  |         } | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'creates status' do | ||||||
|  |         status = sender.statuses.first | ||||||
|  | 
 | ||||||
|  |         expect(status).to_not be_nil | ||||||
|  |         expect(status.thread).to eq original_status | ||||||
|  |         expect(status.reply?).to be true | ||||||
|  |         expect(status.in_reply_to_account).to eq original_status.account | ||||||
|  |         expect(status.conversation).to eq original_status.conversation | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'with mentions' do | ||||||
|  |       let(:recipient) { Fabricate(:account) } | ||||||
|  | 
 | ||||||
|  |       let(:object_json) do | ||||||
|  |         { | ||||||
|  |           id: 'bar', | ||||||
|  |           type: 'Note', | ||||||
|  |           content: 'Lorem ipsum', | ||||||
|  |           tag: [ | ||||||
|  |             { | ||||||
|  |               type: 'Mention', | ||||||
|  |               href: ActivityPub::TagManager.instance.uri_for(recipient), | ||||||
|  |             }, | ||||||
|  |           ], | ||||||
|  |         } | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'creates status' do | ||||||
|  |         status = sender.statuses.first | ||||||
|  | 
 | ||||||
|  |         expect(status).to_not be_nil | ||||||
|  |         expect(status.mentions.map(&:account)).to include(recipient) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'with media attachments' do | ||||||
|  |       let(:object_json) do | ||||||
|  |         { | ||||||
|  |           id: 'bar', | ||||||
|  |           type: 'Note', | ||||||
|  |           content: 'Lorem ipsum', | ||||||
|  |           attachment: [ | ||||||
|  |             { | ||||||
|  |               type: 'Document', | ||||||
|  |               mime_type: 'image/png', | ||||||
|  |               url: 'http://example.com/attachment.png', | ||||||
|  |             }, | ||||||
|  |           ], | ||||||
|  |         } | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'creates status' do | ||||||
|  |         status = sender.statuses.first | ||||||
|  | 
 | ||||||
|  |         expect(status).to_not be_nil | ||||||
|  |         expect(status.media_attachments.map(&:remote_url)).to include('http://example.com/attachment.png') | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'with hashtags' do | ||||||
|  |       let(:object_json) do | ||||||
|  |         { | ||||||
|  |           id: 'bar', | ||||||
|  |           type: 'Note', | ||||||
|  |           content: 'Lorem ipsum', | ||||||
|  |           tag: [ | ||||||
|  |             { | ||||||
|  |               type: 'Hashtag', | ||||||
|  |               href: 'http://example.com/blah', | ||||||
|  |               name: '#test', | ||||||
|  |             }, | ||||||
|  |           ], | ||||||
|  |         } | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'creates status' do | ||||||
|  |         status = sender.statuses.first | ||||||
|  | 
 | ||||||
|  |         expect(status).to_not be_nil | ||||||
|  |         expect(status.tags.map(&:name)).to include('test') | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										28
									
								
								spec/lib/activitypub/activity/delete_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								spec/lib/activitypub/activity/delete_spec.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe ActivityPub::Activity::Delete do | ||||||
|  |   let(:sender)    { Fabricate(:account) } | ||||||
|  |   let(:status)    { Fabricate(:status, account: sender, uri: 'foobar') } | ||||||
|  | 
 | ||||||
|  |   let(:json) do | ||||||
|  |     { | ||||||
|  |       '@context': 'https://www.w3.org/ns/activitystreams', | ||||||
|  |       id: 'foo', | ||||||
|  |       type: 'Delete', | ||||||
|  |       actor: ActivityPub::TagManager.instance.uri_for(sender), | ||||||
|  |       object: ActivityPub::TagManager.instance.uri_for(status), | ||||||
|  |     }.with_indifferent_access | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#perform' do | ||||||
|  |     subject { described_class.new(json, sender) } | ||||||
|  | 
 | ||||||
|  |     before do | ||||||
|  |       subject.perform | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'deletes sender\'s status' do | ||||||
|  |       expect(Status.find_by(id: status.id)).to be_nil | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										28
									
								
								spec/lib/activitypub/activity/follow_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								spec/lib/activitypub/activity/follow_spec.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe ActivityPub::Activity::Follow do | ||||||
|  |   let(:sender)    { Fabricate(:account) } | ||||||
|  |   let(:recipient) { Fabricate(:account) } | ||||||
|  | 
 | ||||||
|  |   let(:json) do | ||||||
|  |     { | ||||||
|  |       '@context': 'https://www.w3.org/ns/activitystreams', | ||||||
|  |       id: 'foo', | ||||||
|  |       type: 'Follow', | ||||||
|  |       actor: ActivityPub::TagManager.instance.uri_for(sender), | ||||||
|  |       object: ActivityPub::TagManager.instance.uri_for(recipient), | ||||||
|  |     }.with_indifferent_access | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#perform' do | ||||||
|  |     subject { described_class.new(json, sender) } | ||||||
|  | 
 | ||||||
|  |     before do | ||||||
|  |       subject.perform | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'creates a follow from sender to recipient' do | ||||||
|  |       expect(sender.following?(recipient)).to be true | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										29
									
								
								spec/lib/activitypub/activity/like_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								spec/lib/activitypub/activity/like_spec.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe ActivityPub::Activity::Like do | ||||||
|  |   let(:sender)    { Fabricate(:account) } | ||||||
|  |   let(:recipient) { Fabricate(:account) } | ||||||
|  |   let(:status)    { Fabricate(:status, account: recipient) } | ||||||
|  | 
 | ||||||
|  |   let(:json) do | ||||||
|  |     { | ||||||
|  |       '@context': 'https://www.w3.org/ns/activitystreams', | ||||||
|  |       id: 'foo', | ||||||
|  |       type: 'Like', | ||||||
|  |       actor: ActivityPub::TagManager.instance.uri_for(sender), | ||||||
|  |       object: ActivityPub::TagManager.instance.uri_for(status), | ||||||
|  |     }.with_indifferent_access | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#perform' do | ||||||
|  |     subject { described_class.new(json, sender) } | ||||||
|  | 
 | ||||||
|  |     before do | ||||||
|  |       subject.perform | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'creates a favourite from sender to status' do | ||||||
|  |       expect(sender.favourited?(status)).to be true | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										107
									
								
								spec/lib/activitypub/activity/undo_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								spec/lib/activitypub/activity/undo_spec.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,107 @@ | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe ActivityPub::Activity::Undo do | ||||||
|  |   let(:sender) { Fabricate(:account) } | ||||||
|  | 
 | ||||||
|  |   let(:json) do | ||||||
|  |     { | ||||||
|  |       '@context': 'https://www.w3.org/ns/activitystreams', | ||||||
|  |       id: 'foo', | ||||||
|  |       type: 'Undo', | ||||||
|  |       actor: ActivityPub::TagManager.instance.uri_for(sender), | ||||||
|  |       object: object_json, | ||||||
|  |     }.with_indifferent_access | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   subject { described_class.new(json, sender) } | ||||||
|  | 
 | ||||||
|  |   describe '#perform' do | ||||||
|  |     context 'with Announce' do | ||||||
|  |       let(:status) { Fabricate(:status) } | ||||||
|  | 
 | ||||||
|  |       let(:object_json) do | ||||||
|  |         { | ||||||
|  |           id: 'bar', | ||||||
|  |           type: 'Announce', | ||||||
|  |           actor: ActivityPub::TagManager.instance.uri_for(sender), | ||||||
|  |           object: ActivityPub::TagManager.instance.uri_for(status), | ||||||
|  |         } | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         Fabricate(:status, reblog: status, account: sender, uri: 'bar') | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'deletes the reblog' do | ||||||
|  |         subject.perform | ||||||
|  |         expect(sender.reblogged?(status)).to be false | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'with Block' do | ||||||
|  |       let(:recipient) { Fabricate(:account) } | ||||||
|  | 
 | ||||||
|  |       let(:object_json) do | ||||||
|  |         { | ||||||
|  |           id: 'bar', | ||||||
|  |           type: 'Block', | ||||||
|  |           actor: ActivityPub::TagManager.instance.uri_for(sender), | ||||||
|  |           object: ActivityPub::TagManager.instance.uri_for(recipient), | ||||||
|  |         } | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         sender.block!(recipient) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'deletes block from sender to recipient' do | ||||||
|  |         subject.perform | ||||||
|  |         expect(sender.blocking?(recipient)).to be false | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'with Follow' do | ||||||
|  |       let(:recipient) { Fabricate(:account) } | ||||||
|  | 
 | ||||||
|  |       let(:object_json) do | ||||||
|  |         { | ||||||
|  |           id: 'bar', | ||||||
|  |           type: 'Follow', | ||||||
|  |           actor: ActivityPub::TagManager.instance.uri_for(sender), | ||||||
|  |           object: ActivityPub::TagManager.instance.uri_for(recipient), | ||||||
|  |         } | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         sender.follow!(recipient) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'deletes follow from sender to recipient' do | ||||||
|  |         subject.perform | ||||||
|  |         expect(sender.following?(recipient)).to be false | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'with Like' do | ||||||
|  |       let(:status) { Fabricate(:status) } | ||||||
|  | 
 | ||||||
|  |       let(:object_json) do | ||||||
|  |         { | ||||||
|  |           id: 'bar', | ||||||
|  |           type: 'Like', | ||||||
|  |           actor: ActivityPub::TagManager.instance.uri_for(sender), | ||||||
|  |           object: ActivityPub::TagManager.instance.uri_for(status), | ||||||
|  |         } | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         Fabricate(:favourite, account: sender, status: status) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'deletes favourite from sender to status' do | ||||||
|  |         subject.perform | ||||||
|  |         expect(sender.favourited?(status)).to be false | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										41
									
								
								spec/lib/activitypub/activity/update_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								spec/lib/activitypub/activity/update_spec.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,41 @@ | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe ActivityPub::Activity::Update do | ||||||
|  |   let!(:sender) { Fabricate(:account) } | ||||||
|  |    | ||||||
|  |   before do | ||||||
|  |     sender.update!(uri: ActivityPub::TagManager.instance.uri_for(sender)) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   let(:modified_sender) do  | ||||||
|  |     sender.dup.tap do |modified_sender| | ||||||
|  |       modified_sender.display_name = 'Totally modified now' | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   let(:actor_json) do | ||||||
|  |     ActiveModelSerializers::SerializableResource.new(modified_sender, serializer: ActivityPub::ActorSerializer, key_transform: :camel_lower).as_json | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   let(:json) do | ||||||
|  |     { | ||||||
|  |       '@context': 'https://www.w3.org/ns/activitystreams', | ||||||
|  |       id: 'foo', | ||||||
|  |       type: 'Update', | ||||||
|  |       actor: ActivityPub::TagManager.instance.uri_for(sender), | ||||||
|  |       object: actor_json, | ||||||
|  |     }.with_indifferent_access | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#perform' do | ||||||
|  |     subject { described_class.new(json, sender) } | ||||||
|  | 
 | ||||||
|  |     before do | ||||||
|  |       subject.perform | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'updates profile' do | ||||||
|  |       expect(sender.reload.display_name).to eq 'Totally modified now' | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										99
									
								
								spec/lib/activitypub/tag_manager_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								spec/lib/activitypub/tag_manager_spec.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,99 @@ | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe ActivityPub::TagManager do | ||||||
|  |   include RoutingHelper | ||||||
|  | 
 | ||||||
|  |   subject { described_class.instance } | ||||||
|  | 
 | ||||||
|  |   describe '#url_for' do | ||||||
|  |     it 'returns a string' do | ||||||
|  |       account = Fabricate(:account) | ||||||
|  |       expect(subject.url_for(account)).to be_a String | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#uri_for' do | ||||||
|  |     it 'returns a string' do | ||||||
|  |       account = Fabricate(:account) | ||||||
|  |       expect(subject.uri_for(account)).to be_a String | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#to' do | ||||||
|  |     it 'returns public collection for public status' do | ||||||
|  |       status = Fabricate(:status, visibility: :public) | ||||||
|  |       expect(subject.to(status)).to eq ['https://www.w3.org/ns/activitystreams#Public'] | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns followers collection for unlisted status' do | ||||||
|  |       status = Fabricate(:status, visibility: :unlisted) | ||||||
|  |       expect(subject.to(status)).to eq [account_followers_url(status.account)] | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns followers collection for private status' do | ||||||
|  |       status = Fabricate(:status, visibility: :private) | ||||||
|  |       expect(subject.to(status)).to eq [account_followers_url(status.account)] | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns URIs of mentions for direct status' do | ||||||
|  |       status    = Fabricate(:status, visibility: :direct) | ||||||
|  |       mentioned = Fabricate(:account) | ||||||
|  |       status.mentions.create(account: mentioned) | ||||||
|  |       expect(subject.to(status)).to eq [subject.uri_for(mentioned)] | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#cc' do | ||||||
|  |     it 'returns followers collection for public status' do | ||||||
|  |       status = Fabricate(:status, visibility: :public) | ||||||
|  |       expect(subject.cc(status)).to eq [account_followers_url(status.account)] | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns public collection for unlisted status' do | ||||||
|  |       status = Fabricate(:status, visibility: :unlisted) | ||||||
|  |       expect(subject.cc(status)).to eq ['https://www.w3.org/ns/activitystreams#Public'] | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns empty array for private status' do | ||||||
|  |       status = Fabricate(:status, visibility: :private) | ||||||
|  |       expect(subject.cc(status)).to eq [] | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns empty array for direct status' do | ||||||
|  |       status = Fabricate(:status, visibility: :direct) | ||||||
|  |       expect(subject.cc(status)).to eq [] | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns URIs of mentions for non-direct status' do | ||||||
|  |       status    = Fabricate(:status, visibility: :public) | ||||||
|  |       mentioned = Fabricate(:account) | ||||||
|  |       status.mentions.create(account: mentioned) | ||||||
|  |       expect(subject.cc(status)).to include(subject.uri_for(mentioned)) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#local_uri?' do | ||||||
|  |     it 'returns false for non-local URI' do | ||||||
|  |       expect(subject.local_uri?('http://example.com/123')).to be false | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns true for local URIs' do | ||||||
|  |       account = Fabricate(:account) | ||||||
|  |       expect(subject.local_uri?(subject.uri_for(account))).to be true | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#uri_to_local_id' do | ||||||
|  |     it 'returns the local ID' do | ||||||
|  |       account = Fabricate(:account) | ||||||
|  |       expect(subject.uri_to_local_id(subject.uri_for(account), :username)).to eq account.username | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#uri_to_resource' do | ||||||
|  |     it 'returns the local resource' do | ||||||
|  |       account = Fabricate(:account) | ||||||
|  |       expect(subject.uri_to_resource(subject.uri_for(account), Account)).to eq account | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,96 @@ | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe ActivityPub::FetchRemoteAccountService do | ||||||
|  |   subject { ActivityPub::FetchRemoteAccountService.new } | ||||||
|  | 
 | ||||||
|  |   let!(:actor) do | ||||||
|  |     { | ||||||
|  |       '@context': 'https://www.w3.org/ns/activitystreams', | ||||||
|  |       id: 'https://example.com/alice', | ||||||
|  |       type: 'Person', | ||||||
|  |       preferredUsername: 'alice', | ||||||
|  |       name: 'Alice', | ||||||
|  |       summary: 'Foo bar', | ||||||
|  |     } | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#call' do | ||||||
|  |     let(:account) { subject.call('https://example.com/alice') } | ||||||
|  | 
 | ||||||
|  |     shared_examples 'sets profile data' do | ||||||
|  |       it 'returns an account' do | ||||||
|  |         expect(account).to be_an Account | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'sets display name' do | ||||||
|  |         expect(account.display_name).to eq 'Alice' | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'sets note' do | ||||||
|  |         expect(account.note).to eq 'Foo bar' | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'sets URL' do | ||||||
|  |         expect(account.url).to eq 'https://example.com/alice' | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when URI and WebFinger share the same host' do | ||||||
|  |       let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) | ||||||
|  |         stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'fetches resource' do | ||||||
|  |         account | ||||||
|  |         expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'looks up webfinger' do | ||||||
|  |         account | ||||||
|  |         expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'sets username and domain from webfinger' do | ||||||
|  |         expect(account.username).to eq 'alice' | ||||||
|  |         expect(account.domain).to eq 'example.com' | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       include_examples 'sets profile data' | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when WebFinger presents different domain than URI' do | ||||||
|  |       let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) | ||||||
|  |         stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) | ||||||
|  |         stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'fetches resource' do | ||||||
|  |         account | ||||||
|  |         expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'looks up webfinger' do | ||||||
|  |         account | ||||||
|  |         expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'looks up "redirected" webfinger' do | ||||||
|  |         account | ||||||
|  |         expect(a_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af')).to have_been_made.once | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'sets username and domain from final webfinger' do | ||||||
|  |         expect(account.username).to eq 'alice' | ||||||
|  |         expect(account.domain).to eq 'iscool.af' | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       include_examples 'sets profile data' | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe ActivityPub::FetchRemoteStatusService do | ||||||
|  |   pending | ||||||
|  | end | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe ActivityPub::ProcessAccountService do | ||||||
|  |   pending | ||||||
|  | end | ||||||
|  | @ -0,0 +1,9 @@ | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe ActivityPub::ProcessCollectionService do | ||||||
|  |   subject { ActivityPub::ProcessCollectionService.new } | ||||||
|  | 
 | ||||||
|  |   describe '#call' do | ||||||
|  |     pending | ||||||
|  |   end | ||||||
|  | end | ||||||
		Loading…
	
		Reference in a new issue