summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEugen <eugen@zeonfederated.com>2017-04-07 05:56:56 +0200
committerGitHub <noreply@github.com>2017-04-07 05:56:56 +0200
commit6d6a429af8fe4bd92ed497f401676353fdc603e0 (patch)
treeb8b1e8d88c01034b7c7ca9320ae4e9235e4b8b2a
parent31597fd37717db79edb69a64c97b1dca2f9a8a08 (diff)
Rewrite Atom generation from stream entries to use Ox instead of Nokogiri (#1124)
* Rewrite Atom generation from stream entries to use Ox instead of Nokogiri::Builder StreamEntry is now limited to only statuses, which allows some optimization. Removed extra queries on AccountsController#show. AtomSerializer instead of AtomBuilderHelper used in AccountsController#show, StreamEntriesController#show, StreamEntryRenderer and PubSubHubbub::DistributionWorker PubSubHubbub::DistributionWorker moves n+1 DomainBlock query to PubSubHubbub::DeliveryWorker instead. All Salmon slaps that aren't based on StreamEntry still use AtomBuilderHelper and Nokogiri * All Salmon slaps now use Ox instead of Nokogiri. No touch from status on account
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock2
-rw-r--r--app/controllers/accounts_controller.rb3
-rw-r--r--app/controllers/stream_entries_controller.rb4
-rw-r--r--app/lib/atom_serializer.rb348
-rw-r--r--app/lib/tag_manager.rb2
-rw-r--r--app/models/stream_entry.rb28
-rw-r--r--app/services/after_block_service.rb18
-rw-r--r--app/services/authorize_follow_service.rb27
-rw-r--r--app/services/block_service.rb18
-rw-r--r--app/services/concerns/stream_entry_renderer.rb3
-rw-r--r--app/services/favourite_service.rb22
-rw-r--r--app/services/follow_service.rb44
-rw-r--r--app/services/reject_follow_service.rb27
-rw-r--r--app/services/unblock_service.rb18
-rw-r--r--app/services/unfavourite_service.rb22
-rw-r--r--app/services/unfollow_service.rb21
-rw-r--r--app/views/accounts/show.atom.ruby27
-rw-r--r--app/views/stream_entries/show.atom.ruby9
-rw-r--r--app/workers/pubsubhubbub/delivery_worker.rb3
-rw-r--r--app/workers/pubsubhubbub/distribution_worker.rb8
21 files changed, 393 insertions, 262 deletions
diff --git a/Gemfile b/Gemfile
index b5705e9d19f..65bd5eb495b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -34,6 +34,7 @@ gem 'doorkeeper'
gem 'rabl'
gem 'rqrcode'
gem 'twitter-text'
+gem 'ox'
gem 'oj'
gem 'hiredis'
gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis']
diff --git a/Gemfile.lock b/Gemfile.lock
index 408d85ade12..f2a199931f3 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -240,6 +240,7 @@ GEM
addressable (~> 2.4)
http (~> 2.0)
nokogiri (~> 1.6)
+ ox (2.4.11)
paperclip (5.1.0)
activemodel (>= 4.2.0)
activesupport (>= 4.2.0)
@@ -482,6 +483,7 @@ DEPENDENCIES
nokogiri
oj
ostatus2
+ ox
paperclip (~> 5.1)
paperclip-av-transcoder
pg
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index dc1aeb5ea5e..619c04be26e 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -16,7 +16,8 @@ class AccountsController < ApplicationController
end
format.atom do
- @entries = @account.stream_entries.order('id desc').where(activity_type: 'Status').where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
+ @entries = @account.stream_entries.order('id desc').where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
+ render xml: AtomSerializer.render(AtomSerializer.new.feed(@account, @entries.to_a))
end
format.activitystreams2
diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb
index de38b360232..469a8c33e2b 100644
--- a/app/controllers/stream_entries_controller.rb
+++ b/app/controllers/stream_entries_controller.rb
@@ -19,7 +19,9 @@ class StreamEntriesController < ApplicationController
end
end
- format.atom
+ format.atom do
+ render xml: AtomSerializer.render(AtomSerializer.new.entry(@stream_entry, true))
+ end
end
end
diff --git a/app/lib/atom_serializer.rb b/app/lib/atom_serializer.rb
new file mode 100644
index 00000000000..21f485c2d37
--- /dev/null
+++ b/app/lib/atom_serializer.rb
@@ -0,0 +1,348 @@
+# frozen_string_literal: true
+
+class AtomSerializer
+ include RoutingHelper
+
+ class << self
+ def render(element)
+ document = Ox::Document.new(version: '1.0')
+ document << element
+ "<?xml version=\"1.0\"?>#{Ox.dump(element)}"
+ end
+ end
+
+ def author(account)
+ author = Ox::Element.new('author')
+
+ uri = TagManager.instance.uri_for(account)
+
+ append_element(author, 'id', uri)
+ append_element(author, 'activity:object-type', TagManager::TYPES[:person])
+ append_element(author, 'uri', uri)
+ append_element(author, 'name', account.username)
+ append_element(author, 'email', account.local? ? "#{account.acct}@#{Rails.configuration.x.local_domain}" : account.acct)
+ append_element(author, 'summary', account.note)
+ append_element(author, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(account))
+ 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)))
+ 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)))
+ append_element(author, 'poco:preferredUsername', account.username)
+ append_element(author, 'poco:displayName', account.display_name) unless account.display_name.blank?
+ append_element(author, 'poco:note', Formatter.instance.simplified_format(account).to_str) unless account.note.blank?
+ append_element(author, 'mastodon:scope', account.locked? ? :private : :public)
+
+ author
+ end
+
+ def feed(account, stream_entries)
+ feed = Ox::Element.new('feed')
+
+ add_namespaces(feed)
+
+ append_element(feed, 'id', account_url(account, format: 'atom'))
+ append_element(feed, 'title', account.display_name)
+ append_element(feed, 'subtitle', account.note)
+ append_element(feed, 'updated', account.updated_at.iso8601)
+ append_element(feed, 'logo', full_asset_url(account.avatar.url(:original)))
+
+ feed << author(account)
+
+ append_element(feed, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(account))
+ append_element(feed, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_url(account, format: 'atom'))
+ 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
+ append_element(feed, 'link', nil, rel: :hub, href: api_push_url)
+ append_element(feed, 'link', nil, rel: :salmon, href: api_salmon_url(account.id))
+
+ stream_entries.each do |stream_entry|
+ feed << entry(stream_entry)
+ end
+
+ feed
+ end
+
+ def entry(stream_entry, root = false)
+ entry = Ox::Element.new('entry')
+
+ add_namespaces(entry) if root
+
+ append_element(entry, 'id', TagManager.instance.unique_tag(stream_entry.created_at, stream_entry.activity_id, stream_entry.activity_type))
+ append_element(entry, 'published', stream_entry.created_at.iso8601)
+ append_element(entry, 'updated', stream_entry.updated_at.iso8601)
+ append_element(entry, 'title', stream_entry&.status&.title)
+ append_element(entry, 'activity:object-type', TagManager::TYPES[stream_entry.object_type])
+ append_element(entry, 'activity:verb', TagManager::VERBS[stream_entry.verb])
+
+ entry << object(stream_entry.target) if stream_entry.targeted?
+
+ serialize_status_attributes(entry, stream_entry.status) unless stream_entry.status.nil?
+
+ append_element(entry, 'link', nil, rel: :alternate, type: 'text/html', href: account_stream_entry_url(stream_entry.account, stream_entry))
+ append_element(entry, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_stream_entry_url(stream_entry.account, stream_entry, format: 'atom'))
+ 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?
+
+ entry
+ end
+
+ def object(status)
+ object = Ox::Element.new('activity:object')
+
+ append_element(object, 'id', TagManager.instance.uri_for(status))
+ append_element(object, 'published', status.created_at.iso8601)
+ append_element(object, 'updated', status.updated_at.iso8601)
+ append_element(object, 'title', status.title)
+
+ object << author(status.account)
+
+ append_element(object, 'activity:object-type', TagManager::TYPES[status.object_type])
+ append_element(object, 'activity:verb', TagManager::VERBS[status.verb])
+
+ serialize_status_attributes(object, status)
+
+ append_element(object, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(status))
+ 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?
+
+ object
+ end
+
+ def follow_salmon(follow)
+ entry = Ox::Element.new('entry')
+ add_namespaces(entry)
+
+ description = "#{follow.account.acct} started following #{follow.target_account.acct}"
+
+ append_element(entry, 'id', TagManager.instance.unique_tag(follow.created_at, follow.id, 'Follow'))
+ append_element(entry, 'title', description)
+ append_element(entry, 'content', description, type: :html)
+
+ entry << author(follow.account)
+
+ append_element(entry, 'activity:object-type', TagManager::TYPES[:activity])
+ append_element(entry, 'activity:verb', TagManager::VERBS[:follow])
+
+ object = author(follow.target_account)
+ object.value = 'activity:object'
+
+ entry << object
+ entry
+ end
+
+ def follow_request_salmon(follow_request)
+ entry = Ox::Element.new('entry')
+ add_namespaces(entry)
+
+ append_element(entry, 'id', TagManager.instance.unique_tag(follow_request.created_at, follow_request.id, 'FollowRequest'))
+ append_element(entry, 'title', "#{follow_request.account.acct} requested to follow #{follow_request.target_account.acct}")
+
+ entry << author(follow_request.account)
+
+ append_element(entry, 'activity:object-type', TagManager::TYPES[:activity])
+ append_element(entry, 'activity:verb', TagManager::VERBS[:request_friend])
+
+ object = author(follow_request.target_account)
+ object.value = 'activity:object'
+
+ entry << object
+ entry
+ end
+
+ def authorize_follow_request_salmon(follow_request)
+ entry = Ox::Element.new('entry')
+ add_namespaces(entry)
+
+ append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, follow_request.id, 'FollowRequest'))
+ append_element(entry, 'title', "#{follow_request.target_account.acct} authorizes follow request by #{follow_request.account.acct}")
+
+ entry << author(follow_request.target_account)
+
+ append_element(entry, 'activity:object-type', TagManager::TYPES[:activity])
+ append_element(entry, 'activity:verb', TagManager::VERBS[:authorize])
+
+ object = Ox::Element.new('activity:object')
+ object << author(follow_request.account)
+
+ append_element(object, 'activity:object-type', TagManager::TYPES[:activity])
+ append_element(object, 'activity:verb', TagManager::VERBS[:request_friend])
+
+ inner_object = author(follow_request.target_account)
+ inner_object.value = 'activity:object'
+
+ object << inner_object
+ entry << object
+ entry
+ end
+
+ def reject_follow_request_salmon(follow_request)
+ entry = Ox::Element.new('entry')
+ add_namespaces(entry)
+
+ append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, follow_request.id, 'FollowRequest'))
+ append_element(entry, 'title', "#{follow_request.target_account.acct} rejects follow request by #{follow_request.account.acct}")
+
+ entry << author(follow_request.target_account)
+
+ append_element(entry, 'activity:object-type', TagManager::TYPES[:activity])
+ append_element(entry, 'activity:verb', TagManager::VERBS[:reject])
+
+ object = Ox::Element.new('activity:object')
+ object << author(follow_request.account)
+
+ append_element(object, 'activity:object-type', TagManager::TYPES[:activity])
+ append_element(object, 'activity:verb', TagManager::VERBS[:request_friend])
+
+ inner_object = author(follow_request.target_account)
+ inner_object.value = 'activity:object'
+
+ object << inner_object
+ entry << object
+ entry
+ end
+
+ def unfollow_salmon(follow)
+ entry = Ox::Element.new('entry')
+ add_namespaces(entry)
+
+ description = "#{follow.account.acct} is no longer following #{follow.target_account.acct}"
+
+ append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, follow.id, 'Follow'))
+ append_element(entry, 'title', description)
+ append_element(entry, 'content', description, type: :html)
+
+ entry << author(follow.account)
+
+ append_element(entry, 'activity:object-type', TagManager::TYPES[:activity])
+ append_element(entry, 'activity:verb', TagManager::VERBS[:unfollow])
+
+ object = author(follow.target_account)
+ object.value = 'activity:object'
+
+ entry << object
+ entry
+ end
+
+ def block_salmon(block)
+ entry = Ox::Element.new('entry')
+ add_namespaces(entry)
+
+ description = "#{block.account.acct} no longer wishes to interact with #{block.target_account.acct}"
+
+ append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, block.id, 'Block'))
+ append_element(entry, 'title', description)
+
+ entry << author(block.account)
+
+ append_element(entry, 'activity:object-type', TagManager::TYPES[:activity])
+ append_element(entry, 'activity:verb', TagManager::VERBS[:block])
+
+ object = author(block.target_account)
+ object.value = 'activity:object'
+
+ entry << object
+ entry
+ end
+
+ def unblock_salmon(block)
+ entry = Ox::Element.new('entry')
+ add_namespaces(entry)
+
+ description = "#{block.account.acct} no longer blocks #{block.target_account.acct}"
+
+ append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, block.id, 'Block'))
+ append_element(entry, 'title', description)
+
+ entry << author(block.account)
+
+ append_element(entry, 'activity:object-type', TagManager::TYPES[:activity])
+ append_element(entry, 'activity:verb', TagManager::VERBS[:unblock])
+
+ object = author(block.target_account)
+ object.value = 'activity:object'
+
+ entry << object
+ entry
+ end
+
+ def favourite_salmon(favourite)
+ entry = Ox::Element.new('entry')
+ add_namespaces(entry)
+
+ description = "#{favourite.account.acct} favourited a status by #{favourite.status.account.acct}"
+
+ append_element(entry, 'id', TagManager.instance.unique_tag(favourite.created_at, favourite.id, 'Favourite'))
+ append_element(entry, 'title', description)
+ append_element(entry, 'content', description, type: :html)
+
+ entry << author(favourite.account)
+
+ append_element(entry, 'activity:object-type', TagManager::TYPES[:activity])
+ append_element(entry, 'activity:verb', TagManager::VERBS[:favorite])
+
+ entry << object(favourite.status)
+
+ append_element(entry, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(favourite.status), href: TagManager.instance.url_for(favourite.status))
+
+ entry
+ end
+
+ def unfavourite_salmon(favourite)
+ entry = Ox::Element.new('entry')
+ add_namespaces(entry)
+
+ description = "#{favourite.account.acct} no longer favourites a status by #{favourite.status.account.acct}"
+
+ append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, favourite.id, 'Favourite'))
+ append_element(entry, 'title', description)
+ append_element(entry, 'content', description, type: :html)
+
+ entry << author(favourite.account)
+
+ append_element(entry, 'activity:object-type', TagManager::TYPES[:activity])
+ append_element(entry, 'activity:verb', TagManager::VERBS[:unfavorite])
+
+ entry << object(favourite.status)
+
+ append_element(entry, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(favourite.status), href: TagManager.instance.url_for(favourite.status))
+
+ entry
+ end
+
+ private
+
+ def append_element(parent, name, content = nil, attributes = {})
+ element = Ox::Element.new(name)
+ attributes.each { |k, v| element[k] = v.to_s }
+ element << content.to_s unless content.nil?
+ parent << element
+ end
+
+ def add_namespaces(parent)
+ parent['xmlns'] = TagManager::XMLNS
+ parent['xmlns:thr'] = TagManager::THR_XMLNS
+ parent['xmlns:activity'] = TagManager::AS_XMLNS
+ parent['xmlns:poco'] = TagManager::POCO_XMLNS
+ parent['xmlns:media'] = TagManager::MEDIA_XMLNS
+ parent['xmlns:ostatus'] = TagManager::OS_XMLNS
+ parent['xmlns:mastodon'] = TagManager::MTDN_XMLNS
+ end
+
+ def serialize_status_attributes(entry, status)
+ append_element(entry, 'summary', status.spoiler_text) unless status.spoiler_text.blank?
+ append_element(entry, 'content', Formatter.instance.format(status.reblog? ? status.reblog : status).to_str, type: 'html')
+
+ status.mentions.each do |mentioned|
+ append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': TagManager::TYPES[:person], href: TagManager.instance.uri_for(mentioned.account))
+ end
+
+ append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': TagManager::TYPES[:collection], href: TagManager::COLLECTIONS[:public]) if status.public_visibility?
+
+ status.tags.each do |tag|
+ append_element(entry, 'category', nil, term: tag.name)
+ end
+
+ append_element(entry, 'category', nil, term: 'nsfw') if status.sensitive?
+
+ status.media_attachments.each do |media|
+ 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)))
+ end
+
+ append_element(entry, 'mastodon:scope', status.visibility)
+ end
+end
diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb
index 2a5e7a40953..07b2fb91e01 100644
--- a/app/lib/tag_manager.rb
+++ b/app/lib/tag_manager.rb
@@ -78,6 +78,8 @@ class TagManager
case target.object_type
when :person
account_url(target)
+ when :note, :comment, :activity
+ unique_tag(target.created_at, target.id, 'Status')
else
unique_tag(target.stream_entry.created_at, target.stream_entry.activity_id, target.stream_entry.activity_type)
end
diff --git a/app/models/stream_entry.rb b/app/models/stream_entry.rb
index ae7ae446e9b..8aff5ae0698 100644
--- a/app/models/stream_entry.rb
+++ b/app/models/stream_entry.rb
@@ -5,25 +5,21 @@ class StreamEntry < ApplicationRecord
belongs_to :account, inverse_of: :stream_entries
belongs_to :activity, polymorphic: true
-
belongs_to :status, foreign_type: 'Status', foreign_key: 'activity_id', inverse_of: :stream_entry
validates :account, :activity, presence: true
- STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, mentions: :account], thread: [:stream_entry, :account]].freeze
+ STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :media_attachments, :tags, mentions: :account], thread: [:stream_entry, :account]].freeze
+ default_scope { where(activity_type: 'Status') }
scope :with_includes, -> { includes(:account, status: STATUS_INCLUDES) }
def object_type
- if orphaned?
- :activity
- else
- targeted? ? :activity : activity.object_type
- end
+ orphaned? || targeted? ? :activity : status.object_type
end
def verb
- orphaned? ? :delete : activity.verb
+ orphaned? ? :delete : status.verb
end
def targeted?
@@ -31,15 +27,15 @@ class StreamEntry < ApplicationRecord
end
def target
- orphaned? ? nil : activity.target
+ orphaned? ? nil : status.target
end
def title
- orphaned? ? nil : activity.title
+ orphaned? ? nil : status.title
end
def content
- orphaned? ? nil : activity.content
+ orphaned? ? nil : sta