diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 20f3437f4e..82a2ccbb6c 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -24,4 +24,4 @@ RAILS_ENV=development ./bin/rails db:setup RAILS_ENV=development ./bin/rails assets:precompile # Precompile assets for test -RAILS_ENV=test NODE_ENV=tests ./bin/rails assets:precompile +RAILS_ENV=test ./bin/rails assets:precompile diff --git a/.eslintrc.js b/.eslintrc.js index 1360fb87f0..1150f734d7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,7 @@ -module.exports = { +// @ts-check +const { defineConfig } = require('eslint-define-config'); + +module.exports = defineConfig({ root: true, extends: [ @@ -193,6 +196,7 @@ module.exports = { 'error', { devDependencies: [ + '.eslintrc.js', 'config/webpack/**', 'app/javascript/mastodon/performance.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-invalid-icu': 'error', '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-offset': 'error', 'formatjs/no-useless-message': 'error', @@ -316,6 +319,7 @@ module.exports = { overrides: [ { files: [ + '.eslintrc.js', '*.config.js', '.*rc.js', 'ide-helper.js', @@ -366,7 +370,7 @@ module.exports = { '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'], '@typescript-eslint/consistent-type-exports': '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', @@ -389,14 +393,6 @@ module.exports = { env: { jest: true, }, - }, - { - files: [ - 'streaming/**/*', - ], - rules: { - 'import/no-commonjs': 'off', - }, - }, + } ], -}; +}); diff --git a/.github/workflows/build-container-image.yml b/.github/workflows/build-container-image.yml index 29868c72f8..e100e15821 100644 --- a/.github/workflows/build-container-image.yml +++ b/.github/workflows/build-container-image.yml @@ -21,6 +21,8 @@ on: type: string labels: type: string + file_to_build: + type: string jobs: build-image: @@ -86,6 +88,7 @@ jobs: - uses: docker/build-push-action@v5 with: context: . + file: ${{ inputs.file_to_build }} build-args: | MASTODON_VERSION_PRERELEASE=${{ inputs.version_prerelease }} MASTODON_VERSION_METADATA=${{ inputs.version_metadata }} diff --git a/.github/workflows/build-nightly.yml b/.github/workflows/build-nightly.yml index b7fb638b96..8b8584a034 100644 --- a/.github/workflows/build-nightly.yml +++ b/.github/workflows/build-nightly.yml @@ -25,6 +25,7 @@ jobs: needs: compute-suffix uses: ./.github/workflows/build-container-image.yml with: + file_to_build: Dockerfile platforms: linux/amd64,linux/arm64 use_native_arm64_builder: false cache: false @@ -40,3 +41,25 @@ jobs: type=raw,value=nightly type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }} 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 diff --git a/.github/workflows/build-push-pr.yml b/.github/workflows/build-push-pr.yml index b5f6cbc746..f8df3cc39d 100644 --- a/.github/workflows/build-push-pr.yml +++ b/.github/workflows/build-push-pr.yml @@ -29,6 +29,7 @@ jobs: needs: compute-suffix uses: ./.github/workflows/build-container-image.yml with: + file_to_build: Dockerfile platforms: linux/amd64,linux/arm64 use_native_arm64_builder: false push_to_images: | @@ -39,3 +40,19 @@ jobs: tags: | type=ref,event=pr 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 diff --git a/.github/workflows/build-releases.yml b/.github/workflows/build-releases.yml index 2b7e7aaa42..4ddbba976f 100644 --- a/.github/workflows/build-releases.yml +++ b/.github/workflows/build-releases.yml @@ -12,6 +12,7 @@ jobs: build-image: uses: ./.github/workflows/build-container-image.yml with: + file_to_build: Dockerfile platforms: linux/amd64,linux/arm64 use_native_arm64_builder: false push_to_images: | @@ -26,3 +27,24 @@ jobs: type=pep440,pattern={{raw}} type=pep440,pattern=v{{major}}.{{minor}} 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 diff --git a/.github/workflows/test-image-build.yml b/.github/workflows/test-image-build.yml index 778e341771..980e071897 100644 --- a/.github/workflows/test-image-build.yml +++ b/.github/workflows/test-image-build.yml @@ -7,6 +7,7 @@ on: - .github/workflows/build-releases.yml - .github/workflows/test-image-build.yml - Dockerfile + - streaming/Dockerfile permissions: contents: read @@ -18,4 +19,17 @@ jobs: uses: ./.github/workflows/build-container-image.yml with: + file_to_build: Dockerfile 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 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 7f68cd9b9c..a7dc02b1ad 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -309,7 +309,7 @@ Style/FetchEnvVar: - 'config/initializers/devise.rb' - 'config/initializers/paperclip.rb' - 'config/initializers/vapid.rb' - - 'lib/mastodon/premailer_webpack_strategy.rb' + - 'lib/premailer_webpack_strategy.rb' - 'lib/mastodon/redis_config.rb' - 'lib/tasks/repo.rake' - 'spec/features/profile_spec.rb' @@ -359,8 +359,8 @@ Style/GuardClause: - 'config/initializers/devise.rb' - 'db/migrate/20170901141119_truncate_preview_cards.rb' - 'db/post_migrate/20220704024901_migrate_settings_to_user_roles.rb' - - 'lib/devise/two_factor_ldap_authenticatable.rb' - - 'lib/devise/two_factor_pam_authenticatable.rb' + - 'lib/devise/strategies/two_factor_ldap_authenticatable.rb' + - 'lib/devise/strategies/two_factor_pam_authenticatable.rb' - 'lib/mastodon/cli/accounts.rb' - 'lib/mastodon/cli/maintenance.rb' - 'lib/mastodon/cli/media.rb' @@ -495,8 +495,8 @@ Style/SafeNavigation: # SupportedStyles: only_raise, only_fail, semantic Style/SignalException: Exclude: - - 'lib/devise/two_factor_ldap_authenticatable.rb' - - 'lib/devise/two_factor_pam_authenticatable.rb' + - 'lib/devise/strategies/two_factor_ldap_authenticatable.rb' + - 'lib/devise/strategies/two_factor_pam_authenticatable.rb' # This cop supports unsafe autocorrection (--autocorrect-all). Style/SingleArgumentDig: diff --git a/Dockerfile b/Dockerfile index 7e032073b8..ed5ebf1e0a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,112 +1,257 @@ # 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 -FROM node:${NODE_VERSION} as build +# 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} -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" \ - PATH="${PATH}:/opt/ruby/bin" +# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA +# 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="" + +# 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" -SHELL ["/bin/bash", "-o", "pipefail", "-c"] +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 +# 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 \ +# Apt update & upgrade to check for security updates to Debian image + apt-get update; \ + apt-get dist-upgrade -yq; \ +# Install jemalloc, curl and other necessary components + apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + ffmpeg \ + file \ + imagemagick \ + libjemalloc2 \ + patchelf \ + procps \ + tini \ + tzdata \ + ; \ +# Patch Ruby to use jemalloc + patchelf --add-needed libjemalloc.so.2 /usr/local/bin/ruby; \ +# Discard patchelf after use + apt-get purge -y \ + patchelf \ + ; + +# 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 apt-get update && \ - apt-get -yq dist-upgrade && \ - apt-get install -y --no-install-recommends build-essential \ - git \ - libicu-dev \ - libidn-dev \ - libpq-dev \ - libjemalloc-dev \ - zlib1g-dev \ - libgdbm-dev \ - libgmp-dev \ - libssl-dev \ - libyaml-dev \ - ca-certificates \ - libreadline8 \ - python3 \ - shared-mime-info && \ - bundle config set --local deployment 'true' && \ - bundle config set --local without 'development test' && \ - bundle config set silence_root_warning true && \ - corepack enable - -COPY Gemfile* package.json yarn.lock .yarnrc.yml /opt/mastodon/ +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 .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 && \ - yarn cache clean +# Create temporary assets build layer from build layer +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 -ARG MASTODON_VERSION_PRERELEASE="" -ARG MASTODON_VERSION_METADATA="" +# Copy bundler and node packages from build layer to container +COPY --from=yarn /opt/mastodon /opt/mastodon/ +COPY --from=bundler /opt/mastodon /opt/mastodon/ +COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/ -ARG UID="991" -ARG GID="991" +ARG TARGETPLATFORM -COPY --link --from=ruby /opt/ruby /opt/ruby - -SHELL ["/bin/bash", "-o", "pipefail", "-c"] - -ENV DEBIAN_FRONTEND="noninteractive" \ - 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,DL3009 -RUN apt-get update && \ - echo "Etc/UTC" > /etc/localtime && \ - groupadd -g "${GID}" mastodon && \ - useradd -l -u "$UID" -g "${GID}" -m -d /opt/mastodon mastodon && \ - apt-get -y --no-install-recommends install whois \ - wget \ - procps \ - libssl3 \ - libpq5 \ - imagemagick \ - ffmpeg \ - libjemalloc2 \ - libicu72 \ - libidn12 \ - 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 -# See the file /etc/apt/apt.conf.d/docker-clean within the Docker image's filesystem - -COPY --chown=mastodon:mastodon . /opt/mastodon -COPY --chown=mastodon:mastodon --from=build /opt/mastodon /opt/mastodon - -ENV RAILS_ENV="production" \ - NODE_ENV="production" \ - RAILS_SERVE_STATIC_FILES="true" \ - BIND="0.0.0.0" \ - MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \ - MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}" - -# Set the run user -USER mastodon -WORKDIR /opt/mastodon +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; + +# Prep final Mastodon Ruby layer +FROM ruby as mastodon + +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 \ +# 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 \ +# Apt update install non-dev versions of necessary components + apt-get install -y --no-install-recommends \ + libssl3 \ + libpq5 \ + libicu72 \ + libidn12 \ + libreadline8 \ + libyaml-0-2 \ + ; -# Precompile assets -RUN OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder rails assets:precompile +# Copy Mastodon sources into final layer +COPY . /opt/mastodon/ -# Set the work dir and the container entry point -ENTRYPOINT ["/usr/bin/tini", "--"] -EXPOSE 3000 4000 +# Copy compiled assets to layer +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/ + +RUN \ +# Precompile bootsnap code for faster Rails startup + bundle exec bootsnap precompile --gemfile app/ lib/; + +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 +# Expose default Puma ports +EXPOSE 3000 +# Set container tini as default entry point +ENTRYPOINT ["/usr/bin/tini", "--"] \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 72b60e05b1..4e8c607ade 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -156,7 +156,7 @@ GEM nokogiri (~> 1, >= 1.10.8) base64 (0.2.0) bcp47_spec (0.2.1) - bcrypt (3.1.19) + bcrypt (3.1.20) better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) @@ -522,7 +522,7 @@ GEM pastel (0.8.0) tty-color (~> 0.5) pg (1.5.4) - pghero (3.3.4) + pghero (3.4.0) activerecord (>= 6) posix-spawn (0.3.15) premailer (1.21.0) @@ -755,7 +755,7 @@ GEM attr_required (>= 0.0.5) httpclient (>= 2.4) sysexits (1.2.0) - temple (0.10.2) + temple (0.10.3) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) terrapin (0.6.0) diff --git a/Vagrantfile b/Vagrantfile index e2c66a476e..6f0f511095 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -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' # 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 sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{ENV["PORT"]} diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index f3943bbb6b..567edb33b1 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -50,7 +50,7 @@ class AccountsController < ApplicationController end 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 def no_replies_scope diff --git a/app/controllers/admin/account_moderation_notes_controller.rb b/app/controllers/admin/account_moderation_notes_controller.rb index 4f36f33f47..8b6c1a4454 100644 --- a/app/controllers/admin/account_moderation_notes_controller.rb +++ b/app/controllers/admin/account_moderation_notes_controller.rb @@ -16,7 +16,7 @@ module Admin @moderation_notes = @account.targeted_moderation_notes.latest @warnings = @account.strikes.custom.latest - render template: 'admin/accounts/show' + render 'admin/accounts/show' end end diff --git a/app/controllers/admin/action_logs_controller.rb b/app/controllers/admin/action_logs_controller.rb index 42edec15a3..37a00ad225 100644 --- a/app/controllers/admin/action_logs_controller.rb +++ b/app/controllers/admin/action_logs_controller.rb @@ -6,7 +6,7 @@ module Admin def 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 private diff --git a/app/controllers/admin/relays_controller.rb b/app/controllers/admin/relays_controller.rb index c1297c8b99..c893802159 100644 --- a/app/controllers/admin/relays_controller.rb +++ b/app/controllers/admin/relays_controller.rb @@ -24,7 +24,7 @@ module Admin @relay.enable! redirect_to admin_relays_path else - render action: :new + render :new end end diff --git a/app/controllers/admin/report_notes_controller.rb b/app/controllers/admin/report_notes_controller.rb index 3fd815b60a..b5f04a1caa 100644 --- a/app/controllers/admin/report_notes_controller.rb +++ b/app/controllers/admin/report_notes_controller.rb @@ -26,7 +26,7 @@ module Admin @form = Admin::StatusBatchAction.new @statuses = @report.statuses.with_includes - render template: 'admin/reports/show' + render 'admin/reports/show' end end diff --git a/app/controllers/concerns/challengable_concern.rb b/app/controllers/concerns/challengable_concern.rb index 2995a25e09..09874fb405 100644 --- a/app/controllers/concerns/challengable_concern.rb +++ b/app/controllers/concerns/challengable_concern.rb @@ -43,7 +43,7 @@ module ChallengableConcern def render_challenge @body_classes = 'lighter' - render template: 'auth/challenges/new', layout: 'auth' + render 'auth/challenges/new', layout: 'auth' end def challenge_passed? diff --git a/app/controllers/disputes/appeals_controller.rb b/app/controllers/disputes/appeals_controller.rb index eefd92b5a8..98b58d2117 100644 --- a/app/controllers/disputes/appeals_controller.rb +++ b/app/controllers/disputes/appeals_controller.rb @@ -11,7 +11,7 @@ class Disputes::AppealsController < Disputes::BaseController redirect_to disputes_strike_path(@strike), notice: I18n.t('disputes.strikes.appealed_msg') rescue ActiveRecord::RecordInvalid => e @appeal = e.record - render template: 'disputes/strikes/show' + render 'disputes/strikes/show' end private diff --git a/app/controllers/filters_controller.rb b/app/controllers/filters_controller.rb index 180ddf070b..1b19269b23 100644 --- a/app/controllers/filters_controller.rb +++ b/app/controllers/filters_controller.rb @@ -26,7 +26,7 @@ class FiltersController < ApplicationController if @filter.save redirect_to filters_path else - render action: :new + render :new end end @@ -34,7 +34,7 @@ class FiltersController < ApplicationController if @filter.update(resource_params) redirect_to filters_path else - render action: :edit + render :edit end end diff --git a/app/controllers/statuses_cleanup_controller.rb b/app/controllers/statuses_cleanup_controller.rb index 3ed1860a00..88cf26d74d 100644 --- a/app/controllers/statuses_cleanup_controller.rb +++ b/app/controllers/statuses_cleanup_controller.rb @@ -15,7 +15,7 @@ class StatusesCleanupController < ApplicationController if @policy.update(resource_params) redirect_to statuses_cleanup_path, notice: I18n.t('generic.changes_saved_msg') else - render action: :show + render :show end rescue ActionController::ParameterMissing # Do nothing diff --git a/app/helpers/admin/account_actions_helper.rb b/app/helpers/admin/account_actions_helper.rb new file mode 100644 index 0000000000..e132680a68 --- /dev/null +++ b/app/helpers/admin/account_actions_helper.rb @@ -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 diff --git a/app/helpers/admin/accounts_helper.rb b/app/helpers/admin/accounts_helper.rb new file mode 100644 index 0000000000..a936797e88 --- /dev/null +++ b/app/helpers/admin/accounts_helper.rb @@ -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 diff --git a/app/helpers/admin/ip_blocks_helper.rb b/app/helpers/admin/ip_blocks_helper.rb new file mode 100644 index 0000000000..4aae3aae7a --- /dev/null +++ b/app/helpers/admin/ip_blocks_helper.rb @@ -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 diff --git a/app/helpers/admin/roles_helper.rb b/app/helpers/admin/roles_helper.rb new file mode 100644 index 0000000000..7b4702e268 --- /dev/null +++ b/app/helpers/admin/roles_helper.rb @@ -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 diff --git a/app/helpers/admin/settings/discovery_helper.rb b/app/helpers/admin/settings/discovery_helper.rb new file mode 100644 index 0000000000..0aa4d4368f --- /dev/null +++ b/app/helpers/admin/settings/discovery_helper.rb @@ -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 diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb new file mode 100644 index 0000000000..22a1c172de --- /dev/null +++ b/app/helpers/filters_helper.rb @@ -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 diff --git a/app/javascript/mastodon/components/admin/Trends.jsx b/app/javascript/mastodon/components/admin/Trends.jsx index 49976276ee..c69b4a8cba 100644 --- a/app/javascript/mastodon/components/admin/Trends.jsx +++ b/app/javascript/mastodon/components/admin/Trends.jsx @@ -6,7 +6,7 @@ import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import api from 'mastodon/api'; -import Hashtag from 'mastodon/components/hashtag'; +import { Hashtag } from 'mastodon/components/hashtag'; export default class Trends extends PureComponent { diff --git a/app/javascript/mastodon/components/dismissable_banner.tsx b/app/javascript/mastodon/components/dismissable_banner.tsx index 4feb74a3a1..4e6d3bb9a7 100644 --- a/app/javascript/mastodon/components/dismissable_banner.tsx +++ b/app/javascript/mastodon/components/dismissable_banner.tsx @@ -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 { useCallback, useState } from 'react'; +import { useCallback, useState, useEffect } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { ReactComponent as CloseIcon } from '@material-symbols/svg-600/outlined/close.svg'; +import { changeSetting } from 'mastodon/actions/settings'; import { bannerSettings } from 'mastodon/settings'; +import { useAppSelector, useAppDispatch } from 'mastodon/store'; import { IconButton } from './icon_button'; @@ -21,13 +28,25 @@ export const DismissableBanner: React.FC> = ({ id, 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 handleDismiss = useCallback(() => { setVisible(false); 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) { return null; diff --git a/app/javascript/mastodon/components/hashtag.jsx b/app/javascript/mastodon/components/hashtag.jsx deleted file mode 100644 index 14bb4ddc64..0000000000 --- a/app/javascript/mastodon/components/hashtag.jsx +++ /dev/null @@ -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) => ( - {displayNumber}, - days: 2, - }} - /> -); - -// @ts-expect-error -export const ImmutableHashtag = ({ hashtag }) => ( - day.get('uses')).toArray()} - /> -); - -ImmutableHashtag.propTypes = { - hashtag: ImmutablePropTypes.map.isRequired, -}; - -// @ts-expect-error -const Hashtag = ({ name, to, people, uses, history, className, description, withGraph }) => ( -
-
- - {name ? <>#{name} : } - - - {description ? ( - {description} - ) : ( - typeof people !== 'undefined' ? : - )} -
- - {typeof uses !== 'undefined' && ( -
- -
- )} - - {withGraph && ( -
- - 0)}> - - - -
- )} -
-); - -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; diff --git a/app/javascript/mastodon/components/hashtag.tsx b/app/javascript/mastodon/components/hashtag.tsx new file mode 100644 index 0000000000..8963e4a40d --- /dev/null +++ b/app/javascript/mastodon/components/hashtag.tsx @@ -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 { + 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, +) => ( + {displayNumber}, + days: 2, + }} + /> +); + +interface ImmutableHashtagProps { + hashtag: Immutable.Map; +} + +export const ImmutableHashtag = ({ hashtag }: ImmutableHashtagProps) => ( + + > + ) + .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 = ({ + name, + to, + people, + uses, + history, + className, + description, + withGraph = true, +}) => ( +
+
+ + {name ? ( + <> + #{name} + + ) : ( + + )} + + + {description ? ( + {description} + ) : typeof people !== 'undefined' ? ( + + ) : ( + + )} +
+ + {typeof uses !== 'undefined' && ( +
+ +
+ )} + + {withGraph && ( +
+ + 0)} + > + + + +
+ )} +
+); diff --git a/app/javascript/mastodon/features/account/components/featured_tags.jsx b/app/javascript/mastodon/features/account/components/featured_tags.jsx index 4d7dd86560..56a9efac02 100644 --- a/app/javascript/mastodon/features/account/components/featured_tags.jsx +++ b/app/javascript/mastodon/features/account/components/featured_tags.jsx @@ -5,7 +5,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import Hashtag from 'mastodon/components/hashtag'; +import { Hashtag } from 'mastodon/components/hashtag'; const messages = defineMessages({ lastStatusAt: { id: 'account.featured_tags.last_status_at', defaultMessage: 'Last post on {date}' }, diff --git a/app/javascript/mastodon/features/followed_tags/index.jsx b/app/javascript/mastodon/features/followed_tags/index.jsx index 7042f2438a..dec53f0121 100644 --- a/app/javascript/mastodon/features/followed_tags/index.jsx +++ b/app/javascript/mastodon/features/followed_tags/index.jsx @@ -13,7 +13,7 @@ import { debounce } from 'lodash'; import { expandFollowedHashtags, fetchFollowedHashtags } from 'mastodon/actions/tags'; 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 Column from 'mastodon/features/ui/components/column'; diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index 07d1bda0f4..a605ecbb8b 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -100,6 +100,15 @@ const initialState = ImmutableMap({ 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([ diff --git a/app/javascript/mastodon/utils/__tests__/base64-test.js b/app/javascript/mastodon/utils/__tests__/base64-test.ts similarity index 100% rename from app/javascript/mastodon/utils/__tests__/base64-test.js rename to app/javascript/mastodon/utils/__tests__/base64-test.ts diff --git a/app/javascript/mastodon/utils/__tests__/html-test.js b/app/javascript/mastodon/utils/__tests__/html-test.s similarity index 100% rename from app/javascript/mastodon/utils/__tests__/html-test.js rename to app/javascript/mastodon/utils/__tests__/html-test.s diff --git a/app/javascript/mastodon/utils/config.js b/app/javascript/mastodon/utils/config.js deleted file mode 100644 index 932cd0cbf5..0000000000 --- a/app/javascript/mastodon/utils/config.js +++ /dev/null @@ -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 || ''; - } -}); diff --git a/app/javascript/mastodon/utils/config.ts b/app/javascript/mastodon/utils/config.ts new file mode 100644 index 0000000000..9222c89d1b --- /dev/null +++ b/app/javascript/mastodon/utils/config.ts @@ -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( + 'meta[name=cdn-host]', + ); + if (cdnHost) { + assetHost = cdnHost.content || ''; + } +}); diff --git a/app/javascript/mastodon/utils/html.js b/app/javascript/mastodon/utils/html.js deleted file mode 100644 index 247e98c88a..0000000000 --- a/app/javascript/mastodon/utils/html.js +++ /dev/null @@ -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(//g, '\n').replace(/<\/p>

/g, '\n\n').replace(/<[^>]*>/g, ''); - return wrapper.textContent; -}; diff --git a/app/javascript/mastodon/utils/html.ts b/app/javascript/mastodon/utils/html.ts new file mode 100644 index 0000000000..0145a04551 --- /dev/null +++ b/app/javascript/mastodon/utils/html.ts @@ -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(//g, '\n') + .replace(/<\/p>

/g, '\n\n') + .replace(/<[^>]*>/g, ''); + return wrapper.textContent; +}; diff --git a/app/javascript/mastodon/utils/icons.jsx b/app/javascript/mastodon/utils/icons.tsx similarity index 69% rename from app/javascript/mastodon/utils/icons.jsx rename to app/javascript/mastodon/utils/icons.tsx index be566032e0..6e432e32fa 100644 --- a/app/javascript/mastodon/utils/icons.jsx +++ b/app/javascript/mastodon/utils/icons.tsx @@ -1,13 +1,23 @@ // Copied from emoji-mart for consistency with emoji picker and since // they don't export the icons in the package export const loupeIcon = ( - + ); export const deleteIcon = ( - + ); diff --git a/app/javascript/mastodon/utils/log_out.js b/app/javascript/mastodon/utils/log_out.ts similarity index 100% rename from app/javascript/mastodon/utils/log_out.js rename to app/javascript/mastodon/utils/log_out.ts diff --git a/app/javascript/mastodon/utils/notifications.js b/app/javascript/mastodon/utils/notifications.js deleted file mode 100644 index 42623ac7c6..0000000000 --- a/app/javascript/mastodon/utils/notifications.js +++ /dev/null @@ -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)); - } -}; diff --git a/app/javascript/mastodon/utils/notifications.ts b/app/javascript/mastodon/utils/notifications.ts new file mode 100644 index 0000000000..08f677f8fe --- /dev/null +++ b/app/javascript/mastodon/utils/notifications.ts @@ -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); + } +}; diff --git a/app/javascript/mastodon/utils/react_router.jsx b/app/javascript/mastodon/utils/react_router.tsx similarity index 53% rename from app/javascript/mastodon/utils/react_router.jsx rename to app/javascript/mastodon/utils/react_router.tsx index fa8f0db2b5..0682fee554 100644 --- a/app/javascript/mastodon/utils/react_router.jsx +++ b/app/javascript/mastodon/utils/react_router.tsx @@ -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 = { match: PropTypes.object.isRequired, @@ -16,31 +16,37 @@ export const WithOptionalRouterPropTypes = { 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 // but does not fail if called outside of a React Router context -export function withOptionalRouter(Component) { - const displayName = `withRouter(${Component.displayName || Component.name})`; - const C = props => { +export function withOptionalRouter< + ComponentType extends React.ComponentType, +>(Component: ComponentType) { + const displayName = `withRouter(${Component.displayName ?? Component.name})`; + const C = (props: React.ComponentProps) => { const { wrappedComponentRef, ...remainingProps } = props; return ( <__RouterContext.Consumer> - {context => { - if(context) + {(context) => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (context) { return ( + // @ts-expect-error - Dynamic covariant generic components are tough to type. ); - else - return ( - - ); + } else { + // @ts-expect-error - Dynamic covariant generic components are tough to type. + return ; + } }} ); @@ -53,8 +59,8 @@ export function withOptionalRouter(Component) { wrappedComponentRef: PropTypes.oneOfType([ PropTypes.string, PropTypes.func, - PropTypes.object - ]) + PropTypes.object, + ]), }; return hoistStatics(C, Component); diff --git a/app/javascript/mastodon/utils/scrollbar.js b/app/javascript/mastodon/utils/scrollbar.ts similarity index 70% rename from app/javascript/mastodon/utils/scrollbar.js rename to app/javascript/mastodon/utils/scrollbar.ts index ca87dd76f5..d505df1244 100644 --- a/app/javascript/mastodon/utils/scrollbar.js +++ b/app/javascript/mastodon/utils/scrollbar.ts @@ -1,11 +1,7 @@ import { isMobile } from '../is_mobile'; -/** @type {number | null} */ -let cachedScrollbarWidth = null; +let cachedScrollbarWidth: number | null = null; -/** - * @returns {number} - */ const getActualScrollbarWidth = () => { const outer = document.createElement('div'); outer.style.visibility = 'hidden'; @@ -16,20 +12,19 @@ const getActualScrollbarWidth = () => { outer.appendChild(inner); const scrollbarWidth = outer.offsetWidth - inner.offsetWidth; - outer.parentNode.removeChild(outer); + outer.remove(); return scrollbarWidth; }; -/** - * @returns {number} - */ export const getScrollbarWidth = () => { if (cachedScrollbarWidth !== null) { return cachedScrollbarWidth; } - const scrollbarWidth = isMobile(window.innerWidth) ? 0 : getActualScrollbarWidth(); + const scrollbarWidth = isMobile(window.innerWidth) + ? 0 + : getActualScrollbarWidth(); cachedScrollbarWidth = scrollbarWidth; return scrollbarWidth; diff --git a/app/lib/account_statuses_filter.rb b/app/lib/account_statuses_filter.rb index 4438529044..88d459f322 100644 --- a/app/lib/account_statuses_filter.rb +++ b/app/lib/account_statuses_filter.rb @@ -70,7 +70,7 @@ class AccountStatusesFilter end 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 def no_replies_scope diff --git a/app/lib/content_security_policy.rb b/app/lib/content_security_policy.rb index e8fcf76a65..966e41f03b 100644 --- a/app/lib/content_security_policy.rb +++ b/app/lib/content_security_policy.rb @@ -9,8 +9,8 @@ class ContentSecurityPolicy url_from_configured_asset_host || url_from_base_host end - def media_host - cdn_host_value || assets_host + def media_hosts + [assets_host, cdn_host_value].compact end private diff --git a/app/lib/vacuum/media_attachments_vacuum.rb b/app/lib/vacuum/media_attachments_vacuum.rb index 7b21c84bbc..ab7ea4092f 100644 --- a/app/lib/vacuum/media_attachments_vacuum.rb +++ b/app/lib/vacuum/media_attachments_vacuum.rb @@ -27,11 +27,11 @@ class Vacuum::MediaAttachmentsVacuum end 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 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 def retention_period? diff --git a/app/models/admin/action_log.rb b/app/models/admin/action_log.rb index f2c121d758..49ae679809 100644 --- a/app/models/admin/action_log.rb +++ b/app/models/admin/action_log.rb @@ -24,12 +24,12 @@ class Admin::ActionLog < ApplicationRecord belongs_to :account belongs_to :target, polymorphic: true, optional: true - default_scope -> { order('id desc') } - before_validation :set_human_identifier before_validation :set_route_param before_validation :set_permalink + scope :latest, -> { order(id: :desc) } + def action super.to_sym end diff --git a/app/models/admin/action_log_filter.rb b/app/models/admin/action_log_filter.rb index 0117974628..d413cb386d 100644 --- a/app/models/admin/action_log_filter.rb +++ b/app/models/admin/action_log_filter.rb @@ -72,7 +72,7 @@ class Admin::ActionLogFilter end def results - scope = Admin::ActionLog.includes(:target) + scope = latest_action_logs.includes(:target) params.each do |key, value| next if key.to_s == 'page' @@ -88,14 +88,18 @@ class Admin::ActionLogFilter def scope_for(key, value) case key 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' - Admin::ActionLog.where(account_id: value) + latest_action_logs.where(account_id: value) when 'target_account_id' 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 raise Mastodon::InvalidParameterError, "Unknown filter: #{key}" end end + + def latest_action_logs + Admin::ActionLog.latest + end end diff --git a/app/models/admin/status_filter.rb b/app/models/admin/status_filter.rb index 645c2e6207..4708785e7d 100644 --- a/app/models/admin/status_filter.rb +++ b/app/models/admin/status_filter.rb @@ -32,7 +32,7 @@ class Admin::StatusFilter def scope_for(key, _value) case key.to_s 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 raise Mastodon::InvalidParameterError, "Unknown filter: #{key}" end diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 6dd8d048e4..5af4ec46ed 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -205,12 +205,11 @@ class MediaAttachment < ApplicationRecord validates :thumbnail, absence: true, if: -> { local? && !audio_or_video? } 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 :cached, -> { remote.where.not(file_file_name: nil) } scope :local, -> { where(remote_url: '') } + scope :ordered, -> { order(id: :asc) } scope :remote, -> { where.not(remote_url: '') } - scope :cached, -> { remote.where.not(file_file_name: nil) } - - default_scope { order(id: :asc) } + scope :unattached, -> { where(status_id: nil, scheduled_status_id: nil) } attr_accessor :skip_download diff --git a/app/models/report.rb b/app/models/report.rb index 81ad721df1..c565362cc6 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -131,25 +131,25 @@ class Report < ApplicationRecord Admin::ActionLog.where( target_type: 'Report', target_id: id - ).unscope(:order).arel, + ).arel, Admin::ActionLog.where( target_type: 'Account', target_id: target_account_id - ).unscope(:order).arel, + ).arel, Admin::ActionLog.where( target_type: 'Status', target_id: status_ids - ).unscope(:order).arel, + ).arel, Admin::ActionLog.where( target_type: 'AccountWarning', target_id: AccountWarning.where(report_id: id).select(:id) - ).unscope(:order).arel, + ).arel, ].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 private diff --git a/app/models/user_role.rb b/app/models/user_role.rb index 5472646c60..89354da542 100644 --- a/app/models/user_role.rb +++ b/app/models/user_role.rb @@ -49,7 +49,7 @@ class UserRole < ApplicationRecord invite_users ).freeze, - moderation: %w( + moderation: %i( view_dashboard view_audit_log manage_users @@ -63,7 +63,7 @@ class UserRole < ApplicationRecord manage_invites ).freeze, - administration: %w( + administration: %i( manage_settings manage_rules manage_roles @@ -72,7 +72,7 @@ class UserRole < ApplicationRecord manage_announcements ).freeze, - devops: %w( + devops: %i( view_devops ).freeze, diff --git a/app/serializers/rest/application_serializer.rb b/app/serializers/rest/application_serializer.rb index e4806a3c9f..635508a17c 100644 --- a/app/serializers/rest/application_serializer.rb +++ b/app/serializers/rest/application_serializer.rb @@ -2,7 +2,10 @@ class REST::ApplicationSerializer < ActiveModel::Serializer 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 object.id.to_s diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index e8b90d85df..0434687999 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -48,6 +48,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer status: object.status_page_url, }, + vapid: { + public_key: Rails.configuration.x.vapid_public_key, + }, + accounts: { max_featured_tags: FeaturedTag::LIMIT, }, diff --git a/app/services/backup_service.rb b/app/services/backup_service.rb index 48df348b05..4552fc0977 100644 --- a/app/services/backup_service.rb +++ b/app/services/backup_service.rb @@ -72,7 +72,7 @@ class BackupService < BaseService end 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| path = m.file&.path next unless path diff --git a/app/services/clear_domain_media_service.rb b/app/services/clear_domain_media_service.rb index 7bf2d62fb0..d3ad43e707 100644 --- a/app/services/clear_domain_media_service.rb +++ b/app/services/clear_domain_media_service.rb @@ -43,7 +43,7 @@ class ClearDomainMediaService < BaseService end def media_from_blocked_domain - MediaAttachment.joins(:account).merge(blocked_domain_accounts).reorder(nil) + MediaAttachment.joins(:account).merge(blocked_domain_accounts) end def emojis_from_blocked_domains diff --git a/app/services/delete_account_service.rb b/app/services/delete_account_service.rb index 190a72e5c5..7c7cb97df2 100644 --- a/app/services/delete_account_service.rb +++ b/app/services/delete_account_service.rb @@ -165,7 +165,7 @@ class DeleteAccountService < BaseService end 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) media_attachment.destroy diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb index 6204fefd6f..078a0423f2 100644 --- a/app/services/resolve_account_service.rb +++ b/app/services/resolve_account_service.rb @@ -100,7 +100,9 @@ class ResolveAccountService < BaseService end 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 def fetch_account! diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index e79c2d3d81..8d5446f1a8 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -65,7 +65,7 @@ class SuspendAccountService < BaseService def privatize_media_attachments! 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 = media_attachment.public_send(attachment_name) styles = MediaAttachment::DEFAULT_STYLES | attachment.styles.keys diff --git a/app/services/unsuspend_account_service.rb b/app/services/unsuspend_account_service.rb index 93cd04a943..652dd6a845 100644 --- a/app/services/unsuspend_account_service.rb +++ b/app/services/unsuspend_account_service.rb @@ -61,7 +61,7 @@ class UnsuspendAccountService < BaseService def publish_media_attachments! 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 = media_attachment.public_send(attachment_name) styles = MediaAttachment::DEFAULT_STYLES | attachment.styles.keys diff --git a/app/views/admin/account_actions/new.html.haml b/app/views/admin/account_actions/new.html.haml index 2a0cae15a0..4cb4401c70 100644 --- a/app/views/admin/account_actions/new.html.haml +++ b/app/views/admin/account_actions/new.html.haml @@ -5,7 +5,7 @@ = f.input :report_id, as: :hidden .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? %hr.spacer/ diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index 9cec8d6325..8354441895 100644 --- a/app/views/admin/accounts/index.html.haml +++ b/app/views/admin/accounts/index.html.haml @@ -10,7 +10,7 @@ .filter-subset.filter-subset--with-select %strong= t('admin.accounts.moderation.title') .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 %strong= t('admin.accounts.role') .input.select.optional diff --git a/app/views/admin/ip_blocks/new.html.haml b/app/views/admin/ip_blocks/new.html.haml index 405c73c90e..ecaf04315b 100644 --- a/app/views/admin/ip_blocks/new.html.haml +++ b/app/views/admin/ip_blocks/new.html.haml @@ -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') .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 = f.input :comment, as: :string, wrapper: :with_block_label diff --git a/app/views/admin/roles/_form.html.haml b/app/views/admin/roles/_form.html.haml index 2400332145..46a1c537a7 100644 --- a/app/views/admin/roles/_form.html.haml +++ b/app/views/admin/roles/_form.html.haml @@ -31,6 +31,6 @@ - (form.object.everyone? ? UserRole::Flags::CATEGORIES.slice(:invites) : UserRole::Flags::CATEGORIES).each do |category, permissions| %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/ diff --git a/app/views/admin/settings/discovery/show.html.haml b/app/views/admin/settings/discovery/show.html.haml index c12ba34aeb..12c122393b 100644 --- a/app/views/admin/settings/discovery/show.html.haml +++ b/app/views/admin/settings/discovery/show.html.haml @@ -45,7 +45,7 @@ %h4= t('admin.settings.security.federation_authentication') .fields-group - = f.input :authorized_fetch, as: :boolean, wrapper: :with_label, label: t('admin.settings.security.authorized_fetch'), warning_hint: authorized_fetch_overridden? ? t('admin.settings.security.authorized_fetch_overridden_hint') : nil, hint: t('admin.settings.security.authorized_fetch_hint'), disabled: authorized_fetch_overridden?, recommended: authorized_fetch_overridden? ? :overridden : nil + = 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') diff --git a/app/views/filters/_filter_fields.html.haml b/app/views/filters/_filter_fields.html.haml index 0690e8dd59..a3260816ed 100644 --- a/app/views/filters/_filter_fields.html.haml +++ b/app/views/filters/_filter_fields.html.haml @@ -10,7 +10,7 @@ %hr.spacer/ .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/ diff --git a/config/application.rb b/config/application.rb index 387827784a..99ee4ffd76 100644 --- a/config/application.rb +++ b/config/application.rb @@ -39,8 +39,8 @@ require_relative '../lib/mastodon/snowflake' require_relative '../lib/mastodon/version' require_relative '../lib/mastodon/rack_middleware' require_relative '../lib/public_file_server_middleware' -require_relative '../lib/devise/two_factor_ldap_authenticatable' -require_relative '../lib/devise/two_factor_pam_authenticatable' +require_relative '../lib/devise/strategies/two_factor_ldap_authenticatable' +require_relative '../lib/devise/strategies/two_factor_pam_authenticatable' require_relative '../lib/chewy/settings_extensions' require_relative '../lib/chewy/index_extensions' require_relative '../lib/chewy/strategy/mastodon' diff --git a/config/initializers/premailer_rails.rb b/config/initializers/premailer_rails.rb index 98b2082719..52576ef883 100644 --- a/config/initializers/premailer_rails.rb +++ b/config/initializers/premailer_rails.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative '../../lib/mastodon/premailer_webpack_strategy' +require_relative '../../lib/premailer_webpack_strategy' Premailer::Rails.config.merge!(remove_ids: true, adapter: :nokogiri, diff --git a/lib/devise/two_factor_ldap_authenticatable.rb b/lib/devise/strategies/two_factor_ldap_authenticatable.rb similarity index 100% rename from lib/devise/two_factor_ldap_authenticatable.rb rename to lib/devise/strategies/two_factor_ldap_authenticatable.rb diff --git a/lib/devise/two_factor_pam_authenticatable.rb b/lib/devise/strategies/two_factor_pam_authenticatable.rb similarity index 100% rename from lib/devise/two_factor_pam_authenticatable.rb rename to lib/devise/strategies/two_factor_pam_authenticatable.rb diff --git a/lib/mastodon/cli/federation.rb b/lib/mastodon/cli/federation.rb new file mode 100644 index 0000000000..1b4cb467a5 --- /dev/null +++ b/lib/mastodon/cli/federation.rb @@ -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 diff --git a/lib/mastodon/cli/main.rb b/lib/mastodon/cli/main.rb index 64f1646f49..ef40b81f33 100644 --- a/lib/mastodon/cli/main.rb +++ b/lib/mastodon/cli/main.rb @@ -8,6 +8,7 @@ require_relative 'canonical_email_blocks' require_relative 'domains' require_relative 'email_domain_blocks' require_relative 'emoji' +require_relative 'federation' require_relative 'feeds' require_relative 'ip_blocks' require_relative 'maintenance' @@ -65,66 +66,7 @@ module Mastodon::CLI desc 'maintenance SUBCOMMAND ...ARGS', 'Various maintenance utilities' subcommand 'maintenance', Maintenance - 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 - 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 + include Federation map %w(--version -v) => :version diff --git a/lib/mastodon/premailer_webpack_strategy.rb b/lib/premailer_webpack_strategy.rb similarity index 100% rename from lib/mastodon/premailer_webpack_strategy.rb rename to lib/premailer_webpack_strategy.rb diff --git a/package.json b/package.json index fdb0d1ec4d..503df8d51d 100644 --- a/package.json +++ b/package.json @@ -164,6 +164,7 @@ "@types/object-assign": "^4.0.30", "@types/prop-types": "^15.7.5", "@types/punycode": "^2.1.0", + "@types/rails__ujs": "^6.0.4", "@types/react": "^18.2.7", "@types/react-dom": "^18.2.4", "@types/react-helmet": "^6.1.6", @@ -187,6 +188,7 @@ "babel-jest": "^29.5.0", "eslint": "^8.41.0", "eslint-config-prettier": "^9.0.0", + "eslint-define-config": "^2.0.0", "eslint-import-resolver-typescript": "^3.5.5", "eslint-plugin-formatjs": "^4.10.1", "eslint-plugin-import": "~2.29.0", diff --git a/spec/controllers/api/v1/accounts/familiar_followers_controller_spec.rb b/spec/controllers/api/v1/accounts/familiar_followers_controller_spec.rb index 2261773094..3c7c7e8b84 100644 --- a/spec/controllers/api/v1/accounts/familiar_followers_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/familiar_followers_controller_spec.rb @@ -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 } 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 diff --git a/spec/controllers/concerns/challengable_concern_spec.rb b/spec/controllers/concerns/challengable_concern_spec.rb index 3324bdd24f..169e2122f8 100644 --- a/spec/controllers/concerns/challengable_concern_spec.rb +++ b/spec/controllers/concerns/challengable_concern_spec.rb @@ -85,7 +85,7 @@ RSpec.describe ChallengableConcern do before { get :foo } it 'renders challenge' do - expect(response).to render_template('auth/challenges/new') + expect(response).to render_template('auth/challenges/new', layout: :auth) end # See Auth::ChallengesControllerSpec @@ -95,7 +95,7 @@ RSpec.describe ChallengableConcern do before { post :bar } it 'renders challenge' do - expect(response).to render_template('auth/challenges/new') + expect(response).to render_template('auth/challenges/new', layout: :auth) end it 'accepts correct password' do @@ -106,7 +106,7 @@ RSpec.describe ChallengableConcern do it 'rejects wrong password' do 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 end end diff --git a/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb b/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb index a5a35e91d0..1b3b0cb0ae 100644 --- a/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb +++ b/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb @@ -20,37 +20,30 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do [true, false].each do |with_otp_secret| let(:user) { Fabricate(:user, email: 'local-part@domain', otp_secret: with_otp_secret ? 'oldotpsecret' : nil) } - describe 'GET #new' do - context 'when signed in and a new otp secret has been set in the session' do - subject do - sign_in user, scope: :user - get :new, session: { challenge_passed_at: Time.now.utc, new_otp_secret: 'thisisasecretforthespecofnewview' } - end + context 'when signed in' do + before { sign_in user, scope: :user } - include_examples 'renders :new' - end + describe 'GET #new' do + 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 - it 'redirects if not signed in' do - get :new - expect(response).to redirect_to('/auth/sign_in') - end + include_examples 'renders :new' + 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 + it 'redirects if a new otp_secret has not been set in the session' do + get :new, session: { challenge_passed_at: Time.now.utc } - describe 'POST #create' do - context 'when signed in' do - before do - sign_in user, scope: :user + expect(response).to redirect_to('/settings/otp_authentication') end + end + describe 'POST #create' do describe 'when form_two_factor_confirmation parameter is not provided' do it 'raises ActionController::ParameterMissing' do post :create, params: {}, session: { challenge_passed_at: Time.now.utc, new_otp_secret: 'thisisasecretforthespecofnewview' } + expect(response).to have_http_status(400) end end @@ -58,69 +51,78 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do describe 'when creation succeeds' do let!(:otp_backup_codes) { user.generate_otp_backup_codes! } - it 'renders page with success' do + before do prepare_user_otp_generation - prepare_user_otp_consumption + prepare_user_otp_consumption_response(true) allow(controller).to receive(:current_user).and_return(user) + end - 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 change { user.reload.otp_secret }.to 'thisisasecretforthespecofnewview' + it 'renders page with success' do + expect { post_create_with_options } + .to change { user.reload.otp_secret }.to 'thisisasecretforthespecofnewview' expect(assigns(:recovery_codes)).to eq otp_backup_codes expect(flash[:notice]).to eq 'Two-factor authentication successfully enabled' expect(response).to have_http_status(200) expect(response).to render_template('settings/two_factor_authentication/recovery_codes/index') 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 describe 'when creation fails' do subject do - options = { otp_secret: 'thisisasecretforthespecofnewview' } - allow(user) - .to receive(:validate_and_consume_otp!) - .with('123456', options) - .and_return(false) - allow(controller).to receive(:current_user).and_return(user) + expect { post_create_with_options } + .to(not_change { user.reload.otp_secret }) + end - 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 }) + before do + prepare_user_otp_consumption_response(false) + allow(controller).to receive(:current_user).and_return(user) end - it 'renders the new view' do + it 'renders page with error message' do subject expect(response.body).to include 'The entered code was invalid! Are server time and device time correct?' end include_examples 'renders :new' end - end - context 'when not signed in' do - it 'redirects if not signed in' do - post :create, params: { form_two_factor_confirmation: { otp_attempt: '123456' } } - expect(response).to redirect_to('/auth/sign_in') + private + + def post_create_with_options + 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 + + 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 diff --git a/spec/fabricators/admin_action_log_fabricator.rb b/spec/fabricators/action_log_fabricator.rb similarity index 68% rename from spec/fabricators/admin_action_log_fabricator.rb rename to spec/fabricators/action_log_fabricator.rb index 3acedbffd3..ce52cb73a0 100644 --- a/spec/fabricators/admin_action_log_fabricator.rb +++ b/spec/fabricators/action_log_fabricator.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -Fabricator('Admin::ActionLog') do +Fabricator(:action_log, from: Admin::ActionLog) do account { Fabricate.build(:account) } action 'MyString' target nil diff --git a/spec/lib/content_security_policy_spec.rb b/spec/lib/content_security_policy_spec.rb index 2e92f815ac..4286f14980 100644 --- a/spec/lib/content_security_policy_spec.rb +++ b/spec/lib/content_security_policy_spec.rb @@ -59,10 +59,10 @@ describe ContentSecurityPolicy do end end - describe '#media_host' do + describe '#media_hosts' do context 'when there is no configured CDN' 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 @@ -74,7 +74,7 @@ describe ContentSecurityPolicy do end 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 @@ -86,7 +86,7 @@ describe ContentSecurityPolicy do end 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 @@ -98,7 +98,7 @@ describe ContentSecurityPolicy do end 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 @@ -110,7 +110,7 @@ describe ContentSecurityPolicy do end 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 @@ -122,7 +122,7 @@ describe ContentSecurityPolicy do end 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 diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb index 0093dcd8de..c514c63b39 100644 --- a/spec/models/report_spec.rb +++ b/spec/models/report_spec.rb @@ -110,9 +110,9 @@ describe Report do let(:status) { Fabricate(:status) } before do - Fabricate('Admin::ActionLog', 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('Admin::ActionLog', target_type: 'Status', account_id: target_account.id, target_id: status.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(:action_log, target_type: 'Account', account_id: target_account.id, target_id: report.target_account_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 it 'returns right logs' do diff --git a/spec/serializers/rest/instance_serializer_spec.rb b/spec/serializers/rest/instance_serializer_spec.rb index 8ac32f2247..d8f2536d20 100644 --- a/spec/serializers/rest/instance_serializer_spec.rb +++ b/spec/serializers/rest/instance_serializer_spec.rb @@ -10,5 +10,11 @@ describe REST::InstanceSerializer do it 'returns recent usage data' do expect(serialization['usage']).to eq({ 'users' => { 'active_month' => 0 } }) 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 diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index 9d91f31cc5..53cbaf4cc1 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -384,7 +384,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do end 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.remote_url).to eq 'https://example.com/foo.png' diff --git a/spec/services/resolve_account_service_spec.rb b/spec/services/resolve_account_service_spec.rb index f446d0ca6d..9f5fdca297 100644 --- a/spec/services/resolve_account_service_spec.rb +++ b/spec/services/resolve_account_service_spec.rb @@ -144,6 +144,19 @@ RSpec.describe ResolveAccountService, type: :service do 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 it 'returns new remote account' do account = subject.call('foo@ap.example.com') diff --git a/streaming/.dockerignore b/streaming/.dockerignore new file mode 100644 index 0000000000..ea611a7b86 --- /dev/null +++ b/streaming/.dockerignore @@ -0,0 +1,7 @@ +.env +.env.* +.gitignore +node_modules +.DS_Store +*.swp +*~ diff --git a/streaming/.eslintrc.js b/streaming/.eslintrc.js new file mode 100644 index 0000000000..5e2d233c68 --- /dev/null +++ b/streaming/.eslintrc.js @@ -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, + }, + ], + }, +}); diff --git a/streaming/Dockerfile b/streaming/Dockerfile new file mode 100644 index 0000000000..6e0a84771c --- /dev/null +++ b/streaming/Dockerfile @@ -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 ] \ No newline at end of file diff --git a/streaming/index.js b/streaming/index.js index 1c12a83fe5..09e0c85be6 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -12,10 +12,12 @@ const { JSDOM } = require('jsdom'); const log = require('npmlog'); const pg = require('pg'); const dbUrlToConfig = require('pg-connection-string').parse; -const metrics = require('prom-client'); const uuid = require('uuid'); const WebSocket = require('ws'); +const { setupMetrics } = require('./metrics'); +const { isTruthy } = require("./utils"); + const environment = process.env.NODE_ENV || 'development'; // 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 { redisPrefix } = redisConfig; - // 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 - 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); + const metrics = setupMetrics(CHANNEL_NAMES, pgPool); + // TODO: migrate all metrics to metrics.X.method() instead of just X.method() + const { + connectedClients, + connectedChannels, + redisSubscriptions, + redisMessagesReceived, + messagesSent, + } = metrics; // When checking metrics in the browser, the favicon is requested this // 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} res diff --git a/streaming/metrics.js b/streaming/metrics.js new file mode 100644 index 0000000000..d05b4c9b16 --- /dev/null +++ b/streaming/metrics.js @@ -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; diff --git a/streaming/package.json b/streaming/package.json index 52245aeebb..0cfc5b3276 100644 --- a/streaming/package.json +++ b/streaming/package.json @@ -12,7 +12,8 @@ "url": "https://github.com/mastodon/mastodon.git" }, "scripts": { - "start": "node ./index.js" + "start": "node ./index.js", + "check:types": "tsc --noEmit" }, "dependencies": { "dotenv": "^16.0.3", @@ -30,7 +31,10 @@ "@types/express": "^4.17.17", "@types/npmlog": "^7.0.0", "@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": { "bufferutil": "^4.0.7", diff --git a/streaming/tsconfig.json b/streaming/tsconfig.json new file mode 100644 index 0000000000..032274d856 --- /dev/null +++ b/streaming/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "target": "esnext", + "module": "CommonJS", + "moduleResolution": "node", + "noUnusedParameters": false, + "paths": {} + }, + "include": ["./*.js", "./.eslintrc.js"] +} diff --git a/streaming/utils.js b/streaming/utils.js new file mode 100644 index 0000000000..ad8dd4889f --- /dev/null +++ b/streaming/utils.js @@ -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; diff --git a/yarn.lock b/yarn.lock index 25a93c7889..183b5eb96c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -42,55 +42,55 @@ __metadata: languageName: node 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": - version: 7.22.13 - resolution: "@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.23.5 + resolution: "@babel/code-frame@npm:7.23.5" dependencies: - "@babel/highlight": "npm:^7.22.13" + "@babel/highlight": "npm:^7.23.4" chalk: "npm:^2.4.2" - checksum: f4cc8ae1000265677daf4845083b72f88d00d311adb1a93c94eb4b07bf0ed6828a81ae4ac43ee7d476775000b93a28a9cddec18fbdc5796212d8dcccd5de72bd + checksum: a10e843595ddd9f97faa99917414813c06214f4d9205294013e20c70fbdf4f943760da37dec1d998bf3e6fc20fa2918a47c0e987a7e458663feb7698063ad7c6 languageName: node linkType: hard -"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.22.9, @babel/compat-data@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/compat-data@npm:7.23.3" - checksum: c6af331753c34ee8a5678bc94404320826cb56b1dda3efc1311ec8fb0774e78225132f3c1acc988440ace667f14a838e297a822692b95758aa63da406e1f97a1 +"@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.5 + resolution: "@babel/compat-data@npm:7.23.5" + checksum: 081278ed46131a890ad566a59c61600a5f9557bd8ee5e535890c8548192532ea92590742fd74bd9db83d74c669ef8a04a7e1c85cdea27f960233e3b83c3a957c languageName: node 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": - version: 7.23.3 - resolution: "@babel/core@npm:7.23.3" + version: 7.23.5 + resolution: "@babel/core@npm:7.23.5" dependencies: "@ampproject/remapping": "npm:^2.2.0" - "@babel/code-frame": "npm:^7.22.13" - "@babel/generator": "npm:^7.23.3" + "@babel/code-frame": "npm:^7.23.5" + "@babel/generator": "npm:^7.23.5" "@babel/helper-compilation-targets": "npm:^7.22.15" "@babel/helper-module-transforms": "npm:^7.23.3" - "@babel/helpers": "npm:^7.23.2" - "@babel/parser": "npm:^7.23.3" + "@babel/helpers": "npm:^7.23.5" + "@babel/parser": "npm:^7.23.5" "@babel/template": "npm:^7.22.15" - "@babel/traverse": "npm:^7.23.3" - "@babel/types": "npm:^7.23.3" + "@babel/traverse": "npm:^7.23.5" + "@babel/types": "npm:^7.23.5" convert-source-map: "npm:^2.0.0" debug: "npm:^4.1.0" gensync: "npm:^1.0.0-beta.2" json5: "npm:^2.2.3" semver: "npm:^6.3.1" - checksum: 08d43b749e24052d12713a7fb1f0c0d1275d4fb056d00846faeb8da79ecf6d0ba91a11b6afec407b8b0f9388d00e2c2f485f282bef0ade4d6d0a17de191a4287 + checksum: 311a512a870ee330a3f9a7ea89e5df790b2b5af0b1bd98b10b4edc0de2ac440f0df4d69ea2c0ee38a4b89041b9a495802741d93603be7d4fd834ec8bb6970bd2 languageName: node linkType: hard -"@babel/generator@npm:^7.23.3, @babel/generator@npm:^7.7.2": - version: 7.23.3 - resolution: "@babel/generator@npm:7.23.3" +"@babel/generator@npm:^7.23.5, @babel/generator@npm:^7.7.2": + version: 7.23.5 + resolution: "@babel/generator@npm:7.23.5" dependencies: - "@babel/types": "npm:^7.23.3" + "@babel/types": "npm:^7.23.5" "@jridgewell/gen-mapping": "npm:^0.3.2" "@jridgewell/trace-mapping": "npm:^0.3.17" jsesc: "npm:^2.5.1" - checksum: d5fff1417eecfada040e01a7c77a4968e81c436aeb35815ce85b4e80cd01e731423613d61033044a6cb5563bb8449ee260e3379b63eb50b38ec0a9ea9c00abfd + checksum: 14c6e874f796c4368e919bed6003bb0adc3ce837760b08f9e646d20aeb5ae7d309723ce6e4f06bcb4a2b5753145446c8e4425851380f695e40e71e1760f49e7b languageName: node linkType: hard @@ -310,10 +310,10 @@ __metadata: languageName: node linkType: hard -"@babel/helper-string-parser@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-string-parser@npm:7.22.5" - checksum: 6b0ff8af724377ec41e5587fffa7605198da74cb8e7d8d48a36826df0c0ba210eb9fedb3d9bef4d541156e0bd11040f021945a6cbb731ccec4aefb4affa17aa4 +"@babel/helper-string-parser@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/helper-string-parser@npm:7.23.4" + checksum: f348d5637ad70b6b54b026d6544bd9040f78d24e7ec245a0fc42293968181f6ae9879c22d89744730d246ce8ec53588f716f102addd4df8bbc79b73ea10004ac languageName: node linkType: hard @@ -324,10 +324,10 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-option@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/helper-validator-option@npm:7.22.15" - checksum: e9661bf80ba18e2dd978217b350fb07298e57ac417f4f1ab9fa011505e20e4857f2c3b4b538473516a9dc03af5ce3a831e5ed973311c28326f4c330b6be981c2 +"@babel/helper-validator-option@npm:^7.22.15, @babel/helper-validator-option@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/helper-validator-option@npm:7.23.5" + checksum: af45d5c0defb292ba6fd38979e8f13d7da63f9623d8ab9ededc394f67eb45857d2601278d151ae9affb6e03d5d608485806cd45af08b4468a0515cf506510e94 languageName: node linkType: hard @@ -342,34 +342,34 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.23.2": - version: 7.23.2 - resolution: "@babel/helpers@npm:7.23.2" +"@babel/helpers@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/helpers@npm:7.23.5" dependencies: "@babel/template": "npm:^7.22.15" - "@babel/traverse": "npm:^7.23.2" - "@babel/types": "npm:^7.23.0" - checksum: 3a6a939c5277a27486e7c626812f0643b35d1c053ac2eb66911f5ae6c0a4e4bcdd40750eba36b766b0ee8a753484287f50ae56232a5f8f2947116723e44b9e35 + "@babel/traverse": "npm:^7.23.5" + "@babel/types": "npm:^7.23.5" + checksum: a37e2728eb4378a4888e5d614e28de7dd79b55ac8acbecd0e5c761273e2a02a8f33b34b1932d9069db55417ace2937cbf8ec37c42f1030ce6d228857d7ccaa4f languageName: node linkType: hard -"@babel/highlight@npm:^7.22.13": - version: 7.22.20 - resolution: "@babel/highlight@npm:7.22.20" +"@babel/highlight@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/highlight@npm:7.23.4" dependencies: "@babel/helper-validator-identifier": "npm:^7.22.20" chalk: "npm:^2.4.2" js-tokens: "npm:^4.0.0" - checksum: f3c3a193afad23434297d88e81d1d6c0c2cf02423de2139ada7ce0a7fc62d8559abf4cc996533c1a9beca7fc990010eb8d544097f75e818ac113bf39ed810aa2 + checksum: fbff9fcb2f5539289c3c097d130e852afd10d89a3a08ac0b5ebebbc055cc84a4bcc3dcfed463d488cde12dd0902ef1858279e31d7349b2e8cee43913744bda33 languageName: node 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": - version: 7.23.3 - resolution: "@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.5 + resolution: "@babel/parser@npm:7.23.5" bin: parser: ./bin/babel-parser.js - checksum: 0fe11eadd4146a9155305b5bfece0f8223a3b1b97357ffa163c0156940de92e76cd0e7a173de819b8692767147e62f33389b312d1537f84cede51092672df6ef + checksum: 3356aa90d7bafb4e2c7310e7c2c3d443c4be4db74913f088d3d577a1eb914ea4188e05fd50a47ce907a27b755c4400c4e3cbeee73dbeb37761f6ca85954f5a20 languageName: node linkType: hard @@ -661,9 +661,9 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-async-generator-functions@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-async-generator-functions@npm:7.23.3" +"@babel/plugin-transform-async-generator-functions@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-async-generator-functions@npm:7.23.4" dependencies: "@babel/helper-environment-visitor": "npm:^7.22.20" "@babel/helper-plugin-utils": "npm:^7.22.5" @@ -671,7 +671,7 @@ __metadata: "@babel/plugin-syntax-async-generators": "npm:^7.8.4" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: e846f282658e097fce4fccf3ee29289bf05f0654846a5994727a36f0cdc2e47abdffd4be4fa65787e94aa975824fae894c90afbfdc8caacd46c12c7f43e99d7f + checksum: f2eef4de609975a3f7da7832576b5ffc93e43c80f87e1a99e886b0f8591096cfc4c37e2d5f52fdeaa2a9c09a25a59f3e621159abaca75d3193922a5c0e4cbe0c languageName: node linkType: hard @@ -699,14 +699,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-block-scoping@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-block-scoping@npm:7.23.3" +"@babel/plugin-transform-block-scoping@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-block-scoping@npm:7.23.4" dependencies: "@babel/helper-plugin-utils": "npm:^7.22.5" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: ccaeded7954c196811d22a35322579254cda52676e823682b6234885a3aaf88fe0d5152dacaec43db9031dcf35a050a5343e36028e5905b0ba9c02d36b30a57f + checksum: 83006804dddf980ab1bcd6d67bc381e24b58c776507c34f990468f820d0da71dba3697355ca4856532fa2eeb2a1e3e73c780f03760b5507a511cbedb0308e276 languageName: node linkType: hard @@ -722,22 +722,22 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-class-static-block@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-class-static-block@npm:7.23.3" +"@babel/plugin-transform-class-static-block@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-class-static-block@npm:7.23.4" dependencies: "@babel/helper-create-class-features-plugin": "npm:^7.22.15" "@babel/helper-plugin-utils": "npm:^7.22.5" "@babel/plugin-syntax-class-static-block": "npm:^7.14.5" peerDependencies: "@babel/core": ^7.12.0 - checksum: 89cdb66d7bc834cd51659eb7286a6bee23add0bc114943d68c4b6c0c834178cf0d55183df0cf508fec9c55ed4155641360e6f55a91c16fe826ccaf1adf381922 + checksum: fdca96640ef29d8641a7f8de106f65f18871b38cc01c0f7b696d2b49c76b77816b30a812c08e759d06dd10b4d9b3af6b5e4ac22a2017a88c4077972224b77ab0 languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-classes@npm:7.23.3" +"@babel/plugin-transform-classes@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/plugin-transform-classes@npm:7.23.5" dependencies: "@babel/helper-annotate-as-pure": "npm:^7.22.5" "@babel/helper-compilation-targets": "npm:^7.22.15" @@ -750,7 +750,7 @@ __metadata: globals: "npm:^11.1.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 88bfd332db0ba5cbfb8557a2ba5a7185151aebc9cfe3035b014aa6d795556acbe672bb8c78da3c9fd1d23f55a333d14b5daa127ef037f5ced5198b6d79a146d6 + checksum: 07988f52b4893151887d1ea6ff79e5fe834078c5731bd09babd5659edbbae21ea4e2de326a02443a63fd776b4c945da6177f07875b56fe66e0b7899e830a9e92 languageName: node linkType: hard @@ -800,15 +800,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-dynamic-import@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-dynamic-import@npm:7.23.3" +"@babel/plugin-transform-dynamic-import@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-dynamic-import@npm:7.23.4" dependencies: "@babel/helper-plugin-utils": "npm:^7.22.5" "@babel/plugin-syntax-dynamic-import": "npm:^7.8.3" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: df3fd130312dc53d068fa76333991dce5e86987b023af8c3b502bd7d36a8e67da6f718e61dc838576a9fbacd06628e29607ee22d9bae30705485c14130eab201 + checksum: 19ae4a4a2ca86d35224734c41c48b2aa6a13139f3cfa1cbd18c0e65e461de8b65687dec7e52b7a72bb49db04465394c776aa1b13a2af5dc975b2a0cde3dcab67 languageName: node linkType: hard @@ -824,15 +824,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-export-namespace-from@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-export-namespace-from@npm:7.23.3" +"@babel/plugin-transform-export-namespace-from@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-export-namespace-from@npm:7.23.4" dependencies: "@babel/helper-plugin-utils": "npm:^7.22.5" "@babel/plugin-syntax-export-namespace-from": "npm:^7.8.3" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 390c6626dcda99023629049d92090242b4575351a4a7b47f97febabd2381f2cd0f624de661d8de8d1f715fedd63753cfd1feddead19e5960c27b88e447465b81 + checksum: 38bf04f851e36240bbe83ace4169da626524f4107bfb91f05b4ad93a5fb6a36d5b3d30b8883c1ba575ccfc1bac7938e90ca2e3cb227f7b3f4a9424beec6fd4a7 languageName: node linkType: hard @@ -860,15 +860,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-json-strings@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-json-strings@npm:7.23.3" +"@babel/plugin-transform-json-strings@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-json-strings@npm:7.23.4" dependencies: "@babel/helper-plugin-utils": "npm:^7.22.5" "@babel/plugin-syntax-json-strings": "npm:^7.8.3" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: e1cef6a485b9da32aba9449fb459dac062dfc401f3d6ad48e7fbdcb73bbe470c995cc15ce5c421b95efe1e9a90d5507eb606360fe10b6d8cb869dd5dae7a2562 + checksum: 39e82223992a9ad857722ae051291935403852ad24b0dd64c645ca1c10517b6bf9822377d88643fed8b3e61a4e3f7e5ae41cf90eb07c40a786505d47d5970e54 languageName: node linkType: hard @@ -883,15 +883,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-logical-assignment-operators@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.23.3" +"@babel/plugin-transform-logical-assignment-operators@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.23.4" dependencies: "@babel/helper-plugin-utils": "npm:^7.22.5" "@babel/plugin-syntax-logical-assignment-operators": "npm:^7.10.4" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 23b7588b26d420c8b132bd08916d49871ca0c8db892f6b58637b10e2a0d918163d413c505db880a9157fc2e61d089040f139298a60d837ccbd0efca0474ac7ca + checksum: 87b034dd13143904e405887e6125d76c27902563486efc66b7d9a9d8f9406b76c6ac42d7b37224014af5783d7edb465db0cdecd659fa3227baad0b3a6a35deff languageName: node linkType: hard @@ -980,7 +980,7 @@ __metadata: languageName: node 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 resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.23.4" dependencies: @@ -992,21 +992,21 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-numeric-separator@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-numeric-separator@npm:7.23.3" +"@babel/plugin-transform-numeric-separator@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-numeric-separator@npm:7.23.4" dependencies: "@babel/helper-plugin-utils": "npm:^7.22.5" "@babel/plugin-syntax-numeric-separator": "npm:^7.10.4" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: d3748cce20e8752e61dfda55e275c699459a3ff8d0bb46585da813136e04066b1ce70b71beef504fcdc8d4cca3c955112cea96d5e9fd5a42a5bc8956d05236c2 + checksum: e34902da4f5588dc4812c92cb1f6a5e3e3647baf7b4623e30942f551bf1297621abec4e322ebfa50b320c987c0f34d9eb4355b3d289961d9035e2126e3119c12 languageName: node linkType: hard -"@babel/plugin-transform-object-rest-spread@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-object-rest-spread@npm:7.23.3" +"@babel/plugin-transform-object-rest-spread@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-object-rest-spread@npm:7.23.4" dependencies: "@babel/compat-data": "npm:^7.23.3" "@babel/helper-compilation-targets": "npm:^7.22.15" @@ -1015,7 +1015,7 @@ __metadata: "@babel/plugin-transform-parameters": "npm:^7.23.3" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 31ab631aaba945c118662943e5f1f54a21f07d64f06e06b25d55871168c460f3eeeccdf7b05aa74a1340e2cfbe781ad3c7ceccd0c2585d39f7b73ba11ebaa9d0 + checksum: b56017992ffe7fcd1dd9a9da67c39995a141820316266bcf7d77dc912980d228ccbd3f36191d234f5cc389b09157b5d2a955e33e8fb368319534affd1c72b262 languageName: node linkType: hard @@ -1031,28 +1031,28 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-optional-catch-binding@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.23.3" +"@babel/plugin-transform-optional-catch-binding@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.23.4" dependencies: "@babel/helper-plugin-utils": "npm:^7.22.5" "@babel/plugin-syntax-optional-catch-binding": "npm:^7.8.3" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 85ac1e94ee8f21648816151628ff931cc16143ec8c904649a1ecfd8960160290eccc5a197b4ae3ee7a1c7a27a7c4189e61b4de24483d5bad4040784afe2d206f + checksum: 4ef61812af0e4928485e28301226ce61139a8b8cea9e9a919215ebec4891b9fea2eb7a83dc3090e2679b7d7b2c8653da601fbc297d2addc54a908b315173991e languageName: node linkType: hard -"@babel/plugin-transform-optional-chaining@npm:^7.23.3": - version: 7.23.3 - resolution: "@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.4 + resolution: "@babel/plugin-transform-optional-chaining@npm:7.23.4" dependencies: "@babel/helper-plugin-utils": "npm:^7.22.5" "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5" "@babel/plugin-syntax-optional-chaining": "npm:^7.8.3" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 2b358962169d871392aa292a67527e5335909438da0ddbb0d19e7838c0f8a2081cc751a49e6e534ac4d6c932254531a205ac22b197f64fc4c89f41bf9f595497 + checksum: 305b773c29ad61255b0e83ec1e92b2f7af6aa58be4cba1e3852bddaa14f7d2afd7b4438f41c28b179d6faac7eb8d4fb5530a17920294f25d459b8f84406bfbfb languageName: node linkType: hard @@ -1079,9 +1079,9 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-private-property-in-object@npm:^7.23.3": - version: 7.23.3 - resolution: "@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.4 + resolution: "@babel/plugin-transform-private-property-in-object@npm:7.23.4" dependencies: "@babel/helper-annotate-as-pure": "npm:^7.22.5" "@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" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 9211dd25a6e87a01535f2d97a663fa6de3472b963c8dcfaacce229a2e3fa6500f2e9fc690bc100a540fc7b66c8364faf7ef19b32e9c9b9791e4561b742c15ed3 + checksum: 8d31b28f24204b4d13514cd3a8f3033abf575b1a6039759ddd6e1d82dd33ba7281f9bc85c9f38072a665d69bfa26dc40737eefaf9d397b024654a483d2357bf5 languageName: node linkType: hard @@ -1333,13 +1333,13 @@ __metadata: linkType: hard "@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 - resolution: "@babel/preset-env@npm:7.23.3" + version: 7.23.5 + resolution: "@babel/preset-env@npm:7.23.5" 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-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-v8-spread-parameters-in-optional-chaining": "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-unicode-sets-regex": "npm:^7.18.6" "@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-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-static-block": "npm:^7.23.3" - "@babel/plugin-transform-classes": "npm:^7.23.3" + "@babel/plugin-transform-class-static-block": "npm:^7.23.4" + "@babel/plugin-transform-classes": "npm:^7.23.5" "@babel/plugin-transform-computed-properties": "npm:^7.23.3" "@babel/plugin-transform-destructuring": "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-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-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-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-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-modules-amd": "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-named-capturing-groups-regex": "npm:^7.22.5" "@babel/plugin-transform-new-target": "npm:^7.23.3" - "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.23.3" - "@babel/plugin-transform-numeric-separator": "npm:^7.23.3" - "@babel/plugin-transform-object-rest-spread": "npm:^7.23.3" + "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.23.4" + "@babel/plugin-transform-numeric-separator": "npm:^7.23.4" + "@babel/plugin-transform-object-rest-spread": "npm:^7.23.4" "@babel/plugin-transform-object-super": "npm:^7.23.3" - "@babel/plugin-transform-optional-catch-binding": "npm:^7.23.3" - "@babel/plugin-transform-optional-chaining": "npm:^7.23.3" + "@babel/plugin-transform-optional-catch-binding": "npm:^7.23.4" + "@babel/plugin-transform-optional-chaining": "npm:^7.23.4" "@babel/plugin-transform-parameters": "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-regenerator": "npm:^7.23.3" "@babel/plugin-transform-reserved-words": "npm:^7.23.3" @@ -1418,7 +1418,7 @@ __metadata: semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 36b02a86817ab5474bb74a8d62a110723b0b05904a52ddc5627cf89457525b8d5ac0739b8e435a6ae12ef8b90cd5fc191169898c3dc2ac9d2c84026b02f2580a + checksum: 2a0e1274dec045186e131c6433659b75492583290e8d41633c616f6bff829cb2e4b2f9a57f556283a54db3bd6aa697911e56a36f607911a29b731c445a5b5a06 languageName: node linkType: hard @@ -1483,11 +1483,11 @@ __metadata: 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": - version: 7.23.4 - resolution: "@babel/runtime@npm:7.23.4" + version: 7.23.5 + resolution: "@babel/runtime@npm:7.23.5" dependencies: regenerator-runtime: "npm:^0.14.0" - checksum: db2bf183cd0119599b903ca51ca0aeea8e0ab478a16be1aae10dd90473ed614159d3e5adfdd8f8f3d840402428ce0d90b5c01aae95da9e45a2dd83e02d85ca27 + checksum: ca679cc91bb7e424bc2db87bb58cc3b06ade916b9adb21fbbdc43e54cdaacb3eea201ceba2a0464b11d2eb65b9fe6a6ffcf4d7521fa52994f19be96f1af14788 languageName: node linkType: hard @@ -1502,32 +1502,32 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:7, @babel/traverse@npm:^7.23.2, @babel/traverse@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/traverse@npm:7.23.3" +"@babel/traverse@npm:7, @babel/traverse@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/traverse@npm:7.23.5" dependencies: - "@babel/code-frame": "npm:^7.22.13" - "@babel/generator": "npm:^7.23.3" + "@babel/code-frame": "npm:^7.23.5" + "@babel/generator": "npm:^7.23.5" "@babel/helper-environment-visitor": "npm:^7.22.20" "@babel/helper-function-name": "npm:^7.23.0" "@babel/helper-hoist-variables": "npm:^7.22.5" "@babel/helper-split-export-declaration": "npm:^7.22.6" - "@babel/parser": "npm:^7.23.3" - "@babel/types": "npm:^7.23.3" + "@babel/parser": "npm:^7.23.5" + "@babel/types": "npm:^7.23.5" debug: "npm:^4.1.0" globals: "npm:^11.1.0" - checksum: 3c2784f4765185126d64fd5eebce0413b7aee6d54f779998594a343a7f973a9693a441ba27533df84e7ab7ce22f1239c6837f35e903132a1b25f7fc7a67bc30f + checksum: c5ea793080ca6719b0a1612198fd25e361cee1f3c14142d7a518d2a1eeb5c1d21f7eec1b26c20ea6e1ddd8ed12ab50b960ff95ffd25be353b6b46e1b54d6f825 languageName: node 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": - version: 7.23.3 - resolution: "@babel/types@npm:7.23.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.5 + resolution: "@babel/types@npm:7.23.5" 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" to-fast-properties: "npm:^2.0.0" - checksum: 371a10dd9c8d8ebf48fc5d9e1b327dafd74453f8ea582dcbddd1cee5ae34e8881b743e783a86c08c04dcd1849b1842455472a911ae8a1c185484fe9b7b5f1595 + checksum: 7dd5e2f59828ed046ad0b06b039df2524a8b728d204affb4fc08da2502b9dd3140b1356b5166515d229dc811539a8b70dcd4bc507e06d62a89f4091a38d0b0fb languageName: node linkType: hard @@ -2317,6 +2317,7 @@ __metadata: "@types/object-assign": "npm:^4.0.30" "@types/prop-types": "npm:^15.7.5" "@types/punycode": "npm:^2.1.0" + "@types/rails__ujs": "npm:^6.0.4" "@types/react": "npm:^18.2.7" "@types/react-dom": "npm:^18.2.4" "@types/react-helmet": "npm:^6.1.6" @@ -2363,6 +2364,7 @@ __metadata: escape-html: "npm:^1.0.3" eslint: "npm:^8.41.0" eslint-config-prettier: "npm:^9.0.0" + eslint-define-config: "npm:^2.0.0" eslint-import-resolver-typescript: "npm:^3.5.5" eslint-plugin-formatjs: "npm:^4.10.1" eslint-plugin-import: "npm:~2.29.0" @@ -2472,8 +2474,10 @@ __metadata: "@types/npmlog": "npm:^7.0.0" "@types/pg": "npm:^8.6.6" "@types/uuid": "npm:^9.0.0" + "@types/ws": "npm:^8.5.9" bufferutil: "npm:^4.0.7" dotenv: "npm:^16.0.3" + eslint-define-config: "npm:^2.0.0" express: "npm:^4.18.2" ioredis: "npm:^5.3.2" jsdom: "npm:^23.0.0" @@ -2481,6 +2485,7 @@ __metadata: pg: "npm:^8.5.0" pg-connection-string: "npm:^2.6.0" prom-client: "npm:^15.0.0" + typescript: "npm:^5.0.4" utf-8-validate: "npm:^6.0.3" uuid: "npm:^9.0.0" ws: "npm:^8.12.1" @@ -3348,6 +3353,13 @@ __metadata: languageName: node 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:*": version: 1.2.7 resolution: "@types/range-parser@npm:1.2.7" @@ -3647,6 +3659,15 @@ __metadata: languageName: node 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:*": version: 21.0.2 resolution: "@types/yargs-parser@npm:21.0.2" @@ -7312,6 +7333,13 @@ __metadata: languageName: node 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": version: 0.3.9 resolution: "eslint-import-resolver-node@npm:0.3.9" @@ -10636,8 +10664,8 @@ __metadata: linkType: hard "jsdom@npm:^23.0.0": - version: 23.0.0 - resolution: "jsdom@npm:23.0.0" + version: 23.0.1 + resolution: "jsdom@npm:23.0.1" dependencies: cssstyle: "npm:^3.0.0" data-urls: "npm:^5.0.0" @@ -10661,11 +10689,11 @@ __metadata: ws: "npm:^8.14.2" xml-name-validator: "npm:^5.0.0" peerDependencies: - canvas: ^3.0.0 + canvas: ^2.11.2 peerDependenciesMeta: canvas: optional: true - checksum: 2c876a02de49e0ed6b667a4eb9b08b8e76ac189a5571ff97791cc9564e713259314deea6d657cc7f59fc30af41b900e7d833c95017e576dfcaf25f32565722af + checksum: 13b2b3693ccb40215d1cce77bac7a295414ee4c0a06e30167f8087c9867145ba23dbd592bd95a801cadd7b3698bfd20b9c3f2c26fd8422607f22609ed2e404ef languageName: node linkType: hard