You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

366 lines
11 KiB

  1. # frozen_string_literal: true
  2. class ActivityPub::Activity::Create < ActivityPub::Activity
  3. def perform
  4. return reject_payload! if unsupported_object_type? || invalid_origin?(@object['id']) || Tombstone.exists?(uri: @object['id']) || !related_to_local_activity?
  5. RedisLock.acquire(lock_options) do |lock|
  6. if lock.acquired?
  7. return if delete_arrived_first?(object_uri)
  8. @status = find_existing_status
  9. if @status.nil?
  10. process_status
  11. elsif @options[:delivered_to_account_id].present?
  12. postprocess_audience_and_deliver
  13. end
  14. else
  15. raise Mastodon::RaceConditionError
  16. end
  17. end
  18. @status
  19. end
  20. private
  21. def process_status
  22. @tags = []
  23. @mentions = []
  24. @params = {}
  25. process_status_params
  26. process_tags
  27. process_audience
  28. ApplicationRecord.transaction do
  29. @status = Status.create!(@params)
  30. attach_tags(@status)
  31. end
  32. resolve_thread(@status)
  33. distribute(@status)
  34. forward_for_reply if @status.public_visibility? || @status.unlisted_visibility?
  35. end
  36. def find_existing_status
  37. status = status_from_uri(object_uri)
  38. status ||= Status.find_by(uri: @object['atomUri']) if @object['atomUri'].present?
  39. status
  40. end
  41. def process_status_params
  42. @params = begin
  43. {
  44. uri: @object['id'],
  45. url: object_url || @object['id'],
  46. account: @account,
  47. text: text_from_content || '',
  48. language: detected_language,
  49. spoiler_text: converted_object_type? ? '' : (text_from_summary || ''),
  50. created_at: @object['published'],
  51. override_timestamps: @options[:override_timestamps],
  52. reply: @object['inReplyTo'].present?,
  53. sensitive: @object['sensitive'] || false,
  54. visibility: visibility_from_audience,
  55. thread: replied_to_status,
  56. conversation: conversation_from_uri(@object['conversation']),
  57. media_attachment_ids: process_attachments.take(4).map(&:id),
  58. }
  59. end
  60. end
  61. def process_audience
  62. (as_array(@object['to']) + as_array(@object['cc'])).uniq.each do |audience|
  63. next if audience == ActivityPub::TagManager::COLLECTIONS[:public]
  64. # Unlike with tags, there is no point in resolving accounts we don't already
  65. # know here, because silent mentions would only be used for local access
  66. # control anyway
  67. account = account_from_uri(audience)
  68. next if account.nil? || @mentions.any? { |mention| mention.account_id == account.id }
  69. @mentions << Mention.new(account: account, silent: true)
  70. # If there is at least one silent mention, then the status can be considered
  71. # as a limited-audience status, and not strictly a direct message, but only
  72. # if we considered a direct message in the first place
  73. next unless @params[:visibility] == :direct
  74. @params[:visibility] = :limited
  75. end
  76. # If the payload was delivered to a specific inbox, the inbox owner must have
  77. # access to it, unless they already have access to it anyway
  78. return if @options[:delivered_to_account_id].nil? || @mentions.any? { |mention| mention.account_id == @options[:delivered_to_account_id] }
  79. @mentions << Mention.new(account_id: @options[:delivered_to_account_id], silent: true)
  80. return unless @params[:visibility] == :direct
  81. @params[:visibility] = :limited
  82. end
  83. def postprocess_audience_and_deliver
  84. return if @status.mentions.find_by(account_id: @options[:delivered_to_account_id])
  85. delivered_to_account = Account.find(@options[:delivered_to_account_id])
  86. @status.mentions.create(account: delivered_to_account, silent: true)
  87. @status.update(visibility: :limited) if @status.direct_visibility?
  88. return unless delivered_to_account.following?(@account)
  89. FeedInsertWorker.perform_async(@status.id, delivered_to_account.id, :home)
  90. end
  91. def attach_tags(status)
  92. @tags.each do |tag|
  93. status.tags << tag
  94. TrendingTags.record_use!(tag, status.account, status.created_at) if status.public_visibility?
  95. end
  96. @mentions.each do |mention|
  97. mention.status = status
  98. mention.save
  99. end
  100. end
  101. def process_tags
  102. return if @object['tag'].nil?
  103. as_array(@object['tag']).each do |tag|
  104. if equals_or_includes?(tag['type'], 'Hashtag')
  105. process_hashtag tag
  106. elsif equals_or_includes?(tag['type'], 'Mention')
  107. process_mention tag
  108. elsif equals_or_includes?(tag['type'], 'Emoji')
  109. process_emoji tag
  110. end
  111. end
  112. end
  113. def process_hashtag(tag)
  114. return if tag['name'].blank?
  115. hashtag = tag['name'].gsub(/\A#/, '').mb_chars.downcase
  116. hashtag = Tag.where(name: hashtag).first_or_create!(name: hashtag)
  117. return if @tags.include?(hashtag)
  118. @tags << hashtag
  119. rescue ActiveRecord::RecordInvalid
  120. nil
  121. end
  122. def process_mention(tag)
  123. return if tag['href'].blank?
  124. account = account_from_uri(tag['href'])
  125. account = ::FetchRemoteAccountService.new.call(tag['href'], id: false) if account.nil?
  126. return if account.nil?
  127. @mentions << Mention.new(account: account, silent: false)
  128. end
  129. def process_emoji(tag)
  130. return if skip_download?
  131. return if tag['name'].blank? || tag['icon'].blank? || tag['icon']['url'].blank?
  132. shortcode = tag['name'].delete(':')
  133. image_url = tag['icon']['url']
  134. uri = tag['id']
  135. updated = tag['updated']
  136. emoji = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain)
  137. return unless emoji.nil? || image_url != emoji.image_remote_url || (updated && updated >= emoji.updated_at)
  138. emoji ||= CustomEmoji.new(domain: @account.domain, shortcode: shortcode, uri: uri)
  139. emoji.image_remote_url = image_url
  140. emoji.save
  141. end
  142. def process_attachments
  143. return [] if @object['attachment'].nil?
  144. media_attachments = []
  145. as_array(@object['attachment']).each do |attachment|
  146. next if attachment['url'].blank?
  147. href = Addressable::URI.parse(attachment['url']).normalize.to_s
  148. media_attachment = MediaAttachment.create(account: @account, remote_url: href, description: attachment['name'].presence, focus: attachment['focalPoint'])
  149. media_attachments << media_attachment
  150. next if unsupported_media_type?(attachment['mediaType']) || skip_download?
  151. media_attachment.file_remote_url = href
  152. media_attachment.save
  153. end
  154. media_attachments
  155. rescue Addressable::URI::InvalidURIError => e
  156. Rails.logger.debug e
  157. media_attachments
  158. end
  159. def resolve_thread(status)
  160. return unless status.reply? && status.thread.nil? && Request.valid_url?(in_reply_to_uri)
  161. ThreadResolveWorker.perform_async(status.id, in_reply_to_uri)
  162. end
  163. def conversation_from_uri(uri)
  164. return nil if uri.nil?
  165. return Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if OStatus::TagManager.instance.local_id?(uri)
  166. Conversation.find_by(uri: uri) || Conversation.create(uri: uri)
  167. end
  168. def visibility_from_audience
  169. if equals_or_includes?(@object['to'], ActivityPub::TagManager::COLLECTIONS[:public])
  170. :public
  171. elsif equals_or_includes?(@object['cc'], ActivityPub::TagManager::COLLECTIONS[:public])
  172. :unlisted
  173. elsif equals_or_includes?(@object['to'], @account.followers_url)
  174. :private
  175. else
  176. :direct
  177. end
  178. end
  179. def audience_includes?(account)
  180. uri = ActivityPub::TagManager.instance.uri_for(account)
  181. equals_or_includes?(@object['to'], uri) || equals_or_includes?(@object['cc'], uri)
  182. end
  183. def replied_to_status
  184. return @replied_to_status if defined?(@replied_to_status)
  185. if in_reply_to_uri.blank?
  186. @replied_to_status = nil
  187. else
  188. @replied_to_status = status_from_uri(in_reply_to_uri)
  189. @replied_to_status ||= status_from_uri(@object['inReplyToAtomUri']) if @object['inReplyToAtomUri'].present?
  190. @replied_to_status
  191. end
  192. end
  193. def in_reply_to_uri
  194. value_or_id(@object['inReplyTo'])
  195. end
  196. def text_from_content
  197. return Formatter.instance.linkify([[text_from_name, text_from_summary.presence].compact.join("\n\n"), object_url || @object['id']].join(' ')) if converted_object_type?
  198. if @object['content'].present?
  199. @object['content']
  200. elsif content_language_map?
  201. @object['contentMap'].values.first
  202. end
  203. end
  204. def text_from_summary
  205. if @object['summary'].present?
  206. @object['summary']
  207. elsif summary_language_map?
  208. @object['summaryMap'].values.first
  209. end
  210. end
  211. def text_from_name
  212. if @object['name'].present?
  213. @object['name']
  214. elsif name_language_map?
  215. @object['nameMap'].values.first
  216. end
  217. end
  218. def detected_language
  219. if content_language_map?
  220. @object['contentMap'].keys.first
  221. elsif name_language_map?
  222. @object['nameMap'].keys.first
  223. elsif summary_language_map?
  224. @object['summaryMap'].keys.first
  225. elsif supported_object_type?
  226. LanguageDetector.instance.detect(text_from_content, @account)
  227. end
  228. end
  229. def object_url
  230. return if @object['url'].blank?
  231. url_candidate = url_to_href(@object['url'], 'text/html')
  232. if invalid_origin?(url_candidate)
  233. nil
  234. else
  235. url_candidate
  236. end
  237. end
  238. def summary_language_map?
  239. @object['summaryMap'].is_a?(Hash) && !@object['summaryMap'].empty?
  240. end
  241. def content_language_map?
  242. @object['contentMap'].is_a?(Hash) && !@object['contentMap'].empty?
  243. end
  244. def name_language_map?
  245. @object['nameMap'].is_a?(Hash) && !@object['nameMap'].empty?
  246. end
  247. def unsupported_media_type?(mime_type)
  248. mime_type.present? && !(MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES).include?(mime_type)
  249. end
  250. def skip_download?
  251. return @skip_download if defined?(@skip_download)
  252. @skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?
  253. end
  254. def invalid_origin?(url)
  255. return true if unsupported_uri_scheme?(url)
  256. needle = Addressable::URI.parse(url).host
  257. haystack = Addressable::URI.parse(@account.uri).host
  258. !haystack.casecmp(needle).zero?
  259. end
  260. def reply_to_local?
  261. !replied_to_status.nil? && replied_to_status.account.local?
  262. end
  263. def related_to_local_activity?
  264. fetch? || followed_by_local_accounts? || requested_through_relay? ||
  265. responds_to_followed_account? || addresses_local_accounts?
  266. end
  267. def responds_to_followed_account?
  268. !replied_to_status.nil? && (replied_to_status.account.local? || replied_to_status.account.passive_relationships.exists?)
  269. end
  270. def addresses_local_accounts?
  271. return true if @options[:delivered_to_account_id]
  272. local_usernames = (as_array(@object['to']) + as_array(@object['cc'])).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) }
  273. return false if local_usernames.empty?
  274. Account.local.where(username: local_usernames).exists?
  275. end
  276. def forward_for_reply
  277. return unless @json['signature'].present? && reply_to_local?
  278. ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url])
  279. end
  280. def lock_options
  281. { redis: Redis.current, key: "create:#{@object['id']}" }
  282. end
  283. end