Merge remote-tracking branch 'parent/main' into kb_migration
This commit is contained in:
commit
32cfd20257
54 changed files with 1045 additions and 138 deletions
18
app/controllers/admin/software_updates_controller.rb
Normal file
18
app/controllers/admin/software_updates_controller.rb
Normal 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
|
|
@ -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
|
||||
|
|
11
app/helpers/authorized_fetch_helper.rb
Normal file
11
app/helpers/authorized_fetch_helper.rb
Normal 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
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
|
@ -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}`}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -103,6 +103,7 @@ code {
|
|||
}
|
||||
}
|
||||
|
||||
.overridden,
|
||||
.recommended,
|
||||
.not_recommended,
|
||||
.kmyblue {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class Admin::SystemCheck
|
||||
ACTIVE_CHECKS = [
|
||||
Admin::SystemCheck::SoftwareVersionCheck,
|
||||
Admin::SystemCheck::MediaPrivacyCheck,
|
||||
Admin::SystemCheck::DatabaseSchemaCheck,
|
||||
Admin::SystemCheck::SidekiqProcessCheck,
|
||||
|
|
27
app/lib/admin/system_check/software_version_check.rb
Normal file
27
app/lib/admin/system_check/software_version_check.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
40
app/models/software_update.rb
Normal file
40
app/models/software_update.rb
Normal 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
|
|
@ -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
|
||||
|
|
7
app/policies/software_update_policy.rb
Normal file
7
app/policies/software_update_policy.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SoftwareUpdatePolicy < ApplicationPolicy
|
||||
def index?
|
||||
role.can?(:view_devops)
|
||||
end
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
82
app/services/software_update_check_service.rb
Normal file
82
app/services/software_update_check_service.rb
Normal 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
|
|
@ -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
|
||||
|
|
29
app/views/admin/software_updates/index.html.haml
Normal file
29
app/views/admin/software_updates/index.html.haml
Normal 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
|
|
@ -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 %>
|
5
app/views/admin_mailer/new_software_updates.text.erb
Normal file
5
app/views/admin_mailer/new_software_updates.text.erb
Normal 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 %>
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
11
app/workers/scheduler/software_update_check_scheduler.rb
Normal file
11
app/workers/scheduler/software_update_check_scheduler.rb
Normal 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
|
|
@ -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.*'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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: "*"
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -212,4 +212,6 @@ namespace :admin do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
resources :software_updates, only: [:index]
|
||||
end
|
||||
|
|
|
@ -66,3 +66,7 @@
|
|||
cron: '0 0 * * *'
|
||||
class: Scheduler::UpdateInstanceInfoScheduler
|
||||
queue: scheduler
|
||||
software_update_check_scheduler:
|
||||
interval: 30 minutes
|
||||
class: Scheduler::SoftwareUpdateCheckScheduler
|
||||
queue: scheduler
|
||||
|
|
16
db/migrate/20230822081029_create_software_updates.rb
Normal file
16
db/migrate/20230822081029_create_software_updates.rb
Normal 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
|
10
db/schema.rb
10
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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
7
spec/fabricators/software_update_fabricator.rb
Normal file
7
spec/fabricators/software_update_fabricator.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Fabricator(:software_update) do
|
||||
version '99.99.99'
|
||||
urgent false
|
||||
type 'patch'
|
||||
end
|
23
spec/features/admin/software_updates_spec.rb
Normal file
23
spec/features/admin/software_updates_spec.rb
Normal 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
|
133
spec/lib/admin/system_check/software_version_check_spec.rb
Normal file
133
spec/lib/admin/system_check/software_version_check_spec.rb
Normal 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
|
|
@ -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
|
||||
|
|
87
spec/models/software_update_spec.rb
Normal file
87
spec/models/software_update_spec.rb
Normal 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
|
25
spec/policies/software_update_policy_spec.rb
Normal file
25
spec/policies/software_update_policy_spec.rb
Normal 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
|
158
spec/services/software_update_check_service_spec.rb
Normal file
158
spec/services/software_update_check_service_spec.rb
Normal 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
|
|
@ -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
|
|
@ -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
101
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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue