diff options
Diffstat (limited to 'app/lib/activitypub')
-rw-r--r-- | app/lib/activitypub/activity.rb | 16 | ||||
-rw-r--r-- | app/lib/activitypub/activity/accept.rb | 9 | ||||
-rw-r--r-- | app/lib/activitypub/activity/announce.rb | 2 | ||||
-rw-r--r-- | app/lib/activitypub/activity/create.rb | 101 | ||||
-rw-r--r-- | app/lib/activitypub/activity/delete.rb | 7 | ||||
-rw-r--r-- | app/lib/activitypub/activity/flag.rb | 2 | ||||
-rw-r--r-- | app/lib/activitypub/activity/follow.rb | 4 | ||||
-rw-r--r-- | app/lib/activitypub/activity/move.rb | 17 | ||||
-rw-r--r-- | app/lib/activitypub/activity/reject.rb | 10 | ||||
-rw-r--r-- | app/lib/activitypub/adapter.rb | 17 | ||||
-rw-r--r-- | app/lib/activitypub/serializer.rb | 8 | ||||
-rw-r--r-- | app/lib/activitypub/tag_manager.rb | 37 |
12 files changed, 161 insertions, 69 deletions
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 66b5763a9..0ca6b92a4 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -5,7 +5,7 @@ class ActivityPub::Activity include Redisable SUPPORTED_TYPES = %w(Note Question).freeze - CONVERTED_TYPES = %w(Image Video Article Page).freeze + CONVERTED_TYPES = %w(Image Audio Video Article Page).freeze def initialize(json, account, **options) @json = json @@ -89,7 +89,7 @@ class ActivityPub::Activity def distribute(status) crawl_links(status) - notify_about_reblog(status) if reblog_of_local_account?(status) + notify_about_reblog(status) if reblog_of_local_account?(status) && !reblog_by_following_group_account?(status) notify_about_mentions(status) # Only continue if the status is supposed to have arrived in real-time. @@ -105,6 +105,10 @@ class ActivityPub::Activity status.reblog? && status.reblog.account.local? end + def reblog_by_following_group_account?(status) + status.reblog? && status.account.group? && status.reblog.account.following?(status.account) + end + def notify_about_reblog(status) NotifyService.new.call(status.reblog.account, status) end @@ -153,6 +157,14 @@ class ActivityPub::Activity fetch_remote_original_status end + def follow_request_from_object + @follow_request ||= FollowRequest.find_by(target_account: @account, uri: object_uri) unless object_uri.nil? + end + + def follow_from_object + @follow ||= ::Follow.find_by(target_account: @account, uri: object_uri) unless object_uri.nil? + end + def fetch_remote_original_status if object_uri.start_with?('http') return if ActivityPub::TagManager.instance.local_uri?(object_uri) diff --git a/app/lib/activitypub/activity/accept.rb b/app/lib/activitypub/activity/accept.rb index 348ee0d1c..7010ff43e 100644 --- a/app/lib/activitypub/activity/accept.rb +++ b/app/lib/activitypub/activity/accept.rb @@ -2,17 +2,18 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity def perform + return accept_follow_for_relay if relay_follow? + return follow_request_from_object.authorize! unless follow_request_from_object.nil? + case @object['type'] when 'Follow' - accept_follow + accept_embedded_follow end end private - def accept_follow - return accept_follow_for_relay if relay_follow? - + def accept_embedded_follow target_account = account_from_uri(target_uri) return if target_account.nil? || !target_account.local? diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb index 1aa6ee9ec..34c646668 100644 --- a/app/lib/activitypub/activity/announce.rb +++ b/app/lib/activitypub/activity/announce.rb @@ -40,7 +40,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity end def announceable?(status) - status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility? + status.account_id == @account.id || status.distributable? end def related_to_local_activity? diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index f55dd35b2..8a12a2b08 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -25,6 +25,14 @@ class ActivityPub::Activity::Create < ActivityPub::Activity private + def audience_to + @object['to'] || @json['to'] + end + + def audience_cc + @object['cc'] || @json['cc'] + end + def process_status @tags = [] @mentions = [] @@ -41,8 +49,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity resolve_thread(@status) fetch_replies(@status) + check_for_spam distribute(@status) - forward_for_reply if @status.public_visibility? || @status.unlisted_visibility? + forward_for_reply if @status.distributable? end def find_existing_status @@ -74,7 +83,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end def process_audience - (as_array(@object['to']) + as_array(@object['cc'])).uniq.each do |audience| + (as_array(audience_to) + as_array(audience_cc)).uniq.each do |audience| next if audience == ActivityPub::TagManager::COLLECTIONS[:public] # Unlike with tags, there is no point in resolving accounts we don't already @@ -147,12 +156,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def process_hashtag(tag) return if tag['name'].blank? - hashtag = tag['name'].gsub(/\A#/, '').mb_chars.downcase - hashtag = Tag.where(name: hashtag).first_or_create!(name: hashtag) - - return if @tags.include?(hashtag) - - @tags << hashtag + Tag.find_or_create_by_names(tag['name']) do |hashtag| + @tags << hashtag unless @tags.include?(hashtag) + end rescue ActiveRecord::RecordInvalid nil end @@ -191,22 +197,25 @@ class ActivityPub::Activity::Create < ActivityPub::Activity media_attachments = [] as_array(@object['attachment']).each do |attachment| - next if attachment['url'].blank? + next if attachment['url'].blank? || media_attachments.size >= 4 - href = Addressable::URI.parse(attachment['url']).normalize.to_s - media_attachment = MediaAttachment.create(account: @account, remote_url: href, description: attachment['name'].presence, focus: attachment['focalPoint'], blurhash: supported_blurhash?(attachment['blurhash']) ? attachment['blurhash'] : nil) - media_attachments << media_attachment + begin + href = Addressable::URI.parse(attachment['url']).normalize.to_s + media_attachment = MediaAttachment.create(account: @account, remote_url: href, description: attachment['name'].presence, focus: attachment['focalPoint'], blurhash: supported_blurhash?(attachment['blurhash']) ? attachment['blurhash'] : nil) + media_attachments << media_attachment - next if unsupported_media_type?(attachment['mediaType']) || skip_download? + next if unsupported_media_type?(attachment['mediaType']) || skip_download? - media_attachment.file_remote_url = href - media_attachment.save + media_attachment.file_remote_url = href + media_attachment.save + rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError + RedownloadMediaWorker.perform_in(rand(30..600).seconds, media_attachment.id) + end end media_attachments rescue Addressable::URI::InvalidURIError => e - Rails.logger.debug e - + Rails.logger.debug "Invalid URL in attachment: #{e}" media_attachments end @@ -231,25 +240,40 @@ class ActivityPub::Activity::Create < ActivityPub::Activity items = @object['oneOf'] end + voters_count = @object['votersCount'] + @account.polls.new( multiple: multiple, expires_at: expires_at, options: items.map { |item| item['name'].presence || item['content'] }.compact, - cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 } + cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 }, + voters_count: voters_count ) end def poll_vote? return false if replied_to_status.nil? || replied_to_status.preloadable_poll.nil? || !replied_to_status.local? || !replied_to_status.preloadable_poll.options.include?(@object['name']) - unless replied_to_status.preloadable_poll.expired? - replied_to_status.preloadable_poll.votes.create!(account: @account, choice: replied_to_status.preloadable_poll.options.index(@object['name']), uri: @object['id']) - ActivityPub::DistributePollUpdateWorker.perform_in(3.minutes, replied_to_status.id) unless replied_to_status.preloadable_poll.hide_totals? - end + poll_vote! unless replied_to_status.preloadable_poll.expired? true end + def poll_vote! + poll = replied_to_status.preloadable_poll + already_voted = true + RedisLock.acquire(poll_lock_options) do |lock| + if lock.acquired? + already_voted = poll.votes.where(account: @account).exists? + poll.votes.create!(account: @account, choice: poll.options.index(@object['name']), uri: @object['id']) + else + raise Mastodon::RaceConditionError + end + end + increment_voters_count! unless already_voted + ActivityPub::DistributePollUpdateWorker.perform_in(3.minutes, replied_to_status.id) unless replied_to_status.preloadable_poll.hide_totals? + end + def resolve_thread(status) return unless status.reply? && status.thread.nil? && Request.valid_url?(in_reply_to_uri) ThreadResolveWorker.perform_async(status.id, in_reply_to_uri) @@ -275,11 +299,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end def visibility_from_audience - if equals_or_includes?(@object['to'], ActivityPub::TagManager::COLLECTIONS[:public]) + if equals_or_includes?(audience_to, ActivityPub::TagManager::COLLECTIONS[:public]) :public - elsif equals_or_includes?(@object['cc'], ActivityPub::TagManager::COLLECTIONS[:public]) + elsif equals_or_includes?(audience_cc, ActivityPub::TagManager::COLLECTIONS[:public]) :unlisted - elsif equals_or_includes?(@object['to'], @account.followers_url) + elsif equals_or_includes?(audience_to, @account.followers_url) :private else :direct @@ -288,7 +312,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def audience_includes?(account) uri = ActivityPub::TagManager.instance.uri_for(account) - equals_or_includes?(@object['to'], uri) || equals_or_includes?(@object['cc'], uri) + equals_or_includes?(audience_to, uri) || equals_or_includes?(audience_cc, uri) end def replied_to_status @@ -370,7 +394,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end def unsupported_media_type?(mime_type) - mime_type.present? && !(MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES).include?(mime_type) + mime_type.present? && !MediaAttachment.supported_mime_types.include?(mime_type) end def supported_blurhash?(blurhash) @@ -380,7 +404,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def skip_download? return @skip_download if defined?(@skip_download) - @skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media? + @skip_download ||= DomainBlock.reject_media?(@account.domain) end def reply_to_local? @@ -399,19 +423,38 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def addresses_local_accounts? return true if @options[:delivered_to_account_id] - local_usernames = (as_array(@object['to']) + as_array(@object['cc'])).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) } + local_usernames = (as_array(audience_to) + as_array(audience_cc)).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) } return false if local_usernames.empty? Account.local.where(username: local_usernames).exists? end + def check_for_spam + SpamCheck.perform(@status) + end + def forward_for_reply return unless @json['signature'].present? && reply_to_local? ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url]) end + def increment_voters_count! + poll = replied_to_status.preloadable_poll + unless poll.voters_count.nil? + poll.voters_count = poll.voters_count + 1 + poll.save + end + rescue ActiveRecord::StaleObjectError + poll.reload + retry + end + def lock_options { redis: Redis.current, key: "create:#{@object['id']}" } end + + def poll_lock_options + { redis: Redis.current, key: "vote:#{replied_to_status.poll_id}:#{@account.id}" } + end end diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb index 0eb14b89c..dc9ff580c 100644 --- a/app/lib/activitypub/activity/delete.rb +++ b/app/lib/activitypub/activity/delete.rb @@ -13,8 +13,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity def delete_person lock_or_return("delete_in_progress:#{@account.id}") do - SuspendAccountService.new.call(@account) - @account.destroy! + SuspendAccountService.new.call(@account, reserve_username: false) end end @@ -31,7 +30,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity return if @status.nil? - if @status.public_visibility? || @status.unlisted_visibility? + if @status.distributable? forward_for_reply forward_for_reblogs end @@ -70,7 +69,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity end def delete_now! - RemoveStatusService.new.call(@status) + RemoveStatusService.new.call(@status, redraft: false) end def payload diff --git a/app/lib/activitypub/activity/flag.rb b/app/lib/activitypub/activity/flag.rb index f73b93058..1659bc61f 100644 --- a/app/lib/activitypub/activity/flag.rb +++ b/app/lib/activitypub/activity/flag.rb @@ -23,7 +23,7 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity private def skip_reports? - DomainBlock.find_by(domain: @account.domain)&.reject_reports? + DomainBlock.reject_reports?(@account.domain) end def object_uris diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb index 3eb88339a..ec92f4255 100644 --- a/app/lib/activitypub/activity/follow.rb +++ b/app/lib/activitypub/activity/follow.rb @@ -8,7 +8,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id']) || @account.requested?(target_account) - if target_account.blocking?(@account) || target_account.domain_blocking?(@account.domain) || target_account.moved? + if target_account.blocking?(@account) || target_account.domain_blocking?(@account.domain) || target_account.moved? || target_account.instance_actor? reject_follow_request!(target_account) return end @@ -21,7 +21,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity follow_request = FollowRequest.create!(account: @account, target_account: target_account, uri: @json['id']) - if target_account.locked? + if target_account.locked? || @account.silenced? NotifyService.new.call(target_account, follow_request) else AuthorizeFollowService.new.call(@account, target_account) diff --git a/app/lib/activitypub/activity/move.rb b/app/lib/activitypub/activity/move.rb index d7a5f595c..12bb82d25 100644 --- a/app/lib/activitypub/activity/move.rb +++ b/app/lib/activitypub/activity/move.rb @@ -10,17 +10,16 @@ class ActivityPub::Activity::Move < ActivityPub::Activity target_account = ActivityPub::FetchRemoteAccountService.new.call(target_uri) - return if target_account.nil? || !target_account.also_known_as.include?(origin_account.uri) + if target_account.nil? || target_account.suspended? || !target_account.also_known_as.include?(origin_account.uri) + unmark_as_processing! + return + end # In case for some reason we didn't have a redirect for the profile already, set it - origin_account.update(moved_to_account: target_account) if origin_account.moved_to_account_id.nil? + origin_account.update(moved_to_account: target_account) # Initiate a re-follow for each follower - origin_account.followers.local.select(:id).find_in_batches do |follower_accounts| - UnfollowFollowWorker.push_bulk(follower_accounts.map(&:id)) do |follower_account_id| - [follower_account_id, origin_account.id, target_account.id] - end - end + MoveWorker.perform_async(origin_account.id, target_account.id) end private @@ -40,4 +39,8 @@ class ActivityPub::Activity::Move < ActivityPub::Activity def mark_as_processing! redis.setex("move_in_progress:#{@account.id}", PROCESSING_COOLDOWN, true) end + + def unmark_as_processing! + redis.del("move_in_progress:#{@account.id}") + end end diff --git a/app/lib/activitypub/activity/reject.rb b/app/lib/activitypub/activity/reject.rb index dba21fb9a..8d771ed81 100644 --- a/app/lib/activitypub/activity/reject.rb +++ b/app/lib/activitypub/activity/reject.rb @@ -2,17 +2,19 @@ class ActivityPub::Activity::Reject < ActivityPub::Activity def perform + return reject_follow_for_relay if relay_follow? + return follow_request_from_object.reject! unless follow_request_from_object.nil? + return UnfollowService.new.call(follow_from_object.target_account, @account) unless follow_from_object.nil? + case @object['type'] when 'Follow' - reject_follow + reject_embedded_follow end end private - def reject_follow - return reject_follow_for_relay if relay_follow? - + def reject_embedded_follow target_account = account_from_uri(target_uri) return if target_account.nil? || !target_account.local? diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb index c259c96f4..78138fb73 100644 --- a/app/lib/activitypub/adapter.rb +++ b/app/lib/activitypub/adapter.rb @@ -20,6 +20,8 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } }, identity_proof: { 'toot' => 'http://joinmastodon.org/ns#', 'IdentityProof' => 'toot:IdentityProof' }, blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' }, + discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' }, + voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' }, }.freeze def self.default_key_transform @@ -31,21 +33,24 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base end def serializable_hash(options = nil) + named_contexts = {} + context_extensions = {} + options = serialization_options(options) - serialized_hash = serializer.serializable_hash(options) + serialized_hash = serializer.serializable_hash(options.merge(named_contexts: named_contexts, context_extensions: context_extensions)) + serialized_hash = serialized_hash.select { |k, _| options[:fields].include?(k) } if options[:fields] serialized_hash = self.class.transform_key_casing!(serialized_hash, instance_options) - { '@context' => serialized_context }.merge(serialized_hash) + { '@context' => serialized_context(named_contexts, context_extensions) }.merge(serialized_hash) end private - def serialized_context + def serialized_context(named_contexts_map, context_extensions_map) context_array = [] - serializer_options = serializer.send(:instance_options) || {} - named_contexts = [:activitystreams] + serializer._named_contexts.keys + serializer_options.fetch(:named_contexts, {}).keys - context_extensions = serializer._context_extensions.keys + serializer_options.fetch(:context_extensions, {}).keys + named_contexts = [:activitystreams] + named_contexts_map.keys + context_extensions = context_extensions_map.keys named_contexts.each do |key| context_array << NAMED_CONTEXT_MAP[key] diff --git a/app/lib/activitypub/serializer.rb b/app/lib/activitypub/serializer.rb index 07bd8c494..1fdc79310 100644 --- a/app/lib/activitypub/serializer.rb +++ b/app/lib/activitypub/serializer.rb @@ -27,4 +27,12 @@ class ActivityPub::Serializer < ActiveModel::Serializer _context_extensions[extension_name] = true end end + + def serializable_hash(adapter_options = nil, options = {}, adapter_instance = self.class.serialization_adapter_instance) + unless adapter_options&.fetch(:named_contexts, nil).nil? + adapter_options[:named_contexts].merge!(_named_contexts) + adapter_options[:context_extensions].merge!(_context_extensions) + end + super(adapter_options, options, adapter_instance) + end end diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 595291342..ed680d762 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -17,7 +17,7 @@ class ActivityPub::TagManager case target.object_type when :person - short_account_url(target) + target.instance_actor? ? about_more_url(instance_actor: true) : short_account_url(target) when :note, :comment, :activity return activity_account_status_url(target.account, target) if target.reblog? short_account_status_url(target.account, target) @@ -29,7 +29,7 @@ class ActivityPub::TagManager case target.object_type when :person - account_url(target) + target.instance_actor? ? instance_actor_url : account_url(target) when :note, :comment, :activity return activity_account_status_url(target.account, target) if target.reblog? account_status_url(target.account, target) @@ -51,7 +51,7 @@ class ActivityPub::TagManager def replies_uri_for(target, page_params = nil) raise ArgumentError, 'target must be a local activity' unless %i(note comment activity).include?(target.object_type) && target.local? - replies_account_status_url(target.account, target, page_params) + account_status_replies_url(target.account, target, page_params) end # Primary audience of a status @@ -68,10 +68,19 @@ class ActivityPub::TagManager if status.account.silenced? # Only notify followers if the account is locally silenced account_ids = status.active_mentions.pluck(:account_id) - to = status.account.followers.where(id: account_ids).map { |account| uri_for(account) } - to.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).map { |request| uri_for(request.account) }) + to = status.account.followers.where(id: account_ids).each_with_object([]) do |account, result| + result << uri_for(account) + result << account.followers_url if account.group? + end + to.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).each_with_object([]) do |request, result| + result << uri_for(request.account) + result << request.account.followers_url if request.account.group? + end) else - status.active_mentions.map { |mention| uri_for(mention.account) } + status.active_mentions.each_with_object([]) do |mention, result| + result << uri_for(mention.account) + result << mention.account.followers_url if mention.account.group? + end end end end @@ -97,10 +106,19 @@ class ActivityPub::TagManager if status.account.silenced? # Only notify followers if the account is locally silenced account_ids = status.active_mentions.pluck(:account_id) - cc.concat(status.account.followers.where(id: account_ids).map { |account| uri_for(account) }) - cc.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).map { |request| uri_for(request.account) }) + cc.concat(status.account.followers.where(id: account_ids).each_with_object([]) do |account, result| + result << uri_for(account) + result << account.followers_url if account.group? + end) + cc.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).each_with_object([]) do |request, result| + result << uri_for(request.account) + result << request.account.followers_url if request.account.group? + end) else - cc.concat(status.active_mentions.map { |mention| uri_for(mention.account) }) + cc.concat(status.active_mentions.each_with_object([]) do |mention, result| + result << uri_for(mention.account) + result << mention.account.followers_url if mention.account.group? + end) end end @@ -119,6 +137,7 @@ class ActivityPub::TagManager def uri_to_local_id(uri, param = :id) path_params = Rails.application.routes.recognize_path(uri) + path_params[:username] = Rails.configuration.x.local_domain if path_params[:controller] == 'instance_actors' path_params[param] end |