Merge pull request #2392 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes into glitch-soclocal
commit
ee02b10e06
147 changed files with 3207 additions and 930 deletions
@ -0,0 +1,49 @@ |
||||
{ |
||||
"name": "Mastodon on GitHub Codespaces", |
||||
"dockerComposeFile": "../docker-compose.yml", |
||||
"service": "app", |
||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", |
||||
|
||||
"features": { |
||||
"ghcr.io/devcontainers/features/sshd:1": {} |
||||
}, |
||||
|
||||
"runServices": ["app", "db", "redis"], |
||||
|
||||
"forwardPorts": [3000, 4000], |
||||
|
||||
"portsAttributes": { |
||||
"3000": { |
||||
"label": "web", |
||||
"onAutoForward": "notify" |
||||
}, |
||||
"4000": { |
||||
"label": "stream", |
||||
"onAutoForward": "silent" |
||||
} |
||||
}, |
||||
|
||||
"otherPortsAttributes": { |
||||
"onAutoForward": "silent" |
||||
}, |
||||
|
||||
"remoteEnv": { |
||||
"LOCAL_DOMAIN": "${localEnv:CODESPACE_NAME}-3000.app.github.dev", |
||||
"LOCAL_HTTPS": "true", |
||||
"STREAMING_API_BASE_URL": "https://${localEnv:CODESPACE_NAME}-4000.app.github.dev", |
||||
"DISABLE_FORGERY_REQUEST_PROTECTION": "true", |
||||
"ES_ENABLED": "", |
||||
"LIBRE_TRANSLATE_ENDPOINT": "" |
||||
}, |
||||
|
||||
"onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", |
||||
"postCreateCommand": ".devcontainer/post-create.sh", |
||||
"waitFor": "postCreateCommand", |
||||
|
||||
"customizations": { |
||||
"vscode": { |
||||
"settings": {}, |
||||
"extensions": ["EditorConfig.EditorConfig", "webben.browserslist"] |
||||
} |
||||
} |
||||
} |
@ -1,4 +1,4 @@ |
||||
web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb |
||||
sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq |
||||
stream: env PORT=4000 yarn run start |
||||
webpack: ./bin/webpack-dev-server --listen-host 0.0.0.0 |
||||
webpack: bin/webpack-dev-server |
||||
|
@ -0,0 +1,56 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class PublicStatusesIndex < Chewy::Index |
||||
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: { |
||||
filter: { |
||||
english_stop: { |
||||
type: 'stop', |
||||
stopwords: '_english_', |
||||
}, |
||||
|
||||
english_stemmer: { |
||||
type: 'stemmer', |
||||
language: 'english', |
||||
}, |
||||
|
||||
english_possessive_stemmer: { |
||||
type: 'stemmer', |
||||
language: 'possessive_english', |
||||
}, |
||||
}, |
||||
|
||||
analyzer: { |
||||
verbatim: { |
||||
tokenizer: 'uax_url_email', |
||||
filter: %w(lowercase), |
||||
}, |
||||
|
||||
content: { |
||||
tokenizer: 'standard', |
||||
filter: %w( |
||||
lowercase |
||||
asciifolding |
||||
cjk_width |
||||
elision |
||||
english_possessive_stemmer |
||||
english_stop |
||||
english_stemmer |
||||
), |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
index_scope ::Status.unscoped |
||||
.kept |
||||
.indexable |
||||
.includes(:media_attachments, :preloadable_poll, :preview_cards) |
||||
|
||||
root date_detection: false do |
||||
field(:id, type: 'long') |
||||
field(:account_id, type: 'long') |
||||
field(:text, type: 'text', analyzer: 'verbatim', value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: 'content') } |
||||
field(:language, type: 'keyword') |
||||
field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties }) |
||||
field(:created_at, type: 'date') |
||||
end |
||||
end |
@ -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,11 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
module AuthorizedFetchHelper |
||||
def authorized_fetch_mode? |
||||
ENV.fetch('AUTHORIZED_FETCH') { Setting.authorized_fetch } == 'true' || Rails.configuration.x.limited_federation_mode |
||||
end |
||||
|
||||
def authorized_fetch_overridden? |
||||
ENV.key?('AUTHORIZED_FETCH') || Rails.configuration.x.limited_federation_mode |
||||
end |
||||
end |
@ -1,28 +0,0 @@ |
||||
// This file will be loaded on public pages, regardless of theme.
|
||||
|
||||
import 'packs/public-path'; |
||||
|
||||
import { delegate } from '@rails/ujs'; |
||||
|
||||
const getProfileAvatarAnimationHandler = (swapTo) => { |
||||
//animate avatar gifs on the profile page when moused over
|
||||
return ({ target }) => { |
||||
const swapSrc = target.getAttribute(swapTo); |
||||
//only change the img source if autoplay is off and the image src is actually different
|
||||
if(target.getAttribute('data-autoplay') !== 'true' && target.src !== swapSrc) { |
||||
target.src = swapSrc; |
||||
} |
||||
}; |
||||
}; |
||||
|
||||
delegate(document, 'img#profile_page_avatar', 'mouseover', getProfileAvatarAnimationHandler('data-original')); |
||||
|
||||
delegate(document, 'img#profile_page_avatar', 'mouseout', getProfileAvatarAnimationHandler('data-static')); |
||||
|
||||
delegate(document, '#account_header', 'change', ({ target }) => { |
||||
const header = document.querySelector('.card .card__img img'); |
||||
const [file] = target.files || []; |
||||
const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc; |
||||
|
||||
header.src = url; |
||||
}); |
@ -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> |
||||
); |
@ -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> |
||||
); |
@ -1 +1 @@ |
||||
import '@testing-library/jest-dom/extend-expect'; |
||||
import '@testing-library/jest-dom'; |
||||
|
@ -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 |
@ -0,0 +1,32 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class Importer::PublicStatusesIndexImporter < Importer::BaseImporter |
||||
def import! |
||||
scope.select(:id).find_in_batches(batch_size: @batch_size) do |batch| |
||||
in_work_unit(batch.pluck(:id)) do |status_ids| |
||||
bulk = ActiveRecord::Base.connection_pool.with_connection do |
||||
build_bulk_body(index.adapter.default_scope.where(id: status_ids)) |
||||
end |
||||
|
||||
indexed = bulk.size |
||||
deleted = 0 |
||||
|
||||
Chewy::Index::Import::BulkRequest.new(index).perform(bulk) |
||||
|
||||
[indexed, deleted] |
||||
end |
||||
end |
||||
|
||||
wait! |
||||
end |
||||
|
||||
private |
||||
|
||||
def index |
||||
PublicStatusesIndex |
||||
end |
||||
|
||||
def scope |
||||
Status.indexable |
||||
end |
||||
end |
@ -0,0 +1,44 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
module AccountStatusesSearch |
||||
extend ActiveSupport::Concern |
||||
|
||||
included do |
||||
after_update_commit :enqueue_update_public_statuses_index, if: :saved_change_to_indexable? |
||||
after_destroy_commit :enqueue_remove_from_public_statuses_index, if: :indexable? |
||||
end |
||||
|
||||
def enqueue_update_public_statuses_index |
||||
if indexable? |
||||
enqueue_add_to_public_statuses_index |
||||
else |
||||
enqueue_remove_from_public_statuses_index |
||||
end |
||||
end |
||||
|
||||
def enqueue_add_to_public_statuses_index |
||||
return unless Chewy.enabled? |
||||
|
||||
AddToPublicStatusesIndexWorker.perform_async(id) |
||||
end |
||||
|
||||
def enqueue_remove_from_public_statuses_index |
||||
return unless Chewy.enabled? |
||||
|
||||
RemoveFromPublicStatusesIndexWorker.perform_async(id) |
||||
end |
||||
|
||||
def add_to_public_statuses_index! |
||||
return unless Chewy.enabled? |
||||
|
||||
statuses.without_reblogs.where(visibility: :public).find_in_batches do |batch| |
||||
PublicStatusesIndex.import(batch) |
||||
end |
||||
end |
||||
|
||||
def remove_from_public_statuses_index! |
||||
return unless Chewy.enabled? |
||||
|
||||
PublicStatusesIndex.filter(term: { account_id: id }).delete_all |
||||
end |
||||
end |
@ -0,0 +1,48 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
module StatusSearchConcern |
||||
extend ActiveSupport::Concern |
||||
|
||||
included do |
||||
scope :indexable, -> { without_reblogs.where(visibility: :public).joins(:account).where(account: { indexable: true }) } |
||||
end |
||||
|
||||
def searchable_by |
||||
@searchable_by ||= begin |
||||
ids = [] |
||||
|
||||
ids << account_id if local? |
||||
|
||||
ids += local_mentioned.pluck(:id) |
||||
ids += local_favorited.pluck(:id) |
||||
ids += local_reblogged.pluck(:id) |
||||
ids += local_bookmarked.pluck(:id) |
||||
ids += preloadable_poll.local_voters.pluck(:id) if preloadable_poll.present? |
||||
|
||||
ids.uniq |
||||
end |
||||
end |
||||
|
||||
def searchable_text |
||||
[ |
||||
spoiler_text, |
||||
FormattingHelper.extract_status_plain_text(self), |
||||
preloadable_poll&.options&.join("\n\n"), |
||||
ordered_media_attachments.map(&:description).join("\n\n"), |
||||
].compact.join("\n\n") |
||||
end |
||||
|
||||
def searchable_properties |
||||
[].tap do |properties| |
||||
properties << 'image' if ordered_media_attachments.any?(&:image?) |
||||
properties << 'video' if ordered_media_attachments.any?(&:video?) |
||||
properties << 'audio' if ordered_media_attachments.any?(&:audio?) |
||||
properties << 'media' if with_media? |
||||
properties << 'poll' if with_poll? |
||||
properties << 'link' if with_preview_card? |
||||
properties << 'embed' if preview_cards.any?(&:video?) |
||||
properties << 'sensitive' if sensitive? |
||||
properties << 'reply' if reply? |
||||
end |
||||
end |
||||
end |
@ -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 |
@ -0,0 +1,7 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class SoftwareUpdatePolicy < ApplicationPolicy |
||||
def index? |
||||
role.can?(:view_devops) |
||||
end |
||||
end |
@ -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 |
@ -0,0 +1,64 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class StatusesSearchService < BaseService |
||||
def call(query, account = nil, options = {}) |
||||
@query = query&.strip |
||||
@account = account |
||||
@options = options |
||||
@limit = options[:limit].to_i |
||||
@offset = options[:offset].to_i |
||||
|
||||
status_search_results |
||||
end |
||||
|
||||
private |
||||
|
||||
def status_search_results |
||||
definition = parsed_query.apply( |
||||
Chewy::Search::Request.new(StatusesIndex, PublicStatusesIndex).filter( |
||||
bool: { |
||||
should: [ |
||||
publicly_searchable, |
||||
non_publicly_searchable, |
||||
], |
||||
|
||||
minimum_should_match: 1, |
||||
} |
||||
) |
||||
) |
||||
|
||||
results = definition.collapse(field: :id).order(id: { order: :desc }).limit(@limit).offset(@offset).objects.compact |
||||
account_ids = results.map(&:account_id) |
||||
account_domains = results.map(&:account_domain) |
||||
preloaded_relations = @account.relations_map(account_ids, account_domains) |
||||
|
||||
results.reject { |status| StatusFilter.new(status, @account, preloaded_relations).filtered? } |
||||
rescue Faraday::ConnectionFailed, Parslet::ParseFailed |
||||
[] |
||||
end |
||||
|
||||
def publicly_searchable |
||||
{ |
||||
term: { _index: PublicStatusesIndex.index_name }, |
||||
} |
||||
end |
||||
|
||||
def non_publicly_searchable |
||||
{ |
||||
bool: { |
||||
must: [ |
||||
{ |
||||
term: { _index: StatusesIndex.index_name }, |
||||
}, |
||||
{ |
||||
term: { searchable_by: @account.id }, |
||||
}, |
||||
], |
||||
}, |
||||
} |
||||
end |
||||
|
||||
def parsed_query |
||||
SearchQueryTransformer.new.apply(SearchQueryParser.new.parse(@query), current_account: @account) |
||||
end |
||||
end |
@ -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 |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue