Add notifications for out-of-support versions of Mastodon (#39734)

This commit is contained in:
Claire
2026-07-09 16:41:13 +02:00
committed by GitHub
parent acd0a161e5
commit 5dad89e56d
15 changed files with 282 additions and 24 deletions

View File

@@ -9,7 +9,7 @@ class AdminMailer < ApplicationMailer
before_action :process_params
before_action :set_instance
after_action :set_important_headers!, only: :new_critical_software_updates
after_action :set_important_headers!, only: [:new_critical_software_updates, :end_of_support_out_of_support_warning]
around_action :set_locale
@@ -57,6 +57,24 @@ class AdminMailer < ApplicationMailer
mail subject: default_i18n_subject(instance: @instance)
end
def end_of_support_three_months_warning
@software_updates = SoftwareUpdate.by_version
mail subject: default_i18n_subject(instance: @instance, branch: SoftwareDeprecation.current_branch)
end
def end_of_support_two_weeks_warning
@software_updates = SoftwareUpdate.by_version
mail subject: default_i18n_subject(instance: @instance, branch: SoftwareDeprecation.current_branch)
end
def end_of_support_out_of_support_warning
@software_updates = SoftwareUpdate.by_version
mail subject: default_i18n_subject(instance: @instance, branch: SoftwareDeprecation.current_branch)
end
def auto_close_registrations
mail subject: default_i18n_subject(instance: @instance)
end

View File

@@ -0,0 +1,37 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: software_deprecations
#
# id :bigint(8) not null, primary key
# branch :string not null
# end_of_support :date not null
# warning_issued :integer not null
# created_at :datetime not null
# updated_at :datetime not null
#
class SoftwareDeprecation < ApplicationRecord
enum :warning_issued, {
none: 0,
three_months_warning: 1,
two_weeks_warning: 2,
out_of_support_warning: 3,
}, validate: true, suffix: :issued
def unsupported?
end_of_support.past?
end
def self.clear_irrelevant_branches!
where.not(branch: current_branch).delete_all
end
def self.current
find_by(branch: current_branch)
end
def self.current_branch
Mastodon::Version.gem_version.segments[...2].join('.')
end
end

View File

@@ -54,6 +54,7 @@ class UserSettings
setting :trends, default: true
setting :appeal, default: true
setting :software_updates, default: 'critical', in: %w(none critical patch all)
setting :end_of_support, default: true
end
namespace :interactions do

View File

@@ -3,9 +3,12 @@
class SoftwareUpdateCheckService < BaseService
def call
clean_outdated_updates!
clean_outdated_deprecations!
return unless SoftwareUpdate.check_enabled?
process_update_notices!(fetch_update_notices)
json = fetch_update_notices
process_update_notices!(json&.fetch('updatesAvailable', nil))
process_deprecation_notice!(json&.fetch('currentVersion', nil))
end
private
@@ -18,6 +21,10 @@ class SoftwareUpdateCheckService < BaseService
end
end
def clean_outdated_deprecations!
SoftwareDeprecation.clear_irrelevant_branches!
end
def fetch_update_notices
Request.new(:get, "#{api_url}?version=#{version}").add_headers('Accept' => 'application/json', 'User-Agent' => 'Mastodon update checker').perform do |res|
return JSON.parse(res.body_with_limit) if res.code == 200
@@ -35,19 +42,17 @@ class SoftwareUpdateCheckService < BaseService
end
def process_update_notices!(update_notices)
return if update_notices.blank? || update_notices['updatesAvailable'].nil?
return if update_notices.blank?
# Clear notices that are not listed by the update server anymore
SoftwareUpdate.where.not(version: update_notices['updatesAvailable'].pluck('version')).delete_all
return if update_notices['updatesAvailable'].blank?
SoftwareUpdate.where.not(version: update_notices.pluck('version')).delete_all
# Check if any of the notices is new, and issue notifications
known_versions = SoftwareUpdate.where(version: update_notices['updatesAvailable'].pluck('version')).pluck(:version)
new_update_notices = update_notices['updatesAvailable'].filter { |notice| known_versions.exclude?(notice['version']) }
known_versions = SoftwareUpdate.where(version: update_notices.pluck('version')).pluck(:version)
new_update_notices = update_notices.filter { |notice| known_versions.exclude?(notice['version']) }
return if new_update_notices.blank?
SoftwareUpdate.upsert_all(update_notices['updatesAvailable'].map do |notice|
SoftwareUpdate.upsert_all(update_notices.map do |notice|
{ version: notice['version'], urgent: notice['urgent'], type: notice['type'], release_notes: notice['releaseNotes'], end_of_support: notice['endOfSupport']&.to_date }
end, unique_by: :version)
@@ -56,6 +61,18 @@ class SoftwareUpdateCheckService < BaseService
notify_devops!(new_updates)
end
def process_deprecation_notice!(current_version)
return if current_version.blank? || current_version['endOfSupport'].blank?
deprecation_notice = SoftwareDeprecation
.create_with(warning_issued: :none, end_of_support: current_version['endOfSupport'].to_date)
.find_or_create_by(branch: SoftwareDeprecation.current_branch)
deprecation_notice.update(end_of_support: current_version['endOfSupport'].to_date)
notify_of_end_of_support!(deprecation_notice)
end
def should_notify_user?(user, urgent_version, patch_version)
case user.settings['notification_emails.software_updates']
when 'none'
@@ -83,4 +100,30 @@ class SoftwareUpdateCheckService < BaseService
end
end
end
def should_notify_about_end_of_support?(user)
user.settings['notification_emails.end_of_support']
end
def send_notification_for_end_of_support_warning!(deprecation_notice, warning_type)
return if deprecation_notice.warning_issued == warning_type
User.those_who_can(:view_devops).includes(:account).find_each do |user|
next unless should_notify_about_end_of_support?(user)
AdminMailer.with(recipient: user.account).public_send("end_of_support_#{warning_type}").deliver_later
end
deprecation_notice.update!(warning_issued: warning_type)
end
def notify_of_end_of_support!(deprecation_notice)
if deprecation_notice.end_of_support.past?
send_notification_for_end_of_support_warning!(deprecation_notice, 'out_of_support_warning')
elsif deprecation_notice.end_of_support < 2.weeks.from_now
send_notification_for_end_of_support_warning!(deprecation_notice, 'two_weeks_warning')
elsif deprecation_notice.end_of_support < 3.months.from_now
send_notification_for_end_of_support_warning!(deprecation_notice, 'three_months_warning')
end
end
end

View File

@@ -1,6 +1,12 @@
- content_for :page_title do
= t('admin.software_updates.title')
- current_deprecation = SoftwareDeprecation.current
- if current_deprecation&.unsupported?
.flash-message.alert= t('admin.software_updates.unsupported_branch')
- elsif current_deprecation.present?
.flash-message.warning= t('admin.software_updates.nearing_eol', date: current_deprecation.end_of_support)
.simple_form
%p.lead
= t('admin.software_updates.description')

View File

@@ -0,0 +1,7 @@
<%= raw t('admin_mailer.end_of_support_out_of_support_warning.body') %>
<% @software_updates.each do |update| %>
- Mastodon <%= update.version %>: <%= update.release_notes %>
<% end %>
<%= raw t('application_mailer.view')%> <%= admin_software_updates_url %>

View File

@@ -0,0 +1,7 @@
<%= raw t('admin_mailer.end_of_support_three_months_warning.body') %>
<% @software_updates.each do |update| %>
- Mastodon <%= update.version %>: <%= update.release_notes %>
<% end %>
<%= raw t('application_mailer.view')%> <%= admin_software_updates_url %>

View File

@@ -0,0 +1,7 @@
<%= raw t('admin_mailer.end_of_support_two_weeks_warning.body') %>
<% @software_updates.each do |update| %>
- Mastodon <%= update.version %>: <%= update.release_notes %>
<% end %>
<%= raw t('application_mailer.view')%> <%= admin_software_updates_url %>

View File

@@ -23,8 +23,8 @@
.fields-group
= ff.input :always_send_emails, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_always_send_emails'), hint: I18n.t('simple_form.hints.defaults.setting_always_send_emails')
- if current_user.can?(:manage_reports, :manage_appeals, :manage_users, :manage_taxonomies) || (SoftwareUpdate.check_enabled? && current_user.can?(:view_devops))
%h2= t 'notifications.administration_emails'
- if current_user.can?(:manage_reports, :manage_appeals, :manage_users, :manage_taxonomies)
%h2= t 'notifications.moderation_emails'
.fields-group
= ff.input :'notification_emails.report', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.report') if current_user.can?(:manage_reports)
@@ -32,13 +32,21 @@
= ff.input :'notification_emails.pending_account', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.pending_account') if current_user.can?(:manage_users)
= ff.input :'notification_emails.trends', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.trending_tag') if current_user.can?(:manage_taxonomies)
- if SoftwareUpdate.check_enabled? && current_user.can?(:view_devops)
.fields-group
= ff.input :'notification_emails.software_updates',
collection: user_settings_collection('notification_emails.software_updates'),
hint: false,
include_blank: false,
label_method: ->(setting) { I18n.t("simple_form.labels.notification_emails.software_updates.#{setting}") },
label: I18n.t('simple_form.labels.notification_emails.software_updates.label'),
wrapper: :with_label,
required: false
- if SoftwareUpdate.check_enabled? && current_user.can?(:view_devops)
%h2= t 'notifications.administration_emails'
.fields-group
= ff.input :'notification_emails.end_of_support',
wrapper: :with_label,
label: I18n.t('simple_form.labels.notification_emails.end_of_support'),
recommended: true
.fields-group
= ff.input :'notification_emails.software_updates',
collection: user_settings_collection('notification_emails.software_updates'),
hint: false,
include_blank: false,
label_method: ->(setting) { I18n.t("simple_form.labels.notification_emails.software_updates.#{setting}") },
label: I18n.t('simple_form.labels.notification_emails.software_updates.label'),
wrapper: :with_label,
required: false

View File

@@ -1013,6 +1013,7 @@ en:
critical_update: Critical — please update quickly
description: It is recommended to keep your Mastodon installation up to date to benefit from the latest fixes and features. Moreover, it is sometimes critical to update Mastodon in a timely manner to avoid security issues. For these reasons, Mastodon checks for updates every 30 minutes, and will notify you according to your email notification preferences.
documentation_link: Learn more
nearing_eol: Support for the branch you are currently running will stop on %{date}, please consider updating.
release_notes: Release notes
supported_until: Support ends on %{date}
title: Available updates
@@ -1021,6 +1022,7 @@ en:
major: Major release
minor: Minor release
patch: Patch release — bugfixes and easy to apply changes
unsupported_branch: The branch you are currently running is unsupported, please update as soon as possible.
unsupported_since: Unsupported since %{date}
version: Version
statuses:
@@ -1287,6 +1289,15 @@ en:
auto_close_registrations:
body: Due to a lack of recent moderator activity, registrations on %{instance} have been automatically switched to requiring manual review, to prevent %{instance} from being used as a platform for potential bad actors. You can switch it back to open registrations at any time.
subject: Registrations for %{instance} have been automatically switched to requiring approval
end_of_support_out_of_support_warning:
body: The Mastodon version currently in use on your server is not supported anymore. We recommend you to update as soon as possible!
subject: Mastodon %{branch} used on %{instance} is now unsupported!
end_of_support_three_months_warning:
body: The Mastodon version currently in use on your server will stop being supported in 3 months. You may want to update to a newer branch ahead of time.
subject: Support for Mastodon %{branch} used on %{instance} to end in 3 months.
end_of_support_two_weeks_warning:
body: The Mastodon version currently in use on your server will stop being supported in 2 weeks. We recommend you update to a newer branch ahead of time.
subject: Support for Mastodon %{branch} used on %{instance} to end in 2 weeks.
new_appeal:
actions:
delete_statuses: to delete their posts
@@ -1922,6 +1933,7 @@ en:
administration_emails: Admin email notifications
email_events: Events for email notifications
email_events_hint: 'Select events that you want to receive notifications for:'
moderation_emails: Moderation email notifications
number:
human:
decimal_units:

View File

@@ -345,6 +345,7 @@ en:
notification_emails:
appeal: Someone appeals a moderator decision
digest: Send digest e-mails
end_of_support: The current Mastodon version is nearing end of support
favourite: Someone favourited your post
follow: Someone followed you
follow_request: Someone requested to follow you

View File

@@ -11,7 +11,7 @@ SimpleNavigation::Configuration.run do |navigation|
SoftwareUpdate.urgent_pending? ? [material_symbol('report'), t('admin.critical_update_pending')] : [material_symbol('system_update_alt'), t('admin.update_available')]
),
admin_software_updates_path,
html: { class: SoftwareUpdate.urgent_pending? ? 'warning' : nil },
html: { class: SoftwareUpdate.urgent_pending? || SoftwareDeprecation.current&.unsupported? ? 'warning' : nil },
if: -> { Rails.configuration.x.mastodon.software_update_url.present? && current_user.can?(:view_devops) && SoftwareUpdate.pending? }
n.item :profile, safe_join([material_symbol('person'), t('settings.profile')]), settings_profile_path, if: -> { current_user.functional? && !self_destruct }, highlights_on: %r{/settings/profile|/settings/featured_tags|/settings/verification}

View File

@@ -0,0 +1,15 @@
# frozen_string_literal: true
class CreateSoftwareDeprecations < ActiveRecord::Migration[8.1]
def change
create_table :software_deprecations do |t|
t.string :branch, null: false
t.date :end_of_support, null: false
t.integer :warning_issued, null: false
t.timestamps
end
add_index :software_deprecations, :branch, unique: true
end
end

View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2026_07_06_122112) do
ActiveRecord::Schema[8.1].define(version: 2026_07_06_143100) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
@@ -1157,6 +1157,15 @@ ActiveRecord::Schema[8.1].define(version: 2026_07_06_122112) do
t.index ["var"], name: "index_site_uploads_on_var", unique: true
end
create_table "software_deprecations", force: :cascade do |t|
t.string "branch", null: false
t.datetime "created_at", null: false
t.date "end_of_support", null: false
t.datetime "updated_at", null: false
t.integer "warning_issued", null: false
t.index ["branch"], name: "index_software_deprecations_on_branch", unique: true
end
create_table "software_updates", force: :cascade do |t|
t.datetime "created_at", null: false
t.date "end_of_support"

View File

@@ -66,10 +66,12 @@ RSpec.describe SoftwareUpdateCheckService do
end
context 'when the server returns new versions' do
let(:deprecation_date) { nil }
let(:server_json) do
{
currentVersion: {
endOfSupport: nil,
endOfSupport: deprecation_date&.iso8601,
},
updatesAvailable: [
{
@@ -104,6 +106,91 @@ RSpec.describe SoftwareUpdateCheckService do
expect { subject.call }.to change { SoftwareUpdate.pluck(:version).sort }.from(['3.5.0', '42.13.12', 'Malformed']).to(['4.2.1', '4.3.0', '5.0.0'])
end
context 'when server returns deprecation in the distant future' do
let(:deprecation_date) { 5.years.from_now.to_date }
it 'updates the software deprecation info' do
expect { subject.call }
.to change { SoftwareDeprecation.pluck(:end_of_support, :warning_issued) }.from([]).to([[deprecation_date, 'none']])
end
context 'when an irrelevant deprecation was stored' do
before do
SoftwareDeprecation.create!(branch: '4.3', end_of_support: '2026-05-06'.to_date, warning_issued: :out_of_support_warning)
end
it 'updates the software deprecation info' do
expect { subject.call }
.to change { SoftwareDeprecation.pluck(:end_of_support, :warning_issued) }.from([['2026-05-06'.to_date, 'out_of_support_warning']]).to([[deprecation_date, 'none']])
end
end
end
context 'when server returns deprecation in 2 months' do
let(:deprecation_date) { 2.months.from_now.to_date }
it 'updates the software deprecation info' do
expect { subject.call }
.to change { SoftwareDeprecation.pluck(:end_of_support, :warning_issued) }.from([]).to([[deprecation_date, 'three_months_warning']])
end
context 'when an irrelevant deprecation was stored' do
before do
SoftwareDeprecation.create!(branch: '4.3', end_of_support: '2026-05-06'.to_date, warning_issued: :out_of_support_warning)
end
it 'updates the software deprecation info and sends email' do
expect { subject.call }
.to change { SoftwareDeprecation.pluck(:end_of_support, :warning_issued) }.from([['2026-05-06'.to_date, 'out_of_support_warning']]).to([[deprecation_date, 'three_months_warning']])
.and(have_enqueued_mail(AdminMailer, :end_of_support_three_months_warning).with(hash_including(params: { recipient: owner_user.account })).once)
end
end
end
context 'when server returns deprecation in 1 week' do
let(:deprecation_date) { 1.week.from_now.to_date }
it 'updates the software deprecation info' do
expect { subject.call }
.to change { SoftwareDeprecation.pluck(:end_of_support, :warning_issued) }.from([]).to([[deprecation_date, 'two_weeks_warning']])
.and(have_enqueued_mail(AdminMailer, :end_of_support_two_weeks_warning).with(hash_including(params: { recipient: owner_user.account })).once)
end
context 'when an irrelevant deprecation was stored' do
before do
SoftwareDeprecation.create!(branch: '4.3', end_of_support: '2026-05-06'.to_date, warning_issued: :out_of_support_warning)
end
it 'updates the software deprecation info and sends email' do
expect { subject.call }
.to change { SoftwareDeprecation.pluck(:end_of_support, :warning_issued) }.from([['2026-05-06'.to_date, 'out_of_support_warning']]).to([[deprecation_date, 'two_weeks_warning']])
.and(have_enqueued_mail(AdminMailer, :end_of_support_two_weeks_warning).with(hash_including(params: { recipient: owner_user.account })).once)
end
end
end
context 'when server returns deprecation in the past' do
let(:deprecation_date) { 1.week.ago.to_date }
it 'updates the software deprecation info' do
expect { subject.call }
.to change { SoftwareDeprecation.pluck(:end_of_support, :warning_issued) }.from([]).to([[deprecation_date, 'out_of_support_warning']])
.and(have_enqueued_mail(AdminMailer, :end_of_support_out_of_support_warning).with(hash_including(params: { recipient: owner_user.account })).once)
end
context 'when an irrelevant deprecation was stored' do
before do
SoftwareDeprecation.create!(branch: '4.3', end_of_support: '2026-05-06'.to_date, warning_issued: :out_of_support_warning)
end
it 'updates the software deprecation info and sends email' do
expect { subject.call }
.to change { SoftwareDeprecation.pluck(:end_of_support, :warning_issued) }.from([['2026-05-06'.to_date, 'out_of_support_warning']]).to([[deprecation_date, 'out_of_support_warning']])
.and(have_enqueued_mail(AdminMailer, :end_of_support_out_of_support_warning).with(hash_including(params: { recipient: owner_user.account })).once)
end
end
end
context 'when no update is urgent' do
it 'sends e-mail notifications according to settings', :aggregate_failures do
expect { subject.call }.to have_enqueued_mail(AdminMailer, :new_software_updates)