about summary refs log tree commit diff
path: root/app/lib
diff options
context:
space:
mode:
Diffstat (limited to 'app/lib')
-rw-r--r--app/lib/activitypub/activity/announce.rb2
-rw-r--r--app/lib/activitypub/activity/create.rb14
-rw-r--r--app/lib/activitypub/tag_manager.rb4
-rw-r--r--app/lib/emoji.rb40
-rw-r--r--app/lib/formatter.rb10
-rw-r--r--app/lib/ostatus/activity/base.rb18
-rw-r--r--app/lib/ostatus/activity/creation.rb54
-rw-r--r--app/lib/ostatus/activity/share.rb2
-rw-r--r--app/lib/ostatus/atom_serializer.rb106
-rw-r--r--app/lib/ostatus/tag_manager.rb73
-rw-r--r--app/lib/tag_manager.rb67
11 files changed, 192 insertions, 198 deletions
diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb
index 556f91235..4516454e1 100644
--- a/app/lib/activitypub/activity/announce.rb
+++ b/app/lib/activitypub/activity/announce.rb
@@ -25,6 +25,8 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
 
   def fetch_remote_original_status
     if object_uri.start_with?('http')
+      return if ActivityPub::TagManager.instance.local_uri?(object_uri)
+
       ActivityPub::FetchRemoteStatusService.new.call(object_uri)
     elsif @object['url'].present?
       ::FetchRemoteStatusService.new.call(@object['url'])
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index 41f2b0bad..4e19b3096 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -68,6 +68,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
   end
 
   def process_hashtag(tag, status)
+    return if tag['name'].blank?
+
     hashtag = tag['name'].gsub(/\A#/, '').mb_chars.downcase
     hashtag = Tag.where(name: hashtag).first_or_initialize(name: hashtag)
 
@@ -75,6 +77,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
   end
 
   def process_mention(tag, status)
+    return if tag['href'].blank?
+
     account = account_from_uri(tag['href'])
     account = FetchRemoteAccountService.new.call(tag['href']) if account.nil?
     return if account.nil?
@@ -82,6 +86,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
   end
 
   def process_emoji(tag, _status)
+    return if tag['name'].blank? || tag['href'].blank?
+
     shortcode = tag['name'].delete(':')
     emoji     = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain)
 
@@ -96,7 +102,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     return unless @object['attachment'].is_a?(Array)
 
     @object['attachment'].each do |attachment|
-      next if unsupported_media_type?(attachment['mediaType'])
+      next if unsupported_media_type?(attachment['mediaType']) || attachment['url'].blank?
 
       href             = Addressable::URI.parse(attachment['url']).normalize.to_s
       media_attachment = MediaAttachment.create(status: status, account: status.account, remote_url: href)
@@ -106,6 +112,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
       media_attachment.file_remote_url = href
       media_attachment.save
     end
+  rescue Addressable::URI::InvalidURIError => e
+    Rails.logger.debug e
   end
 
   def resolve_thread(status)
@@ -115,8 +123,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
 
   def conversation_from_uri(uri)
     return nil if uri.nil?
-    return Conversation.find_by(id: TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if TagManager.instance.local_id?(uri)
-    Conversation.find_by(uri: uri) || Conversation.create!(uri: uri)
+    return Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if OStatus::TagManager.instance.local_id?(uri)
+    Conversation.find_by(uri: uri) || Conversation.create(uri: uri)
   end
 
   def visibility_from_audience
diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb
index 1b4e271db..4ec3b8c56 100644
--- a/app/lib/activitypub/tag_manager.rb
+++ b/app/lib/activitypub/tag_manager.rb
@@ -98,8 +98,8 @@ class ActivityPub::TagManager
       else
         StatusFinder.new(uri).status
       end
-    elsif ::TagManager.instance.local_id?(uri)
-      klass.find_by(id: ::TagManager.instance.unique_tag_to_local_id(uri, klass.to_s))
+    elsif OStatus::TagManager.instance.local_id?(uri)
+      klass.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, klass.to_s))
     else
       klass.find_by(uri: uri.split('#').first)
     end
diff --git a/app/lib/emoji.rb b/app/lib/emoji.rb
deleted file mode 100644
index 45b7f53de..000000000
--- a/app/lib/emoji.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-require 'singleton'
-
-class Emoji
-  include Singleton
-
-  def initialize
-    data = Oj.load(File.open(Rails.root.join('lib', 'assets', 'emoji.json')))
-
-    @map = {}
-
-    data.each do |_, emoji|
-      keys    = [emoji['shortname']] + emoji['aliases']
-      unicode = codepoint_to_unicode(emoji['unicode'])
-
-      keys.each do |key|
-        @map[key] = unicode
-      end
-    end
-  end
-
-  def unicode(shortcode)
-    @map[shortcode]
-  end
-
-  def names
-    @map.keys
-  end
-
-  private
-
-  def codepoint_to_unicode(codepoint)
-    if codepoint.include?('-')
-      codepoint.split('-').map(&:hex).pack('U*')
-    else
-      [codepoint.hex].pack('U')
-    end
-  end
-end
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index 29fea27de..42cd72990 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -22,7 +22,7 @@ class Formatter
     unless status.local?
       html = reformat(raw_content)
       html = encode_custom_emojis(html, status.emojis) if options[:custom_emojify]
-      return html
+      return html.html_safe # rubocop:disable Rails/OutputSafety
     end
 
     linkable_accounts = status.mentions.map(&:account)
@@ -39,7 +39,7 @@ class Formatter
   end
 
   def reformat(html)
-    sanitize(html, Sanitize::Config::MASTODON_STRICT).html_safe # rubocop:disable Rails/OutputSafety
+    sanitize(html, Sanitize::Config::MASTODON_STRICT)
   end
 
   def plaintext(status)
@@ -63,6 +63,12 @@ class Formatter
     Sanitize.fragment(html, config)
   end
 
+  def format_spoiler(status)
+    html = encode(status.spoiler_text)
+    html = encode_custom_emojis(html, status.emojis)
+    html.html_safe # rubocop:disable Rails/OutputSafety
+  end
+
   private
 
   def encode(html)
diff --git a/app/lib/ostatus/activity/base.rb b/app/lib/ostatus/activity/base.rb
index 1dc7abee3..039381397 100644
--- a/app/lib/ostatus/activity/base.rb
+++ b/app/lib/ostatus/activity/base.rb
@@ -11,30 +11,30 @@ class OStatus::Activity::Base
   end
 
   def verb
-    raw = @xml.at_xpath('./activity:verb', activity: TagManager::AS_XMLNS).content
-    TagManager::VERBS.key(raw)
+    raw = @xml.at_xpath('./activity:verb', activity: OStatus::TagManager::AS_XMLNS).content
+    OStatus::TagManager::VERBS.key(raw)
   rescue
     :post
   end
 
   def type
-    raw = @xml.at_xpath('./activity:object-type', activity: TagManager::AS_XMLNS).content
-    TagManager::TYPES.key(raw)
+    raw = @xml.at_xpath('./activity:object-type', activity: OStatus::TagManager::AS_XMLNS).content
+    OStatus::TagManager::TYPES.key(raw)
   rescue
     :activity
   end
 
   def id
-    @xml.at_xpath('./xmlns:id', xmlns: TagManager::XMLNS).content
+    @xml.at_xpath('./xmlns:id', xmlns: OStatus::TagManager::XMLNS).content
   end
 
   def url
-    link = @xml.xpath('./xmlns:link[@rel="alternate"]', xmlns: TagManager::XMLNS).find { |link_candidate| link_candidate['type'] == 'text/html' }
+    link = @xml.xpath('./xmlns:link[@rel="alternate"]', xmlns: OStatus::TagManager::XMLNS).find { |link_candidate| link_candidate['type'] == 'text/html' }
     link.nil? ? nil : link['href']
   end
 
   def activitypub_uri
-    link = @xml.xpath('./xmlns:link[@rel="alternate"]', xmlns: TagManager::XMLNS).find { |link_candidate| ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(link_candidate['type']) }
+    link = @xml.xpath('./xmlns:link[@rel="alternate"]', xmlns: OStatus::TagManager::XMLNS).find { |link_candidate| ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(link_candidate['type']) }
     link.nil? ? nil : link['href']
   end
 
@@ -45,8 +45,8 @@ class OStatus::Activity::Base
   private
 
   def find_status(uri)
-    if TagManager.instance.local_id?(uri)
-      local_id = TagManager.instance.unique_tag_to_local_id(uri, 'Status')
+    if OStatus::TagManager.instance.local_id?(uri)
+      local_id = OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Status')
       return Status.find_by(id: local_id)
     elsif ActivityPub::TagManager.instance.local_uri?(uri)
       local_id = ActivityPub::TagManager.instance.uri_to_local_id(uri)
diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb
index d3f1629c4..2687776f9 100644
--- a/app/lib/ostatus/activity/creation.rb
+++ b/app/lib/ostatus/activity/creation.rb
@@ -14,14 +14,22 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
       return result if result.first.present?
     end
 
-    Rails.logger.debug "Creating remote status #{id}"
-
-    # Return early if status already exists in db
-    status = find_status(id)
+    RedisLock.acquire(lock_options) do |lock|
+      if lock.acquired?
+        # Return early if status already exists in db
+        @status = find_status(id)
+        return [@status, false] unless @status.nil?
+        @status = process_status
+      end
+    end
 
-    return [status, false] unless status.nil?
+    [@status, true]
+  end
 
+  def process_status
+    Rails.logger.debug "Creating remote status #{id}"
     cached_reblog = reblog
+    status = nil
 
     ApplicationRecord.transaction do
       status = Status.create!(
@@ -55,7 +63,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
     LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text?
     DistributionWorker.perform_async(status.id)
 
-    [status, true]
+    status
   end
 
   def perform_via_activitypub
@@ -63,42 +71,42 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
   end
 
   def content
-    @xml.at_xpath('./xmlns:content', xmlns: TagManager::XMLNS).content
+    @xml.at_xpath('./xmlns:content', xmlns: OStatus::TagManager::XMLNS).content
   end
 
   def content_language
-    @xml.at_xpath('./xmlns:content', xmlns: TagManager::XMLNS)['xml:lang']&.presence || 'en'
+    @xml.at_xpath('./xmlns:content', xmlns: OStatus::TagManager::XMLNS)['xml:lang']&.presence || 'en'
   end
 
   def content_warning
-    @xml.at_xpath('./xmlns:summary', xmlns: TagManager::XMLNS)&.content || ''
+    @xml.at_xpath('./xmlns:summary', xmlns: OStatus::TagManager::XMLNS)&.content || ''
   end
 
   def visibility_scope
-    @xml.at_xpath('./mastodon:scope', mastodon: TagManager::MTDN_XMLNS)&.content&.to_sym || :public
+    @xml.at_xpath('./mastodon:scope', mastodon: OStatus::TagManager::MTDN_XMLNS)&.content&.to_sym || :public
   end
 
   def published
-    @xml.at_xpath('./xmlns:published', xmlns: TagManager::XMLNS).content
+    @xml.at_xpath('./xmlns:published', xmlns: OStatus::TagManager::XMLNS).content
   end
 
   def thread?
-    !@xml.at_xpath('./thr:in-reply-to', thr: TagManager::THR_XMLNS).nil?
+    !@xml.at_xpath('./thr:in-reply-to', thr: OStatus::TagManager::THR_XMLNS).nil?
   end
 
   def thread
-    thr = @xml.at_xpath('./thr:in-reply-to', thr: TagManager::THR_XMLNS)
+    thr = @xml.at_xpath('./thr:in-reply-to', thr: OStatus::TagManager::THR_XMLNS)
     [thr['ref'], thr['href']]
   end
 
   private
 
   def find_or_create_conversation
-    uri = @xml.at_xpath('./ostatus:conversation', ostatus: TagManager::OS_XMLNS)&.attribute('ref')&.content
+    uri = @xml.at_xpath('./ostatus:conversation', ostatus: OStatus::TagManager::OS_XMLNS)&.attribute('ref')&.content
     return if uri.nil?
 
-    if TagManager.instance.local_id?(uri)
-      local_id = TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')
+    if OStatus::TagManager.instance.local_id?(uri)
+      local_id = OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')
       return Conversation.find_by(id: local_id)
     end
 
@@ -108,8 +116,8 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
   def save_mentions(parent)
     processed_account_ids = []
 
-    @xml.xpath('./xmlns:link[@rel="mentioned"]', xmlns: TagManager::XMLNS).each do |link|
-      next if [TagManager::TYPES[:group], TagManager::TYPES[:collection]].include? link['ostatus:object-type']
+    @xml.xpath('./xmlns:link[@rel="mentioned"]', xmlns: OStatus::TagManager::XMLNS).each do |link|
+      next if [OStatus::TagManager::TYPES[:group], OStatus::TagManager::TYPES[:collection]].include? link['ostatus:object-type']
 
       mentioned_account = account_from_href(link['href'])
 
@@ -123,14 +131,14 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
   end
 
   def save_hashtags(parent)
-    tags = @xml.xpath('./xmlns:category', xmlns: TagManager::XMLNS).map { |category| category['term'] }.select(&:present?)
+    tags = @xml.xpath('./xmlns:category', xmlns: OStatus::TagManager::XMLNS).map { |category| category['term'] }.select(&:present?)
     ProcessHashtagsService.new.call(parent, tags)
   end
 
   def save_media(parent)
     do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media?
 
-    @xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: TagManager::XMLNS).each do |link|
+    @xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: OStatus::TagManager::XMLNS).each do |link|
       next unless link['href']
 
       media = MediaAttachment.where(status: parent, remote_url: link['href']).first_or_initialize(account: parent.account, status: parent, remote_url: link['href'])
@@ -156,7 +164,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
 
     return if do_not_download
 
-    @xml.xpath('./xmlns:link[@rel="emoji"]', xmlns: TagManager::XMLNS).each do |link|
+    @xml.xpath('./xmlns:link[@rel="emoji"]', xmlns: OStatus::TagManager::XMLNS).each do |link|
       next unless link['href'] && link['name']
 
       shortcode = link['name'].delete(':')
@@ -179,4 +187,8 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
       Account.where(uri: href).or(Account.where(url: href)).first || FetchRemoteAccountService.new.call(href)
     end
   end
+
+  def lock_options
+    { redis: Redis.current, key: "create:#{id}" }
+  end
 end
diff --git a/app/lib/ostatus/activity/share.rb b/app/lib/ostatus/activity/share.rb
index 290008021..5ca601415 100644
--- a/app/lib/ostatus/activity/share.rb
+++ b/app/lib/ostatus/activity/share.rb
@@ -10,7 +10,7 @@ class OStatus::Activity::Share < OStatus::Activity::Creation
   end
 
   def object
-    @xml.at_xpath('.//activity:object', activity: TagManager::AS_XMLNS)
+    @xml.at_xpath('.//activity:object', activity: OStatus::TagManager::AS_XMLNS)
   end
 
   private
diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb
index a6a5cb0c4..a1ac11a51 100644
--- a/app/lib/ostatus/atom_serializer.rb
+++ b/app/lib/ostatus/atom_serializer.rb
@@ -15,10 +15,10 @@ class OStatus::AtomSerializer
   def author(account)
     author = Ox::Element.new('author')
 
-    uri = TagManager.instance.uri_for(account)
+    uri = OStatus::TagManager.instance.uri_for(account)
 
     append_element(author, 'id', uri)
-    append_element(author, 'activity:object-type', TagManager::TYPES[:person])
+    append_element(author, 'activity:object-type', OStatus::TagManager::TYPES[:person])
     append_element(author, 'uri', uri)
     append_element(author, 'name', account.username)
     append_element(author, 'email', account.local? ? account.local_username_and_domain : account.acct)
@@ -65,15 +65,15 @@ class OStatus::AtomSerializer
 
     add_namespaces(entry) if root
 
-    append_element(entry, 'id', TagManager.instance.uri_for(stream_entry.status))
+    append_element(entry, 'id', OStatus::TagManager.instance.uri_for(stream_entry.status))
     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 || "#{stream_entry.account.acct} deleted status")
 
     entry << author(stream_entry.account) if root
 
-    append_element(entry, 'activity:object-type', TagManager::TYPES[stream_entry.object_type])
-    append_element(entry, 'activity:verb', TagManager::VERBS[stream_entry.verb])
+    append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[stream_entry.object_type])
+    append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[stream_entry.verb])
 
     entry << object(stream_entry.target) if stream_entry.targeted?
 
@@ -88,7 +88,7 @@ class OStatus::AtomSerializer
 
     append_element(entry, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(stream_entry.status))
     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?
+    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?
     append_element(entry, 'ostatus:conversation', nil, ref: conversation_uri(stream_entry.status.conversation)) unless stream_entry&.status&.conversation_id.nil?
 
     entry
@@ -97,20 +97,20 @@ class OStatus::AtomSerializer
   def object(status)
     object = Ox::Element.new('activity:object')
 
-    append_element(object, 'id', TagManager.instance.uri_for(status))
+    append_element(object, 'id', OStatus::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])
+    append_element(object, 'activity:object-type', OStatus::TagManager::TYPES[status.object_type])
+    append_element(object, 'activity:verb', OStatus::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)) unless status.thread.nil?
+    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?
     append_element(object, 'ostatus:conversation', nil, ref: conversation_uri(status.conversation)) unless status.conversation_id.nil?
 
     object
@@ -122,14 +122,14 @@ class OStatus::AtomSerializer
 
     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, 'id', OStatus::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])
+    append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
+    append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:follow])
 
     object = author(follow.target_account)
     object.value = 'activity:object'
@@ -142,13 +142,13 @@ class OStatus::AtomSerializer
     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, 'id', OStatus::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])
+    append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
+    append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:request_friend])
 
     object = author(follow_request.target_account)
     object.value = 'activity:object'
@@ -161,19 +161,19 @@ class OStatus::AtomSerializer
     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, 'id', OStatus::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])
+    append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
+    append_element(entry, 'activity:verb', OStatus::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])
+    append_element(object, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
+    append_element(object, 'activity:verb', OStatus::TagManager::VERBS[:request_friend])
 
     inner_object = author(follow_request.target_account)
     inner_object.value = 'activity:object'
@@ -187,19 +187,19 @@ class OStatus::AtomSerializer
     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, 'id', OStatus::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])
+    append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
+    append_element(entry, 'activity:verb', OStatus::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])
+    append_element(object, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
+    append_element(object, 'activity:verb', OStatus::TagManager::VERBS[:request_friend])
 
     inner_object = author(follow_request.target_account)
     inner_object.value = 'activity:object'
@@ -215,14 +215,14 @@ class OStatus::AtomSerializer
 
     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, 'id', OStatus::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])
+    append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
+    append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:unfollow])
 
     object = author(follow.target_account)
     object.value = 'activity:object'
@@ -237,13 +237,13 @@ class OStatus::AtomSerializer
 
     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, 'id', OStatus::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])
+    append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
+    append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:block])
 
     object = author(block.target_account)
     object.value = 'activity:object'
@@ -258,13 +258,13 @@ class OStatus::AtomSerializer
 
     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, 'id', OStatus::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])
+    append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
+    append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:unblock])
 
     object = author(block.target_account)
     object.value = 'activity:object'
@@ -279,18 +279,18 @@ class OStatus::AtomSerializer
 
     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, 'id', OStatus::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])
+    append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
+    append_element(entry, 'activity:verb', OStatus::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))
+    append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(favourite.status), href: TagManager.instance.url_for(favourite.status))
 
     entry
   end
@@ -301,18 +301,18 @@ class OStatus::AtomSerializer
 
     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, 'id', OStatus::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])
+    append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
+    append_element(entry, 'activity:verb', OStatus::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))
+    append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(favourite.status), href: TagManager.instance.url_for(favourite.status))
 
     entry
   end
@@ -332,17 +332,17 @@ class OStatus::AtomSerializer
 
   def conversation_uri(conversation)
     return conversation.uri if conversation.uri?
-    TagManager.instance.unique_tag(conversation.created_at, conversation.id, 'Conversation')
+    OStatus::TagManager.instance.unique_tag(conversation.created_at, conversation.id, 'Conversation')
   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
+    parent['xmlns']          = OStatus::TagManager::XMLNS
+    parent['xmlns:thr']      = OStatus::TagManager::THR_XMLNS
+    parent['xmlns:activity'] = OStatus::TagManager::AS_XMLNS
+    parent['xmlns:poco']     = OStatus::TagManager::POCO_XMLNS
+    parent['xmlns:media']    = OStatus::TagManager::MEDIA_XMLNS
+    parent['xmlns:ostatus']  = OStatus::TagManager::OS_XMLNS
+    parent['xmlns:mastodon'] = OStatus::TagManager::MTDN_XMLNS
   end
 
   def serialize_status_attributes(entry, status)
@@ -352,10 +352,10 @@ class OStatus::AtomSerializer
     append_element(entry, 'content', Formatter.instance.format(status).to_str, type: 'html', 'xml:lang': status.language)
 
     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))
+      append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': OStatus::TagManager::TYPES[:person], href: OStatus::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?
+    append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': OStatus::TagManager::TYPES[:collection], href: OStatus::TagManager::COLLECTIONS[:public]) if status.public_visibility?
 
     status.tags.each do |tag|
       append_element(entry, 'category', nil, term: tag.name)
diff --git a/app/lib/ostatus/tag_manager.rb b/app/lib/ostatus/tag_manager.rb
new file mode 100644
index 000000000..4f4501312
--- /dev/null
+++ b/app/lib/ostatus/tag_manager.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+class OStatus::TagManager
+  include Singleton
+  include RoutingHelper
+
+  VERBS = {
+    post:           'http://activitystrea.ms/schema/1.0/post',
+    share:          'http://activitystrea.ms/schema/1.0/share',
+    favorite:       'http://activitystrea.ms/schema/1.0/favorite',
+    unfavorite:     'http://activitystrea.ms/schema/1.0/unfavorite',
+    delete:         'http://activitystrea.ms/schema/1.0/delete',
+    follow:         'http://activitystrea.ms/schema/1.0/follow',
+    request_friend: 'http://activitystrea.ms/schema/1.0/request-friend',
+    authorize:      'http://activitystrea.ms/schema/1.0/authorize',
+    reject:         'http://activitystrea.ms/schema/1.0/reject',
+    unfollow:       'http://ostatus.org/schema/1.0/unfollow',
+    block:          'http://mastodon.social/schema/1.0/block',
+    unblock:        'http://mastodon.social/schema/1.0/unblock',
+  }.freeze
+
+  TYPES = {
+    activity:   'http://activitystrea.ms/schema/1.0/activity',
+    note:       'http://activitystrea.ms/schema/1.0/note',
+    comment:    'http://activitystrea.ms/schema/1.0/comment',
+    person:     'http://activitystrea.ms/schema/1.0/person',
+    collection: 'http://activitystrea.ms/schema/1.0/collection',
+    group:      'http://activitystrea.ms/schema/1.0/group',
+  }.freeze
+
+  COLLECTIONS = {
+    public: 'http://activityschema.org/collection/public',
+  }.freeze
+
+  XMLNS       = 'http://www.w3.org/2005/Atom'
+  MEDIA_XMLNS = 'http://purl.org/syndication/atommedia'
+  AS_XMLNS    = 'http://activitystrea.ms/spec/1.0/'
+  THR_XMLNS   = 'http://purl.org/syndication/thread/1.0'
+  POCO_XMLNS  = 'http://portablecontacts.net/spec/1.0'
+  DFRN_XMLNS  = 'http://purl.org/macgirvin/dfrn/1.0'
+  OS_XMLNS    = 'http://ostatus.org/schema/1.0'
+  MTDN_XMLNS  = 'http://mastodon.social/schema/1.0'
+
+  def unique_tag(date, id, type)
+    "tag:#{Rails.configuration.x.local_domain},#{date.strftime('%Y-%m-%d')}:objectId=#{id}:objectType=#{type}"
+  end
+
+  def unique_tag_to_local_id(tag, expected_type)
+    return nil unless local_id?(tag)
+
+    if ActivityPub::TagManager.instance.local_uri?(tag)
+      ActivityPub::TagManager.instance.uri_to_local_id(tag)
+    else
+      matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag)
+      return matches[1] unless matches.nil?
+    end
+  end
+
+  def local_id?(id)
+    id.start_with?("tag:#{Rails.configuration.x.local_domain}") || ActivityPub::TagManager.instance.local_uri?(id)
+  end
+
+  def uri_for(target)
+    return target.uri if target.respond_to?(:local?) && !target.local?
+
+    case target.object_type
+    when :person
+      account_url(target)
+    when :note, :comment, :activity
+      target.uri || unique_tag(target.created_at, target.id, 'Status')
+    end
+  end
+end
diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb
index 1d0a24e42..fb364cb98 100644
--- a/app/lib/tag_manager.rb
+++ b/app/lib/tag_manager.rb
@@ -6,62 +6,6 @@ class TagManager
   include Singleton
   include RoutingHelper
 
-  VERBS = {
-    post:           'http://activitystrea.ms/schema/1.0/post',
-    share:          'http://activitystrea.ms/schema/1.0/share',
-    favorite:       'http://activitystrea.ms/schema/1.0/favorite',
-    unfavorite:     'http://activitystrea.ms/schema/1.0/unfavorite',
-    delete:         'http://activitystrea.ms/schema/1.0/delete',
-    follow:         'http://activitystrea.ms/schema/1.0/follow',
-    request_friend: 'http://activitystrea.ms/schema/1.0/request-friend',
-    authorize:      'http://activitystrea.ms/schema/1.0/authorize',
-    reject:         'http://activitystrea.ms/schema/1.0/reject',
-    unfollow:       'http://ostatus.org/schema/1.0/unfollow',
-    block:          'http://mastodon.social/schema/1.0/block',
-    unblock:        'http://mastodon.social/schema/1.0/unblock',
-  }.freeze
-
-  TYPES = {
-    activity:   'http://activitystrea.ms/schema/1.0/activity',
-    note:       'http://activitystrea.ms/schema/1.0/note',
-    comment:    'http://activitystrea.ms/schema/1.0/comment',
-    person:     'http://activitystrea.ms/schema/1.0/person',
-    collection: 'http://activitystrea.ms/schema/1.0/collection',
-    group:      'http://activitystrea.ms/schema/1.0/group',
-  }.freeze
-
-  COLLECTIONS = {
-    public: 'http://activityschema.org/collection/public',
-  }.freeze
-
-  XMLNS       = 'http://www.w3.org/2005/Atom'
-  MEDIA_XMLNS = 'http://purl.org/syndication/atommedia'
-  AS_XMLNS    = 'http://activitystrea.ms/spec/1.0/'
-  THR_XMLNS   = 'http://purl.org/syndication/thread/1.0'
-  POCO_XMLNS  = 'http://portablecontacts.net/spec/1.0'
-  DFRN_XMLNS  = 'http://purl.org/macgirvin/dfrn/1.0'
-  OS_XMLNS    = 'http://ostatus.org/schema/1.0'
-  MTDN_XMLNS  = 'http://mastodon.social/schema/1.0'
-
-  def unique_tag(date, id, type)
-    "tag:#{Rails.configuration.x.local_domain},#{date.strftime('%Y-%m-%d')}:objectId=#{id}:objectType=#{type}"
-  end
-
-  def unique_tag_to_local_id(tag, expected_type)
-    return nil unless local_id?(tag)
-
-    if ActivityPub::TagManager.instance.local_uri?(tag)
-      ActivityPub::TagManager.instance.uri_to_local_id(tag)
-    else
-      matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag)
-      return matches[1] unless matches.nil?
-    end
-  end
-
-  def local_id?(id)
-    id.start_with?("tag:#{Rails.configuration.x.local_domain}") || ActivityPub::TagManager.instance.local_uri?(id)
-  end
-
   def web_domain?(domain)
     domain.nil? || domain.gsub(/[\/]/, '').casecmp(Rails.configuration.x.web_domain).zero?
   end
@@ -90,17 +34,6 @@ class TagManager
     TagManager.instance.web_domain?(domain)
   end
 
-  def uri_for(target)
-    return target.uri if target.respond_to?(:local?) && !target.local?
-
-    case target.object_type
-    when :person
-      account_url(target)
-    when :note, :comment, :activity
-      target.uri || unique_tag(target.created_at, target.id, 'Status')
-    end
-  end
-
   def url_for(target)
     return target.url if target.respond_to?(:local?) && !target.local?