mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-13 07:49:29 +00:00
Merge pull request #3024 from glitch-soc/glitch-soc/merge-4.3
Merge upstream changes up to 6d53e8c6c5 to stable-4.3
This commit is contained in:
@@ -14,7 +14,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
|
|||||||
@account = current_account
|
@account = current_account
|
||||||
UpdateAccountService.new.call(@account, account_params, raise_error: true)
|
UpdateAccountService.new.call(@account, account_params, raise_error: true)
|
||||||
current_user.update(user_params) if user_params
|
current_user.update(user_params) if user_params
|
||||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
|
||||||
render json: @account, serializer: REST::CredentialAccountSerializer
|
render json: @account, serializer: REST::CredentialAccountSerializer
|
||||||
rescue ActiveRecord::RecordInvalid => e
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
render json: ValidationErrorFormatter.new(e).as_json, status: 422
|
render json: ValidationErrorFormatter.new(e).as_json, status: 422
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class Api::V1::Profile::AvatarsController < Api::BaseController
|
|||||||
def destroy
|
def destroy
|
||||||
@account = current_account
|
@account = current_account
|
||||||
UpdateAccountService.new.call(@account, { avatar: nil }, raise_error: true)
|
UpdateAccountService.new.call(@account, { avatar: nil }, raise_error: true)
|
||||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
|
||||||
render json: @account, serializer: REST::CredentialAccountSerializer
|
render json: @account, serializer: REST::CredentialAccountSerializer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class Api::V1::Profile::HeadersController < Api::BaseController
|
|||||||
def destroy
|
def destroy
|
||||||
@account = current_account
|
@account = current_account
|
||||||
UpdateAccountService.new.call(@account, { header: nil }, raise_error: true)
|
UpdateAccountService.new.call(@account, { header: nil }, raise_error: true)
|
||||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
|
||||||
render json: @account, serializer: REST::CredentialAccountSerializer
|
render json: @account, serializer: REST::CredentialAccountSerializer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ class BackupsController < ApplicationController
|
|||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_backup
|
before_action :set_backup
|
||||||
|
|
||||||
|
BACKUP_LINK_TIMEOUT = 1.hour.freeze
|
||||||
|
|
||||||
def download
|
def download
|
||||||
case Paperclip::Attachment.default_options[:storage]
|
case Paperclip::Attachment.default_options[:storage]
|
||||||
when :s3, :azure
|
when :s3, :azure
|
||||||
redirect_to @backup.dump.expiring_url(10), allow_other_host: true
|
redirect_to @backup.dump.expiring_url(BACKUP_LINK_TIMEOUT.to_i), allow_other_host: true
|
||||||
when :fog
|
when :fog
|
||||||
if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present?
|
if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present?
|
||||||
redirect_to @backup.dump.expiring_url(Time.now.utc + 10), allow_other_host: true
|
redirect_to @backup.dump.expiring_url(BACKUP_LINK_TIMEOUT.from_now), allow_other_host: true
|
||||||
else
|
else
|
||||||
redirect_to full_asset_url(@backup.dump.url), allow_other_host: true
|
redirect_to full_asset_url(@backup.dump.url), allow_other_host: true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ module Settings
|
|||||||
def destroy
|
def destroy
|
||||||
if valid_picture?
|
if valid_picture?
|
||||||
if UpdateAccountService.new.call(@account, { @picture => nil, "#{@picture}_remote_url" => '' })
|
if UpdateAccountService.new.call(@account, { @picture => nil, "#{@picture}_remote_url" => '' })
|
||||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
|
||||||
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg'), status: 303
|
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg'), status: 303
|
||||||
else
|
else
|
||||||
redirect_to settings_profile_path
|
redirect_to settings_profile_path
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class Settings::PrivacyController < Settings::BaseController
|
|||||||
def update
|
def update
|
||||||
if UpdateAccountService.new.call(@account, account_params.except(:settings))
|
if UpdateAccountService.new.call(@account, account_params.except(:settings))
|
||||||
current_user.update!(settings_attributes: account_params[:settings])
|
current_user.update!(settings_attributes: account_params[:settings])
|
||||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
|
||||||
redirect_to settings_privacy_path, notice: I18n.t('generic.changes_saved_msg')
|
redirect_to settings_privacy_path, notice: I18n.t('generic.changes_saved_msg')
|
||||||
else
|
else
|
||||||
render :show
|
render :show
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class Settings::ProfilesController < Settings::BaseController
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
if UpdateAccountService.new.call(@account, account_params)
|
if UpdateAccountService.new.call(@account, account_params)
|
||||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
|
||||||
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg')
|
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg')
|
||||||
else
|
else
|
||||||
@account.build_fields
|
@account.build_fields
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class Settings::VerificationsController < Settings::BaseController
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
if UpdateAccountService.new.call(@account, account_params)
|
if UpdateAccountService.new.call(@account, account_params)
|
||||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
|
||||||
redirect_to settings_verification_path, notice: I18n.t('generic.changes_saved_msg')
|
redirect_to settings_verification_path, notice: I18n.t('generic.changes_saved_msg')
|
||||||
else
|
else
|
||||||
render :show
|
render :show
|
||||||
|
|||||||
@@ -2,11 +2,18 @@
|
|||||||
|
|
||||||
module Admin::Trends::StatusesHelper
|
module Admin::Trends::StatusesHelper
|
||||||
def one_line_preview(status)
|
def one_line_preview(status)
|
||||||
text = if status.local?
|
text = begin
|
||||||
status.text.split("\n").first
|
if status.local?
|
||||||
else
|
status.text.split("\n").first
|
||||||
Nokogiri::HTML5(status.text).css('html > body > *').first&.text
|
else
|
||||||
end
|
Nokogiri::HTML5(status.text).css('html > body > *').first&.text
|
||||||
|
end
|
||||||
|
rescue ArgumentError
|
||||||
|
# This can happen if one of the Nokogumbo limits is encountered
|
||||||
|
# Unfortunately, it does not use a more precise error class
|
||||||
|
# nor allows more graceful handling
|
||||||
|
''
|
||||||
|
end
|
||||||
|
|
||||||
return '' if text.blank?
|
return '' if text.blank?
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ class Bookmarks extends ImmutablePureComponent {
|
|||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
|
timelineId='bookmarks'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ class Favourites extends ImmutablePureComponent {
|
|||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
|
timelineId='favourites'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Link } from 'react-router-dom';
|
|||||||
import { AnimatedNumber } from 'flavours/glitch/components/animated_number';
|
import { AnimatedNumber } from 'flavours/glitch/components/animated_number';
|
||||||
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
||||||
import EditedTimestamp from 'flavours/glitch/components/edited_timestamp';
|
import EditedTimestamp from 'flavours/glitch/components/edited_timestamp';
|
||||||
|
import { FilterWarning } from 'flavours/glitch/components/filter_warning';
|
||||||
import type { StatusLike } from 'flavours/glitch/components/hashtag_bar';
|
import type { StatusLike } from 'flavours/glitch/components/hashtag_bar';
|
||||||
import { getHashtagBarForStatus } from 'flavours/glitch/components/hashtag_bar';
|
import { getHashtagBarForStatus } from 'flavours/glitch/components/hashtag_bar';
|
||||||
import { IconLogo } from 'flavours/glitch/components/logo';
|
import { IconLogo } from 'flavours/glitch/components/logo';
|
||||||
@@ -72,6 +73,7 @@ export const DetailedStatus: React.FC<{
|
|||||||
}) => {
|
}) => {
|
||||||
const properStatus = status?.get('reblog') ?? status;
|
const properStatus = status?.get('reblog') ?? status;
|
||||||
const [height, setHeight] = useState(0);
|
const [height, setHeight] = useState(0);
|
||||||
|
const [showDespiteFilter, setShowDespiteFilter] = useState(false);
|
||||||
const nodeRef = useRef<HTMLDivElement>();
|
const nodeRef = useRef<HTMLDivElement>();
|
||||||
const history = useAppHistory();
|
const history = useAppHistory();
|
||||||
|
|
||||||
@@ -108,6 +110,10 @@ export const DetailedStatus: React.FC<{
|
|||||||
[onOpenVideo, status],
|
[onOpenVideo, status],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleFilterToggle = useCallback(() => {
|
||||||
|
setShowDespiteFilter(!showDespiteFilter);
|
||||||
|
}, [showDespiteFilter, setShowDespiteFilter]);
|
||||||
|
|
||||||
const _measureHeight = useCallback(
|
const _measureHeight = useCallback(
|
||||||
(heightJustChanged?: boolean) => {
|
(heightJustChanged?: boolean) => {
|
||||||
if (measureHeight && nodeRef.current) {
|
if (measureHeight && nodeRef.current) {
|
||||||
@@ -358,6 +364,8 @@ export const DetailedStatus: React.FC<{
|
|||||||
);
|
);
|
||||||
contentMedia.push(hashtagBar);
|
contentMedia.push(hashtagBar);
|
||||||
|
|
||||||
|
const matchedFilters = status.get('matched_filters');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={outerStyle}>
|
<div style={outerStyle}>
|
||||||
<div
|
<div
|
||||||
@@ -386,22 +394,32 @@ export const DetailedStatus: React.FC<{
|
|||||||
)}
|
)}
|
||||||
</Permalink>
|
</Permalink>
|
||||||
|
|
||||||
<StatusContent
|
{matchedFilters && (
|
||||||
status={status}
|
<FilterWarning
|
||||||
media={contentMedia}
|
title={matchedFilters.join(', ')}
|
||||||
extraMedia={extraMedia}
|
expanded={showDespiteFilter}
|
||||||
mediaIcons={contentMediaIcons}
|
onClick={handleFilterToggle}
|
||||||
expanded={expanded}
|
/>
|
||||||
collapsed={false}
|
)}
|
||||||
onExpandedToggle={onToggleHidden}
|
|
||||||
onTranslate={handleTranslate}
|
{(!matchedFilters || showDespiteFilter) && (
|
||||||
onUpdate={handleChildUpdate}
|
<StatusContent
|
||||||
tagLinks={tagMisleadingLinks}
|
status={status}
|
||||||
rewriteMentions={rewriteMentions}
|
media={contentMedia}
|
||||||
parseClick={parseClick}
|
extraMedia={extraMedia}
|
||||||
disabled
|
mediaIcons={contentMediaIcons}
|
||||||
{...(statusContentProps as any)}
|
expanded={expanded}
|
||||||
/>
|
collapsed={false}
|
||||||
|
onExpandedToggle={onToggleHidden}
|
||||||
|
onTranslate={handleTranslate}
|
||||||
|
onUpdate={handleChildUpdate}
|
||||||
|
tagLinks={tagMisleadingLinks}
|
||||||
|
rewriteMentions={rewriteMentions}
|
||||||
|
parseClick={parseClick}
|
||||||
|
disabled
|
||||||
|
{...(statusContentProps as any)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className='detailed-status__meta'>
|
<div className='detailed-status__meta'>
|
||||||
<div className='detailed-status__meta__line'>
|
<div className='detailed-status__meta__line'>
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ const makeMapStateToProps = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => {
|
const mapStateToProps = (state, props) => {
|
||||||
const status = getStatus(state, { id: props.params.statusId });
|
const status = getStatus(state, { id: props.params.statusId, contextType: 'detailed' });
|
||||||
|
|
||||||
let ancestorsIds = Immutable.List();
|
let ancestorsIds = Immutable.List();
|
||||||
let descendantsIds = Immutable.List();
|
let descendantsIds = Immutable.List();
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ export const makeGetStatus = () => {
|
|||||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
|
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
|
||||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
|
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
|
||||||
getFilters,
|
getFilters,
|
||||||
|
(_, { contextType }) => ['detailed', 'bookmarks', 'favourites'].includes(contextType),
|
||||||
],
|
],
|
||||||
|
|
||||||
(statusBase, statusReblog, accountBase, accountReblog, filters) => {
|
(statusBase, statusReblog, accountBase, accountReblog, filters, warnInsteadOfHide) => {
|
||||||
if (!statusBase || statusBase.get('isLoading')) {
|
if (!statusBase || statusBase.get('isLoading')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -25,7 +26,7 @@ export const makeGetStatus = () => {
|
|||||||
let filtered = false;
|
let filtered = false;
|
||||||
if ((accountReblog || accountBase).get('id') !== me && filters) {
|
if ((accountReblog || accountBase).get('id') !== me && filters) {
|
||||||
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
|
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
|
||||||
if (filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
|
if (!warnInsteadOfHide && filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
filterResults = filterResults.filter(result => filters.has(result.get('filter')));
|
filterResults = filterResults.filter(result => filters.has(result.get('filter')));
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ export const toServerSideType = (columnType: string) => {
|
|||||||
case 'thread':
|
case 'thread':
|
||||||
case 'account':
|
case 'account':
|
||||||
return columnType;
|
return columnType;
|
||||||
|
case 'detailed':
|
||||||
|
return 'thread';
|
||||||
|
case 'bookmarks':
|
||||||
|
case 'favourites':
|
||||||
|
return 'home';
|
||||||
default:
|
default:
|
||||||
if (columnType.includes('list:')) {
|
if (columnType.includes('list:')) {
|
||||||
return 'home';
|
return 'home';
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
const { onToggleHidden } = this.props;
|
const { onToggleHidden } = this.props;
|
||||||
const status = this._properStatus();
|
const status = this._properStatus();
|
||||||
|
|
||||||
if (status.get('matched_filters')) {
|
if (this.props.status.get('matched_filters')) {
|
||||||
const expandedBecauseOfCW = !status.get('hidden') || status.get('spoiler_text').length === 0;
|
const expandedBecauseOfCW = !status.get('hidden') || status.get('spoiler_text').length === 0;
|
||||||
const expandedBecauseOfFilter = this.state.showDespiteFilter;
|
const expandedBecauseOfFilter = this.state.showDespiteFilter;
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ class Bookmarks extends ImmutablePureComponent {
|
|||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
|
timelineId='bookmarks'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ class Favourites extends ImmutablePureComponent {
|
|||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
|
timelineId='favourites'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?re
|
|||||||
import { AnimatedNumber } from 'mastodon/components/animated_number';
|
import { AnimatedNumber } from 'mastodon/components/animated_number';
|
||||||
import { ContentWarning } from 'mastodon/components/content_warning';
|
import { ContentWarning } from 'mastodon/components/content_warning';
|
||||||
import EditedTimestamp from 'mastodon/components/edited_timestamp';
|
import EditedTimestamp from 'mastodon/components/edited_timestamp';
|
||||||
|
import { FilterWarning } from 'mastodon/components/filter_warning';
|
||||||
import type { StatusLike } from 'mastodon/components/hashtag_bar';
|
import type { StatusLike } from 'mastodon/components/hashtag_bar';
|
||||||
import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar';
|
import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
@@ -68,6 +69,7 @@ export const DetailedStatus: React.FC<{
|
|||||||
}) => {
|
}) => {
|
||||||
const properStatus = status?.get('reblog') ?? status;
|
const properStatus = status?.get('reblog') ?? status;
|
||||||
const [height, setHeight] = useState(0);
|
const [height, setHeight] = useState(0);
|
||||||
|
const [showDespiteFilter, setShowDespiteFilter] = useState(false);
|
||||||
const nodeRef = useRef<HTMLDivElement>();
|
const nodeRef = useRef<HTMLDivElement>();
|
||||||
|
|
||||||
const handleOpenVideo = useCallback(
|
const handleOpenVideo = useCallback(
|
||||||
@@ -80,6 +82,10 @@ export const DetailedStatus: React.FC<{
|
|||||||
[onOpenVideo, status],
|
[onOpenVideo, status],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleFilterToggle = useCallback(() => {
|
||||||
|
setShowDespiteFilter(!showDespiteFilter);
|
||||||
|
}, [showDespiteFilter, setShowDespiteFilter]);
|
||||||
|
|
||||||
const handleExpandedToggle = useCallback(() => {
|
const handleExpandedToggle = useCallback(() => {
|
||||||
if (onToggleHidden) onToggleHidden(status);
|
if (onToggleHidden) onToggleHidden(status);
|
||||||
}, [onToggleHidden, status]);
|
}, [onToggleHidden, status]);
|
||||||
@@ -290,8 +296,12 @@ export const DetailedStatus: React.FC<{
|
|||||||
const { statusContentProps, hashtagBar } = getHashtagBarForStatus(
|
const { statusContentProps, hashtagBar } = getHashtagBarForStatus(
|
||||||
status as StatusLike,
|
status as StatusLike,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const matchedFilters = status.get('matched_filters');
|
||||||
|
|
||||||
const expanded =
|
const expanded =
|
||||||
!status.get('hidden') || status.get('spoiler_text').length === 0;
|
(!matchedFilters || showDespiteFilter) &&
|
||||||
|
(!status.get('hidden') || status.get('spoiler_text').length === 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={outerStyle}>
|
<div style={outerStyle}>
|
||||||
@@ -328,17 +338,26 @@ export const DetailedStatus: React.FC<{
|
|||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{status.get('spoiler_text').length > 0 && (
|
{matchedFilters && (
|
||||||
<ContentWarning
|
<FilterWarning
|
||||||
text={
|
title={matchedFilters.join(', ')}
|
||||||
status.getIn(['translation', 'spoilerHtml']) ||
|
expanded={showDespiteFilter}
|
||||||
status.get('spoilerHtml')
|
onClick={handleFilterToggle}
|
||||||
}
|
|
||||||
expanded={expanded}
|
|
||||||
onClick={handleExpandedToggle}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{status.get('spoiler_text').length > 0 &&
|
||||||
|
(!matchedFilters || showDespiteFilter) && (
|
||||||
|
<ContentWarning
|
||||||
|
text={
|
||||||
|
status.getIn(['translation', 'spoilerHtml']) ||
|
||||||
|
status.get('spoilerHtml')
|
||||||
|
}
|
||||||
|
expanded={expanded}
|
||||||
|
onClick={handleExpandedToggle}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<>
|
<>
|
||||||
<StatusContent
|
<StatusContent
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ const makeMapStateToProps = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => {
|
const mapStateToProps = (state, props) => {
|
||||||
const status = getStatus(state, { id: props.params.statusId });
|
const status = getStatus(state, { id: props.params.statusId, contextType: 'detailed' });
|
||||||
|
|
||||||
let ancestorsIds = Immutable.List();
|
let ancestorsIds = Immutable.List();
|
||||||
let descendantsIds = Immutable.List();
|
let descendantsIds = Immutable.List();
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ export const makeGetStatus = () => {
|
|||||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
|
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
|
||||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
|
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
|
||||||
getFilters,
|
getFilters,
|
||||||
|
(_, { contextType }) => ['detailed', 'bookmarks', 'favourites'].includes(contextType),
|
||||||
],
|
],
|
||||||
|
|
||||||
(statusBase, statusReblog, accountBase, accountReblog, filters) => {
|
(statusBase, statusReblog, accountBase, accountReblog, filters, warnInsteadOfHide) => {
|
||||||
if (!statusBase || statusBase.get('isLoading')) {
|
if (!statusBase || statusBase.get('isLoading')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -31,7 +32,7 @@ export const makeGetStatus = () => {
|
|||||||
let filtered = false;
|
let filtered = false;
|
||||||
if ((accountReblog || accountBase).get('id') !== me && filters) {
|
if ((accountReblog || accountBase).get('id') !== me && filters) {
|
||||||
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
|
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
|
||||||
if (filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
|
if (!warnInsteadOfHide && filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
filterResults = filterResults.filter(result => filters.has(result.get('filter')));
|
filterResults = filterResults.filter(result => filters.has(result.get('filter')));
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ export const toServerSideType = (columnType: string) => {
|
|||||||
case 'thread':
|
case 'thread':
|
||||||
case 'account':
|
case 'account':
|
||||||
return columnType;
|
return columnType;
|
||||||
|
case 'detailed':
|
||||||
|
return 'thread';
|
||||||
|
case 'bookmarks':
|
||||||
|
case 'favourites':
|
||||||
|
return 'home';
|
||||||
default:
|
default:
|
||||||
if (columnType.includes('list:')) {
|
if (columnType.includes('list:')) {
|
||||||
return 'home';
|
return 'home';
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AccountReachFinder
|
class AccountReachFinder
|
||||||
|
RECENT_LIMIT = 2_000
|
||||||
|
STATUS_LIMIT = 200
|
||||||
|
STATUS_SINCE = 2.days
|
||||||
|
|
||||||
def initialize(account)
|
def initialize(account)
|
||||||
@account = account
|
@account = account
|
||||||
end
|
end
|
||||||
|
|
||||||
def inboxes
|
def inboxes
|
||||||
(followers_inboxes + reporters_inboxes + recently_mentioned_inboxes + relay_inboxes).uniq
|
(followers_inboxes + reporters_inboxes + recently_mentioned_inboxes + recently_followed_inboxes + recently_requested_inboxes + relay_inboxes).uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -20,13 +24,46 @@ class AccountReachFinder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def recently_mentioned_inboxes
|
def recently_mentioned_inboxes
|
||||||
cutoff_id = Mastodon::Snowflake.id_at(2.days.ago, with_random: false)
|
Account
|
||||||
recent_statuses = @account.statuses.recent.where(id: cutoff_id...).limit(200)
|
.joins(:mentions)
|
||||||
|
.where(mentions: { status: recent_statuses })
|
||||||
|
.inboxes
|
||||||
|
.take(RECENT_LIMIT)
|
||||||
|
end
|
||||||
|
|
||||||
Account.joins(:mentions).where(mentions: { status: recent_statuses }).inboxes.take(2000)
|
def recently_followed_inboxes
|
||||||
|
@account
|
||||||
|
.following
|
||||||
|
.where(follows: { created_at: recent_date_cutoff... })
|
||||||
|
.inboxes
|
||||||
|
.take(RECENT_LIMIT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def recently_requested_inboxes
|
||||||
|
Account
|
||||||
|
.where(id: @account.follow_requests.where({ created_at: recent_date_cutoff... }).select(:target_account_id))
|
||||||
|
.inboxes
|
||||||
|
.take(RECENT_LIMIT)
|
||||||
end
|
end
|
||||||
|
|
||||||
def relay_inboxes
|
def relay_inboxes
|
||||||
Relay.enabled.pluck(:inbox_url)
|
Relay.enabled.pluck(:inbox_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def oldest_status_id
|
||||||
|
Mastodon::Snowflake
|
||||||
|
.id_at(recent_date_cutoff, with_random: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def recent_date_cutoff
|
||||||
|
@account.suspended? && @account.suspension_origin_local? ? @account.suspended_at - STATUS_SINCE : STATUS_SINCE.ago
|
||||||
|
end
|
||||||
|
|
||||||
|
def recent_statuses
|
||||||
|
@account
|
||||||
|
.statuses
|
||||||
|
.recent
|
||||||
|
.where(id: oldest_status_id...)
|
||||||
|
.limit(STATUS_LIMIT)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -24,7 +24,15 @@ class EmojiFormatter
|
|||||||
def to_s
|
def to_s
|
||||||
return html if custom_emojis.empty? || html.blank?
|
return html if custom_emojis.empty? || html.blank?
|
||||||
|
|
||||||
tree = Nokogiri::HTML5.fragment(html)
|
begin
|
||||||
|
tree = Nokogiri::HTML5.fragment(html)
|
||||||
|
rescue ArgumentError
|
||||||
|
# This can happen if one of the Nokogumbo limits is encountered
|
||||||
|
# Unfortunately, it does not use a more precise error class
|
||||||
|
# nor allows more graceful handling
|
||||||
|
return ''
|
||||||
|
end
|
||||||
|
|
||||||
tree.xpath('./text()|.//text()[not(ancestor[@class="invisible"])]').to_a.each do |node|
|
tree.xpath('./text()|.//text()[not(ancestor[@class="invisible"])]').to_a.each do |node|
|
||||||
i = -1
|
i = -1
|
||||||
inside_shortname = false
|
inside_shortname = false
|
||||||
|
|||||||
@@ -16,7 +16,15 @@ class PlainTextFormatter
|
|||||||
if local?
|
if local?
|
||||||
text
|
text
|
||||||
else
|
else
|
||||||
node = Nokogiri::HTML5.fragment(insert_newlines)
|
begin
|
||||||
|
node = Nokogiri::HTML5.fragment(insert_newlines)
|
||||||
|
rescue ArgumentError
|
||||||
|
# This can happen if one of the Nokogumbo limits is encountered
|
||||||
|
# Unfortunately, it does not use a more precise error class
|
||||||
|
# nor allows more graceful handling
|
||||||
|
return ''
|
||||||
|
end
|
||||||
|
|
||||||
# Elements that are entirely removed with our Sanitize config
|
# Elements that are entirely removed with our Sanitize config
|
||||||
node.xpath('.//iframe|.//math|.//noembed|.//noframes|.//noscript|.//plaintext|.//script|.//style|.//svg|.//xmp').remove
|
node.xpath('.//iframe|.//math|.//noembed|.//noframes|.//noscript|.//plaintext|.//script|.//style|.//svg|.//xmp').remove
|
||||||
node.text.chomp
|
node.text.chomp
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ class Account < ApplicationRecord
|
|||||||
scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) }
|
scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) }
|
||||||
scope :not_domain_blocked_by_account, ->(account) { where(arel_table[:domain].eq(nil).or(arel_table[:domain].not_in(account.excluded_from_timeline_domains))) }
|
scope :not_domain_blocked_by_account, ->(account) { where(arel_table[:domain].eq(nil).or(arel_table[:domain].not_in(account.excluded_from_timeline_domains))) }
|
||||||
scope :dormant, -> { joins(:account_stat).merge(AccountStat.without_recent_activity) }
|
scope :dormant, -> { joins(:account_stat).merge(AccountStat.without_recent_activity) }
|
||||||
scope :with_username, ->(value) { where arel_table[:username].lower.eq(value.to_s.downcase) }
|
scope :with_username, ->(value) { value.is_a?(Array) ? where(arel_table[:username].lower.in(value.map { |x| x.to_s.downcase })) : where(arel_table[:username].lower.eq(value.to_s.downcase)) }
|
||||||
scope :with_domain, ->(value) { where arel_table[:domain].lower.eq(value&.to_s&.downcase) }
|
scope :with_domain, ->(value) { where arel_table[:domain].lower.eq(value&.to_s&.downcase) }
|
||||||
scope :without_memorial, -> { where(memorial: false) }
|
scope :without_memorial, -> { where(memorial: false) }
|
||||||
scope :duplicate_uris, -> { select(:uri, Arel.star.count).group(:uri).having(Arel.star.count.gt(1)) }
|
scope :duplicate_uris, -> { select(:uri, Arel.star.count).group(:uri).having(Arel.star.count.gt(1)) }
|
||||||
|
|||||||
@@ -73,7 +73,14 @@ class Account::Field < ActiveModelSerializers::Model
|
|||||||
end
|
end
|
||||||
|
|
||||||
def extract_url_from_html
|
def extract_url_from_html
|
||||||
doc = Nokogiri::HTML5.fragment(value)
|
begin
|
||||||
|
doc = Nokogiri::HTML5.fragment(value)
|
||||||
|
rescue ArgumentError
|
||||||
|
# This can happen if one of the Nokogumbo limits is encountered
|
||||||
|
# Unfortunately, it does not use a more precise error class
|
||||||
|
# nor allows more graceful handling
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
return if doc.nil?
|
return if doc.nil?
|
||||||
return if doc.children.size != 1
|
return if doc.children.size != 1
|
||||||
|
|||||||
@@ -420,8 +420,10 @@ class MediaAttachment < ApplicationRecord
|
|||||||
|
|
||||||
@paths_to_cache_bust = MediaAttachment.attachment_definitions.keys.flat_map do |attachment_name|
|
@paths_to_cache_bust = MediaAttachment.attachment_definitions.keys.flat_map do |attachment_name|
|
||||||
attachment = public_send(attachment_name)
|
attachment = public_send(attachment_name)
|
||||||
|
next if attachment.blank?
|
||||||
|
|
||||||
styles = DEFAULT_STYLES | attachment.styles.keys
|
styles = DEFAULT_STYLES | attachment.styles.keys
|
||||||
styles.map { |style| attachment.path(style) }
|
styles.map { |style| attachment.url(style) }
|
||||||
end.compact
|
end.compact
|
||||||
rescue => e
|
rescue => e
|
||||||
# We really don't want any error here preventing media deletion
|
# We really don't want any error here preventing media deletion
|
||||||
|
|||||||
@@ -4,32 +4,46 @@ class ActivityPub::SynchronizeFollowersService < BaseService
|
|||||||
include JsonLdHelper
|
include JsonLdHelper
|
||||||
include Payloadable
|
include Payloadable
|
||||||
|
|
||||||
|
MAX_COLLECTION_PAGES = 10
|
||||||
|
|
||||||
def call(account, partial_collection_url)
|
def call(account, partial_collection_url)
|
||||||
@account = account
|
@account = account
|
||||||
|
@expected_followers_ids = []
|
||||||
|
|
||||||
items = collection_items(partial_collection_url)
|
return unless process_collection!(partial_collection_url)
|
||||||
return if items.nil?
|
|
||||||
|
|
||||||
# There could be unresolved accounts (hence the call to .compact) but this
|
|
||||||
# should never happen in practice, since in almost all cases we keep an
|
|
||||||
# Account record, and should we not do that, we should have sent a Delete.
|
|
||||||
# In any case there is not much we can do if that occurs.
|
|
||||||
@expected_followers = items.filter_map { |uri| ActivityPub::TagManager.instance.uri_to_resource(uri, Account) }
|
|
||||||
|
|
||||||
remove_unexpected_local_followers!
|
remove_unexpected_local_followers!
|
||||||
handle_unexpected_outgoing_follows!
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def process_page!(items)
|
||||||
|
page_expected_followers = extract_local_followers(items)
|
||||||
|
@expected_followers_ids.concat(page_expected_followers.pluck(:id))
|
||||||
|
|
||||||
|
handle_unexpected_outgoing_follows!(page_expected_followers)
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_local_followers(items)
|
||||||
|
# There could be unresolved accounts (hence the call to .filter_map) but this
|
||||||
|
# should never happen in practice, since in almost all cases we keep an
|
||||||
|
# Account record, and should we not do that, we should have sent a Delete.
|
||||||
|
# In any case there is not much we can do if that occurs.
|
||||||
|
|
||||||
|
# TODO: this will need changes when switching to numeric IDs
|
||||||
|
|
||||||
|
usernames = items.filter_map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username)&.downcase }
|
||||||
|
Account.local.with_username(usernames)
|
||||||
|
end
|
||||||
|
|
||||||
def remove_unexpected_local_followers!
|
def remove_unexpected_local_followers!
|
||||||
@account.followers.local.where.not(id: @expected_followers.map(&:id)).reorder(nil).find_each do |unexpected_follower|
|
@account.followers.local.where.not(id: @expected_followers_ids).reorder(nil).find_each do |unexpected_follower|
|
||||||
UnfollowService.new.call(unexpected_follower, @account)
|
UnfollowService.new.call(unexpected_follower, @account)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_unexpected_outgoing_follows!
|
def handle_unexpected_outgoing_follows!(expected_followers)
|
||||||
@expected_followers.each do |expected_follower|
|
expected_followers.each do |expected_follower|
|
||||||
next if expected_follower.following?(@account)
|
next if expected_follower.following?(@account)
|
||||||
|
|
||||||
if expected_follower.requested?(@account)
|
if expected_follower.requested?(@account)
|
||||||
@@ -50,18 +64,33 @@ class ActivityPub::SynchronizeFollowersService < BaseService
|
|||||||
Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer))
|
Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer))
|
||||||
end
|
end
|
||||||
|
|
||||||
def collection_items(collection_or_uri)
|
# Only returns true if the whole collection has been processed
|
||||||
collection = fetch_collection(collection_or_uri)
|
def process_collection!(collection_uri, max_pages: MAX_COLLECTION_PAGES)
|
||||||
return unless collection.is_a?(Hash)
|
collection = fetch_collection(collection_uri)
|
||||||
|
return false unless collection.is_a?(Hash)
|
||||||
|
|
||||||
collection = fetch_collection(collection['first']) if collection['first'].present?
|
collection = fetch_collection(collection['first']) if collection['first'].present?
|
||||||
return unless collection.is_a?(Hash)
|
|
||||||
|
|
||||||
|
while collection.is_a?(Hash)
|
||||||
|
process_page!(as_array(collection_page_items(collection)))
|
||||||
|
|
||||||
|
max_pages -= 1
|
||||||
|
|
||||||
|
return true if collection['next'].blank? # We reached the end of the collection
|
||||||
|
return false if max_pages <= 0 # We reached our pages limit
|
||||||
|
|
||||||
|
collection = fetch_collection(collection['next'])
|
||||||
|
end
|
||||||
|
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def collection_page_items(collection)
|
||||||
case collection['type']
|
case collection['type']
|
||||||
when 'Collection', 'CollectionPage'
|
when 'Collection', 'CollectionPage'
|
||||||
as_array(collection['items'])
|
collection['items']
|
||||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
when 'OrderedCollection', 'OrderedCollectionPage'
|
||||||
as_array(collection['orderedItems'])
|
collection['orderedItems']
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class SuspendAccountService < BaseService
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
CacheBusterWorker.perform_async(attachment.path(style)) if Rails.configuration.x.cache_buster_enabled
|
CacheBusterWorker.perform_async(attachment.url(style)) if Rails.configuration.x.cache_buster_enabled
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class UnsuspendAccountService < BaseService
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
CacheBusterWorker.perform_async(attachment.path(style)) if Rails.configuration.x.cache_buster_enabled
|
CacheBusterWorker.perform_async(attachment.url(style)) if Rails.configuration.x.cache_buster_enabled
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ActivityPub::UpdateDistributionWorker < ActivityPub::RawDistributionWorker
|
class ActivityPub::UpdateDistributionWorker < ActivityPub::RawDistributionWorker
|
||||||
|
DEBOUNCE_DELAY = 5.seconds
|
||||||
|
|
||||||
sidekiq_options queue: 'push', lock: :until_executed, lock_ttl: 1.day.to_i
|
sidekiq_options queue: 'push', lock: :until_executed, lock_ttl: 1.day.to_i
|
||||||
|
|
||||||
# Distribute an profile update to servers that might have a copy
|
# Distribute an profile update to servers that might have a copy
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ RSpec.describe Settings::PrivacyController do
|
|||||||
describe 'PUT #update' do
|
describe 'PUT #update' do
|
||||||
context 'when update succeeds' do
|
context 'when update succeeds' do
|
||||||
before do
|
before do
|
||||||
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async)
|
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_in)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'updates the user profile' do
|
it 'updates the user profile' do
|
||||||
@@ -44,14 +44,14 @@ RSpec.describe Settings::PrivacyController do
|
|||||||
.to redirect_to(settings_privacy_path)
|
.to redirect_to(settings_privacy_path)
|
||||||
|
|
||||||
expect(ActivityPub::UpdateDistributionWorker)
|
expect(ActivityPub::UpdateDistributionWorker)
|
||||||
.to have_received(:perform_async).with(account.id)
|
.to have_received(:perform_in).with(anything, account.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when update fails' do
|
context 'when update fails' do
|
||||||
before do
|
before do
|
||||||
allow(UpdateAccountService).to receive(:new).and_return(failing_update_service)
|
allow(UpdateAccountService).to receive(:new).and_return(failing_update_service)
|
||||||
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async)
|
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_in)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'updates the user profile' do
|
it 'updates the user profile' do
|
||||||
@@ -61,7 +61,7 @@ RSpec.describe Settings::PrivacyController do
|
|||||||
.to render_template(:show)
|
.to render_template(:show)
|
||||||
|
|
||||||
expect(ActivityPub::UpdateDistributionWorker)
|
expect(ActivityPub::UpdateDistributionWorker)
|
||||||
.to_not have_received(:perform_async)
|
.to_not have_received(:perform_in)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -29,23 +29,23 @@ RSpec.describe Settings::ProfilesController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'updates the user profile' do
|
it 'updates the user profile' do
|
||||||
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async)
|
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_in)
|
||||||
put :update, params: { account: { display_name: 'New name' } }
|
put :update, params: { account: { display_name: 'New name' } }
|
||||||
expect(account.reload.display_name).to eq 'New name'
|
expect(account.reload.display_name).to eq 'New name'
|
||||||
expect(response).to redirect_to(settings_profile_path)
|
expect(response).to redirect_to(settings_profile_path)
|
||||||
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(account.id)
|
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_in).with(anything, account.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'PUT #update with new profile image' do
|
describe 'PUT #update with new profile image' do
|
||||||
it 'updates profile image' do
|
it 'updates profile image' do
|
||||||
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async)
|
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_in)
|
||||||
expect(account.avatar.instance.avatar_file_name).to be_nil
|
expect(account.avatar.instance.avatar_file_name).to be_nil
|
||||||
|
|
||||||
put :update, params: { account: { avatar: fixture_file_upload('avatar.gif', 'image/gif') } }
|
put :update, params: { account: { avatar: fixture_file_upload('avatar.gif', 'image/gif') } }
|
||||||
expect(response).to redirect_to(settings_profile_path)
|
expect(response).to redirect_to(settings_profile_path)
|
||||||
expect(account.reload.avatar.instance.avatar_file_name).to_not be_nil
|
expect(account.reload.avatar.instance.avatar_file_name).to_not be_nil
|
||||||
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(account.id)
|
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_in).with(anything, account.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -13,13 +13,28 @@ RSpec.describe AccountReachFinder do
|
|||||||
let(:ap_mentioned_example_com) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-3', domain: 'example.com') }
|
let(:ap_mentioned_example_com) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-3', domain: 'example.com') }
|
||||||
let(:ap_mentioned_example_org) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.org/inbox-4', domain: 'example.org') }
|
let(:ap_mentioned_example_org) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.org/inbox-4', domain: 'example.org') }
|
||||||
|
|
||||||
|
let(:ap_followed_example_com) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-5', domain: 'example.com') }
|
||||||
|
let(:ap_followed_example_org) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-6', domain: 'example.org') }
|
||||||
|
|
||||||
|
let(:ap_requested_example_com) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-7', domain: 'example.com') }
|
||||||
|
let(:ap_requested_example_org) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-8', domain: 'example.org') }
|
||||||
|
|
||||||
let(:unrelated_account) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/unrelated-inbox', domain: 'example.com') }
|
let(:unrelated_account) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/unrelated-inbox', domain: 'example.com') }
|
||||||
|
let(:old_followed_account) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/old-followed-inbox', domain: 'example.com') }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
travel_to(2.months.ago) { account.follow!(old_followed_account) }
|
||||||
|
|
||||||
ap_follower_example_com.follow!(account)
|
ap_follower_example_com.follow!(account)
|
||||||
ap_follower_example_org.follow!(account)
|
ap_follower_example_org.follow!(account)
|
||||||
ap_follower_with_shared.follow!(account)
|
ap_follower_with_shared.follow!(account)
|
||||||
|
|
||||||
|
account.follow!(ap_followed_example_com)
|
||||||
|
account.follow!(ap_followed_example_org)
|
||||||
|
|
||||||
|
account.request_follow!(ap_requested_example_com)
|
||||||
|
account.request_follow!(ap_requested_example_org)
|
||||||
|
|
||||||
Fabricate(:status, account: account).tap do |status|
|
Fabricate(:status, account: account).tap do |status|
|
||||||
status.mentions << Mention.new(account: ap_follower_example_com)
|
status.mentions << Mention.new(account: ap_follower_example_com)
|
||||||
status.mentions << Mention.new(account: ap_mentioned_with_shared)
|
status.mentions << Mention.new(account: ap_mentioned_with_shared)
|
||||||
@@ -44,7 +59,10 @@ RSpec.describe AccountReachFinder do
|
|||||||
expect(subject)
|
expect(subject)
|
||||||
.to include(*follower_inbox_urls)
|
.to include(*follower_inbox_urls)
|
||||||
.and include(*mentioned_account_inbox_urls)
|
.and include(*mentioned_account_inbox_urls)
|
||||||
|
.and include(*recently_followed_inbox_urls)
|
||||||
|
.and include(*recently_requested_inbox_urls)
|
||||||
.and not_include(unrelated_account.preferred_inbox_url)
|
.and not_include(unrelated_account.preferred_inbox_url)
|
||||||
|
.and not_include(old_followed_account.preferred_inbox_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
def follower_inbox_urls
|
def follower_inbox_urls
|
||||||
@@ -56,5 +74,15 @@ RSpec.describe AccountReachFinder do
|
|||||||
[ap_mentioned_with_shared, ap_mentioned_example_com, ap_mentioned_example_org]
|
[ap_mentioned_with_shared, ap_mentioned_example_com, ap_mentioned_example_org]
|
||||||
.map(&:preferred_inbox_url)
|
.map(&:preferred_inbox_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def recently_followed_inbox_urls
|
||||||
|
[ap_followed_example_com, ap_followed_example_org]
|
||||||
|
.map(&:preferred_inbox_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def recently_requested_inbox_urls
|
||||||
|
[ap_requested_example_com, ap_requested_example_org]
|
||||||
|
.map(&:preferred_inbox_url)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -295,12 +295,21 @@ RSpec.describe MediaAttachment, :attachment_processing do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'queues CacheBusterWorker jobs' do
|
it 'queues CacheBusterWorker jobs' do
|
||||||
original_path = media.file.path(:original)
|
original_url = media.file.url(:original)
|
||||||
small_path = media.file.path(:small)
|
small_url = media.file.url(:small)
|
||||||
|
|
||||||
expect { media.destroy }
|
expect { media.destroy }
|
||||||
.to enqueue_sidekiq_job(CacheBusterWorker).with(original_path)
|
.to enqueue_sidekiq_job(CacheBusterWorker).with(original_url)
|
||||||
.and enqueue_sidekiq_job(CacheBusterWorker).with(small_path)
|
.and enqueue_sidekiq_job(CacheBusterWorker).with(small_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a missing remote attachment' do
|
||||||
|
let(:media) { Fabricate(:media_attachment, remote_url: 'https://example.com/foo.png', file: nil) }
|
||||||
|
|
||||||
|
it 'does not queue CacheBusterWorker jobs' do
|
||||||
|
expect { media.destroy }
|
||||||
|
.to_not enqueue_sidekiq_job(CacheBusterWorker)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -53,8 +53,6 @@ RSpec.describe 'credentials API' do
|
|||||||
patch '/api/v1/accounts/update_credentials', headers: headers, params: params
|
patch '/api/v1/accounts/update_credentials', headers: headers, params: params
|
||||||
end
|
end
|
||||||
|
|
||||||
before { allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async) }
|
|
||||||
|
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{
|
{
|
||||||
avatar: fixture_file_upload('avatar.gif', 'image/gif'),
|
avatar: fixture_file_upload('avatar.gif', 'image/gif'),
|
||||||
@@ -112,7 +110,7 @@ RSpec.describe 'credentials API' do
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(ActivityPub::UpdateDistributionWorker)
|
expect(ActivityPub::UpdateDistributionWorker)
|
||||||
.to have_received(:perform_async).with(user.account_id)
|
.to have_enqueued_sidekiq_job(user.account_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def expect_account_updates
|
def expect_account_updates
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ RSpec.describe 'Deleting profile images' do
|
|||||||
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
|
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
|
||||||
|
|
||||||
describe 'DELETE /api/v1/profile' do
|
describe 'DELETE /api/v1/profile' do
|
||||||
before do
|
|
||||||
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when deleting an avatar' do
|
context 'when deleting an avatar' do
|
||||||
context 'with wrong scope' do
|
context 'with wrong scope' do
|
||||||
before do
|
before do
|
||||||
@@ -38,7 +34,8 @@ RSpec.describe 'Deleting profile images' do
|
|||||||
account.reload
|
account.reload
|
||||||
expect(account.avatar).to_not exist
|
expect(account.avatar).to_not exist
|
||||||
expect(account.header).to exist
|
expect(account.header).to exist
|
||||||
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(account.id)
|
expect(ActivityPub::UpdateDistributionWorker)
|
||||||
|
.to have_enqueued_sidekiq_job(account.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -61,7 +58,8 @@ RSpec.describe 'Deleting profile images' do
|
|||||||
account.reload
|
account.reload
|
||||||
expect(account.avatar).to exist
|
expect(account.avatar).to exist
|
||||||
expect(account.header).to_not exist
|
expect(account.header).to_not exist
|
||||||
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(account.id)
|
expect(ActivityPub::UpdateDistributionWorker)
|
||||||
|
.to have_enqueued_sidekiq_job(account.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ RSpec.describe 'Notifications' do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'with min_id param' do
|
context 'with min_id param' do
|
||||||
let(:params) { { min_id: user.account.notifications.reload.first.id - 1 } }
|
let(:params) { { min_id: user.account.notifications.order(id: :asc).first.id - 1 } }
|
||||||
|
|
||||||
it 'returns a notification group covering all notifications' do
|
it 'returns a notification group covering all notifications' do
|
||||||
subject
|
subject
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do
|
|||||||
let(:bob) { Fabricate(:account, username: 'bob') }
|
let(:bob) { Fabricate(:account, username: 'bob') }
|
||||||
let(:eve) { Fabricate(:account, username: 'eve') }
|
let(:eve) { Fabricate(:account, username: 'eve') }
|
||||||
let(:mallory) { Fabricate(:account, username: 'mallory') }
|
let(:mallory) { Fabricate(:account, username: 'mallory') }
|
||||||
let(:collection_uri) { 'http://example.com/partial-followers' }
|
let(:collection_uri) { 'https://example.com/partial-followers' }
|
||||||
|
|
||||||
let(:items) do
|
let(:items) do
|
||||||
[alice, eve, mallory].map do |account|
|
[alice, eve, mallory].map do |account|
|
||||||
@@ -27,14 +27,14 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do
|
|||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
end
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
alice.follow!(actor)
|
||||||
|
bob.follow!(actor)
|
||||||
|
mallory.request_follow!(actor)
|
||||||
|
end
|
||||||
|
|
||||||
shared_examples 'synchronizes followers' do
|
shared_examples 'synchronizes followers' do
|
||||||
before do
|
before do
|
||||||
alice.follow!(actor)
|
|
||||||
bob.follow!(actor)
|
|
||||||
mallory.request_follow!(actor)
|
|
||||||
|
|
||||||
allow(ActivityPub::DeliveryWorker).to receive(:perform_async)
|
|
||||||
|
|
||||||
subject.call(actor, collection_uri)
|
subject.call(actor, collection_uri)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do
|
|||||||
expect(mallory)
|
expect(mallory)
|
||||||
.to be_following(actor) # Convert follow request to follow when accepted
|
.to be_following(actor) # Convert follow request to follow when accepted
|
||||||
expect(ActivityPub::DeliveryWorker)
|
expect(ActivityPub::DeliveryWorker)
|
||||||
.to have_received(:perform_async).with(anything, eve.id, actor.inbox_url) # Send Undo Follow to actor
|
.to have_enqueued_sidekiq_job(anything, eve.id, actor.inbox_url) # Send Undo Follow to actor
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do
|
|||||||
it_behaves_like 'synchronizes followers'
|
it_behaves_like 'synchronizes followers'
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the endpoint is a paginated Collection of actor URIs' do
|
context 'when the endpoint is a single-page paginated Collection of actor URIs' do
|
||||||
let(:payload) do
|
let(:payload) do
|
||||||
{
|
{
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
@@ -96,5 +96,106 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do
|
|||||||
|
|
||||||
it_behaves_like 'synchronizes followers'
|
it_behaves_like 'synchronizes followers'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the endpoint is a paginated Collection of actor URIs split across multiple pages' do
|
||||||
|
before do
|
||||||
|
stub_request(:get, 'https://example.com/partial-followers')
|
||||||
|
.to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
type: 'Collection',
|
||||||
|
id: 'https://example.com/partial-followers',
|
||||||
|
first: 'https://example.com/partial-followers/1',
|
||||||
|
}))
|
||||||
|
|
||||||
|
stub_request(:get, 'https://example.com/partial-followers/1')
|
||||||
|
.to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
type: 'CollectionPage',
|
||||||
|
id: 'https://example.com/partial-followers/1',
|
||||||
|
partOf: 'https://example.com/partial-followers',
|
||||||
|
next: 'https://example.com/partial-followers/2',
|
||||||
|
items: [alice, eve].map { |account| ActivityPub::TagManager.instance.uri_for(account) },
|
||||||
|
}))
|
||||||
|
|
||||||
|
stub_request(:get, 'https://example.com/partial-followers/2')
|
||||||
|
.to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
type: 'CollectionPage',
|
||||||
|
id: 'https://example.com/partial-followers/2',
|
||||||
|
partOf: 'https://example.com/partial-followers',
|
||||||
|
items: ActivityPub::TagManager.instance.uri_for(mallory),
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'synchronizes followers'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the endpoint is a paginated Collection of actor URIs split across, but one page errors out' do
|
||||||
|
before do
|
||||||
|
stub_request(:get, 'https://example.com/partial-followers')
|
||||||
|
.to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
type: 'Collection',
|
||||||
|
id: 'https://example.com/partial-followers',
|
||||||
|
first: 'https://example.com/partial-followers/1',
|
||||||
|
}))
|
||||||
|
|
||||||
|
stub_request(:get, 'https://example.com/partial-followers/1')
|
||||||
|
.to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
type: 'CollectionPage',
|
||||||
|
id: 'https://example.com/partial-followers/1',
|
||||||
|
partOf: 'https://example.com/partial-followers',
|
||||||
|
next: 'https://example.com/partial-followers/2',
|
||||||
|
items: [mallory].map { |account| ActivityPub::TagManager.instance.uri_for(account) },
|
||||||
|
}))
|
||||||
|
|
||||||
|
stub_request(:get, 'https://example.com/partial-followers/2')
|
||||||
|
.to_return(status: 404)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'confirms pending follow request but does not remove extra followers' do
|
||||||
|
previous_follower_ids = actor.followers.pluck(:id)
|
||||||
|
|
||||||
|
subject.call(actor, collection_uri)
|
||||||
|
|
||||||
|
expect(previous_follower_ids - actor.followers.reload.pluck(:id))
|
||||||
|
.to be_empty
|
||||||
|
expect(mallory)
|
||||||
|
.to be_following(actor)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the endpoint is a paginated Collection of actor URIs with more pages than we allow' do
|
||||||
|
let(:payload) do
|
||||||
|
{
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
type: 'Collection',
|
||||||
|
id: collection_uri,
|
||||||
|
first: {
|
||||||
|
type: 'CollectionPage',
|
||||||
|
partOf: collection_uri,
|
||||||
|
items: items,
|
||||||
|
next: "#{collection_uri}/page2",
|
||||||
|
},
|
||||||
|
}.with_indifferent_access
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_const('ActivityPub::SynchronizeFollowersService::MAX_COLLECTION_PAGES', 1)
|
||||||
|
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'confirms pending follow request but does not remove extra followers' do
|
||||||
|
previous_follower_ids = actor.followers.pluck(:id)
|
||||||
|
|
||||||
|
subject.call(actor, collection_uri)
|
||||||
|
|
||||||
|
expect(previous_follower_ids - actor.followers.reload.pluck(:id))
|
||||||
|
.to be_empty
|
||||||
|
expect(mallory)
|
||||||
|
.to be_following(actor)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe SuspendAccountService, :inline_jobs do
|
RSpec.describe SuspendAccountService do
|
||||||
shared_examples 'common behavior' do
|
shared_examples 'common behavior' do
|
||||||
subject { described_class.new.call(account) }
|
subject { described_class.new.call(account) }
|
||||||
|
|
||||||
@@ -11,6 +11,7 @@ RSpec.describe SuspendAccountService, :inline_jobs do
|
|||||||
|
|
||||||
before do
|
before do
|
||||||
allow(FeedManager.instance).to receive_messages(unmerge_from_home: nil, unmerge_from_list: nil)
|
allow(FeedManager.instance).to receive_messages(unmerge_from_home: nil, unmerge_from_list: nil)
|
||||||
|
allow(Rails.configuration.x).to receive(:cache_buster_enabled).and_return(true)
|
||||||
|
|
||||||
local_follower.follow!(account)
|
local_follower.follow!(account)
|
||||||
list.accounts << account
|
list.accounts << account
|
||||||
@@ -23,6 +24,7 @@ RSpec.describe SuspendAccountService, :inline_jobs do
|
|||||||
it 'unmerges from feeds of local followers and changes file mode and preserves suspended flag' do
|
it 'unmerges from feeds of local followers and changes file mode and preserves suspended flag' do
|
||||||
expect { subject }
|
expect { subject }
|
||||||
.to change_file_mode
|
.to change_file_mode
|
||||||
|
.and enqueue_sidekiq_job(CacheBusterWorker).with(account.media_attachments.first.file.url(:original))
|
||||||
.and not_change_suspended_flag
|
.and not_change_suspended_flag
|
||||||
expect(FeedManager.instance).to have_received(:unmerge_from_home).with(account, local_follower)
|
expect(FeedManager.instance).to have_received(:unmerge_from_home).with(account, local_follower)
|
||||||
expect(FeedManager.instance).to have_received(:unmerge_from_list).with(account, list)
|
expect(FeedManager.instance).to have_received(:unmerge_from_list).with(account, list)
|
||||||
@@ -38,17 +40,12 @@ RSpec.describe SuspendAccountService, :inline_jobs do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe 'suspending a local account' do
|
describe 'suspending a local account' do
|
||||||
def match_update_actor_request(req, account)
|
def match_update_actor_request(json, account)
|
||||||
json = JSON.parse(req.body)
|
json = JSON.parse(json)
|
||||||
actor_id = ActivityPub::TagManager.instance.uri_for(account)
|
actor_id = ActivityPub::TagManager.instance.uri_for(account)
|
||||||
json['type'] == 'Update' && json['actor'] == actor_id && json['object']['id'] == actor_id && json['object']['suspended']
|
json['type'] == 'Update' && json['actor'] == actor_id && json['object']['id'] == actor_id && json['object']['suspended']
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
|
||||||
stub_request(:post, 'https://alice.com/inbox').to_return(status: 201)
|
|
||||||
stub_request(:post, 'https://bob.com/inbox').to_return(status: 201)
|
|
||||||
end
|
|
||||||
|
|
||||||
include_examples 'common behavior' do
|
include_examples 'common behavior' do
|
||||||
let!(:account) { Fabricate(:account) }
|
let!(:account) { Fabricate(:account) }
|
||||||
let!(:remote_follower) { Fabricate(:account, uri: 'https://alice.com', inbox_url: 'https://alice.com/inbox', protocol: :activitypub, domain: 'alice.com') }
|
let!(:remote_follower) { Fabricate(:account, uri: 'https://alice.com', inbox_url: 'https://alice.com/inbox', protocol: :activitypub, domain: 'alice.com') }
|
||||||
@@ -61,22 +58,20 @@ RSpec.describe SuspendAccountService, :inline_jobs do
|
|||||||
|
|
||||||
it 'sends an Update actor activity to followers and reporters' do
|
it 'sends an Update actor activity to followers and reporters' do
|
||||||
subject
|
subject
|
||||||
expect(a_request(:post, remote_follower.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once
|
|
||||||
expect(a_request(:post, remote_reporter.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once
|
expect(ActivityPub::DeliveryWorker)
|
||||||
|
.to have_enqueued_sidekiq_job(satisfying { |json| match_update_actor_request(json, account) }, account.id, remote_follower.inbox_url).once
|
||||||
|
.and have_enqueued_sidekiq_job(satisfying { |json| match_update_actor_request(json, account) }, account.id, remote_reporter.inbox_url).once
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'suspending a remote account' do
|
describe 'suspending a remote account' do
|
||||||
def match_reject_follow_request(req, account, followee)
|
def match_reject_follow_request(json, account, followee)
|
||||||
json = JSON.parse(req.body)
|
json = JSON.parse(json)
|
||||||
json['type'] == 'Reject' && json['actor'] == ActivityPub::TagManager.instance.uri_for(followee) && json['object']['actor'] == account.uri
|
json['type'] == 'Reject' && json['actor'] == ActivityPub::TagManager.instance.uri_for(followee) && json['object']['actor'] == account.uri
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
|
||||||
stub_request(:post, 'https://bob.com/inbox').to_return(status: 201)
|
|
||||||
end
|
|
||||||
|
|
||||||
include_examples 'common behavior' do
|
include_examples 'common behavior' do
|
||||||
let!(:account) { Fabricate(:account, domain: 'bob.com', uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub) }
|
let!(:account) { Fabricate(:account, domain: 'bob.com', uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub) }
|
||||||
let!(:local_followee) { Fabricate(:account) }
|
let!(:local_followee) { Fabricate(:account) }
|
||||||
@@ -88,7 +83,8 @@ RSpec.describe SuspendAccountService, :inline_jobs do
|
|||||||
it 'sends a Reject Follow activity', :aggregate_failures do
|
it 'sends a Reject Follow activity', :aggregate_failures do
|
||||||
subject
|
subject
|
||||||
|
|
||||||
expect(a_request(:post, account.inbox_url).with { |req| match_reject_follow_request(req, account, local_followee) }).to have_been_made.once
|
expect(ActivityPub::DeliveryWorker)
|
||||||
|
.to have_enqueued_sidekiq_job(satisfying { |json| match_reject_follow_request(json, account, local_followee) }, local_followee.id, account.inbox_url).once
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function configFromEnv(env, environment) {
|
|||||||
if (typeof parsedUrl.password === 'string') baseConfig.password = parsedUrl.password;
|
if (typeof parsedUrl.password === 'string') baseConfig.password = parsedUrl.password;
|
||||||
if (typeof parsedUrl.host === 'string') baseConfig.host = parsedUrl.host;
|
if (typeof parsedUrl.host === 'string') baseConfig.host = parsedUrl.host;
|
||||||
if (typeof parsedUrl.user === 'string') baseConfig.user = parsedUrl.user;
|
if (typeof parsedUrl.user === 'string') baseConfig.user = parsedUrl.user;
|
||||||
if (typeof parsedUrl.port === 'string') {
|
if (typeof parsedUrl.port === 'string' && parsedUrl.port) {
|
||||||
const parsedPort = parseInt(parsedUrl.port, 10);
|
const parsedPort = parseInt(parsedUrl.port, 10);
|
||||||
if (isNaN(parsedPort)) {
|
if (isNaN(parsedPort)) {
|
||||||
throw new Error('Invalid port specified in DATABASE_URL environment variable');
|
throw new Error('Invalid port specified in DATABASE_URL environment variable');
|
||||||
|
|||||||
Reference in New Issue
Block a user