commit
bde9196b70
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)
|
||||||
|
|
|
@ -2,11 +2,14 @@
|
||||||
|
|
||||||
class ActivityPub::InboxesController < Api::BaseController
|
class ActivityPub::InboxesController < Api::BaseController
|
||||||
include SignatureVerification
|
include SignatureVerification
|
||||||
|
include JsonLdHelper
|
||||||
|
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if signed_request_account
|
if unknown_deleted_account?
|
||||||
|
head 202
|
||||||
|
elsif signed_request_account
|
||||||
upgrade_account
|
upgrade_account
|
||||||
process_payload
|
process_payload
|
||||||
head 202
|
head 202
|
||||||
|
@ -17,12 +20,19 @@ class ActivityPub::InboxesController < Api::BaseController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def unknown_deleted_account?
|
||||||
|
json = Oj.load(body, mode: :strict)
|
||||||
|
json['type'] == 'Delete' && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists?
|
||||||
|
rescue Oj::ParseError
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
@account = Account.find_local!(params[:account_username]) if params[:account_username]
|
@account = Account.find_local!(params[:account_username]) if params[:account_username]
|
||||||
end
|
end
|
||||||
|
|
||||||
def body
|
def body
|
||||||
@body ||= request.body.read
|
@body ||= request.body.read.force_encoding('UTF-8')
|
||||||
end
|
end
|
||||||
|
|
||||||
def upgrade_account
|
def upgrade_account
|
||||||
|
@ -36,6 +46,6 @@ class ActivityPub::InboxesController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_payload
|
def process_payload
|
||||||
ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body.force_encoding('UTF-8'), @account&.id)
|
ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body, @account&.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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: 'Հայերեն',
|
||||||
|
|
|
@ -69,9 +69,11 @@ export function normalizeStatus(status, normalOldStatus) {
|
||||||
export function normalizePoll(poll) {
|
export function normalizePoll(poll) {
|
||||||
const normalPoll = { ...poll };
|
const normalPoll = { ...poll };
|
||||||
|
|
||||||
|
const emojiMap = makeEmojiMap(normalPoll);
|
||||||
|
|
||||||
normalPoll.options = poll.options.map(option => ({
|
normalPoll.options = poll.options.map(option => ({
|
||||||
...option,
|
...option,
|
||||||
title_emojified: emojify(escapeTextContentForBrowser(option.title)),
|
title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return normalPoll;
|
return normalPoll;
|
||||||
|
|
|
@ -44,6 +44,11 @@ const timeRemainingString = (intl, date, now) => {
|
||||||
return relativeTime;
|
return relativeTime;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
|
||||||
|
obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
class Poll extends ImmutablePureComponent {
|
class Poll extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
@ -99,6 +104,12 @@ class Poll extends ImmutablePureComponent {
|
||||||
const active = !!this.state.selected[`${optionIndex}`];
|
const active = !!this.state.selected[`${optionIndex}`];
|
||||||
const showResults = poll.get('voted') || poll.get('expired');
|
const showResults = poll.get('voted') || poll.get('expired');
|
||||||
|
|
||||||
|
let titleEmojified = option.get('title_emojified');
|
||||||
|
if (!titleEmojified) {
|
||||||
|
const emojiMap = makeEmojiMap(poll);
|
||||||
|
titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={option.get('title')}>
|
<li key={option.get('title')}>
|
||||||
{showResults && (
|
{showResults && (
|
||||||
|
@ -122,7 +133,7 @@ class Poll extends ImmutablePureComponent {
|
||||||
{!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />}
|
{!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />}
|
||||||
{showResults && <span className='poll__number'>{Math.round(percent)}%</span>}
|
{showResults && <span className='poll__number'>{Math.round(percent)}%</span>}
|
||||||
|
|
||||||
<span dangerouslySetInnerHTML={{ __html: option.get('title_emojified', emojify(escapeTextContentForBrowser(option.get('title')))) }} />
|
<span dangerouslySetInnerHTML={{ __html: titleEmojified }} />
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 'flavours/glitch/actions/timelines';
|
import { expandHashtagTimeline } from 'flavours/glitch/actions/timelines';
|
||||||
import { connectHashtagStream } from 'flavours/glitch/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 'flavours/glitch/features/status/containers/detailed_status_container';
|
import DetailedStatusContainer from 'flavours/glitch/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 'flavours/glitch/actions/timelines';
|
import { expandPublicTimeline, expandCommunityTimeline } from 'flavours/glitch/actions/timelines';
|
||||||
import { connectPublicStream, connectCommunityStream } from 'flavours/glitch/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 'flavours/glitch/features/status/containers/detailed_status_container';
|
import DetailedStatusContainer from 'flavours/glitch/features/status/containers/detailed_status_container';
|
||||||
|
@ -42,24 +41,12 @@ class PublicTimeline extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = () => {
|
||||||
const { dispatch, statusIds, local } = this.props;
|
const { dispatch, statusIds, local } = this.props;
|
||||||
const maxId = statusIds.last();
|
const maxId = statusIds.last();
|
||||||
|
|
|
@ -660,7 +660,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 @@
|
||||||
|
<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 |
|
@ -71,9 +71,11 @@ export function normalizeStatus(status, normalOldStatus) {
|
||||||
export function normalizePoll(poll) {
|
export function normalizePoll(poll) {
|
||||||
const normalPoll = { ...poll };
|
const normalPoll = { ...poll };
|
||||||
|
|
||||||
|
const emojiMap = makeEmojiMap(normalPoll);
|
||||||
|
|
||||||
normalPoll.options = poll.options.map(option => ({
|
normalPoll.options = poll.options.map(option => ({
|
||||||
...option,
|
...option,
|
||||||
title_emojified: emojify(escapeTextContentForBrowser(option.title)),
|
title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return normalPoll;
|
return normalPoll;
|
||||||
|
|
|
@ -44,6 +44,11 @@ const timeRemainingString = (intl, date, now) => {
|
||||||
return relativeTime;
|
return relativeTime;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
|
||||||
|
obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
class Poll extends ImmutablePureComponent {
|
class Poll extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
@ -99,6 +104,12 @@ class Poll extends ImmutablePureComponent {
|
||||||
const active = !!this.state.selected[`${optionIndex}`];
|
const active = !!this.state.selected[`${optionIndex}`];
|
||||||
const showResults = poll.get('voted') || poll.get('expired');
|
const showResults = poll.get('voted') || poll.get('expired');
|
||||||
|
|
||||||
|
let titleEmojified = option.get('title_emojified');
|
||||||
|
if (!titleEmojified) {
|
||||||
|
const emojiMap = makeEmojiMap(poll);
|
||||||
|
titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={option.get('title')}>
|
<li key={option.get('title')}>
|
||||||
{showResults && (
|
{showResults && (
|
||||||
|
@ -122,7 +133,7 @@ class Poll extends ImmutablePureComponent {
|
||||||
{!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />}
|
{!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />}
|
||||||
{showResults && <span className='poll__number'>{Math.round(percent)}%</span>}
|
{showResults && <span className='poll__number'>{Math.round(percent)}%</span>}
|
||||||
|
|
||||||
<span dangerouslySetInnerHTML={{ __html: option.get('title_emojified', emojify(escapeTextContentForBrowser(option.get('title')))) }} />
|
<span dangerouslySetInnerHTML={{ __html: titleEmojified }} />
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -71,6 +71,12 @@ class Formatter
|
||||||
html.html_safe # rubocop:disable Rails/OutputSafety
|
html.html_safe # rubocop:disable Rails/OutputSafety
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def format_poll_option(status, option, **options)
|
||||||
|
html = encode(option.title)
|
||||||
|
html = encode_custom_emojis(html, status.emojis, options[:autoplay])
|
||||||
|
html.html_safe # rubocop:disable Rails/OutputSafety
|
||||||
|
end
|
||||||
|
|
||||||
def format_display_name(account, **options)
|
def format_display_name(account, **options)
|
||||||
html = encode(account.display_name.presence || account.username)
|
html = encode(account.display_name.presence || account.username)
|
||||||
html = encode_custom_emojis(html, account.emojis, options[:autoplay]) if options[:custom_emojify]
|
html = encode_custom_emojis(html, account.emojis, options[:autoplay]) if options[:custom_emojify]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -60,6 +60,10 @@ class Poll < ApplicationRecord
|
||||||
!local?
|
!local?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def emojis
|
||||||
|
@emojis ||= CustomEmoji.from_text(options.join(' '), account.domain)
|
||||||
|
end
|
||||||
|
|
||||||
class Option < ActiveModelSerializers::Model
|
class Option < ActiveModelSerializers::Model
|
||||||
attributes :id, :title, :votes_count, :poll
|
attributes :id, :title, :votes_count, :poll
|
||||||
|
|
||||||
|
|
|
@ -218,7 +218,11 @@ class Status < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def emojis
|
def emojis
|
||||||
@emojis ||= CustomEmoji.from_text([spoiler_text, text].join(' '), account.domain)
|
return @emojis if defined?(@emojis)
|
||||||
|
fields = [spoiler_text, text]
|
||||||
|
fields += owned_poll.options unless owned_poll.nil?
|
||||||
|
@emojis = CustomEmoji.from_text(fields.join(' '), account.domain)
|
||||||
|
@emojis
|
||||||
end
|
end
|
||||||
|
|
||||||
def mark_for_mass_destruction!
|
def mark_for_mass_destruction!
|
||||||
|
|
|
@ -5,6 +5,7 @@ class REST::PollSerializer < ActiveModel::Serializer
|
||||||
:multiple, :votes_count
|
:multiple, :votes_count
|
||||||
|
|
||||||
has_many :loaded_options, key: :options
|
has_many :loaded_options, key: :options
|
||||||
|
has_many :emojis, serializer: REST::CustomEmojiSerializer
|
||||||
|
|
||||||
attribute :voted, if: :current_user?
|
attribute :voted, if: :current_user?
|
||||||
|
|
||||||
|
|
|
@ -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'
|
|
@ -24,7 +24,7 @@
|
||||||
|
|
||||||
- if status.poll
|
- if status.poll
|
||||||
= react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do
|
= react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do
|
||||||
= render partial: 'stream_entries/poll', locals: { poll: status.poll }
|
= render partial: 'stream_entries/poll', locals: { status: status, poll: status.poll, autoplay: autoplay }
|
||||||
- elsif !status.media_attachments.empty?
|
- elsif !status.media_attachments.empty?
|
||||||
- if status.media_attachments.first.video?
|
- if status.media_attachments.first.video?
|
||||||
- video = status.media_attachments.first
|
- video = status.media_attachments.first
|
||||||
|
|
|
@ -10,11 +10,11 @@
|
||||||
|
|
||||||
%label.poll__text><
|
%label.poll__text><
|
||||||
%span.poll__number= percent.round
|
%span.poll__number= percent.round
|
||||||
= option.title
|
= Formatter.instance.format_poll_option(status, option, autoplay: autoplay)
|
||||||
- else
|
- else
|
||||||
%label.poll__text><
|
%label.poll__text><
|
||||||
%span.poll__input{ class: poll.multiple? ? 'checkbox' : nil}><
|
%span.poll__input{ class: poll.multiple? ? 'checkbox' : nil}><
|
||||||
= option.title
|
= Formatter.instance.format_poll_option(status, option, autoplay: autoplay)
|
||||||
.poll__footer
|
.poll__footer
|
||||||
- unless show_results
|
- unless show_results
|
||||||
%button.button.button-secondary{ disabled: true }
|
%button.button.button-secondary{ disabled: true }
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
|
|
||||||
- if status.poll
|
- if status.poll
|
||||||
= react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do
|
= react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do
|
||||||
= render partial: 'stream_entries/poll', locals: { poll: status.poll }
|
= render partial: 'stream_entries/poll', locals: { status: status, poll: status.poll, autoplay: autoplay }
|
||||||
- elsif !status.media_attachments.empty?
|
- elsif !status.media_attachments.empty?
|
||||||
- if status.media_attachments.first.video?
|
- if status.media_attachments.first.video?
|
||||||
- video = status.media_attachments.first
|
- video = status.media_attachments.first
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -10,7 +10,7 @@ RSpec.describe ActivityPub::InboxesController, type: :controller do
|
||||||
Fabricate(:account)
|
Fabricate(:account)
|
||||||
end
|
end
|
||||||
|
|
||||||
post :create
|
post :create, body: '{}'
|
||||||
expect(response).to have_http_status(202)
|
expect(response).to have_http_status(202)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -21,7 +21,7 @@ RSpec.describe ActivityPub::InboxesController, type: :controller do
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
post :create
|
post :create, body: '{}'
|
||||||
expect(response).to have_http_status(401)
|
expect(response).to have_http_status(401)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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