diff --git a/.annotaterb.yml b/.annotaterb.yml new file mode 100644 index 0000000000..df8e92b247 --- /dev/null +++ b/.annotaterb.yml @@ -0,0 +1,59 @@ +--- +:position: before +:position_in_additional_file_patterns: before +:position_in_class: before +:position_in_factory: before +:position_in_fixture: before +:position_in_routes: before +:position_in_serializer: before +:position_in_test: before +:classified_sort: true +:exclude_controllers: true +:exclude_factories: true +:exclude_fixtures: true +:exclude_helpers: true +:exclude_scaffolds: true +:exclude_serializers: true +:exclude_sti_subclasses: true +:exclude_tests: true +:force: false +:format_markdown: false +:format_rdoc: false +:format_yard: false +:frozen: false +:ignore_model_sub_dir: false +:ignore_unknown_models: false +:include_version: false +:show_complete_foreign_keys: false +:show_foreign_keys: false +:show_indexes: false +:simple_indexes: false +:sort: false +:timestamp: false +:trace: false +:with_comment: true +:with_column_comments: true +:with_table_comments: true +:active_admin: false +:command: +:debug: false +:hide_default_column_types: '' +:hide_limit_column_types: 'integer,boolean' +:ignore_columns: +:ignore_routes: +:models: true +:routes: false +:skip_on_db_migrate: false +:target_action: :do_annotations +:wrapper: +:wrapper_close: +:wrapper_open: +:classes_default_to_s: [] +:additional_file_patterns: [] +:model_dir: + - app/models +:require: [] +:root_dir: + - '' + +:show_check_constraints: false diff --git a/.browserslistrc b/.browserslistrc index 6367e4d358..0135379d6e 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -1,10 +1,6 @@ -[production] defaults > 0.2% firefox >= 78 ios >= 15.6 not dead not OperaMini all - -[development] -supports es6-module diff --git a/.bundler-audit.yml b/.bundler-audit.yml deleted file mode 100644 index 3d22165690..0000000000 --- a/.bundler-audit.yml +++ /dev/null @@ -1,6 +0,0 @@ -ignore: - - CVE-2024-53985 - - CVE-2024-53986 - - CVE-2024-53987 - - CVE-2024-53988 - - CVE-2024-53989 diff --git a/.devcontainer/compose.yaml b/.devcontainer/compose.yaml index 5c7263c874..5da1ec3a24 100644 --- a/.devcontainer/compose.yaml +++ b/.devcontainer/compose.yaml @@ -10,6 +10,7 @@ services: RAILS_ENV: development NODE_ENV: development BIND: 0.0.0.0 + BOOTSNAP_CACHE_DIR: /tmp REDIS_HOST: redis REDIS_PORT: '6379' DB_HOST: db @@ -20,12 +21,13 @@ services: ES_HOST: es ES_PORT: '9200' LIBRE_TRANSLATE_ENDPOINT: http://libretranslate:5000 + LOCAL_DOMAIN: ${LOCAL_DOMAIN:-localhost:3000} # 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:3035:3035' - - '127.0.0.1:4000:4000' + - '3000:3000' + - '3035:3035' + - '4000:4000' networks: - external_network - internal_network @@ -69,7 +71,7 @@ services: hard: -1 libretranslate: - image: libretranslate/libretranslate:v1.6.1 + image: libretranslate/libretranslate:v1.6.2 restart: unless-stopped volumes: - lt-data:/home/libretranslate/.local diff --git a/.dockerignore b/.dockerignore index 41da718049..9d990ab9ce 100644 --- a/.dockerignore +++ b/.dockerignore @@ -20,3 +20,9 @@ postgres14 redis elasticsearch chart +.yarn/ +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions diff --git a/.env.production.sample b/.env.production.sample index 1faaf5b57c..a311ad5f8d 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -79,6 +79,9 @@ AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= S3_ALIAS_HOST=files.example.com +# Optional list of hosts that are allowed to serve media for your instance +# EXTRA_MEDIA_HOSTS=https://data.example1.com,https://data.example2.com + # IP and session retention # ----------------------- # Make sure to modify the scheduling of ip_cleanup_scheduler in config/sidekiq.yml @@ -86,3 +89,27 @@ S3_ALIAS_HOST=files.example.com # ----------------------- IP_RETENTION_PERIOD=31556952 SESSION_RETENTION_PERIOD=31556952 + +# Fetch All Replies Behavior +# -------------------------- +# When a user expands a post (DetailedStatus view), fetch all of its replies +# (default: false) +FETCH_REPLIES_ENABLED=false + +# Period to wait between fetching replies (in minutes) +FETCH_REPLIES_COOLDOWN_MINUTES=15 + +# Period to wait after a post is first created before fetching its replies (in minutes) +FETCH_REPLIES_INITIAL_WAIT_MINUTES=5 + +# Max number of replies to fetch - total, recursively through a whole reply tree +FETCH_REPLIES_MAX_GLOBAL=1000 + +# Max number of replies to fetch - for a single post +FETCH_REPLIES_MAX_SINGLE=500 + +# Max number of replies Collection pages to fetch - total +FETCH_REPLIES_MAX_PAGES=500 + +# Maximum allowed character count +MAX_CHARS=5555 diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index d4930e1f52..0000000000 --- a/.eslintignore +++ /dev/null @@ -1,13 +0,0 @@ -/build/** -/coverage/** -/db/** -/lib/** -/log/** -/node_modules/** -/nonobox/** -/public/** -!/public/embed.js -/spec/** -/tmp/** -/vendor/** -!.eslintrc.js diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index b6e4253e61..0000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,368 +0,0 @@ -// @ts-check -const { defineConfig } = require('eslint-define-config'); - -module.exports = defineConfig({ - root: true, - - extends: [ - 'eslint:recommended', - 'plugin:react/recommended', - 'plugin:react-hooks/recommended', - 'plugin:jsx-a11y/recommended', - 'plugin:import/recommended', - 'plugin:promise/recommended', - 'plugin:jsdoc/recommended', - ], - - env: { - browser: true, - node: true, - es6: true, - }, - - parser: '@typescript-eslint/parser', - - plugins: [ - 'react', - 'jsx-a11y', - 'import', - 'promise', - '@typescript-eslint', - 'formatjs', - ], - - parserOptions: { - sourceType: 'module', - ecmaFeatures: { - jsx: true, - }, - ecmaVersion: 2021, - requireConfigFile: false, - babelOptions: { - configFile: false, - presets: ['@babel/react', '@babel/env'], - }, - }, - - settings: { - react: { - version: 'detect', - }, - 'import/ignore': [ - 'node_modules', - '\\.(css|scss|json)$', - ], - 'import/resolver': { - typescript: {}, - }, - }, - - rules: { - 'consistent-return': 'error', - 'dot-notation': 'error', - eqeqeq: ['error', 'always', { 'null': 'ignore' }], - 'indent': ['error', 2], - 'jsx-quotes': ['error', 'prefer-single'], - 'semi': ['error', 'always'], - 'no-case-declarations': 'off', - 'no-catch-shadow': 'error', - 'no-console': [ - 'warn', - { - allow: [ - 'error', - 'warn', - ], - }, - ], - 'no-empty': ['error', { "allowEmptyCatch": true }], - 'no-restricted-properties': [ - 'error', - { 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-unused-expressions': 'error', - 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': [ - 'error', - { - vars: 'all', - args: 'after-used', - destructuredArrayIgnorePattern: '^_', - ignoreRestSiblings: true, - }, - ], - 'valid-typeof': 'error', - - 'react/jsx-filename-extension': ['error', { extensions: ['.jsx', 'tsx'] }], - 'react/jsx-boolean-value': 'error', - 'react/display-name': 'off', - 'react/jsx-fragments': ['error', 'syntax'], - 'react/jsx-equals-spacing': 'error', - '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/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/v6.8.0/src/index.js#L46 - 'jsx-a11y/click-events-have-key-events': 'off', - 'jsx-a11y/label-has-associated-control': 'off', - 'jsx-a11y/media-has-caption': 'off', - 'jsx-a11y/no-autofocus': 'off', - // recommended rule is: - // 'jsx-a11y/no-interactive-element-to-noninteractive-role': [ - // 'error', - // { - // tr: ['none', 'presentation'], - // canvas: ['img'], - // }, - // ], - 'jsx-a11y/no-interactive-element-to-noninteractive-role': 'off', - // recommended rule is: - // 'jsx-a11y/no-noninteractive-tabindex': [ - // 'error', - // { - // tags: [], - // roles: ['tabpanel'], - // allowExpressionValues: true, - // }, - // ], - 'jsx-a11y/no-noninteractive-tabindex': 'off', - // recommended is full 'error' - 'jsx-a11y/no-static-element-interactions': [ - 'warn', - { - handlers: [ - 'onClick', - ], - }, - ], - - // See https://github.com/import-js/eslint-plugin-import/blob/v2.29.1/config/recommended.js - 'import/extensions': [ - 'error', - '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: [ - '.eslintrc.js', - '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,react-router-dom}', - group: 'external', - position: 'before', - }, - // Immutable / Redux / data store - { - pattern: '{immutable,@reduxjs/toolkit,react-redux,react-immutable-proptypes,react-immutable-pure-component}', - group: 'external', - position: 'before', - }, - // Internal packages - { - pattern: '{mastodon/**}', - 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-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: [ - '.eslintrc.js', - '*.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/strict-type-checked', - 'plugin:@typescript-eslint/stylistic-type-checked', - 'plugin:react/recommended', - 'plugin:react-hooks/recommended', - 'plugin:jsx-a11y/recommended', - 'plugin:import/recommended', - 'plugin:import/typescript', - 'plugin:promise/recommended', - 'plugin:jsdoc/recommended-typescript', - ], - - parserOptions: { - projectService: true, - tsconfigRootDir: __dirname, - }, - - rules: { - // Disable formatting rules that have been enabled in the base config - 'indent': 'off', - - // This is not needed as we use noImplicitReturns, which handles this in addition to understanding types - 'consistent-return': 'off', - - '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', - "@typescript-eslint/prefer-nullish-coalescing": ['error', { ignorePrimitives: { boolean: true } }], - "@typescript-eslint/no-restricted-imports": [ - "warn", - { - "name": "react-redux", - "importNames": ["useSelector", "useDispatch"], - "message": "Use typed hooks `useAppDispatch` and `useAppSelector` instead." - } - ], - "@typescript-eslint/restrict-template-expressions": ['warn', { allowNumber: true }], - '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, - }, - } - ], -}); diff --git a/.github/ISSUE_TEMPLATE/3.troubleshooting.yml b/.github/ISSUE_TEMPLATE/3.troubleshooting.yml new file mode 100644 index 0000000000..fa9bfc7c80 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3.troubleshooting.yml @@ -0,0 +1,74 @@ +name: Deployment troubleshooting +description: | + You are a server administrator and you are encountering a technical issue during installation, upgrade or operations of Mastodon. +labels: ['status/to triage'] +type: 'Troubleshooting' +body: + - type: markdown + attributes: + value: | + Make sure that you are submitting a new bug that was not previously reported or already fixed. + + Please use a concise and distinct title for the issue. + - type: textarea + attributes: + label: Steps to reproduce the problem + description: What were you trying to do? + value: | + 1. + 2. + 3. + ... + validations: + required: true + - type: input + attributes: + label: Expected behaviour + description: What should have happened? + validations: + required: true + - type: input + attributes: + label: Actual behaviour + description: What happened? + validations: + required: true + - type: textarea + attributes: + label: Detailed description + validations: + required: false + - type: input + attributes: + label: Mastodon instance + description: The address of the Mastodon instance where you experienced the issue + placeholder: mastodon.social + validations: + required: true + - type: input + attributes: + label: Mastodon version + description: | + This is displayed at the bottom of the About page, eg. `v4.4.0-alpha.1` + placeholder: v4.3.0 + validations: + required: false + - type: textarea + attributes: + label: Environment + description: | + Details about your environment, like how Mastodon is deployed, if containers are used, version numbers, etc. + value: | + Please at least include those informations: + - Operating system: (eg. Ubuntu 22.04) + - Ruby version: (from `ruby --version`, eg. v3.4.1) + - Node.js version: (from `node --version`, eg. v20.18.0) + validations: + required: false + - type: textarea + attributes: + label: Technical details + description: | + Any additional technical details you may have, like logs or error traces + validations: + required: false diff --git a/.github/actions/setup-ruby/action.yml b/.github/actions/setup-ruby/action.yml index 672a06f9ea..3e232f134c 100644 --- a/.github/actions/setup-ruby/action.yml +++ b/.github/actions/setup-ruby/action.yml @@ -21,4 +21,3 @@ runs: with: ruby-version: ${{ inputs.ruby-version }} bundler-cache: true - cache-version: 4.3 diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 8a10676283..e638b9c548 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -15,6 +15,8 @@ // to `null` after any other rule set it to something. dependencyDashboardHeader: 'This issue lists Renovate updates and detected dependencies. Read the [Dependency Dashboard](https://docs.renovatebot.com/key-concepts/dashboard/) docs to learn more. Before approving any upgrade: read the description and comments in the [`renovate.json5` file](https://github.com/mastodon/mastodon/blob/main/.github/renovate.json5).', postUpdateOptions: ['yarnDedupeHighest'], + // The types are now included in recent versions,we ignore them here until we upgrade and remove the dependency + ignoreDeps: ['@types/emoji-mart'], packageRules: [ { // Require Dependency Dashboard Approval for major version bumps of these node packages @@ -97,7 +99,13 @@ { // Group all eslint-related packages with `eslint` in the same PR matchManagers: ['npm'], - matchPackageNames: ['eslint', 'eslint-*', '@typescript-eslint/*'], + matchPackageNames: [ + 'eslint', + 'eslint-*', + 'typescript-eslint', + '@eslint/*', + 'globals', + ], matchUpdateTypes: ['patch', 'minor'], groupName: 'eslint (non-major)', }, diff --git a/.github/workflows/bundler-audit.yml b/.github/workflows/bundler-audit.yml index f80c5837b2..c96e4429af 100644 --- a/.github/workflows/bundler-audit.yml +++ b/.github/workflows/bundler-audit.yml @@ -39,4 +39,4 @@ jobs: bundler-cache: true - name: Run bundler-audit - run: bundle exec bundler-audit check --update + run: bin/bundler-audit check --update diff --git a/.github/workflows/check-i18n.yml b/.github/workflows/check-i18n.yml index 7cb0c0dd98..63529e4f16 100644 --- a/.github/workflows/check-i18n.yml +++ b/.github/workflows/check-i18n.yml @@ -24,7 +24,7 @@ permissions: jobs: check-i18n: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -41,18 +41,18 @@ jobs: git diff --exit-code - name: Check locale file normalization - run: bundle exec i18n-tasks check-normalized + run: bin/i18n-tasks check-normalized - name: Check for unused strings - run: bundle exec i18n-tasks unused + run: bin/i18n-tasks unused - name: Check for missing strings in English YML run: | - bundle exec i18n-tasks add-missing -l en + bin/i18n-tasks add-missing -l en git diff --exit-code - name: Check for wrong string interpolations - run: bundle exec i18n-tasks check-consistent-interpolations + run: bin/i18n-tasks check-consistent-interpolations - name: Check that all required locale files exist - run: bundle exec rake repo:check_locales_files + run: bin/rake repo:check_locales_files diff --git a/.github/workflows/crowdin-download-stable.yml b/.github/workflows/crowdin-download-stable.yml index de21e2e58f..6d9a058629 100644 --- a/.github/workflows/crowdin-download-stable.yml +++ b/.github/workflows/crowdin-download-stable.yml @@ -46,11 +46,11 @@ jobs: uses: ./.github/actions/setup-ruby - name: Run i18n normalize task - run: bundle exec i18n-tasks normalize + run: bin/i18n-tasks normalize # Create or update the pull request - name: Create Pull Request - uses: peter-evans/create-pull-request@v7.0.5 + uses: peter-evans/create-pull-request@v7.0.6 with: commit-message: 'New Crowdin translations' title: 'New Crowdin Translations for ${{ github.base_ref || github.ref_name }} (automated)' diff --git a/.github/workflows/lint-css.yml b/.github/workflows/lint-css.yml index f379c56112..ffab4880e1 100644 --- a/.github/workflows/lint-css.yml +++ b/.github/workflows/lint-css.yml @@ -43,4 +43,4 @@ jobs: uses: ./.github/actions/setup-javascript - name: Stylelint - run: yarn lint:css -f github + run: yarn lint:css --custom-formatter @csstools/stylelint-formatter-github diff --git a/.github/workflows/lint-haml.yml b/.github/workflows/lint-haml.yml index fb40585c37..c596261eb0 100644 --- a/.github/workflows/lint-haml.yml +++ b/.github/workflows/lint-haml.yml @@ -46,4 +46,4 @@ jobs: - name: Run haml-lint run: | echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json" - bundle exec haml-lint --reporter github + bin/haml-lint --reporter github diff --git a/.github/workflows/lint-js.yml b/.github/workflows/lint-js.yml index 621a662387..13468e7799 100644 --- a/.github/workflows/lint-js.yml +++ b/.github/workflows/lint-js.yml @@ -14,7 +14,7 @@ on: - 'tsconfig.json' - '.nvmrc' - '.prettier*' - - '.eslint*' + - 'eslint.config.mjs' - '**/*.js' - '**/*.jsx' - '**/*.ts' @@ -28,7 +28,7 @@ on: - 'tsconfig.json' - '.nvmrc' - '.prettier*' - - '.eslint*' + - 'eslint.config.mjs' - '**/*.js' - '**/*.jsx' - '**/*.ts' @@ -47,7 +47,7 @@ jobs: uses: ./.github/actions/setup-javascript - name: ESLint - run: yarn lint:js --max-warnings 0 + run: yarn workspaces foreach --all --parallel run lint:js --max-warnings 0 - name: Typecheck run: yarn typecheck diff --git a/.github/workflows/lint-ruby.yml b/.github/workflows/lint-ruby.yml index 1d4395e9ac..5bb67b108c 100644 --- a/.github/workflows/lint-ruby.yml +++ b/.github/workflows/lint-ruby.yml @@ -12,6 +12,7 @@ on: - 'Gemfile*' - '.rubocop*.yml' - '.ruby-version' + - 'bin/rubocop' - 'config/brakeman.ignore' - '**/*.rb' - '**/*.rake' @@ -22,6 +23,7 @@ on: - 'Gemfile*' - '.rubocop*.yml' - '.ruby-version' + - 'bin/rubocop' - 'config/brakeman.ignore' - '**/*.rb' - '**/*.rake' diff --git a/.github/workflows/test-migrations.yml b/.github/workflows/test-migrations.yml index 892f59f941..c4a716e8f9 100644 --- a/.github/workflows/test-migrations.yml +++ b/.github/workflows/test-migrations.yml @@ -15,6 +15,7 @@ on: - '**/*.rb' - '.github/workflows/test-migrations.yml' - 'lib/tasks/tests.rake' + - 'lib/tasks/db.rake' pull_request: paths: @@ -66,7 +67,6 @@ jobs: DB_HOST: localhost DB_USER: postgres DB_PASS: postgres - DISABLE_SIMPLECOV: true RAILS_ENV: test BUNDLE_CLEAN: true BUNDLE_FROZEN: true @@ -80,6 +80,18 @@ jobs: - name: Set up Ruby environment uses: ./.github/actions/setup-ruby + - name: Ensure no errors with `db:prepare` + run: | + bin/rails db:drop + bin/rails db:prepare + bin/rails db:migrate + + - name: Ensure no errors with `db:prepare` and SKIP_POST_DEPLOYMENT_MIGRATIONS + run: | + bin/rails db:drop + SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails db:prepare + bin/rails db:migrate + - name: Test "one step migration" flow run: | bin/rails db:drop @@ -93,6 +105,11 @@ jobs: bin/rails db:drop bin/rails db:create SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails tests:migrations:prepare_database + + # Migrate up to v4.2.0 breakpoint + bin/rails db:migrate VERSION=20230907150100 + + # Migrate the rest SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails db:migrate bin/rails db:migrate bin/rails tests:migrations:check_database diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index ee22e19bee..fd4c666059 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -110,7 +110,7 @@ jobs: DB_HOST: localhost DB_USER: postgres DB_PASS: postgres - DISABLE_SIMPLECOV: ${{ matrix.ruby-version != '.ruby-version' }} + COVERAGE: ${{ matrix.ruby-version == '.ruby-version' }} RAILS_ENV: test ALLOW_NOPAM: true PAM_ENABLED: true @@ -128,10 +128,9 @@ jobs: fail-fast: false matrix: ruby-version: - - '3.1' - '3.2' + - '3.3' - '.ruby-version' - - '3.4' steps: - uses: actions/checkout@v4 @@ -172,7 +171,7 @@ jobs: - name: Upload coverage reports to Codecov if: matrix.ruby-version == '.ruby-version' - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: files: coverage/lcov/*.lcov env: @@ -180,7 +179,7 @@ jobs: test-libvips: name: Libvips tests - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest needs: - build @@ -213,7 +212,7 @@ jobs: DB_HOST: localhost DB_USER: postgres DB_PASS: postgres - DISABLE_SIMPLECOV: ${{ matrix.ruby-version != '.ruby-version' }} + COVERAGE: ${{ matrix.ruby-version == '.ruby-version' }} RAILS_ENV: test ALLOW_NOPAM: true PAM_ENABLED: true @@ -231,10 +230,9 @@ jobs: fail-fast: false matrix: ruby-version: - - '3.1' - '3.2' + - '3.3' - '.ruby-version' - - '3.4' steps: - uses: actions/checkout@v4 @@ -260,7 +258,7 @@ jobs: - name: Upload coverage reports to Codecov if: matrix.ruby-version == '.ruby-version' - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: files: coverage/lcov/mastodon.lcov env: @@ -301,7 +299,6 @@ jobs: DB_HOST: localhost DB_USER: postgres DB_PASS: postgres - DISABLE_SIMPLECOV: true RAILS_ENV: test BUNDLE_WITH: test ES_ENABLED: false @@ -312,10 +309,9 @@ jobs: fail-fast: false matrix: ruby-version: - - '3.1' - '3.2' + - '3.3' - '.ruby-version' - - '3.4' steps: - uses: actions/checkout@v4 @@ -419,7 +415,6 @@ jobs: DB_HOST: localhost DB_USER: postgres DB_PASS: postgres - DISABLE_SIMPLECOV: true RAILS_ENV: test BUNDLE_WITH: test ES_ENABLED: true @@ -430,10 +425,9 @@ jobs: fail-fast: false matrix: ruby-version: - - '3.1' - '3.2' + - '3.3' - '.ruby-version' - - '3.4' search-image: - docker.elastic.co/elasticsearch/elasticsearch:7.17.13 include: diff --git a/.nvmrc b/.nvmrc index 65da8ce391..744ca17ec0 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.17 +22.14 diff --git a/.prettierignore b/.prettierignore index 6b2f0c1889..80b4c0159e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -63,6 +63,7 @@ docker-compose.override.yml # Ignore emoji map file /app/javascript/mastodon/features/emoji/emoji_map.json +/app/javascript/mastodon/features/emoji/emoji_sheet.json # Ignore locale files /app/javascript/mastodon/locales/*.json diff --git a/.prettierrc.js b/.prettierrc.js index af39b253f6..65ec869c33 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,4 +1,4 @@ module.exports = { singleQuote: true, jsxSingleQuote: true -} +}; diff --git a/.rubocop.yml b/.rubocop.yml index 5bfa331d27..1bbba515af 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,7 +8,7 @@ AllCops: - lib/mastodon/migration_helpers.rb ExtraDetails: true NewCops: enable - TargetRubyVersion: 3.1 # Oldest supported ruby version + TargetRubyVersion: 3.2 # Oldest supported ruby version inherit_from: - .rubocop/layout.yml @@ -18,6 +18,7 @@ inherit_from: - .rubocop/rspec_rails.yml - .rubocop/rspec.yml - .rubocop/style.yml + - .rubocop/i18n.yml - .rubocop/custom.yml - .rubocop_todo.yml - .rubocop/strict.yml @@ -26,10 +27,10 @@ inherit_mode: merge: - Exclude -require: +plugins: + - rubocop-capybara + - rubocop-i18n + - rubocop-performance - rubocop-rails - rubocop-rspec - rubocop-rspec_rails - - rubocop-performance - - rubocop-capybara - - ./lib/linter/rubocop_middle_dot diff --git a/.rubocop/i18n.yml b/.rubocop/i18n.yml new file mode 100644 index 0000000000..de395d3a79 --- /dev/null +++ b/.rubocop/i18n.yml @@ -0,0 +1,12 @@ +I18n/RailsI18n: + Enabled: true + Exclude: + - 'config/**/*' + - 'db/**/*' + - 'lib/**/*' + - 'spec/**/*' +I18n/GetText: + Enabled: false + +I18n/RailsI18n/DecorateStringFormattingUsingInterpolation: + Enabled: false diff --git a/.rubocop/rails.yml b/.rubocop/rails.yml index ae31c1f266..bbd172e656 100644 --- a/.rubocop/rails.yml +++ b/.rubocop/rails.yml @@ -2,6 +2,9 @@ Rails/BulkChangeTable: Enabled: false # Conflicts with strong_migrations features +Rails/Delegate: + Enabled: false + Rails/FilePath: EnforcedStyle: arguments diff --git a/.rubocop/style.yml b/.rubocop/style.yml index 03e35a70ac..f59340d452 100644 --- a/.rubocop/style.yml +++ b/.rubocop/style.yml @@ -1,4 +1,7 @@ --- +Style/ArrayIntersect: + Enabled: false + Style/ClassAndModuleChildren: Enabled: false @@ -19,6 +22,13 @@ Style/HashSyntax: EnforcedShorthandSyntax: either EnforcedStyle: ruby19_no_mixed_keys +Style/IfUnlessModifier: + Exclude: + - '**/*.haml' + +Style/KeywordArgumentsMerging: + Enabled: false + Style/NumericLiterals: AllowedPatterns: - \d{4}_\d{2}_\d{2}_\d{6} @@ -37,6 +47,9 @@ Style/RedundantFetchBlock: Style/RescueStandardError: EnforcedStyle: implicit +Style/SafeNavigationChainLength: + Enabled: false + Style/SymbolArray: Enabled: false @@ -45,3 +58,6 @@ Style/TrailingCommaInArrayLiteral: Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: comma + +Style/WordArray: + MinSize: 3 # Override default of 2 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index cd5d365c67..13fb25d333 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.66.1. +# using RuboCop version 1.75.2. # 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 @@ -8,7 +8,7 @@ Lint/NonLocalExitFromIterator: Exclude: - - 'app/helpers/jsonld_helper.rb' + - 'app/helpers/json_ld_helper.rb' # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: @@ -39,7 +39,6 @@ Rails/OutputSafety: # Configuration parameters: AllowedVars. Style/FetchEnvVar: Exclude: - - 'app/lib/translation_service.rb' - 'config/environments/production.rb' - 'config/initializers/2_limited_federation_mode.rb' - 'config/initializers/3_omniauth.rb' @@ -50,7 +49,7 @@ Style/FetchEnvVar: - 'lib/tasks/repo.rake' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns. +# Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, Mode, AllowedMethods, AllowedPatterns. # SupportedStyles: annotated, template, unannotated # AllowedMethods: redirect Style/FormatStringToken: @@ -63,31 +62,10 @@ Style/FormatStringToken: Style/GuardClause: Enabled: false -# 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 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' - - 'lib/mastodon/migration_warning.rb' - # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? Style/OptionalBooleanParameter: Exclude: - - 'app/helpers/jsonld_helper.rb' - 'app/lib/admin/system_check/message.rb' - 'app/lib/request.rb' - 'app/lib/webfinger.rb' @@ -108,10 +86,3 @@ Style/RedundantConstantBase: Exclude: - 'config/environments/production.rb' - 'config/initializers/sidekiq.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: WordRegex. -# SupportedStyles: percent, brackets -Style/WordArray: - EnforcedStyle: percent - MinSize: 3 diff --git a/.ruby-version b/.ruby-version index fa7adc7ac7..6cb9d3dd0d 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.3.5 +3.4.3 diff --git a/CHANGELOG.md b/CHANGELOG.md index c05fdd679f..4dd4783597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,29 @@ All notable changes to this project will be documented in this file. +## [4.3.7] - 2025-04-02 + +### Add + +- Add delay to profile updates to debounce them (#34137 by @ClearlyClaire) +- Add support for paginating partial collections in `SynchronizeFollowersService` (#34272 and #34277 by @ClearlyClaire) + +### Changed + +- Change account suspensions to be federated to recently-followed accounts as well (#34294 by @ClearlyClaire) +- Change `AccountReachFinder` to consider statuses based on suspension date (#32805 and #34291 by @ClearlyClaire and @mjankowski) +- Change user archive signed URL TTL from 10 seconds to 1 hour (#34254 by @ClearlyClaire) + +### Fixed + +- Fix static version of animated PNG emojis not being properly extracted (#34337 by @ClearlyClaire) +- Fix filters not applying in detailed view, favourites and bookmarks (#34259 and #34260 by @ClearlyClaire) +- Fix handling of malformed/unusual HTML (#34201 by @ClearlyClaire) +- Fix `CacheBuster` being queued for missing media attachments (#34253 by @ClearlyClaire) +- Fix incorrect URL being used when cache busting (#34189 by @ClearlyClaire) +- Fix streaming server refusing unix socket path in `DATABASE_URL` (#34091 by @ClearlyClaire) +- Fix “x” hotkey not working on boosted filtered posts (#33758 by @ClearlyClaire) + ## [4.3.6] - 2025-03-13 ### Security @@ -187,7 +210,7 @@ The following changelog entries focus on changes visible to users, administrator - `GET /api/v2/notifications`: https://docs.joinmastodon.org/methods/grouped_notifications/#get-grouped - `GET /api/v2/notifications/:group_key`: https://docs.joinmastodon.org/methods/grouped_notifications/#get-notification-group - `GET /api/v2/notifications/:group_key/accounts`: https://docs.joinmastodon.org/methods/grouped_notifications/#get-group-accounts - - `POST /api/v2/notifications/:group_key/dimsiss`: https://docs.joinmastodon.org/methods/grouped_notifications/#dismiss-group + - `POST /api/v2/notifications/:group_key/dismiss`: https://docs.joinmastodon.org/methods/grouped_notifications/#dismiss-group - `GET /api/v2/notifications/:unread_count`: https://docs.joinmastodon.org/methods/grouped_notifications/#unread-group-count - **Add notification policies, filtered notifications and notification requests** (#29366, #29529, #29433, #29565, #29567, #29572, #29575, #29588, #29646, #29652, #29658, #29666, #29693, #29699, #29737, #29706, #29570, #29752, #29810, #29826, #30114, #30251, #30559, #29868, #31008, #31011, #30996, #31149, #31220, #31222, #31225, #31242, #31262, #31250, #31273, #31310, #31316, #31322, #31329, #31324, #31331, #31343, #31342, #31309, #31358, #31378, #31406, #31256, #31456, #31419, #31457, #31508, #31540, #31541, #31723, #32062 and #32281 by @ClearlyClaire, @Gargron, @TheEssem, @mgmn, @oneiros, and @renchap)\ The old “Block notifications from non-followers”, “Block notifications from people you don't follow” and “Block direct messages from people you don't follow” notification settings have been replaced by a new set of settings found directly in the notification column.\ @@ -518,7 +541,7 @@ The following changelog entries focus on changes visible to users, administrator - Fix empty environment variables not using default nil value (#27400 by @renchap) - Fix language sorting in settings (#27158 by @gunchleoc) -## |4.2.11] - 2024-08-16 +## [4.2.11] - 2024-08-16 ### Added diff --git a/Dockerfile b/Dockerfile index bb4e8947aa..6620f4c096 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ -# syntax=docker/dockerfile:1.9 +# syntax=docker/dockerfile:1.12 # This file is designed for production server deployment, not local development work -# For a containerized local dev environment, see: https://github.com/mastodon/mastodon/blob/main/README.md#docker +# For a containerized local dev environment, see: https://github.com/mastodon/mastodon/blob/main/docs/DEVELOPMENT.md#docker # Please see https://docs.docker.com/engine/reference/builder for information about # the extended buildx capabilities used in this file. @@ -9,19 +9,20 @@ # See: https://docs.docker.com/build/building/multi-platform/ ARG TARGETPLATFORM=${TARGETPLATFORM} ARG BUILDPLATFORM=${BUILDPLATFORM} +ARG BASE_REGISTRY="docker.io" -# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.3.x"] +# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"] # renovate: datasource=docker depName=docker.io/ruby -ARG RUBY_VERSION="3.3.5" -# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] +ARG RUBY_VERSION="3.4.2" +# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] # renovate: datasource=node-version depName=node -ARG NODE_MAJOR_VERSION="20" +ARG NODE_MAJOR_VERSION="22" # Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"] ARG DEBIAN_VERSION="bookworm" -# Node image to use for base image based on combined variables (ex: 20-bookworm-slim) -FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim AS node -# Ruby image to use for base image based on combined variables (ex: 3.3.x-slim-bookworm) -FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS ruby +# Node.js image to use for base image based on combined variables (ex: 20-bookworm-slim) +FROM ${BASE_REGISTRY}/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim AS node +# Ruby image to use for base image based on combined variables (ex: 3.4.x-slim-bookworm) +FROM ${BASE_REGISTRY}/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS ruby # Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA # Example: v4.3.0-nightly.2023.11.09+pr-123456 @@ -29,6 +30,8 @@ FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS ruby ARG MASTODON_VERSION_PRERELEASE="" # Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="pr-123456"] ARG MASTODON_VERSION_METADATA="" +# Will be available as Mastodon::Version.source_commit +ARG SOURCE_COMMIT="" # Allow Ruby on Rails to serve static files # See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files @@ -45,30 +48,31 @@ ARG GID="991" # Apply Mastodon build options based on options above ENV \ -# Apply Mastodon version information + # Apply Mastodon version information MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \ MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}" \ -# Apply Mastodon static files and YJIT options + SOURCE_COMMIT="${SOURCE_COMMIT}" \ + # Apply Mastodon static files and YJIT options RAILS_SERVE_STATIC_FILES=${RAILS_SERVE_STATIC_FILES} \ RUBY_YJIT_ENABLE=${RUBY_YJIT_ENABLE} \ -# Apply timezone + # Apply timezone TZ=${TZ} ENV \ -# Configure the IP to bind Mastodon to when serving traffic + # Configure the IP to bind Mastodon to when serving traffic BIND="0.0.0.0" \ -# Use production settings for Yarn, Node and related nodejs based tools + # Use production settings for Yarn, Node.js and related tools NODE_ENV="production" \ -# Use production settings for Ruby on Rails + # Use production settings for Ruby on Rails RAILS_ENV="production" \ -# Add Ruby and Mastodon installation to the PATH + # Add Ruby and Mastodon installation to the PATH DEBIAN_FRONTEND="noninteractive" \ PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" \ -# Optimize jemalloc 5.x performance + # Optimize jemalloc 5.x performance MALLOC_CONF="narenas:2,background_thread:true,thp:never,dirty_decay_ms:1000,muzzy_decay_ms:0" \ -# Enable libvips, should not be changed + # Enable libvips, should not be changed MASTODON_USE_LIBVIPS=true \ -# Sidekiq will touch tmp/sidekiq_process_has_started_and_will_begin_processing_jobs to indicate it is ready. This can be used for a readiness check in Kubernetes + # Sidekiq will touch tmp/sidekiq_process_has_started_and_will_begin_processing_jobs to indicate it is ready. This can be used for a readiness check in Kubernetes MASTODON_SIDEKIQ_READY_FILENAME=sidekiq_process_has_started_and_will_begin_processing_jobs # Set default shell used for running commands @@ -79,14 +83,14 @@ ARG TARGETPLATFORM RUN echo "Target platform is $TARGETPLATFORM" RUN \ -# Remove automatic apt cache Docker cleanup scripts + # Remove automatic apt cache Docker cleanup scripts rm -f /etc/apt/apt.conf.d/docker-clean; \ -# Sets timezone + # Sets timezone echo "${TZ}" > /etc/localtime; \ -# Creates mastodon user/group and sets home directory + # Creates mastodon user/group and sets home directory groupadd -g "${GID}" mastodon; \ useradd -l -u "${UID}" -g "${GID}" -m -d /opt/mastodon mastodon; \ -# Creates /mastodon symlink to /opt/mastodon + # Creates /mastodon symlink to /opt/mastodon ln -s /opt/mastodon /mastodon; # Set /opt/mastodon as working directory @@ -97,105 +101,92 @@ RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/ # hadolint ignore=DL3008,DL3005 RUN \ -# Mount Apt cache and lib directories from Docker buildx caches ---mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \ ---mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \ -# Apt update & upgrade to check for security updates to Debian image + # Mount Apt cache and lib directories from Docker buildx caches + --mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \ + --mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \ + # Apt update & upgrade to check for security updates to Debian image apt-get update; \ apt-get dist-upgrade -yq; \ -# Install jemalloc, curl and other necessary components + # Install jemalloc, curl and other necessary components apt-get install -y --no-install-recommends \ - curl \ - file \ - libjemalloc2 \ - patchelf \ - procps \ - tini \ - tzdata \ - wget \ + curl \ + file \ + libjemalloc2 \ + patchelf \ + procps \ + tini \ + tzdata \ + wget \ ; \ -# Patch Ruby to use jemalloc + # Patch Ruby to use jemalloc patchelf --add-needed libjemalloc.so.2 /usr/local/bin/ruby; \ -# Discard patchelf after use + # Discard patchelf after use apt-get purge -y \ - patchelf \ + patchelf \ ; # Create temporary build layer from base image FROM ruby AS build -# Copy Node package configuration files into working directory -COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/ -COPY .yarn /opt/mastodon/.yarn - -COPY --from=node /usr/local/bin /usr/local/bin -COPY --from=node /usr/local/lib /usr/local/lib - ARG TARGETPLATFORM # hadolint ignore=DL3008 RUN \ -# Mount Apt cache and lib directories from Docker buildx caches ---mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \ ---mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \ -# Install build tools and bundler dependencies from APT + # Mount Apt cache and lib directories from Docker buildx caches + --mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \ + --mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \ + # Install build tools and bundler dependencies from APT apt-get install -y --no-install-recommends \ - autoconf \ - automake \ - build-essential \ - cmake \ - git \ - libgdbm-dev \ - libglib2.0-dev \ - libgmp-dev \ - libicu-dev \ - libidn-dev \ - libpq-dev \ - libssl-dev \ - libtool \ - libyaml-dev \ - meson \ - nasm \ - pkg-config \ - shared-mime-info \ - xz-utils \ - # libvips components - libcgif-dev \ - libexif-dev \ - libexpat1-dev \ - libgirepository1.0-dev \ - libheif-dev/bookworm-backports \ - libimagequant-dev \ - libjpeg62-turbo-dev \ - liblcms2-dev \ - liborc-dev \ - libspng-dev \ - libtiff-dev \ - libwebp-dev \ + autoconf \ + automake \ + build-essential \ + cmake \ + git \ + libgdbm-dev \ + libglib2.0-dev \ + libgmp-dev \ + libicu-dev \ + libidn-dev \ + libpq-dev \ + libssl-dev \ + libtool \ + libyaml-dev \ + meson \ + nasm \ + pkg-config \ + shared-mime-info \ + xz-utils \ + # libvips components + libcgif-dev \ + libexif-dev \ + libexpat1-dev \ + libgirepository1.0-dev \ + libheif-dev/bookworm-backports \ + libimagequant-dev \ + libjpeg62-turbo-dev \ + liblcms2-dev \ + liborc-dev \ + libspng-dev \ + libtiff-dev \ + libwebp-dev \ # ffmpeg components - libdav1d-dev \ - liblzma-dev \ - libmp3lame-dev \ - libopus-dev \ - libsnappy-dev \ - libvorbis-dev \ - libvpx-dev \ - libx264-dev \ - libx265-dev \ + libdav1d-dev \ + liblzma-dev \ + libmp3lame-dev \ + libopus-dev \ + libsnappy-dev \ + libvorbis-dev \ + libvpx-dev \ + libx264-dev \ + libx265-dev \ ; -RUN \ -# Configure Corepack - rm /usr/local/bin/yarn*; \ - corepack enable; \ - corepack prepare --activate; - # Create temporary libvips specific build layer from build layer FROM build AS libvips # libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"] # renovate: datasource=github-releases depName=libvips packageName=libvips/libvips -ARG VIPS_VERSION=8.15.3 +ARG VIPS_VERSION=8.16.1 # libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"] ARG VIPS_URL=https://github.com/libvips/libvips/releases/download @@ -218,7 +209,7 @@ FROM build AS ffmpeg # ffmpeg version to compile, change with [--build-arg FFMPEG_VERSION="7.0.x"] # renovate: datasource=repology depName=ffmpeg packageName=openpkg_current/ffmpeg -ARG FFMPEG_VERSION=7.0.2 +ARG FFMPEG_VERSION=7.1 # ffmpeg download URL, change with [--build-arg FFMPEG_URL="https://ffmpeg.org/releases"] ARG FFMPEG_URL=https://ffmpeg.org/releases @@ -232,28 +223,28 @@ WORKDIR /usr/local/ffmpeg/src/ffmpeg-${FFMPEG_VERSION} # Configure and compile ffmpeg RUN \ ./configure \ - --prefix=/usr/local/ffmpeg \ - --toolchain=hardened \ - --disable-debug \ - --disable-devices \ - --disable-doc \ - --disable-ffplay \ - --disable-network \ - --disable-static \ - --enable-ffmpeg \ - --enable-ffprobe \ - --enable-gpl \ - --enable-libdav1d \ - --enable-libmp3lame \ - --enable-libopus \ - --enable-libsnappy \ - --enable-libvorbis \ - --enable-libvpx \ - --enable-libwebp \ - --enable-libx264 \ - --enable-libx265 \ - --enable-shared \ - --enable-version3 \ + --prefix=/usr/local/ffmpeg \ + --toolchain=hardened \ + --disable-debug \ + --disable-devices \ + --disable-doc \ + --disable-ffplay \ + --disable-network \ + --disable-static \ + --enable-ffmpeg \ + --enable-ffprobe \ + --enable-gpl \ + --enable-libdav1d \ + --enable-libmp3lame \ + --enable-libopus \ + --enable-libsnappy \ + --enable-libvorbis \ + --enable-libvpx \ + --enable-libwebp \ + --enable-libx264 \ + --enable-libx265 \ + --enable-shared \ + --enable-version3 \ ; \ make -j$(nproc); \ make install; @@ -267,58 +258,57 @@ ARG TARGETPLATFORM COPY Gemfile* /opt/mastodon/ RUN \ -# Mount Ruby Gem caches ---mount=type=cache,id=gem-cache-${TARGETPLATFORM},target=/usr/local/bundle/cache/,sharing=locked \ -# Configure bundle to prevent changes to Gemfile and Gemfile.lock + # Mount Ruby Gem caches + --mount=type=cache,id=gem-cache-${TARGETPLATFORM},target=/usr/local/bundle/cache/,sharing=locked \ + # Configure bundle to prevent changes to Gemfile and Gemfile.lock bundle config set --global frozen "true"; \ -# Configure bundle to not cache downloaded Gems + # Configure bundle to not cache downloaded Gems bundle config set --global cache_all "false"; \ -# Configure bundle to only process production Gems + # Configure bundle to only process production Gems bundle config set --local without "development test"; \ -# Configure bundle to not warn about root user + # Configure bundle to not warn about root user bundle config set silence_root_warning "true"; \ -# Download and install required Gems + # Download and install required Gems bundle install -j"$(nproc)"; -# Create temporary node specific build layer from build layer -FROM build AS yarn - -ARG TARGETPLATFORM - -# Copy Node package configuration files into working directory -COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/ -COPY streaming/package.json /opt/mastodon/streaming/ -COPY .yarn /opt/mastodon/.yarn - -# hadolint ignore=DL3008 -RUN \ ---mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \ ---mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \ -# Install Node packages - yarn workspaces focus --production @mastodon/mastodon; - # Create temporary assets build layer from build layer FROM build AS precompiler -# Copy Mastodon sources into precompiler layer +ARG TARGETPLATFORM + +# Copy Mastodon sources into layer COPY . /opt/mastodon/ -# Copy bundler and node packages from build layer to container -COPY --from=yarn /opt/mastodon /opt/mastodon/ -COPY --from=bundler /opt/mastodon /opt/mastodon/ -COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/ -# Copy libvips components to layer for precompiler +# Copy Node.js binaries/libraries into layer +COPY --from=node /usr/local/bin /usr/local/bin +COPY --from=node /usr/local/lib /usr/local/lib + +RUN \ + # Configure Corepack + rm /usr/local/bin/yarn*; \ + corepack enable; \ + corepack prepare --activate; + +# hadolint ignore=DL3008 +RUN \ + --mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \ + --mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \ + # Install Node.js packages + yarn workspaces focus --production @mastodon/mastodon; + +# Copy libvips components into layer for precompiler COPY --from=libvips /usr/local/libvips/bin /usr/local/bin COPY --from=libvips /usr/local/libvips/lib /usr/local/lib - -ARG TARGETPLATFORM +# Copy bundler packages into layer for precompiler +COPY --from=bundler /opt/mastodon /opt/mastodon/ +COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/ RUN \ ldconfig; \ -# Use Ruby on Rails to create Mastodon assets + # Use Ruby on Rails to create Mastodon assets SECRET_KEY_BASE_DUMMY=1 \ bundle exec rails assets:precompile; \ -# Cleanup temporary files + # Cleanup temporary files rm -fr /opt/mastodon/tmp; # Prep final Mastodon Ruby layer @@ -328,49 +318,49 @@ ARG TARGETPLATFORM # hadolint ignore=DL3008 RUN \ -# Mount Apt cache and lib directories from Docker buildx caches ---mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \ ---mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \ -# Mount Corepack and Yarn caches from Docker buildx caches ---mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \ ---mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \ -# Apt update install non-dev versions of necessary components + # Mount Apt cache and lib directories from Docker buildx caches + --mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \ + --mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \ + # Mount Corepack and Yarn caches from Docker buildx caches + --mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \ + --mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \ + # Apt update install non-dev versions of necessary components apt-get install -y --no-install-recommends \ - libexpat1 \ - libglib2.0-0 \ - libicu72 \ - libidn12 \ - libpq5 \ - libreadline8 \ - libssl3 \ - libyaml-0-2 \ + libexpat1 \ + libglib2.0-0 \ + libicu72 \ + libidn12 \ + libpq5 \ + libreadline8 \ + libssl3 \ + libyaml-0-2 \ # libvips components - libcgif0 \ - libexif12 \ - libheif1/bookworm-backports \ - libimagequant0 \ - libjpeg62-turbo \ - liblcms2-2 \ - liborc-0.4-0 \ - libspng0 \ - libtiff6 \ - libwebp7 \ - libwebpdemux2 \ - libwebpmux3 \ + libcgif0 \ + libexif12 \ + libheif1/bookworm-backports \ + libimagequant0 \ + libjpeg62-turbo \ + liblcms2-2 \ + liborc-0.4-0 \ + libspng0 \ + libtiff6 \ + libwebp7 \ + libwebpdemux2 \ + libwebpmux3 \ # ffmpeg components - libdav1d6 \ - libmp3lame0 \ - libopencore-amrnb0 \ - libopencore-amrwb0 \ - libopus0 \ - libsnappy1v5 \ - libtheora0 \ - libvorbis0a \ - libvorbisenc2 \ - libvorbisfile3 \ - libvpx7 \ - libx264-164 \ - libx265-199 \ + libdav1d6 \ + libmp3lame0 \ + libopencore-amrnb0 \ + libopencore-amrwb0 \ + libopus0 \ + libsnappy1v5 \ + libtheora0 \ + libvorbis0a \ + libvorbisenc2 \ + libvorbisfile3 \ + libvpx7 \ + libx264-164 \ + libx265-199 \ ; # Copy Mastodon sources into final layer @@ -390,7 +380,7 @@ COPY --from=ffmpeg /usr/local/ffmpeg/lib /usr/local/lib RUN \ ldconfig; \ -# Smoketest media processors + # Smoketest media processors vips -v; \ ffmpeg -version; \ ffprobe -version; @@ -400,10 +390,10 @@ RUN \ bundle exec bootsnap precompile --gemfile app/ lib/; RUN \ -# Pre-create and chown system volume to Mastodon user + # Pre-create and chown system volume to Mastodon user mkdir -p /opt/mastodon/public/system; \ chown mastodon:mastodon /opt/mastodon/public/system; \ -# Set Mastodon user as owner of tmp folder + # Set Mastodon user as owner of tmp folder chown -R mastodon:mastodon /opt/mastodon/tmp; # Set the running user for resulting container diff --git a/Gemfile b/Gemfile index bcb19421ab..9e5955e0b8 100644 --- a/Gemfile +++ b/Gemfile @@ -1,12 +1,12 @@ # frozen_string_literal: true source 'https://rubygems.org' -ruby '>= 3.1.0' +ruby '>= 3.2.0', '< 3.5.0' gem 'propshaft' gem 'puma', '~> 6.3' gem 'rack', '~> 2.2.7' -gem 'rails', '~> 7.1.1' +gem 'rails', '~> 8.0' gem 'thor', '~> 1.2' gem 'dotenv' @@ -14,18 +14,19 @@ gem 'haml-rails', '~>2.0' gem 'pg', '~> 1.5' gem 'pghero' +gem 'aws-sdk-core', '< 3.216.0', require: false # TODO: https://github.com/mastodon/mastodon/pull/34173#issuecomment-2733378873 gem 'aws-sdk-s3', '~> 1.123', require: false gem 'blurhash', '~> 0.1' -gem 'fog-core', '<= 2.5.0' +gem 'fog-core', '<= 2.6.0' gem 'fog-openstack', '~> 1.0', require: false +gem 'jd-paperclip-azure', '~> 3.0', require: false gem 'kt-paperclip', '~> 7.2' -gem 'md-paperclip-azure', '~> 2.2', require: false gem 'ruby-vips', '~> 2.2', require: false gem 'active_model_serializers', '~> 0.10' gem 'addressable', '~> 2.8' gem 'bootsnap', '~> 1.18.0', require: false -gem 'browser', '< 6' # https://github.com/fnando/browser/issues/543 +gem 'browser' gem 'charlock_holmes', '~> 0.7.7' gem 'chewy', '~> 7.3' gem 'devise', '~> 4.9' @@ -39,7 +40,7 @@ gem 'net-ldap', '~> 0.18' gem 'omniauth', '~> 2.0' gem 'omniauth-cas', '~> 3.0.0.beta.1' -gem 'omniauth_openid_connect', '~> 0.6.1' +gem 'omniauth_openid_connect', '~> 0.8.0' gem 'omniauth-rails_csrf_protection', '~> 1.0' gem 'omniauth-saml', '~> 2.0' @@ -47,21 +48,24 @@ gem 'color_diff', '~> 0.1' gem 'csv', '~> 3.2' gem 'discard', '~> 1.2' gem 'doorkeeper', '~> 5.6' +gem 'faraday-httpclient' gem 'fast_blank', '~> 1.0' gem 'fastimage' gem 'hiredis', '~> 0.6' gem 'htmlentities', '~> 4.3' gem 'http', '~> 5.2.0' gem 'http_accept_language', '~> 2.1' -gem 'httplog', '~> 1.7.0' +gem 'httplog', '~> 1.7.0', require: false gem 'i18n' gem 'idn-ruby', require: 'idn' gem 'inline_svg' gem 'irb', '~> 1.8' gem 'kaminari', '~> 1.2' gem 'link_header', '~> 0.0' +gem 'linzer', '~> 0.6.1' gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' -gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar' +gem 'mime-types', '~> 3.6.0', require: 'mime/types/columnar' +gem 'mutex_m' gem 'nokogiri', '~> 1.15' gem 'oj', '~> 3.14' gem 'ox', '~> 2.14' @@ -71,13 +75,13 @@ gem 'public_suffix', '~> 6.0' gem 'pundit', '~> 2.3' gem 'rack-attack', '~> 6.6' gem 'rack-cors', '~> 2.0', require: 'rack/cors' -gem 'rails-i18n', '~> 7.0' +gem 'rails-i18n', '~> 8.0' gem 'redcarpet', '~> 3.6' gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] gem 'redis-namespace', '~> 1.10' gem 'rqrcode', '~> 2.2' gem 'ruby-progressbar', '~> 1.13' -gem 'sanitize', '~> 6.0' +gem 'sanitize', '~> 7.0' gem 'scenic', '~> 1.7' gem 'sidekiq', '~> 6.5' gem 'sidekiq-bulk', '~> 0.2.0' @@ -92,29 +96,31 @@ gem 'twitter-text', '~> 3.1.0' gem 'tzinfo-data', '~> 1.2023' gem 'webauthn', '~> 3.0' gem 'webpacker', '~> 5.4' -gem 'webpush', github: 'ClearlyClaire/webpush', ref: 'f14a4d52e201128b1b00245d11b6de80d6cfdcd9' +gem 'webpush', github: 'mastodon/webpush', ref: '9631ac63045cfabddacc69fc06e919b4c13eb913' gem 'json-ld' gem 'json-ld-preloaded', '~> 3.2' gem 'rdf-normalize', '~> 0.5' -gem 'opentelemetry-api', '~> 1.4.0' +gem 'prometheus_exporter', '~> 2.2', require: false + +gem 'opentelemetry-api', '~> 1.5.0' group :opentelemetry do - gem 'opentelemetry-exporter-otlp', '~> 0.29.0', require: false - gem 'opentelemetry-instrumentation-active_job', '~> 0.7.1', require: false - gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.20.1', require: false - gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.21.2', require: false - gem 'opentelemetry-instrumentation-excon', '~> 0.22.0', require: false - gem 'opentelemetry-instrumentation-faraday', '~> 0.24.1', require: false - gem 'opentelemetry-instrumentation-http', '~> 0.23.2', require: false - gem 'opentelemetry-instrumentation-http_client', '~> 0.22.3', require: false - gem 'opentelemetry-instrumentation-net_http', '~> 0.22.4', require: false - gem 'opentelemetry-instrumentation-pg', '~> 0.29.0', require: false - gem 'opentelemetry-instrumentation-rack', '~> 0.24.1', require: false - gem 'opentelemetry-instrumentation-rails', '~> 0.31.0', require: false - gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false - gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.2', require: false + gem 'opentelemetry-exporter-otlp', '~> 0.30.0', require: false + gem 'opentelemetry-instrumentation-active_job', '~> 0.8.0', require: false + gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.22.0', require: false + gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.22.0', require: false + gem 'opentelemetry-instrumentation-excon', '~> 0.23.0', require: false + gem 'opentelemetry-instrumentation-faraday', '~> 0.26.0', require: false + gem 'opentelemetry-instrumentation-http', '~> 0.24.0', require: false + gem 'opentelemetry-instrumentation-http_client', '~> 0.23.0', require: false + gem 'opentelemetry-instrumentation-net_http', '~> 0.23.0', require: false + gem 'opentelemetry-instrumentation-pg', '~> 0.30.0', require: false + gem 'opentelemetry-instrumentation-rack', '~> 0.26.0', require: false + gem 'opentelemetry-instrumentation-rails', '~> 0.36.0', require: false + gem 'opentelemetry-instrumentation-redis', '~> 0.26.0', require: false + gem 'opentelemetry-instrumentation-sidekiq', '~> 0.26.0', require: false gem 'opentelemetry-sdk', '~> 1.4', require: false end @@ -123,7 +129,7 @@ group :test do gem 'flatware-rspec' # Adds RSpec Error/Warning annotations to GitHub PRs on the Files tab - gem 'rspec-github', '~> 2.4', require: false + gem 'rspec-github', '~> 3.0', require: false # RSpec helpers for email specs gem 'email_spec' @@ -141,9 +147,6 @@ group :test do # Used to mock environment variables gem 'climate_control' - # Add back helpers functions removed in Rails 5.1 - gem 'rails-controller-testing', '~> 1.0' - # Validate schemas in specs gem 'json-schema', '~> 5.0' @@ -152,7 +155,7 @@ group :test do gem 'shoulda-matchers' - # Coverage formatter for RSpec test if DISABLE_SIMPLECOV is false + # Coverage formatter for RSpec gem 'simplecov', '~> 0.22', require: false gem 'simplecov-lcov', '~> 0.8', require: false @@ -164,13 +167,14 @@ group :development do # Code linting CLI and plugins gem 'rubocop', require: false gem 'rubocop-capybara', require: false + gem 'rubocop-i18n', require: false gem 'rubocop-performance', require: false gem 'rubocop-rails', require: false gem 'rubocop-rspec', require: false gem 'rubocop-rspec_rails', require: false # Annotates modules with schema - gem 'annotate', '~> 3.2' + gem 'annotaterb', '~> 4.13', require: false # Enhanced error message pages for development gem 'better_errors', '~> 2.9' @@ -181,7 +185,7 @@ group :development do gem 'letter_opener_web', '~> 3.0' # Security analysis CLI tools - gem 'brakeman', '~> 6.0', require: false + gem 'brakeman', '~> 7.0', require: false gem 'bundler-audit', '~> 0.9', require: false # Linter CLI for HAML files @@ -193,7 +197,7 @@ end group :development, :test do # Interactive Debugging tools - gem 'debug', '~> 1.8' + gem 'debug', '~> 1.8', require: false # Generate fake data values gem 'faker', '~> 3.2' @@ -205,7 +209,7 @@ group :development, :test do gem 'memory_profiler', require: false gem 'ruby-prof', require: false gem 'stackprof', require: false - gem 'test-prof' + gem 'test-prof', require: false # RSpec runner for rails gem 'rspec-rails', '~> 7.0' @@ -220,7 +224,7 @@ gem 'concurrent-ruby', require: false gem 'connection_pool', require: false gem 'xorcist', '~> 1.1' -gem 'net-http', '~> 0.4.0' +gem 'net-http', '~> 0.6.0' gem 'rubyzip', '~> 2.3' gem 'hcaptcha', '~> 7.1' diff --git a/Gemfile.lock b/Gemfile.lock index cdd6b15dcf..f13df0c43f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,131 +1,116 @@ GIT - remote: https://github.com/ClearlyClaire/webpush.git - revision: f14a4d52e201128b1b00245d11b6de80d6cfdcd9 - ref: f14a4d52e201128b1b00245d11b6de80d6cfdcd9 + remote: https://github.com/mastodon/webpush.git + revision: 9631ac63045cfabddacc69fc06e919b4c13eb913 + ref: 9631ac63045cfabddacc69fc06e919b4c13eb913 specs: - webpush (0.3.8) + webpush (1.1.0) hkdf (~> 0.2) jwt (~> 2.0) GEM remote: https://rubygems.org/ specs: - actioncable (7.1.5.1) - actionpack (= 7.1.5.1) - activesupport (= 7.1.5.1) + actioncable (8.0.2) + actionpack (= 8.0.2) + activesupport (= 8.0.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.1.5.1) - actionpack (= 7.1.5.1) - activejob (= 7.1.5.1) - activerecord (= 7.1.5.1) - activestorage (= 7.1.5.1) - activesupport (= 7.1.5.1) - mail (>= 2.7.1) - net-imap - net-pop - net-smtp - actionmailer (7.1.5.1) - actionpack (= 7.1.5.1) - actionview (= 7.1.5.1) - activejob (= 7.1.5.1) - activesupport (= 7.1.5.1) - mail (~> 2.5, >= 2.5.4) - net-imap - net-pop - net-smtp + actionmailbox (8.0.2) + actionpack (= 8.0.2) + activejob (= 8.0.2) + activerecord (= 8.0.2) + activestorage (= 8.0.2) + activesupport (= 8.0.2) + mail (>= 2.8.0) + actionmailer (8.0.2) + actionpack (= 8.0.2) + actionview (= 8.0.2) + activejob (= 8.0.2) + activesupport (= 8.0.2) + mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.1.5.1) - actionview (= 7.1.5.1) - activesupport (= 7.1.5.1) + actionpack (8.0.2) + actionview (= 8.0.2) + activesupport (= 8.0.2) nokogiri (>= 1.8.5) - racc rack (>= 2.2.4) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - actiontext (7.1.5.1) - actionpack (= 7.1.5.1) - activerecord (= 7.1.5.1) - activestorage (= 7.1.5.1) - activesupport (= 7.1.5.1) + useragent (~> 0.16) + actiontext (8.0.2) + actionpack (= 8.0.2) + activerecord (= 8.0.2) + activestorage (= 8.0.2) + activesupport (= 8.0.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.1.5.1) - activesupport (= 7.1.5.1) + actionview (8.0.2) + activesupport (= 8.0.2) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - active_model_serializers (0.10.14) + active_model_serializers (0.10.15) actionpack (>= 4.1) activemodel (>= 4.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (7.1.5.1) - activesupport (= 7.1.5.1) + activejob (8.0.2) + activesupport (= 8.0.2) globalid (>= 0.3.6) - activemodel (7.1.5.1) - activesupport (= 7.1.5.1) - activerecord (7.1.5.1) - activemodel (= 7.1.5.1) - activesupport (= 7.1.5.1) + activemodel (8.0.2) + activesupport (= 8.0.2) + activerecord (8.0.2) + activemodel (= 8.0.2) + activesupport (= 8.0.2) timeout (>= 0.4.0) - activestorage (7.1.5.1) - actionpack (= 7.1.5.1) - activejob (= 7.1.5.1) - activerecord (= 7.1.5.1) - activesupport (= 7.1.5.1) + activestorage (8.0.2) + actionpack (= 8.0.2) + activejob (= 8.0.2) + activerecord (= 8.0.2) + activesupport (= 8.0.2) marcel (~> 1.0) - activesupport (7.1.5.1) + activesupport (8.0.2) base64 benchmark (>= 0.3) bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) + concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) logger (>= 1.4.2) minitest (>= 5.1) - mutex_m securerandom (>= 0.3) - tzinfo (~> 2.0) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) aes_key_wrap (1.1.0) android_key_attestation (0.3.0) - annotate (3.2.0) - activerecord (>= 3.2, < 8.0) - rake (>= 10.4, < 14.0) - ast (2.4.2) + annotaterb (4.14.0) + ast (2.4.3) attr_required (1.0.2) - awrence (1.2.1) - aws-eventstream (1.3.0) - aws-partitions (1.978.0) - aws-sdk-core (3.209.0) + aws-eventstream (1.3.2) + aws-partitions (1.1087.0) + aws-sdk-core (3.215.1) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) + aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.94.0) - aws-sdk-core (~> 3, >= 3.207.0) + aws-sdk-kms (1.96.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.166.0) - aws-sdk-core (~> 3, >= 3.207.0) + aws-sdk-s3 (1.177.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.10.0) + aws-sigv4 (1.11.0) aws-eventstream (~> 1, >= 1.0.2) - azure-storage-blob (2.0.3) - azure-storage-common (~> 2.0) - nokogiri (~> 1, >= 1.10.8) - azure-storage-common (2.0.4) - faraday (~> 1.0) - faraday_middleware (~> 1.0, >= 1.0.0.rc1) - net-http-persistent (~> 4.0) - nokogiri (~> 1, >= 1.10.8) + azure-blob (0.5.7) + rexml base64 (0.2.0) bcp47_spec (0.2.1) bcrypt (3.1.20) @@ -134,16 +119,16 @@ GEM erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) - bigdecimal (3.1.8) - bindata (2.5.0) + bigdecimal (3.1.9) + bindata (2.5.1) binding_of_caller (1.0.1) debug_inspector (>= 1.2.0) blurhash (0.1.8) bootsnap (1.18.4) msgpack (~> 1.2) - brakeman (6.2.1) + brakeman (7.0.2) racc - browser (5.3.1) + browser (6.2.0) brpoplpush-redis_script (0.1.3) concurrent-ruby (~> 1.0, >= 1.0.5) redis (>= 1.0, < 6) @@ -174,8 +159,8 @@ GEM climate_control (1.2.0) cocoon (1.2.15) color_diff (0.1) - concurrent-ruby (1.3.4) - connection_pool (2.4.1) + concurrent-ruby (1.3.5) + connection_pool (2.5.0) cose (1.3.1) cbor (~> 0.5.9) openssl-signature_algorithm (~> 1.0) @@ -183,15 +168,15 @@ GEM bigdecimal rexml crass (1.0.6) - css_parser (1.19.0) + css_parser (1.21.1) addressable - csv (3.3.0) + csv (3.3.4) database_cleaner-active_record (2.2.0) activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - date (3.3.4) - debug (1.9.2) + date (3.4.1) + debug (1.10.0) irb (~> 1.10) reline (>= 0.3.8) debug_inspector (1.2.0) @@ -201,22 +186,22 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-two-factor (6.0.0) - activesupport (~> 7.0) + devise-two-factor (6.1.0) + activesupport (>= 7.0, < 8.1) devise (~> 4.0) - railties (~> 7.0) + railties (>= 7.0, < 8.1) rotp (~> 6.0) devise_pam_authenticatable2 (9.2.0) devise (>= 4.0.0) rpam2 (~> 4.0) - diff-lcs (1.5.1) - discard (1.3.0) - activerecord (>= 4.2, < 8) + diff-lcs (1.6.1) + discard (1.4.0) + activerecord (>= 4.2, < 9.0) docile (1.4.1) domain_name (0.6.20240107) - doorkeeper (5.7.1) + doorkeeper (5.8.2) railties (>= 5) - dotenv (3.1.4) + dotenv (3.1.8) drb (2.2.1) elasticsearch (7.17.11) elasticsearch-api (= 7.17.11) @@ -232,41 +217,29 @@ GEM htmlentities (~> 4.3.3) launchy (>= 2.1, < 4.0) mail (~> 2.7) - erubi (1.13.0) + email_validator (2.2.4) + activemodel + erubi (1.13.1) et-orbi (1.2.11) tzinfo - excon (0.111.0) + excon (1.2.5) + logger fabrication (2.31.0) - faker (3.4.2) + faker (3.5.1) i18n (>= 1.8.11, < 2) - faraday (1.10.3) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0) - faraday-multipart (~> 1.0) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.0) - faraday-patron (~> 1.0) - faraday-rack (~> 1.0) - faraday-retry (~> 1.0) - ruby2_keywords (>= 0.0.4) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) - faraday-excon (1.1.0) - faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) - faraday-net_http (1.0.2) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) - faraday-rack (1.0.0) - faraday-retry (1.0.3) - faraday_middleware (1.2.0) - faraday (~> 1.0) + faraday (2.13.0) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-follow_redirects (0.3.0) + faraday (>= 1, < 3) + faraday-httpclient (2.0.1) + httpclient (>= 2.2) + faraday-net_http (3.4.0) + net-http (>= 0.5.0) fast_blank (1.0.1) - fastimage (2.3.1) - ffi (1.17.1) + fastimage (2.4.0) + ffi (1.17.2) ffi-compiler (1.3.2) ffi (>= 1.15.5) rake @@ -276,15 +249,15 @@ GEM flatware-rspec (2.3.4) flatware (= 2.3.4) rspec (>= 3.6) - fog-core (2.5.0) + fog-core (2.6.0) builder - excon (~> 0.71) + excon (~> 1.0) formatador (>= 0.2, < 2.0) mime-types fog-json (1.2.0) fog-core multi_json (~> 1.10) - fog-openstack (1.1.3) + fog-openstack (1.1.5) fog-core (~> 2.1) fog-json (>= 1.0) formatador (1.1.0) @@ -293,8 +266,10 @@ GEM raabro (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) - google-protobuf (3.25.5) - googleapis-common-protos-types (1.15.0) + google-protobuf (4.30.2) + bigdecimal + rake (>= 13) + googleapis-common-protos-types (1.19.0) google-protobuf (>= 3.18, < 5.a) haml (6.3.0) temple (>= 0.8.2) @@ -305,17 +280,17 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) - haml_lint (0.58.0) + haml_lint (0.62.0) haml (>= 5.0) parallel (~> 1.10) rainbow rubocop (>= 1.0) sysexits (~> 1.1) - hashdiff (1.1.1) + hashdiff (1.1.2) hashie (5.0.0) hcaptcha (7.1.0) json - highline (3.1.1) + highline (3.1.2) reline hiredis (0.6.3) hkdf (0.3.0) @@ -326,17 +301,18 @@ GEM http-cookie (~> 1.0) http-form_data (~> 2.2) llhttp-ffi (~> 0.5.0) - http-cookie (1.0.5) + http-cookie (1.0.8) domain_name (~> 0.5) http-form_data (2.3.0) http_accept_language (2.1.1) - httpclient (2.8.3) + httpclient (2.9.0) + mutex_m httplog (1.7.0) rack (>= 2.0) rainbow (>= 2.0.0) - i18n (1.14.6) + i18n (1.14.7) concurrent-ruby (~> 1.0) - i18n-tasks (1.0.14) + i18n-tasks (1.0.15) activesupport (>= 4.0.2) ast (>= 2.1.0) erubi @@ -345,23 +321,31 @@ GEM parser (>= 3.2.2.1) rails-i18n rainbow (>= 2.2.2, < 4.0) + ruby-progressbar (~> 1.8, >= 1.8.1) terminal-table (>= 1.5.1) idn-ruby (0.1.5) inline_svg (1.10.0) activesupport (>= 3.0) nokogiri (>= 1.6) - io-console (0.7.2) - irb (1.14.1) + io-console (0.8.0) + irb (1.15.2) + pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) + jd-paperclip-azure (3.0.0) + addressable (~> 2.5) + azure-blob (~> 0.5.2) + hashie (~> 5.0) jmespath (1.6.2) - json (2.7.2) + json (2.10.2) json-canonicalization (1.0.0) - json-jwt (1.15.3.1) + json-jwt (1.16.7) activesupport (>= 4.2) aes_key_wrap + base64 bindata - httpclient + faraday (~> 2.0) + faraday-follow_redirects json-ld (3.3.2) htmlentities (~> 4.3) json-canonicalization (~> 1.0) @@ -370,13 +354,15 @@ GEM rack (>= 2.2, < 4) rdf (~> 3.3) rexml (~> 3.2) - json-ld-preloaded (3.3.0) + json-ld-preloaded (3.3.1) json-ld (~> 3.3) rdf (~> 3.3) - json-schema (5.0.0) + json-schema (5.1.1) addressable (~> 2.8) + bigdecimal (~> 3.1) jsonapi-renderer (0.2.2) - jwt (2.7.1) + jwt (2.10.1) + base64 kaminari (1.2.2) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.2) @@ -395,10 +381,11 @@ GEM marcel (~> 1.0.1) mime-types terrapin (>= 0.6.0, < 2.0) - language_server-protocol (3.17.0.3) - launchy (3.0.1) + language_server-protocol (3.17.0.4) + launchy (3.1.1) addressable (~> 2.8) childprocess (~> 5.0) + logger (~> 1.6) letter_opener (1.10.0) launchy (>= 2.2, < 4) letter_opener_web (3.0.0) @@ -407,16 +394,23 @@ GEM railties (>= 6.1) rexml link_header (0.0.8) - llhttp-ffi (0.5.0) + lint_roller (1.1.0) + linzer (0.6.5) + openssl (~> 3.0, >= 3.0.0) + rack (>= 2.2, < 4.0) + starry (~> 0.2) + stringio (~> 3.1, >= 3.1.2) + uri (~> 1.0, >= 1.0.2) + llhttp-ffi (0.5.1) ffi-compiler (~> 1.0) rake (~> 13.0) - logger (1.6.6) + logger (1.7.0) lograge (0.14.0) actionpack (>= 4) activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.22.0) + loofah (2.24.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -428,26 +422,20 @@ GEM mario-redis-lock (1.2.1) redis (>= 3.0.5) matrix (0.4.2) - md-paperclip-azure (2.2.0) - addressable (~> 2.5) - azure-storage-blob (~> 2.0.1) - hashie (~> 5.0) memory_profiler (1.1.0) - mime-types (3.5.2) + mime-types (3.6.2) + logger mime-types-data (~> 3.2015) - mime-types-data (3.2024.0820) + mime-types-data (3.2025.0408) mini_mime (1.1.5) mini_portile2 (2.8.8) - minitest (5.25.1) - msgpack (1.7.2) + minitest (5.25.5) + msgpack (1.8.0) multi_json (1.15.0) - multipart-post (2.4.1) - mutex_m (0.2.0) - net-http (0.4.1) + mutex_m (0.3.0) + net-http (0.6.0) uri - net-http-persistent (4.0.2) - connection_pool (~> 2.2) - net-imap (0.4.19) + net-imap (0.5.6) date net-protocol net-ldap (0.19.0) @@ -457,18 +445,18 @@ GEM timeout net-smtp (0.5.1) net-protocol - nio4r (2.7.3) - nokogiri (1.18.3) + nio4r (2.7.4) + nokogiri (1.18.7) mini_portile2 (~> 2.8.2) racc (~> 1.4) - oj (3.16.6) + oj (3.16.10) bigdecimal (>= 3.0) ostruct (>= 0.2) omniauth (2.1.3) hashie (>= 3.4.6) rack (>= 2.2.3) rack-protection - omniauth-cas (3.0.0) + omniauth-cas (3.0.1) addressable (~> 2.8) nokogiri (~> 1.12) omniauth (~> 2.1) @@ -478,125 +466,137 @@ GEM omniauth-saml (2.2.3) omniauth (~> 2.1) ruby-saml (~> 1.18) - omniauth_openid_connect (0.6.1) + omniauth_openid_connect (0.8.0) omniauth (>= 1.9, < 3) - openid_connect (~> 1.1) - openid_connect (1.4.2) + openid_connect (~> 2.2) + openid_connect (2.3.1) activemodel attr_required (>= 1.0.0) - json-jwt (>= 1.15.0) - net-smtp - rack-oauth2 (~> 1.21) - swd (~> 1.3) + email_validator + faraday (~> 2.0) + faraday-follow_redirects + json-jwt (>= 1.16) + mail + rack-oauth2 (~> 2.2) + swd (~> 2.0) tzinfo - validate_email validate_url - webfinger (~> 1.2) - openssl (3.2.0) + webfinger (~> 2.0) + openssl (3.3.0) openssl-signature_algorithm (1.3.0) openssl (> 2.0) - opentelemetry-api (1.4.0) - opentelemetry-common (0.21.0) + opentelemetry-api (1.5.0) + opentelemetry-common (0.22.0) opentelemetry-api (~> 1.0) - opentelemetry-exporter-otlp (0.29.0) + opentelemetry-exporter-otlp (0.30.0) google-protobuf (>= 3.18) googleapis-common-protos-types (~> 1.3) opentelemetry-api (~> 1.1) opentelemetry-common (~> 0.20) opentelemetry-sdk (~> 1.2) opentelemetry-semantic_conventions - opentelemetry-helpers-sql-obfuscation (0.2.0) + opentelemetry-helpers-sql-obfuscation (0.3.0) opentelemetry-common (~> 0.21) - opentelemetry-instrumentation-action_mailer (0.1.0) + opentelemetry-instrumentation-action_mailer (0.4.0) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-active_support (~> 0.1) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-action_pack (0.9.0) + opentelemetry-instrumentation-active_support (~> 0.7) + opentelemetry-instrumentation-base (~> 0.23.0) + opentelemetry-instrumentation-action_pack (0.12.0) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-base (~> 0.23.0) opentelemetry-instrumentation-rack (~> 0.21) - opentelemetry-instrumentation-action_view (0.7.2) + opentelemetry-instrumentation-action_view (0.9.0) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-active_support (~> 0.1) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-active_job (0.7.7) + opentelemetry-instrumentation-active_support (~> 0.7) + opentelemetry-instrumentation-base (~> 0.23.0) + opentelemetry-instrumentation-active_job (0.8.0) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-active_model_serializers (0.20.2) + opentelemetry-instrumentation-base (~> 0.23.0) + opentelemetry-instrumentation-active_model_serializers (0.22.0) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-active_record (0.7.3) + opentelemetry-instrumentation-active_support (>= 0.7.0) + opentelemetry-instrumentation-base (~> 0.23.0) + opentelemetry-instrumentation-active_record (0.9.0) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-active_support (0.6.0) + opentelemetry-instrumentation-base (~> 0.23.0) + opentelemetry-instrumentation-active_storage (0.1.1) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-base (0.22.6) + opentelemetry-instrumentation-active_support (~> 0.7) + opentelemetry-instrumentation-base (~> 0.23.0) + opentelemetry-instrumentation-active_support (0.8.0) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.23.0) + opentelemetry-instrumentation-base (0.23.0) opentelemetry-api (~> 1.0) opentelemetry-common (~> 0.21) opentelemetry-registry (~> 0.1) - opentelemetry-instrumentation-concurrent_ruby (0.21.4) + opentelemetry-instrumentation-concurrent_ruby (0.22.0) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-excon (0.22.4) + opentelemetry-instrumentation-base (~> 0.23.0) + opentelemetry-instrumentation-excon (0.23.0) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-faraday (0.24.6) + opentelemetry-instrumentation-base (~> 0.23.0) + opentelemetry-instrumentation-faraday (0.26.0) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-http (0.23.4) + opentelemetry-instrumentation-base (~> 0.23.0) + opentelemetry-instrumentation-http (0.24.0) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-http_client (0.22.7) + opentelemetry-instrumentation-base (~> 0.23.0) + opentelemetry-instrumentation-http_client (0.23.0) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-net_http (0.22.7) + opentelemetry-instrumentation-base (~> 0.23.0) + opentelemetry-instrumentation-net_http (0.23.0) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-pg (0.29.0) + opentelemetry-instrumentation-base (~> 0.23.0) + opentelemetry-instrumentation-pg (0.30.0) opentelemetry-api (~> 1.0) opentelemetry-helpers-sql-obfuscation - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-rack (0.24.6) + opentelemetry-instrumentation-base (~> 0.23.0) + opentelemetry-instrumentation-rack (0.26.0) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-rails (0.31.2) + opentelemetry-instrumentation-base (~> 0.23.0) + opentelemetry-instrumentation-rails (0.36.0) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-action_mailer (~> 0.1.0) - opentelemetry-instrumentation-action_pack (~> 0.9.0) - opentelemetry-instrumentation-action_view (~> 0.7.0) - opentelemetry-instrumentation-active_job (~> 0.7.0) - opentelemetry-instrumentation-active_record (~> 0.7.0) - opentelemetry-instrumentation-active_support (~> 0.6.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-redis (0.25.7) + opentelemetry-instrumentation-action_mailer (~> 0.4.0) + opentelemetry-instrumentation-action_pack (~> 0.12.0) + opentelemetry-instrumentation-action_view (~> 0.9.0) + opentelemetry-instrumentation-active_job (~> 0.8.0) + opentelemetry-instrumentation-active_record (~> 0.9.0) + opentelemetry-instrumentation-active_storage (~> 0.1.0) + opentelemetry-instrumentation-active_support (~> 0.8.0) + opentelemetry-instrumentation-base (~> 0.23.0) + opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0) + opentelemetry-instrumentation-redis (0.26.1) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-sidekiq (0.25.7) + opentelemetry-instrumentation-base (~> 0.23.0) + opentelemetry-instrumentation-sidekiq (0.26.1) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-registry (0.3.1) + opentelemetry-instrumentation-base (~> 0.23.0) + opentelemetry-registry (0.4.0) opentelemetry-api (~> 1.1) - opentelemetry-sdk (1.5.0) + opentelemetry-sdk (1.8.0) opentelemetry-api (~> 1.1) opentelemetry-common (~> 0.20) opentelemetry-registry (~> 0.2) opentelemetry-semantic_conventions - opentelemetry-semantic_conventions (1.10.1) + opentelemetry-semantic_conventions (1.11.0) opentelemetry-api (~> 1.0) orm_adapter (0.5.0) - ostruct (0.6.0) - ox (2.14.18) - parallel (1.26.3) - parser (3.3.5.0) + ostruct (0.6.1) + ox (2.14.22) + bigdecimal (>= 3.0) + parallel (1.27.0) + parser (3.3.8.0) ast (~> 2.4.1) racc parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) - pg (1.5.8) - pghero (3.6.0) + pg (1.5.9) + pghero (3.6.2) activerecord (>= 6.1) + pp (0.6.2) + prettyprint premailer (1.27.0) addressable css_parser (>= 1.19.0) @@ -605,17 +605,22 @@ GEM actionmailer (>= 3) net-smtp premailer (~> 1.7, >= 1.7.9) + prettyprint (0.2.0) + prism (1.4.0) + prometheus_exporter (2.2.0) + webrick propshaft (1.1.0) actionpack (>= 7.0.0) activesupport (>= 7.0.0) rack railties (>= 7.0.0) - psych (5.1.2) + psych (5.2.3) + date stringio public_suffix (6.0.1) - puma (6.4.3) + puma (6.6.0) nio4r (~> 2.0) - pundit (2.4.0) + pundit (2.5.0) activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) @@ -624,10 +629,11 @@ GEM rack (>= 1.0, < 4) rack-cors (2.0.2) rack (>= 2.0.0) - rack-oauth2 (1.21.3) + rack-oauth2 (2.2.1) activesupport attr_required - httpclient + faraday (~> 2.0) + faraday-follow_redirects json-jwt (>= 1.11.0) rack (>= 2.1.0) rack-protection (3.2.0) @@ -637,29 +643,25 @@ GEM rack rack-session (1.0.2) rack (< 3) - rack-test (2.1.0) + rack-test (2.2.0) rack (>= 1.3) - rackup (1.0.0) + rackup (1.0.1) rack (< 3) webrick - rails (7.1.5.1) - actioncable (= 7.1.5.1) - actionmailbox (= 7.1.5.1) - actionmailer (= 7.1.5.1) - actionpack (= 7.1.5.1) - actiontext (= 7.1.5.1) - actionview (= 7.1.5.1) - activejob (= 7.1.5.1) - activemodel (= 7.1.5.1) - activerecord (= 7.1.5.1) - activestorage (= 7.1.5.1) - activesupport (= 7.1.5.1) + rails (8.0.2) + actioncable (= 8.0.2) + actionmailbox (= 8.0.2) + actionmailer (= 8.0.2) + actionpack (= 8.0.2) + actiontext (= 8.0.2) + actionview (= 8.0.2) + activejob (= 8.0.2) + activemodel (= 8.0.2) + activerecord (= 8.0.2) + activestorage (= 8.0.2) + activesupport (= 8.0.2) bundler (>= 1.15.0) - railties (= 7.1.5.1) - rails-controller-testing (1.0.5) - actionpack (>= 5.0.1.rc1) - actionview (>= 5.0.1.rc1) - activesupport (>= 5.0.1.rc1) + railties (= 8.0.2) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -667,13 +669,13 @@ GEM rails-html-sanitizer (1.6.2) loofah (~> 2.21) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - rails-i18n (7.0.9) + rails-i18n (8.0.1) i18n (>= 0.7, < 2) - railties (>= 6.0.0, < 8) - railties (7.1.5.1) - actionpack (= 7.1.5.1) - activesupport (= 7.1.5.1) - irb + railties (>= 8.0.0, < 9) + railties (8.0.2) + actionpack (= 8.0.2) + activesupport (= 8.0.2) + irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) @@ -686,25 +688,25 @@ GEM link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.7.0) rdf (~> 3.3) - rdoc (6.7.0) + rdoc (6.13.1) psych (>= 4.0.0) - redcarpet (3.6.0) + redcarpet (3.6.1) redis (4.8.1) redis-namespace (1.11.0) redis (>= 4) redlock (1.3.2) redis (>= 3.0.0, < 6.0) - regexp_parser (2.9.2) - reline (0.5.10) + regexp_parser (2.10.0) + reline (0.6.1) io-console (~> 0.5) - request_store (1.6.0) + request_store (1.7.0) rack (>= 1.4) responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) - rexml (3.3.9) + rexml (3.4.1) rotp (6.3.0) - rouge (4.3.0) + rouge (4.5.1) rpam2 (4.0.2) rqrcode (2.2.0) chunky_png (~> 1.0) @@ -714,17 +716,17 @@ GEM rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) - rspec-core (3.13.1) + rspec-core (3.13.3) rspec-support (~> 3.13.0) - rspec-expectations (3.13.2) + rspec-expectations (3.13.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-github (2.4.0) + rspec-github (3.0.0) rspec-core (~> 3.0) - rspec-mocks (3.13.1) + rspec-mocks (3.13.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (7.0.1) + rspec-rails (7.1.1) actionpack (>= 7.0) activesupport (>= 7.0) railties (>= 7.0) @@ -732,40 +734,50 @@ GEM rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) rspec-support (~> 3.13) - rspec-sidekiq (5.0.0) + rspec-sidekiq (5.1.0) rspec-core (~> 3.0) rspec-expectations (~> 3.0) rspec-mocks (~> 3.0) - sidekiq (>= 5, < 8) - rspec-support (3.13.1) - rubocop (1.66.1) + sidekiq (>= 5, < 9) + rspec-support (3.13.2) + rubocop (1.75.2) json (~> 2.3) - language_server-protocol (>= 3.17.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 2.4, < 3.0) - rubocop-ast (>= 1.32.2, < 2.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.44.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.32.3) - parser (>= 3.3.1.0) - rubocop-capybara (2.21.0) - rubocop (~> 1.41) - rubocop-performance (1.22.1) - rubocop (>= 1.48.1, < 2.0) - rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rails (2.26.2) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.44.1) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-capybara (2.22.1) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-i18n (3.2.3) + lint_roller (~> 1.1) + rubocop (>= 1.72.1) + rubocop-performance (1.25.0) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + rubocop-rails (2.31.0) activesupport (>= 4.2.0) + lint_roller (~> 1.1) rack (>= 1.1) - rubocop (>= 1.52.0, < 2.0) - rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rspec (3.0.5) - rubocop (~> 1.61) - rubocop-rspec_rails (2.30.0) - rubocop (~> 1.61) - rubocop-rspec (~> 3, >= 3.0.1) - ruby-prof (1.7.0) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + rubocop-rspec (3.5.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-rspec_rails (2.31.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-rspec (~> 3.5) + ruby-prof (1.7.1) ruby-progressbar (1.13.0) ruby-saml (1.18.0) nokogiri (>= 1.13.10) @@ -773,26 +785,25 @@ GEM ruby-vips (2.2.3) ffi (~> 1.12) logger - ruby2_keywords (0.0.5) - rubyzip (2.3.2) - rufus-scheduler (3.9.1) - fugit (~> 1.1, >= 1.1.6) + rubyzip (2.4.1) + rufus-scheduler (3.9.2) + fugit (~> 1.1, >= 1.11.1) safety_net_attestation (0.4.0) jwt (~> 2.0) - sanitize (6.1.3) + sanitize (7.0.0) crass (~> 1.0.2) - nokogiri (>= 1.12.0) + nokogiri (>= 1.16.8) scenic (1.8.0) activerecord (>= 4.0.0) railties (>= 4.0.0) securerandom (0.4.1) - selenium-webdriver (4.25.0) + selenium-webdriver (4.31.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - semantic_range (3.0.0) + semantic_range (3.1.0) shoulda-matchers (6.4.0) activesupport (>= 5.2.0) sidekiq (6.5.12) @@ -820,30 +831,33 @@ GEM docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) - simplecov-html (0.12.3) + simplecov-html (0.13.1) simplecov-lcov (0.8.0) simplecov_json_formatter (0.1.4) - stackprof (0.2.26) - stoplight (4.1.0) + stackprof (0.2.27) + starry (0.2.0) + base64 + stoplight (4.1.1) redlock (~> 1.0) - stringio (3.1.1) - strong_migrations (2.0.0) - activerecord (>= 6.1) - swd (1.3.0) + stringio (3.1.6) + strong_migrations (2.3.0) + activerecord (>= 7) + swd (2.0.3) activesupport (>= 3) attr_required (>= 0.0.5) - httpclient (>= 2.4) + faraday (~> 2.0) + faraday-follow_redirects sysexits (1.2.0) temple (0.10.3) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) - terrapin (1.0.1) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + terrapin (1.1.0) climate_control - test-prof (1.4.2) + test-prof (1.4.4) thor (1.3.2) - tilt (2.4.0) + tilt (2.6.0) timeout (0.4.3) - tpm-key_attestation (0.12.1) + tpm-key_attestation (0.14.0) bindata (~> 2.4) openssl (> 2.0) openssl-signature_algorithm (~> 1.0) @@ -862,34 +876,34 @@ GEM unf (~> 0.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - tzinfo-data (1.2024.2) + tzinfo-data (1.2025.2) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext unf_ext (0.0.9.1) - unicode-display_width (2.5.0) - uri (0.13.2) - validate_email (0.1.6) - activemodel (>= 3.0) - mail (>= 2.2.5) + unicode-display_width (3.1.4) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) + uri (1.0.3) + useragent (0.16.11) validate_url (1.0.15) activemodel (>= 3.0.0) public_suffix warden (1.2.9) rack (>= 2.0.9) - webauthn (3.1.0) + webauthn (3.4.0) android_key_attestation (~> 0.3.0) - awrence (~> 1.1) bindata (~> 2.4) cbor (~> 0.5.9) cose (~> 1.1) openssl (>= 2.2) safety_net_attestation (~> 0.4.0) - tpm-key_attestation (~> 0.12.0) - webfinger (1.2.0) + tpm-key_attestation (~> 0.14.0) + webfinger (2.1.3) activesupport - httpclient (>= 2.4) - webmock (3.24.0) + faraday (~> 2.0) + faraday-follow_redirects + webmock (3.25.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -898,16 +912,17 @@ GEM rack-proxy (>= 0.6.1) railties (>= 5.2) semantic_range (>= 2.3.0) - webrick (1.8.2) + webrick (1.9.1) websocket (1.2.11) - websocket-driver (0.7.6) + websocket-driver (0.7.7) + base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) wisper (2.0.1) xorcist (1.1.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.18) + zeitwerk (2.7.2) PLATFORMS ruby @@ -915,14 +930,15 @@ PLATFORMS DEPENDENCIES active_model_serializers (~> 0.10) addressable (~> 2.8) - annotate (~> 3.2) + annotaterb (~> 4.13) + aws-sdk-core (< 3.216.0) aws-sdk-s3 (~> 1.123) better_errors (~> 2.9) binding_of_caller (~> 1.0) blurhash (~> 0.1) bootsnap (~> 1.18.0) - brakeman (~> 6.0) - browser (< 6) + brakeman (~> 7.0) + browser bundler-audit (~> 0.9) capybara (~> 3.39) charlock_holmes (~> 0.7.7) @@ -944,10 +960,11 @@ DEPENDENCIES email_spec fabrication (~> 2.30) faker (~> 3.2) + faraday-httpclient fast_blank (~> 1.0) fastimage flatware-rspec - fog-core (<= 2.5.0) + fog-core (<= 2.6.0) fog-openstack (~> 1.0) haml-rails (~> 2.0) haml_lint @@ -962,6 +979,7 @@ DEPENDENCIES idn-ruby inline_svg irb (~> 1.8) + jd-paperclip-azure (~> 3.0) json-ld json-ld-preloaded (~> 3.2) json-schema (~> 5.0) @@ -970,13 +988,14 @@ DEPENDENCIES letter_opener (~> 1.8) letter_opener_web (~> 3.0) link_header (~> 0.0) + linzer (~> 0.6.1) lograge (~> 0.12) mail (~> 2.8) mario-redis-lock (~> 1.2) - md-paperclip-azure (~> 2.2) memory_profiler - mime-types (~> 3.5.0) - net-http (~> 0.4.0) + mime-types (~> 3.6.0) + mutex_m + net-http (~> 0.6.0) net-ldap (~> 0.18) nokogiri (~> 1.15) oj (~> 3.14) @@ -984,28 +1003,29 @@ DEPENDENCIES omniauth-cas (~> 3.0.0.beta.1) omniauth-rails_csrf_protection (~> 1.0) omniauth-saml (~> 2.0) - omniauth_openid_connect (~> 0.6.1) - opentelemetry-api (~> 1.4.0) - opentelemetry-exporter-otlp (~> 0.29.0) - opentelemetry-instrumentation-active_job (~> 0.7.1) - opentelemetry-instrumentation-active_model_serializers (~> 0.20.1) - opentelemetry-instrumentation-concurrent_ruby (~> 0.21.2) - opentelemetry-instrumentation-excon (~> 0.22.0) - opentelemetry-instrumentation-faraday (~> 0.24.1) - opentelemetry-instrumentation-http (~> 0.23.2) - opentelemetry-instrumentation-http_client (~> 0.22.3) - opentelemetry-instrumentation-net_http (~> 0.22.4) - opentelemetry-instrumentation-pg (~> 0.29.0) - opentelemetry-instrumentation-rack (~> 0.24.1) - opentelemetry-instrumentation-rails (~> 0.31.0) - opentelemetry-instrumentation-redis (~> 0.25.3) - opentelemetry-instrumentation-sidekiq (~> 0.25.2) + omniauth_openid_connect (~> 0.8.0) + opentelemetry-api (~> 1.5.0) + opentelemetry-exporter-otlp (~> 0.30.0) + opentelemetry-instrumentation-active_job (~> 0.8.0) + opentelemetry-instrumentation-active_model_serializers (~> 0.22.0) + opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0) + opentelemetry-instrumentation-excon (~> 0.23.0) + opentelemetry-instrumentation-faraday (~> 0.26.0) + opentelemetry-instrumentation-http (~> 0.24.0) + opentelemetry-instrumentation-http_client (~> 0.23.0) + opentelemetry-instrumentation-net_http (~> 0.23.0) + opentelemetry-instrumentation-pg (~> 0.30.0) + opentelemetry-instrumentation-rack (~> 0.26.0) + opentelemetry-instrumentation-rails (~> 0.36.0) + opentelemetry-instrumentation-redis (~> 0.26.0) + opentelemetry-instrumentation-sidekiq (~> 0.26.0) opentelemetry-sdk (~> 1.4) ox (~> 2.14) parslet pg (~> 1.5) pghero premailer-rails + prometheus_exporter (~> 2.2) propshaft public_suffix (~> 6.0) puma (~> 6.3) @@ -1014,19 +1034,19 @@ DEPENDENCIES rack-attack (~> 6.6) rack-cors (~> 2.0) rack-test (~> 2.1) - rails (~> 7.1.1) - rails-controller-testing (~> 1.0) - rails-i18n (~> 7.0) + rails (~> 8.0) + rails-i18n (~> 8.0) rdf-normalize (~> 0.5) redcarpet (~> 3.6) redis (~> 4.5) redis-namespace (~> 1.10) rqrcode (~> 2.2) - rspec-github (~> 2.4) + rspec-github (~> 3.0) rspec-rails (~> 7.0) rspec-sidekiq (~> 5.0) rubocop rubocop-capybara + rubocop-i18n rubocop-performance rubocop-rails rubocop-rspec @@ -1035,7 +1055,7 @@ DEPENDENCIES ruby-progressbar (~> 1.13) ruby-vips (~> 2.2) rubyzip (~> 2.3) - sanitize (~> 6.0) + sanitize (~> 7.0) scenic (~> 1.7) selenium-webdriver shoulda-matchers @@ -1062,7 +1082,7 @@ DEPENDENCIES xorcist (~> 1.1) RUBY VERSION - ruby 3.3.4p94 + ruby 3.4.1p0 BUNDLED WITH - 2.6.5 + 2.6.8 diff --git a/README.md b/README.md index 200d58d8c4..854e8ac3d9 100644 --- a/README.md +++ b/README.md @@ -1,123 +1,27 @@ -# ![kmyblue icon](https://raw.githubusercontent.com/kmycode/mastodon/kb_development/app/javascript/icons/favicon-32x32.png) kmyblue +NAS is an KMY & Mastodon Fork -[![Ruby Testing](https://github.com/kmycode/mastodon/actions/workflows/test-ruby.yml/badge.svg)](https://github.com/kmycode/mastodon/actions/workflows/test-ruby.yml) +The following are just a few of the most common features. There are many other minor changes to the specifications. -! FOR ENGLISH USER ! We do not provide English documentation for kmyblue; we assume that you will use automatic translation software, such as Google, to translate the site. +Emoji reactions -kmyblueは、ActivityPubに接続するSNSの1つである[Mastodon](https://github.com/mastodon/mastodon)のフォークです。創作作家のためのMastodonを目指して開発しました。 +Local Public (Does not appear on the federated timeline of remote servers, but does appear on followers' home timelines. This is different from local only) -kmyblueはフォーク名であり、同時に[サーバー名](https://kmy.blue)でもあります。以下は特に記述がない限り、フォークとしてのkmyblueをさします。 +Bookmark classification -kmyblueは AGPL ライセンスで公開されているため、どなたでも自由にフォークし、このソースコードを元に自分でサーバーを立てて公開することができます。確かにサーバーkmyblueは創作作家向けの利用規約が設定されていますが、フォークとしてのkmyblueのルールは全くの別物です。いかなるコミュニティにも平等にお使いいただけます。 -kmyblueは、閉鎖的なコミュニティ、あまり目立ちたくないコミュニティには特に強力な機能を提供します。kmyblueはプライバシーを考慮したうえで強力な独自機能を提供するため、汎用サーバーとして利用するにもある程度十分な機能が揃っています。 +Set who can search your posts for each post (Searchability) -テストコード、Lint どちらも動いています。 +Quote posts, modest quotes (references) -### アジェンダ +Record posts that meet certain conditions such as domains, accounts, and keywords (Subscriptions/Antennas) -- 利用方法 -- kmyblueの開発方針 -- kmyblueは何でないか -- kmyblueの独自機能 -- 英語のサポートについて +Send posts to a designated set of followers (Circles) (different from direct messages) -## 利用方法 +Notification of new posts on lists -### インストール方法 +Exclude posts from people you follow when filtering posts -[Wiki](https://github.com/kmycode/mastodon/wiki/Installation)を参照してください。 +Hide number of followers and followings -### 開発への参加方法 +Automatically delete posts after a specified time has passed -CONTRIBUTING.mdを参照してください。 - -### テスト - -``` -# デバッグ実行(以下のいずれか) -foreman start -DB_USER=postgres DB_PASS=password foreman start - -# 一部を除く全てのテストを行う -RAILS_ENV=test bundle exec rspec spec - -# ElasticSearch連携テストを行う -新 -RAILS_ENV=test ES_ENABLED=true bundle exec rspec --tag search -旧 -RAILS_ENV=test ES_ENABLED=true RUN_SEARCH_SPECS=true bundle exec rspec spec/search -``` - -## kmyblueの開発方針 - -### 本家Mastodonへの積極的追従 - -kmyblueは、追加機能を控えめにする代わりに本家Mastodonに積極的に追従を行います。kmyblueの追加機能そのままに、Mastodonの新機能も利用できるよう調整を行います。 - -### ゆるやかな内輪での運用 - -kmyblueは同人向けサーバーとして出発したため、同人作家に需要のある「内輪ノリを外部にできるだけもらさない」という部分に特化しています。 - -「ローカル公開」は、投稿を見せたくない人に見つかりにくくする効果があります。「サークル」は、フォロワーの中でも特に見せたい人だけに見せる効果があります。 -「検索許可」という独自の検索オプションを利用することで、公開投稿の一部だけを検索されにくくするだけでなく、非収載投稿が誰でも自由に検索できるようになります。 - -内輪とは自分のサーバーに限ったものではありません。内輪同士で複数のサーバーを運営するとき、お互いが深く繋がれる「フレンドサーバー」というシステムも用意しています。 - -### 少人数サーバーでの運用 - -kmyblueは、人の少ないサーバーでの運用を考慮して設計しています。そのため、Fedibirdにあるような、人の多いサーバー向けの機能はあまり作っていません。 - -サーバーの負荷については一部度外視している部分があります。たとえば絵文字リアクション機能はサーバーへ著しい負荷をかける場合があります。ただしkmyblueでは、絵文字リアクション機能そのものを無効にしたり、負荷の高いストリーミング処理を無効にする管理者オプションも存在します。 - -もちろん人の多いサーバーでの運用が不便になるような修正は行っていません。人数にかかわらず、そのままお使いいただけます。 - -### 比較的高い防御力 - -kmyblueでは、「Fediverseは将来的に荒むのではないか」「Fediverseは将来的にスパムに溢れるのではないか」を念頭に設計している部分があります。投稿だけでなく絵文字リアクションも対象にした防衛策があります。 - -管理者は「NGワード」「NGルール」機能の利用が可能です。設定を変更することで、一部のモデレーターもこの機能を利用できます。 -利用者は、独自拡張されたフィルター機能、絵文字リアクションのブロックなどを利用できます。 - -ただし防御力の高さは自由を犠牲にします。例えばNGワードが多すぎると、他のサーバーからの投稿が制限され、かつそれに気づきにくくなります。 - -## kmyblueは何でないか - -kmyblueは、企業・政府機関向けに開発されたものではありません。開発者はセキュリティに関する専門知識を有しておらず、高度なセキュリティを求められる機関向けのソフトウェアを制作する能力はありません。また、kmyblueのメンテナは現在1人のみであり、そのメンテナが飽きたら開発がストップするリスクも高いです。Mastodonのような高い信頼性・安全性を保証することはできないので、導入の際はご自身で安全を十分に確認してからお使いになることを強くおすすめします。 -個人サーバーであっても、安定性を強く求める方にはおすすめできません。glitch-socがよりよい選択肢になるでしょう。 - -kmyblueは、Misskeyではありません。Misskeyは「楽しむ」をコンセプトにしていますが、kmyblueはMastodonの思想を受け継ぎ、炎上や喧騒を避けることのできる落ち着いた場所を目指しています。そのため、思想に合わない機能は実装しないか、大幅に弱体化しています。 - -kmyblueは、Fedibirdではありません。Fedibirdは大規模サーバー向けに設定していると思われる機能があり、例えば購読機能がその代表例です。Fedibirdの購読は擬似的なフォロー体験を与えるものですが、本物のフォローではないため、購読対象の投稿が配送されることを確約したものではありません。小規模サーバーだとかえって不便になる機能を、kmyblueは避けています。 - -## kmyblueの独自機能 - -以下に列挙したものはあくまで代表的なものです。これ以外にも、細かい仕様変更などが多数含まれます。 - -- 絵文字リアクション -- ローカル公開(Local Public)(リモートサーバーの連合タイムラインには流れませんが、フォロワーのホームタイムラインには流れます。**ローカル限定とは異なります**) -- ブックマークの分類 -- 自分の投稿を検索できる人を投稿ごとに設定(検索許可・Searchability) -- 投稿の引用、ひかえめな引用(参照) -- ドメイン・アカウント・キーワードなど特定条件を満たした投稿を記録する機能(購読・アンテナ) -- フォロワーの一部を指名して投稿を送る機能(サークル)(ダイレクトメッセージとは異なります) -- リスト新着投稿の通知 -- 投稿のフィルタリングにおいて、自分がフォローしている相手の投稿を除外 -- フォロー・フォロワー数を隠す機能 -- 指定した時間が経過したあとに投稿を自動削除する機能 -- モデレーション機能の拡張 - -## 英語のサポートについて - -kmyblueのメイン開発者である[雪あすか](https://kmy.blue/@askyq)は、英語の読み書きがほとんどできません。そのため、ドキュメントの英語化、海外向け公式アカウントの新設などを行う予定はありません。 - -要望やバグ報告はIssueに書いて構いませんが、Issue画面内の説明やテンプレートはすべて日本語になっています。投稿が難しければ、Discussionに投稿してください。こちらで必要と判断したものは、改めてIssueとして起票します。 - -そのほか開発者へ質問があれば、[@askyq@kmy.blue](https://kmy.blue/@askyq)へ英語のまま送ってください。 - -ただしkmyblueのドキュメント、[@askyq@kmy.blue](https://kmy.blue/@askyq)内のkmyblueフォークに関係する投稿を、許可なく翻訳して公開することは問題ありません。 - -## 開発者のアカウントについて - -kmyblueのメイン開発者である[雪あすか](https://kmy.blue/@askyq)は、用途別にアカウントを分けるようなことはせず、すべての発言を1つのアカウントで行っています。そのため、kmyblueの開発だけでなく、成人向け同人作品の話も混ざっています。 - -このうち、公開範囲「公開」「ローカル公開」「非収載」であるkmyblueフォークの開発に関係する投稿に限り抽出し、翻訳の有無に関係なく公開することを許可します。これはkmyblueフォークの利用者にとって公共性の高いコンテンツであると思われます。これは、日本と欧米では一般的に考えられている児童ポルノの基準が異なり、欧米のサーバーの中にはこのアカウントをフォローしづらいものもあるという懸念を考慮したものです。 +Expanding moderation functions diff --git a/Rakefile b/Rakefile index e51cf0e17e..488c551fee 100644 --- a/Rakefile +++ b/Rakefile @@ -3,6 +3,6 @@ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. -require File.expand_path('config/application', __dir__) +require_relative 'config/application' Rails.application.load_tasks diff --git a/Vagrantfile b/Vagrantfile index 89f5536edc..ce456060cd 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -174,7 +174,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| if config.vm.networks.any? { |type, options| type == :private_network } config.vm.synced_folder ".", "/vagrant", type: "nfs", mount_options: ['rw', 'actimeo=1'] else - config.vm.synced_folder ".", "/vagrant" + config.vm.synced_folder ".", "/vagrant", type: "rsync", create: true, rsync__args: ["--verbose", "--archive", "--delete", "-z"] end # Otherwise, you can access the site at http://localhost:3000 and http://localhost:4000 , http://localhost:8080 diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index ab1b98e646..c80db3500d 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -49,7 +49,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController def collection_presenter ActivityPub::CollectionPresenter.new( - id: account_collection_url(@account, params[:id]), + id: ActivityPub::TagManager.instance.collection_uri_for(@account, params[:id]), type: @type, size: @size, items: @items diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index 82b1830941..171161d491 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -41,12 +41,8 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController end end - def outbox_url(**kwargs) - if params[:account_username].present? - account_outbox_url(@account, **kwargs) - else - instance_actor_outbox_url(**kwargs) - end + def outbox_url(...) + ActivityPub::TagManager.instance.outbox_uri_for(@account, ...) end def next_page diff --git a/app/controllers/admin/account_actions_controller.rb b/app/controllers/admin/account_actions_controller.rb index e674bf55a0..91849811e3 100644 --- a/app/controllers/admin/account_actions_controller.rb +++ b/app/controllers/admin/account_actions_controller.rb @@ -34,7 +34,8 @@ module Admin end def resource_params - params.require(:admin_account_action).permit(:type, :report_id, :warning_preset_id, :text, :send_email_notification, :include_statuses) + params + .expect(admin_account_action: [:type, :report_id, :warning_preset_id, :text, :send_email_notification, :include_statuses]) end end end diff --git a/app/controllers/admin/account_moderation_notes_controller.rb b/app/controllers/admin/account_moderation_notes_controller.rb index a3c4adf59a..7f65ced517 100644 --- a/app/controllers/admin/account_moderation_notes_controller.rb +++ b/app/controllers/admin/account_moderation_notes_controller.rb @@ -29,10 +29,8 @@ module Admin private def resource_params - params.require(:account_moderation_note).permit( - :content, - :target_account_id - ) + params + .expect(account_moderation_note: [:content, :target_account_id]) end def set_account_moderation_note diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index a4d5018d0a..a779a0cf51 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -172,7 +172,8 @@ module Admin end def form_account_batch_params - params.require(:form_account_batch).permit(:action, account_ids: []) + params + .expect(form_account_batch: [:action, account_ids: []]) end def action_from_button diff --git a/app/controllers/admin/announcements/distributions_controller.rb b/app/controllers/admin/announcements/distributions_controller.rb new file mode 100644 index 0000000000..4bd8769834 --- /dev/null +++ b/app/controllers/admin/announcements/distributions_controller.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Admin::Announcements::DistributionsController < Admin::BaseController + before_action :set_announcement + + def create + authorize @announcement, :distribute? + @announcement.touch(:notification_sent_at) + Admin::DistributeAnnouncementNotificationWorker.perform_async(@announcement.id) + redirect_to admin_announcements_path + end + + private + + def set_announcement + @announcement = Announcement.find(params[:announcement_id]) + end +end diff --git a/app/controllers/admin/announcements/previews_controller.rb b/app/controllers/admin/announcements/previews_controller.rb new file mode 100644 index 0000000000..d77f931a7f --- /dev/null +++ b/app/controllers/admin/announcements/previews_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class Admin::Announcements::PreviewsController < Admin::BaseController + before_action :set_announcement + + def show + authorize @announcement, :distribute? + @user_count = @announcement.scope_for_notification.count + end + + private + + def set_announcement + @announcement = Announcement.find(params[:announcement_id]) + end +end diff --git a/app/controllers/admin/announcements/tests_controller.rb b/app/controllers/admin/announcements/tests_controller.rb new file mode 100644 index 0000000000..f2457eb23a --- /dev/null +++ b/app/controllers/admin/announcements/tests_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Admin::Announcements::TestsController < Admin::BaseController + before_action :set_announcement + + def create + authorize @announcement, :distribute? + UserMailer.announcement_published(current_user, @announcement).deliver_later! + redirect_to admin_announcements_path + end + + private + + def set_announcement + @announcement = Announcement.find(params[:announcement_id]) + end +end diff --git a/app/controllers/admin/announcements_controller.rb b/app/controllers/admin/announcements_controller.rb index 12230a6506..eaf84aab25 100644 --- a/app/controllers/admin/announcements_controller.rb +++ b/app/controllers/admin/announcements_controller.rb @@ -84,6 +84,7 @@ class Admin::AnnouncementsController < Admin::BaseController end def resource_params - params.require(:announcement).permit(:text, :scheduled_at, :starts_at, :ends_at, :all_day) + params + .expect(announcement: [:text, :scheduled_at, :starts_at, :ends_at, :all_day]) end end diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index 48685db17a..14338dd293 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -7,14 +7,14 @@ module Admin layout 'admin' - before_action :set_cache_headers + before_action :set_referrer_policy_header after_action :verify_authorized private - def set_cache_headers - response.cache_control.replace(private: true, no_store: true) + def set_referrer_policy_header + response.headers['Referrer-Policy'] = 'same-origin' end def set_user diff --git a/app/controllers/admin/change_emails_controller.rb b/app/controllers/admin/change_emails_controller.rb index a689d3a530..c923b94b1a 100644 --- a/app/controllers/admin/change_emails_controller.rb +++ b/app/controllers/admin/change_emails_controller.rb @@ -41,9 +41,8 @@ module Admin end def resource_params - params.require(:user).permit( - :unconfirmed_email - ) + params + .expect(user: [:unconfirmed_email]) end end end diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb index 34368f08a2..596b167249 100644 --- a/app/controllers/admin/custom_emojis_controller.rb +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -67,11 +67,13 @@ module Admin end def resource_params - params.require(:custom_emoji).permit(:shortcode, :image, :category_id, :visible_in_picker, :aliases_raw, :license) + params + .expect(custom_emoji: [:shortcode, :image, :category_id, :visible_in_picker, :aliases_raw, :license]) end def update_params - params.require(:custom_emoji).permit(:category_id, :visible_in_picker, :aliases_raw, :license) + params + .expect(custom_emoji: [:category_id, :visible_in_picker, :aliases_raw, :license]) end def filtered_custom_emojis @@ -101,7 +103,8 @@ module Admin end def form_custom_emoji_batch_params - params.require(:form_custom_emoji_batch).permit(:action, :category_id, :category_name, custom_emoji_ids: []) + params + .expect(form_custom_emoji_batch: [:action, :category_id, :category_name, custom_emoji_ids: []]) end end end diff --git a/app/controllers/admin/domain_allows_controller.rb b/app/controllers/admin/domain_allows_controller.rb index b0f139e3a8..913c1a8246 100644 --- a/app/controllers/admin/domain_allows_controller.rb +++ b/app/controllers/admin/domain_allows_controller.rb @@ -37,6 +37,7 @@ class Admin::DomainAllowsController < Admin::BaseController end def resource_params - params.require(:domain_allow).permit(:domain) + params + .expect(domain_allow: [:domain]) end end diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 78d2a2da28..520db814f2 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -35,7 +35,9 @@ module Admin rescue Mastodon::NotPermittedError flash[:alert] = I18n.t('admin.domain_blocks.not_permitted') else - redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') + flash[:notice] = I18n.t('admin.domain_blocks.created_msg') + ensure + redirect_to admin_instances_path(limited: '1') end def new @@ -124,9 +126,14 @@ module Admin end def form_domain_block_batch_params - params.require(:form_domain_block_batch).permit(domain_blocks_attributes: [:enabled, :domain, :severity, :reject_media, :reject_favourite, :reject_reply_exclude_followers, - :reject_send_sensitive, :reject_hashtag, :reject_straight_follow, :reject_new_follow, :reject_friend, :block_trends, :detect_invalid_subscription, - :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden]) + params + .expect( + form_domain_block_batch: [ + domain_blocks_attributes: [[:enabled, :domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate, + :reject_favourite, :reject_reply_exclude_followers, :reject_send_sensitive, :reject_hashtag, + :reject_straight_follow, :reject_new_follow, :reject_friend, :block_trends, :detect_invalid_subscription, :hidden]], + ] + ) end def action_from_button diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb index faa0a061a6..12f221164f 100644 --- a/app/controllers/admin/email_domain_blocks_controller.rb +++ b/app/controllers/admin/email_domain_blocks_controller.rb @@ -5,7 +5,7 @@ module Admin def index authorize :email_domain_block, :index? - @email_domain_blocks = EmailDomainBlock.where(parent_id: nil).includes(:children).order(id: :desc).page(params[:page]) + @email_domain_blocks = EmailDomainBlock.parents.includes(:children).order(id: :desc).page(params[:page]) @form = Form::EmailDomainBlockBatch.new end @@ -58,18 +58,17 @@ module Admin private def set_resolved_records - Resolv::DNS.open do |dns| - dns.timeouts = 5 - @resolved_records = dns.getresources(@email_domain_block.domain, Resolv::DNS::Resource::IN::MX).to_a - end + @resolved_records = DomainResource.new(@email_domain_block.domain).mx end def resource_params - params.require(:email_domain_block).permit(:domain, :allow_with_approval, other_domains: []) + params + .expect(email_domain_block: [:domain, :allow_with_approval, other_domains: []]) end def form_email_domain_block_batch_params - params.require(:form_email_domain_block_batch).permit(email_domain_block_ids: []) + params + .expect(form_email_domain_block_batch: [email_domain_block_ids: []]) end def action_from_button diff --git a/app/controllers/admin/fasp/debug/callbacks_controller.rb b/app/controllers/admin/fasp/debug/callbacks_controller.rb new file mode 100644 index 0000000000..28aba5e489 --- /dev/null +++ b/app/controllers/admin/fasp/debug/callbacks_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class Admin::Fasp::Debug::CallbacksController < Admin::BaseController + def index + authorize [:admin, :fasp, :provider], :update? + + @callbacks = Fasp::DebugCallback + .includes(:fasp_provider) + .order(created_at: :desc) + end + + def destroy + authorize [:admin, :fasp, :provider], :update? + + callback = Fasp::DebugCallback.find(params[:id]) + callback.destroy + + redirect_to admin_fasp_debug_callbacks_path + end +end diff --git a/app/controllers/admin/fasp/debug_calls_controller.rb b/app/controllers/admin/fasp/debug_calls_controller.rb new file mode 100644 index 0000000000..1e1b6dbf3c --- /dev/null +++ b/app/controllers/admin/fasp/debug_calls_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class Admin::Fasp::DebugCallsController < Admin::BaseController + before_action :set_provider + + def create + authorize [:admin, @provider], :update? + + @provider.perform_debug_call + + redirect_to admin_fasp_providers_path + end + + private + + def set_provider + @provider = Fasp::Provider.find(params[:provider_id]) + end +end diff --git a/app/controllers/admin/fasp/providers_controller.rb b/app/controllers/admin/fasp/providers_controller.rb new file mode 100644 index 0000000000..4f1f1271bf --- /dev/null +++ b/app/controllers/admin/fasp/providers_controller.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +class Admin::Fasp::ProvidersController < Admin::BaseController + before_action :set_provider, only: [:show, :edit, :update, :destroy] + + def index + authorize [:admin, :fasp, :provider], :index? + + @providers = Fasp::Provider.order(confirmed: :asc, created_at: :desc) + end + + def show + authorize [:admin, @provider], :show? + end + + def edit + authorize [:admin, @provider], :update? + end + + def update + authorize [:admin, @provider], :update? + + if @provider.update(provider_params) + redirect_to admin_fasp_providers_path + else + render :edit + end + end + + def destroy + authorize [:admin, @provider], :destroy? + + @provider.destroy + + redirect_to admin_fasp_providers_path + end + + private + + def provider_params + params.expect(fasp_provider: [capabilities_attributes: {}]) + end + + def set_provider + @provider = Fasp::Provider.find(params[:id]) + end +end diff --git a/app/controllers/admin/fasp/registrations_controller.rb b/app/controllers/admin/fasp/registrations_controller.rb new file mode 100644 index 0000000000..52c46c2eb6 --- /dev/null +++ b/app/controllers/admin/fasp/registrations_controller.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class Admin::Fasp::RegistrationsController < Admin::BaseController + before_action :set_provider + + def new + authorize [:admin, @provider], :create? + end + + def create + authorize [:admin, @provider], :create? + + @provider.update_info!(confirm: true) + + redirect_to edit_admin_fasp_provider_path(@provider) + end + + private + + def set_provider + @provider = Fasp::Provider.find(params[:provider_id]) + end +end diff --git a/app/controllers/admin/follow_recommendations_controller.rb b/app/controllers/admin/follow_recommendations_controller.rb index a54e41bd8c..b060cfbe94 100644 --- a/app/controllers/admin/follow_recommendations_controller.rb +++ b/app/controllers/admin/follow_recommendations_controller.rb @@ -37,7 +37,8 @@ module Admin end def form_account_batch_params - params.require(:form_account_batch).permit(:action, account_ids: []) + params + .expect(form_account_batch: [:action, account_ids: []]) end def filter_params diff --git a/app/controllers/admin/friend_servers_controller.rb b/app/controllers/admin/friend_servers_controller.rb index 729d3b3912..ec41ba672c 100644 --- a/app/controllers/admin/friend_servers_controller.rb +++ b/app/controllers/admin/friend_servers_controller.rb @@ -79,11 +79,11 @@ module Admin end def resource_params - params.require(:friend_domain).permit(:domain, :inbox_url, :available, :pseudo_relay, :delivery_local, :unlocked, :allow_all_posts) + params.expect(friend_domain: [:domain, :inbox_url, :available, :pseudo_relay, :delivery_local, :unlocked, :allow_all_posts]) end def update_resource_params - params.require(:friend_domain).permit(:inbox_url, :available, :pseudo_relay, :delivery_local, :unlocked, :allow_all_posts) + params.expect(friend_domain: [:inbox_url, :available, :pseudo_relay, :delivery_local, :unlocked, :allow_all_posts]) end def warn_signatures_not_enabled! diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index d7f88a71f3..a48c4773ed 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -5,6 +5,8 @@ module Admin before_action :set_instances, only: :index before_action :set_instance, except: :index + LOGS_LIMIT = 5 + def index authorize :instance, :index? preload_delivery_failures! @@ -13,7 +15,7 @@ module Admin def show authorize :instance, :show? @time_period = (6.days.ago.to_date...Time.now.utc.to_date) - @action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(5) + @action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(LOGS_LIMIT) end def destroy diff --git a/app/controllers/admin/invites_controller.rb b/app/controllers/admin/invites_controller.rb index dabfe97655..ac4ee35271 100644 --- a/app/controllers/admin/invites_controller.rb +++ b/app/controllers/admin/invites_controller.rb @@ -32,14 +32,15 @@ module Admin def deactivate_all authorize :invite, :deactivate_all? - Invite.available.in_batches.update_all(expires_at: Time.now.utc) + Invite.available.in_batches.touch_all(:expires_at) redirect_to admin_invites_path end private def resource_params - params.require(:invite).permit(:max_uses, :expires_in) + params + .expect(invite: [:max_uses, :expires_in]) end def filtered_invites diff --git a/app/controllers/admin/ip_blocks_controller.rb b/app/controllers/admin/ip_blocks_controller.rb index 1bd7ec8059..afabda1b88 100644 --- a/app/controllers/admin/ip_blocks_controller.rb +++ b/app/controllers/admin/ip_blocks_controller.rb @@ -44,7 +44,8 @@ module Admin private def resource_params - params.require(:ip_block).permit(:ip, :severity, :comment, :expires_in) + params + .expect(ip_block: [:ip, :severity, :comment, :expires_in]) end def action_from_button @@ -52,7 +53,8 @@ module Admin end def form_ip_block_batch_params - params.require(:form_ip_block_batch).permit(ip_block_ids: []) + params + .expect(form_ip_block_batch: [ip_block_ids: []]) end end end diff --git a/app/controllers/admin/ng_rules_controller.rb b/app/controllers/admin/ng_rules_controller.rb index f37424cced..0bdda41c0c 100644 --- a/app/controllers/admin/ng_rules_controller.rb +++ b/app/controllers/admin/ng_rules_controller.rb @@ -82,16 +82,16 @@ module Admin end def resource_params - params.require(:ng_rule).permit(:title, :expires_in, :available, :account_domain, :account_username, :account_display_name, - :account_note, :account_field_name, :account_field_value, :account_avatar_state, - :account_header_state, :account_include_local, :status_spoiler_text, :status_text, :status_tag, - :status_sensitive_state, :status_cw_state, :status_media_state, :status_poll_state, - :status_mention_state, :status_reference_state, - :status_quote_state, :status_reply_state, :status_media_threshold, :status_poll_threshold, - :status_mention_threshold, :status_allow_follower_mention, - :reaction_allow_follower, :emoji_reaction_name, :emoji_reaction_origin_domain, - :status_reference_threshold, :account_allow_followed_by_local, :record_history_also_local, - status_visibility: [], status_searchability: [], reaction_type: []) + params.expect(ng_rule: [:title, :expires_in, :available, :account_domain, :account_username, :account_display_name, + :account_note, :account_field_name, :account_field_value, :account_avatar_state, + :account_header_state, :account_include_local, :status_spoiler_text, :status_text, :status_tag, + :status_sensitive_state, :status_cw_state, :status_media_state, :status_poll_state, + :status_mention_state, :status_reference_state, + :status_quote_state, :status_reply_state, :status_media_threshold, :status_poll_threshold, + :status_mention_threshold, :status_allow_follower_mention, + :reaction_allow_follower, :emoji_reaction_name, :emoji_reaction_origin_domain, + :status_reference_threshold, :account_allow_followed_by_local, :record_history_also_local, + status_visibility: [], status_searchability: [], reaction_type: []]) end def test_words! diff --git a/app/controllers/admin/ng_words/keywords_controller.rb b/app/controllers/admin/ng_words/keywords_controller.rb index 9af38fab7b..10969204e8 100644 --- a/app/controllers/admin/ng_words/keywords_controller.rb +++ b/app/controllers/admin/ng_words/keywords_controller.rb @@ -21,6 +21,10 @@ module Admin false end + def avoid_save? + true + end + private def after_update_redirect_path diff --git a/app/controllers/admin/ng_words_controller.rb b/app/controllers/admin/ng_words_controller.rb index a70a435fa4..9e437f8c8b 100644 --- a/app/controllers/admin/ng_words_controller.rb +++ b/app/controllers/admin/ng_words_controller.rb @@ -13,6 +13,12 @@ module Admin return unless validate + if avoid_save? + flash[:notice] = I18n.t('generic.changes_saved_msg') + redirect_to after_update_redirect_path + return + end + @admin_settings = Form::AdminSettings.new(settings_params) if @admin_settings.save @@ -33,14 +39,18 @@ module Admin admin_ng_words_path end + def avoid_save? + false + end + private def settings_params - params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) + params.expect(form_admin_settings: [*Form::AdminSettings::KEYS]) end def settings_params_test - params.require(:form_admin_settings)[:ng_words_test] + params.expect(form_admin_settings: [ng_words_test: [keywords: [], regexps: [], strangers: [], temporary_ids: []]])['ng_words_test'] end end end diff --git a/app/controllers/admin/relays_controller.rb b/app/controllers/admin/relays_controller.rb index c893802159..9a796949de 100644 --- a/app/controllers/admin/relays_controller.rb +++ b/app/controllers/admin/relays_controller.rb @@ -21,6 +21,7 @@ module Admin @relay = Relay.new(resource_params) if @relay.save + log_action :create, @relay @relay.enable! redirect_to admin_relays_path else @@ -31,18 +32,21 @@ module Admin def destroy authorize :relay, :update? @relay.destroy + log_action :destroy, @relay redirect_to admin_relays_path end def enable authorize :relay, :update? @relay.enable! + log_action :enable, @relay redirect_to admin_relays_path end def disable authorize :relay, :update? @relay.disable! + log_action :disable, @relay redirect_to admin_relays_path end @@ -53,7 +57,8 @@ module Admin end def resource_params - params.require(:relay).permit(:inbox_url) + params + .expect(relay: [:inbox_url]) end def warn_signatures_not_enabled! diff --git a/app/controllers/admin/report_notes_controller.rb b/app/controllers/admin/report_notes_controller.rb index 6b16c29fc7..10dbe846e4 100644 --- a/app/controllers/admin/report_notes_controller.rb +++ b/app/controllers/admin/report_notes_controller.rb @@ -47,10 +47,8 @@ module Admin end def resource_params - params.require(:report_note).permit( - :content, - :report_id - ) + params + .expect(report_note: [:content, :report_id]) end def set_report_note diff --git a/app/controllers/admin/roles_controller.rb b/app/controllers/admin/roles_controller.rb index bcfc11159c..2f9af8a6fc 100644 --- a/app/controllers/admin/roles_controller.rb +++ b/app/controllers/admin/roles_controller.rb @@ -61,7 +61,8 @@ module Admin end def resource_params - params.require(:user_role).permit(:name, :color, :highlighted, :position, permissions_as_keys: []) + params + .expect(user_role: [:name, :color, :highlighted, :position, permissions_as_keys: []]) end end end diff --git a/app/controllers/admin/rules_controller.rb b/app/controllers/admin/rules_controller.rb index b8def22ba3..289b6a98c3 100644 --- a/app/controllers/admin/rules_controller.rb +++ b/app/controllers/admin/rules_controller.rb @@ -53,7 +53,8 @@ module Admin end def resource_params - params.require(:rule).permit(:text, :hint, :priority) + params + .expect(rule: [:text, :hint, :priority]) end end end diff --git a/app/controllers/admin/sensitive_words_controller.rb b/app/controllers/admin/sensitive_words_controller.rb index 24cdd4efcb..716dcc708a 100644 --- a/app/controllers/admin/sensitive_words_controller.rb +++ b/app/controllers/admin/sensitive_words_controller.rb @@ -37,7 +37,7 @@ module Admin end def settings_params - params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) + params.expect(form_admin_settings: [*Form::AdminSettings::KEYS]) end def settings_params_test diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index 338a3638c4..2ae5ec8255 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -28,7 +28,8 @@ module Admin end def settings_params - params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) + params + .expect(form_admin_settings: [*Form::AdminSettings::KEYS]) end end end diff --git a/app/controllers/admin/software_updates_controller.rb b/app/controllers/admin/software_updates_controller.rb index 52d8cb41e6..c9be97eb71 100644 --- a/app/controllers/admin/software_updates_controller.rb +++ b/app/controllers/admin/software_updates_controller.rb @@ -6,7 +6,7 @@ module Admin def index authorize :software_update, :index? - @software_updates = SoftwareUpdate.all.sort_by(&:gem_version) + @software_updates = SoftwareUpdate.by_version.filter(&:pending?) end private diff --git a/app/controllers/admin/special_domains_controller.rb b/app/controllers/admin/special_domains_controller.rb index 0ddbf26786..b36fe28d6e 100644 --- a/app/controllers/admin/special_domains_controller.rb +++ b/app/controllers/admin/special_domains_controller.rb @@ -28,7 +28,7 @@ module Admin end def settings_params - params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) + params.expect(form_admin_settings: [*Form::AdminSettings::KEYS]) end end end diff --git a/app/controllers/admin/special_instances_controller.rb b/app/controllers/admin/special_instances_controller.rb index 3fd35d474e..a16bae13ef 100644 --- a/app/controllers/admin/special_instances_controller.rb +++ b/app/controllers/admin/special_instances_controller.rb @@ -28,7 +28,7 @@ module Admin end def settings_params - params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) + params.expect(form_admin_settings: [*Form::AdminSettings::KEYS]) end end end diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb index 2070a7c70c..956950fe0d 100644 --- a/app/controllers/admin/statuses_controller.rb +++ b/app/controllers/admin/statuses_controller.rb @@ -16,6 +16,8 @@ module Admin def show authorize [:admin, @status], :show? + + @status_batch_action = Admin::StatusBatchAction.new end def batch @@ -96,7 +98,8 @@ module Admin helper_method :batched_ordered_status_edits def admin_status_batch_action_params - params.require(:admin_status_batch_action).permit(status_ids: []) + params + .expect(admin_status_batch_action: [status_ids: []]) end def after_create_redirect_path diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb index 4759d15bc4..a7bfd64794 100644 --- a/app/controllers/admin/tags_controller.rb +++ b/app/controllers/admin/tags_controller.rb @@ -37,7 +37,8 @@ module Admin end def tag_params - params.require(:tag).permit(:name, :display_name, :trendable, :usable, :listable) + params + .expect(tag: [:name, :display_name, :trendable, :usable, :listable]) end def filtered_tags diff --git a/app/controllers/admin/terms_of_service/distributions_controller.rb b/app/controllers/admin/terms_of_service/distributions_controller.rb new file mode 100644 index 0000000000..c639b083dd --- /dev/null +++ b/app/controllers/admin/terms_of_service/distributions_controller.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Admin::TermsOfService::DistributionsController < Admin::BaseController + before_action :set_terms_of_service + + def create + authorize @terms_of_service, :distribute? + @terms_of_service.touch(:notification_sent_at) + Admin::DistributeTermsOfServiceNotificationWorker.perform_async(@terms_of_service.id) + redirect_to admin_terms_of_service_index_path + end + + private + + def set_terms_of_service + @terms_of_service = TermsOfService.find(params[:terms_of_service_id]) + end +end diff --git a/app/controllers/admin/terms_of_service/drafts_controller.rb b/app/controllers/admin/terms_of_service/drafts_controller.rb new file mode 100644 index 0000000000..0c67eb9df8 --- /dev/null +++ b/app/controllers/admin/terms_of_service/drafts_controller.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class Admin::TermsOfService::DraftsController < Admin::BaseController + before_action :set_terms_of_service + + def show + authorize :terms_of_service, :create? + end + + def update + authorize @terms_of_service, :update? + + @terms_of_service.published_at = Time.now.utc if params[:action_type] == 'publish' + + if @terms_of_service.update(resource_params) + log_action(:publish, @terms_of_service) if @terms_of_service.published? + redirect_to @terms_of_service.published? ? admin_terms_of_service_index_path : admin_terms_of_service_draft_path + else + render :show + end + end + + private + + def set_terms_of_service + @terms_of_service = TermsOfService.draft.first || TermsOfService.new(text: current_terms_of_service&.text, effective_date: 10.days.from_now) + end + + def current_terms_of_service + TermsOfService.live.first + end + + def resource_params + params + .expect(terms_of_service: [:text, :changelog, :effective_date]) + end +end diff --git a/app/controllers/admin/terms_of_service/generates_controller.rb b/app/controllers/admin/terms_of_service/generates_controller.rb new file mode 100644 index 0000000000..0edc87893e --- /dev/null +++ b/app/controllers/admin/terms_of_service/generates_controller.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class Admin::TermsOfService::GeneratesController < Admin::BaseController + before_action :set_instance_presenter + + def show + authorize :terms_of_service, :create? + + @generator = TermsOfService::Generator.new( + domain: @instance_presenter.domain, + admin_email: @instance_presenter.contact.email + ) + end + + def create + authorize :terms_of_service, :create? + + @generator = TermsOfService::Generator.new(resource_params) + + if @generator.valid? + TermsOfService.create!(text: @generator.render) + redirect_to admin_terms_of_service_draft_path + else + render :show + end + end + + private + + def set_instance_presenter + @instance_presenter = InstancePresenter.new + end + + def resource_params + params + .expect(terms_of_service_generator: [*TermsOfService::Generator::VARIABLES]) + end +end diff --git a/app/controllers/admin/terms_of_service/histories_controller.rb b/app/controllers/admin/terms_of_service/histories_controller.rb new file mode 100644 index 0000000000..8f12341aea --- /dev/null +++ b/app/controllers/admin/terms_of_service/histories_controller.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class Admin::TermsOfService::HistoriesController < Admin::BaseController + def show + authorize :terms_of_service, :index? + @terms_of_service = TermsOfService.published.all + end +end diff --git a/app/controllers/admin/terms_of_service/previews_controller.rb b/app/controllers/admin/terms_of_service/previews_controller.rb new file mode 100644 index 0000000000..0a1a966751 --- /dev/null +++ b/app/controllers/admin/terms_of_service/previews_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class Admin::TermsOfService::PreviewsController < Admin::BaseController + before_action :set_terms_of_service + + def show + authorize @terms_of_service, :distribute? + @user_count = @terms_of_service.scope_for_notification.count + end + + private + + def set_terms_of_service + @terms_of_service = TermsOfService.find(params[:terms_of_service_id]) + end +end diff --git a/app/controllers/admin/terms_of_service/tests_controller.rb b/app/controllers/admin/terms_of_service/tests_controller.rb new file mode 100644 index 0000000000..e2483c1005 --- /dev/null +++ b/app/controllers/admin/terms_of_service/tests_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Admin::TermsOfService::TestsController < Admin::BaseController + before_action :set_terms_of_service + + def create + authorize @terms_of_service, :distribute? + UserMailer.terms_of_service_changed(current_user, @terms_of_service).deliver_later! + redirect_to admin_terms_of_service_preview_path(@terms_of_service) + end + + private + + def set_terms_of_service + @terms_of_service = TermsOfService.find(params[:terms_of_service_id]) + end +end diff --git a/app/controllers/admin/terms_of_service_controller.rb b/app/controllers/admin/terms_of_service_controller.rb new file mode 100644 index 0000000000..10aa5c66ca --- /dev/null +++ b/app/controllers/admin/terms_of_service_controller.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class Admin::TermsOfServiceController < Admin::BaseController + def index + authorize :terms_of_service, :index? + @terms_of_service = TermsOfService.published.first + end +end diff --git a/app/controllers/admin/trends/links/preview_card_providers_controller.rb b/app/controllers/admin/trends/links/preview_card_providers_controller.rb index 5e4b4084f8..5a650d5d8c 100644 --- a/app/controllers/admin/trends/links/preview_card_providers_controller.rb +++ b/app/controllers/admin/trends/links/preview_card_providers_controller.rb @@ -31,7 +31,8 @@ class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseControll end def trends_preview_card_provider_batch_params - params.require(:trends_preview_card_provider_batch).permit(:action, preview_card_provider_ids: []) + params + .expect(trends_preview_card_provider_batch: [:action, preview_card_provider_ids: []]) end def action_from_button diff --git a/app/controllers/admin/trends/links_controller.rb b/app/controllers/admin/trends/links_controller.rb index 83d68eba63..68aa73c992 100644 --- a/app/controllers/admin/trends/links_controller.rb +++ b/app/controllers/admin/trends/links_controller.rb @@ -4,7 +4,7 @@ class Admin::Trends::LinksController < Admin::BaseController def index authorize :preview_card, :review? - @locales = PreviewCardTrend.pluck('distinct language') + @locales = PreviewCardTrend.locales @preview_cards = filtered_preview_cards.page(params[:page]) @form = Trends::PreviewCardBatch.new end @@ -31,7 +31,8 @@ class Admin::Trends::LinksController < Admin::BaseController end def trends_preview_card_batch_params - params.require(:trends_preview_card_batch).permit(:action, preview_card_ids: []) + params + .expect(trends_preview_card_batch: [:action, preview_card_ids: []]) end def action_from_button diff --git a/app/controllers/admin/trends/statuses_controller.rb b/app/controllers/admin/trends/statuses_controller.rb index 3d8b53ea8a..873d777fe3 100644 --- a/app/controllers/admin/trends/statuses_controller.rb +++ b/app/controllers/admin/trends/statuses_controller.rb @@ -4,7 +4,7 @@ class Admin::Trends::StatusesController < Admin::BaseController def index authorize [:admin, :status], :review? - @locales = StatusTrend.pluck('distinct language') + @locales = StatusTrend.locales @statuses = filtered_statuses.page(params[:page]) @form = Trends::StatusBatch.new end @@ -31,7 +31,8 @@ class Admin::Trends::StatusesController < Admin::BaseController end def trends_status_batch_params - params.require(:trends_status_batch).permit(:action, status_ids: []) + params + .expect(trends_status_batch: [:action, status_ids: []]) end def action_from_button diff --git a/app/controllers/admin/trends/tags_controller.rb b/app/controllers/admin/trends/tags_controller.rb index fcd23fbf66..1ccd740686 100644 --- a/app/controllers/admin/trends/tags_controller.rb +++ b/app/controllers/admin/trends/tags_controller.rb @@ -31,7 +31,8 @@ class Admin::Trends::TagsController < Admin::BaseController end def trends_tag_batch_params - params.require(:trends_tag_batch).permit(:action, tag_ids: []) + params + .expect(trends_tag_batch: [:action, tag_ids: []]) end def action_from_button diff --git a/app/controllers/admin/users/roles_controller.rb b/app/controllers/admin/users/roles_controller.rb index f5dfc643d4..e8b58de504 100644 --- a/app/controllers/admin/users/roles_controller.rb +++ b/app/controllers/admin/users/roles_controller.rb @@ -28,7 +28,8 @@ module Admin end def resource_params - params.require(:user).permit(:role_id) + params + .expect(user: [:role_id]) end end end diff --git a/app/controllers/admin/warning_presets_controller.rb b/app/controllers/admin/warning_presets_controller.rb index efbf65b119..dcf88294ee 100644 --- a/app/controllers/admin/warning_presets_controller.rb +++ b/app/controllers/admin/warning_presets_controller.rb @@ -52,7 +52,8 @@ module Admin end def warning_preset_params - params.require(:account_warning_preset).permit(:title, :text) + params + .expect(account_warning_preset: [:title, :text]) end end end diff --git a/app/controllers/admin/webhooks_controller.rb b/app/controllers/admin/webhooks_controller.rb index f1aad7c4b5..31db369637 100644 --- a/app/controllers/admin/webhooks_controller.rb +++ b/app/controllers/admin/webhooks_controller.rb @@ -74,7 +74,8 @@ module Admin end def resource_params - params.require(:webhook).permit(:url, :template, events: []) + params + .expect(webhook: [:url, :template, events: []]) end end end diff --git a/app/controllers/antennas_controller.rb b/app/controllers/antennas_controller.rb deleted file mode 100644 index ca7ee5d2a2..0000000000 --- a/app/controllers/antennas_controller.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -class AntennasController < ApplicationController - layout 'admin' - - before_action :authenticate_user! - before_action :set_antenna, only: [:edit, :update, :destroy] - before_action :set_body_classes - before_action :set_cache_headers - - def index - @antennas = current_account.antennas.includes(:antenna_domains).includes(:antenna_tags).includes(:antenna_accounts) - end - - def edit; end - - def update - if @antenna.update(resource_params) - redirect_to antennas_path - else - render action: :edit - end - end - - def destroy - @antenna.destroy - redirect_to antennas_path - end - - private - - def set_antenna - @antenna = current_account.antennas.find(params[:id]) - end - - def resource_params - params.require(:antenna).permit(:title, :available, :expires_in) - end - - def thin_resource_params - params.require(:antenna).permit(:title) - 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/api/fasp/base_controller.rb b/app/controllers/api/fasp/base_controller.rb new file mode 100644 index 0000000000..690f7e419a --- /dev/null +++ b/app/controllers/api/fasp/base_controller.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +class Api::Fasp::BaseController < ApplicationController + class Error < ::StandardError; end + + DIGEST_PATTERN = /sha-256=:(.*?):/ + KEYID_PATTERN = /keyid="(.*?)"/ + + attr_reader :current_provider + + skip_forgery_protection + + before_action :check_fasp_enabled + before_action :require_authentication + after_action :sign_response + + private + + def require_authentication + validate_content_digest! + validate_signature! + rescue Error, Linzer::Error, ActiveRecord::RecordNotFound => e + logger.debug("FASP Authentication error: #{e}") + authentication_error + end + + def authentication_error + respond_to do |format| + format.json { head 401 } + end + end + + def validate_content_digest! + content_digest_header = request.headers['content-digest'] + raise Error, 'content-digest missing' if content_digest_header.blank? + + digest_received = content_digest_header.match(DIGEST_PATTERN)[1] + + digest_computed = OpenSSL::Digest.base64digest('sha256', request.body&.string || '') + + raise Error, 'content-digest does not match' if digest_received != digest_computed + end + + def validate_signature! + signature_input = request.headers['signature-input']&.encode('UTF-8') + raise Error, 'signature-input is missing' if signature_input.blank? + + keyid = signature_input.match(KEYID_PATTERN)[1] + provider = Fasp::Provider.find(keyid) + linzer_request = Linzer.new_request( + request.method, + request.original_url, + {}, + { + 'content-digest' => request.headers['content-digest'], + 'signature-input' => signature_input, + 'signature' => request.headers['signature'], + } + ) + message = Linzer::Message.new(linzer_request) + key = Linzer.new_ed25519_public_key(provider.provider_public_key_pem, keyid) + signature = Linzer::Signature.build(message.headers) + Linzer.verify(key, message, signature) + @current_provider = provider + end + + def sign_response + response.headers['content-digest'] = "sha-256=:#{OpenSSL::Digest.base64digest('sha256', response.body || '')}:" + + linzer_response = Linzer.new_response(response.body, response.status, { 'content-digest' => response.headers['content-digest'] }) + message = Linzer::Message.new(linzer_response) + key = Linzer.new_ed25519_key(current_provider.server_private_key_pem) + signature = Linzer.sign(key, message, %w(@status content-digest)) + + response.headers.merge!(signature.to_h) + end + + def check_fasp_enabled + raise ActionController::RoutingError unless Mastodon::Feature.fasp_enabled? + end +end diff --git a/app/controllers/api/fasp/debug/v0/callback/responses_controller.rb b/app/controllers/api/fasp/debug/v0/callback/responses_controller.rb new file mode 100644 index 0000000000..794e53f095 --- /dev/null +++ b/app/controllers/api/fasp/debug/v0/callback/responses_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class Api::Fasp::Debug::V0::Callback::ResponsesController < Api::Fasp::BaseController + def create + Fasp::DebugCallback.create( + fasp_provider: current_provider, + ip: request.remote_ip, + request_body: request.raw_post + ) + + respond_to do |format| + format.json { head 201 } + end + end +end diff --git a/app/controllers/api/fasp/registrations_controller.rb b/app/controllers/api/fasp/registrations_controller.rb new file mode 100644 index 0000000000..fecc992fec --- /dev/null +++ b/app/controllers/api/fasp/registrations_controller.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class Api::Fasp::RegistrationsController < Api::Fasp::BaseController + skip_before_action :require_authentication + + def create + @current_provider = Fasp::Provider.create!( + name: params[:name], + base_url: params[:baseUrl], + remote_identifier: params[:serverId], + provider_public_key_base64: params[:publicKey] + ) + + render json: registration_confirmation + end + + private + + def registration_confirmation + { + faspId: current_provider.id.to_s, + publicKey: current_provider.server_public_key_base64, + registrationCompletionUri: new_admin_fasp_provider_registration_url(current_provider), + } + end +end diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index 7488fdec7c..bdd7732b87 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -14,7 +14,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController @account = current_account UpdateAccountService.new.call(@account, account_params, raise_error: true) current_user.update(user_params) if user_params - ActivityPub::UpdateDistributionWorker.perform_async(@account.id) + ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) render json: @account, serializer: REST::CredentialAccountSerializer rescue ActiveRecord::RecordInvalid => e render json: ValidationErrorFormatter.new(e).as_json, status: 422 @@ -34,6 +34,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController :searchability, :hide_collections, :indexable, + attribution_domains: [], fields_attributes: [:name, :value] ) end diff --git a/app/controllers/api/v1/accounts/endorsements_controller.rb b/app/controllers/api/v1/accounts/endorsements_controller.rb new file mode 100644 index 0000000000..1e21994a90 --- /dev/null +++ b/app/controllers/api/v1/accounts/endorsements_controller.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +class Api::V1::Accounts::EndorsementsController < Api::BaseController + include Authorization + + before_action -> { authorize_if_got_token! :read, :'read:accounts' }, only: :index + before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index + before_action :require_user!, except: :index + before_action :set_account + before_action :set_endorsed_accounts, only: :index + after_action :insert_pagination_headers, only: :index + + def index + cache_if_unauthenticated! + render json: @endorsed_accounts, each_serializer: REST::AccountSerializer + end + + def create + AccountPin.find_or_create_by!(account: current_account, target_account: @account) + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter + end + + def destroy + pin = AccountPin.find_by(account: current_account, target_account: @account) + pin&.destroy! + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter + end + + private + + def set_account + @account = Account.find(params[:account_id]) + end + + def set_endorsed_accounts + @endorsed_accounts = @account.unavailable? ? [] : paginated_endorsed_accounts + end + + def paginated_endorsed_accounts + @account.endorsed_accounts.without_suspended.includes(:account_stat, :user).paginate_by_max_id( + limit_param(DEFAULT_ACCOUNTS_LIMIT), + params[:max_id], + params[:since_id] + ) + end + + def relationships_presenter + AccountRelationshipsPresenter.new([@account], current_user.account_id) + end + + def next_path + api_v1_account_endorsements_url pagination_params(max_id: pagination_max_id) if records_continue? + end + + def prev_path + api_v1_account_endorsements_url pagination_params(since_id: pagination_since_id) unless @endorsed_accounts.empty? + end + + def pagination_collection + @endorsed_accounts + end + + def records_continue? + @endorsed_accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) + end +end diff --git a/app/controllers/api/v1/accounts/familiar_followers_controller.rb b/app/controllers/api/v1/accounts/familiar_followers_controller.rb index a49eb2eb27..81f0a9ed0f 100644 --- a/app/controllers/api/v1/accounts/familiar_followers_controller.rb +++ b/app/controllers/api/v1/accounts/familiar_followers_controller.rb @@ -12,7 +12,7 @@ class Api::V1::Accounts::FamiliarFollowersController < Api::BaseController private def set_accounts - @accounts = Account.without_suspended.where(id: account_ids).select('id, hide_collections') + @accounts = Account.without_suspended.where(id: account_ids).select(:id, :hide_collections) end def familiar_followers diff --git a/app/controllers/api/v1/accounts/featured_tags_controller.rb b/app/controllers/api/v1/accounts/featured_tags_controller.rb index 0101fb469b..f95846366c 100644 --- a/app/controllers/api/v1/accounts/featured_tags_controller.rb +++ b/app/controllers/api/v1/accounts/featured_tags_controller.rb @@ -17,6 +17,6 @@ class Api::V1::Accounts::FeaturedTagsController < Api::BaseController end def set_featured_tags - @featured_tags = @account.suspended? ? [] : @account.featured_tags + @featured_tags = @account.unavailable? ? [] : @account.featured_tags end end diff --git a/app/controllers/api/v1/accounts/identity_proofs_controller.rb b/app/controllers/api/v1/accounts/identity_proofs_controller.rb index 48f293f47a..02a45e8758 100644 --- a/app/controllers/api/v1/accounts/identity_proofs_controller.rb +++ b/app/controllers/api/v1/accounts/identity_proofs_controller.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true class Api::V1::Accounts::IdentityProofsController < Api::BaseController + include DeprecationConcern + + deprecate_api '2022-03-30' + before_action :require_user! before_action :set_account diff --git a/app/controllers/api/v1/accounts/pins_controller.rb b/app/controllers/api/v1/accounts/pins_controller.rb deleted file mode 100644 index 0eb13c048c..0000000000 --- a/app/controllers/api/v1/accounts/pins_controller.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Accounts::PinsController < Api::BaseController - include Authorization - - before_action -> { doorkeeper_authorize! :write, :'write:accounts' } - before_action :require_user! - before_action :set_account - - def create - AccountPin.find_or_create_by!(account: current_account, target_account: @account) - render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter - end - - def destroy - pin = AccountPin.find_by(account: current_account, target_account: @account) - pin&.destroy! - render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter - end - - private - - def set_account - @account = Account.find(params[:account_id]) - end - - def relationships_presenter - AccountRelationshipsPresenter.new([@account], current_user.account_id) - end -end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 050d13b0c8..46838aeb66 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -111,8 +111,8 @@ class Api::V1::AccountsController < Api::BaseController render json: { error: I18n.t('accounts.self_follow_error') }, status: 403 if current_user.account.id == @account.id end - def relationships(**options) - AccountRelationshipsPresenter.new([@account], current_user.account_id, **options) + def relationships(**) + AccountRelationshipsPresenter.new([@account], current_user.account_id, **) end def account_ids @@ -124,7 +124,7 @@ class Api::V1::AccountsController < Api::BaseController end def account_params - params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code) + params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code, :date_of_birth) end def invite diff --git a/app/controllers/api/v1/annual_reports_controller.rb b/app/controllers/api/v1/annual_reports_controller.rb index 9bc8e68ac2..b1aee288dd 100644 --- a/app/controllers/api/v1/annual_reports_controller.rb +++ b/app/controllers/api/v1/annual_reports_controller.rb @@ -17,6 +17,17 @@ class Api::V1::AnnualReportsController < Api::BaseController relationships: @relationships end + def show + with_read_replica do + @presenter = AnnualReportsPresenter.new([@annual_report]) + @relationships = StatusRelationshipsPresenter.new(@presenter.statuses, current_account.id) + end + + render json: @presenter, + serializer: REST::AnnualReportsSerializer, + relationships: @relationships + end + def read @annual_report.view! render_empty diff --git a/app/controllers/api/v1/antennas_controller.rb b/app/controllers/api/v1/antennas_controller.rb index 7cb0c5f093..4040263c00 100644 --- a/app/controllers/api/v1/antennas_controller.rb +++ b/app/controllers/api/v1/antennas_controller.rb @@ -21,7 +21,7 @@ class Api::V1::AntennasController < Api::BaseController end def create - @antenna = Antenna.create!({ list_id: 0 }.merge(antenna_params.merge(account: current_account))) + @antenna = Antenna.create!(antenna_params.merge(account: current_account)) render json: @antenna, serializer: REST::AntennaSerializer end @@ -42,6 +42,6 @@ class Api::V1::AntennasController < Api::BaseController end def antenna_params - params.permit(:title, :list_id, :insert_feeds, :stl, :ltl, :with_media_only, :ignore_reblog) + params.permit(:title, :list_id, :insert_feeds, :stl, :ltl, :with_media_only, :ignore_reblog, :favourite) end end diff --git a/app/controllers/api/v1/featured_tags/suggestions_controller.rb b/app/controllers/api/v1/featured_tags/suggestions_controller.rb index 9c72e4380d..d533b1af7b 100644 --- a/app/controllers/api/v1/featured_tags/suggestions_controller.rb +++ b/app/controllers/api/v1/featured_tags/suggestions_controller.rb @@ -5,6 +5,8 @@ class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController before_action :require_user! before_action :set_recently_used_tags, only: :index + RECENT_TAGS_LIMIT = 10 + def index render json: @recently_used_tags, each_serializer: REST::TagSerializer, relationships: TagRelationshipsPresenter.new(@recently_used_tags, current_user&.account_id) end @@ -12,6 +14,6 @@ class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController private def set_recently_used_tags - @recently_used_tags = Tag.suggestions_for_account(current_account).limit(10) + @recently_used_tags = Tag.suggestions_for_account(current_account).limit(RECENT_TAGS_LIMIT) end end diff --git a/app/controllers/api/v1/filters_controller.rb b/app/controllers/api/v1/filters_controller.rb index c97e9720ad..f8d91c5f7f 100644 --- a/app/controllers/api/v1/filters_controller.rb +++ b/app/controllers/api/v1/filters_controller.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true class Api::V1::FiltersController < Api::BaseController + include DeprecationConcern + + deprecate_api '2022-11-14' + before_action -> { doorkeeper_authorize! :read, :'read:filters' }, only: [:index, :show] before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show] before_action :require_user! diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb index 29a09fceef..4b44cfe531 100644 --- a/app/controllers/api/v1/follow_requests_controller.rb +++ b/app/controllers/api/v1/follow_requests_controller.rb @@ -28,8 +28,8 @@ class Api::V1::FollowRequestsController < Api::BaseController @account ||= Account.find(params[:id]) end - def relationships(**options) - AccountRelationshipsPresenter.new([account], current_user.account_id, **options) + def relationships(**) + AccountRelationshipsPresenter.new([account], current_user.account_id, **) end def load_accounts diff --git a/app/controllers/api/v1/instances/terms_of_services_controller.rb b/app/controllers/api/v1/instances/terms_of_services_controller.rb new file mode 100644 index 0000000000..0a861dd7bb --- /dev/null +++ b/app/controllers/api/v1/instances/terms_of_services_controller.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class Api::V1::Instances::TermsOfServicesController < Api::V1::Instances::BaseController + before_action :set_terms_of_service + + def show + cache_even_if_authenticated! + render json: @terms_of_service, serializer: REST::TermsOfServiceSerializer + end + + private + + def set_terms_of_service + @terms_of_service = begin + if params[:date].present? + TermsOfService.published.find_by!(effective_date: params[:date]) + else + TermsOfService.live.first || TermsOfService.published.first! # For the case when none of the published terms have become effective yet + end + end + end +end diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb index 49da75ed28..e01267c000 100644 --- a/app/controllers/api/v1/instances_controller.rb +++ b/app/controllers/api/v1/instances_controller.rb @@ -1,15 +1,9 @@ # frozen_string_literal: true -class Api::V1::InstancesController < Api::BaseController - skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? - skip_around_action :set_locale +class Api::V1::InstancesController < Api::V2::InstancesController + include DeprecationConcern - vary_by '' - - # Override `current_user` to avoid reading session cookies unless in limited federation mode - def current_user - super if limited_federation_mode? - end + deprecate_api '2022-11-14' def show cache_even_if_authenticated! diff --git a/app/controllers/api/v1/lists/accounts_controller.rb b/app/controllers/api/v1/lists/accounts_controller.rb index b1c0e609d0..616159f05f 100644 --- a/app/controllers/api/v1/lists/accounts_controller.rb +++ b/app/controllers/api/v1/lists/accounts_controller.rb @@ -15,17 +15,12 @@ class Api::V1::Lists::AccountsController < Api::BaseController end def create - ApplicationRecord.transaction do - list_accounts.each do |account| - @list.accounts << account - end - end - + AddAccountsToListService.new.call(@list, Account.find(account_ids)) render_empty end def destroy - ListAccount.where(list: @list, account_id: account_ids).destroy_all + RemoveAccountsFromListService.new.call(@list, Account.where(id: account_ids)) render_empty end @@ -43,10 +38,6 @@ class Api::V1::Lists::AccountsController < Api::BaseController end end - def list_accounts - Account.find(account_ids) - end - def account_ids Array(resource_params[:account_ids]) end diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb index 0bacd7fdb0..b019ab6018 100644 --- a/app/controllers/api/v1/lists_controller.rb +++ b/app/controllers/api/v1/lists_controller.rb @@ -7,10 +7,6 @@ class Api::V1::ListsController < Api::BaseController before_action :require_user! before_action :set_list, except: [:index, :create] - rescue_from ArgumentError do |e| - render json: { error: e.to_s }, status: 422 - end - def index @lists = List.where(account: current_account).all render json: @lists, each_serializer: REST::ListSerializer @@ -38,6 +34,16 @@ class Api::V1::ListsController < Api::BaseController render_empty end + def favourite + @list.favourite! + render json: @list, serializer: REST::ListSerializer + end + + def unfavourite + @list.unfavourite! + render json: @list, serializer: REST::ListSerializer + end + private def set_list @@ -45,6 +51,6 @@ class Api::V1::ListsController < Api::BaseController end def list_params - params.permit(:title, :replies_policy, :exclusive, :notify) + params.permit(:title, :replies_policy, :exclusive, :notify, :favourite) end end diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb index 5ea26d55bd..c427e055ea 100644 --- a/app/controllers/api/v1/media_controller.rb +++ b/app/controllers/api/v1/media_controller.rb @@ -3,8 +3,8 @@ class Api::V1::MediaController < Api::BaseController before_action -> { doorkeeper_authorize! :write, :'write:media' } before_action :require_user! - before_action :set_media_attachment, except: [:create] - before_action :check_processing, except: [:create] + before_action :set_media_attachment, except: [:create, :destroy] + before_action :check_processing, except: [:create, :destroy] def show render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment @@ -25,6 +25,15 @@ class Api::V1::MediaController < Api::BaseController render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment end + def destroy + @media_attachment = current_account.media_attachments.find(params[:id]) + + return render json: in_usage_error, status: 422 unless @media_attachment.status_id.nil? + + @media_attachment.destroy + render_empty + end + private def status_code_for_media_attachment @@ -54,4 +63,8 @@ class Api::V1::MediaController < Api::BaseController def processing_error { error: 'Error processing thumbnail for uploaded media' } end + + def in_usage_error + { error: 'Media attachment is currently used by a status' } + end end diff --git a/app/controllers/api/v1/polls/votes_controller.rb b/app/controllers/api/v1/polls/votes_controller.rb index ad1b82cb52..2833687a38 100644 --- a/app/controllers/api/v1/polls/votes_controller.rb +++ b/app/controllers/api/v1/polls/votes_controller.rb @@ -15,7 +15,7 @@ class Api::V1::Polls::VotesController < Api::BaseController private def set_poll - @poll = Poll.attached.find(params[:poll_id]) + @poll = Poll.find(params[:poll_id]) authorize @poll.status, :show? rescue Mastodon::NotPermittedError not_found diff --git a/app/controllers/api/v1/polls_controller.rb b/app/controllers/api/v1/polls_controller.rb index ffc70a8496..b4c25476e8 100644 --- a/app/controllers/api/v1/polls_controller.rb +++ b/app/controllers/api/v1/polls_controller.rb @@ -15,7 +15,7 @@ class Api::V1::PollsController < Api::BaseController private def set_poll - @poll = Poll.attached.find(params[:id]) + @poll = Poll.find(params[:id]) authorize @poll.status, :show? rescue Mastodon::NotPermittedError not_found diff --git a/app/controllers/api/v1/profile/avatars_controller.rb b/app/controllers/api/v1/profile/avatars_controller.rb index bc4d01a597..e6c954ed63 100644 --- a/app/controllers/api/v1/profile/avatars_controller.rb +++ b/app/controllers/api/v1/profile/avatars_controller.rb @@ -7,7 +7,7 @@ class Api::V1::Profile::AvatarsController < Api::BaseController def destroy @account = current_account UpdateAccountService.new.call(@account, { avatar: nil }, raise_error: true) - ActivityPub::UpdateDistributionWorker.perform_async(@account.id) + ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) render json: @account, serializer: REST::CredentialAccountSerializer end end diff --git a/app/controllers/api/v1/profile/headers_controller.rb b/app/controllers/api/v1/profile/headers_controller.rb index 9f4daa2f77..4472a01b05 100644 --- a/app/controllers/api/v1/profile/headers_controller.rb +++ b/app/controllers/api/v1/profile/headers_controller.rb @@ -7,7 +7,7 @@ class Api::V1::Profile::HeadersController < Api::BaseController def destroy @account = current_account UpdateAccountService.new.call(@account, { header: nil }, raise_error: true) - ActivityPub::UpdateDistributionWorker.perform_async(@account.id) + ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) render json: @account, serializer: REST::CredentialAccountSerializer end end diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb index e1ad89ee3e..f2c52f2846 100644 --- a/app/controllers/api/v1/push/subscriptions_controller.rb +++ b/app/controllers/api/v1/push/subscriptions_controller.rb @@ -21,6 +21,7 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController endpoint: subscription_params[:endpoint], key_p256dh: subscription_params[:keys][:p256dh], key_auth: subscription_params[:keys][:auth], + standard: subscription_params[:standard] || false, data: data_params, user_id: current_user.id, access_token_id: doorkeeper_token.id @@ -55,12 +56,12 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController end def subscription_params - params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh]) + params.expect(subscription: [:endpoint, :standard, keys: [:auth, :p256dh]]) end def data_params return {} if params[:data].blank? - params.require(:data).permit(:policy, alerts: Notification::TYPES) + params.expect(data: [:policy, alerts: Notification::TYPES]) end end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 534347d019..1217b70752 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -67,6 +67,8 @@ class Api::V1::StatusesController < Api::BaseController statuses = [@status] + @context.ancestors + @context.descendants + @context.references render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id) + + ActivityPub::FetchAllRepliesWorker.perform_async(@status.id) if !current_account.nil? && @status.should_fetch_replies? end def create @@ -125,7 +127,7 @@ class Api::V1::StatusesController < Api::BaseController @status.account.statuses_count = @status.account.statuses_count - 1 json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true - RemovalWorker.perform_async(@status.id, { 'redraft' => true }) + RemovalWorker.perform_async(@status.id, { 'redraft' => !truthy_param?(:delete_media) }) render json: json end diff --git a/app/controllers/api/v1/suggestions_controller.rb b/app/controllers/api/v1/suggestions_controller.rb index 9ba1cef63c..df9346832f 100644 --- a/app/controllers/api/v1/suggestions_controller.rb +++ b/app/controllers/api/v1/suggestions_controller.rb @@ -2,6 +2,9 @@ class Api::V1::SuggestionsController < Api::BaseController include Authorization + include DeprecationConcern + + deprecate_api '2021-05-16', only: [:index] before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index diff --git a/app/controllers/api/v1/trends/tags_controller.rb b/app/controllers/api/v1/trends/tags_controller.rb index b15dd50131..ecac3579fc 100644 --- a/app/controllers/api/v1/trends/tags_controller.rb +++ b/app/controllers/api/v1/trends/tags_controller.rb @@ -1,11 +1,15 @@ # frozen_string_literal: true class Api::V1::Trends::TagsController < Api::BaseController + include DeprecationConcern + before_action :set_tags after_action :insert_pagination_headers - DEFAULT_TAGS_LIMIT = 10 + DEFAULT_TAGS_LIMIT = (ENV['MAX_TRENDING_TAGS'] || 10).to_i + + deprecate_api '2022-03-30', only: :index, if: -> { request.path == '/api/v1/trends' } def index cache_if_unauthenticated! @@ -27,7 +31,9 @@ class Api::V1::Trends::TagsController < Api::BaseController end def tags_from_trends - Trends.tags.query.allowed + scope = Trends.tags.query.allowed.in_locale(content_locale) + scope = scope.filtered_for(current_account) if user_signed_in? + scope end def next_path diff --git a/app/controllers/api/v2/instances_controller.rb b/app/controllers/api/v2/instances_controller.rb index 8346e28830..62adf95260 100644 --- a/app/controllers/api/v2/instances_controller.rb +++ b/app/controllers/api/v2/instances_controller.rb @@ -1,6 +1,16 @@ # frozen_string_literal: true -class Api::V2::InstancesController < Api::V1::InstancesController +class Api::V2::InstancesController < Api::BaseController + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? + skip_around_action :set_locale + + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in limited federation mode + def current_user + super if limited_federation_mode? + end + def show cache_even_if_authenticated! render_with_cache json: InstancePresenter.new, serializer: REST::InstanceSerializer, root: 'instance' diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb index 167d16fc4d..2711071b4a 100644 --- a/app/controllers/api/web/push_subscriptions_controller.rb +++ b/app/controllers/api/web/push_subscriptions_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::Web::PushSubscriptionsController < Api::Web::BaseController - before_action :require_user! + before_action :require_user!, except: :destroy before_action :set_push_subscription, only: :update before_action :destroy_previous_subscriptions, only: :create, if: :prior_subscriptions? after_action :update_session_with_subscription, only: :create @@ -17,6 +17,13 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer end + def destroy + push_subscription = ::Web::PushSubscription.find_by_token_for(:unsubscribe, params[:id]) + push_subscription&.destroy + + head 200 + end + private def active_session @@ -59,7 +66,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController end def subscription_params - @subscription_params ||= params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh]) + @subscription_params ||= params.expect(subscription: [:standard, :endpoint, keys: [:auth, :p256dh]]) end def web_push_subscription_params @@ -69,11 +76,12 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController endpoint: subscription_params[:endpoint], key_auth: subscription_params[:keys][:auth], key_p256dh: subscription_params[:keys][:p256dh], + standard: subscription_params[:standard] || false, user_id: active_session.user_id, } end def data_params - @data_params ||= params.require(:data).permit(:policy, alerts: Notification::TYPES) + @data_params ||= params.expect(data: [:policy, alerts: Notification::TYPES]) end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 62e3355ae6..1b071e8655 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -22,7 +22,6 @@ class ApplicationController < ActionController::Base helper_method :use_seamless_external_login? helper_method :sso_account_settings helper_method :limited_federation_mode? - helper_method :body_class_string helper_method :skip_csrf_meta_tags? rescue_from ActionController::ParameterMissing, Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request @@ -32,7 +31,7 @@ class ApplicationController < ActionController::Base rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests - rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error + rescue_from(*Mastodon::HTTP_CONNECTION_ERRORS, with: :internal_server_error) rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight, ActiveRecord::SerializationFailure, with: :service_unavailable rescue_from Seahorse::Client::NetworkingError do |e| @@ -71,7 +70,13 @@ class ApplicationController < ActionController::Base end def require_functional! - redirect_to edit_user_registration_path unless current_user.functional? + return if current_user.functional? + + if current_user.confirmed? + redirect_to edit_user_registration_path + else + redirect_to auth_setup_path + end end def skip_csrf_meta_tags? @@ -158,10 +163,6 @@ class ApplicationController < ActionController::Base current_user.setting_theme end - def body_class_string - @body_classes || '' - end - def respond_with_error(code) respond_to do |format| format.any { render "errors/#{code}", layout: 'error', status: code, formats: [:html] } diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 4d94c80158..0b6f5b3af4 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -12,7 +12,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController before_action :set_sessions, only: [:edit, :update] before_action :set_strikes, only: [:edit, :update] before_action :require_not_suspended!, only: [:update] - before_action :set_cache_headers, only: [:edit, :update] before_action :set_rules, only: :new before_action :require_rules_acceptance!, only: :new before_action :set_registration_form_time, only: :new @@ -63,7 +62,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController def configure_sign_up_params devise_parameter_sanitizer.permit(:sign_up) do |user_params| - user_params.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password) + user_params.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password, :date_of_birth) end end @@ -139,7 +138,11 @@ class Auth::RegistrationsController < Devise::RegistrationsController set_locale { render :rules } end - def set_cache_headers - response.cache_control.replace(private: true, no_store: true) + def is_flashing_format? # rubocop:disable Naming/PredicateName + if params[:action] == 'create' + false # Disable flash messages for sign-up + else + super + end end end diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index 18603a32f2..5f9f133659 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -73,7 +73,7 @@ class Auth::SessionsController < Devise::SessionsController end def user_params - params.require(:user).permit(:email, :password, :otp_attempt, :disable_css, credential: {}) + params.expect(user: [:email, :password, :otp_attempt, :disable_css, credential: {}]) end def login_page_params @@ -174,7 +174,7 @@ class Auth::SessionsController < Devise::SessionsController end def disable_custom_css? - user_params[:disable_css].present? && user_params[:disable_css] != '0' + user_params[:disable_css].present? && user_params[:disable_css] == '1' end def disable_custom_css!(user) diff --git a/app/controllers/auth/setup_controller.rb b/app/controllers/auth/setup_controller.rb index ad872dc607..5e7b14646a 100644 --- a/app/controllers/auth/setup_controller.rb +++ b/app/controllers/auth/setup_controller.rb @@ -18,7 +18,7 @@ class Auth::SetupController < ApplicationController if @user.update(user_params) @user.resend_confirmation_instructions unless @user.confirmed? - redirect_to auth_setup_path, notice: I18n.t('auth.setup.new_confirmation_instructions_sent') + redirect_to auth_setup_path, notice: t('auth.setup.new_confirmation_instructions_sent') else render :show end @@ -35,6 +35,6 @@ class Auth::SetupController < ApplicationController end def user_params - params.require(:user).permit(:email) + params.expect(user: [:email]) end end diff --git a/app/controllers/backups_controller.rb b/app/controllers/backups_controller.rb index 5df1af5f2f..076d19874b 100644 --- a/app/controllers/backups_controller.rb +++ b/app/controllers/backups_controller.rb @@ -9,13 +9,15 @@ class BackupsController < ApplicationController before_action :authenticate_user! before_action :set_backup + BACKUP_LINK_TIMEOUT = 1.hour.freeze + def download case Paperclip::Attachment.default_options[:storage] when :s3, :azure - redirect_to @backup.dump.expiring_url(10), allow_other_host: true + redirect_to @backup.dump.expiring_url(BACKUP_LINK_TIMEOUT.to_i), 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 + redirect_to @backup.dump.expiring_url(BACKUP_LINK_TIMEOUT.from_now), allow_other_host: true else redirect_to full_asset_url(@backup.dump.url), allow_other_host: true end diff --git a/app/controllers/concerns/admin/export_controller_concern.rb b/app/controllers/concerns/admin/export_controller_concern.rb index 6228ae67fe..ce03b2a24a 100644 --- a/app/controllers/concerns/admin/export_controller_concern.rb +++ b/app/controllers/concerns/admin/export_controller_concern.rb @@ -24,6 +24,6 @@ module Admin::ExportControllerConcern end def import_params - params.require(:admin_import).permit(:data) + params.expect(admin_import: [:data]) end end diff --git a/app/controllers/concerns/api/error_handling.rb b/app/controllers/concerns/api/error_handling.rb index ad559fe2d7..9ce4795b02 100644 --- a/app/controllers/concerns/api/error_handling.rb +++ b/app/controllers/concerns/api/error_handling.rb @@ -20,7 +20,7 @@ module Api::ErrorHandling render json: { error: 'Record not found' }, status: 404 end - rescue_from HTTP::Error, Mastodon::UnexpectedResponseError do + rescue_from(*Mastodon::HTTP_CONNECTION_ERRORS, Mastodon::UnexpectedResponseError) do render json: { error: 'Remote data could not be fetched' }, status: 503 end diff --git a/app/controllers/concerns/auth/captcha_concern.rb b/app/controllers/concerns/auth/captcha_concern.rb index cfd93978ce..c01da21249 100644 --- a/app/controllers/concerns/auth/captcha_concern.rb +++ b/app/controllers/concerns/auth/captcha_concern.rb @@ -10,7 +10,7 @@ module Auth::CaptchaConcern end def captcha_available? - ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present? + Rails.configuration.x.captcha.secret_key.present? && Rails.configuration.x.captcha.site_key.present? end def captcha_enabled? diff --git a/app/controllers/concerns/cache_concern.rb b/app/controllers/concerns/cache_concern.rb index 3dc0ea840a..1cdd4d7cf0 100644 --- a/app/controllers/concerns/cache_concern.rb +++ b/app/controllers/concerns/cache_concern.rb @@ -38,7 +38,7 @@ module CacheConcern return render(options) end - 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(':') + key = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields]&.join(',')].compact.join(':') expires_in = options.delete(:expires_in) || 3.minutes body = Rails.cache.read(key, raw: true) diff --git a/app/controllers/concerns/challengable_concern.rb b/app/controllers/concerns/challengable_concern.rb index c8d1a0bef7..7fbc469bdf 100644 --- a/app/controllers/concerns/challengable_concern.rb +++ b/app/controllers/concerns/challengable_concern.rb @@ -58,6 +58,6 @@ module ChallengableConcern end def challenge_params - params.require(:form_challenge).permit(:current_password, :return_to) + params.expect(form_challenge: [:current_password, :return_to]) end end diff --git a/app/controllers/concerns/deprecation_concern.rb b/app/controllers/concerns/deprecation_concern.rb new file mode 100644 index 0000000000..ad8de724a1 --- /dev/null +++ b/app/controllers/concerns/deprecation_concern.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module DeprecationConcern + extend ActiveSupport::Concern + + class_methods do + def deprecate_api(date, sunset: nil, **kwargs) + deprecation_timestamp = "@#{date.to_datetime.to_i}" + sunset = sunset&.to_date&.httpdate + + before_action(**kwargs) do + response.headers['Deprecation'] = deprecation_timestamp + response.headers['Sunset'] = sunset if sunset + end + end + end +end diff --git a/app/controllers/concerns/localized.rb b/app/controllers/concerns/localized.rb index ede299d5a4..14742e3b5c 100644 --- a/app/controllers/concerns/localized.rb +++ b/app/controllers/concerns/localized.rb @@ -25,7 +25,7 @@ module Localized end def available_locale_or_nil(locale_name) - locale_name.to_sym if locale_name.present? && I18n.available_locales.include?(locale_name.to_sym) + locale_name.to_sym if locale_name.respond_to?(:to_sym) && I18n.available_locales.include?(locale_name.to_sym) end def content_locale diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index deabacbc80..ffe612f468 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -10,8 +10,6 @@ module SignatureVerification EXPIRATION_WINDOW_LIMIT = 12.hours CLOCK_SKEW_MARGIN = 1.hour - class SignatureVerificationError < StandardError; end - def require_account_signature! render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account end @@ -34,7 +32,7 @@ module SignatureVerification def signature_key_id signature_params['keyId'] - rescue SignatureVerificationError + rescue Mastodon::SignatureVerificationError nil end @@ -45,17 +43,17 @@ module SignatureVerification def signed_request_actor return @signed_request_actor if defined?(@signed_request_actor) - raise SignatureVerificationError, 'Request not signed' unless signed_request? - raise SignatureVerificationError, 'Incompatible request signature. keyId and signature are required' if missing_required_signature_parameters? - raise SignatureVerificationError, 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)' unless %w(rsa-sha256 hs2019).include?(signature_algorithm) - raise SignatureVerificationError, 'Signed request date outside acceptable time window' unless matches_time_window? + raise Mastodon::SignatureVerificationError, 'Request not signed' unless signed_request? + raise Mastodon::SignatureVerificationError, 'Incompatible request signature. keyId and signature are required' if missing_required_signature_parameters? + raise Mastodon::SignatureVerificationError, 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)' unless %w(rsa-sha256 hs2019).include?(signature_algorithm) + raise Mastodon::SignatureVerificationError, 'Signed request date outside acceptable time window' unless matches_time_window? verify_signature_strength! verify_body_digest! actor = actor_from_key_id(signature_params['keyId']) - raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil? + raise Mastodon::SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil? signature = Base64.decode64(signature_params['signature']) compare_signed_string = build_signed_string(include_query_string: true) @@ -68,7 +66,7 @@ module SignatureVerification actor = stoplight_wrapper.run { actor_refresh_key!(actor) } - raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil? + raise Mastodon::SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil? compare_signed_string = build_signed_string(include_query_string: true) return actor unless verify_signature(actor, signature, compare_signed_string).nil? @@ -78,9 +76,9 @@ module SignatureVerification return actor unless verify_signature(actor, signature, compare_signed_string).nil? fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)", signed_string: compare_signed_string, signature: signature_params['signature'] - rescue SignatureVerificationError => e + rescue Mastodon::SignatureVerificationError => e fail_with! e.message - rescue HTTP::Error, OpenSSL::SSL::SSLError => e + rescue *Mastodon::HTTP_CONNECTION_ERRORS => e fail_with! "Failed to fetch remote data: #{e.message}" rescue Mastodon::UnexpectedResponseError fail_with! 'Failed to fetch remote data (got unexpected reply from server)' @@ -104,7 +102,7 @@ module SignatureVerification def signature_params @signature_params ||= SignatureParser.parse(request.headers['Signature']) rescue SignatureParser::ParsingError - raise SignatureVerificationError, 'Error parsing signature parameters' + raise Mastodon::SignatureVerificationError, 'Error parsing signature parameters' end def signature_algorithm @@ -116,31 +114,31 @@ module SignatureVerification end def verify_signature_strength! - raise SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)') - raise SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(HttpSignatureDraft::REQUEST_TARGET) || signed_headers.include?('digest') - raise SignatureVerificationError, 'Mastodon requires the Host header to be signed when doing a GET request' if request.get? && !signed_headers.include?('host') - raise SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if request.post? && !signed_headers.include?('digest') + raise Mastodon::SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)') + raise Mastodon::SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(HttpSignatureDraft::REQUEST_TARGET) || signed_headers.include?('digest') + raise Mastodon::SignatureVerificationError, 'Mastodon requires the Host header to be signed when doing a GET request' if request.get? && !signed_headers.include?('host') + raise Mastodon::SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if request.post? && !signed_headers.include?('digest') end def verify_body_digest! return unless signed_headers.include?('digest') - raise SignatureVerificationError, 'Digest header missing' unless request.headers.key?('Digest') + raise Mastodon::SignatureVerificationError, 'Digest header missing' unless request.headers.key?('Digest') digests = request.headers['Digest'].split(',').map { |digest| digest.split('=', 2) }.map { |key, value| [key.downcase, value] } sha256 = digests.assoc('sha-256') - raise SignatureVerificationError, "Mastodon only supports SHA-256 in Digest header. Offered algorithms: #{digests.map(&:first).join(', ')}" if sha256.nil? + raise Mastodon::SignatureVerificationError, "Mastodon only supports SHA-256 in Digest header. Offered algorithms: #{digests.map(&:first).join(', ')}" if sha256.nil? return if body_digest == sha256[1] digest_size = begin Base64.strict_decode64(sha256[1].strip).length rescue ArgumentError - raise SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a valid base64 string. Given digest: #{sha256[1]}" + raise Mastodon::SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a valid base64 string. Given digest: #{sha256[1]}" 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 Mastodon::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]}" + raise Mastodon::SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}" end def verify_signature(actor, signature, compare_signed_string) @@ -165,13 +163,13 @@ module SignatureVerification "#{HttpSignatureDraft::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" end 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? + raise Mastodon::SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019' + raise Mastodon::SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank? "(created): #{signature_params['created']}" 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? + raise Mastodon::SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019' + raise Mastodon::SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank? "(expires): #{signature_params['expires']}" else @@ -193,7 +191,7 @@ module SignatureVerification expires_time = Time.at(signature_params['expires'].to_i).utc if signature_params['expires'].present? rescue ArgumentError => e - raise SignatureVerificationError, "Invalid Date header: #{e.message}" + raise Mastodon::SignatureVerificationError, "Invalid Date header: #{e.message}" end expires_time ||= created_time + 5.minutes unless created_time.nil? @@ -233,9 +231,9 @@ module SignatureVerification account end rescue Mastodon::PrivateNetworkAddressError => e - raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})" + raise Mastodon::SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})" rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, ActivityPub::FetchRemoteKeyService::Error, Webfinger::Error => e - raise SignatureVerificationError, e.message + raise Mastodon::SignatureVerificationError, e.message end def stoplight_wrapper @@ -251,8 +249,8 @@ module SignatureVerification ActivityPub::FetchRemoteActorService.new.call(actor.uri, only_key: true, suppress_errors: false) rescue Mastodon::PrivateNetworkAddressError => e - raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})" + raise Mastodon::SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})" rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, Webfinger::Error => e - raise SignatureVerificationError, e.message + raise Mastodon::SignatureVerificationError, e.message end end diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb index 9485ecda49..ec2256aa9c 100644 --- a/app/controllers/concerns/web_app_controller_concern.rb +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -7,7 +7,7 @@ module WebAppControllerConcern vary_by 'Accept, Accept-Language, Cookie' before_action :redirect_unauthenticated_to_permalinks! - before_action :set_app_body_class + before_action :set_referer_header content_security_policy do |p| policy = ContentSecurityPolicy.new @@ -24,10 +24,6 @@ module WebAppControllerConcern !(ENV['ONE_CLICK_SSO_LOGIN'] == 'true' && ENV['OMNIAUTH_ONLY'] == 'true' && Devise.omniauth_providers.length == 1) && current_user.nil? end - def set_app_body_class - @body_classes = 'app-body' - end - def redirect_unauthenticated_to_permalinks! return if user_signed_in? && current_account.moved_to_account_id.nil? @@ -46,4 +42,10 @@ module WebAppControllerConcern end end end + + protected + + def set_referer_header + response.set_header('Referrer-Policy', Setting.allow_referrer_origin ? 'strict-origin-when-cross-origin' : 'same-origin') + end end diff --git a/app/controllers/custom_css_controller.rb b/app/controllers/custom_css_controller.rb index f0ff6f8ca7..5b98914114 100644 --- a/app/controllers/custom_css_controller.rb +++ b/app/controllers/custom_css_controller.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true class CustomCssController < ActionController::Base # rubocop:disable Rails/ApplicationController - before_action :set_user_roles - def show - expires_in 3.minutes, public: true + expires_in 1.month, public: true render content_type: 'text/css' end @@ -13,19 +11,5 @@ class CustomCssController < ActionController::Base # rubocop:disable Rails/Appli def custom_css_styles Setting.custom_css end - - def user_custom_css? - return false if current_user.nil? - - current_user.setting_use_custom_css && current_user.custom_css_text.present? - end - - def user_custom_css - current_user.custom_css_text - end - helper_method :custom_css_styles, :user_custom_css?, :user_custom_css - - def set_user_roles - @user_roles = UserRole.providing_styles - end + helper_method :custom_css_styles end diff --git a/app/controllers/disputes/appeals_controller.rb b/app/controllers/disputes/appeals_controller.rb index 98b58d2117..797f31cf78 100644 --- a/app/controllers/disputes/appeals_controller.rb +++ b/app/controllers/disputes/appeals_controller.rb @@ -21,6 +21,6 @@ class Disputes::AppealsController < Disputes::BaseController end def appeal_params - params.require(:appeal).permit(:text) + params.expect(appeal: [:text]) end end diff --git a/app/controllers/disputes/base_controller.rb b/app/controllers/disputes/base_controller.rb index dd24a1b740..07677fd3f3 100644 --- a/app/controllers/disputes/base_controller.rb +++ b/app/controllers/disputes/base_controller.rb @@ -8,11 +8,4 @@ class Disputes::BaseController < ApplicationController skip_before_action :require_functional! before_action :authenticate_user! - before_action :set_cache_headers - - private - - def set_cache_headers - response.cache_control.replace(private: true, no_store: true) - end end diff --git a/app/controllers/filters/statuses_controller.rb b/app/controllers/filters/statuses_controller.rb index 7ada13f680..d85b017aaa 100644 --- a/app/controllers/filters/statuses_controller.rb +++ b/app/controllers/filters/statuses_controller.rb @@ -6,7 +6,6 @@ class Filters::StatusesController < ApplicationController before_action :authenticate_user! before_action :set_filter before_action :set_status_filters - before_action :set_cache_headers PER_PAGE = 20 @@ -34,14 +33,10 @@ class Filters::StatusesController < ApplicationController end def status_filter_batch_action_params - params.require(:form_status_filter_batch_action).permit(status_filter_ids: []) + params.expect(form_status_filter_batch_action: [status_filter_ids: []]) end def action_from_button 'remove' if params[:remove] 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 7746db049f..20b8135908 100644 --- a/app/controllers/filters_controller.rb +++ b/app/controllers/filters_controller.rb @@ -5,7 +5,6 @@ class FiltersController < ApplicationController before_action :authenticate_user! before_action :set_filter, only: [:edit, :update, :destroy] - before_action :set_cache_headers def index @filters = current_account.custom_filters.includes(:keywords, :statuses).order(:phrase) @@ -48,10 +47,6 @@ class FiltersController < ApplicationController end def resource_params - params.require(:custom_filter).permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :exclude_quote, :exclude_profile, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy]) - end - - def set_cache_headers - response.cache_control.replace(private: true, no_store: true) + params.expect(custom_filter: [:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :exclude_quote, :exclude_profile, context: [], keywords_attributes: [[:id, :keyword, :whole_word, :_destroy]]]) end end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index 44d90ec671..85f6ccc5e4 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -46,7 +46,7 @@ class FollowerAccountsController < ApplicationController end def page_url(page) - account_followers_url(@account, page: page) unless page.nil? + ActivityPub::TagManager.instance.followers_uri_for(@account, page: page) unless page.nil? end def next_page_url diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 070852695e..fc65333ac4 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -6,7 +6,6 @@ class InvitesController < ApplicationController layout 'admin' before_action :authenticate_user! - before_action :set_cache_headers def index authorize :invite, :create? @@ -43,10 +42,6 @@ class InvitesController < ApplicationController end def resource_params - params.require(:invite).permit(:max_uses, :expires_in, :autofollow, :comment) - end - - def set_cache_headers - response.cache_control.replace(private: true, no_store: true) + params.expect(invite: [:max_uses, :expires_in, :autofollow, :comment]) end end diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index c4230d62c3..f68d85e44e 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -13,7 +13,7 @@ class MediaProxyController < ApplicationController rescue_from ActiveRecord::RecordInvalid, with: :not_found rescue_from Mastodon::UnexpectedResponseError, with: :not_found rescue_from Mastodon::NotPermittedError, with: :not_found - rescue_from HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, with: :internal_server_error + rescue_from(*Mastodon::HTTP_CONNECTION_ERRORS, with: :internal_server_error) def show with_redis_lock("media_download:#{params[:id]}") do diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index 66e774425d..deafedeaef 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -5,7 +5,6 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController before_action :store_current_location before_action :authenticate_resource_owner! - before_action :set_cache_headers content_security_policy do |p| p.form_action(false) @@ -32,8 +31,4 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController def truthy_param?(key) ActiveModel::Type::Boolean.new.cast(params[key]) end - - def set_cache_headers - 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 267409a9ce..8b11a519ea 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -6,7 +6,6 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio before_action :store_current_location before_action :authenticate_resource_owner! before_action :require_not_suspended!, only: :destroy - before_action :set_cache_headers before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json } @@ -30,17 +29,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio forbidden if current_account.unavailable? 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 + @last_used_at_by_app = current_resource_owner.applications_last_used end end diff --git a/app/controllers/oauth/userinfo_controller.rb b/app/controllers/oauth/userinfo_controller.rb new file mode 100644 index 0000000000..e268b70dcc --- /dev/null +++ b/app/controllers/oauth/userinfo_controller.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class Oauth::UserinfoController < Api::BaseController + before_action -> { doorkeeper_authorize! :profile }, only: [:show] + before_action :require_user! + + def show + @account = current_account + render json: @account, serializer: OauthUserinfoSerializer + end +end diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb index d351afcfb7..7e793fc734 100644 --- a/app/controllers/relationships_controller.rb +++ b/app/controllers/relationships_controller.rb @@ -6,7 +6,6 @@ class RelationshipsController < ApplicationController before_action :authenticate_user! before_action :set_accounts, only: :show before_action :set_relationships, only: :show - before_action :set_cache_headers helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship? @@ -36,7 +35,7 @@ class RelationshipsController < ApplicationController end def form_account_batch_params - params.require(:form_account_batch).permit(:action, account_ids: []) + params.expect(form_account_batch: [:action, account_ids: []]) end def following_relationship? @@ -66,8 +65,4 @@ class RelationshipsController < ApplicationController 'remove_domains_from_followers' end end - - def set_cache_headers - response.cache_control.replace(private: true, no_store: true) - end end diff --git a/app/controllers/settings/aliases_controller.rb b/app/controllers/settings/aliases_controller.rb index a421b8ede3..c21d43eeb3 100644 --- a/app/controllers/settings/aliases_controller.rb +++ b/app/controllers/settings/aliases_controller.rb @@ -30,7 +30,7 @@ class Settings::AliasesController < Settings::BaseController private def resource_params - params.require(:account_alias).permit(:acct) + params.expect(account_alias: [:acct]) end def set_alias diff --git a/app/controllers/settings/applications_controller.rb b/app/controllers/settings/applications_controller.rb index d6573f9b49..8e39741f89 100644 --- a/app/controllers/settings/applications_controller.rb +++ b/app/controllers/settings/applications_controller.rb @@ -2,7 +2,6 @@ class Settings::ApplicationsController < Settings::BaseController before_action :set_application, only: [:show, :update, :destroy, :regenerate] - before_action :prepare_scopes, only: [:create, :update] def index @applications = current_user.applications.order(id: :desc).page(params[:page]) @@ -60,16 +59,6 @@ class Settings::ApplicationsController < Settings::BaseController end def application_params - params.require(:doorkeeper_application).permit( - :name, - :redirect_uri, - :scopes, - :website - ) - end - - def prepare_scopes - scopes = params.fetch(:doorkeeper_application, {}).fetch(:scopes, nil) - params[:doorkeeper_application][:scopes] = scopes.join(' ') if scopes.is_a? Array + params.expect(doorkeeper_application: [:name, :redirect_uri, :website, scopes: []]) end end diff --git a/app/controllers/settings/base_controller.rb b/app/controllers/settings/base_controller.rb index 188334ac23..7f2279aa8f 100644 --- a/app/controllers/settings/base_controller.rb +++ b/app/controllers/settings/base_controller.rb @@ -4,14 +4,9 @@ class Settings::BaseController < ApplicationController layout 'admin' before_action :authenticate_user! - before_action :set_cache_headers private - def set_cache_headers - response.cache_control.replace(private: true, no_store: true) - end - def require_not_suspended! forbidden if current_account.unavailable? end diff --git a/app/controllers/settings/deletes_controller.rb b/app/controllers/settings/deletes_controller.rb index 16c201b6b3..815d95ad83 100644 --- a/app/controllers/settings/deletes_controller.rb +++ b/app/controllers/settings/deletes_controller.rb @@ -21,7 +21,7 @@ class Settings::DeletesController < Settings::BaseController private def resource_params - params.require(:form_delete_confirmation).permit(:password, :username) + params.expect(form_delete_confirmation: [:password, :username]) end def require_not_suspended! diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb index 076ed5dadb..263d20eaea 100644 --- a/app/controllers/settings/exports_controller.rb +++ b/app/controllers/settings/exports_controller.rb @@ -9,7 +9,7 @@ class Settings::ExportsController < Settings::BaseController skip_before_action :require_functional! def show - @export = Export.new(current_account) + @export_summary = ExportSummary.new(preloaded_account) @backups = current_user.backups end @@ -25,4 +25,15 @@ class Settings::ExportsController < Settings::BaseController redirect_to settings_export_path end + + private + + def preloaded_account + current_account.tap do |account| + ActiveRecord::Associations::Preloader.new( + records: [account], + associations: :account_stat + ).call + end + end end diff --git a/app/controllers/settings/featured_tags_controller.rb b/app/controllers/settings/featured_tags_controller.rb index 90c112e219..0f352e1913 100644 --- a/app/controllers/settings/featured_tags_controller.rb +++ b/app/controllers/settings/featured_tags_controller.rb @@ -5,6 +5,8 @@ class Settings::FeaturedTagsController < Settings::BaseController before_action :set_featured_tag, except: [:index, :create] before_action :set_recently_used_tags, only: :index + RECENT_TAGS_LIMIT = 10 + def index @featured_tag = FeaturedTag.new end @@ -38,10 +40,10 @@ class Settings::FeaturedTagsController < Settings::BaseController end def set_recently_used_tags - @recently_used_tags = Tag.suggestions_for_account(current_account).limit(10) + @recently_used_tags = Tag.suggestions_for_account(current_account).limit(RECENT_TAGS_LIMIT) end def featured_tag_params - params.require(:featured_tag).permit(:name) + params.expect(featured_tag: [:name]) end end diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb index 569aa07c53..be1699315f 100644 --- a/app/controllers/settings/imports_controller.rb +++ b/app/controllers/settings/imports_controller.rb @@ -24,6 +24,8 @@ class Settings::ImportsController < Settings::BaseController lists: false, }.freeze + RECENT_IMPORTS_LIMIT = 10 + def index @import = Form::Import.new(current_account: current_account) end @@ -88,7 +90,7 @@ class Settings::ImportsController < Settings::BaseController private def import_params - params.require(:form_import).permit(:data, :type, :mode) + params.expect(form_import: [:data, :type, :mode]) end def set_bulk_import @@ -96,6 +98,6 @@ class Settings::ImportsController < Settings::BaseController end def set_recent_imports - @recent_imports = current_account.bulk_imports.reorder(id: :desc).limit(10) + @recent_imports = current_account.bulk_imports.reorder(id: :desc).limit(RECENT_IMPORTS_LIMIT) end end diff --git a/app/controllers/settings/migration/redirects_controller.rb b/app/controllers/settings/migration/redirects_controller.rb index 6d469f3842..d850e05e94 100644 --- a/app/controllers/settings/migration/redirects_controller.rb +++ b/app/controllers/settings/migration/redirects_controller.rb @@ -33,6 +33,6 @@ class Settings::Migration::RedirectsController < Settings::BaseController private def resource_params - params.require(:form_redirect).permit(:acct, :current_password, :current_username) + params.expect(form_redirect: [:acct, :current_password, :current_username]) end end diff --git a/app/controllers/settings/migrations_controller.rb b/app/controllers/settings/migrations_controller.rb index 62603aba81..92e3611fd9 100644 --- a/app/controllers/settings/migrations_controller.rb +++ b/app/controllers/settings/migrations_controller.rb @@ -27,7 +27,7 @@ class Settings::MigrationsController < Settings::BaseController private def resource_params - params.require(:account_migration).permit(:acct, :current_password, :current_username) + params.expect(account_migration: [:acct, :current_password, :current_username]) end def set_migrations diff --git a/app/controllers/settings/pictures_controller.rb b/app/controllers/settings/pictures_controller.rb index 58a4325307..7e61e6d580 100644 --- a/app/controllers/settings/pictures_controller.rb +++ b/app/controllers/settings/pictures_controller.rb @@ -8,7 +8,7 @@ module Settings def destroy if valid_picture? if UpdateAccountService.new.call(@account, { @picture => nil, "#{@picture}_remote_url" => '' }) - ActivityPub::UpdateDistributionWorker.perform_async(@account.id) + ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg'), status: 303 else redirect_to settings_profile_path diff --git a/app/controllers/settings/preferences/base_controller.rb b/app/controllers/settings/preferences/base_controller.rb index d3e62fb5d9..c2e705da3c 100644 --- a/app/controllers/settings/preferences/base_controller.rb +++ b/app/controllers/settings/preferences/base_controller.rb @@ -25,10 +25,10 @@ class Settings::Preferences::BaseController < Settings::BaseController end def original_user_params - params.require(:user).permit(:locale, :time_zone, :custom_css_text, chosen_languages: [], settings_attributes: UserSettings.keys) + params.expect(user: [:locale, :time_zone, :custom_css_text, chosen_languages: [], settings_attributes: UserSettings.keys]) end def disabled_visibilities_params - params.require(:user).permit(settings_attributes: { enabled_visibilities: [] }) + params.expect(user: [settings_attributes: { enabled_visibilities: [] }]) end end diff --git a/app/controllers/settings/privacy_controller.rb b/app/controllers/settings/privacy_controller.rb index 1102c89fad..96efa03ccf 100644 --- a/app/controllers/settings/privacy_controller.rb +++ b/app/controllers/settings/privacy_controller.rb @@ -8,7 +8,7 @@ class Settings::PrivacyController < Settings::BaseController def update if UpdateAccountService.new.call(@account, account_params.except(:settings)) current_user.update!(settings_attributes: account_params[:settings]) - ActivityPub::UpdateDistributionWorker.perform_async(@account.id) + ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) redirect_to settings_privacy_path, notice: I18n.t('generic.changes_saved_msg') else render :show @@ -18,7 +18,7 @@ class Settings::PrivacyController < Settings::BaseController private def account_params - params.require(:account).permit(:discoverable, :unlocked, :indexable, :show_collections, settings: UserSettings.keys) + params.expect(account: [:discoverable, :unlocked, :indexable, :show_collections, settings: UserSettings.keys]) end def set_account diff --git a/app/controllers/settings/privacy_extra_controller.rb b/app/controllers/settings/privacy_extra_controller.rb index 54cedf2c4b..f1292e644c 100644 --- a/app/controllers/settings/privacy_extra_controller.rb +++ b/app/controllers/settings/privacy_extra_controller.rb @@ -18,7 +18,7 @@ class Settings::PrivacyExtraController < Settings::BaseController private def account_params - params.require(:account).permit(settings: UserSettings.keys) + params.expect(account: [settings: UserSettings.keys]) end def set_account diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index dc759a060b..04a10fbfb9 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -9,7 +9,7 @@ class Settings::ProfilesController < Settings::BaseController def update if UpdateAccountService.new.call(@account, account_params) - ActivityPub::UpdateDistributionWorker.perform_async(@account.id) + ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg') else @account.build_fields @@ -20,7 +20,7 @@ class Settings::ProfilesController < Settings::BaseController private def account_params - params.require(:account).permit(:display_name, :note, :bio_markdown, :avatar, :header, :bot, :my_actor_type, fields_attributes: [:name, :value]) + params.expect(account: [:display_name, :note, :bio_markdown, :avatar, :header, :bot, :my_actor_type, fields_attributes: [[:name, :value]]]) end def set_account diff --git a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb index 1a0afe58b0..eae990e79b 100644 --- a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb +++ b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb @@ -38,7 +38,7 @@ module Settings private def confirmation_params - params.require(:form_two_factor_confirmation).permit(:otp_attempt) + params.expect(form_two_factor_confirmation: [:otp_attempt]) end def prepare_two_factor_form diff --git a/app/controllers/settings/verifications_controller.rb b/app/controllers/settings/verifications_controller.rb index 4e0663253c..4b949ca72d 100644 --- a/app/controllers/settings/verifications_controller.rb +++ b/app/controllers/settings/verifications_controller.rb @@ -8,7 +8,7 @@ class Settings::VerificationsController < Settings::BaseController def update if UpdateAccountService.new.call(@account, account_params) - ActivityPub::UpdateDistributionWorker.perform_async(@account.id) + ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) redirect_to settings_verification_path, notice: I18n.t('generic.changes_saved_msg') else render :show @@ -18,7 +18,9 @@ class Settings::VerificationsController < Settings::BaseController private def account_params - params.require(:account).permit(:attribution_domains_as_text) + params.expect(account: [:attribution_domains]).tap do |params| + params[:attribution_domains] = params[:attribution_domains].split if params[:attribution_domains] + end end def set_account diff --git a/app/controllers/severed_relationships_controller.rb b/app/controllers/severed_relationships_controller.rb index 965753a26f..817abebf62 100644 --- a/app/controllers/severed_relationships_controller.rb +++ b/app/controllers/severed_relationships_controller.rb @@ -4,7 +4,6 @@ class SeveredRelationshipsController < ApplicationController layout 'admin' before_action :authenticate_user! - before_action :set_cache_headers before_action :set_event, only: [:following, :followers] @@ -49,8 +48,4 @@ class SeveredRelationshipsController < ApplicationController def acct(account) account.local? ? account.local_username_and_domain : account.acct end - - def set_cache_headers - response.cache_control.replace(private: true, no_store: true) - end end diff --git a/app/controllers/statuses_cleanup_controller.rb b/app/controllers/statuses_cleanup_controller.rb index 4db02051cc..a25e544392 100644 --- a/app/controllers/statuses_cleanup_controller.rb +++ b/app/controllers/statuses_cleanup_controller.rb @@ -5,7 +5,6 @@ class StatusesCleanupController < ApplicationController before_action :authenticate_user! before_action :set_policy - before_action :set_cache_headers def show; end @@ -15,8 +14,6 @@ class StatusesCleanupController < ApplicationController else render :show end - rescue ActionController::ParameterMissing - # Do nothing end def require_functional! @@ -30,10 +27,6 @@ class StatusesCleanupController < ApplicationController end def resource_params - params.require(:account_statuses_cleanup_policy).permit(:enabled, :min_status_age, :keep_direct, :keep_pinned, :keep_polls, :keep_media, :keep_self_fav, :keep_self_bookmark, :keep_self_emoji, :min_favs, :min_reblogs, :min_emojis) - end - - def set_cache_headers - response.cache_control.replace(private: true, no_store: true) + params.expect(account_statuses_cleanup_policy: [:enabled, :min_status_age, :keep_direct, :keep_pinned, :keep_polls, :keep_media, :keep_self_fav, :keep_self_bookmark, :keep_self_emoji, :min_favs, :min_reblogs, :min_emojis]) end end diff --git a/app/controllers/system_css_controller.rb b/app/controllers/system_css_controller.rb new file mode 100644 index 0000000000..dd90491894 --- /dev/null +++ b/app/controllers/system_css_controller.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class SystemCssController < ActionController::Base # rubocop:disable Rails/ApplicationController + def show + expires_in 3.minutes, public: true + render content_type: 'text/css' + end +end diff --git a/app/controllers/terms_of_service_controller.rb b/app/controllers/terms_of_service_controller.rb new file mode 100644 index 0000000000..672fb07915 --- /dev/null +++ b/app/controllers/terms_of_service_controller.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class TermsOfServiceController < ApplicationController + include WebAppControllerConcern + + skip_before_action :require_functional! + + def show + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? + end +end diff --git a/app/helpers/admin/account_moderation_notes_helper.rb b/app/helpers/admin/account_moderation_notes_helper.rb index 2a3d954a35..7c931c1157 100644 --- a/app/helpers/admin/account_moderation_notes_helper.rb +++ b/app/helpers/admin/account_moderation_notes_helper.rb @@ -12,12 +12,12 @@ module Admin::AccountModerationNotesHelper ) end - def admin_account_inline_link_to(account) + def admin_account_inline_link_to(account, path: nil) return if account.nil? link_to( account_inline_text(account), - admin_account_path(account.id), + path || admin_account_path(account.id), class: class_names('inline-name-tag', suspended: suspended_account?(account)), title: account.acct ) diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index 54a99dec29..07381cc6ad 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -35,6 +35,8 @@ module Admin::ActionLogsHelper else I18n.t('admin.action_logs.deleted_account') end + when 'Relay' + link_to log.human_identifier, admin_relays_path end end diff --git a/app/helpers/admin/settings_helper.rb b/app/helpers/admin/settings_helper.rb index 6937331e1a..9b950d5a63 100644 --- a/app/helpers/admin/settings_helper.rb +++ b/app/helpers/admin/settings_helper.rb @@ -2,7 +2,7 @@ module Admin::SettingsHelper def captcha_available? - ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present? + Rails.configuration.x.captcha.secret_key.present? && Rails.configuration.x.captcha.site_key.present? end def login_activity_title(activity) diff --git a/app/helpers/admin/trends/statuses_helper.rb b/app/helpers/admin/trends/statuses_helper.rb index c7a59660cf..33da1f7216 100644 --- a/app/helpers/admin/trends/statuses_helper.rb +++ b/app/helpers/admin/trends/statuses_helper.rb @@ -2,11 +2,18 @@ module Admin::Trends::StatusesHelper def one_line_preview(status) - text = if status.local? - status.text.split("\n").first - else - Nokogiri::HTML5(status.text).css('html > body > *').first&.text - end + text = begin + if status.local? + status.text.split("\n").first + else + Nokogiri::HTML5(status.text).css('html > body > *').first&.text + end + rescue ArgumentError + # This can happen if one of the Nokogumbo limits is encountered + # Unfortunately, it does not use a more precise error class + # nor allows more graceful handling + '' + end return '' if text.blank? diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 8b7fbde207..0d7d0e8117 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -122,22 +122,6 @@ module ApplicationHelper inline_svg_tag 'check.svg' end - def visibility_icon(status) - if status.public_visibility? - material_symbol('globe', title: I18n.t('statuses.visibilities.public')) - elsif status.unlisted_visibility? - material_symbol('lock_open', title: I18n.t('statuses.visibilities.unlisted')) - elsif status.public_unlisted_visibility? - material_symbol('cloud', title: I18n.t('statuses.visibilities.public_unlisted')) - elsif status.login_visibility? - material_symbol('key', title: I18n.t('statuses.visibilities.login')) - elsif status.private_visibility? || status.limited_visibility? - material_symbol('lock', title: I18n.t('statuses.visibilities.private')) - elsif status.direct_visibility? - material_symbol('alternate_email', title: I18n.t('statuses.visibilities.direct')) - end - end - def interrelationships_icon(relationships, account_id) if relationships.following[account_id] && relationships.followed_by[account_id] material_symbol('sync_alt', title: I18n.t('relationships.mutual'), class: 'active passive') @@ -161,10 +145,11 @@ module ApplicationHelper end def body_classes - output = body_class_string.split + output = [] output << content_for(:body_classes) output << "theme-#{current_theme.parameterize}" output << 'system-font' if current_account&.user&.setting_system_font_ui + output << 'custom-scrollbars' unless current_account&.user&.setting_system_scrollbars_ui output << (current_account&.user&.setting_reduce_motion ? 'reduce-motion' : 'no-reduce-motion') output << 'rtl' if locale_direction == 'rtl' output << "content-font-size__#{current_account&.user&.setting_content_font_size}" @@ -251,6 +236,12 @@ module ApplicationHelper full_asset_url(instance_presenter.mascot&.file&.url || frontend_asset_path('images/elephant_ui_plane.svg')) end + def server_css? + return true if current_account&.user.nil? + + current_account.user.setting_use_server_css + end + def user_custom_css? return false if current_account&.user.nil? @@ -272,6 +263,14 @@ module ApplicationHelper I18n.t 'user_mailer.welcome.hashtags_recent_count', people: number_with_delimiter(people), count: people end + def app_store_url_ios + 'https://apps.apple.com/app/mastodon-for-iphone-and-ipad/id1571998974' + end + + def app_store_url_android + 'https://play.google.com/store/apps/details?id=org.joinmastodon.android' + end + private def storage_host_var diff --git a/app/helpers/context_helper.rb b/app/helpers/context_helper.rb index 03ca88670f..077c5272a5 100644 --- a/app/helpers/context_helper.rb +++ b/app/helpers/context_helper.rb @@ -34,6 +34,7 @@ module ContextHelper license: { 'schema' => 'http://schema.org#', 'license' => 'schema:license' }, suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' }, attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } }, + misskey_license: { 'misskey' => 'https://misskey-hub.net/ns#', '_misskey_license' => 'misskey:_misskey_license' }, }.freeze def full_context diff --git a/app/helpers/formatting_helper.rb b/app/helpers/formatting_helper.rb index f0faf69b43..dc7442ac33 100644 --- a/app/helpers/formatting_helper.rb +++ b/app/helpers/formatting_helper.rb @@ -1,6 +1,14 @@ # frozen_string_literal: true module FormattingHelper + SYNDICATED_EMOJI_STYLES = <<~CSS.squish + height: 1.1em; + margin: -.2ex .15em .2ex; + object-fit: contain; + vertical-align: middle; + width: 1.1em; + CSS + def html_aware_format(text, local, options = {}) HtmlAwareFormatter.new(text, local, options).to_s end @@ -23,46 +31,33 @@ module FormattingHelper end def status_content_format(status) - html_aware_format(status.text, status.local?, markdown: status.markdown, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : [])) - end + MastodonOTELTracer.in_span('HtmlAwareFormatter rendering') do |span| + span.add_attributes( + 'app.formatter.content.type' => 'status', + 'app.formatter.content.origin' => status.local? ? 'local' : 'remote' + ) - def emoji_name_format(emoji_reaction, status) - html_aware_format(emoji_reaction['url'].present? ? ":#{emoji_reaction['name']}:" : emoji_reaction['name'], status.local?, markdown: status.markdown) + html_aware_format(status.text, status.local?, markdown: status.markdown, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : [])) + end end def rss_status_content_format(status) - html = status_content_format(status) - - 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 - - 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]), + wrapped_status_content_format(status), status.emojis, - style: 'min-width: 1.1em; height: 1.1em; object-fit: contain; vertical-align: middle; margin: -.2ex .15em .2ex' + style: SYNDICATED_EMOJI_STYLES ).to_str end def account_bio_format(account) - html_aware_format(account.note, account.local?, markdown: account.user&.setting_bio_markdown) + MastodonOTELTracer.in_span('HtmlAwareFormatter rendering') do |span| + span.add_attributes( + 'app.formatter.content.type' => 'account_bio', + 'app.formatter.content.origin' => account.local? ? 'local' : 'remote' + ) + + html_aware_format(account.note, account.local?, markdown: account.user&.setting_bio_markdown) + end end def account_field_value_format(field, with_rel_me: true) @@ -72,4 +67,51 @@ module FormattingHelper html_aware_format(field.value, field.account.local?, markdown: false, with_rel_me: with_rel_me, with_domains: true, multiline: false) end end + + def markdown(text) + Redcarpet::Markdown.new(Redcarpet::Render::HTML, escape_html: true, no_images: true).render(text).html_safe # rubocop:disable Rails/OutputSafety + end + + private + + def wrapped_status_content_format(status) + safe_join [ + rss_content_preroll(status), + status_content_format(status), + rss_content_postroll(status), + ] + end + + def rss_content_preroll(status) + if status.spoiler_text? + safe_join [ + tag.p { spoiler_with_warning(status) }, + tag.hr, + ] + end + end + + def spoiler_with_warning(status) + safe_join [ + tag.strong { I18n.t('rss.content_warning', locale: available_locale_or_nil(status.language) || I18n.default_locale) }, + status.spoiler_text, + ] + end + + def rss_content_postroll(status) + if status.preloadable_poll + tag.p do + poll_option_tags(status) + end + end + end + + def poll_option_tags(status) + safe_join( + status.preloadable_poll.options.map do |option| + tag.send(status.preloadable_poll.multiple? ? 'checkbox' : 'radio', option, disabled: true) + end, + tag.br + ) + end end diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/json_ld_helper.rb similarity index 84% rename from app/helpers/jsonld_helper.rb rename to app/helpers/json_ld_helper.rb index 2a5c2d8826..693cdf730f 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/json_ld_helper.rb @@ -163,24 +163,49 @@ module JsonLdHelper end end - def fetch_resource(uri, id_is_known, on_behalf_of = nil, request_options: {}) + # Fetch the resource given by uri. + # @param uri [String] + # @param id_is_known [Boolean] + # @param on_behalf_of [nil, Account] + # @param raise_on_error [Symbol<:all, :temporary, :none>] See {#fetch_resource_without_id_validation} for possible values + def fetch_resource(uri, id_is_known, on_behalf_of = nil, raise_on_error: :none, request_options: {}) unless id_is_known - json = fetch_resource_without_id_validation(uri, on_behalf_of) + json = fetch_resource_without_id_validation(uri, on_behalf_of, raise_on_error: raise_on_error) return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id']) uri = json['id'] end - json = fetch_resource_without_id_validation(uri, on_behalf_of, request_options: request_options) + json = fetch_resource_without_id_validation(uri, on_behalf_of, raise_on_error: raise_on_error, request_options: request_options) json.present? && json['id'] == uri ? json : nil end - def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false, request_options: {}) + # Fetch the resource given by uri + # + # If an error is raised, it contains the response and can be captured for handling like + # + # begin + # fetch_resource_without_id_validation(uri, nil, true) + # rescue Mastodon::UnexpectedResponseError => e + # e.response + # end + # + # @param uri [String] + # @param on_behalf_of [nil, Account] + # @param raise_on_error [Symbol<:all, :temporary, :none>] + # - +:all+ - raise if response code is not in the 2xx range + # - +:temporary+ - raise if the response code is not an "unsalvageable error" like a 404 + # (see {#response_error_unsalvageable} ) + # - +:none+ - do not raise, return +nil+ + def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_error: :none, request_options: {}) on_behalf_of ||= Account.representative build_request(uri, on_behalf_of, options: request_options).perform do |response| - raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error + raise Mastodon::UnexpectedResponseError, response if !response_successful?(response) && ( + raise_on_error == :all || + (!response_error_unsalvageable?(response) && raise_on_error == :temporary) + ) body_to_json(response.body_with_limit) if response.code == 200 && valid_activitypub_content_type?(response) end diff --git a/app/helpers/kmyblue_capabilities_helper.rb b/app/helpers/kmyblue_capabilities_helper.rb index 075869ee60..279505bec8 100644 --- a/app/helpers/kmyblue_capabilities_helper.rb +++ b/app/helpers/kmyblue_capabilities_helper.rb @@ -20,6 +20,8 @@ module KmyblueCapabilitiesHelper kmyblue_circle_history kmyblue_list_notification kmyblue_server_features + favourite_list + kmyblue_favourite_antenna ) capabilities << :full_text_search if Chewy.enabled? diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb index c0c8001eaa..0a8ebcde54 100644 --- a/app/helpers/languages_helper.rb +++ b/app/helpers/languages_helper.rb @@ -193,6 +193,7 @@ module LanguagesHelper ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze, cnr: ['Montenegrin', 'crnogorski'].freeze, csb: ['Kashubian', 'Kaszëbsczi'].freeze, + gsw: ['Swiss German', 'Schwiizertütsch'].freeze, jbo: ['Lojban', 'la .lojban.'].freeze, kab: ['Kabyle', 'Taqbaylit'].freeze, ldn: ['Láadan', 'Láadan'].freeze, diff --git a/app/helpers/media_component_helper.rb b/app/helpers/media_component_helper.rb index 60ccdd0835..269566528a 100644 --- a/app/helpers/media_component_helper.rb +++ b/app/helpers/media_component_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module MediaComponentHelper - def render_video_component(status, **options) + def render_video_component(status, **) video = status.ordered_media_attachments.first meta = video.file.meta || {} @@ -18,14 +18,14 @@ module MediaComponentHelper media: [ serialize_media_attachment(video), ].as_json, - }.merge(**options) + }.merge(**) 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) + def render_audio_component(status, **) audio = status.ordered_media_attachments.first meta = audio.file.meta || {} @@ -38,19 +38,19 @@ module MediaComponentHelper foregroundColor: meta.dig('colors', 'foreground'), accentColor: meta.dig('colors', 'accent'), duration: meta.dig('original', 'duration'), - }.merge(**options) + }.merge(**) 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) + def render_media_gallery_component(status, **) 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) + }.merge(**) react_component :media_gallery, component_params do render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } diff --git a/app/helpers/registration_helper.rb b/app/helpers/registration_helper.rb index c3db46c027..f387a48309 100644 --- a/app/helpers/registration_helper.rb +++ b/app/helpers/registration_helper.rb @@ -18,6 +18,6 @@ module RegistrationHelper end def ip_blocked?(remote_ip) - IpBlock.where(severity: :sign_up_block).exists?(['ip >>= ?', remote_ip.to_s]) + IpBlock.severity_sign_up_block.containing(remote_ip.to_s).exists? end end diff --git a/app/helpers/routing_helper.rb b/app/helpers/routing_helper.rb index 15d988f64d..22efc5f092 100644 --- a/app/helpers/routing_helper.rb +++ b/app/helpers/routing_helper.rb @@ -14,8 +14,8 @@ module RoutingHelper end end - def full_asset_url(source, **options) - source = ActionController::Base.helpers.asset_url(source, **options) unless use_storage? + def full_asset_url(source, **) + source = ActionController::Base.helpers.asset_url(source, **) unless use_storage? URI.join(asset_host, source).to_s end @@ -24,12 +24,12 @@ module RoutingHelper Rails.configuration.action_controller.asset_host || root_url end - def frontend_asset_path(source, **options) - asset_pack_path("media/#{source}", **options) + def frontend_asset_path(source, **) + asset_pack_path("media/#{source}", **) end - def frontend_asset_url(source, **options) - full_asset_url(frontend_asset_path(source, **options)) + def frontend_asset_url(source, **) + full_asset_url(frontend_asset_path(source, **)) end def use_storage? diff --git a/app/helpers/self_destruct_helper.rb b/app/helpers/self_destruct_helper.rb index 78557c25e5..f1927b1e04 100644 --- a/app/helpers/self_destruct_helper.rb +++ b/app/helpers/self_destruct_helper.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true module SelfDestructHelper + VERIFY_PURPOSE = 'self-destruct' + def self.self_destruct? - value = ENV.fetch('SELF_DESTRUCT', nil) - value.present? && Rails.application.message_verifier('self-destruct').verify(value) == ENV['LOCAL_DOMAIN'] + value = Rails.configuration.x.mastodon.self_destruct_value + value.present? && Rails.application.message_verifier(VERIFY_PURPOSE).verify(value) == ENV['LOCAL_DOMAIN'] rescue ActiveSupport::MessageVerifier::InvalidSignature false end diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index 938fcf5b53..7e42dd4623 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true module StatusesHelper - EMBEDDED_CONTROLLER = 'statuses' - EMBEDDED_ACTION = 'embed' - VISIBLITY_ICONS = { public: 'globe', unlisted: 'lock_open', @@ -15,7 +12,7 @@ module StatusesHelper }.freeze def nothing_here(extra_classes = '') - content_tag(:div, class: "nothing-here #{extra_classes}") do + tag.div(class: ['nothing-here', extra_classes]) do t('accounts.nothing_here') end end @@ -63,18 +60,10 @@ module StatusesHelper components.compact_blank.join("\n\n") end - def stream_link_target - embedded_view? ? '_blank' : nil - end - def visibility_icon(status) VISIBLITY_ICONS[status.visibility.to_sym] end - def embedded_view? - params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION - end - def prefers_autoplay? ActiveModel::Type::Boolean.new.cast(params[:autoplay]) || current_user&.setting_auto_play_gif end diff --git a/app/helpers/theme_helper.rb b/app/helpers/theme_helper.rb index fab899a533..f4d88a1ef0 100644 --- a/app/helpers/theme_helper.rb +++ b/app/helpers/theme_helper.rb @@ -23,8 +23,51 @@ module ThemeHelper end end + def custom_stylesheet + if active_custom_stylesheet.present? + stylesheet_link_tag( + custom_css_path(active_custom_stylesheet), + host: root_url, + media: :all, + skip_pipeline: true + ) + end + end + + def system_stylesheet + stylesheet_link_tag( + system_css_path, + host: root_url, + media: :all, + skip_pipeline: true + ) + end + + def user_custom_stylesheet + stylesheet_link_tag( + user_custom_css_path({ version: user_custom_css_version }), + host: root_url, + media: :all, + skip_pipeline: true + ) + end + private + def active_custom_stylesheet + if cached_custom_css_digest.present? + [:custom, cached_custom_css_digest.to_s.first(8)] + .compact_blank + .join('-') + end + end + + def cached_custom_css_digest + Rails.cache.fetch(:setting_digest_custom_css) do + Setting.custom_css&.then { |content| Digest::SHA256.hexdigest(content) } + end + end + def theme_color_for(theme) theme == 'mastodon-light' ? Themes::THEME_COLORS[:light] : Themes::THEME_COLORS[:dark] end diff --git a/app/inputs/date_of_birth_input.rb b/app/inputs/date_of_birth_input.rb new file mode 100644 index 0000000000..131234b02e --- /dev/null +++ b/app/inputs/date_of_birth_input.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class DateOfBirthInput < SimpleForm::Inputs::Base + OPTIONS = [ + { autocomplete: 'bday-day', maxlength: 2, pattern: '[0-9]+', placeholder: 'DD' }.freeze, + { autocomplete: 'bday-month', maxlength: 2, pattern: '[0-9]+', placeholder: 'MM' }.freeze, + { autocomplete: 'bday-year', maxlength: 4, pattern: '[0-9]+', placeholder: 'YYYY' }.freeze, + ].freeze + + def input(wrapper_options = nil) + merged_input_options = merge_wrapper_options(input_html_options, wrapper_options) + merged_input_options[:inputmode] = 'numeric' + + values = (object.public_send(attribute_name) || '').split('.') + + safe_join(Array.new(3) do |index| + options = merged_input_options.merge(OPTIONS[index]).merge id: generate_id(index), 'aria-label': I18n.t("simple_form.labels.user.date_of_birth_#{index + 1}i"), value: values[index] + @builder.text_field("#{attribute_name}(#{index + 1}i)", options) + end) + end + + def label_target + "#{attribute_name}_1i" + end + + private + + def generate_id(index) + "#{object_name}_#{attribute_name}_#{index + 1}i" + end +end diff --git a/app/javascript/entrypoints/embed.tsx b/app/javascript/entrypoints/embed.tsx index f8c824d287..6c091e4d07 100644 --- a/app/javascript/entrypoints/embed.tsx +++ b/app/javascript/entrypoints/embed.tsx @@ -1,7 +1,7 @@ import './public-path'; import { createRoot } from 'react-dom/client'; -import { afterInitialRender } from 'mastodon/../hooks/useRenderSignal'; +import { afterInitialRender } from 'mastodon/hooks/useRenderSignal'; import { start } from '../mastodon/common'; import { Status } from '../mastodon/features/standalone/status'; @@ -60,6 +60,10 @@ window.addEventListener('message', (e) => { const data = e.data; + // Only set overflow to `hidden` once we got the expected `message` so the post can still be scrolled if + // embedded without parent Javascript support + document.body.style.overflow = 'hidden'; + // We use a timeout to allow for the React page to render before calculating the height afterInitialRender(() => { window.parent.postMessage( diff --git a/app/javascript/entrypoints/public.tsx b/app/javascript/entrypoints/public.tsx index d33e00d5da..9374d6b2d1 100644 --- a/app/javascript/entrypoints/public.tsx +++ b/app/javascript/entrypoints/public.tsx @@ -68,7 +68,7 @@ function loaded() { if (id) message = localeData[id]; - if (!message) message = defaultMessage as string; + message ??= defaultMessage as string; const messageFormat = new IntlMessageFormat(message, locale); return messageFormat.format(values) as string; @@ -119,7 +119,11 @@ function loaded() { formattedContent = dateFormat.format(datetime); } - content.title = formattedContent; + const timeGiven = content.dateTime.includes('T'); + content.title = timeGiven + ? dateTimeFormat.format(datetime) + : dateFormat.format(datetime); + content.textContent = formattedContent; }); @@ -230,62 +234,6 @@ function loaded() { } }, ); - - Rails.delegate( - document, - 'button.status__content__spoiler-link', - 'click', - function () { - if (!(this instanceof HTMLButtonElement)) return; - - const statusEl = this.parentNode?.parentNode; - - if ( - !( - statusEl instanceof HTMLDivElement && - statusEl.classList.contains('.status__content') - ) - ) - return; - - if (statusEl.dataset.spoiler === 'expanded') { - statusEl.dataset.spoiler = 'folded'; - this.textContent = new IntlMessageFormat( - localeData['status.show_more'] ?? 'Show more', - locale, - ).format() as string; - } else { - statusEl.dataset.spoiler = 'expanded'; - this.textContent = new IntlMessageFormat( - localeData['status.show_less'] ?? 'Show less', - locale, - ).format() as string; - } - }, - ); - - document - .querySelectorAll('button.status__content__spoiler-link') - .forEach((spoilerLink) => { - const statusEl = spoilerLink.parentNode?.parentNode; - - if ( - !( - statusEl instanceof HTMLDivElement && - statusEl.classList.contains('.status__content') - ) - ) - return; - - const message = - statusEl.dataset.spoiler === 'expanded' - ? (localeData['status.show_less'] ?? 'Show less') - : (localeData['status.show_more'] ?? 'Show more'); - spoilerLink.textContent = new IntlMessageFormat( - message, - locale, - ).format() as string; - }); } Rails.delegate( @@ -327,31 +275,24 @@ Rails.delegate(document, '.input-copy button', 'click', ({ target }) => { if (!input) return; - const oldReadOnly = input.readOnly; - - input.readOnly = false; - input.focus(); - input.select(); - input.setSelectionRange(0, input.value.length); - - try { - if (document.execCommand('copy')) { - input.blur(); - + navigator.clipboard + .writeText(input.value) + .then(() => { const parent = target.parentElement; - if (!parent) return; - parent.classList.add('copied'); + if (parent) { + parent.classList.add('copied'); - setTimeout(() => { - parent.classList.remove('copied'); - }, 700); - } - } catch (err) { - console.error(err); - } + setTimeout(() => { + parent.classList.remove('copied'); + }, 700); + } - input.readOnly = oldReadOnly; + return true; + }) + .catch((error: unknown) => { + console.error(error); + }); }); const toggleSidebar = () => { @@ -446,6 +387,24 @@ Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => { }); }); +Rails.delegate(document, '.rules-list button', 'click', ({ target }) => { + if (!(target instanceof HTMLElement)) { + return; + } + + const button = target.closest('button'); + + if (!button) { + return; + } + + if (button.ariaExpanded === 'true') { + button.ariaExpanded = 'false'; + } else { + button.ariaExpanded = 'true'; + } +}); + function main() { ready(loaded).catch((error: unknown) => { console.error(error); diff --git a/app/javascript/icons/android-chrome-144x144.png b/app/javascript/icons/android-chrome-144x144.png old mode 100755 new mode 100644 index d636e94c43..698fb4a260 Binary files a/app/javascript/icons/android-chrome-144x144.png and b/app/javascript/icons/android-chrome-144x144.png differ diff --git a/app/javascript/icons/android-chrome-192x192.png b/app/javascript/icons/android-chrome-192x192.png old mode 100755 new mode 100644 index 4a2681ffb9..2b6b632648 Binary files a/app/javascript/icons/android-chrome-192x192.png and b/app/javascript/icons/android-chrome-192x192.png differ diff --git a/app/javascript/icons/android-chrome-256x256.png b/app/javascript/icons/android-chrome-256x256.png old mode 100755 new mode 100644 index 8fab493ede..51e3849a26 Binary files a/app/javascript/icons/android-chrome-256x256.png and b/app/javascript/icons/android-chrome-256x256.png differ diff --git a/app/javascript/icons/android-chrome-36x36.png b/app/javascript/icons/android-chrome-36x36.png old mode 100755 new mode 100644 index 335d012db1..925f69c4fc Binary files a/app/javascript/icons/android-chrome-36x36.png and b/app/javascript/icons/android-chrome-36x36.png differ diff --git a/app/javascript/icons/android-chrome-384x384.png b/app/javascript/icons/android-chrome-384x384.png old mode 100755 new mode 100644 index 02b1e6fced..9d256a83cb Binary files a/app/javascript/icons/android-chrome-384x384.png and b/app/javascript/icons/android-chrome-384x384.png differ diff --git a/app/javascript/icons/android-chrome-48x48.png b/app/javascript/icons/android-chrome-48x48.png old mode 100755 new mode 100644 index 43cf411b8c..bcfe7475d0 Binary files a/app/javascript/icons/android-chrome-48x48.png and b/app/javascript/icons/android-chrome-48x48.png differ diff --git a/app/javascript/icons/android-chrome-512x512.png b/app/javascript/icons/android-chrome-512x512.png old mode 100755 new mode 100644 index 1856b80c7c..bffacfb699 Binary files a/app/javascript/icons/android-chrome-512x512.png and b/app/javascript/icons/android-chrome-512x512.png differ diff --git a/app/javascript/icons/android-chrome-72x72.png b/app/javascript/icons/android-chrome-72x72.png old mode 100755 new mode 100644 index 335008bf85..16679d5731 Binary files a/app/javascript/icons/android-chrome-72x72.png and b/app/javascript/icons/android-chrome-72x72.png differ diff --git a/app/javascript/icons/android-chrome-96x96.png b/app/javascript/icons/android-chrome-96x96.png old mode 100755 new mode 100644 index d1cb095822..9ade87cf32 Binary files a/app/javascript/icons/android-chrome-96x96.png and b/app/javascript/icons/android-chrome-96x96.png differ diff --git a/app/javascript/icons/apple-touch-icon-1024x1024.png b/app/javascript/icons/apple-touch-icon-1024x1024.png old mode 100755 new mode 100644 index c2a2d516ef..8ec371eb27 Binary files a/app/javascript/icons/apple-touch-icon-1024x1024.png and b/app/javascript/icons/apple-touch-icon-1024x1024.png differ diff --git a/app/javascript/icons/apple-touch-icon-114x114.png b/app/javascript/icons/apple-touch-icon-114x114.png old mode 100755 new mode 100644 index 218b415439..e1563f51e5 Binary files a/app/javascript/icons/apple-touch-icon-114x114.png and b/app/javascript/icons/apple-touch-icon-114x114.png differ diff --git a/app/javascript/icons/apple-touch-icon-120x120.png b/app/javascript/icons/apple-touch-icon-120x120.png old mode 100755 new mode 100644 index be53bc7c10..e9a5f5b0e5 Binary files a/app/javascript/icons/apple-touch-icon-120x120.png and b/app/javascript/icons/apple-touch-icon-120x120.png differ diff --git a/app/javascript/icons/apple-touch-icon-144x144.png b/app/javascript/icons/apple-touch-icon-144x144.png old mode 100755 new mode 100644 index cbb055732f..698fb4a260 Binary files a/app/javascript/icons/apple-touch-icon-144x144.png and b/app/javascript/icons/apple-touch-icon-144x144.png differ diff --git a/app/javascript/icons/apple-touch-icon-152x152.png b/app/javascript/icons/apple-touch-icon-152x152.png old mode 100755 new mode 100644 index 3a7975c054..0cc93cc288 Binary files a/app/javascript/icons/apple-touch-icon-152x152.png and b/app/javascript/icons/apple-touch-icon-152x152.png differ diff --git a/app/javascript/icons/apple-touch-icon-167x167.png b/app/javascript/icons/apple-touch-icon-167x167.png old mode 100755 new mode 100644 index 25be4eb5f5..9bbbf53120 Binary files a/app/javascript/icons/apple-touch-icon-167x167.png and b/app/javascript/icons/apple-touch-icon-167x167.png differ diff --git a/app/javascript/icons/apple-touch-icon-180x180.png b/app/javascript/icons/apple-touch-icon-180x180.png old mode 100755 new mode 100644 index dc0e9bc20b..329b803b91 Binary files a/app/javascript/icons/apple-touch-icon-180x180.png and b/app/javascript/icons/apple-touch-icon-180x180.png differ diff --git a/app/javascript/icons/apple-touch-icon-192x192.png b/app/javascript/icons/apple-touch-icon-192x192.png new file mode 100644 index 0000000000..2b6b632648 Binary files /dev/null and b/app/javascript/icons/apple-touch-icon-192x192.png differ diff --git a/app/javascript/icons/apple-touch-icon-256x256.png b/app/javascript/icons/apple-touch-icon-256x256.png new file mode 100644 index 0000000000..51e3849a26 Binary files /dev/null and b/app/javascript/icons/apple-touch-icon-256x256.png differ diff --git a/app/javascript/icons/apple-touch-icon-36x36.png b/app/javascript/icons/apple-touch-icon-36x36.png new file mode 100644 index 0000000000..925f69c4fc Binary files /dev/null and b/app/javascript/icons/apple-touch-icon-36x36.png differ diff --git a/app/javascript/icons/apple-touch-icon-384x384.png b/app/javascript/icons/apple-touch-icon-384x384.png new file mode 100644 index 0000000000..9d256a83cb Binary files /dev/null and b/app/javascript/icons/apple-touch-icon-384x384.png differ diff --git a/app/javascript/icons/apple-touch-icon-48x48.png b/app/javascript/icons/apple-touch-icon-48x48.png new file mode 100644 index 0000000000..bcfe7475d0 Binary files /dev/null and b/app/javascript/icons/apple-touch-icon-48x48.png differ diff --git a/app/javascript/icons/apple-touch-icon-512x512.png b/app/javascript/icons/apple-touch-icon-512x512.png new file mode 100644 index 0000000000..bffacfb699 Binary files /dev/null and b/app/javascript/icons/apple-touch-icon-512x512.png differ diff --git a/app/javascript/icons/apple-touch-icon-57x57.png b/app/javascript/icons/apple-touch-icon-57x57.png old mode 100755 new mode 100644 index bb0dc957cd..e00e142c64 Binary files a/app/javascript/icons/apple-touch-icon-57x57.png and b/app/javascript/icons/apple-touch-icon-57x57.png differ diff --git a/app/javascript/icons/apple-touch-icon-60x60.png b/app/javascript/icons/apple-touch-icon-60x60.png old mode 100755 new mode 100644 index 9143a0bf07..011285b564 Binary files a/app/javascript/icons/apple-touch-icon-60x60.png and b/app/javascript/icons/apple-touch-icon-60x60.png differ diff --git a/app/javascript/icons/apple-touch-icon-72x72.png b/app/javascript/icons/apple-touch-icon-72x72.png old mode 100755 new mode 100644 index 2b7d19484c..16679d5731 Binary files a/app/javascript/icons/apple-touch-icon-72x72.png and b/app/javascript/icons/apple-touch-icon-72x72.png differ diff --git a/app/javascript/icons/apple-touch-icon-76x76.png b/app/javascript/icons/apple-touch-icon-76x76.png old mode 100755 new mode 100644 index 0985e33bcb..83c8748876 Binary files a/app/javascript/icons/apple-touch-icon-76x76.png and b/app/javascript/icons/apple-touch-icon-76x76.png differ diff --git a/app/javascript/icons/apple-touch-icon-96x96.png b/app/javascript/icons/apple-touch-icon-96x96.png new file mode 100644 index 0000000000..9ade87cf32 Binary files /dev/null and b/app/javascript/icons/apple-touch-icon-96x96.png differ diff --git a/app/javascript/icons/favicon-16x16.png b/app/javascript/icons/favicon-16x16.png old mode 100755 new mode 100644 index 1326ba0462..7f865cfe96 Binary files a/app/javascript/icons/favicon-16x16.png and b/app/javascript/icons/favicon-16x16.png differ diff --git a/app/javascript/icons/favicon-32x32.png b/app/javascript/icons/favicon-32x32.png old mode 100755 new mode 100644 index f5058cb0a5..7f865cfe96 Binary files a/app/javascript/icons/favicon-32x32.png and b/app/javascript/icons/favicon-32x32.png differ diff --git a/app/javascript/icons/favicon-48x48.png b/app/javascript/icons/favicon-48x48.png old mode 100755 new mode 100644 index 6253d054c7..7f865cfe96 Binary files a/app/javascript/icons/favicon-48x48.png and b/app/javascript/icons/favicon-48x48.png differ diff --git a/app/javascript/images/archetypes/booster.png b/app/javascript/images/archetypes/booster.png new file mode 100755 index 0000000000..df2a0226f8 Binary files /dev/null and b/app/javascript/images/archetypes/booster.png differ diff --git a/app/javascript/images/archetypes/lurker.png b/app/javascript/images/archetypes/lurker.png new file mode 100755 index 0000000000..e37f98aab2 Binary files /dev/null and b/app/javascript/images/archetypes/lurker.png differ diff --git a/app/javascript/images/archetypes/oracle.png b/app/javascript/images/archetypes/oracle.png new file mode 100755 index 0000000000..9d4e2177c5 Binary files /dev/null and b/app/javascript/images/archetypes/oracle.png differ diff --git a/app/javascript/images/archetypes/pollster.png b/app/javascript/images/archetypes/pollster.png new file mode 100755 index 0000000000..9fe6281af0 Binary files /dev/null and b/app/javascript/images/archetypes/pollster.png differ diff --git a/app/javascript/images/archetypes/replier.png b/app/javascript/images/archetypes/replier.png new file mode 100755 index 0000000000..6c6325b9f1 Binary files /dev/null and b/app/javascript/images/archetypes/replier.png differ diff --git a/app/javascript/images/logo_full.svg b/app/javascript/images/logo_full.svg deleted file mode 100644 index 03bcf93e39..0000000000 --- a/app/javascript/images/logo_full.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/app/javascript/images/logo_transparent.svg b/app/javascript/images/logo_transparent.svg deleted file mode 100644 index a1e7b403e0..0000000000 --- a/app/javascript/images/logo_transparent.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/app/javascript/images/quote.svg b/app/javascript/images/quote.svg new file mode 100644 index 0000000000..ae6fbbe04a --- /dev/null +++ b/app/javascript/images/quote.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/javascript/images/reticle.png b/app/javascript/images/reticle.png deleted file mode 100644 index a724ac0bcd..0000000000 Binary files a/app/javascript/images/reticle.png and /dev/null differ diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index 3d0e8b8c90..d821381ce0 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -142,6 +142,13 @@ export function fetchAccountFail(id, error) { }; } +/** + * @param {string} id + * @param {Object} options + * @param {boolean} [options.reblogs] + * @param {boolean} [options.notify] + * @returns {function(): void} + */ export function followAccount(id, options = { reblogs: true }) { return (dispatch, getState) => { const alreadyFollowing = getState().getIn(['relationships', id, 'following']); diff --git a/app/javascript/mastodon/actions/alerts.js b/app/javascript/mastodon/actions/alerts.js deleted file mode 100644 index 48dee2587f..0000000000 --- a/app/javascript/mastodon/actions/alerts.js +++ /dev/null @@ -1,66 +0,0 @@ -import { defineMessages } from 'react-intl'; - -import { AxiosError } from 'axios'; - -const messages = defineMessages({ - unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' }, - unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' }, - rateLimitedTitle: { id: 'alert.rate_limited.title', defaultMessage: 'Rate limited' }, - rateLimitedMessage: { id: 'alert.rate_limited.message', defaultMessage: 'Please retry after {retry_time, time, medium}.' }, -}); - -export const ALERT_SHOW = 'ALERT_SHOW'; -export const ALERT_DISMISS = 'ALERT_DISMISS'; -export const ALERT_CLEAR = 'ALERT_CLEAR'; -export const ALERT_NOOP = 'ALERT_NOOP'; - -export const dismissAlert = alert => ({ - type: ALERT_DISMISS, - alert, -}); - -export const clearAlert = () => ({ - type: ALERT_CLEAR, -}); - -export const showAlert = alert => ({ - type: ALERT_SHOW, - alert, -}); - -export const showAlertForError = (error, skipNotFound = false) => { - if (error.response) { - const { data, status, statusText, headers } = error.response; - - // Skip these errors as they are reflected in the UI - if (skipNotFound && (status === 404 || status === 410)) { - return { type: ALERT_NOOP }; - } - - // Rate limit errors - if (status === 429 && headers['x-ratelimit-reset']) { - return showAlert({ - title: messages.rateLimitedTitle, - message: messages.rateLimitedMessage, - values: { 'retry_time': new Date(headers['x-ratelimit-reset']) }, - }); - } - - return showAlert({ - title: `${status}`, - message: data.error || statusText, - }); - } - - // An aborted request, e.g. due to reloading the browser window, it not really error - if (error.code === AxiosError.ECONNABORTED) { - return { type: ALERT_NOOP }; - } - - console.error(error); - - return showAlert({ - title: messages.unexpectedTitle, - message: messages.unexpectedMessage, - }); -}; diff --git a/app/javascript/mastodon/actions/alerts.ts b/app/javascript/mastodon/actions/alerts.ts new file mode 100644 index 0000000000..4fd293e252 --- /dev/null +++ b/app/javascript/mastodon/actions/alerts.ts @@ -0,0 +1,76 @@ +import { defineMessages } from 'react-intl'; + +import { createAction } from '@reduxjs/toolkit'; + +import { AxiosError } from 'axios'; +import type { AxiosResponse } from 'axios'; + +import type { Alert } from 'mastodon/models/alert'; + +interface ApiErrorResponse { + error?: string; +} + +const messages = defineMessages({ + unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' }, + unexpectedMessage: { + id: 'alert.unexpected.message', + defaultMessage: 'An unexpected error occurred.', + }, + rateLimitedTitle: { + id: 'alert.rate_limited.title', + defaultMessage: 'Rate limited', + }, + rateLimitedMessage: { + id: 'alert.rate_limited.message', + defaultMessage: 'Please retry after {retry_time, time, medium}.', + }, +}); + +export const dismissAlert = createAction<{ key: number }>('alerts/dismiss'); + +export const clearAlerts = createAction('alerts/clear'); + +export const showAlert = createAction>('alerts/show'); + +const ignoreAlert = createAction('alerts/ignore'); + +export const showAlertForError = (error: unknown, skipNotFound = false) => { + if (error instanceof AxiosError && error.response) { + const { status, statusText, headers } = error.response; + const { data } = error.response as AxiosResponse; + + // Skip these errors as they are reflected in the UI + if (skipNotFound && (status === 404 || status === 410)) { + return ignoreAlert(); + } + + // Rate limit errors + if (status === 429 && headers['x-ratelimit-reset']) { + return showAlert({ + title: messages.rateLimitedTitle, + message: messages.rateLimitedMessage, + values: { + retry_time: new Date(headers['x-ratelimit-reset'] as string), + }, + }); + } + + return showAlert({ + title: `${status}`, + message: data.error ?? statusText, + }); + } + + // An aborted request, e.g. due to reloading the browser window, is not really an error + if (error instanceof AxiosError && error.code === AxiosError.ECONNABORTED) { + return ignoreAlert(); + } + + console.error(error); + + return showAlert({ + title: messages.unexpectedTitle, + message: messages.unexpectedMessage, + }); +}; diff --git a/app/javascript/mastodon/actions/antennas.js b/app/javascript/mastodon/actions/antennas.js index 4716897586..30dc2d42c6 100644 --- a/app/javascript/mastodon/actions/antennas.js +++ b/app/javascript/mastodon/actions/antennas.js @@ -1,8 +1,5 @@ import api from '../api'; -import { showAlertForError } from './alerts'; -import { importFetchedAccounts } from './importer'; - export const ANTENNA_FETCH_REQUEST = 'ANTENNA_FETCH_REQUEST'; export const ANTENNA_FETCH_SUCCESS = 'ANTENNA_FETCH_SUCCESS'; export const ANTENNA_FETCH_FAIL = 'ANTENNA_FETCH_FAIL'; @@ -11,121 +8,10 @@ export const ANTENNAS_FETCH_REQUEST = 'ANTENNAS_FETCH_REQUEST'; export const ANTENNAS_FETCH_SUCCESS = 'ANTENNAS_FETCH_SUCCESS'; export const ANTENNAS_FETCH_FAIL = 'ANTENNAS_FETCH_FAIL'; -export const ANTENNA_EDITOR_TITLE_CHANGE = 'ANTENNA_EDITOR_TITLE_CHANGE'; -export const ANTENNA_EDITOR_RESET = 'ANTENNA_EDITOR_RESET'; -export const ANTENNA_EDITOR_SETUP = 'ANTENNA_EDITOR_SETUP'; - -export const ANTENNA_CREATE_REQUEST = 'ANTENNA_CREATE_REQUEST'; -export const ANTENNA_CREATE_SUCCESS = 'ANTENNA_CREATE_SUCCESS'; -export const ANTENNA_CREATE_FAIL = 'ANTENNA_CREATE_FAIL'; - -export const ANTENNA_UPDATE_REQUEST = 'ANTENNA_UPDATE_REQUEST'; -export const ANTENNA_UPDATE_SUCCESS = 'ANTENNA_UPDATE_SUCCESS'; -export const ANTENNA_UPDATE_FAIL = 'ANTENNA_UPDATE_FAIL'; - export const ANTENNA_DELETE_REQUEST = 'ANTENNA_DELETE_REQUEST'; export const ANTENNA_DELETE_SUCCESS = 'ANTENNA_DELETE_SUCCESS'; export const ANTENNA_DELETE_FAIL = 'ANTENNA_DELETE_FAIL'; -export const ANTENNA_ACCOUNTS_FETCH_REQUEST = 'ANTENNA_ACCOUNTS_FETCH_REQUEST'; -export const ANTENNA_ACCOUNTS_FETCH_SUCCESS = 'ANTENNA_ACCOUNTS_FETCH_SUCCESS'; -export const ANTENNA_ACCOUNTS_FETCH_FAIL = 'ANTENNA_ACCOUNTS_FETCH_FAIL'; - -export const ANTENNA_EDITOR_SUGGESTIONS_CHANGE = 'ANTENNA_EDITOR_SUGGESTIONS_CHANGE'; -export const ANTENNA_EDITOR_SUGGESTIONS_READY = 'ANTENNA_EDITOR_SUGGESTIONS_READY'; -export const ANTENNA_EDITOR_SUGGESTIONS_CLEAR = 'ANTENNA_EDITOR_SUGGESTIONS_CLEAR'; - -export const ANTENNA_EDITOR_ADD_REQUEST = 'ANTENNA_EDITOR_ADD_REQUEST'; -export const ANTENNA_EDITOR_ADD_SUCCESS = 'ANTENNA_EDITOR_ADD_SUCCESS'; -export const ANTENNA_EDITOR_ADD_FAIL = 'ANTENNA_EDITOR_ADD_FAIL'; - -export const ANTENNA_EDITOR_REMOVE_REQUEST = 'ANTENNA_EDITOR_REMOVE_REQUEST'; -export const ANTENNA_EDITOR_REMOVE_SUCCESS = 'ANTENNA_EDITOR_REMOVE_SUCCESS'; -export const ANTENNA_EDITOR_REMOVE_FAIL = 'ANTENNA_EDITOR_REMOVE_FAIL'; - -export const ANTENNA_EXCLUDE_ACCOUNTS_FETCH_REQUEST = 'ANTENNA_EXCLUDE_ACCOUNTS_FETCH_REQUEST'; -export const ANTENNA_EXCLUDE_ACCOUNTS_FETCH_SUCCESS = 'ANTENNA_EXCLUDE_ACCOUNTS_FETCH_SUCCESS'; -export const ANTENNA_EXCLUDE_ACCOUNTS_FETCH_FAIL = 'ANTENNA_EXCLUDE_ACCOUNTS_FETCH_FAIL'; - -export const ANTENNA_EDITOR_ADD_EXCLUDE_REQUEST = 'ANTENNA_EDITOR_ADD_EXCLUDE_REQUEST'; -export const ANTENNA_EDITOR_ADD_EXCLUDE_SUCCESS = 'ANTENNA_EDITOR_ADD_EXCLUDE_SUCCESS'; -export const ANTENNA_EDITOR_ADD_EXCLUDE_FAIL = 'ANTENNA_EDITOR_ADD_EXCLUDE_FAIL'; - -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_REQUEST = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_REQUEST'; -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_SUCCESS = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_SUCCESS'; -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_FAIL = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_FAIL'; - -export const ANTENNA_EDITOR_FETCH_DOMAINS_REQUEST = 'ANTENNA_EDITOR_FETCH_DOMAINS_REQUEST'; -export const ANTENNA_EDITOR_FETCH_DOMAINS_SUCCESS = 'ANTENNA_EDITOR_FETCH_DOMAINS_SUCCESS'; -export const ANTENNA_EDITOR_FETCH_DOMAINS_FAIL = 'ANTENNA_EDITOR_FETCH_DOMAINS_FAIL'; - -export const ANTENNA_EDITOR_ADD_DOMAIN_REQUEST = 'ANTENNA_EDITOR_ADD_DOMAIN_REQUEST'; -export const ANTENNA_EDITOR_ADD_DOMAIN_SUCCESS = 'ANTENNA_EDITOR_ADD_DOMAIN_SUCCESS'; -export const ANTENNA_EDITOR_ADD_DOMAIN_FAIL = 'ANTENNA_EDITOR_ADD_DOMAIN_FAIL'; - -export const ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_REQUEST = 'ANTENNA_EDITOR_ADD_EXCLUDEDOMAIN_REQUEST'; -export const ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_SUCCESS = 'ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_SUCCESS'; -export const ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_FAIL = 'ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_FAIL'; - -export const ANTENNA_EDITOR_REMOVE_DOMAIN_REQUEST = 'ANTENNA_EDITOR_REMOVE_DOMAIN_REQUEST'; -export const ANTENNA_EDITOR_REMOVE_DOMAIN_SUCCESS = 'ANTENNA_EDITOR_REMOVE_DOMAIN_SUCCESS'; -export const ANTENNA_EDITOR_REMOVE_DOMAIN_FAIL = 'ANTENNA_EDITOR_REMOVE_DOMAIN_FAIL'; - -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_REQUEST = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_REQUEST'; -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_SUCCESS = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_SUCCESS'; -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_FAIL = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_FAIL'; - -export const ANTENNA_EDITOR_FETCH_KEYWORDS_REQUEST = 'ANTENNA_EDITOR_FETCH_KEYWORDS_REQUEST'; -export const ANTENNA_EDITOR_FETCH_KEYWORDS_SUCCESS = 'ANTENNA_EDITOR_FETCH_KEYWORDS_SUCCESS'; -export const ANTENNA_EDITOR_FETCH_KEYWORDS_FAIL = 'ANTENNA_EDITOR_FETCH_KEYWORDS_FAIL'; - -export const ANTENNA_EDITOR_ADD_KEYWORD_REQUEST = 'ANTENNA_EDITOR_ADD_KEYWORD_REQUEST'; -export const ANTENNA_EDITOR_ADD_KEYWORD_SUCCESS = 'ANTENNA_EDITOR_ADD_KEYWORD_SUCCESS'; -export const ANTENNA_EDITOR_ADD_KEYWORD_FAIL = 'ANTENNA_EDITOR_ADD_KEYWORD_FAIL'; - -export const ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_REQUEST = 'ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_REQUEST'; -export const ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_SUCCESS = 'ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_SUCCESS'; -export const ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_FAIL = 'ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_FAIL'; - -export const ANTENNA_EDITOR_REMOVE_KEYWORD_REQUEST = 'ANTENNA_EDITOR_REMOVE_KEYWORD_REQUEST'; -export const ANTENNA_EDITOR_REMOVE_KEYWORD_SUCCESS = 'ANTENNA_EDITOR_REMOVE_KEYWORD_SUCCESS'; -export const ANTENNA_EDITOR_REMOVE_KEYWORD_FAIL = 'ANTENNA_EDITOR_REMOVE_KEYWORD_FAIL'; - -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_REQUEST = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_REQUEST'; -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_SUCCESS = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_SUCCESS'; -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_FAIL = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_FAIL'; - -export const ANTENNA_EDITOR_FETCH_TAGS_REQUEST = 'ANTENNA_EDITOR_FETCH_TAGS_REQUEST'; -export const ANTENNA_EDITOR_FETCH_TAGS_SUCCESS = 'ANTENNA_EDITOR_FETCH_TAGS_SUCCESS'; -export const ANTENNA_EDITOR_FETCH_TAGS_FAIL = 'ANTENNA_EDITOR_FETCH_TAGS_FAIL'; - -export const ANTENNA_EDITOR_ADD_TAG_REQUEST = 'ANTENNA_EDITOR_ADD_TAG_REQUEST'; -export const ANTENNA_EDITOR_ADD_TAG_SUCCESS = 'ANTENNA_EDITOR_ADD_TAG_SUCCESS'; -export const ANTENNA_EDITOR_ADD_TAG_FAIL = 'ANTENNA_EDITOR_ADD_TAG_FAIL'; - -export const ANTENNA_EDITOR_ADD_EXCLUDE_TAG_REQUEST = 'ANTENNA_EDITOR_ADD_EXCLUDE_TAG_REQUEST'; -export const ANTENNA_EDITOR_ADD_EXCLUDE_TAG_SUCCESS = 'ANTENNA_EDITOR_ADD_EXCLUDE_TAG_SUCCESS'; -export const ANTENNA_EDITOR_ADD_EXCLUDE_TAG_FAIL = 'ANTENNA_EDITOR_ADD_EXCLUDE_TAG_FAIL'; - -export const ANTENNA_EDITOR_REMOVE_TAG_REQUEST = 'ANTENNA_EDITOR_REMOVE_TAG_REQUEST'; -export const ANTENNA_EDITOR_REMOVE_TAG_SUCCESS = 'ANTENNA_EDITOR_REMOVE_TAG_SUCCESS'; -export const ANTENNA_EDITOR_REMOVE_TAG_FAIL = 'ANTENNA_EDITOR_REMOVE_TAG_FAIL'; - -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_REQUEST = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_REQUEST'; -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_SUCCESS = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_SUCCESS'; -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_FAIL = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_FAIL'; - -export const ANTENNA_ADDER_RESET = 'ANTENNA_ADDER_RESET'; -export const ANTENNA_ADDER_SETUP = 'ANTENNA_ADDER_SETUP'; - -export const ANTENNA_ADDER_ANTENNAS_FETCH_REQUEST = 'ANTENNA_ADDER_ANTENNAS_FETCH_REQUEST'; -export const ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS = 'ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS'; -export const ANTENNA_ADDER_ANTENNAS_FETCH_FAIL = 'ANTENNA_ADDER_ANTENNAS_FETCH_FAIL'; - -export const ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_REQUEST = 'ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_REQUEST'; -export const ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_SUCCESS = 'ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_SUCCESS'; -export const ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_FAIL = 'ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_FAIL'; - export const fetchAntenna = id => (dispatch, getState) => { if (getState().getIn(['antennas', id])) { return; @@ -176,98 +62,6 @@ export const fetchAntennasFail = error => ({ error, }); -export const submitAntennaEditor = shouldReset => (dispatch, getState) => { - const antennaId = getState().getIn(['antennaEditor', 'antennaId']); - const title = getState().getIn(['antennaEditor', 'title']); - - if (antennaId === null) { - dispatch(createAntenna(title, shouldReset)); - } else { - dispatch(updateAntenna(antennaId, title, shouldReset)); - } -}; - -export const setupAntennaEditor = antennaId => (dispatch, getState) => { - dispatch({ - type: ANTENNA_EDITOR_SETUP, - antenna: getState().getIn(['antennas', antennaId]), - }); - - dispatch(fetchAntennaAccounts(antennaId)); -}; - -export const setupExcludeAntennaEditor = antennaId => (dispatch, getState) => { - dispatch({ - type: ANTENNA_EDITOR_SETUP, - antenna: getState().getIn(['antennas', antennaId]), - }); - - dispatch(fetchAntennaExcludeAccounts(antennaId)); -}; - -export const changeAntennaEditorTitle = value => ({ - type: ANTENNA_EDITOR_TITLE_CHANGE, - value, -}); - -export const createAntenna = (title, shouldReset) => (dispatch, getState) => { - dispatch(createAntennaRequest()); - - api(getState).post('/api/v1/antennas', { title }).then(({ data }) => { - dispatch(createAntennaSuccess(data)); - - if (shouldReset) { - dispatch(resetAntennaEditor()); - } - }).catch(err => dispatch(createAntennaFail(err))); -}; - -export const createAntennaRequest = () => ({ - type: ANTENNA_CREATE_REQUEST, -}); - -export const createAntennaSuccess = antenna => ({ - type: ANTENNA_CREATE_SUCCESS, - antenna, -}); - -export const createAntennaFail = error => ({ - type: ANTENNA_CREATE_FAIL, - error, -}); - -export const updateAntenna = (id, title, shouldReset, list_id, stl, ltl, with_media_only, ignore_reblog, insert_feeds) => (dispatch, getState) => { - dispatch(updateAntennaRequest(id)); - - api(getState).put(`/api/v1/antennas/${id}`, { title, list_id, stl, ltl, with_media_only, ignore_reblog, insert_feeds }).then(({ data }) => { - dispatch(updateAntennaSuccess(data)); - - if (shouldReset) { - dispatch(resetAntennaEditor()); - } - }).catch(err => dispatch(updateAntennaFail(id, err))); -}; - -export const updateAntennaRequest = id => ({ - type: ANTENNA_UPDATE_REQUEST, - id, -}); - -export const updateAntennaSuccess = antenna => ({ - type: ANTENNA_UPDATE_SUCCESS, - antenna, -}); - -export const updateAntennaFail = (id, error) => ({ - type: ANTENNA_UPDATE_FAIL, - id, - error, -}); - -export const resetAntennaEditor = () => ({ - type: ANTENNA_EDITOR_RESET, -}); - export const deleteAntenna = id => (dispatch, getState) => { dispatch(deleteAntennaRequest(id)); @@ -291,696 +85,3 @@ export const deleteAntennaFail = (id, error) => ({ id, error, }); - -export const fetchAntennaAccounts = antennaId => (dispatch, getState) => { - dispatch(fetchAntennaAccountsRequest(antennaId)); - - api(getState).get(`/api/v1/antennas/${antennaId}/accounts`, { params: { limit: 0 } }).then(({ data }) => { - dispatch(importFetchedAccounts(data)); - dispatch(fetchAntennaAccountsSuccess(antennaId, data)); - }).catch(err => dispatch(fetchAntennaAccountsFail(antennaId, err))); -}; - -export const fetchAntennaAccountsRequest = id => ({ - type: ANTENNA_ACCOUNTS_FETCH_REQUEST, - id, -}); - -export const fetchAntennaAccountsSuccess = (id, accounts, next) => ({ - type: ANTENNA_ACCOUNTS_FETCH_SUCCESS, - id, - accounts, - next, -}); - -export const fetchAntennaAccountsFail = (id, error) => ({ - type: ANTENNA_ACCOUNTS_FETCH_FAIL, - id, - error, -}); - -export const fetchAntennaExcludeAccounts = antennaId => (dispatch, getState) => { - dispatch(fetchAntennaExcludeAccountsRequest(antennaId)); - - api(getState).get(`/api/v1/antennas/${antennaId}/exclude_accounts`, { params: { limit: 0 } }).then(({ data }) => { - dispatch(importFetchedAccounts(data)); - dispatch(fetchAntennaExcludeAccountsSuccess(antennaId, data)); - }).catch(err => dispatch(fetchAntennaExcludeAccountsFail(antennaId, err))); -}; - -export const fetchAntennaExcludeAccountsRequest = id => ({ - type: ANTENNA_EXCLUDE_ACCOUNTS_FETCH_REQUEST, - id, -}); - -export const fetchAntennaExcludeAccountsSuccess = (id, accounts, next) => ({ - type: ANTENNA_EXCLUDE_ACCOUNTS_FETCH_SUCCESS, - id, - accounts, - next, -}); - -export const fetchAntennaExcludeAccountsFail = (id, error) => ({ - type: ANTENNA_EXCLUDE_ACCOUNTS_FETCH_FAIL, - id, - error, -}); - -export const fetchAntennaSuggestions = q => (dispatch, getState) => { - const params = { - q, - resolve: false, - }; - - api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => { - dispatch(importFetchedAccounts(data)); - dispatch(fetchAntennaSuggestionsReady(q, data)); - }).catch(error => dispatch(showAlertForError(error))); -}; - -export const fetchAntennaSuggestionsReady = (query, accounts) => ({ - type: ANTENNA_EDITOR_SUGGESTIONS_READY, - query, - accounts, -}); - -export const clearAntennaSuggestions = () => ({ - type: ANTENNA_EDITOR_SUGGESTIONS_CLEAR, -}); - -export const changeAntennaSuggestions = value => ({ - type: ANTENNA_EDITOR_SUGGESTIONS_CHANGE, - value, -}); - -export const addToAntennaEditor = accountId => (dispatch, getState) => { - dispatch(addToAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId)); -}; - -export const addToAntenna = (antennaId, accountId) => (dispatch, getState) => { - dispatch(addToAntennaRequest(antennaId, accountId)); - - api(getState).post(`/api/v1/antennas/${antennaId}/accounts`, { account_ids: [accountId] }) - .then(() => dispatch(addToAntennaSuccess(antennaId, accountId))) - .catch(err => dispatch(addToAntennaFail(antennaId, accountId, err))); -}; - -export const addToAntennaRequest = (antennaId, accountId) => ({ - type: ANTENNA_EDITOR_ADD_REQUEST, - antennaId, - accountId, -}); - -export const addToAntennaSuccess = (antennaId, accountId) => ({ - type: ANTENNA_EDITOR_ADD_SUCCESS, - antennaId, - accountId, -}); - -export const addToAntennaFail = (antennaId, accountId, error) => ({ - type: ANTENNA_EDITOR_ADD_FAIL, - antennaId, - accountId, - error, -}); - -export const addExcludeToAntennaEditor = accountId => (dispatch, getState) => { - dispatch(addExcludeToAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId)); -}; - -export const addExcludeToAntenna = (antennaId, accountId) => (dispatch, getState) => { - dispatch(addExcludeToAntennaRequest(antennaId, accountId)); - - api(getState).post(`/api/v1/antennas/${antennaId}/exclude_accounts`, { account_ids: [accountId] }) - .then(() => dispatch(addExcludeToAntennaSuccess(antennaId, accountId))) - .catch(err => dispatch(addExcludeToAntennaFail(antennaId, accountId, err))); -}; - -export const addExcludeToAntennaRequest = (antennaId, accountId) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_REQUEST, - antennaId, - accountId, -}); - -export const addExcludeToAntennaSuccess = (antennaId, accountId) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_SUCCESS, - antennaId, - accountId, -}); - -export const addExcludeToAntennaFail = (antennaId, accountId, error) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_FAIL, - antennaId, - accountId, - error, -}); - -export const removeFromAntennaEditor = accountId => (dispatch, getState) => { - dispatch(removeFromAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId)); -}; - -export const removeFromAntenna = (antennaId, accountId) => (dispatch, getState) => { - dispatch(removeFromAntennaRequest(antennaId, accountId)); - - api(getState).delete(`/api/v1/antennas/${antennaId}/accounts`, { params: { account_ids: [accountId] } }) - .then(() => dispatch(removeFromAntennaSuccess(antennaId, accountId))) - .catch(err => dispatch(removeFromAntennaFail(antennaId, accountId, err))); -}; - -export const removeFromAntennaRequest = (antennaId, accountId) => ({ - type: ANTENNA_EDITOR_REMOVE_REQUEST, - antennaId, - accountId, -}); - -export const removeFromAntennaSuccess = (antennaId, accountId) => ({ - type: ANTENNA_EDITOR_REMOVE_SUCCESS, - antennaId, - accountId, -}); - -export const removeFromAntennaFail = (antennaId, accountId, error) => ({ - type: ANTENNA_EDITOR_REMOVE_FAIL, - antennaId, - accountId, - error, -}); - -export const removeExcludeFromAntennaEditor = accountId => (dispatch, getState) => { - dispatch(removeExcludeFromAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId)); -}; - -export const removeExcludeFromAntenna = (antennaId, accountId) => (dispatch, getState) => { - dispatch(removeExcludeFromAntennaRequest(antennaId, accountId)); - - api(getState).delete(`/api/v1/antennas/${antennaId}/exclude_accounts`, { params: { account_ids: [accountId] } }) - .then(() => dispatch(removeExcludeFromAntennaSuccess(antennaId, accountId))) - .catch(err => dispatch(removeExcludeFromAntennaFail(antennaId, accountId, err))); -}; - -export const removeExcludeFromAntennaRequest = (antennaId, accountId) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_REQUEST, - antennaId, - accountId, -}); - -export const removeExcludeFromAntennaSuccess = (antennaId, accountId) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_SUCCESS, - antennaId, - accountId, -}); - -export const removeExcludeFromAntennaFail = (antennaId, accountId, error) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_FAIL, - antennaId, - accountId, - error, -}); - -export const fetchAntennaDomains = antennaId => (dispatch, getState) => { - dispatch(fetchAntennaDomainsRequest(antennaId)); - - api(getState).get(`/api/v1/antennas/${antennaId}/domains`, { params: { limit: 0 } }).then(({ data }) => { - dispatch(fetchAntennaDomainsSuccess(antennaId, data)); - }).catch(err => dispatch(fetchAntennaDomainsFail(antennaId, err))); -}; - -export const fetchAntennaDomainsRequest = id => ({ - type: ANTENNA_EDITOR_FETCH_DOMAINS_REQUEST, - id, -}); - -export const fetchAntennaDomainsSuccess = (id, domains) => ({ - type: ANTENNA_EDITOR_FETCH_DOMAINS_SUCCESS, - id, - domains, -}); - -export const fetchAntennaDomainsFail = (id, error) => ({ - type: ANTENNA_EDITOR_FETCH_DOMAINS_FAIL, - id, - error, -}); - -export const addDomainToAntenna = (antennaId, domain) => (dispatch, getState) => { - dispatch(addDomainToAntennaRequest(antennaId, domain)); - - api(getState).post(`/api/v1/antennas/${antennaId}/domains`, { domains: [domain] }) - .then(() => dispatch(addDomainToAntennaSuccess(antennaId, domain))) - .catch(err => dispatch(addDomainToAntennaFail(antennaId, domain, err))); -}; - -export const addDomainToAntennaRequest = (antennaId, domain) => ({ - type: ANTENNA_EDITOR_ADD_DOMAIN_REQUEST, - antennaId, - domain, -}); - -export const addDomainToAntennaSuccess = (antennaId, domain) => ({ - type: ANTENNA_EDITOR_ADD_DOMAIN_SUCCESS, - antennaId, - domain, -}); - -export const addDomainToAntennaFail = (antennaId, domain, error) => ({ - type: ANTENNA_EDITOR_ADD_DOMAIN_FAIL, - antennaId, - domain, - error, -}); - -export const removeDomainFromAntenna = (antennaId, domain) => (dispatch, getState) => { - dispatch(removeDomainFromAntennaRequest(antennaId, domain)); - - api(getState).delete(`/api/v1/antennas/${antennaId}/domains`, { params: { domains: [domain] } }) - .then(() => dispatch(removeDomainFromAntennaSuccess(antennaId, domain))) - .catch(err => dispatch(removeDomainFromAntennaFail(antennaId, domain, err))); -}; - -export const removeDomainFromAntennaRequest = (antennaId, domain) => ({ - type: ANTENNA_EDITOR_REMOVE_DOMAIN_REQUEST, - antennaId, - domain, -}); - -export const removeDomainFromAntennaSuccess = (antennaId, domain) => ({ - type: ANTENNA_EDITOR_REMOVE_DOMAIN_SUCCESS, - antennaId, - domain, -}); - -export const removeDomainFromAntennaFail = (antennaId, domain, error) => ({ - type: ANTENNA_EDITOR_REMOVE_DOMAIN_FAIL, - antennaId, - domain, - error, -}); - -export const addExcludeDomainToAntenna = (antennaId, domain) => (dispatch, getState) => { - dispatch(addExcludeDomainToAntennaRequest(antennaId, domain)); - - api(getState).post(`/api/v1/antennas/${antennaId}/exclude_domains`, { domains: [domain] }) - .then(() => dispatch(addExcludeDomainToAntennaSuccess(antennaId, domain))) - .catch(err => dispatch(addExcludeDomainToAntennaFail(antennaId, domain, err))); -}; - -export const addExcludeDomainToAntennaRequest = (antennaId, domain) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_REQUEST, - antennaId, - domain, -}); - -export const addExcludeDomainToAntennaSuccess = (antennaId, domain) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_SUCCESS, - antennaId, - domain, -}); - -export const addExcludeDomainToAntennaFail = (antennaId, domain, error) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_FAIL, - antennaId, - domain, - error, -}); - -export const removeExcludeDomainFromAntenna = (antennaId, domain) => (dispatch, getState) => { - dispatch(removeExcludeDomainFromAntennaRequest(antennaId, domain)); - - api(getState).delete(`/api/v1/antennas/${antennaId}/exclude_domains`, { params: { domains: [domain] } }) - .then(() => dispatch(removeExcludeDomainFromAntennaSuccess(antennaId, domain))) - .catch(err => dispatch(removeExcludeDomainFromAntennaFail(antennaId, domain, err))); -}; - -export const removeExcludeDomainFromAntennaRequest = (antennaId, domain) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_REQUEST, - antennaId, - domain, -}); - -export const removeExcludeDomainFromAntennaSuccess = (antennaId, domain) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_SUCCESS, - antennaId, - domain, -}); - -export const removeExcludeDomainFromAntennaFail = (antennaId, domain, error) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_FAIL, - antennaId, - domain, - error, -}); - -export const fetchAntennaKeywords = antennaId => (dispatch, getState) => { - dispatch(fetchAntennaKeywordsRequest(antennaId)); - - api(getState).get(`/api/v1/antennas/${antennaId}/keywords`, { params: { limit: 0 } }).then(({ data }) => { - dispatch(fetchAntennaKeywordsSuccess(antennaId, data)); - }).catch(err => dispatch(fetchAntennaKeywordsFail(antennaId, err))); -}; - -export const fetchAntennaKeywordsRequest = id => ({ - type: ANTENNA_EDITOR_FETCH_KEYWORDS_REQUEST, - id, -}); - -export const fetchAntennaKeywordsSuccess = (id, keywords) => ({ - type: ANTENNA_EDITOR_FETCH_KEYWORDS_SUCCESS, - id, - keywords, -}); - -export const fetchAntennaKeywordsFail = (id, error) => ({ - type: ANTENNA_EDITOR_FETCH_KEYWORDS_FAIL, - id, - error, -}); - -export const addKeywordToAntenna = (antennaId, keyword) => (dispatch, getState) => { - dispatch(addKeywordToAntennaRequest(antennaId, keyword)); - - api(getState).post(`/api/v1/antennas/${antennaId}/keywords`, { keywords: [keyword] }) - .then(() => dispatch(addKeywordToAntennaSuccess(antennaId, keyword))) - .catch(err => dispatch(addKeywordToAntennaFail(antennaId, keyword, err))); -}; - -export const addKeywordToAntennaRequest = (antennaId, keyword) => ({ - type: ANTENNA_EDITOR_ADD_KEYWORD_REQUEST, - antennaId, - keyword, -}); - -export const addKeywordToAntennaSuccess = (antennaId, keyword) => ({ - type: ANTENNA_EDITOR_ADD_KEYWORD_SUCCESS, - antennaId, - keyword, -}); - -export const addKeywordToAntennaFail = (antennaId, keyword, error) => ({ - type: ANTENNA_EDITOR_ADD_KEYWORD_FAIL, - antennaId, - keyword, - error, -}); - -export const removeKeywordFromAntenna = (antennaId, keyword) => (dispatch, getState) => { - dispatch(removeKeywordFromAntennaRequest(antennaId, keyword)); - - api(getState).delete(`/api/v1/antennas/${antennaId}/keywords`, { params: { keywords: [keyword] } }) - .then(() => dispatch(removeKeywordFromAntennaSuccess(antennaId, keyword))) - .catch(err => dispatch(removeKeywordFromAntennaFail(antennaId, keyword, err))); -}; - -export const removeKeywordFromAntennaRequest = (antennaId, keyword) => ({ - type: ANTENNA_EDITOR_REMOVE_KEYWORD_REQUEST, - antennaId, - keyword, -}); - -export const removeKeywordFromAntennaSuccess = (antennaId, keyword) => ({ - type: ANTENNA_EDITOR_REMOVE_KEYWORD_SUCCESS, - antennaId, - keyword, -}); - -export const removeKeywordFromAntennaFail = (antennaId, keyword, error) => ({ - type: ANTENNA_EDITOR_REMOVE_KEYWORD_FAIL, - antennaId, - keyword, - error, -}); - -export const addExcludeKeywordToAntenna = (antennaId, keyword) => (dispatch, getState) => { - dispatch(addExcludeKeywordToAntennaRequest(antennaId, keyword)); - - api(getState).post(`/api/v1/antennas/${antennaId}/exclude_keywords`, { keywords: [keyword] }) - .then(() => dispatch(addExcludeKeywordToAntennaSuccess(antennaId, keyword))) - .catch(err => dispatch(addExcludeKeywordToAntennaFail(antennaId, keyword, err))); -}; - -export const addExcludeKeywordToAntennaRequest = (antennaId, keyword) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_REQUEST, - antennaId, - keyword, -}); - -export const addExcludeKeywordToAntennaSuccess = (antennaId, keyword) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_SUCCESS, - antennaId, - keyword, -}); - -export const addExcludeKeywordToAntennaFail = (antennaId, keyword, error) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_FAIL, - antennaId, - keyword, - error, -}); - -export const removeExcludeKeywordFromAntenna = (antennaId, keyword) => (dispatch, getState) => { - dispatch(removeExcludeKeywordFromAntennaRequest(antennaId, keyword)); - - api(getState).delete(`/api/v1/antennas/${antennaId}/exclude_keywords`, { params: { keywords: [keyword] } }) - .then(() => dispatch(removeExcludeKeywordFromAntennaSuccess(antennaId, keyword))) - .catch(err => dispatch(removeExcludeKeywordFromAntennaFail(antennaId, keyword, err))); -}; - -export const removeExcludeKeywordFromAntennaRequest = (antennaId, keyword) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_REQUEST, - antennaId, - keyword, -}); - -export const removeExcludeKeywordFromAntennaSuccess = (antennaId, keyword) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_SUCCESS, - antennaId, - keyword, -}); - -export const removeExcludeKeywordFromAntennaFail = (antennaId, keyword, error) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_FAIL, - antennaId, - keyword, - error, -}); - -export const fetchAntennaTags = antennaId => (dispatch, getState) => { - dispatch(fetchAntennaTagsRequest(antennaId)); - - api(getState).get(`/api/v1/antennas/${antennaId}/tags`, { params: { limit: 0 } }).then(({ data }) => { - dispatch(fetchAntennaTagsSuccess(antennaId, data)); - }).catch(err => dispatch(fetchAntennaTagsFail(antennaId, err))); -}; - -export const fetchAntennaTagsRequest = id => ({ - type: ANTENNA_EDITOR_FETCH_TAGS_REQUEST, - id, -}); - -export const fetchAntennaTagsSuccess = (id, tags) => ({ - type: ANTENNA_EDITOR_FETCH_TAGS_SUCCESS, - id, - tags, -}); - -export const fetchAntennaTagsFail = (id, error) => ({ - type: ANTENNA_EDITOR_FETCH_TAGS_FAIL, - id, - error, -}); - -export const addTagToAntenna = (antennaId, tag) => (dispatch, getState) => { - dispatch(addTagToAntennaRequest(antennaId, tag)); - - api(getState).post(`/api/v1/antennas/${antennaId}/tags`, { tags: [tag] }) - .then(() => dispatch(addTagToAntennaSuccess(antennaId, tag))) - .catch(err => dispatch(addTagToAntennaFail(antennaId, tag, err))); -}; - -export const addTagToAntennaRequest = (antennaId, tag) => ({ - type: ANTENNA_EDITOR_ADD_TAG_REQUEST, - antennaId, - tag, -}); - -export const addTagToAntennaSuccess = (antennaId, tag) => ({ - type: ANTENNA_EDITOR_ADD_TAG_SUCCESS, - antennaId, - tag, -}); - -export const addTagToAntennaFail = (antennaId, tag, error) => ({ - type: ANTENNA_EDITOR_ADD_TAG_FAIL, - antennaId, - tag, - error, -}); - -export const removeTagFromAntenna = (antennaId, tag) => (dispatch, getState) => { - dispatch(removeTagFromAntennaRequest(antennaId, tag)); - - api(getState).delete(`/api/v1/antennas/${antennaId}/tags`, { params: { tags: [tag] } }) - .then(() => dispatch(removeTagFromAntennaSuccess(antennaId, tag))) - .catch(err => dispatch(removeTagFromAntennaFail(antennaId, tag, err))); -}; - -export const removeTagFromAntennaRequest = (antennaId, tag) => ({ - type: ANTENNA_EDITOR_REMOVE_TAG_REQUEST, - antennaId, - tag, -}); - -export const removeTagFromAntennaSuccess = (antennaId, tag) => ({ - type: ANTENNA_EDITOR_REMOVE_TAG_SUCCESS, - antennaId, - tag, -}); - -export const removeTagFromAntennaFail = (antennaId, tag, error) => ({ - type: ANTENNA_EDITOR_REMOVE_TAG_FAIL, - antennaId, - tag, - error, -}); - -export const addExcludeTagToAntenna = (antennaId, tag) => (dispatch, getState) => { - dispatch(addExcludeTagToAntennaRequest(antennaId, tag)); - - api(getState).post(`/api/v1/antennas/${antennaId}/exclude_tags`, { tags: [tag] }) - .then(() => dispatch(addExcludeTagToAntennaSuccess(antennaId, tag))) - .catch(err => dispatch(addExcludeTagToAntennaFail(antennaId, tag, err))); -}; - -export const addExcludeTagToAntennaRequest = (antennaId, tag) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_TAG_REQUEST, - antennaId, - tag, -}); - -export const addExcludeTagToAntennaSuccess = (antennaId, tag) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_TAG_SUCCESS, - antennaId, - tag, -}); - -export const addExcludeTagToAntennaFail = (antennaId, tag, error) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_TAG_FAIL, - antennaId, - tag, - error, -}); - -export const removeExcludeTagFromAntenna = (antennaId, tag) => (dispatch, getState) => { - dispatch(removeExcludeTagFromAntennaRequest(antennaId, tag)); - - api(getState).delete(`/api/v1/antennas/${antennaId}/exclude_tags`, { params: { tags: [tag] } }) - .then(() => dispatch(removeExcludeTagFromAntennaSuccess(antennaId, tag))) - .catch(err => dispatch(removeExcludeTagFromAntennaFail(antennaId, tag, err))); -}; - -export const removeExcludeTagFromAntennaRequest = (antennaId, tag) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_REQUEST, - antennaId, - tag, -}); - -export const removeExcludeTagFromAntennaSuccess = (antennaId, tag) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_SUCCESS, - antennaId, - tag, -}); - -export const removeExcludeTagFromAntennaFail = (antennaId, tag, error) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_FAIL, - antennaId, - tag, - error, -}); - -export const resetAntennaAdder = () => ({ - type: ANTENNA_ADDER_RESET, -}); - -export const setupAntennaAdder = accountId => (dispatch, getState) => { - dispatch({ - type: ANTENNA_ADDER_SETUP, - account: getState().getIn(['accounts', accountId]), - }); - dispatch(fetchAntennas()); - dispatch(fetchAccountAntennas(accountId)); -}; - -export const setupExcludeAntennaAdder = accountId => (dispatch, getState) => { - dispatch({ - type: ANTENNA_ADDER_SETUP, - account: getState().getIn(['accounts', accountId]), - }); - dispatch(fetchAntennas()); - dispatch(fetchExcludeAccountAntennas(accountId)); -}; - -export const fetchAccountAntennas = accountId => (dispatch, getState) => { - dispatch(fetchAccountAntennasRequest(accountId)); - - api(getState).get(`/api/v1/accounts/${accountId}/antennas`) - .then(({ data }) => dispatch(fetchAccountAntennasSuccess(accountId, data))) - .catch(err => dispatch(fetchAccountAntennasFail(accountId, err))); -}; - -export const fetchAccountAntennasRequest = id => ({ - type:ANTENNA_ADDER_ANTENNAS_FETCH_REQUEST, - id, -}); - -export const fetchAccountAntennasSuccess = (id, antennas) => ({ - type: ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS, - id, - antennas, -}); - -export const fetchAccountAntennasFail = (id, err) => ({ - type: ANTENNA_ADDER_ANTENNAS_FETCH_FAIL, - id, - err, -}); - -export const fetchExcludeAccountAntennas = accountId => (dispatch, getState) => { - dispatch(fetchExcludeAccountAntennasRequest(accountId)); - - api(getState).get(`/api/v1/accounts/${accountId}/exclude_antennas`) - .then(({ data }) => dispatch(fetchExcludeAccountAntennasSuccess(accountId, data))) - .catch(err => dispatch(fetchExcludeAccountAntennasFail(accountId, err))); -}; - -export const fetchExcludeAccountAntennasRequest = id => ({ - type:ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_REQUEST, - id, -}); - -export const fetchExcludeAccountAntennasSuccess = (id, antennas) => ({ - type: ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_SUCCESS, - id, - antennas, -}); - -export const fetchExcludeAccountAntennasFail = (id, err) => ({ - type: ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_FAIL, - id, - err, -}); - -export const addToAntennaAdder = antennaId => (dispatch, getState) => { - dispatch(addToAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId']))); -}; - -export const removeFromAntennaAdder = antennaId => (dispatch, getState) => { - dispatch(removeFromAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId']))); -}; - -export const addExcludeToAntennaAdder = antennaId => (dispatch, getState) => { - dispatch(addExcludeToAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId']))); -}; - -export const removeExcludeFromAntennaAdder = antennaId => (dispatch, getState) => { - dispatch(removeExcludeFromAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId']))); -}; - diff --git a/app/javascript/mastodon/actions/antennas_typed.ts b/app/javascript/mastodon/actions/antennas_typed.ts new file mode 100644 index 0000000000..f385b37d6e --- /dev/null +++ b/app/javascript/mastodon/actions/antennas_typed.ts @@ -0,0 +1,13 @@ +import { apiCreate, apiUpdate } from 'mastodon/api/antennas'; +import type { Antenna } from 'mastodon/models/antenna'; +import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; + +export const createAntenna = createDataLoadingThunk( + 'antenna/create', + (antenna: Partial) => apiCreate(antenna), +); + +export const updateAntenna = createDataLoadingThunk( + 'antenna/update', + (antenna: Partial) => apiUpdate(antenna), +); diff --git a/app/javascript/mastodon/actions/bookmark_categories.js b/app/javascript/mastodon/actions/bookmark_categories.js index 7d458b85ec..313d5de8f2 100644 --- a/app/javascript/mastodon/actions/bookmark_categories.js +++ b/app/javascript/mastodon/actions/bookmark_categories.js @@ -1,10 +1,6 @@ -import { bookmarkCategoryNeeded } from 'mastodon/initial_state'; -import { makeGetStatus } from 'mastodon/selectors'; - import api, { getLinks } from '../api'; import { importFetchedStatuses } from './importer'; -import { unbookmark } from './interactions'; export const BOOKMARK_CATEGORY_FETCH_REQUEST = 'BOOKMARK_CATEGORY_FETCH_REQUEST'; export const BOOKMARK_CATEGORY_FETCH_SUCCESS = 'BOOKMARK_CATEGORY_FETCH_SUCCESS'; @@ -14,18 +10,6 @@ export const BOOKMARK_CATEGORIES_FETCH_REQUEST = 'BOOKMARK_CATEGORIES_FETCH_REQU export const BOOKMARK_CATEGORIES_FETCH_SUCCESS = 'BOOKMARK_CATEGORIES_FETCH_SUCCESS'; export const BOOKMARK_CATEGORIES_FETCH_FAIL = 'BOOKMARK_CATEGORIES_FETCH_FAIL'; -export const BOOKMARK_CATEGORY_EDITOR_TITLE_CHANGE = 'BOOKMARK_CATEGORY_EDITOR_TITLE_CHANGE'; -export const BOOKMARK_CATEGORY_EDITOR_RESET = 'BOOKMARK_CATEGORY_EDITOR_RESET'; -export const BOOKMARK_CATEGORY_EDITOR_SETUP = 'BOOKMARK_CATEGORY_EDITOR_SETUP'; - -export const BOOKMARK_CATEGORY_CREATE_REQUEST = 'BOOKMARK_CATEGORY_CREATE_REQUEST'; -export const BOOKMARK_CATEGORY_CREATE_SUCCESS = 'BOOKMARK_CATEGORY_CREATE_SUCCESS'; -export const BOOKMARK_CATEGORY_CREATE_FAIL = 'BOOKMARK_CATEGORY_CREATE_FAIL'; - -export const BOOKMARK_CATEGORY_UPDATE_REQUEST = 'BOOKMARK_CATEGORY_UPDATE_REQUEST'; -export const BOOKMARK_CATEGORY_UPDATE_SUCCESS = 'BOOKMARK_CATEGORY_UPDATE_SUCCESS'; -export const BOOKMARK_CATEGORY_UPDATE_FAIL = 'BOOKMARK_CATEGORY_UPDATE_FAIL'; - export const BOOKMARK_CATEGORY_DELETE_REQUEST = 'BOOKMARK_CATEGORY_DELETE_REQUEST'; export const BOOKMARK_CATEGORY_DELETE_SUCCESS = 'BOOKMARK_CATEGORY_DELETE_SUCCESS'; export const BOOKMARK_CATEGORY_DELETE_FAIL = 'BOOKMARK_CATEGORY_DELETE_FAIL'; @@ -34,25 +18,13 @@ export const BOOKMARK_CATEGORY_STATUSES_FETCH_REQUEST = 'BOOKMARK_CATEGORY_STATU export const BOOKMARK_CATEGORY_STATUSES_FETCH_SUCCESS = 'BOOKMARK_CATEGORY_STATUSES_FETCH_SUCCESS'; export const BOOKMARK_CATEGORY_STATUSES_FETCH_FAIL = 'BOOKMARK_CATEGORY_STATUSES_FETCH_FAIL'; -export const BOOKMARK_CATEGORY_EDITOR_ADD_REQUEST = 'BOOKMARK_CATEGORY_EDITOR_ADD_REQUEST'; -export const BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS = 'BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS'; -export const BOOKMARK_CATEGORY_EDITOR_ADD_FAIL = 'BOOKMARK_CATEGORY_EDITOR_ADD_FAIL'; - -export const BOOKMARK_CATEGORY_EDITOR_REMOVE_REQUEST = 'BOOKMARK_CATEGORY_EDITOR_REMOVE_REQUEST'; -export const BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS = 'BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS'; -export const BOOKMARK_CATEGORY_EDITOR_REMOVE_FAIL = 'BOOKMARK_CATEGORY_EDITOR_REMOVE_FAIL'; - -export const BOOKMARK_CATEGORY_ADDER_RESET = 'BOOKMARK_CATEGORY_ADDER_RESET'; -export const BOOKMARK_CATEGORY_ADDER_SETUP = 'BOOKMARK_CATEGORY_ADDER_SETUP'; - -export const BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_REQUEST = 'BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_REQUEST'; -export const BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_SUCCESS = 'BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_SUCCESS'; -export const BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_FAIL = 'BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_FAIL'; - export const BOOKMARK_CATEGORY_STATUSES_EXPAND_REQUEST = 'BOOKMARK_CATEGORY_STATUSES_EXPAND_REQUEST'; export const BOOKMARK_CATEGORY_STATUSES_EXPAND_SUCCESS = 'BOOKMARK_CATEGORY_STATUSES_EXPAND_SUCCESS'; export const BOOKMARK_CATEGORY_STATUSES_EXPAND_FAIL = 'BOOKMARK_CATEGORY_STATUSES_EXPAND_FAIL'; +export const BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS = 'BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS'; +export const BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS = 'BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS'; + export const fetchBookmarkCategory = id => (dispatch, getState) => { if (getState().getIn(['bookmark_categories', id])) { return; @@ -103,89 +75,6 @@ export const fetchBookmarkCategoriesFail = error => ({ error, }); -export const submitBookmarkCategoryEditor = shouldReset => (dispatch, getState) => { - const bookmarkCategoryId = getState().getIn(['bookmarkCategoryEditor', 'bookmarkCategoryId']); - const title = getState().getIn(['bookmarkCategoryEditor', 'title']); - - if (bookmarkCategoryId === null) { - dispatch(createBookmarkCategory(title, shouldReset)); - } else { - dispatch(updateBookmarkCategory(bookmarkCategoryId, title, shouldReset)); - } -}; - -export const setupBookmarkCategoryEditor = bookmarkCategoryId => (dispatch, getState) => { - dispatch({ - type: BOOKMARK_CATEGORY_EDITOR_SETUP, - bookmarkCategory: getState().getIn(['bookmark_categories', bookmarkCategoryId]), - }); - - dispatch(fetchBookmarkCategoryStatuses(bookmarkCategoryId)); -}; - -export const changeBookmarkCategoryEditorTitle = value => ({ - type: BOOKMARK_CATEGORY_EDITOR_TITLE_CHANGE, - value, -}); - -export const createBookmarkCategory = (title, shouldReset) => (dispatch, getState) => { - dispatch(createBookmarkCategoryRequest()); - - api(getState).post('/api/v1/bookmark_categories', { title }).then(({ data }) => { - dispatch(createBookmarkCategorySuccess(data)); - - if (shouldReset) { - dispatch(resetBookmarkCategoryEditor()); - } - }).catch(err => dispatch(createBookmarkCategoryFail(err))); -}; - -export const createBookmarkCategoryRequest = () => ({ - type: BOOKMARK_CATEGORY_CREATE_REQUEST, -}); - -export const createBookmarkCategorySuccess = bookmarkCategory => ({ - type: BOOKMARK_CATEGORY_CREATE_SUCCESS, - bookmarkCategory, -}); - -export const createBookmarkCategoryFail = error => ({ - type: BOOKMARK_CATEGORY_CREATE_FAIL, - error, -}); - -export const updateBookmarkCategory = (id, title, shouldReset) => (dispatch, getState) => { - dispatch(updateBookmarkCategoryRequest(id)); - - api(getState).put(`/api/v1/bookmark_categories/${id}`, { title }).then(({ data }) => { - dispatch(updateBookmarkCategorySuccess(data)); - - if (shouldReset) { - dispatch(resetBookmarkCategoryEditor()); - } - }).catch(err => dispatch(updateBookmarkCategoryFail(id, err))); -}; - -export const updateBookmarkCategoryRequest = id => ({ - type: BOOKMARK_CATEGORY_UPDATE_REQUEST, - id, -}); - -export const updateBookmarkCategorySuccess = bookmarkCategory => ({ - type: BOOKMARK_CATEGORY_UPDATE_SUCCESS, - bookmarkCategory, -}); - -export const updateBookmarkCategoryFail = (id, error) => ({ - type: BOOKMARK_CATEGORY_UPDATE_FAIL, - id, - error, -}); - -export const resetBookmarkCategoryEditor = () => ({ - type: BOOKMARK_CATEGORY_EDITOR_RESET, -}); - export const deleteBookmarkCategory = id => (dispatch, getState) => { dispatch(deleteBookmarkCategoryRequest(id)); @@ -238,121 +127,11 @@ export const fetchBookmarkCategoryStatusesFail = (id, error) => ({ error, }); -export const addToBookmarkCategory = (bookmarkCategoryId, statusId) => (dispatch, getState) => { - dispatch(addToBookmarkCategoryRequest(bookmarkCategoryId, statusId)); - - api(getState).post(`/api/v1/bookmark_categories/${bookmarkCategoryId}/statuses`, { status_ids: [statusId] }) - .then(() => dispatch(addToBookmarkCategorySuccess(bookmarkCategoryId, statusId))) - .catch(err => dispatch(addToBookmarkCategoryFail(bookmarkCategoryId, statusId, err))); -}; - -export const addToBookmarkCategoryRequest = (bookmarkCategoryId, statusId) => ({ - type: BOOKMARK_CATEGORY_EDITOR_ADD_REQUEST, - bookmarkCategoryId, - statusId, -}); - -export const addToBookmarkCategorySuccess = (bookmarkCategoryId, statusId) => ({ - type: BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS, - bookmarkCategoryId, - statusId, -}); - -export const addToBookmarkCategoryFail = (bookmarkCategoryId, statusId, error) => ({ - type: BOOKMARK_CATEGORY_EDITOR_ADD_FAIL, - bookmarkCategoryId, - statusId, - error, -}); - -export const removeFromBookmarkCategory = (bookmarkCategoryId, statusId) => (dispatch, getState) => { - dispatch(removeFromBookmarkCategoryRequest(bookmarkCategoryId, statusId)); - - api(getState).delete(`/api/v1/bookmark_categories/${bookmarkCategoryId}/statuses`, { params: { status_ids: [statusId] } }) - .then(() => dispatch(removeFromBookmarkCategorySuccess(bookmarkCategoryId, statusId))) - .catch(err => dispatch(removeFromBookmarkCategoryFail(bookmarkCategoryId, statusId, err))); -}; - -export const removeFromBookmarkCategoryRequest = (bookmarkCategoryId, statusId) => ({ - type: BOOKMARK_CATEGORY_EDITOR_REMOVE_REQUEST, - bookmarkCategoryId, - statusId, -}); - -export const removeFromBookmarkCategorySuccess = (bookmarkCategoryId, statusId) => ({ - type: BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS, - bookmarkCategoryId, - statusId, -}); - -export const removeFromBookmarkCategoryFail = (bookmarkCategoryId, statusId, error) => ({ - type: BOOKMARK_CATEGORY_EDITOR_REMOVE_FAIL, - bookmarkCategoryId, - statusId, - error, -}); - -export const resetBookmarkCategoryAdder = () => ({ - type: BOOKMARK_CATEGORY_ADDER_RESET, -}); - -export const setupBookmarkCategoryAdder = statusId => (dispatch, getState) => { - dispatch({ - type: BOOKMARK_CATEGORY_ADDER_SETUP, - status: getState().getIn(['statuses', statusId]), - }); - dispatch(fetchBookmarkCategories()); - dispatch(fetchStatusBookmarkCategories(statusId)); -}; - -export const fetchStatusBookmarkCategories = statusId => (dispatch, getState) => { - dispatch(fetchStatusBookmarkCategoriesRequest(statusId)); - - api(getState).get(`/api/v1/statuses/${statusId}/bookmark_categories`) - .then(({ data }) => dispatch(fetchStatusBookmarkCategoriesSuccess(statusId, data))) - .catch(err => dispatch(fetchStatusBookmarkCategoriesFail(statusId, err))); -}; - -export const fetchStatusBookmarkCategoriesRequest = id => ({ - type:BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_REQUEST, - id, -}); - -export const fetchStatusBookmarkCategoriesSuccess = (id, bookmarkCategories) => ({ - type: BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_SUCCESS, - id, - bookmarkCategories, -}); - -export const fetchStatusBookmarkCategoriesFail = (id, err) => ({ - type: BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_FAIL, - id, - err, -}); - -export const addToBookmarkCategoryAdder = bookmarkCategoryId => (dispatch, getState) => { - dispatch(addToBookmarkCategory(bookmarkCategoryId, getState().getIn(['bookmarkCategoryAdder', 'statusId']))); -}; - -export const removeFromBookmarkCategoryAdder = bookmarkCategoryId => (dispatch, getState) => { - if (bookmarkCategoryNeeded) { - const categories = getState().getIn(['bookmarkCategoryAdder', 'bookmarkCategories', 'items']); - if (categories && categories.count() <= 1) { - const status = makeGetStatus()(getState(), { id: getState().getIn(['bookmarkCategoryAdder', 'statusId']) }); - dispatch(unbookmark(status)); - } else { - dispatch(removeFromBookmarkCategory(bookmarkCategoryId, getState().getIn(['bookmarkCategoryAdder', 'statusId']))); - } - } else { - dispatch(removeFromBookmarkCategory(bookmarkCategoryId, getState().getIn(['bookmarkCategoryAdder', 'statusId']))); - } -}; - export function expandBookmarkCategoryStatuses(bookmarkCategoryId) { return (dispatch, getState) => { - const url = getState().getIn(['bookmark_categories', bookmarkCategoryId, 'next'], null); + const url = getState().getIn(['status_lists', 'bookmark_category_statuses', bookmarkCategoryId, 'next'], null); - if (url === null || getState().getIn(['bookmark_categories', bookmarkCategoryId, 'isLoading'])) { + if (url === null || getState().getIn(['status_lists', 'bookmark_category_statuses', bookmarkCategoryId, 'isLoading'])) { return; } @@ -392,3 +171,19 @@ export function expandBookmarkCategoryStatusesFail(id, error) { }; } +export function bookmarkCategoryEditorAddSuccess(id, statusId) { + return { + type: BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS, + id, + statusId, + }; +} + +export function bookmarkCategoryEditorRemoveSuccess(id, statusId) { + return { + type: BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS, + id, + statusId, + }; +} + diff --git a/app/javascript/mastodon/actions/bookmark_categories_typed.ts b/app/javascript/mastodon/actions/bookmark_categories_typed.ts new file mode 100644 index 0000000000..3570ce5991 --- /dev/null +++ b/app/javascript/mastodon/actions/bookmark_categories_typed.ts @@ -0,0 +1,13 @@ +import { apiCreate, apiUpdate } from 'mastodon/api/bookmark_categories'; +import type { BookmarkCategory } from 'mastodon/models/bookmark_category'; +import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; + +export const createBookmarkCategory = createDataLoadingThunk( + 'bookmark_category/create', + (bookmarkCategory: Partial) => apiCreate(bookmarkCategory), +); + +export const updateBookmarkCategory = createDataLoadingThunk( + 'bookmark_category/update', + (bookmarkCategory: Partial) => apiUpdate(bookmarkCategory), +); diff --git a/app/javascript/mastodon/actions/circles.js b/app/javascript/mastodon/actions/circles.js index 7ed41b4045..221c0c683a 100644 --- a/app/javascript/mastodon/actions/circles.js +++ b/app/javascript/mastodon/actions/circles.js @@ -1,7 +1,6 @@ import api, { getLinks } from '../api'; -import { showAlertForError } from './alerts'; -import { importFetchedAccounts, importFetchedStatuses } from './importer'; +import { importFetchedStatuses } from './importer'; export const CIRCLE_FETCH_REQUEST = 'CIRCLE_FETCH_REQUEST'; export const CIRCLE_FETCH_SUCCESS = 'CIRCLE_FETCH_SUCCESS'; @@ -11,10 +10,6 @@ export const CIRCLES_FETCH_REQUEST = 'CIRCLES_FETCH_REQUEST'; export const CIRCLES_FETCH_SUCCESS = 'CIRCLES_FETCH_SUCCESS'; export const CIRCLES_FETCH_FAIL = 'CIRCLES_FETCH_FAIL'; -export const CIRCLE_EDITOR_TITLE_CHANGE = 'CIRCLE_EDITOR_TITLE_CHANGE'; -export const CIRCLE_EDITOR_RESET = 'CIRCLE_EDITOR_RESET'; -export const CIRCLE_EDITOR_SETUP = 'CIRCLE_EDITOR_SETUP'; - export const CIRCLE_CREATE_REQUEST = 'CIRCLE_CREATE_REQUEST'; export const CIRCLE_CREATE_SUCCESS = 'CIRCLE_CREATE_SUCCESS'; export const CIRCLE_CREATE_FAIL = 'CIRCLE_CREATE_FAIL'; @@ -27,29 +22,6 @@ export const CIRCLE_DELETE_REQUEST = 'CIRCLE_DELETE_REQUEST'; export const CIRCLE_DELETE_SUCCESS = 'CIRCLE_DELETE_SUCCESS'; export const CIRCLE_DELETE_FAIL = 'CIRCLE_DELETE_FAIL'; -export const CIRCLE_ACCOUNTS_FETCH_REQUEST = 'CIRCLE_ACCOUNTS_FETCH_REQUEST'; -export const CIRCLE_ACCOUNTS_FETCH_SUCCESS = 'CIRCLE_ACCOUNTS_FETCH_SUCCESS'; -export const CIRCLE_ACCOUNTS_FETCH_FAIL = 'CIRCLE_ACCOUNTS_FETCH_FAIL'; - -export const CIRCLE_EDITOR_SUGGESTIONS_CHANGE = 'CIRCLE_EDITOR_SUGGESTIONS_CHANGE'; -export const CIRCLE_EDITOR_SUGGESTIONS_READY = 'CIRCLE_EDITOR_SUGGESTIONS_READY'; -export const CIRCLE_EDITOR_SUGGESTIONS_CLEAR = 'CIRCLE_EDITOR_SUGGESTIONS_CLEAR'; - -export const CIRCLE_EDITOR_ADD_REQUEST = 'CIRCLE_EDITOR_ADD_REQUEST'; -export const CIRCLE_EDITOR_ADD_SUCCESS = 'CIRCLE_EDITOR_ADD_SUCCESS'; -export const CIRCLE_EDITOR_ADD_FAIL = 'CIRCLE_EDITOR_ADD_FAIL'; - -export const CIRCLE_EDITOR_REMOVE_REQUEST = 'CIRCLE_EDITOR_REMOVE_REQUEST'; -export const CIRCLE_EDITOR_REMOVE_SUCCESS = 'CIRCLE_EDITOR_REMOVE_SUCCESS'; -export const CIRCLE_EDITOR_REMOVE_FAIL = 'CIRCLE_EDITOR_REMOVE_FAIL'; - -export const CIRCLE_ADDER_RESET = 'CIRCLE_ADDER_RESET'; -export const CIRCLE_ADDER_SETUP = 'CIRCLE_ADDER_SETUP'; - -export const CIRCLE_ADDER_CIRCLES_FETCH_REQUEST = 'CIRCLE_ADDER_CIRCLES_FETCH_REQUEST'; -export const CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS = 'CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS'; -export const CIRCLE_ADDER_CIRCLES_FETCH_FAIL = 'CIRCLE_ADDER_CIRCLES_FETCH_FAIL'; - export const CIRCLE_STATUSES_FETCH_REQUEST = 'CIRCLE_STATUSES_FETCH_REQUEST'; export const CIRCLE_STATUSES_FETCH_SUCCESS = 'CIRCLE_STATUSES_FETCH_SUCCESS'; export const CIRCLE_STATUSES_FETCH_FAIL = 'CIRCLE_STATUSES_FETCH_FAIL'; @@ -108,89 +80,6 @@ export const fetchCirclesFail = error => ({ error, }); -export const submitCircleEditor = shouldReset => (dispatch, getState) => { - const circleId = getState().getIn(['circleEditor', 'circleId']); - const title = getState().getIn(['circleEditor', 'title']); - - if (circleId === null) { - dispatch(createCircle(title, shouldReset)); - } else { - dispatch(updateCircle(circleId, title, shouldReset)); - } -}; - -export const setupCircleEditor = circleId => (dispatch, getState) => { - dispatch({ - type: CIRCLE_EDITOR_SETUP, - circle: getState().getIn(['circles', circleId]), - }); - - dispatch(fetchCircleAccounts(circleId)); -}; - -export const changeCircleEditorTitle = value => ({ - type: CIRCLE_EDITOR_TITLE_CHANGE, - value, -}); - -export const createCircle = (title, shouldReset) => (dispatch, getState) => { - dispatch(createCircleRequest()); - - api(getState).post('/api/v1/circles', { title }).then(({ data }) => { - dispatch(createCircleSuccess(data)); - - if (shouldReset) { - dispatch(resetCircleEditor()); - } - }).catch(err => dispatch(createCircleFail(err))); -}; - -export const createCircleRequest = () => ({ - type: CIRCLE_CREATE_REQUEST, -}); - -export const createCircleSuccess = circle => ({ - type: CIRCLE_CREATE_SUCCESS, - circle, -}); - -export const createCircleFail = error => ({ - type: CIRCLE_CREATE_FAIL, - error, -}); - -export const updateCircle = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => { - dispatch(updateCircleRequest(id)); - - api(getState).put(`/api/v1/circles/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => { - dispatch(updateCircleSuccess(data)); - - if (shouldReset) { - dispatch(resetCircleEditor()); - } - }).catch(err => dispatch(updateCircleFail(id, err))); -}; - -export const updateCircleRequest = id => ({ - type: CIRCLE_UPDATE_REQUEST, - id, -}); - -export const updateCircleSuccess = circle => ({ - type: CIRCLE_UPDATE_SUCCESS, - circle, -}); - -export const updateCircleFail = (id, error) => ({ - type: CIRCLE_UPDATE_FAIL, - id, - error, -}); - -export const resetCircleEditor = () => ({ - type: CIRCLE_EDITOR_RESET, -}); - export const deleteCircle = id => (dispatch, getState) => { dispatch(deleteCircleRequest(id)); @@ -215,169 +104,6 @@ export const deleteCircleFail = (id, error) => ({ error, }); -export const fetchCircleAccounts = circleId => (dispatch, getState) => { - dispatch(fetchCircleAccountsRequest(circleId)); - - api(getState).get(`/api/v1/circles/${circleId}/accounts`, { params: { limit: 0 } }).then(({ data }) => { - dispatch(importFetchedAccounts(data)); - dispatch(fetchCircleAccountsSuccess(circleId, data)); - }).catch(err => dispatch(fetchCircleAccountsFail(circleId, err))); -}; - -export const fetchCircleAccountsRequest = id => ({ - type: CIRCLE_ACCOUNTS_FETCH_REQUEST, - id, -}); - -export const fetchCircleAccountsSuccess = (id, accounts, next) => ({ - type: CIRCLE_ACCOUNTS_FETCH_SUCCESS, - id, - accounts, - next, -}); - -export const fetchCircleAccountsFail = (id, error) => ({ - type: CIRCLE_ACCOUNTS_FETCH_FAIL, - id, - error, -}); - -export const fetchCircleSuggestions = q => (dispatch, getState) => { - const params = { - q, - resolve: false, - follower: true, - }; - - api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => { - dispatch(importFetchedAccounts(data)); - dispatch(fetchCircleSuggestionsReady(q, data)); - }).catch(error => dispatch(showAlertForError(error))); -}; - -export const fetchCircleSuggestionsReady = (query, accounts) => ({ - type: CIRCLE_EDITOR_SUGGESTIONS_READY, - query, - accounts, -}); - -export const clearCircleSuggestions = () => ({ - type: CIRCLE_EDITOR_SUGGESTIONS_CLEAR, -}); - -export const changeCircleSuggestions = value => ({ - type: CIRCLE_EDITOR_SUGGESTIONS_CHANGE, - value, -}); - -export const addToCircleEditor = accountId => (dispatch, getState) => { - dispatch(addToCircle(getState().getIn(['circleEditor', 'circleId']), accountId)); -}; - -export const addToCircle = (circleId, accountId) => (dispatch, getState) => { - dispatch(addToCircleRequest(circleId, accountId)); - - api(getState).post(`/api/v1/circles/${circleId}/accounts`, { account_ids: [accountId] }) - .then(() => dispatch(addToCircleSuccess(circleId, accountId))) - .catch(err => dispatch(addToCircleFail(circleId, accountId, err))); -}; - -export const addToCircleRequest = (circleId, accountId) => ({ - type: CIRCLE_EDITOR_ADD_REQUEST, - circleId, - accountId, -}); - -export const addToCircleSuccess = (circleId, accountId) => ({ - type: CIRCLE_EDITOR_ADD_SUCCESS, - circleId, - accountId, -}); - -export const addToCircleFail = (circleId, accountId, error) => ({ - type: CIRCLE_EDITOR_ADD_FAIL, - circleId, - accountId, - error, -}); - -export const removeFromCircleEditor = accountId => (dispatch, getState) => { - dispatch(removeFromCircle(getState().getIn(['circleEditor', 'circleId']), accountId)); -}; - -export const removeFromCircle = (circleId, accountId) => (dispatch, getState) => { - dispatch(removeFromCircleRequest(circleId, accountId)); - - api(getState).delete(`/api/v1/circles/${circleId}/accounts`, { params: { account_ids: [accountId] } }) - .then(() => dispatch(removeFromCircleSuccess(circleId, accountId))) - .catch(err => dispatch(removeFromCircleFail(circleId, accountId, err))); -}; - -export const removeFromCircleRequest = (circleId, accountId) => ({ - type: CIRCLE_EDITOR_REMOVE_REQUEST, - circleId, - accountId, -}); - -export const removeFromCircleSuccess = (circleId, accountId) => ({ - type: CIRCLE_EDITOR_REMOVE_SUCCESS, - circleId, - accountId, -}); - -export const removeFromCircleFail = (circleId, accountId, error) => ({ - type: CIRCLE_EDITOR_REMOVE_FAIL, - circleId, - accountId, - error, -}); - -export const resetCircleAdder = () => ({ - type: CIRCLE_ADDER_RESET, -}); - -export const setupCircleAdder = accountId => (dispatch, getState) => { - dispatch({ - type: CIRCLE_ADDER_SETUP, - account: getState().getIn(['accounts', accountId]), - }); - dispatch(fetchCircles()); - dispatch(fetchAccountCircles(accountId)); -}; - -export const fetchAccountCircles = accountId => (dispatch, getState) => { - dispatch(fetchAccountCirclesRequest(accountId)); - - api(getState).get(`/api/v1/accounts/${accountId}/circles`) - .then(({ data }) => dispatch(fetchAccountCirclesSuccess(accountId, data))) - .catch(err => dispatch(fetchAccountCirclesFail(accountId, err))); -}; - -export const fetchAccountCirclesRequest = id => ({ - type:CIRCLE_ADDER_CIRCLES_FETCH_REQUEST, - id, -}); - -export const fetchAccountCirclesSuccess = (id, circles) => ({ - type: CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS, - id, - circles, -}); - -export const fetchAccountCirclesFail = (id, err) => ({ - type: CIRCLE_ADDER_CIRCLES_FETCH_FAIL, - id, - err, -}); - -export const addToCircleAdder = circleId => (dispatch, getState) => { - dispatch(addToCircle(circleId, getState().getIn(['circleAdder', 'accountId']))); -}; - -export const removeFromCircleAdder = circleId => (dispatch, getState) => { - dispatch(removeFromCircle(circleId, getState().getIn(['circleAdder', 'accountId']))); -}; - export function fetchCircleStatuses(circleId) { return (dispatch, getState) => { if (getState().getIn(['circles', circleId, 'isLoading'])) { @@ -426,9 +152,9 @@ export function fetchCircleStatusesFail(id, error) { export function expandCircleStatuses(circleId) { return (dispatch, getState) => { - const url = getState().getIn(['circles', circleId, 'next'], null); + const url = getState().getIn(['status_lists', 'circle_statuses', circleId, 'next'], null); - if (url === null || getState().getIn(['circles', circleId, 'isLoading'])) { + if (url === null || getState().getIn(['status_lists', 'circle_statuses', circleId, 'isLoading'])) { return; } diff --git a/app/javascript/mastodon/actions/circles_typed.ts b/app/javascript/mastodon/actions/circles_typed.ts new file mode 100644 index 0000000000..6f4117ff81 --- /dev/null +++ b/app/javascript/mastodon/actions/circles_typed.ts @@ -0,0 +1,13 @@ +import { apiCreate, apiUpdate } from 'mastodon/api/circles'; +import type { Circle } from 'mastodon/models/circle'; +import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; + +export const createCircle = createDataLoadingThunk( + 'circle/create', + (circle: Partial) => apiCreate(circle), +); + +export const updateCircle = createDataLoadingThunk( + 'circle/update', + (circle: Partial) => apiUpdate(circle), +); diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 459847e37b..9a92528f3a 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -272,14 +272,14 @@ export function submitCompose() { insertIfOnline('home'); } - if (statusId === null && response.data.in_reply_to_id === null && response.data.visibility_ex === 'public') { + if (statusId === null && response.data.in_reply_to_id === null && ['public', 'public_unlisted', 'login'].includes(response.data.visibility_ex)) { insertIfOnline('community'); insertIfOnline('public'); insertIfOnline(`account:${response.data.account.id}`); } if (statusId === null && privacy === 'circle' && circleId !== null && circleId !== 0) { - dispatch(submitComposeWithCircleSuccess({ ...response.data }, circleId)); + dispatch(submitComposeWithCircleSuccess({ ...response.data }, `${circleId}`)); } dispatch(showAlert({ @@ -310,7 +310,7 @@ export function submitComposeSuccess(status) { export function submitComposeWithCircleSuccess(status, circleId) { return { type: COMPOSE_WITH_CIRCLE_SUCCESS, - status, + statusId: status.id, circleId, }; } @@ -441,7 +441,7 @@ export function initMediaEditModal(id) { dispatch(openModal({ modalType: 'FOCAL_POINT', - modalProps: { id }, + modalProps: { mediaId: id }, })); }; } diff --git a/app/javascript/mastodon/actions/compose_typed.ts b/app/javascript/mastodon/actions/compose_typed.ts new file mode 100644 index 0000000000..97f0d68c51 --- /dev/null +++ b/app/javascript/mastodon/actions/compose_typed.ts @@ -0,0 +1,70 @@ +import type { List as ImmutableList, Map as ImmutableMap } from 'immutable'; + +import { apiUpdateMedia } from 'mastodon/api/compose'; +import type { ApiMediaAttachmentJSON } from 'mastodon/api_types/media_attachments'; +import type { MediaAttachment } from 'mastodon/models/media_attachment'; +import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; + +type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & { + unattached?: boolean; +}; + +const simulateModifiedApiResponse = ( + media: MediaAttachment, + params: { description?: string; focus?: string }, +): SimulatedMediaAttachmentJSON => { + const [x, y] = (params.focus ?? '').split(','); + + const data = { + ...media.toJS(), + ...params, + meta: { + focus: { + x: parseFloat(x ?? '0'), + y: parseFloat(y ?? '0'), + }, + }, + } as unknown as SimulatedMediaAttachmentJSON; + + return data; +}; + +export const changeUploadCompose = createDataLoadingThunk( + 'compose/changeUpload', + async ( + { + id, + ...params + }: { + id: string; + description: string; + focus: string; + }, + { getState }, + ) => { + const media = ( + (getState().compose as ImmutableMap).get( + 'media_attachments', + ) as ImmutableList + ).find((item) => item.get('id') === id); + + // Editing already-attached media is deferred to editing the post itself. + // For simplicity's sake, fake an API reply. + if (media && !media.get('unattached')) { + return new Promise((resolve) => { + resolve(simulateModifiedApiResponse(media, params)); + }); + } + + return apiUpdateMedia(id, params); + }, + (media: SimulatedMediaAttachmentJSON) => { + return { + media, + attached: typeof media.unattached !== 'undefined' && !media.unattached, + }; + }, + { + useLoadingBar: false, + }, +); diff --git a/app/javascript/mastodon/actions/domain_blocks.js b/app/javascript/mastodon/actions/domain_blocks.js index 727f800af3..279ec1bef7 100644 --- a/app/javascript/mastodon/actions/domain_blocks.js +++ b/app/javascript/mastodon/actions/domain_blocks.js @@ -12,14 +12,6 @@ export const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL'; export const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST'; export const DOMAIN_UNBLOCK_FAIL = 'DOMAIN_UNBLOCK_FAIL'; -export const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST'; -export const DOMAIN_BLOCKS_FETCH_SUCCESS = 'DOMAIN_BLOCKS_FETCH_SUCCESS'; -export const DOMAIN_BLOCKS_FETCH_FAIL = 'DOMAIN_BLOCKS_FETCH_FAIL'; - -export const DOMAIN_BLOCKS_EXPAND_REQUEST = 'DOMAIN_BLOCKS_EXPAND_REQUEST'; -export const DOMAIN_BLOCKS_EXPAND_SUCCESS = 'DOMAIN_BLOCKS_EXPAND_SUCCESS'; -export const DOMAIN_BLOCKS_EXPAND_FAIL = 'DOMAIN_BLOCKS_EXPAND_FAIL'; - export function blockDomain(domain) { return (dispatch, getState) => { dispatch(blockDomainRequest(domain)); @@ -79,80 +71,6 @@ export function unblockDomainFail(domain, error) { }; } -export function fetchDomainBlocks() { - return (dispatch) => { - dispatch(fetchDomainBlocksRequest()); - - api().get('/api/v1/domain_blocks').then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(fetchDomainBlocksSuccess(response.data, next ? next.uri : null)); - }).catch(err => { - dispatch(fetchDomainBlocksFail(err)); - }); - }; -} - -export function fetchDomainBlocksRequest() { - return { - type: DOMAIN_BLOCKS_FETCH_REQUEST, - }; -} - -export function fetchDomainBlocksSuccess(domains, next) { - return { - type: DOMAIN_BLOCKS_FETCH_SUCCESS, - domains, - next, - }; -} - -export function fetchDomainBlocksFail(error) { - return { - type: DOMAIN_BLOCKS_FETCH_FAIL, - error, - }; -} - -export function expandDomainBlocks() { - return (dispatch, getState) => { - const url = getState().getIn(['domain_lists', 'blocks', 'next']); - - if (!url) { - return; - } - - dispatch(expandDomainBlocksRequest()); - - api().get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(expandDomainBlocksSuccess(response.data, next ? next.uri : null)); - }).catch(err => { - dispatch(expandDomainBlocksFail(err)); - }); - }; -} - -export function expandDomainBlocksRequest() { - return { - type: DOMAIN_BLOCKS_EXPAND_REQUEST, - }; -} - -export function expandDomainBlocksSuccess(domains, next) { - return { - type: DOMAIN_BLOCKS_EXPAND_SUCCESS, - domains, - next, - }; -} - -export function expandDomainBlocksFail(error) { - return { - type: DOMAIN_BLOCKS_EXPAND_FAIL, - error, - }; -} - export const initDomainBlockModal = account => dispatch => dispatch(openModal({ modalType: 'DOMAIN_BLOCK', modalProps: { diff --git a/app/javascript/mastodon/actions/dropdown_menu.ts b/app/javascript/mastodon/actions/dropdown_menu.ts index 3694df1ae0..d9d395ba33 100644 --- a/app/javascript/mastodon/actions/dropdown_menu.ts +++ b/app/javascript/mastodon/actions/dropdown_menu.ts @@ -1,11 +1,11 @@ import { createAction } from '@reduxjs/toolkit'; export const openDropdownMenu = createAction<{ - id: string; + id: number; keyboard: boolean; - scrollKey: string; + scrollKey?: string; }>('dropdownMenu/open'); -export const closeDropdownMenu = createAction<{ id: string }>( +export const closeDropdownMenu = createAction<{ id: number }>( 'dropdownMenu/close', ); diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js index ebf58b761a..fc165b1a1f 100644 --- a/app/javascript/mastodon/actions/importer/index.js +++ b/app/javascript/mastodon/actions/importer/index.js @@ -1,10 +1,12 @@ +import { createPollFromServerJSON } from 'mastodon/models/poll'; + import { importAccounts } from '../accounts_typed'; -import { normalizeStatus, normalizePoll } from './normalizer'; +import { normalizeStatus } from './normalizer'; +import { importPolls } from './polls'; export const STATUS_IMPORT = 'STATUS_IMPORT'; export const STATUSES_IMPORT = 'STATUSES_IMPORT'; -export const POLLS_IMPORT = 'POLLS_IMPORT'; export const FILTERS_IMPORT = 'FILTERS_IMPORT'; function pushUnique(array, object) { @@ -25,10 +27,6 @@ export function importFilters(filters) { return { type: FILTERS_IMPORT, filters }; } -export function importPolls(polls) { - return { type: POLLS_IMPORT, polls }; -} - export function importFetchedAccount(account) { return importFetchedAccounts([account]); } @@ -77,7 +75,7 @@ export function importFetchedStatuses(statuses) { } if (status.poll?.id) { - pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id]))); + pushUnique(polls, createPollFromServerJSON(status.poll, getState().polls[status.poll.id])); } if (status.card) { @@ -87,15 +85,9 @@ export function importFetchedStatuses(statuses) { statuses.forEach(processStatus); - dispatch(importPolls(polls)); + dispatch(importPolls({ polls })); dispatch(importFetchedAccounts(accounts)); dispatch(importStatuses(normalStatuses)); dispatch(importFilters(filters)); }; } - -export function importFetchedPoll(poll) { - return (dispatch, getState) => { - dispatch(importPolls([normalizePoll(poll, getState().getIn(['polls', poll.id]))])); - }; -} diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index d9e9fef0c6..b643cf5613 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -1,15 +1,12 @@ import escapeTextContentForBrowser from 'escape-html'; +import { makeEmojiMap } from 'mastodon/models/custom_emoji'; + import emojify from '../../features/emoji/emoji'; import { expandSpoilers, me } from '../../initial_state'; const domParser = new DOMParser(); -const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => { - obj[`:${emoji.shortcode}:`] = emoji; - return obj; -}, {}); - export function searchTextFromRawStatus (status) { const spoilerText = status.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'); @@ -140,38 +137,6 @@ export function normalizeStatusTranslation(translation, status) { return normalTranslation; } -export function normalizePoll(poll, normalOldPoll) { - const normalPoll = { ...poll }; - const emojiMap = makeEmojiMap(poll.emojis); - - normalPoll.options = poll.options.map((option, index) => { - const normalOption = { - ...option, - voted: poll.own_votes && poll.own_votes.includes(index), - titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap), - }; - - if (normalOldPoll && normalOldPoll.getIn(['options', index, 'title']) === option.title) { - normalOption.translation = normalOldPoll.getIn(['options', index, 'translation']); - } - - return normalOption; - }); - - 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.emojis); diff --git a/app/javascript/mastodon/actions/importer/polls.ts b/app/javascript/mastodon/actions/importer/polls.ts new file mode 100644 index 0000000000..5bbe7d57d6 --- /dev/null +++ b/app/javascript/mastodon/actions/importer/polls.ts @@ -0,0 +1,7 @@ +import { createAction } from '@reduxjs/toolkit'; + +import type { Poll } from 'mastodon/models/poll'; + +export const importPolls = createAction<{ polls: Poll[] }>( + 'poll/importMultiple', +); diff --git a/app/javascript/mastodon/actions/lists.js b/app/javascript/mastodon/actions/lists.js index a7cc1a9329..7b6dd22041 100644 --- a/app/javascript/mastodon/actions/lists.js +++ b/app/javascript/mastodon/actions/lists.js @@ -2,9 +2,6 @@ import api from '../api'; -import { showAlertForError } from './alerts'; -import { importFetchedAccounts } from './importer'; - export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST'; export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS'; export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL'; @@ -13,45 +10,10 @@ export const LISTS_FETCH_REQUEST = 'LISTS_FETCH_REQUEST'; export const LISTS_FETCH_SUCCESS = 'LISTS_FETCH_SUCCESS'; export const LISTS_FETCH_FAIL = 'LISTS_FETCH_FAIL'; -export const LIST_EDITOR_TITLE_CHANGE = 'LIST_EDITOR_TITLE_CHANGE'; -export const LIST_EDITOR_RESET = 'LIST_EDITOR_RESET'; -export const LIST_EDITOR_SETUP = 'LIST_EDITOR_SETUP'; - -export const LIST_CREATE_REQUEST = 'LIST_CREATE_REQUEST'; -export const LIST_CREATE_SUCCESS = 'LIST_CREATE_SUCCESS'; -export const LIST_CREATE_FAIL = 'LIST_CREATE_FAIL'; - -export const LIST_UPDATE_REQUEST = 'LIST_UPDATE_REQUEST'; -export const LIST_UPDATE_SUCCESS = 'LIST_UPDATE_SUCCESS'; -export const LIST_UPDATE_FAIL = 'LIST_UPDATE_FAIL'; - export const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST'; export const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS'; export const LIST_DELETE_FAIL = 'LIST_DELETE_FAIL'; -export const LIST_ACCOUNTS_FETCH_REQUEST = 'LIST_ACCOUNTS_FETCH_REQUEST'; -export const LIST_ACCOUNTS_FETCH_SUCCESS = 'LIST_ACCOUNTS_FETCH_SUCCESS'; -export const LIST_ACCOUNTS_FETCH_FAIL = 'LIST_ACCOUNTS_FETCH_FAIL'; - -export const LIST_EDITOR_SUGGESTIONS_CHANGE = 'LIST_EDITOR_SUGGESTIONS_CHANGE'; -export const LIST_EDITOR_SUGGESTIONS_READY = 'LIST_EDITOR_SUGGESTIONS_READY'; -export const LIST_EDITOR_SUGGESTIONS_CLEAR = 'LIST_EDITOR_SUGGESTIONS_CLEAR'; - -export const LIST_EDITOR_ADD_REQUEST = 'LIST_EDITOR_ADD_REQUEST'; -export const LIST_EDITOR_ADD_SUCCESS = 'LIST_EDITOR_ADD_SUCCESS'; -export const LIST_EDITOR_ADD_FAIL = 'LIST_EDITOR_ADD_FAIL'; - -export const LIST_EDITOR_REMOVE_REQUEST = 'LIST_EDITOR_REMOVE_REQUEST'; -export const LIST_EDITOR_REMOVE_SUCCESS = 'LIST_EDITOR_REMOVE_SUCCESS'; -export const LIST_EDITOR_REMOVE_FAIL = 'LIST_EDITOR_REMOVE_FAIL'; - -export const LIST_ADDER_RESET = 'LIST_ADDER_RESET'; -export const LIST_ADDER_SETUP = 'LIST_ADDER_SETUP'; - -export const LIST_ADDER_LISTS_FETCH_REQUEST = 'LIST_ADDER_LISTS_FETCH_REQUEST'; -export const LIST_ADDER_LISTS_FETCH_SUCCESS = 'LIST_ADDER_LISTS_FETCH_SUCCESS'; -export const LIST_ADDER_LISTS_FETCH_FAIL = 'LIST_ADDER_LISTS_FETCH_FAIL'; - export const fetchList = id => (dispatch, getState) => { if (getState().getIn(['lists', id])) { return; @@ -102,94 +64,6 @@ export const fetchListsFail = error => ({ error, }); -export const submitListEditor = shouldReset => (dispatch, getState) => { - const listId = getState().getIn(['listEditor', 'listId']); - const title = getState().getIn(['listEditor', 'title']); - - if (listId === null) { - dispatch(createList(title, shouldReset)); - } else { - dispatch(updateList(listId, title, shouldReset)); - } -}; - -export const setupListEditor = listId => (dispatch, getState) => { - dispatch({ - type: LIST_EDITOR_SETUP, - list: getState().getIn(['lists', listId]), - }); - - dispatch(fetchListAccounts(listId)); -}; - -export const changeListEditorTitle = value => ({ - type: LIST_EDITOR_TITLE_CHANGE, - value, -}); - -export const createList = (title, shouldReset) => (dispatch) => { - dispatch(createListRequest()); - - api().post('/api/v1/lists', { title }).then(({ data }) => { - dispatch(createListSuccess(data)); - - if (shouldReset) { - dispatch(resetListEditor()); - } - }).catch(err => dispatch(createListFail(err))); -}; - -export const createListRequest = () => ({ - type: LIST_CREATE_REQUEST, -}); - -export const createListSuccess = list => ({ - type: LIST_CREATE_SUCCESS, - list, -}); - -export const createListFail = error => ({ - type: LIST_CREATE_FAIL, - error, -}); - -export const updateList = (id, title, shouldReset, isExclusive, replies_policy, notify) => (dispatch) => { - dispatch(updateListRequest(id)); - - api().put(`/api/v1/lists/${id}`, { - title, - replies_policy, - exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive, - notify: typeof notify === 'undefined' ? undefined : !!notify, - }).then(({ data }) => { - dispatch(updateListSuccess(data)); - - if (shouldReset) { - dispatch(resetListEditor()); - } - }).catch(err => dispatch(updateListFail(id, err))); -}; - -export const updateListRequest = id => ({ - type: LIST_UPDATE_REQUEST, - id, -}); - -export const updateListSuccess = list => ({ - type: LIST_UPDATE_SUCCESS, - list, -}); - -export const updateListFail = (id, error) => ({ - type: LIST_UPDATE_FAIL, - id, - error, -}); - -export const resetListEditor = () => ({ - type: LIST_EDITOR_RESET, -}); - export const deleteList = id => (dispatch) => { dispatch(deleteListRequest(id)); @@ -213,166 +87,3 @@ export const deleteListFail = (id, error) => ({ id, error, }); - -export const fetchListAccounts = listId => (dispatch) => { - dispatch(fetchListAccountsRequest(listId)); - - api().get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } }).then(({ data }) => { - dispatch(importFetchedAccounts(data)); - dispatch(fetchListAccountsSuccess(listId, data)); - }).catch(err => dispatch(fetchListAccountsFail(listId, err))); -}; - -export const fetchListAccountsRequest = id => ({ - type: LIST_ACCOUNTS_FETCH_REQUEST, - id, -}); - -export const fetchListAccountsSuccess = (id, accounts, next) => ({ - type: LIST_ACCOUNTS_FETCH_SUCCESS, - id, - accounts, - next, -}); - -export const fetchListAccountsFail = (id, error) => ({ - type: LIST_ACCOUNTS_FETCH_FAIL, - id, - error, -}); - -export const fetchListSuggestions = q => (dispatch) => { - const params = { - q, - resolve: false, - following: true, - }; - - api().get('/api/v1/accounts/search', { params }).then(({ data }) => { - dispatch(importFetchedAccounts(data)); - dispatch(fetchListSuggestionsReady(q, data)); - }).catch(error => dispatch(showAlertForError(error))); -}; - -export const fetchListSuggestionsReady = (query, accounts) => ({ - type: LIST_EDITOR_SUGGESTIONS_READY, - query, - accounts, -}); - -export const clearListSuggestions = () => ({ - type: LIST_EDITOR_SUGGESTIONS_CLEAR, -}); - -export const changeListSuggestions = value => ({ - type: LIST_EDITOR_SUGGESTIONS_CHANGE, - value, -}); - -export const addToListEditor = accountId => (dispatch, getState) => { - dispatch(addToList(getState().getIn(['listEditor', 'listId']), accountId)); -}; - -export const addToList = (listId, accountId) => (dispatch) => { - dispatch(addToListRequest(listId, accountId)); - - api().post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] }) - .then(() => dispatch(addToListSuccess(listId, accountId))) - .catch(err => dispatch(addToListFail(listId, accountId, err))); -}; - -export const addToListRequest = (listId, accountId) => ({ - type: LIST_EDITOR_ADD_REQUEST, - listId, - accountId, -}); - -export const addToListSuccess = (listId, accountId) => ({ - type: LIST_EDITOR_ADD_SUCCESS, - listId, - accountId, -}); - -export const addToListFail = (listId, accountId, error) => ({ - type: LIST_EDITOR_ADD_FAIL, - listId, - accountId, - error, -}); - -export const removeFromListEditor = accountId => (dispatch, getState) => { - dispatch(removeFromList(getState().getIn(['listEditor', 'listId']), accountId)); -}; - -export const removeFromList = (listId, accountId) => (dispatch) => { - dispatch(removeFromListRequest(listId, accountId)); - - api().delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } }) - .then(() => dispatch(removeFromListSuccess(listId, accountId))) - .catch(err => dispatch(removeFromListFail(listId, accountId, err))); -}; - -export const removeFromListRequest = (listId, accountId) => ({ - type: LIST_EDITOR_REMOVE_REQUEST, - listId, - accountId, -}); - -export const removeFromListSuccess = (listId, accountId) => ({ - type: LIST_EDITOR_REMOVE_SUCCESS, - listId, - accountId, -}); - -export const removeFromListFail = (listId, accountId, error) => ({ - type: LIST_EDITOR_REMOVE_FAIL, - listId, - accountId, - error, -}); - -export const resetListAdder = () => ({ - type: LIST_ADDER_RESET, -}); - -export const setupListAdder = accountId => (dispatch, getState) => { - dispatch({ - type: LIST_ADDER_SETUP, - account: getState().getIn(['accounts', accountId]), - }); - dispatch(fetchLists()); - dispatch(fetchAccountLists(accountId)); -}; - -export const fetchAccountLists = accountId => (dispatch) => { - dispatch(fetchAccountListsRequest(accountId)); - - api().get(`/api/v1/accounts/${accountId}/lists`) - .then(({ data }) => dispatch(fetchAccountListsSuccess(accountId, data))) - .catch(err => dispatch(fetchAccountListsFail(accountId, err))); -}; - -export const fetchAccountListsRequest = id => ({ - type:LIST_ADDER_LISTS_FETCH_REQUEST, - id, -}); - -export const fetchAccountListsSuccess = (id, lists) => ({ - type: LIST_ADDER_LISTS_FETCH_SUCCESS, - id, - lists, -}); - -export const fetchAccountListsFail = (id, err) => ({ - type: LIST_ADDER_LISTS_FETCH_FAIL, - id, - err, -}); - -export const addToListAdder = listId => (dispatch, getState) => { - dispatch(addToList(listId, getState().getIn(['listAdder', 'accountId']))); -}; - -export const removeFromListAdder = listId => (dispatch, getState) => { - dispatch(removeFromList(listId, getState().getIn(['listAdder', 'accountId']))); -}; diff --git a/app/javascript/mastodon/actions/lists_typed.ts b/app/javascript/mastodon/actions/lists_typed.ts new file mode 100644 index 0000000000..eca051f52c --- /dev/null +++ b/app/javascript/mastodon/actions/lists_typed.ts @@ -0,0 +1,17 @@ +import { apiCreate, apiUpdate } from 'mastodon/api/lists'; +import type { List } from 'mastodon/models/list'; +import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; + +export const createList = createDataLoadingThunk( + 'list/create', + (list: Partial) => apiCreate(list), +); + +// Kmyblue tracking marker: copied antenna, circle, bookmark_category + +export const updateList = createDataLoadingThunk( + 'list/update', + (list: Partial) => apiUpdate(list), +); + +// Kmyblue tracking marker: copied antenna, circle, bookmark_category diff --git a/app/javascript/mastodon/actions/modal.ts b/app/javascript/mastodon/actions/modal.ts index ab03e46765..49af176a11 100644 --- a/app/javascript/mastodon/actions/modal.ts +++ b/app/javascript/mastodon/actions/modal.ts @@ -9,6 +9,7 @@ export type ModalType = keyof typeof MODAL_COMPONENTS; interface OpenModalPayload { modalType: ModalType; modalProps: ModalProps; + previousModalProps?: ModalProps; } export const openModal = createAction('MODAL_OPEN'); diff --git a/app/javascript/mastodon/actions/notification_groups.ts b/app/javascript/mastodon/actions/notification_groups.ts index 77fbc198e8..c7b192accc 100644 --- a/app/javascript/mastodon/actions/notification_groups.ts +++ b/app/javascript/mastodon/actions/notification_groups.ts @@ -12,7 +12,7 @@ import type { } from 'mastodon/api_types/notifications'; import { allNotificationTypes } from 'mastodon/api_types/notifications'; import type { ApiStatusJSON } from 'mastodon/api_types/statuses'; -import { usePendingItems } from 'mastodon/initial_state'; +import { enableEmojiReaction, usePendingItems } from 'mastodon/initial_state'; import type { NotificationGap } from 'mastodon/reducers/notification_groups'; import { selectSettingsNotificationsExcludedTypes, @@ -37,9 +37,15 @@ function excludeAllTypesExcept(filter: string) { function getExcludedTypes(state: RootState) { const activeFilter = selectSettingsNotificationsQuickFilterActive(state); - return activeFilter === 'all' - ? selectSettingsNotificationsExcludedTypes(state) - : excludeAllTypesExcept(activeFilter); + const types = + activeFilter === 'all' + ? selectSettingsNotificationsExcludedTypes(state) + : excludeAllTypesExcept(activeFilter); + if (!enableEmojiReaction && !types.includes('emoji_reaction')) { + types.push('emoji_reaction'); + } + + return types; } function dispatchAssociatedRecords( diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index f8b2aa13a4..87b842e51f 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -1,57 +1,25 @@ import { IntlMessageFormat } from 'intl-messageformat'; import { defineMessages } from 'react-intl'; -import { List as ImmutableList } from 'immutable'; - -import { compareId } from 'mastodon/compare_id'; -import { enableEmojiReaction, usePendingItems as preferPendingItems } from 'mastodon/initial_state'; - -import api, { getLinks } from '../api'; import { unescapeHTML } from '../utils/html'; import { requestNotificationPermission } from '../utils/notifications'; import { fetchFollowRequests } from './accounts'; import { importFetchedAccount, - importFetchedAccounts, - importFetchedStatus, - importFetchedStatuses, } from './importer'; import { submitMarkers } from './markers'; import { notificationsUpdate } from "./notifications_typed"; import { register as registerPushNotifications } from './push_notifications'; -import { saveSettings } from './settings'; import { STATUS_EMOJI_REACTION_UPDATE } from './statuses'; export * from "./notifications_typed"; -export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; - -export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST'; -export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS'; -export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL'; - export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET'; -export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; -export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING'; - -export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT'; -export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT'; - -export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ'; - export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT'; export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION'; -export const NOTIFICATION_REQUESTS_ACCEPT_REQUEST = 'NOTIFICATION_REQUESTS_ACCEPT_REQUEST'; -export const NOTIFICATION_REQUESTS_ACCEPT_SUCCESS = 'NOTIFICATION_REQUESTS_ACCEPT_SUCCESS'; -export const NOTIFICATION_REQUESTS_ACCEPT_FAIL = 'NOTIFICATION_REQUESTS_ACCEPT_FAIL'; - -export const NOTIFICATION_REQUESTS_DISMISS_REQUEST = 'NOTIFICATION_REQUESTS_DISMISS_REQUEST'; -export const NOTIFICATION_REQUESTS_DISMISS_SUCCESS = 'NOTIFICATION_REQUESTS_DISMISS_SUCCESS'; -export const NOTIFICATION_REQUESTS_DISMISS_FAIL = 'NOTIFICATION_REQUESTS_DISMISS_FAIL'; - const messages = defineMessages({ // mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, group: { id: 'notifications.group', defaultMessage: '{count} notifications' }, @@ -69,10 +37,6 @@ const messages = defineMessages({ message_update: { id: 'notification.update', defaultMessage: '{name} edited a post' }, }); -export const loadPending = () => ({ - type: NOTIFICATIONS_LOAD_PENDING, -}); - export function updateEmojiReactions(emoji_reaction) { return (dispatch) => dispatch({ @@ -83,8 +47,6 @@ export function updateEmojiReactions(emoji_reaction) { export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { - const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']); - const showInColumn = activeFilter === 'all' ? getState().getIn(['settings', 'notifications', 'shows', notification.type], true) : activeFilter === notification.type; const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true); const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true); @@ -93,7 +55,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { if (['mention', 'status'].includes(notification.type) && notification.status.filtered) { const filters = notification.status.filtered.filter(result => result.filter.context.includes('notifications')); - if (filters.some(result => result.filter.filter_action_ex === 'hide')) { + if (filters.some(result => result.filter.filter_action === 'hide')) { return; } @@ -106,25 +68,9 @@ export function updateNotifications(notification, intlMessages, intlLocale) { dispatch(submitMarkers()); - if (showInColumn) { - dispatch(importFetchedAccount(notification.account)); - - if (notification.status) { - dispatch(importFetchedStatus(notification.status)); - } - - if (notification.report) { - dispatch(importFetchedAccount(notification.report.target_account)); - } - - - dispatch(notificationsUpdate({ notification, preferPendingItems, playSound: playSound && !filtered})); - } else if (playSound && !filtered) { - dispatch({ - type: NOTIFICATIONS_UPDATE_NOOP, - meta: { sound: 'boop' }, - }); - } + // `notificationsUpdate` is still used in `user_lists` and `relationships` reducers + dispatch(importFetchedAccount(notification.account)); + dispatch(notificationsUpdate({ notification, playSound: playSound && !filtered})); // Desktop notifications if (typeof window.Notification !== 'undefined' && showAlert && !filtered) { @@ -145,149 +91,8 @@ export function updateNotifications(notification, intlMessages, intlLocale) { }; } -const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); - -const excludeTypesFromFilter = filter => { - const allTypes = ImmutableList([ - 'follow', - 'follow_request', - 'favourite', - 'emoji_reaction', - 'reblog', - 'status_reference', - 'mention', - 'poll', - 'status', - 'list_status', - 'update', - 'admin.sign_up', - 'admin.report', - ]); - - return allTypes.filterNot(item => item === filter).toJS(); -}; - const noOp = () => {}; -let expandNotificationsController = new AbortController(); - -export function expandNotifications({ maxId = undefined, forceLoad = false }) { - return async (dispatch, getState) => { - const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']); - const notifications = getState().get('notifications'); - const isLoadingMore = !!maxId; - - if (notifications.get('isLoading')) { - if (forceLoad) { - expandNotificationsController.abort(); - expandNotificationsController = new AbortController(); - } else { - return; - } - } - - let exclude_types = activeFilter === 'all' - ? excludeTypesFromSettings(getState()) - : excludeTypesFromFilter(activeFilter); - if (!enableEmojiReaction && !exclude_types.includes('emoji_reaction')) { - exclude_types.push('emoji_reaction'); - } - - const params = { - max_id: maxId, - exclude_types, - }; - - if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) { - const a = notifications.getIn(['pendingItems', 0, 'id']); - const b = notifications.getIn(['items', 0, 'id']); - - if (a && b && compareId(a, b) > 0) { - params.since_id = a; - } else { - params.since_id = b || a; - } - } - - const isLoadingRecent = !!params.since_id; - - dispatch(expandNotificationsRequest(isLoadingMore)); - - try { - const response = await api().get('/api/v1/notifications', { params, signal: expandNotificationsController.signal }); - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(importFetchedAccounts(response.data.map(item => item.account))); - dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); - dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account))); - - dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems)); - dispatch(submitMarkers()); - } catch(error) { - dispatch(expandNotificationsFail(error, isLoadingMore)); - } - }; -} - -export function expandNotificationsRequest(isLoadingMore) { - return { - type: NOTIFICATIONS_EXPAND_REQUEST, - skipLoading: !isLoadingMore, - }; -} - -export function expandNotificationsSuccess(notifications, next, isLoadingMore, isLoadingRecent, usePendingItems) { - return { - type: NOTIFICATIONS_EXPAND_SUCCESS, - notifications, - next, - isLoadingRecent: isLoadingRecent, - usePendingItems, - skipLoading: !isLoadingMore, - }; -} - -export function expandNotificationsFail(error, isLoadingMore) { - return { - type: NOTIFICATIONS_EXPAND_FAIL, - error, - skipLoading: !isLoadingMore, - skipAlert: !isLoadingMore || error.name === 'AbortError', - }; -} - -export function scrollTopNotifications(top) { - return { - type: NOTIFICATIONS_SCROLL_TOP, - top, - }; -} - -export function setFilter (filterType) { - return dispatch => { - dispatch({ - type: NOTIFICATIONS_FILTER_SET, - path: ['notifications', 'quickFilter', 'active'], - value: filterType, - }); - dispatch(expandNotifications({ forceLoad: true })); - dispatch(saveSettings()); - }; -} - -export const mountNotifications = () => ({ - type: NOTIFICATIONS_MOUNT, -}); - -export const unmountNotifications = () => ({ - type: NOTIFICATIONS_UNMOUNT, -}); - - -export const markNotificationsAsRead = () => ({ - type: NOTIFICATIONS_MARK_AS_READ, -}); - // Browser support export function setupBrowserNotifications() { return dispatch => { diff --git a/app/javascript/mastodon/actions/notifications_migration.tsx b/app/javascript/mastodon/actions/notifications_migration.tsx deleted file mode 100644 index cd9f5ca3d6..0000000000 --- a/app/javascript/mastodon/actions/notifications_migration.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { createAppAsyncThunk } from 'mastodon/store'; - -import { fetchNotifications } from './notification_groups'; - -export const initializeNotifications = createAppAsyncThunk( - 'notifications/initialize', - (_, { dispatch }) => { - void dispatch(fetchNotifications()); - }, -); diff --git a/app/javascript/mastodon/actions/notifications_typed.ts b/app/javascript/mastodon/actions/notifications_typed.ts index 88d942d45e..3eb1230666 100644 --- a/app/javascript/mastodon/actions/notifications_typed.ts +++ b/app/javascript/mastodon/actions/notifications_typed.ts @@ -9,7 +9,6 @@ export const notificationsUpdate = createAction( ...args }: { notification: ApiNotificationJSON; - usePendingItems: boolean; playSound: boolean; }) => ({ payload: args, diff --git a/app/javascript/mastodon/actions/polls.js b/app/javascript/mastodon/actions/polls.js deleted file mode 100644 index aa49341444..0000000000 --- a/app/javascript/mastodon/actions/polls.js +++ /dev/null @@ -1,61 +0,0 @@ -import api from '../api'; - -import { importFetchedPoll } from './importer'; - -export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST'; -export const POLL_VOTE_SUCCESS = 'POLL_VOTE_SUCCESS'; -export const POLL_VOTE_FAIL = 'POLL_VOTE_FAIL'; - -export const POLL_FETCH_REQUEST = 'POLL_FETCH_REQUEST'; -export const POLL_FETCH_SUCCESS = 'POLL_FETCH_SUCCESS'; -export const POLL_FETCH_FAIL = 'POLL_FETCH_FAIL'; - -export const vote = (pollId, choices) => (dispatch) => { - dispatch(voteRequest()); - - api().post(`/api/v1/polls/${pollId}/votes`, { choices }) - .then(({ data }) => { - dispatch(importFetchedPoll(data)); - dispatch(voteSuccess(data)); - }) - .catch(err => dispatch(voteFail(err))); -}; - -export const fetchPoll = pollId => (dispatch) => { - dispatch(fetchPollRequest()); - - api().get(`/api/v1/polls/${pollId}`) - .then(({ data }) => { - dispatch(importFetchedPoll(data)); - dispatch(fetchPollSuccess(data)); - }) - .catch(err => dispatch(fetchPollFail(err))); -}; - -export const voteRequest = () => ({ - type: POLL_VOTE_REQUEST, -}); - -export const voteSuccess = poll => ({ - type: POLL_VOTE_SUCCESS, - poll, -}); - -export const voteFail = error => ({ - type: POLL_VOTE_FAIL, - error, -}); - -export const fetchPollRequest = () => ({ - type: POLL_FETCH_REQUEST, -}); - -export const fetchPollSuccess = poll => ({ - type: POLL_FETCH_SUCCESS, - poll, -}); - -export const fetchPollFail = error => ({ - type: POLL_FETCH_FAIL, - error, -}); diff --git a/app/javascript/mastodon/actions/polls.ts b/app/javascript/mastodon/actions/polls.ts new file mode 100644 index 0000000000..65a96e8f62 --- /dev/null +++ b/app/javascript/mastodon/actions/polls.ts @@ -0,0 +1,40 @@ +import { apiGetPoll, apiPollVote } from 'mastodon/api/polls'; +import type { ApiPollJSON } from 'mastodon/api_types/polls'; +import { createPollFromServerJSON } from 'mastodon/models/poll'; +import { + createAppAsyncThunk, + createDataLoadingThunk, +} from 'mastodon/store/typed_functions'; + +import { importPolls } from './importer/polls'; + +export const importFetchedPoll = createAppAsyncThunk( + 'poll/importFetched', + (args: { poll: ApiPollJSON }, { dispatch, getState }) => { + const { poll } = args; + + dispatch( + importPolls({ + polls: [createPollFromServerJSON(poll, getState().polls[poll.id])], + }), + ); + }, +); + +export const vote = createDataLoadingThunk( + 'poll/vote', + ({ pollId, choices }: { pollId: string; choices: string[] }) => + apiPollVote(pollId, choices), + async (poll, { dispatch, discardLoadData }) => { + await dispatch(importFetchedPoll({ poll })); + return discardLoadData; + }, +); + +export const fetchPoll = createDataLoadingThunk( + 'poll/fetch', + ({ pollId }: { pollId: string }) => apiGetPoll(pollId), + async (poll, { dispatch }) => { + await dispatch(importFetchedPoll({ poll })); + }, +); diff --git a/app/javascript/mastodon/actions/push_notifications/registerer.js b/app/javascript/mastodon/actions/push_notifications/registerer.js index b3d3850e31..647a6bd9fb 100644 --- a/app/javascript/mastodon/actions/push_notifications/registerer.js +++ b/app/javascript/mastodon/actions/push_notifications/registerer.js @@ -33,7 +33,7 @@ const unsubscribe = ({ registration, subscription }) => subscription ? subscription.unsubscribe().then(() => registration) : registration; const sendSubscriptionToBackend = (subscription) => { - const params = { subscription }; + const params = { subscription: { ...subscription.toJSON(), standard: true } }; if (me) { const data = pushNotificationsSetting.get(me); diff --git a/app/javascript/mastodon/actions/search.js b/app/javascript/mastodon/actions/search.js deleted file mode 100644 index bde17ae0db..0000000000 --- a/app/javascript/mastodon/actions/search.js +++ /dev/null @@ -1,215 +0,0 @@ -import { fromJS } from 'immutable'; - -import { searchHistory } from 'mastodon/settings'; - -import api from '../api'; - -import { fetchRelationships } from './accounts'; -import { importFetchedAccounts, importFetchedStatuses } from './importer'; - -export const SEARCH_CHANGE = 'SEARCH_CHANGE'; -export const SEARCH_CLEAR = 'SEARCH_CLEAR'; -export const SEARCH_SHOW = 'SEARCH_SHOW'; - -export const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST'; -export const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS'; -export const SEARCH_FETCH_FAIL = 'SEARCH_FETCH_FAIL'; - -export const SEARCH_EXPAND_REQUEST = 'SEARCH_EXPAND_REQUEST'; -export const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS'; -export const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL'; - -export const SEARCH_HISTORY_UPDATE = 'SEARCH_HISTORY_UPDATE'; - -export function changeSearch(value) { - return { - type: SEARCH_CHANGE, - value, - }; -} - -export function clearSearch() { - return { - type: SEARCH_CLEAR, - }; -} - -export function submitSearch(type) { - return (dispatch, getState) => { - const value = getState().getIn(['search', 'value']); - const signedIn = !!getState().getIn(['meta', 'me']); - - if (value.length === 0) { - dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, '', type)); - return; - } - - dispatch(fetchSearchRequest(type)); - - api().get('/api/v2/search', { - params: { - q: value, - resolve: signedIn, - limit: 11, - type, - }, - }).then(response => { - if (response.data.accounts) { - dispatch(importFetchedAccounts(response.data.accounts)); - } - - if (response.data.statuses) { - dispatch(importFetchedStatuses(response.data.statuses)); - } - - dispatch(fetchSearchSuccess(response.data, value, type)); - dispatch(fetchRelationships(response.data.accounts.map(item => item.id))); - }).catch(error => { - dispatch(fetchSearchFail(error)); - }); - }; -} - -export function fetchSearchRequest(searchType) { - return { - type: SEARCH_FETCH_REQUEST, - searchType, - }; -} - -export function fetchSearchSuccess(results, searchTerm, searchType) { - return { - type: SEARCH_FETCH_SUCCESS, - results, - searchType, - searchTerm, - }; -} - -export function fetchSearchFail(error) { - return { - type: SEARCH_FETCH_FAIL, - error, - }; -} - -export const expandSearch = type => (dispatch, getState) => { - const value = getState().getIn(['search', 'value']); - const offset = getState().getIn(['search', 'results', type]).size - 1; - - dispatch(expandSearchRequest(type)); - - api().get('/api/v2/search', { - params: { - q: value, - type, - offset, - limit: 11, - }, - }).then(({ data }) => { - if (data.accounts) { - dispatch(importFetchedAccounts(data.accounts)); - } - - if (data.statuses) { - dispatch(importFetchedStatuses(data.statuses)); - } - - dispatch(expandSearchSuccess(data, value, type)); - dispatch(fetchRelationships(data.accounts.map(item => item.id))); - }).catch(error => { - dispatch(expandSearchFail(error)); - }); -}; - -export const expandSearchRequest = (searchType) => ({ - type: SEARCH_EXPAND_REQUEST, - searchType, -}); - -export const expandSearchSuccess = (results, searchTerm, searchType) => ({ - type: SEARCH_EXPAND_SUCCESS, - results, - searchTerm, - searchType, -}); - -export const expandSearchFail = error => ({ - type: SEARCH_EXPAND_FAIL, - error, -}); - -export const showSearch = () => ({ - type: SEARCH_SHOW, -}); - -export const openURL = (value, history, onFailure) => (dispatch, getState) => { - const signedIn = !!getState().getIn(['meta', 'me']); - - if (!signedIn) { - if (onFailure) { - onFailure(); - } - - return; - } - - dispatch(fetchSearchRequest()); - - api().get('/api/v2/search', { params: { q: value, resolve: true } }).then(response => { - if (response.data.accounts?.length > 0) { - dispatch(importFetchedAccounts(response.data.accounts)); - history.push(`/@${response.data.accounts[0].acct}`); - } else if (response.data.statuses?.length > 0) { - dispatch(importFetchedStatuses(response.data.statuses)); - history.push(`/@${response.data.statuses[0].account.acct}/${response.data.statuses[0].id}`); - } else if (onFailure) { - onFailure(); - } - - dispatch(fetchSearchSuccess(response.data, value)); - }).catch(err => { - dispatch(fetchSearchFail(err)); - - if (onFailure) { - onFailure(); - } - }); -}; - -export const clickSearchResult = (q, type) => (dispatch, getState) => { - const previous = getState().getIn(['search', 'recent']); - - if (previous.some(x => x.get('q') === q && x.get('type') === type)) { - return; - } - - const me = getState().getIn(['meta', 'me']); - const current = previous.add(fromJS({ type, q })).takeLast(4); - - searchHistory.set(me, current.toJS()); - dispatch(updateSearchHistory(current)); -}; - -export const forgetSearchResult = q => (dispatch, getState) => { - const previous = getState().getIn(['search', 'recent']); - const me = getState().getIn(['meta', 'me']); - const current = previous.filterNot(result => result.get('q') === q); - - searchHistory.set(me, current.toJS()); - dispatch(updateSearchHistory(current)); -}; - -export const updateSearchHistory = recent => ({ - type: SEARCH_HISTORY_UPDATE, - recent, -}); - -export const hydrateSearch = () => (dispatch, getState) => { - const me = getState().getIn(['meta', 'me']); - const history = searchHistory.get(me); - - if (history !== null) { - dispatch(updateSearchHistory(history)); - } -}; diff --git a/app/javascript/mastodon/actions/search.ts b/app/javascript/mastodon/actions/search.ts new file mode 100644 index 0000000000..13a4ee4432 --- /dev/null +++ b/app/javascript/mastodon/actions/search.ts @@ -0,0 +1,148 @@ +import { createAction } from '@reduxjs/toolkit'; + +import { apiGetSearch } from 'mastodon/api/search'; +import type { ApiSearchType } from 'mastodon/api_types/search'; +import type { + RecentSearch, + SearchType as RecentSearchType, +} from 'mastodon/models/search'; +import { searchHistory } from 'mastodon/settings'; +import { + createDataLoadingThunk, + createAppAsyncThunk, +} from 'mastodon/store/typed_functions'; + +import { fetchRelationships } from './accounts'; +import { importFetchedAccounts, importFetchedStatuses } from './importer'; + +export const SEARCH_HISTORY_UPDATE = 'SEARCH_HISTORY_UPDATE'; + +export const submitSearch = createDataLoadingThunk( + 'search/submit', + async ({ q, type }: { q: string; type?: ApiSearchType }, { getState }) => { + const signedIn = !!getState().meta.get('me'); + + return apiGetSearch({ + q, + type, + resolve: signedIn, + limit: 11, + }); + }, + (data, { dispatch }) => { + if (data.accounts.length > 0) { + dispatch(importFetchedAccounts(data.accounts)); + dispatch(fetchRelationships(data.accounts.map((account) => account.id))); + } + + if (data.statuses.length > 0) { + dispatch(importFetchedStatuses(data.statuses)); + } + + return data; + }, + { + useLoadingBar: false, + }, +); + +export const expandSearch = createDataLoadingThunk( + 'search/expand', + async ({ type }: { type: ApiSearchType }, { getState }) => { + const q = getState().search.q; + const results = getState().search.results; + const offset = results?.[type].length; + + return apiGetSearch({ + q, + type, + limit: 10, + offset, + }); + }, + (data, { dispatch }) => { + if (data.accounts.length > 0) { + dispatch(importFetchedAccounts(data.accounts)); + dispatch(fetchRelationships(data.accounts.map((account) => account.id))); + } + + if (data.statuses.length > 0) { + dispatch(importFetchedStatuses(data.statuses)); + } + + return data; + }, + { + useLoadingBar: true, + }, +); + +export const openURL = createDataLoadingThunk( + 'search/openURL', + ({ url }: { url: string }) => + apiGetSearch({ + q: url, + resolve: true, + limit: 1, + }), + (data, { dispatch }) => { + if (data.accounts.length > 0) { + dispatch(importFetchedAccounts(data.accounts)); + } else if (data.statuses.length > 0) { + dispatch(importFetchedStatuses(data.statuses)); + } + + return data; + }, + { + useLoadingBar: true, + }, +); + +export const clickSearchResult = createAppAsyncThunk( + 'search/clickResult', + ( + { q, type }: { q: string; type?: RecentSearchType }, + { dispatch, getState }, + ) => { + const previous = getState().search.recent; + + if (previous.some((x) => x.q === q && x.type === type)) { + return; + } + + const me = getState().meta.get('me') as string; + const current = [{ type, q }, ...previous].slice(0, 4); + + searchHistory.set(me, current); + dispatch(updateSearchHistory(current)); + }, +); + +export const forgetSearchResult = createAppAsyncThunk( + 'search/forgetResult', + (q: string, { dispatch, getState }) => { + const previous = getState().search.recent; + const me = getState().meta.get('me') as string; + const current = previous.filter((result) => result.q !== q); + + searchHistory.set(me, current); + dispatch(updateSearchHistory(current)); + }, +); + +export const updateSearchHistory = createAction( + 'search/updateHistory', +); + +export const hydrateSearch = createAppAsyncThunk( + 'search/hydrate', + (_args, { dispatch, getState }) => { + const me = getState().meta.get('me') as string; + const history = searchHistory.get(me) as RecentSearch[] | null; + + if (history !== null) { + dispatch(updateSearchHistory(history)); + } + }, +); diff --git a/app/javascript/mastodon/actions/settings.js b/app/javascript/mastodon/actions/settings.js index fbd89f9d4b..7659fb5f98 100644 --- a/app/javascript/mastodon/actions/settings.js +++ b/app/javascript/mastodon/actions/settings.js @@ -29,7 +29,7 @@ const debouncedSave = debounce((dispatch, getState) => { api().put('/api/web/settings', { data }) .then(() => dispatch({ type: SETTING_SAVE })) .catch(error => dispatch(showAlertForError(error))); -}, 5000, { trailing: true }); +}, 2000, { leading: true, trailing: true }); export function saveSettings() { return (dispatch, getState) => debouncedSave(dispatch, getState); diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index 40ead34782..5064e65e7b 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -148,7 +148,7 @@ export function deleteStatus(id, withRedraft = false) { dispatch(deleteStatusRequest(id)); - api().delete(`/api/v1/statuses/${id}`).then(response => { + api().delete(`/api/v1/statuses/${id}`, { params: { delete_media: !withRedraft } }).then(response => { dispatch(deleteStatusSuccess(id)); dispatch(deleteFromTimelines(id)); dispatch(importFetchedAccount(response.data.account)); diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js index 8ab75cdc44..e8fec13453 100644 --- a/app/javascript/mastodon/actions/store.js +++ b/app/javascript/mastodon/actions/store.js @@ -1,4 +1,4 @@ -import { Iterable, fromJS } from 'immutable'; +import { fromJS, isIndexed } from 'immutable'; import { hydrateCompose } from './compose'; import { importFetchedAccounts } from './importer'; @@ -9,8 +9,7 @@ export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY'; const convertState = rawState => fromJS(rawState, (k, v) => - Iterable.isIndexed(v) ? v.toList() : v.toMap()); - + isIndexed(v) ? v.toList() : v.toMap()); export function hydrateStore(rawState) { return dispatch => { diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index a828900ec9..f9d784c2b4 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -11,7 +11,7 @@ import { } from './announcements'; import { updateConversations } from './conversations'; import { processNewNotificationForGroups, refreshStaleNotificationGroups, pollRecentNotifications as pollRecentGroupNotifications } from './notification_groups'; -import { updateNotifications, expandNotifications, updateEmojiReactions } from './notifications'; +import { updateNotifications, updateEmojiReactions } from './notifications'; import { updateStatus } from './statuses'; import { updateTimeline, @@ -111,12 +111,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti // @ts-expect-error dispatch(updateEmojiReactions(JSON.parse(data.payload))); break; - case 'notifications_merged': - const state = getState(); - if (state.notifications.top || !state.notifications.mounted) - dispatch(expandNotifications({ forceLoad: true, maxId: undefined })); + case 'notifications_merged': { dispatch(refreshStaleNotificationGroups()); break; + } case 'conversation': // @ts-expect-error dispatch(updateConversations(JSON.parse(data.payload))); diff --git a/app/javascript/mastodon/actions/suggestions.js b/app/javascript/mastodon/actions/suggestions.js deleted file mode 100644 index 258ffa901d..0000000000 --- a/app/javascript/mastodon/actions/suggestions.js +++ /dev/null @@ -1,58 +0,0 @@ -import api from '../api'; - -import { fetchRelationships } from './accounts'; -import { importFetchedAccounts } from './importer'; - -export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST'; -export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS'; -export const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL'; - -export const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS'; - -export function fetchSuggestions(withRelationships = false) { - return (dispatch) => { - dispatch(fetchSuggestionsRequest()); - - api().get('/api/v2/suggestions', { params: { limit: 20 } }).then(response => { - dispatch(importFetchedAccounts(response.data.map(x => x.account))); - dispatch(fetchSuggestionsSuccess(response.data)); - - if (withRelationships) { - dispatch(fetchRelationships(response.data.map(item => item.account.id))); - } - }).catch(error => dispatch(fetchSuggestionsFail(error))); - }; -} - -export function fetchSuggestionsRequest() { - return { - type: SUGGESTIONS_FETCH_REQUEST, - skipLoading: true, - }; -} - -export function fetchSuggestionsSuccess(suggestions) { - return { - type: SUGGESTIONS_FETCH_SUCCESS, - suggestions, - skipLoading: true, - }; -} - -export function fetchSuggestionsFail(error) { - return { - type: SUGGESTIONS_FETCH_FAIL, - error, - skipLoading: true, - skipAlert: true, - }; -} - -export const dismissSuggestion = accountId => (dispatch) => { - dispatch({ - type: SUGGESTIONS_DISMISS, - id: accountId, - }); - - api().delete(`/api/v1/suggestions/${accountId}`).catch(() => {}); -}; diff --git a/app/javascript/mastodon/actions/suggestions.ts b/app/javascript/mastodon/actions/suggestions.ts new file mode 100644 index 0000000000..0eadfa6b47 --- /dev/null +++ b/app/javascript/mastodon/actions/suggestions.ts @@ -0,0 +1,24 @@ +import { + apiGetSuggestions, + apiDeleteSuggestion, +} from 'mastodon/api/suggestions'; +import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; + +import { fetchRelationships } from './accounts'; +import { importFetchedAccounts } from './importer'; + +export const fetchSuggestions = createDataLoadingThunk( + 'suggestions/fetch', + () => apiGetSuggestions(20), + (data, { dispatch }) => { + dispatch(importFetchedAccounts(data.map((x) => x.account))); + dispatch(fetchRelationships(data.map((x) => x.account.id))); + + return data; + }, +); + +export const dismissSuggestion = createDataLoadingThunk( + 'suggestions/dismiss', + ({ accountId }: { accountId: string }) => apiDeleteSuggestion(accountId), +); diff --git a/app/javascript/mastodon/actions/tags.js b/app/javascript/mastodon/actions/tags.js deleted file mode 100644 index d18d7e514f..0000000000 --- a/app/javascript/mastodon/actions/tags.js +++ /dev/null @@ -1,172 +0,0 @@ -import api, { getLinks } from '../api'; - -export const HASHTAG_FETCH_REQUEST = 'HASHTAG_FETCH_REQUEST'; -export const HASHTAG_FETCH_SUCCESS = 'HASHTAG_FETCH_SUCCESS'; -export const HASHTAG_FETCH_FAIL = 'HASHTAG_FETCH_FAIL'; - -export const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST'; -export const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS'; -export const FOLLOWED_HASHTAGS_FETCH_FAIL = 'FOLLOWED_HASHTAGS_FETCH_FAIL'; - -export const FOLLOWED_HASHTAGS_EXPAND_REQUEST = 'FOLLOWED_HASHTAGS_EXPAND_REQUEST'; -export const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS'; -export const FOLLOWED_HASHTAGS_EXPAND_FAIL = 'FOLLOWED_HASHTAGS_EXPAND_FAIL'; - -export const HASHTAG_FOLLOW_REQUEST = 'HASHTAG_FOLLOW_REQUEST'; -export const HASHTAG_FOLLOW_SUCCESS = 'HASHTAG_FOLLOW_SUCCESS'; -export const HASHTAG_FOLLOW_FAIL = 'HASHTAG_FOLLOW_FAIL'; - -export const HASHTAG_UNFOLLOW_REQUEST = 'HASHTAG_UNFOLLOW_REQUEST'; -export const HASHTAG_UNFOLLOW_SUCCESS = 'HASHTAG_UNFOLLOW_SUCCESS'; -export const HASHTAG_UNFOLLOW_FAIL = 'HASHTAG_UNFOLLOW_FAIL'; - -export const fetchHashtag = name => (dispatch) => { - dispatch(fetchHashtagRequest()); - - api().get(`/api/v1/tags/${name}`).then(({ data }) => { - dispatch(fetchHashtagSuccess(name, data)); - }).catch(err => { - dispatch(fetchHashtagFail(err)); - }); -}; - -export const fetchHashtagRequest = () => ({ - type: HASHTAG_FETCH_REQUEST, -}); - -export const fetchHashtagSuccess = (name, tag) => ({ - type: HASHTAG_FETCH_SUCCESS, - name, - tag, -}); - -export const fetchHashtagFail = error => ({ - type: HASHTAG_FETCH_FAIL, - error, -}); - -export const fetchFollowedHashtags = () => (dispatch) => { - dispatch(fetchFollowedHashtagsRequest()); - - api().get('/api/v1/followed_tags').then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(fetchFollowedHashtagsSuccess(response.data, next ? next.uri : null)); - }).catch(err => { - dispatch(fetchFollowedHashtagsFail(err)); - }); -}; - -export function fetchFollowedHashtagsRequest() { - return { - type: FOLLOWED_HASHTAGS_FETCH_REQUEST, - }; -} - -export function fetchFollowedHashtagsSuccess(followed_tags, next) { - return { - type: FOLLOWED_HASHTAGS_FETCH_SUCCESS, - followed_tags, - next, - }; -} - -export function fetchFollowedHashtagsFail(error) { - return { - type: FOLLOWED_HASHTAGS_FETCH_FAIL, - error, - }; -} - -export function expandFollowedHashtags() { - return (dispatch, getState) => { - const url = getState().getIn(['followed_tags', 'next']); - - if (url === null) { - return; - } - - dispatch(expandFollowedHashtagsRequest()); - - api().get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(expandFollowedHashtagsSuccess(response.data, next ? next.uri : null)); - }).catch(error => { - dispatch(expandFollowedHashtagsFail(error)); - }); - }; -} - -export function expandFollowedHashtagsRequest() { - return { - type: FOLLOWED_HASHTAGS_EXPAND_REQUEST, - }; -} - -export function expandFollowedHashtagsSuccess(followed_tags, next) { - return { - type: FOLLOWED_HASHTAGS_EXPAND_SUCCESS, - followed_tags, - next, - }; -} - -export function expandFollowedHashtagsFail(error) { - return { - type: FOLLOWED_HASHTAGS_EXPAND_FAIL, - error, - }; -} - -export const followHashtag = name => (dispatch) => { - dispatch(followHashtagRequest(name)); - - api().post(`/api/v1/tags/${name}/follow`).then(({ data }) => { - dispatch(followHashtagSuccess(name, data)); - }).catch(err => { - dispatch(followHashtagFail(name, err)); - }); -}; - -export const followHashtagRequest = name => ({ - type: HASHTAG_FOLLOW_REQUEST, - name, -}); - -export const followHashtagSuccess = (name, tag) => ({ - type: HASHTAG_FOLLOW_SUCCESS, - name, - tag, -}); - -export const followHashtagFail = (name, error) => ({ - type: HASHTAG_FOLLOW_FAIL, - name, - error, -}); - -export const unfollowHashtag = name => (dispatch) => { - dispatch(unfollowHashtagRequest(name)); - - api().post(`/api/v1/tags/${name}/unfollow`).then(({ data }) => { - dispatch(unfollowHashtagSuccess(name, data)); - }).catch(err => { - dispatch(unfollowHashtagFail(name, err)); - }); -}; - -export const unfollowHashtagRequest = name => ({ - type: HASHTAG_UNFOLLOW_REQUEST, - name, -}); - -export const unfollowHashtagSuccess = (name, tag) => ({ - type: HASHTAG_UNFOLLOW_SUCCESS, - name, - tag, -}); - -export const unfollowHashtagFail = (name, error) => ({ - type: HASHTAG_UNFOLLOW_FAIL, - name, - error, -}); diff --git a/app/javascript/mastodon/actions/tags_typed.ts b/app/javascript/mastodon/actions/tags_typed.ts new file mode 100644 index 0000000000..6dca32fd84 --- /dev/null +++ b/app/javascript/mastodon/actions/tags_typed.ts @@ -0,0 +1,17 @@ +import { apiGetTag, apiFollowTag, apiUnfollowTag } from 'mastodon/api/tags'; +import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; + +export const fetchHashtag = createDataLoadingThunk( + 'tags/fetch', + ({ tagId }: { tagId: string }) => apiGetTag(tagId), +); + +export const followHashtag = createDataLoadingThunk( + 'tags/follow', + ({ tagId }: { tagId: string }) => apiFollowTag(tagId), +); + +export const unfollowHashtag = createDataLoadingThunk( + 'tags/unfollow', + ({ tagId }: { tagId: string }) => apiUnfollowTag(tagId), +); diff --git a/app/javascript/mastodon/api.ts b/app/javascript/mastodon/api.ts index 51cbe0b695..a41b058d2c 100644 --- a/app/javascript/mastodon/api.ts +++ b/app/javascript/mastodon/api.ts @@ -1,4 +1,9 @@ -import type { AxiosResponse, Method, RawAxiosRequestHeaders } from 'axios'; +import type { + AxiosError, + AxiosResponse, + Method, + RawAxiosRequestHeaders, +} from 'axios'; import axios from 'axios'; import LinkHeader from 'http-link-header'; @@ -41,7 +46,7 @@ const authorizationTokenFromInitialState = (): RawAxiosRequestHeaders => { // eslint-disable-next-line import/no-default-export export default function api(withAuthorization = true) { - return axios.create({ + const instance = axios.create({ transitional: { clarifyTimeoutError: true, }, @@ -60,6 +65,22 @@ export default function api(withAuthorization = true) { }, ], }); + + instance.interceptors.response.use( + (response: AxiosResponse) => { + if (response.headers.deprecation) { + console.warn( + `Deprecated request: ${response.config.method} ${response.config.url}`, + ); + } + return response; + }, + (error: AxiosError) => { + return Promise.reject(error); + }, + ); + + return instance; } type RequestParamsOrData = Record; @@ -68,6 +89,7 @@ export async function apiRequest( method: Method, url: string, args: { + signal?: AbortSignal; params?: RequestParamsOrData; data?: RequestParamsOrData; timeout?: number; diff --git a/app/javascript/mastodon/api/accounts.ts b/app/javascript/mastodon/api/accounts.ts index bd1757e827..717010ba74 100644 --- a/app/javascript/mastodon/api/accounts.ts +++ b/app/javascript/mastodon/api/accounts.ts @@ -5,3 +5,16 @@ export const apiSubmitAccountNote = (id: string, value: string) => apiRequestPost(`v1/accounts/${id}/note`, { comment: value, }); + +export const apiFollowAccount = ( + id: string, + params?: { + reblogs: boolean; + }, +) => + apiRequestPost(`v1/accounts/${id}/follow`, { + ...params, + }); + +export const apiUnfollowAccount = (id: string) => + apiRequestPost(`v1/accounts/${id}/unfollow`); diff --git a/app/javascript/mastodon/api/antennas.ts b/app/javascript/mastodon/api/antennas.ts new file mode 100644 index 0000000000..61fd84185d --- /dev/null +++ b/app/javascript/mastodon/api/antennas.ts @@ -0,0 +1,143 @@ +import { + apiRequestPost, + apiRequestPut, + apiRequestGet, + apiRequestDelete, +} from 'mastodon/api'; +import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; +import type { ApiAntennaJSON } from 'mastodon/api_types/antennas'; + +export const apiCreate = (antenna: Partial) => + apiRequestPost('v1/antennas', antenna); + +export const apiUpdate = (antenna: Partial) => + apiRequestPut(`v1/antennas/${antenna.id}`, antenna); + +export const apiGetAccounts = (antennaId: string) => + apiRequestGet(`v1/antennas/${antennaId}/accounts`, { + limit: 0, + }); + +export const apiGetExcludeAccounts = (antennaId: string) => + apiRequestGet(`v1/antennas/${antennaId}/exclude_accounts`, { + limit: 0, + }); + +export const apiGetDomains = (antennaId: string) => + apiRequestGet<{ domains: string[]; exclude_domains: string[] }>( + `v1/antennas/${antennaId}/domains`, + { + limit: 0, + }, + ); + +export const apiAddDomain = (antennaId: string, domain: string) => + apiRequestPost(`v1/antennas/${antennaId}/domains`, { + domains: [domain], + }); + +export const apiRemoveDomain = (antennaId: string, domain: string) => + apiRequestDelete(`v1/antennas/${antennaId}/domains`, { + domains: [domain], + }); + +export const apiAddExcludeDomain = (antennaId: string, domain: string) => + apiRequestPost(`v1/antennas/${antennaId}/exclude_domains`, { + domains: [domain], + }); + +export const apiRemoveExcludeDomain = (antennaId: string, domain: string) => + apiRequestDelete(`v1/antennas/${antennaId}/exclude_domains`, { + domains: [domain], + }); + +export const apiGetTags = (antennaId: string) => + apiRequestGet<{ tags: string[]; exclude_tags: string[] }>( + `v1/antennas/${antennaId}/tags`, + { + limit: 0, + }, + ); + +export const apiAddTag = (antennaId: string, tag: string) => + apiRequestPost(`v1/antennas/${antennaId}/tags`, { + tags: [tag], + }); + +export const apiRemoveTag = (antennaId: string, tag: string) => + apiRequestDelete(`v1/antennas/${antennaId}/tags`, { + tags: [tag], + }); + +export const apiAddExcludeTag = (antennaId: string, tag: string) => + apiRequestPost(`v1/antennas/${antennaId}/exclude_tags`, { + tags: [tag], + }); + +export const apiRemoveExcludeTag = (antennaId: string, tag: string) => + apiRequestDelete(`v1/antennas/${antennaId}/exclude_tags`, { + tags: [tag], + }); + +export const apiGetKeywords = (antennaId: string) => + apiRequestGet<{ keywords: string[]; exclude_keywords: string[] }>( + `v1/antennas/${antennaId}/keywords`, + { + limit: 0, + }, + ); + +export const apiAddKeyword = (antennaId: string, keyword: string) => + apiRequestPost(`v1/antennas/${antennaId}/keywords`, { + keywords: [keyword], + }); + +export const apiRemoveKeyword = (antennaId: string, keyword: string) => + apiRequestDelete(`v1/antennas/${antennaId}/keywords`, { + keywords: [keyword], + }); + +export const apiAddExcludeKeyword = (antennaId: string, keyword: string) => + apiRequestPost(`v1/antennas/${antennaId}/exclude_keywords`, { + keywords: [keyword], + }); + +export const apiRemoveExcludeKeyword = (antennaId: string, keyword: string) => + apiRequestDelete(`v1/antennas/${antennaId}/exclude_keywords`, { + keywords: [keyword], + }); + +export const apiGetAccountAntennas = (accountId: string) => + apiRequestGet(`v1/accounts/${accountId}/antennas`); + +export const apiAddAccountToAntenna = (antennaId: string, accountId: string) => + apiRequestPost(`v1/antennas/${antennaId}/accounts`, { + account_ids: [accountId], + }); + +export const apiRemoveAccountFromAntenna = ( + antennaId: string, + accountId: string, +) => + apiRequestDelete(`v1/antennas/${antennaId}/accounts`, { + account_ids: [accountId], + }); + +export const apiGetExcludeAccountAntennas = (accountId: string) => + apiRequestGet(`v1/accounts/${accountId}/exclude_antennas`); + +export const apiAddExcludeAccountToAntenna = ( + antennaId: string, + accountId: string, +) => + apiRequestPost(`v1/antennas/${antennaId}/exclude_accounts`, { + account_ids: [accountId], + }); + +export const apiRemoveExcludeAccountFromAntenna = ( + antennaId: string, + accountId: string, +) => + apiRequestDelete(`v1/antennas/${antennaId}/exclude_accounts`, { + account_ids: [accountId], + }); diff --git a/app/javascript/mastodon/api/bookmark_categories.ts b/app/javascript/mastodon/api/bookmark_categories.ts new file mode 100644 index 0000000000..d6d3394b3a --- /dev/null +++ b/app/javascript/mastodon/api/bookmark_categories.ts @@ -0,0 +1,49 @@ +import { + apiRequestPost, + apiRequestPut, + apiRequestGet, + apiRequestDelete, +} from 'mastodon/api'; +import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; +import type { ApiBookmarkCategoryJSON } from 'mastodon/api_types/bookmark_categories'; + +export const apiCreate = (bookmarkCategory: Partial) => + apiRequestPost( + 'v1/bookmark_categories', + bookmarkCategory, + ); + +export const apiUpdate = (bookmarkCategory: Partial) => + apiRequestPut( + `v1/bookmark_categories/${bookmarkCategory.id}`, + bookmarkCategory, + ); + +export const apiGetStatuses = (bookmarkCategoryId: string) => + apiRequestGet( + `v1/bookmark_categories/${bookmarkCategoryId}/statuses`, + { + limit: 0, + }, + ); + +export const apiGetStatusBookmarkCategories = (accountId: string) => + apiRequestGet( + `v1/statuses/${accountId}/bookmark_categories`, + ); + +export const apiAddStatusToBookmarkCategory = ( + bookmarkCategoryId: string, + statusId: string, +) => + apiRequestPost(`v1/bookmark_categories/${bookmarkCategoryId}/statuses`, { + status_ids: [statusId], + }); + +export const apiRemoveStatusFromBookmarkCategory = ( + bookmarkCategoryId: string, + statusId: string, +) => + apiRequestDelete(`v1/bookmark_categories/${bookmarkCategoryId}/statuses`, { + status_ids: [statusId], + }); diff --git a/app/javascript/mastodon/api/circles.ts b/app/javascript/mastodon/api/circles.ts new file mode 100644 index 0000000000..04971e1e6b --- /dev/null +++ b/app/javascript/mastodon/api/circles.ts @@ -0,0 +1,35 @@ +import { + apiRequestPost, + apiRequestPut, + apiRequestGet, + apiRequestDelete, +} from 'mastodon/api'; +import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; +import type { ApiCircleJSON } from 'mastodon/api_types/circles'; + +export const apiCreate = (circle: Partial) => + apiRequestPost('v1/circles', circle); + +export const apiUpdate = (circle: Partial) => + apiRequestPut(`v1/circles/${circle.id}`, circle); + +export const apiGetAccounts = (circleId: string) => + apiRequestGet(`v1/circles/${circleId}/accounts`, { + limit: 0, + }); + +export const apiGetAccountCircles = (accountId: string) => + apiRequestGet(`v1/accounts/${accountId}/circles`); + +export const apiAddAccountToCircle = (circleId: string, accountId: string) => + apiRequestPost(`v1/circles/${circleId}/accounts`, { + account_ids: [accountId], + }); + +export const apiRemoveAccountFromCircle = ( + circleId: string, + accountId: string, +) => + apiRequestDelete(`v1/circles/${circleId}/accounts`, { + account_ids: [accountId], + }); diff --git a/app/javascript/mastodon/api/compose.ts b/app/javascript/mastodon/api/compose.ts new file mode 100644 index 0000000000..757e9961c9 --- /dev/null +++ b/app/javascript/mastodon/api/compose.ts @@ -0,0 +1,7 @@ +import { apiRequestPut } from 'mastodon/api'; +import type { ApiMediaAttachmentJSON } from 'mastodon/api_types/media_attachments'; + +export const apiUpdateMedia = ( + id: string, + params?: { description?: string; focus?: string }, +) => apiRequestPut(`v1/media/${id}`, params); diff --git a/app/javascript/mastodon/api/domain_blocks.ts b/app/javascript/mastodon/api/domain_blocks.ts new file mode 100644 index 0000000000..4e153b0ee9 --- /dev/null +++ b/app/javascript/mastodon/api/domain_blocks.ts @@ -0,0 +1,13 @@ +import api, { getLinks } from 'mastodon/api'; + +export const apiGetDomainBlocks = async (url?: string) => { + const response = await api().request({ + method: 'GET', + url: url ?? '/api/v1/domain_blocks', + }); + + return { + domains: response.data, + links: getLinks(response), + }; +}; diff --git a/app/javascript/mastodon/api/instance.ts b/app/javascript/mastodon/api/instance.ts new file mode 100644 index 0000000000..764e8daab2 --- /dev/null +++ b/app/javascript/mastodon/api/instance.ts @@ -0,0 +1,15 @@ +import { apiRequestGet } from 'mastodon/api'; +import type { + ApiTermsOfServiceJSON, + ApiPrivacyPolicyJSON, +} from 'mastodon/api_types/instance'; + +export const apiGetTermsOfService = (version?: string) => + apiRequestGet( + version + ? `v1/instance/terms_of_service/${version}` + : 'v1/instance/terms_of_service', + ); + +export const apiGetPrivacyPolicy = () => + apiRequestGet('v1/instance/privacy_policy'); diff --git a/app/javascript/mastodon/api/lists.ts b/app/javascript/mastodon/api/lists.ts new file mode 100644 index 0000000000..a5586eb6d4 --- /dev/null +++ b/app/javascript/mastodon/api/lists.ts @@ -0,0 +1,32 @@ +import { + apiRequestPost, + apiRequestPut, + apiRequestGet, + apiRequestDelete, +} from 'mastodon/api'; +import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; +import type { ApiListJSON } from 'mastodon/api_types/lists'; + +export const apiCreate = (list: Partial) => + apiRequestPost('v1/lists', list); + +export const apiUpdate = (list: Partial) => + apiRequestPut(`v1/lists/${list.id}`, list); + +export const apiGetAccounts = (listId: string) => + apiRequestGet(`v1/lists/${listId}/accounts`, { + limit: 0, + }); + +export const apiGetAccountLists = (accountId: string) => + apiRequestGet(`v1/accounts/${accountId}/lists`); + +export const apiAddAccountToList = (listId: string, accountId: string) => + apiRequestPost(`v1/lists/${listId}/accounts`, { + account_ids: [accountId], + }); + +export const apiRemoveAccountFromList = (listId: string, accountId: string) => + apiRequestDelete(`v1/lists/${listId}/accounts`, { + account_ids: [accountId], + }); diff --git a/app/javascript/mastodon/api/polls.ts b/app/javascript/mastodon/api/polls.ts new file mode 100644 index 0000000000..cb659986f5 --- /dev/null +++ b/app/javascript/mastodon/api/polls.ts @@ -0,0 +1,10 @@ +import { apiRequestGet, apiRequestPost } from 'mastodon/api'; +import type { ApiPollJSON } from 'mastodon/api_types/polls'; + +export const apiGetPoll = (pollId: string) => + apiRequestGet(`/v1/polls/${pollId}`); + +export const apiPollVote = (pollId: string, choices: string[]) => + apiRequestPost(`/v1/polls/${pollId}/votes`, { + choices, + }); diff --git a/app/javascript/mastodon/api/search.ts b/app/javascript/mastodon/api/search.ts new file mode 100644 index 0000000000..79b0385fe8 --- /dev/null +++ b/app/javascript/mastodon/api/search.ts @@ -0,0 +1,16 @@ +import { apiRequestGet } from 'mastodon/api'; +import type { + ApiSearchType, + ApiSearchResultsJSON, +} from 'mastodon/api_types/search'; + +export const apiGetSearch = (params: { + q: string; + resolve?: boolean; + type?: ApiSearchType; + limit?: number; + offset?: number; +}) => + apiRequestGet('v2/search', { + ...params, + }); diff --git a/app/javascript/mastodon/api/suggestions.ts b/app/javascript/mastodon/api/suggestions.ts new file mode 100644 index 0000000000..d4817698cc --- /dev/null +++ b/app/javascript/mastodon/api/suggestions.ts @@ -0,0 +1,8 @@ +import { apiRequestGet, apiRequestDelete } from 'mastodon/api'; +import type { ApiSuggestionJSON } from 'mastodon/api_types/suggestions'; + +export const apiGetSuggestions = (limit: number) => + apiRequestGet('v2/suggestions', { limit }); + +export const apiDeleteSuggestion = (accountId: string) => + apiRequestDelete(`v1/suggestions/${accountId}`); diff --git a/app/javascript/mastodon/api/tags.ts b/app/javascript/mastodon/api/tags.ts new file mode 100644 index 0000000000..4b111def81 --- /dev/null +++ b/app/javascript/mastodon/api/tags.ts @@ -0,0 +1,23 @@ +import api, { getLinks, apiRequestPost, apiRequestGet } from 'mastodon/api'; +import type { ApiHashtagJSON } from 'mastodon/api_types/tags'; + +export const apiGetTag = (tagId: string) => + apiRequestGet(`v1/tags/${tagId}`); + +export const apiFollowTag = (tagId: string) => + apiRequestPost(`v1/tags/${tagId}/follow`); + +export const apiUnfollowTag = (tagId: string) => + apiRequestPost(`v1/tags/${tagId}/unfollow`); + +export const apiGetFollowedTags = async (url?: string) => { + const response = await api().request({ + method: 'GET', + url: url ?? '/api/v1/followed_tags', + }); + + return { + tags: response.data, + links: getLinks(response), + }; +}; diff --git a/app/javascript/mastodon/api_types/accounts.ts b/app/javascript/mastodon/api_types/accounts.ts index 80d575cad6..9d7974eda0 100644 --- a/app/javascript/mastodon/api_types/accounts.ts +++ b/app/javascript/mastodon/api_types/accounts.ts @@ -45,7 +45,7 @@ export interface BaseApiAccountJSON { avatar_static: string; bot: boolean; created_at: string; - discoverable: boolean; + discoverable?: boolean; indexable: boolean; display_name: string; emojis: ApiCustomEmojiJSON[]; diff --git a/app/javascript/mastodon/api_types/antennas.ts b/app/javascript/mastodon/api_types/antennas.ts new file mode 100644 index 0000000000..a2a8a997ba --- /dev/null +++ b/app/javascript/mastodon/api_types/antennas.ts @@ -0,0 +1,17 @@ +// See app/serializers/rest/antenna_serializer.rb + +import type { ApiListJSON } from './lists'; + +export interface ApiAntennaJSON { + id: string; + title: string; + stl: boolean; + ltl: boolean; + insert_feeds: boolean; + with_media_only: boolean; + ignore_reblog: boolean; + favourite: boolean; + list: ApiListJSON | null; + + list_id: string | undefined; +} diff --git a/app/javascript/mastodon/api_types/bookmark_categories.ts b/app/javascript/mastodon/api_types/bookmark_categories.ts new file mode 100644 index 0000000000..5407b6b125 --- /dev/null +++ b/app/javascript/mastodon/api_types/bookmark_categories.ts @@ -0,0 +1,6 @@ +// See app/serializers/rest/bookmark_category_serializer.rb + +export interface ApiBookmarkCategoryJSON { + id: string; + title: string; +} diff --git a/app/javascript/mastodon/api_types/circles.ts b/app/javascript/mastodon/api_types/circles.ts new file mode 100644 index 0000000000..9905d480b8 --- /dev/null +++ b/app/javascript/mastodon/api_types/circles.ts @@ -0,0 +1,6 @@ +// See app/serializers/rest/circle_serializer.rb + +export interface ApiCircleJSON { + id: string; + title: string; +} diff --git a/app/javascript/mastodon/api_types/dummy_types.ts b/app/javascript/mastodon/api_types/dummy_types.ts deleted file mode 100644 index 1f8c4db6f2..0000000000 --- a/app/javascript/mastodon/api_types/dummy_types.ts +++ /dev/null @@ -1,11 +0,0 @@ -// A similar definition will eventually be added in the main house. These definitions will replace it. - -export interface ApiListJSON_KmyDummy { - id: string; - title: string; - exclusive: boolean; - notify: boolean; - - // replies_policy - // antennas -} diff --git a/app/javascript/mastodon/api_types/instance.ts b/app/javascript/mastodon/api_types/instance.ts new file mode 100644 index 0000000000..3a29684b70 --- /dev/null +++ b/app/javascript/mastodon/api_types/instance.ts @@ -0,0 +1,11 @@ +export interface ApiTermsOfServiceJSON { + effective_date: string; + effective: boolean; + succeeded_by: string | null; + content: string; +} + +export interface ApiPrivacyPolicyJSON { + updated_at: string; + content: string; +} diff --git a/app/javascript/mastodon/api_types/lists.ts b/app/javascript/mastodon/api_types/lists.ts new file mode 100644 index 0000000000..bc32b33883 --- /dev/null +++ b/app/javascript/mastodon/api_types/lists.ts @@ -0,0 +1,15 @@ +// See app/serializers/rest/list_serializer.rb + +import type { ApiAntennaJSON } from './antennas'; + +export type RepliesPolicyType = 'list' | 'followed' | 'none'; + +export interface ApiListJSON { + id: string; + title: string; + exclusive: boolean; + replies_policy: RepliesPolicyType; + notify: boolean; + favourite: boolean; + antennas?: ApiAntennaJSON[]; +} diff --git a/app/javascript/mastodon/api_types/notifications.ts b/app/javascript/mastodon/api_types/notifications.ts index 89ce9ee497..41daed25ad 100644 --- a/app/javascript/mastodon/api_types/notifications.ts +++ b/app/javascript/mastodon/api_types/notifications.ts @@ -3,7 +3,7 @@ import type { AccountWarningAction } from 'mastodon/models/notification_group'; import type { ApiAccountJSON } from './accounts'; -import type { ApiListJSON_KmyDummy } from './dummy_types'; +import type { ApiListJSON } from './lists'; import type { ApiReportJSON } from './reports'; import type { ApiStatusJSON } from './statuses'; @@ -24,6 +24,7 @@ export const allNotificationTypes = [ 'admin.report', 'moderation_warning', 'severed_relationships', + 'annual_report', ]; export type NotificationWithStatusType = @@ -44,7 +45,8 @@ export type NotificationType = | 'moderation_warning' | 'severed_relationships' | 'admin.sign_up' - | 'admin.report'; + | 'admin.report' + | 'annual_report'; export interface NotifyEmojiReactionJSON { name: string; @@ -69,7 +71,7 @@ export interface BaseNotificationJSON { group_key: string; account: ApiAccountJSON; emoji_reaction?: NotifyEmojiReactionJSON; - list?: ApiListJSON_KmyDummy; + list?: ApiListJSON; } export interface BaseNotificationGroupJSON { @@ -82,7 +84,7 @@ export interface BaseNotificationGroupJSON { page_min_id?: string; page_max_id?: string; emoji_reaction_groups?: NotificationEmojiReactionGroupJSON[]; - list?: ApiListJSON_KmyDummy; + list?: ApiListJSON; } interface NotificationGroupWithStatusJSON extends BaseNotificationGroupJSON { @@ -158,6 +160,15 @@ interface AccountRelationshipSeveranceNotificationJSON event: ApiAccountRelationshipSeveranceEventJSON; } +export interface ApiAnnualReportEventJSON { + year: string; +} + +interface AnnualReportNotificationGroupJSON extends BaseNotificationGroupJSON { + type: 'annual_report'; + annual_report: ApiAnnualReportEventJSON; +} + export type ApiNotificationJSON = | SimpleNotificationJSON | ReportNotificationJSON @@ -170,7 +181,8 @@ export type ApiNotificationGroupJSON = | ReportNotificationGroupJSON | AccountRelationshipSeveranceNotificationGroupJSON | NotificationGroupWithStatusJSON - | ModerationWarningNotificationGroupJSON; + | ModerationWarningNotificationGroupJSON + | AnnualReportNotificationGroupJSON; export interface ApiNotificationGroupsResultJSON { accounts: ApiAccountJSON[]; diff --git a/app/javascript/mastodon/api_types/polls.ts b/app/javascript/mastodon/api_types/polls.ts index 8181f7b813..891a2faba7 100644 --- a/app/javascript/mastodon/api_types/polls.ts +++ b/app/javascript/mastodon/api_types/polls.ts @@ -13,11 +13,11 @@ export interface ApiPollJSON { expired: boolean; multiple: boolean; votes_count: number; - voters_count: number; + voters_count: number | null; options: ApiPollOptionJSON[]; emojis: ApiCustomEmojiJSON[]; - voted: boolean; - own_votes: number[]; + voted?: boolean; + own_votes?: number[]; } diff --git a/app/javascript/mastodon/api_types/search.ts b/app/javascript/mastodon/api_types/search.ts new file mode 100644 index 0000000000..795cbb2b41 --- /dev/null +++ b/app/javascript/mastodon/api_types/search.ts @@ -0,0 +1,11 @@ +import type { ApiAccountJSON } from './accounts'; +import type { ApiStatusJSON } from './statuses'; +import type { ApiHashtagJSON } from './tags'; + +export type ApiSearchType = 'accounts' | 'statuses' | 'hashtags'; + +export interface ApiSearchResultsJSON { + accounts: ApiAccountJSON[]; + statuses: ApiStatusJSON[]; + hashtags: ApiHashtagJSON[]; +} diff --git a/app/javascript/mastodon/api_types/suggestions.ts b/app/javascript/mastodon/api_types/suggestions.ts new file mode 100644 index 0000000000..7d91daf901 --- /dev/null +++ b/app/javascript/mastodon/api_types/suggestions.ts @@ -0,0 +1,13 @@ +import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; + +export type ApiSuggestionSourceJSON = + | 'featured' + | 'most_followed' + | 'most_interactions' + | 'similar_to_recently_followed' + | 'friends_of_friends'; + +export interface ApiSuggestionJSON { + sources: [ApiSuggestionSourceJSON, ...ApiSuggestionSourceJSON[]]; + account: ApiAccountJSON; +} diff --git a/app/javascript/mastodon/api_types/tags.ts b/app/javascript/mastodon/api_types/tags.ts new file mode 100644 index 0000000000..0c16c8bd28 --- /dev/null +++ b/app/javascript/mastodon/api_types/tags.ts @@ -0,0 +1,13 @@ +interface ApiHistoryJSON { + day: string; + accounts: string; + uses: string; +} + +export interface ApiHashtagJSON { + id: string; + name: string; + url: string; + history: [ApiHistoryJSON, ...ApiHistoryJSON[]]; + following?: boolean; +} diff --git a/app/javascript/mastodon/components/account.jsx b/app/javascript/mastodon/components/account.jsx deleted file mode 100644 index 6204dcdf35..0000000000 --- a/app/javascript/mastodon/components/account.jsx +++ /dev/null @@ -1,194 +0,0 @@ -import PropTypes from 'prop-types'; -import { useCallback } from 'react'; - -import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; - -import classNames from 'classnames'; -import { Link } from 'react-router-dom'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; - -import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; -import { EmptyAccount } from 'mastodon/components/empty_account'; -import { ShortNumber } from 'mastodon/components/short_number'; -import { VerifiedBadge } from 'mastodon/components/verified_badge'; - -import DropdownMenuContainer from '../containers/dropdown_menu_container'; -import { me } from '../initial_state'; - -import { Avatar } from './avatar'; -import { Button } from './button'; -import { FollowersCounter } from './counters'; -import { DisplayName } from './display_name'; -import { RelativeTimestamp } from './relative_timestamp'; - -const messages = defineMessages({ - follow: { id: 'account.follow', defaultMessage: 'Follow' }, - unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' }, - unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' }, - unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' }, - mute_notifications: { id: 'account.mute_notifications_short', defaultMessage: 'Mute notifications' }, - unmute_notifications: { id: 'account.unmute_notifications_short', defaultMessage: 'Unmute notifications' }, - mute: { id: 'account.mute_short', defaultMessage: 'Mute' }, - block: { id: 'account.block_short', defaultMessage: 'Block' }, - more: { id: 'status.more', defaultMessage: 'More' }, -}); - -const Account = ({ size = 46, account, onFollow, onBlock, onMute, onMuteNotifications, hidden, hideButtons, minimal, defaultAction, children, withBio }) => { - const intl = useIntl(); - - const handleFollow = useCallback(() => { - onFollow(account); - }, [onFollow, account]); - - const handleBlock = useCallback(() => { - onBlock(account); - }, [onBlock, account]); - - const handleMute = useCallback(() => { - onMute(account); - }, [onMute, account]); - - const handleMuteNotifications = useCallback(() => { - onMuteNotifications(account, true); - }, [onMuteNotifications, account]); - - const handleUnmuteNotifications = useCallback(() => { - onMuteNotifications(account, false); - }, [onMuteNotifications, account]); - - if (!account) { - return ; - } - - if (hidden) { - return ( - <> - {account.get('display_name')} - {account.get('username')} - - ); - } - - let buttons; - - if (!hideButtons && account.get('id') !== me && 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 = + )} + + + ); +}; + +export const AlertsController: React.FC = () => { + const alerts = useAppSelector((state) => state.alerts); + + if (alerts.length === 0) { + return null; + } + + return ( +

+ {alerts.map((alert, idx) => ( + + ))} +
+ ); +}; diff --git a/app/javascript/mastodon/components/alt_text_badge.tsx b/app/javascript/mastodon/components/alt_text_badge.tsx index 99bec1ee51..701cfbe8b4 100644 --- a/app/javascript/mastodon/components/alt_text_badge.tsx +++ b/app/javascript/mastodon/components/alt_text_badge.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useRef } from 'react'; +import { useState, useCallback, useRef, useId } from 'react'; import { FormattedMessage } from 'react-intl'; @@ -8,12 +8,15 @@ import type { UsePopperOptions, } from 'react-overlays/esm/usePopper'; +import { useSelectableClick } from 'mastodon/hooks/useSelectableClick'; + const offset = [0, 4] as OffsetValue; const popperConfig = { strategy: 'fixed' } as UsePopperOptions; export const AltTextBadge: React.FC<{ description: string; }> = ({ description }) => { + const accessibilityId = useId(); const anchorRef = useRef(null); const [open, setOpen] = useState(false); @@ -25,12 +28,16 @@ export const AltTextBadge: React.FC<{ setOpen(false); }, [setOpen]); + const [handleMouseDown, handleMouseUp] = useSelectableClick(handleClose); + return ( <> @@ -47,9 +54,12 @@ export const AltTextBadge: React.FC<{ > {({ props }) => (
-