diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index f991036ad..b3b1d97a2 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -4,10 +4,6 @@ FROM mcr.microsoft.com/devcontainers/ruby:1-3.2-bullseye
# Install Rails
# RUN gem install rails webdrivers
-# Default value to allow debug server to serve content over GitHub Codespace's port forwarding service
-# The value is a comma-separated list of allowed domains
-ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev,.preview.app.github.dev,.app.github.dev"
-
ARG NODE_VERSION="16"
RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"
diff --git a/.devcontainer/codespaces/devcontainer.json b/.devcontainer/codespaces/devcontainer.json
new file mode 100644
index 000000000..ca9156fda
--- /dev/null
+++ b/.devcontainer/codespaces/devcontainer.json
@@ -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"]
+ }
+ }
+}
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index ce14169aa..fa8d6542c 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,5 +1,5 @@
{
- "name": "Mastodon",
+ "name": "Mastodon on local machine",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
@@ -8,13 +8,23 @@
"ghcr.io/devcontainers/features/sshd:1": {}
},
- "runServices": ["app", "db", "redis"],
-
"forwardPorts": [3000, 4000],
- "containerEnv": {
- "ES_ENABLED": "",
- "LIBRE_TRANSLATE_ENDPOINT": ""
+ "portsAttributes": {
+ "3000": {
+ "label": "web",
+ "onAutoForward": "notify",
+ "requireLocalPort": true
+ },
+ "4000": {
+ "label": "stream",
+ "onAutoForward": "silent",
+ "requireLocalPort": true
+ }
+ },
+
+ "otherPortsAttributes": {
+ "onAutoForward": "silent"
},
"onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
index a2658ea8b..20aecd71d 100644
--- a/.devcontainer/docker-compose.yml
+++ b/.devcontainer/docker-compose.yml
@@ -25,6 +25,7 @@ services:
command: sleep infinity
ports:
- '127.0.0.1:3000:3000'
+ - '127.0.0.1:3035:3035'
- '127.0.0.1:4000:4000'
networks:
- external_network
diff --git a/.github/workflows/build-container-image.yml b/.github/workflows/build-container-image.yml
index 1b15d1988..a1aeddf20 100644
--- a/.github/workflows/build-container-image.yml
+++ b/.github/workflows/build-container-image.yml
@@ -8,7 +8,9 @@ on:
type: boolean
push_to_images:
type: string
- version_suffix:
+ version_prerelease:
+ type: string
+ version_metadata:
type: string
flavor:
type: string
@@ -74,8 +76,6 @@ jobs:
if: ${{ inputs.push_to_images != '' }}
with:
images: ${{ inputs.push_to_images }}
- # Only tag with latest when ran against the latest stable branch
- # This needs to be updated after each minor version release
flavor: ${{ inputs.flavor }}
tags: ${{ inputs.tags }}
labels: ${{ inputs.labels }}
@@ -83,7 +83,9 @@ jobs:
- uses: docker/build-push-action@v4
with:
context: .
- build-args: MASTODON_VERSION_SUFFIX=${{ inputs.version_suffix }}
+ build-args: |
+ MASTODON_VERSION_PRERELEASE=${{ inputs.version_prerelease }}
+ MASTODON_VERSION_METADATA=${{ inputs.version_metadata }}
platforms: ${{ inputs.platforms }}
provenance: false
builder: ${{ steps.buildx.outputs.name || steps.buildx-native.outputs.name }}
diff --git a/.github/workflows/build-nightly.yml b/.github/workflows/build-nightly.yml
index a3ae671d6..2600cf924 100644
--- a/.github/workflows/build-nightly.yml
+++ b/.github/workflows/build-nightly.yml
@@ -16,9 +16,9 @@ jobs:
env:
TZ: Etc/UTC
run: |
- echo mastodon_version_suffix=nightly.$(date +'%Y-%m-%d')>> $GITHUB_OUTPUT
+ echo mastodon_version_prerelease=nightly.$(date +'%Y-%m-%d')>> $GITHUB_OUTPUT
outputs:
- suffix: ${{ steps.version_vars.outputs.mastodon_version_suffix }}
+ prerelease: ${{ steps.version_vars.outputs.mastodon_version_prerelease }}
build-image:
needs: compute-suffix
@@ -28,8 +28,7 @@ jobs:
use_native_arm64_builder: false
push_to_images: |
ghcr.io/${{ github.repository_owner }}/mastodon
- # The `-` is important here, result will be v4.1.2-nightly.2022-03-05
- version_suffix: -${{ needs.compute-suffix.outputs.suffix }}
+ version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }}
labels: |
org.opencontainers.image.description=Nightly build image used for testing purposes
flavor: |
@@ -37,5 +36,5 @@ jobs:
tags: |
type=raw,value=edge
type=raw,value=nightly
- type=schedule,pattern=${{ needs.compute-suffix.outputs.suffix }}
+ type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }}
secrets: inherit
diff --git a/.github/workflows/build-push-pr.yml b/.github/workflows/build-push-pr.yml
index f78e5ebad..cf11d6653 100644
--- a/.github/workflows/build-push-pr.yml
+++ b/.github/workflows/build-push-pr.yml
@@ -21,9 +21,9 @@ jobs:
uses: actions/checkout@v3
- id: version_vars
run: |
- echo mastodon_version_suffix=+pr-${{ github.event.pull_request.number }}-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT
+ echo mastodon_version_metadata=pr-${{ github.event.pull_request.number }}-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT
outputs:
- suffix: ${{ steps.version_vars.outputs.mastodon_version_suffix }}
+ metadata: ${{ steps.version_vars.outputs.mastodon_version_metadata }}
build-image:
needs: compute-suffix
@@ -33,7 +33,7 @@ jobs:
use_native_arm64_builder: false
push_to_images: |
ghcr.io/${{ github.repository_owner }}/mastodon
- version_suffix: ${{ needs.compute-suffix.outputs.suffix }}
+ version_metadata: ${{ needs.compute-suffix.outputs.metadata }}
flavor: |
latest=auto
tags: |
diff --git a/.github/workflows/build-releases.yml b/.github/workflows/build-releases.yml
index fa923e960..b9728f6a2 100644
--- a/.github/workflows/build-releases.yml
+++ b/.github/workflows/build-releases.yml
@@ -16,6 +16,8 @@ jobs:
use_native_arm64_builder: false
push_to_images: |
ghcr.io/${{ github.repository_owner }}/mastodon
+ # Only tag with latest when ran against the latest stable branch
+ # This needs to be updated after each minor version release
flavor: |
latest=${{ startsWith(github.ref, 'refs/tags/v4.1.') }}
tags: |
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 107dfaca3..fe66adc08 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -101,7 +101,7 @@ The following changelog entries focus on changes visible to users, administrator
- **Change translation feature to cover Content Warnings, poll options and media descriptions** ([c960657](https://github.com/mastodon/mastodon/pull/24175), [S-H-GAMELINKS](https://github.com/mastodon/mastodon/pull/25251), [c960657](https://github.com/mastodon/mastodon/pull/26168), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26452))
- **Change account search to match by text when opted-in** ([jsgoldstein](https://github.com/mastodon/mastodon/pull/25599), [Gargron](https://github.com/mastodon/mastodon/pull/26378))
- **Change import feature to be clearer, less error-prone and more reliable** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21054), [mgmn](https://github.com/mastodon/mastodon/pull/24874))
-- **Change local and federated timelines to be in a single “Live feeds” column** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25641), [Gargron](https://github.com/mastodon/mastodon/pull/25683), [mgmn](https://github.com/mastodon/mastodon/pull/25694), [Plastikmensch](https://github.com/mastodon/mastodon/pull/26247))
+- **Change local and federated timelines to be tabs of a single “Live feeds” column** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25641), [Gargron](https://github.com/mastodon/mastodon/pull/25683), [mgmn](https://github.com/mastodon/mastodon/pull/25694), [Plastikmensch](https://github.com/mastodon/mastodon/pull/26247))
- **Change user archive export to be faster and more reliable, and export `.zip` archives instead of `.tar.gz` ones** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23360), [TheEssem](https://github.com/mastodon/mastodon/pull/25034))
- **Change `mastodon-streaming` systemd unit files to be templated** ([e-nomem](https://github.com/mastodon/mastodon/pull/24751))
- **Change `statsd` integration to disable sidekiq metrics by default** ([mjankowski](https://github.com/mastodon/mastodon/pull/25265), [mjankowski](https://github.com/mastodon/mastodon/pull/25336), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26310))
@@ -189,6 +189,7 @@ The following changelog entries focus on changes visible to users, administrator
- **Fix log-in flow when involving both OAuth and external authentication** ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24073))
- **Fix broken links in account gallery** ([c960657](https://github.com/mastodon/mastodon/pull/24218))
- **Fix blocking subdomains of an already-blocked domain** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26392))
+- **Fix migration handler not updating lists** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24808))
- Fix uploading of video files for which `ffprobe` reports `0/0` average framerate ([NicolaiSoeborg](https://github.com/mastodon/mastodon/pull/26500))
- Fix cached posts including stale stats ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26409))
- Fix adding column with default value taking longer on Postgres >= 11 ([Gargron](https://github.com/mastodon/mastodon/pull/26375))
diff --git a/Dockerfile b/Dockerfile
index cb5096581..cdabc4c7c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -42,8 +42,8 @@ RUN apt-get update && \
FROM node:${NODE_VERSION}
# Use those args to specify your own version flags & suffixes
-ARG MASTODON_VERSION_FLAGS=""
-ARG MASTODON_VERSION_SUFFIX=""
+ARG MASTODON_VERSION_PRERELEASE=""
+ARG MASTODON_VERSION_METADATA=""
ARG UID="991"
ARG GID="991"
@@ -89,8 +89,8 @@ ENV RAILS_ENV="production" \
NODE_ENV="production" \
RAILS_SERVE_STATIC_FILES="true" \
BIND="0.0.0.0" \
- MASTODON_VERSION_FLAGS="${MASTODON_VERSION_FLAGS}" \
- MASTODON_VERSION_SUFFIX="${MASTODON_VERSION_SUFFIX}"
+ MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \
+ MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}"
# Set the run user
USER mastodon
diff --git a/Gemfile.lock b/Gemfile.lock
index f801b1b56..4e30c4222 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -109,7 +109,7 @@ GEM
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
- addressable (2.8.4)
+ addressable (2.8.5)
public_suffix (>= 2.0.2, < 6.0)
aes_key_wrap (1.1.0)
airbrussh (1.4.1)
@@ -124,8 +124,8 @@ GEM
attr_required (1.0.1)
awrence (1.2.1)
aws-eventstream (1.2.0)
- aws-partitions (1.793.0)
- aws-sdk-core (3.180.3)
+ aws-partitions (1.809.0)
+ aws-sdk-core (3.181.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
@@ -133,8 +133,8 @@ GEM
aws-sdk-kms (1.71.0)
aws-sdk-core (~> 3, >= 3.177.0)
aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.132.1)
- aws-sdk-core (~> 3, >= 3.179.0)
+ aws-sdk-s3 (1.133.0)
+ aws-sdk-core (~> 3, >= 3.181.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.6)
aws-sigv4 (1.6.0)
@@ -203,7 +203,7 @@ GEM
activesupport
cbor (0.5.9.6)
charlock_holmes (0.7.7)
- chewy (7.3.3)
+ chewy (7.3.4)
activesupport (>= 5.2)
elasticsearch (>= 7.12.0, < 7.14.0)
elasticsearch-dsl
@@ -324,7 +324,7 @@ GEM
ruby-progressbar (~> 1.4)
globalid (1.1.0)
activesupport (>= 5.0)
- haml (6.1.1)
+ haml (6.1.2)
temple (>= 0.8.2)
thor
tilt
@@ -333,7 +333,7 @@ GEM
activesupport (>= 5.1)
haml (>= 4.0.6)
railties (>= 5.1)
- haml_lint (0.49.3)
+ haml_lint (0.50.0)
haml (>= 4.0, < 6.2)
parallel (~> 1.10)
rainbow
@@ -482,7 +482,7 @@ GEM
nokogiri (1.15.4)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
- oj (3.16.0)
+ oj (3.16.1)
omniauth (2.1.1)
hashie (>= 3.4.6)
rack (>= 2.2.3)
@@ -519,7 +519,7 @@ GEM
parslet (2.0.0)
pastel (0.8.0)
tty-color (~> 0.5)
- pg (1.5.3)
+ pg (1.5.4)
pghero (3.3.3)
activerecord (>= 6)
posix-spawn (0.3.15)
@@ -731,7 +731,7 @@ GEM
net-ssh (>= 2.8.0)
stackprof (0.2.25)
statsd-ruby (1.5.0)
- stoplight (3.0.1)
+ stoplight (3.0.2)
redlock (~> 1.0)
strong_migrations (0.8.0)
activerecord (>= 5.2)
@@ -795,7 +795,7 @@ GEM
webfinger (1.2.0)
activesupport
httpclient (>= 2.4)
- webmock (3.18.1)
+ webmock (3.19.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
diff --git a/Procfile.dev b/Procfile.dev
index ba04fb661..fbb2c2de2 100644
--- a/Procfile.dev
+++ b/Procfile.dev
@@ -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
diff --git a/app/chewy/accounts_index.rb b/app/chewy/accounts_index.rb
index 1f8571c09..00db257ac 100644
--- a/app/chewy/accounts_index.rb
+++ b/app/chewy/accounts_index.rb
@@ -21,12 +21,13 @@ class AccountsIndex < Chewy::Index
analyzer: {
natural: {
- tokenizer: 'uax_url_email',
+ tokenizer: 'standard',
filter: %w(
- english_possessive_stemmer
lowercase
asciifolding
cjk_width
+ elision
+ english_possessive_stemmer
english_stop
english_stemmer
),
@@ -62,6 +63,6 @@ class AccountsIndex < Chewy::Index
field(:last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at })
field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
- field(:text, type: 'text', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }
+ field(:text, type: 'text', analyzer: 'verbatim', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }
end
end
diff --git a/app/chewy/public_statuses_index.rb b/app/chewy/public_statuses_index.rb
new file mode 100644
index 000000000..5c68a1365
--- /dev/null
+++ b/app/chewy/public_statuses_index.rb
@@ -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
diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb
index 9f680efa5..2be7e4525 100644
--- a/app/chewy/statuses_index.rb
+++ b/app/chewy/statuses_index.rb
@@ -1,31 +1,38 @@
# frozen_string_literal: true
class StatusesIndex < Chewy::Index
- include FormattingHelper
-
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: {
- content: {
+ verbatim: {
tokenizer: 'uax_url_email',
+ filter: %w(lowercase),
+ },
+
+ content: {
+ tokenizer: 'standard',
filter: %w(
- english_possessive_stemmer
lowercase
asciifolding
cjk_width
+ elision
+ english_possessive_stemmer
english_stop
english_stemmer
),
@@ -33,43 +40,15 @@ class StatusesIndex < Chewy::Index
},
}
- # We do not use delete_if option here because it would call a method that we
- # expect to be called with crutches without crutches, causing n+1 queries
- index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll)
-
- crutch :mentions do |collection|
- data = ::Mention.where(status_id: collection.map(&:id)).where(account: Account.local, silent: false).pluck(:status_id, :account_id)
- data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
- end
-
- crutch :favourites do |collection|
- data = ::Favourite.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
- data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
- end
-
- crutch :reblogs do |collection|
- data = ::Status.where(reblog_of_id: collection.map(&:id)).where(account: Account.local).pluck(:reblog_of_id, :account_id)
- data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
- end
-
- crutch :bookmarks do |collection|
- data = ::Bookmark.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
- data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
- end
-
- crutch :votes do |collection|
- data = ::PollVote.joins(:poll).where(poll: { status_id: collection.map(&:id) }).where(account: Account.local).pluck(:status_id, :account_id)
- data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
- end
+ index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preview_cards, :local_mentioned, :local_favorited, :local_reblogged, :local_bookmarked, preloadable_poll: :local_voters), delete_if: ->(status) { status.searchable_by.empty? }
root date_detection: false do
- field :id, type: 'long'
- field :account_id, type: 'long'
-
- field :text, type: 'text', value: ->(status) { status.searchable_text } do
- field :stemmed, type: 'text', analyzer: 'content'
- end
-
- field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) }
+ 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(:searchable_by, type: 'long', value: ->(status) { status.searchable_by })
+ field(:language, type: 'keyword')
+ field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })
+ field(:created_at, type: 'date')
end
end
diff --git a/app/controllers/admin/software_updates_controller.rb b/app/controllers/admin/software_updates_controller.rb
new file mode 100644
index 000000000..52d8cb41e
--- /dev/null
+++ b/app/controllers/admin/software_updates_controller.rb
@@ -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
diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb
index 7c7d70fd3..76ba75824 100644
--- a/app/controllers/api/v1/accounts/credentials_controller.rb
+++ b/app/controllers/api/v1/accounts/credentials_controller.rb
@@ -30,6 +30,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
:bot,
:discoverable,
:hide_collections,
+ :indexable,
fields_attributes: [:name, :value]
)
end
diff --git a/app/controllers/api/v1/statuses/translations_controller.rb b/app/controllers/api/v1/statuses/translations_controller.rb
index 540b17d00..ec5ea5b85 100644
--- a/app/controllers/api/v1/statuses/translations_controller.rb
+++ b/app/controllers/api/v1/statuses/translations_controller.rb
@@ -8,7 +8,15 @@ class Api::V1::Statuses::TranslationsController < Api::BaseController
before_action :set_translation
rescue_from TranslationService::NotConfiguredError, with: :not_found
- rescue_from TranslationService::UnexpectedResponseError, TranslationService::QuotaExceededError, TranslationService::TooManyRequestsError, with: :service_unavailable
+ rescue_from TranslationService::UnexpectedResponseError, with: :service_unavailable
+
+ rescue_from TranslationService::QuotaExceededError do
+ render json: { error: I18n.t('translation.errors.quota_exceeded') }, status: 503
+ end
+
+ rescue_from TranslationService::TooManyRequestsError do
+ render json: { error: I18n.t('translation.errors.too_many_requests') }, status: 503
+ end
def create
render json: @translation, serializer: REST::TranslationSerializer
diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb
index 9cd7b9904..a79d65c12 100644
--- a/app/controllers/api/v1/timelines/tag_controller.rb
+++ b/app/controllers/api/v1/timelines/tag_controller.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class Api::V1::Timelines::TagController < Api::BaseController
+ before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :show, if: :require_auth?
before_action :load_tag
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
@@ -12,6 +13,10 @@ class Api::V1::Timelines::TagController < Api::BaseController
private
+ def require_auth?
+ !Setting.timeline_preview
+ end
+
def load_tag
@tag = Tag.find_normalized(params[:id])
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index c57031da3..4d7805aba 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -12,6 +12,7 @@ class ApplicationController < ActionController::Base
include DomainControlHelper
include ThemingConcern
include DatabaseHelper
+ include AuthorizedFetchHelper
helper_method :current_account
helper_method :current_session
@@ -53,10 +54,6 @@ class ApplicationController < ActionController::Base
private
- def authorized_fetch_mode?
- ENV['AUTHORIZED_FETCH'] == 'true' || Rails.configuration.x.limited_federation_mode
- end
-
def public_fetch_mode?
!authorized_fetch_mode?
end
diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb
index 1d27c92c8..b0c4fff8b 100644
--- a/app/controllers/concerns/signature_verification.rb
+++ b/app/controllers/concerns/signature_verification.rb
@@ -119,6 +119,8 @@ module SignatureVerification
private
def fail_with!(message, **options)
+ Rails.logger.warn { "Signature verification failed: #{message}" }
+
@signature_verification_failure_reason = { error: message }.merge(options)
@signed_request_actor = nil
end
diff --git a/app/controllers/settings/privacy_controller.rb b/app/controllers/settings/privacy_controller.rb
index c2648eedd..1102c89fa 100644
--- a/app/controllers/settings/privacy_controller.rb
+++ b/app/controllers/settings/privacy_controller.rb
@@ -18,7 +18,7 @@ class Settings::PrivacyController < Settings::BaseController
private
def account_params
- params.require(:account).permit(:discoverable, :unlocked, :show_collections, settings: UserSettings.keys)
+ params.require(:account).permit(:discoverable, :unlocked, :indexable, :show_collections, settings: UserSettings.keys)
end
def set_account
diff --git a/app/helpers/authorized_fetch_helper.rb b/app/helpers/authorized_fetch_helper.rb
new file mode 100644
index 000000000..ce87526e6
--- /dev/null
+++ b/app/helpers/authorized_fetch_helper.rb
@@ -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
diff --git a/app/javascript/core/public.js b/app/javascript/core/public.js
deleted file mode 100644
index 01b4157f8..000000000
--- a/app/javascript/core/public.js
+++ /dev/null
@@ -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;
-});
diff --git a/app/javascript/core/settings.js b/app/javascript/core/settings.js
index 0155d933e..d9a8ea4cf 100644
--- a/app/javascript/core/settings.js
+++ b/app/javascript/core/settings.js
@@ -2,21 +2,6 @@
import 'packs/public-path';
import { delegate } from '@rails/ujs';
-import escapeTextContentForBrowser from 'escape-html';
-
-
-import emojify from '../mastodon/features/emoji/emoji';
-
-delegate(document, '#account_display_name', 'input', ({ target }) => {
- const name = document.querySelector('.card .display-name strong');
- if (name) {
- if (target.value) {
- name.innerHTML = emojify(escapeTextContentForBrowser(target.value));
- } else {
- name.textContent = name.textContent = target.dataset.default;
- }
- }
-});
delegate(document, '#edit_profile input[type=file]', 'change', ({ target }) => {
const avatar = document.getElementById(target.id + '-preview');
@@ -26,18 +11,6 @@ delegate(document, '#edit_profile input[type=file]', 'change', ({ target }) => {
avatar.src = url;
});
-delegate(document, '#account_locked', 'change', ({ target }) => {
- const lock = document.querySelector('.card .display-name i');
-
- if (lock) {
- if (target.checked) {
- delete lock.dataset.hidden;
- } else {
- lock.dataset.hidden = 'true';
- }
- }
-});
-
delegate(document, '.input-copy input', 'click', ({ target }) => {
target.focus();
target.select();
diff --git a/app/javascript/core/theme.yml b/app/javascript/core/theme.yml
index 20912e28d..1b2bfb98f 100644
--- a/app/javascript/core/theme.yml
+++ b/app/javascript/core/theme.yml
@@ -13,8 +13,8 @@ pack:
mailer:
filename: mailer.js
stylesheet: true
- modal: public.js
- public: public.js
+ modal:
+ public:
settings: settings.js
sign_up:
share:
diff --git a/app/javascript/flavours/glitch/actions/interactions.js b/app/javascript/flavours/glitch/actions/interactions.js
index 6b8864a03..095fb3155 100644
--- a/app/javascript/flavours/glitch/actions/interactions.js
+++ b/app/javascript/flavours/glitch/actions/interactions.js
@@ -1,11 +1,16 @@
-import api from '../api';
+import api, { getLinks } from '../api';
+import { fetchRelationships } from './accounts';
import { importFetchedAccounts, importFetchedStatus } from './importer';
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
export const REBLOG_FAIL = 'REBLOG_FAIL';
+export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
+export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
+export const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL';
+
export const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
export const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
export const FAVOURITE_FAIL = 'FAVOURITE_FAIL';
@@ -26,6 +31,10 @@ export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
+export const FAVOURITES_EXPAND_REQUEST = 'FAVOURITES_EXPAND_REQUEST';
+export const FAVOURITES_EXPAND_SUCCESS = 'FAVOURITES_EXPAND_SUCCESS';
+export const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL';
+
export const PIN_REQUEST = 'PIN_REQUEST';
export const PIN_SUCCESS = 'PIN_SUCCESS';
export const PIN_FAIL = 'PIN_FAIL';
@@ -259,8 +268,10 @@ export function fetchReblogs(id) {
dispatch(fetchReblogsRequest(id));
api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
- dispatch(fetchReblogsSuccess(id, response.data));
+ dispatch(fetchReblogsSuccess(id, response.data, next ? next.uri : null));
+ dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => {
dispatch(fetchReblogsFail(id, error));
});
@@ -274,17 +285,62 @@ export function fetchReblogsRequest(id) {
};
}
-export function fetchReblogsSuccess(id, accounts) {
+export function fetchReblogsSuccess(id, accounts, next) {
return {
type: REBLOGS_FETCH_SUCCESS,
id,
accounts,
+ next,
};
}
export function fetchReblogsFail(id, error) {
return {
type: REBLOGS_FETCH_FAIL,
+ id,
+ error,
+ };
+}
+
+export function expandReblogs(id) {
+ return (dispatch, getState) => {
+ const url = getState().getIn(['user_lists', 'reblogged_by', id, 'next']);
+ if (url === null) {
+ return;
+ }
+
+ dispatch(expandReblogsRequest(id));
+
+ api(getState).get(url).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+ dispatch(importFetchedAccounts(response.data));
+ dispatch(expandReblogsSuccess(id, response.data, next ? next.uri : null));
+ dispatch(fetchRelationships(response.data.map(item => item.id)));
+ }).catch(error => dispatch(expandReblogsFail(id, error)));
+ };
+}
+
+export function expandReblogsRequest(id) {
+ return {
+ type: REBLOGS_EXPAND_REQUEST,
+ id,
+ };
+}
+
+export function expandReblogsSuccess(id, accounts, next) {
+ return {
+ type: REBLOGS_EXPAND_SUCCESS,
+ id,
+ accounts,
+ next,
+ };
+}
+
+export function expandReblogsFail(id, error) {
+ return {
+ type: REBLOGS_EXPAND_FAIL,
+ id,
error,
};
}
@@ -294,8 +350,10 @@ export function fetchFavourites(id) {
dispatch(fetchFavouritesRequest(id));
api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
- dispatch(fetchFavouritesSuccess(id, response.data));
+ dispatch(fetchFavouritesSuccess(id, response.data, next ? next.uri : null));
+ dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => {
dispatch(fetchFavouritesFail(id, error));
});
@@ -309,17 +367,62 @@ export function fetchFavouritesRequest(id) {
};
}
-export function fetchFavouritesSuccess(id, accounts) {
+export function fetchFavouritesSuccess(id, accounts, next) {
return {
type: FAVOURITES_FETCH_SUCCESS,
id,
accounts,
+ next,
};
}
export function fetchFavouritesFail(id, error) {
return {
type: FAVOURITES_FETCH_FAIL,
+ id,
+ error,
+ };
+}
+
+export function expandFavourites(id) {
+ return (dispatch, getState) => {
+ const url = getState().getIn(['user_lists', 'favourited_by', id, 'next']);
+ if (url === null) {
+ return;
+ }
+
+ dispatch(expandFavouritesRequest(id));
+
+ api(getState).get(url).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+ dispatch(importFetchedAccounts(response.data));
+ dispatch(expandFavouritesSuccess(id, response.data, next ? next.uri : null));
+ dispatch(fetchRelationships(response.data.map(item => item.id)));
+ }).catch(error => dispatch(expandFavouritesFail(id, error)));
+ };
+}
+
+export function expandFavouritesRequest(id) {
+ return {
+ type: FAVOURITES_EXPAND_REQUEST,
+ id,
+ };
+}
+
+export function expandFavouritesSuccess(id, accounts, next) {
+ return {
+ type: FAVOURITES_EXPAND_SUCCESS,
+ id,
+ accounts,
+ next,
+ };
+}
+
+export function expandFavouritesFail(id, error) {
+ return {
+ type: FAVOURITES_EXPAND_FAIL,
+ id,
error,
};
}
diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx
index d3d432ae0..36abc6993 100644
--- a/app/javascript/flavours/glitch/components/status.jsx
+++ b/app/javascript/flavours/glitch/components/status.jsx
@@ -792,6 +792,7 @@ class Status extends ImmutablePureComponent {
tabIndex={0}
data-featured={featured ? 'true' : null}
aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}
+ data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}
>
{!muted && prepend}
diff --git a/app/javascript/flavours/glitch/features/compose/components/search.jsx b/app/javascript/flavours/glitch/features/compose/components/search.jsx
index 2f1b46e5d..e83c1761e 100644
--- a/app/javascript/flavours/glitch/features/compose/components/search.jsx
+++ b/app/javascript/flavours/glitch/features/compose/components/search.jsx
@@ -1,11 +1,7 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
-import {
- injectIntl,
- FormattedMessage,
- defineMessages,
-} from 'react-intl';
+import { defineMessages, injectIntl, FormattedMessage, FormattedList } from 'react-intl';
import classNames from 'classnames';
@@ -52,6 +48,16 @@ class Search extends PureComponent {
options: [],
};
+ defaultOptions = [
+ { label: <>has: >, action: e => { e.preventDefault(); this._insertText('has:') } },
+ { label: <>is: >, action: e => { e.preventDefault(); this._insertText('is:') } },
+ { label: <>language: >, action: e => { e.preventDefault(); this._insertText('language:') } },
+ { label: <>from: >, action: e => { e.preventDefault(); this._insertText('from:') } },
+ { label: <>before: >, action: e => { e.preventDefault(); this._insertText('before:') } },
+ { label: <>during: >, action: e => { e.preventDefault(); this._insertText('during:') } },
+ { label: <>after: >, action: e => { e.preventDefault(); this._insertText('after:') } },
+ ];
+
setRef = c => {
this.searchForm = c;
};
@@ -100,7 +106,7 @@ class Search extends PureComponent {
handleKeyDown = (e) => {
const { selectedOption } = this.state;
- const options = this._getOptions();
+ const options = searchEnabled ? this._getOptions().concat(this.defaultOptions) : this._getOptions();
switch(e.key) {
case 'Escape':
@@ -131,10 +137,9 @@ class Search extends PureComponent {
if (selectedOption === -1) {
this._submit();
} else if (options.length > 0) {
- options[selectedOption].action();
+ options[selectedOption].action(e);
}
- this._unfocus();
break;
case 'Delete':
if (selectedOption > -1 && options.length > 0) {
@@ -161,6 +166,7 @@ class Search extends PureComponent {
router.history.push(`/tags/${query}`);
onClickSearchResult(query, 'hashtag');
+ this._unfocus();
};
handleAccountClick = () => {
@@ -171,6 +177,7 @@ class Search extends PureComponent {
router.history.push(`/@${query}`);
onClickSearchResult(query, 'account');
+ this._unfocus();
};
handleURLClick = () => {
@@ -178,6 +185,7 @@ class Search extends PureComponent {
const { onOpenURL } = this.props;
onOpenURL(router.history);
+ this._unfocus();
};
handleStatusSearch = () => {
@@ -196,6 +204,8 @@ class Search extends PureComponent {
} else if (search.get('type') === 'hashtag') {
router.history.push(`/tags/${search.get('q')}`);
}
+
+ this._unfocus();
};
handleForgetRecentSearchClick = search => {
@@ -208,6 +218,18 @@ class Search extends PureComponent {
document.querySelector('.ui').parentElement.focus();
}
+ _insertText (text) {
+ const { value, onChange } = this.props;
+
+ if (value === '') {
+ onChange(text);
+ } else if (value[value.length - 1] === ' ') {
+ onChange(`${value}${text}`);
+ } else {
+ onChange(`${value} ${text}`);
+ }
+ }
+
_submit (type) {
const { onSubmit, openInRoute } = this.props;
const { router } = this.context;
@@ -217,6 +239,8 @@ class Search extends PureComponent {
if (openInRoute) {
router.history.push('/search');
}
+
+ this._unfocus();
}
_getOptions () {
@@ -337,6 +361,20 @@ class Search extends PureComponent {
>
)}
+
+ {searchEnabled && (
+ <>
+
+
+
+ {this.defaultOptions.map(({ key, label, action }, i) => (
+
+ ))}
+
+ >
+ )}
);
diff --git a/app/javascript/flavours/glitch/features/favourites/index.jsx b/app/javascript/flavours/glitch/features/favourites/index.jsx
index 2b36945ee..49fd62b96 100644
--- a/app/javascript/flavours/glitch/features/favourites/index.jsx
+++ b/app/javascript/flavours/glitch/features/favourites/index.jsx
@@ -8,7 +8,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
-import { fetchFavourites } from 'flavours/glitch/actions/interactions';
+import { debounce } from 'lodash';
+
+import { fetchFavourites, expandFavourites } from 'flavours/glitch/actions/interactions';
import ColumnHeader from 'flavours/glitch/components/column_header';
import { Icon } from 'flavours/glitch/components/icon';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
@@ -23,7 +25,9 @@ const messages = defineMessages({
});
const mapStateToProps = (state, props) => ({
- accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]),
+ accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'items']),
+ hasMore: !!state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'next']),
+ isLoading: state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'isLoading'], true),
});
class Favourites extends ImmutablePureComponent {
@@ -32,6 +36,8 @@ class Favourites extends ImmutablePureComponent {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list,
+ hasMore: PropTypes.bool,
+ isLoading: PropTypes.bool,
multiColumn: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
@@ -42,12 +48,6 @@ class Favourites extends ImmutablePureComponent {
}
}
- UNSAFE_componentWillReceiveProps (nextProps) {
- if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
- this.props.dispatch(fetchFavourites(nextProps.params.statusId));
- }
- }
-
handleHeaderClick = () => {
this.column.scrollTop();
};
@@ -60,8 +60,12 @@ class Favourites extends ImmutablePureComponent {
this.props.dispatch(fetchFavourites(this.props.params.statusId));
};
+ handleLoadMore = debounce(() => {
+ this.props.dispatch(expandFavourites(this.props.params.statusId));
+ }, 300, { leading: true });
+
render () {
- const { intl, accountIds, multiColumn } = this.props;
+ const { intl, accountIds, hasMore, isLoading, multiColumn } = this.props;
if (!accountIds) {
return (
@@ -87,6 +91,9 @@ class Favourites extends ImmutablePureComponent {
/>
diff --git a/app/javascript/flavours/glitch/features/home_timeline/components/critical_update_banner.tsx b/app/javascript/flavours/glitch/features/home_timeline/components/critical_update_banner.tsx
new file mode 100644
index 000000000..d0dd2b6ac
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/home_timeline/components/critical_update_banner.tsx
@@ -0,0 +1,26 @@
+import { FormattedMessage } from 'react-intl';
+
+export const CriticalUpdateBanner = () => (
+
+
+
+
+
+
+ {' '}
+
+
+
+
+
+
+);
diff --git a/app/javascript/flavours/glitch/features/home_timeline/index.jsx b/app/javascript/flavours/glitch/features/home_timeline/index.jsx
index e17680d8b..80dae5e4d 100644
--- a/app/javascript/flavours/glitch/features/home_timeline/index.jsx
+++ b/app/javascript/flavours/glitch/features/home_timeline/index.jsx
@@ -14,7 +14,7 @@ import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/act
import { IconWithBadge } from 'flavours/glitch/components/icon_with_badge';
import { NotSignedInIndicator } from 'flavours/glitch/components/not_signed_in_indicator';
import AnnouncementsContainer from 'flavours/glitch/features/getting_started/containers/announcements_container';
-import { me } from 'flavours/glitch/initial_state';
+import { me, criticalUpdatesPending } from 'flavours/glitch/initial_state';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { expandHomeTimeline } from '../../actions/timelines';
@@ -23,6 +23,7 @@ import ColumnHeader from '../../components/column_header';
import StatusListContainer from '../ui/containers/status_list_container';
import { ColumnSettings } from './components/column_settings';
+import { CriticalUpdateBanner } from './components/critical_update_banner';
import { ExplorePrompt } from './components/explore_prompt';
const messages = defineMessages({
@@ -158,8 +159,9 @@ class HomeTimeline extends PureComponent {
const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
const pinned = !!columnId;
const { signedIn } = this.context.identity;
+ const banners = [];
- let announcementsButton, banner;
+ let announcementsButton;
if (hasAnnouncements) {
announcementsButton = (
@@ -174,8 +176,12 @@ class HomeTimeline extends PureComponent {
);
}
+ if (criticalUpdatesPending) {
+ banners.push();
+ }
+
if (tooSlow) {
- banner = ;
+ banners.push();
}
return (
@@ -197,7 +203,7 @@ class HomeTimeline extends PureComponent {
{signedIn ? (
({
- accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]),
+ accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'items']),
+ hasMore: !!state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'next']),
+ isLoading: state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'isLoading'], true),
});
class Reblogs extends ImmutablePureComponent {
@@ -35,6 +35,8 @@ class Reblogs extends ImmutablePureComponent {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list,
+ hasMore: PropTypes.bool,
+ isLoading: PropTypes.bool,
multiColumn: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
@@ -45,12 +47,6 @@ class Reblogs extends ImmutablePureComponent {
}
}
- UNSAFE_componentWillReceiveProps(nextProps) {
- if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
- this.props.dispatch(fetchReblogs(nextProps.params.statusId));
- }
- }
-
handleHeaderClick = () => {
this.column.scrollTop();
};
@@ -63,8 +59,12 @@ class Reblogs extends ImmutablePureComponent {
this.props.dispatch(fetchReblogs(this.props.params.statusId));
};
+ handleLoadMore = debounce(() => {
+ this.props.dispatch(expandReblogs(this.props.params.statusId));
+ }, 300, { leading: true });
+
render () {
- const { intl, accountIds, multiColumn } = this.props;
+ const { intl, accountIds, hasMore, isLoading, multiColumn } = this.props;
if (!accountIds) {
return (
@@ -91,6 +91,9 @@ class Reblogs extends ImmutablePureComponent {
diff --git a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx
index c0726404e..f6984d5ad 100644
--- a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx
@@ -29,6 +29,7 @@ const messages = defineMessages({
about: { id: 'navigation_bar.about', defaultMessage: 'About' },
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
advancedInterface: { id: 'navigation_bar.advanced_interface', defaultMessage: 'Open in advanced web interface' },
+ openedInClassicInterface: { id: 'navigation_bar.opened_in_classic_interface', defaultMessage: 'Posts, accounts, and other specific pages are opened by default in the classic web interface.' },
app_settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
});
@@ -56,9 +57,13 @@ class NavigationPanel extends Component {
{transientSingleColumn && (
)}
diff --git a/app/javascript/flavours/glitch/initial_state.js b/app/javascript/flavours/glitch/initial_state.js
index 46228c330..8d277f8c1 100644
--- a/app/javascript/flavours/glitch/initial_state.js
+++ b/app/javascript/flavours/glitch/initial_state.js
@@ -100,6 +100,7 @@ export const hasMultiColumnPath = initialPath === '/'
* @typedef InitialState
* @property {Record
} accounts
* @property {InitialStateLanguage[]} languages
+ * @property {boolean=} critical_updates_pending
* @property {InitialStateMeta} meta
* @property {object} local_settings
* @property {number} max_toot_chars
@@ -160,6 +161,7 @@ export const useBlurhash = getMeta('use_blurhash');
export const usePendingItems = getMeta('use_pending_items');
export const version = getMeta('version');
export const languages = initialState?.languages;
+export const criticalUpdatesPending = initialState?.critical_updates_pending;
export const statusPageUrl = getMeta('status_page_url');
export const sso_redirect = getMeta('sso_redirect');
diff --git a/app/javascript/flavours/glitch/reducers/user_lists.js b/app/javascript/flavours/glitch/reducers/user_lists.js
index dd240e99d..d37451d00 100644
--- a/app/javascript/flavours/glitch/reducers/user_lists.js
+++ b/app/javascript/flavours/glitch/reducers/user_lists.js
@@ -44,8 +44,18 @@ import {
FEATURED_TAGS_FETCH_FAIL,
} from 'flavours/glitch/actions/featured_tags';
import {
+ REBLOGS_FETCH_REQUEST,
REBLOGS_FETCH_SUCCESS,
+ REBLOGS_FETCH_FAIL,
+ REBLOGS_EXPAND_REQUEST,
+ REBLOGS_EXPAND_SUCCESS,
+ REBLOGS_EXPAND_FAIL,
+ FAVOURITES_FETCH_REQUEST,
FAVOURITES_FETCH_SUCCESS,
+ FAVOURITES_FETCH_FAIL,
+ FAVOURITES_EXPAND_REQUEST,
+ FAVOURITES_EXPAND_SUCCESS,
+ FAVOURITES_EXPAND_FAIL,
} from 'flavours/glitch/actions/interactions';
import {
MUTES_FETCH_REQUEST,
@@ -133,9 +143,25 @@ export default function userLists(state = initialState, action) {
case FOLLOWING_EXPAND_FAIL:
return state.setIn(['following', action.id, 'isLoading'], false);
case REBLOGS_FETCH_SUCCESS:
- return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
+ return normalizeList(state, ['reblogged_by', action.id], action.accounts, action.next);
+ case REBLOGS_EXPAND_SUCCESS:
+ return appendToList(state, ['reblogged_by', action.id], action.accounts, action.next);
+ case REBLOGS_FETCH_REQUEST:
+ case REBLOGS_EXPAND_REQUEST:
+ return state.setIn(['reblogged_by', action.id, 'isLoading'], true);
+ case REBLOGS_FETCH_FAIL:
+ case REBLOGS_EXPAND_FAIL:
+ return state.setIn(['reblogged_by', action.id, 'isLoading'], false);
case FAVOURITES_FETCH_SUCCESS:
- return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
+ return normalizeList(state, ['favourited_by', action.id], action.accounts, action.next);
+ case FAVOURITES_EXPAND_SUCCESS:
+ return appendToList(state, ['favourited_by', action.id], action.accounts, action.next);
+ case FAVOURITES_FETCH_REQUEST:
+ case FAVOURITES_EXPAND_REQUEST:
+ return state.setIn(['favourited_by', action.id, 'isLoading'], true);
+ case FAVOURITES_FETCH_FAIL:
+ case FAVOURITES_EXPAND_FAIL:
+ return state.setIn(['favourited_by', action.id, 'isLoading'], false);
case NOTIFICATIONS_UPDATE:
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
case FOLLOW_REQUESTS_FETCH_SUCCESS:
diff --git a/app/javascript/flavours/glitch/styles/accounts.scss b/app/javascript/flavours/glitch/styles/accounts.scss
index ad0dfe017..b0fe21bcf 100644
--- a/app/javascript/flavours/glitch/styles/accounts.scss
+++ b/app/javascript/flavours/glitch/styles/accounts.scss
@@ -192,6 +192,8 @@
}
.account-role,
+.information-badge,
+.simple_form .overridden,
.simple_form .recommended,
.simple_form .not_recommended,
.simple_form .glitch_only {
diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss
index 7adeaeee0..2f4027b03 100644
--- a/app/javascript/flavours/glitch/styles/admin.scss
+++ b/app/javascript/flavours/glitch/styles/admin.scss
@@ -143,6 +143,11 @@ $content-width: 840px;
}
}
+ .warning a {
+ color: $gold-star;
+ font-weight: 700;
+ }
+
.simple-navigation-active-leaf a {
color: $primary-text-color;
background-color: $ui-highlight-color;
diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss
index d4860258e..39b9bd291 100644
--- a/app/javascript/flavours/glitch/styles/components/columns.scss
+++ b/app/javascript/flavours/glitch/styles/components/columns.scss
@@ -228,6 +228,22 @@ $ui-header-height: 55px;
top: -48px;
}
+.switch-to-advanced {
+ color: $classic-primary-color;
+ background-color: $classic-base-color;
+ padding: 15px;
+ border-radius: 4px;
+ margin-top: 4px;
+ margin-bottom: 12px;
+ font-size: 13px;
+ line-height: 18px;
+
+ .switch-to-advanced__toggle {
+ color: $ui-button-tertiary-color;
+ font-weight: bold;
+ }
+}
+
.column-link {
background: lighten($ui-base-color, 8%);
color: $primary-text-color;
@@ -961,7 +977,8 @@ $ui-header-height: 55px;
}
}
-.dismissable-banner {
+.dismissable-banner,
+.warning-banner {
position: relative;
margin: 10px;
margin-bottom: 5px;
@@ -1039,6 +1056,21 @@ $ui-header-height: 55px;
}
}
+.warning-banner {
+ border: 1px solid $warning-red;
+ background: rgba($warning-red, 0.15);
+
+ &__message {
+ h1 {
+ color: $warning-red;
+ }
+
+ a {
+ color: $primary-text-color;
+ }
+ }
+}
+
.hashtag-header {
border-bottom: 1px solid lighten($ui-base-color, 8%);
padding: 15px;
diff --git a/app/javascript/flavours/glitch/styles/components/search.scss b/app/javascript/flavours/glitch/styles/components/search.scss
index 99ed697e3..be35077dc 100644
--- a/app/javascript/flavours/glitch/styles/components/search.scss
+++ b/app/javascript/flavours/glitch/styles/components/search.scss
@@ -25,6 +25,12 @@
}
&__menu {
+ margin-bottom: 20px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
&__message {
color: $dark-text-color;
padding: 0 10px;
diff --git a/app/javascript/flavours/glitch/styles/components/single_column.scss b/app/javascript/flavours/glitch/styles/components/single_column.scss
index 2f8f7e2dd..7efcf0c09 100644
--- a/app/javascript/flavours/glitch/styles/components/single_column.scss
+++ b/app/javascript/flavours/glitch/styles/components/single_column.scss
@@ -120,6 +120,7 @@
.filter-form {
display: flex;
+ flex-wrap: wrap;
}
.autosuggest-textarea__textarea {
diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss
index 2b7c02f11..b8fc4a653 100644
--- a/app/javascript/flavours/glitch/styles/forms.scss
+++ b/app/javascript/flavours/glitch/styles/forms.scss
@@ -103,6 +103,7 @@ code {
}
}
+ .overridden,
.recommended,
.not_recommended,
.glitch_only {
@@ -1187,14 +1188,14 @@ code {
}
li:first-child .label {
- left: auto;
inset-inline-start: 0;
+ inset-inline-end: auto;
text-align: start;
transform: none;
}
li:last-child .label {
- left: auto;
+ inset-inline-start: auto;
inset-inline-end: 0;
text-align: end;
transform: none;
diff --git a/app/javascript/flavours/glitch/styles/tables.scss b/app/javascript/flavours/glitch/styles/tables.scss
index b583d3d8e..44ef00ba7 100644
--- a/app/javascript/flavours/glitch/styles/tables.scss
+++ b/app/javascript/flavours/glitch/styles/tables.scss
@@ -12,6 +12,11 @@
border-top: 1px solid $ui-base-color;
text-align: start;
background: darken($ui-base-color, 4%);
+
+ &.critical {
+ font-weight: 700;
+ color: $gold-star;
+ }
}
& > thead > tr > th {
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 5f7b8e949..e68afcbf9 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -84,6 +84,7 @@ const messages = defineMessages({
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
open: { id: 'compose.published.open', defaultMessage: 'Open' },
published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
+ saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
});
export const ensureComposeIsVisible = (getState, routerHistory) => {
@@ -246,7 +247,7 @@ export function submitCompose(routerHistory) {
}
dispatch(showAlert({
- message: messages.published,
+ message: statusId === null ? messages.published : messages.saved,
action: messages.open,
dismissAfter: 10000,
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js
index 092a67ea7..7d0144438 100644
--- a/app/javascript/mastodon/actions/interactions.js
+++ b/app/javascript/mastodon/actions/interactions.js
@@ -1,11 +1,16 @@
-import api from '../api';
+import api, { getLinks } from '../api';
+import { fetchRelationships } from './accounts';
import { importFetchedAccounts, importFetchedStatus } from './importer';
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
export const REBLOG_FAIL = 'REBLOG_FAIL';
+export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
+export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
+export const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL';
+
export const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
export const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
export const FAVOURITE_FAIL = 'FAVOURITE_FAIL';
@@ -26,6 +31,10 @@ export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
+export const FAVOURITES_EXPAND_REQUEST = 'FAVOURITES_EXPAND_REQUEST';
+export const FAVOURITES_EXPAND_SUCCESS = 'FAVOURITES_EXPAND_SUCCESS';
+export const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL';
+
export const PIN_REQUEST = 'PIN_REQUEST';
export const PIN_SUCCESS = 'PIN_SUCCESS';
export const PIN_FAIL = 'PIN_FAIL';
@@ -273,8 +282,10 @@ export function fetchReblogs(id) {
dispatch(fetchReblogsRequest(id));
api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
- dispatch(fetchReblogsSuccess(id, response.data));
+ dispatch(fetchReblogsSuccess(id, response.data, next ? next.uri : null));
+ dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => {
dispatch(fetchReblogsFail(id, error));
});
@@ -288,17 +299,62 @@ export function fetchReblogsRequest(id) {
};
}
-export function fetchReblogsSuccess(id, accounts) {
+export function fetchReblogsSuccess(id, accounts, next) {
return {
type: REBLOGS_FETCH_SUCCESS,
id,
accounts,
+ next,
};
}
export function fetchReblogsFail(id, error) {
return {
type: REBLOGS_FETCH_FAIL,
+ id,
+ error,
+ };
+}
+
+export function expandReblogs(id) {
+ return (dispatch, getState) => {
+ const url = getState().getIn(['user_lists', 'reblogged_by', id, 'next']);
+ if (url === null) {
+ return;
+ }
+
+ dispatch(expandReblogsRequest(id));
+
+ api(getState).get(url).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+ dispatch(importFetchedAccounts(response.data));
+ dispatch(expandReblogsSuccess(id, response.data, next ? next.uri : null));
+ dispatch(fetchRelationships(response.data.map(item => item.id)));
+ }).catch(error => dispatch(expandReblogsFail(id, error)));
+ };
+}
+
+export function expandReblogsRequest(id) {
+ return {
+ type: REBLOGS_EXPAND_REQUEST,
+ id,
+ };
+}
+
+export function expandReblogsSuccess(id, accounts, next) {
+ return {
+ type: REBLOGS_EXPAND_SUCCESS,
+ id,
+ accounts,
+ next,
+ };
+}
+
+export function expandReblogsFail(id, error) {
+ return {
+ type: REBLOGS_EXPAND_FAIL,
+ id,
error,
};
}
@@ -308,8 +364,10 @@ export function fetchFavourites(id) {
dispatch(fetchFavouritesRequest(id));
api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
- dispatch(fetchFavouritesSuccess(id, response.data));
+ dispatch(fetchFavouritesSuccess(id, response.data, next ? next.uri : null));
+ dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => {
dispatch(fetchFavouritesFail(id, error));
});
@@ -323,17 +381,62 @@ export function fetchFavouritesRequest(id) {
};
}
-export function fetchFavouritesSuccess(id, accounts) {
+export function fetchFavouritesSuccess(id, accounts, next) {
return {
type: FAVOURITES_FETCH_SUCCESS,
id,
accounts,
+ next,
};
}
export function fetchFavouritesFail(id, error) {
return {
type: FAVOURITES_FETCH_FAIL,
+ id,
+ error,
+ };
+}
+
+export function expandFavourites(id) {
+ return (dispatch, getState) => {
+ const url = getState().getIn(['user_lists', 'favourited_by', id, 'next']);
+ if (url === null) {
+ return;
+ }
+
+ dispatch(expandFavouritesRequest(id));
+
+ api(getState).get(url).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+ dispatch(importFetchedAccounts(response.data));
+ dispatch(expandFavouritesSuccess(id, response.data, next ? next.uri : null));
+ dispatch(fetchRelationships(response.data.map(item => item.id)));
+ }).catch(error => dispatch(expandFavouritesFail(id, error)));
+ };
+}
+
+export function expandFavouritesRequest(id) {
+ return {
+ type: FAVOURITES_EXPAND_REQUEST,
+ id,
+ };
+}
+
+export function expandFavouritesSuccess(id, accounts, next) {
+ return {
+ type: FAVOURITES_EXPAND_SUCCESS,
+ id,
+ accounts,
+ next,
+ };
+}
+
+export function expandFavouritesFail(id, error) {
+ return {
+ type: FAVOURITES_EXPAND_FAIL,
+ id,
error,
};
}
diff --git a/app/javascript/mastodon/components/hashtag_bar.tsx b/app/javascript/mastodon/components/hashtag_bar.tsx
index 674c481b8..d45a6e20e 100644
--- a/app/javascript/mastodon/components/hashtag_bar.tsx
+++ b/app/javascript/mastodon/components/hashtag_bar.tsx
@@ -10,8 +10,8 @@ import { groupBy, minBy } from 'lodash';
import { getStatusContent } from './status_content';
-// About two lines on desktop
-const VISIBLE_HASHTAGS = 7;
+// Fit on a single line on desktop
+const VISIBLE_HASHTAGS = 3;
// Those types are not correct, they need to be replaced once this part of the state is typed
export type TagLike = Record<{ name: string }>;
@@ -210,7 +210,7 @@ const HashtagBar: React.FC<{
const revealedHashtags = expanded
? hashtags
- : hashtags.slice(0, VISIBLE_HASHTAGS - 1);
+ : hashtags.slice(0, VISIBLE_HASHTAGS);
return (
diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx
index 30692d1cd..e1728910e 100644
--- a/app/javascript/mastodon/components/status.jsx
+++ b/app/javascript/mastodon/components/status.jsx
@@ -550,7 +550,7 @@ class Status extends ImmutablePureComponent {
return (
-
+
{prepend}
diff --git a/app/javascript/mastodon/features/compose/components/search.jsx b/app/javascript/mastodon/features/compose/components/search.jsx
index 682f8d3c8..848b81263 100644
--- a/app/javascript/mastodon/features/compose/components/search.jsx
+++ b/app/javascript/mastodon/features/compose/components/search.jsx
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { defineMessages, injectIntl, FormattedMessage, FormattedList } from 'react-intl';
import classNames from 'classnames';
@@ -45,6 +45,16 @@ class Search extends PureComponent {
options: [],
};
+ defaultOptions = [
+ { label: <>has: >, action: e => { e.preventDefault(); this._insertText('has:') } },
+ { label: <>is: >, action: e => { e.preventDefault(); this._insertText('is:') } },
+ { label: <>language: >, action: e => { e.preventDefault(); this._insertText('language:') } },
+ { label: <>from: >, action: e => { e.preventDefault(); this._insertText('from:') } },
+ { label: <>before: >, action: e => { e.preventDefault(); this._insertText('before:') } },
+ { label: <>during: >, action: e => { e.preventDefault(); this._insertText('during:') } },
+ { label: <>after: >, action: e => { e.preventDefault(); this._insertText('after:') } },
+ ];
+
setRef = c => {
this.searchForm = c;
};
@@ -70,7 +80,7 @@ class Search extends PureComponent {
handleKeyDown = (e) => {
const { selectedOption } = this.state;
- const options = this._getOptions();
+ const options = searchEnabled ? this._getOptions().concat(this.defaultOptions) : this._getOptions();
switch(e.key) {
case 'Escape':
@@ -100,11 +110,9 @@ class Search extends PureComponent {
if (selectedOption === -1) {
this._submit();
} else if (options.length > 0) {
- options[selectedOption].action();
+ options[selectedOption].action(e);
}
- this._unfocus();
-
break;
case 'Delete':
if (selectedOption > -1 && options.length > 0) {
@@ -147,6 +155,7 @@ class Search extends PureComponent {
router.history.push(`/tags/${query}`);
onClickSearchResult(query, 'hashtag');
+ this._unfocus();
};
handleAccountClick = () => {
@@ -157,6 +166,7 @@ class Search extends PureComponent {
router.history.push(`/@${query}`);
onClickSearchResult(query, 'account');
+ this._unfocus();
};
handleURLClick = () => {
@@ -164,6 +174,7 @@ class Search extends PureComponent {
const { value, onOpenURL } = this.props;
onOpenURL(value, router.history);
+ this._unfocus();
};
handleStatusSearch = () => {
@@ -182,6 +193,8 @@ class Search extends PureComponent {
} else if (search.get('type') === 'hashtag') {
router.history.push(`/tags/${search.get('q')}`);
}
+
+ this._unfocus();
};
handleForgetRecentSearchClick = search => {
@@ -194,6 +207,18 @@ class Search extends PureComponent {
document.querySelector('.ui').parentElement.focus();
}
+ _insertText (text) {
+ const { value, onChange } = this.props;
+
+ if (value === '') {
+ onChange(text);
+ } else if (value[value.length - 1] === ' ') {
+ onChange(`${value}${text}`);
+ } else {
+ onChange(`${value} ${text}`);
+ }
+ }
+
_submit (type) {
const { onSubmit, openInRoute } = this.props;
const { router } = this.context;
@@ -203,6 +228,8 @@ class Search extends PureComponent {
if (openInRoute) {
router.history.push('/search');
}
+
+ this._unfocus();
}
_getOptions () {
@@ -325,6 +352,20 @@ class Search extends PureComponent {
>
)}
+
+ {searchEnabled && (
+ <>
+
+
+
+ {this.defaultOptions.map(({ key, label, action }, i) => (
+
+ ))}
+
+ >
+ )}
);
diff --git a/app/javascript/mastodon/features/favourites/index.jsx b/app/javascript/mastodon/features/favourites/index.jsx
index bfde78708..b8ba94872 100644
--- a/app/javascript/mastodon/features/favourites/index.jsx
+++ b/app/javascript/mastodon/features/favourites/index.jsx
@@ -8,7 +8,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
-import { fetchFavourites } from 'mastodon/actions/interactions';
+import { debounce } from 'lodash';
+
+import { fetchFavourites, expandFavourites } from 'mastodon/actions/interactions';
import ColumnHeader from 'mastodon/components/column_header';
import { Icon } from 'mastodon/components/icon';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
@@ -21,7 +23,9 @@ const messages = defineMessages({
});
const mapStateToProps = (state, props) => ({
- accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]),
+ accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'items']),
+ hasMore: !!state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'next']),
+ isLoading: state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'isLoading'], true),
});
class Favourites extends ImmutablePureComponent {
@@ -30,6 +34,8 @@ class Favourites extends ImmutablePureComponent {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list,
+ hasMore: PropTypes.bool,
+ isLoading: PropTypes.bool,
multiColumn: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
@@ -40,18 +46,16 @@ class Favourites extends ImmutablePureComponent {
}
}
- UNSAFE_componentWillReceiveProps (nextProps) {
- if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
- this.props.dispatch(fetchFavourites(nextProps.params.statusId));
- }
- }
-
handleRefresh = () => {
this.props.dispatch(fetchFavourites(this.props.params.statusId));
};
+ handleLoadMore = debounce(() => {
+ this.props.dispatch(expandFavourites(this.props.params.statusId));
+ }, 300, { leading: true });
+
render () {
- const { intl, accountIds, multiColumn } = this.props;
+ const { intl, accountIds, hasMore, isLoading, multiColumn } = this.props;
if (!accountIds) {
return (
@@ -75,6 +79,9 @@ class Favourites extends ImmutablePureComponent {
diff --git a/app/javascript/mastodon/features/home_timeline/components/critical_update_banner.tsx b/app/javascript/mastodon/features/home_timeline/components/critical_update_banner.tsx
new file mode 100644
index 000000000..d0dd2b6ac
--- /dev/null
+++ b/app/javascript/mastodon/features/home_timeline/components/critical_update_banner.tsx
@@ -0,0 +1,26 @@
+import { FormattedMessage } from 'react-intl';
+
+export const CriticalUpdateBanner = () => (
+
+
+
+
+
+
+ {' '}
+
+
+
+
+
+
+);
diff --git a/app/javascript/mastodon/features/home_timeline/index.jsx b/app/javascript/mastodon/features/home_timeline/index.jsx
index 1cd6edd7a..8ff037794 100644
--- a/app/javascript/mastodon/features/home_timeline/index.jsx
+++ b/app/javascript/mastodon/features/home_timeline/index.jsx
@@ -14,7 +14,7 @@ import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/an
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container';
-import { me } from 'mastodon/initial_state';
+import { me, criticalUpdatesPending } from 'mastodon/initial_state';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { expandHomeTimeline } from '../../actions/timelines';
@@ -23,6 +23,7 @@ import ColumnHeader from '../../components/column_header';
import StatusListContainer from '../ui/containers/status_list_container';
import { ColumnSettings } from './components/column_settings';
+import { CriticalUpdateBanner } from './components/critical_update_banner';
import { ExplorePrompt } from './components/explore_prompt';
const messages = defineMessages({
@@ -156,8 +157,9 @@ class HomeTimeline extends PureComponent {
const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
const pinned = !!columnId;
const { signedIn } = this.context.identity;
+ const banners = [];
- let announcementsButton, banner;
+ let announcementsButton;
if (hasAnnouncements) {
announcementsButton = (
@@ -173,8 +175,12 @@ class HomeTimeline extends PureComponent {
);
}
+ if (criticalUpdatesPending) {
+ banners.push();
+ }
+
if (tooSlow) {
- banner = ;
+ banners.push();
}
return (
@@ -196,7 +202,7 @@ class HomeTimeline extends PureComponent {
{signedIn ? (
({
- accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]),
+ accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'items']),
+ hasMore: !!state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'next']),
+ isLoading: state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'isLoading'], true),
});
class Reblogs extends ImmutablePureComponent {
@@ -31,6 +35,8 @@ class Reblogs extends ImmutablePureComponent {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list,
+ hasMore: PropTypes.bool,
+ isLoading: PropTypes.bool,
multiColumn: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
@@ -39,20 +45,18 @@ class Reblogs extends ImmutablePureComponent {
if (!this.props.accountIds) {
this.props.dispatch(fetchReblogs(this.props.params.statusId));
}
- }
-
- UNSAFE_componentWillReceiveProps(nextProps) {
- if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
- this.props.dispatch(fetchReblogs(nextProps.params.statusId));
- }
- }
+ };
handleRefresh = () => {
this.props.dispatch(fetchReblogs(this.props.params.statusId));
};
+ handleLoadMore = debounce(() => {
+ this.props.dispatch(expandReblogs(this.props.params.statusId));
+ }, 300, { leading: true });
+
render () {
- const { intl, accountIds, multiColumn } = this.props;
+ const { intl, accountIds, hasMore, isLoading, multiColumn } = this.props;
if (!accountIds) {
return (
@@ -76,6 +80,9 @@ class Reblogs extends ImmutablePureComponent {
diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
index d36abf8f1..8006ca89a 100644
--- a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
+++ b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
@@ -31,6 +31,7 @@ const messages = defineMessages({
about: { id: 'navigation_bar.about', defaultMessage: 'About' },
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
advancedInterface: { id: 'navigation_bar.advanced_interface', defaultMessage: 'Open in advanced web interface' },
+ openedInClassicInterface: { id: 'navigation_bar.opened_in_classic_interface', defaultMessage: 'Posts, accounts, and other specific pages are opened by default in the classic web interface.' },
});
class NavigationPanel extends Component {
@@ -57,12 +58,17 @@ class NavigationPanel extends Component {
{signedIn && (
diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js
index 13b80c092..34fd5439d 100644
--- a/app/javascript/mastodon/initial_state.js
+++ b/app/javascript/mastodon/initial_state.js
@@ -87,6 +87,7 @@
* @typedef InitialState
* @property {Record} accounts
* @property {InitialStateLanguage[]} languages
+ * @property {boolean=} critical_updates_pending
* @property {InitialStateMeta} meta
* @property {number} max_toot_chars
*/
@@ -141,6 +142,7 @@ export const useBlurhash = getMeta('use_blurhash');
export const usePendingItems = getMeta('use_pending_items');
export const version = getMeta('version');
export const languages = initialState?.languages;
+export const criticalUpdatesPending = initialState?.critical_updates_pending;
// @ts-expect-error
export const statusPageUrl = getMeta('status_page_url');
export const sso_redirect = getMeta('sso_redirect');
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index d4cd8c794..13cddba72 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -137,6 +137,7 @@
"compose.language.search": "Search languages...",
"compose.published.body": "Post published.",
"compose.published.open": "Open",
+ "compose.saved.body": "Post saved.",
"compose_form.direct_message_warning_learn_more": "Learn more",
"compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any sensitive information over Mastodon.",
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is not public. Only public posts can be searched by hashtag.",
@@ -309,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.title": "This is your home base within Mastodon.",
"home.hide_announcements": "Hide announcements",
+ "home.pending_critical_update.body": "Please update your Mastodon server as soon as possible!",
+ "home.pending_critical_update.link": "See updates",
+ "home.pending_critical_update.title": "Critical security update available!",
"home.show_announcements": "Show announcements",
"interaction_modal.description.favourite": "With an account on Mastodon, you can favorite this post to let the author know you appreciate it and save it for later.",
"interaction_modal.description.follow": "With an account on Mastodon, you can follow {name} to receive their posts in your home feed.",
@@ -410,6 +414,7 @@
"navigation_bar.lists": "Lists",
"navigation_bar.logout": "Logout",
"navigation_bar.mutes": "Muted users",
+ "navigation_bar.opened_in_classic_interface": "Posts, accounts, and other specific pages are opened by default in the classic web interface.",
"navigation_bar.personal": "Personal",
"navigation_bar.pins": "Pinned posts",
"navigation_bar.preferences": "Preferences",
@@ -585,8 +590,12 @@
"search.quick_action.open_url": "Open URL in Mastodon",
"search.quick_action.status_search": "Posts matching {x}",
"search.search_or_paste": "Search or paste URL",
+ "search_popout.language_code": "ISO language code",
+ "search_popout.options": "Search options",
"search_popout.quick_actions": "Quick actions",
"search_popout.recent": "Recent searches",
+ "search_popout.specific_date": "specific date",
+ "search_popout.user": "user",
"search_results.accounts": "Profiles",
"search_results.all": "All",
"search_results.hashtags": "Hashtags",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 2bef3bb4b..116ed66d0 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -409,6 +409,7 @@
"navigation_bar.lists": "Listes",
"navigation_bar.logout": "Déconnexion",
"navigation_bar.mutes": "Comptes masqués",
+ "navigation_bar.opened_in_classic_interface": "Les messages, les comptes et les pages spécifiques sont ouvertes dans l’interface classique.",
"navigation_bar.personal": "Personnel",
"navigation_bar.pins": "Messages épinglés",
"navigation_bar.preferences": "Préférences",
diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js
index e33d365c9..089899398 100644
--- a/app/javascript/mastodon/reducers/user_lists.js
+++ b/app/javascript/mastodon/reducers/user_lists.js
@@ -45,8 +45,18 @@ import {
BLOCKS_EXPAND_FAIL,
} from '../actions/blocks';
import {
+ REBLOGS_FETCH_REQUEST,
REBLOGS_FETCH_SUCCESS,
+ REBLOGS_FETCH_FAIL,
+ REBLOGS_EXPAND_REQUEST,
+ REBLOGS_EXPAND_SUCCESS,
+ REBLOGS_EXPAND_FAIL,
+ FAVOURITES_FETCH_REQUEST,
FAVOURITES_FETCH_SUCCESS,
+ FAVOURITES_FETCH_FAIL,
+ FAVOURITES_EXPAND_REQUEST,
+ FAVOURITES_EXPAND_SUCCESS,
+ FAVOURITES_EXPAND_FAIL,
} from '../actions/interactions';
import {
MUTES_FETCH_REQUEST,
@@ -134,9 +144,25 @@ export default function userLists(state = initialState, action) {
case FOLLOWING_EXPAND_FAIL:
return state.setIn(['following', action.id, 'isLoading'], false);
case REBLOGS_FETCH_SUCCESS:
- return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
+ return normalizeList(state, ['reblogged_by', action.id], action.accounts, action.next);
+ case REBLOGS_EXPAND_SUCCESS:
+ return appendToList(state, ['reblogged_by', action.id], action.accounts, action.next);
+ case REBLOGS_FETCH_REQUEST:
+ case REBLOGS_EXPAND_REQUEST:
+ return state.setIn(['reblogged_by', action.id, 'isLoading'], true);
+ case REBLOGS_FETCH_FAIL:
+ case REBLOGS_EXPAND_FAIL:
+ return state.setIn(['reblogged_by', action.id, 'isLoading'], false);
case FAVOURITES_FETCH_SUCCESS:
- return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
+ return normalizeList(state, ['favourited_by', action.id], action.accounts, action.next);
+ case FAVOURITES_EXPAND_SUCCESS:
+ return appendToList(state, ['favourited_by', action.id], action.accounts, action.next);
+ case FAVOURITES_FETCH_REQUEST:
+ case FAVOURITES_EXPAND_REQUEST:
+ return state.setIn(['favourited_by', action.id, 'isLoading'], true);
+ case FAVOURITES_FETCH_FAIL:
+ case FAVOURITES_EXPAND_FAIL:
+ return state.setIn(['favourited_by', action.id, 'isLoading'], false);
case NOTIFICATIONS_UPDATE:
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
case FOLLOW_REQUESTS_FETCH_SUCCESS:
diff --git a/app/javascript/mastodon/test_setup.js b/app/javascript/mastodon/test_setup.js
index 666127af3..7b0828bfa 100644
--- a/app/javascript/mastodon/test_setup.js
+++ b/app/javascript/mastodon/test_setup.js
@@ -1 +1 @@
-import '@testing-library/jest-dom/extend-expect';
+import '@testing-library/jest-dom';
diff --git a/app/javascript/packs/public.jsx b/app/javascript/packs/public.jsx
index 9f959036a..4bfbf7e25 100644
--- a/app/javascript/packs/public.jsx
+++ b/app/javascript/packs/public.jsx
@@ -7,7 +7,6 @@ import { defineMessages } from 'react-intl';
import { delegate } from '@rails/ujs';
import axios from 'axios';
-import { createBrowserHistory } from 'history';
import { throttle } from 'lodash';
import { start } from '../mastodon/common';
@@ -31,23 +30,6 @@ const messages = defineMessages({
function loaded() {
const { messages: localeData } = getLocale();
- const scrollToDetailedStatus = () => {
- const history = createBrowserHistory();
- const detailedStatuses = document.querySelectorAll('.public-layout .detailed-status');
- const location = history.location;
-
- if (detailedStatuses.length === 1 && (!location.state || !location.state.scrolledToDetailedStatus)) {
- detailedStatuses[0].scrollIntoView();
- history.replace(location.pathname, { ...location.state, scrolledToDetailedStatus: true });
- }
- };
-
- const getEmojiAnimationHandler = (swapTo) => {
- return ({ target }) => {
- target.src = target.getAttribute(swapTo);
- };
- };
-
const locale = document.documentElement.lang;
const dateTimeFormat = new Intl.DateTimeFormat(locale, {
@@ -141,27 +123,21 @@ function loaded() {
const root = createRoot(content);
root.render();
document.body.appendChild(content);
- scrollToDetailedStatus();
})
.catch(error => {
console.error(error);
- scrollToDetailedStatus();
});
- } else {
- scrollToDetailedStatus();
}
- delegate(document, '#user_account_attributes_username', 'input', throttle(() => {
- const username = document.getElementById('user_account_attributes_username');
-
- if (username.value && username.value.length > 0) {
- axios.get('/api/v1/accounts/lookup', { params: { acct: username.value } }).then(() => {
- username.setCustomValidity(formatMessage(messages.usernameTaken));
+ delegate(document, '#user_account_attributes_username', 'input', throttle(({ target }) => {
+ if (target.value && target.value.length > 0) {
+ axios.get('/api/v1/accounts/lookup', { params: { acct: target.value } }).then(() => {
+ target.setCustomValidity(formatMessage(messages.usernameTaken));
}).catch(() => {
- username.setCustomValidity('');
+ target.setCustomValidity('');
});
} else {
- username.setCustomValidity('');
+ target.setCustomValidity('');
}
}, 500, { leading: false, trailing: true }));
@@ -179,9 +155,6 @@ function loaded() {
}
});
- delegate(document, '.custom-emoji', 'mouseover', getEmojiAnimationHandler('data-original'));
- delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));
-
delegate(document, '.status__content__spoiler-link', 'click', function() {
const statusEl = this.parentNode.parentNode;
@@ -230,6 +203,9 @@ delegate(document, '.sidebar__toggle__icon', 'keydown', e => {
}
});
+delegate(document, '.custom-emoji', 'mouseover', ({ target }) => target.src = target.getAttribute('data-original'));
+delegate(document, '.custom-emoji', 'mouseout', ({ target }) => target.src = target.getAttribute('data-static'));
+
// Empty the honeypot fields in JS in case something like an extension
// automatically filled them.
delegate(document, '#registration_new_user,#new_user', 'submit', () => {
diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss
index 2a5285ee0..80d6c13ce 100644
--- a/app/javascript/styles/mastodon/accounts.scss
+++ b/app/javascript/styles/mastodon/accounts.scss
@@ -188,6 +188,7 @@
}
.information-badge,
+.simple_form .overridden,
.simple_form .recommended,
.simple_form .not_recommended {
display: inline-block;
@@ -204,6 +205,7 @@
}
.information-badge,
+.simple_form .overridden,
.simple_form .recommended,
.simple_form .not_recommended {
background-color: rgba($ui-secondary-color, 0.1);
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 6bfb23a46..89c2adaea 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -143,6 +143,11 @@ $content-width: 840px;
}
}
+ .warning a {
+ color: $gold-star;
+ font-weight: 700;
+ }
+
.simple-navigation-active-leaf a {
color: $primary-text-color;
background-color: $ui-highlight-color;
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index a5d4eee5e..470bee2cf 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -2381,6 +2381,7 @@ $ui-header-height: 55px;
.filter-form {
display: flex;
+ flex-wrap: wrap;
}
.autosuggest-textarea__textarea {
@@ -3270,6 +3271,22 @@ $ui-header-height: 55px;
border-color: $ui-highlight-color;
}
+.switch-to-advanced {
+ color: $classic-primary-color;
+ background-color: $classic-base-color;
+ padding: 15px;
+ border-radius: 4px;
+ margin-top: 4px;
+ margin-bottom: 12px;
+ font-size: 13px;
+ line-height: 18px;
+
+ .switch-to-advanced__toggle {
+ color: $ui-button-tertiary-color;
+ font-weight: bold;
+ }
+}
+
.column-link {
background: lighten($ui-base-color, 8%);
color: $primary-text-color;
@@ -4991,6 +5008,12 @@ a.status-card {
}
&__menu {
+ margin-bottom: 20px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
&__message {
color: $dark-text-color;
padding: 0 10px;
@@ -8837,7 +8860,8 @@ noscript {
}
}
-.dismissable-banner {
+.dismissable-banner,
+.warning-banner {
position: relative;
margin: 10px;
margin-bottom: 5px;
@@ -8915,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 {
position: relative;
overflow: hidden;
@@ -9302,19 +9341,24 @@ noscript {
display: flex;
flex-wrap: wrap;
font-size: 14px;
+ line-height: 18px;
gap: 4px;
+ color: $darker-text-color;
a {
display: inline-flex;
- color: $dark-text-color;
+ color: inherit;
text-decoration: none;
- &:hover {
- text-decoration: none;
-
- span {
- text-decoration: underline;
- }
+ &:hover span {
+ text-decoration: underline;
}
}
+
+ .link-button {
+ color: inherit;
+ font-size: inherit;
+ line-height: inherit;
+ padding: 0;
+ }
}
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index beb45ab6e..0f8eecee0 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -103,6 +103,7 @@ code {
}
}
+ .overridden,
.recommended,
.not_recommended {
position: absolute;
@@ -1185,14 +1186,14 @@ code {
}
li:first-child .label {
- left: auto;
inset-inline-start: 0;
+ inset-inline-end: auto;
text-align: start;
transform: none;
}
li:last-child .label {
- left: auto;
+ inset-inline-start: auto;
inset-inline-end: 0;
text-align: end;
transform: none;
diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss
index 38cfc8727..dd5b483ec 100644
--- a/app/javascript/styles/mastodon/tables.scss
+++ b/app/javascript/styles/mastodon/tables.scss
@@ -12,6 +12,11 @@
border-top: 1px solid $ui-base-color;
text-align: start;
background: darken($ui-base-color, 4%);
+
+ &.critical {
+ font-weight: 700;
+ color: $gold-star;
+ }
}
& > thead > tr > th {
diff --git a/app/lib/admin/metrics/dimension/software_versions_dimension.rb b/app/lib/admin/metrics/dimension/software_versions_dimension.rb
index 9ab3776c9..72a98a88a 100644
--- a/app/lib/admin/metrics/dimension/software_versions_dimension.rb
+++ b/app/lib/admin/metrics/dimension/software_versions_dimension.rb
@@ -10,7 +10,7 @@ class Admin::Metrics::Dimension::SoftwareVersionsDimension < Admin::Metrics::Dim
protected
def perform_query
- [mastodon_version, ruby_version, postgresql_version, redis_version]
+ [mastodon_version, ruby_version, postgresql_version, redis_version, elasticsearch_version].compact
end
def mastodon_version
@@ -57,6 +57,22 @@ class Admin::Metrics::Dimension::SoftwareVersionsDimension < Admin::Metrics::Dim
}
end
+ def elasticsearch_version
+ return unless Chewy.enabled?
+
+ client_info = Chewy.client.info
+ version = client_info.dig('version', 'number')
+
+ {
+ key: 'elasticsearch',
+ human_key: client_info.dig('version', 'distribution') == 'opensearch' ? 'OpenSearch' : 'Elasticsearch',
+ value: version,
+ human_value: version,
+ }
+ rescue Faraday::ConnectionFailed, Elasticsearch::Transport::Transport::Error
+ nil
+ end
+
def redis_info
@redis_info ||= if redis.is_a?(Redis::Namespace)
redis.redis.info
diff --git a/app/lib/admin/system_check.rb b/app/lib/admin/system_check.rb
index 89dfcef9f..25c88341a 100644
--- a/app/lib/admin/system_check.rb
+++ b/app/lib/admin/system_check.rb
@@ -2,6 +2,7 @@
class Admin::SystemCheck
ACTIVE_CHECKS = [
+ Admin::SystemCheck::SoftwareVersionCheck,
Admin::SystemCheck::MediaPrivacyCheck,
Admin::SystemCheck::DatabaseSchemaCheck,
Admin::SystemCheck::SidekiqProcessCheck,
diff --git a/app/lib/admin/system_check/elasticsearch_check.rb b/app/lib/admin/system_check/elasticsearch_check.rb
index 91070756c..c0a1a21e8 100644
--- a/app/lib/admin/system_check/elasticsearch_check.rb
+++ b/app/lib/admin/system_check/elasticsearch_check.rb
@@ -6,6 +6,7 @@ class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck
AccountsIndex,
TagsIndex,
StatusesIndex,
+ PublicStatusesIndex,
].freeze
def skip?
@@ -85,7 +86,7 @@ class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck
def mismatched_indexes
@mismatched_indexes ||= INDEXES.filter_map do |klass|
- klass.index_name if Chewy.client.indices.get_mapping[klass.index_name]&.deep_symbolize_keys != klass.mappings_hash
+ klass.base_name if Chewy.client.indices.get_mapping[klass.index_name]&.deep_symbolize_keys != klass.mappings_hash
end
end
diff --git a/app/lib/admin/system_check/software_version_check.rb b/app/lib/admin/system_check/software_version_check.rb
new file mode 100644
index 000000000..e142feddf
--- /dev/null
+++ b/app/lib/admin/system_check/software_version_check.rb
@@ -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
diff --git a/app/lib/importer/accounts_index_importer.rb b/app/lib/importer/accounts_index_importer.rb
index fd869c396..d8b919027 100644
--- a/app/lib/importer/accounts_index_importer.rb
+++ b/app/lib/importer/accounts_index_importer.rb
@@ -4,10 +4,10 @@ class Importer::AccountsIndexImporter < Importer::BaseImporter
def import!
scope.includes(:account_stat).find_in_batches(batch_size: @batch_size) do |tmp|
in_work_unit(tmp) do |accounts|
- bulk = Chewy::Index::Import::BulkBuilder.new(index, to_index: accounts).bulk_body
+ bulk = build_bulk_body(accounts)
- indexed = bulk.count { |entry| entry[:index] }
- deleted = bulk.count { |entry| entry[:delete] }
+ indexed = bulk.size
+ deleted = 0
Chewy::Index::Import::BulkRequest.new(index).perform(bulk)
diff --git a/app/lib/importer/base_importer.rb b/app/lib/importer/base_importer.rb
index cc1b7b44d..a21557d30 100644
--- a/app/lib/importer/base_importer.rb
+++ b/app/lib/importer/base_importer.rb
@@ -68,6 +68,14 @@ class Importer::BaseImporter
protected
+ def build_bulk_body(to_import)
+ # Specialize `Chewy::Index::Import::BulkBuilder#bulk_body` to avoid a few
+ # inefficiencies, as none of our fields or join fields and we do not need
+ # `BulkBuilder`'s versatility.
+ crutches = Chewy::Index::Crutch::Crutches.new index, to_import
+ to_import.map { |object| { index: { _id: object.id, data: index.compose(object, crutches, fields: []) } } }
+ end
+
def in_work_unit(...)
work_unit = Concurrent::Promises.future_on(@executor, ...)
diff --git a/app/lib/importer/instances_index_importer.rb b/app/lib/importer/instances_index_importer.rb
index 7318b51b5..ebdceb72e 100644
--- a/app/lib/importer/instances_index_importer.rb
+++ b/app/lib/importer/instances_index_importer.rb
@@ -4,10 +4,10 @@ class Importer::InstancesIndexImporter < Importer::BaseImporter
def import!
index.adapter.default_scope.find_in_batches(batch_size: @batch_size) do |tmp|
in_work_unit(tmp) do |instances|
- bulk = Chewy::Index::Import::BulkBuilder.new(index, to_index: instances).bulk_body
+ bulk = build_bulk_body(instances)
- indexed = bulk.count { |entry| entry[:index] }
- deleted = bulk.count { |entry| entry[:delete] }
+ indexed = bulk.size
+ deleted = 0
Chewy::Index::Import::BulkRequest.new(index).perform(bulk)
diff --git a/app/lib/importer/public_statuses_index_importer.rb b/app/lib/importer/public_statuses_index_importer.rb
new file mode 100644
index 000000000..ebaac3794
--- /dev/null
+++ b/app/lib/importer/public_statuses_index_importer.rb
@@ -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
diff --git a/app/lib/importer/statuses_index_importer.rb b/app/lib/importer/statuses_index_importer.rb
index b0721c2e0..08ad3e379 100644
--- a/app/lib/importer/statuses_index_importer.rb
+++ b/app/lib/importer/statuses_index_importer.rb
@@ -13,32 +13,25 @@ class Importer::StatusesIndexImporter < Importer::BaseImporter
scope.find_in_batches(batch_size: @batch_size) do |tmp|
in_work_unit(tmp.map(&:status_id)) do |status_ids|
- bulk = ActiveRecord::Base.connection_pool.with_connection do
- Chewy::Index::Import::BulkBuilder.new(index, to_index: Status.includes(:media_attachments, :preloadable_poll).where(id: status_ids)).bulk_body
- end
-
- indexed = 0
deleted = 0
- # We can't use the delete_if proc to do the filtering because delete_if
- # is called before rendering the data and we need to filter based
- # on the results of the filter, so this filtering happens here instead
- bulk.map! do |entry|
- new_entry = if entry[:index] && entry.dig(:index, :data, 'searchable_by').blank?
- { delete: entry[:index].except(:data) }
- else
- entry
- end
-
- if new_entry[:index]
- indexed += 1
- else
- deleted += 1
+ bulk = ActiveRecord::Base.connection_pool.with_connection do
+ to_index = index.adapter.default_scope.where(id: status_ids)
+ crutches = Chewy::Index::Crutch::Crutches.new index, to_index
+ to_index.map do |object|
+ # This is unlikely to happen, but the post may have been
+ # un-interacted with since it was queued for indexing
+ if object.searchable_by.empty?
+ deleted += 1
+ { delete: { _id: object.id } }
+ else
+ { index: { _id: object.id, data: index.compose(object, crutches, fields: []) } }
+ end
end
-
- new_entry
end
+ indexed = bulk.size - deleted
+
Chewy::Index::Import::BulkRequest.new(index).perform(bulk)
[indexed, deleted]
diff --git a/app/lib/importer/tags_index_importer.rb b/app/lib/importer/tags_index_importer.rb
index 77710ed7d..067fd8cd2 100644
--- a/app/lib/importer/tags_index_importer.rb
+++ b/app/lib/importer/tags_index_importer.rb
@@ -4,10 +4,10 @@ class Importer::TagsIndexImporter < Importer::BaseImporter
def import!
index.adapter.default_scope.find_in_batches(batch_size: @batch_size) do |tmp|
in_work_unit(tmp) do |tags|
- bulk = Chewy::Index::Import::BulkBuilder.new(index, to_index: tags).bulk_body
+ bulk = build_bulk_body(tags)
- indexed = bulk.count { |entry| entry[:index] }
- deleted = bulk.count { |entry| entry[:delete] }
+ indexed = bulk.size
+ deleted = 0
Chewy::Index::Import::BulkRequest.new(index).perform(bulk)
diff --git a/app/lib/plain_text_formatter.rb b/app/lib/plain_text_formatter.rb
index 8eac730be..d1ff6808b 100644
--- a/app/lib/plain_text_formatter.rb
+++ b/app/lib/plain_text_formatter.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
class PlainTextFormatter
- include ActionView::Helpers::TextHelper
-
NEWLINE_TAGS_RE = %r{(
|
|)+}
attr_reader :text, :local
@@ -18,7 +16,10 @@ class PlainTextFormatter
if local?
text
else
- html_entities.decode(strip_tags(insert_newlines)).chomp
+ node = Nokogiri::HTML.fragment(insert_newlines)
+ # Elements that are entirely removed with our Sanitize config
+ node.xpath('.//iframe|.//math|.//noembed|.//noframes|.//noscript|.//plaintext|.//script|.//style|.//svg|.//xmp').remove
+ node.text.chomp
end
end
@@ -27,8 +28,4 @@ class PlainTextFormatter
def insert_newlines
text.gsub(NEWLINE_TAGS_RE) { |match| "#{match}\n" }
end
-
- def html_entities
- HTMLEntities.new
- end
end
diff --git a/app/lib/search_query_parser.rb b/app/lib/search_query_parser.rb
index 15956d4cf..1c57b9b02 100644
--- a/app/lib/search_query_parser.rb
+++ b/app/lib/search_query_parser.rb
@@ -6,10 +6,10 @@ class SearchQueryParser < Parslet::Parser
rule(:colon) { str(':') }
rule(:space) { match('\s').repeat(1) }
rule(:operator) { (str('+') | str('-')).as(:operator) }
- rule(:prefix) { (term >> colon).as(:prefix) }
+ rule(:prefix) { term >> colon }
rule(:shortcode) { (colon >> term >> colon.maybe).as(:shortcode) }
rule(:phrase) { (quote >> (term >> space.maybe).repeat >> quote).as(:phrase) }
- rule(:clause) { (prefix.maybe >> operator.maybe >> (phrase | term | shortcode)).as(:clause) }
+ rule(:clause) { (operator.maybe >> prefix.maybe.as(:prefix) >> (phrase | term | shortcode)).as(:clause) | prefix.as(:clause) | quote.as(:junk) }
rule(:query) { (clause >> space.maybe).repeat.as(:query) }
root(:query)
end
diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb
index aef05e9d9..e81c0c081 100644
--- a/app/lib/search_query_transformer.rb
+++ b/app/lib/search_query_transformer.rb
@@ -1,58 +1,42 @@
# frozen_string_literal: true
class SearchQueryTransformer < Parslet::Transform
+ SUPPORTED_PREFIXES = %w(
+ has
+ is
+ language
+ from
+ before
+ after
+ during
+ ).freeze
+
class Query
- attr_reader :should_clauses, :must_not_clauses, :must_clauses, :filter_clauses
+ attr_reader :must_not_clauses, :must_clauses, :filter_clauses
def initialize(clauses)
- grouped = clauses.chunk(&:operator).to_h
- @should_clauses = grouped.fetch(:should, [])
+ grouped = clauses.compact.chunk(&:operator).to_h
@must_not_clauses = grouped.fetch(:must_not, [])
@must_clauses = grouped.fetch(:must, [])
@filter_clauses = grouped.fetch(:filter, [])
end
def apply(search)
- should_clauses.each { |clause| search = search.query.should(clause_to_query(clause)) }
- must_clauses.each { |clause| search = search.query.must(clause_to_query(clause)) }
- must_not_clauses.each { |clause| search = search.query.must_not(clause_to_query(clause)) }
- filter_clauses.each { |clause| search = search.filter(**clause_to_filter(clause)) }
+ must_clauses.each { |clause| search = search.query.must(clause.to_query) }
+ must_not_clauses.each { |clause| search = search.query.must_not(clause.to_query) }
+ filter_clauses.each { |clause| search = search.filter(**clause.to_query) }
search.query.minimum_should_match(1)
end
-
- private
-
- def clause_to_query(clause)
- case clause
- when TermClause
- { multi_match: { type: 'most_fields', query: clause.term, fields: ['text', 'text.stemmed'] } }
- when PhraseClause
- { match_phrase: { text: { query: clause.phrase } } }
- else
- raise "Unexpected clause type: #{clause}"
- end
- end
-
- def clause_to_filter(clause)
- case clause
- when PrefixClause
- { term: { clause.filter => clause.term } }
- else
- raise "Unexpected clause type: #{clause}"
- end
- end
end
class Operator
class << self
def symbol(str)
case str
- when '+'
+ when '+', nil
:must
when '-'
:must_not
- when nil
- :should
else
raise "Unknown operator: #{str}"
end
@@ -61,61 +45,133 @@ class SearchQueryTransformer < Parslet::Transform
end
class TermClause
- attr_reader :prefix, :operator, :term
+ attr_reader :operator, :term
- def initialize(prefix, operator, term)
- @prefix = prefix
+ def initialize(operator, term)
@operator = Operator.symbol(operator)
@term = term
end
+
+ def to_query
+ { multi_match: { type: 'most_fields', query: @term, fields: ['text', 'text.stemmed'], operator: 'and' } }
+ end
end
class PhraseClause
- attr_reader :prefix, :operator, :phrase
+ attr_reader :operator, :phrase
- def initialize(prefix, operator, phrase)
- @prefix = prefix
+ def initialize(operator, phrase)
@operator = Operator.symbol(operator)
@phrase = phrase
end
+
+ def to_query
+ { match_phrase: { text: { query: @phrase } } }
+ end
end
class PrefixClause
- attr_reader :filter, :operator, :term
+ attr_reader :operator, :prefix, :term
- def initialize(prefix, term)
+ def initialize(prefix, operator, term, options = {})
+ @prefix = prefix
+ @negated = operator == '-'
+ @options = options
@operator = :filter
+
case prefix
+ when 'has', 'is'
+ @filter = :properties
+ @type = :term
+ @term = term
+ when 'language'
+ @filter = :language
+ @type = :term
+ @term = language_code_from_term(term)
when 'from'
@filter = :account_id
-
- username, domain = term.gsub(/\A@/, '').split('@')
- domain = nil if TagManager.instance.local_domain?(domain)
- account = Account.find_remote!(username, domain)
-
- @term = account.id
+ @type = :term
+ @term = account_id_from_term(term)
+ when 'before'
+ @filter = :created_at
+ @type = :range
+ @term = { lt: term, time_zone: @options[:current_account]&.user_time_zone || 'UTC' }
+ when 'after'
+ @filter = :created_at
+ @type = :range
+ @term = { gt: term, time_zone: @options[:current_account]&.user_time_zone || 'UTC' }
+ when 'during'
+ @filter = :created_at
+ @type = :range
+ @term = { gte: term, lte: term, time_zone: @options[:current_account]&.user_time_zone || 'UTC' }
else
- raise Mastodon::SyntaxError
+ raise "Unknown prefix: #{prefix}"
end
end
+
+ def to_query
+ if @negated
+ { bool: { must_not: { @type => { @filter => @term } } } }
+ else
+ { @type => { @filter => @term } }
+ end
+ end
+
+ private
+
+ def account_id_from_term(term)
+ return @options[:current_account]&.id || -1 if term == 'me'
+
+ username, domain = term.gsub(/\A@/, '').split('@')
+ domain = nil if TagManager.instance.local_domain?(domain)
+ account = Account.find_remote(username, domain)
+
+ # If the account is not found, we want to return empty results, so return
+ # an ID that does not exist
+ account&.id || -1
+ end
+
+ def language_code_from_term(term)
+ language_code = term
+
+ return language_code if LanguagesHelper::SUPPORTED_LOCALES.key?(language_code.to_sym)
+
+ language_code = term.downcase
+
+ return language_code if LanguagesHelper::SUPPORTED_LOCALES.key?(language_code.to_sym)
+
+ language_code = term.split(/[_-]/).first.downcase
+
+ return language_code if LanguagesHelper::SUPPORTED_LOCALES.key?(language_code.to_sym)
+
+ term
+ end
end
rule(clause: subtree(:clause)) do
prefix = clause[:prefix][:term].to_s if clause[:prefix]
operator = clause[:operator]&.to_s
- if clause[:prefix]
- PrefixClause.new(prefix, clause[:term].to_s)
+ if clause[:prefix] && SUPPORTED_PREFIXES.include?(prefix)
+ PrefixClause.new(prefix, operator, clause[:term].to_s, current_account: current_account)
+ elsif clause[:prefix]
+ TermClause.new(operator, "#{prefix} #{clause[:term]}")
elsif clause[:term]
- TermClause.new(prefix, operator, clause[:term].to_s)
+ TermClause.new(operator, clause[:term].to_s)
elsif clause[:shortcode]
- TermClause.new(prefix, operator, ":#{clause[:term]}:")
+ TermClause.new(operator, ":#{clause[:term]}:")
elsif clause[:phrase]
- PhraseClause.new(prefix, operator, clause[:phrase].is_a?(Array) ? clause[:phrase].map { |p| p[:term].to_s }.join(' ') : clause[:phrase].to_s)
+ PhraseClause.new(operator, clause[:phrase].is_a?(Array) ? clause[:phrase].map { |p| p[:term].to_s }.join(' ') : clause[:phrase].to_s)
else
raise "Unexpected clause type: #{clause}"
end
end
- rule(query: sequence(:clauses)) { Query.new(clauses) }
+ rule(junk: subtree(:junk)) do
+ nil
+ end
+
+ rule(query: sequence(:clauses)) do
+ Query.new(clauses)
+ end
end
diff --git a/app/lib/vacuum/statuses_vacuum.rb b/app/lib/vacuum/statuses_vacuum.rb
index 28c087b1c..ad1de0738 100644
--- a/app/lib/vacuum/statuses_vacuum.rb
+++ b/app/lib/vacuum/statuses_vacuum.rb
@@ -20,7 +20,10 @@ class Vacuum::StatusesVacuum
statuses.direct_visibility
.includes(mentions: :account)
.find_each(&:unlink_from_conversations!)
- remove_from_search_index(statuses.ids) if Chewy.enabled?
+ if Chewy.enabled?
+ remove_from_index(statuses.ids, 'chewy:queue:StatusesIndex')
+ remove_from_index(statuses.ids, 'chewy:queue:PublicStatusesIndex')
+ end
# Foreign keys take care of most associated records for us.
# Media attachments will be orphaned.
@@ -38,7 +41,7 @@ class Vacuum::StatusesVacuum
Mastodon::Snowflake.id_at(@retention_period.ago, with_random: false)
end
- def remove_from_search_index(status_ids)
- with_redis { |redis| redis.sadd('chewy:queue:StatusesIndex', status_ids) }
+ def remove_from_index(status_ids, index)
+ with_redis { |redis| redis.sadd(index, status_ids) }
end
end
diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb
index 5baf9b38a..990b92c33 100644
--- a/app/mailers/admin_mailer.rb
+++ b/app/mailers/admin_mailer.rb
@@ -45,6 +45,22 @@ class AdminMailer < ApplicationMailer
end
end
+ def new_software_updates
+ locale_for_account(@me) do
+ mail subject: default_i18n_subject(instance: @instance)
+ end
+ end
+
+ def new_critical_software_updates
+ headers['Priority'] = 'urgent'
+ headers['X-Priority'] = '1'
+ headers['Importance'] = 'high'
+
+ locale_for_account(@me) do
+ mail subject: default_i18n_subject(instance: @instance)
+ end
+ end
+
private
def process_params
diff --git a/app/models/account.rb b/app/models/account.rb
index 28030a426..99930c322 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -82,6 +82,7 @@ class Account < ApplicationRecord
include DomainMaterializable
include AccountMerging
include AccountSearch
+ include AccountStatusesSearch
MAX_DISPLAY_NAME_LENGTH = (ENV['MAX_DISPLAY_NAME_CHARS'] || 30).to_i
MAX_NOTE_LENGTH = (ENV['MAX_BIO_CHARS'] || 500).to_i
diff --git a/app/models/concerns/account_statuses_search.rb b/app/models/concerns/account_statuses_search.rb
new file mode 100644
index 000000000..fa9238e6e
--- /dev/null
+++ b/app/models/concerns/account_statuses_search.rb
@@ -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
diff --git a/app/models/concerns/status_search_concern.rb b/app/models/concerns/status_search_concern.rb
new file mode 100644
index 000000000..3ef45754a
--- /dev/null
+++ b/app/models/concerns/status_search_concern.rb
@@ -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
diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb
index 8f1dc4954..41014fb41 100644
--- a/app/models/form/admin_settings.rb
+++ b/app/models/form/admin_settings.rb
@@ -3,6 +3,8 @@
class Form::AdminSettings
include ActiveModel::Model
+ include AuthorizedFetchHelper
+
KEYS = %i(
site_contact_username
site_contact_email
@@ -42,6 +44,7 @@ class Form::AdminSettings
backups_retention_period
status_page_url
captcha_enabled
+ authorized_fetch
).freeze
INTEGER_KEYS = %i(
@@ -66,6 +69,7 @@ class Form::AdminSettings
noindex
require_invite_text
captcha_enabled
+ authorized_fetch
).freeze
UPLOAD_KEYS = %i(
@@ -77,6 +81,10 @@ class Form::AdminSettings
flavour_and_skin
).freeze
+ OVERRIDEN_SETTINGS = {
+ authorized_fetch: :authorized_fetch_mode?,
+ }.freeze
+
attr_accessor(*KEYS)
validates :registrations_mode, inclusion: { in: %w(open approved none) }, if: -> { defined?(@registrations_mode) }
@@ -96,6 +104,8 @@ class Form::AdminSettings
stored_value = if UPLOAD_KEYS.include?(key)
SiteUpload.where(var: key).first_or_initialize(var: key)
+ elsif OVERRIDEN_SETTINGS.include?(key)
+ public_send(OVERRIDEN_SETTINGS[key])
else
Setting.public_send(key)
end
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index 02b61bd51..49063754a 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -44,6 +44,7 @@ class MediaAttachment < ApplicationRecord
MAX_VIDEO_MATRIX_LIMIT = 8_294_400 # 3840x2160px
MAX_VIDEO_FRAME_RATE = 120
+ MAX_VIDEO_FRAMES = 36_000 # Approx. 5 minutes at 120 fps
IMAGE_FILE_EXTENSIONS = %w(.jpg .jpeg .png .gif .webp .heic .heif .avif).freeze
VIDEO_FILE_EXTENSIONS = %w(.webm .mp4 .m4v .mov).freeze
@@ -98,17 +99,15 @@ class MediaAttachment < ApplicationRecord
convert_options: {
output: {
'loglevel' => 'fatal',
- 'movflags' => 'faststart',
- 'pix_fmt' => 'yuv420p',
- 'vf' => 'scale=\'trunc(iw/2)*2:trunc(ih/2)*2\'',
- 'vsync' => 'cfr',
+ 'preset' => 'veryfast',
+ 'movflags' => 'faststart', # Move metadata to start of file so playback can begin before download finishes
+ 'pix_fmt' => 'yuv420p', # Ensure color space for cross-browser compatibility
+ 'vf' => 'crop=floor(iw/2)*2:floor(ih/2)*2', # h264 requires width and height to be even. Crop instead of scale to avoid blurring
'c:v' => 'h264',
- 'maxrate' => '1300K',
- 'bufsize' => '1300K',
- 'b:v' => '1300K',
- 'frames:v' => 60 * 60 * 3,
- 'crf' => 18,
+ 'c:a' => 'aac',
+ 'b:a' => '192k',
'map_metadata' => '-1',
+ 'frames:v' => MAX_VIDEO_FRAMES,
}.freeze,
}.freeze,
}.freeze
@@ -135,7 +134,7 @@ class MediaAttachment < ApplicationRecord
convert_options: {
output: {
'loglevel' => 'fatal',
- :vf => 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease',
+ :vf => 'scale=\'min(640\, iw):min(640\, ih)\':force_original_aspect_ratio=decrease',
}.freeze,
}.freeze,
format: 'png',
diff --git a/app/models/poll.rb b/app/models/poll.rb
index 74a77978b..efa625eb5 100644
--- a/app/models/poll.rb
+++ b/app/models/poll.rb
@@ -28,6 +28,7 @@ class Poll < ApplicationRecord
has_many :votes, class_name: 'PollVote', inverse_of: :poll, dependent: :delete_all
has_many :voters, -> { group('accounts.id') }, through: :votes, class_name: 'Account', source: :account
+ has_many :local_voters, -> { group('accounts.id').merge(Account.local) }, through: :votes, class_name: 'Account', source: :account
has_many :notifications, as: :activity, dependent: :destroy
diff --git a/app/models/software_update.rb b/app/models/software_update.rb
new file mode 100644
index 000000000..cb3a6df2a
--- /dev/null
+++ b/app/models/software_update.rb
@@ -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
diff --git a/app/models/status.rb b/app/models/status.rb
index ae68f8c85..13bcf1daa 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -39,6 +39,7 @@ class Status < ApplicationRecord
include StatusSnapshotConcern
include RateLimitable
include StatusSafeReblogInsert
+ include StatusSearchConcern
rate_limit by: :account, family: :statuses
@@ -49,6 +50,7 @@ class Status < ApplicationRecord
attr_accessor :override_timestamps
update_index('statuses', :proper)
+ update_index('public_statuses', :proper)
enum visibility: { public: 0, unlisted: 1, private: 2, direct: 3, limited: 4 }, _suffix: :visibility
@@ -72,6 +74,12 @@ class Status < ApplicationRecord
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status
has_many :media_attachments, dependent: :nullify
+ # Those associations are used for the private search index
+ has_many :local_mentioned, -> { merge(Account.local) }, through: :active_mentions, source: :account
+ has_many :local_favorited, -> { merge(Account.local) }, through: :favourites, source: :account
+ has_many :local_reblogged, -> { merge(Account.local) }, through: :reblogs, source: :account
+ has_many :local_bookmarked, -> { merge(Account.local) }, through: :bookmarks, source: :account
+
has_and_belongs_to_many :tags
has_and_belongs_to_many :preview_cards
@@ -172,37 +180,6 @@ class Status < ApplicationRecord
"v3:#{super}"
end
- def searchable_by(preloaded = nil)
- ids = []
-
- ids << account_id if local?
-
- if preloaded.nil?
- ids += mentions.joins(:account).merge(Account.local).active.pluck(:account_id)
- ids += favourites.joins(:account).merge(Account.local).pluck(:account_id)
- ids += reblogs.joins(:account).merge(Account.local).pluck(:account_id)
- ids += bookmarks.joins(:account).merge(Account.local).pluck(:account_id)
- ids += poll.votes.joins(:account).merge(Account.local).pluck(:account_id) if poll.present?
- else
- ids += preloaded.mentions[id] || []
- ids += preloaded.favourites[id] || []
- ids += preloaded.reblogs[id] || []
- ids += preloaded.bookmarks[id] || []
- ids += preloaded.votes[id] || []
- end
-
- ids.uniq
- end
-
- def searchable_text
- [
- spoiler_text,
- FormattingHelper.extract_status_plain_text(self),
- preloadable_poll ? preloadable_poll.options.join("\n\n") : nil,
- ordered_media_attachments.map(&:description).join("\n\n"),
- ].compact.join("\n\n")
- end
-
def to_log_human_identifier
account.acct
end
@@ -277,6 +254,10 @@ class Status < ApplicationRecord
preview_cards.any?
end
+ def with_poll?
+ preloadable_poll.present?
+ end
+
def non_sensitive_with_media?
!sensitive? && with_media?
end
diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb
index 7a64159d8..9956b8851 100644
--- a/app/models/user_settings.rb
+++ b/app/models/user_settings.rb
@@ -52,6 +52,7 @@ class UserSettings
setting :link_trends, default: false
setting :status_trends, default: false
setting :appeal, default: true
+ setting :software_updates, default: 'critical', in: %w(none critical patch all)
end
namespace :interactions do
diff --git a/app/policies/software_update_policy.rb b/app/policies/software_update_policy.rb
new file mode 100644
index 000000000..dcb565814
--- /dev/null
+++ b/app/policies/software_update_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class SoftwareUpdatePolicy < ApplicationPolicy
+ def index?
+ role.can?(:view_devops)
+ end
+end
diff --git a/app/presenters/initial_state_presenter.rb b/app/presenters/initial_state_presenter.rb
index b87cff51e..222cc8566 100644
--- a/app/presenters/initial_state_presenter.rb
+++ b/app/presenters/initial_state_presenter.rb
@@ -3,9 +3,13 @@
class InitialStatePresenter < ActiveModelSerializers::Model
attributes :settings, :push_subscription, :token,
:current_account, :admin, :owner, :text, :visibility,
- :disabled_account, :moved_to_account
+ :disabled_account, :moved_to_account, :critical_updates_pending
def role
current_account&.user_role
end
+
+ def critical_updates_pending
+ role&.can?(:view_devops) && SoftwareUpdate.urgent_pending?
+ end
end
diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb
index 4998d0039..31f39954f 100644
--- a/app/serializers/activitypub/actor_serializer.rb
+++ b/app/serializers/activitypub/actor_serializer.rb
@@ -8,13 +8,13 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
context_extensions :manually_approves_followers, :featured, :also_known_as,
:moved_to, :property_value, :discoverable, :olm, :suspended,
- :memorial
+ :memorial, :indexable
attributes :id, :type, :following, :followers,
:inbox, :outbox, :featured, :featured_tags,
:preferred_username, :name, :summary,
:url, :manually_approves_followers,
- :discoverable, :published, :memorial
+ :discoverable, :indexable, :published, :memorial
has_one :public_key, serializer: ActivityPub::PublicKeySerializer
@@ -99,6 +99,10 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
object.suspended? ? false : (object.discoverable || false)
end
+ def indexable
+ object.suspended? ? false : (object.indexable || false)
+ end
+
def name
object.suspended? ? object.username : (object.display_name.presence || object.username)
end
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index 3903b444b..178c25a49 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -8,6 +8,8 @@ class InitialStateSerializer < ActiveModel::Serializer
:max_toot_chars, :poll_limits,
:languages
+ attribute :critical_updates_pending, if: -> { object&.role&.can?(:view_devops) && SoftwareUpdate.check_enabled? }
+
has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer
has_one :role, serializer: REST::RoleSerializer
diff --git a/app/serializers/webfinger_serializer.rb b/app/serializers/webfinger_serializer.rb
index 3ca344116..b67cd2771 100644
--- a/app/serializers/webfinger_serializer.rb
+++ b/app/serializers/webfinger_serializer.rb
@@ -18,18 +18,31 @@ class WebfingerSerializer < ActiveModel::Serializer
end
def links
- if object.instance_actor?
- [
- { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: about_more_url(instance_actor: true) },
- { rel: 'self', type: 'application/activity+json', href: instance_actor_url },
- { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" },
- ]
- else
- [
- { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(object) },
- { rel: 'self', type: 'application/activity+json', href: account_url(object) },
- { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" },
- ]
+ [
+ { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: profile_page_href },
+ { rel: 'self', type: 'application/activity+json', href: self_href },
+ { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" },
+ ].tap do |x|
+ x << { rel: 'http://webfinger.net/rel/avatar', type: object.avatar.content_type, href: full_asset_url(object.avatar_original_url) } if show_avatar?
end
end
+
+ private
+
+ def show_avatar?
+ media_present = object.avatar.present? && object.avatar.content_type.present?
+
+ # Show avatar only if an instance shows profiles to logged out users
+ allowed_by_config = ENV['DISALLOW_UNAUTHENTICATED_API_ACCESS'] != 'true' && !Rails.configuration.x.limited_federation_mode
+
+ media_present && allowed_by_config
+ end
+
+ def profile_page_href
+ object.instance_actor? ? about_more_url(instance_actor: true) : short_account_url(object)
+ end
+
+ def self_href
+ object.instance_actor? ? instance_actor_url : account_url(object)
+ end
end
diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb
index 348f94a09..b74ae1a41 100644
--- a/app/services/batched_remove_status_service.rb
+++ b/app/services/batched_remove_status_service.rb
@@ -38,7 +38,10 @@ class BatchedRemoveStatusService < BaseService
# Since we skipped all callbacks, we also need to manually
# deindex the statuses
- Chewy.strategy.current.update(StatusesIndex, statuses_and_reblogs) if Chewy.enabled?
+ if Chewy.enabled?
+ Chewy.strategy.current.update(StatusesIndex, statuses_and_reblogs)
+ Chewy.strategy.current.update(PublicStatusesIndex, statuses_and_reblogs)
+ end
return if options[:skip_side_effects]
diff --git a/app/services/concerns/payloadable.rb b/app/services/concerns/payloadable.rb
index 1389a42ed..bd9d9d74b 100644
--- a/app/services/concerns/payloadable.rb
+++ b/app/services/concerns/payloadable.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
module Payloadable
+ include AuthorizedFetchHelper
+
# @param [ActiveModelSerializers::Model] record
# @param [ActiveModelSerializers::Serializer] serializer
# @param [Hash] options
@@ -23,6 +25,6 @@ module Payloadable
end
def signing_enabled?
- ENV['AUTHORIZED_FETCH'] != 'true' && !Rails.configuration.x.limited_federation_mode
+ !authorized_fetch_mode?
end
end
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index 30937471b..9a40d7bdd 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -1,8 +1,10 @@
# frozen_string_literal: true
class SearchService < BaseService
+ QUOTE_EQUIVALENT_CHARACTERS = /[“”„«»「」『』《》]/
+
def call(query, account, limit, options = {})
- @query = query&.strip
+ @query = query&.strip&.gsub(QUOTE_EQUIVALENT_CHARACTERS, '"')
@account = account
@options = options
@limit = limit.to_i
@@ -17,7 +19,7 @@ class SearchService < BaseService
results.merge!(url_resource_results) unless url_resource.nil? || @offset.positive? || (@options[:type].present? && url_resource_symbol != @options[:type].to_sym)
elsif @query.present?
results[:accounts] = perform_accounts_search! if account_searchable?
- results[:statuses] = perform_statuses_search! if full_text_searchable?
+ results[:statuses] = perform_statuses_search! if status_searchable?
results[:hashtags] = perform_hashtags_search! if hashtag_searchable?
end
end
@@ -39,25 +41,15 @@ class SearchService < BaseService
end
def perform_statuses_search!
- definition = parsed_query.apply(StatusesIndex.filter(term: { searchable_by: @account.id }))
-
- definition = definition.filter(term: { account_id: @options[:account_id] }) if @options[:account_id].present?
-
- if @options[:min_id].present? || @options[:max_id].present?
- range = {}
- range[:gt] = @options[:min_id].to_i if @options[:min_id].present?
- range[:lt] = @options[:max_id].to_i if @options[:max_id].present?
- definition = definition.filter(range: { id: range })
- end
-
- results = definition.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
- []
+ StatusesSearchService.new.call(
+ @query,
+ @account,
+ limit: @limit,
+ offset: @offset,
+ account_id: @options[:account_id],
+ min_id: @options[:min_id],
+ max_id: @options[:max_id]
+ )
end
def perform_hashtags_search!
@@ -89,18 +81,16 @@ class SearchService < BaseService
url_resource.class.name.downcase.pluralize.to_sym
end
- def full_text_searchable?
- return false unless Chewy.enabled?
-
- statuses_search? && !@account.nil? && !(@query.include?('@') && !@query.include?(' '))
+ def status_searchable?
+ Chewy.enabled? && status_search? && @account.present?
end
def account_searchable?
- account_search? && !(@query.include?('@') && @query.include?(' '))
+ account_search?
end
def hashtag_searchable?
- hashtag_search? && !@query.include?('@')
+ hashtag_search?
end
def account_search?
@@ -111,11 +101,7 @@ class SearchService < BaseService
@options[:type].blank? || @options[:type] == 'hashtags'
end
- def statuses_search?
+ def status_search?
@options[:type].blank? || @options[:type] == 'statuses'
end
-
- def parsed_query
- SearchQueryTransformer.new.apply(SearchQueryParser.new.parse(@query))
- end
end
diff --git a/app/services/software_update_check_service.rb b/app/services/software_update_check_service.rb
new file mode 100644
index 000000000..49b92f104
--- /dev/null
+++ b/app/services/software_update_check_service.rb
@@ -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
diff --git a/app/services/statuses_search_service.rb b/app/services/statuses_search_service.rb
new file mode 100644
index 000000000..2317a2a1a
--- /dev/null
+++ b/app/services/statuses_search_service.rb
@@ -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
diff --git a/app/views/admin/settings/discovery/show.html.haml b/app/views/admin/settings/discovery/show.html.haml
index 460bb5709..c12ba34ae 100644
--- a/app/views/admin/settings/discovery/show.html.haml
+++ b/app/views/admin/settings/discovery/show.html.haml
@@ -42,6 +42,11 @@
.fields-group
= f.input :peers_api_enabled, as: :boolean, wrapper: :with_label, recommended: :recommended
+ %h4= t('admin.settings.security.federation_authentication')
+
+ .fields-group
+ = f.input :authorized_fetch, as: :boolean, wrapper: :with_label, label: t('admin.settings.security.authorized_fetch'), warning_hint: authorized_fetch_overridden? ? t('admin.settings.security.authorized_fetch_overridden_hint') : nil, hint: t('admin.settings.security.authorized_fetch_hint'), disabled: authorized_fetch_overridden?, recommended: authorized_fetch_overridden? ? :overridden : nil
+
%h4= t('admin.settings.discovery.follow_recommendations')
.fields-group
diff --git a/app/views/admin/software_updates/index.html.haml b/app/views/admin/software_updates/index.html.haml
new file mode 100644
index 000000000..7a223ee07
--- /dev/null
+++ b/app/views/admin/software_updates/index.html.haml
@@ -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
diff --git a/app/views/admin_mailer/new_critical_software_updates.text.erb b/app/views/admin_mailer/new_critical_software_updates.text.erb
new file mode 100644
index 000000000..c901bc50f
--- /dev/null
+++ b/app/views/admin_mailer/new_critical_software_updates.text.erb
@@ -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 %>
diff --git a/app/views/admin_mailer/new_software_updates.text.erb b/app/views/admin_mailer/new_software_updates.text.erb
new file mode 100644
index 000000000..2fc4d1a5f
--- /dev/null
+++ b/app/views/admin_mailer/new_software_updates.text.erb
@@ -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 %>
diff --git a/app/views/settings/preferences/notifications/show.html.haml b/app/views/settings/preferences/notifications/show.html.haml
index a366dbad5..e0cbe36d1 100644
--- a/app/views/settings/preferences/notifications/show.html.haml
+++ b/app/views/settings/preferences/notifications/show.html.haml
@@ -22,7 +22,7 @@
.fields-group
= ff.input :always_send_emails, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_always_send_emails'), hint: I18n.t('simple_form.hints.defaults.setting_always_send_emails')
- - if current_user.can?(:manage_reports, :manage_appeals, :manage_users, :manage_taxonomies)
+ - if current_user.can?(:manage_reports, :manage_appeals, :manage_users, :manage_taxonomies) || (SoftwareUpdate.check_enabled? && current_user.can?(:view_devops))
%h4= t 'notifications.administration_emails'
.fields-group
@@ -33,6 +33,10 @@
= ff.input :'notification_emails.link_trends', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.trending_link') if current_user.can?(:manage_taxonomies)
= ff.input :'notification_emails.status_trends', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.trending_status') if current_user.can?(:manage_taxonomies)
+ - if SoftwareUpdate.check_enabled? && current_user.can?(:view_devops)
+ .fields-group
+ = ff.input :'notification_emails.software_updates', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.software_updates.label'), collection: %w(none critical patch all), label_method: ->(setting) { I18n.t("simple_form.labels.notification_emails.software_updates.#{setting}") }, include_blank: false, hint: false
+
%h4= t 'notifications.other_settings'
.fields-group
diff --git a/app/views/settings/privacy/show.html.haml b/app/views/settings/privacy/show.html.haml
index 6b7ae14e9..3fb14023b 100644
--- a/app/views/settings/privacy/show.html.haml
+++ b/app/views/settings/privacy/show.html.haml
@@ -24,6 +24,9 @@
%p.lead= t('privacy.search_hint_html')
+ .fields-group
+ = f.input :indexable, as: :boolean, wrapper: :with_label
+
= f.simple_fields_for :settings, current_user.settings do |ff|
.fields-group
= ff.input :indexable, wrapper: :with_label
diff --git a/app/workers/add_to_public_statuses_index_worker.rb b/app/workers/add_to_public_statuses_index_worker.rb
new file mode 100644
index 000000000..80d921eab
--- /dev/null
+++ b/app/workers/add_to_public_statuses_index_worker.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class AddToPublicStatusesIndexWorker
+ include Sidekiq::Worker
+ include DatabaseHelper
+
+ sidekiq_options queue: 'pull'
+
+ def perform(account_id)
+ with_primary do
+ @account = Account.find(account_id)
+ end
+
+ return unless @account.indexable?
+
+ with_read_replica do
+ @account.add_to_public_statuses_index!
+ end
+ rescue ActiveRecord::RecordNotFound
+ true
+ end
+end
diff --git a/app/workers/remove_from_public_statuses_index_worker.rb b/app/workers/remove_from_public_statuses_index_worker.rb
new file mode 100644
index 000000000..e108a5c20
--- /dev/null
+++ b/app/workers/remove_from_public_statuses_index_worker.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class RemoveFromPublicStatusesIndexWorker
+ include Sidekiq::Worker
+
+ def perform(account_id)
+ account = Account.find(account_id)
+
+ return if account.indexable?
+
+ account.remove_from_public_statuses_index!
+ rescue ActiveRecord::RecordNotFound
+ true
+ end
+end
diff --git a/app/workers/scheduler/indexing_scheduler.rb b/app/workers/scheduler/indexing_scheduler.rb
index 2868a3b71..ff1b74444 100644
--- a/app/workers/scheduler/indexing_scheduler.rb
+++ b/app/workers/scheduler/indexing_scheduler.rb
@@ -3,6 +3,7 @@
class Scheduler::IndexingScheduler
include Sidekiq::Worker
include Redisable
+ include DatabaseHelper
sidekiq_options retry: 0, lock: :until_executed, lock_ttl: 1.day.to_i
@@ -16,6 +17,7 @@ class Scheduler::IndexingScheduler
with_redis do |redis|
redis.sscan_each("chewy:queue:#{type.name}", count: SCAN_BATCH_SIZE).each_slice(IMPORT_BATCH_SIZE) do |ids|
type.import!(ids)
+
redis.srem("chewy:queue:#{type.name}", ids)
end
end
@@ -23,6 +25,6 @@ class Scheduler::IndexingScheduler
end
def indexes
- [AccountsIndex, TagsIndex, StatusesIndex]
+ [AccountsIndex, TagsIndex, PublicStatusesIndex, StatusesIndex]
end
end
diff --git a/app/workers/scheduler/software_update_check_scheduler.rb b/app/workers/scheduler/software_update_check_scheduler.rb
new file mode 100644
index 000000000..c732bdedc
--- /dev/null
+++ b/app/workers/scheduler/software_update_check_scheduler.rb
@@ -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
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 31a396245..9a6637bdb 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -35,6 +35,8 @@ Rails.application.configure do
config.cache_store = :null_store
end
+ config.action_controller.forgery_protection_origin_check = ENV['DISABLE_FORGERY_REQUEST_PROTECTION'].nil?
+
ActiveSupport::Logger.new(STDOUT).tap do |logger|
logger.formatter = config.log_formatter
config.logger = ActiveSupport::TaggedLogging.new(logger)
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index 6c11ccc5a..e5a4eb9a8 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -57,7 +57,7 @@ ignore_unused:
- 'activerecord.errors.*'
- '{devise,pagination,doorkeeper}.*'
- '{date,datetime,time,number}.*'
- - 'simple_form.{yes,no,recommended,not_recommended,glitch_only}'
+ - 'simple_form.{yes,no,recommended,not_recommended,overridden,glitch_only}'
- 'simple_form.{placeholders,hints,labels}.*'
- 'simple_form.{error_notification,required}.:'
- 'errors.messages.*'
diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb
index 6a0960021..e7b4f2d45 100644
--- a/config/initializers/simple_form.rb
+++ b/config/initializers/simple_form.rb
@@ -108,7 +108,8 @@ SimpleForm.setup do |config|
end
end
- b.use :hint, wrap_with: { tag: :span, class: :hint }
+ b.use :warning_hint, wrap_with: { tag: :span, class: [:hint, 'warning-hint'] }
+ b.use :hint, wrap_with: { tag: :span, class: :hint }
b.use :error, wrap_with: { tag: :span, class: :error }
end
@@ -122,8 +123,8 @@ SimpleForm.setup do |config|
config.wrappers :with_block_label, class: [:input, :with_block_label], hint_class: :field_with_hint, error_class: :field_with_errors do |b|
b.use :html5
b.use :label
- b.use :hint, wrap_with: { tag: :span, class: :hint }
b.use :warning_hint, wrap_with: { tag: :span, class: [:hint, 'warning-hint'] }
+ b.use :hint, wrap_with: { tag: :span, class: :hint }
b.use :input, wrap_with: { tag: :div, class: :label_input }
b.use :error, wrap_with: { tag: :span, class: :error }
end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 71121bb2e..71e5fb843 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -309,6 +309,7 @@ en:
unpublish: Unpublish
unpublished_msg: Announcement successfully unpublished!
updated_msg: Announcement successfully updated!
+ critical_update_pending: Critical update pending
custom_emojis:
assign_category: Assign category
by_domain: Domain
@@ -770,10 +771,27 @@ en:
approved: Approval required for sign up
none: Nobody can sign up
open: Anyone can sign up
+ security:
+ authorized_fetch: Require authentication from federated servers
+ authorized_fetch_hint: Requiring authentication from federated servers enables stricter enforcement of both user-level and server-level blocks. However, this comes at the cost of a performance penalty, reduces the reach of your replies, and may introduce compatibility issues with some federated services. In addition, this will not prevent dedicated actors from fetching your public posts and accounts.
+ authorized_fetch_overridden_hint: You are currently unable to change this setting because it is overridden by an environment variable.
+ federation_authentication: Federation authentication enforcement
title: Server settings
site_uploads:
delete: Delete uploaded file
destroyed_msg: Site upload successfully deleted!
+ software_updates:
+ critical_update: Critical — please update quickly
+ description: It is recommended to keep your Mastodon installation up to date to benefit from the latest fixes and features. Moreover, it is sometimes critical to update Mastodon in a timely manner to avoid security issues. For these reasons, Mastodon checks for updates every 30 minutes, and will notify you according to your e-mail notification preferences.
+ documentation_link: Learn more
+ release_notes: Release notes
+ title: Available updates
+ type: Type
+ types:
+ major: Major release
+ minor: Minor release
+ patch: Patch release — bugfixes and easy to apply changes
+ version: Version
statuses:
account: Author
application: Application
@@ -838,6 +856,12 @@ en:
message_html: You haven't defined any server rules.
sidekiq_process_check:
message_html: No Sidekiq process running for the %{value} queue(s). Please review your Sidekiq configuration
+ software_version_critical_check:
+ action: See available updates
+ message_html: A critical Mastodon update is available, please update as quickly as possible.
+ software_version_patch_check:
+ action: See available updates
+ message_html: A bugfix Mastodon update is available.
upload_check_privacy_error:
action: Check here for more information
message_html: "Your web server is misconfigured. The privacy of your users is at risk."
@@ -951,6 +975,9 @@ en:
body: "%{target} is appealing a moderation decision by %{action_taken_by} from %{date}, which was %{type}. They wrote:"
next_steps: You can approve the appeal to undo the moderation decision, or ignore it.
subject: "%{username} is appealing a moderation decision on %{instance}"
+ new_critical_software_updates:
+ body: New critical versions of Mastodon have been released, you may want to update as soon as possible!
+ subject: Critical Mastodon updates are available for %{instance}!
new_pending_account:
body: The details of the new account are below. You can approve or reject this application.
subject: New account up for review on %{instance} (%{username})
@@ -958,6 +985,9 @@ en:
body: "%{reporter} has reported %{target}"
body_remote: Someone from %{domain} has reported %{target}
subject: New report for %{instance} (#%{id})
+ new_software_updates:
+ body: New Mastodon versions have been released, you may want to update!
+ subject: New Mastodon versions are available for %{instance}!
new_trends:
body: 'The following items need a review before they can be displayed publicly:'
new_trending_links:
@@ -1709,6 +1739,10 @@ en:
default: "%b %d, %Y, %H:%M"
month: "%b %Y"
time: "%H:%M"
+ translation:
+ errors:
+ quota_exceeded: The server-wide usage quota for the translation service has been exceeded.
+ too_many_requests: There have been too many requests to the translation service recently.
two_factor_authentication:
add: Add
disable: Disable 2FA
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index 443b7617f..0b718c5b6 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -6,6 +6,7 @@ en:
discoverable: Your public posts and profile may be featured or recommended in various areas of Mastodon and your profile may be suggested to other users.
display_name: Your full name or your fun name.
fields: Your homepage, pronouns, age, anything you want.
+ indexable: Your public posts may appear in search results on Mastodon. People who have interacted with your posts may be able to search them regardless.
note: 'You can @mention other people or #hashtags.'
show_collections: People will be able to browse through your follows and followers. People that you follow will see that you follow them regardless.
unlocked: People will be able to follow you without requesting approval. Uncheck if you want to review follow requests and chose whether to accept or reject new followers.
@@ -143,6 +144,7 @@ en:
fields:
name: Label
value: Content
+ indexable: Include public posts in search results
show_collections: Show follows and followers on profile
unlocked: Automatically accept new followers
account_alias:
@@ -289,6 +291,12 @@ en:
pending_account: New account needs review
reblog: Someone boosted your post
report: New report is submitted
+ software_updates:
+ all: Notify on all updates
+ critical: Notify on critical updates only
+ label: A new Mastodon version is available
+ none: Never notify of updates (not recommended)
+ patch: Notify on bugfix updates
trending_tag: New trend requires review
rule:
text: Rule
@@ -315,6 +323,7 @@ en:
url: Endpoint URL
'no': 'No'
not_recommended: Not recommended
+ overridden: Overridden
recommended: Recommended
required:
mark: "*"
diff --git a/config/navigation.rb b/config/navigation.rb
index 8a91c59f3..4ea88fb4f 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -3,6 +3,9 @@
SimpleNavigation::Configuration.run do |navigation|
navigation.items do |n|
n.item :web, safe_join([fa_icon('chevron-left fw'), t('settings.back')]), root_path
+
+ n.item :software_updates, safe_join([fa_icon('exclamation-circle fw'), t('admin.critical_update_pending')]), admin_software_updates_path, if: -> { ENV['UPDATE_CHECK_URL'] != '' && current_user.can?(:view_devops) && SoftwareUpdate.urgent_pending? }, html: { class: 'warning' }
+
n.item :profile, safe_join([fa_icon('user fw'), t('settings.profile')]), settings_profile_path, if: -> { current_user.functional? }, highlights_on: %r{/settings/profile|/settings/featured_tags|/settings/verification|/settings/privacy}
n.item :preferences, safe_join([fa_icon('cog fw'), t('settings.preferences')]), settings_preferences_path, if: -> { current_user.functional? } do |s|
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 2e43a98e4..14695c36c 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -202,4 +202,6 @@ namespace :admin do
end
end
end
+
+ resources :software_updates, only: [:index]
end
diff --git a/config/sidekiq.yml b/config/sidekiq.yml
index 12c45c22a..f1ba5651d 100644
--- a/config/sidekiq.yml
+++ b/config/sidekiq.yml
@@ -58,3 +58,7 @@
interval: 1 minute
class: Scheduler::SuspendedUserCleanupScheduler
queue: scheduler
+ software_update_check_scheduler:
+ interval: 30 minutes
+ class: Scheduler::SoftwareUpdateCheckScheduler
+ queue: scheduler
diff --git a/config/webpacker.yml b/config/webpacker.yml
index f5c93a684..921c1306c 100644
--- a/config/webpacker.yml
+++ b/config/webpacker.yml
@@ -59,7 +59,7 @@ development:
# Reference: https://webpack.js.org/configuration/dev-server/
dev_server:
https: false
- host: localhost
+ host: 0.0.0.0
port: 3035
public: localhost:3035
hmr: false
diff --git a/db/migrate/20230822081029_create_software_updates.rb b/db/migrate/20230822081029_create_software_updates.rb
new file mode 100644
index 000000000..146d5d303
--- /dev/null
+++ b/db/migrate/20230822081029_create_software_updates.rb
@@ -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
diff --git a/db/post_migrate/20230803082451_add_unique_index_on_preview_cards_statuses.rb b/db/post_migrate/20230803082451_add_unique_index_on_preview_cards_statuses.rb
index c35ad8002..3e9ab134b 100644
--- a/db/post_migrate/20230803082451_add_unique_index_on_preview_cards_statuses.rb
+++ b/db/post_migrate/20230803082451_add_unique_index_on_preview_cards_statuses.rb
@@ -15,10 +15,22 @@ class AddUniqueIndexOnPreviewCardsStatuses < ActiveRecord::Migration[6.1]
private
+ def supports_concurrent_reindex?
+ @supports_concurrent_reindex ||= begin
+ version = select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i
+ version >= 12_000
+ end
+ end
+
def deduplicate_and_reindex!
deduplicate_preview_cards!
- safety_assured { execute 'REINDEX INDEX CONCURRENTLY preview_cards_statuses_pkey' }
+ if supports_concurrent_reindex?
+ safety_assured { execute 'REINDEX INDEX CONCURRENTLY preview_cards_statuses_pkey' }
+ else
+ remove_index :preview_cards_statuses, name: :preview_cards_statuses_pkey
+ add_index :preview_cards_statuses, [:status_id, :preview_card_id], name: :preview_cards_statuses_pkey, algorithm: :concurrently, unique: true
+ end
rescue ActiveRecord::RecordNotUnique
retry
end
diff --git a/db/schema.rb b/db/schema.rb
index 863cddcbc..8e8411080 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# 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
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
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|
t.bigint "status_id", null: false
t.bigint "account_id"
diff --git a/dist/nginx.conf b/dist/nginx.conf
index 39fa58e50..5bb990386 100644
--- a/dist/nginx.conf
+++ b/dist/nginx.conf
@@ -36,7 +36,11 @@ server {
server_name example.com;
ssl_protocols TLSv1.2 TLSv1.3;
- ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
+
+ # You can use https://ssl-config.mozilla.org/ to generate your cipher set.
+ # We recommend their "Intermediate" level.
+ ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
+
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
diff --git a/lib/mastodon/cli/search.rb b/lib/mastodon/cli/search.rb
index 41862b5b6..481e01d8e 100644
--- a/lib/mastodon/cli/search.rb
+++ b/lib/mastodon/cli/search.rb
@@ -10,6 +10,7 @@ module Mastodon::CLI
InstancesIndex,
AccountsIndex,
TagsIndex,
+ PublicStatusesIndex,
StatusesIndex,
].freeze
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index 26ffaf038..db609de5e 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -16,12 +16,16 @@ module Mastodon
0
end
- def flags
- ENV['MASTODON_VERSION_FLAGS'].presence || '-beta2'
+ def default_prerelease
+ 'beta2'
end
- def suffix
- "+glitch#{ENV.fetch('MASTODON_VERSION_SUFFIX', '')}"
+ def prerelease
+ ENV['MASTODON_VERSION_PRERELEASE'].presence || default_prerelease
+ end
+
+ def build_metadata
+ ['glitch', ENV.fetch('MASTODON_VERSION_METADATA', nil)].compact.join('.')
end
def to_a
@@ -29,7 +33,14 @@ module Mastodon
end
def to_s
- [to_a.join('.'), flags, suffix].join
+ components = [to_a.join('.')]
+ components << "-#{prerelease}" if prerelease.present?
+ components << "+#{build_metadata}" if build_metadata.present?
+ components.join
+ end
+
+ def gem_version
+ @gem_version ||= Gem::Version.new(to_s.split('+')[0])
end
def repository
diff --git a/lib/paperclip/transcoder.rb b/lib/paperclip/transcoder.rb
index 0f2e30f7d..b88cf662c 100644
--- a/lib/paperclip/transcoder.rb
+++ b/lib/paperclip/transcoder.rb
@@ -4,6 +4,9 @@ module Paperclip
# This transcoder is only to be used for the MediaAttachment model
# to check when uploaded videos are actually gifv's
class Transcoder < Paperclip::Processor
+ # This is the H.264 "High" value taken from https://www.dr-lex.be/info-stuff/videocalc.html
+ BITS_PER_PIXEL = 0.11
+
def initialize(file, options = {}, attachment = nil)
super
@@ -38,8 +41,11 @@ module Paperclip
@output_options['vframes'] = 1
when 'mp4'
unless eligible_to_passthrough?(metadata)
- @output_options['acodec'] = 'aac'
- @output_options['strict'] = 'experimental'
+ bitrate = (metadata.width * metadata.height * 30 * BITS_PER_PIXEL) / 1_000
+
+ @output_options['b:v'] = "#{bitrate}k"
+ @output_options['maxrate'] = "#{bitrate + 192}k"
+ @output_options['bufsize'] = "#{bitrate * 5}k"
if high_vfr?(metadata)
@output_options['vsync'] = 'vfr'
diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake
index 010caaf8e..f68d1cf1f 100644
--- a/lib/tasks/mastodon.rake
+++ b/lib/tasks/mastodon.rake
@@ -424,6 +424,10 @@ namespace :mastodon do
end
end
+ prompt.say "\n"
+
+ env['UPDATE_CHECK_URL'] = '' unless prompt.yes?('Do you want Mastodon to periodically check for important updates and notify you? (Recommended)', default: true)
+
prompt.say "\n"
prompt.say 'This configuration will be written to .env.production'
diff --git a/package.json b/package.json
index abd470906..ff9ba32b8 100644
--- a/package.json
+++ b/package.json
@@ -85,6 +85,7 @@
"immutable": "^4.3.0",
"imports-loader": "^1.2.0",
"intl-messageformat": "^10.3.5",
+ "ioredis": "^5.3.2",
"js-yaml": "^4.1.0",
"jsdom": "^22.1.0",
"lodash": "^4.17.21",
@@ -121,7 +122,6 @@
"react-swipeable-views": "^0.14.0",
"react-textarea-autosize": "^8.4.1",
"react-toggle": "^4.1.3",
- "redis": "^4.6.5",
"redux": "^4.2.1",
"redux-immutable": "^4.0.0",
"redux-thunk": "^2.4.2",
@@ -155,7 +155,7 @@
},
"devDependencies": {
"@formatjs/cli": "^6.1.1",
- "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0",
"@types/babel__core": "^7.20.1",
"@types/emoji-mart": "^3.0.9",
diff --git a/spec/chewy/public_statuses_index_spec.rb b/spec/chewy/public_statuses_index_spec.rb
new file mode 100644
index 000000000..2f93d0ff0
--- /dev/null
+++ b/spec/chewy/public_statuses_index_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe PublicStatusesIndex do
+ describe 'Searching the index' do
+ before do
+ mock_elasticsearch_response(described_class, raw_response)
+ end
+
+ it 'returns results from a query' do
+ results = described_class.query(match: { name: 'status' })
+
+ expect(results).to eq []
+ end
+ end
+
+ def raw_response
+ {
+ took: 3,
+ hits: {
+ hits: [
+ {
+ _id: '0',
+ _score: 1.6375021,
+ },
+ ],
+ },
+ }
+ end
+end
diff --git a/spec/controllers/api/v1/timelines/tag_controller_spec.rb b/spec/controllers/api/v1/timelines/tag_controller_spec.rb
index 718911083..8896f02a7 100644
--- a/spec/controllers/api/v1/timelines/tag_controller_spec.rb
+++ b/spec/controllers/api/v1/timelines/tag_controller_spec.rb
@@ -5,36 +5,70 @@ require 'rails_helper'
describe Api::V1::Timelines::TagController do
render_views
- let(:user) { Fabricate(:user) }
+ let(:user) { Fabricate(:user) }
+ let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:statuses') }
before do
allow(controller).to receive(:doorkeeper_token) { token }
end
- context 'with a user context' do
- let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id) }
+ describe 'GET #show' do
+ subject do
+ get :show, params: { id: 'test' }
+ end
- describe 'GET #show' do
+ before do
+ PostStatusService.new.call(user.account, text: 'It is a #test')
+ end
+
+ context 'when the instance allows public preview' do
before do
- PostStatusService.new.call(user.account, text: 'It is a #test')
+ Setting.timeline_preview = true
end
- it 'returns http success' do
- get :show, params: { id: 'test' }
- expect(response).to have_http_status(200)
- expect(response.headers['Link'].links.size).to eq(2)
+ context 'when the user is not authenticated' do
+ let(:token) { nil }
+
+ it 'returns http success', :aggregate_failures do
+ subject
+
+ expect(response).to have_http_status(200)
+ expect(response.headers['Link'].links.size).to eq(2)
+ end
+ end
+
+ context 'when the user is authenticated' do
+ it 'returns http success', :aggregate_failures do
+ subject
+
+ expect(response).to have_http_status(200)
+ expect(response.headers['Link'].links.size).to eq(2)
+ end
end
end
- end
- context 'without a user context' do
- let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil) }
+ context 'when the instance does not allow public preview' do
+ before do
+ Form::AdminSettings.new(timeline_preview: false).save
+ end
- describe 'GET #show' do
- it 'returns http success' do
- get :show, params: { id: 'test' }
- expect(response).to have_http_status(200)
- expect(response.headers['Link']).to be_nil
+ context 'when the user is not authenticated' do
+ let(:token) { nil }
+
+ it 'returns http unauthorized' do
+ subject
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when the user is authenticated' do
+ it 'returns http success', :aggregate_failures do
+ subject
+
+ expect(response).to have_http_status(200)
+ expect(response.headers['Link'].links.size).to eq(2)
+ end
end
end
end
diff --git a/spec/controllers/well_known/webfinger_controller_spec.rb b/spec/controllers/well_known/webfinger_controller_spec.rb
index 8dc0f329b..20770a721 100644
--- a/spec/controllers/well_known/webfinger_controller_spec.rb
+++ b/spec/controllers/well_known/webfinger_controller_spec.rb
@@ -3,6 +3,8 @@
require 'rails_helper'
describe WellKnown::WebfingerController do
+ include RoutingHelper
+
render_views
describe 'GET #show' do
@@ -167,5 +169,67 @@ describe WellKnown::WebfingerController do
expect(response).to have_http_status(400)
end
end
+
+ context 'when an account has an avatar' do
+ let(:alice) { Fabricate(:account, username: 'alice', avatar: attachment_fixture('attachment.jpg')) }
+ let(:resource) { alice.to_webfinger_s }
+
+ it 'returns avatar in response' do
+ perform_show!
+
+ avatar_link = get_avatar_link(body_as_json)
+ expect(avatar_link).to_not be_nil
+ expect(avatar_link[:type]).to eq alice.avatar.content_type
+ expect(avatar_link[:href]).to eq full_asset_url(alice.avatar)
+ end
+
+ context 'with limited federation mode' do
+ before do
+ allow(Rails.configuration.x).to receive(:limited_federation_mode).and_return(true)
+ end
+
+ it 'does not return avatar in response' do
+ perform_show!
+
+ avatar_link = get_avatar_link(body_as_json)
+ expect(avatar_link).to be_nil
+ end
+ end
+
+ context 'when enabling DISALLOW_UNAUTHENTICATED_API_ACCESS' do
+ around do |example|
+ ClimateControl.modify DISALLOW_UNAUTHENTICATED_API_ACCESS: 'true' do
+ example.run
+ end
+ end
+
+ it 'does not return avatar in response' do
+ perform_show!
+
+ avatar_link = get_avatar_link(body_as_json)
+ expect(avatar_link).to be_nil
+ end
+ end
+ end
+
+ context 'when an account does not have an avatar' do
+ let(:alice) { Fabricate(:account, username: 'alice', avatar: nil) }
+ let(:resource) { alice.to_webfinger_s }
+
+ before do
+ perform_show!
+ end
+
+ it 'does not return avatar in response' do
+ avatar_link = get_avatar_link(body_as_json)
+ expect(avatar_link).to be_nil
+ end
+ end
+ end
+
+ private
+
+ def get_avatar_link(json)
+ json[:links].find { |link| link[:rel] == 'http://webfinger.net/rel/avatar' }
end
end
diff --git a/spec/fabricators/software_update_fabricator.rb b/spec/fabricators/software_update_fabricator.rb
new file mode 100644
index 000000000..622fff66e
--- /dev/null
+++ b/spec/fabricators/software_update_fabricator.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+Fabricator(:software_update) do
+ version '99.99.99'
+ urgent false
+ type 'patch'
+end
diff --git a/spec/features/admin/software_updates_spec.rb b/spec/features/admin/software_updates_spec.rb
new file mode 100644
index 000000000..4a635d1a7
--- /dev/null
+++ b/spec/features/admin/software_updates_spec.rb
@@ -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
diff --git a/spec/lib/admin/system_check/elasticsearch_check_spec.rb b/spec/lib/admin/system_check/elasticsearch_check_spec.rb
index f3918d403..a885640ce 100644
--- a/spec/lib/admin/system_check/elasticsearch_check_spec.rb
+++ b/spec/lib/admin/system_check/elasticsearch_check_spec.rb
@@ -17,6 +17,7 @@ describe Admin::SystemCheck::ElasticsearchCheck do
allow(Chewy.client.indices).to receive_messages(get_mapping: {
AccountsIndex.index_name => AccountsIndex.mappings_hash.deep_stringify_keys,
StatusesIndex.index_name => StatusesIndex.mappings_hash.deep_stringify_keys,
+ PublicStatusesIndex.index_name => PublicStatusesIndex.mappings_hash.deep_stringify_keys,
InstancesIndex.index_name => InstancesIndex.mappings_hash.deep_stringify_keys,
TagsIndex.index_name => TagsIndex.mappings_hash.deep_stringify_keys,
}, get_settings: {
@@ -90,6 +91,7 @@ describe Admin::SystemCheck::ElasticsearchCheck do
allow(Chewy.client.indices).to receive(:get_mapping).and_return({
AccountsIndex.index_name => AccountsIndex.mappings_hash.deep_stringify_keys,
StatusesIndex.index_name => StatusesIndex.mappings_hash.deep_stringify_keys,
+ PublicStatusesIndex.index_name => PublicStatusesIndex.mappings_hash.deep_stringify_keys,
InstancesIndex.index_name => InstancesIndex.mappings_hash.deep_stringify_keys,
TagsIndex.index_name => TagsIndex.mappings_hash.deep_stringify_keys,
})
diff --git a/spec/lib/admin/system_check/software_version_check_spec.rb b/spec/lib/admin/system_check/software_version_check_spec.rb
new file mode 100644
index 000000000..de4335fc5
--- /dev/null
+++ b/spec/lib/admin/system_check/software_version_check_spec.rb
@@ -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
diff --git a/spec/lib/importer/public_statuses_index_importer_spec.rb b/spec/lib/importer/public_statuses_index_importer_spec.rb
new file mode 100644
index 000000000..bc7c038a9
--- /dev/null
+++ b/spec/lib/importer/public_statuses_index_importer_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Importer::PublicStatusesIndexImporter do
+ describe 'import!' do
+ let(:pool) { Concurrent::FixedThreadPool.new(5) }
+ let(:importer) { described_class.new(batch_size: 123, executor: pool) }
+
+ before { Fabricate(:status, account: Fabricate(:account, indexable: true)) }
+
+ it 'indexes relevant statuses' do
+ expect { importer.import! }.to update_index(PublicStatusesIndex)
+ end
+ end
+end
diff --git a/spec/lib/search_query_parser_spec.rb b/spec/lib/search_query_parser_spec.rb
new file mode 100644
index 000000000..66b0e8f9e
--- /dev/null
+++ b/spec/lib/search_query_parser_spec.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+require 'parslet/rig/rspec'
+
+describe SearchQueryParser do
+ let(:parser) { described_class.new }
+
+ context 'with term' do
+ it 'consumes "hello"' do
+ expect(parser.term).to parse('hello')
+ end
+ end
+
+ context 'with prefix' do
+ it 'consumes "foo:"' do
+ expect(parser.prefix).to parse('foo:')
+ end
+ end
+
+ context 'with operator' do
+ it 'consumes "+"' do
+ expect(parser.operator).to parse('+')
+ end
+
+ it 'consumes "-"' do
+ expect(parser.operator).to parse('-')
+ end
+ end
+
+ context 'with shortcode' do
+ it 'consumes ":foo:"' do
+ expect(parser.shortcode).to parse(':foo:')
+ end
+ end
+
+ context 'with phrase' do
+ it 'consumes "hello world"' do
+ expect(parser.phrase).to parse('"hello world"')
+ end
+ end
+
+ context 'with clause' do
+ it 'consumes "foo"' do
+ expect(parser.clause).to parse('foo')
+ end
+
+ it 'consumes "-foo"' do
+ expect(parser.clause).to parse('-foo')
+ end
+
+ it 'consumes "foo:bar"' do
+ expect(parser.clause).to parse('foo:bar')
+ end
+
+ it 'consumes "-foo:bar"' do
+ expect(parser.clause).to parse('-foo:bar')
+ end
+
+ it 'consumes \'foo:"hello world"\'' do
+ expect(parser.clause).to parse('foo:"hello world"')
+ end
+
+ it 'consumes \'-foo:"hello world"\'' do
+ expect(parser.clause).to parse('-foo:"hello world"')
+ end
+
+ it 'consumes "foo:"' do
+ expect(parser.clause).to parse('foo:')
+ end
+
+ it 'consumes \'"\'' do
+ expect(parser.clause).to parse('"')
+ end
+ end
+
+ context 'with query' do
+ it 'consumes "hello -world"' do
+ expect(parser.query).to parse('hello -world')
+ end
+
+ it 'consumes \'foo "hello world"\'' do
+ expect(parser.query).to parse('foo "hello world"')
+ end
+
+ it 'consumes "foo:bar hello"' do
+ expect(parser.query).to parse('foo:bar hello')
+ end
+
+ it 'consumes \'"hello" world "\'' do
+ expect(parser.query).to parse('"hello" world "')
+ end
+
+ it 'consumes "foo:bar bar: hello"' do
+ expect(parser.query).to parse('foo:bar bar: hello')
+ end
+ end
+end
diff --git a/spec/lib/search_query_transformer_spec.rb b/spec/lib/search_query_transformer_spec.rb
index 109533469..17f06d283 100644
--- a/spec/lib/search_query_transformer_spec.rb
+++ b/spec/lib/search_query_transformer_spec.rb
@@ -3,16 +3,57 @@
require 'rails_helper'
describe SearchQueryTransformer do
- describe 'initialization' do
- let(:parser) { SearchQueryParser.new.parse('query') }
+ subject { described_class.new.apply(parser, current_account: nil) }
- it 'sets attributes' do
- transformer = described_class.new.apply(parser)
+ let(:parser) { SearchQueryParser.new.parse(query) }
- expect(transformer.should_clauses.first).to be_a(SearchQueryTransformer::TermClause)
- expect(transformer.must_clauses.first).to be_nil
- expect(transformer.must_not_clauses.first).to be_nil
- expect(transformer.filter_clauses.first).to be_nil
+ context 'with "hello world"' do
+ let(:query) { 'hello world' }
+
+ it 'transforms clauses' do
+ expect(subject.must_clauses.map(&:term)).to match_array %w(hello world)
+ expect(subject.must_not_clauses).to be_empty
+ expect(subject.filter_clauses).to be_empty
+ end
+ end
+
+ context 'with "hello -world"' do
+ let(:query) { 'hello -world' }
+
+ it 'transforms clauses' do
+ expect(subject.must_clauses.map(&:term)).to match_array %w(hello)
+ expect(subject.must_not_clauses.map(&:term)).to match_array %w(world)
+ expect(subject.filter_clauses).to be_empty
+ end
+ end
+
+ context 'with "hello is:reply"' do
+ let(:query) { 'hello is:reply' }
+
+ it 'transforms clauses' do
+ expect(subject.must_clauses.map(&:term)).to match_array %w(hello)
+ expect(subject.must_not_clauses).to be_empty
+ expect(subject.filter_clauses.map(&:term)).to match_array %w(reply)
+ end
+ end
+
+ context 'with "foo: bar"' do
+ let(:query) { 'foo: bar' }
+
+ it 'transforms clauses' do
+ expect(subject.must_clauses.map(&:term)).to match_array %w(foo bar)
+ expect(subject.must_not_clauses).to be_empty
+ expect(subject.filter_clauses).to be_empty
+ end
+ end
+
+ context 'with "foo:bar"' do
+ let(:query) { 'foo:bar' }
+
+ it 'transforms clauses' do
+ expect(subject.must_clauses.map(&:term)).to contain_exactly('foo bar')
+ expect(subject.must_not_clauses).to be_empty
+ expect(subject.filter_clauses).to be_empty
end
end
end
diff --git a/spec/mailers/admin_mailer_spec.rb b/spec/mailers/admin_mailer_spec.rb
index 9123804a4..423dce88a 100644
--- a/spec/mailers/admin_mailer_spec.rb
+++ b/spec/mailers/admin_mailer_spec.rb
@@ -85,4 +85,46 @@ RSpec.describe AdminMailer do
expect(mail.body.encoded).to match 'The following items need a review before they can be displayed publicly'
end
end
+
+ describe '.new_software_updates' do
+ let(:recipient) { Fabricate(:account, username: 'Bob') }
+ let(:mail) { described_class.with(recipient: recipient).new_software_updates }
+
+ before do
+ recipient.user.update(locale: :en)
+ end
+
+ it 'renders the headers' do
+ expect(mail.subject).to eq('New Mastodon versions are available for cb6e6126.ngrok.io!')
+ expect(mail.to).to eq [recipient.user_email]
+ expect(mail.from).to eq ['notifications@localhost']
+ end
+
+ it 'renders the body' do
+ expect(mail.body.encoded).to match 'New Mastodon versions have been released, you may want to update!'
+ end
+ end
+
+ describe '.new_critical_software_updates' do
+ let(:recipient) { Fabricate(:account, username: 'Bob') }
+ let(:mail) { described_class.with(recipient: recipient).new_critical_software_updates }
+
+ before do
+ recipient.user.update(locale: :en)
+ end
+
+ it 'renders the headers', :aggregate_failures do
+ expect(mail.subject).to eq('Critical Mastodon updates are available for cb6e6126.ngrok.io!')
+ expect(mail.to).to eq [recipient.user_email]
+ expect(mail.from).to eq ['notifications@localhost']
+
+ expect(mail['Importance'].value).to eq 'high'
+ expect(mail['Priority'].value).to eq 'urgent'
+ expect(mail['X-Priority'].value).to eq '1'
+ end
+
+ it 'renders the body' do
+ expect(mail.body.encoded).to match 'New critical versions of Mastodon have been released, you may want to update as soon as possible!'
+ end
+ end
end
diff --git a/spec/models/concerns/account_statuses_search_spec.rb b/spec/models/concerns/account_statuses_search_spec.rb
new file mode 100644
index 000000000..46362936f
--- /dev/null
+++ b/spec/models/concerns/account_statuses_search_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe AccountStatusesSearch do
+ let(:account) { Fabricate(:account, indexable: indexable) }
+
+ before do
+ allow(Chewy).to receive(:enabled?).and_return(true)
+ end
+
+ describe '#enqueue_update_public_statuses_index' do
+ before do
+ allow(account).to receive(:enqueue_add_to_public_statuses_index)
+ allow(account).to receive(:enqueue_remove_from_public_statuses_index)
+ end
+
+ context 'when account is indexable' do
+ let(:indexable) { true }
+
+ it 'enqueues add_to_public_statuses_index and not to remove_from_public_statuses_index' do
+ account.enqueue_update_public_statuses_index
+ expect(account).to have_received(:enqueue_add_to_public_statuses_index)
+ expect(account).to_not have_received(:enqueue_remove_from_public_statuses_index)
+ end
+ end
+
+ context 'when account is not indexable' do
+ let(:indexable) { false }
+
+ it 'enqueues remove_from_public_statuses_index and not to add_to_public_statuses_index' do
+ account.enqueue_update_public_statuses_index
+ expect(account).to have_received(:enqueue_remove_from_public_statuses_index)
+ expect(account).to_not have_received(:enqueue_add_to_public_statuses_index)
+ end
+ end
+ end
+
+ describe '#enqueue_add_to_public_statuses_index' do
+ let(:indexable) { true }
+ let(:worker) { AddToPublicStatusesIndexWorker }
+
+ before do
+ allow(worker).to receive(:perform_async)
+ end
+
+ it 'enqueues AddToPublicStatusesIndexWorker' do
+ account.enqueue_add_to_public_statuses_index
+ expect(worker).to have_received(:perform_async).with(account.id)
+ end
+ end
+
+ describe '#enqueue_remove_from_public_statuses_index' do
+ let(:indexable) { false }
+ let(:worker) { RemoveFromPublicStatusesIndexWorker }
+
+ before do
+ allow(worker).to receive(:perform_async)
+ end
+
+ it 'enqueues RemoveFromPublicStatusesIndexWorker' do
+ account.enqueue_remove_from_public_statuses_index
+ expect(worker).to have_received(:perform_async).with(account.id)
+ end
+ end
+end
diff --git a/spec/models/software_update_spec.rb b/spec/models/software_update_spec.rb
new file mode 100644
index 000000000..0a494b0c4
--- /dev/null
+++ b/spec/models/software_update_spec.rb
@@ -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
diff --git a/spec/policies/software_update_policy_spec.rb b/spec/policies/software_update_policy_spec.rb
new file mode 100644
index 000000000..e19ba6161
--- /dev/null
+++ b/spec/policies/software_update_policy_spec.rb
@@ -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
diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb
index 123bd4f56..cb69af5f5 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -83,15 +83,6 @@ describe SearchService, type: :service do
expect(Tag).to have_received(:search_for).with('tag', 10, 0, exclude_unreviewed: nil)
expect(results).to eq empty_results.merge(hashtags: [tag])
end
-
- it 'does not include tag when starts with @ character' do
- query = '@username'
- allow(Tag).to receive(:search_for)
-
- results = subject.call(query, nil, 10)
- expect(Tag).to_not have_received(:search_for)
- expect(results).to eq empty_results
- end
end
end
end
diff --git a/spec/services/software_update_check_service_spec.rb b/spec/services/software_update_check_service_spec.rb
new file mode 100644
index 000000000..c8821348a
--- /dev/null
+++ b/spec/services/software_update_check_service_spec.rb
@@ -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
diff --git a/spec/workers/add_to_public_statuses_index_worker_spec.rb b/spec/workers/add_to_public_statuses_index_worker_spec.rb
new file mode 100644
index 000000000..fa1507224
--- /dev/null
+++ b/spec/workers/add_to_public_statuses_index_worker_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe AddToPublicStatusesIndexWorker do
+ describe '#perform' do
+ let(:account) { Fabricate(:account, indexable: indexable) }
+ let(:account_id) { account.id }
+
+ before do
+ allow(Account).to receive(:find).with(account_id).and_return(account) unless account.nil?
+ allow(account).to receive(:add_to_public_statuses_index!) unless account.nil?
+ end
+
+ context 'when account is indexable' do
+ let(:indexable) { true }
+
+ it 'adds the account to the public statuses index' do
+ subject.perform(account_id)
+ expect(account).to have_received(:add_to_public_statuses_index!)
+ end
+ end
+
+ context 'when account is not indexable' do
+ let(:indexable) { false }
+
+ it 'does not add the account to public statuses index' do
+ subject.perform(account_id)
+ expect(account).to_not have_received(:add_to_public_statuses_index!)
+ end
+ end
+
+ context 'when account does not exist' do
+ let(:account) { nil }
+ let(:account_id) { 999 }
+
+ it 'does not raise an error' do
+ expect { subject.perform(account_id) }.to_not raise_error
+ end
+ end
+ end
+end
diff --git a/spec/workers/remove_from_public_statuses_index_worker_spec.rb b/spec/workers/remove_from_public_statuses_index_worker_spec.rb
new file mode 100644
index 000000000..43ff211ea
--- /dev/null
+++ b/spec/workers/remove_from_public_statuses_index_worker_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe RemoveFromPublicStatusesIndexWorker do
+ describe '#perform' do
+ let(:account) { Fabricate(:account, indexable: indexable) }
+ let(:account_id) { account.id }
+
+ before do
+ allow(Account).to receive(:find).with(account_id).and_return(account) unless account.nil?
+ allow(account).to receive(:remove_from_public_statuses_index!) unless account.nil?
+ end
+
+ context 'when account is not indexable' do
+ let(:indexable) { false }
+
+ it 'removes the account from public statuses index' do
+ subject.perform(account_id)
+ expect(account).to have_received(:remove_from_public_statuses_index!)
+ end
+ end
+
+ context 'when account is indexable' do
+ let(:indexable) { true }
+
+ it 'does not remove the account from public statuses index' do
+ subject.perform(account_id)
+ expect(account).to_not have_received(:remove_from_public_statuses_index!)
+ end
+ end
+
+ context 'when account does not exist' do
+ let(:account) { nil }
+ let(:account_id) { 999 }
+
+ it 'does not raise an error' do
+ expect { subject.perform(account_id) }.to_not raise_error
+ end
+ end
+ end
+end
diff --git a/spec/workers/scheduler/software_update_check_scheduler_spec.rb b/spec/workers/scheduler/software_update_check_scheduler_spec.rb
new file mode 100644
index 000000000..f596c0a1e
--- /dev/null
+++ b/spec/workers/scheduler/software_update_check_scheduler_spec.rb
@@ -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
diff --git a/streaming/index.js b/streaming/index.js
index ffb40ebc4..2f784f248 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -6,12 +6,12 @@ const url = require('url');
const dotenv = require('dotenv');
const express = require('express');
+const Redis = require('ioredis');
const { JSDOM } = require('jsdom');
const log = require('npmlog');
const pg = require('pg');
const dbUrlToConfig = require('pg-connection-string').parse;
const metrics = require('prom-client');
-const redis = require('redis');
const uuid = require('uuid');
const WebSocket = require('ws');
@@ -24,30 +24,12 @@ dotenv.config({
log.level = process.env.LOG_LEVEL || 'verbose';
/**
- * @param {Object.} defaultConfig
- * @param {string} redisUrl
+ * @param {Object.} config
*/
-const redisUrlToClient = async (defaultConfig, redisUrl) => {
- const config = defaultConfig;
-
- let client;
-
- if (!redisUrl) {
- client = redis.createClient(config);
- } else if (redisUrl.startsWith('unix://')) {
- client = redis.createClient(Object.assign(config, {
- socket: {
- path: redisUrl.slice(7),
- },
- }));
- } else {
- client = redis.createClient(Object.assign(config, {
- url: redisUrl,
- }));
- }
-
+const createRedisClient = async (config) => {
+ const { redisParams, redisUrl } = config;
+ const client = new Redis(redisUrl, redisParams);
client.on('error', (err) => log.error('Redis Client Error!', err));
- await client.connect();
return client;
};
@@ -147,23 +129,22 @@ const pgConfigFromEnv = (env) => {
* @returns {Object.} configuration for the Redis connection
*/
const redisConfigFromEnv = (env) => {
- const redisNamespace = env.REDIS_NAMESPACE || null;
+ // ioredis *can* transparently add prefixes for us, but it doesn't *in some cases*,
+ // which means we can't use it. But this is something that should be looked into.
+ const redisPrefix = env.REDIS_NAMESPACE ? `${env.REDIS_NAMESPACE}:` : '';
const redisParams = {
- socket: {
- host: env.REDIS_HOST || '127.0.0.1',
- port: env.REDIS_PORT || 6379,
- },
- database: env.REDIS_DB || 0,
+ host: env.REDIS_HOST || '127.0.0.1',
+ port: env.REDIS_PORT || 6379,
+ db: env.REDIS_DB || 0,
password: env.REDIS_PASSWORD || undefined,
};
- if (redisNamespace) {
- redisParams.namespace = redisNamespace;
+ // redisParams.path takes precedence over host and port.
+ if (env.REDIS_URL && env.REDIS_URL.startsWith('unix://')) {
+ redisParams.path = env.REDIS_URL.slice(7);
}
- const redisPrefix = redisNamespace ? `${redisNamespace}:` : '';
-
return {
redisParams,
redisPrefix,
@@ -179,15 +160,15 @@ const startServer = async () => {
const pgPool = new pg.Pool(pgConfigFromEnv(process.env));
const server = http.createServer(app);
- const { redisParams, redisUrl, redisPrefix } = redisConfigFromEnv(process.env);
-
/**
* @type {Object.): void>>}
*/
const subs = {};
- const redisSubscribeClient = await redisUrlToClient(redisParams, redisUrl);
- const redisClient = await redisUrlToClient(redisParams, redisUrl);
+ const redisConfig = redisConfigFromEnv(process.env);
+ const redisSubscribeClient = await createRedisClient(redisConfig);
+ const redisClient = await createRedisClient(redisConfig);
+ const { redisPrefix } = redisConfig;
// Collect metrics from Node.js
metrics.collectDefaultMetrics();
@@ -277,13 +258,13 @@ const startServer = async () => {
};
/**
- * @param {string} message
* @param {string} channel
+ * @param {string} message
*/
- const onRedisMessage = (message, channel) => {
+ const onRedisMessage = (channel, message) => {
const callbacks = subs[channel];
- log.silly(`New message on channel ${channel}`);
+ log.silly(`New message on channel ${redisPrefix}${channel}`);
if (!callbacks) {
return;
@@ -294,6 +275,7 @@ const startServer = async () => {
callbacks.forEach(callback => callback(json));
};
+ redisSubscribeClient.on("message", onRedisMessage);
/**
* @callback SubscriptionListener
@@ -312,8 +294,14 @@ const startServer = async () => {
if (subs[channel].length === 0) {
log.verbose(`Subscribe ${channel}`);
- redisSubscribeClient.subscribe(channel, onRedisMessage);
- redisSubscriptions.inc();
+ redisSubscribeClient.subscribe(channel, (err, count) => {
+ if (err) {
+ log.error(`Error subscribing to ${channel}`);
+ }
+ else {
+ redisSubscriptions.set(count);
+ }
+ });
}
subs[channel].push(callback);
@@ -334,8 +322,14 @@ const startServer = async () => {
if (subs[channel].length === 0) {
log.verbose(`Unsubscribe ${channel}`);
- redisSubscribeClient.unsubscribe(channel);
- redisSubscriptions.dec();
+ redisSubscribeClient.unsubscribe(channel, (err, count) => {
+ if (err) {
+ log.error(`Error unsubscribing to ${channel}`);
+ }
+ else {
+ redisSubscriptions.set(count);
+ }
+ });
delete subs[channel];
}
};
diff --git a/yarn.lock b/yarn.lock
index 783ddbef1..372863288 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -29,7 +29,7 @@
jsonpointer "^5.0.0"
leven "^3.1.0"
-"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.22.10", "@babel/code-frame@^7.22.5":
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.22.10":
version "7.22.10"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.10.tgz#1c20e612b768fefa75f6e90d6ecb86329247f0a3"
integrity sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==
@@ -44,30 +44,38 @@
dependencies:
"@babel/highlight" "^7.22.5"
-"@babel/compat-data@^7.22.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9":
+"@babel/code-frame@^7.22.5":
+ version "7.22.13"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e"
+ integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==
+ dependencies:
+ "@babel/highlight" "^7.22.13"
+ chalk "^2.4.2"
+
+"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9":
version "7.22.9"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730"
integrity sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==
"@babel/core@^7.10.4", "@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.22.1":
- version "7.22.10"
- resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.10.tgz#aad442c7bcd1582252cb4576747ace35bc122f35"
- integrity sha512-fTmqbbUBAwCcre6zPzNngvsI0aNrPZe77AeqvDxWM9Nm+04RrJ3CAmGHA9f7lJQY6ZMhRztNemy4uslDxTX4Qw==
+ version "7.22.11"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.11.tgz#8033acaa2aa24c3f814edaaa057f3ce0ba559c24"
+ integrity sha512-lh7RJrtPdhibbxndr6/xx0w8+CVlY5FJZiaSz908Fpy+G0xkBFTvwLcKJFF4PJxVfGhVWNebikpWGnOoC71juQ==
dependencies:
"@ampproject/remapping" "^2.2.0"
"@babel/code-frame" "^7.22.10"
"@babel/generator" "^7.22.10"
"@babel/helper-compilation-targets" "^7.22.10"
"@babel/helper-module-transforms" "^7.22.9"
- "@babel/helpers" "^7.22.10"
- "@babel/parser" "^7.22.10"
+ "@babel/helpers" "^7.22.11"
+ "@babel/parser" "^7.22.11"
"@babel/template" "^7.22.5"
- "@babel/traverse" "^7.22.10"
- "@babel/types" "^7.22.10"
+ "@babel/traverse" "^7.22.11"
+ "@babel/types" "^7.22.11"
convert-source-map "^1.7.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
- json5 "^2.2.2"
+ json5 "^2.2.3"
semver "^6.3.1"
"@babel/generator@^7.22.10", "@babel/generator@^7.22.5", "@babel/generator@^7.7.2":
@@ -113,10 +121,10 @@
lru-cache "^5.1.1"
semver "^6.3.1"
-"@babel/helper-create-class-features-plugin@^7.22.5":
- version "7.22.10"
- resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.10.tgz#dd2612d59eac45588021ac3d6fa976d08f4e95a3"
- integrity sha512-5IBb77txKYQPpOEdUdIhBx8VrZyDCQ+H82H0+5dX1TmuscP5vJKEE3cKurjtIw/vFwzbVH48VweE78kVDBrqjA==
+"@babel/helper-create-class-features-plugin@^7.22.11", "@babel/helper-create-class-features-plugin@^7.22.5":
+ version "7.22.11"
+ resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.11.tgz#4078686740459eeb4af3494a273ac09148dfb213"
+ integrity sha512-y1grdYL4WzmUDBRGK0pDbIoFd7UZKoDurDzWEoNMYoj1EL+foGRQNyPWDcC+YyegN5y1DUsFFmzjGijB3nSVAQ==
dependencies:
"@babel/helper-annotate-as-pure" "^7.22.5"
"@babel/helper-environment-visitor" "^7.22.5"
@@ -268,16 +276,25 @@
"@babel/template" "^7.22.5"
"@babel/types" "^7.22.10"
-"@babel/helpers@^7.22.10":
- version "7.22.10"
- resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.10.tgz#ae6005c539dfbcb5cd71fb51bfc8a52ba63bc37a"
- integrity sha512-a41J4NW8HyZa1I1vAndrraTlPZ/eZoga2ZgS7fEr0tZJGVU4xqdE80CEm0CcNjha5EZ8fTBYLKHF0kqDUuAwQw==
+"@babel/helpers@^7.22.11":
+ version "7.22.11"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.11.tgz#b02f5d5f2d7abc21ab59eeed80de410ba70b056a"
+ integrity sha512-vyOXC8PBWaGc5h7GMsNx68OH33cypkEDJCHvYVVgVbbxJDROYVtexSk0gK5iCF1xNjRIN2s8ai7hwkWDq5szWg==
dependencies:
"@babel/template" "^7.22.5"
- "@babel/traverse" "^7.22.10"
- "@babel/types" "^7.22.10"
+ "@babel/traverse" "^7.22.11"
+ "@babel/types" "^7.22.11"
-"@babel/highlight@^7.22.10", "@babel/highlight@^7.22.5":
+"@babel/highlight@^7.22.10", "@babel/highlight@^7.22.13":
+ version "7.22.13"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.13.tgz#9cda839e5d3be9ca9e8c26b6dd69e7548f0cbf16"
+ integrity sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.22.5"
+ chalk "^2.4.2"
+ js-tokens "^4.0.0"
+
+"@babel/highlight@^7.22.5":
version "7.22.10"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.10.tgz#02a3f6d8c1cb4521b2fd0ab0da8f4739936137d7"
integrity sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==
@@ -286,11 +303,21 @@
chalk "^2.4.2"
js-tokens "^4.0.0"
-"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.22.10", "@babel/parser@^7.22.5":
+"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7":
version "7.22.10"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.10.tgz#e37634f9a12a1716136c44624ef54283cabd3f55"
integrity sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==
+"@babel/parser@^7.22.11":
+ version "7.22.11"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.11.tgz#becf8ee33aad2a35ed5607f521fe6e72a615f905"
+ integrity sha512-R5zb8eJIBPJriQtbH/htEQy4k7E2dHWlD2Y2VT07JCzwYZHBxV5ZYtM0UhXSNMT74LyxuM+b1jdL7pSesXbC/g==
+
+"@babel/parser@^7.22.5":
+ version "7.22.14"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.14.tgz#c7de58e8de106e88efca42ce17f0033209dfd245"
+ integrity sha512-1KucTHgOvaw/LzCVrEOAyXkr9rQlp0A1HiHRYnSUE9dmb8PvPW7o5sscg+5169r54n3vGlbx6GevTE/Iw/P3AQ==
+
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz#87245a21cd69a73b0b81bcda98d443d6df08f05e"
@@ -467,10 +494,10 @@
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-async-generator-functions@^7.22.10":
- version "7.22.10"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.10.tgz#45946cd17f915b10e65c29b8ed18a0a50fc648c8"
- integrity sha512-eueE8lvKVzq5wIObKK/7dvoeKJ+xc6TvRn6aysIjS6pSCeLy7S/eVi7pEQknZqyqvzaNKdDtem8nUNTBgDVR2g==
+"@babel/plugin-transform-async-generator-functions@^7.22.11":
+ version "7.22.11"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.11.tgz#dbe3b1ff5a52e2e5edc4b19a60d325a675ed2649"
+ integrity sha512-0pAlmeRJn6wU84zzZsEOx1JV1Jf8fqO9ok7wofIJwUnplYo247dcd24P+cMJht7ts9xkzdtB0EPHmOb7F+KzXw==
dependencies:
"@babel/helper-environment-visitor" "^7.22.5"
"@babel/helper-plugin-utils" "^7.22.5"
@@ -508,12 +535,12 @@
"@babel/helper-create-class-features-plugin" "^7.22.5"
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-class-static-block@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz#3e40c46f048403472d6f4183116d5e46b1bff5ba"
- integrity sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA==
+"@babel/plugin-transform-class-static-block@^7.22.11":
+ version "7.22.11"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz#dc8cc6e498f55692ac6b4b89e56d87cec766c974"
+ integrity sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==
dependencies:
- "@babel/helper-create-class-features-plugin" "^7.22.5"
+ "@babel/helper-create-class-features-plugin" "^7.22.11"
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-class-static-block" "^7.14.5"
@@ -562,10 +589,10 @@
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-dynamic-import@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz#d6908a8916a810468c4edff73b5b75bda6ad393e"
- integrity sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ==
+"@babel/plugin-transform-dynamic-import@^7.22.11":
+ version "7.22.11"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz#2c7722d2a5c01839eaf31518c6ff96d408e447aa"
+ integrity sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-dynamic-import" "^7.8.3"
@@ -578,10 +605,10 @@
"@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.5"
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-export-namespace-from@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz#57c41cb1d0613d22f548fddd8b288eedb9973a5b"
- integrity sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg==
+"@babel/plugin-transform-export-namespace-from@^7.22.11":
+ version "7.22.11"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz#b3c84c8f19880b6c7440108f8929caf6056db26c"
+ integrity sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-export-namespace-from" "^7.8.3"
@@ -602,10 +629,10 @@
"@babel/helper-function-name" "^7.22.5"
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-json-strings@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz#14b64352fdf7e1f737eed68de1a1468bd2a77ec0"
- integrity sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A==
+"@babel/plugin-transform-json-strings@^7.22.11":
+ version "7.22.11"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz#689a34e1eed1928a40954e37f74509f48af67835"
+ integrity sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-json-strings" "^7.8.3"
@@ -617,10 +644,10 @@
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-logical-assignment-operators@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz#66ae5f068fd5a9a5dc570df16f56c2a8462a9d6c"
- integrity sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA==
+"@babel/plugin-transform-logical-assignment-operators@^7.22.11":
+ version "7.22.11"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz#24c522a61688bde045b7d9bc3c2597a4d948fc9c"
+ integrity sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
@@ -640,22 +667,22 @@
"@babel/helper-module-transforms" "^7.22.5"
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-modules-commonjs@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz#7d9875908d19b8c0536085af7b053fd5bd651bfa"
- integrity sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==
+"@babel/plugin-transform-modules-commonjs@^7.22.11":
+ version "7.22.11"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.11.tgz#d7991d3abad199c03b68ee66a64f216c47ffdfae"
+ integrity sha512-o2+bg7GDS60cJMgz9jWqRUsWkMzLCxp+jFDeDUT5sjRlAxcJWZ2ylNdI7QQ2+CH5hWu7OnN+Cv3htt7AkSf96g==
dependencies:
- "@babel/helper-module-transforms" "^7.22.5"
+ "@babel/helper-module-transforms" "^7.22.9"
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/helper-simple-access" "^7.22.5"
-"@babel/plugin-transform-modules-systemjs@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz#18c31410b5e579a0092638f95c896c2a98a5d496"
- integrity sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==
+"@babel/plugin-transform-modules-systemjs@^7.22.11":
+ version "7.22.11"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.11.tgz#3386be5875d316493b517207e8f1931d93154bb1"
+ integrity sha512-rIqHmHoMEOhI3VkVf5jQ15l539KrwhzqcBO6wdCNWPWc/JWt9ILNYNUssbRpeq0qWns8svuw8LnMNCvWBIJ8wA==
dependencies:
"@babel/helper-hoist-variables" "^7.22.5"
- "@babel/helper-module-transforms" "^7.22.5"
+ "@babel/helper-module-transforms" "^7.22.9"
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/helper-validator-identifier" "^7.22.5"
@@ -682,29 +709,29 @@
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-nullish-coalescing-operator@^7.22.3", "@babel/plugin-transform-nullish-coalescing-operator@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz#f8872c65776e0b552e0849d7596cddd416c3e381"
- integrity sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA==
+"@babel/plugin-transform-nullish-coalescing-operator@^7.22.11", "@babel/plugin-transform-nullish-coalescing-operator@^7.22.3":
+ version "7.22.11"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz#debef6c8ba795f5ac67cd861a81b744c5d38d9fc"
+ integrity sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
-"@babel/plugin-transform-numeric-separator@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz#57226a2ed9e512b9b446517ab6fa2d17abb83f58"
- integrity sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g==
+"@babel/plugin-transform-numeric-separator@^7.22.11":
+ version "7.22.11"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz#498d77dc45a6c6db74bb829c02a01c1d719cbfbd"
+ integrity sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-numeric-separator" "^7.10.4"
-"@babel/plugin-transform-object-rest-spread@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz#9686dc3447df4753b0b2a2fae7e8bc33cdc1f2e1"
- integrity sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ==
+"@babel/plugin-transform-object-rest-spread@^7.22.11":
+ version "7.22.11"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.11.tgz#dbbb06ce783cd994a8f430d8cefa553e9b42ca62"
+ integrity sha512-nX8cPFa6+UmbepISvlf5jhQyaC7ASs/7UxHmMkuJ/k5xSHvDPPaibMo+v3TXwU/Pjqhep/nFNpd3zn4YR59pnw==
dependencies:
- "@babel/compat-data" "^7.22.5"
- "@babel/helper-compilation-targets" "^7.22.5"
+ "@babel/compat-data" "^7.22.9"
+ "@babel/helper-compilation-targets" "^7.22.10"
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-object-rest-spread" "^7.8.3"
"@babel/plugin-transform-parameters" "^7.22.5"
@@ -717,18 +744,18 @@
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/helper-replace-supers" "^7.22.5"
-"@babel/plugin-transform-optional-catch-binding@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz#842080be3076703be0eaf32ead6ac8174edee333"
- integrity sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg==
+"@babel/plugin-transform-optional-catch-binding@^7.22.11":
+ version "7.22.11"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz#461cc4f578a127bb055527b3e77404cad38c08e0"
+ integrity sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
-"@babel/plugin-transform-optional-chaining@^7.22.10", "@babel/plugin-transform-optional-chaining@^7.22.5":
- version "7.22.10"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.10.tgz#076d28a7e074392e840d4ae587d83445bac0372a"
- integrity sha512-MMkQqZAZ+MGj+jGTG3OTuhKeBpNcO+0oCEbrGNEaOmiEn+1MzRyQlYsruGiU8RTK3zV6XwrVJTmwiDOyYK6J9g==
+"@babel/plugin-transform-optional-chaining@^7.22.12", "@babel/plugin-transform-optional-chaining@^7.22.5":
+ version "7.22.12"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.12.tgz#d7ebf6a88cd2f4d307b0e000ab630acd8124b333"
+ integrity sha512-7XXCVqZtyFWqjDsYDY4T45w4mlx1rf7aOgkc/Ww76xkgBiOlmjPkx36PBLHa1k1rwWvVgYMPsbuVnIamx2ZQJw==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/helper-skip-transparent-expression-wrappers" "^7.22.5"
@@ -749,13 +776,13 @@
"@babel/helper-create-class-features-plugin" "^7.22.5"
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-private-property-in-object@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz#07a77f28cbb251546a43d175a1dda4cf3ef83e32"
- integrity sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ==
+"@babel/plugin-transform-private-property-in-object@^7.22.11":
+ version "7.22.11"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz#ad45c4fc440e9cb84c718ed0906d96cf40f9a4e1"
+ integrity sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==
dependencies:
"@babel/helper-annotate-as-pure" "^7.22.5"
- "@babel/helper-create-class-features-plugin" "^7.22.5"
+ "@babel/helper-create-class-features-plugin" "^7.22.11"
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-private-property-in-object" "^7.14.5"
@@ -877,13 +904,13 @@
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-typescript@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.5.tgz#5c0f7adfc1b5f38c4dbc8f79b1f0f8074134bd7d"
- integrity sha512-SMubA9S7Cb5sGSFFUlqxyClTA9zWJ8qGQrppNUm05LtFuN1ELRFNndkix4zUJrC9F+YivWwa1dHMSyo0e0N9dA==
+"@babel/plugin-transform-typescript@^7.22.11":
+ version "7.22.11"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.11.tgz#9f27fb5e51585729374bb767ab6a6d9005a23329"
+ integrity sha512-0E4/L+7gfvHub7wsbTv03oRtD69X31LByy44fGmFzbZScpupFByMcgCJ0VbBTkzyjSJKuRoGN8tcijOWKTmqOA==
dependencies:
"@babel/helper-annotate-as-pure" "^7.22.5"
- "@babel/helper-create-class-features-plugin" "^7.22.5"
+ "@babel/helper-create-class-features-plugin" "^7.22.11"
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-typescript" "^7.22.5"
@@ -919,9 +946,9 @@
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.12.1", "@babel/preset-env@^7.22.4":
- version "7.22.10"
- resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.22.10.tgz#3263b9fe2c8823d191d28e61eac60a79f9ce8a0f"
- integrity sha512-riHpLb1drNkpLlocmSyEg4oYJIQFeXAK/d7rI6mbD0XsvoTOOweXDmQPG/ErxsEhWk3rl3Q/3F6RFQlVFS8m0A==
+ version "7.22.14"
+ resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.22.14.tgz#1cbb468d899f64fa71c53446f13b7ff8c0005cc1"
+ integrity sha512-daodMIoVo+ol/g+//c/AH+szBkFj4STQUikvBijRGL72Ph+w+AMTSh55DUETe8KJlPlDT1k/mp7NBfOuiWmoig==
dependencies:
"@babel/compat-data" "^7.22.9"
"@babel/helper-compilation-targets" "^7.22.10"
@@ -949,41 +976,41 @@
"@babel/plugin-syntax-top-level-await" "^7.14.5"
"@babel/plugin-syntax-unicode-sets-regex" "^7.18.6"
"@babel/plugin-transform-arrow-functions" "^7.22.5"
- "@babel/plugin-transform-async-generator-functions" "^7.22.10"
+ "@babel/plugin-transform-async-generator-functions" "^7.22.11"
"@babel/plugin-transform-async-to-generator" "^7.22.5"
"@babel/plugin-transform-block-scoped-functions" "^7.22.5"
"@babel/plugin-transform-block-scoping" "^7.22.10"
"@babel/plugin-transform-class-properties" "^7.22.5"
- "@babel/plugin-transform-class-static-block" "^7.22.5"
+ "@babel/plugin-transform-class-static-block" "^7.22.11"
"@babel/plugin-transform-classes" "^7.22.6"
"@babel/plugin-transform-computed-properties" "^7.22.5"
"@babel/plugin-transform-destructuring" "^7.22.10"
"@babel/plugin-transform-dotall-regex" "^7.22.5"
"@babel/plugin-transform-duplicate-keys" "^7.22.5"
- "@babel/plugin-transform-dynamic-import" "^7.22.5"
+ "@babel/plugin-transform-dynamic-import" "^7.22.11"
"@babel/plugin-transform-exponentiation-operator" "^7.22.5"
- "@babel/plugin-transform-export-namespace-from" "^7.22.5"
+ "@babel/plugin-transform-export-namespace-from" "^7.22.11"
"@babel/plugin-transform-for-of" "^7.22.5"
"@babel/plugin-transform-function-name" "^7.22.5"
- "@babel/plugin-transform-json-strings" "^7.22.5"
+ "@babel/plugin-transform-json-strings" "^7.22.11"
"@babel/plugin-transform-literals" "^7.22.5"
- "@babel/plugin-transform-logical-assignment-operators" "^7.22.5"
+ "@babel/plugin-transform-logical-assignment-operators" "^7.22.11"
"@babel/plugin-transform-member-expression-literals" "^7.22.5"
"@babel/plugin-transform-modules-amd" "^7.22.5"
- "@babel/plugin-transform-modules-commonjs" "^7.22.5"
- "@babel/plugin-transform-modules-systemjs" "^7.22.5"
+ "@babel/plugin-transform-modules-commonjs" "^7.22.11"
+ "@babel/plugin-transform-modules-systemjs" "^7.22.11"
"@babel/plugin-transform-modules-umd" "^7.22.5"
"@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5"
"@babel/plugin-transform-new-target" "^7.22.5"
- "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.5"
- "@babel/plugin-transform-numeric-separator" "^7.22.5"
- "@babel/plugin-transform-object-rest-spread" "^7.22.5"
+ "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.11"
+ "@babel/plugin-transform-numeric-separator" "^7.22.11"
+ "@babel/plugin-transform-object-rest-spread" "^7.22.11"
"@babel/plugin-transform-object-super" "^7.22.5"
- "@babel/plugin-transform-optional-catch-binding" "^7.22.5"
- "@babel/plugin-transform-optional-chaining" "^7.22.10"
+ "@babel/plugin-transform-optional-catch-binding" "^7.22.11"
+ "@babel/plugin-transform-optional-chaining" "^7.22.12"
"@babel/plugin-transform-parameters" "^7.22.5"
"@babel/plugin-transform-private-methods" "^7.22.5"
- "@babel/plugin-transform-private-property-in-object" "^7.22.5"
+ "@babel/plugin-transform-private-property-in-object" "^7.22.11"
"@babel/plugin-transform-property-literals" "^7.22.5"
"@babel/plugin-transform-regenerator" "^7.22.10"
"@babel/plugin-transform-reserved-words" "^7.22.5"
@@ -997,7 +1024,7 @@
"@babel/plugin-transform-unicode-regex" "^7.22.5"
"@babel/plugin-transform-unicode-sets-regex" "^7.22.5"
"@babel/preset-modules" "0.1.6-no-external-plugins"
- "@babel/types" "^7.22.10"
+ "@babel/types" "^7.22.11"
babel-plugin-polyfill-corejs2 "^0.4.5"
babel-plugin-polyfill-corejs3 "^0.8.3"
babel-plugin-polyfill-regenerator "^0.5.2"
@@ -1026,15 +1053,15 @@
"@babel/plugin-transform-react-pure-annotations" "^7.22.5"
"@babel/preset-typescript@^7.21.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.22.5.tgz#16367d8b01d640e9a507577ed4ee54e0101e51c8"
- integrity sha512-YbPaal9LxztSGhmndR46FmAbkJ/1fAsw293tSU+I5E5h+cnJ3d4GTwyUgGYmOXJYdGA+uNePle4qbaRzj2NISQ==
+ version "7.22.11"
+ resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.22.11.tgz#f218cd0345524ac888aa3dc32f029de5b064b575"
+ integrity sha512-tWY5wyCZYBGY7IlalfKI1rLiGlIfnwsRHZqlky0HVv8qviwQ1Uo/05M6+s+TcTCVa6Bmoo2uJW5TMFX6Wa4qVg==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/helper-validator-option" "^7.22.5"
"@babel/plugin-syntax-jsx" "^7.22.5"
- "@babel/plugin-transform-modules-commonjs" "^7.22.5"
- "@babel/plugin-transform-typescript" "^7.22.5"
+ "@babel/plugin-transform-modules-commonjs" "^7.22.11"
+ "@babel/plugin-transform-typescript" "^7.22.11"
"@babel/regjsgen@^0.8.0":
version "0.8.0"
@@ -1049,9 +1076,9 @@
regenerator-runtime "^0.12.0"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.22.3", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
- version "7.22.10"
- resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.10.tgz#ae3e9631fd947cb7e3610d3e9d8fef5f76696682"
- integrity sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==
+ version "7.22.11"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.11.tgz#7a9ba3bbe406ad6f9e8dd4da2ece453eb23a77a4"
+ integrity sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA==
dependencies:
regenerator-runtime "^0.14.0"
@@ -1080,10 +1107,10 @@
debug "^4.1.0"
globals "^11.1.0"
-"@babel/traverse@^7.22.10":
- version "7.22.10"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.10.tgz#20252acb240e746d27c2e82b4484f199cf8141aa"
- integrity sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig==
+"@babel/traverse@^7.22.11":
+ version "7.22.11"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.11.tgz#71ebb3af7a05ff97280b83f05f8865ac94b2027c"
+ integrity sha512-mzAenteTfomcB7mfPtyi+4oe5BZ6MXxWcn4CX+h4IRJ+OOGXBrWU6jDQavkQI9Vuc5P+donFabBfFCcmWka9lQ==
dependencies:
"@babel/code-frame" "^7.22.10"
"@babel/generator" "^7.22.10"
@@ -1091,12 +1118,12 @@
"@babel/helper-function-name" "^7.22.5"
"@babel/helper-hoist-variables" "^7.22.5"
"@babel/helper-split-export-declaration" "^7.22.6"
- "@babel/parser" "^7.22.10"
- "@babel/types" "^7.22.10"
+ "@babel/parser" "^7.22.11"
+ "@babel/types" "^7.22.11"
debug "^4.1.0"
globals "^11.1.0"
-"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.10", "@babel/types@^7.22.5", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
+"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.3.3":
version "7.22.10"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.10.tgz#4a9e76446048f2c66982d1a989dd12b8a2d2dc03"
integrity sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==
@@ -1114,6 +1141,15 @@
"@babel/helper-validator-identifier" "^7.22.5"
to-fast-properties "^2.0.0"
+"@babel/types@^7.22.10", "@babel/types@^7.22.11", "@babel/types@^7.22.5", "@babel/types@^7.4.4":
+ version "7.22.11"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.11.tgz#0e65a6a1d4d9cbaa892b2213f6159485fe632ea2"
+ integrity sha512-siazHiGuZRz9aB9NpHy9GOs9xiQPKnMzgdr493iI1M67vRXpnEq8ZOOKzezC5q7zwuQ6sDhdSp4SD9ixKSqKZg==
+ dependencies:
+ "@babel/helper-string-parser" "^7.22.5"
+ "@babel/helper-validator-identifier" "^7.22.5"
+ to-fast-properties "^2.0.0"
+
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
@@ -1249,9 +1285,9 @@
eslint-visitor-keys "^3.3.0"
"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1":
- version "4.6.2"
- resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8"
- integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==
+ version "4.8.0"
+ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.8.0.tgz#11195513186f68d42fbf449f9a7136b2c0c92005"
+ integrity sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==
"@eslint/eslintrc@^2.1.2":
version "2.1.2"
@@ -1268,10 +1304,10 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
-"@eslint/js@^8.47.0":
- version "8.47.0"
- resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.47.0.tgz#5478fdf443ff8158f9de171c704ae45308696c7d"
- integrity sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==
+"@eslint/js@8.48.0":
+ version "8.48.0"
+ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.48.0.tgz#642633964e217905436033a2bd08bf322849b7fb"
+ integrity sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==
"@floating-ui/core@^1.3.1":
version "1.3.1"
@@ -1416,6 +1452,11 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
+"@ioredis/commands@^1.1.1":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11"
+ integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==
+
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
@@ -1500,12 +1541,12 @@
"@types/node" "*"
jest-mock "^29.6.2"
-"@jest/expect-utils@^29.6.2":
- version "29.6.2"
- resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.6.2.tgz#1b97f290d0185d264dd9fdec7567a14a38a90534"
- integrity sha512-6zIhM8go3RV2IG4aIZaZbxwpOzz3ZiM23oxAlkquOIole+G6TrbeXnykxWYlqF7kz2HlBjdKtca20x9atkEQYg==
+"@jest/expect-utils@^29.6.2", "@jest/expect-utils@^29.6.4":
+ version "29.6.4"
+ resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.6.4.tgz#17c7dfe6cec106441f218b0aff4b295f98346679"
+ integrity sha512-FEhkJhqtvBwgSpiTrocquJCdXPsyvNKcl/n7A3u7X4pVoF4bswm11c9d4AV+kfq2Gpv/mM8x7E7DsRvH+djkrg==
dependencies:
- jest-get-type "^29.4.3"
+ jest-get-type "^29.6.3"
"@jest/expect@^29.6.2":
version "29.6.2"
@@ -1567,10 +1608,10 @@
strip-ansi "^6.0.0"
v8-to-istanbul "^9.0.1"
-"@jest/schemas@^29.6.0":
- version "29.6.0"
- resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.0.tgz#0f4cb2c8e3dca80c135507ba5635a4fd755b0040"
- integrity sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==
+"@jest/schemas@^29.6.3":
+ version "29.6.3"
+ resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03"
+ integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==
dependencies:
"@sinclair/typebox" "^0.27.8"
@@ -1624,12 +1665,12 @@
slash "^3.0.0"
write-file-atomic "^4.0.2"
-"@jest/types@^29.6.1":
- version "29.6.1"
- resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.1.tgz#ae79080278acff0a6af5eb49d063385aaa897bf2"
- integrity sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw==
+"@jest/types@^29.6.1", "@jest/types@^29.6.3":
+ version "29.6.3"
+ resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59"
+ integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==
dependencies:
- "@jest/schemas" "^29.6.0"
+ "@jest/schemas" "^29.6.3"
"@types/istanbul-lib-coverage" "^2.0.0"
"@types/istanbul-reports" "^3.0.0"
"@types/node" "*"
@@ -1750,40 +1791,6 @@
resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-7.0.7.tgz#54af8d66160a8a7bf7d8f184703d2bf4b3fab914"
integrity sha512-J2v5Ca7HgejO7diGKiDylaVDQKmbQ5FJih6Oo3hXuBKEuXlcaccJu64lj8MNVLaPVyZx0g4gaOQZQz95QEb/hg==
-"@redis/bloom@1.2.0":
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.2.0.tgz#d3fd6d3c0af3ef92f26767b56414a370c7b63b71"
- integrity sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==
-
-"@redis/client@1.5.9":
- version "1.5.9"
- resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.5.9.tgz#c4ee81bbfedb4f1d9c7c5e9859661b9388fb4021"
- integrity sha512-SffgN+P1zdWJWSXBvJeynvEnmnZrYmtKSRW00xl8pOPFOMJjxRR9u0frSxJpPR6Y4V+k54blJjGW7FgxbTI7bQ==
- dependencies:
- cluster-key-slot "1.1.2"
- generic-pool "3.9.0"
- yallist "4.0.0"
-
-"@redis/graph@1.1.0":
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/@redis/graph/-/graph-1.1.0.tgz#cc2b82e5141a29ada2cce7d267a6b74baa6dd519"
- integrity sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==
-
-"@redis/json@1.0.4":
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/@redis/json/-/json-1.0.4.tgz#f372b5f93324e6ffb7f16aadcbcb4e5c3d39bda1"
- integrity sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==
-
-"@redis/search@1.1.3":
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/@redis/search/-/search-1.1.3.tgz#b5a6837522ce9028267fe6f50762a8bcfd2e998b"
- integrity sha512-4Dg1JjvCevdiCBTZqjhKkGoC5/BcB7k9j99kdMnaXFXg8x4eyOIVg9487CMv7/BUVkFLZCaIh8ead9mU15DNng==
-
-"@redis/time-series@1.0.5":
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-1.0.5.tgz#a6d70ef7a0e71e083ea09b967df0a0ed742bc6ad"
- integrity sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==
-
"@reduxjs/toolkit@^1.9.5":
version "1.9.5"
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.5.tgz#d3987849c24189ca483baa7aa59386c8e52077c4"
@@ -1984,14 +1991,13 @@
lz-string "^1.5.0"
pretty-format "^27.0.2"
-"@testing-library/jest-dom@^5.16.5":
- version "5.17.0"
- resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz#5e97c8f9a15ccf4656da00fecab505728de81e0c"
- integrity sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg==
+"@testing-library/jest-dom@^6.0.0":
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.0.0.tgz#d2ba5a3fd13724d5966b3f8cd24d2cedcab4fa76"
+ integrity sha512-Ye2R3+/oM27jir8CzYPmuWdavTaKwNZcu0d22L9pO/vnOYE0wmrtpw79TQJa8H6gV8/i7yd+pLaqeLlA0rTMfg==
dependencies:
"@adobe/css-tools" "^4.0.1"
"@babel/runtime" "^7.9.2"
- "@types/testing-library__jest-dom" "^5.9.1"
aria-query "^5.0.0"
chalk "^3.0.0"
css.escape "^1.5.1"
@@ -2187,7 +2193,7 @@
dependencies:
"@types/istanbul-lib-report" "*"
-"@types/jest@*", "@types/jest@^29.5.2":
+"@types/jest@^29.5.2":
version "29.5.3"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.3.tgz#7a35dc0044ffb8b56325c6802a4781a626b05777"
integrity sha512-1Nq7YrO/vJE/FYnqYyw0FS8LdrjExSgIiHyKg7xPpn+yi8Q4huZryKnkJatN1ZRH89Kw2v33/8ZMB7DuZeSLlA==
@@ -2250,9 +2256,9 @@
integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
"@types/node@*":
- version "20.4.9"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.9.tgz#c7164e0f8d3f12dfae336af0b1f7fdec8c6b204f"
- integrity sha512-8e2HYcg7ohnTUbHk8focoklEQYvemQmu9M/f43DZVx43kHn0tE3BY/6gSDxS7k0SprtS0NHvj+L80cGLnoOUcQ==
+ version "20.5.7"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.7.tgz#4b8ecac87fbefbc92f431d09c30e176fc0a7c377"
+ integrity sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==
"@types/node@14 || 16 || 17":
version "17.0.45"
@@ -2428,9 +2434,9 @@
"@types/react" "*"
"@types/react@*", "@types/react@16 || 17 || 18", "@types/react@>=16.9.11", "@types/react@^18.0.26", "@types/react@^18.2.7":
- version "18.2.20"
- resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.20.tgz#1605557a83df5c8a2cc4eeb743b3dfc0eb6aaeb2"
- integrity sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw==
+ version "18.2.21"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.21.tgz#774c37fd01b522d0b91aed04811b58e4e0514ed9"
+ integrity sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
@@ -2497,13 +2503,6 @@
resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.8.tgz#b94a4391c85666c7b73299fd3ad79d4faa435310"
integrity sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==
-"@types/testing-library__jest-dom@^5.9.1":
- version "5.14.9"
- resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz#0fb1e6a0278d87b6737db55af5967570b67cb466"
- integrity sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw==
- dependencies:
- "@types/jest" "*"
-
"@types/tough-cookie@*":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397"
@@ -2570,15 +2569,15 @@
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^6.0.0":
- version "6.4.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.4.0.tgz#53428b616f7d80fe879f45a08f11cc0f0b62cf13"
- integrity sha512-62o2Hmc7Gs3p8SLfbXcipjWAa6qk2wZGChXG2JbBtYpwSRmti/9KHLqfbLs9uDigOexG+3PaQ9G2g3201FWLKg==
+ version "6.4.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.4.1.tgz#bc0c6f000134b53c304ad0bec4ee4753cd3e89d2"
+ integrity sha512-3F5PtBzUW0dYlq77Lcqo13fv+58KDwUib3BddilE8ajPJT+faGgxmI9Sw+I8ZS22BYwoir9ZhNXcLi+S+I2bkw==
dependencies:
"@eslint-community/regexpp" "^4.5.1"
- "@typescript-eslint/scope-manager" "6.4.0"
- "@typescript-eslint/type-utils" "6.4.0"
- "@typescript-eslint/utils" "6.4.0"
- "@typescript-eslint/visitor-keys" "6.4.0"
+ "@typescript-eslint/scope-manager" "6.4.1"
+ "@typescript-eslint/type-utils" "6.4.1"
+ "@typescript-eslint/utils" "6.4.1"
+ "@typescript-eslint/visitor-keys" "6.4.1"
debug "^4.3.4"
graphemer "^1.4.0"
ignore "^5.2.4"
@@ -2587,31 +2586,31 @@
ts-api-utils "^1.0.1"
"@typescript-eslint/parser@^6.0.0":
- version "6.4.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.4.0.tgz#47e7c6e22ff1248e8675d95f488890484de67600"
- integrity sha512-I1Ah1irl033uxjxO9Xql7+biL3YD7w9IU8zF+xlzD/YxY6a4b7DYA08PXUUCbm2sEljwJF6ERFy2kTGAGcNilg==
+ version "6.4.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.4.1.tgz#85ad550bf4ac4aa227504f1becb828f8e46c44e3"
+ integrity sha512-610G6KHymg9V7EqOaNBMtD1GgpAmGROsmfHJPXNLCU9bfIuLrkdOygltK784F6Crboyd5tBFayPB7Sf0McrQwg==
dependencies:
- "@typescript-eslint/scope-manager" "6.4.0"
- "@typescript-eslint/types" "6.4.0"
- "@typescript-eslint/typescript-estree" "6.4.0"
- "@typescript-eslint/visitor-keys" "6.4.0"
+ "@typescript-eslint/scope-manager" "6.4.1"
+ "@typescript-eslint/types" "6.4.1"
+ "@typescript-eslint/typescript-estree" "6.4.1"
+ "@typescript-eslint/visitor-keys" "6.4.1"
debug "^4.3.4"
-"@typescript-eslint/scope-manager@6.4.0":
- version "6.4.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.4.0.tgz#3048e4262ba3eafa4e2e69b08912d9037ec646ae"
- integrity sha512-TUS7vaKkPWDVvl7GDNHFQMsMruD+zhkd3SdVW0d7b+7Zo+bd/hXJQ8nsiUZMi1jloWo6c9qt3B7Sqo+flC1nig==
+"@typescript-eslint/scope-manager@6.4.1":
+ version "6.4.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.4.1.tgz#4b073a30be2dbe603e44e9ae0cff7e1d3ed19278"
+ integrity sha512-p/OavqOQfm4/Hdrr7kvacOSFjwQ2rrDVJRPxt/o0TOWdFnjJptnjnZ+sYDR7fi4OimvIuKp+2LCkc+rt9fIW+A==
dependencies:
- "@typescript-eslint/types" "6.4.0"
- "@typescript-eslint/visitor-keys" "6.4.0"
+ "@typescript-eslint/types" "6.4.1"
+ "@typescript-eslint/visitor-keys" "6.4.1"
-"@typescript-eslint/type-utils@6.4.0":
- version "6.4.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.4.0.tgz#c8ac92716ed6a9d5443aa3e342910355b0796ba0"
- integrity sha512-TvqrUFFyGY0cX3WgDHcdl2/mMCWCDv/0thTtx/ODMY1QhEiyFtv/OlLaNIiYLwRpAxAtOLOY9SUf1H3Q3dlwAg==
+"@typescript-eslint/type-utils@6.4.1":
+ version "6.4.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.4.1.tgz#fa21cb13016c8d6f352fe9b2d6c9ab6edc2d1857"
+ integrity sha512-7ON8M8NXh73SGZ5XvIqWHjgX2f+vvaOarNliGhjrJnv1vdjG0LVIz+ToYfPirOoBi56jxAKLfsLm40+RvxVVXA==
dependencies:
- "@typescript-eslint/typescript-estree" "6.4.0"
- "@typescript-eslint/utils" "6.4.0"
+ "@typescript-eslint/typescript-estree" "6.4.1"
+ "@typescript-eslint/utils" "6.4.1"
debug "^4.3.4"
ts-api-utils "^1.0.1"
@@ -2620,10 +2619,10 @@
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.0.tgz#3fcdac7dbf923ec5251545acdd9f1d42d7c4fe32"
integrity sha512-yR2h1NotF23xFFYKHZs17QJnB51J/s+ud4PYU4MqdZbzeNxpgUr05+dNeCN/bb6raslHvGdd6BFCkVhpPk/ZeA==
-"@typescript-eslint/types@6.4.0":
- version "6.4.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.4.0.tgz#5b109a59a805f0d8d375895e42d9e5f0037f66ee"
- integrity sha512-+FV9kVFrS7w78YtzkIsNSoYsnOtrYVnKWSTVXoL1761CsCRv5wpDOINgsXpxD67YCLZtVQekDDyaxfjVWUJmmg==
+"@typescript-eslint/types@6.4.1":
+ version "6.4.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.4.1.tgz#b2c61159f46dda210fed9f117f5d027f65bb5c3b"
+ integrity sha512-zAAopbNuYu++ijY1GV2ylCsQsi3B8QvfPHVqhGdDcbx/NK5lkqMnCGU53amAjccSpk+LfeONxwzUhDzArSfZJg==
"@typescript-eslint/typescript-estree@5.59.0":
version "5.59.0"
@@ -2638,30 +2637,30 @@
semver "^7.3.7"
tsutils "^3.21.0"
-"@typescript-eslint/typescript-estree@6.4.0":
- version "6.4.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.4.0.tgz#3c58d20632db93fec3d6ab902acbedf593d37276"
- integrity sha512-iDPJArf/K2sxvjOR6skeUCNgHR/tCQXBsa+ee1/clRKr3olZjZ/dSkXPZjG6YkPtnW6p5D1egeEPMCW6Gn4yLA==
+"@typescript-eslint/typescript-estree@6.4.1":
+ version "6.4.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.4.1.tgz#91ff88101c710adb0f70a317f2f65efa9441da45"
+ integrity sha512-xF6Y7SatVE/OyV93h1xGgfOkHr2iXuo8ip0gbfzaKeGGuKiAnzS+HtVhSPx8Www243bwlW8IF7X0/B62SzFftg==
dependencies:
- "@typescript-eslint/types" "6.4.0"
- "@typescript-eslint/visitor-keys" "6.4.0"
+ "@typescript-eslint/types" "6.4.1"
+ "@typescript-eslint/visitor-keys" "6.4.1"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
semver "^7.5.4"
ts-api-utils "^1.0.1"
-"@typescript-eslint/utils@6.4.0":
- version "6.4.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.4.0.tgz#23e996b693603c5924b1fbb733cc73196256baa5"
- integrity sha512-BvvwryBQpECPGo8PwF/y/q+yacg8Hn/2XS+DqL/oRsOPK+RPt29h5Ui5dqOKHDlbXrAeHUTnyG3wZA0KTDxRZw==
+"@typescript-eslint/utils@6.4.1":
+ version "6.4.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.4.1.tgz#81bf62ff0c3119a26c19fab683582e29450717bc"
+ integrity sha512-F/6r2RieNeorU0zhqZNv89s9bDZSovv3bZQpUNOmmQK1L80/cV4KEu95YUJWi75u5PhboFoKUJBnZ4FQcoqhDw==
dependencies:
"@eslint-community/eslint-utils" "^4.4.0"
"@types/json-schema" "^7.0.12"
"@types/semver" "^7.5.0"
- "@typescript-eslint/scope-manager" "6.4.0"
- "@typescript-eslint/types" "6.4.0"
- "@typescript-eslint/typescript-estree" "6.4.0"
+ "@typescript-eslint/scope-manager" "6.4.1"
+ "@typescript-eslint/types" "6.4.1"
+ "@typescript-eslint/typescript-estree" "6.4.1"
semver "^7.5.4"
"@typescript-eslint/visitor-keys@5.59.0":
@@ -2672,12 +2671,12 @@
"@typescript-eslint/types" "5.59.0"
eslint-visitor-keys "^3.3.0"
-"@typescript-eslint/visitor-keys@6.4.0":
- version "6.4.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.4.0.tgz#96a426cdb1add28274abd7a34aefe27f8b7d51ef"
- integrity sha512-yJSfyT+uJm+JRDWYRYdCm2i+pmvXJSMtPR9Cq5/XQs4QIgNoLcoRtDdzsLbLsFM/c6um6ohQkg/MLxWvoIndJA==
+"@typescript-eslint/visitor-keys@6.4.1":
+ version "6.4.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.4.1.tgz#e3ccf7b8d42e625946ac5094ed92a405fb4115e0"
+ integrity sha512-y/TyRJsbZPkJIZQXrHfdnxVnxyKegnpEvnRGNam7s3TRR2ykGefEWOhaef00/UUN3IZxizS7BTO3svd3lCOJRQ==
dependencies:
- "@typescript-eslint/types" "6.4.0"
+ "@typescript-eslint/types" "6.4.1"
eslint-visitor-keys "^3.4.1"
"@webassemblyjs/ast@1.9.0":
@@ -2883,16 +2882,16 @@ acorn@^6.4.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
-acorn@^8.0.4, acorn@^8.8.2:
- version "8.8.2"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
- integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
-
-acorn@^8.1.0, acorn@^8.8.1, acorn@^8.9.0:
+acorn@^8.0.4, acorn@^8.1.0, acorn@^8.8.1, acorn@^8.9.0:
version "8.10.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5"
integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==
+acorn@^8.8.2:
+ version "8.8.2"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
+ integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
+
agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
@@ -3319,9 +3318,9 @@ axe-core@^4.6.2:
integrity sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==
axios@^1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f"
- integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-1.5.0.tgz#f02e4af823e2e46a9768cfc74691fdd0517ea267"
+ integrity sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==
dependencies:
follow-redirects "^1.15.0"
form-data "^4.0.0"
@@ -3906,7 +3905,12 @@ caniuse-lite@^1.0.30001502:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001515.tgz#418aefeed9d024cd3129bfae0ccc782d4cb8f12b"
integrity sha512-eEFDwUOZbE24sb+Ecsx3+OvNETqjWIdabMy52oOkIgcUtAsQifjUG9q4U9dgTHJM2mfk4uEPxc0+xuFdJ629QA==
-caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001520:
+caniuse-lite@^1.0.30001517:
+ version "1.0.30001524"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001524.tgz#1e14bce4f43c41a7deaeb5ebfe86664fe8dadb80"
+ integrity sha512-Jj917pJtYg9HSJBF95HVX3Cdr89JUyLT4IZ8SvM5aDRni95swKgYi3TgYLH5hnGfPE/U1dg6IfZ50UsIlLkwSA==
+
+caniuse-lite@^1.0.30001520:
version "1.0.30001520"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001520.tgz#62e2b7a1c7b35269594cf296a80bdf8cb9565006"
integrity sha512-tahF5O9EiiTzwTUqAeFjIZbn4Dnqxzz7ktrgGlMYNLH43Ul26IgTMH/zvL3DG0lZxBYnlT04axvInszUsZULdA==
@@ -4083,7 +4087,7 @@ clone-deep@^4.0.1:
kind-of "^6.0.2"
shallow-clone "^3.0.0"
-cluster-key-slot@1.1.2:
+cluster-key-slot@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac"
integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==
@@ -4305,11 +4309,11 @@ copy-descriptor@^0.1.0:
integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==
core-js-compat@^3.31.0:
- version "3.32.0"
- resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.32.0.tgz#f41574b6893ab15ddb0ac1693681bd56c8550a90"
- integrity sha512-7a9a3D1k4UCVKnLhrgALyFcP7YCsLOQIxPd0dKjf/6GuPcgyiGP70ewWdCGrSK7evyhymi0qO4EqCmSJofDeYw==
+ version "3.32.1"
+ resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.32.1.tgz#55f9a7d297c0761a8eb1d31b593e0f5b6ffae964"
+ integrity sha512-GSvKDv4wE0bPnQtjklV101juQ85g6H3rm5PDP20mqlS5j0kXF3pP97YvAu5hl+uFHqMictp3b2VxOHljWMAtuA==
dependencies:
- browserslist "^4.21.9"
+ browserslist "^4.21.10"
core-js@^2.5.0:
version "2.6.12"
@@ -4829,6 +4833,11 @@ delegates@^1.0.0:
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==
+denque@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
+ integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
+
depd@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
@@ -4884,10 +4893,10 @@ detect-passive-events@^2.0.3:
dependencies:
detect-it "^4.0.1"
-diff-sequences@^29.4.3:
- version "29.4.3"
- resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2"
- integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==
+diff-sequences@^29.6.3:
+ version "29.6.3"
+ resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921"
+ integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==
diffie-hellman@^5.0.0:
version "5.0.3"
@@ -5055,9 +5064,9 @@ electron-to-chromium@^1.4.428:
integrity sha512-/g3UyNDmDd6ebeWapmAoiyy+Sy2HyJ+/X8KyvNeHfKRFfHaA2W8oF5fxD5F3tjBDcjpwo0iek6YNgxNXDBoEtA==
electron-to-chromium@^1.4.477:
- version "1.4.490"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.490.tgz#d99286f6e915667fa18ea4554def1aa60eb4d5f1"
- integrity sha512-6s7NVJz+sATdYnIwhdshx/N/9O6rvMxmhVoDSDFdj6iA45gHR8EQje70+RYsF4GeB+k0IeNSBnP7yG9ZXJFr7A==
+ version "1.4.505"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.505.tgz#00571ade5975b58413f0f56a665b065bfc29cdfc"
+ integrity sha512-0A50eL5BCCKdxig2SsCXhpuztnB9PfUgRMojj5tMvt8O54lbwz3t6wNgnpiTRosw5QjlJB7ixhVyeg8daLQwSQ==
elliptic@^6.5.3:
version "6.5.4"
@@ -5490,14 +5499,14 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
eslint@^8.41.0:
- version "8.47.0"
- resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.47.0.tgz#c95f9b935463fb4fad7005e626c7621052e90806"
- integrity sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==
+ version "8.48.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.48.0.tgz#bf9998ba520063907ba7bfe4c480dc8be03c2155"
+ integrity sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.6.1"
"@eslint/eslintrc" "^2.1.2"
- "@eslint/js" "^8.47.0"
+ "@eslint/js" "8.48.0"
"@humanwhocodes/config-array" "^0.11.10"
"@humanwhocodes/module-importer" "^1.0.1"
"@nodelib/fs.walk" "^1.2.8"
@@ -5701,7 +5710,18 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
dependencies:
homedir-polyfill "^1.0.1"
-expect@^29.0.0, expect@^29.6.2:
+expect@^29.0.0:
+ version "29.6.4"
+ resolved "https://registry.yarnpkg.com/expect/-/expect-29.6.4.tgz#a6e6f66d4613717859b2fe3da98a739437b6f4b8"
+ integrity sha512-F2W2UyQ8XYyftHT57dtfg8Ue3X5qLgm2sSug0ivvLRH/VKNRL/pDxg/TH7zVzbQB0tu80clNFy6LU7OS/VSEKA==
+ dependencies:
+ "@jest/expect-utils" "^29.6.4"
+ jest-get-type "^29.6.3"
+ jest-matcher-utils "^29.6.4"
+ jest-message-util "^29.6.3"
+ jest-util "^29.6.3"
+
+expect@^29.6.2:
version "29.6.2"
resolved "https://registry.yarnpkg.com/expect/-/expect-29.6.2.tgz#7b08e83eba18ddc4a2cf62b5f2d1918f5cd84521"
integrity sha512-iAErsLxJ8C+S02QbLAwgSGSezLQK+XXRDt8IuFXFpwCNw2ECmzZSmjKcCaFVp5VRMk+WAvz6h6jokzEzBFZEuA==
@@ -5962,14 +5982,15 @@ findup-sync@^3.0.0:
resolve-dir "^1.0.1"
flat-cache@^3.0.4:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
- integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.1.0.tgz#0e54ab4a1a60fe87e2946b6b00657f1c99e1af3f"
+ integrity sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==
dependencies:
- flatted "^3.1.0"
+ flatted "^3.2.7"
+ keyv "^4.5.3"
rimraf "^3.0.2"
-flatted@^3.1.0:
+flatted@^3.2.7:
version "3.2.7"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
@@ -6109,11 +6130,6 @@ gauge@^5.0.0:
strip-ansi "^6.0.1"
wide-align "^1.1.5"
-generic-pool@3.9.0:
- version "3.9.0"
- resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.9.0.tgz#36f4a678e963f4fdb8707eab050823abc4e8f5e4"
- integrity sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==
-
gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
@@ -6199,9 +6215,9 @@ glob-parent@^6.0.2:
is-glob "^4.0.3"
glob@^10.2.5, glob@^10.2.6:
- version "10.3.3"
- resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.3.tgz#8360a4ffdd6ed90df84aa8d52f21f452e86a123b"
- integrity sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw==
+ version "10.3.4"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.4.tgz#c85c9c7ab98669102b6defda76d35c5b1ef9766f"
+ integrity sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==
dependencies:
foreground-child "^3.1.0"
jackspeak "^2.0.3"
@@ -6658,9 +6674,9 @@ immutable@^3.8.2:
integrity sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==
immutable@^4.0.0, immutable@^4.0.0-rc.1, immutable@^4.3.0:
- version "4.3.3"
- resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.3.tgz#8934ff6826d996a7642c8dc4b46e694dd19561e3"
- integrity sha512-808ZFYMsIRAjLAu5xkKo0TsbY9LBy9H5MazTKIEHerNkg0ymgilGfBPMR/3G7d/ihGmuK2Hw8S1izY2d3kd3wA==
+ version "4.3.4"
+ resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f"
+ integrity sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==
import-fresh@^3.2.1:
version "3.3.0"
@@ -6793,6 +6809,21 @@ invariant@^2.2.2, invariant@^2.2.4:
dependencies:
loose-envify "^1.0.0"
+ioredis@^5.3.2:
+ version "5.3.2"
+ resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.3.2.tgz#9139f596f62fc9c72d873353ac5395bcf05709f7"
+ integrity sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==
+ dependencies:
+ "@ioredis/commands" "^1.1.1"
+ cluster-key-slot "^1.1.0"
+ debug "^4.3.4"
+ denque "^2.1.0"
+ lodash.defaults "^4.2.0"
+ lodash.isarguments "^3.1.0"
+ redis-errors "^1.2.0"
+ redis-parser "^3.0.0"
+ standard-as-callback "^2.1.0"
+
ip-regex@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
@@ -7304,9 +7335,9 @@ iterator.prototype@^1.1.0:
reflect.getprototypeof "^1.0.3"
jackspeak@^2.0.3:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.2.1.tgz#655e8cf025d872c9c03d3eb63e8f0c024fef16a6"
- integrity sha512-MXbxovZ/Pm42f6cDIDkl3xpwv1AGwObKwfmjs2nQePiy85tP3fatofl3FC1aBsOtP/6fq5SbtgHwWcMsLP+bDw==
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.1.tgz#ce2effa4c458e053640e61938865a5b5fae98456"
+ integrity sha512-4iSY3Bh1Htv+kLhiiZunUhQ+OYXIn0ze3ulq8JeWrFKmhPAJSySV2+kdtRh2pGcCeF0s6oR8Oc+pYZynJj4t8A==
dependencies:
"@isaacs/cliui" "^8.0.2"
optionalDependencies:
@@ -7402,15 +7433,15 @@ jest-config@^29.6.2:
slash "^3.0.0"
strip-json-comments "^3.1.1"
-jest-diff@^29.6.2:
- version "29.6.2"
- resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.6.2.tgz#c36001e5543e82a0805051d3ceac32e6825c1c46"
- integrity sha512-t+ST7CB9GX5F2xKwhwCf0TAR17uNDiaPTZnVymP9lw0lssa9vG+AFyDZoeIHStU3WowFFwT+ky+er0WVl2yGhA==
+jest-diff@^29.6.2, jest-diff@^29.6.4:
+ version "29.6.4"
+ resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.6.4.tgz#85aaa6c92a79ae8cd9a54ebae8d5b6d9a513314a"
+ integrity sha512-9F48UxR9e4XOEZvoUXEHSWY4qC4zERJaOfrbBg9JpbJOO43R1vN76REt/aMGZoY6GD5g84nnJiBIVlscegefpw==
dependencies:
chalk "^4.0.0"
- diff-sequences "^29.4.3"
- jest-get-type "^29.4.3"
- pretty-format "^29.6.2"
+ diff-sequences "^29.6.3"
+ jest-get-type "^29.6.3"
+ pretty-format "^29.6.3"
jest-docblock@^29.4.3:
version "29.4.3"
@@ -7456,10 +7487,10 @@ jest-environment-node@^29.6.2:
jest-mock "^29.6.2"
jest-util "^29.6.2"
-jest-get-type@^29.4.3:
- version "29.4.3"
- resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5"
- integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==
+jest-get-type@^29.4.3, jest-get-type@^29.6.3:
+ version "29.6.3"
+ resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1"
+ integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==
jest-haste-map@^29.6.2:
version "29.6.2"
@@ -7488,28 +7519,28 @@ jest-leak-detector@^29.6.2:
jest-get-type "^29.4.3"
pretty-format "^29.6.2"
-jest-matcher-utils@^29.6.2:
- version "29.6.2"
- resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.6.2.tgz#39de0be2baca7a64eacb27291f0bd834fea3a535"
- integrity sha512-4LiAk3hSSobtomeIAzFTe+N8kL6z0JtF3n6I4fg29iIW7tt99R7ZcIFW34QkX+DuVrf+CUe6wuVOpm7ZKFJzZQ==
+jest-matcher-utils@^29.6.2, jest-matcher-utils@^29.6.4:
+ version "29.6.4"
+ resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.6.4.tgz#327db7ababea49455df3b23e5d6109fe0c709d24"
+ integrity sha512-KSzwyzGvK4HcfnserYqJHYi7sZVqdREJ9DMPAKVbS98JsIAvumihaNUbjrWw0St7p9IY7A9UskCW5MYlGmBQFQ==
dependencies:
chalk "^4.0.0"
- jest-diff "^29.6.2"
- jest-get-type "^29.4.3"
- pretty-format "^29.6.2"
+ jest-diff "^29.6.4"
+ jest-get-type "^29.6.3"
+ pretty-format "^29.6.3"
-jest-message-util@^29.6.2:
- version "29.6.2"
- resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.6.2.tgz#af7adc2209c552f3f5ae31e77cf0a261f23dc2bb"
- integrity sha512-vnIGYEjoPSuRqV8W9t+Wow95SDp6KPX2Uf7EoeG9G99J2OVh7OSwpS4B6J0NfpEIpfkBNHlBZpA2rblEuEFhZQ==
+jest-message-util@^29.6.2, jest-message-util@^29.6.3:
+ version "29.6.3"
+ resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.6.3.tgz#bce16050d86801b165f20cfde34dc01d3cf85fbf"
+ integrity sha512-FtzaEEHzjDpQp51HX4UMkPZjy46ati4T5pEMyM6Ik48ztu4T9LQplZ6OsimHx7EuM9dfEh5HJa6D3trEftu3dA==
dependencies:
"@babel/code-frame" "^7.12.13"
- "@jest/types" "^29.6.1"
+ "@jest/types" "^29.6.3"
"@types/stack-utils" "^2.0.0"
chalk "^4.0.0"
graceful-fs "^4.2.9"
micromatch "^4.0.4"
- pretty-format "^29.6.2"
+ pretty-format "^29.6.3"
slash "^3.0.0"
stack-utils "^2.0.3"
@@ -7636,12 +7667,12 @@ jest-snapshot@^29.6.2:
pretty-format "^29.6.2"
semver "^7.5.3"
-jest-util@^29.6.2:
- version "29.6.2"
- resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.6.2.tgz#8a052df8fff2eebe446769fd88814521a517664d"
- integrity sha512-3eX1qb6L88lJNCFlEADKOkjpXJQyZRiavX1INZ4tRnrBVr2COd3RgcTLyUiEXMNBlDU/cgYq6taUS0fExrWW4w==
+jest-util@^29.6.2, jest-util@^29.6.3:
+ version "29.6.3"
+ resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.6.3.tgz#e15c3eac8716440d1ed076f09bc63ace1aebca63"
+ integrity sha512-QUjna/xSy4B32fzcKTSz1w7YYzgiHrjjJjevdRf61HYk998R5vVMMNmrHESYZVDS5DSWs+1srPLPKxXPkeSDOA==
dependencies:
- "@jest/types" "^29.6.1"
+ "@jest/types" "^29.6.3"
"@types/node" "*"
chalk "^4.0.0"
ci-info "^3.2.0"
@@ -7815,6 +7846,11 @@ jsesc@~0.5.0:
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==
+json-buffer@3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
+ integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
+
json-parse-better-errors@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
@@ -7859,7 +7895,7 @@ json5@^1.0.1, json5@^1.0.2:
dependencies:
minimist "^1.2.0"
-json5@^2.1.2, json5@^2.2.0, json5@^2.2.2:
+json5@^2.1.2, json5@^2.2.0, json5@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
@@ -7906,6 +7942,13 @@ keycode@^2.1.7:
resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.1.tgz#09c23b2be0611d26117ea2501c2c391a01f39eff"
integrity sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==
+keyv@^4.5.3:
+ version "4.5.3"
+ resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.3.tgz#00873d2b046df737963157bd04f294ca818c9c25"
+ integrity sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==
+ dependencies:
+ json-buffer "3.0.1"
+
killable@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"
@@ -8056,6 +8099,16 @@ lodash.debounce@^4.0.8:
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
+lodash.escape@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98"
+ integrity sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw==
+
+lodash.flatten@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
+ integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==
+
lodash.get@^4.0:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
@@ -8066,6 +8119,11 @@ lodash.has@^4.0:
resolved "https://registry.yarnpkg.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862"
integrity sha512-rnYUdIo6xRCJnQmbVFEwcxF144erlD+M3YcJUVesflU9paQaE8p+fJDcIQrlMYbxoANFL+AB9hZrzSBBk5PL+g==
+lodash.invokemap@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/lodash.invokemap/-/lodash.invokemap-4.6.0.tgz#1748cda5d8b0ef8369c4eb3ec54c21feba1f2d62"
+ integrity sha512-CfkycNtMqgUlfjfdh2BhKO/ZXrP8ePOX5lEU/g0R3ItJcnuxWDwokMGKx1hWcfOikmyOVx6X9IwWnDGlgKl61w==
+
lodash.isboolean@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
@@ -8091,6 +8149,11 @@ lodash.merge@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
+lodash.pullall@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/lodash.pullall/-/lodash.pullall-4.2.0.tgz#9d98b8518b7c965b0fae4099bd9fb7df8bbf38ba"
+ integrity sha512-VhqxBKH0ZxPpLhiu68YD1KnHmbhQJQctcipvmFnqIBDYzcIHzf3Zpu0tpeOKtR4x76p9yohc506eGdOjTmyIBg==
+
lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
@@ -8106,6 +8169,11 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
+lodash.uniqby@^4.7.0:
+ version "4.7.0"
+ resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302"
+ integrity sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==
+
lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
@@ -8148,9 +8216,9 @@ lru-cache@^6.0.0:
yallist "^4.0.0"
"lru-cache@^9.1.1 || ^10.0.0":
- version "10.0.0"
- resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.0.tgz#b9e2a6a72a129d81ab317202d93c7691df727e61"
- integrity sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw==
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.1.tgz#0a3be479df549cca0e5d693ac402ff19537a6b7a"
+ integrity sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==
lz-string@^1.5.0:
version "1.5.0"
@@ -8482,9 +8550,9 @@ minipass@^5.0.0:
integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==
"minipass@^5.0.0 || ^6.0.2 || ^7.0.0":
- version "7.0.2"
- resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.2.tgz#58a82b7d81c7010da5bd4b2c0c85ac4b4ec5131e"
- integrity sha512-eL79dXrE1q9dBbDCLg7xfn/vl7MS4F1gvJAgjJrQli/jbQWdUttuVawphqpffoIYfRdq78LHx6GP4bU/EQ2ATA==
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.3.tgz#05ea638da44e475037ed94d1c7efcc76a25e1974"
+ integrity sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==
minizlib@^2.1.1:
version "2.1.2"
@@ -9581,9 +9649,9 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@^8.2.15, postcss@^8.4.24, postcss@^8.4.25:
- version "8.4.28"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.28.tgz#c6cc681ed00109072816e1557f889ef51cf950a5"
- integrity sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==
+ version "8.4.29"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.29.tgz#33bc121cf3b3688d4ddef50be869b2a54185a1dd"
+ integrity sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==
dependencies:
nanoid "^3.3.6"
picocolors "^1.0.0"
@@ -9669,12 +9737,12 @@ pretty-format@^27.0.2:
ansi-styles "^5.0.0"
react-is "^17.0.1"
-pretty-format@^29.0.0, pretty-format@^29.6.2:
- version "29.6.2"
- resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.2.tgz#3d5829261a8a4d89d8b9769064b29c50ed486a47"
- integrity sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==
+pretty-format@^29.0.0, pretty-format@^29.6.2, pretty-format@^29.6.3:
+ version "29.6.3"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.3.tgz#d432bb4f1ca6f9463410c3fb25a0ba88e594ace7"
+ integrity sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==
dependencies:
- "@jest/schemas" "^29.6.0"
+ "@jest/schemas" "^29.6.3"
ansi-styles "^5.0.0"
react-is "^18.0.0"
@@ -10216,17 +10284,17 @@ redent@^4.0.0:
indent-string "^5.0.0"
strip-indent "^4.0.0"
-redis@^4.6.5:
- version "4.6.8"
- resolved "https://registry.yarnpkg.com/redis/-/redis-4.6.8.tgz#54c5992e8a5ba512506fe9f53142cadc405547e7"
- integrity sha512-S7qNkPUYrsofQ0ztWlTHSaK0Qqfl1y+WMIxrzeAGNG+9iUZB4HGeBgkHxE6uJJ6iXrkvLd1RVJ2nvu6H1sAzfQ==
+redis-errors@^1.0.0, redis-errors@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
+ integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==
+
+redis-parser@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
+ integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==
dependencies:
- "@redis/bloom" "1.2.0"
- "@redis/client" "1.5.9"
- "@redis/graph" "1.1.0"
- "@redis/json" "1.0.4"
- "@redis/search" "1.1.3"
- "@redis/time-series" "1.0.5"
+ redis-errors "^1.0.0"
redux-immutable@^4.0.0:
version "4.0.0"
@@ -10861,14 +10929,14 @@ signal-exit@^4.0.1:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
-sirv@^1.0.7:
- version "1.0.19"
- resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.19.tgz#1d73979b38c7fe91fcba49c85280daa9c2363b49"
- integrity sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==
+sirv@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/sirv/-/sirv-2.0.3.tgz#ca5868b87205a74bef62a469ed0296abceccd446"
+ integrity sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==
dependencies:
"@polka/url" "^1.0.0-next.20"
mrmime "^1.0.0"
- totalist "^1.0.0"
+ totalist "^3.0.0"
sisteransi@^1.0.5:
version "1.0.5"
@@ -11144,6 +11212,11 @@ stacktrace-js@^2.0.2:
stack-generator "^2.0.5"
stacktrace-gps "^3.0.4"
+standard-as-callback@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
+ integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==
+
static-extend@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
@@ -11301,7 +11374,6 @@ stringz@^2.1.0:
char-regex "^1.0.2"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
- name strip-ansi-cjs
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -11788,10 +11860,10 @@ toidentifier@1.0.1:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
-totalist@^1.0.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df"
- integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==
+totalist@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8"
+ integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==
tough-cookie@^4.1.2:
version "4.1.3"
@@ -12344,19 +12416,26 @@ webpack-assets-manifest@^4.0.6:
webpack-sources "^1.0"
webpack-bundle-analyzer@^4.8.0:
- version "4.9.0"
- resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.9.0.tgz#fc093c4ab174fd3dcbd1c30b763f56d10141209d"
- integrity sha512-+bXGmO1LyiNx0i9enBu3H8mv42sj/BJWhZNFwjz92tVnBa9J3JMGo2an2IXlEleoDOPn/Hofl5hr/xCpObUDtw==
+ version "4.9.1"
+ resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.9.1.tgz#d00bbf3f17500c10985084f22f1a2bf45cb2f09d"
+ integrity sha512-jnd6EoYrf9yMxCyYDPj8eutJvtjQNp8PHmni/e/ulydHBWhT5J3menXt3HEkScsu9YqMAcG4CfFjs3rj5pVU1w==
dependencies:
"@discoveryjs/json-ext" "0.5.7"
acorn "^8.0.4"
acorn-walk "^8.0.0"
- chalk "^4.1.0"
commander "^7.2.0"
+ escape-string-regexp "^4.0.0"
gzip-size "^6.0.0"
- lodash "^4.17.20"
+ is-plain-object "^5.0.0"
+ lodash.debounce "^4.0.8"
+ lodash.escape "^4.0.1"
+ lodash.flatten "^4.4.0"
+ lodash.invokemap "^4.6.0"
+ lodash.pullall "^4.2.0"
+ lodash.uniqby "^4.7.0"
opener "^1.5.2"
- sirv "^1.0.7"
+ picocolors "^1.0.0"
+ sirv "^2.0.3"
ws "^7.3.1"
webpack-cli@^3.3.12:
@@ -12893,16 +12972,16 @@ y18n@^5.0.5:
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
-yallist@4.0.0, yallist@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
- integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
-
yallist@^3.0.2:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
+yallist@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+ integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
yaml@^1.10.0:
version "1.10.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"