Merge branch 'master' into glitch-soc/merge-upstream
Conflicts: - config/locales/en.yml Conflict caused by the glitch-soc-specific “flavour” string being too close to the newly introduced “identity_proofs” string. Just included both.
This commit is contained in:
		
						commit
						1d6152f440
					
				
							
								
								
									
										4
									
								
								Gemfile
								
								
								
								
							
							
						
						
									
										4
									
								
								Gemfile
								
								
								
								
							| 
						 | 
					@ -15,7 +15,7 @@ gem 'makara', '~> 0.4'
 | 
				
			||||||
gem 'pghero', '~> 2.2'
 | 
					gem 'pghero', '~> 2.2'
 | 
				
			||||||
gem 'dotenv-rails', '~> 2.7'
 | 
					gem 'dotenv-rails', '~> 2.7'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
gem 'aws-sdk-s3', '~> 1.32', require: false
 | 
					gem 'aws-sdk-s3', '~> 1.33', require: false
 | 
				
			||||||
gem 'fog-core', '<= 2.1.0'
 | 
					gem 'fog-core', '<= 2.1.0'
 | 
				
			||||||
gem 'fog-openstack', '~> 0.3', require: false
 | 
					gem 'fog-openstack', '~> 0.3', require: false
 | 
				
			||||||
gem 'paperclip', '~> 6.0'
 | 
					gem 'paperclip', '~> 6.0'
 | 
				
			||||||
| 
						 | 
					@ -128,7 +128,7 @@ group :development do
 | 
				
			||||||
  gem 'letter_opener', '~> 1.7'
 | 
					  gem 'letter_opener', '~> 1.7'
 | 
				
			||||||
  gem 'letter_opener_web', '~> 1.3'
 | 
					  gem 'letter_opener_web', '~> 1.3'
 | 
				
			||||||
  gem 'memory_profiler'
 | 
					  gem 'memory_profiler'
 | 
				
			||||||
  gem 'rubocop', '~> 0.65', require: false
 | 
					  gem 'rubocop', '~> 0.66', require: false
 | 
				
			||||||
  gem 'brakeman', '~> 4.5', require: false
 | 
					  gem 'brakeman', '~> 4.5', require: false
 | 
				
			||||||
  gem 'bundler-audit', '~> 0.6', require: false
 | 
					  gem 'bundler-audit', '~> 0.6', require: false
 | 
				
			||||||
  gem 'scss_lint', '~> 0.57', require: false
 | 
					  gem 'scss_lint', '~> 0.57', require: false
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										26
									
								
								Gemfile.lock
								
								
								
								
							
							
						
						
									
										26
									
								
								Gemfile.lock
								
								
								
								
							| 
						 | 
					@ -77,17 +77,16 @@ GEM
 | 
				
			||||||
      cocaine (~> 0.5.3)
 | 
					      cocaine (~> 0.5.3)
 | 
				
			||||||
    aws-eventstream (1.0.2)
 | 
					    aws-eventstream (1.0.2)
 | 
				
			||||||
    aws-partitions (1.144.0)
 | 
					    aws-partitions (1.144.0)
 | 
				
			||||||
    aws-sdk-core (3.47.0)
 | 
					    aws-sdk-core (3.48.0)
 | 
				
			||||||
      aws-eventstream (~> 1.0, >= 1.0.2)
 | 
					      aws-eventstream (~> 1.0, >= 1.0.2)
 | 
				
			||||||
      aws-partitions (~> 1.0)
 | 
					      aws-partitions (~> 1.0)
 | 
				
			||||||
      aws-sigv4 (~> 1.1)
 | 
					      aws-sigv4 (~> 1.1)
 | 
				
			||||||
      http-2 (~> 0.10)
 | 
					 | 
				
			||||||
      jmespath (~> 1.0)
 | 
					      jmespath (~> 1.0)
 | 
				
			||||||
    aws-sdk-kms (1.14.0)
 | 
					    aws-sdk-kms (1.15.0)
 | 
				
			||||||
      aws-sdk-core (~> 3, >= 3.47.0)
 | 
					      aws-sdk-core (~> 3, >= 3.48.0)
 | 
				
			||||||
      aws-sigv4 (~> 1.1)
 | 
					      aws-sigv4 (~> 1.1)
 | 
				
			||||||
    aws-sdk-s3 (1.32.0)
 | 
					    aws-sdk-s3 (1.33.0)
 | 
				
			||||||
      aws-sdk-core (~> 3, >= 3.47.0)
 | 
					      aws-sdk-core (~> 3, >= 3.48.0)
 | 
				
			||||||
      aws-sdk-kms (~> 1)
 | 
					      aws-sdk-kms (~> 1)
 | 
				
			||||||
      aws-sigv4 (~> 1.0)
 | 
					      aws-sigv4 (~> 1.0)
 | 
				
			||||||
    aws-sigv4 (1.1.0)
 | 
					    aws-sigv4 (1.1.0)
 | 
				
			||||||
| 
						 | 
					@ -261,7 +260,6 @@ GEM
 | 
				
			||||||
    html2text (0.2.1)
 | 
					    html2text (0.2.1)
 | 
				
			||||||
      nokogiri (~> 1.6)
 | 
					      nokogiri (~> 1.6)
 | 
				
			||||||
    htmlentities (4.3.4)
 | 
					    htmlentities (4.3.4)
 | 
				
			||||||
    http-2 (0.10.1)
 | 
					 | 
				
			||||||
    http (3.3.0)
 | 
					    http (3.3.0)
 | 
				
			||||||
      addressable (~> 2.3)
 | 
					      addressable (~> 2.3)
 | 
				
			||||||
      http-cookie (~> 1.0)
 | 
					      http-cookie (~> 1.0)
 | 
				
			||||||
| 
						 | 
					@ -394,7 +392,7 @@ GEM
 | 
				
			||||||
    paperclip-av-transcoder (0.6.4)
 | 
					    paperclip-av-transcoder (0.6.4)
 | 
				
			||||||
      av (~> 0.9.0)
 | 
					      av (~> 0.9.0)
 | 
				
			||||||
      paperclip (>= 2.5.2)
 | 
					      paperclip (>= 2.5.2)
 | 
				
			||||||
    parallel (1.13.0)
 | 
					    parallel (1.14.0)
 | 
				
			||||||
    parallel_tests (2.28.0)
 | 
					    parallel_tests (2.28.0)
 | 
				
			||||||
      parallel
 | 
					      parallel
 | 
				
			||||||
    parser (2.6.0.0)
 | 
					    parser (2.6.0.0)
 | 
				
			||||||
| 
						 | 
					@ -406,7 +404,6 @@ GEM
 | 
				
			||||||
    pghero (2.2.0)
 | 
					    pghero (2.2.0)
 | 
				
			||||||
      activerecord
 | 
					      activerecord
 | 
				
			||||||
    pkg-config (1.3.7)
 | 
					    pkg-config (1.3.7)
 | 
				
			||||||
    powerpack (0.1.2)
 | 
					 | 
				
			||||||
    premailer (1.11.1)
 | 
					    premailer (1.11.1)
 | 
				
			||||||
      addressable
 | 
					      addressable
 | 
				
			||||||
      css_parser (>= 1.6.0)
 | 
					      css_parser (>= 1.6.0)
 | 
				
			||||||
| 
						 | 
					@ -531,15 +528,14 @@ GEM
 | 
				
			||||||
      rspec-core (~> 3.0, >= 3.0.0)
 | 
					      rspec-core (~> 3.0, >= 3.0.0)
 | 
				
			||||||
      sidekiq (>= 2.4.0)
 | 
					      sidekiq (>= 2.4.0)
 | 
				
			||||||
    rspec-support (3.8.0)
 | 
					    rspec-support (3.8.0)
 | 
				
			||||||
    rubocop (0.65.0)
 | 
					    rubocop (0.66.0)
 | 
				
			||||||
      jaro_winkler (~> 1.5.1)
 | 
					      jaro_winkler (~> 1.5.1)
 | 
				
			||||||
      parallel (~> 1.10)
 | 
					      parallel (~> 1.10)
 | 
				
			||||||
      parser (>= 2.5, != 2.5.1.1)
 | 
					      parser (>= 2.5, != 2.5.1.1)
 | 
				
			||||||
      powerpack (~> 0.1)
 | 
					 | 
				
			||||||
      psych (>= 3.1.0)
 | 
					      psych (>= 3.1.0)
 | 
				
			||||||
      rainbow (>= 2.2.2, < 4.0)
 | 
					      rainbow (>= 2.2.2, < 4.0)
 | 
				
			||||||
      ruby-progressbar (~> 1.7)
 | 
					      ruby-progressbar (~> 1.7)
 | 
				
			||||||
      unicode-display_width (~> 1.4.0)
 | 
					      unicode-display_width (>= 1.4.0, < 1.6)
 | 
				
			||||||
    ruby-progressbar (1.10.0)
 | 
					    ruby-progressbar (1.10.0)
 | 
				
			||||||
    ruby-saml (1.9.0)
 | 
					    ruby-saml (1.9.0)
 | 
				
			||||||
      nokogiri (>= 1.5.10)
 | 
					      nokogiri (>= 1.5.10)
 | 
				
			||||||
| 
						 | 
					@ -634,7 +630,7 @@ GEM
 | 
				
			||||||
    unf (0.1.4)
 | 
					    unf (0.1.4)
 | 
				
			||||||
      unf_ext
 | 
					      unf_ext
 | 
				
			||||||
    unf_ext (0.0.7.5)
 | 
					    unf_ext (0.0.7.5)
 | 
				
			||||||
    unicode-display_width (1.4.1)
 | 
					    unicode-display_width (1.5.0)
 | 
				
			||||||
    uniform_notifier (1.12.1)
 | 
					    uniform_notifier (1.12.1)
 | 
				
			||||||
    warden (1.2.7)
 | 
					    warden (1.2.7)
 | 
				
			||||||
      rack (>= 1.0)
 | 
					      rack (>= 1.0)
 | 
				
			||||||
| 
						 | 
					@ -664,7 +660,7 @@ DEPENDENCIES
 | 
				
			||||||
  active_record_query_trace (~> 1.6)
 | 
					  active_record_query_trace (~> 1.6)
 | 
				
			||||||
  addressable (~> 2.6)
 | 
					  addressable (~> 2.6)
 | 
				
			||||||
  annotate (~> 2.7)
 | 
					  annotate (~> 2.7)
 | 
				
			||||||
  aws-sdk-s3 (~> 1.32)
 | 
					  aws-sdk-s3 (~> 1.33)
 | 
				
			||||||
  better_errors (~> 2.5)
 | 
					  better_errors (~> 2.5)
 | 
				
			||||||
  binding_of_caller (~> 0.7)
 | 
					  binding_of_caller (~> 0.7)
 | 
				
			||||||
  bootsnap (~> 1.4)
 | 
					  bootsnap (~> 1.4)
 | 
				
			||||||
| 
						 | 
					@ -754,7 +750,7 @@ DEPENDENCIES
 | 
				
			||||||
  rqrcode (~> 0.10)
 | 
					  rqrcode (~> 0.10)
 | 
				
			||||||
  rspec-rails (~> 3.8)
 | 
					  rspec-rails (~> 3.8)
 | 
				
			||||||
  rspec-sidekiq (~> 3.0)
 | 
					  rspec-sidekiq (~> 3.0)
 | 
				
			||||||
  rubocop (~> 0.65)
 | 
					  rubocop (~> 0.66)
 | 
				
			||||||
  sanitize (~> 5.0)
 | 
					  sanitize (~> 5.0)
 | 
				
			||||||
  scss_lint (~> 0.57)
 | 
					  scss_lint (~> 0.57)
 | 
				
			||||||
  sidekiq (~> 5.2)
 | 
					  sidekiq (~> 5.2)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -53,7 +53,7 @@ module Admin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def reject
 | 
					    def reject
 | 
				
			||||||
      authorize @account.user, :reject?
 | 
					      authorize @account.user, :reject?
 | 
				
			||||||
      SuspendAccountService.new.call(@account, including_user: true, destroy: true)
 | 
					      SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true)
 | 
				
			||||||
      redirect_to admin_accounts_path(pending: '1')
 | 
					      redirect_to admin_accounts_path(pending: '1')
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,30 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Api::ProofsController < Api::BaseController
 | 
				
			||||||
 | 
					  before_action :set_account
 | 
				
			||||||
 | 
					  before_action :set_provider
 | 
				
			||||||
 | 
					  before_action :check_account_approval
 | 
				
			||||||
 | 
					  before_action :check_account_suspension
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def index
 | 
				
			||||||
 | 
					    render json: @account, serializer: @provider.serializer_class
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def set_provider
 | 
				
			||||||
 | 
					    @provider = ProofProvider.find(params[:provider]) || raise(ActiveRecord::RecordNotFound)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def set_account
 | 
				
			||||||
 | 
					    @account = Account.find_local!(params[:username])
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def check_account_approval
 | 
				
			||||||
 | 
					    not_found if @account.user_pending?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def check_account_suspension
 | 
				
			||||||
 | 
					    gone if @account.suspended?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,45 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Settings::IdentityProofsController < Settings::BaseController
 | 
				
			||||||
 | 
					  layout 'admin'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  before_action :authenticate_user!
 | 
				
			||||||
 | 
					  before_action :check_required_params, only: :new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def index
 | 
				
			||||||
 | 
					    @proofs = AccountIdentityProof.where(account: current_account).order(provider: :asc, provider_username: :asc)
 | 
				
			||||||
 | 
					    @proofs.each(&:refresh!)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def new
 | 
				
			||||||
 | 
					    @proof = current_account.identity_proofs.new(
 | 
				
			||||||
 | 
					      token: params[:token],
 | 
				
			||||||
 | 
					      provider: params[:provider],
 | 
				
			||||||
 | 
					      provider_username: params[:provider_username]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    render layout: 'auth'
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def create
 | 
				
			||||||
 | 
					    @proof = current_account.identity_proofs.where(provider: resource_params[:provider], provider_username: resource_params[:provider_username]).first_or_initialize(resource_params)
 | 
				
			||||||
 | 
					    @proof.token = resource_params[:token]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if @proof.save
 | 
				
			||||||
 | 
					      redirect_to @proof.on_success_path(params[:user_agent])
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      flash[:alert] = I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize)
 | 
				
			||||||
 | 
					      redirect_to settings_identity_proofs_path
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def check_required_params
 | 
				
			||||||
 | 
					    redirect_to settings_identity_proofs_path unless [:provider, :provider_username, :token].all? { |k| params[k].present? }
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def resource_params
 | 
				
			||||||
 | 
					    params.require(:account_identity_proof).permit(:provider, :provider_username, :token)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module WellKnown
 | 
				
			||||||
 | 
					  class KeybaseProofConfigController < ActionController::Base
 | 
				
			||||||
 | 
					    def show
 | 
				
			||||||
 | 
					      render json: {}, serializer: ProofProvider::Keybase::ConfigSerializer
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,7 @@ module SettingsHelper
 | 
				
			||||||
    ar: 'العربية',
 | 
					    ar: 'العربية',
 | 
				
			||||||
    ast: 'Asturianu',
 | 
					    ast: 'Asturianu',
 | 
				
			||||||
    bg: 'Български',
 | 
					    bg: 'Български',
 | 
				
			||||||
 | 
					    bn: 'বাংলা',
 | 
				
			||||||
    ca: 'Català',
 | 
					    ca: 'Català',
 | 
				
			||||||
    co: 'Corsu',
 | 
					    co: 'Corsu',
 | 
				
			||||||
    cs: 'Čeština',
 | 
					    cs: 'Čeština',
 | 
				
			||||||
| 
						 | 
					@ -19,8 +20,10 @@ module SettingsHelper
 | 
				
			||||||
    fa: 'فارسی',
 | 
					    fa: 'فارسی',
 | 
				
			||||||
    fi: 'Suomi',
 | 
					    fi: 'Suomi',
 | 
				
			||||||
    fr: 'Français',
 | 
					    fr: 'Français',
 | 
				
			||||||
 | 
					    ga: 'Gaeilge',
 | 
				
			||||||
    gl: 'Galego',
 | 
					    gl: 'Galego',
 | 
				
			||||||
    he: 'עברית',
 | 
					    he: 'עברית',
 | 
				
			||||||
 | 
					    hi: 'हिन्दी',
 | 
				
			||||||
    hr: 'Hrvatski',
 | 
					    hr: 'Hrvatski',
 | 
				
			||||||
    hu: 'Magyar',
 | 
					    hu: 'Magyar',
 | 
				
			||||||
    hy: 'Հայերեն',
 | 
					    hy: 'Հայերեն',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.4144 232.00976"><path d="M107.86523 0C78.203984.2425 49.672422 3.4535937 33.044922 11.089844c0 0-32.97656262 14.752031-32.97656262 65.082031 0 11.525-.224375 25.306175.140625 39.919925 1.19750002 49.22 9.02375002 97.72843 54.53124962 109.77343 20.9825 5.55375 38.99711 6.71547 53.505856 5.91797 26.31125-1.45875 41.08203-9.38867 41.08203-9.38867l-.86914-19.08984s-18.80171 5.92758-39.91796 5.20508c-20.921254-.7175-43.006879-2.25516-46.390629-27.94141-.3125-2.25625-.46875-4.66938-.46875-7.20313 0 0 20.536953 5.0204 46.564449 6.21289 15.915.73001 30.8393-.93343 45.99805-2.74218 29.07-3.47125 54.38125-21.3818 57.5625-37.74805 5.0125-25.78125 4.59961-62.916015 4.59961-62.916015 0-50.33-32.97461-65.082031-32.97461-65.082031C166.80539 3.4535938 138.255.2425 108.59375 0h-.72852zM74.296875 39.326172c12.355 0 21.710234 4.749297 27.896485 14.248047l6.01367 10.080078 6.01563-10.080078c6.185-9.49875 15.54023-14.248047 27.89648-14.248047 10.6775 0 19.28156 3.753672 25.85156 11.076172 6.36875 7.3225 9.53907 17.218828 9.53907 29.673828v60.941408h-24.14454V81.869141c0-12.46875-5.24453-18.798829-15.73828-18.798829-11.6025 0-17.41797 7.508516-17.41797 22.353516v32.375002H96.207031V85.423828c0-14.845-5.815468-22.353515-17.417969-22.353516-10.49375 0-15.740234 6.330079-15.740234 18.798829v59.148439H38.904297V80.076172c0-12.455 3.171016-22.351328 9.541015-29.673828 6.568751-7.3225 15.172813-11.076172 25.851563-11.076172z" fill="#000"/></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 1.5 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 12 KiB  | 
| 
						 | 
					@ -3,7 +3,6 @@ import { connect } from 'react-redux';
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
					import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
import { expandHashtagTimeline } from 'mastodon/actions/timelines';
 | 
					import { expandHashtagTimeline } from 'mastodon/actions/timelines';
 | 
				
			||||||
import { connectHashtagStream } from 'mastodon/actions/streaming';
 | 
					 | 
				
			||||||
import Masonry from 'react-masonry-infinite';
 | 
					import Masonry from 'react-masonry-infinite';
 | 
				
			||||||
import { List as ImmutableList } from 'immutable';
 | 
					import { List as ImmutableList } from 'immutable';
 | 
				
			||||||
import DetailedStatusContainer from 'mastodon/features/status/containers/detailed_status_container';
 | 
					import DetailedStatusContainer from 'mastodon/features/status/containers/detailed_status_container';
 | 
				
			||||||
| 
						 | 
					@ -31,14 +30,6 @@ class HashtagTimeline extends React.PureComponent {
 | 
				
			||||||
    const { dispatch, hashtag } = this.props;
 | 
					    const { dispatch, hashtag } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dispatch(expandHashtagTimeline(hashtag));
 | 
					    dispatch(expandHashtagTimeline(hashtag));
 | 
				
			||||||
    this.disconnect = dispatch(connectHashtagStream(hashtag, hashtag));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  componentWillUnmount () {
 | 
					 | 
				
			||||||
    if (this.disconnect) {
 | 
					 | 
				
			||||||
      this.disconnect();
 | 
					 | 
				
			||||||
      this.disconnect = null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleLoadMore = () => {
 | 
					  handleLoadMore = () => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,6 @@ import { connect } from 'react-redux';
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
					import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
import { expandPublicTimeline, expandCommunityTimeline } from 'mastodon/actions/timelines';
 | 
					import { expandPublicTimeline, expandCommunityTimeline } from 'mastodon/actions/timelines';
 | 
				
			||||||
import { connectPublicStream, connectCommunityStream } from 'mastodon/actions/streaming';
 | 
					 | 
				
			||||||
import Masonry from 'react-masonry-infinite';
 | 
					import Masonry from 'react-masonry-infinite';
 | 
				
			||||||
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
 | 
					import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
 | 
				
			||||||
import DetailedStatusContainer from 'mastodon/features/status/containers/detailed_status_container';
 | 
					import DetailedStatusContainer from 'mastodon/features/status/containers/detailed_status_container';
 | 
				
			||||||
| 
						 | 
					@ -37,27 +36,14 @@ class PublicTimeline extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentDidUpdate (prevProps) {
 | 
					  componentDidUpdate (prevProps) {
 | 
				
			||||||
    if (prevProps.local !== this.props.local) {
 | 
					    if (prevProps.local !== this.props.local) {
 | 
				
			||||||
      this._disconnect();
 | 
					 | 
				
			||||||
      this._connect();
 | 
					      this._connect();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentWillUnmount () {
 | 
					 | 
				
			||||||
    this._disconnect();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  _connect () {
 | 
					  _connect () {
 | 
				
			||||||
    const { dispatch, local } = this.props;
 | 
					    const { dispatch, local } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dispatch(local ? expandCommunityTimeline() : expandPublicTimeline());
 | 
					    dispatch(local ? expandCommunityTimeline() : expandPublicTimeline());
 | 
				
			||||||
    this.disconnect = dispatch(local ? connectCommunityStream() : connectPublicStream());
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  _disconnect () {
 | 
					 | 
				
			||||||
    if (this.disconnect) {
 | 
					 | 
				
			||||||
      this.disconnect();
 | 
					 | 
				
			||||||
      this.disconnect = null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleLoadMore = () => {
 | 
					  handleLoadMore = () => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -657,7 +657,7 @@ $small-breakpoint: 960px;
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    justify-content: center;
 | 
					    justify-content: center;
 | 
				
			||||||
    align-items: center;
 | 
					    align-items: center;
 | 
				
			||||||
    padding: 100px;
 | 
					    padding: 50px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    img {
 | 
					    img {
 | 
				
			||||||
      height: 52px;
 | 
					      height: 52px;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -801,3 +801,58 @@ code {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.connection-prompt {
 | 
				
			||||||
 | 
					  margin-bottom: 25px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .fa-link {
 | 
				
			||||||
 | 
					    background-color: darken($ui-base-color, 4%);
 | 
				
			||||||
 | 
					    border-radius: 100%;
 | 
				
			||||||
 | 
					    font-size: 24px;
 | 
				
			||||||
 | 
					    padding: 10px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &__column {
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex: 1;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    flex-shrink: 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &-sep {
 | 
				
			||||||
 | 
					      flex-grow: 0;
 | 
				
			||||||
 | 
					      overflow: visible;
 | 
				
			||||||
 | 
					      position: relative;
 | 
				
			||||||
 | 
					      z-index: 1;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .account__avatar {
 | 
				
			||||||
 | 
					    margin-bottom: 20px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &__connection {
 | 
				
			||||||
 | 
					    background-color: lighten($ui-base-color, 8%);
 | 
				
			||||||
 | 
					    box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
 | 
				
			||||||
 | 
					    border-radius: 4px;
 | 
				
			||||||
 | 
					    padding: 25px 10px;
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &::after {
 | 
				
			||||||
 | 
					      background-color: darken($ui-base-color, 4%);
 | 
				
			||||||
 | 
					      content: '';
 | 
				
			||||||
 | 
					      display: block;
 | 
				
			||||||
 | 
					      height: 100%;
 | 
				
			||||||
 | 
					      left: 50%;
 | 
				
			||||||
 | 
					      position: absolute;
 | 
				
			||||||
 | 
					      width: 1px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &__row {
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: row;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,46 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					# == Schema Information
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Table name: account_identity_proofs
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  id                :bigint(8)        not null, primary key
 | 
				
			||||||
 | 
					#  account_id        :bigint(8)
 | 
				
			||||||
 | 
					#  provider          :string           default(""), not null
 | 
				
			||||||
 | 
					#  provider_username :string           default(""), not null
 | 
				
			||||||
 | 
					#  token             :text             default(""), not null
 | 
				
			||||||
 | 
					#  verified          :boolean          default(FALSE), not null
 | 
				
			||||||
 | 
					#  live              :boolean          default(FALSE), not null
 | 
				
			||||||
 | 
					#  created_at        :datetime         not null
 | 
				
			||||||
 | 
					#  updated_at        :datetime         not null
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AccountIdentityProof < ApplicationRecord
 | 
				
			||||||
 | 
					  belongs_to :account
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  validates :provider, inclusion: { in: ProofProvider::SUPPORTED_PROVIDERS }
 | 
				
			||||||
 | 
					  validates :provider_username, format: { with: /\A[a-z0-9_]+\z/i }, length: { minimum: 2, maximum: 15 }
 | 
				
			||||||
 | 
					  validates :provider_username, uniqueness: { scope: [:account_id, :provider] }
 | 
				
			||||||
 | 
					  validates :token, format: { with: /\A[a-f0-9]+\z/ }, length: { maximum: 66 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  validate :validate_with_provider, if: :token_changed?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  scope :active, -> { where(verified: true, live: true) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  after_create_commit :queue_worker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  delegate :refresh!, :on_success_path, :badge, to: :provider_instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def provider_instance
 | 
				
			||||||
 | 
					    @provider_instance ||= ProofProvider.find(provider, self)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def queue_worker
 | 
				
			||||||
 | 
					    provider_instance.worker_class.perform_async(id)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def validate_with_provider
 | 
				
			||||||
 | 
					    provider_instance.validate!
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -7,6 +7,9 @@ module AccountAssociations
 | 
				
			||||||
    # Local users
 | 
					    # Local users
 | 
				
			||||||
    has_one :user, inverse_of: :account, dependent: :destroy
 | 
					    has_one :user, inverse_of: :account, dependent: :destroy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Identity proofs
 | 
				
			||||||
 | 
					    has_many :identity_proofs, class_name: 'AccountIdentityProof', dependent: :destroy, inverse_of: :account
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Timelines
 | 
					    # Timelines
 | 
				
			||||||
    has_many :stream_entries, inverse_of: :account, dependent: :destroy
 | 
					    has_many :stream_entries, inverse_of: :account, dependent: :destroy
 | 
				
			||||||
    has_many :statuses, inverse_of: :account, dependent: :destroy
 | 
					    has_many :statuses, inverse_of: :account, dependent: :destroy
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -68,7 +68,7 @@ class SuspendAccountService < BaseService
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def purge_content!
 | 
					  def purge_content!
 | 
				
			||||||
    distribute_delete_actor! if @account.local?
 | 
					    distribute_delete_actor! if @account.local? && !@options[:skip_distribution]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @account.statuses.reorder(nil).find_in_batches do |statuses|
 | 
					    @account.statuses.reorder(nil).find_in_batches do |statuses|
 | 
				
			||||||
      BatchedRemoveStatusService.new.call(statuses, skip_side_effects: @options[:destroy])
 | 
					      BatchedRemoveStatusService.new.call(statuses, skip_side_effects: @options[:destroy])
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,23 +17,25 @@
 | 
				
			||||||
        = render 'registration'
 | 
					        = render 'registration'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      .directory
 | 
					      .directory
 | 
				
			||||||
        .directory__tag{ class: Setting.profile_directory ? nil : 'disabled' }
 | 
					        - if Setting.profile_directory
 | 
				
			||||||
          = optional_link_to Setting.profile_directory, explore_path do
 | 
					          .directory__tag
 | 
				
			||||||
            %h4
 | 
					            = optional_link_to Setting.profile_directory, explore_path do
 | 
				
			||||||
              = fa_icon 'address-book fw'
 | 
					              %h4
 | 
				
			||||||
              = t('about.discover_users')
 | 
					                = fa_icon 'address-book fw'
 | 
				
			||||||
              %small= t('about.browse_directory')
 | 
					                = t('about.discover_users')
 | 
				
			||||||
 | 
					                %small= t('about.browse_directory')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            .avatar-stack
 | 
					              .avatar-stack
 | 
				
			||||||
              - @instance_presenter.sample_accounts.each do |account|
 | 
					                - @instance_presenter.sample_accounts.each do |account|
 | 
				
			||||||
                = image_tag current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url, width: 48, height: 48, alt: '', class: 'account__avatar'
 | 
					                  = image_tag current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url, width: 48, height: 48, alt: '', class: 'account__avatar'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        .directory__tag{ class: Setting.timeline_preview ? nil : 'disabled' }
 | 
					        - if Setting.timeline_preview
 | 
				
			||||||
          = optional_link_to Setting.timeline_preview, public_timeline_path do
 | 
					          .directory__tag
 | 
				
			||||||
            %h4
 | 
					            = optional_link_to Setting.timeline_preview, public_timeline_path do
 | 
				
			||||||
              = fa_icon 'globe fw'
 | 
					              %h4
 | 
				
			||||||
              = t('about.see_whats_happening')
 | 
					                = fa_icon 'globe fw'
 | 
				
			||||||
              %small= t('about.browse_public_posts')
 | 
					                = t('about.see_whats_happening')
 | 
				
			||||||
 | 
					                %small= t('about.browse_public_posts')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        .directory__tag
 | 
					        .directory__tag
 | 
				
			||||||
          = link_to 'https://joinmastodon.org/apps', target: '_blank', rel: 'noopener' do
 | 
					          = link_to 'https://joinmastodon.org/apps', target: '_blank', rel: 'noopener' do
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,17 @@
 | 
				
			||||||
 | 
					- proofs = account.identity_proofs.active
 | 
				
			||||||
 | 
					- fields = account.fields
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.public-account-bio
 | 
					.public-account-bio
 | 
				
			||||||
  - unless account.fields.empty?
 | 
					  - unless fields.empty? && proofs.empty?
 | 
				
			||||||
    .account__header__fields
 | 
					    .account__header__fields
 | 
				
			||||||
      - account.fields.each do |field|
 | 
					      - proofs.each do |proof|
 | 
				
			||||||
 | 
					        %dl
 | 
				
			||||||
 | 
					          %dt= proof.provider.capitalize
 | 
				
			||||||
 | 
					          %dd.verified
 | 
				
			||||||
 | 
					            = link_to fa_icon('check'), proof.badge.proof_url, class: 'verified__mark', title: t('accounts.link_verified_on', date: l(proof.updated_at))
 | 
				
			||||||
 | 
					            = link_to proof.provider_username, proof.badge.profile_url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - fields.each do |field|
 | 
				
			||||||
        %dl
 | 
					        %dl
 | 
				
			||||||
          %dt.emojify{ title: field.name }= Formatter.instance.format_field(account, field.name, custom_emojify: true)
 | 
					          %dt.emojify{ title: field.name }= Formatter.instance.format_field(account, field.name, custom_emojify: true)
 | 
				
			||||||
          %dd{ title: field.value, class: custom_field_classes(field) }
 | 
					          %dd{ title: field.value, class: custom_field_classes(field) }
 | 
				
			||||||
| 
						 | 
					@ -9,6 +19,7 @@
 | 
				
			||||||
              %span.verified__mark{ title: t('accounts.link_verified_on', date: l(field.verified_at)) }
 | 
					              %span.verified__mark{ title: t('accounts.link_verified_on', date: l(field.verified_at)) }
 | 
				
			||||||
                = fa_icon 'check'
 | 
					                = fa_icon 'check'
 | 
				
			||||||
            = Formatter.instance.format_field(account, field.value, custom_emojify: true)
 | 
					            = Formatter.instance.format_field(account, field.value, custom_emojify: true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  = account_badge(account)
 | 
					  = account_badge(account)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  - if account.note.present?
 | 
					  - if account.note.present?
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,20 @@
 | 
				
			||||||
 | 
					%tr
 | 
				
			||||||
 | 
					  %td
 | 
				
			||||||
 | 
					    = link_to proof.badge.profile_url, class: 'name-tag' do
 | 
				
			||||||
 | 
					      = image_tag proof.badge.avatar_url, width: 15, height: 15, alt: '', class: 'avatar'
 | 
				
			||||||
 | 
					      %span.username
 | 
				
			||||||
 | 
					        = proof.provider_username
 | 
				
			||||||
 | 
					        %span= "(#{proof.provider.capitalize})"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  %td
 | 
				
			||||||
 | 
					    - if proof.live?
 | 
				
			||||||
 | 
					      %span.positive-hint
 | 
				
			||||||
 | 
					        = fa_icon 'check-circle fw'
 | 
				
			||||||
 | 
					        = t('identity_proofs.active')
 | 
				
			||||||
 | 
					    - else
 | 
				
			||||||
 | 
					      %span.negative-hint
 | 
				
			||||||
 | 
					        = fa_icon 'times-circle fw'
 | 
				
			||||||
 | 
					        = t('identity_proofs.inactive')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  %td
 | 
				
			||||||
 | 
					    = table_link_to 'external-link', t('identity_proofs.view_proof'), proof.badge.proof_url if proof.badge.proof_url
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,17 @@
 | 
				
			||||||
 | 
					- content_for :page_title do
 | 
				
			||||||
 | 
					  = t('settings.identity_proofs')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					%p= t('identity_proofs.explanation_html')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- unless @proofs.empty?
 | 
				
			||||||
 | 
					  %hr.spacer/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .table-wrapper
 | 
				
			||||||
 | 
					    %table.table
 | 
				
			||||||
 | 
					      %thead
 | 
				
			||||||
 | 
					        %tr
 | 
				
			||||||
 | 
					          %th= t('identity_proofs.identity')
 | 
				
			||||||
 | 
					          %th= t('identity_proofs.status')
 | 
				
			||||||
 | 
					          %th
 | 
				
			||||||
 | 
					      %tbody
 | 
				
			||||||
 | 
					        = render partial: 'settings/identity_proofs/proof', collection: @proofs, as: :proof
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,31 @@
 | 
				
			||||||
 | 
					- content_for :page_title do
 | 
				
			||||||
 | 
					  = t('identity_proofs.authorize_connection_prompt')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.form-container
 | 
				
			||||||
 | 
					  .oauth-prompt
 | 
				
			||||||
 | 
					    %h2= t('identity_proofs.authorize_connection_prompt')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  = simple_form_for @proof, url: settings_identity_proofs_url, html: { method: :post } do |f|
 | 
				
			||||||
 | 
					    = f.input :provider, as: :hidden
 | 
				
			||||||
 | 
					    = f.input :provider_username, as: :hidden
 | 
				
			||||||
 | 
					    = f.input :token, as: :hidden
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    = hidden_field_tag :user_agent, params[:user_agent]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .connection-prompt
 | 
				
			||||||
 | 
					      .connection-prompt__row.connection-prompt__connection
 | 
				
			||||||
 | 
					        .connection-prompt__column
 | 
				
			||||||
 | 
					          = image_tag current_account.avatar.url(:original), size: 96, class: 'account__avatar'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          %p= t('identity_proofs.i_am_html', username: content_tag(:strong,current_account.username), service: site_hostname)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .connection-prompt__column.connection-prompt__column-sep
 | 
				
			||||||
 | 
					          = fa_icon 'link'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .connection-prompt__column
 | 
				
			||||||
 | 
					          = image_tag @proof.badge.avatar_url, size: 96, class: 'account__avatar'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          %p= t('identity_proofs.i_am_html', username: content_tag(:strong, @proof.provider_username), service: @proof.provider.capitalize)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    = f.button :button, t('identity_proofs.authorize'), type: :submit
 | 
				
			||||||
 | 
					    = link_to t('simple_form.no'), settings_identity_proofs_url, class: 'button negative'
 | 
				
			||||||
| 
						 | 
					@ -41,6 +41,7 @@ module Mastodon
 | 
				
			||||||
      :ar,
 | 
					      :ar,
 | 
				
			||||||
      :ast,
 | 
					      :ast,
 | 
				
			||||||
      :bg,
 | 
					      :bg,
 | 
				
			||||||
 | 
					      :bn,
 | 
				
			||||||
      :ca,
 | 
					      :ca,
 | 
				
			||||||
      :co,
 | 
					      :co,
 | 
				
			||||||
      :cs,
 | 
					      :cs,
 | 
				
			||||||
| 
						 | 
					@ -54,8 +55,10 @@ module Mastodon
 | 
				
			||||||
      :fa,
 | 
					      :fa,
 | 
				
			||||||
      :fi,
 | 
					      :fi,
 | 
				
			||||||
      :fr,
 | 
					      :fr,
 | 
				
			||||||
 | 
					      :ga,
 | 
				
			||||||
      :gl,
 | 
					      :gl,
 | 
				
			||||||
      :he,
 | 
					      :he,
 | 
				
			||||||
 | 
					      :hi,
 | 
				
			||||||
      :hr,
 | 
					      :hr,
 | 
				
			||||||
      :hu,
 | 
					      :hu,
 | 
				
			||||||
      :hy,
 | 
					      :hy,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -637,6 +637,21 @@ en:
 | 
				
			||||||
    validation_errors:
 | 
					    validation_errors:
 | 
				
			||||||
      one: Something isn't quite right yet! Please review the error below
 | 
					      one: Something isn't quite right yet! Please review the error below
 | 
				
			||||||
      other: Something isn't quite right yet! Please review %{count} errors below
 | 
					      other: Something isn't quite right yet! Please review %{count} errors below
 | 
				
			||||||
 | 
					  identity_proofs:
 | 
				
			||||||
 | 
					    active: Active
 | 
				
			||||||
 | 
					    authorize: Yes, authorize
 | 
				
			||||||
 | 
					    authorize_connection_prompt: Authorize this cryptographic connection?
 | 
				
			||||||
 | 
					    errors:
 | 
				
			||||||
 | 
					      failed: The cryptographic connection failed. Please try again from %{provider}.
 | 
				
			||||||
 | 
					      keybase:
 | 
				
			||||||
 | 
					        invalid_token: Keybase tokens are hashes of signatures and must be 66 hex characters
 | 
				
			||||||
 | 
					        verification_failed: Keybase does not recognize this token as a signature of Keybase user %{kb_username}. Please retry from Keybase.
 | 
				
			||||||
 | 
					    explanation_html: Here you can cryptographically connect your other identities, such as a Keybase profile. This lets other people send you encrypted messages and trust content you send them.
 | 
				
			||||||
 | 
					    i_am_html: I am %{username} on %{service}.
 | 
				
			||||||
 | 
					    identity: Identity
 | 
				
			||||||
 | 
					    inactive: Inactive
 | 
				
			||||||
 | 
					    status: Verification status
 | 
				
			||||||
 | 
					    view_proof: View proof
 | 
				
			||||||
  imports:
 | 
					  imports:
 | 
				
			||||||
    modes:
 | 
					    modes:
 | 
				
			||||||
      merge: Merge
 | 
					      merge: Merge
 | 
				
			||||||
| 
						 | 
					@ -840,6 +855,7 @@ en:
 | 
				
			||||||
    export: Data export
 | 
					    export: Data export
 | 
				
			||||||
    featured_tags: Featured hashtags
 | 
					    featured_tags: Featured hashtags
 | 
				
			||||||
    flavours: Flavours
 | 
					    flavours: Flavours
 | 
				
			||||||
 | 
					    identity_proofs: Identity proofs
 | 
				
			||||||
    import: Import
 | 
					    import: Import
 | 
				
			||||||
    migrate: Account migration
 | 
					    migrate: Account migration
 | 
				
			||||||
    notifications: Notifications
 | 
					    notifications: Notifications
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +14,7 @@ SimpleNavigation::Configuration.run do |navigation|
 | 
				
			||||||
      settings.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url
 | 
					      settings.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url
 | 
				
			||||||
      settings.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url
 | 
					      settings.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url
 | 
				
			||||||
      settings.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url
 | 
					      settings.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url
 | 
				
			||||||
 | 
					      settings.item :identity_proofs, safe_join([fa_icon('key fw'), t('settings.identity_proofs')]), settings_identity_proofs_path, highlights_on: %r{/settings/identity_proofs*}
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    primary.item :flavours, safe_join([fa_icon('paint-brush fw'), t('settings.flavours')]), settings_flavours_url do |flavours|
 | 
					    primary.item :flavours, safe_join([fa_icon('paint-brush fw'), t('settings.flavours')]), settings_flavours_url do |flavours|
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,6 +22,8 @@ Rails.application.routes.draw do
 | 
				
			||||||
  get '.well-known/host-meta', to: 'well_known/host_meta#show', as: :host_meta, defaults: { format: 'xml' }
 | 
					  get '.well-known/host-meta', to: 'well_known/host_meta#show', as: :host_meta, defaults: { format: 'xml' }
 | 
				
			||||||
  get '.well-known/webfinger', to: 'well_known/webfinger#show', as: :webfinger
 | 
					  get '.well-known/webfinger', to: 'well_known/webfinger#show', as: :webfinger
 | 
				
			||||||
  get '.well-known/change-password', to: redirect('/auth/edit')
 | 
					  get '.well-known/change-password', to: redirect('/auth/edit')
 | 
				
			||||||
 | 
					  get '.well-known/keybase-proof-config', to: 'well_known/keybase_proof_config#show'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get 'manifest', to: 'manifests#show', defaults: { format: 'json' }
 | 
					  get 'manifest', to: 'manifests#show', defaults: { format: 'json' }
 | 
				
			||||||
  get 'intent', to: 'intents#show'
 | 
					  get 'intent', to: 'intents#show'
 | 
				
			||||||
  get 'custom.css', to: 'custom_css#show', as: :custom_css
 | 
					  get 'custom.css', to: 'custom_css#show', as: :custom_css
 | 
				
			||||||
| 
						 | 
					@ -107,6 +109,8 @@ Rails.application.routes.draw do
 | 
				
			||||||
      resource :confirmation, only: [:new, :create]
 | 
					      resource :confirmation, only: [:new, :create]
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    resources :identity_proofs, only: [:index, :show, :new, :create, :update]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    resources :applications, except: [:edit] do
 | 
					    resources :applications, except: [:edit] do
 | 
				
			||||||
      member do
 | 
					      member do
 | 
				
			||||||
        post :regenerate
 | 
					        post :regenerate
 | 
				
			||||||
| 
						 | 
					@ -251,6 +255,9 @@ Rails.application.routes.draw do
 | 
				
			||||||
    # OEmbed
 | 
					    # OEmbed
 | 
				
			||||||
    get '/oembed', to: 'oembed#show', as: :oembed
 | 
					    get '/oembed', to: 'oembed#show', as: :oembed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Identity proofs
 | 
				
			||||||
 | 
					    get :proofs, to: 'proofs#index'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # JSON / REST API
 | 
					    # JSON / REST API
 | 
				
			||||||
    namespace :v1 do
 | 
					    namespace :v1 do
 | 
				
			||||||
      resources :statuses, only: [:create, :show, :destroy] do
 | 
					      resources :statuses, only: [:create, :show, :destroy] do
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					class CreateAccountIdentityProofs < ActiveRecord::Migration[5.2]
 | 
				
			||||||
 | 
					  def change
 | 
				
			||||||
 | 
					    create_table :account_identity_proofs do |t|
 | 
				
			||||||
 | 
					      t.belongs_to :account, foreign_key: { on_delete: :cascade }
 | 
				
			||||||
 | 
					      t.string :provider, null: false, default: ''
 | 
				
			||||||
 | 
					      t.string :provider_username, null: false, default: ''
 | 
				
			||||||
 | 
					      t.text :token, null: false, default: ''
 | 
				
			||||||
 | 
					      t.boolean :verified, null: false, default: false
 | 
				
			||||||
 | 
					      t.boolean :live, null: false, default: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      t.timestamps null: false
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    add_index :account_identity_proofs, [:account_id, :provider, :provider_username], unique: true, name: :index_account_proofs_on_account_and_provider_and_username
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										14
									
								
								db/schema.rb
								
								
								
								
							
							
						
						
									
										14
									
								
								db/schema.rb
								
								
								
								
							| 
						 | 
					@ -36,6 +36,19 @@ ActiveRecord::Schema.define(version: 2019_03_17_135723) do
 | 
				
			||||||
    t.index ["account_id", "domain"], name: "index_account_domain_blocks_on_account_id_and_domain", unique: true
 | 
					    t.index ["account_id", "domain"], name: "index_account_domain_blocks_on_account_id_and_domain", unique: true
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  create_table "account_identity_proofs", force: :cascade do |t|
 | 
				
			||||||
 | 
					    t.bigint "account_id"
 | 
				
			||||||
 | 
					    t.string "provider", default: "", null: false
 | 
				
			||||||
 | 
					    t.string "provider_username", default: "", null: false
 | 
				
			||||||
 | 
					    t.text "token", default: "", null: false
 | 
				
			||||||
 | 
					    t.boolean "verified", default: false, null: false
 | 
				
			||||||
 | 
					    t.boolean "live", default: false, null: false
 | 
				
			||||||
 | 
					    t.datetime "created_at", null: false
 | 
				
			||||||
 | 
					    t.datetime "updated_at", null: false
 | 
				
			||||||
 | 
					    t.index ["account_id", "provider", "provider_username"], name: "index_account_proofs_on_account_and_provider_and_username", unique: true
 | 
				
			||||||
 | 
					    t.index ["account_id"], name: "index_account_identity_proofs_on_account_id"
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  create_table "account_moderation_notes", force: :cascade do |t|
 | 
					  create_table "account_moderation_notes", force: :cascade do |t|
 | 
				
			||||||
    t.text "content", null: false
 | 
					    t.text "content", null: false
 | 
				
			||||||
    t.bigint "account_id", null: false
 | 
					    t.bigint "account_id", null: false
 | 
				
			||||||
| 
						 | 
					@ -744,6 +757,7 @@ ActiveRecord::Schema.define(version: 2019_03_17_135723) do
 | 
				
			||||||
  add_foreign_key "account_conversations", "accounts", on_delete: :cascade
 | 
					  add_foreign_key "account_conversations", "accounts", on_delete: :cascade
 | 
				
			||||||
  add_foreign_key "account_conversations", "conversations", on_delete: :cascade
 | 
					  add_foreign_key "account_conversations", "conversations", on_delete: :cascade
 | 
				
			||||||
  add_foreign_key "account_domain_blocks", "accounts", name: "fk_206c6029bd", on_delete: :cascade
 | 
					  add_foreign_key "account_domain_blocks", "accounts", name: "fk_206c6029bd", on_delete: :cascade
 | 
				
			||||||
 | 
					  add_foreign_key "account_identity_proofs", "accounts", on_delete: :cascade
 | 
				
			||||||
  add_foreign_key "account_moderation_notes", "accounts"
 | 
					  add_foreign_key "account_moderation_notes", "accounts"
 | 
				
			||||||
  add_foreign_key "account_moderation_notes", "accounts", column: "target_account_id"
 | 
					  add_foreign_key "account_moderation_notes", "accounts", column: "target_account_id"
 | 
				
			||||||
  add_foreign_key "account_pins", "accounts", column: "target_account_id", on_delete: :cascade
 | 
					  add_foreign_key "account_pins", "accounts", column: "target_account_id", on_delete: :cascade
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,96 @@
 | 
				
			||||||
 | 
					require 'rails_helper'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe Api::ProofsController do
 | 
				
			||||||
 | 
					  let(:alice) { Fabricate(:account, username: 'alice') }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  before do
 | 
				
			||||||
 | 
					    stub_request(:get, 'https://keybase.io/_/api/1.0/sig/proof_valid.json?domain=cb6e6126.ngrok.io&kb_username=crypto_alice&sig_hash=111111111111111111111111111111111111111111111111111111111111111111&username=alice').to_return(status: 200, body: '{"proof_valid":true,"proof_live":false}')
 | 
				
			||||||
 | 
					    stub_request(:get, 'https://keybase.io/_/api/1.0/sig/proof_live.json?domain=cb6e6126.ngrok.io&kb_username=crypto_alice&sig_hash=111111111111111111111111111111111111111111111111111111111111111111&username=alice').to_return(status: 200, body: '{"proof_valid":true,"proof_live":true}')
 | 
				
			||||||
 | 
					    stub_request(:get, 'https://keybase.io/_/api/1.0/sig/proof_valid.json?domain=cb6e6126.ngrok.io&kb_username=hidden_alice&sig_hash=222222222222222222222222222222222222222222222222222222222222222222&username=alice').to_return(status: 200, body: '{"proof_valid":true,"proof_live":true}')
 | 
				
			||||||
 | 
					    stub_request(:get, 'https://keybase.io/_/api/1.0/sig/proof_live.json?domain=cb6e6126.ngrok.io&kb_username=hidden_alice&sig_hash=222222222222222222222222222222222222222222222222222222222222222222&username=alice').to_return(status: 200, body: '{"proof_valid":true,"proof_live":true}')
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'GET #index' do
 | 
				
			||||||
 | 
					    describe 'with a non-existent username' do
 | 
				
			||||||
 | 
					      it '404s' do
 | 
				
			||||||
 | 
					        get :index, params: { username: 'nonexistent', provider: 'keybase' }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response).to have_http_status(:not_found)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    describe 'with a user that has no proofs' do
 | 
				
			||||||
 | 
					      it 'is an empty list of signatures' do
 | 
				
			||||||
 | 
					        get :index, params: { username: alice.username, provider: 'keybase' }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(body_as_json[:signatures]).to eq []
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    describe 'with a user that has a live, valid proof' do
 | 
				
			||||||
 | 
					      let(:token1) { '111111111111111111111111111111111111111111111111111111111111111111' }
 | 
				
			||||||
 | 
					      let(:kb_name1) { 'crypto_alice' }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      before do
 | 
				
			||||||
 | 
					        Fabricate(:account_identity_proof, account: alice, verified: true, live: true, token: token1, provider_username: kb_name1)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'is a list with that proof in it' do
 | 
				
			||||||
 | 
					        get :index, params: { username: alice.username, provider: 'keybase' }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(body_as_json[:signatures]).to eq [
 | 
				
			||||||
 | 
					          { kb_username: kb_name1, sig_hash: token1 },
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      describe 'add one that is neither live nor valid' do
 | 
				
			||||||
 | 
					        let(:token2) { '222222222222222222222222222222222222222222222222222222222222222222' }
 | 
				
			||||||
 | 
					        let(:kb_name2) { 'hidden_alice' }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        before do
 | 
				
			||||||
 | 
					          Fabricate(:account_identity_proof, account: alice, verified: false, live: false, token: token2, provider_username: kb_name2)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'is a list with both proofs' do
 | 
				
			||||||
 | 
					          get :index, params: { username: alice.username, provider: 'keybase' }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          expect(body_as_json[:signatures]).to eq [
 | 
				
			||||||
 | 
					            { kb_username: kb_name1, sig_hash: token1 },
 | 
				
			||||||
 | 
					            { kb_username: kb_name2, sig_hash: token2 },
 | 
				
			||||||
 | 
					          ]
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    describe 'a user that has an avatar' do
 | 
				
			||||||
 | 
					      let(:alice) { Fabricate(:account, username: 'alice', avatar: attachment_fixture('avatar.gif')) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'and a proof' do
 | 
				
			||||||
 | 
					        let(:token1) { '111111111111111111111111111111111111111111111111111111111111111111' }
 | 
				
			||||||
 | 
					        let(:kb_name1) { 'crypto_alice' }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        before do
 | 
				
			||||||
 | 
					          Fabricate(:account_identity_proof, account: alice, verified: true, live: true, token: token1, provider_username: kb_name1)
 | 
				
			||||||
 | 
					          get :index, params: { username: alice.username, provider: 'keybase' }
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'has two keys: signatures and avatar' do
 | 
				
			||||||
 | 
					          expect(body_as_json.keys).to match_array [:signatures, :avatar]
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'has the correct signatures' do
 | 
				
			||||||
 | 
					          expect(body_as_json[:signatures]).to eq [
 | 
				
			||||||
 | 
					            { kb_username: kb_name1, sig_hash: token1 },
 | 
				
			||||||
 | 
					          ]
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'has the correct avatar url' do
 | 
				
			||||||
 | 
					          first_part = 'https://cb6e6126.ngrok.io/system/accounts/avatars/'
 | 
				
			||||||
 | 
					          last_part  = 'original/avatar.gif'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          expect(body_as_json[:avatar]).to match /#{Regexp.quote(first_part)}(?:\d{3,5}\/){3}#{Regexp.quote(last_part)}/
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,112 @@
 | 
				
			||||||
 | 
					require 'rails_helper'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe Settings::IdentityProofsController do
 | 
				
			||||||
 | 
					  render_views
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let(:user) { Fabricate(:user) }
 | 
				
			||||||
 | 
					  let(:valid_token) { '1'*66 }
 | 
				
			||||||
 | 
					  let(:kbname) { 'kbuser' }
 | 
				
			||||||
 | 
					  let(:provider) { 'keybase' }
 | 
				
			||||||
 | 
					  let(:findable_id) { Faker::Number.number(5) }
 | 
				
			||||||
 | 
					  let(:unfindable_id) { Faker::Number.number(5) }
 | 
				
			||||||
 | 
					  let(:postable_params) do
 | 
				
			||||||
 | 
					    { account_identity_proof: { provider: provider, provider_username: kbname, token: valid_token } }
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  before do
 | 
				
			||||||
 | 
					    allow_any_instance_of(ProofProvider::Keybase::Verifier).to receive(:status) { { 'proof_valid' => true, 'proof_live' => true } }
 | 
				
			||||||
 | 
					    sign_in user, scope: :user
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'new proof creation' do
 | 
				
			||||||
 | 
					    context 'GET #new with no existing proofs' do
 | 
				
			||||||
 | 
					      it 'redirects to :index' do
 | 
				
			||||||
 | 
					        get :new
 | 
				
			||||||
 | 
					        expect(response).to redirect_to settings_identity_proofs_path
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'POST #create' do
 | 
				
			||||||
 | 
					      context 'when saving works' do
 | 
				
			||||||
 | 
					        before do
 | 
				
			||||||
 | 
					          allow(ProofProvider::Keybase::Worker).to receive(:perform_async)
 | 
				
			||||||
 | 
					          allow_any_instance_of(ProofProvider::Keybase::Verifier).to receive(:valid?) { true }
 | 
				
			||||||
 | 
					          allow_any_instance_of(AccountIdentityProof).to receive(:on_success_path) { root_url }
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'serializes a ProofProvider::Keybase::Worker' do
 | 
				
			||||||
 | 
					          expect(ProofProvider::Keybase::Worker).to receive(:perform_async)
 | 
				
			||||||
 | 
					          post :create, params: postable_params
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'delegates redirection to the proof provider' do
 | 
				
			||||||
 | 
					          expect_any_instance_of(AccountIdentityProof).to receive(:on_success_path)
 | 
				
			||||||
 | 
					          post :create, params: postable_params
 | 
				
			||||||
 | 
					          expect(response).to redirect_to root_url
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'when saving fails' do
 | 
				
			||||||
 | 
					        before do
 | 
				
			||||||
 | 
					          allow_any_instance_of(ProofProvider::Keybase::Verifier).to receive(:valid?) { false }
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'redirects to :index' do
 | 
				
			||||||
 | 
					          post :create, params: postable_params
 | 
				
			||||||
 | 
					          expect(response).to redirect_to settings_identity_proofs_path
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'flashes a helpful message' do
 | 
				
			||||||
 | 
					          post :create, params: postable_params
 | 
				
			||||||
 | 
					          expect(flash[:alert]).to eq I18n.t('identity_proofs.errors.failed', provider: 'Keybase')
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'it can also do an update if the provider and username match an existing proof' do
 | 
				
			||||||
 | 
					        before do
 | 
				
			||||||
 | 
					          allow_any_instance_of(ProofProvider::Keybase::Verifier).to receive(:valid?) { true }
 | 
				
			||||||
 | 
					          allow(ProofProvider::Keybase::Worker).to receive(:perform_async)
 | 
				
			||||||
 | 
					          Fabricate(:account_identity_proof, account: user.account, provider: provider, provider_username: kbname)
 | 
				
			||||||
 | 
					          allow_any_instance_of(AccountIdentityProof).to receive(:on_success_path) { root_url }
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'calls update with the new token' do
 | 
				
			||||||
 | 
					          expect_any_instance_of(AccountIdentityProof).to receive(:save) do |proof|
 | 
				
			||||||
 | 
					            expect(proof.token).to eq valid_token
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          post :create, params: postable_params
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'GET #index' do
 | 
				
			||||||
 | 
					    context 'with no existing proofs' do
 | 
				
			||||||
 | 
					      it 'shows the helpful explanation' do
 | 
				
			||||||
 | 
					        get :index
 | 
				
			||||||
 | 
					        expect(response.body).to match I18n.t('identity_proofs.explanation_html')
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'with two proofs' do
 | 
				
			||||||
 | 
					      before do
 | 
				
			||||||
 | 
					        allow_any_instance_of(ProofProvider::Keybase::Verifier).to receive(:valid?) { true }
 | 
				
			||||||
 | 
					        @proof1 = Fabricate(:account_identity_proof, account: user.account)
 | 
				
			||||||
 | 
					        @proof2 = Fabricate(:account_identity_proof, account: user.account)
 | 
				
			||||||
 | 
					        allow_any_instance_of(AccountIdentityProof).to receive(:badge) { double(avatar_url: '', profile_url: '', proof_url: '') }
 | 
				
			||||||
 | 
					        allow_any_instance_of(AccountIdentityProof).to receive(:refresh!) { }
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'has the first proof username on the page' do
 | 
				
			||||||
 | 
					        get :index
 | 
				
			||||||
 | 
					        expect(response.body).to match /#{Regexp.quote(@proof1.provider_username)}/
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'has the second proof username on the page' do
 | 
				
			||||||
 | 
					        get :index
 | 
				
			||||||
 | 
					        expect(response.body).to match /#{Regexp.quote(@proof2.provider_username)}/
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,15 @@
 | 
				
			||||||
 | 
					require 'rails_helper'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe WellKnown::KeybaseProofConfigController, type: :controller do
 | 
				
			||||||
 | 
					  render_views
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'GET #show' do
 | 
				
			||||||
 | 
					    it 'renders json' do
 | 
				
			||||||
 | 
					      get :show
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(response).to have_http_status(200)
 | 
				
			||||||
 | 
					      expect(response.content_type).to eq 'application/json'
 | 
				
			||||||
 | 
					      expect { JSON.parse(response.body) }.not_to raise_exception
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					Fabricator(:account_identity_proof) do
 | 
				
			||||||
 | 
					  account
 | 
				
			||||||
 | 
					  provider 'keybase'
 | 
				
			||||||
 | 
					  provider_username { sequence(:provider_username) { |i| "#{Faker::Lorem.characters(15)}" } }
 | 
				
			||||||
 | 
					  token { sequence(:token) { |i| "#{i}#{Faker::Crypto.sha1()*2}"[0..65] } }
 | 
				
			||||||
 | 
					  verified false
 | 
				
			||||||
 | 
					  live false
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,82 @@
 | 
				
			||||||
 | 
					require 'rails_helper'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe ProofProvider::Keybase::Verifier do
 | 
				
			||||||
 | 
					  let(:my_domain) { Rails.configuration.x.local_domain }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let(:keybase_proof) do
 | 
				
			||||||
 | 
					    local_proof = AccountIdentityProof.new(
 | 
				
			||||||
 | 
					      provider: 'Keybase',
 | 
				
			||||||
 | 
					      provider_username: 'cryptoalice',
 | 
				
			||||||
 | 
					      token: '11111111111111111111111111'
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    described_class.new('alice', 'cryptoalice', '11111111111111111111111111')
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let(:query_params) do
 | 
				
			||||||
 | 
					    "domain=#{my_domain}&kb_username=cryptoalice&sig_hash=11111111111111111111111111&username=alice"
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe '#valid?' do
 | 
				
			||||||
 | 
					    let(:base_url) { 'https://keybase.io/_/api/1.0/sig/proof_valid.json' }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when valid' do
 | 
				
			||||||
 | 
					      before do
 | 
				
			||||||
 | 
					        json_response_body = '{"status":{"code":0,"name":"OK"},"proof_valid":true}'
 | 
				
			||||||
 | 
					        stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'calls out to keybase and returns true' do
 | 
				
			||||||
 | 
					        expect(keybase_proof.valid?).to eq true
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when invalid' do
 | 
				
			||||||
 | 
					      before do
 | 
				
			||||||
 | 
					        json_response_body = '{"status":{"code":0,"name":"OK"},"proof_valid":false}'
 | 
				
			||||||
 | 
					        stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'calls out to keybase and returns false' do
 | 
				
			||||||
 | 
					        expect(keybase_proof.valid?).to eq false
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'with an unexpected api response' do
 | 
				
			||||||
 | 
					      before do
 | 
				
			||||||
 | 
					        json_response_body = '{"status":{"code":100,"desc":"wrong size hex_id","fields":{"sig_hash":"wrong size hex_id"},"name":"INPUT_ERROR"}}'
 | 
				
			||||||
 | 
					        stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'swallows the error and returns false' do
 | 
				
			||||||
 | 
					        expect(keybase_proof.valid?).to eq false
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe '#status' do
 | 
				
			||||||
 | 
					    let(:base_url) { 'https://keybase.io/_/api/1.0/sig/proof_live.json' }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'with a normal response' do
 | 
				
			||||||
 | 
					      before do
 | 
				
			||||||
 | 
					        json_response_body = '{"status":{"code":0,"name":"OK"},"proof_live":false,"proof_valid":true}'
 | 
				
			||||||
 | 
					        stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'calls out to keybase and returns the status fields as proof_valid and proof_live' do
 | 
				
			||||||
 | 
					        expect(keybase_proof.status).to include({ 'proof_valid' => true, 'proof_live' => false })
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'with an unexpected keybase response' do
 | 
				
			||||||
 | 
					      before do
 | 
				
			||||||
 | 
					        json_response_body = '{"status":{"code":100,"desc":"missing non-optional field sig_hash","fields":{"sig_hash":"missing non-optional field sig_hash"},"name":"INPUT_ERROR"}}'
 | 
				
			||||||
 | 
					        stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'raises a ProofProvider::Keybase::UnexpectedResponseError' do
 | 
				
			||||||
 | 
					        expect { keybase_proof.status }.to raise_error ProofProvider::Keybase::UnexpectedResponseError
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
		Loading…
	
		Reference in New Issue