Merge commit '85662a5a57531af5402a6777d0b1089e78c56815' into glitch-soc/merge-upstream

Conflicts:
- `config/initializers/content_security_policy.rb`:
  Upstream reworked the CSP, we kept our version for now.
- `spec/requests/content_security_policy_spec.rb`:
  Upstream reworked the CSP, we kept our version for now.
This commit is contained in:
Claire 2023-12-20 20:10:45 +01:00
commit b8209c3b96
94 changed files with 1386 additions and 731 deletions

View File

@ -24,4 +24,4 @@ RAILS_ENV=development ./bin/rails db:setup
RAILS_ENV=development ./bin/rails assets:precompile RAILS_ENV=development ./bin/rails assets:precompile
# Precompile assets for test # Precompile assets for test
RAILS_ENV=test NODE_ENV=tests ./bin/rails assets:precompile RAILS_ENV=test ./bin/rails assets:precompile

View File

@ -1,4 +1,7 @@
module.exports = { // @ts-check
const { defineConfig } = require('eslint-define-config');
module.exports = defineConfig({
root: true, root: true,
extends: [ extends: [
@ -193,6 +196,7 @@ module.exports = {
'error', 'error',
{ {
devDependencies: [ devDependencies: [
'.eslintrc.js',
'config/webpack/**', 'config/webpack/**',
'app/javascript/mastodon/performance.js', 'app/javascript/mastodon/performance.js',
'app/javascript/mastodon/test_setup.js', 'app/javascript/mastodon/test_setup.js',
@ -297,7 +301,6 @@ module.exports = {
'formatjs/no-id': 'off', // IDs are used for translation keys 'formatjs/no-id': 'off', // IDs are used for translation keys
'formatjs/no-invalid-icu': 'error', 'formatjs/no-invalid-icu': 'error',
'formatjs/no-literal-string-in-jsx': 'off', // Should be looked at, but mainly flagging punctuation outside of strings 'formatjs/no-literal-string-in-jsx': 'off', // Should be looked at, but mainly flagging punctuation outside of strings
'formatjs/no-multiple-plurals': 'off', // Only used by hashtag.jsx
'formatjs/no-multiple-whitespaces': 'error', 'formatjs/no-multiple-whitespaces': 'error',
'formatjs/no-offset': 'error', 'formatjs/no-offset': 'error',
'formatjs/no-useless-message': 'error', 'formatjs/no-useless-message': 'error',
@ -316,6 +319,7 @@ module.exports = {
overrides: [ overrides: [
{ {
files: [ files: [
'.eslintrc.js',
'*.config.js', '*.config.js',
'.*rc.js', '.*rc.js',
'ide-helper.js', 'ide-helper.js',
@ -366,7 +370,7 @@ module.exports = {
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'], '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
'@typescript-eslint/consistent-type-exports': 'error', '@typescript-eslint/consistent-type-exports': 'error',
'@typescript-eslint/consistent-type-imports': 'error', '@typescript-eslint/consistent-type-imports': 'error',
"@typescript-eslint/prefer-nullish-coalescing": ['error', {ignorePrimitives: {boolean: true}}], "@typescript-eslint/prefer-nullish-coalescing": ['error', { ignorePrimitives: { boolean: true } }],
'jsdoc/require-jsdoc': 'off', 'jsdoc/require-jsdoc': 'off',
@ -389,14 +393,6 @@ module.exports = {
env: { env: {
jest: true, jest: true,
}, },
}, }
{
files: [
'streaming/**/*',
],
rules: {
'import/no-commonjs': 'off',
},
},
], ],
}; });

View File

@ -21,6 +21,8 @@ on:
type: string type: string
labels: labels:
type: string type: string
file_to_build:
type: string
jobs: jobs:
build-image: build-image:
@ -86,6 +88,7 @@ jobs:
- uses: docker/build-push-action@v5 - uses: docker/build-push-action@v5
with: with:
context: . context: .
file: ${{ inputs.file_to_build }}
build-args: | build-args: |
MASTODON_VERSION_PRERELEASE=${{ inputs.version_prerelease }} MASTODON_VERSION_PRERELEASE=${{ inputs.version_prerelease }}
MASTODON_VERSION_METADATA=${{ inputs.version_metadata }} MASTODON_VERSION_METADATA=${{ inputs.version_metadata }}

View File

@ -25,6 +25,7 @@ jobs:
needs: compute-suffix needs: compute-suffix
uses: ./.github/workflows/build-container-image.yml uses: ./.github/workflows/build-container-image.yml
with: with:
file_to_build: Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
use_native_arm64_builder: false use_native_arm64_builder: false
cache: false cache: false
@ -40,3 +41,25 @@ jobs:
type=raw,value=nightly type=raw,value=nightly
type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }} type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }}
secrets: inherit secrets: inherit
build-image-streaming:
needs: compute-suffix
uses: ./.github/workflows/build-container-image.yml
with:
file_to_build: streaming/Dockerfile
platforms: linux/amd64,linux/arm64
use_native_arm64_builder: true
cache: false
push_to_images: |
tootsuite/mastodon-streaming
ghcr.io/mastodon/mastodon-streaming
version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }}
labels: |
org.opencontainers.image.description=Nightly build image used for testing purposes
flavor: |
latest=auto
tags: |
type=raw,value=edge
type=raw,value=nightly
type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }}
secrets: inherit

View File

@ -29,6 +29,7 @@ jobs:
needs: compute-suffix needs: compute-suffix
uses: ./.github/workflows/build-container-image.yml uses: ./.github/workflows/build-container-image.yml
with: with:
file_to_build: Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
use_native_arm64_builder: false use_native_arm64_builder: false
push_to_images: | push_to_images: |
@ -39,3 +40,19 @@ jobs:
tags: | tags: |
type=ref,event=pr type=ref,event=pr
secrets: inherit secrets: inherit
build-image-streaming:
needs: compute-suffix
uses: ./.github/workflows/build-container-image.yml
with:
file_to_build: streaming/Dockerfile
platforms: linux/amd64,linux/arm64
use_native_arm64_builder: true
push_to_images: |
ghcr.io/mastodon/mastodon-streaming
version_metadata: ${{ needs.compute-suffix.outputs.metadata }}
flavor: |
latest=auto
tags: |
type=ref,event=pr
secrets: inherit

View File

@ -12,6 +12,7 @@ jobs:
build-image: build-image:
uses: ./.github/workflows/build-container-image.yml uses: ./.github/workflows/build-container-image.yml
with: with:
file_to_build: Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
use_native_arm64_builder: false use_native_arm64_builder: false
push_to_images: | push_to_images: |
@ -26,3 +27,24 @@ jobs:
type=pep440,pattern={{raw}} type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}} type=pep440,pattern=v{{major}}.{{minor}}
secrets: inherit secrets: inherit
build-image-streaming:
if: startsWith(github.ref, 'refs/tags/v4.3.')
uses: ./.github/workflows/build-container-image.yml
with:
file_to_build: streaming/Dockerfile
platforms: linux/amd64,linux/arm64
use_native_arm64_builder: true
push_to_images: |
tootsuite/mastodon-streaming
ghcr.io/mastodon/mastodon-streaming
# Do not use cache when building releases, so apt update is always ran and the release always contain the latest packages
cache: false
# 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.3.') }}
tags: |
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}
secrets: inherit

View File

@ -7,6 +7,7 @@ on:
- .github/workflows/build-releases.yml - .github/workflows/build-releases.yml
- .github/workflows/test-image-build.yml - .github/workflows/test-image-build.yml
- Dockerfile - Dockerfile
- streaming/Dockerfile
permissions: permissions:
contents: read contents: read
@ -18,4 +19,17 @@ jobs:
uses: ./.github/workflows/build-container-image.yml uses: ./.github/workflows/build-container-image.yml
with: with:
file_to_build: Dockerfile
platforms: linux/amd64 # Testing only on native platform so it is performant platforms: linux/amd64 # Testing only on native platform so it is performant
cache: true
build-image-streaming:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-streaming
cancel-in-progress: true
uses: ./.github/workflows/build-container-image.yml
with:
file_to_build: streaming/Dockerfile
platforms: linux/amd64 # Testing only on native platform so it is performant
cache: true

View File

@ -309,7 +309,7 @@ Style/FetchEnvVar:
- 'config/initializers/devise.rb' - 'config/initializers/devise.rb'
- 'config/initializers/paperclip.rb' - 'config/initializers/paperclip.rb'
- 'config/initializers/vapid.rb' - 'config/initializers/vapid.rb'
- 'lib/mastodon/premailer_webpack_strategy.rb' - 'lib/premailer_webpack_strategy.rb'
- 'lib/mastodon/redis_config.rb' - 'lib/mastodon/redis_config.rb'
- 'lib/tasks/repo.rake' - 'lib/tasks/repo.rake'
- 'spec/features/profile_spec.rb' - 'spec/features/profile_spec.rb'
@ -359,8 +359,8 @@ Style/GuardClause:
- 'config/initializers/devise.rb' - 'config/initializers/devise.rb'
- 'db/migrate/20170901141119_truncate_preview_cards.rb' - 'db/migrate/20170901141119_truncate_preview_cards.rb'
- 'db/post_migrate/20220704024901_migrate_settings_to_user_roles.rb' - 'db/post_migrate/20220704024901_migrate_settings_to_user_roles.rb'
- 'lib/devise/two_factor_ldap_authenticatable.rb' - 'lib/devise/strategies/two_factor_ldap_authenticatable.rb'
- 'lib/devise/two_factor_pam_authenticatable.rb' - 'lib/devise/strategies/two_factor_pam_authenticatable.rb'
- 'lib/mastodon/cli/accounts.rb' - 'lib/mastodon/cli/accounts.rb'
- 'lib/mastodon/cli/maintenance.rb' - 'lib/mastodon/cli/maintenance.rb'
- 'lib/mastodon/cli/media.rb' - 'lib/mastodon/cli/media.rb'
@ -495,8 +495,8 @@ Style/SafeNavigation:
# SupportedStyles: only_raise, only_fail, semantic # SupportedStyles: only_raise, only_fail, semantic
Style/SignalException: Style/SignalException:
Exclude: Exclude:
- 'lib/devise/two_factor_ldap_authenticatable.rb' - 'lib/devise/strategies/two_factor_ldap_authenticatable.rb'
- 'lib/devise/two_factor_pam_authenticatable.rb' - 'lib/devise/strategies/two_factor_pam_authenticatable.rb'
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
Style/SingleArgumentDig: Style/SingleArgumentDig:

View File

@ -1,112 +1,257 @@
# syntax=docker/dockerfile:1.4 # syntax=docker/dockerfile:1.4
# This needs to be bookworm-slim because the Ruby image is built on bookworm-slim
ARG NODE_VERSION="20.9-bookworm-slim"
FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim as ruby # Please see https://docs.docker.com/engine/reference/builder for information about
FROM node:${NODE_VERSION} as build # the extended buildx capabilities used in this file.
# Make sure multiarch TARGETPLATFORM is available for interpolation
# See: https://docs.docker.com/build/building/multi-platform/
ARG TARGETPLATFORM=${TARGETPLATFORM}
ARG BUILDPLATFORM=${BUILDPLATFORM}
COPY --link --from=ruby /opt/ruby /opt/ruby # Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.2.2"]
ARG RUBY_VERSION="3.2.2"
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
ARG NODE_MAJOR_VERSION="20"
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
ARG DEBIAN_VERSION="bookworm"
# Node image to use for base image based on combined variables (ex: 20-bookworm-slim)
FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim as node
# Ruby image to use for base image based on combined variables (ex: 3.2.2-slim-bookworm)
FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby
ENV DEBIAN_FRONTEND="noninteractive" \ # Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
PATH="${PATH}:/opt/ruby/bin" # Example: v4.2.0-nightly.2023.11.09+something
# Overwrite existance of 'alpha.0' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"]
ARG MASTODON_VERSION_PRERELEASE=""
# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="something"]
ARG MASTODON_VERSION_METADATA=""
SHELL ["/bin/bash", "-o", "pipefail", "-c"] # Allow Ruby on Rails to serve static files
# See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files
ARG RAILS_SERVE_STATIC_FILES="true"
# Allow to use YJIT compiler
# See: https://github.com/ruby/ruby/blob/master/doc/yjit/yjit.md
ARG RUBY_YJIT_ENABLE="1"
# Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin]
ARG TZ="Etc/UTC"
# Linux UID (user id) for the mastodon user, change with [--build-arg UID=1234]
ARG UID="991"
# Linux GID (group id) for the mastodon user, change with [--build-arg GID=1234]
ARG GID="991"
# Apply Mastodon build options based on options above
ENV \
# Apply Mastodon version information
MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \
MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}" \
# Apply Mastodon static files and YJIT options
RAILS_SERVE_STATIC_FILES=${RAILS_SERVE_STATIC_FILES} \
RUBY_YJIT_ENABLE=${RUBY_YJIT_ENABLE} \
# Apply timezone
TZ=${TZ}
ENV \
# Configure the IP to bind Mastodon to when serving traffic
BIND="0.0.0.0" \
# Use production settings for Yarn, Node and related nodejs based tools
NODE_ENV="production" \
# Use production settings for Ruby on Rails
RAILS_ENV="production" \
# Add Ruby and Mastodon installation to the PATH
DEBIAN_FRONTEND="noninteractive" \
PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" \
# Optimize jemalloc 5.x performance
MALLOC_CONF="narenas:2,background_thread:true,thp:never,dirty_decay_ms:1000,muzzy_decay_ms:0"
# Set default shell used for running commands
SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-c"]
ARG TARGETPLATFORM
RUN echo "Target platform is $TARGETPLATFORM"
RUN \
# Remove automatic apt cache Docker cleanup scripts
rm -f /etc/apt/apt.conf.d/docker-clean; \
# Sets timezone
echo "${TZ}" > /etc/localtime; \
# Creates mastodon user/group and sets home directory
groupadd -g "${GID}" mastodon; \
useradd -l -u "${UID}" -g "${GID}" -m -d /opt/mastodon mastodon; \
# Creates /mastodon symlink to /opt/mastodon
ln -s /opt/mastodon /mastodon;
# Set /opt/mastodon as working directory
WORKDIR /opt/mastodon WORKDIR /opt/mastodon
# hadolint ignore=DL3008 # hadolint ignore=DL3008,DL3005
RUN apt-get update && \ RUN \
apt-get -yq dist-upgrade && \ # Mount Apt cache and lib directories from Docker buildx caches
apt-get install -y --no-install-recommends build-essential \ --mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
git \ --mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
libicu-dev \ # Apt update & upgrade to check for security updates to Debian image
libidn-dev \ apt-get update; \
libpq-dev \ apt-get dist-upgrade -yq; \
libjemalloc-dev \ # Install jemalloc, curl and other necessary components
zlib1g-dev \ apt-get install -y --no-install-recommends \
libgdbm-dev \ ca-certificates \
libgmp-dev \ curl \
libssl-dev \ ffmpeg \
libyaml-dev \ file \
ca-certificates \ imagemagick \
libreadline8 \ libjemalloc2 \
python3 \ patchelf \
shared-mime-info && \ procps \
bundle config set --local deployment 'true' && \ tini \
bundle config set --local without 'development test' && \ tzdata \
bundle config set silence_root_warning true && \ ; \
corepack enable # Patch Ruby to use jemalloc
patchelf --add-needed libjemalloc.so.2 /usr/local/bin/ruby; \
# Discard patchelf after use
apt-get purge -y \
patchelf \
;
COPY Gemfile* package.json yarn.lock .yarnrc.yml /opt/mastodon/ # Create temporary build layer from base image
FROM ruby as build
# Copy Node package configuration files into working directory
COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/
COPY .yarn /opt/mastodon/.yarn
COPY --from=node /usr/local/bin /usr/local/bin
COPY --from=node /usr/local/lib /usr/local/lib
ARG TARGETPLATFORM
# hadolint ignore=DL3008
RUN \
# Mount Apt cache and lib directories from Docker buildx caches
--mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
# Install build tools and bundler dependencies from APT
apt-get install -y --no-install-recommends \
g++ \
gcc \
git \
libgdbm-dev \
libgmp-dev \
libicu-dev \
libidn-dev \
libpq-dev \
libssl-dev \
make \
shared-mime-info \
zlib1g-dev \
;
RUN \
# Configure Corepack
rm /usr/local/bin/yarn*; \
corepack enable; \
corepack prepare --activate;
# Create temporary bundler specific build layer from build layer
FROM build as bundler
ARG TARGETPLATFORM
# Copy Gemfile config into working directory
COPY Gemfile* /opt/mastodon/
RUN \
# Mount Ruby Gem caches
--mount=type=cache,id=gem-cache-${TARGETPLATFORM},target=/usr/local/bundle/cache/,sharing=locked \
# Configure bundle to prevent changes to Gemfile and Gemfile.lock
bundle config set --global frozen "true"; \
# Configure bundle to not cache downloaded Gems
bundle config set --global cache_all "false"; \
# Configure bundle to only process production Gems
bundle config set --local without "development test"; \
# Configure bundle to not warn about root user
bundle config set silence_root_warning "true"; \
# Download and install required Gems
bundle install -j"$(nproc)";
# Create temporary node specific build layer from build layer
FROM build as yarn
ARG TARGETPLATFORM
# Copy Node package configuration files into working directory
COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/
COPY streaming/package.json /opt/mastodon/streaming/ COPY streaming/package.json /opt/mastodon/streaming/
COPY .yarn /opt/mastodon/.yarn COPY .yarn /opt/mastodon/.yarn
RUN bundle install -j"$(nproc)" # hadolint ignore=DL3008
RUN \
--mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \
--mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \
# Install Node packages
yarn workspaces focus --production @mastodon/mastodon;
RUN yarn workspaces focus --all --production && \ # Create temporary assets build layer from build layer
yarn cache clean FROM build as precompiler
FROM node:${NODE_VERSION} # Copy Mastodon sources into precompiler layer
COPY . /opt/mastodon/
# Use those args to specify your own version flags & suffixes # Copy bundler and node packages from build layer to container
ARG MASTODON_VERSION_PRERELEASE="" COPY --from=yarn /opt/mastodon /opt/mastodon/
ARG MASTODON_VERSION_METADATA="" COPY --from=bundler /opt/mastodon /opt/mastodon/
COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/
ARG UID="991" ARG TARGETPLATFORM
ARG GID="991"
COPY --link --from=ruby /opt/ruby /opt/ruby RUN \
# Use Ruby on Rails to create Mastodon assets
OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder bundle exec rails assets:precompile; \
# Cleanup temporary files
rm -fr /opt/mastodon/tmp;
SHELL ["/bin/bash", "-o", "pipefail", "-c"] # Prep final Mastodon Ruby layer
FROM ruby as mastodon
ENV DEBIAN_FRONTEND="noninteractive" \ ARG TARGETPLATFORM
PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin"
# Ignoring these here since we don't want to pin any versions and the Debian image removes apt-get content after use # hadolint ignore=DL3008
# hadolint ignore=DL3008,DL3009 RUN \
RUN apt-get update && \ # Mount Apt cache and lib directories from Docker buildx caches
echo "Etc/UTC" > /etc/localtime && \ --mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
groupadd -g "${GID}" mastodon && \ --mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
useradd -l -u "$UID" -g "${GID}" -m -d /opt/mastodon mastodon && \ # Mount Corepack and Yarn caches from Docker buildx caches
apt-get -y --no-install-recommends install whois \ --mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \
wget \ --mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \
procps \ # Apt update install non-dev versions of necessary components
libssl3 \ apt-get install -y --no-install-recommends \
libpq5 \ libssl3 \
imagemagick \ libpq5 \
ffmpeg \ libicu72 \
libjemalloc2 \ libidn12 \
libicu72 \ libreadline8 \
libidn12 \ libyaml-0-2 \
libyaml-0-2 \ ;
file \
ca-certificates \
tzdata \
libreadline8 \
tini && \
ln -s /opt/mastodon /mastodon && \
corepack enable
# Note: no, cleaning here since Debian does this automatically # Copy Mastodon sources into final layer
# See the file /etc/apt/apt.conf.d/docker-clean within the Docker image's filesystem COPY . /opt/mastodon/
COPY --chown=mastodon:mastodon . /opt/mastodon # Copy compiled assets to layer
COPY --chown=mastodon:mastodon --from=build /opt/mastodon /opt/mastodon COPY --from=precompiler /opt/mastodon/public/packs /opt/mastodon/public/packs
COPY --from=precompiler /opt/mastodon/public/assets /opt/mastodon/public/assets
# Copy bundler components to layer
COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/
ENV RAILS_ENV="production" \ RUN \
NODE_ENV="production" \ # Precompile bootsnap code for faster Rails startup
RAILS_SERVE_STATIC_FILES="true" \ bundle exec bootsnap precompile --gemfile app/ lib/;
BIND="0.0.0.0" \
MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \
MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}"
# Set the run user RUN \
# Pre-create and chown system volume to Mastodon user
mkdir -p /opt/mastodon/public/system; \
chown mastodon:mastodon /opt/mastodon/public/system;
# Set the running user for resulting container
USER mastodon USER mastodon
WORKDIR /opt/mastodon # Expose default Puma ports
EXPOSE 3000
# Precompile assets # Set container tini as default entry point
RUN OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder rails assets:precompile ENTRYPOINT ["/usr/bin/tini", "--"]
# Set the work dir and the container entry point
ENTRYPOINT ["/usr/bin/tini", "--"]
EXPOSE 3000 4000

View File

@ -156,7 +156,7 @@ GEM
nokogiri (~> 1, >= 1.10.8) nokogiri (~> 1, >= 1.10.8)
base64 (0.2.0) base64 (0.2.0)
bcp47_spec (0.2.1) bcp47_spec (0.2.1)
bcrypt (3.1.19) bcrypt (3.1.20)
better_errors (2.10.1) better_errors (2.10.1)
erubi (>= 1.0.0) erubi (>= 1.0.0)
rack (>= 0.9.0) rack (>= 0.9.0)
@ -522,7 +522,7 @@ GEM
pastel (0.8.0) pastel (0.8.0)
tty-color (~> 0.5) tty-color (~> 0.5)
pg (1.5.4) pg (1.5.4)
pghero (3.3.4) pghero (3.4.0)
activerecord (>= 6) activerecord (>= 6)
posix-spawn (0.3.15) posix-spawn (0.3.15)
premailer (1.21.0) premailer (1.21.0)
@ -755,7 +755,7 @@ GEM
attr_required (>= 0.0.5) attr_required (>= 0.0.5)
httpclient (>= 2.4) httpclient (>= 2.4)
sysexits (1.2.0) sysexits (1.2.0)
temple (0.10.2) temple (0.10.3)
terminal-table (3.0.2) terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (>= 1.1.1, < 3)
terrapin (0.6.0) terrapin (0.6.0)

6
Vagrantfile vendored
View File

@ -10,7 +10,11 @@ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
sudo apt-add-repository 'deb https://dl.yarnpkg.com/debian/ stable main' sudo apt-add-repository 'deb https://dl.yarnpkg.com/debian/ stable main'
# Add repo for NodeJS # Add repo for NodeJS
curl -sL https://deb.nodesource.com/setup_16.x | sudo bash - sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
NODE_MAJOR=20
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
sudo apt-get update
# Add firewall rule to redirect 80 to PORT and save # Add firewall rule to redirect 80 to PORT and save
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{ENV["PORT"]} sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{ENV["PORT"]}

View File

@ -50,7 +50,7 @@ class AccountsController < ApplicationController
end end
def only_media_scope def only_media_scope
Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id) Status.joins(:media_attachments).merge(@account.media_attachments).group(:id)
end end
def no_replies_scope def no_replies_scope

View File

@ -16,7 +16,7 @@ module Admin
@moderation_notes = @account.targeted_moderation_notes.latest @moderation_notes = @account.targeted_moderation_notes.latest
@warnings = @account.strikes.custom.latest @warnings = @account.strikes.custom.latest
render template: 'admin/accounts/show' render 'admin/accounts/show'
end end
end end

View File

@ -6,7 +6,7 @@ module Admin
def index def index
authorize :audit_log, :index? authorize :audit_log, :index?
@auditable_accounts = Account.where(id: Admin::ActionLog.reorder(nil).select('distinct account_id')).select(:id, :username) @auditable_accounts = Account.where(id: Admin::ActionLog.select('distinct account_id')).select(:id, :username)
end end
private private

View File

@ -24,7 +24,7 @@ module Admin
@relay.enable! @relay.enable!
redirect_to admin_relays_path redirect_to admin_relays_path
else else
render action: :new render :new
end end
end end

View File

@ -26,7 +26,7 @@ module Admin
@form = Admin::StatusBatchAction.new @form = Admin::StatusBatchAction.new
@statuses = @report.statuses.with_includes @statuses = @report.statuses.with_includes
render template: 'admin/reports/show' render 'admin/reports/show'
end end
end end

View File

@ -43,7 +43,7 @@ module ChallengableConcern
def render_challenge def render_challenge
@body_classes = 'lighter' @body_classes = 'lighter'
render template: 'auth/challenges/new', layout: 'auth' render 'auth/challenges/new', layout: 'auth'
end end
def challenge_passed? def challenge_passed?

View File

@ -11,7 +11,7 @@ class Disputes::AppealsController < Disputes::BaseController
redirect_to disputes_strike_path(@strike), notice: I18n.t('disputes.strikes.appealed_msg') redirect_to disputes_strike_path(@strike), notice: I18n.t('disputes.strikes.appealed_msg')
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
@appeal = e.record @appeal = e.record
render template: 'disputes/strikes/show' render 'disputes/strikes/show'
end end
private private

View File

@ -26,7 +26,7 @@ class FiltersController < ApplicationController
if @filter.save if @filter.save
redirect_to filters_path redirect_to filters_path
else else
render action: :new render :new
end end
end end
@ -34,7 +34,7 @@ class FiltersController < ApplicationController
if @filter.update(resource_params) if @filter.update(resource_params)
redirect_to filters_path redirect_to filters_path
else else
render action: :edit render :edit
end end
end end

View File

@ -15,7 +15,7 @@ class StatusesCleanupController < ApplicationController
if @policy.update(resource_params) if @policy.update(resource_params)
redirect_to statuses_cleanup_path, notice: I18n.t('generic.changes_saved_msg') redirect_to statuses_cleanup_path, notice: I18n.t('generic.changes_saved_msg')
else else
render action: :show render :show
end end
rescue ActionController::ParameterMissing rescue ActionController::ParameterMissing
# Do nothing # Do nothing

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
module Admin::AccountActionsHelper
def account_action_type_label(type)
safe_join(
[
I18n.t("simple_form.labels.admin_account_action.types.#{type}"),
content_tag(:span, I18n.t("simple_form.hints.admin_account_action.types.#{type}"), class: 'hint'),
]
)
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Admin::AccountsHelper
def admin_accounts_moderation_options
[
[t('admin.accounts.moderation.active'), 'active'],
[t('admin.accounts.moderation.silenced'), 'silenced'],
[t('admin.accounts.moderation.disabled'), 'disabled'],
[t('admin.accounts.moderation.suspended'), 'suspended'],
[safe_join([t('admin.accounts.moderation.pending'), "(#{pending_user_count_label})"], ' '), 'pending'],
]
end
private
def pending_user_count_label
number_with_delimiter User.pending.count
end
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
module Admin::IpBlocksHelper
def ip_blocks_severity_label(severity)
safe_join(
[
I18n.t("simple_form.labels.ip_block.severities.#{severity}"),
content_tag(:span, I18n.t("simple_form.hints.ip_block.severities.#{severity}"), class: 'hint'),
]
)
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
module Admin
module RolesHelper
def privilege_label(privilege)
safe_join(
[
t("admin.roles.privileges.#{privilege}"),
content_tag(:span, t("admin.roles.privileges.#{privilege}_description"), class: 'hint'),
]
)
end
def disable_permissions?(permissions)
permissions.filter { |privilege| role_flag_value(privilege).zero? }
end
private
def role_flag_value(privilege)
UserRole::FLAGS[privilege] & current_user.role.computed_permissions
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module Admin::Settings::DiscoveryHelper
def discovery_warning_hint_text
authorized_fetch_overridden? ? t('admin.settings.security.authorized_fetch_overridden_hint') : nil
end
def discovery_hint_text
t('admin.settings.security.authorized_fetch_hint')
end
def discovery_recommended_value
authorized_fetch_overridden? ? :overridden : nil
end
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
module FiltersHelper
def filter_action_label(action)
safe_join(
[
t("simple_form.labels.filters.actions.#{action}"),
content_tag(:span, t("simple_form.hints.filters.actions.#{action}"), class: 'hint'),
]
)
end
end

View File

@ -6,7 +6,7 @@ import { FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import api from 'mastodon/api'; import api from 'mastodon/api';
import Hashtag from 'mastodon/components/hashtag'; import { Hashtag } from 'mastodon/components/hashtag';
export default class Trends extends PureComponent { export default class Trends extends PureComponent {

View File

@ -1,11 +1,18 @@
/* eslint-disable @typescript-eslint/no-unsafe-call,
@typescript-eslint/no-unsafe-return,
@typescript-eslint/no-unsafe-assignment,
@typescript-eslint/no-unsafe-member-access
-- the settings store is not yet typed */
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import { useCallback, useState } from 'react'; import { useCallback, useState, useEffect } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { ReactComponent as CloseIcon } from '@material-symbols/svg-600/outlined/close.svg'; import { ReactComponent as CloseIcon } from '@material-symbols/svg-600/outlined/close.svg';
import { changeSetting } from 'mastodon/actions/settings';
import { bannerSettings } from 'mastodon/settings'; import { bannerSettings } from 'mastodon/settings';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { IconButton } from './icon_button'; import { IconButton } from './icon_button';
@ -21,13 +28,25 @@ export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
id, id,
children, children,
}) => { }) => {
const [visible, setVisible] = useState(!bannerSettings.get(id)); const dismissed = useAppSelector((state) =>
state.settings.getIn(['dismissed_banners', id], false),
);
const dispatch = useAppDispatch();
const [visible, setVisible] = useState(!bannerSettings.get(id) && !dismissed);
const intl = useIntl(); const intl = useIntl();
const handleDismiss = useCallback(() => { const handleDismiss = useCallback(() => {
setVisible(false); setVisible(false);
bannerSettings.set(id, true); bannerSettings.set(id, true);
}, [id]); dispatch(changeSetting(['dismissed_banners', id], true));
}, [id, dispatch]);
useEffect(() => {
if (!visible && !dismissed) {
dispatch(changeSetting(['dismissed_banners', id], true));
}
}, [id, dispatch, visible, dismissed]);
if (!visible) { if (!visible) {
return null; return null;

View File

@ -1,120 +0,0 @@
// @ts-check
import PropTypes from 'prop-types';
import { Component } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Sparklines, SparklinesCurve } from 'react-sparklines';
import { ShortNumber } from 'mastodon/components/short_number';
import { Skeleton } from 'mastodon/components/skeleton';
class SilentErrorBoundary extends Component {
static propTypes = {
children: PropTypes.node,
};
state = {
error: false,
};
componentDidCatch() {
this.setState({ error: true });
}
render() {
if (this.state.error) {
return null;
}
return this.props.children;
}
}
/**
* Used to render counter of how much people are talking about hashtag
* @type {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element}
*/
export const accountsCountRenderer = (displayNumber, pluralReady) => (
<FormattedMessage
id='trends.counter_by_accounts'
defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
days: 2,
}}
/>
);
// @ts-expect-error
export const ImmutableHashtag = ({ hashtag }) => (
<Hashtag
name={hashtag.get('name')}
to={`/tags/${hashtag.get('name')}`}
people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
// @ts-expect-error
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
/>
);
ImmutableHashtag.propTypes = {
hashtag: ImmutablePropTypes.map.isRequired,
};
// @ts-expect-error
const Hashtag = ({ name, to, people, uses, history, className, description, withGraph }) => (
<div className={classNames('trends__item', className)}>
<div className='trends__item__name'>
<Link to={to}>
{name ? <>#<span>{name}</span></> : <Skeleton width={50} />}
</Link>
{description ? (
<span>{description}</span>
) : (
typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} />
)}
</div>
{typeof uses !== 'undefined' && (
<div className='trends__item__current'>
<ShortNumber value={uses} />
</div>
)}
{withGraph && (
<div className='trends__item__sparkline'>
<SilentErrorBoundary>
<Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}>
<SparklinesCurve style={{ fill: 'none' }} />
</Sparklines>
</SilentErrorBoundary>
</div>
)}
</div>
);
Hashtag.propTypes = {
name: PropTypes.string,
to: PropTypes.string,
people: PropTypes.number,
description: PropTypes.node,
uses: PropTypes.number,
history: PropTypes.arrayOf(PropTypes.number),
className: PropTypes.string,
withGraph: PropTypes.bool,
};
Hashtag.defaultProps = {
withGraph: true,
};
export default Hashtag;

View File

@ -0,0 +1,145 @@
import type { JSX } from 'react';
import { Component } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import type Immutable from 'immutable';
import { Sparklines, SparklinesCurve } from 'react-sparklines';
import { ShortNumber } from 'mastodon/components/short_number';
import { Skeleton } from 'mastodon/components/skeleton';
interface SilentErrorBoundaryProps {
children: React.ReactNode;
}
class SilentErrorBoundary extends Component<SilentErrorBoundaryProps> {
state = {
error: false,
};
componentDidCatch() {
this.setState({ error: true });
}
render() {
if (this.state.error) {
return null;
}
return this.props.children;
}
}
/**
* Used to render counter of how much people are talking about hashtag
* @param displayNumber Counter number to display
* @param pluralReady Whether the count is plural
* @returns Formatted counter of how much people are talking about hashtag
*/
export const accountsCountRenderer = (
displayNumber: JSX.Element,
pluralReady: number,
) => (
<FormattedMessage
id='trends.counter_by_accounts'
defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
days: 2,
}}
/>
);
interface ImmutableHashtagProps {
hashtag: Immutable.Map<string, unknown>;
}
export const ImmutableHashtag = ({ hashtag }: ImmutableHashtagProps) => (
<Hashtag
name={hashtag.get('name') as string}
to={`/tags/${hashtag.get('name') as string}`}
people={
(hashtag.getIn(['history', 0, 'accounts']) as number) * 1 +
(hashtag.getIn(['history', 1, 'accounts']) as number) * 1
}
history={(
hashtag.get('history') as Immutable.Collection.Indexed<
Immutable.Map<string, number>
>
)
.reverse()
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
.map((day) => day.get('uses')!)
.toArray()}
/>
);
export interface HashtagProps {
className?: string;
description?: React.ReactNode;
history?: number[];
name: string;
people: number;
to: string;
uses?: number;
withGraph?: boolean;
}
export const Hashtag: React.FC<HashtagProps> = ({
name,
to,
people,
uses,
history,
className,
description,
withGraph = true,
}) => (
<div className={classNames('trends__item', className)}>
<div className='trends__item__name'>
<Link to={to}>
{name ? (
<>
#<span>{name}</span>
</>
) : (
<Skeleton width={50} />
)}
</Link>
{description ? (
<span>{description}</span>
) : typeof people !== 'undefined' ? (
<ShortNumber value={people} renderer={accountsCountRenderer} />
) : (
<Skeleton width={100} />
)}
</div>
{typeof uses !== 'undefined' && (
<div className='trends__item__current'>
<ShortNumber value={uses} />
</div>
)}
{withGraph && (
<div className='trends__item__sparkline'>
<SilentErrorBoundary>
<Sparklines
width={50}
height={28}
data={history ? history : Array.from(Array(7)).map(() => 0)}
>
<SparklinesCurve style={{ fill: 'none' }} />
</Sparklines>
</SilentErrorBoundary>
</div>
)}
</div>
);

View File

@ -5,7 +5,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Hashtag from 'mastodon/components/hashtag'; import { Hashtag } from 'mastodon/components/hashtag';
const messages = defineMessages({ const messages = defineMessages({
lastStatusAt: { id: 'account.featured_tags.last_status_at', defaultMessage: 'Last post on {date}' }, lastStatusAt: { id: 'account.featured_tags.last_status_at', defaultMessage: 'Last post on {date}' },

View File

@ -13,7 +13,7 @@ import { debounce } from 'lodash';
import { expandFollowedHashtags, fetchFollowedHashtags } from 'mastodon/actions/tags'; import { expandFollowedHashtags, fetchFollowedHashtags } from 'mastodon/actions/tags';
import ColumnHeader from 'mastodon/components/column_header'; import ColumnHeader from 'mastodon/components/column_header';
import Hashtag from 'mastodon/components/hashtag'; import { Hashtag } from 'mastodon/components/hashtag';
import ScrollableList from 'mastodon/components/scrollable_list'; import ScrollableList from 'mastodon/components/scrollable_list';
import Column from 'mastodon/features/ui/components/column'; import Column from 'mastodon/features/ui/components/column';

View File

@ -100,6 +100,15 @@ const initialState = ImmutableMap({
body: '', body: '',
}), }),
}), }),
dismissed_banners: ImmutableMap({
'public_timeline': false,
'community_timeline': false,
'home.explore_prompt': false,
'explore/links': false,
'explore/statuses': false,
'explore/tags': false,
}),
}); });
const defaultColumns = fromJS([ const defaultColumns = fromJS([

View File

@ -1,10 +0,0 @@
import ready from '../ready';
export let assetHost = '';
ready(() => {
const cdnHost = document.querySelector('meta[name=cdn-host]');
if (cdnHost) {
assetHost = cdnHost.content || '';
}
});

View File

@ -0,0 +1,13 @@
import ready from '../ready';
export let assetHost = '';
// eslint-disable-next-line @typescript-eslint/no-floating-promises
ready(() => {
const cdnHost = document.querySelector<HTMLMetaElement>(
'meta[name=cdn-host]',
);
if (cdnHost) {
assetHost = cdnHost.content || '';
}
});

View File

@ -1,6 +0,0 @@
// NB: This function can still return unsafe HTML
export const unescapeHTML = (html) => {
const wrapper = document.createElement('div');
wrapper.innerHTML = html.replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n').replace(/<[^>]*>/g, '');
return wrapper.textContent;
};

View File

@ -0,0 +1,9 @@
// NB: This function can still return unsafe HTML
export const unescapeHTML = (html: string) => {
const wrapper = document.createElement('div');
wrapper.innerHTML = html
.replace(/<br\s*\/?>/g, '\n')
.replace(/<\/p><p>/g, '\n\n')
.replace(/<[^>]*>/g, '');
return wrapper.textContent;
};

View File

@ -1,13 +1,23 @@
// Copied from emoji-mart for consistency with emoji picker and since // Copied from emoji-mart for consistency with emoji picker and since
// they don't export the icons in the package // they don't export the icons in the package
export const loupeIcon = ( export const loupeIcon = (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' width='13' height='13'> <svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 20 20'
width='13'
height='13'
>
<path d='M12.9 14.32a8 8 0 1 1 1.41-1.41l5.35 5.33-1.42 1.42-5.33-5.34zM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12z' /> <path d='M12.9 14.32a8 8 0 1 1 1.41-1.41l5.35 5.33-1.42 1.42-5.33-5.34zM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12z' />
</svg> </svg>
); );
export const deleteIcon = ( export const deleteIcon = (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' width='13' height='13'> <svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 20 20'
width='13'
height='13'
>
<path d='M10 8.586L2.929 1.515 1.515 2.929 8.586 10l-7.071 7.071 1.414 1.414L10 11.414l7.071 7.071 1.414-1.414L11.414 10l7.071-7.071-1.414-1.414L10 8.586z' /> <path d='M10 8.586L2.929 1.515 1.515 2.929 8.586 10l-7.071 7.071 1.414 1.414L10 11.414l7.071 7.071 1.414-1.414L11.414 10l7.071-7.071-1.414-1.414L10 8.586z' />
</svg> </svg>
); );

View File

@ -1,30 +0,0 @@
// Handles browser quirks, based on
// https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API
const checkNotificationPromise = () => {
try {
// eslint-disable-next-line promise/valid-params, promise/catch-or-return
Notification.requestPermission().then();
} catch(e) {
return false;
}
return true;
};
const handlePermission = (permission, callback) => {
// Whatever the user answers, we make sure Chrome stores the information
if(!('permission' in Notification)) {
Notification.permission = permission;
}
callback(Notification.permission);
};
export const requestNotificationPermission = (callback) => {
if (checkNotificationPromise()) {
Notification.requestPermission().then((permission) => handlePermission(permission, callback)).catch(console.warn);
} else {
Notification.requestPermission((permission) => handlePermission(permission, callback));
}
};

View File

@ -0,0 +1,13 @@
/**
* Tries Notification.requestPermission, console warning instead of rejecting on error.
* @param callback Runs with the permission result on completion.
*/
export const requestNotificationPermission = async (
callback: NotificationPermissionCallback,
) => {
try {
callback(await Notification.requestPermission());
} catch (error) {
console.warn(error);
}
};

View File

@ -1,8 +1,8 @@
import PropTypes from "prop-types"; import PropTypes from 'prop-types';
import { __RouterContext } from "react-router"; import { __RouterContext } from 'react-router';
import hoistStatics from "hoist-non-react-statics"; import hoistStatics from 'hoist-non-react-statics';
export const WithRouterPropTypes = { export const WithRouterPropTypes = {
match: PropTypes.object.isRequired, match: PropTypes.object.isRequired,
@ -16,31 +16,37 @@ export const WithOptionalRouterPropTypes = {
history: PropTypes.object, history: PropTypes.object,
}; };
export interface OptionalRouterProps {
ref: unknown;
wrappedComponentRef: unknown;
}
// This is copied from https://github.com/remix-run/react-router/blob/v5.3.4/packages/react-router/modules/withRouter.js // This is copied from https://github.com/remix-run/react-router/blob/v5.3.4/packages/react-router/modules/withRouter.js
// but does not fail if called outside of a React Router context // but does not fail if called outside of a React Router context
export function withOptionalRouter(Component) { export function withOptionalRouter<
const displayName = `withRouter(${Component.displayName || Component.name})`; ComponentType extends React.ComponentType<OptionalRouterProps>,
const C = props => { >(Component: ComponentType) {
const displayName = `withRouter(${Component.displayName ?? Component.name})`;
const C = (props: React.ComponentProps<ComponentType>) => {
const { wrappedComponentRef, ...remainingProps } = props; const { wrappedComponentRef, ...remainingProps } = props;
return ( return (
<__RouterContext.Consumer> <__RouterContext.Consumer>
{context => { {(context) => {
if(context) // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (context) {
return ( return (
// @ts-expect-error - Dynamic covariant generic components are tough to type.
<Component <Component
{...remainingProps} {...remainingProps}
{...context} {...context}
ref={wrappedComponentRef} ref={wrappedComponentRef}
/> />
); );
else } else {
return ( // @ts-expect-error - Dynamic covariant generic components are tough to type.
<Component return <Component {...remainingProps} ref={wrappedComponentRef} />;
{...remainingProps} }
ref={wrappedComponentRef}
/>
);
}} }}
</__RouterContext.Consumer> </__RouterContext.Consumer>
); );
@ -53,8 +59,8 @@ export function withOptionalRouter(Component) {
wrappedComponentRef: PropTypes.oneOfType([ wrappedComponentRef: PropTypes.oneOfType([
PropTypes.string, PropTypes.string,
PropTypes.func, PropTypes.func,
PropTypes.object PropTypes.object,
]) ]),
}; };
return hoistStatics(C, Component); return hoistStatics(C, Component);

View File

@ -1,11 +1,7 @@
import { isMobile } from '../is_mobile'; import { isMobile } from '../is_mobile';
/** @type {number | null} */ let cachedScrollbarWidth: number | null = null;
let cachedScrollbarWidth = null;
/**
* @returns {number}
*/
const getActualScrollbarWidth = () => { const getActualScrollbarWidth = () => {
const outer = document.createElement('div'); const outer = document.createElement('div');
outer.style.visibility = 'hidden'; outer.style.visibility = 'hidden';
@ -16,20 +12,19 @@ const getActualScrollbarWidth = () => {
outer.appendChild(inner); outer.appendChild(inner);
const scrollbarWidth = outer.offsetWidth - inner.offsetWidth; const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
outer.parentNode.removeChild(outer); outer.remove();
return scrollbarWidth; return scrollbarWidth;
}; };
/**
* @returns {number}
*/
export const getScrollbarWidth = () => { export const getScrollbarWidth = () => {
if (cachedScrollbarWidth !== null) { if (cachedScrollbarWidth !== null) {
return cachedScrollbarWidth; return cachedScrollbarWidth;
} }
const scrollbarWidth = isMobile(window.innerWidth) ? 0 : getActualScrollbarWidth(); const scrollbarWidth = isMobile(window.innerWidth)
? 0
: getActualScrollbarWidth();
cachedScrollbarWidth = scrollbarWidth; cachedScrollbarWidth = scrollbarWidth;
return scrollbarWidth; return scrollbarWidth;

View File

@ -70,7 +70,7 @@ class AccountStatusesFilter
end end
def only_media_scope def only_media_scope
Status.joins(:media_attachments).merge(account.media_attachments.reorder(nil)).group(Status.arel_table[:id]) Status.joins(:media_attachments).merge(account.media_attachments).group(Status.arel_table[:id])
end end
def no_replies_scope def no_replies_scope

View File

@ -9,8 +9,8 @@ class ContentSecurityPolicy
url_from_configured_asset_host || url_from_base_host url_from_configured_asset_host || url_from_base_host
end end
def media_host def media_hosts
cdn_host_value || assets_host [assets_host, cdn_host_value].compact
end end
private private

View File

@ -27,11 +27,11 @@ class Vacuum::MediaAttachmentsVacuum
end end
def media_attachments_past_retention_period def media_attachments_past_retention_period
MediaAttachment.unscoped.remote.cached.where(MediaAttachment.arel_table[:created_at].lt(@retention_period.ago)).where(MediaAttachment.arel_table[:updated_at].lt(@retention_period.ago)) MediaAttachment.remote.cached.where(MediaAttachment.arel_table[:created_at].lt(@retention_period.ago)).where(MediaAttachment.arel_table[:updated_at].lt(@retention_period.ago))
end end
def orphaned_media_attachments def orphaned_media_attachments
MediaAttachment.unscoped.unattached.where(MediaAttachment.arel_table[:created_at].lt(TTL.ago)) MediaAttachment.unattached.where(MediaAttachment.arel_table[:created_at].lt(TTL.ago))
end end
def retention_period? def retention_period?

View File

@ -24,12 +24,12 @@ class Admin::ActionLog < ApplicationRecord
belongs_to :account belongs_to :account
belongs_to :target, polymorphic: true, optional: true belongs_to :target, polymorphic: true, optional: true
default_scope -> { order('id desc') }
before_validation :set_human_identifier before_validation :set_human_identifier
before_validation :set_route_param before_validation :set_route_param
before_validation :set_permalink before_validation :set_permalink
scope :latest, -> { order(id: :desc) }
def action def action
super.to_sym super.to_sym
end end

View File

@ -72,7 +72,7 @@ class Admin::ActionLogFilter
end end
def results def results
scope = Admin::ActionLog.includes(:target) scope = latest_action_logs.includes(:target)
params.each do |key, value| params.each do |key, value|
next if key.to_s == 'page' next if key.to_s == 'page'
@ -88,14 +88,18 @@ class Admin::ActionLogFilter
def scope_for(key, value) def scope_for(key, value)
case key case key
when 'action_type' when 'action_type'
Admin::ActionLog.where(ACTION_TYPE_MAP[value.to_sym]) latest_action_logs.where(ACTION_TYPE_MAP[value.to_sym])
when 'account_id' when 'account_id'
Admin::ActionLog.where(account_id: value) latest_action_logs.where(account_id: value)
when 'target_account_id' when 'target_account_id'
account = Account.find_or_initialize_by(id: value) account = Account.find_or_initialize_by(id: value)
Admin::ActionLog.where(target: [account, account.user].compact) latest_action_logs.where(target: [account, account.user].compact)
else else
raise Mastodon::InvalidParameterError, "Unknown filter: #{key}" raise Mastodon::InvalidParameterError, "Unknown filter: #{key}"
end end
end end
def latest_action_logs
Admin::ActionLog.latest
end
end end

View File

@ -32,7 +32,7 @@ class Admin::StatusFilter
def scope_for(key, _value) def scope_for(key, _value)
case key.to_s case key.to_s
when 'media' when 'media'
Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id).reorder('statuses.id desc') Status.joins(:media_attachments).merge(@account.media_attachments).group(:id).reorder('statuses.id desc')
else else
raise Mastodon::InvalidParameterError, "Unknown filter: #{key}" raise Mastodon::InvalidParameterError, "Unknown filter: #{key}"
end end

View File

@ -205,12 +205,11 @@ class MediaAttachment < ApplicationRecord
validates :thumbnail, absence: true, if: -> { local? && !audio_or_video? } validates :thumbnail, absence: true, if: -> { local? && !audio_or_video? }
scope :attached, -> { where.not(status_id: nil).or(where.not(scheduled_status_id: nil)) } scope :attached, -> { where.not(status_id: nil).or(where.not(scheduled_status_id: nil)) }
scope :unattached, -> { where(status_id: nil, scheduled_status_id: nil) }
scope :local, -> { where(remote_url: '') }
scope :remote, -> { where.not(remote_url: '') }
scope :cached, -> { remote.where.not(file_file_name: nil) } scope :cached, -> { remote.where.not(file_file_name: nil) }
scope :local, -> { where(remote_url: '') }
default_scope { order(id: :asc) } scope :ordered, -> { order(id: :asc) }
scope :remote, -> { where.not(remote_url: '') }
scope :unattached, -> { where(status_id: nil, scheduled_status_id: nil) }
attr_accessor :skip_download attr_accessor :skip_download

View File

@ -131,25 +131,25 @@ class Report < ApplicationRecord
Admin::ActionLog.where( Admin::ActionLog.where(
target_type: 'Report', target_type: 'Report',
target_id: id target_id: id
).unscope(:order).arel, ).arel,
Admin::ActionLog.where( Admin::ActionLog.where(
target_type: 'Account', target_type: 'Account',
target_id: target_account_id target_id: target_account_id
).unscope(:order).arel, ).arel,
Admin::ActionLog.where( Admin::ActionLog.where(
target_type: 'Status', target_type: 'Status',
target_id: status_ids target_id: status_ids
).unscope(:order).arel, ).arel,
Admin::ActionLog.where( Admin::ActionLog.where(
target_type: 'AccountWarning', target_type: 'AccountWarning',
target_id: AccountWarning.where(report_id: id).select(:id) target_id: AccountWarning.where(report_id: id).select(:id)
).unscope(:order).arel, ).arel,
].reduce { |union, query| Arel::Nodes::UnionAll.new(union, query) } ].reduce { |union, query| Arel::Nodes::UnionAll.new(union, query) }
Admin::ActionLog.from(Arel::Nodes::As.new(subquery, Admin::ActionLog.arel_table)) Admin::ActionLog.latest.from(Arel::Nodes::As.new(subquery, Admin::ActionLog.arel_table))
end end
private private

View File

@ -49,7 +49,7 @@ class UserRole < ApplicationRecord
invite_users invite_users
).freeze, ).freeze,
moderation: %w( moderation: %i(
view_dashboard view_dashboard
view_audit_log view_audit_log
manage_users manage_users
@ -63,7 +63,7 @@ class UserRole < ApplicationRecord
manage_invites manage_invites
).freeze, ).freeze,
administration: %w( administration: %i(
manage_settings manage_settings
manage_rules manage_rules
manage_roles manage_roles
@ -72,7 +72,7 @@ class UserRole < ApplicationRecord
manage_announcements manage_announcements
).freeze, ).freeze,
devops: %w( devops: %i(
view_devops view_devops
).freeze, ).freeze,

View File

@ -2,7 +2,10 @@
class REST::ApplicationSerializer < ActiveModel::Serializer class REST::ApplicationSerializer < ActiveModel::Serializer
attributes :id, :name, :website, :scopes, :redirect_uri, attributes :id, :name, :website, :scopes, :redirect_uri,
:client_id, :client_secret, :vapid_key :client_id, :client_secret
# NOTE: Deprecated in 4.3.0, needs to be removed in 5.0.0
attribute :vapid_key
def id def id
object.id.to_s object.id.to_s

View File

@ -48,6 +48,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
status: object.status_page_url, status: object.status_page_url,
}, },
vapid: {
public_key: Rails.configuration.x.vapid_public_key,
},
accounts: { accounts: {
max_featured_tags: FeaturedTag::LIMIT, max_featured_tags: FeaturedTag::LIMIT,
}, },

View File

@ -72,7 +72,7 @@ class BackupService < BaseService
end end
def dump_media_attachments!(zipfile) def dump_media_attachments!(zipfile)
MediaAttachment.attached.where(account: account).reorder(nil).find_in_batches do |media_attachments| MediaAttachment.attached.where(account: account).find_in_batches do |media_attachments|
media_attachments.each do |m| media_attachments.each do |m|
path = m.file&.path path = m.file&.path
next unless path next unless path

View File

@ -43,7 +43,7 @@ class ClearDomainMediaService < BaseService
end end
def media_from_blocked_domain def media_from_blocked_domain
MediaAttachment.joins(:account).merge(blocked_domain_accounts).reorder(nil) MediaAttachment.joins(:account).merge(blocked_domain_accounts)
end end
def emojis_from_blocked_domains def emojis_from_blocked_domains

View File

@ -165,7 +165,7 @@ class DeleteAccountService < BaseService
end end
def purge_media_attachments! def purge_media_attachments!
@account.media_attachments.reorder(nil).find_each do |media_attachment| @account.media_attachments.find_each do |media_attachment|
next if keep_account_record? && reported_status_ids.include?(media_attachment.status_id) next if keep_account_record? && reported_status_ids.include?(media_attachment.status_id)
media_attachment.destroy media_attachment.destroy

View File

@ -100,7 +100,9 @@ class ResolveAccountService < BaseService
end end
def split_acct(acct) def split_acct(acct)
acct.delete_prefix('acct:').split('@') acct.delete_prefix('acct:').split('@').tap do |parts|
raise Webfinger::Error, 'Webfinger response is missing user or host value' unless parts.size == 2
end
end end
def fetch_account! def fetch_account!

View File

@ -65,7 +65,7 @@ class SuspendAccountService < BaseService
def privatize_media_attachments! def privatize_media_attachments!
attachment_names = MediaAttachment.attachment_definitions.keys attachment_names = MediaAttachment.attachment_definitions.keys
@account.media_attachments.reorder(nil).find_each do |media_attachment| @account.media_attachments.find_each do |media_attachment|
attachment_names.each do |attachment_name| attachment_names.each do |attachment_name|
attachment = media_attachment.public_send(attachment_name) attachment = media_attachment.public_send(attachment_name)
styles = MediaAttachment::DEFAULT_STYLES | attachment.styles.keys styles = MediaAttachment::DEFAULT_STYLES | attachment.styles.keys

View File

@ -61,7 +61,7 @@ class UnsuspendAccountService < BaseService
def publish_media_attachments! def publish_media_attachments!
attachment_names = MediaAttachment.attachment_definitions.keys attachment_names = MediaAttachment.attachment_definitions.keys
@account.media_attachments.reorder(nil).find_each do |media_attachment| @account.media_attachments.find_each do |media_attachment|
attachment_names.each do |attachment_name| attachment_names.each do |attachment_name|
attachment = media_attachment.public_send(attachment_name) attachment = media_attachment.public_send(attachment_name)
styles = MediaAttachment::DEFAULT_STYLES | attachment.styles.keys styles = MediaAttachment::DEFAULT_STYLES | attachment.styles.keys

View File

@ -5,7 +5,7 @@
= f.input :report_id, as: :hidden = f.input :report_id, as: :hidden
.fields-group .fields-group
= f.input :type, as: :radio_buttons, collection: Admin::AccountAction.types_for_account(@account), include_blank: false, wrapper: :with_block_label, label_method: ->(type) { safe_join([I18n.t("simple_form.labels.admin_account_action.types.#{type}"), content_tag(:span, I18n.t("simple_form.hints.admin_account_action.types.#{type}"), class: 'hint')]) }, hint: t('simple_form.hints.admin_account_action.type_html', acct: @account.pretty_acct) = f.input :type, as: :radio_buttons, collection: Admin::AccountAction.types_for_account(@account), include_blank: false, wrapper: :with_block_label, label_method: ->(type) { account_action_type_label(type) }, hint: t('simple_form.hints.admin_account_action.type_html', acct: @account.pretty_acct)
- if @account.local? - if @account.local?
%hr.spacer/ %hr.spacer/

View File

@ -10,7 +10,7 @@
.filter-subset.filter-subset--with-select .filter-subset.filter-subset--with-select
%strong= t('admin.accounts.moderation.title') %strong= t('admin.accounts.moderation.title')
.input.select.optional .input.select.optional
= select_tag :status, options_for_select([[t('admin.accounts.moderation.active'), 'active'], [t('admin.accounts.moderation.silenced'), 'silenced'], [t('admin.accounts.moderation.disabled'), 'disabled'], [t('admin.accounts.moderation.suspended'), 'suspended'], [safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), 'pending']], params[:status]), prompt: I18n.t('generic.all') = select_tag :status, options_for_select(admin_accounts_moderation_options, params[:status]), prompt: I18n.t('generic.all')
.filter-subset.filter-subset--with-select .filter-subset.filter-subset--with-select
%strong= t('admin.accounts.role') %strong= t('admin.accounts.role')
.input.select.optional .input.select.optional

View File

@ -11,7 +11,7 @@
= f.input :expires_in, wrapper: :with_block_label, collection: [1.day, 2.weeks, 1.month, 6.months, 1.year, 3.years].map(&:to_i), label_method: ->(i) { I18n.t("admin.ip_blocks.expires_in.#{i}") }, prompt: I18n.t('invites.expires_in_prompt') = f.input :expires_in, wrapper: :with_block_label, collection: [1.day, 2.weeks, 1.month, 6.months, 1.year, 3.years].map(&:to_i), label_method: ->(i) { I18n.t("admin.ip_blocks.expires_in.#{i}") }, prompt: I18n.t('invites.expires_in_prompt')
.fields-group .fields-group
= f.input :severity, as: :radio_buttons, collection: IpBlock.severities.keys, include_blank: false, wrapper: :with_block_label, label_method: ->(severity) { safe_join([I18n.t("simple_form.labels.ip_block.severities.#{severity}"), content_tag(:span, I18n.t("simple_form.hints.ip_block.severities.#{severity}"), class: 'hint')]) } = f.input :severity, as: :radio_buttons, collection: IpBlock.severities.keys, include_blank: false, wrapper: :with_block_label, label_method: ->(severity) { ip_blocks_severity_label(severity) }
.fields-group .fields-group
= f.input :comment, as: :string, wrapper: :with_block_label = f.input :comment, as: :string, wrapper: :with_block_label

View File

@ -31,6 +31,6 @@
- (form.object.everyone? ? UserRole::Flags::CATEGORIES.slice(:invites) : UserRole::Flags::CATEGORIES).each do |category, permissions| - (form.object.everyone? ? UserRole::Flags::CATEGORIES.slice(:invites) : UserRole::Flags::CATEGORIES).each do |category, permissions|
%h4= t(category, scope: 'admin.roles.categories') %h4= t(category, scope: 'admin.roles.categories')
= form.input :permissions_as_keys, collection: permissions, wrapper: :with_block_label, include_blank: false, label_method: ->(privilege) { safe_join([t("admin.roles.privileges.#{privilege}"), content_tag(:span, t("admin.roles.privileges.#{privilege}_description"), class: 'hint')]) }, required: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', label: false, hint: false, disabled: permissions.filter { |privilege| UserRole::FLAGS[privilege] & current_user.role.computed_permissions == 0 } = form.input :permissions_as_keys, collection: permissions, wrapper: :with_block_label, include_blank: false, label_method: ->(privilege) { privilege_label(privilege) }, required: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', label: false, hint: false, disabled: disable_permissions?(permissions)
%hr.spacer/ %hr.spacer/

View File

@ -45,7 +45,7 @@
%h4= t('admin.settings.security.federation_authentication') %h4= t('admin.settings.security.federation_authentication')
.fields-group .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 = f.input :authorized_fetch, as: :boolean, wrapper: :with_label, label: t('admin.settings.security.authorized_fetch'), warning_hint: discovery_warning_hint_text, hint: discovery_hint_text, disabled: authorized_fetch_overridden?, recommended: discovery_recommended_value
%h4= t('admin.settings.discovery.follow_recommendations') %h4= t('admin.settings.discovery.follow_recommendations')

View File

@ -10,7 +10,7 @@
%hr.spacer/ %hr.spacer/
.fields-group .fields-group
= f.input :filter_action, as: :radio_buttons, collection: %i(warn hide), include_blank: false, wrapper: :with_block_label, label_method: ->(action) { safe_join([t("simple_form.labels.filters.actions.#{action}"), content_tag(:span, t("simple_form.hints.filters.actions.#{action}"), class: 'hint')]) }, hint: t('simple_form.hints.filters.action'), required: true = f.input :filter_action, as: :radio_buttons, collection: %i(warn hide), include_blank: false, wrapper: :with_block_label, label_method: ->(action) { filter_action_label(action) }, hint: t('simple_form.hints.filters.action'), required: true
%hr.spacer/ %hr.spacer/

View File

@ -39,8 +39,8 @@ require_relative '../lib/mastodon/snowflake'
require_relative '../lib/mastodon/version' require_relative '../lib/mastodon/version'
require_relative '../lib/mastodon/rack_middleware' require_relative '../lib/mastodon/rack_middleware'
require_relative '../lib/public_file_server_middleware' require_relative '../lib/public_file_server_middleware'
require_relative '../lib/devise/two_factor_ldap_authenticatable' require_relative '../lib/devise/strategies/two_factor_ldap_authenticatable'
require_relative '../lib/devise/two_factor_pam_authenticatable' require_relative '../lib/devise/strategies/two_factor_pam_authenticatable'
require_relative '../lib/chewy/settings_extensions' require_relative '../lib/chewy/settings_extensions'
require_relative '../lib/chewy/index_extensions' require_relative '../lib/chewy/index_extensions'
require_relative '../lib/chewy/strategy/mastodon' require_relative '../lib/chewy/strategy/mastodon'

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative '../../lib/mastodon/premailer_webpack_strategy' require_relative '../../lib/premailer_webpack_strategy'
Premailer::Rails.config.merge!(remove_ids: true, Premailer::Rails.config.merge!(remove_ids: true,
adapter: :nokogiri, adapter: :nokogiri,

View File

@ -0,0 +1,74 @@
# frozen_string_literal: true
require 'tty-prompt'
module Mastodon::CLI
module Federation
extend ActiveSupport::Concern
included do
desc 'self-destruct', 'Erase the server from the federation'
long_desc <<~LONG_DESC
Erase the server from the federation by broadcasting account delete
activities to all known other servers. This allows a "clean exit" from
running a Mastodon server, as it leaves next to no cache behind on
other servers.
This command is always interactive and requires confirmation twice.
No local data is actually deleted, because emptying the
database or removing files is much faster through other, external
means, such as e.g. deleting the entire VPS. However, because other
servers will delete data about local users, but no local data will be
updated (such as e.g. followers), there will be a state mismatch
that will lead to glitches and issues if you then continue to run and use
the server.
So either you know exactly what you are doing, or you are starting
from a blank slate afterwards by manually clearing out all the local
data!
LONG_DESC
def self_destruct
if SelfDestructHelper.self_destruct?
prompt.ok('Self-destruct mode is already enabled for this Mastodon server')
pending_accounts = Account.local.without_suspended.count + Account.local.suspended.joins(:deletion_request).count
sidekiq_stats = Sidekiq::Stats.new
if pending_accounts.positive?
prompt.warn("#{pending_accounts} accounts are still pending deletion.")
elsif sidekiq_stats.enqueued.positive?
prompt.warn('Deletion notices are still being processed')
elsif sidekiq_stats.retry_size.positive?
prompt.warn('At least one delivery attempt for each deletion notice has been made, but some have failed and are scheduled for retry')
else
prompt.ok('Every deletion notice has been sent! You can safely delete all data and decomission your servers!')
end
exit(0)
end
exit(1) unless prompt.ask('Type in the domain of the server to confirm:', required: true) == Rails.configuration.x.local_domain
prompt.warn('This operation WILL NOT be reversible.')
prompt.warn('While the data won\'t be erased locally, the server will be in a BROKEN STATE afterwards.')
prompt.warn('The deletion process itself may take a long time, and will be handled by Sidekiq, so do not shut it down until it has finished (you will be able to re-run this command to see the state of the self-destruct process).')
exit(1) if prompt.no?('Are you sure you want to proceed?')
self_destruct_value = Rails.application.message_verifier('self-destruct').generate(Rails.configuration.x.local_domain)
prompt.ok('To switch Mastodon to self-destruct mode, add the following variable to your evironment (e.g. by adding a line to your `.env.production`) and restart all Mastodon processes:')
prompt.ok(" SELF_DESTRUCT=#{self_destruct_value}")
prompt.ok("\nYou can re-run this command to see the state of the self-destruct process.")
rescue TTY::Reader::InputInterrupt
exit(1)
end
private
def prompt
@prompt ||= TTY::Prompt.new
end
end
end
end

View File

@ -8,6 +8,7 @@ require_relative 'canonical_email_blocks'
require_relative 'domains' require_relative 'domains'
require_relative 'email_domain_blocks' require_relative 'email_domain_blocks'
require_relative 'emoji' require_relative 'emoji'
require_relative 'federation'
require_relative 'feeds' require_relative 'feeds'
require_relative 'ip_blocks' require_relative 'ip_blocks'
require_relative 'maintenance' require_relative 'maintenance'
@ -65,66 +66,7 @@ module Mastodon::CLI
desc 'maintenance SUBCOMMAND ...ARGS', 'Various maintenance utilities' desc 'maintenance SUBCOMMAND ...ARGS', 'Various maintenance utilities'
subcommand 'maintenance', Maintenance subcommand 'maintenance', Maintenance
desc 'self-destruct', 'Erase the server from the federation' include Federation
long_desc <<~LONG_DESC
Erase the server from the federation by broadcasting account delete
activities to all known other servers. This allows a "clean exit" from
running a Mastodon server, as it leaves next to no cache behind on
other servers.
This command is always interactive and requires confirmation twice.
No local data is actually deleted, because emptying the
database or removing files is much faster through other, external
means, such as e.g. deleting the entire VPS. However, because other
servers will delete data about local users, but no local data will be
updated (such as e.g. followers), there will be a state mismatch
that will lead to glitches and issues if you then continue to run and use
the server.
So either you know exactly what you are doing, or you are starting
from a blank slate afterwards by manually clearing out all the local
data!
LONG_DESC
def self_destruct
require 'tty-prompt'
prompt = TTY::Prompt.new
if SelfDestructHelper.self_destruct?
prompt.ok('Self-destruct mode is already enabled for this Mastodon server')
pending_accounts = Account.local.without_suspended.count + Account.local.suspended.joins(:deletion_request).count
sidekiq_stats = Sidekiq::Stats.new
if pending_accounts.positive?
prompt.warn("#{pending_accounts} accounts are still pending deletion.")
elsif sidekiq_stats.enqueued.positive?
prompt.warn('Deletion notices are still being processed')
elsif sidekiq_stats.retry_size.positive?
prompt.warn('At least one delivery attempt for each deletion notice has been made, but some have failed and are scheduled for retry')
else
prompt.ok('Every deletion notice has been sent! You can safely delete all data and decomission your servers!')
end
exit(0)
end
exit(1) unless prompt.ask('Type in the domain of the server to confirm:', required: true) == Rails.configuration.x.local_domain
prompt.warn('This operation WILL NOT be reversible.')
prompt.warn('While the data won\'t be erased locally, the server will be in a BROKEN STATE afterwards.')
prompt.warn('The deletion process itself may take a long time, and will be handled by Sidekiq, so do not shut it down until it has finished (you will be able to re-run this command to see the state of the self-destruct process).')
exit(1) if prompt.no?('Are you sure you want to proceed?')
self_destruct_value = Rails.application.message_verifier('self-destruct').generate(Rails.configuration.x.local_domain)
prompt.ok('To switch Mastodon to self-destruct mode, add the following variable to your evironment (e.g. by adding a line to your `.env.production`) and restart all Mastodon processes:')
prompt.ok(" SELF_DESTRUCT=#{self_destruct_value}")
prompt.ok("\nYou can re-run this command to see the state of the self-destruct process.")
rescue TTY::Reader::InputInterrupt
exit(1)
end
map %w(--version -v) => :version map %w(--version -v) => :version

View File

@ -164,6 +164,7 @@
"@types/object-assign": "^4.0.30", "@types/object-assign": "^4.0.30",
"@types/prop-types": "^15.7.5", "@types/prop-types": "^15.7.5",
"@types/punycode": "^2.1.0", "@types/punycode": "^2.1.0",
"@types/rails__ujs": "^6.0.4",
"@types/react": "^18.2.7", "@types/react": "^18.2.7",
"@types/react-dom": "^18.2.4", "@types/react-dom": "^18.2.4",
"@types/react-helmet": "^6.1.6", "@types/react-helmet": "^6.1.6",
@ -187,6 +188,7 @@
"babel-jest": "^29.5.0", "babel-jest": "^29.5.0",
"eslint": "^8.41.0", "eslint": "^8.41.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-define-config": "^2.0.0",
"eslint-import-resolver-typescript": "^3.5.5", "eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-formatjs": "^4.10.1", "eslint-plugin-formatjs": "^4.10.1",
"eslint-plugin-import": "~2.29.0", "eslint-plugin-import": "~2.29.0",

View File

@ -28,7 +28,7 @@ describe Api::V1::Accounts::FamiliarFollowersController do
account_ids = [account_a, account_b, account_b, account_a, account_a].map { |a| a.id.to_s } account_ids = [account_a, account_b, account_b, account_a, account_a].map { |a| a.id.to_s }
get :index, params: { id: account_ids } get :index, params: { id: account_ids }
expect(body_as_json.pluck(:id)).to eq [account_a.id.to_s, account_b.id.to_s] expect(body_as_json.pluck(:id)).to contain_exactly(account_a.id.to_s, account_b.id.to_s)
end end
end end
end end

View File

@ -85,7 +85,7 @@ RSpec.describe ChallengableConcern do
before { get :foo } before { get :foo }
it 'renders challenge' do it 'renders challenge' do
expect(response).to render_template('auth/challenges/new') expect(response).to render_template('auth/challenges/new', layout: :auth)
end end
# See Auth::ChallengesControllerSpec # See Auth::ChallengesControllerSpec
@ -95,7 +95,7 @@ RSpec.describe ChallengableConcern do
before { post :bar } before { post :bar }
it 'renders challenge' do it 'renders challenge' do
expect(response).to render_template('auth/challenges/new') expect(response).to render_template('auth/challenges/new', layout: :auth)
end end
it 'accepts correct password' do it 'accepts correct password' do
@ -106,7 +106,7 @@ RSpec.describe ChallengableConcern do
it 'rejects wrong password' do it 'rejects wrong password' do
post :bar, params: { form_challenge: { current_password: 'dddfff888123' } } post :bar, params: { form_challenge: { current_password: 'dddfff888123' } }
expect(response.body).to render_template('auth/challenges/new') expect(response.body).to render_template('auth/challenges/new', layout: :auth)
expect(session[:challenge_passed_at]).to be_nil expect(session[:challenge_passed_at]).to be_nil
end end
end end

View File

@ -20,37 +20,30 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
[true, false].each do |with_otp_secret| [true, false].each do |with_otp_secret|
let(:user) { Fabricate(:user, email: 'local-part@domain', otp_secret: with_otp_secret ? 'oldotpsecret' : nil) } let(:user) { Fabricate(:user, email: 'local-part@domain', otp_secret: with_otp_secret ? 'oldotpsecret' : nil) }
describe 'GET #new' do context 'when signed in' do
context 'when signed in and a new otp secret has been set in the session' do before { sign_in user, scope: :user }
subject do
sign_in user, scope: :user describe 'GET #new' do
get :new, session: { challenge_passed_at: Time.now.utc, new_otp_secret: 'thisisasecretforthespecofnewview' } context 'when a new otp secret has been set in the session' do
subject do
get :new, session: { challenge_passed_at: Time.now.utc, new_otp_secret: 'thisisasecretforthespecofnewview' }
end
include_examples 'renders :new'
end end
include_examples 'renders :new' it 'redirects if a new otp_secret has not been set in the session' do
end get :new, session: { challenge_passed_at: Time.now.utc }
it 'redirects if not signed in' do expect(response).to redirect_to('/settings/otp_authentication')
get :new
expect(response).to redirect_to('/auth/sign_in')
end
it 'redirects if a new otp_secret has not been set in the session' do
sign_in user, scope: :user
get :new, session: { challenge_passed_at: Time.now.utc }
expect(response).to redirect_to('/settings/otp_authentication')
end
end
describe 'POST #create' do
context 'when signed in' do
before do
sign_in user, scope: :user
end end
end
describe 'POST #create' do
describe 'when form_two_factor_confirmation parameter is not provided' do describe 'when form_two_factor_confirmation parameter is not provided' do
it 'raises ActionController::ParameterMissing' do it 'raises ActionController::ParameterMissing' do
post :create, params: {}, session: { challenge_passed_at: Time.now.utc, new_otp_secret: 'thisisasecretforthespecofnewview' } post :create, params: {}, session: { challenge_passed_at: Time.now.utc, new_otp_secret: 'thisisasecretforthespecofnewview' }
expect(response).to have_http_status(400) expect(response).to have_http_status(400)
end end
end end
@ -58,69 +51,78 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
describe 'when creation succeeds' do describe 'when creation succeeds' do
let!(:otp_backup_codes) { user.generate_otp_backup_codes! } let!(:otp_backup_codes) { user.generate_otp_backup_codes! }
it 'renders page with success' do before do
prepare_user_otp_generation prepare_user_otp_generation
prepare_user_otp_consumption prepare_user_otp_consumption_response(true)
allow(controller).to receive(:current_user).and_return(user) allow(controller).to receive(:current_user).and_return(user)
end
expect do it 'renders page with success' do
post :create, expect { post_create_with_options }
params: { form_two_factor_confirmation: { otp_attempt: '123456' } }, .to change { user.reload.otp_secret }.to 'thisisasecretforthespecofnewview'
session: { challenge_passed_at: Time.now.utc, new_otp_secret: 'thisisasecretforthespecofnewview' }
end.to change { user.reload.otp_secret }.to 'thisisasecretforthespecofnewview'
expect(assigns(:recovery_codes)).to eq otp_backup_codes expect(assigns(:recovery_codes)).to eq otp_backup_codes
expect(flash[:notice]).to eq 'Two-factor authentication successfully enabled' expect(flash[:notice]).to eq 'Two-factor authentication successfully enabled'
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response).to render_template('settings/two_factor_authentication/recovery_codes/index') expect(response).to render_template('settings/two_factor_authentication/recovery_codes/index')
end end
def prepare_user_otp_generation
allow(user)
.to receive(:generate_otp_backup_codes!)
.and_return(otp_backup_codes)
end
def prepare_user_otp_consumption
options = { otp_secret: 'thisisasecretforthespecofnewview' }
allow(user)
.to receive(:validate_and_consume_otp!)
.with('123456', options)
.and_return(true)
end
end end
describe 'when creation fails' do describe 'when creation fails' do
subject do subject do
options = { otp_secret: 'thisisasecretforthespecofnewview' } expect { post_create_with_options }
allow(user) .to(not_change { user.reload.otp_secret })
.to receive(:validate_and_consume_otp!)
.with('123456', options)
.and_return(false)
allow(controller).to receive(:current_user).and_return(user)
expect do
post :create,
params: { form_two_factor_confirmation: { otp_attempt: '123456' } },
session: { challenge_passed_at: Time.now.utc, new_otp_secret: 'thisisasecretforthespecofnewview' }
end.to(not_change { user.reload.otp_secret })
end end
it 'renders the new view' do before do
prepare_user_otp_consumption_response(false)
allow(controller).to receive(:current_user).and_return(user)
end
it 'renders page with error message' do
subject subject
expect(response.body).to include 'The entered code was invalid! Are server time and device time correct?' expect(response.body).to include 'The entered code was invalid! Are server time and device time correct?'
end end
include_examples 'renders :new' include_examples 'renders :new'
end end
end
context 'when not signed in' do private
it 'redirects if not signed in' do
post :create, params: { form_two_factor_confirmation: { otp_attempt: '123456' } } def post_create_with_options
expect(response).to redirect_to('/auth/sign_in') post :create,
params: { form_two_factor_confirmation: { otp_attempt: '123456' } },
session: { challenge_passed_at: Time.now.utc, new_otp_secret: 'thisisasecretforthespecofnewview' }
end
def prepare_user_otp_generation
allow(user)
.to receive(:generate_otp_backup_codes!)
.and_return(otp_backup_codes)
end
def prepare_user_otp_consumption_response(result)
options = { otp_secret: 'thisisasecretforthespecofnewview' }
allow(user)
.to receive(:validate_and_consume_otp!)
.with('123456', options)
.and_return(result)
end end
end end
end end
end end
context 'when not signed in' do
it 'redirects on POST to create' do
post :create, params: { form_two_factor_confirmation: { otp_attempt: '123456' } }
expect(response).to redirect_to('/auth/sign_in')
end
it 'redirects on GET to new' do
get :new
expect(response).to redirect_to('/auth/sign_in')
end
end
end end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
Fabricator('Admin::ActionLog') do Fabricator(:action_log, from: Admin::ActionLog) do
account { Fabricate.build(:account) } account { Fabricate.build(:account) }
action 'MyString' action 'MyString'
target nil target nil

View File

@ -59,10 +59,10 @@ describe ContentSecurityPolicy do
end end
end end
describe '#media_host' do describe '#media_hosts' do
context 'when there is no configured CDN' do context 'when there is no configured CDN' do
it 'defaults to using the assets_host value' do it 'defaults to using the assets_host value' do
expect(subject.media_host).to eq(subject.assets_host) expect(subject.media_hosts).to contain_exactly(subject.assets_host)
end end
end end
@ -74,7 +74,7 @@ describe ContentSecurityPolicy do
end end
it 'uses the s3 alias host value' do it 'uses the s3 alias host value' do
expect(subject.media_host).to eq 'https://asset-host.s3-alias.example' expect(subject.media_hosts).to contain_exactly(subject.assets_host, 'https://asset-host.s3-alias.example')
end end
end end
@ -86,7 +86,7 @@ describe ContentSecurityPolicy do
end end
it 'uses the s3 alias host value and preserves the path' do it 'uses the s3 alias host value and preserves the path' do
expect(subject.media_host).to eq 'https://asset-host.s3-alias.example/pathname/' expect(subject.media_hosts).to contain_exactly(subject.assets_host, 'https://asset-host.s3-alias.example/pathname/')
end end
end end
@ -98,7 +98,7 @@ describe ContentSecurityPolicy do
end end
it 'uses the s3 cloudfront host value' do it 'uses the s3 cloudfront host value' do
expect(subject.media_host).to eq 'https://asset-host.s3-cloudfront.example' expect(subject.media_hosts).to contain_exactly(subject.assets_host, 'https://asset-host.s3-cloudfront.example')
end end
end end
@ -110,7 +110,7 @@ describe ContentSecurityPolicy do
end end
it 'uses the azure alias host value' do it 'uses the azure alias host value' do
expect(subject.media_host).to eq 'https://asset-host.azure-alias.example' expect(subject.media_hosts).to contain_exactly(subject.assets_host, 'https://asset-host.azure-alias.example')
end end
end end
@ -122,7 +122,7 @@ describe ContentSecurityPolicy do
end end
it 'uses the s3 hostname host value' do it 'uses the s3 hostname host value' do
expect(subject.media_host).to eq 'https://asset-host.s3.example' expect(subject.media_hosts).to contain_exactly(subject.assets_host, 'https://asset-host.s3.example')
end end
end end
end end

View File

@ -110,9 +110,9 @@ describe Report do
let(:status) { Fabricate(:status) } let(:status) { Fabricate(:status) }
before do before do
Fabricate('Admin::ActionLog', target_type: 'Report', account_id: target_account.id, target_id: report.id, created_at: 2.days.ago) Fabricate(:action_log, target_type: 'Report', account_id: target_account.id, target_id: report.id, created_at: 2.days.ago)
Fabricate('Admin::ActionLog', target_type: 'Account', account_id: target_account.id, target_id: report.target_account_id, created_at: 2.days.ago) Fabricate(:action_log, target_type: 'Account', account_id: target_account.id, target_id: report.target_account_id, created_at: 2.days.ago)
Fabricate('Admin::ActionLog', target_type: 'Status', account_id: target_account.id, target_id: status.id, created_at: 2.days.ago) Fabricate(:action_log, target_type: 'Status', account_id: target_account.id, target_id: status.id, created_at: 2.days.ago)
end end
it 'returns right logs' do it 'returns right logs' do

View File

@ -10,5 +10,11 @@ describe REST::InstanceSerializer do
it 'returns recent usage data' do it 'returns recent usage data' do
expect(serialization['usage']).to eq({ 'users' => { 'active_month' => 0 } }) expect(serialization['usage']).to eq({ 'users' => { 'active_month' => 0 } })
end end
it 'returns the VAPID public key' do
expect(serialization['configuration']['vapid']).to eq({
'public_key' => Rails.configuration.x.vapid_public_key,
})
end
end end
end end

View File

@ -384,7 +384,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do
end end
it 'updates the existing media attachment in-place' do it 'updates the existing media attachment in-place' do
media_attachment = status.media_attachments.reload.first media_attachment = status.media_attachments.ordered.reload.first
expect(media_attachment).to_not be_nil expect(media_attachment).to_not be_nil
expect(media_attachment.remote_url).to eq 'https://example.com/foo.png' expect(media_attachment.remote_url).to eq 'https://example.com/foo.png'

View File

@ -144,6 +144,19 @@ RSpec.describe ResolveAccountService, type: :service do
end end
end end
context 'with webfinger response subject missing a host value' do
let(:body) { Oj.dump({ subject: 'user@' }) }
let(:url) { 'https://host.example/.well-known/webfinger?resource=acct:user@host.example' }
before do
stub_request(:get, url).to_return(status: 200, body: body)
end
it 'returns nil with incomplete subject in response' do
expect(subject.call('user@host.example')).to be_nil
end
end
context 'with an ActivityPub account' do context 'with an ActivityPub account' do
it 'returns new remote account' do it 'returns new remote account' do
account = subject.call('foo@ap.example.com') account = subject.call('foo@ap.example.com')

7
streaming/.dockerignore Normal file
View File

@ -0,0 +1,7 @@
.env
.env.*
.gitignore
node_modules
.DS_Store
*.swp
*~

32
streaming/.eslintrc.js Normal file
View File

@ -0,0 +1,32 @@
// @ts-check
const { defineConfig } = require('eslint-define-config');
module.exports = defineConfig({
extends: ['../.eslintrc.js'],
env: {
browser: false,
},
parserOptions: {
project: true,
tsconfigRootDir: __dirname,
ecmaFeatures: {
jsx: false,
},
ecmaVersion: 2021,
},
rules: {
'import/no-commonjs': 'off',
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: [
'streaming/.eslintrc.js',
],
optionalDependencies: false,
peerDependencies: false,
includeTypes: true,
packageDir: __dirname,
},
],
},
});

104
streaming/Dockerfile Normal file
View File

@ -0,0 +1,104 @@
# syntax=docker/dockerfile:1.4
# Please see https://docs.docker.com/engine/reference/builder for information about
# the extended buildx capabilities used in this file.
# Make sure multiarch TARGETPLATFORM is available for interpolation
# See: https://docs.docker.com/build/building/multi-platform/
ARG TARGETPLATFORM=${TARGETPLATFORM}
ARG BUILDPLATFORM=${BUILDPLATFORM}
# Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
ARG NODE_MAJOR_VERSION="20"
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
ARG DEBIAN_VERSION="bookworm"
# Node image to use for base image based on combined variables (ex: 20-bookworm-slim)
FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim as streaming
# Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin]
ARG TZ="Etc/UTC"
# Linux UID (user id) for the mastodon user, change with [--build-arg UID=1234]
ARG UID="991"
# Linux GID (group id) for the mastodon user, change with [--build-arg GID=1234]
ARG GID="991"
# Apply Mastodon build options based on options above
ENV \
# Apply Mastodon version information
MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \
MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}" \
# Apply timezone
TZ=${TZ}
ENV \
# Configure the IP to bind Mastodon to when serving traffic
BIND="0.0.0.0" \
# Explicitly set PORT to match the exposed port
PORT=4000 \
# Use production settings for Yarn, Node and related nodejs based tools
NODE_ENV="production" \
# Add Ruby and Mastodon installation to the PATH
DEBIAN_FRONTEND="noninteractive"
# Set default shell used for running commands
SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-c"]
ARG TARGETPLATFORM
RUN echo "Target platform is ${TARGETPLATFORM}"
RUN \
# Remove automatic apt cache Docker cleanup scripts
rm -f /etc/apt/apt.conf.d/docker-clean; \
# Sets timezone
echo "${TZ}" > /etc/localtime; \
# Creates mastodon user/group and sets home directory
groupadd -g "${GID}" mastodon; \
useradd -l -u "${UID}" -g "${GID}" -m -d /opt/mastodon mastodon; \
# Creates symlink for /mastodon folder
ln -s /opt/mastodon /mastodon;
# hadolint ignore=DL3008,DL3005
RUN \
# Mount Apt cache and lib directories from Docker buildx caches
--mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
# Upgrade to check for security updates to Debian image
apt-get update; \
apt-get dist-upgrade -yq; \
apt-get install -y --no-install-recommends \
ca-certificates \
curl \
tzdata \
;
# Set /opt/mastodon as working directory
WORKDIR /opt/mastodon
# Copy Node package configuration files from build system to container
COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/
COPY .yarn /opt/mastodon/.yarn
# Copy Streaming source code from build system to container
COPY ./streaming /opt/mastodon/streaming
RUN \
# Mount local Corepack and Yarn caches from Docker buildx caches
--mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \
--mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \
# Configure Corepack
rm /usr/local/bin/yarn*; \
corepack enable; \
corepack prepare --activate;
RUN \
# Mount Corepack and Yarn caches from Docker buildx caches
--mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \
--mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \
# Install Node packages
yarn workspaces focus --production @mastodon/streaming;
# Set the running user for resulting container
USER mastodon
# Expose default Streaming ports
EXPOSE 4000
# Run streaming when started
CMD [ node ./streaming/index.js ]

View File

@ -12,10 +12,12 @@ const { JSDOM } = require('jsdom');
const log = require('npmlog'); const log = require('npmlog');
const pg = require('pg'); const pg = require('pg');
const dbUrlToConfig = require('pg-connection-string').parse; const dbUrlToConfig = require('pg-connection-string').parse;
const metrics = require('prom-client');
const uuid = require('uuid'); const uuid = require('uuid');
const WebSocket = require('ws'); const WebSocket = require('ws');
const { setupMetrics } = require('./metrics');
const { isTruthy } = require("./utils");
const environment = process.env.NODE_ENV || 'development'; const environment = process.env.NODE_ENV || 'development';
// Correctly detect and load .env or .env.production file based on environment: // Correctly detect and load .env or .env.production file based on environment:
@ -196,78 +198,15 @@ const startServer = async () => {
const redisClient = await createRedisClient(redisConfig); const redisClient = await createRedisClient(redisConfig);
const { redisPrefix } = redisConfig; const { redisPrefix } = redisConfig;
// Collect metrics from Node.js const metrics = setupMetrics(CHANNEL_NAMES, pgPool);
metrics.collectDefaultMetrics(); // TODO: migrate all metrics to metrics.X.method() instead of just X.method()
const {
new metrics.Gauge({ connectedClients,
name: 'pg_pool_total_connections', connectedChannels,
help: 'The total number of clients existing within the pool', redisSubscriptions,
collect() { redisMessagesReceived,
this.set(pgPool.totalCount); messagesSent,
}, } = metrics;
});
new metrics.Gauge({
name: 'pg_pool_idle_connections',
help: 'The number of clients which are not checked out but are currently idle in the pool',
collect() {
this.set(pgPool.idleCount);
},
});
new metrics.Gauge({
name: 'pg_pool_waiting_queries',
help: 'The number of queued requests waiting on a client when all clients are checked out',
collect() {
this.set(pgPool.waitingCount);
},
});
const connectedClients = new metrics.Gauge({
name: 'connected_clients',
help: 'The number of clients connected to the streaming server',
labelNames: ['type'],
});
const connectedChannels = new metrics.Gauge({
name: 'connected_channels',
help: 'The number of channels the streaming server is streaming to',
labelNames: [ 'type', 'channel' ]
});
const redisSubscriptions = new metrics.Gauge({
name: 'redis_subscriptions',
help: 'The number of Redis channels the streaming server is subscribed to',
});
const redisMessagesReceived = new metrics.Counter({
name: 'redis_messages_received_total',
help: 'The total number of messages the streaming server has received from redis subscriptions'
});
const messagesSent = new metrics.Counter({
name: 'messages_sent_total',
help: 'The total number of messages the streaming server sent to clients per connection type',
labelNames: [ 'type' ]
});
// Prime the gauges so we don't loose metrics between restarts:
redisSubscriptions.set(0);
connectedClients.set({ type: 'websocket' }, 0);
connectedClients.set({ type: 'eventsource' }, 0);
// For each channel, initialize the gauges at zero; There's only a finite set of channels available
CHANNEL_NAMES.forEach(( channel ) => {
connectedChannels.set({ type: 'websocket', channel }, 0);
connectedChannels.set({ type: 'eventsource', channel }, 0);
});
// Prime the counters so that we don't loose metrics between restarts.
// Unfortunately counters don't support the set() API, so instead I'm using
// inc(0) to achieve the same result.
redisMessagesReceived.inc(0);
messagesSent.inc({ type: 'websocket' }, 0);
messagesSent.inc({ type: 'eventsource' }, 0);
// When checking metrics in the browser, the favicon is requested this // When checking metrics in the browser, the favicon is requested this
// prevents the request from falling through to the API Router, which would // prevents the request from falling through to the API Router, which would
@ -388,25 +327,6 @@ const startServer = async () => {
} }
}; };
const FALSE_VALUES = [
false,
0,
'0',
'f',
'F',
'false',
'FALSE',
'off',
'OFF',
];
/**
* @param {any} value
* @returns {boolean}
*/
const isTruthy = value =>
value && !FALSE_VALUES.includes(value);
/** /**
* @param {any} req * @param {any} req
* @param {any} res * @param {any} res

105
streaming/metrics.js Normal file
View File

@ -0,0 +1,105 @@
// @ts-check
const metrics = require('prom-client');
/**
* @typedef StreamingMetrics
* @property {metrics.Registry} register
* @property {metrics.Gauge<"type">} connectedClients
* @property {metrics.Gauge<"type" | "channel">} connectedChannels
* @property {metrics.Gauge} redisSubscriptions
* @property {metrics.Counter} redisMessagesReceived
* @property {metrics.Counter<"type">} messagesSent
*/
/**
*
* @param {string[]} channels
* @param {import('pg').Pool} pgPool
* @returns {StreamingMetrics}
*/
function setupMetrics(channels, pgPool) {
// Collect metrics from Node.js
metrics.collectDefaultMetrics();
new metrics.Gauge({
name: 'pg_pool_total_connections',
help: 'The total number of clients existing within the pool',
collect() {
this.set(pgPool.totalCount);
},
});
new metrics.Gauge({
name: 'pg_pool_idle_connections',
help: 'The number of clients which are not checked out but are currently idle in the pool',
collect() {
this.set(pgPool.idleCount);
},
});
new metrics.Gauge({
name: 'pg_pool_waiting_queries',
help: 'The number of queued requests waiting on a client when all clients are checked out',
collect() {
this.set(pgPool.waitingCount);
},
});
const connectedClients = new metrics.Gauge({
name: 'connected_clients',
help: 'The number of clients connected to the streaming server',
labelNames: ['type'],
});
const connectedChannels = new metrics.Gauge({
name: 'connected_channels',
help: 'The number of channels the streaming server is streaming to',
labelNames: [ 'type', 'channel' ]
});
const redisSubscriptions = new metrics.Gauge({
name: 'redis_subscriptions',
help: 'The number of Redis channels the streaming server is subscribed to',
});
const redisMessagesReceived = new metrics.Counter({
name: 'redis_messages_received_total',
help: 'The total number of messages the streaming server has received from redis subscriptions'
});
const messagesSent = new metrics.Counter({
name: 'messages_sent_total',
help: 'The total number of messages the streaming server sent to clients per connection type',
labelNames: [ 'type' ]
});
// Prime the gauges so we don't loose metrics between restarts:
redisSubscriptions.set(0);
connectedClients.set({ type: 'websocket' }, 0);
connectedClients.set({ type: 'eventsource' }, 0);
// For each channel, initialize the gauges at zero; There's only a finite set of channels available
channels.forEach(( channel ) => {
connectedChannels.set({ type: 'websocket', channel }, 0);
connectedChannels.set({ type: 'eventsource', channel }, 0);
});
// Prime the counters so that we don't loose metrics between restarts.
// Unfortunately counters don't support the set() API, so instead I'm using
// inc(0) to achieve the same result.
redisMessagesReceived.inc(0);
messagesSent.inc({ type: 'websocket' }, 0);
messagesSent.inc({ type: 'eventsource' }, 0);
return {
register: metrics.register,
connectedClients,
connectedChannels,
redisSubscriptions,
redisMessagesReceived,
messagesSent,
};
}
exports.setupMetrics = setupMetrics;

View File

@ -12,7 +12,8 @@
"url": "https://github.com/mastodon/mastodon.git" "url": "https://github.com/mastodon/mastodon.git"
}, },
"scripts": { "scripts": {
"start": "node ./index.js" "start": "node ./index.js",
"check:types": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
@ -30,7 +31,10 @@
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/npmlog": "^7.0.0", "@types/npmlog": "^7.0.0",
"@types/pg": "^8.6.6", "@types/pg": "^8.6.6",
"@types/uuid": "^9.0.0" "@types/uuid": "^9.0.0",
"@types/ws": "^8.5.9",
"eslint-define-config": "^2.0.0",
"typescript": "^5.0.4"
}, },
"optionalDependencies": { "optionalDependencies": {
"bufferutil": "^4.0.7", "bufferutil": "^4.0.7",

11
streaming/tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"target": "esnext",
"module": "CommonJS",
"moduleResolution": "node",
"noUnusedParameters": false,
"paths": {}
},
"include": ["./*.js", "./.eslintrc.js"]
}

22
streaming/utils.js Normal file
View File

@ -0,0 +1,22 @@
// @ts-check
const FALSE_VALUES = [
false,
0,
'0',
'f',
'F',
'false',
'FALSE',
'off',
'OFF',
];
/**
* @param {any} value
* @returns {boolean}
*/
const isTruthy = value =>
value && !FALSE_VALUES.includes(value);
exports.isTruthy = isTruthy;

302
yarn.lock
View File

@ -42,55 +42,55 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.13": "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.23.5":
version: 7.22.13 version: 7.23.5
resolution: "@babel/code-frame@npm:7.22.13" resolution: "@babel/code-frame@npm:7.23.5"
dependencies: dependencies:
"@babel/highlight": "npm:^7.22.13" "@babel/highlight": "npm:^7.23.4"
chalk: "npm:^2.4.2" chalk: "npm:^2.4.2"
checksum: f4cc8ae1000265677daf4845083b72f88d00d311adb1a93c94eb4b07bf0ed6828a81ae4ac43ee7d476775000b93a28a9cddec18fbdc5796212d8dcccd5de72bd checksum: a10e843595ddd9f97faa99917414813c06214f4d9205294013e20c70fbdf4f943760da37dec1d998bf3e6fc20fa2918a47c0e987a7e458663feb7698063ad7c6
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.22.9, @babel/compat-data@npm:^7.23.3": "@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.22.9, @babel/compat-data@npm:^7.23.3, @babel/compat-data@npm:^7.23.5":
version: 7.23.3 version: 7.23.5
resolution: "@babel/compat-data@npm:7.23.3" resolution: "@babel/compat-data@npm:7.23.5"
checksum: c6af331753c34ee8a5678bc94404320826cb56b1dda3efc1311ec8fb0774e78225132f3c1acc988440ace667f14a838e297a822692b95758aa63da406e1f97a1 checksum: 081278ed46131a890ad566a59c61600a5f9557bd8ee5e535890c8548192532ea92590742fd74bd9db83d74c669ef8a04a7e1c85cdea27f960233e3b83c3a957c
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/core@npm:^7.10.4, @babel/core@npm:^7.11.1, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.22.1": "@babel/core@npm:^7.10.4, @babel/core@npm:^7.11.1, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.22.1":
version: 7.23.3 version: 7.23.5
resolution: "@babel/core@npm:7.23.3" resolution: "@babel/core@npm:7.23.5"
dependencies: dependencies:
"@ampproject/remapping": "npm:^2.2.0" "@ampproject/remapping": "npm:^2.2.0"
"@babel/code-frame": "npm:^7.22.13" "@babel/code-frame": "npm:^7.23.5"
"@babel/generator": "npm:^7.23.3" "@babel/generator": "npm:^7.23.5"
"@babel/helper-compilation-targets": "npm:^7.22.15" "@babel/helper-compilation-targets": "npm:^7.22.15"
"@babel/helper-module-transforms": "npm:^7.23.3" "@babel/helper-module-transforms": "npm:^7.23.3"
"@babel/helpers": "npm:^7.23.2" "@babel/helpers": "npm:^7.23.5"
"@babel/parser": "npm:^7.23.3" "@babel/parser": "npm:^7.23.5"
"@babel/template": "npm:^7.22.15" "@babel/template": "npm:^7.22.15"
"@babel/traverse": "npm:^7.23.3" "@babel/traverse": "npm:^7.23.5"
"@babel/types": "npm:^7.23.3" "@babel/types": "npm:^7.23.5"
convert-source-map: "npm:^2.0.0" convert-source-map: "npm:^2.0.0"
debug: "npm:^4.1.0" debug: "npm:^4.1.0"
gensync: "npm:^1.0.0-beta.2" gensync: "npm:^1.0.0-beta.2"
json5: "npm:^2.2.3" json5: "npm:^2.2.3"
semver: "npm:^6.3.1" semver: "npm:^6.3.1"
checksum: 08d43b749e24052d12713a7fb1f0c0d1275d4fb056d00846faeb8da79ecf6d0ba91a11b6afec407b8b0f9388d00e2c2f485f282bef0ade4d6d0a17de191a4287 checksum: 311a512a870ee330a3f9a7ea89e5df790b2b5af0b1bd98b10b4edc0de2ac440f0df4d69ea2c0ee38a4b89041b9a495802741d93603be7d4fd834ec8bb6970bd2
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/generator@npm:^7.23.3, @babel/generator@npm:^7.7.2": "@babel/generator@npm:^7.23.5, @babel/generator@npm:^7.7.2":
version: 7.23.3 version: 7.23.5
resolution: "@babel/generator@npm:7.23.3" resolution: "@babel/generator@npm:7.23.5"
dependencies: dependencies:
"@babel/types": "npm:^7.23.3" "@babel/types": "npm:^7.23.5"
"@jridgewell/gen-mapping": "npm:^0.3.2" "@jridgewell/gen-mapping": "npm:^0.3.2"
"@jridgewell/trace-mapping": "npm:^0.3.17" "@jridgewell/trace-mapping": "npm:^0.3.17"
jsesc: "npm:^2.5.1" jsesc: "npm:^2.5.1"
checksum: d5fff1417eecfada040e01a7c77a4968e81c436aeb35815ce85b4e80cd01e731423613d61033044a6cb5563bb8449ee260e3379b63eb50b38ec0a9ea9c00abfd checksum: 14c6e874f796c4368e919bed6003bb0adc3ce837760b08f9e646d20aeb5ae7d309723ce6e4f06bcb4a2b5753145446c8e4425851380f695e40e71e1760f49e7b
languageName: node languageName: node
linkType: hard linkType: hard
@ -310,10 +310,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/helper-string-parser@npm:^7.22.5": "@babel/helper-string-parser@npm:^7.23.4":
version: 7.22.5 version: 7.23.4
resolution: "@babel/helper-string-parser@npm:7.22.5" resolution: "@babel/helper-string-parser@npm:7.23.4"
checksum: 6b0ff8af724377ec41e5587fffa7605198da74cb8e7d8d48a36826df0c0ba210eb9fedb3d9bef4d541156e0bd11040f021945a6cbb731ccec4aefb4affa17aa4 checksum: f348d5637ad70b6b54b026d6544bd9040f78d24e7ec245a0fc42293968181f6ae9879c22d89744730d246ce8ec53588f716f102addd4df8bbc79b73ea10004ac
languageName: node languageName: node
linkType: hard linkType: hard
@ -324,10 +324,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/helper-validator-option@npm:^7.22.15": "@babel/helper-validator-option@npm:^7.22.15, @babel/helper-validator-option@npm:^7.23.5":
version: 7.22.15 version: 7.23.5
resolution: "@babel/helper-validator-option@npm:7.22.15" resolution: "@babel/helper-validator-option@npm:7.23.5"
checksum: e9661bf80ba18e2dd978217b350fb07298e57ac417f4f1ab9fa011505e20e4857f2c3b4b538473516a9dc03af5ce3a831e5ed973311c28326f4c330b6be981c2 checksum: af45d5c0defb292ba6fd38979e8f13d7da63f9623d8ab9ededc394f67eb45857d2601278d151ae9affb6e03d5d608485806cd45af08b4468a0515cf506510e94
languageName: node languageName: node
linkType: hard linkType: hard
@ -342,34 +342,34 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/helpers@npm:^7.23.2": "@babel/helpers@npm:^7.23.5":
version: 7.23.2 version: 7.23.5
resolution: "@babel/helpers@npm:7.23.2" resolution: "@babel/helpers@npm:7.23.5"
dependencies: dependencies:
"@babel/template": "npm:^7.22.15" "@babel/template": "npm:^7.22.15"
"@babel/traverse": "npm:^7.23.2" "@babel/traverse": "npm:^7.23.5"
"@babel/types": "npm:^7.23.0" "@babel/types": "npm:^7.23.5"
checksum: 3a6a939c5277a27486e7c626812f0643b35d1c053ac2eb66911f5ae6c0a4e4bcdd40750eba36b766b0ee8a753484287f50ae56232a5f8f2947116723e44b9e35 checksum: a37e2728eb4378a4888e5d614e28de7dd79b55ac8acbecd0e5c761273e2a02a8f33b34b1932d9069db55417ace2937cbf8ec37c42f1030ce6d228857d7ccaa4f
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/highlight@npm:^7.22.13": "@babel/highlight@npm:^7.23.4":
version: 7.22.20 version: 7.23.4
resolution: "@babel/highlight@npm:7.22.20" resolution: "@babel/highlight@npm:7.23.4"
dependencies: dependencies:
"@babel/helper-validator-identifier": "npm:^7.22.20" "@babel/helper-validator-identifier": "npm:^7.22.20"
chalk: "npm:^2.4.2" chalk: "npm:^2.4.2"
js-tokens: "npm:^4.0.0" js-tokens: "npm:^4.0.0"
checksum: f3c3a193afad23434297d88e81d1d6c0c2cf02423de2139ada7ce0a7fc62d8559abf4cc996533c1a9beca7fc990010eb8d544097f75e818ac113bf39ed810aa2 checksum: fbff9fcb2f5539289c3c097d130e852afd10d89a3a08ac0b5ebebbc055cc84a4bcc3dcfed463d488cde12dd0902ef1858279e31d7349b2e8cee43913744bda33
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.15, @babel/parser@npm:^7.23.3": "@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.15, @babel/parser@npm:^7.23.5":
version: 7.23.3 version: 7.23.5
resolution: "@babel/parser@npm:7.23.3" resolution: "@babel/parser@npm:7.23.5"
bin: bin:
parser: ./bin/babel-parser.js parser: ./bin/babel-parser.js
checksum: 0fe11eadd4146a9155305b5bfece0f8223a3b1b97357ffa163c0156940de92e76cd0e7a173de819b8692767147e62f33389b312d1537f84cede51092672df6ef checksum: 3356aa90d7bafb4e2c7310e7c2c3d443c4be4db74913f088d3d577a1eb914ea4188e05fd50a47ce907a27b755c4400c4e3cbeee73dbeb37761f6ca85954f5a20
languageName: node languageName: node
linkType: hard linkType: hard
@ -661,9 +661,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/plugin-transform-async-generator-functions@npm:^7.23.3": "@babel/plugin-transform-async-generator-functions@npm:^7.23.4":
version: 7.23.3 version: 7.23.4
resolution: "@babel/plugin-transform-async-generator-functions@npm:7.23.3" resolution: "@babel/plugin-transform-async-generator-functions@npm:7.23.4"
dependencies: dependencies:
"@babel/helper-environment-visitor": "npm:^7.22.20" "@babel/helper-environment-visitor": "npm:^7.22.20"
"@babel/helper-plugin-utils": "npm:^7.22.5" "@babel/helper-plugin-utils": "npm:^7.22.5"
@ -671,7 +671,7 @@ __metadata:
"@babel/plugin-syntax-async-generators": "npm:^7.8.4" "@babel/plugin-syntax-async-generators": "npm:^7.8.4"
peerDependencies: peerDependencies:
"@babel/core": ^7.0.0-0 "@babel/core": ^7.0.0-0
checksum: e846f282658e097fce4fccf3ee29289bf05f0654846a5994727a36f0cdc2e47abdffd4be4fa65787e94aa975824fae894c90afbfdc8caacd46c12c7f43e99d7f checksum: f2eef4de609975a3f7da7832576b5ffc93e43c80f87e1a99e886b0f8591096cfc4c37e2d5f52fdeaa2a9c09a25a59f3e621159abaca75d3193922a5c0e4cbe0c
languageName: node languageName: node
linkType: hard linkType: hard
@ -699,14 +699,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/plugin-transform-block-scoping@npm:^7.23.3": "@babel/plugin-transform-block-scoping@npm:^7.23.4":
version: 7.23.3 version: 7.23.4
resolution: "@babel/plugin-transform-block-scoping@npm:7.23.3" resolution: "@babel/plugin-transform-block-scoping@npm:7.23.4"
dependencies: dependencies:
"@babel/helper-plugin-utils": "npm:^7.22.5" "@babel/helper-plugin-utils": "npm:^7.22.5"
peerDependencies: peerDependencies:
"@babel/core": ^7.0.0-0 "@babel/core": ^7.0.0-0
checksum: ccaeded7954c196811d22a35322579254cda52676e823682b6234885a3aaf88fe0d5152dacaec43db9031dcf35a050a5343e36028e5905b0ba9c02d36b30a57f checksum: 83006804dddf980ab1bcd6d67bc381e24b58c776507c34f990468f820d0da71dba3697355ca4856532fa2eeb2a1e3e73c780f03760b5507a511cbedb0308e276
languageName: node languageName: node
linkType: hard linkType: hard
@ -722,22 +722,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/plugin-transform-class-static-block@npm:^7.23.3": "@babel/plugin-transform-class-static-block@npm:^7.23.4":
version: 7.23.3 version: 7.23.4
resolution: "@babel/plugin-transform-class-static-block@npm:7.23.3" resolution: "@babel/plugin-transform-class-static-block@npm:7.23.4"
dependencies: dependencies:
"@babel/helper-create-class-features-plugin": "npm:^7.22.15" "@babel/helper-create-class-features-plugin": "npm:^7.22.15"
"@babel/helper-plugin-utils": "npm:^7.22.5" "@babel/helper-plugin-utils": "npm:^7.22.5"
"@babel/plugin-syntax-class-static-block": "npm:^7.14.5" "@babel/plugin-syntax-class-static-block": "npm:^7.14.5"
peerDependencies: peerDependencies:
"@babel/core": ^7.12.0 "@babel/core": ^7.12.0
checksum: 89cdb66d7bc834cd51659eb7286a6bee23add0bc114943d68c4b6c0c834178cf0d55183df0cf508fec9c55ed4155641360e6f55a91c16fe826ccaf1adf381922 checksum: fdca96640ef29d8641a7f8de106f65f18871b38cc01c0f7b696d2b49c76b77816b30a812c08e759d06dd10b4d9b3af6b5e4ac22a2017a88c4077972224b77ab0
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/plugin-transform-classes@npm:^7.23.3": "@babel/plugin-transform-classes@npm:^7.23.5":
version: 7.23.3 version: 7.23.5
resolution: "@babel/plugin-transform-classes@npm:7.23.3" resolution: "@babel/plugin-transform-classes@npm:7.23.5"
dependencies: dependencies:
"@babel/helper-annotate-as-pure": "npm:^7.22.5" "@babel/helper-annotate-as-pure": "npm:^7.22.5"
"@babel/helper-compilation-targets": "npm:^7.22.15" "@babel/helper-compilation-targets": "npm:^7.22.15"
@ -750,7 +750,7 @@ __metadata:
globals: "npm:^11.1.0" globals: "npm:^11.1.0"
peerDependencies: peerDependencies:
"@babel/core": ^7.0.0-0 "@babel/core": ^7.0.0-0
checksum: 88bfd332db0ba5cbfb8557a2ba5a7185151aebc9cfe3035b014aa6d795556acbe672bb8c78da3c9fd1d23f55a333d14b5daa127ef037f5ced5198b6d79a146d6 checksum: 07988f52b4893151887d1ea6ff79e5fe834078c5731bd09babd5659edbbae21ea4e2de326a02443a63fd776b4c945da6177f07875b56fe66e0b7899e830a9e92
languageName: node languageName: node
linkType: hard linkType: hard
@ -800,15 +800,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/plugin-transform-dynamic-import@npm:^7.23.3": "@babel/plugin-transform-dynamic-import@npm:^7.23.4":
version: 7.23.3 version: 7.23.4
resolution: "@babel/plugin-transform-dynamic-import@npm:7.23.3" resolution: "@babel/plugin-transform-dynamic-import@npm:7.23.4"
dependencies: dependencies:
"@babel/helper-plugin-utils": "npm:^7.22.5" "@babel/helper-plugin-utils": "npm:^7.22.5"
"@babel/plugin-syntax-dynamic-import": "npm:^7.8.3" "@babel/plugin-syntax-dynamic-import": "npm:^7.8.3"
peerDependencies: peerDependencies:
"@babel/core": ^7.0.0-0 "@babel/core": ^7.0.0-0
checksum: df3fd130312dc53d068fa76333991dce5e86987b023af8c3b502bd7d36a8e67da6f718e61dc838576a9fbacd06628e29607ee22d9bae30705485c14130eab201 checksum: 19ae4a4a2ca86d35224734c41c48b2aa6a13139f3cfa1cbd18c0e65e461de8b65687dec7e52b7a72bb49db04465394c776aa1b13a2af5dc975b2a0cde3dcab67
languageName: node languageName: node
linkType: hard linkType: hard
@ -824,15 +824,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/plugin-transform-export-namespace-from@npm:^7.23.3": "@babel/plugin-transform-export-namespace-from@npm:^7.23.4":
version: 7.23.3 version: 7.23.4
resolution: "@babel/plugin-transform-export-namespace-from@npm:7.23.3" resolution: "@babel/plugin-transform-export-namespace-from@npm:7.23.4"
dependencies: dependencies:
"@babel/helper-plugin-utils": "npm:^7.22.5" "@babel/helper-plugin-utils": "npm:^7.22.5"
"@babel/plugin-syntax-export-namespace-from": "npm:^7.8.3" "@babel/plugin-syntax-export-namespace-from": "npm:^7.8.3"
peerDependencies: peerDependencies:
"@babel/core": ^7.0.0-0 "@babel/core": ^7.0.0-0
checksum: 390c6626dcda99023629049d92090242b4575351a4a7b47f97febabd2381f2cd0f624de661d8de8d1f715fedd63753cfd1feddead19e5960c27b88e447465b81 checksum: 38bf04f851e36240bbe83ace4169da626524f4107bfb91f05b4ad93a5fb6a36d5b3d30b8883c1ba575ccfc1bac7938e90ca2e3cb227f7b3f4a9424beec6fd4a7
languageName: node languageName: node
linkType: hard linkType: hard
@ -860,15 +860,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/plugin-transform-json-strings@npm:^7.23.3": "@babel/plugin-transform-json-strings@npm:^7.23.4":
version: 7.23.3 version: 7.23.4
resolution: "@babel/plugin-transform-json-strings@npm:7.23.3" resolution: "@babel/plugin-transform-json-strings@npm:7.23.4"
dependencies: dependencies:
"@babel/helper-plugin-utils": "npm:^7.22.5" "@babel/helper-plugin-utils": "npm:^7.22.5"
"@babel/plugin-syntax-json-strings": "npm:^7.8.3" "@babel/plugin-syntax-json-strings": "npm:^7.8.3"
peerDependencies: peerDependencies:
"@babel/core": ^7.0.0-0 "@babel/core": ^7.0.0-0
checksum: e1cef6a485b9da32aba9449fb459dac062dfc401f3d6ad48e7fbdcb73bbe470c995cc15ce5c421b95efe1e9a90d5507eb606360fe10b6d8cb869dd5dae7a2562 checksum: 39e82223992a9ad857722ae051291935403852ad24b0dd64c645ca1c10517b6bf9822377d88643fed8b3e61a4e3f7e5ae41cf90eb07c40a786505d47d5970e54
languageName: node languageName: node
linkType: hard linkType: hard
@ -883,15 +883,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/plugin-transform-logical-assignment-operators@npm:^7.23.3": "@babel/plugin-transform-logical-assignment-operators@npm:^7.23.4":
version: 7.23.3 version: 7.23.4
resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.23.3" resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.23.4"
dependencies: dependencies:
"@babel/helper-plugin-utils": "npm:^7.22.5" "@babel/helper-plugin-utils": "npm:^7.22.5"
"@babel/plugin-syntax-logical-assignment-operators": "npm:^7.10.4" "@babel/plugin-syntax-logical-assignment-operators": "npm:^7.10.4"
peerDependencies: peerDependencies:
"@babel/core": ^7.0.0-0 "@babel/core": ^7.0.0-0
checksum: 23b7588b26d420c8b132bd08916d49871ca0c8db892f6b58637b10e2a0d918163d413c505db880a9157fc2e61d089040f139298a60d837ccbd0efca0474ac7ca checksum: 87b034dd13143904e405887e6125d76c27902563486efc66b7d9a9d8f9406b76c6ac42d7b37224014af5783d7edb465db0cdecd659fa3227baad0b3a6a35deff
languageName: node languageName: node
linkType: hard linkType: hard
@ -980,7 +980,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.22.3, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.23.3": "@babel/plugin-transform-nullish-coalescing-operator@npm:^7.22.3, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.23.4":
version: 7.23.4 version: 7.23.4
resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.23.4" resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.23.4"
dependencies: dependencies:
@ -992,21 +992,21 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/plugin-transform-numeric-separator@npm:^7.23.3": "@babel/plugin-transform-numeric-separator@npm:^7.23.4":
version: 7.23.3 version: 7.23.4
resolution: "@babel/plugin-transform-numeric-separator@npm:7.23.3" resolution: "@babel/plugin-transform-numeric-separator@npm:7.23.4"
dependencies: dependencies:
"@babel/helper-plugin-utils": "npm:^7.22.5" "@babel/helper-plugin-utils": "npm:^7.22.5"
"@babel/plugin-syntax-numeric-separator": "npm:^7.10.4" "@babel/plugin-syntax-numeric-separator": "npm:^7.10.4"
peerDependencies: peerDependencies:
"@babel/core": ^7.0.0-0 "@babel/core": ^7.0.0-0
checksum: d3748cce20e8752e61dfda55e275c699459a3ff8d0bb46585da813136e04066b1ce70b71beef504fcdc8d4cca3c955112cea96d5e9fd5a42a5bc8956d05236c2 checksum: e34902da4f5588dc4812c92cb1f6a5e3e3647baf7b4623e30942f551bf1297621abec4e322ebfa50b320c987c0f34d9eb4355b3d289961d9035e2126e3119c12
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/plugin-transform-object-rest-spread@npm:^7.23.3": "@babel/plugin-transform-object-rest-spread@npm:^7.23.4":
version: 7.23.3 version: 7.23.4
resolution: "@babel/plugin-transform-object-rest-spread@npm:7.23.3" resolution: "@babel/plugin-transform-object-rest-spread@npm:7.23.4"
dependencies: dependencies:
"@babel/compat-data": "npm:^7.23.3" "@babel/compat-data": "npm:^7.23.3"
"@babel/helper-compilation-targets": "npm:^7.22.15" "@babel/helper-compilation-targets": "npm:^7.22.15"
@ -1015,7 +1015,7 @@ __metadata:
"@babel/plugin-transform-parameters": "npm:^7.23.3" "@babel/plugin-transform-parameters": "npm:^7.23.3"
peerDependencies: peerDependencies:
"@babel/core": ^7.0.0-0 "@babel/core": ^7.0.0-0
checksum: 31ab631aaba945c118662943e5f1f54a21f07d64f06e06b25d55871168c460f3eeeccdf7b05aa74a1340e2cfbe781ad3c7ceccd0c2585d39f7b73ba11ebaa9d0 checksum: b56017992ffe7fcd1dd9a9da67c39995a141820316266bcf7d77dc912980d228ccbd3f36191d234f5cc389b09157b5d2a955e33e8fb368319534affd1c72b262
languageName: node languageName: node
linkType: hard linkType: hard
@ -1031,28 +1031,28 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/plugin-transform-optional-catch-binding@npm:^7.23.3": "@babel/plugin-transform-optional-catch-binding@npm:^7.23.4":
version: 7.23.3 version: 7.23.4
resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.23.3" resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.23.4"
dependencies: dependencies:
"@babel/helper-plugin-utils": "npm:^7.22.5" "@babel/helper-plugin-utils": "npm:^7.22.5"
"@babel/plugin-syntax-optional-catch-binding": "npm:^7.8.3" "@babel/plugin-syntax-optional-catch-binding": "npm:^7.8.3"
peerDependencies: peerDependencies:
"@babel/core": ^7.0.0-0 "@babel/core": ^7.0.0-0
checksum: 85ac1e94ee8f21648816151628ff931cc16143ec8c904649a1ecfd8960160290eccc5a197b4ae3ee7a1c7a27a7c4189e61b4de24483d5bad4040784afe2d206f checksum: 4ef61812af0e4928485e28301226ce61139a8b8cea9e9a919215ebec4891b9fea2eb7a83dc3090e2679b7d7b2c8653da601fbc297d2addc54a908b315173991e
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/plugin-transform-optional-chaining@npm:^7.23.3": "@babel/plugin-transform-optional-chaining@npm:^7.23.3, @babel/plugin-transform-optional-chaining@npm:^7.23.4":
version: 7.23.3 version: 7.23.4
resolution: "@babel/plugin-transform-optional-chaining@npm:7.23.3" resolution: "@babel/plugin-transform-optional-chaining@npm:7.23.4"
dependencies: dependencies:
"@babel/helper-plugin-utils": "npm:^7.22.5" "@babel/helper-plugin-utils": "npm:^7.22.5"
"@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5" "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5"
"@babel/plugin-syntax-optional-chaining": "npm:^7.8.3" "@babel/plugin-syntax-optional-chaining": "npm:^7.8.3"
peerDependencies: peerDependencies:
"@babel/core": ^7.0.0-0 "@babel/core": ^7.0.0-0
checksum: 2b358962169d871392aa292a67527e5335909438da0ddbb0d19e7838c0f8a2081cc751a49e6e534ac4d6c932254531a205ac22b197f64fc4c89f41bf9f595497 checksum: 305b773c29ad61255b0e83ec1e92b2f7af6aa58be4cba1e3852bddaa14f7d2afd7b4438f41c28b179d6faac7eb8d4fb5530a17920294f25d459b8f84406bfbfb
languageName: node languageName: node
linkType: hard linkType: hard
@ -1079,9 +1079,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/plugin-transform-private-property-in-object@npm:^7.23.3": "@babel/plugin-transform-private-property-in-object@npm:^7.23.4":
version: 7.23.3 version: 7.23.4
resolution: "@babel/plugin-transform-private-property-in-object@npm:7.23.3" resolution: "@babel/plugin-transform-private-property-in-object@npm:7.23.4"
dependencies: dependencies:
"@babel/helper-annotate-as-pure": "npm:^7.22.5" "@babel/helper-annotate-as-pure": "npm:^7.22.5"
"@babel/helper-create-class-features-plugin": "npm:^7.22.15" "@babel/helper-create-class-features-plugin": "npm:^7.22.15"
@ -1089,7 +1089,7 @@ __metadata:
"@babel/plugin-syntax-private-property-in-object": "npm:^7.14.5" "@babel/plugin-syntax-private-property-in-object": "npm:^7.14.5"
peerDependencies: peerDependencies:
"@babel/core": ^7.0.0-0 "@babel/core": ^7.0.0-0
checksum: 9211dd25a6e87a01535f2d97a663fa6de3472b963c8dcfaacce229a2e3fa6500f2e9fc690bc100a540fc7b66c8364faf7ef19b32e9c9b9791e4561b742c15ed3 checksum: 8d31b28f24204b4d13514cd3a8f3033abf575b1a6039759ddd6e1d82dd33ba7281f9bc85c9f38072a665d69bfa26dc40737eefaf9d397b024654a483d2357bf5
languageName: node languageName: node
linkType: hard linkType: hard
@ -1333,13 +1333,13 @@ __metadata:
linkType: hard linkType: hard
"@babel/preset-env@npm:^7.11.0, @babel/preset-env@npm:^7.12.1, @babel/preset-env@npm:^7.22.4": "@babel/preset-env@npm:^7.11.0, @babel/preset-env@npm:^7.12.1, @babel/preset-env@npm:^7.22.4":
version: 7.23.3 version: 7.23.5
resolution: "@babel/preset-env@npm:7.23.3" resolution: "@babel/preset-env@npm:7.23.5"
dependencies: dependencies:
"@babel/compat-data": "npm:^7.23.3" "@babel/compat-data": "npm:^7.23.5"
"@babel/helper-compilation-targets": "npm:^7.22.15" "@babel/helper-compilation-targets": "npm:^7.22.15"
"@babel/helper-plugin-utils": "npm:^7.22.5" "@babel/helper-plugin-utils": "npm:^7.22.5"
"@babel/helper-validator-option": "npm:^7.22.15" "@babel/helper-validator-option": "npm:^7.23.5"
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "npm:^7.23.3" "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "npm:^7.23.3"
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "npm:^7.23.3" "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "npm:^7.23.3"
"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "npm:^7.23.3" "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "npm:^7.23.3"
@ -1363,25 +1363,25 @@ __metadata:
"@babel/plugin-syntax-top-level-await": "npm:^7.14.5" "@babel/plugin-syntax-top-level-await": "npm:^7.14.5"
"@babel/plugin-syntax-unicode-sets-regex": "npm:^7.18.6" "@babel/plugin-syntax-unicode-sets-regex": "npm:^7.18.6"
"@babel/plugin-transform-arrow-functions": "npm:^7.23.3" "@babel/plugin-transform-arrow-functions": "npm:^7.23.3"
"@babel/plugin-transform-async-generator-functions": "npm:^7.23.3" "@babel/plugin-transform-async-generator-functions": "npm:^7.23.4"
"@babel/plugin-transform-async-to-generator": "npm:^7.23.3" "@babel/plugin-transform-async-to-generator": "npm:^7.23.3"
"@babel/plugin-transform-block-scoped-functions": "npm:^7.23.3" "@babel/plugin-transform-block-scoped-functions": "npm:^7.23.3"
"@babel/plugin-transform-block-scoping": "npm:^7.23.3" "@babel/plugin-transform-block-scoping": "npm:^7.23.4"
"@babel/plugin-transform-class-properties": "npm:^7.23.3" "@babel/plugin-transform-class-properties": "npm:^7.23.3"
"@babel/plugin-transform-class-static-block": "npm:^7.23.3" "@babel/plugin-transform-class-static-block": "npm:^7.23.4"
"@babel/plugin-transform-classes": "npm:^7.23.3" "@babel/plugin-transform-classes": "npm:^7.23.5"
"@babel/plugin-transform-computed-properties": "npm:^7.23.3" "@babel/plugin-transform-computed-properties": "npm:^7.23.3"
"@babel/plugin-transform-destructuring": "npm:^7.23.3" "@babel/plugin-transform-destructuring": "npm:^7.23.3"
"@babel/plugin-transform-dotall-regex": "npm:^7.23.3" "@babel/plugin-transform-dotall-regex": "npm:^7.23.3"
"@babel/plugin-transform-duplicate-keys": "npm:^7.23.3" "@babel/plugin-transform-duplicate-keys": "npm:^7.23.3"
"@babel/plugin-transform-dynamic-import": "npm:^7.23.3" "@babel/plugin-transform-dynamic-import": "npm:^7.23.4"
"@babel/plugin-transform-exponentiation-operator": "npm:^7.23.3" "@babel/plugin-transform-exponentiation-operator": "npm:^7.23.3"
"@babel/plugin-transform-export-namespace-from": "npm:^7.23.3" "@babel/plugin-transform-export-namespace-from": "npm:^7.23.4"
"@babel/plugin-transform-for-of": "npm:^7.23.3" "@babel/plugin-transform-for-of": "npm:^7.23.3"
"@babel/plugin-transform-function-name": "npm:^7.23.3" "@babel/plugin-transform-function-name": "npm:^7.23.3"
"@babel/plugin-transform-json-strings": "npm:^7.23.3" "@babel/plugin-transform-json-strings": "npm:^7.23.4"
"@babel/plugin-transform-literals": "npm:^7.23.3" "@babel/plugin-transform-literals": "npm:^7.23.3"
"@babel/plugin-transform-logical-assignment-operators": "npm:^7.23.3" "@babel/plugin-transform-logical-assignment-operators": "npm:^7.23.4"
"@babel/plugin-transform-member-expression-literals": "npm:^7.23.3" "@babel/plugin-transform-member-expression-literals": "npm:^7.23.3"
"@babel/plugin-transform-modules-amd": "npm:^7.23.3" "@babel/plugin-transform-modules-amd": "npm:^7.23.3"
"@babel/plugin-transform-modules-commonjs": "npm:^7.23.3" "@babel/plugin-transform-modules-commonjs": "npm:^7.23.3"
@ -1389,15 +1389,15 @@ __metadata:
"@babel/plugin-transform-modules-umd": "npm:^7.23.3" "@babel/plugin-transform-modules-umd": "npm:^7.23.3"
"@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.22.5" "@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.22.5"
"@babel/plugin-transform-new-target": "npm:^7.23.3" "@babel/plugin-transform-new-target": "npm:^7.23.3"
"@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.23.3" "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.23.4"
"@babel/plugin-transform-numeric-separator": "npm:^7.23.3" "@babel/plugin-transform-numeric-separator": "npm:^7.23.4"
"@babel/plugin-transform-object-rest-spread": "npm:^7.23.3" "@babel/plugin-transform-object-rest-spread": "npm:^7.23.4"
"@babel/plugin-transform-object-super": "npm:^7.23.3" "@babel/plugin-transform-object-super": "npm:^7.23.3"
"@babel/plugin-transform-optional-catch-binding": "npm:^7.23.3" "@babel/plugin-transform-optional-catch-binding": "npm:^7.23.4"
"@babel/plugin-transform-optional-chaining": "npm:^7.23.3" "@babel/plugin-transform-optional-chaining": "npm:^7.23.4"
"@babel/plugin-transform-parameters": "npm:^7.23.3" "@babel/plugin-transform-parameters": "npm:^7.23.3"
"@babel/plugin-transform-private-methods": "npm:^7.23.3" "@babel/plugin-transform-private-methods": "npm:^7.23.3"
"@babel/plugin-transform-private-property-in-object": "npm:^7.23.3" "@babel/plugin-transform-private-property-in-object": "npm:^7.23.4"
"@babel/plugin-transform-property-literals": "npm:^7.23.3" "@babel/plugin-transform-property-literals": "npm:^7.23.3"
"@babel/plugin-transform-regenerator": "npm:^7.23.3" "@babel/plugin-transform-regenerator": "npm:^7.23.3"
"@babel/plugin-transform-reserved-words": "npm:^7.23.3" "@babel/plugin-transform-reserved-words": "npm:^7.23.3"
@ -1418,7 +1418,7 @@ __metadata:
semver: "npm:^6.3.1" semver: "npm:^6.3.1"
peerDependencies: peerDependencies:
"@babel/core": ^7.0.0-0 "@babel/core": ^7.0.0-0
checksum: 36b02a86817ab5474bb74a8d62a110723b0b05904a52ddc5627cf89457525b8d5ac0739b8e435a6ae12ef8b90cd5fc191169898c3dc2ac9d2c84026b02f2580a checksum: 2a0e1274dec045186e131c6433659b75492583290e8d41633c616f6bff829cb2e4b2f9a57f556283a54db3bd6aa697911e56a36f607911a29b731c445a5b5a06
languageName: node languageName: node
linkType: hard linkType: hard
@ -1483,11 +1483,11 @@ __metadata:
linkType: hard linkType: hard
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.22.3, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": "@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.22.3, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
version: 7.23.4 version: 7.23.5
resolution: "@babel/runtime@npm:7.23.4" resolution: "@babel/runtime@npm:7.23.5"
dependencies: dependencies:
regenerator-runtime: "npm:^0.14.0" regenerator-runtime: "npm:^0.14.0"
checksum: db2bf183cd0119599b903ca51ca0aeea8e0ab478a16be1aae10dd90473ed614159d3e5adfdd8f8f3d840402428ce0d90b5c01aae95da9e45a2dd83e02d85ca27 checksum: ca679cc91bb7e424bc2db87bb58cc3b06ade916b9adb21fbbdc43e54cdaacb3eea201ceba2a0464b11d2eb65b9fe6a6ffcf4d7521fa52994f19be96f1af14788
languageName: node languageName: node
linkType: hard linkType: hard
@ -1502,32 +1502,32 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/traverse@npm:7, @babel/traverse@npm:^7.23.2, @babel/traverse@npm:^7.23.3": "@babel/traverse@npm:7, @babel/traverse@npm:^7.23.5":
version: 7.23.3 version: 7.23.5
resolution: "@babel/traverse@npm:7.23.3" resolution: "@babel/traverse@npm:7.23.5"
dependencies: dependencies:
"@babel/code-frame": "npm:^7.22.13" "@babel/code-frame": "npm:^7.23.5"
"@babel/generator": "npm:^7.23.3" "@babel/generator": "npm:^7.23.5"
"@babel/helper-environment-visitor": "npm:^7.22.20" "@babel/helper-environment-visitor": "npm:^7.22.20"
"@babel/helper-function-name": "npm:^7.23.0" "@babel/helper-function-name": "npm:^7.23.0"
"@babel/helper-hoist-variables": "npm:^7.22.5" "@babel/helper-hoist-variables": "npm:^7.22.5"
"@babel/helper-split-export-declaration": "npm:^7.22.6" "@babel/helper-split-export-declaration": "npm:^7.22.6"
"@babel/parser": "npm:^7.23.3" "@babel/parser": "npm:^7.23.5"
"@babel/types": "npm:^7.23.3" "@babel/types": "npm:^7.23.5"
debug: "npm:^4.1.0" debug: "npm:^4.1.0"
globals: "npm:^11.1.0" globals: "npm:^11.1.0"
checksum: 3c2784f4765185126d64fd5eebce0413b7aee6d54f779998594a343a7f973a9693a441ba27533df84e7ab7ce22f1239c6837f35e903132a1b25f7fc7a67bc30f checksum: c5ea793080ca6719b0a1612198fd25e361cee1f3c14142d7a518d2a1eeb5c1d21f7eec1b26c20ea6e1ddd8ed12ab50b960ff95ffd25be353b6b46e1b54d6f825
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.0.0-beta.49, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.10, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.3, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": "@babel/types@npm:^7.0.0, @babel/types@npm:^7.0.0-beta.49, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.10, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.5, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3":
version: 7.23.3 version: 7.23.5
resolution: "@babel/types@npm:7.23.3" resolution: "@babel/types@npm:7.23.5"
dependencies: dependencies:
"@babel/helper-string-parser": "npm:^7.22.5" "@babel/helper-string-parser": "npm:^7.23.4"
"@babel/helper-validator-identifier": "npm:^7.22.20" "@babel/helper-validator-identifier": "npm:^7.22.20"
to-fast-properties: "npm:^2.0.0" to-fast-properties: "npm:^2.0.0"
checksum: 371a10dd9c8d8ebf48fc5d9e1b327dafd74453f8ea582dcbddd1cee5ae34e8881b743e783a86c08c04dcd1849b1842455472a911ae8a1c185484fe9b7b5f1595 checksum: 7dd5e2f59828ed046ad0b06b039df2524a8b728d204affb4fc08da2502b9dd3140b1356b5166515d229dc811539a8b70dcd4bc507e06d62a89f4091a38d0b0fb
languageName: node languageName: node
linkType: hard linkType: hard
@ -2317,6 +2317,7 @@ __metadata:
"@types/object-assign": "npm:^4.0.30" "@types/object-assign": "npm:^4.0.30"
"@types/prop-types": "npm:^15.7.5" "@types/prop-types": "npm:^15.7.5"
"@types/punycode": "npm:^2.1.0" "@types/punycode": "npm:^2.1.0"
"@types/rails__ujs": "npm:^6.0.4"
"@types/react": "npm:^18.2.7" "@types/react": "npm:^18.2.7"
"@types/react-dom": "npm:^18.2.4" "@types/react-dom": "npm:^18.2.4"
"@types/react-helmet": "npm:^6.1.6" "@types/react-helmet": "npm:^6.1.6"
@ -2363,6 +2364,7 @@ __metadata:
escape-html: "npm:^1.0.3" escape-html: "npm:^1.0.3"
eslint: "npm:^8.41.0" eslint: "npm:^8.41.0"
eslint-config-prettier: "npm:^9.0.0" eslint-config-prettier: "npm:^9.0.0"
eslint-define-config: "npm:^2.0.0"
eslint-import-resolver-typescript: "npm:^3.5.5" eslint-import-resolver-typescript: "npm:^3.5.5"
eslint-plugin-formatjs: "npm:^4.10.1" eslint-plugin-formatjs: "npm:^4.10.1"
eslint-plugin-import: "npm:~2.29.0" eslint-plugin-import: "npm:~2.29.0"
@ -2472,8 +2474,10 @@ __metadata:
"@types/npmlog": "npm:^7.0.0" "@types/npmlog": "npm:^7.0.0"
"@types/pg": "npm:^8.6.6" "@types/pg": "npm:^8.6.6"
"@types/uuid": "npm:^9.0.0" "@types/uuid": "npm:^9.0.0"
"@types/ws": "npm:^8.5.9"
bufferutil: "npm:^4.0.7" bufferutil: "npm:^4.0.7"
dotenv: "npm:^16.0.3" dotenv: "npm:^16.0.3"
eslint-define-config: "npm:^2.0.0"
express: "npm:^4.18.2" express: "npm:^4.18.2"
ioredis: "npm:^5.3.2" ioredis: "npm:^5.3.2"
jsdom: "npm:^23.0.0" jsdom: "npm:^23.0.0"
@ -2481,6 +2485,7 @@ __metadata:
pg: "npm:^8.5.0" pg: "npm:^8.5.0"
pg-connection-string: "npm:^2.6.0" pg-connection-string: "npm:^2.6.0"
prom-client: "npm:^15.0.0" prom-client: "npm:^15.0.0"
typescript: "npm:^5.0.4"
utf-8-validate: "npm:^6.0.3" utf-8-validate: "npm:^6.0.3"
uuid: "npm:^9.0.0" uuid: "npm:^9.0.0"
ws: "npm:^8.12.1" ws: "npm:^8.12.1"
@ -3348,6 +3353,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/rails__ujs@npm:^6.0.4":
version: 6.0.4
resolution: "@types/rails__ujs@npm:6.0.4"
checksum: 7477cb03a0e1339b9cd5c8ac4a197a153e2ff48742b2f527c5a39dcdf80f01493011e368483290d3717662c63066fada3ab203a335804cbb3573cf575f37007e
languageName: node
linkType: hard
"@types/range-parser@npm:*": "@types/range-parser@npm:*":
version: 1.2.7 version: 1.2.7
resolution: "@types/range-parser@npm:1.2.7" resolution: "@types/range-parser@npm:1.2.7"
@ -3647,6 +3659,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/ws@npm:^8.5.9":
version: 8.5.9
resolution: "@types/ws@npm:8.5.9"
dependencies:
"@types/node": "npm:*"
checksum: 678bdd6461c4653f2975c537fb673cb1918c331558e2d2422b69761c9ced67200bb07c664e2593f3864077a891cb7c13ef2a40d303b4aacb06173d095d8aa3ce
languageName: node
linkType: hard
"@types/yargs-parser@npm:*": "@types/yargs-parser@npm:*":
version: 21.0.2 version: 21.0.2
resolution: "@types/yargs-parser@npm:21.0.2" resolution: "@types/yargs-parser@npm:21.0.2"
@ -7312,6 +7333,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"eslint-define-config@npm:^2.0.0":
version: 2.0.0
resolution: "eslint-define-config@npm:2.0.0"
checksum: 617c3143bc1ed8df0b20ae632d428d5f241dbb04483631e1410c58fe65ba3e503cf94631c5973115482b58ba464d052422a718c0f4d49182f8d13ffbb36bf1d6
languageName: node
linkType: hard
"eslint-import-resolver-node@npm:^0.3.9": "eslint-import-resolver-node@npm:^0.3.9":
version: 0.3.9 version: 0.3.9
resolution: "eslint-import-resolver-node@npm:0.3.9" resolution: "eslint-import-resolver-node@npm:0.3.9"
@ -10636,8 +10664,8 @@ __metadata:
linkType: hard linkType: hard
"jsdom@npm:^23.0.0": "jsdom@npm:^23.0.0":
version: 23.0.0 version: 23.0.1
resolution: "jsdom@npm:23.0.0" resolution: "jsdom@npm:23.0.1"
dependencies: dependencies:
cssstyle: "npm:^3.0.0" cssstyle: "npm:^3.0.0"
data-urls: "npm:^5.0.0" data-urls: "npm:^5.0.0"
@ -10661,11 +10689,11 @@ __metadata:
ws: "npm:^8.14.2" ws: "npm:^8.14.2"
xml-name-validator: "npm:^5.0.0" xml-name-validator: "npm:^5.0.0"
peerDependencies: peerDependencies:
canvas: ^3.0.0 canvas: ^2.11.2
peerDependenciesMeta: peerDependenciesMeta:
canvas: canvas:
optional: true optional: true
checksum: 2c876a02de49e0ed6b667a4eb9b08b8e76ac189a5571ff97791cc9564e713259314deea6d657cc7f59fc30af41b900e7d833c95017e576dfcaf25f32565722af checksum: 13b2b3693ccb40215d1cce77bac7a295414ee4c0a06e30167f8087c9867145ba23dbd592bd95a801cadd7b3698bfd20b9c3f2c26fd8422607f22609ed2e404ef
languageName: node languageName: node
linkType: hard linkType: hard