diff --git a/.bundler-audit.yml b/.bundler-audit.yml new file mode 100644 index 0000000000..f84ec80872 --- /dev/null +++ b/.bundler-audit.yml @@ -0,0 +1,3 @@ +--- +ignore: + - CVE-2015-9284 # Mitigation following https://github.com/omniauth/omniauth/wiki/Resolving-CVE-2015-9284#mitigating-in-rails-applications diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index fabb6967e7..0000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,225 +0,0 @@ -version: 2.1 - -orbs: - ruby: circleci/ruby@2.0.0 - node: circleci/node@5.0.3 - -executors: - default: - parameters: - ruby-version: - type: string - docker: - - image: cimg/ruby:<< parameters.ruby-version >> - environment: - BUNDLE_JOBS: 3 - BUNDLE_RETRY: 3 - CONTINUOUS_INTEGRATION: true - DB_HOST: localhost - DB_USER: root - DISABLE_SIMPLECOV: true - RAILS_ENV: test - - image: cimg/postgres:14.5 - environment: - POSTGRES_USER: root - POSTGRES_HOST_AUTH_METHOD: trust - - image: cimg/redis:7.0 - -commands: - install-system-dependencies: - steps: - - run: - name: Install system dependencies - command: | - sudo apt-get update - sudo apt-get install -y libicu-dev libidn11-dev - install-ruby-dependencies: - parameters: - ruby-version: - type: string - steps: - - run: - command: | - bundle config clean 'true' - bundle config frozen 'true' - bundle config without 'development production' - name: Set bundler settings - - ruby/install-deps: - bundler-version: '2.3.26' - key: ruby<< parameters.ruby-version >>-gems-v1 - wait-db: - steps: - - run: - command: dockerize -wait tcp://localhost:5432 -wait tcp://localhost:6379 -timeout 1m - name: Wait for PostgreSQL and Redis - -jobs: - build: - docker: - - image: cimg/ruby:3.0-node - environment: - RAILS_ENV: test - steps: - - checkout - - install-system-dependencies - - install-ruby-dependencies: - ruby-version: '3.0' - - node/install-packages: - cache-version: v1 - pkg-manager: yarn - - run: - command: | - export NODE_OPTIONS=--openssl-legacy-provider - ./bin/rails assets:precompile - name: Precompile assets - - persist_to_workspace: - paths: - - public/assets - - public/packs-test - root: . - - test: - parameters: - ruby-version: - type: string - executor: - name: default - ruby-version: << parameters.ruby-version >> - environment: - ALLOW_NOPAM: true - PAM_ENABLED: true - PAM_DEFAULT_SERVICE: pam_test - PAM_CONTROLLED_SERVICE: pam_test_controlled - parallelism: 4 - steps: - - checkout - - install-system-dependencies - - run: - command: sudo apt-get install -y ffmpeg imagemagick libpam-dev - name: Install additional system dependencies - - run: - command: bundle config with 'pam_authentication' - name: Enable PAM authentication - - install-ruby-dependencies: - ruby-version: << parameters.ruby-version >> - - attach_workspace: - at: . - - wait-db - - run: - command: ./bin/rails db:create db:schema:load db:seed - name: Load database schema - - ruby/rspec-test - - test-migrations: - executor: - name: default - ruby-version: '3.0' - steps: - - checkout - - install-system-dependencies - - install-ruby-dependencies: - ruby-version: '3.0' - - wait-db - - run: - command: ./bin/rails db:create - name: Create database - - run: - command: ./bin/rails db:migrate VERSION=20171010025614 - name: Run migrations up to v2.0.0 - - run: - command: ./bin/rails tests:migrations:populate_v2 - name: Populate database with test data - - run: - command: ./bin/rails db:migrate VERSION=20180514140000 - name: Run migrations up to v2.4.0 - - run: - command: ./bin/rails tests:migrations:populate_v2_4 - name: Populate database with test data - - run: - command: ./bin/rails db:migrate VERSION=20180707154237 - name: Run migrations up to v2.4.3 - - run: - command: ./bin/rails tests:migrations:populate_v2_4_3 - name: Populate database with test data - - run: - command: ./bin/rails db:migrate - name: Run all remaining migrations - - run: - command: ./bin/rails tests:migrations:check_database - name: Check migration result - - test-two-step-migrations: - executor: - name: default - ruby-version: '3.0' - steps: - - checkout - - install-system-dependencies - - install-ruby-dependencies: - ruby-version: '3.0' - - wait-db - - run: - command: ./bin/rails db:create - name: Create database - - run: - command: ./bin/rails db:migrate VERSION=20171010025614 - name: Run migrations up to v2.0.0 - - run: - command: ./bin/rails tests:migrations:populate_v2 - name: Populate database with test data - - run: - command: ./bin/rails db:migrate VERSION=20180514140000 - name: Run pre-deployment migrations up to v2.4.0 - environment: - SKIP_POST_DEPLOYMENT_MIGRATIONS: true - - run: - command: ./bin/rails tests:migrations:populate_v2_4 - name: Populate database with test data - - run: - command: ./bin/rails db:migrate VERSION=20180707154237 - name: Run migrations up to v2.4.3 - environment: - SKIP_POST_DEPLOYMENT_MIGRATIONS: true - - run: - command: ./bin/rails tests:migrations:populate_v2_4_3 - name: Populate database with test data - - run: - command: ./bin/rails db:migrate - name: Run all remaining pre-deployment migrations - environment: - SKIP_POST_DEPLOYMENT_MIGRATIONS: true - - run: - command: ./bin/rails db:migrate - name: Run all post-deployment migrations - - run: - command: ./bin/rails tests:migrations:check_database - name: Check migration result - -workflows: - version: 2 - build-and-test: - jobs: - - build - - test: - matrix: - parameters: - ruby-version: - - '2.7' - - '3.0' - name: test-ruby<< matrix.ruby-version >> - requires: - - build - - test-migrations: - requires: - - build - - test-two-step-migrations: - requires: - - build - - node/run: - cache-version: v1 - name: test-webui - pkg-manager: yarn - requires: - - build - version: '16.19' - yarn-run: test:jest diff --git a/.codeclimate.yml b/.codeclimate.yml deleted file mode 100644 index 59051aae7a..0000000000 --- a/.codeclimate.yml +++ /dev/null @@ -1,39 +0,0 @@ -version: '2' -checks: - argument-count: - enabled: false - complex-logic: - enabled: false - file-lines: - enabled: false - method-complexity: - enabled: false - method-count: - enabled: false - method-lines: - enabled: false - nested-control-flow: - enabled: false - return-statements: - enabled: false - similar-code: - enabled: false - identical-code: - enabled: false -plugins: - brakeman: - enabled: true - bundler-audit: - enabled: true - eslint: - enabled: false - rubocop: - enabled: false - sass-lint: - enabled: false -exclude_patterns: - - spec/ - - vendor/asset/ - - - app/javascript/mastodon/locales/**/*.json - - config/locales/**/*.yml diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 425b86a6bb..f991036add 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,16 +1,14 @@ -# [Choice] Ruby version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.1, 3.0, 2, 2.7, 2.6, 3-bullseye, 3.1-bullseye, 3.0-bullseye, 2-bullseye, 2.7-bullseye, 2.6-bullseye, 3-buster, 3.1-buster, 3.0-buster, 2-buster, 2.7-buster, 2.6-buster -ARG VARIANT=3.1-bullseye -FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT} +# For details, see https://github.com/devcontainers/images/tree/main/src/ruby +FROM mcr.microsoft.com/devcontainers/ruby:1-3.2-bullseye # Install Rails # RUN gem install rails webdrivers # Default value to allow debug server to serve content over GitHub Codespace's port forwarding service # The value is a comma-separated list of allowed domains -ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev" +ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev,.preview.app.github.dev,.app.github.dev" -# [Choice] Node.js version: lts/*, 18, 16, 14 -ARG NODE_VERSION="lts/*" +ARG NODE_VERSION="16" RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1" # [Optional] Uncomment this section to install additional OS packages. @@ -22,3 +20,5 @@ RUN gem install foreman # [Optional] Uncomment this line to install global node packages. RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g yarn" 2>&1 + +COPY welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b98f6a21e0..17208a84e9 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,24 +1,13 @@ +// For more details, see https://aka.ms/devcontainer.json. { "name": "Mastodon", "dockerComposeFile": "docker-compose.yml", "service": "app", - "workspaceFolder": "/mastodon", - - // Set *default* container specific settings.json values on container create. - "settings": {}, - - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "EditorConfig.EditorConfig", - "dbaeumer.vscode-eslint", - "rebornix.Ruby", - "webben.browserslist" - ], + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + // Features to add to the dev container. More info: https://containers.dev/features. "features": { - "ghcr.io/devcontainers/features/sshd:1": { - "version": "latest" - } + "ghcr.io/devcontainers/features/sshd:1": {} }, // Use 'forwardPorts' to make a list of ports inside the container available locally. @@ -26,8 +15,18 @@ "forwardPorts": [3000, 4000], // Use 'postCreateCommand' to run commands after the container is created. + "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", "postCreateCommand": ".devcontainer/post-create.sh", + "waitFor": "postCreateCommand", - // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "vscode" + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": {}, + // Add the IDs of extensions you want installed when the container is created. + "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"] + } + } } diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 95f401379c..a2658ea8ba 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -5,19 +5,12 @@ services: build: context: . dockerfile: Dockerfile - args: - # Update 'VARIANT' to pick a version of Ruby: 3, 3.1, 3.0, 2, 2.7, 2.6 - # Append -bullseye or -buster to pin to an OS version. - # Use -bullseye variants on local arm64/Apple Silicon. - VARIANT: '3.0-bullseye' - # Optional Node.js version to install - NODE_VERSION: '16' volumes: - - ..:/mastodon:cached + - ../..:/workspaces:cached environment: RAILS_ENV: development NODE_ENV: development - + BIND: 0.0.0.0 REDIS_HOST: redis REDIS_PORT: '6379' DB_HOST: db @@ -30,10 +23,12 @@ services: LIBRE_TRANSLATE_ENDPOINT: http://libretranslate:5000 # Overrides default command so things don't shut down after the process ends. command: sleep infinity + ports: + - '127.0.0.1:3000:3000' + - '127.0.0.1:4000:4000' networks: - external_network - internal_network - user: vscode db: image: postgres:14-alpine @@ -49,7 +44,7 @@ services: - internal_network redis: - image: redis:6-alpine + image: redis:7-alpine restart: unless-stopped volumes: - redis-data:/data @@ -74,15 +69,19 @@ services: hard: -1 libretranslate: - image: libretranslate/libretranslate:v1.2.9 + image: libretranslate/libretranslate:v1.3.11 restart: unless-stopped + volumes: + - lt-data:/home/libretranslate/.local networks: + - external_network - internal_network volumes: postgres-data: redis-data: es-data: + lt-data: networks: external_network: diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 02f488f120..a075cc7b3b 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -3,17 +3,22 @@ set -e # Fail the whole script on first error # Fetch Ruby gem dependencies -bundle install --path vendor/bundle --with='development test' - -# Fetch Javascript dependencies -yarn install +bundle config path 'vendor/bundle' +bundle config with 'development test' +bundle install # Make Gemfile.lock pristine again git checkout -- Gemfile.lock +# Fetch Javascript dependencies +yarn --frozen-lockfile + # [re]create, migrate, and seed the test database RAILS_ENV=test ./bin/rails db:setup +# [re]create, migrate, and seed the development database +RAILS_ENV=development ./bin/rails db:setup + # Precompile assets for development RAILS_ENV=development ./bin/rails assets:precompile diff --git a/.devcontainer/welcome-message.txt b/.devcontainer/welcome-message.txt new file mode 100644 index 0000000000..488cf92857 --- /dev/null +++ b/.devcontainer/welcome-message.txt @@ -0,0 +1,8 @@ +👋 Welcome to "Mastodon" in GitHub Codespaces! + +🛠️ Your environment is fully setup with all the required software. + +🔍 To explore VS Code to its fullest, search using the Command Palette (Cmd/Ctrl + Shift + P or F1). + +📝 Edit away, run your app as usual, and we'll automatically make it available for you to access. + diff --git a/.eslintrc.js b/.eslintrc.js index 4d81aa47e0..206faa1c7a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,72 +4,65 @@ module.exports = { extends: [ 'eslint:recommended', 'plugin:react/recommended', + 'plugin:react-hooks/recommended', 'plugin:jsx-a11y/recommended', 'plugin:import/recommended', + 'plugin:promise/recommended', + 'plugin:jsdoc/recommended', + 'plugin:prettier/recommended', ], env: { browser: true, node: true, es6: true, - jest: true, }, globals: { ATTACHMENT_HOST: false, }, - parser: '@babel/eslint-parser', + parser: '@typescript-eslint/parser', plugins: [ 'react', 'jsx-a11y', 'import', 'promise', + '@typescript-eslint', + 'formatjs', ], parserOptions: { sourceType: 'module', ecmaFeatures: { - experimentalObjectRestSpread: true, jsx: true, }, ecmaVersion: 2021, + requireConfigFile: false, + babelOptions: { + configFile: false, + presets: ['@babel/react', '@babel/env'], + }, }, settings: { react: { version: 'detect', }, - 'import/extensions': [ - '.js', - ], 'import/ignore': [ 'node_modules', '\\.(css|scss|json)$', ], 'import/resolver': { - node: { - paths: ['app/javascript'], - }, + typescript: {}, }, }, rules: { - 'brace-style': 'warn', - 'comma-dangle': ['error', 'always-multiline'], - 'comma-spacing': [ - 'warn', - { - before: false, - after: true, - }, - ], - 'comma-style': ['warn', 'last'], 'consistent-return': 'error', 'dot-notation': 'error', - eqeqeq: 'error', - indent: ['warn', 2], + eqeqeq: ['error', 'always', { 'null': 'ignore' }], 'jsx-quotes': ['error', 'prefer-single'], 'no-case-declarations': 'off', 'no-catch-shadow': 'error', @@ -88,41 +81,43 @@ module.exports = { { property: 'substring', message: 'Use .slice instead of .substring.' }, { property: 'substr', message: 'Use .slice instead of .substr.' }, ], + 'no-restricted-syntax': [ + 'error', + { + // eslint-disable-next-line no-restricted-syntax + selector: 'Literal[value=/•/], JSXText[value=/•/]', + // eslint-disable-next-line no-restricted-syntax + message: "Use '·' (middle dot) instead of '•' (bullet)", + }, + ], 'no-self-assign': 'off', - 'no-trailing-spaces': 'warn', 'no-unused-expressions': 'error', - 'no-unused-vars': [ + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ 'error', { vars: 'all', args: 'after-used', + destructuredArrayIgnorePattern: '^_', ignoreRestSiblings: true, }, ], - 'object-curly-spacing': ['error', 'always'], - 'padded-blocks': [ - 'error', - { - classes: 'always', - }, - ], - quotes: ['error', 'single'], - semi: 'error', 'valid-typeof': 'error', + 'react/jsx-filename-extension': ['error', { extensions: ['.jsx', 'tsx'] }], 'react/jsx-boolean-value': 'error', - 'react/jsx-closing-bracket-location': ['error', 'line-aligned'], - 'react/jsx-curly-spacing': 'error', 'react/display-name': 'off', + 'react/jsx-fragments': ['error', 'syntax'], 'react/jsx-equals-spacing': 'error', - 'react/jsx-first-prop-new-line': ['error', 'multiline-multiprop'], - 'react/jsx-indent': ['error', 2], 'react/jsx-no-bind': 'error', + 'react/jsx-no-useless-fragment': 'error', 'react/jsx-no-target-blank': 'off', 'react/jsx-tag-spacing': 'error', + 'react/jsx-uses-react': 'off', // not needed with new JSX transform 'react/jsx-wrap-multilines': 'error', 'react/no-deprecated': 'off', 'react/no-unknown-property': 'off', + 'react/react-in-jsx-scope': 'off', // not needed with new JSX transform 'react/self-closing-comp': 'error', // recommended values found in https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/src/index.js @@ -184,26 +179,205 @@ module.exports = { 'always', { js: 'never', + jsx: 'never', + mjs: 'never', + ts: 'never', + tsx: 'never', }, ], + 'import/first': 'error', 'import/newline-after-import': 'error', + 'import/no-anonymous-default-export': 'error', 'import/no-extraneous-dependencies': [ 'error', { devDependencies: [ 'config/webpack/**', + 'app/javascript/mastodon/performance.js', 'app/javascript/mastodon/test_setup.js', 'app/javascript/**/__tests__/**', ], }, ], + 'import/no-amd': 'error', + 'import/no-commonjs': 'error', + 'import/no-import-module-exports': 'error', + 'import/no-relative-packages': 'error', + 'import/no-self-import': 'error', + 'import/no-useless-path-segments': 'error', 'import/no-webpack-loader-syntax': 'error', + 'import/order': [ + 'error', + { + alphabetize: { order: 'asc' }, + 'newlines-between': 'always', + groups: [ + 'builtin', + 'external', + 'internal', + 'parent', + ['index', 'sibling'], + 'object', + ], + pathGroups: [ + // React core packages + { + pattern: '{react,react-dom,react-dom/client,prop-types}', + group: 'builtin', + position: 'after', + }, + // I18n + { + pattern: '{react-intl,intl-messageformat}', + group: 'builtin', + position: 'after', + }, + // Common React utilities + { + pattern: '{classnames,react-helmet,react-router-dom}', + group: 'external', + position: 'before', + }, + // Immutable / Redux / data store + { + pattern: '{immutable,react-redux,react-immutable-proptypes,react-immutable-pure-component,reselect}', + group: 'external', + position: 'before', + }, + // Internal packages + { + pattern: '{mastodon/**,flavours/glitch-soc/**}', + group: 'internal', + position: 'after', + }, + ], + pathGroupsExcludedImportTypes: [], + }, + ], + + 'promise/always-return': 'off', 'promise/catch-or-return': [ 'error', { allowFinally: true, }, ], + 'promise/no-callback-in-promise': 'off', + 'promise/no-nesting': 'off', + 'promise/no-promise-in-callback': 'off', + + 'formatjs/blocklist-elements': 'error', + 'formatjs/enforce-default-message': ['error', 'literal'], + 'formatjs/enforce-description': 'off', // description values not currently used + 'formatjs/enforce-id': 'off', // Explicit IDs are used in the project + 'formatjs/enforce-placeholders': 'off', // Issues in short_number.jsx + 'formatjs/enforce-plural-rules': 'error', + 'formatjs/no-camel-case': 'off', // disabledAccount is only non-conforming + 'formatjs/no-complex-selectors': 'error', + 'formatjs/no-emoji': 'error', + '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', + 'formatjs/prefer-formatted-message': 'error', + 'formatjs/prefer-pound-in-plural': 'error', + + 'jsdoc/check-types': 'off', + 'jsdoc/no-undefined-types': 'off', + 'jsdoc/require-jsdoc': 'off', + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-property-description': 'off', + 'jsdoc/require-returns-description': 'off', + 'jsdoc/require-returns': 'off', }, + + overrides: [ + { + files: [ + '*.config.js', + '.*rc.js', + 'ide-helper.js', + 'config/webpack/**/*', + 'config/formatjs-formatter.js', + ], + + env: { + commonjs: true, + }, + + parserOptions: { + sourceType: 'script', + }, + + rules: { + 'import/no-commonjs': 'off', + }, + }, + { + files: [ + '**/*.ts', + '**/*.tsx', + ], + + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:jsx-a11y/recommended', + 'plugin:import/recommended', + 'plugin:import/typescript', + 'plugin:promise/recommended', + 'plugin:jsdoc/recommended-typescript', + 'plugin:prettier/recommended', + ], + + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, + + rules: { + 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], + + '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'], + '@typescript-eslint/consistent-type-exports': 'error', + '@typescript-eslint/consistent-type-imports': 'error', + + 'jsdoc/require-jsdoc': 'off', + + // Those rules set stricter rules for TS files + // to enforce better practices when converting from JS + 'import/no-default-export': 'warn', + 'react/prefer-stateless-function': 'warn', + 'react/function-component-definition': ['error', { namedComponents: 'arrow-function' }], + 'react/jsx-uses-react': 'off', // not needed with new JSX transform + 'react/react-in-jsx-scope': 'off', // not needed with new JSX transform + 'react/prop-types': 'off', + }, + }, + { + files: [ + '**/__tests__/*.js', + '**/__tests__/*.jsx', + ], + + env: { + jest: true, + }, + }, + { + files: [ + 'streaming/**/*', + ], + rules: { + 'import/no-commonjs': 'off', + }, + }, + ], }; diff --git a/.github/renovate.json5 b/.github/renovate.json5 new file mode 100644 index 0000000000..1ae40d4161 --- /dev/null +++ b/.github/renovate.json5 @@ -0,0 +1,114 @@ +{ + $schema: 'https://docs.renovatebot.com/renovate-schema.json', + extends: [ + 'config:base', + ':dependencyDashboard', + ':labels(dependencies)', + ':maintainLockFilesMonthly', // update non-direct dependencies monthly + ':prConcurrentLimit10', // only 10 open PRs at the same time + ], + stabilityDays: 3, // Wait 3 days after the package has been published before upgrading it + // packageRules order is important, they are applied from top to bottom and are merged, + // so for example grouping rules needs to be at the bottom + packageRules: [ + { + // Ignore major version bumps for these node packages + matchManagers: ['npm'], + matchPackageNames: [ + '@rails/ujs', // Needs to match the major Rails version + 'tesseract.js', // Requires code changes + 'react-hotkeys', // Requires code changes + + // Requires Webpacker upgrade or replacement + '@types/webpack', + 'babel-loader', + 'compression-webpack-plugin', + 'css-loader', + 'imports-loader', + 'mini-css-extract-plugin', + 'postcss-loader', + 'sass-loader', + 'terser-webpack-plugin', + 'webpack', + 'webpack-assets-manifest', + 'webpack-bundle-analyzer', + 'webpack-dev-server', + 'webpack-cli', + + // react-router: Requires manual upgrade + 'history', + 'react-router-dom', + ], + matchUpdateTypes: ['major'], + enabled: false, + }, + { + // Ignore major version bumps for these Ruby packages + matchManagers: ['bundler'], + matchPackageNames: [ + 'sprockets', // Requires manual upgrade https://github.com/rails/sprockets/blob/master/UPGRADING.md#guide-to-upgrading-from-sprockets-3x-to-4x + 'strong_migrations', // Requires manual upgrade + 'sidekiq', // Requires manual upgrade + 'sidekiq-unique-jobs', // Requires manual upgrades and sync with Sidekiq version + 'redis', // Requires manual upgrade and sync with Sidekiq version + 'fog-openstack', // TODO: was ignored in https://github.com/mastodon/mastodon/pull/13964 + + // Needs major Rails version bump + 'rack', + 'rails', + 'rails-i18n', + ], + matchUpdateTypes: ['major'], + enabled: false, + }, + { + // Update Github Actions and Docker images weekly + matchManagers: ['github-actions', 'dockerfile', 'docker-compose'], + extends: ['schedule:weekly'], + }, + { + // Ignore major & minor bumps for the ruby image, this needs to be synced with .ruby-version + matchManagers: ['dockerfile'], + matchPackageNames: ['moritzheiber/ruby-jemalloc'], + matchUpdateTypes: ['minor', 'major'], + enabled: false, + }, + { + // Ignore major bump for the node image, this needs to be synced with .nvmrc + matchManagers: ['dockerfile'], + matchPackageNames: ['node'], + matchUpdateTypes: ['major'], + enabled: false, + }, + { + // Ignore major postgres bumps in the docker-compose file, as those break dev environments + matchManagers: ['docker-compose'], + matchPackageNames: ['postgres'], + matchUpdateTypes: ['major'], + enabled: false, + }, + { + // Update devDependencies every week, with one grouped PR + matchDepTypes: 'devDependencies', + matchUpdateTypes: ['patch', 'minor'], + excludePackageNames: [ + 'typescript', // Typescript has many changes in minor versions, needs to be checked every time + ], + groupName: 'devDependencies (non-major)', + extends: ['schedule:weekly'], + }, + { + // Update @types/* packages every week, with one grouped PR + matchPackagePrefixes: '@types/', + matchUpdateTypes: ['patch', 'minor'], + groupName: 'DefinitelyTyped types (non-major)', + extends: ['schedule:weekly'], + addLabels: ['typescript'], + }, + // Add labels depending on package manager + { matchManagers: ['npm', 'nvm'], addLabels: ['javascript'] }, + { matchManagers: ['bundler', 'ruby-version'], addLabels: ['ruby'] }, + { matchManagers: ['docker-compose', 'dockerfile'], addLabels: ['docker'] }, + { matchManagers: ['github-actions'], addLabels: ['github_actions'] }, + ], +} diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 36e9bf370a..da4203e357 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest concurrency: - group: ${{ github.ref }} + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true steps: @@ -25,12 +25,15 @@ jobs: - uses: hadolint/hadolint-action@v3.1.0 - uses: docker/setup-qemu-action@v2 - uses: docker/setup-buildx-action@v2 - - uses: docker/login-action@v2 + + - name: Log in to the Github Container registry + uses: docker/login-action@v2 with: registry: ghcr.io - username: ${{ github.repository_owner }} + username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} if: github.event_name != 'pull_request' + - uses: docker/metadata-action@v4 id: meta with: @@ -39,13 +42,22 @@ jobs: type=raw,value=latest,enable={{is_default_branch}} type=edge,branch=main type=sha,prefix=,format=long + + - name: Generate version suffix + id: version_vars + if: github.repository == 'mastodon/mastodon' && github.event_name == 'push' && github.ref_name == 'main' + run: | + echo mastodon_version_suffix=+edge-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT + - uses: docker/build-push-action@v4 with: context: . + build-args: MASTODON_VERSION_SUFFIX=${{ steps.version_vars.outputs.mastodon_version_suffix }} platforms: linux/amd64,linux/arm64 provenance: false builder: ${{ steps.buildx.outputs.name }} push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.github/workflows/build-nightly.yml b/.github/workflows/build-nightly.yml new file mode 100644 index 0000000000..f07f7447ca --- /dev/null +++ b/.github/workflows/build-nightly.yml @@ -0,0 +1,60 @@ +name: Build nightly container image +on: + workflow_dispatch: + schedule: + - cron: '0 2 * * *' # run at 2 AM UTC +permissions: + contents: read + packages: write + +jobs: + build-nightly-image: + runs-on: ubuntu-latest + + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + steps: + - uses: actions/checkout@v3 + - uses: hadolint/hadolint-action@v3.1.0 + - uses: docker/setup-qemu-action@v2 + - uses: docker/setup-buildx-action@v2 + + - name: Log in to the Github Container registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/metadata-action@v4 + id: meta + with: + images: | + ghcr.io/mastodon/mastodon + flavor: | + latest=auto + tags: | + type=raw,value=nightly + type=schedule,pattern=nightly-{{date 'YYYY-MM-DD' tz='Etc/UTC'}} + labels: | + org.opencontainers.image.description=Nightly build image used for testing purposes + + - name: Generate version suffix + id: version_vars + run: | + echo mastodon_version_suffix=+nightly-$(date +'%Y%m%d') >> $GITHUB_OUTPUT + + - uses: docker/build-push-action@v4 + with: + context: . + build-args: MASTODON_VERSION_SUFFIX=${{ steps.version_vars.outputs.mastodon_version_suffix }} + platforms: linux/amd64,linux/arm64 + provenance: false + builder: ${{ steps.buildx.outputs.name }} + push: ${{ github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/check-i18n.yml b/.github/workflows/check-i18n.yml index b2e6197868..b67c503e95 100644 --- a/.github/workflows/check-i18n.yml +++ b/.github/workflows/check-i18n.yml @@ -14,24 +14,49 @@ permissions: jobs: check-i18n: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 + - name: Install system dependencies run: | sudo apt-get update sudo apt-get install -y libicu-dev libidn11-dev + - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: .ruby-version bundler-cache: true + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version-file: '.nvmrc' + + - name: Install all yarn packages + run: yarn --frozen-lockfile + + - name: Check for missing strings in English JSON + run: | + yarn i18n:extract --throws + git diff --exit-code + - name: Check locale file normalization run: bundle exec i18n-tasks check-normalized + - name: Check for unused strings run: bundle exec i18n-tasks unused + + - name: Check for missing strings in English YML + run: | + bundle exec i18n-tasks add-missing -l en + git diff --exit-code + - name: Check for wrong string interpolations run: bundle exec i18n-tasks check-consistent-interpolations + - name: Check that all required locale files exist run: bundle exec rake repo:check_locales_files diff --git a/.github/workflows/haml-lint-problem-matcher.json b/.github/workflows/haml-lint-problem-matcher.json new file mode 100644 index 0000000000..3523ea2951 --- /dev/null +++ b/.github/workflows/haml-lint-problem-matcher.json @@ -0,0 +1,17 @@ +{ + "problemMatcher": [ + { + "owner": "haml-lint", + "severity": "warning", + "pattern": [ + { + "regexp": "^(.*):(\\d+)\\s\\[W]\\s(.*):\\s(.*)$", + "file": 1, + "line": 2, + "code": 3, + "message": 4 + } + ] + } + ] +} diff --git a/.github/workflows/lint-css.yml b/.github/workflows/lint-css.yml index 431b88e8de..4d3c2ce5af 100644 --- a/.github/workflows/lint-css.yml +++ b/.github/workflows/lint-css.yml @@ -3,9 +3,11 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - 'package.json' - 'yarn.lock' + - '.nvmrc' - '.prettier*' - 'stylelint.config.js' - '**/*.css' @@ -17,6 +19,7 @@ on: paths: - 'package.json' - 'yarn.lock' + - '.nvmrc' - '.prettier*' - 'stylelint.config.js' - '**/*.css' @@ -36,6 +39,7 @@ jobs: uses: actions/setup-node@v3 with: cache: yarn + node-version-file: '.nvmrc' - name: Install all yarn packages run: yarn --frozen-lockfile @@ -45,4 +49,4 @@ jobs: - run: echo "::add-matcher::.github/stylelint-matcher.json" - name: Stylelint - run: yarn test:lint:sass + run: yarn lint:sass diff --git a/.github/workflows/lint-haml.yml b/.github/workflows/lint-haml.yml new file mode 100644 index 0000000000..56d817123a --- /dev/null +++ b/.github/workflows/lint-haml.yml @@ -0,0 +1,47 @@ +name: Haml Linting +on: + push: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + paths: + - '.github/workflows/haml-lint-problem-matcher.json' + - '.github/workflows/lint-haml.yml' + - '.haml-lint*.yml' + - '.rubocop*.yml' + - '.ruby-version' + - '**/*.haml' + - 'Gemfile*' + + pull_request: + paths: + - '.github/workflows/haml-lint-problem-matcher.json' + - '.github/workflows/lint-haml.yml' + - '.haml-lint*.yml' + - '.rubocop*.yml' + - '.ruby-version' + - '**/*.haml' + - 'Gemfile*' + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Clone repository + uses: actions/checkout@v3 + + - name: Install native Ruby dependencies + run: | + sudo apt-get update + sudo apt-get install -y libicu-dev libidn11-dev + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Run haml-lint + run: | + echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json" + bundle exec haml-lint diff --git a/.github/workflows/lint-js.yml b/.github/workflows/lint-js.yml index 49d989771c..1f0cfd1e70 100644 --- a/.github/workflows/lint-js.yml +++ b/.github/workflows/lint-js.yml @@ -3,21 +3,32 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - 'package.json' - 'yarn.lock' + - 'tsconfig.json' + - '.nvmrc' - '.prettier*' - '.eslint*' - '**/*.js' + - '**/*.jsx' + - '**/*.ts' + - '**/*.tsx' - '.github/workflows/lint-js.yml' pull_request: paths: - 'package.json' - 'yarn.lock' + - 'tsconfig.json' + - '.nvmrc' - '.prettier*' - '.eslint*' - '**/*.js' + - '**/*.jsx' + - '**/*.ts' + - '**/*.tsx' - '.github/workflows/lint-js.yml' jobs: @@ -32,9 +43,13 @@ jobs: uses: actions/setup-node@v3 with: cache: yarn + node-version-file: '.nvmrc' - name: Install all yarn packages run: yarn --frozen-lockfile - name: ESLint - run: yarn test:lint:js + run: yarn lint:js --max-warnings 0 + + - name: Typecheck + run: yarn typecheck diff --git a/.github/workflows/lint-json.yml b/.github/workflows/lint-json.yml index 524ed083aa..8712d8bd80 100644 --- a/.github/workflows/lint-json.yml +++ b/.github/workflows/lint-json.yml @@ -3,9 +3,11 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - 'package.json' - 'yarn.lock' + - '.nvmrc' - '.prettier*' - '**/*.json' - '.github/workflows/lint-json.yml' @@ -15,6 +17,7 @@ on: paths: - 'package.json' - 'yarn.lock' + - '.nvmrc' - '.prettier*' - '**/*.json' - '.github/workflows/lint-json.yml' @@ -32,9 +35,10 @@ jobs: uses: actions/setup-node@v3 with: cache: yarn + node-version-file: '.nvmrc' - name: Install all yarn packages run: yarn --frozen-lockfile - name: Prettier - run: yarn prettier --check "**/*.json" + run: yarn lint:json diff --git a/.github/workflows/lint-md.yml b/.github/workflows/lint-md.yml new file mode 100644 index 0000000000..d19a0470db --- /dev/null +++ b/.github/workflows/lint-md.yml @@ -0,0 +1,44 @@ +name: Markdown Linting +on: + push: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + paths: + - '.github/workflows/lint-md.yml' + - '.nvmrc' + - '.prettier*' + - '**/*.md' + - '!AUTHORS.md' + - 'package.json' + - 'yarn.lock' + + pull_request: + paths: + - '.github/workflows/lint-md.yml' + - '.nvmrc' + - '.prettier*' + - '**/*.md' + - '!AUTHORS.md' + - 'package.json' + - 'yarn.lock' + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Clone repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version-file: '.nvmrc' + + - name: Install all yarn packages + run: yarn --frozen-lockfile + + - name: Prettier + run: yarn lint:md diff --git a/.github/workflows/lint-ruby.yml b/.github/workflows/lint-ruby.yml index b834e3053f..0395c8639f 100644 --- a/.github/workflows/lint-ruby.yml +++ b/.github/workflows/lint-ruby.yml @@ -3,9 +3,12 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - 'Gemfile*' - - '.rubocop.yml' + - '.rubocop*.yml' + - '.ruby-version' + - '.bundler-audit.yml' - '**/*.rb' - '**/*.rake' - '.github/workflows/lint-ruby.yml' @@ -13,7 +16,9 @@ on: pull_request: paths: - 'Gemfile*' - - '.rubocop.yml' + - '.rubocop*.yml' + - '.ruby-version' + - '.bundler-audit.yml' - '**/*.rb' - '**/*.rake' - '.github/workflows/lint-ruby.yml' @@ -21,21 +26,25 @@ on: jobs: lint: runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Set-up RuboCop Problem Mathcher + steps: + - name: Clone repository + uses: actions/checkout@v3 + + - name: Install native Ruby dependencies + run: sudo apt-get install -y libicu-dev libidn11-dev + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Set-up RuboCop Problem Matcher uses: r7kamura/rubocop-problem-matchers-action@v1 - name: Run rubocop - uses: github/super-linter@v4 - env: - DEFAULT_BRANCH: main - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - LINTER_RULES_PATH: . - RUBY_CONFIG_FILE: .rubocop.yml - VALIDATE_ALL_CODEBASE: false - VALIDATE_RUBY: true + run: bundle exec rubocop + + - name: Run bundler-audit + run: bundle exec bundler-audit diff --git a/.github/workflows/lint-yml.yml b/.github/workflows/lint-yml.yml index 48f8170b3b..295e9610b3 100644 --- a/.github/workflows/lint-yml.yml +++ b/.github/workflows/lint-yml.yml @@ -3,9 +3,11 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - 'package.json' - 'yarn.lock' + - '.nvmrc' - '.prettier*' - '**/*.yaml' - '**/*.yml' @@ -16,6 +18,7 @@ on: paths: - 'package.json' - 'yarn.lock' + - '.nvmrc' - '.prettier*' - '**/*.yaml' - '**/*.yml' @@ -34,9 +37,10 @@ jobs: uses: actions/setup-node@v3 with: cache: yarn + node-version-file: '.nvmrc' - name: Install all yarn packages run: yarn --frozen-lockfile - name: Prettier - run: yarn prettier --check "**/*.{yml,yaml}" + run: yarn lint:yml diff --git a/.github/workflows/rebase-needed.yml b/.github/workflows/rebase-needed.yml index 6f903ee610..131a62a576 100644 --- a/.github/workflows/rebase-needed.yml +++ b/.github/workflows/rebase-needed.yml @@ -2,16 +2,35 @@ name: PR Needs Rebase on: push: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + - 'l10n_main' pull_request_target: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + - 'l10n_main' types: [synchronize] +permissions: + pull-requests: write + jobs: label-rebase-needed: runs-on: ubuntu-latest + + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + steps: - name: Check for merge conflicts uses: eps1lon/actions-label-merge-conflict@releases/2.x with: dirtyLabel: 'rebase needed :construction:' repoToken: '${{ secrets.GITHUB_TOKEN }}' + commentOnClean: This pull request has resolved merge conflicts and is ready for review. commentOnDirty: This pull request has merge conflicts that must be resolved before it can be merged. + retryMax: 10 + continueOnMissingPermissions: false diff --git a/.github/workflows/test-js.yml b/.github/workflows/test-js.yml new file mode 100644 index 0000000000..3306105f9e --- /dev/null +++ b/.github/workflows/test-js.yml @@ -0,0 +1,48 @@ +name: JavaScript Testing +on: + push: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + paths: + - 'package.json' + - 'yarn.lock' + - '.nvmrc' + - '**/*.js' + - '**/*.jsx' + - '**/*.ts' + - '**/*.tsx' + - '**/*.snap' + - '.github/workflows/test-js.yml' + + pull_request: + paths: + - 'package.json' + - 'yarn.lock' + - '.nvmrc' + - '**/*.js' + - '**/*.jsx' + - '**/*.ts' + - '**/*.tsx' + - '**/*.snap' + - '.github/workflows/test-js.yml' + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Clone repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version-file: '.nvmrc' + + - name: Install all yarn packages + run: yarn --frozen-lockfile + + - name: Jest testing + run: yarn jest --reporters github-actions summary diff --git a/.github/workflows/test-migrations-one-step.yml b/.github/workflows/test-migrations-one-step.yml new file mode 100644 index 0000000000..a91fd819a2 --- /dev/null +++ b/.github/workflows/test-migrations-one-step.yml @@ -0,0 +1,111 @@ +name: Test one step migrations +on: + push: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + pull_request: + +jobs: + pre_job: + runs-on: ubuntu-latest + + outputs: + should_skip: ${{ steps.skip_check.outputs.should_skip }} + + steps: + - id: skip_check + uses: fkirc/skip-duplicate-actions@v5 + with: + paths: '["Gemfile*", ".ruby-version", "**/*.rb", ".github/workflows/test-migrations-one-step.yml", "lib/tasks/tests.rake"]' + + test: + runs-on: ubuntu-latest + needs: pre_job + if: needs.pre_job.outputs.should_skip != 'true' + + strategy: + fail-fast: false + + matrix: + postgres: + - 14-alpine + - 15-alpine + + services: + postgres: + image: postgres:${{ matrix.postgres}} + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + env: + CONTINUOUS_INTEGRATION: true + DB_HOST: localhost + DB_USER: postgres + DB_PASS: postgres + DISABLE_SIMPLECOV: true + RAILS_ENV: test + BUNDLE_CLEAN: true + BUNDLE_FROZEN: true + BUNDLE_WITHOUT: 'development production' + BUNDLE_JOBS: 3 + BUNDLE_RETRY: 3 + + steps: + - uses: actions/checkout@v3 + + - name: Install native Ruby dependencies + run: | + sudo apt-get update + sudo apt-get install -y libicu-dev libidn11-dev + + - name: Set up bundler cache + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Create database + run: './bin/rails db:create' + + - name: Run migrations up to v2.0.0 + run: './bin/rails db:migrate VERSION=20171010025614' + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2' + + - name: Run migrations up to v2.4.0 + run: './bin/rails db:migrate VERSION=20180514140000' + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2_4' + + - name: Run migrations up to v2.4.3 + run: './bin/rails db:migrate VERSION=20180707154237' + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2_4_3' + + - name: Run all remaining migrations + run: './bin/rails db:migrate' + + - name: Check migration result + run: './bin/rails tests:migrations:check_database' diff --git a/.github/workflows/test-migrations-two-step.yml b/.github/workflows/test-migrations-two-step.yml new file mode 100644 index 0000000000..50266fb8a0 --- /dev/null +++ b/.github/workflows/test-migrations-two-step.yml @@ -0,0 +1,119 @@ +name: Test two step migrations +on: + push: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + pull_request: + +jobs: + pre_job: + runs-on: ubuntu-latest + + outputs: + should_skip: ${{ steps.skip_check.outputs.should_skip }} + + steps: + - id: skip_check + uses: fkirc/skip-duplicate-actions@v5 + with: + paths: '["Gemfile*", ".ruby-version", "**/*.rb", ".github/workflows/test-migrations-two-step.yml", "lib/tasks/tests.rake"]' + + test: + runs-on: ubuntu-latest + needs: pre_job + if: needs.pre_job.outputs.should_skip != 'true' + + strategy: + fail-fast: false + + matrix: + postgres: + - 14-alpine + - 15-alpine + + services: + postgres: + image: postgres:${{ matrix.postgres}} + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + env: + CONTINUOUS_INTEGRATION: true + DB_HOST: localhost + DB_USER: postgres + DB_PASS: postgres + DISABLE_SIMPLECOV: true + RAILS_ENV: test + BUNDLE_CLEAN: true + BUNDLE_FROZEN: true + BUNDLE_WITHOUT: 'development production' + BUNDLE_JOBS: 3 + BUNDLE_RETRY: 3 + + steps: + - uses: actions/checkout@v3 + + - name: Install native Ruby dependencies + run: | + sudo apt-get update + sudo apt-get install -y libicu-dev libidn11-dev + + - name: Set up bundler cache + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Create database + run: './bin/rails db:create' + + - name: Run migrations up to v2.0.0 + run: './bin/rails db:migrate VERSION=20171010025614' + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2' + + - name: Run pre-deployment migrations up to v2.4.0 + run: './bin/rails db:migrate VERSION=20180514140000' + env: + SKIP_POST_DEPLOYMENT_MIGRATIONS: true + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2_4' + + - name: Run migrations up to v2.4.3 + run: './bin/rails db:migrate VERSION=20180707154237' + env: + SKIP_POST_DEPLOYMENT_MIGRATIONS: true + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2_4_3' + + - name: Run all remaining pre-deployment migrations + run: './bin/rails db:migrate' + env: + SKIP_POST_DEPLOYMENT_MIGRATIONS: true + + - name: Run all post-deployment migrations + run: './bin/rails db:migrate' + + - name: Check migration result + run: './bin/rails tests:migrations:check_database' diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml new file mode 100644 index 0000000000..07cb1d41f8 --- /dev/null +++ b/.github/workflows/test-ruby.yml @@ -0,0 +1,151 @@ +name: Ruby Testing + +on: + push: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + pull_request: + +env: + BUNDLE_CLEAN: true + BUNDLE_FROZEN: true + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + fail-fast: true + matrix: + mode: + - production + - test + env: + RAILS_ENV: ${{ matrix.mode }} + BUNDLE_WITH: ${{ matrix.mode }} + OTP_SECRET: precompile_placeholder + SECRET_KEY_BASE: precompile_placeholder + + steps: + - uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version-file: '.nvmrc' + + - name: Install native Ruby dependencies + run: | + sudo apt-get update + sudo apt-get install -y libicu-dev libidn11-dev + + - name: Set up bundler cache + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - run: yarn --frozen-lockfile --production + - name: Precompile assets + # Previously had set this, but it's not supported + # export NODE_OPTIONS=--openssl-legacy-provider + run: |- + ./bin/rails assets:precompile + + - uses: actions/upload-artifact@v3 + if: matrix.mode == 'test' + with: + path: |- + ./public/assets + ./public/packs-test + name: ${{ github.sha }} + retention-days: 0 + + test: + runs-on: ubuntu-latest + + needs: + - build + + services: + postgres: + image: postgres:14-alpine + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + env: + DB_HOST: localhost + DB_USER: postgres + DB_PASS: postgres + DISABLE_SIMPLECOV: true + RAILS_ENV: test + ALLOW_NOPAM: true + PAM_ENABLED: true + PAM_DEFAULT_SERVICE: pam_test + PAM_CONTROLLED_SERVICE: pam_test_controlled + BUNDLE_WITH: 'pam_authentication test' + CI_JOBS: ${{ matrix.ci_job }}/4 + + strategy: + fail-fast: false + matrix: + ruby-version: + - '3.0' + - '3.1' + - '.ruby-version' + ci_job: + - 1 + - 2 + - 3 + - 4 + steps: + - uses: actions/checkout@v3 + + - uses: actions/download-artifact@v3 + with: + path: './public' + name: ${{ github.sha }} + + - name: Update package index + run: sudo apt-get update + + - name: Install native Ruby dependencies + run: sudo apt-get install -y libicu-dev libidn11-dev + + - name: Install additional system dependencies + run: sudo apt-get install -y ffmpeg imagemagick libpam-dev + + - name: Set up bundler cache + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version}} + bundler-cache: true + + - name: Load database schema + run: './bin/rails db:create db:schema:load db:seed' + + - run: bundle exec rake rspec_chunked diff --git a/.haml-lint.yml b/.haml-lint.yml index 7853d81d7c..d1ed30b260 100644 --- a/.haml-lint.yml +++ b/.haml-lint.yml @@ -1,108 +1,14 @@ -# Whether to ignore frontmatter at the beginning of HAML documents for -# frameworks such as Jekyll/Middleman -skip_frontmatter: false +inherits_from: .haml-lint_todo.yml exclude: - 'vendor/**/*' - - 'spec/**/*' - - 'lib/templates/**/*' - - 'app/views/kaminari/**/*' + - lib/templates/haml/scaffold/_form.html.haml + +require: + - ./lib/linter/haml_middle_dot.rb linters: AltText: - enabled: false - - ClassAttributeWithStaticValue: enabled: true - - ClassesBeforeIds: - enabled: true - - ConsecutiveComments: - enabled: true - - ConsecutiveSilentScripts: - enabled: true - max_consecutive: 2 - - EmptyObjectReference: - enabled: true - - EmptyScript: - enabled: true - - FinalNewline: - enabled: true - present: true - - HtmlAttributes: - enabled: true - - ImplicitDiv: - enabled: true - - LeadingCommentSpace: - enabled: true - - LineLength: - enabled: false - max: 80 - - MultilinePipe: - enabled: true - - MultilineScript: - enabled: true - - ObjectReferenceAttributes: - enabled: true - - RuboCop: - enabled: true - # These cops are incredibly noisy when it comes to HAML templates, so we - # ignore them. - ignored_cops: - - Lint/BlockAlignment - - Lint/EndAlignment - - Lint/Void - - Metrics/BlockLength - - Metrics/LineLength - - Style/AlignParameters - - Style/BlockNesting - - Style/ElseAlignment - - Style/EndOfLine - - Style/FileName - - Style/FinalNewline - - Style/FrozenStringLiteralComment - - Style/IfUnlessModifier - - Style/IndentationWidth - - Style/Next - - Style/TrailingBlankLines - - Style/TrailingWhitespace - - Style/WhileUntilModifier - - RubyComments: - enabled: true - - SpaceBeforeScript: - enabled: true - - SpaceInsideHashAttributes: - enabled: true - style: space - - Indentation: - enabled: true - character: space # or tab - - TagName: - enabled: true - - TrailingWhitespace: - enabled: true - - UnnecessaryInterpolation: - enabled: true - - UnnecessaryStringOutput: + MiddleDot: enabled: true diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml new file mode 100644 index 0000000000..c601683907 --- /dev/null +++ b/.haml-lint_todo.yml @@ -0,0 +1,106 @@ +# This configuration was generated by +# `haml-lint --auto-gen-config` +# on 2023-03-15 00:55:01 -0400 using Haml-Lint version 0.45.0. +# The point is for the user to remove these configuration records +# one by one as the lints are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of Haml-Lint, may require this file to be generated again. + +linters: + # Offense count: 63 + RuboCop: + exclude: + - 'app/views/accounts/_og.html.haml' + - 'app/views/admin/account_warnings/_account_warning.html.haml' + - 'app/views/admin/accounts/index.html.haml' + - 'app/views/admin/accounts/show.html.haml' + - 'app/views/admin/announcements/edit.html.haml' + - 'app/views/admin/announcements/new.html.haml' + - 'app/views/admin/disputes/appeals/_appeal.html.haml' + - 'app/views/admin/domain_blocks/edit.html.haml' + - 'app/views/admin/domain_blocks/new.html.haml' + - 'app/views/admin/ip_blocks/new.html.haml' + - 'app/views/admin/reports/actions/preview.html.haml' + - 'app/views/admin/reports/index.html.haml' + - 'app/views/admin/reports/show.html.haml' + - 'app/views/admin/roles/_form.html.haml' + - 'app/views/admin/settings/about/show.html.haml' + - 'app/views/admin/settings/appearance/show.html.haml' + - 'app/views/admin/settings/registrations/show.html.haml' + - 'app/views/admin/statuses/show.html.haml' + - 'app/views/auth/registrations/new.html.haml' + - 'app/views/disputes/strikes/show.html.haml' + - 'app/views/filters/_filter_fields.html.haml' + - 'app/views/invites/_form.html.haml' + - 'app/views/layouts/application.html.haml' + - 'app/views/layouts/error.html.haml' + - 'app/views/notification_mailer/_status.html.haml' + - 'app/views/settings/applications/_fields.html.haml' + - 'app/views/settings/imports/show.html.haml' + - 'app/views/settings/preferences/appearance/show.html.haml' + - 'app/views/settings/preferences/other/show.html.haml' + - 'app/views/statuses/_detailed_status.html.haml' + - 'app/views/statuses/_poll.html.haml' + - 'app/views/statuses/show.html.haml' + - 'app/views/statuses_cleanup/show.html.haml' + - 'app/views/user_mailer/warning.html.haml' + + # Offense count: 913 + LineLength: + enabled: false + + # Offense count: 22 + UnnecessaryStringOutput: + exclude: + - 'app/views/accounts/show.html.haml' + - 'app/views/admin/custom_emojis/_custom_emoji.html.haml' + - 'app/views/admin/relays/_relay.html.haml' + - 'app/views/admin/rules/_rule.html.haml' + - 'app/views/admin/statuses/index.html.haml' + - 'app/views/auth/registrations/_sessions.html.haml' + - 'app/views/disputes/strikes/show.html.haml' + - 'app/views/notification_mailer/_status.html.haml' + - 'app/views/settings/two_factor_authentication_methods/index.html.haml' + - 'app/views/statuses/_detailed_status.html.haml' + - 'app/views/statuses/_poll.html.haml' + - 'app/views/statuses/_simple_status.html.haml' + - 'app/views/user_mailer/suspicious_sign_in.html.haml' + - 'app/views/user_mailer/webauthn_credential_added.html.haml' + - 'app/views/user_mailer/webauthn_credential_deleted.html.haml' + - 'app/views/user_mailer/welcome.html.haml' + + # Offense count: 3 + ViewLength: + exclude: + - 'app/views/admin/accounts/show.html.haml' + - 'app/views/admin/reports/show.html.haml' + - 'app/views/disputes/strikes/show.html.haml' + + # Offense count: 41 + InstanceVariables: + exclude: + - 'app/views/admin/reports/_actions.html.haml' + - 'app/views/admin/roles/_form.html.haml' + - 'app/views/admin/webhooks/_form.html.haml' + - 'app/views/auth/registrations/_sessions.html.haml' + - 'app/views/auth/registrations/_status.html.haml' + - 'app/views/auth/sessions/two_factor/_otp_authentication_form.html.haml' + - 'app/views/authorize_interactions/_post_follow_actions.html.haml' + - 'app/views/invites/_form.html.haml' + - 'app/views/relationships/_account.html.haml' + - 'app/views/shared/_og.html.haml' + - 'app/views/statuses/_status.html.haml' + + # Offense count: 6 + ConsecutiveSilentScripts: + exclude: + - 'app/views/admin/settings/shared/_links.html.haml' + - 'app/views/settings/login_activities/_login_activity.html.haml' + - 'app/views/statuses/_poll.html.haml' + + # Offense count: 3 + IdNames: + exclude: + - 'app/views/authorize_interactions/error.html.haml' + - 'app/views/oauth/authorizations/error.html.haml' + - 'app/views/shared/_error_messages.html.haml' diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000000..d2ae35e84b --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn lint-staged diff --git a/.nvmrc b/.nvmrc index 030fcd56bf..59ea99ee63 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -16.19 +16.20 diff --git a/.prettierignore b/.prettierignore index e55c284164..27b6d5458a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -51,15 +51,8 @@ *~ *.swp -# Ignore npm debug log -npm-debug.log - -# Ignore yarn log files -yarn-error.log -yarn-debug.log - -# Ignore vagrant log files -*-cloudimg-console.log +# Ignore log files +*.log # Ignore Docker option files docker-compose.override.yml @@ -68,12 +61,22 @@ docker-compose.override.yml /app/javascript/mastodon/features/emoji/emoji_map.json # Ignore locale files -/app/javascript/mastodon/locales +/app/javascript/mastodon/locales/*.json /config/locales # Ignore vendored CSS reset app/javascript/styles/mastodon/reset.scss +# Ignore Javascript pending https://github.com/mastodon/mastodon/pull/23631 +*.js +*.jsx + +# Ignore HTML till cleaned and included in CI +*.html + +# Ignore the generated AUTHORS.md +AUTHORS.md + # Ignore glitch-soc emoji map file /app/javascript/flavours/glitch/features/emoji/emoji_map.json diff --git a/.prettierrc.js b/.prettierrc.js index 1d70813d51..af39b253f6 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,3 +1,4 @@ module.exports = { - singleQuote: true + singleQuote: true, + jsxSingleQuote: true } diff --git a/.profile b/.profile index c6d57b609d..f4826ea303 100644 --- a/.profile +++ b/.profile @@ -1 +1 @@ -LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/app/.apt/lib/x86_64-linux-gnu:/app/.apt/usr/lib/x86_64-linux-gnu/mesa:/app/.apt/usr/lib/x86_64-linux-gnu/pulseaudio +LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/app/.apt/lib/x86_64-linux-gnu:/app/.apt/usr/lib/x86_64-linux-gnu/mesa:/app/.apt/usr/lib/x86_64-linux-gnu/pulseaudio:/app/.apt/usr/lib/x86_64-linux-gnu/openblas-pthread diff --git a/.rubocop.yml b/.rubocop.yml index 3c92234705..eff89bdaee 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,479 +1,211 @@ +# Can be removed once all rules are addressed or moved to this file as documented overrides +inherit_from: .rubocop_todo.yml + +# Used for merging with exclude lists with .rubocop_todo.yml +inherit_mode: + merge: + - Exclude + require: - rubocop-rails - rubocop-rspec - rubocop-performance + - rubocop-capybara + - ./lib/linter/rubocop_middle_dot AllCops: - TargetRubyVersion: 2.7 + TargetRubyVersion: 3.0 # Set to minimum supported version of CI DisplayCopNames: true DisplayStyleGuide: true ExtraDetails: true UseCache: true CacheRootDirectory: tmp - NewCops: enable + NewCops: enable # Opt-in to newly added rules Exclude: - db/schema.rb - - 'app/views/**/*' - - 'config/**/*' - 'bin/*' - 'Rakefile' - 'node_modules/**/*' - 'Vagrantfile' - 'vendor/**/*' - - 'lib/json_ld/*' + - 'lib/json_ld/*' # Generated files - 'lib/templates/**/*' -Bundler/OrderedGems: - Enabled: false - -Layout/AccessModifierIndentation: - EnforcedStyle: indent - -Layout/EmptyLineAfterMagicComment: - Enabled: false - -Layout/EmptyLineAfterGuardClause: - Enabled: false - -Layout/EmptyLineBetweenDefs: - AllowAdjacentOneLineDefs: true - -Layout/EmptyLinesAroundAttributeAccessor: - Enabled: true - +# Reason: Prefer Hashes without extreme indentation +# https://docs.rubocop.org/rubocop/cops_layout.html#layoutfirsthashelementindentation Layout/FirstHashElementIndentation: EnforcedStyle: consistent -Layout/HashAlignment: - Enabled: false - -Layout/SpaceAroundMethodCallOperator: - Enabled: true - -Layout/SpaceInsideHashLiteralBraces: - EnforcedStyle: space - -Lint/DeprecatedOpenSSLConstant: - Enabled: true - -Lint/DuplicateElsifCondition: - Enabled: true - -Lint/MixedRegexpCaptureTypes: - Enabled: true - -Lint/RaiseException: - Enabled: true - -Lint/StructNewOverride: - Enabled: true - -Lint/UselessAccessModifier: - ContextCreatingMethods: - - class_methods - -Metrics/AbcSize: - Max: 34 # RuboCop default 17 - Exclude: - - 'lib/**/*cli*.rb' - - db/*migrate/**/* - - lib/paperclip/color_extractor.rb - - app/workers/scheduler/follow_recommendations_scheduler.rb - - app/services/activitypub/fetch*_service.rb - - lib/paperclip/**/* - CountRepeatedAttributes: false - AllowedMethods: - - update_media_attachments! - - account_link_to - - attempt_oembed - - build_crutches - - calculate_scores - - cc - - dump_actor! - - filter_from_home? - - hydrate - - import_bookmarks! - - import_relationships! - - initialize - - link_to_mention - - log_target - - matches_time_window? - - parse_metadata - - perform_statuses_search! - - privatize_media_attachments! - - process_update - - publish_media_attachments! - - remotable_attachment - - render_initial_state - - render_with_cache - - searchable_by - - self.cached_filters_for - - set_fetchable_attributes! - - signed_request_actor - - statuses_to_delete - - update_poll! - -Metrics/BlockLength: - Max: 55 - Exclude: - - 'lib/mastodon/*_cli.rb' - CountComments: false - CountAsOne: [array, heredoc] - AllowedMethods: - - task - - namespace - - class_methods - - included - -Metrics/BlockNesting: - Max: 3 - Exclude: - - 'lib/mastodon/*_cli.rb' - -Metrics/ClassLength: - CountComments: false - Max: 500 - CountAsOne: [array, heredoc] - Exclude: - - 'lib/mastodon/*_cli.rb' - -Metrics/CyclomaticComplexity: - Max: 12 - Exclude: - - lib/mastodon/*cli*.rb - - db/*migrate/**/* - AllowedMethods: - - attempt_oembed - - blocked? - - build_crutches - - calculate_scores - - cc - - discover_endpoint! - - filter_from_home? - - hydrate - - klass - - link_to_mention - - log_target - - matches_time_window? - - patch_for_forwarding! - - preprocess_attributes! - - process_update - - remotable_attachment - - scan_text! - - self.cached_filters_for - - set_fetchable_attributes! - - setup_redis_env_url - - update_media_attachments! - +# Reason: Currently disabled in .rubocop_todo.yml +# https://docs.rubocop.org/rubocop/cops_layout.html#layoutlinelength Layout/LineLength: - Max: 140 # RuboCop default 120 - AllowHeredoc: true - AllowURI: true - IgnoreCopDirectives: true AllowedPatterns: # Allow comments to be long lines - !ruby/regexp / \# .*$/ - !ruby/regexp /^\# .*$/ Exclude: - - lib/**/*cli*.rb + - 'lib/mastodon/cli/*.rb' - db/*migrate/**/* - db/seeds/**/* +# Reason: +# https://docs.rubocop.org/rubocop/cops_lint.html#lintuselessaccessmodifier +Lint/UselessAccessModifier: + ContextCreatingMethods: + - class_methods + +## Disable most Metrics/*Length cops +# Reason: those are often triggered and force significant refactors when this happend +# but the team feel they are not really improving the code quality. + +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocklength +Metrics/BlockLength: + Enabled: false + +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsclasslength +Metrics/ClassLength: + Enabled: false + +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmethodlength Metrics/MethodLength: - CountComments: false - CountAsOne: [array, heredoc] - Max: 25 # RuboCop default 10 - Exclude: - - 'lib/mastodon/*_cli.rb' - AllowedMethods: - - account_link_to - - attempt_oembed - - body_with_limit - - build_crutches - - cached_filters_for - - calculate_scores - - check_webfinger! - - clean_feeds! - - collection_items - - collection_presenter - - copy_account_notes! - - deduplicate_accounts! - - deduplicate_conversations! - - deduplicate_local_accounts! - - deduplicate_statuses! - - deduplicate_tags! - - deduplicate_users! - - discover_endpoint! - - extract_extra_uris_with_indices - - extract_hashtags_with_indices - - extract_mentions_or_lists_with_indices - - filter_from_home? - - from_elasticsearch - - handle_explicit_update! - - handle_mark_as_sensitive! - - hsl_to_rgb - - import_bookmarks! - - import_domain_blocks! - - import_relationships! - - ldap_options - - matches_time_window? - - outbox_presenter - - pam_get_user - - parallelize_with_progress - - parse_and_transform - - patch_for_forwarding! - - populate_home - - post_process_style - - preload_cache_collection_target_statuses - - privatize_media_attachments! - - provides_callback_for - - publish_media_attachments! - - relevant_account_timestamp - - remotable_attachment - - rgb_to_hsl - - rss_status_content_format - - set_fetchable_attributes! - - setup_redis_env_url - - signed_request_actor - - to_preview_card_attributes - - upgrade_storage_filesystem - - upgrade_storage_s3 - - user_settings_params - - hydrate - - cc - - self_destruct + Enabled: false +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmodulelength Metrics/ModuleLength: - CountComments: false - Max: 200 - CountAsOne: [array, heredoc] + Enabled: false +## End Disable Metrics/*Length cops + +# Reason: Currently disabled in .rubocop_todo.yml +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsabcsize +Metrics/AbcSize: + Exclude: + - 'lib/mastodon/cli/*.rb' + - db/*migrate/**/* + +# Reason: +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocknesting +Metrics/BlockNesting: + Exclude: + - 'lib/mastodon/cli/*.rb' + +# Reason: Currently disabled in .rubocop_todo.yml +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricscyclomaticcomplexity +Metrics/CyclomaticComplexity: + Exclude: + - lib/mastodon/cli/*.rb + - db/*migrate/**/* + +# Reason: +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsparameterlists Metrics/ParameterLists: - Max: 5 # RuboCop default 5 - CountKeywordArgs: true # RuboCop default true - MaxOptionalParameters: 3 # RuboCop default 3 - Exclude: - - app/models/concerns/account_interactions.rb - - app/services/activitypub/fetch_remote_account_service.rb - - app/services/activitypub/fetch_remote_actor_service.rb + CountKeywordArgs: false -Metrics/PerceivedComplexity: - Max: 16 # RuboCop default 8 - AllowedMethods: - - attempt_oembed - - build_crutches - - calculate_scores - - deduplicate_users! - - discover_endpoint! - - filter_from_home? - - hydrate - - patch_for_forwarding! - - process_update - - remove_orphans - - update_media_attachments! +# Reason: Prevailing style is argument file paths +# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsfilepath +Rails/FilePath: + EnforcedStyle: arguments -Naming/MemoizedInstanceVariableName: - Enabled: false - -Naming/MethodParameterName: - Enabled: true - -Rails: - Enabled: true - -Rails/ApplicationController: - Enabled: false - Exclude: - - 'app/controllers/well_known/**/*.rb' - -Rails/BelongsTo: - Enabled: false - -Rails/ContentTag: - Enabled: false - -Rails/EnumHash: - Enabled: false +# Reason: Prevailing style uses numeric status codes, matches RSpec/Rails/HttpStatus +# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railshttpstatus +Rails/HttpStatus: + EnforcedStyle: numeric +# Reason: Allowed in `tootctl` CLI code and in boot ENV checker +# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsexit Rails/Exit: Exclude: - - 'lib/mastodon/*' - - 'lib/cli.rb' + - 'config/boot.rb' + - 'lib/mastodon/cli/*.rb' -Rails/FilePath: - Enabled: false +# Reason: Some single letter camel case files shouldn't be split +# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecfilepath +RSpec/FilePath: + CustomTransform: + ActivityPub: activitypub # Ignore the snake_case due to the amount of files to rename + DeepL: deepl + FetchOEmbedService: fetch_oembed_service + JsonLdHelper: jsonld_helper + OEmbedController: oembed_controller + OStatus: ostatus + NodeInfoController: nodeinfo_controller # NodeInfo isn't snake_cased for any of the instances + Exclude: + - 'spec/config/initializers/rack_attack_spec.rb' # namespaces usually have separate folder + - 'spec/lib/sanitize_config_spec.rb' # namespaces usually have separate folder + - 'spec/controllers/concerns/account_controller_concern_spec.rb' # Concerns describe ApplicationController and don't fit naming + - 'spec/controllers/concerns/export_controller_concern_spec.rb' + - 'spec/controllers/concerns/localized_spec.rb' + - 'spec/controllers/concerns/rate_limit_headers_spec.rb' + - 'spec/controllers/concerns/signature_verification_spec.rb' + - 'spec/controllers/concerns/user_tracking_concern_spec.rb' -Rails/HasAndBelongsToMany: - Enabled: false +# Reason: +# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecnamedsubject +RSpec/NamedSubject: + EnforcedStyle: named_only -Rails/HasManyOrHasOneDependent: - Enabled: false +# Reason: Prevailing style choice +# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecnottonot +RSpec/NotToNot: + EnforcedStyle: to_not -Rails/HelperInstanceVariable: - Enabled: false - -Rails/HttpStatus: - Enabled: false - -Rails/IndexBy: - Enabled: false - -Rails/InverseOf: - Enabled: false - -Rails/LexicallyScopedActionFilter: - Enabled: false - -Rails/OutputSafety: - Enabled: true - -Rails/RakeEnvironment: - Enabled: false - -Rails/RedundantForeignKey: - Enabled: false - -Rails/SkipsModelValidations: - Enabled: false - -Rails/UniqueValidationWithoutIndex: - Enabled: false - -Style/AccessorGrouping: - Enabled: true - -Style/AccessModifierDeclarations: - Enabled: false - -Style/ArrayCoercion: - Enabled: true - -Style/BisectedAttrAccessor: - Enabled: true - -Style/CaseLikeIf: - Enabled: false +# Reason: Prevailing style uses numeric status codes, matches Rails/HttpStatus +# https://docs.rubocop.org/rubocop-rspec/cops_rspec_rails.html#rspecrailshttpstatus +RSpec/Rails/HttpStatus: + EnforcedStyle: numeric +# Reason: +# https://docs.rubocop.org/rubocop/cops_style.html#styleclassandmodulechildren Style/ClassAndModuleChildren: Enabled: false -Style/CollectionMethods: - Enabled: true - PreferredMethods: - find_all: 'select' - +# Reason: Classes mostly self-document with their names +# https://docs.rubocop.org/rubocop/cops_style.html#styledocumentation Style/Documentation: Enabled: false -Style/DoubleNegation: - Enabled: true - -Style/ExpandPathArguments: - Enabled: false - -Style/ExponentialNotation: - Enabled: true - -Style/FormatString: - Enabled: false - -Style/FormatStringToken: - Enabled: false - -Style/FrozenStringLiteralComment: - Enabled: true - -Style/GuardClause: - Enabled: false - -Style/HashAsLastArrayItem: - Enabled: false - -Style/HashEachMethods: - Enabled: true - -Style/HashLikeCase: - Enabled: true - -Style/HashTransformKeys: - Enabled: true - -Style/HashTransformValues: - Enabled: false - +# Reason: Enforce modern Ruby style +# https://docs.rubocop.org/rubocop/cops_style.html#stylehashsyntax Style/HashSyntax: - Enabled: true EnforcedStyle: ruby19_no_mixed_keys -Style/IfUnlessModifier: - Enabled: false - -Style/InverseMethods: - Enabled: false - -Style/Lambda: - Enabled: false - -Style/MutableConstant: - Enabled: false +# Reason: +# https://docs.rubocop.org/rubocop/cops_style.html#stylenumericliterals +Style/NumericLiterals: + AllowedPatterns: + - \d{4}_\d{2}_\d{2}_\d{6} # For DB migration date version number readability +# Reason: +# https://docs.rubocop.org/rubocop/cops_style.html#stylepercentliteraldelimiters Style/PercentLiteralDelimiters: PreferredDelimiters: '%i': '()' '%w': '()' -Style/PerlBackrefs: - AutoCorrect: false - -Style/RedundantFetchBlock: - Enabled: true - -Style/RedundantFileExtensionInRequire: - Enabled: true - -Style/RedundantRegexpCharacterClass: - Enabled: false - -Style/RedundantRegexpEscape: - Enabled: false - -Style/RedundantReturn: - Enabled: true - +# Reason: Prefer less indentation in conditional assignments +# https://docs.rubocop.org/rubocop/cops_style.html#styleredundantbegin Style/RedundantBegin: Enabled: false -Style/RegexpLiteral: - Enabled: false - +# Reason: Overridden to reduce implicit StandardError rescues +# https://docs.rubocop.org/rubocop/cops_style.html#stylerescuestandarderror Style/RescueStandardError: - Enabled: true - -Style/SignalException: - Enabled: false - -Style/SlicingWithRange: - Enabled: true + EnforcedStyle: implicit +# Reason: Originally disabled for CodeClimate, and no config consensus has been found +# https://docs.rubocop.org/rubocop/cops_style.html#stylesymbolarray Style/SymbolArray: Enabled: false +# Reason: +# https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainarrayliteral Style/TrailingCommaInArrayLiteral: EnforcedStyleForMultiline: 'comma' +# Reason: +# https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainhashliteral Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: 'comma' -Style/UnpackFirst: - Enabled: false - -RSpec/ScatteredSetup: - Enabled: false -RSpec/ImplicitExpect: - Enabled: false -RSpec/NamedSubject: - Enabled: false -RSpec/DescribeClass: - Enabled: false -RSpec/LetSetup: - Enabled: false +Style/MiddleDot: + Enabled: true diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 0000000000..c1d580e515 --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,1452 @@ +# This configuration was generated by +# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` +# using RuboCop version 1.52.1. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include. +# Include: **/*.gemfile, **/Gemfile, **/gems.rb +Bundler/OrderedGems: + Exclude: + - 'Gemfile' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, IndentationWidth. +# SupportedStyles: with_first_argument, with_fixed_indentation +Layout/ArgumentAlignment: + Exclude: + - 'config/initializers/cors.rb' + - 'config/initializers/session_store.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. +# SupportedHashRocketStyles: key, separator, table +# SupportedColonStyles: key, separator, table +# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit +Layout/HashAlignment: + Exclude: + - 'config/boot.rb' + - 'config/environments/production.rb' + - 'config/initializers/rack_attack.rb' + - 'config/routes.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowDoxygenCommentStyle, AllowGemfileRubyComment. +Layout/LeadingCommentSpace: + Exclude: + - 'config/application.rb' + - 'config/initializers/omniauth.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: require_no_space, require_space +Layout/SpaceInLambdaLiteral: + Exclude: + - 'config/environments/production.rb' + - 'config/initializers/content_security_policy.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowedMethods, AllowedPatterns. +Lint/AmbiguousBlockAssociation: + Exclude: + - 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb' + - 'spec/controllers/settings/two_factor_authentication/otp_authentication_controller_spec.rb' + - 'spec/services/activitypub/process_status_update_service_spec.rb' + - 'spec/services/post_status_service_spec.rb' + +# Configuration parameters: AllowComments, AllowEmptyLambdas. +Lint/EmptyBlock: + Exclude: + - 'spec/controllers/api/v2/search_controller_spec.rb' + - 'spec/fabricators/access_token_fabricator.rb' + - 'spec/fabricators/conversation_fabricator.rb' + - 'spec/fabricators/system_key_fabricator.rb' + - 'spec/helpers/admin/action_logs_helper_spec.rb' + - 'spec/lib/activitypub/adapter_spec.rb' + - 'spec/models/account_alias_spec.rb' + - 'spec/models/account_deletion_request_spec.rb' + - 'spec/models/account_moderation_note_spec.rb' + - 'spec/models/announcement_mute_spec.rb' + - 'spec/models/announcement_reaction_spec.rb' + - 'spec/models/announcement_spec.rb' + - 'spec/models/backup_spec.rb' + - 'spec/models/conversation_mute_spec.rb' + - 'spec/models/custom_filter_keyword_spec.rb' + - 'spec/models/custom_filter_spec.rb' + - 'spec/models/device_spec.rb' + - 'spec/models/encrypted_message_spec.rb' + - 'spec/models/featured_tag_spec.rb' + - 'spec/models/follow_recommendation_suppression_spec.rb' + - 'spec/models/list_account_spec.rb' + - 'spec/models/list_spec.rb' + - 'spec/models/login_activity_spec.rb' + - 'spec/models/mute_spec.rb' + - 'spec/models/preview_card_spec.rb' + - 'spec/models/preview_card_trend_spec.rb' + - 'spec/models/relay_spec.rb' + - 'spec/models/scheduled_status_spec.rb' + - 'spec/models/status_stat_spec.rb' + - 'spec/models/status_trend_spec.rb' + - 'spec/models/system_key_spec.rb' + - 'spec/models/tag_follow_spec.rb' + - 'spec/models/unavailable_domain_spec.rb' + - 'spec/models/user_invite_request_spec.rb' + - 'spec/models/user_role_spec.rb' + - 'spec/models/web/setting_spec.rb' + +Lint/NonLocalExitFromIterator: + Exclude: + - 'app/helpers/jsonld_helper.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Lint/OrAssignmentToConstant: + Exclude: + - 'lib/sanitize_ext/sanitize_config.rb' + +# This cop supports safe autocorrection (--autocorrect). +Lint/SendWithMixinArgument: + Exclude: + - 'config/application.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. +Lint/UnusedBlockArgument: + Exclude: + - 'config/initializers/content_security_policy.rb' + - 'config/initializers/doorkeeper.rb' + - 'config/initializers/paperclip.rb' + - 'config/initializers/simple_form.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Lint/UselessAssignment: + Exclude: + - 'app/services/activitypub/process_status_update_service.rb' + - 'config/initializers/omniauth.rb' + - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb' + - 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb' + - 'spec/controllers/api/v1/bookmarks_controller_spec.rb' + - 'spec/controllers/api/v1/favourites_controller_spec.rb' + - 'spec/controllers/concerns/account_controller_concern_spec.rb' + - 'spec/helpers/jsonld_helper_spec.rb' + - 'spec/models/account_spec.rb' + - 'spec/models/domain_block_spec.rb' + - 'spec/models/status_spec.rb' + - 'spec/models/user_spec.rb' + - 'spec/models/webauthn_credentials_spec.rb' + - 'spec/services/account_search_service_spec.rb' + - 'spec/services/post_status_service_spec.rb' + - 'spec/services/precompute_feed_service_spec.rb' + - 'spec/services/resolve_url_service_spec.rb' + - 'spec/views/statuses/show.html.haml_spec.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: CheckForMethodsWithNoSideEffects. +Lint/Void: + Exclude: + - 'spec/services/resolve_account_service_spec.rb' + +# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. +Metrics/AbcSize: + Max: 150 + Exclude: + - 'app/serializers/initial_state_serializer.rb' + +# Configuration parameters: CountBlocks, Max. +Metrics/BlockNesting: + Exclude: + - 'lib/tasks/mastodon.rake' + +# Configuration parameters: AllowedMethods, AllowedPatterns. +Metrics/CyclomaticComplexity: + Max: 25 + +# Configuration parameters: AllowedMethods, AllowedPatterns. +Metrics/PerceivedComplexity: + Max: 27 + +Naming/AccessorMethodName: + Exclude: + - 'app/controllers/auth/sessions_controller.rb' + +# Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms. +# CheckDefinitionPathHierarchyRoots: lib, spec, test, src +# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS +Naming/FileName: + Exclude: + - 'config/locales/sr-Latn.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyleForLeadingUnderscores. +# SupportedStylesForLeadingUnderscores: disallowed, required, optional +Naming/MemoizedInstanceVariableName: + Exclude: + - 'app/controllers/api/v1/bookmarks_controller.rb' + - 'app/controllers/api/v1/favourites_controller.rb' + - 'app/controllers/concerns/rate_limit_headers.rb' + - 'app/lib/activitypub/activity.rb' + - 'app/services/resolve_url_service.rb' + - 'app/services/search_service.rb' + - 'config/initializers/rack_attack.rb' + +# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. +# SupportedStyles: snake_case, normalcase, non_integer +# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64 +Naming/VariableNumber: + Exclude: + - 'db/migrate/20180106000232_add_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb' + - 'db/migrate/20180514140000_revert_index_change_on_statuses_for_api_v1_accounts_account_id_statuses.rb' + - 'db/migrate/20190820003045_update_statuses_index.rb' + - 'db/migrate/20190823221802_add_local_index_to_statuses.rb' + - 'db/migrate/20200119112504_add_public_index_to_statuses.rb' + - 'spec/models/account_spec.rb' + - 'spec/models/domain_block_spec.rb' + - 'spec/models/user_spec.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Performance/UnfreezeString: + Exclude: + - 'app/lib/rss/builder.rb' + - 'app/lib/text_formatter.rb' + - 'app/validators/status_length_validator.rb' + - 'lib/tasks/mastodon.rake' + +RSpec/AnyInstance: + Exclude: + - 'spec/controllers/activitypub/inboxes_controller_spec.rb' + - 'spec/controllers/admin/accounts_controller_spec.rb' + - 'spec/controllers/admin/resets_controller_spec.rb' + - 'spec/controllers/admin/settings/branding_controller_spec.rb' + - 'spec/controllers/api/v1/media_controller_spec.rb' + - 'spec/controllers/auth/sessions_controller_spec.rb' + - 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb' + - 'spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb' + - 'spec/lib/request_spec.rb' + - 'spec/lib/status_filter_spec.rb' + - 'spec/models/account_spec.rb' + - 'spec/models/setting_spec.rb' + - 'spec/services/activitypub/process_collection_service_spec.rb' + - 'spec/validators/blacklisted_email_validator_spec.rb' + - 'spec/validators/follow_limit_validator_spec.rb' + - 'spec/workers/activitypub/delivery_worker_spec.rb' + - 'spec/workers/web/push_notification_worker_spec.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +RSpec/EmptyExampleGroup: + Exclude: + - 'spec/helpers/admin/action_logs_helper_spec.rb' + - 'spec/models/account_alias_spec.rb' + - 'spec/models/account_deletion_request_spec.rb' + - 'spec/models/account_moderation_note_spec.rb' + - 'spec/models/announcement_mute_spec.rb' + - 'spec/models/announcement_reaction_spec.rb' + - 'spec/models/announcement_spec.rb' + - 'spec/models/backup_spec.rb' + - 'spec/models/conversation_mute_spec.rb' + - 'spec/models/custom_filter_keyword_spec.rb' + - 'spec/models/custom_filter_spec.rb' + - 'spec/models/device_spec.rb' + - 'spec/models/encrypted_message_spec.rb' + - 'spec/models/featured_tag_spec.rb' + - 'spec/models/follow_recommendation_suppression_spec.rb' + - 'spec/models/list_account_spec.rb' + - 'spec/models/list_spec.rb' + - 'spec/models/login_activity_spec.rb' + - 'spec/models/mute_spec.rb' + - 'spec/models/preview_card_spec.rb' + - 'spec/models/preview_card_trend_spec.rb' + - 'spec/models/relay_spec.rb' + - 'spec/models/scheduled_status_spec.rb' + - 'spec/models/status_stat_spec.rb' + - 'spec/models/status_trend_spec.rb' + - 'spec/models/system_key_spec.rb' + - 'spec/models/tag_follow_spec.rb' + - 'spec/models/unavailable_domain_spec.rb' + - 'spec/models/user_invite_request_spec.rb' + - 'spec/models/web/setting_spec.rb' + - 'spec/services/unmute_service_spec.rb' + +# Configuration parameters: CountAsOne. +RSpec/ExampleLength: + Max: 22 + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: implicit, each, example +RSpec/HookArgument: + Exclude: + - 'spec/controllers/api/v1/streaming_controller_spec.rb' + - 'spec/controllers/well_known/webfinger_controller_spec.rb' + - 'spec/helpers/instance_helper_spec.rb' + - 'spec/models/user_spec.rb' + - 'spec/rails_helper.rb' + - 'spec/serializers/activitypub/note_serializer_spec.rb' + - 'spec/serializers/activitypub/update_poll_serializer_spec.rb' + - 'spec/services/import_service_spec.rb' + - 'spec/spec_helper.rb' + +# Configuration parameters: AssignmentOnly. +RSpec/InstanceVariable: + Exclude: + - 'spec/controllers/api/v1/streaming_controller_spec.rb' + - 'spec/controllers/auth/confirmations_controller_spec.rb' + - 'spec/controllers/auth/passwords_controller_spec.rb' + - 'spec/controllers/auth/sessions_controller_spec.rb' + - 'spec/controllers/concerns/export_controller_concern_spec.rb' + - 'spec/controllers/home_controller_spec.rb' + - 'spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb' + - 'spec/controllers/statuses_cleanup_controller_spec.rb' + - 'spec/models/concerns/account_finder_concern_spec.rb' + - 'spec/models/concerns/account_interactions_spec.rb' + - 'spec/models/public_feed_spec.rb' + - 'spec/serializers/activitypub/note_serializer_spec.rb' + - 'spec/serializers/activitypub/update_poll_serializer_spec.rb' + - 'spec/services/remove_status_service_spec.rb' + - 'spec/services/search_service_spec.rb' + - 'spec/services/unblock_domain_service_spec.rb' + +RSpec/LetSetup: + Exclude: + - 'spec/controllers/admin/accounts_controller_spec.rb' + - 'spec/controllers/admin/action_logs_controller_spec.rb' + - 'spec/controllers/admin/instances_controller_spec.rb' + - 'spec/controllers/admin/reports/actions_controller_spec.rb' + - 'spec/controllers/admin/statuses_controller_spec.rb' + - 'spec/controllers/api/v1/accounts/statuses_controller_spec.rb' + - 'spec/controllers/api/v1/admin/accounts_controller_spec.rb' + - 'spec/controllers/api/v1/filters_controller_spec.rb' + - 'spec/controllers/api/v1/followed_tags_controller_spec.rb' + - 'spec/controllers/api/v2/admin/accounts_controller_spec.rb' + - 'spec/controllers/api/v2/filters/keywords_controller_spec.rb' + - 'spec/controllers/api/v2/filters/statuses_controller_spec.rb' + - 'spec/controllers/api/v2/filters_controller_spec.rb' + - 'spec/controllers/auth/confirmations_controller_spec.rb' + - 'spec/controllers/auth/passwords_controller_spec.rb' + - 'spec/controllers/auth/sessions_controller_spec.rb' + - 'spec/controllers/follower_accounts_controller_spec.rb' + - 'spec/controllers/following_accounts_controller_spec.rb' + - 'spec/controllers/oauth/authorized_applications_controller_spec.rb' + - 'spec/controllers/oauth/tokens_controller_spec.rb' + - 'spec/controllers/settings/imports_controller_spec.rb' + - 'spec/lib/activitypub/activity/delete_spec.rb' + - 'spec/lib/vacuum/preview_cards_vacuum_spec.rb' + - 'spec/models/account_spec.rb' + - 'spec/models/account_statuses_cleanup_policy_spec.rb' + - 'spec/models/canonical_email_block_spec.rb' + - 'spec/models/status_spec.rb' + - 'spec/models/user_spec.rb' + - 'spec/services/account_statuses_cleanup_service_spec.rb' + - 'spec/services/activitypub/fetch_featured_collection_service_spec.rb' + - 'spec/services/activitypub/fetch_remote_status_service_spec.rb' + - 'spec/services/activitypub/process_account_service_spec.rb' + - 'spec/services/activitypub/process_collection_service_spec.rb' + - 'spec/services/batched_remove_status_service_spec.rb' + - 'spec/services/block_domain_service_spec.rb' + - 'spec/services/bulk_import_service_spec.rb' + - 'spec/services/delete_account_service_spec.rb' + - 'spec/services/import_service_spec.rb' + - 'spec/services/notify_service_spec.rb' + - 'spec/services/remove_status_service_spec.rb' + - 'spec/services/report_service_spec.rb' + - 'spec/services/resolve_account_service_spec.rb' + - 'spec/services/suspend_account_service_spec.rb' + - 'spec/services/unallow_domain_service_spec.rb' + - 'spec/services/unsuspend_account_service_spec.rb' + - 'spec/workers/scheduler/user_cleanup_scheduler_spec.rb' + +RSpec/MessageChain: + Exclude: + - 'spec/controllers/api/v1/media_controller_spec.rb' + - 'spec/models/concerns/remotable_spec.rb' + - 'spec/models/session_activation_spec.rb' + - 'spec/models/setting_spec.rb' + +# Configuration parameters: EnforcedStyle. +# SupportedStyles: have_received, receive +RSpec/MessageSpies: + Exclude: + - 'spec/controllers/admin/accounts_controller_spec.rb' + - 'spec/controllers/api/base_controller_spec.rb' + - 'spec/controllers/auth/registrations_controller_spec.rb' + - 'spec/helpers/admin/account_moderation_notes_helper_spec.rb' + - 'spec/helpers/application_helper_spec.rb' + - 'spec/lib/status_finder_spec.rb' + - 'spec/lib/webfinger_resource_spec.rb' + - 'spec/models/admin/account_action_spec.rb' + - 'spec/models/concerns/remotable_spec.rb' + - 'spec/models/follow_request_spec.rb' + - 'spec/models/identity_spec.rb' + - 'spec/models/session_activation_spec.rb' + - 'spec/models/setting_spec.rb' + - 'spec/services/activitypub/fetch_replies_service_spec.rb' + - 'spec/services/activitypub/process_collection_service_spec.rb' + - 'spec/spec_helper.rb' + - 'spec/validators/status_length_validator_spec.rb' + +RSpec/MultipleExpectations: + Max: 8 + +# Configuration parameters: AllowSubject. +RSpec/MultipleMemoizedHelpers: + Max: 21 + +# Configuration parameters: AllowedGroups. +RSpec/NestedGroups: + Max: 6 + +RSpec/PendingWithoutReason: + Exclude: + - 'spec/models/account_spec.rb' + +RSpec/StubbedMock: + Exclude: + - 'spec/controllers/api/base_controller_spec.rb' + - 'spec/controllers/api/v1/media_controller_spec.rb' + - 'spec/controllers/auth/registrations_controller_spec.rb' + - 'spec/helpers/application_helper_spec.rb' + - 'spec/lib/status_filter_spec.rb' + - 'spec/lib/status_finder_spec.rb' + - 'spec/lib/webfinger_resource_spec.rb' + - 'spec/services/activitypub/process_collection_service_spec.rb' + +RSpec/SubjectDeclaration: + Exclude: + - 'spec/controllers/admin/domain_blocks_controller_spec.rb' + - 'spec/models/account_migration_spec.rb' + - 'spec/models/account_spec.rb' + - 'spec/models/relationship_filter_spec.rb' + - 'spec/models/user_role_spec.rb' + - 'spec/policies/account_moderation_note_policy_spec.rb' + - 'spec/policies/account_policy_spec.rb' + - 'spec/policies/backup_policy_spec.rb' + - 'spec/policies/custom_emoji_policy_spec.rb' + - 'spec/policies/domain_block_policy_spec.rb' + - 'spec/policies/email_domain_block_policy_spec.rb' + - 'spec/policies/instance_policy_spec.rb' + - 'spec/policies/invite_policy_spec.rb' + - 'spec/policies/relay_policy_spec.rb' + - 'spec/policies/report_note_policy_spec.rb' + - 'spec/policies/report_policy_spec.rb' + - 'spec/policies/settings_policy_spec.rb' + - 'spec/policies/tag_policy_spec.rb' + - 'spec/policies/user_policy_spec.rb' + - 'spec/services/activitypub/process_account_service_spec.rb' + +RSpec/SubjectStub: + Exclude: + - 'spec/services/unallow_domain_service_spec.rb' + - 'spec/validators/blacklisted_email_validator_spec.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Rails/ApplicationController: + Exclude: + - 'app/controllers/health_controller.rb' + +# Configuration parameters: Database, Include. +# SupportedDatabases: mysql, postgresql +# Include: db/migrate/*.rb +Rails/BulkChangeTable: + Exclude: + - 'db/migrate/20160222143943_add_profile_fields_to_accounts.rb' + - 'db/migrate/20160223162837_add_metadata_to_statuses.rb' + - 'db/migrate/20160305115639_add_devise_to_users.rb' + - 'db/migrate/20160314164231_add_owner_to_application.rb' + - 'db/migrate/20160926213048_remove_owner_from_application.rb' + - 'db/migrate/20161003142332_add_confirmable_to_users.rb' + - 'db/migrate/20170112154826_migrate_settings.rb' + - 'db/migrate/20170127165745_add_devise_two_factor_to_users.rb' + - 'db/migrate/20170322143850_change_primary_key_to_bigint_on_statuses.rb' + - 'db/migrate/20170330021336_add_counter_caches.rb' + - 'db/migrate/20170425202925_add_oembed_to_preview_cards.rb' + - 'db/migrate/20170427011934_re_add_owner_to_application.rb' + - 'db/migrate/20170520145338_change_language_filter_to_opt_out.rb' + - 'db/migrate/20170624134742_add_description_to_session_activations.rb' + - 'db/migrate/20170718211102_add_activitypub_to_accounts.rb' + - 'db/migrate/20171006142024_add_uri_to_custom_emojis.rb' + - 'db/migrate/20180812123222_change_relays_enabled.rb' + - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb' + - 'db/migrate/20190805123746_add_capabilities_to_tags.rb' + - 'db/migrate/20190807135426_add_comments_to_domain_blocks.rb' + - 'db/migrate/20190815225426_add_last_status_at_to_tags.rb' + - 'db/migrate/20190901035623_add_max_score_to_tags.rb' + - 'db/migrate/20200417125749_add_storage_schema_version.rb' + - 'db/migrate/20200608113046_add_sign_in_token_to_users.rb' + - 'db/migrate/20211112011713_add_language_to_preview_cards.rb' + - 'db/migrate/20211231080958_add_category_to_reports.rb' + - 'db/migrate/20220202200743_add_trendable_to_accounts.rb' + - 'db/migrate/20220224010024_add_ips_to_email_domain_blocks.rb' + - 'db/migrate/20220227041951_add_last_used_at_to_oauth_access_tokens.rb' + - 'db/migrate/20220303000827_add_ordered_media_attachment_ids_to_status_edits.rb' + - 'db/migrate/20220824164433_add_human_identifier_to_admin_action_logs.rb' + +# Configuration parameters: Include. +# Include: db/migrate/*.rb +Rails/CreateTableWithTimestamps: + Exclude: + - 'db/migrate/20170508230434_create_conversation_mutes.rb' + - 'db/migrate/20170823162448_create_status_pins.rb' + - 'db/migrate/20171116161857_create_list_accounts.rb' + - 'db/migrate/20180929222014_create_account_conversations.rb' + - 'db/migrate/20181007025445_create_pghero_space_stats.rb' + - 'db/migrate/20190103124649_create_scheduled_statuses.rb' + - 'db/migrate/20220824233535_create_status_trends.rb' + - 'db/migrate/20221006061337_create_preview_card_trends.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: Severity. +Rails/DuplicateAssociation: + Exclude: + - 'app/serializers/activitypub/collection_serializer.rb' + - 'app/serializers/activitypub/note_serializer.rb' + +# Configuration parameters: Include. +# Include: app/models/**/*.rb +Rails/HasAndBelongsToMany: + Exclude: + - 'app/models/concerns/account_associations.rb' + - 'app/models/preview_card.rb' + - 'app/models/status.rb' + - 'app/models/tag.rb' + +# Configuration parameters: Include. +# Include: app/models/**/*.rb +Rails/HasManyOrHasOneDependent: + Exclude: + - 'app/models/concerns/account_counters.rb' + - 'app/models/conversation.rb' + - 'app/models/custom_emoji.rb' + - 'app/models/custom_emoji_category.rb' + - 'app/models/domain_block.rb' + - 'app/models/invite.rb' + - 'app/models/status.rb' + - 'app/models/user.rb' + - 'app/models/web/push_subscription.rb' + +Rails/I18nLocaleTexts: + Exclude: + - 'lib/tasks/mastodon.rake' + - 'spec/helpers/flashes_helper_spec.rb' + +# Configuration parameters: Include. +# Include: app/controllers/**/*.rb, app/mailers/**/*.rb +Rails/LexicallyScopedActionFilter: + Exclude: + - 'app/controllers/auth/passwords_controller.rb' + - 'app/controllers/auth/registrations_controller.rb' + - 'app/controllers/auth/sessions_controller.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Rails/NegateInclude: + Exclude: + - 'app/controllers/concerns/signature_verification.rb' + - 'app/helpers/jsonld_helper.rb' + - 'app/lib/activitypub/activity/create.rb' + - 'app/lib/activitypub/activity/move.rb' + - 'app/lib/feed_manager.rb' + - 'app/lib/link_details_extractor.rb' + - 'app/models/concerns/attachmentable.rb' + - 'app/models/concerns/remotable.rb' + - 'app/models/custom_filter.rb' + - 'app/services/activitypub/process_status_update_service.rb' + - 'app/services/fetch_link_card_service.rb' + - 'app/services/search_service.rb' + - 'app/workers/web/push_notification_worker.rb' + - 'lib/paperclip/color_extractor.rb' + +Rails/OutputSafety: + Exclude: + - 'config/initializers/simple_form.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: Include. +# Include: **/Rakefile, **/*.rake +Rails/RakeEnvironment: + Exclude: + - 'lib/tasks/auto_annotate_models.rake' + - 'lib/tasks/db.rake' + - 'lib/tasks/emojis.rake' + - 'lib/tasks/mastodon.rake' + - 'lib/tasks/repo.rake' + - 'lib/tasks/statistics.rake' + +# Configuration parameters: Include. +# Include: db/**/*.rb +Rails/ReversibleMigration: + Exclude: + - 'db/migrate/20160223164502_make_uris_nullable_in_statuses.rb' + - 'db/migrate/20161122163057_remove_unneeded_indexes.rb' + - 'db/migrate/20170205175257_remove_devices.rb' + - 'db/migrate/20170322143850_change_primary_key_to_bigint_on_statuses.rb' + - 'db/migrate/20170520145338_change_language_filter_to_opt_out.rb' + - 'db/migrate/20170609145826_remove_default_language_from_statuses.rb' + - 'db/migrate/20170711225116_fix_null_booleans.rb' + - 'db/migrate/20171129172043_add_index_on_stream_entries.rb' + - 'db/migrate/20171212195226_remove_duplicate_indexes_in_lists.rb' + - 'db/migrate/20171226094803_more_faster_index_on_notifications.rb' + - 'db/migrate/20180106000232_add_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb' + - 'db/migrate/20180617162849_remove_unused_indexes.rb' + - 'db/migrate/20220827195229_change_canonical_email_blocks_nullable.rb' + +# Configuration parameters: ForbiddenMethods, AllowedMethods. +# ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all +Rails/SkipsModelValidations: + Exclude: + - 'app/controllers/admin/invites_controller.rb' + - 'app/controllers/concerns/session_tracking_concern.rb' + - 'app/models/concerns/account_merging.rb' + - 'app/models/concerns/expireable.rb' + - 'app/models/status.rb' + - 'app/models/trends/links.rb' + - 'app/models/trends/preview_card_batch.rb' + - 'app/models/trends/preview_card_provider_batch.rb' + - 'app/models/trends/status_batch.rb' + - 'app/models/trends/statuses.rb' + - 'app/models/trends/tag_batch.rb' + - 'app/models/trends/tags.rb' + - 'app/models/user.rb' + - 'app/services/activitypub/process_status_update_service.rb' + - 'app/services/approve_appeal_service.rb' + - 'app/services/block_domain_service.rb' + - 'app/services/delete_account_service.rb' + - 'app/services/process_mentions_service.rb' + - 'app/services/unallow_domain_service.rb' + - 'app/services/unblock_domain_service.rb' + - 'app/services/update_status_service.rb' + - 'app/workers/activitypub/post_upgrade_worker.rb' + - 'app/workers/move_worker.rb' + - 'app/workers/scheduler/ip_cleanup_scheduler.rb' + - 'app/workers/scheduler/scheduled_statuses_scheduler.rb' + - 'db/migrate/20161203164520_add_from_account_id_to_notifications.rb' + - 'db/migrate/20170105224407_add_shortcode_to_media_attachments.rb' + - 'db/migrate/20170209184350_add_reply_to_statuses.rb' + - 'db/migrate/20170304202101_add_type_to_media_attachments.rb' + - 'db/migrate/20180528141303_fix_accounts_unique_index.rb' + - 'db/migrate/20180609104432_migrate_web_push_subscriptions2.rb' + - 'db/migrate/20181207011115_downcase_custom_emoji_domains.rb' + - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb' + - 'db/migrate/20191007013357_update_pt_locales.rb' + - 'db/migrate/20220316233212_update_kurdish_locales.rb' + - 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb' + - 'db/post_migrate/20200917193528_migrate_notifications_type.rb' + - 'db/post_migrate/20201017234926_fill_account_suspension_origin.rb' + - 'db/post_migrate/20220617202502_migrate_roles.rb' + - 'db/post_migrate/20221101190723_backfill_admin_action_logs.rb' + - 'db/post_migrate/20221206114142_backfill_admin_action_logs_again.rb' + - 'lib/mastodon/cli/accounts.rb' + - 'lib/mastodon/cli/main.rb' + - 'lib/mastodon/cli/maintenance.rb' + - 'spec/controllers/api/v1/admin/accounts_controller_spec.rb' + - 'spec/lib/activitypub/activity/follow_spec.rb' + - 'spec/services/follow_service_spec.rb' + - 'spec/services/update_account_service_spec.rb' + +# Configuration parameters: Include. +# Include: db/**/*.rb +Rails/ThreeStateBooleanColumn: + Exclude: + - 'db/migrate/20160325130944_add_admin_to_users.rb' + - 'db/migrate/20161123093447_add_sensitive_to_statuses.rb' + - 'db/migrate/20170123203248_add_reject_media_to_domain_blocks.rb' + - 'db/migrate/20170127165745_add_devise_two_factor_to_users.rb' + - 'db/migrate/20170209184350_add_reply_to_statuses.rb' + - 'db/migrate/20170330163835_create_imports.rb' + - 'db/migrate/20170905165803_add_local_to_statuses.rb' + - 'db/migrate/20171210213213_add_local_only_flag_to_statuses.rb' + - 'db/migrate/20181203021853_add_discoverable_to_accounts.rb' + - 'db/migrate/20190509164208_add_by_moderator_to_tombstone.rb' + - 'db/migrate/20190805123746_add_capabilities_to_tags.rb' + - 'db/migrate/20191212163405_add_hide_collections_to_accounts.rb' + - 'db/migrate/20200309150742_add_forwarded_to_reports.rb' + - 'db/migrate/20210609202149_create_login_activities.rb' + - 'db/migrate/20210621221010_add_skip_sign_in_token_to_users.rb' + - 'db/migrate/20211031031021_create_preview_card_providers.rb' + - 'db/migrate/20211115032527_add_trendable_to_preview_cards.rb' + - 'db/migrate/20220202200743_add_trendable_to_accounts.rb' + - 'db/migrate/20220202200926_add_trendable_to_statuses.rb' + - 'db/migrate/20220303000827_add_ordered_media_attachment_ids_to_status_edits.rb' + +# Configuration parameters: Include. +# Include: app/models/**/*.rb +Rails/UniqueValidationWithoutIndex: + Exclude: + - 'app/models/account_alias.rb' + - 'app/models/custom_filter_status.rb' + - 'app/models/identity.rb' + - 'app/models/webauthn_credential.rb' + +# Configuration parameters: Include. +# Include: app/models/**/*.rb +Rails/UnusedIgnoredColumns: + Exclude: + - 'app/models/account.rb' + - 'app/models/account_stat.rb' + - 'app/models/admin/action_log.rb' + - 'app/models/custom_filter.rb' + - 'app/models/email_domain_block.rb' + - 'app/models/report.rb' + - 'app/models/status_edit.rb' + - 'app/models/user.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: exists, where +Rails/WhereExists: + Exclude: + - 'app/controllers/activitypub/inboxes_controller.rb' + - 'app/controllers/admin/email_domain_blocks_controller.rb' + - 'app/controllers/auth/registrations_controller.rb' + - 'app/lib/activitypub/activity/create.rb' + - 'app/lib/delivery_failure_tracker.rb' + - 'app/lib/feed_manager.rb' + - 'app/lib/status_cache_hydrator.rb' + - 'app/lib/suspicious_sign_in_detector.rb' + - 'app/models/concerns/account_interactions.rb' + - 'app/models/featured_tag.rb' + - 'app/models/poll.rb' + - 'app/models/session_activation.rb' + - 'app/models/status.rb' + - 'app/models/user.rb' + - 'app/policies/status_policy.rb' + - 'app/serializers/rest/announcement_serializer.rb' + - 'app/serializers/rest/tag_serializer.rb' + - 'app/services/activitypub/fetch_remote_status_service.rb' + - 'app/services/app_sign_up_service.rb' + - 'app/services/vote_service.rb' + - 'app/validators/reaction_validator.rb' + - 'app/validators/vote_validator.rb' + - 'app/workers/move_worker.rb' + - 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb' + - 'lib/tasks/tests.rake' + - 'spec/models/account_spec.rb' + - 'spec/services/activitypub/process_collection_service_spec.rb' + - 'spec/services/purge_domain_service_spec.rb' + - 'spec/services/unallow_domain_service_spec.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowOnConstant, AllowOnSelfClass. +Style/CaseEquality: + Exclude: + - 'config/initializers/trusted_proxies.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowedMethods, AllowedPatterns. +# AllowedMethods: ==, equal?, eql? +Style/ClassEqualityComparison: + Exclude: + - 'app/helpers/jsonld_helper.rb' + - 'app/serializers/activitypub/outbox_serializer.rb' + +Style/ClassVars: + Exclude: + - 'config/initializers/devise.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/CombinableLoops: + Exclude: + - 'app/models/form/custom_emoji_batch.rb' + - 'app/models/form/ip_block_batch.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowedVars. +Style/FetchEnvVar: + Exclude: + - 'app/lib/redis_configuration.rb' + - 'app/lib/translation_service.rb' + - 'config/environments/development.rb' + - 'config/environments/production.rb' + - 'config/initializers/2_whitelist_mode.rb' + - 'config/initializers/blacklists.rb' + - 'config/initializers/cache_buster.rb' + - 'config/initializers/content_security_policy.rb' + - 'config/initializers/devise.rb' + - 'config/initializers/omniauth.rb' + - 'config/initializers/paperclip.rb' + - 'config/initializers/vapid.rb' + - 'lib/mastodon/premailer_webpack_strategy.rb' + - 'lib/mastodon/redis_config.rb' + - 'lib/tasks/repo.rake' + - 'spec/features/profile_spec.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns. +# SupportedStyles: annotated, template, unannotated +# AllowedMethods: redirect +Style/FormatStringToken: + Exclude: + - 'app/models/privacy_policy.rb' + - 'config/initializers/devise.rb' + - 'lib/paperclip/color_extractor.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: always, always_true, never +Style/FrozenStringLiteralComment: + Exclude: + - 'app/views/accounts/show.rss.ruby' + - 'app/views/tags/show.rss.ruby' + - 'app/views/well_known/host_meta/show.xml.ruby' + - 'config/application.rb' + - 'config/boot.rb' + - 'config/environment.rb' + - 'config/environments/development.rb' + - 'config/environments/production.rb' + - 'config/environments/test.rb' + - 'config/initializers/0_post_deployment_migrations.rb' + - 'config/initializers/active_model_serializers.rb' + - 'config/initializers/application_controller_renderer.rb' + - 'config/initializers/assets.rb' + - 'config/initializers/backtrace_silencers.rb' + - 'config/initializers/cache_logging.rb' + - 'config/initializers/chewy.rb' + - 'config/initializers/content_security_policy.rb' + - 'config/initializers/cookies_serializer.rb' + - 'config/initializers/cors.rb' + - 'config/initializers/devise.rb' + - 'config/initializers/doorkeeper.rb' + - 'config/initializers/fast_blank.rb' + - 'config/initializers/ffmpeg.rb' + - 'config/initializers/filter_parameter_logging.rb' + - 'config/initializers/http_client_proxy.rb' + - 'config/initializers/httplog.rb' + - 'config/initializers/inflections.rb' + - 'config/initializers/mail_delivery_job.rb' + - 'config/initializers/makara.rb' + - 'config/initializers/mime_types.rb' + - 'config/initializers/oj.rb' + - 'config/initializers/omniauth.rb' + - 'config/initializers/open_uri_redirection.rb' + - 'config/initializers/permissions_policy.rb' + - 'config/initializers/pghero.rb' + - 'config/initializers/preload_link_headers.rb' + - 'config/initializers/premailer_rails.rb' + - 'config/initializers/rack_attack_logging.rb' + - 'config/initializers/redis.rb' + - 'config/initializers/session_store.rb' + - 'config/initializers/simple_form.rb' + - 'config/initializers/stoplight.rb' + - 'config/initializers/trusted_proxies.rb' + - 'config/initializers/twitter_regex.rb' + - 'config/initializers/webauthn.rb' + - 'config/initializers/wrap_parameters.rb' + - 'config/locales/sr-Latn.rb' + - 'config/locales/sr.rb' + - 'config/puma.rb' + - 'db/migrate/20160220174730_create_accounts.rb' + - 'db/migrate/20160220211917_create_statuses.rb' + - 'db/migrate/20160221003140_create_users.rb' + - 'db/migrate/20160221003621_create_follows.rb' + - 'db/migrate/20160222122600_create_stream_entries.rb' + - 'db/migrate/20160222143943_add_profile_fields_to_accounts.rb' + - 'db/migrate/20160223162837_add_metadata_to_statuses.rb' + - 'db/migrate/20160223164502_make_uris_nullable_in_statuses.rb' + - 'db/migrate/20160223165723_add_url_to_statuses.rb' + - 'db/migrate/20160223165855_add_url_to_accounts.rb' + - 'db/migrate/20160223171800_create_favourites.rb' + - 'db/migrate/20160224223247_create_mentions.rb' + - 'db/migrate/20160227230233_add_attachment_avatar_to_accounts.rb' + - 'db/migrate/20160305115639_add_devise_to_users.rb' + - 'db/migrate/20160306172223_create_doorkeeper_tables.rb' + - 'db/migrate/20160312193225_add_attachment_header_to_accounts.rb' + - 'db/migrate/20160314164231_add_owner_to_application.rb' + - 'db/migrate/20160316103650_add_missing_indices.rb' + - 'db/migrate/20160322193748_add_avatar_remote_url_to_accounts.rb' + - 'db/migrate/20160325130944_add_admin_to_users.rb' + - 'db/migrate/20160826155805_add_superapp_to_oauth_applications.rb' + - 'db/migrate/20160905150353_create_media_attachments.rb' + - 'db/migrate/20160919221059_add_subscription_expires_at_to_accounts.rb' + - 'db/migrate/20160920003904_remove_verify_token_from_accounts.rb' + - 'db/migrate/20160926213048_remove_owner_from_application.rb' + - 'db/migrate/20161003142332_add_confirmable_to_users.rb' + - 'db/migrate/20161003145426_create_blocks.rb' + - 'db/migrate/20161006213403_rails_settings_migration.rb' + - 'db/migrate/20161009120834_create_domain_blocks.rb' + - 'db/migrate/20161027172456_add_silenced_to_accounts.rb' + - 'db/migrate/20161104173623_create_tags.rb' + - 'db/migrate/20161105130633_create_statuses_tags_join_table.rb' + - 'db/migrate/20161116162355_add_locale_to_users.rb' + - 'db/migrate/20161119211120_create_notifications.rb' + - 'db/migrate/20161122163057_remove_unneeded_indexes.rb' + - 'db/migrate/20161123093447_add_sensitive_to_statuses.rb' + - 'db/migrate/20161128103007_create_subscriptions.rb' + - 'db/migrate/20161130142058_add_last_successful_delivery_at_to_subscriptions.rb' + - 'db/migrate/20161130185319_add_visibility_to_statuses.rb' + - 'db/migrate/20161202132159_add_in_reply_to_account_id_to_statuses.rb' + - 'db/migrate/20161203164520_add_from_account_id_to_notifications.rb' + - 'db/migrate/20161205214545_add_suspended_to_accounts.rb' + - 'db/migrate/20161221152630_add_hidden_to_stream_entries.rb' + - 'db/migrate/20161222201034_add_locked_to_accounts.rb' + - 'db/migrate/20161222204147_create_follow_requests.rb' + - 'db/migrate/20170105224407_add_shortcode_to_media_attachments.rb' + - 'db/migrate/20170109120109_create_web_settings.rb' + - 'db/migrate/20170112154826_migrate_settings.rb' + - 'db/migrate/20170114194937_add_application_to_statuses.rb' + - 'db/migrate/20170114203041_add_website_to_oauth_application.rb' + - 'db/migrate/20170119214911_create_preview_cards.rb' + - 'db/migrate/20170123162658_add_severity_to_domain_blocks.rb' + - 'db/migrate/20170123203248_add_reject_media_to_domain_blocks.rb' + - 'db/migrate/20170125145934_add_spoiler_text_to_statuses.rb' + - 'db/migrate/20170127165745_add_devise_two_factor_to_users.rb' + - 'db/migrate/20170205175257_remove_devices.rb' + - 'db/migrate/20170209184350_add_reply_to_statuses.rb' + - 'db/migrate/20170214110202_create_reports.rb' + - 'db/migrate/20170217012631_add_reblog_of_id_foreign_key_to_statuses.rb' + - 'db/migrate/20170301222600_create_mutes.rb' + - 'db/migrate/20170303212857_add_last_emailed_at_to_users.rb' + - 'db/migrate/20170304202101_add_type_to_media_attachments.rb' + - 'db/migrate/20170317193015_add_search_index_to_accounts.rb' + - 'db/migrate/20170318214217_add_header_remote_url_to_accounts.rb' + - 'db/migrate/20170322021028_add_lowercase_index_to_accounts.rb' + - 'db/migrate/20170322143850_change_primary_key_to_bigint_on_statuses.rb' + - 'db/migrate/20170322162804_add_search_index_to_tags.rb' + - 'db/migrate/20170330021336_add_counter_caches.rb' + - 'db/migrate/20170330163835_create_imports.rb' + - 'db/migrate/20170330164118_add_attachment_data_to_imports.rb' + - 'db/migrate/20170403172249_add_action_taken_by_account_id_to_reports.rb' + - 'db/migrate/20170405112956_add_index_on_mentions_status_id.rb' + - 'db/migrate/20170406215816_add_notifications_and_favourites_indices.rb' + - 'db/migrate/20170409170753_add_last_webfingered_at_to_accounts.rb' + - 'db/migrate/20170414080609_add_devise_two_factor_backupable_to_users.rb' + - 'db/migrate/20170414132105_add_language_to_statuses.rb' + - 'db/migrate/20170418160728_add_indexes_to_reports_for_accounts.rb' + - 'db/migrate/20170423005413_add_allowed_languages_to_user.rb' + - 'db/migrate/20170424003227_create_account_domain_blocks.rb' + - 'db/migrate/20170424112722_add_status_id_index_to_statuses_tags.rb' + - 'db/migrate/20170425131920_add_media_attachment_meta.rb' + - 'db/migrate/20170425202925_add_oembed_to_preview_cards.rb' + - 'db/migrate/20170427011934_re_add_owner_to_application.rb' + - 'db/migrate/20170506235850_create_conversations.rb' + - 'db/migrate/20170507000211_add_conversation_id_to_statuses.rb' + - 'db/migrate/20170507141759_optimize_index_subscriptions.rb' + - 'db/migrate/20170508230434_create_conversation_mutes.rb' + - 'db/migrate/20170516072309_add_index_accounts_on_uri.rb' + - 'db/migrate/20170520145338_change_language_filter_to_opt_out.rb' + - 'db/migrate/20170601210557_add_index_on_media_attachments_account_id.rb' + - 'db/migrate/20170604144747_add_foreign_keys_for_accounts.rb' + - 'db/migrate/20170606113804_change_tag_search_index_to_btree.rb' + - 'db/migrate/20170609145826_remove_default_language_from_statuses.rb' + - 'db/migrate/20170610000000_add_statuses_index_on_account_id_id.rb' + - 'db/migrate/20170623152212_create_session_activations.rb' + - 'db/migrate/20170624134742_add_description_to_session_activations.rb' + - 'db/migrate/20170625140443_add_access_token_id_to_session_activations.rb' + - 'db/migrate/20170711225116_fix_null_booleans.rb' + - 'db/migrate/20170713112503_make_tag_search_case_insensitive.rb' + - 'db/migrate/20170713175513_create_web_push_subscriptions.rb' + - 'db/migrate/20170713190709_add_web_push_subscription_to_session_activations.rb' + - 'db/migrate/20170714184731_add_domain_to_subscriptions.rb' + - 'db/migrate/20170716191202_add_hide_notifications_to_mute.rb' + - 'db/migrate/20170718211102_add_activitypub_to_accounts.rb' + - 'db/migrate/20170720000000_add_index_favourites_on_account_id_and_id.rb' + - 'db/migrate/20170823162448_create_status_pins.rb' + - 'db/migrate/20170824103029_add_timestamps_to_status_pins.rb' + - 'db/migrate/20170829215220_remove_status_pins_account_index.rb' + - 'db/migrate/20170901141119_truncate_preview_cards.rb' + - 'db/migrate/20170901142658_create_join_table_preview_cards_statuses.rb' + - 'db/migrate/20170905044538_add_index_id_account_id_activity_type_on_notifications.rb' + - 'db/migrate/20170905165803_add_local_to_statuses.rb' + - 'db/migrate/20170913000752_create_site_uploads.rb' + - 'db/migrate/20170917153509_create_custom_emojis.rb' + - 'db/migrate/20170918125918_ids_to_bigints.rb' + - 'db/migrate/20170920024819_status_ids_to_timestamp_ids.rb' + - 'db/migrate/20170920032311_fix_reblogs_in_feeds.rb' + - 'db/migrate/20170924022025_ids_to_bigints2.rb' + - 'db/migrate/20170927215609_add_description_to_media_attachments.rb' + - 'db/migrate/20170928082043_create_email_domain_blocks.rb' + - 'db/migrate/20171005102658_create_account_moderation_notes.rb' + - 'db/migrate/20171005171936_add_disabled_to_custom_emojis.rb' + - 'db/migrate/20171006142024_add_uri_to_custom_emojis.rb' + - 'db/migrate/20171010023049_add_foreign_key_to_account_moderation_notes.rb' + - 'db/migrate/20171010025614_change_accounts_nonnullable_in_account_moderation_notes.rb' + - 'db/migrate/20171020084748_add_visible_in_picker_to_custom_emoji.rb' + - 'db/migrate/20171028221157_add_reblogs_to_follows.rb' + - 'db/migrate/20171107143332_add_memorial_to_accounts.rb' + - 'db/migrate/20171107143624_add_disabled_to_users.rb' + - 'db/migrate/20171109012327_add_moderator_to_accounts.rb' + - 'db/migrate/20171114080328_add_index_domain_to_email_domain_blocks.rb' + - 'db/migrate/20171114231651_create_lists.rb' + - 'db/migrate/20171116161857_create_list_accounts.rb' + - 'db/migrate/20171118012443_add_moved_to_account_id_to_accounts.rb' + - 'db/migrate/20171119172437_create_admin_action_logs.rb' + - 'db/migrate/20171122120436_add_index_account_and_reblog_of_id_to_statuses.rb' + - 'db/migrate/20171125024930_create_invites.rb' + - 'db/migrate/20171125031751_add_invite_id_to_users.rb' + - 'db/migrate/20171125185353_add_index_reblog_of_id_and_account_to_statuses.rb' + - 'db/migrate/20171125190735_remove_old_reblog_index_on_statuses.rb' + - 'db/migrate/20171129172043_add_index_on_stream_entries.rb' + - 'db/migrate/20171130000000_add_embed_url_to_preview_cards.rb' + - 'db/migrate/20171201000000_change_account_id_nonnullable_in_lists.rb' + - 'db/migrate/20171212195226_remove_duplicate_indexes_in_lists.rb' + - 'db/migrate/20171226094803_more_faster_index_on_notifications.rb' + - 'db/migrate/20180106000232_add_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb' + - 'db/migrate/20180109143959_add_remember_token_to_users.rb' + - 'db/migrate/20180204034416_create_identities.rb' + - 'db/migrate/20180206000000_change_user_id_nonnullable.rb' + - 'db/migrate/20180211015820_create_backups.rb' + - 'db/migrate/20180304013859_add_featured_collection_url_to_accounts.rb' + - 'db/migrate/20180310000000_change_columns_in_notifications_nonnullable.rb' + - 'db/migrate/20180402031200_add_assigned_account_id_to_reports.rb' + - 'db/migrate/20180402040909_create_report_notes.rb' + - 'db/migrate/20180410204633_add_fields_to_accounts.rb' + - 'db/migrate/20180416210259_add_uri_to_relationships.rb' + - 'db/migrate/20180506221944_add_actor_type_to_accounts.rb' + - 'db/migrate/20180510214435_add_access_token_id_to_web_push_subscriptions.rb' + - 'db/migrate/20180510230049_migrate_web_push_subscriptions.rb' + - 'db/migrate/20180528141303_fix_accounts_unique_index.rb' + - 'db/migrate/20180608213548_reject_following_blocked_users.rb' + - 'db/migrate/20180609104432_migrate_web_push_subscriptions2.rb' + - 'db/migrate/20180615122121_add_autofollow_to_invites.rb' + - 'db/migrate/20180616192031_add_chosen_languages_to_users.rb' + - 'db/migrate/20180617162849_remove_unused_indexes.rb' + - 'db/migrate/20180628181026_create_custom_filters.rb' + - 'db/migrate/20180707154237_add_whole_word_to_custom_filter.rb' + - 'db/migrate/20180711152640_create_relays.rb' + - 'db/migrate/20180808175627_create_account_pins.rb' + - 'db/migrate/20180812123222_change_relays_enabled.rb' + - 'db/migrate/20180812162710_create_status_stats.rb' + - 'db/migrate/20180812173710_copy_status_stats.rb' + - 'db/migrate/20180814171349_add_confidential_to_doorkeeper_application.rb' + - 'db/migrate/20180831171112_create_bookmarks.rb' + - 'db/migrate/20180929222014_create_account_conversations.rb' + - 'db/migrate/20181007025445_create_pghero_space_stats.rb' + - 'db/migrate/20181010141500_add_silent_to_mentions.rb' + - 'db/migrate/20181017170937_add_reject_reports_to_domain_blocks.rb' + - 'db/migrate/20181018205649_add_unread_to_account_conversations.rb' + - 'db/migrate/20181024224956_migrate_account_conversations.rb' + - 'db/migrate/20181026034033_remove_faux_remote_account_duplicates.rb' + - 'db/migrate/20181116165755_create_account_stats.rb' + - 'db/migrate/20181116173541_copy_account_stats.rb' + - 'db/migrate/20181127130500_identity_id_to_bigint.rb' + - 'db/migrate/20181127165847_add_show_replies_to_lists.rb' + - 'db/migrate/20181203003808_create_accounts_tags_join_table.rb' + - 'db/migrate/20181203021853_add_discoverable_to_accounts.rb' + - 'db/migrate/20181204193439_add_last_status_at_to_account_stats.rb' + - 'db/migrate/20181204215309_create_account_tag_stats.rb' + - 'db/migrate/20181207011115_downcase_custom_emoji_domains.rb' + - 'db/migrate/20181213184704_create_account_warnings.rb' + - 'db/migrate/20181213185533_create_account_warning_presets.rb' + - 'db/migrate/20181219235220_add_created_by_application_id_to_users.rb' + - 'db/migrate/20181226021420_add_also_known_as_to_accounts.rb' + - 'db/migrate/20190103124649_create_scheduled_statuses.rb' + - 'db/migrate/20190103124754_add_scheduled_status_id_to_media_attachments.rb' + - 'db/migrate/20190117114553_create_tombstones.rb' + - 'db/migrate/20190201012802_add_overwrite_to_imports.rb' + - 'db/migrate/20190203180359_create_featured_tags.rb' + - 'db/migrate/20190225031541_create_polls.rb' + - 'db/migrate/20190225031625_create_poll_votes.rb' + - 'db/migrate/20190226003449_add_poll_id_to_statuses.rb' + - 'db/migrate/20190304152020_add_uri_to_poll_votes.rb' + - 'db/migrate/20190306145741_add_lock_version_to_polls.rb' + - 'db/migrate/20190307234537_add_approved_to_users.rb' + - 'db/migrate/20190314181829_migrate_open_registrations_setting.rb' + - 'db/migrate/20190316190352_create_account_identity_proofs.rb' + - 'db/migrate/20190317135723_add_uri_to_reports.rb' + - 'db/migrate/20190403141604_add_comment_to_invites.rb' + - 'db/migrate/20190409054914_create_user_invite_requests.rb' + - 'db/migrate/20190420025523_add_blurhash_to_media_attachments.rb' + - 'db/migrate/20190509164208_add_by_moderator_to_tombstone.rb' + - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb' + - 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb' + - 'db/migrate/20190627222225_create_custom_emoji_categories.rb' + - 'db/migrate/20190627222826_add_category_id_to_custom_emojis.rb' + - 'db/migrate/20190701022101_add_trust_level_to_accounts.rb' + - 'db/migrate/20190705002136_create_domain_allows.rb' + - 'db/migrate/20190715164535_add_instance_actor.rb' + - 'db/migrate/20190726175042_add_case_insensitive_index_to_tags.rb' + - 'db/migrate/20190729185330_add_score_to_tags.rb' + - 'db/migrate/20190805123746_add_capabilities_to_tags.rb' + - 'db/migrate/20190807135426_add_comments_to_domain_blocks.rb' + - 'db/migrate/20190815225426_add_last_status_at_to_tags.rb' + - 'db/migrate/20190819134503_add_deleted_at_to_statuses.rb' + - 'db/migrate/20190820003045_update_statuses_index.rb' + - 'db/migrate/20190823221802_add_local_index_to_statuses.rb' + - 'db/migrate/20190901035623_add_max_score_to_tags.rb' + - 'db/migrate/20190904222339_create_markers.rb' + - 'db/migrate/20190914202517_create_account_migrations.rb' + - 'db/migrate/20190915194355_create_account_aliases.rb' + - 'db/migrate/20190927232842_add_voters_count_to_polls.rb' + - 'db/migrate/20191001213028_add_lock_version_to_account_stats.rb' + - 'db/migrate/20191007013357_update_pt_locales.rb' + - 'db/migrate/20191031163205_change_list_account_follow_nullable.rb' + - 'db/migrate/20191212003415_increase_backup_size.rb' + - 'db/migrate/20191212163405_add_hide_collections_to_accounts.rb' + - 'db/migrate/20191218153258_create_announcements.rb' + - 'db/migrate/20200113125135_create_announcement_mutes.rb' + - 'db/migrate/20200114113335_create_announcement_reactions.rb' + - 'db/migrate/20200119112504_add_public_index_to_statuses.rb' + - 'db/migrate/20200126203551_add_published_at_to_announcements.rb' + - 'db/migrate/20200306035625_add_processing_to_media_attachments.rb' + - 'db/migrate/20200309150742_add_forwarded_to_reports.rb' + - 'db/migrate/20200312144258_add_title_to_account_warning_presets.rb' + - 'db/migrate/20200312162302_add_status_ids_to_announcements.rb' + - 'db/migrate/20200312185443_add_parent_id_to_email_domain_blocks.rb' + - 'db/migrate/20200317021758_add_expires_at_to_mutes.rb' + - 'db/migrate/20200407201300_create_unavailable_domains.rb' + - 'db/migrate/20200407202420_migrate_unavailable_inboxes.rb' + - 'db/migrate/20200417125749_add_storage_schema_version.rb' + - 'db/migrate/20200508212852_reset_unique_jobs_locks.rb' + - 'db/migrate/20200510110808_reset_web_app_secret.rb' + - 'db/migrate/20200510181721_remove_duplicated_indexes_pghero.rb' + - 'db/migrate/20200516180352_create_devices.rb' + - 'db/migrate/20200516183822_create_one_time_keys.rb' + - 'db/migrate/20200518083523_create_encrypted_messages.rb' + - 'db/migrate/20200521180606_encrypted_message_ids_to_timestamp_ids.rb' + - 'db/migrate/20200529214050_add_devices_url_to_accounts.rb' + - 'db/migrate/20200601222558_create_system_keys.rb' + - 'db/migrate/20200605155027_add_blurhash_to_preview_cards.rb' + - 'db/migrate/20200608113046_add_sign_in_token_to_users.rb' + - 'db/migrate/20200614002136_add_sensitized_to_accounts.rb' + - 'db/migrate/20200620164023_add_fixed_lowercase_index_to_accounts.rb' + - 'db/migrate/20200622213645_media_attachment_ids_to_timestamp_ids.rb' + - 'db/migrate/20200627125810_add_thumbnail_columns_to_media_attachments.rb' + - 'db/migrate/20200628133322_create_account_notes.rb' + - 'db/migrate/20200630190240_create_webauthn_credentials.rb' + - 'db/migrate/20200630190544_add_webauthn_id_to_users.rb' + - 'db/migrate/20200908193330_create_account_deletion_requests.rb' + - 'db/migrate/20200917192924_add_notify_to_follows.rb' + - 'db/migrate/20200917193034_add_type_to_notifications.rb' + - 'db/migrate/20200917222316_add_index_notifications_on_type.rb' + - 'db/migrate/20201008202037_create_ip_blocks.rb' + - 'db/migrate/20201008220312_add_sign_up_ip_to_users.rb' + - 'db/migrate/20201017233919_add_suspension_origin_to_accounts.rb' + - 'db/migrate/20201206004238_create_instances.rb' + - 'db/migrate/20201218054746_add_obfuscate_to_domain_blocks.rb' + - 'db/migrate/20210221045109_create_rules.rb' + - 'db/migrate/20210306164523_account_ids_to_timestamp_ids.rb' + - 'db/migrate/20210322164601_create_account_summaries.rb' + - 'db/migrate/20210323114347_create_follow_recommendations.rb' + - 'db/migrate/20210324171613_create_follow_recommendation_suppressions.rb' + - 'db/migrate/20210416200740_create_canonical_email_blocks.rb' + - 'db/migrate/20210421121431_add_case_insensitive_btree_index_to_tags.rb' + - 'db/migrate/20210425135952_add_index_on_media_attachments_account_id_status_id.rb' + - 'db/migrate/20210505174616_update_follow_recommendations_to_version_2.rb' + - 'db/migrate/20210609202149_create_login_activities.rb' + - 'db/migrate/20210616214526_create_user_ips.rb' + - 'db/migrate/20210621221010_add_skip_sign_in_token_to_users.rb' + - 'db/migrate/20210630000137_fix_canonical_email_blocks_foreign_key.rb' + - 'db/migrate/20210722120340_create_account_statuses_cleanup_policies.rb' + - 'db/migrate/20210904215403_add_edited_at_to_statuses.rb' + - 'db/migrate/20210908220918_create_status_edits.rb' + - 'db/migrate/20211031031021_create_preview_card_providers.rb' + - 'db/migrate/20211112011713_add_language_to_preview_cards.rb' + - 'db/migrate/20211115032527_add_trendable_to_preview_cards.rb' + - 'db/migrate/20211123212714_add_link_type_to_preview_cards.rb' + - 'db/migrate/20211213040746_update_account_summaries_to_version_2.rb' + - 'db/migrate/20211231080958_add_category_to_reports.rb' + - 'db/migrate/20220105163928_remove_mentions_status_id_index.rb' + - 'db/migrate/20220115125126_add_report_id_to_account_warnings.rb' + - 'db/migrate/20220115125341_fix_account_warning_actions.rb' + - 'db/migrate/20220116202951_add_deleted_at_index_on_statuses.rb' + - 'db/migrate/20220124141035_create_appeals.rb' + - 'db/migrate/20220202200743_add_trendable_to_accounts.rb' + - 'db/migrate/20220202200926_add_trendable_to_statuses.rb' + - 'db/migrate/20220210153119_add_overruled_at_to_account_warnings.rb' + - 'db/migrate/20220224010024_add_ips_to_email_domain_blocks.rb' + - 'db/migrate/20220227041951_add_last_used_at_to_oauth_access_tokens.rb' + - 'db/migrate/20220302232632_add_ordered_media_attachment_ids_to_statuses.rb' + - 'db/migrate/20220303000827_add_ordered_media_attachment_ids_to_status_edits.rb' + - 'db/migrate/20220304195405_migrate_hide_network_preference.rb' + - 'db/migrate/20220307094650_fix_featured_tags_constraints.rb' + - 'db/migrate/20220309213005_fix_reblog_deleted_at.rb' + - 'db/migrate/20220316233212_update_kurdish_locales.rb' + - 'db/migrate/20220428112511_add_index_statuses_on_account_id.rb' + - 'db/migrate/20220428112727_add_index_statuses_pins_on_status_id.rb' + - 'db/migrate/20220428114454_add_index_reports_on_assigned_account_id.rb' + - 'db/migrate/20220428114902_add_index_reports_on_action_taken_by_account_id.rb' + - 'db/migrate/20220606044941_create_webhooks.rb' + - 'db/migrate/20220611210335_create_user_roles.rb' + - 'db/migrate/20220611212541_add_role_id_to_users.rb' + - 'db/migrate/20220710102457_add_display_name_to_tags.rb' + - 'db/migrate/20220714171049_create_tag_follows.rb' + - 'db/migrate/20220824164433_add_human_identifier_to_admin_action_logs.rb' + - 'db/migrate/20220824233535_create_status_trends.rb' + - 'db/migrate/20220827195229_change_canonical_email_blocks_nullable.rb' + - 'db/migrate/20220829192633_add_languages_to_follows.rb' + - 'db/migrate/20220829192658_add_languages_to_follow_requests.rb' + - 'db/migrate/20221006061337_create_preview_card_trends.rb' + - 'db/migrate/20221012181003_add_blurhash_to_site_uploads.rb' + - 'db/migrate/20221021055441_add_index_featured_tags_on_account_id_and_tag_id.rb' + - 'db/migrate/20221025171544_add_index_ip_blocks_on_ip.rb' + - 'db/migrate/20221104133904_add_name_to_featured_tags.rb' + - 'db/post_migrate/20190519130537_remove_boosts_widening_audience.rb' + - 'db/post_migrate/20210308133107_remove_subscription_expires_at_from_accounts.rb' + - 'db/post_migrate/20220118183123_remove_rememberable_from_users.rb' + - 'db/seeds/01_web_app.rb' + - 'db/seeds/02_instance_actor.rb' + - 'db/seeds/03_roles.rb' + - 'db/seeds/04_admin.rb' + - 'lib/rails/engine_extensions.rb' + - 'lib/tasks/branding.rake' + - 'spec/fabricators_spec.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/GlobalStdStream: + Exclude: + - 'config/boot.rb' + - 'config/environments/development.rb' + - 'config/environments/production.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. +Style/GuardClause: + Exclude: + - 'app/controllers/admin/confirmations_controller.rb' + - 'app/controllers/auth/confirmations_controller.rb' + - 'app/controllers/auth/passwords_controller.rb' + - 'app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb' + - 'app/lib/activitypub/activity/block.rb' + - 'app/lib/request.rb' + - 'app/lib/request_pool.rb' + - 'app/lib/webfinger.rb' + - 'app/lib/webfinger_resource.rb' + - 'app/models/concerns/account_counters.rb' + - 'app/models/concerns/ldap_authenticable.rb' + - 'app/models/tag.rb' + - 'app/models/user.rb' + - 'app/services/fan_out_on_write_service.rb' + - 'app/services/post_status_service.rb' + - 'app/services/process_hashtags_service.rb' + - 'app/workers/move_worker.rb' + - 'app/workers/redownload_avatar_worker.rb' + - 'app/workers/redownload_header_worker.rb' + - 'app/workers/redownload_media_worker.rb' + - 'app/workers/remote_account_refresh_worker.rb' + - '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/mastodon/cli/accounts.rb' + - 'lib/mastodon/cli/maintenance.rb' + - 'lib/mastodon/cli/media.rb' + - 'lib/paperclip/attachment_extensions.rb' + - 'lib/tasks/repo.rake' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: braces, no_braces +Style/HashAsLastArrayItem: + Exclude: + - 'app/controllers/admin/statuses_controller.rb' + - 'app/controllers/api/v1/statuses_controller.rb' + - 'app/models/concerns/account_counters.rb' + - 'app/models/concerns/status_threading_concern.rb' + - 'app/models/status.rb' + - 'app/services/batched_remove_status_service.rb' + - 'app/services/notify_service.rb' + - 'db/migrate/20181024224956_migrate_account_conversations.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/HashTransformValues: + Exclude: + - 'app/serializers/rest/web_push_subscription_serializer.rb' + - 'app/services/import_service.rb' + +# This cop supports safe autocorrection (--autocorrect). +Style/IfUnlessModifier: + Exclude: + - 'config/environments/production.rb' + - 'config/initializers/devise.rb' + - 'config/initializers/ffmpeg.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: InverseMethods, InverseBlocks. +Style/InverseMethods: + Exclude: + - 'app/models/custom_filter.rb' + - 'app/services/update_account_service.rb' + - 'spec/controllers/activitypub/replies_controller_spec.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: line_count_dependent, lambda, literal +Style/Lambda: + Exclude: + - 'config/initializers/simple_form.rb' + - 'config/routes.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/MapToHash: + Exclude: + - 'app/models/status.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: literals, strict +Style/MutableConstant: + Exclude: + - 'app/models/tag.rb' + - 'app/services/delete_account_service.rb' + - 'config/initializers/twitter_regex.rb' + - 'lib/mastodon/migration_warning.rb' + +# This cop supports safe autocorrection (--autocorrect). +Style/NilLambda: + Exclude: + - 'config/initializers/paperclip.rb' + +# Configuration parameters: AllowedMethods. +# AllowedMethods: respond_to_missing? +Style/OptionalBooleanParameter: + Exclude: + - 'app/helpers/admin/account_moderation_notes_helper.rb' + - 'app/helpers/jsonld_helper.rb' + - 'app/lib/admin/system_check/message.rb' + - 'app/lib/request.rb' + - 'app/lib/webfinger.rb' + - 'app/services/block_domain_service.rb' + - 'app/services/fetch_resource_service.rb' + - 'app/workers/domain_block_worker.rb' + - 'app/workers/unfollow_follow_worker.rb' + - 'lib/mastodon/redis_config.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: PreferredDelimiters. +Style/PercentLiteralDelimiters: + Exclude: + - 'config/deploy.rb' + - 'config/initializers/doorkeeper.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: short, verbose +Style/PreferredHashMethods: + Exclude: + - 'config/initializers/paperclip.rb' + +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantConstantBase: + Exclude: + - 'config/environments/production.rb' + - 'config/initializers/sidekiq.rb' + - 'config/locales/sr-Latn.rb' + - 'config/locales/sr.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: SafeForConstants. +Style/RedundantFetchBlock: + Exclude: + - 'config/initializers/1_hosts.rb' + - 'config/initializers/chewy.rb' + - 'config/initializers/devise.rb' + - 'config/initializers/paperclip.rb' + - 'config/puma.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. +# AllowedMethods: present?, blank?, presence, try, try! +Style/SafeNavigation: + Exclude: + - 'app/models/concerns/account_finder_concern.rb' + - 'app/models/status.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowAsExpressionSeparator. +Style/Semicolon: + Exclude: + - 'spec/services/activitypub/process_status_update_service_spec.rb' + - 'spec/validators/blacklisted_email_validator_spec.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: only_raise, only_fail, semantic +Style/SignalException: + Exclude: + - 'lib/devise/two_factor_ldap_authenticatable.rb' + - 'lib/devise/two_factor_pam_authenticatable.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/SingleArgumentDig: + Exclude: + - 'lib/webpacker/manifest_extensions.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/SlicingWithRange: + Exclude: + - 'app/lib/emoji_formatter.rb' + - 'app/lib/text_formatter.rb' + - 'app/models/account_alias.rb' + - 'app/models/domain_block.rb' + - 'app/models/email_domain_block.rb' + - 'app/models/preview_card_provider.rb' + - 'app/validators/status_length_validator.rb' + - 'db/migrate/20190726175042_add_case_insensitive_index_to_tags.rb' + - 'lib/active_record/batches.rb' + - 'lib/mastodon/premailer_webpack_strategy.rb' + - 'lib/tasks/repo.rake' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: require_parentheses, require_no_parentheses +Style/StabbyLambdaParentheses: + Exclude: + - 'config/environments/production.rb' + - 'config/initializers/content_security_policy.rb' + +# This cop supports safe autocorrection (--autocorrect). +Style/StderrPuts: + Exclude: + - 'config/boot.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: Mode. +Style/StringConcatenation: + Exclude: + - 'config/initializers/paperclip.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. +# SupportedStyles: single_quotes, double_quotes +Style/StringLiterals: + Exclude: + - 'config/environments/production.rb' + - 'config/initializers/backtrace_silencers.rb' + - 'config/initializers/http_client_proxy.rb' + - 'config/initializers/rack_attack.rb' + - 'config/initializers/webauthn.rb' + - 'config/routes.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, AllowComments. +# AllowedMethods: define_method, mail, respond_to +Style/SymbolProc: + Exclude: + - 'config/initializers/omniauth.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, AllowSafeAssignment. +# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex +Style/TernaryParentheses: + Exclude: + - 'config/environments/development.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyleForMultiline. +# SupportedStylesForMultiline: comma, consistent_comma, no_comma +Style/TrailingCommaInArguments: + Exclude: + - 'config/initializers/paperclip.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyleForMultiline. +# SupportedStylesForMultiline: comma, consistent_comma, no_comma +Style/TrailingCommaInHashLiteral: + Exclude: + - 'config/environments/production.rb' + - 'config/environments/test.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, MinSize, WordRegex. +# SupportedStyles: percent, brackets +Style/WordArray: + Exclude: + - 'app/helpers/languages_helper.rb' + - 'config/initializers/cors.rb' + - 'spec/controllers/settings/imports_controller_spec.rb' + - 'spec/models/form/import_spec.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. +# URISchemes: http, https +Layout/LineLength: + Max: 701 diff --git a/.ruby-version b/.ruby-version index eca690e737..be94e6f53d 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.0.5 +3.2.2 diff --git a/.yarnclean b/.yarnclean index 0cc2b50d7b..21eb734a6c 100644 --- a/.yarnclean +++ b/.yarnclean @@ -44,3 +44,6 @@ Gruntfile.js # for specific ignore !.svgo.yml !sass-lint/**/*.yml + +# breaks lint-staged or generally anything using https://github.com/eemeli/yaml/issues/384 +!**/yaml/dist/**/doc diff --git a/Aptfile b/Aptfile index 8f5bb72a25..5e033f1365 100644 --- a/Aptfile +++ b/Aptfile @@ -1,4 +1,5 @@ ffmpeg +libopenblas0-pthread libpq-dev libxdamage1 libxfixes3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a6998e8c1..425c098505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,120 @@ -Changelog -========= +# Changelog All notable changes to this project will be documented in this file. +## [4.1.3] - 2023-07-06 + +### Added + +- Add fallback redirection when getting a webfinger query `LOCAL_DOMAIN@LOCAL_DOMAIN` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23600)) + +### Changed + +- Change OpenGraph-based embeds to allow fullscreen ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25058)) +- Change AccessTokensVacuum to also delete expired tokens ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24868)) +- Change profile updates to be sent to recently-mentioned servers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24852)) +- Change automatic post deletion thresholds and load detection ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24614)) +- Change `/api/v1/statuses/:id/history` to always return at least one item ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25510)) +- Change auto-linking to allow carets in URL query params ([renchap](https://github.com/mastodon/mastodon/pull/25216)) + +### Removed + +- Remove invalid `X-Frame-Options: ALLOWALL` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25070)) + +### Fixed + +- Fix wrong view being displayed when a webhook fails validation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25464)) +- Fix soft-deleted post cleanup scheduler overwhelming the streaming server ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25519)) +- Fix incorrect pagination headers in `/api/v2/admin/accounts` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25477)) +- Fix multiple inefficiencies in automatic post cleanup worker ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24607), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24785), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24840)) +- Fix performance of streaming by parsing message JSON once ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25278), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25361)) +- Fix CSP headers when `S3_ALIAS_HOST` includes a path component ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25273)) +- Fix `tootctl accounts approve --number N` not aproving N earliest registrations ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24605)) +- Fix reports not being closed when performing batch suspensions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24988)) +- Fix being able to vote on your own polls ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25015)) +- Fix race condition when reblogging a status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25016)) +- Fix “Authorized applications” inefficiently and incorrectly getting last use date ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25060)) +- Fix “Authorized applications” crashing when listing apps with certain admin API scopes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25713)) +- Fix multiple N+1s in ConversationsController ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25134), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25399), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25499)) +- Fix user archive takeouts when using OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24431)) +- Fix searching for remote content by URL not working under certain conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25637)) +- Fix inefficiencies in indexing content for search ([VyrCossont](https://github.com/mastodon/mastodon/pull/24285), [VyrCossont](https://github.com/mastodon/mastodon/pull/24342)) + +### Security + +- Add finer permission requirements for managing webhooks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25463)) +- Update dependencies +- Add hardening headers for user-uploaded files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25756)) +- Fix verified links possibly hiding important parts of the URL (CVE-2023-36462) +- Fix timeout handling of outbound HTTP requests (CVE-2023-36461) +- Fix arbitrary file creation through media processing (CVE-2023-36460) +- Fix possible XSS in preview cards (CVE-2023-36459) + +## [4.1.2] - 2023-04-04 + +### Fixed + +- Fix crash in `tootctl` commands making use of parallelization when Elasticsearch is enabled ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24182), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24377)) +- Fix crash in `db:setup` when Elasticsearch is enabled ([rrgeorge](https://github.com/mastodon/mastodon/pull/24302)) +- Fix user archive takeout when using OpenStack Swift or S3 providers with no ACL support ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24200)) +- Fix invalid/expired invites being processed on sign-up ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24337)) + +### Security + +- Update Ruby to 3.0.6 due to ReDoS vulnerabilities ([saizai](https://github.com/mastodon/mastodon/pull/24334)) +- Fix unescaped user input in LDAP query ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24379)) + +## [4.1.1] - 2023-03-16 + +### Added + +- Add redirection from paths with url-encoded `@` to their decoded form ([thijskh](https://github.com/mastodon/mastodon/pull/23593)) +- Add `lang` attribute to native language names in language picker in Web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23749)) +- Add headers to outgoing mails to avoid auto-replies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23597)) +- Add support for refreshing many accounts at once with `tootctl accounts refresh` ([9p4](https://github.com/mastodon/mastodon/pull/23304)) +- Add confirmation modal when clicking to edit a post with a non-empty compose form ([PauloVilarinho](https://github.com/mastodon/mastodon/pull/23936)) +- Add support for the HAproxy PROXY protocol through the `PROXY_PROTO_V1` environment variable ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24064)) +- Add `SENDFILE_HEADER` environment variable ([Gargron](https://github.com/mastodon/mastodon/pull/24123)) +- Add cache headers to static files served through Rails ([Gargron](https://github.com/mastodon/mastodon/pull/24120)) + +### Changed + +- Increase contrast of upload progress bar background ([toolmantim](https://github.com/mastodon/mastodon/pull/23836)) +- Change post auto-deletion throttling constants to better scale with server size ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23320)) +- Change order of bookmark and favourite sidebar entries in single-column UI for consistency ([TerryGarcia](https://github.com/mastodon/mastodon/pull/23701)) +- Change `ActivityPub::DeliveryWorker` retries to be spread out more ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21956)) + +### Fixed + +- Fix “Remove all followers from the selected domains” also removing follows and notifications ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23805)) +- Fix streaming metrics format ([emilweth](https://github.com/mastodon/mastodon/pull/23519), [emilweth](https://github.com/mastodon/mastodon/pull/23520)) +- Fix case-sensitive check for previously used hashtags in hashtag autocompletion ([deanveloper](https://github.com/mastodon/mastodon/pull/23526)) +- Fix focus point of already-attached media not saving after edit ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23566)) +- Fix sidebar behavior in settings/admin UI on mobile ([wxt2005](https://github.com/mastodon/mastodon/pull/23764)) +- Fix inefficiency when searching accounts per username in admin interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23801)) +- Fix duplicate “Publish” button on mobile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23804)) +- Fix server error when failing to follow back followers from `/relationships` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23787)) +- Fix server error when attempting to display the edit history of a trendable post in the admin interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23574)) +- Fix `tootctl accounts migrate` crashing because of a typo ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23567)) +- Fix original account being unfollowed on migration before the follow request to the new account could be sent ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21957)) +- Fix the “Back” button in column headers sometimes leaving Mastodon ([c960657](https://github.com/mastodon/mastodon/pull/23953)) +- Fix pgBouncer resetting application name on every transaction ([Gargron](https://github.com/mastodon/mastodon/pull/23958)) +- Fix unconfirmed accounts being counted as active users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23803)) +- Fix `/api/v1/streaming` sub-paths not being redirected ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23988)) +- Fix drag'n'drop upload area text that spans multiple lines not being centered ([vintprox](https://github.com/mastodon/mastodon/pull/24029)) +- Fix sidekiq jobs not triggering Elasticsearch index updates ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24046)) +- Fix tags being unnecessarily stripped from plain-text short site description ([c960657](https://github.com/mastodon/mastodon/pull/23975)) +- Fix HTML entities not being un-escaped in extracted plain-text from remote posts ([c960657](https://github.com/mastodon/mastodon/pull/24019)) +- Fix dashboard crash on ElasticSearch server error ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23751)) +- Fix incorrect post links in strikes when the account is remote ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23611)) +- Fix misleading error code when receiving invalid WebAuthn credentials ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23568)) +- Fix duplicate mails being sent when the SMTP server is too slow to close the connection ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23750)) + +### Security + +- Change user backups to use expiring URLs for download when possible ([Gargron](https://github.com/mastodon/mastodon/pull/24136)) +- Add warning for object storage misconfiguration ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24137)) + ## [4.1.0] - 2023-02-10 ### Added @@ -219,6 +331,7 @@ All notable changes to this project will be documented in this file. - Fix unbounded recursion in post discovery ([ClearlyClaire,nametoolong](https://github.com/mastodon/mastodon/pull/23506)) ## [4.0.2] - 2022-11-15 + ### Fixed - Fix wrong color on mentions hidden behind content warning in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/20724)) @@ -226,6 +339,7 @@ All notable changes to this project will be documented in this file. - Fix `unsafe-eval` being used when `wasm-unsafe-eval` is enough in Content Security Policy ([Gargron](https://github.com/mastodon/mastodon/pull/20729), [prplecake](https://github.com/mastodon/mastodon/pull/20606)) ## [4.0.1] - 2022-11-14 + ### Fixed - Fix nodes order being sometimes mangled when rewriting emoji ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20677)) @@ -429,6 +543,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix out-of-bound reads in blurhash transcoder ([delroth](https://github.com/mastodon/mastodon/pull/20388)) ## [3.5.3] - 2022-05-26 + ### Added - **Add language dropdown to compose form in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/18420), [ykzts](https://github.com/mastodon/mastodon/pull/18460)) @@ -476,6 +591,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix confirmation redirect to app without `Location` header ([Gargron](https://github.com/mastodon/mastodon/pull/18523)) ## [3.5.2] - 2022-05-04 + ### Added - Add warning on direct messages screen in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/18289)) @@ -528,6 +644,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix error in alias settings page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18004)) ## [3.5.1] - 2022-04-08 + ### Added - Add pagination for trending statuses in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/17976)) @@ -571,6 +688,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix error when indexing statuses into Elasticsearch ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17912)) ## [3.5.0] - 2022-03-30 + ### Added - **Add support for incoming edited posts** ([Gargron](https://github.com/mastodon/mastodon/pull/16697), [Gargron](https://github.com/mastodon/mastodon/pull/17727), [Gargron](https://github.com/mastodon/mastodon/pull/17728), [Gargron](https://github.com/mastodon/mastodon/pull/17320), [Gargron](https://github.com/mastodon/mastodon/pull/17404), [Gargron](https://github.com/mastodon/mastodon/pull/17390), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17335), [Gargron](https://github.com/mastodon/mastodon/pull/17696), [Gargron](https://github.com/mastodon/mastodon/pull/17745), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17740), [Gargron](https://github.com/mastodon/mastodon/pull/17697), [Gargron](https://github.com/mastodon/mastodon/pull/17648), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17531), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17499), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17498), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17380), [Gargron](https://github.com/mastodon/mastodon/pull/17373), [Gargron](https://github.com/mastodon/mastodon/pull/17334), [Gargron](https://github.com/mastodon/mastodon/pull/17333), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17699), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17748)) @@ -770,6 +888,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix being able to bypass e-mail restrictions ([Gargron](https://github.com/mastodon/mastodon/pull/17909)) ## [3.4.6] - 2022-02-03 + ### Fixed - Fix `mastodon:webpush:generate_vapid_key` task requiring a functional environment ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17338)) @@ -784,6 +903,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Disable legacy XSS filtering ([Wonderfall](https://github.com/mastodon/mastodon/pull/17289)) ## [3.4.5] - 2022-01-31 + ### Added - Add more advanced migration tests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17393)) @@ -798,6 +918,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix followers synchronization mechanism ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16510)) ## [3.4.4] - 2021-11-26 + ### Fixed - Fix error when suspending user with an already blocked canonical email ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17036)) @@ -815,11 +936,13 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix handling of recursive toots in WebUI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17041)) ## [3.4.3] - 2021-11-06 + ### Fixed - Fix login being broken due to inaccurately applied backport fix in 3.4.2 ([Gargron](https://github.com/mastodon/mastodon/commit/5c47a18c8df3231aa25c6d1f140a71a7fac9cbf9)) ## [3.4.2] - 2021-11-06 + ### Added - Add `configuration` attribute to `GET /api/v1/instance` ([Gargron](https://github.com/mastodon/mastodon/pull/16485)) @@ -863,6 +986,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix revoking a specific session not working ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16943)) ## [3.4.1] - 2021-06-03 + ### Added - Add new emoji assets from Twemoji 13.1.0 ([Gargron](https://github.com/mastodon/mastodon/pull/16345)) @@ -882,6 +1006,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix mailer jobs for deleted notifications erroring out ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16294)) ## [3.4.0] - 2021-05-16 + ### Added - **Add follow recommendations for onboarding** ([Gargron](https://github.com/mastodon/mastodon/pull/15945), [Gargron](https://github.com/mastodon/mastodon/pull/16161), [Gargron](https://github.com/mastodon/mastodon/pull/16060), [Gargron](https://github.com/mastodon/mastodon/pull/16077), [Gargron](https://github.com/mastodon/mastodon/pull/16078), [Gargron](https://github.com/mastodon/mastodon/pull/16160), [Gargron](https://github.com/mastodon/mastodon/pull/16079), [noellabo](https://github.com/mastodon/mastodon/pull/16044), [noellabo](https://github.com/mastodon/mastodon/pull/16045), [Gargron](https://github.com/mastodon/mastodon/pull/16152), [Gargron](https://github.com/mastodon/mastodon/pull/16153), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16082), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16173), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16159), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16189)) @@ -917,7 +1042,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - This method allows an app through which a user signed-up to request a new confirmation e-mail to be sent, or to change the e-mail of the account before it is confirmed - Add `GET /api/v1/accounts/lookup` to REST API ([Gargron](https://github.com/mastodon/mastodon/pull/15740), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15750)) - This method allows to quickly convert a username of a known account to an ID that can be used with the REST API, or to check if a username is available - for sign-up + for sign-up - Add `policy` param to `POST /api/v1/push/subscriptions` in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/16040)) - This param allows an app to control from whom notifications should be delivered as push notifications to the app - Add `details` to error response for `POST /api/v1/accounts` in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/15803)) @@ -1027,6 +1152,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix app name, website and redirect URIs not having a maximum length ([Gargron](https://github.com/mastodon/mastodon/pull/16042)) ## [3.3.0] - 2020-12-27 + ### Added - **Add hotkeys for audio/video control in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/15158), [Gargron](https://github.com/mastodon/mastodon/pull/15198)) @@ -1203,6 +1329,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix resolving accounts sometimes creating duplicate records for a given ActivityPub identifier ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15364)) ## [3.2.2] - 2020-12-19 + ### Added - Add `tootctl maintenance fix-duplicates` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14860), [Gargron](https://github.com/mastodon/mastodon/pull/15223)) @@ -1229,6 +1356,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix resolving accounts sometimes creating duplicate records for a given ActivityPub identifier ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15364)) ## [3.2.1] - 2020-10-19 + ### Added - Add support for latest HTTP Signatures spec draft ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14556)) @@ -1258,6 +1386,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix files served as `application/octet-stream` being rejected without attempting mime type detection ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14452)) ## [3.2.0] - 2020-07-27 + ### Added - Add `SMTP_SSL` environment variable ([OmmyZhang](https://github.com/mastodon/mastodon/pull/14309)) @@ -1393,7 +1522,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix unique username constraint for local users not being enforced in database ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14099)) - Fix unnecessary gap under video modal in web UI ([mfmfuyu](https://github.com/mastodon/mastodon/pull/14098)) - Fix 2FA and sign in token pages not respecting user locale ([mfmfuyu](https://github.com/mastodon/mastodon/pull/14087)) -- Fix unapproved users being able to view profiles when in limited-federation mode *and* requiring approval for sign-ups ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14093)) +- Fix unapproved users being able to view profiles when in limited-federation mode _and_ requiring approval for sign-ups ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14093)) - Fix initial audio volume not corresponding to what's displayed in audio player in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14057)) - Fix timelines sometimes jumping when closing modals in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14019)) - Fix memory usage of downloading remote files ([Gargron](https://github.com/mastodon/mastodon/pull/14184), [Gargron](https://github.com/mastodon/mastodon/pull/14181), [noellabo](https://github.com/mastodon/mastodon/pull/14356)) @@ -1411,6 +1540,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Clear out media attachments in a separate worker (slow) ## [3.1.5] - 2020-07-07 + ### Security - Fix media attachment enumeration ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14254)) @@ -1418,6 +1548,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix other sessions not being logged out on password change ([Gargron](https://github.com/mastodon/mastodon/pull/14252)) ## [3.1.4] - 2020-05-14 + ### Added - Add `vi` to available locales ([taicv](https://github.com/mastodon/mastodon/pull/13542)) @@ -1456,7 +1587,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix regression in `tootctl media remove-orphans` ([Gargron](https://github.com/mastodon/mastodon/pull/13405)) - Fix old unique jobs digests not having been cleaned up ([Gargron](https://github.com/mastodon/mastodon/pull/13683)) - Fix own following/followers not showing muted users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13614)) -- Fix list of followed people ignoring sorting on Follows & Followers page ([taras2358](https://github.com/mastodon/mastodon/pull/13676)) +- Fix list of followed people ignoring sorting on Follows & Followers page ([taras2358](https://github.com/mastodon/mastodon/pull/13676)) - Fix wrong pgHero Content-Security-Policy when `CDN_HOST` is set ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13595)) - Fix needlessly deduplicating usernames on collisions with remote accounts when signing-up through SAML/CAS ([kaiyou](https://github.com/mastodon/mastodon/pull/13581)) - Fix page incorrectly scrolling when bringing up dropdown menus in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13574)) @@ -1485,6 +1616,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - The issue only affects developers of apps who are shared between multiple users, such as server-side apps like cross-posters ## [3.1.3] - 2020-04-05 + ### Added - Add ability to filter audit log in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/13381)) @@ -1558,6 +1690,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix re-sending of e-mail confirmation not being rate limited ([Gargron](https://github.com/mastodon/mastodon/pull/13360)) ## [v3.1.2] - 2020-02-27 + ### Added - Add `--reset-password` option to `tootctl accounts modify` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13126)) @@ -1584,11 +1717,13 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix leak of arbitrary statuses through unfavourite action in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/13161)) ## [3.1.1] - 2020-02-10 + ### Fixed - Fix yanked dependency preventing installation ([mayaeh](https://github.com/mastodon/mastodon/pull/13059)) ## [3.1.0] - 2020-02-09 + ### Added - Add bookmarks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/7107), [Gargron](https://github.com/mastodon/mastodon/pull/12494), [Gomasy](https://github.com/mastodon/mastodon/pull/12381)) @@ -1753,6 +1888,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix settings pages being cacheable by the browser ([Gargron](https://github.com/mastodon/mastodon/pull/12714)) ## [3.0.1] - 2019-10-10 + ### Added - Add `tootctl media usage` command ([Gargron](https://github.com/mastodon/mastodon/pull/12115)) @@ -1786,6 +1922,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix `tootctl accounts cull` advertising unused option flag ([Kjwon15](https://github.com/mastodon/mastodon/pull/12074)) ## [3.0.0] - 2019-10-03 + ### Added - Add "not available" label to unloaded media attachments in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/11715), [Gargron](https://github.com/mastodon/mastodon/pull/11745)) @@ -1982,6 +2119,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix performance of GIF re-encoding and always strip EXIF data from videos ([Gargron](https://github.com/mastodon/mastodon/pull/12057)) ## [2.9.3] - 2019-08-10 + ### Added - Add GIF and WebP support for custom emojis ([Gargron](https://github.com/mastodon/mastodon/pull/11519)) @@ -2041,6 +2179,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix blocked domains still being able to fill database with account records ([Gargron](https://github.com/mastodon/mastodon/pull/11219)) ## [2.9.2] - 2019-06-22 + ### Added - Add `short_description` and `approval_required` to `GET /api/v1/instance` ([Gargron](https://github.com/mastodon/mastodon/pull/11146)) @@ -2055,6 +2194,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix audio not being downloaded from remote servers ([Gargron](https://github.com/mastodon/mastodon/pull/11145)) ## [2.9.1] - 2019-06-22 + ### Added - Add moderation API ([Gargron](https://github.com/mastodon/mastodon/pull/9387)) @@ -2080,6 +2220,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix scrolling behaviour in compose form ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11093)) ## [2.9.0] - 2019-06-13 + ### Added - **Add single-column mode in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/10807), [Gargron](https://github.com/mastodon/mastodon/pull/10848), [Gargron](https://github.com/mastodon/mastodon/pull/11003), [Gargron](https://github.com/mastodon/mastodon/pull/10961), [Hanage999](https://github.com/mastodon/mastodon/pull/10915), [noellabo](https://github.com/mastodon/mastodon/pull/10917), [abcang](https://github.com/mastodon/mastodon/pull/10859), [Gargron](https://github.com/mastodon/mastodon/pull/10820), [Gargron](https://github.com/mastodon/mastodon/pull/10835), [Gargron](https://github.com/mastodon/mastodon/pull/10809), [Gargron](https://github.com/mastodon/mastodon/pull/10963), [noellabo](https://github.com/mastodon/mastodon/pull/10883), [Hanage999](https://github.com/mastodon/mastodon/pull/10839)) @@ -2134,6 +2275,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix login sometimes redirecting to paths that are not pages ([Gargron](https://github.com/mastodon/mastodon/pull/11019)) ## [2.8.4] - 2019-05-24 + ### Fixed - Fix delivery not retrying on some inbox errors that should be retriable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10812)) @@ -2145,6 +2287,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Require specific OAuth scopes for specific endpoints of the streaming API, instead of merely requiring a token for all endpoints, and allow using WebSockets protocol negotiation to specify the access token instead of using a query string ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10818)) ## [2.8.3] - 2019-05-19 + ### Added - Add `og:image:alt` OpenGraph tag ([BenLubar](https://github.com/mastodon/mastodon/pull/10779)) @@ -2167,6 +2310,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix "invited by" not showing up in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10791)) ## [2.8.2] - 2019-05-05 + ### Added - Add `SOURCE_TAG` environment variable ([ushitora-anqou](https://github.com/mastodon/mastodon/pull/10698)) @@ -2179,6 +2323,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix closing video modal scrolling timelines to top ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10695)) ## [2.8.1] - 2019-05-04 + ### Added - Add link to existing domain block when trying to block an already-blocked domain ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10663)) @@ -2218,6 +2363,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix confirmation modals being too narrow for a secondary action button ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10586)) ## [2.8.0] - 2019-04-10 + ### Added - Add polls ([Gargron](https://github.com/mastodon/mastodon/pull/10111), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10155), [Gargron](https://github.com/mastodon/mastodon/pull/10184), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10196), [Gargron](https://github.com/mastodon/mastodon/pull/10248), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10255), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10322), [Gargron](https://github.com/mastodon/mastodon/pull/10138), [Gargron](https://github.com/mastodon/mastodon/pull/10139), [Gargron](https://github.com/mastodon/mastodon/pull/10144), [Gargron](https://github.com/mastodon/mastodon/pull/10145),[Gargron](https://github.com/mastodon/mastodon/pull/10146), [Gargron](https://github.com/mastodon/mastodon/pull/10148), [Gargron](https://github.com/mastodon/mastodon/pull/10151), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10150), [Gargron](https://github.com/mastodon/mastodon/pull/10168), [Gargron](https://github.com/mastodon/mastodon/pull/10165), [Gargron](https://github.com/mastodon/mastodon/pull/10172), [Gargron](https://github.com/mastodon/mastodon/pull/10170), [Gargron](https://github.com/mastodon/mastodon/pull/10171), [Gargron](https://github.com/mastodon/mastodon/pull/10186), [Gargron](https://github.com/mastodon/mastodon/pull/10189), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10200), [rinsuki](https://github.com/mastodon/mastodon/pull/10203), [Gargron](https://github.com/mastodon/mastodon/pull/10213), [Gargron](https://github.com/mastodon/mastodon/pull/10246), [Gargron](https://github.com/mastodon/mastodon/pull/10265), [Gargron](https://github.com/mastodon/mastodon/pull/10261), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10333), [Gargron](https://github.com/mastodon/mastodon/pull/10352), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10140), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10142), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10141), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10162), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10161), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10158), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10156), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10160), [Gargron](https://github.com/mastodon/mastodon/pull/10185), [Gargron](https://github.com/mastodon/mastodon/pull/10188), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10195), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10208), [Gargron](https://github.com/mastodon/mastodon/pull/10187), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10214), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10209)) @@ -2301,6 +2447,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix `tootctl accounts cull` sometimes removing accounts that are temporarily unreachable ([BenLubar](https://github.com/mastodon/mastodon/pull/10460)) ## [2.7.4] - 2019-03-05 + ### Fixed - Fix web UI not cleaning up notifications after block ([Gargron](https://github.com/mastodon/mastodon/pull/10108)) @@ -2315,6 +2462,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix edit profile page crash for suspended-then-unsuspended users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10178)) ## [2.7.3] - 2019-02-23 + ### Added - Add domain filter to the admin federation page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10071)) @@ -2332,6 +2480,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Change custom emojis to randomize stored file name ([hinaloe](https://github.com/mastodon/mastodon/pull/10090)) ## [2.7.2] - 2019-02-17 + ### Added - Add support for IPv6 in e-mail validation ([zoc](https://github.com/mastodon/mastodon/pull/10009)) @@ -2373,6 +2522,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Change error graphic to hover-to-play ([Gargron](https://github.com/mastodon/mastodon/pull/10055)) ## [2.7.1] - 2019-01-28 + ### Fixed - Fix SSO authentication not working due to missing agreement boolean ([Gargron](https://github.com/mastodon/mastodon/pull/9915)) @@ -2387,6 +2537,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix missing strong style for landing page description ([Kjwon15](https://github.com/mastodon/mastodon/pull/9892)) ## [2.7.0] - 2019-01-20 + ### Added - Add link for adding a user to a list from their profile ([namelessGonbai](https://github.com/mastodon/mastodon/pull/9062)) @@ -2516,6 +2667,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Add tombstones for remote statuses to prevent replay attacks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9830)) ## [2.6.5] - 2018-12-01 + ### Changed - Change lists to display replies to others on the list and list owner ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9324)) @@ -2525,11 +2677,13 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix failures caused by commonly-used JSON-LD contexts being unavailable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9412)) ## [2.6.4] - 2018-11-30 + ### Fixed - Fix yarn dependencies not installing due to yanked event-stream package ([Gargron](https://github.com/mastodon/mastodon/pull/9401)) ## [2.6.3] - 2018-11-30 + ### Added - Add hyphen to characters allowed in remote usernames ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9345)) @@ -2549,6 +2703,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix TLS handshake timeout not being enforced ([Gargron](https://github.com/mastodon/mastodon/pull/9381)) ## [2.6.2] - 2018-11-23 + ### Added - Add Page to whitelisted ActivityPub types ([mbajur](https://github.com/mastodon/mastodon/pull/9188)) @@ -2583,12 +2738,14 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix HTTP connection timeout of 10s not being enforced ([Gargron](https://github.com/mastodon/mastodon/pull/9329)) ## [2.6.1] - 2018-10-30 + ### Fixed - Fix resolving resources by URL not working due to a regression in [valerauko](https://github.com/mastodon/mastodon/pull/9132) ([Gargron](https://github.com/mastodon/mastodon/pull/9171)) - Fix reducer error in web UI when a conversation has no last status ([Gargron](https://github.com/mastodon/mastodon/pull/9173)) ## [2.6.0] - 2018-10-30 + ### Added - Add link ownership verification ([Gargron](https://github.com/mastodon/mastodon/pull/8703)) @@ -2693,11 +2850,13 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix handling of content types with profile ([valerauko](https://github.com/mastodon/mastodon/pull/9132)) ## [2.5.2] - 2018-10-12 + ### Security - Fix XSS vulnerability ([Gargron](https://github.com/mastodon/mastodon/pull/8959)) ## [2.5.1] - 2018-10-07 + ### Fixed - Fix database migrations for PostgreSQL below 9.5 ([Gargron](https://github.com/mastodon/mastodon/pull/8903)) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index b3eafdc482..2ee2e538bc 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ed670f5a6a..a232915b6f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,10 @@ -# Contributing to Mastodon Glitch Edition # +# Contributing to Mastodon Glitch Edition Thank you for your interest in contributing to the `glitch-soc` project! Here are some guidelines, and ways you can help. -> (This document is a bit of a work-in-progress, so please bear with us. -> If you don't see what you're looking for here, please don't hesitate to reach out!) +> (This document is a bit of a work-in-progress, so please bear with us. +> If you don't see what you're looking for here, please don't hesitate to reach out!) ## Translations @@ -12,34 +12,32 @@ You can submit glitch-soc-specific translations via [Crowdin](https://crowdin.co [![Crowdin](https://badges.crowdin.net/glitch-soc/localized.svg)](https://crowdin.com/project/glitch-soc) -## Planning ## +## Planning Right now a lot of the planning for this project takes place in our development Discord, or through GitHub Issues and Projects. We're working on ways to improve the planning structure and better solicit feedback, and if you feel like you can help in this respect, feel free to give us a holler. -## Documentation ## +## Documentation The documentation for this repository is available at [`glitch-soc/docs`](https://github.com/glitch-soc/docs) (online at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/)). Right now, we've mostly focused on the features that make this fork different from upstream in some manner. Adding screenshots, improving descriptions, and so forth are all ways to help contribute to the project even if you don't know any code. -## Frontend Development ## +## Frontend Development Check out [the documentation here](https://glitch-soc.github.io/docs/contributing/frontend/) for more information. -## Backend Development ## +## Backend Development See the guidelines below. - - - - +--- You should also try to follow the guidelines set out in the original `CONTRIBUTING.md` from `mastodon/mastodon`, reproduced below.
-CONTRIBUTING -======= -Contributing +# Contributing Thank you for considering contributing to Mastodon 🐘 @@ -68,9 +66,9 @@ You can submit translations via [Crowdin](https://crowdin.com/project/mastodon). Example: -|Not ideal|Better| -|---|----| -|Fixed NoMethodError in RemovalWorker|Fix nil error when removing statuses caused by race condition| +| Not ideal | Better | +| ------------------------------------ | ------------------------------------------------------------- | +| Fixed NoMethodError in RemovalWorker | Fix nil error when removing statuses caused by race condition | It is not always possible to phrase every change in such a manner, but it is desired. @@ -82,8 +80,6 @@ It is not always possible to phrase every change in such a manner, but it is des - Code style rules (rubocop, eslint) - Normalization of locale files (i18n-tasks) -**Note**: You may need to log in and authorise the GitHub account your fork of this repository belongs to with CircleCI to enable some of the automated checks to run. - ## Documentation The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/documentation](https://github.com/mastodon/documentation). diff --git a/Capfile b/Capfile index bf3ae7e249..86efa5bacf 100644 --- a/Capfile +++ b/Capfile @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'capistrano/setup' require 'capistrano/deploy' require 'capistrano/scm/git' diff --git a/Dockerfile b/Dockerfile index 04e3b58b1e..cb5096581c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ # syntax=docker/dockerfile:1.4 # This needs to be bullseye-slim because the Ruby image is built on bullseye-slim -ARG NODE_VERSION="16.19-bullseye-slim" +ARG NODE_VERSION="16.20-bullseye-slim" -FROM ghcr.io/moritzheiber/ruby-jemalloc:3.0.5-slim as ruby +FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim as ruby FROM node:${NODE_VERSION} as build COPY --link --from=ruby /opt/ruby /opt/ruby @@ -18,7 +18,6 @@ COPY Gemfile* package.json yarn.lock /opt/mastodon/ # hadolint ignore=DL3008 RUN apt-get update && \ apt-get install -y --no-install-recommends build-essential \ - ca-certificates \ git \ libicu-dev \ libidn11-dev \ @@ -37,11 +36,15 @@ RUN apt-get update && \ bundle config set --local without 'development test' && \ bundle config set silence_root_warning true && \ bundle install -j"$(nproc)" && \ - yarn install --pure-lockfile --network-timeout 600000 && \ + yarn install --pure-lockfile --production --network-timeout 600000 && \ yarn cache clean FROM node:${NODE_VERSION} +# Use those args to specify your own version flags & suffixes +ARG MASTODON_VERSION_FLAGS="" +ARG MASTODON_VERSION_SUFFIX="" + ARG UID="991" ARG GID="991" @@ -52,7 +55,7 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"] ENV DEBIAN_FRONTEND="noninteractive" \ PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" -# Ignoreing these here since we don't want to pin any versions and the Debian image removes apt-get content after use +# 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 && \ @@ -85,7 +88,9 @@ COPY --chown=mastodon:mastodon --from=build /opt/mastodon /opt/mastodon ENV RAILS_ENV="production" \ NODE_ENV="production" \ RAILS_SERVE_STATIC_FILES="true" \ - BIND="0.0.0.0" + BIND="0.0.0.0" \ + MASTODON_VERSION_FLAGS="${MASTODON_VERSION_FLAGS}" \ + MASTODON_VERSION_SUFFIX="${MASTODON_VERSION_SUFFIX}" # Set the run user USER mastodon diff --git a/Gemfile b/Gemfile index a3d2a9bdef..7a0fbdc82d 100644 --- a/Gemfile +++ b/Gemfile @@ -1,27 +1,24 @@ # frozen_string_literal: true source 'https://rubygems.org' -ruby '>= 2.7.0', '< 3.1.0' +ruby '>= 3.0.0' -gem 'pkg-config', '~> 1.5' -gem 'rexml', '~> 3.2' - -gem 'puma', '~> 5.6' +gem 'puma', '~> 6.3' gem 'rails', '~> 6.1.7' gem 'sprockets', '~> 3.7.2' gem 'thor', '~> 1.2' -gem 'rack', '~> 2.2.6' +gem 'rack', '~> 2.2.7' gem 'haml-rails', '~>2.0' -gem 'pg', '~> 1.4' +gem 'pg', '~> 1.5' gem 'makara', '~> 0.5' gem 'pghero' gem 'dotenv-rails', '~> 2.8' -gem 'aws-sdk-s3', '~> 1.119', require: false +gem 'aws-sdk-s3', '~> 1.123', require: false gem 'fog-core', '<= 2.4.0' gem 'fog-openstack', '~> 0.3', require: false -gem 'kt-paperclip', '~> 7.1' +gem 'kt-paperclip', '~> 7.2' gem 'blurhash', '~> 0.1' gem 'active_model_serializers', '~> 0.10' @@ -29,18 +26,18 @@ gem 'addressable', '~> 2.8' gem 'bootsnap', '~> 1.16.0', require: false gem 'browser' gem 'charlock_holmes', '~> 0.7.7' -gem 'chewy', '~> 7.2' -gem 'devise', '~> 4.8' -gem 'devise-two-factor', '~> 4.0' +gem 'chewy', '~> 7.3' +gem 'devise', '~> 4.9' +gem 'devise-two-factor', '~> 4.1' group :pam_authentication, optional: true do gem 'devise_pam_authenticatable2', '~> 9.2' end -gem 'net-ldap', '~> 0.17' +gem 'net-ldap', '~> 0.18' gem 'omniauth-cas', '~> 2.0' gem 'omniauth-saml', '~> 1.10' -gem 'omniauth_openid_connect', '~> 0.6.0' +gem 'omniauth_openid_connect', '~> 0.6.1' gem 'omniauth', '~> 1.9' gem 'omniauth-rails_csrf_protection', '~> 0.1' @@ -60,9 +57,8 @@ gem 'idn-ruby', require: 'idn' gem 'kaminari', '~> 1.2' gem 'link_header', '~> 0.0' gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar' -gem 'nokogiri', '~> 1.14' -gem 'nsa', '~> 0.2' -gem 'oj', '~> 3.13' +gem 'nokogiri', '~> 1.15' +gem 'oj', '~> 3.14' gem 'ox', '~> 2.14' gem 'parslet' gem 'posix-spawn' @@ -70,14 +66,14 @@ gem 'public_suffix', '~> 5.0' gem 'pundit', '~> 2.3' gem 'premailer-rails' gem 'rack-attack', '~> 6.6' -gem 'rack-cors', '~> 1.1', require: 'rack/cors' +gem 'rack-cors', '~> 2.0', require: 'rack/cors' gem 'rails-i18n', '~> 6.0' -gem 'rails-settings-cached', '~> 0.6' +gem 'rails-settings-cached', '~> 0.6', git: 'https://github.com/mastodon/rails-settings-cached.git', branch: 'v0.6.6-aliases-true' gem 'redcarpet', '~> 3.6' gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' -gem 'rqrcode', '~> 2.1' -gem 'ruby-progressbar', '~> 1.11' +gem 'rqrcode', '~> 2.2' +gem 'ruby-progressbar', '~> 1.13' gem 'sanitize', '~> 6.0' gem 'scenic', '~> 1.7' gem 'sidekiq', '~> 6.5' @@ -88,66 +84,99 @@ gem 'simple-navigation', '~> 4.4' gem 'simple_form', '~> 5.2' gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie' gem 'stoplight', '~> 3.0.1' -gem 'strong_migrations', '~> 0.7' +gem 'strong_migrations', '~> 0.8' gem 'tty-prompt', '~> 0.23', require: false gem 'twitter-text', '~> 3.1.0' -gem 'tzinfo-data', '~> 1.2022' +gem 'tzinfo-data', '~> 1.2023' gem 'webpacker', '~> 5.4' gem 'webpush', github: 'ClearlyClaire/webpush', ref: 'f14a4d52e201128b1b00245d11b6de80d6cfdcd9' -gem 'webauthn', '~> 2.5' +gem 'webauthn', '~> 3.0' gem 'json-ld' gem 'json-ld-preloaded', '~> 3.2' gem 'rdf-normalize', '~> 0.5' -group :development, :test do - gem 'fabrication', '~> 2.30' - gem 'fuubar', '~> 2.5' - gem 'i18n-tasks', '~> 1.0', require: false - gem 'pry-byebug', '~> 3.10' - gem 'pry-rails', '~> 0.3' - gem 'rspec-rails', '~> 5.1' - gem 'rubocop-performance', require: false - gem 'rubocop-rails', require: false - gem 'rubocop-rspec', require: false - gem 'rubocop', require: false -end - -group :production, :test do - gem 'private_address_check', '~> 0.5' -end +gem 'private_address_check', '~> 0.5' group :test do - gem 'capybara', '~> 3.38' - gem 'climate_control', '~> 0.2' - gem 'faker', '~> 3.1' - gem 'json-schema', '~> 3.0' - gem 'rack-test', '~> 2.0' - gem 'rails-controller-testing', '~> 1.0' - gem 'rspec_junit_formatter', '~> 0.6' + # RSpec runner for rails + gem 'rspec-rails', '~> 6.0' + + # Used to split testing into chunks in CI + gem 'rspec_chunked', '~> 0.6' + + # RSpec progress bar formatter + gem 'fuubar', '~> 2.5' + + # Extra RSpec extenion methods and helpers for sidekiq gem 'rspec-sidekiq', '~> 3.1' + + # Browser integration testing + gem 'capybara', '~> 3.39' + + # Used to mock environment variables + gem 'climate_control', '~> 0.2' + + # Generating fake data for specs + gem 'faker', '~> 3.2' + + # Generate test objects for specs + gem 'fabrication', '~> 2.30' + + # Add back helpers functions removed in Rails 5.1 + gem 'rails-controller-testing', '~> 1.0' + + # Validate schemas in specs + gem 'json-schema', '~> 4.0' + + # Test harness fo rack components + gem 'rack-test', '~> 2.1' + + # Coverage formatter for RSpec test if DISABLE_SIMPLECOV is false gem 'simplecov', '~> 0.22', require: false + + # Stub web requests for specs gem 'webmock', '~> 3.18' end group :development do - gem 'active_record_query_trace', '~> 1.8' + # Code linting CLI and plugins + gem 'rubocop', require: false + gem 'rubocop-capybara', require: false + gem 'rubocop-performance', require: false + gem 'rubocop-rails', require: false + gem 'rubocop-rspec', require: false + + # Annotates modules with schema gem 'annotate', '~> 3.2' + + # Enhanced error message pages for development gem 'better_errors', '~> 2.9' gem 'binding_of_caller', '~> 1.0' - gem 'bullet', '~> 7.0' + + # Preview mail in the browser gem 'letter_opener', '~> 1.8' gem 'letter_opener_web', '~> 2.0' - gem 'memory_profiler' + + # Security analysis CLI tools gem 'brakeman', '~> 5.4', require: false gem 'bundler-audit', '~> 0.9', require: false + # Linter CLI for HAML files + gem 'haml_lint', require: false + + # Deployment automation gem 'capistrano', '~> 3.17' gem 'capistrano-rails', '~> 1.6' gem 'capistrano-rbenv', '~> 2.2' gem 'capistrano-yarn', '~> 2.0' - gem 'stackprof' + # Validate missing i18n keys + gem 'i18n-tasks', '~> 1.0', require: false + + # Profiling tools + gem 'memory_profiler', require: false + gem 'stackprof', require: false end group :production do @@ -158,7 +187,9 @@ gem 'concurrent-ruby', require: false gem 'connection_pool', require: false gem 'xorcist', '~> 1.1' -gem 'hcaptcha', '~> 7.1' gem 'cocoon', '~> 1.2' gem 'net-http', '~> 0.3.2' +gem 'rubyzip', '~> 2.3' + +gem 'hcaptcha', '~> 7.1' diff --git a/Gemfile.lock b/Gemfile.lock index 0ad3d63280..b2d75e9d4a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,43 +7,51 @@ GIT hkdf (~> 0.2) jwt (~> 2.0) +GIT + remote: https://github.com/mastodon/rails-settings-cached.git + revision: 86328ef0bd04ce21cc0504ff5e334591e8c2ccab + branch: v0.6.6-aliases-true + specs: + rails-settings-cached (0.6.6) + rails (>= 4.2.0) + GEM remote: https://rubygems.org/ specs: - actioncable (6.1.7.2) - actionpack (= 6.1.7.2) - activesupport (= 6.1.7.2) + actioncable (6.1.7.4) + actionpack (= 6.1.7.4) + activesupport (= 6.1.7.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.7.2) - actionpack (= 6.1.7.2) - activejob (= 6.1.7.2) - activerecord (= 6.1.7.2) - activestorage (= 6.1.7.2) - activesupport (= 6.1.7.2) + actionmailbox (6.1.7.4) + actionpack (= 6.1.7.4) + activejob (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) mail (>= 2.7.1) - actionmailer (6.1.7.2) - actionpack (= 6.1.7.2) - actionview (= 6.1.7.2) - activejob (= 6.1.7.2) - activesupport (= 6.1.7.2) + actionmailer (6.1.7.4) + actionpack (= 6.1.7.4) + actionview (= 6.1.7.4) + activejob (= 6.1.7.4) + activesupport (= 6.1.7.4) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.7.2) - actionview (= 6.1.7.2) - activesupport (= 6.1.7.2) + actionpack (6.1.7.4) + actionview (= 6.1.7.4) + activesupport (= 6.1.7.4) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.7.2) - actionpack (= 6.1.7.2) - activerecord (= 6.1.7.2) - activestorage (= 6.1.7.2) - activesupport (= 6.1.7.2) + actiontext (6.1.7.4) + actionpack (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) nokogiri (>= 1.8.5) - actionview (6.1.7.2) - activesupport (= 6.1.7.2) + actionview (6.1.7.4) + activesupport (= 6.1.7.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -53,29 +61,28 @@ GEM activemodel (>= 4.1, < 7.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - active_record_query_trace (1.8) - activejob (6.1.7.2) - activesupport (= 6.1.7.2) + activejob (6.1.7.4) + activesupport (= 6.1.7.4) globalid (>= 0.3.6) - activemodel (6.1.7.2) - activesupport (= 6.1.7.2) - activerecord (6.1.7.2) - activemodel (= 6.1.7.2) - activesupport (= 6.1.7.2) - activestorage (6.1.7.2) - actionpack (= 6.1.7.2) - activejob (= 6.1.7.2) - activerecord (= 6.1.7.2) - activesupport (= 6.1.7.2) + activemodel (6.1.7.4) + activesupport (= 6.1.7.4) + activerecord (6.1.7.4) + activemodel (= 6.1.7.4) + activesupport (= 6.1.7.4) + activestorage (6.1.7.4) + actionpack (= 6.1.7.4) + activejob (= 6.1.7.4) + activerecord (= 6.1.7.4) + activesupport (= 6.1.7.4) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.7.2) + activesupport (6.1.7.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) zeitwerk (~> 2.3) - addressable (2.8.1) + addressable (2.8.4) public_suffix (>= 2.0.2, < 6.0) aes_key_wrap (1.1.0) airbrussh (1.4.1) @@ -85,31 +92,31 @@ GEM activerecord (>= 3.2, < 8.0) rake (>= 10.4, < 14.0) ast (2.4.2) - attr_encrypted (3.1.0) + attr_encrypted (4.0.0) encryptor (~> 3.0.0) attr_required (1.0.1) awrence (1.2.1) aws-eventstream (1.2.0) - aws-partitions (1.701.0) - aws-sdk-core (3.170.0) + aws-partitions (1.780.0) + aws-sdk-core (3.175.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.5) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.62.0) - aws-sdk-core (~> 3, >= 3.165.0) + aws-sdk-kms (1.67.0) + aws-sdk-core (~> 3, >= 3.174.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.119.0) - aws-sdk-core (~> 3, >= 3.165.0) + aws-sdk-s3 (1.126.0) + aws-sdk-core (~> 3, >= 3.174.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) aws-sigv4 (1.5.2) aws-eventstream (~> 1, >= 1.0.2) - bcrypt (3.1.17) - better_errors (2.9.1) - coderay (>= 1.0.0) + bcrypt (3.1.18) + better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) + rouge (>= 1.0.0) better_html (2.0.1) actionview (>= 6.0) activesupport (>= 6.0) @@ -117,31 +124,27 @@ GEM erubi (~> 1.4) parser (>= 2.4) smart_properties - bindata (2.4.14) + bindata (2.4.15) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) blurhash (0.1.7) bootsnap (1.16.0) msgpack (~> 1.2) - brakeman (5.4.0) - browser (4.2.0) + brakeman (5.4.1) + browser (5.3.1) brpoplpush-redis_script (0.1.3) concurrent-ruby (~> 1.0, >= 1.0.5) redis (>= 1.0, < 6) builder (3.2.4) - bullet (7.0.7) - activesupport (>= 3.0.0) - uniform_notifier (~> 1.11) bundler-audit (0.9.1) bundler (>= 1.2.0, < 3) thor (~> 1.0) - byebug (11.1.3) - capistrano (3.17.1) + capistrano (3.17.3) airbrussh (>= 1.0.0) i18n rake (>= 10.0.0) sshkit (>= 1.9.0) - capistrano-bundler (2.0.1) + capistrano-bundler (2.1.0) capistrano (~> 3.1) capistrano-rails (1.6.2) capistrano (~> 3.1) @@ -151,7 +154,7 @@ GEM sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (3.38.0) + capybara (3.39.2) addressable matrix mini_mime (>= 0.1.3) @@ -164,36 +167,35 @@ GEM activesupport cbor (0.5.9.6) charlock_holmes (0.7.7) - chewy (7.2.7) + chewy (7.3.2) activesupport (>= 5.2) elasticsearch (>= 7.12.0, < 7.14.0) elasticsearch-dsl chunky_png (1.4.0) climate_control (0.2.0) cocoon (1.2.15) - coderay (1.1.3) color_diff (0.1) - concurrent-ruby (1.2.0) - connection_pool (2.3.0) - cose (1.2.1) + concurrent-ruby (1.2.2) + connection_pool (2.4.1) + cose (1.3.0) cbor (~> 0.5.9) openssl-signature_algorithm (~> 1.0) crack (0.4.5) rexml crass (1.0.6) - css_parser (1.12.0) + css_parser (1.14.0) addressable date (3.3.3) - debug_inspector (1.0.0) - devise (4.8.1) + debug_inspector (1.1.0) + devise (4.9.2) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-two-factor (4.0.2) + devise-two-factor (4.1.0) activesupport (< 7.1) - attr_encrypted (>= 1.3, < 4, != 2) + attr_encrypted (>= 1.3, < 5, != 2) devise (~> 4.0) railties (< 7.1) rotp (~> 6.0) @@ -206,7 +208,7 @@ GEM docile (1.4.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - doorkeeper (5.6.4) + doorkeeper (5.6.6) railties (>= 5) dotenv (2.8.1) dotenv-rails (2.8.1) @@ -226,9 +228,9 @@ GEM erubi (1.12.0) et-orbi (1.2.7) tzinfo - excon (0.95.0) + excon (0.100.0) fabrication (2.30.0) - faker (3.1.1) + faker (3.2.0) i18n (>= 1.8.11, < 2) faraday (1.10.3) faraday-em_http (~> 1.0) @@ -254,7 +256,7 @@ GEM faraday-rack (1.0.0) faraday-retry (1.0.3) fast_blank (1.0.1) - fastimage (2.2.6) + fastimage (2.2.7) ffi (1.15.5) ffi-compiler (1.0.1) ffi (>= 1.0.0) @@ -289,11 +291,17 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) + haml_lint (0.45.0) + haml (>= 4.0, < 6.2) + parallel (~> 1.10) + rainbow + rubocop (>= 0.50.0) + sysexits (~> 1.1) hashdiff (1.0.1) hashie (5.0.0) hcaptcha (7.1.0) json - highline (2.0.3) + highline (2.1.0) hiredis (0.6.3) hkdf (0.3.0) htmlentities (4.3.4) @@ -310,7 +318,7 @@ GEM httplog (1.6.2) rack (>= 2.0) rainbow (>= 2.0.0) - i18n (1.12.0) + i18n (1.14.1) concurrent-ruby (~> 1.0) i18n-tasks (1.0.12) activesupport (>= 4.0.2) @@ -327,26 +335,26 @@ GEM ipaddress (0.8.3) jmespath (1.6.2) json (2.6.3) - json-canonicalization (0.3.0) + json-canonicalization (0.3.2) json-jwt (1.15.3) activesupport (>= 4.2) aes_key_wrap bindata httpclient - json-ld (3.2.3) + json-ld (3.2.5) htmlentities (~> 4.3) - json-canonicalization (~> 0.3) + json-canonicalization (~> 0.3, >= 0.3.2) link_header (~> 0.0, >= 0.0.8) multi_json (~> 1.15) - rack (~> 2.2) - rdf (~> 3.2, >= 3.2.9) + rack (>= 2.2, < 4) + rdf (~> 3.2, >= 3.2.10) json-ld-preloaded (3.2.2) json-ld (~> 3.2) rdf (~> 3.2) - json-schema (3.0.0) + json-schema (4.0.0) addressable (>= 2.8) jsonapi-renderer (0.2.2) - jwt (2.5.0) + jwt (2.7.1) kaminari (1.2.2) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.2) @@ -359,14 +367,14 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) - kt-paperclip (7.1.1) + kt-paperclip (7.2.0) activemodel (>= 4.2.0) activesupport (>= 4.2.0) marcel (~> 1.0.1) mime-types terrapin (~> 0.6.0) - launchy (2.5.0) - addressable (~> 2.7) + launchy (2.5.2) + addressable (~> 2.8) letter_opener (1.8.1) launchy (>= 2.2, < 3) letter_opener_web (2.0.0) @@ -383,10 +391,10 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.19.1) + loofah (2.21.3) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.8.0.1) + nokogiri (>= 1.12.0) + mail (2.8.1) mini_mime (>= 0.1.1) net-imap net-pop @@ -401,38 +409,33 @@ GEM method_source (1.0.0) mime-types (3.4.1) mime-types-data (~> 3.2015) - mime-types-data (3.2022.0105) + mime-types-data (3.2023.0218.1) mini_mime (1.1.2) - mini_portile2 (2.8.1) - minitest (5.17.0) - msgpack (1.6.0) + mini_portile2 (2.8.2) + minitest (5.18.1) + msgpack (1.7.1) multi_json (1.15.0) multipart-post (2.3.0) net-http (0.3.2) uri - net-imap (0.3.4) + net-imap (0.3.6) date net-protocol - net-ldap (0.17.1) + net-ldap (0.18.0) net-pop (0.1.2) net-protocol net-protocol (0.2.1) timeout - net-scp (4.0.0.rc1) + net-scp (4.0.0) net-ssh (>= 2.6.5, < 8.0.0) net-smtp (0.3.3) net-protocol - net-ssh (7.0.1) - nio4r (2.5.8) - nokogiri (1.14.1) - mini_portile2 (~> 2.8.0) + net-ssh (7.1.0) + nio4r (2.5.9) + nokogiri (1.15.2) + mini_portile2 (~> 2.8.2) racc (~> 1.4) - nsa (0.2.8) - activesupport (>= 4.2, < 7) - concurrent-ruby (~> 1.0, >= 1.0.2) - sidekiq (>= 3.5) - statsd-ruby (~> 1.4, >= 1.4.0) - oj (3.13.23) + oj (3.15.0) omniauth (1.9.2) hashie (>= 3.4.6) rack (>= 1.6.2, < 3) @@ -446,7 +449,7 @@ GEM omniauth-saml (1.10.3) omniauth (~> 1.3, >= 1.3.2) ruby-saml (~> 1.9) - omniauth_openid_connect (0.6.0) + omniauth_openid_connect (0.6.1) omniauth (>= 1.9, < 3) openid_connect (~> 1.1) openid_connect (1.4.2) @@ -460,23 +463,23 @@ GEM validate_email validate_url webfinger (~> 1.2) - openssl (3.0.0) - openssl-signature_algorithm (1.2.1) - openssl (> 2.0, < 3.1) + openssl (3.1.0) + openssl-signature_algorithm (1.3.0) + openssl (> 2.0) orm_adapter (0.5.0) - ox (2.14.14) - parallel (1.22.1) - parser (3.2.1.0) + ox (2.14.16) + parallel (1.23.0) + parser (3.2.2.3) ast (~> 2.4.1) + racc parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) - pg (1.4.5) - pghero (3.1.0) + pg (1.5.3) + pghero (3.3.3) activerecord (>= 6) - pkg-config (1.5.1) posix-spawn (0.3.15) - premailer (1.18.0) + premailer (1.21.0) addressable css_parser (>= 1.12.0) htmlentities (>= 4.0.0) @@ -485,25 +488,17 @@ GEM net-smtp premailer (~> 1.7, >= 1.7.9) private_address_check (0.5.0) - pry (0.14.1) - coderay (~> 1.1) - method_source (~> 1.0) - pry-byebug (3.10.1) - byebug (~> 11.0) - pry (>= 0.13, < 0.15) - pry-rails (0.3.9) - pry (>= 0.10.4) public_suffix (5.0.1) - puma (5.6.5) + puma (6.3.0) nio4r (~> 2.0) pundit (2.3.0) activesupport (>= 3.0.0) raabro (1.4.0) - racc (1.6.2) - rack (2.2.6.2) + racc (1.7.1) + rack (2.2.7) rack-attack (6.6.1) rack (>= 1.0, < 3) - rack-cors (1.1.1) + rack-cors (2.0.1) rack (>= 2.0.0) rack-oauth2 (1.21.3) activesupport @@ -513,22 +508,22 @@ GEM rack (>= 2.1.0) rack-proxy (0.7.6) rack - rack-test (2.0.2) + rack-test (2.1.0) rack (>= 1.3) - rails (6.1.7.2) - actioncable (= 6.1.7.2) - actionmailbox (= 6.1.7.2) - actionmailer (= 6.1.7.2) - actionpack (= 6.1.7.2) - actiontext (= 6.1.7.2) - actionview (= 6.1.7.2) - activejob (= 6.1.7.2) - activemodel (= 6.1.7.2) - activerecord (= 6.1.7.2) - activestorage (= 6.1.7.2) - activesupport (= 6.1.7.2) + rails (6.1.7.4) + actioncable (= 6.1.7.4) + actionmailbox (= 6.1.7.4) + actionmailer (= 6.1.7.4) + actionpack (= 6.1.7.4) + actiontext (= 6.1.7.4) + actionview (= 6.1.7.4) + activejob (= 6.1.7.4) + activemodel (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) bundler (>= 1.15.0) - railties (= 6.1.7.2) + railties (= 6.1.7.4) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -537,96 +532,99 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.5.0) - loofah (~> 2.19, >= 2.19.1) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) rails-i18n (6.0.0) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 7) - rails-settings-cached (0.6.6) - rails (>= 4.2.0) - railties (6.1.7.2) - actionpack (= 6.1.7.2) - activesupport (= 6.1.7.2) + railties (6.1.7.4) + actionpack (= 6.1.7.4) + activesupport (= 6.1.7.4) method_source rake (>= 12.2) thor (~> 1.0) rainbow (3.1.1) rake (13.0.6) - rdf (3.2.9) + rdf (3.2.11) link_header (~> 0.0, >= 0.0.8) - rdf-normalize (0.5.1) + rdf-normalize (0.6.0) rdf (~> 3.2) redcarpet (3.6.0) - redis (4.5.1) - redis-namespace (1.10.0) + redis (4.8.1) + redis-namespace (1.11.0) redis (>= 4) redlock (1.3.2) redis (>= 3.0.0, < 6.0) - regexp_parser (2.7.0) + regexp_parser (2.8.1) request_store (1.5.1) rack (>= 1.4) - responders (3.0.1) - actionpack (>= 5.0) - railties (>= 5.0) + responders (3.1.0) + actionpack (>= 5.2) + railties (>= 5.2) rexml (3.2.5) - rotp (6.2.0) + rotp (6.2.2) + rouge (4.1.2) rpam2 (4.0.2) - rqrcode (2.1.2) + rqrcode (2.2.0) chunky_png (~> 1.0) rqrcode_core (~> 1.0) rqrcode_core (1.2.0) - rspec-core (3.11.0) - rspec-support (~> 3.11.0) - rspec-expectations (3.11.0) + rspec-core (3.12.2) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-mocks (3.11.1) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-rails (5.1.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - railties (>= 5.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) + rspec-support (~> 3.12.0) + rspec-rails (6.0.3) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) rspec-sidekiq (3.1.0) rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) - rspec-support (3.11.1) - rspec_junit_formatter (0.6.0) - rspec-core (>= 2, < 4, != 2.12.0) - rubocop (1.45.1) + rspec-support (3.12.0) + rspec_chunked (0.6) + rubocop (1.52.1) json (~> 2.3) parallel (~> 1.10) - parser (>= 3.2.0.0) + parser (>= 3.2.2.3) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.24.1, < 2.0) + rubocop-ast (>= 1.28.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.24.1) - parser (>= 3.1.1.0) - rubocop-capybara (2.17.0) + rubocop-ast (1.29.0) + parser (>= 3.2.1.0) + rubocop-capybara (2.18.0) rubocop (~> 1.41) - rubocop-performance (1.16.0) + rubocop-factory_bot (2.23.1) + rubocop (~> 1.33) + rubocop-performance (1.18.0) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) - rubocop-rails (2.17.4) + rubocop-rails (2.19.1) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) - rubocop-rspec (2.18.1) + rubocop-rspec (2.22.0) rubocop (~> 1.33) rubocop-capybara (~> 2.17) - ruby-progressbar (1.11.0) - ruby-saml (1.13.0) - nokogiri (>= 1.10.5) + rubocop-factory_bot (~> 2.22) + ruby-progressbar (1.13.0) + ruby-saml (1.15.0) + nokogiri (>= 1.13.10) rexml ruby2_keywords (0.0.5) - rufus-scheduler (3.8.2) + rubyzip (2.3.2) + rufus-scheduler (3.9.1) fugit (~> 1.1, >= 1.1.6) safety_net_attestation (0.4.0) jwt (~> 2.0) @@ -637,15 +635,15 @@ GEM activerecord (>= 4.0.0) railties (>= 4.0.0) semantic_range (3.0.0) - sidekiq (6.5.8) + sidekiq (6.5.9) connection_pool (>= 2.2.5, < 3) rack (~> 2.0) redis (>= 4.5.0, < 5) sidekiq-bulk (0.2.0) sidekiq - sidekiq-scheduler (5.0.1) + sidekiq-scheduler (5.0.3) rufus-scheduler (~> 3.2) - sidekiq (>= 4, < 8) + sidekiq (>= 6, < 8) tilt (>= 1.4.0) sidekiq-unique-jobs (7.1.29) brpoplpush-redis_script (> 0.1.1, <= 2.0.0) @@ -672,30 +670,30 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) - sshkit (1.21.2) + sshkit (1.21.4) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) - stackprof (0.2.23) - statsd-ruby (1.5.0) + stackprof (0.2.25) stoplight (3.0.1) redlock (~> 1.0) - strong_migrations (0.7.9) - activerecord (>= 5) + strong_migrations (0.8.0) + activerecord (>= 5.2) swd (1.3.0) activesupport (>= 3) attr_required (>= 0.0.5) httpclient (>= 2.4) - temple (0.10.0) + sysexits (1.2.0) + temple (0.10.2) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) - thor (1.2.1) - tilt (2.0.11) - timeout (0.3.1) - tpm-key_attestation (0.11.0) + thor (1.2.2) + tilt (2.2.0) + timeout (0.3.2) + tpm-key_attestation (0.12.0) bindata (~> 2.4) - openssl (> 2.0, < 3.1) + openssl (> 2.0) openssl-signature_algorithm (~> 1.0) tty-color (0.6.0) tty-cursor (0.7.1) @@ -712,14 +710,13 @@ GEM unf (~> 0.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - tzinfo-data (1.2022.7) + tzinfo-data (1.2023.3) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext unf_ext (0.0.8.2) unicode-display_width (2.4.2) - uniform_notifier (1.16.0) - uri (0.12.0) + uri (0.12.2) validate_email (0.1.6) activemodel (>= 3.0) mail (>= 2.2.5) @@ -728,15 +725,15 @@ GEM public_suffix warden (1.2.9) rack (>= 2.0.9) - webauthn (2.5.2) + webauthn (3.0.0) android_key_attestation (~> 0.3.0) awrence (~> 1.1) bindata (~> 2.4) cbor (~> 0.5.9) cose (~> 1.1) - openssl (>= 2.2, < 3.1) + openssl (>= 2.2) safety_net_attestation (~> 0.4.0) - tpm-key_attestation (~> 0.11.0) + tpm-key_attestation (~> 0.12.0) webfinger (1.2.0) activesupport httpclient (>= 2.4) @@ -756,52 +753,51 @@ GEM xorcist (1.1.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.7) + zeitwerk (2.6.8) PLATFORMS ruby DEPENDENCIES active_model_serializers (~> 0.10) - active_record_query_trace (~> 1.8) addressable (~> 2.8) annotate (~> 3.2) - aws-sdk-s3 (~> 1.119) + aws-sdk-s3 (~> 1.123) better_errors (~> 2.9) binding_of_caller (~> 1.0) blurhash (~> 0.1) bootsnap (~> 1.16.0) brakeman (~> 5.4) browser - bullet (~> 7.0) bundler-audit (~> 0.9) capistrano (~> 3.17) capistrano-rails (~> 1.6) capistrano-rbenv (~> 2.2) capistrano-yarn (~> 2.0) - capybara (~> 3.38) + capybara (~> 3.39) charlock_holmes (~> 0.7.7) - chewy (~> 7.2) + chewy (~> 7.3) climate_control (~> 0.2) cocoon (~> 1.2) color_diff (~> 0.1) concurrent-ruby connection_pool - devise (~> 4.8) - devise-two-factor (~> 4.0) + devise (~> 4.9) + devise-two-factor (~> 4.1) devise_pam_authenticatable2 (~> 9.2) discard (~> 1.2) doorkeeper (~> 5.6) dotenv-rails (~> 2.8) ed25519 (~> 1.3) fabrication (~> 2.30) - faker (~> 3.1) + faker (~> 3.2) fast_blank (~> 1.0) fastimage fog-core (<= 2.4.0) fog-openstack (~> 0.3) fuubar (~> 2.5) haml-rails (~> 2.0) + haml_lint hcaptcha (~> 7.1) hiredis (~> 0.6) htmlentities (~> 4.3) @@ -812,9 +808,9 @@ DEPENDENCIES idn-ruby json-ld json-ld-preloaded (~> 3.2) - json-schema (~> 3.0) + json-schema (~> 4.0) kaminari (~> 1.2) - kt-paperclip (~> 7.1) + kt-paperclip (~> 7.2) letter_opener (~> 1.8) letter_opener_web (~> 2.0) link_header (~> 0.0) @@ -824,50 +820,47 @@ DEPENDENCIES memory_profiler mime-types (~> 3.4.1) net-http (~> 0.3.2) - net-ldap (~> 0.17) - nokogiri (~> 1.14) - nsa (~> 0.2) - oj (~> 3.13) + net-ldap (~> 0.18) + nokogiri (~> 1.15) + oj (~> 3.14) omniauth (~> 1.9) omniauth-cas (~> 2.0) omniauth-rails_csrf_protection (~> 0.1) omniauth-saml (~> 1.10) - omniauth_openid_connect (~> 0.6.0) + omniauth_openid_connect (~> 0.6.1) ox (~> 2.14) parslet - pg (~> 1.4) + pg (~> 1.5) pghero - pkg-config (~> 1.5) posix-spawn premailer-rails private_address_check (~> 0.5) - pry-byebug (~> 3.10) - pry-rails (~> 0.3) public_suffix (~> 5.0) - puma (~> 5.6) + puma (~> 6.3) pundit (~> 2.3) - rack (~> 2.2.6) + rack (~> 2.2.7) rack-attack (~> 6.6) - rack-cors (~> 1.1) - rack-test (~> 2.0) + rack-cors (~> 2.0) + rack-test (~> 2.1) rails (~> 6.1.7) rails-controller-testing (~> 1.0) rails-i18n (~> 6.0) - rails-settings-cached (~> 0.6) + rails-settings-cached (~> 0.6)! rdf-normalize (~> 0.5) redcarpet (~> 3.6) redis (~> 4.5) redis-namespace (~> 1.10) - rexml (~> 3.2) - rqrcode (~> 2.1) - rspec-rails (~> 5.1) + rqrcode (~> 2.2) + rspec-rails (~> 6.0) rspec-sidekiq (~> 3.1) - rspec_junit_formatter (~> 0.6) + rspec_chunked (~> 0.6) rubocop + rubocop-capybara rubocop-performance rubocop-rails rubocop-rspec - ruby-progressbar (~> 1.11) + ruby-progressbar (~> 1.13) + rubyzip (~> 2.3) sanitize (~> 6.0) scenic (~> 1.7) sidekiq (~> 6.5) @@ -881,13 +874,19 @@ DEPENDENCIES sprockets-rails (~> 3.4) stackprof stoplight (~> 3.0.1) - strong_migrations (~> 0.7) + strong_migrations (~> 0.8) thor (~> 1.2) tty-prompt (~> 0.23) twitter-text (~> 3.1.0) - tzinfo-data (~> 1.2022) - webauthn (~> 2.5) + tzinfo-data (~> 1.2023) + webauthn (~> 3.0) webmock (~> 3.18) webpacker (~> 5.4) webpush! xorcist (~> 1.1) + +RUBY VERSION + ruby 3.2.2p53 + +BUNDLED WITH + 2.4.13 diff --git a/README.md b/README.md index 256f2d2def..f878752fe3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Mastodon Glitch Edition # +# Mastodon Glitch Edition -> Now with automated deploys! +> Now with automated deploys! [![Build Status](https://img.shields.io/circleci/project/github/glitch-soc/mastodon.svg)][circleci] [![Code Climate](https://img.shields.io/codeclimate/maintainability/glitch-soc/mastodon.svg)][code_climate] diff --git a/SECURITY.md b/SECURITY.md index 234172999d..6a51c126ab 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ If you believe you've identified a security vulnerability in Mastodon (a bug that allows something to happen that shouldn't be possible), you can reach us at . -You should *not* report such issues on GitHub or in other public spaces to give us time to publish a fix for the issue without exposing Mastodon's users to increased risk. +You should _not_ report such issues on GitHub or in other public spaces to give us time to publish a fix for the issue without exposing Mastodon's users to increased risk. ## Scope diff --git a/app/chewy/accounts_index.rb b/app/chewy/accounts_index.rb index e38e14a106..abde8e92f1 100644 --- a/app/chewy/accounts_index.rb +++ b/app/chewy/accounts_index.rb @@ -2,8 +2,37 @@ class AccountsIndex < Chewy::Index settings index: { refresh_interval: '30s' }, analysis: { + filter: { + english_stop: { + type: 'stop', + stopwords: '_english_', + }, + + english_stemmer: { + type: 'stemmer', + language: 'english', + }, + + english_possessive_stemmer: { + type: 'stemmer', + language: 'possessive_english', + }, + }, + analyzer: { - content: { + natural: { + tokenizer: 'uax_url_email', + filter: %w( + english_possessive_stemmer + lowercase + asciifolding + cjk_width + english_stop + english_stemmer + ), + }, + + verbatim: { tokenizer: 'whitespace', filter: %w(lowercase asciifolding cjk_width), }, @@ -26,18 +55,13 @@ class AccountsIndex < Chewy::Index index_scope ::Account.searchable.includes(:account_stat) root date_detection: false do - field :id, type: 'long' - - field :display_name, type: 'text', analyzer: 'content' do - field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content' - end - - field :acct, type: 'text', analyzer: 'content', value: ->(account) { [account.username, account.domain].compact.join('@') } do - field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content' - end - - field :following_count, type: 'long', value: ->(account) { account.following_count } - field :followers_count, type: 'long', value: ->(account) { account.followers_count } - field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at } + field(:id, type: 'long') + field(:following_count, type: 'long') + field(:followers_count, type: 'long') + field(:properties, type: 'keyword', value: ->(account) { account.searchable_properties }) + field(:last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }) + field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' } + field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' } + field(:text, type: 'text', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' } end end diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 1043486140..c4b7e9c9d2 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -8,7 +8,7 @@ class AboutController < ApplicationController before_action :set_instance_presenter def show - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? end private diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 4d03a04b77..929bb54aa7 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -7,8 +7,9 @@ class AccountsController < ApplicationController include AccountControllerConcern include SignatureAuthentication + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } - before_action :set_cache_headers skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) } skip_before_action :require_functional!, unless: :whitelist_mode? @@ -16,7 +17,7 @@ class AccountsController < ApplicationController def show respond_to do |format| format.html do - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in? @rss_url = rss_url end diff --git a/app/controllers/activitypub/base_controller.rb b/app/controllers/activitypub/base_controller.rb index b8a7e0ab96..388d4b9e1d 100644 --- a/app/controllers/activitypub/base_controller.rb +++ b/app/controllers/activitypub/base_controller.rb @@ -7,10 +7,6 @@ class ActivityPub::BaseController < Api::BaseController private - def set_cache_headers - response.headers['Vary'] = 'Signature' if authorized_fetch_mode? - end - def skip_temporary_suspension_response? false end diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index 23d8740711..4ed59388ff 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -4,11 +4,12 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController include SignatureVerification include AccountOwnedConcern + vary_by -> { 'Signature' if authorized_fetch_mode? } + before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :set_items before_action :set_size before_action :set_type - before_action :set_cache_headers def show expires_in 3.minutes, public: public_fetch_mode? diff --git a/app/controllers/activitypub/followers_synchronizations_controller.rb b/app/controllers/activitypub/followers_synchronizations_controller.rb index 4e445bcb1f..976caa3445 100644 --- a/app/controllers/activitypub/followers_synchronizations_controller.rb +++ b/app/controllers/activitypub/followers_synchronizations_controller.rb @@ -4,9 +4,10 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro include SignatureVerification include AccountOwnedConcern + vary_by -> { 'Signature' if authorized_fetch_mode? } + before_action :require_account_signature! before_action :set_items - before_action :set_cache_headers def show expires_in 0, public: false diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index 60d201f763..bf10ba762a 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -6,9 +6,10 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController include SignatureVerification include AccountOwnedConcern + vary_by -> { 'Signature' if authorized_fetch_mode? || page_requested? } + before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :set_statuses - before_action :set_cache_headers def show if page_requested? @@ -16,6 +17,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController else expires_in(3.minutes, public: public_fetch_mode?) end + render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end @@ -80,8 +82,4 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController def set_account @account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative end - - def set_cache_headers - response.headers['Vary'] = 'Signature' if authorized_fetch_mode? || page_requested? - end end diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb index 8e0f9de2ee..c38ff89d1c 100644 --- a/app/controllers/activitypub/replies_controller.rb +++ b/app/controllers/activitypub/replies_controller.rb @@ -7,9 +7,10 @@ class ActivityPub::RepliesController < ActivityPub::BaseController DESCENDANTS_LIMIT = 60 + vary_by -> { 'Signature' if authorized_fetch_mode? } + before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :set_status - before_action :set_cache_headers before_action :set_replies def index diff --git a/app/controllers/admin/announcements_controller.rb b/app/controllers/admin/announcements_controller.rb index 351b9a9910..8f9708183a 100644 --- a/app/controllers/admin/announcements_controller.rb +++ b/app/controllers/admin/announcements_controller.rb @@ -14,6 +14,10 @@ class Admin::AnnouncementsController < Admin::BaseController @announcement = Announcement.new end + def edit + authorize :announcement, :update? + end + def create authorize :announcement, :create? @@ -28,10 +32,6 @@ class Admin::AnnouncementsController < Admin::BaseController end end - def edit - authorize :announcement, :update? - end - def update authorize :announcement, :update? diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index c645ce12bb..a71bb61298 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -9,6 +9,8 @@ module Admin before_action :set_pack before_action :set_body_classes + before_action :set_cache_headers + after_action :verify_authorized private @@ -21,6 +23,10 @@ module Admin use_pack 'admin' end + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end + def set_user @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) end diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index 924b623ad8..3a6df662ea 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -14,17 +14,5 @@ module Admin @pending_tags_count = Tag.pending_review.count @pending_appeals_count = Appeal.pending.count end - - private - - def redis_info - @redis_info ||= begin - if redis.is_a?(Redis::Namespace) - redis.redis.info - else - redis.info - end - end - end end end diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 74764640b8..b9691c5a3a 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -2,7 +2,7 @@ module Admin class DomainBlocksController < BaseController - before_action :set_domain_block, only: [:show, :destroy, :edit, :update] + before_action :set_domain_block, only: [:destroy, :edit, :update] def batch authorize :domain_block, :create? @@ -31,31 +31,41 @@ module Admin @domain_block = DomainBlock.new(resource_params) existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil + # Disallow accidentally downgrading a domain block if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block) @domain_block.save - flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe # rubocop:disable Rails/OutputSafety + flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe @domain_block.errors.delete(:domain) - render :new - else - if existing_domain_block.present? - @domain_block = existing_domain_block - @domain_block.update(resource_params) - end + return render :new + end - if @domain_block.save - DomainBlockWorker.perform_async(@domain_block.id) - log_action :create, @domain_block - redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') - else - render :new - end + # Allow transparently upgrading a domain block + if existing_domain_block.present? + @domain_block = existing_domain_block + @domain_block.assign_attributes(resource_params) + end + + # Require explicit confirmation when suspending + return render :confirm_suspension if requires_confirmation? + + if @domain_block.save + DomainBlockWorker.perform_async(@domain_block.id) + log_action :create, @domain_block + redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') + else + render :new end end def update authorize :domain_block, :update? - if @domain_block.update(update_params) + @domain_block.assign_attributes(update_params) + + # Require explicit confirmation when suspending + return render :confirm_suspension if requires_confirmation? + + if @domain_block.save DomainBlockWorker.perform_async(@domain_block.id, @domain_block.severity_previously_changed?) log_action :update, @domain_block redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') @@ -90,9 +100,11 @@ module Admin end def action_from_button - if params[:save] - 'save' - end + 'save' if params[:save] + end + + def requires_confirmation? + @domain_block.valid? && (@domain_block.new_record? || @domain_block.severity_changed?) && @domain_block.severity.to_s == 'suspend' && !params[:confirm] end end end diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb index a0a43de192..4a3228ec30 100644 --- a/app/controllers/admin/email_domain_blocks_controller.rb +++ b/app/controllers/admin/email_domain_blocks_controller.rb @@ -2,8 +2,6 @@ module Admin class EmailDomainBlocksController < BaseController - before_action :set_email_domain_block, only: [:show, :destroy] - def index authorize :email_domain_block, :index? @@ -59,10 +57,6 @@ module Admin private - def set_email_domain_block - @email_domain_block = EmailDomainBlock.find(params[:id]) - end - def set_resolved_records Resolv::DNS.open do |dns| dns.timeouts = 5 diff --git a/app/controllers/admin/roles_controller.rb b/app/controllers/admin/roles_controller.rb index d76aa745bd..bcfc11159c 100644 --- a/app/controllers/admin/roles_controller.rb +++ b/app/controllers/admin/roles_controller.rb @@ -16,6 +16,10 @@ module Admin @role = UserRole.new end + def edit + authorize @role, :update? + end + def create authorize :user_role, :create? @@ -30,10 +34,6 @@ module Admin end end - def edit - authorize @role, :update? - end - def update authorize @role, :update? diff --git a/app/controllers/admin/rules_controller.rb b/app/controllers/admin/rules_controller.rb index f3bed3ad8e..d31aec6ea8 100644 --- a/app/controllers/admin/rules_controller.rb +++ b/app/controllers/admin/rules_controller.rb @@ -11,6 +11,10 @@ module Admin @rule = Rule.new end + def edit + authorize @rule, :update? + end + def create authorize :rule, :create? @@ -24,10 +28,6 @@ module Admin end end - def edit - authorize @rule, :update? - end - def update authorize @rule, :update? diff --git a/app/controllers/admin/warning_presets_controller.rb b/app/controllers/admin/warning_presets_controller.rb index b376f8d9b1..efbf65b119 100644 --- a/app/controllers/admin/warning_presets_controller.rb +++ b/app/controllers/admin/warning_presets_controller.rb @@ -11,6 +11,10 @@ module Admin @warning_preset = AccountWarningPreset.new end + def edit + authorize @warning_preset, :update? + end + def create authorize :account_warning_preset, :create? @@ -24,10 +28,6 @@ module Admin end end - def edit - authorize @warning_preset, :update? - end - def update authorize @warning_preset, :update? diff --git a/app/controllers/admin/webhooks_controller.rb b/app/controllers/admin/webhooks_controller.rb index d6fb1a4eaf..f1aad7c4b5 100644 --- a/app/controllers/admin/webhooks_controller.rb +++ b/app/controllers/admin/webhooks_controller.rb @@ -10,16 +10,25 @@ module Admin @webhooks = Webhook.page(params[:page]) end + def show + authorize @webhook, :show? + end + def new authorize :webhook, :create? @webhook = Webhook.new end + def edit + authorize @webhook, :update? + end + def create authorize :webhook, :create? @webhook = Webhook.new(resource_params) + @webhook.current_account = current_account if @webhook.save redirect_to admin_webhook_path(@webhook) @@ -28,21 +37,15 @@ module Admin end end - def show - authorize @webhook, :show? - end - - def edit - authorize @webhook, :update? - end - def update authorize @webhook, :update? + @webhook.current_account = current_account + if @webhook.update(resource_params) redirect_to admin_webhook_path(@webhook) else - render :show + render :edit end end @@ -71,7 +74,7 @@ module Admin end def resource_params - params.require(:webhook).permit(:url, events: []) + params.require(:webhook).permit(:url, :template, events: []) end end end diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 41f3ce2ee3..2629ab782f 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -6,13 +6,14 @@ class Api::BaseController < ApplicationController include RateLimitHeaders include AccessTokenTrackingConcern + include ApiCachingConcern - skip_before_action :store_current_location skip_before_action :require_functional!, unless: :whitelist_mode? before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access? before_action :require_not_suspended! - before_action :set_cache_headers + + vary_by 'Authorization' protect_from_forgery with: :null_session @@ -148,10 +149,6 @@ class Api::BaseController < ApplicationController doorkeeper_authorize!(*scopes) if doorkeeper_token end - def set_cache_headers - response.headers['Cache-Control'] = 'private, no-store' - end - def disallow_unauthenticated_api_access? ENV['DISALLOW_UNAUTHENTICATED_API_ACCESS'] == 'true' || Rails.configuration.x.whitelist_mode end diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index 94b707771f..7c7d70fd32 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -13,7 +13,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController def update @account = current_account UpdateAccountService.new.call(@account, account_params, raise_error: true) - UserSettingsDecorator.new(current_user).update(user_settings_params) if user_settings_params + current_user.update(user_params) if user_params ActivityPub::UpdateDistributionWorker.perform_async(@account.id) render json: @account, serializer: REST::CredentialAccountSerializer end @@ -34,15 +34,17 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController ) end - def user_settings_params + def user_params return nil if params[:source].blank? source_params = params.require(:source) { - 'setting_default_privacy' => source_params.fetch(:privacy, @account.user.setting_default_privacy), - 'setting_default_sensitive' => source_params.fetch(:sensitive, @account.user.setting_default_sensitive), - 'setting_default_language' => source_params.fetch(:language, @account.user.setting_default_language), + settings_attributes: { + default_privacy: source_params.fetch(:privacy, @account.user.setting_default_privacy), + default_sensitive: source_params.fetch(:sensitive, @account.user.setting_default_sensitive), + default_language: source_params.fetch(:language, @account.user.setting_default_language), + }, } end end diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index b61de13b91..1a996d362a 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -6,6 +6,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController after_action :insert_pagination_headers def index + cache_if_unauthenticated! @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer end @@ -45,15 +46,11 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController end def next_path - if records_continue? - api_v1_account_followers_url pagination_params(max_id: pagination_max_id) - end + api_v1_account_followers_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless @accounts.empty? - api_v1_account_followers_url pagination_params(since_id: pagination_since_id) - end + api_v1_account_followers_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index 37d3c2d783..6e6ebae43b 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -6,6 +6,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController after_action :insert_pagination_headers def index + cache_if_unauthenticated! @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer end @@ -45,15 +46,11 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController end def next_path - if records_continue? - api_v1_account_following_index_url pagination_params(max_id: pagination_max_id) - end + api_v1_account_following_index_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless @accounts.empty? - api_v1_account_following_index_url pagination_params(since_id: pagination_since_id) - end + api_v1_account_following_index_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/accounts/lookup_controller.rb b/app/controllers/api/v1/accounts/lookup_controller.rb index 8597f891d6..6d63398781 100644 --- a/app/controllers/api/v1/accounts/lookup_controller.rb +++ b/app/controllers/api/v1/accounts/lookup_controller.rb @@ -5,6 +5,7 @@ class Api::V1::Accounts::LookupController < Api::BaseController before_action :set_account def show + cache_if_unauthenticated! render json: @account, serializer: REST::AccountSerializer end diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 38c9f5a20d..51f541bd23 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -7,6 +7,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) } def index + cache_if_unauthenticated! @statuses = load_statuses render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end @@ -39,15 +40,11 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def next_path - if records_continue? - api_v1_account_statuses_url pagination_params(max_id: pagination_max_id) - end + api_v1_account_statuses_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless @statuses.empty? - api_v1_account_statuses_url pagination_params(min_id: pagination_since_id) - end + api_v1_account_statuses_url pagination_params(min_id: pagination_since_id) unless @statuses.empty? end def records_continue? diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index be84720aa9..ddb94d5ca4 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -18,6 +18,7 @@ class Api::V1::AccountsController < Api::BaseController override_rate_limit_headers :follow, family: :follows def show + cache_if_unauthenticated! render json: @account, serializer: REST::AccountSerializer end @@ -30,7 +31,7 @@ class Api::V1::AccountsController < Api::BaseController self.response_body = Oj.dump(response.body) self.status = response.status rescue ActiveRecord::RecordInvalid => e - render json: ValidationErrorFormatter.new(e, 'account.username': :username, 'invite_request.text': :reason).as_json, status: :unprocessable_entity + render json: ValidationErrorFormatter.new(e, 'account.username': :username, 'invite_request.text': :reason).as_json, status: 422 end def follow @@ -89,7 +90,7 @@ class Api::V1::AccountsController < Api::BaseController end def account_params - params.permit(:username, :email, :password, :agreement, :locale, :reason) + params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone) end def check_enabled_registrations diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb index f483000728..ff9cae6398 100644 --- a/app/controllers/api/v1/admin/accounts_controller.rb +++ b/app/controllers/api/v1/admin/accounts_controller.rb @@ -120,9 +120,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController translated_params[:status] = status.to_s if params[status].present? end - if params[:staff].present? - translated_params[:role_ids] = UserRole.that_can(:manage_reports).map(&:id) - end + translated_params[:role_ids] = UserRole.that_can(:manage_reports).map(&:id) if params[:staff].present? translated_params end diff --git a/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb b/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb index 9ef1b3be71..7b192b979f 100644 --- a/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb +++ b/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb @@ -58,7 +58,7 @@ class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController end def set_canonical_email_blocks_from_test - @canonical_email_blocks = CanonicalEmailBlock.matching_email(params[:email]) + @canonical_email_blocks = CanonicalEmailBlock.matching_email(params.require(:email)) end def set_canonical_email_block diff --git a/app/controllers/api/v1/admin/domain_allows_controller.rb b/app/controllers/api/v1/admin/domain_allows_controller.rb index 0658199f0f..dd54d67106 100644 --- a/app/controllers/api/v1/admin/domain_allows_controller.rb +++ b/app/controllers/api/v1/admin/domain_allows_controller.rb @@ -16,19 +16,6 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController PAGINATION_PARAMS = %i(limit).freeze - def create - authorize :domain_allow, :create? - - @domain_allow = DomainAllow.find_by(resource_params) - - if @domain_allow.nil? - @domain_allow = DomainAllow.create!(resource_params) - log_action :create, @domain_allow - end - - render json: @domain_allow, serializer: REST::Admin::DomainAllowSerializer - end - def index authorize :domain_allow, :index? render json: @domain_allows, each_serializer: REST::Admin::DomainAllowSerializer @@ -39,6 +26,19 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController render json: @domain_allow, serializer: REST::Admin::DomainAllowSerializer end + def create + authorize :domain_allow, :create? + + @domain_allow = DomainAllow.find_by(domain: resource_params[:domain]) + + if @domain_allow.nil? + @domain_allow = DomainAllow.create!(resource_params) + log_action :create, @domain_allow + end + + render json: @domain_allow, serializer: REST::Admin::DomainAllowSerializer + end + def destroy authorize @domain_allow, :destroy? UnallowDomainService.new.call(@domain_allow) diff --git a/app/controllers/api/v1/admin/domain_blocks_controller.rb b/app/controllers/api/v1/admin/domain_blocks_controller.rb index 8b77e9717d..2538c7c7c2 100644 --- a/app/controllers/api/v1/admin/domain_blocks_controller.rb +++ b/app/controllers/api/v1/admin/domain_blocks_controller.rb @@ -16,6 +16,16 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController PAGINATION_PARAMS = %i(limit).freeze + def index + authorize :domain_block, :index? + render json: @domain_blocks, each_serializer: REST::Admin::DomainBlockSerializer + end + + def show + authorize @domain_block, :show? + render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer + end + def create authorize :domain_block, :create? @@ -28,16 +38,6 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer end - def index - authorize :domain_block, :index? - render json: @domain_blocks, each_serializer: REST::Admin::DomainBlockSerializer - end - - def show - authorize @domain_block, :show? - render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer - end - def update authorize @domain_block, :update? @domain_block.update!(domain_block_params) diff --git a/app/controllers/api/v1/admin/email_domain_blocks_controller.rb b/app/controllers/api/v1/admin/email_domain_blocks_controller.rb index e53d0b1573..850eda6224 100644 --- a/app/controllers/api/v1/admin/email_domain_blocks_controller.rb +++ b/app/controllers/api/v1/admin/email_domain_blocks_controller.rb @@ -18,15 +18,6 @@ class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController limit ).freeze - def create - authorize :email_domain_block, :create? - - @email_domain_block = EmailDomainBlock.create!(resource_params) - log_action :create, @email_domain_block - - render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer - end - def index authorize :email_domain_block, :index? render json: @email_domain_blocks, each_serializer: REST::Admin::EmailDomainBlockSerializer @@ -37,6 +28,15 @@ class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer end + def create + authorize :email_domain_block, :create? + + @email_domain_block = EmailDomainBlock.create!(resource_params) + log_action :create, @email_domain_block + + render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer + end + def destroy authorize @email_domain_block, :destroy? @email_domain_block.destroy! diff --git a/app/controllers/api/v1/admin/ip_blocks_controller.rb b/app/controllers/api/v1/admin/ip_blocks_controller.rb index 201ab6b1ff..61c1912344 100644 --- a/app/controllers/api/v1/admin/ip_blocks_controller.rb +++ b/app/controllers/api/v1/admin/ip_blocks_controller.rb @@ -18,13 +18,6 @@ class Api::V1::Admin::IpBlocksController < Api::BaseController limit ).freeze - def create - authorize :ip_block, :create? - @ip_block = IpBlock.create!(resource_params) - log_action :create, @ip_block - render json: @ip_block, serializer: REST::Admin::IpBlockSerializer - end - def index authorize :ip_block, :index? render json: @ip_blocks, each_serializer: REST::Admin::IpBlockSerializer @@ -35,6 +28,13 @@ class Api::V1::Admin::IpBlocksController < Api::BaseController render json: @ip_block, serializer: REST::Admin::IpBlockSerializer end + def create + authorize :ip_block, :create? + @ip_block = IpBlock.create!(resource_params) + log_action :create, @ip_block + render json: @ip_block, serializer: REST::Admin::IpBlockSerializer + end + def update authorize @ip_block, :update? @ip_block.update(resource_params) diff --git a/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb b/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb new file mode 100644 index 0000000000..5d9fcc82c0 --- /dev/null +++ b/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +class Api::V1::Admin::Trends::Links::PreviewCardProvidersController < Api::BaseController + include Authorization + + LIMIT = 100 + + before_action -> { authorize_if_got_token! :'admin:read' }, only: :index + before_action -> { authorize_if_got_token! :'admin:write' }, except: :index + before_action :set_providers, only: :index + + after_action :verify_authorized + after_action :insert_pagination_headers, only: :index + + PAGINATION_PARAMS = %i(limit).freeze + + def index + authorize :preview_card_provider, :index? + + render json: @providers, each_serializer: REST::Admin::Trends::Links::PreviewCardProviderSerializer + end + + def approve + authorize :preview_card_provider, :review? + + provider = PreviewCardProvider.find(params[:id]) + provider.update(trendable: true, reviewed_at: Time.now.utc) + render json: provider, serializer: REST::Admin::Trends::Links::PreviewCardProviderSerializer + end + + def reject + authorize :preview_card_provider, :review? + + provider = PreviewCardProvider.find(params[:id]) + provider.update(trendable: false, reviewed_at: Time.now.utc) + render json: provider, serializer: REST::Admin::Trends::Links::PreviewCardProviderSerializer + end + + private + + def set_providers + @providers = PreviewCardProvider.all.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_admin_trends_links_preview_card_providers_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v1_admin_trends_links_preview_card_providers_url(pagination_params(min_id: pagination_since_id)) unless @providers.empty? + end + + def pagination_max_id + @providers.last.id + end + + def pagination_since_id + @providers.first.id + end + + def records_continue? + @providers.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end +end diff --git a/app/controllers/api/v1/admin/trends/links_controller.rb b/app/controllers/api/v1/admin/trends/links_controller.rb index cc63889806..7f4ca48288 100644 --- a/app/controllers/api/v1/admin/trends/links_controller.rb +++ b/app/controllers/api/v1/admin/trends/links_controller.rb @@ -1,7 +1,36 @@ # frozen_string_literal: true class Api::V1::Admin::Trends::LinksController < Api::V1::Trends::LinksController - before_action -> { authorize_if_got_token! :'admin:read' } + include Authorization + + before_action -> { authorize_if_got_token! :'admin:read' }, only: :index + before_action -> { authorize_if_got_token! :'admin:write' }, except: :index + + after_action :verify_authorized, except: :index + + def index + if current_user&.can?(:manage_taxonomies) + render json: @links, each_serializer: REST::Admin::Trends::LinkSerializer + else + super + end + end + + def approve + authorize :preview_card, :review? + + link = PreviewCard.find(params[:id]) + link.update(trendable: true) + render json: link, serializer: REST::Admin::Trends::LinkSerializer + end + + def reject + authorize :preview_card, :review? + + link = PreviewCard.find(params[:id]) + link.update(trendable: false) + render json: link, serializer: REST::Admin::Trends::LinkSerializer + end private diff --git a/app/controllers/api/v1/admin/trends/statuses_controller.rb b/app/controllers/api/v1/admin/trends/statuses_controller.rb index c39f77363c..34b6580df1 100644 --- a/app/controllers/api/v1/admin/trends/statuses_controller.rb +++ b/app/controllers/api/v1/admin/trends/statuses_controller.rb @@ -1,7 +1,36 @@ # frozen_string_literal: true class Api::V1::Admin::Trends::StatusesController < Api::V1::Trends::StatusesController - before_action -> { authorize_if_got_token! :'admin:read' } + include Authorization + + before_action -> { authorize_if_got_token! :'admin:read' }, only: :index + before_action -> { authorize_if_got_token! :'admin:write' }, except: :index + + after_action :verify_authorized, except: :index + + def index + if current_user&.can?(:manage_taxonomies) + render json: @statuses, each_serializer: REST::Admin::Trends::StatusSerializer + else + super + end + end + + def approve + authorize [:admin, :status], :review? + + status = Status.find(params[:id]) + status.update(trendable: true) + render json: status, serializer: REST::Admin::Trends::StatusSerializer + end + + def reject + authorize [:admin, :status], :review? + + status = Status.find(params[:id]) + status.update(trendable: false) + render json: status, serializer: REST::Admin::Trends::StatusSerializer + end private diff --git a/app/controllers/api/v1/admin/trends/tags_controller.rb b/app/controllers/api/v1/admin/trends/tags_controller.rb index e77df30216..2eeea95225 100644 --- a/app/controllers/api/v1/admin/trends/tags_controller.rb +++ b/app/controllers/api/v1/admin/trends/tags_controller.rb @@ -1,7 +1,12 @@ # frozen_string_literal: true class Api::V1::Admin::Trends::TagsController < Api::V1::Trends::TagsController - before_action -> { authorize_if_got_token! :'admin:read' } + include Authorization + + before_action -> { authorize_if_got_token! :'admin:read' }, only: :index + before_action -> { authorize_if_got_token! :'admin:write' }, except: :index + + after_action :verify_authorized, except: :index def index if current_user&.can?(:manage_taxonomies) @@ -11,6 +16,22 @@ class Api::V1::Admin::Trends::TagsController < Api::V1::Trends::TagsController end end + def approve + authorize :tag, :review? + + tag = Tag.find(params[:id]) + tag.update(trendable: true, reviewed_at: Time.now.utc) + render json: tag, serializer: REST::Admin::TagSerializer + end + + def reject + authorize :tag, :review? + + tag = Tag.find(params[:id]) + tag.update(trendable: false, reviewed_at: Time.now.utc) + render json: tag, serializer: REST::Admin::TagSerializer + end + private def enabled? diff --git a/app/controllers/api/v1/announcements_controller.rb b/app/controllers/api/v1/announcements_controller.rb index ee79fc19f1..82e9cf7de4 100644 --- a/app/controllers/api/v1/announcements_controller.rb +++ b/app/controllers/api/v1/announcements_controller.rb @@ -18,9 +18,7 @@ class Api::V1::AnnouncementsController < Api::BaseController private def set_announcements - @announcements = begin - Announcement.published.chronological - end + @announcements = Announcement.published.chronological end def set_announcement diff --git a/app/controllers/api/v1/blocks_controller.rb b/app/controllers/api/v1/blocks_controller.rb index a65e762c9f..06a8bfa891 100644 --- a/app/controllers/api/v1/blocks_controller.rb +++ b/app/controllers/api/v1/blocks_controller.rb @@ -33,15 +33,11 @@ class Api::V1::BlocksController < Api::BaseController end def next_path - if records_continue? - api_v1_blocks_url pagination_params(max_id: pagination_max_id) - end + api_v1_blocks_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless paginated_blocks.empty? - api_v1_blocks_url pagination_params(since_id: pagination_since_id) - end + api_v1_blocks_url pagination_params(since_id: pagination_since_id) unless paginated_blocks.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/conversations_controller.rb b/app/controllers/api/v1/conversations_controller.rb index 6c75834037..b3ca2f7903 100644 --- a/app/controllers/api/v1/conversations_controller.rb +++ b/app/controllers/api/v1/conversations_controller.rb @@ -11,7 +11,7 @@ class Api::V1::ConversationsController < Api::BaseController def index @conversations = paginated_conversations - render json: @conversations, each_serializer: REST::ConversationSerializer + render json: @conversations, each_serializer: REST::ConversationSerializer, relationships: StatusRelationshipsPresenter.new(@conversations.map(&:last_status), current_user&.account_id) end def read @@ -19,6 +19,11 @@ class Api::V1::ConversationsController < Api::BaseController render json: @conversation, serializer: REST::ConversationSerializer end + def unread + @conversation.update!(unread: true) + render json: @conversation, serializer: REST::ConversationSerializer + end + def destroy @conversation.destroy! render_empty @@ -32,6 +37,19 @@ class Api::V1::ConversationsController < Api::BaseController def paginated_conversations AccountConversation.where(account: current_account) + .includes( + account: :account_stat, + last_status: [ + :media_attachments, + :preview_cards, + :status_stat, + :tags, + { + active_mentions: [account: :account_stat], + account: :account_stat, + }, + ] + ) .to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) end @@ -40,15 +58,11 @@ class Api::V1::ConversationsController < Api::BaseController end def next_path - if records_continue? - api_v1_conversations_url pagination_params(max_id: pagination_max_id) - end + api_v1_conversations_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless @conversations.empty? - api_v1_conversations_url pagination_params(min_id: pagination_since_id) - end + api_v1_conversations_url pagination_params(min_id: pagination_since_id) unless @conversations.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/custom_emojis_controller.rb b/app/controllers/api/v1/custom_emojis_controller.rb index 08b3474cc8..76bc2b18ad 100644 --- a/app/controllers/api/v1/custom_emojis_controller.rb +++ b/app/controllers/api/v1/custom_emojis_controller.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true class Api::V1::CustomEmojisController < Api::BaseController - skip_before_action :set_cache_headers + vary_by '', unless: :disallow_unauthenticated_api_access? def index - expires_in 3.minutes, public: true + cache_even_if_authenticated! unless disallow_unauthenticated_api_access? render_with_cache(each_serializer: REST::CustomEmojiSerializer) { CustomEmoji.listed.includes(:category) } end end diff --git a/app/controllers/api/v1/directories_controller.rb b/app/controllers/api/v1/directories_controller.rb index c91543e3a3..1109435507 100644 --- a/app/controllers/api/v1/directories_controller.rb +++ b/app/controllers/api/v1/directories_controller.rb @@ -5,6 +5,7 @@ class Api::V1::DirectoriesController < Api::BaseController before_action :set_accounts def show + cache_if_unauthenticated! render json: @accounts, each_serializer: REST::AccountSerializer end @@ -20,11 +21,35 @@ class Api::V1::DirectoriesController < Api::BaseController def accounts_scope Account.discoverable.tap do |scope| - scope.merge!(Account.local) if truthy_param?(:local) - scope.merge!(Account.by_recent_status) if params[:order].blank? || params[:order] == 'active' - scope.merge!(Account.order(id: :desc)) if params[:order] == 'new' - scope.merge!(Account.not_excluded_by_account(current_account)) if current_account - scope.merge!(Account.not_domain_blocked_by_account(current_account)) if current_account && !truthy_param?(:local) + scope.merge!(account_order_scope) + scope.merge!(local_account_scope) if local_accounts? + scope.merge!(account_exclusion_scope) if current_account + scope.merge!(account_domain_block_scope) if current_account && !local_accounts? end end + + def local_accounts? + truthy_param?(:local) + end + + def account_order_scope + case params[:order] + when 'new' + Account.order(id: :desc) + when 'active', nil + Account.by_recent_status + end + end + + def local_account_scope + Account.local + end + + def account_exclusion_scope + Account.not_excluded_by_account(current_account) + end + + def account_domain_block_scope + Account.not_domain_blocked_by_account(current_account) + end end diff --git a/app/controllers/api/v1/domain_blocks_controller.rb b/app/controllers/api/v1/domain_blocks_controller.rb index 1891261b9c..34def3c44a 100644 --- a/app/controllers/api/v1/domain_blocks_controller.rb +++ b/app/controllers/api/v1/domain_blocks_controller.rb @@ -43,15 +43,11 @@ class Api::V1::DomainBlocksController < Api::BaseController end def next_path - if records_continue? - api_v1_domain_blocks_url pagination_params(max_id: pagination_max_id) - end + api_v1_domain_blocks_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless @blocks.empty? - api_v1_domain_blocks_url pagination_params(since_id: pagination_since_id) - end + api_v1_domain_blocks_url pagination_params(since_id: pagination_since_id) unless @blocks.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/emails/confirmations_controller.rb b/app/controllers/api/v1/emails/confirmations_controller.rb index 3faaea2fb7..16e91b4497 100644 --- a/app/controllers/api/v1/emails/confirmations_controller.rb +++ b/app/controllers/api/v1/emails/confirmations_controller.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true class Api::V1::Emails::ConfirmationsController < Api::BaseController - before_action -> { doorkeeper_authorize! :write, :'write:accounts' } - before_action :require_user_owned_by_application! - before_action :require_user_not_confirmed! + before_action -> { authorize_if_got_token! :read, :'read:accounts' }, only: :check + before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :check + before_action :require_user_owned_by_application!, except: :check + before_action :require_user_not_confirmed!, except: :check + before_action :require_authenticated_user!, only: :check def create current_user.update!(email: params[:email]) if params.key?(:email) @@ -12,13 +14,17 @@ class Api::V1::Emails::ConfirmationsController < Api::BaseController render_empty end + def check + render json: current_user.confirmed? + end + private def require_user_owned_by_application! - render json: { error: 'This method is only available to the application the user originally signed-up with' }, status: :forbidden unless current_user && current_user.created_by_application_id == doorkeeper_token.application_id + render json: { error: 'This method is only available to the application the user originally signed-up with' }, status: 403 unless current_user && current_user.created_by_application_id == doorkeeper_token.application_id end def require_user_not_confirmed! - render json: { error: 'This method is only available while the e-mail is awaiting confirmation' }, status: :forbidden unless !current_user.confirmed? || current_user.unconfirmed_email.present? + render json: { error: 'This method is only available while the e-mail is awaiting confirmation' }, status: 403 unless !current_user.confirmed? || current_user.unconfirmed_email.present? end end diff --git a/app/controllers/api/v1/endorsements_controller.rb b/app/controllers/api/v1/endorsements_controller.rb index 9e80f468a7..46e3fcd647 100644 --- a/app/controllers/api/v1/endorsements_controller.rb +++ b/app/controllers/api/v1/endorsements_controller.rb @@ -35,17 +35,13 @@ class Api::V1::EndorsementsController < Api::BaseController def next_path return if unlimited? - if records_continue? - api_v1_endorsements_url pagination_params(max_id: pagination_max_id) - end + api_v1_endorsements_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path return if unlimited? - unless @accounts.empty? - api_v1_endorsements_url pagination_params(since_id: pagination_since_id) - end + api_v1_endorsements_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb index 2a873696c0..bd7f3d775e 100644 --- a/app/controllers/api/v1/favourites_controller.rb +++ b/app/controllers/api/v1/favourites_controller.rb @@ -36,15 +36,11 @@ class Api::V1::FavouritesController < Api::BaseController end def next_path - if records_continue? - api_v1_favourites_url pagination_params(max_id: pagination_max_id) - end + api_v1_favourites_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless results.empty? - api_v1_favourites_url pagination_params(min_id: pagination_since_id) - end + api_v1_favourites_url pagination_params(min_id: pagination_since_id) unless results.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/featured_tags_controller.rb b/app/controllers/api/v1/featured_tags_controller.rb index edb42a94ea..516046f009 100644 --- a/app/controllers/api/v1/featured_tags_controller.rb +++ b/app/controllers/api/v1/featured_tags_controller.rb @@ -13,7 +13,7 @@ class Api::V1::FeaturedTagsController < Api::BaseController end def create - featured_tag = CreateFeaturedTagService.new.call(current_account, featured_tag_params[:name]) + featured_tag = CreateFeaturedTagService.new.call(current_account, params.require(:name)) render json: featured_tag, serializer: REST::FeaturedTagSerializer end @@ -31,8 +31,4 @@ class Api::V1::FeaturedTagsController < Api::BaseController def set_featured_tags @featured_tags = current_account.featured_tags.order(statuses_count: :desc) end - - def featured_tag_params - params.permit(:name) - end end diff --git a/app/controllers/api/v1/filters_controller.rb b/app/controllers/api/v1/filters_controller.rb index 772791b255..ed98acce30 100644 --- a/app/controllers/api/v1/filters_controller.rb +++ b/app/controllers/api/v1/filters_controller.rb @@ -11,6 +11,10 @@ class Api::V1::FiltersController < Api::BaseController render json: @filters, each_serializer: REST::V1::FilterSerializer end + def show + render json: @filter, serializer: REST::V1::FilterSerializer + end + def create ApplicationRecord.transaction do filter_category = current_account.custom_filters.create!(filter_params) @@ -20,10 +24,6 @@ class Api::V1::FiltersController < Api::BaseController render json: @filter, serializer: REST::V1::FilterSerializer end - def show - render json: @filter, serializer: REST::V1::FilterSerializer - end - def update ApplicationRecord.transaction do @filter.update!(keyword_params) diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb index 54ff0e11d0..7c197ce6ba 100644 --- a/app/controllers/api/v1/follow_requests_controller.rb +++ b/app/controllers/api/v1/follow_requests_controller.rb @@ -53,15 +53,11 @@ class Api::V1::FollowRequestsController < Api::BaseController end def next_path - if records_continue? - api_v1_follow_requests_url pagination_params(max_id: pagination_max_id) - end + api_v1_follow_requests_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless @accounts.empty? - api_v1_follow_requests_url pagination_params(since_id: pagination_since_id) - end + api_v1_follow_requests_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/instances/activity_controller.rb b/app/controllers/api/v1/instances/activity_controller.rb index bad61425a5..3d55d990af 100644 --- a/app/controllers/api/v1/instances/activity_controller.rb +++ b/app/controllers/api/v1/instances/activity_controller.rb @@ -3,11 +3,12 @@ class Api::V1::Instances::ActivityController < Api::BaseController before_action :require_enabled_api! - skip_before_action :set_cache_headers skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + vary_by '' + def show - expires_in 1.day, public: true + cache_even_if_authenticated! render_with_cache json: :activity, expires_in: 1.day end diff --git a/app/controllers/api/v1/instances/domain_blocks_controller.rb b/app/controllers/api/v1/instances/domain_blocks_controller.rb index 37a6906fb6..e954c45897 100644 --- a/app/controllers/api/v1/instances/domain_blocks_controller.rb +++ b/app/controllers/api/v1/instances/domain_blocks_controller.rb @@ -6,8 +6,15 @@ class Api::V1::Instances::DomainBlocksController < Api::BaseController before_action :require_enabled_api! before_action :set_domain_blocks + vary_by '', if: -> { Setting.show_domain_blocks == 'all' } + def index - expires_in 3.minutes, public: true + if Setting.show_domain_blocks == 'all' + cache_even_if_authenticated! + else + cache_if_unauthenticated! + end + render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: (Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?)) end diff --git a/app/controllers/api/v1/instances/extended_descriptions_controller.rb b/app/controllers/api/v1/instances/extended_descriptions_controller.rb index c72e16cff2..a0665725bd 100644 --- a/app/controllers/api/v1/instances/extended_descriptions_controller.rb +++ b/app/controllers/api/v1/instances/extended_descriptions_controller.rb @@ -2,11 +2,19 @@ class Api::V1::Instances::ExtendedDescriptionsController < Api::BaseController skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_around_action :set_locale before_action :set_extended_description + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in whitelist mode + def current_user + super if whitelist_mode? + end + def show - expires_in 3.minutes, public: true + cache_even_if_authenticated! render json: @extended_description, serializer: REST::ExtendedDescriptionSerializer end diff --git a/app/controllers/api/v1/instances/peers_controller.rb b/app/controllers/api/v1/instances/peers_controller.rb index 2877fec52d..70281362a8 100644 --- a/app/controllers/api/v1/instances/peers_controller.rb +++ b/app/controllers/api/v1/instances/peers_controller.rb @@ -3,11 +3,18 @@ class Api::V1::Instances::PeersController < Api::BaseController before_action :require_enabled_api! - skip_before_action :set_cache_headers skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_around_action :set_locale + + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in whitelist mode + def current_user + super if whitelist_mode? + end def index - expires_in 1.day, public: true + cache_even_if_authenticated! render_with_cache(expires_in: 1.day) { Instance.where.not(domain: DomainBlock.select(:domain)).pluck(:domain) } end diff --git a/app/controllers/api/v1/instances/privacy_policies_controller.rb b/app/controllers/api/v1/instances/privacy_policies_controller.rb index dbd69f54d4..36889f7335 100644 --- a/app/controllers/api/v1/instances/privacy_policies_controller.rb +++ b/app/controllers/api/v1/instances/privacy_policies_controller.rb @@ -5,8 +5,10 @@ class Api::V1::Instances::PrivacyPoliciesController < Api::BaseController before_action :set_privacy_policy + vary_by '' + def show - expires_in 1.day, public: true + cache_even_if_authenticated! render json: @privacy_policy, serializer: REST::PrivacyPolicySerializer end diff --git a/app/controllers/api/v1/instances/rules_controller.rb b/app/controllers/api/v1/instances/rules_controller.rb index 93cf3c7594..d3eeca3262 100644 --- a/app/controllers/api/v1/instances/rules_controller.rb +++ b/app/controllers/api/v1/instances/rules_controller.rb @@ -2,10 +2,19 @@ class Api::V1::Instances::RulesController < Api::BaseController skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_around_action :set_locale before_action :set_rules + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in whitelist mode + def current_user + super if whitelist_mode? + end + def index + cache_even_if_authenticated! render json: @rules, each_serializer: REST::RuleSerializer end diff --git a/app/controllers/api/v1/instances/translation_languages_controller.rb b/app/controllers/api/v1/instances/translation_languages_controller.rb new file mode 100644 index 0000000000..c4680cccb8 --- /dev/null +++ b/app/controllers/api/v1/instances/translation_languages_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class Api::V1::Instances::TranslationLanguagesController < Api::BaseController + skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + + before_action :set_languages + + vary_by '' + + def show + cache_even_if_authenticated! + render json: @languages + end + + private + + def set_languages + if TranslationService.configured? + @languages = Rails.cache.fetch('translation_service/languages', expires_in: 7.days, race_condition_ttl: 1.hour) { TranslationService.configured.languages } + @languages['und'] = @languages.delete(nil) if @languages.key?(nil) + else + @languages = {} + end + end +end diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb index 913319a869..5a6701ff96 100644 --- a/app/controllers/api/v1/instances_controller.rb +++ b/app/controllers/api/v1/instances_controller.rb @@ -1,11 +1,18 @@ # frozen_string_literal: true class Api::V1::InstancesController < Api::BaseController - skip_before_action :set_cache_headers skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_around_action :set_locale + + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in whitelist mode + def current_user + super if whitelist_mode? + end def show - expires_in 3.minutes, public: true + cache_even_if_authenticated! render_with_cache json: InstancePresenter.new, serializer: REST::V1::InstanceSerializer, root: 'instance' end end diff --git a/app/controllers/api/v1/lists/accounts_controller.rb b/app/controllers/api/v1/lists/accounts_controller.rb index b66ea9bfe6..8e12cb7b65 100644 --- a/app/controllers/api/v1/lists/accounts_controller.rb +++ b/app/controllers/api/v1/lists/accounts_controller.rb @@ -62,17 +62,13 @@ class Api::V1::Lists::AccountsController < Api::BaseController def next_path return if unlimited? - if records_continue? - api_v1_list_accounts_url pagination_params(max_id: pagination_max_id) - end + api_v1_list_accounts_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path return if unlimited? - unless @accounts.empty? - api_v1_list_accounts_url pagination_params(since_id: pagination_since_id) - end + api_v1_list_accounts_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb index 843ca2ec2b..4bbbed2673 100644 --- a/app/controllers/api/v1/lists_controller.rb +++ b/app/controllers/api/v1/lists_controller.rb @@ -42,6 +42,6 @@ class Api::V1::ListsController < Api::BaseController end def list_params - params.permit(:title, :replies_policy) + params.permit(:title, :replies_policy, :exclusive) end end diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb index f9c935bf3e..5ea26d55bd 100644 --- a/app/controllers/api/v1/media_controller.rb +++ b/app/controllers/api/v1/media_controller.rb @@ -6,19 +6,20 @@ class Api::V1::MediaController < Api::BaseController before_action :set_media_attachment, except: [:create] before_action :check_processing, except: [:create] + def show + render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment + end + def create @media_attachment = current_account.media_attachments.create!(media_attachment_params) render json: @media_attachment, serializer: REST::MediaAttachmentSerializer rescue Paperclip::Errors::NotIdentifiedByImageMagickError render json: file_type_error, status: 422 - rescue Paperclip::Error + rescue Paperclip::Error => e + Rails.logger.error "#{e.class}: #{e.message}" render json: processing_error, status: 500 end - def show - render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment - end - def update @media_attachment.update!(updateable_media_attachment_params) render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb index 6cde53a2a7..555485823c 100644 --- a/app/controllers/api/v1/mutes_controller.rb +++ b/app/controllers/api/v1/mutes_controller.rb @@ -33,15 +33,11 @@ class Api::V1::MutesController < Api::BaseController end def next_path - if records_continue? - api_v1_mutes_url pagination_params(max_id: pagination_max_id) - end + api_v1_mutes_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless paginated_mutes.empty? - api_v1_mutes_url pagination_params(since_id: pagination_since_id) - end + api_v1_mutes_url pagination_params(since_id: pagination_since_id) unless paginated_mutes.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index 93785b14a0..7a64d13005 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -28,7 +28,7 @@ class Api::V1::NotificationsController < Api::BaseController end def dismiss - current_account.notifications.find_by!(id: params[:id]).destroy! + current_account.notifications.find(params[:id]).destroy! render_empty end @@ -67,15 +67,11 @@ class Api::V1::NotificationsController < Api::BaseController end def next_path - unless @notifications.empty? - api_v1_notifications_url pagination_params(max_id: pagination_max_id) - end + api_v1_notifications_url pagination_params(max_id: pagination_max_id) unless @notifications.empty? end def prev_path - unless @notifications.empty? - api_v1_notifications_url pagination_params(min_id: pagination_since_id) - end + api_v1_notifications_url pagination_params(min_id: pagination_since_id) unless @notifications.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/polls_controller.rb b/app/controllers/api/v1/polls_controller.rb index 6435e9f0dc..ffc70a8496 100644 --- a/app/controllers/api/v1/polls_controller.rb +++ b/app/controllers/api/v1/polls_controller.rb @@ -8,6 +8,7 @@ class Api::V1::PollsController < Api::BaseController before_action :refresh_poll def show + cache_if_unauthenticated! render json: @poll, serializer: REST::PollSerializer, include_results: true end diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb index 7148d63a4e..3634acf956 100644 --- a/app/controllers/api/v1/push/subscriptions_controller.rb +++ b/app/controllers/api/v1/push/subscriptions_controller.rb @@ -6,6 +6,10 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController before_action :set_push_subscription before_action :check_push_subscription, only: [:show, :update] + def show + render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer + end + def create @push_subscription&.destroy! @@ -21,10 +25,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer end - def show - render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer - end - def update @push_subscription.update!(data: data_params) render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer diff --git a/app/controllers/api/v1/scheduled_statuses_controller.rb b/app/controllers/api/v1/scheduled_statuses_controller.rb index f90642a738..2220b6d22e 100644 --- a/app/controllers/api/v1/scheduled_statuses_controller.rb +++ b/app/controllers/api/v1/scheduled_statuses_controller.rb @@ -52,15 +52,11 @@ class Api::V1::ScheduledStatusesController < Api::BaseController end def next_path - if records_continue? - api_v1_scheduled_statuses_url pagination_params(max_id: pagination_max_id) - end + api_v1_scheduled_statuses_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless @statuses.empty? - api_v1_scheduled_statuses_url pagination_params(min_id: pagination_since_id) - end + api_v1_scheduled_statuses_url pagination_params(min_id: pagination_since_id) unless @statuses.empty? end def records_continue? diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb index 2b614a8375..73eb11e711 100644 --- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb @@ -8,6 +8,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController after_action :insert_pagination_headers def index + cache_if_unauthenticated! @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer end @@ -41,15 +42,11 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController end def next_path - if records_continue? - api_v1_status_favourited_by_index_url pagination_params(max_id: pagination_max_id) - end + api_v1_status_favourited_by_index_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless @accounts.empty? - api_v1_status_favourited_by_index_url pagination_params(since_id: pagination_since_id) - end + api_v1_status_favourited_by_index_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/statuses/histories_controller.rb b/app/controllers/api/v1/statuses/histories_controller.rb index 7fe73a6f54..2913472b04 100644 --- a/app/controllers/api/v1/statuses/histories_controller.rb +++ b/app/controllers/api/v1/statuses/histories_controller.rb @@ -7,11 +7,16 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController before_action :set_status def show - render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer + cache_if_unauthenticated! + render json: status_edits, each_serializer: REST::StatusEditSerializer end private + def status_edits + @status.edits.includes(:account, status: [:account]).to_a.presence || [@status.build_snapshot(at_time: @status.edited_at || @status.created_at)] + end + def set_status @status = Status.find(params[:status_id]) authorize @status, :show? diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb index 24db30fcc0..41672e7539 100644 --- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb @@ -8,6 +8,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController after_action :insert_pagination_headers def index + cache_if_unauthenticated! @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer end @@ -37,15 +38,11 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController end def next_path - if records_continue? - api_v1_status_reblogged_by_index_url pagination_params(max_id: pagination_max_id) - end + api_v1_status_reblogged_by_index_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless @accounts.empty? - api_v1_status_reblogged_by_index_url pagination_params(since_id: pagination_since_id) - end + api_v1_status_reblogged_by_index_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb index 1be15a5a43..e3769437b7 100644 --- a/app/controllers/api/v1/statuses/reblogs_controller.rb +++ b/app/controllers/api/v1/statuses/reblogs_controller.rb @@ -2,6 +2,8 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController include Authorization + include Redisable + include Lockable before_action -> { doorkeeper_authorize! :write, :'write:statuses' } before_action :require_user! @@ -10,7 +12,9 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController override_rate_limit_headers :create, family: :statuses def create - @status = ReblogService.new.call(current_account, @reblog, reblog_params) + with_redis_lock("reblog:#{current_account.id}:#{@reblog.id}") do + @status = ReblogService.new.call(current_account, @reblog, reblog_params) + end render json: @status, serializer: REST::StatusSerializer end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 8dcf6331ed..960f8cf765 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -24,11 +24,14 @@ class Api::V1::StatusesController < Api::BaseController DESCENDANTS_DEPTH_LIMIT = 20 def show + cache_if_unauthenticated! @status = cache_collection([@status], Status).first render json: @status, serializer: REST::StatusSerializer end def context + cache_if_unauthenticated! + ancestors_limit = CONTEXT_LIMIT descendants_limit = CONTEXT_LIMIT descendants_depth_limit = nil diff --git a/app/controllers/api/v1/streaming_controller.rb b/app/controllers/api/v1/streaming_controller.rb index b23a60170c..0cdd00d62f 100644 --- a/app/controllers/api/v1/streaming_controller.rb +++ b/app/controllers/api/v1/streaming_controller.rb @@ -5,7 +5,7 @@ class Api::V1::StreamingController < Api::BaseController if Rails.configuration.x.streaming_api_base_url == request.host not_found else - redirect_to streaming_api_url, status: 301 + redirect_to streaming_api_url, status: 301, allow_other_host: true end end diff --git a/app/controllers/api/v1/tags_controller.rb b/app/controllers/api/v1/tags_controller.rb index 272362c314..284ec85937 100644 --- a/app/controllers/api/v1/tags_controller.rb +++ b/app/controllers/api/v1/tags_controller.rb @@ -8,6 +8,7 @@ class Api::V1::TagsController < Api::BaseController override_rate_limit_headers :follow, family: :follows def show + cache_if_unauthenticated! render json: @tag, serializer: REST::TagSerializer end @@ -25,6 +26,7 @@ class Api::V1::TagsController < Api::BaseController def set_or_create_tag return not_found unless Tag::HASHTAG_NAME_RE.match?(params[:id]) + @tag = Tag.find_normalized(params[:id]) || Tag.new(name: Tag.normalize(params[:id]), display_name: params[:id]) end end diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index 493fe4776a..6af504ff63 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -5,6 +5,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController after_action :insert_pagination_headers, unless: -> { @statuses.empty? } def show + cache_if_unauthenticated! @statuses = load_statuses render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end @@ -40,7 +41,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController only_media: truthy_param?(:only_media), allow_local_only: truthy_param?(:allow_local_only), with_replies: Setting.show_replies_in_public_timelines, - with_reblogs: Setting.show_reblogs_in_public_timelines, + with_reblogs: Setting.show_reblogs_in_public_timelines ) end diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb index 64a1db58df..9cd7b99046 100644 --- a/app/controllers/api/v1/timelines/tag_controller.rb +++ b/app/controllers/api/v1/timelines/tag_controller.rb @@ -5,6 +5,7 @@ class Api::V1::Timelines::TagController < Api::BaseController after_action :insert_pagination_headers, unless: -> { @statuses.empty? } def show + cache_if_unauthenticated! @statuses = load_statuses render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end diff --git a/app/controllers/api/v1/trends/links_controller.rb b/app/controllers/api/v1/trends/links_controller.rb index 8ff3b364e2..57cfa0b7e4 100644 --- a/app/controllers/api/v1/trends/links_controller.rb +++ b/app/controllers/api/v1/trends/links_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Api::V1::Trends::LinksController < Api::BaseController + vary_by 'Authorization, Accept-Language' + before_action :set_links after_action :insert_pagination_headers @@ -8,6 +10,7 @@ class Api::V1::Trends::LinksController < Api::BaseController DEFAULT_LINKS_LIMIT = 10 def index + cache_if_unauthenticated! render json: @links, each_serializer: REST::Trends::LinkSerializer end @@ -18,13 +21,11 @@ class Api::V1::Trends::LinksController < Api::BaseController end def set_links - @links = begin - if enabled? - links_from_trends.offset(offset_param).limit(limit_param(DEFAULT_LINKS_LIMIT)) - else - [] - end - end + @links = if enabled? + links_from_trends.offset(offset_param).limit(limit_param(DEFAULT_LINKS_LIMIT)) + else + [] + end end def links_from_trends diff --git a/app/controllers/api/v1/trends/statuses_controller.rb b/app/controllers/api/v1/trends/statuses_controller.rb index c275d5fc81..c186864c3b 100644 --- a/app/controllers/api/v1/trends/statuses_controller.rb +++ b/app/controllers/api/v1/trends/statuses_controller.rb @@ -1,11 +1,14 @@ # frozen_string_literal: true class Api::V1::Trends::StatusesController < Api::BaseController + vary_by 'Authorization, Accept-Language' + before_action :set_statuses after_action :insert_pagination_headers def index + cache_if_unauthenticated! render json: @statuses, each_serializer: REST::StatusSerializer end @@ -16,13 +19,11 @@ class Api::V1::Trends::StatusesController < Api::BaseController end def set_statuses - @statuses = begin - if enabled? - cache_collection(statuses_from_trends.offset(offset_param).limit(limit_param(DEFAULT_STATUSES_LIMIT)), Status) - else - [] - end - end + @statuses = if enabled? + cache_collection(statuses_from_trends.offset(offset_param).limit(limit_param(DEFAULT_STATUSES_LIMIT)), Status) + else + [] + end end def statuses_from_trends diff --git a/app/controllers/api/v1/trends/tags_controller.rb b/app/controllers/api/v1/trends/tags_controller.rb index 885a4ad7e8..6cc8194def 100644 --- a/app/controllers/api/v1/trends/tags_controller.rb +++ b/app/controllers/api/v1/trends/tags_controller.rb @@ -8,6 +8,7 @@ class Api::V1::Trends::TagsController < Api::BaseController DEFAULT_TAGS_LIMIT = (ENV['MAX_TRENDING_TAGS'] || 10).to_i def index + cache_if_unauthenticated! render json: @tags, each_serializer: REST::TagSerializer, relationships: TagRelationshipsPresenter.new(@tags, current_user&.account_id) end @@ -18,13 +19,11 @@ class Api::V1::Trends::TagsController < Api::BaseController end def set_tags - @tags = begin - if enabled? - tags_from_trends.offset(offset_param).limit(limit_param(DEFAULT_TAGS_LIMIT)) - else - [] - end - end + @tags = if enabled? + tags_from_trends.offset(offset_param).limit(limit_param(DEFAULT_TAGS_LIMIT)) + else + [] + end end def tags_from_trends diff --git a/app/controllers/api/v2/admin/accounts_controller.rb b/app/controllers/api/v2/admin/accounts_controller.rb index b25831aa09..65cf0c4db4 100644 --- a/app/controllers/api/v2/admin/accounts_controller.rb +++ b/app/controllers/api/v2/admin/accounts_controller.rb @@ -18,6 +18,14 @@ class Api::V2::Admin::AccountsController < Api::V1::Admin::AccountsController private + def next_path + api_v2_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v2_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty? + end + def filtered_accounts AccountFilter.new(translated_filter_params).results end @@ -25,9 +33,7 @@ class Api::V2::Admin::AccountsController < Api::V1::Admin::AccountsController def translated_filter_params translated_params = filter_params.slice(*AccountFilter::KEYS) - if params[:permissions] == 'staff' - translated_params[:role_ids] = UserRole.that_can(:manage_reports).map(&:id) - end + translated_params[:role_ids] = UserRole.that_can(:manage_reports).map(&:id) if params[:permissions] == 'staff' translated_params end diff --git a/app/controllers/api/v2/filters/keywords_controller.rb b/app/controllers/api/v2/filters/keywords_controller.rb index c63e1d986b..fe1a991944 100644 --- a/app/controllers/api/v2/filters/keywords_controller.rb +++ b/app/controllers/api/v2/filters/keywords_controller.rb @@ -12,13 +12,13 @@ class Api::V2::Filters::KeywordsController < Api::BaseController render json: @keywords, each_serializer: REST::FilterKeywordSerializer end - def create - @keyword = current_account.custom_filters.find(params[:filter_id]).keywords.create!(resource_params) - + def show render json: @keyword, serializer: REST::FilterKeywordSerializer end - def show + def create + @keyword = current_account.custom_filters.find(params[:filter_id]).keywords.create!(resource_params) + render json: @keyword, serializer: REST::FilterKeywordSerializer end diff --git a/app/controllers/api/v2/filters/statuses_controller.rb b/app/controllers/api/v2/filters/statuses_controller.rb index 755c14cffa..2e95497a66 100644 --- a/app/controllers/api/v2/filters/statuses_controller.rb +++ b/app/controllers/api/v2/filters/statuses_controller.rb @@ -12,13 +12,13 @@ class Api::V2::Filters::StatusesController < Api::BaseController render json: @status_filters, each_serializer: REST::FilterStatusSerializer end - def create - @status_filter = current_account.custom_filters.find(params[:filter_id]).statuses.create!(resource_params) - + def show render json: @status_filter, serializer: REST::FilterStatusSerializer end - def show + def create + @status_filter = current_account.custom_filters.find(params[:filter_id]).statuses.create!(resource_params) + render json: @status_filter, serializer: REST::FilterStatusSerializer end diff --git a/app/controllers/api/v2/filters_controller.rb b/app/controllers/api/v2/filters_controller.rb index 8ff3076cfb..2fcdeeae45 100644 --- a/app/controllers/api/v2/filters_controller.rb +++ b/app/controllers/api/v2/filters_controller.rb @@ -11,13 +11,13 @@ class Api::V2::FiltersController < Api::BaseController render json: @filters, each_serializer: REST::FilterSerializer, rules_requested: true end - def create - @filter = current_account.custom_filters.create!(resource_params) - + def show render json: @filter, serializer: REST::FilterSerializer, rules_requested: true end - def show + def create + @filter = current_account.custom_filters.create!(resource_params) + render json: @filter, serializer: REST::FilterSerializer, rules_requested: true end diff --git a/app/controllers/api/v2/instances_controller.rb b/app/controllers/api/v2/instances_controller.rb index bcd90cff22..8346e28830 100644 --- a/app/controllers/api/v2/instances_controller.rb +++ b/app/controllers/api/v2/instances_controller.rb @@ -2,7 +2,7 @@ class Api::V2::InstancesController < Api::V1::InstancesController def show - expires_in 3.minutes, public: true + cache_even_if_authenticated! render_with_cache json: InstancePresenter.new, serializer: REST::InstanceSerializer, root: 'instance' end end diff --git a/app/controllers/api/v2/media_controller.rb b/app/controllers/api/v2/media_controller.rb index 288f847f17..72bc694421 100644 --- a/app/controllers/api/v2/media_controller.rb +++ b/app/controllers/api/v2/media_controller.rb @@ -6,7 +6,8 @@ class Api::V2::MediaController < Api::V1::MediaController render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: @media_attachment.not_processed? ? 202 : 200 rescue Paperclip::Errors::NotIdentifiedByImageMagickError render json: file_type_error, status: 422 - rescue Paperclip::Error + rescue Paperclip::Error => e + Rails.logger.error "#{e.class}: #{e.message}" render json: processing_error, status: 500 end end diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb index b084eae425..cc74db58e5 100644 --- a/app/controllers/api/v2/search_controller.rb +++ b/app/controllers/api/v2/search_controller.rb @@ -34,11 +34,11 @@ class Api::V2::SearchController < Api::BaseController params[:q], current_account, limit_param(RESULTS_LIMIT), - search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed)) + search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed), following: truthy_param?(:following)) ) end def search_params - params.permit(:type, :offset, :min_id, :max_id, :account_id) + params.permit(:type, :offset, :min_id, :max_id, :account_id, :following) end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ee3c5204d8..7c09040fbf 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -18,7 +18,11 @@ class ApplicationController < ActionController::Base helper_method :current_skin helper_method :single_user_mode? helper_method :use_seamless_external_login? + helper_method :omniauth_only? + helper_method :sso_account_settings helper_method :whitelist_mode? + helper_method :body_class_string + helper_method :skip_csrf_meta_tags? rescue_from ActionController::ParameterMissing, Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request rescue_from Mastodon::NotPermittedError, with: :forbidden @@ -35,9 +39,11 @@ class ApplicationController < ActionController::Base service_unavailable end - before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? + before_action :store_referrer, except: :raise_not_found, if: :devise_controller? before_action :require_functional!, if: :user_signed_in? + before_action :set_cache_control_defaults + skip_before_action :verify_authenticity_token, only: :raise_not_found def raise_not_found @@ -54,16 +60,31 @@ class ApplicationController < ActionController::Base !authorized_fetch_mode? end - def store_current_location - store_location_for(:user, request.url) unless [:json, :rss].include?(request.format&.to_sym) + def store_referrer + return if request.referer.blank? + + redirect_uri = URI(request.referer) + return if redirect_uri.path.start_with?('/auth') + + stored_url = redirect_uri.to_s if redirect_uri.host == request.host && redirect_uri.port == request.port + + store_location_for(:user, stored_url) end def require_functional! redirect_to edit_user_registration_path unless current_user.functional? end + def skip_csrf_meta_tags? + false + end + def after_sign_out_path_for(_resource_or_scope) - new_user_session_path + if ENV['OMNIAUTH_ONLY'] == 'true' && ENV['OIDC_ENABLED'] == 'true' + '/auth/auth/openid_connect/logout' + else + new_user_session_path + end end protected @@ -116,6 +137,14 @@ class ApplicationController < ActionController::Base Devise.pam_authentication || Devise.ldap_authentication end + def omniauth_only? + ENV['OMNIAUTH_ONLY'] == 'true' + end + + def sso_account_settings + ENV.fetch('SSO_ACCOUNT_SETTINGS', nil) + end + def current_account return @current_account if defined?(@current_account) @@ -128,6 +157,10 @@ class ApplicationController < ActionController::Base @current_session = SessionActivation.find_by(session_id: cookies.signed['_session_id']) if cookies.signed['_session_id'].present? end + def body_class_string + @body_classes || '' + end + def respond_with_error(code) respond_to do |format| format.any do @@ -137,4 +170,8 @@ class ApplicationController < ActionController::Base format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code } end end + + def set_cache_control_defaults + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb index 0817a905ca..3283c5f362 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -15,12 +15,6 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController skip_before_action :require_functional! - def new - super - - resource.email = current_user.unconfirmed_email || current_user.email if user_signed_in? - end - def show old_session_values = session.to_hash reset_session @@ -29,6 +23,12 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController super end + def new + super + + resource.email = current_user.unconfirmed_email || current_user.email if user_signed_in? + end + def confirm_captcha check_captcha! do |message| flash.now[:alert] = message @@ -51,14 +51,12 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController # step. confirmation_token = params[:confirmation_token] return if confirmation_token.nil? + @confirmation_user = User.find_first_by_auth_conditions(confirmation_token: confirmation_token) end def captcha_user_bypass? return true if @confirmation_user.nil? || @confirmation_user.confirmed? - - invite = Invite.find(@confirmation_user.invite_id) if @confirmation_user.invite_id.present? - invite.present? && !invite.max_uses.nil? end def set_pack @@ -90,8 +88,10 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController def after_confirmation_path_for(_resource_name, user) if user.created_by_application && truthy_param?(:redirect_to_app) user.created_by_application.confirmation_redirect_uri + elsif user_signed_in? + web_url('start') else - super + new_user_session_path end end end diff --git a/app/controllers/auth/omniauth_callbacks_controller.rb b/app/controllers/auth/omniauth_callbacks_controller.rb index 3d7962de56..9e0fb942aa 100644 --- a/app/controllers/auth/omniauth_callbacks_controller.rb +++ b/app/controllers/auth/omniauth_callbacks_controller.rb @@ -33,7 +33,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController def after_sign_in_path_for(resource) if resource.email_present? - root_path + stored_location_for(resource) || root_path else auth_setup_path(missing_email: '1') end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 40c38bc6dd..a9d92b6e2b 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -25,18 +25,16 @@ class Auth::RegistrationsController < Devise::RegistrationsController super(&:build_invite_request) end - def destroy - not_found - end - def update super do |resource| - if resource.saved_change_to_encrypted_password? - resource.clear_other_sessions(current_session.session_id) - end + resource.clear_other_sessions(current_session.session_id) if resource.saved_change_to_encrypted_password? end end + def destroy + not_found + end + protected def update_resource(resource, params) @@ -49,7 +47,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController super(hash) resource.locale = I18n.locale - resource.invite_code = params[:invite_code] if resource.invite_code.blank? + resource.invite_code = @invite&.code if resource.invite_code.blank? resource.registration_form_time = session[:registration_form_time] resource.sign_up_ip = request.remote_ip @@ -134,7 +132,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def set_sessions - @sessions = current_user.session_activations + @sessions = current_user.session_activations.order(updated_at: :desc) end def set_strikes @@ -159,6 +157,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def set_cache_headers - response.headers['Cache-Control'] = 'private, no-store' + response.cache_control.replace(private: true, no_store: true) end end diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index 16c18baa24..b1abb9f1df 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -53,9 +53,9 @@ class Auth::SessionsController < Devise::SessionsController session[:webauthn_challenge] = options_for_get.challenge - render json: options_for_get, status: :ok + render json: options_for_get, status: 200 else - render json: { error: t('webauthn_credentials.not_enabled') }, status: :unauthorized + render json: { error: t('webauthn_credentials.not_enabled') }, status: 401 end end @@ -115,9 +115,7 @@ class Auth::SessionsController < Devise::SessionsController def home_paths(resource) paths = [about_path] - if single_user_mode? && resource.is_a?(User) - paths << short_account_path(username: resource.account) - end + paths << short_account_path(username: resource.account) if single_user_mode? && resource.is_a?(User) paths end diff --git a/app/controllers/auth/setup_controller.rb b/app/controllers/auth/setup_controller.rb index db5a866f21..8edca4d01b 100644 --- a/app/controllers/auth/setup_controller.rb +++ b/app/controllers/auth/setup_controller.rb @@ -11,15 +11,7 @@ class Auth::SetupController < ApplicationController skip_before_action :require_functional! - def show - flash.now[:notice] = begin - if @user.pending? - I18n.t('devise.registrations.signed_up_but_pending') - else - I18n.t('devise.registrations.signed_up_but_unconfirmed') - end - end - end + def show; end def update # This allows updating the e-mail without entering a password as is required @@ -27,14 +19,13 @@ class Auth::SetupController < ApplicationController # that were not confirmed yet if @user.update(user_params) - redirect_to auth_setup_path, notice: I18n.t('devise.confirmations.send_instructions') + @user.resend_confirmation_instructions unless @user.confirmed? + redirect_to auth_setup_path, notice: I18n.t('auth.setup.new_confirmation_instructions_sent') else render :show end end - helper_method :missing_email? - private def require_unconfirmed_or_pending! @@ -53,11 +44,7 @@ class Auth::SetupController < ApplicationController params.require(:user).permit(:email) end - def missing_email? - truthy_param?(:missing_email) - end - def set_pack - use_pack 'auth' + use_pack 'sign_up' end end diff --git a/app/controllers/authorize_interactions_controller.rb b/app/controllers/authorize_interactions_controller.rb index 97fe4a9abd..73f0f2b88d 100644 --- a/app/controllers/authorize_interactions_controller.rb +++ b/app/controllers/authorize_interactions_controller.rb @@ -60,7 +60,7 @@ class AuthorizeInteractionsController < ApplicationController end def uri_param - params[:uri] || params.fetch(:acct, '').gsub(/\Aacct:/, '') + params[:uri] || params.fetch(:acct, '').delete_prefix('acct:') end def set_body_classes diff --git a/app/controllers/backups_controller.rb b/app/controllers/backups_controller.rb new file mode 100644 index 0000000000..205df48d44 --- /dev/null +++ b/app/controllers/backups_controller.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class BackupsController < ApplicationController + include RoutingHelper + + skip_before_action :require_functional! + + before_action :authenticate_user! + before_action :set_backup + + def download + case Paperclip::Attachment.default_options[:storage] + when :s3 + redirect_to @backup.dump.expiring_url(10), allow_other_host: true + when :fog + if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present? + redirect_to @backup.dump.expiring_url(Time.now.utc + 10), allow_other_host: true + else + redirect_to full_asset_url(@backup.dump.url), allow_other_host: true + end + when :filesystem + redirect_to full_asset_url(@backup.dump.url), allow_other_host: true + end + end + + private + + def set_backup + @backup = current_user.backups.find(params[:id]) + end +end diff --git a/app/controllers/concerns/account_controller_concern.rb b/app/controllers/concerns/account_controller_concern.rb index 2f7d84df04..e9cff22ca8 100644 --- a/app/controllers/concerns/account_controller_concern.rb +++ b/app/controllers/concerns/account_controller_concern.rb @@ -10,7 +10,8 @@ module AccountControllerConcern included do before_action :set_instance_presenter - before_action :set_link_headers, if: -> { request.format.nil? || request.format == :html } + + after_action :set_link_headers, if: -> { request.format.nil? || request.format == :html } end private diff --git a/app/controllers/concerns/api_caching_concern.rb b/app/controllers/concerns/api_caching_concern.rb new file mode 100644 index 0000000000..705abce80f --- /dev/null +++ b/app/controllers/concerns/api_caching_concern.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module ApiCachingConcern + extend ActiveSupport::Concern + + def cache_if_unauthenticated! + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? + end + + def cache_even_if_authenticated! + expires_in(5.minutes, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless whitelist_mode? + end +end diff --git a/app/controllers/concerns/cache_concern.rb b/app/controllers/concerns/cache_concern.rb index 05e431b19a..55ebe1bd64 100644 --- a/app/controllers/concerns/cache_concern.rb +++ b/app/controllers/concerns/cache_concern.rb @@ -3,8 +3,182 @@ module CacheConcern extend ActiveSupport::Concern + module ActiveRecordCoder + EMPTY_HASH = {}.freeze + + class << self + def dump(record) + instances = InstanceTracker.new + serialized_associations = serialize_associations(record, instances) + serialized_records = instances.map { |r| serialize_record(r) } + [serialized_associations, *serialized_records] + end + + def load(payload) + instances = InstanceTracker.new + serialized_associations, *serialized_records = payload + serialized_records.each { |attrs| instances.push(deserialize_record(*attrs)) } + deserialize_associations(serialized_associations, instances) + end + + private + + # Records without associations, or which have already been visited before, + # are serialized by their id alone. + # + # Records with associations are serialized as a two-element array including + # their id and the record's association cache. + # + def serialize_associations(record, instances) + return unless record + + if (id = instances.lookup(record)) + payload = id + else + payload = instances.push(record) + + cached_associations = record.class.reflect_on_all_associations.select do |reflection| + record.association_cached?(reflection.name) + end + + unless cached_associations.empty? + serialized_associations = cached_associations.map do |reflection| + association = record.association(reflection.name) + + serialized_target = if reflection.collection? + association.target.map { |target_record| serialize_associations(target_record, instances) } + else + serialize_associations(association.target, instances) + end + + [reflection.name, serialized_target] + end + + payload = [payload, serialized_associations] + end + end + + payload + end + + def deserialize_associations(payload, instances) + return unless payload + + id, associations = payload + record = instances.fetch(id) + + associations&.each do |name, serialized_target| + begin + association = record.association(name) + rescue ActiveRecord::AssociationNotFoundError + raise AssociationMissingError, "undefined association: #{name}" + end + + target = if association.reflection.collection? + serialized_target.map! { |serialized_record| deserialize_associations(serialized_record, instances) } + else + deserialize_associations(serialized_target, instances) + end + + association.target = target + end + + record + end + + def serialize_record(record) + arguments = [record.class.name, attributes_for_database(record)] + arguments << true if record.new_record? + arguments + end + + if Rails.gem_version >= Gem::Version.new('7.0') + def attributes_for_database(record) + attributes = record.attributes_for_database + attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr } + attributes + end + else + def attributes_for_database(record) + attributes = record.instance_variable_get(:@attributes).send(:attributes).transform_values(&:value_for_database) + attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr } + attributes + end + end + + def deserialize_record(class_name, attributes_from_database, new_record = false) # rubocop:disable Style/OptionalBooleanParameter + begin + klass = Object.const_get(class_name) + rescue NameError + raise ClassMissingError, "undefined class: #{class_name}" + end + + # Ideally we'd like to call `klass.instantiate`, however it doesn't allow to pass + # wether the record was persisted or not. + attributes = klass.attributes_builder.build_from_database(attributes_from_database, EMPTY_HASH) + klass.allocate.init_with_attributes(attributes, new_record) + end + end + + class Error < StandardError + end + + class ClassMissingError < Error + end + + class AssociationMissingError < Error + end + + class InstanceTracker + def initialize + @instances = [] + @ids = {}.compare_by_identity + end + + def map(&block) + @instances.map(&block) + end + + def fetch(...) + @instances.fetch(...) + end + + def push(instance) + id = @ids[instance] = @instances.size + @instances << instance + id + end + + def lookup(instance) + @ids[instance] + end + end + end + + class_methods do + def vary_by(value, **kwargs) + before_action(**kwargs) do |controller| + response.headers['Vary'] = value.respond_to?(:call) ? controller.instance_exec(&value) : value + end + end + end + + included do + after_action :enforce_cache_control! + end + + # Prevents high-entropy headers such as `Cookie`, `Signature` or `Authorization` + # from being used as cache keys, while allowing to `Vary` on them (to not serve + # anonymous cached data to authenticated requests when authentication matters) + def enforce_cache_control! + vary = response.headers['Vary']&.split&.map { |x| x.strip.downcase } + return unless vary.present? && %w(cookie authorization signature).any? { |header| vary.include?(header) && request.headers[header].present? } + + response.cache_control.replace(private: true, no_store: true) + end + def render_with_cache(**options) - raise ArgumentError, 'only JSON render calls are supported' unless options.key?(:json) || block_given? + raise ArgumentError, 'Only JSON render calls are supported' unless options.key?(:json) || block_given? key = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields].nil? ? nil : options[:fields].join(',')].compact.join(':') expires_in = options.delete(:expires_in) || 3.minutes @@ -24,18 +198,19 @@ module CacheConcern end end - def set_cache_headers - response.headers['Vary'] = public_fetch_mode? ? 'Accept' : 'Accept, Signature' - end - def cache_collection(raw, klass) return raw unless klass.respond_to?(:with_includes) raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation) return [] if raw.empty? - cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id) - uncached_ids = raw.map(&:id) - cached_keys_with_value.keys + cached_keys_with_value = begin + Rails.cache.read_multi(*raw).transform_keys(&:id).transform_values { |r| ActiveRecordCoder.load(r) } + rescue ActiveRecordCoder::Error + {} # The serialization format may have changed, let's pretend it's a cache miss. + end + + uncached_ids = raw.map(&:id) - cached_keys_with_value.keys klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!) @@ -43,7 +218,7 @@ module CacheConcern uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id) uncached.each_value do |item| - Rails.cache.write(item, item) + Rails.cache.write(item, ActiveRecordCoder.dump(item)) end end diff --git a/app/controllers/concerns/captcha_concern.rb b/app/controllers/concerns/captcha_concern.rb index 538c1ffb14..576304d1ca 100644 --- a/app/controllers/concerns/captcha_concern.rb +++ b/app/controllers/concerns/captcha_concern.rb @@ -2,6 +2,7 @@ module CaptchaConcern extend ActiveSupport::Concern + include Hcaptcha::Adapters::ViewMethods included do @@ -35,18 +36,22 @@ module CaptchaConcern flash.delete(:hcaptcha_error) yield message end + false end end def extend_csp_for_captcha! policy = request.content_security_policy + return unless captcha_required? && policy.present? %w(script_src frame_src style_src connect_src).each do |directive| values = policy.send(directive) + values << 'https://hcaptcha.com' unless values.include?('https://hcaptcha.com') || values.include?('https:') values << 'https://*.hcaptcha.com' unless values.include?('https://*.hcaptcha.com') || values.include?('https:') + policy.send(directive, *values) end end diff --git a/app/controllers/concerns/rate_limit_headers.rb b/app/controllers/concerns/rate_limit_headers.rb index b8696df736..30702f00e7 100644 --- a/app/controllers/concerns/rate_limit_headers.rb +++ b/app/controllers/concerns/rate_limit_headers.rb @@ -6,13 +6,11 @@ module RateLimitHeaders class_methods do def override_rate_limit_headers(method_name, options = {}) around_action(only: method_name, if: :current_account) do |_controller, block| - begin - block.call - ensure - rate_limiter = RateLimiter.new(current_account, options) - rate_limit_headers = rate_limiter.to_headers - response.headers.merge!(rate_limit_headers) unless response.headers['X-RateLimit-Remaining'].present? && rate_limit_headers['X-RateLimit-Remaining'].to_i > response.headers['X-RateLimit-Remaining'].to_i - end + block.call + ensure + rate_limiter = RateLimiter.new(current_account, options) + rate_limit_headers = rate_limiter.to_headers + response.headers.merge!(rate_limit_headers) unless response.headers['X-RateLimit-Remaining'].present? && rate_limit_headers['X-RateLimit-Remaining'].to_i > response.headers['X-RateLimit-Remaining'].to_i end end end @@ -67,6 +65,6 @@ module RateLimitHeaders end def reset_period_offset - api_throttle_data[:period] - request_time.to_i % api_throttle_data[:period] + api_throttle_data[:period] - (request_time.to_i % api_throttle_data[:period]) end end diff --git a/app/controllers/concerns/session_tracking_concern.rb b/app/controllers/concerns/session_tracking_concern.rb index eaaa4ac597..3f56c0d026 100644 --- a/app/controllers/concerns/session_tracking_concern.rb +++ b/app/controllers/concerns/session_tracking_concern.rb @@ -13,6 +13,7 @@ module SessionTrackingConcern def set_session_activity return unless session_needs_update? + current_session.touch end diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 9c04ab4ca6..1d27c92c8c 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -138,7 +138,7 @@ module SignatureVerification end def signed_headers - signature_params.fetch('headers', signature_algorithm == 'hs2019' ? '(created)' : 'date').downcase.split(' ') + signature_params.fetch('headers', signature_algorithm == 'hs2019' ? '(created)' : 'date').downcase.split end def verify_signature_strength! @@ -165,6 +165,7 @@ module SignatureVerification end raise SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a SHA-256 digest. Given digest: #{sha256[1]}" if digest_size != 32 + raise SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}" end @@ -179,14 +180,15 @@ module SignatureVerification def build_signed_string signed_headers.map do |signed_header| - if signed_header == Request::REQUEST_TARGET + case signed_header + when Request::REQUEST_TARGET "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" - elsif signed_header == '(created)' + when '(created)' raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019' raise SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank? "(created): #{signature_params['created']}" - elsif signed_header == '(expires)' + when '(expires)' raise SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019' raise SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank? @@ -243,7 +245,7 @@ module SignatureVerification end if key_id.start_with?('acct:') - stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''), suppress_errors: false) } + stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) } elsif !ActivityPub::TagManager.instance.local_uri?(key_id) account = ActivityPub::TagManager.instance.uri_to_actor(key_id) account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) } diff --git a/app/controllers/concerns/theming_concern.rb b/app/controllers/concerns/theming_concern.rb index f993a81d72..82a53dbf51 100644 --- a/app/controllers/concerns/theming_concern.rb +++ b/app/controllers/concerns/theming_concern.rb @@ -75,7 +75,7 @@ module ThemingConcern end fallbacks.each do |fallback| - return resolve_pack(Themes.instance.flavour(fallback), pack_name) if Themes.instance.flavour(fallback) + return resolve_pack(Themes.instance.flavour(fallback), pack_name, skin) if Themes.instance.flavour(fallback) end nil diff --git a/app/controllers/concerns/two_factor_authentication_concern.rb b/app/controllers/concerns/two_factor_authentication_concern.rb index c9477a1d42..b30cd354d2 100644 --- a/app/controllers/concerns/two_factor_authentication_concern.rb +++ b/app/controllers/concerns/two_factor_authentication_concern.rb @@ -57,10 +57,10 @@ module TwoFactorAuthenticationConcern if valid_webauthn_credential?(user, webauthn_credential) on_authentication_success(user, :webauthn) - render json: { redirect_path: after_sign_in_path_for(user) }, status: :ok + render json: { redirect_path: after_sign_in_path_for(user) }, status: 200 else on_authentication_failure(user, :webauthn, :invalid_credential) - render json: { error: t('webauthn_credentials.invalid_credential') }, status: :unprocessable_entity + render json: { error: t('webauthn_credentials.invalid_credential') }, status: 422 end end @@ -81,13 +81,11 @@ module TwoFactorAuthenticationConcern @body_classes = 'lighter' @webauthn_enabled = user.webauthn_enabled? - @scheme_type = begin - if user.webauthn_enabled? && user_params[:otp_attempt].blank? - 'webauthn' - else - 'totp' - end - end + @scheme_type = if user.webauthn_enabled? && user_params[:otp_attempt].blank? + 'webauthn' + else + 'totp' + end set_locale { render :two_factor } end diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb index 7ba7a57e3d..96c31566e0 100644 --- a/app/controllers/concerns/web_app_controller_concern.rb +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -7,6 +7,12 @@ module WebAppControllerConcern prepend_before_action :redirect_unauthenticated_to_permalinks! before_action :set_pack before_action :set_app_body_class + + vary_by 'Accept, Accept-Language, Cookie' + end + + def skip_csrf_meta_tags? + current_user.nil? end def set_app_body_class diff --git a/app/controllers/custom_css_controller.rb b/app/controllers/custom_css_controller.rb index 9270c467dc..e7a02ea89c 100644 --- a/app/controllers/custom_css_controller.rb +++ b/app/controllers/custom_css_controller.rb @@ -1,18 +1,8 @@ # frozen_string_literal: true -class CustomCssController < ApplicationController - skip_before_action :store_current_location - skip_before_action :require_functional! - skip_before_action :update_user_sign_in - skip_before_action :set_session_activity - - skip_around_action :set_locale - - before_action :set_cache_headers - +class CustomCssController < ActionController::Base # rubocop:disable Rails/ApplicationController def show expires_in 3.minutes, public: true - request.session_options[:skip] = true render content_type: 'text/css' end end diff --git a/app/controllers/disputes/base_controller.rb b/app/controllers/disputes/base_controller.rb index 7830c55247..f51f44c620 100644 --- a/app/controllers/disputes/base_controller.rb +++ b/app/controllers/disputes/base_controller.rb @@ -10,6 +10,7 @@ class Disputes::BaseController < ApplicationController before_action :set_body_classes before_action :authenticate_user! before_action :set_pack + before_action :set_cache_headers private @@ -20,4 +21,8 @@ class Disputes::BaseController < ApplicationController def set_body_classes @body_classes = 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/emojis_controller.rb b/app/controllers/emojis_controller.rb index 41f1e1c5ca..72bc56de04 100644 --- a/app/controllers/emojis_controller.rb +++ b/app/controllers/emojis_controller.rb @@ -2,15 +2,12 @@ class EmojisController < ApplicationController before_action :set_emoji - before_action :set_cache_headers + + vary_by -> { 'Signature' if authorized_fetch_mode? } def show - respond_to do |format| - format.json do - expires_in 3.minutes, public: true - render_with_cache json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter - end - end + expires_in 3.minutes, public: true + render_with_cache json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter end private diff --git a/app/controllers/filters/statuses_controller.rb b/app/controllers/filters/statuses_controller.rb index 4f63de7b69..97206c7eda 100644 --- a/app/controllers/filters/statuses_controller.rb +++ b/app/controllers/filters/statuses_controller.rb @@ -8,6 +8,7 @@ class Filters::StatusesController < ApplicationController before_action :set_status_filters before_action :set_pack before_action :set_body_classes + before_action :set_cache_headers PER_PAGE = 20 @@ -43,12 +44,14 @@ class Filters::StatusesController < ApplicationController end def action_from_button - if params[:remove] - 'remove' - end + 'remove' if params[:remove] end def set_body_classes @body_classes = 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/filters_controller.rb b/app/controllers/filters_controller.rb index 2ab3b0a744..180ddf070b 100644 --- a/app/controllers/filters_controller.rb +++ b/app/controllers/filters_controller.rb @@ -7,6 +7,7 @@ class FiltersController < ApplicationController before_action :set_filter, only: [:edit, :update, :destroy] before_action :set_pack before_action :set_body_classes + before_action :set_cache_headers def index @filters = current_account.custom_filters.includes(:keywords, :statuses).order(:phrase) @@ -17,6 +18,8 @@ class FiltersController < ApplicationController @filter.keywords.build end + def edit; end + def create @filter = current_account.custom_filters.build(resource_params) @@ -27,8 +30,6 @@ class FiltersController < ApplicationController end end - def edit; end - def update if @filter.update(resource_params) redirect_to filters_path @@ -59,4 +60,8 @@ class FiltersController < ApplicationController def set_body_classes @body_classes = 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index 1f5ed30de9..2e55cf6c34 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -5,8 +5,9 @@ class FollowerAccountsController < ApplicationController include SignatureVerification include WebAppControllerConcern + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } - before_action :set_cache_headers skip_around_action :set_locale, if: -> { request.format == :json } skip_before_action :require_functional!, unless: :whitelist_mode? @@ -14,7 +15,7 @@ class FollowerAccountsController < ApplicationController def index respond_to do |format| format.html do - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in? end format.json do diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index febd13c975..2aa31bdf08 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -5,8 +5,9 @@ class FollowingAccountsController < ApplicationController include SignatureVerification include WebAppControllerConcern + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } - before_action :set_cache_headers skip_around_action :set_locale, if: -> { request.format == :json } skip_before_action :require_functional!, unless: :whitelist_mode? @@ -14,7 +15,7 @@ class FollowingAccountsController < ApplicationController def index respond_to do |format| format.html do - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in? end format.json do diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index d8ee82a7a2..ee940e6707 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -6,7 +6,7 @@ class HomeController < ApplicationController before_action :set_instance_presenter def index - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? end private diff --git a/app/controllers/instance_actors_controller.rb b/app/controllers/instance_actors_controller.rb index 0853897f20..8422d74bc3 100644 --- a/app/controllers/instance_actors_controller.rb +++ b/app/controllers/instance_actors_controller.rb @@ -1,10 +1,13 @@ # frozen_string_literal: true -class InstanceActorsController < ApplicationController - include AccountControllerConcern +class InstanceActorsController < ActivityPub::BaseController + vary_by '' - skip_before_action :check_account_confirmation - skip_around_action :set_locale + serialization_scope nil + + before_action :set_account + skip_before_action :require_functional! + skip_before_action :update_user_sign_in def show expires_in 10.minutes, public: true diff --git a/app/controllers/intents_controller.rb b/app/controllers/intents_controller.rb index ca89fc7fe6..ea024e30e6 100644 --- a/app/controllers/intents_controller.rb +++ b/app/controllers/intents_controller.rb @@ -9,7 +9,7 @@ class IntentsController < ApplicationController if uri.scheme == 'web+mastodon' case uri.host when 'follow' - return redirect_to authorize_interaction_path(uri: uri.query_values['uri'].gsub(/\Aacct:/, '')) + return redirect_to authorize_interaction_path(uri: uri.query_values['uri'].delete_prefix('acct:')) when 'share' return redirect_to share_path(text: uri.query_values['text']) end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 0b3c082dce..2db4bc5cbd 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -8,6 +8,7 @@ class InvitesController < ApplicationController before_action :authenticate_user! before_action :set_pack before_action :set_body_classes + before_action :set_cache_headers def index authorize :invite, :create? @@ -54,4 +55,8 @@ class InvitesController < ApplicationController def set_body_classes @body_classes = 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/mail_subscriptions_controller.rb b/app/controllers/mail_subscriptions_controller.rb new file mode 100644 index 0000000000..b071a80605 --- /dev/null +++ b/app/controllers/mail_subscriptions_controller.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +class MailSubscriptionsController < ApplicationController + layout 'auth' + + skip_before_action :require_functional! + + before_action :set_body_classes + before_action :set_user + before_action :set_type + + def show; end + + def create + @user.settings[email_type_from_param] = false + @user.save! + end + + private + + def set_user + @user = GlobalID::Locator.locate_signed(params[:token], for: 'unsubscribe') + end + + def set_body_classes + @body_classes = 'lighter' + end + + def set_type + @type = email_type_from_param + end + + def email_type_from_param + case params[:type] + when 'follow', 'reblog', 'favourite', 'mention', 'follow_request' + "notification_emails.#{params[:type]}" + else + raise ArgumentError + end + end +end diff --git a/app/controllers/manifests_controller.rb b/app/controllers/manifests_controller.rb index 960510f601..4fba9198f3 100644 --- a/app/controllers/manifests_controller.rb +++ b/app/controllers/manifests_controller.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true -class ManifestsController < ApplicationController - skip_before_action :store_current_location - skip_before_action :require_functional! +class ManifestsController < ActionController::Base # rubocop:disable Rails/ApplicationController + # Prevent `active_model_serializer`'s `ActionController::Serialization` from calling `current_user` + # and thus re-issuing session cookies + serialization_scope nil def show expires_in 3.minutes, public: true diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index f9160d8c43..ac820e92bc 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -3,7 +3,6 @@ class MediaController < ApplicationController include Authorization - skip_before_action :store_current_location skip_before_action :require_functional!, unless: :whitelist_mode? before_action :authenticate_user!, if: :whitelist_mode? @@ -33,7 +32,7 @@ class MediaController < ApplicationController scope = MediaAttachment.local.attached # If id is 19 characters long, it's a shortcode, otherwise it's an identifier - @media_attachment = id.size == 19 ? scope.find_by!(shortcode: id) : scope.find_by!(id: id) + @media_attachment = id.size == 19 ? scope.find_by!(shortcode: id) : scope.find(id) end def verify_permitted_status! @@ -47,7 +46,7 @@ class MediaController < ApplicationController end def allow_iframing - response.headers['X-Frame-Options'] = 'ALLOWALL' + response.headers.delete('X-Frame-Options') end def set_pack diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index 3b228722f3..8d480d704e 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -6,7 +6,6 @@ class MediaProxyController < ApplicationController include Redisable include Lockable - skip_before_action :store_current_location skip_before_action :require_functional! before_action :authenticate_user!, if: :whitelist_mode? @@ -17,13 +16,13 @@ class MediaProxyController < ApplicationController rescue_from HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, with: :internal_server_error def show - with_lock("media_download:#{params[:id]}") do + with_redis_lock("media_download:#{params[:id]}") do @media_attachment = MediaAttachment.remote.attached.find(params[:id]) authorize @media_attachment.status, :show? redownload! if @media_attachment.needs_redownload? && !reject_media? end - redirect_to full_asset_url(@media_attachment.file.url(version)) + redirect_to full_asset_url(@media_attachment.file.url(version)), allow_other_host: true end private diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index d6e7d0800c..62fc9c1b0d 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -39,6 +39,6 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController end def set_cache_headers - response.headers['Cache-Control'] = 'private, no-store' + response.cache_control.replace(private: true, no_store: true) end end diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index b2564a7915..0a1df55066 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -8,6 +8,9 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio before_action :set_pack before_action :require_not_suspended!, only: :destroy before_action :set_body_classes + before_action :set_cache_headers + + before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json } skip_before_action :require_functional! @@ -35,4 +38,18 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio def require_not_suspended! forbidden if current_account.suspended? end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end + + def set_last_used_at_by_app + @last_used_at_by_app = Doorkeeper::AccessToken + .select('DISTINCT ON (application_id) application_id, last_used_at') + .where(resource_owner_id: current_resource_owner.id) + .where.not(last_used_at: nil) + .order(application_id: :desc, last_used_at: :desc) + .pluck(:application_id, :last_used_at) + .to_h + end end diff --git a/app/controllers/privacy_controller.rb b/app/controllers/privacy_controller.rb index 2c98bf3bf4..070ee8a06a 100644 --- a/app/controllers/privacy_controller.rb +++ b/app/controllers/privacy_controller.rb @@ -8,7 +8,7 @@ class PrivacyController < ApplicationController before_action :set_instance_presenter def show - expires_in 0, public: true if current_account.nil? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? end private diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb index d40770726c..f83098f731 100644 --- a/app/controllers/relationships_controller.rb +++ b/app/controllers/relationships_controller.rb @@ -8,6 +8,7 @@ class RelationshipsController < ApplicationController before_action :set_pack before_action :set_relationships, only: :show before_action :set_body_classes + before_action :set_cache_headers helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship? @@ -20,6 +21,8 @@ class RelationshipsController < ApplicationController @form.save rescue ActionController::ParameterMissing # Do nothing + rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound + flash[:alert] = I18n.t('relationships.follow_failure') if action_from_button == 'follow' ensure redirect_to relationships_path(filter_params) end @@ -61,8 +64,8 @@ class RelationshipsController < ApplicationController 'unfollow' elsif params[:remove_from_followers] 'remove_from_followers' - elsif params[:block_domains] - 'block_domains' + elsif params[:block_domains] || params[:remove_domains_from_followers] + 'remove_domains_from_followers' end end @@ -73,4 +76,8 @@ class RelationshipsController < ApplicationController def set_pack use_pack 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/settings/applications_controller.rb b/app/controllers/settings/applications_controller.rb index e6e137c2bc..d4b7205681 100644 --- a/app/controllers/settings/applications_controller.rb +++ b/app/controllers/settings/applications_controller.rb @@ -8,6 +8,8 @@ class Settings::ApplicationsController < Settings::BaseController @applications = current_user.applications.order(id: :desc).page(params[:page]) end + def show; end + def new @application = Doorkeeper::Application.new( redirect_uri: Doorkeeper.configuration.native_redirect_uri, @@ -15,8 +17,6 @@ class Settings::ApplicationsController < Settings::BaseController ) end - def show; end - def create @application = current_user.applications.build(application_params) diff --git a/app/controllers/settings/base_controller.rb b/app/controllers/settings/base_controller.rb index bf17b918cc..56aeb49aa0 100644 --- a/app/controllers/settings/base_controller.rb +++ b/app/controllers/settings/base_controller.rb @@ -19,7 +19,7 @@ class Settings::BaseController < ApplicationController end def set_cache_headers - response.headers['Cache-Control'] = 'private, no-store' + response.cache_control.replace(private: true, no_store: true) end def require_not_suspended! diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb index deaa7940eb..46a340aeb3 100644 --- a/app/controllers/settings/exports_controller.rb +++ b/app/controllers/settings/exports_controller.rb @@ -15,7 +15,7 @@ class Settings::ExportsController < Settings::BaseController def create backup = nil - with_lock("backup:#{current_user.id}") do + with_redis_lock("backup:#{current_user.id}") do authorize :backup, :create? backup = current_user.backups.create! end diff --git a/app/controllers/settings/flavours_controller.rb b/app/controllers/settings/flavours_controller.rb index 62c52eee97..b179b9429f 100644 --- a/app/controllers/settings/flavours_controller.rb +++ b/app/controllers/settings/flavours_controller.rb @@ -12,27 +12,15 @@ class Settings::FlavoursController < Settings::BaseController end def show - unless Themes.instance.flavours.include?(params[:flavour]) || (params[:flavour] == current_flavour) - redirect_to action: 'show', flavour: current_flavour - end + redirect_to action: 'show', flavour: current_flavour unless Themes.instance.flavours.include?(params[:flavour]) || (params[:flavour] == current_flavour) @listing = Themes.instance.flavours @selected = params[:flavour] end def update - user_settings.update(user_settings_params) + current_user.settings.update(flavour: params.require(:flavour), skin: params.dig(:user, :setting_skin)) + current_user.save redirect_to action: 'show', flavour: params[:flavour] end - - private - - def user_settings - UserSettingsDecorator.new(current_user) - end - - def user_settings_params - { setting_flavour: params.require(:flavour), - setting_skin: params.dig(:user, :setting_skin) }.with_indifferent_access - end end diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb index d4516526ee..983caf22fa 100644 --- a/app/controllers/settings/imports_controller.rb +++ b/app/controllers/settings/imports_controller.rb @@ -1,31 +1,101 @@ # frozen_string_literal: true -class Settings::ImportsController < Settings::BaseController - before_action :set_account +require 'csv' - def show - @import = Import.new +class Settings::ImportsController < Settings::BaseController + before_action :set_bulk_import, only: [:show, :confirm, :destroy] + before_action :set_recent_imports, only: [:index] + + TYPE_TO_FILENAME_MAP = { + following: 'following_accounts_failures.csv', + blocking: 'blocked_accounts_failures.csv', + muting: 'muted_accounts_failures.csv', + domain_blocking: 'blocked_domains_failures.csv', + bookmarks: 'bookmarks_failures.csv', + lists: 'lists_failures.csv', + }.freeze + + TYPE_TO_HEADERS_MAP = { + following: ['Account address', 'Show boosts', 'Notify on new posts', 'Languages'], + blocking: false, + muting: ['Account address', 'Hide notifications'], + domain_blocking: false, + bookmarks: false, + lists: false, + }.freeze + + def index + @import = Form::Import.new(current_account: current_account) + end + + def show; end + + def failures + @bulk_import = current_account.bulk_imports.where(state: :finished).find(params[:id]) + + respond_to do |format| + format.csv do + filename = TYPE_TO_FILENAME_MAP[@bulk_import.type.to_sym] + headers = TYPE_TO_HEADERS_MAP[@bulk_import.type.to_sym] + + export_data = CSV.generate(headers: headers, write_headers: true) do |csv| + @bulk_import.rows.find_each do |row| + case @bulk_import.type.to_sym + when :following + csv << [row.data['acct'], row.data.fetch('show_reblogs', true), row.data.fetch('notify', false), row.data['languages']&.join(', ')] + when :blocking + csv << [row.data['acct']] + when :muting + csv << [row.data['acct'], row.data.fetch('hide_notifications', true)] + when :domain_blocking + csv << [row.data['domain']] + when :bookmarks + csv << [row.data['uri']] + when :lists + csv << [row.data['list_name'], row.data['acct']] + end + end + end + + send_data export_data, filename: filename + end + end + end + + def confirm + @bulk_import.update!(state: :scheduled) + BulkImportWorker.perform_async(@bulk_import.id) + redirect_to settings_imports_path, notice: I18n.t('imports.success') end def create - @import = Import.new(import_params) - @import.account = @account + @import = Form::Import.new(import_params.merge(current_account: current_account)) if @import.save - ImportWorker.perform_async(@import.id) - redirect_to settings_import_path, notice: I18n.t('imports.success') + redirect_to settings_import_path(@import.bulk_import.id) else - render :show + # We need to set recent imports as we are displaying the index again + set_recent_imports + render :index end end + def destroy + @bulk_import.destroy! + redirect_to settings_imports_path + end + private - def set_account - @account = current_user.account + def import_params + params.require(:form_import).permit(:data, :type, :mode) end - def import_params - params.require(:import).permit(:data, :type, :mode) + def set_bulk_import + @bulk_import = current_account.bulk_imports.where(state: :unconfirmed).find(params[:id]) + end + + def set_recent_imports + @recent_imports = current_account.bulk_imports.reorder(id: :desc).limit(10) end end diff --git a/app/controllers/settings/preferences/appearance_controller.rb b/app/controllers/settings/preferences/appearance_controller.rb index 80ea57bd2d..4d7d12bb7f 100644 --- a/app/controllers/settings/preferences/appearance_controller.rb +++ b/app/controllers/settings/preferences/appearance_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Settings::Preferences::AppearanceController < Settings::PreferencesController +class Settings::Preferences::AppearanceController < Settings::Preferences::BaseController private def after_update_redirect_path diff --git a/app/controllers/settings/preferences/base_controller.rb b/app/controllers/settings/preferences/base_controller.rb new file mode 100644 index 0000000000..c1f8b49898 --- /dev/null +++ b/app/controllers/settings/preferences/base_controller.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class Settings::Preferences::BaseController < Settings::BaseController + def show; end + + def update + if current_user.update(user_params) + I18n.locale = current_user.locale + redirect_to after_update_redirect_path, notice: I18n.t('generic.changes_saved_msg') + else + render :show + end + end + + private + + def after_update_redirect_path + raise 'Override in controller' + end + + def user_params + params.require(:user).permit(:locale, :time_zone, chosen_languages: [], settings_attributes: UserSettings.keys) + end +end diff --git a/app/controllers/settings/preferences/notifications_controller.rb b/app/controllers/settings/preferences/notifications_controller.rb index a16ae6a672..66d6c9a2f7 100644 --- a/app/controllers/settings/preferences/notifications_controller.rb +++ b/app/controllers/settings/preferences/notifications_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Settings::Preferences::NotificationsController < Settings::PreferencesController +class Settings::Preferences::NotificationsController < Settings::Preferences::BaseController private def after_update_redirect_path diff --git a/app/controllers/settings/preferences/other_controller.rb b/app/controllers/settings/preferences/other_controller.rb index 07eb89a762..a19fbf5c48 100644 --- a/app/controllers/settings/preferences/other_controller.rb +++ b/app/controllers/settings/preferences/other_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Settings::Preferences::OtherController < Settings::PreferencesController +class Settings::Preferences::OtherController < Settings::Preferences::BaseController private def after_update_redirect_path diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb deleted file mode 100644 index 4c13364369..0000000000 --- a/app/controllers/settings/preferences_controller.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -class Settings::PreferencesController < Settings::BaseController - def show; end - - def update - user_settings.update(user_settings_params.to_h) - - if current_user.update(user_params) - I18n.locale = current_user.locale - redirect_to after_update_redirect_path, notice: I18n.t('generic.changes_saved_msg') - else - render :show - end - end - - private - - def after_update_redirect_path - settings_preferences_path - end - - def user_settings - UserSettingsDecorator.new(current_user) - end - - def user_params - params.require(:user).permit( - :locale, - chosen_languages: [] - ) - end - - def user_settings_params - params.require(:user).permit( - :setting_default_privacy, - :setting_default_sensitive, - :setting_default_language, - :setting_unfollow_modal, - :setting_boost_modal, - :setting_favourite_modal, - :setting_delete_modal, - :setting_auto_play_gif, - :setting_display_media, - :setting_expand_spoilers, - :setting_reduce_motion, - :setting_disable_swiping, - :setting_system_font_ui, - :setting_system_emoji_font, - :setting_noindex, - :setting_hide_followers_count, - :setting_aggregate_reblogs, - :setting_show_application, - :setting_advanced_layout, - :setting_default_content_type, - :setting_use_blurhash, - :setting_use_pending_items, - :setting_trends, - :setting_crop_images, - :setting_always_send_emails, - notification_emails: %i(follow follow_request reblog favourite mention report pending_account trending_tag trending_link trending_status appeal), - interactions: %i(must_be_follower must_be_following must_be_following_dm) - ) - end -end diff --git a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb index cbba842a98..0bff01ec27 100644 --- a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb +++ b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb @@ -22,18 +22,9 @@ module Settings private - def confirmation_params - params.require(:form_two_factor_confirmation).permit(:otp_attempt) - end - def verify_otp_not_enabled redirect_to settings_two_factor_authentication_methods_path if current_user.otp_enabled? end - - def acceptable_code? - current_user.validate_and_consume_otp!(confirmation_params[:otp_attempt]) || - current_user.invalidate_otp_backup_code!(confirmation_params[:otp_attempt]) - end end end end diff --git a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb index 7e2d43dcd3..3f9e713572 100644 --- a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb +++ b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb @@ -8,9 +8,8 @@ module Settings before_action :require_otp_enabled before_action :require_webauthn_enabled, only: [:index, :destroy] - def new; end - def index; end + def new; end def options current_user.update(webauthn_id: WebAuthn.generate_user_id) unless current_user.webauthn_id @@ -27,7 +26,7 @@ module Settings session[:webauthn_challenge] = options_for_create.challenge - render json: options_for_create, status: :ok + render json: options_for_create, status: 200 end def create @@ -52,7 +51,7 @@ module Settings end else flash[:error] = I18n.t('webauthn_credentials.create.error') - status = :internal_server_error + status = :unprocessable_entity end else flash[:error] = t('webauthn_credentials.create.error') diff --git a/app/controllers/settings/verifications_controller.rb b/app/controllers/settings/verifications_controller.rb new file mode 100644 index 0000000000..fc4f23bb18 --- /dev/null +++ b/app/controllers/settings/verifications_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class Settings::VerificationsController < Settings::BaseController + before_action :set_account + + def show + @verified_links = @account.fields.select(&:verified?) + end + + private + + def set_account + @account = current_account + end +end diff --git a/app/controllers/statuses_cleanup_controller.rb b/app/controllers/statuses_cleanup_controller.rb index 0e7bb835f5..3ed1860a00 100644 --- a/app/controllers/statuses_cleanup_controller.rb +++ b/app/controllers/statuses_cleanup_controller.rb @@ -7,6 +7,7 @@ class StatusesCleanupController < ApplicationController before_action :set_policy before_action :set_body_classes before_action :set_pack + before_action :set_cache_headers def show; end @@ -41,4 +42,8 @@ class StatusesCleanupController < ApplicationController def set_body_classes @body_classes = 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index e5221df3a2..0efafb8456 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -6,14 +6,16 @@ class StatusesController < ApplicationController include Authorization include AccountOwnedConcern + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_status before_action :set_instance_presenter - before_action :set_link_headers before_action :redirect_to_original, only: :show - before_action :set_cache_headers before_action :set_body_classes, only: :embed + after_action :set_link_headers + skip_around_action :set_locale, if: -> { request.format == :json } skip_before_action :require_functional!, only: [:show, :embed], unless: :whitelist_mode? @@ -28,7 +30,7 @@ class StatusesController < ApplicationController end format.json do - expires_in 3.minutes, public: @status.distributable? && public_fetch_mode? + expires_in 3.minutes, public: true if @status.distributable? && public_fetch_mode? render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter end end @@ -44,7 +46,7 @@ class StatusesController < ApplicationController return not_found if @status.hidden? || @status.reblog? expires_in 180, public: true - response.headers['X-Frame-Options'] = 'ALLOWALL' + response.headers.delete('X-Frame-Options') render layout: 'embedded' end @@ -71,6 +73,6 @@ class StatusesController < ApplicationController end def redirect_to_original - redirect_to ActivityPub::TagManager.instance.url_for(@status.reblog) if @status.reblog? + redirect_to(ActivityPub::TagManager.instance.url_for(@status.reblog), allow_other_host: true) if @status.reblog? end end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 750e15fa31..7e249dbea5 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -7,6 +7,8 @@ class TagsController < ApplicationController PAGE_SIZE = 20 PAGE_SIZE_MAX = 200 + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :authenticate_user!, if: :whitelist_mode? before_action :set_local @@ -19,7 +21,7 @@ class TagsController < ApplicationController def show respond_to do |format| format.html do - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in? end format.rss do @@ -58,7 +60,7 @@ class TagsController < ApplicationController def collection_presenter ActivityPub::CollectionPresenter.new( id: tag_url(@tag), - type: :ordered, + type: :ordered ) end end diff --git a/app/controllers/well_known/host_meta_controller.rb b/app/controllers/well_known/host_meta_controller.rb index 2fd6bc7cc9..201da9fbc3 100644 --- a/app/controllers/well_known/host_meta_controller.rb +++ b/app/controllers/well_known/host_meta_controller.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true module WellKnown - class HostMetaController < ActionController::Base + class HostMetaController < ActionController::Base # rubocop:disable Rails/ApplicationController include RoutingHelper - before_action { response.headers['Vary'] = 'Accept' } - def show @webfinger_template = "#{webfinger_url}?resource={uri}" expires_in 3.days, public: true diff --git a/app/controllers/well_known/nodeinfo_controller.rb b/app/controllers/well_known/nodeinfo_controller.rb index 11a699ebc8..e20e8c62a0 100644 --- a/app/controllers/well_known/nodeinfo_controller.rb +++ b/app/controllers/well_known/nodeinfo_controller.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true module WellKnown - class NodeInfoController < ActionController::Base + class NodeInfoController < ActionController::Base # rubocop:disable Rails/ApplicationController include CacheConcern - before_action { response.headers['Vary'] = 'Accept' } + # Prevent `active_model_serializer`'s `ActionController::Serialization` from calling `current_user` + # and thus re-issuing session cookies + serialization_scope nil def index expires_in 3.days, public: true diff --git a/app/controllers/well_known/webfinger_controller.rb b/app/controllers/well_known/webfinger_controller.rb index 2b296ea3be..0d897e8e24 100644 --- a/app/controllers/well_known/webfinger_controller.rb +++ b/app/controllers/well_known/webfinger_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module WellKnown - class WebfingerController < ActionController::Base + class WebfingerController < ActionController::Base # rubocop:disable Rails/ApplicationController include RoutingHelper before_action :set_account @@ -18,7 +18,14 @@ module WellKnown private def set_account - @account = Account.find_local!(username_from_resource) + username = username_from_resource + @account = begin + if username == Rails.configuration.x.local_domain + Account.representative + else + Account.find_local!(username) + end + end end def username_from_resource @@ -34,7 +41,12 @@ module WellKnown end def check_account_suspension - expires_in(3.minutes, public: true) && gone if @account.suspended_permanently? + gone if @account.suspended_permanently? + end + + def gone + expires_in(3.minutes, public: true) + head 410 end def bad_request @@ -46,9 +58,5 @@ module WellKnown expires_in(3.minutes, public: true) head 404 end - - def gone - head 410 - end end end diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb index e15aee6df1..b8277ee17e 100644 --- a/app/helpers/accounts_helper.rb +++ b/app/helpers/accounts_helper.rb @@ -28,7 +28,7 @@ module AccountsHelper end def hide_followers_count?(account) - Setting.hide_followers_count || account.user&.setting_hide_followers_count + Setting.hide_followers_count || account.user&.settings&.[]('hide_followers_count') end def account_description(account) diff --git a/app/helpers/admin/announcements_helper.rb b/app/helpers/admin/announcements_helper.rb deleted file mode 100644 index 0c053ddec3..0000000000 --- a/app/helpers/admin/announcements_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module Admin::AnnouncementsHelper - def time_range(announcement) - if announcement.all_day? - safe_join([l(announcement.starts_at.to_date), ' - ', l(announcement.ends_at.to_date)]) - else - safe_join([l(announcement.starts_at), ' - ', l(announcement.ends_at)]) - end - end -end diff --git a/app/helpers/admin/dashboard_helper.rb b/app/helpers/admin/dashboard_helper.rb index c21d413419..6096ff1381 100644 --- a/app/helpers/admin/dashboard_helper.rb +++ b/app/helpers/admin/dashboard_helper.rb @@ -19,19 +19,17 @@ module Admin::DashboardHelper end def relevant_account_timestamp(account) - timestamp, exact = begin - if account.user_current_sign_in_at && account.user_current_sign_in_at < 24.hours.ago - [account.user_current_sign_in_at, true] - elsif account.user_current_sign_in_at - [account.user_current_sign_in_at, false] - elsif account.user_pending? - [account.user_created_at, true] - elsif account.last_status_at.present? - [account.last_status_at, true] - else - [nil, false] - end - end + timestamp, exact = if account.user_current_sign_in_at && account.user_current_sign_in_at < 24.hours.ago + [account.user_current_sign_in_at, true] + elsif account.user_current_sign_in_at + [account.user_current_sign_in_at, false] + elsif account.user_pending? + [account.user_created_at, true] + elsif account.last_status_at.present? + [account.last_status_at, true] + else + [nil, false] + end return '-' if timestamp.nil? return t('generic.today') unless exact diff --git a/app/helpers/admin/trends/statuses_helper.rb b/app/helpers/admin/trends/statuses_helper.rb index 214c1e2a68..79fee44dc4 100644 --- a/app/helpers/admin/trends/statuses_helper.rb +++ b/app/helpers/admin/trends/statuses_helper.rb @@ -2,13 +2,11 @@ module Admin::Trends::StatusesHelper def one_line_preview(status) - text = begin - if status.local? - status.text.split("\n").first - else - Nokogiri::HTML(status.text).css('html > body > *').first&.text - end - end + text = if status.local? + status.text.split("\n").first + else + Nokogiri::HTML(status.text).css('html > body > *').first&.text + end return '' if text.blank? diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 7746436256..3148756b75 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -32,10 +32,6 @@ module ApplicationHelper paths.any? { |path| current_page?(path) } ? 'active' : '' end - def active_link_to(label, path, **options) - link_to label, path, options.merge(class: active_nav_class(path)) - end - def show_landing_strip? !user_signed_in? && !single_user_mode? end @@ -56,7 +52,7 @@ module ApplicationHelper if closed_registrations? || omniauth_only? 'https://joinmastodon.org/#getting-started' else - new_user_registration_path + ENV.fetch('SSO_ACCOUNT_SIGN_UP', new_user_registration_path) end end @@ -105,17 +101,22 @@ module ApplicationHelper def can?(action, record) return false if record.nil? + policy(record).public_send("#{action}?") end def fa_icon(icon, attributes = {}) class_names = attributes[:class]&.split(' ') || [] class_names << 'fa' - class_names += icon.split(' ').map { |cl| "fa-#{cl}" } + class_names += icon.split.map { |cl| "fa-#{cl}" } content_tag(:i, nil, attributes.merge(class: class_names.join(' '))) end + def check_icon + content_tag(:svg, tag.path('fill-rule': 'evenodd', 'clip-rule': 'evenodd', d: 'M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z'), xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 20 20', fill: 'currentColor') + end + def visibility_icon(status) if status.public_visibility? fa_icon('globe', title: I18n.t('statuses.visibilities.public')) @@ -142,34 +143,22 @@ module ApplicationHelper if prefers_autoplay? image_tag(custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:") else - image_tag(custom_emoji.image.url(:static), class: 'emojione custom-emoji', alt: ":#{custom_emoji.shortcode}", 'data-original' => full_asset_url(custom_emoji.image.url), 'data-static' => full_asset_url(custom_emoji.image.url(:static))) + image_tag(custom_emoji.image.url(:static), :class => 'emojione custom-emoji', :alt => ":#{custom_emoji.shortcode}", 'data-original' => full_asset_url(custom_emoji.image.url), 'data-static' => full_asset_url(custom_emoji.image.url(:static))) end end def opengraph(property, content) - tag(:meta, content: content, property: property) - end - - def react_component(name, props = {}, &block) - if block.nil? - content_tag(:div, nil, data: { component: name.to_s.camelcase, props: Oj.dump(props) }) - else - content_tag(:div, data: { component: name.to_s.camelcase, props: Oj.dump(props) }, &block) - end - end - - def react_admin_component(name, props = {}) - content_tag(:div, nil, data: { 'admin-component': name.to_s.camelcase, props: Oj.dump({ locale: I18n.locale }.merge(props)) }) + tag.meta(content: content, property: property) end def body_classes - output = (@body_classes || '').split(' ') + output = body_class_string.split output << "flavour-#{current_flavour.parameterize}" output << "skin-#{current_skin.parameterize}" output << 'system-font' if current_account&.user&.setting_system_font_ui output << (current_account&.user&.setting_reduce_motion ? 'reduce-motion' : 'no-reduce-motion') output << 'rtl' if locale_direction == 'rtl' - output.reject(&:blank?).join(' ') + output.compact_blank.join(' ') end def cdn_host @@ -181,11 +170,11 @@ module ApplicationHelper end def storage_host - "https://#{ENV['S3_ALIAS_HOST'].presence || ENV['S3_CLOUDFRONT_HOST']}" + "https://#{storage_host_var}" end def storage_host? - ENV['S3_ALIAS_HOST'].present? || ENV['S3_CLOUDFRONT_HOST'].present? + storage_host_var.present? end def quote_wrap(text, line_width: 80, break_sequence: "\n") @@ -217,9 +206,7 @@ module ApplicationHelper state_params[:moved_to_account] = current_account.moved_to_account end - if single_user_mode? - state_params[:owner] = Account.local.without_suspended.where('id > 0').first - end + state_params[:owner] = Account.local.without_suspended.where('id > 0').first if single_user_mode? json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json # rubocop:disable Rails/OutputSafety @@ -245,4 +232,10 @@ module ApplicationHelper def prerender_custom_emojis(html, custom_emojis, other_options = {}) EmojiFormatter.new(html, custom_emojis, other_options.merge(animate: prefers_autoplay?)).to_s end + + private + + def storage_host_var + ENV.fetch('S3_ALIAS_HOST', nil) || ENV.fetch('S3_CLOUDFRONT_HOST', nil) + end end diff --git a/app/helpers/branding_helper.rb b/app/helpers/branding_helper.rb index ad7702aea7..2b9c233c23 100644 --- a/app/helpers/branding_helper.rb +++ b/app/helpers/branding_helper.rb @@ -11,11 +11,11 @@ module BrandingHelper end def _logo_as_symbol_wordmark - content_tag(:svg, tag(:use, href: '#logo-symbol-wordmark'), viewBox: '0 0 261 66', class: 'logo logo--wordmark') + content_tag(:svg, tag.use(href: '#logo-symbol-wordmark'), viewBox: '0 0 261 66', class: 'logo logo--wordmark') end def _logo_as_symbol_icon - content_tag(:svg, tag(:use, href: '#logo-symbol-icon'), viewBox: '0 0 79 79', class: 'logo logo--icon') + content_tag(:svg, tag.use(href: '#logo-symbol-icon'), viewBox: '0 0 79 79', class: 'logo logo--icon') end def render_logo @@ -23,14 +23,12 @@ module BrandingHelper end def render_symbol(version = :icon) - path = begin - case version - when :icon - 'logo-symbol-icon.svg' - when :wordmark - 'logo-symbol-wordmark.svg' - end - end + path = case version + when :icon + 'logo-symbol-icon.svg' + when :wordmark + 'logo-symbol-wordmark.svg' + end render(file: Rails.root.join('app', 'javascript', 'images', path)).html_safe # rubocop:disable Rails/OutputSafety end diff --git a/app/helpers/domain_control_helper.rb b/app/helpers/domain_control_helper.rb index ac60cad295..ffcf375ea7 100644 --- a/app/helpers/domain_control_helper.rb +++ b/app/helpers/domain_control_helper.rb @@ -4,13 +4,11 @@ module DomainControlHelper def domain_not_allowed?(uri_or_domain) return if uri_or_domain.blank? - domain = begin - if uri_or_domain.include?('://') - Addressable::URI.parse(uri_or_domain).host - else - uri_or_domain - end - end + domain = if uri_or_domain.include?('://') + Addressable::URI.parse(uri_or_domain).host + else + uri_or_domain + end if whitelist_mode? !DomainAllow.allowed?(domain) diff --git a/app/helpers/formatting_helper.rb b/app/helpers/formatting_helper.rb index 05c003037e..f44cf79730 100644 --- a/app/helpers/formatting_helper.rb +++ b/app/helpers/formatting_helper.rb @@ -21,30 +21,26 @@ module FormattingHelper def rss_status_content_format(status) html = status_content_format(status) - before_html = begin - if status.spoiler_text? - tag.p do - tag.strong do - I18n.t('rss.content_warning', locale: available_locale_or_nil(status.language) || I18n.default_locale) - end + before_html = if status.spoiler_text? + tag.p do + tag.strong do + I18n.t('rss.content_warning', locale: available_locale_or_nil(status.language) || I18n.default_locale) + end - status.spoiler_text - end + tag.hr - end - end + status.spoiler_text + end + tag.hr + end - after_html = begin - if status.preloadable_poll - tag.p do - safe_join( - status.preloadable_poll.options.map do |o| - tag.send(status.preloadable_poll.multiple? ? 'checkbox' : 'radio', o, disabled: true) - end, - tag.br - ) - end - end - end + after_html = if status.preloadable_poll + tag.p do + safe_join( + status.preloadable_poll.options.map do |o| + tag.send(status.preloadable_poll.multiple? ? 'checkbox' : 'radio', o, disabled: true) + end, + tag.br + ) + end + end prerender_custom_emojis( safe_join([before_html, html, after_html]), @@ -58,6 +54,10 @@ module FormattingHelper end def account_field_value_format(field, with_rel_me: true) - html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false) + if field.verified? && !field.account.local? + TextFormatter.shortened_link(field.value_for_verification) + else + html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false) + end end end diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb index f41104709e..c5b83326db 100644 --- a/app/helpers/home_helper.rb +++ b/app/helpers/home_helper.rb @@ -8,7 +8,7 @@ module HomeHelper end def account_link_to(account, button = '', path: nil) - content_tag(:div, class: 'account') do + content_tag(:div, class: 'account account--minimal') do content_tag(:div, class: 'account__wrapper') do section = if account.nil? content_tag(:div, class: 'account__display-name') do @@ -41,9 +41,9 @@ module HomeHelper def obscured_counter(count) if count <= 0 - 0 + '0' elsif count == 1 - 1 + '1' else '1+' end @@ -57,14 +57,6 @@ module HomeHelper end end - def optional_link_to(condition, path, options = {}, &block) - if condition - link_to(path, options, &block) - else - content_tag(:div, &block) - end - end - def sign_up_message if closed_registrations? t('auth.registration_closed', instance: site_hostname) diff --git a/app/helpers/instance_helper.rb b/app/helpers/instance_helper.rb index daacb535b6..893afdd51f 100644 --- a/app/helpers/instance_helper.rb +++ b/app/helpers/instance_helper.rb @@ -9,15 +9,17 @@ module InstanceHelper @site_hostname ||= Addressable::URI.parse("//#{Rails.configuration.x.local_domain}").display_uri.host end - def description_for_sign_up - prefix = begin - if @invite.present? - I18n.t('auth.description.prefix_invited_by_user', name: @invite.user.account.username) - else - I18n.t('auth.description.prefix_sign_up') - end - end + def description_for_sign_up(invite = nil) + safe_join([description_prefix(invite), I18n.t('auth.description.suffix')], ' ') + end - safe_join([prefix, I18n.t('auth.description.suffix')], ' ') + private + + def description_prefix(invite) + if invite.present? + I18n.t('auth.description.prefix_invited_by_user', name: invite.user.account.username) + else + I18n.t('auth.description.prefix_sign_up') + end end end diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index e5787fd471..ce3ff094f6 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -26,15 +26,13 @@ module JsonLdHelper # The url attribute can be a string, an array of strings, or an array of objects. # The objects could include a mimeType. Not-included mimeType means it's text/html. def url_to_href(value, preferred_type = nil) - single_value = begin - if value.is_a?(Array) && !value.first.is_a?(String) - value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) } - elsif value.is_a?(Array) - value.first - else - value - end - end + single_value = if value.is_a?(Array) && !value.first.is_a?(String) + value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) } + elsif value.is_a?(Array) + value.first + else + value + end if single_value.nil? || single_value.is_a?(String) single_value @@ -65,11 +63,11 @@ module JsonLdHelper uri.nil? || !uri.start_with?('http://', 'https://') end - def invalid_origin?(url) - return true if unsupported_uri_scheme?(url) + def non_matching_uri_hosts?(base_url, comparison_url) + return true if unsupported_uri_scheme?(comparison_url) - needle = Addressable::URI.parse(url).host - haystack = Addressable::URI.parse(@account.uri).host + needle = Addressable::URI.parse(comparison_url).host + haystack = Addressable::URI.parse(base_url).host !haystack.casecmp(needle).zero? end diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb index bb35ce08cd..840a18d3e6 100644 --- a/app/helpers/languages_helper.rb +++ b/app/helpers/languages_helper.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -# rubocop:disable Metrics/ModuleLength, Style/WordArray module LanguagesHelper ISO_639_1 = { @@ -200,7 +199,6 @@ module LanguagesHelper sma: ['Southern Sami', 'Åarjelsaemien Gïele'].freeze, smj: ['Lule Sami', 'Julevsámegiella'].freeze, szl: ['Silesian', 'ślůnsko godka'].freeze, - tai: ['Tai', 'ภาษาไท or ภาษาไต'].freeze, tok: ['Toki Pona', 'toki pona'].freeze, zba: ['Balaibalan', 'باليبلن'].freeze, zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze, @@ -274,4 +272,4 @@ module LanguagesHelper end end -# rubocop:enable Metrics/ModuleLength, Style/WordArray +# rubocop:enable Metrics/ModuleLength diff --git a/app/helpers/media_component_helper.rb b/app/helpers/media_component_helper.rb new file mode 100644 index 0000000000..a57d0b4b62 --- /dev/null +++ b/app/helpers/media_component_helper.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +module MediaComponentHelper + def render_video_component(status, **options) + video = status.ordered_media_attachments.first + + meta = video.file.meta || {} + + component_params = { + sensitive: sensitive_viewer?(status, current_account), + src: full_asset_url(video.file.url(:original)), + preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), + alt: video.description, + blurhash: video.blurhash, + frameRate: meta.dig('original', 'frame_rate'), + inline: true, + media: [ + serialize_media_attachment(video), + ].as_json, + }.merge(**options) + + react_component :video, component_params do + render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } + end + end + + def render_audio_component(status, **options) + audio = status.ordered_media_attachments.first + + meta = audio.file.meta || {} + + component_params = { + src: full_asset_url(audio.file.url(:original)), + poster: full_asset_url(audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url), + alt: audio.description, + backgroundColor: meta.dig('colors', 'background'), + foregroundColor: meta.dig('colors', 'foreground'), + accentColor: meta.dig('colors', 'accent'), + duration: meta.dig('original', 'duration'), + }.merge(**options) + + react_component :audio, component_params do + render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } + end + end + + def render_media_gallery_component(status, **options) + component_params = { + sensitive: sensitive_viewer?(status, current_account), + autoplay: prefers_autoplay?, + media: status.ordered_media_attachments.map { |a| serialize_media_attachment(a).as_json }, + }.merge(**options) + + react_component :media_gallery, component_params do + render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } + end + end + + def render_card_component(status, **options) + component_params = { + sensitive: sensitive_viewer?(status, current_account), + card: serialize_status_card(status).as_json, + }.merge(**options) + + react_component :card, component_params + end + + def render_poll_component(status, **options) + component_params = { + disabled: true, + poll: serialize_status_poll(status).as_json, + }.merge(**options) + + react_component :poll, component_params do + render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: prefers_autoplay? } + end + end + + private + + def serialize_media_attachment(attachment) + ActiveModelSerializers::SerializableResource.new( + attachment, + serializer: REST::MediaAttachmentSerializer + ) + end + + def serialize_status_card(status) + ActiveModelSerializers::SerializableResource.new( + status.preview_card, + serializer: REST::PreviewCardSerializer + ) + end + + def serialize_status_poll(status) + ActiveModelSerializers::SerializableResource.new( + status.preloadable_poll, + serializer: REST::PollSerializer, + scope: current_user, + scope_name: :current_user + ) + end + + def sensitive_viewer?(status, account) + if !account.nil? && account.id == status.account_id + status.sensitive + else + status.account.sensitized? || status.sensitive + end + end +end diff --git a/app/helpers/react_component_helper.rb b/app/helpers/react_component_helper.rb new file mode 100644 index 0000000000..ce616e8306 --- /dev/null +++ b/app/helpers/react_component_helper.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module ReactComponentHelper + def react_component(name, props = {}, &block) + data = { component: name.to_s.camelcase, props: Oj.dump(props) } + if block.nil? + div_tag_with_data(data) + else + content_tag(:div, data: data, &block) + end + end + + def react_admin_component(name, props = {}) + data = { 'admin-component': name.to_s.camelcase, props: Oj.dump(props) } + div_tag_with_data(data) + end + + private + + def div_tag_with_data(data) + content_tag(:div, nil, data: data) + end +end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 3d5592867c..889ca7f402 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -5,10 +5,6 @@ module SettingsHelper LanguagesHelper::SUPPORTED_LOCALES.keys end - def hash_to_object(hash) - HashObject.new(hash) - end - def session_device_icon(session) device = session.detection.device @@ -28,13 +24,4 @@ module SettingsHelper safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ') end end - - def picture_hint(hint, picture) - if picture.original_filename.nil? - hint - else - link = link_to t('generic.delete'), settings_profile_picture_path(picture.name.to_s), data: { method: :delete } - safe_join([hint, link], '
'.html_safe) - end - end end diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index d1e3fddafe..f1f1ea872e 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -51,14 +51,14 @@ module StatusesHelper end def status_description(status) - components = [[media_summary(status), status_text_summary(status)].reject(&:blank?).join(' · ')] + components = [[media_summary(status), status_text_summary(status)].compact_blank.join(' · ')] if status.spoiler_text.blank? components << status.text components << poll_summary(status) end - components.reject(&:blank?).join("\n\n") + components.compact_blank.join("\n\n") end def stream_link_target @@ -105,94 +105,10 @@ module StatusesHelper end end - def sensitized?(status, account) - if !account.nil? && account.id == status.account_id - status.sensitive - else - status.account.sensitized? || status.sensitive - end - end - def embedded_view? params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION end - def render_video_component(status, **options) - video = status.ordered_media_attachments.first - - meta = video.file.meta || {} - - component_params = { - sensitive: sensitized?(status, current_account), - src: full_asset_url(video.file.url(:original)), - preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), - alt: video.description, - blurhash: video.blurhash, - frameRate: meta.dig('original', 'frame_rate'), - inline: true, - media: [ - ActiveModelSerializers::SerializableResource.new(video, serializer: REST::MediaAttachmentSerializer), - ].as_json, - }.merge(**options) - - react_component :video, component_params do - render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } - end - end - - def render_audio_component(status, **options) - audio = status.ordered_media_attachments.first - - meta = audio.file.meta || {} - - component_params = { - src: full_asset_url(audio.file.url(:original)), - poster: full_asset_url(audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url), - alt: audio.description, - backgroundColor: meta.dig('colors', 'background'), - foregroundColor: meta.dig('colors', 'foreground'), - accentColor: meta.dig('colors', 'accent'), - duration: meta.dig('original', 'duration'), - }.merge(**options) - - react_component :audio, component_params do - render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } - end - end - - def render_media_gallery_component(status, **options) - component_params = { - sensitive: sensitized?(status, current_account), - autoplay: prefers_autoplay?, - media: status.ordered_media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }, - }.merge(**options) - - react_component :media_gallery, component_params do - render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } - end - end - - def render_card_component(status, **options) - component_params = { - sensitive: sensitized?(status, current_account), - maxDescription: 160, - card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json, - }.merge(**options) - - react_component :card, component_params - end - - def render_poll_component(status, **options) - component_params = { - disabled: true, - poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json, - }.merge(**options) - - react_component :poll, component_params do - render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: prefers_autoplay? } - end - end - def prefers_autoplay? ActiveModel::Type::Boolean.new.cast(params[:autoplay]) || current_user&.setting_auto_play_gif end diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js index ac1b2f95fb..97b2f4e309 100644 --- a/app/javascript/core/admin.js +++ b/app/javascript/core/admin.js @@ -2,6 +2,7 @@ import 'packs/public-path'; import { delegate } from '@rails/ujs'; + import ready from '../mastodon/ready'; const setAnnouncementEndsAttributes = (target) => { diff --git a/app/javascript/core/mailer.js b/app/javascript/core/mailer.js index a4b6d54464..a2ad5e73ac 100644 --- a/app/javascript/core/mailer.js +++ b/app/javascript/core/mailer.js @@ -1,3 +1,3 @@ -require('../styles/mailer.scss'); +import '../styles/mailer.scss'; require.context('../icons'); diff --git a/app/javascript/core/public.js b/app/javascript/core/public.js index 5c7a51f447..01b4157f8c 100644 --- a/app/javascript/core/public.js +++ b/app/javascript/core/public.js @@ -1,10 +1,8 @@ // This file will be loaded on public pages, regardless of theme. import 'packs/public-path'; -import ready from '../mastodon/ready'; -const { delegate } = require('@rails/ujs'); -const { length } = require('stringz'); +import { delegate } from '@rails/ujs'; const getProfileAvatarAnimationHandler = (swapTo) => { //animate avatar gifs on the profile page when moused over diff --git a/app/javascript/core/settings.js b/app/javascript/core/settings.js index d578463a33..40537377c0 100644 --- a/app/javascript/core/settings.js +++ b/app/javascript/core/settings.js @@ -1,9 +1,9 @@ // This file will be loaded on settings pages, regardless of theme. import 'packs/public-path'; +import { delegate } from '@rails/ujs'; import escapeTextContentForBrowser from 'escape-html'; -const { delegate } = require('@rails/ujs'); import emojify from '../mastodon/features/emoji/emoji'; diff --git a/app/javascript/core/theme.yml b/app/javascript/core/theme.yml index b9144e43aa..30676dcf58 100644 --- a/app/javascript/core/theme.yml +++ b/app/javascript/core/theme.yml @@ -16,4 +16,5 @@ pack: modal: public.js public: public.js settings: settings.js + sign_up: share: diff --git a/app/javascript/core/two_factor_authentication.js b/app/javascript/core/two_factor_authentication.js index f076cdf30a..e76700a480 100644 --- a/app/javascript/core/two_factor_authentication.js +++ b/app/javascript/core/two_factor_authentication.js @@ -1,6 +1,8 @@ import 'packs/public-path'; -import axios from 'axios'; + import * as WebAuthnJSON from '@github/webauthn-json'; +import axios from 'axios'; + import ready from '../mastodon/ready'; import 'regenerator-runtime/runtime'; diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js index 6b5b2ade5c..d4f18ff2d7 100644 --- a/app/javascript/flavours/glitch/actions/accounts.js +++ b/app/javascript/flavours/glitch/actions/accounts.js @@ -1,5 +1,6 @@ import api, { getLinks } from '../api'; -import { importAccount, importFetchedAccount, importFetchedAccounts } from './importer'; + +import { importFetchedAccount, importFetchedAccounts } from './importer'; export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST'; export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS'; @@ -81,7 +82,10 @@ export const PINNED_ACCOUNTS_FETCH_REQUEST = 'PINNED_ACCOUNTS_FETCH_REQUEST'; export const PINNED_ACCOUNTS_FETCH_SUCCESS = 'PINNED_ACCOUNTS_FETCH_SUCCESS'; export const PINNED_ACCOUNTS_FETCH_FAIL = 'PINNED_ACCOUNTS_FETCH_FAIL'; -export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY'; +export const PINNED_ACCOUNTS_SUGGESTIONS_FETCH_REQUEST = 'PINNED_ACCOUNTS_SUGGESTIONS_FETCH_REQUEST'; +export const PINNED_ACCOUNTS_SUGGESTIONS_FETCH_SUCCESS = 'PINNED_ACCOUNTS_SUGGESTIONS_FETCH_SUCCESS'; +export const PINNED_ACCOUNTS_SUGGESTIONS_FETCH_FAIL = 'PINNED_ACCOUNTS_SUGGESTIONS_FETCH_FAIL'; + export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR'; export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE'; @@ -841,6 +845,8 @@ export function fetchPinnedAccountsFail(error) { export function fetchPinnedAccountsSuggestions(q) { return (dispatch, getState) => { + dispatch(fetchPinnedAccountsSuggestionsRequest()); + const params = { q, resolve: false, @@ -850,19 +856,32 @@ export function fetchPinnedAccountsSuggestions(q) { api(getState).get('/api/v1/accounts/search', { params }).then(response => { dispatch(importFetchedAccounts(response.data)); - dispatch(fetchPinnedAccountsSuggestionsReady(q, response.data)); - }); + dispatch(fetchPinnedAccountsSuggestionsSuccess(q, response.data)); + }).catch(err => dispatch(fetchPinnedAccountsSuggestionsFail(err))); }; } -export function fetchPinnedAccountsSuggestionsReady(query, accounts) { +export function fetchPinnedAccountsSuggestionsRequest() { return { - type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY, + type: PINNED_ACCOUNTS_SUGGESTIONS_FETCH_REQUEST, + }; +} + +export function fetchPinnedAccountsSuggestionsSuccess(query, accounts) { + return { + type: PINNED_ACCOUNTS_SUGGESTIONS_FETCH_SUCCESS, query, accounts, }; } +export function fetchPinnedAccountsSuggestionsFail(error) { + return { + type: PINNED_ACCOUNTS_SUGGESTIONS_FETCH_FAIL, + error, + }; +} + export function clearPinnedAccountsSuggestions() { return { type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR, diff --git a/app/javascript/flavours/glitch/actions/announcements.js b/app/javascript/flavours/glitch/actions/announcements.js index 586dcfd337..339c5f3adc 100644 --- a/app/javascript/flavours/glitch/actions/announcements.js +++ b/app/javascript/flavours/glitch/actions/announcements.js @@ -1,4 +1,5 @@ import api from '../api'; + import { normalizeAnnouncement } from './importer/normalizer'; export const ANNOUNCEMENTS_FETCH_REQUEST = 'ANNOUNCEMENTS_FETCH_REQUEST'; diff --git a/app/javascript/flavours/glitch/actions/app.js b/app/javascript/flavours/glitch/actions/app.js deleted file mode 100644 index de2d93e292..0000000000 --- a/app/javascript/flavours/glitch/actions/app.js +++ /dev/null @@ -1,6 +0,0 @@ -export const APP_LAYOUT_CHANGE = 'APP_LAYOUT_CHANGE'; - -export const changeLayout = layout => ({ - type: APP_LAYOUT_CHANGE, - layout, -}); diff --git a/app/javascript/flavours/glitch/actions/app.ts b/app/javascript/flavours/glitch/actions/app.ts new file mode 100644 index 0000000000..6fbfc07f68 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/app.ts @@ -0,0 +1,9 @@ +import { createAction } from '@reduxjs/toolkit'; + +import type { LayoutType } from '../is_mobile'; + +interface ChangeLayoutPayload { + layout: LayoutType; +} +export const changeLayout = + createAction('APP_LAYOUT_CHANGE'); diff --git a/app/javascript/flavours/glitch/actions/blocks.js b/app/javascript/flavours/glitch/actions/blocks.js index 192aa3ce40..e293657ad3 100644 --- a/app/javascript/flavours/glitch/actions/blocks.js +++ b/app/javascript/flavours/glitch/actions/blocks.js @@ -1,4 +1,5 @@ import api, { getLinks } from '../api'; + import { fetchRelationships } from './accounts'; import { importFetchedAccounts } from './importer'; import { openModal } from './modal'; @@ -94,6 +95,6 @@ export function initBlockModal(account) { account, }); - dispatch(openModal('BLOCK')); + dispatch(openModal({ modalType: 'BLOCK' })); }; } diff --git a/app/javascript/flavours/glitch/actions/bookmarks.js b/app/javascript/flavours/glitch/actions/bookmarks.js index 3c8eec5468..0b16f61e63 100644 --- a/app/javascript/flavours/glitch/actions/bookmarks.js +++ b/app/javascript/flavours/glitch/actions/bookmarks.js @@ -1,4 +1,5 @@ import api, { getLinks } from '../api'; + import { importFetchedStatuses } from './importer'; export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST'; diff --git a/app/javascript/flavours/glitch/actions/boosts.js b/app/javascript/flavours/glitch/actions/boosts.js index c0f0f3acc5..1fc2e391e2 100644 --- a/app/javascript/flavours/glitch/actions/boosts.js +++ b/app/javascript/flavours/glitch/actions/boosts.js @@ -14,7 +14,10 @@ export function initBoostModal(props) { privacy, }); - dispatch(openModal('BOOST', props)); + dispatch(openModal({ + modalType: 'BOOST', + modalProps: props, + })); }; } diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 01f0f36668..9e0b123704 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -1,11 +1,14 @@ +import { defineMessages } from 'react-intl'; + import axios from 'axios'; import { throttle } from 'lodash'; -import { defineMessages } from 'react-intl'; + import api from 'flavours/glitch/api'; import { search as emojiSearch } from 'flavours/glitch/features/emoji/emoji_mart_search_light'; import { tagHistory } from 'flavours/glitch/settings'; import { recoverHashtags } from 'flavours/glitch/utils/hashtag'; import resizeImage from 'flavours/glitch/utils/resize_image'; + import { showAlert, showAlertForError } from './alerts'; import { useEmoji } from './emojis'; import { importFetchedAccounts, importFetchedStatus } from './importer'; @@ -186,11 +189,19 @@ export function submitCompose(routerHistory) { // API call. let media_attributes; if (statusId !== null) { - media_attributes = media.map(item => ({ - id: item.get('id'), - description: item.get('description'), - focus: item.get('focus'), - })); + media_attributes = media.map(item => { + let focus; + + if (item.getIn(['meta', 'focus'])) { + focus = `${item.getIn(['meta', 'focus', 'x']).toFixed(2)},${item.getIn(['meta', 'focus', 'y']).toFixed(2)}`; + } + + return { + id: item.get('id'), + description: item.get('description'), + focus, + }; + }); } api(getState).request({ @@ -405,7 +416,10 @@ export function initMediaEditModal(id) { id, }); - dispatch(openModal('FOCAL_POINT', { id })); + dispatch(openModal({ + modalType: 'FOCAL_POINT', + modalProps: { id }, + })); }; } @@ -433,16 +447,12 @@ export function changeUploadCompose(id, params) { // Editing already-attached media is deferred to editing the post itself. // For simplicity's sake, fake an API reply. if (media && !media.get('unattached')) { - let { description, focus } = params; - const data = media.toJS(); - - if (description) { - data.description = description; - } + const { focus, ...other } = params; + const data = { ...media.toJS(), ...other }; if (focus) { - focus = focus.split(','); - data.meta = { focus: { x: parseFloat(focus[0]), y: parseFloat(focus[1]) } }; + const [x, y] = focus.split(','); + data.meta = { focus: { x: parseFloat(x), y: parseFloat(y) } }; } dispatch(changeUploadComposeSuccess(data, true)); diff --git a/app/javascript/flavours/glitch/actions/conversations.js b/app/javascript/flavours/glitch/actions/conversations.js index 4ef654b1f9..8c4c4529fb 100644 --- a/app/javascript/flavours/glitch/actions/conversations.js +++ b/app/javascript/flavours/glitch/actions/conversations.js @@ -1,4 +1,5 @@ import api, { getLinks } from '../api'; + import { importFetchedAccounts, importFetchedStatuses, diff --git a/app/javascript/flavours/glitch/actions/directory.js b/app/javascript/flavours/glitch/actions/directory.js index 4b2b6dd56d..cda63f2b5a 100644 --- a/app/javascript/flavours/glitch/actions/directory.js +++ b/app/javascript/flavours/glitch/actions/directory.js @@ -1,6 +1,7 @@ import api from '../api'; -import { importFetchedAccounts } from './importer'; + import { fetchRelationships } from './accounts'; +import { importFetchedAccounts } from './importer'; export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST'; export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS'; diff --git a/app/javascript/flavours/glitch/actions/favourites.js b/app/javascript/flavours/glitch/actions/favourites.js index 7388e0c580..2d4d4e6206 100644 --- a/app/javascript/flavours/glitch/actions/favourites.js +++ b/app/javascript/flavours/glitch/actions/favourites.js @@ -1,4 +1,5 @@ import api, { getLinks } from '../api'; + import { importFetchedStatuses } from './importer'; export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST'; diff --git a/app/javascript/flavours/glitch/actions/filters.js b/app/javascript/flavours/glitch/actions/filters.js index e9c609fc87..a11956ac56 100644 --- a/app/javascript/flavours/glitch/actions/filters.js +++ b/app/javascript/flavours/glitch/actions/filters.js @@ -1,4 +1,5 @@ import api from '../api'; + import { openModal } from './modal'; export const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST'; @@ -14,9 +15,12 @@ export const FILTERS_CREATE_SUCCESS = 'FILTERS_CREATE_SUCCESS'; export const FILTERS_CREATE_FAIL = 'FILTERS_CREATE_FAIL'; export const initAddFilter = (status, { contextType }) => dispatch => - dispatch(openModal('FILTER', { - statusId: status?.get('id'), - contextType: contextType, + dispatch(openModal({ + modalType: 'FILTER', + modalProps: { + statusId: status?.get('id'), + contextType: contextType, + }, })); export const fetchFilters = () => (dispatch, getState) => { diff --git a/app/javascript/flavours/glitch/actions/history.js b/app/javascript/flavours/glitch/actions/history.js index c142aaf617..52401b7dce 100644 --- a/app/javascript/flavours/glitch/actions/history.js +++ b/app/javascript/flavours/glitch/actions/history.js @@ -1,4 +1,5 @@ import api from '../api'; + import { importFetchedAccounts } from './importer'; export const HISTORY_FETCH_REQUEST = 'HISTORY_FETCH_REQUEST'; diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index 1c9f524e43..540e6cba78 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -1,11 +1,12 @@ import escapeTextContentForBrowser from 'escape-html'; + import emojify from 'flavours/glitch/features/emoji/emoji'; -import { unescapeHTML } from 'flavours/glitch/utils/html'; import { autoHideCW } from 'flavours/glitch/utils/content_warning'; +import { unescapeHTML } from 'flavours/glitch/utils/html'; const domParser = new DOMParser(); -const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { +const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => { obj[`:${emoji.shortcode}:`] = emoji; return obj; }, {}); @@ -19,7 +20,7 @@ export function searchTextFromRawStatus (status) { export function normalizeAccount(account) { account = { ...account }; - const emojiMap = makeEmojiMap(account); + const emojiMap = makeEmojiMap(account.emojis); const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name; account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap); @@ -77,7 +78,7 @@ export function normalizeStatus(status, normalOldStatus, settings) { } else { const spoilerText = normalStatus.spoiler_text || ''; const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); - const emojiMap = makeEmojiMap(normalStatus); + const emojiMap = makeEmojiMap(normalStatus.emojis); normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); @@ -88,22 +89,48 @@ export function normalizeStatus(status, normalOldStatus, settings) { return normalStatus; } +export function normalizeStatusTranslation(translation, status) { + const emojiMap = makeEmojiMap(status.get('emojis').toJS()); + + const normalTranslation = { + detected_source_language: translation.detected_source_language, + language: translation.language, + provider: translation.provider, + contentHtml: emojify(translation.content, emojiMap), + spoilerHtml: emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap), + spoiler_text: translation.spoiler_text, + }; + + return normalTranslation; +} + export function normalizePoll(poll) { const normalPoll = { ...poll }; - const emojiMap = makeEmojiMap(normalPoll); + const emojiMap = makeEmojiMap(poll.emojis); normalPoll.options = poll.options.map((option, index) => ({ ...option, voted: poll.own_votes && poll.own_votes.includes(index), - title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap), + titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap), })); return normalPoll; } +export function normalizePollOptionTranslation(translation, poll) { + const emojiMap = makeEmojiMap(poll.get('emojis').toJS()); + + const normalTranslation = { + ...translation, + titleHtml: emojify(escapeTextContentForBrowser(translation.title), emojiMap), + }; + + return normalTranslation; +} + export function normalizeAnnouncement(announcement) { const normalAnnouncement = { ...announcement }; - const emojiMap = makeEmojiMap(normalAnnouncement); + const emojiMap = makeEmojiMap(normalAnnouncement.emojis); normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap); diff --git a/app/javascript/flavours/glitch/actions/interactions.js b/app/javascript/flavours/glitch/actions/interactions.js index c7b552a656..6b8864a039 100644 --- a/app/javascript/flavours/glitch/actions/interactions.js +++ b/app/javascript/flavours/glitch/actions/interactions.js @@ -1,4 +1,5 @@ import api from '../api'; + import { importFetchedAccounts, importFetchedStatus } from './importer'; export const REBLOG_REQUEST = 'REBLOG_REQUEST'; diff --git a/app/javascript/flavours/glitch/actions/lists.js b/app/javascript/flavours/glitch/actions/lists.js index 5ab9224363..b0789cd426 100644 --- a/app/javascript/flavours/glitch/actions/lists.js +++ b/app/javascript/flavours/glitch/actions/lists.js @@ -1,6 +1,7 @@ import api from '../api'; -import { importFetchedAccounts } from './importer'; + import { showAlertForError } from './alerts'; +import { importFetchedAccounts } from './importer'; export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST'; export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS'; @@ -150,10 +151,10 @@ export const createListFail = error => ({ error, }); -export const updateList = (id, title, shouldReset, replies_policy) => (dispatch, getState) => { +export const updateList = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => { dispatch(updateListRequest(id)); - api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy }).then(({ data }) => { + api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => { dispatch(updateListSuccess(data)); if (shouldReset) { diff --git a/app/javascript/flavours/glitch/actions/local_settings.js b/app/javascript/flavours/glitch/actions/local_settings.js index adf7fd2abc..f2878daa50 100644 --- a/app/javascript/flavours/glitch/actions/local_settings.js +++ b/app/javascript/flavours/glitch/actions/local_settings.js @@ -1,4 +1,5 @@ import { expandSpoilers, disableSwiping } from 'flavours/glitch/initial_state'; + import { openModal } from './modal'; export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE'; @@ -27,9 +28,12 @@ export function checkDeprecatedLocalSettings() { } if (changed_settings.length > 0) { - dispatch(openModal('DEPRECATED_SETTINGS', { - settings: changed_settings, - onConfirm: () => dispatch(clearDeprecatedLocalSettings()), + dispatch(openModal({ + modalType: 'DEPRECATED_SETTINGS', + modalProps: { + settings: changed_settings, + onConfirm: () => dispatch(clearDeprecatedLocalSettings()), + }, })); } }; diff --git a/app/javascript/flavours/glitch/actions/markers.js b/app/javascript/flavours/glitch/actions/markers.js index dfd701cbb8..ccb1b23d6f 100644 --- a/app/javascript/flavours/glitch/actions/markers.js +++ b/app/javascript/flavours/glitch/actions/markers.js @@ -1,8 +1,10 @@ -import api from '../api'; -import { debounce } from 'lodash'; -import compareId from '../compare_id'; import { List as ImmutableList } from 'immutable'; +import { debounce } from 'lodash'; + +import api from '../api'; +import { compareId } from '../compare_id'; + export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST'; export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS'; export const MARKERS_FETCH_FAIL = 'MARKERS_FETCH_FAIL'; @@ -55,7 +57,7 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => { client.open('POST', '/api/v1/markers', false); client.setRequestHeader('Content-Type', 'application/json'); client.setRequestHeader('Authorization', `Bearer ${accessToken}`); - client.SUBMIT(JSON.stringify(params)); + client.send(JSON.stringify(params)); } catch (e) { // Do not make the BeforeUnload handler error out } diff --git a/app/javascript/flavours/glitch/actions/modal.js b/app/javascript/flavours/glitch/actions/modal.js deleted file mode 100644 index ef2ae0e4c7..0000000000 --- a/app/javascript/flavours/glitch/actions/modal.js +++ /dev/null @@ -1,18 +0,0 @@ -export const MODAL_OPEN = 'MODAL_OPEN'; -export const MODAL_CLOSE = 'MODAL_CLOSE'; - -export function openModal(type, props) { - return { - type: MODAL_OPEN, - modalType: type, - modalProps: props, - }; -} - -export function closeModal(type, options = { ignoreFocus: false }) { - return { - type: MODAL_CLOSE, - modalType: type, - ignoreFocus: options.ignoreFocus, - }; -} diff --git a/app/javascript/flavours/glitch/actions/modal.ts b/app/javascript/flavours/glitch/actions/modal.ts new file mode 100644 index 0000000000..af34f5d6af --- /dev/null +++ b/app/javascript/flavours/glitch/actions/modal.ts @@ -0,0 +1,17 @@ +import { createAction } from '@reduxjs/toolkit'; + +import type { MODAL_COMPONENTS } from '../features/ui/components/modal_root'; + +export type ModalType = keyof typeof MODAL_COMPONENTS; + +interface OpenModalPayload { + modalType: ModalType; + modalProps: unknown; +} +export const openModal = createAction('MODAL_OPEN'); + +interface CloseModalPayload { + modalType: ModalType | undefined; + ignoreFocus: boolean; +} +export const closeModal = createAction('MODAL_CLOSE'); diff --git a/app/javascript/flavours/glitch/actions/mutes.js b/app/javascript/flavours/glitch/actions/mutes.js index aa47d14642..4af927d932 100644 --- a/app/javascript/flavours/glitch/actions/mutes.js +++ b/app/javascript/flavours/glitch/actions/mutes.js @@ -1,7 +1,9 @@ +import { openModal } from 'flavours/glitch/actions/modal'; + import api, { getLinks } from '../api'; + import { fetchRelationships } from './accounts'; import { importFetchedAccounts } from './importer'; -import { openModal } from 'flavours/glitch/actions/modal'; export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST'; export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS'; @@ -96,7 +98,7 @@ export function initMuteModal(account) { account, }); - dispatch(openModal('MUTE')); + dispatch(openModal({ modalType: 'MUTE' })); }; } diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index 989bc41444..a80746b756 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -1,5 +1,15 @@ +import { IntlMessageFormat } from 'intl-messageformat'; +import { defineMessages } from 'react-intl'; + +import { List as ImmutableList } from 'immutable'; + +import { compareId } from 'flavours/glitch/compare_id'; +import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state'; +import { unescapeHTML } from 'flavours/glitch/utils/html'; +import { requestNotificationPermission } from 'flavours/glitch/utils/notifications'; + import api, { getLinks } from '../api'; -import IntlMessageFormat from 'intl-messageformat'; + import { fetchFollowRequests, fetchRelationships } from './accounts'; import { importFetchedAccount, @@ -9,12 +19,9 @@ import { } from './importer'; import { submitMarkers } from './markers'; import { saveSettings } from './settings'; -import { defineMessages } from 'react-intl'; -import { List as ImmutableList } from 'immutable'; -import { unescapeHTML } from 'flavours/glitch/utils/html'; -import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state'; -import compareId from 'flavours/glitch/compare_id'; -import { requestNotificationPermission } from 'flavours/glitch/utils/notifications'; + + + export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; diff --git a/app/javascript/flavours/glitch/actions/onboarding.js b/app/javascript/flavours/glitch/actions/onboarding.js index 5038b7eb67..a4a525c427 100644 --- a/app/javascript/flavours/glitch/actions/onboarding.js +++ b/app/javascript/flavours/glitch/actions/onboarding.js @@ -6,7 +6,9 @@ export function showOnboardingOnce() { const alreadySeen = getState().getIn(['settings', 'onboarded']); if (!alreadySeen) { - dispatch(openModal('ONBOARDING')); + dispatch(openModal({ + modalType: 'ONBOARDING', + })); dispatch(changeSetting(['onboarded'], true)); dispatch(saveSettings()); } diff --git a/app/javascript/flavours/glitch/actions/picture_in_picture.js b/app/javascript/flavours/glitch/actions/picture_in_picture.js index 33d8d57d47..898375abeb 100644 --- a/app/javascript/flavours/glitch/actions/picture_in_picture.js +++ b/app/javascript/flavours/glitch/actions/picture_in_picture.js @@ -20,9 +20,10 @@ export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE'; * @param {string} accountId * @param {string} playerType * @param {MediaProps} props - * @return {object} + * @returns {object} */ export const deployPictureInPicture = (statusId, accountId, playerType, props) => { + // @ts-expect-error return (dispatch, getState) => { // Do not open a player for a toot that does not exist if (getState().hasIn(['statuses', statusId])) { diff --git a/app/javascript/flavours/glitch/actions/pin_statuses.js b/app/javascript/flavours/glitch/actions/pin_statuses.js index d8c0a13737..8aca199e97 100644 --- a/app/javascript/flavours/glitch/actions/pin_statuses.js +++ b/app/javascript/flavours/glitch/actions/pin_statuses.js @@ -1,12 +1,14 @@ +import { me } from 'flavours/glitch/initial_state'; + import api from '../api'; + import { importFetchedStatuses } from './importer'; + export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST'; export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS'; export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL'; -import { me } from 'flavours/glitch/initial_state'; - export function fetchPinnedStatuses() { return (dispatch, getState) => { dispatch(fetchPinnedStatusesRequest()); diff --git a/app/javascript/flavours/glitch/actions/polls.js b/app/javascript/flavours/glitch/actions/polls.js index 8e8b82df5d..a37410dc90 100644 --- a/app/javascript/flavours/glitch/actions/polls.js +++ b/app/javascript/flavours/glitch/actions/polls.js @@ -1,4 +1,5 @@ import api from '../api'; + import { importFetchedPoll } from './importer'; export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST'; diff --git a/app/javascript/flavours/glitch/actions/push_notifications/index.js b/app/javascript/flavours/glitch/actions/push_notifications/index.js index 9dcc4bd4bb..46b63867f1 100644 --- a/app/javascript/flavours/glitch/actions/push_notifications/index.js +++ b/app/javascript/flavours/glitch/actions/push_notifications/index.js @@ -1,5 +1,5 @@ -import { setAlerts } from './setter'; import { saveSettings } from './registerer'; +import { setAlerts } from './setter'; export function changeAlerts(path, value) { return dispatch => { diff --git a/app/javascript/flavours/glitch/actions/push_notifications/registerer.js b/app/javascript/flavours/glitch/actions/push_notifications/registerer.js index bc5634233f..336bbc6869 100644 --- a/app/javascript/flavours/glitch/actions/push_notifications/registerer.js +++ b/app/javascript/flavours/glitch/actions/push_notifications/registerer.js @@ -1,5 +1,6 @@ import api from '../../api'; import { pushNotificationsSetting } from '../../settings'; + import { setBrowserSupport, setSubscription, clearSubscription } from './setter'; // Taken from https://www.npmjs.com/package/web-push diff --git a/app/javascript/flavours/glitch/actions/reports.js b/app/javascript/flavours/glitch/actions/reports.js index fbe5b3791b..756b8cd05e 100644 --- a/app/javascript/flavours/glitch/actions/reports.js +++ b/app/javascript/flavours/glitch/actions/reports.js @@ -1,4 +1,5 @@ import api from '../api'; + import { openModal } from './modal'; export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST'; @@ -6,9 +7,12 @@ export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS'; export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL'; export const initReport = (account, status) => dispatch => - dispatch(openModal('REPORT', { - accountId: account.get('id'), - statusId: status?.get('id'), + dispatch(openModal({ + modalType: 'REPORT', + modalProps: { + accountId: account.get('id'), + statusId: status?.get('id'), + }, })); export const submitReport = (params, onSuccess, onFail) => (dispatch, getState) => { diff --git a/app/javascript/flavours/glitch/actions/search.js b/app/javascript/flavours/glitch/actions/search.js index 0012808e5b..d5154c6a84 100644 --- a/app/javascript/flavours/glitch/actions/search.js +++ b/app/javascript/flavours/glitch/actions/search.js @@ -1,4 +1,5 @@ import api from '../api'; + import { fetchRelationships } from './accounts'; import { importFetchedAccounts, importFetchedStatuses } from './importer'; diff --git a/app/javascript/flavours/glitch/actions/server.js b/app/javascript/flavours/glitch/actions/server.js index 31d4aea100..65f3efc3a7 100644 --- a/app/javascript/flavours/glitch/actions/server.js +++ b/app/javascript/flavours/glitch/actions/server.js @@ -1,10 +1,15 @@ import api from '../api'; + import { importFetchedAccount } from './importer'; export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST'; export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS'; export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL'; +export const SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST = 'SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST'; +export const SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS = 'SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS'; +export const SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL = 'SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL'; + export const EXTENDED_DESCRIPTION_REQUEST = 'EXTENDED_DESCRIPTION_REQUEST'; export const EXTENDED_DESCRIPTION_SUCCESS = 'EXTENDED_DESCRIPTION_SUCCESS'; export const EXTENDED_DESCRIPTION_FAIL = 'EXTENDED_DESCRIPTION_FAIL'; @@ -14,6 +19,10 @@ export const SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS = 'SERVER_DOMAIN_BLOCKS_FETCH_SU export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL'; export const fetchServer = () => (dispatch, getState) => { + if (getState().getIn(['server', 'server', 'isLoading'])) { + return; + } + dispatch(fetchServerRequest()); api(getState) @@ -37,7 +46,34 @@ const fetchServerFail = error => ({ error, }); +export const fetchServerTranslationLanguages = () => (dispatch, getState) => { + dispatch(fetchServerTranslationLanguagesRequest()); + + api(getState) + .get('/api/v1/instance/translation_languages').then(({ data }) => { + dispatch(fetchServerTranslationLanguagesSuccess(data)); + }).catch(err => dispatch(fetchServerTranslationLanguagesFail(err))); +}; + +const fetchServerTranslationLanguagesRequest = () => ({ + type: SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST, +}); + +const fetchServerTranslationLanguagesSuccess = translationLanguages => ({ + type: SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS, + translationLanguages, +}); + +const fetchServerTranslationLanguagesFail = error => ({ + type: SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL, + error, +}); + export const fetchExtendedDescription = () => (dispatch, getState) => { + if (getState().getIn(['server', 'extendedDescription', 'isLoading'])) { + return; + } + dispatch(fetchExtendedDescriptionRequest()); api(getState) @@ -61,6 +97,10 @@ const fetchExtendedDescriptionFail = error => ({ }); export const fetchDomainBlocks = () => (dispatch, getState) => { + if (getState().getIn(['server', 'domainBlocks', 'isLoading'])) { + return; + } + dispatch(fetchDomainBlocksRequest()); api(getState) diff --git a/app/javascript/flavours/glitch/actions/settings.js b/app/javascript/flavours/glitch/actions/settings.js index 60f0abf950..120ae133ed 100644 --- a/app/javascript/flavours/glitch/actions/settings.js +++ b/app/javascript/flavours/glitch/actions/settings.js @@ -1,5 +1,7 @@ -import api from '../api'; import { debounce } from 'lodash'; + +import api from '../api'; + import { showAlertForError } from './alerts'; export const SETTING_CHANGE = 'SETTING_CHANGE'; diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js index 487cd69884..5bdd31c343 100644 --- a/app/javascript/flavours/glitch/actions/statuses.js +++ b/app/javascript/flavours/glitch/actions/statuses.js @@ -1,8 +1,8 @@ import api from '../api'; -import { deleteFromTimelines } from './timelines'; -import { importFetchedStatus, importFetchedStatuses } from './importer'; import { ensureComposeIsVisible, setComposeToStatus } from './compose'; +import { importFetchedStatus, importFetchedStatuses } from './importer'; +import { deleteFromTimelines } from './timelines'; export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS'; @@ -344,7 +344,8 @@ export const translateStatusFail = (id, error) => ({ error, }); -export const undoStatusTranslation = id => ({ +export const undoStatusTranslation = (id, pollId) => ({ type: STATUS_TRANSLATE_UNDO, id, + pollId, }); diff --git a/app/javascript/flavours/glitch/actions/store.js b/app/javascript/flavours/glitch/actions/store.js index 137b68e222..e57b37a122 100644 --- a/app/javascript/flavours/glitch/actions/store.js +++ b/app/javascript/flavours/glitch/actions/store.js @@ -1,4 +1,5 @@ import { Iterable, fromJS } from 'immutable'; + import { hydrateCompose } from './compose'; import { importFetchedAccounts } from './importer'; import { saveSettings } from './settings'; diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index ffac1b2582..f1c44d2e29 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -1,6 +1,18 @@ // @ts-check +import { getLocale } from 'flavours/glitch/locales'; + import { connectStream } from '../stream'; + +import { + fetchAnnouncements, + updateAnnouncements, + updateReaction as updateAnnouncementsReaction, + deleteAnnouncement, +} from './announcements'; +import { updateConversations } from './conversations'; +import { updateNotifications, expandNotifications } from './notifications'; +import { updateStatus } from './statuses'; import { updateTimeline, deleteFromTimelines, @@ -12,22 +24,10 @@ import { fillCommunityTimelineGaps, fillListTimelineGaps, } from './timelines'; -import { updateNotifications, expandNotifications } from './notifications'; -import { updateConversations } from './conversations'; -import { updateStatus } from './statuses'; -import { - fetchAnnouncements, - updateAnnouncements, - updateReaction as updateAnnouncementsReaction, - deleteAnnouncement, -} from './announcements'; -import { getLocale } from 'mastodon/locales'; - -const { messages } = getLocale(); /** * @param {number} max - * @return {number} + * @returns {number} */ const randomUpTo = max => Math.floor(Math.random() * Math.floor(max)); @@ -40,19 +40,24 @@ const randomUpTo = max => * @param {function(Function, Function): void} [options.fallback] * @param {function(): void} [options.fillGaps] * @param {function(object): boolean} [options.accept] - * @return {function(): void} + * @returns {function(): void} */ -export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => - connectStream(channelName, params, (dispatch, getState) => { +export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => { + const { messages } = getLocale(); + + return connectStream(channelName, params, (dispatch, getState) => { const locale = getState().getIn(['meta', 'locale']); + // @ts-expect-error let pollingId; /** * @param {function(Function, Function): void} fallback */ + const useFallback = fallback => { fallback(dispatch, () => { + // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000)); }); }; @@ -61,6 +66,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti onConnect() { dispatch(connectTimeline(timelineId)); + // @ts-expect-error if (pollingId) { clearTimeout(pollingId); pollingId = null; @@ -75,6 +81,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti dispatch(disconnectTimeline(timelineId)); if (options.fallback) { + // @ts-expect-error pollingId = setTimeout(() => useFallback(options.fallback), randomUpTo(40000)); } }, @@ -82,24 +89,30 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti onReceive (data) { switch(data.event) { case 'update': + // @ts-expect-error dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept)); break; case 'status.update': + // @ts-expect-error dispatch(updateStatus(JSON.parse(data.payload))); break; case 'delete': dispatch(deleteFromTimelines(data.payload)); break; case 'notification': + // @ts-expect-error dispatch(updateNotifications(JSON.parse(data.payload), messages, locale)); break; case 'conversation': + // @ts-expect-error dispatch(updateConversations(JSON.parse(data.payload))); break; case 'announcement': + // @ts-expect-error dispatch(updateAnnouncements(JSON.parse(data.payload))); break; case 'announcement.reaction': + // @ts-expect-error dispatch(updateAnnouncementsReaction(JSON.parse(data.payload))); break; case 'announcement.delete': @@ -109,27 +122,31 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti }, }; }); +}; /** * @param {Function} dispatch * @param {function(): void} done */ const refreshHomeTimelineAndNotification = (dispatch, done) => { + // @ts-expect-error dispatch(expandHomeTimeline({}, () => + // @ts-expect-error dispatch(expandNotifications({}, () => dispatch(fetchAnnouncements(done)))))); }; /** - * @return {function(): void} + * @returns {function(): void} */ export const connectUserStream = () => + // @ts-expect-error connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps }); /** * @param {Object} options * @param {boolean} [options.onlyMedia] - * @return {function(): void} + * @returns {function(): void} */ export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia })) }); @@ -139,7 +156,7 @@ export const connectCommunityStream = ({ onlyMedia } = {}) => * @param {boolean} [options.onlyMedia] * @param {boolean} [options.onlyRemote] * @param {boolean} [options.allowLocalOnly] - * @return {function(): void} + * @returns {function(): void} */ export const connectPublicStream = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}) => connectTimelineStream(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => fillPublicTimelineGaps({ onlyMedia, onlyRemote, allowLocalOnly }) }); @@ -149,20 +166,20 @@ export const connectPublicStream = ({ onlyMedia, onlyRemote, allowLocalOnly } = * @param {string} tagName * @param {boolean} onlyLocal * @param {function(object): boolean} accept - * @return {function(): void} + * @returns {function(): void} */ export const connectHashtagStream = (columnId, tagName, onlyLocal, accept) => connectTimelineStream(`hashtag:${columnId}${onlyLocal ? ':local' : ''}`, `hashtag${onlyLocal ? ':local' : ''}`, { tag: tagName }, { accept }); /** - * @return {function(): void} + * @returns {function(): void} */ export const connectDirectStream = () => connectTimelineStream('direct', 'direct'); /** * @param {string} listId - * @return {function(): void} + * @returns {function(): void} */ export const connectListStream = listId => connectTimelineStream(`list:${listId}`, 'list', { list: listId }, { fillGaps: () => fillListTimelineGaps(listId) }); diff --git a/app/javascript/flavours/glitch/actions/suggestions.js b/app/javascript/flavours/glitch/actions/suggestions.js index 9e8cd1ea40..870a311024 100644 --- a/app/javascript/flavours/glitch/actions/suggestions.js +++ b/app/javascript/flavours/glitch/actions/suggestions.js @@ -1,6 +1,7 @@ import api from '../api'; -import { importFetchedAccounts } from './importer'; + import { fetchRelationships } from './accounts'; +import { importFetchedAccounts } from './importer'; export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST'; export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS'; diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index eb817daf9e..7d4d56a784 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -1,10 +1,12 @@ +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; + +import api, { getLinks } from 'flavours/glitch/api'; +import { compareId } from 'flavours/glitch/compare_id'; +import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state'; +import { toServerSideType } from 'flavours/glitch/utils/filters'; + import { importFetchedStatus, importFetchedStatuses } from './importer'; import { submitMarkers } from './markers'; -import api, { getLinks } from 'flavours/glitch/api'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -import compareId from 'flavours/glitch/compare_id'; -import { me, usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state'; -import { toServerSideType } from 'flavours/glitch/utils/filters'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_DELETE = 'TIMELINE_DELETE'; @@ -121,7 +123,6 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems)); @@ -163,10 +164,10 @@ export const expandListTimeline = (id, { maxId } = {}, done = noOp) = export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => { return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId, - any: parseTags(tags, 'any'), - all: parseTags(tags, 'all'), - none: parseTags(tags, 'none'), - local: local, + any: parseTags(tags, 'any'), + all: parseTags(tags, 'all'), + none: parseTags(tags, 'none'), + local: local, }, done); }; diff --git a/app/javascript/flavours/glitch/actions/trends.js b/app/javascript/flavours/glitch/actions/trends.js index edda0b5b5d..d314423884 100644 --- a/app/javascript/flavours/glitch/actions/trends.js +++ b/app/javascript/flavours/glitch/actions/trends.js @@ -1,4 +1,5 @@ import api, { getLinks } from '../api'; + import { importFetchedStatuses } from './importer'; export const TRENDS_TAGS_FETCH_REQUEST = 'TRENDS_TAGS_FETCH_REQUEST'; diff --git a/app/javascript/flavours/glitch/api.js b/app/javascript/flavours/glitch/api.js index 6bbddbef66..948ffbc95c 100644 --- a/app/javascript/flavours/glitch/api.js +++ b/app/javascript/flavours/glitch/api.js @@ -2,8 +2,8 @@ import axios from 'axios'; import LinkHeader from 'http-link-header'; -import ready from './ready'; +import ready from './ready'; /** * @param {import('axios').AxiosResponse} response * @returns {LinkHeader} @@ -36,7 +36,7 @@ const setCSRFHeader = () => { ready(setCSRFHeader); /** - * @param {() => import('immutable').Map} getState + * @param {() => import('immutable').Map} getState * @returns {import('axios').RawAxiosRequestHeaders} */ const authorizationHeaderFromState = getState => { @@ -52,7 +52,7 @@ const authorizationHeaderFromState = getState => { }; /** - * @param {() => import('immutable').Map} getState + * @param {() => import('immutable').Map} getState * @returns {import('axios').AxiosInstance} */ export default function api(getState) { diff --git a/app/javascript/flavours/glitch/base_polyfills.js b/app/javascript/flavours/glitch/base_polyfills.js deleted file mode 100644 index 12096d9021..0000000000 --- a/app/javascript/flavours/glitch/base_polyfills.js +++ /dev/null @@ -1,47 +0,0 @@ -import 'intl'; -import 'intl/locale-data/jsonp/en'; -import 'es6-symbol/implement'; -import includes from 'array-includes'; -import assign from 'object-assign'; -import values from 'object.values'; -import isNaN from 'is-nan'; -import { decode as decodeBase64 } from './utils/base64'; -import promiseFinally from 'promise.prototype.finally'; - -if (!Array.prototype.includes) { - includes.shim(); -} - -if (!Object.assign) { - Object.assign = assign; -} - -if (!Object.values) { - values.shim(); -} - -if (!Number.isNaN) { - Number.isNaN = isNaN; -} - -promiseFinally.shim(); - -if (!HTMLCanvasElement.prototype.toBlob) { - const BASE64_MARKER = ';base64,'; - - Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { - value(callback, type = 'image/png', quality) { - const dataURL = this.toDataURL(type, quality); - let data; - - if (dataURL.indexOf(BASE64_MARKER) >= 0) { - const [, base64] = dataURL.split(BASE64_MARKER); - data = decodeBase64(base64); - } else { - [, data] = dataURL.split(','); - } - - callback(new Blob([data], { type })); - }, - }); -} diff --git a/app/javascript/flavours/glitch/blurhash.js b/app/javascript/flavours/glitch/blurhash.js deleted file mode 100644 index 5adcc3e770..0000000000 --- a/app/javascript/flavours/glitch/blurhash.js +++ /dev/null @@ -1,112 +0,0 @@ -const DIGIT_CHARACTERS = [ - '0', - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - 'A', - 'B', - 'C', - 'D', - 'E', - 'F', - 'G', - 'H', - 'I', - 'J', - 'K', - 'L', - 'M', - 'N', - 'O', - 'P', - 'Q', - 'R', - 'S', - 'T', - 'U', - 'V', - 'W', - 'X', - 'Y', - 'Z', - 'a', - 'b', - 'c', - 'd', - 'e', - 'f', - 'g', - 'h', - 'i', - 'j', - 'k', - 'l', - 'm', - 'n', - 'o', - 'p', - 'q', - 'r', - 's', - 't', - 'u', - 'v', - 'w', - 'x', - 'y', - 'z', - '#', - '$', - '%', - '*', - '+', - ',', - '-', - '.', - ':', - ';', - '=', - '?', - '@', - '[', - ']', - '^', - '_', - '{', - '|', - '}', - '~', -]; - -export const decode83 = (str) => { - let value = 0; - let c, digit; - - for (let i = 0; i < str.length; i++) { - c = str[i]; - digit = DIGIT_CHARACTERS.indexOf(c); - value = value * 83 + digit; - } - - return value; -}; - -export const intToRGB = int => ({ - r: Math.max(0, (int >> 16)), - g: Math.max(0, (int >> 8) & 255), - b: Math.max(0, (int & 255)), -}); - -export const getAverageFromBlurhash = blurhash => { - if (!blurhash) { - return null; - } - - return intToRGB(decode83(blurhash.slice(2, 6))); -}; diff --git a/app/javascript/flavours/glitch/blurhash.ts b/app/javascript/flavours/glitch/blurhash.ts new file mode 100644 index 0000000000..dadf2b7f2c --- /dev/null +++ b/app/javascript/flavours/glitch/blurhash.ts @@ -0,0 +1,112 @@ +const DIGIT_CHARACTERS = [ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + 'G', + 'H', + 'I', + 'J', + 'K', + 'L', + 'M', + 'N', + 'O', + 'P', + 'Q', + 'R', + 'S', + 'T', + 'U', + 'V', + 'W', + 'X', + 'Y', + 'Z', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', + 'j', + 'k', + 'l', + 'm', + 'n', + 'o', + 'p', + 'q', + 'r', + 's', + 't', + 'u', + 'v', + 'w', + 'x', + 'y', + 'z', + '#', + '$', + '%', + '*', + '+', + ',', + '-', + '.', + ':', + ';', + '=', + '?', + '@', + '[', + ']', + '^', + '_', + '{', + '|', + '}', + '~', +]; + +export const decode83 = (str: string) => { + let value = 0; + let c, digit; + + for (let i = 0; i < str.length; i++) { + c = str[i]; + digit = DIGIT_CHARACTERS.indexOf(c); + value = value * 83 + digit; + } + + return value; +}; + +export const intToRGB = (int: number) => ({ + r: Math.max(0, int >> 16), + g: Math.max(0, (int >> 8) & 255), + b: Math.max(0, int & 255), +}); + +export const getAverageFromBlurhash = (blurhash: string) => { + if (!blurhash) { + return null; + } + + return intToRGB(decode83(blurhash.slice(2, 6))); +}; diff --git a/app/javascript/flavours/glitch/compare_id.js b/app/javascript/flavours/glitch/compare_id.js deleted file mode 100644 index d2bd74f447..0000000000 --- a/app/javascript/flavours/glitch/compare_id.js +++ /dev/null @@ -1,11 +0,0 @@ -export default function compareId (id1, id2) { - if (id1 === id2) { - return 0; - } - - if (id1.length === id2.length) { - return id1 > id2 ? 1 : -1; - } else { - return id1.length > id2.length ? 1 : -1; - } -} diff --git a/app/javascript/flavours/glitch/compare_id.ts b/app/javascript/flavours/glitch/compare_id.ts new file mode 100644 index 0000000000..30b0572481 --- /dev/null +++ b/app/javascript/flavours/glitch/compare_id.ts @@ -0,0 +1,11 @@ +export function compareId(id1: string, id2: string) { + if (id1 === id2) { + return 0; + } + + if (id1.length === id2.length) { + return id1 > id2 ? 1 : -1; + } else { + return id1.length > id2.length ? 1 : -1; + } +} diff --git a/app/javascript/flavours/glitch/components/account.js b/app/javascript/flavours/glitch/components/account.js deleted file mode 100644 index 7ce4b65aac..0000000000 --- a/app/javascript/flavours/glitch/components/account.js +++ /dev/null @@ -1,186 +0,0 @@ -import React, { Fragment } from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import Avatar from './avatar'; -import DisplayName from './display_name'; -import Permalink from './permalink'; -import IconButton from './icon_button'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { me } from 'flavours/glitch/initial_state'; -import RelativeTimestamp from './relative_timestamp'; -import Skeleton from 'flavours/glitch/components/skeleton'; - -const messages = defineMessages({ - follow: { id: 'account.follow', defaultMessage: 'Follow' }, - unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, - unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, - unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, - mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' }, - unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' }, - mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, - block: { id: 'account.block', defaultMessage: 'Block @{name}' }, -}); - -export default @injectIntl -class Account extends ImmutablePureComponent { - - static propTypes = { - size: PropTypes.number, - account: ImmutablePropTypes.map, - onFollow: PropTypes.func.isRequired, - onBlock: PropTypes.func.isRequired, - onMute: PropTypes.func.isRequired, - onMuteNotifications: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - hidden: PropTypes.bool, - small: PropTypes.bool, - actionIcon: PropTypes.string, - actionTitle: PropTypes.string, - defaultAction: PropTypes.string, - onActionClick: PropTypes.func, - }; - - static defaultProps = { - size: 36, - }; - - handleFollow = () => { - this.props.onFollow(this.props.account); - }; - - handleBlock = () => { - this.props.onBlock(this.props.account); - }; - - handleMute = () => { - this.props.onMute(this.props.account); - }; - - handleMuteNotifications = () => { - this.props.onMuteNotifications(this.props.account, true); - }; - - handleUnmuteNotifications = () => { - this.props.onMuteNotifications(this.props.account, false); - }; - - handleAction = () => { - this.props.onActionClick(this.props.account); - }; - - render () { - const { - account, - hidden, - intl, - small, - onActionClick, - actionIcon, - actionTitle, - defaultAction, - size, - } = this.props; - - if (!account) { - return ( -

-
-
-
- -
-
-
- ); - } - - if (hidden) { - return ( - - {account.get('display_name')} - {account.get('username')} - - ); - } - - let buttons; - - if (onActionClick) { - if (actionIcon) { - buttons = ; - } - } else if (account.get('id') !== me && !small && account.get('relationship', null) !== null) { - const following = account.getIn(['relationship', 'following']); - const requested = account.getIn(['relationship', 'requested']); - const blocking = account.getIn(['relationship', 'blocking']); - const muting = account.getIn(['relationship', 'muting']); - - if (requested) { - buttons = ; - } else if (blocking) { - buttons = ; - } else if (muting) { - let hidingNotificationsButton; - if (account.getIn(['relationship', 'muting_notifications'])) { - hidingNotificationsButton = ; - } else { - hidingNotificationsButton = ; - } - buttons = ( - - - {hidingNotificationsButton} - - ); - } else if (defaultAction === 'mute') { - buttons = ; - } else if (defaultAction === 'block') { - buttons = ; - } else if (!account.get('moved') || following) { - buttons = ; - } - } - - let mute_expires_at; - if (account.get('mute_expires_at')) { - mute_expires_at =
; - } - - return small ? ( - -
- -
- -
- ) : ( -
-
- -
- {mute_expires_at} - -
- {buttons ? -
- {buttons} -
- : null} -
-
- ); - } - -} diff --git a/app/javascript/flavours/glitch/components/account.jsx b/app/javascript/flavours/glitch/components/account.jsx new file mode 100644 index 0000000000..518464b040 --- /dev/null +++ b/app/javascript/flavours/glitch/components/account.jsx @@ -0,0 +1,191 @@ +import PropTypes from 'prop-types'; + +import { defineMessages, injectIntl } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +import { Skeleton } from 'flavours/glitch/components/skeleton'; +import { me } from 'flavours/glitch/initial_state'; + +import { Avatar } from './avatar'; +import { DisplayName } from './display_name'; +import { IconButton } from './icon_button'; +import Permalink from './permalink'; +import { RelativeTimestamp } from './relative_timestamp'; + + +const messages = defineMessages({ + follow: { id: 'account.follow', defaultMessage: 'Follow' }, + unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, + requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, + unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, + unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, + mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' }, + unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' }, + mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, + block: { id: 'account.block', defaultMessage: 'Block @{name}' }, +}); + +class Account extends ImmutablePureComponent { + + static propTypes = { + size: PropTypes.number, + account: ImmutablePropTypes.map, + onFollow: PropTypes.func.isRequired, + onBlock: PropTypes.func.isRequired, + onMute: PropTypes.func.isRequired, + onMuteNotifications: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + hidden: PropTypes.bool, + small: PropTypes.bool, + actionIcon: PropTypes.string, + actionTitle: PropTypes.string, + defaultAction: PropTypes.string, + onActionClick: PropTypes.func, + }; + + static defaultProps = { + size: 36, + }; + + handleFollow = () => { + this.props.onFollow(this.props.account); + }; + + handleBlock = () => { + this.props.onBlock(this.props.account); + }; + + handleMute = () => { + this.props.onMute(this.props.account); + }; + + handleMuteNotifications = () => { + this.props.onMuteNotifications(this.props.account, true); + }; + + handleUnmuteNotifications = () => { + this.props.onMuteNotifications(this.props.account, false); + }; + + handleAction = () => { + this.props.onActionClick(this.props.account); + }; + + render () { + const { + account, + hidden, + intl, + small, + onActionClick, + actionIcon, + actionTitle, + defaultAction, + size, + } = this.props; + + if (!account) { + return ( +
+
+
+
+ +
+
+
+ ); + } + + if (hidden) { + return ( + <> + {account.get('display_name')} + {account.get('username')} + + ); + } + + let buttons; + + if (onActionClick) { + if (actionIcon) { + buttons = ; + } + } else if (account.get('id') !== me && !small && account.get('relationship', null) !== null) { + const following = account.getIn(['relationship', 'following']); + const requested = account.getIn(['relationship', 'requested']); + const blocking = account.getIn(['relationship', 'blocking']); + const muting = account.getIn(['relationship', 'muting']); + + if (requested) { + buttons = ; + } else if (blocking) { + buttons = ; + } else if (muting) { + let hidingNotificationsButton; + if (account.getIn(['relationship', 'muting_notifications'])) { + hidingNotificationsButton = ; + } else { + hidingNotificationsButton = ; + } + buttons = ( + <> + + {hidingNotificationsButton} + + ); + } else if (defaultAction === 'mute') { + buttons = ; + } else if (defaultAction === 'block') { + buttons = ; + } else if (!account.get('moved') || following) { + buttons = ; + } + } + + let mute_expires_at; + if (account.get('mute_expires_at')) { + mute_expires_at =
; + } + + return small ? ( + +
+ +
+ +
+ ) : ( +
+
+ +
+ {mute_expires_at} + +
+ {buttons ? +
+ {buttons} +
+ : null} +
+
+ ); + } + +} + +export default injectIntl(Account); diff --git a/app/javascript/flavours/glitch/components/admin/Counter.js b/app/javascript/flavours/glitch/components/admin/Counter.js deleted file mode 100644 index 5b6a19f8da..0000000000 --- a/app/javascript/flavours/glitch/components/admin/Counter.js +++ /dev/null @@ -1,117 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import api from 'flavours/glitch/api'; -import { FormattedNumber } from 'react-intl'; -import { Sparklines, SparklinesCurve } from 'react-sparklines'; -import classNames from 'classnames'; -import Skeleton from 'flavours/glitch/components/skeleton'; - -const percIncrease = (a, b) => { - let percent; - - if (b !== 0) { - if (a !== 0) { - percent = (b - a) / a; - } else { - percent = 1; - } - } else if (b === 0 && a === 0) { - percent = 0; - } else { - percent = - 1; - } - - return percent; -}; - -export default class Counter extends React.PureComponent { - - static propTypes = { - measure: PropTypes.string.isRequired, - start_at: PropTypes.string.isRequired, - end_at: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - href: PropTypes.string, - params: PropTypes.object, - target: PropTypes.string, - }; - - state = { - loading: true, - data: null, - }; - - componentDidMount () { - const { measure, start_at, end_at, params } = this.props; - - api().post('/api/v1/admin/measures', { keys: [measure], start_at, end_at, [measure]: params }).then(res => { - this.setState({ - loading: false, - data: res.data, - }); - }).catch(err => { - console.error(err); - }); - } - - render () { - const { label, href, target } = this.props; - const { loading, data } = this.state; - - let content; - - if (loading) { - content = ( - - - - - ); - } else { - const measure = data[0]; - const percentChange = measure.previous_total && percIncrease(measure.previous_total * 1, measure.total * 1); - - content = ( - - {measure.human_value || } - {measure.previous_total && ( 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'})} - - ); - } - - const inner = ( - -
- {content} -
- -
- {label} -
- -
- {!loading && ( - x.value * 1)}> - - - )} -
-
- ); - - if (href) { - return ( - - {inner} - - ); - } else { - return ( -
- {inner} -
- ); - } - } - -} diff --git a/app/javascript/flavours/glitch/components/admin/Counter.jsx b/app/javascript/flavours/glitch/components/admin/Counter.jsx new file mode 100644 index 0000000000..9bb792fc9d --- /dev/null +++ b/app/javascript/flavours/glitch/components/admin/Counter.jsx @@ -0,0 +1,121 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { FormattedNumber } from 'react-intl'; + +import classNames from 'classnames'; + +import { Sparklines, SparklinesCurve } from 'react-sparklines'; + +import api from 'flavours/glitch/api'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; + +const percIncrease = (a, b) => { + let percent; + + if (b !== 0) { + if (a !== 0) { + percent = (b - a) / a; + } else { + percent = 1; + } + } else if (b === 0 && a === 0) { + percent = 0; + } else { + percent = - 1; + } + + return percent; +}; + +export default class Counter extends PureComponent { + + static propTypes = { + measure: PropTypes.string.isRequired, + start_at: PropTypes.string.isRequired, + end_at: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + href: PropTypes.string, + params: PropTypes.object, + target: PropTypes.string, + }; + + state = { + loading: true, + data: null, + }; + + componentDidMount () { + const { measure, start_at, end_at, params } = this.props; + + api().post('/api/v1/admin/measures', { keys: [measure], start_at, end_at, [measure]: params }).then(res => { + this.setState({ + loading: false, + data: res.data, + }); + }).catch(err => { + console.error(err); + }); + } + + render () { + const { label, href, target } = this.props; + const { loading, data } = this.state; + + let content; + + if (loading) { + content = ( + <> + + + + ); + } else { + const measure = data[0]; + const percentChange = measure.previous_total && percIncrease(measure.previous_total * 1, measure.total * 1); + + content = ( + <> + {measure.human_value || } + {measure.previous_total && ( 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'})} + + ); + } + + const inner = ( + <> +
+ {content} +
+ +
+ {label} +
+ +
+ {!loading && ( + x.value * 1)}> + + + )} +
+ + ); + + if (href) { + return ( + + {inner} + + ); + } else { + return ( +
+ {inner} +
+ ); + } + } + +} diff --git a/app/javascript/flavours/glitch/components/admin/Dimension.js b/app/javascript/flavours/glitch/components/admin/Dimension.js deleted file mode 100644 index 3dac8c6c24..0000000000 --- a/app/javascript/flavours/glitch/components/admin/Dimension.js +++ /dev/null @@ -1,93 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import api from 'flavours/glitch/api'; -import { FormattedNumber } from 'react-intl'; -import { roundTo10 } from 'flavours/glitch/utils/numbers'; -import Skeleton from 'flavours/glitch/components/skeleton'; - -export default class Dimension extends React.PureComponent { - - static propTypes = { - dimension: PropTypes.string.isRequired, - start_at: PropTypes.string.isRequired, - end_at: PropTypes.string.isRequired, - limit: PropTypes.number.isRequired, - label: PropTypes.string.isRequired, - params: PropTypes.object, - }; - - state = { - loading: true, - data: null, - }; - - componentDidMount () { - const { start_at, end_at, dimension, limit, params } = this.props; - - api().post('/api/v1/admin/dimensions', { keys: [dimension], start_at, end_at, limit, [dimension]: params }).then(res => { - this.setState({ - loading: false, - data: res.data, - }); - }).catch(err => { - console.error(err); - }); - } - - render () { - const { label, limit } = this.props; - const { loading, data } = this.state; - - let content; - - if (loading) { - content = ( - - - {Array.from(Array(limit)).map((_, i) => ( - - - - - - ))} - -
- - - -
- ); - } else { - const sum = data[0].data.reduce((sum, cur) => sum + (cur.value * 1), 0); - - content = ( - - - {data[0].data.map(item => ( - - - - - - ))} - -
- - {item.human_key} - - {typeof item.human_value !== 'undefined' ? item.human_value : } -
- ); - } - - return ( -
-

{label}

- - {content} -
- ); - } - -} diff --git a/app/javascript/flavours/glitch/components/admin/Dimension.jsx b/app/javascript/flavours/glitch/components/admin/Dimension.jsx new file mode 100644 index 0000000000..793fe2dd76 --- /dev/null +++ b/app/javascript/flavours/glitch/components/admin/Dimension.jsx @@ -0,0 +1,95 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { FormattedNumber } from 'react-intl'; + +import api from 'flavours/glitch/api'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; +import { roundTo10 } from 'flavours/glitch/utils/numbers'; + +export default class Dimension extends PureComponent { + + static propTypes = { + dimension: PropTypes.string.isRequired, + start_at: PropTypes.string.isRequired, + end_at: PropTypes.string.isRequired, + limit: PropTypes.number.isRequired, + label: PropTypes.string.isRequired, + params: PropTypes.object, + }; + + state = { + loading: true, + data: null, + }; + + componentDidMount () { + const { start_at, end_at, dimension, limit, params } = this.props; + + api().post('/api/v1/admin/dimensions', { keys: [dimension], start_at, end_at, limit, [dimension]: params }).then(res => { + this.setState({ + loading: false, + data: res.data, + }); + }).catch(err => { + console.error(err); + }); + } + + render () { + const { label, limit } = this.props; + const { loading, data } = this.state; + + let content; + + if (loading) { + content = ( + + + {Array.from(Array(limit)).map((_, i) => ( + + + + + + ))} + +
+ + + +
+ ); + } else { + const sum = data[0].data.reduce((sum, cur) => sum + (cur.value * 1), 0); + + content = ( + + + {data[0].data.map(item => ( + + + + + + ))} + +
+ + {item.human_key} + + {typeof item.human_value !== 'undefined' ? item.human_value : } +
+ ); + } + + return ( +
+

{label}

+ + {content} +
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/components/admin/ImpactReport.jsx b/app/javascript/flavours/glitch/components/admin/ImpactReport.jsx new file mode 100644 index 0000000000..9ec1460fcf --- /dev/null +++ b/app/javascript/flavours/glitch/components/admin/ImpactReport.jsx @@ -0,0 +1,91 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { FormattedNumber, FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + +import api from 'flavours/glitch/api'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; + +export default class ImpactReport extends PureComponent { + + static propTypes = { + domain: PropTypes.string.isRequired, + }; + + state = { + loading: true, + data: null, + }; + + componentDidMount () { + const { domain } = this.props; + + const params = { + domain: domain, + include_subdomains: true, + }; + + api().post('/api/v1/admin/measures', { + keys: ['instance_accounts', 'instance_follows', 'instance_followers'], + start_at: null, + end_at: null, + instance_accounts: params, + instance_follows: params, + instance_followers: params, + }).then(res => { + this.setState({ + loading: false, + data: res.data, + }); + }).catch(err => { + console.error(err); + }); + } + + render () { + const { loading, data } = this.state; + + return ( +
+

+ + + + + + + + + + 0 })}> + + + + + + 0 })}> + + + + + +
+ + + {loading ? : } +
+ + + {loading ? : } +
+ + + {loading ? : } +
+
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.js b/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.js deleted file mode 100644 index 771dbb452d..0000000000 --- a/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.js +++ /dev/null @@ -1,159 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import api from 'flavours/glitch/api'; -import { injectIntl, defineMessages } from 'react-intl'; -import classNames from 'classnames'; - -const messages = defineMessages({ - other: { id: 'report.categories.other', defaultMessage: 'Other' }, - spam: { id: 'report.categories.spam', defaultMessage: 'Spam' }, - violation: { id: 'report.categories.violation', defaultMessage: 'Content violates one or more server rules' }, -}); - -class Category extends React.PureComponent { - - static propTypes = { - id: PropTypes.string.isRequired, - text: PropTypes.string.isRequired, - selected: PropTypes.bool, - disabled: PropTypes.bool, - onSelect: PropTypes.func, - children: PropTypes.node, - }; - - handleClick = () => { - const { id, disabled, onSelect } = this.props; - - if (!disabled) { - onSelect(id); - } - }; - - render () { - const { id, text, disabled, selected, children } = this.props; - - return ( -
- {selected && } - -
- - {text} -
- - {(selected && children) && ( -
- {children} -
- )} -
- ); - } - -} - -class Rule extends React.PureComponent { - - static propTypes = { - id: PropTypes.string.isRequired, - text: PropTypes.string.isRequired, - selected: PropTypes.bool, - disabled: PropTypes.bool, - onToggle: PropTypes.func, - }; - - handleClick = () => { - const { id, disabled, onToggle } = this.props; - - if (!disabled) { - onToggle(id); - } - }; - - render () { - const { id, text, disabled, selected } = this.props; - - return ( -
- - {selected && } - {text} -
- ); - } - -} - -export default @injectIntl -class ReportReasonSelector extends React.PureComponent { - - static propTypes = { - id: PropTypes.string.isRequired, - category: PropTypes.string.isRequired, - rule_ids: PropTypes.arrayOf(PropTypes.string), - disabled: PropTypes.bool, - intl: PropTypes.object.isRequired, - }; - - state = { - category: this.props.category, - rule_ids: this.props.rule_ids || [], - rules: [], - }; - - componentDidMount() { - api().get('/api/v1/instance').then(res => { - this.setState({ - rules: res.data.rules, - }); - }).catch(err => { - console.error(err); - }); - } - - _save = () => { - const { id, disabled } = this.props; - const { category, rule_ids } = this.state; - - if (disabled) { - return; - } - - api().put(`/api/v1/admin/reports/${id}`, { - category, - rule_ids, - }).catch(err => { - console.error(err); - }); - }; - - handleSelect = id => { - this.setState({ category: id }, () => this._save()); - }; - - handleToggle = id => { - const { rule_ids } = this.state; - - if (rule_ids.includes(id)) { - this.setState({ rule_ids: rule_ids.filter(x => x !== id ) }, () => this._save()); - } else { - this.setState({ rule_ids: [...rule_ids, id] }, () => this._save()); - } - }; - - render () { - const { disabled, intl } = this.props; - const { rules, category, rule_ids } = this.state; - - return ( -
- - - - {rules.map(rule => )} - -
- ); - } - -} diff --git a/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.jsx b/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.jsx new file mode 100644 index 0000000000..d72465e4ad --- /dev/null +++ b/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.jsx @@ -0,0 +1,163 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { injectIntl, defineMessages } from 'react-intl'; + +import classNames from 'classnames'; + +import api from 'flavours/glitch/api'; + +const messages = defineMessages({ + other: { id: 'report.categories.other', defaultMessage: 'Other' }, + spam: { id: 'report.categories.spam', defaultMessage: 'Spam' }, + violation: { id: 'report.categories.violation', defaultMessage: 'Content violates one or more server rules' }, +}); + +class Category extends PureComponent { + + static propTypes = { + id: PropTypes.string.isRequired, + text: PropTypes.string.isRequired, + selected: PropTypes.bool, + disabled: PropTypes.bool, + onSelect: PropTypes.func, + children: PropTypes.node, + }; + + handleClick = () => { + const { id, disabled, onSelect } = this.props; + + if (!disabled) { + onSelect(id); + } + }; + + render () { + const { id, text, disabled, selected, children } = this.props; + + return ( +
+ {selected && } + +
+ + {text} +
+ + {(selected && children) && ( +
+ {children} +
+ )} +
+ ); + } + +} + +class Rule extends PureComponent { + + static propTypes = { + id: PropTypes.string.isRequired, + text: PropTypes.string.isRequired, + selected: PropTypes.bool, + disabled: PropTypes.bool, + onToggle: PropTypes.func, + }; + + handleClick = () => { + const { id, disabled, onToggle } = this.props; + + if (!disabled) { + onToggle(id); + } + }; + + render () { + const { id, text, disabled, selected } = this.props; + + return ( +
+ + {selected && } + {text} +
+ ); + } + +} + +class ReportReasonSelector extends PureComponent { + + static propTypes = { + id: PropTypes.string.isRequired, + category: PropTypes.string.isRequired, + rule_ids: PropTypes.arrayOf(PropTypes.string), + disabled: PropTypes.bool, + intl: PropTypes.object.isRequired, + }; + + state = { + category: this.props.category, + rule_ids: this.props.rule_ids || [], + rules: [], + }; + + componentDidMount() { + api().get('/api/v1/instance').then(res => { + this.setState({ + rules: res.data.rules, + }); + }).catch(err => { + console.error(err); + }); + } + + _save = () => { + const { id, disabled } = this.props; + const { category, rule_ids } = this.state; + + if (disabled) { + return; + } + + api().put(`/api/v1/admin/reports/${id}`, { + category, + rule_ids, + }).catch(err => { + console.error(err); + }); + }; + + handleSelect = id => { + this.setState({ category: id }, () => this._save()); + }; + + handleToggle = id => { + const { rule_ids } = this.state; + + if (rule_ids.includes(id)) { + this.setState({ rule_ids: rule_ids.filter(x => x !== id ) }, () => this._save()); + } else { + this.setState({ rule_ids: [...rule_ids, id] }, () => this._save()); + } + }; + + render () { + const { disabled, intl } = this.props; + const { rules, category, rule_ids } = this.state; + + return ( +
+ + + + {rules.map(rule => )} + +
+ ); + } + +} + +export default injectIntl(ReportReasonSelector); diff --git a/app/javascript/flavours/glitch/components/admin/Retention.js b/app/javascript/flavours/glitch/components/admin/Retention.js deleted file mode 100644 index e1ba3f6c9d..0000000000 --- a/app/javascript/flavours/glitch/components/admin/Retention.js +++ /dev/null @@ -1,151 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import api from 'flavours/glitch/api'; -import { FormattedMessage, FormattedNumber, FormattedDate } from 'react-intl'; -import classNames from 'classnames'; -import { roundTo10 } from 'flavours/glitch/utils/numbers'; - -const dateForCohort = cohort => { - switch(cohort.frequency) { - case 'day': - return ; - default: - return ; - } -}; - -export default class Retention extends React.PureComponent { - - static propTypes = { - start_at: PropTypes.string, - end_at: PropTypes.string, - frequency: PropTypes.string, - }; - - state = { - loading: true, - data: null, - }; - - componentDidMount () { - const { start_at, end_at, frequency } = this.props; - - api().post('/api/v1/admin/retention', { start_at, end_at, frequency }).then(res => { - this.setState({ - loading: false, - data: res.data, - }); - }).catch(err => { - console.error(err); - }); - } - - render () { - const { loading, data } = this.state; - const { frequency } = this.props; - - let content; - - if (loading) { - content = ; - } else { - content = ( - - - - - - - - {data[0].data.slice(1).map((retention, i) => ( - - ))} - - - - - - - - {data[0].data.slice(1).map((retention, i) => { - const average = data.reduce((sum, cohort, k) => cohort.data[i + 1] ? sum + (cohort.data[i + 1].rate - sum)/(k + 1) : sum, 0); - - return ( - - ); - })} - - - - - {data.slice(0, -1).map(cohort => ( - - - - - - {cohort.data.slice(1).map(retention => ( - - ))} - - ))} - -
-
- -
-
-
- -
-
-
- {i + 1} -
-
-
- -
-
-
- sum + ((cohort.data[0].value * 1) - sum) / (i + 1), 0)} maximumFractionDigits={0} /> -
-
-
- -
-
-
- {dateForCohort(cohort)} -
-
-
- -
-
-
- -
-
- ); - } - - let title = null; - switch(frequency) { - case 'day': - title = ; - break; - default: - title = ; - } - - return ( -
-

{title}

- - {content} -
- ); - } - -} diff --git a/app/javascript/flavours/glitch/components/admin/Retention.jsx b/app/javascript/flavours/glitch/components/admin/Retention.jsx new file mode 100644 index 0000000000..2cfc30b6fb --- /dev/null +++ b/app/javascript/flavours/glitch/components/admin/Retention.jsx @@ -0,0 +1,154 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { FormattedMessage, FormattedNumber, FormattedDate } from 'react-intl'; + +import classNames from 'classnames'; + +import api from 'flavours/glitch/api'; +import { roundTo10 } from 'flavours/glitch/utils/numbers'; + +const dateForCohort = cohort => { + switch(cohort.frequency) { + case 'day': + return ; + default: + return ; + } +}; + +export default class Retention extends PureComponent { + + static propTypes = { + start_at: PropTypes.string, + end_at: PropTypes.string, + frequency: PropTypes.string, + }; + + state = { + loading: true, + data: null, + }; + + componentDidMount () { + const { start_at, end_at, frequency } = this.props; + + api().post('/api/v1/admin/retention', { start_at, end_at, frequency }).then(res => { + this.setState({ + loading: false, + data: res.data, + }); + }).catch(err => { + console.error(err); + }); + } + + render () { + const { loading, data } = this.state; + const { frequency } = this.props; + + let content; + + if (loading) { + content = ; + } else { + content = ( + + + + + + + + {data[0].data.slice(1).map((retention, i) => ( + + ))} + + + + + + + + {data[0].data.slice(1).map((retention, i) => { + const average = data.reduce((sum, cohort, k) => cohort.data[i + 1] ? sum + (cohort.data[i + 1].rate - sum)/(k + 1) : sum, 0); + + return ( + + ); + })} + + + + + {data.slice(0, -1).map(cohort => ( + + + + + + {cohort.data.slice(1).map(retention => ( + + ))} + + ))} + +
+
+ +
+
+
+ +
+
+
+ {i + 1} +
+
+
+ +
+
+
+ sum + ((cohort.data[0].value * 1) - sum) / (i + 1), 0)} maximumFractionDigits={0} /> +
+
+
+ +
+
+
+ {dateForCohort(cohort)} +
+
+
+ +
+
+
+ +
+
+ ); + } + + let title = null; + switch(frequency) { + case 'day': + title = ; + break; + default: + title = ; + } + + return ( +
+

{title}

+ + {content} +
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/components/admin/Trends.js b/app/javascript/flavours/glitch/components/admin/Trends.js deleted file mode 100644 index 774bf36e6e..0000000000 --- a/app/javascript/flavours/glitch/components/admin/Trends.js +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import api from 'flavours/glitch/api'; -import { FormattedMessage } from 'react-intl'; -import classNames from 'classnames'; -import Hashtag from 'flavours/glitch/components/hashtag'; - -export default class Trends extends React.PureComponent { - - static propTypes = { - limit: PropTypes.number.isRequired, - }; - - state = { - loading: true, - data: null, - }; - - componentDidMount () { - const { limit } = this.props; - - api().get('/api/v1/admin/trends/tags', { params: { limit } }).then(res => { - this.setState({ - loading: false, - data: res.data, - }); - }).catch(err => { - console.error(err); - }); - } - - render () { - const { limit } = this.props; - const { loading, data } = this.state; - - let content; - - if (loading) { - content = ( -
- {Array.from(Array(limit)).map((_, i) => ( - - ))} -
- ); - } else { - content = ( -
- {data.map(hashtag => ( - day.uses)} - className={classNames(hashtag.requires_review && 'trends__item--requires-review', !hashtag.trendable && !hashtag.requires_review && 'trends__item--disabled')} - /> - ))} -
- ); - } - - return ( -
-

- - {content} -
- ); - } - -} diff --git a/app/javascript/flavours/glitch/components/admin/Trends.jsx b/app/javascript/flavours/glitch/components/admin/Trends.jsx new file mode 100644 index 0000000000..975ea6e0f2 --- /dev/null +++ b/app/javascript/flavours/glitch/components/admin/Trends.jsx @@ -0,0 +1,76 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + +import api from 'flavours/glitch/api'; +import Hashtag from 'flavours/glitch/components/hashtag'; + +export default class Trends extends PureComponent { + + static propTypes = { + limit: PropTypes.number.isRequired, + }; + + state = { + loading: true, + data: null, + }; + + componentDidMount () { + const { limit } = this.props; + + api().get('/api/v1/admin/trends/tags', { params: { limit } }).then(res => { + this.setState({ + loading: false, + data: res.data, + }); + }).catch(err => { + console.error(err); + }); + } + + render () { + const { limit } = this.props; + const { loading, data } = this.state; + + let content; + + if (loading) { + content = ( +
+ {Array.from(Array(limit)).map((_, i) => ( + + ))} +
+ ); + } else { + content = ( +
+ {data.map(hashtag => ( + day.uses)} + className={classNames(hashtag.requires_review && 'trends__item--requires-review', !hashtag.trendable && !hashtag.requires_review && 'trends__item--disabled')} + /> + ))} +
+ ); + } + + return ( +
+

+ + {content} +
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/components/animated_number.js b/app/javascript/flavours/glitch/components/animated_number.js deleted file mode 100644 index dd21d97f0c..0000000000 --- a/app/javascript/flavours/glitch/components/animated_number.js +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ShortNumber from 'mastodon/components/short_number'; -import TransitionMotion from 'react-motion/lib/TransitionMotion'; -import spring from 'react-motion/lib/spring'; -import { reduceMotion } from 'flavours/glitch/initial_state'; - -const obfuscatedCount = count => { - if (count < 0) { - return 0; - } else if (count <= 1) { - return count; - } else { - return '1+'; - } -}; - -export default class AnimatedNumber extends React.PureComponent { - - static propTypes = { - value: PropTypes.number.isRequired, - obfuscate: PropTypes.bool, - }; - - state = { - direction: 1, - }; - - componentWillReceiveProps (nextProps) { - if (nextProps.value > this.props.value) { - this.setState({ direction: 1 }); - } else if (nextProps.value < this.props.value) { - this.setState({ direction: -1 }); - } - } - - willEnter = () => { - const { direction } = this.state; - - return { y: -1 * direction }; - }; - - willLeave = () => { - const { direction } = this.state; - - return { y: spring(1 * direction, { damping: 35, stiffness: 400 }) }; - }; - - render () { - const { value, obfuscate } = this.props; - const { direction } = this.state; - - if (reduceMotion) { - return obfuscate ? obfuscatedCount(value) : ; - } - - const styles = [{ - key: `${value}`, - data: value, - style: { y: spring(0, { damping: 35, stiffness: 400 }) }, - }]; - - return ( - - {items => ( - - {items.map(({ key, data, style }) => ( - 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : } - ))} - - )} - - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/animated_number.tsx b/app/javascript/flavours/glitch/components/animated_number.tsx new file mode 100644 index 0000000000..81e0af395b --- /dev/null +++ b/app/javascript/flavours/glitch/components/animated_number.tsx @@ -0,0 +1,82 @@ +import { useCallback, useState } from 'react'; +import * as React from 'react'; + +import { TransitionMotion, spring } from 'react-motion'; + +import { reduceMotion } from '../initial_state'; + +import ShortNumber from './short_number'; + +const obfuscatedCount = (count: number) => { + if (count < 0) { + return 0; + } else if (count <= 1) { + return count; + } else { + return '1+'; + } +}; + +interface Props { + value: number; + obfuscate?: boolean; +} +export const AnimatedNumber: React.FC = ({ value, obfuscate }) => { + const [previousValue, setPreviousValue] = useState(value); + const [direction, setDirection] = useState<1 | -1>(1); + + if (previousValue !== value) { + setPreviousValue(value); + setDirection(value > previousValue ? 1 : -1); + } + + const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]); + const willLeave = useCallback( + () => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }), + [direction] + ); + + if (reduceMotion) { + return obfuscate ? ( + <>{obfuscatedCount(value)} + ) : ( + + ); + } + + const styles = [ + { + key: `${value}`, + data: value, + style: { y: spring(0, { damping: 35, stiffness: 400 }) }, + }, + ]; + + return ( + + {(items) => ( + + {items.map(({ key, data, style }) => ( + 0 ? 'absolute' : 'static', + transform: `translateY(${style.y * 100}%)`, + }} + > + {obfuscate ? ( + obfuscatedCount(data as number) + ) : ( + + )} + + ))} + + )} + + ); +}; diff --git a/app/javascript/flavours/glitch/components/attachment_list.js b/app/javascript/flavours/glitch/components/attachment_list.js deleted file mode 100644 index 68b80b19fe..0000000000 --- a/app/javascript/flavours/glitch/components/attachment_list.js +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { FormattedMessage } from 'react-intl'; -import classNames from 'classnames'; -import Icon from 'flavours/glitch/components/icon'; - -const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; - -export default class AttachmentList extends ImmutablePureComponent { - - static propTypes = { - media: ImmutablePropTypes.list.isRequired, - compact: PropTypes.bool, - }; - - render () { - const { media, compact } = this.props; - - return ( -
- {!compact && ( -
- -
- )} - - -
- ); - } - -} diff --git a/app/javascript/flavours/glitch/components/attachment_list.jsx b/app/javascript/flavours/glitch/components/attachment_list.jsx new file mode 100644 index 0000000000..173157b0d5 --- /dev/null +++ b/app/javascript/flavours/glitch/components/attachment_list.jsx @@ -0,0 +1,51 @@ +import PropTypes from 'prop-types'; + +import { FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +import { Icon } from 'flavours/glitch/components/icon'; + +const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; + +export default class AttachmentList extends ImmutablePureComponent { + + static propTypes = { + media: ImmutablePropTypes.list.isRequired, + compact: PropTypes.bool, + }; + + render () { + const { media, compact } = this.props; + + return ( +
+ {!compact && ( +
+ +
+ )} + + +
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/components/autosuggest_emoji.js b/app/javascript/flavours/glitch/components/autosuggest_emoji.js deleted file mode 100644 index 83fafbd10d..0000000000 --- a/app/javascript/flavours/glitch/components/autosuggest_emoji.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import unicodeMapping from 'flavours/glitch/features/emoji/emoji_unicode_mapping_light'; - -import { assetHost } from 'flavours/glitch/utils/config'; - -export default class AutosuggestEmoji extends React.PureComponent { - - static propTypes = { - emoji: PropTypes.object.isRequired, - }; - - render () { - const { emoji } = this.props; - let url; - - if (emoji.custom) { - url = emoji.imageUrl; - } else { - const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')]; - - if (!mapping) { - return null; - } - - url = `${assetHost}/emoji/${mapping.filename}.svg`; - } - - return ( -
- {emoji.native - - {emoji.colons} -
- ); - } - -} diff --git a/app/javascript/flavours/glitch/components/autosuggest_emoji.jsx b/app/javascript/flavours/glitch/components/autosuggest_emoji.jsx new file mode 100644 index 0000000000..32a996fd7c --- /dev/null +++ b/app/javascript/flavours/glitch/components/autosuggest_emoji.jsx @@ -0,0 +1,42 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import unicodeMapping from 'flavours/glitch/features/emoji/emoji_unicode_mapping_light'; +import { assetHost } from 'flavours/glitch/utils/config'; + +export default class AutosuggestEmoji extends PureComponent { + + static propTypes = { + emoji: PropTypes.object.isRequired, + }; + + render () { + const { emoji } = this.props; + let url; + + if (emoji.custom) { + url = emoji.imageUrl; + } else { + const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')]; + + if (!mapping) { + return null; + } + + url = `${assetHost}/emoji/${mapping.filename}.svg`; + } + + return ( +
+ {emoji.native + + {emoji.colons} +
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/components/autosuggest_hashtag.js b/app/javascript/flavours/glitch/components/autosuggest_hashtag.js deleted file mode 100644 index d787ed07ad..0000000000 --- a/app/javascript/flavours/glitch/components/autosuggest_hashtag.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ShortNumber from 'flavours/glitch/components/short_number'; -import { FormattedMessage } from 'react-intl'; - -export default class AutosuggestHashtag extends React.PureComponent { - - static propTypes = { - tag: PropTypes.shape({ - name: PropTypes.string.isRequired, - url: PropTypes.string, - history: PropTypes.array, - }).isRequired, - }; - - render() { - const { tag } = this.props; - const weeklyUses = tag.history && ( - total + day.uses * 1, 0)} - /> - ); - - return ( -
-
- #{tag.name} -
- {tag.history !== undefined && ( -
- -
- )} -
- ); - } - -} diff --git a/app/javascript/flavours/glitch/components/autosuggest_hashtag.tsx b/app/javascript/flavours/glitch/components/autosuggest_hashtag.tsx new file mode 100644 index 0000000000..932370884a --- /dev/null +++ b/app/javascript/flavours/glitch/components/autosuggest_hashtag.tsx @@ -0,0 +1,42 @@ +import { FormattedMessage } from 'react-intl'; + +import ShortNumber from 'flavours/glitch/components/short_number'; + +interface Props { + tag: { + name: string; + url?: string; + history?: Array<{ + uses: number; + accounts: string; + day: string; + }>; + following?: boolean; + type: 'hashtag'; + }; +} + +export const AutosuggestHashtag: React.FC = ({ tag }) => { + const weeklyUses = tag.history && ( + total + day.uses * 1, 0)} + /> + ); + + return ( +
+
+ #{tag.name} +
+ {tag.history !== undefined && ( +
+ +
+ )} +
+ ); +}; diff --git a/app/javascript/flavours/glitch/components/autosuggest_input.js b/app/javascript/flavours/glitch/components/autosuggest_input.js deleted file mode 100644 index 1a030fb858..0000000000 --- a/app/javascript/flavours/glitch/components/autosuggest_input.js +++ /dev/null @@ -1,227 +0,0 @@ -import React from 'react'; -import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container'; -import AutosuggestEmoji from './autosuggest_emoji'; -import AutosuggestHashtag from './autosuggest_hashtag'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import classNames from 'classnames'; - -const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => { - let word; - - let left = str.slice(0, caretPosition).search(/[^\s\u200B]+$/); - let right = str.slice(caretPosition).search(/[\s\u200B]/); - - if (right < 0) { - word = str.slice(left); - } else { - word = str.slice(left, right + caretPosition); - } - - if (!word || word.trim().length < 3 || searchTokens.indexOf(word[0]) === -1) { - return [null, null]; - } - - word = word.trim().toLowerCase(); - - if (word.length > 0) { - return [left, word]; - } else { - return [null, null]; - } -}; - -export default class AutosuggestInput extends ImmutablePureComponent { - - static propTypes = { - value: PropTypes.string, - suggestions: ImmutablePropTypes.list, - disabled: PropTypes.bool, - placeholder: PropTypes.string, - onSuggestionSelected: PropTypes.func.isRequired, - onSuggestionsClearRequested: PropTypes.func.isRequired, - onSuggestionsFetchRequested: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - onKeyUp: PropTypes.func, - onKeyDown: PropTypes.func, - autoFocus: PropTypes.bool, - className: PropTypes.string, - id: PropTypes.string, - searchTokens: PropTypes.arrayOf(PropTypes.string), - maxLength: PropTypes.number, - lang: PropTypes.string, - spellCheck: PropTypes.string, - }; - - static defaultProps = { - autoFocus: true, - searchTokens: ['@', ':', '#'], - }; - - state = { - suggestionsHidden: true, - focused: false, - selectedSuggestion: 0, - lastToken: null, - tokenStart: 0, - }; - - onChange = (e) => { - const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart, this.props.searchTokens); - - if (token !== null && this.state.lastToken !== token) { - this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart }); - this.props.onSuggestionsFetchRequested(token); - } else if (token === null) { - this.setState({ lastToken: null }); - this.props.onSuggestionsClearRequested(); - } - - this.props.onChange(e); - }; - - onKeyDown = (e) => { - const { suggestions, disabled } = this.props; - const { selectedSuggestion, suggestionsHidden } = this.state; - - if (disabled) { - e.preventDefault(); - return; - } - - if (e.which === 229 || e.isComposing) { - // Ignore key events during text composition - // e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac) - return; - } - - switch(e.key) { - case 'Escape': - if (suggestions.size === 0 || suggestionsHidden) { - document.querySelector('.ui').parentElement.focus(); - } else { - e.preventDefault(); - this.setState({ suggestionsHidden: true }); - } - - break; - case 'ArrowDown': - if (suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) }); - } - - break; - case 'ArrowUp': - if (suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) }); - } - - break; - case 'Enter': - case 'Tab': - // Select suggestion - if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - e.stopPropagation(); - this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion)); - } - - break; - } - - if (e.defaultPrevented || !this.props.onKeyDown) { - return; - } - - this.props.onKeyDown(e); - }; - - onBlur = () => { - this.setState({ suggestionsHidden: true, focused: false }); - }; - - onFocus = () => { - this.setState({ focused: true }); - }; - - onSuggestionClick = (e) => { - const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index')); - e.preventDefault(); - this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); - this.input.focus(); - }; - - componentWillReceiveProps (nextProps) { - if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { - this.setState({ suggestionsHidden: false }); - } - } - - setInput = (c) => { - this.input = c; - }; - - renderSuggestion = (suggestion, i) => { - const { selectedSuggestion } = this.state; - let inner, key; - - if (suggestion.type === 'emoji') { - inner = ; - key = suggestion.id; - } else if (suggestion.type ==='hashtag') { - inner = ; - key = suggestion.name; - } else if (suggestion.type === 'account') { - inner = ; - key = suggestion.id; - } - - return ( -
- {inner} -
- ); - }; - - render () { - const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, lang, spellCheck } = this.props; - const { suggestionsHidden } = this.state; - - return ( -
- - -
- {suggestions.map(this.renderSuggestion)} -
-
- ); - } - -} diff --git a/app/javascript/flavours/glitch/components/autosuggest_input.jsx b/app/javascript/flavours/glitch/components/autosuggest_input.jsx new file mode 100644 index 0000000000..f0833c8c6b --- /dev/null +++ b/app/javascript/flavours/glitch/components/autosuggest_input.jsx @@ -0,0 +1,230 @@ +import PropTypes from 'prop-types'; + +import classNames from 'classnames'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container'; + +import AutosuggestEmoji from './autosuggest_emoji'; +import { AutosuggestHashtag } from './autosuggest_hashtag'; + +const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => { + let word; + + let left = str.slice(0, caretPosition).search(/[^\s\u200B]+$/); + let right = str.slice(caretPosition).search(/[\s\u200B]/); + + if (right < 0) { + word = str.slice(left); + } else { + word = str.slice(left, right + caretPosition); + } + + if (!word || word.trim().length < 3 || searchTokens.indexOf(word[0]) === -1) { + return [null, null]; + } + + word = word.trim().toLowerCase(); + + if (word.length > 0) { + return [left, word]; + } else { + return [null, null]; + } +}; + +export default class AutosuggestInput extends ImmutablePureComponent { + + static propTypes = { + value: PropTypes.string, + suggestions: ImmutablePropTypes.list, + disabled: PropTypes.bool, + placeholder: PropTypes.string, + onSuggestionSelected: PropTypes.func.isRequired, + onSuggestionsClearRequested: PropTypes.func.isRequired, + onSuggestionsFetchRequested: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onKeyUp: PropTypes.func, + onKeyDown: PropTypes.func, + autoFocus: PropTypes.bool, + className: PropTypes.string, + id: PropTypes.string, + searchTokens: PropTypes.arrayOf(PropTypes.string), + maxLength: PropTypes.number, + lang: PropTypes.string, + spellCheck: PropTypes.bool, + }; + + static defaultProps = { + autoFocus: true, + searchTokens: ['@', ':', '#'], + }; + + state = { + suggestionsHidden: true, + focused: false, + selectedSuggestion: 0, + lastToken: null, + tokenStart: 0, + }; + + onChange = (e) => { + const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart, this.props.searchTokens); + + if (token !== null && this.state.lastToken !== token) { + this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart }); + this.props.onSuggestionsFetchRequested(token); + } else if (token === null) { + this.setState({ lastToken: null }); + this.props.onSuggestionsClearRequested(); + } + + this.props.onChange(e); + }; + + onKeyDown = (e) => { + const { suggestions, disabled } = this.props; + const { selectedSuggestion, suggestionsHidden } = this.state; + + if (disabled) { + e.preventDefault(); + return; + } + + if (e.which === 229 || e.isComposing) { + // Ignore key events during text composition + // e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac) + return; + } + + switch(e.key) { + case 'Escape': + if (suggestions.size === 0 || suggestionsHidden) { + document.querySelector('.ui').parentElement.focus(); + } else { + e.preventDefault(); + this.setState({ suggestionsHidden: true }); + } + + break; + case 'ArrowDown': + if (suggestions.size > 0 && !suggestionsHidden) { + e.preventDefault(); + this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) }); + } + + break; + case 'ArrowUp': + if (suggestions.size > 0 && !suggestionsHidden) { + e.preventDefault(); + this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) }); + } + + break; + case 'Enter': + case 'Tab': + // Select suggestion + if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) { + e.preventDefault(); + e.stopPropagation(); + this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion)); + } + + break; + } + + if (e.defaultPrevented || !this.props.onKeyDown) { + return; + } + + this.props.onKeyDown(e); + }; + + onBlur = () => { + this.setState({ suggestionsHidden: true, focused: false }); + }; + + onFocus = () => { + this.setState({ focused: true }); + }; + + onSuggestionClick = (e) => { + const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index')); + e.preventDefault(); + this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); + this.input.focus(); + }; + + UNSAFE_componentWillReceiveProps (nextProps) { + if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { + this.setState({ suggestionsHidden: false }); + } + } + + setInput = (c) => { + this.input = c; + }; + + renderSuggestion = (suggestion, i) => { + const { selectedSuggestion } = this.state; + let inner, key; + + if (suggestion.type === 'emoji') { + inner = ; + key = suggestion.id; + } else if (suggestion.type ==='hashtag') { + inner = ; + key = suggestion.name; + } else if (suggestion.type === 'account') { + inner = ; + key = suggestion.id; + } + + return ( +
+ {inner} +
+ ); + }; + + render () { + const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, lang, spellCheck } = this.props; + const { suggestionsHidden } = this.state; + + return ( +
+ + +
+ {suggestions.map(this.renderSuggestion)} +
+
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/components/autosuggest_textarea.js b/app/javascript/flavours/glitch/components/autosuggest_textarea.js deleted file mode 100644 index 6e6e567b93..0000000000 --- a/app/javascript/flavours/glitch/components/autosuggest_textarea.js +++ /dev/null @@ -1,235 +0,0 @@ -import React from 'react'; -import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container'; -import AutosuggestEmoji from './autosuggest_emoji'; -import AutosuggestHashtag from './autosuggest_hashtag'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import Textarea from 'react-textarea-autosize'; -import classNames from 'classnames'; - -const textAtCursorMatchesToken = (str, caretPosition) => { - let word; - - let left = str.slice(0, caretPosition).search(/[^\s\u200B]+$/); - let right = str.slice(caretPosition).search(/[\s\u200B]/); - - if (right < 0) { - word = str.slice(left); - } else { - word = str.slice(left, right + caretPosition); - } - - if (!word || word.trim().length < 3 || ['@', ':', '#'].indexOf(word[0]) === -1) { - return [null, null]; - } - - word = word.trim().toLowerCase(); - - if (word.length > 0) { - return [left, word]; - } else { - return [null, null]; - } -}; - -export default class AutosuggestTextarea extends ImmutablePureComponent { - - static propTypes = { - value: PropTypes.string, - suggestions: ImmutablePropTypes.list, - disabled: PropTypes.bool, - placeholder: PropTypes.string, - onSuggestionSelected: PropTypes.func.isRequired, - onSuggestionsClearRequested: PropTypes.func.isRequired, - onSuggestionsFetchRequested: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - onKeyUp: PropTypes.func, - onKeyDown: PropTypes.func, - onPaste: PropTypes.func.isRequired, - autoFocus: PropTypes.bool, - lang: PropTypes.string, - }; - - static defaultProps = { - autoFocus: true, - }; - - state = { - suggestionsHidden: true, - focused: false, - selectedSuggestion: 0, - lastToken: null, - tokenStart: 0, - }; - - onChange = (e) => { - const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart); - - if (token !== null && this.state.lastToken !== token) { - this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart }); - this.props.onSuggestionsFetchRequested(token); - } else if (token === null) { - this.setState({ lastToken: null }); - this.props.onSuggestionsClearRequested(); - } - - this.props.onChange(e); - }; - - onKeyDown = (e) => { - const { suggestions, disabled } = this.props; - const { selectedSuggestion, suggestionsHidden } = this.state; - - if (disabled) { - e.preventDefault(); - return; - } - - if (e.which === 229 || e.isComposing) { - // Ignore key events during text composition - // e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac) - return; - } - - switch(e.key) { - case 'Escape': - if (suggestions.size === 0 || suggestionsHidden) { - document.querySelector('.ui').parentElement.focus(); - } else { - e.preventDefault(); - this.setState({ suggestionsHidden: true }); - } - - break; - case 'ArrowDown': - if (suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) }); - } - - break; - case 'ArrowUp': - if (suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) }); - } - - break; - case 'Enter': - case 'Tab': - // Select suggestion - if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - e.stopPropagation(); - this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion)); - } - - break; - } - - if (e.defaultPrevented || !this.props.onKeyDown) { - return; - } - - this.props.onKeyDown(e); - }; - - onBlur = () => { - this.setState({ suggestionsHidden: true, focused: false }); - }; - - onFocus = (e) => { - this.setState({ focused: true }); - if (this.props.onFocus) { - this.props.onFocus(e); - } - }; - - onSuggestionClick = (e) => { - const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index')); - e.preventDefault(); - this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); - this.textarea.focus(); - }; - - componentWillReceiveProps (nextProps) { - if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { - this.setState({ suggestionsHidden: false }); - } - } - - setTextarea = (c) => { - this.textarea = c; - }; - - onPaste = (e) => { - if (e.clipboardData && e.clipboardData.files.length === 1) { - this.props.onPaste(e.clipboardData.files); - e.preventDefault(); - } - }; - - renderSuggestion = (suggestion, i) => { - const { selectedSuggestion } = this.state; - let inner, key; - - if (suggestion.type === 'emoji') { - inner = ; - key = suggestion.id; - } else if (suggestion.type === 'hashtag') { - inner = ; - key = suggestion.name; - } else if (suggestion.type === 'account') { - inner = ; - key = suggestion.id; - } - - return ( -
- {inner} -
- ); - }; - - render () { - const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, lang, children } = this.props; - const { suggestionsHidden } = this.state; - - return [ -
-
-