Merge branch 'master' into glitch-soc/merge-upstream
This commit is contained in:
		
						commit
						ce7d055d3c
					
				|  | @ -80,7 +80,7 @@ Rails/HttpStatus: | ||||||
| Rails/Exit: | Rails/Exit: | ||||||
|   Exclude: |   Exclude: | ||||||
|     - 'lib/mastodon/*' |     - 'lib/mastodon/*' | ||||||
|     - 'lib/cli' |     - 'lib/cli.rb' | ||||||
| 
 | 
 | ||||||
| Style/ClassAndModuleChildren: | Style/ClassAndModuleChildren: | ||||||
|   Enabled: false |   Enabled: false | ||||||
|  |  | ||||||
|  | @ -0,0 +1,19 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class Api::V1::Accounts::IdentityProofsController < Api::BaseController | ||||||
|  |   before_action :require_user! | ||||||
|  |   before_action :set_account | ||||||
|  | 
 | ||||||
|  |   respond_to :json | ||||||
|  | 
 | ||||||
|  |   def index | ||||||
|  |     @proofs = @account.identity_proofs.active | ||||||
|  |     render json: @proofs, each_serializer: REST::IdentityProofSerializer | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   private | ||||||
|  | 
 | ||||||
|  |   def set_account | ||||||
|  |     @account = Account.find(params[:account_id]) | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -18,7 +18,12 @@ class Settings::IdentityProofsController < Settings::BaseController | ||||||
|       provider_username: params[:provider_username] |       provider_username: params[:provider_username] | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|  |     if current_account.username == params[:username] | ||||||
|       render layout: 'auth' |       render layout: 'auth' | ||||||
|  |     else | ||||||
|  |       flash[:alert] = I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username) | ||||||
|  |       redirect_to settings_identity_proofs_path | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def create |   def create | ||||||
|  | @ -26,6 +31,7 @@ class Settings::IdentityProofsController < Settings::BaseController | ||||||
|     @proof.token = resource_params[:token] |     @proof.token = resource_params[:token] | ||||||
| 
 | 
 | ||||||
|     if @proof.save |     if @proof.save | ||||||
|  |       PostStatusService.new.call(current_user.account, text: post_params[:status_text]) if publish_proof? | ||||||
|       redirect_to @proof.on_success_path(params[:user_agent]) |       redirect_to @proof.on_success_path(params[:user_agent]) | ||||||
|     else |     else | ||||||
|       flash[:alert] = I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize) |       flash[:alert] = I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize) | ||||||
|  | @ -36,10 +42,22 @@ class Settings::IdentityProofsController < Settings::BaseController | ||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|   def check_required_params |   def check_required_params | ||||||
|     redirect_to settings_identity_proofs_path unless [:provider, :provider_username, :token].all? { |k| params[k].present? } |     redirect_to settings_identity_proofs_path unless [:provider, :provider_username, :username, :token].all? { |k| params[k].present? } | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def resource_params |   def resource_params | ||||||
|     params.require(:account_identity_proof).permit(:provider, :provider_username, :token) |     params.require(:account_identity_proof).permit(:provider, :provider_username, :token) | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   def publish_proof? | ||||||
|  |     ActiveModel::Type::Boolean.new.cast(post_params[:post_status]) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def post_params | ||||||
|  |     params.require(:account_identity_proof).permit(:post_status, :status_text) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def set_body_classes | ||||||
|  |     @body_classes = '' | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -0,0 +1,30 @@ | ||||||
|  | import api from '../api'; | ||||||
|  | 
 | ||||||
|  | export const IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST = 'IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST'; | ||||||
|  | export const IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS = 'IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS'; | ||||||
|  | export const IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL    = 'IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL'; | ||||||
|  | 
 | ||||||
|  | export const fetchAccountIdentityProofs = accountId => (dispatch, getState) => { | ||||||
|  |   dispatch(fetchAccountIdentityProofsRequest(accountId)); | ||||||
|  | 
 | ||||||
|  |   api(getState).get(`/api/v1/accounts/${accountId}/identity_proofs`) | ||||||
|  |     .then(({ data }) => dispatch(fetchAccountIdentityProofsSuccess(accountId, data))) | ||||||
|  |     .catch(err => dispatch(fetchAccountIdentityProofsFail(accountId, err))); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const fetchAccountIdentityProofsRequest = id => ({ | ||||||
|  |   type: IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST, | ||||||
|  |   id, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | export const fetchAccountIdentityProofsSuccess = (accountId, identity_proofs) => ({ | ||||||
|  |   type: IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS, | ||||||
|  |   accountId, | ||||||
|  |   identity_proofs, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | export const fetchAccountIdentityProofsFail = (accountId, err) => ({ | ||||||
|  |   type: IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL, | ||||||
|  |   accountId, | ||||||
|  |   err, | ||||||
|  | }); | ||||||
|  | @ -62,6 +62,7 @@ class Header extends ImmutablePureComponent { | ||||||
| 
 | 
 | ||||||
|   static propTypes = { |   static propTypes = { | ||||||
|     account: ImmutablePropTypes.map, |     account: ImmutablePropTypes.map, | ||||||
|  |     identity_props: ImmutablePropTypes.list, | ||||||
|     onFollow: PropTypes.func.isRequired, |     onFollow: PropTypes.func.isRequired, | ||||||
|     onBlock: PropTypes.func.isRequired, |     onBlock: PropTypes.func.isRequired, | ||||||
|     intl: PropTypes.object.isRequired, |     intl: PropTypes.object.isRequired, | ||||||
|  | @ -81,7 +82,7 @@ class Header extends ImmutablePureComponent { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   render () { |   render () { | ||||||
|     const { account, intl, domain } = this.props; |     const { account, intl, domain, identity_proofs } = this.props; | ||||||
| 
 | 
 | ||||||
|     if (!account) { |     if (!account) { | ||||||
|       return null; |       return null; | ||||||
|  | @ -234,8 +235,20 @@ class Header extends ImmutablePureComponent { | ||||||
| 
 | 
 | ||||||
|           <div className='account__header__extra'> |           <div className='account__header__extra'> | ||||||
|             <div className='account__header__bio'> |             <div className='account__header__bio'> | ||||||
|               {fields.size > 0 && ( |               { (fields.size > 0 || identity_proofs.size > 0) && ( | ||||||
|                 <div className='account__header__fields'> |                 <div className='account__header__fields'> | ||||||
|  |                   {identity_proofs.map((proof, i) => ( | ||||||
|  |                     <dl key={i}> | ||||||
|  |                       <dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} /> | ||||||
|  | 
 | ||||||
|  |                       <dd className='verified'> | ||||||
|  |                         <a href={proof.get('proof_url')} target='_blank' rel='noopener'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}> | ||||||
|  |                           <Icon id='check' className='verified__mark' /> | ||||||
|  |                         </span></a> | ||||||
|  |                         <a href={proof.get('profile_url')} target='_blank' rel='noopener'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a> | ||||||
|  |                       </dd> | ||||||
|  |                     </dl> | ||||||
|  |                   ))} | ||||||
|                   {fields.map((pair, i) => ( |                   {fields.map((pair, i) => ( | ||||||
|                     <dl key={i}> |                     <dl key={i}> | ||||||
|                       <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} /> |                       <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} /> | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ export default class Header extends ImmutablePureComponent { | ||||||
| 
 | 
 | ||||||
|   static propTypes = { |   static propTypes = { | ||||||
|     account: ImmutablePropTypes.map, |     account: ImmutablePropTypes.map, | ||||||
|  |     identity_proofs: ImmutablePropTypes.list, | ||||||
|     onFollow: PropTypes.func.isRequired, |     onFollow: PropTypes.func.isRequired, | ||||||
|     onBlock: PropTypes.func.isRequired, |     onBlock: PropTypes.func.isRequired, | ||||||
|     onMention: PropTypes.func.isRequired, |     onMention: PropTypes.func.isRequired, | ||||||
|  | @ -84,7 +85,7 @@ export default class Header extends ImmutablePureComponent { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   render () { |   render () { | ||||||
|     const { account, hideTabs } = this.props; |     const { account, hideTabs, identity_proofs } = this.props; | ||||||
| 
 | 
 | ||||||
|     if (account === null) { |     if (account === null) { | ||||||
|       return <MissingIndicator />; |       return <MissingIndicator />; | ||||||
|  | @ -96,6 +97,7 @@ export default class Header extends ImmutablePureComponent { | ||||||
| 
 | 
 | ||||||
|         <InnerHeader |         <InnerHeader | ||||||
|           account={account} |           account={account} | ||||||
|  |           identity_proofs={identity_proofs} | ||||||
|           onFollow={this.handleFollow} |           onFollow={this.handleFollow} | ||||||
|           onBlock={this.handleBlock} |           onBlock={this.handleBlock} | ||||||
|           onMention={this.handleMention} |           onMention={this.handleMention} | ||||||
|  |  | ||||||
|  | @ -21,6 +21,7 @@ import { openModal } from '../../../actions/modal'; | ||||||
| import { blockDomain, unblockDomain } from '../../../actions/domain_blocks'; | import { blockDomain, unblockDomain } from '../../../actions/domain_blocks'; | ||||||
| import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | ||||||
| import { unfollowModal } from '../../../initial_state'; | import { unfollowModal } from '../../../initial_state'; | ||||||
|  | import { List as ImmutableList } from 'immutable'; | ||||||
| 
 | 
 | ||||||
| const messages = defineMessages({ | const messages = defineMessages({ | ||||||
|   unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, |   unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, | ||||||
|  | @ -35,6 +36,7 @@ const makeMapStateToProps = () => { | ||||||
|   const mapStateToProps = (state, { accountId }) => ({ |   const mapStateToProps = (state, { accountId }) => ({ | ||||||
|     account: getAccount(state, accountId), |     account: getAccount(state, accountId), | ||||||
|     domain: state.getIn(['meta', 'domain']), |     domain: state.getIn(['meta', 'domain']), | ||||||
|  |     identity_proofs: state.getIn(['identity_proofs', accountId], ImmutableList()), | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   return mapStateToProps; |   return mapStateToProps; | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ import ColumnBackButton from '../../components/column_back_button'; | ||||||
| import { List as ImmutableList } from 'immutable'; | import { List as ImmutableList } from 'immutable'; | ||||||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | import ImmutablePureComponent from 'react-immutable-pure-component'; | ||||||
| import { FormattedMessage } from 'react-intl'; | import { FormattedMessage } from 'react-intl'; | ||||||
|  | import { fetchAccountIdentityProofs } from '../../actions/identity_proofs'; | ||||||
| 
 | 
 | ||||||
| const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => { | const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => { | ||||||
|   const path = withReplies ? `${accountId}:with_replies` : accountId; |   const path = withReplies ? `${accountId}:with_replies` : accountId; | ||||||
|  | @ -42,6 +43,7 @@ class AccountTimeline extends ImmutablePureComponent { | ||||||
|     const { params: { accountId }, withReplies } = this.props; |     const { params: { accountId }, withReplies } = this.props; | ||||||
| 
 | 
 | ||||||
|     this.props.dispatch(fetchAccount(accountId)); |     this.props.dispatch(fetchAccount(accountId)); | ||||||
|  |     this.props.dispatch(fetchAccountIdentityProofs(accountId)); | ||||||
|     if (!withReplies) { |     if (!withReplies) { | ||||||
|       this.props.dispatch(expandAccountFeaturedTimeline(accountId)); |       this.props.dispatch(expandAccountFeaturedTimeline(accountId)); | ||||||
|     } |     } | ||||||
|  | @ -51,6 +53,7 @@ class AccountTimeline extends ImmutablePureComponent { | ||||||
|   componentWillReceiveProps (nextProps) { |   componentWillReceiveProps (nextProps) { | ||||||
|     if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) { |     if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) { | ||||||
|       this.props.dispatch(fetchAccount(nextProps.params.accountId)); |       this.props.dispatch(fetchAccount(nextProps.params.accountId)); | ||||||
|  |       this.props.dispatch(fetchAccountIdentityProofs(nextProps.params.accountId)); | ||||||
|       if (!nextProps.withReplies) { |       if (!nextProps.withReplies) { | ||||||
|         this.props.dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId)); |         this.props.dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId)); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  | @ -83,7 +83,7 @@ | ||||||
|   "compose_form.spoiler.unmarked": "Text není skrytý", |   "compose_form.spoiler.unmarked": "Text není skrytý", | ||||||
|   "compose_form.spoiler_placeholder": "Sem napište vaše varování", |   "compose_form.spoiler_placeholder": "Sem napište vaše varování", | ||||||
|   "confirmation_modal.cancel": "Zrušit", |   "confirmation_modal.cancel": "Zrušit", | ||||||
|   "confirmations.block.block_and_report": "Block & Report", |   "confirmations.block.block_and_report": "Blokovat a nahlásit", | ||||||
|   "confirmations.block.confirm": "Blokovat", |   "confirmations.block.confirm": "Blokovat", | ||||||
|   "confirmations.block.message": "Jste si jistý/á, že chcete zablokovat uživatele {name}?", |   "confirmations.block.message": "Jste si jistý/á, že chcete zablokovat uživatele {name}?", | ||||||
|   "confirmations.delete.confirm": "Smazat", |   "confirmations.delete.confirm": "Smazat", | ||||||
|  |  | ||||||
|  | @ -0,0 +1,25 @@ | ||||||
|  | import { Map as ImmutableMap, fromJS } from 'immutable'; | ||||||
|  | import { | ||||||
|  |   IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST, | ||||||
|  |   IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS, | ||||||
|  |   IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL, | ||||||
|  | } from '../actions/identity_proofs'; | ||||||
|  | 
 | ||||||
|  | const initialState = ImmutableMap(); | ||||||
|  | 
 | ||||||
|  | export default function identityProofsReducer(state = initialState, action) { | ||||||
|  |   switch(action.type) { | ||||||
|  |   case IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST: | ||||||
|  |     return state.set('isLoading', true); | ||||||
|  |   case IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL: | ||||||
|  |     return state.set('isLoading', false); | ||||||
|  |   case IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS: | ||||||
|  |     return state.update(identity_proofs => identity_proofs.withMutations(map => { | ||||||
|  |       map.set('isLoading', false); | ||||||
|  |       map.set('loaded', true); | ||||||
|  |       map.set(action.accountId, fromJS(action.identity_proofs)); | ||||||
|  |     })); | ||||||
|  |   default: | ||||||
|  |     return state; | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | @ -30,6 +30,7 @@ import filters from './filters'; | ||||||
| import conversations from './conversations'; | import conversations from './conversations'; | ||||||
| import suggestions from './suggestions'; | import suggestions from './suggestions'; | ||||||
| import polls from './polls'; | import polls from './polls'; | ||||||
|  | import identity_proofs from './identity_proofs'; | ||||||
| 
 | 
 | ||||||
| const reducers = { | const reducers = { | ||||||
|   dropdown_menu, |   dropdown_menu, | ||||||
|  | @ -56,6 +57,7 @@ const reducers = { | ||||||
|   notifications, |   notifications, | ||||||
|   height_cache, |   height_cache, | ||||||
|   custom_emojis, |   custom_emojis, | ||||||
|  |   identity_proofs, | ||||||
|   lists, |   lists, | ||||||
|   listEditor, |   listEditor, | ||||||
|   listAdder, |   listAdder, | ||||||
|  |  | ||||||
|  | @ -3064,15 +3064,19 @@ a.status-card.compact:hover { | ||||||
| .relationship-tag { | .relationship-tag { | ||||||
|   color: $primary-text-color; |   color: $primary-text-color; | ||||||
|   margin-bottom: 4px; |   margin-bottom: 4px; | ||||||
|   opacity: 0.7; |  | ||||||
|   display: block; |   display: block; | ||||||
|   vertical-align: top; |   vertical-align: top; | ||||||
|   background-color: rgba($base-overlay-background, 0.4); |   background-color: $base-overlay-background; | ||||||
|   text-transform: uppercase; |   text-transform: uppercase; | ||||||
|   font-size: 11px; |   font-size: 11px; | ||||||
|   font-weight: 500; |   font-weight: 500; | ||||||
|   padding: 4px; |   padding: 4px; | ||||||
|   border-radius: 4px; |   border-radius: 4px; | ||||||
|  |   opacity: 0.7; | ||||||
|  | 
 | ||||||
|  |   &:hover { | ||||||
|  |     opacity: 1; | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .setting-toggle { | .setting-toggle { | ||||||
|  |  | ||||||
|  | @ -10,12 +10,10 @@ | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .logo-container { | .logo-container { | ||||||
|   margin: 100px auto; |   margin: 100px auto 50px; | ||||||
|   margin-bottom: 50px; |  | ||||||
| 
 | 
 | ||||||
|   @media screen and (max-width: 400px) { |   @media screen and (max-width: 500px) { | ||||||
|     margin: 30px auto; |     margin: 40px auto 0; | ||||||
|     margin-bottom: 20px; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   h1 { |   h1 { | ||||||
|  |  | ||||||
|  | @ -854,13 +854,19 @@ code { | ||||||
|     flex: 1; |     flex: 1; | ||||||
|     flex-direction: column; |     flex-direction: column; | ||||||
|     flex-shrink: 1; |     flex-shrink: 1; | ||||||
|  |     max-width: 50%; | ||||||
| 
 | 
 | ||||||
|     &-sep { |     &-sep { | ||||||
|  |       align-self: center; | ||||||
|       flex-grow: 0; |       flex-grow: 0; | ||||||
|       overflow: visible; |       overflow: visible; | ||||||
|       position: relative; |       position: relative; | ||||||
|       z-index: 1; |       z-index: 1; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     p { | ||||||
|  |       word-break: break-word; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   .account__avatar { |   .account__avatar { | ||||||
|  | @ -882,12 +888,13 @@ code { | ||||||
|       height: 100%; |       height: 100%; | ||||||
|       left: 50%; |       left: 50%; | ||||||
|       position: absolute; |       position: absolute; | ||||||
|  |       top: 0; | ||||||
|       width: 1px; |       width: 1px; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   &__row { |   &__row { | ||||||
|     align-items: center; |     align-items: flex-start; | ||||||
|     display: flex; |     display: flex; | ||||||
|     flex-direction: row; |     flex-direction: row; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,8 @@ | ||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| 
 | 
 | ||||||
| class ProofProvider::Keybase | class ProofProvider::Keybase | ||||||
|   BASE_URL = 'https://keybase.io' |   BASE_URL = ENV.fetch('KEYBASE_BASE_URL', 'https://keybase.io') | ||||||
|  |   DOMAIN = ENV.fetch('KEYBASE_DOMAIN', Rails.configuration.x.local_domain) | ||||||
| 
 | 
 | ||||||
|   class Error < StandardError; end |   class Error < StandardError; end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ class ProofProvider::Keybase::ConfigSerializer < ActiveModel::Serializer | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def domain |   def domain | ||||||
|     Rails.configuration.x.local_domain |     ProofProvider::Keybase::DOMAIN | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def display_name |   def display_name | ||||||
|  | @ -66,6 +66,6 @@ class ProofProvider::Keybase::ConfigSerializer < ActiveModel::Serializer | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def contact |   def contact | ||||||
|     [Setting.site_contact_email.presence].compact |     [Setting.site_contact_email.presence || 'unknown'].compact | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -49,14 +49,10 @@ class ProofProvider::Keybase::Verifier | ||||||
| 
 | 
 | ||||||
|   def query_params |   def query_params | ||||||
|     { |     { | ||||||
|       domain: domain, |       domain: ProofProvider::Keybase::DOMAIN, | ||||||
|       kb_username: @provider_username, |       kb_username: @provider_username, | ||||||
|       username: @local_username, |       username: @local_username, | ||||||
|       sig_hash: @token, |       sig_hash: @token, | ||||||
|     } |     } | ||||||
|   end |   end | ||||||
| 
 |  | ||||||
|   def domain |  | ||||||
|     Rails.configuration.x.local_domain |  | ||||||
|   end |  | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -26,7 +26,7 @@ class AccountIdentityProof < ApplicationRecord | ||||||
| 
 | 
 | ||||||
|   scope :active, -> { where(verified: true, live: true) } |   scope :active, -> { where(verified: true, live: true) } | ||||||
| 
 | 
 | ||||||
|   after_create_commit :queue_worker |   after_commit :queue_worker, if: :saved_change_to_token? | ||||||
| 
 | 
 | ||||||
|   delegate :refresh!, :on_success_path, :badge, to: :provider_instance |   delegate :refresh!, :on_success_path, :badge, to: :provider_instance | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,17 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class REST::IdentityProofSerializer < ActiveModel::Serializer | ||||||
|  |   attributes :provider, :provider_username, :updated_at, :proof_url, :profile_url | ||||||
|  | 
 | ||||||
|  |   def proof_url | ||||||
|  |     object.badge.proof_url | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def profile_url | ||||||
|  |     object.badge.profile_url | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def provider | ||||||
|  |     object.provider.capitalize | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -27,5 +27,10 @@ | ||||||
| 
 | 
 | ||||||
|           %p= t('identity_proofs.i_am_html', username: content_tag(:strong, @proof.provider_username), service: @proof.provider.capitalize) |           %p= t('identity_proofs.i_am_html', username: content_tag(:strong, @proof.provider_username), service: @proof.provider.capitalize) | ||||||
| 
 | 
 | ||||||
|  |     .connection-prompt__post | ||||||
|  |       = f.input :post_status, label: t('identity_proofs.publicize_checkbox'), as: :boolean, wrapper: :with_label, :input_html => { checked: true } | ||||||
|  | 
 | ||||||
|  |       = f.input :status_text, as: :text, input_html: { value: t('identity_proofs.publicize_toot', username: @proof.provider_username, service: @proof.provider.capitalize, url: @proof.badge.proof_url), rows: 4 } | ||||||
|  | 
 | ||||||
|     = f.button :button, t('identity_proofs.authorize'), type: :submit |     = f.button :button, t('identity_proofs.authorize'), type: :submit | ||||||
|     = link_to t('simple_form.no'), settings_identity_proofs_url, class: 'button negative' |     = link_to t('simple_form.no'), settings_identity_proofs_url, class: 'button negative' | ||||||
|  |  | ||||||
|  | @ -2,8 +2,9 @@ | ||||||
| cs: | cs: | ||||||
|   activerecord: |   activerecord: | ||||||
|     attributes: |     attributes: | ||||||
|       status: |       poll: | ||||||
|         owned_poll: Anketa |         expires_at: Uzávěrka | ||||||
|  |         options: Volby | ||||||
|     errors: |     errors: | ||||||
|       models: |       models: | ||||||
|         account: |         account: | ||||||
|  |  | ||||||
|  | @ -249,6 +249,7 @@ cs: | ||||||
|       feature_profile_directory: Adresář profilů |       feature_profile_directory: Adresář profilů | ||||||
|       feature_registrations: Registrace |       feature_registrations: Registrace | ||||||
|       feature_relay: Federovací most |       feature_relay: Federovací most | ||||||
|  |       feature_timeline_preview: Náhled časové osy | ||||||
|       features: Vlastnosti |       features: Vlastnosti | ||||||
|       hidden_service: Federace se skrytými službami |       hidden_service: Federace se skrytými službami | ||||||
|       open_reports: otevřená hlášení |       open_reports: otevřená hlášení | ||||||
|  |  | ||||||
|  | @ -652,10 +652,13 @@ en: | ||||||
|       keybase: |       keybase: | ||||||
|         invalid_token: Keybase tokens are hashes of signatures and must be 66 hex characters |         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. |         verification_failed: Keybase does not recognize this token as a signature of Keybase user %{kb_username}. Please retry from Keybase. | ||||||
|  |       wrong_user: Cannot create a proof for %{proving} while logged in as %{current}. Log in as %{proving} and try again. | ||||||
|     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. |     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}. |     i_am_html: I am %{username} on %{service}. | ||||||
|     identity: Identity |     identity: Identity | ||||||
|     inactive: Inactive |     inactive: Inactive | ||||||
|  |     publicize_checkbox: 'And toot this:' | ||||||
|  |     publicize_toot: 'It is proven! I am %{username} on %{service}: %{url}' | ||||||
|     status: Verification status |     status: Verification status | ||||||
|     view_proof: View proof |     view_proof: View proof | ||||||
|   imports: |   imports: | ||||||
|  |  | ||||||
|  | @ -366,6 +366,7 @@ Rails.application.routes.draw do | ||||||
|         resources :followers, only: :index, controller: 'accounts/follower_accounts' |         resources :followers, only: :index, controller: 'accounts/follower_accounts' | ||||||
|         resources :following, only: :index, controller: 'accounts/following_accounts' |         resources :following, only: :index, controller: 'accounts/following_accounts' | ||||||
|         resources :lists, only: :index, controller: 'accounts/lists' |         resources :lists, only: :index, controller: 'accounts/lists' | ||||||
|  |         resources :identity_proofs, only: :index, controller: 'accounts/identity_proofs' | ||||||
| 
 | 
 | ||||||
|         member do |         member do | ||||||
|           post :follow |           post :follow | ||||||
|  |  | ||||||
							
								
								
									
										73
									
								
								lib/cli.rb
								
								
								
								
							
							
						
						
									
										73
									
								
								lib/cli.rb
								
								
								
								
							|  | @ -41,6 +41,79 @@ module Mastodon | ||||||
|     desc 'domains SUBCOMMAND ...ARGS', 'Manage account domains' |     desc 'domains SUBCOMMAND ...ARGS', 'Manage account domains' | ||||||
|     subcommand 'domains', Mastodon::DomainsCLI |     subcommand 'domains', Mastodon::DomainsCLI | ||||||
| 
 | 
 | ||||||
|  |     option :dry_run, type: :boolean | ||||||
|  |     desc 'self-destruct', 'Erase the server from the federation' | ||||||
|  |     long_desc <<~LONG_DESC | ||||||
|  |       Erase the server from the federation by broadcasting account delete | ||||||
|  |       activities to all known other servers. This allows a "clean exit" from | ||||||
|  |       running a Mastodon server, as it leaves next to no cache behind on | ||||||
|  |       other servers. | ||||||
|  | 
 | ||||||
|  |       This command is always interactive and requires confirmation twice. | ||||||
|  | 
 | ||||||
|  |       No local data is actually deleted, because emptying the | ||||||
|  |       database or removing files is much faster through other, external | ||||||
|  |       means, such as e.g. deleting the entire VPS. However, because other | ||||||
|  |       servers will delete data about local users, but no local data will be | ||||||
|  |       updated (such as e.g. followers), there will be a state mismatch | ||||||
|  |       that will lead to glitches and issues if you then continue to run and use | ||||||
|  |       the server. | ||||||
|  | 
 | ||||||
|  |       So either you know exactly what you are doing, or you are starting | ||||||
|  |       from a blank slate afterwards by manually clearing out all the local | ||||||
|  |       data! | ||||||
|  |     LONG_DESC | ||||||
|  |     def self_destruct | ||||||
|  |       require 'tty-prompt' | ||||||
|  | 
 | ||||||
|  |       prompt = TTY::Prompt.new | ||||||
|  | 
 | ||||||
|  |       exit(1) unless prompt.ask('Type in the domain of the server to confirm:', required: true) == Rails.configuration.x.local_domain | ||||||
|  | 
 | ||||||
|  |       prompt.warn('This operation WILL NOT be reversible. It can also take a long time.') | ||||||
|  |       prompt.warn('While the data won\'t be erased locally, the server will be in a BROKEN STATE afterwards.') | ||||||
|  |       prompt.warn('A running Sidekiq process is required. Do not shut it down until queues clear.') | ||||||
|  | 
 | ||||||
|  |       exit(1) if prompt.no?('Are you sure you want to proceed?') | ||||||
|  | 
 | ||||||
|  |       inboxes   = Account.inboxes | ||||||
|  |       processed = 0 | ||||||
|  |       dry_run   = options[:dry_run] ? ' (DRY RUN)' : '' | ||||||
|  | 
 | ||||||
|  |       if inboxes.empty? | ||||||
|  |         prompt.ok('It seems like your server has not federated with anything') | ||||||
|  |         prompt.ok('You can shut it down and delete it any time') | ||||||
|  |         return | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       prompt.warn('Do NOT interrupt this process...') | ||||||
|  | 
 | ||||||
|  |       Account.local.without_suspended.find_each do |account| | ||||||
|  |         payload = ActiveModelSerializers::SerializableResource.new( | ||||||
|  |           account, | ||||||
|  |           serializer: ActivityPub::DeleteActorSerializer, | ||||||
|  |           adapter: ActivityPub::Adapter | ||||||
|  |         ).as_json | ||||||
|  | 
 | ||||||
|  |         json = Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(account)) | ||||||
|  | 
 | ||||||
|  |         unless options[:dry_run] | ||||||
|  |           ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url| | ||||||
|  |             [json, account.id, inbox_url] | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           account.update_column(:suspended, true) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         processed += 1 | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       prompt.ok("Queued #{inboxes.size * processed} items into Sidekiq for #{processed} accounts#{dry_run}") | ||||||
|  |       prompt.ok('Wait until Sidekiq processes all items, then you can shut everything down and delete the data') | ||||||
|  |     rescue TTY::Reader::InputInterrupt | ||||||
|  |       exit(1) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     map %w(--version -v) => :version |     map %w(--version -v) => :version | ||||||
| 
 | 
 | ||||||
|     desc 'version', 'Show version' |     desc 'version', 'Show version' | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| require 'rails_helper' | require 'rails_helper' | ||||||
| 
 | 
 | ||||||
| describe Settings::IdentityProofsController do | describe Settings::IdentityProofsController do | ||||||
|  |   include RoutingHelper | ||||||
|   render_views |   render_views | ||||||
| 
 | 
 | ||||||
|   let(:user) { Fabricate(:user) } |   let(:user) { Fabricate(:user) } | ||||||
|  | @ -9,8 +10,15 @@ describe Settings::IdentityProofsController do | ||||||
|   let(:provider) { 'keybase' } |   let(:provider) { 'keybase' } | ||||||
|   let(:findable_id) { Faker::Number.number(5) } |   let(:findable_id) { Faker::Number.number(5) } | ||||||
|   let(:unfindable_id) { Faker::Number.number(5) } |   let(:unfindable_id) { Faker::Number.number(5) } | ||||||
|  |   let(:new_proof_params) do | ||||||
|  |     { provider: provider, provider_username: kbname, token: valid_token, username: user.account.username } | ||||||
|  |   end | ||||||
|  |   let(:status_text) { "i just proved that i am also #{kbname} on #{provider}." } | ||||||
|  |   let(:status_posting_params) do | ||||||
|  |     { post_status: '0', status_text: status_text } | ||||||
|  |   end | ||||||
|   let(:postable_params) do |   let(:postable_params) do | ||||||
|     { account_identity_proof: { provider: provider, provider_username: kbname, token: valid_token } } |     { account_identity_proof: new_proof_params.merge(status_posting_params) } | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   before do |   before do | ||||||
|  | @ -19,13 +27,35 @@ describe Settings::IdentityProofsController do | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe 'new proof creation' do |   describe 'new proof creation' do | ||||||
|     context 'GET #new with no existing proofs' do |     context 'GET #new' do | ||||||
|  |       context 'with all of the correct params' do | ||||||
|  |         before do | ||||||
|  |           allow_any_instance_of(ProofProvider::Keybase::Badge).to receive(:avatar_url) { full_pack_url('media/images/void.png') } | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it 'renders the template' do | ||||||
|  |           get :new, params: new_proof_params | ||||||
|  |           expect(response).to render_template(:new) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'without any params' do | ||||||
|         it 'redirects to :index' do |         it 'redirects to :index' do | ||||||
|         get :new |           get :new, params: {} | ||||||
|           expect(response).to redirect_to settings_identity_proofs_path |           expect(response).to redirect_to settings_identity_proofs_path | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |       context 'with params to prove a different, not logged-in user' do | ||||||
|  |         let(:wrong_user_params) { new_proof_params.merge(username: 'someone_else') } | ||||||
|  | 
 | ||||||
|  |         it 'shows a helpful alert' do | ||||||
|  |           get :new, params: wrong_user_params | ||||||
|  |           expect(flash[:alert]).to eq I18n.t('identity_proofs.errors.wrong_user', proving: 'someone_else', current: user.account.username) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     context 'POST #create' do |     context 'POST #create' do | ||||||
|       context 'when saving works' do |       context 'when saving works' do | ||||||
|         before do |         before do | ||||||
|  | @ -44,6 +74,23 @@ describe Settings::IdentityProofsController do | ||||||
|           post :create, params: postable_params |           post :create, params: postable_params | ||||||
|           expect(response).to redirect_to root_url |           expect(response).to redirect_to root_url | ||||||
|         end |         end | ||||||
|  | 
 | ||||||
|  |         it 'does not post a status' do | ||||||
|  |           expect(PostStatusService).not_to receive(:new) | ||||||
|  |           post :create, params: postable_params | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         context 'and the user has requested to post a status' do | ||||||
|  |           let(:postable_params_with_status) do | ||||||
|  |             postable_params.tap { |p| p[:account_identity_proof][:post_status] = '1' } | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           it 'posts a status' do | ||||||
|  |             expect_any_instance_of(PostStatusService).to receive(:call).with(user.account, text: status_text) | ||||||
|  | 
 | ||||||
|  |             post :create, params: postable_params_with_status | ||||||
|  |           end | ||||||
|  |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'when saving fails' do |       context 'when saving fails' do | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue