From f4f444528a9b8fe3d5b0b47c9f9a1bdd1eada3cb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Mar 2025 10:03:20 +0100 Subject: [PATCH 01/78] Update dependency react-textarea-autosize to v8.5.8 (#34169) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 283e1a2b55..060b028e83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14926,15 +14926,15 @@ __metadata: linkType: hard "react-textarea-autosize@npm:^8.4.1": - version: 8.5.7 - resolution: "react-textarea-autosize@npm:8.5.7" + version: 8.5.8 + resolution: "react-textarea-autosize@npm:8.5.8" dependencies: "@babel/runtime": "npm:^7.20.13" use-composed-ref: "npm:^1.3.0" use-latest: "npm:^1.2.1" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10c0/ff004797ea28faca442460c42b30042d4c34a140f324eeeddee74508688dbc0f98966d21282c945630655006ad28a87edbcb59e6da7f9e762f4f3042c72f9f24 + checksum: 10c0/3d7add9773fd3dc189a6668efb82c1d2d5238ee5b7e933204f5f7da9df8daef81df50b36ca573a57beaa31b2727c6176ea806422790ad23be6cf7f5a5f71bbb9 languageName: node linkType: hard From 7a6a898ca176fe94a856ab0cd1e31ac66c7962d5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Mar 2025 10:17:00 +0100 Subject: [PATCH 02/78] New Crowdin Translations (automated) (#34170) Co-authored-by: GitHub Actions --- config/locales/simple_form.bg.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/locales/simple_form.bg.yml b/config/locales/simple_form.bg.yml index 7a4fea91b2..bff9860005 100644 --- a/config/locales/simple_form.bg.yml +++ b/config/locales/simple_form.bg.yml @@ -136,6 +136,7 @@ bg: text: Може да се структурира със синтаксиса на Markdown. terms_of_service_generator: admin_email: Правните бележки включват насрещни известия, постановления на съда, заявки за сваляне и заявки от правоохранителните органи. + arbitration_address: Може да е същото като физическия адрес горе или "неприложимо", ако се употребява имейл. choice_of_law: Град, регион, територия, щат или държава, чиито вътрешни материални права ще уреждат всички искове. domain: Неповторимо идентифициране на онлайн услугата, която предоставяте. jurisdiction: Впишете държавата, където живее всеки, който плаща сметките. Ако е дружество или друго образувание, то впишете държавата, в която е регистрирано, и градът, регионът, територията или щатът според случая. From 2d97215aadf9defd60916b47b8e796da16c400fa Mon Sep 17 00:00:00 2001 From: rinsuki <428rinsuki+git@gmail.com> Date: Fri, 14 Mar 2025 18:30:14 +0900 Subject: [PATCH 03/78] chore: Allow yuvj420p (full color range yuv420p) movies passthrough (#34098) --- app/models/media_attachment.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 6708cd7793..72330aa0aa 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -115,7 +115,7 @@ class MediaAttachment < ApplicationRecord VIDEO_PASSTHROUGH_OPTIONS = { video_codecs: ['h264'].freeze, audio_codecs: ['aac', nil].freeze, - colorspaces: ['yuv420p'].freeze, + colorspaces: ['yuv420p', 'yuvj420p'].freeze, options: { format: 'mp4', convert_options: { From 24ec83ee520d19ca5348505deefd008fb5e0c261 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 14 Mar 2025 12:10:26 +0100 Subject: [PATCH 04/78] Fix timeline banners sizing (#34171) --- app/javascript/styles/mastodon/components.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 5e44553da8..011f8263f4 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -9288,6 +9288,7 @@ noscript { border: 1px solid $highlight-text-color; background: rgba($highlight-text-color, 0.15); overflow: hidden; + flex-shrink: 0; &__background-image { width: 125%; From 4a6cf67c4692a7be9d5fdc7083a9bda7956d3cad Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Fri, 14 Mar 2025 14:52:04 +0100 Subject: [PATCH 05/78] Add middleware to record queue time (#34172) --- config/initializers/prometheus_exporter.rb | 10 ++++-- .../middleware/prometheus_queue_time.rb | 26 +++++++++++++++ .../middleware/prometheus_queue_time_spec.rb | 32 +++++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 lib/mastodon/middleware/prometheus_queue_time.rb create mode 100644 spec/lib/mastodon/middleware/prometheus_queue_time_spec.rb diff --git a/config/initializers/prometheus_exporter.rb b/config/initializers/prometheus_exporter.rb index fab095658f..fab08ceebb 100644 --- a/config/initializers/prometheus_exporter.rb +++ b/config/initializers/prometheus_exporter.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true if ENV['MASTODON_PROMETHEUS_EXPORTER_ENABLED'] == 'true' + require 'prometheus_exporter' + require 'prometheus_exporter/middleware' + if ENV['MASTODON_PROMETHEUS_EXPORTER_LOCAL'] == 'true' - require 'prometheus_exporter' require 'prometheus_exporter/server' require 'prometheus_exporter/client' @@ -17,9 +19,11 @@ if ENV['MASTODON_PROMETHEUS_EXPORTER_ENABLED'] == '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 - require 'prometheus_exporter/middleware' - # Per-action/controller request stats like HTTP status and timings 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 diff --git a/lib/mastodon/middleware/prometheus_queue_time.rb b/lib/mastodon/middleware/prometheus_queue_time.rb new file mode 100644 index 0000000000..fae171612d --- /dev/null +++ b/lib/mastodon/middleware/prometheus_queue_time.rb @@ -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 diff --git a/spec/lib/mastodon/middleware/prometheus_queue_time_spec.rb b/spec/lib/mastodon/middleware/prometheus_queue_time_spec.rb new file mode 100644 index 0000000000..eaab93772d --- /dev/null +++ b/spec/lib/mastodon/middleware/prometheus_queue_time_spec.rb @@ -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 From d213c585ffdcbbff3a553c829cd82cc5e2357897 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 14 Mar 2025 15:07:29 +0100 Subject: [PATCH 06/78] Add age verification on sign-up (#34150) --- app/controllers/api/v1/accounts_controller.rb | 2 +- .../auth/registrations_controller.rb | 2 +- app/inputs/date_of_birth_input.rb | 31 +++++++++ app/javascript/styles/mastodon/forms.scss | 16 +++++ app/models/form/admin_settings.rb | 3 + app/models/user.rb | 68 ++++++++++++------- app/serializers/rest/instance_serializer.rb | 1 + app/services/app_sign_up_service.rb | 2 +- app/validators/date_of_birth_validator.rb | 15 ++++ .../settings/registrations/show.html.haml | 3 + app/views/auth/registrations/new.html.haml | 23 ++++--- config/locales/activerecord.en.yml | 2 + config/locales/simple_form.en.yml | 6 ++ ...0313123400_add_age_verified_at_to_users.rb | 7 ++ db/schema.rb | 3 +- .../auth/registrations_controller_spec.rb | 36 ++++++++++ spec/requests/api/v1/accounts_spec.rb | 35 +++++++++- .../date_of_birth_validator_spec.rb | 51 ++++++++++++++ 18 files changed, 268 insertions(+), 38 deletions(-) create mode 100644 app/inputs/date_of_birth_input.rb create mode 100644 app/validators/date_of_birth_validator.rb create mode 100644 db/migrate/20250313123400_add_age_verified_at_to_users.rb create mode 100644 spec/validators/date_of_birth_validator_spec.rb diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index f7d3de7f94..ae8df69a28 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -119,7 +119,7 @@ class Api::V1::AccountsController < Api::BaseController end 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 def invite diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 6e34b6b627..0b6f5b3af4 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -62,7 +62,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController def configure_sign_up_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 diff --git a/app/inputs/date_of_birth_input.rb b/app/inputs/date_of_birth_input.rb new file mode 100644 index 0000000000..131234b02e --- /dev/null +++ b/app/inputs/date_of_birth_input.rb @@ -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 diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 7df7e14b2b..73043842a4 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -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 { min-width: 32ch; } diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index ffd2d4049c..086a6d29d4 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -39,12 +39,14 @@ class Form::AdminSettings authorized_fetch app_icon favicon + min_age ).freeze INTEGER_KEYS = %i( media_cache_retention_period content_cache_retention_period backups_retention_period + min_age ).freeze BOOLEAN_KEYS = %i( @@ -88,6 +90,7 @@ class Form::AdminSettings 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 :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 :status_page_url, url: true, allow_blank: true validate :validate_site_uploads diff --git a/app/models/user.rb b/app/models/user.rb index ce24d34651..72f7490043 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,41 +5,42 @@ # Table name: users # # id :bigint(8) not null, primary key -# email :string default(""), not null -# created_at :datetime not null -# updated_at :datetime not null -# encrypted_password :string default(""), not null -# 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 +# age_verified_at :datetime +# approved :boolean default(TRUE), not null +# chosen_languages :string is an Array +# confirmation_sent_at :datetime # confirmation_token :string # confirmed_at :datetime -# confirmation_sent_at :datetime -# unconfirmed_email :string -# locale :string +# consumed_timestep :integer +# current_sign_in_at :datetime +# disabled :boolean default(FALSE), not null +# email :string default(""), not null # encrypted_otp_secret :string # encrypted_otp_secret_iv :string # encrypted_otp_secret_salt :string -# consumed_timestep :integer -# otp_required_for_login :boolean default(FALSE), not null +# encrypted_password :string default(""), not null # last_emailed_at :datetime +# last_sign_in_at :datetime +# locale :string # otp_backup_codes :string is an Array -# account_id :bigint(8) not null -# disabled :boolean default(FALSE), not null -# invite_id :bigint(8) -# chosen_languages :string is an Array -# created_by_application_id :bigint(8) -# approved :boolean default(TRUE), not null +# otp_required_for_login :boolean default(FALSE), not null +# otp_secret :string +# reset_password_sent_at :datetime +# reset_password_token :string +# settings :text +# sign_in_count :integer default(0), not null # sign_in_token :string # sign_in_token_sent_at :datetime -# webauthn_id :string # sign_up_ip :inet -# role_id :bigint(8) -# settings :text # 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 @@ -111,6 +112,7 @@ class User < ApplicationRecord validates_with RegistrationFormTimeValidator, on: :create validates :website, 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 scope :account_not_suspended, -> { joins(:account).merge(Account.without_suspended) } @@ -129,6 +131,7 @@ class User < ApplicationRecord before_validation :sanitize_role before_create :set_approved + before_create :set_age_verified_at after_commit :send_pending_devise_notifications after_create_commit :trigger_webhooks @@ -140,7 +143,7 @@ class User < ApplicationRecord delegate :can?, to: :role - attr_reader :invite_code + attr_reader :invite_code, :date_of_birth attr_writer :external, :bypass_invite_request_check, :current_account def self.those_who_can(*any_of_privileges) @@ -157,6 +160,17 @@ class User < ApplicationRecord Rails.env.local? 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 if role_id.nil? UserRole.everyone @@ -432,6 +446,10 @@ class User < ApplicationRecord 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? # 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? diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index 9163cabf1e..30da6e2e1a 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -107,6 +107,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer enabled: registrations_enabled?, approval_required: Setting.registrations_mode == 'approved', message: registrations_enabled? ? nil : registrations_message, + min_age: Setting.min_age.presence, url: ENV.fetch('SSO_ACCOUNT_SIGN_UP', nil), } end diff --git a/app/services/app_sign_up_service.rb b/app/services/app_sign_up_service.rb index 7665880115..a4399efd65 100644 --- a/app/services/app_sign_up_service.rb +++ b/app/services/app_sign_up_service.rb @@ -41,7 +41,7 @@ class AppSignUpService < BaseService end 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 def account_params diff --git a/app/validators/date_of_birth_validator.rb b/app/validators/date_of_birth_validator.rb new file mode 100644 index 0000000000..79119d2c4c --- /dev/null +++ b/app/validators/date_of_birth_validator.rb @@ -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 diff --git a/app/views/admin/settings/registrations/show.html.haml b/app/views/admin/settings/registrations/show.html.haml index 4dbc5fbecf..cb5a3eb6ba 100644 --- a/app/views/admin/settings/registrations/show.html.haml +++ b/app/views/admin/settings/registrations/show.html.haml @@ -12,6 +12,9 @@ .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__column.fields-row__column-6.fields-group = f.input :registrations_mode, diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml index 5e9aa02d68..25479104ee 100644 --- a/app/views/auth/registrations/new.html.haml +++ b/app/views/auth/registrations/new.html.haml @@ -21,20 +21,19 @@ = f.simple_fields_for :account do |ff| = ff.input :username, 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 }, - label: false, + input_html: { autocomplete: 'off', pattern: '[a-zA-Z0-9_]+', maxlength: Account::USERNAME_LENGTH_LIMIT, placeholder: ' ' }, required: true, wrapper: :with_label = f.input :email, hint: false, - input_html: { 'aria-label': t('simple_form.labels.defaults.email'), autocomplete: 'username' }, - placeholder: t('simple_form.labels.defaults.email'), - required: true + input_html: { autocomplete: 'username', placeholder: ' ' }, + required: true, + wrapper: :with_label = f.input :password, 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 }, - placeholder: t('simple_form.labels.defaults.password'), - required: true + input_html: { autocomplete: 'new-password', minlength: User.password_length.first, maxlength: User.password_length.last, placeholder: ' ' }, + required: true, + wrapper: :with_label = f.input :password_confirmation, hint: false, 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, 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? %p.lead= t('auth.sign_up.manual_review', domain: site_hostname) diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml index ed389c1323..6940d589ca 100644 --- a/config/locales/activerecord.en.yml +++ b/config/locales/activerecord.en.yml @@ -55,6 +55,8 @@ en: too_soon: is too soon, must be later than %{date} user: attributes: + date_of_birth: + below_limit: is below the age limit email: blocked: uses a disallowed e-mail provider unreachable: does not seem to exist diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 682a8179b2..cfb3adf13a 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -88,6 +88,7 @@ en: favicon: WEBP, PNG, GIF or JPG. Overrides the default Mastodon favicon with a custom icon. 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. + 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. 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 @@ -146,6 +147,7 @@ en: min_age: Should not be below the minimum age required by the laws of your jurisdiction. user: 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. user_role: color: Color to be used for the role throughout the UI, as RGB in hex format @@ -271,6 +273,7 @@ en: favicon: Favicon mascot: Custom mascot (legacy) media_cache_retention_period: Media cache retention period + min_age: Minimum age requirement peers_api_enabled: Publish list of discovered servers in the API profile_directory: Enable profile directory registrations_mode: Who can sign-up @@ -349,6 +352,9 @@ en: jurisdiction: Legal jurisdiction min_age: Minimum age user: + date_of_birth_1i: Day + date_of_birth_2i: Month + date_of_birth_3i: Year role: Role time_zone: Time zone user_role: diff --git a/db/migrate/20250313123400_add_age_verified_at_to_users.rb b/db/migrate/20250313123400_add_age_verified_at_to_users.rb new file mode 100644 index 0000000000..c6cd6120ef --- /dev/null +++ b/db/migrate/20250313123400_add_age_verified_at_to_users.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 66c63e53f5..32d94b48ec 100644 --- a/db/schema.rb +++ b/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_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 enable_extension "pg_catalog.plpgsql" @@ -1188,6 +1188,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_05_074104) do t.text "settings" t.string "time_zone" t.string "otp_secret" + t.datetime "age_verified_at" t.index ["account_id"], name: "index_users_on_account_id" 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)" diff --git a/spec/controllers/auth/registrations_controller_spec.rb b/spec/controllers/auth/registrations_controller_spec.rb index 739cb455e8..4e43592a4e 100644 --- a/spec/controllers/auth/registrations_controller_spec.rb +++ b/spec/controllers/auth/registrations_controller_spec.rb @@ -342,6 +342,42 @@ RSpec.describe Auth::RegistrationsController do 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 end diff --git a/spec/requests/api/v1/accounts_spec.rb b/spec/requests/api/v1/accounts_spec.rb index 16010ae2e7..9fe5b3d491 100644 --- a/spec/requests/api/v1/accounts_spec.rb +++ b/spec/requests/api/v1/accounts_spec.rb @@ -74,12 +74,45 @@ RSpec.describe '/api/v1/accounts' do describe 'POST /api/v1/accounts' 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 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(: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 let(:agreement) { 'true' } diff --git a/spec/validators/date_of_birth_validator_spec.rb b/spec/validators/date_of_birth_validator_spec.rb new file mode 100644 index 0000000000..33e69e811b --- /dev/null +++ b/spec/validators/date_of_birth_validator_spec.rb @@ -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 From 2f98134ac69ee840095c9d8389e4b2fff72f20c1 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 14 Mar 2025 11:24:40 -0400 Subject: [PATCH 07/78] Use bundler version 2.6.6 (#34173) --- Gemfile.lock | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 66a597e99e..753b70b31f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -93,23 +93,24 @@ GEM annotaterb (4.14.0) ast (2.4.2) attr_required (1.0.2) - aws-eventstream (1.3.0) - aws-partitions (1.1032.0) - aws-sdk-core (3.214.1) + aws-eventstream (1.3.2) + aws-partitions (1.1066.0) + aws-sdk-core (3.220.1) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) + base64 jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.96.0) - aws-sdk-core (~> 3, >= 3.210.0) + aws-sdk-kms (1.99.0) + aws-sdk-core (~> 3, >= 3.216.0) aws-sigv4 (~> 1.5) aws-sdk-s3 (1.177.0) aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.10.1) + aws-sigv4 (1.11.0) aws-eventstream (~> 1, >= 1.0.2) - azure-blob (0.5.4) + azure-blob (0.5.7) rexml base64 (0.2.0) bcp47_spec (0.2.1) @@ -168,7 +169,7 @@ GEM bigdecimal rexml crass (1.0.6) - css_parser (1.21.0) + css_parser (1.21.1) addressable csv (3.3.2) database_cleaner-active_record (2.2.0) @@ -222,7 +223,8 @@ GEM erubi (1.13.1) et-orbi (1.2.11) tzinfo - excon (1.2.3) + excon (1.2.5) + logger fabrication (2.31.0) faker (3.5.1) i18n (>= 1.8.11, < 2) @@ -265,7 +267,9 @@ GEM raabro (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) - google-protobuf (3.25.6) + google-protobuf (4.30.1) + bigdecimal + rake (>= 13) googleapis-common-protos-types (1.18.0) google-protobuf (>= 3.18, < 5.a) haml (6.3.0) @@ -302,7 +306,8 @@ GEM domain_name (~> 0.5) http-form_data (2.3.0) http_accept_language (2.1.1) - httpclient (2.8.3) + httpclient (2.9.0) + mutex_m httplog (1.7.0) rack (>= 2.0) rainbow (>= 2.0.0) @@ -378,7 +383,7 @@ GEM mime-types terrapin (>= 0.6.0, < 2.0) language_server-protocol (3.17.0.4) - launchy (3.1.0) + launchy (3.1.1) addressable (~> 2.8) childprocess (~> 5.0) logger (~> 1.6) @@ -391,7 +396,7 @@ GEM rexml link_header (0.0.8) lint_roller (1.1.0) - llhttp-ffi (0.5.0) + llhttp-ffi (0.5.1) ffi-compiler (~> 1.0) rake (~> 13.0) logger (1.6.6) @@ -416,10 +421,10 @@ GEM mime-types (3.6.0) logger mime-types-data (~> 3.2015) - mime-types-data (3.2025.0220) + mime-types-data (3.2025.0304) mini_mime (1.1.5) mini_portile2 (2.8.8) - minitest (5.25.4) + minitest (5.25.5) msgpack (1.8.0) multi_json (1.15.0) mutex_m (0.3.0) @@ -1069,4 +1074,4 @@ RUBY VERSION ruby 3.4.1p0 BUNDLED WITH - 2.6.5 + 2.6.6 From 8c59fbe41bd85c0365e89e1e1e8fec0817427202 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 11:28:51 +0100 Subject: [PATCH 08/78] chore(deps): update dependency nokogiri to v1.18.4 (#34175) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 753b70b31f..2afcdd4655 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -441,7 +441,7 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.4) - nokogiri (1.18.3) + nokogiri (1.18.4) mini_portile2 (~> 2.8.2) racc (~> 1.4) oj (3.16.10) From e74774e366f1f4ecb189ec2e235c46ea39a6d57e Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Mon, 17 Mar 2025 16:50:13 +0100 Subject: [PATCH 09/78] Disable installation of instrumentation hooks (#34192) --- config/initializers/prometheus_exporter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/prometheus_exporter.rb b/config/initializers/prometheus_exporter.rb index fab08ceebb..fdfee59dc8 100644 --- a/config/initializers/prometheus_exporter.rb +++ b/config/initializers/prometheus_exporter.rb @@ -24,6 +24,6 @@ if ENV['MASTODON_PROMETHEUS_EXPORTER_ENABLED'] == 'true' 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 + Rails.application.middleware.unshift Mastodon::Middleware::PrometheusQueueTime, instrument: false end end From 2a5853989facc6ff40f2e94c027851fd2c1be163 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 17:27:45 +0100 Subject: [PATCH 10/78] fix(deps): update dependency pg to v8.14.1 (#34194) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 060b028e83..d003afac18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13191,8 +13191,8 @@ __metadata: linkType: hard "pg@npm:^8.5.0": - version: 8.14.0 - resolution: "pg@npm:8.14.0" + version: 8.14.1 + resolution: "pg@npm:8.14.1" dependencies: pg-cloudflare: "npm:^1.1.1" pg-connection-string: "npm:^2.7.0" @@ -13208,7 +13208,7 @@ __metadata: peerDependenciesMeta: pg-native: optional: true - checksum: 10c0/14d9fe726189107b028d5603b299776d039e36ed657c99057bcc1c125f889cb46536e0c48c6d98952231733c788f98c631bf74d5f8c9cbf85c4ac7c0a119b8b4 + checksum: 10c0/221741cfcea4ab32c8b57bd60703bc36cfb5622dcac56c19e45f504ef8669f2f2e0429af8850f58079cfc89055da35b5a5e12de19e0505e3f61a4b4349388dcb languageName: node linkType: hard From e30001bc80a149b8dd5df3ac0111226a7663cd89 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 17 Mar 2025 17:40:28 +0100 Subject: [PATCH 11/78] Fix incorrect URL being used when cache busting (#34189) --- app/models/media_attachment.rb | 2 +- app/services/suspend_account_service.rb | 2 +- app/services/unsuspend_account_service.rb | 2 +- spec/models/media_attachment_spec.rb | 8 ++--- spec/services/suspend_account_service_spec.rb | 30 ++++++++----------- 5 files changed, 20 insertions(+), 24 deletions(-) diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 72330aa0aa..605a81c016 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -421,7 +421,7 @@ class MediaAttachment < ApplicationRecord @paths_to_cache_bust = MediaAttachment.attachment_definitions.keys.flat_map do |attachment_name| attachment = public_send(attachment_name) styles = DEFAULT_STYLES | attachment.styles.keys - styles.map { |style| attachment.path(style) } + styles.map { |style| attachment.url(style) } end.compact rescue => e # We really don't want any error here preventing media deletion diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index 44210799f9..3934a738f7 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -95,7 +95,7 @@ class SuspendAccountService < BaseService end end - CacheBusterWorker.perform_async(attachment.path(style)) if Rails.configuration.x.cache_buster_enabled + CacheBusterWorker.perform_async(attachment.url(style)) if Rails.configuration.x.cache_buster_enabled end end end diff --git a/app/services/unsuspend_account_service.rb b/app/services/unsuspend_account_service.rb index 652dd6a845..7d3bb806a6 100644 --- a/app/services/unsuspend_account_service.rb +++ b/app/services/unsuspend_account_service.rb @@ -91,7 +91,7 @@ class UnsuspendAccountService < BaseService end end - CacheBusterWorker.perform_async(attachment.path(style)) if Rails.configuration.x.cache_buster_enabled + CacheBusterWorker.perform_async(attachment.url(style)) if Rails.configuration.x.cache_buster_enabled end end end diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb index 5f91ae0967..bf818c1e1e 100644 --- a/spec/models/media_attachment_spec.rb +++ b/spec/models/media_attachment_spec.rb @@ -295,12 +295,12 @@ RSpec.describe MediaAttachment, :attachment_processing do end it 'queues CacheBusterWorker jobs' do - original_path = media.file.path(:original) - small_path = media.file.path(:small) + original_url = media.file.url(:original) + small_url = media.file.url(:small) expect { media.destroy } - .to enqueue_sidekiq_job(CacheBusterWorker).with(original_path) - .and enqueue_sidekiq_job(CacheBusterWorker).with(small_path) + .to enqueue_sidekiq_job(CacheBusterWorker).with(original_url) + .and enqueue_sidekiq_job(CacheBusterWorker).with(small_url) end end diff --git a/spec/services/suspend_account_service_spec.rb b/spec/services/suspend_account_service_spec.rb index 4a2f494e0c..c15c23ca30 100644 --- a/spec/services/suspend_account_service_spec.rb +++ b/spec/services/suspend_account_service_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe SuspendAccountService, :inline_jobs do +RSpec.describe SuspendAccountService do shared_examples 'common behavior' do subject { described_class.new.call(account) } @@ -11,6 +11,7 @@ RSpec.describe SuspendAccountService, :inline_jobs do before do allow(FeedManager.instance).to receive_messages(unmerge_from_home: nil, unmerge_from_list: nil) + allow(Rails.configuration.x).to receive(:cache_buster_enabled).and_return(true) local_follower.follow!(account) list.accounts << account @@ -23,6 +24,7 @@ RSpec.describe SuspendAccountService, :inline_jobs do it 'unmerges from feeds of local followers and changes file mode and preserves suspended flag' do expect { subject } .to change_file_mode + .and enqueue_sidekiq_job(CacheBusterWorker).with(account.media_attachments.first.file.url(:original)) .and not_change_suspended_flag expect(FeedManager.instance).to have_received(:unmerge_from_home).with(account, local_follower) expect(FeedManager.instance).to have_received(:unmerge_from_list).with(account, list) @@ -38,17 +40,12 @@ RSpec.describe SuspendAccountService, :inline_jobs do end describe 'suspending a local account' do - def match_update_actor_request(req, account) - json = JSON.parse(req.body) + def match_update_actor_request(json, account) + json = JSON.parse(json) actor_id = ActivityPub::TagManager.instance.uri_for(account) json['type'] == 'Update' && json['actor'] == actor_id && json['object']['id'] == actor_id && json['object']['suspended'] end - before do - stub_request(:post, 'https://alice.com/inbox').to_return(status: 201) - stub_request(:post, 'https://bob.com/inbox').to_return(status: 201) - end - include_examples 'common behavior' do let!(:account) { Fabricate(:account) } let!(:remote_follower) { Fabricate(:account, uri: 'https://alice.com', inbox_url: 'https://alice.com/inbox', protocol: :activitypub, domain: 'alice.com') } @@ -61,22 +58,20 @@ RSpec.describe SuspendAccountService, :inline_jobs do it 'sends an Update actor activity to followers and reporters' do subject - expect(a_request(:post, remote_follower.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once - expect(a_request(:post, remote_reporter.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once + + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(satisfying { |json| match_update_actor_request(json, account) }, account.id, remote_follower.inbox_url).once + .and have_enqueued_sidekiq_job(satisfying { |json| match_update_actor_request(json, account) }, account.id, remote_reporter.inbox_url).once end end end describe 'suspending a remote account' do - def match_reject_follow_request(req, account, followee) - json = JSON.parse(req.body) + def match_reject_follow_request(json, account, followee) + json = JSON.parse(json) json['type'] == 'Reject' && json['actor'] == ActivityPub::TagManager.instance.uri_for(followee) && json['object']['actor'] == account.uri end - before do - stub_request(:post, 'https://bob.com/inbox').to_return(status: 201) - end - include_examples 'common behavior' do let!(:account) { Fabricate(:account, domain: 'bob.com', uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub) } let!(:local_followee) { Fabricate(:account) } @@ -88,7 +83,8 @@ RSpec.describe SuspendAccountService, :inline_jobs do it 'sends a Reject Follow activity', :aggregate_failures do subject - expect(a_request(:post, account.inbox_url).with { |req| match_reject_follow_request(req, account, local_followee) }).to have_been_made.once + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(satisfying { |json| match_reject_follow_request(json, account, local_followee) }, local_followee.id, account.inbox_url).once end end end From 30e334b51a4e7add05401895bff5a2ea4a97e0bd Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 17 Mar 2025 17:49:09 +0100 Subject: [PATCH 12/78] Fix language detection sometimes kicking in *after* posting (#34193) --- .../mastodon/features/compose/components/language_dropdown.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/mastodon/features/compose/components/language_dropdown.tsx b/app/javascript/mastodon/features/compose/components/language_dropdown.tsx index 21c4359981..d11891308f 100644 --- a/app/javascript/mastodon/features/compose/components/language_dropdown.tsx +++ b/app/javascript/mastodon/features/compose/components/language_dropdown.tsx @@ -378,6 +378,7 @@ export const LanguageDropdown: React.FC = () => { if (text.length > 20) { debouncedGuess(text, setGuess); } else { + debouncedGuess.cancel(); setGuess(''); } }, [text, setGuess]); From 8ef546fe6b2453864c0f14c358ae8fff41c3aaa1 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 18 Mar 2025 04:16:42 -0400 Subject: [PATCH 13/78] Convert `oauth/tokens#revoke` spec controller->request (#34174) --- .../oauth/tokens_controller_spec.rb | 23 ------------------- spec/requests/oauth/token_spec.rb | 21 ++++++++++++++++- 2 files changed, 20 insertions(+), 24 deletions(-) delete mode 100644 spec/controllers/oauth/tokens_controller_spec.rb diff --git a/spec/controllers/oauth/tokens_controller_spec.rb b/spec/controllers/oauth/tokens_controller_spec.rb deleted file mode 100644 index a2eed797e0..0000000000 --- a/spec/controllers/oauth/tokens_controller_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Oauth::TokensController do - describe 'POST #revoke' do - let!(:user) { Fabricate(:user) } - let!(:application) { Fabricate(:application, confidential: false) } - let!(:access_token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: application) } - let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) } - - it 'revokes the token and removes subscriptions' do - post :revoke, params: { client_id: application.uid, token: access_token.token } - - expect(access_token.reload.revoked_at) - .to_not be_nil - expect(Web::PushSubscription.where(access_token: access_token).count) - .to eq(0) - expect { web_push_subscription.reload } - .to raise_error(ActiveRecord::RecordNotFound) - end - end -end diff --git a/spec/requests/oauth/token_spec.rb b/spec/requests/oauth/token_spec.rb index 18d232e5ab..74f301c577 100644 --- a/spec/requests/oauth/token_spec.rb +++ b/spec/requests/oauth/token_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe 'Obtaining OAuth Tokens' do +RSpec.describe 'Managing OAuth Tokens' do describe 'POST /oauth/token' do subject do post '/oauth/token', params: params @@ -104,4 +104,23 @@ RSpec.describe 'Obtaining OAuth Tokens' do end end end + + describe 'POST /oauth/revoke' do + subject { post '/oauth/revoke', params: { client_id: application.uid, token: access_token.token } } + + let!(:user) { Fabricate(:user) } + let!(:application) { Fabricate(:application, confidential: false) } + let!(:access_token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: application) } + let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) } + + it 'revokes the token and removes subscriptions' do + expect { subject } + .to change { access_token.reload.revoked_at }.from(nil).to(be_present) + + expect(Web::PushSubscription.where(access_token: access_token).count) + .to eq(0) + expect { web_push_subscription.reload } + .to raise_error(ActiveRecord::RecordNotFound) + end + end end From 795d465f8da06153c899d0b5e48457830c5a30ba Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 18 Mar 2025 04:18:36 -0400 Subject: [PATCH 14/78] Convert `disputes/strikes` spec controller->request/system (#34191) --- .../disputes/strikes_controller_spec.rb | 32 ------------------- spec/requests/disputes/strikes_spec.rb | 22 +++++++++++++ spec/system/disputes/strikes_spec.rb | 27 ++++++++++++++++ 3 files changed, 49 insertions(+), 32 deletions(-) delete mode 100644 spec/controllers/disputes/strikes_controller_spec.rb create mode 100644 spec/requests/disputes/strikes_spec.rb create mode 100644 spec/system/disputes/strikes_spec.rb diff --git a/spec/controllers/disputes/strikes_controller_spec.rb b/spec/controllers/disputes/strikes_controller_spec.rb deleted file mode 100644 index f6d28fc09a..0000000000 --- a/spec/controllers/disputes/strikes_controller_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Disputes::StrikesController do - render_views - - before { sign_in current_user, scope: :user } - - describe '#show' do - let(:current_user) { Fabricate(:user) } - let(:strike) { Fabricate(:account_warning, target_account: current_user.account) } - - before do - get :show, params: { id: strike.id } - end - - context 'when meant for the user' do - it 'returns http success' do - expect(response).to have_http_status(:success) - end - end - - context 'when meant for a different user' do - let(:strike) { Fabricate(:account_warning) } - - it 'returns http forbidden' do - expect(response).to have_http_status(403) - end - end - end -end diff --git a/spec/requests/disputes/strikes_spec.rb b/spec/requests/disputes/strikes_spec.rb new file mode 100644 index 0000000000..48685893c2 --- /dev/null +++ b/spec/requests/disputes/strikes_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Disputes Strikes' do + before { sign_in current_user } + + describe 'GET /disputes/strikes/:id' do + let(:current_user) { Fabricate(:user) } + + context 'when meant for a different user' do + let(:strike) { Fabricate(:account_warning) } + + it 'returns http forbidden' do + get disputes_strike_path(strike) + + expect(response) + .to have_http_status(403) + end + end + end +end diff --git a/spec/system/disputes/strikes_spec.rb b/spec/system/disputes/strikes_spec.rb new file mode 100644 index 0000000000..d2b6b08c46 --- /dev/null +++ b/spec/system/disputes/strikes_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Disputes Strikes' do + before { sign_in(current_user) } + + describe 'viewing strike disputes' do + let(:current_user) { Fabricate(:user) } + let!(:strike) { Fabricate(:account_warning, target_account: current_user.account) } + + it 'shows a list of strikes and details for each' do + visit disputes_strikes_path + expect(page) + .to have_title(I18n.t('settings.strikes')) + + find('.strike-entry').click + expect(page) + .to have_title(strike_page_title) + .and have_content(strike.text) + end + + def strike_page_title + I18n.t('disputes.strikes.title', action: I18n.t(strike.action, scope: 'disputes.strikes.title_actions'), date: I18n.l(strike.created_at.to_date)) + end + end +end From 6bce43cdb844b7a298344631ee27a2a35bf88712 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 11:32:20 +0100 Subject: [PATCH 15/78] chore(deps): update dependency mime-types to v3.6.1 (#34196) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2afcdd4655..05536976ea 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -418,7 +418,7 @@ GEM redis (>= 3.0.5) matrix (0.4.2) memory_profiler (1.1.0) - mime-types (3.6.0) + mime-types (3.6.1) logger mime-types-data (~> 3.2015) mime-types-data (3.2025.0304) From 9d5cbbbf0f74ab504dd3b84273338e1fb383aaf4 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 18 Mar 2025 11:32:35 +0100 Subject: [PATCH 16/78] Fix account notes not being displayed (#34166) --- .../features/account/components/account_note.jsx | 13 ++++++------- .../account/containers/account_note_container.js | 8 ++++---- .../account_timeline/components/account_header.tsx | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/app/javascript/mastodon/features/account/components/account_note.jsx b/app/javascript/mastodon/features/account/components/account_note.jsx index e736e7ad64..84fd6beeff 100644 --- a/app/javascript/mastodon/features/account/components/account_note.jsx +++ b/app/javascript/mastodon/features/account/components/account_note.jsx @@ -4,7 +4,6 @@ import { PureComponent } from 'react'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { is } from 'immutable'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Textarea from 'react-textarea-autosize'; @@ -49,7 +48,7 @@ class InlineAlert extends PureComponent { class AccountNote extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.record.isRequired, + accountId: PropTypes.string.isRequired, value: PropTypes.string, onSave: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -66,7 +65,7 @@ class AccountNote extends ImmutablePureComponent { } UNSAFE_componentWillReceiveProps (nextProps) { - const accountWillChange = !is(this.props.account, nextProps.account); + const accountWillChange = !is(this.props.accountId, nextProps.accountId); const newState = {}; if (accountWillChange && this._isDirty()) { @@ -141,21 +140,21 @@ class AccountNote extends ImmutablePureComponent { } render () { - const { account, intl } = this.props; + const { accountId, intl } = this.props; const { value, saved } = this.state; - if (!account) { + if (!accountId) { return null; } return (
-