Merge commit '379115e601361c2b5da775fbf28b7dff9dc02e71' into glitch-soc/merge-upstream

Conflicts:
- `config/navigation.rb`:
  Conflict due to glitch-soc having extra navigation items for its theming
  system.
  Ported upstream changes.
master
Claire 2023-10-24 19:31:14 +02:00
commit e25cc4deb7
25 changed files with 382 additions and 166 deletions

View File

@ -80,7 +80,6 @@ RSpec/AnyInstance:
- 'spec/controllers/admin/accounts_controller_spec.rb' - 'spec/controllers/admin/accounts_controller_spec.rb'
- 'spec/controllers/admin/resets_controller_spec.rb' - 'spec/controllers/admin/resets_controller_spec.rb'
- 'spec/controllers/admin/settings/branding_controller_spec.rb' - 'spec/controllers/admin/settings/branding_controller_spec.rb'
- 'spec/controllers/api/v1/media_controller_spec.rb'
- 'spec/controllers/auth/sessions_controller_spec.rb' - 'spec/controllers/auth/sessions_controller_spec.rb'
- 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb' - 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb'
- 'spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb' - 'spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb'
@ -180,7 +179,6 @@ RSpec/LetSetup:
RSpec/MessageChain: RSpec/MessageChain:
Exclude: Exclude:
- 'spec/controllers/api/v1/media_controller_spec.rb'
- 'spec/models/concerns/remotable_spec.rb' - 'spec/models/concerns/remotable_spec.rb'
- 'spec/models/session_activation_spec.rb' - 'spec/models/session_activation_spec.rb'
- 'spec/models/setting_spec.rb' - 'spec/models/setting_spec.rb'

View File

@ -13,6 +13,7 @@ class ApplicationController < ActionController::Base
include ThemingConcern include ThemingConcern
include DatabaseHelper include DatabaseHelper
include AuthorizedFetchHelper include AuthorizedFetchHelper
include SelfDestructHelper
helper_method :current_account helper_method :current_account
helper_method :current_session helper_method :current_session
@ -41,6 +42,8 @@ class ApplicationController < ActionController::Base
service_unavailable service_unavailable
end end
before_action :check_self_destruct!
before_action :store_referrer, except: :raise_not_found, if: :devise_controller? before_action :store_referrer, except: :raise_not_found, if: :devise_controller?
before_action :require_functional!, if: :user_signed_in? before_action :require_functional!, if: :user_signed_in?
@ -169,6 +172,15 @@ class ApplicationController < ActionController::Base
end end
end end
def check_self_destruct!
return unless self_destruct?
respond_to do |format|
format.any { render 'errors/self_destruct', layout: 'auth', status: 410, formats: [:html] }
format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[410] }, status: code }
end
end
def set_cache_control_defaults def set_cache_control_defaults
response.cache_control.replace(private: true, no_store: true) response.cache_control.replace(private: true, no_store: true)
end end

View File

@ -8,6 +8,7 @@ class Auth::ChallengesController < ApplicationController
before_action :set_pack before_action :set_pack
before_action :authenticate_user! before_action :authenticate_user!
skip_before_action :check_self_destruct!
skip_before_action :require_functional! skip_before_action :require_functional!
def create def create

View File

@ -13,6 +13,7 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
before_action :extend_csp_for_captcha!, only: [:show, :confirm_captcha] before_action :extend_csp_for_captcha!, only: [:show, :confirm_captcha]
before_action :require_captcha_if_needed!, only: [:show] before_action :require_captcha_if_needed!, only: [:show]
skip_before_action :check_self_destruct!
skip_before_action :require_functional! skip_before_action :require_functional!
def show def show

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
skip_before_action :check_self_destruct!
skip_before_action :verify_authenticity_token skip_before_action :verify_authenticity_token
def self.provides_callback_for(provider) def self.provides_callback_for(provider)

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Auth::PasswordsController < Devise::PasswordsController class Auth::PasswordsController < Devise::PasswordsController
skip_before_action :check_self_destruct!
before_action :check_validity_of_reset_password_token, only: :edit before_action :check_validity_of_reset_password_token, only: :edit
before_action :set_pack before_action :set_pack
before_action :set_body_classes before_action :set_body_classes

View File

@ -18,6 +18,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :require_rules_acceptance!, only: :new before_action :require_rules_acceptance!, only: :new
before_action :set_registration_form_time, only: :new before_action :set_registration_form_time, only: :new
skip_before_action :check_self_destruct!, only: [:edit, :update]
skip_before_action :require_functional!, only: [:edit, :update] skip_before_action :require_functional!, only: [:edit, :update]
def new def new

View File

@ -3,6 +3,7 @@
class Auth::SessionsController < Devise::SessionsController class Auth::SessionsController < Devise::SessionsController
layout 'auth' layout 'auth'
skip_before_action :check_self_destruct!
skip_before_action :require_no_authentication, only: [:create] skip_before_action :require_no_authentication, only: [:create]
skip_before_action :require_functional! skip_before_action :require_functional!
skip_before_action :update_user_sign_in skip_before_action :update_user_sign_in

View File

@ -3,6 +3,7 @@
class BackupsController < ApplicationController class BackupsController < ApplicationController
include RoutingHelper include RoutingHelper
skip_before_action :check_self_destruct!
skip_before_action :require_functional! skip_before_action :require_functional!
before_action :authenticate_user! before_action :authenticate_user!

View File

@ -7,6 +7,7 @@ module ExportControllerConcern
before_action :authenticate_user! before_action :authenticate_user!
before_action :load_export before_action :load_export
skip_before_action :check_self_destruct!
skip_before_action :require_functional! skip_before_action :require_functional!
end end

View File

@ -5,6 +5,7 @@ class Settings::ExportsController < Settings::BaseController
include Redisable include Redisable
include Lockable include Lockable
skip_before_action :check_self_destruct!
skip_before_action :require_functional! skip_before_action :require_functional!
def show def show

View File

@ -1,6 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class Settings::LoginActivitiesController < Settings::BaseController class Settings::LoginActivitiesController < Settings::BaseController
skip_before_action :check_self_destruct!
skip_before_action :require_functional!
def index def index
@login_activities = LoginActivity.where(user: current_user).order(id: :desc).page(params[:page]) @login_activities = LoginActivity.where(user: current_user).order(id: :desc).page(params[:page])
end end

View File

@ -3,6 +3,7 @@
module Settings module Settings
module TwoFactorAuthentication module TwoFactorAuthentication
class WebauthnCredentialsController < BaseController class WebauthnCredentialsController < BaseController
skip_before_action :check_self_destruct!
skip_before_action :require_functional! skip_before_action :require_functional!
before_action :require_otp_enabled before_action :require_otp_enabled

View File

@ -4,6 +4,7 @@ module Settings
class TwoFactorAuthenticationMethodsController < BaseController class TwoFactorAuthenticationMethodsController < BaseController
include ChallengableConcern include ChallengableConcern
skip_before_action :check_self_destruct!
skip_before_action :require_functional! skip_before_action :require_functional!
before_action :require_challenge!, only: :disable before_action :require_challenge!, only: :disable

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module SelfDestructHelper
def self.self_destruct?
value = ENV.fetch('SELF_DESTRUCT', nil)
value.present? && Rails.application.message_verifier('self-destruct').verify(value) == ENV['LOCAL_DOMAIN']
rescue ActiveSupport::MessageVerifier::InvalidSignature
false
end
def self_destruct?
SelfDestructHelper.self_destruct?
end
end

View File

@ -1,7 +1,11 @@
- content_for :page_title do - content_for :page_title do
= t('settings.account_settings') = t('settings.account_settings')
= render partial: 'status', locals: { user: @user, strikes: @strikes } - if self_destruct?
.flash-message.warning
= t('auth.status.self_destruct', domain: ENV['LOCAL_DOMAIN'])
- else
= render partial: 'status', locals: { user: @user, strikes: @strikes }
%h3= t('auth.security') %h3= t('auth.security')
@ -32,7 +36,7 @@
= render partial: 'sessions', object: @sessions = render partial: 'sessions', object: @sessions
- unless current_account.suspended? - unless current_account.suspended? || self_destruct?
%hr.spacer/ %hr.spacer/
%h3= t('auth.migrate_account') %h3= t('auth.migrate_account')

View File

@ -0,0 +1,20 @@
- content_for :page_title do
= t('self_destruct.title')
.simple_form
%h1.title= t('self_destruct.title')
%p.lead= t('self_destruct.lead_html', domain: ENV['LOCAL_DOMAIN'])
.form-footer
%ul.no-list
- if user_signed_in?
%li= link_to t('settings.account_settings'), edit_user_registration_path
- else
- if controller_name != 'sessions'
%li= link_to_login t('auth.login')
- if controller_name != 'passwords' && controller_name != 'registrations'
%li= link_to t('auth.forgot_password'), new_user_password_path
- if user_signed_in?
%li= link_to t('auth.logout'), destroy_user_session_path, data: { method: :delete }

View File

@ -0,0 +1,72 @@
# frozen_string_literal: true
class Scheduler::SelfDestructScheduler
include Sidekiq::Worker
include SelfDestructHelper
MAX_ENQUEUED = 10_000
MAX_REDIS_MEM_USAGE = 0.5
MAX_ACCOUNT_DELETIONS_PER_JOB = 50
sidekiq_options retry: 0, lock: :until_executed, lock_ttl: 1.day.to_i
def perform
return unless self_destruct?
return if sidekiq_overwhelmed?
delete_accounts!
end
private
def sidekiq_overwhelmed?
redis_mem_info = Sidekiq.redis_info
Sidekiq::Stats.new.enqueued > MAX_ENQUEUED || redis_mem_info['used_memory'].to_f > redis_mem_info['total_system_memory'].to_f * MAX_REDIS_MEM_USAGE
end
def delete_accounts!
# We currently do not distinguish between deleted accounts and suspended
# accounts, and we do not want to remove the records in this scheduler, as
# we still rely on it for account delivery and don't want to perform
# needless work when the database can be outright dropped after the
# self-destruct.
# Deleted accounts are suspended accounts that do not have a pending
# deletion request.
# This targets accounts that have not been deleted nor marked for deletion yet
Account.local.without_suspended.reorder(id: :asc).take(MAX_ACCOUNT_DELETIONS_PER_JOB).each do |account|
delete_account!(account)
end
return if sidekiq_overwhelmed?
# This targets accounts that have been marked for deletion but have not been
# deleted yet
Account.local.suspended.joins(:deletion_request).take(MAX_ACCOUNT_DELETIONS_PER_JOB).each do |account|
delete_account!(account)
account.deletion_request&.destroy
end
end
def inboxes
@inboxes ||= Account.inboxes
end
def delete_account!(account)
payload = ActiveModelSerializers::SerializableResource.new(
account,
serializer: ActivityPub::DeleteActorSerializer,
adapter: ActivityPub::Adapter
).as_json
json = Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(account))
ActivityPub::DeliveryWorker.push_bulk(inboxes, limit: 1_000) do |inbox_url|
[json, account.id, inbox_url]
end
# Do not call `Account#suspend!` because we don't want to issue a deletion request
account.update!(suspended_at: Time.now.utc, suspension_origin: :local)
end
end

View File

@ -17,6 +17,18 @@ Sidekiq.configure_server do |config|
chain.add SidekiqUniqueJobs::Middleware::Client chain.add SidekiqUniqueJobs::Middleware::Client
end end
config.on(:startup) do
if SelfDestructHelper.self_destruct?
Sidekiq.schedule = {
'self_destruct_scheduler' => {
'interval' => ['1m'],
'class' => 'Scheduler::SelfDestructScheduler',
'queue' => 'scheduler',
},
}
end
end
SidekiqUniqueJobs::Server.configure(config) SidekiqUniqueJobs::Server.configure(config)
end end

View File

@ -1102,6 +1102,7 @@ en:
functional: Your account is fully operational. functional: Your account is fully operational.
pending: Your application is pending review by our staff. This may take some time. You will receive an e-mail if your application is approved. pending: Your application is pending review by our staff. This may take some time. You will receive an e-mail if your application is approved.
redirecting_to: Your account is inactive because it is currently redirecting to %{acct}. redirecting_to: Your account is inactive because it is currently redirecting to %{acct}.
self_destruct: As %{domain} is closing down, you will only get limited access to your account.
view_strikes: View past strikes against your account view_strikes: View past strikes against your account
too_fast: Form submitted too fast, try again. too_fast: Form submitted too fast, try again.
use_security_key: Use security key use_security_key: Use security key
@ -1572,6 +1573,9 @@ en:
over_daily_limit: You have exceeded the limit of %{limit} scheduled posts for today over_daily_limit: You have exceeded the limit of %{limit} scheduled posts for today
over_total_limit: You have exceeded the limit of %{limit} scheduled posts over_total_limit: You have exceeded the limit of %{limit} scheduled posts
too_soon: The scheduled date must be in the future too_soon: The scheduled date must be in the future
self_destruct:
lead_html: Unfortunately, <strong>%{domain}</strong> is permanently closing down. If you had an account there, you will not be able to continue using it, but you can still request a backup of your data.
title: This server is closing down
sessions: sessions:
activity: Last activity activity: Last activity
browser: Browser browser: Browser

View File

@ -1,14 +1,16 @@
# frozen_string_literal: true # frozen_string_literal: true
SimpleNavigation::Configuration.run do |navigation| SimpleNavigation::Configuration.run do |navigation|
self_destruct = SelfDestructHelper.self_destruct?
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 :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? && !self_destruct }, 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? && !self_destruct } do |s|
s.item :appearance, safe_join([fa_icon('desktop fw'), t('settings.appearance')]), settings_preferences_appearance_path s.item :appearance, safe_join([fa_icon('desktop fw'), t('settings.appearance')]), settings_preferences_appearance_path
s.item :notifications, safe_join([fa_icon('bell fw'), t('settings.notifications')]), settings_preferences_notifications_path s.item :notifications, safe_join([fa_icon('bell fw'), t('settings.notifications')]), settings_preferences_notifications_path
s.item :other, safe_join([fa_icon('cog fw'), t('preferences.other')]), settings_preferences_other_path s.item :other, safe_join([fa_icon('cog fw'), t('preferences.other')]), settings_preferences_other_path
@ -20,31 +22,31 @@ SimpleNavigation::Configuration.run do |navigation|
end end
end end
n.item :relationships, safe_join([fa_icon('users fw'), t('settings.relationships')]), relationships_path, if: -> { current_user.functional? } n.item :relationships, safe_join([fa_icon('users fw'), t('settings.relationships')]), relationships_path, if: -> { current_user.functional? && !self_destruct }
n.item :filters, safe_join([fa_icon('filter fw'), t('filters.index.title')]), filters_path, highlights_on: %r{/filters}, if: -> { current_user.functional? } n.item :filters, safe_join([fa_icon('filter fw'), t('filters.index.title')]), filters_path, highlights_on: %r{/filters}, if: -> { current_user.functional? && !self_destruct }
n.item :statuses_cleanup, safe_join([fa_icon('history fw'), t('settings.statuses_cleanup')]), statuses_cleanup_path, if: -> { current_user.functional_or_moved? } n.item :statuses_cleanup, safe_join([fa_icon('history fw'), t('settings.statuses_cleanup')]), statuses_cleanup_path, if: -> { current_user.functional_or_moved? && !self_destruct }
n.item :security, safe_join([fa_icon('lock fw'), t('settings.account')]), edit_user_registration_path do |s| n.item :security, safe_join([fa_icon('lock fw'), t('settings.account')]), edit_user_registration_path do |s|
s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_path, highlights_on: %r{/auth/edit|/settings/delete|/settings/migration|/settings/aliases|/settings/login_activities|^/disputes} s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_path, highlights_on: %r{/auth/edit|/settings/delete|/settings/migration|/settings/aliases|/settings/login_activities|^/disputes}
s.item :two_factor_authentication, safe_join([fa_icon('mobile fw'), t('settings.two_factor_authentication')]), settings_two_factor_authentication_methods_path, highlights_on: %r{/settings/two_factor_authentication|/settings/otp_authentication|/settings/security_keys} s.item :two_factor_authentication, safe_join([fa_icon('mobile fw'), t('settings.two_factor_authentication')]), settings_two_factor_authentication_methods_path, highlights_on: %r{/settings/two_factor_authentication|/settings/otp_authentication|/settings/security_keys}
s.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_path s.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_path, if: -> { !self_destruct }
end end
n.item :data, safe_join([fa_icon('cloud-download fw'), t('settings.import_and_export')]), settings_export_path do |s| n.item :data, safe_join([fa_icon('cloud-download fw'), t('settings.import_and_export')]), settings_export_path do |s|
s.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_imports_path, if: -> { current_user.functional? } s.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_imports_path, if: -> { current_user.functional? && !self_destruct }
s.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_path s.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_path
end end
n.item :invites, safe_join([fa_icon('user-plus fw'), t('invites.title')]), invites_path, if: -> { current_user.can?(:invite_users) && current_user.functional? } n.item :invites, safe_join([fa_icon('user-plus fw'), t('invites.title')]), invites_path, if: -> { current_user.can?(:invite_users) && current_user.functional? && !self_destruct }
n.item :development, safe_join([fa_icon('code fw'), t('settings.development')]), settings_applications_path, if: -> { current_user.functional? } n.item :development, safe_join([fa_icon('code fw'), t('settings.development')]), settings_applications_path, if: -> { current_user.functional? && !self_destruct }
n.item :trends, safe_join([fa_icon('fire fw'), t('admin.trends.title')]), admin_trends_statuses_path, if: -> { current_user.can?(:manage_taxonomies) } do |s| n.item :trends, safe_join([fa_icon('fire fw'), t('admin.trends.title')]), admin_trends_statuses_path, if: -> { current_user.can?(:manage_taxonomies) && !self_destruct } do |s|
s.item :statuses, safe_join([fa_icon('comments-o fw'), t('admin.trends.statuses.title')]), admin_trends_statuses_path, highlights_on: %r{/admin/trends/statuses} s.item :statuses, safe_join([fa_icon('comments-o fw'), t('admin.trends.statuses.title')]), admin_trends_statuses_path, highlights_on: %r{/admin/trends/statuses}
s.item :tags, safe_join([fa_icon('hashtag fw'), t('admin.trends.tags.title')]), admin_trends_tags_path, highlights_on: %r{/admin/tags|/admin/trends/tags} s.item :tags, safe_join([fa_icon('hashtag fw'), t('admin.trends.tags.title')]), admin_trends_tags_path, highlights_on: %r{/admin/tags|/admin/trends/tags}
s.item :links, safe_join([fa_icon('newspaper-o fw'), t('admin.trends.links.title')]), admin_trends_links_path, highlights_on: %r{/admin/trends/links} s.item :links, safe_join([fa_icon('newspaper-o fw'), t('admin.trends.links.title')]), admin_trends_links_path, highlights_on: %r{/admin/trends/links}
end end
n.item :moderation, safe_join([fa_icon('gavel fw'), t('moderation.title')]), nil, if: -> { current_user.can?(:manage_reports, :view_audit_log, :manage_users, :manage_invites, :manage_taxonomies, :manage_federation, :manage_blocks) } do |s| n.item :moderation, safe_join([fa_icon('gavel fw'), t('moderation.title')]), nil, if: -> { current_user.can?(:manage_reports, :view_audit_log, :manage_users, :manage_invites, :manage_taxonomies, :manage_federation, :manage_blocks) && !self_destruct } do |s|
s.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_path, highlights_on: %r{/admin/reports}, if: -> { current_user.can?(:manage_reports) } s.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_path, highlights_on: %r{/admin/reports}, if: -> { current_user.can?(:manage_reports) }
s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_path(origin: 'local'), highlights_on: %r{/admin/accounts|/admin/pending_accounts|/admin/disputes|/admin/users}, if: -> { current_user.can?(:manage_users) } s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_path(origin: 'local'), highlights_on: %r{/admin/accounts|/admin/pending_accounts|/admin/disputes|/admin/users}, if: -> { current_user.can?(:manage_users) }
s.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path, if: -> { current_user.can?(:manage_invites) } s.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path, if: -> { current_user.can?(:manage_invites) }
@ -55,7 +57,7 @@ SimpleNavigation::Configuration.run do |navigation|
s.item :action_logs, safe_join([fa_icon('bars fw'), t('admin.action_logs.title')]), admin_action_logs_path, if: -> { current_user.can?(:view_audit_log) } s.item :action_logs, safe_join([fa_icon('bars fw'), t('admin.action_logs.title')]), admin_action_logs_path, if: -> { current_user.can?(:view_audit_log) }
end end
n.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), nil, if: -> { current_user.can?(:view_dashboard, :manage_settings, :manage_rules, :manage_announcements, :manage_custom_emojis, :manage_webhooks, :manage_federation) } do |s| n.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), nil, if: -> { current_user.can?(:view_dashboard, :manage_settings, :manage_rules, :manage_announcements, :manage_custom_emojis, :manage_webhooks, :manage_federation) && !self_destruct } do |s|
s.item :dashboard, safe_join([fa_icon('tachometer fw'), t('admin.dashboard.title')]), admin_dashboard_path, if: -> { current_user.can?(:view_dashboard) } s.item :dashboard, safe_join([fa_icon('tachometer fw'), t('admin.dashboard.title')]), admin_dashboard_path, if: -> { current_user.can?(:view_dashboard) }
s.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), admin_settings_path, if: -> { current_user.can?(:manage_settings) }, highlights_on: %r{/admin/settings} s.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), admin_settings_path, if: -> { current_user.can?(:manage_settings) }, highlights_on: %r{/admin/settings}
s.item :rules, safe_join([fa_icon('gavel fw'), t('admin.rules.title')]), admin_rules_path, highlights_on: %r{/admin/rules}, if: -> { current_user.can?(:manage_rules) } s.item :rules, safe_join([fa_icon('gavel fw'), t('admin.rules.title')]), admin_rules_path, highlights_on: %r{/admin/rules}, if: -> { current_user.can?(:manage_rules) }

View File

@ -7,6 +7,7 @@
- [mailers, 2] - [mailers, 2]
- [pull] - [pull]
- [scheduler] - [scheduler]
:scheduler: :scheduler:
:listened_queues_only: true :listened_queues_only: true
:schedule: :schedule:

View File

@ -65,7 +65,6 @@ module Mastodon::CLI
desc 'maintenance SUBCOMMAND ...ARGS', 'Various maintenance utilities' desc 'maintenance SUBCOMMAND ...ARGS', 'Various maintenance utilities'
subcommand 'maintenance', Maintenance subcommand 'maintenance', Maintenance
option :dry_run, type: :boolean
desc 'self-destruct', 'Erase the server from the federation' desc 'self-destruct', 'Erase the server from the federation'
long_desc <<~LONG_DESC long_desc <<~LONG_DESC
Erase the server from the federation by broadcasting account delete Erase the server from the federation by broadcasting account delete
@ -92,55 +91,37 @@ module Mastodon::CLI
prompt = TTY::Prompt.new prompt = TTY::Prompt.new
exit(1) unless prompt.ask('Type in the domain of the server to confirm:', required: true) == Rails.configuration.x.local_domain if SelfDestructHelper.self_destruct?
prompt.ok('Self-destruct mode is already enabled for this Mastodon server')
unless dry_run? pending_accounts = Account.local.without_suspended.count + Account.local.suspended.joins(:deletion_request).count
prompt.warn('This operation WILL NOT be reversible. It can also take a long time.') sidekiq_stats = Sidekiq::Stats.new
prompt.warn('While the data won\'t be erased locally, the server will be in a BROKEN STATE afterwards.')
prompt.warn('A running Sidekiq process is required. Do not shut it down until queues clear.')
exit(1) if prompt.no?('Are you sure you want to proceed?') if pending_accounts.positive?
end prompt.warn("#{pending_accounts} accounts are still pending deletion.")
elsif sidekiq_stats.enqueued.positive?
inboxes = Account.inboxes prompt.warn('Deletion notices are still being processed')
processed = 0 elsif sidekiq_stats.retry_size.positive?
prompt.warn('At least one delivery attempt for each deletion notice has been made, but some have failed and are scheduled for retry')
Setting.registrations_mode = 'none' unless dry_run? else
prompt.ok('Every deletion notice has been sent! You can safely delete all data and decomission your servers!')
if inboxes.empty?
Account.local.without_suspended.in_batches.update_all(suspended_at: Time.now.utc, suspension_origin: :local) unless dry_run?
prompt.ok('It seems like your server has not federated with anything')
prompt.ok('You can shut it down and delete it any time')
return
end
prompt.warn('Do NOT interrupt this process...')
delete_account = lambda do |account|
payload = ActiveModelSerializers::SerializableResource.new(
account,
serializer: ActivityPub::DeleteActorSerializer,
adapter: ActivityPub::Adapter
).as_json
json = Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(account))
unless dry_run?
ActivityPub::DeliveryWorker.push_bulk(inboxes, limit: 1_000) do |inbox_url|
[json, account.id, inbox_url]
end
account.suspend!(block_email: false)
end end
processed += 1 exit(0)
end end
Account.local.without_suspended.find_each { |account| delete_account.call(account) } exit(1) unless prompt.ask('Type in the domain of the server to confirm:', required: true) == Rails.configuration.x.local_domain
Account.local.suspended.joins(:deletion_request).find_each { |account| delete_account.call(account) }
prompt.ok("Queued #{inboxes.size * processed} items into Sidekiq for #{processed} accounts#{dry_run_mode_suffix}") prompt.warn('This operation WILL NOT be reversible.')
prompt.ok('Wait until Sidekiq processes all items, then you can shut everything down and delete the data') prompt.warn('While the data won\'t be erased locally, the server will be in a BROKEN STATE afterwards.')
prompt.warn('The deletion process itself may take a long time, and will be handled by Sidekiq, so do not shut it down until it has finished (you will be able to re-run this command to see the state of the self-destruct process).')
exit(1) if prompt.no?('Are you sure you want to proceed?')
self_destruct_value = Rails.application.message_verifier('self-destruct').generate(Rails.configuration.x.local_domain)
prompt.ok('To switch Mastodon to self-destruct mode, add the following variable to your evironment (e.g. by adding a line to your `.env.production`) and restart all Mastodon processes:')
prompt.ok(" SELF_DESTRUCT=#{self_destruct_value}")
prompt.ok("\nYou can re-run this command to see the state of the self-destruct process.")
rescue TTY::Reader::InputInterrupt rescue TTY::Reader::InputInterrupt
exit(1) exit(1)
end end

View File

@ -1,107 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Api::V1::MediaController do
render_views
let(:user) { Fabricate(:user) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:media') }
before do
allow(controller).to receive(:doorkeeper_token) { token }
end
describe 'POST #create' do
describe 'with paperclip errors' do
context 'when imagemagick cant identify the file type' do
it 'returns http 422' do
allow_any_instance_of(Account).to receive_message_chain(:media_attachments, :create!).and_raise(Paperclip::Errors::NotIdentifiedByImageMagickError)
post :create, params: { file: fixture_file_upload('attachment.jpg', 'image/jpeg') }
expect(response).to have_http_status(422)
end
end
context 'when there is a generic error' do
it 'returns http 422' do
allow_any_instance_of(Account).to receive_message_chain(:media_attachments, :create!).and_raise(Paperclip::Error)
post :create, params: { file: fixture_file_upload('attachment.jpg', 'image/jpeg') }
expect(response).to have_http_status(500)
end
end
end
context 'with image/jpeg' do
before do
post :create, params: { file: fixture_file_upload('attachment.jpg', 'image/jpeg') }
end
it 'creates a media attachment', :aggregate_failures do
expect(response).to have_http_status(200)
expect(MediaAttachment.first).to_not be_nil
expect(MediaAttachment.first).to have_attached_file(:file)
expect(body_as_json[:id]).to eq MediaAttachment.first.id.to_s
end
end
context 'with image/gif' do
before do
post :create, params: { file: fixture_file_upload('attachment.gif', 'image/gif') }
end
it 'creates a media attachment', :aggregate_failures do
expect(response).to have_http_status(200)
expect(MediaAttachment.first).to_not be_nil
expect(MediaAttachment.first).to have_attached_file(:file)
expect(body_as_json[:id]).to eq MediaAttachment.first.id.to_s
end
end
context 'with video/webm' do
before do
post :create, params: { file: fixture_file_upload('attachment.webm', 'video/webm') }
end
it 'creates a media attachment', :aggregate_failures do
expect(response).to have_http_status(200)
expect(MediaAttachment.first).to_not be_nil
expect(MediaAttachment.first).to have_attached_file(:file)
expect(body_as_json[:id]).to eq MediaAttachment.first.id.to_s
end
end
end
describe 'PUT #update' do
context 'when somebody else\'s' do
let(:media) { Fabricate(:media_attachment, status: nil) }
it 'returns http not found' do
put :update, params: { id: media.id, description: 'Lorem ipsum!!!' }
expect(response).to have_http_status(404)
end
end
context 'when the author \'s' do
let(:status) { nil }
let(:media) { Fabricate(:media_attachment, status: status, account: user.account) }
before do
put :update, params: { id: media.id, description: 'Lorem ipsum!!!' }
end
it 'updates the description' do
expect(media.reload.description).to eq 'Lorem ipsum!!!'
end
context 'when already attached to a status' do
let(:status) { Fabricate(:status, account: user.account) }
it 'returns http not found' do
expect(response).to have_http_status(404)
end
end
end
end
end

View File

@ -0,0 +1,189 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Media' do
let(:user) { Fabricate(:user) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:scopes) { 'write:media' }
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
describe 'GET /api/v1/media/:id' do
subject do
get "/api/v1/media/#{media.id}", headers: headers
end
let(:media) { Fabricate(:media_attachment, account: user.account) }
it_behaves_like 'forbidden for wrong scope', 'read'
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'returns the media information' do
subject
expect(body_as_json).to match(
a_hash_including(
id: media.id.to_s,
description: media.description,
type: media.type
)
)
end
context 'when the media is still being processed' do
before do
media.update(processing: :in_progress)
end
it 'returns http partial content' do
subject
expect(response).to have_http_status(206)
end
end
context 'when the media belongs to somebody else' do
let(:media) { Fabricate(:media_attachment) }
it 'returns http not found' do
subject
expect(response).to have_http_status(404)
end
end
context 'when media is attached to a status' do
let(:media) { Fabricate(:media_attachment, account: user.account, status: Fabricate.build(:status)) }
it 'returns http not found' do
subject
expect(response).to have_http_status(404)
end
end
end
describe 'POST /api/v1/media' do
subject do
post '/api/v1/media', headers: headers, params: params
end
let(:params) { {} }
shared_examples 'a successful media upload' do |media_type|
it 'uploads the file successfully', :aggregate_failures do
subject
expect(response).to have_http_status(200)
expect(MediaAttachment.first).to be_present
expect(MediaAttachment.first).to have_attached_file(:file)
end
it 'returns the correct media content' do
subject
body = body_as_json
expect(body).to match(
a_hash_including(id: MediaAttachment.first.id.to_s, description: params[:description], type: media_type)
)
end
end
it_behaves_like 'forbidden for wrong scope', 'read read:media'
describe 'when paperclip errors occur' do
let(:media_attachments) { double }
let(:params) { { file: fixture_file_upload('attachment.jpg', 'image/jpeg') } }
before do
allow(User).to receive(:find).with(token.resource_owner_id).and_return(user)
allow(user.account).to receive(:media_attachments).and_return(media_attachments)
end
context 'when imagemagick cannot identify the file type' do
it 'returns http unprocessable entity' do
allow(media_attachments).to receive(:create!).and_raise(Paperclip::Errors::NotIdentifiedByImageMagickError)
subject
expect(response).to have_http_status(422)
end
end
context 'when there is a generic error' do
it 'returns http 500' do
allow(media_attachments).to receive(:create!).and_raise(Paperclip::Error)
subject
expect(response).to have_http_status(500)
end
end
end
context 'with image/jpeg', paperclip_processing: true do
let(:params) { { file: fixture_file_upload('attachment.jpg', 'image/jpeg'), description: 'jpeg image' } }
it_behaves_like 'a successful media upload', 'image'
end
context 'with image/gif', paperclip_processing: true do
let(:params) { { file: fixture_file_upload('attachment.gif', 'image/gif') } }
it_behaves_like 'a successful media upload', 'image'
end
context 'with video/webm', paperclip_processing: true do
let(:params) { { file: fixture_file_upload('attachment.webm', 'video/webm') } }
it_behaves_like 'a successful media upload', 'gifv'
end
end
describe 'PUT /api/v1/media/:id' do
subject do
put "/api/v1/media/#{media.id}", headers: headers, params: params
end
let(:params) { {} }
let(:media) { Fabricate(:media_attachment, status: status, account: user.account, description: 'old') }
it_behaves_like 'forbidden for wrong scope', 'read read:media'
context 'when the media belongs to somebody else' do
let(:media) { Fabricate(:media_attachment, status: nil) }
let(:params) { { description: 'Lorem ipsum!!!' } }
it 'returns http not found' do
subject
expect(response).to have_http_status(404)
end
end
context 'when the requesting user owns the media' do
let(:status) { nil }
let(:params) { { description: 'Lorem ipsum!!!' } }
it 'updates the description' do
expect { subject }.to change { media.reload.description }.from('old').to('Lorem ipsum!!!')
end
context 'when the media is attached to a status' do
let(:status) { Fabricate(:status, account: user.account) }
it 'returns http not found' do
subject
expect(response).to have_http_status(404)
end
end
end
end
end