Add: #348 新規登録の上限人数 (#527)

* Add: #348 新規登録の上限人数

* Fix test

* Fix test

* Wip

* Fix test

* Add invite support

* Wip

* Fix test

* Fix test

* Fix test
This commit is contained in:
KMY(雪あすか) 2024-02-12 22:05:32 +09:00 committed by GitHub
parent d7cc6b788c
commit e317edecb8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 362 additions and 6 deletions

View file

@ -1,9 +1,18 @@
# frozen_string_literal: true
class Admin::Settings::RegistrationsController < Admin::SettingsController
include RegistrationLimitationHelper
before_action :set_limitation_counts, only: :show # rubocop:disable Rails/LexicallyScopedActionFilter
private
def after_update_redirect_path
admin_settings_registrations_path
end
def set_limitation_counts
@current_users_count = user_count_for_registration
@current_users_count_today = today_increase_user_count
end
end

View file

@ -2,6 +2,7 @@
class Auth::ConfirmationsController < Devise::ConfirmationsController
include Auth::CaptchaConcern
include RegistrationLimitationHelper
layout 'auth'
@ -16,6 +17,11 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
skip_before_action :require_functional!
def show
if reach_registrations_limit?
render :limitation_error
return
end
old_session_values = session.to_hash
reset_session
session.update old_session_values.except('session_id')

View file

@ -3,8 +3,10 @@
module RegistrationHelper
extend ActiveSupport::Concern
include RegistrationLimitationHelper
def allowed_registration?(remote_ip, invite)
!Rails.configuration.x.single_user_mode && !omniauth_only? && (registrations_open? || invite&.valid_for_use?) && !ip_blocked?(remote_ip)
!Rails.configuration.x.single_user_mode && !omniauth_only? && ((registrations_open? && !reach_registrations_limit?) || invite&.valid_for_use?) && !ip_blocked?(remote_ip)
end
def registrations_open?

View file

@ -0,0 +1,55 @@
# frozen_string_literal: true
module RegistrationLimitationHelper
def reach_registrations_limit?
return true unless registrations_in_time?
((Setting.registrations_limit.presence || 0).positive? && Setting.registrations_limit <= user_count_for_registration) ||
((Setting.registrations_limit_per_day.presence || 0).positive? && Setting.registrations_limit_per_day <= today_increase_user_count)
end
def user_count_for_registration
Rails.cache.fetch('registrations:user_count') { User.confirmed.enabled.joins(:account).merge(Account.without_suspended).count }
end
def today_increase_user_count
today_date = Time.now.utc.beginning_of_day.to_i
count = 0
if Rails.cache.fetch('registrations:today_date') { today_date } == today_date
count = Rails.cache.fetch('registrations:today_increase_user_count') { today_increase_user_count_value }
else
count = today_increase_user_count_value
Rails.cache.write('registrations:today_date', today_date)
Rails.cache.write('registrations:today_increase_user_count', count)
end
count
end
def today_increase_user_count_value
User.confirmed.enabled.where('users.created_at >= ?', Time.now.utc.beginning_of_day).joins(:account).merge(Account.without_suspended).count
end
def registrations_in_time?
start_hour = Setting.registrations_start_hour || 0
end_hour = Setting.registrations_end_hour || 24
secondary_start_hour = Setting.registrations_secondary_start_hour || 0
secondary_end_hour = Setting.registrations_secondary_end_hour || 0
return true if start_hour >= end_hour && secondary_start_hour >= secondary_end_hour
current_hour = Time.now.utc.hour
primary_permitted = false
primary_permitted = start_hour <= current_hour && current_hour < end_hour if start_hour < end_hour && end_hour.positive?
secondary_permitted = false
secondary_permitted = secondary_start_hour <= current_hour && current_hour < secondary_end_hour if secondary_start_hour < secondary_end_hour && secondary_end_hour.positive?
primary_permitted || secondary_permitted
end
def reset_registration_limit_caches!
Rails.cache.delete('registrations:user_count')
Rails.cache.delete('registrations:today_increase_user_count')
end
end

View file

@ -4,7 +4,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { fetchServer } from 'mastodon/actions/server';
import { domain } from 'mastodon/initial_state';
import { domain, registrationsReachLimit } from 'mastodon/initial_state';
const mapStateToProps = state => ({
message: state.getIn(['server', 'server', 'registrations', 'message']),
@ -27,6 +27,16 @@ class ClosedRegistrationsModal extends ImmutablePureComponent {
dangerouslySetInnerHTML={{ __html: this.props.message }}
/>
);
} else if (registrationsReachLimit) {
closedRegistrationsMessage = (
<p className='prose'>
<FormattedMessage
id='closed_registrations_modal.description_when_reaching_limit'
defaultMessage='New registrations are currently temporarily restricted. Either the maximum number of registrations has been reached or it is outside the time frame available for registration. Please contact the administrator for more information or wait until the restriction is lifted.'
values={{ domain: <strong>{domain}</strong> }}
/>
</p>
);
} else {
closedRegistrationsMessage = (
<p className='prose'>

View file

@ -48,6 +48,7 @@
* @property {string=} owner
* @property {boolean} profile_directory
* @property {boolean} registrations_open
* @property {boolean} registrations_reach_limit
* @property {boolean} reduce_motion
* @property {string} repository
* @property {boolean} search_enabled
@ -134,6 +135,7 @@ export const owner = getMeta('owner');
export const profile_directory = getMeta('profile_directory');
export const reduceMotion = getMeta('reduce_motion');
export const registrationsOpen = getMeta('registrations_open');
export const registrationsReachLimit = getMeta('registrations_reach_limit');
export const repository = getMeta('repository');
export const searchEnabled = getMeta('search_enabled');
export const trendsEnabled = getMeta('trends_enabled');

View file

@ -113,6 +113,7 @@
"circles.edit": "Edit circle",
"closed_registrations.other_server_instructions": "Since Mastodon is decentralized, you can create an account on another server and still interact with this one.",
"closed_registrations_modal.description": "Creating an account on {domain} is currently not possible, but please keep in mind that you do not need an account specifically on {domain} to use Mastodon.",
"closed_registrations_modal.description_when_reaching_limit": "New registrations are currently temporarily restricted. Either the maximum number of registrations has been reached or it is outside the time frame available for registration. Please contact the administrator for more information or wait until the restriction is lifted.",
"closed_registrations_modal.find_another_server": "Find another server",
"closed_registrations_modal.preamble": "Mastodon is decentralized, so no matter where you create your account, you will be able to follow and interact with anyone on this server. You can even self-host it!",
"closed_registrations_modal.title": "Signing up on Mastodon",

View file

@ -170,6 +170,7 @@
"circles.subheading": "あなたのサークル",
"closed_registrations.other_server_instructions": "Mastodonは分散型なので他のサーバーにアカウントを作ってもこのサーバーとやり取りできます。",
"closed_registrations_modal.description": "現在{domain}でアカウント作成はできませんがMastodonは{domain}のアカウントでなくても利用できます。",
"closed_registrations_modal.description_when_reaching_limit": "新規登録は現在一時的に制限されています。登録の上限人数に達したか、または登録可能な時間帯の範囲外です。詳細を管理人に問い合わせるか、制限が解除されるまでお待ち下さい。",
"closed_registrations_modal.find_another_server": "別のサーバーを探す",
"closed_registrations_modal.preamble": "Mastodonは分散型なのでどのサーバーでアカウントを作成してもこのサーバーのユーザーを誰でもフォローして交流することができます。また自分でホスティングすることもできます",
"closed_registrations_modal.title": "Mastodonでアカウントを作成",

View file

@ -15,6 +15,12 @@ class Form::AdminSettings
registrations_mode
closed_registrations_message
registration_button_message
registrations_limit
registrations_limit_per_day
registrations_start_hour
registrations_end_hour
registrations_secondary_start_hour
registrations_secondary_end_hour
timeline_preview
bootstrap_timeline_accounts
theme
@ -61,6 +67,12 @@ class Form::AdminSettings
content_cache_retention_period
backups_retention_period
post_hash_tags_max
registrations_limit
registrations_limit_per_day
registrations_start_hour
registrations_end_hour
registrations_secondary_start_hour
registrations_secondary_end_hour
).freeze
BOOLEAN_KEYS = %i(

View file

@ -55,6 +55,7 @@ class User < ApplicationRecord
include LanguagesHelper
include Redisable
include RegistrationLimitationHelper
include User::HasSettings
include User::LdapAuthenticable
include User::Omniauthable
@ -192,6 +193,8 @@ class User < ApplicationRecord
end
def confirm
raise Mastodon::ValidationError, I18n.t('devise.registrations.sign_up_failed_because_reach_limit') if !invited? && reach_registrations_limit?
wrap_email_confirmation do
super
end
@ -482,6 +485,7 @@ class User < ApplicationRecord
ActivityTracker.record('activity:logins', id)
UserMailer.welcome(self).deliver_later
TriggerWebhookWorker.perform_async('account.approved', 'Account', account_id)
reset_registration_limit_caches!
end
def prepare_returning_user!

View file

@ -3,6 +3,7 @@
class InitialStateSerializer < ActiveModel::Serializer
include RoutingHelper
include DtlHelper
include RegistrationLimitationHelper
attributes :meta, :compose, :accounts,
:media_attachments, :settings,
@ -122,7 +123,8 @@ class InitialStateSerializer < ActiveModel::Serializer
locale: I18n.locale,
mascot: instance_presenter.mascot&.file&.url,
profile_directory: Setting.profile_directory,
registrations_open: Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode,
registrations_open: Setting.registrations_mode != 'none' && !reach_registrations_limit? && !Rails.configuration.x.single_user_mode,
registrations_reach_limit: Setting.registrations_mode != 'none' && reach_registrations_limit?,
repository: Mastodon::Version.repository,
search_enabled: Chewy.enabled?,
single_user_mode: Rails.configuration.x.single_user_mode,

View file

@ -3,6 +3,7 @@
class NodeInfo::Serializer < ActiveModel::Serializer
include RoutingHelper
include KmyblueCapabilitiesHelper
include RegistrationLimitationHelper
attributes :version, :software, :protocols, :services, :usage, :open_registrations, :metadata
@ -35,7 +36,7 @@ class NodeInfo::Serializer < ActiveModel::Serializer
end
def open_registrations
Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode
Setting.registrations_mode != 'none' && !reach_registrations_limit? && !Rails.configuration.x.single_user_mode
end
def metadata

View file

@ -9,6 +9,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
include RoutingHelper
include KmyblueCapabilitiesHelper
include RegistrationLimitationHelper
attributes :domain, :title, :version, :source_url, :description,
:usage, :thumbnail, :languages, :configuration,
@ -110,6 +111,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
{
enabled: registrations_enabled?,
approval_required: Setting.registrations_mode == 'approved',
limit_reached: Setting.registrations_mode != 'none' && reach_registrations_limit?,
message: registrations_enabled? ? nil : registrations_message,
url: ENV.fetch('SSO_ACCOUNT_SIGN_UP', nil),
}
@ -118,7 +120,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
private
def registrations_enabled?
Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode
Setting.registrations_mode != 'none' && !reach_registrations_limit? && !Rails.configuration.x.single_user_mode
end
def registrations_message

View file

@ -3,6 +3,7 @@
class REST::V1::InstanceSerializer < ActiveModel::Serializer
include RoutingHelper
include KmyblueCapabilitiesHelper
include RegistrationLimitationHelper
attributes :uri, :title, :short_description, :description, :email,
:version, :urls, :stats, :thumbnail,
@ -109,7 +110,7 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer
end
def registrations
Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode
Setting.registrations_mode != 'none' && !reach_registrations_limit? && !Rails.configuration.x.single_user_mode
end
def approval_required

View file

@ -2,6 +2,7 @@
class DeleteAccountService < BaseService
include Payloadable
include RegistrationLimitationHelper
ASSOCIATIONS_ON_SUSPEND = %w(
account_notes
@ -143,6 +144,8 @@ class DeleteAccountService < BaseService
else
@account.user.destroy
end
reset_registration_limit_caches!
end
def purge_content!

View file

@ -27,5 +27,19 @@
.fields-group
= f.input :registration_button_message, as: :text, kmyblue: true, hint: false, wrapper: :with_label, input_html: { rows: 2 }
.fields-group
= f.input :registrations_limit, kmyblue: true, wrapper: :with_label, input_html: { pattern: '[0-9]+' }, label: I18n.t('simple_form.labels.form_admin_settings.registrations_limit', count: @current_users_count)
.fields-group
= f.input :registrations_limit_per_day, kmyblue: true, wrapper: :with_label, input_html: { pattern: '[0-9]+' }, label: I18n.t('simple_form.labels.form_admin_settings.registrations_limit_per_day', count: @current_users_count_today)
.fields-group
= f.input :registrations_start_hour, kmyblue: true, wrapper: :with_label, input_html: { pattern: '[0-9]+' }
= f.input :registrations_end_hour, kmyblue: true, wrapper: :with_label, input_html: { pattern: '[0-9]+' }
.fields-group
= f.input :registrations_secondary_start_hour, kmyblue: true, wrapper: :with_label, input_html: { pattern: '[0-9]+' }
= f.input :registrations_secondary_end_hour, kmyblue: true, wrapper: :with_label, input_html: { pattern: '[0-9]+' }
.actions
= f.button :button, t('generic.save_changes'), type: :submit

View file

@ -0,0 +1,11 @@
- content_for :page_title do
= t('auth.registration_limit.title')
= form_tag root_url, method: 'GET', class: 'simple_form' do
= render 'auth/shared/progress', stage: 'confirm'
%h1.title= t('auth.registration_limit.title')
%p.lead= t('auth.registration_limit.hint_html')
.actions
= button_tag t('challenge.confirm'), class: 'button', type: :submit