diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 000000000..54dd3aaf3 --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,7 @@ +[production] +defaults +not IE 11 +not dead + +[development] +supports es6-module diff --git a/.circleci/config.yml b/.circleci/config.yml index 4fcc8c618..2a60ae684 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,7 @@ version: 2.1 orbs: - ruby: circleci/ruby@1.4.0 + ruby: circleci/ruby@1.4.1 node: circleci/node@5.0.1 executors: @@ -133,6 +133,12 @@ jobs: - run: command: ./bin/rails tests:migrations:populate_v2_4 name: Populate database with test data + - run: + command: ./bin/rails db:migrate VERSION=20180707154237 + name: Run migrations up to v2.4.3 + - run: + command: ./bin/rails tests:migrations:populate_v2_4_3 + name: Populate database with test data - run: command: ./bin/rails db:migrate name: Run all remaining migrations @@ -167,14 +173,22 @@ jobs: - run: command: ./bin/rails tests:migrations:populate_v2_4 name: Populate database with test data + - run: + command: ./bin/rails db:migrate VERSION=20180707154237 + name: Run migrations up to v2.4.3 + environment: + SKIP_POST_DEPLOYMENT_MIGRATIONS: true + - run: + command: ./bin/rails tests:migrations:populate_v2_4_3 + name: Populate database with test data - run: command: ./bin/rails db:migrate - name: Run all pre-deployment migrations + name: Run all remaining pre-deployment migrations environment: SKIP_POST_DEPLOYMENT_MIGRATIONS: true - run: command: ./bin/rails db:migrate - name: Run all post-deployment remaining migrations + name: Run all post-deployment migrations - run: command: ./bin/rails tests:migrations:check_database name: Check migration result diff --git a/.codeclimate.yml b/.codeclimate.yml index ee9022cda..59051aae7 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -26,13 +26,11 @@ plugins: bundler-audit: enabled: true eslint: - enabled: true - channel: eslint-7 + enabled: false rubocop: - enabled: true - channel: rubocop-1-9-1 + enabled: false sass-lint: - enabled: true + enabled: false exclude_patterns: - spec/ - vendor/asset/ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 628efc8ec..9d9e54d4f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -11,7 +11,8 @@ "extensions": [ "EditorConfig.EditorConfig", "dbaeumer.vscode-eslint", - "rebornix.Ruby" + "rebornix.Ruby", + "webben.browserslist" ], // Use 'forwardPorts' to make a list of ports inside the container available locally. diff --git a/.eslintrc.js b/.eslintrc.js index 7dda01108..e4ada6fe0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -12,7 +12,7 @@ module.exports = { ATTACHMENT_HOST: false, }, - parser: 'babel-eslint', + parser: '@babel/eslint-parser', plugins: [ 'react', @@ -27,7 +27,7 @@ module.exports = { experimentalObjectRestSpread: true, jsx: true, }, - ecmaVersion: 2018, + ecmaVersion: 2021, }, settings: { @@ -79,6 +79,11 @@ module.exports = { 'no-irregular-whitespace': 'error', 'no-mixed-spaces-and-tabs': 'warn', 'no-nested-ternary': 'warn', + 'no-restricted-properties': [ + 'error', + { property: 'substring', message: 'Use .slice instead of .substring.' }, + { property: 'substr', message: 'Use .slice instead of .substr.' }, + ], 'no-trailing-spaces': 'warn', 'no-undef': 'error', 'no-unreachable': 'error', diff --git a/.github/stylelint-matcher.json b/.github/stylelint-matcher.json new file mode 100644 index 000000000..cdfd4086b --- /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-image.yml b/.github/workflows/build-image.yml index 880fdfac9..63aefc37e 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -14,26 +14,26 @@ jobs: build-image: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: docker/setup-qemu-action@v1 - - uses: docker/setup-buildx-action@v1 - - uses: docker/login-action@v1 + - uses: actions/checkout@v3 + - uses: docker/setup-qemu-action@v2 + - uses: docker/setup-buildx-action@v2 + - uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} if: github.event_name != 'pull_request' - - uses: docker/metadata-action@v3 + - uses: docker/metadata-action@v4 id: meta with: images: ghcr.io/${{ github.repository_owner }}/mastodon flavor: | - latest=true + latest=auto tags: | type=edge,branch=main type=match,pattern=v(.*),group=0 type=ref,event=pr - - uses: docker/build-push-action@v2 + - uses: docker/build-push-action@v3 with: context: . platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/check-i18n.yml b/.github/workflows/check-i18n.yml index be38a096d..1c60515f8 100644 --- a/.github/workflows/check-i18n.yml +++ b/.github/workflows/check-i18n.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install system dependencies run: | sudo apt-get update diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 000000000..f77a9720e --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,83 @@ +--- +################################# +################################# +## Super Linter GitHub Actions ## +################################# +################################# +name: Lint Code Base + +# +# Documentation: +# https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions +# + +############################# +# Start the job on all push # +############################# +on: + push: + branches-ignore: [main] + # Remove the line above to run when pushing to master + pull_request: + branches: [main] + +############### +# Set the Job # +############### +permissions: + checks: write + contents: read + pull-requests: write + statuses: write + +jobs: + build: + # Name the Job + name: Lint Code Base + # Set the agent to run on + runs-on: ubuntu-latest + + ################## + # Load all steps # + ################## + steps: + ########################## + # Checkout the code base # + ########################## + - name: Checkout Code + uses: actions/checkout@v3 + with: + # Full git history is needed to get a proper list of changed files within `super-linter` + fetch-depth: 0 + + - name: Set-up Node.js + uses: actions/setup-node@v3 + with: + node-version: 16.x + cache: yarn + - name: Intall dependencies + run: yarn install --frozen-lockfile + - name: Set-up RuboCop Problem Mathcher + uses: r7kamura/rubocop-problem-matchers-action@v1 + - name: Set-up Stylelint Problem Matcher + uses: xt0rted/stylelint-problem-matcher@v1 + # https://github.com/xt0rted/stylelint-problem-matcher/issues/360 + - run: echo "::add-matcher::.github/stylelint-matcher.json" + + ################################ + # Run Linter against code base # + ################################ + - name: Lint Code Base + uses: github/super-linter@v4 + env: + CSS_FILE_NAME: stylelint.config.js + DEFAULT_BRANCH: main + NO_COLOR: 1 # https://github.com/xt0rted/stylelint-problem-matcher/issues/360 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JAVASCRIPT_ES_CONFIG_FILE: .eslintrc.js + LINTER_RULES_PATH: . + RUBY_CONFIG_FILE: .rubocop.yml + VALIDATE_ALL_CODEBASE: false + VALIDATE_CSS: true + VALIDATE_JAVASCRIPT_ES: true + VALIDATE_RUBY: true diff --git a/.rubocop.yml b/.rubocop.yml index a76937426..9e3ff21f2 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -281,6 +281,9 @@ Style/RedundantRegexpEscape: Style/RedundantReturn: Enabled: true +Style/RedundantBegin: + Enabled: false + Style/RegexpLiteral: Enabled: false diff --git a/.sass-lint.yml b/.sass-lint.yml deleted file mode 100644 index a84adff3f..000000000 --- a/.sass-lint.yml +++ /dev/null @@ -1,37 +0,0 @@ -# Linter Documentation: -# https://github.com/sasstools/sass-lint/tree/v1.13.1/docs/options - -files: - include: app/javascript/styles/**/*.scss - ignore: - - app/javascript/styles/mastodon/reset.scss - -rules: - # Disallows - no-color-literals: 0 - no-css-comments: 0 - no-duplicate-properties: 0 - no-ids: 0 - no-important: 0 - no-mergeable-selectors: 0 - no-misspelled-properties: 0 - no-qualifying-elements: 0 - no-transition-all: 0 - no-vendor-prefixes: 0 - - # Nesting - force-element-nesting: 0 - force-attribute-nesting: 0 - force-pseudo-nesting: 0 - - # Name Formats - class-name-format: 0 - leading-zero: 0 - - # Style Guide - attribute-quotes: 0 - hex-length: 0 - indentation: 0 - nesting-depth: 0 - property-sort-order: 0 - quotes: 0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 52a62a213..ed4cdd881 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,149 @@ Changelog All notable changes to this project will be documented in this file. -## Unreleased +## [3.5.3] - 2022-05-26 +### Added + +- **Add language dropdown to compose form in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/18420), [ykzts](https://github.com/mastodon/mastodon/pull/18460)) +- **Add warning for limited accounts in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/18344)) +- Add `limited` attribute to accounts in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/18344)) + +### Changed + +- **Change RSS feeds** ([Gargron](https://github.com/mastodon/mastodon/pull/18356), [tribela](https://github.com/mastodon/mastodon/pull/18406)) + - Titles are now date and time of post + - Bodies now render all content faithfully, including polls and emojis + - All media attachments are included with Media RSS +- Change "dangerous" to "sensitive" in privacy policy and web UI ([Gargron](https://github.com/mastodon/mastodon/pull/18515)) +- Change unconfirmed accounts to not be visible in REST API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17530)) +- Change `tootctl search deploy` to improve performance ([Gargron](https://github.com/mastodon/mastodon/pull/18463), [Gargron](https://github.com/mastodon/mastodon/pull/18514)) +- Change search indexing to use batches to minimize resource usage ([Gargron](https://github.com/mastodon/mastodon/pull/18451)) + +### Fixed + +- Fix follower and other counters being able to go negative ([Gargron](https://github.com/mastodon/mastodon/pull/18517)) +- Fix unnecessary query on when creating a status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17901)) +- Fix warning an account outside of a report closing all reports for that account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18387)) +- Fix error when resolving a link that redirects to a local post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18314)) +- Fix preferred posting language returning unusable value in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/18428)) +- Fix race condition error when external status is reblogged ([ykzts](https://github.com/mastodon/mastodon/pull/18424)) +- Fix missing string for appeal validation error ([Gargron](https://github.com/mastodon/mastodon/pull/18410)) +- Fix block/mute lists showing a follow button in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18364)) +- Fix Redis configuration not being changed by `mastodon:setup` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18383)) +- Fix streaming notifications not using quick filter logic in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18316)) +- Fix ambiguous wording on appeal actions in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18328)) +- Fix floating action button obscuring last element in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18332)) +- Fix account warnings not being recorded in audit log ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18338)) +- Fix leftover icons for direct visibility statuses ([Steffo99](https://github.com/mastodon/mastodon/pull/18305)) +- Fix link verification requiring case sensitivity on links ([sgolemon](https://github.com/mastodon/mastodon/pull/18320)) +- Fix embeds not setting their height correctly ([rinsuki](https://github.com/mastodon/mastodon/pull/18301)) + +### Security + +- Fix concurrent unfollowing decrementing follower count more than once ([Gargron](https://github.com/mastodon/mastodon/pull/18527)) +- Fix being able to appeal a strike unlimited times ([Gargron](https://github.com/mastodon/mastodon/pull/18529)) +- Fix being able to report otherwise inaccessible statuses ([Gargron](https://github.com/mastodon/mastodon/pull/18528)) +- Fix empty votes arbitrarily increasing voters count in polls ([Gargron](https://github.com/mastodon/mastodon/pull/18526)) +- Fix moderator identity leak when approving appeal of sensitive marked statuses ([Gargron](https://github.com/mastodon/mastodon/pull/18525)) +- Fix suspended users being able to access APIs that don't require a user ([Gargron](https://github.com/mastodon/mastodon/pull/18524)) +- Fix confirmation redirect to app without `Location` header ([Gargron](https://github.com/mastodon/mastodon/pull/18523)) + +## [3.5.2] - 2022-05-04 +### Added + +- Add warning on direct messages screen in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/18289)) + - We already had a warning when composing a direct message, it has now been reworded to be more clear + - Same warning is now displayed when viewing sent and received direct messages +- Add ability to set approval-based registration through tootctl ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18248)) +- Add pre-filling of domain from search filter in domain allow/block admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18172)) + +## Changed + +- Change name of “Direct” visibility to “Mentioned people only” in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/18146), [Gargron](https://github.com/mastodon/mastodon/pull/18289), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18291)) +- Change trending posts to only show one post from each account ([Gargron](https://github.com/mastodon/mastodon/pull/18181)) +- Change half-life of trending posts from 6 hours to 2 hours ([Gargron](https://github.com/mastodon/mastodon/pull/18182)) +- Change full-text search feature to also include polls you have voted in ([tribela](https://github.com/mastodon/mastodon/pull/18070)) +- Change Redis from using one connection per process, to using a connection pool ([Gargron](https://github.com/mastodon/mastodon/pull/18135), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18160), [Gargron](https://github.com/mastodon/mastodon/pull/18171)) + - Different threads no longer have to wait on a mutex over a single connection + - However, this does increase the number of Redis connections by a fair amount + - We are planning to optimize Redis use so that the pool can be made smaller in the future + +## Removed + +- Remove IP matching from e-mail domain blocks ([Gargron](https://github.com/mastodon/mastodon/pull/18190)) + - The IPs of the blocked e-mail domain or its MX records are no longer checked + - Previously it was too easy to block e-mail providers by mistake + +## Fixed + +- Fix compatibility with Friendica's pinned posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18254), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18260)) +- Fix error when looking up handle with surrounding spaces in REST API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18225)) +- Fix double render error when authorizing interaction ([Gargron](https://github.com/mastodon/mastodon/pull/18203)) +- Fix error when a post references an invalid media attachment ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18211)) +- Fix error when trying to revoke OAuth token without supplying a token ([Gargron](https://github.com/mastodon/mastodon/pull/18205)) +- Fix error caused by missing subject in Webfinger response ([Gargron](https://github.com/mastodon/mastodon/pull/18204)) +- Fix error on attempting to delete an account moderation note ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18196)) +- Fix light-mode emoji borders in web UI ([Gaelan](https://github.com/mastodon/mastodon/pull/18131)) +- Fix being able to scroll away from the loading bar in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/18170)) +- Fix error when a bookmark or favorite has been reported and deleted ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18174)) +- Fix being offered empty “Server rules violation” report option in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18165)) +- Fix temporary network errors preventing from authorizing interactions with remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18161)) +- Fix incorrect link in "new trending tags" email ([cdzombak](https://github.com/mastodon/mastodon/pull/18156)) +- Fix missing indexes on some foreign keys ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18157)) +- Fix n+1 query on feed merge and populate operations ([Gargron](https://github.com/mastodon/mastodon/pull/18111)) +- Fix feed unmerge worker being exceptionally slow in some conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18110)) +- Fix PeerTube videos appearing with an erroneous “Edited at” marker ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18100)) +- Fix instance actor being created incorrectly when running through migrations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18109)) +- Fix web push notifications containing HTML entities ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18071)) +- Fix inconsistent parsing of `TRUSTED_PROXY_IP` ([ykzts](https://github.com/mastodon/mastodon/pull/18051)) +- Fix error when fetching pinned posts ([tribela](https://github.com/mastodon/mastodon/pull/18030)) +- Fix wrong optimization in feed populate operation ([dogelover911](https://github.com/mastodon/mastodon/pull/18009)) +- Fix error in alias settings page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18004)) + +## [3.5.1] - 2022-04-08 +### Added + +- Add pagination for trending statuses in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/17976)) + +### Changed + +- Change e-mail notifications to only be sent when recipient is offline ([Gargron](https://github.com/mastodon/mastodon/pull/17984)) + - Send e-mails for mentions and follows by default again + - But only when recipient does not have push notifications through an app +- Change `website` attribute to be nullable on `Application` entity in REST API ([rinsuki](https://github.com/mastodon/mastodon/pull/17962)) + +### Removed + +- Remove sign-in token authentication, instead send e-mail about new sign-in ([Gargron](https://github.com/mastodon/mastodon/pull/17970)) + - You no longer need to enter a security code sent through e-mail + - Instead you get an e-mail about a new sign-in from an unfamiliar IP address + +### Fixed + +- Fix error resposes for `from` search prefix ([single-right-quote](https://github.com/mastodon/mastodon/pull/17963)) +- Fix dangling language-specific trends ([Gargron](https://github.com/mastodon/mastodon/pull/17997)) +- Fix extremely rare race condition when deleting a status or account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17994)) +- Fix trends returning less results per page when filtered in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17996)) +- Fix pagination header on empty trends responses in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17986)) +- Fix cookies secure flag being set when served over Tor ([Gargron](https://github.com/mastodon/mastodon/pull/17992)) +- Fix migration error handling ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17991)) +- Fix error when re-running some migrations if they get interrupted at the wrong moment ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17989)) +- Fix potentially missing statuses when reconnecting to streaming API in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17981), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17987), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17980)) +- Fix error when sending warning emails with custom text ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17983)) +- Fix unset `SMTP_RETURN_PATH` environment variable causing e-mail not to send ([Gargron](https://github.com/mastodon/mastodon/pull/17982)) +- Fix possible duplicate statuses in timelines in some edge cases in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17971)) +- Fix spurious edits and require incoming edits to be explicitly marked as such ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17918)) +- Fix error when encountering invalid pinned statuses ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17964)) +- Fix inconsistency in error handling when removing a status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17974)) +- Fix admin API unconditionally requiring CSRF token ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17975)) +- Fix trending tags endpoint missing `offset` param in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17973)) +- Fix unusual number formatting in some locales ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17929)) +- Fix `S3_FORCE_SINGLE_REQUEST` environment variable not working ([HolgerHuo](https://github.com/mastodon/mastodon/pull/17922)) +- Fix failure to build assets with OpenSSL 3 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17930)) +- Fix PWA manifest using outdated routes ([HolgerHuo](https://github.com/mastodon/mastodon/pull/17921)) +- Fix error when indexing statuses into Elasticsearch ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17912)) + +## [3.5.0] - 2022-03-30 ### Added - **Add support for incoming edited posts** ([Gargron](https://github.com/mastodon/mastodon/pull/16697), [Gargron](https://github.com/mastodon/mastodon/pull/17727), [Gargron](https://github.com/mastodon/mastodon/pull/17728), [Gargron](https://github.com/mastodon/mastodon/pull/17320), [Gargron](https://github.com/mastodon/mastodon/pull/17404), [Gargron](https://github.com/mastodon/mastodon/pull/17390), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17335), [Gargron](https://github.com/mastodon/mastodon/pull/17696), [Gargron](https://github.com/mastodon/mastodon/pull/17745), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17740), [Gargron](https://github.com/mastodon/mastodon/pull/17697), [Gargron](https://github.com/mastodon/mastodon/pull/17648), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17531), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17499), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17498), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17380), [Gargron](https://github.com/mastodon/mastodon/pull/17373), [Gargron](https://github.com/mastodon/mastodon/pull/17334), [Gargron](https://github.com/mastodon/mastodon/pull/17333), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17699), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17748)) @@ -81,6 +223,7 @@ All notable changes to this project will be documented in this file. - Add lazy loading for emoji picker in web UI ([mashirozx](https://github.com/mastodon/mastodon/pull/16907), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17011)) - Add single option votes tooltip in polls in web UI ([Brawaru](https://github.com/mastodon/mastodon/pull/16849)) - Add confirmation modal when closing media edit modal with unsaved changes in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16518)) +- Add hint about missing media attachment description in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/17845)) - Add support for fetching Create and Announce activities by URI in ActivityPub ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16383)) - Add `S3_FORCE_SINGLE_REQUEST` environment variable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16866)) - Add `OMNIAUTH_ONLY` environment variable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17288), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17345)) @@ -130,6 +273,11 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fix IDN domains not being rendered correctly in a few left-over places ([Gargron](https://github.com/mastodon/mastodon/pull/17848)) +- Fix Sanskrit translation not being used in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17820)) +- Fix Kurdish languages having the wrong language codes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17812)) +- Fix pghero making database schema suggestions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17807)) +- Fix encoding glitch in the OpenGraph description of a profile page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17821)) - Fix web manifest not permitting PWA usage from alternate domains ([HolgerHuo](https://github.com/mastodon/mastodon/pull/16714)) - Fix not being able to edit media attachments for scheduled posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17690)) - Fix subscribed relay activities being recorded as boosts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17571)) @@ -191,6 +339,11 @@ All notable changes to this project will be documented in this file. - Fix hashtag autocomplete overriding user-typed case ([weex](https://github.com/mastodon/mastodon/pull/16460)) - Fix WebAuthn authentication setup to not prompt for PIN ([truongnmt](https://github.com/mastodon/mastodon/pull/16545)) +### Security + +- Fix being able to post URLs longer than 4096 characters ([Gargron](https://github.com/mastodon/mastodon/pull/17908)) +- Fix being able to bypass e-mail restrictions ([Gargron](https://github.com/mastodon/mastodon/pull/17909)) + ## [3.4.6] - 2022-02-03 ### Fixed diff --git a/Dockerfile b/Dockerfile index 2073cbebf..6180e0796 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ SHELL ["/bin/bash", "-c"] RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections # Install Node v16 (LTS) -ENV NODE_VER="16.14.2" +ENV NODE_VER="16.15.1" RUN ARCH= && \ dpkgArch="$(dpkg --print-architecture)" && \ case "${dpkgArch##*-}" in \ diff --git a/Gemfile b/Gemfile index 8bb361f49..9a638cd06 100644 --- a/Gemfile +++ b/Gemfile @@ -1,24 +1,24 @@ # frozen_string_literal: true source 'https://rubygems.org' -ruby '>= 2.5.0', '< 3.1.0' +ruby '>= 2.6.0', '< 3.1.0' gem 'pkg-config', '~> 1.4' gem 'rexml', '~> 3.2' gem 'puma', '~> 5.6' -gem 'rails', '~> 6.1.5' +gem 'rails', '~> 6.1.6' gem 'sprockets', '~> 3.7.2' gem 'thor', '~> 1.2' gem 'rack', '~> 2.2.3' gem 'hamlit-rails', '~> 0.2' -gem 'pg', '~> 1.3' +gem 'pg', '~> 1.4' gem 'makara', '~> 0.5' gem 'pghero', '~> 2.8' gem 'dotenv-rails', '~> 2.7' -gem 'aws-sdk-s3', '~> 1.113', require: false +gem 'aws-sdk-s3', '~> 1.114', require: false gem 'fog-core', '<= 2.1.0' gem 'fog-openstack', '~> 0.3', require: false gem 'kt-paperclip', '~> 7.1' @@ -26,7 +26,7 @@ gem 'blurhash', '~> 0.1' gem 'active_model_serializers', '~> 0.10' gem 'addressable', '~> 2.8' -gem 'bootsnap', '~> 1.10.3', require: false +gem 'bootsnap', '~> 1.12.0', require: false gem 'browser' gem 'charlock_holmes', '~> 0.7.7' gem 'chewy', '~> 7.2' @@ -40,7 +40,7 @@ end gem 'net-ldap', '~> 0.17' gem 'omniauth-cas', '~> 2.0' gem 'omniauth-saml', '~> 1.10' -gem 'gitlab-omniauth-openid-connect', '~>0.5.0', require: 'omniauth_openid_connect' +gem 'gitlab-omniauth-openid-connect', '~>0.9.1', require: 'omniauth_openid_connect' gem 'omniauth', '~> 1.9' gem 'omniauth-rails_csrf_protection', '~> 0.1' @@ -53,7 +53,7 @@ gem 'fastimage' gem 'hiredis', '~> 0.6' gem 'redis-namespace', '~> 1.8' gem 'htmlentities', '~> 4.3' -gem 'http', '~> 5.0' +gem 'http', '~> 5.1' gem 'http_accept_language', '~> 2.1' gem 'httplog', '~> 1.5.0' gem 'idn-ruby', require: 'idn' @@ -79,13 +79,13 @@ gem 'ruby-progressbar', '~> 1.11' gem 'sanitize', '~> 6.0' gem 'scenic', '~> 1.6' gem 'sidekiq', '~> 6.4' -gem 'sidekiq-scheduler', '~> 3.1' +gem 'sidekiq-scheduler', '~> 4.0' gem 'sidekiq-unique-jobs', '~> 7.1' -gem 'sidekiq-bulk', '~>0.2.0' -gem 'simple-navigation', '~> 4.3' +gem 'sidekiq-bulk', '~> 0.2.0' +gem 'simple-navigation', '~> 4.4' gem 'simple_form', '~> 5.1' gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie' -gem 'stoplight', '~> 2.2.1' +gem 'stoplight', '~> 3.0.0' gem 'strong_migrations', '~> 0.7' gem 'tty-prompt', '~> 0.23', require: false gem 'twitter-text', '~> 3.1.0' @@ -101,9 +101,9 @@ gem 'rdf-normalize', '~> 0.5' gem 'redcarpet', '~> 3.5' group :development, :test do - gem 'fabrication', '~> 2.27' + gem 'fabrication', '~> 2.29' gem 'fuubar', '~> 2.5' - gem 'i18n-tasks', '~> 0.9', require: false + gem 'i18n-tasks', '~> 1.0', require: false gem 'pry-byebug', '~> 3.9' gem 'pry-rails', '~> 0.3' gem 'rspec-rails', '~> 5.1' @@ -114,10 +114,10 @@ group :production, :test do end group :test do - gem 'capybara', '~> 3.36' + gem 'capybara', '~> 3.37' gem 'climate_control', '~> 0.2' - gem 'faker', '~> 2.20' - gem 'microformats', '~> 4.2' + gem 'faker', '~> 2.21' + gem 'microformats', '~> 4.4' gem 'rails-controller-testing', '~> 1.0' gem 'rspec-sidekiq', '~> 3.1' gem 'simplecov', '~> 0.21', require: false @@ -134,12 +134,12 @@ group :development do gem 'letter_opener', '~> 1.8' gem 'letter_opener_web', '~> 2.0' gem 'memory_profiler' - gem 'rubocop', '~> 1.26', require: false - gem 'rubocop-rails', '~> 2.14', require: false + gem 'rubocop', '~> 1.30', require: false + gem 'rubocop-rails', '~> 2.15', require: false gem 'brakeman', '~> 5.2', require: false gem 'bundler-audit', '~> 0.9', require: false - gem 'capistrano', '~> 3.16' + gem 'capistrano', '~> 3.17' gem 'capistrano-rails', '~> 1.6' gem 'capistrano-rbenv', '~> 2.2' gem 'capistrano-yarn', '~> 2.0' @@ -148,7 +148,7 @@ group :development do end group :production do - gem 'lograge', '~> 0.11' + gem 'lograge', '~> 0.12' end gem 'concurrent-ruby', require: false @@ -157,3 +157,4 @@ gem 'connection_pool', require: false gem 'xorcist', '~> 1.1' gem 'hcaptcha', '~> 7.1' +gem 'cocoon', '~> 1.2' diff --git a/Gemfile.lock b/Gemfile.lock index 88ac007d5..6599bb9ce 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,40 +1,40 @@ GEM remote: https://rubygems.org/ specs: - actioncable (6.1.5) - actionpack (= 6.1.5) - activesupport (= 6.1.5) + actioncable (6.1.6) + actionpack (= 6.1.6) + activesupport (= 6.1.6) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.5) - actionpack (= 6.1.5) - activejob (= 6.1.5) - activerecord (= 6.1.5) - activestorage (= 6.1.5) - activesupport (= 6.1.5) + actionmailbox (6.1.6) + actionpack (= 6.1.6) + activejob (= 6.1.6) + activerecord (= 6.1.6) + activestorage (= 6.1.6) + activesupport (= 6.1.6) mail (>= 2.7.1) - actionmailer (6.1.5) - actionpack (= 6.1.5) - actionview (= 6.1.5) - activejob (= 6.1.5) - activesupport (= 6.1.5) + actionmailer (6.1.6) + actionpack (= 6.1.6) + actionview (= 6.1.6) + activejob (= 6.1.6) + activesupport (= 6.1.6) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.5) - actionview (= 6.1.5) - activesupport (= 6.1.5) + actionpack (6.1.6) + actionview (= 6.1.6) + activesupport (= 6.1.6) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.5) - actionpack (= 6.1.5) - activerecord (= 6.1.5) - activestorage (= 6.1.5) - activesupport (= 6.1.5) + actiontext (6.1.6) + actionpack (= 6.1.6) + activerecord (= 6.1.6) + activestorage (= 6.1.6) + activesupport (= 6.1.6) nokogiri (>= 1.8.5) - actionview (6.1.5) - activesupport (= 6.1.5) + actionview (6.1.6) + activesupport (= 6.1.6) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -45,22 +45,22 @@ GEM case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) active_record_query_trace (1.8) - activejob (6.1.5) - activesupport (= 6.1.5) + activejob (6.1.6) + activesupport (= 6.1.6) globalid (>= 0.3.6) - activemodel (6.1.5) - activesupport (= 6.1.5) - activerecord (6.1.5) - activemodel (= 6.1.5) - activesupport (= 6.1.5) - activestorage (6.1.5) - actionpack (= 6.1.5) - activejob (= 6.1.5) - activerecord (= 6.1.5) - activesupport (= 6.1.5) + activemodel (6.1.6) + activesupport (= 6.1.6) + activerecord (6.1.6) + activemodel (= 6.1.6) + activesupport (= 6.1.6) + activestorage (6.1.6) + actionpack (= 6.1.6) + activejob (= 6.1.6) + activerecord (= 6.1.6) + activesupport (= 6.1.6) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.5) + activesupport (6.1.6) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -81,47 +81,55 @@ GEM attr_required (1.0.1) awrence (1.1.1) aws-eventstream (1.2.0) - aws-partitions (1.558.0) - aws-sdk-core (3.127.0) + aws-partitions (1.587.0) + aws-sdk-core (3.130.2) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.55.0) + aws-sdk-kms (1.56.0) aws-sdk-core (~> 3, >= 3.127.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.113.0) + aws-sdk-s3 (1.114.0) aws-sdk-core (~> 3, >= 3.127.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) - aws-sigv4 (1.4.0) + aws-sigv4 (1.5.0) aws-eventstream (~> 1, >= 1.0.2) - bcrypt (3.1.16) + bcrypt (3.1.17) better_errors (2.9.1) coderay (>= 1.0.0) erubi (>= 1.0.0) rack (>= 0.9.0) + better_html (1.0.16) + actionview (>= 4.0) + activesupport (>= 4.0) + ast (~> 2.0) + erubi (~> 1.4) + html_tokenizer (~> 0.0.6) + parser (>= 2.4) + smart_properties bindata (2.4.10) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) blurhash (0.1.6) ffi (~> 1.14) - bootsnap (1.10.3) + bootsnap (1.12.0) msgpack (~> 1.2) - brakeman (5.2.1) + brakeman (5.2.3) browser (4.2.0) brpoplpush-redis_script (0.1.2) concurrent-ruby (~> 1.0, >= 1.0.5) redis (>= 1.0, <= 5.0) builder (3.2.4) - bullet (7.0.1) + bullet (7.0.2) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) - bundler-audit (0.9.0.1) + bundler-audit (0.9.1) bundler (>= 1.2.0, < 3) thor (~> 1.0) byebug (11.1.3) - capistrano (3.16.0) + capistrano (3.17.0) airbrussh (>= 1.0.0) i18n rake (>= 10.0.0) @@ -136,7 +144,7 @@ GEM sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (3.36.0) + capybara (3.37.1) addressable matrix mini_mime (>= 0.1.3) @@ -155,9 +163,10 @@ GEM elasticsearch-dsl chunky_png (1.4.0) climate_control (0.2.0) + cocoon (1.2.15) coderay (1.1.3) color_diff (0.1) - concurrent-ruby (1.1.9) + concurrent-ruby (1.1.10) connection_pool (2.2.5) cose (1.0.0) cbor (~> 0.5.9) @@ -174,11 +183,11 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-two-factor (4.0.1) - activesupport (< 6.2) + devise-two-factor (4.0.2) + activesupport (< 7.1) attr_encrypted (>= 1.3, < 4, != 2) devise (~> 4.0) - railties (< 6.2) + railties (< 7.1) rotp (~> 6.0) devise_pam_authenticatable2 (9.2.0) devise (>= 4.0.0) @@ -195,7 +204,6 @@ GEM dotenv-rails (2.7.6) dotenv (= 2.7.6) railties (>= 3.2) - e2mmap (0.1.0) ed25519 (1.3.0) elasticsearch (7.13.3) elasticsearch-api (= 7.13.3) @@ -208,11 +216,11 @@ GEM multi_json encryptor (3.0.0) erubi (1.10.0) - et-orbi (1.2.6) + et-orbi (1.2.7) tzinfo excon (0.76.0) - fabrication (2.27.0) - faker (2.20.0) + fabrication (2.29.0) + faker (2.21.0) i18n (>= 1.8.11, < 2) faraday (1.9.3) faraday-em_http (~> 1.0) @@ -256,13 +264,13 @@ GEM fog-json (>= 1.0) ipaddress (>= 0.8) formatador (0.2.5) - fugit (1.5.2) - et-orbi (~> 1.1, >= 1.1.8) + fugit (1.5.3) + et-orbi (~> 1, >= 1.2.7) raabro (~> 1.4) fuubar (2.5.1) rspec-core (~> 3.0) ruby-progressbar (~> 1.4) - gitlab-omniauth-openid-connect (0.5.0) + gitlab-omniauth-openid-connect (0.9.1) addressable (~> 2.7) omniauth (~> 1.9) openid_connect (~> 1.2) @@ -278,19 +286,20 @@ GEM hamlit (>= 1.2.0) railties (>= 4.0.1) hashdiff (1.0.1) - hashie (4.1.0) + hashie (5.0.0) hcaptcha (7.1.0) json highline (2.0.3) hiredis (0.6.3) hkdf (0.3.0) + html_tokenizer (0.0.7) htmlentities (4.3.4) - http (5.0.4) + http (5.1.0) addressable (~> 2.8) http-cookie (~> 1.0) http-form_data (~> 2.2) llhttp-ffi (~> 0.4.0) - http-cookie (1.0.4) + http-cookie (1.0.5) domain_name (~> 0.5) http-form_data (2.3.0) http_accept_language (2.1.1) @@ -300,9 +309,10 @@ GEM rainbow (>= 2.0.0) i18n (1.10.0) concurrent-ruby (~> 1.0) - i18n-tasks (0.9.37) + i18n-tasks (1.0.11) activesupport (>= 4.0.2) ast (>= 2.1.0) + better_html (~> 1.0) erubi highline (>= 2.0.0) i18n @@ -312,8 +322,8 @@ GEM terminal-table (>= 1.5.1) idn-ruby (0.1.4) ipaddress (0.8.3) - jmespath (1.6.0) - json (2.5.1) + jmespath (1.6.1) + json (2.6.2) json-canonicalization (0.3.0) json-jwt (1.13.0) activesupport (>= 4.2) @@ -362,12 +372,12 @@ GEM llhttp-ffi (0.4.0) ffi-compiler (~> 1.0) rake (~> 13.0) - lograge (0.11.2) + lograge (0.12.0) actionpack (>= 4) activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.15.0) + loofah (2.18.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) @@ -380,7 +390,7 @@ GEM matrix (0.4.2) memory_profiler (1.0.0) method_source (1.0.0) - microformats (4.3.1) + microformats (4.4.1) json (~> 2.2) nokogiri (~> 1.10) mime-types (3.4.1) @@ -388,16 +398,16 @@ GEM mime-types-data (3.2022.0105) mini_mime (1.1.2) mini_portile2 (2.8.0) - minitest (5.15.0) - msgpack (1.4.4) + minitest (5.16.0) + msgpack (1.5.2) multi_json (1.15.0) multipart-post (2.1.1) - net-ldap (0.17.0) + net-ldap (0.17.1) net-scp (3.0.0) net-ssh (>= 2.6.5, < 7.0.0) net-ssh (6.1.0) nio4r (2.5.8) - nokogiri (1.13.3) + nokogiri (1.13.6) mini_portile2 (~> 2.8.0) racc (~> 1.4) nsa (0.2.8) @@ -405,7 +415,7 @@ GEM concurrent-ruby (~> 1.0, >= 1.0.2) sidekiq (>= 3.5) statsd-ruby (~> 1.4, >= 1.4.0) - oj (3.13.11) + oj (3.13.14) omniauth (1.9.1) hashie (>= 3.4.6) rack (>= 1.6.2, < 3) @@ -419,7 +429,7 @@ GEM omniauth-saml (1.10.3) omniauth (~> 1.3, >= 1.3.2) ruby-saml (~> 1.9) - openid_connect (1.2.0) + openid_connect (1.3.0) activemodel attr_required (>= 1.0.0) json-jwt (>= 1.5.0) @@ -432,15 +442,15 @@ GEM openssl (2.2.0) openssl-signature_algorithm (0.4.0) orm_adapter (0.5.0) - ox (2.14.10) - parallel (1.22.0) - parser (3.1.1.0) + ox (2.14.11) + parallel (1.22.1) + parser (3.1.2.0) ast (~> 2.4.1) parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) - pg (1.3.4) - pghero (2.8.2) + pg (1.4.1) + pghero (2.8.3) activerecord (>= 5) pkg-config (1.4.7) posix-spawn (0.3.15) @@ -460,19 +470,19 @@ GEM pry (~> 0.13.0) pry-rails (0.3.9) pry (>= 0.10.4) - public_suffix (4.0.6) - puma (5.6.2) + public_suffix (4.0.7) + puma (5.6.4) nio4r (~> 2.0) pundit (2.2.0) activesupport (>= 3.0.0) raabro (1.4.0) racc (1.6.0) - rack (2.2.3) - rack-attack (6.6.0) + rack (2.2.3.1) + rack-attack (6.6.1) rack (>= 1.0, < 3) rack-cors (1.1.1) rack (>= 2.0.0) - rack-oauth2 (1.16.0) + rack-oauth2 (1.19.0) activesupport attr_required httpclient @@ -482,20 +492,20 @@ GEM rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (6.1.5) - actioncable (= 6.1.5) - actionmailbox (= 6.1.5) - actionmailer (= 6.1.5) - actionpack (= 6.1.5) - actiontext (= 6.1.5) - actionview (= 6.1.5) - activejob (= 6.1.5) - activemodel (= 6.1.5) - activerecord (= 6.1.5) - activestorage (= 6.1.5) - activesupport (= 6.1.5) + rails (6.1.6) + actioncable (= 6.1.6) + actionmailbox (= 6.1.6) + actionmailer (= 6.1.6) + actionpack (= 6.1.6) + actiontext (= 6.1.6) + actionview (= 6.1.6) + activejob (= 6.1.6) + activemodel (= 6.1.6) + activerecord (= 6.1.6) + activestorage (= 6.1.6) + activesupport (= 6.1.6) bundler (>= 1.15.0) - railties (= 6.1.5) + railties (= 6.1.6) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -504,16 +514,16 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.4.2) + rails-html-sanitizer (1.4.3) loofah (~> 2.3) rails-i18n (6.0.0) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 7) rails-settings-cached (0.6.6) rails (>= 4.2.0) - railties (6.1.5) - actionpack (= 6.1.5) - activesupport (= 6.1.5) + railties (6.1.6) + actionpack (= 6.1.6) + activesupport (= 6.1.6) method_source rake (>= 12.2) thor (~> 1.0) @@ -527,8 +537,8 @@ GEM redis (4.5.1) redis-namespace (1.8.2) redis (>= 3.0.4) - regexp_parser (2.2.1) - request_store (1.5.0) + regexp_parser (2.5.0) + request_store (1.5.1) rack (>= 1.4) responders (3.0.1) actionpack (>= 5.0) @@ -545,10 +555,10 @@ GEM rspec-expectations (3.11.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.11.0) - rspec-mocks (3.11.0) + rspec-mocks (3.11.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.11.0) - rspec-rails (5.1.1) + rspec-rails (5.1.2) actionpack (>= 5.2) activesupport (>= 5.2) railties (>= 5.2) @@ -562,18 +572,18 @@ GEM rspec-support (3.11.0) rspec_junit_formatter (0.5.1) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (1.26.0) + rubocop (1.30.1) parallel (~> 1.10) parser (>= 3.1.0.0) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) - rexml - rubocop-ast (>= 1.16.0, < 2.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.18.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.16.0) + rubocop-ast (1.18.0) parser (>= 3.1.1.0) - rubocop-rails (2.14.2) + rubocop-rails (2.15.0) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.7.0, < 2.0) @@ -594,25 +604,23 @@ GEM railties (>= 4.0.0) securecompare (1.0.0) semantic_range (3.0.0) - sidekiq (6.4.1) + sidekiq (6.4.2) connection_pool (>= 2.2.2) rack (~> 2.0) redis (>= 4.2.0) sidekiq-bulk (0.2.0) sidekiq - sidekiq-scheduler (3.1.1) - e2mmap - redis (>= 3, < 5) + sidekiq-scheduler (4.0.2) + redis (>= 4.2.0) rufus-scheduler (~> 3.2) - sidekiq (>= 3) - thwait + sidekiq (>= 4) tilt (>= 1.4.0) - sidekiq-unique-jobs (7.1.15) + sidekiq-unique-jobs (7.1.25) brpoplpush-redis_script (> 0.1.1, <= 2.0.0) concurrent-ruby (~> 1.0, >= 1.0.5) sidekiq (>= 5.0, < 8.0) thor (>= 0.20, < 3.0) - simple-navigation (4.3.0) + simple-navigation (4.4.0) activesupport (>= 2.3.2) simple_form (5.1.0) actionpack (>= 5.2) @@ -623,6 +631,7 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.2) + smart_properties (1.17.0) sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -635,10 +644,10 @@ GEM net-ssh (>= 2.8.0) stackprof (0.2.19) statsd-ruby (1.5.0) - stoplight (2.2.1) + stoplight (3.0.0) strong_migrations (0.7.9) activerecord (>= 5) - swd (1.2.0) + swd (1.3.0) activesupport (>= 3) attr_required (>= 0.0.5) httpclient (>= 2.4) @@ -648,8 +657,6 @@ GEM terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) thor (1.2.1) - thwait (0.2.0) - e2mmap tilt (2.0.10) tpm-key_attestation (0.9.0) bindata (~> 2.4) @@ -673,9 +680,9 @@ GEM tzinfo (>= 1.0.0) unf (0.1.4) unf_ext - unf_ext (0.0.8) + unf_ext (0.0.8.2) unicode-display_width (2.1.0) - uniform_notifier (1.14.2) + uniform_notifier (1.16.0) validate_email (0.1.6) activemodel (>= 3.0) mail (>= 2.2.5) @@ -694,7 +701,7 @@ GEM safety_net_attestation (~> 0.4.0) securecompare (~> 1.0) tpm-key_attestation (~> 0.9.0) - webfinger (1.1.0) + webfinger (1.2.0) activesupport httpclient (>= 2.4) webmock (3.14.0) @@ -716,7 +723,7 @@ GEM xorcist (1.1.2) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.5.4) + zeitwerk (2.6.0) PLATFORMS ruby @@ -726,23 +733,24 @@ DEPENDENCIES active_record_query_trace (~> 1.8) addressable (~> 2.8) annotate (~> 3.2) - aws-sdk-s3 (~> 1.113) + aws-sdk-s3 (~> 1.114) better_errors (~> 2.9) binding_of_caller (~> 1.0) blurhash (~> 0.1) - bootsnap (~> 1.10.3) + bootsnap (~> 1.12.0) brakeman (~> 5.2) browser bullet (~> 7.0) bundler-audit (~> 0.9) - capistrano (~> 3.16) + capistrano (~> 3.17) capistrano-rails (~> 1.6) capistrano-rbenv (~> 2.2) capistrano-yarn (~> 2.0) - capybara (~> 3.36) + capybara (~> 3.37) charlock_holmes (~> 0.7.7) chewy (~> 7.2) climate_control (~> 0.2) + cocoon (~> 1.2) color_diff (~> 0.1) concurrent-ruby connection_pool @@ -753,22 +761,22 @@ DEPENDENCIES doorkeeper (~> 5.5) dotenv-rails (~> 2.7) ed25519 (~> 1.3) - fabrication (~> 2.27) - faker (~> 2.20) + fabrication (~> 2.29) + faker (~> 2.21) fast_blank (~> 1.0) fastimage fog-core (<= 2.1.0) fog-openstack (~> 0.3) fuubar (~> 2.5) - gitlab-omniauth-openid-connect (~> 0.5.0) + gitlab-omniauth-openid-connect (~> 0.9.1) hamlit-rails (~> 0.2) hcaptcha (~> 7.1) hiredis (~> 0.6) htmlentities (~> 4.3) - http (~> 5.0) + http (~> 5.1) http_accept_language (~> 2.1) httplog (~> 1.5.0) - i18n-tasks (~> 0.9) + i18n-tasks (~> 1.0) idn-ruby json-ld json-ld-preloaded (~> 3.2) @@ -777,11 +785,11 @@ DEPENDENCIES letter_opener (~> 1.8) letter_opener_web (~> 2.0) link_header (~> 0.0) - lograge (~> 0.11) + lograge (~> 0.12) makara (~> 0.5) mario-redis-lock (~> 1.2) memory_profiler - microformats (~> 4.2) + microformats (~> 4.4) mime-types (~> 3.4.1) net-ldap (~> 0.17) nokogiri (~> 1.13) @@ -793,7 +801,7 @@ DEPENDENCIES omniauth-saml (~> 1.10) ox (~> 2.14) parslet - pg (~> 1.3) + pg (~> 1.4) pghero (~> 2.8) pkg-config (~> 1.4) posix-spawn @@ -806,7 +814,7 @@ DEPENDENCIES rack (~> 2.2.3) rack-attack (~> 6.6) rack-cors (~> 1.1) - rails (~> 6.1.5) + rails (~> 6.1.6) rails-controller-testing (~> 1.0) rails-i18n (~> 6.0) rails-settings-cached (~> 0.6) @@ -819,22 +827,22 @@ DEPENDENCIES rspec-rails (~> 5.1) rspec-sidekiq (~> 3.1) rspec_junit_formatter (~> 0.5) - rubocop (~> 1.26) - rubocop-rails (~> 2.14) + rubocop (~> 1.30) + rubocop-rails (~> 2.15) ruby-progressbar (~> 1.11) sanitize (~> 6.0) scenic (~> 1.6) sidekiq (~> 6.4) sidekiq-bulk (~> 0.2.0) - sidekiq-scheduler (~> 3.1) + sidekiq-scheduler (~> 4.0) sidekiq-unique-jobs (~> 7.1) - simple-navigation (~> 4.3) + simple-navigation (~> 4.4) simple_form (~> 5.1) simplecov (~> 0.21) sprockets (~> 3.7.2) sprockets-rails (~> 3.4) stackprof - stoplight (~> 2.2.1) + stoplight (~> 3.0.0) strong_migrations (~> 0.7) thor (~> 1.2) tty-prompt (~> 0.23) diff --git a/SECURITY.md b/SECURITY.md index 5531a306e..62e23f736 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,8 +12,9 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through | Version | Supported | | ------- | ------------------ | +| 3.5.x | Yes | | 3.4.x | Yes | -| 3.3.x | Yes | +| 3.3.x | No | | < 3.3 | No | [bug-bounty]: https://app.intigriti.com/programs/mastodon/mastodonio/detail diff --git a/app/chewy/accounts_index.rb b/app/chewy/accounts_index.rb index 6f9ea76e9..e38e14a10 100644 --- a/app/chewy/accounts_index.rb +++ b/app/chewy/accounts_index.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class AccountsIndex < Chewy::Index - settings index: { refresh_interval: '5m' }, analysis: { + settings index: { refresh_interval: '30s' }, analysis: { analyzer: { content: { tokenizer: 'whitespace', @@ -23,7 +23,7 @@ class AccountsIndex < Chewy::Index }, } - index_scope ::Account.searchable.includes(:account_stat), delete_if: ->(account) { account.destroyed? || !account.searchable? } + index_scope ::Account.searchable.includes(:account_stat) root date_detection: false do field :id, type: 'long' @@ -36,8 +36,8 @@ class AccountsIndex < Chewy::Index field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content' end - field :following_count, type: 'long', value: ->(account) { account.following.local.count } - field :followers_count, type: 'long', value: ->(account) { account.followers.local.count } + field :following_count, type: 'long', value: ->(account) { account.following_count } + field :followers_count, type: 'long', value: ->(account) { account.followers_count } field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at } end end diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb index 65cbb6fcd..6dd4fb18b 100644 --- a/app/chewy/statuses_index.rb +++ b/app/chewy/statuses_index.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true class StatusesIndex < Chewy::Index - settings index: { refresh_interval: '15m' }, analysis: { + include FormattingHelper + + settings index: { refresh_interval: '30s' }, analysis: { filter: { english_stop: { type: 'stop', @@ -31,6 +33,8 @@ class StatusesIndex < Chewy::Index }, } + # We do not use delete_if option here because it would call a method that we + # expect to be called with crutches without crutches, causing n+1 queries index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll) crutch :mentions do |collection| @@ -53,11 +57,16 @@ class StatusesIndex < Chewy::Index data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) } end + crutch :votes do |collection| + data = ::PollVote.joins(:poll).where(poll: { status_id: collection.map(&:id) }).where(account: Account.local).pluck(:status_id, :account_id) + data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) } + end + root date_detection: false do field :id, type: 'long' field :account_id, type: 'long' - field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.ordered_media_attachments.map(&:description)).concat(status.preloadable_poll ? status.preloadable_poll.options : []).join("\n\n") } do + field :text, type: 'text', value: ->(status) { status.searchable_text } do field :stemmed, type: 'text', analyzer: 'content' end diff --git a/app/chewy/tags_index.rb b/app/chewy/tags_index.rb index f9db2b03a..df3d9e4cc 100644 --- a/app/chewy/tags_index.rb +++ b/app/chewy/tags_index.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class TagsIndex < Chewy::Index - settings index: { refresh_interval: '15m' }, analysis: { + settings index: { refresh_interval: '30s' }, analysis: { analyzer: { content: { tokenizer: 'keyword', @@ -23,7 +23,11 @@ class TagsIndex < Chewy::Index }, } - index_scope ::Tag.listable, delete_if: ->(tag) { tag.destroyed? || !tag.listable? } + index_scope ::Tag.listable + + crutch :time_period do + 7.days.ago.to_date..0.days.ago.to_date + end root date_detection: false do field :name, type: 'text', analyzer: 'content' do @@ -31,7 +35,7 @@ class TagsIndex < Chewy::Index end field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? } - field :usage, type: 'long', value: ->(tag) { tag.history.reduce(0) { |total, day| total + day.accounts } } + field :usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts } field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at } end end diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 03c07c50b..9949206cb 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -45,7 +45,6 @@ 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 = cache_collection(@statuses, Status) - render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag]) end format.json do diff --git a/app/controllers/activitypub/base_controller.rb b/app/controllers/activitypub/base_controller.rb index 196d85a32..b8a7e0ab9 100644 --- a/app/controllers/activitypub/base_controller.rb +++ b/app/controllers/activitypub/base_controller.rb @@ -2,6 +2,7 @@ class ActivityPub::BaseController < Api::BaseController skip_before_action :require_authenticated_user! + skip_before_action :require_not_suspended! skip_around_action :set_locale private diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index e376baab2..da9c6dd16 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -2,6 +2,8 @@ module Admin class DashboardController < BaseController + include Redisable + def index @system_checks = Admin::SystemCheck.perform @time_period = (29.days.ago.to_date...Time.now.utc.to_date) @@ -15,10 +17,10 @@ module Admin def redis_info @redis_info ||= begin - if Redis.current.is_a?(Redis::Namespace) - Redis.current.redis.info + if redis.is_a?(Redis::Namespace) + redis.redis.info else - Redis.current.info + redis.info end end end diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 16defc1ea..48e9781d6 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -4,6 +4,17 @@ module Admin class DomainBlocksController < BaseController before_action :set_domain_block, only: [:show, :destroy, :edit, :update] + def batch + @form = Form::DomainBlockBatch.new(form_domain_block_batch_params.merge(current_account: current_account, action: action_from_button)) + @form.save + rescue ActionController::ParameterMissing + flash[:alert] = I18n.t('admin.email_domain_blocks.no_domain_block_selected') + rescue Mastodon::NotPermittedError + flash[:alert] = I18n.t('admin.domain_blocks.created_msg') + else + redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') + end + def new authorize :domain_block, :create? @domain_block = DomainBlock.new(domain: params[:_domain]) @@ -76,5 +87,15 @@ module Admin def resource_params 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_reports, :private_comment, :public_comment, :obfuscate]) + end + + def action_from_button + if params[:save] + 'save' + end + end end end diff --git a/app/controllers/admin/export_domain_allows_controller.rb b/app/controllers/admin/export_domain_allows_controller.rb new file mode 100644 index 000000000..eb2955ac3 --- /dev/null +++ b/app/controllers/admin/export_domain_allows_controller.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'csv' + +module Admin + class ExportDomainAllowsController < BaseController + include AdminExportControllerConcern + + before_action :set_dummy_import!, only: [:new] + + ROWS_PROCESSING_LIMIT = 20_000 + + def new + authorize :domain_allow, :create? + end + + def export + authorize :instance, :index? + send_export_file + end + + def import + authorize :domain_allow, :create? + begin + @import = Admin::Import.new(import_params) + parse_import_data!(export_headers) + + @data.take(ROWS_PROCESSING_LIMIT).each do |row| + domain = row['#domain'].strip + next if DomainAllow.allowed?(domain) + + domain_allow = DomainAllow.new(domain: domain) + log_action :create, domain_allow if domain_allow.save + end + flash[:notice] = I18n.t('admin.domain_allows.created_msg') + rescue ActionController::ParameterMissing + flash[:error] = I18n.t('admin.export_domain_allows.no_file') + end + redirect_to admin_instances_path + end + + private + + def export_filename + 'domain_allows.csv' + end + + def export_headers + %w(#domain) + end + + def export_data + CSV.generate(headers: export_headers, write_headers: true) do |content| + DomainAllow.allowed_domains.each do |instance| + content << [instance.domain] + end + end + end + end +end diff --git a/app/controllers/admin/export_domain_blocks_controller.rb b/app/controllers/admin/export_domain_blocks_controller.rb new file mode 100644 index 000000000..db8863551 --- /dev/null +++ b/app/controllers/admin/export_domain_blocks_controller.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'csv' + +module Admin + class ExportDomainBlocksController < BaseController + include AdminExportControllerConcern + + before_action :set_dummy_import!, only: [:new] + + ROWS_PROCESSING_LIMIT = 20_000 + + def new + authorize :domain_block, :create? + end + + def export + authorize :instance, :index? + send_export_file + end + + def import + authorize :domain_block, :create? + + @import = Admin::Import.new(import_params) + parse_import_data!(export_headers) + + @global_private_comment = I18n.t('admin.export_domain_blocks.import.private_comment_template', source: @import.data_file_name, date: I18n.l(Time.now.utc)) + + @form = Form::DomainBlockBatch.new + @domain_blocks = @data.take(ROWS_PROCESSING_LIMIT).filter_map do |row| + domain = row['#domain'].strip + next if DomainBlock.rule_for(domain).present? + + domain_block = DomainBlock.new(domain: domain, + severity: row['#severity'].strip, + reject_media: row['#reject_media'].strip, + reject_reports: row['#reject_reports'].strip, + private_comment: @global_private_comment, + public_comment: row['#public_comment']&.strip, + obfuscate: row['#obfuscate'].strip) + + domain_block if domain_block.valid? + end + + @warning_domains = Instance.where(domain: @domain_blocks.map(&:domain)).where('EXISTS (SELECT 1 FROM follows JOIN accounts ON follows.account_id = accounts.id OR follows.target_account_id = accounts.id WHERE accounts.domain = instances.domain)').pluck(:domain) + rescue ActionController::ParameterMissing + flash.now[:alert] = I18n.t('admin.export_domain_blocks.no_file') + set_dummy_import! + render :new + end + + private + + def export_filename + 'domain_blocks.csv' + end + + def export_headers + %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_user_facing_limitations.each do |instance| + content << [instance.domain, instance.severity, instance.reject_media, instance.reject_reports, instance.public_comment, instance.obfuscate] + end + end + end + end +end diff --git a/app/controllers/admin/sign_in_token_authentications_controller.rb b/app/controllers/admin/sign_in_token_authentications_controller.rb deleted file mode 100644 index e620ab292..000000000 --- a/app/controllers/admin/sign_in_token_authentications_controller.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -module Admin - class SignInTokenAuthenticationsController < BaseController - before_action :set_target_user - - def create - authorize @user, :enable_sign_in_token_auth? - @user.update(skip_sign_in_token: false) - log_action :enable_sign_in_token_auth, @user - redirect_to admin_account_path(@user.account_id) - end - - def destroy - authorize @user, :disable_sign_in_token_auth? - @user.update(skip_sign_in_token: true) - log_action :disable_sign_in_token_auth, @user - redirect_to admin_account_path(@user.account_id) - end - - private - - def set_target_user - @user = User.find(params[:user_id]) - end - end -end diff --git a/app/controllers/admin/webhooks/secrets_controller.rb b/app/controllers/admin/webhooks/secrets_controller.rb new file mode 100644 index 000000000..16af1cf7b --- /dev/null +++ b/app/controllers/admin/webhooks/secrets_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Admin + class Webhooks::SecretsController < BaseController + before_action :set_webhook + + def rotate + authorize @webhook, :rotate_secret? + @webhook.rotate_secret! + redirect_to admin_webhook_path(@webhook) + end + + private + + def set_webhook + @webhook = Webhook.find(params[:webhook_id]) + end + end +end diff --git a/app/controllers/admin/webhooks_controller.rb b/app/controllers/admin/webhooks_controller.rb new file mode 100644 index 000000000..d6fb1a4ea --- /dev/null +++ b/app/controllers/admin/webhooks_controller.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Admin + class WebhooksController < BaseController + before_action :set_webhook, except: [:index, :new, :create] + + def index + authorize :webhook, :index? + + @webhooks = Webhook.page(params[:page]) + end + + def new + authorize :webhook, :create? + + @webhook = Webhook.new + end + + def create + authorize :webhook, :create? + + @webhook = Webhook.new(resource_params) + + if @webhook.save + redirect_to admin_webhook_path(@webhook) + else + render :new + end + end + + def show + authorize @webhook, :show? + end + + def edit + authorize @webhook, :update? + end + + def update + authorize @webhook, :update? + + if @webhook.update(resource_params) + redirect_to admin_webhook_path(@webhook) + else + render :show + end + end + + def enable + authorize @webhook, :enable? + @webhook.enable! + redirect_to admin_webhook_path(@webhook) + end + + def disable + authorize @webhook, :disable? + @webhook.disable! + redirect_to admin_webhook_path(@webhook) + end + + def destroy + authorize @webhook, :destroy? + @webhook.destroy! + redirect_to admin_webhooks_path + end + + private + + def set_webhook + @webhook = Webhook.find(params[:id]) + end + + def resource_params + params.require(:webhook).permit(:url, events: []) + end + end +end diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index d96285b44..2e393fbb6 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -11,6 +11,7 @@ class Api::BaseController < ApplicationController skip_before_action :require_functional!, unless: :whitelist_mode? before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access? + before_action :require_not_suspended! before_action :set_cache_headers protect_from_forgery with: :null_session @@ -97,6 +98,10 @@ class Api::BaseController < ApplicationController render json: { error: 'This method requires an authenticated user' }, status: 401 unless current_user end + def require_not_suspended! + render json: { error: 'Your login is currently disabled' }, status: 403 if current_user&.account&.suspended? + end + def require_user! if !current_user render json: { error: 'This method requires an authenticated user' }, status: 422 diff --git a/app/controllers/api/v1/accounts/lookup_controller.rb b/app/controllers/api/v1/accounts/lookup_controller.rb index aee6be18a..8597f891d 100644 --- a/app/controllers/api/v1/accounts/lookup_controller.rb +++ b/app/controllers/api/v1/accounts/lookup_controller.rb @@ -12,5 +12,7 @@ class Api::V1::Accounts::LookupController < Api::BaseController def set_account @account = ResolveAccountService.new.call(params[:acct], skip_webfinger: true) || raise(ActiveRecord::RecordNotFound) + rescue Addressable::URI::InvalidURIError + raise(ActiveRecord::RecordNotFound) end end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 5134bfb94..5537cc9b0 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -9,6 +9,8 @@ class Api::V1::AccountsController < Api::BaseController 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] skip_before_action :require_authenticated_user!, only: :create @@ -74,6 +76,14 @@ class Api::V1::AccountsController < Api::BaseController @account = Account.find(params[:id]) end + def check_account_approval + raise(ActiveRecord::RecordNotFound) if @account.local? && @account.user_pending? + end + + def check_account_confirmation + raise(ActiveRecord::RecordNotFound) if @account.local? && !@account.user_confirmed? + end + def relationships(**options) AccountRelationshipsPresenter.new([@account.id], current_user.account_id, **options) end diff --git a/app/controllers/api/v1/admin/account_actions_controller.rb b/app/controllers/api/v1/admin/account_actions_controller.rb index 15af50822..6c9e04402 100644 --- a/app/controllers/api/v1/admin/account_actions_controller.rb +++ b/app/controllers/api/v1/admin/account_actions_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::V1::Admin::AccountActionsController < Api::BaseController - protect_from_forgery with: :exception - before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:accounts' } before_action :require_staff! before_action :set_account diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb index 65330b8c8..65ed69f7b 100644 --- a/app/controllers/api/v1/admin/accounts_controller.rb +++ b/app/controllers/api/v1/admin/accounts_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::V1::Admin::AccountsController < Api::BaseController - protect_from_forgery with: :exception - include Authorization include AccountableConcern @@ -67,8 +65,9 @@ class Api::V1::Admin::AccountsController < Api::BaseController def destroy authorize @account, :destroy? + json = render_to_body json: @account, serializer: REST::Admin::AccountSerializer Admin::AccountDeletionWorker.perform_async(@account.id) - render json: @account, serializer: REST::Admin::AccountSerializer + render json: json end def unsensitive @@ -104,13 +103,27 @@ class Api::V1::Admin::AccountsController < Api::BaseController end def filtered_accounts - AccountFilter.new(filter_params).results + AccountFilter.new(translated_filter_params).results end def filter_params params.permit(*FILTER_PARAMS) end + def translated_filter_params + translated_params = { origin: 'local', status: 'active' }.merge(filter_params.slice(*AccountFilter::KEYS)) + + translated_params[:origin] = 'remote' if params[:remote].present? + + %i(active pending disabled silenced suspended).each do |status| + translated_params[:status] = status.to_s if params[status].present? + end + + translated_params[:permissions] = 'staff' if params[:staff].present? + + translated_params + end + def insert_pagination_headers set_pagination_headers(next_path, prev_path) end diff --git a/app/controllers/api/v1/admin/dimensions_controller.rb b/app/controllers/api/v1/admin/dimensions_controller.rb index b1f738990..49a5be1c3 100644 --- a/app/controllers/api/v1/admin/dimensions_controller.rb +++ b/app/controllers/api/v1/admin/dimensions_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::V1::Admin::DimensionsController < Api::BaseController - protect_from_forgery with: :exception - before_action -> { authorize_if_got_token! :'admin:read' } before_action :require_staff! before_action :set_dimensions diff --git a/app/controllers/api/v1/admin/domain_allows_controller.rb b/app/controllers/api/v1/admin/domain_allows_controller.rb new file mode 100644 index 000000000..838978ddb --- /dev/null +++ b/app/controllers/api/v1/admin/domain_allows_controller.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +class Api::V1::Admin::DomainAllowsController < Api::BaseController + include Authorization + include AccountableConcern + + LIMIT = 100 + + before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:domain_allows' }, only: [:index, :show] + before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:domain_allows' }, except: [:index, :show] + before_action :require_staff! + before_action :set_domain_allows, only: :index + before_action :set_domain_allow, only: [:show, :destroy] + + after_action :insert_pagination_headers, only: :index + + PAGINATION_PARAMS = %i(limit).freeze + + def create + authorize :domain_allow, :create? + + @domain_allow = DomainAllow.find_by(resource_params) + + if @domain_allow.nil? + @domain_allow = DomainAllow.create!(resource_params) + log_action :create, @domain_allow + end + + render json: @domain_allow, serializer: REST::Admin::DomainAllowSerializer + end + + def index + authorize :domain_allow, :index? + render json: @domain_allows, each_serializer: REST::Admin::DomainAllowSerializer + end + + def show + authorize @domain_allow, :show? + render json: @domain_allow, serializer: REST::Admin::DomainAllowSerializer + end + + def destroy + authorize @domain_allow, :destroy? + UnallowDomainService.new.call(@domain_allow) + log_action :destroy, @domain_allow + render json: @domain_allow, serializer: REST::Admin::DomainAllowSerializer + end + + private + + def set_domain_allows + @domain_allows = filtered_domain_allows.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + end + + def set_domain_allow + @domain_allow = DomainAllow.find(params[:id]) + end + + def filtered_domain_allows + # TODO: no filtering yet + 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 + + def prev_path + api_v1_admin_domain_allows_url(pagination_params(min_id: pagination_since_id)) unless @domain_allows.empty? + end + + def pagination_max_id + @domain_allows.last.id + end + + def pagination_since_id + @domain_allows.first.id + end + + def records_continue? + @domain_allows.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end + + def resource_params + params.permit(:domain) + end +end diff --git a/app/controllers/api/v1/admin/domain_blocks_controller.rb b/app/controllers/api/v1/admin/domain_blocks_controller.rb new file mode 100644 index 000000000..229870eee --- /dev/null +++ b/app/controllers/api/v1/admin/domain_blocks_controller.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +class Api::V1::Admin::DomainBlocksController < Api::BaseController + include Authorization + include AccountableConcern + + LIMIT = 100 + + before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:domain_blocks' }, only: [:index, :show] + before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:domain_blocks' }, except: [:index, :show] + before_action :require_staff! + before_action :set_domain_blocks, only: :index + before_action :set_domain_block, only: [:show, :update, :destroy] + + after_action :insert_pagination_headers, only: :index + + PAGINATION_PARAMS = %i(limit).freeze + + def create + authorize :domain_block, :create? + + 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 existing_domain_block.present? + + @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 + end + + def index + authorize :domain_block, :index? + render json: @domain_blocks, each_serializer: REST::Admin::DomainBlockSerializer + end + + def show + authorize @domain_block, :show? + render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer + end + + def update + authorize @domain_block, :update? + + @domain_block.update(domain_block_params) + severity_changed = @domain_block.severity_changed? + @domain_block.save! + DomainBlockWorker.perform_async(@domain_block.id, severity_changed) + log_action :update, @domain_block + render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer + end + + def destroy + authorize @domain_block, :destroy? + UnblockDomainService.new.call(@domain_block) + log_action :destroy, @domain_block + render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer + end + + private + + 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 + + def set_domain_block + @domain_block = DomainBlock.find(params[:id]) + end + + def filtered_domain_blocks + # TODO: no filtering yet + DomainBlock.all + end + + def domain_block_params + 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 + api_v1_admin_domain_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v1_admin_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @domain_blocks.empty? + end + + def pagination_max_id + @domain_blocks.last.id + end + + def pagination_since_id + @domain_blocks.first.id + end + + def records_continue? + @domain_blocks.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end + + def resource_params + params.permit(:domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate) + end +end diff --git a/app/controllers/api/v1/admin/measures_controller.rb b/app/controllers/api/v1/admin/measures_controller.rb index d64c3cdf7..da95d3422 100644 --- a/app/controllers/api/v1/admin/measures_controller.rb +++ b/app/controllers/api/v1/admin/measures_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::V1::Admin::MeasuresController < Api::BaseController - protect_from_forgery with: :exception - before_action -> { authorize_if_got_token! :'admin:read' } before_action :require_staff! before_action :set_measures diff --git a/app/controllers/api/v1/admin/reports_controller.rb b/app/controllers/api/v1/admin/reports_controller.rb index fbfd0ee12..865ba3d23 100644 --- a/app/controllers/api/v1/admin/reports_controller.rb +++ b/app/controllers/api/v1/admin/reports_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::V1::Admin::ReportsController < Api::BaseController - protect_from_forgery with: :exception - include Authorization include AccountableConcern diff --git a/app/controllers/api/v1/admin/retention_controller.rb b/app/controllers/api/v1/admin/retention_controller.rb index 4af5a5c4d..98d1a3d81 100644 --- a/app/controllers/api/v1/admin/retention_controller.rb +++ b/app/controllers/api/v1/admin/retention_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::V1::Admin::RetentionController < Api::BaseController - protect_from_forgery with: :exception - before_action -> { authorize_if_got_token! :'admin:read' } before_action :require_staff! before_action :set_cohorts diff --git a/app/controllers/api/v1/admin/trends/links_controller.rb b/app/controllers/api/v1/admin/trends/links_controller.rb index 63b3d9358..0a191fe4b 100644 --- a/app/controllers/api/v1/admin/trends/links_controller.rb +++ b/app/controllers/api/v1/admin/trends/links_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::V1::Admin::Trends::LinksController < Api::BaseController - protect_from_forgery with: :exception - before_action -> { authorize_if_got_token! :'admin:read' } before_action :require_staff! before_action :set_links diff --git a/app/controllers/api/v1/admin/trends/statuses_controller.rb b/app/controllers/api/v1/admin/trends/statuses_controller.rb index 86633cc74..cb145f165 100644 --- a/app/controllers/api/v1/admin/trends/statuses_controller.rb +++ b/app/controllers/api/v1/admin/trends/statuses_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::V1::Admin::Trends::StatusesController < Api::BaseController - protect_from_forgery with: :exception - before_action -> { authorize_if_got_token! :'admin:read' } before_action :require_staff! before_action :set_statuses diff --git a/app/controllers/api/v1/admin/trends/tags_controller.rb b/app/controllers/api/v1/admin/trends/tags_controller.rb index 5cc4c269d..9c28b0412 100644 --- a/app/controllers/api/v1/admin/trends/tags_controller.rb +++ b/app/controllers/api/v1/admin/trends/tags_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::V1::Admin::Trends::TagsController < Api::BaseController - protect_from_forgery with: :exception - before_action -> { authorize_if_got_token! :'admin:read' } before_action :require_staff! before_action :set_tags diff --git a/app/controllers/api/v1/bookmarks_controller.rb b/app/controllers/api/v1/bookmarks_controller.rb index aa3fb88f0..0cc231840 100644 --- a/app/controllers/api/v1/bookmarks_controller.rb +++ b/app/controllers/api/v1/bookmarks_controller.rb @@ -21,7 +21,7 @@ class Api::V1::BookmarksController < Api::BaseController end def results - @_results ||= account_bookmarks.eager_load(:status).to_a_paginated_by_id( + @_results ||= account_bookmarks.joins(:status).eager_load(:status).to_a_paginated_by_id( limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id) ) diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb index 21836bc17..2a873696c 100644 --- a/app/controllers/api/v1/favourites_controller.rb +++ b/app/controllers/api/v1/favourites_controller.rb @@ -21,7 +21,7 @@ class Api::V1::FavouritesController < Api::BaseController end def results - @_results ||= account_favourites.eager_load(:status).to_a_paginated_by_id( + @_results ||= account_favourites.joins(:status).eager_load(:status).to_a_paginated_by_id( limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id) ) diff --git a/app/controllers/api/v1/filters/keywords_controller.rb b/app/controllers/api/v1/filters/keywords_controller.rb new file mode 100644 index 000000000..d3718a137 --- /dev/null +++ b/app/controllers/api/v1/filters/keywords_controller.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +class Api::V1::Filters::KeywordsController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:filters' }, only: [:index, :show] + before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show] + before_action :require_user! + + before_action :set_keywords, only: :index + before_action :set_keyword, only: [:show, :update, :destroy] + + def index + render json: @keywords, each_serializer: REST::FilterKeywordSerializer + end + + def create + @keyword = current_account.custom_filters.find(params[:filter_id]).keywords.create!(resource_params) + + render json: @keyword, serializer: REST::FilterKeywordSerializer + end + + def show + render json: @keyword, serializer: REST::FilterKeywordSerializer + end + + def update + @keyword.update!(resource_params) + + render json: @keyword, serializer: REST::FilterKeywordSerializer + end + + def destroy + @keyword.destroy! + render_empty + end + + private + + def set_keywords + filter = current_account.custom_filters.includes(:keywords).find(params[:filter_id]) + @keywords = filter.keywords + end + + def set_keyword + @keyword = CustomFilterKeyword.includes(:custom_filter).where(custom_filter: { account: current_account }).find(params[:id]) + end + + def resource_params + params.permit(:keyword, :whole_word) + end +end diff --git a/app/controllers/api/v1/filters_controller.rb b/app/controllers/api/v1/filters_controller.rb index b0ace3af0..07cd14147 100644 --- a/app/controllers/api/v1/filters_controller.rb +++ b/app/controllers/api/v1/filters_controller.rb @@ -8,21 +8,32 @@ class Api::V1::FiltersController < Api::BaseController before_action :set_filter, only: [:show, :update, :destroy] def index - render json: @filters, each_serializer: REST::FilterSerializer + render json: @filters, each_serializer: REST::V1::FilterSerializer end def create - @filter = current_account.custom_filters.create!(resource_params) - render json: @filter, serializer: REST::FilterSerializer + ApplicationRecord.transaction do + filter_category = current_account.custom_filters.create!(resource_params) + @filter = filter_category.keywords.create!(keyword_params) + end + + render json: @filter, serializer: REST::V1::FilterSerializer end def show - render json: @filter, serializer: REST::FilterSerializer + render json: @filter, serializer: REST::V1::FilterSerializer end def update - @filter.update!(resource_params) - render json: @filter, serializer: REST::FilterSerializer + ApplicationRecord.transaction do + @filter.update!(keyword_params) + @filter.custom_filter.assign_attributes(filter_params) + raise Mastodon::ValidationError, I18n.t('filters.errors.deprecated_api_multiple_keywords') if @filter.custom_filter.changed? && @filter.custom_filter.keywords.count > 1 + + @filter.custom_filter.save! + end + + render json: @filter, serializer: REST::V1::FilterSerializer end def destroy @@ -33,14 +44,22 @@ class Api::V1::FiltersController < Api::BaseController private def set_filters - @filters = current_account.custom_filters + @filters = CustomFilterKeyword.includes(:custom_filter).where(custom_filter: { account: current_account }) end def set_filter - @filter = current_account.custom_filters.find(params[:id]) + @filter = CustomFilterKeyword.includes(:custom_filter).where(custom_filter: { account: current_account }).find(params[:id]) end def resource_params params.permit(:phrase, :expires_in, :irreversible, :whole_word, context: []) end + + def filter_params + resource_params.slice(:expires_in, :irreversible, :context) + end + + def keyword_params + resource_params.slice(:phrase, :whole_word) + end end diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index c47d6ccfd..ac49167cb 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class Api::V1::NotificationsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, except: [:clear, :dismiss] - before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: [:clear, :dismiss] + before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, except: [:clear, :dismiss, :destroy, :destroy_multiple] + before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: [:clear, :dismiss, :destroy, :destroy_multiple] before_action :require_user! after_action :insert_pagination_headers, only: :index diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb index 47f2e6440..7148d63a4 100644 --- a/app/controllers/api/v1/push/subscriptions_controller.rb +++ b/app/controllers/api/v1/push/subscriptions_controller.rb @@ -52,6 +52,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController def data_params return {} if params[:data].blank? - params.require(:data).permit(:policy, alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status]) + params.require(:data).permit(:policy, alerts: Notification::TYPES) end end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 7de446ac4..b2cee3e92 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -79,10 +79,12 @@ class Api::V1::StatusesController < Api::BaseController authorize @status, :destroy? @status.discard - RemovalWorker.perform_async(@status.id, { 'redraft' => true }) @status.account.statuses_count = @status.account.statuses_count - 1 + json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true + + RemovalWorker.perform_async(@status.id, { 'redraft' => true }) - render json: @status, serializer: REST::StatusSerializer, source_requested: true + render json: json end private diff --git a/app/controllers/api/v1/trends/links_controller.rb b/app/controllers/api/v1/trends/links_controller.rb index ad20e7f8b..2385fe438 100644 --- a/app/controllers/api/v1/trends/links_controller.rb +++ b/app/controllers/api/v1/trends/links_controller.rb @@ -3,6 +3,10 @@ class Api::V1::Trends::LinksController < Api::BaseController before_action :set_links + after_action :insert_pagination_headers + + DEFAULT_LINKS_LIMIT = 10 + def index render json: @links, each_serializer: REST::Trends::LinkSerializer end @@ -20,6 +24,30 @@ class Api::V1::Trends::LinksController < Api::BaseController end def links_from_trends - Trends.links.query.allowed.in_locale(content_locale).limit(limit_param(10)) + Trends.links.query.allowed.in_locale(content_locale).offset(offset_param).limit(limit_param(DEFAULT_LINKS_LIMIT)) + 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 + + def next_path + api_v1_trends_links_url pagination_params(offset: offset_param + limit_param(DEFAULT_LINKS_LIMIT)) if records_continue? + end + + def prev_path + api_v1_trends_links_url pagination_params(offset: offset_param - limit_param(DEFAULT_LINKS_LIMIT)) if offset_param > limit_param(DEFAULT_LINKS_LIMIT) + end + + def records_continue? + @links.size == limit_param(DEFAULT_LINKS_LIMIT) + end + + def offset_param + params[:offset].to_i end end diff --git a/app/controllers/api/v1/trends/statuses_controller.rb b/app/controllers/api/v1/trends/statuses_controller.rb index d4ec97ae5..1f2fff582 100644 --- a/app/controllers/api/v1/trends/statuses_controller.rb +++ b/app/controllers/api/v1/trends/statuses_controller.rb @@ -3,6 +3,8 @@ class Api::V1::Trends::StatusesController < Api::BaseController before_action :set_statuses + after_action :insert_pagination_headers + def index render json: @statuses, each_serializer: REST::StatusSerializer end @@ -22,6 +24,30 @@ class Api::V1::Trends::StatusesController < Api::BaseController def statuses_from_trends scope = Trends.statuses.query.allowed.in_locale(content_locale) scope = scope.filtered_for(current_account) if user_signed_in? - scope.limit(limit_param(DEFAULT_STATUSES_LIMIT)) + scope.offset(offset_param).limit(limit_param(DEFAULT_STATUSES_LIMIT)) + 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 + + def next_path + api_v1_trends_statuses_url pagination_params(offset: offset_param + limit_param(DEFAULT_STATUSES_LIMIT)) if records_continue? + end + + def prev_path + api_v1_trends_statuses_url pagination_params(offset: offset_param - limit_param(DEFAULT_STATUSES_LIMIT)) if offset_param > limit_param(DEFAULT_STATUSES_LIMIT) + end + + def offset_param + params[:offset].to_i + end + + def records_continue? + @statuses.size == limit_param(DEFAULT_STATUSES_LIMIT) end end diff --git a/app/controllers/api/v1/trends/tags_controller.rb b/app/controllers/api/v1/trends/tags_controller.rb index 1334b72d2..38003f599 100644 --- a/app/controllers/api/v1/trends/tags_controller.rb +++ b/app/controllers/api/v1/trends/tags_controller.rb @@ -3,6 +3,10 @@ class Api::V1::Trends::TagsController < Api::BaseController before_action :set_tags + after_action :insert_pagination_headers + + DEFAULT_TAGS_LIMIT = 10 + def index render json: @tags, each_serializer: REST::TagSerializer end @@ -12,10 +16,34 @@ class Api::V1::Trends::TagsController < Api::BaseController def set_tags @tags = begin if Setting.trends - Trends.tags.query.allowed.limit(limit_param(10)) + Trends.tags.query.allowed.offset(offset_param).limit(limit_param(DEFAULT_TAGS_LIMIT)) else [] end end 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 + + def next_path + api_v1_trends_tags_url pagination_params(offset: offset_param + limit_param(DEFAULT_TAGS_LIMIT)) if records_continue? + end + + def prev_path + api_v1_trends_tags_url pagination_params(offset: offset_param - limit_param(DEFAULT_TAGS_LIMIT)) if offset_param > limit_param(DEFAULT_TAGS_LIMIT) + end + + def offset_param + params[:offset].to_i + end + + def records_continue? + @tags.size == limit_param(DEFAULT_TAGS_LIMIT) + end end diff --git a/app/controllers/api/v2/admin/accounts_controller.rb b/app/controllers/api/v2/admin/accounts_controller.rb new file mode 100644 index 000000000..a89e6835e --- /dev/null +++ b/app/controllers/api/v2/admin/accounts_controller.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class Api::V2::Admin::AccountsController < Api::V1::Admin::AccountsController + FILTER_PARAMS = %i( + origin + status + permissions + username + by_domain + display_name + email + ip + invited_by + ).freeze + + PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze + + private + + def filtered_accounts + AccountFilter.new(filter_params).results + end + + def filter_params + params.permit(*FILTER_PARAMS) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end +end diff --git a/app/controllers/api/v2/filters_controller.rb b/app/controllers/api/v2/filters_controller.rb new file mode 100644 index 000000000..8ff3076cf --- /dev/null +++ b/app/controllers/api/v2/filters_controller.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +class Api::V2::FiltersController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:filters' }, only: [:index, :show] + before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show] + before_action :require_user! + before_action :set_filters, only: :index + before_action :set_filter, only: [:show, :update, :destroy] + + def index + render json: @filters, each_serializer: REST::FilterSerializer, rules_requested: true + end + + def create + @filter = current_account.custom_filters.create!(resource_params) + + render json: @filter, serializer: REST::FilterSerializer, rules_requested: true + end + + def show + render json: @filter, serializer: REST::FilterSerializer, rules_requested: true + end + + def update + @filter.update!(resource_params) + + render json: @filter, serializer: REST::FilterSerializer, rules_requested: true + end + + def destroy + @filter.destroy! + render_empty + end + + private + + def set_filters + @filters = current_account.custom_filters.includes(:keywords) + end + + def set_filter + @filter = current_account.custom_filters.find(params[:id]) + end + + def resource_params + 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 ddcf92200..77eeab5b0 100644 --- a/app/controllers/api/v2/search_controller.rb +++ b/app/controllers/api/v2/search_controller.rb @@ -11,6 +11,10 @@ class Api::V2::SearchController < Api::BaseController def index @search = Search.new(search_results) render json: @search, serializer: REST::SearchSerializer + rescue Mastodon::SyntaxError + unprocessable_entity + rescue ActiveRecord::RecordNotFound + not_found end private diff --git a/app/controllers/api/web/embeds_controller.rb b/app/controllers/api/web/embeds_controller.rb index 741ba910f..58f6345e6 100644 --- a/app/controllers/api/web/embeds_controller.rb +++ b/app/controllers/api/web/embeds_controller.rb @@ -15,7 +15,7 @@ class Api::Web::EmbedsController < Api::Web::BaseController return not_found if oembed.nil? begin - oembed[:html] = Formatter.instance.sanitize(oembed[:html], Sanitize::Config::MASTODON_OEMBED) + oembed[:html] = Sanitize.fragment(oembed[:html], Sanitize::Config::MASTODON_OEMBED) rescue ArgumentError return not_found end diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb index 17ad56fa8..0817a905c 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -89,7 +89,7 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController def after_confirmation_path_for(_resource_name, user) if user.created_by_application && truthy_param?(:redirect_to_app) - user.created_by_application.redirect_uri + user.created_by_application.confirmation_redirect_uri else super end diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index 8607077f7..13dfebcdd 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -8,13 +8,18 @@ class Auth::SessionsController < Devise::SessionsController skip_before_action :update_user_sign_in prepend_before_action :set_pack + prepend_before_action :check_suspicious!, only: [:create] include TwoFactorAuthenticationConcern - include SignInTokenAuthenticationConcern before_action :set_instance_presenter, only: [:new] before_action :set_body_classes + def check_suspicious! + user = find_user + @login_is_suspicious = suspicious_sign_in?(user) unless user.nil? + end + def create super do |resource| # We only need to call this if this hasn't already been @@ -68,7 +73,7 @@ class Auth::SessionsController < Devise::SessionsController end def user_params - params.require(:user).permit(:email, :password, :otp_attempt, :sign_in_token_attempt, credential: {}) + params.require(:user).permit(:email, :password, :otp_attempt, credential: {}) end def after_sign_in_path_for(resource) @@ -148,6 +153,12 @@ class Auth::SessionsController < Devise::SessionsController ip: request.remote_ip, user_agent: request.user_agent ) + + UserMailer.suspicious_sign_in(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later! if @login_is_suspicious + end + + def suspicious_sign_in?(user) + SuspiciousSignInDetector.new(user).suspicious?(request) end def on_authentication_failure(user, security_measure, failure_reason) diff --git a/app/controllers/authorize_interactions_controller.rb b/app/controllers/authorize_interactions_controller.rb index f0bcac75b..97fe4a9ab 100644 --- a/app/controllers/authorize_interactions_controller.rb +++ b/app/controllers/authorize_interactions_controller.rb @@ -14,7 +14,7 @@ class AuthorizeInteractionsController < ApplicationController if @resource.is_a?(Account) render :show elsif @resource.is_a?(Status) - redirect_to web_url("statuses/#{@resource.id}") + redirect_to web_url("@#{@resource.account.pretty_acct}/#{@resource.id}") else render :error end @@ -26,15 +26,17 @@ class AuthorizeInteractionsController < ApplicationController else render :error end - rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError + rescue ActiveRecord::RecordNotFound render :error end private def set_resource - @resource = located_resource || render(:error) + @resource = located_resource authorize(@resource, :show?) if @resource.is_a?(Status) + rescue Mastodon::NotPermittedError + not_found end def located_resource diff --git a/app/controllers/concerns/admin_export_controller_concern.rb b/app/controllers/concerns/admin_export_controller_concern.rb new file mode 100644 index 000000000..013915d02 --- /dev/null +++ b/app/controllers/concerns/admin_export_controller_concern.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module AdminExportControllerConcern + extend ActiveSupport::Concern + + private + + def send_export_file + respond_to do |format| + format.csv { send_data export_data, filename: export_filename } + end + end + + def export_data + raise 'Override in controller' + end + + def export_filename + raise 'Override in controller' + end + + def set_dummy_import! + @import = Admin::Import.new + end + + def import_params + params.require(:admin_import).permit(:data) + end + + def import_data + Paperclip.io_adapters.for(@import.data).read + end + + def parse_import_data!(default_headers) + data = CSV.parse(import_data, headers: true) + data = CSV.parse(import_data, headers: default_headers) unless data.headers&.first&.strip&.include?(default_headers[0]) + @data = data.reject(&:blank?) + end +end diff --git a/app/controllers/concerns/sign_in_token_authentication_concern.rb b/app/controllers/concerns/sign_in_token_authentication_concern.rb deleted file mode 100644 index 4eb3d7181..000000000 --- a/app/controllers/concerns/sign_in_token_authentication_concern.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -module SignInTokenAuthenticationConcern - extend ActiveSupport::Concern - - included do - prepend_before_action :authenticate_with_sign_in_token, if: :sign_in_token_required?, only: [:create] - end - - def sign_in_token_required? - find_user&.suspicious_sign_in?(request.remote_ip) - end - - def valid_sign_in_token_attempt?(user) - Devise.secure_compare(user.sign_in_token, user_params[:sign_in_token_attempt]) - end - - def authenticate_with_sign_in_token - if user_params[:email].present? - user = self.resource = find_user_from_params - prompt_for_sign_in_token(user) if user&.external_or_valid_password?(user_params[:password]) - elsif session[:attempt_user_id] - user = self.resource = User.find_by(id: session[:attempt_user_id]) - return if user.nil? - - if session[:attempt_user_updated_at] != user.updated_at.to_s - restart_session - elsif user_params.key?(:sign_in_token_attempt) - authenticate_with_sign_in_token_attempt(user) - end - end - end - - def authenticate_with_sign_in_token_attempt(user) - if valid_sign_in_token_attempt?(user) - on_authentication_success(user, :sign_in_token) - else - on_authentication_failure(user, :sign_in_token, :invalid_sign_in_token) - flash.now[:alert] = I18n.t('users.invalid_sign_in_token') - prompt_for_sign_in_token(user) - end - end - - def prompt_for_sign_in_token(user) - if user.sign_in_token_expired? - user.generate_sign_in_token && user.save - UserMailer.sign_in_token(user, request.remote_ip, request.user_agent, Time.now.utc.to_s).deliver_later! - end - - set_attempt_session(user) - use_pack 'auth' - - @body_classes = 'lighter' - - set_locale { render :sign_in_token } - end -end diff --git a/app/controllers/filters_controller.rb b/app/controllers/filters_controller.rb index 0d4c1b97c..6d778312e 100644 --- a/app/controllers/filters_controller.rb +++ b/app/controllers/filters_controller.rb @@ -4,17 +4,17 @@ class FiltersController < ApplicationController layout 'admin' before_action :authenticate_user! - before_action :set_filters, only: :index before_action :set_filter, only: [:edit, :update, :destroy] before_action :set_pack before_action :set_body_classes def index - @filters = current_account.custom_filters.order(:phrase) + @filters = current_account.custom_filters.includes(:keywords).order(:phrase) end def new - @filter = current_account.custom_filters.build + @filter = current_account.custom_filters.build(action: :warn) + @filter.keywords.build end def create @@ -48,16 +48,12 @@ class FiltersController < ApplicationController use_pack 'settings' end - def set_filters - @filters = current_account.custom_filters - end - def set_filter @filter = current_account.custom_filters.find(params[:id]) end def resource_params - params.require(:custom_filter).permit(:phrase, :expires_in, :irreversible, :whole_word, context: []) + 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/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index bc291c962..11c6b6d50 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -22,7 +22,10 @@ class FollowingAccountsController < ApplicationController end format.json do - raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections? + if page_requested? && @account.hide_collections? + forbidden + next + end expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?) diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index 5596e92d1..3b228722f 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -3,6 +3,8 @@ class MediaProxyController < ApplicationController include RoutingHelper include Authorization + include Redisable + include Lockable skip_before_action :store_current_location skip_before_action :require_functional! @@ -15,14 +17,10 @@ class MediaProxyController < ApplicationController rescue_from HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, with: :internal_server_error def show - RedisLock.acquire(lock_options) do |lock| - if lock.acquired? - @media_attachment = MediaAttachment.remote.attached.find(params[:id]) - authorize @media_attachment.status, :show? - redownload! if @media_attachment.needs_redownload? && !reject_media? - else - raise Mastodon::RaceConditionError - end + with_lock("media_download:#{params[:id]}") do + @media_attachment = MediaAttachment.remote.attached.find(params[:id]) + authorize @media_attachment.status, :show? + redownload! if @media_attachment.needs_redownload? && !reject_media? end redirect_to full_asset_url(@media_attachment.file.url(version)) @@ -44,10 +42,6 @@ class MediaProxyController < ApplicationController end end - def lock_options - { redis: Redis.current, key: "media_download:#{params[:id]}", autorelease: 15.minutes.seconds } - end - def reject_media? DomainBlock.reject_media?(@media_attachment.account.domain) end diff --git a/app/controllers/oauth/tokens_controller.rb b/app/controllers/oauth/tokens_controller.rb index fa6d58f25..34087b20b 100644 --- a/app/controllers/oauth/tokens_controller.rb +++ b/app/controllers/oauth/tokens_controller.rb @@ -2,7 +2,8 @@ class Oauth::TokensController < Doorkeeper::TokensController def revoke - unsubscribe_for_token if authorized? && token.accessible? + unsubscribe_for_token if token.present? && authorized? && token.accessible? + super end diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb index 30138d29e..deaa7940e 100644 --- a/app/controllers/settings/exports_controller.rb +++ b/app/controllers/settings/exports_controller.rb @@ -2,6 +2,8 @@ class Settings::ExportsController < Settings::BaseController include Authorization + include Redisable + include Lockable skip_before_action :require_functional! @@ -13,21 +15,13 @@ class Settings::ExportsController < Settings::BaseController def create backup = nil - RedisLock.acquire(lock_options) do |lock| - if lock.acquired? - authorize :backup, :create? - backup = current_user.backups.create! - else - raise Mastodon::RaceConditionError - end + with_lock("backup:#{current_user.id}") do + authorize :backup, :create? + backup = current_user.backups.create! end BackupWorker.perform_async(backup.id) redirect_to settings_export_path end - - def lock_options - { redis: Redis.current, key: "backup:#{current_user.id}" } - end end diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 1fddd087b..669ed00c6 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -57,7 +57,8 @@ class Settings::PreferencesController < Settings::BaseController :setting_use_pending_items, :setting_trends, :setting_crop_images, - notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag trending_link trending_status), + :setting_always_send_emails, + notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag trending_link trending_status appeal), interactions: %i(must_be_follower must_be_following must_be_following_dm) ) end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 64736e77f..46821a200 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -27,7 +27,6 @@ class TagsController < ApplicationController format.rss do expires_in 0, public: true - render xml: RSS::TagSerializer.render(@tag, @statuses) end format.json do diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb index bb2374c0e..d2e198265 100644 --- a/app/helpers/accounts_helper.rb +++ b/app/helpers/accounts_helper.rb @@ -2,10 +2,12 @@ module AccountsHelper def display_name(account, **options) + str = account.display_name.presence || account.username + if options[:custom_emojify] - Formatter.instance.format_display_name(account, **options) + prerender_custom_emojis(h(str), account.emojis) else - account.display_name.presence || account.username + str end end @@ -21,20 +23,20 @@ module AccountsHelper if user_signed_in? if account.id == current_user.account_id link_to settings_profile_url, class: 'button logo-button' do - safe_join([svg_logo, t('settings.edit_profile')]) + safe_join([logo_as_symbol, t('settings.edit_profile')]) end elsif current_account.following?(account) || current_account.requested?(account) link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do - safe_join([svg_logo, t('accounts.unfollow')]) + safe_join([logo_as_symbol, t('accounts.unfollow')]) end elsif !(account.memorial? || account.moved?) link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do - safe_join([svg_logo, t('accounts.follow')]) + safe_join([logo_as_symbol, t('accounts.follow')]) end end elsif !(account.memorial? || account.moved?) link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do - safe_join([svg_logo, t('accounts.follow')]) + safe_join([logo_as_symbol, t('accounts.follow')]) end end end @@ -103,12 +105,4 @@ module AccountsHelper [prepend_stats.join(', '), account.note].join(' · ') end - - def svg_logo - content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976') - end - - def svg_logo_full - content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678') - end end diff --git a/app/helpers/admin/trends/statuses_helper.rb b/app/helpers/admin/trends/statuses_helper.rb index d16e3dd12..214c1e2a6 100644 --- a/app/helpers/admin/trends/statuses_helper.rb +++ b/app/helpers/admin/trends/statuses_helper.rb @@ -12,9 +12,6 @@ module Admin::Trends::StatusesHelper return '' if text.blank? - html = Formatter.instance.send(:encode, text) - html = Formatter.instance.send(:encode_custom_emojis, html, status.emojis, prefers_autoplay?) - - html.html_safe # rubocop:disable Rails/OutputSafety + prerender_custom_emojis(h(text), status.emojis) end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index eace78af6..62739b964 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -19,8 +19,11 @@ module ApplicationHelper # is looked up from the locales definition, and rails-i18n comes with # values that don't seem to make much sense for many languages, so # override these values with a default of 3 digits of precision. - options[:precision] = 3 - options[:strip_insignificant_zeros] = true + options = options.merge( + precision: 3, + strip_insignificant_zeros: true, + significant: true + ) number_to_human(number, **options) end @@ -80,7 +83,8 @@ module ApplicationHelper end def provider_sign_in_link(provider) - link_to I18n.t("auth.providers.#{provider}", default: provider.to_s.chomp('_oauth2').capitalize), omniauth_authorize_path(:user, provider), class: "button button-#{provider}", method: :post + label = Devise.omniauth_configs[provider]&.strategy&.display_name.presence || I18n.t("auth.providers.#{provider}", default: provider.to_s.chomp('_oauth2').capitalize) + link_to label, omniauth_authorize_path(:user, provider), class: "button button-#{provider}", method: :post end def open_deletion? @@ -95,11 +99,6 @@ module ApplicationHelper end end - def favicon_path - env_suffix = Rails.env.production? ? '' : '-dev' - "/favicon#{env_suffix}.ico" - end - def title Rails.env.production? ? site_title : "#{site_title} (Dev)" end @@ -129,7 +128,7 @@ module ApplicationHelper elsif status.private_visibility? || status.limited_visibility? fa_icon('lock', title: I18n.t('statuses.visibilities.private')) elsif status.direct_visibility? - fa_icon('envelope', title: I18n.t('statuses.visibilities.direct')) + fa_icon('at', title: I18n.t('statuses.visibilities.direct')) end end @@ -143,8 +142,8 @@ module ApplicationHelper end end - def custom_emoji_tag(custom_emoji, animate = true) - if animate + def custom_emoji_tag(custom_emoji) + if prefers_autoplay? image_tag(custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:") else image_tag(custom_emoji.image.url(:static), class: 'emojione custom-emoji', alt: ":#{custom_emoji.shortcode}", 'data-original' => full_asset_url(custom_emoji.image.url), 'data-static' => full_asset_url(custom_emoji.image.url(:static))) @@ -195,7 +194,7 @@ module ApplicationHelper def quote_wrap(text, line_width: 80, break_sequence: "\n") text = word_wrap(text, line_width: line_width - 2, break_sequence: break_sequence) - text.split("\n").map { |line| '> ' + line }.join("\n") + text.split("\n").map { |line| "> #{line}" }.join("\n") end def render_initial_state @@ -240,4 +239,8 @@ module ApplicationHelper end end.values end + + def prerender_custom_emojis(html, custom_emojis, other_options = {}) + EmojiFormatter.new(html, custom_emojis, other_options.merge(animate: prefers_autoplay?)).to_s + end end diff --git a/app/helpers/branding_helper.rb b/app/helpers/branding_helper.rb new file mode 100644 index 000000000..ad7702aea --- /dev/null +++ b/app/helpers/branding_helper.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module BrandingHelper + def logo_as_symbol(version = :icon) + case version + when :icon + _logo_as_symbol_icon + when :wordmark + _logo_as_symbol_wordmark + end + end + + def _logo_as_symbol_wordmark + content_tag(:svg, tag(:use, href: '#logo-symbol-wordmark'), viewBox: '0 0 261 66', class: 'logo logo--wordmark') + end + + def _logo_as_symbol_icon + content_tag(:svg, tag(:use, href: '#logo-symbol-icon'), viewBox: '0 0 79 79', class: 'logo logo--icon') + end + + def render_logo + image_pack_tag('logo.svg', alt: 'Mastodon', class: 'logo logo--icon') + end + + def render_symbol(version = :icon) + path = begin + case version + when :icon + 'logo-symbol-icon.svg' + when :wordmark + 'logo-symbol-wordmark.svg' + end + end + + render(file: Rails.root.join('app', 'javascript', 'images', path)).html_safe # rubocop:disable Rails/OutputSafety + end +end diff --git a/app/helpers/formatting_helper.rb b/app/helpers/formatting_helper.rb new file mode 100644 index 000000000..448177bec --- /dev/null +++ b/app/helpers/formatting_helper.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module FormattingHelper + def html_aware_format(text, local, options = {}) + HtmlAwareFormatter.new(text, local, options).to_s + end + + def linkify(text, options = {}) + TextFormatter.new(text, options).to_s + end + + def extract_status_plain_text(status) + PlainTextFormatter.new(status.text, status.local?).to_s + end + module_function :extract_status_plain_text + + def status_content_format(status) + html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), content_type: status.content_type) + end + + def rss_status_content_format(status) + html = status_content_format(status) + + before_html = begin + if status.spoiler_text? + "

#{I18n.t('rss.content_warning', locale: available_locale_or_nil(status.language) || I18n.default_locale)} #{h(status.spoiler_text)}


" + else + '' + end + end.html_safe # rubocop:disable Rails/OutputSafety + + after_html = begin + if status.preloadable_poll + "

#{status.preloadable_poll.options.map { |o| " #{h(o)}" }.join('
')}

" + else + '' + end + end.html_safe # rubocop:disable Rails/OutputSafety + + prerender_custom_emojis( + safe_join([before_html, html, after_html]), + status.emojis, + 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?) + end + + def account_field_value_format(field, with_rel_me: true) + html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false) + end +end diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb index d39bb6c93..4077e19bd 100644 --- a/app/helpers/languages_helper.rb +++ b/app/helpers/languages_helper.rb @@ -254,4 +254,8 @@ module LanguagesHelper def valid_locale?(locale) locale.present? && SUPPORTED_LOCALES.key?(locale.to_sym) end + + def available_locale_or_nil(locale_name) + locale_name.to_sym if locale_name.present? && I18n.available_locales.include?(locale_name.to_sym) + end end diff --git a/app/helpers/routing_helper.rb b/app/helpers/routing_helper.rb index fb24a1b28..0d5a8505a 100644 --- a/app/helpers/routing_helper.rb +++ b/app/helpers/routing_helper.rb @@ -2,6 +2,7 @@ module RoutingHelper extend ActiveSupport::Concern + include Rails.application.routes.url_helpers include ActionView::Helpers::AssetTagHelper include Webpacker::Helper @@ -15,15 +16,17 @@ module RoutingHelper def full_asset_url(source, **options) source = ActionController::Base.helpers.asset_url(source, **options) unless use_storage? - URI.join(root_url, source).to_s + URI.join(asset_host, source).to_s + end + + def asset_host + Rails.configuration.action_controller.asset_host || root_url end def full_pack_url(source, **options) full_asset_url(asset_pack_path(source, **options)) end - private - def use_storage? Rails.configuration.x.use_s3 || Rails.configuration.x.use_swift end diff --git a/app/helpers/settings/keyword_mutes_helper.rb b/app/helpers/settings/keyword_mutes_helper.rb deleted file mode 100644 index 7b98cd59e..000000000 --- a/app/helpers/settings/keyword_mutes_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module Settings::KeywordMutesHelper -end diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index d328f89b7..488eabeec 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -101,7 +101,7 @@ module StatusesHelper when 'private' fa_icon 'lock fw' when 'direct' - fa_icon 'envelope fw' + fa_icon 'at fw' end end @@ -113,20 +113,6 @@ module StatusesHelper end end - private - - def simplified_text(text) - text.dup.tap do |new_text| - URI.extract(new_text).each do |url| - new_text.gsub!(url, '') - end - - new_text.gsub!(Account::MENTION_RE, '') - new_text.gsub!(Tag::HASHTAG_RE, '') - new_text.gsub!(/\s+/, '') - end - end - def embedded_view? params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION end diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js index d2db89ca7..c1b9f07a4 100644 --- a/app/javascript/core/admin.js +++ b/app/javascript/core/admin.js @@ -101,4 +101,20 @@ ready(() => { const registrationMode = document.getElementById('form_admin_settings_registrations_mode'); if (registrationMode) onChangeRegistrationMode(registrationMode); + + const checkAllElement = document.querySelector('#batch_checkbox_all'); + if (checkAllElement) { + checkAllElement.checked = [].every.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked); + checkAllElement.indeterminate = !checkAllElement.checked && [].some.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked); + } + + document.querySelector('a#add-instance-button')?.addEventListener('click', (e) => { + const domain = document.getElementById('by_domain')?.value; + + if (domain) { + const url = new URL(event.target.href); + url.searchParams.set('_domain', domain); + e.target.href = url; + } + }); }); diff --git a/app/javascript/core/mailer.js b/app/javascript/core/mailer.js index baef7e7fb..a4b6d5446 100644 --- a/app/javascript/core/mailer.js +++ b/app/javascript/core/mailer.js @@ -1 +1,3 @@ -import 'styles/mailer.scss'; +require('../styles/mailer.scss'); + +require.context('../icons'); diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js index 0cf64e076..f5871beb3 100644 --- a/app/javascript/flavours/glitch/actions/accounts.js +++ b/app/javascript/flavours/glitch/actions/accounts.js @@ -88,6 +88,8 @@ export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE = 'PINNED_ACCOUNTS_EDITOR export const PINNED_ACCOUNTS_EDITOR_RESET = 'PINNED_ACCOUNTS_EDITOR_RESET'; +export const ACCOUNT_REVEAL = 'ACCOUNT_REVEAL'; + export function fetchAccount(id) { return (dispatch, getState) => { dispatch(fetchRelationships([id])); @@ -798,6 +800,11 @@ export function unpinAccountFail(error) { }; }; +export const revealAccount = id => ({ + type: ACCOUNT_REVEAL, + id, +}); + export function fetchPinnedAccounts() { return (dispatch, getState) => { dispatch(fetchPinnedAccountsRequest()); diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index e9b657424..a669d6a66 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -48,12 +48,13 @@ export const COMPOSE_MOUNT = 'COMPOSE_MOUNT'; export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT'; export const COMPOSE_ADVANCED_OPTIONS_CHANGE = 'COMPOSE_ADVANCED_OPTIONS_CHANGE'; -export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE'; -export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE'; +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_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE'; -export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE'; +export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE'; +export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE'; export const COMPOSE_CONTENT_TYPE_CHANGE = 'COMPOSE_CONTENT_TYPE_CHANGE'; +export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE'; export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT'; @@ -189,6 +190,7 @@ export function submitCompose(routerHistory) { spoiler_text: spoilerText, visibility: getState().getIn(['compose', 'privacy']), poll: getState().getIn(['compose', 'poll'], null), + language: getState().getIn(['compose', 'language']), }, headers: { 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), @@ -675,6 +677,11 @@ export function changeComposeSensitivity() { }; }; +export const changeComposeLanguage = language => ({ + type: COMPOSE_LANGUAGE_CHANGE, + language, +}); + export function changeComposeSpoilerness() { return { type: COMPOSE_SPOILERNESS_CHANGE, diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index bda15a9b0..c38af196a 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -1,7 +1,6 @@ import escapeTextContentForBrowser from 'escape-html'; import emojify from 'flavours/glitch/util/emoji'; import { unescapeHTML } from 'flavours/glitch/util/html'; -import { expandSpoilers } from 'flavours/glitch/util/initial_state'; const domParser = new DOMParser(); diff --git a/app/javascript/flavours/glitch/actions/languages.js b/app/javascript/flavours/glitch/actions/languages.js new file mode 100644 index 000000000..ad186ba0c --- /dev/null +++ b/app/javascript/flavours/glitch/actions/languages.js @@ -0,0 +1,12 @@ +import { saveSettings } from './settings'; + +export const LANGUAGE_USE = 'LANGUAGE_USE'; + +export const useLanguage = language => dispatch => { + dispatch({ + type: LANGUAGE_USE, + language, + }); + + dispatch(saveSettings()); +}; diff --git a/app/javascript/flavours/glitch/actions/local_settings.js b/app/javascript/flavours/glitch/actions/local_settings.js index 28660a4e8..856674eb3 100644 --- a/app/javascript/flavours/glitch/actions/local_settings.js +++ b/app/javascript/flavours/glitch/actions/local_settings.js @@ -1,4 +1,46 @@ +import { expandSpoilers, disableSwiping } from 'flavours/glitch/util/initial_state'; +import { openModal } from './modal'; + export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE'; +export const LOCAL_SETTING_DELETE = 'LOCAL_SETTING_DELETE'; + +export function checkDeprecatedLocalSettings() { + return (dispatch, getState) => { + const local_auto_unfold = getState().getIn(['local_settings', 'content_warnings', 'auto_unfold']); + const local_swipe_to_change_columns = getState().getIn(['local_settings', 'swipe_to_change_columns']); + let changed_settings = []; + + if (local_auto_unfold !== null && local_auto_unfold !== undefined) { + if (local_auto_unfold === expandSpoilers) { + dispatch(deleteLocalSetting(['content_warnings', 'auto_unfold'])); + } else { + changed_settings.push('user_setting_expand_spoilers'); + } + } + + if (local_swipe_to_change_columns !== null && local_swipe_to_change_columns !== undefined) { + if (local_swipe_to_change_columns === !disableSwiping) { + dispatch(deleteLocalSetting(['swipe_to_change_columns'])); + } else { + changed_settings.push('user_setting_disable_swiping'); + } + } + + if (changed_settings.length > 0) { + dispatch(openModal('DEPRECATED_SETTINGS', { + settings: changed_settings, + onConfirm: () => dispatch(clearDeprecatedLocalSettings()), + })); + } + }; +}; + +export function clearDeprecatedLocalSettings() { + return (dispatch) => { + dispatch(deleteLocalSetting(['content_warnings', 'auto_unfold'])); + dispatch(deleteLocalSetting(['swipe_to_change_columns'])); + }; +}; export function changeLocalSetting(key, value) { return dispatch => { @@ -12,6 +54,17 @@ export function changeLocalSetting(key, value) { }; }; +export function deleteLocalSetting(key) { + return dispatch => { + dispatch({ + type: LOCAL_SETTING_DELETE, + key, + }); + + dispatch(saveLocalSettings()); + }; +}; + // __TODO :__ // Right now `saveLocalSettings()` doesn't keep track of which user // is currently signed in, but it might be better to give each user diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index 42ad39efa..3993b1ea5 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -70,7 +70,8 @@ export const loadPending = () => ({ export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { - const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true); + const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']); + const showInColumn = activeFilter === 'all' ? getState().getIn(['settings', 'notifications', 'shows', notification.type], true) : activeFilter === notification.type; const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true); const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true); const filters = getFiltersRegex(getState(), { contextType: 'notifications' }); @@ -102,6 +103,10 @@ export function updateNotifications(notification, intlMessages, intlLocale) { dispatch(importFetchedStatus(notification.status)); } + if (notification.report) { + dispatch(importFetchedAccount(notification.report.target_account)); + } + dispatch({ type: NOTIFICATIONS_UPDATE, notification, @@ -145,6 +150,7 @@ const excludeTypesFromFilter = filter => { 'status', 'update', 'admin.sign_up', + 'admin.report', ]); return allTypes.filterNot(item => item === filter).toJS(); @@ -190,6 +196,7 @@ export function expandNotifications({ maxId } = {}, done = noOp) { dispatch(importFetchedAccounts(response.data.map(item => item.account))); dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); + dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account))); dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems)); fetchRelatedRelationships(dispatch, response.data); diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index 223924534..90d6a0231 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -7,6 +7,10 @@ import { expandHomeTimeline, connectTimeline, disconnectTimeline, + fillHomeTimelineGaps, + fillPublicTimelineGaps, + fillCommunityTimelineGaps, + fillListTimelineGaps, } from './timelines'; import { updateNotifications, expandNotifications } from './notifications'; import { updateConversations } from './conversations'; @@ -35,6 +39,7 @@ const randomUpTo = max => * @param {Object.} params * @param {Object} options * @param {function(Function, Function): void} [options.fallback] + * @param {function(): void} [options.fillGaps] * @param {function(object): boolean} [options.accept] * @return {function(): void} */ @@ -61,6 +66,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti clearTimeout(pollingId); pollingId = null; } + + if (options.fillGaps) { + dispatch(options.fillGaps()); + } }, onDisconnect() { @@ -119,7 +128,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => { * @return {function(): void} */ export const connectUserStream = () => - connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification }); + connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps }); /** * @param {Object} options @@ -127,7 +136,7 @@ export const connectUserStream = () => * @return {function(): void} */ export const connectCommunityStream = ({ onlyMedia } = {}) => - connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`); + connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia })) }); /** * @param {Object} options @@ -137,7 +146,7 @@ export const connectCommunityStream = ({ onlyMedia } = {}) => * @return {function(): void} */ export const connectPublicStream = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}) => - connectTimelineStream(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`); + connectTimelineStream(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => fillPublicTimelineGaps({ onlyMedia, onlyRemote, allowLocalOnly }) }); /** * @param {string} columnId @@ -160,4 +169,4 @@ export const connectDirectStream = () => * @return {function(): void} */ export const connectListStream = listId => - connectTimelineStream(`list:${listId}`, 'list', { list: listId }); + connectTimelineStream(`list:${listId}`, 'list', { list: listId }, { fillGaps: () => fillListTimelineGaps(listId) }); diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index 24cc0d63f..0b36d8ac3 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -138,6 +138,22 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { }; }; +export function fillTimelineGaps(timelineId, path, params = {}, done = noOp) { + return (dispatch, getState) => { + const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); + const items = timeline.get('items'); + const nullIndexes = items.map((statusId, index) => statusId === null ? index : null); + const gaps = nullIndexes.map(index => index > 0 ? items.get(index - 1) : null); + + // Only expand at most two gaps to avoid doing too many requests + done = gaps.take(2).reduce((done, maxId) => { + return (() => dispatch(expandTimeline(timelineId, path, { ...params, maxId }, done))); + }, done); + + done(); + }; +} + export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done); export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); @@ -156,6 +172,11 @@ export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = }, done); }; +export const fillHomeTimelineGaps = (done = noOp) => fillTimelineGaps('home', '/api/v1/timelines/home', {}, done); +export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => fillTimelineGaps(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia, allow_local_only: !!allowLocalOnly }, 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 function expandTimelineRequest(timeline, isLoadingMore) { return { type: TIMELINE_EXPAND_REQUEST, @@ -199,6 +220,7 @@ export function connectTimeline(timeline) { return { type: TIMELINE_CONNECT, timeline, + usePendingItems: preferPendingItems, }; }; diff --git a/app/javascript/flavours/glitch/components/account.js b/app/javascript/flavours/glitch/components/account.js index 396a36ea0..489f60736 100644 --- a/app/javascript/flavours/glitch/components/account.js +++ b/app/javascript/flavours/glitch/components/account.js @@ -16,8 +16,10 @@ const messages = defineMessages({ requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, - mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'You are not currently muting notifications from @{name}. Click to mute notifications' }, - unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'You are currently muting notifications from @{name}. Click to unmute notifications' }, + mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' }, + unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' }, + mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, + block: { id: 'account.block', defaultMessage: 'Block @{name}' }, }); export default @injectIntl @@ -34,6 +36,7 @@ class Account extends ImmutablePureComponent { small: PropTypes.bool, actionIcon: PropTypes.string, actionTitle: PropTypes.string, + defaultAction: PropTypes.string, onActionClick: PropTypes.func, }; @@ -70,6 +73,7 @@ class Account extends ImmutablePureComponent { onActionClick, actionIcon, actionTitle, + defaultAction, } = this.props; if (!account) { @@ -114,6 +118,10 @@ class Account extends ImmutablePureComponent { {hidingNotificationsButton} ); + } else if (defaultAction === 'mute') { + buttons = ; + } else if (defaultAction === 'block') { + buttons = ; } else if (!account.get('moved') || following) { buttons = ; } diff --git a/app/javascript/flavours/glitch/components/admin/Counter.js b/app/javascript/flavours/glitch/components/admin/Counter.js index ecb242950..a4d6cef41 100644 --- a/app/javascript/flavours/glitch/components/admin/Counter.js +++ b/app/javascript/flavours/glitch/components/admin/Counter.js @@ -33,6 +33,7 @@ export default class Counter extends React.PureComponent { label: PropTypes.string.isRequired, href: PropTypes.string, params: PropTypes.object, + target: PropTypes.string, }; state = { @@ -54,7 +55,7 @@ export default class Counter extends React.PureComponent { } render () { - const { label, href } = this.props; + const { label, href, target } = this.props; const { loading, data } = this.state; let content; @@ -100,7 +101,7 @@ export default class Counter extends React.PureComponent { if (href) { return ( - + {inner} ); diff --git a/app/javascript/flavours/glitch/components/avatar.js b/app/javascript/flavours/glitch/components/avatar.js index c5e9072c4..6d53a5298 100644 --- a/app/javascript/flavours/glitch/components/avatar.js +++ b/app/javascript/flavours/glitch/components/avatar.js @@ -1,13 +1,13 @@ -import classNames from 'classnames'; import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { autoPlayGif } from 'flavours/glitch/util/initial_state'; +import classNames from 'classnames'; export default class Avatar extends React.PureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.map, className: PropTypes.string, size: PropTypes.number.isRequired, style: PropTypes.object, @@ -45,11 +45,6 @@ export default class Avatar extends React.PureComponent { } = this.props; const { hovering } = this.state; - const src = account.get('avatar'); - const staticSrc = account.get('avatar_static'); - - const computedClass = classNames('account__avatar', { 'account__avatar-inline': inline }, className); - const style = { ...this.props.style, width: `${size}px`, @@ -57,19 +52,24 @@ export default class Avatar extends React.PureComponent { backgroundSize: `${size}px ${size}px`, }; - if (hovering || animate) { - style.backgroundImage = `url(${src})`; - } else { - style.backgroundImage = `url(${staticSrc})`; + if (account) { + const src = account.get('avatar'); + const staticSrc = account.get('avatar_static'); + + if (hovering || animate) { + style.backgroundImage = `url(${src})`; + } else { + style.backgroundImage = `url(${staticSrc})`; + } } return (
); } diff --git a/app/javascript/flavours/glitch/components/common_counter.js b/app/javascript/flavours/glitch/components/common_counter.js index e10cd9b76..dd9b62de9 100644 --- a/app/javascript/flavours/glitch/components/common_counter.js +++ b/app/javascript/flavours/glitch/components/common_counter.js @@ -25,7 +25,7 @@ export function counterRenderer(counterType, isBold = true) { return (displayNumber, pluralReady) => ( + return ( + {contents} ); diff --git a/app/javascript/flavours/glitch/components/modal_root.js b/app/javascript/flavours/glitch/components/modal_root.js index 0595f6a0e..056277447 100644 --- a/app/javascript/flavours/glitch/components/modal_root.js +++ b/app/javascript/flavours/glitch/components/modal_root.js @@ -55,6 +55,10 @@ export default class ModalRoot extends React.PureComponent { window.addEventListener('keyup', this.handleKeyUp, false); window.addEventListener('keydown', this.handleKeyDown, false); this.history = this.context.router ? this.context.router.history : createBrowserHistory(); + + if (this.props.children) { + this._handleModalOpen(); + } } componentWillReceiveProps (nextProps) { diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 02ff9ab28..8a5fda676 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -67,7 +67,6 @@ class Status extends ImmutablePureComponent { containerId: PropTypes.string, id: PropTypes.string, status: ImmutablePropTypes.map, - otherAccounts: ImmutablePropTypes.list, account: ImmutablePropTypes.map, onReply: PropTypes.func, onFavourite: PropTypes.func, @@ -100,6 +99,7 @@ class Status extends ImmutablePureComponent { scrollKey: PropTypes.string, deployPictureInPicture: PropTypes.func, usingPiP: PropTypes.bool, + settings: ImmutablePropTypes.map.isRequired, }; state = { @@ -491,7 +491,6 @@ class Status extends ImmutablePureComponent { intl, status, account, - otherAccounts, settings, collapsed, muted, @@ -581,10 +580,7 @@ class Status extends ImmutablePureComponent { // backgrounds for collapsed statuses are enabled. attachments = status.get('media_attachments'); - if (status.get('poll')) { - media.push(); - mediaIcons.push('tasks'); - } + if (usingPiP) { media.push(); mediaIcons.push('video-camera'); @@ -684,6 +680,11 @@ class Status extends ImmutablePureComponent { mediaIcons.push('link'); } + if (status.get('poll')) { + media.push(); + mediaIcons.push('tasks'); + } + // Here we prepare extra data-* attributes for CSS selectors. // Users can use those for theming, hiding avatars etc via UserStyle const selectorAttribs = { @@ -742,7 +743,6 @@ class Status extends ImmutablePureComponent { friend={account} collapsed={isCollapsed} parseClick={parseClick} - otherAccounts={otherAccounts} /> ) : null} @@ -752,7 +752,7 @@ class Status extends ImmutablePureComponent { collapsible={settings.getIn(['collapsed', 'enabled'])} collapsed={isCollapsed} setCollapsed={setCollapsed} - directMessage={!!otherAccounts} + settings={settings.get('status_icons')} /> ) : null} diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index d553c2413..1a8e94dac 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -67,7 +67,6 @@ class StatusActionBar extends ImmutablePureComponent { onFilter: PropTypes.func, withDismiss: PropTypes.bool, showReplyCount: PropTypes.bool, - directMessage: PropTypes.bool, scrollKey: PropTypes.string, intl: PropTypes.object.isRequired, }; @@ -197,7 +196,7 @@ class StatusActionBar extends ImmutablePureComponent { } render () { - const { status, intl, withDismiss, showReplyCount, directMessage, scrollKey } = this.props; + const { status, intl, withDismiss, showReplyCount, scrollKey } = this.props; const anonymousAccess = !me; const mutingConversation = status.get('muted'); @@ -311,25 +310,24 @@ class StatusActionBar extends ImmutablePureComponent { return (
{replyButton} - {!directMessage && [ - , - , - shareButton, - , - filterButton, -
- -
, - ]} + + + {shareButton} + + {filterButton} + +
+ +
{status.get('edited_at') && *} diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index 1d32b35e5..6a027f8d2 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -267,6 +267,7 @@ export default class StatusContent extends React.PureComponent { const content = { __html: status.get('contentHtml') }; const spoilerContent = { __html: status.get('spoilerHtml') }; + const lang = status.get('language'); const classNames = classnames('status__content', { 'status__content--with-action': parseClick && !disabled, 'status__content--with-spoiler': status.get('spoiler_text').length > 0, @@ -327,7 +328,7 @@ export default class StatusContent extends React.PureComponent {
@@ -367,6 +369,7 @@ export default class StatusContent extends React.PureComponent { tabIndex='0' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} + lang={lang} /> {media}
@@ -385,6 +388,7 @@ export default class StatusContent extends React.PureComponent { tabIndex='0' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} + lang={lang} /> {media} diff --git a/app/javascript/flavours/glitch/components/status_header.js b/app/javascript/flavours/glitch/components/status_header.js index cc476139b..990dea536 100644 --- a/app/javascript/flavours/glitch/components/status_header.js +++ b/app/javascript/flavours/glitch/components/status_header.js @@ -15,7 +15,6 @@ export default class StatusHeader extends React.PureComponent { status: ImmutablePropTypes.map.isRequired, friend: ImmutablePropTypes.map, parseClick: PropTypes.func.isRequired, - otherAccounts: ImmutablePropTypes.list, }; // Handles clicks on account name/image @@ -34,57 +33,39 @@ export default class StatusHeader extends React.PureComponent { const { status, friend, - otherAccounts, } = this.props; const account = status.get('account'); let statusAvatar; - if (otherAccounts && otherAccounts.size > 0) { - statusAvatar = ; - } else if (friend === undefined || friend === null) { + if (friend === undefined || friend === null) { statusAvatar = ; } else { statusAvatar = ; } - if (!otherAccounts) { - return ( -
- - {statusAvatar} - - - - -
- ); - } else { - // This is a DM conversation - return ( -
- - {statusAvatar} - - - - - -
- ); - } + return ( +
+ + {statusAvatar} + + + + +
+ ); } } diff --git a/app/javascript/flavours/glitch/components/status_icons.js b/app/javascript/flavours/glitch/components/status_icons.js index e66947f4a..2226aaef2 100644 --- a/app/javascript/flavours/glitch/components/status_icons.js +++ b/app/javascript/flavours/glitch/components/status_icons.js @@ -8,6 +8,7 @@ import { defineMessages, injectIntl } from 'react-intl'; import IconButton from './icon_button'; import VisibilityIcon from './status_visibility_icon'; import Icon from 'flavours/glitch/components/icon'; +import { languages } from 'flavours/glitch/util/initial_state'; // Messages for use with internationalization stuff. const messages = defineMessages({ @@ -22,6 +23,23 @@ const messages = defineMessages({ localOnly: { id: 'status.local_only', defaultMessage: 'Only visible from your instance' }, }); +const LanguageIcon = ({ language }) => { + if (!languages) return null; + + const lang = languages.find((lang) => lang[0] === language); + if (!lang) return null; + + return ( + + ); +}; + +LanguageIcon.propTypes = { + language: PropTypes.string.isRequired, +}; + export default @injectIntl class StatusIcons extends React.PureComponent { @@ -30,9 +48,9 @@ class StatusIcons extends React.PureComponent { mediaIcons: PropTypes.arrayOf(PropTypes.string), collapsible: PropTypes.bool, collapsed: PropTypes.bool, - directMessage: PropTypes.bool, setCollapsed: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, + settings: ImmutablePropTypes.map.isRequired, }; // Handles clicks on collapsed button @@ -81,13 +99,14 @@ class StatusIcons extends React.PureComponent { mediaIcons, collapsible, collapsed, - directMessage, + settings, intl, } = this.props; return (
- {status.get('in_reply_to_id', null) !== null ? ( + {settings.get('language') && status.get('language') && } + {settings.get('reply') && status.get('in_reply_to_id', null) !== null ? ( ) : null} - {status.get('local_only') && + {settings.get('local_only') && status.get('local_only') &&
); } diff --git a/app/javascript/flavours/glitch/components/status_prepend.js b/app/javascript/flavours/glitch/components/status_prepend.js index 1661ca8f5..d85009362 100644 --- a/app/javascript/flavours/glitch/components/status_prepend.js +++ b/app/javascript/flavours/glitch/components/status_prepend.js @@ -38,7 +38,7 @@ export default class StatusPrepend extends React.PureComponent { switch (type) { case 'featured': return ( - + ); case 'reblogged_by': return ( diff --git a/app/javascript/flavours/glitch/components/status_visibility_icon.js b/app/javascript/flavours/glitch/components/status_visibility_icon.js index e2e0f30b8..07d56c7a8 100644 --- a/app/javascript/flavours/glitch/components/status_visibility_icon.js +++ b/app/javascript/flavours/glitch/components/status_visibility_icon.js @@ -9,7 +9,7 @@ const messages = defineMessages({ public: { id: 'privacy.public.short', defaultMessage: 'Public' }, unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, private: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, - direct: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, + direct: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' }, }); export default @injectIntl diff --git a/app/javascript/flavours/glitch/containers/mastodon.js b/app/javascript/flavours/glitch/containers/mastodon.js index de8ea8ee2..989e37024 100644 --- a/app/javascript/flavours/glitch/containers/mastodon.js +++ b/app/javascript/flavours/glitch/containers/mastodon.js @@ -8,6 +8,7 @@ import UI from 'flavours/glitch/features/ui'; import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis'; import { hydrateStore } from 'flavours/glitch/actions/store'; import { connectUserStream } from 'flavours/glitch/actions/streaming'; +import { checkDeprecatedLocalSettings } from 'flavours/glitch/actions/local_settings'; import { IntlProvider, addLocaleData } from 'react-intl'; import { getLocale } from 'locales'; import initialState from 'flavours/glitch/util/initial_state'; @@ -20,6 +21,9 @@ export const store = configureStore(); const hydrateAction = hydrateStore(initialState); store.dispatch(hydrateAction); +// check for deprecated local settings +store.dispatch(checkDeprecatedLocalSettings()); + // load custom emojis store.dispatch(fetchCustomEmojis()); diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index 4b0494fff..45aba53f7 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -37,7 +37,7 @@ const messages = defineMessages({ showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' }, enableNotifications: { id: 'account.enable_notifications', defaultMessage: 'Notify me when @{name} posts' }, disableNotifications: { id: 'account.disable_notifications', defaultMessage: 'Stop notifying me when @{name} posts' }, - pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' }, + pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, @@ -82,6 +82,7 @@ class Header extends ImmutablePureComponent { onEditAccountNote: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, domain: PropTypes.string.isRequired, + hidden: PropTypes.bool, }; openEditProfile = () => { @@ -115,7 +116,7 @@ class Header extends ImmutablePureComponent { } render () { - const { account, intl, domain, identity_proofs } = this.props; + const { account, hidden, intl, domain } = this.props; if (!account) { return null; @@ -270,23 +271,29 @@ class Header extends ImmutablePureComponent { {info} - + {!(suspended || hidden) && }
- +
-
- {actionBtn} - {bellBtn} + {!suspended && ( +
+ {!hidden && ( + + {actionBtn} + {bellBtn} + + )} - -
+ +
+ )}
@@ -298,23 +305,11 @@ class Header extends ImmutablePureComponent { - {!suspended && ( + {!(suspended || hidden) && (
- { (fields.size > 0 || identity_proofs.size > 0) && ( + { fields.size > 0 && (
- {identity_proofs.map((proof, i) => ( -
-
- -
- - - - -
-
- ))} {fields.map((pair, i) => (
diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.js b/app/javascript/flavours/glitch/features/account_timeline/components/header.js index e70f011b7..645ff29ea 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/header.js +++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js @@ -12,7 +12,6 @@ export default class Header extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map, - identity_proofs: ImmutablePropTypes.list, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, @@ -26,6 +25,7 @@ export default class Header extends ImmutablePureComponent { onAddToList: PropTypes.func.isRequired, hideTabs: PropTypes.bool, domain: PropTypes.string.isRequired, + hidden: PropTypes.bool, }; static contextTypes = { @@ -93,7 +93,7 @@ export default class Header extends ImmutablePureComponent { } render () { - const { account, hideTabs, identity_proofs } = this.props; + const { account, hidden, hideTabs } = this.props; if (account === null) { return null; @@ -101,11 +101,10 @@ export default class Header extends ImmutablePureComponent { return (
- {account.get('moved') && } + {(!hidden && account.get('moved')) && }
); } diff --git a/app/javascript/flavours/glitch/features/notifications/components/notification.js b/app/javascript/flavours/glitch/features/notifications/components/notification.js index e0cd3c7a6..d676a4207 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/notification.js +++ b/app/javascript/flavours/glitch/features/notifications/components/notification.js @@ -9,6 +9,7 @@ import StatusContainer from 'flavours/glitch/containers/status_container'; import NotificationFollow from './follow'; import NotificationFollowRequestContainer from '../containers/follow_request_container'; import NotificationAdminSignup from './admin_signup'; +import NotificationAdminReportContainer from '../containers/admin_report_container'; export default class Notification extends ImmutablePureComponent { @@ -77,6 +78,19 @@ export default class Notification extends ImmutablePureComponent { unread={this.props.unread} /> ); + case 'admin.report': + return ( +