Newer
Older
# frozen_string_literal: true
class PostStatusService < BaseService
# Post a text status update, fetch and notify remote users mentioned
# @param [Account] account Account from which to post
# @option [String] :text Message
# @option [Status] :thread Optional status to reply to
# @option [String] :visibility
# @option [String] :language
# @option [String] :scheduled_at
# @option [Enumerable] :media_ids Optional array of media IDs to attach
# @option [Doorkeeper::Application] :application

Eugen Rochko
committed
# @option [String] :idempotency Optional idempotency key
# @option [Boolean] :with_rate_limit
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!

Eugen Rochko
committed
end
redis.setex(idempotency_key, 3_600, @status.id) if idempotency_given?
def preprocess_attributes!
@text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present?
@visibility = @options[:visibility] || @account.user&.setting_default_privacy

ThibG
committed
@visibility = :unlisted if @visibility&.to_sym == :public && @account.silenced?
@scheduled_at = @options[:scheduled_at]&.to_datetime
@scheduled_at = nil if scheduled_in_the_past?
rescue ArgumentError
raise ActiveRecord::RecordInvalid
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!(status_attributes)

Eugen Rochko
committed
end
process_hashtags_service.call(@status)
process_mentions_service.call(@status)
end
status_for_validation = @account.statuses.build(status_attributes)
if status_for_validation.valid?
status_for_validation.destroy
# The following transaction block is needed to wrap the UPDATEs to
# the media attachments when the scheduled status is created
ApplicationRecord.transaction do
@status = @account.scheduled_statuses.create!(scheduled_status_attributes)
end
else
raise ActiveRecord::RecordInvalid
end
end
def postprocess_status!
LinkCrawlWorker.perform_async(@status.id) unless @status.spoiler_text?
DistributionWorker.perform_async(@status.id)
ActivityPub::DistributionWorker.perform_async(@status.id)
PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll
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 @options[:media_ids].size > 4 || @options[:poll].present?
@media = @account.media_attachments.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(&:audio_or_video?)
def language_from_option(str)
ISO_639.find(str)&.alpha2
end
ProcessMentionsService.new
ProcessHashtagsService.new

Eugen Rochko
committed
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
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)
end
def status_attributes
{
text: @text,
media_attachments: @media || [],
thread: @in_reply_to,

Eugen Rochko
committed
poll_attributes: poll_attributes,
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],
rate_limit: @options[:with_rate_limit],
}.compact
end
def scheduled_status_attributes
{
scheduled_at: @scheduled_at,
media_attachments: @media || [],
params: scheduled_options,
}
end
def poll_attributes
return if @options[:poll].blank?
@options[:poll].merge(account: @account, voters_count: 0)
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
options_hash[:with_rate_limit] = false