diff options
Diffstat (limited to 'app/services/update_status_service.rb')
-rw-r--r-- | app/services/update_status_service.rb | 161 |
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..9dc4fbbcd --- /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[:local_only] = @status.local_only? if @params[:local_only] == true && (@status.edited.positive? || @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? + + 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 |