Followers-only post federation (#2111)
* Make private toots get PuSHed to subscription URLs that belong to domains where you have approved followers * Authorized followers controller, stub for bulk action * Soft block in the background * Add simple test for new controller * Rename Settings::FollowersController to Settings::FollowerDomainsController, paginate results, rename "private" post setting to "followers-only", fix pagination style, improve post privacy preferences style, improve warning style * Extract compose form warnings into own container, show warning when posting to followers-only with unlocked accountmain
parent
ef5937da1f
commit
501514960a
@ -0,0 +1,25 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class Warning extends React.PureComponent {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { message } = this.props;
|
||||
|
||||
return (
|
||||
<div className='compose-form__warning'>
|
||||
{message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Warning.propTypes = {
|
||||
message: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
export default Warning;
|
@ -0,0 +1,48 @@
|
||||
import { connect } from 'react-redux';
|
||||
import Warning from '../components/warning';
|
||||
import { createSelector } from 'reselect';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const getMentionedUsernames = createSelector(state => state.getIn(['compose', 'text']), text => text.match(/(?:^|[^\/\w])@([a-z0-9_]+@[a-z0-9\.\-]+)/ig));
|
||||
|
||||
const getMentionedDomains = createSelector(getMentionedUsernames, mentionedUsernamesWithDomains => {
|
||||
return mentionedUsernamesWithDomains !== null ? [...new Set(mentionedUsernamesWithDomains.map(item => item.split('@')[2]))] : [];
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const mentionedUsernames = getMentionedUsernames(state);
|
||||
const mentionedUsernamesWithDomains = getMentionedDomains(state);
|
||||
|
||||
return {
|
||||
needsLeakWarning: (state.getIn(['compose', 'privacy']) === 'private' || state.getIn(['compose', 'privacy']) === 'direct') && mentionedUsernames !== null,
|
||||
mentionedDomains: mentionedUsernamesWithDomains,
|
||||
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', state.getIn(['meta', 'me']), 'locked'])
|
||||
};
|
||||
};
|
||||
|
||||
const WarningWrapper = ({ needsLeakWarning, needsLockWarning, mentionedDomains }) => {
|
||||
if (needsLockWarning) {
|
||||
return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
|
||||
} else if (needsLeakWarning) {
|
||||
return (
|
||||
<Warning
|
||||
message={<FormattedMessage
|
||||
id='compose_form.privacy_disclaimer'
|
||||
defaultMessage='Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}} to not leak your status?'
|
||||
values={{ domains: <strong>{mentionedDomains.join(', ')}</strong>, domainsCount: mentionedDomains.length }}
|
||||
/>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
WarningWrapper.propTypes = {
|
||||
needsLeakWarning: PropTypes.bool,
|
||||
needsLockWarning: PropTypes.bool,
|
||||
mentionedDomains: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(WarningWrapper);
|
@ -0,0 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Settings::FollowerDomainsController < ApplicationController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
|
||||
def show
|
||||
@account = current_account
|
||||
@domains = current_account.followers.reorder(nil).group('accounts.domain').select('accounts.domain, count(accounts.id) as accounts_from_domain').page(params[:page]).per(10)
|
||||
end
|
||||
|
||||
def update
|
||||
domains = bulk_params[:select] || []
|
||||
|
||||
domains.each do |domain|
|
||||
SoftBlockDomainFollowersWorker.perform_async(current_account.id, domain)
|
||||
end
|
||||
|
||||
redirect_to settings_follower_domains_path, notice: I18n.t('followers.success', count: domains.size)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def bulk_params
|
||||
params.permit(select: [])
|
||||
end
|
||||
end
|
@ -0,0 +1,33 @@
|
||||
- content_for :page_title do
|
||||
= t('settings.followers')
|
||||
|
||||
= form_tag settings_follower_domains_path, method: :patch, class: 'table-form' do
|
||||
- unless @account.locked?
|
||||
.warning
|
||||
%strong
|
||||
= fa_icon('warning')
|
||||
= t('followers.unlocked_warning_title')
|
||||
= t('followers.unlocked_warning_html', lock_link: link_to(t('followers.lock_link'), settings_profile_url))
|
||||
|
||||
%p= t('followers.explanation_html')
|
||||
%p= t('followers.true_privacy_html')
|
||||
|
||||
%table.table
|
||||
%thead
|
||||
%tr
|
||||
%th
|
||||
%th= t('followers.domain')
|
||||
%th= t('followers.followers_count')
|
||||
%tbody
|
||||
- @domains.each do |domain|
|
||||
%tr
|
||||
%td
|
||||
= check_box_tag 'select[]', domain.domain, false, disabled: !@account.locked? unless domain.domain.nil?
|
||||
%td
|
||||
%samp= domain.domain.presence || Rails.configuration.x.local_domain
|
||||
%td= number_with_delimiter domain.accounts_from_domain
|
||||
|
||||
.action-pagination
|
||||
.actions
|
||||
= button_tag t('followers.purge'), type: :submit, class: 'button', disabled: !@account.locked?
|
||||
= paginate @domains
|
@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SoftBlockDomainFollowersWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: 'pull'
|
||||
|
||||
def perform(account_id, domain)
|
||||
Account.find(account_id).followers.where(domain: domain).pluck(:id).each do |follower_id|
|
||||
SoftBlockWorker.perform_async(account_id, follower_id)
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SoftBlockWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: 'pull'
|
||||
|
||||
def perform(account_id, target_account_id)
|
||||
account = Account.find(account_id)
|
||||
target_account = Account.find(target_account_id)
|
||||
|
||||
BlockService.new.call(account, target_account)
|
||||
UnblockService.new.call(account, target_account)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
@ -0,0 +1,34 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Settings::FollowerDomainsController do
|
||||
let(:user) { Fabricate(:user) }
|
||||
|
||||
before do
|
||||
sign_in user, scope: :user
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
it 'returns http success' do
|
||||
get :show
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PATCH #update' do
|
||||
let(:poopfeast) { Fabricate(:account, username: 'poopfeast', domain: 'example.com', salmon_url: 'http://example.com/salmon') }
|
||||
|
||||
before do
|
||||
stub_request(:post, 'http://example.com/salmon').to_return(status: 200)
|
||||
poopfeast.follow!(user.account)
|
||||
patch :update, params: { select: ['example.com'] }
|
||||
end
|
||||
|
||||
it 'redirects back to followers page' do
|
||||
expect(response).to redirect_to(settings_follower_domains_path)
|
||||
end
|
||||
|
||||
it 'soft-blocks followers from selected domains' do
|
||||
expect(poopfeast.following?(user.account)).to be false
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in new issue