diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index f991036add..b3b1d97a24 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 0000000000..ca9156fdaa
--- /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 ce14169aae..fa8d6542c1 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 a2658ea8ba..20aecd71d6 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 1b15d19885..a1aeddf201 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 a3ae671d67..2600cf924b 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 f78e5ebada..cf11d66533 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 fa923e9606..b9728f6a23 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 107dfaca3f..fe66adc08a 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 cb5096581c..cdabc4c7c8 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 f801b1b560..4e30c42222 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 ba04fb661b..fbb2c2de23 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 1f8571c09d..00db257ac7 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 0000000000..5c68a13658
--- /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 9f680efa52..2be7e45250 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 0000000000..52d8cb41e6
--- /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 7c7d70fd32..76ba758245 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 540b17d009..ec5ea5b85b 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 9cd7b99046..a79d65c124 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 c57031da3c..4d7805abad 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 1d27c92c8c..b0c4fff8bc 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 c2648eedd8..1102c89fad 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 0000000000..ce87526e6a
--- /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 01b4157f8c..0000000000
--- 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 0155d933e4..d9a8ea4cfd 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 20912e28d0..1b2bfb98f1 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 6b8864a039..095fb3155e 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 d3d432ae05..36abc69930 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 2f1b46e5d9..e83c1761ec 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 2b36945eee..49fd62b966 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 0000000000..d0dd2b6acd
--- /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 e17680d8bb..80dae5e4d0 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 c0726404ee..f6984d5adb 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 46228c3306..8d277f8c18 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 dd240e99d4..d37451d005 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 ad0dfe0177..b0fe21bcf1 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 7adeaeee01..2f4027b03f 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 d4860258ed..39b9bd291d 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 99ed697e37..be35077dcd 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 2f8f7e2dd2..7efcf0c097 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 2b7c02f115..b8fc4a653f 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 b583d3d8ea..44ef00ba73 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 5f7b8e949f..e68afcbf9d 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 092a67ea75..7d0144438a 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 674c481b81..d45a6e20eb 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 30692d1cd9..e1728910ee 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 682f8d3c8c..848b812632 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 bfde78708e..b8ba948728 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 0000000000..d0dd2b6acd
--- /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 1cd6edd7aa..8ff0377946 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 d36abf8f17..8006ca89a2 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 13b80c0925..34fd5439db 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 d4cd8c7945..13cddba723 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 2bef3bb4b3..116ed66d03 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 e33d365c9c..089899398e 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 666127af39..7b0828bfa8 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 9f959036a4..4bfbf7e25a 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 2a5285ee02..80d6c13cef 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 6bfb23a46f..89c2adaea3 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 a5d4eee5ef..470bee2cf5 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 beb45ab6e9..0f8eecee01 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 38cfc87271..dd5b483ec4 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 9ab3776c91..72a98a88ab 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 89dfcef9f1..25c88341a4 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 91070756cb..c0a1a21e86 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 0000000000..e142feddf0
--- /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 fd869c3960..d8b9190275 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 cc1b7b44d7..a21557d303 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 7318b51b5d..ebdceb72ed 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 0000000000..ebaac3794f
--- /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 b0721c2e02..08ad3e3797 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 77710ed7de..067fd8cd2d 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 8eac730bec..d1ff6808b2 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 15956d4cfd..1c57b9b024 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 aef05e9d9d..e81c0c081e 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 28c087b1c2..ad1de07380 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 5baf9b38a5..990b92c337 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 28030a4266..99930c3229 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 0000000000..fa9238e6ef
--- /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 0000000000..3ef45754ab
--- /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 8f1dc4954d..41014fb418 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 02b61bd512..49063754ad 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 74a77978b9..efa625eb5b 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 0000000000..cb3a6df2ae
--- /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 ae68f8c854..13bcf1daa2 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 7a64159d84..9956b88513 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 0000000000..dcb565814f
--- /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 b87cff51e1..222cc8566c 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 4998d00399..31f39954fb 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 3903b444b0..178c25a49b 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 3ca3441169..b67cd2771a 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 348f94a090..b74ae1a412 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 1389a42ed6..bd9d9d74b5 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 30937471bd..9a40d7bdd5 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 0000000000..49b92f104d
--- /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 0000000000..2317a2a1ac
--- /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 460bb57091..c12ba34aeb 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 0000000000..7a223ee07b
--- /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 0000000000..c901bc50f7
--- /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 0000000000..2fc4d1a5f2
--- /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 a366dbad5b..e0cbe36d1e 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 6b7ae14e94..3fb14023b0 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 0000000000..80d921eab0
--- /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 0000000000..e108a5c209
--- /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 2868a3b715..ff1b744442 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 0000000000..c732bdedc0
--- /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 31a3962458..9a6637bdb9 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 6c11ccc5a4..e5a4eb9a82 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 6a0960021f..e7b4f2d458 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 71121bb2e2..71e5fb843e 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 443b7617ff..0b718c5b65 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 8a91c59f38..4ea88fb4f2 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 2e43a98e42..14695c36c5 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 12c45c22a1..f1ba5651dd 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 f5c93a6840..921c1306c1 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 0000000000..146d5d3037
--- /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 c35ad80028..3e9ab134b7 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 863cddcbc1..8e84110807 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 39fa58e50d..5bb9903864 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 41862b5b6b..481e01d8e7 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 26ffaf0388..db609de5e9 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 0f2e30f7d5..b88cf662c2 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 010caaf8ea..f68d1cf1f8 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 abd470906f..ff9ba32b8e 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 0000000000..2f93d0ff02
--- /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 7189110833..8896f02a77 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 8dc0f329b6..20770a7211 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 0000000000..622fff66e8
--- /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 0000000000..4a635d1a79
--- /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 f3918d403d..a885640ce0 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 0000000000..de4335fc51
--- /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 0000000000..bc7c038a97
--- /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 0000000000..66b0e8f9e2
--- /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 1095334695..17f06d2833 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 9123804a48..423dce88ab 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 0000000000..46362936f4
--- /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 0000000000..0a494b0c4c
--- /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 0000000000..e19ba61612
--- /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 123bd4f560..cb69af5f54 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 0000000000..c8821348ac
--- /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 0000000000..fa15072241
--- /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 0000000000..43ff211eaa
--- /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 0000000000..f596c0a1ec
--- /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 ffb40ebc4a..2f784f2489 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 783ddbef17..3728632883 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"