diff --git a/.browserslistrc b/.browserslistrc index 0376af4bcc..54dd3aaf34 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -1,9 +1,7 @@ [production] defaults -> 0.2% -ios >= 15.6 +not IE 11 not dead -not OperaMini all [development] supports es6-module diff --git a/.bundler-audit.yml b/.bundler-audit.yml deleted file mode 100644 index 9c4d4511f1..0000000000 --- a/.bundler-audit.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -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 - - CVE-2024-27456 - - CVE-2023-51774 diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 97331f74ea..21ee078d60 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -70,7 +70,7 @@ services: hard: -1 libretranslate: - image: libretranslate/libretranslate:v1.5.7 + image: libretranslate/libretranslate:v1.5.3 restart: unless-stopped volumes: - lt-data:/home/libretranslate/.local diff --git a/.env.development b/.env.development deleted file mode 100644 index 0330da8377..0000000000 --- a/.env.development +++ /dev/null @@ -1,4 +0,0 @@ -# Required by ActiveRecord encryption feature -ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=fkSxKD2bF396kdQbrP1EJ7WbU7ZgNokR -ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=r0hvVmzBVsjxC7AMlwhOzmtc36ZCOS1E -ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=PhdFyyfy5xJ7WVd2lWBpcPScRQHzRTNr diff --git a/.env.test b/.env.test index 539bdeb795..2f8c1afd6e 100644 --- a/.env.test +++ b/.env.test @@ -3,10 +3,3 @@ NODE_ENV=production # Federation LOCAL_DOMAIN=cb6e6126.ngrok.io LOCAL_HTTPS=true -# Elasticsearch -ES_PREFIX=test - -# Required by ActiveRecord encryption feature -ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=fkSxKD2bF396kdQbrP1EJ7WbU7ZgNokR -ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=r0hvVmzBVsjxC7AMlwhOzmtc36ZCOS1E -ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=PhdFyyfy5xJ7WVd2lWBpcPScRQHzRTNr diff --git a/.eslintrc.js b/.eslintrc.js index 759003b55e..1b36bcee25 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -123,7 +123,7 @@ module.exports = defineConfig({ 'react/react-in-jsx-scope': 'off', // not needed with new JSX transform 'react/self-closing-comp': 'error', - // recommended values found in https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.8.0/src/index.js#L46 + // recommended values found in https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/src/index.js 'jsx-a11y/accessible-emoji': 'warn', 'jsx-a11y/click-events-have-key-events': 'off', 'jsx-a11y/label-has-associated-control': 'off', @@ -165,7 +165,7 @@ module.exports = defineConfig({ // }, // ], 'jsx-a11y/no-noninteractive-tabindex': 'off', - 'jsx-a11y/no-onchange': 'off', + 'jsx-a11y/no-onchange': 'warn', // recommended is full 'error' 'jsx-a11y/no-static-element-interactions': [ 'warn', @@ -176,7 +176,7 @@ module.exports = defineConfig({ }, ], - // See https://github.com/import-js/eslint-plugin-import/blob/v2.29.1/config/recommended.js + // See https://github.com/import-js/eslint-plugin-import/blob/main/config/recommended.js 'import/extensions': [ 'error', 'always', @@ -338,6 +338,7 @@ module.exports = defineConfig({ 'plugin:import/typescript', 'plugin:promise/recommended', 'plugin:jsdoc/recommended-typescript', + 'plugin:prettier/recommended', ], parserOptions: { @@ -346,9 +347,6 @@ module.exports = defineConfig({ }, rules: { - // Disable formatting rules that have been enabled in the base config - 'indent': 'off', - 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'], @@ -363,7 +361,6 @@ module.exports = defineConfig({ "message": "Use typed hooks `useAppDispatch` and `useAppSelector` instead." } ], - "@typescript-eslint/restrict-template-expressions": ['warn', { allowNumber: true }], 'jsdoc/require-jsdoc': 'off', // Those rules set stricter rules for TS files diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index fa7a0c5353..0000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -custom: https://fantia.jp/fanclubs/484677 diff --git a/.github/ISSUE_TEMPLATE/1.bug_report.yml b/.github/ISSUE_TEMPLATE/1.bug_report.yml deleted file mode 100644 index 10421eed7b..0000000000 --- a/.github/ISSUE_TEMPLATE/1.bug_report.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: バグ報告 -description: kmyblueのバグ報告(ただし情報改竄、秘密情報の漏洩、システムの破損などが発生するバグは、こちらではなく「Security」タブよりセキュリティインシデントとして報告してください) -labels: [bug] -body: - - type: textarea - attributes: - label: バグの再現手順 - description: どのように操作したらバグが発生したのか、バグが発生する直前までの手順を順番に詳しく教えてください - value: | - 1. - 2. - 3. - ... - validations: - required: true - - type: textarea - attributes: - label: 期待する動作 - description: どのように動いてほしかったですか? - validations: - required: true - - type: textarea - attributes: - label: 実際の動作 - description: どのようなバグが発生しましたか? - validations: - required: true - - type: textarea - attributes: - label: 詳しい情報 - validations: - required: false - - type: input - attributes: - label: バグが発生したkmyblueサーバーのドメイン - description: サーバー固有の問題の可能性もありますので、プライバシー上可能な範囲内で、できるだけ書いてください - placeholder: kmy.blue - validations: - required: false - - type: input - attributes: - label: バグが発生したkmyblueのバージョン - description: | - Mastodonではなくkmyblueのバージョンを記述してください。例えばバージョン表記が `v4.2.0+kmyblue.5.1-LTS` の場合、バージョンは `5.1`になります - - バージョンは、PCだと画面左下、スマホだと概要画面の一番下に書いてあります - placeholder: '5.1' - validations: - required: true - - type: input - attributes: - label: ブラウザの名前 - description: | - ブラウザの名前を書いてください。可能であればバージョンも併記してください - placeholder: Firefox 105.0.3 - validations: - required: false - - type: input - attributes: - label: OS - description: | - あなたのOSと、できればバージョンも教えてください。スマホの場合は、「Android」「iPhone」にバージョンをつけてください - placeholder: Windows11 - validations: - required: false - - type: textarea - attributes: - label: その他の詳細情報 - description: | - あなたの環境が特殊な場合、詳しいことを教えてください(例: VPS、tor、学内LANなど) - - サーバー管理者の場合は、Ruby、Node.jsのバージョン、Cloudflareの使用可否なども可能なら書いてください - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/1.web_bug_report.yml b/.github/ISSUE_TEMPLATE/1.web_bug_report.yml new file mode 100644 index 0000000000..20e27d103c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1.web_bug_report.yml @@ -0,0 +1,76 @@ +name: Bug Report (Web Interface) +description: If you are using Mastodon's web interface and something is not working as expected +labels: [bug, 'status/to triage', 'area/web interface'] +body: + - type: markdown + attributes: + value: | + Make sure that you are submitting a new bug that was not previously reported or already fixed. + + Please use a concise and distinct title for the issue. + - type: textarea + attributes: + label: Steps to reproduce the problem + description: What were you trying to do? + value: | + 1. + 2. + 3. + ... + validations: + required: true + - type: input + attributes: + label: Expected behaviour + description: What should have happened? + validations: + required: true + - type: input + attributes: + label: Actual behaviour + description: What happened? + validations: + required: true + - type: textarea + attributes: + label: Detailed description + validations: + required: false + - type: input + attributes: + label: Mastodon instance + description: The address of the Mastodon instance where you experienced the issue + placeholder: mastodon.social + validations: + required: true + - type: input + attributes: + label: Mastodon version + description: | + This is displayed at the bottom of the About page, eg. `v4.1.2+nightly-20230627` + placeholder: v4.1.2 + validations: + required: true + - type: input + attributes: + label: Browser name and version + description: | + What browser are you using when getting this bug? Please specify the version as well. + placeholder: Firefox 105.0.3 + validations: + required: true + - type: input + attributes: + label: Operating system + description: | + What OS are you running? Please specify the version as well. + placeholder: macOS 13.4.1 + validations: + required: true + - type: textarea + attributes: + label: Technical details + description: | + Any additional technical details you may have. This can include the full error log, inspector's output… + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/2.feature_request.yml b/.github/ISSUE_TEMPLATE/2.feature_request.yml deleted file mode 100644 index 10fb4bb23b..0000000000 --- a/.github/ISSUE_TEMPLATE/2.feature_request.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: 機能要望 -description: 機能の提案 -labels: [enhancement] -body: - - type: textarea - attributes: - label: 欲しい機能 - description: 欲しい機能の詳細を書いてください - validations: - required: true - - type: textarea - attributes: - label: 必要性 - description: この機能はあなたにとってなぜ必要でしょうか?どういった状況で使われるものですか? - validations: - required: true diff --git a/.github/ISSUE_TEMPLATE/2.server_bug_report.yml b/.github/ISSUE_TEMPLATE/2.server_bug_report.yml new file mode 100644 index 0000000000..49d5f57209 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2.server_bug_report.yml @@ -0,0 +1,65 @@ +name: Bug Report (server / API) +description: | + If something is not working as expected, but is not from using the web interface. +labels: [bug, 'status/to triage'] +body: + - type: markdown + attributes: + value: | + Make sure that you are submitting a new bug that was not previously reported or already fixed. + + Please use a concise and distinct title for the issue. + - type: textarea + attributes: + label: Steps to reproduce the problem + description: What were you trying to do? + value: | + 1. + 2. + 3. + ... + validations: + required: true + - type: input + attributes: + label: Expected behaviour + description: What should have happened? + validations: + required: true + - type: input + attributes: + label: Actual behaviour + description: What happened? + validations: + required: true + - type: textarea + attributes: + label: Detailed description + validations: + required: false + - type: input + attributes: + label: Mastodon instance + description: The address of the Mastodon instance where you experienced the issue + placeholder: mastodon.social + validations: + required: false + - type: input + attributes: + label: Mastodon version + description: | + This is displayed at the bottom of the About page, eg. `v4.1.2+nightly-20230627` + placeholder: v4.1.2 + validations: + required: false + - type: textarea + attributes: + label: Technical details + description: | + Any additional technical details you may have, like logs or error traces + value: | + If this is happening on your own Mastodon server, please fill out those: + - Ruby version: (from `ruby --version`, eg. v3.1.2) + - Node.js version: (from `node --version`, eg. v18.16.0) + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/3.feature_request.yml b/.github/ISSUE_TEMPLATE/3.feature_request.yml new file mode 100644 index 0000000000..2cabcf61e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3.feature_request.yml @@ -0,0 +1,22 @@ +name: Feature Request +description: I have a suggestion +labels: [suggestion] +body: + - type: markdown + attributes: + value: | + Please use a concise and distinct title for the issue. + + Consider: Could it be implemented as a 3rd party app using the REST API instead? + - type: textarea + attributes: + label: Pitch + description: Describe your idea for a feature. Make sure it has not already been suggested/implemented/turned down before. + validations: + required: true + - type: textarea + attributes: + label: Motivation + description: Why do you think this feature is needed? Who would benefit from it? + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/3.spec_change_request.yml b/.github/ISSUE_TEMPLATE/3.spec_change_request.yml deleted file mode 100644 index e71befe859..0000000000 --- a/.github/ISSUE_TEMPLATE/3.spec_change_request.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: 仕様変更・改善要望 -description: 既存の仕様や挙動変更の要望 -labels: [specchange] -body: - - type: markdown - attributes: - value: 意図したものとは明らかに異なる挙動をしているものはバグとして、もともと仕様として決められた動きをしているものを変更したいときはこちらでお願いします - - type: textarea - attributes: - label: 挙動を変更してほしい機能や動作 - validations: - required: true - - type: textarea - attributes: - label: 現在の挙動 - validations: - required: true - - type: textarea - attributes: - label: 変更してほしい新しい挙動 - validations: - required: true - - type: textarea - attributes: - label: 必要性 - description: この変更はあなたにとってなぜ必要でしょうか?どういった状況で使われるものですか? - validations: - required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 0086358db1..f5d3196528 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1 +1,5 @@ -blank_issues_enabled: true +blank_issues_enabled: false +contact_links: + - name: GitHub Discussions + url: https://github.com/mastodon/mastodon/discussions + about: Please ask and answer questions here. diff --git a/.github/actions/setup-javascript/action.yml b/.github/actions/setup-javascript/action.yml index 808adc7de6..07fd4d08d3 100644 --- a/.github/actions/setup-javascript/action.yml +++ b/.github/actions/setup-javascript/action.yml @@ -23,7 +23,7 @@ runs: shell: bash run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT - - uses: actions/cache@v4 + - uses: actions/cache@v3 id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} diff --git a/.github/codecov.yml b/.github/codecov.yml index 9d6413a106..5532c49618 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -1,4 +1,3 @@ -comment: false # Do not leave PR comments coverage: status: project: @@ -9,3 +8,6 @@ coverage: default: # Github status check is not blocking informational: true +comment: + # Only write a comment in PR if there are changes + require_changes: true diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 378d4fc83c..dab99829a1 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -125,29 +125,6 @@ ], groupName: null, // We dont want them to belong to any group }, - { - // Group all RuboCop packages with `rubocop` in the same PR - matchManagers: ['bundler'], - matchPackageNames: ['rubocop'], - matchPackagePrefixes: ['rubocop-'], - matchUpdateTypes: ['patch', 'minor'], - groupName: 'RuboCop (non-major)', - }, - { - // Group all RSpec packages with `rspec` in the same PR - matchManagers: ['bundler'], - matchPackageNames: ['rspec'], - matchPackagePrefixes: ['rspec-'], - matchUpdateTypes: ['patch', 'minor'], - groupName: 'RSpec (non-major)', - }, - { - // Group all opentelemetry-ruby packages in the same PR - matchManagers: ['bundler'], - matchPackagePrefixes: ['opentelemetry-'], - matchUpdateTypes: ['patch', 'minor'], - groupName: 'opentelemetry-ruby (non-major)', - }, // Add labels depending on package manager { matchManagers: ['npm', 'nvm'], addLabels: ['javascript'] }, { matchManagers: ['bundler', 'ruby-version'], addLabels: ['ruby'] }, diff --git a/.github/stylelint-matcher.json b/.github/stylelint-matcher.json new file mode 100644 index 0000000000..cdfd4086bd --- /dev/null +++ b/.github/stylelint-matcher.json @@ -0,0 +1,21 @@ +{ + "problemMatcher": [ + { + "owner": "stylelint", + "pattern": [ + { + "regexp": "^([^\\s].*)$", + "file": 1 + }, + { + "regexp": "^\\s+((\\d+):(\\d+))?\\s+(✖|×)\\s+(.*)\\s{2,}(.*)$", + "line": 2, + "column": 3, + "message": 5, + "code": 6, + "loop": true + } + ] + } + ] +} diff --git a/.github/workflows/build-container-image.yml b/.github/workflows/build-container-image.yml new file mode 100644 index 0000000000..e100e15821 --- /dev/null +++ b/.github/workflows/build-container-image.yml @@ -0,0 +1,102 @@ +on: + workflow_call: + inputs: + platforms: + required: true + type: string + cache: + type: boolean + default: true + use_native_arm64_builder: + type: boolean + push_to_images: + type: string + version_prerelease: + type: string + version_metadata: + type: string + flavor: + type: string + tags: + type: string + labels: + type: string + file_to_build: + type: string + +jobs: + build-image: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-qemu-action@v3 + if: contains(inputs.platforms, 'linux/arm64') && !inputs.use_native_arm64_builder + + - uses: docker/setup-buildx-action@v3 + id: buildx + if: ${{ !(inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')) }} + + - name: Start a local Docker Builder + if: inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64') + run: | + docker run --rm -d --name buildkitd -p 1234:1234 --privileged moby/buildkit:latest --addr tcp://0.0.0.0:1234 + + - uses: docker/setup-buildx-action@v3 + id: buildx-native + if: inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64') + with: + driver: remote + endpoint: tcp://localhost:1234 + platforms: linux/amd64 + append: | + - endpoint: tcp://${{ vars.DOCKER_BUILDER_HETZNER_ARM64_01_HOST }}:13865 + platforms: linux/arm64 + name: mastodon-docker-builder-arm64-01 + driver-opts: + - servername=mastodon-docker-builder-arm64-01 + env: + BUILDER_NODE_1_AUTH_TLS_CACERT: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_CACERT }} + BUILDER_NODE_1_AUTH_TLS_CERT: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_CERT }} + BUILDER_NODE_1_AUTH_TLS_KEY: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_KEY }} + + - name: Log in to Docker Hub + if: contains(inputs.push_to_images, 'tootsuite') + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Log in to the Github Container registry + if: contains(inputs.push_to_images, 'ghcr.io') + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/metadata-action@v5 + id: meta + if: ${{ inputs.push_to_images != '' }} + with: + images: ${{ inputs.push_to_images }} + flavor: ${{ inputs.flavor }} + tags: ${{ inputs.tags }} + labels: ${{ inputs.labels }} + + - uses: docker/build-push-action@v5 + with: + context: . + file: ${{ inputs.file_to_build }} + build-args: | + MASTODON_VERSION_PRERELEASE=${{ inputs.version_prerelease }} + MASTODON_VERSION_METADATA=${{ inputs.version_metadata }} + platforms: ${{ inputs.platforms }} + provenance: false + builder: ${{ steps.buildx.outputs.name || steps.buildx-native.outputs.name }} + push: ${{ inputs.push_to_images != '' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: ${{ inputs.cache && 'type=gha' || '' }} + cache-to: ${{ inputs.cache && 'type=gha,mode=max' || '' }} diff --git a/.github/workflows/build-security.yml b/.github/workflows/build-nightly.yml similarity index 83% rename from .github/workflows/build-security.yml rename to .github/workflows/build-nightly.yml index 1e2455d3d9..7c6f74b457 100644 --- a/.github/workflows/build-security.yml +++ b/.github/workflows/build-nightly.yml @@ -1,6 +1,8 @@ -name: Build security nightly container image +name: Build nightly container image on: workflow_dispatch: + schedule: + - cron: '0 2 * * *' # run at 2 AM UTC permissions: contents: read @@ -15,7 +17,7 @@ jobs: env: TZ: Etc/UTC run: | - echo mastodon_version_prerelease=nightly.$(date --date='next day' +'%Y-%m-%d')-security>> $GITHUB_OUTPUT + echo mastodon_version_prerelease=nightly.$(date +'%Y-%m-%d')>> $GITHUB_OUTPUT outputs: prerelease: ${{ steps.version_vars.outputs.mastodon_version_prerelease }} @@ -38,7 +40,7 @@ jobs: tags: | type=raw,value=edge type=raw,value=nightly - type=raw,value=${{ needs.compute-suffix.outputs.prerelease }} + type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }} secrets: inherit build-image-streaming: @@ -60,5 +62,5 @@ jobs: tags: | type=raw,value=edge type=raw,value=nightly - type=raw,value=${{ needs.compute-suffix.outputs.prerelease }} + type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }} secrets: inherit diff --git a/.github/workflows/build-push-pr.yml b/.github/workflows/build-push-pr.yml new file mode 100644 index 0000000000..72baed5121 --- /dev/null +++ b/.github/workflows/build-push-pr.yml @@ -0,0 +1,58 @@ +name: Build container image for PR +on: + pull_request: + types: [labeled, synchronize, reopened, ready_for_review, opened] + +permissions: + contents: read + packages: write + +jobs: + compute-suffix: + runs-on: ubuntu-latest + # This is only allowed to run if: + # - the PR branch is in the `mastodon/mastodon` repository + # - the PR is not a draft + # - the PR has the "build-image" label + if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !github.event.pull_request.draft && contains(github.event.pull_request.labels.*.name, 'build-image') }} + steps: + # Repository needs to be cloned so `git rev-parse` below works + - name: Clone repository + uses: actions/checkout@v4 + - id: version_vars + run: | + echo mastodon_version_metadata=pr-${{ github.event.pull_request.number }}-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT + outputs: + metadata: ${{ steps.version_vars.outputs.mastodon_version_metadata }} + + build-image: + needs: compute-suffix + uses: ./.github/workflows/build-container-image.yml + with: + file_to_build: Dockerfile + platforms: linux/amd64,linux/arm64 + use_native_arm64_builder: true + push_to_images: | + ghcr.io/mastodon/mastodon + version_metadata: ${{ needs.compute-suffix.outputs.metadata }} + flavor: | + latest=auto + tags: | + type=ref,event=pr + secrets: inherit + + build-image-streaming: + needs: compute-suffix + uses: ./.github/workflows/build-container-image.yml + with: + file_to_build: streaming/Dockerfile + platforms: linux/amd64,linux/arm64 + use_native_arm64_builder: true + push_to_images: | + ghcr.io/mastodon/mastodon-streaming + version_metadata: ${{ needs.compute-suffix.outputs.metadata }} + flavor: | + latest=auto + tags: | + type=ref,event=pr + secrets: inherit diff --git a/.github/workflows/build-releases.yml b/.github/workflows/build-releases.yml new file mode 100644 index 0000000000..3f0bef32ac --- /dev/null +++ b/.github/workflows/build-releases.yml @@ -0,0 +1,51 @@ +name: Build container release images +on: + push: + tags: + - '*' + +permissions: + contents: read + packages: write + +jobs: + build-image: + uses: ./.github/workflows/build-container-image.yml + with: + file_to_build: Dockerfile + platforms: linux/amd64,linux/arm64 + use_native_arm64_builder: true + push_to_images: | + tootsuite/mastodon + ghcr.io/mastodon/mastodon + # Do not use cache when building releases, so apt update is always ran and the release always contain the latest packages + cache: false + # Only tag with latest when ran against the latest stable branch + # This needs to be updated after each minor version release + flavor: | + latest=${{ startsWith(github.ref, 'refs/tags/v4.2.') }} + tags: | + type=pep440,pattern={{raw}} + type=pep440,pattern=v{{major}}.{{minor}} + secrets: inherit + + build-image-streaming: + if: startsWith(github.ref, 'refs/tags/v4.3.') + uses: ./.github/workflows/build-container-image.yml + with: + file_to_build: streaming/Dockerfile + platforms: linux/amd64,linux/arm64 + use_native_arm64_builder: true + push_to_images: | + tootsuite/mastodon-streaming + ghcr.io/mastodon/mastodon-streaming + # Do not use cache when building releases, so apt update is always ran and the release always contain the latest packages + cache: false + # Only tag with latest when ran against the latest stable branch + # This needs to be updated after each minor version release + flavor: | + latest=${{ startsWith(github.ref, 'refs/tags/v4.3.') }} + tags: | + type=pep440,pattern={{raw}} + type=pep440,pattern=v{{major}}.{{minor}} + secrets: inherit diff --git a/.github/workflows/crowdin-download.yml b/.github/workflows/crowdin-download.yml new file mode 100644 index 0000000000..d3988d2f1a --- /dev/null +++ b/.github/workflows/crowdin-download.yml @@ -0,0 +1,71 @@ +name: Crowdin / Download translations +on: + schedule: + - cron: '17 4 * * *' # Every day + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + download-translations: + runs-on: ubuntu-latest + if: github.repository == 'mastodon/mastodon' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Increase Git http.postBuffer + # This is needed due to a bug in Ubuntu's cURL version? + # See https://github.com/orgs/community/discussions/55820 + run: | + git config --global http.version HTTP/1.1 + git config --global http.postBuffer 157286400 + + # Download the translation files from Crowdin + - name: crowdin action + uses: crowdin/github-action@v1 + with: + upload_sources: false + upload_translations: false + download_translations: true + crowdin_branch_name: main + push_translations: false + create_pull_request: false + env: + CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} + + # As the files are extracted from a Docker container, they belong to root:root + # We need to fix this before the next steps + - name: Fix file permissions + run: sudo chown -R runner:docker . + + # This is needed to run the normalize step + - name: Set up Ruby environment + uses: ./.github/actions/setup-ruby + + - name: Run i18n normalize task + run: bundle exec i18n-tasks normalize + + # Create or update the pull request + - name: Create Pull Request + uses: peter-evans/create-pull-request@v5.0.2 + with: + commit-message: 'New Crowdin translations' + title: 'New Crowdin Translations (automated)' + author: 'GitHub Actions ' + body: | + New Crowdin translations, automated with Github Actions + + See `.github/workflows/crowdin-download.yml` + + This PR will be updated every day with new translations. + + Due to a limitation in Github Actions, checks are not running on this PR without manual action. + If you want to run the checks, then close and re-open it. + branch: i18n/crowdin/translations + base: main + labels: i18n diff --git a/.github/workflows/crowdin-upload.yml b/.github/workflows/crowdin-upload.yml new file mode 100644 index 0000000000..705af12c02 --- /dev/null +++ b/.github/workflows/crowdin-upload.yml @@ -0,0 +1,35 @@ +name: Crowdin / Upload translations + +on: + push: + branches: + - main + paths: + - crowdin.yml + - app/javascript/mastodon/locales/en.json + - config/locales/en.yml + - config/locales/simple_form.en.yml + - config/locales/activerecord.en.yml + - config/locales/devise.en.yml + - config/locales/doorkeeper.en.yml + - .github/workflows/crowdin-upload.yml + +jobs: + upload-translations: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: crowdin action + uses: crowdin/github-action@v1 + with: + upload_sources: true + upload_translations: false + download_translations: false + crowdin_branch_name: main + + env: + CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml deleted file mode 100644 index 2d483b5022..0000000000 --- a/.github/workflows/format-check.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Check formatting -on: - push: - pull_request: - -jobs: - lint: - runs-on: ubuntu-latest - - steps: - - name: Clone repository - uses: actions/checkout@v4 - - - name: Set up Javascript environment - uses: ./.github/actions/setup-javascript - - - name: Check formatting with Prettier - run: yarn format:check diff --git a/.github/workflows/lint-css.yml b/.github/workflows/lint-css.yml index d3b8035cd8..7229bec582 100644 --- a/.github/workflows/lint-css.yml +++ b/.github/workflows/lint-css.yml @@ -38,5 +38,9 @@ jobs: - name: Set up Javascript environment uses: ./.github/actions/setup-javascript + - uses: xt0rted/stylelint-problem-matcher@v1 + + - run: echo "::add-matcher::.github/stylelint-matcher.json" + - name: Stylelint - run: yarn lint:css -f github + run: yarn lint:sass diff --git a/.github/workflows/lint-haml.yml b/.github/workflows/lint-haml.yml index 25615b720d..8dcab845ee 100644 --- a/.github/workflows/lint-haml.yml +++ b/.github/workflows/lint-haml.yml @@ -36,4 +36,4 @@ jobs: - name: Run haml-lint run: | echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json" - bundle exec haml-lint --reporter github + bundle exec haml-lint diff --git a/.github/workflows/lint-json.yml b/.github/workflows/lint-json.yml new file mode 100644 index 0000000000..7796bf92c4 --- /dev/null +++ b/.github/workflows/lint-json.yml @@ -0,0 +1,38 @@ +name: JSON Linting +on: + push: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + paths: + - 'package.json' + - 'yarn.lock' + - '.nvmrc' + - '.prettier*' + - '**/*.json' + - '.github/workflows/lint-json.yml' + - '!app/javascript/mastodon/locales/*.json' + + pull_request: + paths: + - 'package.json' + - 'yarn.lock' + - '.nvmrc' + - '.prettier*' + - '**/*.json' + - '.github/workflows/lint-json.yml' + - '!app/javascript/mastodon/locales/*.json' + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Clone repository + uses: actions/checkout@v4 + + - name: Set up Javascript environment + uses: ./.github/actions/setup-javascript + + - name: Prettier + run: yarn lint:json diff --git a/.github/workflows/lint-md.yml b/.github/workflows/lint-md.yml new file mode 100644 index 0000000000..51c59937a3 --- /dev/null +++ b/.github/workflows/lint-md.yml @@ -0,0 +1,38 @@ +name: Markdown Linting +on: + push: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + paths: + - '.github/workflows/lint-md.yml' + - '.nvmrc' + - '.prettier*' + - '**/*.md' + - '!AUTHORS.md' + - 'package.json' + - 'yarn.lock' + + pull_request: + paths: + - '.github/workflows/lint-md.yml' + - '.nvmrc' + - '.prettier*' + - '**/*.md' + - '!AUTHORS.md' + - 'package.json' + - 'yarn.lock' + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Clone repository + uses: actions/checkout@v4 + + - name: Set up Javascript environment + uses: ./.github/actions/setup-javascript + + - name: Prettier + run: yarn lint:md diff --git a/.github/workflows/lint-yml.yml b/.github/workflows/lint-yml.yml new file mode 100644 index 0000000000..908bdef5cc --- /dev/null +++ b/.github/workflows/lint-yml.yml @@ -0,0 +1,40 @@ +name: YML Linting +on: + push: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + paths: + - 'package.json' + - 'yarn.lock' + - '.nvmrc' + - '.prettier*' + - '**/*.yaml' + - '**/*.yml' + - '.github/workflows/lint-yml.yml' + - '!config/locales/*.yml' + + pull_request: + paths: + - 'package.json' + - 'yarn.lock' + - '.nvmrc' + - '.prettier*' + - '**/*.yaml' + - '**/*.yml' + - '.github/workflows/lint-yml.yml' + - '!config/locales/*.yml' + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Clone repository + uses: actions/checkout@v4 + + - name: Set up Javascript environment + uses: ./.github/actions/setup-javascript + + - name: Prettier + run: yarn lint:yml diff --git a/.github/workflows/test-image-build.yml b/.github/workflows/test-image-build.yml new file mode 100644 index 0000000000..980e071897 --- /dev/null +++ b/.github/workflows/test-image-build.yml @@ -0,0 +1,35 @@ +name: Test container image build +on: + pull_request: + paths: + - .github/workflows/build-nightly.yml + - .github/workflows/build-push-pr.yml + - .github/workflows/build-releases.yml + - .github/workflows/test-image-build.yml + - Dockerfile + - streaming/Dockerfile +permissions: + contents: read + +jobs: + build-image: + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + uses: ./.github/workflows/build-container-image.yml + with: + file_to_build: Dockerfile + platforms: linux/amd64 # Testing only on native platform so it is performant + cache: true + + build-image-streaming: + concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-streaming + cancel-in-progress: true + + uses: ./.github/workflows/build-container-image.yml + with: + file_to_build: streaming/Dockerfile + platforms: linux/amd64 # Testing only on native platform so it is performant + cache: true diff --git a/.github/workflows/test-js.yml b/.github/workflows/test-js.yml index 481afdba30..79622b6c1f 100644 --- a/.github/workflows/test-js.yml +++ b/.github/workflows/test-js.yml @@ -38,5 +38,5 @@ jobs: - name: Set up Javascript environment uses: ./.github/actions/setup-javascript - - name: JavaScript testing + - name: Jest testing run: yarn jest --reporters github-actions summary diff --git a/.github/workflows/test-migrations-one-step.yml b/.github/workflows/test-migrations-one-step.yml index 1ff5cc06b9..5dca8e376d 100644 --- a/.github/workflows/test-migrations-one-step.yml +++ b/.github/workflows/test-migrations-one-step.yml @@ -78,8 +78,23 @@ jobs: - name: Create database run: './bin/rails db:create' - - name: Run historical migrations with data population - run: './bin/rails tests:migrations:prepare_database' + - name: Run migrations up to v2.0.0 + run: './bin/rails db:migrate VERSION=20171010025614' + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2' + + - name: Run migrations up to v2.4.0 + run: './bin/rails db:migrate VERSION=20180514140000' + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2_4' + + - name: Run migrations up to v2.4.3 + run: './bin/rails db:migrate VERSION=20180707154237' + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2_4_3' - name: Run all remaining migrations run: './bin/rails db:migrate' diff --git a/.github/workflows/test-migrations-two-step.yml b/.github/workflows/test-migrations-two-step.yml index 6698847315..59485d285d 100644 --- a/.github/workflows/test-migrations-two-step.yml +++ b/.github/workflows/test-migrations-two-step.yml @@ -45,7 +45,6 @@ jobs: --health-retries 5 ports: - 5432:5432 - redis: image: redis:7-alpine options: >- @@ -78,11 +77,28 @@ jobs: - name: Create database run: './bin/rails db:create' - - name: Run historical migrations with data population - run: './bin/rails tests:migrations:prepare_database' + - name: Run migrations up to v2.0.0 + run: './bin/rails db:migrate VERSION=20171010025614' + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2' + + - name: Run pre-deployment migrations up to v2.4.0 + run: './bin/rails db:migrate VERSION=20180514140000' env: SKIP_POST_DEPLOYMENT_MIGRATIONS: true + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2_4' + + - name: Run migrations up to v2.4.3 + run: './bin/rails db:migrate VERSION=20180707154237' + env: + SKIP_POST_DEPLOYMENT_MIGRATIONS: true + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2_4_3' + - name: Run all remaining pre-deployment migrations run: './bin/rails db:migrate' env: diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index 5892c59066..ae25648a0b 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -28,9 +28,6 @@ jobs: env: RAILS_ENV: ${{ matrix.mode }} BUNDLE_WITH: ${{ matrix.mode }} - ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: precompile_placeholder - ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: precompile_placeholder - ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY: precompile_placeholder OTP_SECRET: precompile_placeholder SECRET_KEY_BASE: precompile_placeholder @@ -55,7 +52,7 @@ jobs: run: | tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs* - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v3 if: matrix.mode == 'test' with: path: |- @@ -109,19 +106,18 @@ jobs: CAS_ENABLED: true BUNDLE_WITH: 'pam_authentication test' GITHUB_RSPEC: ${{ matrix.ruby-version == '.ruby-version' && github.event.pull_request && 'true' }} - ES_ENABLED: false strategy: fail-fast: false matrix: ruby-version: + - '3.0' - '3.1' - - '3.2' - '.ruby-version' steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v3 with: path: './' name: ${{ github.sha }} @@ -143,11 +139,9 @@ jobs: - name: Upload coverage reports to Codecov if: matrix.ruby-version == '.ruby-version' - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v3 with: files: coverage/lcov/mastodon.lcov - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} test-e2e: name: End to End testing @@ -187,22 +181,19 @@ jobs: DISABLE_SIMPLECOV: true RAILS_ENV: test BUNDLE_WITH: test - ES_ENABLED: false - LOCAL_DOMAIN: localhost:3000 - LOCAL_HTTPS: false strategy: fail-fast: false matrix: ruby-version: + - '3.0' - '3.1' - - '3.2' - '.ruby-version' steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v3 with: path: './public' name: ${{ github.sha }} @@ -219,21 +210,21 @@ jobs: - name: Load database schema run: './bin/rails db:create db:schema:load db:seed' - - run: bin/rspec spec/system --tag streaming --tag js + - run: bundle exec rake spec:system - name: Archive logs - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 if: failure() with: name: e2e-logs-${{ matrix.ruby-version }} path: log/ - name: Archive test screenshots - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 if: failure() with: name: e2e-screenshots - path: tmp/capybara/ + path: tmp/screenshots/ test-search: name: Elastic Search integration testing @@ -266,8 +257,8 @@ jobs: ports: - 6379:6379 - elasticsearch: - image: ${{ contains(matrix.search-image, 'elasticsearch') && matrix.search-image || '' }} + search: + image: ${{ matrix.search-image }} env: discovery.type: single-node xpack.security.enabled: false @@ -279,20 +270,6 @@ jobs: ports: - 9200:9200 - opensearch: - image: ${{ contains(matrix.search-image, 'opensearch') && matrix.search-image || '' }} - env: - discovery.type: single-node - DISABLE_INSTALL_DEMO_CONFIG: true - DISABLE_SECURITY_PLUGIN: true - options: >- - --health-cmd "curl http://localhost:9200/_cluster/health" - --health-interval 10s - --health-timeout 5s - --health-retries 10 - ports: - - 9200:9200 - env: DB_HOST: localhost DB_USER: postgres @@ -308,21 +285,19 @@ jobs: fail-fast: false matrix: ruby-version: + - '3.0' - '3.1' - - '3.2' - '.ruby-version' search-image: - docker.elastic.co/elasticsearch/elasticsearch:7.17.13 include: - ruby-version: '.ruby-version' search-image: docker.elastic.co/elasticsearch/elasticsearch:8.10.2 - - ruby-version: '.ruby-version' - search-image: opensearchproject/opensearch:2 steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v3 with: path: './public' name: ${{ github.sha }} @@ -342,105 +317,15 @@ jobs: - run: bin/rspec --tag search - name: Archive logs - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 if: failure() with: name: test-search-logs-${{ matrix.ruby-version }} path: log/ - name: Archive test screenshots - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 if: failure() with: name: test-search-screenshots - path: tmp/capybara/ - - test-back-and-return: - name: Back to original and return test - runs-on: ubuntu-latest - - needs: - - build - - services: - postgres: - image: postgres:14-alpine - env: - POSTGRES_PASSWORD: postgres - POSTGRES_USER: postgres - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 - - redis: - image: redis:7-alpine - options: >- - --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 6379:6379 - - env: - DB_HOST: localhost - DB_USER: postgres - DB_PASS: postgres - DISABLE_SIMPLECOV: ${{ matrix.ruby-version != '.ruby-version' }} - RAILS_ENV: test - ALLOW_NOPAM: true - PAM_ENABLED: true - PAM_DEFAULT_SERVICE: pam_test - PAM_CONTROLLED_SERVICE: pam_test_controlled - OIDC_ENABLED: true - OIDC_SCOPE: read - SAML_ENABLED: true - CAS_ENABLED: true - BUNDLE_WITH: 'pam_authentication test' - GITHUB_RSPEC: ${{ matrix.ruby-version == '.ruby-version' && github.event.pull_request && 'true' }} - ES_ENABLED: false - BACK_UPSTREAM_FORCE: true - - strategy: - fail-fast: false - matrix: - ruby-version: - - '.ruby-version' - steps: - - uses: actions/checkout@v4 - - - uses: actions/download-artifact@v4 - with: - path: './' - name: ${{ github.sha }} - - - name: Expand archived asset artifacts - run: | - tar xvzf artifacts.tar.gz - - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby - with: - ruby-version: ${{ matrix.ruby-version}} - additional-system-dependencies: ffmpeg imagemagick libpam-dev - - - name: Load database schema - run: './bin/rails db:create db:schema:load db:seed' - - - name: Back to upstream schema - run: 'bundle exec rake dangerous:back_upstream' - - - name: Return to kmyblue - run: './bin/rails db:migrate' - - - run: bin/rspec - - - name: Upload coverage reports to Codecov - if: matrix.ruby-version == '.ruby-version' - uses: codecov/codecov-action@v3 - with: - files: coverage/lcov/mastodon-back-ret.lcov + path: tmp/screenshots/ diff --git a/.gitignore b/.gitignore index 4106e3f7f8..c5af8eb67f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,12 +24,10 @@ /public/packs-test .env .env.production +.env.development /node_modules/ /build/ -# Ignore elasticsearch config -/.elasticsearch.yml - # Ignore Vagrant files .vagrant/ @@ -71,6 +69,3 @@ yarn-debug.log # Ignore Docker option files docker-compose.override.yml - -# Ignore dotenv .local files -.env*.local diff --git a/.haml-lint.yml b/.haml-lint.yml index 7dbc88e9db..8cfcaec8d9 100644 --- a/.haml-lint.yml +++ b/.haml-lint.yml @@ -2,6 +2,7 @@ inherits_from: .haml-lint_todo.yml exclude: - 'vendor/**/*' + - lib/templates/haml/scaffold/_form.html.haml require: - ./lib/linter/haml_middle_dot.rb @@ -12,6 +13,4 @@ linters: MiddleDot: enabled: true LineLength: - max: 300 - ViewLength: - max: 200 # Override default value of 100 inherited from rubocop + max: 320 diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml index 841561291f..af2d2e8f4e 100644 --- a/.haml-lint_todo.yml +++ b/.haml-lint_todo.yml @@ -10,27 +10,4 @@ linters: # Offense count: 1 LineLength: exclude: - - 'app/views/admin/ng_rules/_ng_rule_fields.html.haml' - 'app/views/admin/roles/_form.html.haml' - - # Offense count: 9 - RuboCop: - exclude: - - 'app/views/home/index.html.haml' - - ViewLength: - exclude: - - 'app/views/admin/accounts/index.html.haml' - - 'app/views/admin/instances/show.html.haml' - - 'app/views/admin/ng_rules/_ng_rule_fields.html.haml' - - 'app/views/admin/settings/discovery/show.html.haml' - - 'app/views/settings/preferences/appearance/show.html.haml' - - 'app/views/settings/preferences/other/show.html.haml' - - InstanceVariables: - exclude: - - 'app/views/application/_sidebar.html.haml' - - 'app/views/admin/ng_rules/_ng_rule_fields.html.haml' - - 'app/views/admin/ng_words/keywords/_ng_word.html.haml' - - 'app/views/admin/ng_words/white_list/_specified_domain.html.haml' - - 'app/views/admin/sensitive_words/_sensitive_word.html.haml' diff --git a/.husky/pre-commit b/.husky/pre-commit index 3723623171..d2ae35e84b 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + yarn lint-staged diff --git a/.nvmrc b/.nvmrc index 973f49d55c..a3597ecbd1 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.13 +20.11 diff --git a/.prettierignore b/.prettierignore index 6b2f0c1889..51850b2b28 100644 --- a/.prettierignore +++ b/.prettierignore @@ -54,13 +54,6 @@ # Ignore Docker option files docker-compose.override.yml -# Ignore public -/public/assets -/public/emoji -/public/packs -/public/packs-test -/public/system - # Ignore emoji map file /app/javascript/mastodon/features/emoji/emoji_map.json @@ -81,5 +74,4 @@ app/javascript/styles/mastodon/reset.scss # Ignore the generated AUTHORS.md AUTHORS.md -# Process a few selected JS files !lint-staged.config.js diff --git a/.rubocop.yml b/.rubocop.yml index 5596727890..a06621d660 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -9,13 +9,12 @@ inherit_mode: require: - rubocop-rails - rubocop-rspec - - rubocop-rspec_rails - rubocop-performance - rubocop-capybara - ./lib/linter/rubocop_middle_dot AllCops: - TargetRubyVersion: 3.1 # Set to minimum supported version of CI + TargetRubyVersion: 3.0 # Set to minimum supported version of CI DisplayCopNames: true DisplayStyleGuide: true ExtraDetails: true @@ -23,7 +22,7 @@ AllCops: CacheRootDirectory: tmp NewCops: enable # Opt-in to newly added rules Exclude: - - 'db/schema.rb' + - db/schema.rb - 'bin/*' - 'node_modules/**/*' - 'Vagrantfile' @@ -40,7 +39,13 @@ Layout/FirstHashElementIndentation: # Reason: Currently disabled in .rubocop_todo.yml # https://docs.rubocop.org/rubocop/cops_layout.html#layoutlinelength Layout/LineLength: - Max: 300 # Default of 120 causes a duplicate entry in generated todo file + Max: 320 # Default of 120 causes a duplicate entry in generated todo file + +# Reason: +# https://docs.rubocop.org/rubocop/cops_lint.html#lintuselessaccessmodifier +Lint/UselessAccessModifier: + ContextCreatingMethods: + - class_methods ## Disable most Metrics/*Length cops # Reason: those are often triggered and force significant refactors when this happend @@ -68,18 +73,12 @@ Metrics/ModuleLength: # https://docs.rubocop.org/rubocop/cops_metrics.html#metricsabcsize Metrics/AbcSize: Exclude: - - 'app/serializers/initial_state_serializer.rb' - 'lib/mastodon/cli/*.rb' # Reason: Currently disabled in .rubocop_todo.yml # https://docs.rubocop.org/rubocop/cops_metrics.html#metricscyclomaticcomplexity Metrics/CyclomaticComplexity: Exclude: - - 'app/lib/feed_manager.rb' - - 'app/policies/status_policy.rb' - - 'app/services/activitypub/process_account_service.rb' - - 'app/services/delivery_antenna_service.rb' - - 'app/services/post_status_service.rb' - lib/mastodon/cli/*.rb # Reason: @@ -87,17 +86,6 @@ Metrics/CyclomaticComplexity: Metrics/ParameterLists: CountKeywordArgs: false -Metrics/PerceivedComplexity: - Exclude: - - 'app/policies/status_policy.rb' - - 'app/services/delivery_antenna_service.rb' - - 'app/services/post_status_service.rb' - -# Reason: Prefer seeing a variable name -# https://docs.rubocop.org/rubocop/cops_naming.html#namingblockforwarding -Naming/BlockForwarding: - EnforcedStyle: explicit - # Reason: Prevailing style is argument file paths # https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsfilepath Rails/FilePath: @@ -108,26 +96,22 @@ Rails/FilePath: Rails/HttpStatus: EnforcedStyle: numeric +# Reason: Allowed in `tootctl` CLI code and in boot ENV checker +# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsexit +Rails/Exit: + Exclude: + - 'config/boot.rb' + - 'lib/mastodon/cli/*.rb' + # 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 + Exclude: + - 'db/*migrate/**/*' # 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. @@ -140,11 +124,6 @@ Rails/UnusedIgnoredColumns: Rails/NegateInclude: Enabled: false -# Reason: Enforce default limit, but allow some elements to span lines -# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecexamplelength -RSpec/ExampleLength: - CountAsOne: ['array', 'heredoc', 'method_call'] - # Reason: Deprecated cop, will be removed in 3.0, replaced by SpecFilePathFormat # https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecfilepath RSpec/FilePath: @@ -160,6 +139,11 @@ RSpec/NamedSubject: RSpec/NotToNot: EnforcedStyle: to_not +# Reason: Prevailing style uses numeric status codes, matches Rails/HttpStatus +# https://docs.rubocop.org/rubocop-rspec/cops_rspec_rails.html#rspecrailshttpstatus +RSpec/Rails/HttpStatus: + EnforcedStyle: numeric + # Reason: Match overrides from Rspec/FilePath rule above # https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecspecfilepathformat RSpec/SpecFilePathFormat: @@ -170,11 +154,6 @@ RSpec/SpecFilePathFormat: OEmbedController: oembed_controller OStatus: ostatus -# Reason: Prevailing style uses numeric status codes, matches Rails/HttpStatus -# https://docs.rubocop.org/rubocop-rspec/cops_rspec_rails.html#rspecrailshttpstatus -RSpecRails/HttpStatus: - EnforcedStyle: numeric - # Reason: # https://docs.rubocop.org/rubocop/cops_style.html#styleclassandmodulechildren Style/ClassAndModuleChildren: @@ -185,25 +164,10 @@ Style/ClassAndModuleChildren: Style/Documentation: Enabled: false -# Reason: Route redirects are not token-formatted and must be skipped -# https://docs.rubocop.org/rubocop/cops_style.html#styleformatstringtoken -Style/FormatStringToken: - inherit_mode: - merge: - - AllowedMethods # The rubocop-rails config adds `redirect` - AllowedMethods: - - redirect_with_vary - -# Reason: Prevailing style choice -# https://docs.rubocop.org/rubocop/cops_style.html#stylehashaslastarrayitem -Style/HashAsLastArrayItem: - Enabled: false - # Reason: Enforce modern Ruby style # https://docs.rubocop.org/rubocop/cops_style.html#stylehashsyntax Style/HashSyntax: EnforcedStyle: ruby19_no_mixed_keys - EnforcedShorthandSyntax: either # Reason: # https://docs.rubocop.org/rubocop/cops_style.html#stylenumericliterals @@ -223,16 +187,16 @@ Style/PercentLiteralDelimiters: Style/RedundantBegin: Enabled: false -# Reason: Prevailing style choice -# https://docs.rubocop.org/rubocop/cops_style.html#styleredundantfetchblock -Style/RedundantFetchBlock: - Enabled: false - # Reason: Overridden to reduce implicit StandardError rescues # https://docs.rubocop.org/rubocop/cops_style.html#stylerescuestandarderror Style/RescueStandardError: EnforcedStyle: implicit +# Reason: Simplify some spec layouts +# https://docs.rubocop.org/rubocop/cops_style.html#stylesemicolon +Style/Semicolon: + AllowAsExpressionSeparator: true + # Reason: Originally disabled for CodeClimate, and no config consensus has been found # https://docs.rubocop.org/rubocop/cops_style.html#stylesymbolarray Style/SymbolArray: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 119df96090..602d99c9f0 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,11 +1,25 @@ # 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.63.5. +# using RuboCop version 1.59.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include. +# Include: **/*.gemfile, **/Gemfile, **/gems.rb +Bundler/OrderedGems: + Exclude: + - 'Gemfile' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. +# URISchemes: http, https +Layout/LineLength: + Exclude: + - 'app/models/account.rb' + Lint/NonLocalExitFromIterator: Exclude: - 'app/helpers/jsonld_helper.rb' @@ -29,27 +43,119 @@ Metrics/PerceivedComplexity: # Configuration parameters: CountAsOne. RSpec/ExampleLength: - Max: 18 + Max: 22 RSpec/MultipleExpectations: - Max: 7 + Max: 8 # Configuration parameters: AllowSubject. RSpec/MultipleMemoizedHelpers: Max: 17 - Exclude: - - 'spec/lib/activitypub/activity/create_spec.rb' - - 'spec/services/delete_account_service_spec.rb' - - 'spec/services/fan_out_on_write_service_spec.rb' # Configuration parameters: AllowedGroups. RSpec/NestedGroups: Max: 6 +# Configuration parameters: Include. +# Include: app/models/**/*.rb +Rails/HasAndBelongsToMany: + Exclude: + - 'app/models/concerns/account/associations.rb' + - 'app/models/status.rb' + - 'app/models/tag.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' + - '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: + Exclude: + - 'app/models/account_alias.rb' + - 'app/models/custom_filter_status.rb' + - 'app/models/identity.rb' + - 'app/models/webauthn_credential.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/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/vote_service.rb' + - 'app/validators/reaction_validator.rb' + - 'app/validators/vote_validator.rb' + - 'app/workers/move_worker.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 unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowedMethods, AllowedPatterns. # AllowedMethods: ==, equal?, eql? @@ -58,12 +164,17 @@ Style/ClassEqualityComparison: - 'app/helpers/jsonld_helper.rb' - 'app/serializers/activitypub/outbox_serializer.rb' +Style/ClassVars: + Exclude: + - 'config/initializers/devise.rb' + # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowedVars. Style/FetchEnvVar: Exclude: - 'app/lib/redis_configuration.rb' - 'app/lib/translation_service.rb' + - 'config/environments/development.rb' - 'config/environments/production.rb' - 'config/initializers/2_limited_federation_mode.rb' - 'config/initializers/3_omniauth.rb' @@ -73,8 +184,9 @@ Style/FetchEnvVar: - 'config/initializers/paperclip.rb' - 'config/initializers/vapid.rb' - 'lib/mastodon/redis_config.rb' + - 'lib/premailer_webpack_strategy.rb' - 'lib/tasks/repo.rake' - - 'spec/system/profile_spec.rb' + - 'spec/features/profile_spec.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns. @@ -82,6 +194,7 @@ Style/FetchEnvVar: # AllowedMethods: redirect Style/FormatStringToken: Exclude: + - 'app/models/privacy_policy.rb' - 'config/initializers/devise.rb' - 'lib/paperclip/color_extractor.rb' @@ -95,6 +208,10 @@ 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' @@ -118,14 +235,35 @@ Style/GuardClause: - 'lib/mastodon/cli/accounts.rb' - 'lib/mastodon/cli/maintenance.rb' - 'lib/mastodon/cli/media.rb' + - 'lib/paperclip/attachment_extensions.rb' - 'lib/tasks/repo.rake' +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: braces, no_braces +Style/HashAsLastArrayItem: + Exclude: + - 'app/controllers/admin/statuses_controller.rb' + - 'app/controllers/api/v1/statuses_controller.rb' + - 'app/models/concerns/account/counters.rb' + - 'app/models/concerns/status/threading_concern.rb' + - 'app/models/status.rb' + - 'app/services/batched_remove_status_service.rb' + - 'app/services/notify_service.rb' + # This cop supports unsafe autocorrection (--autocorrect-all). Style/HashTransformValues: Exclude: - 'app/serializers/rest/web_push_subscription_serializer.rb' - 'app/services/import_service.rb' +# This cop supports safe autocorrection (--autocorrect). +Style/IfUnlessModifier: + Exclude: + - 'config/environments/production.rb' + - 'config/initializers/devise.rb' + - 'config/initializers/ffmpeg.rb' + # This cop supports unsafe autocorrection (--autocorrect-all). Style/MapToHash: Exclude: @@ -160,6 +298,13 @@ Style/OptionalBooleanParameter: - 'app/workers/unfollow_follow_worker.rb' - 'lib/mastodon/redis_config.rb' +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: PreferredDelimiters. +Style/PercentLiteralDelimiters: + Exclude: + - 'config/deploy.rb' + - 'config/initializers/doorkeeper.rb' + # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: short, verbose @@ -173,6 +318,16 @@ Style/RedundantConstantBase: - 'config/environments/production.rb' - 'config/initializers/sidekiq.rb' +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: SafeForConstants. +Style/RedundantFetchBlock: + Exclude: + - 'config/initializers/1_hosts.rb' + - 'config/initializers/chewy.rb' + - 'config/initializers/devise.rb' + - 'config/initializers/paperclip.rb' + - 'config/puma.rb' + # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. # AllowedMethods: present?, blank?, presence, try, try! @@ -180,12 +335,59 @@ Style/SafeNavigation: Exclude: - 'app/models/concerns/account/finder_concern.rb' +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: only_raise, only_fail, semantic +Style/SignalException: + Exclude: + - 'lib/devise/strategies/two_factor_ldap_authenticatable.rb' + - 'lib/devise/strategies/two_factor_pam_authenticatable.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/SingleArgumentDig: + Exclude: + - 'lib/webpacker/manifest_extensions.rb' + # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Mode. Style/StringConcatenation: Exclude: - 'config/initializers/paperclip.rb' +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. +# SupportedStyles: single_quotes, double_quotes +Style/StringLiterals: + Exclude: + - 'config/environments/production.rb' + - 'config/initializers/backtrace_silencers.rb' + - 'config/initializers/http_client_proxy.rb' + - 'config/initializers/rack_attack.rb' + - 'config/initializers/webauthn.rb' + - 'config/routes.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, AllowSafeAssignment. +# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex +Style/TernaryParentheses: + Exclude: + - 'config/environments/development.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyleForMultiline. +# SupportedStylesForMultiline: comma, consistent_comma, no_comma +Style/TrailingCommaInArguments: + Exclude: + - 'config/initializers/paperclip.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyleForMultiline. +# SupportedStylesForMultiline: comma, consistent_comma, no_comma +Style/TrailingCommaInHashLiteral: + Exclude: + - 'config/environments/production.rb' + - 'config/environments/test.rb' + # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: WordRegex. # SupportedStyles: percent, brackets diff --git a/.ruby-version b/.ruby-version index bea438e9ad..be94e6f53d 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.3.1 +3.2.2 diff --git a/.simplecov b/.simplecov new file mode 100644 index 0000000000..fbd0207bec --- /dev/null +++ b/.simplecov @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +if ENV['CI'] + require 'simplecov-lcov' + SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true + SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter +else + SimpleCov.formatter = SimpleCov::Formatter::HTMLFormatter +end + +SimpleCov.start 'rails' do + enable_coverage :branch + + add_filter 'lib/linter' + + add_group 'Libraries', 'lib' + add_group 'Policies', 'app/policies' + add_group 'Presenters', 'app/presenters' + add_group 'Serializers', 'app/serializers' + add_group 'Services', 'app/services' + add_group 'Validators', 'app/validators' +end diff --git a/AUTHORS_KB.md b/AUTHORS_KB.md deleted file mode 100644 index 2cda5fd31d..0000000000 --- a/AUTHORS_KB.md +++ /dev/null @@ -1,18 +0,0 @@ -# Authors for kmyblue fork - -## 貢献者 - -kmyblueフォークは、以下の方の貢献によって成り立っています。 -本家Mastodonの貢献者については、`AUTHORS.md`をご覧ください。 - -- [aoisensi](https://github.com/aoisensi) -- [KMY](https://github.com/kmycode) -- [S-H-GAMELINKS](https://github.com/S-H-GAMELINKS) -- [Yuicho](https://github.com/yuicho) - -## 特記 - -kmyblueフォークの開発にあたって、API・Activity仕様の設計(一部機能については内部仕様)策定の過程で下記リポジトリのコードを参考にしました。 -kmyblueフォークに直接貢献したわけではありませんが、以下のリポジトリにある絵文字リアクション機能・検索範囲機能のコードのうち一部にkmyblueへ転写した箇所がございますため、お名前記載させていただきます。 - -- [Fedibird](https://github.com/fedibird/mastodon) diff --git a/CHANGELOG.md b/CHANGELOG.md index a53790afaf..6f775fcfa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,101 +2,6 @@ All notable changes to this project will be documented in this file. -## [4.2.7] - 2024-02-16 - -### Fixed - -- Fix OmniAuth tests and edge cases in error handling ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29201), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/29207)) -- Fix new installs by upgrading to the latest release of the `nsa` gem, instead of a no longer existing commit ([mjankowski](https://github.com/mastodon/mastodon/pull/29065)) - -### Security - -- Fix insufficient checking of remote posts ([GHSA-jhrq-qvrm-qr36](https://github.com/mastodon/mastodon/security/advisories/GHSA-jhrq-qvrm-qr36)) - -## [4.2.6] - 2024-02-14 - -### Security - -- Update the `sidekiq-unique-jobs` dependency (see [GHSA-cmh9-rx85-xj38](https://github.com/mhenrixon/sidekiq-unique-jobs/security/advisories/GHSA-cmh9-rx85-xj38)) - In addition, we have disabled the web interface for `sidekiq-unique-jobs` out of caution. - If you need it, you can re-enable it by setting `ENABLE_SIDEKIQ_UNIQUE_JOBS_UI=true`. - If you only need to clear all locks, you can now use `bundle exec rake sidekiq_unique_jobs:delete_all_locks`. -- Update the `nokogiri` dependency (see [GHSA-xc9x-jj77-9p9j](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-xc9x-jj77-9p9j)) -- Disable administrative Doorkeeper routes ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/29187)) -- Fix ongoing streaming sessions not being invalidated when applications get deleted in some cases ([GHSA-7w3c-p9j8-mq3x](https://github.com/mastodon/mastodon/security/advisories/GHSA-7w3c-p9j8-mq3x)) - In some rare cases, the streaming server was not notified of access tokens revocation on application deletion. -- Change external authentication behavior to never reattach a new identity to an existing user by default ([GHSA-vm39-j3vx-pch3](https://github.com/mastodon/mastodon/security/advisories/GHSA-vm39-j3vx-pch3)) - Up until now, Mastodon has allowed new identities from external authentication providers to attach to an existing local user based on their verified e-mail address. - This allowed upgrading users from a database-stored password to an external authentication provider, or move from one authentication provider to another. - However, this behavior may be unexpected, and means that when multiple authentication providers are configured, the overall security would be that of the least secure authentication provider. - For these reasons, this behavior is now locked under the `ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH` environment variable. - In addition, regardless of this environment variable, Mastodon will refuse to attach two identities from the same authentication provider to the same account. - -## [4.2.5] - 2024-02-01 - -### Security - -- Fix insufficient origin validation (CVE-2024-23832, [GHSA-3fjr-858r-92rw](https://github.com/mastodon/mastodon/security/advisories/GHSA-3fjr-858r-92rw)) - -## [4.2.4] - 2024-01-24 - -### Fixed - -- Fix error when processing remote files with unusually long names ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28823)) -- Fix processing of compacted single-item JSON-LD collections ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28816)) -- Retry 401 errors on replies fetching ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/28788)) -- Fix `RecordNotUnique` errors in LinkCrawlWorker ([tribela](https://github.com/mastodon/mastodon/pull/28748)) -- Fix Mastodon not correctly processing HTTP Signatures with query strings ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28443), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28476)) -- Fix potential redirection loop of streaming endpoint ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28665)) -- Fix streaming API redirection ignoring the port of `streaming_api_base_url` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28558)) -- Fix error when processing link preview with an array as `inLanguage` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28252)) -- Fix unsupported time zone or locale preventing sign-up ([Gargron](https://github.com/mastodon/mastodon/pull/28035)) -- Fix "Hide these posts from home" list setting not refreshing when switching lists ([brianholley](https://github.com/mastodon/mastodon/pull/27763)) -- Fix missing background behind dismissable banner in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/27479)) -- Fix line wrapping of language selection button with long locale codes ([gunchleoc](https://github.com/mastodon/mastodon/pull/27100), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27127)) -- Fix `Undo Announce` activity not being sent to non-follower authors ([MitarashiDango](https://github.com/mastodon/mastodon/pull/18482)) -- Fix N+1s because of association preloaders not actually getting called ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28339)) -- Fix empty column explainer getting cropped under certain conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28337)) -- Fix `LinkCrawlWorker` error when encountering empty OEmbed response ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28268)) -- Fix call to inefficient `delete_matched` cache method in domain blocks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28367)) - -### Security - -- Add rate-limit of TOTP authentication attempts at controller level ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28801)) - -## [4.2.3] - 2023-12-05 - -### Fixed - -- Fix dependency on `json-canonicalization` version that has been made unavailable since last release - -## [4.2.2] - 2023-12-04 - -### Changed - -- Change dismissed banners to be stored server-side ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27055)) -- Change GIF max matrix size error to explicitly mention GIF files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27927)) -- Change `Follow` activities delivery to bypass availability check ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/27586)) -- Change single-column navigation notice to be displayed outside of the logo container ([renchap](https://github.com/mastodon/mastodon/pull/27462), [renchap](https://github.com/mastodon/mastodon/pull/27476)) -- Change Content-Security-Policy to be tighter on media paths ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26889)) -- Change post language code to include country code when relevant ([gunchleoc](https://github.com/mastodon/mastodon/pull/27099), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27207)) - -### Fixed - -- Fix upper border radius of onboarding columns ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27890)) -- Fix incoming status creation date not being restricted to standard ISO8601 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27655), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28081)) -- Fix some posts from threads received out-of-order sometimes not being inserted into timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27653)) -- Fix posts from force-sensitized accounts being able to trend ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27620)) -- Fix error when trying to delete already-deleted file with OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27569)) -- Fix batch attachment deletion when using OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27554)) -- Fix processing LDSigned activities from actors with unknown public keys ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27474)) -- Fix error and incorrect URLs in `/api/v1/accounts/:id/featured_tags` for remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27459)) -- Fix report processing notice not mentioning the report number when performing a custom action ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27442)) -- Fix handling of `inLanguage` attribute in preview card processing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27423)) -- Fix own posts being removed from home timeline when unfollowing a used hashtag ([kmycode](https://github.com/mastodon/mastodon/pull/27391)) -- Fix some link anchors being recognized as hashtags ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27271), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27584)) -- Fix format-dependent redirects being cached regardless of requested format ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27634)) - ## [4.2.1] - 2023-10-10 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 43f8a79249..b68a9bde3e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,35 +1,50 @@ -# CONTRIBUTING +# Contributing -kmyblueは、コミュニティの意見も聞くには聞きますが導入する・しないは管理人が決定します。 +Thank you for considering contributing to Mastodon 🐘 -## バグ報告 +You can contribute in the following ways: -バグについて、L最新よりも過去のバージョンへの対応は、LTSや特別な場合以外は行いません。 +- Finding and reporting bugs +- Translating the Mastodon interface into various languages +- Contributing code to Mastodon by fixing bugs or implementing features +- Improving the documentation -以下のいずれかの方法で報告してください。 +If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon). -- [GitHub Issues](https://github.com/kmycode/mastodon/issues) (セキュリティインシデントはここの一番下から) -- [kmyblue開発者への連絡](https://kmy.blue/@askyq) -- [kmyblue開発者へのメール](https://kmy.blue/about) +## 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). -新しい機能や既存機能の修正については、プルリクエストのためにコードを作成する前に、まずGitHub Issuesで機能の提案を行いkmyblue開発者の考えを聞くことをおすすめします。バグ修正、翻訳、テストコードなどは基本受け入れますが、依存モジュールのバージョンアップについては特別な事情がなければ本家Mastodonよりも先に行かないようにしてください。 +## 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. -### kmyblueの開発方針 +## Translations -下記のものに矛盾がなければ、あとは管理人の意向次第です。 +You can submit translations via [Crowdin](https://crowdin.com/project/mastodon). They are periodically merged into the codebase. -- **自分の投稿を見せたくない人に見せない** -- **他人の見たくない投稿を見ない** -- ただし本家Mastodonで上記原則に矛盾した機能が追加された場合は従う -- 画面を騒がしくするような機能(絵文字を大きく表示するなど)は追加しないか、控えめにする。ただし他のソフトウェアにも導入され利用者が多くいる場合などは別途判断して、オプトアウト可能な設定項目とともに追加する -- 負荷を著しく上げるような機能はできるだけ追加しない +[![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)](https://crowdin.com/project/mastodon) -kmyblueが意図的に実装していない機能は、例えば以下のものがあります。詳しい理由が知りたい場合は[この記事を参照するか](https://note.com/kmycode/n/n463410b5e03c)、別途お問い合わせください。もちろん明確な根拠がある場合、あなたはこれに抗議する権利を有しますが、あなたがこのkmyblueをフォークして新しいリポジトリを作るほうがより自由でしょう。 +## Pull requests -- お気に入り一覧の公開 -- ブックマーク分類の公開 -- Fedibird、Misskeyにあるような詳細な画面表示オプション +**Please use clean, concise titles for your pull requests.** Unless the pull request is about refactoring code, updating dependencies or other internal tasks, assume that the person reading the pull request title is not a programmer or Mastodon developer, but instead a Mastodon user or server administrator, and **try to describe your change or fix from their perspective**. We use commit squashing, so the final commit in the main branch will carry the title of the pull request, and commits from the main branch are fed into the changelog. The changelog is separated into [keepachangelog.com categories](https://keepachangelog.com/en/1.0.0/), and while that spec does not prescribe how the entries ought to be named, for easier sorting, start your pull request titles using one of the verbs "Add", "Change", "Deprecate", "Remove", or "Fix" (present tense). + +Example: + +| Not ideal | Better | +| ------------------------------------ | ------------------------------------------------------------- | +| Fixed NoMethodError in RemovalWorker | Fix nil error when removing statuses caused by race condition | + +It is not always possible to phrase every change in such a manner, but it is desired. + +**The smaller the set of changes in the pull request is, the quicker it can be reviewed and merged.** Splitting tasks into multiple smaller pull requests is often preferable. + +**Pull requests that do not pass automated checks may not be reviewed**. In particular, you need to keep in mind: + +- Unit and integration tests (rspec, jest) +- Code style rules (rubocop, eslint) +- Normalization of locale files (i18n-tasks) + +## Documentation + +The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/documentation](https://github.com/mastodon/documentation). diff --git a/Dockerfile b/Dockerfile index 4278242bc9..96f8b5cd27 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1.7 +# syntax=docker/dockerfile:1.4 # Please see https://docs.docker.com/engine/reference/builder for information about # the extended buildx capabilities used in this file. @@ -7,20 +7,20 @@ ARG TARGETPLATFORM=${TARGETPLATFORM} ARG BUILDPLATFORM=${BUILDPLATFORM} -# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.3.1"] -ARG RUBY_VERSION="3.3.1" +# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.2.2"] +ARG RUBY_VERSION="3.2.2" # # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] ARG NODE_MAJOR_VERSION="20" # Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"] ARG DEBIAN_VERSION="bookworm" # Node image to use for base image based on combined variables (ex: 20-bookworm-slim) FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim as node -# Ruby image to use for base image based on combined variables (ex: 3.3.1-slim-bookworm) +# Ruby image to use for base image based on combined variables (ex: 3.2.2-slim-bookworm) FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby # Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA # Example: v4.2.0-nightly.2023.11.09+something -# Overwrite existence of 'alpha.0' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"] +# Overwrite existance of 'alpha.0' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"] ARG MASTODON_VERSION_PRERELEASE="" # Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="something"] ARG MASTODON_VERSION_METADATA="" @@ -29,7 +29,7 @@ ARG MASTODON_VERSION_METADATA="" # See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files ARG RAILS_SERVE_STATIC_FILES="true" # Allow to use YJIT compiler -# See: https://github.com/ruby/ruby/blob/v3_2_4/doc/yjit/yjit.md +# See: https://github.com/ruby/ruby/blob/master/doc/yjit/yjit.md ARG RUBY_YJIT_ENABLE="1" # Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin] ARG TZ="Etc/UTC" @@ -205,12 +205,7 @@ ARG TARGETPLATFORM RUN \ # Use Ruby on Rails to create Mastodon assets - ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=precompile_placeholder \ - ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=precompile_placeholder \ - ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=precompile_placeholder \ - OTP_SECRET=precompile_placeholder \ - SECRET_KEY_BASE=precompile_placeholder \ - bundle exec rails assets:precompile; \ + OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder bundle exec rails assets:precompile; \ # Cleanup temporary files rm -fr /opt/mastodon/tmp; @@ -262,4 +257,4 @@ USER mastodon # Expose default Puma ports EXPOSE 3000 # Set container tini as default entry point -ENTRYPOINT ["/usr/bin/tini", "--"] +ENTRYPOINT ["/usr/bin/tini", "--"] \ No newline at end of file diff --git a/FEDERATION.md b/FEDERATION.md index 2819fa935a..e3721d7241 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -1,35 +1,19 @@ -# 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 +## ActivityPub federation 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 ActivityPub vocabulary](https://docs.joinmastodon.org/spec/activitypub/) +Supported 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. -- [WebFinger information and examples](https://docs.joinmastodon.org/spec/webfinger/) +More information and examples are available at: https://docs.joinmastodon.org/spec/webfinger/ #### HTTP Signatures @@ -37,13 +21,11 @@ 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. -- [HTTP Signatures information and examples](https://docs.joinmastodon.org/spec/security/#http) +More information on HTTP Signatures, as well as examples, can be found here: 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/) - -### Additional documentation - -- [Mastodon documentation](https://docs.joinmastodon.org/) +- 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 diff --git a/Gemfile b/Gemfile index 240dcce95a..2d4e504ac7 100644 --- a/Gemfile +++ b/Gemfile @@ -1,37 +1,37 @@ # frozen_string_literal: true source 'https://rubygems.org' -ruby '>= 3.1.0' +ruby '>= 3.0.0' -gem 'propshaft' gem 'puma', '~> 6.3' -gem 'rack', '~> 2.2.7' gem 'rails', '~> 7.1.1' +gem 'propshaft' gem 'thor', '~> 1.2' +gem 'rack', '~> 2.2.7' # For why irb is in the Gemfile, see: https://ruby.social/@st0012/111444685161478182 gem 'irb', '~> 1.8' -gem 'dotenv' gem 'haml-rails', '~>2.0' gem 'pg', '~> 1.5' gem 'pghero' +gem 'dotenv-rails', '~> 2.8' gem 'aws-sdk-s3', '~> 1.123', require: false -gem 'blurhash', '~> 0.1' gem 'fog-core', '<= 2.4.0' gem 'fog-openstack', '~> 1.0', require: false gem 'kt-paperclip', '~> 7.2' gem 'md-paperclip-azure', '~> 2.2', require: false +gem 'blurhash', '~> 0.1' gem 'active_model_serializers', '~> 0.10' gem 'addressable', '~> 2.8' -gem 'bootsnap', '~> 1.18.0', require: false +gem 'bootsnap', '~> 1.17.0', require: false gem 'browser' gem 'charlock_holmes', '~> 0.7.7' gem 'chewy', '~> 7.3' gem 'devise', '~> 4.9' -gem 'devise-two-factor' +gem 'devise-two-factor', '~> 4.1' group :pam_authentication, optional: true do gem 'devise_pam_authenticatable2', '~> 9.2' @@ -39,11 +39,11 @@ end gem 'net-ldap', '~> 0.18' -gem 'omniauth', '~> 2.0' gem 'omniauth-cas', '~> 3.0.0.beta.1' -gem 'omniauth_openid_connect', '~> 0.6.1' -gem 'omniauth-rails_csrf_protection', '~> 1.0' 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' @@ -53,49 +53,48 @@ gem 'ed25519', '~> 1.3' gem 'fast_blank', '~> 1.0' gem 'fastimage' gem 'hiredis', '~> 0.6' +gem 'redis-namespace', '~> 1.10' gem 'htmlentities', '~> 4.3' -gem 'http', '~> 5.2.0' +gem 'http', '~> 5.1' gem 'http_accept_language', '~> 2.1' gem 'httplog', '~> 1.6.2' -gem 'i18n' gem 'idn-ruby', require: 'idn' -gem 'inline_svg' gem 'kaminari', '~> 1.2' gem 'link_header', '~> 0.0' -gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar' gem 'nokogiri', '~> 1.15' -gem 'nsa' +gem 'nsa', github: 'jhawthorn/nsa', ref: 'e020fcc3a54d993ab45b7194d89ab720296c111b' gem 'oj', '~> 3.14' gem 'ox', '~> 2.14' gem 'parslet' -gem 'premailer-rails' +gem 'posix-spawn' gem 'public_suffix', '~> 5.0' gem 'pundit', '~> 2.3' +gem 'premailer-rails' gem 'rack-attack', '~> 6.6' gem 'rack-cors', '~> 2.0', require: 'rack/cors' gem 'rails-i18n', '~> 7.0' gem 'redcarpet', '~> 3.6' gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] -gem 'redis-namespace', '~> 1.10' +gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'rqrcode', '~> 2.2' gem 'ruby-progressbar', '~> 1.13' gem 'sanitize', '~> 6.0' gem 'scenic', '~> 1.7' gem 'sidekiq', '~> 6.5' -gem 'sidekiq-bulk', '~> 0.2.0' gem 'sidekiq-scheduler', '~> 5.0' gem 'sidekiq-unique-jobs', '~> 7.1' -gem 'simple_form', '~> 5.2' +gem 'sidekiq-bulk', '~> 0.2.0' gem 'simple-navigation', '~> 4.4' -gem 'stoplight', '~> 4.1' -gem 'strong_migrations', '1.8.0' +gem 'simple_form', '~> 5.2' +gem 'stoplight', '~> 3.0.1' +gem 'strong_migrations', '1.7.0' gem 'tty-prompt', '~> 0.23', require: false gem 'twitter-text', '~> 3.1.0' gem 'tzinfo-data', '~> 1.2023' -gem 'webauthn', '~> 3.0' gem 'webpacker', '~> 5.4' gem 'webpush', github: 'ClearlyClaire/webpush', ref: 'f14a4d52e201128b1b00245d11b6de80d6cfdcd9' +gem 'webauthn', '~> 3.0' gem 'json-ld' gem 'json-ld-preloaded', '~> 3.2' @@ -103,24 +102,6 @@ gem 'rdf-normalize', '~> 0.5' gem 'private_address_check', '~> 0.5' -group :opentelemetry do - gem 'opentelemetry-exporter-otlp', '~> 0.26.3', require: false - gem 'opentelemetry-instrumentation-active_job', '~> 0.7.1', require: false - gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.20.1', require: false - gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.21.2', require: false - gem 'opentelemetry-instrumentation-excon', '~> 0.22.0', require: false - gem 'opentelemetry-instrumentation-faraday', '~> 0.24.1', require: false - gem 'opentelemetry-instrumentation-http', '~> 0.23.2', require: false - gem 'opentelemetry-instrumentation-http_client', '~> 0.22.3', require: false - gem 'opentelemetry-instrumentation-net_http', '~> 0.22.4', require: false - gem 'opentelemetry-instrumentation-pg', '~> 0.27.1', require: false - gem 'opentelemetry-instrumentation-rack', '~> 0.24.1', require: false - gem 'opentelemetry-instrumentation-rails', '~> 0.30.0', require: false - gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false - gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.2', require: false - gem 'opentelemetry-sdk', '~> 1.4', require: false -end - group :test do # Adds RSpec Error/Warning annotations to GitHub PRs on the Files tab gem 'rspec-github', '~> 2.4', require: false @@ -131,8 +112,8 @@ group :test do # RSpec helpers for email specs gem 'email_spec' - # Extra RSpec extension methods and helpers for sidekiq - gem 'rspec-sidekiq', '~> 5.0' + # Extra RSpec extenion methods and helpers for sidekiq + gem 'rspec-sidekiq', '~> 4.0' # Browser integration testing gem 'capybara', '~> 3.39' @@ -142,7 +123,13 @@ group :test do gem 'database_cleaner-active_record' # Used to mock environment variables - gem 'climate_control' + gem 'climate_control', '~> 0.2' + + # Generating fake data for specs + gem 'faker', '~> 3.2' + + # Generate test objects for specs + gem 'fabrication', '~> 2.30' # Add back helpers functions removed in Rails 5.1 gem 'rails-controller-testing', '~> 1.0' @@ -178,7 +165,7 @@ group :development do # Preview mail in the browser gem 'letter_opener', '~> 1.8' - gem 'letter_opener_web', '~> 3.0' + gem 'letter_opener_web', '~> 2.0' # Security analysis CLI tools gem 'brakeman', '~> 6.0', require: false @@ -195,12 +182,6 @@ group :development, :test do # Interactive Debugging tools gem 'debug', '~> 1.8' - # Generate fake data values - gem 'faker', '~> 3.2' - - # Generate factory objects - gem 'fabrication', '~> 2.30' - # Profiling tools gem 'memory_profiler', require: false gem 'ruby-prof', require: false @@ -215,14 +196,12 @@ group :production do gem 'lograge', '~> 0.12' end -gem 'cocoon', '~> 1.2' gem 'concurrent-ruby', require: false gem 'connection_pool', require: false gem 'xorcist', '~> 1.1' +gem 'cocoon', '~> 1.2' gem 'net-http', '~> 0.4.0' gem 'rubyzip', '~> 2.3' gem 'hcaptcha', '~> 7.1' - -gem 'mail', '~> 2.8' diff --git a/Gemfile.lock b/Gemfile.lock index 6001c73db4..e9c62eb115 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,38 +7,49 @@ GIT hkdf (~> 0.2) jwt (~> 2.0) +GIT + remote: https://github.com/jhawthorn/nsa.git + revision: e020fcc3a54d993ab45b7194d89ab720296c111b + ref: e020fcc3a54d993ab45b7194d89ab720296c111b + specs: + nsa (0.2.8) + activesupport (>= 4.2, < 7.2) + concurrent-ruby (~> 1.0, >= 1.0.2) + sidekiq (>= 3.5) + statsd-ruby (~> 1.4, >= 1.4.0) + GEM remote: https://rubygems.org/ specs: - actioncable (7.1.3.2) - actionpack (= 7.1.3.2) - activesupport (= 7.1.3.2) + actioncable (7.1.2) + actionpack (= 7.1.2) + activesupport (= 7.1.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.1.3.2) - actionpack (= 7.1.3.2) - activejob (= 7.1.3.2) - activerecord (= 7.1.3.2) - activestorage (= 7.1.3.2) - activesupport (= 7.1.3.2) + actionmailbox (7.1.2) + actionpack (= 7.1.2) + activejob (= 7.1.2) + activerecord (= 7.1.2) + activestorage (= 7.1.2) + activesupport (= 7.1.2) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.1.3.2) - actionpack (= 7.1.3.2) - actionview (= 7.1.3.2) - activejob (= 7.1.3.2) - activesupport (= 7.1.3.2) + actionmailer (7.1.2) + actionpack (= 7.1.2) + actionview (= 7.1.2) + activejob (= 7.1.2) + activesupport (= 7.1.2) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.2) - actionpack (7.1.3.2) - actionview (= 7.1.3.2) - activesupport (= 7.1.3.2) + actionpack (7.1.2) + actionview (= 7.1.2) + activesupport (= 7.1.2) nokogiri (>= 1.8.5) racc rack (>= 2.2.4) @@ -46,15 +57,15 @@ GEM rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - actiontext (7.1.3.2) - actionpack (= 7.1.3.2) - activerecord (= 7.1.3.2) - activestorage (= 7.1.3.2) - activesupport (= 7.1.3.2) + actiontext (7.1.2) + actionpack (= 7.1.2) + activerecord (= 7.1.2) + activestorage (= 7.1.2) + activesupport (= 7.1.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.1.3.2) - activesupport (= 7.1.3.2) + actionview (7.1.2) + activesupport (= 7.1.2) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) @@ -64,22 +75,22 @@ GEM activemodel (>= 4.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (7.1.3.2) - activesupport (= 7.1.3.2) + activejob (7.1.2) + activesupport (= 7.1.2) globalid (>= 0.3.6) - activemodel (7.1.3.2) - activesupport (= 7.1.3.2) - activerecord (7.1.3.2) - activemodel (= 7.1.3.2) - activesupport (= 7.1.3.2) + activemodel (7.1.2) + activesupport (= 7.1.2) + activerecord (7.1.2) + activemodel (= 7.1.2) + activesupport (= 7.1.2) timeout (>= 0.4.0) - activestorage (7.1.3.2) - actionpack (= 7.1.3.2) - activejob (= 7.1.3.2) - activerecord (= 7.1.3.2) - activesupport (= 7.1.3.2) + activestorage (7.1.2) + actionpack (= 7.1.2) + activejob (= 7.1.2) + activerecord (= 7.1.2) + activesupport (= 7.1.2) marcel (~> 1.0) - activesupport (7.1.3.2) + activesupport (7.1.2) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) @@ -97,20 +108,22 @@ GEM activerecord (>= 3.2, < 8.0) rake (>= 10.4, < 14.0) ast (2.4.2) - attr_required (1.0.2) + attr_encrypted (4.0.0) + encryptor (~> 3.0.0) + attr_required (1.0.1) awrence (1.2.1) aws-eventstream (1.3.0) - aws-partitions (1.929.0) - aws-sdk-core (3.196.1) + 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.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.81.0) - aws-sdk-core (~> 3, >= 3.193.0) + aws-sdk-kms (1.75.0) + aws-sdk-core (~> 3, >= 3.188.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.151.0) - aws-sdk-core (~> 3, >= 3.194.0) + aws-sdk-s3 (1.142.0) + aws-sdk-core (~> 3, >= 3.189.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.8) aws-sigv4 (1.8.0) @@ -130,14 +143,21 @@ GEM erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) - bigdecimal (3.1.8) - bindata (2.5.0) - binding_of_caller (1.0.1) - debug_inspector (>= 1.2.0) + better_html (2.0.2) + actionview (>= 6.0) + activesupport (>= 6.0) + ast (~> 2.0) + erubi (~> 1.4) + parser (>= 2.4) + smart_properties + bigdecimal (3.1.5) + bindata (2.4.15) + binding_of_caller (1.0.0) + debug_inspector (>= 0.0.1) blurhash (0.1.7) - bootsnap (1.18.3) + bootsnap (1.17.0) msgpack (~> 1.2) - brakeman (6.1.2) + brakeman (6.1.1) racc browser (5.3.1) brpoplpush-redis_script (0.1.3) @@ -147,91 +167,97 @@ GEM bundler-audit (0.9.1) bundler (>= 1.2.0, < 3) thor (~> 1.0) - capybara (3.40.0) + capybara (3.39.2) addressable matrix mini_mime (>= 0.1.3) - nokogiri (~> 1.11) + nokogiri (~> 1.8) rack (>= 1.6.0) rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) case_transform (0.2) activesupport - cbor (0.5.9.8) + cbor (0.5.9.6) charlock_holmes (0.7.7) - chewy (7.6.0) + chewy (7.4.0) activesupport (>= 5.2) - elasticsearch (>= 7.14.0, < 8) + elasticsearch (>= 7.12.0, < 7.14.0) elasticsearch-dsl chunky_png (1.4.0) - climate_control (1.2.0) + climate_control (0.2.0) cocoon (1.2.15) color_diff (0.1) - concurrent-ruby (1.2.3) + concurrent-ruby (1.2.2) connection_pool (2.4.1) cose (1.3.0) cbor (~> 0.5.9) openssl-signature_algorithm (~> 1.0) - crack (1.0.0) - bigdecimal + crack (0.4.5) rexml crass (1.0.6) - css_parser (1.17.1) + css_parser (1.14.0) addressable - csv (3.3.0) + 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.9.2) + debug (1.9.1) irb (~> 1.10) reline (>= 0.3.8) - debug_inspector (1.2.0) - devise (4.9.4) + debug_inspector (1.1.0) + devise (4.9.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-two-factor (5.0.0) + devise-two-factor (4.1.1) activesupport (~> 7.0) + attr_encrypted (>= 1.3, < 5, != 2) devise (~> 4.0) railties (~> 7.0) rotp (~> 6.0) devise_pam_authenticatable2 (9.2.0) devise (>= 4.0.0) rpam2 (~> 4.0) - diff-lcs (1.5.1) + diff-lcs (1.5.0) discard (1.3.0) activerecord (>= 4.2, < 8) docile (1.4.0) - domain_name (0.6.20240107) - doorkeeper (5.6.9) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) + doorkeeper (5.6.8) railties (>= 5) - dotenv (3.1.2) - drb (2.2.1) + dotenv (2.8.1) + dotenv-rails (2.8.1) + dotenv (= 2.8.1) + railties (>= 3.2) + drb (2.2.0) + ruby2_keywords ed25519 (1.3.0) - elasticsearch (7.17.10) - elasticsearch-api (= 7.17.10) - elasticsearch-transport (= 7.17.10) - elasticsearch-api (7.17.10) + elasticsearch (7.13.3) + elasticsearch-api (= 7.13.3) + elasticsearch-transport (= 7.13.3) + elasticsearch-api (7.13.3) multi_json elasticsearch-dsl (0.1.10) - elasticsearch-transport (7.17.10) - faraday (>= 1, < 3) + elasticsearch-transport (7.13.3) + faraday (~> 1) multi_json email_spec (2.2.2) htmlentities (~> 4.3.3) launchy (~> 2.1) mail (~> 2.7) + encryptor (3.0.0) erubi (1.12.0) - et-orbi (1.2.11) + et-orbi (1.2.7) tzinfo - excon (0.110.0) + excon (0.109.0) fabrication (2.31.0) - faker (3.3.1) + faker (3.2.2) i18n (>= 1.8.11, < 2) faraday (1.10.3) faraday-em_http (~> 1.0) @@ -259,10 +285,10 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fast_blank (1.0.1) - fastimage (2.3.1) - ffi (1.16.3) - ffi-compiler (1.3.2) - ffi (>= 1.15.5) + fastimage (2.3.0) + ffi (1.15.5) + ffi-compiler (1.0.1) + ffi (>= 1.0.0) rake fog-core (2.4.0) builder @@ -272,11 +298,11 @@ GEM fog-json (1.2.0) fog-core multi_json (~> 1.10) - fog-openstack (1.1.1) + fog-openstack (1.1.0) fog-core (~> 2.1) fog-json (>= 1.0) formatador (1.1.0) - fugit (1.10.1) + fugit (1.8.1) et-orbi (~> 1, >= 1.2.7) raabro (~> 1.4) fuubar (2.5.1) @@ -284,9 +310,6 @@ GEM ruby-progressbar (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) - google-protobuf (3.25.3) - googleapis-common-protos-types (1.14.0) - google-protobuf (~> 3.18) haml (6.3.0) temple (>= 0.8.2) thor @@ -296,39 +319,39 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) - haml_lint (0.58.0) + haml_lint (0.53.0) haml (>= 5.0) parallel (~> 1.10) rainbow rubocop (>= 1.0) sysexits (~> 1.1) - hashdiff (1.1.0) + hashdiff (1.0.1) hashie (5.0.0) hcaptcha (7.1.0) json - highline (3.0.1) + highline (2.1.0) hiredis (0.6.3) hkdf (0.3.0) htmlentities (4.3.4) - http (5.2.0) + http (5.1.1) addressable (~> 2.8) - base64 (~> 0.1) http-cookie (~> 1.0) http-form_data (~> 2.2) - llhttp-ffi (~> 0.5.0) + llhttp-ffi (~> 0.4.0) http-cookie (1.0.5) domain_name (~> 0.5) http-form_data (2.3.0) http_accept_language (2.1.1) httpclient (2.8.3) - httplog (1.6.3) + httplog (1.6.2) rack (>= 2.0) rainbow (>= 2.0.0) - i18n (1.14.5) + i18n (1.14.1) concurrent-ruby (~> 1.0) - i18n-tasks (1.0.14) + i18n-tasks (1.0.13) activesupport (>= 4.0.2) ast (>= 2.1.0) + better_html (>= 1.0, < 3.0) erubi highline (>= 2.0.0) i18n @@ -337,17 +360,14 @@ GEM rainbow (>= 2.2.2, < 4.0) terminal-table (>= 1.5.1) idn-ruby (0.1.5) - inline_svg (1.9.0) - activesupport (>= 3.0) - nokogiri (>= 1.6) - io-console (0.7.2) - irb (1.13.1) - rdoc (>= 4.0.0) + io-console (0.7.1) + irb (1.11.1) + rdoc reline (>= 0.4.2) jmespath (1.6.2) - json (2.7.2) + json (2.7.1) json-canonicalization (1.0.0) - json-jwt (1.15.3.1) + json-jwt (1.15.3) activesupport (>= 4.2) aes_key_wrap bindata @@ -362,7 +382,7 @@ GEM json-ld-preloaded (3.3.0) json-ld (~> 3.3) rdf (~> 3.3) - json-schema (4.3.0) + json-schema (4.1.1) addressable (>= 2.8) jsonapi-renderer (0.2.2) jwt (2.7.1) @@ -378,24 +398,24 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) - kt-paperclip (7.2.2) + kt-paperclip (7.2.1) activemodel (>= 4.2.0) activesupport (>= 4.2.0) marcel (~> 1.0.1) mime-types - terrapin (>= 0.6.0, < 2.0) + terrapin (~> 0.6.0) language_server-protocol (3.17.0.3) launchy (2.5.2) addressable (~> 2.8) - letter_opener (1.10.0) - launchy (>= 2.2, < 4) - letter_opener_web (3.0.0) - actionmailer (>= 6.1) - letter_opener (~> 1.9) - railties (>= 6.1) + letter_opener (1.8.1) + launchy (>= 2.2, < 3) + letter_opener_web (2.0.0) + actionmailer (>= 5.2) + letter_opener (~> 1.7) + railties (>= 5.2) rexml link_header (0.0.8) - llhttp-ffi (0.5.0) + llhttp-ffi (0.4.0) ffi-compiler (~> 1.0) rake (~> 13.0) lograge (0.14.0) @@ -411,7 +431,7 @@ GEM net-imap net-pop net-smtp - marcel (1.0.4) + marcel (1.0.2) mario-redis-lock (1.2.1) redis (>= 3.0.5) matrix (0.4.2) @@ -422,19 +442,19 @@ GEM memory_profiler (1.0.1) mime-types (3.5.2) mime-types-data (~> 3.2015) - mime-types-data (3.2024.0507) + mime-types-data (3.2023.1205) mini_mime (1.1.5) - mini_portile2 (2.8.6) - minitest (5.22.3) + mini_portile2 (2.8.5) + minitest (5.20.0) msgpack (1.7.2) multi_json (1.15.0) - multipart-post (2.4.0) + multipart-post (2.3.0) mutex_m (0.2.0) net-http (0.4.1) uri net-http-persistent (4.0.2) connection_pool (~> 2.2) - net-imap (0.4.11) + net-imap (0.4.4) date net-protocol net-ldap (0.19.0) @@ -442,24 +462,19 @@ GEM net-protocol net-protocol (0.2.2) timeout - net-smtp (0.5.0) + net-smtp (0.4.0) net-protocol - nio4r (2.7.1) - nokogiri (1.16.5) + nio4r (2.5.9) + nokogiri (1.16.0) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nsa (0.3.0) - activesupport (>= 4.2, < 7.2) - concurrent-ruby (~> 1.0, >= 1.0.2) - sidekiq (>= 3.5) - statsd-ruby (~> 1.4, >= 1.4.0) oj (3.16.3) bigdecimal (>= 3.0) - omniauth (2.1.2) + omniauth (2.1.1) hashie (>= 3.4.6) rack (>= 2.2.3) rack-protection - omniauth-cas (3.0.0) + omniauth-cas (3.0.0.beta.1) addressable (~> 2.8) nokogiri (~> 1.12) omniauth (~> 2.1) @@ -486,109 +501,20 @@ GEM openssl (3.2.0) openssl-signature_algorithm (1.3.0) openssl (> 2.0) - opentelemetry-api (1.2.5) - opentelemetry-common (0.20.1) - opentelemetry-api (~> 1.0) - opentelemetry-exporter-otlp (0.26.3) - google-protobuf (~> 3.14) - googleapis-common-protos-types (~> 1.3) - opentelemetry-api (~> 1.1) - opentelemetry-common (~> 0.20) - opentelemetry-sdk (~> 1.2) - opentelemetry-semantic_conventions - opentelemetry-helpers-sql-obfuscation (0.1.0) - opentelemetry-common (~> 0.20) - opentelemetry-instrumentation-action_pack (0.9.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-rack (~> 0.21) - opentelemetry-instrumentation-action_view (0.7.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-active_support (~> 0.1) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-active_job (0.7.1) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-active_model_serializers (0.20.1) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-active_record (0.7.2) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-active_support (0.5.1) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-base (0.22.3) - opentelemetry-api (~> 1.0) - opentelemetry-registry (~> 0.1) - opentelemetry-instrumentation-concurrent_ruby (0.21.3) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-excon (0.22.1) - opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-faraday (0.24.2) - opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-http (0.23.3) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-http_client (0.22.4) - opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-net_http (0.22.4) - opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-pg (0.27.3) - opentelemetry-api (~> 1.0) - opentelemetry-helpers-sql-obfuscation - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-rack (0.24.3) - opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-rails (0.30.1) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-action_pack (~> 0.9.0) - opentelemetry-instrumentation-action_view (~> 0.7.0) - opentelemetry-instrumentation-active_job (~> 0.7.0) - opentelemetry-instrumentation-active_record (~> 0.7.0) - opentelemetry-instrumentation-active_support (~> 0.5.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-redis (0.25.4) - opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-sidekiq (0.25.3) - opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-registry (0.3.1) - opentelemetry-api (~> 1.1) - opentelemetry-sdk (1.4.1) - opentelemetry-api (~> 1.1) - opentelemetry-common (~> 0.20) - opentelemetry-registry (~> 0.2) - opentelemetry-semantic_conventions - opentelemetry-semantic_conventions (1.10.0) - opentelemetry-api (~> 1.0) orm_adapter (0.5.0) - ox (2.14.18) + ox (2.14.17) parallel (1.24.0) - parser (3.3.1.0) + parser (3.2.2.4) ast (~> 2.4.1) racc parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) - pg (1.5.6) - pghero (3.4.1) + pg (1.5.4) + pghero (3.4.0) activerecord (>= 6) - premailer (1.23.0) + posix-spawn (0.3.15) + premailer (1.21.0) addressable css_parser (>= 1.12.0) htmlentities (>= 4.0.0) @@ -604,17 +530,17 @@ GEM railties (>= 7.0.0) psych (5.1.2) stringio - public_suffix (5.0.5) + public_suffix (5.0.4) puma (6.4.2) nio4r (~> 2.0) - pundit (2.3.2) + pundit (2.3.1) activesupport (>= 3.0.0) raabro (1.4.0) racc (1.7.3) - rack (2.2.9) + rack (2.2.8) rack-attack (6.7.0) rack (>= 1.0, < 4) - rack-cors (2.0.2) + rack-cors (2.0.1) rack (>= 2.0.0) rack-oauth2 (1.21.3) activesupport @@ -622,32 +548,31 @@ GEM httpclient json-jwt (>= 1.11.0) rack (>= 2.1.0) - rack-protection (3.2.0) - base64 (>= 0.1.0) - rack (~> 2.2, >= 2.2.4) - rack-proxy (0.7.7) + rack-protection (3.0.5) rack - rack-session (1.0.2) + rack-proxy (0.7.6) + rack + rack-session (1.0.1) rack (< 3) rack-test (2.1.0) rack (>= 1.3) rackup (1.0.0) rack (< 3) webrick - rails (7.1.3.2) - actioncable (= 7.1.3.2) - actionmailbox (= 7.1.3.2) - actionmailer (= 7.1.3.2) - actionpack (= 7.1.3.2) - actiontext (= 7.1.3.2) - actionview (= 7.1.3.2) - activejob (= 7.1.3.2) - activemodel (= 7.1.3.2) - activerecord (= 7.1.3.2) - activestorage (= 7.1.3.2) - activesupport (= 7.1.3.2) + 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) bundler (>= 1.15.0) - railties (= 7.1.3.2) + railties (= 7.1.2) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -659,25 +584,25 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - rails-i18n (7.0.9) + rails-i18n (7.0.8) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - railties (7.1.3.2) - actionpack (= 7.1.3.2) - activesupport (= 7.1.3.2) + railties (7.1.2) + actionpack (= 7.1.2) + activesupport (= 7.1.2) irb rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.2.1) + rake (13.1.0) rdf (3.3.1) bcp47_spec (~> 0.2) link_header (~> 0.0, >= 0.0.8) - rdf-normalize (0.7.0) - rdf (~> 3.3) - rdoc (6.6.3.1) + rdf-normalize (0.6.1) + rdf (~> 3.2) + rdoc (6.6.2) psych (>= 4.0.0) redcarpet (3.6.0) redis (4.8.1) @@ -685,82 +610,78 @@ GEM redis (>= 4) redlock (1.3.2) redis (>= 3.0.0, < 6.0) - regexp_parser (2.9.0) - reline (0.5.7) + regexp_parser (2.8.3) + reline (0.4.2) io-console (~> 0.5) - request_store (1.6.0) + request_store (1.5.1) rack (>= 1.4) responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) - rexml (3.2.8) - strscan (>= 3.0.9) + rexml (3.2.6) rotp (6.3.0) - rouge (4.2.1) + rouge (4.1.2) rpam2 (4.0.2) rqrcode (2.2.0) chunky_png (~> 1.0) rqrcode_core (~> 1.0) rqrcode_core (1.2.0) - rspec-core (3.13.0) - rspec-support (~> 3.13.0) - rspec-expectations (3.13.0) + rspec-core (3.12.2) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.13.0) + rspec-support (~> 3.12.0) rspec-github (2.4.0) rspec-core (~> 3.0) - rspec-mocks (3.13.1) + rspec-mocks (3.12.6) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.13.0) - rspec-rails (6.1.2) + rspec-support (~> 3.12.0) + rspec-rails (6.1.0) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) - rspec-core (~> 3.13) - rspec-expectations (~> 3.13) - rspec-mocks (~> 3.13) - rspec-support (~> 3.13) - rspec-sidekiq (5.0.0) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) + rspec-sidekiq (4.1.0) rspec-core (~> 3.0) rspec-expectations (~> 3.0) rspec-mocks (~> 3.0) sidekiq (>= 5, < 8) - rspec-support (3.13.1) - rubocop (1.63.5) + rspec-support (3.12.1) + rubocop (1.59.0) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.3.0.2) + parser (>= 3.2.2.4) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.31.1, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.31.3) - parser (>= 3.3.1.0) + rubocop-ast (1.30.0) + parser (>= 3.2.1.0) rubocop-capybara (2.20.0) rubocop (~> 1.41) - rubocop-factory_bot (2.25.1) - rubocop (~> 1.41) - rubocop-performance (1.21.0) + rubocop-factory_bot (2.25.0) + rubocop (~> 1.33) + rubocop-performance (1.20.2) rubocop (>= 1.48.1, < 2.0) - rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rails (2.24.1) + 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.31.1, < 2.0) - rubocop-rspec (2.29.2) + rubocop-ast (>= 1.30.0, < 2.0) + rubocop-rspec (2.26.1) rubocop (~> 1.40) rubocop-capybara (~> 2.17) rubocop-factory_bot (~> 2.22) - rubocop-rspec_rails (~> 2.28) - rubocop-rspec_rails (2.28.3) - rubocop (~> 1.40) ruby-prof (1.7.0) ruby-progressbar (1.13.0) - ruby-saml (1.16.0) + ruby-saml (1.15.0) nokogiri (>= 1.13.10) rexml ruby2_keywords (0.0.5) @@ -772,11 +693,10 @@ GEM sanitize (6.1.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) - scenic (1.8.0) + scenic (1.7.0) activerecord (>= 4.0.0) railties (>= 4.0.0) - selenium-webdriver (4.21.0) - base64 (~> 0.2) + selenium-webdriver (4.16.0) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) @@ -791,7 +711,7 @@ GEM rufus-scheduler (~> 3.2) sidekiq (>= 6, < 8) tilt (>= 1.4.0) - sidekiq-unique-jobs (7.1.33) + 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) @@ -809,14 +729,14 @@ GEM simplecov-html (0.12.3) simplecov-lcov (0.8.0) simplecov_json_formatter (0.1.4) - stackprof (0.2.26) + smart_properties (1.17.0) + stackprof (0.2.25) statsd-ruby (1.5.0) - stoplight (4.1.0) + stoplight (3.0.2) redlock (~> 1.0) stringio (3.1.0) - strong_migrations (1.8.0) + strong_migrations (1.7.0) activerecord (>= 5.2) - strscan (3.1.0) swd (1.3.0) activesupport (>= 3) attr_required (>= 0.0.5) @@ -825,10 +745,10 @@ GEM temple (0.10.3) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) - terrapin (1.0.1) - climate_control - test-prof (1.3.3) - thor (1.3.1) + terrapin (0.6.0) + climate_control (>= 0.0.3, < 1.0) + test-prof (1.3.1) + thor (1.3.0) tilt (2.3.0) timeout (0.4.1) tpm-key_attestation (0.12.0) @@ -844,19 +764,19 @@ GEM tty-cursor (~> 0.7) tty-screen (~> 0.8) wisper (~> 2.0) - tty-screen (0.8.2) + tty-screen (0.8.1) twitter-text (3.1.0) idn-ruby unf (~> 0.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - tzinfo-data (1.2024.1) + tzinfo-data (1.2023.4) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext - unf_ext (0.0.9.1) + unf_ext (0.0.8.2) unicode-display_width (2.5.0) - uri (0.13.0) + uri (0.12.2) validate_email (0.1.6) activemodel (>= 3.0) mail (>= 2.2.5) @@ -877,7 +797,7 @@ GEM webfinger (1.2.0) activesupport httpclient (>= 2.4) - webmock (3.23.0) + webmock (3.19.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -895,7 +815,7 @@ GEM xorcist (1.1.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.14) + zeitwerk (2.6.12) PLATFORMS ruby @@ -908,14 +828,14 @@ DEPENDENCIES better_errors (~> 2.9) binding_of_caller (~> 1.0) blurhash (~> 0.1) - bootsnap (~> 1.18.0) + bootsnap (~> 1.17.0) brakeman (~> 6.0) browser bundler-audit (~> 0.9) capybara (~> 3.39) charlock_holmes (~> 0.7.7) chewy (~> 7.3) - climate_control + climate_control (~> 0.2) cocoon (~> 1.2) color_diff (~> 0.1) concurrent-ruby @@ -924,11 +844,11 @@ DEPENDENCIES database_cleaner-active_record debug (~> 1.8) devise (~> 4.9) - devise-two-factor + devise-two-factor (~> 4.1) devise_pam_authenticatable2 (~> 9.2) discard (~> 1.2) doorkeeper (~> 5.6) - dotenv + dotenv-rails (~> 2.8) ed25519 (~> 1.3) email_spec fabrication (~> 2.30) @@ -943,13 +863,11 @@ DEPENDENCIES hcaptcha (~> 7.1) hiredis (~> 0.6) htmlentities (~> 4.3) - http (~> 5.2.0) + http (~> 5.1) http_accept_language (~> 2.1) httplog (~> 1.6.2) - i18n i18n-tasks (~> 1.0) idn-ruby - inline_svg irb (~> 1.8) json-ld json-ld-preloaded (~> 3.2) @@ -957,10 +875,9 @@ DEPENDENCIES kaminari (~> 1.2) kt-paperclip (~> 7.2) letter_opener (~> 1.8) - letter_opener_web (~> 3.0) + letter_opener_web (~> 2.0) link_header (~> 0.0) lograge (~> 0.12) - mail (~> 2.8) mario-redis-lock (~> 1.2) md-paperclip-azure (~> 2.2) memory_profiler @@ -968,32 +885,18 @@ DEPENDENCIES net-http (~> 0.4.0) net-ldap (~> 0.18) nokogiri (~> 1.15) - nsa + nsa! oj (~> 3.14) omniauth (~> 2.0) omniauth-cas (~> 3.0.0.beta.1) omniauth-rails_csrf_protection (~> 1.0) omniauth-saml (~> 2.0) omniauth_openid_connect (~> 0.6.1) - opentelemetry-exporter-otlp (~> 0.26.3) - opentelemetry-instrumentation-active_job (~> 0.7.1) - opentelemetry-instrumentation-active_model_serializers (~> 0.20.1) - opentelemetry-instrumentation-concurrent_ruby (~> 0.21.2) - opentelemetry-instrumentation-excon (~> 0.22.0) - opentelemetry-instrumentation-faraday (~> 0.24.1) - opentelemetry-instrumentation-http (~> 0.23.2) - opentelemetry-instrumentation-http_client (~> 0.22.3) - opentelemetry-instrumentation-net_http (~> 0.22.4) - opentelemetry-instrumentation-pg (~> 0.27.1) - opentelemetry-instrumentation-rack (~> 0.24.1) - opentelemetry-instrumentation-rails (~> 0.30.0) - opentelemetry-instrumentation-redis (~> 0.25.3) - opentelemetry-instrumentation-sidekiq (~> 0.25.2) - opentelemetry-sdk (~> 1.4) ox (~> 2.14) parslet pg (~> 1.5) pghero + posix-spawn premailer-rails private_address_check (~> 0.5) propshaft @@ -1014,7 +917,7 @@ DEPENDENCIES rqrcode (~> 2.2) rspec-github (~> 2.4) rspec-rails (~> 6.0) - rspec-sidekiq (~> 5.0) + rspec-sidekiq (~> 4.0) rubocop rubocop-capybara rubocop-performance @@ -1035,8 +938,8 @@ DEPENDENCIES simplecov (~> 0.22) simplecov-lcov (~> 0.8) stackprof - stoplight (~> 4.1) - strong_migrations (= 1.8.0) + stoplight (~> 3.0.1) + strong_migrations (= 1.7.0) test-prof thor (~> 1.2) tty-prompt (~> 0.23) @@ -1049,7 +952,7 @@ DEPENDENCIES xorcist (~> 1.1) RUBY VERSION - ruby 3.3.1p55 + ruby 3.2.2p53 BUNDLED WITH - 2.5.9 + 2.4.20 diff --git a/README.md b/README.md index e438d0bd7b..267f0ed295 100644 --- a/README.md +++ b/README.md @@ -1,112 +1,142 @@ -# ![kmyblue icon](https://raw.githubusercontent.com/kmycode/mastodon/kb_development/app/javascript/icons/favicon-32x32.png) kmyblue +

+ + + Mastodon +

-[![Ruby Testing](https://github.com/kmycode/mastodon/actions/workflows/test-ruby.yml/badge.svg)](https://github.com/kmycode/mastodon/actions/workflows/test-ruby.yml) +[![GitHub release](https://img.shields.io/github/release/mastodon/mastodon.svg)][releases] +[![Ruby Testing](https://github.com/mastodon/mastodon/actions/workflows/test-ruby.yml/badge.svg)](https://github.com/mastodon/mastodon/actions/workflows/test-ruby.yml) +[![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)][crowdin] -kmyblueは[Mastodon](https://github.com/mastodon/mastodon)のフォークです。創作作家のためのMastodonを目指して開発しました。 +[releases]: https://github.com/mastodon/mastodon/releases +[crowdin]: https://crowdin.com/project/mastodon -kmyblueはフォーク名であり、同時に[サーバー名](https://kmy.blue)でもあります。以下は特に記述がない限り、フォークとしてのkmyblueをさします。 +Mastodon is a **free, open-source social network server** based on ActivityPub where users can follow friends and discover new ones. On Mastodon, users can publish anything they want: links, pictures, text, and video. All Mastodon servers are interoperable as a federated network (users on one server can seamlessly communicate with users from another one, including non-Mastodon software that implements ActivityPub!) -kmyblueは AGPL ライセンスで公開されているため、どなたでも自由にフォークし、このソースコードを元に自分でサーバーを立てて公開することができます。確かにサーバーkmyblueは創作作家向けのものですが、フォークとしてのkmyblueはAGPLでライセンスつけられており、ルールは全くの別物です。創作活動の一部(エロ関係含む)または全体を否定するコミュニティなどにも平等にお使いいただけます。サーバーkmyblueのルールを適用する必要もなく、「Anyone But Kmyblue」なルールを設定することすら許容されます。 -kmyblueは、特に非収載投稿の検索が強化されているため、ローカルタイムラインに掲載されていない投稿も検索・購読することが可能な場合があります。閉鎖的なコミュニティ、あまり目立ちたくないコミュニティには特に強力な機能を提供します。それ以外のコミュニティに対しても、kmyblueはプライバシーを考慮したうえで強力な検索・購読機能を提供するため、汎用サーバーとして利用するにもある程度十分な機能が揃っています。 +Click below to **learn more** in a video: -ただしkmyblueにおいて**テストコードは飾り**でしかありません。これはkmyblueを利用する人が本家Mastodonより圧倒的に少なく、バグやセキュリティインシデントを発見するだけの人数が足りないことを意味します。kmyblueは対策として自動テストを拡充しています。独自機能のテストを記述するだけでなく、本家のテストコードの補強も行っておりますが、確認漏れは必ず発生するものです。不具合が発生しても自己責任になります。既知のバグもいくつかありますし、直す予定のないものも含まれます。 +[![Screenshot](https://blog.joinmastodon.org/2018/06/why-activitypub-is-the-future/ezgif-2-60f1b00403.gif)][youtube_demo] -テストコード、Lint どちらも動いています。 +[youtube_demo]: https://www.youtube.com/watch?v=IPSbNdBmWKE -## インストール方法 +## Navigation -[Wiki](https://github.com/kmycode/mastodon/wiki/Installation)を参照してください。 +- [Project homepage 🐘](https://joinmastodon.org) +- [Support the development via Patreon][patreon] +- [View sponsors](https://joinmastodon.org/sponsors) +- [Blog](https://blog.joinmastodon.org) +- [Documentation](https://docs.joinmastodon.org) +- [Roadmap](https://joinmastodon.org/roadmap) +- [Official Docker image](https://github.com/mastodon/mastodon/pkgs/container/mastodon) +- [Browse Mastodon servers](https://joinmastodon.org/communities) +- [Browse Mastodon apps](https://joinmastodon.org/apps) -## 開発への参加方法 +[patreon]: https://www.patreon.com/mastodon -CONTRIBUTING.mdを参照してください。 +## Features -## テスト + -``` -# デバッグ実行(以下のいずれか) -foreman start -DB_USER=postgres DB_PASS=password foreman start +### No vendor lock-in: Fully interoperable with any conforming platform -# 一部を除く全てのテストを行う -RAILS_ENV=test bundle exec rspec spec +It doesn't have to be Mastodon; whatever implements ActivityPub is part of the social network! [Learn more](https://blog.joinmastodon.org/2018/06/why-activitypub-is-the-future/) -# ElasticSearch連携テストを行う -新 -RAILS_ENV=test ES_ENABLED=true bundle exec rspec --tag search -旧 -RAILS_ENV=test ES_ENABLED=true RUN_SEARCH_SPECS=true bundle exec rspec spec/search -``` +### Real-time, chronological timeline updates -## kmyblueのブランチ +Updates of people you're following appear in real-time in the UI via WebSockets. There's a firehose view as well! -- **main** - 管理者が本家MastodonにPRするときに使うことがあります -- **kb_development** - 開発中の最新のソースコードです。メジャーバージョンアップデートは通常このブランチから公開されます -- **kb_lts** - LTSの管理に使います。LTSはこのブランチから公開されます -- **kb_patch** - 修正パッチの管理に使います。マイナーバージョンアップデートは通常このブランチから公開されます +### Media attachments like images and short videos -## kmyblueの強み +Upload and view images and WebM/MP4 videos attached to the updates. Videos with no audio track are treated like GIFs; normal videos loop continuously! -追加の詳細は下記記事もご覧ください。 +### Safety and moderation tools -https://note.com/kmycode/n/n5fd5e823ed40 +Mastodon includes private posts, locked accounts, phrase filtering, muting, blocking, and all sorts of other features, along with a reporting and moderation system. [Learn more](https://blog.joinmastodon.org/2018/07/cage-the-mastodon/) -以下に書いているもの以外にも多数の機能が存在します。 +### OAuth2 and a straightforward REST API -### 本家Mastodonへの積極的追従 +Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Streaming APIs. This results in a rich app ecosystem with a lot of choices! -kmyblueは、追加機能を控えめにする代わりに本家Mastodonに積極的に追従を行います。kmyblueの追加機能そのままに、Mastodonの新機能も利用できるよう調整を行います。 +## Deployment -### ゆるやかな内輪での運用 +### Tech stack -kmyblueは同人向けサーバーとして出発したため、同人作家に需要のある「内輪ノリを外部にできるだけもらさない」という部分に特化しています。 +- **Ruby on Rails** powers the REST API and other web pages +- **React.js** and Redux are used for the dynamic parts of the interface +- **Node.js** powers the streaming API -「ローカル公開」という機能によって、「ローカルタイムラインに流すが他のサーバーの連合タイムラインに流さない」投稿が可能です。ただしMisskeyのローカル限定とは異なり、他のサーバーのフォロワーのタイムラインにも投稿は流れます。自分のサーバーの中で内輪で盛り上がって、他のサーバーの連合タイムラインには外面だけの投稿を流すことも可能です。 +### Requirements -「サークル」という機能によって、特定のフォロワーにだけ見える投稿を行うことも可能です。その投稿に返信することで、相手サークルの会話に参加することも可能です。ただしサークル投稿を正常に処理できるソフトウェアは現在、kmyblue・Fedibirdに限ります。 +- **PostgreSQL** 12+ +- **Redis** 4+ +- **Ruby** 2.7+ +- **Node.js** 16+ -また、通常のMastodonでは公開投稿を他のサーバーの人に自由に検索できるようにすることも可能ですが、kmyblueでは非収載投稿に対して同様の設定が可能です。つまり、ローカルタイムラインにも連合タイムラインにも流れない、誰かの目に自然に触れることはない、でも特定キーワードを使った検索では引っかかりたい、そのような需要に対応できます。 +The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). The [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation. -内輪とは自分のサーバーに限ったものではありません。内輪同士で複数のサーバーを運営するとき、お互いが深く繋がれるフレンドサーバーというシステムも用意しています。 +## Development -ただしkmyblueは、同時に連合も重視しています。ローカル限定投稿など、連合を大きく制限させるような機能は作る予定はありません。 +### Vagrant -### 少人数サーバーでの運用 +A **Vagrant** configuration is included for development purposes. To use it, complete the following steps: -kmyblueは、人の少ないサーバーでの運用を考慮して設計しています。そのため、他のサーバーのアカウントの購読機能はFedibirdほど発達していませんし、人の多いサーバー向けの独自改造もほとんど存在しません。 +- Install Vagrant and Virtualbox +- Install the `vagrant-hostsupdater` plugin: `vagrant plugin install vagrant-hostsupdater` +- Run `vagrant up` +- Run `vagrant ssh -c "cd /vagrant && foreman start"` +- Open `http://mastodon.local` in your browser -ただしサーバーの負荷については一部度外視している部分があります。たとえば絵文字リアクション機能はサーバーへ著しい負荷をかける場合があります。絵文字リアクション機能そのものを無効にする管理者オプションも存在します。 +### MacOS -もちろん人の多いサーバーでの運用が不便になるような修正は行っていません。人の多いサーバーでもそのままお使いいただけます。 +To set up **MacOS** for native development, complete the following steps: -### 比較的高い防御力 +- Install the latest stable Ruby version (use a Ruby version manager for easy installation and management of Ruby versions) +- 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` +- Run `bundle exec rails db:setup` (optionally prepend `RAILS_ENV=development` to target the dev environment) +- Finally, run `overmind start -f Procfile.dev` -kmyblueでは、「Fediverseは将来的に荒むのではないか」「Fediverseは将来的にスパムに溢れるのではないか」を念頭に設計している部分があります。 +### Docker -個別ユーザー向けの設定項目が複数あります。 +For development with **Docker**, complete the following steps: -- Misskeyは、たとえMastodonの投稿であっても非収載投稿を自由に検索できますが、kmyblueではそれをブロックできるユーザー設定が存在します -- 他の人からの絵文字リアクションの受け入れを制限する設定も可能であり、例えば他のサーバーから好ましくない絵文字リアクションを受け取ることを防止できます -- 公開タイムラインの引用表示はデフォルトで無効になっています。不快な投稿を引用したものが公開タイムラインに流れても、ある程度は防止できます - - フィルター(ワードミュート)は、引用された投稿の内容にも適用されます。この場合、引用投稿そのものが表示されなくなります -- 自分のフォローしている相手の投稿をフィルターから除外する設定が存在します。防御を上げすぎると不便な箇所が出てくるので、そちらも緩和できるよう可能な限り配慮しています +- Install Docker Desktop +- Run `docker compose -f .devcontainer/docker-compose.yml up -d` +- Run `docker compose -f .devcontainer/docker-compose.yml exec app .devcontainer/post-create.sh` +- Finally, run `docker compose -f .devcontainer/docker-compose.yml exec app foreman start -f Procfile.dev` -管理者向けには、スパムへの利用を前提とした正規表現可能なNGワード設定、細かい指定が可能な拡張ドメインブロック機能を用意しています。 +If you are using an IDE with [support for the Development Container specification](https://containers.dev/supporting), it will run the above `docker compose` commands automatically. For **Visual Studio Code** this requires the [Dev Container extension](https://containers.dev/supporting#dev-containers). -ただし防御力の高さは自由を犠牲にします。例えばkmyblueは、絵文字リアクションの表示サイズ調整機能など、MisskeyやFedibirdには当たり前のようにある表示設定は存在しません。騒がしくなるようなものはあまり作りたいとは考えていません。 +### GitHub Codespaces -### その他の主な機能 +To get you coding in just a few minutes, GitHub Codespaces provides a web-based version of Visual Studio Code and a cloud-hosted development environment fully configured with the software needed for this project.. -- 絵文字リアクションによる手軽な交流 -- 絵文字デッキによる頻繁に使用する絵文字の登録・選択 -- 検索機能の強化(検索許可) -- 投稿の引用 -- ブックマークの分類 +- Click this button to create a new codespace:
+ [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=52281283&devcontainer_path=.devcontainer%2Fcodespaces%2Fdevcontainer.json) +- Wait for the environment to build. This will take a few minutes. +- When the editor is ready, run `foreman start -f Procfile.dev` in the terminal. +- After a few seconds, a popup will appear with a button labeled _Open in Browser_. This will open Mastodon. +- On the _Ports_ tab, right click on the “stream” row and select _Port visibility_ → _Public_. -## kmyblueは何でないか +## Contributing -kmyblueは、Misskeyではありません。絵文字リアクションなどMisskeyと同様の機能はありますが、根本的にUIの使い勝手が違う他にも、例えばブックマークを分類できてもそれを公開する機能を作っていません。Misskeyは「楽しむ」をコンセプトにしていますが、kmyblueはMastodonの思想を受け継ぎ、炎上や喧騒を避けることのできる落ち着いた場所を目指しています。そのため、思想に合わない機能は実装しないか、大幅に弱体化しています。 +Mastodon is **free, open-source software** licensed under **AGPLv3**. -kmyblueは、Fedibirdではありません。確かにローカルタイムラインを無効にしFedibirdのような運営を可能にする設定は存在します。しかしkmyblueは本家追従を優先する観点からWebで対応する範囲をある程度絞り込んでいるため、Fedibirdにあるような豊富な表示設定は作っていません。絵文字の大きさすら調整することはできません。また、Fedibirdではアカウントの購読機能があります。kmyblueにも同様の機能はあるものの、Fedibirdのように一発ですぐできるようなUIではありません。購読機能は相手のフォローを伴わないため、特に利用者に擬似的なフォロー体験を与えるアカウント購読は、人の少ない小規模サーバーには向いていません。これは、小規模サーバーの運用を想定しているkmyblueがあえて作っていない部分です。 +You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository or submit translations using Crowdin. To get started, take a look at [CONTRIBUTING.md](CONTRIBUTING.md). If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon). -kmyblueは、企業・政府機関向けに開発されたものではありません。そもそも管理者はセキュリティに関する資格や専門知識を有しておらず、高度なセキュリティの求められる機関向けのソフトウェアを制作する能力はありません。kmyblueは確かに本家Mastodonに対して大幅に機能を追加していますが、そもそも個人によるフォークは、開発者が飽きたらそこで終わりというリスクも伴います。高い信頼性・安全性を保証することはできないので、導入の際はご自身で安全を十分に確認してからお使いになることを強くおすすめします。 +**IRC channel**: #mastodon on irc.libera.chat + +## License + +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. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License along with this program. If not, see . diff --git a/SECURITY.md b/SECURITY.md index d5b27adfac..81472b01b4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,25 +1,20 @@ -# セキュリティポリシー +# Security Policy -kmyblueのプログラムにおいてセキュリティインシデントを発見した場合、kmyblueに報告してください。 +If you believe you've identified a security vulnerability in Mastodon (a bug that allows something to happen that shouldn't be possible), you can either: -kmyblueにセキュリティインシデントを報告する場合、以下の手順を踏んでください。 +- open a [Github security issue on the Mastodon project](https://github.com/mastodon/mastodon/security/advisories/new) +- reach us at -- [こちらのリンクから新規インシデントを起票してください](https://github.com/kmycode/mastodon/security/advisories/new) -- メール 、または[@askyq@kmy.blue](https://kmy.blue/@askyq)宛に、**セキュリティインシデントを起票したことだけ**を連絡してください。セキュリティインシデントの内容は、絶対に連絡に含めないでください(リンクくらいなら含めていいかな) +You should _not_ report such issues on public GitHub issues or in other public spaces to give us time to publish a fix for the issue without exposing Mastodon's users to increased risk. -他のkmyblueフォークの利用者の安全のために少しでも時間稼ぎをしなければいけないので、この問題をIssueを含む公開された場所で記述しないでください。 +## Scope -## 範囲 +A "vulnerability in Mastodon" is a vulnerability in the code distributed through our main source code repository on GitHub. Vulnerabilities that are specific to a given installation (e.g. misconfiguration) should be reported to the owner of that installation and not us. -こちらが対応できる範囲は、当リポジトリで公開しているソースコードのみとなります。当リポジトリの依存パッケージ内に問題がある場合は、そちらに報告してください。 +## Supported Versions -もしあなたに専門知識があり、それが本家Mastodon由来の問題であると信じるに足る根拠がある場合、kmyblueではなくMastodonのほうに報告してください。kmyblueに報告されても、Mastodonより先に修正してしまうことでMastodonにセキュリティリスクを発生させる可能性がありますし、本家Mastodonの対応を待つにしてもkmyblueのほうに来てしまったセキュリティインシデントの対応に困ります(本家がなかなか対応してくれない可能性を考えると削除しづらい)。もし間違ってkmyblueに来た場合、kmyblue開発者の責任で振り分けを行います。 - -## サポートするバージョン - -下記以外のバージョンは、セキュリティインシデントを起票されても対応しません。 - -- 最新メジャーバージョン、かつ、最新マイナーバージョン - - 最新メジャーバージョンのサポートは、次のメジャーバージョンが出た時点で終了します -- LTS - - LTSのサポートは、次のLTSが出た時点で終了します(ただし移行期間があってもいいと思ってるので、1〜3ヶ月以内ならセキュリティインシデントの程度に応じて対応する可能性があります) +| Version | Supported | +| ------- | --------- | +| 4.2.x | Yes | +| 4.1.x | Yes | +| < 4.1 | No | diff --git a/Vagrantfile b/Vagrantfile index 8a95e91f36..6f0f511095 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -173,7 +173,6 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| # Otherwise, you can access the site at http://localhost:3000 and http://localhost:4000 , http://localhost:8080 config.vm.network :forwarded_port, guest: 3000, host: 3000 - config.vm.network :forwarded_port, guest: 3035, host: 3035 config.vm.network :forwarded_port, guest: 4000, host: 4000 config.vm.network :forwarded_port, guest: 8080, host: 8080 config.vm.network :forwarded_port, guest: 9200, host: 9200 @@ -189,7 +188,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.post_up_message = <(account) { account.public_following_count }) - field(:followers_count, type: 'long', value: ->(account) { account.public_followers_count }) + field(:following_count, type: 'long') + field(:followers_count, type: 'long') field(:properties, type: 'keyword', value: ->(account) { account.searchable_properties }) field(:last_status_at, type: 'date', value: ->(account) { clamp_date(account.last_status_at || account.created_at) }) - field(:domain, type: 'keyword', value: ->(account) { account.domain || '' }) - field(:display_name, type: 'text', analyzer: ChewyConfig.instance.accounts_analyzers.dig('display_name', 'analyzer')) do - field :edge_ngram, type: 'text', analyzer: ChewyConfig.instance.accounts_analyzers.dig('display_name', 'edge_ngram', 'analyzer'), search_analyzer: ChewyConfig.instance.accounts_analyzers.dig('display_name', 'edge_ngram', 'search_analyzer') - end - field(:username, type: 'text', analyzer: ChewyConfig.instance.accounts_analyzers.dig('username', 'analyzer'), value: lambda { |account| - [account.username, account.domain].compact.join('@') - }) do - field :edge_ngram, type: 'text', analyzer: ChewyConfig.instance.accounts_analyzers.dig('username', 'edge_ngram', 'analyzer'), - search_analyzer: ChewyConfig.instance.accounts_analyzers.dig('username', 'edge_ngram', 'search_analyzer') - end - field(:text, type: 'text', analyzer: ChewyConfig.instance.accounts_analyzers.dig('text', 'analyzer'), value: ->(account) { account.searchable_text }) { field(:stemmed, type: 'text', analyzer: ChewyConfig.instance.accounts_analyzers.dig('text', 'stemmed', 'analyzer')) } + field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' } + field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' } + field(:text, type: 'text', analyzer: 'verbatim', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' } end end diff --git a/app/chewy/public_statuses_index.rb b/app/chewy/public_statuses_index.rb index b71406d3e3..09a4dfc093 100644 --- a/app/chewy/public_statuses_index.rb +++ b/app/chewy/public_statuses_index.rb @@ -3,22 +3,66 @@ class PublicStatusesIndex < Chewy::Index include DatetimeClampingConcern - # ElasticSearch config is moved to "/config/elasticsearch.default.yml". - # Edit it when original Mastodon changed ElasticSearch config. - settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: ChewyConfig.instance.public_statuses + settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: { + filter: { + english_stop: { + type: 'stop', + stopwords: '_english_', + }, + + english_stemmer: { + type: 'stemmer', + language: 'english', + }, + + english_possessive_stemmer: { + type: 'stemmer', + language: 'possessive_english', + }, + }, + + analyzer: { + verbatim: { + tokenizer: 'uax_url_email', + filter: %w(lowercase), + }, + + content: { + tokenizer: 'standard', + filter: %w( + lowercase + asciifolding + cjk_width + elision + english_possessive_stemmer + english_stop + english_stemmer + ), + }, + + hashtag: { + tokenizer: 'keyword', + filter: %w( + word_delimiter_graph + lowercase + asciifolding + cjk_width + ), + }, + }, + } index_scope ::Status.unscoped .kept .indexable - .includes(:media_attachments, :preloadable_poll, :tags, :account, preview_cards_status: :preview_card) + .includes(:media_attachments, :preloadable_poll, :tags, preview_cards_status: :preview_card) root date_detection: false do field(:id, type: 'long') field(:account_id, type: 'long') - field(:text, type: 'text', analyzer: ChewyConfig.instance.public_statuses_analyzers.dig('text', 'analyzer'), value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: ChewyConfig.instance.public_statuses_analyzers.dig('text', 'stemmed', 'analyzer')) } - field(:tags, type: 'text', analyzer: ChewyConfig.instance.public_statuses_analyzers.dig('tags', 'analyzer'), value: ->(status) { status.tags.map(&:display_name) }) + field(:text, type: 'text', analyzer: 'verbatim', value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: 'content') } + field(:tags, type: 'text', analyzer: 'hashtag', value: ->(status) { status.tags.map(&:display_name) }) field(:language, type: 'keyword') - field(:domain, type: 'keyword', value: ->(status) { status.account.domain || '' }) field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties }) field(:created_at, type: 'date', value: ->(status) { clamp_date(status.created_at) }) end diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb index 44cb86d755..e739ccecb4 100644 --- a/app/chewy/statuses_index.rb +++ b/app/chewy/statuses_index.rb @@ -3,49 +3,64 @@ class StatusesIndex < Chewy::Index include DatetimeClampingConcern - # ElasticSearch config is moved to "/config/elasticsearch.default.yml". - # Edit it when original Mastodon changed ElasticSearch config. - settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: ChewyConfig.instance.statuses + settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: { + filter: { + english_stop: { + type: 'stop', + stopwords: '_english_', + }, - index_scope ::Status.unscoped.kept.without_reblogs.includes( - :account, - :media_attachments, - :local_mentioned, - :local_favorited, - :local_reblogged, - :local_bookmarked, - :local_emoji_reacted, - :tags, - :local_referenced, - preview_cards_status: :preview_card, - preloadable_poll: :local_voters - ), - delete_if: lambda { |status| - if status.searchability == 'direct' - status.searchable_by.empty? - else - status.searchability == 'limited' ? !status.local? : false - end - } + english_stemmer: { + type: 'stemmer', + language: 'english', + }, + + english_possessive_stemmer: { + type: 'stemmer', + language: 'possessive_english', + }, + }, + + analyzer: { + verbatim: { + tokenizer: 'uax_url_email', + filter: %w(lowercase), + }, + + content: { + tokenizer: 'standard', + filter: %w( + lowercase + asciifolding + cjk_width + elision + english_possessive_stemmer + english_stop + english_stemmer + ), + }, + + hashtag: { + tokenizer: 'keyword', + filter: %w( + word_delimiter_graph + lowercase + asciifolding + cjk_width + ), + }, + }, + } + + index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :local_mentioned, :local_favorited, :local_reblogged, :local_bookmarked, :tags, preview_cards_status: :preview_card, preloadable_poll: :local_voters), delete_if: ->(status) { status.searchable_by.empty? } root date_detection: false do field(:id, type: 'long') field(:account_id, type: 'long') - field(:text, type: 'text', analyzer: ChewyConfig.instance.statuses_analyzers.dig('text', 'analyzer'), value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: ChewyConfig.instance.statuses_analyzers.dig('text', 'stemmed', 'analyzer')) } - field(:tags, type: 'text', analyzer: ChewyConfig.instance.statuses_analyzers.dig('tags', 'analyzer'), value: ->(status) { status.tags.map(&:display_name) }) + field(:text, type: 'text', analyzer: 'verbatim', value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: 'content') } + field(:tags, type: 'text', analyzer: 'hashtag', value: ->(status) { status.tags.map(&:display_name) }) field(:searchable_by, type: 'long', value: ->(status) { status.searchable_by }) - field(:mentioned_by, type: 'long', value: ->(status) { status.mentioned_by }) - field(:favourited_by, type: 'long', value: ->(status) { status.favourited_by }) - field(:reblogged_by, type: 'long', value: ->(status) { status.reblogged_by }) - field(:bookmarked_by, type: 'long', value: ->(status) { status.bookmarked_by }) - field(:bookmark_categoried_by, type: 'long', value: ->(status) { status.bookmark_categoried_by }) - field(:emoji_reacted_by, type: 'long', value: ->(status) { status.emoji_reacted_by }) - field(:referenced_by, type: 'long', value: ->(status) { status.referenced_by }) - field(:voted_by, type: 'long', value: ->(status) { status.voted_by }) - field(:searchability, type: 'keyword', value: ->(status) { status.compute_searchability }) - field(:visibility, type: 'keyword', value: ->(status) { status.searchable_visibility }) field(:language, type: 'keyword') - field(:domain, type: 'keyword', value: ->(status) { status.account.domain || '' }) field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties }) field(:created_at, type: 'date', value: ->(status) { clamp_date(status.created_at) }) end diff --git a/app/chewy/tags_index.rb b/app/chewy/tags_index.rb index 965718e83e..c99218a47f 100644 --- a/app/chewy/tags_index.rb +++ b/app/chewy/tags_index.rb @@ -3,9 +3,36 @@ class TagsIndex < Chewy::Index include DatetimeClampingConcern - # ElasticSearch config is moved to "/config/elasticsearch.default.yml". - # Edit it when original Mastodon changed ElasticSearch config. - settings index: index_preset(refresh_interval: '30s'), analysis: ChewyConfig.instance.tags + settings index: index_preset(refresh_interval: '30s'), analysis: { + analyzer: { + content: { + tokenizer: 'keyword', + filter: %w( + word_delimiter_graph + lowercase + asciifolding + cjk_width + ), + }, + + edge_ngram: { + tokenizer: 'edge_ngram', + filter: %w( + lowercase + asciifolding + cjk_width + ), + }, + }, + + tokenizer: { + edge_ngram: { + type: 'edge_ngram', + min_gram: 2, + max_gram: 15, + }, + }, + } index_scope ::Tag.listable @@ -14,9 +41,7 @@ class TagsIndex < Chewy::Index end root date_detection: false do - field(:name, type: 'text', analyzer: ChewyConfig.instance.tags_analyzers.dig('name', 'analyzer'), value: :display_name) do - field(:edge_ngram, type: 'text', analyzer: ChewyConfig.instance.tags_analyzers.dig('name', 'edge_ngram', 'analyzer'), search_analyzer: ChewyConfig.instance.tags_analyzers.dig('name', 'edge_ngram', 'search_analyzer')) - end + field(:name, type: 'text', analyzer: 'content', value: :display_name) { field(:edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content') } field(:reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }) field(:usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts }) field(:last_status_at, type: 'date', value: ->(tag) { clamp_date(tag.last_status_at || tag.created_at) }) diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 98e68bd873..4e475fe782 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -25,7 +25,7 @@ class AccountsController < ApplicationController limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE @statuses = filtered_statuses.without_reblogs.limit(limit) - @statuses = preload_collection(@statuses, Status) + @statuses = cache_collection(@statuses, Status) end format.json do @@ -46,11 +46,7 @@ class AccountsController < ApplicationController end def default_statuses - if current_account.present? - @account.statuses.distributable_visibility - else - @account.statuses.distributable_visibility_for_anonymous - end + @account.statuses.where(visibility: [:public, :unlisted]) end def only_media_scope diff --git a/app/controllers/activitypub/base_controller.rb b/app/controllers/activitypub/base_controller.rb index c2563c492e..388d4b9e1d 100644 --- a/app/controllers/activitypub/base_controller.rb +++ b/app/controllers/activitypub/base_controller.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true class ActivityPub::BaseController < Api::BaseController - include SignatureVerification - include AccountOwnedConcern - skip_before_action :require_authenticated_user! skip_before_action :require_not_suspended! skip_around_action :set_locale diff --git a/app/controllers/activitypub/claims_controller.rb b/app/controllers/activitypub/claims_controller.rb index 480baaf2bc..339333e462 100644 --- a/app/controllers/activitypub/claims_controller.rb +++ b/app/controllers/activitypub/claims_controller.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true class ActivityPub::ClaimsController < ActivityPub::BaseController + include SignatureVerification + include AccountOwnedConcern + skip_before_action :authenticate_user! before_action :require_account_signature! diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index c25362c9bc..2188eb72a3 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true class ActivityPub::CollectionsController < ActivityPub::BaseController + include SignatureVerification + include AccountOwnedConcern + vary_by -> { 'Signature' if authorized_fetch_mode? } before_action :require_account_signature!, if: :authorized_fetch_mode? @@ -18,7 +21,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController def set_items case params[:id] when 'featured' - @items = for_signed_account { preload_collection(@account.pinned_statuses, Status) } + @items = for_signed_account { cache_collection(@account.pinned_statuses, Status) } @items = @items.map { |item| item.distributable? ? item : ActivityPub::TagManager.instance.uri_for(item) } when 'tags' @items = for_signed_account { @account.featured_tags } diff --git a/app/controllers/activitypub/contexts_controller.rb b/app/controllers/activitypub/contexts_controller.rb deleted file mode 100644 index a3263ed82e..0000000000 --- a/app/controllers/activitypub/contexts_controller.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -class ActivityPub::ContextsController < ActivityPub::BaseController - include SignatureVerification - - vary_by -> { 'Signature' if authorized_fetch_mode? } - - before_action :set_context - - def show - expires_in 3.minutes, public: true - render json: @context, - serializer: ActivityPub::ContextSerializer, - adapter: ActivityPub::Adapter, - content_type: 'application/activity+json' - end - - private - - def set_context - @context = Conversation.find(params[:id]) - end -end diff --git a/app/controllers/activitypub/followers_synchronizations_controller.rb b/app/controllers/activitypub/followers_synchronizations_controller.rb index 392dd36bcd..976caa3445 100644 --- a/app/controllers/activitypub/followers_synchronizations_controller.rb +++ b/app/controllers/activitypub/followers_synchronizations_controller.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseController + include SignatureVerification + include AccountOwnedConcern + vary_by -> { 'Signature' if authorized_fetch_mode? } before_action :require_account_signature! @@ -21,7 +24,7 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro end def set_items - @items = @account.followers.matches_uri_prefix(uri_prefix).pluck(:uri) + @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) end def collection_presenter diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index 49cfc8ad1c..5ee85474e7 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true class ActivityPub::InboxesController < ActivityPub::BaseController + include SignatureVerification include JsonLdHelper + include AccountOwnedConcern before_action :skip_unknown_actor_activity before_action :require_actor_signature! @@ -22,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.exists?(uri: json['actor']) + 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? rescue Oj::ParseError false end @@ -60,10 +62,11 @@ class ActivityPub::InboxesController < ActivityPub::BaseController return if raw_params.blank? || ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] == 'true' || signed_request_account.nil? # Re-using the syntax for signature parameters - params = SignatureParser.parse(raw_params) + tree = SignatureParamsParser.new.parse(raw_params) + params = SignatureParamsTransformer.new.apply(tree) ActivityPub::PrepareFollowersSynchronizationService.new.call(signed_request_account, params) - rescue SignatureParser::ParsingError + rescue Parslet::ParseFailed Rails.logger.warn 'Error parsing Collection-Synchronization header' end diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index 82b1830941..bf10ba762a 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -3,6 +3,9 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController LIMIT = 20 + include SignatureVerification + include AccountOwnedConcern + vary_by -> { 'Signature' if authorized_fetch_mode? || page_requested? } before_action :require_account_signature!, if: :authorized_fetch_mode? @@ -34,7 +37,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController ActivityPub::CollectionPresenter.new( id: outbox_url, type: :ordered, - size: @account.user&.setting_hide_statuses_count ? 0 : @account.statuses_count, + size: @account.statuses_count, first: outbox_url(page: true), last: outbox_url(page: true, min_id: 0) ) @@ -60,7 +63,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController def set_statuses return unless page_requested? - @statuses = preload_collection_paginated_by_id( + @statuses = cache_collection_paginated_by_id( AccountStatusesFilter.new(@account, signed_request_account).results, Status, LIMIT, diff --git a/app/controllers/activitypub/references_controller.rb b/app/controllers/activitypub/references_controller.rb deleted file mode 100644 index 8f41fd6922..0000000000 --- a/app/controllers/activitypub/references_controller.rb +++ /dev/null @@ -1,89 +0,0 @@ -# frozen_string_literal: true - -class ActivityPub::ReferencesController < ActivityPub::BaseController - include SignatureVerification - include Authorization - include AccountOwnedConcern - - before_action :require_signature!, if: :authorized_fetch_mode? - before_action :set_status - - def index - expires_in 0, public: public_fetch_mode? - render json: references_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json', skip_activities: true - end - - private - - def pundit_user - signed_request_account - end - - def set_status - @status = @account.statuses.find(params[:status_id]) - authorize @status, :show? - rescue Mastodon::NotPermittedError - not_found - end - - def load_statuses - cached_references - end - - def cached_references - preload_collection(Status.where(id: results).reorder(:id), Status) - end - - def results - @results ||= begin - references = @status.reference_objects.order(target_status_id: :asc) - references = references.where('target_status_id > ?', page_params[:min_id]) if page_params[:min_id].present? - references = references.limit(limit_param(references_limit)) - references.pluck(:target_status_id) - end - end - - def references_limit - StatusReference::REFERENCES_LIMIT - end - - def pagination_min_id - results.last - end - - def records_continue? - results.size == limit_param(references_limit) - end - - def references_collection_presenter - page = ActivityPub::CollectionPresenter.new( - id: ActivityPub::TagManager.instance.references_uri_for(@status, page_params), - type: :unordered, - part_of: ActivityPub::TagManager.instance.references_uri_for(@status), - items: load_statuses.map(&:uri), - next: next_page - ) - - return page if page_requested? - - ActivityPub::CollectionPresenter.new( - type: :unordered, - id: ActivityPub::TagManager.instance.references_uri_for(@status), - first: page - ) - end - - def page_requested? - truthy_param?(:page) - end - - def next_page - return unless records_continue? - - ActivityPub::TagManager.instance.references_uri_for(@status, page_params.merge(min_id: pagination_min_id)) - end - - def page_params - params_slice(:min_id, :limit).merge(page: true) - end -end diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb index 11aac48c9c..c38ff89d1c 100644 --- a/app/controllers/activitypub/replies_controller.rb +++ b/app/controllers/activitypub/replies_controller.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true class ActivityPub::RepliesController < ActivityPub::BaseController + include SignatureVerification include Authorization + include AccountOwnedConcern DESCENDANTS_LIMIT = 60 @@ -31,7 +33,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController def set_replies @replies = only_other_accounts? ? Status.where.not(account_id: @account.id).joins(:account).merge(Account.without_suspended) : @account.statuses - @replies = @replies.distributable_visibility.where(in_reply_to_id: @status.id) + @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted]) @replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id]) end diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 724af2360d..9beb8fde6b 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -3,13 +3,13 @@ module Admin class AccountsController < BaseController before_action :set_account, except: [:index, :batch] - before_action :require_remote_account!, only: [:redownload, :approve_remote, :reject_remote] + before_action :require_remote_account!, only: [:redownload] before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject] def index authorize :account, :index? - @accounts = filtered_accounts.page(params[:page]).without_count + @accounts = filtered_accounts.page(params[:page]) @form = Form::AccountBatch.new end @@ -66,20 +66,6 @@ module Admin redirect_to admin_accounts_path(status: 'pending'), notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct) end - def approve_remote - authorize @account, :approve_remote? - @account.approve_remote! - log_action :approve_remote, @account - redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.approved_msg', username: @account.acct) - end - - def reject_remote - authorize @account, :reject_remote? - @account.reject_remote! - log_action :reject_remote, @account - redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct) - end - def destroy authorize @account, :destroy? Admin::AccountDeletionWorker.perform_async(@account.id) @@ -142,7 +128,7 @@ module Admin def unblock_email authorize @account, :unblock_email? - CanonicalEmailBlock.matching_account(@account).delete_all + CanonicalEmailBlock.where(reference_account: @account).delete_all log_action :unblock_email, @account @@ -182,12 +168,6 @@ module Admin 'approve' elsif params[:reject] 'reject' - elsif params[:approve_remote] - 'approve_remote' - elsif params[:approve_remote_domain] - 'approve_remote_domain' - elsif params[:reject_remote] - 'reject_remote' end end end diff --git a/app/controllers/admin/action_logs_controller.rb b/app/controllers/admin/action_logs_controller.rb index 8b8e83fde7..37a00ad225 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.auditable.select(:id, :username) + @auditable_accounts = Account.where(id: Admin::ActionLog.select('distinct account_id')).select(:id, :username) end private diff --git a/app/controllers/admin/confirmations_controller.rb b/app/controllers/admin/confirmations_controller.rb index 702550eecc..6f4e426797 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 :redirect_confirmed_user, only: [:resend], if: :user_confirmed? + before_action :check_confirmation, only: [:resend] def create authorize @user, :confirm? - @user.mark_email_as_confirmed! + @user.confirm! log_action :confirm, @user redirect_to admin_accounts_path end @@ -25,13 +25,11 @@ module Admin private - 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? + def check_confirmation + if @user.confirmed? + flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed') + redirect_to admin_accounts_path + end end end end diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb index 34368f08a2..00d069cdfb 100644 --- a/app/controllers/admin/custom_emojis_controller.rb +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -2,12 +2,10 @@ module Admin class CustomEmojisController < BaseController - before_action :set_custom_emoji, only: [:edit, :update] - def index authorize :custom_emoji, :index? - @custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]).without_count + @custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]) @form = Form::CustomEmojiBatch.new end @@ -17,10 +15,6 @@ module Admin @custom_emoji = CustomEmoji.new end - def edit - authorize :custom_emoji, :create? - end - def create authorize :custom_emoji, :create? @@ -34,19 +28,6 @@ module Admin end end - def update - authorize :custom_emoji, :create? - - @custom_emoji.assign_attributes(update_params) - - if @custom_emoji.save - log_action :update, @custom_emoji - redirect_to admin_custom_emojis_path(filter_params), notice: I18n.t('admin.custom_emojis.updated_msg') - else - render :new - end - end - def batch authorize :custom_emoji, :index? @@ -62,16 +43,8 @@ module Admin private - def set_custom_emoji - @custom_emoji = CustomEmoji.find(params[:id]) - end - def resource_params - params.require(:custom_emoji).permit(:shortcode, :image, :category_id, :visible_in_picker, :aliases_raw, :license) - end - - def update_params - params.require(:custom_emoji).permit(:category_id, :visible_in_picker, :aliases_raw, :license) + params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker) end def filtered_custom_emojis diff --git a/app/controllers/admin/domain_allows_controller.rb b/app/controllers/admin/domain_allows_controller.rb index b0f139e3a8..31be1978bb 100644 --- a/app/controllers/admin/domain_allows_controller.rb +++ b/app/controllers/admin/domain_allows_controller.rb @@ -25,8 +25,6 @@ class Admin::DomainAllowsController < Admin::BaseController def destroy authorize @domain_allow, :destroy? UnallowDomainService.new.call(@domain_allow) - log_action :destroy, @domain_allow - redirect_to admin_instances_path, notice: I18n.t('admin.domain_allows.destroyed_msg') end diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 2a2faf9cce..325b33df80 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -88,19 +88,15 @@ module Admin end def update_params - params.require(:domain_block).permit(:severity, :reject_media, :reject_favourite, :reject_reply_exclude_followers, :reject_send_sensitive, :reject_hashtag, - :reject_straight_follow, :reject_new_follow, :reject_friend, :block_trends, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden) + params.require(:domain_block).permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate) end def resource_params - params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_favourite, :reject_reply_exclude_followers, :reject_send_sensitive, :reject_hashtag, - :reject_straight_follow, :reject_new_follow, :reject_friend, :block_trends, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden) + params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate) end def form_domain_block_batch_params - params.require(:form_domain_block_batch).permit(domain_blocks_attributes: [:enabled, :domain, :severity, :reject_media, :reject_favourite, :reject_reply_exclude_followers, - :reject_send_sensitive, :reject_hashtag, :reject_straight_follow, :reject_new_follow, :reject_friend, :block_trends, :detect_invalid_subscription, - :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden]) + params.require(:form_domain_block_batch).permit(domain_blocks_attributes: [:enabled, :domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate]) end def action_from_button diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb index faa0a061a6..ff754bc0b4 100644 --- a/app/controllers/admin/email_domain_blocks_controller.rb +++ b/app/controllers/admin/email_domain_blocks_controller.rb @@ -38,7 +38,7 @@ module Admin log_action :create, @email_domain_block (@email_domain_block.other_domains || []).uniq.each do |domain| - next if EmailDomainBlock.exists?(domain: domain) + next if EmailDomainBlock.where(domain: domain).exists? 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 diff --git a/app/controllers/admin/export_domain_blocks_controller.rb b/app/controllers/admin/export_domain_blocks_controller.rb index 8d7350c765..ffc4478172 100644 --- a/app/controllers/admin/export_domain_blocks_controller.rb +++ b/app/controllers/admin/export_domain_blocks_controller.rb @@ -36,17 +36,7 @@ module Admin reject_reports: row.fetch('#reject_reports', false), private_comment: @global_private_comment, public_comment: row['#public_comment'], - obfuscate: row.fetch('#obfuscate', false), - reject_favourite: row.fetch('#reject_favourite', false), - reject_send_sensitive: row.fetch('#reject_send_sensitive', false), - reject_hashtag: row.fetch('#reject_hashtag', false), - reject_straight_follow: row.fetch('#reject_straight_follow', false), - reject_new_follow: row.fetch('#reject_new_follow', false), - hidden: row.fetch('#hidden', false), - detect_invalid_subscription: row.fetch('#detect_invalid_subscription', false), - reject_reply_exclude_followers: row.fetch('#reject_reply_exclude_followers', false), - reject_friend: row.fetch('#reject_friend', false), - block_trends: row.fetch('#block_trends', false)) + obfuscate: row.fetch('#obfuscate', false)) if domain_block.invalid? flash.now[:alert] = I18n.t('admin.export_domain_blocks.invalid_domain_block', error: domain_block.errors.full_messages.join(', ')) @@ -59,7 +49,7 @@ module Admin next end - @warning_domains = instances_from_imported_blocks.pluck(:domain) + @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) rescue ActionController::ParameterMissing flash.now[:alert] = I18n.t('admin.export_domain_blocks.no_file') set_dummy_import! @@ -68,56 +58,18 @@ module Admin private - def instances_from_imported_blocks - Instance.with_domain_follows(@domain_blocks.map(&:domain)) - end - def export_filename 'domain_blocks.csv' end def export_headers - %w( - #domain - #severity - #reject_media - #reject_reports - #public_comment - #obfuscate - #reject_favourite - #reject_send_sensitive - #reject_hashtag - #reject_straight_follow - #reject_new_follow - #hidden - #detect_invalid_subscription - #reject_reply_exclude_followers - #reject_friend - #block_trends - ) + %w(#domain #severity #reject_media #reject_reports #public_comment #obfuscate) end def export_data CSV.generate(headers: export_headers, write_headers: true) do |content| 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, - instance.reject_favourite, - instance.reject_send_sensitive, - instance.reject_hashtag, - instance.reject_straight_follow, - instance.reject_new_follow, - instance.hidden, - instance.detect_invalid_subscription, - instance.reject_reply_exclude_followers, - instance.reject_friend, - instance.block_trends, - ] + content << [instance.domain, instance.severity, instance.reject_media, instance.reject_reports, instance.public_comment, instance.obfuscate] end end end diff --git a/app/controllers/admin/friend_servers_controller.rb b/app/controllers/admin/friend_servers_controller.rb deleted file mode 100644 index 729d3b3912..0000000000 --- a/app/controllers/admin/friend_servers_controller.rb +++ /dev/null @@ -1,93 +0,0 @@ -# frozen_string_literal: true - -module Admin - class FriendServersController < BaseController - before_action :set_friend, except: [:index, :new, :create] - before_action :warn_signatures_not_enabled!, only: [:new, :edit, :create, :follow, :unfollow, :accept, :reject] - - def index - authorize :friend_server, :update? - @friends = FriendDomain.all - end - - def new - authorize :friend_server, :update? - @friend = FriendDomain.new - end - - def edit - authorize :friend_server, :update? - end - - def create - authorize :friend_server, :update? - - @friend = FriendDomain.new(resource_params) - - if @friend.save - @friend.follow! - redirect_to admin_friend_servers_path - else - render action: :new - end - end - - def update - authorize :friend_server, :update? - - if @friend.update(update_resource_params) - redirect_to admin_friend_servers_path - else - render action: :edit - end - end - - def destroy - authorize :friend_server, :update? - @friend.destroy - redirect_to admin_friend_servers_path - end - - def follow - authorize :friend_server, :update? - @friend.follow! - render action: :edit - end - - def unfollow - authorize :friend_server, :update? - @friend.unfollow! - render action: :edit - end - - def accept - authorize :friend_server, :update? - @friend.accept! - render action: :edit - end - - def reject - authorize :friend_server, :update? - @friend.reject! - render action: :edit - end - - private - - def set_friend - @friend = FriendDomain.find(params[:id]) - end - - def resource_params - params.require(:friend_domain).permit(:domain, :inbox_url, :available, :pseudo_relay, :delivery_local, :unlocked, :allow_all_posts) - end - - def update_resource_params - params.require(:friend_domain).permit(:inbox_url, :available, :pseudo_relay, :delivery_local, :unlocked, :allow_all_posts) - end - - def warn_signatures_not_enabled! - flash.now[:error] = I18n.t('admin.relays.signatures_not_enabled') if authorized_fetch_mode? - end - end -end diff --git a/app/controllers/admin/ng_rule_histories_controller.rb b/app/controllers/admin/ng_rule_histories_controller.rb deleted file mode 100644 index 9dccefaf49..0000000000 --- a/app/controllers/admin/ng_rule_histories_controller.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module Admin - class NgRuleHistoriesController < BaseController - before_action :set_ng_rule - before_action :set_histories - - PER_PAGE = 20 - - def show - authorize :ng_words, :show? - end - - private - - def set_ng_rule - @ng_rule = ::NgRule.find(params[:id]) - end - - def set_histories - @histories = NgRuleHistory.where(ng_rule_id: params[:id]).order(id: :desc).page(params[:page]).per(PER_PAGE) - end - end -end diff --git a/app/controllers/admin/ng_rules_controller.rb b/app/controllers/admin/ng_rules_controller.rb deleted file mode 100644 index f37424cced..0000000000 --- a/app/controllers/admin/ng_rules_controller.rb +++ /dev/null @@ -1,115 +0,0 @@ -# frozen_string_literal: true - -module Admin - class NgRulesController < BaseController - before_action :set_ng_rule, only: [:edit, :update, :destroy, :duplicate] - - def index - authorize :ng_words, :show? - - @ng_rules = ::NgRule.order(id: :asc) - end - - def new - authorize :ng_words, :show? - - @ng_rule = ::NgRule.build - end - - def edit - authorize :ng_words, :show? - end - - def create - authorize :ng_words, :create? - - begin - test_words! - rescue - flash[:alert] = I18n.t('admin.ng_rules.test_error') - redirect_to new_admin_ng_rule_path - return - end - - @ng_rule = ::NgRule.build(resource_params) - - if @ng_rule.save - redirect_to admin_ng_rules_path - else - render :new - end - end - - def update - authorize :ng_words, :create? - - begin - test_words! - rescue - flash[:alert] = I18n.t('admin.ng_rules.test_error') - redirect_to edit_admin_ng_rule_path(id: @ng_rule.id) - return - end - - if @ng_rule.update(resource_params) - redirect_to admin_ng_rules_path - else - render :edit - end - end - - def duplicate - authorize :ng_words, :create? - - @ng_rule = @ng_rule.copy! - - flash[:alert] = I18n.t('admin.ng_rules.copy_error') unless @ng_rule.save - - redirect_to admin_ng_rules_path - end - - def destroy - authorize :ng_words, :create? - - @ng_rule.destroy - redirect_to admin_ng_rules_path - end - - private - - def set_ng_rule - @ng_rule = ::NgRule.find(params[:id]) - end - - def resource_params - params.require(:ng_rule).permit(:title, :expires_in, :available, :account_domain, :account_username, :account_display_name, - :account_note, :account_field_name, :account_field_value, :account_avatar_state, - :account_header_state, :account_include_local, :status_spoiler_text, :status_text, :status_tag, - :status_sensitive_state, :status_cw_state, :status_media_state, :status_poll_state, - :status_mention_state, :status_reference_state, - :status_quote_state, :status_reply_state, :status_media_threshold, :status_poll_threshold, - :status_mention_threshold, :status_allow_follower_mention, - :reaction_allow_follower, :emoji_reaction_name, :emoji_reaction_origin_domain, - :status_reference_threshold, :account_allow_followed_by_local, :record_history_also_local, - status_visibility: [], status_searchability: [], reaction_type: []) - end - - def test_words! - arr = [ - resource_params[:account_domain], - resource_params[:account_username], - resource_params[:account_display_name], - resource_params[:account_note], - resource_params[:account_field_name], - resource_params[:account_field_value], - resource_params[:status_spoiler_text], - resource_params[:status_text], - resource_params[:status_tag], - resource_params[:emoji_reaction_name], - resource_params[:emoji_reaction_origin_domain], - ].compact_blank.join("\n") - - Admin::NgRule.extract_test!(arr) if arr.present? - end - end -end diff --git a/app/controllers/admin/ng_words/keywords_controller.rb b/app/controllers/admin/ng_words/keywords_controller.rb deleted file mode 100644 index 9af38fab7b..0000000000 --- a/app/controllers/admin/ng_words/keywords_controller.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -module Admin - class NgWords::KeywordsController < NgWordsController - def show - super - @ng_words = ::NgWord.caches.presence || [::NgWord.new] - end - - protected - - def validate - begin - ::NgWord.save_from_raws(settings_params_test) - return true - rescue - flash[:alert] = I18n.t('admin.ng_words.test_error') - redirect_to after_update_redirect_path - end - - false - end - - private - - def after_update_redirect_path - admin_ng_words_keywords_path - end - end -end diff --git a/app/controllers/admin/ng_words/settings_controller.rb b/app/controllers/admin/ng_words/settings_controller.rb deleted file mode 100644 index 63edadfce5..0000000000 --- a/app/controllers/admin/ng_words/settings_controller.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module Admin - class NgWords::SettingsController < NgWordsController - protected - - def after_update_redirect_path - admin_ng_words_settings_path - end - end -end diff --git a/app/controllers/admin/ng_words/white_list_controller.rb b/app/controllers/admin/ng_words/white_list_controller.rb deleted file mode 100644 index 8fdb7df327..0000000000 --- a/app/controllers/admin/ng_words/white_list_controller.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module Admin - class NgWords::WhiteListController < NgWordsController - def show - super - @white_list_domains = SpecifiedDomain.white_list_domain_caches.presence || [SpecifiedDomain.new] - end - - protected - - def validate - begin - SpecifiedDomain.save_from_raws_as_white_list(settings_params_list) - return true - rescue - flash[:alert] = I18n.t('admin.ng_words.save_error') - redirect_to after_update_redirect_path - end - - false - end - - def after_update_redirect_path - admin_ng_words_white_list_path - end - - private - - def settings_params_list - params.require(:form_admin_settings)[:specified_domains] - end - end -end diff --git a/app/controllers/admin/ng_words_controller.rb b/app/controllers/admin/ng_words_controller.rb deleted file mode 100644 index a70a435fa4..0000000000 --- a/app/controllers/admin/ng_words_controller.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -module Admin - class NgWordsController < BaseController - def show - authorize :ng_words, :show? - - @admin_settings = Form::AdminSettings.new - end - - def create - authorize :ng_words, :create? - - return unless validate - - @admin_settings = Form::AdminSettings.new(settings_params) - - if @admin_settings.save - flash[:notice] = I18n.t('generic.changes_saved_msg') - redirect_to after_update_redirect_path - else - render :show - end - end - - protected - - def validate - true - end - - def after_update_redirect_path - admin_ng_words_path - end - - private - - def settings_params - params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) - end - - def settings_params_test - params.require(:form_admin_settings)[:ng_words_test] - end - end -end diff --git a/app/controllers/admin/ngword_histories_controller.rb b/app/controllers/admin/ngword_histories_controller.rb deleted file mode 100644 index 90f13db2fe..0000000000 --- a/app/controllers/admin/ngword_histories_controller.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module Admin - class NgwordHistoriesController < BaseController - before_action :set_histories - - PER_PAGE = 20 - - def index - authorize :ng_words, :show? - end - - private - - def set_histories - @histories = NgwordHistory.order(id: :desc).page(params[:page]).per(PER_PAGE) - end - end -end diff --git a/app/controllers/admin/reports/actions_controller.rb b/app/controllers/admin/reports/actions_controller.rb index 5572108d59..554f7906f8 100644 --- a/app/controllers/admin/reports/actions_controller.rb +++ b/app/controllers/admin/reports/actions_controller.rb @@ -12,7 +12,7 @@ class Admin::Reports::ActionsController < Admin::BaseController authorize @report, :show? case action_from_button - when 'delete', 'mark_as_sensitive', 'force_cw' + when 'delete', 'mark_as_sensitive' status_batch_action = Admin::StatusBatchAction.new( type: action_from_button, status_ids: @report.status_ids, @@ -52,8 +52,6 @@ class Admin::Reports::ActionsController < Admin::BaseController 'delete' elsif params[:mark_as_sensitive] 'mark_as_sensitive' - elsif params[:force_cw] - 'force_cw' elsif params[:silence] 'silence' elsif params[:suspend] diff --git a/app/controllers/admin/rules_controller.rb b/app/controllers/admin/rules_controller.rb index b8def22ba3..d31aec6ea8 100644 --- a/app/controllers/admin/rules_controller.rb +++ b/app/controllers/admin/rules_controller.rb @@ -53,7 +53,7 @@ module Admin end def resource_params - params.require(:rule).permit(:text, :hint, :priority) + params.require(:rule).permit(:text, :priority) end end end diff --git a/app/controllers/admin/sensitive_words_controller.rb b/app/controllers/admin/sensitive_words_controller.rb deleted file mode 100644 index 24cdd4efcb..0000000000 --- a/app/controllers/admin/sensitive_words_controller.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -module Admin - class SensitiveWordsController < BaseController - def show - authorize :sensitive_words, :show? - - @admin_settings = Form::AdminSettings.new - @sensitive_words = ::SensitiveWord.caches.presence || [::SensitiveWord.new] - end - - def create - authorize :sensitive_words, :create? - - begin - ::SensitiveWord.save_from_raws(settings_params_test) - rescue - flash[:alert] = I18n.t('admin.ng_words.test_error') - redirect_to after_update_redirect_path - return - end - - @admin_settings = Form::AdminSettings.new(settings_params) - - if @admin_settings.save - flash[:notice] = I18n.t('generic.changes_saved_msg') - redirect_to after_update_redirect_path - else - render :index - end - end - - private - - def after_update_redirect_path - admin_sensitive_words_path - end - - def settings_params - params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) - end - - def settings_params_test - params.require(:form_admin_settings)[:sensitive_words_test] - end - end -end diff --git a/app/controllers/admin/settings/registrations_controller.rb b/app/controllers/admin/settings/registrations_controller.rb index 6dbc86df9a..b4a74349c0 100644 --- a/app/controllers/admin/settings/registrations_controller.rb +++ b/app/controllers/admin/settings/registrations_controller.rb @@ -1,18 +1,9 @@ # frozen_string_literal: true class Admin::Settings::RegistrationsController < Admin::SettingsController - include RegistrationLimitationHelper - - before_action :set_limitation_counts, only: :show # rubocop:disable Rails/LexicallyScopedActionFilter - private def after_update_redirect_path admin_settings_registrations_path end - - def set_limitation_counts - @current_users_count = user_count_for_registration - @current_users_count_today = today_increase_user_count - end end diff --git a/app/controllers/admin/site_uploads_controller.rb b/app/controllers/admin/site_uploads_controller.rb index 96e61cf6bb..a5d2cf41cf 100644 --- a/app/controllers/admin/site_uploads_controller.rb +++ b/app/controllers/admin/site_uploads_controller.rb @@ -9,7 +9,7 @@ module Admin @site_upload.destroy! - redirect_back fallback_location: admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg') + redirect_to admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg') end private diff --git a/app/controllers/admin/special_domains_controller.rb b/app/controllers/admin/special_domains_controller.rb deleted file mode 100644 index 0ddbf26786..0000000000 --- a/app/controllers/admin/special_domains_controller.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module Admin - class SpecialDomainsController < BaseController - def show - authorize :instance, :show? - - @admin_settings = Form::AdminSettings.new - end - - def create - authorize :instance, :destroy? - - @admin_settings = Form::AdminSettings.new(settings_params) - - if @admin_settings.save - flash[:notice] = I18n.t('generic.changes_saved_msg') - redirect_to after_update_redirect_path - else - render :show - end - end - - private - - def after_update_redirect_path - admin_special_domains_path - end - - def settings_params - params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) - end - end -end diff --git a/app/controllers/admin/special_instances_controller.rb b/app/controllers/admin/special_instances_controller.rb deleted file mode 100644 index 3fd35d474e..0000000000 --- a/app/controllers/admin/special_instances_controller.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module Admin - class SpecialInstancesController < BaseController - def show - authorize :instance, :show? - - @admin_settings = Form::AdminSettings.new - end - - def create - authorize :instance, :destroy? - - @admin_settings = Form::AdminSettings.new(settings_params) - - if @admin_settings.save - flash[:notice] = I18n.t('generic.changes_saved_msg') - redirect_to after_update_redirect_path - else - render :show - end - end - - private - - def after_update_redirect_path - admin_special_instances_path - end - - def settings_params - params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) - end - end -end diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb index 2070a7c70c..e53b22dca3 100644 --- a/app/controllers/admin/statuses_controller.rb +++ b/app/controllers/admin/statuses_controller.rb @@ -4,7 +4,7 @@ module Admin class StatusesController < BaseController before_action :set_account before_action :set_statuses, except: :show - before_action :set_status, only: [:show, :remove_history, :remove_media, :force_sensitive, :force_cw, :remove_status] + before_action :set_status, only: :show PER_PAGE = 20 @@ -29,65 +29,6 @@ module Admin redirect_to after_create_redirect_path end - def remove_history - authorize [:admin, @status], :show? - UpdateStatusService.new.call( - @status, - edit_status_account_id, - no_history: true, - bypass_validation: true - ) - log_action(:remove_history, @status) - redirect_to admin_account_status_path - end - - def remove_media - authorize [:admin, @status], :show? - UpdateStatusService.new.call( - @status, - edit_status_account_id, - media_ids: [], - media_attributes: [], - bypass_validation: true - ) - log_action(:remove_media, @status) - redirect_to admin_account_status_path - end - - def force_sensitive - authorize [:admin, @status], :show? - UpdateStatusService.new.call( - @status, - edit_status_account_id, - sensitive: true, - bypass_validation: true - ) - log_action(:force_sensitive, @status) - redirect_to admin_account_status_path - end - - def force_cw - authorize [:admin, @status], :show? - UpdateStatusService.new.call( - @status, - edit_status_account_id, - spoiler_text: 'CW', - bypass_validation: true - ) - log_action(:force_cw, @status) - redirect_to admin_account_status_path - end - - def remove_status - authorize [:admin, @status], :show? - @status.discard_with_reblogs - StatusPin.find_by(status: @status)&.destroy - @status.account.statuses_count = @status.account.statuses_count - 1 - RemovalWorker.perform_async(@status.id, { 'redraft' => false }) - log_action(:remove_status, @status) - redirect_to admin_account_path - end - private def batched_ordered_status_edits @@ -121,13 +62,6 @@ module Admin @statuses = Admin::StatusFilter.new(@account, filter_params).results.preload(:application, :preloadable_poll, :media_attachments, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, active_mentions: :account]).page(params[:page]).per(PER_PAGE) end - def edit_status_account_id - return @edit_account_id || @account.id if @edit_account_checked - - @edit_account_checked = true - @edit_account_id = Account.representative.id - end - def filter_params params.slice(*Admin::StatusFilter::KEYS).permit(*Admin::StatusFilter::KEYS) end diff --git a/app/controllers/antennas_controller.rb b/app/controllers/antennas_controller.rb deleted file mode 100644 index ca7ee5d2a2..0000000000 --- a/app/controllers/antennas_controller.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -class AntennasController < ApplicationController - layout 'admin' - - before_action :authenticate_user! - before_action :set_antenna, only: [:edit, :update, :destroy] - before_action :set_body_classes - before_action :set_cache_headers - - def index - @antennas = current_account.antennas.includes(:antenna_domains).includes(:antenna_tags).includes(:antenna_accounts) - end - - def edit; end - - def update - if @antenna.update(resource_params) - redirect_to antennas_path - else - render action: :edit - end - end - - def destroy - @antenna.destroy - redirect_to antennas_path - end - - private - - def set_antenna - @antenna = current_account.antennas.find(params[:id]) - end - - def resource_params - params.require(:antenna).permit(:title, :available, :expires_in) - end - - def thin_resource_params - params.require(:antenna).permit(:title) - end - - def set_body_classes - @body_classes = 'admin' - end - - def set_cache_headers - response.cache_control.replace(private: true, no_store: true) - end -end diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index cc4e22d27d..98fa1897ef 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -1,15 +1,13 @@ # frozen_string_literal: true class Api::BaseController < ApplicationController - DEFAULT_STATUSES_LIMIT = 20 - DEFAULT_ACCOUNTS_LIMIT = 40 + DEFAULT_STATUSES_LIMIT = 20 + DEFAULT_ACCOUNTS_LIMIT = 40 include Api::RateLimitHeaders include Api::AccessTokenTrackingConcern include Api::CachingConcern include Api::ContentSecurityPolicy - include Api::ErrorHandling - include Api::Pagination skip_before_action :require_functional!, unless: :limited_federation_mode? @@ -20,6 +18,51 @@ class Api::BaseController < ApplicationController protect_from_forgery with: :null_session + rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e| + render json: { error: e.to_s }, status: 422 + end + + rescue_from ActiveRecord::RecordNotUnique do + render json: { error: 'Duplicate record' }, status: 422 + end + + rescue_from Date::Error do + render json: { error: 'Invalid date supplied' }, status: 422 + end + + rescue_from ActiveRecord::RecordNotFound do + render json: { error: 'Record not found' }, status: 404 + end + + rescue_from HTTP::Error, Mastodon::UnexpectedResponseError do + render json: { error: 'Remote data could not be fetched' }, status: 503 + end + + rescue_from OpenSSL::SSL::SSLError do + render json: { error: 'Remote SSL certificate could not be verified' }, status: 503 + end + + rescue_from Mastodon::NotPermittedError do + render json: { error: 'This action is not allowed' }, status: 403 + end + + rescue_from Seahorse::Client::NetworkingError do |e| + Rails.logger.warn "Storage server error: #{e}" + render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503 + end + + rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight do + render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503 + end + + rescue_from Mastodon::RateLimitExceededError do + render json: { error: I18n.t('errors.429') }, status: 429 + end + + rescue_from ActionController::ParameterMissing, Mastodon::InvalidParameterError do |e| + render json: { error: e.to_s }, status: 400 + end + def doorkeeper_unauthorized_render_options(error: nil) { json: { error: error.try(:description) || 'Not authorized' } } end @@ -30,6 +73,13 @@ class Api::BaseController < ApplicationController protected + def set_pagination_headers(next_path = nil, prev_path = nil) + links = [] + links << [next_path, [%w(rel next)]] if next_path + links << [prev_path, [%w(rel prev)]] if prev_path + response.headers['Link'] = LinkHeader.new(links) unless links.empty? + end + def limit_param(default_limit) return default_limit unless params[:limit] @@ -58,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 @@ -86,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/antennas_controller.rb b/app/controllers/api/v1/accounts/antennas_controller.rb deleted file mode 100644 index 957a4fb555..0000000000 --- a/app/controllers/api/v1/accounts/antennas_controller.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Accounts::AntennasController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:lists' } - before_action :require_user! - before_action :set_account - - def index - @antennas = @account.suspended? ? [] : @account.joined_antennas.where(account: current_account) - render json: @antennas, each_serializer: REST::AntennaSerializer - end - - private - - def set_account - @account = Account.find(params[:account_id]) - end -end diff --git a/app/controllers/api/v1/accounts/circles_controller.rb b/app/controllers/api/v1/accounts/circles_controller.rb deleted file mode 100644 index 1b21eb7ce4..0000000000 --- a/app/controllers/api/v1/accounts/circles_controller.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Accounts::CirclesController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:lists' } - before_action :require_user! - before_action :set_account - - def index - @circles = @account.suspended? ? [] : @account.joined_circles.where(account: current_account) - render json: @circles, each_serializer: REST::CircleSerializer - end - - private - - def set_account - @account = Account.find(params[:account_id]) - end -end diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index daa7f87364..8f31336b9f 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Accounts::CredentialsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:accounts', :'read:me' }, except: [:update] + before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, except: [:update] before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:update] before_action :require_user! @@ -31,8 +31,6 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController :locked, :bot, :discoverable, - :searchability, - :dissubscribable, :hide_collections, :indexable, fields_attributes: [:name, :value] @@ -47,7 +45,6 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController { settings_attributes: { default_privacy: source_params.fetch(:privacy, @account.user.setting_default_privacy), - default_searchability: source_params.fetch(:searchability, @account.user.setting_default_searchability), default_sensitive: source_params.fetch(:sensitive, @account.user.setting_default_sensitive), default_language: source_params.fetch(:language, @account.user.setting_default_language), }, diff --git a/app/controllers/api/v1/accounts/exclude_antennas_controller.rb b/app/controllers/api/v1/accounts/exclude_antennas_controller.rb deleted file mode 100644 index c1f5c5981c..0000000000 --- a/app/controllers/api/v1/accounts/exclude_antennas_controller.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Accounts::ExcludeAntennasController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:lists' } - before_action :require_user! - before_action :set_account - - def index - @antennas = @account.suspended? ? [] : current_account.antennas.where('exclude_accounts @> \'[?]\'', @account.id) - render json: @antennas, each_serializer: REST::AntennaSerializer - end - - private - - def set_account - @account = Account.find(params[:account_id]) - end -end diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index 449866fa55..21b1095f18 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.not_excluded_by_account(current_account) unless current_account.nil? || current_account.id == @account.id + scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) 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, :user).references(:active_relationships) + Account.includes(:active_relationships, :account_stat).references(:active_relationships) end def paginated_follows @@ -41,6 +41,10 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController ) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_account_followers_url pagination_params(max_id: pagination_max_id) if records_continue? end diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index c4f4313f8f..1db521f79c 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.not_excluded_by_account(current_account) unless current_account.nil? || current_account.id == @account.id + scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) 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, :user).references(:passive_relationships) + Account.includes(:passive_relationships, :account_stat).references(:passive_relationships) end def paginated_follows @@ -41,6 +41,10 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController ) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_account_following_index_url pagination_params(max_id: pagination_max_id) if records_continue? end diff --git a/app/controllers/api/v1/accounts/search_controller.rb b/app/controllers/api/v1/accounts/search_controller.rb index 0ac7858e16..3061fcb7e7 100644 --- a/app/controllers/api/v1/accounts/search_controller.rb +++ b/app/controllers/api/v1/accounts/search_controller.rb @@ -18,7 +18,6 @@ class Api::V1::Accounts::SearchController < Api::BaseController limit: limit_param(DEFAULT_ACCOUNTS_LIMIT), resolve: truthy_param?(:resolve), following: truthy_param?(:following), - follower: truthy_param?(:follower), offset: params[:offset] ) end diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 6213f6f9e1..fe4279302f 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -4,14 +4,12 @@ class Api::V1::Accounts::StatusesController < Api::BaseController before_action -> { authorize_if_got_token! :read, :'read:statuses' } before_action :set_account - after_action :insert_pagination_headers + after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) } def index cache_if_unauthenticated! @statuses = load_statuses - render json: @statuses, each_serializer: REST::StatusSerializer, - relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), - emoji_reaction_permitted_account_ids: EmojiReactionAccountsPresenter.new(@statuses, current_user&.account_id) + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end private @@ -21,11 +19,11 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def load_statuses - @account.unavailable? ? [] : preloaded_account_statuses + @account.unavailable? ? [] : cached_account_statuses end - def preloaded_account_statuses - preload_collection_paginated_by_id( + def cached_account_statuses + cache_collection_paginated_by_id( AccountStatusesFilter.new(@account, current_account, params).results, Status, limit_param(DEFAULT_STATUSES_LIMIT), @@ -37,6 +35,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController params.slice(:limit, *AccountStatusesFilter::KEYS).permit(:limit, *AccountStatusesFilter::KEYS).merge(core_params) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_account_statuses_url pagination_params(max_id: pagination_max_id) if records_continue? end @@ -49,7 +51,11 @@ class Api::V1::Accounts::StatusesController < Api::BaseController @statuses.size == limit_param(DEFAULT_STATUSES_LIMIT) end - def pagination_collection - @statuses + def pagination_max_id + @statuses.last.id + end + + def pagination_since_id + @statuses.first.id end end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 881aec13e2..23fc85b475 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -9,22 +9,16 @@ class Api::V1::AccountsController < Api::BaseController before_action -> { doorkeeper_authorize! :follow, :write, :'write:blocks' }, only: [:block, :unblock] before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create] - before_action :require_user!, except: [:index, :show, :create] - before_action :set_account, except: [:index, :create] - before_action :set_accounts, only: [:index] - before_action :check_account_approval, except: [:index, :create] - before_action :check_account_confirmation, except: [:index, :create] + before_action :require_user!, except: [:show, :create] + before_action :set_account, except: [:create] + before_action :check_account_approval, except: [:create] + before_action :check_account_confirmation, except: [:create] before_action :check_enabled_registrations, only: [:create] - before_action :check_accounts_limit, only: [:index] skip_before_action :require_authenticated_user!, only: :create override_rate_limit_headers :follow, family: :follows - def index - render json: @accounts, each_serializer: REST::AccountSerializer - end - def show cache_if_unauthenticated! render json: @account, serializer: REST::AccountSerializer @@ -44,12 +38,7 @@ class Api::V1::AccountsController < Api::BaseController def follow follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, languages: params.key?(:languages) ? params[:languages] : nil, with_rate_limit: true) - options = if @account.locked? || current_user.account.silenced? || (current_user.account.bot? && @account.user&.setting_lock_follow_from_bot) - {} - else - { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify?, languages: follow.languages } }, - requested_map: { @account.id => false } } - end + options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify?, languages: follow.languages } }, requested_map: { @account.id => false } } render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(**options) end @@ -90,10 +79,6 @@ class Api::V1::AccountsController < Api::BaseController @account = Account.find(params[:id]) end - def set_accounts - @accounts = Account.where(id: account_ids).without_unapproved - end - def check_account_approval raise(ActiveRecord::RecordNotFound) if @account.local? && @account.user_pending? end @@ -102,22 +87,10 @@ class Api::V1::AccountsController < Api::BaseController raise(ActiveRecord::RecordNotFound) if @account.local? && !@account.user_confirmed? end - def check_accounts_limit - raise(Mastodon::ValidationError) if account_ids.size > DEFAULT_ACCOUNTS_LIMIT - end - def relationships(**options) AccountRelationshipsPresenter.new([@account], current_user.account_id, **options) end - def account_ids - Array(accounts_params[:ids]).uniq.map(&:to_i) - end - - def accounts_params - params.permit(ids: []) - end - def account_params params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code) end diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb index ff6f41e01d..ff9cae6398 100644 --- a/app/controllers/api/v1/admin/accounts_controller.rb +++ b/app/controllers/api/v1/admin/accounts_controller.rb @@ -125,6 +125,10 @@ class Api::V1::Admin::AccountsController < Api::BaseController translated_params end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue? end @@ -133,8 +137,12 @@ class Api::V1::Admin::AccountsController < Api::BaseController api_v1_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty? end - def pagination_collection - @accounts + def pagination_max_id + @accounts.last.id + end + + def pagination_since_id + @accounts.first.id end def records_continue? diff --git a/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb b/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb index 701f668de6..7b192b979f 100644 --- a/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb +++ b/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb @@ -65,6 +65,10 @@ class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController @canonical_email_block = CanonicalEmailBlock.find(params[:id]) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_admin_canonical_email_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue? end @@ -73,8 +77,12 @@ class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController api_v1_admin_canonical_email_blocks_url(pagination_params(min_id: pagination_since_id)) unless @canonical_email_blocks.empty? end - def pagination_collection - @canonical_email_blocks + def pagination_max_id + @canonical_email_blocks.last.id + end + + def pagination_since_id + @canonical_email_blocks.first.id end def records_continue? diff --git a/app/controllers/api/v1/admin/domain_allows_controller.rb b/app/controllers/api/v1/admin/domain_allows_controller.rb index a7ae84e306..dd54d67106 100644 --- a/app/controllers/api/v1/admin/domain_allows_controller.rb +++ b/app/controllers/api/v1/admin/domain_allows_controller.rb @@ -61,6 +61,10 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController DomainAllow.all end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_admin_domain_allows_url(pagination_params(max_id: pagination_max_id)) if records_continue? end @@ -69,8 +73,12 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController api_v1_admin_domain_allows_url(pagination_params(min_id: pagination_since_id)) unless @domain_allows.empty? end - def pagination_collection - @domain_allows + def pagination_max_id + @domain_allows.last.id + end + + def pagination_since_id + @domain_allows.first.id end def records_continue? diff --git a/app/controllers/api/v1/admin/domain_blocks_controller.rb b/app/controllers/api/v1/admin/domain_blocks_controller.rb index e225de1f2f..2538c7c7c2 100644 --- a/app/controllers/api/v1/admin/domain_blocks_controller.rb +++ b/app/controllers/api/v1/admin/domain_blocks_controller.rb @@ -29,11 +29,10 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController def create authorize :domain_block, :create? - @domain_block = DomainBlock.new(resource_params) existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil - return render json: existing_domain_block, serializer: REST::Admin::ExistingDomainBlockErrorSerializer, status: 422 if conflicts_with_existing_block?(@domain_block, existing_domain_block) + return render json: existing_domain_block, serializer: REST::Admin::ExistingDomainBlockErrorSerializer, status: 422 if existing_domain_block.present? - @domain_block.save! + @domain_block = DomainBlock.create!(resource_params) DomainBlockWorker.perform_async(@domain_block.id) log_action :create, @domain_block render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer @@ -56,10 +55,6 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController private - def conflicts_with_existing_block?(domain_block, existing_domain_block) - existing_domain_block.present? && (existing_domain_block.domain == TagManager.instance.normalize_domain(domain_block.domain) || !domain_block.stricter_than?(existing_domain_block)) - end - def set_domain_blocks @domain_blocks = filtered_domain_blocks.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) end @@ -74,8 +69,11 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController end def domain_block_params - params.permit(:severity, :reject_media, :reject_favourite, :reject_reply_exclude_followers, :reject_reports, :reject_send_sensitive, :reject_hashtag, :reject_straight_follow, - :reject_new_follow, :reject_friend, :block_trends, :detect_invalid_subscription, :private_comment, :public_comment, :obfuscate, :hidden) + params.permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) end def next_path @@ -86,8 +84,12 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController api_v1_admin_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @domain_blocks.empty? end - def pagination_collection - @domain_blocks + def pagination_max_id + @domain_blocks.last.id + end + + def pagination_since_id + @domain_blocks.first.id end def records_continue? @@ -99,7 +101,6 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController end def resource_params - params.permit(:domain, :severity, :reject_media, :reject_favourite, :reject_reply_exclude_followers, :reject_send_sensitive, :reject_hashtag, :reject_straight_follow, - :reject_new_follow, :reject_friend, :block_trends, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden) + params.permit(:domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate) end end 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 bdedb9d040..df54b9f0a4 100644 --- a/app/controllers/api/v1/admin/email_domain_blocks_controller.rb +++ b/app/controllers/api/v1/admin/email_domain_blocks_controller.rb @@ -58,6 +58,10 @@ class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController params.permit(:domain, :allow_with_approval) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_admin_email_domain_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue? end @@ -66,8 +70,12 @@ class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController api_v1_admin_email_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @email_domain_blocks.empty? end - def pagination_collection - @email_domain_blocks + def pagination_max_id + @email_domain_blocks.last.id + end + + def pagination_since_id + @email_domain_blocks.first.id end def records_continue? diff --git a/app/controllers/api/v1/admin/ip_blocks_controller.rb b/app/controllers/api/v1/admin/ip_blocks_controller.rb index 3625781149..61c1912344 100644 --- a/app/controllers/api/v1/admin/ip_blocks_controller.rb +++ b/app/controllers/api/v1/admin/ip_blocks_controller.rb @@ -63,6 +63,10 @@ class Api::V1::Admin::IpBlocksController < Api::BaseController params.permit(:ip, :severity, :comment, :expires_in) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_admin_ip_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue? end @@ -71,8 +75,12 @@ class Api::V1::Admin::IpBlocksController < Api::BaseController api_v1_admin_ip_blocks_url(pagination_params(min_id: pagination_since_id)) unless @ip_blocks.empty? end - def pagination_collection - @ip_blocks + def pagination_max_id + @ip_blocks.last.id + end + + def pagination_since_id + @ip_blocks.first.id end def records_continue? diff --git a/app/controllers/api/v1/admin/reports_controller.rb b/app/controllers/api/v1/admin/reports_controller.rb index 9b5beeab67..9dfb181a28 100644 --- a/app/controllers/api/v1/admin/reports_controller.rb +++ b/app/controllers/api/v1/admin/reports_controller.rb @@ -35,7 +35,6 @@ class Api::V1::Admin::ReportsController < Api::BaseController def update authorize @report, :update? @report.update!(report_params) - log_action :update, @report render json: @report, serializer: REST::Admin::ReportSerializer end @@ -89,6 +88,10 @@ class Api::V1::Admin::ReportsController < Api::BaseController params.permit(*FILTER_PARAMS) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_admin_reports_url(pagination_params(max_id: pagination_max_id)) if records_continue? end @@ -97,8 +100,12 @@ class Api::V1::Admin::ReportsController < Api::BaseController api_v1_admin_reports_url(pagination_params(min_id: pagination_since_id)) unless @reports.empty? end - def pagination_collection - @reports + def pagination_max_id + @reports.last.id + end + + def pagination_since_id + @reports.first.id end def records_continue? diff --git a/app/controllers/api/v1/admin/tags_controller.rb b/app/controllers/api/v1/admin/tags_controller.rb index c754980720..6a7c9f5bf3 100644 --- a/app/controllers/api/v1/admin/tags_controller.rb +++ b/app/controllers/api/v1/admin/tags_controller.rb @@ -44,6 +44,10 @@ class Api::V1::Admin::TagsController < Api::BaseController params.permit(:display_name, :trendable, :usable, :listable) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_admin_tags_url(pagination_params(max_id: pagination_max_id)) if records_continue? end @@ -52,8 +56,12 @@ class Api::V1::Admin::TagsController < Api::BaseController api_v1_admin_tags_url(pagination_params(min_id: pagination_since_id)) unless @tags.empty? end - def pagination_collection - @tags + def pagination_max_id + @tags.last.id + end + + def pagination_since_id + @tags.first.id end def records_continue? diff --git a/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb b/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb index 8bb5e22716..5d9fcc82c0 100644 --- a/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb +++ b/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb @@ -42,6 +42,10 @@ class Api::V1::Admin::Trends::Links::PreviewCardProvidersController < Api::BaseC @providers = PreviewCardProvider.all.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_admin_trends_links_preview_card_providers_url(pagination_params(max_id: pagination_max_id)) if records_continue? end @@ -50,8 +54,12 @@ class Api::V1::Admin::Trends::Links::PreviewCardProvidersController < Api::BaseC api_v1_admin_trends_links_preview_card_providers_url(pagination_params(min_id: pagination_since_id)) unless @providers.empty? end - def pagination_collection - @providers + def pagination_max_id + @providers.last.id + end + + def pagination_since_id + @providers.first.id end def records_continue? diff --git a/app/controllers/api/v1/annual_reports_controller.rb b/app/controllers/api/v1/annual_reports_controller.rb deleted file mode 100644 index 9bc8e68ac2..0000000000 --- a/app/controllers/api/v1/annual_reports_controller.rb +++ /dev/null @@ -1,30 +0,0 @@ -# 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/antennas/accounts_controller.rb b/app/controllers/api/v1/antennas/accounts_controller.rb deleted file mode 100644 index c50cbcdf3f..0000000000 --- a/app/controllers/api/v1/antennas/accounts_controller.rb +++ /dev/null @@ -1,95 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Antennas::AccountsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:show] - before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:show] - - before_action :require_user! - before_action :set_antenna - - after_action :insert_pagination_headers, only: :show - - def show - @accounts = load_accounts - render json: @accounts, each_serializer: REST::AccountSerializer - end - - def create - ApplicationRecord.transaction do - antenna_accounts.each do |account| - @antenna.antenna_accounts.create!(account: account, exclude: false) - @antenna.update!(any_accounts: false) if @antenna.any_accounts - end - end - - render_empty - end - - def destroy - AntennaAccount.where(antenna: @antenna, account_id: account_ids).destroy_all - @antenna.update!(any_accounts: true) unless @antenna.antenna_accounts.where(exclude: false).any? - render_empty - end - - private - - def set_antenna - @antenna = Antenna.where(account: current_account).find(params[:antenna_id]) - end - - def load_accounts - if unlimited? - @antenna.accounts.without_suspended.includes(:account_stat).all - else - @antenna.accounts.without_suspended.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) - end - end - - def antenna_accounts - Account.find(account_ids) - end - - def account_ids - Array(resource_params[:account_ids]) - end - - def resource_params - params.permit(account_ids: []) - end - - def insert_pagination_headers - set_pagination_headers(next_path, prev_path) - end - - def next_path - return if unlimited? - - api_v1_list_accounts_url pagination_params(max_id: pagination_max_id) if records_continue? - end - - def prev_path - return if unlimited? - - api_v1_list_accounts_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? - end - - def pagination_max_id - @accounts.last.id - end - - def pagination_since_id - @accounts.first.id - end - - def records_continue? - @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) - end - - def pagination_params(core_params) - params.slice(:limit).permit(:limit).merge(core_params) - end - - def unlimited? - params[:limit] == '0' - end -end diff --git a/app/controllers/api/v1/antennas/domains_controller.rb b/app/controllers/api/v1/antennas/domains_controller.rb deleted file mode 100644 index 554b8d613c..0000000000 --- a/app/controllers/api/v1/antennas/domains_controller.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Antennas::DomainsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:show] - before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:show] - - before_action :require_user! - before_action :set_antenna - - def show - @domains = load_domains - @exclude_domains = load_exclude_domains - render json: { domains: @domains, exclude_domains: @exclude_domains } - end - - def create - ApplicationRecord.transaction do - domains.each do |domain| - @antenna.antenna_domains.create!(name: domain, exclude: false) - @antenna.update!(any_domains: false) if @antenna.any_domains - end - end - - render_empty - end - - def destroy - AntennaDomain.where(antenna: @antenna, name: domains).destroy_all - @antenna.update!(any_domains: true) unless @antenna.antenna_domains.where(exclude: false).any? - render_empty - end - - private - - def set_antenna - @antenna = Antenna.where(account: current_account).find(params[:antenna_id]) - end - - def load_domains - @antenna.antenna_domains.pluck(:name) - end - - def load_exclude_domains - @antenna.exclude_domains || [] - end - - def domains - Array(resource_params[:domains]) - end - - def resource_params - params.permit(domains: []) - end -end diff --git a/app/controllers/api/v1/antennas/exclude_accounts_controller.rb b/app/controllers/api/v1/antennas/exclude_accounts_controller.rb deleted file mode 100644 index cdb9173c11..0000000000 --- a/app/controllers/api/v1/antennas/exclude_accounts_controller.rb +++ /dev/null @@ -1,104 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Antennas::ExcludeAccountsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:show] - before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:show] - - before_action :require_user! - before_action :set_antenna - - after_action :insert_pagination_headers, only: :show - - def show - @accounts = load_accounts - render json: @accounts, each_serializer: REST::AccountSerializer - end - - def create - new_accounts = @antenna.exclude_accounts || [] - antenna_accounts.each do |account| - raise Mastodon::ValidationError, I18n.t('antennas.errors.duplicate_account') if new_accounts.include?(account.id) - - new_accounts << account.id - end - - raise Mastodon::ValidationError, I18n.t('antennas.errors.limit.accounts') if new_accounts.size > Antenna::ACCOUNTS_PER_ANTENNA_LIMIT - - @antenna.update!(exclude_accounts: new_accounts) - - render_empty - end - - def destroy - new_accounts = @antenna.exclude_accounts || [] - new_accounts -= antenna_accounts.pluck(:id) - - @antenna.update!(exclude_accounts: new_accounts) - - render_empty - end - - private - - def set_antenna - @antenna = Antenna.where(account: current_account).find(params[:antenna_id]) - end - - def load_accounts - return [] if @antenna.exclude_accounts.nil? - - if unlimited? - Account.where(id: @antenna.exclude_accounts).without_suspended.includes(:account_stat).all - else - Account.where(id: @antenna.exclude_accounts).without_suspended.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) - end - end - - def antenna_accounts - Account.find(account_ids) - end - - def account_ids - Array(resource_params[:account_ids]) - end - - def resource_params - params.permit(account_ids: []) - end - - def insert_pagination_headers - set_pagination_headers(next_path, prev_path) - end - - def next_path - return if unlimited? - - api_v1_list_accounts_url pagination_params(max_id: pagination_max_id) if records_continue? - end - - def prev_path - return if unlimited? - - api_v1_list_accounts_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? - end - - def pagination_max_id - @accounts.last.id - end - - def pagination_since_id - @accounts.first.id - end - - def records_continue? - @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) - end - - def pagination_params(core_params) - params.slice(:limit).permit(:limit).merge(core_params) - end - - def unlimited? - params[:limit] == '0' - end -end diff --git a/app/controllers/api/v1/antennas/exclude_domains_controller.rb b/app/controllers/api/v1/antennas/exclude_domains_controller.rb deleted file mode 100644 index 235a44d593..0000000000 --- a/app/controllers/api/v1/antennas/exclude_domains_controller.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Antennas::ExcludeDomainsController < Api::BaseController - before_action -> { doorkeeper_authorize! :write, :'write:lists' } - - before_action :require_user! - before_action :set_antenna - - def create - new_domains = @antenna.exclude_domains || [] - domains.each do |domain| - raise Mastodon::ValidationError, I18n.t('antennas.errors.duplicate_domain') if new_domains.include?(domain) - - new_domains << domain - end - - raise Mastodon::ValidationError, I18n.t('antennas.errors.limit.domains') if new_domains.size > Antenna::KEYWORDS_PER_ANTENNA_LIMIT - - @antenna.update!(exclude_domains: new_domains) - - render_empty - end - - def destroy - new_domains = @antenna.exclude_domains || [] - new_domains -= domains - - @antenna.update!(exclude_domains: new_domains) - - render_empty - end - - private - - def set_antenna - @antenna = Antenna.where(account: current_account).find(params[:antenna_id]) - end - - def domains - Array(resource_params[:domains]) - end - - def resource_params - params.permit(domains: []) - end -end diff --git a/app/controllers/api/v1/antennas/exclude_keywords_controller.rb b/app/controllers/api/v1/antennas/exclude_keywords_controller.rb deleted file mode 100644 index 171dac5f80..0000000000 --- a/app/controllers/api/v1/antennas/exclude_keywords_controller.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Antennas::ExcludeKeywordsController < Api::BaseController - before_action -> { doorkeeper_authorize! :write, :'write:lists' } - - before_action :require_user! - before_action :set_antenna - - def create - new_keywords = @antenna.exclude_keywords || [] - keywords.each do |keyword| - raise Mastodon::ValidationError, I18n.t('antennas.errors.duplicate_keyword') if new_keywords.include?(keyword) - - new_keywords << keyword - end - - raise Mastodon::ValidationError, I18n.t('antennas.errors.limit.keywords') if new_keywords.size > Antenna::KEYWORDS_PER_ANTENNA_LIMIT - - @antenna.update!(exclude_keywords: new_keywords) - - render_empty - end - - def destroy - new_keywords = @antenna.exclude_keywords || [] - new_keywords -= keywords - - @antenna.update!(exclude_keywords: new_keywords) - - render_empty - end - - private - - def set_antenna - @antenna = Antenna.where(account: current_account).find(params[:antenna_id]) - end - - def keywords - Array(resource_params[:keywords]) - end - - def resource_params - params.permit(keywords: []) - end -end diff --git a/app/controllers/api/v1/antennas/exclude_tags_controller.rb b/app/controllers/api/v1/antennas/exclude_tags_controller.rb deleted file mode 100644 index bf9e087369..0000000000 --- a/app/controllers/api/v1/antennas/exclude_tags_controller.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Antennas::ExcludeTagsController < Api::BaseController - before_action -> { doorkeeper_authorize! :write, :'write:lists' } - - before_action :require_user! - before_action :set_antenna - - def create - new_tags = @antenna.exclude_tags || [] - tags.map(&:id).each do |tag| - raise Mastodon::ValidationError, I18n.t('antennas.errors.duplicate_tag') if new_tags.include?(tag) - - new_tags << tag - end - - raise Mastodon::ValidationError, I18n.t('antennas.errors.limit.tags') if new_tags.size > Antenna::TAGS_PER_ANTENNA_LIMIT - - @antenna.update!(exclude_tags: new_tags) - - render_empty - end - - def destroy - new_tags = @antenna.exclude_tags || [] - new_tags -= exist_tags.pluck(:id) - - @antenna.update!(exclude_tags: new_tags) - - render_empty - end - - private - - def set_antenna - @antenna = Antenna.where(account: current_account).find(params[:antenna_id]) - end - - def tags - Tag.find_or_create_by_names(Array(resource_params[:tags])) - end - - def exist_tags - Tag.matching_name(Array(resource_params[:tags])) - end - - def resource_params - params.permit(tags: []) - end -end diff --git a/app/controllers/api/v1/antennas/keywords_controller.rb b/app/controllers/api/v1/antennas/keywords_controller.rb deleted file mode 100644 index 5260a66bc0..0000000000 --- a/app/controllers/api/v1/antennas/keywords_controller.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Antennas::KeywordsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:show] - before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:show] - - before_action :require_user! - before_action :set_antenna - - def show - @keywords = load_keywords - @exclude_keywords = load_exclude_keywords - render json: { keywords: @keywords, exclude_keywords: @exclude_keywords } - end - - def create - new_keywords = @antenna.keywords || [] - keywords.each do |keyword| - raise Mastodon::ValidationError, I18n.t('antennas.errors.duplicate_keyword') if new_keywords.include?(keyword) - raise Mastodon::ValidationError, I18n.t('antennas.errors.too_short_keyword') if keyword.length < 2 - - new_keywords << keyword - end - - raise Mastodon::ValidationError, I18n.t('antennas.errors.limit.keywords') if new_keywords.size > Antenna::KEYWORDS_PER_ANTENNA_LIMIT - - @antenna.update!(keywords: new_keywords, any_keywords: new_keywords.empty?) - - render_empty - end - - def destroy - new_keywords = @antenna.keywords || [] - new_keywords -= keywords - - @antenna.update!(keywords: new_keywords, any_keywords: new_keywords.empty?) - - render_empty - end - - private - - def set_antenna - @antenna = Antenna.where(account: current_account).find(params[:antenna_id]) - end - - def load_keywords - @antenna.keywords || [] - end - - def load_exclude_keywords - @antenna.exclude_keywords || [] - end - - def keywords - Array(resource_params[:keywords]) - end - - def resource_params - params.permit(keywords: []) - end -end diff --git a/app/controllers/api/v1/antennas/tags_controller.rb b/app/controllers/api/v1/antennas/tags_controller.rb deleted file mode 100644 index fe0bb6b4eb..0000000000 --- a/app/controllers/api/v1/antennas/tags_controller.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Antennas::TagsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:show] - before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:show] - - before_action :require_user! - before_action :set_antenna - - def show - @tags = load_tags - @exclude_tags = load_exclude_tags - render json: { tags: @tags, exclude_tags: @exclude_tags.pluck(:name) } - end - - def create - ApplicationRecord.transaction do - tags.each do |tag| - @antenna.antenna_tags.create!(tag: tag, exclude: false) - @antenna.update!(any_tags: false) if @antenna.any_tags - end - end - - render_empty - end - - def destroy - AntennaTag.where(antenna: @antenna, tag: exist_tags).destroy_all - @antenna.update!(any_tags: true) unless @antenna.antenna_tags.where(exclude: false).any? - render_empty - end - - private - - def set_antenna - @antenna = Antenna.where(account: current_account).find(params[:antenna_id]) - end - - def load_tags - @antenna.tags.pluck(:name) - end - - def load_exclude_tags - Tag.where(id: @antenna.exclude_tags || []) - end - - def tags - Tag.find_or_create_by_names(Array(resource_params[:tags])) - end - - def exist_tags - Tag.matching_name(Array(resource_params[:tags])) - end - - def resource_params - params.permit(tags: []) - end -end diff --git a/app/controllers/api/v1/antennas_controller.rb b/app/controllers/api/v1/antennas_controller.rb deleted file mode 100644 index 37bfb7f552..0000000000 --- a/app/controllers/api/v1/antennas_controller.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::AntennasController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:index, :show] - before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:index, :show] - - before_action :require_user! - before_action :set_antenna, except: [:index, :create] - - rescue_from ArgumentError do |e| - render json: { error: e.to_s }, status: 422 - end - - def index - @antennas = Antenna.where(account: current_account).all - render json: @antennas, each_serializer: REST::AntennaSerializer - end - - def show - render json: @antenna, serializer: REST::AntennaSerializer - end - - def create - @antenna = Antenna.create!(antenna_params.merge(account: current_account, list_id: 0)) - render json: @antenna, serializer: REST::AntennaSerializer - end - - def update - @antenna.update!(antenna_params) - render json: @antenna, serializer: REST::AntennaSerializer - end - - def destroy - @antenna.destroy! - render_empty - end - - private - - def set_antenna - @antenna = Antenna.where(account: current_account).find(params[:id]) - end - - def antenna_params - params.permit(:title, :list_id, :insert_feeds, :stl, :ltl, :with_media_only, :ignore_reblog) - end -end diff --git a/app/controllers/api/v1/blocks_controller.rb b/app/controllers/api/v1/blocks_controller.rb index 234ab2e82c..06a8bfa891 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, :user]) + @paginated_blocks ||= Block.eager_load(target_account: :account_stat) .joins(:target_account) .merge(Account.without_suspended) .where(account: current_account) @@ -28,6 +28,10 @@ class Api::V1::BlocksController < Api::BaseController ) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_blocks_url pagination_params(max_id: pagination_max_id) if records_continue? end @@ -36,8 +40,12 @@ class Api::V1::BlocksController < Api::BaseController api_v1_blocks_url pagination_params(since_id: pagination_since_id) unless paginated_blocks.empty? end - def pagination_collection - paginated_blocks + def pagination_max_id + paginated_blocks.last.id + end + + def pagination_since_id + paginated_blocks.first.id end def records_continue? diff --git a/app/controllers/api/v1/bookmark_categories/statuses_controller.rb b/app/controllers/api/v1/bookmark_categories/statuses_controller.rb deleted file mode 100644 index a195fce97d..0000000000 --- a/app/controllers/api/v1/bookmark_categories/statuses_controller.rb +++ /dev/null @@ -1,94 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::BookmarkCategories::StatusesController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:show] - before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:show] - - before_action :require_user! - before_action :set_bookmark_category - - after_action :insert_pagination_headers, only: :show - - def show - @statuses = load_statuses - render json: @statuses, each_serializer: REST::StatusSerializer - end - - def create - ApplicationRecord.transaction do - bookmark_category_statuses.each do |status| - Bookmark.find_or_create_by!(account: current_account, status: status) - @bookmark_category.statuses << status - end - end - - render_empty - end - - def destroy - BookmarkCategoryStatus.where(bookmark_category: @bookmark_category, status_id: status_ids).destroy_all - render_empty - end - - private - - def set_bookmark_category - @bookmark_category = current_account.bookmark_categories.find(params[:bookmark_category_id]) - end - - def load_statuses - if unlimited? - @bookmark_category.statuses.includes(:status_stat).all - else - @bookmark_category.statuses.includes(:status_stat).paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id]) - end - end - - def bookmark_category_statuses - Status.find(status_ids) - end - - def status_ids - Array(resource_params[:status_ids]) - end - - def resource_params - params.permit(status_ids: []) - end - - def insert_pagination_headers - set_pagination_headers(next_path, prev_path) - end - - def next_path - return if unlimited? - - api_v1_bookmark_category_statuses_url pagination_params(max_id: pagination_max_id) if records_continue? - end - - def prev_path - return if unlimited? - - api_v1_bookmark_category_statuses_url pagination_params(since_id: pagination_since_id) unless @statuses.empty? - end - - def pagination_max_id - @statuses.last.id - end - - def pagination_since_id - @statuses.first.id - end - - def records_continue? - @statuses.size == limit_param(DEFAULT_STATUSES_LIMIT) - end - - def pagination_params(core_params) - params.slice(:limit).permit(:limit).merge(core_params) - end - - def unlimited? - params[:limit] == '0' - end -end diff --git a/app/controllers/api/v1/bookmark_categories_controller.rb b/app/controllers/api/v1/bookmark_categories_controller.rb deleted file mode 100644 index c32828630d..0000000000 --- a/app/controllers/api/v1/bookmark_categories_controller.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::BookmarkCategoriesController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:index, :show] - before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:index, :show] - - before_action :require_user! - before_action :set_bookmark_category, except: [:index, :create] - - rescue_from ArgumentError do |e| - render json: { error: e.to_s }, status: 422 - end - - def index - @bookmark_categories = BookmarkCategory.where(account: current_account).all - render json: @bookmark_categories, each_serializer: REST::BookmarkCategorySerializer - end - - def show - render json: @bookmark_category, serializer: REST::BookmarkCategorySerializer - end - - def create - @bookmark_category = BookmarkCategory.create!(bookmark_category_params.merge(account: current_account)) - render json: @bookmark_category, serializer: REST::BookmarkCategorySerializer - end - - def update - @bookmark_category.update!(bookmark_category_params) - render json: @bookmark_category, serializer: REST::BookmarkCategorySerializer - end - - def destroy - @bookmark_category.destroy! - render_empty - end - - private - - def set_bookmark_category - @bookmark_category = BookmarkCategory.where(account: current_account).find(params[:id]) - end - - def bookmark_category_params - params.permit(:title) - end -end diff --git a/app/controllers/api/v1/bookmarks_controller.rb b/app/controllers/api/v1/bookmarks_controller.rb index a2209d812e..498eb16f44 100644 --- a/app/controllers/api/v1/bookmarks_controller.rb +++ b/app/controllers/api/v1/bookmarks_controller.rb @@ -7,19 +7,17 @@ class Api::V1::BookmarksController < Api::BaseController def index @statuses = load_statuses - render json: @statuses, each_serializer: REST::StatusSerializer, - relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), - emoji_reaction_permitted_account_ids: EmojiReactionAccountsPresenter.new(@statuses, current_user&.account_id) + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end private def load_statuses - preloaded_bookmarks + cached_bookmarks end - def preloaded_bookmarks - preload_collection(results.map(&:status), Status) + def cached_bookmarks + cache_collection(results.map(&:status), Status) end def results @@ -33,6 +31,10 @@ class Api::V1::BookmarksController < Api::BaseController current_account.bookmarks end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_bookmarks_url pagination_params(max_id: pagination_max_id) if records_continue? end @@ -41,8 +43,12 @@ class Api::V1::BookmarksController < Api::BaseController api_v1_bookmarks_url pagination_params(min_id: pagination_since_id) unless results.empty? end - def pagination_collection - results + def pagination_max_id + results.last.id + end + + def pagination_since_id + results.first.id end def records_continue? diff --git a/app/controllers/api/v1/circles/accounts_controller.rb b/app/controllers/api/v1/circles/accounts_controller.rb deleted file mode 100644 index e0d43bd950..0000000000 --- a/app/controllers/api/v1/circles/accounts_controller.rb +++ /dev/null @@ -1,93 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Circles::AccountsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:show] - before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:show] - - before_action :require_user! - before_action :set_circle - - after_action :insert_pagination_headers, only: :show - - def show - @accounts = load_accounts - render json: @accounts, each_serializer: REST::AccountSerializer - end - - def create - ApplicationRecord.transaction do - circle_accounts.each do |account| - @circle.accounts << account - end - end - - render_empty - end - - def destroy - CircleAccount.where(circle: @circle, account_id: account_ids).destroy_all - render_empty - end - - private - - def set_circle - @circle = Circle.where(account: current_account).find(params[:circle_id]) - end - - def load_accounts - if unlimited? - @circle.accounts.without_suspended.includes(:account_stat).all - else - @circle.accounts.without_suspended.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) - end - end - - def circle_accounts - Account.find(account_ids) - end - - def account_ids - Array(resource_params[:account_ids]) - end - - def resource_params - params.permit(account_ids: []) - end - - def insert_pagination_headers - set_pagination_headers(next_path, prev_path) - end - - def next_path - return if unlimited? - - api_v1_circle_accounts_url pagination_params(max_id: pagination_max_id) if records_continue? - end - - def prev_path - return if unlimited? - - api_v1_circle_accounts_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? - end - - def pagination_max_id - @accounts.last.id - end - - def pagination_since_id - @accounts.first.id - end - - def records_continue? - @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) - end - - def pagination_params(core_params) - params.slice(:limit).permit(:limit).merge(core_params) - end - - def unlimited? - params[:limit] == '0' - end -end diff --git a/app/controllers/api/v1/circles/statuses_controller.rb b/app/controllers/api/v1/circles/statuses_controller.rb deleted file mode 100644 index 705731936b..0000000000 --- a/app/controllers/api/v1/circles/statuses_controller.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Circles::StatusesController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:show] - - before_action :require_user! - before_action :set_circle - - after_action :insert_pagination_headers, only: :show - - def show - @statuses = load_statuses - render json: @statuses, each_serializer: REST::StatusSerializer - end - - private - - def set_circle - @circle = current_account.circles.find(params[:circle_id]) - end - - def load_statuses - if unlimited? - @circle.statuses.includes(:status_stat).all - else - @circle.statuses.includes(:status_stat).paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id]) - end - end - - def insert_pagination_headers - set_pagination_headers(next_path, prev_path) - end - - def next_path - return if unlimited? - - api_v1_circle_statuses_url pagination_params(max_id: pagination_max_id) if records_continue? - end - - def prev_path - return if unlimited? - - api_v1_circle_statuses_url pagination_params(since_id: pagination_since_id) unless @statuses.empty? - end - - def pagination_max_id - @statuses.last.id - end - - def pagination_since_id - @statuses.first.id - end - - def records_continue? - @statuses.size == limit_param(DEFAULT_STATUSES_LIMIT) - end - - def pagination_params(core_params) - params.slice(:limit).permit(:limit).merge(core_params) - end - - def unlimited? - params[:limit] == '0' - end -end diff --git a/app/controllers/api/v1/circles_controller.rb b/app/controllers/api/v1/circles_controller.rb deleted file mode 100644 index 53c9adf14e..0000000000 --- a/app/controllers/api/v1/circles_controller.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::CirclesController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:index, :show] - before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:index, :show] - - before_action :require_user! - before_action :set_circle, except: [:index, :create] - - rescue_from ArgumentError do |e| - render json: { error: e.to_s }, status: 422 - end - - def index - @circles = Circle.where(account: current_account).all - render json: @circles, each_serializer: REST::CircleSerializer - end - - def show - render json: @circle, serializer: REST::CircleSerializer - end - - def create - @circle = Circle.create!(circle_params.merge(account: current_account)) - render json: @circle, serializer: REST::CircleSerializer - end - - def update - @circle.update!(circle_params) - render json: @circle, serializer: REST::CircleSerializer - end - - def destroy - @circle.destroy! - render_empty - end - - private - - def set_circle - @circle = Circle.where(account: current_account).find(params[:id]) - end - - def circle_params - params.permit(:title) - end -end diff --git a/app/controllers/api/v1/conversations_controller.rb b/app/controllers/api/v1/conversations_controller.rb index a95c816e1c..6a3567e624 100644 --- a/app/controllers/api/v1/conversations_controller.rb +++ b/app/controllers/api/v1/conversations_controller.rb @@ -53,6 +53,10 @@ class Api::V1::ConversationsController < Api::BaseController .to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_conversations_url pagination_params(max_id: pagination_max_id) if records_continue? end diff --git a/app/controllers/api/v1/crypto/encrypted_messages_controller.rb b/app/controllers/api/v1/crypto/encrypted_messages_controller.rb index d3de220393..68cf4384f7 100644 --- a/app/controllers/api/v1/crypto/encrypted_messages_controller.rb +++ b/app/controllers/api/v1/crypto/encrypted_messages_controller.rb @@ -29,6 +29,10 @@ class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController @encrypted_messages = @current_device.encrypted_messages.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_crypto_encrypted_messages_url pagination_params(max_id: pagination_max_id) if records_continue? end @@ -37,8 +41,12 @@ class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController api_v1_crypto_encrypted_messages_url pagination_params(min_id: pagination_since_id) unless @encrypted_messages.empty? end - def pagination_collection - @encrypted_messages + def pagination_max_id + @encrypted_messages.last.id + end + + def pagination_since_id + @encrypted_messages.first.id end def records_continue? diff --git a/app/controllers/api/v1/directories_controller.rb b/app/controllers/api/v1/directories_controller.rb index 6c540404ea..e79b20ce42 100644 --- a/app/controllers/api/v1/directories_controller.rb +++ b/app/controllers/api/v1/directories_controller.rb @@ -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.includes(:account_stat, user: :role) + end end def local_accounts? diff --git a/app/controllers/api/v1/domain_blocks_controller.rb b/app/controllers/api/v1/domain_blocks_controller.rb index 3dee2d176c..34def3c44a 100644 --- a/app/controllers/api/v1/domain_blocks_controller.rb +++ b/app/controllers/api/v1/domain_blocks_controller.rb @@ -38,6 +38,10 @@ class Api::V1::DomainBlocksController < Api::BaseController current_account.domain_blocks end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_domain_blocks_url pagination_params(max_id: pagination_max_id) if records_continue? end @@ -46,8 +50,12 @@ class Api::V1::DomainBlocksController < Api::BaseController api_v1_domain_blocks_url pagination_params(since_id: pagination_since_id) unless @blocks.empty? end - def pagination_collection - @blocks + def pagination_max_id + @blocks.last.id + end + + def pagination_since_id + @blocks.first.id end def records_continue? diff --git a/app/controllers/api/v1/emoji_reactions_controller.rb b/app/controllers/api/v1/emoji_reactions_controller.rb deleted file mode 100644 index 5e913eef2a..0000000000 --- a/app/controllers/api/v1/emoji_reactions_controller.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::EmojiReactionsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:favourites' } - before_action :require_user! - after_action :insert_pagination_headers - - def index - @statuses = load_statuses - render json: @statuses, each_serializer: REST::StatusSerializer, - relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), - emoji_reaction_permitted_account_ids: EmojiReactionAccountsPresenter.new(@statuses, current_user&.account_id) - end - - private - - def load_statuses - cached_emoji_reactions - end - - def cached_emoji_reactions - preload_collection(results.map(&:status), EmojiReaction) - end - - def results - @results ||= account_emoji_reactions.joins(:status).eager_load(:status).to_a_paginated_by_id( - limit_param(DEFAULT_STATUSES_LIMIT), - params_slice(:max_id, :since_id, :min_id) - ) - end - - def account_emoji_reactions - current_account.emoji_reactions - end - - def insert_pagination_headers - set_pagination_headers(next_path, prev_path) - end - - def next_path - api_v1_emoji_reactions_url pagination_params(max_id: pagination_max_id) if records_continue? - end - - def prev_path - api_v1_emoji_reactions_url pagination_params(min_id: pagination_since_id) unless results.empty? - end - - def pagination_max_id - results.last.id - end - - def pagination_since_id - results.first.id - end - - def records_continue? - results.size == limit_param(DEFAULT_STATUSES_LIMIT) - end - - def pagination_params(core_params) - params.slice(:limit).permit(:limit).merge(core_params) - end -end diff --git a/app/controllers/api/v1/endorsements_controller.rb b/app/controllers/api/v1/endorsements_controller.rb index 9a723d89e4..46e3fcd647 100644 --- a/app/controllers/api/v1/endorsements_controller.rb +++ b/app/controllers/api/v1/endorsements_controller.rb @@ -25,7 +25,11 @@ class Api::V1::EndorsementsController < Api::BaseController end def endorsed_accounts - current_account.endorsed_accounts.includes(:account_stat, :user).without_suspended + current_account.endorsed_accounts.includes(:account_stat).without_suspended + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) end def next_path @@ -40,8 +44,12 @@ class Api::V1::EndorsementsController < Api::BaseController api_v1_endorsements_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? end - def pagination_collection - @accounts + def pagination_max_id + @accounts.last.id + end + + def pagination_since_id + @accounts.first.id end def records_continue? diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb index 95c795468b..faf1bda96a 100644 --- a/app/controllers/api/v1/favourites_controller.rb +++ b/app/controllers/api/v1/favourites_controller.rb @@ -7,19 +7,17 @@ class Api::V1::FavouritesController < Api::BaseController def index @statuses = load_statuses - render json: @statuses, each_serializer: REST::StatusSerializer, - relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), - emoji_reaction_permitted_account_ids: EmojiReactionAccountsPresenter.new(@statuses, current_user&.account_id) + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end private def load_statuses - preloaded_favourites + cached_favourites end - def preloaded_favourites - preload_collection(results.map(&:status), Status) + def cached_favourites + cache_collection(results.map(&:status), Status) end def results @@ -33,6 +31,10 @@ class Api::V1::FavouritesController < Api::BaseController current_account.favourites end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_favourites_url pagination_params(max_id: pagination_max_id) if records_continue? end @@ -41,8 +43,12 @@ class Api::V1::FavouritesController < Api::BaseController api_v1_favourites_url pagination_params(min_id: pagination_since_id) unless results.empty? end - def pagination_collection - results + def pagination_max_id + results.last.id + end + + def pagination_since_id + results.first.id end def records_continue? diff --git a/app/controllers/api/v1/featured_tags/suggestions_controller.rb b/app/controllers/api/v1/featured_tags/suggestions_controller.rb index 9c72e4380d..76633210a1 100644 --- a/app/controllers/api/v1/featured_tags/suggestions_controller.rb +++ b/app/controllers/api/v1/featured_tags/suggestions_controller.rb @@ -12,6 +12,6 @@ class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController private def set_recently_used_tags - @recently_used_tags = Tag.suggestions_for_account(current_account).limit(10) + @recently_used_tags = Tag.recently_used(current_account).where.not(id: current_account.featured_tags).limit(10) end end diff --git a/app/controllers/api/v1/filters_controller.rb b/app/controllers/api/v1/filters_controller.rb index 4345b61ac7..ed98acce30 100644 --- a/app/controllers/api/v1/filters_controller.rb +++ b/app/controllers/api/v1/filters_controller.rb @@ -52,11 +52,11 @@ class Api::V1::FiltersController < Api::BaseController end def resource_params - params.permit(:phrase, :expires_in, :irreversible, :exclude_follows, :exclude_localusers, :with_quote, :whole_word, context: []) + params.permit(:phrase, :expires_in, :irreversible, :whole_word, context: []) end def filter_params - resource_params.slice(:phrase, :expires_in, :irreversible, :exclude_follows, :exclude_localusers, :with_quote, :context) + resource_params.slice(:phrase, :expires_in, :irreversible, :context) end def keyword_params diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb index 7ffd7614bb..ee717ebbcc 100644 --- a/app/controllers/api/v1/follow_requests_controller.rb +++ b/app/controllers/api/v1/follow_requests_controller.rb @@ -37,7 +37,7 @@ class Api::V1::FollowRequestsController < Api::BaseController end def default_accounts - Account.without_suspended.includes(:follow_requests, :account_stat, :user).references(:follow_requests) + Account.without_suspended.includes(:follow_requests, :account_stat).references(:follow_requests) end def paginated_follow_requests @@ -48,6 +48,10 @@ class Api::V1::FollowRequestsController < Api::BaseController ) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_follow_requests_url pagination_params(max_id: pagination_max_id) if records_continue? end diff --git a/app/controllers/api/v1/followed_tags_controller.rb b/app/controllers/api/v1/followed_tags_controller.rb index 8888612b16..eae2bdc010 100644 --- a/app/controllers/api/v1/followed_tags_controller.rb +++ b/app/controllers/api/v1/followed_tags_controller.rb @@ -22,6 +22,10 @@ class Api::V1::FollowedTagsController < Api::BaseController ) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_followed_tags_url pagination_params(max_id: pagination_max_id) if records_continue? end @@ -30,8 +34,12 @@ class Api::V1::FollowedTagsController < Api::BaseController api_v1_followed_tags_url pagination_params(since_id: pagination_since_id) unless @results.empty? end - def pagination_collection - @results + def pagination_max_id + @results.last.id + end + + def pagination_since_id + @results.first.id end def records_continue? diff --git a/app/controllers/api/v1/lists/accounts_controller.rb b/app/controllers/api/v1/lists/accounts_controller.rb index aecf391049..8e12cb7b65 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, :user).all + @list.accounts.without_suspended.includes(:account_stat).all else - @list.accounts.without_suspended.includes(:account_stat, :user).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) + @list.accounts.without_suspended.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) end end @@ -55,6 +55,10 @@ class Api::V1::Lists::AccountsController < Api::BaseController params.permit(account_ids: []) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path return if unlimited? @@ -67,8 +71,12 @@ class Api::V1::Lists::AccountsController < Api::BaseController api_v1_list_accounts_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? end - def pagination_collection - @accounts + def pagination_max_id + @accounts.last.id + end + + def pagination_since_id + @accounts.first.id end def records_continue? diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb index 0bacd7fdb0..4bbbed2673 100644 --- a/app/controllers/api/v1/lists_controller.rb +++ b/app/controllers/api/v1/lists_controller.rb @@ -31,9 +31,6 @@ class Api::V1::ListsController < Api::BaseController end def destroy - antenna = Antenna.find_by(list_id: @list.id) - antenna.update!(list_id: 0) if antenna.present? - @list.destroy! render_empty end @@ -45,6 +42,6 @@ class Api::V1::ListsController < Api::BaseController end def list_params - params.permit(:title, :replies_policy, :exclusive, :notify) + params.permit(:title, :replies_policy, :exclusive) end end diff --git a/app/controllers/api/v1/markers_controller.rb b/app/controllers/api/v1/markers_controller.rb index 8eaf7767df..f8dfba8a94 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_create_by(timeline: timeline) + @markers[timeline] = current_user.markers.find_or_initialize_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 dbfd7e103a..555485823c 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: [:account_stat, :user]) + @paginated_mutes ||= Mute.eager_load(:target_account) .joins(:target_account) .merge(Account.without_suspended) .where(account: current_account) @@ -28,6 +28,10 @@ class Api::V1::MutesController < Api::BaseController ) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_mutes_url pagination_params(max_id: pagination_max_id) if records_continue? end @@ -36,8 +40,12 @@ class Api::V1::MutesController < Api::BaseController api_v1_mutes_url pagination_params(since_id: pagination_since_id) unless paginated_mutes.empty? end - def pagination_collection - paginated_mutes + def pagination_max_id + paginated_mutes.last.id + end + + def pagination_since_id + paginated_mutes.first.id end def records_continue? diff --git a/app/controllers/api/v1/notifications/policies_controller.rb b/app/controllers/api/v1/notifications/policies_controller.rb deleted file mode 100644 index 1ec336f9a5..0000000000 --- a/app/controllers/api/v1/notifications/policies_controller.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Notifications::PoliciesController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, only: :show - before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: :update - - before_action :require_user! - before_action :set_policy - - def show - render json: @policy, serializer: REST::NotificationPolicySerializer - end - - def update - @policy.update!(resource_params) - render json: @policy, serializer: REST::NotificationPolicySerializer - end - - private - - def set_policy - @policy = NotificationPolicy.find_or_initialize_by(account: current_account) - - with_read_replica do - @policy.summarize! - end - end - - def resource_params - params.permit( - :filter_not_following, - :filter_not_followers, - :filter_new_accounts, - :filter_private_mentions - ) - end -end diff --git a/app/controllers/api/v1/notifications/requests_controller.rb b/app/controllers/api/v1/notifications/requests_controller.rb deleted file mode 100644 index 0e58379a38..0000000000 --- a/app/controllers/api/v1/notifications/requests_controller.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Notifications::RequestsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, only: :index - before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, except: :index - - before_action :require_user! - before_action :set_request, except: :index - - after_action :insert_pagination_headers, only: :index - - def index - with_read_replica do - @requests = load_requests - @relationships = relationships - end - - render json: @requests, each_serializer: REST::NotificationRequestSerializer, relationships: @relationships - end - - def show - render json: @request, serializer: REST::NotificationRequestSerializer - end - - def accept - AcceptNotificationRequestService.new.call(@request) - render_empty - end - - def dismiss - @request.update!(dismissed: true) - render_empty - end - - private - - def load_requests - requests = NotificationRequest.where(account: current_account).where(dismissed: truthy_param?(:dismissed) || false).includes(:last_status, from_account: [:account_stat, :user]).to_a_paginated_by_id( - limit_param(DEFAULT_ACCOUNTS_LIMIT), - params_slice(:max_id, :since_id, :min_id) - ) - - NotificationRequest.preload_cache_collection(requests) do |statuses| - preload_collection(statuses, Status) - end - end - - def relationships - StatusRelationshipsPresenter.new(@requests.map(&:last_status), current_user&.account_id) - end - - def set_request - @request = NotificationRequest.where(account: current_account).find(params[:id]) - end - - def next_path - api_v1_notifications_requests_url pagination_params(max_id: pagination_max_id) unless @requests.empty? - end - - def prev_path - api_v1_notifications_requests_url pagination_params(min_id: pagination_since_id) unless @requests.empty? - end - - def pagination_max_id - @requests.last.id - end - - def pagination_since_id - @requests.first.id - end - - def pagination_params(core_params) - params.slice(:dismissed).permit(:dismissed).merge(core_params) - end -end diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index 1d0aa10d2e..406ab97538 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -41,7 +41,7 @@ class Api::V1::NotificationsController < Api::BaseController ) Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses| - preload_collection(target_statuses, Status) + cache_collection(target_statuses, Status) end end @@ -49,8 +49,7 @@ class Api::V1::NotificationsController < Api::BaseController current_account.notifications.without_suspended.browserable( types: Array(browserable_params[:types]), exclude_types: Array(browserable_params[:exclude_types]), - from_account_id: browserable_params[:account_id], - include_filtered: truthy_param?(:include_filtered) + from_account_id: browserable_params[:account_id] ) end @@ -58,6 +57,10 @@ class Api::V1::NotificationsController < Api::BaseController @notifications.reject { |notification| notification.target_status.nil? }.map(&:target_status) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_notifications_url pagination_params(max_id: pagination_max_id) unless @notifications.empty? end @@ -66,15 +69,19 @@ class Api::V1::NotificationsController < Api::BaseController api_v1_notifications_url pagination_params(min_id: pagination_since_id) unless @notifications.empty? end - def pagination_collection - @notifications + def pagination_max_id + @notifications.last.id + end + + def pagination_since_id + @notifications.first.id end def browserable_params - params.permit(:account_id, :include_filtered, types: [], exclude_types: []) + params.permit(:account_id, types: [], exclude_types: []) end def pagination_params(core_params) - params.slice(:limit, :account_id, :types, :exclude_types, :include_filtered).permit(:limit, :account_id, :include_filtered, types: [], exclude_types: []).merge(core_params) + params.slice(:limit, :account_id, :types, :exclude_types).permit(:limit, :account_id, types: [], exclude_types: []).merge(core_params) end end diff --git a/app/controllers/api/v1/peers/search_controller.rb b/app/controllers/api/v1/peers/search_controller.rb index 1780554c5d..0c503d9bc5 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: normalized_domain, + domain: TagManager.instance.normalize_domain(params[:q].strip), }, }, @@ -37,18 +37,11 @@ class Api::V1::Peers::SearchController < Api::BaseController }, }).limit(10).pluck(:domain) else - domain = normalized_domain - @domains = Instance.searchable.domain_starts_with(domain).limit(10).pluck(:domain) + 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) 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/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb index e1ad89ee3e..3634acf956 100644 --- a/app/controllers/api/v1/push/subscriptions_controller.rb +++ b/app/controllers/api/v1/push/subscriptions_controller.rb @@ -1,12 +1,9 @@ # frozen_string_literal: true class Api::V1::Push::SubscriptionsController < Api::BaseController - include Redisable - include Lockable - before_action -> { doorkeeper_authorize! :push } before_action :require_user! - before_action :set_push_subscription, only: [:show, :update] + before_action :set_push_subscription before_action :check_push_subscription, only: [:show, :update] def show @@ -14,18 +11,16 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController end def create - with_redis_lock("push_subscription:#{current_user.id}") do - destroy_web_push_subscriptions! + @push_subscription&.destroy! - @push_subscription = Web::PushSubscription.create!( - endpoint: subscription_params[:endpoint], - key_p256dh: subscription_params[:keys][:p256dh], - key_auth: subscription_params[:keys][:auth], - data: data_params, - user_id: current_user.id, - access_token_id: doorkeeper_token.id - ) - end + @push_subscription = Web::PushSubscription.create!( + endpoint: subscription_params[:endpoint], + key_p256dh: subscription_params[:keys][:p256dh], + key_auth: subscription_params[:keys][:auth], + data: data_params, + user_id: current_user.id, + access_token_id: doorkeeper_token.id + ) render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer end @@ -36,18 +31,14 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController end def destroy - destroy_web_push_subscriptions! + @push_subscription&.destroy! render_empty end private - def destroy_web_push_subscriptions! - doorkeeper_token.web_push_subscriptions.destroy_all - end - def set_push_subscription - @push_subscription = doorkeeper_token.web_push_subscriptions.first + @push_subscription = Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id) end def check_push_subscription diff --git a/app/controllers/api/v1/reaction_deck_controller.rb b/app/controllers/api/v1/reaction_deck_controller.rb deleted file mode 100644 index 6229eb89d4..0000000000 --- a/app/controllers/api/v1/reaction_deck_controller.rb +++ /dev/null @@ -1,92 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::ReactionDeckController < Api::BaseController - include RoutingHelper - - before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:index] - before_action -> { doorkeeper_authorize! :write, :'write:lists' }, only: [:create] - - before_action :require_user! - before_action :set_deck, only: [:index, :create] - - rescue_from ArgumentError do |e| - render json: { error: e.to_s }, status: 422 - end - - def index - render json: remove_metas(@deck) - end - - def create - deck = [] - - shortcodes = [] - (deck_params['emojis'] || []).each do |shortcode| - shortcodes << shortcode.delete(':') - break if shortcodes.length >= User::REACTION_DECK_MAX - end - - custom_emojis = CustomEmoji.where(shortcode: shortcodes, domain: nil) - - shortcodes.each do |shortcode| - custom_emoji = custom_emojis.find { |em| em.shortcode == shortcode } - - emoji_data = {} - - if custom_emoji - emoji_data['name'] = custom_emoji.shortcode - emoji_data['url'] = full_asset_url(custom_emoji.image.url) - emoji_data['static_url'] = full_asset_url(custom_emoji.image.url(:static)) - emoji_data['width'] = custom_emoji.image_width - emoji_data['height'] = custom_emoji.image_height - emoji_data['custom_emoji_id'] = custom_emoji.id - else - emoji_data['name'] = shortcode - end - - deck << emoji_data - end - - current_user.settings['reaction_deck'] = deck.to_json - current_user.save! - - render json: remove_metas(deck) - end - - private - - def set_deck - deck = current_user.setting_reaction_deck ? JSON.parse(current_user.setting_reaction_deck) : [] - @deck = remove_unused_custom_emojis(deck) - end - - def remove_unused_custom_emojis(deck) - custom_ids = [] - deck.each do |item| - custom_ids << item['custom_emoji_id'].to_i if item.key?('custom_emoji_id') - end - custom_emojis = CustomEmoji.where(id: custom_ids) - - deck.each do |item| - next if item['custom_emoji_id'].nil? - - custom_emoji = custom_emojis.find { |em| em.id == item['custom_emoji_id'].to_i } - remove = custom_emoji.nil? || custom_emoji.disabled - item['remove'] = remove if remove - end - deck.filter { |item| !item.key?('remove') } - end - - def remove_metas(deck) - deck.tap do |d| - d.each do |item| - item.delete('custom_emoji_id') - # item.delete('id') if item.key?('id') - end - end - end - - def deck_params - params - end -end diff --git a/app/controllers/api/v1/scheduled_statuses_controller.rb b/app/controllers/api/v1/scheduled_statuses_controller.rb index 1217ed014e..2220b6d22e 100644 --- a/app/controllers/api/v1/scheduled_statuses_controller.rb +++ b/app/controllers/api/v1/scheduled_statuses_controller.rb @@ -47,6 +47,10 @@ class Api::V1::ScheduledStatusesController < Api::BaseController params.slice(:limit).permit(:limit).merge(core_params) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_scheduled_statuses_url pagination_params(max_id: pagination_max_id) if records_continue? end @@ -59,7 +63,11 @@ class Api::V1::ScheduledStatusesController < Api::BaseController @statuses.size == limit_param(DEFAULT_STATUSES_LIMIT) end - def pagination_collection - @statuses + def pagination_max_id + @statuses.last.id + end + + def pagination_since_id + @statuses.first.id end end diff --git a/app/controllers/api/v1/statuses/bookmark_categories_controller.rb b/app/controllers/api/v1/statuses/bookmark_categories_controller.rb deleted file mode 100644 index 9d65b96296..0000000000 --- a/app/controllers/api/v1/statuses/bookmark_categories_controller.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Statuses::BookmarkCategoriesController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:lists' } - before_action :require_user! - before_action :set_status - - def index - @statuses = @status.deleted_at.present? ? [] : @status.joined_bookmark_categories.where(account: current_account) - render json: @statuses, each_serializer: REST::BookmarkCategorySerializer - end - - private - - def set_status - @status = Status.find(params[:status_id]) - end -end diff --git a/app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_controller.rb b/app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_controller.rb deleted file mode 100644 index 9c2fb3d4a5..0000000000 --- a/app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_controller.rb +++ /dev/null @@ -1,76 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Statuses::EmojiReactionedByAccountsController < Api::BaseController - include Authorization - - before_action -> { authorize_if_got_token! :read, :'read:accounts' } - before_action :set_status - after_action :insert_pagination_headers - - def index - @accounts = load_accounts - render json: @accounts, each_serializer: REST::EmojiReactionAccountSerializer - end - - private - - def load_accounts - return [] unless Setting.enable_emoji_reaction - return [] if current_account.nil? && @status.account.emoji_reaction_policy != :allow - return [] if current_account.present? && !@status.account.show_emoji_reaction?(current_account) - - scope = default_accounts - scope = scope.where.not(account_id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? - scope.merge(paginated_emoji_reactions).to_a - end - - def default_accounts - EmojiReaction - .where(status_id: @status.id) - .includes(:account) - .where(account: { suspended_at: nil }) - end - - def paginated_emoji_reactions - EmojiReaction.paginate_by_max_id( - limit_param(DEFAULT_ACCOUNTS_LIMIT), - params[:max_id], - params[:since_id] - ) - end - - def insert_pagination_headers - set_pagination_headers(next_path, prev_path) - end - - def next_path - api_v1_status_emoji_reactioned_by_index_url pagination_params(max_id: pagination_max_id) if records_continue? - end - - def prev_path - api_v1_status_emoji_reactioned_by_index_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? - end - - def pagination_max_id - @accounts.last.id - end - - def pagination_since_id - @accounts.first.id - end - - def records_continue? - @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) - end - - def set_status - @status = Status.find(params[:status_id]) - authorize @status, :show? - rescue Mastodon::NotPermittedError - not_found - end - - def pagination_params(core_params) - params.slice(:limit).permit(:limit).merge(core_params) - end -end diff --git a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb deleted file mode 100644 index 1f103beb71..0000000000 --- a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Statuses::EmojiReactionsController < Api::BaseController - include Authorization - - before_action -> { doorkeeper_authorize! :write, :'write:favourites' } - before_action :require_user! - before_action :set_status, only: %i(create update) - before_action :set_status_without_authorize, only: [:destroy] - - def create - create_private(params[:emoji] || params[:id]) - end - - # For compatible with Fedibird API - def update - create_private(params[:id]) - end - - def destroy - emoji = params[:emoji] || params[:id] - - if emoji - shortcode, domain = emoji.split('@') - emoji_reaction = EmojiReaction.where(account_id: current_account.id).where(status_id: @status.id).where(name: shortcode) - .find { |reaction| domain == '' ? reaction.custom_emoji.nil? : reaction.custom_emoji&.domain == domain } - - authorize @status, :show? if emoji_reaction.nil? - - UnEmojiReactService.new.call(current_account, @status, emoji_reaction) if emoji_reaction.present? - else - authorize @status, :show? - end - - render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new( - [@status], current_account.id - ) - rescue Mastodon::NotPermittedError - not_found - end - - private - - def create_private(emoji) - count = EmojiReaction.where(account: current_account, status: @status).count - raise Mastodon::ValidationError, I18n.t('reactions.errors.limit_reached') if count >= EmojiReaction::EMOJI_REACTION_PER_ACCOUNT_LIMIT - raise Mastodon::ValidationError, I18n.t('reactions.errors.disabled') unless Setting.enable_emoji_reaction - - EmojiReactService.new.call(current_account, @status, emoji) - render json: @status, serializer: REST::StatusSerializer - end - - def set_status - set_status_without_authorize - authorize @status, :show? - rescue Mastodon::NotPermittedError - not_found - end - - def set_status_without_authorize - @status = Status.find(params[:status_id]) - 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 bbc8082e0c..3cca246ce8 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.not_excluded_by_account(current_account) unless current_account.nil? + scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? scope.merge(paginated_favourites).to_a end def default_accounts Account .without_suspended - .includes(:favourites, :account_stat, :user) + .includes(:favourites, :account_stat) .references(:favourites) .where(favourites: { status_id: @status.id }) end @@ -34,6 +34,10 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::V1::Statuses::Bas ) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_status_favourited_by_index_url pagination_params(max_id: pagination_max_id) if records_continue? end diff --git a/app/controllers/api/v1/statuses/mentioned_accounts_controller.rb b/app/controllers/api/v1/statuses/mentioned_accounts_controller.rb deleted file mode 100644 index 4d905ef1a6..0000000000 --- a/app/controllers/api/v1/statuses/mentioned_accounts_controller.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Statuses::MentionedAccountsController < Api::BaseController - include Authorization - - before_action -> { authorize_if_got_token! :read, :'read:accounts' } - before_action :set_status - after_action :insert_pagination_headers - - def index - cache_if_unauthenticated! - @accounts = load_accounts - render json: @accounts, each_serializer: REST::AccountSerializer - end - - private - - def load_accounts - scope = default_accounts - scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? - scope.merge(paginated_mentioned_users).to_a - end - - def default_accounts - Account - .without_suspended - .includes(:mentions, :account_stat) - .references(:mentions) - .where(mentions: { status_id: @status.id }) - end - - def paginated_mentioned_users - Mention.paginate_by_max_id( - limit_param(DEFAULT_ACCOUNTS_LIMIT), - params[:max_id], - params[:since_id] - ) - end - - def insert_pagination_headers - set_pagination_headers(next_path, prev_path) - end - - def next_path - api_v1_status_mentioned_by_index_url pagination_params(max_id: pagination_max_id) if records_continue? - end - - def prev_path - api_v1_status_mentioned_by_index_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? - end - - def pagination_max_id - @accounts.last.mentions.last.id - end - - def pagination_since_id - @accounts.first.mentions.first.id - end - - def records_continue? - @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) - end - - def set_status - @status = Status.find(params[:status_id]) - authorize @status, :show_mentioned_users? - rescue Mastodon::NotPermittedError - not_found - end - - def pagination_params(core_params) - params.slice(:limit).permit(:limit).merge(core_params) - end -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 bac96b032b..dd3e60846b 100644 --- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb @@ -14,22 +14,26 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::V1::Statuses::Base def load_accounts scope = default_accounts - scope = scope.not_excluded_by_account(current_account) unless current_account.nil? + scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? scope.merge(paginated_statuses).to_a end def default_accounts - Account.without_suspended.includes(:statuses, :account_stat, :user).references(:statuses) + Account.without_suspended.includes(:statuses, :account_stat).references(:statuses) end def paginated_statuses - Status.where(reblog_of_id: @status.id).distributable_visibility.paginate_by_max_id( + Status.where(reblog_of_id: @status.id).where(visibility: [:public, :unlisted]).paginate_by_max_id( limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id] ) end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def next_path api_v1_status_reblogged_by_index_url pagination_params(max_id: pagination_max_id) if records_continue? end diff --git a/app/controllers/api/v1/statuses/referred_by_statuses_controller.rb b/app/controllers/api/v1/statuses/referred_by_statuses_controller.rb deleted file mode 100644 index c13c5ff0e8..0000000000 --- a/app/controllers/api/v1/statuses/referred_by_statuses_controller.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Statuses::ReferredByStatusesController < Api::BaseController - include Authorization - - before_action -> { authorize_if_got_token! :read, :'read:accounts' } - before_action :set_status - after_action :insert_pagination_headers - - def index - @statuses = load_statuses - render json: @statuses, each_serializer: REST::StatusSerializer, - relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), - emoji_reaction_permitted_account_ids: EmojiReactionAccountsPresenter.new(@statuses, current_user&.account_id) - end - - private - - def load_statuses - cached_references - end - - def cached_references - results - end - - def results - return @results if @results - - account = current_user&.account - statuses = Status.where(id: @status.referenced_by_status_objects.select(:status_id)) - account_ids = statuses.map(&:account_id).uniq - domains = statuses.filter_map(&:account_domain).uniq - relations = account&.relations_map(account_ids, domains) || {} - - statuses = preload_collection_paginated_by_id( - statuses, - Status, - limit_param(DEFAULT_STATUSES_LIMIT), - params_slice(:max_id, :since_id, :min_id) - ) - - @results = statuses.filter { |status| !StatusFilter.new(status, account, relations).filtered? } - end - - def insert_pagination_headers - set_pagination_headers(next_path, prev_path) - end - - def next_path - api_v1_status_referred_by_index_url pagination_params(max_id: pagination_max_id) if records_continue? - end - - def prev_path - api_v1_status_referred_by_index_url pagination_params(min_id: pagination_since_id) unless results.empty? - end - - def pagination_max_id - results.last.id - end - - def pagination_since_id - results.first.id - end - - def records_continue? - results.size == limit_param(DEFAULT_STATUSES_LIMIT) - end - - def set_status - @status = Status.find(params[:status_id]) - authorize @status, :show? - rescue Mastodon::NotPermittedError - not_found - end - - def pagination_params(core_params) - params.slice(:limit).permit(:limit).merge(core_params) - end -end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index e8636f2138..064e7632a8 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -5,11 +5,9 @@ class Api::V1::StatusesController < Api::BaseController before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy] before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy] - before_action :require_user!, except: [:index, :show, :context] - before_action :set_statuses, only: [:index] - before_action :set_status, only: [:show, :context] - before_action :set_thread, only: [:create] - before_action :check_statuses_limit, only: [:index] + before_action :require_user!, except: [:show, :context] + before_action :set_status, only: [:show, :context] + before_action :set_thread, only: [:create] override_rate_limit_headers :create, family: :statuses override_rate_limit_headers :update, family: :statuses @@ -25,14 +23,9 @@ class Api::V1::StatusesController < Api::BaseController DESCENDANTS_LIMIT = 60 DESCENDANTS_DEPTH_LIMIT = 20 - def index - @statuses = preload_collection(@statuses, Status) - render json: @statuses, each_serializer: REST::StatusSerializer - end - def show cache_if_unauthenticated! - @status = preload_collection([@status], Status).first + @status = cache_collection([@status], Status).first render json: @status, serializer: REST::StatusSerializer end @@ -51,20 +44,11 @@ class Api::V1::StatusesController < Api::BaseController ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(ancestors_limit, current_account) descendants_results = @status.descendants(descendants_limit, current_account, descendants_depth_limit) - references_results = @status.readable_references(current_account) - loaded_ancestors = preload_collection(ancestors_results, Status) - loaded_descendants = preload_collection(descendants_results, Status) - loaded_references = preload_collection(references_results, Status) + loaded_ancestors = cache_collection(ancestors_results, Status) + loaded_descendants = cache_collection(descendants_results, Status) - if params[:with_reference] - loaded_references.reject! { |status| loaded_ancestors.any? { |ancestor| ancestor.id == status.id } } - else - loaded_ancestors = (loaded_ancestors + loaded_references).uniq(&:id) - loaded_references = [] - end - - @context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants, references: loaded_references) - statuses = [@status] + @context.ancestors + @context.descendants + @context.references + @context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants) + statuses = [@status] + @context.ancestors + @context.descendants render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id) end @@ -77,11 +61,7 @@ class Api::V1::StatusesController < Api::BaseController media_ids: status_params[:media_ids], sensitive: status_params[:sensitive], spoiler_text: status_params[:spoiler_text], - markdown: status_params[:markdown], visibility: status_params[:visibility], - force_visibility: status_params[:force_visibility], - searchability: status_params[:searchability], - circle_id: status_params[:circle_id], language: status_params[:language], scheduled_at: status_params[:scheduled_at], application: doorkeeper_token.application, @@ -91,9 +71,13 @@ class Api::V1::StatusesController < Api::BaseController with_rate_limit: true ) - render json: @status, serializer: serializer_for_status + render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer rescue PostStatusService::UnexpectedMentionsError => e - render json: unexpected_accounts_error_json(e), status: 422 + unexpected_accounts = ActiveModel::Serializer::CollectionSerializer.new( + e.accounts, + serializer: REST::AccountSerializer + ) + render json: { error: e.message, unexpected_accounts: unexpected_accounts }, status: 422 end def update @@ -109,7 +93,6 @@ class Api::V1::StatusesController < Api::BaseController sensitive: status_params[:sensitive], language: status_params[:language], spoiler_text: status_params[:spoiler_text], - markdown: status_params[:markdown], poll: status_params[:poll] ) @@ -132,10 +115,6 @@ class Api::V1::StatusesController < Api::BaseController private - def set_statuses - @statuses = Status.permitted_statuses_from_ids(status_ids, current_account) - end - def set_status @status = Status.find(params[:id]) authorize @status, :show? @@ -150,18 +129,6 @@ class Api::V1::StatusesController < Api::BaseController render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404 end - def check_statuses_limit - raise(Mastodon::ValidationError) if status_ids.size > DEFAULT_STATUSES_LIMIT - end - - def status_ids - Array(statuses_params[:ids]).uniq.map(&:to_i) - end - - def statuses_params - params.permit(ids: []) - end - def status_params params.permit( :status, @@ -169,13 +136,8 @@ class Api::V1::StatusesController < Api::BaseController :sensitive, :spoiler_text, :visibility, - :force_visibility, - :searchability, - :circle_id, :language, - :markdown, :scheduled_at, - :status_reference_ids, allowed_mentions: [], media_ids: [], media_attributes: [ @@ -193,21 +155,6 @@ class Api::V1::StatusesController < Api::BaseController ) end - def serializer_for_status - @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer - end - - def unexpected_accounts_error_json(error) - { - error: error.message, - unexpected_accounts: serialized_accounts(error.accounts), - } - end - - def serialized_accounts(accounts) - ActiveModel::Serializer::CollectionSerializer.new(accounts, serializer: REST::AccountSerializer) - end - def pagination_params(core_params) params.slice(:limit).permit(:limit).merge(core_params) end diff --git a/app/controllers/api/v1/timelines/antenna_controller.rb b/app/controllers/api/v1/timelines/antenna_controller.rb deleted file mode 100644 index 69554361be..0000000000 --- a/app/controllers/api/v1/timelines/antenna_controller.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Timelines::AntennaController < Api::V1::Timelines::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:lists' } - before_action :require_user! - before_action :set_antenna - before_action :set_statuses - - PERMITTED_PARAMS = %i(limit).freeze - - def show - render json: @statuses, - each_serializer: REST::StatusSerializer, - relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id) - end - - private - - def set_antenna - @antenna = Antenna.where(account: current_account).find(params[:id]) - end - - def set_statuses - @statuses = cached_list_statuses - end - - def cached_list_statuses - preload_collection list_statuses, Status - end - - def list_statuses - list_feed.get( - limit_param(DEFAULT_STATUSES_LIMIT), - params[:max_id], - params[:since_id], - params[:min_id] - ) - end - - def list_feed - AntennaFeed.new(@antenna) - end - - def next_path - api_v1_timelines_antenna_url params[:id], next_path_params - end - - def prev_path - api_v1_timelines_antenna_url params[:id], prev_path_params - end -end diff --git a/app/controllers/api/v1/timelines/base_controller.rb b/app/controllers/api/v1/timelines/base_controller.rb index e79eba79ee..173e173cc9 100644 --- a/app/controllers/api/v1/timelines/base_controller.rb +++ b/app/controllers/api/v1/timelines/base_controller.rb @@ -5,8 +5,16 @@ class Api::V1::Timelines::BaseController < Api::BaseController private - def pagination_collection - @statuses + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def pagination_max_id + @statuses.last.id + end + + def pagination_since_id + @statuses.first.id end def next_path_params diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb index e48c5ae251..36fdbea647 100644 --- a/app/controllers/api/v1/timelines/home_controller.rb +++ b/app/controllers/api/v1/timelines/home_controller.rb @@ -10,24 +10,22 @@ class Api::V1::Timelines::HomeController < Api::V1::Timelines::BaseController with_read_replica do @statuses = load_statuses @relationships = StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) - @emoji_reactions = EmojiReactionAccountsPresenter.new(@statuses, current_user&.account_id) end render json: @statuses, each_serializer: REST::StatusSerializer, relationships: @relationships, - emoji_reaction_permitted_account_ids: @emoji_reactions, status: account_home_feed.regenerating? ? 206 : 200 end private def load_statuses - preloaded_home_statuses + cached_home_statuses end - def preloaded_home_statuses - preload_collection home_statuses, Status + def cached_home_statuses + cache_collection home_statuses, Status end def home_statuses diff --git a/app/controllers/api/v1/timelines/list_controller.rb b/app/controllers/api/v1/timelines/list_controller.rb index d8cdbdb74c..14b884ecd9 100644 --- a/app/controllers/api/v1/timelines/list_controller.rb +++ b/app/controllers/api/v1/timelines/list_controller.rb @@ -21,11 +21,11 @@ class Api::V1::Timelines::ListController < Api::V1::Timelines::BaseController end def set_statuses - @statuses = preloaded_list_statuses + @statuses = cached_list_statuses end - def preloaded_list_statuses - preload_collection list_statuses, Status + def cached_list_statuses + cache_collection list_statuses, Status end def list_statuses diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index cb56d309d4..35af8dc4b5 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -8,9 +8,7 @@ class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController def show cache_if_unauthenticated! @statuses = load_statuses - render json: @statuses, each_serializer: REST::StatusSerializer, - relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), - emoji_reaction_permitted_account_ids: EmojiReactionAccountsPresenter.new(@statuses, current_user&.account_id) + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end private @@ -20,11 +18,11 @@ class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController end def load_statuses - preloaded_public_statuses_page + cached_public_statuses_page end - def preloaded_public_statuses_page - preload_collection(public_statuses, Status) + def cached_public_statuses_page + cache_collection(public_statuses, Status) end def public_statuses diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb index 761a4299b8..4ba439dbb2 100644 --- a/app/controllers/api/v1/timelines/tag_controller.rb +++ b/app/controllers/api/v1/timelines/tag_controller.rb @@ -9,9 +9,7 @@ class Api::V1::Timelines::TagController < Api::V1::Timelines::BaseController def show cache_if_unauthenticated! @statuses = load_statuses - render json: @statuses, each_serializer: REST::StatusSerializer, - relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), - emoji_reaction_permitted_account_ids: EmojiReactionAccountsPresenter.new(@statuses, current_user&.account_id) + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end private @@ -25,11 +23,11 @@ class Api::V1::Timelines::TagController < Api::V1::Timelines::BaseController end def load_statuses - preloaded_tagged_statuses + cached_tagged_statuses end - def preloaded_tagged_statuses - @tag.nil? ? [] : preload_collection(tag_timeline_statuses, Status) + def cached_tagged_statuses + @tag.nil? ? [] : cache_collection(tag_timeline_statuses, Status) end def tag_timeline_statuses diff --git a/app/controllers/api/v1/trends/links_controller.rb b/app/controllers/api/v1/trends/links_controller.rb index 8edf5bbcef..57cfa0b7e4 100644 --- a/app/controllers/api/v1/trends/links_controller.rb +++ b/app/controllers/api/v1/trends/links_controller.rb @@ -34,6 +34,10 @@ class Api::V1::Trends::LinksController < Api::BaseController scope end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def pagination_params(core_params) params.slice(:limit).permit(:limit).merge(core_params) end diff --git a/app/controllers/api/v1/trends/statuses_controller.rb b/app/controllers/api/v1/trends/statuses_controller.rb index c6fbbce167..c186864c3b 100644 --- a/app/controllers/api/v1/trends/statuses_controller.rb +++ b/app/controllers/api/v1/trends/statuses_controller.rb @@ -20,7 +20,7 @@ class Api::V1::Trends::StatusesController < Api::BaseController def set_statuses @statuses = if enabled? - preload_collection(statuses_from_trends.offset(offset_param).limit(limit_param(DEFAULT_STATUSES_LIMIT)), Status) + cache_collection(statuses_from_trends.offset(offset_param).limit(limit_param(DEFAULT_STATUSES_LIMIT)), Status) else [] end @@ -32,6 +32,10 @@ class Api::V1::Trends::StatusesController < Api::BaseController scope end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def pagination_params(core_params) params.slice(:limit).permit(:limit).merge(core_params) end diff --git a/app/controllers/api/v1/trends/tags_controller.rb b/app/controllers/api/v1/trends/tags_controller.rb index 6d3855a90a..aca3dd7089 100644 --- a/app/controllers/api/v1/trends/tags_controller.rb +++ b/app/controllers/api/v1/trends/tags_controller.rb @@ -30,6 +30,10 @@ class Api::V1::Trends::TagsController < Api::BaseController Trends.tags.query.allowed end + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + def pagination_params(core_params) params.slice(:limit).permit(:limit).merge(core_params) end diff --git a/app/controllers/api/v2/filters_controller.rb b/app/controllers/api/v2/filters_controller.rb index 0b63caa36a..2fcdeeae45 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, :statuses) + @filters = current_account.custom_filters.includes(:keywords) end def set_filter @@ -43,6 +43,6 @@ class Api::V2::FiltersController < Api::BaseController end def resource_params - params.permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :with_quote, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy]) + params.permit(:title, :expires_in, :filter_action, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy]) end end diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb index a701cbe582..3cfc6e7919 100644 --- a/app/controllers/api/v2/search_controller.rb +++ b/app/controllers/api/v2/search_controller.rb @@ -3,7 +3,7 @@ class Api::V2::SearchController < Api::BaseController include Authorization - RESULTS_LIMIT = 40 + RESULTS_LIMIT = 20 before_action -> { authorize_if_got_token! :read, :'read:search' } before_action :validate_search_params! @@ -63,6 +63,6 @@ class Api::V2::SearchController < Api::BaseController end def search_params - params.permit(:type, :offset, :min_id, :max_id, :account_id, :following, :searchability) + params.permit(:type, :offset, :min_id, :max_id, :account_id, :following) end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 66e0f7e305..5f8725f6fc 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -9,7 +9,6 @@ class ApplicationController < ActionController::Base include UserTrackingConcern include SessionTrackingConcern include CacheConcern - include PreloadingConcern include DomainControlHelper include DatabaseHelper include AuthorizedFetchHelper @@ -130,7 +129,7 @@ class ApplicationController < ActionController::Base end def single_user_mode? - @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.without_internal.exists? + @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists? end def use_seamless_external_login? @@ -179,7 +178,7 @@ class ApplicationController < ActionController::Base respond_to do |format| format.any { render 'errors/self_destruct', layout: 'auth', status: 410, formats: [:html] } - format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[410] }, status: 410 } + format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[410] }, status: code } end end diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb index a99dceeb25..d9cd630905 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -2,13 +2,12 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController include Auth::CaptchaConcern - include RegistrationLimitationHelper layout 'auth' before_action :set_body_classes before_action :set_confirmation_user!, only: [:show, :confirm_captcha] - before_action :redirect_confirmed_user, if: :signed_in_confirmed_user? + before_action :require_unconfirmed! before_action :extend_csp_for_captcha!, only: [:show, :confirm_captcha] before_action :require_captcha_if_needed!, only: [:show] @@ -17,11 +16,6 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController skip_before_action :require_functional! def show - if reach_registrations_limit? && !current_user&.valid_invitation? - render :limitation_error - return - end - old_session_values = session.to_hash reset_session session.update old_session_values.except('session_id') @@ -71,12 +65,10 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController @confirmation_user.nil? || @confirmation_user.confirmed? 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? + 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 end def set_body_classes diff --git a/app/controllers/auth/omniauth_callbacks_controller.rb b/app/controllers/auth/omniauth_callbacks_controller.rb index 9d496220a3..707b50ef9e 100644 --- a/app/controllers/auth/omniauth_callbacks_controller.rb +++ b/app/controllers/auth/omniauth_callbacks_controller.rb @@ -7,7 +7,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController def self.provides_callback_for(provider) define_method provider do @provider = provider - @user = User.find_for_omniauth(request.env['omniauth.auth'], current_user) + @user = User.find_for_oauth(request.env['omniauth.auth'], current_user) if @user.persisted? record_login_activity @@ -17,9 +17,6 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController session["devise.#{provider}_data"] = request.env['omniauth.auth'] redirect_to new_user_registration_url end - rescue ActiveRecord::RecordInvalid - flash[:alert] = I18n.t('devise.failure.omniauth_user_creation_failure') if is_navigational_format? - redirect_to new_user_session_url end end diff --git a/app/controllers/auth/passwords_controller.rb b/app/controllers/auth/passwords_controller.rb index de001f062b..a752194d5b 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 :redirect_invalid_reset_token, only: :edit, unless: :reset_password_token_is_valid? + before_action :check_validity_of_reset_password_token, only: :edit before_action :set_body_classes layout 'auth' @@ -19,9 +19,11 @@ class Auth::PasswordsController < Devise::PasswordsController private - def redirect_invalid_reset_token - flash[:error] = I18n.t('auth.invalid_reset_password_token') - redirect_to new_password_path(resource_name) + 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 end def set_body_classes diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index 6ed7b2baac..148ad53755 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -1,10 +1,6 @@ # 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! @@ -134,23 +130,9 @@ 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) @@ -181,16 +163,5 @@ 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.get("2fa_failure_notification:#{user.id}").present? - - redis.set("2fa_failure_notification:#{user.id}", '1', ex: 1.hour) - - 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/api/error_handling.rb b/app/controllers/concerns/api/error_handling.rb deleted file mode 100644 index ad559fe2d7..0000000000 --- a/app/controllers/concerns/api/error_handling.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true - -module Api::ErrorHandling - extend ActiveSupport::Concern - - included do - rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e| - render json: { error: e.to_s }, status: 422 - end - - rescue_from ActiveRecord::RecordNotUnique do - render json: { error: 'Duplicate record' }, status: 422 - end - - rescue_from Date::Error do - render json: { error: 'Invalid date supplied' }, status: 422 - end - - rescue_from ActiveRecord::RecordNotFound do - render json: { error: 'Record not found' }, status: 404 - end - - rescue_from HTTP::Error, Mastodon::UnexpectedResponseError do - render json: { error: 'Remote data could not be fetched' }, status: 503 - end - - rescue_from OpenSSL::SSL::SSLError do - render json: { error: 'Remote SSL certificate could not be verified' }, status: 503 - end - - rescue_from Mastodon::NotPermittedError do - render json: { error: 'This action is not allowed' }, status: 403 - end - - rescue_from Seahorse::Client::NetworkingError do |e| - Rails.logger.warn "Storage server error: #{e}" - render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503 - end - - rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight do - render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503 - end - - rescue_from Mastodon::RateLimitExceededError do - render json: { error: I18n.t('errors.429') }, status: 429 - end - - rescue_from ActionController::ParameterMissing, Mastodon::InvalidParameterError do |e| - render json: { error: e.to_s }, status: 400 - end - end -end diff --git a/app/controllers/concerns/api/pagination.rb b/app/controllers/concerns/api/pagination.rb deleted file mode 100644 index d84a1d99f7..0000000000 --- a/app/controllers/concerns/api/pagination.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -module Api::Pagination - extend ActiveSupport::Concern - - protected - - def pagination_max_id - pagination_collection.last.id - end - - def pagination_since_id - pagination_collection.first.id - end - - def set_pagination_headers(next_path = nil, prev_path = nil) - links = [] - links << [next_path, [%w(rel next)]] if next_path - links << [prev_path, [%w(rel prev)]] if prev_path - response.headers['Link'] = LinkHeader.new(links) unless links.empty? - 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 - - private - - def insert_pagination_headers - set_pagination_headers(next_path, prev_path) - end - - def pagination_options_invalid? - params.slice(:limit, :offset).values.map(&:to_i).any?(&:negative?) - 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 404164751a..effdb8d21c 100644 --- a/app/controllers/concerns/auth/two_factor_authentication_concern.rb +++ b/app/controllers/concerns/auth/two_factor_authentication_concern.rb @@ -66,11 +66,6 @@ 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 3dc0ea840a..62f763fe2f 100644 --- a/app/controllers/concerns/cache_concern.rb +++ b/app/controllers/concerns/cache_concern.rb @@ -28,16 +28,6 @@ module CacheConcern def render_with_cache(**options) raise ArgumentError, 'Only JSON render calls are supported' unless options.key?(:json) || block_given? - if options.delete(:cancel_cache) - if block_given? - options[:json] = yield - elsif options[:json].is_a?(Symbol) - options[:json] = send(options[:json]) - end - - return render(options) - end - key = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields].nil? ? nil : options[:fields].join(',')].compact.join(':') expires_in = options.delete(:expires_in) || 3.minutes body = Rails.cache.read(key, raw: true) @@ -55,4 +45,28 @@ module CacheConcern Rails.cache.write(key, response.body, expires_in: expires_in, raw: true) end end + + def cache_collection(raw, klass) + return raw unless klass.respond_to?(:with_includes) + + raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation) + return [] if raw.empty? + + cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id) + + uncached_ids = raw.map(&:id) - cached_keys_with_value.keys + + klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!) + + unless uncached_ids.empty? + uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id) + 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] } + end + + def cache_collection_paginated_by_id(raw, klass, limit, options) + cache_collection raw.cache_ids.to_a_paginated_by_id(limit, options), klass + end end diff --git a/app/controllers/concerns/preloading_concern.rb b/app/controllers/concerns/preloading_concern.rb deleted file mode 100644 index 61e2213649..0000000000 --- a/app/controllers/concerns/preloading_concern.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module PreloadingConcern - extend ActiveSupport::Concern - - def preload_collection(scope, klass) - return scope unless klass.respond_to?(:preload_cacheable_associations) - - scope.to_a.tap do |records| - klass.preload_cacheable_associations(records) - end - end - - def preload_collection_paginated_by_id(scope, klass, limit, options) - preload_collection scope.to_a_paginated_by_id(limit, options), klass - end -end diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 68f09ee023..35391e64c4 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -12,6 +12,39 @@ module SignatureVerification class SignatureVerificationError < StandardError; end + class SignatureParamsParser < Parslet::Parser + rule(:token) { match("[0-9a-zA-Z!#$%&'*+.^_`|~-]").repeat(1).as(:token) } + rule(:quoted_string) { str('"') >> (qdtext | quoted_pair).repeat.as(:quoted_string) >> str('"') } + # qdtext and quoted_pair are not exactly according to spec but meh + rule(:qdtext) { match('[^\\\\"]') } + rule(:quoted_pair) { str('\\') >> any } + rule(:bws) { match('\s').repeat } + rule(:param) { (token.as(:key) >> bws >> str('=') >> bws >> (token | quoted_string).as(:value)).as(:param) } + rule(:comma) { bws >> str(',') >> bws } + # Old versions of node-http-signature add an incorrect "Signature " prefix to the header + rule(:buggy_prefix) { str('Signature ') } + rule(:params) { buggy_prefix.maybe >> (param >> (comma >> param).repeat).as(:params) } + root(:params) + end + + class SignatureParamsTransformer < Parslet::Transform + rule(params: subtree(:param)) do + (param.is_a?(Array) ? param : [param]).each_with_object({}) { |(key, value), hash| hash[key] = value } + end + + rule(param: { key: simple(:key), value: simple(:val) }) do + [key, val] + end + + rule(quoted_string: simple(:string)) do + string.to_s + end + + rule(token: simple(:string)) do + string.to_s + end + end + def require_account_signature! render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account end @@ -66,7 +99,7 @@ module SignatureVerification compare_signed_string = build_signed_string(include_query_string: false) return actor unless verify_signature(actor, signature, compare_signed_string).nil? - actor = stoplight_wrapper.run { actor_refresh_key!(actor) } + actor = stoplight_wrap_request { actor_refresh_key!(actor) } raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil? @@ -102,8 +135,12 @@ module SignatureVerification end def signature_params - @signature_params ||= SignatureParser.parse(request.headers['Signature']) - rescue SignatureParser::ParsingError + @signature_params ||= begin + raw_signature = request.headers['Signature'] + tree = SignatureParamsParser.new.parse(raw_signature) + SignatureParamsTransformer.new.apply(tree) + end + rescue Parslet::ParseFailed raise SignatureVerificationError, 'Error parsing signature parameters' end @@ -226,10 +263,10 @@ module SignatureVerification end if key_id.start_with?('acct:') - stoplight_wrapper.run { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) } + stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) } elsif !ActivityPub::TagManager.instance.local_uri?(key_id) account = ActivityPub::TagManager.instance.uri_to_actor(key_id) - account ||= stoplight_wrapper.run { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) } + account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) } account end rescue Mastodon::PrivateNetworkAddressError => e @@ -238,11 +275,12 @@ module SignatureVerification raise SignatureVerificationError, e.message end - def stoplight_wrapper - Stoplight("source:#{request.remote_ip}") + def stoplight_wrap_request(&block) + Stoplight("source:#{request.remote_ip}", &block) .with_threshold(1) .with_cool_off_time(5.minutes.seconds) .with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) } + .run end def actor_refresh_key!(actor) diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb index b8c909877b..5687d6e5b6 100644 --- a/app/controllers/concerns/web_app_controller_concern.rb +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -21,19 +21,10 @@ module WebAppControllerConcern def redirect_unauthenticated_to_permalinks! return if user_signed_in? && current_account.moved_to_account_id.nil? - permalink_redirector = PermalinkRedirector.new(request.path) - return if permalink_redirector.redirect_path.blank? + redirect_path = PermalinkRedirector.new(request.path).redirect_path + return if redirect_path.blank? expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? - - 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 + redirect_to(redirect_path) end end diff --git a/app/controllers/custom_css_controller.rb b/app/controllers/custom_css_controller.rb index eb6417698a..62f8e0d772 100644 --- a/app/controllers/custom_css_controller.rb +++ b/app/controllers/custom_css_controller.rb @@ -16,6 +16,6 @@ class CustomCssController < ActionController::Base # rubocop:disable Rails/Appli helper_method :custom_css_styles def set_user_roles - @user_roles = UserRole.providing_styles + @user_roles = UserRole.where(highlighted: true).where.not(color: [nil, '']) end end diff --git a/app/controllers/filters_controller.rb b/app/controllers/filters_controller.rb index 0b7f9f9e68..bd9964426b 100644 --- a/app/controllers/filters_controller.rb +++ b/app/controllers/filters_controller.rb @@ -49,7 +49,7 @@ class FiltersController < ApplicationController end def resource_params - params.require(:custom_filter).permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :exclude_quote, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy]) + params.require(:custom_filter).permit(:title, :expires_in, :filter_action, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy]) end def set_body_classes diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index 44d90ec671..5effd9495e 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -62,7 +62,7 @@ class FollowerAccountsController < ApplicationController ActivityPub::CollectionPresenter.new( id: account_followers_url(@account, page: params.fetch(:page, 1)), type: :ordered, - size: @account.public_followers_count, + size: @account.followers_count, items: follows.map { |follow| ActivityPub::TagManager.instance.uri_for(follow.account) }, part_of: account_followers_url(@account), next: next_page_url, @@ -72,7 +72,7 @@ class FollowerAccountsController < ApplicationController ActivityPub::CollectionPresenter.new( id: account_followers_url(@account), type: :ordered, - size: @account.public_followers_count, + size: @account.followers_count, first: page_url(1) ) end diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 87a702f711..268fad96d0 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -65,7 +65,7 @@ class FollowingAccountsController < ApplicationController ActivityPub::CollectionPresenter.new( id: account_following_index_url(@account, page: params.fetch(:page, 1)), type: :ordered, - size: @account.public_following_count, + size: @account.following_count, items: follows.map { |follow| ActivityPub::TagManager.instance.uri_for(follow.target_account) }, part_of: account_following_index_url(@account), next: next_page_url, @@ -75,7 +75,7 @@ class FollowingAccountsController < ApplicationController ActivityPub::CollectionPresenter.new( id: account_following_index_url(@account), type: :ordered, - size: @account.public_following_count, + size: @account.following_count, first: page_url(1) ) end diff --git a/app/controllers/instance_actors_controller.rb b/app/controllers/instance_actors_controller.rb index f2b1eaa3e7..8422d74bc3 100644 --- a/app/controllers/instance_actors_controller.rb +++ b/app/controllers/instance_actors_controller.rb @@ -6,8 +6,6 @@ class InstanceActorsController < ActivityPub::BaseController serialization_scope nil before_action :set_account - - skip_before_action :authenticate_user! # From `AccountOwnedConcern` skip_before_action :require_functional! skip_before_action :update_user_sign_in @@ -18,11 +16,6 @@ class InstanceActorsController < ActivityPub::BaseController private - # Skips various `before_action` from `AccountOwnedConcern` - def account_required? - false - end - def set_account @account = Account.representative end diff --git a/app/controllers/intents_controller.rb b/app/controllers/intents_controller.rb index 65c315208d..ea024e30e6 100644 --- a/app/controllers/intents_controller.rb +++ b/app/controllers/intents_controller.rb @@ -1,26 +1,27 @@ # frozen_string_literal: true class IntentsController < ApplicationController - EXPECTED_SCHEME = 'web+mastodon' + before_action :check_uri - before_action :handle_invalid_uri, unless: :valid_uri? rescue_from Addressable::URI::InvalidURIError, with: :handle_invalid_uri def show - case uri.host - when 'follow' - redirect_to authorize_interaction_path(uri: uri.query_values['uri'].delete_prefix('acct:')) - when 'share' - redirect_to share_path(text: uri.query_values['text']) - else - handle_invalid_uri + if uri.scheme == 'web+mastodon' + case uri.host + when 'follow' + return redirect_to authorize_interaction_path(uri: uri.query_values['uri'].delete_prefix('acct:')) + when 'share' + return redirect_to share_path(text: uri.query_values['text']) + end end + + not_found end private - def valid_uri? - uri.present? && uri.scheme == EXPECTED_SCHEME + def check_uri + not_found if uri.blank? end def handle_invalid_uri diff --git a/app/controllers/redirect/accounts_controller.rb b/app/controllers/redirect/accounts_controller.rb deleted file mode 100644 index 713ccf2ca1..0000000000 --- a/app/controllers/redirect/accounts_controller.rb +++ /dev/null @@ -1,10 +0,0 @@ -# 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 deleted file mode 100644 index 90894ec1ed..0000000000 --- a/app/controllers/redirect/base_controller.rb +++ /dev/null @@ -1,24 +0,0 @@ -# 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 deleted file mode 100644 index 37a938c651..0000000000 --- a/app/controllers/redirect/statuses_controller.rb +++ /dev/null @@ -1,10 +0,0 @@ -# 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/settings/applications_controller.rb b/app/controllers/settings/applications_controller.rb index 6849979b11..d4b7205681 100644 --- a/app/controllers/settings/applications_controller.rb +++ b/app/controllers/settings/applications_controller.rb @@ -13,7 +13,7 @@ class Settings::ApplicationsController < Settings::BaseController def new @application = Doorkeeper::Application.new( redirect_uri: Doorkeeper.configuration.native_redirect_uri, - scopes: 'read:me' + scopes: 'read write follow' ) end diff --git a/app/controllers/settings/featured_tags_controller.rb b/app/controllers/settings/featured_tags_controller.rb index 90c112e219..c384402650 100644 --- a/app/controllers/settings/featured_tags_controller.rb +++ b/app/controllers/settings/featured_tags_controller.rb @@ -38,7 +38,7 @@ class Settings::FeaturedTagsController < Settings::BaseController end def set_recently_used_tags - @recently_used_tags = Tag.suggestions_for_account(current_account).limit(10) + @recently_used_tags = Tag.recently_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10) end def featured_tag_params diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb index 569aa07c53..983caf22fa 100644 --- a/app/controllers/settings/imports_controller.rb +++ b/app/controllers/settings/imports_controller.rb @@ -31,7 +31,7 @@ class Settings::ImportsController < Settings::BaseController def show; end def failures - @bulk_import = current_account.bulk_imports.state_finished.find(params[:id]) + @bulk_import = current_account.bulk_imports.where(state: :finished).find(params[:id]) respond_to do |format| format.csv do @@ -92,7 +92,7 @@ class Settings::ImportsController < Settings::BaseController end def set_bulk_import - @bulk_import = current_account.bulk_imports.state_unconfirmed.find(params[:id]) + @bulk_import = current_account.bulk_imports.where(state: :unconfirmed).find(params[:id]) end def set_recent_imports diff --git a/app/controllers/settings/preferences/base_controller.rb b/app/controllers/settings/preferences/base_controller.rb index ce6e2bba44..c1f8b49898 100644 --- a/app/controllers/settings/preferences/base_controller.rb +++ b/app/controllers/settings/preferences/base_controller.rb @@ -19,16 +19,6 @@ class Settings::Preferences::BaseController < Settings::BaseController end def user_params - original_user_params.tap do |params| - params[:settings_attributes]&.merge!(disabled_visibilities_params[:settings_attributes] || {}) - end - end - - def original_user_params params.require(:user).permit(:locale, :time_zone, chosen_languages: [], settings_attributes: UserSettings.keys) end - - def disabled_visibilities_params - params.require(:user).permit(settings_attributes: { enabled_visibilities: [] }) - end end diff --git a/app/controllers/settings/preferences/other_controller.rb b/app/controllers/settings/preferences/other_controller.rb index 02925fa6e5..a19fbf5c48 100644 --- a/app/controllers/settings/preferences/other_controller.rb +++ b/app/controllers/settings/preferences/other_controller.rb @@ -1,13 +1,6 @@ # frozen_string_literal: true class Settings::Preferences::OtherController < Settings::Preferences::BaseController - include DtlHelper - - def show - @dtl_enabled = dtl_enabled? - @dtl_tag = dtl_tag_name - end - private def after_update_redirect_path diff --git a/app/controllers/settings/preferences/reaching_controller.rb b/app/controllers/settings/preferences/reaching_controller.rb deleted file mode 100644 index bd3e6ae9b2..0000000000 --- a/app/controllers/settings/preferences/reaching_controller.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -class Settings::Preferences::ReachingController < Settings::Preferences::BaseController - private - - def after_update_redirect_path - settings_preferences_reaching_path - end -end diff --git a/app/controllers/settings/privacy_extra_controller.rb b/app/controllers/settings/privacy_extra_controller.rb deleted file mode 100644 index 49c71d5071..0000000000 --- a/app/controllers/settings/privacy_extra_controller.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -class Settings::PrivacyExtraController < Settings::BaseController - before_action :set_account - - def show; end - - def update - if UpdateAccountService.new.call(@account, account_params.except(:settings)) - current_user.update!(settings_attributes: account_params[:settings]) - ActivityPub::UpdateDistributionWorker.perform_async(@account.id) - redirect_to settings_privacy_extra_path, notice: I18n.t('generic.changes_saved_msg') - else - render :show - end - end - - private - - def account_params - params.require(:account).permit(:subscription_policy, settings: UserSettings.keys) - end - - def set_account - @account = current_account - end -end diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index a023b073b3..8ae69b7fe0 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -20,8 +20,7 @@ class Settings::ProfilesController < Settings::BaseController private def account_params - # params.require(:account).permit(:display_name, :note, :bio_markdown, :avatar, :header, :locked, :my_actor_type, :searchability, :dissubscribable, :discoverable, :discoverable_local, :hide_collections, fields_attributes: [:name, :value]) - params.require(:account).permit(:display_name, :note, :bio_markdown, :avatar, :header, :bot, :my_actor_type, :dissubscribable, fields_attributes: [:name, :value]) + params.require(:account).permit(:display_name, :note, :avatar, :header, :bot, fields_attributes: [:name, :value]) end def set_account 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 9714d54f95..c86ede4f3a 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 :redirect_invalid_otp, unless: -> { current_user.otp_enabled? } - before_action :redirect_invalid_webauthn, only: [:index, :destroy], unless: -> { current_user.webauthn_enabled? } + before_action :require_otp_enabled + before_action :require_webauthn_enabled, only: [:index, :destroy] def index; end def new; end @@ -85,14 +85,18 @@ module Settings private - def redirect_invalid_otp - flash[:error] = t('webauthn_credentials.otp_required') - redirect_to settings_two_factor_authentication_methods_path + 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 end - def redirect_invalid_webauthn - flash[:error] = t('webauthn_credentials.not_enabled') - redirect_to settings_two_factor_authentication_methods_path + 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 end end end diff --git a/app/controllers/severed_relationships_controller.rb b/app/controllers/severed_relationships_controller.rb deleted file mode 100644 index 168e85e3fe..0000000000 --- a/app/controllers/severed_relationships_controller.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -class SeveredRelationshipsController < ApplicationController - layout 'admin' - - before_action :authenticate_user! - before_action :set_body_classes - before_action :set_cache_headers - - before_action :set_event, only: [:following, :followers] - - def index - @events = AccountRelationshipSeveranceEvent.where(account: current_account) - end - - def following - respond_to do |format| - format.csv { send_data following_data, filename: "following-#{@event.target_name}-#{@event.created_at.to_date.iso8601}.csv" } - end - end - - def followers - respond_to do |format| - format.csv { send_data followers_data, filename: "followers-#{@event.target_name}-#{@event.created_at.to_date.iso8601}.csv" } - end - end - - private - - def set_event - @event = AccountRelationshipSeveranceEvent.find(params[:id]) - end - - def following_data - CSV.generate(headers: ['Account address', 'Show boosts', 'Notify on new posts', 'Languages'], write_headers: true) do |csv| - @event.severed_relationships.active.about_local_account(current_account).includes(:remote_account).reorder(id: :desc).each do |follow| - csv << [acct(follow.target_account), follow.show_reblogs, follow.notify, follow.languages&.join(', ')] - end - end - end - - def followers_data - CSV.generate(headers: ['Account address'], write_headers: true) do |csv| - @event.severed_relationships.passive.about_local_account(current_account).includes(:remote_account).reorder(id: :desc).each do |follow| - csv << [acct(follow.account)] - end - end - end - - def acct(account) - account.local? ? account.local_username_and_domain : account.acct - end - - def set_body_classes - @body_classes = 'admin' - end - - def set_cache_headers - response.cache_control.replace(private: true, no_store: true) - end -end diff --git a/app/controllers/statuses_cleanup_controller.rb b/app/controllers/statuses_cleanup_controller.rb index 04c3c0e05c..4a3fc10ca4 100644 --- a/app/controllers/statuses_cleanup_controller.rb +++ b/app/controllers/statuses_cleanup_controller.rb @@ -31,7 +31,7 @@ class StatusesCleanupController < ApplicationController end def resource_params - params.require(:account_statuses_cleanup_policy).permit(:enabled, :min_status_age, :keep_direct, :keep_pinned, :keep_polls, :keep_media, :keep_self_fav, :keep_self_bookmark, :keep_self_emoji, :min_favs, :min_reblogs, :min_emojis) + params.require(:account_statuses_cleanup_policy).permit(:enabled, :min_status_age, :keep_direct, :keep_pinned, :keep_polls, :keep_media, :keep_self_fav, :keep_self_bookmark, :min_favs, :min_reblogs) end def set_body_classes diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index be87161cc8..db7eddd78b 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -29,15 +29,15 @@ class StatusesController < ApplicationController end format.json do - expires_in 3.minutes, public: true if @status.distributable? && public_fetch_mode? && !misskey_software? && !@status.expires? - render_with_cache json: @status, content_type: 'application/activity+json', serializer: status_activity_serializer, adapter: ActivityPub::Adapter, cancel_cache: misskey_software? + expires_in 3.minutes, public: true if @status.distributable? && public_fetch_mode? + render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter end end end def activity - expires_in 3.minutes, public: @status.distributable? && public_fetch_mode? && !misskey_software? - render_with_cache json: ActivityPub::ActivityPresenter.from_status(@status, for_misskey: misskey_software?), content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter, cancel_cache: misskey_software? + expires_in 3.minutes, public: @status.distributable? && public_fetch_mode? + render_with_cache json: ActivityPub::ActivityPresenter.from_status(@status), content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter end def embed @@ -61,41 +61,11 @@ class StatusesController < ApplicationController def set_status @status = @account.statuses.find(params[:id]) - - if request.authorization.present? && request.authorization.match(/^Bearer /i) - raise Mastodon::NotPermittedError unless @status.capability_tokens.find_by(token: request.authorization.gsub(/^Bearer /i, '')) - elsif request.format == :json && @status.expires? - raise Mastodon::NotPermittedError unless StatusPolicy.new(signed_request_account, @status).show_activity? - else - authorize @status, :show? - end + authorize @status, :show? rescue Mastodon::NotPermittedError not_found end - def misskey_software? - return @misskey_software if defined?(@misskey_software) - - @misskey_software = false - - return false if !@status.local? || signed_request_account&.domain.blank? || !@status.sending_maybe_compromised_privacy? - - return @misskey_software = true if DomainBlock.detect_invalid_subscription?(signed_request_account.domain) - - info = InstanceInfo.find_by(domain: signed_request_account.domain) - return false if info.nil? - - @misskey_software = %w(misskey calckey cherrypick sharkey).include?(info.software) - end - - def status_activity_serializer - if misskey_software? - ActivityPub::NoteForMisskeySerializer - else - ActivityPub::NoteSerializer - end - end - def redirect_to_original redirect_to(ActivityPub::TagManager.instance.url_for(@status.reblog), allow_other_host: true) if @status.reblog? end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index d6c0d872c8..b0bdbde956 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -45,7 +45,7 @@ class TagsController < ApplicationController end def set_statuses - @statuses = preload_collection(TagFeed.new(@tag, nil, local: @local).get(limit_param), Status) + @statuses = cache_collection(TagFeed.new(@tag, nil, local: @local).get(limit_param), Status) end def limit_param diff --git a/app/controllers/well_known/oauth_metadata_controller.rb b/app/controllers/well_known/oauth_metadata_controller.rb deleted file mode 100644 index c80be2d652..0000000000 --- a/app/controllers/well_known/oauth_metadata_controller.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module WellKnown - class OauthMetadataController < ActionController::Base # rubocop:disable Rails/ApplicationController - include CacheConcern - - # Prevent `active_model_serializer`'s `ActionController::Serialization` from calling `current_user` - # and thus re-issuing session cookies - serialization_scope nil - - def show - # Due to this document potentially changing between Mastodon versions (as - # new OAuth scopes are added), we don't use expires_in to cache upstream, - # instead just caching in the rails cache: - render_with_cache( - json: ::OauthMetadataPresenter.new, - serializer: ::OauthMetadataSerializer, - content_type: 'application/json', - expires_in: 15.minutes - ) - end - end -end diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb index 6136c82ce7..6301919a9e 100644 --- a/app/helpers/accounts_helper.rb +++ b/app/helpers/accounts_helper.rb @@ -27,25 +27,21 @@ 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 = [ [ - account_formatted_stat(account.public_statuses_count), - I18n.t('accounts.posts', count: account.public_statuses_count), + number_to_human(account.statuses_count, precision: 3, strip_insignificant_zeros: true), + I18n.t('accounts.posts', count: account.statuses_count), ].join(' '), [ - account_formatted_stat(account.public_following_count), - I18n.t('accounts.following', count: account.public_following_count), + number_to_human(account.following_count, precision: 3, strip_insignificant_zeros: true), + I18n.t('accounts.following', count: account.following_count), ].join(' '), [ - account_formatted_stat(account.public_followers_count), - I18n.t('accounts.followers', count: account.public_followers_count), + number_to_human(account.followers_count, precision: 3, strip_insignificant_zeros: true), + I18n.t('accounts.followers', count: account.followers_count), ].join(' '), ].join(', ') diff --git a/app/helpers/admin/accounts_helper.rb b/app/helpers/admin/accounts_helper.rb index a2d2f75308..a936797e88 100644 --- a/app/helpers/admin/accounts_helper.rb +++ b/app/helpers/admin/accounts_helper.rb @@ -7,7 +7,6 @@ module Admin::AccountsHelper [t('admin.accounts.moderation.silenced'), 'silenced'], [t('admin.accounts.moderation.disabled'), 'disabled'], [t('admin.accounts.moderation.suspended'), 'suspended'], - [t('admin.accounts.moderation.remote_pending'), 'remote_pending'], [safe_join([t('admin.accounts.moderation.pending'), "(#{pending_user_count_label})"], ' '), 'pending'], ] end diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index 4355b5f787..4018ef6b1c 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -23,9 +23,7 @@ module Admin::ActionLogsHelper link_to log.human_identifier, disputes_strike_path(log.target_id) when 'Announcement' link_to truncate(log.human_identifier), edit_admin_announcement_path(log.target_id) - when 'CustomEmoji' - link_to log.human_identifier, edit_admin_custom_emoji_path(log.target_id) - when 'IpBlock', 'Instance' + when 'IpBlock', 'Instance', 'CustomEmoji' log.human_identifier when 'CanonicalEmailBlock' content_tag(:samp, (log.human_identifier.presence || '')[0...7], title: log.human_identifier) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 8dee31e18d..4f7f66985d 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true module ApplicationHelper - include RegistrationLimitationHelper - DANGEROUS_SCOPES = %w( read write @@ -30,12 +28,20 @@ module ApplicationHelper number_to_human(number, **options) end + def active_nav_class(*paths) + paths.any? { |path| current_page?(path) } ? 'active' : '' + end + + def show_landing_strip? + !user_signed_in? && !single_user_mode? + end + def open_registrations? - Setting.registrations_mode == 'open' && registrations_in_time? + Setting.registrations_mode == 'open' end def approved_registrations? - Setting.registrations_mode == 'approved' || (Setting.registrations_mode == 'open' && !registrations_in_time?) + Setting.registrations_mode == 'approved' end def closed_registrations? @@ -115,16 +121,8 @@ module ApplicationHelper content_tag(:i, nil, attributes.merge(class: class_names.join(' '))) end - def material_symbol(icon, attributes = {}) - inline_svg_tag( - "400-24px/#{icon}.svg", - class: %w(icon).concat(attributes[:class].to_s.split), - role: :img - ) - end - def check_icon - inline_svg_tag 'check.svg' + content_tag(:svg, tag.path('fill-rule': 'evenodd', 'clip-rule': 'evenodd', d: 'M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z'), xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 20 20', fill: 'currentColor') end def visibility_icon(status) @@ -132,10 +130,6 @@ module ApplicationHelper fa_icon('globe', title: I18n.t('statuses.visibilities.public')) elsif status.unlisted_visibility? fa_icon('unlock', title: I18n.t('statuses.visibilities.unlisted')) - elsif status.public_unlisted_visibility? - fa_icon('cloud', title: I18n.t('statuses.visibilities.public_unlisted')) - elsif status.login_visibility? - fa_icon('key', title: I18n.t('statuses.visibilities.login')) elsif status.private_visibility? || status.limited_visibility? fa_icon('lock', title: I18n.t('statuses.visibilities.private')) elsif status.direct_visibility? @@ -201,14 +195,10 @@ module ApplicationHelper text: [params[:title], params[:text], params[:url]].compact.join(' '), } - permit_visibilities = %w(public unlisted public_unlisted login private direct) - permit_searchabilities = %w(public unlisted public_unlisted login private direct) - default_privacy = current_account&.user&.setting_default_privacy + permit_visibilities = %w(public unlisted private direct) + default_privacy = current_account&.user&.setting_default_privacy permit_visibilities.shift(permit_visibilities.index(default_privacy) + 1) if default_privacy.present? state_params[:visibility] = params[:visibility] if permit_visibilities.include? params[:visibility] - default_searchability = current_account&.user&.setting_default_searchability - permit_searchabilities.shift(permit_searchabilities.index(default_privacy) + 1) if default_searchability.present? - state_params[:searchability] = params[:searchability] if permit_searchabilities.include? params[:searchability] if user_signed_in? && current_user.functional? state_params[:settings] = state_params[:settings].merge(Web::Setting.find_by(user: current_user)&.data || {}) @@ -223,7 +213,7 @@ module ApplicationHelper state_params[:moved_to_account] = current_account.moved_to_account end - state_params[:owner] = Account.local.without_suspended.without_internal.first if single_user_mode? + state_params[:owner] = Account.local.without_suspended.where('id > 0').first if single_user_mode? json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json # rubocop:disable Rails/OutputSafety @@ -250,22 +240,6 @@ module ApplicationHelper EmojiFormatter.new(html, custom_emojis, other_options.merge(animate: prefers_autoplay?)).to_s end - def prerender_custom_emojis_from_hash(html, custom_emojis_hash) - prerender_custom_emojis(html, JSON.parse([custom_emojis_hash].to_json, object_class: OpenStruct)) # rubocop:disable Style/OpenStructUse - end - - def instance_presenter - @instance_presenter ||= InstancePresenter.new - end - - def favicon_path(size = '48') - instance_presenter.favicon&.file&.url(size) - end - - def app_icon_path(size = '48') - instance_presenter.app_icon&.file&.url(size) - end - private def storage_host_var diff --git a/app/helpers/branding_helper.rb b/app/helpers/branding_helper.rb index 8201f36e3c..2b9c233c23 100644 --- a/app/helpers/branding_helper.rb +++ b/app/helpers/branding_helper.rb @@ -19,6 +19,17 @@ module BrandingHelper end def render_logo - image_tag(frontend_asset_path('images/logo.svg'), alt: 'Mastodon', class: 'logo logo--icon') + image_pack_tag('logo.svg', alt: 'Mastodon', class: 'logo logo--icon') + end + + def render_symbol(version = :icon) + path = case version + when :icon + 'logo-symbol-icon.svg' + when :wordmark + 'logo-symbol-wordmark.svg' + end + + render(file: Rails.root.join('app', 'javascript', 'images', path)).html_safe # rubocop:disable Rails/OutputSafety end end diff --git a/app/helpers/context_helper.rb b/app/helpers/context_helper.rb index 804c48c7b0..945ef9b91a 100644 --- a/app/helpers/context_helper.rb +++ b/app/helpers/context_helper.rb @@ -23,31 +23,13 @@ module ContextHelper indexable: { 'toot' => 'http://joinmastodon.org/ns#', 'indexable' => 'toot:indexable' }, memorial: { 'toot' => 'http://joinmastodon.org/ns#', 'memorial' => 'toot:memorial' }, voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' }, - emoji_reactions: { 'fedibird' => 'http://fedibird.com/ns#', 'emojiReactions' => { '@id' => 'fedibird:emojiReactions', '@type' => '@id' } }, - searchable_by: { 'fedibird' => 'http://fedibird.com/ns#', 'searchableBy' => { '@id' => 'fedibird:searchableBy', '@type' => '@id' } }, - subscribable_by: { 'kmyblue' => 'http://kmy.blue/ns#', 'subscribableBy' => { '@id' => 'kmyblue:subscribableBy', '@type' => '@id' } }, - limited_scope: { 'kmyblue' => 'http://kmy.blue/ns#', 'limitedScope' => 'kmyblue:limitedScope' }, - other_setting: { 'fedibird' => 'http://fedibird.com/ns#', 'otherSetting' => 'fedibird:otherSetting' }, - references: { 'fedibird' => 'http://fedibird.com/ns#', 'references' => { '@id' => 'fedibird:references', '@type' => '@id' } }, - quote_uri: { 'fedibird' => 'http://fedibird.com/ns#', 'quoteUri' => 'fedibird:quoteUri' }, - keywords: { 'schema' => 'http://schema.org#', 'keywords' => 'schema:keywords' }, - license: { 'schema' => 'http://schema.org#', 'license' => 'schema:license' }, olm: { - 'toot' => 'http://joinmastodon.org/ns#', - 'Device' => 'toot:Device', - 'Ed25519Signature' => 'toot:Ed25519Signature', - 'Ed25519Key' => 'toot:Ed25519Key', - 'Curve25519Key' => 'toot:Curve25519Key', - 'EncryptedMessage' => 'toot:EncryptedMessage', - 'publicKeyBase64' => 'toot:publicKeyBase64', - 'deviceId' => 'toot:deviceId', + 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, - 'messageFranking' => 'toot:messageFranking', - 'messageType' => 'toot:messageType', - 'cipherText' => 'toot:cipherText', + 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' }, suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' }, }.freeze @@ -57,11 +39,13 @@ module ContextHelper end def serialized_context(named_contexts_map, context_extensions_map) + context_array = [] + named_contexts = named_contexts_map.keys context_extensions = context_extensions_map.keys - context_array = named_contexts.map do |key| - NAMED_CONTEXT_MAP[key] + named_contexts.each do |key| + context_array << NAMED_CONTEXT_MAP[key] end extensions = context_extensions.each_with_object({}) do |key, h| diff --git a/app/helpers/dtl_helper.rb b/app/helpers/dtl_helper.rb deleted file mode 100644 index aa2c414c5f..0000000000 --- a/app/helpers/dtl_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module DtlHelper - def dtl_enabled? - ENV.fetch('DTL_ENABLED', 'false') == 'true' - end - - def dtl_tag_name - ENV.fetch('DTL_TAG', 'kmyblue') - end -end diff --git a/app/helpers/follow_helper.rb b/app/helpers/follow_helper.rb deleted file mode 100644 index d7d7ff35e9..0000000000 --- a/app/helpers/follow_helper.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -module FollowHelper - def request_pending_follow?(source_account, target_account) - target_account.locked? || source_account.silenced? || block_straight_follow?(source_account) || - ((source_account.bot? || proxy_account?(source_account)) && target_account.user&.setting_lock_follow_from_bot) - end - - def block_straight_follow?(account) - return false if account.local? - - DomainBlock.reject_straight_follow?(account.domain) - end - - def proxy_account?(account) - (account.username.downcase.include?('_proxy') || - account.username.downcase.end_with?('proxy') || - account.username.downcase.include?('_bot_') || - account.username.downcase.end_with?('bot') || - account.display_name&.downcase&.include?('proxy') || - account.display_name&.include?('プロキシ') || - account.note&.include?('プロキシ')) && - (account.following_count.zero? || account.following_count > account.followers_count) && - proxyable_software?(account) - end - - def proxyable_software?(account) - return false if account.local? - - info = InstanceInfo.find_by(domain: account.domain) - return false if info.nil? - - %w(misskey calckey firefish meisskey cherrypick sharkey).include?(info.software) - end -end diff --git a/app/helpers/formatting_helper.rb b/app/helpers/formatting_helper.rb index f0faf69b43..7d1423e52d 100644 --- a/app/helpers/formatting_helper.rb +++ b/app/helpers/formatting_helper.rb @@ -18,16 +18,8 @@ module FormattingHelper end module_function :extract_status_plain_text - def extract_status_plain_text_with_spoiler_text(status) - PlainTextFormatter.new("#{status.spoiler_text}\n#{status.text}", status.local?).to_s - end - def status_content_format(status) - html_aware_format(status.text, status.local?, markdown: status.markdown, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : [])) - end - - def emoji_name_format(emoji_reaction, status) - html_aware_format(emoji_reaction['url'].present? ? ":#{emoji_reaction['name']}:" : emoji_reaction['name'], status.local?, markdown: status.markdown) + html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : [])) end def rss_status_content_format(status) @@ -57,19 +49,19 @@ module FormattingHelper prerender_custom_emojis( safe_join([before_html, html, after_html]), status.emojis, - style: 'min-width: 1.1em; height: 1.1em; object-fit: contain; vertical-align: middle; margin: -.2ex .15em .2ex' + style: 'width: 1.1em; height: 1.1em; object-fit: contain; vertical-align: middle; margin: -.2ex .15em .2ex' ).to_str end def account_bio_format(account) - html_aware_format(account.note, account.local?, markdown: account.user&.setting_bio_markdown) + html_aware_format(account.note, account.local?) end def account_field_value_format(field, with_rel_me: true) if field.verified? && !field.account.local? TextFormatter.shortened_link(field.value_for_verification) else - html_aware_format(field.value, field.account.local?, markdown: false, with_rel_me: with_rel_me, with_domains: true, multiline: false) + html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false) end end end diff --git a/app/helpers/high_load_helper.rb b/app/helpers/high_load_helper.rb deleted file mode 100644 index b4606c039f..0000000000 --- a/app/helpers/high_load_helper.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -module HighLoadHelper - def allow_high_load? - ENV.fetch('ALLOW_HIGH_LOAD', 'true') == 'true' - end - module_function :allow_high_load? -end diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index 75901e3ff1..ce3ff094f6 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -51,14 +51,6 @@ module JsonLdHelper end end - def as_array_ex(value) - if value.is_a?(Hash) - [] - else - as_array(value) - end - end - def value_or_id(value) value.is_a?(String) || value.nil? ? value : value['id'] end @@ -119,7 +111,7 @@ module JsonLdHelper patch_for_forwarding!(value, compacted_value) elsif value.is_a?(Array) compacted_value = [compacted_value] unless compacted_value.is_a?(Array) - next if value.size != compacted_value.size + return if value.size != compacted_value.size compacted[key] = value.zip(compacted_value).map do |v, vc| if v.is_a?(Hash) && vc.is_a?(Hash) @@ -163,8 +155,8 @@ module JsonLdHelper end end - def fetch_resource(uri, id_is_known, on_behalf_of = nil, request_options: {}) - unless id_is_known + def fetch_resource(uri, id, on_behalf_of = nil) + unless id json = fetch_resource_without_id_validation(uri, on_behalf_of) return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id']) @@ -172,29 +164,17 @@ module JsonLdHelper uri = json['id'] end - json = fetch_resource_without_id_validation(uri, on_behalf_of, request_options: request_options) + json = fetch_resource_without_id_validation(uri, on_behalf_of) json.present? && json['id'] == uri ? json : nil end - def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false, request_options: {}) + def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false) on_behalf_of ||= Account.representative - build_request(uri, on_behalf_of, options: request_options).perform do |response| + build_request(uri, on_behalf_of).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 && valid_activitypub_content_type?(response) - end - end - - def valid_activitypub_content_type?(response) - return true if response.mime_type == 'application/activity+json' - - # When the mime type is `application/ld+json`, we need to check the profile, - # but `http.rb` does not parse it for us. - return false unless response.mime_type == 'application/ld+json' - - response.headers[HTTP::Headers::CONTENT_TYPE]&.split(';')&.map(&:strip)&.any? do |str| - str.start_with?('profile="') && str[9...-1].split.include?('https://www.w3.org/ns/activitystreams') + body_to_json(response.body_with_limit) if response.code == 200 end end @@ -224,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, options: {}) - Request.new(:get, uri, **options).tap do |request| + def build_request(uri, on_behalf_of = nil) + Request.new(:get, uri).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/kmyblue_capabilities_helper.rb b/app/helpers/kmyblue_capabilities_helper.rb deleted file mode 100644 index 075869ee60..0000000000 --- a/app/helpers/kmyblue_capabilities_helper.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -module KmyblueCapabilitiesHelper - def fedibird_capabilities - capabilities = %i( - enable_wide_emoji - kmyblue_searchability - searchability - kmyblue_markdown - kmyblue_reaction_deck - kmyblue_visibility_login - status_reference - visibility_mutual - visibility_limited - kmyblue_limited_scope - kmyblue_antenna - kmyblue_bookmark_category - kmyblue_quote - kmyblue_searchability_limited - kmyblue_circle_history - kmyblue_list_notification - kmyblue_server_features - ) - - capabilities << :full_text_search if Chewy.enabled? - if Setting.enable_emoji_reaction - capabilities << :emoji_reaction - capabilities << :enable_wide_emoji_reaction - end - capabilities << :kmyblue_visibility_public_unlisted if Setting.enable_public_unlisted_visibility - capabilities << :kmyblue_searchability_public_unlisted if Setting.enable_public_unlisted_visibility - capabilities << :kmyblue_no_public_visibility unless Setting.enable_public_visibility - capabilities << :timeline_no_local unless Setting.enable_local_timeline - - capabilities - end - - def capabilities_for_nodeinfo - capabilities = %i( - enable_wide_emoji - status_reference - quote - emoji_keywords - circle - ) - - if Setting.enable_emoji_reaction - capabilities << :emoji_reaction - capabilities << :enable_wide_emoji_reaction - end - - capabilities - end -end diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb index 9e1c0a7db1..87f0f288d3 100644 --- a/app/helpers/languages_helper.rb +++ b/app/helpers/languages_helper.rb @@ -109,7 +109,6 @@ module LanguagesHelper mn: ['Mongolian', 'Монгол хэл'].freeze, mr: ['Marathi', 'मराठी'].freeze, ms: ['Malay', 'Bahasa Melayu'].freeze, - 'ms-Arab': ['Jawi Malay', 'بهاس ملايو'].freeze, mt: ['Maltese', 'Malti'].freeze, my: ['Burmese', 'ဗမာစာ'].freeze, na: ['Nauru', 'Ekakairũ Naoero'].freeze, @@ -128,7 +127,7 @@ module LanguagesHelper om: ['Oromo', 'Afaan Oromoo'].freeze, or: ['Oriya', 'ଓଡ଼ିଆ'].freeze, os: ['Ossetian', 'ирон æвзаг'].freeze, - pa: ['Punjabi', 'ਪੰਜਾਬੀ'].freeze, + pa: ['Panjabi', 'ਪੰਜਾਬੀ'].freeze, pi: ['Pāli', 'पाऴि'].freeze, pl: ['Polish', 'Polski'].freeze, ps: ['Pashto', 'پښتو'].freeze, @@ -192,20 +191,15 @@ module LanguagesHelper chr: ['Cherokee', 'ᏣᎳᎩ ᎦᏬᏂᎯᏍᏗ'].freeze, ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze, cnr: ['Montenegrin', 'crnogorski'].freeze, - csb: ['Kashubian', 'Kaszëbsczi'].freeze, jbo: ['Lojban', 'la .lojban.'].freeze, kab: ['Kabyle', 'Taqbaylit'].freeze, ldn: ['Láadan', 'Láadan'].freeze, lfn: ['Lingua Franca Nova', 'lingua franca nova'].freeze, - moh: ['Mohawk', 'Kanienʼkéha'].freeze, - nds: ['Low German', 'Plattdüütsch'].freeze, - pdc: ['Pennsylvania Dutch', 'Pennsilfaani-Deitsch'].freeze, sco: ['Scots', 'Scots'].freeze, sma: ['Southern Sami', 'Åarjelsaemien Gïele'].freeze, smj: ['Lule Sami', 'Julevsámegiella'].freeze, szl: ['Silesian', 'ślůnsko godka'].freeze, tok: ['Toki Pona', 'toki pona'].freeze, - vai: ['Vai', 'ꕙꔤ'].freeze, xal: ['Kalmyk', 'Хальмг келн'].freeze, zba: ['Balaibalan', 'باليبلن'].freeze, zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze, diff --git a/app/helpers/mascot_helper.rb b/app/helpers/mascot_helper.rb index 34b656411e..8ee04383ec 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 || frontend_asset_path('images/elephant_ui_plane.svg')) + full_asset_url(instance_presenter.mascot&.file&.url || asset_pack_path('media/images/elephant_ui_plane.svg')) end def instance_presenter diff --git a/app/helpers/ng_rule_helper.rb b/app/helpers/ng_rule_helper.rb deleted file mode 100644 index 104442b117..0000000000 --- a/app/helpers/ng_rule_helper.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -module NgRuleHelper - def check_invalid_status_for_ng_rule!(account, **options) - (check_for_ng_rule!(account, **options) { |rule| !rule.check_status_or_record! }).none? - end - - def check_invalid_reaction_for_ng_rule!(account, **options) - (check_for_ng_rule!(account, **options) { |rule| !rule.check_reaction_or_record! }).none? - end - - private - - def check_for_ng_rule!(account, **options, &block) - NgRule.cached_rules - .map { |raw_rule| Admin::NgRule.new(raw_rule, account, **options) } - .filter(&block) - end - - def do_account_action_for_rule!(account, action) - case action - when :silence - account.silence! - when :suspend - account.suspend! - end - end -end diff --git a/app/helpers/react_component_helper.rb b/app/helpers/react_component_helper.rb index 821a6f1e2d..ce616e8306 100644 --- a/app/helpers/react_component_helper.rb +++ b/app/helpers/react_component_helper.rb @@ -15,20 +15,9 @@ module ReactComponentHelper div_tag_with_data(data) end - def serialized_media_attachments(media_attachments) - media_attachments.map { |attachment| serialized_attachment(attachment) } - end - private def div_tag_with_data(data) content_tag(:div, nil, data: data) end - - def serialized_attachment(attachment) - ActiveModelSerializers::SerializableResource.new( - attachment, - serializer: REST::MediaAttachmentSerializer - ).as_json - end end diff --git a/app/helpers/registration_helper.rb b/app/helpers/registration_helper.rb index c3db46c027..ef5462ac88 100644 --- a/app/helpers/registration_helper.rb +++ b/app/helpers/registration_helper.rb @@ -3,10 +3,8 @@ module RegistrationHelper extend ActiveSupport::Concern - include RegistrationLimitationHelper - def allowed_registration?(remote_ip, invite) - !Rails.configuration.x.single_user_mode && !omniauth_only? && ((registrations_open? && !reach_registrations_limit?) || invite&.valid_for_use?) && !ip_blocked?(remote_ip) + !Rails.configuration.x.single_user_mode && !omniauth_only? && (registrations_open? || invite&.valid_for_use?) && !ip_blocked?(remote_ip) end def registrations_open? diff --git a/app/helpers/registration_limitation_helper.rb b/app/helpers/registration_limitation_helper.rb deleted file mode 100644 index 56523b816c..0000000000 --- a/app/helpers/registration_limitation_helper.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -module RegistrationLimitationHelper - def reach_registrations_limit? - ((Setting.registrations_limit.presence || 0).positive? && Setting.registrations_limit <= user_count_for_registration) || - ((Setting.registrations_limit_per_day.presence || 0).positive? && Setting.registrations_limit_per_day <= today_increase_user_count) - end - - def user_count_for_registration - Rails.cache.fetch('registrations:user_count') { User.confirmed.enabled.joins(:account).merge(Account.without_suspended).count } - end - - def today_increase_user_count - today_date = Time.now.utc.beginning_of_day.to_i - count = 0 - - if Rails.cache.fetch('registrations:today_date') { today_date } == today_date - count = Rails.cache.fetch('registrations:today_increase_user_count') { today_increase_user_count_value } - else - count = today_increase_user_count_value - Rails.cache.write('registrations:today_date', today_date) - Rails.cache.write('registrations:today_increase_user_count', count) - end - - count - end - - def today_increase_user_count_value - User.confirmed.enabled.where('users.created_at >= ?', Time.now.utc.beginning_of_day).joins(:account).merge(Account.without_suspended).count - end - - def registrations_in_time? - start_hour = Setting.registrations_start_hour - end_hour = Setting.registrations_end_hour - secondary_start_hour = Setting.registrations_secondary_start_hour - secondary_end_hour = Setting.registrations_secondary_end_hour - - start_hour = 0 unless start_hour.is_a?(Integer) - end_hour = 0 unless end_hour.is_a?(Integer) - secondary_start_hour = 0 unless secondary_start_hour.is_a?(Integer) - secondary_end_hour = 0 unless secondary_end_hour.is_a?(Integer) - - return true if start_hour >= end_hour && secondary_start_hour >= secondary_end_hour - - current_hour = Time.now.utc.hour - - (start_hour < end_hour && end_hour.positive? && current_hour.between?(start_hour, end_hour - 1)) || - (secondary_start_hour < secondary_end_hour && secondary_end_hour.positive? && current_hour.between?(secondary_start_hour, secondary_end_hour - 1)) - end - - def reset_registration_limit_caches! - Rails.cache.delete('registrations:user_count') - Rails.cache.delete('registrations:today_increase_user_count') - end -end diff --git a/app/helpers/routing_helper.rb b/app/helpers/routing_helper.rb index 15d988f64d..2fb9ce72cb 100644 --- a/app/helpers/routing_helper.rb +++ b/app/helpers/routing_helper.rb @@ -24,12 +24,8 @@ module RoutingHelper Rails.configuration.action_controller.asset_host || root_url end - 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)) + def full_pack_url(source, **options) + full_asset_url(asset_pack_path(source, **options)) end def use_storage? diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index 8ded11e03d..286c53d834 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -4,6 +4,14 @@ module StatusesHelper EMBEDDED_CONTROLLER = 'statuses' EMBEDDED_ACTION = 'embed' + def link_to_newer(url) + link_to t('statuses.show_newer'), url, class: 'load-more load-gap' + end + + def link_to_older(url) + link_to t('statuses.show_older'), url, class: 'load-more load-gap' + end + def nothing_here(extra_classes = '') content_tag(:div, class: "nothing-here #{extra_classes}") do t('accounts.nothing_here') @@ -63,14 +71,8 @@ module StatusesHelper fa_icon 'globe fw' when 'unlisted' fa_icon 'unlock fw' - when 'public_unlisted' - fa_icon 'cloud fw' - when 'login' - fa_icon 'key fw' when 'private' fa_icon 'lock fw' - when 'limited' - fa_icon 'get-pocket fw' when 'direct' fa_icon 'at fw' end diff --git a/app/helpers/theme_helper.rb b/app/helpers/theme_helper.rb deleted file mode 100644 index d15259851c..0000000000 --- a/app/helpers/theme_helper.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -module ThemeHelper - def theme_style_tags(theme) - if theme == 'system' - stylesheet_pack_tag('mastodon-light', media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous') + - stylesheet_pack_tag('default', media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous') - else - stylesheet_pack_tag theme, media: 'all', crossorigin: 'anonymous' - end - end - - def theme_color_tags(theme) - if theme == 'system' - tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:dark], media: '(prefers-color-scheme: dark)') + - tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:light], media: '(prefers-color-scheme: light)') - else - tag.meta name: 'theme-color', content: theme_color_for(theme) - end - end - - private - - def theme_color_for(theme) - theme == 'mastodon-light' ? Themes::THEME_COLORS[:light] : Themes::THEME_COLORS[:dark] - end -end diff --git a/app/javascript/entrypoints/admin.tsx b/app/javascript/entrypoints/admin.tsx deleted file mode 100644 index 64192f54ad..0000000000 --- a/app/javascript/entrypoints/admin.tsx +++ /dev/null @@ -1,440 +0,0 @@ -import './public-path'; -import { createRoot } from 'react-dom/client'; - -import Rails from '@rails/ujs'; - -import ready from '../mastodon/ready'; - -const setAnnouncementEndsAttributes = (target: HTMLInputElement) => { - const valid = target.value && target.validity.valid; - const element = document.querySelector( - 'input[type="datetime-local"]#announcement_ends_at', - ); - - if (!element) return; - - if (valid) { - element.classList.remove('optional'); - element.required = true; - element.min = target.value; - } else { - element.classList.add('optional'); - element.removeAttribute('required'); - element.removeAttribute('min'); - } -}; - -Rails.delegate( - document, - 'input[type="datetime-local"]#announcement_starts_at', - 'change', - ({ target }) => { - if (target instanceof HTMLInputElement) - setAnnouncementEndsAttributes(target); - }, -); - -const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]'; - -const showSelectAll = () => { - const selectAllMatchingElement = document.querySelector( - '.batch-table__select-all', - ); - selectAllMatchingElement?.classList.add('active'); -}; - -const hideSelectAll = () => { - const selectAllMatchingElement = document.querySelector( - '.batch-table__select-all', - ); - const hiddenField = document.querySelector( - 'input#select_all_matching', - ); - const selectedMsg = document.querySelector( - '.batch-table__select-all .selected', - ); - const notSelectedMsg = document.querySelector( - '.batch-table__select-all .not-selected', - ); - - selectAllMatchingElement?.classList.remove('active'); - selectedMsg?.classList.remove('active'); - notSelectedMsg?.classList.add('active'); - if (hiddenField) hiddenField.value = '0'; -}; - -Rails.delegate(document, '#batch_checkbox_all', 'change', ({ target }) => { - if (!(target instanceof HTMLInputElement)) return; - - const selectAllMatchingElement = document.querySelector( - '.batch-table__select-all', - ); - - document - .querySelectorAll(batchCheckboxClassName) - .forEach((content) => { - content.checked = target.checked; - }); - - if (selectAllMatchingElement) { - if (target.checked) { - showSelectAll(); - } else { - hideSelectAll(); - } - } -}); - -Rails.delegate(document, '.batch-table__select-all button', 'click', () => { - const hiddenField = document.querySelector( - '#select_all_matching', - ); - - if (!hiddenField) return; - - const active = hiddenField.value === '1'; - const selectedMsg = document.querySelector( - '.batch-table__select-all .selected', - ); - const notSelectedMsg = document.querySelector( - '.batch-table__select-all .not-selected', - ); - - if (!selectedMsg || !notSelectedMsg) return; - - if (active) { - hiddenField.value = '0'; - selectedMsg.classList.remove('active'); - notSelectedMsg.classList.add('active'); - } else { - hiddenField.value = '1'; - notSelectedMsg.classList.remove('active'); - selectedMsg.classList.add('active'); - } -}); - -Rails.delegate(document, batchCheckboxClassName, 'change', () => { - const checkAllElement = document.querySelector( - 'input#batch_checkbox_all', - ); - const selectAllMatchingElement = document.querySelector( - '.batch-table__select-all', - ); - - if (checkAllElement) { - const allCheckboxes = Array.from( - document.querySelectorAll(batchCheckboxClassName), - ); - checkAllElement.checked = allCheckboxes.every((content) => content.checked); - checkAllElement.indeterminate = - !checkAllElement.checked && - allCheckboxes.some((content) => content.checked); - - if (selectAllMatchingElement) { - if (checkAllElement.checked) { - showSelectAll(); - } else { - hideSelectAll(); - } - } - } -}); - -Rails.delegate( - document, - '.filter-subset--with-select select', - 'change', - ({ target }) => { - if (target instanceof HTMLSelectElement) target.form?.submit(); - }, -); - -const onDomainBlockSeverityChange = (target: HTMLSelectElement) => { - const rejectMediaDiv = document.querySelector( - '.input.with_label.domain_block_reject_media', - ); - const rejectReportsDiv = document.querySelector( - '.input.with_label.domain_block_reject_reports', - ); - - if (rejectMediaDiv && rejectMediaDiv instanceof HTMLElement) { - rejectMediaDiv.style.display = - target.value === 'suspend' ? 'none' : 'block'; - } - - if (rejectReportsDiv && rejectReportsDiv instanceof HTMLElement) { - rejectReportsDiv.style.display = - target.value === 'suspend' ? 'none' : 'block'; - } -}; - -Rails.delegate(document, '#domain_block_severity', 'change', ({ target }) => { - if (target instanceof HTMLSelectElement) onDomainBlockSeverityChange(target); -}); - -const onEnableBootstrapTimelineAccountsChange = (target: HTMLInputElement) => { - const bootstrapTimelineAccountsField = - document.querySelector( - '#form_admin_settings_bootstrap_timeline_accounts', - ); - - if (bootstrapTimelineAccountsField) { - bootstrapTimelineAccountsField.disabled = !target.checked; - if (target.checked) { - bootstrapTimelineAccountsField.parentElement?.classList.remove( - 'disabled', - ); - bootstrapTimelineAccountsField.parentElement?.parentElement?.classList.remove( - 'disabled', - ); - } else { - bootstrapTimelineAccountsField.parentElement?.classList.add('disabled'); - bootstrapTimelineAccountsField.parentElement?.parentElement?.classList.add( - 'disabled', - ); - } - } -}; - -Rails.delegate( - document, - '#form_admin_settings_enable_bootstrap_timeline_accounts', - 'change', - ({ target }) => { - if (target instanceof HTMLInputElement) - onEnableBootstrapTimelineAccountsChange(target); - }, -); - -const onChangeRegistrationMode = (target: HTMLSelectElement) => { - const enabled = target.value === 'approved'; - - document - .querySelectorAll( - '.form_admin_settings_registrations_mode .warning-hint', - ) - .forEach((warning_hint) => { - warning_hint.style.display = target.value === 'open' ? 'inline' : 'none'; - }); - - const toggleEnabled = (input: HTMLInputElement, value: boolean) => { - input.disabled = !value; - if (value) { - let element: HTMLElement | null = input; - do { - element.classList.remove('disabled'); - element = element.parentElement; - } while (element && !element.classList.contains('fields-group')); - } else { - let element: HTMLElement | null = input; - do { - element.classList.add('disabled'); - element = element.parentElement; - } while (element && !element.classList.contains('fields-group')); - } - }; - - document - .querySelectorAll( - 'input#form_admin_settings_require_invite_text', - ) - .forEach((input) => { - toggleEnabled(input, enabled); - }); - - document - .querySelectorAll( - '#form_admin_settings_registrations_start_hour, #form_admin_settings_registrations_end_hour, #form_admin_settings_registrations_secondary_start_hour, #form_admin_settings_registrations_secondary_end_hour', - ) - .forEach((input) => { - toggleEnabled(input, target.value === 'open'); - }); -}; - -const convertUTCDateTimeToLocal = (value: string) => { - const date = new Date(value + 'Z'); - const twoChars = (x: number) => x.toString().padStart(2, '0'); - return `${date.getFullYear()}-${twoChars(date.getMonth() + 1)}-${twoChars(date.getDate())}T${twoChars(date.getHours())}:${twoChars(date.getMinutes())}`; -}; - -function convertLocalDatetimeToUTC(value: string) { - const date = new Date(value); - const fullISO8601 = date.toISOString(); - return fullISO8601.slice(0, fullISO8601.indexOf('T') + 6); -} - -Rails.delegate( - document, - '#form_admin_settings_registrations_mode', - 'change', - ({ target }) => { - if (target instanceof HTMLSelectElement) onChangeRegistrationMode(target); - }, -); - -const addTableRow = (tableId: string) => { - const templateElement = document.querySelector(`#${tableId} .template-row`)!; // eslint-disable-line @typescript-eslint/no-non-null-assertion - const tableElement = document.querySelector(`#${tableId} tbody`)!; // eslint-disable-line @typescript-eslint/no-non-null-assertion - - if ( - typeof templateElement === 'undefined' || - typeof tableElement === 'undefined' - ) - return; - - let temporaryId = 0; - tableElement - .querySelectorAll('.temporary_id') - .forEach((input) => { - if (parseInt(input.value) + 1 > temporaryId) { - temporaryId = parseInt(input.value) + 1; - } - }); - - const cloned = templateElement.cloneNode(true) as HTMLTableRowElement; - cloned.className = ''; - cloned.querySelector('.temporary_id')!.value = // eslint-disable-line @typescript-eslint/no-non-null-assertion - temporaryId.toString(); - cloned - .querySelectorAll('input[type=checkbox]') - .forEach((input) => { - input.value = temporaryId.toString(); - }); - tableElement.appendChild(cloned); -}; - -const removeTableRow = (target: EventTarget | null, tableId: string) => { - const tableRowElement = (target as HTMLElement).closest('tr') as Node; - const tableElement = document.querySelector(`#${tableId} tbody`)!; // eslint-disable-line @typescript-eslint/no-non-null-assertion - - if ( - typeof tableRowElement === 'undefined' || - typeof tableElement === 'undefined' - ) - return; - - tableElement.removeChild(tableRowElement); -}; - -const setupTableList = (id: string) => { - Rails.delegate(document, `#${id} .add-row-button`, 'click', (ev) => { - ev.preventDefault(); - addTableRow(id); - }); - - Rails.delegate(document, `#${id} .delete-row-button`, 'click', (ev) => { - ev.preventDefault(); - removeTableRow(ev.target, id); - }); -}; - -setupTableList('sensitive-words-table'); -setupTableList('ng-words-table'); -setupTableList('white-list-table'); - -async function mountReactComponent(element: Element) { - const componentName = element.getAttribute('data-admin-component'); - const stringProps = element.getAttribute('data-props'); - - if (!stringProps) return; - - const componentProps = JSON.parse(stringProps) as object; - - const { default: AdminComponent } = await import( - '@/mastodon/containers/admin_component' - ); - - const { default: Component } = (await import( - `@/mastodon/components/admin/${componentName}` - )) as { default: React.ComponentType }; - - const root = createRoot(element); - - root.render( - - - , - ); -} - -ready(() => { - const domainBlockSeveritySelect = document.querySelector( - 'select#domain_block_severity', - ); - if (domainBlockSeveritySelect) - onDomainBlockSeverityChange(domainBlockSeveritySelect); - - const enableBootstrapTimelineAccounts = - document.querySelector( - 'input#form_admin_settings_enable_bootstrap_timeline_accounts', - ); - if (enableBootstrapTimelineAccounts) - onEnableBootstrapTimelineAccountsChange(enableBootstrapTimelineAccounts); - - const registrationMode = document.querySelector( - 'select#form_admin_settings_registrations_mode', - ); - if (registrationMode) onChangeRegistrationMode(registrationMode); - - const checkAllElement = document.querySelector( - 'input#batch_checkbox_all', - ); - if (checkAllElement) { - const allCheckboxes = Array.from( - document.querySelectorAll(batchCheckboxClassName), - ); - checkAllElement.checked = allCheckboxes.every((content) => content.checked); - checkAllElement.indeterminate = - !checkAllElement.checked && - allCheckboxes.some((content) => content.checked); - } - - document - .querySelector('a#add-instance-button') - ?.addEventListener('click', (e) => { - const domain = document.querySelector( - 'input[type="text"]#by_domain', - )?.value; - - if (domain && e.target instanceof HTMLAnchorElement) { - const url = new URL(e.target.href); - url.searchParams.set('_domain', domain); - e.target.href = url.toString(); - } - }); - - document - .querySelectorAll('input[type="datetime-local"]') - .forEach((element) => { - if (element.value) { - element.value = convertUTCDateTimeToLocal(element.value); - } - if (element.placeholder) { - element.placeholder = convertUTCDateTimeToLocal(element.placeholder); - } - }); - - Rails.delegate(document, 'form', 'submit', ({ target }) => { - if (target instanceof HTMLFormElement) - target - .querySelectorAll('input[type="datetime-local"]') - .forEach((element) => { - if (element.value && element.validity.valid) { - element.value = convertLocalDatetimeToUTC(element.value); - } - }); - }); - - const announcementStartsAt = document.querySelector( - 'input[type="datetime-local"]#announcement_starts_at', - ); - if (announcementStartsAt) { - setAnnouncementEndsAttributes(announcementStartsAt); - } - - document.querySelectorAll('[data-admin-component]').forEach((element) => { - void mountReactComponent(element); - }); -}).catch((reason: unknown) => { - throw reason; -}); diff --git a/app/javascript/entrypoints/public.tsx b/app/javascript/entrypoints/public.tsx deleted file mode 100644 index d45927226c..0000000000 --- a/app/javascript/entrypoints/public.tsx +++ /dev/null @@ -1,462 +0,0 @@ -import { createRoot } from 'react-dom/client'; - -import './public-path'; - -import { IntlMessageFormat } from 'intl-messageformat'; -import type { MessageDescriptor, PrimitiveType } from 'react-intl'; -import { defineMessages } from 'react-intl'; - -import Rails from '@rails/ujs'; -import axios from 'axios'; -import { throttle } from 'lodash'; - -import { start } from '../mastodon/common'; -import { timeAgoString } from '../mastodon/components/relative_timestamp'; -import emojify from '../mastodon/features/emoji/emoji'; -import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions'; -import { loadLocale, getLocale } from '../mastodon/locales'; -import { loadPolyfills } from '../mastodon/polyfills'; -import ready from '../mastodon/ready'; - -import 'cocoon-js-vanilla'; - -start(); - -const messages = defineMessages({ - usernameTaken: { - id: 'username.taken', - defaultMessage: 'That username is taken. Try another', - }, - passwordExceedsLength: { - id: 'password_confirmation.exceeds_maxlength', - defaultMessage: 'Password confirmation exceeds the maximum password length', - }, - passwordDoesNotMatch: { - id: 'password_confirmation.mismatching', - defaultMessage: 'Password confirmation does not match', - }, -}); - -interface SetHeightMessage { - type: 'setHeight'; - id: string; - height: number; -} - -function isSetHeightMessage(data: unknown): data is SetHeightMessage { - if ( - data && - typeof data === 'object' && - 'type' in data && - data.type === 'setHeight' - ) - return true; - else return false; -} - -window.addEventListener('message', (e) => { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- typings are not correct, it can be null in very rare cases - if (!e.data || !isSetHeightMessage(e.data) || !window.parent) return; - - const data = e.data; - - ready(() => { - window.parent.postMessage( - { - type: 'setHeight', - id: data.id, - height: document.getElementsByTagName('html')[0].scrollHeight, - }, - '*', - ); - }).catch((e: unknown) => { - console.error('Error in setHeightMessage postMessage', e); - }); -}); - -function loaded() { - const { messages: localeData } = getLocale(); - - const locale = document.documentElement.lang; - - const dateTimeFormat = new Intl.DateTimeFormat(locale, { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - }); - - const dateFormat = new Intl.DateTimeFormat(locale, { - year: 'numeric', - month: 'short', - day: 'numeric', - }); - - const timeFormat = new Intl.DateTimeFormat(locale, { - timeStyle: 'short', - }); - - const formatMessage = ( - { id, defaultMessage }: MessageDescriptor, - values?: Record, - ) => { - let message: string | undefined = undefined; - - if (id) message = localeData[id]; - - if (!message) message = defaultMessage as string; - - const messageFormat = new IntlMessageFormat(message, locale); - return messageFormat.format(values) as string; - }; - - document.querySelectorAll('.emojify').forEach((content) => { - content.innerHTML = emojify(content.innerHTML); - }); - - document - .querySelectorAll('time.formatted') - .forEach((content) => { - const datetime = new Date(content.dateTime); - const formattedDate = dateTimeFormat.format(datetime); - - content.title = formattedDate; - content.textContent = formattedDate; - }); - - const isToday = (date: Date) => { - const today = new Date(); - - return ( - date.getDate() === today.getDate() && - date.getMonth() === today.getMonth() && - date.getFullYear() === today.getFullYear() - ); - }; - const todayFormat = new IntlMessageFormat( - localeData['relative_format.today'] || 'Today at {time}', - locale, - ); - - document - .querySelectorAll('time.relative-formatted') - .forEach((content) => { - const datetime = new Date(content.dateTime); - - let formattedContent: string; - - if (isToday(datetime)) { - const formattedTime = timeFormat.format(datetime); - - formattedContent = todayFormat.format({ - time: formattedTime, - }) as string; - } else { - formattedContent = dateFormat.format(datetime); - } - - content.title = formattedContent; - content.textContent = formattedContent; - }); - - document - .querySelectorAll('time.time-ago') - .forEach((content) => { - const datetime = new Date(content.dateTime); - const now = new Date(); - - const timeGiven = content.dateTime.includes('T'); - content.title = timeGiven - ? dateTimeFormat.format(datetime) - : dateFormat.format(datetime); - content.textContent = timeAgoString( - { - formatMessage, - formatDate: (date: Date, options) => - new Intl.DateTimeFormat(locale, options).format(date), - }, - datetime, - now.getTime(), - now.getFullYear(), - timeGiven, - ); - }); - - const reactComponents = document.querySelectorAll('[data-component]'); - - if (reactComponents.length > 0) { - import( - /* webpackChunkName: "containers/media_container" */ '../mastodon/containers/media_container' - ) - .then(({ default: MediaContainer }) => { - reactComponents.forEach((component) => { - Array.from(component.children).forEach((child) => { - component.removeChild(child); - }); - }); - - const content = document.createElement('div'); - - const root = createRoot(content); - root.render( - , - ); - document.body.appendChild(content); - - return true; - }) - .catch((error: unknown) => { - console.error(error); - }); - } - - Rails.delegate( - document, - 'input#user_account_attributes_username', - 'input', - throttle( - ({ target }) => { - if (!(target instanceof HTMLInputElement)) return; - - if (target.value && target.value.length > 0) { - axios - .get('/api/v1/accounts/lookup', { params: { acct: target.value } }) - .then(() => { - target.setCustomValidity(formatMessage(messages.usernameTaken)); - return true; - }) - .catch(() => { - target.setCustomValidity(''); - }); - } else { - target.setCustomValidity(''); - } - }, - 500, - { leading: false, trailing: true }, - ), - ); - - Rails.delegate( - document, - '#user_password,#user_password_confirmation', - 'input', - () => { - const password = document.querySelector( - 'input#user_password', - ); - const confirmation = document.querySelector( - 'input#user_password_confirmation', - ); - if (!confirmation || !password) return; - - if ( - confirmation.value && - confirmation.value.length > password.maxLength - ) { - confirmation.setCustomValidity( - formatMessage(messages.passwordExceedsLength), - ); - } else if (password.value && password.value !== confirmation.value) { - confirmation.setCustomValidity( - formatMessage(messages.passwordDoesNotMatch), - ); - } else { - confirmation.setCustomValidity(''); - } - }, - ); - - Rails.delegate( - document, - 'button.status__content__spoiler-link', - 'click', - function () { - if (!(this instanceof HTMLButtonElement)) return; - - const statusEl = this.parentNode?.parentNode; - - if ( - !( - statusEl instanceof HTMLDivElement && - statusEl.classList.contains('.status__content') - ) - ) - return; - - if (statusEl.dataset.spoiler === 'expanded') { - statusEl.dataset.spoiler = 'folded'; - this.textContent = new IntlMessageFormat( - localeData['status.show_more'] || 'Show more', - locale, - ).format() as string; - } else { - statusEl.dataset.spoiler = 'expanded'; - this.textContent = new IntlMessageFormat( - localeData['status.show_less'] || 'Show less', - locale, - ).format() as string; - } - }, - ); - - document - .querySelectorAll('button.status__content__spoiler-link') - .forEach((spoilerLink) => { - const statusEl = spoilerLink.parentNode?.parentNode; - - if ( - !( - statusEl instanceof HTMLDivElement && - statusEl.classList.contains('.status__content') - ) - ) - return; - - const message = - statusEl.dataset.spoiler === 'expanded' - ? localeData['status.show_less'] || 'Show less' - : localeData['status.show_more'] || 'Show more'; - spoilerLink.textContent = new IntlMessageFormat( - message, - locale, - ).format() as string; - }); -} - -Rails.delegate( - document, - '#edit_profile input[type=file]', - 'change', - ({ target }) => { - if (!(target instanceof HTMLInputElement)) return; - - const avatar = document.querySelector( - `img#${target.id}-preview`, - ); - - if (!avatar) return; - - let file: File | undefined; - if (target.files) file = target.files[0]; - - const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc; - - if (url) avatar.src = url; - }, -); - -Rails.delegate(document, '.input-copy input', 'click', ({ target }) => { - if (!(target instanceof HTMLInputElement)) return; - - target.focus(); - target.select(); - target.setSelectionRange(0, target.value.length); -}); - -Rails.delegate(document, '.input-copy button', 'click', ({ target }) => { - if (!(target instanceof HTMLButtonElement)) return; - - const input = target.parentNode?.querySelector( - '.input-copy__wrapper input', - ); - - if (!input) return; - - const oldReadOnly = input.readOnly; - - input.readOnly = false; - input.focus(); - input.select(); - input.setSelectionRange(0, input.value.length); - - try { - if (document.execCommand('copy')) { - input.blur(); - - const parent = target.parentElement; - - if (!parent) return; - parent.classList.add('copied'); - - setTimeout(() => { - parent.classList.remove('copied'); - }, 700); - } - } catch (err) { - console.error(err); - } - - input.readOnly = oldReadOnly; -}); - -const toggleSidebar = () => { - const sidebar = document.querySelector('.sidebar ul'); - const toggleButton = document.querySelector( - 'a.sidebar__toggle__icon', - ); - - if (!sidebar || !toggleButton) return; - - if (sidebar.classList.contains('visible')) { - document.body.style.overflow = ''; - toggleButton.setAttribute('aria-expanded', 'false'); - } else { - document.body.style.overflow = 'hidden'; - toggleButton.setAttribute('aria-expanded', 'true'); - } - - toggleButton.classList.toggle('active'); - sidebar.classList.toggle('visible'); -}; - -Rails.delegate(document, '.sidebar__toggle__icon', 'click', () => { - toggleSidebar(); -}); - -Rails.delegate(document, '.sidebar__toggle__icon', 'keydown', (e) => { - if (e.key === ' ' || e.key === 'Enter') { - e.preventDefault(); - toggleSidebar(); - } -}); - -Rails.delegate(document, 'img.custom-emoji', 'mouseover', ({ target }) => { - if (target instanceof HTMLImageElement && target.dataset.original) - target.src = target.dataset.original; -}); -Rails.delegate(document, 'img.custom-emoji', 'mouseout', ({ target }) => { - if (target instanceof HTMLImageElement && target.dataset.static) - target.src = target.dataset.static; -}); - -// Empty the honeypot fields in JS in case something like an extension -// automatically filled them. -Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => { - [ - 'user_website', - 'user_confirm_password', - 'registration_user_website', - 'registration_user_confirm_password', - ].forEach((id) => { - const field = document.querySelector(`input#${id}`); - if (field) { - field.value = ''; - } - }); -}); - -function main() { - ready(loaded).catch((error: unknown) => { - console.error(error); - }); -} - -loadPolyfills() - .then(loadLocale) - .then(main) - .then(loadKeyboardExtensions) - .catch((error: unknown) => { - console.error(error); - }); diff --git a/app/javascript/entrypoints/sign_up.ts b/app/javascript/entrypoints/sign_up.ts deleted file mode 100644 index 880738fcb7..0000000000 --- a/app/javascript/entrypoints/sign_up.ts +++ /dev/null @@ -1,48 +0,0 @@ -import './public-path'; -import axios from 'axios'; - -import ready from '../mastodon/ready'; - -async function checkConfirmation() { - const response = await axios.get('/api/v1/emails/check_confirmation'); - - if (response.data) { - window.location.href = '/start'; - } -} - -ready(() => { - setInterval(() => { - void checkConfirmation(); - }, 5000); - - document - .querySelectorAll('button.timer-button') - .forEach((button) => { - let counter = 30; - - const container = document.createElement('span'); - - const updateCounter = () => { - container.innerText = ` (${counter})`; - }; - - updateCounter(); - - const countdown = setInterval(() => { - counter--; - - if (counter === 0) { - button.disabled = false; - button.removeChild(container); - clearInterval(countdown); - } else { - updateCounter(); - } - }, 1000); - - button.appendChild(container); - }); -}).catch((e: unknown) => { - throw e; -}); diff --git a/app/javascript/entrypoints/two_factor_authentication.ts b/app/javascript/entrypoints/two_factor_authentication.ts deleted file mode 100644 index 981481694b..0000000000 --- a/app/javascript/entrypoints/two_factor_authentication.ts +++ /dev/null @@ -1,197 +0,0 @@ -import * as WebAuthnJSON from '@github/webauthn-json'; -import axios, { AxiosError } from 'axios'; - -import ready from '../mastodon/ready'; - -import 'regenerator-runtime/runtime'; - -type PublicKeyCredentialCreationOptionsJSON = - WebAuthnJSON.CredentialCreationOptionsJSON['publicKey']; - -function exceptionHasAxiosError( - error: unknown, -): error is AxiosError<{ error: unknown }> { - return ( - error instanceof AxiosError && - typeof error.response?.data === 'object' && - 'error' in error.response.data - ); -} - -function logAxiosResponseError(error: unknown) { - if (exceptionHasAxiosError(error)) console.error(error); -} - -function getCSRFToken() { - return document - .querySelector('meta[name="csrf-token"]') - ?.getAttribute('content'); -} - -function hideFlashMessages() { - document.querySelectorAll('.flash-message').forEach((flashMessage) => { - flashMessage.classList.add('hidden'); - }); -} - -async function callback( - url: string, - body: - | { - credential: WebAuthnJSON.PublicKeyCredentialWithAttestationJSON; - nickname: string; - } - | { - user: { credential: WebAuthnJSON.PublicKeyCredentialWithAssertionJSON }; - }, -) { - try { - const response = await axios.post<{ redirect_path: string }>( - url, - JSON.stringify(body), - { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - 'X-CSRF-Token': getCSRFToken(), - }, - }, - ); - - window.location.replace(response.data.redirect_path); - } catch (error) { - if (error instanceof AxiosError && error.response?.status === 422) { - const errorMessage = document.getElementById( - 'security-key-error-message', - ); - errorMessage?.classList.remove('hidden'); - - logAxiosResponseError(error); - } else { - console.error(error); - } - } -} - -async function handleWebauthnCredentialRegistration(nickname: string) { - try { - const response = await axios.get( - '/settings/security_keys/options', - ); - - const credentialOptions = response.data; - - try { - const credential = await WebAuthnJSON.create({ - publicKey: credentialOptions, - }); - - const params = { - credential: credential, - nickname: nickname, - }; - - await callback('/settings/security_keys', params); - } catch (error) { - const errorMessage = document.getElementById( - 'security-key-error-message', - ); - errorMessage?.classList.remove('hidden'); - console.error(error); - } - } catch (error) { - logAxiosResponseError(error); - } -} - -async function handleWebauthnCredentialAuthentication() { - try { - const response = await axios.get( - 'sessions/security_key_options', - ); - - const credentialOptions = response.data; - - try { - const credential = await WebAuthnJSON.get({ - publicKey: credentialOptions, - }); - - const params = { user: { credential: credential } }; - void callback('sign_in', params); - } catch (error) { - const errorMessage = document.getElementById( - 'security-key-error-message', - ); - errorMessage?.classList.remove('hidden'); - console.error(error); - } - } catch (error) { - logAxiosResponseError(error); - } -} - -ready(() => { - if (!WebAuthnJSON.supported()) { - const unsupported_browser_message = document.getElementById( - 'unsupported-browser-message', - ); - if (unsupported_browser_message) { - unsupported_browser_message.classList.remove('hidden'); - const button = document.querySelector( - 'button.btn.js-webauthn', - ); - if (button) button.disabled = true; - } - } - - const webAuthnCredentialRegistrationForm = - document.querySelector('form#new_webauthn_credential'); - if (webAuthnCredentialRegistrationForm) { - webAuthnCredentialRegistrationForm.addEventListener('submit', (event) => { - event.preventDefault(); - - if (!(event.target instanceof HTMLFormElement)) return; - - const nickname = event.target.querySelector( - 'input[name="new_webauthn_credential[nickname]"]', - ); - - if (nickname?.value) { - void handleWebauthnCredentialRegistration(nickname.value); - } else { - nickname?.focus(); - } - }); - } - - const webAuthnCredentialAuthenticationForm = - document.getElementById('webauthn-form'); - if (webAuthnCredentialAuthenticationForm) { - webAuthnCredentialAuthenticationForm.addEventListener('submit', (event) => { - event.preventDefault(); - void handleWebauthnCredentialAuthentication(); - }); - - const otpAuthenticationForm = document.getElementById( - 'otp-authentication-form', - ); - - const linkToOtp = document.getElementById('link-to-otp'); - - linkToOtp?.addEventListener('click', () => { - webAuthnCredentialAuthenticationForm.classList.add('hidden'); - otpAuthenticationForm?.classList.remove('hidden'); - hideFlashMessages(); - }); - - const linkToWebAuthn = document.getElementById('link-to-webauthn'); - linkToWebAuthn?.addEventListener('click', () => { - otpAuthenticationForm?.classList.add('hidden'); - webAuthnCredentialAuthenticationForm.classList.remove('hidden'); - hideFlashMessages(); - }); - } -}).catch((e: unknown) => { - throw e; -}); diff --git a/app/javascript/fonts/inter/inter-variable-font-slnt-wght.woff2 b/app/javascript/fonts/inter/inter-variable-font-slnt-wght.woff2 deleted file mode 100644 index e6345f2e3d..0000000000 Binary files a/app/javascript/fonts/inter/inter-variable-font-slnt-wght.woff2 and /dev/null differ diff --git a/app/javascript/icons/android-chrome-144x144.png b/app/javascript/icons/android-chrome-144x144.png old mode 100755 new mode 100644 index d636e94c43..698fb4a260 Binary files a/app/javascript/icons/android-chrome-144x144.png and b/app/javascript/icons/android-chrome-144x144.png differ diff --git a/app/javascript/icons/android-chrome-192x192.png b/app/javascript/icons/android-chrome-192x192.png old mode 100755 new mode 100644 index 4a2681ffb9..2b6b632648 Binary files a/app/javascript/icons/android-chrome-192x192.png and b/app/javascript/icons/android-chrome-192x192.png differ diff --git a/app/javascript/icons/android-chrome-256x256.png b/app/javascript/icons/android-chrome-256x256.png old mode 100755 new mode 100644 index 8fab493ede..51e3849a26 Binary files a/app/javascript/icons/android-chrome-256x256.png and b/app/javascript/icons/android-chrome-256x256.png differ diff --git a/app/javascript/icons/android-chrome-36x36.png b/app/javascript/icons/android-chrome-36x36.png old mode 100755 new mode 100644 index 335d012db1..925f69c4fc Binary files a/app/javascript/icons/android-chrome-36x36.png and b/app/javascript/icons/android-chrome-36x36.png differ diff --git a/app/javascript/icons/android-chrome-384x384.png b/app/javascript/icons/android-chrome-384x384.png old mode 100755 new mode 100644 index 02b1e6fced..9d256a83cb Binary files a/app/javascript/icons/android-chrome-384x384.png and b/app/javascript/icons/android-chrome-384x384.png differ diff --git a/app/javascript/icons/android-chrome-48x48.png b/app/javascript/icons/android-chrome-48x48.png old mode 100755 new mode 100644 index 43cf411b8c..bcfe7475d0 Binary files a/app/javascript/icons/android-chrome-48x48.png and b/app/javascript/icons/android-chrome-48x48.png differ diff --git a/app/javascript/icons/android-chrome-512x512.png b/app/javascript/icons/android-chrome-512x512.png old mode 100755 new mode 100644 index 1856b80c7c..bffacfb699 Binary files a/app/javascript/icons/android-chrome-512x512.png and b/app/javascript/icons/android-chrome-512x512.png differ diff --git a/app/javascript/icons/android-chrome-72x72.png b/app/javascript/icons/android-chrome-72x72.png old mode 100755 new mode 100644 index 335008bf85..16679d5731 Binary files a/app/javascript/icons/android-chrome-72x72.png and b/app/javascript/icons/android-chrome-72x72.png differ diff --git a/app/javascript/icons/android-chrome-96x96.png b/app/javascript/icons/android-chrome-96x96.png old mode 100755 new mode 100644 index d1cb095822..9ade87cf32 Binary files a/app/javascript/icons/android-chrome-96x96.png and b/app/javascript/icons/android-chrome-96x96.png differ diff --git a/app/javascript/icons/apple-touch-icon-1024x1024.png b/app/javascript/icons/apple-touch-icon-1024x1024.png old mode 100755 new mode 100644 index c2a2d516ef..8ec371eb27 Binary files a/app/javascript/icons/apple-touch-icon-1024x1024.png and b/app/javascript/icons/apple-touch-icon-1024x1024.png differ diff --git a/app/javascript/icons/apple-touch-icon-114x114.png b/app/javascript/icons/apple-touch-icon-114x114.png old mode 100755 new mode 100644 index 218b415439..e1563f51e5 Binary files a/app/javascript/icons/apple-touch-icon-114x114.png and b/app/javascript/icons/apple-touch-icon-114x114.png differ diff --git a/app/javascript/icons/apple-touch-icon-120x120.png b/app/javascript/icons/apple-touch-icon-120x120.png old mode 100755 new mode 100644 index be53bc7c10..e9a5f5b0e5 Binary files a/app/javascript/icons/apple-touch-icon-120x120.png and b/app/javascript/icons/apple-touch-icon-120x120.png differ diff --git a/app/javascript/icons/apple-touch-icon-144x144.png b/app/javascript/icons/apple-touch-icon-144x144.png old mode 100755 new mode 100644 index cbb055732f..698fb4a260 Binary files a/app/javascript/icons/apple-touch-icon-144x144.png and b/app/javascript/icons/apple-touch-icon-144x144.png differ diff --git a/app/javascript/icons/apple-touch-icon-152x152.png b/app/javascript/icons/apple-touch-icon-152x152.png old mode 100755 new mode 100644 index 3a7975c054..0cc93cc288 Binary files a/app/javascript/icons/apple-touch-icon-152x152.png and b/app/javascript/icons/apple-touch-icon-152x152.png differ diff --git a/app/javascript/icons/apple-touch-icon-167x167.png b/app/javascript/icons/apple-touch-icon-167x167.png old mode 100755 new mode 100644 index 25be4eb5f5..9bbbf53120 Binary files a/app/javascript/icons/apple-touch-icon-167x167.png and b/app/javascript/icons/apple-touch-icon-167x167.png differ diff --git a/app/javascript/icons/apple-touch-icon-180x180.png b/app/javascript/icons/apple-touch-icon-180x180.png old mode 100755 new mode 100644 index dc0e9bc20b..329b803b91 Binary files a/app/javascript/icons/apple-touch-icon-180x180.png and b/app/javascript/icons/apple-touch-icon-180x180.png differ diff --git a/app/javascript/icons/apple-touch-icon-57x57.png b/app/javascript/icons/apple-touch-icon-57x57.png old mode 100755 new mode 100644 index bb0dc957cd..e00e142c64 Binary files a/app/javascript/icons/apple-touch-icon-57x57.png and b/app/javascript/icons/apple-touch-icon-57x57.png differ diff --git a/app/javascript/icons/apple-touch-icon-60x60.png b/app/javascript/icons/apple-touch-icon-60x60.png old mode 100755 new mode 100644 index 9143a0bf07..011285b564 Binary files a/app/javascript/icons/apple-touch-icon-60x60.png and b/app/javascript/icons/apple-touch-icon-60x60.png differ diff --git a/app/javascript/icons/apple-touch-icon-72x72.png b/app/javascript/icons/apple-touch-icon-72x72.png old mode 100755 new mode 100644 index 2b7d19484c..16679d5731 Binary files a/app/javascript/icons/apple-touch-icon-72x72.png and b/app/javascript/icons/apple-touch-icon-72x72.png differ diff --git a/app/javascript/icons/apple-touch-icon-76x76.png b/app/javascript/icons/apple-touch-icon-76x76.png old mode 100755 new mode 100644 index 0985e33bcb..83c8748876 Binary files a/app/javascript/icons/apple-touch-icon-76x76.png and b/app/javascript/icons/apple-touch-icon-76x76.png differ diff --git a/app/javascript/icons/favicon-16x16.png b/app/javascript/icons/favicon-16x16.png old mode 100755 new mode 100644 index 1326ba0462..eed8e0035c Binary files a/app/javascript/icons/favicon-16x16.png and b/app/javascript/icons/favicon-16x16.png differ diff --git a/app/javascript/icons/favicon-32x32.png b/app/javascript/icons/favicon-32x32.png old mode 100755 new mode 100644 index f5058cb0a5..9165746bcf Binary files a/app/javascript/icons/favicon-32x32.png and b/app/javascript/icons/favicon-32x32.png differ diff --git a/app/javascript/icons/favicon-48x48.png b/app/javascript/icons/favicon-48x48.png old mode 100755 new mode 100644 index 6253d054c7..259676c0a9 Binary files a/app/javascript/icons/favicon-48x48.png and b/app/javascript/icons/favicon-48x48.png differ diff --git a/app/javascript/images/check.svg b/app/javascript/images/check.svg deleted file mode 100644 index 8a0ebe878d..0000000000 --- a/app/javascript/images/check.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/app/javascript/images/mailer-new/common/header-bg-end.png b/app/javascript/images/mailer-new/common/header-bg-end.png deleted file mode 100644 index 900196678a..0000000000 Binary files a/app/javascript/images/mailer-new/common/header-bg-end.png and /dev/null 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 deleted file mode 100644 index 0037c1ad93..0000000000 Binary files a/app/javascript/images/mailer-new/common/header-bg-start.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/common/logo-footer.png b/app/javascript/images/mailer-new/common/logo-footer.png deleted file mode 100644 index 2baafd8d7f..0000000000 Binary files a/app/javascript/images/mailer-new/common/logo-footer.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/common/logo-header.png b/app/javascript/images/mailer-new/common/logo-header.png deleted file mode 100644 index 46a6bddaa1..0000000000 Binary files a/app/javascript/images/mailer-new/common/logo-header.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/2fa-disabled.png b/app/javascript/images/mailer-new/heading/2fa-disabled.png deleted file mode 100644 index b1e342a87c..0000000000 Binary files a/app/javascript/images/mailer-new/heading/2fa-disabled.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/2fa-enabled.png b/app/javascript/images/mailer-new/heading/2fa-enabled.png deleted file mode 100644 index 3ce3e04f84..0000000000 Binary files a/app/javascript/images/mailer-new/heading/2fa-enabled.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/2fa-recovery.png b/app/javascript/images/mailer-new/heading/2fa-recovery.png deleted file mode 100644 index cefb21e1eb..0000000000 Binary files a/app/javascript/images/mailer-new/heading/2fa-recovery.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/LICENSE b/app/javascript/images/mailer-new/heading/LICENSE deleted file mode 100644 index 974db1ac4b..0000000000 --- a/app/javascript/images/mailer-new/heading/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020-2024 Paweł Kuna - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/app/javascript/images/mailer-new/heading/README.md b/app/javascript/images/mailer-new/heading/README.md deleted file mode 100644 index ecd4b949e7..0000000000 --- a/app/javascript/images/mailer-new/heading/README.md +++ /dev/null @@ -1 +0,0 @@ -Images in this folder are based on [Tabler.io icons](https://tabler.io/icons). diff --git a/app/javascript/images/mailer-new/heading/appeal-approved.png b/app/javascript/images/mailer-new/heading/appeal-approved.png deleted file mode 100755 index b2476ec346..0000000000 Binary files a/app/javascript/images/mailer-new/heading/appeal-approved.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/appeal-rejected.png b/app/javascript/images/mailer-new/heading/appeal-rejected.png deleted file mode 100644 index 7ae38ad0c1..0000000000 Binary files a/app/javascript/images/mailer-new/heading/appeal-rejected.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/archive.png b/app/javascript/images/mailer-new/heading/archive.png deleted file mode 100644 index b0c7fad84d..0000000000 Binary files a/app/javascript/images/mailer-new/heading/archive.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/boost.png b/app/javascript/images/mailer-new/heading/boost.png deleted file mode 100644 index e33b759976..0000000000 Binary files a/app/javascript/images/mailer-new/heading/boost.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/email.png b/app/javascript/images/mailer-new/heading/email.png deleted file mode 100644 index c922c5239e..0000000000 Binary files a/app/javascript/images/mailer-new/heading/email.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/favorite.png b/app/javascript/images/mailer-new/heading/favorite.png deleted file mode 100644 index 0e483ee9b2..0000000000 Binary files a/app/javascript/images/mailer-new/heading/favorite.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/follow.png b/app/javascript/images/mailer-new/heading/follow.png deleted file mode 100644 index ff5b7e0042..0000000000 Binary files a/app/javascript/images/mailer-new/heading/follow.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/key-added.png b/app/javascript/images/mailer-new/heading/key-added.png deleted file mode 100755 index 82dcd464bf..0000000000 Binary files a/app/javascript/images/mailer-new/heading/key-added.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/key-deleted.png b/app/javascript/images/mailer-new/heading/key-deleted.png deleted file mode 100755 index 2930f591a0..0000000000 Binary files a/app/javascript/images/mailer-new/heading/key-deleted.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/key-disabled.png b/app/javascript/images/mailer-new/heading/key-disabled.png deleted file mode 100755 index e0f259359a..0000000000 Binary files a/app/javascript/images/mailer-new/heading/key-disabled.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/key-enabled.png b/app/javascript/images/mailer-new/heading/key-enabled.png deleted file mode 100644 index b2476ec346..0000000000 Binary files a/app/javascript/images/mailer-new/heading/key-enabled.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/login.png b/app/javascript/images/mailer-new/heading/login.png deleted file mode 100644 index 89a6e9ee33..0000000000 Binary files a/app/javascript/images/mailer-new/heading/login.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/mention.png b/app/javascript/images/mailer-new/heading/mention.png deleted file mode 100644 index c4dccff8ef..0000000000 Binary files a/app/javascript/images/mailer-new/heading/mention.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/password.png b/app/javascript/images/mailer-new/heading/password.png deleted file mode 100755 index 552c7c0687..0000000000 Binary files a/app/javascript/images/mailer-new/heading/password.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/user.png b/app/javascript/images/mailer-new/heading/user.png deleted file mode 100644 index f1dd58a18d..0000000000 Binary files a/app/javascript/images/mailer-new/heading/user.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/heading/warning.png b/app/javascript/images/mailer-new/heading/warning.png deleted file mode 100755 index 7764837abe..0000000000 Binary files a/app/javascript/images/mailer-new/heading/warning.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/store-icons/btn-app-store.png b/app/javascript/images/mailer-new/store-icons/btn-app-store.png deleted file mode 100644 index ee3bd9385c..0000000000 Binary files a/app/javascript/images/mailer-new/store-icons/btn-app-store.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/store-icons/btn-google-play.png b/app/javascript/images/mailer-new/store-icons/btn-google-play.png deleted file mode 100644 index ed43ff29aa..0000000000 Binary files a/app/javascript/images/mailer-new/store-icons/btn-google-play.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome-icons/LICENSE b/app/javascript/images/mailer-new/welcome-icons/LICENSE deleted file mode 100644 index 974db1ac4b..0000000000 --- a/app/javascript/images/mailer-new/welcome-icons/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020-2024 Paweł Kuna - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/app/javascript/images/mailer-new/welcome-icons/README.md b/app/javascript/images/mailer-new/welcome-icons/README.md deleted file mode 100644 index ecd4b949e7..0000000000 --- a/app/javascript/images/mailer-new/welcome-icons/README.md +++ /dev/null @@ -1 +0,0 @@ -Images in this folder are based on [Tabler.io icons](https://tabler.io/icons). diff --git a/app/javascript/images/mailer-new/welcome-icons/apps_step-off.png b/app/javascript/images/mailer-new/welcome-icons/apps_step-off.png deleted file mode 100755 index ca270f5478..0000000000 Binary files a/app/javascript/images/mailer-new/welcome-icons/apps_step-off.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome-icons/apps_step-on.png b/app/javascript/images/mailer-new/welcome-icons/apps_step-on.png deleted file mode 100644 index fd631bf97e..0000000000 Binary files a/app/javascript/images/mailer-new/welcome-icons/apps_step-on.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome-icons/edit_profile_step-off.png b/app/javascript/images/mailer-new/welcome-icons/edit_profile_step-off.png deleted file mode 100644 index dfcdd04e16..0000000000 Binary files a/app/javascript/images/mailer-new/welcome-icons/edit_profile_step-off.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome-icons/edit_profile_step-on.png b/app/javascript/images/mailer-new/welcome-icons/edit_profile_step-on.png deleted file mode 100644 index c3776d17df..0000000000 Binary files a/app/javascript/images/mailer-new/welcome-icons/edit_profile_step-on.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome-icons/follow_step-off.png b/app/javascript/images/mailer-new/welcome-icons/follow_step-off.png deleted file mode 100755 index a262454d2d..0000000000 Binary files a/app/javascript/images/mailer-new/welcome-icons/follow_step-off.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome-icons/follow_step-on.png b/app/javascript/images/mailer-new/welcome-icons/follow_step-on.png deleted file mode 100644 index 3ac011539b..0000000000 Binary files a/app/javascript/images/mailer-new/welcome-icons/follow_step-on.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome-icons/post_step-off.png b/app/javascript/images/mailer-new/welcome-icons/post_step-off.png deleted file mode 100755 index 972de65a56..0000000000 Binary files a/app/javascript/images/mailer-new/welcome-icons/post_step-off.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome-icons/post_step-on.png b/app/javascript/images/mailer-new/welcome-icons/post_step-on.png deleted file mode 100644 index aa318e66c8..0000000000 Binary files a/app/javascript/images/mailer-new/welcome-icons/post_step-on.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome-icons/share_step-off.png b/app/javascript/images/mailer-new/welcome-icons/share_step-off.png deleted file mode 100755 index f45e9a2c9a..0000000000 Binary files a/app/javascript/images/mailer-new/welcome-icons/share_step-off.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome-icons/share_step-on.png b/app/javascript/images/mailer-new/welcome-icons/share_step-on.png deleted file mode 100644 index 98782d9317..0000000000 Binary files a/app/javascript/images/mailer-new/welcome-icons/share_step-on.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome/checkbox-off.png b/app/javascript/images/mailer-new/welcome/checkbox-off.png deleted file mode 100644 index 51c190efe6..0000000000 Binary files a/app/javascript/images/mailer-new/welcome/checkbox-off.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome/checkbox-on.png b/app/javascript/images/mailer-new/welcome/checkbox-on.png deleted file mode 100644 index 162095e7df..0000000000 Binary files a/app/javascript/images/mailer-new/welcome/checkbox-on.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome/feature_audience.png b/app/javascript/images/mailer-new/welcome/feature_audience.png deleted file mode 100644 index 902de133b4..0000000000 Binary files a/app/javascript/images/mailer-new/welcome/feature_audience.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome/feature_control.png b/app/javascript/images/mailer-new/welcome/feature_control.png deleted file mode 100644 index 1afb6c238c..0000000000 Binary files a/app/javascript/images/mailer-new/welcome/feature_control.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome/feature_creativity.png b/app/javascript/images/mailer-new/welcome/feature_creativity.png deleted file mode 100644 index 3365856699..0000000000 Binary files a/app/javascript/images/mailer-new/welcome/feature_creativity.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome/feature_moderation.png b/app/javascript/images/mailer-new/welcome/feature_moderation.png deleted file mode 100644 index 7cee9b29b8..0000000000 Binary files a/app/javascript/images/mailer-new/welcome/feature_moderation.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome/purple-extra-soft-spacer.png b/app/javascript/images/mailer-new/welcome/purple-extra-soft-spacer.png deleted file mode 100644 index ec1ad5c957..0000000000 Binary files a/app/javascript/images/mailer-new/welcome/purple-extra-soft-spacer.png and /dev/null differ diff --git a/app/javascript/images/mailer-new/welcome/purple-extra-soft-wave.png b/app/javascript/images/mailer-new/welcome/purple-extra-soft-wave.png deleted file mode 100644 index ba8f6dd3d9..0000000000 Binary files a/app/javascript/images/mailer-new/welcome/purple-extra-soft-wave.png and /dev/null differ diff --git a/app/javascript/images/warning-stripes.svg b/app/javascript/images/warning-stripes.svg deleted file mode 100755 index 9d68acdada..0000000000 --- a/app/javascript/images/warning-stripes.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/javascript/mastodon/actions/antennas.js b/app/javascript/mastodon/actions/antennas.js deleted file mode 100644 index 4716897586..0000000000 --- a/app/javascript/mastodon/actions/antennas.js +++ /dev/null @@ -1,986 +0,0 @@ -import api from '../api'; - -import { showAlertForError } from './alerts'; -import { importFetchedAccounts } from './importer'; - -export const ANTENNA_FETCH_REQUEST = 'ANTENNA_FETCH_REQUEST'; -export const ANTENNA_FETCH_SUCCESS = 'ANTENNA_FETCH_SUCCESS'; -export const ANTENNA_FETCH_FAIL = 'ANTENNA_FETCH_FAIL'; - -export const ANTENNAS_FETCH_REQUEST = 'ANTENNAS_FETCH_REQUEST'; -export const ANTENNAS_FETCH_SUCCESS = 'ANTENNAS_FETCH_SUCCESS'; -export const ANTENNAS_FETCH_FAIL = 'ANTENNAS_FETCH_FAIL'; - -export const ANTENNA_EDITOR_TITLE_CHANGE = 'ANTENNA_EDITOR_TITLE_CHANGE'; -export const ANTENNA_EDITOR_RESET = 'ANTENNA_EDITOR_RESET'; -export const ANTENNA_EDITOR_SETUP = 'ANTENNA_EDITOR_SETUP'; - -export const ANTENNA_CREATE_REQUEST = 'ANTENNA_CREATE_REQUEST'; -export const ANTENNA_CREATE_SUCCESS = 'ANTENNA_CREATE_SUCCESS'; -export const ANTENNA_CREATE_FAIL = 'ANTENNA_CREATE_FAIL'; - -export const ANTENNA_UPDATE_REQUEST = 'ANTENNA_UPDATE_REQUEST'; -export const ANTENNA_UPDATE_SUCCESS = 'ANTENNA_UPDATE_SUCCESS'; -export const ANTENNA_UPDATE_FAIL = 'ANTENNA_UPDATE_FAIL'; - -export const ANTENNA_DELETE_REQUEST = 'ANTENNA_DELETE_REQUEST'; -export const ANTENNA_DELETE_SUCCESS = 'ANTENNA_DELETE_SUCCESS'; -export const ANTENNA_DELETE_FAIL = 'ANTENNA_DELETE_FAIL'; - -export const ANTENNA_ACCOUNTS_FETCH_REQUEST = 'ANTENNA_ACCOUNTS_FETCH_REQUEST'; -export const ANTENNA_ACCOUNTS_FETCH_SUCCESS = 'ANTENNA_ACCOUNTS_FETCH_SUCCESS'; -export const ANTENNA_ACCOUNTS_FETCH_FAIL = 'ANTENNA_ACCOUNTS_FETCH_FAIL'; - -export const ANTENNA_EDITOR_SUGGESTIONS_CHANGE = 'ANTENNA_EDITOR_SUGGESTIONS_CHANGE'; -export const ANTENNA_EDITOR_SUGGESTIONS_READY = 'ANTENNA_EDITOR_SUGGESTIONS_READY'; -export const ANTENNA_EDITOR_SUGGESTIONS_CLEAR = 'ANTENNA_EDITOR_SUGGESTIONS_CLEAR'; - -export const ANTENNA_EDITOR_ADD_REQUEST = 'ANTENNA_EDITOR_ADD_REQUEST'; -export const ANTENNA_EDITOR_ADD_SUCCESS = 'ANTENNA_EDITOR_ADD_SUCCESS'; -export const ANTENNA_EDITOR_ADD_FAIL = 'ANTENNA_EDITOR_ADD_FAIL'; - -export const ANTENNA_EDITOR_REMOVE_REQUEST = 'ANTENNA_EDITOR_REMOVE_REQUEST'; -export const ANTENNA_EDITOR_REMOVE_SUCCESS = 'ANTENNA_EDITOR_REMOVE_SUCCESS'; -export const ANTENNA_EDITOR_REMOVE_FAIL = 'ANTENNA_EDITOR_REMOVE_FAIL'; - -export const ANTENNA_EXCLUDE_ACCOUNTS_FETCH_REQUEST = 'ANTENNA_EXCLUDE_ACCOUNTS_FETCH_REQUEST'; -export const ANTENNA_EXCLUDE_ACCOUNTS_FETCH_SUCCESS = 'ANTENNA_EXCLUDE_ACCOUNTS_FETCH_SUCCESS'; -export const ANTENNA_EXCLUDE_ACCOUNTS_FETCH_FAIL = 'ANTENNA_EXCLUDE_ACCOUNTS_FETCH_FAIL'; - -export const ANTENNA_EDITOR_ADD_EXCLUDE_REQUEST = 'ANTENNA_EDITOR_ADD_EXCLUDE_REQUEST'; -export const ANTENNA_EDITOR_ADD_EXCLUDE_SUCCESS = 'ANTENNA_EDITOR_ADD_EXCLUDE_SUCCESS'; -export const ANTENNA_EDITOR_ADD_EXCLUDE_FAIL = 'ANTENNA_EDITOR_ADD_EXCLUDE_FAIL'; - -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_REQUEST = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_REQUEST'; -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_SUCCESS = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_SUCCESS'; -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_FAIL = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_FAIL'; - -export const ANTENNA_EDITOR_FETCH_DOMAINS_REQUEST = 'ANTENNA_EDITOR_FETCH_DOMAINS_REQUEST'; -export const ANTENNA_EDITOR_FETCH_DOMAINS_SUCCESS = 'ANTENNA_EDITOR_FETCH_DOMAINS_SUCCESS'; -export const ANTENNA_EDITOR_FETCH_DOMAINS_FAIL = 'ANTENNA_EDITOR_FETCH_DOMAINS_FAIL'; - -export const ANTENNA_EDITOR_ADD_DOMAIN_REQUEST = 'ANTENNA_EDITOR_ADD_DOMAIN_REQUEST'; -export const ANTENNA_EDITOR_ADD_DOMAIN_SUCCESS = 'ANTENNA_EDITOR_ADD_DOMAIN_SUCCESS'; -export const ANTENNA_EDITOR_ADD_DOMAIN_FAIL = 'ANTENNA_EDITOR_ADD_DOMAIN_FAIL'; - -export const ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_REQUEST = 'ANTENNA_EDITOR_ADD_EXCLUDEDOMAIN_REQUEST'; -export const ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_SUCCESS = 'ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_SUCCESS'; -export const ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_FAIL = 'ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_FAIL'; - -export const ANTENNA_EDITOR_REMOVE_DOMAIN_REQUEST = 'ANTENNA_EDITOR_REMOVE_DOMAIN_REQUEST'; -export const ANTENNA_EDITOR_REMOVE_DOMAIN_SUCCESS = 'ANTENNA_EDITOR_REMOVE_DOMAIN_SUCCESS'; -export const ANTENNA_EDITOR_REMOVE_DOMAIN_FAIL = 'ANTENNA_EDITOR_REMOVE_DOMAIN_FAIL'; - -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_REQUEST = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_REQUEST'; -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_SUCCESS = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_SUCCESS'; -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_FAIL = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_FAIL'; - -export const ANTENNA_EDITOR_FETCH_KEYWORDS_REQUEST = 'ANTENNA_EDITOR_FETCH_KEYWORDS_REQUEST'; -export const ANTENNA_EDITOR_FETCH_KEYWORDS_SUCCESS = 'ANTENNA_EDITOR_FETCH_KEYWORDS_SUCCESS'; -export const ANTENNA_EDITOR_FETCH_KEYWORDS_FAIL = 'ANTENNA_EDITOR_FETCH_KEYWORDS_FAIL'; - -export const ANTENNA_EDITOR_ADD_KEYWORD_REQUEST = 'ANTENNA_EDITOR_ADD_KEYWORD_REQUEST'; -export const ANTENNA_EDITOR_ADD_KEYWORD_SUCCESS = 'ANTENNA_EDITOR_ADD_KEYWORD_SUCCESS'; -export const ANTENNA_EDITOR_ADD_KEYWORD_FAIL = 'ANTENNA_EDITOR_ADD_KEYWORD_FAIL'; - -export const ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_REQUEST = 'ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_REQUEST'; -export const ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_SUCCESS = 'ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_SUCCESS'; -export const ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_FAIL = 'ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_FAIL'; - -export const ANTENNA_EDITOR_REMOVE_KEYWORD_REQUEST = 'ANTENNA_EDITOR_REMOVE_KEYWORD_REQUEST'; -export const ANTENNA_EDITOR_REMOVE_KEYWORD_SUCCESS = 'ANTENNA_EDITOR_REMOVE_KEYWORD_SUCCESS'; -export const ANTENNA_EDITOR_REMOVE_KEYWORD_FAIL = 'ANTENNA_EDITOR_REMOVE_KEYWORD_FAIL'; - -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_REQUEST = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_REQUEST'; -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_SUCCESS = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_SUCCESS'; -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_FAIL = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_FAIL'; - -export const ANTENNA_EDITOR_FETCH_TAGS_REQUEST = 'ANTENNA_EDITOR_FETCH_TAGS_REQUEST'; -export const ANTENNA_EDITOR_FETCH_TAGS_SUCCESS = 'ANTENNA_EDITOR_FETCH_TAGS_SUCCESS'; -export const ANTENNA_EDITOR_FETCH_TAGS_FAIL = 'ANTENNA_EDITOR_FETCH_TAGS_FAIL'; - -export const ANTENNA_EDITOR_ADD_TAG_REQUEST = 'ANTENNA_EDITOR_ADD_TAG_REQUEST'; -export const ANTENNA_EDITOR_ADD_TAG_SUCCESS = 'ANTENNA_EDITOR_ADD_TAG_SUCCESS'; -export const ANTENNA_EDITOR_ADD_TAG_FAIL = 'ANTENNA_EDITOR_ADD_TAG_FAIL'; - -export const ANTENNA_EDITOR_ADD_EXCLUDE_TAG_REQUEST = 'ANTENNA_EDITOR_ADD_EXCLUDE_TAG_REQUEST'; -export const ANTENNA_EDITOR_ADD_EXCLUDE_TAG_SUCCESS = 'ANTENNA_EDITOR_ADD_EXCLUDE_TAG_SUCCESS'; -export const ANTENNA_EDITOR_ADD_EXCLUDE_TAG_FAIL = 'ANTENNA_EDITOR_ADD_EXCLUDE_TAG_FAIL'; - -export const ANTENNA_EDITOR_REMOVE_TAG_REQUEST = 'ANTENNA_EDITOR_REMOVE_TAG_REQUEST'; -export const ANTENNA_EDITOR_REMOVE_TAG_SUCCESS = 'ANTENNA_EDITOR_REMOVE_TAG_SUCCESS'; -export const ANTENNA_EDITOR_REMOVE_TAG_FAIL = 'ANTENNA_EDITOR_REMOVE_TAG_FAIL'; - -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_REQUEST = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_REQUEST'; -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_SUCCESS = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_SUCCESS'; -export const ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_FAIL = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_FAIL'; - -export const ANTENNA_ADDER_RESET = 'ANTENNA_ADDER_RESET'; -export const ANTENNA_ADDER_SETUP = 'ANTENNA_ADDER_SETUP'; - -export const ANTENNA_ADDER_ANTENNAS_FETCH_REQUEST = 'ANTENNA_ADDER_ANTENNAS_FETCH_REQUEST'; -export const ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS = 'ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS'; -export const ANTENNA_ADDER_ANTENNAS_FETCH_FAIL = 'ANTENNA_ADDER_ANTENNAS_FETCH_FAIL'; - -export const ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_REQUEST = 'ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_REQUEST'; -export const ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_SUCCESS = 'ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_SUCCESS'; -export const ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_FAIL = 'ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_FAIL'; - -export const fetchAntenna = id => (dispatch, getState) => { - if (getState().getIn(['antennas', id])) { - return; - } - - dispatch(fetchAntennaRequest(id)); - - api(getState).get(`/api/v1/antennas/${id}`) - .then(({ data }) => dispatch(fetchAntennaSuccess(data))) - .catch(err => dispatch(fetchAntennaFail(id, err))); -}; - -export const fetchAntennaRequest = id => ({ - type: ANTENNA_FETCH_REQUEST, - id, -}); - -export const fetchAntennaSuccess = antenna => ({ - type: ANTENNA_FETCH_SUCCESS, - antenna, -}); - -export const fetchAntennaFail = (id, error) => ({ - type: ANTENNA_FETCH_FAIL, - id, - error, -}); - -export const fetchAntennas = () => (dispatch, getState) => { - dispatch(fetchAntennasRequest()); - - api(getState).get('/api/v1/antennas') - .then(({ data }) => dispatch(fetchAntennasSuccess(data))) - .catch(err => dispatch(fetchAntennasFail(err))); -}; - -export const fetchAntennasRequest = () => ({ - type: ANTENNAS_FETCH_REQUEST, -}); - -export const fetchAntennasSuccess = antennas => ({ - type: ANTENNAS_FETCH_SUCCESS, - antennas, -}); - -export const fetchAntennasFail = error => ({ - type: ANTENNAS_FETCH_FAIL, - error, -}); - -export const submitAntennaEditor = shouldReset => (dispatch, getState) => { - const antennaId = getState().getIn(['antennaEditor', 'antennaId']); - const title = getState().getIn(['antennaEditor', 'title']); - - if (antennaId === null) { - dispatch(createAntenna(title, shouldReset)); - } else { - dispatch(updateAntenna(antennaId, title, shouldReset)); - } -}; - -export const setupAntennaEditor = antennaId => (dispatch, getState) => { - dispatch({ - type: ANTENNA_EDITOR_SETUP, - antenna: getState().getIn(['antennas', antennaId]), - }); - - dispatch(fetchAntennaAccounts(antennaId)); -}; - -export const setupExcludeAntennaEditor = antennaId => (dispatch, getState) => { - dispatch({ - type: ANTENNA_EDITOR_SETUP, - antenna: getState().getIn(['antennas', antennaId]), - }); - - dispatch(fetchAntennaExcludeAccounts(antennaId)); -}; - -export const changeAntennaEditorTitle = value => ({ - type: ANTENNA_EDITOR_TITLE_CHANGE, - value, -}); - -export const createAntenna = (title, shouldReset) => (dispatch, getState) => { - dispatch(createAntennaRequest()); - - api(getState).post('/api/v1/antennas', { title }).then(({ data }) => { - dispatch(createAntennaSuccess(data)); - - if (shouldReset) { - dispatch(resetAntennaEditor()); - } - }).catch(err => dispatch(createAntennaFail(err))); -}; - -export const createAntennaRequest = () => ({ - type: ANTENNA_CREATE_REQUEST, -}); - -export const createAntennaSuccess = antenna => ({ - type: ANTENNA_CREATE_SUCCESS, - antenna, -}); - -export const createAntennaFail = error => ({ - type: ANTENNA_CREATE_FAIL, - error, -}); - -export const updateAntenna = (id, title, shouldReset, list_id, stl, ltl, with_media_only, ignore_reblog, insert_feeds) => (dispatch, getState) => { - dispatch(updateAntennaRequest(id)); - - api(getState).put(`/api/v1/antennas/${id}`, { title, list_id, stl, ltl, with_media_only, ignore_reblog, insert_feeds }).then(({ data }) => { - dispatch(updateAntennaSuccess(data)); - - if (shouldReset) { - dispatch(resetAntennaEditor()); - } - }).catch(err => dispatch(updateAntennaFail(id, err))); -}; - -export const updateAntennaRequest = id => ({ - type: ANTENNA_UPDATE_REQUEST, - id, -}); - -export const updateAntennaSuccess = antenna => ({ - type: ANTENNA_UPDATE_SUCCESS, - antenna, -}); - -export const updateAntennaFail = (id, error) => ({ - type: ANTENNA_UPDATE_FAIL, - id, - error, -}); - -export const resetAntennaEditor = () => ({ - type: ANTENNA_EDITOR_RESET, -}); - -export const deleteAntenna = id => (dispatch, getState) => { - dispatch(deleteAntennaRequest(id)); - - api(getState).delete(`/api/v1/antennas/${id}`) - .then(() => dispatch(deleteAntennaSuccess(id))) - .catch(err => dispatch(deleteAntennaFail(id, err))); -}; - -export const deleteAntennaRequest = id => ({ - type: ANTENNA_DELETE_REQUEST, - id, -}); - -export const deleteAntennaSuccess = id => ({ - type: ANTENNA_DELETE_SUCCESS, - id, -}); - -export const deleteAntennaFail = (id, error) => ({ - type: ANTENNA_DELETE_FAIL, - id, - error, -}); - -export const fetchAntennaAccounts = antennaId => (dispatch, getState) => { - dispatch(fetchAntennaAccountsRequest(antennaId)); - - api(getState).get(`/api/v1/antennas/${antennaId}/accounts`, { params: { limit: 0 } }).then(({ data }) => { - dispatch(importFetchedAccounts(data)); - dispatch(fetchAntennaAccountsSuccess(antennaId, data)); - }).catch(err => dispatch(fetchAntennaAccountsFail(antennaId, err))); -}; - -export const fetchAntennaAccountsRequest = id => ({ - type: ANTENNA_ACCOUNTS_FETCH_REQUEST, - id, -}); - -export const fetchAntennaAccountsSuccess = (id, accounts, next) => ({ - type: ANTENNA_ACCOUNTS_FETCH_SUCCESS, - id, - accounts, - next, -}); - -export const fetchAntennaAccountsFail = (id, error) => ({ - type: ANTENNA_ACCOUNTS_FETCH_FAIL, - id, - error, -}); - -export const fetchAntennaExcludeAccounts = antennaId => (dispatch, getState) => { - dispatch(fetchAntennaExcludeAccountsRequest(antennaId)); - - api(getState).get(`/api/v1/antennas/${antennaId}/exclude_accounts`, { params: { limit: 0 } }).then(({ data }) => { - dispatch(importFetchedAccounts(data)); - dispatch(fetchAntennaExcludeAccountsSuccess(antennaId, data)); - }).catch(err => dispatch(fetchAntennaExcludeAccountsFail(antennaId, err))); -}; - -export const fetchAntennaExcludeAccountsRequest = id => ({ - type: ANTENNA_EXCLUDE_ACCOUNTS_FETCH_REQUEST, - id, -}); - -export const fetchAntennaExcludeAccountsSuccess = (id, accounts, next) => ({ - type: ANTENNA_EXCLUDE_ACCOUNTS_FETCH_SUCCESS, - id, - accounts, - next, -}); - -export const fetchAntennaExcludeAccountsFail = (id, error) => ({ - type: ANTENNA_EXCLUDE_ACCOUNTS_FETCH_FAIL, - id, - error, -}); - -export const fetchAntennaSuggestions = q => (dispatch, getState) => { - const params = { - q, - resolve: false, - }; - - api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => { - dispatch(importFetchedAccounts(data)); - dispatch(fetchAntennaSuggestionsReady(q, data)); - }).catch(error => dispatch(showAlertForError(error))); -}; - -export const fetchAntennaSuggestionsReady = (query, accounts) => ({ - type: ANTENNA_EDITOR_SUGGESTIONS_READY, - query, - accounts, -}); - -export const clearAntennaSuggestions = () => ({ - type: ANTENNA_EDITOR_SUGGESTIONS_CLEAR, -}); - -export const changeAntennaSuggestions = value => ({ - type: ANTENNA_EDITOR_SUGGESTIONS_CHANGE, - value, -}); - -export const addToAntennaEditor = accountId => (dispatch, getState) => { - dispatch(addToAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId)); -}; - -export const addToAntenna = (antennaId, accountId) => (dispatch, getState) => { - dispatch(addToAntennaRequest(antennaId, accountId)); - - api(getState).post(`/api/v1/antennas/${antennaId}/accounts`, { account_ids: [accountId] }) - .then(() => dispatch(addToAntennaSuccess(antennaId, accountId))) - .catch(err => dispatch(addToAntennaFail(antennaId, accountId, err))); -}; - -export const addToAntennaRequest = (antennaId, accountId) => ({ - type: ANTENNA_EDITOR_ADD_REQUEST, - antennaId, - accountId, -}); - -export const addToAntennaSuccess = (antennaId, accountId) => ({ - type: ANTENNA_EDITOR_ADD_SUCCESS, - antennaId, - accountId, -}); - -export const addToAntennaFail = (antennaId, accountId, error) => ({ - type: ANTENNA_EDITOR_ADD_FAIL, - antennaId, - accountId, - error, -}); - -export const addExcludeToAntennaEditor = accountId => (dispatch, getState) => { - dispatch(addExcludeToAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId)); -}; - -export const addExcludeToAntenna = (antennaId, accountId) => (dispatch, getState) => { - dispatch(addExcludeToAntennaRequest(antennaId, accountId)); - - api(getState).post(`/api/v1/antennas/${antennaId}/exclude_accounts`, { account_ids: [accountId] }) - .then(() => dispatch(addExcludeToAntennaSuccess(antennaId, accountId))) - .catch(err => dispatch(addExcludeToAntennaFail(antennaId, accountId, err))); -}; - -export const addExcludeToAntennaRequest = (antennaId, accountId) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_REQUEST, - antennaId, - accountId, -}); - -export const addExcludeToAntennaSuccess = (antennaId, accountId) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_SUCCESS, - antennaId, - accountId, -}); - -export const addExcludeToAntennaFail = (antennaId, accountId, error) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_FAIL, - antennaId, - accountId, - error, -}); - -export const removeFromAntennaEditor = accountId => (dispatch, getState) => { - dispatch(removeFromAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId)); -}; - -export const removeFromAntenna = (antennaId, accountId) => (dispatch, getState) => { - dispatch(removeFromAntennaRequest(antennaId, accountId)); - - api(getState).delete(`/api/v1/antennas/${antennaId}/accounts`, { params: { account_ids: [accountId] } }) - .then(() => dispatch(removeFromAntennaSuccess(antennaId, accountId))) - .catch(err => dispatch(removeFromAntennaFail(antennaId, accountId, err))); -}; - -export const removeFromAntennaRequest = (antennaId, accountId) => ({ - type: ANTENNA_EDITOR_REMOVE_REQUEST, - antennaId, - accountId, -}); - -export const removeFromAntennaSuccess = (antennaId, accountId) => ({ - type: ANTENNA_EDITOR_REMOVE_SUCCESS, - antennaId, - accountId, -}); - -export const removeFromAntennaFail = (antennaId, accountId, error) => ({ - type: ANTENNA_EDITOR_REMOVE_FAIL, - antennaId, - accountId, - error, -}); - -export const removeExcludeFromAntennaEditor = accountId => (dispatch, getState) => { - dispatch(removeExcludeFromAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId)); -}; - -export const removeExcludeFromAntenna = (antennaId, accountId) => (dispatch, getState) => { - dispatch(removeExcludeFromAntennaRequest(antennaId, accountId)); - - api(getState).delete(`/api/v1/antennas/${antennaId}/exclude_accounts`, { params: { account_ids: [accountId] } }) - .then(() => dispatch(removeExcludeFromAntennaSuccess(antennaId, accountId))) - .catch(err => dispatch(removeExcludeFromAntennaFail(antennaId, accountId, err))); -}; - -export const removeExcludeFromAntennaRequest = (antennaId, accountId) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_REQUEST, - antennaId, - accountId, -}); - -export const removeExcludeFromAntennaSuccess = (antennaId, accountId) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_SUCCESS, - antennaId, - accountId, -}); - -export const removeExcludeFromAntennaFail = (antennaId, accountId, error) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_FAIL, - antennaId, - accountId, - error, -}); - -export const fetchAntennaDomains = antennaId => (dispatch, getState) => { - dispatch(fetchAntennaDomainsRequest(antennaId)); - - api(getState).get(`/api/v1/antennas/${antennaId}/domains`, { params: { limit: 0 } }).then(({ data }) => { - dispatch(fetchAntennaDomainsSuccess(antennaId, data)); - }).catch(err => dispatch(fetchAntennaDomainsFail(antennaId, err))); -}; - -export const fetchAntennaDomainsRequest = id => ({ - type: ANTENNA_EDITOR_FETCH_DOMAINS_REQUEST, - id, -}); - -export const fetchAntennaDomainsSuccess = (id, domains) => ({ - type: ANTENNA_EDITOR_FETCH_DOMAINS_SUCCESS, - id, - domains, -}); - -export const fetchAntennaDomainsFail = (id, error) => ({ - type: ANTENNA_EDITOR_FETCH_DOMAINS_FAIL, - id, - error, -}); - -export const addDomainToAntenna = (antennaId, domain) => (dispatch, getState) => { - dispatch(addDomainToAntennaRequest(antennaId, domain)); - - api(getState).post(`/api/v1/antennas/${antennaId}/domains`, { domains: [domain] }) - .then(() => dispatch(addDomainToAntennaSuccess(antennaId, domain))) - .catch(err => dispatch(addDomainToAntennaFail(antennaId, domain, err))); -}; - -export const addDomainToAntennaRequest = (antennaId, domain) => ({ - type: ANTENNA_EDITOR_ADD_DOMAIN_REQUEST, - antennaId, - domain, -}); - -export const addDomainToAntennaSuccess = (antennaId, domain) => ({ - type: ANTENNA_EDITOR_ADD_DOMAIN_SUCCESS, - antennaId, - domain, -}); - -export const addDomainToAntennaFail = (antennaId, domain, error) => ({ - type: ANTENNA_EDITOR_ADD_DOMAIN_FAIL, - antennaId, - domain, - error, -}); - -export const removeDomainFromAntenna = (antennaId, domain) => (dispatch, getState) => { - dispatch(removeDomainFromAntennaRequest(antennaId, domain)); - - api(getState).delete(`/api/v1/antennas/${antennaId}/domains`, { params: { domains: [domain] } }) - .then(() => dispatch(removeDomainFromAntennaSuccess(antennaId, domain))) - .catch(err => dispatch(removeDomainFromAntennaFail(antennaId, domain, err))); -}; - -export const removeDomainFromAntennaRequest = (antennaId, domain) => ({ - type: ANTENNA_EDITOR_REMOVE_DOMAIN_REQUEST, - antennaId, - domain, -}); - -export const removeDomainFromAntennaSuccess = (antennaId, domain) => ({ - type: ANTENNA_EDITOR_REMOVE_DOMAIN_SUCCESS, - antennaId, - domain, -}); - -export const removeDomainFromAntennaFail = (antennaId, domain, error) => ({ - type: ANTENNA_EDITOR_REMOVE_DOMAIN_FAIL, - antennaId, - domain, - error, -}); - -export const addExcludeDomainToAntenna = (antennaId, domain) => (dispatch, getState) => { - dispatch(addExcludeDomainToAntennaRequest(antennaId, domain)); - - api(getState).post(`/api/v1/antennas/${antennaId}/exclude_domains`, { domains: [domain] }) - .then(() => dispatch(addExcludeDomainToAntennaSuccess(antennaId, domain))) - .catch(err => dispatch(addExcludeDomainToAntennaFail(antennaId, domain, err))); -}; - -export const addExcludeDomainToAntennaRequest = (antennaId, domain) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_REQUEST, - antennaId, - domain, -}); - -export const addExcludeDomainToAntennaSuccess = (antennaId, domain) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_SUCCESS, - antennaId, - domain, -}); - -export const addExcludeDomainToAntennaFail = (antennaId, domain, error) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_FAIL, - antennaId, - domain, - error, -}); - -export const removeExcludeDomainFromAntenna = (antennaId, domain) => (dispatch, getState) => { - dispatch(removeExcludeDomainFromAntennaRequest(antennaId, domain)); - - api(getState).delete(`/api/v1/antennas/${antennaId}/exclude_domains`, { params: { domains: [domain] } }) - .then(() => dispatch(removeExcludeDomainFromAntennaSuccess(antennaId, domain))) - .catch(err => dispatch(removeExcludeDomainFromAntennaFail(antennaId, domain, err))); -}; - -export const removeExcludeDomainFromAntennaRequest = (antennaId, domain) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_REQUEST, - antennaId, - domain, -}); - -export const removeExcludeDomainFromAntennaSuccess = (antennaId, domain) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_SUCCESS, - antennaId, - domain, -}); - -export const removeExcludeDomainFromAntennaFail = (antennaId, domain, error) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_FAIL, - antennaId, - domain, - error, -}); - -export const fetchAntennaKeywords = antennaId => (dispatch, getState) => { - dispatch(fetchAntennaKeywordsRequest(antennaId)); - - api(getState).get(`/api/v1/antennas/${antennaId}/keywords`, { params: { limit: 0 } }).then(({ data }) => { - dispatch(fetchAntennaKeywordsSuccess(antennaId, data)); - }).catch(err => dispatch(fetchAntennaKeywordsFail(antennaId, err))); -}; - -export const fetchAntennaKeywordsRequest = id => ({ - type: ANTENNA_EDITOR_FETCH_KEYWORDS_REQUEST, - id, -}); - -export const fetchAntennaKeywordsSuccess = (id, keywords) => ({ - type: ANTENNA_EDITOR_FETCH_KEYWORDS_SUCCESS, - id, - keywords, -}); - -export const fetchAntennaKeywordsFail = (id, error) => ({ - type: ANTENNA_EDITOR_FETCH_KEYWORDS_FAIL, - id, - error, -}); - -export const addKeywordToAntenna = (antennaId, keyword) => (dispatch, getState) => { - dispatch(addKeywordToAntennaRequest(antennaId, keyword)); - - api(getState).post(`/api/v1/antennas/${antennaId}/keywords`, { keywords: [keyword] }) - .then(() => dispatch(addKeywordToAntennaSuccess(antennaId, keyword))) - .catch(err => dispatch(addKeywordToAntennaFail(antennaId, keyword, err))); -}; - -export const addKeywordToAntennaRequest = (antennaId, keyword) => ({ - type: ANTENNA_EDITOR_ADD_KEYWORD_REQUEST, - antennaId, - keyword, -}); - -export const addKeywordToAntennaSuccess = (antennaId, keyword) => ({ - type: ANTENNA_EDITOR_ADD_KEYWORD_SUCCESS, - antennaId, - keyword, -}); - -export const addKeywordToAntennaFail = (antennaId, keyword, error) => ({ - type: ANTENNA_EDITOR_ADD_KEYWORD_FAIL, - antennaId, - keyword, - error, -}); - -export const removeKeywordFromAntenna = (antennaId, keyword) => (dispatch, getState) => { - dispatch(removeKeywordFromAntennaRequest(antennaId, keyword)); - - api(getState).delete(`/api/v1/antennas/${antennaId}/keywords`, { params: { keywords: [keyword] } }) - .then(() => dispatch(removeKeywordFromAntennaSuccess(antennaId, keyword))) - .catch(err => dispatch(removeKeywordFromAntennaFail(antennaId, keyword, err))); -}; - -export const removeKeywordFromAntennaRequest = (antennaId, keyword) => ({ - type: ANTENNA_EDITOR_REMOVE_KEYWORD_REQUEST, - antennaId, - keyword, -}); - -export const removeKeywordFromAntennaSuccess = (antennaId, keyword) => ({ - type: ANTENNA_EDITOR_REMOVE_KEYWORD_SUCCESS, - antennaId, - keyword, -}); - -export const removeKeywordFromAntennaFail = (antennaId, keyword, error) => ({ - type: ANTENNA_EDITOR_REMOVE_KEYWORD_FAIL, - antennaId, - keyword, - error, -}); - -export const addExcludeKeywordToAntenna = (antennaId, keyword) => (dispatch, getState) => { - dispatch(addExcludeKeywordToAntennaRequest(antennaId, keyword)); - - api(getState).post(`/api/v1/antennas/${antennaId}/exclude_keywords`, { keywords: [keyword] }) - .then(() => dispatch(addExcludeKeywordToAntennaSuccess(antennaId, keyword))) - .catch(err => dispatch(addExcludeKeywordToAntennaFail(antennaId, keyword, err))); -}; - -export const addExcludeKeywordToAntennaRequest = (antennaId, keyword) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_REQUEST, - antennaId, - keyword, -}); - -export const addExcludeKeywordToAntennaSuccess = (antennaId, keyword) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_SUCCESS, - antennaId, - keyword, -}); - -export const addExcludeKeywordToAntennaFail = (antennaId, keyword, error) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_FAIL, - antennaId, - keyword, - error, -}); - -export const removeExcludeKeywordFromAntenna = (antennaId, keyword) => (dispatch, getState) => { - dispatch(removeExcludeKeywordFromAntennaRequest(antennaId, keyword)); - - api(getState).delete(`/api/v1/antennas/${antennaId}/exclude_keywords`, { params: { keywords: [keyword] } }) - .then(() => dispatch(removeExcludeKeywordFromAntennaSuccess(antennaId, keyword))) - .catch(err => dispatch(removeExcludeKeywordFromAntennaFail(antennaId, keyword, err))); -}; - -export const removeExcludeKeywordFromAntennaRequest = (antennaId, keyword) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_REQUEST, - antennaId, - keyword, -}); - -export const removeExcludeKeywordFromAntennaSuccess = (antennaId, keyword) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_SUCCESS, - antennaId, - keyword, -}); - -export const removeExcludeKeywordFromAntennaFail = (antennaId, keyword, error) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_FAIL, - antennaId, - keyword, - error, -}); - -export const fetchAntennaTags = antennaId => (dispatch, getState) => { - dispatch(fetchAntennaTagsRequest(antennaId)); - - api(getState).get(`/api/v1/antennas/${antennaId}/tags`, { params: { limit: 0 } }).then(({ data }) => { - dispatch(fetchAntennaTagsSuccess(antennaId, data)); - }).catch(err => dispatch(fetchAntennaTagsFail(antennaId, err))); -}; - -export const fetchAntennaTagsRequest = id => ({ - type: ANTENNA_EDITOR_FETCH_TAGS_REQUEST, - id, -}); - -export const fetchAntennaTagsSuccess = (id, tags) => ({ - type: ANTENNA_EDITOR_FETCH_TAGS_SUCCESS, - id, - tags, -}); - -export const fetchAntennaTagsFail = (id, error) => ({ - type: ANTENNA_EDITOR_FETCH_TAGS_FAIL, - id, - error, -}); - -export const addTagToAntenna = (antennaId, tag) => (dispatch, getState) => { - dispatch(addTagToAntennaRequest(antennaId, tag)); - - api(getState).post(`/api/v1/antennas/${antennaId}/tags`, { tags: [tag] }) - .then(() => dispatch(addTagToAntennaSuccess(antennaId, tag))) - .catch(err => dispatch(addTagToAntennaFail(antennaId, tag, err))); -}; - -export const addTagToAntennaRequest = (antennaId, tag) => ({ - type: ANTENNA_EDITOR_ADD_TAG_REQUEST, - antennaId, - tag, -}); - -export const addTagToAntennaSuccess = (antennaId, tag) => ({ - type: ANTENNA_EDITOR_ADD_TAG_SUCCESS, - antennaId, - tag, -}); - -export const addTagToAntennaFail = (antennaId, tag, error) => ({ - type: ANTENNA_EDITOR_ADD_TAG_FAIL, - antennaId, - tag, - error, -}); - -export const removeTagFromAntenna = (antennaId, tag) => (dispatch, getState) => { - dispatch(removeTagFromAntennaRequest(antennaId, tag)); - - api(getState).delete(`/api/v1/antennas/${antennaId}/tags`, { params: { tags: [tag] } }) - .then(() => dispatch(removeTagFromAntennaSuccess(antennaId, tag))) - .catch(err => dispatch(removeTagFromAntennaFail(antennaId, tag, err))); -}; - -export const removeTagFromAntennaRequest = (antennaId, tag) => ({ - type: ANTENNA_EDITOR_REMOVE_TAG_REQUEST, - antennaId, - tag, -}); - -export const removeTagFromAntennaSuccess = (antennaId, tag) => ({ - type: ANTENNA_EDITOR_REMOVE_TAG_SUCCESS, - antennaId, - tag, -}); - -export const removeTagFromAntennaFail = (antennaId, tag, error) => ({ - type: ANTENNA_EDITOR_REMOVE_TAG_FAIL, - antennaId, - tag, - error, -}); - -export const addExcludeTagToAntenna = (antennaId, tag) => (dispatch, getState) => { - dispatch(addExcludeTagToAntennaRequest(antennaId, tag)); - - api(getState).post(`/api/v1/antennas/${antennaId}/exclude_tags`, { tags: [tag] }) - .then(() => dispatch(addExcludeTagToAntennaSuccess(antennaId, tag))) - .catch(err => dispatch(addExcludeTagToAntennaFail(antennaId, tag, err))); -}; - -export const addExcludeTagToAntennaRequest = (antennaId, tag) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_TAG_REQUEST, - antennaId, - tag, -}); - -export const addExcludeTagToAntennaSuccess = (antennaId, tag) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_TAG_SUCCESS, - antennaId, - tag, -}); - -export const addExcludeTagToAntennaFail = (antennaId, tag, error) => ({ - type: ANTENNA_EDITOR_ADD_EXCLUDE_TAG_FAIL, - antennaId, - tag, - error, -}); - -export const removeExcludeTagFromAntenna = (antennaId, tag) => (dispatch, getState) => { - dispatch(removeExcludeTagFromAntennaRequest(antennaId, tag)); - - api(getState).delete(`/api/v1/antennas/${antennaId}/exclude_tags`, { params: { tags: [tag] } }) - .then(() => dispatch(removeExcludeTagFromAntennaSuccess(antennaId, tag))) - .catch(err => dispatch(removeExcludeTagFromAntennaFail(antennaId, tag, err))); -}; - -export const removeExcludeTagFromAntennaRequest = (antennaId, tag) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_REQUEST, - antennaId, - tag, -}); - -export const removeExcludeTagFromAntennaSuccess = (antennaId, tag) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_SUCCESS, - antennaId, - tag, -}); - -export const removeExcludeTagFromAntennaFail = (antennaId, tag, error) => ({ - type: ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_FAIL, - antennaId, - tag, - error, -}); - -export const resetAntennaAdder = () => ({ - type: ANTENNA_ADDER_RESET, -}); - -export const setupAntennaAdder = accountId => (dispatch, getState) => { - dispatch({ - type: ANTENNA_ADDER_SETUP, - account: getState().getIn(['accounts', accountId]), - }); - dispatch(fetchAntennas()); - dispatch(fetchAccountAntennas(accountId)); -}; - -export const setupExcludeAntennaAdder = accountId => (dispatch, getState) => { - dispatch({ - type: ANTENNA_ADDER_SETUP, - account: getState().getIn(['accounts', accountId]), - }); - dispatch(fetchAntennas()); - dispatch(fetchExcludeAccountAntennas(accountId)); -}; - -export const fetchAccountAntennas = accountId => (dispatch, getState) => { - dispatch(fetchAccountAntennasRequest(accountId)); - - api(getState).get(`/api/v1/accounts/${accountId}/antennas`) - .then(({ data }) => dispatch(fetchAccountAntennasSuccess(accountId, data))) - .catch(err => dispatch(fetchAccountAntennasFail(accountId, err))); -}; - -export const fetchAccountAntennasRequest = id => ({ - type:ANTENNA_ADDER_ANTENNAS_FETCH_REQUEST, - id, -}); - -export const fetchAccountAntennasSuccess = (id, antennas) => ({ - type: ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS, - id, - antennas, -}); - -export const fetchAccountAntennasFail = (id, err) => ({ - type: ANTENNA_ADDER_ANTENNAS_FETCH_FAIL, - id, - err, -}); - -export const fetchExcludeAccountAntennas = accountId => (dispatch, getState) => { - dispatch(fetchExcludeAccountAntennasRequest(accountId)); - - api(getState).get(`/api/v1/accounts/${accountId}/exclude_antennas`) - .then(({ data }) => dispatch(fetchExcludeAccountAntennasSuccess(accountId, data))) - .catch(err => dispatch(fetchExcludeAccountAntennasFail(accountId, err))); -}; - -export const fetchExcludeAccountAntennasRequest = id => ({ - type:ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_REQUEST, - id, -}); - -export const fetchExcludeAccountAntennasSuccess = (id, antennas) => ({ - type: ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_SUCCESS, - id, - antennas, -}); - -export const fetchExcludeAccountAntennasFail = (id, err) => ({ - type: ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_FAIL, - id, - err, -}); - -export const addToAntennaAdder = antennaId => (dispatch, getState) => { - dispatch(addToAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId']))); -}; - -export const removeFromAntennaAdder = antennaId => (dispatch, getState) => { - dispatch(removeFromAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId']))); -}; - -export const addExcludeToAntennaAdder = antennaId => (dispatch, getState) => { - dispatch(addExcludeToAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId']))); -}; - -export const removeExcludeFromAntennaAdder = antennaId => (dispatch, getState) => { - dispatch(removeExcludeFromAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId']))); -}; - diff --git a/app/javascript/mastodon/actions/blocks.js b/app/javascript/mastodon/actions/blocks.js index 54296d0905..e293657ad3 100644 --- a/app/javascript/mastodon/actions/blocks.js +++ b/app/javascript/mastodon/actions/blocks.js @@ -12,6 +12,8 @@ export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST'; export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS'; export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL'; +export const BLOCKS_INIT_MODAL = 'BLOCKS_INIT_MODAL'; + export function fetchBlocks() { return (dispatch, getState) => { dispatch(fetchBlocksRequest()); @@ -88,12 +90,11 @@ export function expandBlocksFail(error) { export function initBlockModal(account) { return dispatch => { - dispatch(openModal({ - modalType: 'BLOCK', - modalProps: { - accountId: account.get('id'), - acct: account.get('acct'), - }, - })); + dispatch({ + type: BLOCKS_INIT_MODAL, + account, + }); + + dispatch(openModal({ modalType: 'BLOCK' })); }; } diff --git a/app/javascript/mastodon/actions/bookmark_categories.js b/app/javascript/mastodon/actions/bookmark_categories.js deleted file mode 100644 index 7d458b85ec..0000000000 --- a/app/javascript/mastodon/actions/bookmark_categories.js +++ /dev/null @@ -1,394 +0,0 @@ -import { bookmarkCategoryNeeded } from 'mastodon/initial_state'; -import { makeGetStatus } from 'mastodon/selectors'; - -import api, { getLinks } from '../api'; - -import { importFetchedStatuses } from './importer'; -import { unbookmark } from './interactions'; - -export const BOOKMARK_CATEGORY_FETCH_REQUEST = 'BOOKMARK_CATEGORY_FETCH_REQUEST'; -export const BOOKMARK_CATEGORY_FETCH_SUCCESS = 'BOOKMARK_CATEGORY_FETCH_SUCCESS'; -export const BOOKMARK_CATEGORY_FETCH_FAIL = 'BOOKMARK_CATEGORY_FETCH_FAIL'; - -export const BOOKMARK_CATEGORIES_FETCH_REQUEST = 'BOOKMARK_CATEGORIES_FETCH_REQUEST'; -export const BOOKMARK_CATEGORIES_FETCH_SUCCESS = 'BOOKMARK_CATEGORIES_FETCH_SUCCESS'; -export const BOOKMARK_CATEGORIES_FETCH_FAIL = 'BOOKMARK_CATEGORIES_FETCH_FAIL'; - -export const BOOKMARK_CATEGORY_EDITOR_TITLE_CHANGE = 'BOOKMARK_CATEGORY_EDITOR_TITLE_CHANGE'; -export const BOOKMARK_CATEGORY_EDITOR_RESET = 'BOOKMARK_CATEGORY_EDITOR_RESET'; -export const BOOKMARK_CATEGORY_EDITOR_SETUP = 'BOOKMARK_CATEGORY_EDITOR_SETUP'; - -export const BOOKMARK_CATEGORY_CREATE_REQUEST = 'BOOKMARK_CATEGORY_CREATE_REQUEST'; -export const BOOKMARK_CATEGORY_CREATE_SUCCESS = 'BOOKMARK_CATEGORY_CREATE_SUCCESS'; -export const BOOKMARK_CATEGORY_CREATE_FAIL = 'BOOKMARK_CATEGORY_CREATE_FAIL'; - -export const BOOKMARK_CATEGORY_UPDATE_REQUEST = 'BOOKMARK_CATEGORY_UPDATE_REQUEST'; -export const BOOKMARK_CATEGORY_UPDATE_SUCCESS = 'BOOKMARK_CATEGORY_UPDATE_SUCCESS'; -export const BOOKMARK_CATEGORY_UPDATE_FAIL = 'BOOKMARK_CATEGORY_UPDATE_FAIL'; - -export const BOOKMARK_CATEGORY_DELETE_REQUEST = 'BOOKMARK_CATEGORY_DELETE_REQUEST'; -export const BOOKMARK_CATEGORY_DELETE_SUCCESS = 'BOOKMARK_CATEGORY_DELETE_SUCCESS'; -export const BOOKMARK_CATEGORY_DELETE_FAIL = 'BOOKMARK_CATEGORY_DELETE_FAIL'; - -export const BOOKMARK_CATEGORY_STATUSES_FETCH_REQUEST = 'BOOKMARK_CATEGORY_STATUSES_FETCH_REQUEST'; -export const BOOKMARK_CATEGORY_STATUSES_FETCH_SUCCESS = 'BOOKMARK_CATEGORY_STATUSES_FETCH_SUCCESS'; -export const BOOKMARK_CATEGORY_STATUSES_FETCH_FAIL = 'BOOKMARK_CATEGORY_STATUSES_FETCH_FAIL'; - -export const BOOKMARK_CATEGORY_EDITOR_ADD_REQUEST = 'BOOKMARK_CATEGORY_EDITOR_ADD_REQUEST'; -export const BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS = 'BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS'; -export const BOOKMARK_CATEGORY_EDITOR_ADD_FAIL = 'BOOKMARK_CATEGORY_EDITOR_ADD_FAIL'; - -export const BOOKMARK_CATEGORY_EDITOR_REMOVE_REQUEST = 'BOOKMARK_CATEGORY_EDITOR_REMOVE_REQUEST'; -export const BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS = 'BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS'; -export const BOOKMARK_CATEGORY_EDITOR_REMOVE_FAIL = 'BOOKMARK_CATEGORY_EDITOR_REMOVE_FAIL'; - -export const BOOKMARK_CATEGORY_ADDER_RESET = 'BOOKMARK_CATEGORY_ADDER_RESET'; -export const BOOKMARK_CATEGORY_ADDER_SETUP = 'BOOKMARK_CATEGORY_ADDER_SETUP'; - -export const BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_REQUEST = 'BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_REQUEST'; -export const BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_SUCCESS = 'BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_SUCCESS'; -export const BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_FAIL = 'BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_FAIL'; - -export const BOOKMARK_CATEGORY_STATUSES_EXPAND_REQUEST = 'BOOKMARK_CATEGORY_STATUSES_EXPAND_REQUEST'; -export const BOOKMARK_CATEGORY_STATUSES_EXPAND_SUCCESS = 'BOOKMARK_CATEGORY_STATUSES_EXPAND_SUCCESS'; -export const BOOKMARK_CATEGORY_STATUSES_EXPAND_FAIL = 'BOOKMARK_CATEGORY_STATUSES_EXPAND_FAIL'; - -export const fetchBookmarkCategory = id => (dispatch, getState) => { - if (getState().getIn(['bookmark_categories', id])) { - return; - } - - dispatch(fetchBookmarkCategoryRequest(id)); - - api(getState).get(`/api/v1/bookmark_categories/${id}`) - .then(({ data }) => dispatch(fetchBookmarkCategorySuccess(data))) - .catch(err => dispatch(fetchBookmarkCategoryFail(id, err))); -}; - -export const fetchBookmarkCategoryRequest = id => ({ - type: BOOKMARK_CATEGORY_FETCH_REQUEST, - id, -}); - -export const fetchBookmarkCategorySuccess = bookmarkCategory => ({ - type: BOOKMARK_CATEGORY_FETCH_SUCCESS, - bookmarkCategory, -}); - -export const fetchBookmarkCategoryFail = (id, error) => ({ - type: BOOKMARK_CATEGORY_FETCH_FAIL, - id, - error, -}); - -export const fetchBookmarkCategories = () => (dispatch, getState) => { - dispatch(fetchBookmarkCategoriesRequest()); - - api(getState).get('/api/v1/bookmark_categories') - .then(({ data }) => dispatch(fetchBookmarkCategoriesSuccess(data))) - .catch(err => dispatch(fetchBookmarkCategoriesFail(err))); -}; - -export const fetchBookmarkCategoriesRequest = () => ({ - type: BOOKMARK_CATEGORIES_FETCH_REQUEST, -}); - -export const fetchBookmarkCategoriesSuccess = bookmarkCategories => ({ - type: BOOKMARK_CATEGORIES_FETCH_SUCCESS, - bookmarkCategories, -}); - -export const fetchBookmarkCategoriesFail = error => ({ - type: BOOKMARK_CATEGORIES_FETCH_FAIL, - error, -}); - -export const submitBookmarkCategoryEditor = shouldReset => (dispatch, getState) => { - const bookmarkCategoryId = getState().getIn(['bookmarkCategoryEditor', 'bookmarkCategoryId']); - const title = getState().getIn(['bookmarkCategoryEditor', 'title']); - - if (bookmarkCategoryId === null) { - dispatch(createBookmarkCategory(title, shouldReset)); - } else { - dispatch(updateBookmarkCategory(bookmarkCategoryId, title, shouldReset)); - } -}; - -export const setupBookmarkCategoryEditor = bookmarkCategoryId => (dispatch, getState) => { - dispatch({ - type: BOOKMARK_CATEGORY_EDITOR_SETUP, - bookmarkCategory: getState().getIn(['bookmark_categories', bookmarkCategoryId]), - }); - - dispatch(fetchBookmarkCategoryStatuses(bookmarkCategoryId)); -}; - -export const changeBookmarkCategoryEditorTitle = value => ({ - type: BOOKMARK_CATEGORY_EDITOR_TITLE_CHANGE, - value, -}); - -export const createBookmarkCategory = (title, shouldReset) => (dispatch, getState) => { - dispatch(createBookmarkCategoryRequest()); - - api(getState).post('/api/v1/bookmark_categories', { title }).then(({ data }) => { - dispatch(createBookmarkCategorySuccess(data)); - - if (shouldReset) { - dispatch(resetBookmarkCategoryEditor()); - } - }).catch(err => dispatch(createBookmarkCategoryFail(err))); -}; - -export const createBookmarkCategoryRequest = () => ({ - type: BOOKMARK_CATEGORY_CREATE_REQUEST, -}); - -export const createBookmarkCategorySuccess = bookmarkCategory => ({ - type: BOOKMARK_CATEGORY_CREATE_SUCCESS, - bookmarkCategory, -}); - -export const createBookmarkCategoryFail = error => ({ - type: BOOKMARK_CATEGORY_CREATE_FAIL, - error, -}); - -export const updateBookmarkCategory = (id, title, shouldReset) => (dispatch, getState) => { - dispatch(updateBookmarkCategoryRequest(id)); - - api(getState).put(`/api/v1/bookmark_categories/${id}`, { title }).then(({ data }) => { - dispatch(updateBookmarkCategorySuccess(data)); - - if (shouldReset) { - dispatch(resetBookmarkCategoryEditor()); - } - }).catch(err => dispatch(updateBookmarkCategoryFail(id, err))); -}; - -export const updateBookmarkCategoryRequest = id => ({ - type: BOOKMARK_CATEGORY_UPDATE_REQUEST, - id, -}); - -export const updateBookmarkCategorySuccess = bookmarkCategory => ({ - type: BOOKMARK_CATEGORY_UPDATE_SUCCESS, - bookmarkCategory, -}); - -export const updateBookmarkCategoryFail = (id, error) => ({ - type: BOOKMARK_CATEGORY_UPDATE_FAIL, - id, - error, -}); - -export const resetBookmarkCategoryEditor = () => ({ - type: BOOKMARK_CATEGORY_EDITOR_RESET, -}); - -export const deleteBookmarkCategory = id => (dispatch, getState) => { - dispatch(deleteBookmarkCategoryRequest(id)); - - api(getState).delete(`/api/v1/bookmark_categories/${id}`) - .then(() => dispatch(deleteBookmarkCategorySuccess(id))) - .catch(err => dispatch(deleteBookmarkCategoryFail(id, err))); -}; - -export const deleteBookmarkCategoryRequest = id => ({ - type: BOOKMARK_CATEGORY_DELETE_REQUEST, - id, -}); - -export const deleteBookmarkCategorySuccess = id => ({ - type: BOOKMARK_CATEGORY_DELETE_SUCCESS, - id, -}); - -export const deleteBookmarkCategoryFail = (id, error) => ({ - type: BOOKMARK_CATEGORY_DELETE_FAIL, - id, - error, -}); - -export const fetchBookmarkCategoryStatuses = bookmarkCategoryId => (dispatch, getState) => { - dispatch(fetchBookmarkCategoryStatusesRequest(bookmarkCategoryId)); - - api(getState).get(`/api/v1/bookmark_categories/${bookmarkCategoryId}/statuses`).then((response) => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); - dispatch(fetchBookmarkCategoryStatusesSuccess(bookmarkCategoryId, response.data, next ? next.uri : null)); - }).catch(err => dispatch(fetchBookmarkCategoryStatusesFail(bookmarkCategoryId, err))); -}; - -export const fetchBookmarkCategoryStatusesRequest = id => ({ - type: BOOKMARK_CATEGORY_STATUSES_FETCH_REQUEST, - id, -}); - -export const fetchBookmarkCategoryStatusesSuccess = (id, statuses, next) => ({ - type: BOOKMARK_CATEGORY_STATUSES_FETCH_SUCCESS, - id, - statuses, - next, -}); - -export const fetchBookmarkCategoryStatusesFail = (id, error) => ({ - type: BOOKMARK_CATEGORY_STATUSES_FETCH_FAIL, - id, - error, -}); - -export const addToBookmarkCategory = (bookmarkCategoryId, statusId) => (dispatch, getState) => { - dispatch(addToBookmarkCategoryRequest(bookmarkCategoryId, statusId)); - - api(getState).post(`/api/v1/bookmark_categories/${bookmarkCategoryId}/statuses`, { status_ids: [statusId] }) - .then(() => dispatch(addToBookmarkCategorySuccess(bookmarkCategoryId, statusId))) - .catch(err => dispatch(addToBookmarkCategoryFail(bookmarkCategoryId, statusId, err))); -}; - -export const addToBookmarkCategoryRequest = (bookmarkCategoryId, statusId) => ({ - type: BOOKMARK_CATEGORY_EDITOR_ADD_REQUEST, - bookmarkCategoryId, - statusId, -}); - -export const addToBookmarkCategorySuccess = (bookmarkCategoryId, statusId) => ({ - type: BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS, - bookmarkCategoryId, - statusId, -}); - -export const addToBookmarkCategoryFail = (bookmarkCategoryId, statusId, error) => ({ - type: BOOKMARK_CATEGORY_EDITOR_ADD_FAIL, - bookmarkCategoryId, - statusId, - error, -}); - -export const removeFromBookmarkCategory = (bookmarkCategoryId, statusId) => (dispatch, getState) => { - dispatch(removeFromBookmarkCategoryRequest(bookmarkCategoryId, statusId)); - - api(getState).delete(`/api/v1/bookmark_categories/${bookmarkCategoryId}/statuses`, { params: { status_ids: [statusId] } }) - .then(() => dispatch(removeFromBookmarkCategorySuccess(bookmarkCategoryId, statusId))) - .catch(err => dispatch(removeFromBookmarkCategoryFail(bookmarkCategoryId, statusId, err))); -}; - -export const removeFromBookmarkCategoryRequest = (bookmarkCategoryId, statusId) => ({ - type: BOOKMARK_CATEGORY_EDITOR_REMOVE_REQUEST, - bookmarkCategoryId, - statusId, -}); - -export const removeFromBookmarkCategorySuccess = (bookmarkCategoryId, statusId) => ({ - type: BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS, - bookmarkCategoryId, - statusId, -}); - -export const removeFromBookmarkCategoryFail = (bookmarkCategoryId, statusId, error) => ({ - type: BOOKMARK_CATEGORY_EDITOR_REMOVE_FAIL, - bookmarkCategoryId, - statusId, - error, -}); - -export const resetBookmarkCategoryAdder = () => ({ - type: BOOKMARK_CATEGORY_ADDER_RESET, -}); - -export const setupBookmarkCategoryAdder = statusId => (dispatch, getState) => { - dispatch({ - type: BOOKMARK_CATEGORY_ADDER_SETUP, - status: getState().getIn(['statuses', statusId]), - }); - dispatch(fetchBookmarkCategories()); - dispatch(fetchStatusBookmarkCategories(statusId)); -}; - -export const fetchStatusBookmarkCategories = statusId => (dispatch, getState) => { - dispatch(fetchStatusBookmarkCategoriesRequest(statusId)); - - api(getState).get(`/api/v1/statuses/${statusId}/bookmark_categories`) - .then(({ data }) => dispatch(fetchStatusBookmarkCategoriesSuccess(statusId, data))) - .catch(err => dispatch(fetchStatusBookmarkCategoriesFail(statusId, err))); -}; - -export const fetchStatusBookmarkCategoriesRequest = id => ({ - type:BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_REQUEST, - id, -}); - -export const fetchStatusBookmarkCategoriesSuccess = (id, bookmarkCategories) => ({ - type: BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_SUCCESS, - id, - bookmarkCategories, -}); - -export const fetchStatusBookmarkCategoriesFail = (id, err) => ({ - type: BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_FAIL, - id, - err, -}); - -export const addToBookmarkCategoryAdder = bookmarkCategoryId => (dispatch, getState) => { - dispatch(addToBookmarkCategory(bookmarkCategoryId, getState().getIn(['bookmarkCategoryAdder', 'statusId']))); -}; - -export const removeFromBookmarkCategoryAdder = bookmarkCategoryId => (dispatch, getState) => { - if (bookmarkCategoryNeeded) { - const categories = getState().getIn(['bookmarkCategoryAdder', 'bookmarkCategories', 'items']); - if (categories && categories.count() <= 1) { - const status = makeGetStatus()(getState(), { id: getState().getIn(['bookmarkCategoryAdder', 'statusId']) }); - dispatch(unbookmark(status)); - } else { - dispatch(removeFromBookmarkCategory(bookmarkCategoryId, getState().getIn(['bookmarkCategoryAdder', 'statusId']))); - } - } else { - dispatch(removeFromBookmarkCategory(bookmarkCategoryId, getState().getIn(['bookmarkCategoryAdder', 'statusId']))); - } -}; - -export function expandBookmarkCategoryStatuses(bookmarkCategoryId) { - return (dispatch, getState) => { - const url = getState().getIn(['bookmark_categories', bookmarkCategoryId, 'next'], null); - - if (url === null || getState().getIn(['bookmark_categories', bookmarkCategoryId, 'isLoading'])) { - return; - } - - dispatch(expandBookmarkCategoryStatusesRequest(bookmarkCategoryId)); - - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); - dispatch(expandBookmarkCategoryStatusesSuccess(bookmarkCategoryId, response.data, next ? next.uri : null)); - }).catch(error => { - dispatch(expandBookmarkCategoryStatusesFail(bookmarkCategoryId, error)); - }); - }; -} - -export function expandBookmarkCategoryStatusesRequest(id) { - return { - type: BOOKMARK_CATEGORY_STATUSES_EXPAND_REQUEST, - id, - }; -} - -export function expandBookmarkCategoryStatusesSuccess(id, statuses, next) { - return { - type: BOOKMARK_CATEGORY_STATUSES_EXPAND_SUCCESS, - id, - statuses, - next, - }; -} - -export function expandBookmarkCategoryStatusesFail(id, error) { - return { - type: BOOKMARK_CATEGORY_STATUSES_EXPAND_FAIL, - id, - error, - }; -} - diff --git a/app/javascript/mastodon/actions/bookmarks.js b/app/javascript/mastodon/actions/bookmarks.js index 91c1d61e10..0b16f61e63 100644 --- a/app/javascript/mastodon/actions/bookmarks.js +++ b/app/javascript/mastodon/actions/bookmarks.js @@ -1,5 +1,3 @@ -// Kmyblue tracking marker: copied bookmark_categories.js - import api, { getLinks } from '../api'; import { importFetchedStatuses } from './importer'; diff --git a/app/javascript/mastodon/actions/boosts.js b/app/javascript/mastodon/actions/boosts.js new file mode 100644 index 0000000000..1fc2e391e2 --- /dev/null +++ b/app/javascript/mastodon/actions/boosts.js @@ -0,0 +1,32 @@ +import { openModal } from './modal'; + +export const BOOSTS_INIT_MODAL = 'BOOSTS_INIT_MODAL'; +export const BOOSTS_CHANGE_PRIVACY = 'BOOSTS_CHANGE_PRIVACY'; + +export function initBoostModal(props) { + return (dispatch, getState) => { + const default_privacy = getState().getIn(['compose', 'default_privacy']); + + const privacy = props.status.get('visibility') === 'private' ? 'private' : default_privacy; + + dispatch({ + type: BOOSTS_INIT_MODAL, + privacy, + }); + + dispatch(openModal({ + modalType: 'BOOST', + modalProps: props, + })); + }; +} + + +export function changeBoostPrivacy(privacy) { + return dispatch => { + dispatch({ + type: BOOSTS_CHANGE_PRIVACY, + privacy, + }); + }; +} diff --git a/app/javascript/mastodon/actions/circles.js b/app/javascript/mastodon/actions/circles.js deleted file mode 100644 index 7ed41b4045..0000000000 --- a/app/javascript/mastodon/actions/circles.js +++ /dev/null @@ -1,470 +0,0 @@ -import api, { getLinks } from '../api'; - -import { showAlertForError } from './alerts'; -import { importFetchedAccounts, importFetchedStatuses } from './importer'; - -export const CIRCLE_FETCH_REQUEST = 'CIRCLE_FETCH_REQUEST'; -export const CIRCLE_FETCH_SUCCESS = 'CIRCLE_FETCH_SUCCESS'; -export const CIRCLE_FETCH_FAIL = 'CIRCLE_FETCH_FAIL'; - -export const CIRCLES_FETCH_REQUEST = 'CIRCLES_FETCH_REQUEST'; -export const CIRCLES_FETCH_SUCCESS = 'CIRCLES_FETCH_SUCCESS'; -export const CIRCLES_FETCH_FAIL = 'CIRCLES_FETCH_FAIL'; - -export const CIRCLE_EDITOR_TITLE_CHANGE = 'CIRCLE_EDITOR_TITLE_CHANGE'; -export const CIRCLE_EDITOR_RESET = 'CIRCLE_EDITOR_RESET'; -export const CIRCLE_EDITOR_SETUP = 'CIRCLE_EDITOR_SETUP'; - -export const CIRCLE_CREATE_REQUEST = 'CIRCLE_CREATE_REQUEST'; -export const CIRCLE_CREATE_SUCCESS = 'CIRCLE_CREATE_SUCCESS'; -export const CIRCLE_CREATE_FAIL = 'CIRCLE_CREATE_FAIL'; - -export const CIRCLE_UPDATE_REQUEST = 'CIRCLE_UPDATE_REQUEST'; -export const CIRCLE_UPDATE_SUCCESS = 'CIRCLE_UPDATE_SUCCESS'; -export const CIRCLE_UPDATE_FAIL = 'CIRCLE_UPDATE_FAIL'; - -export const CIRCLE_DELETE_REQUEST = 'CIRCLE_DELETE_REQUEST'; -export const CIRCLE_DELETE_SUCCESS = 'CIRCLE_DELETE_SUCCESS'; -export const CIRCLE_DELETE_FAIL = 'CIRCLE_DELETE_FAIL'; - -export const CIRCLE_ACCOUNTS_FETCH_REQUEST = 'CIRCLE_ACCOUNTS_FETCH_REQUEST'; -export const CIRCLE_ACCOUNTS_FETCH_SUCCESS = 'CIRCLE_ACCOUNTS_FETCH_SUCCESS'; -export const CIRCLE_ACCOUNTS_FETCH_FAIL = 'CIRCLE_ACCOUNTS_FETCH_FAIL'; - -export const CIRCLE_EDITOR_SUGGESTIONS_CHANGE = 'CIRCLE_EDITOR_SUGGESTIONS_CHANGE'; -export const CIRCLE_EDITOR_SUGGESTIONS_READY = 'CIRCLE_EDITOR_SUGGESTIONS_READY'; -export const CIRCLE_EDITOR_SUGGESTIONS_CLEAR = 'CIRCLE_EDITOR_SUGGESTIONS_CLEAR'; - -export const CIRCLE_EDITOR_ADD_REQUEST = 'CIRCLE_EDITOR_ADD_REQUEST'; -export const CIRCLE_EDITOR_ADD_SUCCESS = 'CIRCLE_EDITOR_ADD_SUCCESS'; -export const CIRCLE_EDITOR_ADD_FAIL = 'CIRCLE_EDITOR_ADD_FAIL'; - -export const CIRCLE_EDITOR_REMOVE_REQUEST = 'CIRCLE_EDITOR_REMOVE_REQUEST'; -export const CIRCLE_EDITOR_REMOVE_SUCCESS = 'CIRCLE_EDITOR_REMOVE_SUCCESS'; -export const CIRCLE_EDITOR_REMOVE_FAIL = 'CIRCLE_EDITOR_REMOVE_FAIL'; - -export const CIRCLE_ADDER_RESET = 'CIRCLE_ADDER_RESET'; -export const CIRCLE_ADDER_SETUP = 'CIRCLE_ADDER_SETUP'; - -export const CIRCLE_ADDER_CIRCLES_FETCH_REQUEST = 'CIRCLE_ADDER_CIRCLES_FETCH_REQUEST'; -export const CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS = 'CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS'; -export const CIRCLE_ADDER_CIRCLES_FETCH_FAIL = 'CIRCLE_ADDER_CIRCLES_FETCH_FAIL'; - -export const CIRCLE_STATUSES_FETCH_REQUEST = 'CIRCLE_STATUSES_FETCH_REQUEST'; -export const CIRCLE_STATUSES_FETCH_SUCCESS = 'CIRCLE_STATUSES_FETCH_SUCCESS'; -export const CIRCLE_STATUSES_FETCH_FAIL = 'CIRCLE_STATUSES_FETCH_FAIL'; - -export const CIRCLE_STATUSES_EXPAND_REQUEST = 'CIRCLE_STATUSES_EXPAND_REQUEST'; -export const CIRCLE_STATUSES_EXPAND_SUCCESS = 'CIRCLE_STATUSES_EXPAND_SUCCESS'; -export const CIRCLE_STATUSES_EXPAND_FAIL = 'CIRCLE_STATUSES_EXPAND_FAIL'; - -export const fetchCircle = id => (dispatch, getState) => { - if (getState().getIn(['circles', id])) { - return; - } - - dispatch(fetchCircleRequest(id)); - - api(getState).get(`/api/v1/circles/${id}`) - .then(({ data }) => dispatch(fetchCircleSuccess(data))) - .catch(err => dispatch(fetchCircleFail(id, err))); -}; - -export const fetchCircleRequest = id => ({ - type: CIRCLE_FETCH_REQUEST, - id, -}); - -export const fetchCircleSuccess = circle => ({ - type: CIRCLE_FETCH_SUCCESS, - circle, -}); - -export const fetchCircleFail = (id, error) => ({ - type: CIRCLE_FETCH_FAIL, - id, - error, -}); - -export const fetchCircles = () => (dispatch, getState) => { - dispatch(fetchCirclesRequest()); - - api(getState).get('/api/v1/circles') - .then(({ data }) => dispatch(fetchCirclesSuccess(data))) - .catch(err => dispatch(fetchCirclesFail(err))); -}; - -export const fetchCirclesRequest = () => ({ - type: CIRCLES_FETCH_REQUEST, -}); - -export const fetchCirclesSuccess = circles => ({ - type: CIRCLES_FETCH_SUCCESS, - circles, -}); - -export const fetchCirclesFail = error => ({ - type: CIRCLES_FETCH_FAIL, - error, -}); - -export const submitCircleEditor = shouldReset => (dispatch, getState) => { - const circleId = getState().getIn(['circleEditor', 'circleId']); - const title = getState().getIn(['circleEditor', 'title']); - - if (circleId === null) { - dispatch(createCircle(title, shouldReset)); - } else { - dispatch(updateCircle(circleId, title, shouldReset)); - } -}; - -export const setupCircleEditor = circleId => (dispatch, getState) => { - dispatch({ - type: CIRCLE_EDITOR_SETUP, - circle: getState().getIn(['circles', circleId]), - }); - - dispatch(fetchCircleAccounts(circleId)); -}; - -export const changeCircleEditorTitle = value => ({ - type: CIRCLE_EDITOR_TITLE_CHANGE, - value, -}); - -export const createCircle = (title, shouldReset) => (dispatch, getState) => { - dispatch(createCircleRequest()); - - api(getState).post('/api/v1/circles', { title }).then(({ data }) => { - dispatch(createCircleSuccess(data)); - - if (shouldReset) { - dispatch(resetCircleEditor()); - } - }).catch(err => dispatch(createCircleFail(err))); -}; - -export const createCircleRequest = () => ({ - type: CIRCLE_CREATE_REQUEST, -}); - -export const createCircleSuccess = circle => ({ - type: CIRCLE_CREATE_SUCCESS, - circle, -}); - -export const createCircleFail = error => ({ - type: CIRCLE_CREATE_FAIL, - error, -}); - -export const updateCircle = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => { - dispatch(updateCircleRequest(id)); - - api(getState).put(`/api/v1/circles/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => { - dispatch(updateCircleSuccess(data)); - - if (shouldReset) { - dispatch(resetCircleEditor()); - } - }).catch(err => dispatch(updateCircleFail(id, err))); -}; - -export const updateCircleRequest = id => ({ - type: CIRCLE_UPDATE_REQUEST, - id, -}); - -export const updateCircleSuccess = circle => ({ - type: CIRCLE_UPDATE_SUCCESS, - circle, -}); - -export const updateCircleFail = (id, error) => ({ - type: CIRCLE_UPDATE_FAIL, - id, - error, -}); - -export const resetCircleEditor = () => ({ - type: CIRCLE_EDITOR_RESET, -}); - -export const deleteCircle = id => (dispatch, getState) => { - dispatch(deleteCircleRequest(id)); - - api(getState).delete(`/api/v1/circles/${id}`) - .then(() => dispatch(deleteCircleSuccess(id))) - .catch(err => dispatch(deleteCircleFail(id, err))); -}; - -export const deleteCircleRequest = id => ({ - type: CIRCLE_DELETE_REQUEST, - id, -}); - -export const deleteCircleSuccess = id => ({ - type: CIRCLE_DELETE_SUCCESS, - id, -}); - -export const deleteCircleFail = (id, error) => ({ - type: CIRCLE_DELETE_FAIL, - id, - error, -}); - -export const fetchCircleAccounts = circleId => (dispatch, getState) => { - dispatch(fetchCircleAccountsRequest(circleId)); - - api(getState).get(`/api/v1/circles/${circleId}/accounts`, { params: { limit: 0 } }).then(({ data }) => { - dispatch(importFetchedAccounts(data)); - dispatch(fetchCircleAccountsSuccess(circleId, data)); - }).catch(err => dispatch(fetchCircleAccountsFail(circleId, err))); -}; - -export const fetchCircleAccountsRequest = id => ({ - type: CIRCLE_ACCOUNTS_FETCH_REQUEST, - id, -}); - -export const fetchCircleAccountsSuccess = (id, accounts, next) => ({ - type: CIRCLE_ACCOUNTS_FETCH_SUCCESS, - id, - accounts, - next, -}); - -export const fetchCircleAccountsFail = (id, error) => ({ - type: CIRCLE_ACCOUNTS_FETCH_FAIL, - id, - error, -}); - -export const fetchCircleSuggestions = q => (dispatch, getState) => { - const params = { - q, - resolve: false, - follower: true, - }; - - api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => { - dispatch(importFetchedAccounts(data)); - dispatch(fetchCircleSuggestionsReady(q, data)); - }).catch(error => dispatch(showAlertForError(error))); -}; - -export const fetchCircleSuggestionsReady = (query, accounts) => ({ - type: CIRCLE_EDITOR_SUGGESTIONS_READY, - query, - accounts, -}); - -export const clearCircleSuggestions = () => ({ - type: CIRCLE_EDITOR_SUGGESTIONS_CLEAR, -}); - -export const changeCircleSuggestions = value => ({ - type: CIRCLE_EDITOR_SUGGESTIONS_CHANGE, - value, -}); - -export const addToCircleEditor = accountId => (dispatch, getState) => { - dispatch(addToCircle(getState().getIn(['circleEditor', 'circleId']), accountId)); -}; - -export const addToCircle = (circleId, accountId) => (dispatch, getState) => { - dispatch(addToCircleRequest(circleId, accountId)); - - api(getState).post(`/api/v1/circles/${circleId}/accounts`, { account_ids: [accountId] }) - .then(() => dispatch(addToCircleSuccess(circleId, accountId))) - .catch(err => dispatch(addToCircleFail(circleId, accountId, err))); -}; - -export const addToCircleRequest = (circleId, accountId) => ({ - type: CIRCLE_EDITOR_ADD_REQUEST, - circleId, - accountId, -}); - -export const addToCircleSuccess = (circleId, accountId) => ({ - type: CIRCLE_EDITOR_ADD_SUCCESS, - circleId, - accountId, -}); - -export const addToCircleFail = (circleId, accountId, error) => ({ - type: CIRCLE_EDITOR_ADD_FAIL, - circleId, - accountId, - error, -}); - -export const removeFromCircleEditor = accountId => (dispatch, getState) => { - dispatch(removeFromCircle(getState().getIn(['circleEditor', 'circleId']), accountId)); -}; - -export const removeFromCircle = (circleId, accountId) => (dispatch, getState) => { - dispatch(removeFromCircleRequest(circleId, accountId)); - - api(getState).delete(`/api/v1/circles/${circleId}/accounts`, { params: { account_ids: [accountId] } }) - .then(() => dispatch(removeFromCircleSuccess(circleId, accountId))) - .catch(err => dispatch(removeFromCircleFail(circleId, accountId, err))); -}; - -export const removeFromCircleRequest = (circleId, accountId) => ({ - type: CIRCLE_EDITOR_REMOVE_REQUEST, - circleId, - accountId, -}); - -export const removeFromCircleSuccess = (circleId, accountId) => ({ - type: CIRCLE_EDITOR_REMOVE_SUCCESS, - circleId, - accountId, -}); - -export const removeFromCircleFail = (circleId, accountId, error) => ({ - type: CIRCLE_EDITOR_REMOVE_FAIL, - circleId, - accountId, - error, -}); - -export const resetCircleAdder = () => ({ - type: CIRCLE_ADDER_RESET, -}); - -export const setupCircleAdder = accountId => (dispatch, getState) => { - dispatch({ - type: CIRCLE_ADDER_SETUP, - account: getState().getIn(['accounts', accountId]), - }); - dispatch(fetchCircles()); - dispatch(fetchAccountCircles(accountId)); -}; - -export const fetchAccountCircles = accountId => (dispatch, getState) => { - dispatch(fetchAccountCirclesRequest(accountId)); - - api(getState).get(`/api/v1/accounts/${accountId}/circles`) - .then(({ data }) => dispatch(fetchAccountCirclesSuccess(accountId, data))) - .catch(err => dispatch(fetchAccountCirclesFail(accountId, err))); -}; - -export const fetchAccountCirclesRequest = id => ({ - type:CIRCLE_ADDER_CIRCLES_FETCH_REQUEST, - id, -}); - -export const fetchAccountCirclesSuccess = (id, circles) => ({ - type: CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS, - id, - circles, -}); - -export const fetchAccountCirclesFail = (id, err) => ({ - type: CIRCLE_ADDER_CIRCLES_FETCH_FAIL, - id, - err, -}); - -export const addToCircleAdder = circleId => (dispatch, getState) => { - dispatch(addToCircle(circleId, getState().getIn(['circleAdder', 'accountId']))); -}; - -export const removeFromCircleAdder = circleId => (dispatch, getState) => { - dispatch(removeFromCircle(circleId, getState().getIn(['circleAdder', 'accountId']))); -}; - -export function fetchCircleStatuses(circleId) { - return (dispatch, getState) => { - if (getState().getIn(['circles', circleId, 'isLoading'])) { - return; - } - const items = getState().getIn(['circles', circleId, 'items']); - if (items && items.size > 0) { - return; - } - - dispatch(fetchCircleStatusesRequest(circleId)); - - api(getState).get(`/api/v1/circles/${circleId}/statuses`).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); - dispatch(fetchCircleStatusesSuccess(circleId, response.data, next ? next.uri : null)); - }).catch(error => { - dispatch(fetchCircleStatusesFail(circleId, error)); - }); - }; -} - -export function fetchCircleStatusesRequest(id) { - return { - type: CIRCLE_STATUSES_FETCH_REQUEST, - id, - }; -} - -export function fetchCircleStatusesSuccess(id, statuses, next) { - return { - type: CIRCLE_STATUSES_FETCH_SUCCESS, - id, - statuses, - next, - }; -} - -export function fetchCircleStatusesFail(id, error) { - return { - type: CIRCLE_STATUSES_FETCH_FAIL, - id, - error, - }; -} - -export function expandCircleStatuses(circleId) { - return (dispatch, getState) => { - const url = getState().getIn(['circles', circleId, 'next'], null); - - if (url === null || getState().getIn(['circles', circleId, 'isLoading'])) { - return; - } - - dispatch(expandCircleStatusesRequest(circleId)); - - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); - dispatch(expandCircleStatusesSuccess(circleId, response.data, next ? next.uri : null)); - }).catch(error => { - dispatch(expandCircleStatusesFail(circleId, error)); - }); - }; -} - -export function expandCircleStatusesRequest(id) { - return { - type: CIRCLE_STATUSES_EXPAND_REQUEST, - id, - }; -} - -export function expandCircleStatusesSuccess(id, statuses, next) { - return { - type: CIRCLE_STATUSES_EXPAND_SUCCESS, - id, - statuses, - next, - }; -} - -export function expandCircleStatusesFail(id, error) { - return { - type: CIRCLE_STATUSES_EXPAND_FAIL, - id, - error, - }; -} - diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index b013e134fe..6abfd6157e 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -28,8 +28,6 @@ export const COMPOSE_DIRECT = 'COMPOSE_DIRECT'; export const COMPOSE_MENTION = 'COMPOSE_MENTION'; export const COMPOSE_RESET = 'COMPOSE_RESET'; -export const COMPOSE_WITH_CIRCLE_SUCCESS = 'COMPOSE_WITH_CIRCLE_SUCCESS'; - export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST'; export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS'; export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL'; @@ -56,16 +54,11 @@ export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT'; export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE'; export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE'; export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE'; -export const COMPOSE_MARKDOWN_CHANGE = 'COMPOSE_MARKDOWN_CHANGE'; export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE'; -export const COMPOSE_SEARCHABILITY_CHANGE= 'COMPOSE_SEARCHABILITY_CHANGE'; export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE'; export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE'; export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT'; -export const COMPOSE_EXPIRATION_INSERT = 'COMPOSE_EXPIRATION_INSERT'; -export const COMPOSE_FEATURED_TAG_INSERT = 'COMPOSE_FEATURED_TAG_INSERT'; -export const COMPOSE_REFERENCE_INSERT = 'COMPOSE_REFERENCE_INSERT'; export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST'; export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS'; @@ -78,13 +71,10 @@ export const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE'; export const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE'; export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE'; -export const COMPOSE_CIRCLE_CHANGE = 'COMPOSE_CIRCLE_CHANGE'; - export const INIT_MEDIA_EDIT_MODAL = 'INIT_MEDIA_EDIT_MODAL'; export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION'; export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS'; -export const COMPOSE_CHANGE_MEDIA_ORDER = 'COMPOSE_CHANGE_MEDIA_ORDER'; export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS'; export const COMPOSE_FOCUS = 'COMPOSE_FOCUS'; @@ -178,8 +168,6 @@ export function submitCompose(routerHistory) { const status = getState().getIn(['compose', 'text'], ''); const media = getState().getIn(['compose', 'media_attachments']); const statusId = getState().getIn(['compose', 'id'], null); - const circleId = getState().getIn(['compose', 'circle_id'], null); - const privacy = getState().getIn(['compose', 'privacy']); if ((!status || !status.length) && media.size === 0) { return; @@ -215,12 +203,9 @@ export function submitCompose(routerHistory) { in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), media_ids: media.map(item => item.get('id')), media_attributes, - sensitive: media.size > 0 ? getState().getIn(['compose', 'spoiler']) : false, + sensitive: getState().getIn(['compose', 'sensitive']), spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '', - markdown: getState().getIn(['compose', 'markdown']), visibility: getState().getIn(['compose', 'privacy']), - searchability: getState().getIn(['compose', 'searchability']), - circle_id: getState().getIn(['compose', 'circle_id']), poll: getState().getIn(['compose', 'poll'], null), language: getState().getIn(['compose', 'language']), }, @@ -249,20 +234,16 @@ export function submitCompose(routerHistory) { dispatch(importFetchedStatus({ ...response.data })); } - if (statusId === null && response.data.visibility_ex !== 'direct') { + if (statusId === null && response.data.visibility !== 'direct') { insertIfOnline('home'); } - if (statusId === null && response.data.in_reply_to_id === null && response.data.visibility_ex === 'public') { + if (statusId === null && response.data.in_reply_to_id === null && response.data.visibility === 'public') { insertIfOnline('community'); insertIfOnline('public'); insertIfOnline(`account:${response.data.account.id}`); } - if (statusId === null && privacy === 'circle' && circleId !== null && circleId !== 0) { - dispatch(submitComposeWithCircleSuccess({ ...response.data }, circleId)); - } - dispatch(showAlert({ message: statusId === null ? messages.published : messages.saved, action: messages.open, @@ -288,14 +269,6 @@ export function submitComposeSuccess(status) { }; } -export function submitComposeWithCircleSuccess(status, circleId) { - return { - type: COMPOSE_WITH_CIRCLE_SUCCESS, - status, - circleId, - }; -} - export function submitComposeFail(error) { return { type: COMPOSE_SUBMIT_FAIL, @@ -308,8 +281,6 @@ export function uploadCompose(files) { const uploadLimit = 4; const media = getState().getIn(['compose', 'media_attachments']); const pending = getState().getIn(['compose', 'pending_media_attachments']); - const defaultSensitive = getState().getIn(['compose', 'default_sensitive']); - const spoiler = getState().getIn(['compose', 'spoiler']); const progress = new Array(files.length).fill(0); let total = Array.from(files).reduce((a, v) => a + v.size, 0); @@ -319,10 +290,15 @@ export function uploadCompose(files) { return; } + if (getState().getIn(['compose', 'poll'])) { + dispatch(showAlert({ message: messages.uploadErrorPoll })); + return; + } + dispatch(uploadComposeRequest()); for (const [i, file] of Array.from(files).entries()) { - if (media.size + i >= 4) break; + if (media.size + i > 3) break; const data = new FormData(); data.append('file', file); @@ -338,10 +314,6 @@ export function uploadCompose(files) { if (status === 200) { dispatch(uploadComposeSuccess(data, file)); - - if (defaultSensitive && !spoiler && (media.size + i) === 0) { - dispatch(changeComposeSpoilerness()); - } } else if (status === 202) { dispatch(uploadComposeProcessing()); @@ -772,12 +744,6 @@ export function changeComposeSpoilerText(text) { }; } -export function changeComposeMarkdown() { - return { - type: COMPOSE_MARKDOWN_CHANGE, - }; -} - export function changeComposeVisibility(value) { return { type: COMPOSE_VISIBILITY_CHANGE, @@ -785,13 +751,6 @@ export function changeComposeVisibility(value) { }; } -export function changeComposeSearchability(value) { - return { - type: COMPOSE_SEARCHABILITY_CHANGE, - value, - }; -} - export function insertEmojiCompose(position, emoji, needsSpace) { return { type: COMPOSE_EMOJI_INSERT, @@ -801,31 +760,6 @@ export function insertEmojiCompose(position, emoji, needsSpace) { }; } -export function insertExpirationCompose(position, data) { - return { - type: COMPOSE_EXPIRATION_INSERT, - position, - data, - }; -} - -export function insertFeaturedTagCompose(position, data) { - return { - type: COMPOSE_FEATURED_TAG_INSERT, - position, - data, - }; -} - -export function insertReferenceCompose(position, url, attributeType) { - return { - type: COMPOSE_REFERENCE_INSERT, - position, - url, - attributeType, - }; -} - export function changeComposing(value) { return { type: COMPOSE_COMPOSING_CHANGE, @@ -852,12 +786,11 @@ export function addPollOption(title) { }; } -export function changePollOption(index, title, maxOptions) { +export function changePollOption(index, title) { return { type: COMPOSE_POLL_OPTION_CHANGE, index, title, - maxOptions, }; } @@ -875,16 +808,3 @@ export function changePollSettings(expiresIn, isMultiple) { isMultiple, }; } - -export const changeMediaOrder = (a, b) => ({ - type: COMPOSE_CHANGE_MEDIA_ORDER, - a, - b, -}); - -export function changeCircle(circleId) { - return { - type: COMPOSE_CIRCLE_CHANGE, - circleId, - }; -} diff --git a/app/javascript/mastodon/actions/domain_blocks.js b/app/javascript/mastodon/actions/domain_blocks.js index 55c0a6ce9d..718002613f 100644 --- a/app/javascript/mastodon/actions/domain_blocks.js +++ b/app/javascript/mastodon/actions/domain_blocks.js @@ -1,8 +1,6 @@ import api, { getLinks } from '../api'; import { blockDomainSuccess, unblockDomainSuccess } from "./domain_blocks_typed"; -import { openModal } from './modal'; - export * from "./domain_blocks_typed"; @@ -152,12 +150,3 @@ export function expandDomainBlocksFail(error) { error, }; } - -export const initDomainBlockModal = account => dispatch => dispatch(openModal({ - modalType: 'DOMAIN_BLOCK', - modalProps: { - domain: account.get('acct').split('@')[1], - acct: account.get('acct'), - accountId: account.get('id'), - }, -})); diff --git a/app/javascript/mastodon/actions/emoji_reactions.js b/app/javascript/mastodon/actions/emoji_reactions.js deleted file mode 100644 index 6b97b1f743..0000000000 --- a/app/javascript/mastodon/actions/emoji_reactions.js +++ /dev/null @@ -1,94 +0,0 @@ -import api, { getLinks } from '../api'; - -import { importFetchedStatuses } from './importer'; - -export const EMOJI_REACTED_STATUSES_FETCH_REQUEST = 'EMOJI_REACTED_STATUSES_FETCH_REQUEST'; -export const EMOJI_REACTED_STATUSES_FETCH_SUCCESS = 'EMOJI_REACTED_STATUSES_FETCH_SUCCESS'; -export const EMOJI_REACTED_STATUSES_FETCH_FAIL = 'EMOJI_REACTED_STATUSES_FETCH_FAIL'; - -export const EMOJI_REACTED_STATUSES_EXPAND_REQUEST = 'EMOJI_REACTED_STATUSES_EXPAND_REQUEST'; -export const EMOJI_REACTED_STATUSES_EXPAND_SUCCESS = 'EMOJI_REACTED_STATUSES_EXPAND_SUCCESS'; -export const EMOJI_REACTED_STATUSES_EXPAND_FAIL = 'EMOJI_REACTED_STATUSES_EXPAND_FAIL'; - -export function fetchEmojiReactedStatuses() { - return (dispatch, getState) => { - if (getState().getIn(['status_lists', 'emoji_reactions', 'isLoading'])) { - return; - } - - dispatch(fetchEmojiReactedStatusesRequest()); - - api(getState).get('/api/v1/emoji_reactions').then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); - dispatch(fetchEmojiReactedStatusesSuccess(response.data, next ? next.uri : null)); - }).catch(error => { - dispatch(fetchEmojiReactedStatusesFail(error)); - }); - }; -} - -export function fetchEmojiReactedStatusesRequest() { - return { - type: EMOJI_REACTED_STATUSES_FETCH_REQUEST, - skipLoading: true, - }; -} - -export function fetchEmojiReactedStatusesSuccess(statuses, next) { - return { - type: EMOJI_REACTED_STATUSES_FETCH_SUCCESS, - statuses, - next, - skipLoading: true, - }; -} - -export function fetchEmojiReactedStatusesFail(error) { - return { - type: EMOJI_REACTED_STATUSES_FETCH_FAIL, - error, - skipLoading: true, - }; -} - -export function expandEmojiReactedStatuses() { - return (dispatch, getState) => { - const url = getState().getIn(['status_lists', 'emoji_reactions', 'next'], null); - - if (url === null || getState().getIn(['status_lists', 'emoji_reactions', 'isLoading'])) { - return; - } - - dispatch(expandEmojiReactedStatusesRequest()); - - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); - dispatch(expandEmojiReactedStatusesSuccess(response.data, next ? next.uri : null)); - }).catch(error => { - dispatch(expandEmojiReactedStatusesFail(error)); - }); - }; -} - -export function expandEmojiReactedStatusesRequest() { - return { - type: EMOJI_REACTED_STATUSES_EXPAND_REQUEST, - }; -} - -export function expandEmojiReactedStatusesSuccess(statuses, next) { - return { - type: EMOJI_REACTED_STATUSES_EXPAND_SUCCESS, - statuses, - next, - }; -} - -export function expandEmojiReactedStatusesFail(error) { - return { - type: EMOJI_REACTED_STATUSES_EXPAND_FAIL, - error, - }; -} diff --git a/app/javascript/mastodon/actions/favourites.js b/app/javascript/mastodon/actions/favourites.js index e5e2c481aa..2d4d4e6206 100644 --- a/app/javascript/mastodon/actions/favourites.js +++ b/app/javascript/mastodon/actions/favourites.js @@ -1,5 +1,3 @@ -// Kmyblue tracking marker: copied emoji_reactions.js - import api, { getLinks } from '../api'; import { importFetchedStatuses } from './importer'; diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js index 906e384354..16f191b584 100644 --- a/app/javascript/mastodon/actions/importer/index.js +++ b/app/javascript/mastodon/actions/importer/index.js @@ -72,10 +72,6 @@ export function importFetchedStatuses(statuses) { processStatus(status.reblog); } - if (status.quote && status.quote.id && !getState().getIn(['statuses', status.id])) { - processStatus(status.quote); - } - if (status.poll && status.poll.id) { pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id]))); } diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index 05b6250761..b5a30343e4 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -1,7 +1,7 @@ import escapeTextContentForBrowser from 'escape-html'; import emojify from '../../features/emoji/emoji'; -import { expandSpoilers, me } from '../../initial_state'; +import { expandSpoilers } from '../../initial_state'; const domParser = new DOMParser(); @@ -40,14 +40,6 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.filtered = status.filtered.map(normalizeFilterResult); } - if (status.emoji_reactions) { - normalStatus.emoji_reactions = normalizeEmojiReactions(status.emoji_reactions); - } - - if (!status.visibility_ex) { - normalStatus.visibility_ex = status.visibility; - } - // Only calculate these values when status first encountered and // when the underlying values change. Otherwise keep the ones // already in the reducer @@ -58,11 +50,6 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.spoiler_text = normalOldStatus.get('spoiler_text'); normalStatus.hidden = normalOldStatus.get('hidden'); - // for quoted post - if (!normalStatus.filtered && normalOldStatus.get('filtered')) { - normalStatus.filtered = normalOldStatus.get('filtered'); - } - if (normalOldStatus.get('translation')) { normalStatus.translation = normalOldStatus.get('translation'); } @@ -74,10 +61,6 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.spoiler_text = ''; } - if (normalStatus.emojis && normalStatus.emojis.some((emoji) => emoji.is_sensitive) && !normalStatus.spoiler_text) { - normalStatus.spoiler_text = '[Contains sensitive custom emoji(s)]'; - } - const spoilerText = normalStatus.spoiler_text || ''; const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); const emojiMap = makeEmojiMap(normalStatus.emojis); @@ -103,17 +86,6 @@ export function normalizeStatus(status, normalOldStatus) { return normalStatus; } -export function normalizeEmojiReactions(emoji_reactions) { - const myAccountId = me; - let converted = []; - for (let emoji_reaction of emoji_reactions) { - let obj = emoji_reaction; - obj.me = obj.account_ids.some((id) => id === myAccountId); - converted.push(obj); - } - return converted; -} - export function normalizeStatusTranslation(translation, status) { const emojiMap = makeEmojiMap(status.get('emojis').toJS()); diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js index 640c5c3128..7d0144438a 100644 --- a/app/javascript/mastodon/actions/interactions.js +++ b/app/javascript/mastodon/actions/interactions.js @@ -1,7 +1,7 @@ import api, { getLinks } from '../api'; import { fetchRelationships } from './accounts'; -import { importFetchedAccounts, importFetchedStatus, importFetchedStatuses } from './importer'; +import { importFetchedAccounts, importFetchedStatus } from './importer'; export const REBLOG_REQUEST = 'REBLOG_REQUEST'; export const REBLOG_SUCCESS = 'REBLOG_SUCCESS'; @@ -15,10 +15,6 @@ export const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST'; export const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS'; export const FAVOURITE_FAIL = 'FAVOURITE_FAIL'; -export const EMOJIREACT_REQUEST = 'EMOJIREACT_REQUEST'; -export const EMOJIREACT_SUCCESS = 'EMOJIREACT_SUCCESS'; -export const EMOJIREACT_FAIL = 'EMOJIREACT_FAIL'; - export const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST'; export const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS'; export const UNREBLOG_FAIL = 'UNREBLOG_FAIL'; @@ -27,10 +23,6 @@ export const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST'; export const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS'; export const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL'; -export const UNEMOJIREACT_REQUEST = 'UNEMOJIREACT_REQUEST'; -export const UNEMOJIREACT_SUCCESS = 'UNEMOJIREACT_SUCCESS'; -export const UNEMOJIREACT_FAIL = 'UNEMOJIREACT_FAIL'; - export const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST'; export const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS'; export const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL'; @@ -41,19 +33,7 @@ export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL'; export const FAVOURITES_EXPAND_REQUEST = 'FAVOURITES_EXPAND_REQUEST'; export const FAVOURITES_EXPAND_SUCCESS = 'FAVOURITES_EXPAND_SUCCESS'; -export const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL'; - -export const STATUS_REFERENCES_FETCH_REQUEST = 'STATUS_REFERENCES_FETCH_REQUEST'; -export const STATUS_REFERENCES_FETCH_SUCCESS = 'STATUS_REFERENCES_FETCH_SUCCESS'; -export const STATUS_REFERENCES_FETCH_FAIL = 'STATUS_REFERENCES_FETCH_FAIL'; - -export const EMOJI_REACTIONS_FETCH_REQUEST = 'EMOJI_REACTIONS_FETCH_REQUEST'; -export const EMOJI_REACTIONS_FETCH_SUCCESS = 'EMOJI_REACTIONS_FETCH_SUCCESS'; -export const EMOJI_REACTIONS_FETCH_FAIL = 'EMOJI_REACTIONS_FETCH_FAIL'; - -export const EMOJI_REACTIONS_EXPAND_REQUEST = 'EMOJI_REACTIONS_EXPAND_REQUEST'; -export const EMOJI_REACTIONS_EXPAND_SUCCESS = 'EMOJI_REACTIONS_EXPAND_SUCCESS'; -export const EMOJI_REACTIONS_EXPAND_FAIL = 'EMOJI_REACTIONS_EXPAND_FAIL'; +export const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL'; export const PIN_REQUEST = 'PIN_REQUEST'; export const PIN_SUCCESS = 'PIN_SUCCESS'; @@ -71,14 +51,6 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST'; export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS'; export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL'; -export const MENTIONED_USERS_FETCH_REQUEST = 'MENTIONED_USERS_FETCH_REQUEST'; -export const MENTIONED_USERS_FETCH_SUCCESS = 'MENTIONED_USERS_FETCH_SUCCESS'; -export const MENTIONED_USERS_FETCH_FAIL = 'MENTIONED_USERS_FETCH_FAIL'; - -export const MENTIONED_USERS_EXPAND_REQUEST = 'MENTIONED_USERS_EXPAND_REQUEST'; -export const MENTIONED_USERS_EXPAND_SUCCESS = 'MENTIONED_USERS_EXPAND_SUCCESS'; -export const MENTIONED_USERS_EXPAND_FAIL = 'MENTIONED_USERS_EXPAND_FAIL'; - export function reblog(status, visibility) { return function (dispatch, getState) { dispatch(reblogRequest(status)); @@ -233,91 +205,6 @@ export function unfavouriteFail(status, error) { }; } -export function emojiReact(status, emoji) { - return function (dispatch, getState) { - dispatch(emojiReactRequest(status, emoji)); - - const api_emoji = typeof emoji !== 'string' ? (emoji.custom ? (emoji.name + (emoji.domain || '')) : emoji.native) : emoji; - - api(getState).post(`/api/v1/statuses/${status.get('id')}/emoji_reactions`, { emoji: api_emoji }).then(function (response) { - dispatch(importFetchedStatus(response.data)); - dispatch(emojiReactSuccess(status, emoji)); - }).catch(function (error) { - dispatch(emojiReactFail(status, emoji, error)); - }); - }; -} - -export function unEmojiReact(status, emoji) { - return (dispatch, getState) => { - dispatch(unEmojiReactRequest(status, emoji)); - - api(getState).post(`/api/v1/statuses/${status.get('id')}/emoji_unreaction`, { emoji }).then((response) => { - // TODO: do not update because this api has a bug - dispatch(importFetchedStatus(response.data)); - dispatch(unEmojiReactSuccess(status, emoji)); - }).catch(error => { - dispatch(unEmojiReactFail(status, emoji, error)); - }); - }; -} - -export function emojiReactRequest(status, emoji) { - return { - type: EMOJIREACT_REQUEST, - status: status, - emoji: emoji, - skipLoading: true, - }; -} - -export function emojiReactSuccess(status, emoji) { - return { - type: EMOJIREACT_SUCCESS, - status: status, - emoji: emoji, - skipLoading: true, - }; -} - -export function emojiReactFail(status, emoji, error) { - return { - type: EMOJIREACT_FAIL, - status: status, - emoji: emoji, - error: error, - skipLoading: true, - }; -} - -export function unEmojiReactRequest(status, emoji) { - return { - type: UNEMOJIREACT_REQUEST, - status: status, - emoji: emoji, - skipLoading: true, - }; -} - -export function unEmojiReactSuccess(status, emoji) { - return { - type: UNEMOJIREACT_SUCCESS, - status: status, - emoji: emoji, - skipLoading: true, - }; -} - -export function unEmojiReactFail(status, emoji, error) { - return { - type: UNEMOJIREACT_FAIL, - status: status, - emoji: emoji, - error: error, - skipLoading: true, - }; -} - export function bookmark(status) { return function (dispatch, getState) { dispatch(bookmarkRequest(status)); @@ -554,120 +441,6 @@ export function expandFavouritesFail(id, error) { }; } -export function fetchEmojiReactions(id) { - return (dispatch, getState) => { - dispatch(fetchEmojiReactionsRequest(id)); - - api(getState).get(`/api/v1/statuses/${id}/emoji_reactioned_by`).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data.map((er) => er.account))); - dispatch(fetchEmojiReactionsSuccess(id, response.data, next ? next.uri : null)); - }).catch(error => { - dispatch(fetchEmojiReactionsFail(id, error)); - }); - }; -} - -export function fetchEmojiReactionsRequest(id) { - return { - type: EMOJI_REACTIONS_FETCH_REQUEST, - id, - }; -} - -export function fetchEmojiReactionsSuccess(id, accounts, next) { - return { - type: EMOJI_REACTIONS_FETCH_SUCCESS, - id, - accounts, - next, - }; -} - -export function fetchEmojiReactionsFail(id, error) { - return { - type: EMOJI_REACTIONS_FETCH_FAIL, - error, - }; -} - -export function expandEmojiReactions(id) { - return (dispatch, getState) => { - const url = getState().getIn(['user_lists', 'emoji_reactioned_by', id, 'next']); - if (url === null) { - return; - } - - dispatch(expandEmojiReactionsRequest(id)); - - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(importFetchedAccounts(response.data.map((er) => er.account))); - dispatch(expandEmojiReactionsSuccess(id, response.data, next ? next.uri : null)); - }).catch(error => dispatch(expandEmojiReactionsFail(id, error))); - }; -} - -export function expandEmojiReactionsRequest(id) { - return { - type: EMOJI_REACTIONS_EXPAND_REQUEST, - id, - }; -} - -export function expandEmojiReactionsSuccess(id, accounts, next) { - return { - type: EMOJI_REACTIONS_EXPAND_SUCCESS, - id, - accounts, - next, - }; -} - -export function expandEmojiReactionsFail(id, error) { - return { - type: EMOJI_REACTIONS_EXPAND_FAIL, - id, - error, - }; -} - -export function fetchStatusReferences(id) { - return (dispatch, getState) => { - dispatch(fetchStatusReferencesRequest(id)); - - api(getState).get(`/api/v1/statuses/${id}/referred_by`).then(response => { - dispatch(importFetchedStatuses(response.data)); - dispatch(fetchStatusReferencesSuccess(id, response.data)); - }).catch(error => { - dispatch(fetchStatusReferencesFail(id, error)); - }); - }; -} - -export function fetchStatusReferencesRequest(id) { - return { - type: STATUS_REFERENCES_FETCH_REQUEST, - id, - }; -} - -export function fetchStatusReferencesSuccess(id, statuses) { - return { - type: STATUS_REFERENCES_FETCH_SUCCESS, - id, - statuses, - }; -} - -export function fetchStatusReferencesFail(id, error) { - return { - type: STATUS_REFERENCES_FETCH_FAIL, - error, - }; -} - export function pin(status) { return (dispatch, getState) => { dispatch(pinRequest(status)); @@ -743,85 +516,3 @@ export function unpinFail(status, error) { skipLoading: true, }; } - -export function fetchMentionedUsers(id) { - return (dispatch, getState) => { - dispatch(fetchMentionedUsersRequest(id)); - - api(getState).get(`/api/v1/statuses/${id}/mentioned_by`).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data)); - dispatch(fetchMentionedUsersSuccess(id, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map(item => item.id))); - }).catch(error => { - dispatch(fetchMentionedUsersFail(id, error)); - }); - }; -} - -export function fetchMentionedUsersRequest(id) { - return { - type: MENTIONED_USERS_FETCH_REQUEST, - id, - }; -} - -export function fetchMentionedUsersSuccess(id, accounts, next) { - return { - type: MENTIONED_USERS_FETCH_SUCCESS, - id, - accounts, - next, - }; -} - -export function fetchMentionedUsersFail(id, error) { - return { - type: MENTIONED_USERS_FETCH_FAIL, - id, - error, - }; -} - -export function expandMentionedUsers(id) { - return (dispatch, getState) => { - const url = getState().getIn(['user_lists', 'mentioned_users', id, 'next']); - if (url === null) { - return; - } - - dispatch(expandMentionedUsersRequest(id)); - - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(importFetchedAccounts(response.data)); - dispatch(expandMentionedUsersSuccess(id, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map(item => item.id))); - }).catch(error => dispatch(expandMentionedUsersFail(id, error))); - }; -} - -export function expandMentionedUsersRequest(id) { - return { - type: MENTIONED_USERS_EXPAND_REQUEST, - id, - }; -} - -export function expandMentionedUsersSuccess(id, accounts, next) { - return { - type: MENTIONED_USERS_EXPAND_SUCCESS, - id, - accounts, - next, - }; -} - -export function expandMentionedUsersFail(id, error) { - return { - type: MENTIONED_USERS_EXPAND_FAIL, - id, - error, - }; -} diff --git a/app/javascript/mastodon/actions/lists.js b/app/javascript/mastodon/actions/lists.js index e494a40a5d..b0789cd426 100644 --- a/app/javascript/mastodon/actions/lists.js +++ b/app/javascript/mastodon/actions/lists.js @@ -1,5 +1,3 @@ -// Kmyblue tracking marker: copied circles.js, antennas.js - import api from '../api'; import { showAlertForError } from './alerts'; @@ -153,15 +151,10 @@ export const createListFail = error => ({ error, }); -export const updateList = (id, title, shouldReset, isExclusive, replies_policy, notify) => (dispatch, getState) => { +export const updateList = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => { dispatch(updateListRequest(id)); - api(getState).put(`/api/v1/lists/${id}`, { - title, - replies_policy, - exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive, - notify: typeof notify === 'undefined' ? undefined : !!notify, - }).then(({ data }) => { + api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => { dispatch(updateListSuccess(data)); if (shouldReset) { @@ -245,6 +238,7 @@ export const fetchListSuggestions = q => (dispatch, getState) => { const params = { q, resolve: false, + limit: 4, following: true, }; diff --git a/app/javascript/mastodon/actions/markers.js b/app/javascript/mastodon/actions/markers.js new file mode 100644 index 0000000000..cfc329a8b7 --- /dev/null +++ b/app/javascript/mastodon/actions/markers.js @@ -0,0 +1,152 @@ +import { List as ImmutableList } from 'immutable'; + +import { debounce } from 'lodash'; + +import api from '../api'; +import { compareId } from '../compare_id'; + +export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST'; +export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS'; +export const MARKERS_FETCH_FAIL = 'MARKERS_FETCH_FAIL'; +export const MARKERS_SUBMIT_SUCCESS = 'MARKERS_SUBMIT_SUCCESS'; + +export const synchronouslySubmitMarkers = () => (dispatch, getState) => { + const accessToken = getState().getIn(['meta', 'access_token'], ''); + const params = _buildParams(getState()); + + if (Object.keys(params).length === 0 || accessToken === '') { + return; + } + + // The Fetch API allows us to perform requests that will be carried out + // after the page closes. But that only works if the `keepalive` attribute + // is supported. + if (window.fetch && 'keepalive' in new Request('')) { + fetch('/api/v1/markers', { + keepalive: true, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${accessToken}`, + }, + body: JSON.stringify(params), + }); + + return; + } else if (navigator && navigator.sendBeacon) { + // Failing that, we can use sendBeacon, but we have to encode the data as + // FormData for DoorKeeper to recognize the token. + const formData = new FormData(); + + formData.append('bearer_token', accessToken); + + for (const [id, value] of Object.entries(params)) { + formData.append(`${id}[last_read_id]`, value.last_read_id); + } + + if (navigator.sendBeacon('/api/v1/markers', formData)) { + return; + } + } + + // If neither Fetch nor sendBeacon worked, try to perform a synchronous + // request. + try { + const client = new XMLHttpRequest(); + + client.open('POST', '/api/v1/markers', false); + client.setRequestHeader('Content-Type', 'application/json'); + client.setRequestHeader('Authorization', `Bearer ${accessToken}`); + client.send(JSON.stringify(params)); + } catch (e) { + // Do not make the BeforeUnload handler error out + } +}; + +const _buildParams = (state) => { + const params = {}; + + const lastHomeId = state.getIn(['timelines', 'home', 'items'], ImmutableList()).find(item => item !== null); + const lastNotificationId = state.getIn(['notifications', 'lastReadId']); + + if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) { + params.home = { + last_read_id: lastHomeId, + }; + } + + if (lastNotificationId && compareId(lastNotificationId, state.getIn(['markers', 'notifications'])) > 0) { + params.notifications = { + last_read_id: lastNotificationId, + }; + } + + return params; +}; + +const debouncedSubmitMarkers = debounce((dispatch, getState) => { + const accessToken = getState().getIn(['meta', 'access_token'], ''); + const params = _buildParams(getState()); + + if (Object.keys(params).length === 0 || accessToken === '') { + return; + } + + api(getState).post('/api/v1/markers', params).then(() => { + dispatch(submitMarkersSuccess(params)); + }).catch(() => {}); +}, 300000, { leading: true, trailing: true }); + +export function submitMarkersSuccess({ home, notifications }) { + return { + type: MARKERS_SUBMIT_SUCCESS, + home: (home || {}).last_read_id, + notifications: (notifications || {}).last_read_id, + }; +} + +export function submitMarkers(params = {}) { + const result = (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState); + + if (params.immediate === true) { + debouncedSubmitMarkers.flush(); + } + + return result; +} + +export const fetchMarkers = () => (dispatch, getState) => { + const params = { timeline: ['notifications'] }; + + dispatch(fetchMarkersRequest()); + + api(getState).get('/api/v1/markers', { params }).then(response => { + dispatch(fetchMarkersSuccess(response.data)); + }).catch(error => { + dispatch(fetchMarkersFail(error)); + }); +}; + +export function fetchMarkersRequest() { + return { + type: MARKERS_FETCH_REQUEST, + skipLoading: true, + }; +} + +export function fetchMarkersSuccess(markers) { + return { + type: MARKERS_FETCH_SUCCESS, + markers, + skipLoading: true, + }; +} + +export function fetchMarkersFail(error) { + return { + type: MARKERS_FETCH_FAIL, + error, + skipLoading: true, + skipAlert: true, + }; +} diff --git a/app/javascript/mastodon/actions/markers.ts b/app/javascript/mastodon/actions/markers.ts deleted file mode 100644 index 91f78ee286..0000000000 --- a/app/javascript/mastodon/actions/markers.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { debounce } from 'lodash'; - -import type { MarkerJSON } from 'mastodon/api_types/markers'; -import type { AppDispatch, RootState } from 'mastodon/store'; -import { createAppAsyncThunk } from 'mastodon/store/typed_functions'; - -import api, { authorizationTokenFromState } from '../api'; -import { compareId } from '../compare_id'; - -export const synchronouslySubmitMarkers = createAppAsyncThunk( - 'markers/submit', - async (_args, { getState }) => { - const accessToken = authorizationTokenFromState(getState); - const params = buildPostMarkersParams(getState()); - - if (Object.keys(params).length === 0 || !accessToken) { - return; - } - - // The Fetch API allows us to perform requests that will be carried out - // after the page closes. But that only works if the `keepalive` attribute - // is supported. - if ('fetch' in window && 'keepalive' in new Request('')) { - await fetch('/api/v1/markers', { - keepalive: true, - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify(params), - }); - - return; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - } else if ('navigator' && 'sendBeacon' in navigator) { - // Failing that, we can use sendBeacon, but we have to encode the data as - // FormData for DoorKeeper to recognize the token. - const formData = new FormData(); - - formData.append('bearer_token', accessToken); - - for (const [id, value] of Object.entries(params)) { - if (value.last_read_id) - formData.append(`${id}[last_read_id]`, value.last_read_id); - } - - if (navigator.sendBeacon('/api/v1/markers', formData)) { - return; - } - } - - // If neither Fetch nor sendBeacon worked, try to perform a synchronous - // request. - try { - const client = new XMLHttpRequest(); - - client.open('POST', '/api/v1/markers', false); - client.setRequestHeader('Content-Type', 'application/json'); - client.setRequestHeader('Authorization', `Bearer ${accessToken}`); - client.send(JSON.stringify(params)); - } catch (e) { - // Do not make the BeforeUnload handler error out - } - }, -); - -interface MarkerParam { - last_read_id?: string; -} - -function getLastNotificationId(state: RootState): string | undefined { - // @ts-expect-error state.notifications is not yet typed - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call - return state.getIn(['notifications', 'lastReadId']); -} - -const buildPostMarkersParams = (state: RootState) => { - const params = {} as { home?: MarkerParam; notifications?: MarkerParam }; - - const lastNotificationId = getLastNotificationId(state); - - if ( - lastNotificationId && - compareId(lastNotificationId, state.markers.notifications) > 0 - ) { - params.notifications = { - last_read_id: lastNotificationId, - }; - } - - return params; -}; - -export const submitMarkersAction = createAppAsyncThunk<{ - home: string | undefined; - notifications: string | undefined; -}>('markers/submitAction', async (_args, { getState }) => { - const accessToken = authorizationTokenFromState(getState); - const params = buildPostMarkersParams(getState()); - - if (Object.keys(params).length === 0 || accessToken === '') { - return { home: undefined, notifications: undefined }; - } - - await api(getState).post('/api/v1/markers', params); - - return { - home: params.home?.last_read_id, - notifications: params.notifications?.last_read_id, - }; -}); - -const debouncedSubmitMarkers = debounce( - (dispatch: AppDispatch) => { - void dispatch(submitMarkersAction()); - }, - 300000, - { - leading: true, - trailing: true, - }, -); - -export const submitMarkers = createAppAsyncThunk( - 'markers/submit', - (params: { immediate?: boolean }, { dispatch }) => { - debouncedSubmitMarkers(dispatch); - - if (params.immediate) { - debouncedSubmitMarkers.flush(); - } - }, -); - -export const fetchMarkers = createAppAsyncThunk( - 'markers/fetch', - async (_args, { getState }) => { - const response = await api(getState).get>( - `/api/v1/markers`, - { params: { timeline: ['notifications'] } }, - ); - - return { markers: response.data }; - }, -); diff --git a/app/javascript/mastodon/actions/mutes.js b/app/javascript/mastodon/actions/mutes.js index 99c113f414..fb041078b8 100644 --- a/app/javascript/mastodon/actions/mutes.js +++ b/app/javascript/mastodon/actions/mutes.js @@ -12,6 +12,10 @@ export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST'; export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS'; export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL'; +export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL'; +export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS'; +export const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION'; + export function fetchMutes() { return (dispatch, getState) => { dispatch(fetchMutesRequest()); @@ -88,12 +92,26 @@ export function expandMutesFail(error) { export function initMuteModal(account) { return dispatch => { - dispatch(openModal({ - modalType: 'MUTE', - modalProps: { - accountId: account.get('id'), - acct: account.get('acct'), - }, - })); + dispatch({ + type: MUTES_INIT_MODAL, + account, + }); + + dispatch(openModal({ modalType: 'MUTE' })); + }; +} + +export function toggleHideNotifications() { + return dispatch => { + dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS }); + }; +} + +export function changeMuteDuration(duration) { + return dispatch => { + dispatch({ + type: MUTES_CHANGE_DURATION, + duration, + }); }; } diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index c8b1178d77..eafbf42d1b 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -4,7 +4,7 @@ import { defineMessages } from 'react-intl'; import { List as ImmutableList } from 'immutable'; import { compareId } from 'mastodon/compare_id'; -import { enableEmojiReaction, usePendingItems as preferPendingItems } from 'mastodon/initial_state'; +import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; import api, { getLinks } from '../api'; import { unescapeHTML } from '../utils/html'; @@ -21,7 +21,6 @@ import { submitMarkers } from './markers'; import { notificationsUpdate } from "./notifications_typed"; import { register as registerPushNotifications } from './push_notifications'; import { saveSettings } from './settings'; -import { STATUS_EMOJI_REACTION_UPDATE } from './statuses'; export * from "./notifications_typed"; @@ -45,38 +44,6 @@ export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ'; export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT'; export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION'; -export const NOTIFICATION_POLICY_FETCH_REQUEST = 'NOTIFICATION_POLICY_FETCH_REQUEST'; -export const NOTIFICATION_POLICY_FETCH_SUCCESS = 'NOTIFICATION_POLICY_FETCH_SUCCESS'; -export const NOTIFICATION_POLICY_FETCH_FAIL = 'NOTIFICATION_POLICY_FETCH_FAIL'; - -export const NOTIFICATION_REQUESTS_FETCH_REQUEST = 'NOTIFICATION_REQUESTS_FETCH_REQUEST'; -export const NOTIFICATION_REQUESTS_FETCH_SUCCESS = 'NOTIFICATION_REQUESTS_FETCH_SUCCESS'; -export const NOTIFICATION_REQUESTS_FETCH_FAIL = 'NOTIFICATION_REQUESTS_FETCH_FAIL'; - -export const NOTIFICATION_REQUESTS_EXPAND_REQUEST = 'NOTIFICATION_REQUESTS_EXPAND_REQUEST'; -export const NOTIFICATION_REQUESTS_EXPAND_SUCCESS = 'NOTIFICATION_REQUESTS_EXPAND_SUCCESS'; -export const NOTIFICATION_REQUESTS_EXPAND_FAIL = 'NOTIFICATION_REQUESTS_EXPAND_FAIL'; - -export const NOTIFICATION_REQUEST_FETCH_REQUEST = 'NOTIFICATION_REQUEST_FETCH_REQUEST'; -export const NOTIFICATION_REQUEST_FETCH_SUCCESS = 'NOTIFICATION_REQUEST_FETCH_SUCCESS'; -export const NOTIFICATION_REQUEST_FETCH_FAIL = 'NOTIFICATION_REQUEST_FETCH_FAIL'; - -export const NOTIFICATION_REQUEST_ACCEPT_REQUEST = 'NOTIFICATION_REQUEST_ACCEPT_REQUEST'; -export const NOTIFICATION_REQUEST_ACCEPT_SUCCESS = 'NOTIFICATION_REQUEST_ACCEPT_SUCCESS'; -export const NOTIFICATION_REQUEST_ACCEPT_FAIL = 'NOTIFICATION_REQUEST_ACCEPT_FAIL'; - -export const NOTIFICATION_REQUEST_DISMISS_REQUEST = 'NOTIFICATION_REQUEST_DISMISS_REQUEST'; -export const NOTIFICATION_REQUEST_DISMISS_SUCCESS = 'NOTIFICATION_REQUEST_DISMISS_SUCCESS'; -export const NOTIFICATION_REQUEST_DISMISS_FAIL = 'NOTIFICATION_REQUEST_DISMISS_FAIL'; - -export const NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST'; -export const NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS'; -export const NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL = 'NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL'; - -export const NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST'; -export const NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS'; -export const NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL'; - defineMessages({ mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, group: { id: 'notifications.group', defaultMessage: '{count} notifications' }, @@ -94,14 +61,6 @@ export const loadPending = () => ({ type: NOTIFICATIONS_LOAD_PENDING, }); -export function updateEmojiReactions(emoji_reaction) { - return (dispatch) => - dispatch({ - type: STATUS_EMOJI_REACTION_UPDATE, - emoji_reaction, - }); -} - export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']); @@ -114,7 +73,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { if (['mention', 'status'].includes(notification.type) && notification.status.filtered) { const filters = notification.status.filtered.filter(result => result.filter.context.includes('notifications')); - if (filters.some(result => result.filter.filter_action_ex === 'hide')) { + if (filters.some(result => result.filter.filter_action === 'hide')) { return; } @@ -171,13 +130,10 @@ const excludeTypesFromFilter = filter => { 'follow', 'follow_request', 'favourite', - 'emoji_reaction', 'reblog', - 'status_reference', 'mention', 'poll', 'status', - 'list_status', 'update', 'admin.sign_up', 'admin.report', @@ -206,16 +162,11 @@ export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) { } } - let exclude_types = activeFilter === 'all' - ? excludeTypesFromSettings(getState()) - : excludeTypesFromFilter(activeFilter); - if (!enableEmojiReaction && !exclude_types.includes('emoji_reaction')) { - exclude_types.push('emoji_reaction'); - } - const params = { max_id: maxId, - exclude_types, + exclude_types: activeFilter === 'all' + ? excludeTypesFromSettings(getState()) + : excludeTypesFromFilter(activeFilter), }; if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) { @@ -362,270 +313,3 @@ export function setBrowserPermission (value) { value, }; } - -export const fetchNotificationPolicy = () => (dispatch, getState) => { - dispatch(fetchNotificationPolicyRequest()); - - api(getState).get('/api/v1/notifications/policy').then(({ data }) => { - dispatch(fetchNotificationPolicySuccess(data)); - }).catch(err => { - dispatch(fetchNotificationPolicyFail(err)); - }); -}; - -export const fetchNotificationPolicyRequest = () => ({ - type: NOTIFICATION_POLICY_FETCH_REQUEST, -}); - -export const fetchNotificationPolicySuccess = policy => ({ - type: NOTIFICATION_POLICY_FETCH_SUCCESS, - policy, -}); - -export const fetchNotificationPolicyFail = error => ({ - type: NOTIFICATION_POLICY_FETCH_FAIL, - error, -}); - -export const updateNotificationsPolicy = params => (dispatch, getState) => { - dispatch(fetchNotificationPolicyRequest()); - - api(getState).put('/api/v1/notifications/policy', params).then(({ data }) => { - dispatch(fetchNotificationPolicySuccess(data)); - }).catch(err => { - dispatch(fetchNotificationPolicyFail(err)); - }); -}; - -export const fetchNotificationRequests = () => (dispatch, getState) => { - const params = {}; - - if (getState().getIn(['notificationRequests', 'isLoading'])) { - return; - } - - if (getState().getIn(['notificationRequests', 'items'])?.size > 0) { - params.since_id = getState().getIn(['notificationRequests', 'items', 0, 'id']); - } - - dispatch(fetchNotificationRequestsRequest()); - - api(getState).get('/api/v1/notifications/requests', { params }).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data.map(x => x.account))); - dispatch(fetchNotificationRequestsSuccess(response.data, next ? next.uri : null)); - }).catch(err => { - dispatch(fetchNotificationRequestsFail(err)); - }); -}; - -export const fetchNotificationRequestsRequest = () => ({ - type: NOTIFICATION_REQUESTS_FETCH_REQUEST, -}); - -export const fetchNotificationRequestsSuccess = (requests, next) => ({ - type: NOTIFICATION_REQUESTS_FETCH_SUCCESS, - requests, - next, -}); - -export const fetchNotificationRequestsFail = error => ({ - type: NOTIFICATION_REQUESTS_FETCH_FAIL, - error, -}); - -export const expandNotificationRequests = () => (dispatch, getState) => { - const url = getState().getIn(['notificationRequests', 'next']); - - if (!url || getState().getIn(['notificationRequests', 'isLoading'])) { - return; - } - - dispatch(expandNotificationRequestsRequest()); - - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data.map(x => x.account))); - dispatch(expandNotificationRequestsSuccess(response.data, next?.uri)); - }).catch(err => { - dispatch(expandNotificationRequestsFail(err)); - }); -}; - -export const expandNotificationRequestsRequest = () => ({ - type: NOTIFICATION_REQUESTS_EXPAND_REQUEST, -}); - -export const expandNotificationRequestsSuccess = (requests, next) => ({ - type: NOTIFICATION_REQUESTS_EXPAND_SUCCESS, - requests, - next, -}); - -export const expandNotificationRequestsFail = error => ({ - type: NOTIFICATION_REQUESTS_EXPAND_FAIL, - error, -}); - -export const fetchNotificationRequest = id => (dispatch, getState) => { - const current = getState().getIn(['notificationRequests', 'current']); - - if (current.getIn(['item', 'id']) === id || current.get('isLoading')) { - return; - } - - dispatch(fetchNotificationRequestRequest(id)); - - api(getState).get(`/api/v1/notifications/requests/${id}`).then(({ data }) => { - dispatch(fetchNotificationRequestSuccess(data)); - }).catch(err => { - dispatch(fetchNotificationRequestFail(id, err)); - }); -}; - -export const fetchNotificationRequestRequest = id => ({ - type: NOTIFICATION_REQUEST_FETCH_REQUEST, - id, -}); - -export const fetchNotificationRequestSuccess = request => ({ - type: NOTIFICATION_REQUEST_FETCH_SUCCESS, - request, -}); - -export const fetchNotificationRequestFail = (id, error) => ({ - type: NOTIFICATION_REQUEST_FETCH_FAIL, - id, - error, -}); - -export const acceptNotificationRequest = id => (dispatch, getState) => { - dispatch(acceptNotificationRequestRequest(id)); - - api(getState).post(`/api/v1/notifications/requests/${id}/accept`).then(() => { - dispatch(acceptNotificationRequestSuccess(id)); - }).catch(err => { - dispatch(acceptNotificationRequestFail(id, err)); - }); -}; - -export const acceptNotificationRequestRequest = id => ({ - type: NOTIFICATION_REQUEST_ACCEPT_REQUEST, - id, -}); - -export const acceptNotificationRequestSuccess = id => ({ - type: NOTIFICATION_REQUEST_ACCEPT_SUCCESS, - id, -}); - -export const acceptNotificationRequestFail = (id, error) => ({ - type: NOTIFICATION_REQUEST_ACCEPT_FAIL, - id, - error, -}); - -export const dismissNotificationRequest = id => (dispatch, getState) => { - dispatch(dismissNotificationRequestRequest(id)); - - api(getState).post(`/api/v1/notifications/requests/${id}/dismiss`).then(() =>{ - dispatch(dismissNotificationRequestSuccess(id)); - }).catch(err => { - dispatch(dismissNotificationRequestFail(id, err)); - }); -}; - -export const dismissNotificationRequestRequest = id => ({ - type: NOTIFICATION_REQUEST_DISMISS_REQUEST, - id, -}); - -export const dismissNotificationRequestSuccess = id => ({ - type: NOTIFICATION_REQUEST_DISMISS_SUCCESS, - id, -}); - -export const dismissNotificationRequestFail = (id, error) => ({ - type: NOTIFICATION_REQUEST_DISMISS_FAIL, - id, - error, -}); - -export const fetchNotificationsForRequest = accountId => (dispatch, getState) => { - const current = getState().getIn(['notificationRequests', 'current']); - const params = { account_id: accountId }; - - if (current.getIn(['item', 'account']) === accountId) { - if (current.getIn(['notifications', 'isLoading'])) { - return; - } - - if (current.getIn(['notifications', 'items'])?.size > 0) { - params.since_id = current.getIn(['notifications', 'items', 0, 'id']); - } - } - - dispatch(fetchNotificationsForRequestRequest()); - - api(getState).get('/api/v1/notifications', { params }).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data.map(item => item.account))); - dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); - dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account))); - - dispatch(fetchNotificationsForRequestSuccess(response.data, next?.uri)); - }).catch(err => { - dispatch(fetchNotificationsForRequestFail(err)); - }); -}; - -export const fetchNotificationsForRequestRequest = () => ({ - type: NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST, -}); - -export const fetchNotificationsForRequestSuccess = (notifications, next) => ({ - type: NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS, - notifications, - next, -}); - -export const fetchNotificationsForRequestFail = (error) => ({ - type: NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL, - error, -}); - -export const expandNotificationsForRequest = () => (dispatch, getState) => { - const url = getState().getIn(['notificationRequests', 'current', 'notifications', 'next']); - - if (!url || getState().getIn(['notificationRequests', 'current', 'notifications', 'isLoading'])) { - return; - } - - dispatch(expandNotificationsForRequestRequest()); - - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data.map(item => item.account))); - dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); - dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account))); - - dispatch(expandNotificationsForRequestSuccess(response.data, next?.uri)); - }).catch(err => { - dispatch(expandNotificationsForRequestFail(err)); - }); -}; - -export const expandNotificationsForRequestRequest = () => ({ - type: NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST, -}); - -export const expandNotificationsForRequestSuccess = (notifications, next) => ({ - type: NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS, - notifications, - next, -}); - -export const expandNotificationsForRequestFail = (error) => ({ - type: NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL, - error, -}); diff --git a/app/javascript/mastodon/actions/picture_in_picture.js b/app/javascript/mastodon/actions/picture_in_picture.js new file mode 100644 index 0000000000..898375abeb --- /dev/null +++ b/app/javascript/mastodon/actions/picture_in_picture.js @@ -0,0 +1,46 @@ +// @ts-check + +export const PICTURE_IN_PICTURE_DEPLOY = 'PICTURE_IN_PICTURE_DEPLOY'; +export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE'; + +/** + * @typedef MediaProps + * @property {string} src + * @property {boolean} muted + * @property {number} volume + * @property {number} currentTime + * @property {string} poster + * @property {string} backgroundColor + * @property {string} foregroundColor + * @property {string} accentColor + */ + +/** + * @param {string} statusId + * @param {string} accountId + * @param {string} playerType + * @param {MediaProps} props + * @returns {object} + */ +export const deployPictureInPicture = (statusId, accountId, playerType, props) => { + // @ts-expect-error + return (dispatch, getState) => { + // Do not open a player for a toot that does not exist + if (getState().hasIn(['statuses', statusId])) { + dispatch({ + type: PICTURE_IN_PICTURE_DEPLOY, + statusId, + accountId, + playerType, + props, + }); + } + }; +}; + +/* + * @return {object} + */ +export const removePictureInPicture = () => ({ + type: PICTURE_IN_PICTURE_REMOVE, +}); diff --git a/app/javascript/mastodon/actions/picture_in_picture.ts b/app/javascript/mastodon/actions/picture_in_picture.ts deleted file mode 100644 index d34b508a33..0000000000 --- a/app/javascript/mastodon/actions/picture_in_picture.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; - -import type { PIPMediaProps } from 'mastodon/reducers/picture_in_picture'; -import { createAppAsyncThunk } from 'mastodon/store/typed_functions'; - -interface DeployParams { - statusId: string; - accountId: string; - playerType: 'audio' | 'video'; - props: PIPMediaProps; -} - -export const removePictureInPicture = createAction('pip/remove'); - -export const deployPictureInPictureAction = - createAction('pip/deploy'); - -export const deployPictureInPicture = createAppAsyncThunk( - 'pip/deploy', - (args: DeployParams, { dispatch, getState }) => { - const { statusId } = args; - - // Do not open a player for a toot that does not exist - - // @ts-expect-error state.statuses is not yet typed - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - if (getState().hasIn(['statuses', statusId])) { - dispatch(deployPictureInPictureAction(args)); - } - }, -); diff --git a/app/javascript/mastodon/actions/reaction_deck.js b/app/javascript/mastodon/actions/reaction_deck.js deleted file mode 100644 index 11510d5d0d..0000000000 --- a/app/javascript/mastodon/actions/reaction_deck.js +++ /dev/null @@ -1,79 +0,0 @@ -import api from '../api'; - -export const REACTION_DECK_FETCH_REQUEST = 'REACTION_DECK_FETCH_REQUEST'; -export const REACTION_DECK_FETCH_SUCCESS = 'REACTION_DECK_FETCH_SUCCESS'; -export const REACTION_DECK_FETCH_FAIL = 'REACTION_DECK_FETCH_FAIL'; - -export const REACTION_DECK_UPDATE_REQUEST = 'REACTION_DECK_UPDATE_REQUEST'; -export const REACTION_DECK_UPDATE_SUCCESS = 'REACTION_DECK_UPDATE_SUCCESS'; -export const REACTION_DECK_UPDATE_FAIL = 'REACTION_DECK_UPDATE_FAIL'; - -export function fetchReactionDeck() { - return (dispatch, getState) => { - dispatch(fetchReactionDeckRequest()); - - api(getState).get('/api/v1/reaction_deck').then(response => { - dispatch(fetchReactionDeckSuccess(response.data)); - }).catch(error => { - dispatch(fetchReactionDeckFail(error)); - }); - }; -} - -export function fetchReactionDeckRequest() { - return { - type: REACTION_DECK_FETCH_REQUEST, - skipLoading: true, - }; -} - -export function fetchReactionDeckSuccess(emojis) { - return { - type: REACTION_DECK_FETCH_SUCCESS, - emojis, - skipLoading: true, - }; -} - -export function fetchReactionDeckFail(error) { - return { - type: REACTION_DECK_FETCH_FAIL, - error, - skipLoading: true, - }; -} - -export function updateReactionDeck(emojis) { - return (dispatch, getState) => { - dispatch(updateReactionDeckRequest()); - - api(getState).post('/api/v1/reaction_deck', { emojis }).then(response => { - dispatch(updateReactionDeckSuccess(response.data)); - }).catch(error => { - dispatch(updateReactionDeckFail(error)); - }); - }; -} - -export function updateReactionDeckRequest() { - return { - type: REACTION_DECK_UPDATE_REQUEST, - skipLoading: true, - }; -} - -export function updateReactionDeckSuccess(emojis) { - return { - type: REACTION_DECK_UPDATE_SUCCESS, - emojis, - skipLoading: true, - }; -} - -export function updateReactionDeckFail(error) { - return { - type: REACTION_DECK_UPDATE_FAIL, - error, - skipLoading: true, - }; -} diff --git a/app/javascript/mastodon/actions/search.js b/app/javascript/mastodon/actions/search.js index a34a490e76..38a089b486 100644 --- a/app/javascript/mastodon/actions/search.js +++ b/app/javascript/mastodon/actions/search.js @@ -179,11 +179,6 @@ 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); @@ -212,4 +207,4 @@ export const hydrateSearch = () => (dispatch, getState) => { if (history !== null) { dispatch(updateSearchHistory(history)); } -}; +}; \ No newline at end of file diff --git a/app/javascript/mastodon/actions/settings.js b/app/javascript/mastodon/actions/settings.js index fbd89f9d4b..3685b0684e 100644 --- a/app/javascript/mastodon/actions/settings.js +++ b/app/javascript/mastodon/actions/settings.js @@ -20,7 +20,7 @@ export function changeSetting(path, value) { } const debouncedSave = debounce((dispatch, getState) => { - if (getState().getIn(['settings', 'saved']) || !getState().getIn(['meta', 'me'])) { + if (getState().getIn(['settings', 'saved'])) { return; } diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index 9807e6f67c..3aed807358 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -39,8 +39,6 @@ export const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS'; export const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL'; export const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO'; -export const STATUS_EMOJI_REACTION_UPDATE = 'STATUS_EMOJI_REACTION_UPDATE'; - export function fetchStatusRequest(id, skipLoading) { return { type: STATUS_FETCH_REQUEST, @@ -180,9 +178,9 @@ export function fetchContext(id) { return (dispatch, getState) => { dispatch(fetchContextRequest(id)); - api(getState).get(`/api/v1/statuses/${id}/context?with_reference=1`).then(response => { - dispatch(importFetchedStatuses(response.data.ancestors.concat(response.data.descendants).concat(response.data.references))); - dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants, response.data.references)); + api(getState).get(`/api/v1/statuses/${id}/context`).then(response => { + dispatch(importFetchedStatuses(response.data.ancestors.concat(response.data.descendants))); + dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants)); }).catch(error => { if (error.response && error.response.status === 404) { @@ -201,13 +199,12 @@ export function fetchContextRequest(id) { }; } -export function fetchContextSuccess(id, ancestors, descendants, references) { +export function fetchContextSuccess(id, ancestors, descendants) { return { type: CONTEXT_FETCH_SUCCESS, id, ancestors, descendants, - references, statuses: ancestors.concat(descendants), }; } @@ -351,8 +348,3 @@ export const undoStatusTranslation = (id, pollId) => ({ id, pollId, }); - -export const updateEmojiReaction = (emoji_reaction) => ({ - type: STATUS_EMOJI_REACTION_UPDATE, - emoji_reaction, -}); diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 6517e5ad20..9daeb3c60f 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -10,7 +10,7 @@ import { deleteAnnouncement, } from './announcements'; import { updateConversations } from './conversations'; -import { updateNotifications, expandNotifications, updateEmojiReactions } from './notifications'; +import { updateNotifications, expandNotifications } from './notifications'; import { updateStatus } from './statuses'; import { updateTimeline, @@ -22,7 +22,6 @@ import { fillPublicTimelineGaps, fillCommunityTimelineGaps, fillListTimelineGaps, - fillAntennaTimelineGaps, } from './timelines'; /** @@ -103,10 +102,6 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti // @ts-expect-error dispatch(updateNotifications(JSON.parse(data.payload), messages, locale)); break; - case 'emoji_reaction': - // @ts-expect-error - dispatch(updateEmojiReactions(JSON.parse(data.payload))); - break; case 'conversation': // @ts-expect-error dispatch(updateConversations(JSON.parse(data.payload))); @@ -186,10 +181,3 @@ export const connectDirectStream = () => */ export const connectListStream = listId => connectTimelineStream(`list:${listId}`, 'list', { list: listId }, { fillGaps: () => fillListTimelineGaps(listId) }); - -/** - * @param {string} antennaId - * @returns {function(): void} - */ -export const connectAntennaStream = antennaId => - connectTimelineStream(`antenna:${antennaId}`, 'antenna', { antenna: antennaId }, { fillGaps: () => fillAntennaTimelineGaps(antennaId) }); diff --git a/app/javascript/mastodon/actions/suggestions.js b/app/javascript/mastodon/actions/suggestions.js index 8eafe38b21..870a311024 100644 --- a/app/javascript/mastodon/actions/suggestions.js +++ b/app/javascript/mastodon/actions/suggestions.js @@ -54,5 +54,12 @@ export const dismissSuggestion = accountId => (dispatch, getState) => { id: accountId, }); - api(getState).delete(`/api/v1/suggestions/${accountId}`).catch(() => {}); + api(getState).delete(`/api/v1/suggestions/${accountId}`).then(() => { + dispatch(fetchSuggestionsRequest()); + + api(getState).get('/api/v2/suggestions').then(response => { + dispatch(importFetchedAccounts(response.data.map(x => x.account))); + dispatch(fetchSuggestionsSuccess(response.data)); + }).catch(error => dispatch(fetchSuggestionsFail(error))); + }).catch(() => {}); }; diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index 786eb7e4b3..08561c71f4 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -21,10 +21,6 @@ export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL'; -export const TIMELINE_INSERT = 'TIMELINE_INSERT'; - -export const TIMELINE_SUGGESTIONS = 'inline-follow-suggestions'; -export const TIMELINE_GAP = null; export const loadPending = timeline => ({ type: TIMELINE_LOAD_PENDING, @@ -116,19 +112,9 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems)); - if (timelineId === 'home' && !isLoadingMore && !isLoadingRecent) { - const now = new Date(); - const fittingIndex = response.data.findIndex(status => now - (new Date(status.created_at)) > 4 * 3600 * 1000); - - if (fittingIndex !== -1) { - dispatch(insertIntoTimeline(timelineId, TIMELINE_SUGGESTIONS, Math.max(1, fittingIndex))); - } - } - if (timelineId === 'home') { dispatch(submitMarkers()); } @@ -163,7 +149,6 @@ export const expandAccountTimeline = (accountId, { maxId, withReplies, t export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged }); export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 }); export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); -export const expandAntennaTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`antenna:${id}`, `/api/v1/timelines/antenna/${id}`, { max_id: maxId }, done); export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => { return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId, @@ -178,7 +163,6 @@ export const fillHomeTimelineGaps = (done = noOp) => fillTimelineGaps('home export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote } = {}, done = noOp) => fillTimelineGaps(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia }, done); export const fillCommunityTimelineGaps = ({ onlyMedia } = {}, done = noOp) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia }, done); export const fillListTimelineGaps = (id, done = noOp) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {}, done); -export const fillAntennaTimelineGaps = (id, done = noOp) => fillTimelineGaps(`antenna:${id}`, `/api/v1/timelines/antenna/${id}`, {}, done); export function expandTimelineRequest(timeline, isLoadingMore) { return { @@ -237,10 +221,3 @@ export const markAsPartial = timeline => ({ type: TIMELINE_MARK_AS_PARTIAL, timeline, }); - -export const insertIntoTimeline = (timeline, key, index) => ({ - type: TIMELINE_INSERT, - timeline, - index, - key, -}); diff --git a/app/javascript/mastodon/api.ts b/app/javascript/mastodon/api.ts index de597a3e3b..f262fd8570 100644 --- a/app/javascript/mastodon/api.ts +++ b/app/javascript/mastodon/api.ts @@ -29,14 +29,9 @@ const setCSRFHeader = () => { void ready(setCSRFHeader); -export const authorizationTokenFromState = (getState?: GetState) => { - return ( - getState && (getState().meta.get('access_token', '') as string | false) - ); -}; - const authorizationHeaderFromState = (getState?: GetState) => { - const accessToken = authorizationTokenFromState(getState); + const accessToken = + getState && (getState().meta.get('access_token', '') as string); if (!accessToken) { return {}; diff --git a/app/javascript/mastodon/api_types/accounts.ts b/app/javascript/mastodon/api_types/accounts.ts index 41694af4b4..5bf3e64288 100644 --- a/app/javascript/mastodon/api_types/accounts.ts +++ b/app/javascript/mastodon/api_types/accounts.ts @@ -12,32 +12,6 @@ export interface ApiAccountRoleJSON { name: string; } -export interface ApiAccountOtherSettingsJSON { - noindex: boolean; - hide_network: boolean; - hide_statuses_count: boolean; - hide_following_count: boolean; - hide_followers_count: boolean; - translatable_private: boolean; - link_preview: boolean; - allow_quote: boolean; - emoji_reaction_policy: - | 'allow' - | 'outside_only' - | 'following_only' - | 'followers_only' - | 'mutuals_only' - | 'block'; - subscription_policy: 'allow' | 'followers_only' | 'block'; -} - -export interface ApiServerFeaturesJSON { - circle: boolean; - emoji_reaction: boolean; - quote: boolean; - status_reference: boolean; -} - // See app/serializers/rest/account_serializer.rb export interface ApiAccountJSON { acct: string; @@ -48,7 +22,6 @@ export interface ApiAccountJSON { discoverable: boolean; indexable: boolean; display_name: string; - emoji_reaction_available_server: boolean; emojis: ApiCustomEmojiJSON[]; fields: ApiAccountFieldJSON[]; followers_count: number; @@ -61,10 +34,7 @@ export interface ApiAccountJSON { locked: boolean; noindex?: boolean; note: string; - other_settings: ApiAccountOtherSettingsJSON; roles?: ApiAccountJSON[]; - server_features: ApiServerFeaturesJSON; - subscribable: boolean; statuses_count: number; uri: string; url: string; diff --git a/app/javascript/mastodon/api_types/custom_emoji.ts b/app/javascript/mastodon/api_types/custom_emoji.ts index 9f25e6e410..05144d6f68 100644 --- a/app/javascript/mastodon/api_types/custom_emoji.ts +++ b/app/javascript/mastodon/api_types/custom_emoji.ts @@ -5,9 +5,4 @@ export interface ApiCustomEmojiJSON { url: string; category?: string; visible_in_picker: boolean; - width?: number; - height?: number; - sensitive?: boolean; - aliases?: string[]; - license?: string; } diff --git a/app/javascript/mastodon/api_types/markers.ts b/app/javascript/mastodon/api_types/markers.ts deleted file mode 100644 index f7664fd7c1..0000000000 --- a/app/javascript/mastodon/api_types/markers.ts +++ /dev/null @@ -1,7 +0,0 @@ -// See app/serializers/rest/account_serializer.rb - -export interface MarkerJSON { - last_read_id: string; - version: string; - updated_at: string; -} diff --git a/app/javascript/mastodon/api_types/media_attachments.ts b/app/javascript/mastodon/api_types/media_attachments.ts deleted file mode 100644 index fc027ccd2a..0000000000 --- a/app/javascript/mastodon/api_types/media_attachments.ts +++ /dev/null @@ -1,22 +0,0 @@ -// See app/serializers/rest/media_attachment_serializer.rb - -export type MediaAttachmentType = - | 'image' - | 'gifv' - | 'video' - | 'unknown' - | 'audio'; - -export interface ApiMediaAttachmentJSON { - id: string; - type: MediaAttachmentType; - url: string; - preview_url: string; - remoteUrl: string; - preview_remote_url: string; - text_url: string; - // TODO: how to define this? - meta: unknown; - description?: string; - blurhash: string; -} diff --git a/app/javascript/mastodon/api_types/polls.ts b/app/javascript/mastodon/api_types/polls.ts deleted file mode 100644 index 8181f7b813..0000000000 --- a/app/javascript/mastodon/api_types/polls.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { ApiCustomEmojiJSON } from './custom_emoji'; - -// See app/serializers/rest/poll_serializer.rb - -export interface ApiPollOptionJSON { - title: string; - votes_count: number; -} - -export interface ApiPollJSON { - id: string; - expires_at: string; - expired: boolean; - multiple: boolean; - votes_count: number; - voters_count: number; - - options: ApiPollOptionJSON[]; - emojis: ApiCustomEmojiJSON[]; - - voted: boolean; - own_votes: number[]; -} diff --git a/app/javascript/mastodon/api_types/statuses.ts b/app/javascript/mastodon/api_types/statuses.ts deleted file mode 100644 index 51dccd14d6..0000000000 --- a/app/javascript/mastodon/api_types/statuses.ts +++ /dev/null @@ -1,97 +0,0 @@ -// See app/serializers/rest/status_serializer.rb - -import type { ApiAccountJSON } from './accounts'; -import type { ApiCustomEmojiJSON } from './custom_emoji'; -import type { ApiMediaAttachmentJSON } from './media_attachments'; -import type { ApiPollJSON } from './polls'; - -// See app/modals/status.rb visibility+limited_scope -export type StatusVisibility = - | 'public' - | 'unlisted' - | 'private' - | 'direct' - | 'public_unlisted' - | 'login' - | 'mutual' - | 'circle' - | 'personal' - | 'reply' - | 'limited'; - -export interface ApiStatusApplicationJSON { - name: string; - website: string; -} - -export interface ApiTagJSON { - name: string; - url: string; -} - -export interface ApiMentionJSON { - id: string; - username: string; - url: string; - acct: string; -} - -export interface ApiPreviewCardJSON { - url: string; - title: string; - description: string; - language: string; - type: string; - author_name: string; - author_url: string; - provider_name: string; - provider_url: string; - html: string; - width: number; - height: number; - image: string; - image_description: string; - embed_url: string; - blurhash: string; - published_at: string; -} - -export interface ApiStatusJSON { - id: string; - created_at: string; - in_reply_to_id?: string; - in_reply_to_account_id?: string; - sensitive: boolean; - spoiler_text?: string; - visibility: StatusVisibility; - language: string; - uri: string; - url: string; - replies_count: number; - reblogs_count: number; - favorites_count: number; - edited_at?: string; - - favorited?: boolean; - reblogged?: boolean; - muted?: boolean; - bookmarked?: boolean; - pinned?: boolean; - - // filtered: FilterResult[] - filtered: unknown; // TODO - content?: string; - text?: string; - - reblog?: ApiStatusJSON; - application?: ApiStatusApplicationJSON; - account: ApiAccountJSON; - media_attachments: ApiMediaAttachmentJSON[]; - mentions: ApiMentionJSON[]; - - tags: ApiTagJSON[]; - emojis: ApiCustomEmojiJSON[]; - - card?: ApiPreviewCardJSON; - poll?: ApiPollJSON; -} diff --git a/app/javascript/mastodon/common.js b/app/javascript/mastodon/common.js index 511568aa0f..0ec8449343 100644 --- a/app/javascript/mastodon/common.js +++ b/app/javascript/mastodon/common.js @@ -2,7 +2,7 @@ import Rails from '@rails/ujs'; import 'font-awesome/css/font-awesome.css'; export function start() { - require.context('../images/', true, /\.(jpg|png|svg)$/); + require.context('../images/', true); try { Rails.start(); 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 dc955b7abe..1c37278483 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,11 +9,7 @@ exports[` renders emoji with custom url 1`] = ` className="emojione" src="http://example.com/emoji.png" /> -

- :foobar: -
+ :foobar: `; @@ -26,10 +22,6 @@ 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 980dc9e100..f82dd9153a 100644 --- a/app/javascript/mastodon/components/account.jsx +++ b/app/javascript/mastodon/components/account.jsx @@ -1,19 +1,17 @@ import PropTypes from 'prop-types'; -import { useCallback } from 'react'; -import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import { Link } from 'react-router-dom'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; -import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import { EmptyAccount } from 'mastodon/components/empty_account'; import { ShortNumber } from 'mastodon/components/short_number'; import { VerifiedBadge } from 'mastodon/components/verified_badge'; -import DropdownMenuContainer from '../containers/dropdown_menu_container'; import { me } from '../initial_state'; import { Avatar } from './avatar'; @@ -32,162 +30,151 @@ const messages = defineMessages({ unmute_notifications: { id: 'account.unmute_notifications_short', defaultMessage: 'Unmute notifications' }, mute: { id: 'account.mute_short', defaultMessage: 'Mute' }, block: { id: 'account.block_short', defaultMessage: 'Block' }, - more: { id: 'status.more', defaultMessage: 'More' }, }); -const Account = ({ size = 46, account, onFollow, onBlock, onMute, onMuteNotifications, hidden, hideButtons, minimal, defaultAction, children, withBio }) => { - const intl = useIntl(); +class Account extends ImmutablePureComponent { - const handleFollow = useCallback(() => { - onFollow(account); - }, [onFollow, account]); + static propTypes = { + size: PropTypes.number, + account: ImmutablePropTypes.record, + onFollow: PropTypes.func.isRequired, + onBlock: PropTypes.func.isRequired, + onMute: PropTypes.func.isRequired, + onMuteNotifications: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + hidden: PropTypes.bool, + minimal: PropTypes.bool, + defaultAction: PropTypes.string, + withBio: PropTypes.bool, + }; - const handleBlock = useCallback(() => { - onBlock(account); - }, [onBlock, account]); + static defaultProps = { + size: 46, + }; - const handleMute = useCallback(() => { - onMute(account); - }, [onMute, account]); + handleFollow = () => { + this.props.onFollow(this.props.account); + }; - const handleMuteNotifications = useCallback(() => { - onMuteNotifications(account, true); - }, [onMuteNotifications, account]); + handleBlock = () => { + this.props.onBlock(this.props.account); + }; - const handleUnmuteNotifications = useCallback(() => { - onMuteNotifications(account, false); - }, [onMuteNotifications, account]); + handleMute = () => { + this.props.onMute(this.props.account); + }; - if (!account) { - return ; - } + handleMuteNotifications = () => { + this.props.onMuteNotifications(this.props.account, true); + }; - if (hidden) { - return ( - <> - {account.get('display_name')} - {account.get('username')} - - ); - } + handleUnmuteNotifications = () => { + this.props.onMuteNotifications(this.props.account, false); + }; - let buttons; + render () { + const { account, intl, hidden, withBio, defaultAction, size, minimal } = this.props; - if (!hideButtons && account.get('id') !== me && account.get('relationship', null) !== null) { - const following = account.getIn(['relationship', 'following']); - const requested = account.getIn(['relationship', 'requested']); - const blocking = account.getIn(['relationship', 'blocking']); - const muting = account.getIn(['relationship', 'muting']); + if (!account) { + return ; + } - if (requested) { - buttons =