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.

378 lines
16 KiB

  1. # frozen_string_literal: true
  2. class OStatus::AtomSerializer
  3. include RoutingHelper
  4. include ActionView::Helpers::SanitizeHelper
  5. class << self
  6. def render(element)
  7. document = Ox::Document.new(version: '1.0')
  8. document << element
  9. ('<?xml version="1.0"?>' + Ox.dump(element, effort: :tolerant)).force_encoding('UTF-8')
  10. end
  11. end
  12. def author(account)
  13. author = Ox::Element.new('author')
  14. uri = OStatus::TagManager.instance.uri_for(account)
  15. append_element(author, 'id', uri)
  16. append_element(author, 'activity:object-type', OStatus::TagManager::TYPES[:person])
  17. append_element(author, 'uri', uri)
  18. append_element(author, 'name', account.username)
  19. append_element(author, 'email', account.local? ? account.local_username_and_domain : account.acct)
  20. append_element(author, 'summary', Formatter.instance.simplified_format(account).to_str, type: :html) if account.note?
  21. append_element(author, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(account))
  22. append_element(author, 'link', nil, rel: :avatar, type: account.avatar_content_type, 'media:width': 120, 'media:height': 120, href: full_asset_url(account.avatar.url(:original))) if account.avatar?
  23. append_element(author, 'link', nil, rel: :header, type: account.header_content_type, 'media:width': 700, 'media:height': 335, href: full_asset_url(account.header.url(:original))) if account.header?
  24. account.emojis.each do |emoji|
  25. append_element(author, 'link', nil, rel: :emoji, href: full_asset_url(emoji.image.url), name: emoji.shortcode)
  26. end
  27. append_element(author, 'poco:preferredUsername', account.username)
  28. append_element(author, 'poco:displayName', account.display_name) if account.display_name?
  29. append_element(author, 'poco:note', account.local? ? account.note : strip_tags(account.note)) if account.note?
  30. append_element(author, 'mastodon:scope', account.locked? ? :private : :public)
  31. author
  32. end
  33. def feed(account, stream_entries)
  34. feed = Ox::Element.new('feed')
  35. add_namespaces(feed)
  36. append_element(feed, 'id', account_url(account, format: 'atom'))
  37. append_element(feed, 'title', account.display_name.presence || account.username)
  38. append_element(feed, 'subtitle', account.note)
  39. append_element(feed, 'updated', account.updated_at.iso8601)
  40. append_element(feed, 'logo', full_asset_url(account.avatar.url(:original)))
  41. feed << author(account)
  42. append_element(feed, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(account))
  43. append_element(feed, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_url(account, format: 'atom'))
  44. append_element(feed, 'link', nil, rel: :next, type: 'application/atom+xml', href: account_url(account, format: 'atom', max_id: stream_entries.last.id)) if stream_entries.size == 20
  45. append_element(feed, 'link', nil, rel: :hub, href: api_push_url)
  46. append_element(feed, 'link', nil, rel: :salmon, href: api_salmon_url(account.id))
  47. stream_entries.each do |stream_entry|
  48. feed << entry(stream_entry)
  49. end
  50. feed
  51. end
  52. def entry(stream_entry, root = false)
  53. entry = Ox::Element.new('entry')
  54. add_namespaces(entry) if root
  55. append_element(entry, 'id', OStatus::TagManager.instance.uri_for(stream_entry.status))
  56. append_element(entry, 'published', stream_entry.created_at.iso8601)
  57. append_element(entry, 'updated', stream_entry.updated_at.iso8601)
  58. append_element(entry, 'title', stream_entry&.status&.title || "#{stream_entry.account.acct} deleted status")
  59. entry << author(stream_entry.account) if root
  60. append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[stream_entry.object_type])
  61. append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[stream_entry.verb])
  62. entry << object(stream_entry.target) if stream_entry.targeted?
  63. if stream_entry.status.nil?
  64. append_element(entry, 'content', 'Deleted status')
  65. elsif stream_entry.status.destroyed?
  66. append_element(entry, 'content', 'Deleted status')
  67. append_element(entry, 'link', nil, rel: :alternate, type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(stream_entry.status)) if stream_entry.account.local?
  68. else
  69. serialize_status_attributes(entry, stream_entry.status)
  70. end
  71. append_element(entry, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(stream_entry.status))
  72. append_element(entry, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_stream_entry_url(stream_entry.account, stream_entry, format: 'atom'))
  73. append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(stream_entry.thread), href: ::TagManager.instance.url_for(stream_entry.thread)) if stream_entry.threaded?
  74. append_element(entry, 'ostatus:conversation', nil, ref: conversation_uri(stream_entry.status.conversation)) unless stream_entry&.status&.conversation_id.nil?
  75. entry
  76. end
  77. def object(status)
  78. object = Ox::Element.new('activity:object')
  79. append_element(object, 'id', OStatus::TagManager.instance.uri_for(status))
  80. append_element(object, 'published', status.created_at.iso8601)
  81. append_element(object, 'updated', status.updated_at.iso8601)
  82. append_element(object, 'title', status.title)
  83. object << author(status.account)
  84. append_element(object, 'activity:object-type', OStatus::TagManager::TYPES[status.object_type])
  85. append_element(object, 'activity:verb', OStatus::TagManager::VERBS[status.verb])
  86. serialize_status_attributes(object, status)
  87. append_element(object, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(status))
  88. append_element(object, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(status.thread), href: ::TagManager.instance.url_for(status.thread)) unless status.thread.nil?
  89. append_element(object, 'ostatus:conversation', nil, ref: conversation_uri(status.conversation)) unless status.conversation_id.nil?
  90. object
  91. end
  92. def follow_salmon(follow)
  93. entry = Ox::Element.new('entry')
  94. add_namespaces(entry)
  95. description = "#{follow.account.acct} started following #{follow.target_account.acct}"
  96. append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(follow.created_at, follow.id, 'Follow'))
  97. append_element(entry, 'title', description)
  98. append_element(entry, 'content', description, type: :html)
  99. entry << author(follow.account)
  100. append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
  101. append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:follow])
  102. object = author(follow.target_account)
  103. object.value = 'activity:object'
  104. entry << object
  105. entry
  106. end
  107. def follow_request_salmon(follow_request)
  108. entry = Ox::Element.new('entry')
  109. add_namespaces(entry)
  110. append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(follow_request.created_at, follow_request.id, 'FollowRequest'))
  111. append_element(entry, 'title', "#{follow_request.account.acct} requested to follow #{follow_request.target_account.acct}")
  112. entry << author(follow_request.account)
  113. append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
  114. append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:request_friend])
  115. object = author(follow_request.target_account)
  116. object.value = 'activity:object'
  117. entry << object
  118. entry
  119. end
  120. def authorize_follow_request_salmon(follow_request)
  121. entry = Ox::Element.new('entry')
  122. add_namespaces(entry)
  123. append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, follow_request.id, 'FollowRequest'))
  124. append_element(entry, 'title', "#{follow_request.target_account.acct} authorizes follow request by #{follow_request.account.acct}")
  125. entry << author(follow_request.target_account)
  126. append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
  127. append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:authorize])
  128. object = Ox::Element.new('activity:object')
  129. object << author(follow_request.account)
  130. append_element(object, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
  131. append_element(object, 'activity:verb', OStatus::TagManager::VERBS[:request_friend])
  132. inner_object = author(follow_request.target_account)
  133. inner_object.value = 'activity:object'
  134. object << inner_object
  135. entry << object
  136. entry
  137. end
  138. def reject_follow_request_salmon(follow_request)
  139. entry = Ox::Element.new('entry')
  140. add_namespaces(entry)
  141. append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, follow_request.id, 'FollowRequest'))
  142. append_element(entry, 'title', "#{follow_request.target_account.acct} rejects follow request by #{follow_request.account.acct}")
  143. entry << author(follow_request.target_account)
  144. append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
  145. append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:reject])
  146. object = Ox::Element.new('activity:object')
  147. object << author(follow_request.account)
  148. append_element(object, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
  149. append_element(object, 'activity:verb', OStatus::TagManager::VERBS[:request_friend])
  150. inner_object = author(follow_request.target_account)
  151. inner_object.value = 'activity:object'
  152. object << inner_object
  153. entry << object
  154. entry
  155. end
  156. def unfollow_salmon(follow)
  157. entry = Ox::Element.new('entry')
  158. add_namespaces(entry)
  159. description = "#{follow.account.acct} is no longer following #{follow.target_account.acct}"
  160. append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, follow.id, 'Follow'))
  161. append_element(entry, 'title', description)
  162. append_element(entry, 'content', description, type: :html)
  163. entry << author(follow.account)
  164. append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
  165. append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:unfollow])
  166. object = author(follow.target_account)
  167. object.value = 'activity:object'
  168. entry << object
  169. entry
  170. end
  171. def block_salmon(block)
  172. entry = Ox::Element.new('entry')
  173. add_namespaces(entry)
  174. description = "#{block.account.acct} no longer wishes to interact with #{block.target_account.acct}"
  175. append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, block.id, 'Block'))
  176. append_element(entry, 'title', description)
  177. entry << author(block.account)
  178. append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
  179. append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:block])
  180. object = author(block.target_account)
  181. object.value = 'activity:object'
  182. entry << object
  183. entry
  184. end
  185. def unblock_salmon(block)
  186. entry = Ox::Element.new('entry')
  187. add_namespaces(entry)
  188. description = "#{block.account.acct} no longer blocks #{block.target_account.acct}"
  189. append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, block.id, 'Block'))
  190. append_element(entry, 'title', description)
  191. entry << author(block.account)
  192. append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
  193. append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:unblock])
  194. object = author(block.target_account)
  195. object.value = 'activity:object'
  196. entry << object
  197. entry
  198. end
  199. def favourite_salmon(favourite)
  200. entry = Ox::Element.new('entry')
  201. add_namespaces(entry)
  202. description = "#{favourite.account.acct} favourited a status by #{favourite.status.account.acct}"
  203. append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(favourite.created_at, favourite.id, 'Favourite'))
  204. append_element(entry, 'title', description)
  205. append_element(entry, 'content', description, type: :html)
  206. entry << author(favourite.account)
  207. append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
  208. append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:favorite])
  209. entry << object(favourite.status)
  210. append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(favourite.status), href: ::TagManager.instance.url_for(favourite.status))
  211. entry
  212. end
  213. def unfavourite_salmon(favourite)
  214. entry = Ox::Element.new('entry')
  215. add_namespaces(entry)
  216. description = "#{favourite.account.acct} no longer favourites a status by #{favourite.status.account.acct}"
  217. append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, favourite.id, 'Favourite'))
  218. append_element(entry, 'title', description)
  219. append_element(entry, 'content', description, type: :html)
  220. entry << author(favourite.account)
  221. append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
  222. append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:unfavorite])
  223. entry << object(favourite.status)
  224. append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(favourite.status), href: ::TagManager.instance.url_for(favourite.status))
  225. entry
  226. end
  227. private
  228. def append_element(parent, name, content = nil, **attributes)
  229. element = Ox::Element.new(name)
  230. attributes.each { |k, v| element[k] = sanitize_str(v) }
  231. element << sanitize_str(content) unless content.nil?
  232. parent << element
  233. end
  234. def sanitize_str(raw_str)
  235. raw_str.to_s
  236. end
  237. def conversation_uri(conversation)
  238. return conversation.uri if conversation.uri?
  239. OStatus::TagManager.instance.unique_tag(conversation.created_at, conversation.id, 'Conversation')
  240. end
  241. def add_namespaces(parent)
  242. parent['xmlns'] = OStatus::TagManager::XMLNS
  243. parent['xmlns:thr'] = OStatus::TagManager::THR_XMLNS
  244. parent['xmlns:activity'] = OStatus::TagManager::AS_XMLNS
  245. parent['xmlns:poco'] = OStatus::TagManager::POCO_XMLNS
  246. parent['xmlns:media'] = OStatus::TagManager::MEDIA_XMLNS
  247. parent['xmlns:ostatus'] = OStatus::TagManager::OS_XMLNS
  248. parent['xmlns:mastodon'] = OStatus::TagManager::MTDN_XMLNS
  249. end
  250. def serialize_status_attributes(entry, status)
  251. append_element(entry, 'link', nil, rel: :alternate, type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(status)) if status.account.local?
  252. append_element(entry, 'summary', status.spoiler_text, 'xml:lang': status.language) if status.spoiler_text?
  253. append_element(entry, 'content', Formatter.instance.format(status).to_str || '.', type: 'html', 'xml:lang': status.language)
  254. status.active_mentions.sort_by(&:id).each do |mentioned|
  255. append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': OStatus::TagManager::TYPES[:person], href: OStatus::TagManager.instance.uri_for(mentioned.account))
  256. end
  257. append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': OStatus::TagManager::TYPES[:collection], href: OStatus::TagManager::COLLECTIONS[:public]) if status.public_visibility?
  258. status.tags.each do |tag|
  259. append_element(entry, 'category', nil, term: tag.name)
  260. end
  261. status.media_attachments.each do |media|
  262. append_element(entry, 'link', nil, rel: :enclosure, type: media.file_content_type, length: media.file_file_size, href: full_asset_url(media.file.url(:original, false)))
  263. end
  264. append_element(entry, 'category', nil, term: 'nsfw') if status.sensitive? && status.media_attachments.any?
  265. append_element(entry, 'mastodon:scope', status.visibility)
  266. status.emojis.each do |emoji|
  267. append_element(entry, 'link', nil, rel: :emoji, href: full_asset_url(emoji.image.url), name: emoji.shortcode)
  268. end
  269. end
  270. end