mirror of https://github.com/mastodon/mastodon
Compare commits
139 Commits
Author | SHA1 | Date |
---|---|---|
Claire | 44c265e4c7 | |
Claire | 4a57e44809 | |
Claire | 47c6079d8d | |
Claire | 69205dff9a | |
Emelia Smith | d187195f2c | |
blah | 3387868dd9 | |
blah | 3ba6ed76ea | |
Claire | b1ed009c65 | |
Claire | 35f21191ee | |
Claire | 2ffce0d5f7 | |
Claire | 688defd60d | |
Jonathan de Jong | d9b05f6860 | |
Claire | f3fd8d8695 | |
Claire | 49693fe42f | |
Claire | 16262f815d | |
Claire | d4e0a12b27 | |
Claire | db59d8486b | |
Matt Jankowski | 7fb3ee0bc6 | |
David Aaron | 9bd027823d | |
Jakob Gillich | 57d4d46050 | |
Claire | c91116f780 | |
Essem | f45b5f5006 | |
Claire | 47441e51f3 | |
Claire | af02650322 | |
Claire | 75346a71f7 | |
Claire | 49af3e26dc | |
Claire | 412c3e13ec | |
Claire | 31c5e63a58 | |
Nicolai Søborg | e8eeb746ac | |
yufushiro | 0158c31c02 | |
Claire | 9deb178126 | |
Claire | 8e6fe19225 | |
Claire | 4eb709ea7e | |
Claire | 86a31fc019 | |
Claire | 16e47e1aae | |
Emelia Smith | dcffd6b3d7 | |
Daniel M Brasil | 8de0f7e198 | |
Claire | e37551421e | |
Claire | 2e0eab9d18 | |
Claire | ce75c175cd | |
Claire | a3d31ffc1e | |
Emelia Smith | 50f4af28b0 | |
Claire | e655b35d7e | |
Claire | 80c00f4aa5 | |
Claire | 1a0192537d | |
Claire | 668cd00e13 | |
Claire | 0bd52de492 | |
Claire | ced65ffbb4 | |
Claire | 6398fc0b66 | |
Claire | 7709bbba65 | |
Michael Stanclift | 4f6d121b24 | |
Claire | 687421ebbe | |
Claire | 517c4a8a7a | |
Claire | dca0d8427e | |
Claire | b10c974ba1 | |
Claire | ca4b23bf0d | |
Claire | 32e5a9f053 | |
Claire | 987f909994 | |
Claire | c02fa93c57 | |
Claire | c309011346 | |
Claire | 6b538225af | |
Renaud Chaput | 3c72c7b34e | |
Vyr Cossont | 07f60ffcbb | |
Vyr Cossont | c1467453f6 | |
Emelia Smith | 00e65a77df | |
Daniel M Brasil | f9521bc2b5 | |
Emelia Smith | e4bff6cd76 | |
Emelia Smith | 6f819c7071 | |
Claire | 4aa1c4e2ad | |
Daniel M Brasil | 176ae71fd4 | |
Claire | feac95333f | |
Claire | bb1e7e112e | |
Claire | e233060ea5 | |
Claire | 3faebae2d1 | |
Claire | 95f59da157 | |
Claire | 6f94b4ae19 | |
Claire | 283184b390 | |
Claire | d54980ef2d | |
Claire | 08579976e0 | |
Claire | ff3f40a675 | |
Claire | 0dce749192 | |
Claire | 1bd831b9a9 | |
Claire | 55144262d0 | |
Claire | 40438675f8 | |
Claire | 0f4c908b64 | |
Sai | 3eb5b47768 | |
Robert R George | 520e9cc765 | |
Claire | d25493e262 | |
Claire | 3d67a9329e | |
Claire | 547634dfa6 | |
Claire | f90daf58db | |
Eugen Rochko | a42b48ea4e | |
Claire | 251dd0b72b | |
Nick Schonning | 18840cbc6e | |
Renaud Chaput | 727126255a | |
Nick Schonning | 98d654b8bb | |
Renaud Chaput | 25c517144c | |
Claire | f036546c22 | |
Claire | 9256d653a5 | |
Jeremy Kescher | d0c0808ad4 | |
Claire | cb622b23b1 | |
Claire | fe866f8afb | |
Claire | a1e765991e | |
Claire | 76b9f42712 | |
Claire | 708e590117 | |
Rodion Borisov | a717aa929c | |
Claire | bbb7c54367 | |
Eugen Rochko | 282596a66e | |
Claire | e6f6fe6106 | |
Claire | 86b1adf7d7 | |
Claire | 4beeec4e50 | |
Claire | 3c44ba0411 | |
Dean Bassett | 339d4fa61c | |
Claire | 62f0eab635 | |
Claire | 8c8d578e38 | |
Claire | a8a3e86216 | |
Claire | be1caad933 | |
Claire | 84a40824ad | |
Claire | 533bf92d21 | |
Claire | 6a2b48190c | |
Claire | 6cbc589990 | |
Claire | a2bfb16cb8 | |
Claire | cfc0507010 | |
Claire | eade64097c | |
Claire | 1f0be21317 | |
Claire | 0ca877f084 | |
Claire | cc233af129 | |
Claire | 83f1c6460a | |
Claire | e26dd2ea8f | |
Claire | da5d81c90d | |
Claire | ee66f5790f | |
Claire | 696f7b3608 | |
Claire | b22e1476ca | |
Claire | 105ab82425 | |
Claire | 2dd8f977e8 | |
Claire | 2db06e1d08 | |
Eugen Rochko | 063579373e | |
Pierre Bourdon | 1659788de4 | |
Claire | 47eaf85f02 |
|
@ -1,209 +0,0 @@
|
|||
version: 2.1
|
||||
|
||||
orbs:
|
||||
ruby: circleci/ruby@1.4.1
|
||||
node: circleci/node@5.0.1
|
||||
|
||||
executors:
|
||||
default:
|
||||
parameters:
|
||||
ruby-version:
|
||||
type: string
|
||||
docker:
|
||||
- image: cimg/ruby:<< parameters.ruby-version >>
|
||||
environment:
|
||||
BUNDLE_JOBS: 3
|
||||
BUNDLE_RETRY: 3
|
||||
CONTINUOUS_INTEGRATION: true
|
||||
DB_HOST: localhost
|
||||
DB_USER: root
|
||||
DISABLE_SIMPLECOV: true
|
||||
RAILS_ENV: test
|
||||
- image: cimg/postgres:14.0
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
- image: cimg/redis:6.2
|
||||
|
||||
commands:
|
||||
install-system-dependencies:
|
||||
steps:
|
||||
- run:
|
||||
name: Install system dependencies
|
||||
command: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libicu-dev libidn11-dev
|
||||
install-ruby-dependencies:
|
||||
parameters:
|
||||
ruby-version:
|
||||
type: string
|
||||
steps:
|
||||
- run:
|
||||
command: |
|
||||
bundle config clean 'true'
|
||||
bundle config frozen 'true'
|
||||
bundle config without 'development production'
|
||||
name: Set bundler settings
|
||||
- ruby/install-deps:
|
||||
bundler-version: '2.3.8'
|
||||
key: ruby<< parameters.ruby-version >>-gems-v1
|
||||
wait-db:
|
||||
steps:
|
||||
- run:
|
||||
command: dockerize -wait tcp://localhost:5432 -wait tcp://localhost:6379 -timeout 1m
|
||||
name: Wait for PostgreSQL and Redis
|
||||
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: cimg/ruby:3.0-node
|
||||
environment:
|
||||
RAILS_ENV: test
|
||||
steps:
|
||||
- checkout
|
||||
- install-system-dependencies
|
||||
- install-ruby-dependencies:
|
||||
ruby-version: '3.0'
|
||||
- node/install-packages:
|
||||
cache-version: v1
|
||||
pkg-manager: yarn
|
||||
- run:
|
||||
command: ./bin/rails assets:precompile
|
||||
name: Precompile assets
|
||||
- persist_to_workspace:
|
||||
paths:
|
||||
- public/assets
|
||||
- public/packs-test
|
||||
root: .
|
||||
|
||||
test:
|
||||
parameters:
|
||||
ruby-version:
|
||||
type: string
|
||||
executor:
|
||||
name: default
|
||||
ruby-version: << parameters.ruby-version >>
|
||||
environment:
|
||||
ALLOW_NOPAM: true
|
||||
PAM_ENABLED: true
|
||||
PAM_DEFAULT_SERVICE: pam_test
|
||||
PAM_CONTROLLED_SERVICE: pam_test_controlled
|
||||
parallelism: 4
|
||||
steps:
|
||||
- checkout
|
||||
- install-system-dependencies
|
||||
- run:
|
||||
command: sudo apt-get install -y ffmpeg imagemagick libpam-dev
|
||||
name: Install additional system dependencies
|
||||
- run:
|
||||
command: bundle config with 'pam_authentication'
|
||||
name: Enable PAM authentication
|
||||
- install-ruby-dependencies:
|
||||
ruby-version: << parameters.ruby-version >>
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- wait-db
|
||||
- run:
|
||||
command: ./bin/rails db:create db:schema:load db:seed
|
||||
name: Load database schema
|
||||
- ruby/rspec-test
|
||||
|
||||
test-migrations:
|
||||
executor:
|
||||
name: default
|
||||
ruby-version: '3.0'
|
||||
steps:
|
||||
- checkout
|
||||
- install-system-dependencies
|
||||
- install-ruby-dependencies:
|
||||
ruby-version: '3.0'
|
||||
- wait-db
|
||||
- run:
|
||||
command: ./bin/rails db:create
|
||||
name: Create database
|
||||
- run:
|
||||
command: ./bin/rails db:migrate VERSION=20171010025614
|
||||
name: Run migrations up to v2.0.0
|
||||
- run:
|
||||
command: ./bin/rails tests:migrations:populate_v2
|
||||
name: Populate database with test data
|
||||
- run:
|
||||
command: ./bin/rails db:migrate VERSION=20180514140000
|
||||
name: Run migrations up to v2.4.0
|
||||
- run:
|
||||
command: ./bin/rails tests:migrations:populate_v2_4
|
||||
name: Populate database with test data
|
||||
- run:
|
||||
command: ./bin/rails db:migrate
|
||||
name: Run all remaining migrations
|
||||
- run:
|
||||
command: ./bin/rails tests:migrations:check_database
|
||||
name: Check migration result
|
||||
|
||||
test-two-step-migrations:
|
||||
executor:
|
||||
name: default
|
||||
ruby-version: '3.0'
|
||||
steps:
|
||||
- checkout
|
||||
- install-system-dependencies
|
||||
- install-ruby-dependencies:
|
||||
ruby-version: '3.0'
|
||||
- wait-db
|
||||
- run:
|
||||
command: ./bin/rails db:create
|
||||
name: Create database
|
||||
- run:
|
||||
command: ./bin/rails db:migrate VERSION=20171010025614
|
||||
name: Run migrations up to v2.0.0
|
||||
- run:
|
||||
command: ./bin/rails tests:migrations:populate_v2
|
||||
name: Populate database with test data
|
||||
- run:
|
||||
command: ./bin/rails db:migrate VERSION=20180514140000
|
||||
name: Run pre-deployment migrations up to v2.4.0
|
||||
environment:
|
||||
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
|
||||
- run:
|
||||
command: ./bin/rails tests:migrations:populate_v2_4
|
||||
name: Populate database with test data
|
||||
- run:
|
||||
command: ./bin/rails db:migrate
|
||||
name: Run all pre-deployment migrations
|
||||
environment:
|
||||
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
|
||||
- run:
|
||||
command: ./bin/rails db:migrate
|
||||
name: Run all post-deployment remaining migrations
|
||||
- run:
|
||||
command: ./bin/rails tests:migrations:check_database
|
||||
name: Check migration result
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build-and-test:
|
||||
jobs:
|
||||
- build
|
||||
- test:
|
||||
matrix:
|
||||
parameters:
|
||||
ruby-version:
|
||||
- '2.7'
|
||||
- '3.0'
|
||||
name: test-ruby<< matrix.ruby-version >>
|
||||
requires:
|
||||
- build
|
||||
- test-migrations:
|
||||
requires:
|
||||
- build
|
||||
- test-two-step-migrations:
|
||||
requires:
|
||||
- build
|
||||
- node/run:
|
||||
cache-version: v1
|
||||
name: test-webui
|
||||
pkg-manager: yarn
|
||||
requires:
|
||||
- build
|
||||
version: lts
|
||||
yarn-run: test:jest
|
|
@ -0,0 +1,92 @@
|
|||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
platforms:
|
||||
required: true
|
||||
type: string
|
||||
cache:
|
||||
type: boolean
|
||||
default: true
|
||||
use_native_arm64_builder:
|
||||
type: boolean
|
||||
push_to_images:
|
||||
type: string
|
||||
flavor:
|
||||
type: string
|
||||
tags:
|
||||
type: string
|
||||
labels:
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build-image:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: docker/setup-qemu-action@v2
|
||||
if: contains(inputs.platforms, 'linux/arm64') && !inputs.use_native_arm64_builder
|
||||
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
id: buildx
|
||||
if: ${{ !(inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')) }}
|
||||
|
||||
- name: Start a local Docker Builder
|
||||
if: inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')
|
||||
run: |
|
||||
docker run --rm -d --name buildkitd -p 1234:1234 --privileged moby/buildkit:latest --addr tcp://0.0.0.0:1234
|
||||
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
id: buildx-native
|
||||
if: inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')
|
||||
with:
|
||||
driver: remote
|
||||
endpoint: tcp://localhost:1234
|
||||
platforms: linux/amd64
|
||||
append: |
|
||||
- endpoint: tcp://${{ vars.DOCKER_BUILDER_HETZNER_ARM64_01_HOST }}:13865
|
||||
platforms: linux/arm64
|
||||
name: mastodon-docker-builder-arm64-01
|
||||
driver-opts:
|
||||
- servername=mastodon-docker-builder-arm64-01
|
||||
env:
|
||||
BUILDER_NODE_1_AUTH_TLS_CACERT: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_CACERT }}
|
||||
BUILDER_NODE_1_AUTH_TLS_CERT: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_CERT }}
|
||||
BUILDER_NODE_1_AUTH_TLS_KEY: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_KEY }}
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
if: contains(inputs.push_to_images, 'tootsuite')
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Log in to the Github Container registry
|
||||
if: contains(inputs.push_to_images, 'ghcr.io')
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: docker/metadata-action@v4
|
||||
id: meta
|
||||
if: ${{ inputs.push_to_images != '' }}
|
||||
with:
|
||||
images: ${{ inputs.push_to_images }}
|
||||
flavor: ${{ inputs.flavor }}
|
||||
tags: ${{ inputs.tags }}
|
||||
labels: ${{ inputs.labels }}
|
||||
|
||||
- uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ inputs.platforms }}
|
||||
provenance: false
|
||||
builder: ${{ steps.buildx.outputs.name || steps.buildx-native.outputs.name }}
|
||||
push: ${{ inputs.push_to_images != '' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: ${{ inputs.cache && 'type=gha' || '' }}
|
||||
cache-to: ${{ inputs.cache && 'type=gha,mode=max' || '' }}
|
|
@ -1,42 +0,0 @@
|
|||
name: Build container image
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
tags:
|
||||
- '*'
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/build-image.yml
|
||||
- Dockerfile
|
||||
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
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
if: github.event_name != 'pull_request'
|
||||
- uses: docker/metadata-action@v3
|
||||
id: meta
|
||||
with:
|
||||
images: tootsuite/mastodon
|
||||
flavor: |
|
||||
latest=auto
|
||||
tags: |
|
||||
type=edge,branch=main
|
||||
type=match,pattern=v(.*),group=0
|
||||
type=ref,event=pr
|
||||
- uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
cache-from: type=registry,ref=tootsuite/mastodon:latest
|
||||
cache-to: type=inline
|
|
@ -0,0 +1,27 @@
|
|||
name: Build container release images
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build-image:
|
||||
uses: ./.github/workflows/build-container-image.yml
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
use_native_arm64_builder: true
|
||||
push_to_images: |
|
||||
tootsuite/mastodon
|
||||
ghcr.io/mastodon/mastodon
|
||||
# Do not use cache when building releases, so apt update is always ran and the release always contain the latest packages
|
||||
cache: false
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
secrets: inherit
|
|
@ -0,0 +1,15 @@
|
|||
name: Test container image build
|
||||
on:
|
||||
pull_request:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-image:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
uses: ./.github/workflows/build-container-image.yml
|
||||
with:
|
||||
platforms: linux/amd64 # Testing only on native platform so it is performant
|
|
@ -1 +1 @@
|
|||
3.0.3
|
||||
3.0.6
|
||||
|
|
235
CHANGELOG.md
235
CHANGELOG.md
|
@ -3,6 +3,241 @@ Changelog
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## End of life notice
|
||||
|
||||
**The 3.5.x branch has reached its end of life and will not receive any further update.**
|
||||
This means that no security fix will be made available for this branch after this date, and you will need to update to a more recent version (such as the 4.2.x branch) to receive security fixes.
|
||||
|
||||
## [3.5.18] - 2024-02-14
|
||||
|
||||
### Security
|
||||
|
||||
- Update the `sidekiq-unique-jobs` dependency (see [GHSA-cmh9-rx85-xj38](https://github.com/mhenrixon/sidekiq-unique-jobs/security/advisories/GHSA-cmh9-rx85-xj38))
|
||||
In addition, we have disabled the web interface for `sidekiq-unique-jobs` out of caution.
|
||||
If you need it, you can re-enable it by setting `ENABLE_SIDEKIQ_UNIQUE_JOBS_UI=true`.
|
||||
If you only need to clear all locks, you can now use `bundle exec rake sidekiq_unique_jobs:delete_all_locks`.
|
||||
- Update the `nokogiri` dependency (see [GHSA-xc9x-jj77-9p9j](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-xc9x-jj77-9p9j))
|
||||
- Disable administrative Doorkeeper routes ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/29187))
|
||||
- Fix ongoing streaming sessions not being invalidated when applications get deleted in some cases ([GHSA-7w3c-p9j8-mq3x](https://github.com/mastodon/mastodon/security/advisories/GHSA-7w3c-p9j8-mq3x))
|
||||
In some rare cases, the streaming server was not notified of access tokens revocation on application deletion.
|
||||
- Change external authentication behavior to never reattach a new identity to an existing user by default ([GHSA-vm39-j3vx-pch3](https://github.com/mastodon/mastodon/security/advisories/GHSA-vm39-j3vx-pch3))
|
||||
Up until now, Mastodon has allowed new identities from external authentication providers to attach to an existing local user based on their verified e-mail address.
|
||||
This allowed upgrading users from a database-stored password to an external authentication provider, or move from one authentication provider to another.
|
||||
However, this behavior may be unexpected, and means that when multiple authentication providers are configured, the overall security would be that of the least secure authentication provider.
|
||||
For these reasons, this behavior is now locked under the `ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH` environment variable.
|
||||
In addition, regardless of this environment variable, Mastodon will refuse to attach two identities from the same authentication provider to the same account.
|
||||
|
||||
## [3.5.17] - 2024-02-01
|
||||
|
||||
### Security
|
||||
|
||||
- Fix insufficient origin validation (CVE-2024-23832, [GHSA-3fjr-858r-92rw](https://github.com/mastodon/mastodon/security/advisories/GHSA-3fjr-858r-92rw))
|
||||
|
||||
## [3.5.16] - 2023-12-04
|
||||
|
||||
### Changed
|
||||
|
||||
- Change GIF max matrix size error to explicitly mention GIF files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27927))
|
||||
- Change `Follow` activities delivery to bypass availability check ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/27586))
|
||||
- Change Content-Security-Policy to be tighter on media paths ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26889))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix incoming status creation date not being restricted to standard ISO8601 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27655), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28081))
|
||||
- Fix posts from force-sensitized accounts being able to trend ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27620))
|
||||
- Fix processing LDSigned activities from actors with unknown public keys ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27474))
|
||||
|
||||
## [3.5.15] - 2023-10-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Change user archive export allowed period from 7 days to 6 days ([suddjian](https://github.com/mastodon/mastodon/pull/27200))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix mentions being matched in some URL query strings ([mjankowski](https://github.com/mastodon/mastodon/pull/25656))
|
||||
- Fix importer returning negative row estimates ([jgillich](https://github.com/mastodon/mastodon/pull/27258))
|
||||
- Fix filtering audit log for entries about disabling 2FA ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27186))
|
||||
- Fix tIME chunk not being properly removed from PNG uploads ([TheEssem](https://github.com/mastodon/mastodon/pull/27111))
|
||||
- Fix inefficient queries in “Follows and followers” as well as several admin pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27116), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27306))
|
||||
|
||||
## [3.5.14] - 2023-09-19
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix moderator rights inconsistencies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26729))
|
||||
- Fix crash when encountering invalid URL ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26814))
|
||||
- Fix cached posts including stale stats ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26409))
|
||||
- Fix uploading of video files for which `ffprobe` reports `0/0` average framerate ([NicolaiSoeborg](https://github.com/mastodon/mastodon/pull/26500))
|
||||
- Fix unexpected audio stream transcoding when uploaded video is eligible to passthrough ([yufushiro](https://github.com/mastodon/mastodon/pull/26608))
|
||||
|
||||
### Security
|
||||
|
||||
- Fix incorrect domain name normalization (CVE-2023-42451)
|
||||
|
||||
## [3.5.13] - 2023-09-05
|
||||
|
||||
### Changed
|
||||
|
||||
- Change remote report processing to accept reports with long comments, but truncate them ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25028))
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Fix blocking subdomains of an already-blocked domain** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26392))
|
||||
- Fix `/api/v1/timelines/tag/:hashtag` allowing for unauthenticated access when public preview is disabled ([danielmbrasil](https://github.com/mastodon/mastodon/pull/26237))
|
||||
- Fix inefficiencies in `PlainTextFormatter` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26727))
|
||||
|
||||
## [3.5.12] - 2023-07-31
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix memory leak in streaming server ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26228))
|
||||
- Fix incorrect connect timeout in outgoing requests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26116))
|
||||
|
||||
## [3.5.11] - 2023-07-21
|
||||
|
||||
### Added
|
||||
|
||||
- Add check preventing Sidekiq workers from running with Makara configured ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25850))
|
||||
|
||||
### Changed
|
||||
|
||||
- Change request timeout handling to use a longer deadline ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26055))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix moderation interface for remote instances with a .zip TLD ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25886))
|
||||
- Fix remote accounts being possibly persisted to database with incomplete protocol values ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25886))
|
||||
- Fix trending publishers table not rendering correctly on narrow screens ([vmstan](https://github.com/mastodon/mastodon/pull/25945))
|
||||
|
||||
### Security
|
||||
|
||||
- Fix CSP headers being unintentionally wide ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26105))
|
||||
|
||||
## [3.5.10] - 2023-07-07
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix crash in admin interface when viewing a remote user with verified links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25796))
|
||||
- Fix processing of media files with unusual names ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25788))
|
||||
|
||||
## [3.5.9] - 2023-07-06
|
||||
|
||||
### Changed
|
||||
|
||||
- Change OpenGraph-based embeds to allow fullscreen ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25058))
|
||||
- Change profile updates to be sent to recently-mentioned servers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24852))
|
||||
- Change auto-linking to allow carets in URL query params ([renchap](https://github.com/mastodon/mastodon/pull/25216))
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove invalid `X-Frame-Options: ALLOWALL` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25070))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix soft-deleted post cleanup scheduler overwhelming the streaming server ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25519))
|
||||
- Fix incorrect pagination headers in `/api/v2/admin/accounts` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25477))
|
||||
- Fix performance of streaming by parsing message JSON once ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25278), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25361))
|
||||
- Fix CSP headers when `S3_ALIAS_HOST` includes a path component ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25273))
|
||||
- Fix `tootctl accounts approve --number N` not aproving N earliest registrations ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24605))
|
||||
- Fix being able to vote on your own polls ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25015))
|
||||
- Fix race condition when reblogging a status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25016))
|
||||
- Fix “Authorized applications” inefficiently and incorrectly getting last use date ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25060))
|
||||
- Fix multiple N+1s in ConversationsController ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25134), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25399), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25499))
|
||||
- Fix user archive takeouts when using OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24431))
|
||||
- Fix inefficiencies in indexing content for search ([VyrCossont](https://github.com/mastodon/mastodon/pull/24285), [VyrCossont](https://github.com/mastodon/mastodon/pull/24342))
|
||||
|
||||
### Security
|
||||
|
||||
- Update dependencies
|
||||
- Add hardening headers for user-uploaded files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25756))
|
||||
- Fix verified links possibly hiding important parts of the URL (CVE-2023-36462)
|
||||
- Fix timeout handling of outbound HTTP requests (CVE-2023-36461)
|
||||
- Fix arbitrary file creation through media processing (CVE-2023-36460)
|
||||
- Fix possible XSS in preview cards (CVE-2023-36459)
|
||||
|
||||
## [3.5.8] - 2023-04-04
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix crash in `tootctl` commands making use of parallelization when Elasticsearch is enabled ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24182), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24377))
|
||||
- Fix crash in `db:setup` when Elasticsearch is enabled ([rrgeorge](https://github.com/mastodon/mastodon/pull/24302))
|
||||
- Fix user archive takeout when using OpenStack Swift or S3 providers with no ACL support ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24200))
|
||||
- Fix invalid/expired invites being processed on sign-up ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24337))
|
||||
|
||||
### Security
|
||||
|
||||
- Update Ruby to 3.0.6 due to ReDoS vulnerabilities ([saizai](https://github.com/mastodon/mastodon/pull/24332))
|
||||
- Fix unescaped user input in LDAP query ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24379))
|
||||
|
||||
# [3.5.7] - 2023-03-16
|
||||
|
||||
### Added
|
||||
|
||||
- Add `lang` attribute to native language names in language picker in Web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23749))
|
||||
- Add headers to outgoing mails to avoid auto-replies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23597))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix “Remove all followers from the selected domains” being more destructive than it claims ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23805))
|
||||
- Fix case-sensitive check for previously used hashtags in hashtag autocompletion ([deanveloper](https://github.com/mastodon/mastodon/pull/23526))
|
||||
- Fix inefficiency when searching accounts per username in admin interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23801))
|
||||
- Fix server error when failing to follow back followers from `/relationships` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23787))
|
||||
- Fix original account being unfollowed on migration before the follow request to the new account could be sent ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21957))
|
||||
- Fix pgBouncer resetting application name on every transaction ([Gargron](https://github.com/mastodon/mastodon/pull/23958))
|
||||
- Fix unconfirmed accounts being counted as active users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23803))
|
||||
- Fix `/api/v1/streaming` sub-paths not being redirected ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23988))
|
||||
- Fix drag'n'drop upload area text that spans multiple lines not being centered ([vintprox](https://github.com/mastodon/mastodon/pull/24029))
|
||||
- Fix sidekiq jobs not triggering Elasticsearch index updates ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24046))
|
||||
- Fix missing null check on applications on strike disputes ([kescherCode](https://github.com/mastodon/mastodon/pull/19851))
|
||||
- Fix dashboard crash on ElasticSearch server error ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23751))
|
||||
- Fix incorrect post links in strikes when the account is remote ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23611))
|
||||
- Fix misleading error code when receiving invalid WebAuthn credentials ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23568))
|
||||
|
||||
### Security
|
||||
|
||||
- Change user backups to use expiring URLs for download when possible ([Gargron](https://github.com/mastodon/mastodon/pull/24136))
|
||||
- Add warning for object storage misconfiguration ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24137))
|
||||
|
||||
## [3.5.6] - 2023-02-09
|
||||
### Fixed
|
||||
|
||||
- **Fix changing domain block severity not undoing individual account effects** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23480))
|
||||
- Fix suspension worker crashing on S3-compatible setups without ACL support ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23481))
|
||||
- Fix possible race conditions when suspending/unsuspending accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23482))
|
||||
- Fix some performance issues with `/admin/instances` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23483))
|
||||
- Fix voter count not being cleared when a poll is reset ([afontenot](https://github.com/mastodon/mastodon/pull/23484))
|
||||
- Fix attachments of edited statuses not being fetched ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23485))
|
||||
- Fix 500 error when marking posts as sensitive while some of them are deleted ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23486))
|
||||
- Fix user clean-up scheduler crash when an unconfirmed account has a moderation note ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23487))
|
||||
- Fix pending account approval and rejection not being recorded in the admin audit log ([FrancisMurillo](https://github.com/mastodon/mastodon/pull/23488))
|
||||
- Fix replies sometimes being delivered to user-blocked domains ([tribela](https://github.com/mastodon/mastodon/pull/23490))
|
||||
- Fix sanitizer parsing link text as HTML when stripping unsupported links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23491))
|
||||
- Fix REST API serializer for `Account` not including `moved` when the moved account has itself moved ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23492))
|
||||
|
||||
### Security
|
||||
|
||||
- Add `form-action` CSP directive ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23478))
|
||||
- Fix unbounded recursion in account discovery ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22026))
|
||||
- Fix unbounded recursion in post discovery ([ClearlyClaire,nametoolong](https://github.com/mastodon/mastodon/pull/23507))
|
||||
|
||||
## [3.5.5] - 2022-11-14
|
||||
## Fixed
|
||||
|
||||
- Fix nodes order being sometimes mangled when rewriting emoji ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20677))
|
||||
|
||||
## [3.5.4] - 2022-11-14
|
||||
### Fixed
|
||||
|
||||
- Fix error when a remote report includes a private post the server has no access to ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18760))
|
||||
|
||||
### Security
|
||||
|
||||
- Fix emoji substitution not applying only to text nodes in backend code ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20641))
|
||||
- Fix emoji substitution not applying only to text nodes in Web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20640))
|
||||
- Fix rate limiting for paths with formats ([Gargron](https://github.com/mastodon/mastodon/pull/20675))
|
||||
- Fix out-of-bound reads in blurhash transcoder ([delroth](https://github.com/mastodon/mastodon/pull/20388))
|
||||
|
||||
## [3.5.3] - 2022-05-26
|
||||
### Added
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ RUN ARCH= && \
|
|||
esac && \
|
||||
echo "Etc/UTC" > /etc/localtime && \
|
||||
apt-get update && \
|
||||
apt-get -yq dist-upgrade && \
|
||||
apt-get install -y --no-install-recommends ca-certificates wget python apt-utils && \
|
||||
cd ~ && \
|
||||
wget -q https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER-linux-$ARCH.tar.gz && \
|
||||
|
@ -27,7 +28,7 @@ RUN ARCH= && \
|
|||
mv node-v$NODE_VER-linux-$ARCH /opt/node
|
||||
|
||||
# Install Ruby 3.0
|
||||
ENV RUBY_VER="3.0.3"
|
||||
ENV RUBY_VER="3.0.6"
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends build-essential \
|
||||
bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \
|
||||
|
@ -46,7 +47,7 @@ RUN apt-get update && \
|
|||
|
||||
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin"
|
||||
|
||||
RUN npm install -g npm@latest && \
|
||||
RUN npm install -g npm@9 && \
|
||||
npm install -g yarn && \
|
||||
gem install bundler && \
|
||||
apt-get update && \
|
||||
|
|
1
Gemfile
1
Gemfile
|
@ -66,6 +66,7 @@ gem 'oj', '~> 3.13'
|
|||
gem 'ox', '~> 2.14'
|
||||
gem 'parslet'
|
||||
gem 'posix-spawn'
|
||||
gem 'public_suffix', '~> 4.0.7'
|
||||
gem 'pundit', '~> 2.2'
|
||||
gem 'premailer-rails'
|
||||
gem 'rack-attack', '~> 6.6'
|
||||
|
|
155
Gemfile.lock
155
Gemfile.lock
|
@ -1,40 +1,40 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (6.1.6)
|
||||
actionpack (= 6.1.6)
|
||||
activesupport (= 6.1.6)
|
||||
actioncable (6.1.7.4)
|
||||
actionpack (= 6.1.7.4)
|
||||
activesupport (= 6.1.7.4)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.1.6)
|
||||
actionpack (= 6.1.6)
|
||||
activejob (= 6.1.6)
|
||||
activerecord (= 6.1.6)
|
||||
activestorage (= 6.1.6)
|
||||
activesupport (= 6.1.6)
|
||||
actionmailbox (6.1.7.4)
|
||||
actionpack (= 6.1.7.4)
|
||||
activejob (= 6.1.7.4)
|
||||
activerecord (= 6.1.7.4)
|
||||
activestorage (= 6.1.7.4)
|
||||
activesupport (= 6.1.7.4)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.1.6)
|
||||
actionpack (= 6.1.6)
|
||||
actionview (= 6.1.6)
|
||||
activejob (= 6.1.6)
|
||||
activesupport (= 6.1.6)
|
||||
actionmailer (6.1.7.4)
|
||||
actionpack (= 6.1.7.4)
|
||||
actionview (= 6.1.7.4)
|
||||
activejob (= 6.1.7.4)
|
||||
activesupport (= 6.1.7.4)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.1.6)
|
||||
actionview (= 6.1.6)
|
||||
activesupport (= 6.1.6)
|
||||
actionpack (6.1.7.4)
|
||||
actionview (= 6.1.7.4)
|
||||
activesupport (= 6.1.7.4)
|
||||
rack (~> 2.0, >= 2.0.9)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (6.1.6)
|
||||
actionpack (= 6.1.6)
|
||||
activerecord (= 6.1.6)
|
||||
activestorage (= 6.1.6)
|
||||
activesupport (= 6.1.6)
|
||||
actiontext (6.1.7.4)
|
||||
actionpack (= 6.1.7.4)
|
||||
activerecord (= 6.1.7.4)
|
||||
activestorage (= 6.1.7.4)
|
||||
activesupport (= 6.1.7.4)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.1.6)
|
||||
activesupport (= 6.1.6)
|
||||
actionview (6.1.7.4)
|
||||
activesupport (= 6.1.7.4)
|
||||
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.6)
|
||||
activesupport (= 6.1.6)
|
||||
activejob (6.1.7.4)
|
||||
activesupport (= 6.1.7.4)
|
||||
globalid (>= 0.3.6)
|
||||
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)
|
||||
activemodel (6.1.7.4)
|
||||
activesupport (= 6.1.7.4)
|
||||
activerecord (6.1.7.4)
|
||||
activemodel (= 6.1.7.4)
|
||||
activesupport (= 6.1.7.4)
|
||||
activestorage (6.1.7.4)
|
||||
actionpack (= 6.1.7.4)
|
||||
activejob (= 6.1.7.4)
|
||||
activerecord (= 6.1.7.4)
|
||||
activesupport (= 6.1.7.4)
|
||||
marcel (~> 1.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (6.1.6)
|
||||
activesupport (6.1.7.4)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
|
@ -165,7 +165,7 @@ GEM
|
|||
climate_control (0.2.0)
|
||||
coderay (1.1.3)
|
||||
color_diff (0.1)
|
||||
concurrent-ruby (1.1.10)
|
||||
concurrent-ruby (1.2.2)
|
||||
connection_pool (2.2.5)
|
||||
cose (1.0.0)
|
||||
cbor (~> 0.5.9)
|
||||
|
@ -197,7 +197,7 @@ GEM
|
|||
docile (1.3.4)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
doorkeeper (5.5.4)
|
||||
doorkeeper (5.6.6)
|
||||
railties (>= 5)
|
||||
dotenv (2.7.6)
|
||||
dotenv-rails (2.7.6)
|
||||
|
@ -214,7 +214,7 @@ GEM
|
|||
faraday (~> 1)
|
||||
multi_json
|
||||
encryptor (3.0.0)
|
||||
erubi (1.10.0)
|
||||
erubi (1.12.0)
|
||||
et-orbi (1.2.7)
|
||||
tzinfo
|
||||
excon (0.76.0)
|
||||
|
@ -273,7 +273,7 @@ GEM
|
|||
addressable (~> 2.7)
|
||||
omniauth (~> 1.9)
|
||||
openid_connect (~> 1.2)
|
||||
globalid (1.0.0)
|
||||
globalid (1.0.1)
|
||||
activesupport (>= 5.0)
|
||||
hamlit (2.13.0)
|
||||
temple (>= 0.8.2)
|
||||
|
@ -304,7 +304,7 @@ GEM
|
|||
httplog (1.5.0)
|
||||
rack (>= 1.0)
|
||||
rainbow (>= 2.0.0)
|
||||
i18n (1.10.0)
|
||||
i18n (1.14.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-tasks (1.0.10)
|
||||
activesupport (>= 4.0.2)
|
||||
|
@ -374,9 +374,9 @@ GEM
|
|||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.18.0)
|
||||
loofah (2.21.3)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.7.1)
|
||||
mini_mime (>= 0.1.1)
|
||||
makara (0.5.1)
|
||||
|
@ -394,8 +394,8 @@ GEM
|
|||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2022.0105)
|
||||
mini_mime (1.1.2)
|
||||
mini_portile2 (2.8.0)
|
||||
minitest (5.15.0)
|
||||
mini_portile2 (2.8.5)
|
||||
minitest (5.18.1)
|
||||
msgpack (1.5.1)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.1.1)
|
||||
|
@ -403,9 +403,9 @@ GEM
|
|||
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.6)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
nio4r (2.5.9)
|
||||
nokogiri (1.16.2)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nsa (0.2.8)
|
||||
activesupport (>= 4.2, < 7)
|
||||
|
@ -413,7 +413,7 @@ GEM
|
|||
sidekiq (>= 3.5)
|
||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||
oj (3.13.11)
|
||||
omniauth (1.9.1)
|
||||
omniauth (1.9.2)
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 1.6.2, < 3)
|
||||
omniauth-cas (2.0.0)
|
||||
|
@ -473,8 +473,8 @@ GEM
|
|||
pundit (2.2.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.6.0)
|
||||
rack (2.2.3)
|
||||
racc (1.7.3)
|
||||
rack (2.2.7)
|
||||
rack-attack (6.6.1)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-cors (1.1.1)
|
||||
|
@ -489,20 +489,20 @@ GEM
|
|||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
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)
|
||||
rails (6.1.7.4)
|
||||
actioncable (= 6.1.7.4)
|
||||
actionmailbox (= 6.1.7.4)
|
||||
actionmailer (= 6.1.7.4)
|
||||
actionpack (= 6.1.7.4)
|
||||
actiontext (= 6.1.7.4)
|
||||
actionview (= 6.1.7.4)
|
||||
activejob (= 6.1.7.4)
|
||||
activemodel (= 6.1.7.4)
|
||||
activerecord (= 6.1.7.4)
|
||||
activestorage (= 6.1.7.4)
|
||||
activesupport (= 6.1.7.4)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 6.1.6)
|
||||
railties (= 6.1.7.4)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
|
@ -511,16 +511,17 @@ GEM
|
|||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.4.2)
|
||||
loofah (~> 2.3)
|
||||
rails-html-sanitizer (1.6.0)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
rails-i18n (6.0.0)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 6.0.0, < 7)
|
||||
rails-settings-cached (0.6.6)
|
||||
rails (>= 4.2.0)
|
||||
railties (6.1.6)
|
||||
actionpack (= 6.1.6)
|
||||
activesupport (= 6.1.6)
|
||||
railties (6.1.7.4)
|
||||
actionpack (= 6.1.7.4)
|
||||
activesupport (= 6.1.7.4)
|
||||
method_source
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
|
@ -592,7 +593,7 @@ GEM
|
|||
fugit (~> 1.1, >= 1.1.6)
|
||||
safety_net_attestation (0.4.0)
|
||||
jwt (~> 2.0)
|
||||
sanitize (6.0.0)
|
||||
sanitize (6.0.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
scenic (1.6.0)
|
||||
|
@ -611,10 +612,11 @@ GEM
|
|||
rufus-scheduler (~> 3.2)
|
||||
sidekiq (>= 4)
|
||||
tilt (>= 1.4.0)
|
||||
sidekiq-unique-jobs (7.1.22)
|
||||
sidekiq-unique-jobs (7.1.33)
|
||||
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||
sidekiq (>= 5.0, < 8.0)
|
||||
redis (< 5.0)
|
||||
sidekiq (>= 5.0, < 7.0)
|
||||
thor (>= 0.20, < 3.0)
|
||||
simple-navigation (4.3.0)
|
||||
activesupport (>= 2.3.2)
|
||||
|
@ -652,7 +654,7 @@ GEM
|
|||
unicode-display_width (>= 1.1.1, < 3)
|
||||
terrapin (0.6.0)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
thor (1.2.1)
|
||||
thor (1.2.2)
|
||||
tilt (2.0.10)
|
||||
tpm-key_attestation (0.9.0)
|
||||
bindata (~> 2.4)
|
||||
|
@ -670,7 +672,7 @@ GEM
|
|||
twitter-text (3.1.0)
|
||||
idn-ruby
|
||||
unf (~> 0.1.0)
|
||||
tzinfo (2.0.4)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2022.1)
|
||||
tzinfo (>= 1.0.0)
|
||||
|
@ -719,7 +721,7 @@ GEM
|
|||
xorcist (1.1.2)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.5.4)
|
||||
zeitwerk (2.6.8)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
@ -803,6 +805,7 @@ DEPENDENCIES
|
|||
private_address_check (~> 0.5)
|
||||
pry-byebug (~> 3.9)
|
||||
pry-rails (~> 0.3)
|
||||
public_suffix (~> 4.0.7)
|
||||
puma (~> 5.6)
|
||||
pundit (~> 2.2)
|
||||
rack (~> 2.2.3)
|
||||
|
|
|
@ -5,13 +5,11 @@
|
|||
[![Build Status](https://img.shields.io/circleci/project/github/mastodon/mastodon.svg)][circleci]
|
||||
[![Code Climate](https://img.shields.io/codeclimate/maintainability/mastodon/mastodon.svg)][code_climate]
|
||||
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)][crowdin]
|
||||
[![Docker Pulls](https://img.shields.io/docker/pulls/tootsuite/mastodon.svg)][docker]
|
||||
|
||||
[releases]: https://github.com/mastodon/mastodon/releases
|
||||
[circleci]: https://circleci.com/gh/mastodon/mastodon
|
||||
[code_climate]: https://codeclimate.com/github/mastodon/mastodon
|
||||
[crowdin]: https://crowdin.com/project/mastodon
|
||||
[docker]: https://hub.docker.com/r/tootsuite/mastodon/
|
||||
|
||||
Mastodon is a **free, open-source social network server** based on ActivityPub where users can follow friends and discover new ones. On Mastodon, users can publish anything they want: links, pictures, text, video. All Mastodon servers are interoperable as a federated network (users on one server can seamlessly communicate with users from another one, including non-Mastodon software that implements ActivityPub)!
|
||||
|
||||
|
@ -28,6 +26,7 @@ Click below to **learn more** in a video:
|
|||
- [View sponsors](https://joinmastodon.org/sponsors)
|
||||
- [Blog](https://blog.joinmastodon.org)
|
||||
- [Documentation](https://docs.joinmastodon.org)
|
||||
- [Official Docker image](https://github.com/mastodon/mastodon/pkgs/container/mastodon)
|
||||
- [Browse Mastodon servers](https://joinmastodon.org/communities)
|
||||
- [Browse Mastodon apps](https://joinmastodon.org/apps)
|
||||
|
||||
|
|
13
SECURITY.md
13
SECURITY.md
|
@ -11,10 +11,9 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
|
|||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 3.5.x | Yes |
|
||||
| 3.4.x | Yes |
|
||||
| 3.3.x | No |
|
||||
| < 3.3 | No |
|
||||
|
||||
[bug-bounty]: https://app.intigriti.com/programs/mastodon/mastodonio/detail
|
||||
| ------- | ---------------- |
|
||||
| 4.2.x | Yes |
|
||||
| 4.1.x | Yes |
|
||||
| 4.0.x | No |
|
||||
| 3.5.x | No |
|
||||
| < 3.5 | No |
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AccountsIndex < Chewy::Index
|
||||
include DatetimeClampingConcern
|
||||
|
||||
settings index: { refresh_interval: '30s' }, analysis: {
|
||||
analyzer: {
|
||||
content: {
|
||||
|
@ -38,6 +40,6 @@ class AccountsIndex < Chewy::Index
|
|||
|
||||
field :following_count, type: 'long', value: ->(account) { account.following_count }
|
||||
field :followers_count, type: 'long', value: ->(account) { account.followers_count }
|
||||
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
|
||||
field :last_status_at, type: 'date', value: ->(account) { clamp_date(account.last_status_at || account.created_at) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module DatetimeClampingConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
MIN_ISO8601_DATETIME = '0000-01-01T00:00:00Z'.to_datetime.freeze
|
||||
MAX_ISO8601_DATETIME = '9999-12-31T23:59:59Z'.to_datetime.freeze
|
||||
|
||||
class_methods do
|
||||
def clamp_date(datetime)
|
||||
datetime.clamp(MIN_ISO8601_DATETIME, MAX_ISO8601_DATETIME)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class TagsIndex < Chewy::Index
|
||||
include DatetimeClampingConcern
|
||||
|
||||
settings index: { refresh_interval: '30s' }, analysis: {
|
||||
analyzer: {
|
||||
content: {
|
||||
|
@ -36,6 +38,6 @@ class TagsIndex < Chewy::Index
|
|||
|
||||
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
|
||||
field :usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts }
|
||||
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }
|
||||
field :last_status_at, type: 'date', value: ->(tag) { clamp_date(tag.last_status_at || tag.created_at) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -49,12 +49,14 @@ module Admin
|
|||
def approve
|
||||
authorize @account.user, :approve?
|
||||
@account.user.approve!
|
||||
log_action :approve, @account.user
|
||||
redirect_to admin_accounts_path(status: 'pending'), notice: I18n.t('admin.accounts.approved_msg', username: @account.acct)
|
||||
end
|
||||
|
||||
def reject
|
||||
authorize @account.user, :reject?
|
||||
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
|
||||
log_action :reject, @account.user
|
||||
redirect_to admin_accounts_path(status: 'pending'), notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct)
|
||||
end
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ module Admin
|
|||
@domain_block.errors.delete(:domain)
|
||||
render :new
|
||||
else
|
||||
if existing_domain_block.present?
|
||||
if existing_domain_block.present? && existing_domain_block.domain == TagManager.instance.normalize_domain(@domain_block.domain.strip)
|
||||
@domain_block = existing_domain_block
|
||||
@domain_block.update(resource_params)
|
||||
end
|
||||
|
@ -43,12 +43,8 @@ module Admin
|
|||
def update
|
||||
authorize :domain_block, :update?
|
||||
|
||||
@domain_block.update(update_params)
|
||||
|
||||
severity_changed = @domain_block.severity_changed?
|
||||
|
||||
if @domain_block.save
|
||||
DomainBlockWorker.perform_async(@domain_block.id, severity_changed)
|
||||
if @domain_block.update(update_params)
|
||||
DomainBlockWorker.perform_async(@domain_block.id, @domain_block.severity_previously_changed?)
|
||||
log_action :update, @domain_block
|
||||
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
|
||||
else
|
||||
|
|
|
@ -57,7 +57,7 @@ module Admin
|
|||
end
|
||||
|
||||
def preload_delivery_failures!
|
||||
warning_domains_map = DeliveryFailureTracker.warning_domains_map
|
||||
warning_domains_map = DeliveryFailureTracker.warning_domains_map(@instances.map(&:domain))
|
||||
|
||||
@instances.each do |instance|
|
||||
instance.failure_days = warning_domains_map[instance.domain]
|
||||
|
|
|
@ -54,12 +54,14 @@ class Api::V1::Admin::AccountsController < Api::BaseController
|
|||
def approve
|
||||
authorize @account.user, :approve?
|
||||
@account.user.approve!
|
||||
log_action :approve, @account.user
|
||||
render json: @account, serializer: REST::Admin::AccountSerializer
|
||||
end
|
||||
|
||||
def reject
|
||||
authorize @account.user, :reject?
|
||||
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
|
||||
log_action :reject, @account.user
|
||||
render json: @account, serializer: REST::Admin::AccountSerializer
|
||||
end
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ class Api::V1::ConversationsController < Api::BaseController
|
|||
|
||||
def index
|
||||
@conversations = paginated_conversations
|
||||
render json: @conversations, each_serializer: REST::ConversationSerializer
|
||||
render json: @conversations, each_serializer: REST::ConversationSerializer, relationships: StatusRelationshipsPresenter.new(@conversations.map(&:last_status), current_user&.account_id)
|
||||
end
|
||||
|
||||
def read
|
||||
|
@ -32,6 +32,19 @@ class Api::V1::ConversationsController < Api::BaseController
|
|||
|
||||
def paginated_conversations
|
||||
AccountConversation.where(account: current_account)
|
||||
.includes(
|
||||
account: :account_stat,
|
||||
last_status: [
|
||||
:media_attachments,
|
||||
:preview_cards,
|
||||
:status_stat,
|
||||
:tags,
|
||||
{
|
||||
active_mentions: [account: :account_stat],
|
||||
account: :account_stat,
|
||||
},
|
||||
]
|
||||
)
|
||||
.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||
end
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
class Api::V1::Statuses::ReblogsController < Api::BaseController
|
||||
include Authorization
|
||||
include Redisable
|
||||
include Lockable
|
||||
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
|
||||
before_action :require_user!
|
||||
|
@ -10,7 +12,9 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
|
|||
override_rate_limit_headers :create, family: :statuses
|
||||
|
||||
def create
|
||||
with_lock("reblog:#{current_account.id}:#{@reblog.id}") do
|
||||
@status = ReblogService.new.call(current_account, @reblog, reblog_params)
|
||||
end
|
||||
|
||||
render json: @status, serializer: REST::StatusSerializer
|
||||
end
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Timelines::TagController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :show, if: :require_auth?
|
||||
before_action :load_tag
|
||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||
|
||||
|
@ -11,6 +12,10 @@ class Api::V1::Timelines::TagController < Api::BaseController
|
|||
|
||||
private
|
||||
|
||||
def require_auth?
|
||||
!Setting.timeline_preview
|
||||
end
|
||||
|
||||
def load_tag
|
||||
@tag = Tag.find_normalized(params[:id])
|
||||
end
|
||||
|
|
|
@ -17,6 +17,14 @@ class Api::V2::Admin::AccountsController < Api::V1::Admin::AccountsController
|
|||
|
||||
private
|
||||
|
||||
def next_path
|
||||
api_v2_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue?
|
||||
end
|
||||
|
||||
def prev_path
|
||||
api_v2_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty?
|
||||
end
|
||||
|
||||
def filtered_accounts
|
||||
AccountFilter.new(filter_params).results
|
||||
end
|
||||
|
|
|
@ -5,7 +5,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
|
||||
def self.provides_callback_for(provider)
|
||||
define_method provider do
|
||||
@user = User.find_for_oauth(request.env['omniauth.auth'], current_user)
|
||||
@user = User.find_for_omniauth(request.env['omniauth.auth'], current_user)
|
||||
|
||||
if @user.persisted?
|
||||
LoginActivity.create(
|
||||
|
|
|
@ -46,7 +46,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
super(hash)
|
||||
|
||||
resource.locale = I18n.locale
|
||||
resource.invite_code = params[:invite_code] if resource.invite_code.blank?
|
||||
resource.invite_code = @invite&.code if resource.invite_code.blank?
|
||||
resource.registration_form_time = session[:registration_form_time]
|
||||
resource.sign_up_ip = request.remote_ip
|
||||
|
||||
|
|
|
@ -12,6 +12,10 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
before_action :set_instance_presenter, only: [:new]
|
||||
before_action :set_body_classes
|
||||
|
||||
content_security_policy only: :new do |p|
|
||||
p.form_action(false)
|
||||
end
|
||||
|
||||
def create
|
||||
super do |resource|
|
||||
# We only need to call this if this hasn't already been
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BackupsController < ApplicationController
|
||||
include RoutingHelper
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_backup
|
||||
|
||||
def download
|
||||
case Paperclip::Attachment.default_options[:storage]
|
||||
when :s3
|
||||
redirect_to @backup.dump.expiring_url(10)
|
||||
when :fog
|
||||
if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present?
|
||||
redirect_to @backup.dump.expiring_url(Time.now.utc + 10)
|
||||
else
|
||||
redirect_to full_asset_url(@backup.dump.url)
|
||||
end
|
||||
when :filesystem
|
||||
redirect_to full_asset_url(@backup.dump.url)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_backup
|
||||
@backup = current_user.backups.find(params[:id])
|
||||
end
|
||||
end
|
|
@ -219,7 +219,7 @@ module SignatureVerification
|
|||
stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, '')) }
|
||||
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
|
||||
account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
|
||||
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false) }
|
||||
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id) }
|
||||
account
|
||||
end
|
||||
rescue Mastodon::HostValidationError
|
||||
|
|
|
@ -46,6 +46,6 @@ class MediaController < ApplicationController
|
|||
end
|
||||
|
||||
def allow_iframing
|
||||
response.headers['X-Frame-Options'] = 'ALLOWALL'
|
||||
response.headers.delete('X-Frame-Options')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,6 +7,10 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
|||
before_action :authenticate_resource_owner!
|
||||
before_action :set_cache_headers
|
||||
|
||||
content_security_policy do |p|
|
||||
p.form_action(false)
|
||||
end
|
||||
|
||||
include Localized
|
||||
|
||||
private
|
||||
|
|
|
@ -8,6 +8,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
|||
before_action :require_not_suspended!, only: :destroy
|
||||
before_action :set_body_classes
|
||||
|
||||
before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json }
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
include Localized
|
||||
|
@ -30,4 +32,14 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
|||
def require_not_suspended!
|
||||
forbidden if current_account.suspended?
|
||||
end
|
||||
|
||||
def set_last_used_at_by_app
|
||||
@last_used_at_by_app = Doorkeeper::AccessToken
|
||||
.select('DISTINCT ON (application_id) application_id, last_used_at')
|
||||
.where(resource_owner_id: current_resource_owner.id)
|
||||
.where.not(last_used_at: nil)
|
||||
.order(application_id: :desc, last_used_at: :desc)
|
||||
.pluck(:application_id, :last_used_at)
|
||||
.to_h
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,6 +19,8 @@ class RelationshipsController < ApplicationController
|
|||
@form.save
|
||||
rescue ActionController::ParameterMissing
|
||||
# Do nothing
|
||||
rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound
|
||||
flash[:alert] = I18n.t('relationships.follow_failure') if action_from_button == 'follow'
|
||||
ensure
|
||||
redirect_to relationships_path(filter_params)
|
||||
end
|
||||
|
@ -60,8 +62,8 @@ class RelationshipsController < ApplicationController
|
|||
'unfollow'
|
||||
elsif params[:remove_from_followers]
|
||||
'remove_from_followers'
|
||||
elsif params[:block_domains]
|
||||
'block_domains'
|
||||
elsif params[:block_domains] || params[:remove_domains_from_followers]
|
||||
'remove_domains_from_followers'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ module Settings
|
|||
end
|
||||
else
|
||||
flash[:error] = I18n.t('webauthn_credentials.create.error')
|
||||
status = :internal_server_error
|
||||
status = :unprocessable_entity
|
||||
end
|
||||
else
|
||||
flash[:error] = t('webauthn_credentials.create.error')
|
||||
|
|
|
@ -48,7 +48,7 @@ class StatusesController < ApplicationController
|
|||
return not_found if @status.hidden? || @status.reblog?
|
||||
|
||||
expires_in 180, public: true
|
||||
response.headers['X-Frame-Options'] = 'ALLOWALL'
|
||||
response.headers.delete('X-Frame-Options')
|
||||
|
||||
render layout: 'embedded'
|
||||
end
|
||||
|
|
|
@ -49,6 +49,10 @@ module FormattingHelper
|
|||
end
|
||||
|
||||
def account_field_value_format(field, with_rel_me: true)
|
||||
if field.verified? && !field.account.local?
|
||||
TextFormatter.shortened_link(field.value_for_verification)
|
||||
else
|
||||
html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -157,8 +157,8 @@ module JsonLdHelper
|
|||
end
|
||||
end
|
||||
|
||||
def fetch_resource(uri, id, on_behalf_of = nil)
|
||||
unless id
|
||||
def fetch_resource(uri, id_is_known, on_behalf_of = nil)
|
||||
unless id_is_known
|
||||
json = fetch_resource_without_id_validation(uri, on_behalf_of)
|
||||
|
||||
return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id'])
|
||||
|
|
|
@ -222,7 +222,7 @@ class LanguageDropdownMenu extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<div key={lang[0]} role='option' tabIndex='0' data-index={lang[0]} className={classNames('language-dropdown__dropdown__results__item', { active: lang[0] === value })} aria-selected={lang[0] === value} onClick={this.handleClick} onKeyDown={this.handleKeyDown}>
|
||||
<span className='language-dropdown__dropdown__results__item__native-name'>{lang[2]}</span> <span className='language-dropdown__dropdown__results__item__common-name'>({lang[1]})</span>
|
||||
<span className='language-dropdown__dropdown__results__item__native-name' lang={lang[0]}>{lang[2]}</span> <span className='language-dropdown__dropdown__results__item__common-name'>({lang[1]})</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@ describe('emoji', () => {
|
|||
});
|
||||
|
||||
it('works with unclosed tags', () => {
|
||||
expect(emojify('hello>')).toEqual('hello>');
|
||||
expect(emojify('<hello')).toEqual('<hello');
|
||||
expect(emojify('hello>')).toEqual('hello>');
|
||||
expect(emojify('<hello')).toEqual('');
|
||||
});
|
||||
|
||||
it('works with unclosed shortcodes', () => {
|
||||
|
@ -22,23 +22,23 @@ describe('emoji', () => {
|
|||
|
||||
it('does unicode', () => {
|
||||
expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual(
|
||||
'<img draggable="false" class="emojione" alt="👩👩👦👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg" />');
|
||||
'<img draggable="false" class="emojione" alt="👩👩👦👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg">');
|
||||
expect(emojify('👨👩👧👧')).toEqual(
|
||||
'<img draggable="false" class="emojione" alt="👨👩👧👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg" />');
|
||||
expect(emojify('👩👩👦')).toEqual('<img draggable="false" class="emojione" alt="👩👩👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg" />');
|
||||
'<img draggable="false" class="emojione" alt="👨👩👧👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg">');
|
||||
expect(emojify('👩👩👦')).toEqual('<img draggable="false" class="emojione" alt="👩👩👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg">');
|
||||
expect(emojify('\u2757')).toEqual(
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg">');
|
||||
});
|
||||
|
||||
it('does multiple unicode', () => {
|
||||
expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual(
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" />');
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg">');
|
||||
expect(emojify('\u2757#\uFE0F\u20E3')).toEqual(
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" />');
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg">');
|
||||
expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual(
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" /> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg">');
|
||||
expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual(
|
||||
'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" /> bar');
|
||||
'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"> bar');
|
||||
});
|
||||
|
||||
it('ignores unicode inside of tags', () => {
|
||||
|
@ -46,16 +46,16 @@ describe('emoji', () => {
|
|||
});
|
||||
|
||||
it('does multiple emoji properly (issue 5188)', () => {
|
||||
expect(emojify('👌🌈💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg" /><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg" /><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg" />');
|
||||
expect(emojify('👌 🌈 💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg" /> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg" /> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg" />');
|
||||
expect(emojify('👌🌈💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg">');
|
||||
expect(emojify('👌 🌈 💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg">');
|
||||
});
|
||||
|
||||
it('does an emoji that has no shortcode', () => {
|
||||
expect(emojify('👁🗨')).toEqual('<img draggable="false" class="emojione" alt="👁🗨" title="" src="/emoji/1f441-200d-1f5e8.svg" />');
|
||||
expect(emojify('👁🗨')).toEqual('<img draggable="false" class="emojione" alt="👁🗨" title="" src="/emoji/1f441-200d-1f5e8.svg">');
|
||||
});
|
||||
|
||||
it('does an emoji whose filename is irregular', () => {
|
||||
expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg" />');
|
||||
expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg">');
|
||||
});
|
||||
|
||||
it('avoid emojifying on invisible text', () => {
|
||||
|
@ -67,26 +67,26 @@ describe('emoji', () => {
|
|||
|
||||
it('avoid emojifying on invisible text with nested tags', () => {
|
||||
expect(emojify('<span class="invisible">😄<span class="foo">bar</span>😴</span>😇'))
|
||||
.toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
|
||||
.toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">');
|
||||
expect(emojify('<span class="invisible">😄<span class="invisible">😕</span>😴</span>😇'))
|
||||
.toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
|
||||
expect(emojify('<span class="invisible">😄<br/>😴</span>😇'))
|
||||
.toEqual('<span class="invisible">😄<br/>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
|
||||
.toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">');
|
||||
expect(emojify('<span class="invisible">😄<br>😴</span>😇'))
|
||||
.toEqual('<span class="invisible">😄<br>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">');
|
||||
});
|
||||
|
||||
it('skips the textual presentation VS15 character', () => {
|
||||
expect(emojify('✴︎')) // This is U+2734 EIGHT POINTED BLACK STAR then U+FE0E VARIATION SELECTOR-15
|
||||
.toEqual('<img draggable="false" class="emojione" alt="✴" title=":eight_pointed_black_star:" src="/emoji/2734_border.svg" />');
|
||||
.toEqual('<img draggable="false" class="emojione" alt="✴" title=":eight_pointed_black_star:" src="/emoji/2734_border.svg">');
|
||||
});
|
||||
|
||||
it('does an simple emoji properly', () => {
|
||||
expect(emojify('♀♂'))
|
||||
.toEqual('<img draggable="false" class="emojione" alt="♀" title=":female_sign:" src="/emoji/2640.svg" /><img draggable="false" class="emojione" alt="♂" title=":male_sign:" src="/emoji/2642.svg" />');
|
||||
.toEqual('<img draggable="false" class="emojione" alt="♀" title=":female_sign:" src="/emoji/2640.svg"><img draggable="false" class="emojione" alt="♂" title=":male_sign:" src="/emoji/2642.svg">');
|
||||
});
|
||||
|
||||
it('does an emoji containing ZWJ properly', () => {
|
||||
expect(emojify('💂♀️💂♂️'))
|
||||
.toEqual('<img draggable="false" class="emojione" alt="💂\u200D♀️" title=":female-guard:" src="/emoji/1f482-200d-2640-fe0f_border.svg" /><img draggable="false" class="emojione" alt="💂\u200D♂️" title=":male-guard:" src="/emoji/1f482-200d-2642-fe0f_border.svg" />');
|
||||
.toEqual('<img draggable="false" class="emojione" alt="💂\u200D♀️" title=":female-guard:" src="/emoji/1f482-200d-2640-fe0f_border.svg"><img draggable="false" class="emojione" alt="💂\u200D♂️" title=":male-guard:" src="/emoji/1f482-200d-2642-fe0f_border.svg">');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,15 +19,26 @@ const emojiFilename = (filename) => {
|
|||
return borderedEmoji.includes(filename) ? (filename + '_border') : filename;
|
||||
};
|
||||
|
||||
const emojify = (str, customEmojis = {}) => {
|
||||
const tagCharsWithoutEmojis = '<&';
|
||||
const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&';
|
||||
let rtn = '', tagChars = tagCharsWithEmojis, invisible = 0;
|
||||
const domParser = new DOMParser();
|
||||
|
||||
const emojifyTextNode = (node, customEmojis) => {
|
||||
let str = node.textContent;
|
||||
|
||||
const fragment = new DocumentFragment();
|
||||
|
||||
for (;;) {
|
||||
let match, i = 0, tag;
|
||||
while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || !(match = trie.search(str.slice(i))))) {
|
||||
let match, i = 0;
|
||||
|
||||
if (customEmojis === null) {
|
||||
while (i < str.length && !(match = trie.search(str.slice(i)))) {
|
||||
i += str.codePointAt(i) < 65536 ? 1 : 2;
|
||||
}
|
||||
} else {
|
||||
while (i < str.length && str[i] !== ':' && !(match = trie.search(str.slice(i)))) {
|
||||
i += str.codePointAt(i) < 65536 ? 1 : 2;
|
||||
}
|
||||
}
|
||||
|
||||
let rend, replacement = '';
|
||||
if (i === str.length) {
|
||||
break;
|
||||
|
@ -35,8 +46,6 @@ const emojify = (str, customEmojis = {}) => {
|
|||
if (!(() => {
|
||||
rend = str.indexOf(':', i + 1) + 1;
|
||||
if (!rend) return false; // no pair of ':'
|
||||
const lt = str.indexOf('<', i + 1);
|
||||
if (!(lt === -1 || lt >= rend)) return false; // tag appeared before closing ':'
|
||||
const shortname = str.slice(i, rend);
|
||||
// now got a replacee as ':shortname:'
|
||||
// if you want additional emoji handler, add statements below which set replacement and return true.
|
||||
|
@ -47,29 +56,6 @@ const emojify = (str, customEmojis = {}) => {
|
|||
}
|
||||
return false;
|
||||
})()) rend = ++i;
|
||||
} else if (tag >= 0) { // <, &
|
||||
rend = str.indexOf('>;'[tag], i + 1) + 1;
|
||||
if (!rend) {
|
||||
break;
|
||||
}
|
||||
if (tag === 0) {
|
||||
if (invisible) {
|
||||
if (str[i + 1] === '/') { // closing tag
|
||||
if (!--invisible) {
|
||||
tagChars = tagCharsWithEmojis;
|
||||
}
|
||||
} else if (str[rend - 2] !== '/') { // opening tag
|
||||
invisible++;
|
||||
}
|
||||
} else {
|
||||
if (str.startsWith('<span class="invisible">', i)) {
|
||||
// avoid emojifying on invisible text
|
||||
invisible = 1;
|
||||
tagChars = tagCharsWithoutEmojis;
|
||||
}
|
||||
}
|
||||
}
|
||||
i = rend;
|
||||
} else { // matched to unicode emoji
|
||||
const { filename, shortCode } = unicodeMapping[match];
|
||||
const title = shortCode ? `:${shortCode}:` : '';
|
||||
|
@ -80,10 +66,43 @@ const emojify = (str, customEmojis = {}) => {
|
|||
rend += 1;
|
||||
}
|
||||
}
|
||||
rtn += str.slice(0, i) + replacement;
|
||||
|
||||
fragment.append(document.createTextNode(str.slice(0, i)));
|
||||
if (replacement) {
|
||||
fragment.append(domParser.parseFromString(replacement, 'text/html').documentElement.getElementsByTagName('img')[0]);
|
||||
}
|
||||
node.textContent = str.slice(0, i);
|
||||
str = str.slice(rend);
|
||||
}
|
||||
return rtn + str;
|
||||
|
||||
fragment.append(document.createTextNode(str));
|
||||
node.parentElement.replaceChild(fragment, node);
|
||||
};
|
||||
|
||||
const emojifyNode = (node, customEmojis) => {
|
||||
for (const child of node.childNodes) {
|
||||
switch(child.nodeType) {
|
||||
case Node.TEXT_NODE:
|
||||
emojifyTextNode(child, customEmojis);
|
||||
break;
|
||||
case Node.ELEMENT_NODE:
|
||||
if (!child.classList.contains('invisible'))
|
||||
emojifyNode(child, customEmojis);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const emojify = (str, customEmojis = {}) => {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = str;
|
||||
|
||||
if (!Object.keys(customEmojis).length)
|
||||
customEmojis = null;
|
||||
|
||||
emojifyNode(wrapper, customEmojis);
|
||||
|
||||
return wrapper.innerHTML;
|
||||
};
|
||||
|
||||
export default emojify;
|
||||
|
|
|
@ -184,11 +184,12 @@ const ignoreSuggestion = (state, position, token, completion, path) => {
|
|||
};
|
||||
|
||||
const sortHashtagsByUse = (state, tags) => {
|
||||
const personalHistory = state.get('tagHistory');
|
||||
const personalHistory = state.get('tagHistory').map(tag => tag.toLowerCase());
|
||||
|
||||
return tags.sort((a, b) => {
|
||||
const usedA = personalHistory.includes(a.name);
|
||||
const usedB = personalHistory.includes(b.name);
|
||||
const tagsWithLowercase = tags.map(t => ({ ...t, lowerName: t.name.toLowerCase() }));
|
||||
const sorted = tagsWithLowercase.sort((a, b) => {
|
||||
const usedA = personalHistory.includes(a.lowerName);
|
||||
const usedB = personalHistory.includes(b.lowerName);
|
||||
|
||||
if (usedA === usedB) {
|
||||
return 0;
|
||||
|
@ -198,6 +199,8 @@ const sortHashtagsByUse = (state, tags) => {
|
|||
return 1;
|
||||
}
|
||||
});
|
||||
sorted.forEach(tag => delete tag.lowerName);
|
||||
return sorted;
|
||||
};
|
||||
|
||||
const insertEmoji = (state, position, emojiData, needsSpace) => {
|
||||
|
|
|
@ -4261,6 +4261,7 @@ a.status-card.compact:hover {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
color: $secondary-text-color;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
|
|
|
@ -6,7 +6,7 @@ class AccountReachFinder
|
|||
end
|
||||
|
||||
def inboxes
|
||||
(followers_inboxes + reporters_inboxes + relay_inboxes).uniq
|
||||
(followers_inboxes + reporters_inboxes + recently_mentioned_inboxes + relay_inboxes).uniq
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -19,6 +19,13 @@ class AccountReachFinder
|
|||
Account.where(id: @account.targeted_reports.select(:account_id)).inboxes
|
||||
end
|
||||
|
||||
def recently_mentioned_inboxes
|
||||
cutoff_id = Mastodon::Snowflake.id_at(2.days.ago, with_random: false)
|
||||
recent_statuses = @account.statuses.recent.where(id: cutoff_id...).limit(200)
|
||||
|
||||
Account.joins(:mentions).where(mentions: { status: recent_statuses }).inboxes.take(2000)
|
||||
end
|
||||
|
||||
def relay_inboxes
|
||||
Relay.enabled.pluck(:inbox_url)
|
||||
end
|
||||
|
|
|
@ -106,7 +106,8 @@ class ActivityPub::Activity
|
|||
actor_id = value_or_id(first_of_value(@object['attributedTo']))
|
||||
|
||||
if actor_id == @account.uri
|
||||
return ActivityPub::Activity.factory({ 'type' => 'Create', 'actor' => actor_id, 'object' => @object }, @account).perform
|
||||
virtual_object = { 'type' => 'Create', 'actor' => actor_id, 'object' => @object }
|
||||
return ActivityPub::Activity.factory(virtual_object, @account, request_id: @options[:request_id]).perform
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -152,9 +153,10 @@ class ActivityPub::Activity
|
|||
def fetch_remote_original_status
|
||||
if object_uri.start_with?('http')
|
||||
return if ActivityPub::TagManager.instance.local_uri?(object_uri)
|
||||
ActivityPub::FetchRemoteStatusService.new.call(object_uri, id: true, on_behalf_of: @account.followers.local.first)
|
||||
|
||||
ActivityPub::FetchRemoteStatusService.new.call(object_uri, on_behalf_of: @account.followers.local.first, request_id: @options[:request_id])
|
||||
elsif @object['url'].present?
|
||||
::FetchRemoteStatusService.new.call(@object['url'])
|
||||
::FetchRemoteStatusService.new.call(@object['url'], request_id: @options[:request_id])
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -222,7 +222,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
return if tag['href'].blank?
|
||||
|
||||
account = account_from_uri(tag['href'])
|
||||
account = ActivityPub::FetchRemoteAccountService.new.call(tag['href']) if account.nil?
|
||||
account = ActivityPub::FetchRemoteAccountService.new.call(tag['href'], request_id: @options[:request_id]) if account.nil?
|
||||
|
||||
return if account.nil?
|
||||
|
||||
|
@ -327,18 +327,18 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
def resolve_thread(status)
|
||||
return unless status.reply? && status.thread.nil? && Request.valid_url?(in_reply_to_uri)
|
||||
|
||||
ThreadResolveWorker.perform_async(status.id, in_reply_to_uri)
|
||||
ThreadResolveWorker.perform_async(status.id, in_reply_to_uri, { 'request_id' => @options[:request_id]})
|
||||
end
|
||||
|
||||
def fetch_replies(status)
|
||||
collection = @object['replies']
|
||||
return if collection.nil?
|
||||
|
||||
replies = ActivityPub::FetchRepliesService.new.call(status, collection, false)
|
||||
replies = ActivityPub::FetchRepliesService.new.call(status, collection, allow_synchronous_requests: false, request_id: @options[:request_id])
|
||||
return unless replies.nil?
|
||||
|
||||
uri = value_or_id(collection)
|
||||
ActivityPub::FetchRepliesWorker.perform_async(status.id, uri) unless uri.nil?
|
||||
ActivityPub::FetchRepliesWorker.perform_async(status.id, uri, { 'request_id' => @options[:request_id]}) unless uri.nil?
|
||||
end
|
||||
|
||||
def conversation_from_uri(uri)
|
||||
|
|
|
@ -16,7 +16,7 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity
|
|||
@account,
|
||||
target_account,
|
||||
status_ids: target_statuses.nil? ? [] : target_statuses.map(&:id),
|
||||
comment: @json['content'] || '',
|
||||
comment: report_comment,
|
||||
uri: report_uri
|
||||
)
|
||||
end
|
||||
|
@ -35,4 +35,8 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity
|
|||
def report_uri
|
||||
@json['id'] unless @json['id'].nil? || invalid_origin?(@json['id'])
|
||||
end
|
||||
|
||||
def report_comment
|
||||
(@json['content'] || '')[0...5000]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,7 +18,7 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
|
|||
def update_account
|
||||
return reject_payload! if @account.uri != object_uri
|
||||
|
||||
ActivityPub::ProcessAccountService.new.call(@account.username, @account.domain, @object, signed_with_known_key: true)
|
||||
ActivityPub::ProcessAccountService.new.call(@account.username, @account.domain, @object, signed_with_known_key: true, request_id: @options[:request_id])
|
||||
end
|
||||
|
||||
def update_status
|
||||
|
@ -28,6 +28,6 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
|
|||
|
||||
return if @status.nil?
|
||||
|
||||
ActivityPub::ProcessStatusUpdateService.new.call(@status, @object)
|
||||
ActivityPub::ProcessStatusUpdateService.new.call(@status, @object, request_id: @options[:request_id])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,7 +19,7 @@ class ActivityPub::LinkedDataSignature
|
|||
return unless type == 'RsaSignature2017'
|
||||
|
||||
creator = ActivityPub::TagManager.instance.uri_to_resource(creator_uri, Account)
|
||||
creator ||= ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false)
|
||||
creator = ActivityPub::FetchRemoteKeyService.new.call(creator_uri) if creator&.public_key.blank?
|
||||
|
||||
return if creator.nil?
|
||||
|
||||
|
@ -27,9 +27,9 @@ class ActivityPub::LinkedDataSignature
|
|||
document_hash = hash(@json.without('signature'))
|
||||
to_be_verified = options_hash + document_hash
|
||||
|
||||
if creator.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), Base64.decode64(signature), to_be_verified)
|
||||
creator
|
||||
end
|
||||
creator if creator.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), Base64.decode64(signature), to_be_verified)
|
||||
rescue OpenSSL::PKey::RSAError
|
||||
false
|
||||
end
|
||||
|
||||
def sign!(creator, sign_with: nil)
|
||||
|
|
|
@ -53,7 +53,8 @@ class ActivityPub::Parser::StatusParser
|
|||
end
|
||||
|
||||
def created_at
|
||||
@object['published']&.to_datetime
|
||||
datetime = @object['published']&.to_datetime
|
||||
datetime if datetime.present? && (0..9999).cover?(datetime.year)
|
||||
rescue ArgumentError
|
||||
nil
|
||||
end
|
||||
|
|
|
@ -27,6 +27,8 @@ class ActivityPub::TagManager
|
|||
when :note, :comment, :activity
|
||||
return activity_account_status_url(target.account, target) if target.reblog?
|
||||
short_account_status_url(target.account, target)
|
||||
when :flag
|
||||
target.uri
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -41,6 +43,8 @@ class ActivityPub::TagManager
|
|||
account_status_url(target.account, target)
|
||||
when :emoji
|
||||
emoji_url(target)
|
||||
when :flag
|
||||
target.uri
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::AccountStatusesFilter < AccountStatusesFilter
|
||||
private
|
||||
|
||||
def blocked?
|
||||
false
|
||||
end
|
||||
end
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class Admin::SystemCheck
|
||||
ACTIVE_CHECKS = [
|
||||
Admin::SystemCheck::MediaPrivacyCheck,
|
||||
Admin::SystemCheck::DatabaseSchemaCheck,
|
||||
Admin::SystemCheck::SidekiqProcessCheck,
|
||||
Admin::SystemCheck::RulesCheck,
|
||||
|
|
|
@ -20,7 +20,7 @@ class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck
|
|||
def running_version
|
||||
@running_version ||= begin
|
||||
Chewy.client.info['version']['number']
|
||||
rescue Faraday::ConnectionFailed
|
||||
rescue Faraday::ConnectionFailed, Elasticsearch::Transport::Transport::Error
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::SystemCheck::MediaPrivacyCheck < Admin::SystemCheck::BaseCheck
|
||||
include RoutingHelper
|
||||
|
||||
def skip?
|
||||
!current_user.can?(:view_devops)
|
||||
end
|
||||
|
||||
def pass?
|
||||
check_media_uploads!
|
||||
@failure_message.nil?
|
||||
end
|
||||
|
||||
def message
|
||||
Admin::SystemCheck::Message.new(@failure_message, @failure_value, @failure_action, true)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_media_uploads!
|
||||
if Rails.configuration.x.use_s3
|
||||
check_media_listing_inaccessible_s3!
|
||||
else
|
||||
check_media_listing_inaccessible!
|
||||
end
|
||||
end
|
||||
|
||||
def check_media_listing_inaccessible!
|
||||
full_url = full_asset_url(media_attachment.file.url(:original, false))
|
||||
|
||||
# Check if we can list the uploaded file. If true, that's an error
|
||||
directory_url = Addressable::URI.parse(full_url)
|
||||
directory_url.query = nil
|
||||
filename = directory_url.path.gsub(%r{.*/}, '')
|
||||
directory_url.path = directory_url.path.gsub(%r{/[^/]+\Z}, '/')
|
||||
Request.new(:get, directory_url, allow_local: true).perform do |res|
|
||||
if res.truncated_body&.include?(filename)
|
||||
@failure_message = use_storage? ? :upload_check_privacy_error_object_storage : :upload_check_privacy_error
|
||||
@failure_action = 'https://docs.joinmastodon.org/admin/optional/object-storage/#FS'
|
||||
end
|
||||
end
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
|
||||
def check_media_listing_inaccessible_s3!
|
||||
urls_to_check = []
|
||||
paperclip_options = Paperclip::Attachment.default_options
|
||||
s3_protocol = paperclip_options[:s3_protocol]
|
||||
s3_host_alias = paperclip_options[:s3_host_alias]
|
||||
s3_host_name = paperclip_options[:s3_host_name]
|
||||
bucket_name = paperclip_options.dig(:s3_credentials, :bucket)
|
||||
|
||||
urls_to_check << "#{s3_protocol}://#{s3_host_alias}/" if s3_host_alias.present?
|
||||
urls_to_check << "#{s3_protocol}://#{s3_host_name}/#{bucket_name}/"
|
||||
urls_to_check.uniq.each do |full_url|
|
||||
check_s3_listing!(full_url)
|
||||
break if @failure_message.present?
|
||||
end
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
|
||||
def check_s3_listing!(full_url)
|
||||
bucket_url = Addressable::URI.parse(full_url)
|
||||
bucket_url.path = bucket_url.path.delete_suffix(media_attachment.file.path(:original))
|
||||
bucket_url.query = "max-keys=1&x-random=#{SecureRandom.hex(10)}"
|
||||
Request.new(:get, bucket_url, allow_local: true).perform do |res|
|
||||
if res.truncated_body&.include?('ListBucketResult')
|
||||
@failure_message = :upload_check_privacy_error_object_storage
|
||||
@failure_action = 'https://docs.joinmastodon.org/admin/optional/object-storage/#S3'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def media_attachment
|
||||
@media_attachment ||= begin
|
||||
attachment = Account.representative.media_attachments.first
|
||||
if attachment.present?
|
||||
attachment.touch # rubocop:disable Rails/SkipsModelValidations
|
||||
attachment
|
||||
else
|
||||
create_test_attachment!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create_test_attachment!
|
||||
Tempfile.create(%w(test-upload .jpg), binmode: true) do |tmp_file|
|
||||
tmp_file.write(
|
||||
Base64.decode64(
|
||||
'/9j/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAA' \
|
||||
'AAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA' \
|
||||
'QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE' \
|
||||
'BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIAAEAAgMBEQACEQEDEQH/x' \
|
||||
'ABKAAEAAAAAAAAAAAAAAAAAAAALEAEAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAA' \
|
||||
'AAAAAEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8H//2Q=='
|
||||
)
|
||||
)
|
||||
tmp_file.flush
|
||||
Account.representative.media_attachments.create!(file: tmp_file)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,11 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::SystemCheck::Message
|
||||
attr_reader :key, :value, :action
|
||||
attr_reader :key, :value, :action, :critical
|
||||
|
||||
def initialize(key, value = nil, action = nil)
|
||||
def initialize(key, value = nil, action = nil, critical = false)
|
||||
@key = key
|
||||
@value = value
|
||||
@action = action
|
||||
@critical = critical
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,16 +4,32 @@ module ApplicationExtension
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
include Redisable
|
||||
|
||||
validates :name, length: { maximum: 60 }
|
||||
validates :website, url: true, length: { maximum: 2_000 }, if: :website?
|
||||
validates :redirect_uri, length: { maximum: 2_000 }
|
||||
end
|
||||
|
||||
def most_recently_used_access_token
|
||||
@most_recently_used_access_token ||= access_tokens.where.not(last_used_at: nil).order(last_used_at: :desc).first
|
||||
# The relationship used between Applications and AccessTokens is using
|
||||
# dependent: delete_all, which means the ActiveRecord callback in
|
||||
# AccessTokenExtension is not run, so instead we manually announce to
|
||||
# streaming that these tokens are being deleted.
|
||||
before_destroy :push_to_streaming_api, prepend: true
|
||||
end
|
||||
|
||||
def confirmation_redirect_uri
|
||||
redirect_uri.lines.first.strip
|
||||
end
|
||||
|
||||
def push_to_streaming_api
|
||||
# TODO: #28793 Combine into a single topic
|
||||
payload = Oj.dump(event: :kill)
|
||||
access_tokens.in_batches do |tokens|
|
||||
redis.pipelined do |pipeline|
|
||||
tokens.ids.each do |id|
|
||||
pipeline.publish("timeline:access_token:#{id}", payload)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -65,8 +65,13 @@ class DeliveryFailureTracker
|
|||
domains - UnavailableDomain.all.pluck(:domain)
|
||||
end
|
||||
|
||||
def warning_domains_map
|
||||
def warning_domains_map(domains = nil)
|
||||
if domains.nil?
|
||||
warning_domains.index_with { |domain| redis.scard(exhausted_deliveries_key_by(domain)) }
|
||||
else
|
||||
domains -= UnavailableDomain.where(domain: domains).pluck(:domain)
|
||||
domains.index_with { |domain| redis.scard(exhausted_deliveries_key_by(domain)) }.filter { |_, days| days.positive? }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -23,48 +23,40 @@ class EmojiFormatter
|
|||
def to_s
|
||||
return html if custom_emojis.empty? || html.blank?
|
||||
|
||||
tree = Nokogiri::HTML.fragment(html)
|
||||
tree.xpath('./text()|.//text()[not(ancestor[@class="invisible"])]').to_a.each do |node|
|
||||
i = -1
|
||||
tag_open_index = nil
|
||||
inside_shortname = false
|
||||
shortname_start_index = -1
|
||||
invisible_depth = 0
|
||||
last_index = 0
|
||||
result = ''.dup
|
||||
text = node.content
|
||||
result = Nokogiri::XML::NodeSet.new(tree.document)
|
||||
|
||||
while i + 1 < html.size
|
||||
while i + 1 < text.size
|
||||
i += 1
|
||||
|
||||
if invisible_depth.zero? && inside_shortname && html[i] == ':'
|
||||
if inside_shortname && text[i] == ':'
|
||||
inside_shortname = false
|
||||
shortcode = html[shortname_start_index + 1..i - 1]
|
||||
char_after = html[i + 1]
|
||||
shortcode = text[shortname_start_index + 1..i - 1]
|
||||
char_after = text[i + 1]
|
||||
|
||||
next unless (char_after.nil? || !DISALLOWED_BOUNDING_REGEX.match?(char_after)) && (emoji = emoji_map[shortcode])
|
||||
|
||||
result << html[last_index..shortname_start_index - 1] if shortname_start_index.positive?
|
||||
result << image_for_emoji(shortcode, emoji)
|
||||
last_index = i + 1
|
||||
elsif tag_open_index && html[i] == '>'
|
||||
tag = html[tag_open_index..i]
|
||||
tag_open_index = nil
|
||||
result << Nokogiri::XML::Text.new(text[last_index..shortname_start_index - 1], tree.document) if shortname_start_index.positive?
|
||||
result << Nokogiri::HTML.fragment(image_for_emoji(shortcode, emoji))
|
||||
|
||||
if invisible_depth.positive?
|
||||
invisible_depth += count_tag_nesting(tag)
|
||||
elsif tag == '<span class="invisible">'
|
||||
invisible_depth = 1
|
||||
end
|
||||
elsif html[i] == '<'
|
||||
tag_open_index = i
|
||||
inside_shortname = false
|
||||
elsif !tag_open_index && html[i] == ':' && (i.zero? || !DISALLOWED_BOUNDING_REGEX.match?(html[i - 1]))
|
||||
last_index = i + 1
|
||||
elsif text[i] == ':' && (i.zero? || !DISALLOWED_BOUNDING_REGEX.match?(text[i - 1]))
|
||||
inside_shortname = true
|
||||
shortname_start_index = i
|
||||
end
|
||||
end
|
||||
|
||||
result << html[last_index..-1]
|
||||
result << Nokogiri::XML::Text.new(text[last_index..-1], tree.document)
|
||||
node.replace(result)
|
||||
end
|
||||
|
||||
result.html_safe # rubocop:disable Rails/OutputSafety
|
||||
tree.to_html.html_safe # rubocop:disable Rails/OutputSafety
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -34,7 +34,9 @@ class Importer::BaseImporter
|
|||
# Estimate the amount of documents that would be indexed. Not exact!
|
||||
# @returns [Integer]
|
||||
def estimate!
|
||||
ActiveRecord::Base.connection_pool.with_connection { |connection| connection.select_one("SELECT reltuples AS estimate FROM pg_class WHERE relname = '#{index.adapter.target.table_name}'")['estimate'].to_i }
|
||||
reltuples = ActiveRecord::Base.connection_pool.with_connection { |connection| connection.select_one("SELECT reltuples FROM pg_class WHERE relname = '#{index.adapter.target.table_name}'")['reltuples'].to_i }
|
||||
# If the table has never yet been vacuumed or analyzed, reltuples contains -1
|
||||
[reltuples, 0].max
|
||||
end
|
||||
|
||||
# Import data from the database into the index
|
||||
|
|
|
@ -140,7 +140,7 @@ class LinkDetailsExtractor
|
|||
end
|
||||
|
||||
def html
|
||||
player_url.present? ? content_tag(:iframe, nil, src: player_url, width: width, height: height, allowtransparency: 'true', scrolling: 'no', frameborder: '0') : nil
|
||||
player_url.present? ? content_tag(:iframe, nil, src: player_url, width: width, height: height, allowfullscreen: 'true', allowtransparency: 'true', scrolling: 'no', frameborder: '0') : nil
|
||||
end
|
||||
|
||||
def width
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PlainTextFormatter
|
||||
include ActionView::Helpers::TextHelper
|
||||
|
||||
NEWLINE_TAGS_RE = /(<br \/>|<br>|<\/p>)+/.freeze
|
||||
NEWLINE_TAGS_RE = %r{(<br />|<br>|</p>)+}
|
||||
|
||||
attr_reader :text, :local
|
||||
|
||||
|
@ -18,7 +16,10 @@ class PlainTextFormatter
|
|||
if local?
|
||||
text
|
||||
else
|
||||
strip_tags(insert_newlines).chomp
|
||||
node = Nokogiri::HTML.fragment(insert_newlines)
|
||||
# Elements that are entirely removed with our Sanitize config
|
||||
node.xpath('.//iframe|.//math|.//noembed|.//noframes|.//noscript|.//plaintext|.//script|.//style|.//svg|.//xmp').remove
|
||||
node.text.chomp
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -4,14 +4,60 @@ require 'ipaddr'
|
|||
require 'socket'
|
||||
require 'resolv'
|
||||
|
||||
# Monkey-patch the HTTP.rb timeout class to avoid using a timeout block
|
||||
# Use our own timeout class to avoid using HTTP.rb's timeout block
|
||||
# around the Socket#open method, since we use our own timeout blocks inside
|
||||
# that method
|
||||
class HTTP::Timeout::PerOperation
|
||||
#
|
||||
# Also changes how the read timeout behaves so that it is cumulative (closer
|
||||
# to HTTP::Timeout::Global, but still having distinct timeouts for other
|
||||
# operation types)
|
||||
class PerOperationWithDeadline < HTTP::Timeout::PerOperation
|
||||
READ_DEADLINE = 30
|
||||
|
||||
def initialize(*args)
|
||||
super
|
||||
|
||||
@read_deadline = options.fetch(:read_deadline, READ_DEADLINE)
|
||||
end
|
||||
|
||||
def connect(socket_class, host, port, nodelay = false)
|
||||
@socket = socket_class.open(host, port)
|
||||
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
|
||||
end
|
||||
|
||||
# Reset deadline when the connection is re-used for different requests
|
||||
def reset_counter
|
||||
@deadline = nil
|
||||
end
|
||||
|
||||
# Read data from the socket
|
||||
def readpartial(size, buffer = nil)
|
||||
@deadline ||= Process.clock_gettime(Process::CLOCK_MONOTONIC) + @read_deadline
|
||||
|
||||
timeout = false
|
||||
loop do
|
||||
result = @socket.read_nonblock(size, buffer, exception: false)
|
||||
|
||||
return :eof if result.nil?
|
||||
|
||||
remaining_time = @deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
raise HTTP::TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout
|
||||
raise HTTP::TimeoutError, "Read timed out after a total of #{@read_deadline} seconds" if remaining_time <= 0
|
||||
return result if result != :wait_readable
|
||||
|
||||
# marking the socket for timeout. Why is this not being raised immediately?
|
||||
# it seems there is some race-condition on the network level between calling
|
||||
# #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting
|
||||
# for reads, and when waiting for x seconds, it returns nil suddenly without completing
|
||||
# the x seconds. In a normal case this would be a timeout on wait/read, but it can
|
||||
# also mean that the socket has been closed by the server. Therefore we "mark" the
|
||||
# socket for timeout and try to read more bytes. If it returns :eof, it's all good, no
|
||||
# timeout. Else, the first timeout was a proper timeout.
|
||||
# This hack has to be done because io/wait#wait_readable doesn't provide a value for when
|
||||
# the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks.
|
||||
timeout = true unless @socket.to_io.wait_readable([remaining_time, @read_timeout].min)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Request
|
||||
|
@ -20,7 +66,7 @@ class Request
|
|||
# We enforce a 5s timeout on DNS resolving, 5s timeout on socket opening
|
||||
# and 5s timeout on the TLS handshake, meaning the worst case should take
|
||||
# about 15s in total
|
||||
TIMEOUT = { connect: 5, read: 10, write: 10 }.freeze
|
||||
TIMEOUT = { connect_timeout: 5, read_timeout: 10, write_timeout: 10, read_deadline: 30 }.freeze
|
||||
|
||||
include RoutingHelper
|
||||
|
||||
|
@ -31,6 +77,7 @@ class Request
|
|||
@url = Addressable::URI.parse(url).normalize
|
||||
@http_client = options.delete(:http_client)
|
||||
@options = options.merge(socket_class: use_proxy? ? ProxySocket : Socket)
|
||||
@options = @options.merge(timeout_class: PerOperationWithDeadline, timeout_options: TIMEOUT)
|
||||
@options = @options.merge(Rails.configuration.x.http_client_proxy) if use_proxy?
|
||||
@headers = {}
|
||||
|
||||
|
@ -94,7 +141,7 @@ class Request
|
|||
end
|
||||
|
||||
def http_client
|
||||
HTTP.use(:auto_inflate).timeout(TIMEOUT.dup).follow(max_hops: 3)
|
||||
HTTP.use(:auto_inflate).follow(max_hops: 3)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -218,11 +265,11 @@ class Request
|
|||
end
|
||||
|
||||
until socks.empty?
|
||||
_, available_socks, = IO.select(nil, socks, nil, Request::TIMEOUT[:connect])
|
||||
_, available_socks, = IO.select(nil, socks, nil, Request::TIMEOUT[:connect_timeout])
|
||||
|
||||
if available_socks.nil?
|
||||
socks.each(&:close)
|
||||
raise HTTP::TimeoutError, "Connect timed out after #{Request::TIMEOUT[:connect]} seconds"
|
||||
raise HTTP::TimeoutError, "Connect timed out after #{Request::TIMEOUT[:connect_timeout]} seconds"
|
||||
end
|
||||
|
||||
available_socks.each do |sock|
|
||||
|
|
|
@ -70,7 +70,7 @@ class StatusReachFinder
|
|||
|
||||
def followers_inboxes
|
||||
if @status.in_reply_to_local_account? && distributable?
|
||||
@status.account.followers.or(@status.thread.account.followers).inboxes
|
||||
@status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).inboxes
|
||||
elsif @status.direct_visibility? || @status.limited_visibility?
|
||||
[]
|
||||
else
|
||||
|
|
|
@ -7,18 +7,18 @@ class TagManager
|
|||
include RoutingHelper
|
||||
|
||||
def web_domain?(domain)
|
||||
domain.nil? || domain.gsub(/[\/]/, '').casecmp(Rails.configuration.x.web_domain).zero?
|
||||
domain.nil? || domain.delete_suffix('/').casecmp(Rails.configuration.x.web_domain).zero?
|
||||
end
|
||||
|
||||
def local_domain?(domain)
|
||||
domain.nil? || domain.gsub(/[\/]/, '').casecmp(Rails.configuration.x.local_domain).zero?
|
||||
domain.nil? || domain.delete_suffix('/').casecmp(Rails.configuration.x.local_domain).zero?
|
||||
end
|
||||
|
||||
def normalize_domain(domain)
|
||||
return if domain.nil?
|
||||
|
||||
uri = Addressable::URI.new
|
||||
uri.host = domain.gsub(/[\/]/, '')
|
||||
uri.host = domain.delete_suffix('/')
|
||||
uri.normalized_host
|
||||
end
|
||||
|
||||
|
@ -27,5 +27,7 @@ class TagManager
|
|||
domain = uri.host + (uri.port ? ":#{uri.port}" : '')
|
||||
|
||||
TagManager.instance.web_domain?(domain)
|
||||
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
|
||||
false
|
||||
end
|
||||
end
|
||||
|
|
|
@ -48,6 +48,26 @@ class TextFormatter
|
|||
html.html_safe # rubocop:disable Rails/OutputSafety
|
||||
end
|
||||
|
||||
class << self
|
||||
include ERB::Util
|
||||
|
||||
def shortened_link(url, rel_me: false)
|
||||
url = Addressable::URI.parse(url).to_s
|
||||
rel = rel_me ? (DEFAULT_REL + %w(me)) : DEFAULT_REL
|
||||
|
||||
prefix = url.match(URL_PREFIX_REGEX).to_s
|
||||
display_url = url[prefix.length, 30]
|
||||
suffix = url[prefix.length + 30..-1]
|
||||
cutoff = url[prefix.length..-1].length > 30
|
||||
|
||||
<<~HTML.squish.html_safe # rubocop:disable Rails/OutputSafety
|
||||
<a href="#{h(url)}" target="_blank" rel="#{rel.join(' ')}"><span class="invisible">#{h(prefix)}</span><span class="#{cutoff ? 'ellipsis' : ''}">#{h(display_url)}</span><span class="invisible">#{h(suffix)}</span></a>
|
||||
HTML
|
||||
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
|
||||
h(url)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def rewrite
|
||||
|
@ -70,19 +90,7 @@ class TextFormatter
|
|||
end
|
||||
|
||||
def link_to_url(entity)
|
||||
url = Addressable::URI.parse(entity[:url]).to_s
|
||||
rel = with_rel_me? ? (DEFAULT_REL + %w(me)) : DEFAULT_REL
|
||||
|
||||
prefix = url.match(URL_PREFIX_REGEX).to_s
|
||||
display_url = url[prefix.length, 30]
|
||||
suffix = url[prefix.length + 30..-1]
|
||||
cutoff = url[prefix.length..-1].length > 30
|
||||
|
||||
<<~HTML.squish
|
||||
<a href="#{h(url)}" target="_blank" rel="#{rel.join(' ')}"><span class="invisible">#{h(prefix)}</span><span class="#{cutoff ? 'ellipsis' : ''}">#{h(display_url)}</span><span class="invisible">#{h(suffix)}</span></a>
|
||||
HTML
|
||||
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
|
||||
h(entity[:url])
|
||||
TextFormatter.shortened_link(entity[:url], rel_me: with_rel_me?)
|
||||
end
|
||||
|
||||
def link_to_hashtag(entity)
|
||||
|
|
|
@ -43,6 +43,9 @@ class VideoMetadataExtractor
|
|||
@height = video_stream[:height]
|
||||
@frame_rate = video_stream[:avg_frame_rate] == '0/0' ? nil : Rational(video_stream[:avg_frame_rate])
|
||||
@r_frame_rate = video_stream[:r_frame_rate] == '0/0' ? nil : Rational(video_stream[:r_frame_rate])
|
||||
# For some video streams the frame_rate reported by `ffprobe` will be 0/0, but for these streams we
|
||||
# should use `r_frame_rate` instead. Video screencast generated by Gnome Screencast have this issue.
|
||||
@frame_rate ||= @r_frame_rate
|
||||
end
|
||||
|
||||
if (audio_stream = audio_streams.first)
|
||||
|
|
|
@ -7,6 +7,8 @@ class ApplicationMailer < ActionMailer::Base
|
|||
helper :instance
|
||||
helper :formatting
|
||||
|
||||
after_action :set_autoreply_headers!
|
||||
|
||||
protected
|
||||
|
||||
def locale_for_account(account)
|
||||
|
@ -14,4 +16,10 @@ class ApplicationMailer < ActionMailer::Base
|
|||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def set_autoreply_headers!
|
||||
headers['Precedence'] = 'list'
|
||||
headers['X-Auto-Response-Suppress'] = 'All'
|
||||
headers['Auto-Submitted'] = 'auto-generated'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -61,9 +61,9 @@ class Account < ApplicationRecord
|
|||
trust_level
|
||||
)
|
||||
|
||||
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
|
||||
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[[:word:]]+)?)/i
|
||||
URL_PREFIX_RE = /\Ahttp(s?):\/\/[^\/]+/
|
||||
USERNAME_RE = /[a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?/i
|
||||
MENTION_RE = %r{(?<![=/[:word:]])@((#{USERNAME_RE})(?:@[[:word:].-]+[[:word:]]+)?)}i
|
||||
URL_PREFIX_RE = %r{\Ahttp(s?)://[^/]+}
|
||||
|
||||
include Attachmentable
|
||||
include AccountAssociations
|
||||
|
@ -106,15 +106,15 @@ class Account < ApplicationRecord
|
|||
scope :bots, -> { where(actor_type: %w(Application Service)) }
|
||||
scope :groups, -> { where(actor_type: 'Group') }
|
||||
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
|
||||
scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
|
||||
scope :matches_username, ->(value) { where('lower((username)::text) LIKE lower(?)', "#{value}%") }
|
||||
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
|
||||
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
||||
scope :without_unapproved, -> { left_outer_joins(:user).remote.or(left_outer_joins(:user).merge(User.approved.confirmed)) }
|
||||
scope :searchable, -> { without_unapproved.without_suspended.where(moved_to_account_id: nil) }
|
||||
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) }
|
||||
scope :followable_by, ->(account) { joins(arel_table.join(Follow.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(Follow.arel_table[:target_account_id]).and(Follow.arel_table[:account_id].eq(account.id))).join_sources).where(Follow.arel_table[:id].eq(nil)).joins(arel_table.join(FollowRequest.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(FollowRequest.arel_table[:target_account_id]).and(FollowRequest.arel_table[:account_id].eq(account.id))).join_sources).where(FollowRequest.arel_table[:id].eq(nil)) }
|
||||
scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc, accounts.id desc')) }
|
||||
scope :by_recent_sign_in, -> { order(Arel.sql('(case when users.current_sign_in_at is null then 1 else 0 end) asc, users.current_sign_in_at desc, accounts.id desc')) }
|
||||
scope :by_recent_status, -> { includes(:account_stat).merge(AccountStat.order('last_status_at DESC NULLS LAST')).references(:account_stat) }
|
||||
scope :by_recent_sign_in, -> { order(Arel.sql('users.current_sign_in_at DESC NULLS LAST')) }
|
||||
scope :popular, -> { order('account_stats.followers_count desc') }
|
||||
scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) }
|
||||
scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) }
|
||||
|
|
|
@ -16,29 +16,28 @@
|
|||
class AccountConversation < ApplicationRecord
|
||||
include Redisable
|
||||
|
||||
attr_writer :participant_accounts
|
||||
|
||||
before_validation :set_last_status
|
||||
after_commit :push_to_streaming_api
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :conversation
|
||||
belongs_to :last_status, class_name: 'Status'
|
||||
|
||||
before_validation :set_last_status
|
||||
|
||||
def participant_account_ids=(arr)
|
||||
self[:participant_account_ids] = arr.sort
|
||||
@participant_accounts = nil
|
||||
end
|
||||
|
||||
def participant_accounts
|
||||
if participant_account_ids.empty?
|
||||
[account]
|
||||
else
|
||||
participants = Account.where(id: participant_account_ids)
|
||||
participants.empty? ? [account] : participants
|
||||
end
|
||||
@participant_accounts ||= Account.where(id: participant_account_ids).to_a
|
||||
@participant_accounts.presence || [account]
|
||||
end
|
||||
|
||||
class << self
|
||||
def to_a_paginated_by_id(limit, options = {})
|
||||
array = begin
|
||||
if options[:min_id]
|
||||
paginate_by_min_id(limit, options[:min_id], options[:max_id]).reverse
|
||||
else
|
||||
|
@ -46,6 +45,17 @@ class AccountConversation < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
# Preload participants
|
||||
participant_ids = array.flat_map(&:participant_account_ids)
|
||||
accounts_by_id = Account.where(id: participant_ids).index_by(&:id)
|
||||
|
||||
array.each do |conversation|
|
||||
conversation.participant_accounts = conversation.participant_account_ids.filter_map { |id| accounts_by_id[id] }
|
||||
end
|
||||
|
||||
array
|
||||
end
|
||||
|
||||
def paginate_by_min_id(limit, min_id = nil, max_id = nil)
|
||||
query = order(arel_table[:last_status_id].asc).limit(limit)
|
||||
query = query.where(arel_table[:last_status_id].gt(min_id)) if min_id.present?
|
||||
|
|
|
@ -31,7 +31,7 @@ class Admin::ActionLogFilter
|
|||
destroy_instance: { target_type: 'Instance', action: 'destroy' }.freeze,
|
||||
destroy_unavailable_domain: { target_type: 'UnavailableDomain', action: 'destroy' }.freeze,
|
||||
destroy_status: { target_type: 'Status', action: 'destroy' }.freeze,
|
||||
disable_2fa_user: { target_type: 'User', action: 'disable' }.freeze,
|
||||
disable_2fa_user: { target_type: 'User', action: 'disable_2fa' }.freeze,
|
||||
disable_custom_emoji: { target_type: 'CustomEmoji', action: 'disable' }.freeze,
|
||||
disable_user: { target_type: 'User', action: 'disable' }.freeze,
|
||||
enable_custom_emoji: { target_type: 'CustomEmoji', action: 'enable' }.freeze,
|
||||
|
|
|
@ -73,7 +73,7 @@ class Admin::StatusBatchAction
|
|||
# Can't use a transaction here because UpdateStatusService queues
|
||||
# Sidekiq jobs
|
||||
statuses.includes(:media_attachments, :preview_cards).find_each do |status|
|
||||
next unless status.with_media? || status.with_preview_card?
|
||||
next if status.discarded? || !(status.with_media? || status.with_preview_card?)
|
||||
|
||||
authorize(status, :update?)
|
||||
|
||||
|
@ -89,6 +89,7 @@ class Admin::StatusBatchAction
|
|||
report.resolve!(current_account)
|
||||
log_action(:resolve, report)
|
||||
end
|
||||
end
|
||||
|
||||
@warning = target_account.strikes.create!(
|
||||
action: :mark_statuses_as_sensitive,
|
||||
|
@ -96,7 +97,6 @@ class Admin::StatusBatchAction
|
|||
report: report,
|
||||
status_ids: status_ids
|
||||
)
|
||||
end
|
||||
|
||||
UserMailer.warning(target_account.user, @warning).deliver_later! if warnable?
|
||||
end
|
||||
|
@ -137,6 +137,6 @@ class Admin::StatusBatchAction
|
|||
end
|
||||
|
||||
def allowed_status_ids
|
||||
AccountStatusesFilter.new(@report.target_account, current_account).results.with_discarded.where(id: status_ids).pluck(:id)
|
||||
Admin::AccountStatusesFilter.new(@report.target_account, current_account).results.with_discarded.where(id: status_ids).pluck(:id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,6 +17,6 @@
|
|||
class Backup < ApplicationRecord
|
||||
belongs_to :user, inverse_of: :backups
|
||||
|
||||
has_attached_file :dump
|
||||
has_attached_file :dump, s3_permissions: ->(*) { ENV['S3_PERMISSION'] == '' ? nil : 'private' }
|
||||
do_not_validate_attachment_file_type :dump
|
||||
end
|
||||
|
|
|
@ -22,15 +22,14 @@ module Attachmentable
|
|||
|
||||
included do
|
||||
def self.has_attached_file(name, options = {}) # rubocop:disable Naming/PredicateName
|
||||
options = { validate_media_type: false }.merge(options)
|
||||
super(name, options)
|
||||
send(:"before_#{name}_post_process") do
|
||||
|
||||
send(:"before_#{name}_validate", prepend: true) do
|
||||
attachment = send(name)
|
||||
check_image_dimension(attachment)
|
||||
set_file_content_type(attachment)
|
||||
obfuscate_file_name(attachment)
|
||||
set_file_extension(attachment)
|
||||
Paperclip::Validators::MediaTypeSpoofDetectionValidator.new(attributes: [name]).validate(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -53,9 +52,13 @@ module Attachmentable
|
|||
return if attachment.blank? || !/image.*/.match?(attachment.content_type) || attachment.queued_for_write[:original].blank?
|
||||
|
||||
width, height = FastImage.size(attachment.queued_for_write[:original].path)
|
||||
matrix_limit = attachment.content_type == 'image/gif' ? GIF_MATRIX_LIMIT : MAX_MATRIX_LIMIT
|
||||
return unless width.present? && height.present?
|
||||
|
||||
raise Mastodon::DimensionsValidationError, "#{width}x#{height} images are not supported" if width.present? && height.present? && (width * height > matrix_limit)
|
||||
if attachment.content_type == 'image/gif' && width * height > GIF_MATRIX_LIMIT
|
||||
raise Mastodon::DimensionsValidationError, "#{width}x#{height} GIF files are not supported"
|
||||
elsif width * height > MAX_MATRIX_LIMIT
|
||||
raise Mastodon::DimensionsValidationError, "#{width}x#{height} images are not supported"
|
||||
end
|
||||
end
|
||||
|
||||
def appropriate_extension(attachment)
|
||||
|
|
|
@ -3,11 +3,24 @@
|
|||
module DomainMaterializable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
include Redisable
|
||||
|
||||
included do
|
||||
after_create_commit :refresh_instances_view
|
||||
end
|
||||
|
||||
def refresh_instances_view
|
||||
Instance.refresh unless domain.nil? || Instance.where(domain: domain).exists?
|
||||
return if domain.nil? || Instance.exists?(domain: domain)
|
||||
|
||||
Instance.refresh
|
||||
count_unique_subdomains!
|
||||
end
|
||||
|
||||
def count_unique_subdomains!
|
||||
second_and_top_level_domain = PublicSuffix.domain(domain, ignore_private: true)
|
||||
with_redis do |redis|
|
||||
redis.pfadd("unique_subdomains_for:#{second_and_top_level_domain}", domain)
|
||||
redis.expire("unique_subdomains_for:#{second_and_top_level_domain}", 1.minute.seconds)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,7 +6,7 @@ module LdapAuthenticable
|
|||
class_methods do
|
||||
def authenticate_with_ldap(params = {})
|
||||
ldap = Net::LDAP.new(ldap_options)
|
||||
filter = format(Devise.ldap_search_filter, uid: Devise.ldap_uid, mail: Devise.ldap_mail, email: params[:email])
|
||||
filter = format(Devise.ldap_search_filter, uid: Devise.ldap_uid, mail: Devise.ldap_mail, email: Net::LDAP::Filter.escape(params[:email]))
|
||||
|
||||
if (user_info = ldap.bind_as(base: Devise.ldap_base, filter: filter, password: params[:password]))
|
||||
ldap_get_user(user_info.first)
|
||||
|
|
|
@ -19,17 +19,18 @@ module Omniauthable
|
|||
end
|
||||
|
||||
class_methods do
|
||||
def find_for_oauth(auth, signed_in_resource = nil)
|
||||
def find_for_omniauth(auth, signed_in_resource = nil)
|
||||
# EOLE-SSO Patch
|
||||
auth.uid = (auth.uid[0][:uid] || auth.uid[0][:user]) if auth.uid.is_a? Hashie::Array
|
||||
identity = Identity.find_for_oauth(auth)
|
||||
identity = Identity.find_for_omniauth(auth)
|
||||
|
||||
# If a signed_in_resource is provided it always overrides the existing user
|
||||
# to prevent the identity being locked with accidentally created accounts.
|
||||
# Note that this may leave zombie accounts (with no associated identity) which
|
||||
# can be cleaned up at a later date.
|
||||
user = signed_in_resource || identity.user
|
||||
user ||= create_for_oauth(auth)
|
||||
user ||= reattach_for_auth(auth)
|
||||
user ||= create_for_auth(auth)
|
||||
|
||||
if identity.user.nil?
|
||||
identity.user = user
|
||||
|
@ -39,19 +40,35 @@ module Omniauthable
|
|||
user
|
||||
end
|
||||
|
||||
def create_for_oauth(auth)
|
||||
# Check if the user exists with provided email. If no email was provided,
|
||||
private
|
||||
|
||||
def reattach_for_auth(auth)
|
||||
# If allowed, check if a user exists with the provided email address,
|
||||
# and return it if they does not have an associated identity with the
|
||||
# current authentication provider.
|
||||
|
||||
# This can be used to provide a choice of alternative auth providers
|
||||
# or provide smooth gradual transition between multiple auth providers,
|
||||
# but this is discouraged because any insecure provider will put *all*
|
||||
# local users at risk, regardless of which provider they registered with.
|
||||
|
||||
return unless ENV['ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH'] == 'true'
|
||||
|
||||
email, email_is_verified = email_from_auth(auth)
|
||||
return unless email_is_verified
|
||||
|
||||
user = User.find_by(email: email)
|
||||
return if user.nil? || Identity.exists?(provider: auth.provider, user_id: user.id)
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
def create_for_auth(auth)
|
||||
# Create a user for the given auth params. If no email was provided,
|
||||
# we assign a temporary email and ask the user to verify it on
|
||||
# the next step via Auth::SetupController.show
|
||||
|
||||
strategy = Devise.omniauth_configs[auth.provider.to_sym].strategy
|
||||
assume_verified = strategy&.security&.assume_email_is_verified
|
||||
email_is_verified = auth.info.verified || auth.info.verified_email || auth.info.email_verified || assume_verified
|
||||
email = auth.info.verified_email || auth.info.email
|
||||
|
||||
user = User.find_by(email: email) if email_is_verified
|
||||
|
||||
return user unless user.nil?
|
||||
email, email_is_verified = email_from_auth(auth)
|
||||
|
||||
user = User.new(user_params_from_auth(email, auth))
|
||||
|
||||
|
@ -61,7 +78,14 @@ module Omniauthable
|
|||
user
|
||||
end
|
||||
|
||||
private
|
||||
def email_from_auth(auth)
|
||||
strategy = Devise.omniauth_configs[auth.provider.to_sym].strategy
|
||||
assume_verified = strategy&.security&.assume_email_is_verified
|
||||
email_is_verified = auth.info.verified || auth.info.verified_email || auth.info.email_verified || assume_verified
|
||||
email = auth.info.verified_email || auth.info.email
|
||||
|
||||
[email, email_is_verified]
|
||||
end
|
||||
|
||||
def user_params_from_auth(email, auth)
|
||||
{
|
||||
|
|
|
@ -16,8 +16,8 @@ class Form::AccountBatch
|
|||
unfollow!
|
||||
when 'remove_from_followers'
|
||||
remove_from_followers!
|
||||
when 'block_domains'
|
||||
block_domains!
|
||||
when 'remove_domains_from_followers'
|
||||
remove_domains_from_followers!
|
||||
when 'approve'
|
||||
approve!
|
||||
when 'reject'
|
||||
|
@ -34,9 +34,15 @@ class Form::AccountBatch
|
|||
private
|
||||
|
||||
def follow!
|
||||
error = nil
|
||||
|
||||
accounts.each do |target_account|
|
||||
FollowService.new.call(current_account, target_account)
|
||||
rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound => e
|
||||
error ||= e
|
||||
end
|
||||
|
||||
raise error if error.present?
|
||||
end
|
||||
|
||||
def unfollow!
|
||||
|
@ -49,10 +55,8 @@ class Form::AccountBatch
|
|||
RemoveFromFollowersService.new.call(current_account, account_ids)
|
||||
end
|
||||
|
||||
def block_domains!
|
||||
AfterAccountDomainBlockWorker.push_bulk(account_domains) do |domain|
|
||||
[current_account.id, domain]
|
||||
end
|
||||
def remove_domains_from_followers!
|
||||
RemoveDomainsFromFollowersService.new.call(current_account, account_domains)
|
||||
end
|
||||
|
||||
def account_domains
|
||||
|
|
|
@ -12,11 +12,11 @@
|
|||
#
|
||||
|
||||
class Identity < ApplicationRecord
|
||||
belongs_to :user, dependent: :destroy
|
||||
belongs_to :user
|
||||
validates :uid, presence: true, uniqueness: { scope: :provider }
|
||||
validates :provider, presence: true
|
||||
|
||||
def self.find_for_oauth(auth)
|
||||
def self.find_for_omniauth(auth)
|
||||
find_or_create_by(uid: auth.uid, provider: auth.provider)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -156,7 +156,7 @@ class MediaAttachment < ApplicationRecord
|
|||
}.freeze
|
||||
|
||||
GLOBAL_CONVERT_OPTIONS = {
|
||||
all: '-quality 90 -strip +set modify-date +set create-date',
|
||||
all: '-quality 90 -strip +set date:modify +set date:create +set date:timestamp',
|
||||
}.freeze
|
||||
|
||||
belongs_to :account, inverse_of: :media_attachments, optional: true
|
||||
|
|
|
@ -85,6 +85,7 @@ class Poll < ApplicationRecord
|
|||
def reset_votes!
|
||||
self.cached_tallies = options.map { 0 }
|
||||
self.votes_count = 0
|
||||
self.voters_count = 0
|
||||
votes.delete_all unless new_record?
|
||||
end
|
||||
|
||||
|
|
|
@ -60,13 +60,13 @@ class RelationshipFilter
|
|||
def relationship_scope(value)
|
||||
case value
|
||||
when 'following'
|
||||
account.following.eager_load(:account_stat).reorder(nil)
|
||||
account.following.includes(:account_stat).reorder(nil)
|
||||
when 'followed_by'
|
||||
account.followers.eager_load(:account_stat).reorder(nil)
|
||||
account.followers.includes(:account_stat).reorder(nil)
|
||||
when 'mutual'
|
||||
account.followers.eager_load(:account_stat).reorder(nil).merge(Account.where(id: account.following))
|
||||
account.followers.includes(:account_stat).reorder(nil).merge(Account.where(id: account.following))
|
||||
when 'invited'
|
||||
Account.joins(user: :invite).merge(Invite.where(user: account.user)).eager_load(:account_stat).reorder(nil)
|
||||
Account.joins(user: :invite).merge(Invite.where(user: account.user)).includes(:account_stat).reorder(nil)
|
||||
else
|
||||
raise "Unknown relationship: #{value}"
|
||||
end
|
||||
|
@ -112,7 +112,7 @@ class RelationshipFilter
|
|||
def activity_scope(value)
|
||||
case value
|
||||
when 'dormant'
|
||||
AccountStat.where(last_status_at: nil).or(AccountStat.where(AccountStat.arel_table[:last_status_at].lt(1.month.ago)))
|
||||
Account.joins(:account_stat).where(account_stat: { last_status_at: [nil, ...1.month.ago] })
|
||||
else
|
||||
raise "Unknown activity: #{value}"
|
||||
end
|
||||
|
|
|
@ -38,7 +38,10 @@ class Report < ApplicationRecord
|
|||
scope :resolved, -> { where.not(action_taken_at: nil) }
|
||||
scope :with_accounts, -> { includes([:account, :target_account, :action_taken_by_account, :assigned_account].index_with({ user: [:invite_request, :invite] })) }
|
||||
|
||||
validates :comment, length: { maximum: 1_000 }
|
||||
# A report is considered local if the reporter is local
|
||||
delegate :local?, to: :account
|
||||
|
||||
validates :comment, length: { maximum: 1_000 }, if: :local?
|
||||
validates :rule_ids, absence: true, unless: :violation?
|
||||
|
||||
validate :validate_rule_ids
|
||||
|
@ -49,10 +52,6 @@ class Report < ApplicationRecord
|
|||
violation: 2_000,
|
||||
}
|
||||
|
||||
def local?
|
||||
false # Force uri_for to use uri attribute
|
||||
end
|
||||
|
||||
before_validation :set_uri, only: :create
|
||||
|
||||
def object_type
|
||||
|
|
|
@ -345,13 +345,25 @@ class Status < ApplicationRecord
|
|||
|
||||
account_ids.uniq!
|
||||
|
||||
status_ids = cached_items.map { |item| item.reblog? ? item.reblog_of_id : item.id }.uniq
|
||||
|
||||
return if account_ids.empty?
|
||||
|
||||
accounts = Account.where(id: account_ids).includes(:account_stat, :user).index_by(&:id)
|
||||
|
||||
status_stats = StatusStat.where(status_id: status_ids).index_by(&:status_id)
|
||||
|
||||
cached_items.each do |item|
|
||||
item.account = accounts[item.account_id]
|
||||
item.reblog.account = accounts[item.reblog.account_id] if item.reblog?
|
||||
|
||||
if item.reblog?
|
||||
status_stat = status_stats[item.reblog.id]
|
||||
item.reblog.status_stat = status_stat if status_stat.present?
|
||||
else
|
||||
status_stat = status_stats[item.id]
|
||||
item.status_stat = status_stat if status_stat.present?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -75,7 +75,7 @@ class Trends::Statuses < Trends::Base
|
|||
private
|
||||
|
||||
def eligible?(status)
|
||||
status.public_visibility? && status.account.discoverable? && !status.account.silenced? && status.spoiler_text.blank? && !status.sensitive? && !status.reply?
|
||||
status.public_visibility? && status.account.discoverable? && !status.account.silenced? && !status.account.sensitized? && status.spoiler_text.blank? && !status.sensitive? && !status.reply?
|
||||
end
|
||||
|
||||
def calculate_scores(statuses, at_time)
|
||||
|
|
|
@ -340,6 +340,25 @@ class User < ApplicationRecord
|
|||
super
|
||||
end
|
||||
|
||||
def revoke_access!
|
||||
Doorkeeper::AccessGrant.by_resource_owner(self).update_all(revoked_at: Time.now.utc)
|
||||
|
||||
Doorkeeper::AccessToken.by_resource_owner(self).in_batches do |batch|
|
||||
batch.update_all(revoked_at: Time.now.utc)
|
||||
Web::PushSubscription.where(access_token_id: batch).delete_all
|
||||
|
||||
# Revoke each access token for the Streaming API, since `update_all``
|
||||
# doesn't trigger ActiveRecord Callbacks:
|
||||
# TODO: #28793 Combine into a single topic
|
||||
payload = Oj.dump(event: :kill)
|
||||
redis.pipelined do |pipeline|
|
||||
batch.ids.each do |id|
|
||||
pipeline.publish("timeline:access_token:#{id}", payload)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def reset_password!
|
||||
# First, change password to something random and deactivate all sessions
|
||||
transaction do
|
||||
|
@ -348,12 +367,7 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
# Then, remove all authorized applications and connected push subscriptions
|
||||
Doorkeeper::AccessGrant.by_resource_owner(self).in_batches.update_all(revoked_at: Time.now.utc)
|
||||
|
||||
Doorkeeper::AccessToken.by_resource_owner(self).in_batches do |batch|
|
||||
batch.update_all(revoked_at: Time.now.utc)
|
||||
Web::PushSubscription.where(access_token_id: batch).delete_all
|
||||
end
|
||||
revoke_access!
|
||||
|
||||
# Finally, send a reset password prompt to the user
|
||||
send_reset_password_instructions
|
||||
|
@ -442,10 +456,13 @@ class User < ApplicationRecord
|
|||
def prepare_new_user!
|
||||
BootstrapTimelineWorker.perform_async(account_id)
|
||||
ActivityTracker.increment('activity:accounts:local')
|
||||
ActivityTracker.record('activity:logins', id)
|
||||
UserMailer.welcome(self).deliver_later
|
||||
end
|
||||
|
||||
def prepare_returning_user!
|
||||
return unless confirmed?
|
||||
|
||||
ActivityTracker.record('activity:logins', id)
|
||||
regenerate_feed! if needs_feed_update?
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BackupPolicy < ApplicationPolicy
|
||||
MIN_AGE = 1.week
|
||||
MIN_AGE = 6.days
|
||||
|
||||
def create?
|
||||
user_signed_in? && current_user.backups.where('created_at >= ?', MIN_AGE.ago).count.zero?
|
||||
|
|
|
@ -15,6 +15,16 @@ class REST::AccountSerializer < ActiveModel::Serializer
|
|||
attribute :suspended, if: :suspended?
|
||||
attribute :silenced, key: :limited, if: :silenced?
|
||||
|
||||
class AccountDecorator < SimpleDelegator
|
||||
def self.model_name
|
||||
Account.model_name
|
||||
end
|
||||
|
||||
def moved?
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
class FieldSerializer < ActiveModel::Serializer
|
||||
include FormattingHelper
|
||||
|
||||
|
@ -84,7 +94,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
|
|||
end
|
||||
|
||||
def moved_to_account
|
||||
object.suspended? ? nil : object.moved_to_account
|
||||
object.suspended? ? nil : AccountDecorator.new(object.moved_to_account)
|
||||
end
|
||||
|
||||
def emojis
|
||||
|
@ -106,6 +116,6 @@ class REST::AccountSerializer < ActiveModel::Serializer
|
|||
delegate :suspended?, :silenced?, to: :object
|
||||
|
||||
def moved_and_not_nested?
|
||||
object.moved? && object.moved_to_account.moved_to_account_id.nil?
|
||||
object.moved?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,4 +11,8 @@ class REST::PreviewCardSerializer < ActiveModel::Serializer
|
|||
def image
|
||||
object.image? ? full_asset_url(object.image.url(:original)) : nil
|
||||
end
|
||||
|
||||
def html
|
||||
Sanitize.fragment(object.html, Sanitize::Config::MASTODON_OEMBED)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
class ActivityPub::FetchFeaturedCollectionService < BaseService
|
||||
include JsonLdHelper
|
||||
|
||||
def call(account)
|
||||
def call(account, **options)
|
||||
return if account.featured_collection_url.blank? || account.suspended? || account.local?
|
||||
|
||||
@account = account
|
||||
@options = options
|
||||
@json = fetch_resource(@account.featured_collection_url, true, local_follower)
|
||||
|
||||
return unless supported_context?(@json)
|
||||
|
@ -38,9 +39,9 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService
|
|||
def process_items(items)
|
||||
status_ids = items.filter_map do |item|
|
||||
uri = value_or_id(item)
|
||||
next if ActivityPub::TagManager.instance.local_uri?(uri)
|
||||
next if ActivityPub::TagManager.instance.local_uri?(uri) || invalid_origin?(uri)
|
||||
|
||||
status = ActivityPub::FetchRemoteStatusService.new.call(uri, on_behalf_of: local_follower)
|
||||
status = ActivityPub::FetchRemoteStatusService.new.call(uri, on_behalf_of: local_follower, expected_actor_uri: @account.uri, request_id: @options[:request_id])
|
||||
next unless status&.account_id == @account.id
|
||||
|
||||
status.id
|
||||
|
|
|
@ -8,15 +8,15 @@ class ActivityPub::FetchRemoteAccountService < BaseService
|
|||
SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
|
||||
|
||||
# Does a WebFinger roundtrip on each call, unless `only_key` is true
|
||||
def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false)
|
||||
def call(uri, prefetched_body: nil, break_on_redirect: false, only_key: false, request_id: nil)
|
||||
return if domain_not_allowed?(uri)
|
||||
return ActivityPub::TagManager.instance.uri_to_resource(uri, Account) if ActivityPub::TagManager.instance.local_uri?(uri)
|
||||
|
||||
@json = begin
|
||||
if prefetched_body.nil?
|
||||
fetch_resource(uri, id)
|
||||
fetch_resource(uri, true)
|
||||
else
|
||||
body_to_json(prefetched_body, compare_id: id ? uri : nil)
|
||||
body_to_json(prefetched_body, compare_id: uri)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -28,7 +28,7 @@ class ActivityPub::FetchRemoteAccountService < BaseService
|
|||
|
||||
return unless only_key || verified_webfinger?
|
||||
|
||||
ActivityPub::ProcessAccountService.new.call(@username, @domain, @json, only_key: only_key, verified_webfinger: !only_key)
|
||||
ActivityPub::ProcessAccountService.new.call(@username, @domain, @json, only_key: only_key, verified_webfinger: !only_key, request_id: request_id)
|
||||
rescue Oj::ParseError
|
||||
nil
|
||||
end
|
||||
|
|
|
@ -4,23 +4,10 @@ class ActivityPub::FetchRemoteKeyService < BaseService
|
|||
include JsonLdHelper
|
||||
|
||||
# Returns account that owns the key
|
||||
def call(uri, id: true, prefetched_body: nil)
|
||||
def call(uri)
|
||||
return if uri.blank?
|
||||
|
||||
if prefetched_body.nil?
|
||||
if id
|
||||
@json = fetch_resource_without_id_validation(uri)
|
||||
if person?
|
||||
@json = fetch_resource(@json['id'], true)
|
||||
elsif uri != @json['id']
|
||||
return
|
||||
end
|
||||
else
|
||||
@json = fetch_resource(uri, id)
|
||||
end
|
||||
else
|
||||
@json = body_to_json(prefetched_body, compare_id: id ? uri : nil)
|
||||
end
|
||||
@json = fetch_resource(uri, false)
|
||||
|
||||
return unless supported_context?(@json) && expected_type?
|
||||
return find_account(@json['id'], @json) if person?
|
||||
|
|
|
@ -2,14 +2,18 @@
|
|||
|
||||
class ActivityPub::FetchRemoteStatusService < BaseService
|
||||
include JsonLdHelper
|
||||
include Redisable
|
||||
|
||||
DISCOVERIES_PER_REQUEST = 1000
|
||||
|
||||
# Should be called when uri has already been checked for locality
|
||||
def call(uri, id: true, prefetched_body: nil, on_behalf_of: nil)
|
||||
def call(uri, prefetched_body: nil, on_behalf_of: nil, expected_actor_uri: nil, request_id: nil)
|
||||
@request_id = request_id || "#{Time.now.utc.to_i}-status-#{uri}"
|
||||
@json = begin
|
||||
if prefetched_body.nil?
|
||||
fetch_resource(uri, id, on_behalf_of)
|
||||
fetch_resource(uri, true, on_behalf_of)
|
||||
else
|
||||
body_to_json(prefetched_body, compare_id: id ? uri : nil)
|
||||
body_to_json(prefetched_body, compare_id: uri)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -30,6 +34,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService
|
|||
end
|
||||
|
||||
return if activity_json.nil? || object_uri.nil? || !trustworthy_attribution?(@json['id'], actor_uri)
|
||||
return if expected_actor_uri.present? && actor_uri != expected_actor_uri
|
||||
return ActivityPub::TagManager.instance.uri_to_resource(object_uri, Status) if ActivityPub::TagManager.instance.local_uri?(object_uri)
|
||||
|
||||
actor = account_from_uri(actor_uri)
|
||||
|
@ -40,7 +45,13 @@ class ActivityPub::FetchRemoteStatusService < BaseService
|
|||
# activity as an update rather than create
|
||||
activity_json['type'] = 'Update' if equals_or_includes_any?(activity_json['type'], %w(Create)) && Status.where(uri: object_uri, account_id: actor.id).exists?
|
||||
|
||||
ActivityPub::Activity.factory(activity_json, actor).perform
|
||||
with_redis do |redis|
|
||||
discoveries = redis.incr("status_discovery_per_request:#{@request_id}")
|
||||
redis.expire("status_discovery_per_request:#{@request_id}", 5.minutes.seconds)
|
||||
return nil if discoveries > DISCOVERIES_PER_REQUEST
|
||||
end
|
||||
|
||||
ActivityPub::Activity.factory(activity_json, actor, request_id: @request_id).perform
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -52,7 +63,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService
|
|||
|
||||
def account_from_uri(uri)
|
||||
actor = ActivityPub::TagManager.instance.uri_to_resource(uri, Account)
|
||||
actor = ActivityPub::FetchRemoteAccountService.new.call(uri, id: true) if actor.nil? || actor.possibly_stale?
|
||||
actor = ActivityPub::FetchRemoteAccountService.new.call(uri, request_id: @request_id) if actor.nil? || actor.possibly_stale?
|
||||
actor
|
||||
end
|
||||
|
||||
|
|
|
@ -3,14 +3,14 @@
|
|||
class ActivityPub::FetchRepliesService < BaseService
|
||||
include JsonLdHelper
|
||||
|
||||
def call(parent_status, collection_or_uri, allow_synchronous_requests = true)
|
||||
def call(parent_status, collection_or_uri, allow_synchronous_requests: true, request_id: nil)
|
||||
@account = parent_status.account
|
||||
@allow_synchronous_requests = allow_synchronous_requests
|
||||
|
||||
@items = collection_items(collection_or_uri)
|
||||
return if @items.nil?
|
||||
|
||||
FetchReplyWorker.push_bulk(filtered_replies)
|
||||
FetchReplyWorker.push_bulk(filtered_replies) { |reply_uri| [reply_uri, { 'request_id' => request_id}] }
|
||||
|
||||
@items
|
||||
end
|
||||
|
|
|
@ -6,6 +6,9 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
include Redisable
|
||||
include Lockable
|
||||
|
||||
SUBDOMAINS_RATELIMIT = 10
|
||||
DISCOVERIES_PER_REQUEST = 400
|
||||
|
||||
# Should be called with confirmed valid JSON
|
||||
# and WebFinger-resolved username and domain
|
||||
def call(username, domain, json, options = {})
|
||||
|
@ -15,9 +18,12 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
@json = json
|
||||
@uri = @json['id']
|
||||
@username = username
|
||||
@domain = domain
|
||||
@domain = TagManager.instance.normalize_domain(domain)
|
||||
@collections = {}
|
||||
|
||||
# The key does not need to be unguessable, it just needs to be somewhat unique
|
||||
@options[:request_id] ||= "#{Time.now.utc.to_i}-#{username}@#{domain}"
|
||||
|
||||
with_lock("process_account:#{@uri}") do
|
||||
@account = Account.remote.find_by(uri: @uri) if @options[:only_key]
|
||||
@account ||= Account.find_remote(@username, @domain)
|
||||
|
@ -25,7 +31,18 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
@old_protocol = @account&.protocol
|
||||
@suspension_changed = false
|
||||
|
||||
create_account if @account.nil?
|
||||
if @account.nil?
|
||||
with_redis do |redis|
|
||||
return nil if redis.pfcount("unique_subdomains_for:#{PublicSuffix.domain(@domain, ignore_private: true)}") >= SUBDOMAINS_RATELIMIT
|
||||
|
||||
discoveries = redis.incr("discovery_per_request:#{@options[:request_id]}")
|
||||
redis.expire("discovery_per_request:#{@options[:request_id]}", 5.minutes.seconds)
|
||||
return nil if discoveries > DISCOVERIES_PER_REQUEST
|
||||
end
|
||||
|
||||
create_account
|
||||
end
|
||||
|
||||
update_account
|
||||
process_tags
|
||||
|
||||
|
@ -60,6 +77,9 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
@account.suspended_at = domain_block.created_at if auto_suspend?
|
||||
@account.suspension_origin = :local if auto_suspend?
|
||||
@account.silenced_at = domain_block.created_at if auto_silence?
|
||||
|
||||
set_immediate_protocol_attributes!
|
||||
|
||||
@account.save
|
||||
end
|
||||
|
||||
|
@ -149,7 +169,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
end
|
||||
|
||||
def check_featured_collection!
|
||||
ActivityPub::SynchronizeFeaturedCollectionWorker.perform_async(@account.id)
|
||||
ActivityPub::SynchronizeFeaturedCollectionWorker.perform_async(@account.id, { 'request_id' => @options[:request_id] })
|
||||
end
|
||||
|
||||
def check_links!
|
||||
|
@ -249,7 +269,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
|
||||
def moved_account
|
||||
account = ActivityPub::TagManager.instance.uri_to_resource(@json['movedTo'], Account)
|
||||
account ||= ActivityPub::FetchRemoteAccountService.new.call(@json['movedTo'], id: true, break_on_redirect: true)
|
||||
account ||= ActivityPub::FetchRemoteAccountService.new.call(@json['movedTo'], break_on_redirect: true, request_id: @options[:request_id])
|
||||
account
|
||||
end
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
|||
include Redisable
|
||||
include Lockable
|
||||
|
||||
def call(status, json)
|
||||
def call(status, json, request_id: nil)
|
||||
raise ArgumentError, 'Status has unsaved changes' if status.changed?
|
||||
|
||||
@json = json
|
||||
|
@ -15,6 +15,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
|||
@account = status.account
|
||||
@media_attachments_changed = false
|
||||
@poll_changed = false
|
||||
@request_id = request_id
|
||||
|
||||
# Only native types can be updated at the moment
|
||||
return @status if !expected_type? || already_updated_more_recently?
|
||||
|
@ -92,7 +93,13 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
|||
|
||||
next if unsupported_media_type?(media_attachment_parser.file_content_type) || skip_download?
|
||||
|
||||
RedownloadMediaWorker.perform_async(media_attachment.id) if media_attachment.remote_url_previously_changed? || media_attachment.thumbnail_remote_url_previously_changed?
|
||||
begin
|
||||
media_attachment.download_file! if media_attachment.remote_url_previously_changed?
|
||||
media_attachment.download_thumbnail! if media_attachment.thumbnail_remote_url_previously_changed?
|
||||
media_attachment.save
|
||||
rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError
|
||||
RedownloadMediaWorker.perform_in(rand(30..600).seconds, media_attachment.id)
|
||||
end
|
||||
rescue Addressable::URI::InvalidURIError => e
|
||||
Rails.logger.debug "Invalid URL in attachment: #{e}"
|
||||
end
|
||||
|
@ -185,7 +192,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
|||
next if href.blank?
|
||||
|
||||
account = ActivityPub::TagManager.instance.uri_to_resource(href, Account)
|
||||
account ||= ActivityPub::FetchRemoteAccountService.new.call(href)
|
||||
account ||= ActivityPub::FetchRemoteAccountService.new.call(href, request_id: @request_id)
|
||||
|
||||
next if account.nil?
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FetchRemoteStatusService < BaseService
|
||||
def call(url, prefetched_body = nil)
|
||||
def call(url, prefetched_body: nil, request_id: nil)
|
||||
if prefetched_body.nil?
|
||||
resource_url, resource_options = FetchResourceService.new.call(url)
|
||||
else
|
||||
|
@ -9,6 +9,6 @@ class FetchRemoteStatusService < BaseService
|
|||
resource_options = { prefetched_body: prefetched_body }
|
||||
end
|
||||
|
||||
ActivityPub::FetchRemoteStatusService.new.call(resource_url, **resource_options) unless resource_url.nil?
|
||||
ActivityPub::FetchRemoteStatusService.new.call(resource_url, **resource_options.merge(request_id: request_id)) unless resource_url.nil?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -47,7 +47,15 @@ class FetchResourceService < BaseService
|
|||
body = response.body_with_limit
|
||||
json = body_to_json(body)
|
||||
|
||||
[json['id'], { prefetched_body: body, id: true }] if supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) || expected_type?(json))
|
||||
return unless supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) || expected_type?(json))
|
||||
|
||||
if json['id'] != @url
|
||||
return if terminal
|
||||
|
||||
return process(json['id'], terminal: true)
|
||||
end
|
||||
|
||||
[@url, { prefetched_body: body }]
|
||||
elsif !terminal
|
||||
link_header = response['Link'] && parse_link_header(response)
|
||||
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FollowMigrationService < FollowService
|
||||
# Follow an account with the same settings as another account, and unfollow the old account once the request is sent
|
||||
# @param [Account] source_account From which to follow
|
||||
# @param [Account] target_account Account to follow
|
||||
# @param [Account] old_target_account Account to unfollow once the follow request has been sent to the new one
|
||||
# @option [Boolean] bypass_locked Whether to immediately follow the new account even if it is locked
|
||||
def call(source_account, target_account, old_target_account, bypass_locked: false)
|
||||
@old_target_account = old_target_account
|
||||
|
||||
follow = source_account.active_relationships.find_by(target_account: old_target_account)
|
||||
reblogs = follow&.show_reblogs?
|
||||
notify = follow&.notify?
|
||||
|
||||
super(source_account, target_account, reblogs: reblogs, notify: notify, bypass_locked: bypass_locked, bypass_limit: true)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def request_follow!
|
||||
follow_request = @source_account.request_follow!(@target_account, **follow_options.merge(rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit]))
|
||||
|
||||
if @target_account.local?
|
||||
LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, 'follow_request')
|
||||
UnfollowService.new.call(@source_account, @old_target_account, skip_unmerge: true)
|
||||
elsif @target_account.activitypub?
|
||||
ActivityPub::MigratedFollowDeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url, @old_target_account.id)
|
||||
end
|
||||
|
||||
follow_request
|
||||
end
|
||||
|
||||
def direct_follow!
|
||||
follow = super
|
||||
UnfollowService.new.call(@source_account, @old_target_account, skip_unmerge: true)
|
||||
follow
|
||||
end
|
||||
|
||||
def follow_options
|
||||
@options.slice(:reblogs, :notify)
|
||||
end
|
||||
end
|
|
@ -70,7 +70,7 @@ class FollowService < BaseService
|
|||
if @target_account.local?
|
||||
LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, 'follow_request')
|
||||
elsif @target_account.activitypub?
|
||||
ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url)
|
||||
ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url, { 'bypass_availability' => true })
|
||||
end
|
||||
|
||||
follow_request
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveDomainsFromFollowersService < BaseService
|
||||
include Payloadable
|
||||
|
||||
def call(source_account, target_domains)
|
||||
source_account.passive_relationships.where(account_id: Account.where(domain: target_domains)).find_each do |follow|
|
||||
follow.destroy
|
||||
|
||||
create_notification(follow) if source_account.local? && !follow.account.local? && follow.account.activitypub?
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_notification(follow)
|
||||
ActivityPub::DeliveryWorker.perform_async(build_json(follow), follow.target_account_id, follow.account.inbox_url)
|
||||
end
|
||||
|
||||
def build_json(follow)
|
||||
Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer))
|
||||
end
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue