Compare commits
23 commits
9c90c4efbd
...
9fba335937
Author | SHA1 | Date | |
---|---|---|---|
9fba335937 | |||
|
5d8c09194b | ||
|
15d7698462 | ||
|
1f9feb7c4c | ||
|
015858aef7 | ||
|
1a27e4e4cf | ||
|
7accf9aa12 | ||
|
bea340816d | ||
|
34936ca889 | ||
|
dec5d55670 | ||
|
629c30fdca | ||
|
3451993172 | ||
|
94155b48c4 | ||
|
5ddbf42dae | ||
|
b9f10c70b3 | ||
|
5a44db38ac | ||
|
b661192a12 | ||
|
d6f89e1476 | ||
|
53c3a56ac5 | ||
|
5768cce8ff | ||
|
a251eb57d3 | ||
|
e8bfe2515b | ||
|
4aab39f7c9 |
36 changed files with 777 additions and 207 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
|
||||
|
|
19
.github/workflows/test-ruby.yml
vendored
19
.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,7 +118,6 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
ruby-version:
|
||||
- '3.0'
|
||||
- '3.1'
|
||||
- '.ruby-version'
|
||||
ci_job:
|
||||
|
@ -129,7 +128,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 +196,13 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
ruby-version:
|
||||
- '3.0'
|
||||
- '3.1'
|
||||
- '.ruby-version'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: './public'
|
||||
name: ${{ github.sha }}
|
||||
|
@ -238,14 +236,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 +308,13 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
ruby-version:
|
||||
- '3.0'
|
||||
- '3.1'
|
||||
- '.ruby-version'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: './public'
|
||||
name: ${{ github.sha }}
|
||||
|
@ -351,14 +348,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
|
||||
|
|
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -2,6 +2,33 @@
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [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
|
||||
|
|
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'
|
||||
|
|
10
Gemfile.lock
10
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)
|
||||
|
@ -533,7 +533,7 @@ GEM
|
|||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.10)
|
||||
rack (2.2.11)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-cors (2.0.2)
|
||||
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -72,10 +72,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!
|
||||
|
|
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
|
|
@ -1,19 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PollValidator < ActiveModel::Validator
|
||||
class PollOptionsValidator < ActiveModel::Validator
|
||||
MAX_OPTIONS = 25
|
||||
MAX_OPTION_CHARS = 150
|
||||
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
|
|
@ -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.17
|
||||
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.17
|
||||
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.17
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: bundle exec sidekiq
|
||||
|
|
|
@ -13,7 +13,7 @@ module Mastodon
|
|||
end
|
||||
|
||||
def patch
|
||||
15
|
||||
17
|
||||
end
|
||||
|
||||
def default_prerelease
|
||||
|
|
|
@ -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 },
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!
|
||||
|
|
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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue