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

This commit is contained in:
Claire 2023-12-18 20:47:27 +01:00
commit a111fd7a0b
94 changed files with 699 additions and 560 deletions

View File

@ -43,23 +43,6 @@ Metrics/CyclomaticComplexity:
Metrics/PerceivedComplexity: Metrics/PerceivedComplexity:
Max: 27 Max: 27
RSpec/AnyInstance:
Exclude:
- 'spec/controllers/activitypub/inboxes_controller_spec.rb'
- 'spec/controllers/admin/accounts_controller_spec.rb'
- 'spec/controllers/admin/resets_controller_spec.rb'
- 'spec/controllers/auth/sessions_controller_spec.rb'
- 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb'
- 'spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb'
- 'spec/lib/request_spec.rb'
- 'spec/lib/status_filter_spec.rb'
- 'spec/models/account_spec.rb'
- 'spec/models/setting_spec.rb'
- 'spec/services/activitypub/process_collection_service_spec.rb'
- 'spec/validators/follow_limit_validator_spec.rb'
- 'spec/workers/activitypub/delivery_worker_spec.rb'
- 'spec/workers/web/push_notification_worker_spec.rb'
# Configuration parameters: CountAsOne. # Configuration parameters: CountAsOne.
RSpec/ExampleLength: RSpec/ExampleLength:
Max: 22 Max: 22

View File

@ -7,6 +7,7 @@ class Api::BaseController < ApplicationController
include RateLimitHeaders include RateLimitHeaders
include AccessTokenTrackingConcern include AccessTokenTrackingConcern
include ApiCachingConcern include ApiCachingConcern
include Api::ContentSecurityPolicy
skip_before_action :require_functional!, unless: :limited_federation_mode? skip_before_action :require_functional!, unless: :limited_federation_mode?
@ -17,26 +18,6 @@ class Api::BaseController < ApplicationController
protect_from_forgery with: :null_session protect_from_forgery with: :null_session
content_security_policy do |p|
# Set every directive that does not have a fallback
p.default_src :none
p.frame_ancestors :none
p.form_action :none
# Disable every directive with a fallback to cut on response size
p.base_uri false
p.font_src false
p.img_src false
p.style_src false
p.media_src false
p.frame_src false
p.manifest_src false
p.connect_src false
p.script_src false
p.child_src false
p.worker_src false
end
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e| rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
render json: { error: e.to_s }, status: 422 render json: { error: e.to_s }, status: 422
end end

View File

@ -13,7 +13,7 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr
cache_if_unauthenticated! cache_if_unauthenticated!
end end
render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: (Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?)) render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: show_rationale_in_response?
end end
private private
@ -25,4 +25,16 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr
def set_domain_blocks def set_domain_blocks
@domain_blocks = DomainBlock.with_user_facing_limitations.by_severity @domain_blocks = DomainBlock.with_user_facing_limitations.by_severity
end end
def show_rationale_in_response?
always_show_rationale? || show_rationale_for_user?
end
def always_show_rationale?
Setting.show_domain_blocks_rationale == 'all'
end
def show_rationale_for_user?
Setting.show_domain_blocks_rationale == 'users' && user_signed_in?
end
end end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class Api::V1::Statuses::BaseController < Api::BaseController
include Authorization
before_action :set_status
private
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
end

View File

@ -1,11 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Statuses::BookmarksController < Api::BaseController class Api::V1::Statuses::BookmarksController < Api::V1::Statuses::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :write, :'write:bookmarks' } before_action -> { doorkeeper_authorize! :write, :'write:bookmarks' }
before_action :require_user! before_action :require_user!
before_action :set_status, only: [:create] skip_before_action :set_status, only: [:destroy]
def create def create
current_account.bookmarks.find_or_create_by!(account: current_account, status: @status) current_account.bookmarks.find_or_create_by!(account: current_account, status: @status)
@ -28,13 +26,4 @@ class Api::V1::Statuses::BookmarksController < Api::BaseController
rescue Mastodon::NotPermittedError rescue Mastodon::NotPermittedError
not_found not_found
end end
private
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
end end

View File

@ -1,10 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController class Api::V1::Statuses::FavouritedByAccountsController < Api::V1::Statuses::BaseController
include Authorization
before_action -> { authorize_if_got_token! :read, :'read:accounts' } before_action -> { authorize_if_got_token! :read, :'read:accounts' }
before_action :set_status
after_action :insert_pagination_headers after_action :insert_pagination_headers
def index def index
@ -61,13 +58,6 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end end
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
def pagination_params(core_params) def pagination_params(core_params)
params.slice(:limit).permit(:limit).merge(core_params) params.slice(:limit).permit(:limit).merge(core_params)
end end

View File

@ -1,11 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Statuses::FavouritesController < Api::BaseController class Api::V1::Statuses::FavouritesController < Api::V1::Statuses::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :write, :'write:favourites' } before_action -> { doorkeeper_authorize! :write, :'write:favourites' }
before_action :require_user! before_action :require_user!
before_action :set_status, only: [:create] skip_before_action :set_status, only: [:destroy]
def create def create
FavouriteService.new.call(current_account, @status) FavouriteService.new.call(current_account, @status)
@ -30,13 +28,4 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
rescue Mastodon::NotPermittedError rescue Mastodon::NotPermittedError
not_found not_found
end end
private
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
end end

View File

@ -1,10 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Statuses::HistoriesController < Api::BaseController class Api::V1::Statuses::HistoriesController < Api::V1::Statuses::BaseController
include Authorization
before_action -> { authorize_if_got_token! :read, :'read:statuses' } before_action -> { authorize_if_got_token! :read, :'read:statuses' }
before_action :set_status
def show def show
cache_if_unauthenticated! cache_if_unauthenticated!
@ -16,11 +13,4 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController
def status_edits def status_edits
@status.edits.includes(:account, status: [:account]).to_a.presence || [@status.build_snapshot(at_time: @status.edited_at || @status.created_at)] @status.edits.includes(:account, status: [:account]).to_a.presence || [@status.build_snapshot(at_time: @status.edited_at || @status.created_at)]
end end
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
end end

View File

@ -1,11 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Statuses::MutesController < Api::BaseController class Api::V1::Statuses::MutesController < Api::V1::Statuses::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :write, :'write:mutes' } before_action -> { doorkeeper_authorize! :write, :'write:mutes' }
before_action :require_user! before_action :require_user!
before_action :set_status
before_action :set_conversation before_action :set_conversation
def create def create
@ -24,13 +21,6 @@ class Api::V1::Statuses::MutesController < Api::BaseController
private private
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
def set_conversation def set_conversation
@conversation = @status.conversation @conversation = @status.conversation
raise Mastodon::ValidationError if @conversation.nil? raise Mastodon::ValidationError if @conversation.nil?

View File

@ -1,11 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Statuses::PinsController < Api::BaseController class Api::V1::Statuses::PinsController < Api::V1::Statuses::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :write, :'write:accounts' } before_action -> { doorkeeper_authorize! :write, :'write:accounts' }
before_action :require_user! before_action :require_user!
before_action :set_status
def create def create
StatusPin.create!(account: current_account, status: @status) StatusPin.create!(account: current_account, status: @status)
@ -26,10 +23,6 @@ class Api::V1::Statuses::PinsController < Api::BaseController
private private
def set_status
@status = Status.find(params[:status_id])
end
def distribute_add_activity! def distribute_add_activity!
json = ActiveModelSerializers::SerializableResource.new( json = ActiveModelSerializers::SerializableResource.new(
@status, @status,

View File

@ -1,10 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController class Api::V1::Statuses::RebloggedByAccountsController < Api::V1::Statuses::BaseController
include Authorization
before_action -> { authorize_if_got_token! :read, :'read:accounts' } before_action -> { authorize_if_got_token! :read, :'read:accounts' }
before_action :set_status
after_action :insert_pagination_headers after_action :insert_pagination_headers
def index def index
@ -57,13 +54,6 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end end
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
def pagination_params(core_params) def pagination_params(core_params)
params.slice(:limit).permit(:limit).merge(core_params) params.slice(:limit).permit(:limit).merge(core_params)
end end

View File

@ -1,13 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Statuses::ReblogsController < Api::BaseController class Api::V1::Statuses::ReblogsController < Api::V1::Statuses::BaseController
include Authorization
include Redisable include Redisable
include Lockable include Lockable
before_action -> { doorkeeper_authorize! :write, :'write:statuses' } before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
before_action :require_user! before_action :require_user!
before_action :set_reblog, only: [:create] before_action :set_reblog, only: [:create]
skip_before_action :set_status
override_rate_limit_headers :create, family: :statuses override_rate_limit_headers :create, family: :statuses

View File

@ -1,21 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Statuses::SourcesController < Api::BaseController class Api::V1::Statuses::SourcesController < Api::V1::Statuses::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :read, :'read:statuses' } before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
before_action :set_status
def show def show
render json: @status, serializer: REST::StatusSourceSerializer render json: @status, serializer: REST::StatusSourceSerializer
end end
private
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
end end

View File

@ -1,10 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Statuses::TranslationsController < Api::BaseController class Api::V1::Statuses::TranslationsController < Api::V1::Statuses::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :read, :'read:statuses' } before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
before_action :set_status
before_action :set_translation before_action :set_translation
rescue_from TranslationService::NotConfiguredError, with: :not_found rescue_from TranslationService::NotConfiguredError, with: :not_found
@ -24,13 +21,6 @@ class Api::V1::Statuses::TranslationsController < Api::BaseController
private private
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
def set_translation def set_translation
@translation = TranslateStatusService.new.call(@status, content_locale) @translation = TranslateStatusService.new.call(@status, content_locale)
end end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
module Api::ContentSecurityPolicy
extend ActiveSupport::Concern
included do
content_security_policy do |policy|
# Set every directive that does not have a fallback
policy.default_src :none
policy.frame_ancestors :none
policy.form_action :none
# Disable every directive with a fallback to cut on response size
policy.base_uri false
policy.font_src false
policy.img_src false
policy.style_src false
policy.media_src false
policy.frame_src false
policy.manifest_src false
policy.connect_src false
policy.script_src false
policy.child_src false
policy.worker_src false
end
end
end

View File

@ -19,6 +19,8 @@ import { ReactComponent as StarIcon } from '@material-symbols/svg-600/outlined/s
import { ReactComponent as StarBorderIcon } from '@material-symbols/svg-600/outlined/star.svg'; import { ReactComponent as StarBorderIcon } from '@material-symbols/svg-600/outlined/star.svg';
import { ReactComponent as VisibilityIcon } from '@material-symbols/svg-600/outlined/visibility.svg'; import { ReactComponent as VisibilityIcon } from '@material-symbols/svg-600/outlined/visibility.svg';
import { ReactComponent as RepeatDisabledIcon } from 'mastodon/../svg-icons/repeat_disabled.svg';
import { ReactComponent as RepeatPrivateIcon } from 'mastodon/../svg-icons/repeat_private.svg';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import { WithRouterPropTypes } from 'mastodon/utils/react_router'; import { WithRouterPropTypes } from 'mastodon/utils/react_router';
@ -348,6 +350,7 @@ class StatusActionBar extends ImmutablePureComponent {
let replyIcon; let replyIcon;
let replyIconComponent; let replyIconComponent;
let replyTitle; let replyTitle;
if (status.get('in_reply_to_id', null) === null) { if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'reply'; replyIcon = 'reply';
replyIconComponent = ReplyIcon; replyIconComponent = ReplyIcon;
@ -360,15 +363,20 @@ class StatusActionBar extends ImmutablePureComponent {
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private'; const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
let reblogTitle = ''; let reblogTitle, reblogIconComponent;
if (status.get('reblogged')) { if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private); reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon;
} else if (publicStatus) { } else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog); reblogTitle = intl.formatMessage(messages.reblog);
reblogIconComponent = RepeatIcon;
} else if (reblogPrivate) { } else if (reblogPrivate) {
reblogTitle = intl.formatMessage(messages.reblog_private); reblogTitle = intl.formatMessage(messages.reblog_private);
reblogIconComponent = RepeatPrivateIcon;
} else { } else {
reblogTitle = intl.formatMessage(messages.cannot_reblog); reblogTitle = intl.formatMessage(messages.cannot_reblog);
reblogIconComponent = RepeatDisabledIcon;
} }
const filterButton = this.props.onFilter && ( const filterButton = this.props.onFilter && (
@ -380,7 +388,7 @@ class StatusActionBar extends ImmutablePureComponent {
return ( return (
<div className='status__action-bar'> <div className='status__action-bar'>
<IconButton className='status__action-bar__button' title={replyTitle} icon={isReply ? 'reply' : replyIcon} iconComponent={isReply ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} counter={status.get('replies_count')} /> <IconButton className='status__action-bar__button' title={replyTitle} icon={isReply ? 'reply' : replyIcon} iconComponent={isReply ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} counter={status.get('replies_count')} />
<IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={RepeatIcon} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} /> <IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} /> <IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} /> <IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />

View File

@ -204,7 +204,7 @@ class ListTimeline extends PureComponent {
</div> </div>
<div className='setting-toggle'> <div className='setting-toggle'>
<Toggle id={`list-${id}-exclusive`} defaultChecked={isExclusive} onChange={this.onExclusiveToggle} /> <Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} />
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'> <label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' /> <FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
</label> </label>

View File

@ -18,6 +18,8 @@ import { ReactComponent as ReplyAllIcon } from '@material-symbols/svg-600/outlin
import { ReactComponent as StarIcon } from '@material-symbols/svg-600/outlined/star-fill.svg'; import { ReactComponent as StarIcon } from '@material-symbols/svg-600/outlined/star-fill.svg';
import { ReactComponent as StarBorderIcon } from '@material-symbols/svg-600/outlined/star.svg'; import { ReactComponent as StarBorderIcon } from '@material-symbols/svg-600/outlined/star.svg';
import { ReactComponent as RepeatDisabledIcon } from 'mastodon/../svg-icons/repeat_disabled.svg';
import { ReactComponent as RepeatPrivateIcon } from 'mastodon/../svg-icons/repeat_private.svg';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import { WithRouterPropTypes } from 'mastodon/utils/react_router'; import { WithRouterPropTypes } from 'mastodon/utils/react_router';
@ -280,6 +282,7 @@ class ActionBar extends PureComponent {
let replyIcon; let replyIcon;
let replyIconComponent; let replyIconComponent;
if (status.get('in_reply_to_id', null) === null) { if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'reply'; replyIcon = 'reply';
replyIconComponent = ReplyIcon; replyIconComponent = ReplyIcon;
@ -290,21 +293,26 @@ class ActionBar extends PureComponent {
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private'; const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
let reblogTitle; let reblogTitle, reblogIconComponent;
if (status.get('reblogged')) { if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private); reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon;
} else if (publicStatus) { } else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog); reblogTitle = intl.formatMessage(messages.reblog);
reblogIconComponent = RepeatIcon;
} else if (reblogPrivate) { } else if (reblogPrivate) {
reblogTitle = intl.formatMessage(messages.reblog_private); reblogTitle = intl.formatMessage(messages.reblog_private);
reblogIconComponent = RepeatPrivateIcon;
} else { } else {
reblogTitle = intl.formatMessage(messages.cannot_reblog); reblogTitle = intl.formatMessage(messages.cannot_reblog);
reblogIconComponent = RepeatDisabledIcon;
} }
return ( return (
<div className='detailed-status__action-bar'> <div className='detailed-status__action-bar'>
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} iconComponent={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} /></div> <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} iconComponent={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={RepeatIcon} onClick={this.handleReblogClick} /></div> <div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} /></div> <div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} /></div>
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} /></div> <div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} /></div>

View File

@ -14,6 +14,7 @@
"account.badges.group": "Groep", "account.badges.group": "Groep",
"account.block": "Blokkeer @{name}", "account.block": "Blokkeer @{name}",
"account.block_domain": "Blokkeer domein {domain}", "account.block_domain": "Blokkeer domein {domain}",
"account.block_short": "Blokkeer",
"account.blocked": "Geblokkeer", "account.blocked": "Geblokkeer",
"account.browse_more_on_origin_server": "Verken die oorspronklike profiel", "account.browse_more_on_origin_server": "Verken die oorspronklike profiel",
"account.cancel_follow_request": "Herroep volgversoek", "account.cancel_follow_request": "Herroep volgversoek",
@ -45,6 +46,7 @@
"account.posts_with_replies": "Plasings en antwoorde", "account.posts_with_replies": "Plasings en antwoorde",
"account.report": "Rapporteer @{name}", "account.report": "Rapporteer @{name}",
"account.requested": "Wag op goedkeuring. Klik om volgversoek te kanselleer", "account.requested": "Wag op goedkeuring. Klik om volgversoek te kanselleer",
"account.requested_follow": "{name} het versoek om jou te volg",
"account.share": "Deel @{name} se profiel", "account.share": "Deel @{name} se profiel",
"account.show_reblogs": "Wys aangestuurde plasings van @{name}", "account.show_reblogs": "Wys aangestuurde plasings van @{name}",
"account.statuses_counter": "{count, plural, one {{counter} Plaas} other {{counter} Plasings}}", "account.statuses_counter": "{count, plural, one {{counter} Plaas} other {{counter} Plasings}}",
@ -82,6 +84,7 @@
"column.community": "Plaaslike tydlyn", "column.community": "Plaaslike tydlyn",
"column.directory": "Blaai deur profiele", "column.directory": "Blaai deur profiele",
"column.domain_blocks": "Geblokkeerde domeine", "column.domain_blocks": "Geblokkeerde domeine",
"column.favourites": "Gunstelinge",
"column.follow_requests": "Volgversoeke", "column.follow_requests": "Volgversoeke",
"column.home": "Tuis", "column.home": "Tuis",
"column.lists": "Lyste", "column.lists": "Lyste",
@ -271,6 +274,7 @@
"privacy.unlisted.short": "Ongelys", "privacy.unlisted.short": "Ongelys",
"privacy_policy.last_updated": "Laaste bywerking op {date}", "privacy_policy.last_updated": "Laaste bywerking op {date}",
"privacy_policy.title": "Privaatheidsbeleid", "privacy_policy.title": "Privaatheidsbeleid",
"regeneration_indicator.sublabel": "Jou tuis-voer word voorberei!",
"reply_indicator.cancel": "Kanselleer", "reply_indicator.cancel": "Kanselleer",
"report.placeholder": "Type or paste additional comments", "report.placeholder": "Type or paste additional comments",
"report.submit": "Submit report", "report.submit": "Submit report",

View File

@ -201,7 +201,7 @@
"disabled_account_banner.text": "Ваш уліковы запіс {disabledAccount} часова адключаны.", "disabled_account_banner.text": "Ваш уліковы запіс {disabledAccount} часова адключаны.",
"dismissable_banner.community_timeline": "Гэта самыя апошнія допісы ад людзей, уліковыя запісы якіх размяшчаюцца на {domain}.", "dismissable_banner.community_timeline": "Гэта самыя апошнія допісы ад людзей, уліковыя запісы якіх размяшчаюцца на {domain}.",
"dismissable_banner.dismiss": "Адхіліць", "dismissable_banner.dismiss": "Адхіліць",
"dismissable_banner.explore_links": "Гэтыя навіны абмяркоўваюцца прама зараз на гэтым і іншых серверах дэцэнтралізаванай сеткі.", "dismissable_banner.explore_links": "Гэтыя навіны абмяркоўваюцца цяпер на гэтым і іншых серверах дэцэнтралізаванай сеткі.",
"dismissable_banner.explore_statuses": "Допісы з гэтага і іншых сервераў дэцэнтралізаванай сеткі, якія набіраюць папулярнасць прама зараз.", "dismissable_banner.explore_statuses": "Допісы з гэтага і іншых сервераў дэцэнтралізаванай сеткі, якія набіраюць папулярнасць прама зараз.",
"dismissable_banner.explore_tags": "Гэтыя хэштэгі зараз набіраюць папулярнасць сярод людзей на гэтым і іншых серверах дэцэнтралізаванай сеткі", "dismissable_banner.explore_tags": "Гэтыя хэштэгі зараз набіраюць папулярнасць сярод людзей на гэтым і іншых серверах дэцэнтралізаванай сеткі",
"dismissable_banner.public_timeline": "Гэта апошнія публічныя допісы людзей з усей сеткі, за якімі сочаць карыстальнікі {domain}.", "dismissable_banner.public_timeline": "Гэта апошнія публічныя допісы людзей з усей сеткі, за якімі сочаць карыстальнікі {domain}.",
@ -482,7 +482,7 @@
"onboarding.share.lead": "Дайце людзям ведаць, як яны могуць знайсці вас на Mastodon!", "onboarding.share.lead": "Дайце людзям ведаць, як яны могуць знайсці вас на Mastodon!",
"onboarding.share.message": "Я {username} на #Mastodon! Сачыце за мной на {url}", "onboarding.share.message": "Я {username} на #Mastodon! Сачыце за мной на {url}",
"onboarding.share.next_steps": "Магчымыя наступныя крокі:", "onboarding.share.next_steps": "Магчымыя наступныя крокі:",
"onboarding.share.title": "Падзяліцеся сваім профілем", "onboarding.share.title": "Абагульце свой профіль",
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:", "onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
"onboarding.start.skip": "Want to skip right ahead?", "onboarding.start.skip": "Want to skip right ahead?",
"onboarding.start.title": "Вы зрабілі гэта!", "onboarding.start.title": "Вы зрабілі гэта!",
@ -493,7 +493,7 @@
"onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.", "onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
"onboarding.steps.setup_profile.title": "Customize your profile", "onboarding.steps.setup_profile.title": "Customize your profile",
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!", "onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
"onboarding.steps.share_profile.title": "Share your profile", "onboarding.steps.share_profile.title": "Абагульць ваш профіль у Mastodon",
"onboarding.tips.2fa": "<strong>Ці вы ведаеце?</strong> Вы можаце абараніць свой уліковы запіс, усталяваўшы двухфактарную аўтэнтыфікацыю ў наладах уліковага запісу. Яна працуе з любой праграмай TOTP на ваш выбар, нумар тэлефона не патрэбны!", "onboarding.tips.2fa": "<strong>Ці вы ведаеце?</strong> Вы можаце абараніць свой уліковы запіс, усталяваўшы двухфактарную аўтэнтыфікацыю ў наладах уліковага запісу. Яна працуе з любой праграмай TOTP на ваш выбар, нумар тэлефона не патрэбны!",
"onboarding.tips.accounts_from_other_servers": "<strong>Ці вы ведаеце?</strong> Паколькі Mastodon дэцэнтралізаваны, некаторыя профілі, якія вам трапляюцца, будуць размяшчацца на іншых серверах, адрозных ад вашага. І ўсё ж вы можаце бесперашкодна ўзаемадзейнічаць з імі! Іх сервер пазначаны ў другой палове імя карыстальніка!", "onboarding.tips.accounts_from_other_servers": "<strong>Ці вы ведаеце?</strong> Паколькі Mastodon дэцэнтралізаваны, некаторыя профілі, якія вам трапляюцца, будуць размяшчацца на іншых серверах, адрозных ад вашага. І ўсё ж вы можаце бесперашкодна ўзаемадзейнічаць з імі! Іх сервер пазначаны ў другой палове імя карыстальніка!",
"onboarding.tips.migration": "<strong>Ці вы ведаеце?</strong> Калі вы адчуваеце, што {domain} не з'яўляецца для вас лепшым выбарам у будучыні, вы можаце перайсці на іншы сервер Mastodon, не губляючы сваіх падпісчыкаў. Вы нават можаце стварыць свой уласны сервер!", "onboarding.tips.migration": "<strong>Ці вы ведаеце?</strong> Калі вы адчуваеце, што {domain} не з'яўляецца для вас лепшым выбарам у будучыні, вы можаце перайсці на іншы сервер Mastodon, не губляючы сваіх падпісчыкаў. Вы нават можаце стварыць свой уласны сервер!",

View File

@ -222,6 +222,7 @@
"emoji_button.search_results": "Resultats de la cerca", "emoji_button.search_results": "Resultats de la cerca",
"emoji_button.symbols": "Símbols", "emoji_button.symbols": "Símbols",
"emoji_button.travel": "Viatges i llocs", "emoji_button.travel": "Viatges i llocs",
"empty_column.account_hides_collections": "Aquest usuari ha elegit no mostrar aquesta informació",
"empty_column.account_suspended": "Compte suspès", "empty_column.account_suspended": "Compte suspès",
"empty_column.account_timeline": "No hi ha tuts aquí!", "empty_column.account_timeline": "No hi ha tuts aquí!",
"empty_column.account_unavailable": "Perfil no disponible", "empty_column.account_unavailable": "Perfil no disponible",

View File

@ -222,6 +222,7 @@
"emoji_button.search_results": "Sykresultaten", "emoji_button.search_results": "Sykresultaten",
"emoji_button.symbols": "Symboalen", "emoji_button.symbols": "Symboalen",
"emoji_button.travel": "Reizgje en lokaasjes", "emoji_button.travel": "Reizgje en lokaasjes",
"empty_column.account_hides_collections": "Dizze brûker hat derfoar keazen dizze ynformaasje net beskikber te meitsjen",
"empty_column.account_suspended": "Account beskoattele", "empty_column.account_suspended": "Account beskoattele",
"empty_column.account_timeline": "Hjir binne gjin berjochten!", "empty_column.account_timeline": "Hjir binne gjin berjochten!",
"empty_column.account_unavailable": "Profyl net beskikber", "empty_column.account_unavailable": "Profyl net beskikber",

View File

@ -41,6 +41,8 @@
"account.languages": "Keisti prenumeruojamas kalbas", "account.languages": "Keisti prenumeruojamas kalbas",
"account.locked_info": "Šios paskyros privatumo būsena nustatyta kaip užrakinta. Savininkas (-ė) rankiniu būdu peržiūri, kas gali sekti.", "account.locked_info": "Šios paskyros privatumo būsena nustatyta kaip užrakinta. Savininkas (-ė) rankiniu būdu peržiūri, kas gali sekti.",
"account.media": "Medija", "account.media": "Medija",
"account.mention": "Paminėti @{name}",
"account.moved_to": "{name} nurodė, kad dabar jų nauja paskyra yra:",
"account.mute": "Užtildyti @{name}", "account.mute": "Užtildyti @{name}",
"account.muted": "Užtildytas", "account.muted": "Užtildytas",
"account.posts": "Toots", "account.posts": "Toots",
@ -53,10 +55,15 @@
"account.unfollow": "Nebesekti", "account.unfollow": "Nebesekti",
"account.unmute_short": "Atitildyti", "account.unmute_short": "Atitildyti",
"account_note.placeholder": "Click to add a note", "account_note.placeholder": "Click to add a note",
"alert.unexpected.title": "Oi!", "alert.unexpected.message": "Įvyko netikėta klaida.",
"alert.unexpected.title": "Ups!",
"announcement.announcement": "Skelbimas", "announcement.announcement": "Skelbimas",
"attachments_list.unprocessed": "(neapdorotas)",
"audio.hide": "Slėpti garsą", "audio.hide": "Slėpti garsą",
"autosuggest_hashtag.per_week": "{count} per savaitę", "autosuggest_hashtag.per_week": "{count} per savaitę",
"boost_modal.combo": "Gali spausti {combo}, kad praleisti kitą kartą",
"bundle_column_error.copy_stacktrace": "Kopijuoti klaidos ataskaitą",
"bundle_column_error.error.body": "Užklausos puslapio nepavyko atvaizduoti. Tai gali būti dėl mūsų kodo klaidos arba naršyklės suderinamumo problemos.",
"bundle_column_error.error.title": "O, ne!", "bundle_column_error.error.title": "O, ne!",
"column.domain_blocks": "Hidden domains", "column.domain_blocks": "Hidden domains",
"column.lists": "Sąrašai", "column.lists": "Sąrašai",

View File

@ -222,6 +222,7 @@
"emoji_button.search_results": "Rezultati iskanja", "emoji_button.search_results": "Rezultati iskanja",
"emoji_button.symbols": "Simboli", "emoji_button.symbols": "Simboli",
"emoji_button.travel": "Potovanja in kraji", "emoji_button.travel": "Potovanja in kraji",
"empty_column.account_hides_collections": "Ta uporabnik se je odločil, da te informacije ne bo dal na voljo",
"empty_column.account_suspended": "Račun je suspendiran", "empty_column.account_suspended": "Račun je suspendiran",
"empty_column.account_timeline": "Tukaj ni objav!", "empty_column.account_timeline": "Tukaj ni objav!",
"empty_column.account_unavailable": "Profil ni na voljo", "empty_column.account_unavailable": "Profil ni na voljo",

View File

@ -222,6 +222,7 @@
"emoji_button.search_results": "Rezultati pretrage", "emoji_button.search_results": "Rezultati pretrage",
"emoji_button.symbols": "Simboli", "emoji_button.symbols": "Simboli",
"emoji_button.travel": "Putovanja i mesta", "emoji_button.travel": "Putovanja i mesta",
"empty_column.account_hides_collections": "Ovaj korisnik je odlučio da ove informacije ne učini dostupnim",
"empty_column.account_suspended": "Nalog je suspendovan", "empty_column.account_suspended": "Nalog je suspendovan",
"empty_column.account_timeline": "Nema objava ovde!", "empty_column.account_timeline": "Nema objava ovde!",
"empty_column.account_unavailable": "Profil je nedostupan", "empty_column.account_unavailable": "Profil je nedostupan",

View File

@ -222,6 +222,7 @@
"emoji_button.search_results": "Резултати претраге", "emoji_button.search_results": "Резултати претраге",
"emoji_button.symbols": "Симболи", "emoji_button.symbols": "Симболи",
"emoji_button.travel": "Путовања и места", "emoji_button.travel": "Путовања и места",
"empty_column.account_hides_collections": "Овај корисник је одлучио да ове информације не учини доступним",
"empty_column.account_suspended": "Налог је суспендован", "empty_column.account_suspended": "Налог је суспендован",
"empty_column.account_timeline": "Нема објава овде!", "empty_column.account_timeline": "Нема објава овде!",
"empty_column.account_unavailable": "Профил је недоступан", "empty_column.account_unavailable": "Профил је недоступан",

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 13V17.8787L17 15.8787V13H19Z"/>
<path d="M2.41421 2.70711L1 4.12132L5 8.12132V11H7V10.1213L13.8787 17H6.85L8.4 15.45L7 14L3 18L7 22L8.4 20.55L6.85 19H15.8787L19.3848 22.5061L20.799 21.0919L2.41421 2.70711Z"/>
<path d="M17.15 7H8.12132L6.12132 5H17.15L15.6 3.45L17 2L21 6L17 10L15.6 8.55L17.15 7Z"/>
</svg>

After

Width:  |  Height:  |  Size: 415 B

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.4 15.45L7 14L3 18L7 22L8.4 20.55L6.85 19H13.5V18C13.5 17.6567 13.5638 17.3171 13.6988 17H6.85L8.4 15.45Z"/>
<path d="M5 5V11H7V7H17.15L15.6 8.55L17 10L21 6L17 2L15.6 3.45L17.15 5H5Z"/>
<path d="M16 22C15.7167 22 15.475 21.9083 15.275 21.725C15.0917 21.525 15 21.2833 15 21V18C15 17.7167 15.0917 17.4833 15.275 17.3C15.475 17.1 15.7167 17 16 17V16C16 15.45 16.1917 14.9833 16.575 14.6C16.975 14.2 17.45 14 18 14C18.55 14 19.0167 14.2 19.4 14.6C19.8 14.9833 20 15.45 20 16V17C20.2833 17 20.5167 17.1 20.7 17.3C20.9 17.4833 21 17.7167 21 18V21C21 21.2833 20.9 21.525 20.7 21.725C20.5167 21.9083 20.2833 22 20 22H16ZM17 17H19V16C19 15.7167 18.9 15.4833 18.7 15.3C18.5167 15.1 18.2833 15 18 15C17.7167 15 17.475 15.1 17.275 15.3C17.0917 15.4833 17 15.7167 17 16V17Z"/>
</svg>

After

Width:  |  Height:  |  Size: 879 B

View File

@ -33,11 +33,11 @@ class Webhook < ApplicationRecord
validates :secret, presence: true, length: { minimum: 12 } validates :secret, presence: true, length: { minimum: 12 }
validates :events, presence: true validates :events, presence: true
validate :validate_events validate :events_validation_error, if: :invalid_events?
validate :validate_permissions validate :validate_permissions
validate :validate_template validate :validate_template
before_validation :strip_events normalizes :events, with: ->(events) { events.filter_map { |event| event.strip.presence } }
before_validation :generate_secret before_validation :generate_secret
def rotate_secret! def rotate_secret!
@ -69,8 +69,12 @@ class Webhook < ApplicationRecord
private private
def validate_events def events_validation_error
errors.add(:events, :invalid) if events.any? { |e| EVENTS.exclude?(e) } errors.add(:events, :invalid)
end
def invalid_events?
events.blank? || events.difference(EVENTS).any?
end end
def validate_permissions def validate_permissions
@ -88,10 +92,6 @@ class Webhook < ApplicationRecord
end end
end end
def strip_events
self.events = events.filter_map { |str| str.strip.presence } if events.present?
end
def generate_secret def generate_secret
self.secret = SecureRandom.hex(20) if secret.blank? self.secret = SecureRandom.hex(20) if secret.blank?
end end

View File

@ -180,7 +180,7 @@ class ActivityPub::ProcessAccountService < BaseService
end end
def check_links! def check_links!
VerifyAccountLinksWorker.perform_async(@account.id) VerifyAccountLinksWorker.perform_in(rand(10.minutes.to_i), @account.id)
end end
def process_duplicate_accounts! def process_duplicate_accounts!

View File

@ -30,11 +30,7 @@ class UpdateAccountService < BaseService
def check_links(account) def check_links(account)
return unless account.fields.any?(&:requires_verification?) return unless account.fields.any?(&:requires_verification?)
if account.local?
VerifyAccountLinksWorker.perform_async(account.id) VerifyAccountLinksWorker.perform_async(account.id)
else
VerifyAccountLinksWorker.perform_in(rand(10.minutes.to_i), account.id)
end
end end
def process_hashtags(account) def process_hashtags(account)

View File

@ -53,3 +53,7 @@ af:
position: position:
elevated: kan nie hoër as jou huidige rol wees nie elevated: kan nie hoër as jou huidige rol wees nie
own_role: kan nie verander word met jou huidige rol nie own_role: kan nie verander word met jou huidige rol nie
webhook:
attributes:
events:
invalid_permissions: geleenthede waartoe jy nie toegang het nie mag nie ingesluit word nie

View File

@ -5,7 +5,23 @@ af:
contact_unavailable: NVT contact_unavailable: NVT
hosted_on: Mastodon gehuisves op %{domain} hosted_on: Mastodon gehuisves op %{domain}
title: Aangaande title: Aangaande
accounts:
follow: Volg
followers:
one: Volgeling
other: Volgelinge
following: Volg
nothing_here: Daar is niks hier nie!
posts:
one: Plasing
other: Plasings
posts_tab_heading: Plasings
admin: admin:
account_actions:
action: Voer aksie uit
title: Voer modereer aksie uit op %{acct}
account_moderation_notes:
create: Los nota
accounts: accounts:
location: location:
local: Plaaslik local: Plaaslik
@ -102,6 +118,7 @@ af:
types: types:
bookmarks: Boekmerke bookmarks: Boekmerke
invites: invites:
invalid: Hierdie uitnodiging is nie geldig nie
title: Nooi ander title: Nooi ander
login_activities: login_activities:
description_html: Indien jy onbekende aktiwiteite gewaar, oorweeg dit om jou wagwoord te verander en tweefaktorverifikasie te aktiveer. description_html: Indien jy onbekende aktiwiteite gewaar, oorweeg dit om jou wagwoord te verander en tweefaktorverifikasie te aktiveer.

View File

@ -1418,6 +1418,7 @@ be:
'86400': 1 дзень '86400': 1 дзень
expires_in_prompt: Ніколі expires_in_prompt: Ніколі
generate: Стварыць запрашальную спасылку generate: Стварыць запрашальную спасылку
invalid: Гэта запрашэнне несапраўднае
invited_by: 'Вас запрасіў(-ла):' invited_by: 'Вас запрасіў(-ла):'
max_uses: max_uses:
few: "%{count} выкарыстанні" few: "%{count} выкарыстанні"

View File

@ -1358,6 +1358,7 @@ ca:
'86400': 1 dia '86400': 1 dia
expires_in_prompt: Mai expires_in_prompt: Mai
generate: Genera generate: Genera
invalid: Aquesta invitació no és vàlida
invited_by: 'Has estat invitat per:' invited_by: 'Has estat invitat per:'
max_uses: max_uses:
one: 1 ús one: 1 ús

View File

@ -1468,6 +1468,7 @@ cy:
'86400': 1 diwrnod '86400': 1 diwrnod
expires_in_prompt: Byth expires_in_prompt: Byth
generate: Cynhyrchu dolen wahoddiad generate: Cynhyrchu dolen wahoddiad
invalid: Nid yw'r gwahoddiad hwn yn ddilys
invited_by: 'Cawsoch eich gwahodd gan:' invited_by: 'Cawsoch eich gwahodd gan:'
max_uses: max_uses:
few: "%{count} defnydd" few: "%{count} defnydd"

View File

@ -1110,6 +1110,7 @@ da:
functional: Din konto er fuldt funktionel. functional: Din konto er fuldt funktionel.
pending: Din ansøgning afventer gennemgang af vores medarbejdere. Dette kan tage noget tid. Du modtager en e-mail, hvis din ansøgning godkendes. pending: Din ansøgning afventer gennemgang af vores medarbejdere. Dette kan tage noget tid. Du modtager en e-mail, hvis din ansøgning godkendes.
redirecting_to: Din konto er inaktiv, da den pt. er omdirigerer til %{acct}. redirecting_to: Din konto er inaktiv, da den pt. er omdirigerer til %{acct}.
self_destruct: Da %{domain} er under nedlukning, vil kontoadgangen være begrænset.
view_strikes: Se tidligere anmeldelser af din konto view_strikes: Se tidligere anmeldelser af din konto
too_fast: Formularen indsendt for hurtigt, forsøg igen. too_fast: Formularen indsendt for hurtigt, forsøg igen.
use_security_key: Brug sikkerhedsnøgle use_security_key: Brug sikkerhedsnøgle
@ -1367,6 +1368,7 @@ da:
'86400': 1 dag '86400': 1 dag
expires_in_prompt: Aldrig expires_in_prompt: Aldrig
generate: Generér invitationslink generate: Generér invitationslink
invalid: Denne invitation er ikke gyldig
invited_by: 'Du blev inviteret af:' invited_by: 'Du blev inviteret af:'
max_uses: max_uses:
one: 1 benyttelse one: 1 benyttelse
@ -1579,6 +1581,9 @@ da:
over_daily_limit: Den daglige grænse på %{limit} planlagte indlæg er nået over_daily_limit: Den daglige grænse på %{limit} planlagte indlæg er nået
over_total_limit: Grænsen på %{limit} planlagte indlæg er nået over_total_limit: Grænsen på %{limit} planlagte indlæg er nået
too_soon: Den planlagte dato skal være i fremtiden too_soon: Den planlagte dato skal være i fremtiden
self_destruct:
lead_html: Desværre lukker <strong>%{domain}</strong> permanent. Har man en konto dér, vil fortsat brug heraf ikke være mulig. Man kan dog stadig anmode om en sikkerhedskopi af sine data.
title: Denne server er under nedlukning
sessions: sessions:
activity: Seneste aktivitet activity: Seneste aktivitet
browser: Browser browser: Browser

View File

@ -1368,6 +1368,7 @@ de:
'86400': 1 Tag '86400': 1 Tag
expires_in_prompt: Nie expires_in_prompt: Nie
generate: Einladungslink erstellen generate: Einladungslink erstellen
invalid: Diese Einladung ist ungültig
invited_by: 'Du wurdest eingeladen von:' invited_by: 'Du wurdest eingeladen von:'
max_uses: max_uses:
one: Eine Verwendung one: Eine Verwendung

View File

@ -18,7 +18,7 @@ be:
unconfirmed: Вы павінны пацвердзіць свой адрас электроннай пошты, перш чым працягнуць unconfirmed: Вы павінны пацвердзіць свой адрас электроннай пошты, перш чым працягнуць
mailer: mailer:
confirmation_instructions: confirmation_instructions:
action: Пацвердзіце адрас электроннай пошты action: Пацвердзіць адрас электроннай пошты
action_with_app: Пацвердзіць і вярнуцца да %{app} action_with_app: Пацвердзіць і вярнуцца да %{app}
explanation: Вы стварылі ўліковы запіс на %{host} з гэтым адрасам электроннай пошты. Вам спатрэбіцца ўсяго адзін клік, каб пацвердзіць яго. Калі гэта былі не вы, то проста праігнаруйце гэты ліст. explanation: Вы стварылі ўліковы запіс на %{host} з гэтым адрасам электроннай пошты. Вам спатрэбіцца ўсяго адзін клік, каб пацвердзіць яго. Калі гэта былі не вы, то проста праігнаруйце гэты ліст.
explanation_when_pending: Вы падалі заяўку на запрашэнне на %{host} з гэтым адрасам электроннай пошты. Як толькі вы пацвердзіце свой адрас электроннай пошты, мы разгледзім вашу заяўку. Вы можаце ўвайсці, каб змяніць свае дадзеныя або выдаліць свой уліковы запіс, але вы не можаце атрымаць доступ да большасці функцый, пакуль ваш уліковы запіс не будзе зацверджаны. Калі ваша заяўка будзе адхілена, вашы даныя будуць выдалены, таму ад вас не спатрэбіцца ніякіх дадатковых дзеянняў. Калі гэта былі не вы, ігнаруйце гэты ліст explanation_when_pending: Вы падалі заяўку на запрашэнне на %{host} з гэтым адрасам электроннай пошты. Як толькі вы пацвердзіце свой адрас электроннай пошты, мы разгледзім вашу заяўку. Вы можаце ўвайсці, каб змяніць свае дадзеныя або выдаліць свой уліковы запіс, але вы не можаце атрымаць доступ да большасці функцый, пакуль ваш уліковы запіс не будзе зацверджаны. Калі ваша заяўка будзе адхілена, вашы даныя будуць выдалены, таму ад вас не спатрэбіцца ніякіх дадатковых дзеянняў. Калі гэта былі не вы, ігнаруйце гэты ліст

View File

@ -149,6 +149,7 @@ af:
write:blocks: blokkeer rekeninge en domeine write:blocks: blokkeer rekeninge en domeine
write:bookmarks: laat n boekmerk by plasings write:bookmarks: laat n boekmerk by plasings
write:conversations: doof en wis gesprekke uit write:conversations: doof en wis gesprekke uit
write:favourites: gunsteling plasings
write:filters: skep filters write:filters: skep filters
write:follows: volg mense write:follows: volg mense
write:lists: skep lyste write:lists: skep lyste

View File

@ -1368,6 +1368,7 @@ es-AR:
'86400': 1 día '86400': 1 día
expires_in_prompt: Nunca expires_in_prompt: Nunca
generate: Generar enlace de invitación generate: Generar enlace de invitación
invalid: Esta invitación no es válida
invited_by: 'Fuiste invitado por:' invited_by: 'Fuiste invitado por:'
max_uses: max_uses:
one: 1 uso one: 1 uso

View File

@ -1368,6 +1368,7 @@ es-MX:
'86400': 1 día '86400': 1 día
expires_in_prompt: Nunca expires_in_prompt: Nunca
generate: Generar generate: Generar
invalid: Esta invitación no es válida
invited_by: 'Fuiste invitado por:' invited_by: 'Fuiste invitado por:'
max_uses: max_uses:
one: 1 uso one: 1 uso

View File

@ -1368,6 +1368,7 @@ es:
'86400': 1 día '86400': 1 día
expires_in_prompt: Nunca expires_in_prompt: Nunca
generate: Generar generate: Generar
invalid: Esta invitación no es válida
invited_by: 'Fuiste invitado por:' invited_by: 'Fuiste invitado por:'
max_uses: max_uses:
one: 1 uso one: 1 uso

View File

@ -1363,6 +1363,7 @@ eu:
'86400': Egun 1 '86400': Egun 1
expires_in_prompt: Inoiz ez expires_in_prompt: Inoiz ez
generate: Sortu generate: Sortu
invalid: Gonbidapen hau ez da baliozkoa
invited_by: 'Honek gonbidatu zaitu:' invited_by: 'Honek gonbidatu zaitu:'
max_uses: max_uses:
one: Erabilera 1 one: Erabilera 1

View File

@ -1157,6 +1157,7 @@ fa:
'86400': ۱ روز '86400': ۱ روز
expires_in_prompt: هیچ وقت expires_in_prompt: هیچ وقت
generate: ساختن generate: ساختن
invalid: این دعوت‌نامه معتبر نیست
invited_by: 'دعوت‌کنندهٔ شما:' invited_by: 'دعوت‌کنندهٔ شما:'
max_uses: max_uses:
one: ۱ بار one: ۱ بار

View File

@ -1368,6 +1368,7 @@ fo:
'86400': 1 dag '86400': 1 dag
expires_in_prompt: Ongantíð expires_in_prompt: Ongantíð
generate: Ger innbjóðingarleinki generate: Ger innbjóðingarleinki
invalid: Henda innbjóðing er ikki gildug
invited_by: 'Tú var bjóðað/ur av:' invited_by: 'Tú var bjóðað/ur av:'
max_uses: max_uses:
one: 1 brúk one: 1 brúk

View File

@ -1368,6 +1368,7 @@ fr-QC:
'86400': 1 jour '86400': 1 jour
expires_in_prompt: Jamais expires_in_prompt: Jamais
generate: Générer un lien d'invitation generate: Générer un lien d'invitation
invalid: Cette invitation nest pas valide
invited_by: 'Vous avez été invité·e par:' invited_by: 'Vous avez été invité·e par:'
max_uses: max_uses:
one: 1 utilisation one: 1 utilisation

View File

@ -1368,6 +1368,7 @@ fr:
'86400': 1 jour '86400': 1 jour
expires_in_prompt: Jamais expires_in_prompt: Jamais
generate: Générer un lien d'invitation generate: Générer un lien d'invitation
invalid: Cette invitation nest pas valide
invited_by: 'Vous avez été invité·e par:' invited_by: 'Vous avez été invité·e par:'
max_uses: max_uses:
one: 1 utilisation one: 1 utilisation

View File

@ -1368,6 +1368,7 @@ fy:
'86400': 1 dei '86400': 1 dei
expires_in_prompt: Nea expires_in_prompt: Nea
generate: Utnûgingskeppeling generearje generate: Utnûgingskeppeling generearje
invalid: Dizze útnûging is net jildich
invited_by: 'Jo binne útnûge troch:' invited_by: 'Jo binne útnûge troch:'
max_uses: max_uses:
one: 1 kear one: 1 kear

View File

@ -1368,6 +1368,7 @@ gl:
'86400': 1 día '86400': 1 día
expires_in_prompt: Nunca expires_in_prompt: Nunca
generate: Xerar generate: Xerar
invalid: Este convite non é válido
invited_by: 'Convidoute:' invited_by: 'Convidoute:'
max_uses: max_uses:
one: 1 uso one: 1 uso

View File

@ -113,8 +113,8 @@ he:
previous_strikes_description_html: previous_strikes_description_html:
many: לחשבון הזה יש <strong>%{count}</strong> פסילות. many: לחשבון הזה יש <strong>%{count}</strong> פסילות.
one: לחשבון הזה פסילה <strong>אחת</strong>. one: לחשבון הזה פסילה <strong>אחת</strong>.
other: לחשבון הזה <strong>%{count}</strong> פסילות. other: לחשבון הזה יש <strong>%{count}</strong> פסילות.
two: לחשבון הזה <strong>%{count}</strong> פסילות. two: לחשבון הזה יש <strong>שתי</strong> פסילות.
promote: להעלות בדרגה promote: להעלות בדרגה
protocol: פרטיכל protocol: פרטיכל
public: פומבי public: פומבי
@ -1418,6 +1418,7 @@ he:
'86400': יום אחד '86400': יום אחד
expires_in_prompt: לעולם לא expires_in_prompt: לעולם לא
generate: יצירת קישור להזמנה generate: יצירת קישור להזמנה
invalid: הזמנה זו אינה תקפה
invited_by: הוזמנת ע"י invited_by: הוזמנת ע"י
max_uses: max_uses:
many: "%{count} שימושים" many: "%{count} שימושים"

View File

@ -1368,6 +1368,7 @@ hu:
'86400': 1 nap '86400': 1 nap
expires_in_prompt: Soha expires_in_prompt: Soha
generate: Generálás generate: Generálás
invalid: Ez a meghívó nem érvényes
invited_by: 'Téged meghívott:' invited_by: 'Téged meghívott:'
max_uses: max_uses:
one: 1 használat one: 1 használat

View File

@ -1372,6 +1372,7 @@ is:
'86400': 1 dagur '86400': 1 dagur
expires_in_prompt: Aldrei expires_in_prompt: Aldrei
generate: Útbúa boðstengil generate: Útbúa boðstengil
invalid: Þetta boð er ekki gilt
invited_by: 'Þér var boðið af:' invited_by: 'Þér var boðið af:'
max_uses: max_uses:
one: 1 afnot one: 1 afnot

View File

@ -1370,6 +1370,7 @@ it:
'86400': 1 giorno '86400': 1 giorno
expires_in_prompt: Mai expires_in_prompt: Mai
generate: Genera generate: Genera
invalid: Questo invito non è valido
invited_by: 'Sei stato invitato da:' invited_by: 'Sei stato invitato da:'
max_uses: max_uses:
one: un uso one: un uso

View File

@ -1345,6 +1345,7 @@ ko:
'86400': 1 '86400': 1
expires_in_prompt: 영원히 expires_in_prompt: 영원히
generate: 초대 링크 생성하기 generate: 초대 링크 생성하기
invalid: 이 초대는 올바르지 않습니다
invited_by: '당신을 초대한 사람:' invited_by: '당신을 초대한 사람:'
max_uses: max_uses:
other: "%{count}회" other: "%{count}회"

View File

@ -383,6 +383,7 @@ lt:
'86400': 1 dienos '86400': 1 dienos
expires_in_prompt: Niekada expires_in_prompt: Niekada
generate: Generuoti generate: Generuoti
invalid: Šis kvietimas negalioja.
invited_by: 'Jus pakvietė:' invited_by: 'Jus pakvietė:'
max_uses: max_uses:
few: "%{count} panaudojimai" few: "%{count} panaudojimai"

View File

@ -1368,6 +1368,7 @@ nl:
'86400': 1 dag '86400': 1 dag
expires_in_prompt: Nooit expires_in_prompt: Nooit
generate: Uitnodigingslink genereren generate: Uitnodigingslink genereren
invalid: Deze uitnodiging is niet geldig
invited_by: 'Jij bent uitgenodigd door:' invited_by: 'Jij bent uitgenodigd door:'
max_uses: max_uses:
one: 1 keer one: 1 keer

View File

@ -1368,6 +1368,7 @@ nn:
'86400': 1 dag '86400': 1 dag
expires_in_prompt: Aldri expires_in_prompt: Aldri
generate: Lag innbydingslenkje generate: Lag innbydingslenkje
invalid: Denne invitasjonen er ikkje gyldig
invited_by: 'Du vart innboden av:' invited_by: 'Du vart innboden av:'
max_uses: max_uses:
one: 1 bruk one: 1 bruk

View File

@ -1368,6 +1368,7 @@
'86400': 1 dag '86400': 1 dag
expires_in_prompt: Aldri expires_in_prompt: Aldri
generate: Generer generate: Generer
invalid: Denne invitasjonen er ikke gyldig
invited_by: 'Du ble invitert av:' invited_by: 'Du ble invitert av:'
max_uses: max_uses:
one: 1 bruk one: 1 bruk

View File

@ -1418,6 +1418,7 @@ pl:
'86400': dobie '86400': dobie
expires_in_prompt: Nigdy expires_in_prompt: Nigdy
generate: Wygeneruj generate: Wygeneruj
invalid: Niepoprawne zaproszenie
invited_by: 'Zostałeś(-aś) zaproszony(-a) przez:' invited_by: 'Zostałeś(-aś) zaproszony(-a) przez:'
max_uses: max_uses:
few: "%{count} użycia" few: "%{count} użycia"

View File

@ -1368,6 +1368,7 @@ pt-BR:
'86400': 1 dia '86400': 1 dia
expires_in_prompt: Nunca expires_in_prompt: Nunca
generate: Gerar convite generate: Gerar convite
invalid: Este convite não é válido
invited_by: 'Você recebeu convite de:' invited_by: 'Você recebeu convite de:'
max_uses: max_uses:
one: 1 uso one: 1 uso

View File

@ -1368,6 +1368,7 @@ pt-PT:
'86400': 1 dia '86400': 1 dia
expires_in_prompt: Nunca expires_in_prompt: Nunca
generate: Gerar hiperligação de convite generate: Gerar hiperligação de convite
invalid: Este convite não é válido
invited_by: 'Foi convidado por:' invited_by: 'Foi convidado por:'
max_uses: max_uses:
one: 1 uso one: 1 uso

View File

@ -53,7 +53,7 @@ be:
password: Не менш за 8 сімвалаў password: Не менш за 8 сімвалаў
phrase: Параўнанне адбудзецца нягледзячы на рэгістр тэксту і папярэджанні аб змесціве допісу phrase: Параўнанне адбудзецца нягледзячы на рэгістр тэксту і папярэджанні аб змесціве допісу
scopes: Якімі API праграм будзе дазволена карыстацца. Калі вы абярэце найвышэйшы ўзровень, не трэба абіраць асобныя. scopes: Якімі API праграм будзе дазволена карыстацца. Калі вы абярэце найвышэйшы ўзровень, не трэба абіраць асобныя.
setting_aggregate_reblogs: Не паказваць новыя пашырэнні для допісаў, якія нядаўна пашырылі(уплывае выключна на будучыя пашырэнні) setting_aggregate_reblogs: Не паказваць новыя пашырэнні для допісаў, якія пашырылі нядаўна (закранае толькі нядаўнія пашырэнні)
setting_always_send_emails: Звычайна лісты з апавяшчэннямі не будуць дасылацца, калі вы актыўна карыстаецеся Mastodon setting_always_send_emails: Звычайна лісты з апавяшчэннямі не будуць дасылацца, калі вы актыўна карыстаецеся Mastodon
setting_default_sensitive: Далікатныя медыя прадвызначана схаваныя. Іх можна адкрыць адзіным клікам setting_default_sensitive: Далікатныя медыя прадвызначана схаваныя. Іх можна адкрыць адзіным клікам
setting_display_media_default: Хаваць медыя пазначаныя як далікатныя setting_display_media_default: Хаваць медыя пазначаныя як далікатныя

View File

@ -592,6 +592,8 @@ sk:
title: Ohľadom title: Ohľadom
appearance: appearance:
title: Vzhľad title: Vzhľad
content_retention:
title: Ponechanie obsahu
discovery: discovery:
follow_recommendations: Odporúčania pre nasledovanie follow_recommendations: Odporúčania pre nasledovanie
profile_directory: Katalóg profilov profile_directory: Katalóg profilov
@ -616,6 +618,7 @@ sk:
delete: Vymaž nahratý súbor delete: Vymaž nahratý súbor
destroyed_msg: Nahratie bolo zo stránky úspešne vymazané! destroyed_msg: Nahratie bolo zo stránky úspešne vymazané!
software_updates: software_updates:
critical_update: Kritické — prosím aktualizuj rýchlo
documentation_link: Zisti viac documentation_link: Zisti viac
title: Dostupné aktualizácie title: Dostupné aktualizácie
types: types:
@ -646,6 +649,10 @@ sk:
appeal_approved: Namietnuté appeal_approved: Namietnuté
appeal_rejected: Námietka zamietnutá appeal_rejected: Námietka zamietnutá
system_checks: system_checks:
elasticsearch_preset:
action: Pozri dokumentáciu
elasticsearch_preset_single_node:
action: Pozri dokumentáciu
rules_check: rules_check:
action: Spravuj serverové pravidlá action: Spravuj serverové pravidlá
message_html: Neurčil/a si žiadne serverové pravidlá. message_html: Neurčil/a si žiadne serverové pravidlá.
@ -925,6 +932,7 @@ sk:
'86400': 1 deň '86400': 1 deň
expires_in_prompt: Nikdy expires_in_prompt: Nikdy
generate: Vygeneruj generate: Vygeneruj
invalid: Táto pozvánka je neplatná
invited_by: 'Bol/a si pozvaný/á užívateľom:' invited_by: 'Bol/a si pozvaný/á užívateľom:'
max_uses: max_uses:
few: "%{count} využití" few: "%{count} využití"

View File

@ -1418,6 +1418,7 @@ sl:
'86400': 1 dan '86400': 1 dan
expires_in_prompt: Nikoli expires_in_prompt: Nikoli
generate: Ustvari generate: Ustvari
invalid: To povabilo ni veljavno
invited_by: 'Povabil/a vas je:' invited_by: 'Povabil/a vas je:'
max_uses: max_uses:
few: "%{count} uporabe" few: "%{count} uporabe"

View File

@ -1362,6 +1362,7 @@ sq:
'86400': 1 ditë '86400': 1 ditë
expires_in_prompt: Kurrë expires_in_prompt: Kurrë
generate: Prodho lidhje ftese generate: Prodho lidhje ftese
invalid: Kjo ftesë sështë e vlefshme
invited_by: 'Qetë ftuar nga:' invited_by: 'Qetë ftuar nga:'
max_uses: max_uses:
one: 1 përdorim one: 1 përdorim

View File

@ -1393,6 +1393,7 @@ sr-Latn:
'86400': 1 dan '86400': 1 dan
expires_in_prompt: Nikad expires_in_prompt: Nikad
generate: Generiši generate: Generiši
invalid: Ova pozivnica nije važeća
invited_by: 'Pozvao Vas je:' invited_by: 'Pozvao Vas je:'
max_uses: max_uses:
few: "%{count} korišćenja" few: "%{count} korišćenja"

View File

@ -1393,6 +1393,7 @@ sr:
'86400': 1 дан '86400': 1 дан
expires_in_prompt: Никад expires_in_prompt: Никад
generate: Генериши generate: Генериши
invalid: Ова позивница није важећа
invited_by: 'Позвао Вас је:' invited_by: 'Позвао Вас је:'
max_uses: max_uses:
few: "%{count} коришћења" few: "%{count} коришћења"

View File

@ -1368,6 +1368,7 @@ sv:
'86400': 1 dag '86400': 1 dag
expires_in_prompt: Aldrig expires_in_prompt: Aldrig
generate: Skapa generate: Skapa
invalid: Ogiltig inbjudan
invited_by: 'Du blev inbjuden av:' invited_by: 'Du blev inbjuden av:'
max_uses: max_uses:
one: 1 användning one: 1 användning

View File

@ -1343,6 +1343,7 @@ th:
'86400': 1 วัน '86400': 1 วัน
expires_in_prompt: ไม่เลย expires_in_prompt: ไม่เลย
generate: สร้างลิงก์เชิญ generate: สร้างลิงก์เชิญ
invalid: คำเชิญนี้ไม่ถูกต้อง
invited_by: 'คุณได้รับเชิญโดย:' invited_by: 'คุณได้รับเชิญโดย:'
max_uses: max_uses:
other: "%{count} การใช้งาน" other: "%{count} การใช้งาน"

View File

@ -1368,6 +1368,7 @@ tr:
'86400': 1 gün '86400': 1 gün
expires_in_prompt: Asla expires_in_prompt: Asla
generate: Davet bağlantısı oluştur generate: Davet bağlantısı oluştur
invalid: Bu davet geçerli değil
invited_by: 'Davet edildiniz:' invited_by: 'Davet edildiniz:'
max_uses: max_uses:
one: 1 kullanım one: 1 kullanım

View File

@ -1418,6 +1418,7 @@ uk:
'86400': 1 день '86400': 1 день
expires_in_prompt: Ніколи expires_in_prompt: Ніколи
generate: Згенерувати generate: Згенерувати
invalid: Це запрошення не дійсне
invited_by: 'Вас запросив:' invited_by: 'Вас запросив:'
max_uses: max_uses:
few: "%{count} використання" few: "%{count} використання"

View File

@ -1343,6 +1343,7 @@ zh-CN:
'86400': 1 天后 '86400': 1 天后
expires_in_prompt: 永不过期 expires_in_prompt: 永不过期
generate: 生成邀请链接 generate: 生成邀请链接
invalid: 此邀请无效
invited_by: 你的邀请人是: invited_by: 你的邀请人是:
max_uses: max_uses:
other: "%{count} 次" other: "%{count} 次"

View File

@ -1343,6 +1343,7 @@ zh-HK:
'86400': 1 天後 '86400': 1 天後
expires_in_prompt: 永不過期 expires_in_prompt: 永不過期
generate: 生成邀請連結 generate: 生成邀請連結
invalid: 此邀請無效
invited_by: 你的邀請人是: invited_by: 你的邀請人是:
max_uses: max_uses:
other: "%{count} 次" other: "%{count} 次"

View File

@ -1345,6 +1345,7 @@ zh-TW:
'86400': 1 天後 '86400': 1 天後
expires_in_prompt: 永不過期 expires_in_prompt: 永不過期
generate: 建立邀請連結 generate: 建立邀請連結
invalid: 此邀請是無效的
invited_by: 您的邀請人是: invited_by: 您的邀請人是:
max_uses: max_uses:
other: "%{count} 則" other: "%{count} 則"

View File

@ -1,6 +1,6 @@
module.exports = { module.exports = {
test: /\.svg$/, test: /\.svg$/,
include: /node_modules\/@material-symbols/, include: [/node_modules\/@material-symbols/, /svg-icons/],
issuer: /\.[jt]sx?$/, issuer: /\.[jt]sx?$/,
use: [ use: [
{ {

View File

@ -179,7 +179,7 @@
"@types/react-dom": "^18.2.4", "@types/react-dom": "^18.2.4",
"@types/react-helmet": "^6.1.6", "@types/react-helmet": "^6.1.6",
"@types/react-immutable-proptypes": "^2.1.0", "@types/react-immutable-proptypes": "^2.1.0",
"@types/react-motion": "^0.0.36", "@types/react-motion": "^0.0.37",
"@types/react-overlays": "^3.1.0", "@types/react-overlays": "^3.1.0",
"@types/react-router": "^5.1.20", "@types/react-router": "^5.1.20",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
@ -203,7 +203,7 @@
"eslint-plugin-formatjs": "^4.10.1", "eslint-plugin-formatjs": "^4.10.1",
"eslint-plugin-import": "~2.29.0", "eslint-plugin-import": "~2.29.0",
"eslint-plugin-jsdoc": "^46.1.0", "eslint-plugin-jsdoc": "^46.1.0",
"eslint-plugin-jsx-a11y": "~6.7.1", "eslint-plugin-jsx-a11y": "~6.8.0",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-promise": "~6.1.1", "eslint-plugin-promise": "~6.1.1",
"eslint-plugin-react": "~7.33.0", "eslint-plugin-react": "~7.33.0",
@ -245,5 +245,5 @@
"*.{js,jsx,ts,tsx}": "eslint --fix", "*.{js,jsx,ts,tsx}": "eslint --fix",
"*.{css,scss}": "stylelint --fix" "*.{css,scss}": "stylelint --fix"
}, },
"packageManager": "yarn@4.0.1" "packageManager": "yarn@4.0.2"
} }

View File

@ -58,7 +58,7 @@ RSpec.describe ActivityPub::InboxesController do
before do before do
allow(ActivityPub::FollowersSynchronizationWorker).to receive(:perform_async).and_return(nil) allow(ActivityPub::FollowersSynchronizationWorker).to receive(:perform_async).and_return(nil)
allow_any_instance_of(Account).to receive(:local_followers_hash).and_return('somehash') allow(remote_account).to receive(:local_followers_hash).and_return('somehash')
request.headers['Collection-Synchronization'] = synchronization_header request.headers['Collection-Synchronization'] = synchronization_header
post :create, body: '{}' post :create, body: '{}'

View File

@ -20,8 +20,7 @@ RSpec.describe Admin::AccountsController do
it 'filters with parameters' do it 'filters with parameters' do
account_filter = instance_double(AccountFilter, results: Account.all) account_filter = instance_double(AccountFilter, results: Account.all)
allow(AccountFilter).to receive(:new).and_return(account_filter) allow(AccountFilter).to receive(:new).and_return(account_filter)
params = {
get :index, params: {
origin: 'local', origin: 'local',
by_domain: 'domain', by_domain: 'domain',
status: 'active', status: 'active',
@ -31,17 +30,9 @@ RSpec.describe Admin::AccountsController do
ip: '0.0.0.42', ip: '0.0.0.42',
} }
expect(AccountFilter).to have_received(:new) do |params| get :index, params: params
h = params.to_h
expect(h[:origin]).to eq 'local' expect(AccountFilter).to have_received(:new).with(hash_including(params))
expect(h[:by_domain]).to eq 'domain'
expect(h[:status]).to eq 'active'
expect(h[:username]).to eq 'username'
expect(h[:display_name]).to eq 'display name'
expect(h[:email]).to eq 'local-part@domain'
expect(h[:ip]).to eq '0.0.0.42'
end
end end
it 'paginates accounts' do it 'paginates accounts' do
@ -236,7 +227,8 @@ RSpec.describe Admin::AccountsController do
let(:account) { Fabricate(:account, domain: 'example.com') } let(:account) { Fabricate(:account, domain: 'example.com') }
before do before do
allow_any_instance_of(ResolveAccountService).to receive(:call) service = instance_double(ResolveAccountService, call: nil)
allow(ResolveAccountService).to receive(:new).and_return(service)
end end
context 'when user is admin' do context 'when user is admin' do

View File

@ -13,12 +13,20 @@ describe Admin::ResetsController do
describe 'POST #create' do describe 'POST #create' do
it 'redirects to admin accounts page' do it 'redirects to admin accounts page' do
expect_any_instance_of(User).to receive(:send_reset_password_instructions) do |value| expect do
expect(value.account_id).to eq account.id
end
post :create, params: { account_id: account.id } post :create, params: { account_id: account.id }
end.to change(Devise.mailer.deliveries, :size).by(2)
expect(Devise.mailer.deliveries).to have_attributes(
first: have_attributes(
to: include(account.user.email),
subject: I18n.t('devise.mailer.password_change.subject')
),
last: have_attributes(
to: include(account.user.email),
subject: I18n.t('devise.mailer.reset_password_instructions.subject')
)
)
expect(response).to redirect_to(admin_account_path(account.id)) expect(response).to redirect_to(admin_account_path(account.id))
end end
end end

View File

@ -126,7 +126,7 @@ RSpec.describe Auth::SessionsController do
let!(:previous_login) { Fabricate(:login_activity, user: user, ip: previous_ip) } let!(:previous_login) { Fabricate(:login_activity, user: user, ip: previous_ip) }
before do before do
allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return(current_ip) allow(controller.request).to receive(:remote_ip).and_return(current_ip)
user.update(current_sign_in_at: 1.month.ago) user.update(current_sign_in_at: 1.month.ago)
post :create, params: { user: { email: user.email, password: user.password } } post :create, params: { user: { email: user.email, password: user.password } }
end end
@ -279,7 +279,9 @@ RSpec.describe Auth::SessionsController do
context 'when the server has an decryption error' do context 'when the server has an decryption error' do
before do before do
allow_any_instance_of(User).to receive(:validate_and_consume_otp!).and_raise(OpenSSL::Cipher::CipherError) allow(user).to receive(:validate_and_consume_otp!).with(user.current_otp).and_raise(OpenSSL::Cipher::CipherError)
allow(User).to receive(:find_by).with(id: user.id).and_return(user)
post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s } post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
end end

View File

@ -61,6 +61,7 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
it 'renders page with success' do it 'renders page with success' do
prepare_user_otp_generation prepare_user_otp_generation
prepare_user_otp_consumption prepare_user_otp_consumption
allow(controller).to receive(:current_user).and_return(user)
expect do expect do
post :create, post :create,
@ -75,30 +76,28 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
end end
def prepare_user_otp_generation def prepare_user_otp_generation
expect_any_instance_of(User).to receive(:generate_otp_backup_codes!) do |value| allow(user)
expect(value).to eq user .to receive(:generate_otp_backup_codes!)
otp_backup_codes .and_return(otp_backup_codes)
end
end end
def prepare_user_otp_consumption def prepare_user_otp_consumption
expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, code, options| options = { otp_secret: 'thisisasecretforthespecofnewview' }
expect(value).to eq user allow(user)
expect(code).to eq '123456' .to receive(:validate_and_consume_otp!)
expect(options).to eq({ otp_secret: 'thisisasecretforthespecofnewview' }) .with('123456', options)
true .and_return(true)
end
end end
end end
describe 'when creation fails' do describe 'when creation fails' do
subject do subject do
expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, code, options| options = { otp_secret: 'thisisasecretforthespecofnewview' }
expect(value).to eq user allow(user)
expect(code).to eq '123456' .to receive(:validate_and_consume_otp!)
expect(options).to eq({ otp_secret: 'thisisasecretforthespecofnewview' }) .with('123456', options)
false .and_return(false)
end allow(controller).to receive(:current_user).and_return(user)
expect do expect do
post :create, post :create,

View File

@ -9,10 +9,8 @@ describe Settings::TwoFactorAuthentication::RecoveryCodesController do
it 'updates the codes and shows them on a view when signed in' do it 'updates the codes and shows them on a view when signed in' do
user = Fabricate(:user) user = Fabricate(:user)
otp_backup_codes = user.generate_otp_backup_codes! otp_backup_codes = user.generate_otp_backup_codes!
expect_any_instance_of(User).to receive(:generate_otp_backup_codes!) do |value| allow(user).to receive(:generate_otp_backup_codes!).and_return(otp_backup_codes)
expect(value).to eq user allow(controller).to receive(:current_user).and_return(user)
otp_backup_codes
end
sign_in user, scope: :user sign_in user, scope: :user
post :create, session: { challenge_passed_at: Time.now.utc } post :create, session: { challenge_passed_at: Time.now.utc }

View File

@ -64,8 +64,11 @@ describe Request do
end end
it 'closes underlying connection' do it 'closes underlying connection' do
expect_any_instance_of(HTTP::Client).to receive(:close) allow(subject.send(:http_client)).to receive(:close)
expect { |block| subject.perform(&block) }.to yield_control expect { |block| subject.perform(&block) }.to yield_control
expect(subject.send(:http_client)).to have_received(:close)
end end
it 'returns response which implements body_with_limit' do it 'returns response which implements body_with_limit' do

View File

@ -23,7 +23,8 @@ describe StatusFilter do
context 'when status policy does not allow show' do context 'when status policy does not allow show' do
it 'filters the status' do it 'filters the status' do
allow_any_instance_of(StatusPolicy).to receive(:show?).and_return(false) policy = instance_double(StatusPolicy, show?: false)
allow(StatusPolicy).to receive(:new).and_return(policy)
expect(filter).to be_filtered expect(filter).to be_filtered
end end
@ -74,7 +75,8 @@ describe StatusFilter do
context 'when status policy does not allow show' do context 'when status policy does not allow show' do
it 'filters the status' do it 'filters the status' do
allow_any_instance_of(StatusPolicy).to receive(:show?).and_return(false) policy = instance_double(StatusPolicy, show?: false)
allow(StatusPolicy).to receive(:new).and_return(policy)
expect(filter).to be_filtered expect(filter).to be_filtered
end end

View File

@ -209,9 +209,13 @@ RSpec.describe Account do
expect(account.refresh!).to be_nil expect(account.refresh!).to be_nil
end end
it 'calls not ResolveAccountService#call' do it 'does not call ResolveAccountService#call' do
expect_any_instance_of(ResolveAccountService).to_not receive(:call).with(acct) service = instance_double(ResolveAccountService, call: nil)
allow(ResolveAccountService).to receive(:new).and_return(service)
account.refresh! account.refresh!
expect(service).to_not have_received(:call).with(acct)
end end
end end
@ -219,8 +223,12 @@ RSpec.describe Account do
let(:domain) { 'example.com' } let(:domain) { 'example.com' }
it 'calls ResolveAccountService#call' do it 'calls ResolveAccountService#call' do
expect_any_instance_of(ResolveAccountService).to receive(:call).with(acct).once service = instance_double(ResolveAccountService, call: nil)
allow(ResolveAccountService).to receive(:new).and_return(service)
account.refresh! account.refresh!
expect(service).to have_received(:call).with(acct).once
end end
end end
end end

View File

@ -52,7 +52,8 @@ RSpec.describe Setting do
before do before do
allow(RailsSettings::Settings).to receive(:object).with(key).and_return(object) allow(RailsSettings::Settings).to receive(:object).with(key).and_return(object)
allow(described_class).to receive(:default_settings).and_return(default_settings) allow(described_class).to receive(:default_settings).and_return(default_settings)
allow_any_instance_of(Settings::ScopedSettings).to receive(:thing_scoped).and_return(records) settings_double = instance_double(Settings::ScopedSettings, thing_scoped: records)
allow(Settings::ScopedSettings).to receive(:new).and_return(settings_double)
Rails.cache.delete(cache_key) Rails.cache.delete(cache_key)
end end
@ -128,7 +129,8 @@ RSpec.describe Setting do
describe '.all_as_records' do describe '.all_as_records' do
before do before do
allow_any_instance_of(Settings::ScopedSettings).to receive(:thing_scoped).and_return(records) settings_double = instance_double(Settings::ScopedSettings, thing_scoped: records)
allow(Settings::ScopedSettings).to receive(:new).and_return(settings_double)
allow(described_class).to receive(:default_settings).and_return(default_settings) allow(described_class).to receive(:default_settings).and_return(default_settings)
end end

View File

@ -5,6 +5,37 @@ require 'rails_helper'
RSpec.describe Webhook do RSpec.describe Webhook do
let(:webhook) { Fabricate(:webhook) } let(:webhook) { Fabricate(:webhook) }
describe 'Validations' do
it 'requires presence of events' do
record = described_class.new(events: nil)
record.valid?
expect(record).to model_have_error_on_field(:events)
end
it 'requires non-empty events value' do
record = described_class.new(events: [])
record.valid?
expect(record).to model_have_error_on_field(:events)
end
it 'requires valid events value from EVENTS' do
record = described_class.new(events: ['account.invalid'])
record.valid?
expect(record).to model_have_error_on_field(:events)
end
end
describe 'Normalizations' do
it 'cleans up events values' do
record = described_class.new(events: ['account.approved', 'account.created ', ''])
expect(record.events).to eq(%w(account.approved account.created))
end
end
describe '#rotate_secret!' do describe '#rotate_secret!' do
it 'changes the secret' do it 'changes the secret' do
previous_value = webhook.secret previous_value = webhook.secret

View File

@ -102,17 +102,25 @@ describe 'GET /api/v1/accounts/relationships' do
end end
end end
it 'returns JSON with correct data on cached requests too' do it 'returns JSON with correct data on previously cached requests' do
subject # Initial request including multiple accounts in params
subject get '/api/v1/accounts/relationships', headers: headers, params: { id: [simon.id, lewis.id] }
expect(body_as_json.size).to eq(2)
# Subsequent request with different id, should override cache from first request
get '/api/v1/accounts/relationships', headers: headers, params: { id: [simon.id] }
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
json = body_as_json expect(body_as_json)
.to be_an(Enumerable)
expect(json).to be_a Enumerable .and have_attributes(
expect(json.first[:following]).to be true size: 1,
expect(json.first[:showing_reblogs]).to be true first: hash_including(
following: true,
showing_reblogs: true
)
)
end end
it 'returns JSON with correct data after change too' do it 'returns JSON with correct data after change too' do

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
require 'rails_helper'
describe 'API namespace minimal Content-Security-Policy' do
before { stub_tests_controller }
after { Rails.application.reload_routes! }
it 'returns the correct CSP headers' do
get '/api/v1/tests'
expect(response).to have_http_status(200)
expect(response.headers['Content-Security-Policy']).to eq(minimal_csp_headers)
end
private
def stub_tests_controller
stub_const('Api::V1::TestsController', api_tests_controller)
Rails.application.routes.draw do
get '/api/v1/tests', to: 'api/v1/tests#index'
end
end
def api_tests_controller
Class.new(Api::BaseController) do
def index
head 200
end
private
def user_signed_in? = false
def current_user = nil
end
end
def minimal_csp_headers
"default-src 'none'; frame-ancestors 'none'; form-action 'none'"
end
end

View File

@ -76,7 +76,8 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
let(:forwarder) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/other_account') } let(:forwarder) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/other_account') }
it 'does not process payload if no signature exists' do it 'does not process payload if no signature exists' do
allow_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_actor!).and_return(nil) signature_double = instance_double(ActivityPub::LinkedDataSignature, verify_actor!: nil)
allow(ActivityPub::LinkedDataSignature).to receive(:new).and_return(signature_double)
allow(ActivityPub::Activity).to receive(:factory) allow(ActivityPub::Activity).to receive(:factory)
subject.call(json, forwarder) subject.call(json, forwarder)
@ -87,7 +88,8 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
it 'processes payload with actor if valid signature exists' do it 'processes payload with actor if valid signature exists' do
payload['signature'] = { 'type' => 'RsaSignature2017' } payload['signature'] = { 'type' => 'RsaSignature2017' }
allow_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_actor!).and_return(actor) signature_double = instance_double(ActivityPub::LinkedDataSignature, verify_actor!: actor)
allow(ActivityPub::LinkedDataSignature).to receive(:new).and_return(signature_double)
allow(ActivityPub::Activity).to receive(:factory).with(instance_of(Hash), actor, instance_of(Hash)) allow(ActivityPub::Activity).to receive(:factory).with(instance_of(Hash), actor, instance_of(Hash))
subject.call(json, forwarder) subject.call(json, forwarder)
@ -98,7 +100,8 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
it 'does not process payload if invalid signature exists' do it 'does not process payload if invalid signature exists' do
payload['signature'] = { 'type' => 'RsaSignature2017' } payload['signature'] = { 'type' => 'RsaSignature2017' }
allow_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_actor!).and_return(nil) signature_double = instance_double(ActivityPub::LinkedDataSignature, verify_actor!: nil)
allow(ActivityPub::LinkedDataSignature).to receive(:new).and_return(signature_double)
allow(ActivityPub::Activity).to receive(:factory) allow(ActivityPub::Activity).to receive(:factory)
subject.call(json, forwarder) subject.call(json, forwarder)

View File

@ -11,7 +11,8 @@ describe ActivityPub::DeliveryWorker do
let(:payload) { 'test' } let(:payload) { 'test' }
before do before do
allow_any_instance_of(Account).to receive(:remote_followers_hash).with('https://example.com/api').and_return('somehash') allow(sender).to receive(:remote_followers_hash).with('https://example.com/api').and_return('somehash')
allow(Account).to receive(:find).with(sender.id).and_return(sender)
end end
describe 'perform' do describe 'perform' do

View File

@ -23,8 +23,8 @@ describe Web::PushNotificationWorker do
describe 'perform' do describe 'perform' do
before do before do
allow_any_instance_of(subscription.class).to receive(:contact_email).and_return(contact_email) allow(subscription).to receive_messages(contact_email: contact_email, vapid_key: vapid_key)
allow_any_instance_of(subscription.class).to receive(:vapid_key).and_return(vapid_key) allow(Web::PushSubscription).to receive(:find).with(subscription.id).and_return(subscription)
allow(Webpush::Encryption).to receive(:encrypt).and_return(payload) allow(Webpush::Encryption).to receive(:encrypt).and_return(payload)
allow(JWT).to receive(:encode).and_return('jwt.encoded.payload') allow(JWT).to receive(:encode).and_return('jwt.encoded.payload')

649
yarn.lock

File diff suppressed because it is too large Load Diff