Merge remote-tracking branch 'parent/main' into upstream-20231110
This commit is contained in:
commit
bfc7b0101d
44 changed files with 992 additions and 744 deletions
|
@ -15,6 +15,6 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
|||
RUN gem install foreman
|
||||
|
||||
# [Optional] Uncomment this line to install global node packages.
|
||||
RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g yarn" 2>&1
|
||||
RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && corepack enable" 2>&1
|
||||
|
||||
COPY welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt
|
||||
|
|
|
@ -11,6 +11,7 @@ bundle install
|
|||
git checkout -- Gemfile.lock
|
||||
|
||||
# Fetch Javascript dependencies
|
||||
corepack prepare
|
||||
yarn install --immutable
|
||||
|
||||
# [re]create, migrate, and seed the test database
|
||||
|
|
1
.github/renovate.json5
vendored
1
.github/renovate.json5
vendored
|
@ -12,6 +12,7 @@
|
|||
// If we do not want a package to be grouped with others, we need to set its groupName
|
||||
// to `null` after any other rule set it to something.
|
||||
dependencyDashboardHeader: 'This issue lists Renovate updates and detected dependencies. Read the [Dependency Dashboard](https://docs.renovatebot.com/key-concepts/dashboard/) docs to learn more. Before approving any upgrade: read the description and comments in the [`renovate.json5` file](https://github.com/mastodon/mastodon/blob/main/.github/renovate.json5).',
|
||||
postUpdateOptions: ['yarnDedupeHighest'],
|
||||
packageRules: [
|
||||
{
|
||||
// Require Dependency Dashboard Approval for major version bumps of these node packages
|
||||
|
|
|
@ -35,7 +35,6 @@ linters:
|
|||
- 'app/views/admin/accounts/_local_account.html.haml'
|
||||
- 'app/views/admin/roles/_form.html.haml'
|
||||
- 'app/views/home/index.html.haml'
|
||||
- 'app/views/layouts/application.html.haml'
|
||||
|
||||
ViewLength:
|
||||
exclude:
|
||||
|
|
|
@ -24,15 +24,6 @@ Lint/NonLocalExitFromIterator:
|
|||
Exclude:
|
||||
- 'app/helpers/jsonld_helper.rb'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
|
||||
Lint/UnusedBlockArgument:
|
||||
Exclude:
|
||||
- 'config/initializers/content_security_policy.rb'
|
||||
- 'config/initializers/doorkeeper.rb'
|
||||
- 'config/initializers/paperclip.rb'
|
||||
- 'config/initializers/simple_form.rb'
|
||||
|
||||
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
|
||||
Metrics/AbcSize:
|
||||
Max: 144
|
||||
|
@ -113,12 +104,6 @@ RSpec/LetSetup:
|
|||
- 'spec/services/unsuspend_account_service_spec.rb'
|
||||
- 'spec/workers/scheduler/user_cleanup_scheduler_spec.rb'
|
||||
|
||||
RSpec/MessageChain:
|
||||
Exclude:
|
||||
- 'spec/models/concerns/remotable_spec.rb'
|
||||
- 'spec/models/session_activation_spec.rb'
|
||||
- 'spec/models/setting_spec.rb'
|
||||
|
||||
RSpec/MultipleExpectations:
|
||||
Max: 8
|
||||
|
||||
|
@ -158,11 +143,6 @@ Rails/HasManyOrHasOneDependent:
|
|||
- 'app/models/user.rb'
|
||||
- 'app/models/web/push_subscription.rb'
|
||||
|
||||
Rails/I18nLocaleTexts:
|
||||
Exclude:
|
||||
- 'lib/tasks/mastodon.rake'
|
||||
- 'spec/helpers/flashes_helper_spec.rb'
|
||||
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/controllers/**/*.rb, app/mailers/**/*.rb
|
||||
Rails/LexicallyScopedActionFilter:
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -16,7 +16,7 @@ gem 'dotenv-rails', '~> 2.8'
|
|||
|
||||
gem 'aws-sdk-s3', '~> 1.123', require: false
|
||||
gem 'fog-core', '<= 2.4.0'
|
||||
gem 'fog-openstack', '~> 0.3', require: false
|
||||
gem 'fog-openstack', '~> 1.0', require: false
|
||||
gem 'kt-paperclip', '~> 7.2'
|
||||
gem 'md-paperclip-azure', '~> 2.2', require: false
|
||||
gem 'blurhash', '~> 0.1'
|
||||
|
|
20
Gemfile.lock
20
Gemfile.lock
|
@ -263,7 +263,7 @@ GEM
|
|||
erubi (1.12.0)
|
||||
et-orbi (1.2.7)
|
||||
tzinfo
|
||||
excon (0.100.0)
|
||||
excon (0.104.0)
|
||||
fabrication (2.30.0)
|
||||
faker (3.2.2)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
|
@ -298,19 +298,18 @@ GEM
|
|||
ffi-compiler (1.0.1)
|
||||
ffi (>= 1.0.0)
|
||||
rake
|
||||
fog-core (2.1.0)
|
||||
fog-core (2.3.0)
|
||||
builder
|
||||
excon (~> 0.58)
|
||||
formatador (~> 0.2)
|
||||
excon (~> 0.71)
|
||||
formatador (>= 0.2, < 2.0)
|
||||
mime-types
|
||||
fog-json (1.2.0)
|
||||
fog-core
|
||||
multi_json (~> 1.10)
|
||||
fog-openstack (0.3.10)
|
||||
fog-core (>= 1.45, <= 2.1.0)
|
||||
fog-openstack (1.1.0)
|
||||
fog-core (~> 2.1)
|
||||
fog-json (>= 1.0)
|
||||
ipaddress (>= 0.8)
|
||||
formatador (0.3.0)
|
||||
formatador (1.1.0)
|
||||
fugit (1.8.1)
|
||||
et-orbi (~> 1, >= 1.2.7)
|
||||
raabro (~> 1.4)
|
||||
|
@ -370,7 +369,6 @@ GEM
|
|||
terminal-table (>= 1.5.1)
|
||||
idn-ruby (0.1.5)
|
||||
io-console (0.6.0)
|
||||
ipaddress (0.8.3)
|
||||
irb (1.8.1)
|
||||
rdoc
|
||||
reline (>= 0.3.8)
|
||||
|
@ -452,7 +450,7 @@ GEM
|
|||
memory_profiler (1.0.1)
|
||||
mime-types (3.5.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2023.0808)
|
||||
mime-types-data (3.2023.1003)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.5)
|
||||
minitest (5.20.0)
|
||||
|
@ -860,7 +858,7 @@ DEPENDENCIES
|
|||
fast_blank (~> 1.0)
|
||||
fastimage
|
||||
fog-core (<= 2.4.0)
|
||||
fog-openstack (~> 0.3)
|
||||
fog-openstack (~> 1.0)
|
||||
fuubar (~> 2.5)
|
||||
haml-rails (~> 2.0)
|
||||
haml_lint
|
||||
|
|
|
@ -5,10 +5,11 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
|
|||
before_action :require_user!
|
||||
|
||||
def index
|
||||
accounts = Account.without_suspended.where(id: account_ids).select('id')
|
||||
scope = Account.where(id: account_ids).select('id')
|
||||
scope.merge!(Account.without_suspended) unless truthy_param?(:with_suspended)
|
||||
# .where doesn't guarantee that our results are in the same order
|
||||
# we requested them, so return the "right" order to the requestor.
|
||||
@accounts = accounts.index_by(&:id).values_at(*account_ids).compact
|
||||
@accounts = scope.index_by(&:id).values_at(*account_ids).compact
|
||||
render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships
|
||||
end
|
||||
|
||||
|
|
|
@ -91,6 +91,14 @@ module ApplicationHelper
|
|||
end
|
||||
end
|
||||
|
||||
def html_title
|
||||
safe_join(
|
||||
[content_for(:page_title).to_s.chomp, title]
|
||||
.select(&:present?),
|
||||
' - '
|
||||
)
|
||||
end
|
||||
|
||||
def title
|
||||
Rails.env.production? ? site_title : "#{site_title} (Dev)"
|
||||
end
|
||||
|
|
|
@ -460,7 +460,7 @@ export function fetchRelationships(accountIds) {
|
|||
|
||||
dispatch(fetchRelationshipsRequest(newAccountIds));
|
||||
|
||||
api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
|
||||
api(getState).get(`/api/v1/accounts/relationships?with_suspended=true&${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
|
||||
dispatch(fetchRelationshipsSuccess({ relationships: response.data }));
|
||||
}).catch(error => {
|
||||
dispatch(fetchRelationshipsFail(error));
|
||||
|
|
|
@ -29,6 +29,7 @@ export interface ApiAccountOtherSettingsJSON {
|
|||
| 'followers_only'
|
||||
| 'mutuals_only'
|
||||
| 'block';
|
||||
subscription_policy: 'allow' | 'followers_only' | 'block';
|
||||
}
|
||||
|
||||
// See app/serializers/rest/account_serializer.rb
|
||||
|
@ -63,4 +64,5 @@ export interface ApiAccountJSON {
|
|||
suspended?: boolean;
|
||||
limited?: boolean;
|
||||
memorial?: boolean;
|
||||
hide_collections: boolean;
|
||||
}
|
||||
|
|
|
@ -121,7 +121,7 @@ class Account extends ImmutablePureComponent {
|
|||
buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />;
|
||||
} else if (defaultAction === 'block') {
|
||||
buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />;
|
||||
} else if (!account.get('moved') || following) {
|
||||
} else if (!account.get('suspended') && !account.get('moved') || following) {
|
||||
buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -295,7 +295,7 @@ class Header extends ImmutablePureComponent {
|
|||
lockedIcon = <Icon id='lock' icon={LockIcon} title={intl.formatMessage(messages.account_locked)} />;
|
||||
}
|
||||
|
||||
if (signedIn && account.get('id') !== me) {
|
||||
if (signedIn && account.get('id') !== me && !account.get('suspended')) {
|
||||
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
|
||||
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
|
||||
menu.push(null);
|
||||
|
@ -305,7 +305,7 @@ class Header extends ImmutablePureComponent {
|
|||
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') });
|
||||
}
|
||||
|
||||
if ('share' in navigator) {
|
||||
if ('share' in navigator && !account.get('suspended')) {
|
||||
menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare });
|
||||
menu.push(null);
|
||||
}
|
||||
|
@ -358,7 +358,9 @@ class Header extends ImmutablePureComponent {
|
|||
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock, dangerous: true });
|
||||
}
|
||||
|
||||
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true });
|
||||
if (!account.get('suspended')) {
|
||||
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (signedIn && isRemote) {
|
||||
|
@ -406,7 +408,7 @@ class Header extends ImmutablePureComponent {
|
|||
|
||||
<div className='account__header__image'>
|
||||
<div className='account__header__info'>
|
||||
{!suspended && info}
|
||||
{info}
|
||||
</div>
|
||||
|
||||
{!(suspended || hidden) && <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />}
|
||||
|
@ -418,18 +420,16 @@ class Header extends ImmutablePureComponent {
|
|||
<Avatar account={suspended || hidden ? undefined : account} size={90} />
|
||||
</a>
|
||||
|
||||
{!suspended && (
|
||||
<div className='account__header__tabs__buttons'>
|
||||
{!hidden && (
|
||||
<>
|
||||
{actionBtn}
|
||||
{bellBtn}
|
||||
</>
|
||||
)}
|
||||
<div className='account__header__tabs__buttons'>
|
||||
{!hidden && (
|
||||
<>
|
||||
{actionBtn}
|
||||
{bellBtn}
|
||||
</>
|
||||
)}
|
||||
|
||||
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
|
||||
</div>
|
||||
)}
|
||||
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='account__header__tabs__name'>
|
||||
|
|
|
@ -45,6 +45,7 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
|
|||
hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'followers', accountId, 'isLoading'], true),
|
||||
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
||||
hideCollections: state.getIn(['accounts', accountId, 'hide_collections'], false),
|
||||
hidden: getAccountHidden(state, accountId),
|
||||
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
|
||||
};
|
||||
|
@ -111,7 +112,7 @@ class Followers extends ImmutablePureComponent {
|
|||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { accountId, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl } = this.props;
|
||||
const { accountId, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl, hideCollections } = this.props;
|
||||
|
||||
if (!isAccount) {
|
||||
return (
|
||||
|
@ -137,6 +138,8 @@ class Followers extends ImmutablePureComponent {
|
|||
emptyMessage = <LimitedAccountHint accountId={accountId} />;
|
||||
} else if (blockedBy) {
|
||||
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
|
||||
} else if (hideCollections && accountIds.isEmpty()) {
|
||||
emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />;
|
||||
} else if (remote && accountIds.isEmpty()) {
|
||||
emptyMessage = <RemoteHint url={remoteUrl} />;
|
||||
} else {
|
||||
|
|
|
@ -45,6 +45,7 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
|
|||
hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'following', accountId, 'isLoading'], true),
|
||||
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
||||
hideCollections: state.getIn(['accounts', accountId, 'hide_collections'], false),
|
||||
hidden: getAccountHidden(state, accountId),
|
||||
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
|
||||
};
|
||||
|
@ -111,7 +112,7 @@ class Following extends ImmutablePureComponent {
|
|||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { accountId, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl } = this.props;
|
||||
const { accountId, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl, hideCollections } = this.props;
|
||||
|
||||
if (!isAccount) {
|
||||
return (
|
||||
|
@ -137,6 +138,8 @@ class Following extends ImmutablePureComponent {
|
|||
emptyMessage = <LimitedAccountHint accountId={accountId} />;
|
||||
} else if (blockedBy) {
|
||||
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
|
||||
} else if (hideCollections && accountIds.isEmpty()) {
|
||||
emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />;
|
||||
} else if (remote && accountIds.isEmpty()) {
|
||||
emptyMessage = <RemoteHint url={remoteUrl} />;
|
||||
} else {
|
||||
|
|
|
@ -469,6 +469,10 @@ class Video extends PureComponent {
|
|||
};
|
||||
|
||||
_syncVideoToVolumeState = (volume = null, muted = null) => {
|
||||
if (!this.video) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.video.volume = volume ?? this.state.volume;
|
||||
this.video.muted = muted ?? this.state.muted;
|
||||
};
|
||||
|
|
|
@ -238,6 +238,7 @@
|
|||
"emoji_button.search_results": "Search results",
|
||||
"emoji_button.symbols": "Symbols",
|
||||
"emoji_button.travel": "Travel & Places",
|
||||
"empty_column.account_hides_collections": "This user has chosen to not make this information available",
|
||||
"empty_column.account_suspended": "Account suspended",
|
||||
"empty_column.account_timeline": "No posts here!",
|
||||
"empty_column.account_unavailable": "Profile unavailable",
|
||||
|
|
|
@ -59,6 +59,7 @@ const AccountOtherSettingsFactory = ImmutableRecord<AccountOtherSettingsShape>({
|
|||
link_preview: true,
|
||||
allow_quote: true,
|
||||
emoji_reaction_policy: 'allow',
|
||||
subscription_policy: 'allow',
|
||||
});
|
||||
|
||||
// Account
|
||||
|
@ -111,6 +112,7 @@ export const accountDefaultValues: AccountShape = {
|
|||
memorial: false,
|
||||
limited: false,
|
||||
moved: null,
|
||||
hide_collections: false,
|
||||
other_settings: AccountOtherSettingsFactory(),
|
||||
subscribable: true,
|
||||
};
|
||||
|
|
|
@ -18,12 +18,37 @@ class StatusCacheHydrator
|
|||
# We take advantage of the fact that some relationships can only occur with an original status, not
|
||||
# the reblog that wraps it, so we can assume that some values are always false
|
||||
if payload[:reblog]
|
||||
hydrate_reblog_payload(payload, account_id)
|
||||
else
|
||||
hydrate_non_reblog_payload(payload, account_id, account)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def hydrate_non_reblog_payload(empty_payload, account_id, account)
|
||||
empty_payload.tap do |payload|
|
||||
payload[:favourited] = Favourite.where(account_id: account_id, status_id: @status.id).exists?
|
||||
payload[:reblogged] = Status.where(account_id: account_id, reblog_of_id: @status.id).exists?
|
||||
payload[:muted] = ConversationMute.where(account_id: account_id, conversation_id: @status.conversation_id).exists?
|
||||
payload[:bookmarked] = Bookmark.where(account_id: account_id, status_id: @status.id).exists?
|
||||
payload[:pinned] = StatusPin.where(account_id: account_id, status_id: @status.id).exists? if @status.account_id == account_id
|
||||
payload[:filtered] = mapped_applied_custom_filter(account_id, @status)
|
||||
payload[:emoji_reactions] = @status.emoji_reactions_grouped_by_name(account)
|
||||
|
||||
if payload[:poll]
|
||||
payload[:poll][:voted] = @status.account_id == account_id
|
||||
payload[:poll][:own_votes] = []
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def hydrate_reblog_payload(empty_payload, account_id)
|
||||
empty_payload.tap do |payload|
|
||||
payload[:muted] = false
|
||||
payload[:bookmarked] = false
|
||||
payload[:pinned] = false if @status.account_id == account_id
|
||||
payload[:filtered] = CustomFilter
|
||||
.apply_cached_filters(CustomFilter.cached_filters_for(account_id), @status.reblog, following?(account_id))
|
||||
.map { |filter| serialized_filter(filter) }
|
||||
payload[:filtered] = mapped_applied_custom_filter(account_id, @status.reblog)
|
||||
|
||||
# If the reblogged status is being delivered to the author who disabled the display of the application
|
||||
# used to create the status, we need to hydrate it here too
|
||||
|
@ -50,27 +75,14 @@ class StatusCacheHydrator
|
|||
|
||||
payload[:favourited] = payload[:reblog][:favourited]
|
||||
payload[:reblogged] = payload[:reblog][:reblogged]
|
||||
else
|
||||
payload[:favourited] = Favourite.where(account_id: account_id, status_id: @status.id).exists?
|
||||
payload[:reblogged] = Status.where(account_id: account_id, reblog_of_id: @status.id).exists?
|
||||
payload[:muted] = ConversationMute.where(account_id: account_id, conversation_id: @status.conversation_id).exists?
|
||||
payload[:bookmarked] = Bookmark.where(account_id: account_id, status_id: @status.id).exists?
|
||||
payload[:pinned] = StatusPin.where(account_id: account_id, status_id: @status.id).exists? if @status.account_id == account_id
|
||||
payload[:filtered] = CustomFilter
|
||||
.apply_cached_filters(CustomFilter.cached_filters_for(account_id), @status, following?(account_id))
|
||||
.map { |filter| serialized_filter(filter) }
|
||||
payload[:emoji_reactions] = @status.emoji_reactions_grouped_by_name(account)
|
||||
|
||||
if payload[:poll]
|
||||
payload[:poll][:voted] = @status.account_id == account_id
|
||||
payload[:poll][:own_votes] = []
|
||||
end
|
||||
end
|
||||
|
||||
payload
|
||||
end
|
||||
|
||||
private
|
||||
def mapped_applied_custom_filter(account_id, status)
|
||||
CustomFilter
|
||||
.apply_cached_filters(CustomFilter.cached_filters_for(account_id), status, following?(account_id))
|
||||
.map { |filter| serialized_filter(filter) }
|
||||
end
|
||||
|
||||
def following?(account_id)
|
||||
Follow.exists?(account_id: account_id, target_account_id: @status.account_id)
|
||||
|
|
|
@ -8,7 +8,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
|
|||
|
||||
attributes :id, :username, :acct, :display_name, :locked, :bot, :discoverable, :group, :created_at,
|
||||
:note, :url, :uri, :avatar, :avatar_static, :header, :header_static, :subscribable,
|
||||
:followers_count, :following_count, :statuses_count, :last_status_at, :other_settings, :noindex
|
||||
:followers_count, :following_count, :statuses_count, :last_status_at, :hide_collections, :other_settings, :noindex
|
||||
|
||||
has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested?
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
%meta{ name: 'theme-color', content: '#191b22' }/
|
||||
%meta{ name: 'apple-mobile-web-app-capable', content: 'yes' }/
|
||||
|
||||
%title= content_for?(:page_title) ? safe_join([yield(:page_title).chomp.html_safe, title], ' - ') : title
|
||||
%title= html_title
|
||||
|
||||
= stylesheet_pack_tag 'common', media: 'all', crossorigin: 'anonymous'
|
||||
= stylesheet_pack_tag current_theme, media: 'all', crossorigin: 'anonymous'
|
||||
|
|
|
@ -67,7 +67,7 @@ end
|
|||
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
|
||||
# Rails.application.config.content_security_policy_report_only = true
|
||||
|
||||
Rails.application.config.content_security_policy_nonce_generator = ->(request) { SecureRandom.base64(16) }
|
||||
Rails.application.config.content_security_policy_nonce_generator = ->(_request) { SecureRandom.base64(16) }
|
||||
|
||||
Rails.application.config.content_security_policy_nonce_directives = %w(style-src)
|
||||
|
||||
|
@ -92,7 +92,7 @@ Rails.application.reloader.to_prepare do
|
|||
p.worker_src :none
|
||||
end
|
||||
|
||||
LetterOpenerWeb::LettersController.after_action do |p|
|
||||
LetterOpenerWeb::LettersController.after_action do
|
||||
request.content_security_policy_nonce_directives = %w(script-src)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -171,7 +171,7 @@ Doorkeeper.configure do
|
|||
# Under some circumstances you might want to have applications auto-approved,
|
||||
# so that the user skips the authorization step.
|
||||
# For example if dealing with a trusted application.
|
||||
skip_authorization do |resource_owner, client|
|
||||
skip_authorization do |_resource_owner, client|
|
||||
client.application.superapp?
|
||||
end
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ Paperclip.interpolates :filename do |attachment, style|
|
|||
end
|
||||
end
|
||||
|
||||
Paperclip.interpolates :prefix_path do |attachment, style|
|
||||
Paperclip.interpolates :prefix_path do |attachment, _style|
|
||||
if attachment.storage_schema_version >= 1 && attachment.instance.respond_to?(:local?) && !attachment.instance.local?
|
||||
'cache' + File::SEPARATOR
|
||||
else
|
||||
|
@ -19,7 +19,7 @@ Paperclip.interpolates :prefix_path do |attachment, style|
|
|||
end
|
||||
end
|
||||
|
||||
Paperclip.interpolates :prefix_url do |attachment, style|
|
||||
Paperclip.interpolates :prefix_url do |attachment, _style|
|
||||
if attachment.storage_schema_version >= 1 && attachment.instance.respond_to?(:local?) && !attachment.instance.local?
|
||||
'cache/'
|
||||
else
|
||||
|
|
|
@ -43,6 +43,7 @@ end
|
|||
Sidekiq.logger.level = ::Logger.const_get(ENV.fetch('RAILS_LOG_LEVEL', 'info').upcase.to_s)
|
||||
|
||||
SidekiqUniqueJobs.configure do |config|
|
||||
config.enabled = !Rails.env.test?
|
||||
config.reaper = :ruby
|
||||
config.reaper_count = 1000
|
||||
config.reaper_interval = 600
|
||||
|
|
|
@ -188,7 +188,7 @@ SimpleForm.setup do |config|
|
|||
# config.item_wrapper_class = nil
|
||||
|
||||
# How the label text should be generated altogether with the required text.
|
||||
config.label_text = lambda { |label, required, explicit_label| "#{label} #{required}" }
|
||||
config.label_text = lambda { |label, required, _explicit_label| "#{label} #{required}" }
|
||||
|
||||
# You can define the class to use on all labels. Default is nil.
|
||||
# config.label_class = nil
|
||||
|
|
|
@ -1 +1,14 @@
|
|||
---
|
||||
lt:
|
||||
activerecord:
|
||||
errors:
|
||||
models:
|
||||
account:
|
||||
attributes:
|
||||
username:
|
||||
invalid: turi būti tik raidės, skaičiai ir pabraukimai.
|
||||
reserved: užimtas.
|
||||
admin/webhook:
|
||||
attributes:
|
||||
url:
|
||||
invalid: nėra tinkamas URL adresas.
|
||||
|
|
|
@ -1600,8 +1600,8 @@ ko:
|
|||
windows: 윈도우
|
||||
windows_mobile: 윈도우 모바일
|
||||
windows_phone: 윈도우 폰
|
||||
revoke: 삭제
|
||||
revoke_success: 세션을 성공적으로 삭제하였습니다
|
||||
revoke: 취소
|
||||
revoke_success: 세션을 성공적으로 취소하였습니다
|
||||
title: 세션
|
||||
view_authentication_history: 내 계정에 대한 인증 이력 보기
|
||||
settings:
|
||||
|
|
|
@ -232,6 +232,12 @@ lt:
|
|||
unassign: Nepriskirti
|
||||
unresolved: Neišspręsti
|
||||
updated_at: Atnaujinti
|
||||
roles:
|
||||
everyone: Numatytieji leidimai
|
||||
everyone_full_description_html: Tai – <strong>bazinis vaidmuo</strong>, turintis įtakos <strong>visiems naudotojams</strong>, net ir tiems, kurie neturi priskirto vaidmens. Visi kiti vaidmenys iš jo paveldi teises.
|
||||
settings:
|
||||
domain_blocks:
|
||||
all: Visiems
|
||||
statuses:
|
||||
back_to_account: Atgal į paskyros puslapį
|
||||
media:
|
||||
|
@ -250,6 +256,10 @@ lt:
|
|||
body: "%{reporter} parašė skundą apie %{target}"
|
||||
body_remote: Kažkas iš %{domain} parašė skundą apie %{target}
|
||||
subject: Naujas skundas %{instance} (#%{id})
|
||||
appearance:
|
||||
localization:
|
||||
body: Mastodon verčia savanoriai.
|
||||
guide_link_text: Visi gali prisidėti.
|
||||
application_mailer:
|
||||
notification_preferences: Keisti el pašto parinktis
|
||||
settings: 'Keisti el pašto parinktis: %{link}'
|
||||
|
@ -458,9 +468,9 @@ lt:
|
|||
private: Tik sekėjams
|
||||
private_long: Rodyti tik sekėjams
|
||||
public: Viešas
|
||||
public_long: Matyti gali visi
|
||||
public_long: Visi gali matyti
|
||||
unlisted: Neįtrauktas į sąrašus
|
||||
unlisted_long: Matyti gali visi, tačiau nėra įtraukta į viešas laiko juostas
|
||||
unlisted_long: Matyti gali visi, tačiau nėra įtraukti į viešąsias laiko skales
|
||||
stream_entries:
|
||||
sensitive_content: Jautrus turinys
|
||||
themes:
|
||||
|
@ -507,4 +517,5 @@ lt:
|
|||
seamless_external_login: Jūs esate prisijungę per išorini įrenginį, todėl slaptąžodis ir el pašto nustatymai neprieinami.
|
||||
signed_in_as: 'Prisijungta kaip:'
|
||||
verification:
|
||||
hint_html: "<strong>Savo tapatybės patvirtinimas Mastodon skirtas visiems.</strong> Remiantis atviraisiais žiniatinklio standartais, dabar ir visam laikui nemokamas. Viskas, ko tau reikia, yra asmeninė svetainė, pagal kurią žmonės tave atpažįsta. Kai iš savo profilio pateiksi nuorodą į šią svetainę, patikrinsime, ar svetainėje yra nuoroda į tavo profilį, ir parodysime vizualinį indikatorių."
|
||||
verification: Patvirtinimas
|
||||
|
|
|
@ -1079,6 +1079,7 @@ ru:
|
|||
confirmations:
|
||||
awaiting_review: Ваш адрес электронной почты подтвержден! Сотрудники %{domain} проверяют вашу регистрацию. Вы получите письмо, если они подтвердят вашу учетную запись!
|
||||
awaiting_review_title: Ваша регистрация проверяется
|
||||
clicking_this_link: нажатие на эту ссылку
|
||||
login_link: войти
|
||||
proceed_to_login_html: Теперь вы можете перейти к %{login_link}.
|
||||
registration_complete: Ваша регистрация на %{domain} завершена!
|
||||
|
@ -1536,7 +1537,7 @@ ru:
|
|||
update:
|
||||
subject: "%{name} изменил(а) пост"
|
||||
notifications:
|
||||
administration_emails: E-mail уведомления администратора
|
||||
administration_emails: Уведомления администратора по электронной почте
|
||||
email_events: События для e-mail уведомлений
|
||||
email_events_hint: 'Выберите события, для которых вы хотели бы получать уведомления:'
|
||||
other_settings: Остальные настройки уведомлений
|
||||
|
@ -1631,6 +1632,7 @@ ru:
|
|||
over_total_limit: Вы превысили лимит на %{limit} запланированных постов
|
||||
too_soon: Запланированная дата должна быть в будущем
|
||||
self_destruct:
|
||||
lead_html: К сожалению, <strong>%{domain}</strong> закрывается навсегда. Если вас учётная запись находиться здесь вы не сможете продолжить использовать его, но вы можете запросить резервную копию ваших данных.
|
||||
title: Этот сервер закрывается
|
||||
sessions:
|
||||
activity: Последняя активность
|
||||
|
|
|
@ -1 +1,45 @@
|
|||
---
|
||||
lt:
|
||||
simple_form:
|
||||
hints:
|
||||
account:
|
||||
discoverable: Tavo vieši įrašai ir profilis gali būti rodomi arba rekomenduojami įvairiose Mastodon vietose, o profilis gali būti siūlomas kitiems naudotojams.
|
||||
display_name: Tavo pilnas vardas arba smagus vardas.
|
||||
fields: Tavo pagrindinis puslapis, įvardžiai, amžius, bet kas, ko tik nori.
|
||||
indexable: Tavo vieši įrašai gali būti rodomi Mastodon paieškos rezultatuose. Žmonės, kurie bendravo su tavo įrašais, gali jų ieškoti nepriklausomai nuo to.
|
||||
note: 'Gali @paminėti kitus žmones arba #saitažodžius.'
|
||||
show_collections: Žmonės galės peržiūrėti tavo sekimus ir sekėjus. Žmonės, kuriuos seki, matys, kad juos seki, nepaisant to.
|
||||
unlocked: Žmonės galės tave sekti nepaprašę patvirtinimo. Panaikink žymėjimą, jei nori peržiūrėti sekimo prašymus ir pasirinkti, ar priimti, ar atmesti naujus sekėjus.
|
||||
account_warning_preset:
|
||||
text: Gali naudoti įrašų sintaksę, pavyzdžiui, URL adresus, saitažodus ir paminėjimus
|
||||
defaults:
|
||||
header: PNG, GIF arba JPG. Ne daugiau kaip %{size}. Bus sumažintas iki %{dimensions}tšk.
|
||||
inbox_url: Nukopijuok URL adresą iš pradinio puslapio perdavėjo, kurį nori naudoti
|
||||
irreversible: Filtruoti įrašai išnyks negrįžtamai, net jei vėliau filtras bus pašalintas
|
||||
locale: Naudotojo sąsajos kalba, el. laiškai ir stumiamieji pranešimai
|
||||
password: Naudok bent 8 simbolius
|
||||
phrase: Bus suderinta, neatsižvelgiant į teksto korpusą arba įrašo turinio įspėjimą
|
||||
setting_display_media_hide_all: Visada slėpti žiniasklaidą
|
||||
setting_display_media_show_all: Visada rodyti žiniasklaidą
|
||||
setting_use_blurhash: Gradientai pagrįsti paslėptų vaizdų spalvomis, tačiau užgožia bet kokias detales
|
||||
setting_use_pending_items: Slėpti laiko skalės naujienas po paspaudimo, vietoj automatinio kanalo slinkimo
|
||||
featured_tag:
|
||||
name: 'Štai keletas pastaruoju metu dažniausiai saitažodžių, kurių tu naudojai:'
|
||||
form_admin_settings:
|
||||
peers_api_enabled: Domenų pavadinimų sąrašas, su kuriais šis serveris susidūrė fediverse. Čia nėra duomenų apie tai, ar tu bendrauji su tam tikru serveriu, tik apie tai, kad tavo serveris apie jį žino. Tai naudojama tarnybose, kurios renka federacijos statistiką bendrąja prasme.
|
||||
site_contact_email: Kaip žmonės gali su tavimi susisiekti teisiniais ar pagalbos užklausimais.
|
||||
site_contact_username: Kaip žmonės gali tave pasiekti Mastodon.
|
||||
site_extended_description: Bet kokia papildoma informacija, kuri gali būti naudinga lankytojams ir naudotojams. Gali būti struktūrizuota naudojant Markdown sintaksę.
|
||||
trends: Trendai rodo, kurios įrašai, saitažodžiai ir naujienų istorijos tavo serveryje sulaukia didžiausio susidomėjimo.
|
||||
sessions:
|
||||
webauthn: Jei tai USB raktas, būtinai jį įkišk ir, jei reikia, paliesk.
|
||||
settings:
|
||||
indexable: Tavo profilio puslapis gali būti rodomas paieškos rezultatuose Google, Bing ir kituose.
|
||||
labels:
|
||||
featured_tag:
|
||||
name: Saitažodis
|
||||
tag:
|
||||
listable: Leisti šį saitažodį rodyti paieškose ir pasiūlymuose
|
||||
name: Saitažodis
|
||||
trendable: Leisti šį saitažodį rodyti pagal trendus
|
||||
usable: Leisti įrašams naudoti šį saitažodį
|
||||
|
|
|
@ -336,6 +336,10 @@ namespace :api, format: false do
|
|||
resources :statuses, only: [:show, :destroy]
|
||||
end
|
||||
|
||||
namespace :accounts do
|
||||
resources :relationships, only: :index
|
||||
end
|
||||
|
||||
namespace :admin do
|
||||
resources :accounts, only: [:index]
|
||||
end
|
||||
|
|
|
@ -427,7 +427,11 @@ namespace :mastodon do
|
|||
from: env['SMTP_FROM_ADDRESS'],
|
||||
}
|
||||
|
||||
mail = ActionMailer::Base.new.mail to: send_to, subject: 'Test', body: 'Mastodon SMTP configuration works!'
|
||||
mail = ActionMailer::Base.new.mail(
|
||||
to: send_to,
|
||||
subject: 'Test', # rubocop:disable Rails/I18nLocaleTexts
|
||||
body: 'Mastodon SMTP configuration works!'
|
||||
)
|
||||
mail.deliver
|
||||
break
|
||||
rescue => e
|
||||
|
|
|
@ -213,7 +213,7 @@
|
|||
"husky": "^8.0.3",
|
||||
"jest": "^29.5.0",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"lint-staged": "^13.2.2",
|
||||
"lint-staged": "^15.0.0",
|
||||
"prettier": "^3.0.0",
|
||||
"react-test-renderer": "^18.2.0",
|
||||
"stylelint": "^15.10.1",
|
||||
|
|
|
@ -7,66 +7,44 @@ RSpec.describe AccountsController do
|
|||
|
||||
let(:account) { Fabricate(:account) }
|
||||
|
||||
shared_examples 'unapproved account check' do
|
||||
describe 'unapproved account check' do
|
||||
before { account.user.update(approved: false) }
|
||||
|
||||
it 'returns http not found' do
|
||||
get :show, params: { username: account.username, format: format }
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
%w(html json rss).each do |format|
|
||||
get :show, params: { username: account.username, format: format }
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'permanently suspended account check' do
|
||||
describe 'permanently suspended account check' do
|
||||
before do
|
||||
account.suspend!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
get :show, params: { username: account.username, format: format }
|
||||
|
||||
expect(response).to have_http_status(410)
|
||||
%w(html json rss).each do |format|
|
||||
get :show, params: { username: account.username, format: format }
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'temporarily suspended account check' do |code: 403|
|
||||
describe 'temporarily suspended account check' do
|
||||
before { account.suspend! }
|
||||
|
||||
it 'returns appropriate http response code' do
|
||||
get :show, params: { username: account.username, format: format }
|
||||
{ html: 403, json: 200, rss: 403 }.each do |format, code|
|
||||
get :show, params: { username: account.username, format: format }
|
||||
|
||||
expect(response).to have_http_status(code)
|
||||
expect(response).to have_http_status(code)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
context 'with basic account status checks' do
|
||||
context 'with HTML' do
|
||||
let(:format) { 'html' }
|
||||
|
||||
it_behaves_like 'unapproved account check'
|
||||
it_behaves_like 'permanently suspended account check'
|
||||
it_behaves_like 'temporarily suspended account check'
|
||||
end
|
||||
|
||||
context 'with JSON' do
|
||||
let(:format) { 'json' }
|
||||
|
||||
it_behaves_like 'unapproved account check'
|
||||
it_behaves_like 'permanently suspended account check'
|
||||
it_behaves_like 'temporarily suspended account check', code: 200
|
||||
end
|
||||
|
||||
context 'with RSS' do
|
||||
let(:format) { 'rss' }
|
||||
|
||||
it_behaves_like 'unapproved account check'
|
||||
it_behaves_like 'permanently suspended account check'
|
||||
it_behaves_like 'temporarily suspended account check'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with existing statuses' do
|
||||
let!(:status) { Fabricate(:status, account: account) }
|
||||
let!(:status_reply) { Fabricate(:status, account: account, thread: Fabricate(:status)) }
|
||||
|
@ -227,22 +205,15 @@ RSpec.describe AccountsController do
|
|||
context 'with RSS' do
|
||||
let(:format) { 'rss' }
|
||||
|
||||
shared_examples 'common RSS response' do
|
||||
it 'returns http success' do
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
|
||||
end
|
||||
|
||||
context 'with a normal account in an RSS request' do
|
||||
before do
|
||||
get :show, params: { username: account.username, format: format }
|
||||
end
|
||||
|
||||
it_behaves_like 'common RSS response'
|
||||
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
|
||||
|
||||
it 'responds with correct statuses', :aggregate_failures do
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response.body).to include_status_tag(status_media)
|
||||
expect(response.body).to include_status_tag(status_self_reply)
|
||||
expect(response.body).to include_status_tag(status)
|
||||
|
@ -259,9 +230,10 @@ RSpec.describe AccountsController do
|
|||
get :show, params: { username: account.username, format: format }
|
||||
end
|
||||
|
||||
it_behaves_like 'common RSS response'
|
||||
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
|
||||
|
||||
it 'responds with correct statuses with replies', :aggregate_failures do
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response.body).to include_status_tag(status_media)
|
||||
expect(response.body).to include_status_tag(status_reply)
|
||||
expect(response.body).to include_status_tag(status_self_reply)
|
||||
|
@ -278,9 +250,10 @@ RSpec.describe AccountsController do
|
|||
get :show, params: { username: account.username, format: format }
|
||||
end
|
||||
|
||||
it_behaves_like 'common RSS response'
|
||||
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
|
||||
|
||||
it 'responds with correct statuses with media', :aggregate_failures do
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response.body).to include_status_tag(status_media)
|
||||
expect(response.body).to_not include_status_tag(status_direct)
|
||||
expect(response.body).to_not include_status_tag(status_private)
|
||||
|
@ -302,9 +275,10 @@ RSpec.describe AccountsController do
|
|||
get :show, params: { username: account.username, format: format, tag: tag.to_param }
|
||||
end
|
||||
|
||||
it_behaves_like 'common RSS response'
|
||||
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
|
||||
|
||||
it 'responds with correct statuses with a tag', :aggregate_failures do
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response.body).to include_status_tag(status_tag)
|
||||
expect(response.body).to_not include_status_tag(status_direct)
|
||||
expect(response.body).to_not include_status_tag(status_media)
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe Api::V1::Accounts::RelationshipsController do
|
||||
render_views
|
||||
|
||||
let(:user) { Fabricate(:user) }
|
||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:follows') }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:doorkeeper_token) { token }
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
let(:simon) { Fabricate(:account) }
|
||||
let(:lewis) { Fabricate(:account) }
|
||||
|
||||
before do
|
||||
user.account.follow!(simon)
|
||||
lewis.follow!(user.account)
|
||||
end
|
||||
|
||||
context 'when provided only one ID' do
|
||||
before do
|
||||
get :index, params: { id: simon.id }
|
||||
end
|
||||
|
||||
it 'returns JSON with correct data', :aggregate_failures do
|
||||
json = body_as_json
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json).to be_a Enumerable
|
||||
expect(json.first[:following]).to be true
|
||||
expect(json.first[:followed_by]).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when provided multiple IDs' do
|
||||
before do
|
||||
get :index, params: { id: [simon.id, lewis.id] }
|
||||
end
|
||||
|
||||
it 'returns http success' do
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
context 'when there is returned JSON data' do
|
||||
let(:json) { body_as_json }
|
||||
|
||||
it 'returns an enumerable json with correct elements', :aggregate_failures do
|
||||
expect(json).to be_a Enumerable
|
||||
|
||||
expect_simon_item_one
|
||||
expect_lewis_item_two
|
||||
end
|
||||
|
||||
def expect_simon_item_one
|
||||
expect(json.first[:id]).to eq simon.id.to_s
|
||||
expect(json.first[:following]).to be true
|
||||
expect(json.first[:showing_reblogs]).to be true
|
||||
expect(json.first[:followed_by]).to be false
|
||||
expect(json.first[:muting]).to be false
|
||||
expect(json.first[:requested]).to be false
|
||||
expect(json.first[:domain_blocking]).to be false
|
||||
end
|
||||
|
||||
def expect_lewis_item_two
|
||||
expect(json.second[:id]).to eq lewis.id.to_s
|
||||
expect(json.second[:following]).to be false
|
||||
expect(json.second[:showing_reblogs]).to be false
|
||||
expect(json.second[:followed_by]).to be true
|
||||
expect(json.second[:muting]).to be false
|
||||
expect(json.second[:requested]).to be false
|
||||
expect(json.second[:domain_blocking]).to be false
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns JSON with correct data on cached requests too' do
|
||||
get :index, params: { id: [simon.id] }
|
||||
|
||||
json = body_as_json
|
||||
|
||||
expect(json).to be_a Enumerable
|
||||
expect(json.first[:following]).to be true
|
||||
expect(json.first[:showing_reblogs]).to be true
|
||||
end
|
||||
|
||||
it 'returns JSON with correct data after change too' do
|
||||
user.account.unfollow!(simon)
|
||||
|
||||
get :index, params: { id: [simon.id] }
|
||||
|
||||
json = body_as_json
|
||||
|
||||
expect(json).to be_a Enumerable
|
||||
expect(json.first[:following]).to be false
|
||||
expect(json.first[:showing_reblogs]).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,5 +2,5 @@
|
|||
|
||||
Fabricator(:session_activation) do
|
||||
user { Fabricate.build(:user) }
|
||||
session_id 'MyString'
|
||||
session_id { sequence(:session_id) { |i| "session_id_#{i}" } }
|
||||
end
|
||||
|
|
|
@ -296,5 +296,51 @@ describe ApplicationHelper do
|
|||
expect(helper.title).to eq 'site title'
|
||||
expect(Rails.env).to have_received(:production?)
|
||||
end
|
||||
|
||||
it 'returns site title with note on non-production environment' do
|
||||
Setting.site_title = 'site title'
|
||||
allow(Rails.env).to receive(:production?).and_return(false)
|
||||
expect(helper.title).to eq 'site title (Dev)'
|
||||
expect(Rails.env).to have_received(:production?)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'html_title' do
|
||||
before do
|
||||
allow(Rails.env).to receive(:production?).and_return(true)
|
||||
end
|
||||
|
||||
around do |example|
|
||||
site_title = Setting.site_title
|
||||
example.run
|
||||
Setting.site_title = site_title
|
||||
end
|
||||
|
||||
context 'with a page_title content_for value' do
|
||||
it 'uses the value in the html title' do
|
||||
Setting.site_title = 'Site Title'
|
||||
helper.content_for(:page_title, 'Test Value')
|
||||
|
||||
expect(helper.html_title).to eq 'Test Value - Site Title'
|
||||
expect(helper.html_title).to be_html_safe
|
||||
end
|
||||
|
||||
it 'removes extra new lines' do
|
||||
Setting.site_title = 'Site Title'
|
||||
helper.content_for(:page_title, "Test Value\n")
|
||||
|
||||
expect(helper.html_title).to eq 'Test Value - Site Title'
|
||||
expect(helper.html_title).to be_html_safe
|
||||
end
|
||||
end
|
||||
|
||||
context 'without any page_title content_for value' do
|
||||
it 'returns the site title' do
|
||||
Setting.site_title = 'Site Title'
|
||||
|
||||
expect(helper.html_title).to eq 'Site Title'
|
||||
expect(helper.html_title).to be_html_safe
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,16 +4,23 @@ require 'rails_helper'
|
|||
|
||||
describe FlashesHelper do
|
||||
describe 'user_facing_flashes' do
|
||||
it 'returns user facing flashes' do
|
||||
before do
|
||||
# rubocop:disable Rails/I18nLocaleTexts
|
||||
flash[:alert] = 'an alert'
|
||||
flash[:error] = 'an error'
|
||||
flash[:notice] = 'a notice'
|
||||
flash[:success] = 'a success'
|
||||
flash[:not_user_facing] = 'a not user facing flash'
|
||||
expect(helper.user_facing_flashes).to eq 'alert' => 'an alert',
|
||||
'error' => 'an error',
|
||||
'notice' => 'a notice',
|
||||
'success' => 'a success'
|
||||
# rubocop:enable Rails/I18nLocaleTexts
|
||||
end
|
||||
|
||||
it 'returns user facing flashes' do
|
||||
expect(helper.user_facing_flashes).to eq(
|
||||
'alert' => 'an alert',
|
||||
'error' => 'an error',
|
||||
'notice' => 'a notice',
|
||||
'success' => 'a success'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -69,7 +69,9 @@ RSpec.describe Remotable do
|
|||
|
||||
context 'with an invalid URL' do
|
||||
before do
|
||||
allow(Addressable::URI).to receive_message_chain(:parse, :normalize).with(url).with(no_args).and_raise(Addressable::URI::InvalidURIError)
|
||||
parsed = instance_double(Addressable::URI)
|
||||
allow(parsed).to receive(:normalize).with(no_args).and_raise(Addressable::URI::InvalidURIError)
|
||||
allow(Addressable::URI).to receive(:parse).with(url).and_return(parsed)
|
||||
end
|
||||
|
||||
it 'makes no request' do
|
||||
|
|
|
@ -98,34 +98,44 @@ RSpec.describe SessionActivation do
|
|||
end
|
||||
|
||||
context 'when id exists' do
|
||||
let(:id) { '1' }
|
||||
let!(:session_activation) { Fabricate(:session_activation) }
|
||||
|
||||
it 'calls where.destroy_all' do
|
||||
expect(described_class).to receive_message_chain(:where, :destroy_all)
|
||||
.with(session_id: id).with(no_args)
|
||||
it 'destroys the record' do
|
||||
described_class.deactivate(session_activation.session_id)
|
||||
|
||||
described_class.deactivate(id)
|
||||
expect { session_activation.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.purge_old' do
|
||||
it 'calls order.offset.destroy_all' do
|
||||
expect(described_class).to receive_message_chain(:order, :offset, :destroy_all)
|
||||
.with('created_at desc').with(Rails.configuration.x.max_session_activations).with(no_args)
|
||||
around do |example|
|
||||
before = Rails.configuration.x.max_session_activations
|
||||
Rails.configuration.x.max_session_activations = 1
|
||||
example.run
|
||||
Rails.configuration.x.max_session_activations = before
|
||||
end
|
||||
|
||||
let!(:oldest_session_activation) { Fabricate(:session_activation, created_at: 10.days.ago) }
|
||||
let!(:newest_session_activation) { Fabricate(:session_activation, created_at: 5.days.ago) }
|
||||
|
||||
it 'preserves the newest X records based on config' do
|
||||
described_class.purge_old
|
||||
|
||||
expect { oldest_session_activation.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { newest_session_activation.reload }.to_not raise_error
|
||||
end
|
||||
end
|
||||
|
||||
describe '.exclusive' do
|
||||
let(:id) { '1' }
|
||||
let!(:unwanted_session_activation) { Fabricate(:session_activation) }
|
||||
let!(:wanted_session_activation) { Fabricate(:session_activation) }
|
||||
|
||||
it 'calls where.destroy_all' do
|
||||
expect(described_class).to receive_message_chain(:where, :not, :destroy_all)
|
||||
.with(session_id: id).with(no_args)
|
||||
it 'preserves supplied record and destroys all others' do
|
||||
described_class.exclusive(wanted_session_activation.session_id)
|
||||
|
||||
described_class.exclusive(id)
|
||||
expect { unwanted_session_activation.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { wanted_session_activation.reload }.to_not raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -77,10 +77,13 @@ RSpec.describe Setting do
|
|||
let(:default_value) { { default_value: 'default_value' } }
|
||||
|
||||
it 'calls default_value.with_indifferent_access.merge!' do
|
||||
expect(default_value).to receive_message_chain(:with_indifferent_access, :merge!)
|
||||
.with(db_val.value)
|
||||
indifferent_hash = instance_double(Hash, merge!: nil)
|
||||
allow(default_value).to receive(:with_indifferent_access).and_return(indifferent_hash)
|
||||
|
||||
described_class[key]
|
||||
|
||||
expect(default_value).to have_received(:with_indifferent_access)
|
||||
expect(indifferent_hash).to have_received(:merge!).with(db_val.value)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
133
spec/requests/api/v1/accounts/relationships_spec.rb
Normal file
133
spec/requests/api/v1/accounts/relationships_spec.rb
Normal file
|
@ -0,0 +1,133 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe 'GET /api/v1/accounts/relationships' do
|
||||
subject do
|
||||
get '/api/v1/accounts/relationships', headers: headers, params: params
|
||||
end
|
||||
|
||||
let(:user) { Fabricate(:user) }
|
||||
let(:scopes) { 'read:follows' }
|
||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
|
||||
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
|
||||
|
||||
let(:simon) { Fabricate(:account) }
|
||||
let(:lewis) { Fabricate(:account) }
|
||||
let(:bob) { Fabricate(:account, suspended: true) }
|
||||
|
||||
before do
|
||||
user.account.follow!(simon)
|
||||
lewis.follow!(user.account)
|
||||
end
|
||||
|
||||
context 'when provided only one ID' do
|
||||
let(:params) { { id: simon.id } }
|
||||
|
||||
it 'returns JSON with correct data', :aggregate_failures do
|
||||
subject
|
||||
|
||||
json = body_as_json
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json).to be_a Enumerable
|
||||
expect(json.first[:following]).to be true
|
||||
expect(json.first[:followed_by]).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when provided multiple IDs' do
|
||||
let(:params) { { id: [simon.id, lewis.id, bob.id] } }
|
||||
|
||||
context 'when there is returned JSON data' do
|
||||
let(:json) { body_as_json }
|
||||
|
||||
context 'with default parameters' do
|
||||
it 'returns an enumerable json with correct elements, excluding suspended accounts', :aggregate_failures do
|
||||
subject
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json).to be_a Enumerable
|
||||
expect(json.size).to eq 2
|
||||
|
||||
expect_simon_item_one
|
||||
expect_lewis_item_two
|
||||
end
|
||||
end
|
||||
|
||||
context 'with `with_suspended` parameter' do
|
||||
let(:params) { { id: [simon.id, lewis.id, bob.id], with_suspended: true } }
|
||||
|
||||
it 'returns an enumerable json with correct elements, including suspended accounts', :aggregate_failures do
|
||||
subject
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json).to be_a Enumerable
|
||||
expect(json.size).to eq 3
|
||||
|
||||
expect_simon_item_one
|
||||
expect_lewis_item_two
|
||||
expect_bob_item_three
|
||||
end
|
||||
end
|
||||
|
||||
def expect_simon_item_one
|
||||
expect(json.first[:id]).to eq simon.id.to_s
|
||||
expect(json.first[:following]).to be true
|
||||
expect(json.first[:showing_reblogs]).to be true
|
||||
expect(json.first[:followed_by]).to be false
|
||||
expect(json.first[:muting]).to be false
|
||||
expect(json.first[:requested]).to be false
|
||||
expect(json.first[:domain_blocking]).to be false
|
||||
end
|
||||
|
||||
def expect_lewis_item_two
|
||||
expect(json.second[:id]).to eq lewis.id.to_s
|
||||
expect(json.second[:following]).to be false
|
||||
expect(json.second[:showing_reblogs]).to be false
|
||||
expect(json.second[:followed_by]).to be true
|
||||
expect(json.second[:muting]).to be false
|
||||
expect(json.second[:requested]).to be false
|
||||
expect(json.second[:domain_blocking]).to be false
|
||||
end
|
||||
|
||||
def expect_bob_item_three
|
||||
expect(json.third[:id]).to eq bob.id.to_s
|
||||
expect(json.third[:following]).to be false
|
||||
expect(json.third[:showing_reblogs]).to be false
|
||||
expect(json.third[:followed_by]).to be false
|
||||
expect(json.third[:muting]).to be false
|
||||
expect(json.third[:requested]).to be false
|
||||
expect(json.third[:domain_blocking]).to be false
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns JSON with correct data on cached requests too' do
|
||||
subject
|
||||
subject
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
|
||||
json = body_as_json
|
||||
|
||||
expect(json).to be_a Enumerable
|
||||
expect(json.first[:following]).to be true
|
||||
expect(json.first[:showing_reblogs]).to be true
|
||||
end
|
||||
|
||||
it 'returns JSON with correct data after change too' do
|
||||
subject
|
||||
user.account.unfollow!(simon)
|
||||
|
||||
get '/api/v1/accounts/relationships', headers: headers, params: { id: [simon.id] }
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
|
||||
json = body_as_json
|
||||
|
||||
expect(json).to be_a Enumerable
|
||||
expect(json.first[:following]).to be false
|
||||
expect(json.first[:showing_reblogs]).to be false
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue