Merge commit 'e387175fc9a3ebfd72ab45ebfe43ecfabef7b0c3' into glitch-soc/merge-upstream

pull/62/head
Claire 2023-05-25 23:47:28 +02:00
commit e2ab9d4dad
46 changed files with 570 additions and 303 deletions

View File

@ -239,31 +239,6 @@ Naming/VariableNumber:
- 'spec/models/user_spec.rb'
- 'spec/services/activitypub/fetch_featured_collection_service_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
Performance/MapCompact:
Exclude:
- 'app/lib/admin/metrics/dimension.rb'
- 'app/lib/admin/metrics/measure.rb'
- 'app/lib/feed_manager.rb'
- 'app/models/account.rb'
- 'app/models/account_statuses_cleanup_policy.rb'
- 'app/models/account_suggestions/setting_source.rb'
- 'app/models/account_suggestions/source.rb'
- 'app/models/follow_recommendation_filter.rb'
- 'app/models/notification.rb'
- 'app/models/user_role.rb'
- 'app/models/webhook.rb'
- 'app/services/process_mentions_service.rb'
- 'app/validators/existing_username_validator.rb'
- 'db/migrate/20200407202420_migrate_unavailable_inboxes.rb'
- 'spec/presenters/status_relationships_presenter_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: SafeMultiline.
Performance/StartWith:
Exclude:
- 'app/lib/extractor.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
Performance/UnfreezeString:
Exclude:
@ -599,10 +574,6 @@ RSpec/PredicateMatcher:
- 'spec/models/user_spec.rb'
- 'spec/services/post_status_service_spec.rb'
RSpec/RepeatedExample:
Exclude:
- 'spec/policies/status_policy_spec.rb'
RSpec/StubbedMock:
Exclude:
- 'spec/controllers/api/base_controller_spec.rb'

View File

@ -17,7 +17,7 @@ gem 'makara', '~> 0.5'
gem 'pghero'
gem 'dotenv-rails', '~> 2.8'
gem 'aws-sdk-s3', '~> 1.120', require: false
gem 'aws-sdk-s3', '~> 1.122', require: false
gem 'fog-core', '<= 2.4.0'
gem 'fog-openstack', '~> 0.3', require: false
gem 'kt-paperclip', '~> 7.1', github: 'kreeti/kt-paperclip', ref: '11abf222dc31bff71160a1d138b445214f434b2b'
@ -75,7 +75,7 @@ gem 'rails-settings-cached', '~> 0.6', git: 'https://github.com/mastodon/rails-s
gem 'redcarpet', '~> 3.6'
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 2.1'
gem 'rqrcode', '~> 2.2'
gem 'ruby-progressbar', '~> 1.13'
gem 'sanitize', '~> 6.0'
gem 'scenic', '~> 1.7'

View File

@ -109,16 +109,16 @@ GEM
attr_required (1.0.1)
awrence (1.2.1)
aws-eventstream (1.2.0)
aws-partitions (1.752.0)
aws-sdk-core (3.171.0)
aws-partitions (1.761.0)
aws-sdk-core (3.172.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.63.0)
aws-sdk-kms (1.64.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.121.0)
aws-sdk-s3 (1.122.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
@ -189,7 +189,7 @@ GEM
coderay (1.1.3)
color_diff (0.1)
concurrent-ruby (1.2.2)
connection_pool (2.4.0)
connection_pool (2.4.1)
cose (1.3.0)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 1.0)
@ -398,9 +398,9 @@ GEM
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.20.0)
loofah (2.21.3)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
nokogiri (>= 1.12.0)
mail (2.8.1)
mini_mime (>= 0.1.1)
net-imap
@ -576,7 +576,7 @@ GEM
rexml (3.2.5)
rotp (6.2.2)
rpam2 (4.0.2)
rqrcode (2.1.2)
rqrcode (2.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
@ -588,20 +588,20 @@ GEM
rspec-mocks (3.12.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-rails (6.0.1)
rspec-rails (6.0.2)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
rspec-core (~> 3.11)
rspec-expectations (~> 3.11)
rspec-mocks (~> 3.11)
rspec-support (~> 3.11)
rspec-core (~> 3.12)
rspec-expectations (~> 3.12)
rspec-mocks (~> 3.12)
rspec-support (~> 3.12)
rspec-sidekiq (3.1.0)
rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0)
rspec-support (3.12.0)
rspec_chunked (0.6)
rubocop (1.50.2)
rubocop (1.51.0)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.2.0.0)
@ -611,11 +611,11 @@ GEM
rubocop-ast (>= 1.28.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.28.0)
rubocop-ast (1.28.1)
parser (>= 3.2.1.0)
rubocop-capybara (2.18.0)
rubocop (~> 1.41)
rubocop-performance (1.17.1)
rubocop-performance (1.18.0)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rails (2.19.1)
@ -761,7 +761,7 @@ GEM
xorcist (1.1.3)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.7)
zeitwerk (2.6.8)
PLATFORMS
ruby
@ -770,7 +770,7 @@ DEPENDENCIES
active_model_serializers (~> 0.10)
addressable (~> 2.8)
annotate (~> 3.2)
aws-sdk-s3 (~> 1.120)
aws-sdk-s3 (~> 1.122)
better_errors (~> 2.9)
binding_of_caller (~> 1.0)
blurhash (~> 0.1)
@ -860,7 +860,7 @@ DEPENDENCIES
redcarpet (~> 3.6)
redis (~> 4.5)
redis-namespace (~> 1.10)
rqrcode (~> 2.1)
rqrcode (~> 2.2)
rspec-rails (~> 6.0)
rspec-sidekiq (~> 3.1)
rspec_chunked (~> 0.6)

View File

@ -13,7 +13,7 @@ class Api::V1::FeaturedTagsController < Api::BaseController
end
def create
featured_tag = CreateFeaturedTagService.new.call(current_account, featured_tag_params[:name])
featured_tag = CreateFeaturedTagService.new.call(current_account, params.require(:name))
render json: featured_tag, serializer: REST::FeaturedTagSerializer
end
@ -33,6 +33,6 @@ class Api::V1::FeaturedTagsController < Api::BaseController
end
def featured_tag_params
params.permit(:name)
params.require(:name)
end
end

View File

@ -4,7 +4,7 @@ import api from 'mastodon/api';
import { FormattedNumber } from 'react-intl';
import { Sparklines, SparklinesCurve } from 'react-sparklines';
import classNames from 'classnames';
import Skeleton from 'mastodon/components/skeleton';
import { Skeleton } from 'mastodon/components/skeleton';
const percIncrease = (a, b) => {
let percent;

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import api from 'mastodon/api';
import { FormattedNumber } from 'react-intl';
import { roundTo10 } from 'mastodon/utils/numbers';
import Skeleton from 'mastodon/components/skeleton';
import { Skeleton } from 'mastodon/components/skeleton';
export default class Dimension extends React.PureComponent {

View File

@ -5,7 +5,7 @@ import type { List } from 'immutable';
import type { Account } from '../../types/resources';
import { autoPlayGif } from '../initial_state';
import Skeleton from './skeleton';
import { Skeleton } from './skeleton';
interface Props {
account?: Account;

View File

@ -3,7 +3,7 @@ import React from 'react';
import classNames from 'classnames';
import { DisplayName } from 'mastodon/components/display_name';
import Skeleton from 'mastodon/components/skeleton';
import { Skeleton } from 'mastodon/components/skeleton';
interface Props {
size?: number;

View File

@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router-dom';
import ShortNumber from 'mastodon/components/short_number';
import Skeleton from 'mastodon/components/skeleton';
import { Skeleton } from 'mastodon/components/skeleton';
import classNames from 'classnames';
class SilentErrorBoundary extends React.Component {

View File

@ -4,7 +4,7 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { fetchServer } from 'mastodon/actions/server';
import ShortNumber from 'mastodon/components/short_number';
import Skeleton from 'mastodon/components/skeleton';
import { Skeleton } from 'mastodon/components/skeleton';
import Account from 'mastodon/containers/account_container';
import { domain } from 'mastodon/initial_state';
import { ServerHeroImage } from 'mastodon/components/server_hero_image';

View File

@ -1,11 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
const Skeleton = ({ width, height }) => <span className='skeleton' style={{ width, height }}>&zwnj;</span>;
Skeleton.propTypes = {
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
};
export default Skeleton;

View File

@ -0,0 +1,12 @@
import React from 'react';
interface Props {
width?: number | string;
height?: number | string;
}
export const Skeleton: React.FC<Props> = ({ width, height }) => (
<span className='skeleton' style={{ width, height }}>
&zwnj;
</span>
);

View File

@ -1,18 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
const TimelineHint = ({ resource, url }) => (
<div className='timeline-hint'>
<strong><FormattedMessage id='timeline_hint.remote_resource_not_displayed' defaultMessage='{resource} from other servers are not displayed.' values={{ resource }} /></strong>
<br />
<a href={url} target='_blank' rel='noopener'><FormattedMessage id='account.browse_more_on_origin_server' defaultMessage='Browse more on the original profile' /></a>
</div>
);
TimelineHint.propTypes = {
resource: PropTypes.node.isRequired,
url: PropTypes.string.isRequired,
};
export default TimelineHint;

View File

@ -0,0 +1,27 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
interface Props {
resource: JSX.Element;
url: string;
}
export const TimelineHint: React.FC<Props> = ({ resource, url }) => (
<div className='timeline-hint'>
<strong>
<FormattedMessage
id='timeline_hint.remote_resource_not_displayed'
defaultMessage='{resource} from other servers are not displayed.'
values={{ resource }}
/>
</strong>
<br />
<a href={url} target='_blank' rel='noopener noreferrer'>
<FormattedMessage
id='account.browse_more_on_origin_server'
defaultMessage='Browse more on the original profile'
/>
</a>
</div>
);

View File

@ -8,7 +8,7 @@ import LinkFooter from 'mastodon/features/ui/components/link_footer';
import { Helmet } from 'react-helmet';
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'mastodon/actions/server';
import Account from 'mastodon/containers/account_container';
import Skeleton from 'mastodon/components/skeleton';
import { Skeleton } from 'mastodon/components/skeleton';
import { Icon } from 'mastodon/components/icon';
import classNames from 'classnames';
import { ServerHeroImage } from 'mastodon/components/server_hero_image';

View File

@ -12,7 +12,7 @@ import ColumnBackButton from '../../components/column_back_button';
import { List as ImmutableList } from 'immutable';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import TimelineHint from 'mastodon/components/timeline_hint';
import { TimelineHint } from 'mastodon/components/timeline_hint';
import { me } from 'mastodon/initial_state';
import LimitedAccountHint from './components/limited_account_hint';
import { getAccountHidden } from 'mastodon/selectors';

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { Blurhash } from 'mastodon/components/blurhash';
import { accountsCountRenderer } from 'mastodon/components/hashtag';
import ShortNumber from 'mastodon/components/short_number';
import Skeleton from 'mastodon/components/skeleton';
import { Skeleton } from 'mastodon/components/skeleton';
import classNames from 'classnames';
export default class Story extends React.PureComponent {

View File

@ -17,7 +17,7 @@ import Column from '../ui/components/column';
import HeaderContainer from '../account_timeline/containers/header_container';
import ColumnBackButton from '../../components/column_back_button';
import ScrollableList from '../../components/scrollable_list';
import TimelineHint from 'mastodon/components/timeline_hint';
import { TimelineHint } from 'mastodon/components/timeline_hint';
import LimitedAccountHint from '../account_timeline/components/limited_account_hint';
import { getAccountHidden } from 'mastodon/selectors';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';

View File

@ -17,7 +17,7 @@ import Column from '../ui/components/column';
import HeaderContainer from '../account_timeline/containers/header_container';
import ColumnBackButton from '../../components/column_back_button';
import ScrollableList from '../../components/scrollable_list';
import TimelineHint from 'mastodon/components/timeline_hint';
import { TimelineHint } from 'mastodon/components/timeline_hint';
import LimitedAccountHint from '../account_timeline/components/limited_account_hint';
import { getAccountHidden } from 'mastodon/selectors';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';

View File

@ -4,7 +4,7 @@ import { Helmet } from 'react-helmet';
import { FormattedMessage, FormattedDate, injectIntl, defineMessages } from 'react-intl';
import Column from 'mastodon/components/column';
import api from 'mastodon/api';
import Skeleton from 'mastodon/components/skeleton';
import { Skeleton } from 'mastodon/components/skeleton';
const messages = defineMessages({
title: { id: 'privacy_policy.title', defaultMessage: 'Privacy Policy' },

View File

@ -85,7 +85,7 @@ class EmbedModal extends ImmutablePureComponent {
className='embed-modal__iframe'
frameBorder='0'
ref={this.setIframeRef}
sandbox='allow-same-origin'
sandbox='allow-scripts allow-same-origin'
title='preview'
/>
</div>

View File

@ -14,9 +14,9 @@ class Admin::Metrics::Dimension
}.freeze
def self.retrieve(dimension_keys, start_at, end_at, limit, params)
Array(dimension_keys).map do |key|
Array(dimension_keys).filter_map do |key|
klass = DIMENSIONS[key.to_sym]
klass&.new(start_at, end_at, limit, klass.with_params? ? params.require(key.to_sym) : nil)
end.compact
end
end
end

View File

@ -19,9 +19,9 @@ class Admin::Metrics::Measure
}.freeze
def self.retrieve(measure_keys, start_at, end_at, params)
Array(measure_keys).map do |key|
Array(measure_keys).filter_map do |key|
klass = MEASURES[key.to_sym]
klass&.new(start_at, end_at, klass.with_params? ? params.require(key.to_sym) : nil)
end.compact
end
end
end

View File

@ -64,7 +64,7 @@ module Extractor
end_position = match_data.char_end(1)
after = ::Regexp.last_match.post_match
if %r{\A://}.match?(after)
if after.start_with?('://')
hash_text.match(/(.+)(https?\Z)/) do |matched|
hash_text = matched[1]
end_position -= matched[2].codepoint_length

View File

@ -213,7 +213,7 @@ class FeedManager
timeline_key = key(:home, account.id)
timeline_status_ids = redis.zrange(timeline_key, 0, -1)
statuses = Status.where(id: timeline_status_ids).select(:id, :reblog_of_id, :account_id).to_a
reblogged_ids = Status.where(id: statuses.map(&:reblog_of_id).compact, account: target_account).pluck(:id)
reblogged_ids = Status.where(id: statuses.filter_map(&:reblog_of_id), account: target_account).pluck(:id)
with_mentions_ids = Mention.active.where(status_id: statuses.flat_map { |s| [s.id, s.reblog_of_id] }.compact, account: target_account).pluck(:status_id)
target_statuses = statuses.select do |status|
@ -233,7 +233,7 @@ class FeedManager
timeline_key = key(:list, list.id)
timeline_status_ids = redis.zrange(timeline_key, 0, -1)
statuses = Status.where(id: timeline_status_ids).select(:id, :reblog_of_id, :account_id).to_a
reblogged_ids = Status.where(id: statuses.map(&:reblog_of_id).compact, account: target_account).pluck(:id)
reblogged_ids = Status.where(id: statuses.filter_map(&:reblog_of_id), account: target_account).pluck(:id)
with_mentions_ids = Mention.active.where(status_id: statuses.flat_map { |s| [s.id, s.reblog_of_id] }.compact, account: target_account).pluck(:status_id)
target_statuses = statuses.select do |status|
@ -603,9 +603,9 @@ class FeedManager
arr
end
crutches[:following] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:in_reply_to_account_id).compact).pluck(:target_account_id).index_with(true)
crutches[:following] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map(&:in_reply_to_account_id)).pluck(:target_account_id).index_with(true)
crutches[:languages] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:account_id)).pluck(:target_account_id, :languages).to_h
crutches[:hiding_reblogs] = Follow.where(account_id: receiver_id, target_account_id: statuses.map { |s| s.account_id if s.reblog? }.compact, show_reblogs: false).pluck(:target_account_id).index_with(true)
crutches[:hiding_reblogs] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map { |s| s.account_id if s.reblog? }, show_reblogs: false).pluck(:target_account_id).index_with(true)
crutches[:blocking] = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true)
crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true)
crutches[:domain_blocking] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.flat_map { |s| [s.account.domain, s.reblog&.account&.domain] }.compact).pluck(:domain).index_with(true)

View File

@ -299,11 +299,11 @@ class Account < ApplicationRecord
end
def fields
(self[:fields] || []).map do |f|
(self[:fields] || []).filter_map do |f|
Account::Field.new(self, f)
rescue
nil
end.compact
end
end
def fields_attributes=(attributes)

View File

@ -117,12 +117,12 @@ class AccountStatusesCleanupPolicy < ApplicationRecord
private
def update_last_inspected
if EXCEPTION_BOOLS.map { |name| attribute_change_to_be_saved(name) }.compact.include?([true, false])
if EXCEPTION_BOOLS.filter_map { |name| attribute_change_to_be_saved(name) }.include?([true, false])
# Policy has been widened in such a way that any previously-inspected status
# may need to be deleted, so we'll have to start again.
redis.del("account_cleanup:#{account_id}")
end
redis.del("account_cleanup:#{account_id}") if EXCEPTION_THRESHOLDS.map { |name| attribute_change_to_be_saved(name) }.compact.any? { |old, new| old.present? && (new.nil? || new > old) }
redis.del("account_cleanup:#{account_id}") if EXCEPTION_THRESHOLDS.filter_map { |name| attribute_change_to_be_saved(name) }.any? { |old, new| old.present? && (new.nil? || new > old) }
end
def validate_local_account

View File

@ -48,14 +48,14 @@ class AccountSuggestions::SettingSource < AccountSuggestions::Source
end
def setting_to_usernames_and_domains
setting.split(',').map do |str|
setting.split(',').filter_map do |str|
username, domain = str.strip.gsub(/\A@/, '').split('@', 2)
domain = nil if TagManager.instance.local_domain?(domain)
next if username.blank?
[username.downcase, domain&.downcase]
end.compact
end
end
def setting

View File

@ -20,7 +20,7 @@ class AccountSuggestions::Source
map = scope.index_by { |account| to_ordered_list_key(account) }
ordered_list.map { |ordered_list_key| map[ordered_list_key] }.compact.map do |account|
ordered_list.filter_map { |ordered_list_key| map[ordered_list_key] }.map do |account|
AccountSuggestions::Suggestion.new(
account: account,
source: key

View File

@ -22,7 +22,7 @@ class FollowRecommendationFilter
account_ids = redis.zrevrange("follow_recommendations:#{@language}", 0, -1).map(&:to_i)
accounts = Account.where(id: account_ids).index_by(&:id)
account_ids.map { |id| accounts[id] }.compact
account_ids.filter_map { |id| accounts[id] }
end
end
end

View File

@ -114,7 +114,7 @@ class Notification < ApplicationRecord
ActiveRecord::Associations::Preloader.new.preload(grouped_notifications, associations)
end
unique_target_statuses = notifications.map(&:target_status).compact.uniq
unique_target_statuses = notifications.filter_map(&:target_status).uniq
# Call cache_collection in block
cached_statuses_by_id = yield(unique_target_statuses).index_by(&:id)

View File

@ -125,7 +125,7 @@ class UserRole < ApplicationRecord
end
def permissions_as_keys=(value)
self.permissions = value.map(&:presence).compact.reduce(Flags::NONE) { |bitmask, privilege| FLAGS.key?(privilege.to_sym) ? (bitmask | FLAGS[privilege.to_sym]) : bitmask }
self.permissions = value.filter_map(&:presence).reduce(Flags::NONE) { |bitmask, privilege| FLAGS.key?(privilege.to_sym) ? (bitmask | FLAGS[privilege.to_sym]) : bitmask }
end
def can?(*any_of_privileges)

View File

@ -53,7 +53,7 @@ class Webhook < ApplicationRecord
end
def strip_events
self.events = events.map { |str| str.strip.presence }.compact if events.present?
self.events = events.filter_map { |str| str.strip.presence } if events.present?
end
def generate_secret

View File

@ -68,7 +68,7 @@ class ProcessMentionsService < BaseService
def assign_mentions!
# Make sure we never mention blocked accounts
unless @current_mentions.empty?
mentioned_domains = @current_mentions.map { |m| m.account.domain }.compact.uniq
mentioned_domains = @current_mentions.filter_map { |m| m.account.domain }.uniq
blocked_domains = Set.new(mentioned_domains.empty? ? [] : AccountDomainBlock.where(account_id: @status.account_id, domain: mentioned_domains))
mentioned_account_ids = @current_mentions.map(&:account_id)
blocked_account_ids = Set.new(@status.account.block_relationships.where(target_account_id: mentioned_account_ids).pluck(:target_account_id))

View File

@ -4,14 +4,14 @@ class ExistingUsernameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank?
usernames_and_domains = value.split(',').map do |str|
usernames_and_domains = value.split(',').filter_map do |str|
username, domain = str.strip.gsub(/\A@/, '').split('@', 2)
domain = nil if TagManager.instance.local_domain?(domain)
next if username.blank?
[str, username, domain]
end.compact
end
usernames_with_no_accounts = usernames_and_domains.filter_map do |(str, username, domain)|
str unless Account.find_remote(username, domain)

View File

@ -5,6 +5,7 @@
= render 'auth/shared/progress', stage: 'confirm'
= hidden_field_tag :confirmation_token, params[:confirmation_token]
= hidden_field_tag :redirect_to_app, params[:redirect_to_app]
%p.lead= t('auth.captcha_confirmation.hint_html')

View File

@ -5,9 +5,9 @@ class MigrateUnavailableInboxes < ActiveRecord::Migration[5.2]
redis = RedisConfiguration.pool.checkout
urls = redis.smembers('unavailable_inboxes')
hosts = urls.map do |url|
hosts = urls.filter_map do |url|
Addressable::URI.parse(url).normalized_host
end.compact.uniq
end.uniq
UnavailableDomain.delete_all

View File

@ -67,7 +67,7 @@
"file-loader": "^6.2.0",
"font-awesome": "^4.7.0",
"fuzzysort": "^2.0.4",
"glob": "^10.2.2",
"glob": "^10.2.6",
"history": "^4.10.1",
"http-link-header": "^1.1.1",
"immutable": "^4.3.0",
@ -116,7 +116,7 @@
"regenerator-runtime": "^0.13.11",
"requestidlecallback": "^0.3.0",
"reselect": "^4.1.8",
"rimraf": "^5.0.0",
"rimraf": "^5.0.1",
"sass": "^1.62.1",
"sass-loader": "^10.2.0",
"stacktrace-js": "^2.0.2",
@ -131,7 +131,7 @@
"webpack-assets-manifest": "^4.0.6",
"webpack-bundle-analyzer": "^4.8.0",
"webpack-cli": "^3.3.12",
"webpack-merge": "^5.8.0",
"webpack-merge": "^5.9.0",
"wicg-inert": "^3.1.2",
"workbox-expiration": "^6.5.4",
"workbox-precaching": "^6.5.4",
@ -178,8 +178,8 @@
"@types/uuid": "^9.0.0",
"@types/webpack": "^4.41.33",
"@types/yargs": "^17.0.24",
"@typescript-eslint/eslint-plugin": "^5.59.6",
"@typescript-eslint/parser": "^5.59.6",
"@typescript-eslint/eslint-plugin": "^5.59.7",
"@typescript-eslint/parser": "^5.59.7",
"babel-jest": "^29.5.0",
"eslint": "^8.40.0",
"eslint-config-prettier": "^8.8.0",
@ -199,7 +199,7 @@
"prettier": "^2.8.8",
"react-intl-translations-manager": "^5.0.3",
"react-test-renderer": "^18.2.0",
"stylelint": "^15.6.1",
"stylelint": "^15.6.2",
"stylelint-config-standard-scss": "^9.0.0",
"typescript": "^5.0.4",
"webpack-dev-server": "^3.11.3",
@ -216,7 +216,7 @@
},
"lint-staged": {
"*": "prettier --ignore-unknown --write",
"Capfile|Gemfile|*.{rb,ruby,ru,rake}": "bundle exec rubocop -a",
"Capfile|Gemfile|*.{rb,ruby,ru,rake}": "bundle exec rubocop --force-exclusion -a",
"*.{js,jsx,ts,tsx}": "eslint --fix",
"*.{css,scss}": "stylelint --fix"
}

View File

@ -18,4 +18,59 @@ describe Admin::AnnouncementsController do
expect(response).to have_http_status(:success)
end
end
describe 'GET #new' do
it 'returns http success and renders new' do
get :new
expect(response).to have_http_status(:success)
expect(response).to render_template(:new)
end
end
describe 'GET #edit' do
let(:announcement) { Fabricate(:announcement) }
it 'returns http success and renders edit' do
get :edit, params: { id: announcement.id }
expect(response).to have_http_status(:success)
expect(response).to render_template(:edit)
end
end
describe 'POST #create' do
it 'creates a new announcement and redirects' do
expect do
post :create, params: { announcement: { text: 'The announcement message.' } }
end.to change(Announcement, :count).by(1)
expect(response).to redirect_to(admin_announcements_path)
expect(flash.notice).to match(I18n.t('admin.announcements.published_msg'))
end
end
describe 'PUT #update' do
let(:announcement) { Fabricate(:announcement, text: 'Original text') }
it 'updates an announcement and redirects' do
put :update, params: { id: announcement.id, announcement: { text: 'Updated text.' } }
expect(response).to redirect_to(admin_announcements_path)
expect(flash.notice).to match(I18n.t('admin.announcements.updated_msg'))
end
end
describe 'DELETE #destroy' do
let!(:announcement) { Fabricate(:announcement, text: 'Original text') }
it 'destroys an announcement and redirects' do
expect do
delete :destroy, params: { id: announcement.id }
end.to change(Announcement, :count).by(-1)
expect(response).to redirect_to(admin_announcements_path)
expect(flash.notice).to match(I18n.t('admin.announcements.destroyed_msg'))
end
end
end

View File

@ -1,23 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
describe Api::V1::FeaturedTagsController do
render_views
let(:user) { Fabricate(:user) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
let(:account) { Fabricate(:account) }
before do
allow(controller).to receive(:doorkeeper_token) { token }
end
describe 'GET #index' do
it 'returns http success' do
get :index, params: { account_id: account.id, limit: 2 }
expect(response).to have_http_status(200)
end
end
end

View File

@ -3,5 +3,5 @@
Fabricator(:featured_tag) do
account
tag
name 'Tag'
name { sequence(:name) { |i| "Tag#{i}" } }
end

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
require 'rails_helper'
describe 'email confirmation flow when captcha is enabled' do
let(:user) { Fabricate(:user, confirmed_at: nil, confirmation_token: 'foobar', created_by_application: client_app) }
let(:client_app) { nil }
before do
# rubocop:disable RSpec/AnyInstance -- easiest way to deal with that that I know of
allow_any_instance_of(Auth::ConfirmationsController).to receive(:captcha_enabled?).and_return(true)
allow_any_instance_of(Auth::ConfirmationsController).to receive(:check_captcha!).and_return(true)
allow_any_instance_of(Auth::ConfirmationsController).to receive(:render_captcha).and_return(nil)
# rubocop:enable RSpec/AnyInstance
end
context 'when the user signed up through an app' do
let(:client_app) { Fabricate(:application) }
it 'logs in' do
visit "/auth/confirmation?confirmation_token=#{user.confirmation_token}&redirect_to_app=true"
# It presents the user with a captcha form
expect(page).to have_title(I18n.t('auth.captcha_confirmation.title'))
# It does not confirm the user just yet
expect(user.reload.confirmed?).to be false
# It redirects to app and confirms user
click_on I18n.t('challenge.confirm')
expect(user.reload.confirmed?).to be true
expect(page).to have_current_path(/\A#{client_app.confirmation_redirect_uri}/, url: true)
end
end
end

View File

@ -11,75 +11,79 @@ RSpec.describe StatusPolicy, type: :model do
let(:bob) { Fabricate(:account, username: 'bob') }
let(:status) { Fabricate(:status, account: alice) }
permissions :show?, :reblog? do
it 'grants access when no viewer' do
expect(subject).to permit(nil, status)
end
context 'with the permissions of show? and reblog?' do
permissions :show?, :reblog? do
it 'grants access when no viewer' do
expect(subject).to permit(nil, status)
end
it 'denies access when viewer is blocked' do
block = Fabricate(:block)
status.visibility = :private
status.account = block.target_account
it 'denies access when viewer is blocked' do
block = Fabricate(:block)
status.visibility = :private
status.account = block.target_account
expect(subject).to_not permit(block.account, status)
expect(subject).to_not permit(block.account, status)
end
end
end
permissions :show? do
it 'grants access when direct and account is viewer' do
status.visibility = :direct
context 'with the permission of show?' do
permissions :show? do
it 'grants access when direct and account is viewer' do
status.visibility = :direct
expect(subject).to permit(status.account, status)
end
expect(subject).to permit(status.account, status)
end
it 'grants access when direct and viewer is mentioned' do
status.visibility = :direct
status.mentions = [Fabricate(:mention, account: alice)]
it 'grants access when direct and viewer is mentioned' do
status.visibility = :direct
status.mentions = [Fabricate(:mention, account: alice)]
expect(subject).to permit(alice, status)
end
expect(subject).to permit(alice, status)
end
it 'grants access when direct and non-owner viewer is mentioned and mentions are loaded' do
status.visibility = :direct
status.mentions = [Fabricate(:mention, account: bob)]
status.mentions.load
it 'grants access when direct and non-owner viewer is mentioned and mentions are loaded' do
status.visibility = :direct
status.mentions = [Fabricate(:mention, account: bob)]
status.mentions.load
expect(subject).to permit(bob, status)
end
expect(subject).to permit(bob, status)
end
it 'denies access when direct and viewer is not mentioned' do
viewer = Fabricate(:account)
status.visibility = :direct
it 'denies access when direct and viewer is not mentioned' do
viewer = Fabricate(:account)
status.visibility = :direct
expect(subject).to_not permit(viewer, status)
end
expect(subject).to_not permit(viewer, status)
end
it 'grants access when private and account is viewer' do
status.visibility = :private
it 'grants access when private and account is viewer' do
status.visibility = :private
expect(subject).to permit(status.account, status)
end
expect(subject).to permit(status.account, status)
end
it 'grants access when private and account is following viewer' do
follow = Fabricate(:follow)
status.visibility = :private
status.account = follow.target_account
it 'grants access when private and account is following viewer' do
follow = Fabricate(:follow)
status.visibility = :private
status.account = follow.target_account
expect(subject).to permit(follow.account, status)
end
expect(subject).to permit(follow.account, status)
end
it 'grants access when private and viewer is mentioned' do
status.visibility = :private
status.mentions = [Fabricate(:mention, account: alice)]
it 'grants access when private and viewer is mentioned' do
status.visibility = :private
status.mentions = [Fabricate(:mention, account: alice)]
expect(subject).to permit(alice, status)
end
expect(subject).to permit(alice, status)
end
it 'denies access when private and viewer is not mentioned or followed' do
viewer = Fabricate(:account)
status.visibility = :private
it 'denies access when private and viewer is not mentioned or followed' do
viewer = Fabricate(:account)
status.visibility = :private
expect(subject).to_not permit(viewer, status)
expect(subject).to_not permit(viewer, status)
end
end
it 'denies access when local-only and the viewer is not logged in' do
@ -95,55 +99,63 @@ RSpec.describe StatusPolicy, type: :model do
end
end
permissions :reblog? do
it 'denies access when private' do
viewer = Fabricate(:account)
status.visibility = :private
context 'with the permission of reblog?' do
permissions :reblog? do
it 'denies access when private' do
viewer = Fabricate(:account)
status.visibility = :private
expect(subject).to_not permit(viewer, status)
end
expect(subject).to_not permit(viewer, status)
end
it 'denies access when direct' do
viewer = Fabricate(:account)
status.visibility = :direct
it 'denies access when direct' do
viewer = Fabricate(:account)
status.visibility = :direct
expect(subject).to_not permit(viewer, status)
expect(subject).to_not permit(viewer, status)
end
end
end
permissions :destroy?, :unreblog? do
it 'grants access when account is deleter' do
expect(subject).to permit(status.account, status)
end
context 'with the permissions of destroy? and unreblog?' do
permissions :destroy?, :unreblog? do
it 'grants access when account is deleter' do
expect(subject).to permit(status.account, status)
end
it 'denies access when account is not deleter' do
expect(subject).to_not permit(bob, status)
end
it 'denies access when account is not deleter' do
expect(subject).to_not permit(bob, status)
end
it 'denies access when no deleter' do
expect(subject).to_not permit(nil, status)
it 'denies access when no deleter' do
expect(subject).to_not permit(nil, status)
end
end
end
permissions :favourite? do
it 'grants access when viewer is not blocked' do
follow = Fabricate(:follow)
status.account = follow.target_account
context 'with the permission of favourite?' do
permissions :favourite? do
it 'grants access when viewer is not blocked' do
follow = Fabricate(:follow)
status.account = follow.target_account
expect(subject).to permit(follow.account, status)
end
expect(subject).to permit(follow.account, status)
end
it 'denies when viewer is blocked' do
block = Fabricate(:block)
status.account = block.target_account
it 'denies when viewer is blocked' do
block = Fabricate(:block)
status.account = block.target_account
expect(subject).to_not permit(block.account, status)
expect(subject).to_not permit(block.account, status)
end
end
end
permissions :update? do
it 'grants access if owner' do
expect(subject).to permit(status.account, status)
context 'with the permission of update?' do
permissions :update? do
it 'grants access if owner' do
expect(subject).to permit(status.account, status)
end
end
end
end

View File

@ -15,7 +15,7 @@ RSpec.describe StatusRelationshipsPresenter do
let(:presenter) { StatusRelationshipsPresenter.new(statuses, current_account_id, **options) }
let(:current_account_id) { Fabricate(:account).id }
let(:statuses) { [Fabricate(:status)] }
let(:status_ids) { statuses.map(&:id) + statuses.map(&:reblog_of_id).compact }
let(:status_ids) { statuses.map(&:id) + statuses.filter_map(&:reblog_of_id) }
let(:default_map) { { 1 => true } }
context 'when options are not set' do

View File

@ -0,0 +1,201 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'FeaturedTags' do
let(:user) { Fabricate(:user) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:scopes) { 'read:accounts write:accounts' }
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
shared_examples 'forbidden for wrong scope' do |wrong_scope|
let(:scopes) { wrong_scope }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
describe 'GET /api/v1/featured_tags' do
context 'with wrong scope' do
before do
get '/api/v1/featured_tags', headers: headers
end
it_behaves_like 'forbidden for wrong scope', 'read:statuses'
end
context 'when Authorization header is missing' do
it 'returns http unauthorized' do
get '/api/v1/featured_tags'
expect(response).to have_http_status(401)
end
end
it 'returns http success' do
get '/api/v1/featured_tags', headers: headers
expect(response).to have_http_status(200)
end
context 'when the requesting user has no featured tag' do
before { Fabricate.times(3, :featured_tag) }
it 'returns an empty body' do
get '/api/v1/featured_tags', headers: headers
body = body_as_json
expect(body).to be_empty
end
end
context 'when the requesting user has featured tags' do
let!(:user_featured_tags) { Fabricate.times(5, :featured_tag, account: user.account) }
it 'returns only the featured tags belonging to the requesting user' do
get '/api/v1/featured_tags', headers: headers
body = body_as_json
expected_ids = user_featured_tags.pluck(:id).map(&:to_s)
expect(body.pluck(:id)).to match_array(expected_ids)
end
end
end
describe 'POST /api/v1/featured_tags' do
let(:params) { { name: 'tag' } }
it 'returns http success' do
post '/api/v1/featured_tags', headers: headers, params: params
expect(response).to have_http_status(200)
end
it 'returns the correct tag name' do
post '/api/v1/featured_tags', headers: headers, params: params
body = body_as_json
expect(body[:name]).to eq(params[:name])
end
it 'creates a new featured tag for the requesting user' do
post '/api/v1/featured_tags', headers: headers, params: params
featured_tag = FeaturedTag.find_by(name: params[:name], account: user.account)
expect(featured_tag).to be_present
end
context 'with wrong scope' do
before do
post '/api/v1/featured_tags', headers: headers, params: params
end
it_behaves_like 'forbidden for wrong scope', 'read:statuses'
end
context 'when Authorization header is missing' do
it 'returns http unauthorized' do
post '/api/v1/featured_tags', params: params
expect(response).to have_http_status(401)
end
end
context 'when required param "name" is not provided' do
it 'returns http bad request' do
post '/api/v1/featured_tags', headers: headers
expect(response).to have_http_status(400)
end
end
context 'when provided tag name is invalid' do
let(:params) { { name: 'asj&*!' } }
it 'returns http unprocessable entity' do
post '/api/v1/featured_tags', headers: headers, params: params
expect(response).to have_http_status(422)
end
end
context 'when tag name is already taken'