about summary refs log tree commit diff
path: root/app/lib
diff options
context:
space:
mode:
authorReverite <github@reverite.sh>2019-03-21 15:35:55 -0700
committerReverite <github@reverite.sh>2019-03-21 15:35:55 -0700
commit592735fd80acd0aeffb5a5674255ed48d7a8db0b (patch)
tree0eefc67f624a07df0af860edecd68d5dc64c7ee9 /app/lib
parent75eeb003b09c53d3b4e98046d1c20b0ad8a887bb (diff)
parentbde9196b70299405ebe9b16500b7a3f65539b2c3 (diff)
Merge remote-tracking branch 'glitch/master' into production
Diffstat (limited to 'app/lib')
-rw-r--r--app/lib/activitypub/activity/flag.rb7
-rw-r--r--app/lib/formatter.rb6
-rw-r--r--app/lib/language_detector.rb31
-rw-r--r--app/lib/proof_provider.rb12
-rw-r--r--app/lib/proof_provider/keybase.rb59
-rw-r--r--app/lib/proof_provider/keybase/badge.rb48
-rw-r--r--app/lib/proof_provider/keybase/config_serializer.rb70
-rw-r--r--app/lib/proof_provider/keybase/serializer.rb25
-rw-r--r--app/lib/proof_provider/keybase/verifier.rb62
-rw-r--r--app/lib/proof_provider/keybase/worker.rb33
10 files changed, 345 insertions, 8 deletions
diff --git a/app/lib/activitypub/activity/flag.rb b/app/lib/activitypub/activity/flag.rb
index 0d10d6c3c..f73b93058 100644
--- a/app/lib/activitypub/activity/flag.rb
+++ b/app/lib/activitypub/activity/flag.rb
@@ -14,7 +14,8 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity
         @account,
         target_account,
         status_ids: target_statuses.nil? ? [] : target_statuses.map(&:id),
-        comment: @json['content'] || ''
+        comment: @json['content'] || '',
+        uri: report_uri
       )
     end
   end
@@ -28,4 +29,8 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity
   def object_uris
     @object_uris ||= Array(@object.is_a?(Array) ? @object.map { |item| value_or_id(item) } : value_or_id(@object))
   end
+
+  def report_uri
+    @json['id'] unless @json['id'].nil? || invalid_origin?(@json['id'])
+  end
 end
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index 464e1ee7e..aadf03b2a 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -71,6 +71,12 @@ class Formatter
     html.html_safe # rubocop:disable Rails/OutputSafety
   end
 
+  def format_poll_option(status, option, **options)
+    html = encode(option.title)
+    html = encode_custom_emojis(html, status.emojis, options[:autoplay])
+    html.html_safe # rubocop:disable Rails/OutputSafety
+  end
+
   def format_display_name(account, **options)
     html = encode(account.display_name.presence || account.username)
     html = encode_custom_emojis(html, account.emojis, options[:autoplay]) if options[:custom_emojify]
diff --git a/app/lib/language_detector.rb b/app/lib/language_detector.rb
index 58c8e2069..70a9084d1 100644
--- a/app/lib/language_detector.rb
+++ b/app/lib/language_detector.rb
@@ -3,7 +3,8 @@
 class LanguageDetector
   include Singleton
 
-  CHARACTER_THRESHOLD = 140
+  CHARACTER_THRESHOLD    = 140
+  RELIABLE_CHARACTERS_RE = /[\p{Hebrew}\p{Arabic}\p{Syriac}\p{Thaana}\p{Nko}\p{Han}\p{Katakana}\p{Hiragana}\p{Hangul}]+/m
 
   def initialize
     @identifier = CLD3::NNetLanguageIdentifier.new(1, 2048)
@@ -11,15 +12,14 @@ class LanguageDetector
 
   def detect(text, account)
     input_text = prepare_text(text)
+
     return if input_text.blank?
 
     detect_language_code(input_text) || default_locale(account)
   end
 
   def language_names
-    @language_names =
-      CLD3::TaskContextParams::LANGUAGE_NAMES.map { |name| iso6391(name.to_s).to_sym }
-                                             .uniq
+    @language_names = CLD3::TaskContextParams::LANGUAGE_NAMES.map { |name| iso6391(name.to_s).to_sym }.uniq
   end
 
   private
@@ -29,12 +29,29 @@ class LanguageDetector
   end
 
   def unreliable_input?(text)
-    text.size < CHARACTER_THRESHOLD
+    !reliable_input?(text)
+  end
+
+  def reliable_input?(text)
+    sufficient_text_length?(text) || language_specific_character_set?(text)
+  end
+
+  def sufficient_text_length?(text)
+    text.size >= CHARACTER_THRESHOLD
+  end
+
+  def language_specific_character_set?(text)
+    words = text.scan(RELIABLE_CHARACTERS_RE)
+
+    if words.present?
+      words.reduce(0) { |acc, elem| acc + elem.size }.to_f / text.size.to_f > 0.3
+    else
+      false
+    end
   end
 
   def detect_language_code(text)
     return if unreliable_input?(text)
-
     result = @identifier.find_language(text)
     iso6391(result.language.to_s).to_sym if result.reliable?
   end
@@ -77,6 +94,6 @@ class LanguageDetector
   end
 
   def default_locale(account)
-    return account.user_locale&.to_sym || I18n.default_locale if account.local?
+    account.user_locale&.to_sym || I18n.default_locale if account.local?
   end
 end
diff --git a/app/lib/proof_provider.rb b/app/lib/proof_provider.rb
new file mode 100644
index 000000000..102c50f4f
--- /dev/null
+++ b/app/lib/proof_provider.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module ProofProvider
+  SUPPORTED_PROVIDERS = %w(keybase).freeze
+
+  def self.find(identifier, proof = nil)
+    case identifier
+    when 'keybase'
+      ProofProvider::Keybase.new(proof)
+    end
+  end
+end
diff --git a/app/lib/proof_provider/keybase.rb b/app/lib/proof_provider/keybase.rb
new file mode 100644
index 000000000..96322a265
--- /dev/null
+++ b/app/lib/proof_provider/keybase.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+class ProofProvider::Keybase
+  BASE_URL = 'https://keybase.io'
+
+  class Error < StandardError; end
+
+  class ExpectedProofLiveError < Error; end
+
+  class UnexpectedResponseError < Error; end
+
+  def initialize(proof = nil)
+    @proof = proof
+  end
+
+  def serializer_class
+    ProofProvider::Keybase::Serializer
+  end
+
+  def worker_class
+    ProofProvider::Keybase::Worker
+  end
+
+  def validate!
+    unless @proof.token&.size == 66
+      @proof.errors.add(:base, I18n.t('identity_proofs.errors.keybase.invalid_token'))
+      return
+    end
+
+    return if @proof.provider_username.blank?
+
+    if verifier.valid?
+      @proof.verified = true
+      @proof.live     = false
+    else
+      @proof.errors.add(:base, I18n.t('identity_proofs.errors.keybase.verification_failed', kb_username: @proof.provider_username))
+    end
+  end
+
+  def refresh!
+    worker_class.new.perform(@proof)
+  rescue ProofProvider::Keybase::Error
+    nil
+  end
+
+  def on_success_path(user_agent = nil)
+    verifier.on_success_path(user_agent)
+  end
+
+  def badge
+    @badge ||= ProofProvider::Keybase::Badge.new(@proof.account.username, @proof.provider_username, @proof.token)
+  end
+
+  private
+
+  def verifier
+    @verifier ||= ProofProvider::Keybase::Verifier.new(@proof.account.username, @proof.provider_username, @proof.token)
+  end
+end
diff --git a/app/lib/proof_provider/keybase/badge.rb b/app/lib/proof_provider/keybase/badge.rb
new file mode 100644
index 000000000..3aa067ecf
--- /dev/null
+++ b/app/lib/proof_provider/keybase/badge.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+class ProofProvider::Keybase::Badge
+  include RoutingHelper
+
+  def initialize(local_username, provider_username, token)
+    @local_username    = local_username
+    @provider_username = provider_username
+    @token             = token
+  end
+
+  def proof_url
+    "#{ProofProvider::Keybase::BASE_URL}/#{@provider_username}/sigchain\##{@token}"
+  end
+
+  def profile_url
+    "#{ProofProvider::Keybase::BASE_URL}/#{@provider_username}"
+  end
+
+  def icon_url
+    "#{ProofProvider::Keybase::BASE_URL}/#{@provider_username}/proof_badge/#{@token}?username=#{@local_username}&domain=#{domain}"
+  end
+
+  def avatar_url
+    Rails.cache.fetch("proof_providers/keybase/#{@provider_username}/avatar_url", expires_in: 5.minutes) { remote_avatar_url } || default_avatar_url
+  end
+
+  private
+
+  def remote_avatar_url
+    request = Request.new(:get, "#{ProofProvider::Keybase::BASE_URL}/_/api/1.0/user/pic_url.json", params: { username: @provider_username })
+
+    request.perform do |res|
+      json = Oj.load(res.body_with_limit, mode: :strict)
+      json['pic_url'] if json.is_a?(Hash)
+    end
+  rescue Oj::ParseError, HTTP::Error, OpenSSL::SSL::SSLError
+    nil
+  end
+
+  def default_avatar_url
+    asset_pack_path('media/images/proof_providers/keybase.png')
+  end
+
+  def domain
+    Rails.configuration.x.local_domain
+  end
+end
diff --git a/app/lib/proof_provider/keybase/config_serializer.rb b/app/lib/proof_provider/keybase/config_serializer.rb
new file mode 100644
index 000000000..474ea74e2
--- /dev/null
+++ b/app/lib/proof_provider/keybase/config_serializer.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+class ProofProvider::Keybase::ConfigSerializer < ActiveModel::Serializer
+  include RoutingHelper
+
+  attributes :version, :domain, :display_name, :username,
+             :brand_color, :logo, :description, :prefill_url,
+             :profile_url, :check_url, :check_path, :avatar_path,
+             :contact
+
+  def version
+    1
+  end
+
+  def domain
+    Rails.configuration.x.local_domain
+  end
+
+  def display_name
+    Setting.site_title
+  end
+
+  def logo
+    { svg_black: full_asset_url(asset_pack_path('media/images/logo_transparent_black.svg')), svg_full: full_asset_url(asset_pack_path('media/images/logo.svg')) }
+  end
+
+  def brand_color
+    '#282c37'
+  end
+
+  def description
+    Setting.site_short_description.presence || Setting.site_description.presence || I18n.t('about.about_mastodon_html')
+  end
+
+  def username
+    { min: 1, max: 30, re: Account::USERNAME_RE.inspect }
+  end
+
+  def prefill_url
+    params = {
+      provider: 'keybase',
+      token: '%{sig_hash}',
+      provider_username: '%{kb_username}',
+      username: '%{username}',
+      user_agent: '%{kb_ua}',
+    }
+
+    CGI.unescape(new_settings_identity_proof_url(params))
+  end
+
+  def profile_url
+    CGI.unescape(short_account_url('%{username}')) # rubocop:disable Style/FormatStringToken
+  end
+
+  def check_url
+    CGI.unescape(api_proofs_url(username: '%{username}', provider: 'keybase'))
+  end
+
+  def check_path
+    ['signatures']
+  end
+
+  def avatar_path
+    ['avatar']
+  end
+
+  def contact
+    [Setting.site_contact_email.presence].compact
+  end
+end
diff --git a/app/lib/proof_provider/keybase/serializer.rb b/app/lib/proof_provider/keybase/serializer.rb
new file mode 100644
index 000000000..d29283600
--- /dev/null
+++ b/app/lib/proof_provider/keybase/serializer.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class ProofProvider::Keybase::Serializer < ActiveModel::Serializer
+  include RoutingHelper
+
+  attribute :avatar
+
+  has_many :identity_proofs, key: :signatures
+
+  def avatar
+    full_asset_url(object.avatar_original_url)
+  end
+
+  class AccountIdentityProofSerializer < ActiveModel::Serializer
+    attributes :sig_hash, :kb_username
+
+    def sig_hash
+      object.token
+    end
+
+    def kb_username
+      object.provider_username
+    end
+  end
+end
diff --git a/app/lib/proof_provider/keybase/verifier.rb b/app/lib/proof_provider/keybase/verifier.rb
new file mode 100644
index 000000000..86f249dd7
--- /dev/null
+++ b/app/lib/proof_provider/keybase/verifier.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+class ProofProvider::Keybase::Verifier
+  def initialize(local_username, provider_username, token)
+    @local_username    = local_username
+    @provider_username = provider_username
+    @token             = token
+  end
+
+  def valid?
+    request = Request.new(:get, "#{ProofProvider::Keybase::BASE_URL}/_/api/1.0/sig/proof_valid.json", params: query_params)
+
+    request.perform do |res|
+      json = Oj.load(res.body_with_limit, mode: :strict)
+
+      if json.is_a?(Hash)
+        json.fetch('proof_valid', false)
+      else
+        false
+      end
+    end
+  rescue Oj::ParseError, HTTP::Error, OpenSSL::SSL::SSLError
+    false
+  end
+
+  def on_success_path(user_agent = nil)
+    url = Addressable::URI.parse("#{ProofProvider::Keybase::BASE_URL}/_/proof_creation_success")
+    url.query_values = query_params.merge(kb_ua: user_agent || 'unknown')
+    url.to_s
+  end
+
+  def status
+    request = Request.new(:get, "#{ProofProvider::Keybase::BASE_URL}/_/api/1.0/sig/proof_live.json", params: query_params)
+
+    request.perform do |res|
+      raise ProofProvider::Keybase::UnexpectedResponseError unless res.code == 200
+
+      json = Oj.load(res.body_with_limit, mode: :strict)
+
+      raise ProofProvider::Keybase::UnexpectedResponseError unless json.is_a?(Hash) && json.key?('proof_valid') && json.key?('proof_live')
+
+      json
+    end
+  rescue Oj::ParseError, HTTP::Error, OpenSSL::SSL::SSLError
+    raise ProofProvider::Keybase::UnexpectedResponseError
+  end
+
+  private
+
+  def query_params
+    {
+      domain: domain,
+      kb_username: @provider_username,
+      username: @local_username,
+      sig_hash: @token,
+    }
+  end
+
+  def domain
+    Rails.configuration.x.local_domain
+  end
+end
diff --git a/app/lib/proof_provider/keybase/worker.rb b/app/lib/proof_provider/keybase/worker.rb
new file mode 100644
index 000000000..2872f59c1
--- /dev/null
+++ b/app/lib/proof_provider/keybase/worker.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class ProofProvider::Keybase::Worker
+  include Sidekiq::Worker
+
+  sidekiq_options queue: 'pull', retry: 20, unique: :until_executed
+
+  sidekiq_retry_in do |count, exception|
+    # Retry aggressively when the proof is valid but not live in Keybase.
+    # This is likely because Keybase just hasn't noticed the proof being
+    # served from here yet.
+
+    if exception.class == ProofProvider::Keybase::ExpectedProofLiveError
+      case count
+      when 0..2 then 0.seconds
+      when 2..6 then 1.second
+      end
+    end
+  end
+
+  def perform(proof_id)
+    proof    = proof_id.is_a?(AccountIdentityProof) ? proof_id : AccountIdentityProof.find(proof_id)
+    verifier = ProofProvider::Keybase::Verifier.new(proof.account.username, proof.provider_username, proof.token)
+    status   = verifier.status
+
+    # If Keybase thinks the proof is valid, and it exists here in Mastodon,
+    # then it should be live. Keybase just has to notice that it's here
+    # and then update its state. That might take a couple seconds.
+    raise ProofProvider::Keybase::ExpectedProofLiveError if status['proof_valid'] && !status['proof_live']
+
+    proof.update!(verified: status['proof_valid'], live: status['proof_live'])
+  end
+end