Merge commit '2f98134ac69ee840095c9d8389e4b2fff72f20c1' into glitch-soc/merge-upstream

This commit is contained in:
Claire 2025-03-15 17:49:32 +01:00
commit 0efa669fe9
27 changed files with 362 additions and 63 deletions

View file

@ -186,7 +186,7 @@ FROM build AS libvips
# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"] # libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"]
# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips # renovate: datasource=github-releases depName=libvips packageName=libvips/libvips
ARG VIPS_VERSION=8.16.0 ARG VIPS_VERSION=8.16.1
# libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"] # libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"]
ARG VIPS_URL=https://github.com/libvips/libvips/releases/download ARG VIPS_URL=https://github.com/libvips/libvips/releases/download

View file

@ -93,23 +93,24 @@ GEM
annotaterb (4.14.0) annotaterb (4.14.0)
ast (2.4.2) ast (2.4.2)
attr_required (1.0.2) attr_required (1.0.2)
aws-eventstream (1.3.0) aws-eventstream (1.3.2)
aws-partitions (1.1032.0) aws-partitions (1.1066.0)
aws-sdk-core (3.214.1) aws-sdk-core (3.220.1)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0) aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9) aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.96.0) aws-sdk-kms (1.99.0)
aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.177.0) aws-sdk-s3 (1.177.0)
aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sigv4 (1.10.1) aws-sigv4 (1.11.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
azure-blob (0.5.4) azure-blob (0.5.7)
rexml rexml
base64 (0.2.0) base64 (0.2.0)
bcp47_spec (0.2.1) bcp47_spec (0.2.1)
@ -168,7 +169,7 @@ GEM
bigdecimal bigdecimal
rexml rexml
crass (1.0.6) crass (1.0.6)
css_parser (1.21.0) css_parser (1.21.1)
addressable addressable
csv (3.3.2) csv (3.3.2)
database_cleaner-active_record (2.2.0) database_cleaner-active_record (2.2.0)
@ -222,7 +223,8 @@ GEM
erubi (1.13.1) erubi (1.13.1)
et-orbi (1.2.11) et-orbi (1.2.11)
tzinfo tzinfo
excon (1.2.3) excon (1.2.5)
logger
fabrication (2.31.0) fabrication (2.31.0)
faker (3.5.1) faker (3.5.1)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
@ -265,7 +267,9 @@ GEM
raabro (~> 1.4) raabro (~> 1.4)
globalid (1.2.1) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
google-protobuf (3.25.6) google-protobuf (4.30.1)
bigdecimal
rake (>= 13)
googleapis-common-protos-types (1.18.0) googleapis-common-protos-types (1.18.0)
google-protobuf (>= 3.18, < 5.a) google-protobuf (>= 3.18, < 5.a)
haml (6.3.0) haml (6.3.0)
@ -302,7 +306,8 @@ GEM
domain_name (~> 0.5) domain_name (~> 0.5)
http-form_data (2.3.0) http-form_data (2.3.0)
http_accept_language (2.1.1) http_accept_language (2.1.1)
httpclient (2.8.3) httpclient (2.9.0)
mutex_m
httplog (1.7.0) httplog (1.7.0)
rack (>= 2.0) rack (>= 2.0)
rainbow (>= 2.0.0) rainbow (>= 2.0.0)
@ -378,7 +383,7 @@ GEM
mime-types mime-types
terrapin (>= 0.6.0, < 2.0) terrapin (>= 0.6.0, < 2.0)
language_server-protocol (3.17.0.4) language_server-protocol (3.17.0.4)
launchy (3.1.0) launchy (3.1.1)
addressable (~> 2.8) addressable (~> 2.8)
childprocess (~> 5.0) childprocess (~> 5.0)
logger (~> 1.6) logger (~> 1.6)
@ -391,7 +396,7 @@ GEM
rexml rexml
link_header (0.0.8) link_header (0.0.8)
lint_roller (1.1.0) lint_roller (1.1.0)
llhttp-ffi (0.5.0) llhttp-ffi (0.5.1)
ffi-compiler (~> 1.0) ffi-compiler (~> 1.0)
rake (~> 13.0) rake (~> 13.0)
logger (1.6.6) logger (1.6.6)
@ -416,10 +421,10 @@ GEM
mime-types (3.6.0) mime-types (3.6.0)
logger logger
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2025.0220) mime-types-data (3.2025.0304)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.8) mini_portile2 (2.8.8)
minitest (5.25.4) minitest (5.25.5)
msgpack (1.8.0) msgpack (1.8.0)
multi_json (1.15.0) multi_json (1.15.0)
mutex_m (0.3.0) mutex_m (0.3.0)
@ -729,7 +734,7 @@ GEM
rspec-mocks (~> 3.0) rspec-mocks (~> 3.0)
sidekiq (>= 5, < 9) sidekiq (>= 5, < 9)
rspec-support (3.13.2) rspec-support (3.13.2)
rubocop (1.73.2) rubocop (1.74.0)
json (~> 2.3) json (~> 2.3)
language_server-protocol (~> 3.17.0.2) language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0) lint_roller (~> 1.1.0)
@ -1069,4 +1074,4 @@ RUBY VERSION
ruby 3.4.1p0 ruby 3.4.1p0
BUNDLED WITH BUNDLED WITH
2.6.5 2.6.6

View file

@ -119,7 +119,7 @@ class Api::V1::AccountsController < Api::BaseController
end end
def account_params def account_params
params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code) params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code, :date_of_birth)
end end
def invite def invite

View file

@ -62,7 +62,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
def configure_sign_up_params def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up) do |user_params| devise_parameter_sanitizer.permit(:sign_up) do |user_params|
user_params.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password) user_params.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password, :date_of_birth)
end end
end end

View file

@ -0,0 +1,31 @@
# frozen_string_literal: true
class DateOfBirthInput < SimpleForm::Inputs::Base
OPTIONS = [
{ autocomplete: 'bday-day', maxlength: 2, pattern: '[0-9]+', placeholder: 'DD' }.freeze,
{ autocomplete: 'bday-month', maxlength: 2, pattern: '[0-9]+', placeholder: 'MM' }.freeze,
{ autocomplete: 'bday-year', maxlength: 4, pattern: '[0-9]+', placeholder: 'YYYY' }.freeze,
].freeze
def input(wrapper_options = nil)
merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)
merged_input_options[:inputmode] = 'numeric'
values = (object.public_send(attribute_name) || '').split('.')
safe_join(Array.new(3) do |index|
options = merged_input_options.merge(OPTIONS[index]).merge id: generate_id(index), 'aria-label': I18n.t("simple_form.labels.user.date_of_birth_#{index + 1}i"), value: values[index]
@builder.text_field("#{attribute_name}(#{index + 1}i)", options)
end)
end
def label_target
"#{attribute_name}_1i"
end
private
def generate_id(index)
"#{object_name}_#{attribute_name}_#{index + 1}i"
end
end

View file

@ -9288,6 +9288,7 @@ noscript {
border: 1px solid $highlight-text-color; border: 1px solid $highlight-text-color;
background: rgba($highlight-text-color, 0.15); background: rgba($highlight-text-color, 0.15);
overflow: hidden; overflow: hidden;
flex-shrink: 0;
&__background-image { &__background-image {
width: 125%; width: 125%;

View file

@ -353,6 +353,22 @@ code {
} }
} }
.input.date_of_birth .label_input {
display: flex;
gap: 8px;
align-items: center;
input {
box-sizing: content-box;
width: 32px;
flex: 0;
&:last-child {
width: 64px;
}
}
}
.input.select.select--languages { .input.select.select--languages {
min-width: 32ch; min-width: 32ch;
} }

View file

@ -46,12 +46,14 @@ class Form::AdminSettings
authorized_fetch authorized_fetch
app_icon app_icon
favicon favicon
min_age
).freeze ).freeze
INTEGER_KEYS = %i( INTEGER_KEYS = %i(
media_cache_retention_period media_cache_retention_period
content_cache_retention_period content_cache_retention_period
backups_retention_period backups_retention_period
min_age
).freeze ).freeze
BOOLEAN_KEYS = %i( BOOLEAN_KEYS = %i(
@ -103,6 +105,7 @@ class Form::AdminSettings
validates :show_domain_blocks, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks) } validates :show_domain_blocks, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks) }
validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks_rationale) } validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks_rationale) }
validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) } validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) }
validates :min_age, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@min_age) }
validates :site_short_description, length: { maximum: DESCRIPTION_LIMIT }, if: -> { defined?(@site_short_description) } validates :site_short_description, length: { maximum: DESCRIPTION_LIMIT }, if: -> { defined?(@site_short_description) }
validates :status_page_url, url: true, allow_blank: true validates :status_page_url, url: true, allow_blank: true
validate :validate_site_uploads validate :validate_site_uploads

View file

@ -115,7 +115,7 @@ class MediaAttachment < ApplicationRecord
VIDEO_PASSTHROUGH_OPTIONS = { VIDEO_PASSTHROUGH_OPTIONS = {
video_codecs: ['h264'].freeze, video_codecs: ['h264'].freeze,
audio_codecs: ['aac', nil].freeze, audio_codecs: ['aac', nil].freeze,
colorspaces: ['yuv420p'].freeze, colorspaces: ['yuv420p', 'yuvj420p'].freeze,
options: { options: {
format: 'mp4', format: 'mp4',
convert_options: { convert_options: {

View file

@ -5,41 +5,42 @@
# Table name: users # Table name: users
# #
# id :bigint(8) not null, primary key # id :bigint(8) not null, primary key
# email :string default(""), not null # age_verified_at :datetime
# created_at :datetime not null # approved :boolean default(TRUE), not null
# updated_at :datetime not null # chosen_languages :string is an Array
# encrypted_password :string default(""), not null # confirmation_sent_at :datetime
# reset_password_token :string
# reset_password_sent_at :datetime
# sign_in_count :integer default(0), not null
# current_sign_in_at :datetime
# last_sign_in_at :datetime
# confirmation_token :string # confirmation_token :string
# confirmed_at :datetime # confirmed_at :datetime
# confirmation_sent_at :datetime # consumed_timestep :integer
# unconfirmed_email :string # current_sign_in_at :datetime
# locale :string # disabled :boolean default(FALSE), not null
# email :string default(""), not null
# encrypted_otp_secret :string # encrypted_otp_secret :string
# encrypted_otp_secret_iv :string # encrypted_otp_secret_iv :string
# encrypted_otp_secret_salt :string # encrypted_otp_secret_salt :string
# consumed_timestep :integer # encrypted_password :string default(""), not null
# otp_required_for_login :boolean default(FALSE), not null
# last_emailed_at :datetime # last_emailed_at :datetime
# last_sign_in_at :datetime
# locale :string
# otp_backup_codes :string is an Array # otp_backup_codes :string is an Array
# account_id :bigint(8) not null # otp_required_for_login :boolean default(FALSE), not null
# disabled :boolean default(FALSE), not null # otp_secret :string
# invite_id :bigint(8) # reset_password_sent_at :datetime
# chosen_languages :string is an Array # reset_password_token :string
# created_by_application_id :bigint(8) # settings :text
# approved :boolean default(TRUE), not null # sign_in_count :integer default(0), not null
# sign_in_token :string # sign_in_token :string
# sign_in_token_sent_at :datetime # sign_in_token_sent_at :datetime
# webauthn_id :string
# sign_up_ip :inet # sign_up_ip :inet
# role_id :bigint(8)
# settings :text
# time_zone :string # time_zone :string
# otp_secret :string # unconfirmed_email :string
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint(8) not null
# created_by_application_id :bigint(8)
# invite_id :bigint(8)
# role_id :bigint(8)
# webauthn_id :string
# #
class User < ApplicationRecord class User < ApplicationRecord
@ -111,6 +112,7 @@ class User < ApplicationRecord
validates_with RegistrationFormTimeValidator, on: :create validates_with RegistrationFormTimeValidator, on: :create
validates :website, absence: true, on: :create validates :website, absence: true, on: :create
validates :confirm_password, absence: true, on: :create validates :confirm_password, absence: true, on: :create
validates :date_of_birth, presence: true, date_of_birth: true, on: :create, if: -> { Setting.min_age.present? }
validate :validate_role_elevation validate :validate_role_elevation
scope :account_not_suspended, -> { joins(:account).merge(Account.without_suspended) } scope :account_not_suspended, -> { joins(:account).merge(Account.without_suspended) }
@ -129,6 +131,7 @@ class User < ApplicationRecord
before_validation :sanitize_role before_validation :sanitize_role
before_create :set_approved before_create :set_approved
before_create :set_age_verified_at
after_commit :send_pending_devise_notifications after_commit :send_pending_devise_notifications
after_create_commit :trigger_webhooks after_create_commit :trigger_webhooks
@ -140,7 +143,7 @@ class User < ApplicationRecord
delegate :can?, to: :role delegate :can?, to: :role
attr_reader :invite_code attr_reader :invite_code, :date_of_birth
attr_writer :external, :bypass_invite_request_check, :current_account attr_writer :external, :bypass_invite_request_check, :current_account
def self.those_who_can(*any_of_privileges) def self.those_who_can(*any_of_privileges)
@ -157,6 +160,17 @@ class User < ApplicationRecord
Rails.env.local? Rails.env.local?
end end
def date_of_birth=(hash_or_string)
@date_of_birth = begin
if hash_or_string.is_a?(Hash)
day, month, year = hash_or_string.values_at(1, 2, 3)
"#{day}.#{month}.#{year}"
else
hash_or_string
end
end
end
def role def role
if role_id.nil? if role_id.nil?
UserRole.everyone UserRole.everyone
@ -432,6 +446,10 @@ class User < ApplicationRecord
end end
end end
def set_age_verified_at
self.age_verified_at = Time.now.utc if Setting.min_age.present?
end
def grant_approval_on_confirmation? def grant_approval_on_confirmation?
# Re-check approval on confirmation if the server has switched to open registrations # Re-check approval on confirmation if the server has switched to open registrations
open_registrations? && !sign_up_from_ip_requires_approval? && !sign_up_email_requires_approval? open_registrations? && !sign_up_from_ip_requires_approval? && !sign_up_email_requires_approval?

View file

@ -108,6 +108,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
enabled: registrations_enabled?, enabled: registrations_enabled?,
approval_required: Setting.registrations_mode == 'approved', approval_required: Setting.registrations_mode == 'approved',
message: registrations_enabled? ? nil : registrations_message, message: registrations_enabled? ? nil : registrations_message,
min_age: Setting.min_age.presence,
url: ENV.fetch('SSO_ACCOUNT_SIGN_UP', nil), url: ENV.fetch('SSO_ACCOUNT_SIGN_UP', nil),
} }
end end

View file

@ -41,7 +41,7 @@ class AppSignUpService < BaseService
end end
def user_params def user_params
@params.slice(:email, :password, :agreement, :locale, :time_zone, :invite_code) @params.slice(:email, :password, :agreement, :locale, :time_zone, :invite_code, :date_of_birth)
end end
def account_params def account_params

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
class DateOfBirthValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors.add(attribute, :below_limit) if value.present? && value.to_date > min_age.ago
rescue Date::Error
record.errors.add(attribute, :invalid)
end
private
def min_age
Setting.min_age.to_i.years
end
end

View file

@ -12,6 +12,9 @@
.flash-message= t('admin.settings.registrations.moderation_recommandation') .flash-message= t('admin.settings.registrations.moderation_recommandation')
.fields-group
= f.input :min_age, as: :string, wrapper: :with_block_label, input_html: { inputmode: 'numeric' }
.fields-row .fields-row
.fields-row__column.fields-row__column-6.fields-group .fields-row__column.fields-row__column-6.fields-group
= f.input :registrations_mode, = f.input :registrations_mode,

View file

@ -21,20 +21,19 @@
= f.simple_fields_for :account do |ff| = f.simple_fields_for :account do |ff|
= ff.input :username, = ff.input :username,
append: "@#{site_hostname}", append: "@#{site_hostname}",
input_html: { 'aria-label': t('simple_form.labels.defaults.username'), autocomplete: 'off', placeholder: t('simple_form.labels.defaults.username'), pattern: '[a-zA-Z0-9_]+', maxlength: Account::USERNAME_LENGTH_LIMIT }, input_html: { autocomplete: 'off', pattern: '[a-zA-Z0-9_]+', maxlength: Account::USERNAME_LENGTH_LIMIT, placeholder: ' ' },
label: false,
required: true, required: true,
wrapper: :with_label wrapper: :with_label
= f.input :email, = f.input :email,
hint: false, hint: false,
input_html: { 'aria-label': t('simple_form.labels.defaults.email'), autocomplete: 'username' }, input_html: { autocomplete: 'username', placeholder: ' ' },
placeholder: t('simple_form.labels.defaults.email'), required: true,
required: true wrapper: :with_label
= f.input :password, = f.input :password,
hint: false, hint: false,
input_html: { 'aria-label': t('simple_form.labels.defaults.password'), autocomplete: 'new-password', minlength: User.password_length.first, maxlength: User.password_length.last }, input_html: { autocomplete: 'new-password', minlength: User.password_length.first, maxlength: User.password_length.last, placeholder: ' ' },
placeholder: t('simple_form.labels.defaults.password'), required: true,
required: true wrapper: :with_label
= f.input :password_confirmation, = f.input :password_confirmation,
hint: false, hint: false,
input_html: { 'aria-label': t('simple_form.labels.defaults.confirm_password'), autocomplete: 'new-password', maxlength: User.password_length.last }, input_html: { 'aria-label': t('simple_form.labels.defaults.confirm_password'), autocomplete: 'new-password', maxlength: User.password_length.last },
@ -53,6 +52,14 @@
required: false, required: false,
wrapper: :with_label wrapper: :with_label
- if Setting.min_age.present?
.fields-group
= f.input :date_of_birth,
as: :date_of_birth,
hint: t('simple_form.hints.user.date_of_birth', age: Setting.min_age.to_i),
required: true,
wrapper: :with_block_label
- if approved_registrations? && @invite.blank? - if approved_registrations? && @invite.blank?
%p.lead= t('auth.sign_up.manual_review', domain: site_hostname) %p.lead= t('auth.sign_up.manual_review', domain: site_hostname)

View file

@ -1,8 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
if ENV['MASTODON_PROMETHEUS_EXPORTER_ENABLED'] == 'true' if ENV['MASTODON_PROMETHEUS_EXPORTER_ENABLED'] == 'true'
require 'prometheus_exporter'
require 'prometheus_exporter/middleware'
if ENV['MASTODON_PROMETHEUS_EXPORTER_LOCAL'] == 'true' if ENV['MASTODON_PROMETHEUS_EXPORTER_LOCAL'] == 'true'
require 'prometheus_exporter'
require 'prometheus_exporter/server' require 'prometheus_exporter/server'
require 'prometheus_exporter/client' require 'prometheus_exporter/client'
@ -17,9 +19,11 @@ if ENV['MASTODON_PROMETHEUS_EXPORTER_ENABLED'] == 'true'
if ENV['MASTODON_PROMETHEUS_EXPORTER_WEB_DETAILED_METRICS'] == 'true' if ENV['MASTODON_PROMETHEUS_EXPORTER_WEB_DETAILED_METRICS'] == 'true'
# Optional, as those metrics might generate extra overhead and be redundant with what OTEL provides # Optional, as those metrics might generate extra overhead and be redundant with what OTEL provides
require 'prometheus_exporter/middleware'
# Per-action/controller request stats like HTTP status and timings # Per-action/controller request stats like HTTP status and timings
Rails.application.middleware.unshift PrometheusExporter::Middleware Rails.application.middleware.unshift PrometheusExporter::Middleware
else
# Include stripped down version of PrometheusExporter::Middleware that only collects queue time
require 'mastodon/middleware/prometheus_queue_time'
Rails.application.middleware.unshift Mastodon::Middleware::PrometheusQueueTime
end end
end end

View file

@ -55,6 +55,8 @@ en:
too_soon: is too soon, must be later than %{date} too_soon: is too soon, must be later than %{date}
user: user:
attributes: attributes:
date_of_birth:
below_limit: is below the age limit
email: email:
blocked: uses a disallowed e-mail provider blocked: uses a disallowed e-mail provider
unreachable: does not seem to exist unreachable: does not seem to exist

View file

@ -136,6 +136,7 @@ bg:
text: Може да се структурира със синтаксиса на Markdown. text: Може да се структурира със синтаксиса на Markdown.
terms_of_service_generator: terms_of_service_generator:
admin_email: Правните бележки включват насрещни известия, постановления на съда, заявки за сваляне и заявки от правоохранителните органи. admin_email: Правните бележки включват насрещни известия, постановления на съда, заявки за сваляне и заявки от правоохранителните органи.
arbitration_address: Може да е същото като физическия адрес горе или "неприложимо", ако се употребява имейл.
choice_of_law: Град, регион, територия, щат или държава, чиито вътрешни материални права ще уреждат всички искове. choice_of_law: Град, регион, територия, щат или държава, чиито вътрешни материални права ще уреждат всички искове.
domain: Неповторимо идентифициране на онлайн услугата, която предоставяте. domain: Неповторимо идентифициране на онлайн услугата, която предоставяте.
jurisdiction: Впишете държавата, където живее всеки, който плаща сметките. Ако е дружество или друго образувание, то впишете държавата, в която е регистрирано, и градът, регионът, територията или щатът според случая. jurisdiction: Впишете държавата, където живее всеки, който плаща сметките. Ако е дружество или друго образувание, то впишете държавата, в която е регистрирано, и градът, регионът, територията или щатът според случая.

View file

@ -88,6 +88,7 @@ en:
favicon: WEBP, PNG, GIF or JPG. Overrides the default Mastodon favicon with a custom icon. favicon: WEBP, PNG, GIF or JPG. Overrides the default Mastodon favicon with a custom icon.
mascot: Overrides the illustration in the advanced web interface. mascot: Overrides the illustration in the advanced web interface.
media_cache_retention_period: Media files from posts made by remote users are cached on your server. When set to a positive value, media will be deleted after the specified number of days. If the media data is requested after it is deleted, it will be re-downloaded, if the source content is still available. Due to restrictions on how often link preview cards poll third-party sites, it is recommended to set this value to at least 14 days, or link preview cards will not be updated on demand before that time. media_cache_retention_period: Media files from posts made by remote users are cached on your server. When set to a positive value, media will be deleted after the specified number of days. If the media data is requested after it is deleted, it will be re-downloaded, if the source content is still available. Due to restrictions on how often link preview cards poll third-party sites, it is recommended to set this value to at least 14 days, or link preview cards will not be updated on demand before that time.
min_age: Users will be asked to confirm their date of birth during sign-up
peers_api_enabled: A list of domain names this server has encountered in the fediverse. No data is included here about whether you federate with a given server, just that your server knows about it. This is used by services that collect statistics on federation in a general sense. peers_api_enabled: A list of domain names this server has encountered in the fediverse. No data is included here about whether you federate with a given server, just that your server knows about it. This is used by services that collect statistics on federation in a general sense.
profile_directory: The profile directory lists all users who have opted-in to be discoverable. profile_directory: The profile directory lists all users who have opted-in to be discoverable.
require_invite_text: When sign-ups require manual approval, make the “Why do you want to join?” text input mandatory rather than optional require_invite_text: When sign-ups require manual approval, make the “Why do you want to join?” text input mandatory rather than optional
@ -146,6 +147,7 @@ en:
min_age: Should not be below the minimum age required by the laws of your jurisdiction. min_age: Should not be below the minimum age required by the laws of your jurisdiction.
user: user:
chosen_languages: When checked, only posts in selected languages will be displayed in public timelines chosen_languages: When checked, only posts in selected languages will be displayed in public timelines
date_of_birth: We have to make sure you're at least %{age} to use Mastodon. We won't store this.
role: The role controls which permissions the user has. role: The role controls which permissions the user has.
user_role: user_role:
color: Color to be used for the role throughout the UI, as RGB in hex format color: Color to be used for the role throughout the UI, as RGB in hex format
@ -271,6 +273,7 @@ en:
favicon: Favicon favicon: Favicon
mascot: Custom mascot (legacy) mascot: Custom mascot (legacy)
media_cache_retention_period: Media cache retention period media_cache_retention_period: Media cache retention period
min_age: Minimum age requirement
peers_api_enabled: Publish list of discovered servers in the API peers_api_enabled: Publish list of discovered servers in the API
profile_directory: Enable profile directory profile_directory: Enable profile directory
registrations_mode: Who can sign-up registrations_mode: Who can sign-up
@ -349,6 +352,9 @@ en:
jurisdiction: Legal jurisdiction jurisdiction: Legal jurisdiction
min_age: Minimum age min_age: Minimum age
user: user:
date_of_birth_1i: Day
date_of_birth_2i: Month
date_of_birth_3i: Year
role: Role role: Role
time_zone: Time zone time_zone: Time zone
user_role: user_role:

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddAgeVerifiedAtToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :age_verified_at, :datetime
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_03_05_074104) do ActiveRecord::Schema[8.0].define(version: 2025_03_13_123400) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql" enable_extension "pg_catalog.plpgsql"
@ -1191,6 +1191,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_05_074104) do
t.text "settings" t.text "settings"
t.string "time_zone" t.string "time_zone"
t.string "otp_secret" t.string "otp_secret"
t.datetime "age_verified_at"
t.index ["account_id"], name: "index_users_on_account_id" t.index ["account_id"], name: "index_users_on_account_id"
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id", where: "(created_by_application_id IS NOT NULL)" t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id", where: "(created_by_application_id IS NOT NULL)"

View file

@ -0,0 +1,26 @@
# frozen_string_literal: true
module Mastodon
module Middleware
class PrometheusQueueTime < ::PrometheusExporter::Middleware
# Overwrite to only collect the queue time metric
def call(env)
queue_time = measure_queue_time(env)
result = @app.call(env)
result
ensure
obj = {
type: 'web',
queue_time: queue_time,
default_labels: default_labels(env, result),
}
labels = custom_labels(env)
obj = obj.merge(custom_labels: labels) if labels
@client.send_json(obj)
end
end
end
end

View file

@ -342,6 +342,42 @@ RSpec.describe Auth::RegistrationsController do
end end
end end
context 'when age verification is enabled' do
subject { post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', agreement: 'true' }.merge(date_of_birth) } }
before do
Setting.min_age = 16
end
let(:date_of_birth) { {} }
context 'when date of birth is below age limit' do
let(:date_of_birth) { 13.years.ago.then { |date| { 'date_of_birth(1i)': date.day.to_s, 'date_of_birth(2i)': date.month.to_s, 'date_of_birth(3i)': date.year.to_s } } }
it 'does not create user' do
subject
user = User.find_by(email: 'test@example.com')
expect(user).to be_nil
end
end
context 'when date of birth is above age limit' do
let(:date_of_birth) { 17.years.ago.then { |date| { 'date_of_birth(1i)': date.day.to_s, 'date_of_birth(2i)': date.month.to_s, 'date_of_birth(3i)': date.year.to_s } } }
it 'redirects to setup and creates user' do
subject
expect(response).to redirect_to auth_setup_path
expect(User.find_by(email: 'test@example.com'))
.to be_present
.and have_attributes(
age_verified_at: not_eq(nil)
)
end
end
end
include_examples 'checks for enabled registrations', :create include_examples 'checks for enabled registrations', :create
end end

View file

@ -0,0 +1,32 @@
# frozen_string_literal: true
require 'rails_helper'
require 'prometheus_exporter'
require 'prometheus_exporter/middleware'
require 'mastodon/middleware/prometheus_queue_time'
RSpec.describe Mastodon::Middleware::PrometheusQueueTime do
subject { described_class.new(app, client:) }
let(:app) do
proc { |_env| [200, {}, 'OK'] }
end
let(:client) do
instance_double(PrometheusExporter::Client, send_json: true)
end
describe '#call' do
let(:env) do
{
'HTTP_X_REQUEST_START' => "t=#{(Time.now.to_f * 1000).to_i}",
}
end
it 'reports a queue time to the client' do
subject.call(env)
expect(client).to have_received(:send_json)
.with(hash_including(queue_time: instance_of(Float)))
end
end
end

View file

@ -74,12 +74,45 @@ RSpec.describe '/api/v1/accounts' do
describe 'POST /api/v1/accounts' do describe 'POST /api/v1/accounts' do
subject do subject do
post '/api/v1/accounts', headers: headers, params: { username: 'test', password: '12345678', email: 'hello@world.tld', agreement: agreement } post '/api/v1/accounts', headers: headers, params: { username: 'test', password: '12345678', email: 'hello@world.tld', agreement: agreement, date_of_birth: date_of_birth }
end end
let(:client_app) { Fabricate(:application) } let(:client_app) { Fabricate(:application) }
let(:token) { Doorkeeper::AccessToken.find_or_create_for(application: client_app, resource_owner: nil, scopes: 'read write', use_refresh_token: false) } let(:token) { Doorkeeper::AccessToken.find_or_create_for(application: client_app, resource_owner: nil, scopes: 'read write', use_refresh_token: false) }
let(:agreement) { nil } let(:agreement) { nil }
let(:date_of_birth) { nil }
context 'when age verification is enabled' do
before do
Setting.min_age = 16
end
let(:agreement) { 'true' }
context 'when date of birth is below age limit' do
let(:date_of_birth) { 13.years.ago.strftime('%d.%m.%Y') }
it 'returns http unprocessable entity' do
subject
expect(response).to have_http_status(422)
expect(response.content_type)
.to start_with('application/json')
end
end
context 'when date of birth is over age limit' do
let(:date_of_birth) { 17.years.ago.strftime('%d.%m.%Y') }
it 'creates a user', :aggregate_failures do
subject
expect(response).to have_http_status(200)
expect(response.content_type)
.to start_with('application/json')
end
end
end
context 'when given truthy agreement' do context 'when given truthy agreement' do
let(:agreement) { 'true' } let(:agreement) { 'true' }

View file

@ -0,0 +1,51 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe DateOfBirthValidator do
let(:record_class) do
Class.new do
include ActiveModel::Validations
attr_accessor :date_of_birth
validates :date_of_birth, date_of_birth: true
end
end
let(:record) { record_class.new }
before do
Setting.min_age = 16
end
describe '#validate_each' do
context 'with an invalid date' do
it 'adds errors' do
record.date_of_birth = '76.830.10'
expect(record).to_not be_valid
expect(record.errors.first.attribute).to eq(:date_of_birth)
expect(record.errors.first.type).to eq(:invalid)
end
end
context 'with a date below age limit' do
it 'adds errors' do
record.date_of_birth = 13.years.ago.strftime('%d.%m.%Y')
expect(record).to_not be_valid
expect(record.errors.first.attribute).to eq(:date_of_birth)
expect(record.errors.first.type).to eq(:below_limit)
end
end
context 'with a date above age limit' do
it 'does not add errors' do
record.date_of_birth = 16.years.ago.strftime('%d.%m.%Y')
expect(record).to be_valid
end
end
end
end

View file

@ -14950,15 +14950,15 @@ __metadata:
linkType: hard linkType: hard
"react-textarea-autosize@npm:^8.4.1": "react-textarea-autosize@npm:^8.4.1":
version: 8.5.7 version: 8.5.8
resolution: "react-textarea-autosize@npm:8.5.7" resolution: "react-textarea-autosize@npm:8.5.8"
dependencies: dependencies:
"@babel/runtime": "npm:^7.20.13" "@babel/runtime": "npm:^7.20.13"
use-composed-ref: "npm:^1.3.0" use-composed-ref: "npm:^1.3.0"
use-latest: "npm:^1.2.1" use-latest: "npm:^1.2.1"
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 10c0/ff004797ea28faca442460c42b30042d4c34a140f324eeeddee74508688dbc0f98966d21282c945630655006ad28a87edbcb59e6da7f9e762f4f3042c72f9f24 checksum: 10c0/3d7add9773fd3dc189a6668efb82c1d2d5238ee5b7e933204f5f7da9df8daef81df50b36ca573a57beaa31b2727c6176ea806422790ad23be6cf7f5a5f71bbb9
languageName: node languageName: node
linkType: hard linkType: hard