process_account_service.rb 8.46 KB
Newer Older
Eugen Rochko's avatar
Eugen Rochko committed
1 2 3 4
# frozen_string_literal: true

class ActivityPub::ProcessAccountService < BaseService
  include JsonLdHelper
5
  include DomainControlHelper
Eugen Rochko's avatar
Eugen Rochko committed
6 7 8

  # Should be called with confirmed valid JSON
  # and WebFinger-resolved username and domain
9
  def call(username, domain, json, options = {})
10
    return if json['inbox'].blank? || unsupported_uri_scheme?(json['id']) || domain_not_allowed?(domain)
11

12
    @options     = options
13 14 15 16 17
    @json        = json
    @uri         = @json['id']
    @username    = username
    @domain      = domain
    @collections = {}
Eugen Rochko's avatar
Eugen Rochko committed
18

19 20
    RedisLock.acquire(lock_options) do |lock|
      if lock.acquired?
21 22 23 24
        @account          = Account.remote.find_by(uri: @uri) if @options[:only_key]
        @account        ||= Account.find_remote(@username, @domain)
        @old_public_key   = @account&.public_key
        @old_protocol     = @account&.protocol
25 26 27

        create_account if @account.nil?
        update_account
28
        process_tags
29
        process_attachments
30 31
      else
        raise Mastodon::RaceConditionError
32 33 34
      end
    end

35 36
    return if @account.nil?

37
    after_protocol_change! if protocol_changed?
38
    after_key_change! if key_changed? && !@options[:signed_with_known_key]
39 40
    clear_tombstones! if key_changed?

41 42 43 44
    unless @options[:only_key]
      check_featured_collection! if @account.featured_collection_url.present?
      check_links! unless @account.fields.empty?
    end
Eugen Rochko's avatar
Eugen Rochko committed
45 46 47 48 49 50 51 52 53 54

    @account
  rescue Oj::ParseError
    nil
  end

  private

  def create_account
    @account = Account.new
55 56 57 58 59
    @account.protocol     = :activitypub
    @account.username     = @username
    @account.domain       = @domain
    @account.private_key  = nil
    @account.suspended_at = domain_block.created_at if auto_suspend?
60
    @account.silenced_at  = domain_block.created_at if auto_silence?
Eugen Rochko's avatar
Eugen Rochko committed
61 62 63
  end

  def update_account
64
    @account.last_webfingered_at = Time.now.utc unless @options[:only_key]
Eugen Rochko's avatar
Eugen Rochko committed
65
    @account.protocol            = :activitypub
66 67

    set_immediate_attributes!
68
    set_fetchable_attributes! unless @options[:only_keys]
69

70
    @account.save_with_optional_media!
Eugen Rochko's avatar
Eugen Rochko committed
71 72
  end

73
  def set_immediate_attributes!
74 75 76 77 78 79
    @account.inbox_url               = @json['inbox'] || ''
    @account.outbox_url              = @json['outbox'] || ''
    @account.shared_inbox_url        = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || ''
    @account.followers_url           = @json['followers'] || ''
    @account.featured_collection_url = @json['featured'] || ''
    @account.url                     = url || @uri
80
    @account.uri                     = @uri
81 82 83
    @account.display_name            = @json['name'] || ''
    @account.note                    = @json['summary'] || ''
    @account.locked                  = @json['manuallyApprovesFollowers'] || false
Eugen Rochko's avatar
Eugen Rochko committed
84
    @account.fields                  = property_values || {}
85
    @account.also_known_as           = as_array(@json['alsoKnownAs'] || []).map { |item| value_or_id(item) }
86
    @account.actor_type              = actor_type
87
    @account.discoverable            = @json['discoverable'] || false
88 89 90 91 92 93 94 95 96
  end

  def set_fetchable_attributes!
    @account.avatar_remote_url = image_url('icon')  unless skip_download?
    @account.header_remote_url = image_url('image') unless skip_download?
    @account.public_key        = public_key || ''
    @account.statuses_count    = outbox_total_items    if outbox_total_items.present?
    @account.following_count   = following_total_items if following_total_items.present?
    @account.followers_count   = followers_total_items if followers_total_items.present?
97
    @account.moved_to_account  = @json['movedTo'].present? ? moved_account : nil
98 99 100
  end

  def after_protocol_change!
101 102 103
    ActivityPub::PostUpgradeWorker.perform_async(@account.domain)
  end

104 105 106 107
  def after_key_change!
    RefollowWorker.perform_async(@account.id)
  end

108 109 110 111
  def check_featured_collection!
    ActivityPub::SynchronizeFeaturedCollectionWorker.perform_async(@account.id)
  end

112 113 114 115
  def check_links!
    VerifyAccountLinksWorker.perform_async(@account.id)
  end

116 117 118 119 120 121 122 123
  def actor_type
    if @json['type'].is_a?(Array)
      @json['type'].find { |type| ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES.include?(type) }
    else
      @json['type']
    end
  end

Eugen Rochko's avatar
Eugen Rochko committed
124 125 126 127
  def image_url(key)
    value = first_of_value(@json[key])

    return if value.nil?
128
    return value['url'] if value.is_a?(Hash)
Eugen Rochko's avatar
Eugen Rochko committed
129

130
    image = fetch_resource_without_id_validation(value)
Eugen Rochko's avatar
Eugen Rochko committed
131 132 133 134 135 136 137 138 139
    image['url'] if image
  end

  def public_key
    value = first_of_value(@json['publicKey'])

    return if value.nil?
    return value['publicKeyPem'] if value.is_a?(Hash)

140
    key = fetch_resource_without_id_validation(value)
Eugen Rochko's avatar
Eugen Rochko committed
141 142 143
    key['publicKeyPem'] if key
  end

144 145
  def url
    return if @json['url'].blank?
146 147 148 149 150 151 152 153 154 155

    url_candidate = url_to_href(@json['url'], 'text/html')

    if unsupported_uri_scheme?(url_candidate) || mismatching_origin?(url_candidate)
      nil
    else
      url_candidate
    end
  end

Eugen Rochko's avatar
Eugen Rochko committed
156 157
  def property_values
    return unless @json['attachment'].is_a?(Array)
158
    as_array(@json['attachment']).select { |attachment| attachment['type'] == 'PropertyValue' }.map { |attachment| attachment.slice('name', 'value') }
Eugen Rochko's avatar
Eugen Rochko committed
159 160
  end

161 162 163 164 165
  def mismatching_origin?(url)
    needle   = Addressable::URI.parse(url).host
    haystack = Addressable::URI.parse(@uri).host

    !haystack.casecmp(needle).zero?
166 167
  end

168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
  def outbox_total_items
    collection_total_items('outbox')
  end

  def following_total_items
    collection_total_items('following')
  end

  def followers_total_items
    collection_total_items('followers')
  end

  def collection_total_items(type)
    return if @json[type].blank?
    return @collections[type] if @collections.key?(type)

184
    collection = fetch_resource_without_id_validation(@json[type])
185 186 187 188 189 190

    @collections[type] = collection.is_a?(Hash) && collection['totalItems'].present? && collection['totalItems'].is_a?(Numeric) ? collection['totalItems'] : nil
  rescue HTTP::Error, OpenSSL::SSL::SSLError
    @collections[type] = nil
  end

Eugen Rochko's avatar
Eugen Rochko committed
191 192
  def moved_account
    account   = ActivityPub::TagManager.instance.uri_to_resource(@json['movedTo'], Account)
193
    account ||= ActivityPub::FetchRemoteAccountService.new.call(@json['movedTo'], id: true, break_on_redirect: true)
Eugen Rochko's avatar
Eugen Rochko committed
194 195 196
    account
  end

197 198 199 200
  def skip_download?
    @account.suspended? || domain_block&.reject_media?
  end

Eugen Rochko's avatar
Eugen Rochko committed
201
  def auto_suspend?
202
    domain_block&.suspend?
Eugen Rochko's avatar
Eugen Rochko committed
203 204 205
  end

  def auto_silence?
206
    domain_block&.silence?
Eugen Rochko's avatar
Eugen Rochko committed
207 208 209 210
  end

  def domain_block
    return @domain_block if defined?(@domain_block)
211
    @domain_block = DomainBlock.rule_for(@domain)
Eugen Rochko's avatar
Eugen Rochko committed
212
  end
213 214 215 216 217

  def key_changed?
    !@old_public_key.nil? && @old_public_key != @account.public_key
  end

218
  def clear_tombstones!
219
    Tombstone.where(account_id: @account.id).delete_all
220 221
  end

222 223 224 225 226 227 228
  def protocol_changed?
    !@old_protocol.nil? && @old_protocol != @account.protocol
  end

  def lock_options
    { redis: Redis.current, key: "process_account:#{@uri}" }
  end
229

230
  def process_tags
231
    return if @json['tag'].blank?
232

233
    as_array(@json['tag']).each do |tag|
234
      process_emoji tag if equals_or_includes?(tag['type'], 'Emoji')
235 236 237
    end
  end

238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
  def process_attachments
    return if @json['attachment'].blank?

    previous_proofs = @account.identity_proofs.to_a
    current_proofs  = []

    as_array(@json['attachment']).each do |attachment|
      next unless equals_or_includes?(attachment['type'], 'IdentityProof')
      current_proofs << process_identity_proof(attachment)
    end

    previous_proofs.each do |previous_proof|
      next if current_proofs.any? { |current_proof| current_proof.id == previous_proof.id }
      previous_proof.delete
    end
  end

255
  def process_emoji(tag)
256 257 258 259 260 261 262 263 264
    return if skip_download?
    return if tag['name'].blank? || tag['icon'].blank? || tag['icon']['url'].blank?

    shortcode = tag['name'].delete(':')
    image_url = tag['icon']['url']
    uri       = tag['id']
    updated   = tag['updated']
    emoji     = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain)

265
    return unless emoji.nil? || image_url != emoji.image_remote_url || (updated && updated >= emoji.updated_at)
266 267 268 269 270

    emoji ||= CustomEmoji.new(domain: @account.domain, shortcode: shortcode, uri: uri)
    emoji.image_remote_url = image_url
    emoji.save
  end
271 272 273 274 275 276 277 278

  def process_identity_proof(attachment)
    provider          = attachment['signatureAlgorithm']
    provider_username = attachment['name']
    token             = attachment['signatureValue']

    @account.identity_proofs.where(provider: provider, provider_username: provider_username).find_or_create_by(provider: provider, provider_username: provider_username, token: token)
  end
Eugen Rochko's avatar
Eugen Rochko committed
279
end