diff options
Diffstat (limited to 'app/services')
-rw-r--r-- | app/services/activitypub/fetch_remote_account_service.rb | 8 | ||||
-rw-r--r-- | app/services/activitypub/process_account_service.rb | 11 | ||||
-rw-r--r-- | app/services/app_sign_up_service.rb | 2 | ||||
-rw-r--r-- | app/services/follow_service.rb | 2 | ||||
-rw-r--r-- | app/services/post_status_service.rb | 176 | ||||
-rw-r--r-- | app/services/report_service.rb | 2 | ||||
-rw-r--r-- | app/services/suspend_account_service.rb | 1 |
7 files changed, 144 insertions, 58 deletions
diff --git a/app/services/activitypub/fetch_remote_account_service.rb b/app/services/activitypub/fetch_remote_account_service.rb index 8430d12d5..3c2044941 100644 --- a/app/services/activitypub/fetch_remote_account_service.rb +++ b/app/services/activitypub/fetch_remote_account_service.rb @@ -5,8 +5,8 @@ class ActivityPub::FetchRemoteAccountService < BaseService SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze - # Does a WebFinger roundtrip on each call - def call(uri, id: true, prefetched_body: nil, break_on_redirect: false) + # Does a WebFinger roundtrip on each call, unless `only_key` is true + def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false) return ActivityPub::TagManager.instance.uri_to_resource(uri, Account) if ActivityPub::TagManager.instance.local_uri?(uri) @json = if prefetched_body.nil? @@ -21,9 +21,9 @@ class ActivityPub::FetchRemoteAccountService < BaseService @username = @json['preferredUsername'] @domain = Addressable::URI.parse(@uri).normalized_host - return unless verified_webfinger? + return unless only_key || verified_webfinger? - ActivityPub::ProcessAccountService.new.call(@username, @domain, @json) + ActivityPub::ProcessAccountService.new.call(@username, @domain, @json, only_key: only_key) rescue Oj::ParseError nil end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 5c865dae2..d6c791b44 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -33,8 +33,10 @@ class ActivityPub::ProcessAccountService < BaseService after_protocol_change! if protocol_changed? after_key_change! if key_changed? && !@options[:signed_with_known_key] - check_featured_collection! if @account.featured_collection_url.present? - check_links! unless @account.fields.empty? + unless @options[:only_key] + check_featured_collection! if @account.featured_collection_url.present? + check_links! unless @account.fields.empty? + end @account rescue Oj::ParseError @@ -54,11 +56,11 @@ class ActivityPub::ProcessAccountService < BaseService end def update_account - @account.last_webfingered_at = Time.now.utc + @account.last_webfingered_at = Time.now.utc unless @options[:only_key] @account.protocol = :activitypub set_immediate_attributes! - set_fetchable_attributes! + set_fetchable_attributes! unless @options[:only_keys] @account.save_with_optional_media! end @@ -75,6 +77,7 @@ class ActivityPub::ProcessAccountService < BaseService @account.note = @json['summary'] || '' @account.locked = @json['manuallyApprovesFollowers'] || false @account.fields = property_values || {} + @account.also_known_as = as_array(@json['alsoKnownAs'] || []).map { |item| value_or_id(item) } @account.actor_type = actor_type end diff --git a/app/services/app_sign_up_service.rb b/app/services/app_sign_up_service.rb index 1878587e8..d621cc462 100644 --- a/app/services/app_sign_up_service.rb +++ b/app/services/app_sign_up_service.rb @@ -4,7 +4,7 @@ class AppSignUpService < BaseService def call(app, params) return unless allowed_registrations? - user_params = params.slice(:email, :password, :agreement) + user_params = params.slice(:email, :password, :agreement, :locale) account_params = params.slice(:username) user = User.create!(user_params.merge(created_by_application: app, password_confirmation: user_params[:password], account_attributes: account_params)) diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 24b3e1f70..9d36a1449 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -10,7 +10,7 @@ class FollowService < BaseService target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true) raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended? - raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account) + raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account) || target_account.moved? if source_account.following?(target_account) # We're already following this account, but we'll call follow! again to diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 28ecc848d..2ca92dc50 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -1,77 +1,101 @@ # frozen_string_literal: true class PostStatusService < BaseService + MIN_SCHEDULE_OFFSET = 5.minutes.freeze + # Post a text status update, fetch and notify remote users mentioned # @param [Account] account Account from which to post - # @param [String] text Message - # @param [Status] in_reply_to Optional status to reply to # @param [Hash] options + # @option [String] :text Message + # @option [Status] :thread Optional status to reply to # @option [Boolean] :sensitive # @option [String] :visibility # @option [String] :spoiler_text + # @option [String] :language + # @option [String] :scheduled_at # @option [Enumerable] :media_ids Optional array of media IDs to attach # @option [Doorkeeper::Application] :application # @option [String] :idempotency Optional idempotency key # @return [Status] - def call(account, text, in_reply_to = nil, **options) - if options[:idempotency].present? - existing_id = redis.get("idempotency:status:#{account.id}:#{options[:idempotency]}") - return Status.find(existing_id) if existing_id + def call(account, options = {}) + @account = account + @options = options + @text = @options[:text] || '' + @in_reply_to = @options[:thread] + + return idempotency_duplicate if idempotency_given? && idempotency_duplicate? + + validate_media! + preprocess_attributes! + + if scheduled? + schedule_status! + else + process_status! + postprocess_status! + bump_potential_friendship! end - media = validate_media!(options[:media_ids]) - status = nil - if text.blank? && options[:spoiler_text].present? - text = '.' - text = media.find(&:video?) ? '📹' : '🖼' if media.size > 0 + redis.setex(idempotency_key, 3_600, @status.id) if idempotency_given? + + @status + end + + private + + def preprocess_attributes! + if @text.blank? && @options[:spoiler_text].present? + @text = '.' + @text = @media.find(&:video?) ? '📹' : '🖼' if @media.size > 0 end + @visibility = @options[:visibility] || @account.user&.setting_default_privacy + @visibility = :unlisted if @visibility == :public && @account.silenced + @scheduled_at = @options[:scheduled_at]&.to_datetime + @scheduled_at = nil if scheduled_in_the_past? + end - visibility = options[:visibility] || account.user&.setting_default_privacy - visibility = :unlisted if visibility == :public && account.silenced + def process_status! + # The following transaction block is needed to wrap the UPDATEs to + # the media attachments when the status is created ApplicationRecord.transaction do - status = account.statuses.create!(text: text, - media_attachments: media || [], - thread: in_reply_to, - sensitive: (options[:sensitive].nil? ? account.user&.setting_default_sensitive : options[:sensitive]) || options[:spoiler_text].present?, - spoiler_text: options[:spoiler_text] || '', - visibility: visibility, - language: language_from_option(options[:language]) || account.user&.setting_default_language&.presence || LanguageDetector.instance.detect(text, account), - application: options[:application]) + @status = @account.statuses.create!(status_attributes) end - process_hashtags_service.call(status) - process_mentions_service.call(status) + process_hashtags_service.call(@status) + process_mentions_service.call(@status) + end - LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text? - DistributionWorker.perform_async(status.id) + def schedule_status! + if @account.statuses.build(status_attributes).valid? + # The following transaction block is needed to wrap the UPDATEs to + # the media attachments when the scheduled status is created - unless status.local_only? - Pubsubhubbub::DistributionWorker.perform_async(status.stream_entry.id) - ActivityPub::DistributionWorker.perform_async(status.id) + ApplicationRecord.transaction do + @status = @account.scheduled_statuses.create!(scheduled_status_attributes) + end + else + raise ActiveRecord::RecordInvalid end + end - if options[:idempotency].present? - redis.setex("idempotency:status:#{account.id}:#{options[:idempotency]}", 3_600, status.id) + def postprocess_status! + LinkCrawlWorker.perform_async(@status.id) unless @status.spoiler_text? + DistributionWorker.perform_async(@status.id) + unless @status.local_only? + Pubsubhubbub::DistributionWorker.perform_async(@status.stream_entry.id) + ActivityPub::DistributionWorker.perform_async(@status.id) end - - bump_potential_friendship(account, status) - - status end - private - - def validate_media!(media_ids) - return if media_ids.blank? || !media_ids.is_a?(Enumerable) + def validate_media! + return if @options[:media_ids].blank? || !@options[:media_ids].is_a?(Enumerable) - raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if media_ids.size > 4 + raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if @options[:media_ids].size > 4 - media = MediaAttachment.where(status_id: nil).where(id: media_ids.take(4).map(&:to_i)) + @media = MediaAttachment.where(status_id: nil).where(id: @options[:media_ids].take(4).map(&:to_i)) - raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if media.size > 1 && media.find(&:video?) - - media + raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && @media.find(&:video?) end def language_from_option(str) @@ -90,10 +114,68 @@ class PostStatusService < BaseService Redis.current end - def bump_potential_friendship(account, status) - return if !status.reply? || account.id == status.in_reply_to_account_id + def scheduled? + @scheduled_at.present? + end + + def idempotency_key + "idempotency:status:#{@account.id}:#{@options[:idempotency]}" + end + + def idempotency_given? + @options[:idempotency].present? + end + + def idempotency_duplicate + if scheduled? + @account.schedule_statuses.find(@idempotency_duplicate) + else + @account.statuses.find(@idempotency_duplicate) + end + end + + def idempotency_duplicate? + @idempotency_duplicate = redis.get(idempotency_key) + end + + def scheduled_in_the_past? + @scheduled_at.present? && @scheduled_at <= Time.now.utc + MIN_SCHEDULE_OFFSET + end + + def bump_potential_friendship! + return if !@status.reply? || @account.id == @status.in_reply_to_account_id ActivityTracker.increment('activity:interactions') - return if account.following?(status.in_reply_to_account_id) - PotentialFriendshipTracker.record(account.id, status.in_reply_to_account_id, :reply) + return if @account.following?(@status.in_reply_to_account_id) + PotentialFriendshipTracker.record(@account.id, @status.in_reply_to_account_id, :reply) + end + + def status_attributes + { + text: @text, + media_attachments: @media || [], + thread: @in_reply_to, + sensitive: (@options[:sensitive].nil? ? @account.user&.setting_default_sensitive : @options[:sensitive]) || @options[:spoiler_text].present?, + spoiler_text: @options[:spoiler_text] || '', + visibility: @visibility, + language: language_from_option(@options[:language]) || @account.user&.setting_default_language&.presence || LanguageDetector.instance.detect(@text, @account), + application: @options[:application], + } + end + + def scheduled_status_attributes + { + scheduled_at: @scheduled_at, + media_attachments: @media || [], + params: scheduled_options, + } + end + + def scheduled_options + @options.tap do |options_hash| + options_hash[:in_reply_to_id] = options_hash.delete(:thread)&.id + options_hash[:application_id] = options_hash.delete(:application)&.id + options_hash[:scheduled_at] = nil + options_hash[:idempotency] = nil + end end end diff --git a/app/services/report_service.rb b/app/services/report_service.rb index 057d05ab9..1bcc1c0d5 100644 --- a/app/services/report_service.rb +++ b/app/services/report_service.rb @@ -52,6 +52,6 @@ class ReportService < BaseService end def some_local_account - @some_local_account ||= Account.local.where(suspended: false).first + @some_local_account ||= Account.representative end end diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index 6ab6b2901..1bc2314de 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -20,6 +20,7 @@ class SuspendAccountService < BaseService owned_lists passive_relationships report_notes + scheduled_statuses status_pins stream_entries subscriptions |