mirror of
https://github.com/mastodon/mastodon.git
synced 2025-09-06 18:01:05 +00:00
Add ability to block words in usernames (#35407)
This commit is contained in:
parent
8cf7a77808
commit
20bbd20ef1
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
|
|
@ -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'
|
||||
|
|
|
@ -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 |
|
@ -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
|
||||
|
|
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
|
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
|
|
@ -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
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
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