1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
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
|