Merge branch 'master' into glitch-soc/merge-upstream

signup-info-prompt
Thibaut Girka 2019-03-28 18:35:25 +01:00
commit ce7d055d3c
26 changed files with 299 additions and 31 deletions

View File

@ -80,7 +80,7 @@ Rails/HttpStatus:
Rails/Exit:
Exclude:
- 'lib/mastodon/*'
- 'lib/cli'
- 'lib/cli.rb'
Style/ClassAndModuleChildren:
Enabled: false

View File

@ -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

View File

@ -18,7 +18,12 @@ class Settings::IdentityProofsController < Settings::BaseController
provider_username: params[:provider_username]
)
if current_account.username == params[:username]
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
def create
@ -26,6 +31,7 @@ class Settings::IdentityProofsController < Settings::BaseController
@proof.token = resource_params[:token]
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])
else
flash[:alert] = I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize)
@ -36,10 +42,22 @@ class Settings::IdentityProofsController < Settings::BaseController
private
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
def resource_params
params.require(:account_identity_proof).permit(:provider, :provider_username, :token)
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

View File

@ -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,
});

View File

@ -62,6 +62,7 @@ class Header extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
identity_props: ImmutablePropTypes.list,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
@ -81,7 +82,7 @@ class Header extends ImmutablePureComponent {
}
render () {
const { account, intl, domain } = this.props;
const { account, intl, domain, identity_proofs } = this.props;
if (!account) {
return null;
@ -234,8 +235,20 @@ class Header extends ImmutablePureComponent {
<div className='account__header__extra'>
<div className='account__header__bio'>
{fields.size > 0 && (
{ (fields.size > 0 || identity_proofs.size > 0) && (
<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) => (
<dl key={i}>
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />

View File

@ -12,6 +12,7 @@ export default class Header extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
identity_proofs: ImmutablePropTypes.list,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
@ -84,7 +85,7 @@ export default class Header extends ImmutablePureComponent {
}
render () {
const { account, hideTabs } = this.props;
const { account, hideTabs, identity_proofs } = this.props;
if (account === null) {
return <MissingIndicator />;
@ -96,6 +97,7 @@ export default class Header extends ImmutablePureComponent {
<InnerHeader
account={account}
identity_proofs={identity_proofs}
onFollow={this.handleFollow}
onBlock={this.handleBlock}
onMention={this.handleMention}

View File

@ -21,6 +21,7 @@ import { openModal } from '../../../actions/modal';
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { unfollowModal } from '../../../initial_state';
import { List as ImmutableList } from 'immutable';
const messages = defineMessages({
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
@ -35,6 +36,7 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, { accountId }) => ({
account: getAccount(state, accountId),
domain: state.getIn(['meta', 'domain']),
identity_proofs: state.getIn(['identity_proofs', accountId], ImmutableList()),
});
return mapStateToProps;

View File

@ -12,6 +12,7 @@ import ColumnBackButton from '../../components/column_back_button';
import { List as ImmutableList } from 'immutable';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import { fetchAccountIdentityProofs } from '../../actions/identity_proofs';
const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => {
const path = withReplies ? `${accountId}:with_replies` : accountId;
@ -42,6 +43,7 @@ class AccountTimeline extends ImmutablePureComponent {
const { params: { accountId }, withReplies } = this.props;
this.props.dispatch(fetchAccount(accountId));
this.props.dispatch(fetchAccountIdentityProofs(accountId));
if (!withReplies) {
this.props.dispatch(expandAccountFeaturedTimeline(accountId));
}
@ -51,6 +53,7 @@ class AccountTimeline extends ImmutablePureComponent {
componentWillReceiveProps (nextProps) {
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(fetchAccountIdentityProofs(nextProps.params.accountId));
if (!nextProps.withReplies) {
this.props.dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId));
}

View File

@ -83,7 +83,7 @@
"compose_form.spoiler.unmarked": "Text není skrytý",
"compose_form.spoiler_placeholder": "Sem napište vaše varování",
"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.message": "Jste si jistý/á, že chcete zablokovat uživatele {name}?",
"confirmations.delete.confirm": "Smazat",

View File

@ -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;
}
};

View File

@ -30,6 +30,7 @@ import filters from './filters';
import conversations from './conversations';
import suggestions from './suggestions';
import polls from './polls';
import identity_proofs from './identity_proofs';
const reducers = {
dropdown_menu,
@ -56,6 +57,7 @@ const reducers = {
notifications,
height_cache,
custom_emojis,
identity_proofs,
lists,
listEditor,
listAdder,

View File

@ -3064,15 +3064,19 @@ a.status-card.compact:hover {
.relationship-tag {
color: $primary-text-color;
margin-bottom: 4px;
opacity: 0.7;
display: block;
vertical-align: top;
background-color: rgba($base-overlay-background, 0.4);
background-color: $base-overlay-background;
text-transform: uppercase;
font-size: 11px;
font-weight: 500;
padding: 4px;
border-radius: 4px;
opacity: 0.7;
&:hover {
opacity: 1;
}
}
.setting-toggle {

View File

@ -10,12 +10,10 @@
}
.logo-container {
margin: 100px auto;
margin-bottom: 50px;
margin: 100px auto 50px;
@media screen and (max-width: 400px) {
margin: 30px auto;
margin-bottom: 20px;
@media screen and (max-width: 500px) {
margin: 40px auto 0;
}
h1 {

View File

@ -854,13 +854,19 @@ code {
flex: 1;
flex-direction: column;
flex-shrink: 1;
max-width: 50%;
&-sep {
align-self: center;
flex-grow: 0;
overflow: visible;
position: relative;
z-index: 1;
}
p {
word-break: break-word;
}
}
.account__avatar {
@ -882,12 +888,13 @@ code {
height: 100%;
left: 50%;
position: absolute;
top: 0;
width: 1px;
}
}
&__row {
align-items: center;
align-items: flex-start;
display: flex;
flex-direction: row;
}

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true
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

View File

@ -14,7 +14,7 @@ class ProofProvider::Keybase::ConfigSerializer < ActiveModel::Serializer
end
def domain
Rails.configuration.x.local_domain
ProofProvider::Keybase::DOMAIN
end
def display_name
@ -66,6 +66,6 @@ class ProofProvider::Keybase::ConfigSerializer < ActiveModel::Serializer
end
def contact
[Setting.site_contact_email.presence].compact
[Setting.site_contact_email.presence || 'unknown'].compact
end
end

View File

@ -49,14 +49,10 @@ class ProofProvider::Keybase::Verifier
def query_params
{
domain: domain,
domain: ProofProvider::Keybase::DOMAIN,
kb_username: @provider_username,
username: @local_username,
sig_hash: @token,
}
end
def domain
Rails.configuration.x.local_domain
end
end

View File

@ -26,7 +26,7 @@ class AccountIdentityProof < ApplicationRecord
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

View File

@ -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

View File

@ -27,5 +27,10 @@
%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
= link_to t('simple_form.no'), settings_identity_proofs_url, class: 'button negative'

View File

@ -2,8 +2,9 @@
cs:
activerecord:
attributes:
status:
owned_poll: Anketa
poll:
expires_at: Uzávěrka
options: Volby
errors:
models:
account:

View File

@ -249,6 +249,7 @@ cs:
feature_profile_directory: Adresář profilů
feature_registrations: Registrace
feature_relay: Federovací most
feature_timeline_preview: Náhled časové osy
features: Vlastnosti
hidden_service: Federace se skrytými službami
open_reports: otevřená hlášení

View File

@ -652,10 +652,13 @@ en:
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.
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.
i_am_html: I am %{username} on %{service}.
identity: Identity
inactive: Inactive
publicize_checkbox: 'And toot this:'
publicize_toot: 'It is proven! I am %{username} on %{service}: %{url}'
status: Verification status
view_proof: View proof
imports:

View File

@ -366,6 +366,7 @@ Rails.application.routes.draw do
resources :followers, only: :index, controller: 'accounts/follower_accounts'
resources :following, only: :index, controller: 'accounts/following_accounts'
resources :lists, only: :index, controller: 'accounts/lists'
resources :identity_proofs, only: :index, controller: 'accounts/identity_proofs'
member do
post :follow

View File

@ -41,6 +41,79 @@ module Mastodon
desc 'domains SUBCOMMAND ...ARGS', 'Manage account domains'
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
desc 'version', 'Show version'

View File

@ -1,6 +1,7 @@
require 'rails_helper'
describe Settings::IdentityProofsController do
include RoutingHelper
render_views
let(:user) { Fabricate(:user) }
@ -9,8 +10,15 @@ describe Settings::IdentityProofsController do
let(:provider) { 'keybase' }
let(:findable_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
{ account_identity_proof: { provider: provider, provider_username: kbname, token: valid_token } }
{ account_identity_proof: new_proof_params.merge(status_posting_params) }
end
before do
@ -19,13 +27,35 @@ describe Settings::IdentityProofsController do
end
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
get :new
get :new, params: {}
expect(response).to redirect_to settings_identity_proofs_path
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 'when saving works' do
before do
@ -44,6 +74,23 @@ describe Settings::IdentityProofsController do
post :create, params: postable_params
expect(response).to redirect_to root_url
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
context 'when saving fails' do