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

master
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:
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.
RSpec/ExampleLength:
Max: 22

View File

@ -7,6 +7,7 @@ class Api::BaseController < ApplicationController
include RateLimitHeaders
include AccessTokenTrackingConcern
include ApiCachingConcern
include Api::ContentSecurityPolicy
skip_before_action :require_functional!, unless: :limited_federation_mode?
@ -17,26 +18,6 @@ class Api::BaseController < ApplicationController
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|
render json: { error: e.to_s }, status: 422
end

View File

@ -13,7 +13,7 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr
cache_if_unauthenticated!
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
private
@ -25,4 +25,16 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr
def set_domain_blocks
@domain_blocks = DomainBlock.with_user_facing_limitations.by_severity
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

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
class Api::V1::Statuses::BookmarksController < Api::BaseController
include Authorization
class Api::V1::Statuses::BookmarksController < Api::V1::Statuses::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:bookmarks' }
before_action :require_user!
before_action :set_status, only: [:create]
skip_before_action :set_status, only: [:destroy]
def create
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
not_found
end
private
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,7 @@
# frozen_string_literal: true
class Api::V1::Statuses::TranslationsController < Api::BaseController
include Authorization
class Api::V1::Statuses::TranslationsController < Api::V1::Statuses::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
before_action :set_status
before_action :set_translation
rescue_from TranslationService::NotConfiguredError, with: :not_found
@ -24,13 +21,6 @@ class Api::V1::Statuses::TranslationsController < Api::BaseController
private
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
def set_translation
@translation = TranslateStatusService.new.call(@status, content_locale)
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 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 { WithRouterPropTypes } from 'mastodon/utils/react_router';
@ -348,6 +350,7 @@ class StatusActionBar extends ImmutablePureComponent {
let replyIcon;
let replyIconComponent;
let replyTitle;
if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'reply';
replyIconComponent = ReplyIcon;
@ -360,15 +363,20 @@ class StatusActionBar extends ImmutablePureComponent {
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
let reblogTitle = '';
let reblogTitle, reblogIconComponent;
if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon;
} else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog);
reblogIconComponent = RepeatIcon;
} else if (reblogPrivate) {
reblogTitle = intl.formatMessage(messages.reblog_private);
reblogIconComponent = RepeatPrivateIcon;
} else {
reblogTitle = intl.formatMessage(messages.cannot_reblog);
reblogIconComponent = RepeatDisabledIcon;
}
const filterButton = this.props.onFilter && (
@ -380,7 +388,7 @@ class StatusActionBar extends ImmutablePureComponent {
return (
<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={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 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 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'>
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
</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 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 { WithRouterPropTypes } from 'mastodon/utils/react_router';
@ -280,6 +282,7 @@ class ActionBar extends PureComponent {
let replyIcon;
let replyIconComponent;
if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'reply';
replyIconComponent = ReplyIcon;
@ -290,21 +293,26 @@ class ActionBar extends PureComponent {
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
let reblogTitle;
let reblogTitle, reblogIconComponent;
if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon;
} else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog);
reblogIconComponent = RepeatIcon;
} else if (reblogPrivate) {
reblogTitle = intl.formatMessage(messages.reblog_private);
reblogIconComponent = RepeatPrivateIcon;
} else {
reblogTitle = intl.formatMessage(messages.cannot_reblog);
reblogIconComponent = RepeatDisabledIcon;
}
return (
<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 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='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.block": "Blokkeer @{name}",
"account.block_domain": "Blokkeer domein {domain}",
"account.block_short": "Blokkeer",
"account.blocked": "Geblokkeer",
"account.browse_more_on_origin_server": "Verken die oorspronklike profiel",
"account.cancel_follow_request": "Herroep volgversoek",
@ -45,6 +46,7 @@
"account.posts_with_replies": "Plasings en antwoorde",
"account.report": "Rapporteer @{name}",
"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.show_reblogs": "Wys aangestuurde plasings van @{name}",
"account.statuses_counter": "{count, plural, one {{counter} Plaas} other {{counter} Plasings}}",
@ -82,6 +84,7 @@
"column.community": "Plaaslike tydlyn",
"column.directory": "Blaai deur profiele",
"column.domain_blocks": "Geblokkeerde domeine",
"column.favourites": "Gunstelinge",
"column.follow_requests": "Volgversoeke",
"column.home": "Tuis",
"column.lists": "Lyste",
@ -271,6 +274,7 @@
"privacy.unlisted.short": "Ongelys",
"privacy_policy.last_updated": "Laaste bywerking op {date}",
"privacy_policy.title": "Privaatheidsbeleid",
"regeneration_indicator.sublabel": "Jou tuis-voer word voorberei!",
"reply_indicator.cancel": "Kanselleer",
"report.placeholder": "Type or paste additional comments",
"report.submit": "Submit report",

View File

@ -201,7 +201,7 @@
"disabled_account_banner.text": "Ваш уліковы запіс {disabledAccount} часова адключаны.",
"dismissable_banner.community_timeline": "Гэта самыя апошнія допісы ад людзей, уліковыя запісы якіх размяшчаюцца на {domain}.",
"dismissable_banner.dismiss": "Адхіліць",
"dismissable_banner.explore_links": "Гэтыя навіны абмяркоўваюцца прама зараз на гэтым і іншых серверах дэцэнтралізаванай сеткі.",
"dismissable_banner.explore_links": "Гэтыя навіны абмяркоўваюцца цяпер на гэтым і іншых серверах дэцэнтралізаванай сеткі.",
"dismissable_banner.explore_statuses": "Допісы з гэтага і іншых сервераў дэцэнтралізаванай сеткі, якія набіраюць папулярнасць прама зараз.",
"dismissable_banner.explore_tags": "Гэтыя хэштэгі зараз набіраюць папулярнасць сярод людзей на гэтым і іншых серверах дэцэнтралізаванай сеткі",
"dismissable_banner.public_timeline": "Гэта апошнія публічныя допісы людзей з усей сеткі, за якімі сочаць карыстальнікі {domain}.",
@ -482,7 +482,7 @@
"onboarding.share.lead": "Дайце людзям ведаць, як яны могуць знайсці вас на Mastodon!",
"onboarding.share.message": "Я {username} на #Mastodon! Сачыце за мной на {url}",
"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.skip": "Want to skip right ahead?",
"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.title": "Customize your profile",
"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.accounts_from_other_servers": "<strong>Ці вы ведаеце?</strong> Паколькі Mastodon дэцэнтралізаваны, некаторыя профілі, якія вам трапляюцца, будуць размяшчацца на іншых серверах, адрозных ад вашага. І ўсё ж вы можаце бесперашкодна ўзаемадзейнічаць з імі! Іх сервер пазначаны ў другой палове імя карыстальніка!",
"onboarding.tips.migration": "<strong>Ці вы ведаеце?</strong> Калі вы адчуваеце, што {domain} не з'яўляецца для вас лепшым выбарам у будучыні, вы можаце перайсці на іншы сервер Mastodon, не губляючы сваіх падпісчыкаў. Вы нават можаце стварыць свой уласны сервер!",

View File

@ -222,6 +222,7 @@
"emoji_button.search_results": "Resultats de la cerca",
"emoji_button.symbols": "Símbols",
"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_timeline": "No hi ha tuts aquí!",
"empty_column.account_unavailable": "Perfil no disponible",

View File

@ -222,6 +222,7 @@
"emoji_button.search_results": "Sykresultaten",
"emoji_button.symbols": "Symboalen",
"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_timeline": "Hjir binne gjin berjochten!",
"empty_column.account_unavailable": "Profyl net beskikber",

View File

@ -41,6 +41,8 @@
"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.media": "Medija",
"account.mention": "Paminėti @{name}",
"account.moved_to": "{name} nurodė, kad dabar jų nauja paskyra yra:",
"account.mute": "Užtildyti @{name}",
"account.muted": "Užtildytas",
"account.posts": "Toots",
@ -53,10 +55,15 @@
"account.unfollow": "Nebesekti",
"account.unmute_short": "Atitildyti",
"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",
"attachments_list.unprocessed": "(neapdorotas)",
"audio.hide": "Slėpti garsą",
"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!",
"column.domain_blocks": "Hidden domains",
"column.lists": "Sąrašai",

View File

@ -222,6 +222,7 @@
"emoji_button.search_results": "Rezultati iskanja",
"emoji_button.symbols": "Simboli",
"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_timeline": "Tukaj ni objav!",
"empty_column.account_unavailable": "Profil ni na voljo",

View File

@ -222,6 +222,7 @@
"emoji_button.search_results": "Rezultati pretrage",
"emoji_button.symbols": "Simboli",
"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_timeline": "Nema objava ovde!",
"empty_column.account_unavailable": "Profil je nedostupan",

View File

@ -222,6 +222,7 @@
"emoji_button.search_results": "Резултати претраге",
"emoji_button.symbols": "Симболи",
"emoji_button.travel": "Путовања и места",
"empty_column.account_hides_collections": "Овај корисник је одлучио да ове информације не учини доступним",
"empty_column.account_suspended": "Налог је суспендован",
"empty_column.account_timeline": "Нема објава овде!",
"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 :events, presence: true
validate :validate_events
validate :events_validation_error, if: :invalid_events?
validate :validate_permissions
validate :validate_template
before_validation :strip_events
normalizes :events, with: ->(events) { events.filter_map { |event| event.strip.presence } }
before_validation :generate_secret
def rotate_secret!
@ -69,8 +69,12 @@ class Webhook < ApplicationRecord
private
def validate_events
errors.add(:events, :invalid) if events.any? { |e| EVENTS.exclude?(e) }
def events_validation_error
errors.add(:events, :invalid)
end
def invalid_events?
events.blank? || events.difference(EVENTS).any?
end
def validate_permissions
@ -88,10 +92,6 @@ class Webhook < ApplicationRecord
end
end
def strip_events
self.events = events.filter_map { |str| str.strip.presence } if events.present?
end
def generate_secret
self.secret = SecureRandom.hex(20) if secret.blank?
end

View File

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

View File

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

View File

@ -53,3 +53,7 @@ af:
position:
elevated: kan nie hoër as jou huidige rol wees 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
hosted_on: Mastodon gehuisves op %{domain}
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:
account_actions:
action: Voer aksie uit
title: Voer modereer aksie uit op %{acct}
account_moderation_notes:
create: Los nota
accounts:
location:
local: Plaaslik
@ -102,6 +118,7 @@ af:
types:
bookmarks: Boekmerke
invites:
invalid: Hierdie uitnodiging is nie geldig nie
title: Nooi ander
login_activities:
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 дзень
expires_in_prompt: Ніколі
generate: Стварыць запрашальную спасылку
invalid: Гэта запрашэнне несапраўднае
invited_by: 'Вас запрасіў(-ла):'
max_uses:
few: "%{count} выкарыстанні"

View File

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

View File

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

View File

@ -1110,6 +1110,7 @@ da:
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.
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
too_fast: Formularen indsendt for hurtigt, forsøg igen.
use_security_key: Brug sikkerhedsnøgle
@ -1367,6 +1368,7 @@ da:
'86400': 1 dag
expires_in_prompt: Aldrig
generate: Generér invitationslink
invalid: Denne invitation er ikke gyldig
invited_by: 'Du blev inviteret af:'
max_uses:
one: 1 benyttelse
@ -1579,6 +1581,9 @@ da:
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
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:
activity: Seneste aktivitet
browser: Browser

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -179,7 +179,7 @@
"@types/react-dom": "^18.2.4",
"@types/react-helmet": "^6.1.6",
"@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-router": "^5.1.20",
"@types/react-router-dom": "^5.3.3",
@ -203,7 +203,7 @@
"eslint-plugin-formatjs": "^4.10.1",
"eslint-plugin-import": "~2.29.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-promise": "~6.1.1",
"eslint-plugin-react": "~7.33.0",
@ -245,5 +245,5 @@
"*.{js,jsx,ts,tsx}": "eslint --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
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
post :create, body: '{}'

View File

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

View File

@ -13,12 +13,20 @@ describe Admin::ResetsController do
describe 'POST #create' do
it 'redirects to admin accounts page' do
expect_any_instance_of(User).to receive(:send_reset_password_instructions) do |value|
expect(value.account_id).to eq account.id
end
post :create, params: { account_id: account.id }
expect do
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))
end
end

View File

@ -126,7 +126,7 @@ RSpec.describe Auth::SessionsController do
let!(:previous_login) { Fabricate(:login_activity, user: user, ip: previous_ip) }
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)
post :create, params: { user: { email: user.email, password: user.password } }
end
@ -279,7 +279,9 @@ RSpec.describe Auth::SessionsController do
context 'when the server has an decryption error' 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 }
end

View File

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

View File

@ -64,8 +64,11 @@ describe Request do
end
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(subject.send(:http_client)).to have_received(:close)
end
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
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
end
@ -74,7 +75,8 @@ describe StatusFilter do
context 'when status policy does not allow show' 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
end

View File

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

View File

@ -52,7 +52,8 @@ RSpec.describe Setting do
before do
allow(RailsSettings::Settings).to receive(:object).with(key).and_return(object)
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)
end
@ -128,7 +129,8 @@ RSpec.describe Setting do
describe '.all_as_records' 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)
end

View File

@ -5,6 +5,37 @@ require 'rails_helper'
RSpec.describe Webhook do
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
it 'changes the secret' do
previous_value = webhook.secret

View File

@ -102,17 +102,25 @@ describe 'GET /api/v1/accounts/relationships' do
end
end
it 'returns JSON with correct data on cached requests too' do
subject
subject
it 'returns JSON with correct data on previously cached requests' do
# Initial request including multiple accounts in params
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)
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
expect(body_as_json)
.to be_an(Enumerable)
.and have_attributes(
size: 1,
first: hash_including(
following: true,
showing_reblogs: true
)
)
end
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') }
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)
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
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))
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
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)
subject.call(json, forwarder)

View File

@ -11,7 +11,8 @@ describe ActivityPub::DeliveryWorker do
let(:payload) { 'test' }
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
describe 'perform' do

View File

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

649
yarn.lock

File diff suppressed because it is too large Load Diff