mirror of
https://github.com/yingziwu/mastodon.git
synced 2026-02-11 23:15:17 +00:00
Merge tag 'v4.2.20'
This commit is contained in:
commit
2fbf224392
62 changed files with 1267 additions and 317 deletions
160
.github/workflows/build-container-image.yml
vendored
160
.github/workflows/build-container-image.yml
vendored
|
|
@ -1,14 +1,9 @@
|
|||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
platforms:
|
||||
required: true
|
||||
type: string
|
||||
cache:
|
||||
type: boolean
|
||||
default: true
|
||||
use_native_arm64_builder:
|
||||
type: boolean
|
||||
push_to_images:
|
||||
type: string
|
||||
version_prerelease:
|
||||
|
|
@ -22,42 +17,36 @@ on:
|
|||
labels:
|
||||
type: string
|
||||
|
||||
# This builds multiple images with one runner each, allowing us to build for multiple architectures
|
||||
# using Github's runners.
|
||||
# The two-step process is adapted form:
|
||||
# https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners
|
||||
jobs:
|
||||
# Build each (amd64 and arm64) image separately
|
||||
build-image:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ startsWith(matrix.platform, 'linux/arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: docker/setup-qemu-action@v2
|
||||
if: contains(inputs.platforms, 'linux/arm64') && !inputs.use_native_arm64_builder
|
||||
- name: Prepare
|
||||
env:
|
||||
PUSH_TO_IMAGES: ${{ inputs.push_to_images }}
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
# Transform multi-line variable into comma-separated variable
|
||||
image_names=${PUSH_TO_IMAGES//$'\n'/,}
|
||||
echo "IMAGE_NAMES=${image_names%,}" >> $GITHUB_ENV
|
||||
|
||||
- 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')
|
||||
|
|
@ -74,8 +63,88 @@ jobs:
|
|||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: docker/metadata-action@v4
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
if: ${{ inputs.push_to_images != '' }}
|
||||
with:
|
||||
images: ${{ inputs.push_to_images }}
|
||||
flavor: ${{ inputs.flavor }}
|
||||
labels: ${{ inputs.labels }}
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
build-args: |
|
||||
MASTODON_VERSION_PRERELEASE=${{ inputs.version_prerelease }}
|
||||
MASTODON_VERSION_METADATA=${{ inputs.version_metadata }}
|
||||
SOURCE_COMMIT=${{ github.sha }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
provenance: false
|
||||
push: ${{ inputs.push_to_images != '' }}
|
||||
cache-from: ${{ inputs.cache && 'type=gha' || '' }}
|
||||
cache-to: ${{ inputs.cache && 'type=gha,mode=max' || '' }}
|
||||
outputs: type=image,"name=${{ env.IMAGE_NAMES }}",push-by-digest=true,name-canonical=true,push=${{ inputs.push_to_images != '' }}
|
||||
|
||||
- name: Export digest
|
||||
if: ${{ inputs.push_to_images != '' }}
|
||||
run: |
|
||||
mkdir -p "${{ runner.temp }}/digests"
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
if: ${{ inputs.push_to_images != '' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
# Then merge the docker images into a single one
|
||||
merge-images:
|
||||
if: ${{ inputs.push_to_images != '' }}
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- build-image
|
||||
|
||||
env:
|
||||
PUSH_TO_IMAGES: ${{ inputs.push_to_images }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
if: contains(inputs.push_to_images, 'tootsuite')
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Log in to the GitHub Container registry
|
||||
if: contains(inputs.push_to_images, 'ghcr.io')
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
if: ${{ inputs.push_to_images != '' }}
|
||||
with:
|
||||
images: ${{ inputs.push_to_images }}
|
||||
|
|
@ -83,17 +152,14 @@ jobs:
|
|||
tags: ${{ inputs.tags }}
|
||||
labels: ${{ inputs.labels }}
|
||||
|
||||
- uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
build-args: |
|
||||
MASTODON_VERSION_PRERELEASE=${{ inputs.version_prerelease }}
|
||||
MASTODON_VERSION_METADATA=${{ inputs.version_metadata }}
|
||||
platforms: ${{ inputs.platforms }}
|
||||
provenance: false
|
||||
builder: ${{ steps.buildx.outputs.name || steps.buildx-native.outputs.name }}
|
||||
push: ${{ inputs.push_to_images != '' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: ${{ inputs.cache && 'type=gha' || '' }}
|
||||
cache-to: ${{ inputs.cache && 'type=gha,mode=max' || '' }}
|
||||
- name: Create manifest list and push
|
||||
working-directory: ${{ runner.temp }}/digests
|
||||
run: |
|
||||
echo "$PUSH_TO_IMAGES" | xargs -I{} \
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '{}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
echo "$PUSH_TO_IMAGES" | xargs -i{} \
|
||||
docker buildx imagetools inspect {}:${{ steps.meta.outputs.version }}
|
||||
|
|
|
|||
2
.github/workflows/build-nightly.yml
vendored
2
.github/workflows/build-nightly.yml
vendored
|
|
@ -24,8 +24,6 @@ jobs:
|
|||
needs: compute-suffix
|
||||
uses: ./.github/workflows/build-container-image.yml
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
use_native_arm64_builder: true
|
||||
cache: false
|
||||
push_to_images: |
|
||||
tootsuite/mastodon
|
||||
|
|
|
|||
2
.github/workflows/build-push-pr.yml
vendored
2
.github/workflows/build-push-pr.yml
vendored
|
|
@ -29,8 +29,6 @@ jobs:
|
|||
needs: compute-suffix
|
||||
uses: ./.github/workflows/build-container-image.yml
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
use_native_arm64_builder: true
|
||||
push_to_images: |
|
||||
ghcr.io/mastodon/mastodon
|
||||
version_metadata: ${{ needs.compute-suffix.outputs.metadata }}
|
||||
|
|
|
|||
2
.github/workflows/build-releases.yml
vendored
2
.github/workflows/build-releases.yml
vendored
|
|
@ -12,8 +12,6 @@ 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
|
||||
|
|
|
|||
2
.github/workflows/test-image-build.yml
vendored
2
.github/workflows/test-image-build.yml
vendored
|
|
@ -17,5 +17,3 @@ jobs:
|
|||
cancel-in-progress: true
|
||||
|
||||
uses: ./.github/workflows/build-container-image.yml
|
||||
with:
|
||||
platforms: linux/amd64 # Testing only on native platform so it is performant
|
||||
|
|
|
|||
22
.github/workflows/test-ruby.yml
vendored
22
.github/workflows/test-ruby.yml
vendored
|
|
@ -58,7 +58,7 @@ jobs:
|
|||
run: |-
|
||||
./bin/rails assets:precompile
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: matrix.mode == 'test'
|
||||
with:
|
||||
path: |-
|
||||
|
|
@ -118,9 +118,9 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
ruby-version:
|
||||
- '3.0'
|
||||
- '3.1'
|
||||
- '.ruby-version'
|
||||
- '3.3'
|
||||
ci_job:
|
||||
- 1
|
||||
- 2
|
||||
|
|
@ -129,7 +129,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: './public'
|
||||
name: ${{ github.sha }}
|
||||
|
|
@ -197,14 +197,14 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
ruby-version:
|
||||
- '3.0'
|
||||
- '3.1'
|
||||
- '.ruby-version'
|
||||
- '3.3'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: './public'
|
||||
name: ${{ github.sha }}
|
||||
|
|
@ -238,14 +238,14 @@ jobs:
|
|||
- run: bundle exec rake spec:system
|
||||
|
||||
- name: Archive logs
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: e2e-logs-${{ matrix.ruby-version }}
|
||||
path: log/
|
||||
|
||||
- name: Archive test screenshots
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: e2e-screenshots
|
||||
|
|
@ -310,14 +310,14 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
ruby-version:
|
||||
- '3.0'
|
||||
- '3.1'
|
||||
- '.ruby-version'
|
||||
- '3.3'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: './public'
|
||||
name: ${{ github.sha }}
|
||||
|
|
@ -351,14 +351,14 @@ jobs:
|
|||
- run: bundle exec rake spec:search
|
||||
|
||||
- name: Archive logs
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: test-search-logs-${{ matrix.ruby-version }}
|
||||
path: log/
|
||||
|
||||
- name: Archive test screenshots
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: test-search-screenshots
|
||||
|
|
|
|||
66
CHANGELOG.md
66
CHANGELOG.md
|
|
@ -2,6 +2,72 @@
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [4.2.20] - 2025-04-02
|
||||
|
||||
### Add
|
||||
|
||||
- Add delay to profile updates to debounce them (#34137 by @ClearlyClaire)
|
||||
- Add support for paginating partial collections in `SynchronizeFollowersService` (#34272 and #34277 by @ClearlyClaire)
|
||||
|
||||
### Changed
|
||||
|
||||
- Change account suspensions to be federated to recently-followed accounts as well (#34294 by @ClearlyClaire)
|
||||
- Change `AccountReachFinder` to consider statuses based on suspension date (#32805 and #34291 by @ClearlyClaire and @mjankowski)
|
||||
- Change user archive signed URL TTL from 10 seconds to 1 hour (#34254 by @ClearlyClaire)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix incorrect URL being used when cache busting (#34189 by @ClearlyClaire)
|
||||
|
||||
## [4.2.19] - 2025-03-13
|
||||
|
||||
### Security
|
||||
|
||||
- Update dependency `omniauth-saml`
|
||||
- Update dependency `rack`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Stoplight errors when using `REDIS_NAMESPACE` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/34126))
|
||||
|
||||
## [4.2.18] - 2025-03-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Change hashtag suggestion to prefer personal history capitalization (#34070 by @ClearlyClaire)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix processing errors for some HEIF images from iOS 18 (#34086 by @renchap)
|
||||
- Fix streaming server not filtering unknown-language posts from public timelines (#33774 by @ClearlyClaire)
|
||||
|
||||
## [4.2.17] - 2025-02-27
|
||||
|
||||
### Security
|
||||
|
||||
- Update dependencies
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove support for Ruby 3.0
|
||||
|
||||
## [4.2.16] - 2025-02-27
|
||||
|
||||
### Security
|
||||
|
||||
- Update dependencies
|
||||
- Change HTML sanitization to remove unusable and unused `embed` tag (#34021 by @ClearlyClaire, [GHSA-mq2m-hr29-8gqf](https://github.com/mastodon/mastodon/security/advisories/GHSA-mq2m-hr29-8gqf))
|
||||
- Fix rate-limit on sign-up email verification ([GHSA-v39f-c9jj-8w7h](https://github.com/mastodon/mastodon/security/advisories/GHSA-v39f-c9jj-8w7h))
|
||||
- Fix improper disclosure of domain blocks to unverified users ([GHSA-94h4-fj37-c825](https://github.com/mastodon/mastodon/security/advisories/GHSA-94h4-fj37-c825))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix emoji rewrite adding unnecessary curft to the DOM for most emoji (#33818 by @ClearlyClaire)
|
||||
- Fix incorrect signature after HTTP redirect (#33757 and #33769 by @ClearlyClaire)
|
||||
- Fix polls not being validated on edition (#33755 by @ClearlyClaire)
|
||||
- Fix featured tags for remote accounts not being kept up to date (#33372, #33406, and #33425 by @ClearlyClaire and @mjankowski)
|
||||
- Fix exclusive lists interfering with notifications (#28162 by @ShadowJonathan)
|
||||
|
||||
## [4.2.15] - 2025-01-16
|
||||
|
||||
### Security
|
||||
|
|
|
|||
|
|
@ -57,6 +57,9 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|||
ENV DEBIAN_FRONTEND="noninteractive" \
|
||||
PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin"
|
||||
|
||||
# Add backport repository for some specific packages where we need the latest version
|
||||
RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list
|
||||
|
||||
# Ignoring these here since we don't want to pin any versions and the Debian image removes apt-get content after use
|
||||
# hadolint ignore=DL3008,DL3009
|
||||
RUN apt-get update && \
|
||||
|
|
@ -74,6 +77,7 @@ RUN apt-get update && \
|
|||
libicu72 \
|
||||
libidn12 \
|
||||
libyaml-0-2 \
|
||||
libheif1/bookworm-backports \
|
||||
file \
|
||||
ca-certificates \
|
||||
tzdata \
|
||||
|
|
|
|||
4
Gemfile
4
Gemfile
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
ruby '>= 3.0.0'
|
||||
ruby '>= 3.1.0'
|
||||
|
||||
gem 'puma', '~> 6.3'
|
||||
gem 'rails', '~> 7.0'
|
||||
|
|
@ -60,7 +60,7 @@ gem 'idn-ruby', require: 'idn'
|
|||
gem 'kaminari', '~> 1.2'
|
||||
gem 'link_header', '~> 0.0'
|
||||
gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar'
|
||||
gem 'nokogiri', '~> 1.15'
|
||||
gem 'nokogiri', '~> 1.17'
|
||||
gem 'nsa'
|
||||
gem 'oj', '~> 3.14'
|
||||
gem 'ox', '~> 2.14'
|
||||
|
|
|
|||
18
Gemfile.lock
18
Gemfile.lock
|
|
@ -455,7 +455,7 @@ GEM
|
|||
uri
|
||||
net-http-persistent (4.0.2)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.3.7)
|
||||
net-imap (0.3.8)
|
||||
date
|
||||
net-protocol
|
||||
net-ldap (0.18.0)
|
||||
|
|
@ -469,7 +469,7 @@ GEM
|
|||
net-protocol
|
||||
net-ssh (7.1.0)
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.16.8)
|
||||
nokogiri (1.18.3)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nsa (0.3.0)
|
||||
|
|
@ -478,16 +478,16 @@ GEM
|
|||
sidekiq (>= 3.5)
|
||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||
oj (3.16.1)
|
||||
omniauth (2.1.2)
|
||||
omniauth (2.1.3)
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 2.2.3)
|
||||
rack-protection
|
||||
omniauth-rails_csrf_protection (1.0.1)
|
||||
actionpack (>= 4.2)
|
||||
omniauth (~> 2.0)
|
||||
omniauth-saml (2.1.2)
|
||||
omniauth-saml (2.1.3)
|
||||
omniauth (~> 2.1)
|
||||
ruby-saml (~> 1.17)
|
||||
ruby-saml (~> 1.18)
|
||||
omniauth_openid_connect (0.6.1)
|
||||
omniauth (>= 1.9, < 3)
|
||||
openid_connect (~> 1.1)
|
||||
|
|
@ -533,7 +533,7 @@ GEM
|
|||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.10)
|
||||
rack (2.2.13)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-cors (2.0.2)
|
||||
|
|
@ -666,7 +666,7 @@ GEM
|
|||
rubocop-factory_bot (~> 2.22)
|
||||
ruby-prof (1.6.3)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-saml (1.17.0)
|
||||
ruby-saml (1.18.0)
|
||||
nokogiri (>= 1.13.10)
|
||||
rexml
|
||||
ruby2_keywords (0.0.5)
|
||||
|
|
@ -769,7 +769,7 @@ GEM
|
|||
unf_ext
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (2.4.2)
|
||||
uri (0.12.2)
|
||||
uri (0.12.4)
|
||||
validate_email (0.1.6)
|
||||
activemodel (>= 3.0)
|
||||
mail (>= 2.2.5)
|
||||
|
|
@ -878,7 +878,7 @@ DEPENDENCIES
|
|||
mime-types (~> 3.5.0)
|
||||
net-http (~> 0.3.2)
|
||||
net-ldap (~> 0.18)
|
||||
nokogiri (~> 1.15)
|
||||
nokogiri (~> 1.17)
|
||||
nsa
|
||||
oj (~> 3.14)
|
||||
omniauth (~> 2.0)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
|
|||
@account = current_account
|
||||
UpdateAccountService.new.call(@account, account_params, raise_error: true)
|
||||
current_user.update(user_params) if user_params
|
||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
||||
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
|
||||
render json: @account, serializer: REST::CredentialAccountSerializer
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -15,16 +15,40 @@ class Api::V1::Instances::DomainBlocksController < Api::BaseController
|
|||
cache_if_unauthenticated!
|
||||
end
|
||||
|
||||
render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: (Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?))
|
||||
render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: show_rationale_in_response?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def require_enabled_api!
|
||||
head 404 unless Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?)
|
||||
head 404 unless api_enabled?
|
||||
end
|
||||
|
||||
def api_enabled?
|
||||
show_domain_blocks_for_all? || show_domain_blocks_to_user?
|
||||
end
|
||||
|
||||
def show_domain_blocks_for_all?
|
||||
Setting.show_domain_blocks == 'all'
|
||||
end
|
||||
|
||||
def show_domain_blocks_to_user?
|
||||
Setting.show_domain_blocks == 'users' && user_signed_in? && current_user.functional_or_moved?
|
||||
end
|
||||
|
||||
def set_domain_blocks
|
||||
@domain_blocks = DomainBlock.with_user_facing_limitations.by_severity
|
||||
end
|
||||
|
||||
def show_rationale_in_response?
|
||||
always_show_rationale? || show_rationale_for_user?
|
||||
end
|
||||
|
||||
def always_show_rationale?
|
||||
Setting.show_domain_blocks_rationale == 'all'
|
||||
end
|
||||
|
||||
def show_rationale_for_user?
|
||||
Setting.show_domain_blocks_rationale == 'users' && user_signed_in? && current_user.functional_or_moved?
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ class Api::V1::Profile::AvatarsController < Api::BaseController
|
|||
def destroy
|
||||
@account = current_account
|
||||
UpdateAccountService.new.call(@account, { avatar: nil }, raise_error: true)
|
||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
||||
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
|
||||
render json: @account, serializer: REST::CredentialAccountSerializer
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ class Api::V1::Profile::HeadersController < Api::BaseController
|
|||
def destroy
|
||||
@account = current_account
|
||||
UpdateAccountService.new.call(@account, { header: nil }, raise_error: true)
|
||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
||||
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
|
||||
render json: @account, serializer: REST::CredentialAccountSerializer
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,13 +8,15 @@ class BackupsController < ApplicationController
|
|||
before_action :authenticate_user!
|
||||
before_action :set_backup
|
||||
|
||||
BACKUP_LINK_TIMEOUT = 1.hour.freeze
|
||||
|
||||
def download
|
||||
case Paperclip::Attachment.default_options[:storage]
|
||||
when :s3, :azure
|
||||
redirect_to @backup.dump.expiring_url(10), allow_other_host: true
|
||||
redirect_to @backup.dump.expiring_url(BACKUP_LINK_TIMEOUT.to_i), allow_other_host: true
|
||||
when :fog
|
||||
if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present?
|
||||
redirect_to @backup.dump.expiring_url(Time.now.utc + 10), allow_other_host: true
|
||||
redirect_to @backup.dump.expiring_url(BACKUP_LINK_TIMEOUT.from_now), allow_other_host: true
|
||||
else
|
||||
redirect_to full_asset_url(@backup.dump.url), allow_other_host: true
|
||||
end
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ module SignatureVerification
|
|||
|
||||
def verify_signature_strength!
|
||||
raise SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)')
|
||||
raise SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(Request::REQUEST_TARGET) || signed_headers.include?('digest')
|
||||
raise SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(HttpSignatureDraft::REQUEST_TARGET) || signed_headers.include?('digest')
|
||||
raise SignatureVerificationError, 'Mastodon requires the Host header to be signed when doing a GET request' if request.get? && !signed_headers.include?('host')
|
||||
raise SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if request.post? && !signed_headers.include?('digest')
|
||||
end
|
||||
|
|
@ -192,14 +192,14 @@ module SignatureVerification
|
|||
def build_signed_string(include_query_string: true)
|
||||
signed_headers.map do |signed_header|
|
||||
case signed_header
|
||||
when Request::REQUEST_TARGET
|
||||
when HttpSignatureDraft::REQUEST_TARGET
|
||||
if include_query_string
|
||||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.original_fullpath}"
|
||||
"#{HttpSignatureDraft::REQUEST_TARGET}: #{request.method.downcase} #{request.original_fullpath}"
|
||||
else
|
||||
# Current versions of Mastodon incorrectly omit the query string from the (request-target) pseudo-header.
|
||||
# Therefore, temporarily support such incorrect signatures for compatibility.
|
||||
# TODO: remove eventually some time after release of the fixed version
|
||||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
||||
"#{HttpSignatureDraft::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
||||
end
|
||||
when '(created)'
|
||||
raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ module Settings
|
|||
def destroy
|
||||
if valid_picture?
|
||||
if UpdateAccountService.new.call(@account, { @picture => nil, "#{@picture}_remote_url" => '' })
|
||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
||||
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
|
||||
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg'), status: 303
|
||||
else
|
||||
redirect_to settings_profile_path
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ class Settings::PrivacyController < Settings::BaseController
|
|||
def update
|
||||
if UpdateAccountService.new.call(@account, account_params.except(:settings))
|
||||
current_user.update!(settings_attributes: account_params[:settings])
|
||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
||||
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
|
||||
redirect_to settings_privacy_path, notice: I18n.t('generic.changes_saved_msg')
|
||||
else
|
||||
render :show
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class Settings::ProfilesController < Settings::BaseController
|
|||
|
||||
def update
|
||||
if UpdateAccountService.new.call(@account, account_params)
|
||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
||||
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
|
||||
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg')
|
||||
else
|
||||
@account.build_fields
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ const emojifyTextNode = (node, customEmojis) => {
|
|||
};
|
||||
|
||||
const emojifyNode = (node, customEmojis) => {
|
||||
for (const child of node.childNodes) {
|
||||
for (const child of Array.from(node.childNodes)) {
|
||||
switch(child.nodeType) {
|
||||
case Node.TEXT_NODE:
|
||||
emojifyTextNode(child, customEmojis);
|
||||
|
|
|
|||
|
|
@ -254,12 +254,26 @@ const expiresInFromExpiresAt = expires_at => {
|
|||
|
||||
const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => {
|
||||
prefix = prefix.toLowerCase();
|
||||
|
||||
if (suggestions.length < 4) {
|
||||
const localTags = tagHistory.filter(tag => tag.toLowerCase().startsWith(prefix) && !suggestions.some(suggestion => suggestion.type === 'hashtag' && suggestion.name.toLowerCase() === tag.toLowerCase()));
|
||||
return suggestions.concat(localTags.slice(0, 4 - suggestions.length).toJS().map(tag => ({ type: 'hashtag', name: tag })));
|
||||
} else {
|
||||
return suggestions;
|
||||
suggestions = suggestions.concat(localTags.slice(0, 4 - suggestions.length).toJS().map(tag => ({ type: 'hashtag', name: tag })));
|
||||
}
|
||||
|
||||
// Prefer capitalization from personal history, unless personal history is all lower-case
|
||||
const fixSuggestionCapitalization = (suggestion) => {
|
||||
if (suggestion.type !== 'hashtag')
|
||||
return suggestion;
|
||||
|
||||
const tagFromHistory = tagHistory.find((tag) => tag.localeCompare(suggestion.name, undefined, { sensitivity: 'accent' }) === 0);
|
||||
|
||||
if (!tagFromHistory || tagFromHistory.toLowerCase() === tagFromHistory)
|
||||
return suggestion;
|
||||
|
||||
return { ...suggestion, name: tagFromHistory };
|
||||
};
|
||||
|
||||
return suggestions.map(fixSuggestionCapitalization);
|
||||
};
|
||||
|
||||
const normalizeSuggestions = (state, { accounts, emojis, tags, token }) => {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AccountReachFinder
|
||||
RECENT_LIMIT = 2_000
|
||||
STATUS_LIMIT = 200
|
||||
STATUS_SINCE = 2.days
|
||||
|
||||
def initialize(account)
|
||||
@account = account
|
||||
end
|
||||
|
||||
def inboxes
|
||||
(followers_inboxes + reporters_inboxes + recently_mentioned_inboxes + relay_inboxes).uniq
|
||||
(followers_inboxes + reporters_inboxes + recently_mentioned_inboxes + recently_followed_inboxes + recently_requested_inboxes + relay_inboxes).uniq
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -20,13 +24,46 @@ class AccountReachFinder
|
|||
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(RECENT_LIMIT)
|
||||
end
|
||||
|
||||
Account.joins(:mentions).where(mentions: { status: recent_statuses }).inboxes.take(2000)
|
||||
def recently_followed_inboxes
|
||||
@account
|
||||
.following
|
||||
.where(follows: { created_at: recent_date_cutoff... })
|
||||
.inboxes
|
||||
.take(RECENT_LIMIT)
|
||||
end
|
||||
|
||||
def recently_requested_inboxes
|
||||
Account
|
||||
.where(id: @account.follow_requests.where({ created_at: recent_date_cutoff... }).select(:target_account_id))
|
||||
.inboxes
|
||||
.take(RECENT_LIMIT)
|
||||
end
|
||||
|
||||
def relay_inboxes
|
||||
Relay.enabled.pluck(:inbox_url)
|
||||
end
|
||||
|
||||
def oldest_status_id
|
||||
Mastodon::Snowflake
|
||||
.id_at(recent_date_cutoff, with_random: false)
|
||||
end
|
||||
|
||||
def recent_date_cutoff
|
||||
@account.suspended? && @account.suspension_origin_local? ? @account.suspended_at - STATUS_SINCE : STATUS_SINCE.ago
|
||||
end
|
||||
|
||||
def recent_statuses
|
||||
@account
|
||||
.statuses
|
||||
.recent
|
||||
.where(id: oldest_status_id...)
|
||||
.limit(STATUS_LIMIT)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
ApplicationRecord.transaction do
|
||||
@status = Status.create!(@params)
|
||||
attach_tags(@status)
|
||||
attach_mentions(@status)
|
||||
end
|
||||
|
||||
resolve_thread(@status)
|
||||
|
|
@ -188,6 +189,15 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
# not a big deal
|
||||
Trends.tags.register(status)
|
||||
|
||||
# Update featured tags
|
||||
return if @tags.empty? || !status.distributable?
|
||||
|
||||
@account.featured_tags.where(tag_id: @tags.pluck(:id)).find_each do |featured_tag|
|
||||
featured_tag.increment(status.created_at)
|
||||
end
|
||||
end
|
||||
|
||||
def attach_mentions(status)
|
||||
@mentions.each do |mention|
|
||||
mention.status = status
|
||||
mention.save
|
||||
|
|
|
|||
|
|
@ -32,24 +32,31 @@ class FeedManager
|
|||
"feed:#{type}:#{id}:#{subtype}"
|
||||
end
|
||||
|
||||
# The filter result of the status to a particular feed
|
||||
# @param [Symbol] timeline_type
|
||||
# @param [Status] status
|
||||
# @param [Account|List] receiver
|
||||
# @return [void|Symbol] nil, :filter, or :skip_home
|
||||
def filter(timeline_type, status, receiver)
|
||||
case timeline_type
|
||||
when :home
|
||||
filter_from_home(status, receiver.id, build_crutches(receiver.id, [status]), :home)
|
||||
when :list
|
||||
(filter_from_list?(status, receiver) ? :filter : nil) || filter_from_home(status, receiver.account_id, build_crutches(receiver.account_id, [status]), :list)
|
||||
when :mentions
|
||||
filter_from_mentions?(status, receiver.id) ? :filter : nil
|
||||
when :tags
|
||||
filter_from_tags?(status, receiver.id, build_crutches(receiver.id, [status])) ? :filter : nil
|
||||
end
|
||||
end
|
||||
|
||||
# Check if the status should not be added to a feed
|
||||
# @param [Symbol] timeline_type
|
||||
# @param [Status] status
|
||||
# @param [Account|List] receiver
|
||||
# @return [Boolean]
|
||||
def filter?(timeline_type, status, receiver)
|
||||
case timeline_type
|
||||
when :home
|
||||
filter_from_home?(status, receiver.id, build_crutches(receiver.id, [status]), :home)
|
||||
when :list
|
||||
filter_from_list?(status, receiver) || filter_from_home?(status, receiver.account_id, build_crutches(receiver.account_id, [status]), :list)
|
||||
when :mentions
|
||||
filter_from_mentions?(status, receiver.id)
|
||||
when :tags
|
||||
filter_from_tags?(status, receiver.id, build_crutches(receiver.id, [status]))
|
||||
else
|
||||
false
|
||||
end
|
||||
!!filter(timeline_type, status, receiver)
|
||||
end
|
||||
|
||||
# Add a status to a home feed and send a streaming API update
|
||||
|
|
@ -125,7 +132,7 @@ class FeedManager
|
|||
crutches = build_crutches(into_account.id, statuses)
|
||||
|
||||
statuses.each do |status|
|
||||
next if filter_from_home?(status, into_account.id, crutches)
|
||||
next if filter_from_home(status, into_account.id, crutches)
|
||||
|
||||
add_to_feed(:home, into_account.id, status, aggregate_reblogs: aggregate)
|
||||
end
|
||||
|
|
@ -153,7 +160,7 @@ class FeedManager
|
|||
crutches = build_crutches(list.account_id, statuses)
|
||||
|
||||
statuses.each do |status|
|
||||
next if filter_from_home?(status, list.account_id, crutches) || filter_from_list?(status, list)
|
||||
next if filter_from_home(status, list.account_id, crutches) || filter_from_list?(status, list)
|
||||
|
||||
add_to_feed(:list, list.id, status, aggregate_reblogs: aggregate)
|
||||
end
|
||||
|
|
@ -285,7 +292,7 @@ class FeedManager
|
|||
crutches = build_crutches(account.id, statuses)
|
||||
|
||||
statuses.each do |status|
|
||||
next if filter_from_home?(status, account.id, crutches)
|
||||
next if filter_from_home(status, account.id, crutches)
|
||||
|
||||
add_to_feed(:home, account.id, status, aggregate_reblogs: aggregate)
|
||||
end
|
||||
|
|
@ -378,12 +385,12 @@ class FeedManager
|
|||
# @param [Status] status
|
||||
# @param [Integer] receiver_id
|
||||
# @param [Hash] crutches
|
||||
# @return [Boolean]
|
||||
def filter_from_home?(status, receiver_id, crutches, timeline_type = :home)
|
||||
return false if receiver_id == status.account_id
|
||||
return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
|
||||
return true if timeline_type != :list && crutches[:exclusive_list_users][status.account_id].present?
|
||||
return true if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language)
|
||||
# @return [void|Symbol] nil, :skip_home, or :filter
|
||||
def filter_from_home(status, receiver_id, crutches, timeline_type = :home)
|
||||
return if receiver_id == status.account_id
|
||||
return :filter if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
|
||||
return :skip_home if timeline_type != :list && crutches[:exclusive_list_users][status.account_id].present?
|
||||
return :filter if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language)
|
||||
|
||||
check_for_blocks = crutches[:active_mentions][status.id] || []
|
||||
check_for_blocks.push(status.account_id)
|
||||
|
|
@ -393,24 +400,22 @@ class FeedManager
|
|||
check_for_blocks.concat(crutches[:active_mentions][status.reblog_of_id] || [])
|
||||
end
|
||||
|
||||
return true if check_for_blocks.any? { |target_account_id| crutches[:blocking][target_account_id] || crutches[:muting][target_account_id] }
|
||||
return true if crutches[:blocked_by][status.account_id]
|
||||
return :filter if check_for_blocks.any? { |target_account_id| crutches[:blocking][target_account_id] || crutches[:muting][target_account_id] }
|
||||
return :filter if crutches[:blocked_by][status.account_id]
|
||||
|
||||
if status.reply? && !status.in_reply_to_account_id.nil? # Filter out if it's a reply
|
||||
should_filter = !crutches[:following][status.in_reply_to_account_id] # and I'm not following the person it's a reply to
|
||||
should_filter &&= receiver_id != status.in_reply_to_account_id # and it's not a reply to me
|
||||
should_filter &&= status.account_id != status.in_reply_to_account_id # and it's not a self-reply
|
||||
|
||||
return !!should_filter
|
||||
elsif status.reblog? # Filter out a reblog
|
||||
should_filter = crutches[:hiding_reblogs][status.account_id] # if the reblogger's reblogs are suppressed
|
||||
should_filter ||= crutches[:blocked_by][status.reblog.account_id] # or if the author of the reblogged status is blocking me
|
||||
should_filter ||= crutches[:domain_blocking][status.reblog.account.domain] # or the author's domain is blocked
|
||||
|
||||
return !!should_filter
|
||||
else
|
||||
should_filter = false
|
||||
end
|
||||
|
||||
false
|
||||
should_filter ? :filter : nil
|
||||
end
|
||||
|
||||
# Check if status should not be added to the mentions feed
|
||||
|
|
|
|||
31
app/lib/http_signature_draft.rb
Normal file
31
app/lib/http_signature_draft.rb
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This implements an older draft of HTTP Signatures:
|
||||
# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures
|
||||
|
||||
class HttpSignatureDraft
|
||||
REQUEST_TARGET = '(request-target)'
|
||||
|
||||
def initialize(keypair, key_id, full_path: true)
|
||||
@keypair = keypair
|
||||
@key_id = key_id
|
||||
@full_path = full_path
|
||||
end
|
||||
|
||||
def request_target(verb, url)
|
||||
if url.query.nil? || !@full_path
|
||||
"#{verb} #{url.path}"
|
||||
else
|
||||
"#{verb} #{url.path}?#{url.query}"
|
||||
end
|
||||
end
|
||||
|
||||
def sign(signed_headers, verb, url)
|
||||
signed_headers = signed_headers.merge(REQUEST_TARGET => request_target(verb, url))
|
||||
signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n")
|
||||
algorithm = 'rsa-sha256'
|
||||
signature = Base64.strict_encode64(@keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string))
|
||||
|
||||
"keyId=\"#{@key_id}\",algorithm=\"#{algorithm}\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\""
|
||||
end
|
||||
end
|
||||
|
|
@ -61,8 +61,6 @@ class PerOperationWithDeadline < HTTP::Timeout::PerOperation
|
|||
end
|
||||
|
||||
class Request
|
||||
REQUEST_TARGET = '(request-target)'
|
||||
|
||||
# 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
|
||||
|
|
@ -78,11 +76,21 @@ class Request
|
|||
@http_client = options.delete(:http_client)
|
||||
@allow_local = options.delete(:allow_local)
|
||||
@full_path = !options.delete(:omit_query_string)
|
||||
@options = options.merge(socket_class: use_proxy? || @allow_local ? ProxySocket : Socket)
|
||||
@options = @options.merge(timeout_class: PerOperationWithDeadline, timeout_options: TIMEOUT)
|
||||
@options = {
|
||||
follow: {
|
||||
max_hops: 3,
|
||||
on_redirect: ->(response, request) { re_sign_on_redirect(response, request) },
|
||||
},
|
||||
}.merge(options).merge(
|
||||
socket_class: use_proxy? || @allow_local ? ProxySocket : Socket,
|
||||
timeout_class: PerOperationWithDeadline,
|
||||
timeout_options: TIMEOUT
|
||||
)
|
||||
@options = @options.merge(proxy_url) if use_proxy?
|
||||
@headers = {}
|
||||
|
||||
@signing = nil
|
||||
|
||||
raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if block_hidden_service?
|
||||
|
||||
set_common_headers!
|
||||
|
|
@ -92,8 +100,9 @@ class Request
|
|||
def on_behalf_of(actor, sign_with: nil)
|
||||
raise ArgumentError, 'actor must not be nil' if actor.nil?
|
||||
|
||||
@actor = actor
|
||||
@keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : @actor.keypair
|
||||
key_id = ActivityPub::TagManager.instance.key_uri_for(actor)
|
||||
keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : actor.keypair
|
||||
@signing = HttpSignatureDraft.new(keypair, key_id, full_path: @full_path)
|
||||
|
||||
self
|
||||
end
|
||||
|
|
@ -125,7 +134,7 @@ class Request
|
|||
end
|
||||
|
||||
def headers
|
||||
(@actor ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
|
||||
(@signing ? @headers.merge('Signature' => signature) : @headers)
|
||||
end
|
||||
|
||||
class << self
|
||||
|
|
@ -140,14 +149,13 @@ class Request
|
|||
end
|
||||
|
||||
def http_client
|
||||
HTTP.use(:auto_inflate).follow(max_hops: 3)
|
||||
HTTP.use(:auto_inflate)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_common_headers!
|
||||
@headers[REQUEST_TARGET] = request_target
|
||||
@headers['User-Agent'] = Mastodon::Version.user_agent
|
||||
@headers['Host'] = @url.host
|
||||
@headers['Date'] = Time.now.utc.httpdate
|
||||
|
|
@ -158,31 +166,28 @@ class Request
|
|||
@headers['Digest'] = "SHA-256=#{Digest::SHA256.base64digest(@options[:body])}"
|
||||
end
|
||||
|
||||
def request_target
|
||||
if @url.query.nil? || !@full_path
|
||||
"#{@verb} #{@url.path}"
|
||||
else
|
||||
"#{@verb} #{@url.path}?#{@url.query}"
|
||||
end
|
||||
end
|
||||
|
||||
def signature
|
||||
algorithm = 'rsa-sha256'
|
||||
signature = Base64.strict_encode64(@keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string))
|
||||
|
||||
"keyId=\"#{key_id}\",algorithm=\"#{algorithm}\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\""
|
||||
@signing.sign(@headers.without('User-Agent', 'Accept-Encoding'), @verb, @url)
|
||||
end
|
||||
|
||||
def signed_string
|
||||
signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n")
|
||||
end
|
||||
def re_sign_on_redirect(_response, request)
|
||||
# Delete existing signature if there is one, since it will be invalid
|
||||
request.headers.delete('Signature')
|
||||
|
||||
def signed_headers
|
||||
@headers.without('User-Agent', 'Accept-Encoding')
|
||||
end
|
||||
return unless @signing.present? && @verb == :get
|
||||
|
||||
def key_id
|
||||
ActivityPub::TagManager.instance.key_uri_for(@actor)
|
||||
signed_headers = request.headers.to_h.slice(*@headers.keys)
|
||||
unless @headers.keys.all? { |key| signed_headers.key?(key) }
|
||||
# We have lost some headers in the process, so don't sign the new
|
||||
# request, in order to avoid issuing a valid signature with fewer
|
||||
# conditions than expected.
|
||||
|
||||
Rails.logger.warn { "Some headers (#{@headers.keys - signed_headers.keys}) have been lost on redirect from {@uri} to #{request.uri}, this should not happen. Skipping signatures" }
|
||||
return
|
||||
end
|
||||
|
||||
signature_value = @signing.sign(signed_headers.without('User-Agent', 'Accept-Encoding'), @verb, Addressable::URI.parse(request.uri))
|
||||
request.headers['Signature'] = signature_value
|
||||
end
|
||||
|
||||
def http_client
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@ class Account < ApplicationRecord
|
|||
scope :by_domain_and_subdomains, ->(domain) { where(domain: Instance.by_domain_and_subdomains(domain).select(:domain)) }
|
||||
scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) }
|
||||
scope :not_domain_blocked_by_account, ->(account) { where(arel_table[:domain].eq(nil).or(arel_table[:domain].not_in(account.excluded_from_timeline_domains))) }
|
||||
scope :with_username, ->(value) { value.is_a?(Array) ? where(arel_table[:username].lower.in(value.map { |x| x.to_s.downcase })) : where(arel_table[:username].lower.eq(value.to_s.downcase)) }
|
||||
|
||||
after_update_commit :trigger_update_webhooks
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ class Poll < ApplicationRecord
|
|||
|
||||
validates :options, presence: true
|
||||
validates :expires_at, presence: true, if: :local?
|
||||
validates_with PollValidator, on: :create, if: :local?
|
||||
validates_with PollOptionsValidator, if: :local?
|
||||
validates_with PollExpirationValidator, if: -> { local? && expires_at_changed? }
|
||||
|
||||
scope :attached, -> { where.not(status_id: nil) }
|
||||
scope :unattached, -> { where(status_id: nil) }
|
||||
|
|
|
|||
|
|
@ -68,10 +68,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
|||
},
|
||||
|
||||
polls: {
|
||||
max_options: PollValidator::MAX_OPTIONS,
|
||||
max_characters_per_option: PollValidator::MAX_OPTION_CHARS,
|
||||
min_expiration: PollValidator::MIN_EXPIRATION,
|
||||
max_expiration: PollValidator::MAX_EXPIRATION,
|
||||
max_options: PollOptionsValidator::MAX_OPTIONS,
|
||||
max_characters_per_option: PollOptionsValidator::MAX_OPTION_CHARS,
|
||||
min_expiration: PollExpirationValidator::MIN_EXPIRATION,
|
||||
max_expiration: PollExpirationValidator::MAX_EXPIRATION,
|
||||
},
|
||||
|
||||
translation: {
|
||||
|
|
|
|||
|
|
@ -78,10 +78,10 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer
|
|||
},
|
||||
|
||||
polls: {
|
||||
max_options: PollValidator::MAX_OPTIONS,
|
||||
max_characters_per_option: PollValidator::MAX_OPTION_CHARS,
|
||||
min_expiration: PollValidator::MIN_EXPIRATION,
|
||||
max_expiration: PollValidator::MAX_EXPIRATION,
|
||||
max_options: PollOptionsValidator::MAX_OPTIONS,
|
||||
max_characters_per_option: PollOptionsValidator::MAX_OPTION_CHARS,
|
||||
min_expiration: PollExpirationValidator::MIN_EXPIRATION,
|
||||
max_expiration: PollExpirationValidator::MAX_EXPIRATION,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -186,7 +186,26 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
|||
end
|
||||
|
||||
def update_tags!
|
||||
@status.tags = Tag.find_or_create_by_names(@raw_tags)
|
||||
previous_tags = @status.tags.to_a
|
||||
current_tags = @status.tags = Tag.find_or_create_by_names(@raw_tags)
|
||||
|
||||
return unless @status.distributable?
|
||||
|
||||
added_tags = current_tags - previous_tags
|
||||
|
||||
unless added_tags.empty?
|
||||
@account.featured_tags.where(tag_id: added_tags.pluck(:id)).find_each do |featured_tag|
|
||||
featured_tag.increment(@status.created_at)
|
||||
end
|
||||
end
|
||||
|
||||
removed_tags = previous_tags - current_tags
|
||||
|
||||
return if removed_tags.empty?
|
||||
|
||||
@account.featured_tags.where(tag_id: removed_tags.pluck(:id)).find_each do |featured_tag|
|
||||
featured_tag.decrement(@status)
|
||||
end
|
||||
end
|
||||
|
||||
def update_mentions!
|
||||
|
|
|
|||
|
|
@ -4,32 +4,46 @@ class ActivityPub::SynchronizeFollowersService < BaseService
|
|||
include JsonLdHelper
|
||||
include Payloadable
|
||||
|
||||
MAX_COLLECTION_PAGES = 10
|
||||
|
||||
def call(account, partial_collection_url)
|
||||
@account = account
|
||||
@expected_followers_ids = []
|
||||
|
||||
items = collection_items(partial_collection_url)
|
||||
return if items.nil?
|
||||
|
||||
# There could be unresolved accounts (hence the call to .compact) but this
|
||||
# should never happen in practice, since in almost all cases we keep an
|
||||
# Account record, and should we not do that, we should have sent a Delete.
|
||||
# In any case there is not much we can do if that occurs.
|
||||
@expected_followers = items.filter_map { |uri| ActivityPub::TagManager.instance.uri_to_resource(uri, Account) }
|
||||
return unless process_collection!(partial_collection_url)
|
||||
|
||||
remove_unexpected_local_followers!
|
||||
handle_unexpected_outgoing_follows!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_page!(items)
|
||||
page_expected_followers = extract_local_followers(items)
|
||||
@expected_followers_ids.concat(page_expected_followers.pluck(:id))
|
||||
|
||||
handle_unexpected_outgoing_follows!(page_expected_followers)
|
||||
end
|
||||
|
||||
def extract_local_followers(items)
|
||||
# There could be unresolved accounts (hence the call to .filter_map) but this
|
||||
# should never happen in practice, since in almost all cases we keep an
|
||||
# Account record, and should we not do that, we should have sent a Delete.
|
||||
# In any case there is not much we can do if that occurs.
|
||||
|
||||
# TODO: this will need changes when switching to numeric IDs
|
||||
|
||||
usernames = items.filter_map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username)&.downcase }
|
||||
Account.local.with_username(usernames)
|
||||
end
|
||||
|
||||
def remove_unexpected_local_followers!
|
||||
@account.followers.local.where.not(id: @expected_followers.map(&:id)).each do |unexpected_follower|
|
||||
@account.followers.local.where.not(id: @expected_followers_ids).reorder(nil).find_each do |unexpected_follower|
|
||||
UnfollowService.new.call(unexpected_follower, @account)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_unexpected_outgoing_follows!
|
||||
@expected_followers.each do |expected_follower|
|
||||
def handle_unexpected_outgoing_follows!(expected_followers)
|
||||
expected_followers.each do |expected_follower|
|
||||
next if expected_follower.following?(@account)
|
||||
|
||||
if expected_follower.requested?(@account)
|
||||
|
|
@ -50,18 +64,33 @@ class ActivityPub::SynchronizeFollowersService < BaseService
|
|||
Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer))
|
||||
end
|
||||
|
||||
def collection_items(collection_or_uri)
|
||||
collection = fetch_collection(collection_or_uri)
|
||||
return unless collection.is_a?(Hash)
|
||||
# Only returns true if the whole collection has been processed
|
||||
def process_collection!(collection_uri, max_pages: MAX_COLLECTION_PAGES)
|
||||
collection = fetch_collection(collection_uri)
|
||||
return false unless collection.is_a?(Hash)
|
||||
|
||||
collection = fetch_collection(collection['first']) if collection['first'].present?
|
||||
return unless collection.is_a?(Hash)
|
||||
|
||||
while collection.is_a?(Hash)
|
||||
process_page!(as_array(collection_page_items(collection)))
|
||||
|
||||
max_pages -= 1
|
||||
|
||||
return true if collection['next'].blank? # We reached the end of the collection
|
||||
return false if max_pages <= 0 # We reached our pages limit
|
||||
|
||||
collection = fetch_collection(collection['next'])
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def collection_page_items(collection)
|
||||
case collection['type']
|
||||
when 'Collection', 'CollectionPage'
|
||||
as_array(collection['items'])
|
||||
collection['items']
|
||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
||||
as_array(collection['orderedItems'])
|
||||
collection['orderedItems']
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ class SuspendAccountService < BaseService
|
|||
end
|
||||
end
|
||||
|
||||
CacheBusterWorker.perform_async(attachment.path(style)) if Rails.configuration.x.cache_buster_enabled
|
||||
CacheBusterWorker.perform_async(attachment.url(style)) if Rails.configuration.x.cache_buster_enabled
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ class UnsuspendAccountService < BaseService
|
|||
end
|
||||
end
|
||||
|
||||
CacheBusterWorker.perform_async(attachment.path(style)) if Rails.configuration.x.cache_buster_enabled
|
||||
CacheBusterWorker.perform_async(attachment.url(style)) if Rails.configuration.x.cache_buster_enabled
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
13
app/validators/poll_expiration_validator.rb
Normal file
13
app/validators/poll_expiration_validator.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PollExpirationValidator < ActiveModel::Validator
|
||||
MAX_EXPIRATION = 1.month.freeze
|
||||
MIN_EXPIRATION = 5.minutes.freeze
|
||||
|
||||
def validate(poll)
|
||||
current_time = Time.now.utc
|
||||
|
||||
poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_long')) if poll.expires_at.nil? || poll.expires_at - current_time > MAX_EXPIRATION
|
||||
poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_short')) if poll.expires_at.present? && (poll.expires_at - current_time).ceil < MIN_EXPIRATION
|
||||
end
|
||||
end
|
||||
|
|
@ -3,17 +3,11 @@
|
|||
class PollValidator < ActiveModel::Validator
|
||||
MAX_OPTIONS = 12
|
||||
MAX_OPTION_CHARS = 50
|
||||
MAX_EXPIRATION = 1.month.freeze
|
||||
MIN_EXPIRATION = 5.minutes.freeze
|
||||
|
||||
def validate(poll)
|
||||
current_time = Time.now.utc
|
||||
|
||||
poll.errors.add(:options, I18n.t('polls.errors.too_few_options')) unless poll.options.size > 1
|
||||
poll.errors.add(:options, I18n.t('polls.errors.too_many_options', max: MAX_OPTIONS)) if poll.options.size > MAX_OPTIONS
|
||||
poll.errors.add(:options, I18n.t('polls.errors.over_character_limit', max: MAX_OPTION_CHARS)) if poll.options.any? { |option| option.mb_chars.grapheme_length > MAX_OPTION_CHARS }
|
||||
poll.errors.add(:options, I18n.t('polls.errors.duplicate_options')) unless poll.options.uniq.size == poll.options.size
|
||||
poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_long')) if poll.expires_at.nil? || poll.expires_at - current_time > MAX_EXPIRATION
|
||||
poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_short')) if poll.expires_at.present? && (poll.expires_at - current_time).ceil < MIN_EXPIRATION
|
||||
end
|
||||
end
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::UpdateDistributionWorker < ActivityPub::RawDistributionWorker
|
||||
DEBOUNCE_DELAY = 5.seconds
|
||||
|
||||
sidekiq_options queue: 'push', lock: :until_executed, lock_ttl: 1.day.to_i
|
||||
|
||||
# Distribute an profile update to servers that might have a copy
|
||||
|
|
|
|||
|
|
@ -29,27 +29,31 @@ class FeedInsertWorker
|
|||
private
|
||||
|
||||
def check_and_insert
|
||||
if feed_filtered?
|
||||
filter_result = feed_filter
|
||||
|
||||
if filter_result
|
||||
perform_unpush if update?
|
||||
else
|
||||
perform_push
|
||||
perform_notify if notify?
|
||||
end
|
||||
|
||||
perform_notify if notify?(filter_result)
|
||||
end
|
||||
|
||||
def feed_filtered?
|
||||
def feed_filter
|
||||
case @type
|
||||
when :home
|
||||
FeedManager.instance.filter?(:home, @status, @follower)
|
||||
FeedManager.instance.filter(:home, @status, @follower)
|
||||
when :tags
|
||||
FeedManager.instance.filter?(:tags, @status, @follower)
|
||||
FeedManager.instance.filter(:tags, @status, @follower)
|
||||
when :list
|
||||
FeedManager.instance.filter?(:list, @status, @list)
|
||||
FeedManager.instance.filter(:list, @status, @list)
|
||||
end
|
||||
end
|
||||
|
||||
def notify?
|
||||
return false if @type != :home || @status.reblog? || (@status.reply? && @status.in_reply_to_account_id != @status.account_id)
|
||||
def notify?(filter_result)
|
||||
return false if @type != :home || @status.reblog? || (@status.reply? && @status.in_reply_to_account_id != @status.account_id) ||
|
||||
filter_result == :filter
|
||||
|
||||
Follow.find_by(account: @follower, target_account: @status.account)&.notify?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ class Rack::Attack
|
|||
end
|
||||
|
||||
throttle('throttle_email_confirmations/ip', limit: 25, period: 5.minutes) do |req|
|
||||
req.throttleable_remote_ip if req.post? && (req.path_matches?('/auth/confirmation') || req.path == '/api/v1/emails/confirmations')
|
||||
req.throttleable_remote_ip if (req.post? && (req.path_matches?('/auth/confirmation') || req.path == '/api/v1/emails/confirmations')) || ((req.put? || req.patch?) && req.path_matches?('/auth/setup'))
|
||||
end
|
||||
|
||||
throttle('throttle_email_confirmations/email', limit: 5, period: 30.minutes) do |req|
|
||||
|
|
@ -133,6 +133,14 @@ class Rack::Attack
|
|||
end
|
||||
end
|
||||
|
||||
throttle('throttle_auth_setup/email', limit: 5, period: 10.minutes) do |req|
|
||||
req.params.dig('user', 'email').presence if (req.put? || req.patch?) && req.path_matches?('/auth/setup')
|
||||
end
|
||||
|
||||
throttle('throttle_auth_setup/account', limit: 5, period: 10.minutes) do |req|
|
||||
req.warden_user_id if (req.put? || req.patch?) && req.path_matches?('/auth/setup')
|
||||
end
|
||||
|
||||
throttle('throttle_login_attempts/ip', limit: 25, period: 5.minutes) do |req|
|
||||
req.throttleable_remote_ip if req.post? && req.path_matches?('/auth/sign_in')
|
||||
end
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ services:
|
|||
|
||||
web:
|
||||
build: .
|
||||
image: ghcr.io/mastodon/mastodon:v4.2.15
|
||||
image: ghcr.io/mastodon/mastodon:v4.2.20
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
|
||||
|
|
@ -77,7 +77,7 @@ services:
|
|||
|
||||
streaming:
|
||||
build: .
|
||||
image: ghcr.io/mastodon/mastodon:v4.2.15
|
||||
image: ghcr.io/mastodon/mastodon:v4.2.20
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: node ./streaming
|
||||
|
|
@ -95,7 +95,7 @@ services:
|
|||
|
||||
sidekiq:
|
||||
build: .
|
||||
image: ghcr.io/mastodon/mastodon:v4.2.15
|
||||
image: ghcr.io/mastodon/mastodon:v4.2.20
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: bundle exec sidekiq
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ module Mastodon
|
|||
end
|
||||
|
||||
def patch
|
||||
15
|
||||
20
|
||||
end
|
||||
|
||||
def default_prerelease
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ class Redis
|
|||
def exists?(...)
|
||||
call_with_namespace('exists?', ...)
|
||||
end
|
||||
|
||||
def with
|
||||
yield self
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -91,19 +91,17 @@ class Sanitize
|
|||
]
|
||||
)
|
||||
|
||||
MASTODON_OEMBED ||= freeze_config(
|
||||
elements: %w(audio embed iframe source video),
|
||||
MASTODON_OEMBED = freeze_config(
|
||||
elements: %w(audio iframe source video),
|
||||
|
||||
attributes: {
|
||||
'audio' => %w(controls),
|
||||
'embed' => %w(height src type width),
|
||||
'iframe' => %w(allowfullscreen frameborder height scrolling src width),
|
||||
'source' => %w(src type),
|
||||
'video' => %w(controls height loop width),
|
||||
},
|
||||
|
||||
protocols: {
|
||||
'embed' => { 'src' => HTTP_PROTOCOLS },
|
||||
'iframe' => { 'src' => HTTP_PROTOCOLS },
|
||||
'source' => { 'src' => HTTP_PROTOCOLS },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ describe Api::V1::Accounts::CredentialsController do
|
|||
|
||||
describe 'with valid data' do
|
||||
before do
|
||||
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async)
|
||||
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_in)
|
||||
|
||||
patch :update, params: {
|
||||
display_name: "Alice Isn't Dead",
|
||||
|
|
@ -58,7 +58,7 @@ describe Api::V1::Accounts::CredentialsController do
|
|||
end
|
||||
|
||||
it 'queues up an account update distribution' do
|
||||
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(user.account_id)
|
||||
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_in).with(anything, user.account_id)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
70
spec/controllers/settings/privacy_controller_spec.rb
Normal file
70
spec/controllers/settings/privacy_controller_spec.rb
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Settings::PrivacyController do
|
||||
render_views
|
||||
|
||||
let!(:user) { Fabricate(:user) }
|
||||
let(:account) { user.account }
|
||||
|
||||
before do
|
||||
sign_in user, scope: :user
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
before do
|
||||
get :show
|
||||
end
|
||||
|
||||
it 'returns http success with private cache control headers', :aggregate_failures do
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
expect(response.headers['Cache-Control']).to include('private, no-store')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT #update' do
|
||||
context 'when update succeeds' do
|
||||
before do
|
||||
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_in)
|
||||
end
|
||||
|
||||
it 'updates the user profile' do
|
||||
put :update, params: { account: { discoverable: '1', settings: { indexable: '1' } } }
|
||||
|
||||
expect(account.reload.discoverable)
|
||||
.to be(true)
|
||||
|
||||
expect(response)
|
||||
.to redirect_to(settings_privacy_path)
|
||||
|
||||
expect(ActivityPub::UpdateDistributionWorker)
|
||||
.to have_received(:perform_in).with(anything, account.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when update fails' do
|
||||
before do
|
||||
allow(UpdateAccountService).to receive(:new).and_return(failing_update_service)
|
||||
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_in)
|
||||
end
|
||||
|
||||
it 'updates the user profile' do
|
||||
put :update, params: { account: { discoverable: '1', settings: { indexable: '1' } } }
|
||||
|
||||
expect(response)
|
||||
.to render_template(:show)
|
||||
|
||||
expect(ActivityPub::UpdateDistributionWorker)
|
||||
.to_not have_received(:perform_in)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def failing_update_service
|
||||
instance_double(UpdateAccountService, call: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -32,23 +32,23 @@ RSpec.describe Settings::ProfilesController do
|
|||
end
|
||||
|
||||
it 'updates the user profile' do
|
||||
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async)
|
||||
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_in)
|
||||
put :update, params: { account: { display_name: 'New name' } }
|
||||
expect(account.reload.display_name).to eq 'New name'
|
||||
expect(response).to redirect_to(settings_profile_path)
|
||||
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(account.id)
|
||||
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_in).with(anything, account.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT #update with new profile image' do
|
||||
it 'updates profile image' do
|
||||
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async)
|
||||
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_in)
|
||||
expect(account.avatar.instance.avatar_file_name).to be_nil
|
||||
|
||||
put :update, params: { account: { avatar: fixture_file_upload('avatar.gif', 'image/gif') } }
|
||||
expect(response).to redirect_to(settings_profile_path)
|
||||
expect(account.reload.avatar.instance.avatar_file_name).to_not be_nil
|
||||
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(account.id)
|
||||
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_in).with(anything, account.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,13 +13,28 @@ RSpec.describe AccountReachFinder do
|
|||
let(:ap_mentioned_example_com) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-3', domain: 'example.com') }
|
||||
let(:ap_mentioned_example_org) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.org/inbox-4', domain: 'example.org') }
|
||||
|
||||
let(:ap_followed_example_com) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-5', domain: 'example.com') }
|
||||
let(:ap_followed_example_org) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-6', domain: 'example.org') }
|
||||
|
||||
let(:ap_requested_example_com) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-7', domain: 'example.com') }
|
||||
let(:ap_requested_example_org) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-8', domain: 'example.org') }
|
||||
|
||||
let(:unrelated_account) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/unrelated-inbox', domain: 'example.com') }
|
||||
let(:old_followed_account) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/old-followed-inbox', domain: 'example.com') }
|
||||
|
||||
before do
|
||||
travel_to(2.months.ago) { account.follow!(old_followed_account) }
|
||||
|
||||
ap_follower_example_com.follow!(account)
|
||||
ap_follower_example_org.follow!(account)
|
||||
ap_follower_with_shared.follow!(account)
|
||||
|
||||
account.follow!(ap_followed_example_com)
|
||||
account.follow!(ap_followed_example_org)
|
||||
|
||||
account.request_follow!(ap_requested_example_com)
|
||||
account.request_follow!(ap_requested_example_org)
|
||||
|
||||
Fabricate(:status, account: account).tap do |status|
|
||||
status.mentions << Mention.new(account: ap_follower_example_com)
|
||||
status.mentions << Mention.new(account: ap_mentioned_with_shared)
|
||||
|
|
@ -38,16 +53,36 @@ RSpec.describe AccountReachFinder do
|
|||
end
|
||||
|
||||
describe '#inboxes' do
|
||||
it 'includes the preferred inbox URL of followers' do
|
||||
expect(described_class.new(account).inboxes).to include(*[ap_follower_example_com, ap_follower_example_org, ap_follower_with_shared].map(&:preferred_inbox_url))
|
||||
subject { described_class.new(account).inboxes }
|
||||
|
||||
it 'includes the preferred inbox URL of followers and recently mentioned accounts but not unrelated users' do
|
||||
expect(subject)
|
||||
.to include(*follower_inbox_urls)
|
||||
.and include(*mentioned_account_inbox_urls)
|
||||
.and include(*recently_followed_inbox_urls)
|
||||
.and include(*recently_requested_inbox_urls)
|
||||
.and not_include(unrelated_account.preferred_inbox_url)
|
||||
.and not_include(old_followed_account.preferred_inbox_url)
|
||||
end
|
||||
|
||||
it 'includes the preferred inbox URL of recently-mentioned accounts' do
|
||||
expect(described_class.new(account).inboxes).to include(*[ap_mentioned_with_shared, ap_mentioned_example_com, ap_mentioned_example_org].map(&:preferred_inbox_url))
|
||||
def follower_inbox_urls
|
||||
[ap_follower_example_com, ap_follower_example_org, ap_follower_with_shared]
|
||||
.map(&:preferred_inbox_url)
|
||||
end
|
||||
|
||||
it 'does not include the inbox of unrelated users' do
|
||||
expect(described_class.new(account).inboxes).to_not include(unrelated_account.preferred_inbox_url)
|
||||
def mentioned_account_inbox_urls
|
||||
[ap_mentioned_with_shared, ap_mentioned_example_com, ap_mentioned_example_org]
|
||||
.map(&:preferred_inbox_url)
|
||||
end
|
||||
|
||||
def recently_followed_inbox_urls
|
||||
[ap_followed_example_com, ap_followed_example_org]
|
||||
.map(&:preferred_inbox_url)
|
||||
end
|
||||
|
||||
def recently_requested_inbox_urls
|
||||
[ap_requested_example_com, ap_requested_example_org]
|
||||
.map(&:preferred_inbox_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -130,10 +130,6 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
context 'when fetching' do
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
before do
|
||||
subject.perform
|
||||
end
|
||||
|
||||
context 'when object publication date is below ISO8601 range' do
|
||||
let(:object_json) do
|
||||
{
|
||||
|
|
@ -145,6 +141,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status with a valid creation date', :aggregate_failures do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -165,6 +163,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status with a valid creation date', :aggregate_failures do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -186,6 +186,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status with appropriate creation and edition dates', :aggregate_failures do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -209,17 +211,13 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
it 'creates status and does not mark it as edited' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.text).to eq 'Lorem ipsum'
|
||||
end
|
||||
|
||||
it 'does not mark status as edited' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.edited?).to be false
|
||||
end
|
||||
end
|
||||
|
|
@ -234,7 +232,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'does not create a status' do
|
||||
expect(sender.statuses.count).to be_zero
|
||||
expect { subject.perform }.to_not change(sender.statuses, :count)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -248,6 +246,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -255,6 +255,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'missing to/cc defaults to direct privacy' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -273,6 +275,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -291,6 +295,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -309,6 +315,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -327,6 +335,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -345,6 +355,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -363,6 +375,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -381,6 +395,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -403,6 +419,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -422,15 +440,13 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
it 'creates status with a silent mention' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.visibility).to eq 'limited'
|
||||
end
|
||||
|
||||
it 'creates silent mention' do
|
||||
status = sender.statuses.first
|
||||
expect(status.mentions.first).to be_silent
|
||||
end
|
||||
end
|
||||
|
|
@ -452,6 +468,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -472,6 +490,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -500,6 +520,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -522,6 +544,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
expect(status).to_not be_nil
|
||||
end
|
||||
|
|
@ -544,6 +568,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -569,6 +595,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -594,6 +622,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -619,6 +649,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -642,6 +674,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
expect(status).to_not be_nil
|
||||
end
|
||||
|
|
@ -664,6 +698,42 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.tags.map(&:name)).to include('test')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with featured hashtags' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
to: 'https://www.w3.org/ns/activitystreams#Public',
|
||||
tag: [
|
||||
{
|
||||
type: 'Hashtag',
|
||||
href: 'http://example.com/blah',
|
||||
name: '#test',
|
||||
},
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
sender.featured_tags.create!(name: 'test')
|
||||
end
|
||||
|
||||
it 'creates status and updates featured tag' do
|
||||
expect { subject.perform }
|
||||
.to change(sender.statuses, :count).by(1)
|
||||
.and change { sender.featured_tags.first.reload.statuses_count }.by(1)
|
||||
.and change { sender.featured_tags.first.reload.last_status_at }.from(nil).to(be_present)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -687,6 +757,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
expect(status).to_not be_nil
|
||||
end
|
||||
|
|
@ -709,6 +781,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
expect(status).to_not be_nil
|
||||
end
|
||||
|
|
@ -733,6 +807,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -759,6 +835,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
|
|
@ -784,6 +862,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
expect(status).to_not be_nil
|
||||
end
|
||||
|
|
@ -805,6 +885,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
expect(status).to_not be_nil
|
||||
end
|
||||
|
|
@ -835,13 +917,13 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
it 'creates status with a poll' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
expect(status).to_not be_nil
|
||||
expect(status.poll).to_not be_nil
|
||||
end
|
||||
|
||||
it 'creates a poll' do
|
||||
poll = sender.polls.first
|
||||
expect(poll).to_not be_nil
|
||||
expect(poll.status).to_not be_nil
|
||||
|
|
@ -864,6 +946,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'adds a vote to the poll with correct uri' do
|
||||
expect { subject.perform }.to change(poll.votes, :count).by(1)
|
||||
|
||||
vote = poll.votes.first
|
||||
expect(vote).to_not be_nil
|
||||
expect(vote.uri).to eq object_json[:id]
|
||||
|
|
@ -889,6 +973,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
it 'does not add a vote to the poll' do
|
||||
expect { subject.perform }.to_not change(poll.votes, :count)
|
||||
|
||||
expect(poll.votes.first).to be_nil
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,10 +13,13 @@ RSpec.describe ActivityPub::LinkedDataSignature do
|
|||
{
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => 'http://example.com/hello-world',
|
||||
'type' => 'Note',
|
||||
'content' => 'Hello world',
|
||||
}
|
||||
end
|
||||
|
||||
let(:json) { raw_json.merge('signature' => signature) }
|
||||
let(:signed_json) { raw_json.merge('signature' => signature) }
|
||||
let(:json) { signed_json }
|
||||
|
||||
before do
|
||||
stub_jsonld_contexts!
|
||||
|
|
@ -94,6 +97,54 @@ RSpec.describe ActivityPub::LinkedDataSignature do
|
|||
expect(subject.verify_actor!).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an attribute has been removed from the document' do
|
||||
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => sign(sender, raw_signature, raw_json)) }
|
||||
let(:json) { signed_json.without('content') }
|
||||
|
||||
let(:raw_signature) do
|
||||
{
|
||||
'creator' => 'http://example.com/alice',
|
||||
'created' => '2017-09-23T20:21:34Z',
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject.verify_actor!).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an attribute has been added to the document' do
|
||||
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => sign(sender, raw_signature, raw_json)) }
|
||||
let(:json) { signed_json.merge('attributedTo' => 'http://example.com/bob') }
|
||||
|
||||
let(:raw_signature) do
|
||||
{
|
||||
'creator' => 'http://example.com/alice',
|
||||
'created' => '2017-09-23T20:21:34Z',
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject.verify_actor!).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an existing attribute has been changed' do
|
||||
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => sign(sender, raw_signature, raw_json)) }
|
||||
let(:json) { signed_json.merge('content' => 'oops') }
|
||||
|
||||
let(:raw_signature) do
|
||||
{
|
||||
'creator' => 'http://example.com/alice',
|
||||
'created' => '2017-09-23T20:21:34Z',
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject.verify_actor!).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#sign!' do
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ RSpec.describe FeedManager do
|
|||
allow(List).to receive(:where).and_return(list)
|
||||
status = Fabricate(:status, text: 'I post a lot', account: bob)
|
||||
expect(described_class.instance.filter?(:home, status, alice)).to be true
|
||||
expect(described_class.instance.filter(:home, status, alice)).to be :skip_home
|
||||
end
|
||||
|
||||
it 'returns true for reblog from followee on exclusive list' do
|
||||
|
|
@ -172,6 +173,7 @@ RSpec.describe FeedManager do
|
|||
status = Fabricate(:status, text: 'I post a lot', account: bob)
|
||||
reblog = Fabricate(:status, reblog: status, account: jeff)
|
||||
expect(described_class.instance.filter?(:home, reblog, alice)).to be true
|
||||
expect(described_class.instance.filter(:home, reblog, alice)).to be :skip_home
|
||||
end
|
||||
|
||||
it 'returns false for post from followee on non-exclusive list' do
|
||||
|
|
|
|||
|
|
@ -33,12 +33,12 @@ describe RequestPool do
|
|||
|
||||
subject
|
||||
|
||||
threads = Array.new(20) do |_i|
|
||||
threads = Array.new(5) do
|
||||
Thread.new do
|
||||
20.times do
|
||||
subject.with('http://example.com') do |http_client|
|
||||
http_client.get('/').flush
|
||||
end
|
||||
subject.with('http://example.com') do |http_client|
|
||||
http_client.get('/').flush
|
||||
# Nudge scheduler to yield and exercise the full pool
|
||||
sleep(0.01)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -58,14 +58,13 @@ describe Request do
|
|||
expect(a_request(:get, 'http://example.com')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'sets headers' do
|
||||
expect { |block| subject.perform(&block) }.to yield_control
|
||||
expect(a_request(:get, 'http://example.com').with(headers: subject.headers)).to have_been_made
|
||||
end
|
||||
it 'makes a request with expected headers, yields, and closes the underlying connection' do
|
||||
allow(subject.send(:http_client)).to receive(:close)
|
||||
|
||||
it 'closes underlying connection' do
|
||||
expect_any_instance_of(HTTP::Client).to receive(:close)
|
||||
expect { |block| subject.perform(&block) }.to yield_control
|
||||
|
||||
expect(a_request(:get, 'http://example.com').with(headers: subject.headers)).to have_been_made
|
||||
expect(subject.send(:http_client)).to have_received(:close)
|
||||
end
|
||||
|
||||
it 'returns response which implements body_with_limit' do
|
||||
|
|
@ -75,6 +74,29 @@ describe Request do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with a redirect and HTTP signatures' do
|
||||
let(:account) { Fabricate(:account) }
|
||||
|
||||
before do
|
||||
stub_request(:get, 'http://example.com').to_return(status: 301, headers: { Location: 'http://redirected.example.com/foo' })
|
||||
stub_request(:get, 'http://redirected.example.com/foo').to_return(body: 'lorem ipsum')
|
||||
end
|
||||
|
||||
it 'makes a request with expected headers and follows redirects' do
|
||||
expect { |block| subject.on_behalf_of(account).perform(&block) }.to yield_control
|
||||
|
||||
# request.headers includes the `Signature` sent for the first request
|
||||
expect(a_request(:get, 'http://example.com').with(headers: subject.headers)).to have_been_made.once
|
||||
|
||||
# request.headers includes the `Signature`, but it has changed
|
||||
expect(a_request(:get, 'http://redirected.example.com/foo').with(headers: subject.headers.merge({ 'Host' => 'redirected.example.com' }))).to_not have_been_made
|
||||
|
||||
# `with(headers: )` matching tests for inclusion, so strip `Signature`
|
||||
# This doesn't actually test that there is a signature, but it tests that the original signature is not passed
|
||||
expect(a_request(:get, 'http://redirected.example.com/foo').with(headers: subject.headers.without('Signature').merge({ 'Host' => 'redirected.example.com' }))).to have_been_made.once
|
||||
end
|
||||
end
|
||||
|
||||
context 'with private host' do
|
||||
around do |example|
|
||||
WebMock.disable!
|
||||
|
|
|
|||
|
|
@ -39,7 +39,25 @@ RSpec.describe 'credentials API' do
|
|||
patch '/api/v1/accounts/update_credentials', headers: headers, params: params
|
||||
end
|
||||
|
||||
let(:params) { { discoverable: true, locked: false, indexable: true } }
|
||||
before do
|
||||
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_in)
|
||||
end
|
||||
|
||||
let(:params) do
|
||||
{
|
||||
avatar: fixture_file_upload('avatar.gif', 'image/gif'),
|
||||
discoverable: true,
|
||||
display_name: "Alice Isn't Dead",
|
||||
header: fixture_file_upload('attachment.jpg', 'image/jpeg'),
|
||||
indexable: true,
|
||||
locked: false,
|
||||
note: 'Hello!',
|
||||
source: {
|
||||
privacy: 'unlisted',
|
||||
sensitive: true,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
it_behaves_like 'forbidden for wrong scope', 'read read:accounts'
|
||||
|
||||
|
|
@ -59,6 +77,27 @@ RSpec.describe 'credentials API' do
|
|||
}),
|
||||
locked: false,
|
||||
})
|
||||
|
||||
expect(ActivityPub::UpdateDistributionWorker)
|
||||
.to have_received(:perform_in).with(anything, user.account_id)
|
||||
end
|
||||
|
||||
def expect_account_updates
|
||||
expect(user.account.reload)
|
||||
.to have_attributes(
|
||||
display_name: eq("Alice Isn't Dead"),
|
||||
note: 'Hello!',
|
||||
avatar: exist,
|
||||
header: exist
|
||||
)
|
||||
end
|
||||
|
||||
def expect_user_updates
|
||||
expect(user.reload)
|
||||
.to have_attributes(
|
||||
setting_default_privacy: eq('unlisted'),
|
||||
setting_default_sensitive: be(true)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
137
spec/requests/api/v1/instances/domain_blocks_spec.rb
Normal file
137
spec/requests/api/v1/instances/domain_blocks_spec.rb
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Domain Blocks' do
|
||||
describe 'GET /api/v1/instance/domain_blocks' do
|
||||
let(:user) { Fabricate(:user) }
|
||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id).token }
|
||||
|
||||
before { Fabricate(:domain_block) }
|
||||
|
||||
context 'with domain blocks set to all' do
|
||||
before { Setting.show_domain_blocks = 'all' }
|
||||
|
||||
it 'returns http success' do
|
||||
get api_v1_instance_domain_blocks_path
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
|
||||
expect(response.content_type)
|
||||
.to start_with('application/json')
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to be_present
|
||||
.and(be_an(Array))
|
||||
.and(have_attributes(size: 1))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with domain blocks set to users' do
|
||||
before { Setting.show_domain_blocks = 'users' }
|
||||
|
||||
context 'without authentication token' do
|
||||
it 'returns http not found' do
|
||||
get api_v1_instance_domain_blocks_path
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with authentication token' do
|
||||
context 'with unapproved user' do
|
||||
before { user.update(approved: false) }
|
||||
|
||||
it 'returns http not found' do
|
||||
get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token}" }
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unconfirmed user' do
|
||||
before { user.update(confirmed_at: nil) }
|
||||
|
||||
it 'returns http not found' do
|
||||
get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token}" }
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with disabled user' do
|
||||
before { user.update(disabled: true) }
|
||||
|
||||
it 'returns http not found' do
|
||||
get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token}" }
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with suspended user' do
|
||||
before { user.account.update(suspended_at: Time.zone.now) }
|
||||
|
||||
it 'returns http not found' do
|
||||
get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token}" }
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with moved user' do
|
||||
before { user.account.update(moved_to_account_id: Fabricate(:account).id) }
|
||||
|
||||
it 'returns http success' do
|
||||
get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token}" }
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
|
||||
expect(response.content_type)
|
||||
.to start_with('application/json')
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to be_present
|
||||
.and(be_an(Array))
|
||||
.and(have_attributes(size: 1))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with normal user' do
|
||||
it 'returns http success' do
|
||||
get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token}" }
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
|
||||
expect(response.content_type)
|
||||
.to start_with('application/json')
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to be_present
|
||||
.and(be_an(Array))
|
||||
.and(have_attributes(size: 1))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with domain blocks set to disabled' do
|
||||
before { Setting.show_domain_blocks = 'disabled' }
|
||||
|
||||
it 'returns http not found' do
|
||||
get api_v1_instance_domain_blocks_path
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -16,7 +16,7 @@ RSpec.describe 'Deleting profile images' do
|
|||
|
||||
describe 'DELETE /api/v1/profile' do
|
||||
before do
|
||||
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async)
|
||||
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_in)
|
||||
end
|
||||
|
||||
context 'when deleting an avatar' do
|
||||
|
|
@ -48,12 +48,7 @@ RSpec.describe 'Deleting profile images' do
|
|||
account.reload
|
||||
|
||||
expect(account.header).to exist
|
||||
end
|
||||
|
||||
it 'queues up an account update distribution' do
|
||||
delete '/api/v1/profile/avatar', headers: headers
|
||||
|
||||
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(account.id)
|
||||
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_in).with(anything, account.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -86,12 +81,7 @@ RSpec.describe 'Deleting profile images' do
|
|||
account.reload
|
||||
|
||||
expect(account.header).to_not exist
|
||||
end
|
||||
|
||||
it 'queues up an account update distribution' do
|
||||
delete '/api/v1/profile/header', headers: headers
|
||||
|
||||
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(account.id)
|
||||
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_in).with(anything, account.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -292,16 +292,22 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do
|
|||
updated: '2021-09-08T22:39:25Z',
|
||||
tag: [
|
||||
{ type: 'Hashtag', name: 'foo' },
|
||||
{ type: 'Hashtag', name: 'bar' },
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
subject.call(status, json, json)
|
||||
status.account.featured_tags.create!(name: 'bar')
|
||||
status.account.featured_tags.create!(name: 'test')
|
||||
end
|
||||
|
||||
it 'updates tags' do
|
||||
expect(status.tags.reload.map(&:name)).to eq %w(foo)
|
||||
it 'updates tags and featured tags' do
|
||||
expect { subject.call(status, json, json) }
|
||||
.to change { status.tags.reload.pluck(:name) }.from(%w(test foo)).to(%w(foo bar))
|
||||
.and change { status.account.featured_tags.find_by(name: 'test').statuses_count }.by(-1)
|
||||
.and change { status.account.featured_tags.find_by(name: 'bar').statuses_count }.by(1)
|
||||
.and change { status.account.featured_tags.find_by(name: 'bar').last_status_at }.from(nil).to(be_present)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService, type: :service do
|
|||
let(:bob) { Fabricate(:account, username: 'bob') }
|
||||
let(:eve) { Fabricate(:account, username: 'eve') }
|
||||
let(:mallory) { Fabricate(:account, username: 'mallory') }
|
||||
let(:collection_uri) { 'http://example.com/partial-followers' }
|
||||
let(:collection_uri) { 'https://example.com/partial-followers' }
|
||||
|
||||
let(:items) do
|
||||
[
|
||||
|
|
@ -29,31 +29,33 @@ RSpec.describe ActivityPub::SynchronizeFollowersService, type: :service do
|
|||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
around do |example|
|
||||
Sidekiq::Testing.fake! do
|
||||
example.run
|
||||
Sidekiq::Worker.clear_all
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
alice.follow!(actor)
|
||||
bob.follow!(actor)
|
||||
mallory.request_follow!(actor)
|
||||
end
|
||||
|
||||
shared_examples 'synchronizes followers' do
|
||||
before do
|
||||
alice.follow!(actor)
|
||||
bob.follow!(actor)
|
||||
mallory.request_follow!(actor)
|
||||
|
||||
allow(ActivityPub::DeliveryWorker).to receive(:perform_async)
|
||||
|
||||
subject.call(actor, collection_uri)
|
||||
end
|
||||
|
||||
it 'keeps expected followers' do
|
||||
expect(alice.following?(actor)).to be true
|
||||
end
|
||||
|
||||
it 'removes local followers not in the remote list' do
|
||||
expect(bob.following?(actor)).to be false
|
||||
end
|
||||
|
||||
it 'converts follow requests to follow relationships when they have been accepted' do
|
||||
expect(mallory.following?(actor)).to be true
|
||||
end
|
||||
|
||||
it 'sends an Undo Follow to the actor' do
|
||||
expect(ActivityPub::DeliveryWorker).to have_received(:perform_async).with(anything, eve.id, actor.inbox_url)
|
||||
it 'maintains following records and sends Undo Follow to actor' do
|
||||
expect(alice)
|
||||
.to be_following(actor) # Keep expected followers
|
||||
expect(bob)
|
||||
.to_not be_following(actor) # Remove local followers not in remote list
|
||||
expect(mallory)
|
||||
.to be_following(actor) # Convert follow request to follow when accepted
|
||||
expect(ActivityPub::DeliveryWorker)
|
||||
.to have_enqueued_sidekiq_job(anything, eve.id, actor.inbox_url) # Send Undo Follow to actor
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -83,7 +85,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService, type: :service do
|
|||
it_behaves_like 'synchronizes followers'
|
||||
end
|
||||
|
||||
context 'when the endpoint is a paginated Collection of actor URIs' do
|
||||
context 'when the endpoint is a single-page paginated Collection of actor URIs' do
|
||||
let(:payload) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
|
|
@ -103,5 +105,106 @@ RSpec.describe ActivityPub::SynchronizeFollowersService, type: :service do
|
|||
|
||||
it_behaves_like 'synchronizes followers'
|
||||
end
|
||||
|
||||
context 'when the endpoint is a paginated Collection of actor URIs split across multiple pages' do
|
||||
before do
|
||||
stub_request(:get, 'https://example.com/partial-followers')
|
||||
.to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
type: 'Collection',
|
||||
id: 'https://example.com/partial-followers',
|
||||
first: 'https://example.com/partial-followers/1',
|
||||
}))
|
||||
|
||||
stub_request(:get, 'https://example.com/partial-followers/1')
|
||||
.to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
type: 'CollectionPage',
|
||||
id: 'https://example.com/partial-followers/1',
|
||||
partOf: 'https://example.com/partial-followers',
|
||||
next: 'https://example.com/partial-followers/2',
|
||||
items: [alice, eve].map { |account| ActivityPub::TagManager.instance.uri_for(account) },
|
||||
}))
|
||||
|
||||
stub_request(:get, 'https://example.com/partial-followers/2')
|
||||
.to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
type: 'CollectionPage',
|
||||
id: 'https://example.com/partial-followers/2',
|
||||
partOf: 'https://example.com/partial-followers',
|
||||
items: ActivityPub::TagManager.instance.uri_for(mallory),
|
||||
}))
|
||||
end
|
||||
|
||||
it_behaves_like 'synchronizes followers'
|
||||
end
|
||||
|
||||
context 'when the endpoint is a paginated Collection of actor URIs split across, but one page errors out' do
|
||||
before do
|
||||
stub_request(:get, 'https://example.com/partial-followers')
|
||||
.to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
type: 'Collection',
|
||||
id: 'https://example.com/partial-followers',
|
||||
first: 'https://example.com/partial-followers/1',
|
||||
}))
|
||||
|
||||
stub_request(:get, 'https://example.com/partial-followers/1')
|
||||
.to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
type: 'CollectionPage',
|
||||
id: 'https://example.com/partial-followers/1',
|
||||
partOf: 'https://example.com/partial-followers',
|
||||
next: 'https://example.com/partial-followers/2',
|
||||
items: [mallory].map { |account| ActivityPub::TagManager.instance.uri_for(account) },
|
||||
}))
|
||||
|
||||
stub_request(:get, 'https://example.com/partial-followers/2')
|
||||
.to_return(status: 404)
|
||||
end
|
||||
|
||||
it 'confirms pending follow request but does not remove extra followers' do
|
||||
previous_follower_ids = actor.followers.pluck(:id)
|
||||
|
||||
subject.call(actor, collection_uri)
|
||||
|
||||
expect(previous_follower_ids - actor.followers.reload.pluck(:id))
|
||||
.to be_empty
|
||||
expect(mallory)
|
||||
.to be_following(actor)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the endpoint is a paginated Collection of actor URIs with more pages than we allow' do
|
||||
let(:payload) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
type: 'Collection',
|
||||
id: collection_uri,
|
||||
first: {
|
||||
type: 'CollectionPage',
|
||||
partOf: collection_uri,
|
||||
items: items,
|
||||
next: "#{collection_uri}/page2",
|
||||
},
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
before do
|
||||
stub_const('ActivityPub::SynchronizeFollowersService::MAX_COLLECTION_PAGES', 1)
|
||||
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||
end
|
||||
|
||||
it 'confirms pending follow request but does not remove extra followers' do
|
||||
previous_follower_ids = actor.followers.pluck(:id)
|
||||
|
||||
subject.call(actor, collection_uri)
|
||||
|
||||
expect(previous_follower_ids - actor.followers.reload.pluck(:id))
|
||||
.to be_empty
|
||||
expect(mallory)
|
||||
.to be_following(actor)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,6 +3,13 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SuspendAccountService, type: :service do
|
||||
around do |example|
|
||||
Sidekiq::Testing.fake! do
|
||||
example.run
|
||||
Sidekiq::Worker.clear_all
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'common behavior' do
|
||||
subject { described_class.new.call(account) }
|
||||
|
||||
|
|
@ -11,15 +18,20 @@ RSpec.describe SuspendAccountService, type: :service do
|
|||
|
||||
before do
|
||||
allow(FeedManager.instance).to receive_messages(unmerge_from_home: nil, unmerge_from_list: nil)
|
||||
allow(Rails.configuration.x).to receive(:cache_buster_enabled).and_return(true)
|
||||
|
||||
local_follower.follow!(account)
|
||||
list.accounts << account
|
||||
|
||||
account.suspend!
|
||||
|
||||
Fabricate(:media_attachment, file: attachment_fixture('boop.ogg'), account: account)
|
||||
end
|
||||
|
||||
it "unmerges from local followers' feeds" do
|
||||
subject
|
||||
it 'unmerges from feeds of local followers and changes file mode' do
|
||||
expect { subject }
|
||||
.to change { File.stat(account.media_attachments.first.file.path).mode }
|
||||
.and enqueue_sidekiq_job(CacheBusterWorker).with(account.media_attachments.first.file.url(:original))
|
||||
expect(FeedManager.instance).to have_received(:unmerge_from_home).with(account, local_follower)
|
||||
expect(FeedManager.instance).to have_received(:unmerge_from_list).with(account, list)
|
||||
end
|
||||
|
|
@ -30,17 +42,12 @@ RSpec.describe SuspendAccountService, type: :service do
|
|||
end
|
||||
|
||||
describe 'suspending a local account' do
|
||||
def match_update_actor_request(req, account)
|
||||
json = JSON.parse(req.body)
|
||||
def match_update_actor_request(json, account)
|
||||
json = JSON.parse(json)
|
||||
actor_id = ActivityPub::TagManager.instance.uri_for(account)
|
||||
json['type'] == 'Update' && json['actor'] == actor_id && json['object']['id'] == actor_id && json['object']['suspended']
|
||||
end
|
||||
|
||||
before do
|
||||
stub_request(:post, 'https://alice.com/inbox').to_return(status: 201)
|
||||
stub_request(:post, 'https://bob.com/inbox').to_return(status: 201)
|
||||
end
|
||||
|
||||
include_examples 'common behavior' do
|
||||
let!(:account) { Fabricate(:account) }
|
||||
let!(:remote_follower) { Fabricate(:account, uri: 'https://alice.com', inbox_url: 'https://alice.com/inbox', protocol: :activitypub, domain: 'alice.com') }
|
||||
|
|
@ -53,22 +60,21 @@ RSpec.describe SuspendAccountService, type: :service do
|
|||
|
||||
it 'sends an update actor to followers and reporters' do
|
||||
subject
|
||||
expect(a_request(:post, remote_follower.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once
|
||||
expect(a_request(:post, remote_reporter.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once
|
||||
|
||||
expect(ActivityPub::DeliveryWorker)
|
||||
.to have_enqueued_sidekiq_job(satisfying { |json| match_update_actor_request(json, account) }, account.id, remote_follower.inbox_url)
|
||||
expect(ActivityPub::DeliveryWorker)
|
||||
.to have_enqueued_sidekiq_job(satisfying { |json| match_update_actor_request(json, account) }, account.id, remote_reporter.inbox_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'suspending a remote account' do
|
||||
def match_reject_follow_request(req, account, followee)
|
||||
json = JSON.parse(req.body)
|
||||
def match_reject_follow_request(json, account, followee)
|
||||
json = JSON.parse(json)
|
||||
json['type'] == 'Reject' && json['actor'] == ActivityPub::TagManager.instance.uri_for(followee) && json['object']['actor'] == account.uri
|
||||
end
|
||||
|
||||
before do
|
||||
stub_request(:post, 'https://bob.com/inbox').to_return(status: 201)
|
||||
end
|
||||
|
||||
include_examples 'common behavior' do
|
||||
let!(:account) { Fabricate(:account, domain: 'bob.com', uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub) }
|
||||
let!(:local_followee) { Fabricate(:account) }
|
||||
|
|
@ -79,7 +85,9 @@ RSpec.describe SuspendAccountService, type: :service do
|
|||
|
||||
it 'sends a reject follow' do
|
||||
subject
|
||||
expect(a_request(:post, account.inbox_url).with { |req| match_reject_follow_request(req, account, local_followee) }).to have_been_made.once
|
||||
|
||||
expect(ActivityPub::DeliveryWorker)
|
||||
.to have_enqueued_sidekiq_job(satisfying { |json| match_reject_follow_request(json, account, local_followee) }, local_followee.id, account.inbox_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe PollValidator, type: :validator do
|
||||
RSpec.describe PollExpirationValidator, type: :validator do
|
||||
describe '#validate' do
|
||||
before do
|
||||
validator.validate(poll)
|
||||
|
|
@ -14,16 +14,24 @@ RSpec.describe PollValidator, type: :validator do
|
|||
let(:options) { %w(foo bar) }
|
||||
let(:expires_at) { 1.day.from_now }
|
||||
|
||||
it 'have no errors' do
|
||||
it 'has no errors' do
|
||||
expect(errors).to_not have_received(:add)
|
||||
end
|
||||
|
||||
context 'when expires is just 5 min ago' do
|
||||
context 'when the poll expires in 5 min from now' do
|
||||
let(:expires_at) { 5.minutes.from_now }
|
||||
|
||||
it 'not calls errors add' do
|
||||
it 'has no errors' do
|
||||
expect(errors).to_not have_received(:add)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the poll expires in the past' do
|
||||
let(:expires_at) { 5.minutes.ago }
|
||||
|
||||
it 'has errors' do
|
||||
expect(errors).to have_received(:add)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
45
spec/validators/poll_options_validator_spec.rb
Normal file
45
spec/validators/poll_options_validator_spec.rb
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe PollOptionsValidator do
|
||||
describe '#validate' do
|
||||
before do
|
||||
validator.validate(poll)
|
||||
end
|
||||
|
||||
let(:validator) { described_class.new }
|
||||
let(:poll) { instance_double(Poll, options: options, expires_at: expires_at, errors: errors) }
|
||||
let(:errors) { instance_double(ActiveModel::Errors, add: nil) }
|
||||
let(:options) { %w(foo bar) }
|
||||
let(:expires_at) { 1.day.from_now }
|
||||
|
||||
it 'has no errors' do
|
||||
expect(errors).to_not have_received(:add)
|
||||
end
|
||||
|
||||
context 'when the poll has duplicate options' do
|
||||
let(:options) { %w(foo foo) }
|
||||
|
||||
it 'adds errors' do
|
||||
expect(errors).to have_received(:add)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the poll has no options' do
|
||||
let(:options) { [] }
|
||||
|
||||
it 'adds errors' do
|
||||
expect(errors).to have_received(:add)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the poll has too many options' do
|
||||
let(:options) { Array.new(described_class::MAX_OPTIONS + 1) { |i| "option #{i}" } }
|
||||
|
||||
it 'adds errors' do
|
||||
expect(errors).to have_received(:add)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -8,6 +8,7 @@ describe FeedInsertWorker do
|
|||
describe 'perform' do
|
||||
let(:follower) { Fabricate(:account) }
|
||||
let(:status) { Fabricate(:status) }
|
||||
let(:list) { Fabricate(:list) }
|
||||
|
||||
context 'when there are no records' do
|
||||
it 'skips push with missing status' do
|
||||
|
|
@ -31,7 +32,7 @@ describe FeedInsertWorker do
|
|||
|
||||
context 'when there are real records' do
|
||||
it 'skips the push when there is a filter' do
|
||||
instance = instance_double(FeedManager, push_to_home: nil, filter?: true)
|
||||
instance = instance_double(FeedManager, push_to_home: nil, filter?: true, filter: :filter)
|
||||
allow(FeedManager).to receive(:instance).and_return(instance)
|
||||
result = subject.perform(status.id, follower.id)
|
||||
|
||||
|
|
@ -40,13 +41,31 @@ describe FeedInsertWorker do
|
|||
end
|
||||
|
||||
it 'pushes the status onto the home timeline without filter' do
|
||||
instance = instance_double(FeedManager, push_to_home: nil, filter?: false)
|
||||
instance = instance_double(FeedManager, push_to_home: nil, filter?: false, filter: nil)
|
||||
allow(FeedManager).to receive(:instance).and_return(instance)
|
||||
result = subject.perform(status.id, follower.id)
|
||||
|
||||
expect(result).to be_nil
|
||||
expect(instance).to have_received(:push_to_home).with(follower, status, update: nil)
|
||||
end
|
||||
|
||||
it 'pushes the status onto the tags timeline without filter' do
|
||||
instance = instance_double(FeedManager, push_to_home: nil, filter?: false, filter: nil)
|
||||
allow(FeedManager).to receive(:instance).and_return(instance)
|
||||
result = subject.perform(status.id, follower.id, :tags)
|
||||
|
||||
expect(result).to be_nil
|
||||
expect(instance).to have_received(:push_to_home).with(follower, status, update: nil)
|
||||
end
|
||||
|
||||
it 'pushes the status onto the list timeline without filter' do
|
||||
instance = instance_double(FeedManager, push_to_list: nil, filter?: false, filter: nil)
|
||||
allow(FeedManager).to receive(:instance).and_return(instance)
|
||||
result = subject.perform(status.id, list.id, :list)
|
||||
|
||||
expect(result).to be_nil
|
||||
expect(instance).to have_received(:push_to_list).with(list, status, update: nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -789,7 +789,7 @@ const startServer = async () => {
|
|||
// filtering of statuses:
|
||||
|
||||
// Filter based on language:
|
||||
if (Array.isArray(req.chosenLanguages) && payload.language !== null && req.chosenLanguages.indexOf(payload.language) === -1) {
|
||||
if (Array.isArray(req.chosenLanguages) && req.chosenLanguages.indexOf(payload.language) === -1) {
|
||||
log.silly(req.requestId, `Message ${payload.id} filtered by language (${payload.language})`);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue