Add admin notifications for new Mastodon versions (#26582)
This commit is contained in:
		
							
								
								
									
										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
 | 
				
			||||||
@@ -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 { IconWithBadge } from 'mastodon/components/icon_with_badge';
 | 
				
			||||||
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
 | 
					import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
 | 
				
			||||||
import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container';
 | 
					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 { addColumn, removeColumn, moveColumn } from '../../actions/columns';
 | 
				
			||||||
import { expandHomeTimeline } from '../../actions/timelines';
 | 
					import { expandHomeTimeline } from '../../actions/timelines';
 | 
				
			||||||
@@ -23,6 +23,7 @@ import ColumnHeader from '../../components/column_header';
 | 
				
			|||||||
import StatusListContainer from '../ui/containers/status_list_container';
 | 
					import StatusListContainer from '../ui/containers/status_list_container';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { ColumnSettings } from './components/column_settings';
 | 
					import { ColumnSettings } from './components/column_settings';
 | 
				
			||||||
 | 
					import { CriticalUpdateBanner } from './components/critical_update_banner';
 | 
				
			||||||
import { ExplorePrompt } from './components/explore_prompt';
 | 
					import { ExplorePrompt } from './components/explore_prompt';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const messages = defineMessages({
 | 
					const messages = defineMessages({
 | 
				
			||||||
@@ -156,8 +157,9 @@ class HomeTimeline extends PureComponent {
 | 
				
			|||||||
    const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
 | 
					    const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
 | 
				
			||||||
    const pinned = !!columnId;
 | 
					    const pinned = !!columnId;
 | 
				
			||||||
    const { signedIn } = this.context.identity;
 | 
					    const { signedIn } = this.context.identity;
 | 
				
			||||||
 | 
					    const banners = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let announcementsButton, banner;
 | 
					    let announcementsButton;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (hasAnnouncements) {
 | 
					    if (hasAnnouncements) {
 | 
				
			||||||
      announcementsButton = (
 | 
					      announcementsButton = (
 | 
				
			||||||
@@ -173,8 +175,12 @@ class HomeTimeline extends PureComponent {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (criticalUpdatesPending) {
 | 
				
			||||||
 | 
					      banners.push(<CriticalUpdateBanner key='critical-update-banner' />);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (tooSlow) {
 | 
					    if (tooSlow) {
 | 
				
			||||||
      banner = <ExplorePrompt />;
 | 
					      banners.push(<ExplorePrompt key='explore-prompt' />);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
@@ -196,7 +202,7 @@ class HomeTimeline extends PureComponent {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        {signedIn ? (
 | 
					        {signedIn ? (
 | 
				
			||||||
          <StatusListContainer
 | 
					          <StatusListContainer
 | 
				
			||||||
            prepend={banner}
 | 
					            prepend={banners}
 | 
				
			||||||
            alwaysPrepend
 | 
					            alwaysPrepend
 | 
				
			||||||
            trackScroll={!pinned}
 | 
					            trackScroll={!pinned}
 | 
				
			||||||
            scrollKey={`home_timeline-${columnId}`}
 | 
					            scrollKey={`home_timeline-${columnId}`}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -87,6 +87,7 @@
 | 
				
			|||||||
 * @typedef InitialState
 | 
					 * @typedef InitialState
 | 
				
			||||||
 * @property {Record<string, Account>} accounts
 | 
					 * @property {Record<string, Account>} accounts
 | 
				
			||||||
 * @property {InitialStateLanguage[]} languages
 | 
					 * @property {InitialStateLanguage[]} languages
 | 
				
			||||||
 | 
					 * @property {boolean=} critical_updates_pending
 | 
				
			||||||
 * @property {InitialStateMeta} meta
 | 
					 * @property {InitialStateMeta} meta
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -140,6 +141,7 @@ export const useBlurhash = getMeta('use_blurhash');
 | 
				
			|||||||
export const usePendingItems = getMeta('use_pending_items');
 | 
					export const usePendingItems = getMeta('use_pending_items');
 | 
				
			||||||
export const version = getMeta('version');
 | 
					export const version = getMeta('version');
 | 
				
			||||||
export const languages = initialState?.languages;
 | 
					export const languages = initialState?.languages;
 | 
				
			||||||
 | 
					export const criticalUpdatesPending = initialState?.critical_updates_pending;
 | 
				
			||||||
// @ts-expect-error
 | 
					// @ts-expect-error
 | 
				
			||||||
export const statusPageUrl = getMeta('status_page_url');
 | 
					export const statusPageUrl = getMeta('status_page_url');
 | 
				
			||||||
export const sso_redirect = getMeta('sso_redirect');
 | 
					export const sso_redirect = getMeta('sso_redirect');
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -310,6 +310,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.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.explore_prompt.title": "This is your home base within Mastodon.",
 | 
				
			||||||
  "home.hide_announcements": "Hide announcements",
 | 
					  "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",
 | 
					  "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.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.",
 | 
					  "interaction_modal.description.follow": "With an account on Mastodon, you can follow {name} to receive their posts in your home feed.",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -143,6 +143,11 @@ $content-width: 840px;
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .warning a {
 | 
				
			||||||
 | 
					        color: $gold-star;
 | 
				
			||||||
 | 
					        font-weight: 700;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      .simple-navigation-active-leaf a {
 | 
					      .simple-navigation-active-leaf a {
 | 
				
			||||||
        color: $primary-text-color;
 | 
					        color: $primary-text-color;
 | 
				
			||||||
        background-color: $ui-highlight-color;
 | 
					        background-color: $ui-highlight-color;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8860,7 +8860,8 @@ noscript {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.dismissable-banner {
 | 
					.dismissable-banner,
 | 
				
			||||||
 | 
					.warning-banner {
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
  margin: 10px;
 | 
					  margin: 10px;
 | 
				
			||||||
  margin-bottom: 5px;
 | 
					  margin-bottom: 5px;
 | 
				
			||||||
@@ -8938,6 +8939,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 {
 | 
					.image {
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
  overflow: hidden;
 | 
					  overflow: hidden;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,11 @@
 | 
				
			|||||||
    border-top: 1px solid $ui-base-color;
 | 
					    border-top: 1px solid $ui-base-color;
 | 
				
			||||||
    text-align: start;
 | 
					    text-align: start;
 | 
				
			||||||
    background: darken($ui-base-color, 4%);
 | 
					    background: darken($ui-base-color, 4%);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &.critical {
 | 
				
			||||||
 | 
					      font-weight: 700;
 | 
				
			||||||
 | 
					      color: $gold-star;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  & > thead > tr > th {
 | 
					  & > thead > tr > th {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class Admin::SystemCheck
 | 
					class Admin::SystemCheck
 | 
				
			||||||
  ACTIVE_CHECKS = [
 | 
					  ACTIVE_CHECKS = [
 | 
				
			||||||
 | 
					    Admin::SystemCheck::SoftwareVersionCheck,
 | 
				
			||||||
    Admin::SystemCheck::MediaPrivacyCheck,
 | 
					    Admin::SystemCheck::MediaPrivacyCheck,
 | 
				
			||||||
    Admin::SystemCheck::DatabaseSchemaCheck,
 | 
					    Admin::SystemCheck::DatabaseSchemaCheck,
 | 
				
			||||||
    Admin::SystemCheck::SidekiqProcessCheck,
 | 
					    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
 | 
				
			||||||
  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
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def process_params
 | 
					  def process_params
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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
 | 
				
			||||||
@@ -44,6 +44,7 @@ class UserSettings
 | 
				
			|||||||
    setting :pending_account, default: true
 | 
					    setting :pending_account, default: true
 | 
				
			||||||
    setting :trends, default: true
 | 
					    setting :trends, default: true
 | 
				
			||||||
    setting :appeal, default: true
 | 
					    setting :appeal, default: true
 | 
				
			||||||
 | 
					    setting :software_updates, default: 'critical', in: %w(none critical patch all)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  namespace :interactions do
 | 
					  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
 | 
					class InitialStatePresenter < ActiveModelSerializers::Model
 | 
				
			||||||
  attributes :settings, :push_subscription, :token,
 | 
					  attributes :settings, :push_subscription, :token,
 | 
				
			||||||
             :current_account, :admin, :owner, :text, :visibility,
 | 
					             :current_account, :admin, :owner, :text, :visibility,
 | 
				
			||||||
             :disabled_account, :moved_to_account
 | 
					             :disabled_account, :moved_to_account, :critical_updates_pending
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def role
 | 
					  def role
 | 
				
			||||||
    current_account&.user_role
 | 
					    current_account&.user_role
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def critical_updates_pending
 | 
				
			||||||
 | 
					    role&.can?(:view_devops) && SoftwareUpdate.urgent_pending?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,8 @@ class InitialStateSerializer < ActiveModel::Serializer
 | 
				
			|||||||
             :media_attachments, :settings,
 | 
					             :media_attachments, :settings,
 | 
				
			||||||
             :languages
 | 
					             :languages
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attribute :critical_updates_pending, if: -> { object&.role&.can?(:view_devops) && SoftwareUpdate.check_enabled? }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer
 | 
					  has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer
 | 
				
			||||||
  has_one :role, serializer: REST::RoleSerializer
 | 
					  has_one :role, serializer: REST::RoleSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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
 | 
				
			||||||
							
								
								
									
										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
 | 
					    .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')
 | 
					      = 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'
 | 
					      %h4= t 'notifications.administration_emails'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      .fields-group
 | 
					      .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.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)
 | 
					        = 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'
 | 
					  %h4= t 'notifications.other_settings'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .fields-group
 | 
					  .fields-group
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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
 | 
				
			||||||
@@ -309,6 +309,7 @@ en:
 | 
				
			|||||||
      unpublish: Unpublish
 | 
					      unpublish: Unpublish
 | 
				
			||||||
      unpublished_msg: Announcement successfully unpublished!
 | 
					      unpublished_msg: Announcement successfully unpublished!
 | 
				
			||||||
      updated_msg: Announcement successfully updated!
 | 
					      updated_msg: Announcement successfully updated!
 | 
				
			||||||
 | 
					    critical_update_pending: Critical update pending
 | 
				
			||||||
    custom_emojis:
 | 
					    custom_emojis:
 | 
				
			||||||
      assign_category: Assign category
 | 
					      assign_category: Assign category
 | 
				
			||||||
      by_domain: Domain
 | 
					      by_domain: Domain
 | 
				
			||||||
@@ -779,6 +780,18 @@ en:
 | 
				
			|||||||
    site_uploads:
 | 
					    site_uploads:
 | 
				
			||||||
      delete: Delete uploaded file
 | 
					      delete: Delete uploaded file
 | 
				
			||||||
      destroyed_msg: Site upload successfully deleted!
 | 
					      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:
 | 
					    statuses:
 | 
				
			||||||
      account: Author
 | 
					      account: Author
 | 
				
			||||||
      application: Application
 | 
					      application: Application
 | 
				
			||||||
@@ -843,6 +856,12 @@ en:
 | 
				
			|||||||
        message_html: You haven't defined any server rules.
 | 
					        message_html: You haven't defined any server rules.
 | 
				
			||||||
      sidekiq_process_check:
 | 
					      sidekiq_process_check:
 | 
				
			||||||
        message_html: No Sidekiq process running for the %{value} queue(s). Please review your Sidekiq configuration
 | 
					        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:
 | 
					      upload_check_privacy_error:
 | 
				
			||||||
        action: Check here for more information
 | 
					        action: Check here for more information
 | 
				
			||||||
        message_html: "<strong>Your web server is misconfigured. The privacy of your users is at risk.</strong>"
 | 
					        message_html: "<strong>Your web server is misconfigured. The privacy of your users is at risk.</strong>"
 | 
				
			||||||
@@ -956,6 +975,9 @@ en:
 | 
				
			|||||||
      body: "%{target} is appealing a moderation decision by %{action_taken_by} from %{date}, which was %{type}. They wrote:"
 | 
					      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.
 | 
					      next_steps: You can approve the appeal to undo the moderation decision, or ignore it.
 | 
				
			||||||
      subject: "%{username} is appealing a moderation decision on %{instance}"
 | 
					      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:
 | 
					    new_pending_account:
 | 
				
			||||||
      body: The details of the new account are below. You can approve or reject this application.
 | 
					      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})
 | 
					      subject: New account up for review on %{instance} (%{username})
 | 
				
			||||||
@@ -963,6 +985,9 @@ en:
 | 
				
			|||||||
      body: "%{reporter} has reported %{target}"
 | 
					      body: "%{reporter} has reported %{target}"
 | 
				
			||||||
      body_remote: Someone from %{domain} has reported %{target}
 | 
					      body_remote: Someone from %{domain} has reported %{target}
 | 
				
			||||||
      subject: New report for %{instance} (#%{id})
 | 
					      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:
 | 
					    new_trends:
 | 
				
			||||||
      body: 'The following items need a review before they can be displayed publicly:'
 | 
					      body: 'The following items need a review before they can be displayed publicly:'
 | 
				
			||||||
      new_trending_links:
 | 
					      new_trending_links:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -291,6 +291,12 @@ en:
 | 
				
			|||||||
        pending_account: New account needs review
 | 
					        pending_account: New account needs review
 | 
				
			||||||
        reblog: Someone boosted your post
 | 
					        reblog: Someone boosted your post
 | 
				
			||||||
        report: New report is submitted
 | 
					        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
 | 
					        trending_tag: New trend requires review
 | 
				
			||||||
      rule:
 | 
					      rule:
 | 
				
			||||||
        text: Rule
 | 
					        text: Rule
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,9 @@
 | 
				
			|||||||
SimpleNavigation::Configuration.run do |navigation|
 | 
					SimpleNavigation::Configuration.run do |navigation|
 | 
				
			||||||
  navigation.items do |n|
 | 
					  navigation.items do |n|
 | 
				
			||||||
    n.item :web, safe_join([fa_icon('chevron-left fw'), t('settings.back')]), root_path
 | 
					    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 :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|
 | 
					    n.item :preferences, safe_join([fa_icon('cog fw'), t('settings.preferences')]), settings_preferences_path, if: -> { current_user.functional? } do |s|
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -201,4 +201,6 @@ namespace :admin do
 | 
				
			|||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  resources :software_updates, only: [:index]
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -58,3 +58,7 @@
 | 
				
			|||||||
      interval: 1 minute
 | 
					      interval: 1 minute
 | 
				
			||||||
      class: Scheduler::SuspendedUserCleanupScheduler
 | 
					      class: Scheduler::SuspendedUserCleanupScheduler
 | 
				
			||||||
      queue: scheduler
 | 
					      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
 | 
				
			||||||
							
								
								
									
										12
									
								
								db/schema.rb
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								db/schema.rb
									
									
									
									
									
								
							@@ -10,7 +10,7 @@
 | 
				
			|||||||
#
 | 
					#
 | 
				
			||||||
# It's strongly recommended that you check this file into your version control system.
 | 
					# It's strongly recommended that you check this file into your version control system.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ActiveRecord::Schema[7.0].define(version: 2023_08_18_142253) do
 | 
					ActiveRecord::Schema[7.0].define(version: 2023_08_22_081029) do
 | 
				
			||||||
  # These are extensions that must be enabled in order to support this database
 | 
					  # These are extensions that must be enabled in order to support this database
 | 
				
			||||||
  enable_extension "plpgsql"
 | 
					  enable_extension "plpgsql"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -903,6 +903,16 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_18_142253) do
 | 
				
			|||||||
    t.index ["var"], name: "index_site_uploads_on_var", unique: true
 | 
					    t.index ["var"], name: "index_site_uploads_on_var", unique: true
 | 
				
			||||||
  end
 | 
					  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_edits", force: :cascade do |t|
 | 
					  create_table "status_edits", force: :cascade do |t|
 | 
				
			||||||
    t.bigint "status_id", null: false
 | 
					    t.bigint "status_id", null: false
 | 
				
			||||||
    t.bigint "account_id"
 | 
					    t.bigint "account_id"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,6 +39,10 @@ module Mastodon
 | 
				
			|||||||
      components.join
 | 
					      components.join
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def gem_version
 | 
				
			||||||
 | 
					      @gem_version ||= Gem::Version.new(to_s.split('+')[0])
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def repository
 | 
					    def repository
 | 
				
			||||||
      ENV.fetch('GITHUB_REPOSITORY', 'mastodon/mastodon')
 | 
					      ENV.fetch('GITHUB_REPOSITORY', 'mastodon/mastodon')
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -424,6 +424,10 @@ namespace :mastodon do
 | 
				
			|||||||
        end
 | 
					        end
 | 
				
			||||||
      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 "\n"
 | 
				
			||||||
      prompt.say 'This configuration will be written to .env.production'
 | 
					      prompt.say 'This configuration will be written to .env.production'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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'
 | 
					      expect(mail.body.encoded).to match 'The following items need a review before they can be displayed publicly'
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  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
 | 
					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
 | 
				
			||||||
		Reference in New Issue
	
	Block a user