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.

360 lines
14 KiB

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