forked from treehouse/mastodon
Merge pull request #973 from ThibG/glitch-soc/merge-upstream
Merge upstream changessignup-info-prompt
commit
fad29fd230
|
@ -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