mirror of
https://github.com/mastodon/mastodon.git
synced 2025-09-05 17:31:12 +00:00
Merge branch 'main' into feature/require-mfa-by-admin
This commit is contained in:
commit
63d0efa0c2
18
Gemfile.lock
18
Gemfile.lock
|
@ -96,7 +96,7 @@ GEM
|
|||
ast (2.4.3)
|
||||
attr_required (1.0.2)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1131.0)
|
||||
aws-partitions (1.1135.0)
|
||||
aws-sdk-core (3.215.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
|
@ -233,7 +233,7 @@ GEM
|
|||
fabrication (3.0.0)
|
||||
faker (3.5.2)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (2.13.2)
|
||||
faraday (2.13.4)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
|
@ -345,7 +345,7 @@ GEM
|
|||
azure-blob (~> 0.5.2)
|
||||
hashie (~> 5.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.13.0)
|
||||
json (2.13.2)
|
||||
json-canonicalization (1.0.0)
|
||||
json-jwt (1.16.7)
|
||||
activesupport (>= 4.2)
|
||||
|
@ -438,7 +438,7 @@ GEM
|
|||
mime-types (3.7.0)
|
||||
logger
|
||||
mime-types-data (~> 3.2025, >= 3.2025.0507)
|
||||
mime-types-data (3.2025.0715)
|
||||
mime-types-data (3.2025.0722)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.9)
|
||||
minitest (5.25.5)
|
||||
|
@ -601,13 +601,13 @@ GEM
|
|||
ox (2.14.23)
|
||||
bigdecimal (>= 3.0)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.8.0)
|
||||
parser (3.3.9.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
parslet (2.0.0)
|
||||
pastel (0.8.0)
|
||||
tty-color (~> 0.5)
|
||||
pg (1.5.9)
|
||||
pg (1.6.0)
|
||||
pghero (3.7.0)
|
||||
activerecord (>= 7.1)
|
||||
playwright-ruby-client (1.54.0)
|
||||
|
@ -731,7 +731,7 @@ GEM
|
|||
railties (>= 5.2)
|
||||
rexml (3.4.1)
|
||||
rotp (6.3.0)
|
||||
rouge (4.5.2)
|
||||
rouge (4.6.0)
|
||||
rpam2 (4.0.2)
|
||||
rqrcode (3.1.0)
|
||||
chunky_png (~> 1.0)
|
||||
|
@ -868,7 +868,7 @@ GEM
|
|||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
sysexits (1.2.0)
|
||||
temple (0.10.3)
|
||||
temple (0.10.4)
|
||||
terminal-table (4.0.0)
|
||||
unicode-display_width (>= 1.1.1, < 4)
|
||||
terrapin (1.1.1)
|
||||
|
@ -1108,4 +1108,4 @@ RUBY VERSION
|
|||
ruby 3.4.1p0
|
||||
|
||||
BUNDLED WITH
|
||||
2.7.0
|
||||
2.7.1
|
||||
|
|
77
app/controllers/admin/username_blocks_controller.rb
Normal file
77
app/controllers/admin/username_blocks_controller.rb
Normal file
|
@ -0,0 +1,77 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::UsernameBlocksController < Admin::BaseController
|
||||
before_action :set_username_block, only: [:edit, :update]
|
||||
|
||||
def index
|
||||
authorize :username_block, :index?
|
||||
@username_blocks = UsernameBlock.order(username: :asc).page(params[:page])
|
||||
@form = Form::UsernameBlockBatch.new
|
||||
end
|
||||
|
||||
def batch
|
||||
authorize :username_block, :index?
|
||||
|
||||
@form = Form::UsernameBlockBatch.new(form_username_block_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||
@form.save
|
||||
rescue ActionController::ParameterMissing
|
||||
flash[:alert] = I18n.t('admin.username_blocks.no_username_block_selected')
|
||||
rescue Mastodon::NotPermittedError
|
||||
flash[:alert] = I18n.t('admin.username_blocks.not_permitted')
|
||||
ensure
|
||||
redirect_to admin_username_blocks_path
|
||||
end
|
||||
|
||||
def new
|
||||
authorize :username_block, :create?
|
||||
@username_block = UsernameBlock.new(exact: true)
|
||||
end
|
||||
|
||||
def edit
|
||||
authorize @username_block, :update?
|
||||
end
|
||||
|
||||
def create
|
||||
authorize :username_block, :create?
|
||||
|
||||
@username_block = UsernameBlock.new(resource_params)
|
||||
|
||||
if @username_block.save
|
||||
log_action :create, @username_block
|
||||
redirect_to admin_username_blocks_path, notice: I18n.t('admin.username_blocks.created_msg')
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @username_block, :update?
|
||||
|
||||
if @username_block.update(resource_params)
|
||||
log_action :update, @username_block
|
||||
redirect_to admin_username_blocks_path, notice: I18n.t('admin.username_blocks.updated_msg')
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_username_block
|
||||
@username_block = UsernameBlock.find(params[:id])
|
||||
end
|
||||
|
||||
def form_username_block_batch_params
|
||||
params
|
||||
.expect(form_username_block_batch: [username_block_ids: []])
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params
|
||||
.expect(username_block: [:username, :comparison, :allow_with_approval])
|
||||
end
|
||||
|
||||
def action_from_button
|
||||
'delete' if params[:delete]
|
||||
end
|
||||
end
|
|
@ -66,7 +66,11 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
add_async_refresh_header(async_refresh)
|
||||
elsif !current_account.nil? && @status.should_fetch_replies?
|
||||
add_async_refresh_header(AsyncRefresh.create(refresh_key))
|
||||
ActivityPub::FetchAllRepliesWorker.perform_async(@status.id)
|
||||
|
||||
WorkerBatch.new.within do |batch|
|
||||
batch.connect(refresh_key, threshold: 1.0)
|
||||
ActivityPub::FetchAllRepliesWorker.perform_async(@status.id, { 'batch_id' => batch.id })
|
||||
end
|
||||
end
|
||||
|
||||
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
|
||||
|
|
|
@ -13,6 +13,8 @@ module Admin::ActionLogsHelper
|
|||
end
|
||||
when 'UserRole'
|
||||
link_to log.human_identifier, admin_roles_path(log.target_id)
|
||||
when 'UsernameBlock'
|
||||
link_to log.human_identifier, edit_admin_username_block_path(log.target_id)
|
||||
when 'Report'
|
||||
link_to "##{log.human_identifier.presence || log.target_id}", admin_report_path(log.target_id)
|
||||
when 'Instance', 'DomainBlock', 'DomainAllow', 'UnavailableDomain'
|
||||
|
|
|
@ -2,8 +2,6 @@ import { useEffect, useState, useCallback } from 'react';
|
|||
|
||||
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {
|
||||
fetchContext,
|
||||
completeContextRefresh,
|
||||
|
@ -22,8 +20,7 @@ const messages = defineMessages({
|
|||
|
||||
export const RefreshController: React.FC<{
|
||||
statusId: string;
|
||||
withBorder?: boolean;
|
||||
}> = ({ statusId, withBorder }) => {
|
||||
}> = ({ statusId }) => {
|
||||
const refresh = useAppSelector(
|
||||
(state) => state.contexts.refreshing[statusId],
|
||||
);
|
||||
|
@ -78,12 +75,7 @@ export const RefreshController: React.FC<{
|
|||
|
||||
if (ready && !loading) {
|
||||
return (
|
||||
<button
|
||||
className={classNames('load-more load-gap', {
|
||||
'timeline-hint--with-descendants': withBorder,
|
||||
})}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<button className='load-more load-gap' onClick={handleClick}>
|
||||
<FormattedMessage
|
||||
id='status.context.load_new_replies'
|
||||
defaultMessage='New replies available'
|
||||
|
@ -98,9 +90,7 @@ export const RefreshController: React.FC<{
|
|||
|
||||
return (
|
||||
<div
|
||||
className={classNames('load-more load-gap', {
|
||||
'timeline-hint--with-descendants': withBorder,
|
||||
})}
|
||||
className='load-more load-gap'
|
||||
aria-busy
|
||||
aria-live='polite'
|
||||
aria-label={intl.formatMessage(messages.loading)}
|
||||
|
|
|
@ -580,7 +580,6 @@ class Status extends ImmutablePureComponent {
|
|||
remoteHint = (
|
||||
<RefreshController
|
||||
statusId={status.get('id')}
|
||||
withBorder={!!descendants}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -653,8 +652,8 @@ class Status extends ImmutablePureComponent {
|
|||
</div>
|
||||
</Hotkeys>
|
||||
|
||||
{descendants}
|
||||
{remoteHint}
|
||||
{descendants}
|
||||
</div>
|
||||
</ScrollContainer>
|
||||
|
||||
|
|
|
@ -235,7 +235,7 @@
|
|||
"confirmations.logout.message": "مطمئنید میخواهید خارج شوید؟",
|
||||
"confirmations.logout.title": "خروج؟",
|
||||
"confirmations.missing_alt_text.confirm": "متن جایگزین را اضافه کنید",
|
||||
"confirmations.missing_alt_text.message": "پست شما حاوی رسانه بدون متن جایگزین است. افزودن توضیحات کمک می کند تا محتوای شما برای افراد بیشتری قابل دسترسی باشد.",
|
||||
"confirmations.missing_alt_text.message": "فرستهتان رسانههایی بدون متن جایگزین دارد. افزودن شرح به دسترسپذیر شدن محتوایتان برای افراد بیشتری کمک میکند.",
|
||||
"confirmations.missing_alt_text.secondary": "به هر حال پست کن",
|
||||
"confirmations.missing_alt_text.title": "متن جایگزین اضافه شود؟",
|
||||
"confirmations.mute.confirm": "خموش",
|
||||
|
@ -424,7 +424,7 @@
|
|||
"hints.profiles.see_more_followers": "دیدن پیگیرندگان بیشتر روی {domain}",
|
||||
"hints.profiles.see_more_follows": "دیدن پیگرفتههای بیشتر روی {domain}",
|
||||
"hints.profiles.see_more_posts": "دیدن فرستههای بیشتر روی {domain}",
|
||||
"home.column_settings.show_quotes": "نمایش نقلقولها",
|
||||
"home.column_settings.show_quotes": "نمایش نقلها",
|
||||
"home.column_settings.show_reblogs": "نمایش تقویتها",
|
||||
"home.column_settings.show_replies": "نمایش پاسخها",
|
||||
"home.hide_announcements": "نهفتن اعلامیهها",
|
||||
|
@ -845,6 +845,8 @@
|
|||
"status.bookmark": "نشانک",
|
||||
"status.cancel_reblog_private": "ناتقویت",
|
||||
"status.cannot_reblog": "این فرسته قابل تقویت نیست",
|
||||
"status.context.load_new_replies": "پاسخهای جدیدی موجودند",
|
||||
"status.context.loading": "بررسی کردن برای پاسخهای بیشتر",
|
||||
"status.continued_thread": "رشتهٔ دنباله دار",
|
||||
"status.copy": "رونوشت از پیوند فرسته",
|
||||
"status.delete": "حذف",
|
||||
|
@ -873,7 +875,7 @@
|
|||
"status.quote_error.filtered": "نهفته بنا بر یکی از پالایههایتان",
|
||||
"status.quote_error.not_found": "این فرسته قابل نمایش نیست.",
|
||||
"status.quote_error.pending_approval": "این فرسته منظر تأیید نگارندهٔ اصلی است.",
|
||||
"status.quote_error.rejected": "از آنجا که نگارندهٔ اصلی فرسته اجازهٔ نقل قولش را نمیدهد این فرسته قابل نمایش نیست.",
|
||||
"status.quote_error.rejected": "از آنجا که نگارندهٔ اصلی این فرسته اجازهٔ نقلش را نمیدهد قابل نمایش نیست.",
|
||||
"status.quote_error.removed": "این فرسته به دست نگارندهاش برداشته شده.",
|
||||
"status.quote_error.unauthorized": "از آنجا که اجازهٔ دیدن این فرسته را ندارید قابل نمایش نیست.",
|
||||
"status.quote_post_author": "فرسته توسط {name}",
|
||||
|
|
|
@ -224,6 +224,8 @@
|
|||
"confirmations.discard_draft.edit.message": "Continuar vai descartar quaisquer mudanças feitas ao post sendo editado.",
|
||||
"confirmations.discard_draft.edit.title": "Descartar mudanças no seu post?",
|
||||
"confirmations.discard_draft.post.cancel": "Continuar rascunho",
|
||||
"confirmations.discard_draft.post.message": "Continuar eliminará a publicação que está sendo elaborada no momento.",
|
||||
"confirmations.discard_draft.post.title": "Eliminar seu esboço de publicação?",
|
||||
"confirmations.discard_edit_media.confirm": "Descartar",
|
||||
"confirmations.discard_edit_media.message": "Há mudanças não salvas na descrição ou pré-visualização da mídia. Descartar assim mesmo?",
|
||||
"confirmations.follow_to_list.confirm": "Seguir e adicionar à lista",
|
||||
|
@ -333,9 +335,13 @@
|
|||
"errors.unexpected_crash.copy_stacktrace": "Copiar dados do erro para área de transferência",
|
||||
"errors.unexpected_crash.report_issue": "Reportar problema",
|
||||
"explore.suggested_follows": "Pessoas",
|
||||
"explore.title": "Em alta",
|
||||
"explore.trending_links": "Notícias",
|
||||
"explore.trending_statuses": "Publicações",
|
||||
"explore.trending_tags": "Hashtags",
|
||||
"featured_carousel.header": "{count, plural, one {Postagem fixada} other {Postagens fixadas}}",
|
||||
"featured_carousel.next": "Próximo",
|
||||
"featured_carousel.previous": "Anterior",
|
||||
"filter_modal.added.context_mismatch_explanation": "Esta categoria de filtro não se aplica ao contexto no qual você acessou esta publicação. Se quiser que a publicação seja filtrada nesse contexto também, você terá que editar o filtro.",
|
||||
"filter_modal.added.context_mismatch_title": "Incompatibilidade de contexto!",
|
||||
"filter_modal.added.expired_explanation": "Esta categoria de filtro expirou, você precisará alterar a data de expiração para aplicar.",
|
||||
|
@ -550,6 +556,7 @@
|
|||
"navigation_bar.lists": "Listas",
|
||||
"navigation_bar.logout": "Sair",
|
||||
"navigation_bar.moderation": "Moderação",
|
||||
"navigation_bar.more": "Mais",
|
||||
"navigation_bar.mutes": "Usuários silenciados",
|
||||
"navigation_bar.opened_in_classic_interface": "Publicações, contas e outras páginas específicas são abertas por padrão na interface 'web' clássica.",
|
||||
"navigation_bar.preferences": "Preferências",
|
||||
|
|
|
@ -845,6 +845,8 @@
|
|||
"status.bookmark": "Bokmärk",
|
||||
"status.cancel_reblog_private": "Sluta boosta",
|
||||
"status.cannot_reblog": "Detta inlägg kan inte boostas",
|
||||
"status.context.load_new_replies": "Nya svar finns",
|
||||
"status.context.loading": "Letar efter fler svar",
|
||||
"status.continued_thread": "Fortsatt tråd",
|
||||
"status.copy": "Kopiera inläggslänk",
|
||||
"status.delete": "Radera",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m791-55-91-91q-48 32-103.5 49T480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-61 17-116.5T146-700l-91-91 57-57 736 736-57 57ZM477-161q45 0 86-11.5t77-33.5l-91-91q-38 17-74.5 47.5T412-168q16 3 32 5t33 2Zm-124-25q35-72 79.5-107t67.5-47q-29-9-58.5-14.5T380-360q-45 0-89 11t-85 31q26 43 63.5 77.5T353-186Zm461-74L690-384q31-10 50.5-36t19.5-60q0-42-29-71t-71-29q-34 0-60 19.5T564-510l-44-44q2-61-41-104.5T374-700L260-814q48-32 103.5-49T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 61-17 116.5T814-260ZM380-420q11 0 20.5-1.5T420-426L246-600q-3 10-4.5 19.5T240-560q0 58 41 99t99 41Z"/></svg>
|
After Width: | Height: | Size: 698 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m791-55-91-91q-48 32-103.5 49T480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-61 17-116.5T146-700l-91-91 57-57 736 736-57 57ZM412-168q26-51 62-81.5t75-47.5L204-642q-21 36-32.5 76.5T160-480q0 45 11.5 86t34.5 76q41-20 85-31t89-11q32 0 61.5 5.5T500-340q-23 12-43.5 28T418-278q-12-2-20.5-2H380q-32 0-63.5 7T256-252q32 32 71.5 53.5T412-168Zm402-92-58-58q21-35 32.5-76t11.5-86q0-134-93-227t-227-93q-45 0-85.5 11.5T318-756l-58-58q48-32 103.5-49T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 61-17 116.5T814-260ZM520-554 374-700q62-2 105 41.5T520-554ZM380-420q-58 0-99-41t-41-99q0-33 14.5-60.5T292-668l196 196q-20 23-47.5 37.5T380-420Zm310 36L564-510q10-31 36-50.5t60-19.5q42 0 71 29t29 71q0 34-19.5 60T690-384ZM537-537ZM423-423Z"/></svg>
|
After Width: | Height: | Size: 843 B |
|
@ -33,9 +33,7 @@ class Antispam
|
|||
end
|
||||
|
||||
def local_preflight_check!(status)
|
||||
return unless spammy_texts.any? { |spammy_text| status.text.include?(spammy_text) }
|
||||
return unless suspicious_reply_or_mention?(status)
|
||||
return unless status.account.created_at >= ACCOUNT_AGE_EXEMPTION.ago
|
||||
return unless considered_spam?(status)
|
||||
|
||||
report_if_needed!(status.account)
|
||||
|
||||
|
@ -44,10 +42,26 @@ class Antispam
|
|||
|
||||
private
|
||||
|
||||
def considered_spam?(status)
|
||||
(all_time_suspicious?(status) || recent_suspicious?(status)) && suspicious_reply_or_mention?(status)
|
||||
end
|
||||
|
||||
def all_time_suspicious?(status)
|
||||
all_time_spammy_texts.any? { |spammy_text| status.text.include?(spammy_text) }
|
||||
end
|
||||
|
||||
def recent_suspicious?(status)
|
||||
status.account.created_at >= ACCOUNT_AGE_EXEMPTION.ago && spammy_texts.any? { |spammy_text| status.text.include?(spammy_text) }
|
||||
end
|
||||
|
||||
def spammy_texts
|
||||
redis.smembers('antispam:spammy_texts')
|
||||
end
|
||||
|
||||
def all_time_spammy_texts
|
||||
redis.smembers('antispam:all_time_spammy_texts')
|
||||
end
|
||||
|
||||
def suspicious_reply_or_mention?(status)
|
||||
parent = status.thread
|
||||
return true if parent.present? && !Follow.exists?(account_id: parent.account_id, target_account: status.account_id)
|
||||
|
|
|
@ -77,6 +77,9 @@ class Admin::ActionLogFilter
|
|||
update_user_role: { target_type: 'UserRole', action: 'update' }.freeze,
|
||||
update_ip_block: { target_type: 'IpBlock', action: 'update' }.freeze,
|
||||
unblock_email_account: { target_type: 'Account', action: 'unblock_email' }.freeze,
|
||||
create_username_block: { target_type: 'UsernameBlock', action: 'create' }.freeze,
|
||||
update_username_block: { target_type: 'UsernameBlock', action: 'update' }.freeze,
|
||||
destroy_username_block: { target_type: 'UsernameBlock', action: 'destroy' }.freeze,
|
||||
}.freeze
|
||||
|
||||
attr_reader :params
|
||||
|
|
|
@ -33,7 +33,7 @@ module Status::FetchRepliesConcern
|
|||
|
||||
def should_fetch_replies?
|
||||
# we aren't brand new, and we haven't fetched replies since the debounce window
|
||||
!local? && created_at <= FETCH_REPLIES_INITIAL_WAIT_MINUTES.ago && (
|
||||
!local? && distributable? && created_at <= FETCH_REPLIES_INITIAL_WAIT_MINUTES.ago && (
|
||||
fetched_replies_at.nil? || fetched_replies_at <= FETCH_REPLIES_COOLDOWN_MINUTES.ago
|
||||
)
|
||||
end
|
||||
|
|
31
app/models/form/username_block_batch.rb
Normal file
31
app/models/form/username_block_batch.rb
Normal file
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Form::UsernameBlockBatch < Form::BaseBatch
|
||||
attr_accessor :username_block_ids
|
||||
|
||||
def save
|
||||
case action
|
||||
when 'delete'
|
||||
delete!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def username_blocks
|
||||
@username_blocks ||= UsernameBlock.where(id: username_block_ids)
|
||||
end
|
||||
|
||||
def delete!
|
||||
verify_authorization(:destroy?)
|
||||
|
||||
username_blocks.each do |username_block|
|
||||
username_block.destroy
|
||||
log_action :destroy, username_block
|
||||
end
|
||||
end
|
||||
|
||||
def verify_authorization(permission)
|
||||
username_blocks.each { |username_block| authorize(username_block, permission) }
|
||||
end
|
||||
end
|
|
@ -443,7 +443,7 @@ class User < ApplicationRecord
|
|||
|
||||
def set_approved
|
||||
self.approved = begin
|
||||
if sign_up_from_ip_requires_approval? || sign_up_email_requires_approval?
|
||||
if sign_up_from_ip_requires_approval? || sign_up_email_requires_approval? || sign_up_username_requires_approval?
|
||||
false
|
||||
else
|
||||
open_registrations? || valid_invitation? || external?
|
||||
|
@ -499,6 +499,10 @@ class User < ApplicationRecord
|
|||
EmailDomainBlock.requires_approval?(records + [domain], attempt_ip: sign_up_ip)
|
||||
end
|
||||
|
||||
def sign_up_username_requires_approval?
|
||||
account.username? && UsernameBlock.matches?(account.username, allow_with_approval: true)
|
||||
end
|
||||
|
||||
def open_registrations?
|
||||
Setting.registrations_mode == 'open'
|
||||
end
|
||||
|
|
62
app/models/username_block.rb
Normal file
62
app/models/username_block.rb
Normal file
|
@ -0,0 +1,62 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: username_blocks
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# allow_with_approval :boolean default(FALSE), not null
|
||||
# exact :boolean default(FALSE), not null
|
||||
# normalized_username :string not null
|
||||
# username :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class UsernameBlock < ApplicationRecord
|
||||
HOMOGLYPHS = {
|
||||
'1' => 'i',
|
||||
'2' => 'z',
|
||||
'3' => 'e',
|
||||
'4' => 'a',
|
||||
'5' => 's',
|
||||
'7' => 't',
|
||||
'8' => 'b',
|
||||
'9' => 'g',
|
||||
'0' => 'o',
|
||||
}.freeze
|
||||
|
||||
validates :username, presence: true, uniqueness: true
|
||||
|
||||
scope :matches_exactly, ->(str) { where(exact: true).where(normalized_username: str) }
|
||||
scope :matches_partially, ->(str) { where(exact: false).where(Arel::Nodes.build_quoted(str).matches(Arel::Nodes.build_quoted('%').concat(arel_table[:normalized_username]).concat(Arel::Nodes.build_quoted('%')))) }
|
||||
|
||||
before_save :set_normalized_username
|
||||
|
||||
def comparison
|
||||
exact? ? 'equals' : 'contains'
|
||||
end
|
||||
|
||||
def comparison=(val)
|
||||
self.exact = val == 'equals'
|
||||
end
|
||||
|
||||
def self.matches?(str, allow_with_approval: false)
|
||||
normalized_str = str.downcase.gsub(Regexp.union(HOMOGLYPHS.keys), HOMOGLYPHS)
|
||||
where(allow_with_approval: allow_with_approval).matches_exactly(normalized_str).or(matches_partially(normalized_str)).any?
|
||||
end
|
||||
|
||||
def to_log_human_identifier
|
||||
username
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_normalized_username
|
||||
self.normalized_username = normalize(username)
|
||||
end
|
||||
|
||||
def normalize(str)
|
||||
str.downcase.gsub(Regexp.union(HOMOGLYPHS.keys), HOMOGLYPHS)
|
||||
end
|
||||
end
|
|
@ -24,7 +24,7 @@ class WorkerBatch
|
|||
|
||||
begin
|
||||
Thread.current[:batch] = self
|
||||
yield
|
||||
yield(self)
|
||||
ensure
|
||||
Thread.current[:batch] = nil
|
||||
end
|
||||
|
@ -33,10 +33,7 @@ class WorkerBatch
|
|||
# Add jobs to the batch. Usually when the batch is created.
|
||||
# @param [Array<String>] jids
|
||||
def add_jobs(jids)
|
||||
if jids.blank?
|
||||
finish!
|
||||
return
|
||||
end
|
||||
return if jids.empty?
|
||||
|
||||
redis.multi do |pipeline|
|
||||
pipeline.sadd(key('jobs'), jids)
|
||||
|
@ -48,7 +45,7 @@ class WorkerBatch
|
|||
|
||||
# Remove a job from the batch, such as when it's been processed or it has failed.
|
||||
# @param [String] jid
|
||||
def remove_job(jid)
|
||||
def remove_job(jid, increment: false)
|
||||
_, pending, processed, async_refresh_key, threshold = redis.multi do |pipeline|
|
||||
pipeline.srem(key('jobs'), jid)
|
||||
pipeline.hincrby(key, 'pending', -1)
|
||||
|
@ -57,10 +54,8 @@ class WorkerBatch
|
|||
pipeline.hget(key, 'threshold')
|
||||
end
|
||||
|
||||
if async_refresh_key.present?
|
||||
async_refresh = AsyncRefresh.new(async_refresh_key)
|
||||
async_refresh.increment_result_count(by: 1)
|
||||
end
|
||||
async_refresh = AsyncRefresh.new(async_refresh_key) if async_refresh_key.present?
|
||||
async_refresh&.increment_result_count(by: 1) if increment
|
||||
|
||||
if pending.zero? || processed >= (threshold || 1.0).to_f * (processed + pending)
|
||||
async_refresh&.finish!
|
||||
|
|
19
app/policies/username_block_policy.rb
Normal file
19
app/policies/username_block_policy.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UsernameBlockPolicy < ApplicationPolicy
|
||||
def index?
|
||||
role.can?(:manage_blocks)
|
||||
end
|
||||
|
||||
def create?
|
||||
role.can?(:manage_blocks)
|
||||
end
|
||||
|
||||
def update?
|
||||
role.can?(:manage_blocks)
|
||||
end
|
||||
|
||||
def destroy?
|
||||
role.can?(:manage_blocks)
|
||||
end
|
||||
end
|
|
@ -6,7 +6,7 @@ class ActivityPub::FetchAllRepliesService < ActivityPub::FetchRepliesService
|
|||
# Limit of replies to fetch per status
|
||||
MAX_REPLIES = (ENV['FETCH_REPLIES_MAX_SINGLE'] || 500).to_i
|
||||
|
||||
def call(status_uri, collection_or_uri, max_pages: 1, async_refresh_key: nil, request_id: nil)
|
||||
def call(status_uri, collection_or_uri, max_pages: 1, batch_id: nil, request_id: nil)
|
||||
@status_uri = status_uri
|
||||
|
||||
super
|
||||
|
|
|
@ -6,7 +6,7 @@ class ActivityPub::FetchRepliesService < BaseService
|
|||
# Limit of fetched replies
|
||||
MAX_REPLIES = 5
|
||||
|
||||
def call(reference_uri, collection_or_uri, max_pages: 1, allow_synchronous_requests: true, async_refresh_key: nil, request_id: nil)
|
||||
def call(reference_uri, collection_or_uri, max_pages: 1, allow_synchronous_requests: true, batch_id: nil, request_id: nil)
|
||||
@reference_uri = reference_uri
|
||||
@allow_synchronous_requests = allow_synchronous_requests
|
||||
|
||||
|
@ -15,10 +15,7 @@ class ActivityPub::FetchRepliesService < BaseService
|
|||
|
||||
@items = filter_replies(@items)
|
||||
|
||||
batch = WorkerBatch.new
|
||||
batch.connect(async_refresh_key) if async_refresh_key.present?
|
||||
batch.finish! if @items.empty?
|
||||
batch.within do
|
||||
WorkerBatch.new(batch_id).within do |batch|
|
||||
FetchReplyWorker.push_bulk(@items) do |reply_uri|
|
||||
[reply_uri, { 'request_id' => request_id, 'batch_id' => batch.id }]
|
||||
end
|
||||
|
|
|
@ -28,14 +28,6 @@ class UnreservedUsernameValidator < ActiveModel::Validator
|
|||
end
|
||||
|
||||
def settings_username_reserved?
|
||||
settings_has_reserved_usernames? && settings_reserves_username?
|
||||
end
|
||||
|
||||
def settings_has_reserved_usernames?
|
||||
Setting.reserved_usernames.present?
|
||||
end
|
||||
|
||||
def settings_reserves_username?
|
||||
Setting.reserved_usernames.include?(@username.downcase)
|
||||
UsernameBlock.matches?(@username, allow_with_approval: false)
|
||||
end
|
||||
end
|
||||
|
|
16
app/views/admin/username_blocks/_form.html.haml
Normal file
16
app/views/admin/username_blocks/_form.html.haml
Normal file
|
@ -0,0 +1,16 @@
|
|||
.fields-group
|
||||
= form.input :username,
|
||||
wrapper: :with_block_label,
|
||||
input_html: { autocomplete: 'new-password', pattern: '[a-zA-Z0-9_]+', maxlength: Account::USERNAME_LENGTH_LIMIT }
|
||||
|
||||
.fields-group
|
||||
= form.input :comparison,
|
||||
as: :select,
|
||||
wrapper: :with_block_label,
|
||||
collection: %w(equals contains),
|
||||
include_blank: false,
|
||||
label_method: ->(type) { I18n.t(type, scope: 'admin.username_blocks.comparison') }
|
||||
|
||||
.fields-group
|
||||
= form.input :allow_with_approval,
|
||||
wrapper: :with_label
|
12
app/views/admin/username_blocks/_username_block.html.haml
Normal file
12
app/views/admin/username_blocks/_username_block.html.haml
Normal file
|
@ -0,0 +1,12 @@
|
|||
.batch-table__row
|
||||
%label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
|
||||
= f.check_box :username_block_ids, { multiple: true, include_hidden: false }, username_block.id
|
||||
.sr-only= username_block.username
|
||||
.batch-table__row__content.pending-account
|
||||
.pending-account__header
|
||||
= t(username_block.exact? ? 'admin.username_blocks.matches_exactly_html' : 'admin.username_blocks.contains_html', string: content_tag(:samp, link_to(username_block.username, edit_admin_username_block_path(username_block))))
|
||||
%br/
|
||||
- if username_block.allow_with_approval?
|
||||
= t('admin.email_domain_blocks.allow_registrations_with_approval')
|
||||
- else
|
||||
= t('admin.username_blocks.block_registrations')
|
10
app/views/admin/username_blocks/edit.html.haml
Normal file
10
app/views/admin/username_blocks/edit.html.haml
Normal file
|
@ -0,0 +1,10 @@
|
|||
- content_for :page_title do
|
||||
= t('admin.username_blocks.edit.title')
|
||||
|
||||
= simple_form_for @username_block, url: admin_username_block_path(@username_block) do |form|
|
||||
= render 'shared/error_messages', object: @username_block
|
||||
|
||||
= render form
|
||||
|
||||
.actions
|
||||
= form.button :button, t('generic.save_changes'), type: :submit
|
26
app/views/admin/username_blocks/index.html.haml
Normal file
26
app/views/admin/username_blocks/index.html.haml
Normal file
|
@ -0,0 +1,26 @@
|
|||
- content_for :page_title do
|
||||
= t('admin.username_blocks.title')
|
||||
|
||||
- content_for :heading_actions do
|
||||
= link_to t('admin.username_blocks.add_new'), new_admin_username_block_path, class: 'button'
|
||||
|
||||
= form_with model: @form, url: batch_admin_username_blocks_path do |f|
|
||||
= hidden_field_tag :page, params[:page] || 1
|
||||
|
||||
.batch-table
|
||||
.batch-table__toolbar
|
||||
%label.batch-table__toolbar__select.batch-checkbox-all
|
||||
= check_box_tag :batch_checkbox_all, nil, false
|
||||
.batch-table__toolbar__actions
|
||||
= f.button safe_join([material_symbol('close'), t('admin.username_blocks.delete')]),
|
||||
class: 'table-action-link',
|
||||
data: { confirm: t('admin.reports.are_you_sure') },
|
||||
name: :delete,
|
||||
type: :submit
|
||||
.batch-table__body
|
||||
- if @username_blocks.empty?
|
||||
= nothing_here 'nothing-here--under-tabs'
|
||||
- else
|
||||
= render partial: 'username_block', collection: @username_blocks, locals: { f: f }
|
||||
|
||||
= paginate @username_blocks
|
10
app/views/admin/username_blocks/new.html.haml
Normal file
10
app/views/admin/username_blocks/new.html.haml
Normal file
|
@ -0,0 +1,10 @@
|
|||
- content_for :page_title do
|
||||
= t('admin.username_blocks.new.title')
|
||||
|
||||
= simple_form_for @username_block, url: admin_username_blocks_path do |form|
|
||||
= render 'shared/error_messages', object: @username_block
|
||||
|
||||
= render form
|
||||
|
||||
.actions
|
||||
= form.button :button, t('admin.username_blocks.new.create'), type: :submit
|
|
@ -16,7 +16,9 @@ class ActivityPub::FetchAllRepliesWorker
|
|||
MAX_PAGES = (ENV['FETCH_REPLIES_MAX_PAGES'] || 500).to_i
|
||||
|
||||
def perform(root_status_id, options = {})
|
||||
@batch = WorkerBatch.new(options['batch_id'])
|
||||
@root_status = Status.remote.find_by(id: root_status_id)
|
||||
|
||||
return unless @root_status&.should_fetch_replies?
|
||||
|
||||
@root_status.touch(:fetched_replies_at)
|
||||
|
@ -45,6 +47,8 @@ class ActivityPub::FetchAllRepliesWorker
|
|||
|
||||
# Workers shouldn't be returning anything, but this is used in tests
|
||||
fetched_uris
|
||||
ensure
|
||||
@batch.remove_job(jid)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -53,9 +57,10 @@ class ActivityPub::FetchAllRepliesWorker
|
|||
# status URI, or the prefetched body of the Note object
|
||||
def get_replies(status, max_pages, options = {})
|
||||
replies_collection_or_uri = get_replies_uri(status)
|
||||
|
||||
return if replies_collection_or_uri.nil?
|
||||
|
||||
ActivityPub::FetchAllRepliesService.new.call(value_or_id(status), replies_collection_or_uri, max_pages: max_pages, async_refresh_key: "context:#{@root_status.id}:refresh", **options.deep_symbolize_keys)
|
||||
ActivityPub::FetchAllRepliesService.new.call(value_or_id(status), replies_collection_or_uri, max_pages: max_pages, **options.deep_symbolize_keys)
|
||||
end
|
||||
|
||||
# Get the URI of the replies collection of a status
|
||||
|
@ -78,9 +83,12 @@ class ActivityPub::FetchAllRepliesWorker
|
|||
# @param root_status_uri [String]
|
||||
def get_root_replies(root_status_uri, options = {})
|
||||
root_status_body = fetch_resource(root_status_uri, true)
|
||||
|
||||
return if root_status_body.nil?
|
||||
|
||||
FetchReplyWorker.perform_async(root_status_uri, { **options.deep_stringify_keys, 'prefetched_body' => root_status_body })
|
||||
@batch.within do
|
||||
FetchReplyWorker.perform_async(root_status_uri, { **options.deep_stringify_keys, 'prefetched_body' => root_status_body })
|
||||
end
|
||||
|
||||
get_replies(root_status_body, MAX_PAGES, options)
|
||||
end
|
||||
|
|
|
@ -7,9 +7,9 @@ class FetchReplyWorker
|
|||
sidekiq_options queue: 'pull', retry: 3
|
||||
|
||||
def perform(child_url, options = {})
|
||||
batch = WorkerBatch.new(options.delete('batch_id')) if options['batch_id']
|
||||
FetchRemoteStatusService.new.call(child_url, **options.symbolize_keys)
|
||||
batch = WorkerBatch.new(options.delete('batch_id')) if options['batch_id']
|
||||
result = FetchRemoteStatusService.new.call(child_url, **options.symbolize_keys)
|
||||
ensure
|
||||
batch&.remove_job(jid)
|
||||
batch&.remove_job(jid, increment: result.present?)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -115,8 +115,10 @@ class MoveWorker
|
|||
|
||||
def carry_mutes_over!
|
||||
@source_account.muted_by_relationships.where(account: Account.local).find_each do |mute|
|
||||
MuteService.new.call(mute.account, @target_account, notifications: mute.hide_notifications) unless skip_mute_move?(mute)
|
||||
add_account_note_if_needed!(mute.account, 'move_handler.carry_mutes_over_text')
|
||||
unless skip_mute_move?(mute)
|
||||
MuteService.new.call(mute.account, @target_account, notifications: mute.hide_notifications)
|
||||
add_account_note_if_needed!(mute.account, 'move_handler.carry_mutes_over_text')
|
||||
end
|
||||
rescue => e
|
||||
@deferred_error = e
|
||||
end
|
||||
|
|
|
@ -72,6 +72,8 @@ ignore_unused:
|
|||
- 'preferences.other' # some locales are missing other keys, therefore leading i18n-tasks to detect `preferences` as plural and not finding use
|
||||
- 'edit_profile.other' # some locales are missing other keys, therefore leading i18n-tasks to detect `preferences` as plural and not finding use
|
||||
- 'admin.terms_of_service.generate' # temporarily disabled
|
||||
- 'admin.username_blocks.matches_exactly_html'
|
||||
- 'admin.username_blocks.contains_html'
|
||||
|
||||
ignore_inconsistent_interpolations:
|
||||
- '*.one'
|
||||
|
|
|
@ -53,7 +53,7 @@ ru:
|
|||
subtitle: Двухфакторная аутентификация отключена для вашей учетной записи.
|
||||
title: 2FA отключена
|
||||
two_factor_enabled:
|
||||
explanation: Для входа потребуется одноразовый код, сгенерированный сопряжённым приложением TOTP.
|
||||
explanation: Для входа потребуется одноразовый код, сгенерированный сопряжённым приложением-аутентификатором.
|
||||
subject: 'Mastodon: Двухфакторная аутентификация включена'
|
||||
subtitle: Двухфакторная аутентификация включена для вашей учётной записи.
|
||||
title: 2FA включена
|
||||
|
@ -75,7 +75,7 @@ ru:
|
|||
title: Один из ваших электронных ключей удалён
|
||||
webauthn_disabled:
|
||||
explanation: Аутентификация по электронным ключам деактивирована для вашей учетной записи.
|
||||
extra: Теперь вход возможен с использованием только лишь одноразового кода, сгенерированного сопряжённым приложением TOTP.
|
||||
extra: Теперь вход возможен с использованием только с помощью одноразового кода, сгенерированного сопряжённым приложением-аутентификатором.
|
||||
subject: 'Mastodon: Аутентификация по электронным ключам деактивирована'
|
||||
title: Вход по электронным ключам деактивирован
|
||||
webauthn_enabled:
|
||||
|
|
|
@ -190,6 +190,7 @@ en:
|
|||
create_relay: Create Relay
|
||||
create_unavailable_domain: Create Unavailable Domain
|
||||
create_user_role: Create Role
|
||||
create_username_block: Create Username Rule
|
||||
demote_user: Demote User
|
||||
destroy_announcement: Delete Announcement
|
||||
destroy_canonical_email_block: Delete Email Block
|
||||
|
@ -203,6 +204,7 @@ en:
|
|||
destroy_status: Delete Post
|
||||
destroy_unavailable_domain: Delete Unavailable Domain
|
||||
destroy_user_role: Destroy Role
|
||||
destroy_username_block: Delete Username Rule
|
||||
disable_2fa_user: Disable 2FA
|
||||
disable_custom_emoji: Disable Custom Emoji
|
||||
disable_relay: Disable Relay
|
||||
|
@ -237,6 +239,7 @@ en:
|
|||
update_report: Update Report
|
||||
update_status: Update Post
|
||||
update_user_role: Update Role
|
||||
update_username_block: Update Username Rule
|
||||
actions:
|
||||
approve_appeal_html: "%{name} approved moderation decision appeal from %{target}"
|
||||
approve_user_html: "%{name} approved sign-up from %{target}"
|
||||
|
@ -255,6 +258,7 @@ en:
|
|||
create_relay_html: "%{name} created a relay %{target}"
|
||||
create_unavailable_domain_html: "%{name} stopped delivery to domain %{target}"
|
||||
create_user_role_html: "%{name} created %{target} role"
|
||||
create_username_block_html: "%{name} added rule for usernames containing %{target}"
|
||||
demote_user_html: "%{name} demoted user %{target}"
|
||||
destroy_announcement_html: "%{name} deleted announcement %{target}"
|
||||
destroy_canonical_email_block_html: "%{name} unblocked email with the hash %{target}"
|
||||
|
@ -268,6 +272,7 @@ en:
|
|||
destroy_status_html: "%{name} removed post by %{target}"
|
||||
destroy_unavailable_domain_html: "%{name} resumed delivery to domain %{target}"
|
||||
destroy_user_role_html: "%{name} deleted %{target} role"
|
||||
destroy_username_block_html: "%{name} removed rule for usernames containing %{target}"
|
||||
disable_2fa_user_html: "%{name} disabled two factor requirement for user %{target}"
|
||||
disable_custom_emoji_html: "%{name} disabled emoji %{target}"
|
||||
disable_relay_html: "%{name} disabled the relay %{target}"
|
||||
|
@ -302,6 +307,7 @@ en:
|
|||
update_report_html: "%{name} updated report %{target}"
|
||||
update_status_html: "%{name} updated post by %{target}"
|
||||
update_user_role_html: "%{name} changed %{target} role"
|
||||
update_username_block_html: "%{name} updated rule for usernames containing %{target}"
|
||||
deleted_account: deleted account
|
||||
empty: No logs found.
|
||||
filter_by_action: Filter by action
|
||||
|
@ -1085,6 +1091,25 @@ en:
|
|||
other: Used by %{count} people over the last week
|
||||
title: Recommendations & Trends
|
||||
trending: Trending
|
||||
username_blocks:
|
||||
add_new: Add new
|
||||
block_registrations: Block registrations
|
||||
comparison:
|
||||
contains: Contains
|
||||
equals: Equals
|
||||
contains_html: Contains %{string}
|
||||
created_msg: Successfully created username rule
|
||||
delete: Delete
|
||||
edit:
|
||||
title: Edit username rule
|
||||
matches_exactly_html: Equals %{string}
|
||||
new:
|
||||
create: Create rule
|
||||
title: Create new username rule
|
||||
no_username_block_selected: No username rules were changed as none were selected
|
||||
not_permitted: Not permitted
|
||||
title: Username rules
|
||||
updated_msg: Successfully updated username rule
|
||||
warning_presets:
|
||||
add_new: Add new
|
||||
delete: Delete
|
||||
|
|
|
@ -653,8 +653,8 @@ fa:
|
|||
mark_as_sensitive_description_html: رسانهٔ درون فرستهٔ گزارش شده به عنوان حسّاس علامت خورده و شکایتی ضبط خواهد شد تا بتوانید خلافهای آینده از همین حساب را بهتر مدیریت کنید.
|
||||
other_description_html: دیدن انتخاب های بیشتر برای کنترل رفتار حساب و سفارشی سازی ارتباط با حساب گزارش شده.
|
||||
resolve_description_html: هیچ کنشی علیه حساب گزارش شده انجام نخواهد شد. هیچ شکایتی ضبط نشده و گزارش بسته خواهد شد.
|
||||
silence_description_html: این حساب فقط برای کسانی قابل مشاهده خواهد بود که قبلاً آن را دنبال می کنند یا به صورت دستی آن را جستجو می کنند و دسترسی آن را به شدت محدود می کند. همیشه می توان برگرداند. همه گزارشهای مربوط به این حساب را میبندد.
|
||||
suspend_description_html: اکانت و تمامی محتویات آن غیرقابل دسترسی و در نهایت حذف خواهد شد و تعامل با آن غیر ممکن خواهد بود. قابل برگشت در عرض 30 روز همه گزارشهای مربوط به این حساب را میبندد.
|
||||
silence_description_html: حساب فقط برای کسانی که از پیش پی میگرفتندش یا به صورت دستی به دنیالش گشتهاند نمایان خواهد بود که رسشش را شدیداً محدود میکند. همواره برگشتپذیر است. همهٔ گزارشها علیه این حساب را خواهد بست.
|
||||
suspend_description_html: حساب و همهٔ محتوایش غیرقابل دسترس شده و در نهایت حذف خواهند شد. تعامل با آن ممکن نخواهد بود. بازگشتپذیر تا ۳۰ روز. همهٔ گزارشها علیه این حساب را خواهد بست.
|
||||
actions_description_html: تصمیم گیری کنش اقدامی برای حل این گزارش. در صورت انجام کنش تنبیهی روی حساب گزارش شده، غیر از زمان یکه دستهٔ <strong>هرزنامه</strong> گزیده باشد، برایش آگاهی رایانامهای فرستاده خواهد شد.
|
||||
actions_description_remote_html: تصمیم بگیرید که چه اقدامی برای حل این گزارش انجام دهید. این فقط بر نحوه ارتباط سرور <strong>شما</strong> با این حساب راه دور و مدیریت محتوای آن تأثیر می گذارد.
|
||||
actions_no_posts: این گزارش هیچ پست مرتبطی برای حذف ندارد
|
||||
|
@ -714,7 +714,7 @@ fa:
|
|||
actions:
|
||||
delete_html: پست های توهین آمیز را حذف کنید
|
||||
mark_as_sensitive_html: رسانه پست های توهین آمیز را به عنوان حساس علامت گذاری کنید
|
||||
silence_html: دسترسی <strong>@%{acct}</strong> را به شدت محدود کنید و نمایه و محتویات آنها را فقط برای افرادی که قبلاً آنها را دنبال میکنند قابل مشاهده کنید یا به صورت دستی نمایه آن را جستجو کنید
|
||||
silence_html: محدودیت شدید رسش <strong>@%{acct}</strong> با نمایان کردن نماگر و محتوایش فقط به افرادی که از پیش پی میگرفتندش و به صورت دستی به دنبالش گشتهاند
|
||||
suspend_html: تعلیق <strong>@%{acct}</strong>، غیرقابل دسترس کردن نمایه و محتوای آنها و تعامل با آنها غیر ممکن
|
||||
close_report: 'علامت گذاری گزارش #%{id} به عنوان حل شده است'
|
||||
close_reports_html: "<strong>همه</strong> گزارشها در برابر <strong>@%{acct}</strong> را بهعنوان حلوفصل علامتگذاری کنید"
|
||||
|
@ -1872,6 +1872,7 @@ fa:
|
|||
edited_at_html: ویراسته در %{date}
|
||||
errors:
|
||||
in_reply_not_found: به نظر نمیرسد وضعیتی که میخواهید به آن پاسخ دهید، وجود داشته باشد.
|
||||
quoted_status_not_found: به نظر نمیرسد فرستهای که میخواهید نقلش کنید وجود داشته باشد.
|
||||
over_character_limit: از حد مجاز %{max} حرف فراتر رفتید
|
||||
pin_errors:
|
||||
direct: فرستههایی که فقط برای کاربران اشاره شده نمایانند نمیتوانند سنجاق شوند
|
||||
|
@ -2002,7 +2003,7 @@ fa:
|
|||
details: 'جزییات ورود:'
|
||||
explanation: ما ورود به حساب شما را از یک آدرس آی پی جدید شناسایی کرده ایم.
|
||||
further_actions_html: اگر این شما نبودید، توصیه می کنیم فورا %{action} را فعال کنید و برای ایمن نگه داشتن حساب خود، احراز هویت دو مرحله ای را فعال کنید.
|
||||
subject: حساب شما از یک آدرس آی پی جدید قابل دسترسی است
|
||||
subject: نشانی آیپی جدیدی به حسابتان دسترسی پیدا کرده
|
||||
title: یک ورود جدید
|
||||
terms_of_service_changed:
|
||||
agreement: با ادامه استفاده از %{domain}، با این شرایط موافقت می کنید. اگر با شرایط بهروزرسانی شده مخالف هستید، میتوانید در هر زمان با حذف حساب خود، قرارداد خود را با %{domain} فسخ کنید.
|
||||
|
|
|
@ -2001,6 +2001,7 @@ ga:
|
|||
edited_at_html: "%{date} curtha in eagar"
|
||||
errors:
|
||||
in_reply_not_found: Is cosúil nach ann don phostáil a bhfuil tú ag iarraidh freagra a thabhairt air.
|
||||
quoted_status_not_found: Is cosúil nach bhfuil an post atá tú ag iarraidh a lua ann.
|
||||
over_character_limit: teorainn carachtar %{max} sáraithe
|
||||
pin_errors:
|
||||
direct: Ní féidir postálacha nach bhfuil le feiceáil ach ag úsáideoirí luaite a phinnáil
|
||||
|
|
|
@ -1611,7 +1611,7 @@ ru:
|
|||
limit: Вы достигли максимального количества списков
|
||||
login_activities:
|
||||
authentication_methods:
|
||||
otp: приложения двухфакторной аутентификации
|
||||
otp: приложения для генерации кодов
|
||||
password: пароля
|
||||
webauthn: электронного ключа
|
||||
description_html: Если вы заметили действия, которых не совершали, вам следует сменить пароль и включить двухфакторную аутентификацию.
|
||||
|
@ -1621,11 +1621,18 @@ ru:
|
|||
title: История входов
|
||||
mail_subscriptions:
|
||||
unsubscribe:
|
||||
action: Да, отписаться
|
||||
action: Да, я хочу отписаться
|
||||
complete: Подписка отменена
|
||||
confirmation_html: Вы точно желаете отписаться от всех уведомления типа «%{type}», доставляемых из сервера Mastodon %{domain} на ваш адрес электронной почты %{email}? Вы всегда сможете подписаться снова в <a href="%{settings_path}">настройках e-mail уведомлений</a>.
|
||||
resubscribe_html: Если вы отписались от рассылки по ошибке, вы можете повторно подписаться на рассылку в настройках <a href="%{settings_path}">настроек почтовых уведомлений</a>.
|
||||
success_html: Вы больше не будете получать %{type} для Mastodon на %{domain} на вашу электронную почту %{email}.
|
||||
confirmation_html: Вы уверены в том, что хотите отписаться от всех %{type}, которые вы получаете на адрес %{email} для учётной записи на сервере Mastodon %{domain}? Вы всегда сможете подписаться снова в <a href="%{settings_path}">настройках уведомлений по электронной почте</a>.
|
||||
emails:
|
||||
notification_emails:
|
||||
favourite: уведомлений о добавлении ваших постов в избранное
|
||||
follow: уведомлений о новых подписчиках
|
||||
follow_request: уведомлений о новых запросах на подписку
|
||||
mention: уведомлений о новых упоминаниях
|
||||
reblog: уведомлений о продвижении ваших постов
|
||||
resubscribe_html: Если вы отписались по ошибке и хотите подписаться снова, перейдите на страницу <a href="%{settings_path}">настройки уведомлений по электронной почте</a>.
|
||||
success_html: Вы отказались от %{type}, которые вы получали на адрес %{email} для вашей учётной записи на сервере Mastodon %{domain}.
|
||||
title: Отписаться
|
||||
media_attachments:
|
||||
validations:
|
||||
|
@ -1721,13 +1728,13 @@ ru:
|
|||
trillion: трлн
|
||||
unit: ''
|
||||
otp_authentication:
|
||||
code_hint: Для подтверждения введите код, сгенерированный приложением-аутентификатором
|
||||
description_html: Подключив <strong>двуфакторную авторизацию</strong>, для входа в свою учётную запись вам будет необходим смартфон и приложение-аутентификатор на нём, которое будет генерировать специальные временные коды. Без этих кодов войти в учётную запись не получиться, даже если все данные верны, что существенно увеличивает безопасность вашей учётной записи.
|
||||
code_hint: Для подтверждения введите код из приложения-аутентификатора
|
||||
description_html: Подключите <strong>двухфакторную аутентификацию</strong> с использованием специального приложения-аутентификатора, и тогда для входа в вашу учётную запись необходимо будет иметь при себе смартфон, который будет генерировать одноразовые коды.
|
||||
enable: Включить
|
||||
instructions_html: "<strong>Отсканируйте этот QR-код с помощью приложения-аутентификатора</strong>, такого как Google Authenticator, Яндекс.Ключ или andOTP. После сканирования и добавления, приложение начнёт генерировать коды, которые потребуется вводить для завершения входа в учётную запись."
|
||||
manual_instructions: 'Если отсканировать QR-код не получается или не представляется возможным, вы можете ввести ключ настройки вручную:'
|
||||
setup: Настроить
|
||||
wrong_code: Введенный код недействителен! Время сервера и время устройства правильно?
|
||||
instructions_html: "<strong>Откройте Google Authenticator или другое приложение-аутентификатор на вашем смартфоне и отсканируйте этот QR-код</strong>. В дальнейшем это приложение будет генерировать одноразовые коды, которые потребуется вводить для подтверждения входа в вашу учётную запись."
|
||||
manual_instructions: 'Если отсканировать QR-код не получается, введите секретный ключ вручную:'
|
||||
setup: Подключить
|
||||
wrong_code: Одноразовый код, который вы ввели, не подходит! Совпадает ли время на устройстве с временем на сервере?
|
||||
pagination:
|
||||
newer: Позже
|
||||
next: Вперёд
|
||||
|
@ -1751,14 +1758,14 @@ ru:
|
|||
posting_defaults: Предустановки для новых постов
|
||||
public_timelines: Публичные ленты
|
||||
privacy:
|
||||
hint_html: "<strong>Настройте, как вы хотите, чтобы ваш профиль и ваши сообщения были найдены.</strong> Различные функции в Mastodon могут помочь вам охватить более широкую аудиторию, если они включены. Уделите время изучению этих настроек, чтобы убедиться, что они подходят для вашего случая использования."
|
||||
hint_html: "<strong>Здесь вы можете определить то, как другие смогут обнаружить ваши посты и ваш профиль.</strong> Множество разных функций в Mastodon существуют для того, чтобы помочь вам выйти на более широкую аудиторию, если вы того захотите. Ознакомьтесь с этими настройками и в случае необходимости измените их согласно вашим желаниям."
|
||||
privacy: Приватность
|
||||
privacy_hint_html: Определите, какую информацию вы хотите раскрыть в интересах других. Люди находят интересные профили и приложения, просматривая список подписчиков других людей и узнавая, из каких приложений они публикуют свои сообщения, но вы можете предпочесть скрыть это.
|
||||
privacy_hint_html: Решите, сколько данных о себе вы готовы раскрыть ради того, чтобы они пошли на пользу другим. Просматривая ваши подписки, кто-то может обнаружить профили интересных людей, а ещё кто-нибудь может найти своё любимое приложение, увидев его название рядом с вашими постами. Тем не менее вы можете предпочесть не раскрывать эту информацию.
|
||||
reach: Видимость
|
||||
reach_hint_html: Укажите, хотите ли вы, чтобы новые люди обнаруживали вас и могли следить за вами. Хотите ли вы, чтобы ваши сообщения появлялись на экране Обзора? Хотите ли вы, чтобы другие люди видели вас в своих рекомендациях? Хотите ли вы автоматически принимать всех новых подписчиков или иметь возможность детально контролировать каждого из них?
|
||||
reach_hint_html: Решите, нужна ли вам новая аудитория и новые подписчики. Настройте по своему желанию, показывать ли ваши посты в разделе «Актуальное», рекомендовать ли ваш профиль другим людям, принимать ли всех новых подписчиков автоматически или рассматривать каждый запрос на подписку в отдельности.
|
||||
search: Поиск
|
||||
search_hint_html: Определите, как вас могут найти. Хотите ли вы, чтобы люди находили вас по тому, о чём вы публично писали? Хотите ли вы, чтобы люди за пределами Mastodon находили ваш профиль при поиске в Интернете? Следует помнить, что полное исключение из всех поисковых систем не может быть гарантировано для публичной информации.
|
||||
title: Приватность и доступ
|
||||
title: Приватность и видимость
|
||||
privacy_policy:
|
||||
title: Политика конфиденциальности
|
||||
reactions:
|
||||
|
|
|
@ -160,6 +160,10 @@ en:
|
|||
name: Public name of the role, if role is set to be displayed as a badge
|
||||
permissions_as_keys: Users with this role will have access to...
|
||||
position: Higher role decides conflict resolution in certain situations. Certain actions can only be performed on roles with a lower priority
|
||||
username_block:
|
||||
allow_with_approval: Instead of preventing sign-up outright, matching sign-ups will require your approval
|
||||
comparison: Please be mindful of the Scunthorpe Problem when blocking partial matches
|
||||
username: Will be matched regardless of casing and common homoglyphs like "4" for "a" or "3" for "e"
|
||||
webhook:
|
||||
events: Select events to send
|
||||
template: Compose your own JSON payload using variable interpolation. Leave blank for default JSON.
|
||||
|
@ -371,6 +375,10 @@ en:
|
|||
name: Name
|
||||
permissions_as_keys: Permissions
|
||||
position: Priority
|
||||
username_block:
|
||||
allow_with_approval: Allow registrations with approval
|
||||
comparison: Method of comparison
|
||||
username: Word to match
|
||||
webhook:
|
||||
events: Enabled events
|
||||
template: Payload template
|
||||
|
|
|
@ -56,7 +56,7 @@ fa:
|
|||
scopes: واسطهای برنامهنویسی که این برنامه به آن دسترسی دارد. اگر بالاترین سطح دسترسی را انتخاب کنید، دیگر نیازی به انتخاب سطحهای پایینی ندارید.
|
||||
setting_aggregate_reblogs: برای تقویتهایی که به تازگی برایتان نمایش داده شدهاند، تقویتهای بیشتر را نمایش نده (فقط روی تقویتهای اخیر تأثیر میگذارد)
|
||||
setting_always_send_emails: در حالت عادی آگاهیهای رایانامهای هنگامی که فعّالانه از ماستودون استفاده میکنید فرستاده نمیشوند
|
||||
setting_default_quote_policy: کاربران اشاره شده همواره مجاز به نقل قولند. این تنظیمات تنها روی فرستههای ایجاد شده با نگارش بعدی ماستودون موثّر است، ولی میتوانید ترجیحاتتان را پیشاپیش بگزینید
|
||||
setting_default_quote_policy: کاربران اشاره شده همواره مجاز به نقلند. این تنظیمات تنها روی فرستههای ایجاد شده با نگارش بعدی ماستودون موثّر است، ولی میتوانید ترجیحاتتان را پیشاپیش بگزینید
|
||||
setting_default_sensitive: تصاویر حساس به طور پیشفرض پنهان هستند و میتوانند با یک کلیک آشکار شوند
|
||||
setting_display_media_default: تصویرهایی را که به عنوان حساس علامت زده شدهاند پنهان کن
|
||||
setting_display_media_hide_all: همیشه همهٔ عکسها و ویدیوها را پنهان کن
|
||||
|
@ -150,6 +150,9 @@ fa:
|
|||
min_age: نباید کمتر از کمینهٔ زمان لازم از سوی قوانین حقوقیتان باشد.
|
||||
user:
|
||||
chosen_languages: اگر انتخاب کنید، تنها نوشتههایی که به زبانهای برگزیدهٔ شما نوشته شدهاند در فهرست نوشتههای عمومی نشان داده میشوند
|
||||
date_of_birth:
|
||||
one: برای استفاده از %{domain} باید مطمئن شویم کمینه %{count} سال را دارید. این مورد را ذخیره نخواهیم کرد.
|
||||
other: برای استفاده از %{domain} باید مطمئن شویم کمینه %{count} سال را دارید. این مورد را ذخیره نخواهیم کرد.
|
||||
role: نقش کنترل می کند که کاربر چه مجوزهایی دارد.
|
||||
user_role:
|
||||
color: رنگی که برای نقش در سرتاسر UI استفاده می شود، به عنوان RGB در قالب هگز
|
||||
|
@ -230,7 +233,7 @@ fa:
|
|||
setting_boost_modal: نمایش پیغام تأیید پیش از تقویت کردن
|
||||
setting_default_language: زبان نوشتههای شما
|
||||
setting_default_privacy: حریم خصوصی نوشتهها
|
||||
setting_default_quote_policy: افراد مجاز به نقل قول
|
||||
setting_default_quote_policy: افراد مجاز به نقل
|
||||
setting_default_sensitive: همیشه تصاویر را به عنوان حساس علامت بزن
|
||||
setting_delete_modal: نمایش پیغام تأیید پیش از پاک کردن یک نوشته
|
||||
setting_disable_hover_cards: از کار انداختن پیشنمایش نمایه هنگام رفتن رویش
|
||||
|
|
|
@ -1872,6 +1872,7 @@ sv:
|
|||
edited_at_html: 'Ändrad: %{date}'
|
||||
errors:
|
||||
in_reply_not_found: Inlägget du försöker svara på verkar inte existera.
|
||||
quoted_status_not_found: Inlägget du försöker svara på verkar inte existera.
|
||||
over_character_limit: teckengräns på %{max} har överskridits
|
||||
pin_errors:
|
||||
direct: Inlägg som endast är synliga för nämnda användare kan inte fästas
|
||||
|
|
|
@ -59,6 +59,7 @@ SimpleNavigation::Configuration.run do |navigation|
|
|||
current_user.can?(:manage_federation)
|
||||
}
|
||||
s.item :email_domain_blocks, safe_join([material_symbol('mail'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_path, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.can?(:manage_blocks) }
|
||||
s.item :username_blocks, safe_join([material_symbol('supervised_user_circle_off'), t('admin.username_blocks.title')]), admin_username_blocks_path, highlights_on: %r{/admin/username_blocks}, if: -> { current_user.can?(:manage_blocks) }
|
||||
s.item :ip_blocks, safe_join([material_symbol('hide_source'), t('admin.ip_blocks.title')]), admin_ip_blocks_path, highlights_on: %r{/admin/ip_blocks}, if: -> { current_user.can?(:manage_blocks) }
|
||||
s.item :action_logs, safe_join([material_symbol('list'), t('admin.action_logs.title')]), admin_action_logs_path, if: -> { current_user.can?(:view_audit_log) }
|
||||
end
|
||||
|
|
|
@ -230,4 +230,10 @@ namespace :admin do
|
|||
end
|
||||
|
||||
resources :software_updates, only: [:index]
|
||||
|
||||
resources :username_blocks, except: [:show, :destroy] do
|
||||
collection do
|
||||
post :batch
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,28 +20,6 @@ defaults: &defaults
|
|||
trends: true
|
||||
trends_as_landing_page: true
|
||||
trendable_by_default: false
|
||||
reserved_usernames:
|
||||
- abuse
|
||||
- account
|
||||
- accounts
|
||||
- admin
|
||||
- administration
|
||||
- administrator
|
||||
- admins
|
||||
- help
|
||||
- helpdesk
|
||||
- instance
|
||||
- mod
|
||||
- moderator
|
||||
- moderators
|
||||
- mods
|
||||
- owner
|
||||
- root
|
||||
- security
|
||||
- server
|
||||
- staff
|
||||
- support
|
||||
- webmaster
|
||||
disallowed_hashtags: # space separated string or list of hashtags without the hash
|
||||
bootstrap_timeline_accounts: ''
|
||||
activity_api_enabled: true
|
||||
|
|
23
db/migrate/20250717003848_create_username_blocks.rb
Normal file
23
db/migrate/20250717003848_create_username_blocks.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateUsernameBlocks < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :username_blocks do |t|
|
||||
t.string :username, null: false
|
||||
t.string :normalized_username, null: false
|
||||
t.boolean :exact, null: false, default: false
|
||||
t.boolean :allow_with_approval, null: false, default: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :username_blocks, 'lower(username)', unique: true, name: 'index_username_blocks_on_username_lower_btree'
|
||||
add_index :username_blocks, :normalized_username
|
||||
|
||||
reversible do |dir|
|
||||
dir.up do
|
||||
load Rails.root.join('db', 'seeds', '05_blocked_usernames.rb')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
13
db/schema.rb
13
db/schema.rb
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_06_27_132728) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_07_17_003848) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_catalog.plpgsql"
|
||||
|
||||
|
@ -1238,6 +1238,17 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_27_132728) do
|
|||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "username_blocks", force: :cascade do |t|
|
||||
t.string "username", null: false
|
||||
t.string "normalized_username", null: false
|
||||
t.boolean "exact", default: false, null: false
|
||||
t.boolean "allow_with_approval", default: false, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index "lower((username)::text)", name: "index_username_blocks_on_username_lower_btree", unique: true
|
||||
t.index ["normalized_username"], name: "index_username_blocks_on_normalized_username"
|
||||
end
|
||||
|
||||
create_table "users", force: :cascade do |t|
|
||||
t.string "email", default: "", null: false
|
||||
t.datetime "created_at", precision: nil, null: false
|
||||
|
|
34
db/seeds/05_blocked_usernames.rb
Normal file
34
db/seeds/05_blocked_usernames.rb
Normal file
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
%w(
|
||||
abuse
|
||||
account
|
||||
accounts
|
||||
admin
|
||||
administration
|
||||
administrator
|
||||
admins
|
||||
help
|
||||
helpdesk
|
||||
instance
|
||||
mod
|
||||
moderator
|
||||
moderators
|
||||
mods
|
||||
owner
|
||||
root
|
||||
security
|
||||
server
|
||||
staff
|
||||
support
|
||||
webmaster
|
||||
).each do |str|
|
||||
UsernameBlock.create_with(username: str, exact: true).find_or_create_by(username: str)
|
||||
end
|
||||
|
||||
%w(
|
||||
mastodon
|
||||
mastadon
|
||||
).each do |str|
|
||||
UsernameBlock.create_with(username: str, exact: false).find_or_create_by(username: str)
|
||||
end
|
7
spec/fabricators/username_block_fabricator.rb
Normal file
7
spec/fabricators/username_block_fabricator.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Fabricator(:username_block) do
|
||||
username { sequence(:email) { |i| "#{i}#{Faker::Internet.username}" } }
|
||||
exact false
|
||||
allow_with_approval false
|
||||
end
|
63
spec/models/username_block_spec.rb
Normal file
63
spec/models/username_block_spec.rb
Normal file
|
@ -0,0 +1,63 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe UsernameBlock do
|
||||
describe '.matches?' do
|
||||
context 'when there is an exact block' do
|
||||
before do
|
||||
Fabricate(:username_block, username: 'carriage', exact: true)
|
||||
end
|
||||
|
||||
it 'returns true on exact match' do
|
||||
expect(described_class.matches?('carriage')).to be true
|
||||
end
|
||||
|
||||
it 'returns true on case insensitive match' do
|
||||
expect(described_class.matches?('CaRRiagE')).to be true
|
||||
end
|
||||
|
||||
it 'returns true on homoglyph match' do
|
||||
expect(described_class.matches?('c4rr14g3')).to be true
|
||||
end
|
||||
|
||||
it 'returns false on partial match' do
|
||||
expect(described_class.matches?('foo_carriage')).to be false
|
||||
end
|
||||
|
||||
it 'returns false on no match' do
|
||||
expect(described_class.matches?('foo')).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is a partial block' do
|
||||
before do
|
||||
Fabricate(:username_block, username: 'carriage', exact: false)
|
||||
end
|
||||
|
||||
it 'returns true on exact match' do
|
||||
expect(described_class.matches?('carriage')).to be true
|
||||
end
|
||||
|
||||
it 'returns true on case insensitive match' do
|
||||
expect(described_class.matches?('CaRRiagE')).to be true
|
||||
end
|
||||
|
||||
it 'returns true on homoglyph match' do
|
||||
expect(described_class.matches?('c4rr14g3')).to be true
|
||||
end
|
||||
|
||||
it 'returns true on suffix match' do
|
||||
expect(described_class.matches?('foo_carriage')).to be true
|
||||
end
|
||||
|
||||
it 'returns true on prefix match' do
|
||||
expect(described_class.matches?('carriage_foo')).to be true
|
||||
end
|
||||
|
||||
it 'returns false on no match' do
|
||||
expect(described_class.matches?('foo')).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -42,14 +42,6 @@ RSpec.describe WorkerBatch do
|
|||
it 'does not persist the job IDs' do
|
||||
expect(subject.jobs).to eq []
|
||||
end
|
||||
|
||||
context 'when async refresh is connected' do
|
||||
let(:async_refresh) { AsyncRefresh.new(async_refresh_key) }
|
||||
|
||||
it 'immediately marks the async refresh as finished' do
|
||||
expect(async_refresh.reload.finished?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when called with an array of job IDs' do
|
||||
|
@ -71,7 +63,7 @@ RSpec.describe WorkerBatch do
|
|||
before do
|
||||
subject.connect(async_refresh_key, threshold: 0.5) if async_refresh.present?
|
||||
subject.add_jobs(%w(foo bar baz))
|
||||
subject.remove_job('foo')
|
||||
subject.remove_job('foo', increment: true)
|
||||
end
|
||||
|
||||
it 'removes the job from pending jobs' do
|
||||
|
|
97
spec/requests/admin/username_blocks_spec.rb
Normal file
97
spec/requests/admin/username_blocks_spec.rb
Normal file
|
@ -0,0 +1,97 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Admin Username Blocks' do
|
||||
describe 'GET /admin/username_blocks' do
|
||||
before { sign_in Fabricate(:admin_user) }
|
||||
|
||||
it 'returns http success' do
|
||||
get admin_username_blocks_path
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /admin/username_blocks' do
|
||||
before { sign_in Fabricate(:admin_user) }
|
||||
|
||||
it 'gracefully handles invalid nested params' do
|
||||
post admin_username_blocks_path(username_block: 'invalid')
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(400)
|
||||
end
|
||||
|
||||
it 'creates a username block' do
|
||||
post admin_username_blocks_path(username_block: { username: 'banana', comparison: 'contains', allow_with_approval: '0' })
|
||||
|
||||
expect(response)
|
||||
.to redirect_to(admin_username_blocks_path)
|
||||
expect(UsernameBlock.find_by(username: 'banana'))
|
||||
.to_not be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /admin/username_blocks/batch' do
|
||||
before { sign_in Fabricate(:admin_user) }
|
||||
|
||||
let(:username_blocks) { Fabricate.times(2, :username_block) }
|
||||
|
||||
it 'gracefully handles invalid nested params' do
|
||||
post batch_admin_username_blocks_path(form_username_block_batch: 'invalid')
|
||||
|
||||
expect(response)
|
||||
.to redirect_to(admin_username_blocks_path)
|
||||
end
|
||||
|
||||
it 'deletes selected username blocks' do
|
||||
post batch_admin_username_blocks_path(form_username_block_batch: { username_block_ids: username_blocks.map(&:id) }, delete: '1')
|
||||
|
||||
expect(response)
|
||||
.to redirect_to(admin_username_blocks_path)
|
||||
expect(UsernameBlock.where(id: username_blocks.map(&:id)))
|
||||
.to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /admin/username_blocks/new' do
|
||||
before { sign_in Fabricate(:admin_user) }
|
||||
|
||||
it 'returns http success' do
|
||||
get new_admin_username_block_path
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /admin/username_blocks/:id/edit' do
|
||||
before { sign_in Fabricate(:admin_user) }
|
||||
|
||||
let(:username_block) { Fabricate(:username_block) }
|
||||
|
||||
it 'returns http success' do
|
||||
get edit_admin_username_block_path(username_block)
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /admin/username_blocks/:id' do
|
||||
before { sign_in Fabricate(:admin_user) }
|
||||
|
||||
let(:username_block) { Fabricate(:username_block, username: 'banana') }
|
||||
|
||||
it 'updates username block' do
|
||||
put admin_username_block_path(username_block, username_block: { username: 'bebebe' })
|
||||
|
||||
expect(response)
|
||||
.to redirect_to(admin_username_blocks_path)
|
||||
expect(username_block.reload.username)
|
||||
.to eq 'bebebe'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,8 +10,13 @@ RSpec.describe UnreservedUsernameValidator do
|
|||
attr_accessor :username
|
||||
|
||||
validates_with UnreservedUsernameValidator
|
||||
|
||||
def self.name
|
||||
'Foo'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:record) { record_class.new }
|
||||
|
||||
describe '#validate' do
|
||||
|
@ -114,7 +119,7 @@ RSpec.describe UnreservedUsernameValidator do
|
|||
end
|
||||
|
||||
def stub_reserved_usernames(value)
|
||||
allow(Setting).to receive(:[]).with('reserved_usernames').and_return(value)
|
||||
value&.each { |str| Fabricate(:username_block, username: str, exact: true) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue
Block a user