about summary refs log tree commit diff
path: root/app/services/resolve_account_service.rb
blob: 4323e7f06d57f77c8fe37a618ba463a7faddf3c8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# 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
      else
        raise Mastodon::RaceConditionError
      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)

    @atom_body = Request.new(:get, atom_url).perform do |response|
      raise Mastodon::UnexpectedResponseError, response unless response.code == 200
      response.body_with_limit
    end
  end

  def actor_json
    return @actor_json if defined?(@actor_json)

    json        = fetch_resource(actor_url, false)
    @actor_json = supported_context?(json) && equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) ? 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