mirror of
https://git.bsd.gay/fef/nyastodon.git
synced 2024-12-24 19:23:41 +01:00
Merge commit '85662a5a57531af5402a6777d0b1089e78c56815' into glitch-soc/merge-upstream
Conflicts: - `config/initializers/content_security_policy.rb`: Upstream reworked the CSP, we kept our version for now. - `spec/requests/content_security_policy_spec.rb`: Upstream reworked the CSP, we kept our version for now.
This commit is contained in:
commit
b8209c3b96
94 changed files with 1386 additions and 731 deletions
|
@ -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
|
||||
|
|
22
.eslintrc.js
22
.eslintrc.js
|
@ -1,4 +1,7 @@
|
|||
module.exports = {
|
||||
// @ts-check
|
||||
const { defineConfig } = require('eslint-define-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
root: true,
|
||||
|
||||
extends: [
|
||||
|
@ -193,6 +196,7 @@ module.exports = {
|
|||
'error',
|
||||
{
|
||||
devDependencies: [
|
||||
'.eslintrc.js',
|
||||
'config/webpack/**',
|
||||
'app/javascript/mastodon/performance.js',
|
||||
'app/javascript/mastodon/test_setup.js',
|
||||
|
@ -297,7 +301,6 @@ module.exports = {
|
|||
'formatjs/no-id': 'off', // IDs are used for translation keys
|
||||
'formatjs/no-invalid-icu': 'error',
|
||||
'formatjs/no-literal-string-in-jsx': 'off', // Should be looked at, but mainly flagging punctuation outside of strings
|
||||
'formatjs/no-multiple-plurals': 'off', // Only used by hashtag.jsx
|
||||
'formatjs/no-multiple-whitespaces': 'error',
|
||||
'formatjs/no-offset': 'error',
|
||||
'formatjs/no-useless-message': 'error',
|
||||
|
@ -316,6 +319,7 @@ module.exports = {
|
|||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'.eslintrc.js',
|
||||
'*.config.js',
|
||||
'.*rc.js',
|
||||
'ide-helper.js',
|
||||
|
@ -366,7 +370,7 @@ module.exports = {
|
|||
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
|
||||
'@typescript-eslint/consistent-type-exports': 'error',
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
"@typescript-eslint/prefer-nullish-coalescing": ['error', {ignorePrimitives: {boolean: true}}],
|
||||
"@typescript-eslint/prefer-nullish-coalescing": ['error', { ignorePrimitives: { boolean: true } }],
|
||||
|
||||
'jsdoc/require-jsdoc': 'off',
|
||||
|
||||
|
@ -389,14 +393,6 @@ module.exports = {
|
|||
env: {
|
||||
jest: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'streaming/**/*',
|
||||
],
|
||||
rules: {
|
||||
'import/no-commonjs': 'off',
|
||||
},
|
||||
},
|
||||
}
|
||||
],
|
||||
};
|
||||
});
|
||||
|
|
3
.github/workflows/build-container-image.yml
vendored
3
.github/workflows/build-container-image.yml
vendored
|
@ -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 }}
|
||||
|
|
23
.github/workflows/build-nightly.yml
vendored
23
.github/workflows/build-nightly.yml
vendored
|
@ -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
|
||||
|
|
17
.github/workflows/build-push-pr.yml
vendored
17
.github/workflows/build-push-pr.yml
vendored
|
@ -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
|
||||
|
|
22
.github/workflows/build-releases.yml
vendored
22
.github/workflows/build-releases.yml
vendored
|
@ -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
|
||||
|
|
14
.github/workflows/test-image-build.yml
vendored
14
.github/workflows/test-image-build.yml
vendored
|
@ -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:
|
||||
|
|
319
Dockerfile
319
Dockerfile
|
@ -1,112 +1,257 @@
|
|||
# syntax=docker/dockerfile:1.4
|
||||
# This needs to be bookworm-slim because the Ruby image is built on bookworm-slim
|
||||
ARG NODE_VERSION="20.9-bookworm-slim"
|
||||
|
||||
FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim as ruby
|
||||
FROM node:${NODE_VERSION} as build
|
||||
# Please see https://docs.docker.com/engine/reference/builder for information about
|
||||
# the extended buildx capabilities used in this file.
|
||||
# Make sure multiarch TARGETPLATFORM is available for interpolation
|
||||
# See: https://docs.docker.com/build/building/multi-platform/
|
||||
ARG TARGETPLATFORM=${TARGETPLATFORM}
|
||||
ARG BUILDPLATFORM=${BUILDPLATFORM}
|
||||
|
||||
COPY --link --from=ruby /opt/ruby /opt/ruby
|
||||
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.2.2"]
|
||||
ARG RUBY_VERSION="3.2.2"
|
||||
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
|
||||
ARG NODE_MAJOR_VERSION="20"
|
||||
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
|
||||
ARG DEBIAN_VERSION="bookworm"
|
||||
# Node image to use for base image based on combined variables (ex: 20-bookworm-slim)
|
||||
FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim as node
|
||||
# Ruby image to use for base image based on combined variables (ex: 3.2.2-slim-bookworm)
|
||||
FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby
|
||||
|
||||
ENV DEBIAN_FRONTEND="noninteractive" \
|
||||
PATH="${PATH}:/opt/ruby/bin"
|
||||
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
|
||||
# Example: v4.2.0-nightly.2023.11.09+something
|
||||
# Overwrite existance of 'alpha.0' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"]
|
||||
ARG MASTODON_VERSION_PRERELEASE=""
|
||||
# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="something"]
|
||||
ARG MASTODON_VERSION_METADATA=""
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
# Allow Ruby on Rails to serve static files
|
||||
# See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files
|
||||
ARG RAILS_SERVE_STATIC_FILES="true"
|
||||
# Allow to use YJIT compiler
|
||||
# See: https://github.com/ruby/ruby/blob/master/doc/yjit/yjit.md
|
||||
ARG RUBY_YJIT_ENABLE="1"
|
||||
# Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin]
|
||||
ARG TZ="Etc/UTC"
|
||||
# Linux UID (user id) for the mastodon user, change with [--build-arg UID=1234]
|
||||
ARG UID="991"
|
||||
# Linux GID (group id) for the mastodon user, change with [--build-arg GID=1234]
|
||||
ARG GID="991"
|
||||
|
||||
# Apply Mastodon build options based on options above
|
||||
ENV \
|
||||
# Apply Mastodon version information
|
||||
MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \
|
||||
MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}" \
|
||||
# Apply Mastodon static files and YJIT options
|
||||
RAILS_SERVE_STATIC_FILES=${RAILS_SERVE_STATIC_FILES} \
|
||||
RUBY_YJIT_ENABLE=${RUBY_YJIT_ENABLE} \
|
||||
# Apply timezone
|
||||
TZ=${TZ}
|
||||
|
||||
ENV \
|
||||
# Configure the IP to bind Mastodon to when serving traffic
|
||||
BIND="0.0.0.0" \
|
||||
# Use production settings for Yarn, Node and related nodejs based tools
|
||||
NODE_ENV="production" \
|
||||
# Use production settings for Ruby on Rails
|
||||
RAILS_ENV="production" \
|
||||
# Add Ruby and Mastodon installation to the PATH
|
||||
DEBIAN_FRONTEND="noninteractive" \
|
||||
PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" \
|
||||
# Optimize jemalloc 5.x performance
|
||||
MALLOC_CONF="narenas:2,background_thread:true,thp:never,dirty_decay_ms:1000,muzzy_decay_ms:0"
|
||||
|
||||
# Set default shell used for running commands
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-c"]
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
RUN echo "Target platform is $TARGETPLATFORM"
|
||||
|
||||
RUN \
|
||||
# Remove automatic apt cache Docker cleanup scripts
|
||||
rm -f /etc/apt/apt.conf.d/docker-clean; \
|
||||
# Sets timezone
|
||||
echo "${TZ}" > /etc/localtime; \
|
||||
# Creates mastodon user/group and sets home directory
|
||||
groupadd -g "${GID}" mastodon; \
|
||||
useradd -l -u "${UID}" -g "${GID}" -m -d /opt/mastodon mastodon; \
|
||||
# Creates /mastodon symlink to /opt/mastodon
|
||||
ln -s /opt/mastodon /mastodon;
|
||||
|
||||
# Set /opt/mastodon as working directory
|
||||
WORKDIR /opt/mastodon
|
||||
|
||||
# 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
|
||||
# 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 \
|
||||
;
|
||||
|
||||
COPY Gemfile* package.json yarn.lock .yarnrc.yml /opt/mastodon/
|
||||
# Create temporary build layer from base image
|
||||
FROM ruby as build
|
||||
|
||||
# Copy Node package configuration files into working directory
|
||||
COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/
|
||||
COPY .yarn /opt/mastodon/.yarn
|
||||
|
||||
COPY --from=node /usr/local/bin /usr/local/bin
|
||||
COPY --from=node /usr/local/lib /usr/local/lib
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# hadolint ignore=DL3008
|
||||
RUN \
|
||||
# Mount Apt cache and lib directories from Docker buildx caches
|
||||
--mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
|
||||
# Install build tools and bundler dependencies from APT
|
||||
apt-get install -y --no-install-recommends \
|
||||
g++ \
|
||||
gcc \
|
||||
git \
|
||||
libgdbm-dev \
|
||||
libgmp-dev \
|
||||
libicu-dev \
|
||||
libidn-dev \
|
||||
libpq-dev \
|
||||
libssl-dev \
|
||||
make \
|
||||
shared-mime-info \
|
||||
zlib1g-dev \
|
||||
;
|
||||
|
||||
RUN \
|
||||
# Configure Corepack
|
||||
rm /usr/local/bin/yarn*; \
|
||||
corepack enable; \
|
||||
corepack prepare --activate;
|
||||
|
||||
# Create temporary bundler specific build layer from build layer
|
||||
FROM build as bundler
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# Copy Gemfile config into working directory
|
||||
COPY Gemfile* /opt/mastodon/
|
||||
|
||||
RUN \
|
||||
# Mount Ruby Gem caches
|
||||
--mount=type=cache,id=gem-cache-${TARGETPLATFORM},target=/usr/local/bundle/cache/,sharing=locked \
|
||||
# Configure bundle to prevent changes to Gemfile and Gemfile.lock
|
||||
bundle config set --global frozen "true"; \
|
||||
# Configure bundle to not cache downloaded Gems
|
||||
bundle config set --global cache_all "false"; \
|
||||
# Configure bundle to only process production Gems
|
||||
bundle config set --local without "development test"; \
|
||||
# Configure bundle to not warn about root user
|
||||
bundle config set silence_root_warning "true"; \
|
||||
# Download and install required Gems
|
||||
bundle install -j"$(nproc)";
|
||||
|
||||
# Create temporary node specific build layer from build layer
|
||||
FROM build as yarn
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# Copy Node package configuration files into working directory
|
||||
COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/
|
||||
COPY streaming/package.json /opt/mastodon/streaming/
|
||||
COPY .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
|
||||
RUN \
|
||||
# Use Ruby on Rails to create Mastodon assets
|
||||
OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder bundle exec rails assets:precompile; \
|
||||
# Cleanup temporary files
|
||||
rm -fr /opt/mastodon/tmp;
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
# Prep final Mastodon Ruby layer
|
||||
FROM ruby as mastodon
|
||||
|
||||
ENV DEBIAN_FRONTEND="noninteractive" \
|
||||
PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin"
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# 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
|
||||
# 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 \
|
||||
;
|
||||
|
||||
# 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 Mastodon sources into final layer
|
||||
COPY . /opt/mastodon/
|
||||
|
||||
COPY --chown=mastodon:mastodon . /opt/mastodon
|
||||
COPY --chown=mastodon:mastodon --from=build /opt/mastodon /opt/mastodon
|
||||
# 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/
|
||||
|
||||
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}"
|
||||
RUN \
|
||||
# Precompile bootsnap code for faster Rails startup
|
||||
bundle exec bootsnap precompile --gemfile app/ lib/;
|
||||
|
||||
# Set the run user
|
||||
RUN \
|
||||
# Pre-create and chown system volume to Mastodon user
|
||||
mkdir -p /opt/mastodon/public/system; \
|
||||
chown mastodon:mastodon /opt/mastodon/public/system;
|
||||
|
||||
# Set the running user for resulting container
|
||||
USER mastodon
|
||||
WORKDIR /opt/mastodon
|
||||
|
||||
# Precompile assets
|
||||
RUN OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder rails assets:precompile
|
||||
|
||||
# Set the work dir and the container entry point
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
EXPOSE 3000 4000
|
||||
# Expose default Puma ports
|
||||
EXPOSE 3000
|
||||
# Set container tini as default entry point
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
|
@ -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
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
|
||||
|
|
12
app/helpers/admin/account_actions_helper.rb
Normal file
12
app/helpers/admin/account_actions_helper.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin::AccountActionsHelper
|
||||
def account_action_type_label(type)
|
||||
safe_join(
|
||||
[
|
||||
I18n.t("simple_form.labels.admin_account_action.types.#{type}"),
|
||||
content_tag(:span, I18n.t("simple_form.hints.admin_account_action.types.#{type}"), class: 'hint'),
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
19
app/helpers/admin/accounts_helper.rb
Normal file
19
app/helpers/admin/accounts_helper.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin::AccountsHelper
|
||||
def admin_accounts_moderation_options
|
||||
[
|
||||
[t('admin.accounts.moderation.active'), 'active'],
|
||||
[t('admin.accounts.moderation.silenced'), 'silenced'],
|
||||
[t('admin.accounts.moderation.disabled'), 'disabled'],
|
||||
[t('admin.accounts.moderation.suspended'), 'suspended'],
|
||||
[safe_join([t('admin.accounts.moderation.pending'), "(#{pending_user_count_label})"], ' '), 'pending'],
|
||||
]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def pending_user_count_label
|
||||
number_with_delimiter User.pending.count
|
||||
end
|
||||
end
|
12
app/helpers/admin/ip_blocks_helper.rb
Normal file
12
app/helpers/admin/ip_blocks_helper.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin::IpBlocksHelper
|
||||
def ip_blocks_severity_label(severity)
|
||||
safe_join(
|
||||
[
|
||||
I18n.t("simple_form.labels.ip_block.severities.#{severity}"),
|
||||
content_tag(:span, I18n.t("simple_form.hints.ip_block.severities.#{severity}"), class: 'hint'),
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
24
app/helpers/admin/roles_helper.rb
Normal file
24
app/helpers/admin/roles_helper.rb
Normal file
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
module RolesHelper
|
||||
def privilege_label(privilege)
|
||||
safe_join(
|
||||
[
|
||||
t("admin.roles.privileges.#{privilege}"),
|
||||
content_tag(:span, t("admin.roles.privileges.#{privilege}_description"), class: 'hint'),
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
def disable_permissions?(permissions)
|
||||
permissions.filter { |privilege| role_flag_value(privilege).zero? }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def role_flag_value(privilege)
|
||||
UserRole::FLAGS[privilege] & current_user.role.computed_permissions
|
||||
end
|
||||
end
|
||||
end
|
15
app/helpers/admin/settings/discovery_helper.rb
Normal file
15
app/helpers/admin/settings/discovery_helper.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin::Settings::DiscoveryHelper
|
||||
def discovery_warning_hint_text
|
||||
authorized_fetch_overridden? ? t('admin.settings.security.authorized_fetch_overridden_hint') : nil
|
||||
end
|
||||
|
||||
def discovery_hint_text
|
||||
t('admin.settings.security.authorized_fetch_hint')
|
||||
end
|
||||
|
||||
def discovery_recommended_value
|
||||
authorized_fetch_overridden? ? :overridden : nil
|
||||
end
|
||||
end
|
12
app/helpers/filters_helper.rb
Normal file
12
app/helpers/filters_helper.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module FiltersHelper
|
||||
def filter_action_label(action)
|
||||
safe_join(
|
||||
[
|
||||
t("simple_form.labels.filters.actions.#{action}"),
|
||||
content_tag(:span, t("simple_form.hints.filters.actions.#{action}"), class: 'hint'),
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
|
@ -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;
|
145
app/javascript/mastodon/components/hashtag.tsx
Normal file
145
app/javascript/mastodon/components/hashtag.tsx
Normal file
|
@ -0,0 +1,145 @@
|
|||
import type { JSX } from 'react';
|
||||
import { Component } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import type Immutable from 'immutable';
|
||||
|
||||
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
||||
|
||||
import { ShortNumber } from 'mastodon/components/short_number';
|
||||
import { Skeleton } from 'mastodon/components/skeleton';
|
||||
|
||||
interface SilentErrorBoundaryProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
class SilentErrorBoundary extends Component<SilentErrorBoundaryProps> {
|
||||
state = {
|
||||
error: false,
|
||||
};
|
||||
|
||||
componentDidCatch() {
|
||||
this.setState({ error: true });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to render counter of how much people are talking about hashtag
|
||||
* @param displayNumber Counter number to display
|
||||
* @param pluralReady Whether the count is plural
|
||||
* @returns Formatted counter of how much people are talking about hashtag
|
||||
*/
|
||||
export const accountsCountRenderer = (
|
||||
displayNumber: JSX.Element,
|
||||
pluralReady: number,
|
||||
) => (
|
||||
<FormattedMessage
|
||||
id='trends.counter_by_accounts'
|
||||
defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}'
|
||||
values={{
|
||||
count: pluralReady,
|
||||
counter: <strong>{displayNumber}</strong>,
|
||||
days: 2,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
interface ImmutableHashtagProps {
|
||||
hashtag: Immutable.Map<string, unknown>;
|
||||
}
|
||||
|
||||
export const ImmutableHashtag = ({ hashtag }: ImmutableHashtagProps) => (
|
||||
<Hashtag
|
||||
name={hashtag.get('name') as string}
|
||||
to={`/tags/${hashtag.get('name') as string}`}
|
||||
people={
|
||||
(hashtag.getIn(['history', 0, 'accounts']) as number) * 1 +
|
||||
(hashtag.getIn(['history', 1, 'accounts']) as number) * 1
|
||||
}
|
||||
history={(
|
||||
hashtag.get('history') as Immutable.Collection.Indexed<
|
||||
Immutable.Map<string, number>
|
||||
>
|
||||
)
|
||||
.reverse()
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
.map((day) => day.get('uses')!)
|
||||
.toArray()}
|
||||
/>
|
||||
);
|
||||
|
||||
export interface HashtagProps {
|
||||
className?: string;
|
||||
description?: React.ReactNode;
|
||||
history?: number[];
|
||||
name: string;
|
||||
people: number;
|
||||
to: string;
|
||||
uses?: number;
|
||||
withGraph?: boolean;
|
||||
}
|
||||
|
||||
export const Hashtag: React.FC<HashtagProps> = ({
|
||||
name,
|
||||
to,
|
||||
people,
|
||||
uses,
|
||||
history,
|
||||
className,
|
||||
description,
|
||||
withGraph = true,
|
||||
}) => (
|
||||
<div className={classNames('trends__item', className)}>
|
||||
<div className='trends__item__name'>
|
||||
<Link to={to}>
|
||||
{name ? (
|
||||
<>
|
||||
#<span>{name}</span>
|
||||
</>
|
||||
) : (
|
||||
<Skeleton width={50} />
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{description ? (
|
||||
<span>{description}</span>
|
||||
) : typeof people !== 'undefined' ? (
|
||||
<ShortNumber value={people} renderer={accountsCountRenderer} />
|
||||
) : (
|
||||
<Skeleton width={100} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{typeof uses !== 'undefined' && (
|
||||
<div className='trends__item__current'>
|
||||
<ShortNumber value={uses} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{withGraph && (
|
||||
<div className='trends__item__sparkline'>
|
||||
<SilentErrorBoundary>
|
||||
<Sparklines
|
||||
width={50}
|
||||
height={28}
|
||||
data={history ? history : Array.from(Array(7)).map(() => 0)}
|
||||
>
|
||||
<SparklinesCurve style={{ fill: 'none' }} />
|
||||
</Sparklines>
|
||||
</SilentErrorBoundary>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
|
@ -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 || '';
|
||||
}
|
||||
});
|
13
app/javascript/mastodon/utils/config.ts
Normal file
13
app/javascript/mastodon/utils/config.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import ready from '../ready';
|
||||
|
||||
export let assetHost = '';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
ready(() => {
|
||||
const cdnHost = document.querySelector<HTMLMetaElement>(
|
||||
'meta[name=cdn-host]',
|
||||
);
|
||||
if (cdnHost) {
|
||||
assetHost = cdnHost.content || '';
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
};
|
9
app/javascript/mastodon/utils/html.ts
Normal file
9
app/javascript/mastodon/utils/html.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
// NB: This function can still return unsafe HTML
|
||||
export const unescapeHTML = (html: string) => {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = html
|
||||
.replace(/<br\s*\/?>/g, '\n')
|
||||
.replace(/<\/p><p>/g, '\n\n')
|
||||
.replace(/<[^>]*>/g, '');
|
||||
return wrapper.textContent;
|
||||
};
|
|
@ -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));
|
||||
}
|
||||
};
|
13
app/javascript/mastodon/utils/notifications.ts
Normal file
13
app/javascript/mastodon/utils/notifications.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Tries Notification.requestPermission, console warning instead of rejecting on error.
|
||||
* @param callback Runs with the permission result on completion.
|
||||
*/
|
||||
export const requestNotificationPermission = async (
|
||||
callback: NotificationPermissionCallback,
|
||||
) => {
|
||||
try {
|
||||
callback(await Notification.requestPermission());
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
};
|
|
@ -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 :local, -> { where(remote_url: '') }
|
||||
scope :remote, -> { where.not(remote_url: '') }
|
||||
scope :cached, -> { remote.where.not(file_file_name: nil) }
|
||||
|
||||
default_scope { order(id: :asc) }
|
||||
scope :local, -> { where(remote_url: '') }
|
||||
scope :ordered, -> { order(id: :asc) }
|
||||
scope :remote, -> { where.not(remote_url: '') }
|
||||
scope :unattached, -> { where(status_id: nil, scheduled_status_id: nil) }
|
||||
|
||||
attr_accessor :skip_download
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
74
lib/mastodon/cli/federation.rb
Normal file
74
lib/mastodon/cli/federation.rb
Normal file
|
@ -0,0 +1,74 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'tty-prompt'
|
||||
|
||||
module Mastodon::CLI
|
||||
module Federation
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
desc 'self-destruct', 'Erase the server from the federation'
|
||||
long_desc <<~LONG_DESC
|
||||
Erase the server from the federation by broadcasting account delete
|
||||
activities to all known other servers. This allows a "clean exit" from
|
||||
running a Mastodon server, as it leaves next to no cache behind on
|
||||
other servers.
|
||||
|
||||
This command is always interactive and requires confirmation twice.
|
||||
|
||||
No local data is actually deleted, because emptying the
|
||||
database or removing files is much faster through other, external
|
||||
means, such as e.g. deleting the entire VPS. However, because other
|
||||
servers will delete data about local users, but no local data will be
|
||||
updated (such as e.g. followers), there will be a state mismatch
|
||||
that will lead to glitches and issues if you then continue to run and use
|
||||
the server.
|
||||
|
||||
So either you know exactly what you are doing, or you are starting
|
||||
from a blank slate afterwards by manually clearing out all the local
|
||||
data!
|
||||
LONG_DESC
|
||||
def self_destruct
|
||||
if SelfDestructHelper.self_destruct?
|
||||
prompt.ok('Self-destruct mode is already enabled for this Mastodon server')
|
||||
|
||||
pending_accounts = Account.local.without_suspended.count + Account.local.suspended.joins(:deletion_request).count
|
||||
sidekiq_stats = Sidekiq::Stats.new
|
||||
|
||||
if pending_accounts.positive?
|
||||
prompt.warn("#{pending_accounts} accounts are still pending deletion.")
|
||||
elsif sidekiq_stats.enqueued.positive?
|
||||
prompt.warn('Deletion notices are still being processed')
|
||||
elsif sidekiq_stats.retry_size.positive?
|
||||
prompt.warn('At least one delivery attempt for each deletion notice has been made, but some have failed and are scheduled for retry')
|
||||
else
|
||||
prompt.ok('Every deletion notice has been sent! You can safely delete all data and decomission your servers!')
|
||||
end
|
||||
|
||||
exit(0)
|
||||
end
|
||||
|
||||
exit(1) unless prompt.ask('Type in the domain of the server to confirm:', required: true) == Rails.configuration.x.local_domain
|
||||
|
||||
prompt.warn('This operation WILL NOT be reversible.')
|
||||
prompt.warn('While the data won\'t be erased locally, the server will be in a BROKEN STATE afterwards.')
|
||||
prompt.warn('The deletion process itself may take a long time, and will be handled by Sidekiq, so do not shut it down until it has finished (you will be able to re-run this command to see the state of the self-destruct process).')
|
||||
|
||||
exit(1) if prompt.no?('Are you sure you want to proceed?')
|
||||
|
||||
self_destruct_value = Rails.application.message_verifier('self-destruct').generate(Rails.configuration.x.local_domain)
|
||||
prompt.ok('To switch Mastodon to self-destruct mode, add the following variable to your evironment (e.g. by adding a line to your `.env.production`) and restart all Mastodon processes:')
|
||||
prompt.ok(" SELF_DESTRUCT=#{self_destruct_value}")
|
||||
prompt.ok("\nYou can re-run this command to see the state of the self-destruct process.")
|
||||
rescue TTY::Reader::InputInterrupt
|
||||
exit(1)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def prompt
|
||||
@prompt ||= TTY::Prompt.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -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' }
|
||||
context 'when signed in' do
|
||||
before { sign_in user, scope: :user }
|
||||
|
||||
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
|
||||
|
||||
include_examples 'renders :new'
|
||||
end
|
||||
|
||||
include_examples 'renders :new'
|
||||
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 }
|
||||
|
||||
it 'redirects if not signed in' do
|
||||
get :new
|
||||
expect(response).to redirect_to('/auth/sign_in')
|
||||
end
|
||||
|
||||
it 'redirects if a new otp_secret has not been set in the session' do
|
||||
sign_in user, scope: :user
|
||||
get :new, session: { challenge_passed_at: Time.now.utc }
|
||||
expect(response).to redirect_to('/settings/otp_authentication')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #create' do
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in user, scope: :user
|
||||
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 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 })
|
||||
expect { post_create_with_options }
|
||||
.to(not_change { user.reload.otp_secret })
|
||||
end
|
||||
|
||||
it 'renders the new view' do
|
||||
before do
|
||||
prepare_user_otp_consumption_response(false)
|
||||
allow(controller).to receive(:current_user).and_return(user)
|
||||
end
|
||||
|
||||
it 'renders page with error message' do
|
||||
subject
|
||||
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')
|
||||
|
|
7
streaming/.dockerignore
Normal file
7
streaming/.dockerignore
Normal file
|
@ -0,0 +1,7 @@
|
|||
.env
|
||||
.env.*
|
||||
.gitignore
|
||||
node_modules
|
||||
.DS_Store
|
||||
*.swp
|
||||
*~
|
32
streaming/.eslintrc.js
Normal file
32
streaming/.eslintrc.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
// @ts-check
|
||||
const { defineConfig } = require('eslint-define-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
extends: ['../.eslintrc.js'],
|
||||
env: {
|
||||
browser: false,
|
||||
},
|
||||
parserOptions: {
|
||||
project: true,
|
||||
tsconfigRootDir: __dirname,
|
||||
ecmaFeatures: {
|
||||
jsx: false,
|
||||
},
|
||||
ecmaVersion: 2021,
|
||||
},
|
||||
rules: {
|
||||
'import/no-commonjs': 'off',
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
{
|
||||
devDependencies: [
|
||||
'streaming/.eslintrc.js',
|
||||
],
|
||||
optionalDependencies: false,
|
||||
peerDependencies: false,
|
||||
includeTypes: true,
|
||||
packageDir: __dirname,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
104
streaming/Dockerfile
Normal file
104
streaming/Dockerfile
Normal file
|
@ -0,0 +1,104 @@
|
|||
# syntax=docker/dockerfile:1.4
|
||||
|
||||
# Please see https://docs.docker.com/engine/reference/builder for information about
|
||||
# the extended buildx capabilities used in this file.
|
||||
# Make sure multiarch TARGETPLATFORM is available for interpolation
|
||||
# See: https://docs.docker.com/build/building/multi-platform/
|
||||
ARG TARGETPLATFORM=${TARGETPLATFORM}
|
||||
ARG BUILDPLATFORM=${BUILDPLATFORM}
|
||||
|
||||
# Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
|
||||
ARG NODE_MAJOR_VERSION="20"
|
||||
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
|
||||
ARG DEBIAN_VERSION="bookworm"
|
||||
# Node image to use for base image based on combined variables (ex: 20-bookworm-slim)
|
||||
FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim as streaming
|
||||
|
||||
# Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin]
|
||||
ARG TZ="Etc/UTC"
|
||||
# Linux UID (user id) for the mastodon user, change with [--build-arg UID=1234]
|
||||
ARG UID="991"
|
||||
# Linux GID (group id) for the mastodon user, change with [--build-arg GID=1234]
|
||||
ARG GID="991"
|
||||
|
||||
# Apply Mastodon build options based on options above
|
||||
ENV \
|
||||
# Apply Mastodon version information
|
||||
MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \
|
||||
MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}" \
|
||||
# Apply timezone
|
||||
TZ=${TZ}
|
||||
|
||||
ENV \
|
||||
# Configure the IP to bind Mastodon to when serving traffic
|
||||
BIND="0.0.0.0" \
|
||||
# Explicitly set PORT to match the exposed port
|
||||
PORT=4000 \
|
||||
# Use production settings for Yarn, Node and related nodejs based tools
|
||||
NODE_ENV="production" \
|
||||
# Add Ruby and Mastodon installation to the PATH
|
||||
DEBIAN_FRONTEND="noninteractive"
|
||||
|
||||
# Set default shell used for running commands
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-c"]
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
RUN echo "Target platform is ${TARGETPLATFORM}"
|
||||
|
||||
RUN \
|
||||
# Remove automatic apt cache Docker cleanup scripts
|
||||
rm -f /etc/apt/apt.conf.d/docker-clean; \
|
||||
# Sets timezone
|
||||
echo "${TZ}" > /etc/localtime; \
|
||||
# Creates mastodon user/group and sets home directory
|
||||
groupadd -g "${GID}" mastodon; \
|
||||
useradd -l -u "${UID}" -g "${GID}" -m -d /opt/mastodon mastodon; \
|
||||
# Creates symlink for /mastodon folder
|
||||
ln -s /opt/mastodon /mastodon;
|
||||
|
||||
# hadolint ignore=DL3008,DL3005
|
||||
RUN \
|
||||
# Mount Apt cache and lib directories from Docker buildx caches
|
||||
--mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
|
||||
# Upgrade to check for security updates to Debian image
|
||||
apt-get update; \
|
||||
apt-get dist-upgrade -yq; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
tzdata \
|
||||
;
|
||||
|
||||
# Set /opt/mastodon as working directory
|
||||
WORKDIR /opt/mastodon
|
||||
|
||||
# Copy Node package configuration files from build system to container
|
||||
COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/
|
||||
COPY .yarn /opt/mastodon/.yarn
|
||||
# Copy Streaming source code from build system to container
|
||||
COPY ./streaming /opt/mastodon/streaming
|
||||
|
||||
RUN \
|
||||
# Mount local Corepack and Yarn caches from Docker buildx caches
|
||||
--mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \
|
||||
--mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \
|
||||
# Configure Corepack
|
||||
rm /usr/local/bin/yarn*; \
|
||||
corepack enable; \
|
||||
corepack prepare --activate;
|
||||
|
||||
RUN \
|
||||
# Mount Corepack and Yarn caches from Docker buildx caches
|
||||
--mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \
|
||||
--mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \
|
||||
# Install Node packages
|
||||
yarn workspaces focus --production @mastodon/streaming;
|
||||
|
||||
# Set the running user for resulting container
|
||||
USER mastodon
|
||||
# Expose default Streaming ports
|
||||
EXPOSE 4000
|
||||
# Run streaming when started
|
||||
CMD [ node ./streaming/index.js ]
|
|
@ -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
|
||||
|
|
105
streaming/metrics.js
Normal file
105
streaming/metrics.js
Normal file
|
@ -0,0 +1,105 @@
|
|||
// @ts-check
|
||||
|
||||
const metrics = require('prom-client');
|
||||
|
||||
/**
|
||||
* @typedef StreamingMetrics
|
||||
* @property {metrics.Registry} register
|
||||
* @property {metrics.Gauge<"type">} connectedClients
|
||||
* @property {metrics.Gauge<"type" | "channel">} connectedChannels
|
||||
* @property {metrics.Gauge} redisSubscriptions
|
||||
* @property {metrics.Counter} redisMessagesReceived
|
||||
* @property {metrics.Counter<"type">} messagesSent
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string[]} channels
|
||||
* @param {import('pg').Pool} pgPool
|
||||
* @returns {StreamingMetrics}
|
||||
*/
|
||||
function setupMetrics(channels, pgPool) {
|
||||
// Collect metrics from Node.js
|
||||
metrics.collectDefaultMetrics();
|
||||
|
||||
new metrics.Gauge({
|
||||
name: 'pg_pool_total_connections',
|
||||
help: 'The total number of clients existing within the pool',
|
||||
collect() {
|
||||
this.set(pgPool.totalCount);
|
||||
},
|
||||
});
|
||||
|
||||
new metrics.Gauge({
|
||||
name: 'pg_pool_idle_connections',
|
||||
help: 'The number of clients which are not checked out but are currently idle in the pool',
|
||||
collect() {
|
||||
this.set(pgPool.idleCount);
|
||||
},
|
||||
});
|
||||
|
||||
new metrics.Gauge({
|
||||
name: 'pg_pool_waiting_queries',
|
||||
help: 'The number of queued requests waiting on a client when all clients are checked out',
|
||||
collect() {
|
||||
this.set(pgPool.waitingCount);
|
||||
},
|
||||
});
|
||||
|
||||
const connectedClients = new metrics.Gauge({
|
||||
name: 'connected_clients',
|
||||
help: 'The number of clients connected to the streaming server',
|
||||
labelNames: ['type'],
|
||||
});
|
||||
|
||||
const connectedChannels = new metrics.Gauge({
|
||||
name: 'connected_channels',
|
||||
help: 'The number of channels the streaming server is streaming to',
|
||||
labelNames: [ 'type', 'channel' ]
|
||||
});
|
||||
|
||||
const redisSubscriptions = new metrics.Gauge({
|
||||
name: 'redis_subscriptions',
|
||||
help: 'The number of Redis channels the streaming server is subscribed to',
|
||||
});
|
||||
|
||||
const redisMessagesReceived = new metrics.Counter({
|
||||
name: 'redis_messages_received_total',
|
||||
help: 'The total number of messages the streaming server has received from redis subscriptions'
|
||||
});
|
||||
|
||||
const messagesSent = new metrics.Counter({
|
||||
name: 'messages_sent_total',
|
||||
help: 'The total number of messages the streaming server sent to clients per connection type',
|
||||
labelNames: [ 'type' ]
|
||||
});
|
||||
|
||||
// Prime the gauges so we don't loose metrics between restarts:
|
||||
redisSubscriptions.set(0);
|
||||
connectedClients.set({ type: 'websocket' }, 0);
|
||||
connectedClients.set({ type: 'eventsource' }, 0);
|
||||
|
||||
// For each channel, initialize the gauges at zero; There's only a finite set of channels available
|
||||
channels.forEach(( channel ) => {
|
||||
connectedChannels.set({ type: 'websocket', channel }, 0);
|
||||
connectedChannels.set({ type: 'eventsource', channel }, 0);
|
||||
});
|
||||
|
||||
// Prime the counters so that we don't loose metrics between restarts.
|
||||
// Unfortunately counters don't support the set() API, so instead I'm using
|
||||
// inc(0) to achieve the same result.
|
||||
redisMessagesReceived.inc(0);
|
||||
messagesSent.inc({ type: 'websocket' }, 0);
|
||||
messagesSent.inc({ type: 'eventsource' }, 0);
|
||||
|
||||
return {
|
||||
register: metrics.register,
|
||||
connectedClients,
|
||||
connectedChannels,
|
||||
redisSubscriptions,
|
||||
redisMessagesReceived,
|
||||
messagesSent,
|
||||
};
|
||||
}
|
||||
|
||||
exports.setupMetrics = setupMetrics;
|
|
@ -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",
|
||||
|
|
11
streaming/tsconfig.json
Normal file
11
streaming/tsconfig.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"noUnusedParameters": false,
|
||||
"paths": {}
|
||||
},
|
||||
"include": ["./*.js", "./.eslintrc.js"]
|
||||
}
|
22
streaming/utils.js
Normal file
22
streaming/utils.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
// @ts-check
|
||||
|
||||
const FALSE_VALUES = [
|
||||
false,
|
||||
0,
|
||||
'0',
|
||||
'f',
|
||||
'F',
|
||||
'false',
|
||||
'FALSE',
|
||||
'off',
|
||||
'OFF',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param {any} value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isTruthy = value =>
|
||||
value && !FALSE_VALUES.includes(value);
|
||||
|
||||
exports.isTruthy = isTruthy;
|
302
yarn.lock
302
yarn.lock
|
@ -42,55 +42,55 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.13":
|
||||
version: 7.22.13
|
||||
resolution: "@babel/code-frame@npm:7.22.13"
|
||||
"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.23.5":
|
||||
version: 7.23.5
|
||||
resolution: "@babel/code-frame@npm:7.23.5"
|
||||
dependencies:
|
||||
"@babel/highlight": "npm:^7.22.13"
|
||||
"@babel/highlight": "npm:^7.23.4"
|
||||
chalk: "npm:^2.4.2"
|
||||
checksum: f4cc8ae1000265677daf4845083b72f88d00d311adb1a93c94eb4b07bf0ed6828a81ae4ac43ee7d476775000b93a28a9cddec18fbdc5796212d8dcccd5de72bd
|
||||
checksum: a10e843595ddd9f97faa99917414813c06214f4d9205294013e20c70fbdf4f943760da37dec1d998bf3e6fc20fa2918a47c0e987a7e458663feb7698063ad7c6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.22.9, @babel/compat-data@npm:^7.23.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/compat-data@npm:7.23.3"
|
||||
checksum: c6af331753c34ee8a5678bc94404320826cb56b1dda3efc1311ec8fb0774e78225132f3c1acc988440ace667f14a838e297a822692b95758aa63da406e1f97a1
|
||||
"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.22.9, @babel/compat-data@npm:^7.23.3, @babel/compat-data@npm:^7.23.5":
|
||||
version: 7.23.5
|
||||
resolution: "@babel/compat-data@npm:7.23.5"
|
||||
checksum: 081278ed46131a890ad566a59c61600a5f9557bd8ee5e535890c8548192532ea92590742fd74bd9db83d74c669ef8a04a7e1c85cdea27f960233e3b83c3a957c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/core@npm:^7.10.4, @babel/core@npm:^7.11.1, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.22.1":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/core@npm:7.23.3"
|
||||
version: 7.23.5
|
||||
resolution: "@babel/core@npm:7.23.5"
|
||||
dependencies:
|
||||
"@ampproject/remapping": "npm:^2.2.0"
|
||||
"@babel/code-frame": "npm:^7.22.13"
|
||||
"@babel/generator": "npm:^7.23.3"
|
||||
"@babel/code-frame": "npm:^7.23.5"
|
||||
"@babel/generator": "npm:^7.23.5"
|
||||
"@babel/helper-compilation-targets": "npm:^7.22.15"
|
||||
"@babel/helper-module-transforms": "npm:^7.23.3"
|
||||
"@babel/helpers": "npm:^7.23.2"
|
||||
"@babel/parser": "npm:^7.23.3"
|
||||
"@babel/helpers": "npm:^7.23.5"
|
||||
"@babel/parser": "npm:^7.23.5"
|
||||
"@babel/template": "npm:^7.22.15"
|
||||
"@babel/traverse": "npm:^7.23.3"
|
||||
"@babel/types": "npm:^7.23.3"
|
||||
"@babel/traverse": "npm:^7.23.5"
|
||||
"@babel/types": "npm:^7.23.5"
|
||||
convert-source-map: "npm:^2.0.0"
|
||||
debug: "npm:^4.1.0"
|
||||
gensync: "npm:^1.0.0-beta.2"
|
||||
json5: "npm:^2.2.3"
|
||||
semver: "npm:^6.3.1"
|
||||
checksum: 08d43b749e24052d12713a7fb1f0c0d1275d4fb056d00846faeb8da79ecf6d0ba91a11b6afec407b8b0f9388d00e2c2f485f282bef0ade4d6d0a17de191a4287
|
||||
checksum: 311a512a870ee330a3f9a7ea89e5df790b2b5af0b1bd98b10b4edc0de2ac440f0df4d69ea2c0ee38a4b89041b9a495802741d93603be7d4fd834ec8bb6970bd2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/generator@npm:^7.23.3, @babel/generator@npm:^7.7.2":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/generator@npm:7.23.3"
|
||||
"@babel/generator@npm:^7.23.5, @babel/generator@npm:^7.7.2":
|
||||
version: 7.23.5
|
||||
resolution: "@babel/generator@npm:7.23.5"
|
||||
dependencies:
|
||||
"@babel/types": "npm:^7.23.3"
|
||||
"@babel/types": "npm:^7.23.5"
|
||||
"@jridgewell/gen-mapping": "npm:^0.3.2"
|
||||
"@jridgewell/trace-mapping": "npm:^0.3.17"
|
||||
jsesc: "npm:^2.5.1"
|
||||
checksum: d5fff1417eecfada040e01a7c77a4968e81c436aeb35815ce85b4e80cd01e731423613d61033044a6cb5563bb8449ee260e3379b63eb50b38ec0a9ea9c00abfd
|
||||
checksum: 14c6e874f796c4368e919bed6003bb0adc3ce837760b08f9e646d20aeb5ae7d309723ce6e4f06bcb4a2b5753145446c8e4425851380f695e40e71e1760f49e7b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -310,10 +310,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-string-parser@npm:^7.22.5":
|
||||
version: 7.22.5
|
||||
resolution: "@babel/helper-string-parser@npm:7.22.5"
|
||||
checksum: 6b0ff8af724377ec41e5587fffa7605198da74cb8e7d8d48a36826df0c0ba210eb9fedb3d9bef4d541156e0bd11040f021945a6cbb731ccec4aefb4affa17aa4
|
||||
"@babel/helper-string-parser@npm:^7.23.4":
|
||||
version: 7.23.4
|
||||
resolution: "@babel/helper-string-parser@npm:7.23.4"
|
||||
checksum: f348d5637ad70b6b54b026d6544bd9040f78d24e7ec245a0fc42293968181f6ae9879c22d89744730d246ce8ec53588f716f102addd4df8bbc79b73ea10004ac
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -324,10 +324,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-validator-option@npm:^7.22.15":
|
||||
version: 7.22.15
|
||||
resolution: "@babel/helper-validator-option@npm:7.22.15"
|
||||
checksum: e9661bf80ba18e2dd978217b350fb07298e57ac417f4f1ab9fa011505e20e4857f2c3b4b538473516a9dc03af5ce3a831e5ed973311c28326f4c330b6be981c2
|
||||
"@babel/helper-validator-option@npm:^7.22.15, @babel/helper-validator-option@npm:^7.23.5":
|
||||
version: 7.23.5
|
||||
resolution: "@babel/helper-validator-option@npm:7.23.5"
|
||||
checksum: af45d5c0defb292ba6fd38979e8f13d7da63f9623d8ab9ededc394f67eb45857d2601278d151ae9affb6e03d5d608485806cd45af08b4468a0515cf506510e94
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -342,34 +342,34 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helpers@npm:^7.23.2":
|
||||
version: 7.23.2
|
||||
resolution: "@babel/helpers@npm:7.23.2"
|
||||
"@babel/helpers@npm:^7.23.5":
|
||||
version: 7.23.5
|
||||
resolution: "@babel/helpers@npm:7.23.5"
|
||||
dependencies:
|
||||
"@babel/template": "npm:^7.22.15"
|
||||
"@babel/traverse": "npm:^7.23.2"
|
||||
"@babel/types": "npm:^7.23.0"
|
||||
checksum: 3a6a939c5277a27486e7c626812f0643b35d1c053ac2eb66911f5ae6c0a4e4bcdd40750eba36b766b0ee8a753484287f50ae56232a5f8f2947116723e44b9e35
|
||||
"@babel/traverse": "npm:^7.23.5"
|
||||
"@babel/types": "npm:^7.23.5"
|
||||
checksum: a37e2728eb4378a4888e5d614e28de7dd79b55ac8acbecd0e5c761273e2a02a8f33b34b1932d9069db55417ace2937cbf8ec37c42f1030ce6d228857d7ccaa4f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/highlight@npm:^7.22.13":
|
||||
version: 7.22.20
|
||||
resolution: "@babel/highlight@npm:7.22.20"
|
||||
"@babel/highlight@npm:^7.23.4":
|
||||
version: 7.23.4
|
||||
resolution: "@babel/highlight@npm:7.23.4"
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier": "npm:^7.22.20"
|
||||
chalk: "npm:^2.4.2"
|
||||
js-tokens: "npm:^4.0.0"
|
||||
checksum: f3c3a193afad23434297d88e81d1d6c0c2cf02423de2139ada7ce0a7fc62d8559abf4cc996533c1a9beca7fc990010eb8d544097f75e818ac113bf39ed810aa2
|
||||
checksum: fbff9fcb2f5539289c3c097d130e852afd10d89a3a08ac0b5ebebbc055cc84a4bcc3dcfed463d488cde12dd0902ef1858279e31d7349b2e8cee43913744bda33
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.15, @babel/parser@npm:^7.23.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/parser@npm:7.23.3"
|
||||
"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.15, @babel/parser@npm:^7.23.5":
|
||||
version: 7.23.5
|
||||
resolution: "@babel/parser@npm:7.23.5"
|
||||
bin:
|
||||
parser: ./bin/babel-parser.js
|
||||
checksum: 0fe11eadd4146a9155305b5bfece0f8223a3b1b97357ffa163c0156940de92e76cd0e7a173de819b8692767147e62f33389b312d1537f84cede51092672df6ef
|
||||
checksum: 3356aa90d7bafb4e2c7310e7c2c3d443c4be4db74913f088d3d577a1eb914ea4188e05fd50a47ce907a27b755c4400c4e3cbeee73dbeb37761f6ca85954f5a20
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -661,9 +661,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-async-generator-functions@npm:^7.23.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/plugin-transform-async-generator-functions@npm:7.23.3"
|
||||
"@babel/plugin-transform-async-generator-functions@npm:^7.23.4":
|
||||
version: 7.23.4
|
||||
resolution: "@babel/plugin-transform-async-generator-functions@npm:7.23.4"
|
||||
dependencies:
|
||||
"@babel/helper-environment-visitor": "npm:^7.22.20"
|
||||
"@babel/helper-plugin-utils": "npm:^7.22.5"
|
||||
|
@ -671,7 +671,7 @@ __metadata:
|
|||
"@babel/plugin-syntax-async-generators": "npm:^7.8.4"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: e846f282658e097fce4fccf3ee29289bf05f0654846a5994727a36f0cdc2e47abdffd4be4fa65787e94aa975824fae894c90afbfdc8caacd46c12c7f43e99d7f
|
||||
checksum: f2eef4de609975a3f7da7832576b5ffc93e43c80f87e1a99e886b0f8591096cfc4c37e2d5f52fdeaa2a9c09a25a59f3e621159abaca75d3193922a5c0e4cbe0c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -699,14 +699,14 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-block-scoping@npm:^7.23.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/plugin-transform-block-scoping@npm:7.23.3"
|
||||
"@babel/plugin-transform-block-scoping@npm:^7.23.4":
|
||||
version: 7.23.4
|
||||
resolution: "@babel/plugin-transform-block-scoping@npm:7.23.4"
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils": "npm:^7.22.5"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: ccaeded7954c196811d22a35322579254cda52676e823682b6234885a3aaf88fe0d5152dacaec43db9031dcf35a050a5343e36028e5905b0ba9c02d36b30a57f
|
||||
checksum: 83006804dddf980ab1bcd6d67bc381e24b58c776507c34f990468f820d0da71dba3697355ca4856532fa2eeb2a1e3e73c780f03760b5507a511cbedb0308e276
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -722,22 +722,22 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-class-static-block@npm:^7.23.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/plugin-transform-class-static-block@npm:7.23.3"
|
||||
"@babel/plugin-transform-class-static-block@npm:^7.23.4":
|
||||
version: 7.23.4
|
||||
resolution: "@babel/plugin-transform-class-static-block@npm:7.23.4"
|
||||
dependencies:
|
||||
"@babel/helper-create-class-features-plugin": "npm:^7.22.15"
|
||||
"@babel/helper-plugin-utils": "npm:^7.22.5"
|
||||
"@babel/plugin-syntax-class-static-block": "npm:^7.14.5"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.12.0
|
||||
checksum: 89cdb66d7bc834cd51659eb7286a6bee23add0bc114943d68c4b6c0c834178cf0d55183df0cf508fec9c55ed4155641360e6f55a91c16fe826ccaf1adf381922
|
||||
checksum: fdca96640ef29d8641a7f8de106f65f18871b38cc01c0f7b696d2b49c76b77816b30a812c08e759d06dd10b4d9b3af6b5e4ac22a2017a88c4077972224b77ab0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-classes@npm:^7.23.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/plugin-transform-classes@npm:7.23.3"
|
||||
"@babel/plugin-transform-classes@npm:^7.23.5":
|
||||
version: 7.23.5
|
||||
resolution: "@babel/plugin-transform-classes@npm:7.23.5"
|
||||
dependencies:
|
||||
"@babel/helper-annotate-as-pure": "npm:^7.22.5"
|
||||
"@babel/helper-compilation-targets": "npm:^7.22.15"
|
||||
|
@ -750,7 +750,7 @@ __metadata:
|
|||
globals: "npm:^11.1.0"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 88bfd332db0ba5cbfb8557a2ba5a7185151aebc9cfe3035b014aa6d795556acbe672bb8c78da3c9fd1d23f55a333d14b5daa127ef037f5ced5198b6d79a146d6
|
||||
checksum: 07988f52b4893151887d1ea6ff79e5fe834078c5731bd09babd5659edbbae21ea4e2de326a02443a63fd776b4c945da6177f07875b56fe66e0b7899e830a9e92
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -800,15 +800,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-dynamic-import@npm:^7.23.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/plugin-transform-dynamic-import@npm:7.23.3"
|
||||
"@babel/plugin-transform-dynamic-import@npm:^7.23.4":
|
||||
version: 7.23.4
|
||||
resolution: "@babel/plugin-transform-dynamic-import@npm:7.23.4"
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils": "npm:^7.22.5"
|
||||
"@babel/plugin-syntax-dynamic-import": "npm:^7.8.3"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: df3fd130312dc53d068fa76333991dce5e86987b023af8c3b502bd7d36a8e67da6f718e61dc838576a9fbacd06628e29607ee22d9bae30705485c14130eab201
|
||||
checksum: 19ae4a4a2ca86d35224734c41c48b2aa6a13139f3cfa1cbd18c0e65e461de8b65687dec7e52b7a72bb49db04465394c776aa1b13a2af5dc975b2a0cde3dcab67
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -824,15 +824,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-export-namespace-from@npm:^7.23.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/plugin-transform-export-namespace-from@npm:7.23.3"
|
||||
"@babel/plugin-transform-export-namespace-from@npm:^7.23.4":
|
||||
version: 7.23.4
|
||||
resolution: "@babel/plugin-transform-export-namespace-from@npm:7.23.4"
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils": "npm:^7.22.5"
|
||||
"@babel/plugin-syntax-export-namespace-from": "npm:^7.8.3"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 390c6626dcda99023629049d92090242b4575351a4a7b47f97febabd2381f2cd0f624de661d8de8d1f715fedd63753cfd1feddead19e5960c27b88e447465b81
|
||||
checksum: 38bf04f851e36240bbe83ace4169da626524f4107bfb91f05b4ad93a5fb6a36d5b3d30b8883c1ba575ccfc1bac7938e90ca2e3cb227f7b3f4a9424beec6fd4a7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -860,15 +860,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-json-strings@npm:^7.23.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/plugin-transform-json-strings@npm:7.23.3"
|
||||
"@babel/plugin-transform-json-strings@npm:^7.23.4":
|
||||
version: 7.23.4
|
||||
resolution: "@babel/plugin-transform-json-strings@npm:7.23.4"
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils": "npm:^7.22.5"
|
||||
"@babel/plugin-syntax-json-strings": "npm:^7.8.3"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: e1cef6a485b9da32aba9449fb459dac062dfc401f3d6ad48e7fbdcb73bbe470c995cc15ce5c421b95efe1e9a90d5507eb606360fe10b6d8cb869dd5dae7a2562
|
||||
checksum: 39e82223992a9ad857722ae051291935403852ad24b0dd64c645ca1c10517b6bf9822377d88643fed8b3e61a4e3f7e5ae41cf90eb07c40a786505d47d5970e54
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -883,15 +883,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-logical-assignment-operators@npm:^7.23.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.23.3"
|
||||
"@babel/plugin-transform-logical-assignment-operators@npm:^7.23.4":
|
||||
version: 7.23.4
|
||||
resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.23.4"
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils": "npm:^7.22.5"
|
||||
"@babel/plugin-syntax-logical-assignment-operators": "npm:^7.10.4"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 23b7588b26d420c8b132bd08916d49871ca0c8db892f6b58637b10e2a0d918163d413c505db880a9157fc2e61d089040f139298a60d837ccbd0efca0474ac7ca
|
||||
checksum: 87b034dd13143904e405887e6125d76c27902563486efc66b7d9a9d8f9406b76c6ac42d7b37224014af5783d7edb465db0cdecd659fa3227baad0b3a6a35deff
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -980,7 +980,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.22.3, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.23.3":
|
||||
"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.22.3, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.23.4":
|
||||
version: 7.23.4
|
||||
resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.23.4"
|
||||
dependencies:
|
||||
|
@ -992,21 +992,21 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-numeric-separator@npm:^7.23.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/plugin-transform-numeric-separator@npm:7.23.3"
|
||||
"@babel/plugin-transform-numeric-separator@npm:^7.23.4":
|
||||
version: 7.23.4
|
||||
resolution: "@babel/plugin-transform-numeric-separator@npm:7.23.4"
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils": "npm:^7.22.5"
|
||||
"@babel/plugin-syntax-numeric-separator": "npm:^7.10.4"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: d3748cce20e8752e61dfda55e275c699459a3ff8d0bb46585da813136e04066b1ce70b71beef504fcdc8d4cca3c955112cea96d5e9fd5a42a5bc8956d05236c2
|
||||
checksum: e34902da4f5588dc4812c92cb1f6a5e3e3647baf7b4623e30942f551bf1297621abec4e322ebfa50b320c987c0f34d9eb4355b3d289961d9035e2126e3119c12
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-object-rest-spread@npm:^7.23.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/plugin-transform-object-rest-spread@npm:7.23.3"
|
||||
"@babel/plugin-transform-object-rest-spread@npm:^7.23.4":
|
||||
version: 7.23.4
|
||||
resolution: "@babel/plugin-transform-object-rest-spread@npm:7.23.4"
|
||||
dependencies:
|
||||
"@babel/compat-data": "npm:^7.23.3"
|
||||
"@babel/helper-compilation-targets": "npm:^7.22.15"
|
||||
|
@ -1015,7 +1015,7 @@ __metadata:
|
|||
"@babel/plugin-transform-parameters": "npm:^7.23.3"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 31ab631aaba945c118662943e5f1f54a21f07d64f06e06b25d55871168c460f3eeeccdf7b05aa74a1340e2cfbe781ad3c7ceccd0c2585d39f7b73ba11ebaa9d0
|
||||
checksum: b56017992ffe7fcd1dd9a9da67c39995a141820316266bcf7d77dc912980d228ccbd3f36191d234f5cc389b09157b5d2a955e33e8fb368319534affd1c72b262
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -1031,28 +1031,28 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-optional-catch-binding@npm:^7.23.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.23.3"
|
||||
"@babel/plugin-transform-optional-catch-binding@npm:^7.23.4":
|
||||
version: 7.23.4
|
||||
resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.23.4"
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils": "npm:^7.22.5"
|
||||
"@babel/plugin-syntax-optional-catch-binding": "npm:^7.8.3"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 85ac1e94ee8f21648816151628ff931cc16143ec8c904649a1ecfd8960160290eccc5a197b4ae3ee7a1c7a27a7c4189e61b4de24483d5bad4040784afe2d206f
|
||||
checksum: 4ef61812af0e4928485e28301226ce61139a8b8cea9e9a919215ebec4891b9fea2eb7a83dc3090e2679b7d7b2c8653da601fbc297d2addc54a908b315173991e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-optional-chaining@npm:^7.23.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/plugin-transform-optional-chaining@npm:7.23.3"
|
||||
"@babel/plugin-transform-optional-chaining@npm:^7.23.3, @babel/plugin-transform-optional-chaining@npm:^7.23.4":
|
||||
version: 7.23.4
|
||||
resolution: "@babel/plugin-transform-optional-chaining@npm:7.23.4"
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils": "npm:^7.22.5"
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5"
|
||||
"@babel/plugin-syntax-optional-chaining": "npm:^7.8.3"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 2b358962169d871392aa292a67527e5335909438da0ddbb0d19e7838c0f8a2081cc751a49e6e534ac4d6c932254531a205ac22b197f64fc4c89f41bf9f595497
|
||||
checksum: 305b773c29ad61255b0e83ec1e92b2f7af6aa58be4cba1e3852bddaa14f7d2afd7b4438f41c28b179d6faac7eb8d4fb5530a17920294f25d459b8f84406bfbfb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -1079,9 +1079,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-private-property-in-object@npm:^7.23.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/plugin-transform-private-property-in-object@npm:7.23.3"
|
||||
"@babel/plugin-transform-private-property-in-object@npm:^7.23.4":
|
||||
version: 7.23.4
|
||||
resolution: "@babel/plugin-transform-private-property-in-object@npm:7.23.4"
|
||||
dependencies:
|
||||
"@babel/helper-annotate-as-pure": "npm:^7.22.5"
|
||||
"@babel/helper-create-class-features-plugin": "npm:^7.22.15"
|
||||
|
@ -1089,7 +1089,7 @@ __metadata:
|
|||
"@babel/plugin-syntax-private-property-in-object": "npm:^7.14.5"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 9211dd25a6e87a01535f2d97a663fa6de3472b963c8dcfaacce229a2e3fa6500f2e9fc690bc100a540fc7b66c8364faf7ef19b32e9c9b9791e4561b742c15ed3
|
||||
checksum: 8d31b28f24204b4d13514cd3a8f3033abf575b1a6039759ddd6e1d82dd33ba7281f9bc85c9f38072a665d69bfa26dc40737eefaf9d397b024654a483d2357bf5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -1333,13 +1333,13 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"@babel/preset-env@npm:^7.11.0, @babel/preset-env@npm:^7.12.1, @babel/preset-env@npm:^7.22.4":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/preset-env@npm:7.23.3"
|
||||
version: 7.23.5
|
||||
resolution: "@babel/preset-env@npm:7.23.5"
|
||||
dependencies:
|
||||
"@babel/compat-data": "npm:^7.23.3"
|
||||
"@babel/compat-data": "npm:^7.23.5"
|
||||
"@babel/helper-compilation-targets": "npm:^7.22.15"
|
||||
"@babel/helper-plugin-utils": "npm:^7.22.5"
|
||||
"@babel/helper-validator-option": "npm:^7.22.15"
|
||||
"@babel/helper-validator-option": "npm:^7.23.5"
|
||||
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "npm:^7.23.3"
|
||||
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "npm:^7.23.3"
|
||||
"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "npm:^7.23.3"
|
||||
|
@ -1363,25 +1363,25 @@ __metadata:
|
|||
"@babel/plugin-syntax-top-level-await": "npm:^7.14.5"
|
||||
"@babel/plugin-syntax-unicode-sets-regex": "npm:^7.18.6"
|
||||
"@babel/plugin-transform-arrow-functions": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-async-generator-functions": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-async-generator-functions": "npm:^7.23.4"
|
||||
"@babel/plugin-transform-async-to-generator": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-block-scoped-functions": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-block-scoping": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-block-scoping": "npm:^7.23.4"
|
||||
"@babel/plugin-transform-class-properties": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-class-static-block": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-classes": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-class-static-block": "npm:^7.23.4"
|
||||
"@babel/plugin-transform-classes": "npm:^7.23.5"
|
||||
"@babel/plugin-transform-computed-properties": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-destructuring": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-dotall-regex": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-duplicate-keys": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-dynamic-import": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-dynamic-import": "npm:^7.23.4"
|
||||
"@babel/plugin-transform-exponentiation-operator": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-export-namespace-from": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-export-namespace-from": "npm:^7.23.4"
|
||||
"@babel/plugin-transform-for-of": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-function-name": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-json-strings": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-json-strings": "npm:^7.23.4"
|
||||
"@babel/plugin-transform-literals": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-logical-assignment-operators": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-logical-assignment-operators": "npm:^7.23.4"
|
||||
"@babel/plugin-transform-member-expression-literals": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-modules-amd": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-modules-commonjs": "npm:^7.23.3"
|
||||
|
@ -1389,15 +1389,15 @@ __metadata:
|
|||
"@babel/plugin-transform-modules-umd": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.22.5"
|
||||
"@babel/plugin-transform-new-target": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-numeric-separator": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-object-rest-spread": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.23.4"
|
||||
"@babel/plugin-transform-numeric-separator": "npm:^7.23.4"
|
||||
"@babel/plugin-transform-object-rest-spread": "npm:^7.23.4"
|
||||
"@babel/plugin-transform-object-super": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-optional-catch-binding": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-optional-chaining": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-optional-catch-binding": "npm:^7.23.4"
|
||||
"@babel/plugin-transform-optional-chaining": "npm:^7.23.4"
|
||||
"@babel/plugin-transform-parameters": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-private-methods": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-private-property-in-object": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-private-property-in-object": "npm:^7.23.4"
|
||||
"@babel/plugin-transform-property-literals": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-regenerator": "npm:^7.23.3"
|
||||
"@babel/plugin-transform-reserved-words": "npm:^7.23.3"
|
||||
|
@ -1418,7 +1418,7 @@ __metadata:
|
|||
semver: "npm:^6.3.1"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 36b02a86817ab5474bb74a8d62a110723b0b05904a52ddc5627cf89457525b8d5ac0739b8e435a6ae12ef8b90cd5fc191169898c3dc2ac9d2c84026b02f2580a
|
||||
checksum: 2a0e1274dec045186e131c6433659b75492583290e8d41633c616f6bff829cb2e4b2f9a57f556283a54db3bd6aa697911e56a36f607911a29b731c445a5b5a06
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -1483,11 +1483,11 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.22.3, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
|
||||
version: 7.23.4
|
||||
resolution: "@babel/runtime@npm:7.23.4"
|
||||
version: 7.23.5
|
||||
resolution: "@babel/runtime@npm:7.23.5"
|
||||
dependencies:
|
||||
regenerator-runtime: "npm:^0.14.0"
|
||||
checksum: db2bf183cd0119599b903ca51ca0aeea8e0ab478a16be1aae10dd90473ed614159d3e5adfdd8f8f3d840402428ce0d90b5c01aae95da9e45a2dd83e02d85ca27
|
||||
checksum: ca679cc91bb7e424bc2db87bb58cc3b06ade916b9adb21fbbdc43e54cdaacb3eea201ceba2a0464b11d2eb65b9fe6a6ffcf4d7521fa52994f19be96f1af14788
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -1502,32 +1502,32 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/traverse@npm:7, @babel/traverse@npm:^7.23.2, @babel/traverse@npm:^7.23.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/traverse@npm:7.23.3"
|
||||
"@babel/traverse@npm:7, @babel/traverse@npm:^7.23.5":
|
||||
version: 7.23.5
|
||||
resolution: "@babel/traverse@npm:7.23.5"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:^7.22.13"
|
||||
"@babel/generator": "npm:^7.23.3"
|
||||
"@babel/code-frame": "npm:^7.23.5"
|
||||
"@babel/generator": "npm:^7.23.5"
|
||||
"@babel/helper-environment-visitor": "npm:^7.22.20"
|
||||
"@babel/helper-function-name": "npm:^7.23.0"
|
||||
"@babel/helper-hoist-variables": "npm:^7.22.5"
|
||||
"@babel/helper-split-export-declaration": "npm:^7.22.6"
|
||||
"@babel/parser": "npm:^7.23.3"
|
||||
"@babel/types": "npm:^7.23.3"
|
||||
"@babel/parser": "npm:^7.23.5"
|
||||
"@babel/types": "npm:^7.23.5"
|
||||
debug: "npm:^4.1.0"
|
||||
globals: "npm:^11.1.0"
|
||||
checksum: 3c2784f4765185126d64fd5eebce0413b7aee6d54f779998594a343a7f973a9693a441ba27533df84e7ab7ce22f1239c6837f35e903132a1b25f7fc7a67bc30f
|
||||
checksum: c5ea793080ca6719b0a1612198fd25e361cee1f3c14142d7a518d2a1eeb5c1d21f7eec1b26c20ea6e1ddd8ed12ab50b960ff95ffd25be353b6b46e1b54d6f825
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.0.0-beta.49, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.10, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.3, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/types@npm:7.23.3"
|
||||
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.0.0-beta.49, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.10, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.5, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3":
|
||||
version: 7.23.5
|
||||
resolution: "@babel/types@npm:7.23.5"
|
||||
dependencies:
|
||||
"@babel/helper-string-parser": "npm:^7.22.5"
|
||||
"@babel/helper-string-parser": "npm:^7.23.4"
|
||||
"@babel/helper-validator-identifier": "npm:^7.22.20"
|
||||
to-fast-properties: "npm:^2.0.0"
|
||||
checksum: 371a10dd9c8d8ebf48fc5d9e1b327dafd74453f8ea582dcbddd1cee5ae34e8881b743e783a86c08c04dcd1849b1842455472a911ae8a1c185484fe9b7b5f1595
|
||||
checksum: 7dd5e2f59828ed046ad0b06b039df2524a8b728d204affb4fc08da2502b9dd3140b1356b5166515d229dc811539a8b70dcd4bc507e06d62a89f4091a38d0b0fb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -2317,6 +2317,7 @@ __metadata:
|
|||
"@types/object-assign": "npm:^4.0.30"
|
||||
"@types/prop-types": "npm:^15.7.5"
|
||||
"@types/punycode": "npm:^2.1.0"
|
||||
"@types/rails__ujs": "npm:^6.0.4"
|
||||
"@types/react": "npm:^18.2.7"
|
||||
"@types/react-dom": "npm:^18.2.4"
|
||||
"@types/react-helmet": "npm:^6.1.6"
|
||||
|
@ -2363,6 +2364,7 @@ __metadata:
|
|||
escape-html: "npm:^1.0.3"
|
||||
eslint: "npm:^8.41.0"
|
||||
eslint-config-prettier: "npm:^9.0.0"
|
||||
eslint-define-config: "npm:^2.0.0"
|
||||
eslint-import-resolver-typescript: "npm:^3.5.5"
|
||||
eslint-plugin-formatjs: "npm:^4.10.1"
|
||||
eslint-plugin-import: "npm:~2.29.0"
|
||||
|
@ -2472,8 +2474,10 @@ __metadata:
|
|||
"@types/npmlog": "npm:^7.0.0"
|
||||
"@types/pg": "npm:^8.6.6"
|
||||
"@types/uuid": "npm:^9.0.0"
|
||||
"@types/ws": "npm:^8.5.9"
|
||||
bufferutil: "npm:^4.0.7"
|
||||
dotenv: "npm:^16.0.3"
|
||||
eslint-define-config: "npm:^2.0.0"
|
||||
express: "npm:^4.18.2"
|
||||
ioredis: "npm:^5.3.2"
|
||||
jsdom: "npm:^23.0.0"
|
||||
|
@ -2481,6 +2485,7 @@ __metadata:
|
|||
pg: "npm:^8.5.0"
|
||||
pg-connection-string: "npm:^2.6.0"
|
||||
prom-client: "npm:^15.0.0"
|
||||
typescript: "npm:^5.0.4"
|
||||
utf-8-validate: "npm:^6.0.3"
|
||||
uuid: "npm:^9.0.0"
|
||||
ws: "npm:^8.12.1"
|
||||
|
@ -3348,6 +3353,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/rails__ujs@npm:^6.0.4":
|
||||
version: 6.0.4
|
||||
resolution: "@types/rails__ujs@npm:6.0.4"
|
||||
checksum: 7477cb03a0e1339b9cd5c8ac4a197a153e2ff48742b2f527c5a39dcdf80f01493011e368483290d3717662c63066fada3ab203a335804cbb3573cf575f37007e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/range-parser@npm:*":
|
||||
version: 1.2.7
|
||||
resolution: "@types/range-parser@npm:1.2.7"
|
||||
|
@ -3647,6 +3659,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/ws@npm:^8.5.9":
|
||||
version: 8.5.9
|
||||
resolution: "@types/ws@npm:8.5.9"
|
||||
dependencies:
|
||||
"@types/node": "npm:*"
|
||||
checksum: 678bdd6461c4653f2975c537fb673cb1918c331558e2d2422b69761c9ced67200bb07c664e2593f3864077a891cb7c13ef2a40d303b4aacb06173d095d8aa3ce
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/yargs-parser@npm:*":
|
||||
version: 21.0.2
|
||||
resolution: "@types/yargs-parser@npm:21.0.2"
|
||||
|
@ -7312,6 +7333,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint-define-config@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "eslint-define-config@npm:2.0.0"
|
||||
checksum: 617c3143bc1ed8df0b20ae632d428d5f241dbb04483631e1410c58fe65ba3e503cf94631c5973115482b58ba464d052422a718c0f4d49182f8d13ffbb36bf1d6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint-import-resolver-node@npm:^0.3.9":
|
||||
version: 0.3.9
|
||||
resolution: "eslint-import-resolver-node@npm:0.3.9"
|
||||
|
@ -10636,8 +10664,8 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"jsdom@npm:^23.0.0":
|
||||
version: 23.0.0
|
||||
resolution: "jsdom@npm:23.0.0"
|
||||
version: 23.0.1
|
||||
resolution: "jsdom@npm:23.0.1"
|
||||
dependencies:
|
||||
cssstyle: "npm:^3.0.0"
|
||||
data-urls: "npm:^5.0.0"
|
||||
|
@ -10661,11 +10689,11 @@ __metadata:
|
|||
ws: "npm:^8.14.2"
|
||||
xml-name-validator: "npm:^5.0.0"
|
||||
peerDependencies:
|
||||
canvas: ^3.0.0
|
||||
canvas: ^2.11.2
|
||||
peerDependenciesMeta:
|
||||
canvas:
|
||||
optional: true
|
||||
checksum: 2c876a02de49e0ed6b667a4eb9b08b8e76ac189a5571ff97791cc9564e713259314deea6d657cc7f59fc30af41b900e7d833c95017e576dfcaf25f32565722af
|
||||
checksum: 13b2b3693ccb40215d1cce77bac7a295414ee4c0a06e30167f8087c9867145ba23dbd592bd95a801cadd7b3698bfd20b9c3f2c26fd8422607f22609ed2e404ef
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
Loading…
Reference in a new issue