diff --git a/.bundler-audit.yml b/.bundler-audit.yml new file mode 100644 index 0000000000..0671df390f --- /dev/null +++ b/.bundler-audit.yml @@ -0,0 +1,6 @@ +--- +ignore: + # devise-two-factor advisory about brute-forcing TOTP + # We have rate-limits on authentication endpoints in place (including second + # factor verification) since Mastodon v3.2.0 + - CVE-2024-0227 diff --git a/.devcontainer/codespaces/devcontainer.json b/.devcontainer/codespaces/devcontainer.json index ca9156fdaa..b32e4026d2 100644 --- a/.devcontainer/codespaces/devcontainer.json +++ b/.devcontainer/codespaces/devcontainer.json @@ -5,7 +5,7 @@ "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", "features": { - "ghcr.io/devcontainers/features/sshd:1": {} + "ghcr.io/devcontainers/features/sshd:1": {}, }, "runServices": ["app", "db", "redis"], @@ -15,16 +15,16 @@ "portsAttributes": { "3000": { "label": "web", - "onAutoForward": "notify" + "onAutoForward": "notify", }, "4000": { "label": "stream", - "onAutoForward": "silent" - } + "onAutoForward": "silent", + }, }, "otherPortsAttributes": { - "onAutoForward": "silent" + "onAutoForward": "silent", }, "remoteEnv": { @@ -33,7 +33,7 @@ "STREAMING_API_BASE_URL": "https://${localEnv:CODESPACE_NAME}-4000.app.github.dev", "DISABLE_FORGERY_REQUEST_PROTECTION": "true", "ES_ENABLED": "", - "LIBRE_TRANSLATE_ENDPOINT": "" + "LIBRE_TRANSLATE_ENDPOINT": "", }, "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", @@ -43,7 +43,7 @@ "customizations": { "vscode": { "settings": {}, - "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"] - } - } + "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"], + }, + }, } diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index fa8d6542c1..ed71235b3b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,7 +5,7 @@ "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", "features": { - "ghcr.io/devcontainers/features/sshd:1": {} + "ghcr.io/devcontainers/features/sshd:1": {}, }, "forwardPorts": [3000, 4000], @@ -14,17 +14,17 @@ "3000": { "label": "web", "onAutoForward": "notify", - "requireLocalPort": true + "requireLocalPort": true, }, "4000": { "label": "stream", "onAutoForward": "silent", - "requireLocalPort": true - } + "requireLocalPort": true, + }, }, "otherPortsAttributes": { - "onAutoForward": "silent" + "onAutoForward": "silent", }, "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", @@ -34,7 +34,7 @@ "customizations": { "vscode": { "settings": {}, - "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"] - } - } + "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"], + }, + }, } diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 40dc72c12d..88979723c3 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -70,7 +70,7 @@ services: hard: -1 libretranslate: - image: libretranslate/libretranslate:v1.5.2 + image: libretranslate/libretranslate:v1.5.4 restart: unless-stopped volumes: - lt-data:/home/libretranslate/.local diff --git a/.eslintrc.js b/.eslintrc.js index 176879034b..ebe07f6e79 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -120,7 +120,6 @@ module.exports = defineConfig({ 'react/jsx-uses-react': 'off', // not needed with new JSX transform 'react/jsx-wrap-multilines': 'error', 'react/no-deprecated': 'off', - 'react/no-unknown-property': 'off', 'react/react-in-jsx-scope': 'off', // not needed with new JSX transform 'react/self-closing-comp': 'error', @@ -166,7 +165,7 @@ module.exports = defineConfig({ // }, // ], 'jsx-a11y/no-noninteractive-tabindex': 'off', - 'jsx-a11y/no-onchange': 'warn', + 'jsx-a11y/no-onchange': 'off', // recommended is full 'error' 'jsx-a11y/no-static-element-interactions': [ 'warn', @@ -246,7 +245,7 @@ module.exports = defineConfig({ }, // Immutable / Redux / data store { - pattern: '{immutable,react-redux,react-immutable-proptypes,react-immutable-pure-component,reselect}', + pattern: '{immutable,@reduxjs/toolkit,react-redux,react-immutable-proptypes,react-immutable-pure-component}', group: 'external', position: 'before', }, @@ -354,7 +353,14 @@ module.exports = defineConfig({ '@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." + } + ], 'jsdoc/require-jsdoc': 'off', // Those rules set stricter rules for TS files diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 895dbfbad2..dab99829a1 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -50,7 +50,6 @@ matchManagers: ['bundler'], matchPackageNames: [ 'rack', // Needs to be synced with Rails version - 'sprockets', // Requires manual upgrade https://github.com/rails/sprockets/blob/master/UPGRADING.md#guide-to-upgrading-from-sprockets-3x-to-4x 'strong_migrations', // Requires manual upgrade 'sidekiq', // Requires manual upgrade 'sidekiq-unique-jobs', // Requires manual upgrades and sync with Sidekiq version @@ -100,6 +99,16 @@ matchUpdateTypes: ['patch', 'minor'], groupName: 'eslint (non-major)', }, + { + // Group actions/*-artifact in the same PR + matchManagers: ['github-actions'], + matchPackageNames: [ + 'actions/download-artifact', + 'actions/upload-artifact', + ], + matchUpdateTypes: ['major'], + groupName: 'artifact actions (major)', + }, { // Update @types/* packages every week, with one grouped PR matchPackagePrefixes: '@types/', diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3b40c3fd07..6fb93b7fef 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -31,7 +31,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -44,7 +44,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -57,6 +57,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: '/language:${{matrix.language}}' diff --git a/.github/workflows/test-migrations-one-step.yml b/.github/workflows/test-migrations-one-step.yml index 5dca8e376d..1ff5cc06b9 100644 --- a/.github/workflows/test-migrations-one-step.yml +++ b/.github/workflows/test-migrations-one-step.yml @@ -78,23 +78,8 @@ jobs: - name: Create database run: './bin/rails db:create' - - name: Run migrations up to v2.0.0 - run: './bin/rails db:migrate VERSION=20171010025614' - - - name: Populate database with test data - run: './bin/rails tests:migrations:populate_v2' - - - name: Run migrations up to v2.4.0 - run: './bin/rails db:migrate VERSION=20180514140000' - - - name: Populate database with test data - run: './bin/rails tests:migrations:populate_v2_4' - - - name: Run migrations up to v2.4.3 - run: './bin/rails db:migrate VERSION=20180707154237' - - - name: Populate database with test data - run: './bin/rails tests:migrations:populate_v2_4_3' + - name: Run historical migrations with data population + run: './bin/rails tests:migrations:prepare_database' - name: Run all remaining migrations run: './bin/rails db:migrate' diff --git a/.github/workflows/test-migrations-two-step.yml b/.github/workflows/test-migrations-two-step.yml index 59485d285d..6698847315 100644 --- a/.github/workflows/test-migrations-two-step.yml +++ b/.github/workflows/test-migrations-two-step.yml @@ -45,6 +45,7 @@ jobs: --health-retries 5 ports: - 5432:5432 + redis: image: redis:7-alpine options: >- @@ -77,28 +78,11 @@ jobs: - name: Create database run: './bin/rails db:create' - - name: Run migrations up to v2.0.0 - run: './bin/rails db:migrate VERSION=20171010025614' - - - name: Populate database with test data - run: './bin/rails tests:migrations:populate_v2' - - - name: Run pre-deployment migrations up to v2.4.0 - run: './bin/rails db:migrate VERSION=20180514140000' + - name: Run historical migrations with data population + run: './bin/rails tests:migrations:prepare_database' env: SKIP_POST_DEPLOYMENT_MIGRATIONS: true - - name: Populate database with test data - run: './bin/rails tests:migrations:populate_v2_4' - - - name: Run migrations up to v2.4.3 - run: './bin/rails db:migrate VERSION=20180707154237' - env: - SKIP_POST_DEPLOYMENT_MIGRATIONS: true - - - name: Populate database with test data - run: './bin/rails tests:migrations:populate_v2_4_3' - - name: Run all remaining pre-deployment migrations run: './bin/rails db:migrate' env: diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index ae25648a0b..346703ced4 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -52,7 +52,7 @@ jobs: run: | tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs* - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: matrix.mode == 'test' with: path: |- @@ -117,7 +117,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: path: './' name: ${{ github.sha }} @@ -193,7 +193,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: path: './public' name: ${{ github.sha }} @@ -213,14 +213,14 @@ jobs: - run: bundle exec rake spec:system - name: Archive logs - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: e2e-logs-${{ matrix.ruby-version }} path: log/ - name: Archive test screenshots - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: e2e-screenshots @@ -297,7 +297,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: path: './public' name: ${{ github.sha }} @@ -317,14 +317,14 @@ jobs: - run: bin/rspec --tag search - name: Archive logs - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: test-search-logs-${{ matrix.ruby-version }} path: log/ - name: Archive test screenshots - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: test-search-screenshots diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml index 3be2a3d490..af2d2e8f4e 100644 --- a/.haml-lint_todo.yml +++ b/.haml-lint_todo.yml @@ -1,33 +1,13 @@ # This configuration was generated by # `haml-lint --auto-gen-config` -# on 2023-10-26 09:32:34 -0400 using Haml-Lint version 0.51.0. +# on 2024-01-09 11:30:07 -0500 using Haml-Lint version 0.53.0. # The point is for the user to remove these configuration records # one by one as the lints are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of Haml-Lint, may require this file to be generated again. linters: - # Offense count: 16 + # Offense count: 1 LineLength: exclude: - - 'app/views/admin/account_actions/new.html.haml' - - 'app/views/admin/accounts/index.html.haml' - - 'app/views/admin/ip_blocks/new.html.haml' - - 'app/views/admin/roles/_form.html.haml' - - 'app/views/admin/settings/discovery/show.html.haml' - - 'app/views/auth/registrations/edit.html.haml' - - 'app/views/auth/registrations/new.html.haml' - - 'app/views/filters/_filter_fields.html.haml' - - 'app/views/media/player.html.haml' - - 'app/views/settings/applications/_fields.html.haml' - - 'app/views/settings/imports/index.html.haml' - - 'app/views/settings/preferences/appearance/show.html.haml' - - 'app/views/settings/preferences/notifications/show.html.haml' - - 'app/views/settings/preferences/other/show.html.haml' - - # Offense count: 9 - RuboCop: - exclude: - - 'app/views/admin/accounts/_buttons.html.haml' - - 'app/views/admin/accounts/_local_account.html.haml' - 'app/views/admin/roles/_form.html.haml' diff --git a/.nvmrc b/.nvmrc index 48ef2c10ba..a3597ecbd1 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.9 +20.11 diff --git a/.prettierignore b/.prettierignore index 305f0fd753..51850b2b28 100644 --- a/.prettierignore +++ b/.prettierignore @@ -73,3 +73,5 @@ app/javascript/styles/mastodon/reset.scss # Ignore the generated AUTHORS.md AUTHORS.md + +!lint-staged.config.js diff --git a/.rubocop.yml b/.rubocop.yml index 68352e3198..6998941144 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -74,14 +74,12 @@ Metrics/ModuleLength: Metrics/AbcSize: Exclude: - 'lib/mastodon/cli/*.rb' - - db/*migrate/**/* # Reason: Currently disabled in .rubocop_todo.yml # https://docs.rubocop.org/rubocop/cops_metrics.html#metricscyclomaticcomplexity Metrics/CyclomaticComplexity: Exclude: - lib/mastodon/cli/*.rb - - db/*migrate/**/* # Reason: # https://docs.rubocop.org/rubocop/cops_metrics.html#metricsparameterlists @@ -98,22 +96,48 @@ Rails/FilePath: Rails/HttpStatus: EnforcedStyle: numeric -# Reason: Allowed in `tootctl` CLI code and in boot ENV checker +# Reason: Allowed in boot ENV checker # https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsexit Rails/Exit: Exclude: - 'config/boot.rb' - - 'lib/mastodon/cli/*.rb' -# Reason: Some single letter camel case files shouldn't be split +# Reason: Conflicts with `Lint/UselessMethodDefinition` for inherited controller actions +# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railslexicallyscopedactionfilter +Rails/LexicallyScopedActionFilter: + Exclude: + - 'app/controllers/auth/*' + +# Reason: These tasks are doing local work which do not need full env loaded +# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsrakeenvironment +Rails/RakeEnvironment: + Exclude: + - 'lib/tasks/auto_annotate_models.rake' + - 'lib/tasks/emojis.rake' + - 'lib/tasks/mastodon.rake' + - 'lib/tasks/repo.rake' + - 'lib/tasks/statistics.rake' + +# Reason: There are appropriate times to use these features +# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsskipsmodelvalidations +Rails/SkipsModelValidations: + Enabled: false + +# Reason: We want to preserve the ability to migrate from arbitrary old versions, +# and cannot guarantee that every installation has run every migration as they upgrade. +# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsunusedignoredcolumns +Rails/UnusedIgnoredColumns: + Enabled: false + +# Reason: Prevailing style choice +# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsnegateinclude +Rails/NegateInclude: + Enabled: false + +# Reason: Deprecated cop, will be removed in 3.0, replaced by SpecFilePathFormat # https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecfilepath RSpec/FilePath: - CustomTransform: - ActivityPub: activitypub - DeepL: deepl - FetchOEmbedService: fetch_oembed_service - OEmbedController: oembed_controller - OStatus: ostatus + Enabled: false # Reason: # https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecnamedsubject diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 03543c8e0d..c8165c1edf 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-exclude-limit --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.57.2. +# using RuboCop version 1.60.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 @@ -13,20 +13,13 @@ Bundler/OrderedGems: Exclude: - 'Gemfile' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. -# URISchemes: http, https -Layout/LineLength: - Exclude: - - 'app/models/account.rb' - Lint/NonLocalExitFromIterator: Exclude: - 'app/helpers/jsonld_helper.rb' # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: - Max: 125 + Max: 82 # Configuration parameters: CountBlocks, Max. Metrics/BlockNesting: @@ -45,164 +38,29 @@ Metrics/PerceivedComplexity: RSpec/ExampleLength: Max: 22 -RSpec/LetSetup: - Exclude: - - 'spec/controllers/api/v1/accounts/statuses_controller_spec.rb' - - 'spec/controllers/api/v1/filters_controller_spec.rb' - - 'spec/controllers/api/v2/admin/accounts_controller_spec.rb' - - 'spec/controllers/api/v2/filters/keywords_controller_spec.rb' - - 'spec/controllers/api/v2/filters/statuses_controller_spec.rb' - - 'spec/controllers/auth/confirmations_controller_spec.rb' - - 'spec/controllers/auth/passwords_controller_spec.rb' - - 'spec/controllers/auth/sessions_controller_spec.rb' - - 'spec/controllers/follower_accounts_controller_spec.rb' - - 'spec/controllers/following_accounts_controller_spec.rb' - - 'spec/controllers/oauth/authorized_applications_controller_spec.rb' - - 'spec/controllers/oauth/tokens_controller_spec.rb' - - 'spec/controllers/settings/imports_controller_spec.rb' - - 'spec/lib/activitypub/activity/delete_spec.rb' - - 'spec/lib/vacuum/applications_vacuum_spec.rb' - - 'spec/lib/vacuum/preview_cards_vacuum_spec.rb' - - 'spec/models/account_spec.rb' - - 'spec/models/account_statuses_cleanup_policy_spec.rb' - - 'spec/models/canonical_email_block_spec.rb' - - 'spec/models/status_spec.rb' - - 'spec/models/user_spec.rb' - - 'spec/services/account_statuses_cleanup_service_spec.rb' - - 'spec/services/activitypub/fetch_featured_collection_service_spec.rb' - - 'spec/services/activitypub/fetch_remote_status_service_spec.rb' - - 'spec/services/activitypub/process_account_service_spec.rb' - - 'spec/services/activitypub/process_collection_service_spec.rb' - - 'spec/services/batched_remove_status_service_spec.rb' - - 'spec/services/block_domain_service_spec.rb' - - 'spec/services/bulk_import_service_spec.rb' - - 'spec/services/delete_account_service_spec.rb' - - 'spec/services/import_service_spec.rb' - - 'spec/services/notify_service_spec.rb' - - 'spec/services/remove_status_service_spec.rb' - - 'spec/services/report_service_spec.rb' - - 'spec/services/resolve_account_service_spec.rb' - - 'spec/services/suspend_account_service_spec.rb' - - 'spec/services/unallow_domain_service_spec.rb' - - 'spec/services/unsuspend_account_service_spec.rb' - - 'spec/workers/scheduler/user_cleanup_scheduler_spec.rb' - RSpec/MultipleExpectations: Max: 8 # Configuration parameters: AllowSubject. RSpec/MultipleMemoizedHelpers: - Max: 21 + Max: 17 # Configuration parameters: AllowedGroups. RSpec/NestedGroups: Max: 6 -# This cop supports unsafe autocorrection (--autocorrect-all). -Rails/ApplicationController: - Exclude: - - 'app/controllers/health_controller.rb' - # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/HasAndBelongsToMany: Exclude: - 'app/models/concerns/account/associations.rb' - - 'app/models/preview_card.rb' - 'app/models/status.rb' - 'app/models/tag.rb' -# Configuration parameters: Include. -# Include: app/controllers/**/*.rb, app/mailers/**/*.rb -Rails/LexicallyScopedActionFilter: - Exclude: - - 'app/controllers/auth/passwords_controller.rb' - - 'app/controllers/auth/registrations_controller.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -Rails/NegateInclude: - Exclude: - - 'app/controllers/concerns/signature_verification.rb' - - 'app/helpers/jsonld_helper.rb' - - 'app/lib/activitypub/activity/create.rb' - - 'app/lib/activitypub/activity/move.rb' - - 'app/lib/feed_manager.rb' - - 'app/lib/link_details_extractor.rb' - - 'app/models/concerns/attachmentable.rb' - - 'app/models/concerns/remotable.rb' - - 'app/models/custom_filter.rb' - - 'app/services/activitypub/process_status_update_service.rb' - - 'app/services/fetch_link_card_service.rb' - - 'app/workers/web/push_notification_worker.rb' - - 'lib/paperclip/color_extractor.rb' - Rails/OutputSafety: Exclude: - 'config/initializers/simple_form.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: Include. -# Include: **/Rakefile, **/*.rake -Rails/RakeEnvironment: - Exclude: - - 'lib/tasks/auto_annotate_models.rake' - - 'lib/tasks/db.rake' - - 'lib/tasks/emojis.rake' - - 'lib/tasks/mastodon.rake' - - 'lib/tasks/repo.rake' - - 'lib/tasks/statistics.rake' - -# Configuration parameters: ForbiddenMethods, AllowedMethods. -# ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all -Rails/SkipsModelValidations: - Exclude: - - 'app/controllers/admin/invites_controller.rb' - - 'app/controllers/concerns/session_tracking_concern.rb' - - 'app/models/concerns/account/merging.rb' - - 'app/models/concerns/expireable.rb' - - 'app/models/status.rb' - - 'app/models/trends/links.rb' - - 'app/models/trends/preview_card_batch.rb' - - 'app/models/trends/preview_card_provider_batch.rb' - - 'app/models/trends/status_batch.rb' - - 'app/models/trends/statuses.rb' - - 'app/models/trends/tag_batch.rb' - - 'app/models/trends/tags.rb' - - 'app/models/user.rb' - - 'app/services/activitypub/process_status_update_service.rb' - - 'app/services/approve_appeal_service.rb' - - 'app/services/block_domain_service.rb' - - 'app/services/delete_account_service.rb' - - 'app/services/process_mentions_service.rb' - - 'app/services/unallow_domain_service.rb' - - 'app/services/unblock_domain_service.rb' - - 'app/services/update_status_service.rb' - - 'app/workers/activitypub/post_upgrade_worker.rb' - - 'app/workers/move_worker.rb' - - 'app/workers/scheduler/ip_cleanup_scheduler.rb' - - 'app/workers/scheduler/scheduled_statuses_scheduler.rb' - - 'db/migrate/20161203164520_add_from_account_id_to_notifications.rb' - - 'db/migrate/20170105224407_add_shortcode_to_media_attachments.rb' - - 'db/migrate/20170209184350_add_reply_to_statuses.rb' - - 'db/migrate/20170304202101_add_type_to_media_attachments.rb' - - 'db/migrate/20180528141303_fix_accounts_unique_index.rb' - - 'db/migrate/20180609104432_migrate_web_push_subscriptions2.rb' - - 'db/migrate/20181207011115_downcase_custom_emoji_domains.rb' - - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb' - - 'db/migrate/20191007013357_update_pt_locales.rb' - - 'db/migrate/20220316233212_update_kurdish_locales.rb' - - 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb' - - 'db/post_migrate/20200917193528_migrate_notifications_type.rb' - - 'db/post_migrate/20201017234926_fill_account_suspension_origin.rb' - - 'db/post_migrate/20220617202502_migrate_roles.rb' - - 'db/post_migrate/20221101190723_backfill_admin_action_logs.rb' - - 'db/post_migrate/20221206114142_backfill_admin_action_logs_again.rb' - - 'lib/mastodon/cli/accounts.rb' - - 'lib/mastodon/cli/maintenance.rb' - - 'spec/lib/activitypub/activity/follow_spec.rb' - - 'spec/services/follow_service_spec.rb' - - 'spec/services/update_account_service_spec.rb' - # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/UniqueValidationWithoutIndex: @@ -212,60 +70,6 @@ Rails/UniqueValidationWithoutIndex: - 'app/models/identity.rb' - 'app/models/webauthn_credential.rb' -# Configuration parameters: Include. -# Include: app/models/**/*.rb -Rails/UnusedIgnoredColumns: - Exclude: - - 'app/models/account.rb' - - 'app/models/account_stat.rb' - - 'app/models/admin/action_log.rb' - - 'app/models/custom_filter.rb' - - 'app/models/email_domain_block.rb' - - 'app/models/report.rb' - - 'app/models/status_edit.rb' - - 'app/models/user.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: exists, where -Rails/WhereExists: - Exclude: - - 'app/controllers/activitypub/inboxes_controller.rb' - - 'app/controllers/admin/email_domain_blocks_controller.rb' - - 'app/controllers/auth/registrations_controller.rb' - - 'app/lib/activitypub/activity/create.rb' - - 'app/lib/delivery_failure_tracker.rb' - - 'app/lib/feed_manager.rb' - - 'app/lib/status_cache_hydrator.rb' - - 'app/lib/suspicious_sign_in_detector.rb' - - 'app/models/concerns/account/interactions.rb' - - 'app/models/featured_tag.rb' - - 'app/models/poll.rb' - - 'app/models/session_activation.rb' - - 'app/models/status.rb' - - 'app/models/user.rb' - - 'app/policies/status_policy.rb' - - 'app/serializers/rest/announcement_serializer.rb' - - 'app/serializers/rest/tag_serializer.rb' - - 'app/services/activitypub/fetch_remote_status_service.rb' - - 'app/services/app_sign_up_service.rb' - - 'app/services/vote_service.rb' - - 'app/validators/reaction_validator.rb' - - 'app/validators/vote_validator.rb' - - 'app/workers/move_worker.rb' - - 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb' - - 'lib/tasks/tests.rake' - - 'spec/models/account_spec.rb' - - 'spec/services/activitypub/process_collection_service_spec.rb' - - 'spec/services/purge_domain_service_spec.rb' - - 'spec/services/unallow_domain_service_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowOnConstant, AllowOnSelfClass. -Style/CaseEquality: - Exclude: - - 'config/initializers/trusted_proxies.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowedMethods, AllowedPatterns. # AllowedMethods: ==, equal?, eql? @@ -293,8 +97,8 @@ Style/FetchEnvVar: - 'config/initializers/devise.rb' - 'config/initializers/paperclip.rb' - 'config/initializers/vapid.rb' - - 'lib/premailer_webpack_strategy.rb' - 'lib/mastodon/redis_config.rb' + - 'lib/premailer_webpack_strategy.rb' - 'lib/tasks/repo.rake' - 'spec/features/profile_spec.rb' @@ -304,14 +108,12 @@ Style/FetchEnvVar: # AllowedMethods: redirect Style/FormatStringToken: Exclude: - - 'app/models/privacy_policy.rb' - 'config/initializers/devise.rb' - 'lib/paperclip/color_extractor.rb' # This cop supports unsafe autocorrection (--autocorrect-all). Style/GlobalStdStream: Exclude: - - 'config/boot.rb' - 'config/environments/development.rb' - 'config/environments/production.rb' @@ -319,10 +121,6 @@ Style/GlobalStdStream: # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. Style/GuardClause: Exclude: - - 'app/controllers/admin/confirmations_controller.rb' - - 'app/controllers/auth/confirmations_controller.rb' - - 'app/controllers/auth/passwords_controller.rb' - - 'app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb' - 'app/lib/activitypub/activity/block.rb' - 'app/lib/request.rb' - 'app/lib/request_pool.rb' @@ -341,8 +139,6 @@ Style/GuardClause: - 'app/workers/redownload_media_worker.rb' - 'app/workers/remote_account_refresh_worker.rb' - 'config/initializers/devise.rb' - - 'db/migrate/20170901141119_truncate_preview_cards.rb' - - 'db/post_migrate/20220704024901_migrate_settings_to_user_roles.rb' - 'lib/devise/strategies/two_factor_ldap_authenticatable.rb' - 'lib/devise/strategies/two_factor_pam_authenticatable.rb' - 'lib/mastodon/cli/accounts.rb' @@ -363,7 +159,6 @@ Style/HashAsLastArrayItem: - 'app/models/status.rb' - 'app/services/batched_remove_status_service.rb' - 'app/services/notify_service.rb' - - 'db/migrate/20181024224956_migrate_account_conversations.rb' # This cop supports unsafe autocorrection (--autocorrect-all). Style/HashTransformValues: @@ -378,22 +173,6 @@ Style/IfUnlessModifier: - 'config/initializers/devise.rb' - 'config/initializers/ffmpeg.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: InverseMethods, InverseBlocks. -Style/InverseMethods: - Exclude: - - 'app/models/custom_filter.rb' - - 'app/services/update_account_service.rb' - - 'spec/controllers/activitypub/replies_controller_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: line_count_dependent, lambda, literal -Style/Lambda: - Exclude: - - 'config/initializers/simple_form.rb' - - 'config/routes.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). Style/MapToHash: Exclude: @@ -458,15 +237,6 @@ Style/RedundantFetchBlock: - 'config/initializers/paperclip.rb' - 'config/puma.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowMultipleReturnValues. -Style/RedundantReturn: - Exclude: - - 'app/controllers/api/v1/directories_controller.rb' - - 'app/controllers/auth/confirmations_controller.rb' - - 'app/lib/ostatus/tag_manager.rb' - - 'app/models/form/import.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. # AllowedMethods: present?, blank?, presence, try, try! @@ -487,11 +257,6 @@ Style/SingleArgumentDig: Exclude: - 'lib/webpacker/manifest_extensions.rb' -# This cop supports safe autocorrection (--autocorrect). -Style/StderrPuts: - Exclude: - - 'config/boot.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Mode. Style/StringConcatenation: @@ -510,20 +275,6 @@ Style/StringLiterals: - 'config/initializers/webauthn.rb' - 'config/routes.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, AllowComments. -# AllowedMethods: define_method, mail, respond_to -Style/SymbolProc: - Exclude: - - 'config/initializers/3_omniauth.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, AllowSafeAssignment. -# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex -Style/TernaryParentheses: - Exclude: - - 'config/environments/development.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyleForMultiline. # SupportedStylesForMultiline: comma, consistent_comma, no_comma @@ -540,8 +291,8 @@ Style/TrailingCommaInHashLiteral: - 'config/environments/test.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, MinSize, WordRegex. +# Configuration parameters: WordRegex. # SupportedStyles: percent, brackets Style/WordArray: - Exclude: - - 'app/helpers/languages_helper.rb' + EnforcedStyle: percent + MinSize: 3 diff --git a/.ruby-version b/.ruby-version index be94e6f53d..b347b11eac 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2.2 +3.2.3 diff --git a/.yarn/patches/compression-webpack-plugin-npm-6.1.1-3a2a65987e.patch b/.yarn/patches/compression-webpack-plugin-npm-6.1.1-3a2a65987e.patch deleted file mode 100644 index 30d5db2739..0000000000 --- a/.yarn/patches/compression-webpack-plugin-npm-6.1.1-3a2a65987e.patch +++ /dev/null @@ -1,22 +0,0 @@ -diff --git a/dist/index.js b/dist/index.js -index 57e375592d984e9a429bcd9f800fa2d15cd662e4..0c47d96df3608e23adfd77d887a8f72abbd501c0 100644 ---- a/dist/index.js -+++ b/dist/index.js -@@ -5,7 +5,7 @@ Object.defineProperty(exports, "__esModule", { - }); - exports.default = void 0; - --var _crypto = _interopRequireDefault(require("crypto")); -+var _createHash = _interopRequireDefault(require("webpack/lib/util/createHash")); - - var _path = _interopRequireDefault(require("path")); - -@@ -227,7 +227,7 @@ class CompressionPlugin { - originalAlgorithm: this.options.algorithm, - compressionOptions: this.options.compressionOptions, - name, -- contentHash: _crypto.default.createHash("md4").update(input).digest("hex") -+ contentHash: _createHash.default("md4").update(input).digest("hex") - }; - } else { - cacheData.name = (0, _serializeJavascript.default)({ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c1a5fef798..b68a9bde3e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,6 +11,10 @@ You can contribute in the following ways: If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon). +## API Changes and Additions + +Please note that any changes or additions made to the API should have an accompanying pull request on [our documentation repository](https://github.com/mastodon/documentation). + ## Bug reports Bug reports and feature suggestions must use descriptive and concise titles and be submitted to [GitHub Issues](https://github.com/mastodon/mastodon/issues). Please use the search function to make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected. diff --git a/Dockerfile b/Dockerfile index 4d5bd57f11..119c266b89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,15 +7,15 @@ ARG TARGETPLATFORM=${TARGETPLATFORM} ARG BUILDPLATFORM=${BUILDPLATFORM} -# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.2.2"] -ARG RUBY_VERSION="3.2.2" +# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.2.3"] +ARG RUBY_VERSION="3.2.3" # # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] ARG NODE_MAJOR_VERSION="20" # Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"] ARG DEBIAN_VERSION="bookworm" # Node image to use for base image based on combined variables (ex: 20-bookworm-slim) FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim as node -# Ruby image to use for base image based on combined variables (ex: 3.2.2-slim-bookworm) +# Ruby image to use for base image based on combined variables (ex: 3.2.3-slim-bookworm) FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby # Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA @@ -103,6 +103,7 @@ RUN \ procps \ tini \ tzdata \ + wget \ ; \ # Patch Ruby to use jemalloc patchelf --add-needed libjemalloc.so.2 /usr/local/bin/ruby; \ diff --git a/FEDERATION.md b/FEDERATION.md index e3721d7241..2819fa935a 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -1,19 +1,35 @@ -## ActivityPub federation in Mastodon +# Federation + +## Supported federation protocols and standards + +- [ActivityPub](https://www.w3.org/TR/activitypub/) (Server-to-Server) +- [WebFinger](https://webfinger.net/) +- [Http Signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures) +- [NodeInfo](https://nodeinfo.diaspora.software/) + +## Supported FEPs + +- [FEP-67ff: FEDERATION.md](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md) +- [FEP-f1d5: NodeInfo in Fediverse Software](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md) +- [FEP-8fcf: Followers collection synchronization across servers](https://codeberg.org/fediverse/fep/src/branch/main/fep/8fcf/fep-8fcf.md) +- [FEP-5feb: Search indexing consent for actors](https://codeberg.org/fediverse/fep/src/branch/main/fep/5feb/fep-5feb.md) + +## ActivityPub in Mastodon Mastodon largely follows the ActivityPub server-to-server specification but it makes uses of some non-standard extensions, some of which are required for interacting with Mastodon at all. -Supported vocabulary: https://docs.joinmastodon.org/spec/activitypub/ +- [Supported ActivityPub vocabulary](https://docs.joinmastodon.org/spec/activitypub/) ### Required extensions -#### Webfinger +#### WebFinger In Mastodon, users are identified by a `username` and `domain` pair (e.g., `Gargron@mastodon.social`). This is used both for discovery and for unambiguously mentioning users across the fediverse. Furthermore, this is part of Mastodon's database design from its very beginnings. As a result, Mastodon requires that each ActivityPub actor uniquely maps back to an `acct:` URI that can be resolved via WebFinger. -More information and examples are available at: https://docs.joinmastodon.org/spec/webfinger/ +- [WebFinger information and examples](https://docs.joinmastodon.org/spec/webfinger/) #### HTTP Signatures @@ -21,11 +37,13 @@ In order to authenticate activities, Mastodon relies on HTTP Signatures, signing Mastodon requires all `POST` requests to be signed, and MAY require `GET` requests to be signed, depending on the configuration of the Mastodon server. -More information on HTTP Signatures, as well as examples, can be found here: https://docs.joinmastodon.org/spec/security/#http +- [HTTP Signatures information and examples](https://docs.joinmastodon.org/spec/security/#http) ### Optional extensions -- Linked-Data Signatures: https://docs.joinmastodon.org/spec/security/#ld -- Bearcaps: https://docs.joinmastodon.org/spec/bearcaps/ -- Followers collection synchronization: https://codeberg.org/fediverse/fep/src/branch/main/fep/8fcf/fep-8fcf.md -- Search indexing consent for actors: https://codeberg.org/fediverse/fep/src/branch/main/fep/5feb/fep-5feb.md +- [Linked-Data Signatures](https://docs.joinmastodon.org/spec/security/#ld) +- [Bearcaps](https://docs.joinmastodon.org/spec/bearcaps/) + +### Additional documentation + +- [Mastodon documentation](https://docs.joinmastodon.org/) diff --git a/Gemfile b/Gemfile index e3fb39e169..4951304e39 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,7 @@ ruby '>= 3.0.0' gem 'puma', '~> 6.3' gem 'rails', '~> 7.1.1' -gem 'sprockets', '~> 3.7.2' +gem 'propshaft' gem 'thor', '~> 1.2' gem 'rack', '~> 2.2.7' @@ -39,15 +39,14 @@ end gem 'net-ldap', '~> 0.18' -# TODO: Point back at released omniauth-cas gem when PR merged -# https://github.com/dlindahl/omniauth-cas/pull/68 -gem 'omniauth-cas', github: 'stanhu/omniauth-cas', ref: '4211e6d05941b4a981f9a36b49ec166cecd0e271' +gem 'omniauth-cas', '~> 3.0.0.beta.1' gem 'omniauth-saml', '~> 2.0' gem 'omniauth_openid_connect', '~> 0.6.1' gem 'omniauth', '~> 2.0' gem 'omniauth-rails_csrf_protection', '~> 1.0' gem 'color_diff', '~> 0.1' +gem 'csv', '~> 3.2' gem 'discard', '~> 1.2' gem 'doorkeeper', '~> 5.6' gem 'ed25519', '~> 1.3' @@ -75,7 +74,6 @@ gem 'premailer-rails' gem 'rack-attack', '~> 6.6' gem 'rack-cors', '~> 2.0', require: 'rack/cors' gem 'rails-i18n', '~> 7.0' -gem 'rails-settings-cached', '~> 0.6', git: 'https://github.com/mastodon/rails-settings-cached.git', branch: 'v0.6.6-aliases-true' gem 'redcarpet', '~> 3.6' gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' @@ -89,9 +87,8 @@ gem 'sidekiq-unique-jobs', '~> 7.1' gem 'sidekiq-bulk', '~> 0.2.0' gem 'simple-navigation', '~> 4.4' gem 'simple_form', '~> 5.2' -gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie' gem 'stoplight', '~> 3.0.1' -gem 'strong_migrations', '1.6.4' +gem 'strong_migrations', '1.7.0' gem 'tty-prompt', '~> 0.23', require: false gem 'twitter-text', '~> 3.1.0' gem 'tzinfo-data', '~> 1.2023' @@ -126,7 +123,7 @@ group :test do gem 'database_cleaner-active_record' # Used to mock environment variables - gem 'climate_control', '~> 0.2' + gem 'climate_control' # Generating fake data for specs gem 'faker', '~> 3.2' diff --git a/Gemfile.lock b/Gemfile.lock index 037fe145d2..57b2580722 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -18,56 +18,38 @@ GIT sidekiq (>= 3.5) statsd-ruby (~> 1.4, >= 1.4.0) -GIT - remote: https://github.com/mastodon/rails-settings-cached.git - revision: 86328ef0bd04ce21cc0504ff5e334591e8c2ccab - branch: v0.6.6-aliases-true - specs: - rails-settings-cached (0.6.6) - rails (>= 4.2.0) - -GIT - remote: https://github.com/stanhu/omniauth-cas.git - revision: 4211e6d05941b4a981f9a36b49ec166cecd0e271 - ref: 4211e6d05941b4a981f9a36b49ec166cecd0e271 - specs: - omniauth-cas (2.0.0) - addressable (~> 2.3) - nokogiri (~> 1.5) - omniauth (>= 1.2, < 3) - GEM remote: https://rubygems.org/ specs: - actioncable (7.1.2) - actionpack (= 7.1.2) - activesupport (= 7.1.2) + actioncable (7.1.3) + actionpack (= 7.1.3) + activesupport (= 7.1.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.1.2) - actionpack (= 7.1.2) - activejob (= 7.1.2) - activerecord (= 7.1.2) - activestorage (= 7.1.2) - activesupport (= 7.1.2) + actionmailbox (7.1.3) + actionpack (= 7.1.3) + activejob (= 7.1.3) + activerecord (= 7.1.3) + activestorage (= 7.1.3) + activesupport (= 7.1.3) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.1.2) - actionpack (= 7.1.2) - actionview (= 7.1.2) - activejob (= 7.1.2) - activesupport (= 7.1.2) + actionmailer (7.1.3) + actionpack (= 7.1.3) + actionview (= 7.1.3) + activejob (= 7.1.3) + activesupport (= 7.1.3) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.2) - actionpack (7.1.2) - actionview (= 7.1.2) - activesupport (= 7.1.2) + actionpack (7.1.3) + actionview (= 7.1.3) + activesupport (= 7.1.3) nokogiri (>= 1.8.5) racc rack (>= 2.2.4) @@ -75,15 +57,15 @@ GEM rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - actiontext (7.1.2) - actionpack (= 7.1.2) - activerecord (= 7.1.2) - activestorage (= 7.1.2) - activesupport (= 7.1.2) + actiontext (7.1.3) + actionpack (= 7.1.3) + activerecord (= 7.1.3) + activestorage (= 7.1.3) + activesupport (= 7.1.3) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.1.2) - activesupport (= 7.1.2) + actionview (7.1.3) + activesupport (= 7.1.3) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) @@ -93,22 +75,22 @@ GEM activemodel (>= 4.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (7.1.2) - activesupport (= 7.1.2) + activejob (7.1.3) + activesupport (= 7.1.3) globalid (>= 0.3.6) - activemodel (7.1.2) - activesupport (= 7.1.2) - activerecord (7.1.2) - activemodel (= 7.1.2) - activesupport (= 7.1.2) + activemodel (7.1.3) + activesupport (= 7.1.3) + activerecord (7.1.3) + activemodel (= 7.1.3) + activesupport (= 7.1.3) timeout (>= 0.4.0) - activestorage (7.1.2) - actionpack (= 7.1.2) - activejob (= 7.1.2) - activerecord (= 7.1.2) - activesupport (= 7.1.2) + activestorage (7.1.3) + actionpack (= 7.1.3) + activejob (= 7.1.3) + activerecord (= 7.1.3) + activesupport (= 7.1.3) marcel (~> 1.0) - activesupport (7.1.2) + activesupport (7.1.3) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) @@ -118,7 +100,7 @@ GEM minitest (>= 5.1) mutex_m tzinfo (~> 2.0) - addressable (2.8.5) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) aes_key_wrap (1.1.0) android_key_attestation (0.3.0) @@ -131,20 +113,20 @@ GEM attr_required (1.0.1) awrence (1.2.1) aws-eventstream (1.3.0) - aws-partitions (1.857.0) - aws-sdk-core (3.188.0) - aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (1.873.0) + aws-sdk-core (3.190.1) + aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.5) + aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.73.0) + aws-sdk-kms (1.75.0) aws-sdk-core (~> 3, >= 3.188.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.140.0) - aws-sdk-core (~> 3, >= 3.188.0) + aws-sdk-s3 (1.142.0) + aws-sdk-core (~> 3, >= 3.189.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.6) - aws-sigv4 (1.7.0) + aws-sigv4 (~> 1.8) + aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) azure-storage-blob (2.0.3) azure-storage-common (~> 2.0) @@ -168,14 +150,15 @@ GEM erubi (~> 1.4) parser (>= 2.4) smart_properties - bigdecimal (3.1.4) + bigdecimal (3.1.6) bindata (2.4.15) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) blurhash (0.1.7) - bootsnap (1.17.0) + bootsnap (1.17.1) msgpack (~> 1.2) - brakeman (6.0.1) + brakeman (6.1.1) + racc browser (5.3.1) brpoplpush-redis_script (0.1.3) concurrent-ruby (~> 1.0, >= 1.0.5) @@ -197,15 +180,15 @@ GEM activesupport cbor (0.5.9.6) charlock_holmes (0.7.7) - chewy (7.3.4) + chewy (7.5.0) activesupport (>= 5.2) elasticsearch (>= 7.12.0, < 7.14.0) elasticsearch-dsl chunky_png (1.4.0) - climate_control (0.2.0) + climate_control (1.2.0) cocoon (1.2.15) color_diff (0.1) - concurrent-ruby (1.2.2) + concurrent-ruby (1.2.3) connection_pool (2.4.1) cose (1.3.0) cbor (~> 0.5.9) @@ -215,14 +198,15 @@ GEM crass (1.0.6) css_parser (1.14.0) addressable + csv (3.2.8) database_cleaner-active_record (2.1.0) activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) date (3.3.4) - debug (1.8.0) - irb (>= 1.5.0) - reline (>= 0.3.1) + debug (1.9.1) + irb (~> 1.10) + reline (>= 0.3.8) debug_inspector (1.1.0) devise (4.9.3) bcrypt (~> 3.0) @@ -271,9 +255,9 @@ GEM erubi (1.12.0) et-orbi (1.2.7) tzinfo - excon (0.104.0) + excon (0.109.0) fabrication (2.31.0) - faker (3.2.2) + faker (3.2.3) i18n (>= 1.8.11, < 2) faraday (1.10.3) faraday-em_http (~> 1.0) @@ -301,12 +285,12 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fast_blank (1.0.1) - fastimage (2.2.7) + fastimage (2.3.0) ffi (1.15.5) ffi-compiler (1.0.1) ffi (>= 1.0.0) rake - fog-core (2.3.0) + fog-core (2.4.0) builder excon (~> 0.71) formatador (>= 0.2, < 2.0) @@ -326,7 +310,7 @@ GEM ruby-progressbar (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) - haml (6.2.0) + haml (6.3.0) temple (>= 0.8.2) thor tilt @@ -335,8 +319,8 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) - haml_lint (0.51.0) - haml (>= 4.0) + haml_lint (0.55.0) + haml (>= 5.0) parallel (~> 1.10) rainbow rubocop (>= 1.0) @@ -376,21 +360,21 @@ GEM rainbow (>= 2.2.2, < 4.0) terminal-table (>= 1.5.1) idn-ruby (0.1.5) - io-console (0.6.0) - irb (1.10.0) + io-console (0.7.2) + irb (1.11.1) rdoc - reline (>= 0.3.8) + reline (>= 0.4.2) jmespath (1.6.2) - json (2.7.0) - json-canonicalization (0.3.2) + json (2.7.1) + json-canonicalization (1.0.0) json-jwt (1.15.3) activesupport (>= 4.2) aes_key_wrap bindata httpclient - json-ld (3.3.0) + json-ld (3.3.1) htmlentities (~> 4.3) - json-canonicalization (~> 0.3, >= 0.3.2) + json-canonicalization (~> 1.0) link_header (~> 0.0, >= 0.0.8) multi_json (~> 1.15) rack (>= 2.2, < 4) @@ -414,12 +398,12 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) - kt-paperclip (7.2.1) + kt-paperclip (7.2.2) activemodel (>= 4.2.0) activesupport (>= 4.2.0) marcel (~> 1.0.1) mime-types - terrapin (~> 0.6.0) + terrapin (>= 0.6.0, < 2.0) language_server-protocol (3.17.0.3) launchy (2.5.2) addressable (~> 2.8) @@ -456,39 +440,44 @@ GEM azure-storage-blob (~> 2.0.1) hashie (~> 5.0) memory_profiler (1.0.1) - mime-types (3.5.1) + mime-types (3.5.2) mime-types-data (~> 3.2015) - mime-types-data (3.2023.1003) + mime-types-data (3.2023.1205) mini_mime (1.1.5) mini_portile2 (2.8.5) - minitest (5.20.0) + minitest (5.21.2) msgpack (1.7.2) multi_json (1.15.0) multipart-post (2.3.0) mutex_m (0.2.0) - net-http (0.4.0) + net-http (0.4.1) uri net-http-persistent (4.0.2) connection_pool (~> 2.2) - net-imap (0.4.4) + net-imap (0.4.9.1) date net-protocol - net-ldap (0.18.0) + net-ldap (0.19.0) net-pop (0.1.2) net-protocol net-protocol (0.2.2) timeout - net-smtp (0.4.0) + net-smtp (0.4.0.1) net-protocol nio4r (2.5.9) - nokogiri (1.15.5) + nokogiri (1.16.0) mini_portile2 (~> 2.8.2) racc (~> 1.4) - oj (3.16.1) + oj (3.16.3) + bigdecimal (>= 3.0) omniauth (2.1.1) hashie (>= 3.4.6) rack (>= 2.2.3) rack-protection + omniauth-cas (3.0.0.beta.1) + addressable (~> 2.8) + nokogiri (~> 1.12) + omniauth (~> 2.1) omniauth-rails_csrf_protection (1.0.1) actionpack (>= 4.2) omniauth (~> 2.0) @@ -509,13 +498,13 @@ GEM validate_email validate_url webfinger (~> 1.2) - openssl (3.1.0) + openssl (3.2.0) openssl-signature_algorithm (1.3.0) openssl (> 2.0) orm_adapter (0.5.0) ox (2.14.17) - parallel (1.23.0) - parser (3.2.2.4) + parallel (1.24.0) + parser (3.3.0.5) ast (~> 2.4.1) racc parslet (2.0.0) @@ -534,10 +523,15 @@ GEM net-smtp premailer (~> 1.7, >= 1.7.9) private_address_check (0.5.0) - psych (5.1.1.1) + propshaft (0.8.0) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack + railties (>= 7.0.0) + psych (5.1.2) stringio public_suffix (5.0.4) - puma (6.4.0) + puma (6.4.2) nio4r (~> 2.0) pundit (2.3.1) activesupport (>= 3.0.0) @@ -558,27 +552,27 @@ GEM rack rack-proxy (0.7.6) rack - rack-session (1.0.1) + rack-session (1.0.2) rack (< 3) rack-test (2.1.0) rack (>= 1.3) rackup (1.0.0) rack (< 3) webrick - rails (7.1.2) - actioncable (= 7.1.2) - actionmailbox (= 7.1.2) - actionmailer (= 7.1.2) - actionpack (= 7.1.2) - actiontext (= 7.1.2) - actionview (= 7.1.2) - activejob (= 7.1.2) - activemodel (= 7.1.2) - activerecord (= 7.1.2) - activestorage (= 7.1.2) - activesupport (= 7.1.2) + rails (7.1.3) + actioncable (= 7.1.3) + actionmailbox (= 7.1.3) + actionmailer (= 7.1.3) + actionpack (= 7.1.3) + actiontext (= 7.1.3) + actionview (= 7.1.3) + activejob (= 7.1.3) + activemodel (= 7.1.3) + activerecord (= 7.1.3) + activestorage (= 7.1.3) + activesupport (= 7.1.3) bundler (>= 1.15.0) - railties (= 7.1.2) + railties (= 7.1.3) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -593,9 +587,9 @@ GEM rails-i18n (7.0.8) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - railties (7.1.2) - actionpack (= 7.1.2) - activesupport (= 7.1.2) + railties (7.1.3) + actionpack (= 7.1.3) + activesupport (= 7.1.3) irb rackup (>= 1.0.0) rake (>= 12.2) @@ -606,9 +600,9 @@ GEM rdf (3.3.1) bcp47_spec (~> 0.2) link_header (~> 0.0, >= 0.0.8) - rdf-normalize (0.6.1) - rdf (~> 3.2) - rdoc (6.6.0) + rdf-normalize (0.7.0) + rdf (~> 3.3) + rdoc (6.6.2) psych (>= 4.0.0) redcarpet (3.6.0) redis (4.8.1) @@ -616,8 +610,8 @@ GEM redis (>= 4) redlock (1.3.2) redis (>= 3.0.0, < 6.0) - regexp_parser (2.8.2) - reline (0.4.1) + regexp_parser (2.9.0) + reline (0.4.2) io-console (~> 0.5) request_store (1.5.1) rack (>= 1.4) @@ -642,7 +636,7 @@ GEM rspec-mocks (3.12.6) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-rails (6.1.0) + rspec-rails (6.1.1) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) @@ -656,11 +650,11 @@ GEM rspec-mocks (~> 3.0) sidekiq (>= 5, < 8) rspec-support (3.12.1) - rubocop (1.58.0) + rubocop (1.60.2) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.2.4) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) @@ -669,23 +663,23 @@ GEM unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.30.0) parser (>= 3.2.1.0) - rubocop-capybara (2.19.0) + rubocop-capybara (2.20.0) rubocop (~> 1.41) - rubocop-factory_bot (2.24.0) + rubocop-factory_bot (2.25.0) rubocop (~> 1.33) - rubocop-performance (1.19.1) - rubocop (>= 1.7.0, < 2.0) - rubocop-ast (>= 0.4.0) - rubocop-rails (2.22.2) + rubocop-performance (1.20.2) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) + rubocop-rails (2.23.1) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) rubocop-ast (>= 1.30.0, < 2.0) - rubocop-rspec (2.25.0) + rubocop-rspec (2.26.1) rubocop (~> 1.40) rubocop-capybara (~> 2.17) rubocop-factory_bot (~> 2.22) - ruby-prof (1.6.3) + ruby-prof (1.7.0) ruby-progressbar (1.13.0) ruby-saml (1.15.0) nokogiri (>= 1.13.10) @@ -702,7 +696,8 @@ GEM scenic (1.7.0) activerecord (>= 4.0.0) railties (>= 4.0.0) - selenium-webdriver (4.15.0) + selenium-webdriver (4.17.0) + base64 (~> 0.2) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) @@ -717,7 +712,7 @@ GEM rufus-scheduler (~> 3.2) sidekiq (>= 6, < 8) tilt (>= 1.4.0) - sidekiq-unique-jobs (7.1.30) + sidekiq-unique-jobs (7.1.31) brpoplpush-redis_script (> 0.1.1, <= 2.0.0) concurrent-ruby (~> 1.0, >= 1.0.5) redis (< 5.0) @@ -736,19 +731,12 @@ GEM simplecov-lcov (0.8.0) simplecov_json_formatter (0.1.4) smart_properties (1.17.0) - sprockets (3.7.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.4.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - sprockets (>= 3.0.0) - stackprof (0.2.25) + stackprof (0.2.26) statsd-ruby (1.5.0) stoplight (3.0.2) redlock (~> 1.0) stringio (3.1.0) - strong_migrations (1.6.4) + strong_migrations (1.7.0) activerecord (>= 5.2) swd (1.3.0) activesupport (>= 3) @@ -758,9 +746,9 @@ GEM temple (0.10.3) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) - terrapin (0.6.0) - climate_control (>= 0.0.3, < 1.0) - test-prof (1.3.0) + terrapin (1.0.1) + climate_control + test-prof (1.3.1) thor (1.3.0) tilt (2.3.0) timeout (0.4.1) @@ -783,7 +771,7 @@ GEM unf (~> 0.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - tzinfo-data (1.2023.3) + tzinfo-data (1.2023.4) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext @@ -798,7 +786,7 @@ GEM public_suffix warden (1.2.9) rack (>= 2.0.9) - webauthn (3.0.0) + webauthn (3.1.0) android_key_attestation (~> 0.3.0) awrence (~> 1.1) bindata (~> 2.4) @@ -848,11 +836,12 @@ DEPENDENCIES capybara (~> 3.39) charlock_holmes (~> 0.7.7) chewy (~> 7.3) - climate_control (~> 0.2) + climate_control cocoon (~> 1.2) color_diff (~> 0.1) concurrent-ruby connection_pool + csv (~> 3.2) database_cleaner-active_record debug (~> 1.8) devise (~> 4.9) @@ -900,7 +889,7 @@ DEPENDENCIES nsa! oj (~> 3.14) omniauth (~> 2.0) - omniauth-cas! + omniauth-cas (~> 3.0.0.beta.1) omniauth-rails_csrf_protection (~> 1.0) omniauth-saml (~> 2.0) omniauth_openid_connect (~> 0.6.1) @@ -911,6 +900,7 @@ DEPENDENCIES posix-spawn premailer-rails private_address_check (~> 0.5) + propshaft public_suffix (~> 5.0) puma (~> 6.3) pundit (~> 2.3) @@ -921,7 +911,6 @@ DEPENDENCIES rails (~> 7.1.1) rails-controller-testing (~> 1.0) rails-i18n (~> 7.0) - rails-settings-cached (~> 0.6)! rdf-normalize (~> 0.5) redcarpet (~> 3.6) redis (~> 4.5) @@ -949,11 +938,9 @@ DEPENDENCIES simple_form (~> 5.2) simplecov (~> 0.22) simplecov-lcov (~> 0.8) - sprockets (~> 3.7.2) - sprockets-rails (~> 3.4) stackprof stoplight (~> 3.0.1) - strong_migrations (= 1.6.4) + strong_migrations (= 1.7.0) test-prof thor (~> 1.2) tty-prompt (~> 0.23) @@ -969,4 +956,4 @@ RUBY VERSION ruby 3.2.2p53 BUNDLED WITH - 2.4.20 + 2.5.4 diff --git a/README.md b/README.md index beab2c3557..267f0ed295 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ To set up **MacOS** for native development, complete the following steps: - Run `brew install postgresql@14` - Run `brew install redis` - Run `brew install imagemagick` +- Run `brew install libidn` - Install Foreman or a similar tool (such as [overmind](https://github.com/DarthSim/overmind)) to handle multiple process launching. - Navigate to Mastodon's root directory and run `brew install nvm` then `nvm use` to use the version from .nvmrc - Run `corepack enable && corepack prepare` @@ -132,7 +133,7 @@ You can open issues for bugs you've found or features you think are missing. You ## License -Copyright (C) 2016-2023 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md)) +Copyright (C) 2016-2024 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md)) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. diff --git a/SECURITY.md b/SECURITY.md index 954ff73a24..81472b01b4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -13,10 +13,8 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through ## Supported Versions -| Version | Supported | -| ------- | ---------------- | -| 4.2.x | Yes | -| 4.1.x | Yes | -| 4.0.x | No | -| 3.5.x | Until 2023-12-31 | -| < 3.5 | No | +| Version | Supported | +| ------- | --------- | +| 4.2.x | Yes | +| 4.1.x | Yes | +| < 4.1 | No | diff --git a/app/controllers/activitypub/followers_synchronizations_controller.rb b/app/controllers/activitypub/followers_synchronizations_controller.rb index 976caa3445..d2942104e5 100644 --- a/app/controllers/activitypub/followers_synchronizations_controller.rb +++ b/app/controllers/activitypub/followers_synchronizations_controller.rb @@ -24,7 +24,7 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro end def set_items - @items = @account.followers.where(Account.arel_table[:uri].matches("#{Account.sanitize_sql_like(uri_prefix)}/%", false, true)).or(@account.followers.where(uri: uri_prefix)).pluck(:uri) + @items = @account.followers.matches_uri_prefix(uri_prefix).pluck(:uri) end def collection_presenter diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index 5ee85474e7..ba85e0a722 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -24,7 +24,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController def unknown_affected_account? json = Oj.load(body, mode: :strict) - json.is_a?(Hash) && %w(Delete Update).include?(json['type']) && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists? + json.is_a?(Hash) && %w(Delete Update).include?(json['type']) && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.exists?(uri: json['actor']) rescue Oj::ParseError false end diff --git a/app/controllers/admin/action_logs_controller.rb b/app/controllers/admin/action_logs_controller.rb index 37a00ad225..8b8e83fde7 100644 --- a/app/controllers/admin/action_logs_controller.rb +++ b/app/controllers/admin/action_logs_controller.rb @@ -6,7 +6,7 @@ module Admin def index authorize :audit_log, :index? - @auditable_accounts = Account.where(id: Admin::ActionLog.select('distinct account_id')).select(:id, :username) + @auditable_accounts = Account.auditable.select(:id, :username) end private diff --git a/app/controllers/admin/confirmations_controller.rb b/app/controllers/admin/confirmations_controller.rb index 6f4e426797..702550eecc 100644 --- a/app/controllers/admin/confirmations_controller.rb +++ b/app/controllers/admin/confirmations_controller.rb @@ -3,11 +3,11 @@ module Admin class ConfirmationsController < BaseController before_action :set_user - before_action :check_confirmation, only: [:resend] + before_action :redirect_confirmed_user, only: [:resend], if: :user_confirmed? def create authorize @user, :confirm? - @user.confirm! + @user.mark_email_as_confirmed! log_action :confirm, @user redirect_to admin_accounts_path end @@ -25,11 +25,13 @@ module Admin private - def check_confirmation - if @user.confirmed? - flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed') - redirect_to admin_accounts_path - end + def redirect_confirmed_user + flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed') + redirect_to admin_accounts_path + end + + def user_confirmed? + @user.confirmed? end end end diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb index 4a3228ec30..faa0a061a6 100644 --- a/app/controllers/admin/email_domain_blocks_controller.rb +++ b/app/controllers/admin/email_domain_blocks_controller.rb @@ -38,9 +38,9 @@ module Admin log_action :create, @email_domain_block (@email_domain_block.other_domains || []).uniq.each do |domain| - next if EmailDomainBlock.where(domain: domain).exists? + next if EmailDomainBlock.exists?(domain: domain) - other_email_domain_block = EmailDomainBlock.create!(domain: domain, parent: @email_domain_block) + other_email_domain_block = EmailDomainBlock.create!(domain: domain, allow_with_approval: @email_domain_block.allow_with_approval, parent: @email_domain_block) log_action :create, other_email_domain_block end end @@ -65,7 +65,7 @@ module Admin end def resource_params - params.require(:email_domain_block).permit(:domain, other_domains: []) + params.require(:email_domain_block).permit(:domain, :allow_with_approval, other_domains: []) end def form_email_domain_block_batch_params diff --git a/app/controllers/admin/export_domain_blocks_controller.rb b/app/controllers/admin/export_domain_blocks_controller.rb index 433b8a1587..9caafd9684 100644 --- a/app/controllers/admin/export_domain_blocks_controller.rb +++ b/app/controllers/admin/export_domain_blocks_controller.rb @@ -49,7 +49,7 @@ module Admin next end - @warning_domains = Instance.where(domain: @domain_blocks.map(&:domain)).where('EXISTS (SELECT 1 FROM follows JOIN accounts ON follows.account_id = accounts.id OR follows.target_account_id = accounts.id WHERE accounts.domain = instances.domain)').pluck(:domain) + @warning_domains = instances_from_imported_blocks.pluck(:domain) rescue ActionController::ParameterMissing flash.now[:alert] = I18n.t('admin.export_domain_blocks.no_file') set_dummy_import! @@ -58,6 +58,10 @@ module Admin private + def instances_from_imported_blocks + Instance.with_domain_follows(@domain_blocks.map(&:domain)) + end + def export_filename 'domain_blocks.csv' end @@ -68,7 +72,7 @@ module Admin def export_data CSV.generate(headers: export_headers, write_headers: true) do |content| - DomainBlock.with_limitations.each do |instance| + DomainBlock.with_limitations.order(id: :asc).each do |instance| content << [instance.domain, instance.severity, instance.reject_media, instance.reject_reports, instance.public_comment, instance.obfuscate] end end diff --git a/app/controllers/admin/follow_recommendations_controller.rb b/app/controllers/admin/follow_recommendations_controller.rb index 841e3cc7fb..a54e41bd8c 100644 --- a/app/controllers/admin/follow_recommendations_controller.rb +++ b/app/controllers/admin/follow_recommendations_controller.rb @@ -8,7 +8,7 @@ module Admin authorize :follow_recommendation, :show? @form = Form::AccountBatch.new - @accounts = filtered_follow_recommendations + @accounts = filtered_follow_recommendations.page(params[:page]) end def update diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index b3c60fcaf4..98fa1897ef 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -108,6 +108,10 @@ class Api::BaseController < ApplicationController render json: { error: 'Your login is currently disabled' }, status: 403 if current_user&.account&.unavailable? end + def require_valid_pagination_options! + render json: { error: 'Pagination values for `offset` and `limit` must be positive' }, status: 400 if pagination_options_invalid? + end + def require_user! if !current_user render json: { error: 'This method requires an authenticated user' }, status: 422 @@ -136,6 +140,10 @@ class Api::BaseController < ApplicationController private + def pagination_options_invalid? + params.slice(:limit, :offset).values.map(&:to_i).any?(&:negative?) + end + def respond_with_error(code) render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code end diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index 21b1095f18..f60181f1eb 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -21,7 +21,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController return [] if hide_results? scope = default_accounts - scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? || current_account.id == @account.id + scope = scope.not_excluded_by_account(current_account) unless current_account.nil? || current_account.id == @account.id scope.merge(paginated_follows).to_a end @@ -30,7 +30,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController end def default_accounts - Account.includes(:active_relationships, :account_stat).references(:active_relationships) + Account.includes(:active_relationships, :account_stat, :user).references(:active_relationships) end def paginated_follows diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index 1db521f79c..3ab8c1efd6 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -21,7 +21,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController return [] if hide_results? scope = default_accounts - scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? || current_account.id == @account.id + scope = scope.not_excluded_by_account(current_account) unless current_account.nil? || current_account.id == @account.id scope.merge(paginated_follows).to_a end @@ -30,7 +30,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController end def default_accounts - Account.includes(:passive_relationships, :account_stat).references(:passive_relationships) + Account.includes(:passive_relationships, :account_stat, :user).references(:passive_relationships) end def paginated_follows diff --git a/app/controllers/api/v1/accounts/notes_controller.rb b/app/controllers/api/v1/accounts/notes_controller.rb index 032e807d11..6d115631a2 100644 --- a/app/controllers/api/v1/accounts/notes_controller.rb +++ b/app/controllers/api/v1/accounts/notes_controller.rb @@ -25,6 +25,6 @@ class Api::V1::Accounts::NotesController < Api::BaseController end def relationships_presenter - AccountRelationshipsPresenter.new([@account.id], current_user.account_id) + AccountRelationshipsPresenter.new([@account], current_user.account_id) end end diff --git a/app/controllers/api/v1/accounts/pins_controller.rb b/app/controllers/api/v1/accounts/pins_controller.rb index 73f845c614..0eb13c048c 100644 --- a/app/controllers/api/v1/accounts/pins_controller.rb +++ b/app/controllers/api/v1/accounts/pins_controller.rb @@ -25,6 +25,6 @@ class Api::V1::Accounts::PinsController < Api::BaseController end def relationships_presenter - AccountRelationshipsPresenter.new([@account.id], current_user.account_id) + AccountRelationshipsPresenter.new([@account], current_user.account_id) end end diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb index e5ae5b007b..d43832177a 100644 --- a/app/controllers/api/v1/accounts/relationships_controller.rb +++ b/app/controllers/api/v1/accounts/relationships_controller.rb @@ -5,7 +5,7 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController before_action :require_user! def index - @accounts = Account.where(id: account_ids).select('id') + @accounts = Account.where(id: account_ids).select(:id, :domain) @accounts.merge!(Account.without_suspended) unless truthy_param?(:with_suspended) render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index be251b4259..23fc85b475 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -88,7 +88,7 @@ class Api::V1::AccountsController < Api::BaseController end def relationships(**options) - AccountRelationshipsPresenter.new([@account.id], current_user.account_id, **options) + AccountRelationshipsPresenter.new([@account], current_user.account_id, **options) end def account_params diff --git a/app/controllers/api/v1/admin/email_domain_blocks_controller.rb b/app/controllers/api/v1/admin/email_domain_blocks_controller.rb index 850eda6224..df54b9f0a4 100644 --- a/app/controllers/api/v1/admin/email_domain_blocks_controller.rb +++ b/app/controllers/api/v1/admin/email_domain_blocks_controller.rb @@ -55,7 +55,7 @@ class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController end def resource_params - params.permit(:domain) + params.permit(:domain, :allow_with_approval) end def insert_pagination_headers diff --git a/app/controllers/api/v1/annual_reports_controller.rb b/app/controllers/api/v1/annual_reports_controller.rb new file mode 100644 index 0000000000..9bc8e68ac2 --- /dev/null +++ b/app/controllers/api/v1/annual_reports_controller.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class Api::V1::AnnualReportsController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index + before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index + before_action :require_user! + before_action :set_annual_report, except: :index + + def index + with_read_replica do + @presenter = AnnualReportsPresenter.new(GeneratedAnnualReport.where(account_id: current_account.id).pending) + @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 + end + + private + + def set_annual_report + @annual_report = GeneratedAnnualReport.find_by!(account_id: current_account.id, year: params[:id]) + end +end diff --git a/app/controllers/api/v1/blocks_controller.rb b/app/controllers/api/v1/blocks_controller.rb index 06a8bfa891..0934622f88 100644 --- a/app/controllers/api/v1/blocks_controller.rb +++ b/app/controllers/api/v1/blocks_controller.rb @@ -17,7 +17,7 @@ class Api::V1::BlocksController < Api::BaseController end def paginated_blocks - @paginated_blocks ||= Block.eager_load(target_account: :account_stat) + @paginated_blocks ||= Block.eager_load(target_account: [:account_stat, :user]) .joins(:target_account) .merge(Account.without_suspended) .where(account: current_account) diff --git a/app/controllers/api/v1/directories_controller.rb b/app/controllers/api/v1/directories_controller.rb index 35c504a7ff..6c540404ea 100644 --- a/app/controllers/api/v1/directories_controller.rb +++ b/app/controllers/api/v1/directories_controller.rb @@ -12,7 +12,7 @@ class Api::V1::DirectoriesController < Api::BaseController private def require_enabled! - return not_found unless Setting.profile_directory + not_found unless Setting.profile_directory end def set_accounts @@ -27,7 +27,7 @@ class Api::V1::DirectoriesController < Api::BaseController scope.merge!(local_account_scope) if local_accounts? scope.merge!(account_exclusion_scope) if current_account scope.merge!(account_domain_block_scope) if current_account && !local_accounts? - end + end.includes(:account_stat, user: :role) end def local_accounts? diff --git a/app/controllers/api/v1/endorsements_controller.rb b/app/controllers/api/v1/endorsements_controller.rb index 46e3fcd647..2216a9860d 100644 --- a/app/controllers/api/v1/endorsements_controller.rb +++ b/app/controllers/api/v1/endorsements_controller.rb @@ -25,7 +25,7 @@ class Api::V1::EndorsementsController < Api::BaseController end def endorsed_accounts - current_account.endorsed_accounts.includes(:account_stat).without_suspended + current_account.endorsed_accounts.includes(:account_stat, :user).without_suspended end def insert_pagination_headers diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb index 7c197ce6ba..87f6df5f94 100644 --- a/app/controllers/api/v1/follow_requests_controller.rb +++ b/app/controllers/api/v1/follow_requests_controller.rb @@ -25,11 +25,11 @@ class Api::V1::FollowRequestsController < Api::BaseController private def account - Account.find(params[:id]) + @account ||= Account.find(params[:id]) end def relationships(**options) - AccountRelationshipsPresenter.new([params[:id]], current_user.account_id, **options) + AccountRelationshipsPresenter.new([account], current_user.account_id, **options) end def load_accounts @@ -37,7 +37,7 @@ class Api::V1::FollowRequestsController < Api::BaseController end def default_accounts - Account.without_suspended.includes(:follow_requests, :account_stat).references(:follow_requests) + Account.without_suspended.includes(:follow_requests, :account_stat, :user).references(:follow_requests) end def paginated_follow_requests diff --git a/app/controllers/api/v1/lists/accounts_controller.rb b/app/controllers/api/v1/lists/accounts_controller.rb index 8e12cb7b65..0604ad60fc 100644 --- a/app/controllers/api/v1/lists/accounts_controller.rb +++ b/app/controllers/api/v1/lists/accounts_controller.rb @@ -37,9 +37,9 @@ class Api::V1::Lists::AccountsController < Api::BaseController def load_accounts if unlimited? - @list.accounts.without_suspended.includes(:account_stat).all + @list.accounts.without_suspended.includes(:account_stat, :user).all else - @list.accounts.without_suspended.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) + @list.accounts.without_suspended.includes(:account_stat, :user).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) end end diff --git a/app/controllers/api/v1/markers_controller.rb b/app/controllers/api/v1/markers_controller.rb index f8dfba8a94..8eaf7767df 100644 --- a/app/controllers/api/v1/markers_controller.rb +++ b/app/controllers/api/v1/markers_controller.rb @@ -19,7 +19,7 @@ class Api::V1::MarkersController < Api::BaseController @markers = {} resource_params.each_pair do |timeline, timeline_params| - @markers[timeline] = current_user.markers.find_or_initialize_by(timeline: timeline) + @markers[timeline] = current_user.markers.find_or_create_by(timeline: timeline) @markers[timeline].update!(timeline_params) end end diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb index 555485823c..2fb685ac39 100644 --- a/app/controllers/api/v1/mutes_controller.rb +++ b/app/controllers/api/v1/mutes_controller.rb @@ -17,7 +17,7 @@ class Api::V1::MutesController < Api::BaseController end def paginated_mutes - @paginated_mutes ||= Mute.eager_load(:target_account) + @paginated_mutes ||= Mute.eager_load(target_account: [:account_stat, :user]) .joins(:target_account) .merge(Account.without_suspended) .where(account: current_account) diff --git a/app/controllers/api/v1/peers/search_controller.rb b/app/controllers/api/v1/peers/search_controller.rb index 0c503d9bc5..1780554c5d 100644 --- a/app/controllers/api/v1/peers/search_controller.rb +++ b/app/controllers/api/v1/peers/search_controller.rb @@ -27,7 +27,7 @@ class Api::V1::Peers::SearchController < Api::BaseController @domains = InstancesIndex.query(function_score: { query: { prefix: { - domain: TagManager.instance.normalize_domain(params[:q].strip), + domain: normalized_domain, }, }, @@ -37,11 +37,18 @@ class Api::V1::Peers::SearchController < Api::BaseController }, }).limit(10).pluck(:domain) else - domain = params[:q].strip - domain = TagManager.instance.normalize_domain(domain) - @domains = Instance.searchable.where(Instance.arel_table[:domain].matches("#{Instance.sanitize_sql_like(domain)}%", false, true)).limit(10).pluck(:domain) + domain = normalized_domain + @domains = Instance.searchable.domain_starts_with(domain).limit(10).pluck(:domain) end rescue Addressable::URI::InvalidURIError @domains = [] end + + def normalized_domain + TagManager.instance.normalize_domain(query_value) + end + + def query_value + params[:q].strip + end end diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb index 3cca246ce8..069ad37cb2 100644 --- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb @@ -14,14 +14,14 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::V1::Statuses::Bas def load_accounts scope = default_accounts - scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? + scope = scope.not_excluded_by_account(current_account) unless current_account.nil? scope.merge(paginated_favourites).to_a end def default_accounts Account .without_suspended - .includes(:favourites, :account_stat) + .includes(:favourites, :account_stat, :user) .references(:favourites) .where(favourites: { status_id: @status.id }) end diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb index dd3e60846b..b8a997518d 100644 --- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb @@ -14,12 +14,12 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::V1::Statuses::Base def load_accounts scope = default_accounts - scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? + scope = scope.not_excluded_by_account(current_account) unless current_account.nil? scope.merge(paginated_statuses).to_a end def default_accounts - Account.without_suspended.includes(:statuses, :account_stat).references(:statuses) + Account.without_suspended.includes(:statuses, :account_stat, :user).references(:statuses) end def paginated_statuses diff --git a/app/controllers/api/v1/streaming_controller.rb b/app/controllers/api/v1/streaming_controller.rb index 0cdd00d62f..adb14676e1 100644 --- a/app/controllers/api/v1/streaming_controller.rb +++ b/app/controllers/api/v1/streaming_controller.rb @@ -2,7 +2,7 @@ class Api::V1::StreamingController < Api::BaseController def index - if Rails.configuration.x.streaming_api_base_url == request.host + if same_host? not_found else redirect_to streaming_api_url, status: 301, allow_other_host: true @@ -11,9 +11,16 @@ class Api::V1::StreamingController < Api::BaseController private + def same_host? + base_url = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url) + request.host == base_url.host && request.port == (base_url.port || 80) + end + def streaming_api_url Addressable::URI.parse(request.url).tap do |uri| - uri.host = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url).host + base_url = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url) + uri.host = base_url.host + uri.port = base_url.port end.to_s end end diff --git a/app/controllers/api/v1/suggestions_controller.rb b/app/controllers/api/v1/suggestions_controller.rb index 9737ae5cb6..9ba1cef63c 100644 --- a/app/controllers/api/v1/suggestions_controller.rb +++ b/app/controllers/api/v1/suggestions_controller.rb @@ -3,22 +3,23 @@ class Api::V1::SuggestionsController < Api::BaseController include Authorization - before_action -> { doorkeeper_authorize! :read } + before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index + before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index before_action :require_user! + before_action :set_suggestions def index - suggestions = suggestions_source.get(current_account, limit: limit_param(DEFAULT_ACCOUNTS_LIMIT)) - render json: suggestions.map(&:account), each_serializer: REST::AccountSerializer + render json: @suggestions.get(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:offset].to_i).map(&:account), each_serializer: REST::AccountSerializer end def destroy - suggestions_source.remove(current_account, params[:id]) + @suggestions.remove(params[:id]) render_empty end private - def suggestions_source - AccountSuggestions::PastInteractionsSource.new + def set_suggestions + @suggestions = AccountSuggestions.new(current_account) end end diff --git a/app/controllers/api/v2/filters_controller.rb b/app/controllers/api/v2/filters_controller.rb index 2fcdeeae45..09d4813f34 100644 --- a/app/controllers/api/v2/filters_controller.rb +++ b/app/controllers/api/v2/filters_controller.rb @@ -35,7 +35,7 @@ class Api::V2::FiltersController < Api::BaseController private def set_filters - @filters = current_account.custom_filters.includes(:keywords) + @filters = current_account.custom_filters.includes(:keywords, :statuses) end def set_filter diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb index 4339bee21e..3cfc6e7919 100644 --- a/app/controllers/api/v2/search_controller.rb +++ b/app/controllers/api/v2/search_controller.rb @@ -12,6 +12,7 @@ class Api::V2::SearchController < Api::BaseController before_action :query_pagination_error, if: :pagination_requested? before_action :remote_resolve_error, if: :remote_resolve_requested? end + before_action :require_valid_pagination_options! def index @search = Search.new(search_results) diff --git a/app/controllers/api/v2/suggestions_controller.rb b/app/controllers/api/v2/suggestions_controller.rb index 35eb276c01..8516796e86 100644 --- a/app/controllers/api/v2/suggestions_controller.rb +++ b/app/controllers/api/v2/suggestions_controller.rb @@ -3,17 +3,23 @@ class Api::V2::SuggestionsController < Api::BaseController include Authorization - before_action -> { doorkeeper_authorize! :read } + before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index + before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index before_action :require_user! before_action :set_suggestions def index - render json: @suggestions, each_serializer: REST::SuggestionSerializer + render json: @suggestions.get(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:offset].to_i), each_serializer: REST::SuggestionSerializer + end + + def destroy + @suggestions.remove(params[:id]) + render_empty end private def set_suggestions - @suggestions = AccountSuggestions.get(current_account, limit_param(DEFAULT_ACCOUNTS_LIMIT)) + @suggestions = AccountSuggestions.new(current_account) end end diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb index 9f6be9c424..7ca7be5f8e 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -7,7 +7,7 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController before_action :set_body_classes before_action :set_confirmation_user!, only: [:show, :confirm_captcha] - before_action :require_unconfirmed! + before_action :redirect_confirmed_user, if: :signed_in_confirmed_user? before_action :extend_csp_for_captcha!, only: [:show, :confirm_captcha] before_action :require_captcha_if_needed!, only: [:show] @@ -62,13 +62,15 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController end def captcha_user_bypass? - return true if @confirmation_user.nil? || @confirmation_user.confirmed? + @confirmation_user.nil? || @confirmation_user.confirmed? end - def require_unconfirmed! - if user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank? - redirect_to(current_user.approved? ? root_path : edit_user_registration_path) - end + def redirect_confirmed_user + redirect_to(current_user.approved? ? root_path : edit_user_registration_path) + end + + def signed_in_confirmed_user? + user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank? end def set_body_classes diff --git a/app/controllers/auth/passwords_controller.rb b/app/controllers/auth/passwords_controller.rb index a752194d5b..de001f062b 100644 --- a/app/controllers/auth/passwords_controller.rb +++ b/app/controllers/auth/passwords_controller.rb @@ -2,7 +2,7 @@ class Auth::PasswordsController < Devise::PasswordsController skip_before_action :check_self_destruct! - before_action :check_validity_of_reset_password_token, only: :edit + before_action :redirect_invalid_reset_token, only: :edit, unless: :reset_password_token_is_valid? before_action :set_body_classes layout 'auth' @@ -19,11 +19,9 @@ class Auth::PasswordsController < Devise::PasswordsController private - def check_validity_of_reset_password_token - unless reset_password_token_is_valid? - flash[:error] = I18n.t('auth.invalid_reset_password_token') - redirect_to new_password_path(resource_name) - end + def redirect_invalid_reset_token + flash[:error] = I18n.t('auth.invalid_reset_password_token') + redirect_to new_password_path(resource_name) end def set_body_classes diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index 148ad53755..962b78de65 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true class Auth::SessionsController < Devise::SessionsController + include Redisable + + MAX_2FA_ATTEMPTS_PER_HOUR = 10 + layout 'auth' skip_before_action :check_self_destruct! @@ -130,9 +134,23 @@ class Auth::SessionsController < Devise::SessionsController session.delete(:attempt_user_updated_at) end + def clear_2fa_attempt_from_user(user) + redis.del(second_factor_attempts_key(user)) + end + + def check_second_factor_rate_limits(user) + attempts, = redis.multi do |multi| + multi.incr(second_factor_attempts_key(user)) + multi.expire(second_factor_attempts_key(user), 1.hour) + end + + attempts >= MAX_2FA_ATTEMPTS_PER_HOUR + end + def on_authentication_success(user, security_measure) @on_authentication_success_called = true + clear_2fa_attempt_from_user(user) clear_attempt_from_session user.update_sign_in!(new_sign_in: true) @@ -163,5 +181,14 @@ class Auth::SessionsController < Devise::SessionsController ip: request.remote_ip, user_agent: request.user_agent ) + + # Only send a notification email every hour at most + return if redis.set("2fa_failure_notification:#{user.id}", '1', ex: 1.hour, get: true).present? + + UserMailer.failed_2fa(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later! + end + + def second_factor_attempts_key(user) + "2fa_auth_attempts:#{user.id}:#{Time.now.utc.hour}" end end diff --git a/app/controllers/concerns/auth/two_factor_authentication_concern.rb b/app/controllers/concerns/auth/two_factor_authentication_concern.rb index effdb8d21c..404164751a 100644 --- a/app/controllers/concerns/auth/two_factor_authentication_concern.rb +++ b/app/controllers/concerns/auth/two_factor_authentication_concern.rb @@ -66,6 +66,11 @@ module Auth::TwoFactorAuthenticationConcern end def authenticate_with_two_factor_via_otp(user) + if check_second_factor_rate_limits(user) + flash.now[:alert] = I18n.t('users.rate_limited') + return prompt_for_two_factor(user) + end + if valid_otp_attempt?(user) on_authentication_success(user, :otp) else diff --git a/app/controllers/concerns/cache_concern.rb b/app/controllers/concerns/cache_concern.rb index 2dfe5e263a..62f763fe2f 100644 --- a/app/controllers/concerns/cache_concern.rb +++ b/app/controllers/concerns/cache_concern.rb @@ -3,150 +3,6 @@ module CacheConcern extend ActiveSupport::Concern - module ActiveRecordCoder - EMPTY_HASH = {}.freeze - - class << self - def dump(record) - instances = InstanceTracker.new - serialized_associations = serialize_associations(record, instances) - serialized_records = instances.map { |r| serialize_record(r) } - [serialized_associations, *serialized_records] - end - - def load(payload) - instances = InstanceTracker.new - serialized_associations, *serialized_records = payload - serialized_records.each { |attrs| instances.push(deserialize_record(*attrs)) } - deserialize_associations(serialized_associations, instances) - end - - private - - # Records without associations, or which have already been visited before, - # are serialized by their id alone. - # - # Records with associations are serialized as a two-element array including - # their id and the record's association cache. - # - def serialize_associations(record, instances) - return unless record - - if (id = instances.lookup(record)) - payload = id - else - payload = instances.push(record) - - cached_associations = record.class.reflect_on_all_associations.select do |reflection| - record.association_cached?(reflection.name) - end - - unless cached_associations.empty? - serialized_associations = cached_associations.map do |reflection| - association = record.association(reflection.name) - - serialized_target = if reflection.collection? - association.target.map { |target_record| serialize_associations(target_record, instances) } - else - serialize_associations(association.target, instances) - end - - [reflection.name, serialized_target] - end - - payload = [payload, serialized_associations] - end - end - - payload - end - - def deserialize_associations(payload, instances) - return unless payload - - id, associations = payload - record = instances.fetch(id) - - associations&.each do |name, serialized_target| - begin - association = record.association(name) - rescue ActiveRecord::AssociationNotFoundError - raise AssociationMissingError, "undefined association: #{name}" - end - - target = if association.reflection.collection? - serialized_target.map! { |serialized_record| deserialize_associations(serialized_record, instances) } - else - deserialize_associations(serialized_target, instances) - end - - association.target = target - end - - record - end - - def serialize_record(record) - arguments = [record.class.name, attributes_for_database(record)] - arguments << true if record.new_record? - arguments - end - - def attributes_for_database(record) - attributes = record.attributes_for_database - attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr } - attributes - end - - def deserialize_record(class_name, attributes_from_database, new_record = false) # rubocop:disable Style/OptionalBooleanParameter - begin - klass = Object.const_get(class_name) - rescue NameError - raise ClassMissingError, "undefined class: #{class_name}" - end - - # Ideally we'd like to call `klass.instantiate`, however it doesn't allow to pass - # wether the record was persisted or not. - attributes = klass.attributes_builder.build_from_database(attributes_from_database, EMPTY_HASH) - klass.allocate.init_with_attributes(attributes, new_record) - end - end - - class Error < StandardError - end - - class ClassMissingError < Error - end - - class AssociationMissingError < Error - end - - class InstanceTracker - def initialize - @instances = [] - @ids = {}.compare_by_identity - end - - def map(&block) - @instances.map(&block) - end - - def fetch(...) - @instances.fetch(...) - end - - def push(instance) - id = @ids[instance] = @instances.size - @instances << instance - id - end - - def lookup(instance) - @ids[instance] - end - end - end - class_methods do def vary_by(value, **kwargs) before_action(**kwargs) do |controller| @@ -196,11 +52,7 @@ module CacheConcern raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation) return [] if raw.empty? - cached_keys_with_value = begin - Rails.cache.read_multi(*raw).transform_keys(&:id).transform_values { |r| ActiveRecordCoder.load(r) } - rescue ActiveRecordCoder::Error - {} # The serialization format may have changed, let's pretend it's a cache miss. - end + cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id) uncached_ids = raw.map(&:id) - cached_keys_with_value.keys @@ -208,10 +60,7 @@ module CacheConcern unless uncached_ids.empty? uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id) - - uncached.each_value do |item| - Rails.cache.write(item, ActiveRecordCoder.dump(item)) - end + Rails.cache.write_multi(uncached.values.to_h { |i| [i, i] }) end raw.filter_map { |item| cached_keys_with_value[item.id] || uncached[item.id] } diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index f0a344f1c9..35391e64c4 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -91,14 +91,23 @@ module SignatureVerification raise 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 + compare_signed_string = build_signed_string(include_query_string: true) return actor unless verify_signature(actor, signature, compare_signed_string).nil? + # Compatibility quirk with older Mastodon versions + compare_signed_string = build_signed_string(include_query_string: false) + return actor unless verify_signature(actor, signature, compare_signed_string).nil? + actor = stoplight_wrap_request { actor_refresh_key!(actor) } raise 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? + + # Compatibility quirk with older Mastodon versions + compare_signed_string = build_signed_string(include_query_string: false) 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'] @@ -180,11 +189,18 @@ module SignatureVerification nil end - def build_signed_string + def build_signed_string(include_query_string: true) signed_headers.map do |signed_header| case signed_header when Request::REQUEST_TARGET - "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" + if include_query_string + "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.original_fullpath}" + else + # Current versions of Mastodon incorrectly omit the query string from the (request-target) pseudo-header. + # Therefore, temporarily support such incorrect signatures for compatibility. + # TODO: remove eventually some time after release of the fixed version + "#{Request::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? diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb index 5687d6e5b6..b8c909877b 100644 --- a/app/controllers/concerns/web_app_controller_concern.rb +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -21,10 +21,19 @@ module WebAppControllerConcern def redirect_unauthenticated_to_permalinks! return if user_signed_in? && current_account.moved_to_account_id.nil? - redirect_path = PermalinkRedirector.new(request.path).redirect_path - return if redirect_path.blank? + permalink_redirector = PermalinkRedirector.new(request.path) + return if permalink_redirector.redirect_path.blank? expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? - redirect_to(redirect_path) + + respond_to do |format| + format.html do + redirect_to(permalink_redirector.redirect_confirmation_path, allow_other_host: false) + end + + format.json do + redirect_to(permalink_redirector.redirect_uri, allow_other_host: true) + end + end end end diff --git a/app/controllers/custom_css_controller.rb b/app/controllers/custom_css_controller.rb index e7a02ea89c..62f8e0d772 100644 --- a/app/controllers/custom_css_controller.rb +++ b/app/controllers/custom_css_controller.rb @@ -1,8 +1,21 @@ # 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 render content_type: 'text/css' end + + private + + def custom_css_styles + Setting.custom_css + end + helper_method :custom_css_styles + + def set_user_roles + @user_roles = UserRole.where(highlighted: true).where.not(color: [nil, '']) + end end diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb index 2a22a05570..7bc424d0a4 100644 --- a/app/controllers/health_controller.rb +++ b/app/controllers/health_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class HealthController < ActionController::Base +class HealthController < ActionController::Base # rubocop:disable Rails/ApplicationController def show render plain: 'OK' end diff --git a/app/controllers/redirect/accounts_controller.rb b/app/controllers/redirect/accounts_controller.rb new file mode 100644 index 0000000000..713ccf2ca1 --- /dev/null +++ b/app/controllers/redirect/accounts_controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class Redirect::AccountsController < Redirect::BaseController + private + + def set_resource + @resource = Account.find(params[:id]) + not_found if @resource.local? + end +end diff --git a/app/controllers/redirect/base_controller.rb b/app/controllers/redirect/base_controller.rb new file mode 100644 index 0000000000..90894ec1ed --- /dev/null +++ b/app/controllers/redirect/base_controller.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class Redirect::BaseController < ApplicationController + vary_by 'Accept-Language' + + before_action :set_resource + before_action :set_app_body_class + + def show + @redirect_path = ActivityPub::TagManager.instance.url_for(@resource) + + render 'redirects/show', layout: 'application' + end + + private + + def set_app_body_class + @body_classes = 'app-body' + end + + def set_resource + raise NotImplementedError + end +end diff --git a/app/controllers/redirect/statuses_controller.rb b/app/controllers/redirect/statuses_controller.rb new file mode 100644 index 0000000000..37a938c651 --- /dev/null +++ b/app/controllers/redirect/statuses_controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class Redirect::StatusesController < Redirect::BaseController + private + + def set_resource + @resource = Status.find(params[:id]) + not_found if @resource.local? || !@resource.distributable? + end +end diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb index e87b5a656f..dd794f3199 100644 --- a/app/controllers/relationships_controller.rb +++ b/app/controllers/relationships_controller.rb @@ -33,7 +33,7 @@ class RelationshipsController < ApplicationController end def set_relationships - @relationships = AccountRelationshipsPresenter.new(@accounts.pluck(:id), current_user.account_id) + @relationships = AccountRelationshipsPresenter.new(@accounts, current_user.account_id) end def form_account_batch_params diff --git a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb index c86ede4f3a..9714d54f95 100644 --- a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb +++ b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb @@ -6,8 +6,8 @@ module Settings skip_before_action :check_self_destruct! skip_before_action :require_functional! - before_action :require_otp_enabled - before_action :require_webauthn_enabled, only: [:index, :destroy] + before_action :redirect_invalid_otp, unless: -> { current_user.otp_enabled? } + before_action :redirect_invalid_webauthn, only: [:index, :destroy], unless: -> { current_user.webauthn_enabled? } def index; end def new; end @@ -85,18 +85,14 @@ module Settings private - def require_otp_enabled - unless current_user.otp_enabled? - flash[:error] = t('webauthn_credentials.otp_required') - redirect_to settings_two_factor_authentication_methods_path - end + def redirect_invalid_otp + flash[:error] = t('webauthn_credentials.otp_required') + redirect_to settings_two_factor_authentication_methods_path end - def require_webauthn_enabled - unless current_user.webauthn_enabled? - flash[:error] = t('webauthn_credentials.not_enabled') - redirect_to settings_two_factor_authentication_methods_path - end + def redirect_invalid_webauthn + flash[:error] = t('webauthn_credentials.not_enabled') + redirect_to settings_two_factor_authentication_methods_path end end end diff --git a/app/controllers/well_known/webfinger_controller.rb b/app/controllers/well_known/webfinger_controller.rb index 364fbf8a18..72f0ea890f 100644 --- a/app/controllers/well_known/webfinger_controller.rb +++ b/app/controllers/well_known/webfinger_controller.rb @@ -21,7 +21,7 @@ module WellKnown username = username_from_resource @account = begin - if username == Rails.configuration.x.local_domain + if username == Rails.configuration.x.local_domain || username == Rails.configuration.x.web_domain Account.representative else Account.find_local!(username) diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb index 6301919a9e..158a0815e1 100644 --- a/app/helpers/accounts_helper.rb +++ b/app/helpers/accounts_helper.rb @@ -27,20 +27,24 @@ module AccountsHelper end end + def account_formatted_stat(value) + number_to_human(value, precision: 3, strip_insignificant_zeros: true) + end + def account_description(account) prepend_str = [ [ - number_to_human(account.statuses_count, precision: 3, strip_insignificant_zeros: true), + account_formatted_stat(account.statuses_count), I18n.t('accounts.posts', count: account.statuses_count), ].join(' '), [ - number_to_human(account.following_count, precision: 3, strip_insignificant_zeros: true), + account_formatted_stat(account.following_count), I18n.t('accounts.following', count: account.following_count), ].join(' '), [ - number_to_human(account.followers_count, precision: 3, strip_insignificant_zeros: true), + account_formatted_stat(account.followers_count), I18n.t('accounts.followers', count: account.followers_count), ].join(' '), ].join(', ') diff --git a/app/helpers/admin/settings_helper.rb b/app/helpers/admin/settings_helper.rb index 552a3ee5a8..6937331e1a 100644 --- a/app/helpers/admin/settings_helper.rb +++ b/app/helpers/admin/settings_helper.rb @@ -4,4 +4,60 @@ module Admin::SettingsHelper def captcha_available? ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present? end + + def login_activity_title(activity) + t( + "login_activities.#{login_activity_key(activity)}", + method: login_activity_method(activity), + ip: login_activity_ip(activity), + browser: login_activity_browser(activity) + ) + end + + private + + def login_activity_key(activity) + activity.success? ? 'successful_sign_in_html' : 'failed_sign_in_html' + end + + def login_activity_method(activity) + content_tag( + :span, + login_activity_method_string(activity), + class: 'target' + ) + end + + def login_activity_ip(activity) + content_tag( + :span, + activity.ip, + class: 'target' + ) + end + + def login_activity_browser(activity) + content_tag( + :span, + login_activity_browser_description(activity), + class: 'target', + title: activity.user_agent + ) + end + + def login_activity_method_string(activity) + if activity.omniauth? + t("auth.providers.#{activity.provider}") + else + t("login_activities.authentication_methods.#{activity.authentication_method}") + end + end + + def login_activity_browser_description(activity) + t( + 'sessions.description', + browser: t(activity.browser, scope: 'sessions.browsers', default: activity.browser.to_s), + platform: t(activity.platform, scope: 'sessions.platforms', default: activity.platform.to_s) + ) + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 48d9119fbd..4f7f66985d 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -110,11 +110,11 @@ module ApplicationHelper def can?(action, record) return false if record.nil? - policy(record).public_send("#{action}?") + policy(record).public_send(:"#{action}?") end def fa_icon(icon, attributes = {}) - class_names = attributes[:class]&.split(' ') || [] + class_names = attributes[:class]&.split || [] class_names << 'fa' class_names += icon.split.map { |cl| "fa-#{cl}" } diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index ce3ff094f6..b3d0d032c4 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -155,7 +155,7 @@ module JsonLdHelper end end - def fetch_resource(uri, id, on_behalf_of = nil) + def fetch_resource(uri, id, on_behalf_of = nil, request_options: {}) unless id json = fetch_resource_without_id_validation(uri, on_behalf_of) @@ -164,14 +164,14 @@ module JsonLdHelper uri = json['id'] end - json = fetch_resource_without_id_validation(uri, on_behalf_of) + json = fetch_resource_without_id_validation(uri, on_behalf_of, 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) + def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false, request_options: {}) on_behalf_of ||= Account.representative - build_request(uri, on_behalf_of).perform do |response| + 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 body_to_json(response.body_with_limit) if response.code == 200 @@ -204,8 +204,8 @@ module JsonLdHelper response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code)) end - def build_request(uri, on_behalf_of = nil) - Request.new(:get, uri).tap do |request| + def build_request(uri, on_behalf_of = nil, options: {}) + Request.new(:get, uri, **options).tap do |request| request.on_behalf_of(on_behalf_of) if on_behalf_of request.add_headers('Accept' => 'application/activity+json, application/ld+json') end diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb index 0d4edceb7b..87f0f288d3 100644 --- a/app/helpers/languages_helper.rb +++ b/app/helpers/languages_helper.rb @@ -224,7 +224,7 @@ module LanguagesHelper 'en-GB': 'English (British)', 'es-AR': 'EspaÃąol (Argentina)', 'es-MX': 'EspaÃąol (MÊxico)', - 'fr-QC': 'Français (Canadien)', + 'fr-CA': 'Français (Canadien)', 'pt-BR': 'PortuguÃĒs (Brasil)', 'pt-PT': 'PortuguÃĒs (Portugal)', 'sr-Latn': 'Srpski (latinica)', diff --git a/app/helpers/mascot_helper.rb b/app/helpers/mascot_helper.rb index 8ee04383ec..34b656411e 100644 --- a/app/helpers/mascot_helper.rb +++ b/app/helpers/mascot_helper.rb @@ -2,7 +2,7 @@ module MascotHelper def mascot_url - full_asset_url(instance_presenter.mascot&.file&.url || asset_pack_path('media/images/elephant_ui_plane.svg')) + full_asset_url(instance_presenter.mascot&.file&.url || frontend_asset_path('images/elephant_ui_plane.svg')) end def instance_presenter diff --git a/app/helpers/routing_helper.rb b/app/helpers/routing_helper.rb index 2fb9ce72cb..15d988f64d 100644 --- a/app/helpers/routing_helper.rb +++ b/app/helpers/routing_helper.rb @@ -24,8 +24,12 @@ module RoutingHelper Rails.configuration.action_controller.asset_host || root_url end - def full_pack_url(source, **options) - full_asset_url(asset_pack_path(source, **options)) + def frontend_asset_path(source, **options) + asset_pack_path("media/#{source}", **options) + end + + def frontend_asset_url(source, **options) + full_asset_url(frontend_asset_path(source, **options)) end def use_storage? diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 3c72b22c66..10863a316c 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -9,6 +9,19 @@ module SettingsHelper LanguagesHelper.sorted_locale_keys(I18n.available_locales) end + def featured_tags_hint(recently_used_tags) + safe_join( + [ + t('simple_form.hints.featured_tag.name'), + safe_join( + links_for_featured_tags(recently_used_tags), + ', ' + ), + ], + ' ' + ) + end + def session_device_icon(session) device = session.detection.device @@ -28,4 +41,18 @@ module SettingsHelper safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: '', class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ') end end + + private + + def links_for_featured_tags(tags) + tags.map { |tag| post_link_to_featured_tag(tag) } + end + + def post_link_to_featured_tag(tag) + link_to( + "##{tag.display_name}", + settings_featured_tags_path(featured_tag: { name: tag.name }), + method: :post + ) + end end diff --git a/app/javascript/__mocks__/svg.js b/app/javascript/__mocks__/svg.js index e725dc2da6..762bc165d0 100644 --- a/app/javascript/__mocks__/svg.js +++ b/app/javascript/__mocks__/svg.js @@ -1,3 +1,3 @@ -// eslint-disable-next-line import/no-anonymous-default-export -export default 'SvgrURL'; -export const ReactComponent = 'div'; +const ReactComponent = 'div'; + +export default ReactComponent; diff --git a/app/javascript/fonts/inter/inter-variable-font-slnt-wght.woff2 b/app/javascript/fonts/inter/inter-variable-font-slnt-wght.woff2 new file mode 100644 index 0000000000..e6345f2e3d Binary files /dev/null and b/app/javascript/fonts/inter/inter-variable-font-slnt-wght.woff2 differ diff --git a/app/javascript/images/mailer-new/common/header-bg-end.png b/app/javascript/images/mailer-new/common/header-bg-end.png new file mode 100644 index 0000000000..900196678a Binary files /dev/null and b/app/javascript/images/mailer-new/common/header-bg-end.png differ diff --git a/app/javascript/images/mailer-new/common/header-bg-start.png b/app/javascript/images/mailer-new/common/header-bg-start.png new file mode 100644 index 0000000000..0037c1ad93 Binary files /dev/null and b/app/javascript/images/mailer-new/common/header-bg-start.png differ diff --git a/app/javascript/images/mailer-new/common/logo-footer.png b/app/javascript/images/mailer-new/common/logo-footer.png new file mode 100644 index 0000000000..2baafd8d7f Binary files /dev/null and b/app/javascript/images/mailer-new/common/logo-footer.png differ diff --git a/app/javascript/images/mailer-new/common/logo-header.png b/app/javascript/images/mailer-new/common/logo-header.png new file mode 100644 index 0000000000..46a6bddaa1 Binary files /dev/null and b/app/javascript/images/mailer-new/common/logo-header.png differ diff --git a/app/javascript/images/mailer-new/heading/2fa-disabled.png b/app/javascript/images/mailer-new/heading/2fa-disabled.png new file mode 100644 index 0000000000..b1e342a87c Binary files /dev/null and b/app/javascript/images/mailer-new/heading/2fa-disabled.png differ diff --git a/app/javascript/images/mailer-new/heading/2fa-enabled.png b/app/javascript/images/mailer-new/heading/2fa-enabled.png new file mode 100644 index 0000000000..3ce3e04f84 Binary files /dev/null and b/app/javascript/images/mailer-new/heading/2fa-enabled.png differ diff --git a/app/javascript/images/mailer-new/heading/2fa-recovery.png b/app/javascript/images/mailer-new/heading/2fa-recovery.png new file mode 100644 index 0000000000..cefb21e1eb Binary files /dev/null and b/app/javascript/images/mailer-new/heading/2fa-recovery.png differ diff --git a/app/javascript/images/mailer-new/heading/appeal-approved.png b/app/javascript/images/mailer-new/heading/appeal-approved.png new file mode 100755 index 0000000000..b2476ec346 Binary files /dev/null and b/app/javascript/images/mailer-new/heading/appeal-approved.png differ diff --git a/app/javascript/images/mailer-new/heading/appeal-rejected.png b/app/javascript/images/mailer-new/heading/appeal-rejected.png new file mode 100644 index 0000000000..7ae38ad0c1 Binary files /dev/null and b/app/javascript/images/mailer-new/heading/appeal-rejected.png differ diff --git a/app/javascript/images/mailer-new/heading/archive.png b/app/javascript/images/mailer-new/heading/archive.png new file mode 100644 index 0000000000..b0c7fad84d Binary files /dev/null and b/app/javascript/images/mailer-new/heading/archive.png differ diff --git a/app/javascript/images/mailer-new/heading/boost.png b/app/javascript/images/mailer-new/heading/boost.png new file mode 100644 index 0000000000..e33b759976 Binary files /dev/null and b/app/javascript/images/mailer-new/heading/boost.png differ diff --git a/app/javascript/images/mailer-new/heading/email.png b/app/javascript/images/mailer-new/heading/email.png new file mode 100644 index 0000000000..c922c5239e Binary files /dev/null and b/app/javascript/images/mailer-new/heading/email.png differ diff --git a/app/javascript/images/mailer-new/heading/favorite.png b/app/javascript/images/mailer-new/heading/favorite.png new file mode 100644 index 0000000000..0e483ee9b2 Binary files /dev/null and b/app/javascript/images/mailer-new/heading/favorite.png differ diff --git a/app/javascript/images/mailer-new/heading/follow.png b/app/javascript/images/mailer-new/heading/follow.png new file mode 100644 index 0000000000..ff5b7e0042 Binary files /dev/null and b/app/javascript/images/mailer-new/heading/follow.png differ diff --git a/app/javascript/images/mailer-new/heading/key-added.png b/app/javascript/images/mailer-new/heading/key-added.png new file mode 100755 index 0000000000..82dcd464bf Binary files /dev/null and b/app/javascript/images/mailer-new/heading/key-added.png differ diff --git a/app/javascript/images/mailer-new/heading/key-deleted.png b/app/javascript/images/mailer-new/heading/key-deleted.png new file mode 100755 index 0000000000..2930f591a0 Binary files /dev/null and b/app/javascript/images/mailer-new/heading/key-deleted.png differ diff --git a/app/javascript/images/mailer-new/heading/key-disabled.png b/app/javascript/images/mailer-new/heading/key-disabled.png new file mode 100755 index 0000000000..e0f259359a Binary files /dev/null and b/app/javascript/images/mailer-new/heading/key-disabled.png differ diff --git a/app/javascript/images/mailer-new/heading/key-enabled.png b/app/javascript/images/mailer-new/heading/key-enabled.png new file mode 100644 index 0000000000..b2476ec346 Binary files /dev/null and b/app/javascript/images/mailer-new/heading/key-enabled.png differ diff --git a/app/javascript/images/mailer-new/heading/login.png b/app/javascript/images/mailer-new/heading/login.png new file mode 100644 index 0000000000..89a6e9ee33 Binary files /dev/null and b/app/javascript/images/mailer-new/heading/login.png differ diff --git a/app/javascript/images/mailer-new/heading/mention.png b/app/javascript/images/mailer-new/heading/mention.png new file mode 100644 index 0000000000..c4dccff8ef Binary files /dev/null and b/app/javascript/images/mailer-new/heading/mention.png differ diff --git a/app/javascript/images/mailer-new/heading/password.png b/app/javascript/images/mailer-new/heading/password.png new file mode 100755 index 0000000000..552c7c0687 Binary files /dev/null and b/app/javascript/images/mailer-new/heading/password.png differ diff --git a/app/javascript/images/mailer-new/heading/user.png b/app/javascript/images/mailer-new/heading/user.png new file mode 100644 index 0000000000..f1dd58a18d Binary files /dev/null and b/app/javascript/images/mailer-new/heading/user.png differ diff --git a/app/javascript/images/mailer-new/heading/warning.png b/app/javascript/images/mailer-new/heading/warning.png new file mode 100755 index 0000000000..7764837abe Binary files /dev/null and b/app/javascript/images/mailer-new/heading/warning.png differ diff --git a/app/javascript/images/mailer-new/welcome/checkbox-off.png b/app/javascript/images/mailer-new/welcome/checkbox-off.png new file mode 100644 index 0000000000..51c190efe6 Binary files /dev/null and b/app/javascript/images/mailer-new/welcome/checkbox-off.png differ diff --git a/app/javascript/images/mailer-new/welcome/checkbox-on.png b/app/javascript/images/mailer-new/welcome/checkbox-on.png new file mode 100644 index 0000000000..162095e7df Binary files /dev/null and b/app/javascript/images/mailer-new/welcome/checkbox-on.png differ diff --git a/app/javascript/images/mailer-new/welcome/step1-on.png b/app/javascript/images/mailer-new/welcome/step1-on.png new file mode 100644 index 0000000000..c3776d17df Binary files /dev/null and b/app/javascript/images/mailer-new/welcome/step1-on.png differ diff --git a/app/javascript/images/mailer-new/welcome/step2-off.png b/app/javascript/images/mailer-new/welcome/step2-off.png new file mode 100755 index 0000000000..a262454d2d Binary files /dev/null and b/app/javascript/images/mailer-new/welcome/step2-off.png differ diff --git a/app/javascript/images/mailer-new/welcome/step3-off.png b/app/javascript/images/mailer-new/welcome/step3-off.png new file mode 100755 index 0000000000..972de65a56 Binary files /dev/null and b/app/javascript/images/mailer-new/welcome/step3-off.png differ diff --git a/app/javascript/images/mailer-new/welcome/step4-off.png b/app/javascript/images/mailer-new/welcome/step4-off.png new file mode 100755 index 0000000000..f45e9a2c9a Binary files /dev/null and b/app/javascript/images/mailer-new/welcome/step4-off.png differ diff --git a/app/javascript/images/mailer-new/welcome/step5-off.png b/app/javascript/images/mailer-new/welcome/step5-off.png new file mode 100755 index 0000000000..ca270f5478 Binary files /dev/null and b/app/javascript/images/mailer-new/welcome/step5-off.png differ diff --git a/app/javascript/images/warning-stripes.svg b/app/javascript/images/warning-stripes.svg new file mode 100755 index 0000000000..9d68acdada --- /dev/null +++ b/app/javascript/images/warning-stripes.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/javascript/mastodon/actions/accounts_typed.ts b/app/javascript/mastodon/actions/accounts_typed.ts index b908e7528e..058a68a099 100644 --- a/app/javascript/mastodon/actions/accounts_typed.ts +++ b/app/javascript/mastodon/actions/accounts_typed.ts @@ -21,7 +21,7 @@ function actionWithSkipLoadingTrue(args: Args) { } export const followAccountSuccess = createAction( - 'accounts/followAccountSuccess', + 'accounts/followAccount/SUCCESS', actionWithSkipLoadingTrue<{ relationship: ApiRelationshipJSON; alreadyFollowing: boolean; @@ -29,7 +29,7 @@ export const followAccountSuccess = createAction( ); export const unfollowAccountSuccess = createAction( - 'accounts/unfollowAccountSuccess', + 'accounts/unfollowAccount/SUCCESS', actionWithSkipLoadingTrue<{ relationship: ApiRelationshipJSON; statuses: unknown; @@ -38,60 +38,60 @@ export const unfollowAccountSuccess = createAction( ); export const authorizeFollowRequestSuccess = createAction<{ id: string }>( - 'accounts/followRequestAuthorizeSuccess', + 'accounts/followRequestAuthorize/SUCCESS', ); export const rejectFollowRequestSuccess = createAction<{ id: string }>( - 'accounts/followRequestRejectSuccess', + 'accounts/followRequestReject/SUCCESS', ); export const followAccountRequest = createAction( - 'accounts/followRequest', + 'accounts/follow/REQUEST', actionWithSkipLoadingTrue<{ id: string; locked: boolean }>, ); export const followAccountFail = createAction( - 'accounts/followFail', + 'accounts/follow/FAIL', actionWithSkipLoadingTrue<{ id: string; error: string; locked: boolean }>, ); export const unfollowAccountRequest = createAction( - 'accounts/unfollowRequest', + 'accounts/unfollow/REQUEST', actionWithSkipLoadingTrue<{ id: string }>, ); export const unfollowAccountFail = createAction( - 'accounts/unfollowFail', + 'accounts/unfollow/FAIL', actionWithSkipLoadingTrue<{ id: string; error: string }>, ); export const blockAccountSuccess = createAction<{ relationship: ApiRelationshipJSON; statuses: unknown; -}>('accounts/blockSuccess'); +}>('accounts/block/SUCCESS'); export const unblockAccountSuccess = createAction<{ relationship: ApiRelationshipJSON; -}>('accounts/unblockSuccess'); +}>('accounts/unblock/SUCCESS'); export const muteAccountSuccess = createAction<{ relationship: ApiRelationshipJSON; statuses: unknown; -}>('accounts/muteSuccess'); +}>('accounts/mute/SUCCESS'); export const unmuteAccountSuccess = createAction<{ relationship: ApiRelationshipJSON; -}>('accounts/unmuteSuccess'); +}>('accounts/unmute/SUCCESS'); export const pinAccountSuccess = createAction<{ relationship: ApiRelationshipJSON; -}>('accounts/pinSuccess'); +}>('accounts/pin/SUCCESS'); export const unpinAccountSuccess = createAction<{ relationship: ApiRelationshipJSON; -}>('accounts/unpinSuccess'); +}>('accounts/unpin/SUCCESS'); export const fetchRelationshipsSuccess = createAction( - 'relationships/fetchSuccess', + 'relationships/fetch/SUCCESS', actionWithSkipLoadingTrue<{ relationships: ApiRelationshipJSON[] }>, ); diff --git a/app/javascript/mastodon/actions/domain_blocks_typed.ts b/app/javascript/mastodon/actions/domain_blocks_typed.ts index 08e0b4a178..6a4cace0de 100644 --- a/app/javascript/mastodon/actions/domain_blocks_typed.ts +++ b/app/javascript/mastodon/actions/domain_blocks_typed.ts @@ -5,9 +5,9 @@ import type { Account } from 'mastodon/models/account'; export const blockDomainSuccess = createAction<{ domain: string; accounts: Account[]; -}>('domain_blocks/blockSuccess'); +}>('domain_blocks/block/SUCCESS'); export const unblockDomainSuccess = createAction<{ domain: string; accounts: Account[]; -}>('domain_blocks/unblockSuccess'); +}>('domain_blocks/unblock/SUCCESS'); diff --git a/app/javascript/mastodon/actions/notifications_typed.ts b/app/javascript/mastodon/actions/notifications_typed.ts index 7e51fa51e7..176362f4b1 100644 --- a/app/javascript/mastodon/actions/notifications_typed.ts +++ b/app/javascript/mastodon/actions/notifications_typed.ts @@ -18,6 +18,6 @@ export const notificationsUpdate = createAction( playSound: boolean; }) => ({ payload: args, - meta: { playSound: playSound ? { sound: 'boop' } : undefined }, + meta: { sound: playSound ? 'boop' : undefined }, }), ); diff --git a/app/javascript/mastodon/actions/search.js b/app/javascript/mastodon/actions/search.js index 38a089b486..a34a490e76 100644 --- a/app/javascript/mastodon/actions/search.js +++ b/app/javascript/mastodon/actions/search.js @@ -179,6 +179,11 @@ export const openURL = (value, history, onFailure) => (dispatch, getState) => { 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); @@ -207,4 +212,4 @@ export const hydrateSearch = () => (dispatch, getState) => { if (history !== null) { dispatch(updateSearchHistory(history)); } -}; \ No newline at end of file +}; diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/autosuggest_emoji-test.jsx.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/autosuggest_emoji-test.jsx.snap index 1c37278483..dc955b7abe 100644 --- a/app/javascript/mastodon/components/__tests__/__snapshots__/autosuggest_emoji-test.jsx.snap +++ b/app/javascript/mastodon/components/__tests__/__snapshots__/autosuggest_emoji-test.jsx.snap @@ -9,7 +9,11 @@ exports[` renders emoji with custom url 1`] = ` className="emojione" src="http://example.com/emoji.png" /> - :foobar: +
+ :foobar: +
`; @@ -22,6 +26,10 @@ exports[` renders native emoji 1`] = ` className="emojione" src="/emoji/1f499.svg" /> - :foobar: +
+ :foobar: +
`; diff --git a/app/javascript/mastodon/components/account.jsx b/app/javascript/mastodon/components/account.jsx index f82dd9153a..4a99dd0bbf 100644 --- a/app/javascript/mastodon/components/account.jsx +++ b/app/javascript/mastodon/components/account.jsx @@ -37,10 +37,10 @@ class Account extends ImmutablePureComponent { static propTypes = { size: PropTypes.number, account: ImmutablePropTypes.record, - onFollow: PropTypes.func.isRequired, - onBlock: PropTypes.func.isRequired, - onMute: PropTypes.func.isRequired, - onMuteNotifications: PropTypes.func.isRequired, + onFollow: PropTypes.func, + onBlock: PropTypes.func, + onMute: PropTypes.func, + onMuteNotifications: PropTypes.func, intl: PropTypes.object.isRequired, hidden: PropTypes.bool, minimal: PropTypes.bool, diff --git a/app/javascript/mastodon/components/attachment_list.jsx b/app/javascript/mastodon/components/attachment_list.jsx index ebf092b83d..c5ac046751 100644 --- a/app/javascript/mastodon/components/attachment_list.jsx +++ b/app/javascript/mastodon/components/attachment_list.jsx @@ -7,8 +7,7 @@ import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { ReactComponent as LinkIcon } from '@material-symbols/svg-600/outlined/link.svg'; - +import LinkIcon from '@/material-icons/400-24px/link.svg?react'; import { Icon } from 'mastodon/components/icon'; const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; diff --git a/app/javascript/mastodon/components/autosuggest_emoji.jsx b/app/javascript/mastodon/components/autosuggest_emoji.jsx index 28f628b4ac..adea1d9f4b 100644 --- a/app/javascript/mastodon/components/autosuggest_emoji.jsx +++ b/app/javascript/mastodon/components/autosuggest_emoji.jsx @@ -35,7 +35,7 @@ export default class AutosuggestEmoji extends PureComponent { alt={emoji.native || emoji.colons} /> - {emoji.colons} +
{emoji.colons}
); } diff --git a/app/javascript/mastodon/components/autosuggest_hashtag.tsx b/app/javascript/mastodon/components/autosuggest_hashtag.tsx index e83d493c2c..036946b0f1 100644 --- a/app/javascript/mastodon/components/autosuggest_hashtag.tsx +++ b/app/javascript/mastodon/components/autosuggest_hashtag.tsx @@ -1,5 +1,3 @@ -import { FormattedMessage } from 'react-intl'; - import { ShortNumber } from 'mastodon/components/short_number'; interface Props { @@ -16,27 +14,18 @@ interface Props { }; } -export const AutosuggestHashtag: React.FC = ({ tag }) => { - const weeklyUses = tag.history && ( - total + day.uses * 1, 0)} - /> - ); - - return ( -
-
- #{tag.name} -
- {tag.history !== undefined && ( -
- -
- )} +export const AutosuggestHashtag: React.FC = ({ tag }) => ( +
+
+ #{tag.name}
- ); -}; + + {tag.history !== undefined && ( +
+ total + day.uses * 1, 0)} + /> +
+ )} +
+); diff --git a/app/javascript/mastodon/components/autosuggest_input.jsx b/app/javascript/mastodon/components/autosuggest_input.jsx index 06cbb5d75b..f707a18e1d 100644 --- a/app/javascript/mastodon/components/autosuggest_input.jsx +++ b/app/javascript/mastodon/components/autosuggest_input.jsx @@ -5,6 +5,8 @@ import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import Overlay from 'react-overlays/Overlay'; + import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; import AutosuggestEmoji from './autosuggest_emoji'; @@ -195,34 +197,37 @@ export default class AutosuggestInput extends ImmutablePureComponent { return (
- - -
- {suggestions.map(this.renderSuggestion)} -
+ + {({ props }) => ( +
+
+ {suggestions.map(this.renderSuggestion)} +
+
+ )} +
); } diff --git a/app/javascript/mastodon/components/autosuggest_textarea.jsx b/app/javascript/mastodon/components/autosuggest_textarea.jsx index 4d173af59d..07fbccacc9 100644 --- a/app/javascript/mastodon/components/autosuggest_textarea.jsx +++ b/app/javascript/mastodon/components/autosuggest_textarea.jsx @@ -5,6 +5,7 @@ import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import Overlay from 'react-overlays/Overlay'; import Textarea from 'react-textarea-autosize'; import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; @@ -52,7 +53,6 @@ const AutosuggestTextarea = forwardRef(({ onFocus, autoFocus = true, lang, - children, }, textareaRef) => { const [suggestionsHidden, setSuggestionsHidden] = useState(true); @@ -183,40 +183,38 @@ const AutosuggestTextarea = forwardRef(({ ); }; - return [ -
-
-