about summary refs log tree commit diff
path: root/app/services/update_status_service.rb
diff options
context:
space:
mode:
Diffstat (limited to 'app/services/update_status_service.rb')
-rw-r--r--app/services/update_status_service.rb161
1 files changed, 161 insertions, 0 deletions
diff --git a/app/services/update_status_service.rb b/app/services/update_status_service.rb
new file mode 100644
index 000000000..cedd534ea
--- /dev/null
+++ b/app/services/update_status_service.rb
@@ -0,0 +1,161 @@
+# frozen_string_literal: true
+
+class UpdateStatusService < BaseService
+  include Redisable
+  include ImgProxyHelper
+
+  ALLOWED_ATTRIBUTES = %i(
+    spoiler_text
+    title
+    text
+    original_text
+    footer
+    content_type
+    language
+    sensitive
+    visibility
+    local_only
+    media_attachments
+    media_attachment_ids
+    application
+    expires_at
+  ).freeze
+
+  # Updates the content of an existing status.
+  # @param [Status] status The status to update.
+  # @param [Hash] params The attributes of the new status.
+  # @param [Enumerable] mentions Additional mentions added to the status.
+  # @param [Enumerable] tags New tags for the status to belong to (implicit tags are preserved).
+  def call(status, params, mentions = nil, tags = nil)
+    raise ActiveRecord::RecordNotFound if status.blank? || status.discarded? || status.destroyed?
+    return status if params.blank?
+
+    @status                 = status
+    @account                = @status.account
+    @params                 = params.with_indifferent_access.slice(*ALLOWED_ATTRIBUTES).compact
+    @mentions               = (@status.mentions | (mentions || [])).to_set
+    @tags                   = (tags.nil? ? @status.tags : (tags || [])).to_set
+
+    @params[:text]        ||= ''
+    @params[:original_text] = @params[:text]
+    @params[:published]     = true if @status.published?
+    @params[:edited]      ||= 1 + @status.edited if @params[:published].presence || @status.published?
+    @params[:expires_at]  ||= Time.now.utc + (@status.expires_at - @status.created_at) if @status.expires_at.present?
+
+    @params[:originally_local_only] = @params[:local_only] unless @status.published?
+
+    RemoveStatusService.new.call(@status, unpublish: true) if @status.published? && !@status.local_only? && @params[:local_only]
+    update_tags if @status.local?
+
+    @delete_payload         = Oj.dump(event: :delete, payload: @status.id.to_s)
+    @deleted_tag_ids        = @status.tags.pluck(:id) - @tags.pluck(:id)
+    @deleted_tag_names      = @status.tags.pluck(:name) - @tags.pluck(:name)
+    @deleted_attachment_ids = @status.media_attachment_ids - (@params[:media_attachment_ids] || @params[:media_attachments]&.pluck(:id) || [])
+
+    ApplicationRecord.transaction do
+      @status.update!(@params)
+
+      if @account.local?
+        ProcessCommandTagsService.new.call(@account, @status)
+      else
+        process_inline_images!
+      end
+
+      update_mentions
+      @status.save!
+
+      detach_deleted_tags
+      attach_updated_tags
+    end
+
+    prune_tags
+    prune_attachments
+    reset_status_caches
+
+    SpamCheck.perform(@status) if @status.published?
+    distribute
+
+    @status
+  end
+
+  private
+
+  def prune_attachments
+    @new_inline_ids = @status.inlined_attachments.pluck(:media_attachment_id)
+    RemoveMediaAttachmentsWorker.perform_async(@deleted_attachment_ids) if @deleted_attachment_ids.present?
+  end
+
+  def detach_deleted_tags
+    @status.tags -= Tag.where(id: @deleted_tag_ids) if @deleted_tag_ids.present?
+  end
+
+  def prune_tags
+    @account.featured_tags.where(tag_id: @deleted_tag_ids).each do |featured_tag|
+      featured_tag.decrement(@status.id)
+    end
+
+    return unless @status.distributable? && @deleted_tag_names.present?
+
+    @deleted_tag_names.each do |hashtag|
+      redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", @delete_payload)
+      redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", @delete_payload) if @status.local?
+    end
+  end
+
+  def update_tags
+    old_explicit_tags = Tag.matching_name(Extractor.extract_hashtags(@status.text))
+    @tags |= Tag.find_or_create_by_names(Extractor.extract_hashtags(@params[:text]))
+
+    # Preserve implicit tags attached to the original status.
+    # TODO: Let locals remove them from edits.
+    @tags |= @status.tags.where.not(id: old_explicit_tags.select(:id))
+  end
+
+  def update_mentions
+    @new_mention_ids = @mentions.pluck(:id) - @status.mention_ids
+    @status.text, @mentions = ResolveMentionsService.new.call(@status, mentions: @mentions)
+    @new_mention_ids |= (@mentions.pluck(:id) - @new_mention_ids)
+  end
+
+  def attach_updated_tags
+    tag_ids = @status.tag_ids.to_set
+    new_tag_ids = []
+    now = Time.now.utc
+
+    @tags.each do |tag|
+      next if tag_ids.include?(tag.id) || /\A(#{Tag::HASHTAG_NAME_RE})\z/i =~ $LAST_READ_LINE
+
+      @status.tags << tag
+      new_tag_ids << tag.id
+      TrendingTags.record_use!(tag, @account, now) if @status.distributable?
+    end
+
+    return unless @status.local? && @status.distributable?
+
+    @account.featured_tags.where(tag_id: new_tag_ids).each do |featured_tag|
+      featured_tag.increment(now)
+    end
+  end
+
+  def reset_status_caches
+    Rails.cache.delete_matched("statuses/#{@status.id}-*")
+    Rails.cache.delete("statuses/#{@status.id}")
+    Rails.cache.delete(@status)
+    Rails.cache.delete_matched("format:#{@status.id}:*")
+    redis.zremrangebyscore("spam_check:#{@account.id}", @status.id, @status.id)
+  end
+
+  def distribute
+    LinkCrawlWorker.perform_in(rand(1..30).seconds, @status.id) unless @status.spoiler_text?
+    DistributionWorker.perform_async(@status.id)
+
+    return unless @status.published?
+
+    ActivityPub::DistributionWorker.perform_async(@status.id) if @status.local? && !@status.local_only?
+
+    return unless @status.notify?
+
+    mentions = @status.active_mentions.includes(:account).where(id: @new_mention_ids, accounts: { domain: nil })
+    mentions.each { |mention| LocalNotificationWorker.perform_async(mention.account.id, mention.id, mention.class.name) }
+  end
+end