Merge remote-tracking branch 'parent/main' into kb_migration

This commit is contained in:
KMY 2023-09-03 10:55:46 +09:00
commit 32cfd20257
54 changed files with 1045 additions and 138 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 {
</>
)}
<h4><FormattedMessage id='search_popout.options' defaultMessage='Search options' /></h4>
{searchEnabled && (
<>
<h4><FormattedMessage id='search_popout.options' defaultMessage='Search options' /></h4>
<div className='search__popout__menu'>
{this.defaultOptions.map(({ key, label, action }, i) => (
<button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === (options.length + i) })}>
{label}
</button>
))}
</div>
<div className='search__popout__menu'>
{this.defaultOptions.map(({ key, label, action }, i) => (
<button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === (options.length + i) })}>
{label}
</button>
))}
</div>
</>
)}
</div>
</div>
);

View file

@ -0,0 +1,26 @@
import { FormattedMessage } from 'react-intl';
export const CriticalUpdateBanner = () => (
<div className='warning-banner'>
<div className='warning-banner__message'>
<h1>
<FormattedMessage
id='home.pending_critical_update.title'
defaultMessage='Critical security update available!'
/>
</h1>
<p>
<FormattedMessage
id='home.pending_critical_update.body'
defaultMessage='Please update your Mastodon server as soon as possible!'
/>{' '}
<a href='/admin/software_updates'>
<FormattedMessage
id='home.pending_critical_update.link'
defaultMessage='See updates'
/>
</a>
</p>
</div>
</div>
);

View file

@ -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(<CriticalUpdateBanner key='critical-update-banner' />);
}
if (tooSlow) {
banner = <ExplorePrompt />;
banners.push(<ExplorePrompt key='explore-prompt' />);
}
return (
@ -196,7 +202,7 @@ class HomeTimeline extends PureComponent {
{signedIn ? (
<StatusListContainer
prepend={banner}
prepend={banners}
alwaysPrepend
trackScroll={!pinned}
scrollKey={`home_timeline-${columnId}`}

View file

@ -91,6 +91,7 @@
* @typedef InitialState
* @property {Record<string, Account>} 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');

View file

@ -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.",

View file

@ -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);

View file

@ -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;

View file

@ -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;

View file

@ -103,6 +103,7 @@ code {
}
}
.overridden,
.recommended,
.not_recommended,
.kmyblue {

View file

@ -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 {

View file

@ -2,6 +2,7 @@
class Admin::SystemCheck
ACTIVE_CHECKS = [
Admin::SystemCheck::SoftwareVersionCheck,
Admin::SystemCheck::MediaPrivacyCheck,
Admin::SystemCheck::DatabaseSchemaCheck,
Admin::SystemCheck::SidekiqProcessCheck,

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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',

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class SoftwareUpdatePolicy < ApplicationPolicy
def index?
role.can?(:view_devops)
end
end

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 %>

View file

@ -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 %>

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.*'

View file

@ -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

View file

@ -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: "<strong>Your web server is misconfigured. The privacy of your users is at risk.</strong>"
@ -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:

View file

@ -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: "*"

View file

@ -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|

View file

@ -212,4 +212,6 @@ namespace :admin do
end
end
end
resources :software_updates, only: [:index]
end

View file

@ -66,3 +66,7 @@
cron: '0 0 * * *'
class: Scheduler::UpdateInstanceInfoScheduler
queue: scheduler
software_update_check_scheduler:
interval: 30 minutes
class: Scheduler::SoftwareUpdateCheckScheduler
queue: scheduler

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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'

View file

@ -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",

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
Fabricator(:software_update) do
version '99.99.99'
urgent false
type 'patch'
end

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.<string, any>} defaultConfig
* @param {string} redisUrl
* @param {Object.<string, any>} 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.<string, any>} 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.<string, Array.<function(Object<string, any>): 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];
}
};

101
yarn.lock
View file

@ -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"