about summary refs log tree commit diff
path: root/app/services/resolve_account_service.rb
diff options
context:
space:
mode:
authorAkihiko Odaki <akihiko.odaki.4i@stu.hosei.ac.jp>2018-01-22 22:25:09 +0900
committerEugen Rochko <eugen@zeonfederated.com>2018-01-22 14:25:09 +0100
commit613e7c7521252bd85e473d9c63cbc8b8e1a733a8 (patch)
tree521ac854f97d34dc96b77bd625a38e9bef416574 /app/services/resolve_account_service.rb
parent17cecd75cadfd9914ffc233de01d41204ef7802c (diff)
Rename ResolveRemoteAccountService to ResolveAccountService (#6327)
The service used to be named ResolveRemoteAccountService resolves local
accounts as well.
Diffstat (limited to 'app/services/resolve_account_service.rb')
-rw-r--r--app/services/resolve_account_service.rb208
1 files changed, 208 insertions, 0 deletions
diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb
new file mode 100644
index 000000000..fd6d30605
--- /dev/null
+++ b/app/services/resolve_account_service.rb
@@ -0,0 +1,208 @@
+# frozen_string_literal: true
+
+class ResolveAccountService < BaseService
+  include OStatus2::MagicKey
+  include JsonLdHelper
+
+  DFRN_NS = 'http://purl.org/macgirvin/dfrn/1.0'
+
+  # Find or create a local account for a remote user.
+  # When creating, look up the user's webfinger and fetch all
+  # important information from their feed
+  # @param [String] uri User URI in the form of username@domain
+  # @return [Account]
+  def call(uri, update_profile = true, redirected = nil)
+    @username, @domain = uri.split('@')
+    @update_profile    = update_profile
+
+    return Account.find_local(@username) if TagManager.instance.local_domain?(@domain)
+
+    @account = Account.find_remote(@username, @domain)
+
+    return @account unless webfinger_update_due?
+
+    Rails.logger.debug "Looking up webfinger for #{uri}"
+
+    @webfinger = Goldfinger.finger("acct:#{uri}")
+
+    confirmed_username, confirmed_domain = @webfinger.subject.gsub(/\Aacct:/, '').split('@')
+
+    if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
+      @username = confirmed_username
+      @domain   = confirmed_domain
+    elsif redirected.nil?
+      return call("#{confirmed_username}@#{confirmed_domain}", update_profile, true)
+    else
+      Rails.logger.debug 'Requested and returned acct URIs do not match'
+      return
+    end
+
+    return if links_missing?
+    return Account.find_local(@username) if TagManager.instance.local_domain?(@domain)
+
+    RedisLock.acquire(lock_options) do |lock|
+      if lock.acquired?
+        @account = Account.find_remote(@username, @domain)
+
+        if activitypub_ready? || @account&.activitypub?
+          handle_activitypub
+        else
+          handle_ostatus
+        end
+      end
+    end
+
+    @account
+  rescue Goldfinger::Error => e
+    Rails.logger.debug "Webfinger query for #{uri} unsuccessful: #{e}"
+    nil
+  end
+
+  private
+
+  def links_missing?
+    !(activitypub_ready? || ostatus_ready?)
+  end
+
+  def ostatus_ready?
+    !(@webfinger.link('http://schemas.google.com/g/2010#updates-from').nil? ||
+      @webfinger.link('salmon').nil? ||
+      @webfinger.link('http://webfinger.net/rel/profile-page').nil? ||
+      @webfinger.link('magic-public-key').nil? ||
+      canonical_uri.nil? ||
+      hub_url.nil?)
+  end
+
+  def webfinger_update_due?
+    @account.nil? || @account.possibly_stale?
+  end
+
+  def activitypub_ready?
+    !@webfinger.link('self').nil? &&
+      ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self').type) &&
+      !actor_json.nil? &&
+      actor_json['inbox'].present?
+  end
+
+  def handle_ostatus
+    create_account if @account.nil?
+    update_account
+    update_account_profile if update_profile?
+  end
+
+  def update_profile?
+    @update_profile
+  end
+
+  def handle_activitypub
+    return if actor_json.nil?
+
+    @account = ActivityPub::ProcessAccountService.new.call(@username, @domain, actor_json)
+  rescue Oj::ParseError
+    nil
+  end
+
+  def create_account
+    Rails.logger.debug "Creating new remote account for #{@username}@#{@domain}"
+
+    @account = Account.new(username: @username, domain: @domain)
+    @account.suspended   = true if auto_suspend?
+    @account.silenced    = true if auto_silence?
+    @account.private_key = nil
+  end
+
+  def update_account
+    @account.last_webfingered_at = Time.now.utc
+    @account.protocol            = :ostatus
+    @account.remote_url          = atom_url
+    @account.salmon_url          = salmon_url
+    @account.url                 = url
+    @account.public_key          = public_key
+    @account.uri                 = canonical_uri
+    @account.hub_url             = hub_url
+    @account.save!
+  end
+
+  def auto_suspend?
+    domain_block&.suspend?
+  end
+
+  def auto_silence?
+    domain_block&.silence?
+  end
+
+  def domain_block
+    return @domain_block if defined?(@domain_block)
+    @domain_block = DomainBlock.find_by(domain: @domain)
+  end
+
+  def atom_url
+    @atom_url ||= @webfinger.link('http://schemas.google.com/g/2010#updates-from').href
+  end
+
+  def salmon_url
+    @salmon_url ||= @webfinger.link('salmon').href
+  end
+
+  def actor_url
+    @actor_url ||= @webfinger.link('self').href
+  end
+
+  def url
+    @url ||= @webfinger.link('http://webfinger.net/rel/profile-page').href
+  end
+
+  def public_key
+    @public_key ||= magic_key_to_pem(@webfinger.link('magic-public-key').href)
+  end
+
+  def canonical_uri
+    return @canonical_uri if defined?(@canonical_uri)
+
+    author_uri = atom.at_xpath('/xmlns:feed/xmlns:author/xmlns:uri')
+
+    if author_uri.nil?
+      owner      = atom.at_xpath('/xmlns:feed').at_xpath('./dfrn:owner', dfrn: DFRN_NS)
+      author_uri = owner.at_xpath('./xmlns:uri') unless owner.nil?
+    end
+
+    @canonical_uri = author_uri.nil? ? nil : author_uri.content
+  end
+
+  def hub_url
+    return @hub_url if defined?(@hub_url)
+
+    hubs     = atom.xpath('//xmlns:link[@rel="hub"]')
+    @hub_url = hubs.empty? || hubs.first['href'].nil? ? nil : hubs.first['href']
+  end
+
+  def atom_body
+    return @atom_body if defined?(@atom_body)
+
+    response = Request.new(:get, atom_url).perform
+
+    raise Mastodon::UnexpectedResponseError, response unless response.code == 200
+
+    @atom_body = response.to_s
+  end
+
+  def actor_json
+    return @actor_json if defined?(@actor_json)
+
+    json        = fetch_resource(actor_url, false)
+    @actor_json = supported_context?(json) && json['type'] == 'Person' ? json : nil
+  end
+
+  def atom
+    return @atom if defined?(@atom)
+    @atom = Nokogiri::XML(atom_body)
+  end
+
+  def update_account_profile
+    RemoteProfileUpdateWorker.perform_async(@account.id, atom_body.force_encoding('UTF-8'), false)
+  end
+
+  def lock_options
+    { redis: Redis.current, key: "resolve:#{@username}@#{@domain}" }
+  end
+end