diff --git a/app/controllers/admin/software_updates_controller.rb b/app/controllers/admin/software_updates_controller.rb
new file mode 100644
index 0000000000..52d8cb41e6
--- /dev/null
+++ b/app/controllers/admin/software_updates_controller.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Admin
+ class SoftwareUpdatesController < BaseController
+ before_action :check_enabled!
+
+ def index
+ authorize :software_update, :index?
+ @software_updates = SoftwareUpdate.all.sort_by(&:gem_version)
+ end
+
+ private
+
+ def check_enabled!
+ not_found unless SoftwareUpdate.check_enabled?
+ end
+ end
+end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 975315e247..6ec93f824e 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -11,6 +11,7 @@ class ApplicationController < ActionController::Base
include CacheConcern
include DomainControlHelper
include DatabaseHelper
+ include AuthorizedFetchHelper
helper_method :current_account
helper_method :current_session
@@ -51,10 +52,6 @@ class ApplicationController < ActionController::Base
private
- def authorized_fetch_mode?
- ENV['AUTHORIZED_FETCH'] == 'true' || Rails.configuration.x.limited_federation_mode
- end
-
def public_fetch_mode?
!authorized_fetch_mode?
end
diff --git a/app/helpers/authorized_fetch_helper.rb b/app/helpers/authorized_fetch_helper.rb
new file mode 100644
index 0000000000..ce87526e6a
--- /dev/null
+++ b/app/helpers/authorized_fetch_helper.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module AuthorizedFetchHelper
+ def authorized_fetch_mode?
+ ENV.fetch('AUTHORIZED_FETCH') { Setting.authorized_fetch } == 'true' || Rails.configuration.x.limited_federation_mode
+ end
+
+ def authorized_fetch_overridden?
+ ENV.key?('AUTHORIZED_FETCH') || Rails.configuration.x.limited_federation_mode
+ end
+end
diff --git a/app/javascript/mastodon/features/compose/components/search.jsx b/app/javascript/mastodon/features/compose/components/search.jsx
index 5ae6ee1090..fb4fa99b8e 100644
--- a/app/javascript/mastodon/features/compose/components/search.jsx
+++ b/app/javascript/mastodon/features/compose/components/search.jsx
@@ -81,7 +81,7 @@ class Search extends PureComponent {
handleKeyDown = (e) => {
const { selectedOption } = this.state;
- const options = this._getOptions().concat(this.defaultOptions);
+ const options = searchEnabled ? this._getOptions().concat(this.defaultOptions) : this._getOptions();
switch(e.key) {
case 'Escape':
@@ -354,15 +354,19 @@ class Search extends PureComponent {
>
)}
-
+ {searchEnabled && (
+ <>
+
-
- {this.defaultOptions.map(({ key, label, action }, i) => (
-
- ))}
-
+
+ {this.defaultOptions.map(({ key, label, action }, i) => (
+
+ ))}
+
+ >
+ )}
);
diff --git a/app/javascript/mastodon/features/home_timeline/components/critical_update_banner.tsx b/app/javascript/mastodon/features/home_timeline/components/critical_update_banner.tsx
new file mode 100644
index 0000000000..d0dd2b6acd
--- /dev/null
+++ b/app/javascript/mastodon/features/home_timeline/components/critical_update_banner.tsx
@@ -0,0 +1,26 @@
+import { FormattedMessage } from 'react-intl';
+
+export const CriticalUpdateBanner = () => (
+
+
+
+
+
+
+ {' '}
+
+
+
+
+
+
+);
diff --git a/app/javascript/mastodon/features/home_timeline/index.jsx b/app/javascript/mastodon/features/home_timeline/index.jsx
index 1cd6edd7aa..8ff0377946 100644
--- a/app/javascript/mastodon/features/home_timeline/index.jsx
+++ b/app/javascript/mastodon/features/home_timeline/index.jsx
@@ -14,7 +14,7 @@ import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/an
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container';
-import { me } from 'mastodon/initial_state';
+import { me, criticalUpdatesPending } from 'mastodon/initial_state';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { expandHomeTimeline } from '../../actions/timelines';
@@ -23,6 +23,7 @@ import ColumnHeader from '../../components/column_header';
import StatusListContainer from '../ui/containers/status_list_container';
import { ColumnSettings } from './components/column_settings';
+import { CriticalUpdateBanner } from './components/critical_update_banner';
import { ExplorePrompt } from './components/explore_prompt';
const messages = defineMessages({
@@ -156,8 +157,9 @@ class HomeTimeline extends PureComponent {
const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
const pinned = !!columnId;
const { signedIn } = this.context.identity;
+ const banners = [];
- let announcementsButton, banner;
+ let announcementsButton;
if (hasAnnouncements) {
announcementsButton = (
@@ -173,8 +175,12 @@ class HomeTimeline extends PureComponent {
);
}
+ if (criticalUpdatesPending) {
+ banners.push();
+ }
+
if (tooSlow) {
- banner = ;
+ banners.push();
}
return (
@@ -196,7 +202,7 @@ class HomeTimeline extends PureComponent {
{signedIn ? (
} accounts
* @property {InitialStateLanguage[]} languages
+ * @property {boolean=} critical_updates_pending
* @property {InitialStateMeta} meta
*/
@@ -148,6 +149,7 @@ export const useBlurhash = getMeta('use_blurhash');
export const usePendingItems = getMeta('use_pending_items');
export const version = getMeta('version');
export const languages = initialState?.languages;
+export const criticalUpdatesPending = initialState?.critical_updates_pending;
// @ts-expect-error
export const statusPageUrl = getMeta('status_page_url');
export const sso_redirect = getMeta('sso_redirect');
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 1dbcf52a20..203e86b097 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -318,6 +318,9 @@
"home.explore_prompt.body": "Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. If that feels too quiet, you may want to:",
"home.explore_prompt.title": "This is your home base within Mastodon.",
"home.hide_announcements": "Hide announcements",
+ "home.pending_critical_update.body": "Please update your Mastodon server as soon as possible!",
+ "home.pending_critical_update.link": "See updates",
+ "home.pending_critical_update.title": "Critical security update available!",
"home.show_announcements": "Show announcements",
"interaction_modal.description.favourite": "With an account on Mastodon, you can favorite this post to let the author know you appreciate it and save it for later.",
"interaction_modal.description.follow": "With an account on Mastodon, you can follow {name} to receive their posts in your home feed.",
diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss
index ec29a80c71..0ed434939f 100644
--- a/app/javascript/styles/mastodon/accounts.scss
+++ b/app/javascript/styles/mastodon/accounts.scss
@@ -188,6 +188,7 @@
}
.information-badge,
+.simple_form .overridden,
.simple_form .recommended,
.simple_form .not_recommended,
.simple_form .kmyblue {
@@ -205,6 +206,7 @@
}
.information-badge,
+.simple_form .overridden,
.simple_form .recommended,
.simple_form .not_recommended {
background-color: rgba($ui-secondary-color, 0.1);
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 41f86eb12d..b0c26362c8 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -143,6 +143,11 @@ $content-width: 840px;
}
}
+ .warning a {
+ color: $gold-star;
+ font-weight: 700;
+ }
+
.simple-navigation-active-leaf a {
color: $primary-text-color;
background-color: $ui-highlight-color;
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index ba644aebc8..84d3934e59 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -228,20 +228,20 @@
color: lighten($lighter-text-color, 7%);
background-color: transparent;
}
+ }
- &.active {
+ &.active {
+ color: $highlight-text-color;
+
+ &:hover,
+ &:active,
+ &:focus {
color: $highlight-text-color;
+ background-color: transparent;
+ }
- &:hover,
- &:active,
- &:focus {
- color: $highlight-text-color;
- background-color: transparent;
- }
-
- &.disabled {
- color: lighten($highlight-text-color, 13%);
- }
+ &.disabled {
+ color: lighten($highlight-text-color, 13%);
}
}
@@ -9219,7 +9219,8 @@ noscript {
}
}
-.dismissable-banner {
+.dismissable-banner,
+.warning-banner {
position: relative;
margin: 10px;
margin-bottom: 5px;
@@ -9297,6 +9298,21 @@ noscript {
}
}
+.warning-banner {
+ border: 1px solid $warning-red;
+ background: rgba($warning-red, 0.15);
+
+ &__message {
+ h1 {
+ color: $warning-red;
+ }
+
+ a {
+ color: $primary-text-color;
+ }
+ }
+}
+
.image {
position: relative;
overflow: hidden;
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index 045d62a72b..215404ef37 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -103,6 +103,7 @@ code {
}
}
+ .overridden,
.recommended,
.not_recommended,
.kmyblue {
diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss
index 9a48d14460..572d66229b 100644
--- a/app/javascript/styles/mastodon/tables.scss
+++ b/app/javascript/styles/mastodon/tables.scss
@@ -12,6 +12,11 @@
border-top: 1px solid $ui-base-color;
text-align: start;
background: darken($ui-base-color, 4%);
+
+ &.critical {
+ font-weight: 700;
+ color: $gold-star;
+ }
}
& > thead > tr > th {
diff --git a/app/lib/admin/system_check.rb b/app/lib/admin/system_check.rb
index 89dfcef9f1..25c88341a4 100644
--- a/app/lib/admin/system_check.rb
+++ b/app/lib/admin/system_check.rb
@@ -2,6 +2,7 @@
class Admin::SystemCheck
ACTIVE_CHECKS = [
+ Admin::SystemCheck::SoftwareVersionCheck,
Admin::SystemCheck::MediaPrivacyCheck,
Admin::SystemCheck::DatabaseSchemaCheck,
Admin::SystemCheck::SidekiqProcessCheck,
diff --git a/app/lib/admin/system_check/software_version_check.rb b/app/lib/admin/system_check/software_version_check.rb
new file mode 100644
index 0000000000..e142feddf0
--- /dev/null
+++ b/app/lib/admin/system_check/software_version_check.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class Admin::SystemCheck::SoftwareVersionCheck < Admin::SystemCheck::BaseCheck
+ include RoutingHelper
+
+ def skip?
+ !current_user.can?(:view_devops) || !SoftwareUpdate.check_enabled?
+ end
+
+ def pass?
+ software_updates.empty?
+ end
+
+ def message
+ if software_updates.any?(&:urgent?)
+ Admin::SystemCheck::Message.new(:software_version_critical_check, nil, admin_software_updates_path, true)
+ else
+ Admin::SystemCheck::Message.new(:software_version_patch_check, nil, admin_software_updates_path)
+ end
+ end
+
+ private
+
+ def software_updates
+ @software_updates ||= SoftwareUpdate.pending_to_a.filter { |update| update.urgent? || update.patch_type? }
+ end
+end
diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb
index 5baf9b38a5..990b92c337 100644
--- a/app/mailers/admin_mailer.rb
+++ b/app/mailers/admin_mailer.rb
@@ -45,6 +45,22 @@ class AdminMailer < ApplicationMailer
end
end
+ def new_software_updates
+ locale_for_account(@me) do
+ mail subject: default_i18n_subject(instance: @instance)
+ end
+ end
+
+ def new_critical_software_updates
+ headers['Priority'] = 'urgent'
+ headers['X-Priority'] = '1'
+ headers['Importance'] = 'high'
+
+ locale_for_account(@me) do
+ mail subject: default_i18n_subject(instance: @instance)
+ end
+ end
+
private
def process_params
diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb
index e11fe50961..a5e3b556bd 100644
--- a/app/models/form/admin_settings.rb
+++ b/app/models/form/admin_settings.rb
@@ -3,6 +3,8 @@
class Form::AdminSettings
include ActiveModel::Model
+ include AuthorizedFetchHelper
+
KEYS = %i(
site_contact_username
site_contact_email
@@ -40,6 +42,7 @@ class Form::AdminSettings
post_hash_tags_max
sensitive_words
sensitive_words_for_full
+ authorized_fetch
).freeze
INTEGER_KEYS = %i(
@@ -63,6 +66,7 @@ class Form::AdminSettings
captcha_enabled
enable_block_emoji_reaction_settings
hide_local_users_for_anonymous
+ authorized_fetch
).freeze
UPLOAD_KEYS = %i(
@@ -70,6 +74,10 @@ class Form::AdminSettings
mascot
).freeze
+ OVERRIDEN_SETTINGS = {
+ authorized_fetch: :authorized_fetch_mode?,
+ }.freeze
+
STRING_ARRAY_KEYS = %i(
ng_words
sensitive_words
@@ -97,6 +105,8 @@ class Form::AdminSettings
SiteUpload.where(var: key).first_or_initialize(var: key)
elsif STRING_ARRAY_KEYS.include?(key)
Setting.public_send(key)&.join("\n") || ''
+ elsif OVERRIDEN_SETTINGS.include?(key)
+ public_send(OVERRIDEN_SETTINGS[key])
else
Setting.public_send(key)
end
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index 197ee777fe..d17794feb0 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -107,6 +107,7 @@ class MediaAttachment < ApplicationRecord
'preset' => 'veryfast',
'movflags' => 'faststart', # Move metadata to start of file so playback can begin before download finishes
'pix_fmt' => 'yuv420p', # Ensure color space for cross-browser compatibility
+ 'vf' => 'crop=floor(iw/2)*2:floor(ih/2)*2', # h264 requires width and height to be even. Crop instead of scale to avoid blurring
'c:v' => 'h264',
'c:a' => 'aac',
'b:a' => '192k',
diff --git a/app/models/software_update.rb b/app/models/software_update.rb
new file mode 100644
index 0000000000..cb3a6df2ae
--- /dev/null
+++ b/app/models/software_update.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: software_updates
+#
+# id :bigint(8) not null, primary key
+# version :string not null
+# urgent :boolean default(FALSE), not null
+# type :integer default("patch"), not null
+# release_notes :string default(""), not null
+# created_at :datetime not null
+# updated_at :datetime not null
+#
+
+class SoftwareUpdate < ApplicationRecord
+ self.inheritance_column = nil
+
+ enum type: { patch: 0, minor: 1, major: 2 }, _suffix: :type
+
+ def gem_version
+ Gem::Version.new(version)
+ end
+
+ class << self
+ def check_enabled?
+ ENV['UPDATE_CHECK_URL'] != ''
+ end
+
+ def pending_to_a
+ return [] unless check_enabled?
+
+ all.to_a.filter { |update| update.gem_version > Mastodon::Version.gem_version }
+ end
+
+ def urgent_pending?
+ pending_to_a.any?(&:urgent?)
+ end
+ end
+end
diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb
index 802d706191..25eacb226e 100644
--- a/app/models/user_settings.rb
+++ b/app/models/user_settings.rb
@@ -69,6 +69,7 @@ class UserSettings
setting :trends, default: true
setting :appeal, default: true
setting :warning, default: true
+ setting :software_updates, default: 'critical', in: %w(none critical patch all)
end
namespace :interactions do
diff --git a/app/policies/software_update_policy.rb b/app/policies/software_update_policy.rb
new file mode 100644
index 0000000000..dcb565814f
--- /dev/null
+++ b/app/policies/software_update_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class SoftwareUpdatePolicy < ApplicationPolicy
+ def index?
+ role.can?(:view_devops)
+ end
+end
diff --git a/app/presenters/initial_state_presenter.rb b/app/presenters/initial_state_presenter.rb
index 492f658030..0d2b6bfdef 100644
--- a/app/presenters/initial_state_presenter.rb
+++ b/app/presenters/initial_state_presenter.rb
@@ -3,9 +3,13 @@
class InitialStatePresenter < ActiveModelSerializers::Model
attributes :settings, :push_subscription, :token,
:current_account, :admin, :owner, :text, :visibility, :searchability,
- :disabled_account, :moved_to_account
+ :disabled_account, :moved_to_account, :critical_updates_pending
def role
current_account&.user_role
end
+
+ def critical_updates_pending
+ role&.can?(:view_devops) && SoftwareUpdate.urgent_pending?
+ end
end
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index 063c17e57d..faa97ea750 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -7,6 +7,8 @@ class InitialStateSerializer < ActiveModel::Serializer
:media_attachments, :settings,
:languages
+ attribute :critical_updates_pending, if: -> { object&.role&.can?(:view_devops) && SoftwareUpdate.check_enabled? }
+
has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer
has_one :role, serializer: REST::RoleSerializer
diff --git a/app/services/concerns/payloadable.rb b/app/services/concerns/payloadable.rb
index 1389a42ed6..bd9d9d74b5 100644
--- a/app/services/concerns/payloadable.rb
+++ b/app/services/concerns/payloadable.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
module Payloadable
+ include AuthorizedFetchHelper
+
# @param [ActiveModelSerializers::Model] record
# @param [ActiveModelSerializers::Serializer] serializer
# @param [Hash] options
@@ -23,6 +25,6 @@ module Payloadable
end
def signing_enabled?
- ENV['AUTHORIZED_FETCH'] != 'true' && !Rails.configuration.x.limited_federation_mode
+ !authorized_fetch_mode?
end
end
diff --git a/app/services/software_update_check_service.rb b/app/services/software_update_check_service.rb
new file mode 100644
index 0000000000..49b92f104d
--- /dev/null
+++ b/app/services/software_update_check_service.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+class SoftwareUpdateCheckService < BaseService
+ def call
+ clean_outdated_updates!
+ return unless SoftwareUpdate.check_enabled?
+
+ process_update_notices!(fetch_update_notices)
+ end
+
+ private
+
+ def clean_outdated_updates!
+ SoftwareUpdate.find_each do |software_update|
+ software_update.delete if Mastodon::Version.gem_version >= software_update.gem_version
+ rescue ArgumentError
+ software_update.delete
+ end
+ 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 Oj.load(res.body_with_limit, mode: :strict) if res.code == 200
+ end
+ rescue HTTP::Error, OpenSSL::SSL::SSLError, Oj::ParseError
+ nil
+ end
+
+ def api_url
+ ENV.fetch('UPDATE_CHECK_URL', 'https://api.joinmastodon.org/update-check')
+ end
+
+ def version
+ @version ||= Mastodon::Version.to_s.split('+')[0]
+ end
+
+ def process_update_notices!(update_notices)
+ return if update_notices.blank? || update_notices['updatesAvailable'].blank?
+
+ # Clear notices that are not listed by the update server anymore
+ SoftwareUpdate.where.not(version: update_notices['updatesAvailable'].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']) }
+ return if new_update_notices.blank?
+
+ new_updates = new_update_notices.map do |notice|
+ SoftwareUpdate.create!(version: notice['version'], urgent: notice['urgent'], type: notice['type'], release_notes: notice['releaseNotes'])
+ end
+
+ notify_devops!(new_updates)
+ end
+
+ def should_notify_user?(user, urgent_version, patch_version)
+ case user.settings['notification_emails.software_updates']
+ when 'none'
+ false
+ when 'critical'
+ urgent_version
+ when 'patch'
+ urgent_version || patch_version
+ when 'all'
+ true
+ end
+ end
+
+ def notify_devops!(new_updates)
+ has_new_urgent_version = new_updates.any?(&:urgent?)
+ has_new_patch_version = new_updates.any?(&:patch_type?)
+
+ User.those_who_can(:view_devops).includes(:account).find_each do |user|
+ next unless should_notify_user?(user, has_new_urgent_version, has_new_patch_version)
+
+ if has_new_urgent_version
+ AdminMailer.with(recipient: user.account).new_critical_software_updates.deliver_later
+ else
+ AdminMailer.with(recipient: user.account).new_software_updates.deliver_later
+ end
+ end
+ end
+end
diff --git a/app/views/admin/settings/discovery/show.html.haml b/app/views/admin/settings/discovery/show.html.haml
index c48e0fdc62..62011d5c56 100644
--- a/app/views/admin/settings/discovery/show.html.haml
+++ b/app/views/admin/settings/discovery/show.html.haml
@@ -39,6 +39,11 @@
.fields-group
= f.input :peers_api_enabled, as: :boolean, wrapper: :with_label, recommended: :recommended
+ %h4= t('admin.settings.security.federation_authentication')
+
+ .fields-group
+ = f.input :authorized_fetch, as: :boolean, wrapper: :with_label, label: t('admin.settings.security.authorized_fetch'), warning_hint: authorized_fetch_overridden? ? t('admin.settings.security.authorized_fetch_overridden_hint') : nil, hint: t('admin.settings.security.authorized_fetch_hint'), disabled: authorized_fetch_overridden?, recommended: authorized_fetch_overridden? ? :overridden : nil
+
%h4= t('admin.settings.discovery.follow_recommendations')
.fields-group
diff --git a/app/views/admin/software_updates/index.html.haml b/app/views/admin/software_updates/index.html.haml
new file mode 100644
index 0000000000..7a223ee07b
--- /dev/null
+++ b/app/views/admin/software_updates/index.html.haml
@@ -0,0 +1,29 @@
+- content_for :page_title do
+ = t('admin.software_updates.title')
+
+.simple_form
+ %p.lead
+ = t('admin.software_updates.description')
+ = link_to t('admin.software_updates.documentation_link'), 'https://docs.joinmastodon.org/admin/upgrading/#automated_checks', target: '_new'
+
+%hr.spacer
+
+- unless @software_updates.empty?
+ .table-wrapper
+ %table.table
+ %thead
+ %tr
+ %th= t('admin.software_updates.version')
+ %th= t('admin.software_updates.type')
+ %th
+ %th
+ %tbody
+ - @software_updates.each do |update|
+ %tr
+ %td= update.version
+ %td= t("admin.software_updates.types.#{update.type}")
+ - if update.urgent?
+ %td.critical= t("admin.software_updates.critical_update")
+ - else
+ %td
+ %td= table_link_to 'link', t('admin.software_updates.release_notes'), update.release_notes
diff --git a/app/views/admin_mailer/new_critical_software_updates.text.erb b/app/views/admin_mailer/new_critical_software_updates.text.erb
new file mode 100644
index 0000000000..c901bc50f7
--- /dev/null
+++ b/app/views/admin_mailer/new_critical_software_updates.text.erb
@@ -0,0 +1,5 @@
+<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
+
+<%= raw t('admin_mailer.new_critical_software_updates.body') %>
+
+<%= raw t('application_mailer.view')%> <%= admin_software_updates_url %>
diff --git a/app/views/admin_mailer/new_software_updates.text.erb b/app/views/admin_mailer/new_software_updates.text.erb
new file mode 100644
index 0000000000..2fc4d1a5f2
--- /dev/null
+++ b/app/views/admin_mailer/new_software_updates.text.erb
@@ -0,0 +1,5 @@
+<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
+
+<%= raw t('admin_mailer.new_software_updates.body') %>
+
+<%= raw t('application_mailer.view')%> <%= admin_software_updates_url %>
diff --git a/app/views/settings/preferences/notifications/show.html.haml b/app/views/settings/preferences/notifications/show.html.haml
index 71f6415427..3e40a736ee 100644
--- a/app/views/settings/preferences/notifications/show.html.haml
+++ b/app/views/settings/preferences/notifications/show.html.haml
@@ -22,7 +22,7 @@
.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)
+ - if current_user.can?(:manage_reports, :manage_appeals, :manage_users, :manage_taxonomies) || (SoftwareUpdate.check_enabled? && current_user.can?(:view_devops))
%h4= t 'notifications.administration_emails'
.fields-group
@@ -31,6 +31,10 @@
= 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', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.software_updates.label'), collection: %w(none critical patch all), label_method: ->(setting) { I18n.t("simple_form.labels.notification_emails.software_updates.#{setting}") }, include_blank: false, hint: false
+
%h4= t 'notifications.other_settings'
.fields-group
diff --git a/app/workers/scheduler/indexing_scheduler.rb b/app/workers/scheduler/indexing_scheduler.rb
index 1b09730c7d..ff1b744442 100644
--- a/app/workers/scheduler/indexing_scheduler.rb
+++ b/app/workers/scheduler/indexing_scheduler.rb
@@ -16,9 +16,7 @@ class Scheduler::IndexingScheduler
indexes.each do |type|
with_redis do |redis|
redis.sscan_each("chewy:queue:#{type.name}", count: SCAN_BATCH_SIZE).each_slice(IMPORT_BATCH_SIZE) do |ids|
- with_read_replica do
- type.import!(ids)
- end
+ type.import!(ids)
redis.srem("chewy:queue:#{type.name}", ids)
end
diff --git a/app/workers/scheduler/software_update_check_scheduler.rb b/app/workers/scheduler/software_update_check_scheduler.rb
new file mode 100644
index 0000000000..c732bdedc0
--- /dev/null
+++ b/app/workers/scheduler/software_update_check_scheduler.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class Scheduler::SoftwareUpdateCheckScheduler
+ include Sidekiq::Worker
+
+ sidekiq_options retry: 0, lock: :until_executed, lock_ttl: 1.hour.to_i
+
+ def perform
+ SoftwareUpdateCheckService.new.call
+ end
+end
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index d0677b80fb..2d4487ce56 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -50,7 +50,7 @@ ignore_unused:
- 'activerecord.errors.*'
- '{devise,pagination,doorkeeper}.*'
- '{date,datetime,time,number}.*'
- - 'simple_form.{yes,no,recommended,not_recommended}'
+ - 'simple_form.{yes,no,recommended,not_recommended,overridden}'
- 'simple_form.{placeholders,hints,labels}.*'
- 'simple_form.{error_notification,required}.:'
- 'errors.messages.*'
diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb
index 3cdaed7d03..4e2edd7d72 100644
--- a/config/initializers/simple_form.rb
+++ b/config/initializers/simple_form.rb
@@ -110,7 +110,8 @@ SimpleForm.setup do |config|
end
end
- b.use :hint, wrap_with: { tag: :span, class: :hint }
+ b.use :warning_hint, wrap_with: { tag: :span, class: [:hint, 'warning-hint'] }
+ b.use :hint, wrap_with: { tag: :span, class: :hint }
b.use :error, wrap_with: { tag: :span, class: :error }
end
@@ -124,8 +125,8 @@ SimpleForm.setup do |config|
config.wrappers :with_block_label, class: [:input, :with_block_label], hint_class: :field_with_hint, error_class: :field_with_errors do |b|
b.use :html5
b.use :label
- b.use :hint, wrap_with: { tag: :span, class: :hint }
b.use :warning_hint, wrap_with: { tag: :span, class: [:hint, 'warning-hint'] }
+ b.use :hint, wrap_with: { tag: :span, class: :hint }
b.use :input, wrap_with: { tag: :div, class: :label_input }
b.use :error, wrap_with: { tag: :span, class: :error }
end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 9ac7bce983..ebcf085ad8 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -314,6 +314,7 @@ en:
unpublish: Unpublish
unpublished_msg: Announcement successfully unpublished!
updated_msg: Announcement successfully updated!
+ critical_update_pending: Critical update pending
custom_emojis:
assign_category: Assign category
by_domain: Domain
@@ -822,10 +823,27 @@ en:
approved: Approval required for sign up
none: Nobody can sign up
open: Anyone can sign up
+ security:
+ authorized_fetch: Require authentication from federated servers
+ authorized_fetch_hint: Requiring authentication from federated servers enables stricter enforcement of both user-level and server-level blocks. However, this comes at the cost of a performance penalty, reduces the reach of your replies, and may introduce compatibility issues with some federated services. In addition, this will not prevent dedicated actors from fetching your public posts and accounts.
+ authorized_fetch_overridden_hint: You are currently unable to change this setting because it is overridden by an environment variable.
+ federation_authentication: Federation authentication enforcement
title: Server settings
site_uploads:
delete: Delete uploaded file
destroyed_msg: Site upload successfully deleted!
+ software_updates:
+ 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 e-mail notification preferences.
+ documentation_link: Learn more
+ release_notes: Release notes
+ title: Available updates
+ type: Type
+ types:
+ major: Major release
+ minor: Minor release
+ patch: Patch release — bugfixes and easy to apply changes
+ version: Version
statuses:
account: Author
application: Application
@@ -897,6 +915,12 @@ en:
message_html: You haven't defined any server rules.
sidekiq_process_check:
message_html: No Sidekiq process running for the %{value} queue(s). Please review your Sidekiq configuration
+ software_version_critical_check:
+ action: See available updates
+ message_html: A critical Mastodon update is available, please update as quickly as possible.
+ software_version_patch_check:
+ action: See available updates
+ message_html: A bugfix Mastodon update is available.
upload_check_privacy_error:
action: Check here for more information
message_html: "Your web server is misconfigured. The privacy of your users is at risk."
@@ -1010,6 +1034,9 @@ en:
body: "%{target} is appealing a moderation decision by %{action_taken_by} from %{date}, which was %{type}. They wrote:"
next_steps: You can approve the appeal to undo the moderation decision, or ignore it.
subject: "%{username} is appealing a moderation decision on %{instance}"
+ new_critical_software_updates:
+ body: New critical versions of Mastodon have been released, you may want to update as soon as possible!
+ subject: Critical Mastodon updates are available for %{instance}!
new_pending_account:
body: The details of the new account are below. You can approve or reject this application.
subject: New account up for review on %{instance} (%{username})
@@ -1017,6 +1044,9 @@ en:
body: "%{reporter} has reported %{target}"
body_remote: Someone from %{domain} has reported %{target}
subject: New report for %{instance} (#%{id})
+ new_software_updates:
+ body: New Mastodon versions have been released, you may want to update!
+ subject: New Mastodon versions are available for %{instance}!
new_trends:
body: 'The following items need a review before they can be displayed publicly:'
new_trending_links:
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index bf2d957c72..c280a0cf1e 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -336,6 +336,12 @@ en:
pending_account: New account needs review
reblog: Someone boosted your post
report: New report is submitted
+ software_updates:
+ all: Notify on all updates
+ critical: Notify on critical updates only
+ label: A new Mastodon version is available
+ none: Never notify of updates (not recommended)
+ patch: Notify on bugfix updates
trending_tag: New trend requires review
rule:
text: Rule
@@ -362,6 +368,7 @@ en:
url: Endpoint URL
'no': 'No'
not_recommended: Not recommended
+ overridden: Overridden
recommended: Recommended
required:
mark: "*"
diff --git a/config/navigation.rb b/config/navigation.rb
index b381a6cf43..72b915e8bb 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -3,6 +3,9 @@
SimpleNavigation::Configuration.run do |navigation|
navigation.items do |n|
n.item :web, safe_join([fa_icon('chevron-left fw'), t('settings.back')]), root_path
+
+ n.item :software_updates, safe_join([fa_icon('exclamation-circle fw'), t('admin.critical_update_pending')]), admin_software_updates_path, if: -> { ENV['UPDATE_CHECK_URL'] != '' && current_user.can?(:view_devops) && SoftwareUpdate.urgent_pending? }, html: { class: 'warning' }
+
n.item :profile, safe_join([fa_icon('user fw'), t('settings.profile')]), settings_profile_path, if: -> { current_user.functional? }, highlights_on: %r{/settings/profile|/settings/featured_tags|/settings/verification|/settings/privacy}
n.item :preferences, safe_join([fa_icon('cog fw'), t('settings.preferences')]), settings_preferences_path, if: -> { current_user.functional? } do |s|
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index aeca09acdc..176ef6cb14 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -212,4 +212,6 @@ namespace :admin do
end
end
end
+
+ resources :software_updates, only: [:index]
end
diff --git a/config/sidekiq.yml b/config/sidekiq.yml
index 75e4ef0be7..f954c00aaa 100644
--- a/config/sidekiq.yml
+++ b/config/sidekiq.yml
@@ -66,3 +66,7 @@
cron: '0 0 * * *'
class: Scheduler::UpdateInstanceInfoScheduler
queue: scheduler
+ software_update_check_scheduler:
+ interval: 30 minutes
+ class: Scheduler::SoftwareUpdateCheckScheduler
+ queue: scheduler
diff --git a/db/migrate/20230822081029_create_software_updates.rb b/db/migrate/20230822081029_create_software_updates.rb
new file mode 100644
index 0000000000..146d5d3037
--- /dev/null
+++ b/db/migrate/20230822081029_create_software_updates.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class CreateSoftwareUpdates < ActiveRecord::Migration[7.0]
+ def change
+ create_table :software_updates do |t|
+ t.string :version, null: false
+ t.boolean :urgent, default: false, null: false
+ t.integer :type, default: 0, null: false
+ t.string :release_notes, default: '', null: false
+
+ t.timestamps
+ end
+
+ add_index :software_updates, :version, unique: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index d4344a0832..5cd59bc8b1 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1073,6 +1073,16 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_26_023400) do
t.index ["var"], name: "index_site_uploads_on_var", unique: true
end
+ create_table "software_updates", force: :cascade do |t|
+ t.string "version", null: false
+ t.boolean "urgent", default: false, null: false
+ t.integer "type", default: 0, null: false
+ t.string "release_notes", default: "", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["version"], name: "index_software_updates_on_version", unique: true
+ end
+
create_table "status_capability_tokens", force: :cascade do |t|
t.bigint "status_id", null: false
t.string "token"
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index 05f35b98aa..555f1a7e19 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -39,6 +39,10 @@ module Mastodon
components.join
end
+ def gem_version
+ @gem_version ||= Gem::Version.new(to_s.split('+')[0])
+ end
+
def repository
ENV.fetch('GITHUB_REPOSITORY', 'kmycode/mastodon')
end
diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake
index 010caaf8ea..f68d1cf1f8 100644
--- a/lib/tasks/mastodon.rake
+++ b/lib/tasks/mastodon.rake
@@ -424,6 +424,10 @@ namespace :mastodon do
end
end
+ prompt.say "\n"
+
+ env['UPDATE_CHECK_URL'] = '' unless prompt.yes?('Do you want Mastodon to periodically check for important updates and notify you? (Recommended)', default: true)
+
prompt.say "\n"
prompt.say 'This configuration will be written to .env.production'
diff --git a/package.json b/package.json
index e6a880f262..3aa9d7c8ed 100644
--- a/package.json
+++ b/package.json
@@ -82,6 +82,7 @@
"immutable": "^4.3.0",
"imports-loader": "^1.2.0",
"intl-messageformat": "^10.3.5",
+ "ioredis": "^5.3.2",
"js-yaml": "^4.1.0",
"jsdom": "^22.1.0",
"lodash": "^4.17.21",
@@ -119,7 +120,6 @@
"react-swipeable-views": "^0.14.0",
"react-textarea-autosize": "^8.4.1",
"react-toggle": "^4.1.3",
- "redis": "^4.6.5",
"redux": "^4.2.1",
"redux-immutable": "^4.0.0",
"redux-thunk": "^2.4.2",
diff --git a/spec/fabricators/software_update_fabricator.rb b/spec/fabricators/software_update_fabricator.rb
new file mode 100644
index 0000000000..622fff66e8
--- /dev/null
+++ b/spec/fabricators/software_update_fabricator.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+Fabricator(:software_update) do
+ version '99.99.99'
+ urgent false
+ type 'patch'
+end
diff --git a/spec/features/admin/software_updates_spec.rb b/spec/features/admin/software_updates_spec.rb
new file mode 100644
index 0000000000..4a635d1a79
--- /dev/null
+++ b/spec/features/admin/software_updates_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'finding software updates through the admin interface' do
+ before do
+ Fabricate(:software_update, version: '99.99.99', type: 'major', urgent: true, release_notes: 'https://github.com/mastodon/mastodon/releases/v99')
+
+ sign_in Fabricate(:user, role: UserRole.find_by(name: 'Owner')), scope: :user
+ end
+
+ it 'shows a link to the software updates page, which links to release notes' do
+ visit settings_profile_path
+ click_on I18n.t('admin.critical_update_pending')
+
+ expect(page).to have_title(I18n.t('admin.software_updates.title'))
+
+ expect(page).to have_content('99.99.99')
+
+ click_on I18n.t('admin.software_updates.release_notes')
+ expect(page).to have_current_path('https://github.com/mastodon/mastodon/releases/v99', url: true)
+ end
+end
diff --git a/spec/lib/admin/system_check/software_version_check_spec.rb b/spec/lib/admin/system_check/software_version_check_spec.rb
new file mode 100644
index 0000000000..de4335fc51
--- /dev/null
+++ b/spec/lib/admin/system_check/software_version_check_spec.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::SystemCheck::SoftwareVersionCheck do
+ include RoutingHelper
+
+ subject(:check) { described_class.new(user) }
+
+ let(:user) { Fabricate(:user) }
+
+ describe 'skip?' do
+ context 'when user cannot view devops' do
+ before { allow(user).to receive(:can?).with(:view_devops).and_return(false) }
+
+ it 'returns true' do
+ expect(check.skip?).to be true
+ end
+ end
+
+ context 'when user can view devops' do
+ before { allow(user).to receive(:can?).with(:view_devops).and_return(true) }
+
+ it 'returns false' do
+ expect(check.skip?).to be false
+ end
+
+ context 'when checks are disabled' do
+ around do |example|
+ ClimateControl.modify UPDATE_CHECK_URL: '' do
+ example.run
+ end
+ end
+
+ it 'returns true' do
+ expect(check.skip?).to be true
+ end
+ end
+ end
+ end
+
+ describe 'pass?' do
+ context 'when there is no known update' do
+ it 'returns true' do
+ expect(check.pass?).to be true
+ end
+ end
+
+ context 'when there is a non-urgent major release' do
+ before do
+ Fabricate(:software_update, version: '99.99.99', type: 'major', urgent: false)
+ end
+
+ it 'returns true' do
+ expect(check.pass?).to be true
+ end
+ end
+
+ context 'when there is an urgent major release' do
+ before do
+ Fabricate(:software_update, version: '99.99.99', type: 'major', urgent: true)
+ end
+
+ it 'returns false' do
+ expect(check.pass?).to be false
+ end
+ end
+
+ context 'when there is an urgent minor release' do
+ before do
+ Fabricate(:software_update, version: '99.99.99', type: 'minor', urgent: true)
+ end
+
+ it 'returns false' do
+ expect(check.pass?).to be false
+ end
+ end
+
+ context 'when there is an urgent patch release' do
+ before do
+ Fabricate(:software_update, version: '99.99.99', type: 'patch', urgent: true)
+ end
+
+ it 'returns false' do
+ expect(check.pass?).to be false
+ end
+ end
+
+ context 'when there is a non-urgent patch release' do
+ before do
+ Fabricate(:software_update, version: '99.99.99', type: 'patch', urgent: false)
+ end
+
+ it 'returns false' do
+ expect(check.pass?).to be false
+ end
+ end
+ end
+
+ describe 'message' do
+ context 'when there is a non-urgent patch release pending' do
+ before do
+ Fabricate(:software_update, version: '99.99.99', type: 'patch', urgent: false)
+ end
+
+ it 'sends class name symbol to message instance' do
+ allow(Admin::SystemCheck::Message).to receive(:new)
+ .with(:software_version_patch_check, anything, anything)
+
+ check.message
+
+ expect(Admin::SystemCheck::Message).to have_received(:new)
+ .with(:software_version_patch_check, nil, admin_software_updates_path)
+ end
+ end
+
+ context 'when there is an urgent patch release pending' do
+ before do
+ Fabricate(:software_update, version: '99.99.99', type: 'patch', urgent: true)
+ end
+
+ it 'sends class name symbol to message instance' do
+ allow(Admin::SystemCheck::Message).to receive(:new)
+ .with(:software_version_critical_check, anything, anything, anything)
+
+ check.message
+
+ expect(Admin::SystemCheck::Message).to have_received(:new)
+ .with(:software_version_critical_check, nil, admin_software_updates_path, true)
+ end
+ end
+ end
+end
diff --git a/spec/mailers/admin_mailer_spec.rb b/spec/mailers/admin_mailer_spec.rb
index 9123804a48..423dce88ab 100644
--- a/spec/mailers/admin_mailer_spec.rb
+++ b/spec/mailers/admin_mailer_spec.rb
@@ -85,4 +85,46 @@ RSpec.describe AdminMailer do
expect(mail.body.encoded).to match 'The following items need a review before they can be displayed publicly'
end
end
+
+ describe '.new_software_updates' do
+ let(:recipient) { Fabricate(:account, username: 'Bob') }
+ let(:mail) { described_class.with(recipient: recipient).new_software_updates }
+
+ before do
+ recipient.user.update(locale: :en)
+ end
+
+ it 'renders the headers' do
+ expect(mail.subject).to eq('New Mastodon versions are available for cb6e6126.ngrok.io!')
+ expect(mail.to).to eq [recipient.user_email]
+ expect(mail.from).to eq ['notifications@localhost']
+ end
+
+ it 'renders the body' do
+ expect(mail.body.encoded).to match 'New Mastodon versions have been released, you may want to update!'
+ end
+ end
+
+ describe '.new_critical_software_updates' do
+ let(:recipient) { Fabricate(:account, username: 'Bob') }
+ let(:mail) { described_class.with(recipient: recipient).new_critical_software_updates }
+
+ before do
+ recipient.user.update(locale: :en)
+ end
+
+ it 'renders the headers', :aggregate_failures do
+ expect(mail.subject).to eq('Critical Mastodon updates are available for cb6e6126.ngrok.io!')
+ expect(mail.to).to eq [recipient.user_email]
+ expect(mail.from).to eq ['notifications@localhost']
+
+ expect(mail['Importance'].value).to eq 'high'
+ expect(mail['Priority'].value).to eq 'urgent'
+ expect(mail['X-Priority'].value).to eq '1'
+ end
+
+ it 'renders the body' do
+ expect(mail.body.encoded).to match 'New critical versions of Mastodon have been released, you may want to update as soon as possible!'
+ end
+ end
end
diff --git a/spec/models/software_update_spec.rb b/spec/models/software_update_spec.rb
new file mode 100644
index 0000000000..0a494b0c4c
--- /dev/null
+++ b/spec/models/software_update_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe SoftwareUpdate do
+ describe '.pending_to_a' do
+ before do
+ allow(Mastodon::Version).to receive(:gem_version).and_return(Gem::Version.new(mastodon_version))
+
+ Fabricate(:software_update, version: '3.4.42', type: 'patch', urgent: true)
+ Fabricate(:software_update, version: '3.5.0', type: 'minor', urgent: false)
+ Fabricate(:software_update, version: '4.2.0', type: 'major', urgent: false)
+ end
+
+ context 'when the Mastodon version is an outdated release' do
+ let(:mastodon_version) { '3.4.0' }
+
+ it 'returns the expected versions' do
+ expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('3.4.42', '3.5.0', '4.2.0')
+ end
+ end
+
+ context 'when the Mastodon version is more recent than anything last returned by the server' do
+ let(:mastodon_version) { '5.0.0' }
+
+ it 'returns the expected versions' do
+ expect(described_class.pending_to_a.pluck(:version)).to eq []
+ end
+ end
+
+ context 'when the Mastodon version is an outdated nightly' do
+ let(:mastodon_version) { '4.3.0-nightly.2023-09-10' }
+
+ before do
+ Fabricate(:software_update, version: '4.3.0-nightly.2023-09-12', type: 'major', urgent: true)
+ end
+
+ it 'returns the expected versions' do
+ expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-nightly.2023-09-12')
+ end
+ end
+
+ context 'when the Mastodon version is a very outdated nightly' do
+ let(:mastodon_version) { '4.2.0-nightly.2023-07-10' }
+
+ it 'returns the expected versions' do
+ expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.2.0')
+ end
+ end
+
+ context 'when the Mastodon version is an outdated dev version' do
+ let(:mastodon_version) { '4.3.0-0.dev.0' }
+
+ before do
+ Fabricate(:software_update, version: '4.3.0-0.dev.2', type: 'major', urgent: true)
+ end
+
+ it 'returns the expected versions' do
+ expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-0.dev.2')
+ end
+ end
+
+ context 'when the Mastodon version is an outdated beta version' do
+ let(:mastodon_version) { '4.3.0-beta1' }
+
+ before do
+ Fabricate(:software_update, version: '4.3.0-beta2', type: 'major', urgent: true)
+ end
+
+ it 'returns the expected versions' do
+ expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-beta2')
+ end
+ end
+
+ context 'when the Mastodon version is an outdated beta version and there is a rc' do
+ let(:mastodon_version) { '4.3.0-beta1' }
+
+ before do
+ Fabricate(:software_update, version: '4.3.0-rc1', type: 'major', urgent: true)
+ end
+
+ it 'returns the expected versions' do
+ expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-rc1')
+ end
+ end
+ end
+end
diff --git a/spec/policies/software_update_policy_spec.rb b/spec/policies/software_update_policy_spec.rb
new file mode 100644
index 0000000000..e19ba61612
--- /dev/null
+++ b/spec/policies/software_update_policy_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+require 'pundit/rspec'
+
+RSpec.describe SoftwareUpdatePolicy do
+ subject { described_class }
+
+ let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Owner')).account }
+ let(:john) { Fabricate(:account) }
+
+ permissions :index? do
+ context 'when owner' do
+ it 'permits' do
+ expect(subject).to permit(admin, SoftwareUpdate)
+ end
+ end
+
+ context 'when not owner' do
+ it 'denies' do
+ expect(subject).to_not permit(john, SoftwareUpdate)
+ end
+ end
+ end
+end
diff --git a/spec/services/software_update_check_service_spec.rb b/spec/services/software_update_check_service_spec.rb
new file mode 100644
index 0000000000..c8821348ac
--- /dev/null
+++ b/spec/services/software_update_check_service_spec.rb
@@ -0,0 +1,158 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe SoftwareUpdateCheckService, type: :service do
+ subject { described_class.new }
+
+ shared_examples 'when the feature is enabled' do
+ let(:full_update_check_url) { "#{update_check_url}?version=#{Mastodon::Version.to_s.split('+')[0]}" }
+
+ let(:devops_role) { Fabricate(:user_role, name: 'DevOps', permissions: UserRole::FLAGS[:view_devops]) }
+ let(:owner_user) { Fabricate(:user, role: UserRole.find_by(name: 'Owner')) }
+ let(:old_devops_user) { Fabricate(:user) }
+ let(:none_user) { Fabricate(:user, role: devops_role) }
+ let(:patch_user) { Fabricate(:user, role: devops_role) }
+ let(:critical_user) { Fabricate(:user, role: devops_role) }
+
+ around do |example|
+ queue_adapter = ActiveJob::Base.queue_adapter
+ ActiveJob::Base.queue_adapter = :test
+
+ example.run
+
+ ActiveJob::Base.queue_adapter = queue_adapter
+ end
+
+ before do
+ Fabricate(:software_update, version: '3.5.0', type: 'major', urgent: false)
+ Fabricate(:software_update, version: '42.13.12', type: 'major', urgent: false)
+
+ owner_user.settings.update('notification_emails.software_updates': 'all')
+ owner_user.save!
+
+ old_devops_user.settings.update('notification_emails.software_updates': 'all')
+ old_devops_user.save!
+
+ none_user.settings.update('notification_emails.software_updates': 'none')
+ none_user.save!
+
+ patch_user.settings.update('notification_emails.software_updates': 'patch')
+ patch_user.save!
+
+ critical_user.settings.update('notification_emails.software_updates': 'critical')
+ critical_user.save!
+ end
+
+ context 'when the update server errors out' do
+ before do
+ stub_request(:get, full_update_check_url).to_return(status: 404)
+ end
+
+ it 'deletes outdated update records but keeps valid update records' do
+ expect { subject.call }.to change { SoftwareUpdate.pluck(:version).sort }.from(['3.5.0', '42.13.12']).to(['42.13.12'])
+ end
+ end
+
+ context 'when the server returns new versions' do
+ let(:server_json) do
+ {
+ updatesAvailable: [
+ {
+ version: '4.2.1',
+ urgent: false,
+ type: 'patch',
+ releaseNotes: 'https://github.com/mastodon/mastodon/releases/v4.2.1',
+ },
+ {
+ version: '4.3.0',
+ urgent: false,
+ type: 'minor',
+ releaseNotes: 'https://github.com/mastodon/mastodon/releases/v4.3.0',
+ },
+ {
+ version: '5.0.0',
+ urgent: false,
+ type: 'minor',
+ releaseNotes: 'https://github.com/mastodon/mastodon/releases/v5.0.0',
+ },
+ ],
+ }
+ end
+
+ before do
+ stub_request(:get, full_update_check_url).to_return(body: Oj.dump(server_json))
+ end
+
+ it 'updates the list of known updates' do
+ expect { subject.call }.to change { SoftwareUpdate.pluck(:version).sort }.from(['3.5.0', '42.13.12']).to(['4.2.1', '4.3.0', '5.0.0'])
+ 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)
+ .with(hash_including(params: { recipient: owner_user.account })).once
+ .and(have_enqueued_mail(AdminMailer, :new_software_updates).with(hash_including(params: { recipient: patch_user.account })).once)
+ .and(have_enqueued_mail.at_most(2))
+ end
+ end
+
+ context 'when an update is urgent' do
+ let(:server_json) do
+ {
+ updatesAvailable: [
+ {
+ version: '5.0.0',
+ urgent: true,
+ type: 'minor',
+ releaseNotes: 'https://github.com/mastodon/mastodon/releases/v5.0.0',
+ },
+ ],
+ }
+ end
+
+ it 'sends e-mail notifications according to settings', :aggregate_failures do
+ expect { subject.call }.to have_enqueued_mail(AdminMailer, :new_critical_software_updates)
+ .with(hash_including(params: { recipient: owner_user.account })).once
+ .and(have_enqueued_mail(AdminMailer, :new_critical_software_updates).with(hash_including(params: { recipient: patch_user.account })).once)
+ .and(have_enqueued_mail(AdminMailer, :new_critical_software_updates).with(hash_including(params: { recipient: critical_user.account })).once)
+ .and(have_enqueued_mail.at_most(3))
+ end
+ end
+ end
+ end
+
+ context 'when update checking is disabled' do
+ around do |example|
+ ClimateControl.modify UPDATE_CHECK_URL: '' do
+ example.run
+ end
+ end
+
+ before do
+ Fabricate(:software_update, version: '3.5.0', type: 'major', urgent: false)
+ end
+
+ it 'deletes outdated update records' do
+ expect { subject.call }.to change(SoftwareUpdate, :count).from(1).to(0)
+ end
+ end
+
+ context 'when using the default update checking API' do
+ let(:update_check_url) { 'https://api.joinmastodon.org/update-check' }
+
+ it_behaves_like 'when the feature is enabled'
+ end
+
+ context 'when using a custom update check URL' do
+ let(:update_check_url) { 'https://api.example.com/update_check' }
+
+ around do |example|
+ ClimateControl.modify UPDATE_CHECK_URL: 'https://api.example.com/update_check' do
+ example.run
+ end
+ end
+
+ it_behaves_like 'when the feature is enabled'
+ end
+end
diff --git a/spec/workers/scheduler/software_update_check_scheduler_spec.rb b/spec/workers/scheduler/software_update_check_scheduler_spec.rb
new file mode 100644
index 0000000000..f596c0a1ec
--- /dev/null
+++ b/spec/workers/scheduler/software_update_check_scheduler_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Scheduler::SoftwareUpdateCheckScheduler do
+ subject { described_class.new }
+
+ describe 'perform' do
+ let(:service_double) { instance_double(SoftwareUpdateCheckService, call: nil) }
+
+ before do
+ allow(SoftwareUpdateCheckService).to receive(:new).and_return(service_double)
+ end
+
+ it 'calls SoftwareUpdateCheckService' do
+ subject.perform
+ expect(service_double).to have_received(:call)
+ end
+ end
+end
diff --git a/streaming/index.js b/streaming/index.js
index 8b16bfe533..464e0194a0 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -6,12 +6,12 @@ const url = require('url');
const dotenv = require('dotenv');
const express = require('express');
+const Redis = require('ioredis');
const { JSDOM } = require('jsdom');
const log = require('npmlog');
const pg = require('pg');
const dbUrlToConfig = require('pg-connection-string').parse;
const metrics = require('prom-client');
-const redis = require('redis');
const uuid = require('uuid');
const WebSocket = require('ws');
@@ -24,30 +24,12 @@ dotenv.config({
log.level = process.env.LOG_LEVEL || 'verbose';
/**
- * @param {Object.} defaultConfig
- * @param {string} redisUrl
+ * @param {Object.} config
*/
-const redisUrlToClient = async (defaultConfig, redisUrl) => {
- const config = defaultConfig;
-
- let client;
-
- if (!redisUrl) {
- client = redis.createClient(config);
- } else if (redisUrl.startsWith('unix://')) {
- client = redis.createClient(Object.assign(config, {
- socket: {
- path: redisUrl.slice(7),
- },
- }));
- } else {
- client = redis.createClient(Object.assign(config, {
- url: redisUrl,
- }));
- }
-
+const createRedisClient = async (config) => {
+ const { redisParams, redisUrl } = config;
+ const client = new Redis(redisUrl, redisParams);
client.on('error', (err) => log.error('Redis Client Error!', err));
- await client.connect();
return client;
};
@@ -147,23 +129,22 @@ const pgConfigFromEnv = (env) => {
* @returns {Object.} configuration for the Redis connection
*/
const redisConfigFromEnv = (env) => {
- const redisNamespace = env.REDIS_NAMESPACE || null;
+ // ioredis *can* transparently add prefixes for us, but it doesn't *in some cases*,
+ // which means we can't use it. But this is something that should be looked into.
+ const redisPrefix = env.REDIS_NAMESPACE ? `${env.REDIS_NAMESPACE}:` : '';
const redisParams = {
- socket: {
- host: env.REDIS_HOST || '127.0.0.1',
- port: env.REDIS_PORT || 6379,
- },
- database: env.REDIS_DB || 0,
+ host: env.REDIS_HOST || '127.0.0.1',
+ port: env.REDIS_PORT || 6379,
+ db: env.REDIS_DB || 0,
password: env.REDIS_PASSWORD || undefined,
};
- if (redisNamespace) {
- redisParams.namespace = redisNamespace;
+ // redisParams.path takes precedence over host and port.
+ if (env.REDIS_URL && env.REDIS_URL.startsWith('unix://')) {
+ redisParams.path = env.REDIS_URL.slice(7);
}
- const redisPrefix = redisNamespace ? `${redisNamespace}:` : '';
-
return {
redisParams,
redisPrefix,
@@ -179,15 +160,15 @@ const startServer = async () => {
const pgPool = new pg.Pool(pgConfigFromEnv(process.env));
const server = http.createServer(app);
- const { redisParams, redisUrl, redisPrefix } = redisConfigFromEnv(process.env);
-
/**
* @type {Object.): void>>}
*/
const subs = {};
- const redisSubscribeClient = await redisUrlToClient(redisParams, redisUrl);
- const redisClient = await redisUrlToClient(redisParams, redisUrl);
+ const redisConfig = redisConfigFromEnv(process.env);
+ const redisSubscribeClient = await createRedisClient(redisConfig);
+ const redisClient = await createRedisClient(redisConfig);
+ const { redisPrefix } = redisConfig;
// Collect metrics from Node.js
metrics.collectDefaultMetrics();
@@ -277,13 +258,13 @@ const startServer = async () => {
};
/**
- * @param {string} message
* @param {string} channel
+ * @param {string} message
*/
- const onRedisMessage = (message, channel) => {
+ const onRedisMessage = (channel, message) => {
const callbacks = subs[channel];
- log.silly(`New message on channel ${channel}`);
+ log.silly(`New message on channel ${redisPrefix}${channel}`);
if (!callbacks) {
return;
@@ -294,6 +275,7 @@ const startServer = async () => {
callbacks.forEach(callback => callback(json));
};
+ redisSubscribeClient.on("message", onRedisMessage);
/**
* @callback SubscriptionListener
@@ -312,8 +294,14 @@ const startServer = async () => {
if (subs[channel].length === 0) {
log.verbose(`Subscribe ${channel}`);
- redisSubscribeClient.subscribe(channel, onRedisMessage);
- redisSubscriptions.inc();
+ redisSubscribeClient.subscribe(channel, (err, count) => {
+ if (err) {
+ log.error(`Error subscribing to ${channel}`);
+ }
+ else {
+ redisSubscriptions.set(count);
+ }
+ });
}
subs[channel].push(callback);
@@ -334,8 +322,14 @@ const startServer = async () => {
if (subs[channel].length === 0) {
log.verbose(`Unsubscribe ${channel}`);
- redisSubscribeClient.unsubscribe(channel);
- redisSubscriptions.dec();
+ redisSubscribeClient.unsubscribe(channel, (err, count) => {
+ if (err) {
+ log.error(`Error unsubscribing to ${channel}`);
+ }
+ else {
+ redisSubscriptions.set(count);
+ }
+ });
delete subs[channel];
}
};
diff --git a/yarn.lock b/yarn.lock
index 2ffc5b1c45..196ca73587 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1452,6 +1452,11 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
+"@ioredis/commands@^1.1.1":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11"
+ integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==
+
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
@@ -1786,40 +1791,6 @@
resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-7.0.7.tgz#54af8d66160a8a7bf7d8f184703d2bf4b3fab914"
integrity sha512-J2v5Ca7HgejO7diGKiDylaVDQKmbQ5FJih6Oo3hXuBKEuXlcaccJu64lj8MNVLaPVyZx0g4gaOQZQz95QEb/hg==
-"@redis/bloom@1.2.0":
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.2.0.tgz#d3fd6d3c0af3ef92f26767b56414a370c7b63b71"
- integrity sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==
-
-"@redis/client@1.5.9":
- version "1.5.9"
- resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.5.9.tgz#c4ee81bbfedb4f1d9c7c5e9859661b9388fb4021"
- integrity sha512-SffgN+P1zdWJWSXBvJeynvEnmnZrYmtKSRW00xl8pOPFOMJjxRR9u0frSxJpPR6Y4V+k54blJjGW7FgxbTI7bQ==
- dependencies:
- cluster-key-slot "1.1.2"
- generic-pool "3.9.0"
- yallist "4.0.0"
-
-"@redis/graph@1.1.0":
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/@redis/graph/-/graph-1.1.0.tgz#cc2b82e5141a29ada2cce7d267a6b74baa6dd519"
- integrity sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==
-
-"@redis/json@1.0.4":
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/@redis/json/-/json-1.0.4.tgz#f372b5f93324e6ffb7f16aadcbcb4e5c3d39bda1"
- integrity sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==
-
-"@redis/search@1.1.3":
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/@redis/search/-/search-1.1.3.tgz#b5a6837522ce9028267fe6f50762a8bcfd2e998b"
- integrity sha512-4Dg1JjvCevdiCBTZqjhKkGoC5/BcB7k9j99kdMnaXFXg8x4eyOIVg9487CMv7/BUVkFLZCaIh8ead9mU15DNng==
-
-"@redis/time-series@1.0.5":
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-1.0.5.tgz#a6d70ef7a0e71e083ea09b967df0a0ed742bc6ad"
- integrity sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==
-
"@reduxjs/toolkit@^1.9.5":
version "1.9.5"
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.5.tgz#d3987849c24189ca483baa7aa59386c8e52077c4"
@@ -4121,7 +4092,7 @@ clone-deep@^4.0.1:
kind-of "^6.0.2"
shallow-clone "^3.0.0"
-cluster-key-slot@1.1.2:
+cluster-key-slot@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac"
integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==
@@ -4874,6 +4845,11 @@ delegates@^1.0.0:
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==
+denque@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
+ integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
+
depd@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
@@ -6156,11 +6132,6 @@ gauge@^5.0.0:
strip-ansi "^6.0.1"
wide-align "^1.1.5"
-generic-pool@3.9.0:
- version "3.9.0"
- resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.9.0.tgz#36f4a678e963f4fdb8707eab050823abc4e8f5e4"
- integrity sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==
-
gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
@@ -6840,6 +6811,21 @@ invariant@^2.2.2, invariant@^2.2.4:
dependencies:
loose-envify "^1.0.0"
+ioredis@^5.3.2:
+ version "5.3.2"
+ resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.3.2.tgz#9139f596f62fc9c72d873353ac5395bcf05709f7"
+ integrity sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==
+ dependencies:
+ "@ioredis/commands" "^1.1.1"
+ cluster-key-slot "^1.1.0"
+ debug "^4.3.4"
+ denque "^2.1.0"
+ lodash.defaults "^4.2.0"
+ lodash.isarguments "^3.1.0"
+ redis-errors "^1.2.0"
+ redis-parser "^3.0.0"
+ standard-as-callback "^2.1.0"
+
ip-regex@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
@@ -10335,17 +10321,17 @@ redent@^4.0.0:
indent-string "^5.0.0"
strip-indent "^4.0.0"
-redis@^4.6.5:
- version "4.6.8"
- resolved "https://registry.yarnpkg.com/redis/-/redis-4.6.8.tgz#54c5992e8a5ba512506fe9f53142cadc405547e7"
- integrity sha512-S7qNkPUYrsofQ0ztWlTHSaK0Qqfl1y+WMIxrzeAGNG+9iUZB4HGeBgkHxE6uJJ6iXrkvLd1RVJ2nvu6H1sAzfQ==
+redis-errors@^1.0.0, redis-errors@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
+ integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==
+
+redis-parser@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
+ integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==
dependencies:
- "@redis/bloom" "1.2.0"
- "@redis/client" "1.5.9"
- "@redis/graph" "1.1.0"
- "@redis/json" "1.0.4"
- "@redis/search" "1.1.3"
- "@redis/time-series" "1.0.5"
+ redis-errors "^1.0.0"
redux-immutable@^4.0.0:
version "4.0.0"
@@ -11263,6 +11249,11 @@ stacktrace-js@^2.0.2:
stack-generator "^2.0.5"
stacktrace-gps "^3.0.4"
+standard-as-callback@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
+ integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==
+
static-extend@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
@@ -13023,16 +13014,16 @@ y18n@^5.0.5:
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
-yallist@4.0.0, yallist@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
- integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
-
yallist@^3.0.2:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
+yallist@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+ integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
yaml@^1.10.0:
version "1.10.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"