commit
c92b5559a9
153
.github/workflows/test-ruby.yml
vendored
Normal file
153
.github/workflows/test-ruby.yml
vendored
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
name: Ruby Testing
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- 'dependabot/**'
|
||||||
|
- 'renovate/**'
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
env:
|
||||||
|
BUNDLE_CLEAN: true
|
||||||
|
BUNDLE_FROZEN: true
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
mode:
|
||||||
|
- production
|
||||||
|
- test
|
||||||
|
env:
|
||||||
|
RAILS_ENV: ${{ matrix.mode }}
|
||||||
|
BUNDLE_WITH: ${{ matrix.mode }}
|
||||||
|
OTP_SECRET: precompile_placeholder
|
||||||
|
SECRET_KEY_BASE: precompile_placeholder
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: yarn
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
|
- name: Install native Ruby dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libicu-dev libidn11-dev
|
||||||
|
|
||||||
|
- name: Set up bundler cache
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: .ruby-version
|
||||||
|
bundler-cache: true
|
||||||
|
|
||||||
|
- run: yarn --frozen-lockfile --production
|
||||||
|
- name: Precompile assets
|
||||||
|
# Previously had set this, but it's not supported
|
||||||
|
# export NODE_OPTIONS=--openssl-legacy-provider
|
||||||
|
run: |-
|
||||||
|
./bin/rails assets:precompile
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: matrix.mode == 'test'
|
||||||
|
with:
|
||||||
|
path: |-
|
||||||
|
./public/assets
|
||||||
|
./public/packs-test
|
||||||
|
name: ${{ github.sha }}
|
||||||
|
retention-days: 0
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:14-alpine
|
||||||
|
env:
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
options: >-
|
||||||
|
--health-cmd "redis-cli ping"
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
ports:
|
||||||
|
- 6379:6379
|
||||||
|
|
||||||
|
env:
|
||||||
|
DB_HOST: localhost
|
||||||
|
DB_USER: postgres
|
||||||
|
DB_PASS: postgres
|
||||||
|
DISABLE_SIMPLECOV: true
|
||||||
|
RAILS_ENV: test
|
||||||
|
ALLOW_NOPAM: true
|
||||||
|
PAM_ENABLED: true
|
||||||
|
PAM_DEFAULT_SERVICE: pam_test
|
||||||
|
PAM_CONTROLLED_SERVICE: pam_test_controlled
|
||||||
|
OIDC_ENABLED: true
|
||||||
|
OIDC_SCOPE: read
|
||||||
|
SAML_ENABLED: true
|
||||||
|
CAS_ENABLED: true
|
||||||
|
BUNDLE_WITH: 'pam_authentication test'
|
||||||
|
CI_JOBS: ${{ matrix.ci_job }}/4
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
ruby-version:
|
||||||
|
- '.ruby-version'
|
||||||
|
ci_job:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
- 3
|
||||||
|
- 4
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
path: './public'
|
||||||
|
name: ${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Update package index
|
||||||
|
run: sudo apt-get update
|
||||||
|
|
||||||
|
- name: Install native Ruby dependencies
|
||||||
|
run: sudo apt-get install -y libicu-dev libidn11-dev
|
||||||
|
|
||||||
|
- name: Install additional system dependencies
|
||||||
|
run: sudo apt-get install -y ffmpeg imagemagick libpam-dev
|
||||||
|
|
||||||
|
- name: Set up bundler cache
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: ${{ matrix.ruby-version}}
|
||||||
|
bundler-cache: true
|
||||||
|
|
||||||
|
- name: Load database schema
|
||||||
|
run: './bin/rails db:create db:schema:load db:seed'
|
||||||
|
|
||||||
|
- run: bin/rspec
|
75
CHANGELOG.md
75
CHANGELOG.md
@ -3,6 +3,81 @@ Changelog
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [4.1.18] - 2024-07-04
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix incorrect permission checking on multiple API endpoints ([GHSA-58x8-3qxw-6hm7](https://github.com/mastodon/mastodon/security/advisories/GHSA-58x8-3qxw-6hm7))
|
||||||
|
- Fix incorrect authorship checking when processing some activities (CVE-2024-37903, [GHSA-xjvf-fm67-4qc3](https://github.com/mastodon/mastodon/security/advisories/GHSA-xjvf-fm67-4qc3))
|
||||||
|
- Fix ongoing streaming sessions not being invalidated when application tokens get revoked ([GHSA-vp5r-5pgw-jwqx](https://github.com/mastodon/mastodon/security/advisories/GHSA-vp5r-5pgw-jwqx))
|
||||||
|
- Update dependencies
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change preview cards generation to skip unusually long URLs ([oneiros](https://github.com/mastodon/mastodon/pull/30854))
|
||||||
|
- Change search modifiers to be case-insensitive ([Gargron](https://github.com/mastodon/mastodon/pull/30865))
|
||||||
|
- Change `STATSD_ADDR` handling to emit a warning rather than crashing if the address is unreachable ([timothyjrogers](https://github.com/mastodon/mastodon/pull/30691))
|
||||||
|
- Change PWA start URL from `/home` to `/` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27377))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix scheduled statuses scheduled in less than 5 minutes being immediately published ([danielmbrasil](https://github.com/mastodon/mastodon/pull/30584))
|
||||||
|
- Fix encoding detection for link cards ([oneiros](https://github.com/mastodon/mastodon/pull/30780))
|
||||||
|
- Fix `/admin/accounts/:account_id/statuses/:id` for edited posts with media attachments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30819))
|
||||||
|
|
||||||
|
## [4.1.17] - 2024-05-30
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Update dependencies
|
||||||
|
- Fix private mention filtering ([GHSA-5fq7-3p3j-9vrf](https://github.com/mastodon/mastodon/security/advisories/GHSA-5fq7-3p3j-9vrf))
|
||||||
|
- Fix password change endpoint not being rate-limited ([GHSA-q3rg-xx5v-4mxh](https://github.com/mastodon/mastodon/security/advisories/GHSA-q3rg-xx5v-4mxh))
|
||||||
|
- Add hardening around rate-limit bypass ([GHSA-c2r5-cfqr-c553](https://github.com/mastodon/mastodon/security/advisories/GHSA-c2r5-cfqr-c553))
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add fallback redirection when getting a webfinger query `WEB_DOMAIN@WEB_DOMAIN` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28592))
|
||||||
|
- Add `digest` attribute to `Admin::DomainBlock` entity in REST API ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/29092))
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Remove superfluous application-level caching in some controllers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29862))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix leaking Elasticsearch connections in Sidekiq processes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30450))
|
||||||
|
- Fix language of remote posts not being recognized when using unusual casing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30403))
|
||||||
|
- Fix off-by-one in `tootctl media` commands ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30306))
|
||||||
|
- Fix removal of allowed domains (in `LIMITED_FEDERATION_MODE`) not being recorded in the audit log ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/30125))
|
||||||
|
- Fix not being able to block a subdomain of an already-blocked domain through the API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30119))
|
||||||
|
- Fix `Idempotency-Key` being ignored when scheduling a post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30084))
|
||||||
|
- Fix crash when supplying the `FFMPEG_BINARY` environment variable ([timothyjrogers](https://github.com/mastodon/mastodon/pull/30022))
|
||||||
|
- Fix improper email address validation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29838))
|
||||||
|
- Fix results/query in `api/v1/featured_tags/suggestions` ([mjankowski](https://github.com/mastodon/mastodon/pull/29597))
|
||||||
|
- Fix unblocking internationalized domain names under certain conditions ([tribela](https://github.com/mastodon/mastodon/pull/29530))
|
||||||
|
- Fix admin account created by `mastodon:setup` not being auto-approved ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29379))
|
||||||
|
- Fix reference to non-existent var in CLI maintenance command ([mjankowski](https://github.com/mastodon/mastodon/pull/28363))
|
||||||
|
|
||||||
|
## [4.1.16] - 2024-02-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add hourly task to automatically require approval for new registrations in the absence of moderators ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29318), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/29355))
|
||||||
|
In order to prevent future abandoned Mastodon servers from being used for spam, harassment and other malicious activity, Mastodon will now automatically switch new user registrations to require moderator approval whenever they are left open and no activity (including non-moderation actions from apps) from any logged-in user with permission to access moderation reports has been detected in a full week.
|
||||||
|
When this happens, users with the permission to change server settings will receive an email notification.
|
||||||
|
This feature is disabled when `EMAIL_DOMAIN_ALLOWLIST` is used, and can also be disabled with `DISABLE_AUTOMATIC_SWITCHING_TO_APPROVED_REGISTRATIONS=true`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change registrations to be closed by default on new installations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29280))
|
||||||
|
If you are running a server and never changed your registrations mode from the default, updating will automatically close your registrations.
|
||||||
|
Simply re-enable them through the administration interface or using `tootctl settings registrations open` if you want to enable them again.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix processing of remote ActivityPub actors making use of `Link` objects as `Image` `url` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29335))
|
||||||
|
- Fix link verifications when page size exceeds 1MB ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29358))
|
||||||
|
|
||||||
## [4.1.15] - 2024-02-16
|
## [4.1.15] - 2024-02-16
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
1
Gemfile
1
Gemfile
@ -158,3 +158,4 @@ gem 'concurrent-ruby', require: false
|
|||||||
gem 'connection_pool', require: false
|
gem 'connection_pool', require: false
|
||||||
gem 'xorcist', '~> 1.1'
|
gem 'xorcist', '~> 1.1'
|
||||||
gem 'cocoon', '~> 1.2'
|
gem 'cocoon', '~> 1.2'
|
||||||
|
gem 'mail', '~> 2.8'
|
||||||
|
137
Gemfile.lock
137
Gemfile.lock
@ -10,40 +10,40 @@ GIT
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (6.1.7.6)
|
actioncable (6.1.7.8)
|
||||||
actionpack (= 6.1.7.6)
|
actionpack (= 6.1.7.8)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 6.1.7.8)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (6.1.7.6)
|
actionmailbox (6.1.7.8)
|
||||||
actionpack (= 6.1.7.6)
|
actionpack (= 6.1.7.8)
|
||||||
activejob (= 6.1.7.6)
|
activejob (= 6.1.7.8)
|
||||||
activerecord (= 6.1.7.6)
|
activerecord (= 6.1.7.8)
|
||||||
activestorage (= 6.1.7.6)
|
activestorage (= 6.1.7.8)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 6.1.7.8)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
actionmailer (6.1.7.6)
|
actionmailer (6.1.7.8)
|
||||||
actionpack (= 6.1.7.6)
|
actionpack (= 6.1.7.8)
|
||||||
actionview (= 6.1.7.6)
|
actionview (= 6.1.7.8)
|
||||||
activejob (= 6.1.7.6)
|
activejob (= 6.1.7.8)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 6.1.7.8)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (6.1.7.6)
|
actionpack (6.1.7.8)
|
||||||
actionview (= 6.1.7.6)
|
actionview (= 6.1.7.8)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 6.1.7.8)
|
||||||
rack (~> 2.0, >= 2.0.9)
|
rack (~> 2.0, >= 2.0.9)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actiontext (6.1.7.6)
|
actiontext (6.1.7.8)
|
||||||
actionpack (= 6.1.7.6)
|
actionpack (= 6.1.7.8)
|
||||||
activerecord (= 6.1.7.6)
|
activerecord (= 6.1.7.8)
|
||||||
activestorage (= 6.1.7.6)
|
activestorage (= 6.1.7.8)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 6.1.7.8)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.1.7.6)
|
actionview (6.1.7.8)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 6.1.7.8)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
@ -54,22 +54,22 @@ GEM
|
|||||||
case_transform (>= 0.2)
|
case_transform (>= 0.2)
|
||||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||||
active_record_query_trace (1.8)
|
active_record_query_trace (1.8)
|
||||||
activejob (6.1.7.6)
|
activejob (6.1.7.8)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 6.1.7.8)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.1.7.6)
|
activemodel (6.1.7.8)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 6.1.7.8)
|
||||||
activerecord (6.1.7.6)
|
activerecord (6.1.7.8)
|
||||||
activemodel (= 6.1.7.6)
|
activemodel (= 6.1.7.8)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 6.1.7.8)
|
||||||
activestorage (6.1.7.6)
|
activestorage (6.1.7.8)
|
||||||
actionpack (= 6.1.7.6)
|
actionpack (= 6.1.7.8)
|
||||||
activejob (= 6.1.7.6)
|
activejob (= 6.1.7.8)
|
||||||
activerecord (= 6.1.7.6)
|
activerecord (= 6.1.7.8)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 6.1.7.8)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
mini_mime (>= 1.1.0)
|
mini_mime (>= 1.1.0)
|
||||||
activesupport (6.1.7.6)
|
activesupport (6.1.7.8)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
@ -105,6 +105,7 @@ GEM
|
|||||||
aws-sigv4 (~> 1.4)
|
aws-sigv4 (~> 1.4)
|
||||||
aws-sigv4 (1.5.2)
|
aws-sigv4 (1.5.2)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
|
base64 (0.2.0)
|
||||||
bcrypt (3.1.17)
|
bcrypt (3.1.17)
|
||||||
better_errors (2.9.1)
|
better_errors (2.9.1)
|
||||||
coderay (>= 1.0.0)
|
coderay (>= 1.0.0)
|
||||||
@ -173,7 +174,7 @@ GEM
|
|||||||
cocoon (1.2.15)
|
cocoon (1.2.15)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
color_diff (0.1)
|
color_diff (0.1)
|
||||||
concurrent-ruby (1.2.2)
|
concurrent-ruby (1.2.3)
|
||||||
connection_pool (2.3.0)
|
connection_pool (2.3.0)
|
||||||
cose (1.2.1)
|
cose (1.2.1)
|
||||||
cbor (~> 0.5.9)
|
cbor (~> 0.5.9)
|
||||||
@ -183,7 +184,7 @@ GEM
|
|||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
css_parser (1.12.0)
|
css_parser (1.12.0)
|
||||||
addressable
|
addressable
|
||||||
date (3.3.3)
|
date (3.3.4)
|
||||||
debug_inspector (1.0.0)
|
debug_inspector (1.0.0)
|
||||||
devise (4.8.1)
|
devise (4.8.1)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
@ -330,7 +331,7 @@ GEM
|
|||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.6.3)
|
json (2.6.3)
|
||||||
json-canonicalization (0.3.0)
|
json-canonicalization (0.3.0)
|
||||||
json-jwt (1.15.3)
|
json-jwt (1.15.3.1)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
aes_key_wrap
|
aes_key_wrap
|
||||||
bindata
|
bindata
|
||||||
@ -395,7 +396,7 @@ GEM
|
|||||||
net-smtp
|
net-smtp
|
||||||
makara (0.5.1)
|
makara (0.5.1)
|
||||||
activerecord (>= 5.2.0)
|
activerecord (>= 5.2.0)
|
||||||
marcel (1.0.2)
|
marcel (1.0.4)
|
||||||
mario-redis-lock (1.2.1)
|
mario-redis-lock (1.2.1)
|
||||||
redis (>= 3.0.5)
|
redis (>= 3.0.5)
|
||||||
matrix (0.4.2)
|
matrix (0.4.2)
|
||||||
@ -405,7 +406,7 @@ GEM
|
|||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2022.0105)
|
mime-types-data (3.2022.0105)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.5)
|
mini_portile2 (2.8.7)
|
||||||
minitest (5.17.0)
|
minitest (5.17.0)
|
||||||
msgpack (1.6.0)
|
msgpack (1.6.0)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
@ -416,15 +417,15 @@ GEM
|
|||||||
net-ldap (0.17.1)
|
net-ldap (0.17.1)
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
net-protocol
|
net-protocol
|
||||||
net-protocol (0.2.1)
|
net-protocol (0.2.2)
|
||||||
timeout
|
timeout
|
||||||
net-scp (4.0.0.rc1)
|
net-scp (4.0.0.rc1)
|
||||||
net-ssh (>= 2.6.5, < 8.0.0)
|
net-ssh (>= 2.6.5, < 8.0.0)
|
||||||
net-smtp (0.3.3)
|
net-smtp (0.3.4)
|
||||||
net-protocol
|
net-protocol
|
||||||
net-ssh (7.0.1)
|
net-ssh (7.0.1)
|
||||||
nio4r (2.5.9)
|
nio4r (2.5.9)
|
||||||
nokogiri (1.16.2)
|
nokogiri (1.16.6)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nsa (0.2.8)
|
nsa (0.2.8)
|
||||||
@ -491,13 +492,13 @@ GEM
|
|||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (5.0.1)
|
public_suffix (5.0.1)
|
||||||
puma (5.6.7)
|
puma (5.6.8)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.3.0)
|
pundit (2.3.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.7.3)
|
racc (1.7.3)
|
||||||
rack (2.2.8)
|
rack (2.2.9)
|
||||||
rack-attack (6.6.1)
|
rack-attack (6.6.1)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rack-cors (1.1.1)
|
rack-cors (1.1.1)
|
||||||
@ -512,20 +513,20 @@ GEM
|
|||||||
rack
|
rack
|
||||||
rack-test (2.0.2)
|
rack-test (2.0.2)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rails (6.1.7.6)
|
rails (6.1.7.8)
|
||||||
actioncable (= 6.1.7.6)
|
actioncable (= 6.1.7.8)
|
||||||
actionmailbox (= 6.1.7.6)
|
actionmailbox (= 6.1.7.8)
|
||||||
actionmailer (= 6.1.7.6)
|
actionmailer (= 6.1.7.8)
|
||||||
actionpack (= 6.1.7.6)
|
actionpack (= 6.1.7.8)
|
||||||
actiontext (= 6.1.7.6)
|
actiontext (= 6.1.7.8)
|
||||||
actionview (= 6.1.7.6)
|
actionview (= 6.1.7.8)
|
||||||
activejob (= 6.1.7.6)
|
activejob (= 6.1.7.8)
|
||||||
activemodel (= 6.1.7.6)
|
activemodel (= 6.1.7.8)
|
||||||
activerecord (= 6.1.7.6)
|
activerecord (= 6.1.7.8)
|
||||||
activestorage (= 6.1.7.6)
|
activestorage (= 6.1.7.8)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 6.1.7.8)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 6.1.7.6)
|
railties (= 6.1.7.8)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
rails-controller-testing (1.0.5)
|
rails-controller-testing (1.0.5)
|
||||||
actionpack (>= 5.0.1.rc1)
|
actionpack (>= 5.0.1.rc1)
|
||||||
@ -541,9 +542,9 @@ GEM
|
|||||||
railties (>= 6.0.0, < 7)
|
railties (>= 6.0.0, < 7)
|
||||||
rails-settings-cached (0.6.6)
|
rails-settings-cached (0.6.6)
|
||||||
rails (>= 4.2.0)
|
rails (>= 4.2.0)
|
||||||
railties (6.1.7.6)
|
railties (6.1.7.8)
|
||||||
actionpack (= 6.1.7.6)
|
actionpack (= 6.1.7.8)
|
||||||
activesupport (= 6.1.7.6)
|
activesupport (= 6.1.7.8)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
@ -565,7 +566,8 @@ GEM
|
|||||||
responders (3.0.1)
|
responders (3.0.1)
|
||||||
actionpack (>= 5.0)
|
actionpack (>= 5.0)
|
||||||
railties (>= 5.0)
|
railties (>= 5.0)
|
||||||
rexml (3.2.5)
|
rexml (3.2.8)
|
||||||
|
strscan (>= 3.0.9)
|
||||||
rotp (6.2.0)
|
rotp (6.2.0)
|
||||||
rpam2 (4.0.2)
|
rpam2 (4.0.2)
|
||||||
rqrcode (2.1.2)
|
rqrcode (2.1.2)
|
||||||
@ -663,7 +665,8 @@ GEM
|
|||||||
simplecov-html (0.12.3)
|
simplecov-html (0.12.3)
|
||||||
simplecov_json_formatter (0.1.4)
|
simplecov_json_formatter (0.1.4)
|
||||||
smart_properties (1.17.0)
|
smart_properties (1.17.0)
|
||||||
sprockets (3.7.2)
|
sprockets (3.7.3)
|
||||||
|
base64
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
rack (> 1, < 3)
|
rack (> 1, < 3)
|
||||||
sprockets-rails (3.4.2)
|
sprockets-rails (3.4.2)
|
||||||
@ -679,6 +682,7 @@ GEM
|
|||||||
redlock (~> 1.0)
|
redlock (~> 1.0)
|
||||||
strong_migrations (0.7.9)
|
strong_migrations (0.7.9)
|
||||||
activerecord (>= 5)
|
activerecord (>= 5)
|
||||||
|
strscan (3.1.0)
|
||||||
swd (1.3.0)
|
swd (1.3.0)
|
||||||
activesupport (>= 3)
|
activesupport (>= 3)
|
||||||
attr_required (>= 0.0.5)
|
attr_required (>= 0.0.5)
|
||||||
@ -753,7 +757,7 @@ GEM
|
|||||||
xorcist (1.1.3)
|
xorcist (1.1.3)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.6.12)
|
zeitwerk (2.6.16)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
@ -816,6 +820,7 @@ DEPENDENCIES
|
|||||||
letter_opener_web (~> 2.0)
|
letter_opener_web (~> 2.0)
|
||||||
link_header (~> 0.0)
|
link_header (~> 0.0)
|
||||||
lograge (~> 0.12)
|
lograge (~> 0.12)
|
||||||
|
mail (~> 2.8)
|
||||||
makara (~> 0.5)
|
makara (~> 0.5)
|
||||||
mario-redis-lock (~> 1.2)
|
mario-redis-lock (~> 1.2)
|
||||||
memory_profiler
|
memory_profiler
|
||||||
|
@ -25,6 +25,8 @@ class Admin::DomainAllowsController < Admin::BaseController
|
|||||||
def destroy
|
def destroy
|
||||||
authorize @domain_allow, :destroy?
|
authorize @domain_allow, :destroy?
|
||||||
UnallowDomainService.new.call(@domain_allow)
|
UnallowDomainService.new.call(@domain_allow)
|
||||||
|
log_action :destroy, @domain_allow
|
||||||
|
|
||||||
redirect_to admin_instances_path, notice: I18n.t('admin.domain_allows.destroyed_msg')
|
redirect_to admin_instances_path, notice: I18n.t('admin.domain_allows.destroyed_msg')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -19,10 +19,11 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
|
|||||||
def create
|
def create
|
||||||
authorize :domain_block, :create?
|
authorize :domain_block, :create?
|
||||||
|
|
||||||
|
@domain_block = DomainBlock.new(resource_params)
|
||||||
existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil
|
existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil
|
||||||
return render json: existing_domain_block, serializer: REST::Admin::ExistingDomainBlockErrorSerializer, status: 422 if existing_domain_block.present?
|
return render json: existing_domain_block, serializer: REST::Admin::ExistingDomainBlockErrorSerializer, status: 422 if conflicts_with_existing_block?(@domain_block, existing_domain_block)
|
||||||
|
|
||||||
@domain_block = DomainBlock.create!(resource_params)
|
@domain_block.save!
|
||||||
DomainBlockWorker.perform_async(@domain_block.id)
|
DomainBlockWorker.perform_async(@domain_block.id)
|
||||||
log_action :create, @domain_block
|
log_action :create, @domain_block
|
||||||
render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer
|
render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer
|
||||||
@ -55,6 +56,10 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def conflicts_with_existing_block?(domain_block, existing_domain_block)
|
||||||
|
existing_domain_block.present? && (existing_domain_block.domain == TagManager.instance.normalize_domain(domain_block.domain) || !domain_block.stricter_than?(existing_domain_block))
|
||||||
|
end
|
||||||
|
|
||||||
def set_domain_blocks
|
def set_domain_blocks
|
||||||
@domain_blocks = filtered_domain_blocks.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
@domain_blocks = filtered_domain_blocks.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||||
end
|
end
|
||||||
|
@ -12,6 +12,10 @@ class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def set_recently_used_tags
|
def set_recently_used_tags
|
||||||
@recently_used_tags = Tag.recently_used(current_account).where.not(id: current_account.featured_tags).limit(10)
|
@recently_used_tags = Tag.recently_used(current_account).where.not(id: featured_tag_ids).limit(10)
|
||||||
|
end
|
||||||
|
|
||||||
|
def featured_tag_ids
|
||||||
|
current_account.featured_tags.pluck(:tag_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -6,6 +6,7 @@ class Api::V1::ScheduledStatusesController < Api::BaseController
|
|||||||
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, except: [:update, :destroy]
|
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, except: [:update, :destroy]
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:update, :destroy]
|
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:update, :destroy]
|
||||||
|
|
||||||
|
before_action :require_user!
|
||||||
before_action :set_statuses, only: :index
|
before_action :set_statuses, only: :index
|
||||||
before_action :set_status, except: :index
|
before_action :set_status, except: :index
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ class Api::V1::Statuses::TranslationsController < Api::BaseController
|
|||||||
include Authorization
|
include Authorization
|
||||||
|
|
||||||
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
|
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
|
||||||
|
before_action :require_user!
|
||||||
before_action :set_status
|
before_action :set_status
|
||||||
before_action :set_translation
|
before_action :set_translation
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Timelines::PublicController < Api::BaseController
|
class Api::V1::Timelines::PublicController < Api::BaseController
|
||||||
|
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
|
||||||
before_action :require_user!, only: [:show], if: :require_auth?
|
before_action :require_user!, only: [:show], if: :require_auth?
|
||||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Timelines::TagController < Api::BaseController
|
class Api::V1::Timelines::TagController < Api::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :show, if: :require_auth?
|
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
|
||||||
|
before_action :require_user!, if: :require_auth?
|
||||||
before_action :load_tag
|
before_action :load_tag
|
||||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||||
|
|
||||||
|
@ -28,29 +28,19 @@ module CacheConcern
|
|||||||
response.headers['Vary'] = public_fetch_mode? ? 'Accept' : 'Accept, Signature'
|
response.headers['Vary'] = public_fetch_mode? ? 'Accept' : 'Accept, Signature'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO: Rename this method, as it does not perform any caching anymore.
|
||||||
def cache_collection(raw, klass)
|
def cache_collection(raw, klass)
|
||||||
return raw unless klass.respond_to?(:with_includes)
|
return raw unless klass.respond_to?(:preload_cacheable_associations)
|
||||||
|
|
||||||
raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
|
records = raw.to_a
|
||||||
return [] if raw.empty?
|
|
||||||
|
|
||||||
cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id)
|
klass.preload_cacheable_associations(records)
|
||||||
uncached_ids = raw.map(&:id) - cached_keys_with_value.keys
|
|
||||||
|
|
||||||
klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!)
|
records
|
||||||
|
|
||||||
unless uncached_ids.empty?
|
|
||||||
uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id)
|
|
||||||
|
|
||||||
uncached.each_value do |item|
|
|
||||||
Rails.cache.write(item, item)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
raw.filter_map { |item| cached_keys_with_value[item.id] || uncached[item.id] }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO: Rename this method, as it does not perform any caching anymore.
|
||||||
def cache_collection_paginated_by_id(raw, klass, limit, options)
|
def cache_collection_paginated_by_id(raw, klass, limit, options)
|
||||||
cache_collection raw.cache_ids.to_a_paginated_by_id(limit, options), klass
|
cache_collection raw.to_a_paginated_by_id(limit, options), klass
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -16,6 +16,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
|||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
Web::PushSubscription.unsubscribe_for(params[:id], current_resource_owner)
|
Web::PushSubscription.unsubscribe_for(params[:id], current_resource_owner)
|
||||||
|
Doorkeeper::Application.find_by(id: params[:id])&.close_streaming_sessions(current_resource_owner)
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ module WellKnown
|
|||||||
def set_account
|
def set_account
|
||||||
username = username_from_resource
|
username = username_from_resource
|
||||||
@account = begin
|
@account = begin
|
||||||
if username == Rails.configuration.x.local_domain
|
if username == Rails.configuration.x.local_domain || username == Rails.configuration.x.web_domain
|
||||||
Account.representative
|
Account.representative
|
||||||
else
|
else
|
||||||
Account.find_local!(username)
|
Account.find_local!(username)
|
||||||
|
@ -111,7 +111,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||||||
def find_existing_status
|
def find_existing_status
|
||||||
status = status_from_uri(object_uri)
|
status = status_from_uri(object_uri)
|
||||||
status ||= Status.find_by(uri: @object['atomUri']) if @object['atomUri'].present?
|
status ||= Status.find_by(uri: @object['atomUri']) if @object['atomUri'].present?
|
||||||
status
|
status if status&.account_id == @account.id
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_status_params
|
def process_status_params
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
class ActivityPub::Parser::StatusParser
|
class ActivityPub::Parser::StatusParser
|
||||||
include JsonLdHelper
|
include JsonLdHelper
|
||||||
|
|
||||||
|
NORMALIZED_LOCALE_NAMES = LanguagesHelper::SUPPORTED_LOCALES.keys.index_by(&:downcase).freeze
|
||||||
|
|
||||||
# @param [Hash] json
|
# @param [Hash] json
|
||||||
# @param [Hash] magic_values
|
# @param [Hash] magic_values
|
||||||
# @option magic_values [String] :followers_collection
|
# @option magic_values [String] :followers_collection
|
||||||
@ -86,6 +88,13 @@ class ActivityPub::Parser::StatusParser
|
|||||||
end
|
end
|
||||||
|
|
||||||
def language
|
def language
|
||||||
|
lang = raw_language_code
|
||||||
|
lang.presence && NORMALIZED_LOCALE_NAMES.fetch(lang.downcase.to_sym, lang)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def raw_language_code
|
||||||
if content_language_map?
|
if content_language_map?
|
||||||
@object['contentMap'].keys.first
|
@object['contentMap'].keys.first
|
||||||
elsif name_language_map?
|
elsif name_language_map?
|
||||||
@ -95,8 +104,6 @@ class ActivityPub::Parser::StatusParser
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def audience_to
|
def audience_to
|
||||||
as_array(@object['to'] || @json['to']).map { |x| value_or_id(x) }
|
as_array(@object['to'] || @json['to']).map { |x| value_or_id(x) }
|
||||||
end
|
end
|
||||||
|
@ -14,17 +14,19 @@ module ApplicationExtension
|
|||||||
# dependent: delete_all, which means the ActiveRecord callback in
|
# dependent: delete_all, which means the ActiveRecord callback in
|
||||||
# AccessTokenExtension is not run, so instead we manually announce to
|
# AccessTokenExtension is not run, so instead we manually announce to
|
||||||
# streaming that these tokens are being deleted.
|
# streaming that these tokens are being deleted.
|
||||||
before_destroy :push_to_streaming_api, prepend: true
|
before_destroy :close_streaming_sessions, prepend: true
|
||||||
end
|
end
|
||||||
|
|
||||||
def confirmation_redirect_uri
|
def confirmation_redirect_uri
|
||||||
redirect_uri.lines.first.strip
|
redirect_uri.lines.first.strip
|
||||||
end
|
end
|
||||||
|
|
||||||
def push_to_streaming_api
|
def close_streaming_sessions(resource_owner = nil)
|
||||||
# TODO: #28793 Combine into a single topic
|
# TODO: #28793 Combine into a single topic
|
||||||
payload = Oj.dump(event: :kill)
|
payload = Oj.dump(event: :kill)
|
||||||
access_tokens.in_batches do |tokens|
|
scope = access_tokens
|
||||||
|
scope = scope.where(resource_owner_id: resource_owner.id) unless resource_owner.nil?
|
||||||
|
scope.in_batches do |tokens|
|
||||||
redis.pipelined do |pipeline|
|
redis.pipelined do |pipeline|
|
||||||
tokens.ids.each do |id|
|
tokens.ids.each do |id|
|
||||||
pipeline.publish("timeline:access_token:#{id}", payload)
|
pipeline.publish("timeline:access_token:#{id}", payload)
|
||||||
|
@ -255,15 +255,20 @@ class LinkDetailsExtractor
|
|||||||
end
|
end
|
||||||
|
|
||||||
def document
|
def document
|
||||||
@document ||= Nokogiri::HTML(@html, nil, encoding)
|
@document ||= detect_encoding_and_parse_document
|
||||||
end
|
end
|
||||||
|
|
||||||
def encoding
|
def detect_encoding_and_parse_document
|
||||||
@encoding ||= begin
|
[detect_encoding, nil, @html_charset, 'UTF-8'].uniq.each do |encoding|
|
||||||
|
document = Nokogiri::HTML(@html, nil, encoding)
|
||||||
|
return document if document.to_s.valid_encoding?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def detect_encoding
|
||||||
guess = detector.detect(@html, @html_charset)
|
guess = detector.detect(@html, @html_charset)
|
||||||
guess&.fetch(:confidence, 0).to_i > 60 ? guess&.fetch(:encoding, nil) : nil
|
guess&.fetch(:confidence, 0).to_i > 60 ? guess&.fetch(:encoding, nil) : nil
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def detector
|
def detector
|
||||||
@detector ||= CharlockHolmes::EncodingDetector.new.tap do |detector|
|
@detector ||= CharlockHolmes::EncodingDetector.new.tap do |detector|
|
||||||
|
@ -101,7 +101,7 @@ class SearchQueryTransformer < Parslet::Transform
|
|||||||
end
|
end
|
||||||
|
|
||||||
rule(clause: subtree(:clause)) do
|
rule(clause: subtree(:clause)) do
|
||||||
prefix = clause[:prefix][:term].to_s if clause[:prefix]
|
prefix = clause[:prefix][:term].to_s.downcase if clause[:prefix]
|
||||||
operator = clause[:operator]&.to_s
|
operator = clause[:operator]&.to_s
|
||||||
|
|
||||||
if clause[:prefix]
|
if clause[:prefix]
|
||||||
|
@ -22,7 +22,7 @@ class VideoMetadataExtractor
|
|||||||
private
|
private
|
||||||
|
|
||||||
def ffmpeg_command_output
|
def ffmpeg_command_output
|
||||||
command = Terrapin::CommandLine.new('ffprobe', '-i :path -print_format :format -show_format -show_streams -show_error -loglevel :loglevel')
|
command = Terrapin::CommandLine.new(Rails.configuration.x.ffprobe_binary, '-i :path -print_format :format -show_format -show_streams -show_error -loglevel :loglevel')
|
||||||
command.run(path: @path, format: 'json', loglevel: 'fatal')
|
command.run(path: @path, format: 'json', loglevel: 'fatal')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -47,4 +47,13 @@ class AdminMailer < ApplicationMailer
|
|||||||
mail to: @me.user_email, subject: I18n.t('admin_mailer.new_trends.subject', instance: @instance)
|
mail to: @me.user_email, subject: I18n.t('admin_mailer.new_trends.subject', instance: @instance)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def auto_close_registrations(recipient)
|
||||||
|
@me = recipient
|
||||||
|
@instance = Rails.configuration.x.local_domain
|
||||||
|
|
||||||
|
locale_for_account(@me) do
|
||||||
|
mail to: @me.user_email, subject: I18n.t('admin_mailer.auto_close_registrations.subject', instance: @instance)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -187,7 +187,7 @@ module AccountInteractions
|
|||||||
end
|
end
|
||||||
|
|
||||||
def unblock_domain!(other_domain)
|
def unblock_domain!(other_domain)
|
||||||
block = domain_blocks.find_by(domain: other_domain)
|
block = domain_blocks.find_by(domain: normalized_domain(other_domain))
|
||||||
block&.destroy
|
block&.destroy
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -299,4 +299,8 @@ module AccountInteractions
|
|||||||
def remove_potential_friendship(other_account)
|
def remove_potential_friendship(other_account)
|
||||||
PotentialFriendshipTracker.remove(id, other_account.id)
|
PotentialFriendshipTracker.remove(id, other_account.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def normalized_domain(domain)
|
||||||
|
TagManager.instance.normalize_domain(domain)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -14,6 +14,10 @@ module Cacheable
|
|||||||
includes(@cache_associated)
|
includes(@cache_associated)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def preload_cacheable_associations(records)
|
||||||
|
ActiveRecord::Associations::Preloader.new.preload(records, @cache_associated)
|
||||||
|
end
|
||||||
|
|
||||||
def cache_ids
|
def cache_ids
|
||||||
select(:id, :updated_at)
|
select(:id, :updated_at)
|
||||||
end
|
end
|
||||||
|
@ -28,7 +28,7 @@ class Feed
|
|||||||
unhydrated = redis.zrangebyscore(key, "(#{min_id}", "(#{max_id}", limit: [0, limit], with_scores: true).map(&:first).map(&:to_i)
|
unhydrated = redis.zrangebyscore(key, "(#{min_id}", "(#{max_id}", limit: [0, limit], with_scores: true).map(&:first).map(&:to_i)
|
||||||
end
|
end
|
||||||
|
|
||||||
Status.where(id: unhydrated).cache_ids
|
Status.where(id: unhydrated)
|
||||||
end
|
end
|
||||||
|
|
||||||
def key
|
def key
|
||||||
|
@ -29,7 +29,7 @@ class PublicFeed
|
|||||||
scope.merge!(media_only_scope) if media_only?
|
scope.merge!(media_only_scope) if media_only?
|
||||||
scope.merge!(language_scope) if account&.chosen_languages.present?
|
scope.merge!(language_scope) if account&.chosen_languages.present?
|
||||||
|
|
||||||
scope.cache_ids.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id)
|
scope.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -361,38 +361,6 @@ class Status < ApplicationRecord
|
|||||||
StatusPin.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |p, h| h[p.status_id] = true }
|
StatusPin.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |p, h| h[p.status_id] = true }
|
||||||
end
|
end
|
||||||
|
|
||||||
def reload_stale_associations!(cached_items)
|
|
||||||
account_ids = []
|
|
||||||
|
|
||||||
cached_items.each do |item|
|
|
||||||
account_ids << item.account_id
|
|
||||||
account_ids << item.reblog.account_id if item.reblog?
|
|
||||||
end
|
|
||||||
|
|
||||||
account_ids.uniq!
|
|
||||||
|
|
||||||
status_ids = cached_items.map { |item| item.reblog? ? item.reblog_of_id : item.id }.uniq
|
|
||||||
|
|
||||||
return if account_ids.empty?
|
|
||||||
|
|
||||||
accounts = Account.where(id: account_ids).includes(:account_stat, :user).index_by(&:id)
|
|
||||||
|
|
||||||
status_stats = StatusStat.where(status_id: status_ids).index_by(&:status_id)
|
|
||||||
|
|
||||||
cached_items.each do |item|
|
|
||||||
item.account = accounts[item.account_id]
|
|
||||||
item.reblog.account = accounts[item.reblog.account_id] if item.reblog?
|
|
||||||
|
|
||||||
if item.reblog?
|
|
||||||
status_stat = status_stats[item.reblog.id]
|
|
||||||
item.reblog.status_stat = status_stat if status_stat.present?
|
|
||||||
else
|
|
||||||
status_stat = status_stats[item.id]
|
|
||||||
item.status_stat = status_stat if status_stat.present?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def from_text(text)
|
def from_text(text)
|
||||||
return [] if text.blank?
|
return [] if text.blank?
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ class StatusEdit < ApplicationRecord
|
|||||||
default_scope { order(id: :asc) }
|
default_scope { order(id: :asc) }
|
||||||
|
|
||||||
delegate :local?, :application, :edited?, :edited_at,
|
delegate :local?, :application, :edited?, :edited_at,
|
||||||
:discarded?, :visibility, to: :status
|
:discarded?, :visibility, :language, to: :status
|
||||||
|
|
||||||
def emojis
|
def emojis
|
||||||
return @emojis if defined?(@emojis)
|
return @emojis if defined?(@emojis)
|
||||||
|
@ -33,7 +33,7 @@ class TagFeed < PublicFeed
|
|||||||
scope.merge!(account_filters_scope) if account?
|
scope.merge!(account_filters_scope) if account?
|
||||||
scope.merge!(media_only_scope) if media_only?
|
scope.merge!(media_only_scope) if media_only?
|
||||||
|
|
||||||
scope.cache_ids.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id)
|
scope.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -94,6 +94,9 @@ class User < ApplicationRecord
|
|||||||
validates :invite_request, presence: true, on: :create, if: :invite_text_required?
|
validates :invite_request, presence: true, on: :create, if: :invite_text_required?
|
||||||
|
|
||||||
validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale?
|
validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale?
|
||||||
|
|
||||||
|
validates :email, presence: true, email_address: true
|
||||||
|
|
||||||
validates_with BlacklistedEmailValidator, if: -> { ENV['EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION'] == 'true' || !confirmed? }
|
validates_with BlacklistedEmailValidator, if: -> { ENV['EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION'] == 'true' || !confirmed? }
|
||||||
validates_with EmailMxValidator, if: :validate_email_dns?
|
validates_with EmailMxValidator, if: :validate_email_dns?
|
||||||
validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create
|
validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create
|
||||||
|
@ -16,11 +16,18 @@ class ManifestSerializer < ActiveModel::Serializer
|
|||||||
512
|
512
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
attributes :name, :short_name,
|
attributes :id, :name, :short_name,
|
||||||
:icons, :theme_color, :background_color,
|
:icons, :theme_color, :background_color,
|
||||||
:display, :start_url, :scope,
|
:display, :start_url, :scope,
|
||||||
:share_target, :shortcuts
|
:share_target, :shortcuts
|
||||||
|
|
||||||
|
def id
|
||||||
|
# This is set to `/home` because that was the old value of `start_url` and
|
||||||
|
# thus the fallback ID computed by Chrome:
|
||||||
|
# https://developer.chrome.com/blog/pwa-manifest-id/
|
||||||
|
'/home'
|
||||||
|
end
|
||||||
|
|
||||||
def name
|
def name
|
||||||
object.title
|
object.title
|
||||||
end
|
end
|
||||||
@ -53,7 +60,7 @@ class ManifestSerializer < ActiveModel::Serializer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def start_url
|
def start_url
|
||||||
'/home'
|
'/'
|
||||||
end
|
end
|
||||||
|
|
||||||
def scope
|
def scope
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class REST::Admin::DomainBlockSerializer < ActiveModel::Serializer
|
class REST::Admin::DomainBlockSerializer < ActiveModel::Serializer
|
||||||
attributes :id, :domain, :created_at, :severity,
|
attributes :id, :domain, :digest, :created_at, :severity,
|
||||||
:reject_media, :reject_reports,
|
:reject_media, :reject_reports,
|
||||||
:private_comment, :public_comment, :obfuscate
|
:private_comment, :public_comment, :obfuscate
|
||||||
|
|
||||||
def id
|
def id
|
||||||
object.id.to_s
|
object.id.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def digest
|
||||||
|
object.domain_digest
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -199,10 +199,15 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||||||
value = first_of_value(@json[key])
|
value = first_of_value(@json[key])
|
||||||
|
|
||||||
return if value.nil?
|
return if value.nil?
|
||||||
return value['url'] if value.is_a?(Hash)
|
|
||||||
|
|
||||||
image = fetch_resource_without_id_validation(value)
|
if value.is_a?(String)
|
||||||
image['url'] if image
|
value = fetch_resource_without_id_validation(value)
|
||||||
|
return if value.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
value = first_of_value(value['url']) if value.is_a?(Hash) && value['type'] == 'Image'
|
||||||
|
value = value['href'] if value.is_a?(Hash)
|
||||||
|
value if value.is_a?(String)
|
||||||
end
|
end
|
||||||
|
|
||||||
def public_key
|
def public_key
|
||||||
|
@ -15,6 +15,9 @@ class FetchLinkCardService < BaseService
|
|||||||
)
|
)
|
||||||
}iox
|
}iox
|
||||||
|
|
||||||
|
# URL size limit to safely store in PosgreSQL's unique indexes
|
||||||
|
BYTESIZE_LIMIT = 2692
|
||||||
|
|
||||||
def call(status)
|
def call(status)
|
||||||
@status = status
|
@status = status
|
||||||
@original_url = parse_urls
|
@original_url = parse_urls
|
||||||
@ -85,7 +88,7 @@ class FetchLinkCardService < BaseService
|
|||||||
|
|
||||||
def bad_url?(uri)
|
def bad_url?(uri)
|
||||||
# Avoid local instance URLs and invalid URLs
|
# Avoid local instance URLs and invalid URLs
|
||||||
uri.host.blank? || TagManager.instance.local_url?(uri.to_s) || !%w(http https).include?(uri.scheme)
|
uri.host.blank? || TagManager.instance.local_url?(uri.to_s) || !%w(http https).include?(uri.scheme) || uri.to_s.bytesize > BYTESIZE_LIMIT
|
||||||
end
|
end
|
||||||
|
|
||||||
def mention_link?(anchor)
|
def mention_link?(anchor)
|
||||||
|
@ -62,16 +62,17 @@ class NotifyService < BaseService
|
|||||||
LEFT JOIN mentions m ON m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id
|
LEFT JOIN mentions m ON m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id
|
||||||
WHERE s.id = :id
|
WHERE s.id = :id
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT s.id, s.in_reply_to_id, m.id, st.path || s.id, st.depth + 1
|
SELECT s.id, s.in_reply_to_id, m.id, ancestors.path || s.id, ancestors.depth + 1
|
||||||
FROM ancestors st
|
FROM ancestors
|
||||||
JOIN statuses s ON s.id = st.in_reply_to_id
|
JOIN statuses s ON s.id = ancestors.in_reply_to_id
|
||||||
LEFT JOIN mentions m ON m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id
|
/* early exit if we already have a mention matching our requirements */
|
||||||
WHERE st.mention_id IS NULL AND NOT s.id = ANY(path) AND st.depth < :depth_limit
|
LEFT JOIN mentions m ON m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id AND s.account_id = :recipient_id
|
||||||
|
WHERE ancestors.mention_id IS NULL AND NOT s.id = ANY(path) AND ancestors.depth < :depth_limit
|
||||||
)
|
)
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM ancestors st
|
FROM ancestors
|
||||||
JOIN statuses s ON s.id = st.id
|
JOIN statuses s ON s.id = ancestors.id
|
||||||
WHERE st.mention_id IS NOT NULL AND s.visibility = 3
|
WHERE ancestors.mention_id IS NOT NULL AND s.account_id = :recipient_id AND s.visibility = 3
|
||||||
SQL
|
SQL
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -164,7 +164,7 @@ class PostStatusService < BaseService
|
|||||||
|
|
||||||
def idempotency_duplicate
|
def idempotency_duplicate
|
||||||
if scheduled?
|
if scheduled?
|
||||||
@account.schedule_statuses.find(@idempotency_duplicate)
|
@account.scheduled_statuses.find(@idempotency_duplicate)
|
||||||
else
|
else
|
||||||
@account.statuses.find(@idempotency_duplicate)
|
@account.statuses.find(@idempotency_duplicate)
|
||||||
end
|
end
|
||||||
@ -175,7 +175,7 @@ class PostStatusService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def scheduled_in_the_past?
|
def scheduled_in_the_past?
|
||||||
@scheduled_at.present? && @scheduled_at <= Time.now.utc + MIN_SCHEDULE_OFFSET
|
@scheduled_at.present? && @scheduled_at <= Time.now.utc
|
||||||
end
|
end
|
||||||
|
|
||||||
def bump_potential_friendship!
|
def bump_potential_friendship!
|
||||||
@ -217,7 +217,7 @@ class PostStatusService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def scheduled_options
|
def scheduled_options
|
||||||
@options.tap do |options_hash|
|
@options.dup.tap do |options_hash|
|
||||||
options_hash[:in_reply_to_id] = options_hash.delete(:thread)&.id
|
options_hash[:in_reply_to_id] = options_hash.delete(:thread)&.id
|
||||||
options_hash[:application_id] = options_hash.delete(:application)&.id
|
options_hash[:application_id] = options_hash.delete(:application)&.id
|
||||||
options_hash[:scheduled_at] = nil
|
options_hash[:scheduled_at] = nil
|
||||||
|
@ -19,7 +19,7 @@ class VerifyLinkService < BaseService
|
|||||||
|
|
||||||
def perform_request!
|
def perform_request!
|
||||||
@body = Request.new(:get, @url).add_headers('Accept' => 'text/html').perform do |res|
|
@body = Request.new(:get, @url).add_headers('Accept' => 'text/html').perform do |res|
|
||||||
res.code == 200 ? res.body_with_limit : nil
|
res.code == 200 ? res.truncated_body : nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
18
app/validators/email_address_validator.rb
Normal file
18
app/validators/email_address_validator.rb
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# NOTE: I initially wrote this as `EmailValidator` but it ended up clashing
|
||||||
|
# with an indirect dependency of ours, `validate_email`, which, turns out,
|
||||||
|
# has the same approach as we do, but with an extra check disallowing
|
||||||
|
# single-label domains. Decided to not switch to `validate_email` because
|
||||||
|
# we do want to allow at least `localhost`.
|
||||||
|
|
||||||
|
class EmailAddressValidator < ActiveModel::EachValidator
|
||||||
|
def validate_each(record, attribute, value)
|
||||||
|
value = value.strip
|
||||||
|
|
||||||
|
address = Mail::Address.new(value)
|
||||||
|
record.errors.add(attribute, :invalid) if address.address != value
|
||||||
|
rescue Mail::Field::FieldError
|
||||||
|
record.errors.add(attribute, :invalid)
|
||||||
|
end
|
||||||
|
end
|
@ -13,6 +13,8 @@
|
|||||||
|
|
||||||
%p.lead= t('admin.settings.registrations.preamble')
|
%p.lead= t('admin.settings.registrations.preamble')
|
||||||
|
|
||||||
|
.flash-message= t('admin.settings.registrations.moderation_recommandation')
|
||||||
|
|
||||||
.fields-row
|
.fields-row
|
||||||
.fields-row__column.fields-row__column-6.fields-group
|
.fields-row__column.fields-row__column-6.fields-group
|
||||||
= f.input :registrations_mode, collection: %w(open approved none), wrapper: :with_label, include_blank: false, label_method: lambda { |mode| I18n.t("admin.settings.registrations_mode.modes.#{mode}") }
|
= f.input :registrations_mode, collection: %w(open approved none), wrapper: :with_label, include_blank: false, label_method: lambda { |mode| I18n.t("admin.settings.registrations_mode.modes.#{mode}") }
|
||||||
|
3
app/views/admin_mailer/auto_close_registrations.text.erb
Normal file
3
app/views/admin_mailer/auto_close_registrations.text.erb
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<%= raw t('admin_mailer.auto_close_registrations.body', instance: @instance) %>
|
||||||
|
|
||||||
|
<%= raw t('application_mailer.view')%> <%= admin_settings_registrations_url %>
|
33
app/workers/scheduler/auto_close_registrations_scheduler.rb
Normal file
33
app/workers/scheduler/auto_close_registrations_scheduler.rb
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Scheduler::AutoCloseRegistrationsScheduler
|
||||||
|
include Sidekiq::Worker
|
||||||
|
include Redisable
|
||||||
|
|
||||||
|
sidekiq_options retry: 0
|
||||||
|
|
||||||
|
# Automatically switch away from open registrations if no
|
||||||
|
# moderator had any activity in that period of time
|
||||||
|
OPEN_REGISTRATIONS_MODERATOR_THRESHOLD = 1.week + UserTrackingConcern::SIGN_IN_UPDATE_FREQUENCY
|
||||||
|
|
||||||
|
def perform
|
||||||
|
return if Rails.configuration.x.email_domains_whitelist.present? || ENV['DISABLE_AUTOMATIC_SWITCHING_TO_APPROVED_REGISTRATIONS'] == 'true'
|
||||||
|
return unless Setting.registrations_mode == 'open'
|
||||||
|
|
||||||
|
switch_to_approval_mode! unless active_moderators?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def active_moderators?
|
||||||
|
User.those_who_can(:manage_reports).exists?(current_sign_in_at: OPEN_REGISTRATIONS_MODERATOR_THRESHOLD.ago...)
|
||||||
|
end
|
||||||
|
|
||||||
|
def switch_to_approval_mode!
|
||||||
|
Setting.registrations_mode = 'approved'
|
||||||
|
|
||||||
|
User.those_who_can(:manage_settings).includes(:account).find_each do |user|
|
||||||
|
AdminMailer.auto_close_registrations(user.account).deliver_later
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -44,6 +44,7 @@ require_relative '../lib/chewy/strategy/bypass_with_warning'
|
|||||||
require_relative '../lib/webpacker/manifest_extensions'
|
require_relative '../lib/webpacker/manifest_extensions'
|
||||||
require_relative '../lib/webpacker/helper_extensions'
|
require_relative '../lib/webpacker/helper_extensions'
|
||||||
require_relative '../lib/rails/engine_extensions'
|
require_relative '../lib/rails/engine_extensions'
|
||||||
|
require_relative '../lib/action_dispatch/remote_ip_extensions'
|
||||||
require_relative '../lib/active_record/database_tasks_extensions'
|
require_relative '../lib/active_record/database_tasks_extensions'
|
||||||
require_relative '../lib/active_record/batches'
|
require_relative '../lib/active_record/batches'
|
||||||
require_relative '../lib/simple_navigation/item_extensions'
|
require_relative '../lib/simple_navigation/item_extensions'
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
if ENV['FFMPEG_BINARY'].present?
|
# frozen_string_literal: true
|
||||||
FFMPEG.ffmpeg_binary = ENV['FFMPEG_BINARY']
|
|
||||||
|
Rails.application.configure do
|
||||||
|
config.x.ffmpeg_binary = ENV['FFMPEG_BINARY'] || 'ffmpeg'
|
||||||
|
config.x.ffprobe_binary = ENV['FFPROBE_BINARY'] || 'ffprobe'
|
||||||
end
|
end
|
||||||
|
@ -37,6 +37,10 @@ class Rack::Attack
|
|||||||
authenticated_token&.id
|
authenticated_token&.id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def warden_user_id
|
||||||
|
@env['warden']&.user&.id
|
||||||
|
end
|
||||||
|
|
||||||
def unauthenticated?
|
def unauthenticated?
|
||||||
!authenticated_user_id
|
!authenticated_user_id
|
||||||
end
|
end
|
||||||
@ -58,10 +62,6 @@ class Rack::Attack
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Rack::Attack.safelist('allow from localhost') do |req|
|
|
||||||
req.remote_ip == '127.0.0.1' || req.remote_ip == '::1'
|
|
||||||
end
|
|
||||||
|
|
||||||
Rack::Attack.blocklist('deny from blocklist') do |req|
|
Rack::Attack.blocklist('deny from blocklist') do |req|
|
||||||
IpBlock.blocked?(req.remote_ip)
|
IpBlock.blocked?(req.remote_ip)
|
||||||
end
|
end
|
||||||
@ -137,6 +137,10 @@ class Rack::Attack
|
|||||||
req.session[:attempt_user_id] || req.params.dig('user', 'email').presence if req.post? && req.path_matches?('/auth/sign_in')
|
req.session[:attempt_user_id] || req.params.dig('user', 'email').presence if req.post? && req.path_matches?('/auth/sign_in')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
throttle('throttle_password_change/account', limit: 10, period: 10.minutes) do |req|
|
||||||
|
req.warden_user_id if req.put? || (req.patch? && req.path_matches?('/auth'))
|
||||||
|
end
|
||||||
|
|
||||||
self.throttled_responder = lambda do |request|
|
self.throttled_responder = lambda do |request|
|
||||||
now = Time.now.utc
|
now = Time.now.utc
|
||||||
match_data = request.env['rack.attack.match_data']
|
match_data = request.env['rack.attack.match_data']
|
||||||
|
@ -3,13 +3,17 @@
|
|||||||
if ENV['STATSD_ADDR'].present?
|
if ENV['STATSD_ADDR'].present?
|
||||||
host, port = ENV['STATSD_ADDR'].split(':')
|
host, port = ENV['STATSD_ADDR'].split(':')
|
||||||
|
|
||||||
$statsd = ::Statsd.new(host, port)
|
begin
|
||||||
$statsd.namespace = ENV.fetch('STATSD_NAMESPACE') { ['Mastodon', Rails.env].join('.') }
|
statsd = Statsd.new(host, port)
|
||||||
|
statsd.namespace = ENV.fetch('STATSD_NAMESPACE') { ['Mastodon', Rails.env].join('.') }
|
||||||
|
|
||||||
::NSA.inform_statsd($statsd) do |informant|
|
NSA.inform_statsd(statsd) do |informant|
|
||||||
informant.collect(:action_controller, :web)
|
informant.collect(:action_controller, :web)
|
||||||
informant.collect(:active_record, :db)
|
informant.collect(:active_record, :db)
|
||||||
informant.collect(:active_support_cache, :cache)
|
informant.collect(:active_support_cache, :cache)
|
||||||
informant.collect(:sidekiq, :sidekiq)
|
informant.collect(:sidekiq, :sidekiq)
|
||||||
end
|
end
|
||||||
|
rescue
|
||||||
|
Rails.logger.warn("statsd address #{ENV['STATSD_ADDR']} not reachable, proceeding without statsd")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -745,6 +745,7 @@ en:
|
|||||||
disabled: To no one
|
disabled: To no one
|
||||||
users: To logged-in local users
|
users: To logged-in local users
|
||||||
registrations:
|
registrations:
|
||||||
|
moderation_recommandation: Please make sure you have an adequate and reactive moderation team before you open registrations to everyone!
|
||||||
preamble: Control who can create an account on your server.
|
preamble: Control who can create an account on your server.
|
||||||
title: Registrations
|
title: Registrations
|
||||||
registrations_mode:
|
registrations_mode:
|
||||||
@ -905,6 +906,9 @@ en:
|
|||||||
title: Webhooks
|
title: Webhooks
|
||||||
webhook: Webhook
|
webhook: Webhook
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
|
auto_close_registrations:
|
||||||
|
body: Due to a lack of recent moderator activity, registrations on %{instance} have been automatically switched to requiring manual review, to prevent %{instance} from being used as a platform for potential bad actors. You can switch it back to open registrations at any time.
|
||||||
|
subject: Registrations for %{instance} have been automatically switched to requiring approval
|
||||||
new_appeal:
|
new_appeal:
|
||||||
actions:
|
actions:
|
||||||
delete_statuses: to delete their posts
|
delete_statuses: to delete their posts
|
||||||
|
@ -9,7 +9,7 @@ defaults: &defaults
|
|||||||
site_terms: ''
|
site_terms: ''
|
||||||
site_contact_username: ''
|
site_contact_username: ''
|
||||||
site_contact_email: ''
|
site_contact_email: ''
|
||||||
registrations_mode: 'open'
|
registrations_mode: 'none'
|
||||||
profile_directory: true
|
profile_directory: true
|
||||||
closed_registrations_message: ''
|
closed_registrations_message: ''
|
||||||
open_deletion: true
|
open_deletion: true
|
||||||
|
@ -58,3 +58,7 @@
|
|||||||
interval: 1 minute
|
interval: 1 minute
|
||||||
class: Scheduler::SuspendedUserCleanupScheduler
|
class: Scheduler::SuspendedUserCleanupScheduler
|
||||||
queue: scheduler
|
queue: scheduler
|
||||||
|
auto_close_registrations_scheduler:
|
||||||
|
interval: 1 hour
|
||||||
|
class: Scheduler::AutoCloseRegistrationsScheduler
|
||||||
|
queue: scheduler
|
||||||
|
@ -56,7 +56,7 @@ services:
|
|||||||
|
|
||||||
web:
|
web:
|
||||||
build: .
|
build: .
|
||||||
image: ghcr.io/mastodon/mastodon:v4.1.15
|
image: ghcr.io/mastodon/mastodon:v4.1.18
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
|
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
|
||||||
@ -77,7 +77,7 @@ services:
|
|||||||
|
|
||||||
streaming:
|
streaming:
|
||||||
build: .
|
build: .
|
||||||
image: ghcr.io/mastodon/mastodon:v4.1.15
|
image: ghcr.io/mastodon/mastodon:v4.1.18
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: node ./streaming
|
command: node ./streaming
|
||||||
@ -95,7 +95,7 @@ services:
|
|||||||
|
|
||||||
sidekiq:
|
sidekiq:
|
||||||
build: .
|
build: .
|
||||||
image: ghcr.io/mastodon/mastodon:v4.1.15
|
image: ghcr.io/mastodon/mastodon:v4.1.18
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bundle exec sidekiq
|
command: bundle exec sidekiq
|
||||||
|
72
lib/action_dispatch/remote_ip_extensions.rb
Normal file
72
lib/action_dispatch/remote_ip_extensions.rb
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Mastodon is not made to be directly accessed without a reverse proxy.
|
||||||
|
# This monkey-patch prevents remote IP address spoofing when being accessed
|
||||||
|
# directly.
|
||||||
|
#
|
||||||
|
# See PR: https://github.com/rails/rails/pull/51610
|
||||||
|
|
||||||
|
# In addition to the PR above, it also raises an error if a request with
|
||||||
|
# `X-Forwarded-For` or `Client-Ip` comes directly from a client without
|
||||||
|
# going through a trusted proxy.
|
||||||
|
|
||||||
|
# rubocop:disable all -- This is a mostly vendored file
|
||||||
|
|
||||||
|
module ActionDispatch
|
||||||
|
class RemoteIp
|
||||||
|
module GetIpExtensions
|
||||||
|
def calculate_ip
|
||||||
|
# Set by the Rack web server, this is a single value.
|
||||||
|
remote_addr = ips_from(@req.remote_addr).last
|
||||||
|
|
||||||
|
# Could be a CSV list and/or repeated headers that were concatenated.
|
||||||
|
client_ips = ips_from(@req.client_ip).reverse!
|
||||||
|
forwarded_ips = ips_from(@req.x_forwarded_for).reverse!
|
||||||
|
|
||||||
|
# `Client-Ip` and `X-Forwarded-For` should not, generally, both be set. If they
|
||||||
|
# are both set, it means that either:
|
||||||
|
#
|
||||||
|
# 1) This request passed through two proxies with incompatible IP header
|
||||||
|
# conventions.
|
||||||
|
#
|
||||||
|
# 2) The client passed one of `Client-Ip` or `X-Forwarded-For`
|
||||||
|
# (whichever the proxy servers weren't using) themselves.
|
||||||
|
#
|
||||||
|
# Either way, there is no way for us to determine which header is the right one
|
||||||
|
# after the fact. Since we have no idea, if we are concerned about IP spoofing
|
||||||
|
# we need to give up and explode. (If you're not concerned about IP spoofing you
|
||||||
|
# can turn the `ip_spoofing_check` option off.)
|
||||||
|
should_check_ip = @check_ip && client_ips.last && forwarded_ips.last
|
||||||
|
if should_check_ip && !forwarded_ips.include?(client_ips.last)
|
||||||
|
# We don't know which came from the proxy, and which from the user
|
||||||
|
raise IpSpoofAttackError, "IP spoofing attack?! " \
|
||||||
|
"HTTP_CLIENT_IP=#{@req.client_ip.inspect} " \
|
||||||
|
"HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# NOTE: Mastodon addition to make sure we don't get requests from a non-trusted client
|
||||||
|
if @check_ip && (forwarded_ips.last || client_ips.last) && !@proxies.any? { |proxy| proxy === remote_addr }
|
||||||
|
raise IpSpoofAttackError, "IP spoofing attack?! client #{remote_addr} is not a trusted proxy " \
|
||||||
|
"HTTP_CLIENT_IP=#{@req.client_ip.inspect} " \
|
||||||
|
"HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# We assume these things about the IP headers:
|
||||||
|
#
|
||||||
|
# - X-Forwarded-For will be a list of IPs, one per proxy, or blank
|
||||||
|
# - Client-Ip is propagated from the outermost proxy, or is blank
|
||||||
|
# - REMOTE_ADDR will be the IP that made the request to Rack
|
||||||
|
ips = forwarded_ips + client_ips
|
||||||
|
ips.compact!
|
||||||
|
|
||||||
|
# If every single IP option is in the trusted list, return the IP that's
|
||||||
|
# furthest away
|
||||||
|
filter_proxies([remote_addr] + ips).first || ips.last || remote_addr
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ActionDispatch::RemoteIp::GetIp.prepend(ActionDispatch::RemoteIp::GetIpExtensions)
|
||||||
|
|
||||||
|
# rubocop:enable all
|
@ -234,7 +234,7 @@ module Mastodon
|
|||||||
users = User.where(id: row['ids'].split(',')).sort_by(&:updated_at).reverse
|
users = User.where(id: row['ids'].split(',')).sort_by(&:updated_at).reverse
|
||||||
ref_user = users.shift
|
ref_user = users.shift
|
||||||
@prompt.warn "Multiple users registered with e-mail address #{ref_user.email}."
|
@prompt.warn "Multiple users registered with e-mail address #{ref_user.email}."
|
||||||
@prompt.warn "e-mail will be disabled for the following accounts: #{user.map(&:account).map(&:acct).join(', ')}"
|
@prompt.warn "e-mail will be disabled for the following accounts: #{users.map(&:account).map(&:acct).join(', ')}"
|
||||||
@prompt.warn 'Please reach out to them and set another address with `tootctl account modify` or delete them.'
|
@prompt.warn 'Please reach out to them and set another address with `tootctl account modify` or delete them.'
|
||||||
|
|
||||||
i = 0
|
i = 0
|
||||||
|
@ -143,7 +143,7 @@ module Mastodon
|
|||||||
|
|
||||||
model_name = path_segments.first.classify
|
model_name = path_segments.first.classify
|
||||||
attachment_name = path_segments[1].singularize
|
attachment_name = path_segments[1].singularize
|
||||||
record_id = path_segments[2..-2].join.to_i
|
record_id = path_segments[2...-2].join.to_i
|
||||||
file_name = path_segments.last
|
file_name = path_segments.last
|
||||||
record = record_map.dig(model_name, record_id)
|
record = record_map.dig(model_name, record_id)
|
||||||
attachment = record&.public_send(attachment_name)
|
attachment = record&.public_send(attachment_name)
|
||||||
@ -186,7 +186,7 @@ module Mastodon
|
|||||||
end
|
end
|
||||||
|
|
||||||
model_name = path_segments.first.classify
|
model_name = path_segments.first.classify
|
||||||
record_id = path_segments[2..-2].join.to_i
|
record_id = path_segments[2...-2].join.to_i
|
||||||
attachment_name = path_segments[1].singularize
|
attachment_name = path_segments[1].singularize
|
||||||
file_name = path_segments.last
|
file_name = path_segments.last
|
||||||
|
|
||||||
@ -322,7 +322,7 @@ module Mastodon
|
|||||||
end
|
end
|
||||||
|
|
||||||
model_name = path_segments.first.classify
|
model_name = path_segments.first.classify
|
||||||
record_id = path_segments[2..-2].join.to_i
|
record_id = path_segments[2...-2].join.to_i
|
||||||
|
|
||||||
unless PRELOAD_MODEL_WHITELIST.include?(model_name)
|
unless PRELOAD_MODEL_WHITELIST.include?(model_name)
|
||||||
say("Cannot find corresponding model: #{model_name}", :red)
|
say("Cannot find corresponding model: #{model_name}", :red)
|
||||||
@ -372,7 +372,7 @@ module Mastodon
|
|||||||
next unless [7, 10].include?(segments.size)
|
next unless [7, 10].include?(segments.size)
|
||||||
|
|
||||||
model_name = segments.first.classify
|
model_name = segments.first.classify
|
||||||
record_id = segments[2..-2].join.to_i
|
record_id = segments[2...-2].join.to_i
|
||||||
|
|
||||||
next unless PRELOAD_MODEL_WHITELIST.include?(model_name)
|
next unless PRELOAD_MODEL_WHITELIST.include?(model_name)
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ class Mastodon::SidekiqMiddleware
|
|||||||
rescue Mastodon::HostValidationError
|
rescue Mastodon::HostValidationError
|
||||||
# Do not retry
|
# Do not retry
|
||||||
rescue => e
|
rescue => e
|
||||||
|
clean_up_elasticsearch_connections!
|
||||||
limit_backtrace_and_raise(e)
|
limit_backtrace_and_raise(e)
|
||||||
ensure
|
ensure
|
||||||
clean_up_sockets!
|
clean_up_sockets!
|
||||||
@ -25,6 +26,32 @@ class Mastodon::SidekiqMiddleware
|
|||||||
clean_up_statsd_socket!
|
clean_up_statsd_socket!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# This is a hack to immediately free up unused Elasticsearch connections.
|
||||||
|
#
|
||||||
|
# Indeed, Chewy creates one `Elasticsearch::Client` instance per thread,
|
||||||
|
# and each such client manages its long-lasting connection to
|
||||||
|
# Elasticsearch.
|
||||||
|
#
|
||||||
|
# As far as I know, neither `chewy`, `elasticsearch-transport` or even
|
||||||
|
# `faraday` provide a reliable way to immediately close a connection, and
|
||||||
|
# rely on the underlying object to be garbage-collected instead.
|
||||||
|
#
|
||||||
|
# Furthermore, `sidekiq` creates a new thread each time a job throws an
|
||||||
|
# exception, meaning that each failure will create a new connection, and
|
||||||
|
# the old one will only be closed on full garbage collection.
|
||||||
|
def clean_up_elasticsearch_connections!
|
||||||
|
return unless Chewy.enabled? && Chewy.current[:chewy_client].present?
|
||||||
|
|
||||||
|
Chewy.client.transport.connections.each do |connection|
|
||||||
|
# NOTE: This bit of code is tailored for the HTTPClient Faraday adapter
|
||||||
|
connection.connection.app.instance_variable_get(:@client)&.reset_all
|
||||||
|
end
|
||||||
|
|
||||||
|
Chewy.current.delete(:chewy_client)
|
||||||
|
rescue
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
def clean_up_redis_socket!
|
def clean_up_redis_socket!
|
||||||
RedisConfiguration.pool.checkin if Thread.current[:redis]
|
RedisConfiguration.pool.checkin if Thread.current[:redis]
|
||||||
Thread.current[:redis] = nil
|
Thread.current[:redis] = nil
|
||||||
|
@ -13,7 +13,7 @@ module Mastodon
|
|||||||
end
|
end
|
||||||
|
|
||||||
def patch
|
def patch
|
||||||
15
|
18
|
||||||
end
|
end
|
||||||
|
|
||||||
def flags
|
def flags
|
||||||
|
@ -35,7 +35,7 @@ module Paperclip
|
|||||||
dst.binmode
|
dst.binmode
|
||||||
|
|
||||||
begin
|
begin
|
||||||
command = Terrapin::CommandLine.new('ffmpeg', '-i :source -loglevel :loglevel -y :destination', logger: Paperclip.logger)
|
command = Terrapin::CommandLine.new(Rails.configuration.x.ffmpeg_binary, '-i :source -loglevel :loglevel -y :destination', logger: Paperclip.logger)
|
||||||
command.run(source: @file.path, destination: dst.path, loglevel: 'fatal')
|
command.run(source: @file.path, destination: dst.path, loglevel: 'fatal')
|
||||||
rescue Terrapin::ExitStatusError
|
rescue Terrapin::ExitStatusError
|
||||||
dst.close(true)
|
dst.close(true)
|
||||||
|
@ -51,7 +51,7 @@ module Paperclip
|
|||||||
command_arguments, interpolations = prepare_command(destination)
|
command_arguments, interpolations = prepare_command(destination)
|
||||||
|
|
||||||
begin
|
begin
|
||||||
command = Terrapin::CommandLine.new('ffmpeg', command_arguments.join(' '), logger: Paperclip.logger)
|
command = Terrapin::CommandLine.new(Rails.configuration.x.ffmpeg_binary, command_arguments.join(' '), logger: Paperclip.logger)
|
||||||
command.run(interpolations)
|
command.run(interpolations)
|
||||||
rescue Terrapin::ExitStatusError => e
|
rescue Terrapin::ExitStatusError => e
|
||||||
raise Paperclip::Error, "Error while transcoding #{@basename}: #{e}"
|
raise Paperclip::Error, "Error while transcoding #{@basename}: #{e}"
|
||||||
|
@ -516,6 +516,7 @@ namespace :mastodon do
|
|||||||
owner_role = UserRole.find_by(name: 'Owner')
|
owner_role = UserRole.find_by(name: 'Owner')
|
||||||
user = User.new(email: email, password: password, confirmed_at: Time.now.utc, account_attributes: { username: username }, bypass_invite_request_check: true, role: owner_role)
|
user = User.new(email: email, password: password, confirmed_at: Time.now.utc, account_attributes: { username: username }, bypass_invite_request_check: true, role: owner_role)
|
||||||
user.save(validate: false)
|
user.save(validate: false)
|
||||||
|
user.approve!
|
||||||
|
|
||||||
Setting.site_contact_username = username
|
Setting.site_contact_username = username
|
||||||
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe Rack::Attack do
|
describe Rack::Attack, type: :request do
|
||||||
include Rack::Test::Methods
|
|
||||||
|
|
||||||
def app
|
def app
|
||||||
Rails.application
|
Rails.application
|
||||||
end
|
end
|
||||||
@ -12,7 +10,7 @@ describe Rack::Attack do
|
|||||||
it 'does not change the request status' do
|
it 'does not change the request status' do
|
||||||
limit.times do
|
limit.times do
|
||||||
request.call
|
request.call
|
||||||
expect(last_response.status).to_not eq(429)
|
expect(response.status).to_not eq(429)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -21,7 +19,7 @@ describe Rack::Attack do
|
|||||||
it 'returns http too many requests' do
|
it 'returns http too many requests' do
|
||||||
(limit * 2).times do |i|
|
(limit * 2).times do |i|
|
||||||
request.call
|
request.call
|
||||||
expect(last_response.status).to eq(429) if i > limit
|
expect(response.status).to eq(429) if i > limit
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -32,7 +30,7 @@ describe Rack::Attack do
|
|||||||
describe 'throttle excessive sign-up requests by IP address' do
|
describe 'throttle excessive sign-up requests by IP address' do
|
||||||
context 'through the website' do
|
context 'through the website' do
|
||||||
let(:limit) { 25 }
|
let(:limit) { 25 }
|
||||||
let(:request) { ->() { post path, {}, 'REMOTE_ADDR' => remote_ip } }
|
let(:request) { ->() { post path, headers: { 'REMOTE_ADDR' => remote_ip } } }
|
||||||
|
|
||||||
context 'for exact path' do
|
context 'for exact path' do
|
||||||
let(:path) { '/auth' }
|
let(:path) { '/auth' }
|
||||||
@ -47,7 +45,7 @@ describe Rack::Attack do
|
|||||||
|
|
||||||
context 'through the API' do
|
context 'through the API' do
|
||||||
let(:limit) { 5 }
|
let(:limit) { 5 }
|
||||||
let(:request) { ->() { post path, {}, 'REMOTE_ADDR' => remote_ip } }
|
let(:request) { ->() { post path, headers: { 'REMOTE_ADDR' => remote_ip } } }
|
||||||
|
|
||||||
context 'for exact path' do
|
context 'for exact path' do
|
||||||
let(:path) { '/api/v1/accounts' }
|
let(:path) { '/api/v1/accounts' }
|
||||||
@ -59,7 +57,7 @@ describe Rack::Attack do
|
|||||||
|
|
||||||
it 'returns http not found' do
|
it 'returns http not found' do
|
||||||
request.call
|
request.call
|
||||||
expect(last_response.status).to eq(404)
|
expect(response.status).to eq(404)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -67,7 +65,7 @@ describe Rack::Attack do
|
|||||||
|
|
||||||
describe 'throttle excessive sign-in requests by IP address' do
|
describe 'throttle excessive sign-in requests by IP address' do
|
||||||
let(:limit) { 25 }
|
let(:limit) { 25 }
|
||||||
let(:request) { ->() { post path, {}, 'REMOTE_ADDR' => remote_ip } }
|
let(:request) { ->() { post path, headers: { 'REMOTE_ADDR' => remote_ip } } }
|
||||||
|
|
||||||
context 'for exact path' do
|
context 'for exact path' do
|
||||||
let(:path) { '/auth/sign_in' }
|
let(:path) { '/auth/sign_in' }
|
||||||
@ -79,4 +77,28 @@ describe Rack::Attack do
|
|||||||
it_behaves_like 'throttled endpoint'
|
it_behaves_like 'throttled endpoint'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'throttle excessive password change requests by account' do
|
||||||
|
let(:user) { Fabricate(:user, email: 'user@host.example') }
|
||||||
|
let(:limit) { 10 }
|
||||||
|
let(:period) { 10.minutes }
|
||||||
|
let(:request) { -> { put path, headers: { 'REMOTE_ADDR' => remote_ip } } }
|
||||||
|
let(:path) { '/auth' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in user, scope: :user
|
||||||
|
|
||||||
|
# Unfortunately, devise's `sign_in` helper causes the `session` to be
|
||||||
|
# loaded in the next request regardless of whether it's actually accessed
|
||||||
|
# by the client code.
|
||||||
|
#
|
||||||
|
# So, we make an extra query to clear issue a session cookie instead.
|
||||||
|
#
|
||||||
|
# A less resource-intensive way to deal with that would be to generate the
|
||||||
|
# session cookie manually, but this seems pretty involved.
|
||||||
|
get '/'
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'throttled endpoint'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -39,6 +39,21 @@ describe Admin::StatusesController do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'GET #show' do
|
||||||
|
before do
|
||||||
|
status.media_attachments << Fabricate(:media_attachment, type: :image, account: status.account)
|
||||||
|
status.save!
|
||||||
|
status.snapshot!(at_time: status.created_at, rate_limit: false)
|
||||||
|
status.update!(text: 'Hello, this is an edited post')
|
||||||
|
status.snapshot!(rate_limit: false)
|
||||||
|
get :show, params: { account_id: account.id, id: status.id }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'POST #batch' do
|
describe 'POST #batch' do
|
||||||
subject { post :batch, params: { :account_id => account.id, action => '', :admin_status_batch_action => { status_ids: status_ids } } }
|
subject { post :batch, params: { :account_id => account.id, action => '', :admin_status_batch_action => { status_ids: status_ids } } }
|
||||||
|
|
||||||
|
@ -16,6 +16,8 @@ RSpec.describe Api::V1::Admin::DomainBlocksController, type: :controller do
|
|||||||
let(:scopes) { wrong_scope }
|
let(:scopes) { wrong_scope }
|
||||||
|
|
||||||
it 'returns http forbidden' do
|
it 'returns http forbidden' do
|
||||||
|
subject
|
||||||
|
|
||||||
expect(response).to have_http_status(403)
|
expect(response).to have_http_status(403)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -24,6 +26,8 @@ RSpec.describe Api::V1::Admin::DomainBlocksController, type: :controller do
|
|||||||
let(:role) { UserRole.find_by(name: wrong_role) }
|
let(:role) { UserRole.find_by(name: wrong_role) }
|
||||||
|
|
||||||
it 'returns http forbidden' do
|
it 'returns http forbidden' do
|
||||||
|
subject
|
||||||
|
|
||||||
expect(response).to have_http_status(403)
|
expect(response).to have_http_status(403)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -140,39 +144,70 @@ RSpec.describe Api::V1::Admin::DomainBlocksController, type: :controller do
|
|||||||
|
|
||||||
describe 'POST #create' do
|
describe 'POST #create' do
|
||||||
let(:existing_block_domain) { 'example.com' }
|
let(:existing_block_domain) { 'example.com' }
|
||||||
|
let(:params) { { domain: 'foo.bar.com', severity: :silence } }
|
||||||
let!(:block) { Fabricate(:domain_block, domain: existing_block_domain, severity: :suspend) }
|
let!(:block) { Fabricate(:domain_block, domain: existing_block_domain, severity: :suspend) }
|
||||||
|
|
||||||
before do
|
subject do
|
||||||
post :create, params: { domain: 'foo.bar.com', severity: :silence }
|
post :create, params: params
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
|
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
|
||||||
it_behaves_like 'forbidden for wrong role', ''
|
it_behaves_like 'forbidden for wrong role', ''
|
||||||
it_behaves_like 'forbidden for wrong role', 'Moderator'
|
it_behaves_like 'forbidden for wrong role', 'Moderator'
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'creates a domain block and returns expected domain name', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
expect(body_as_json[:domain]).to eq 'foo.bar.com'
|
||||||
|
|
||||||
it 'returns expected domain name' do
|
|
||||||
json = body_as_json
|
|
||||||
expect(json[:domain]).to eq 'foo.bar.com'
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates a domain block' do
|
|
||||||
expect(DomainBlock.find_by(domain: 'foo.bar.com')).to_not be_nil
|
expect(DomainBlock.find_by(domain: 'foo.bar.com')).to_not be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when a stricter domain block already exists' do
|
context 'when a looser domain block already exists on a higher level domain' do
|
||||||
let(:existing_block_domain) { 'bar.com' }
|
let(:params) { { domain: 'foo.bar.com', severity: :suspend } }
|
||||||
|
|
||||||
it 'returns http unprocessable entity' do
|
before do
|
||||||
expect(response).to have_http_status(422)
|
Fabricate(:domain_block, domain: 'bar.com', severity: :silence)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'renders existing domain block in error' do
|
it 'creates a domain block with the expected domain name and severity', :aggregate_failures do
|
||||||
json = body_as_json
|
subject
|
||||||
expect(json[:existing_domain_block][:domain]).to eq existing_block_domain
|
|
||||||
|
body = body_as_json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(body).to match a_hash_including(
|
||||||
|
{
|
||||||
|
domain: 'foo.bar.com',
|
||||||
|
severity: 'suspend',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(DomainBlock.find_by(domain: 'foo.bar.com')).to be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a domain block already exists on the same domain' do
|
||||||
|
before do
|
||||||
|
Fabricate(:domain_block, domain: 'foo.bar.com', severity: :silence)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns existing domain block in error', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(422)
|
||||||
|
expect(body_as_json[:existing_domain_block][:domain]).to eq('foo.bar.com')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a stricter domain block already exists on a higher level domain' do
|
||||||
|
let(:existing_block_domain) { 'bar.com' }
|
||||||
|
|
||||||
|
it 'returns http unprocessable entity with existing domain block in error', :aggregate_reblogs do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(422)
|
||||||
|
expect(body_as_json[:existing_domain_block][:domain]).to eq existing_block_domain
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Api::V1::FeaturedTags::SuggestionsController do
|
||||||
|
render_views
|
||||||
|
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
|
||||||
|
let(:account) { Fabricate(:account, user: user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(controller).to receive(:doorkeeper_token) { token }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET #index' do
|
||||||
|
let!(:unused_featured_tag) { Fabricate(:tag, name: 'unused_featured_tag') }
|
||||||
|
let!(:used_tag) { Fabricate(:tag, name: 'used_tag') }
|
||||||
|
let!(:used_featured_tag) { Fabricate(:tag, name: 'used_featured_tag') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
_unused_tag = Fabricate(:tag, name: 'unused_tag')
|
||||||
|
|
||||||
|
# Make relevant tags used by account
|
||||||
|
status = Fabricate(:status, account: account)
|
||||||
|
status.tags << used_tag
|
||||||
|
status.tags << used_featured_tag
|
||||||
|
|
||||||
|
# Feature the relevant tags
|
||||||
|
Fabricate :featured_tag, account: account, name: unused_featured_tag.name
|
||||||
|
Fabricate :featured_tag, account: account, name: used_featured_tag.name
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http success and recently used but not featured tags', :aggregate_failures do
|
||||||
|
get :index, params: { account_id: account.id, limit: 2 }
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(200)
|
||||||
|
expect(body_as_json)
|
||||||
|
.to contain_exactly(
|
||||||
|
include(name: used_tag.name)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,34 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Api::V1::ScheduledStatusesController do
|
||||||
|
render_views
|
||||||
|
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:statuses') }
|
||||||
|
let(:account) { Fabricate(:account) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(controller).to receive(:doorkeeper_token) { token }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an application token' do
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: 'read:statuses') }
|
||||||
|
|
||||||
|
it 'returns http unprocessable entity' do
|
||||||
|
get :index
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(422)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET #index' do
|
||||||
|
it 'returns http success' do
|
||||||
|
get :index
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,53 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Api::V1::Statuses::TranslationsController do
|
||||||
|
render_views
|
||||||
|
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:statuses', application: app) }
|
||||||
|
|
||||||
|
context 'with an application token' do
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: 'read:statuses', application: app) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(controller).to receive(:doorkeeper_token) { token }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST /api/v1/statuses/:status_id/translate' do
|
||||||
|
let(:status) { Fabricate(:status, account: user.account, text: 'Hola', language: 'es') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
post :create, params: { status_id: status.id }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http unprocessable entity' do
|
||||||
|
expect(response).to have_http_status(422)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an oauth token' do
|
||||||
|
before do
|
||||||
|
allow(controller).to receive(:doorkeeper_token) { token }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST #create' do
|
||||||
|
let(:status) { Fabricate(:status, account: user.account, text: 'Hola', language: 'es') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
translation = TranslationService::Translation.new(text: 'Hello')
|
||||||
|
service = instance_double(TranslationService::DeepL, translate: translation)
|
||||||
|
allow(TranslationService).to receive_messages(configured?: true, configured: service)
|
||||||
|
Rails.cache.write('translation_service/languages', { 'es' => ['en'] })
|
||||||
|
post :create, params: { status_id: status.id }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -163,6 +163,46 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
|
|||||||
expect(response.headers['X-RateLimit-Remaining']).to eq '0'
|
expect(response.headers['X-RateLimit-Remaining']).to eq '0'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with missing thread' do
|
||||||
|
subject { post :create, params: params }
|
||||||
|
|
||||||
|
let(:params) { { status: 'Hello world', in_reply_to_id: 0 } }
|
||||||
|
|
||||||
|
it 'returns http not found' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when scheduling a status' do
|
||||||
|
subject { post :create, params: params }
|
||||||
|
|
||||||
|
let(:params) { { status: 'Hello world', scheduled_at: 10.minutes.from_now } }
|
||||||
|
let(:account) { user.account }
|
||||||
|
|
||||||
|
it 'returns HTTP 200' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a scheduled status' do
|
||||||
|
expect { subject }.to change { account.scheduled_statuses.count }.from(0).to(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the scheduling time is less than 5 minutes' do
|
||||||
|
let(:params) { { status: 'Hello world', scheduled_at: 4.minutes.from_now } }
|
||||||
|
|
||||||
|
it 'does not create a scheduled status', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(422)
|
||||||
|
expect(account.scheduled_statuses).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'DELETE #destroy' do
|
describe 'DELETE #destroy' do
|
||||||
|
@ -12,7 +12,7 @@ describe Api::V1::Timelines::PublicController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'with a user context' do
|
context 'with a user context' do
|
||||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id) }
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:statuses') }
|
||||||
|
|
||||||
describe 'GET #show' do
|
describe 'GET #show' do
|
||||||
before do
|
before do
|
||||||
@ -42,7 +42,7 @@ describe Api::V1::Timelines::PublicController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'without a user context' do
|
context 'without a user context' do
|
||||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil) }
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: 'read:statuses') }
|
||||||
|
|
||||||
describe 'GET #show' do
|
describe 'GET #show' do
|
||||||
it 'returns http success' do
|
it 'returns http success' do
|
||||||
|
@ -6,7 +6,8 @@ describe Api::V1::Timelines::TagController do
|
|||||||
render_views
|
render_views
|
||||||
|
|
||||||
let(:user) { Fabricate(:user) }
|
let(:user) { Fabricate(:user) }
|
||||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:statuses') }
|
let(:scopes) { 'read:statuses' }
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(controller).to receive(:doorkeeper_token) { token }
|
allow(controller).to receive(:doorkeeper_token) { token }
|
||||||
@ -48,13 +49,23 @@ describe Api::V1::Timelines::TagController do
|
|||||||
Form::AdminSettings.new(timeline_preview: false).save
|
Form::AdminSettings.new(timeline_preview: false).save
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the user is not authenticated' do
|
context 'without an access token' do
|
||||||
let(:token) { nil }
|
let(:token) { nil }
|
||||||
|
|
||||||
it 'returns http unauthorized' do
|
it 'returns http unprocessable entity' do
|
||||||
subject
|
subject
|
||||||
|
|
||||||
expect(response).to have_http_status(401)
|
expect(response).to have_http_status(422)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an application access token, not bound to a user' do
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: scopes) }
|
||||||
|
|
||||||
|
it 'returns http unprocessable entity' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(422)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -242,37 +242,4 @@ describe ApplicationController, type: :controller do
|
|||||||
|
|
||||||
include_examples 'respond_with_error', 422
|
include_examples 'respond_with_error', 422
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'cache_collection' do
|
|
||||||
class C < ApplicationController
|
|
||||||
public :cache_collection
|
|
||||||
end
|
|
||||||
|
|
||||||
shared_examples 'receives :with_includes' do |fabricator, klass|
|
|
||||||
it 'uses raw if it is not an ActiveRecord::Relation' do
|
|
||||||
record = Fabricate(fabricator)
|
|
||||||
expect(C.new.cache_collection([record], klass)).to eq [record]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
shared_examples 'cacheable' do |fabricator, klass|
|
|
||||||
include_examples 'receives :with_includes', fabricator, klass
|
|
||||||
|
|
||||||
it 'calls cache_ids of raw if it is an ActiveRecord::Relation' do
|
|
||||||
record = Fabricate(fabricator)
|
|
||||||
relation = klass.none
|
|
||||||
allow(relation).to receive(:cache_ids).and_return([record])
|
|
||||||
expect(C.new.cache_collection(relation, klass)).to eq [record]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns raw unless class responds to :with_includes' do
|
|
||||||
raw = Object.new
|
|
||||||
expect(C.new.cache_collection(raw, Object)).to eq raw
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'Status' do
|
|
||||||
include_examples 'cacheable', :status, Status
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -45,9 +45,11 @@ describe Oauth::AuthorizedApplicationsController do
|
|||||||
let!(:application) { Fabricate(:application) }
|
let!(:application) { Fabricate(:application) }
|
||||||
let!(:access_token) { Fabricate(:accessible_access_token, application: application, resource_owner_id: user.id) }
|
let!(:access_token) { Fabricate(:accessible_access_token, application: application, resource_owner_id: user.id) }
|
||||||
let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) }
|
let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) }
|
||||||
|
let(:redis_pipeline_stub) { instance_double(Redis::Namespace, publish: nil) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
sign_in user, scope: :user
|
sign_in user, scope: :user
|
||||||
|
allow(redis).to receive(:pipelined).and_yield(redis_pipeline_stub)
|
||||||
post :destroy, params: { id: application.id }
|
post :destroy, params: { id: application.id }
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -58,5 +60,13 @@ describe Oauth::AuthorizedApplicationsController do
|
|||||||
it 'removes subscriptions for the application\'s access tokens' do
|
it 'removes subscriptions for the application\'s access tokens' do
|
||||||
expect(Web::PushSubscription.where(user: user).count).to eq 0
|
expect(Web::PushSubscription.where(user: user).count).to eq 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'removes the web_push_subscription' do
|
||||||
|
expect { web_push_subscription.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a session kill payload to the streaming server' do
|
||||||
|
expect(redis_pipeline_stub).to have_received(:publish).with("timeline:access_token:#{access_token.id}", '{"event":"kill"}')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -160,7 +160,11 @@ describe Settings::ApplicationsController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe 'destroy' do
|
describe 'destroy' do
|
||||||
|
let(:redis_pipeline_stub) { instance_double(Redis::Namespace, publish: nil) }
|
||||||
|
let!(:access_token) { Fabricate(:accessible_access_token, application: app) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
allow(redis).to receive(:pipelined).and_yield(redis_pipeline_stub)
|
||||||
post :destroy, params: { id: app.id }
|
post :destroy, params: { id: app.id }
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -171,6 +175,10 @@ describe Settings::ApplicationsController do
|
|||||||
it 'removes the app' do
|
it 'removes the app' do
|
||||||
expect(Doorkeeper::Application.find_by(id: app.id)).to be_nil
|
expect(Doorkeeper::Application.find_by(id: app.id)).to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'sends a session kill payload to the streaming server' do
|
||||||
|
expect(redis_pipeline_stub).to have_received(:publish).with("timeline:access_token:#{access_token.id}", '{"event":"kill"}')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'regenerate' do
|
describe 'regenerate' do
|
||||||
|
17
spec/fixtures/requests/low_confidence_latin1.txt
vendored
Normal file
17
spec/fixtures/requests/low_confidence_latin1.txt
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
HTTP/1.1 200 OK
|
||||||
|
server: nginx
|
||||||
|
date: Thu, 13 Jun 2024 14:33:13 GMT
|
||||||
|
content-type: text/html; charset=ISO-8859-1
|
||||||
|
content-length: 158
|
||||||
|
accept-ranges: bytes
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Tofu á l'orange</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Tofu á l'orange</h2>
|
||||||
|
</body>
|
||||||
|
</html>
|
50
spec/lib/activitypub/parser/status_parser_spec.rb
Normal file
50
spec/lib/activitypub/parser/status_parser_spec.rb
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe ActivityPub::Parser::StatusParser do
|
||||||
|
subject { described_class.new(json) }
|
||||||
|
|
||||||
|
let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor') }
|
||||||
|
let(:follower) { Fabricate(:account, username: 'bob') }
|
||||||
|
|
||||||
|
let(:json) do
|
||||||
|
{
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#foo'].join,
|
||||||
|
type: 'Create',
|
||||||
|
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||||
|
object: object_json,
|
||||||
|
}.with_indifferent_access
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), 'post1'].join('/'),
|
||||||
|
type: 'Note',
|
||||||
|
to: [
|
||||||
|
'https://www.w3.org/ns/activitystreams#Public',
|
||||||
|
ActivityPub::TagManager.instance.uri_for(follower),
|
||||||
|
],
|
||||||
|
content: '@bob lorem ipsum',
|
||||||
|
contentMap: {
|
||||||
|
EN: '@bob lorem ipsum',
|
||||||
|
},
|
||||||
|
published: 1.hour.ago.utc.iso8601,
|
||||||
|
updated: 1.hour.ago.utc.iso8601,
|
||||||
|
tag: {
|
||||||
|
type: 'Mention',
|
||||||
|
href: ActivityPub::TagManager.instance.uri_for(follower),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'correctly parses status' do
|
||||||
|
expect(subject).to have_attributes(
|
||||||
|
text: '@bob lorem ipsum',
|
||||||
|
uri: [ActivityPub::TagManager.instance.uri_for(sender), 'post1'].join('/'),
|
||||||
|
reply: false,
|
||||||
|
language: :en
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
@ -248,6 +248,24 @@ describe AccountInteractions do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#block_idna_domain!' do
|
||||||
|
subject do
|
||||||
|
[
|
||||||
|
account.block_domain!(idna_domain),
|
||||||
|
account.block_domain!(punycode_domain),
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:idna_domain) { '대한민국.한국' }
|
||||||
|
let(:punycode_domain) { 'xn--3e0bs9hfvinn1a.xn--3e0b707e' }
|
||||||
|
|
||||||
|
it 'creates single AccountDomainBlock' do
|
||||||
|
expect do
|
||||||
|
expect(subject).to all(be_a AccountDomainBlock)
|
||||||
|
end.to change { account.domain_blocks.count }.by 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#unfollow!' do
|
describe '#unfollow!' do
|
||||||
subject { account.unfollow!(target_account) }
|
subject { account.unfollow!(target_account) }
|
||||||
|
|
||||||
@ -343,6 +361,28 @@ describe AccountInteractions do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#unblock_idna_domain!' do
|
||||||
|
subject { account.unblock_domain!(punycode_domain) }
|
||||||
|
|
||||||
|
let(:idna_domain) { '대한민국.한국' }
|
||||||
|
let(:punycode_domain) { 'xn--3e0bs9hfvinn1a.xn--3e0b707e' }
|
||||||
|
|
||||||
|
context 'when blocking the domain' do
|
||||||
|
it 'returns destroyed AccountDomainBlock' do
|
||||||
|
account_domain_block = Fabricate(:account_domain_block, domain: idna_domain)
|
||||||
|
account.domain_blocks << account_domain_block
|
||||||
|
expect(subject).to be_a AccountDomainBlock
|
||||||
|
expect(subject).to be_destroyed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when unblocking idna domain' do
|
||||||
|
it 'returns nil' do
|
||||||
|
expect(subject).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#following?' do
|
describe '#following?' do
|
||||||
subject { account.following?(target_account) }
|
subject { account.following?(target_account) }
|
||||||
|
|
||||||
|
@ -25,7 +25,6 @@ RSpec.describe HomeFeed, type: :model do
|
|||||||
results = subject.get(3)
|
results = subject.get(3)
|
||||||
|
|
||||||
expect(results.map(&:id)).to eq [3, 2]
|
expect(results.map(&:id)).to eq [3, 2]
|
||||||
expect(results.first.attributes.keys).to eq %w(id updated_at)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -142,22 +142,12 @@ RSpec.describe Setting, type: :model do
|
|||||||
context 'records includes nothing' do
|
context 'records includes nothing' do
|
||||||
let(:records) { [] }
|
let(:records) { [] }
|
||||||
|
|
||||||
context 'default_value is not a Hash' do
|
|
||||||
it 'includes Setting with value of default_value' do
|
it 'includes Setting with value of default_value' do
|
||||||
setting = described_class.all_as_records[key]
|
setting = described_class.all_as_records[key]
|
||||||
|
|
||||||
expect(setting).to be_kind_of Setting
|
expect(setting).to be_a described_class
|
||||||
expect(setting).to have_attributes(var: key)
|
expect(setting).to have_attributes(var: key)
|
||||||
expect(setting).to have_attributes(value: 'default_value')
|
expect(setting).to have_attributes(value: default_value)
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'default_value is a Hash' do
|
|
||||||
let(:default_value) { { 'foo' => 'fuga' } }
|
|
||||||
|
|
||||||
it 'returns {}' do
|
|
||||||
expect(described_class.all_as_records).to eq({})
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -40,6 +40,12 @@ RSpec.describe User, type: :model do
|
|||||||
expect(user.valid?).to be true
|
expect(user.valid?).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'is valid with a localhost e-mail address' do
|
||||||
|
user = Fabricate.build(:user, email: 'admin@localhost')
|
||||||
|
user.valid?
|
||||||
|
expect(user.valid?).to be true
|
||||||
|
end
|
||||||
|
|
||||||
it 'cleans out empty string from languages' do
|
it 'cleans out empty string from languages' do
|
||||||
user = Fabricate.build(:user, chosen_languages: [''])
|
user = Fabricate.build(:user, chosen_languages: [''])
|
||||||
user.valid?
|
user.valid?
|
||||||
|
@ -41,6 +41,7 @@ RSpec.configure do |config|
|
|||||||
|
|
||||||
config.include Devise::Test::ControllerHelpers, type: :controller
|
config.include Devise::Test::ControllerHelpers, type: :controller
|
||||||
config.include Devise::Test::ControllerHelpers, type: :view
|
config.include Devise::Test::ControllerHelpers, type: :view
|
||||||
|
config.include Devise::Test::IntegrationHelpers, type: :request
|
||||||
config.include Paperclip::Shoulda::Matchers
|
config.include Paperclip::Shoulda::Matchers
|
||||||
config.include ActiveSupport::Testing::TimeHelpers
|
config.include ActiveSupport::Testing::TimeHelpers
|
||||||
config.include Redisable
|
config.include Redisable
|
||||||
|
133
spec/requests/api/v1/timelines/public_spec.rb
Normal file
133
spec/requests/api/v1/timelines/public_spec.rb
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe 'Public' do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:scopes) { 'read:statuses' }
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
|
||||||
|
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
|
||||||
|
|
||||||
|
shared_examples 'forbidden for wrong scope' do |wrong_scope|
|
||||||
|
let(:scopes) { wrong_scope }
|
||||||
|
|
||||||
|
it 'returns http forbidden' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'a successful request to the public timeline' do
|
||||||
|
it 'returns the expected statuses successfully', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(body_as_json.pluck(:id)).to match_array(expected_statuses.map { |status| status.id.to_s })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET /api/v1/timelines/public' do
|
||||||
|
subject do
|
||||||
|
get '/api/v1/timelines/public', headers: headers, params: params
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:private_status) { Fabricate(:status, visibility: :private) } # rubocop:disable RSpec/LetSetup
|
||||||
|
let!(:local_status) { Fabricate(:status, account: Fabricate.build(:account, domain: nil)) }
|
||||||
|
let!(:remote_status) { Fabricate(:status, account: Fabricate.build(:account, domain: 'example.com')) }
|
||||||
|
let!(:media_status) { Fabricate(:status, media_attachments: [Fabricate.build(:media_attachment)]) }
|
||||||
|
|
||||||
|
let(:params) { {} }
|
||||||
|
|
||||||
|
context 'when the instance allows public preview' do
|
||||||
|
let(:expected_statuses) { [local_status, remote_status, media_status] }
|
||||||
|
|
||||||
|
it_behaves_like 'forbidden for wrong scope', 'profile'
|
||||||
|
|
||||||
|
context 'with an authorized user' do
|
||||||
|
it_behaves_like 'a successful request to the public timeline'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an anonymous user' do
|
||||||
|
let(:headers) { {} }
|
||||||
|
|
||||||
|
it_behaves_like 'a successful request to the public timeline'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with local param' do
|
||||||
|
let(:params) { { local: true } }
|
||||||
|
let(:expected_statuses) { [local_status, media_status] }
|
||||||
|
|
||||||
|
it_behaves_like 'a successful request to the public timeline'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with remote param' do
|
||||||
|
let(:params) { { remote: true } }
|
||||||
|
let(:expected_statuses) { [remote_status] }
|
||||||
|
|
||||||
|
it_behaves_like 'a successful request to the public timeline'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with only_media param' do
|
||||||
|
let(:params) { { only_media: true } }
|
||||||
|
let(:expected_statuses) { [media_status] }
|
||||||
|
|
||||||
|
it_behaves_like 'a successful request to the public timeline'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with limit param' do
|
||||||
|
let(:params) { { limit: 1 } }
|
||||||
|
|
||||||
|
it 'returns only the requested number of statuses', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(body_as_json.size).to eq(params[:limit])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets the correct pagination headers', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
headers = response.headers['Link']
|
||||||
|
|
||||||
|
expect(headers.find_link(%w(rel prev)).href).to eq(api_v1_timelines_public_url(limit: 1, min_id: media_status.id.to_s))
|
||||||
|
expect(headers.find_link(%w(rel next)).href).to eq(api_v1_timelines_public_url(limit: 1, max_id: media_status.id.to_s))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the instance does not allow public preview' do
|
||||||
|
before do
|
||||||
|
Form::AdminSettings.new(timeline_preview: false).save
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'forbidden for wrong scope', 'profile'
|
||||||
|
|
||||||
|
context 'without an authentication token' do
|
||||||
|
let(:headers) { {} }
|
||||||
|
|
||||||
|
it 'returns http unprocessable entity' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(422)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an application access token, not bound to a user' do
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: scopes) }
|
||||||
|
|
||||||
|
it 'returns http unprocessable entity' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(422)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an authenticated user' do
|
||||||
|
let(:expected_statuses) { [local_status, remote_status, media_status] }
|
||||||
|
|
||||||
|
it_behaves_like 'a successful request to the public timeline'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -3,7 +3,7 @@ require 'rails_helper'
|
|||||||
RSpec.describe ActivityPub::ProcessAccountService, type: :service do
|
RSpec.describe ActivityPub::ProcessAccountService, type: :service do
|
||||||
subject { described_class.new }
|
subject { described_class.new }
|
||||||
|
|
||||||
context 'property values' do
|
context 'with property values, an avatar, and a profile header' do
|
||||||
let(:payload) do
|
let(:payload) do
|
||||||
{
|
{
|
||||||
id: 'https://foo.test',
|
id: 'https://foo.test',
|
||||||
@ -14,19 +14,50 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do
|
|||||||
{ type: 'PropertyValue', name: 'Occupation', value: 'Unit test' },
|
{ type: 'PropertyValue', name: 'Occupation', value: 'Unit test' },
|
||||||
{ type: 'PropertyValue', name: 'non-string', value: ['foo', 'bar'] },
|
{ type: 'PropertyValue', name: 'non-string', value: ['foo', 'bar'] },
|
||||||
],
|
],
|
||||||
|
image: {
|
||||||
|
type: 'Image',
|
||||||
|
mediaType: 'image/png',
|
||||||
|
url: 'https://foo.test/image.png',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: 'Image',
|
||||||
|
url: [
|
||||||
|
{
|
||||||
|
mediaType: 'image/png',
|
||||||
|
href: 'https://foo.test/icon.png',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'parses out of attachment' do
|
before do
|
||||||
|
stub_request(:get, 'https://foo.test/image.png').to_return(request_fixture('avatar.txt'))
|
||||||
|
stub_request(:get, 'https://foo.test/icon.png').to_return(request_fixture('avatar.txt'))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'parses property values, avatar and profile header as expected' do
|
||||||
account = subject.call('alice', 'example.com', payload)
|
account = subject.call('alice', 'example.com', payload)
|
||||||
expect(account.fields).to be_a Array
|
|
||||||
expect(account.fields.size).to eq 2
|
expect(account.fields)
|
||||||
expect(account.fields[0]).to be_a Account::Field
|
.to be_an(Array)
|
||||||
expect(account.fields[0].name).to eq 'Pronouns'
|
.and have_attributes(size: 2)
|
||||||
expect(account.fields[0].value).to eq 'They/them'
|
expect(account.fields.first)
|
||||||
expect(account.fields[1]).to be_a Account::Field
|
.to be_an(Account::Field)
|
||||||
expect(account.fields[1].name).to eq 'Occupation'
|
.and have_attributes(
|
||||||
expect(account.fields[1].value).to eq 'Unit test'
|
name: eq('Pronouns'),
|
||||||
|
value: eq('They/them')
|
||||||
|
)
|
||||||
|
expect(account.fields.last)
|
||||||
|
.to be_an(Account::Field)
|
||||||
|
.and have_attributes(
|
||||||
|
name: eq('Occupation'),
|
||||||
|
value: eq('Unit test')
|
||||||
|
)
|
||||||
|
expect(account).to have_attributes(
|
||||||
|
avatar_remote_url: 'https://foo.test/icon.png',
|
||||||
|
header_remote_url: 'https://foo.test/image.png'
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ RSpec.describe FetchLinkCardService, type: :service do
|
|||||||
stub_request(:get, 'http://example.com/test?data=file.gpx%5E1').to_return(status: 200)
|
stub_request(:get, 'http://example.com/test?data=file.gpx%5E1').to_return(status: 200)
|
||||||
stub_request(:get, 'http://example.com/test-').to_return(request_fixture('idn.txt'))
|
stub_request(:get, 'http://example.com/test-').to_return(request_fixture('idn.txt'))
|
||||||
stub_request(:get, 'http://example.com/windows-1251').to_return(request_fixture('windows-1251.txt'))
|
stub_request(:get, 'http://example.com/windows-1251').to_return(request_fixture('windows-1251.txt'))
|
||||||
|
stub_request(:get, 'http://example.com/low_confidence_latin1').to_return(request_fixture('low_confidence_latin1.txt'))
|
||||||
|
|
||||||
subject.call(status)
|
subject.call(status)
|
||||||
end
|
end
|
||||||
@ -62,7 +63,15 @@ RSpec.describe FetchLinkCardService, type: :service do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context do
|
context 'with a URL of a page in ISO-8859-1 encoding, that charlock_holmes cannot detect' do
|
||||||
|
let(:status) { Fabricate(:status, text: 'Check out http://example.com/low_confidence_latin1') }
|
||||||
|
|
||||||
|
it 'decodes the HTML' do
|
||||||
|
expect(status.preview_card.title).to eq("Tofu á l'orange")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a Japanese path URL' do
|
||||||
let(:status) { Fabricate(:status, text: 'テストhttp://example.com/日本語') }
|
let(:status) { Fabricate(:status, text: 'テストhttp://example.com/日本語') }
|
||||||
|
|
||||||
it 'works with Japanese path string' do
|
it 'works with Japanese path string' do
|
||||||
@ -95,6 +104,19 @@ RSpec.describe FetchLinkCardService, type: :service do
|
|||||||
expect(a_request(:get, 'http://example.com/test?data=file.gpx%5E1')).to have_been_made.once
|
expect(a_request(:get, 'http://example.com/test?data=file.gpx%5E1')).to have_been_made.once
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with an URL too long for PostgreSQL unique indexes' do
|
||||||
|
let(:url) { "http://example.com/#{'a' * 2674}" }
|
||||||
|
let(:status) { Fabricate(:status, text: url) }
|
||||||
|
|
||||||
|
it 'does not fetch the URL' do
|
||||||
|
expect(a_request(:get, url)).to_not have_been_made
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create a preview card' do
|
||||||
|
expect(status.preview_card).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'in a remote status' do
|
context 'in a remote status' do
|
||||||
|
@ -72,11 +72,11 @@ RSpec.describe NotifyService, type: :service do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'if the message chain is initiated by recipient, but without a mention to the sender, even if the sender sends multiple messages in a row' do
|
context 'when the message chain is initiated by recipient, but without a mention to the sender, even if the sender sends multiple messages in a row' do
|
||||||
let(:reply_to) { Fabricate(:status, account: recipient) }
|
let(:public_status) { Fabricate(:status, account: recipient) }
|
||||||
let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) }
|
let(:intermediate_reply) { Fabricate(:status, account: sender, thread: public_status, visibility: :direct) }
|
||||||
let(:dummy_reply) { Fabricate(:status, account: sender, visibility: :direct, thread: reply_to) }
|
let!(:intermediate_mention) { Fabricate(:mention, account: sender, status: intermediate_reply) }
|
||||||
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: dummy_reply)) }
|
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: intermediate_reply)) }
|
||||||
|
|
||||||
it 'does not notify' do
|
it 'does not notify' do
|
||||||
expect { subject }.to_not change(Notification, :count)
|
expect { subject }.to_not change(Notification, :count)
|
||||||
|
@ -52,6 +52,23 @@ RSpec.describe PostStatusService, type: :service do
|
|||||||
it 'does not change statuses count' do
|
it 'does not change statuses count' do
|
||||||
expect { subject.call(account, text: 'Hi future!', scheduled_at: future, thread: previous_status) }.not_to change { [account.statuses_count, previous_status.replies_count] }
|
expect { subject.call(account, text: 'Hi future!', scheduled_at: future, thread: previous_status) }.not_to change { [account.statuses_count, previous_status.replies_count] }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns existing status when used twice with idempotency key' do
|
||||||
|
account = Fabricate(:account)
|
||||||
|
status1 = subject.call(account, text: 'test', idempotency: 'meepmeep', scheduled_at: future)
|
||||||
|
status2 = subject.call(account, text: 'test', idempotency: 'meepmeep', scheduled_at: future)
|
||||||
|
expect(status2.id).to eq status1.id
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when scheduled_at is less than min offset' do
|
||||||
|
let(:invalid_scheduled_time) { 4.minutes.from_now }
|
||||||
|
|
||||||
|
it 'raises invalid record error' do
|
||||||
|
expect do
|
||||||
|
subject.call(account, text: 'Hi future!', scheduled_at: invalid_scheduled_time)
|
||||||
|
end.to raise_error(ActiveRecord::RecordInvalid)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates response to the original status of boost' do
|
it 'creates response to the original status of boost' do
|
||||||
|
@ -74,6 +74,20 @@ RSpec.describe VerifyLinkService, type: :service do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'when a document is truncated but the link back is valid' do
|
context 'when a document is truncated but the link back is valid' do
|
||||||
|
let(:html) do
|
||||||
|
"
|
||||||
|
<!doctype html>
|
||||||
|
<body>
|
||||||
|
<a rel=\"me\" href=\"#{ActivityPub::TagManager.instance.url_for(account)}\">
|
||||||
|
"
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'marks the field as verified' do
|
||||||
|
expect(field.verified?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a link tag might be truncated' do
|
||||||
let(:html) do
|
let(:html) do
|
||||||
"
|
"
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
@ -87,19 +101,6 @@ RSpec.describe VerifyLinkService, type: :service do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when a link back might be truncated' do
|
|
||||||
let(:html) do
|
|
||||||
"
|
|
||||||
<!doctype html>
|
|
||||||
<body>
|
|
||||||
<a rel=\"me\" href=\"#{ActivityPub::TagManager.instance.url_for(account)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not mark the field as verified' do
|
|
||||||
expect(field.verified?).to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when a link does not contain a link back' do
|
context 'when a link does not contain a link back' do
|
||||||
let(:html) { '' }
|
let(:html) { '' }
|
||||||
|
|
||||||
|
@ -30,6 +30,12 @@ RSpec.configure do |config|
|
|||||||
config.before :suite do
|
config.before :suite do
|
||||||
Rails.application.load_seed
|
Rails.application.load_seed
|
||||||
Chewy.strategy(:bypass)
|
Chewy.strategy(:bypass)
|
||||||
|
|
||||||
|
# NOTE: we switched registrations mode to closed by default, but the specs
|
||||||
|
# very heavily rely on having it enabled by default, as it relies on users
|
||||||
|
# being approved by default except in select cases where explicitly testing
|
||||||
|
# other registration modes
|
||||||
|
Setting.registrations_mode = 'open'
|
||||||
end
|
end
|
||||||
|
|
||||||
config.after :suite do
|
config.after :suite do
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Scheduler::AutoCloseRegistrationsScheduler do
|
||||||
|
subject { described_class.new }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
let(:moderator_activity_date) { Time.now.utc }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Fabricate(:user, role: UserRole.find_by(name: 'Owner'), current_sign_in_at: 10.years.ago)
|
||||||
|
Fabricate(:user, role: UserRole.find_by(name: 'Moderator'), current_sign_in_at: moderator_activity_date)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when registrations are open' do
|
||||||
|
before do
|
||||||
|
Setting.registrations_mode = 'open'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a moderator has logged in recently' do
|
||||||
|
let(:moderator_activity_date) { Time.now.utc }
|
||||||
|
|
||||||
|
it 'does not change registrations mode' do
|
||||||
|
expect { subject.perform }.to_not change(Setting, :registrations_mode)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a moderator has not recently signed in' do
|
||||||
|
let(:moderator_activity_date) { 1.year.ago }
|
||||||
|
|
||||||
|
it 'changes registrations mode from open to approved' do
|
||||||
|
expect { subject.perform }.to change(Setting, :registrations_mode).from('open').to('approved')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when registrations are closed' do
|
||||||
|
before do
|
||||||
|
Setting.registrations_mode = 'none'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a moderator has logged in recently' do
|
||||||
|
let(:moderator_activity_date) { Time.now.utc }
|
||||||
|
|
||||||
|
it 'does not change registrations mode' do
|
||||||
|
expect { subject.perform }.to_not change(Setting, :registrations_mode)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a moderator has not recently signed in' do
|
||||||
|
let(:moderator_activity_date) { 1.year.ago }
|
||||||
|
|
||||||
|
it 'does not change registrations mode' do
|
||||||
|
expect { subject.perform }.to_not change(Setting, :registrations_mode)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user