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

Conflicts:
- `config/initializers/content_security_policy.rb`:
  Upstream reworked the CSP, we kept our version for now.
- `spec/requests/content_security_policy_spec.rb`:
  Upstream reworked the CSP, we kept our version for now.
th-downstream
Claire 11 months ago
commit b8209c3b96

@ -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

@ -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',
},
},
}
],
};
});

@ -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 }}

@ -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

@ -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

@ -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

@ -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

@ -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:

@ -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
# 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", "--"]
EXPOSE 3000 4000

@ -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)

6
Vagrantfile vendored

@ -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"]}

@ -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

@ -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

@ -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

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

@ -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

@ -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?

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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 {

@ -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<PropsWithChildren<Props>> = ({
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;

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

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

@ -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}' },

@ -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';

@ -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([

@ -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 || '';
}
});

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

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

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

@ -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 = (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' width='13' height='13'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 20 20'
width='13'
height='13'
>
<path d='M12.9 14.32a8 8 0 1 1 1.41-1.41l5.35 5.33-1.42 1.42-5.33-5.34zM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12z' />
</svg>
);
export const deleteIcon = (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' width='13' height='13'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 20 20'
width='13'
height='13'
>
<path d='M10 8.586L2.929 1.515 1.515 2.929 8.586 10l-7.071 7.071 1.414 1.414L10 11.414l7.071 7.071 1.414-1.414L11.414 10l7.071-7.071-1.414-1.414L10 8.586z' />
</svg>
);

@ -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));
}
};

@ -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);
}
};

@ -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<OptionalRouterProps>,
>(Component: ComponentType) {
const displayName = `withRouter(${Component.displayName ?? Component.name})`;
const C = (props: React.ComponentProps<ComponentType>) => {
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.
<Component
{...remainingProps}
{...context}
ref={wrappedComponentRef}
/>
);
else
return (
<Component
{...remainingProps}
ref={wrappedComponentRef}
/>
);
} else {
// @ts-expect-error - Dynamic covariant generic components are tough to type.
return <Component {...remainingProps} ref={wrappedComponentRef} />;
}
}}
</__RouterContext.Consumer>
);
@ -53,8 +59,8 @@ export function withOptionalRouter(Component) {
wrappedComponentRef: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
PropTypes.object
])
PropTypes.object,
]),
};
return hoistStatics(C, Component);

@ -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;

@ -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

@ -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

@ -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?

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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,

@ -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

@ -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,
},

@ -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

@ -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

@ -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

@ -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!

@ -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

@ -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

@ -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/

@ -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

@ -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

@ -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/

@ -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')

@ -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/

@ -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'

@ -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,

@ -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

@ -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

@ -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",

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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'

@ -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')

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

@ -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,
},
],
},
});

@ -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 ]

@ -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

@ -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;

@ -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",

@ -0,0 +1,11 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"target": "esnext",
"module": "CommonJS",
"moduleResolution": "node",
"noUnusedParameters": false,
"paths": {}
},
"include": ["./*.js", "./.eslintrc.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;

@ -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

Loading…
Cancel
Save