Merge tag 'v3.0.0' into hometown-dev

This commit is contained in:
Darius Kazemi 2019-10-08 13:24:20 -07:00
commit 228e0f0f6e
1012 changed files with 31176 additions and 15165 deletions

View File

@ -3,7 +3,7 @@ version: 2
aliases:
- &defaults
docker:
- image: circleci/ruby:2.6.0-stretch-node
- image: circleci/ruby:2.6-stretch-node
environment: &ruby_environment
BUNDLE_APP_CONFIG: ./.bundle/
DB_HOST: localhost
@ -105,14 +105,14 @@ jobs:
install-ruby2.5:
<<: *defaults
docker:
- image: circleci/ruby:2.5.3-stretch-node
- image: circleci/ruby:2.5-stretch-node
environment: *ruby_environment
<<: *install_ruby_dependencies
install-ruby2.4:
<<: *defaults
docker:
- image: circleci/ruby:2.4.5-stretch-node
- image: circleci/ruby:2.4-stretch-node
environment: *ruby_environment
<<: *install_ruby_dependencies
@ -131,40 +131,40 @@ jobs:
test-ruby2.6:
<<: *defaults
docker:
- image: circleci/ruby:2.6.0-stretch-node
- image: circleci/ruby:2.6-stretch-node
environment: *ruby_environment
- image: circleci/postgres:10.6-alpine
environment:
POSTGRES_USER: root
- image: circleci/redis:5.0.3-alpine3.8
- image: circleci/redis:5-alpine
<<: *test_steps
test-ruby2.5:
<<: *defaults
docker:
- image: circleci/ruby:2.5.3-stretch-node
- image: circleci/ruby:2.5-stretch-node
environment: *ruby_environment
- image: circleci/postgres:10.6-alpine
environment:
POSTGRES_USER: root
- image: circleci/redis:4.0.12-alpine
- image: circleci/redis:5-alpine
<<: *test_steps
test-ruby2.4:
<<: *defaults
docker:
- image: circleci/ruby:2.4.5-stretch-node
- image: circleci/ruby:2.4-stretch-node
environment: *ruby_environment
- image: circleci/postgres:10.6-alpine
environment:
POSTGRES_USER: root
- image: circleci/redis:4.0.12-alpine
- image: circleci/redis:5-alpine
<<: *test_steps
test-webui:
<<: *defaults
docker:
- image: circleci/node:8.15.0-stretch
- image: circleci/node:12.9-stretch
steps:
- *attach_workspace
- run: ./bin/retry yarn test:jest
@ -173,9 +173,11 @@ jobs:
<<: *defaults
steps:
- *attach_workspace
- *install_system_dependencies
- run: bundle exec i18n-tasks check-normalized
- run: bundle exec i18n-tasks unused -l en
- run: bundle exec i18n-tasks check-consistent-interpolations
- run: bundle exec rake repo:check_locales_files
workflows:
version: 2

View File

@ -11,24 +11,14 @@ DB_NAME=gonano
DB_PASS=$DATA_DB_PASS
DB_PORT=5432
DATABASE_URL=postgresql://$DATA_DB_USER:$DATA_DB_PASS@$DATA_DB_HOST/gonano
# DATABASE_URL=postgresql://$DATA_DB_USER:$DATA_DB_PASS@$DATA_DB_HOST/gonano
# Optional ElasticSearch configuration
ES_ENABLED=true
ES_HOST=$DATA_ELASTIC_HOST
ES_PORT=9200
# Optimizations
LD_PRELOAD=/data/lib/libjemalloc.so
# ImageMagick optimizations
MAGICK_TEMPORARY_PATH=/app/tmp
MAGICK_MEMORY_LIMIT=128MiB
MAGICK_MAP_LIMIT=64MiB
MAGICK_TIME_LIMIT=15
MAGICK_AREA_LIMIT=16MP
MAGICK_WIDTH_LIMIT=8KP
MAGICK_HEIGHT_LIMIT=8KP
BIND=0.0.0.0
# Federation
# Note: Changing LOCAL_DOMAIN at a later time will cause unwanted side effects, including breaking all existing federation.
@ -84,6 +74,7 @@ SMTP_PORT=587
SMTP_LOGIN=$SMTP_LOGIN
SMTP_PASSWORD=$SMTP_PASSWORD
SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
#SMTP_REPLY_TO=
#SMTP_DOMAIN= # defaults to LOCAL_DOMAIN
#SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail
#SMTP_AUTH_METHOD=plain
@ -97,9 +88,17 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# PAPERCLIP_ROOT_URL=/system
# Optional asset host for multi-server setups
# The asset host must allow cross origin request from WEB_DOMAIN or LOCAL_DOMAIN
# if WEB_DOMAIN is not set. For example, the server may have the
# following header field:
# Access-Control-Allow-Origin: https://example.com/
# CDN_HOST=https://assets.example.com
# S3 (optional)
# The attachment host must allow cross origin request from WEB_DOMAIN or
# LOCAL_DOMAIN if WEB_DOMAIN is not set. For example, the server may have the
# following header field:
# Access-Control-Allow-Origin: https://192.168.1.123:9000/
# S3_ENABLED=true
# S3_BUCKET=
# AWS_ACCESS_KEY_ID=
@ -109,6 +108,8 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# S3_HOSTNAME=192.168.1.123:9000
# S3 (Minio Config (optional) Please check Minio instance for details)
# The attachment host must allow cross origin request - see the description
# above.
# S3_ENABLED=true
# S3_BUCKET=
# AWS_ACCESS_KEY_ID=
@ -119,12 +120,30 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# S3_ENDPOINT=
# S3_SIGNATURE_VERSION=
# Google Cloud Storage (optional)
# Use S3 compatible API. Since GCS does not support Multipart Upload,
# increase the value of S3_MULTIPART_THRESHOLD to disable Multipart Upload.
# The attachment host must allow cross origin request - see the description
# above.
# S3_ENABLED=true
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# S3_REGION=
# S3_PROTOCOL=https
# S3_HOSTNAME=storage.googleapis.com
# S3_ENDPOINT=https://storage.googleapis.com
# S3_MULTIPART_THRESHOLD=52428801 # 50.megabytes
# Swift (optional)
# The attachment host must allow cross origin request - see the description
# above.
# SWIFT_ENABLED=true
# SWIFT_USERNAME=
# For Keystone V3, the value for SWIFT_TENANT should be the project name
# SWIFT_TENANT=
# SWIFT_PASSWORD=
# Some OpenStack V3 providers require PROJECT_ID (optional)
# SWIFT_PROJECT_ID=
# Keystone V2 and V3 URLs are supported. Use a V3 URL if possible to avoid
# issues with token rate-limiting during high load.
# SWIFT_AUTH_URL=
@ -171,8 +190,8 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# The pam environment variable "email" is provided by:
# https://github.com/devkral/pam_email_extractor
# PAM_ENABLED=true
# Fallback Suffix for email address generation (nil by default)
# PAM_DEFAULT_SUFFIX=pam
# Fallback email domain for email address generation (LOCAL_DOMAIN by default)
# PAM_EMAIL_DOMAIN=example.com
# Name of the pam service (pam "auth" section is evaluated)
# PAM_DEFAULT_SERVICE=rpam
# Name of the pam service used for checking if an user can register (pam "account" section is evaluated) (nil (disabled) by default)
@ -220,7 +239,14 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# SAML_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true
# SAML_ATTRIBUTES_STATEMENTS_UID="urn:oid:0.9.2342.19200300.100.1.1"
# SAML_ATTRIBUTES_STATEMENTS_EMAIL="urn:oid:1.3.6.1.4.1.5923.1.1.1.6"
# SAML_ATTRIBUTES_STATEMENTS_FULL_NAME="urn:oid:2.5.4.42"
# SAML_ATTRIBUTES_STATEMENTS_FULL_NAME="urn:oid:2.16.840.1.113730.3.1.241"
# SAML_ATTRIBUTES_STATEMENTS_FIRST_NAME="urn:oid:2.5.4.42"
# SAML_ATTRIBUTES_STATEMENTS_LAST_NAME="urn:oid:2.5.4.4"
# SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1"
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED=
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL=
# Use HTTP proxy for outgoing request (optional)
# http_proxy=http://gateway.local:8118
# Access control for hidden service.
# ALLOW_ACCESS_TO_HIDDEN_SERVICE=true

View File

@ -69,6 +69,7 @@ SMTP_PORT=587
SMTP_LOGIN=
SMTP_PASSWORD=
SMTP_FROM_ADDRESS=notifications@example.com
#SMTP_REPLY_TO=
#SMTP_DOMAIN= # defaults to LOCAL_DOMAIN
#SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail
#SMTP_AUTH_METHOD=plain
@ -114,6 +115,20 @@ SMTP_FROM_ADDRESS=notifications@example.com
# S3_ENDPOINT=
# S3_SIGNATURE_VERSION=
# Google Cloud Storage (optional)
# Use S3 compatible API. Since GCS does not support Multipart Upload,
# increase the value of S3_MULTIPART_THRESHOLD to disable Multipart Upload.
# The attachment host must allow cross origin request - see the description
# above.
# S3_ENABLED=true
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# S3_REGION=
# S3_PROTOCOL=https
# S3_HOSTNAME=storage.googleapis.com
# S3_ENDPOINT=https://storage.googleapis.com
# S3_MULTIPART_THRESHOLD=52428801 # 50.megabytes
# Swift (optional)
# The attachment host must allow cross origin request - see the description
# above.
@ -163,7 +178,7 @@ STREAMING_CLUSTER_NUM=1
# LDAP_BIND_DN=
# LDAP_PASSWORD=
# LDAP_UID=cn
# LDAP_SEARCH_FILTER="%{uid}=%{email}"
# LDAP_SEARCH_FILTER=%{uid}=%{email}
# PAM authentication (optional)
# PAM authentication uses for the email generation the "email" pam variable

View File

@ -1 +1 @@
2.6.1
2.6.5

View File

@ -3,6 +3,202 @@ Changelog
All notable changes to this project will be documented in this file.
## [3.0.0] - 2019-10-03
### Added
- Add "not available" label to unloaded media attachments in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11715), [Gargron](https://github.com/tootsuite/mastodon/pull/11745))
- **Add profile directory to web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11688), [mayaeh](https://github.com/tootsuite/mastodon/pull/11872))
- Add profile directory opt-in federation
- Add profile directory REST API
- Add special alert for throttled requests in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11677))
- Add confirmation modal when logging out from the web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11671))
- **Add audio player in web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11644), [Gargron](https://github.com/tootsuite/mastodon/pull/11652), [Gargron](https://github.com/tootsuite/mastodon/pull/11654), [ThibG](https://github.com/tootsuite/mastodon/pull/11629), [Gargron](https://github.com/tootsuite/mastodon/pull/12056))
- **Add autosuggestions for hashtags in web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11422), [ThibG](https://github.com/tootsuite/mastodon/pull/11632), [Gargron](https://github.com/tootsuite/mastodon/pull/11764), [Gargron](https://github.com/tootsuite/mastodon/pull/11588), [Gargron](https://github.com/tootsuite/mastodon/pull/11442))
- **Add media editing modal with OCR tool in web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11563), [Gargron](https://github.com/tootsuite/mastodon/pull/11566), [ThibG](https://github.com/tootsuite/mastodon/pull/11575), [ThibG](https://github.com/tootsuite/mastodon/pull/11576), [Gargron](https://github.com/tootsuite/mastodon/pull/11577), [Gargron](https://github.com/tootsuite/mastodon/pull/11573), [Gargron](https://github.com/tootsuite/mastodon/pull/11571))
- Add indicator of unread notifications to window title when web UI is out of focus ([Gargron](https://github.com/tootsuite/mastodon/pull/11560), [Gargron](https://github.com/tootsuite/mastodon/pull/11572))
- Add indicator for which options you voted for in a poll in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11195))
- **Add search results pagination to web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11409), [ThibG](https://github.com/tootsuite/mastodon/pull/11447))
- **Add option to disable real-time updates in web UI ("slow mode")** ([Gargron](https://github.com/tootsuite/mastodon/pull/9984), [ykzts](https://github.com/tootsuite/mastodon/pull/11880), [ThibG](https://github.com/tootsuite/mastodon/pull/11883), [Gargron](https://github.com/tootsuite/mastodon/pull/11898), [ThibG](https://github.com/tootsuite/mastodon/pull/11859))
- Add option to disable blurhash previews in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11188))
- Add native smooth scrolling when supported in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11207))
- Add scrolling to the search bar on focus in web UI ([Kjwon15](https://github.com/tootsuite/mastodon/pull/12032))
- Add refresh button to list of rebloggers/favouriters in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/12031))
- Add error description and button to copy stack trace to web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/12033))
- Add search and sort functions to hashtag admin UI ([mayaeh](https://github.com/tootsuite/mastodon/pull/11829), [Gargron](https://github.com/tootsuite/mastodon/pull/11897), [mayaeh](https://github.com/tootsuite/mastodon/pull/11875))
- Add setting for default search engine indexing in admin UI ([brortao](https://github.com/tootsuite/mastodon/pull/11804))
- Add account bio to account view in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11473))
- **Add option to include reported statuses in warning e-mail from admin UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11639), [Gargron](https://github.com/tootsuite/mastodon/pull/11812), [Gargron](https://github.com/tootsuite/mastodon/pull/11741), [Gargron](https://github.com/tootsuite/mastodon/pull/11698), [mayaeh](https://github.com/tootsuite/mastodon/pull/11765))
- Add number of pending accounts and pending hashtags to dashboard in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11514))
- **Add account migration UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11846), [noellabo](https://github.com/tootsuite/mastodon/pull/11905), [noellabo](https://github.com/tootsuite/mastodon/pull/11907), [noellabo](https://github.com/tootsuite/mastodon/pull/11906), [noellabo](https://github.com/tootsuite/mastodon/pull/11902))
- **Add table of contents to about page** ([Gargron](https://github.com/tootsuite/mastodon/pull/11885), [ykzts](https://github.com/tootsuite/mastodon/pull/11941), [ykzts](https://github.com/tootsuite/mastodon/pull/11895), [Kjwon15](https://github.com/tootsuite/mastodon/pull/11916))
- **Add password challenge to 2FA settings, e-mail notifications** ([Gargron](https://github.com/tootsuite/mastodon/pull/11878))
- **Add optional public list of domain blocks with comments** ([ThibG](https://github.com/tootsuite/mastodon/pull/11298), [ThibG](https://github.com/tootsuite/mastodon/pull/11515), [Gargron](https://github.com/tootsuite/mastodon/pull/11908))
- Add an RSS feed for featured hashtags ([noellabo](https://github.com/tootsuite/mastodon/pull/10502))
- Add explanations to featured hashtags UI and profile ([Gargron](https://github.com/tootsuite/mastodon/pull/11586))
- **Add hashtag trends with admin and user settings** ([Gargron](https://github.com/tootsuite/mastodon/pull/11490), [Gargron](https://github.com/tootsuite/mastodon/pull/11502), [Gargron](https://github.com/tootsuite/mastodon/pull/11641), [Gargron](https://github.com/tootsuite/mastodon/pull/11594), [Gargron](https://github.com/tootsuite/mastodon/pull/11517), [mayaeh](https://github.com/tootsuite/mastodon/pull/11845), [Gargron](https://github.com/tootsuite/mastodon/pull/11774), [Gargron](https://github.com/tootsuite/mastodon/pull/11712), [Gargron](https://github.com/tootsuite/mastodon/pull/11791), [Gargron](https://github.com/tootsuite/mastodon/pull/11743), [Gargron](https://github.com/tootsuite/mastodon/pull/11740), [Gargron](https://github.com/tootsuite/mastodon/pull/11714), [ThibG](https://github.com/tootsuite/mastodon/pull/11631), [Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/11569), [Gargron](https://github.com/tootsuite/mastodon/pull/11524), [Gargron](https://github.com/tootsuite/mastodon/pull/11513))
- Add hashtag usage breakdown to admin UI
- Add batch actions for hashtags to admin UI
- Add trends to web UI
- Add trends to public pages
- Add user preference to hide trends
- Add admin setting to disable trends
- **Add categories for custom emojis** ([Gargron](https://github.com/tootsuite/mastodon/pull/11196), [Gargron](https://github.com/tootsuite/mastodon/pull/11793), [Gargron](https://github.com/tootsuite/mastodon/pull/11920), [highemerly](https://github.com/tootsuite/mastodon/pull/11876))
- Add custom emoji categories to emoji picker in web UI
- Add `category` to custom emojis in REST API
- Add batch actions for custom emojis in admin UI
- Add max image dimensions to error message ([raboof](https://github.com/tootsuite/mastodon/pull/11552))
- Add aac, m4a, 3gp, amr, wma to allowed audio formats ([Gargron](https://github.com/tootsuite/mastodon/pull/11342), [umonaca](https://github.com/tootsuite/mastodon/pull/11687))
- **Add search syntax for operators and phrases** ([Gargron](https://github.com/tootsuite/mastodon/pull/11411))
- **Add REST API for managing featured hashtags** ([noellabo](https://github.com/tootsuite/mastodon/pull/11778))
- **Add REST API for managing timeline read markers** ([Gargron](https://github.com/tootsuite/mastodon/pull/11762))
- Add `exclude_unreviewed` param to `GET /api/v2/search` REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/11977))
- Add `reason` param to `POST /api/v1/accounts` REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/12064))
- **Add ActivityPub secure mode** ([Gargron](https://github.com/tootsuite/mastodon/pull/11269), [ThibG](https://github.com/tootsuite/mastodon/pull/11332), [ThibG](https://github.com/tootsuite/mastodon/pull/11295))
- Add HTTP signatures to all outgoing ActivityPub GET requests ([Gargron](https://github.com/tootsuite/mastodon/pull/11284), [ThibG](https://github.com/tootsuite/mastodon/pull/11300))
- Add support for ActivityPub Audio activities ([ThibG](https://github.com/tootsuite/mastodon/pull/11189))
- Add ActivityPub actor representing the entire server ([ThibG](https://github.com/tootsuite/mastodon/pull/11321), [rtucker](https://github.com/tootsuite/mastodon/pull/11400), [ThibG](https://github.com/tootsuite/mastodon/pull/11561), [Gargron](https://github.com/tootsuite/mastodon/pull/11798))
- **Add whitelist mode** ([Gargron](https://github.com/tootsuite/mastodon/pull/11291), [mayaeh](https://github.com/tootsuite/mastodon/pull/11634))
- Add config of multipart threshold for S3 ([ykzts](https://github.com/tootsuite/mastodon/pull/11924), [ykzts](https://github.com/tootsuite/mastodon/pull/11944))
- Add health check endpoint for web ([ykzts](https://github.com/tootsuite/mastodon/pull/11770), [ykzts](https://github.com/tootsuite/mastodon/pull/11947))
- Add HTTP signature keyId to request log ([Gargron](https://github.com/tootsuite/mastodon/pull/11591))
- Add `SMTP_REPLY_TO` environment variable ([hugogameiro](https://github.com/tootsuite/mastodon/pull/11718))
- Add `tootctl preview_cards remove` command ([mayaeh](https://github.com/tootsuite/mastodon/pull/11320))
- Add `tootctl media refresh` command ([Gargron](https://github.com/tootsuite/mastodon/pull/11775))
- Add `tootctl cache recount` command ([Gargron](https://github.com/tootsuite/mastodon/pull/11597))
- Add option to exclude suspended domains from `tootctl domains crawl` ([dariusk](https://github.com/tootsuite/mastodon/pull/11454))
- Add parallelization to `tootctl search deploy` ([noellabo](https://github.com/tootsuite/mastodon/pull/12051))
- Add soft delete for statuses for instant deletes through API ([Gargron](https://github.com/tootsuite/mastodon/pull/11623), [Gargron](https://github.com/tootsuite/mastodon/pull/11648))
- Add rails-level JSON caching ([Gargron](https://github.com/tootsuite/mastodon/pull/11333), [Gargron](https://github.com/tootsuite/mastodon/pull/11271))
- **Add request pool to improve delivery performance** ([Gargron](https://github.com/tootsuite/mastodon/pull/10353), [ykzts](https://github.com/tootsuite/mastodon/pull/11756))
- Add concurrent connection attempts to resolved IP addresses ([ThibG](https://github.com/tootsuite/mastodon/pull/11757))
- Add index for remember_token to improve login performance ([abcang](https://github.com/tootsuite/mastodon/pull/11881))
- **Add more accurate hashtag search** ([Gargron](https://github.com/tootsuite/mastodon/pull/11579), [Gargron](https://github.com/tootsuite/mastodon/pull/11427), [Gargron](https://github.com/tootsuite/mastodon/pull/11448))
- **Add more accurate account search** ([Gargron](https://github.com/tootsuite/mastodon/pull/11537), [Gargron](https://github.com/tootsuite/mastodon/pull/11580))
- **Add a spam check** ([Gargron](https://github.com/tootsuite/mastodon/pull/11217), [Gargron](https://github.com/tootsuite/mastodon/pull/11806), [ThibG](https://github.com/tootsuite/mastodon/pull/11296))
- Add new languages ([Gargron](https://github.com/tootsuite/mastodon/pull/12062))
- Breton
- Spanish (Argentina)
- Estonian
- Macedonian
- New Norwegian
- Add NodeInfo endpoint ([Gargron](https://github.com/tootsuite/mastodon/pull/12002), [Gargron](https://github.com/tootsuite/mastodon/pull/12058))
### Changed
- **Change conversations UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11896))
- Change dashboard to short number notation ([noellabo](https://github.com/tootsuite/mastodon/pull/11847), [noellabo](https://github.com/tootsuite/mastodon/pull/11911))
- Change REST API `GET /api/v1/timelines/public` to require authentication when public preview is off ([ThibG](https://github.com/tootsuite/mastodon/pull/11802))
- Change REST API `POST /api/v1/follow_requests/:id/(approve|reject)` to return relationship ([ThibG](https://github.com/tootsuite/mastodon/pull/11800))
- Change rate limit for media proxy ([ykzts](https://github.com/tootsuite/mastodon/pull/11814))
- Change unlisted custom emoji to not appear in autosuggestions ([Gargron](https://github.com/tootsuite/mastodon/pull/11818))
- Change max length of media descriptions from 420 to 1500 characters ([Gargron](https://github.com/tootsuite/mastodon/pull/11819), [ThibG](https://github.com/tootsuite/mastodon/pull/11836))
- **Change deletes to preserve soft-deleted statuses in unresolved reports** ([Gargron](https://github.com/tootsuite/mastodon/pull/11805))
- **Change tootctl to use inline parallelization instead of Sidekiq** ([Gargron](https://github.com/tootsuite/mastodon/pull/11776))
- **Change account deletion page to have better explanations** ([Gargron](https://github.com/tootsuite/mastodon/pull/11753), [Gargron](https://github.com/tootsuite/mastodon/pull/11763))
- Change hashtag component in web UI to show numbers for 2 last days ([Gargron](https://github.com/tootsuite/mastodon/pull/11742), [Gargron](https://github.com/tootsuite/mastodon/pull/11755), [Gargron](https://github.com/tootsuite/mastodon/pull/11754))
- Change OpenGraph description on sign-up page to reflect invite ([Gargron](https://github.com/tootsuite/mastodon/pull/11744))
- Change layout of public profile directory to be the same as in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11705))
- Change detailed status child ordering to sort self-replies on top ([ThibG](https://github.com/tootsuite/mastodon/pull/11686))
- Change window resize handler to switch to/from mobile layout as soon as needed ([ThibG](https://github.com/tootsuite/mastodon/pull/11656))
- Change icon button styles to make hover/focus states more obvious ([ThibG](https://github.com/tootsuite/mastodon/pull/11474))
- Change contrast of status links that are not mentions or hashtags ([ThibG](https://github.com/tootsuite/mastodon/pull/11406))
- **Change hashtags to preserve first-used casing** ([Gargron](https://github.com/tootsuite/mastodon/pull/11416), [Gargron](https://github.com/tootsuite/mastodon/pull/11508), [Gargron](https://github.com/tootsuite/mastodon/pull/11504), [Gargron](https://github.com/tootsuite/mastodon/pull/11507), [Gargron](https://github.com/tootsuite/mastodon/pull/11441))
- **Change unconfirmed user login behaviour** ([Gargron](https://github.com/tootsuite/mastodon/pull/11375), [ThibG](https://github.com/tootsuite/mastodon/pull/11394), [Gargron](https://github.com/tootsuite/mastodon/pull/11860))
- **Change single-column mode to scroll the whole page** ([Gargron](https://github.com/tootsuite/mastodon/pull/11359), [Gargron](https://github.com/tootsuite/mastodon/pull/11894), [Gargron](https://github.com/tootsuite/mastodon/pull/11891), [ThibG](https://github.com/tootsuite/mastodon/pull/11655), [Gargron](https://github.com/tootsuite/mastodon/pull/11463), [Gargron](https://github.com/tootsuite/mastodon/pull/11458), [ThibG](https://github.com/tootsuite/mastodon/pull/11395), [Gargron](https://github.com/tootsuite/mastodon/pull/11418))
- Change `tootctl accounts follow` to only work with local accounts ([angristan](https://github.com/tootsuite/mastodon/pull/11592))
- Change Dockerfile ([Shleeble](https://github.com/tootsuite/mastodon/pull/11710), [ykzts](https://github.com/tootsuite/mastodon/pull/11768), [Shleeble](https://github.com/tootsuite/mastodon/pull/11707))
- Change supported Node versions to include v12 ([abcang](https://github.com/tootsuite/mastodon/pull/11706))
- Change Portuguese language from `pt` to `pt-PT` ([Gargron](https://github.com/tootsuite/mastodon/pull/11820))
- Change domain block silence to always require approval on follow ([ThibG](https://github.com/tootsuite/mastodon/pull/11975))
- Change link preview fetcher to not perform a HEAD request first ([Gargron](https://github.com/tootsuite/mastodon/pull/12028))
- Change `tootctl domains purge` to accept multiple domains at once ([Gargron](https://github.com/tootsuite/mastodon/pull/12046))
### Removed
- **Remove OStatus support** ([Gargron](https://github.com/tootsuite/mastodon/pull/11205), [Gargron](https://github.com/tootsuite/mastodon/pull/11303), [Gargron](https://github.com/tootsuite/mastodon/pull/11460), [ThibG](https://github.com/tootsuite/mastodon/pull/11280), [ThibG](https://github.com/tootsuite/mastodon/pull/11278))
- Remove Atom feeds and old URLs in the form of `GET /:username/updates/:id` ([Gargron](https://github.com/tootsuite/mastodon/pull/11247))
- Remove WebP support ([angristan](https://github.com/tootsuite/mastodon/pull/11589))
- Remove deprecated config options from Heroku and Scalingo ([ykzts](https://github.com/tootsuite/mastodon/pull/11925))
- Remove deprecated REST API `GET /api/v1/search` API ([Gargron](https://github.com/tootsuite/mastodon/pull/11823))
- Remove deprecated REST API `GET /api/v1/statuses/:id/card` ([Gargron](https://github.com/tootsuite/mastodon/pull/11213))
- Remove deprecated REST API `POST /api/v1/notifications/dismiss?id=:id` ([Gargron](https://github.com/tootsuite/mastodon/pull/11214))
- Remove deprecated REST API `GET /api/v1/timelines/direct` ([Gargron](https://github.com/tootsuite/mastodon/pull/11212))
### Fixed
- Fix manifest warning ([ykzts](https://github.com/tootsuite/mastodon/pull/11767))
- Fix admin UI for custom emoji not respecting GIF autoplay preference ([ThibG](https://github.com/tootsuite/mastodon/pull/11801))
- Fix page body not being scrollable in admin/settings layout ([Gargron](https://github.com/tootsuite/mastodon/pull/11893))
- Fix placeholder colors for inputs not being explicitly defined ([Gargron](https://github.com/tootsuite/mastodon/pull/11890))
- Fix incorrect enclosure length in RSS ([tsia](https://github.com/tootsuite/mastodon/pull/11889))
- Fix TOTP codes not being filtered from logs during enabling/disabling ([Gargron](https://github.com/tootsuite/mastodon/pull/11877))
- Fix webfinger response not returning 410 when account is suspended ([Gargron](https://github.com/tootsuite/mastodon/pull/11869))
- Fix ActivityPub Move handler queuing jobs that will fail if account is suspended ([Gargron](https://github.com/tootsuite/mastodon/pull/11864))
- Fix SSO login not using existing account when e-mail is verified ([Gargron](https://github.com/tootsuite/mastodon/pull/11862))
- Fix web UI allowing uploads past status limit via drag & drop ([Gargron](https://github.com/tootsuite/mastodon/pull/11863))
- Fix expiring polls not being displayed as such in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11835))
- Fix 2FA challenge and password challenge for non-database users ([Gargron](https://github.com/tootsuite/mastodon/pull/11831), [Gargron](https://github.com/tootsuite/mastodon/pull/11943))
- Fix profile fields overflowing page width in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11828))
- Fix web push subscriptions being deleted on rate limit or timeout ([Gargron](https://github.com/tootsuite/mastodon/pull/11826))
- Fix display of long poll options in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11717), [ThibG](https://github.com/tootsuite/mastodon/pull/11833))
- Fix search API not resolving URL when `type` is given ([Gargron](https://github.com/tootsuite/mastodon/pull/11822))
- Fix hashtags being split by ZWNJ character ([Gargron](https://github.com/tootsuite/mastodon/pull/11821))
- Fix scroll position resetting when opening media modals in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11815))
- Fix duplicate HTML IDs on about page ([ThibG](https://github.com/tootsuite/mastodon/pull/11803))
- Fix admin UI showing superfluous reject media/reports on suspended domain blocks ([ThibG](https://github.com/tootsuite/mastodon/pull/11749))
- Fix ActivityPub context not being dynamically computed ([ThibG](https://github.com/tootsuite/mastodon/pull/11746))
- Fix Mastodon logo style on hover on public pages' footer ([ThibG](https://github.com/tootsuite/mastodon/pull/11735))
- Fix height of dashboard counters ([ThibG](https://github.com/tootsuite/mastodon/pull/11736))
- Fix custom emoji animation on hover in web UI directory bios ([ThibG](https://github.com/tootsuite/mastodon/pull/11716))
- Fix non-numbers being passed to Redis and causing an error ([Gargron](https://github.com/tootsuite/mastodon/pull/11697))
- Fix error in REST API for an account's statuses ([Gargron](https://github.com/tootsuite/mastodon/pull/11700))
- Fix uncaught error when resource param is missing in Webfinger request ([Gargron](https://github.com/tootsuite/mastodon/pull/11701))
- Fix uncaught domain normalization error in remote follow ([Gargron](https://github.com/tootsuite/mastodon/pull/11703))
- Fix uncaught 422 and 500 errors ([Gargron](https://github.com/tootsuite/mastodon/pull/11590), [Gargron](https://github.com/tootsuite/mastodon/pull/11811))
- Fix uncaught parameter missing exceptions and missing error templates ([Gargron](https://github.com/tootsuite/mastodon/pull/11702))
- Fix encoding error when checking e-mail MX records ([Gargron](https://github.com/tootsuite/mastodon/pull/11696))
- Fix items in StatusContent render list not all having a key ([ThibG](https://github.com/tootsuite/mastodon/pull/11645))
- Fix remote and staff-removed statuses leaving media behind for a day ([Gargron](https://github.com/tootsuite/mastodon/pull/11638))
- Fix CSP needlessly allowing blob URLs in script-src ([ThibG](https://github.com/tootsuite/mastodon/pull/11620))
- Fix ignoring whole status because of one invalid hashtag ([Gargron](https://github.com/tootsuite/mastodon/pull/11621))
- Fix hidden statuses losing focus ([ThibG](https://github.com/tootsuite/mastodon/pull/11208))
- Fix loading bar being obscured by other elements in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11598))
- Fix multiple issues with replies collection for pages further than self-replies ([ThibG](https://github.com/tootsuite/mastodon/pull/11582))
- Fix blurhash and autoplay not working on public pages ([Gargron](https://github.com/tootsuite/mastodon/pull/11585))
- Fix 422 being returned instead of 404 when POSTing to unmatched routes ([Gargron](https://github.com/tootsuite/mastodon/pull/11574), [Gargron](https://github.com/tootsuite/mastodon/pull/11704))
- Fix client-side resizing of image uploads ([ThibG](https://github.com/tootsuite/mastodon/pull/11570))
- Fix short number formatting for numbers above million in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11559))
- Fix ActivityPub and REST API queries setting cookies and preventing caching ([ThibG](https://github.com/tootsuite/mastodon/pull/11539), [ThibG](https://github.com/tootsuite/mastodon/pull/11557), [ThibG](https://github.com/tootsuite/mastodon/pull/11336), [ThibG](https://github.com/tootsuite/mastodon/pull/11331))
- Fix some emojis in profile metadata labels are not emojified. ([kedamaDQ](https://github.com/tootsuite/mastodon/pull/11534))
- Fix account search always returning exact match on paginated results ([Gargron](https://github.com/tootsuite/mastodon/pull/11525))
- Fix acct URIs with IDN domains not being resolved ([Gargron](https://github.com/tootsuite/mastodon/pull/11520))
- Fix admin dashboard missing latest features ([Gargron](https://github.com/tootsuite/mastodon/pull/11505))
- Fix jumping of toot date when clicking spoiler button ([ariasuni](https://github.com/tootsuite/mastodon/pull/11449))
- Fix boost to original audience not working on mobile in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11371))
- Fix handling of webfinger redirects in ResolveAccountService ([ThibG](https://github.com/tootsuite/mastodon/pull/11279))
- Fix URLs appearing twice in errors of ActivityPub::DeliveryWorker ([Gargron](https://github.com/tootsuite/mastodon/pull/11231))
- Fix support for HTTP proxies ([ThibG](https://github.com/tootsuite/mastodon/pull/11245))
- Fix HTTP requests to IPv6 hosts ([ThibG](https://github.com/tootsuite/mastodon/pull/11240))
- Fix error in ElasticSearch index import ([mayaeh](https://github.com/tootsuite/mastodon/pull/11192))
- Fix duplicate account error when seeding development database ([ysksn](https://github.com/tootsuite/mastodon/pull/11366))
- Fix performance of session clean-up scheduler ([abcang](https://github.com/tootsuite/mastodon/pull/11871))
- Fix older migrations not running ([zunda](https://github.com/tootsuite/mastodon/pull/11377))
- Fix URLs counting towards RTL detection ([ahangarha](https://github.com/tootsuite/mastodon/pull/11759))
- Fix unnecessary status re-rendering in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11211))
- Fix http_parser.rb gem not being compiled when no network available ([petabyteboy](https://github.com/tootsuite/mastodon/pull/11444))
- Fix muted text color not applying to all text ([trwnh](https://github.com/tootsuite/mastodon/pull/11996))
- Fix follower/following lists resetting on back-navigation in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11986))
- Fix n+1 query when approving multiple follow requests ([abcang](https://github.com/tootsuite/mastodon/pull/12004))
- Fix records not being indexed into ElasticSearch sometimes ([Gargron](https://github.com/tootsuite/mastodon/pull/12024))
- Fix needlessly indexing unsearchable statuses into ElasticSearch ([Gargron](https://github.com/tootsuite/mastodon/pull/12041))
- Fix new user bootstrapping crashing when to-be-followed accounts are invalid ([ThibG](https://github.com/tootsuite/mastodon/pull/12037))
- Fix featured hashtag URL being interpreted as media or replies tab ([Gargron](https://github.com/tootsuite/mastodon/pull/12048))
- Fix account counters being overwritten by parallel writes ([Gargron](https://github.com/tootsuite/mastodon/pull/12045))
### Security
- Fix performance of GIF re-encoding and always strip EXIF data from videos ([Gargron](https://github.com/tootsuite/mastodon/pull/12057))
## [2.9.3] - 2019-08-10
### Added

View File

@ -4,22 +4,20 @@ FROM ubuntu:18.04 as build-dep
SHELL ["bash", "-c"]
# Install Node
ENV NODE_VER="8.15.0"
ENV NODE_VER="12.11.1"
RUN echo "Etc/UTC" > /etc/localtime && \
apt update && \
apt -y install wget make gcc g++ python && \
apt -y install wget python && \
cd ~ && \
wget https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER.tar.gz && \
tar xf node-v$NODE_VER.tar.gz && \
cd node-v$NODE_VER && \
./configure --prefix=/opt/node && \
make -j$(nproc) > /dev/null && \
make install
wget https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER-linux-x64.tar.gz && \
tar xf node-v$NODE_VER-linux-x64.tar.gz && \
rm node-v$NODE_VER-linux-x64.tar.gz && \
mv node-v$NODE_VER-linux-x64 /opt/node
# Install jemalloc
ENV JE_VER="5.1.0"
ENV JE_VER="5.2.1"
RUN apt update && \
apt -y install autoconf && \
apt -y install make autoconf gcc g++ && \
cd ~ && \
wget https://github.com/jemalloc/jemalloc/archive/$JE_VER.tar.gz && \
tar xf $JE_VER.tar.gz && \
@ -30,7 +28,7 @@ RUN apt update && \
make install_bin install_include install_lib
# Install ruby
ENV RUBY_VER="2.6.1"
ENV RUBY_VER="2.6.5"
ENV CPPFLAGS="-I/opt/jemalloc/include"
ENV LDFLAGS="-L/opt/jemalloc/lib/"
RUN apt update && \

53
Gemfile
View File

@ -5,17 +5,17 @@ ruby '>= 2.4.0', '< 2.7.0'
gem 'pkg-config', '~> 1.3'
gem 'puma', '~> 3.12'
gem 'puma', '~> 4.2'
gem 'rails', '~> 5.2.3'
gem 'thor', '~> 0.20'
gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 1.1'
gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.2'
gem 'pghero', '~> 2.3'
gem 'dotenv-rails', '~> 2.7'
gem 'aws-sdk-s3', '~> 1.42', require: false
gem 'aws-sdk-s3', '~> 1.48', require: false
gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false
gem 'paperclip', '~> 6.0'
@ -24,15 +24,15 @@ gem 'streamio-ffmpeg', '~> 3.0'
gem 'blurhash', '~> 0.1'
gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.6'
gem 'addressable', '~> 2.7'
gem 'bootsnap', '~> 1.4', require: false
gem 'browser'
gem 'charlock_holmes', '~> 0.7.6'
gem 'iso-639'
gem 'chewy', '~> 5.0'
gem 'chewy', '~> 5.1'
gem 'cld3', '~> 3.2.4'
gem 'devise', '~> 4.6'
gem 'devise-two-factor', '~> 3.0'
gem 'devise', '~> 4.7'
gem 'devise-two-factor', '~> 3.1'
group :pam_authentication, optional: true do
gem 'devise_pam_authenticatable2', '~> 9.2'
@ -43,42 +43,48 @@ gem 'omniauth-cas', '~> 1.1'
gem 'omniauth-saml', '~> 1.10'
gem 'omniauth', '~> 1.9'
gem 'doorkeeper', '~> 5.1'
gem 'discard', '~> 1.1'
gem 'doorkeeper', '~> 5.2'
gem 'fast_blank', '~> 1.0'
gem 'fastimage'
gem 'goldfinger', '~> 2.1'
gem 'hiredis', '~> 0.6'
gem 'redis-namespace', '~> 1.5'
gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b'
gem 'htmlentities', '~> 4.3'
gem 'http', '~> 3.3'
gem 'http_accept_language', '~> 2.1'
gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2'
gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2', submodules: true
gem 'httplog', '~> 1.3'
gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.1'
gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.2', require: 'mime/types/columnar'
gem 'mime-types', '~> 3.3', require: 'mime/types/columnar'
gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b176939f851338d0a4971a532'
gem 'nokogiri', '~> 1.10'
gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.7'
gem 'oj', '~> 3.9'
gem 'ostatus2', '~> 2.0'
gem 'ox', '~> 2.11'
gem 'parslet'
gem 'parallel', '~> 1.17'
gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
gem 'pundit', '~> 2.0'
gem 'pundit', '~> 2.1'
gem 'premailer-rails'
gem 'rack-attack', '~> 6.0'
gem 'rack-attack', '~> 6.1'
gem 'rack-cors', '~> 1.0', require: 'rack/cors'
gem 'rails-i18n', '~> 5.1'
gem 'rails-settings-cached', '~> 0.6'
gem 'redis', '~> 4.1', require: ['redis', 'redis/connection/hiredis']
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 0.10'
gem 'sanitize', '~> 5.0'
gem 'ruby-progressbar', '~> 1.10'
gem 'sanitize', '~> 5.1'
gem 'sidekiq', '~> 5.2'
gem 'sidekiq-scheduler', '~> 3.0'
gem 'sidekiq-unique-jobs', '~> 6.0'
gem 'sidekiq-bulk', '~>0.2.0'
gem 'simple-navigation', '~> 4.0'
gem 'simple-navigation', '~> 4.1'
gem 'simple_form', '~> 4.1'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
gem 'stoplight', '~> 2.1.3'
@ -90,7 +96,7 @@ gem 'tzinfo-data', '~> 1.2019'
gem 'webpacker', '~> 4.0'
gem 'webpush'
gem 'json-ld', '~> 3.0'
gem 'json-ld', git: 'https://github.com/ruby-rdf/json-ld.git', ref: 'e742697a0906e74e8bb777ef98137bc3955d981d'
gem 'json-ld-preloaded', '~> 3.0'
gem 'rdf-normalize', '~> 0.3'
@ -108,14 +114,14 @@ group :production, :test do
end
group :test do
gem 'capybara', '~> 3.24'
gem 'capybara', '~> 3.29'
gem 'climate_control', '~> 0.2'
gem 'faker', '~> 1.9'
gem 'faker', '~> 2.4'
gem 'microformats', '~> 4.1'
gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.16', require: false
gem 'webmock', '~> 3.6'
gem 'simplecov', '~> 0.17', require: false
gem 'webmock', '~> 3.7'
gem 'parallel_tests', '~> 2.29'
end
@ -128,9 +134,9 @@ group :development do
gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 1.3'
gem 'memory_profiler'
gem 'rubocop', '~> 0.71', require: false
gem 'rubocop-rails', '~> 2.0', require: false
gem 'brakeman', '~> 4.5', require: false
gem 'rubocop', '~> 0.74', require: false
gem 'rubocop-rails', '~> 2.3', require: false
gem 'brakeman', '~> 4.6', require: false
gem 'bundler-audit', '~> 0.6', require: false
gem 'capistrano', '~> 3.11'
@ -148,3 +154,4 @@ group :production do
end
gem 'concurrent-ruby', require: false
gem 'connection_pool', require: false

View File

@ -1,3 +1,11 @@
GIT
remote: https://github.com/ianheggie/health_check
revision: 0b799ead604f900ed50685e9b2d469cd2befba5b
ref: 0b799ead604f900ed50685e9b2d469cd2befba5b
specs:
health_check (4.0.0.pre)
rails (>= 4.0)
GIT
remote: https://github.com/rtomayko/posix-spawn
revision: 58465d2e213991f8afb13b984854a49fcdcc980c
@ -5,13 +13,34 @@ GIT
specs:
posix-spawn (0.3.13)
GIT
remote: https://github.com/ruby-rdf/json-ld.git
revision: e742697a0906e74e8bb777ef98137bc3955d981d
ref: e742697a0906e74e8bb777ef98137bc3955d981d
specs:
json-ld (3.0.2)
htmlentities (~> 4.3)
json-canonicalization (~> 0.1)
link_header (~> 0.0, >= 0.0.8)
multi_json (~> 1.13)
rack (>= 1.6, < 3.0)
rdf (~> 3.0, >= 3.0.8)
GIT
remote: https://github.com/tmm1/http_parser.rb
revision: 54b17ba8c7d8d20a16dfc65d1775241833219cf2
ref: 54b17ba8c7d8d20a16dfc65d1775241833219cf2
submodules: true
specs:
http_parser.rb (0.6.1)
GIT
remote: https://github.com/witgo/nilsimsa
revision: fd184883048b922b176939f851338d0a4971a532
ref: fd184883048b922b176939f851338d0a4971a532
specs:
nilsimsa (1.1.2)
GEM
remote: https://rubygems.org/
specs:
@ -38,9 +67,9 @@ GEM
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
active_model_serializers (0.10.9)
actionpack (>= 4.1, < 6)
activemodel (>= 4.1, < 6)
active_model_serializers (0.10.10)
actionpack (>= 4.1, < 6.1)
activemodel (>= 4.1, < 6.1)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_record_query_trace (1.6.2)
@ -62,9 +91,9 @@ GEM
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
addressable (2.6.0)
public_suffix (>= 2.0.2, < 4.0)
airbrussh (1.3.0)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
airbrussh (1.3.3)
sshkit (>= 1.6.1, != 1.7.0)
annotate (2.7.5)
activerecord (>= 3.2, < 7.0)
@ -76,17 +105,17 @@ GEM
av (0.9.0)
cocaine (~> 0.5.3)
aws-eventstream (1.0.3)
aws-partitions (1.175.0)
aws-sdk-core (3.55.0)
aws-partitions (1.207.0)
aws-sdk-core (3.65.1)
aws-eventstream (~> 1.0, >= 1.0.2)
aws-partitions (~> 1.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.21.0)
aws-sdk-core (~> 3, >= 3.53.0)
aws-sdk-kms (1.24.0)
aws-sdk-core (~> 3, >= 3.61.1)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.42.0)
aws-sdk-core (~> 3, >= 3.53.0)
aws-sdk-s3 (1.48.0)
aws-sdk-core (~> 3, >= 3.61.1)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.0)
@ -101,19 +130,19 @@ GEM
debug_inspector (>= 0.0.1)
blurhash (0.1.3)
ffi (~> 1.10.0)
bootsnap (1.4.4)
bootsnap (1.4.5)
msgpack (~> 1.0)
brakeman (4.5.1)
browser (2.5.3)
brakeman (4.6.1)
browser (2.6.1)
builder (3.2.3)
bullet (6.0.0)
bullet (6.0.2)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
bundler-audit (0.6.1)
bundler (>= 1.2.0, < 3)
thor (~> 0.18)
byebug (11.0.0)
capistrano (3.11.0)
capistrano (3.11.1)
airbrussh (>= 1.0.0)
i18n
rake (>= 10.0.0)
@ -129,7 +158,7 @@ GEM
sshkit (~> 1.3)
capistrano-yarn (2.0.2)
capistrano (~> 3.0)
capybara (3.24.0)
capybara (3.29.0)
addressable
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
@ -140,7 +169,7 @@ GEM
case_transform (0.2)
activesupport
charlock_holmes (0.7.6)
chewy (5.0.0)
chewy (5.1.0)
activesupport (>= 4.0)
elasticsearch (>= 2.0.0)
elasticsearch-dsl
@ -156,10 +185,10 @@ GEM
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.4)
css_parser (1.6.0)
css_parser (1.7.0)
addressable
debug_inspector (0.0.3)
derailed_benchmarks (1.3.5)
derailed_benchmarks (1.3.6)
benchmark-ips (~> 2)
get_process_mem (~> 0)
heapy (~> 0)
@ -167,38 +196,40 @@ GEM
rack (>= 1)
rake (> 10, < 13)
thor (~> 0.19)
devise (4.6.2)
devise (4.7.1)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0, < 6.0)
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
devise-two-factor (3.0.3)
activesupport (< 5.3)
devise-two-factor (3.1.0)
activesupport (< 6.1)
attr_encrypted (>= 1.3, < 4, != 2)
devise (~> 4.0)
railties (< 5.3)
railties (< 6.1)
rotp (~> 2.0)
devise_pam_authenticatable2 (9.2.0)
devise (>= 4.0.0)
rpam2 (~> 4.0)
diff-lcs (1.3)
docile (1.3.0)
discard (1.1.0)
activerecord (>= 4.2, < 7)
docile (1.3.2)
domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.1.0)
doorkeeper (5.2.1)
railties (>= 5)
dotenv (2.7.2)
dotenv-rails (2.7.2)
dotenv (= 2.7.2)
dotenv (2.7.5)
dotenv-rails (2.7.5)
dotenv (= 2.7.5)
railties (>= 3.2, < 6.1)
elasticsearch (6.0.2)
elasticsearch-api (= 6.0.2)
elasticsearch-transport (= 6.0.2)
elasticsearch-api (6.0.2)
elasticsearch (7.3.0)
elasticsearch-api (= 7.3.0)
elasticsearch-transport (= 7.3.0)
elasticsearch-api (7.3.0)
multi_json
elasticsearch-dsl (0.1.5)
elasticsearch-transport (6.0.2)
elasticsearch-dsl (0.1.8)
elasticsearch-transport (7.3.0)
faraday
multi_json
encryptor (3.0.0)
@ -208,12 +239,12 @@ GEM
tzinfo
excon (0.62.0)
fabrication (2.20.2)
faker (1.9.3)
i18n (>= 0.7)
faraday (0.15.0)
faker (2.4.0)
i18n (~> 1.6.0)
faraday (0.15.4)
multipart-post (>= 1.2, < 3)
fast_blank (1.0.0)
fastimage (2.1.5)
fastimage (2.1.7)
ffi (1.10.0)
fog-core (2.1.0)
builder
@ -253,7 +284,7 @@ GEM
railties (>= 4.0.1)
hamster (3.0.0)
concurrent-ruby (~> 1.0)
hashdiff (0.4.0)
hashdiff (1.0.0)
hashie (3.6.0)
heapy (0.1.4)
highline (2.0.1)
@ -269,7 +300,7 @@ GEM
domain_name (~> 0.5)
http-form_data (2.1.1)
http_accept_language (2.1.1)
httplog (1.3.1)
httplog (1.3.2)
rack (>= 1.0)
rainbow (>= 2.0.0)
i18n (1.6.0)
@ -287,17 +318,15 @@ GEM
idn-ruby (0.1.0)
ipaddress (0.8.3)
iso-639 (0.2.8)
jaro_winkler (1.5.2)
jaro_winkler (1.5.3)
jmespath (1.4.0)
json (2.1.0)
json-ld (3.0.2)
multi_json (~> 1.12)
rdf (>= 2.2.8, < 4.0)
json-ld-preloaded (3.0.2)
json (2.2.0)
json-canonicalization (0.1.0)
json-ld-preloaded (3.0.4)
json-ld (~> 3.0)
multi_json (~> 1.12)
rdf (~> 3.0)
jsonapi-renderer (0.2.0)
jsonapi-renderer (0.2.2)
jwt (2.1.0)
kaminari (1.1.1)
activesupport (>= 4.1.0)
@ -336,37 +365,37 @@ GEM
mimemagic (~> 0.3.2)
mario-redis-lock (1.2.1)
redis (>= 3.0.5)
memory_profiler (0.9.13)
memory_profiler (0.9.14)
method_source (0.9.2)
microformats (4.1.0)
json (~> 2.1)
nokogiri (~> 1.8, >= 1.8.3)
mime-types (3.2.2)
mime-types (3.3)
mime-types-data (~> 3.2015)
mime-types-data (3.2018.0812)
mime-types-data (3.2019.0904)
mimemagic (0.3.3)
mini_mime (1.0.1)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.11.3)
msgpack (1.2.10)
minitest (5.12.0)
msgpack (1.3.1)
multi_json (1.13.1)
multipart-post (2.0.0)
multipart-post (2.1.1)
necromancer (0.5.0)
net-ldap (0.16.1)
net-scp (1.2.1)
net-ssh (>= 2.6.5)
net-ssh (5.0.2)
nio4r (2.3.1)
nokogiri (1.10.3)
net-scp (2.0.0)
net-ssh (>= 2.6.5, < 6.0.0)
net-ssh (5.2.0)
nio4r (2.5.1)
nokogiri (1.10.4)
mini_portile2 (~> 2.4.0)
nokogumbo (2.0.0)
nokogumbo (2.0.1)
nokogiri (~> 1.8, >= 1.8.4)
nsa (0.2.7)
activesupport (>= 4.2, < 6)
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.7.12)
oj (3.9.1)
omniauth (1.9.0)
hashie (>= 3.4.6, < 3.7.0)
rack (>= 1.6.2, < 3)
@ -393,23 +422,24 @@ GEM
av (~> 0.9.0)
paperclip (>= 2.5.2)
parallel (1.17.0)
parallel_tests (2.29.0)
parallel_tests (2.29.2)
parallel
parser (2.6.3.0)
parser (2.6.4.0)
ast (~> 2.4.0)
parslet (1.8.2)
pastel (0.7.2)
equatable (~> 0.5.0)
tty-color (~> 0.4.0)
pg (1.1.4)
pghero (2.2.1)
activerecord
pkg-config (1.3.7)
pghero (2.3.0)
activerecord (>= 5)
pkg-config (1.3.8)
premailer (1.11.1)
addressable
css_parser (>= 1.6.0)
htmlentities (>= 4.0.0)
premailer-rails (1.10.2)
actionmailer (>= 3, < 6)
premailer-rails (1.10.3)
actionmailer (>= 3)
premailer (~> 1.7, >= 1.7.9)
private_address_check (0.5.0)
pry (0.12.2)
@ -420,13 +450,14 @@ GEM
pry (~> 0.10)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (3.1.0)
puma (3.12.1)
pundit (2.0.1)
public_suffix (4.0.1)
puma (4.2.0)
nio4r (~> 2.0)
pundit (2.1.0)
activesupport (>= 3.0.0)
raabro (1.1.6)
rack (2.0.7)
rack-attack (6.0.0)
rack-attack (6.1.0)
rack (>= 1.0, < 3)
rack-cors (1.0.3)
rack-protection (2.0.5)
@ -455,7 +486,7 @@ GEM
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.0.4)
rails-html-sanitizer (1.2.0)
loofah (~> 2.2, >= 2.2.2)
rails-i18n (5.1.3)
i18n (>= 0.7, < 2)
@ -469,13 +500,13 @@ GEM
rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0)
rainbow (3.0.0)
rake (12.3.2)
rdf (3.0.9)
rake (12.3.3)
rdf (3.0.12)
hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.3.3)
rdf (>= 2.2, < 4.0)
redis (4.1.2)
redis (4.1.3)
redis-actionpack (5.0.2)
actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3)
@ -494,12 +525,12 @@ GEM
redis-store (>= 1.2, < 2)
redis-store (1.5.0)
redis (>= 2.2, < 5)
regexp_parser (1.5.1)
regexp_parser (1.6.0)
request_store (1.4.1)
rack (>= 1.4)
responders (2.4.1)
actionpack (>= 4.2.0, < 6.0)
railties (>= 4.2.0, < 6.0)
responders (3.0.0)
actionpack (>= 5.0)
railties (>= 5.0)
rotp (2.1.2)
rpam2 (4.0.2)
rqrcode (0.10.1)
@ -524,23 +555,23 @@ GEM
rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0)
rspec-support (3.8.0)
rubocop (0.71.0)
rubocop (0.74.0)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
parser (>= 2.6)
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7)
rubocop-rails (2.0.1)
rubocop-rails (2.3.2)
rack (>= 1.1)
rubocop (>= 0.70.0)
rubocop (>= 0.72.0)
ruby-progressbar (1.10.1)
ruby-saml (1.9.0)
nokogiri (>= 1.5.10)
rufus-scheduler (3.5.2)
fugit (~> 1.1, >= 1.1.5)
safe_yaml (1.0.5)
sanitize (5.0.0)
sanitize (5.1.0)
crass (~> 1.0.2)
nokogiri (>= 1.8.0)
nokogumbo (~> 2.0)
@ -560,12 +591,12 @@ GEM
concurrent-ruby (~> 1.0, >= 1.0.5)
sidekiq (>= 4.0, < 7.0)
thor (~> 0)
simple-navigation (4.0.5)
simple-navigation (4.1.0)
activesupport (>= 2.3.2)
simple_form (4.1.0)
actionpack (>= 5.0)
activemodel (>= 5.0)
simplecov (0.16.1)
simplecov (0.17.1)
docile (~> 1.1)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
@ -577,7 +608,7 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sshkit (1.17.0)
sshkit (1.20.0)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
stackprof (0.2.12)
@ -585,7 +616,7 @@ GEM
stoplight (2.1.3)
streamio-ffmpeg (3.0.2)
multi_json (~> 1.8)
strong_migrations (0.4.0)
strong_migrations (0.4.1)
activerecord (>= 5)
temple (0.8.1)
terminal-table (1.8.0)
@ -612,7 +643,7 @@ GEM
unf (~> 0.1.0)
tzinfo (1.2.5)
thread_safe (~> 0.1)
tzinfo-data (1.2019.1)
tzinfo-data (1.2019.3)
tzinfo (>= 1.0.0)
unf (0.1.4)
unf_ext
@ -621,7 +652,7 @@ GEM
uniform_notifier (1.12.1)
warden (1.2.8)
rack (>= 2.0.6)
webmock (3.6.0)
webmock (3.7.5)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@ -645,14 +676,14 @@ PLATFORMS
DEPENDENCIES
active_model_serializers (~> 0.10)
active_record_query_trace (~> 1.6)
addressable (~> 2.6)
addressable (~> 2.7)
annotate (~> 2.7)
aws-sdk-s3 (~> 1.42)
aws-sdk-s3 (~> 1.48)
better_errors (~> 2.5)
binding_of_caller (~> 0.7)
blurhash (~> 0.1)
bootsnap (~> 1.4)
brakeman (~> 4.5)
brakeman (~> 4.6)
browser
bullet (~> 6.0)
bundler-audit (~> 0.6)
@ -660,20 +691,22 @@ DEPENDENCIES
capistrano-rails (~> 1.4)
capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0)
capybara (~> 3.24)
capybara (~> 3.29)
charlock_holmes (~> 0.7.6)
chewy (~> 5.0)
chewy (~> 5.1)
cld3 (~> 3.2.4)
climate_control (~> 0.2)
concurrent-ruby
connection_pool
derailed_benchmarks
devise (~> 4.6)
devise-two-factor (~> 3.0)
devise (~> 4.7)
devise-two-factor (~> 3.1)
devise_pam_authenticatable2 (~> 9.2)
doorkeeper (~> 5.1)
discard (~> 1.1)
doorkeeper (~> 5.2)
dotenv-rails (~> 2.7)
fabrication (~> 2.20)
faker (~> 1.9)
faker (~> 2.4)
fast_blank (~> 1.0)
fastimage
fog-core (<= 2.1.0)
@ -681,6 +714,7 @@ DEPENDENCIES
fuubar (~> 2.4)
goldfinger (~> 2.1)
hamlit-rails (~> 0.2)
health_check!
hiredis (~> 0.6)
htmlentities (~> 4.3)
http (~> 3.3)
@ -690,7 +724,7 @@ DEPENDENCIES
i18n-tasks (~> 0.9)
idn-ruby
iso-639
json-ld (~> 3.0)
json-ld!
json-ld-preloaded (~> 3.0)
kaminari (~> 1.1)
letter_opener (~> 1.7)
@ -701,11 +735,12 @@ DEPENDENCIES
mario-redis-lock (~> 1.2)
memory_profiler
microformats (~> 4.1)
mime-types (~> 3.2)
mime-types (~> 3.3)
net-ldap (~> 0.10)
nilsimsa!
nokogiri (~> 1.10)
nsa (~> 0.2)
oj (~> 3.7)
oj (~> 3.9)
omniauth (~> 1.9)
omniauth-cas (~> 1.1)
omniauth-saml (~> 1.10)
@ -713,18 +748,20 @@ DEPENDENCIES
ox (~> 2.11)
paperclip (~> 6.0)
paperclip-av-transcoder (~> 0.6)
parallel (~> 1.17)
parallel_tests (~> 2.29)
parslet
pg (~> 1.1)
pghero (~> 2.2)
pghero (~> 2.3)
pkg-config (~> 1.3)
posix-spawn!
premailer-rails
private_address_check (~> 0.5)
pry-byebug (~> 3.7)
pry-rails (~> 0.3)
puma (~> 3.12)
pundit (~> 2.0)
rack-attack (~> 6.0)
puma (~> 4.2)
pundit (~> 2.1)
rack-attack (~> 6.1)
rack-cors (~> 1.0)
rails (~> 5.2.3)
rails-controller-testing (~> 1.0)
@ -737,16 +774,17 @@ DEPENDENCIES
rqrcode (~> 0.10)
rspec-rails (~> 3.8)
rspec-sidekiq (~> 3.0)
rubocop (~> 0.71)
rubocop-rails (~> 2.0)
sanitize (~> 5.0)
rubocop (~> 0.74)
rubocop-rails (~> 2.3)
ruby-progressbar (~> 1.10)
sanitize (~> 5.1)
sidekiq (~> 5.2)
sidekiq-bulk (~> 0.2.0)
sidekiq-scheduler (~> 3.0)
sidekiq-unique-jobs (~> 6.0)
simple-navigation (~> 4.0)
simple-navigation (~> 4.1)
simple_form (~> 4.1)
simplecov (~> 0.16)
simplecov (~> 0.17)
sprockets-rails (~> 3.2)
stackprof
stoplight (~> 2.1.3)
@ -757,12 +795,12 @@ DEPENDENCIES
tty-prompt (~> 0.19)
twitter-text (~> 1.14)
tzinfo-data (~> 1.2019)
webmock (~> 3.6)
webmock (~> 3.7)
webpacker (~> 4.0)
webpush
RUBY VERSION
ruby 2.6.1p33
ruby 2.6.5p114
BUNDLED WITH
1.17.3

View File

@ -13,15 +13,6 @@
"description": "The domain that your Mastodon instance will run on (this can be appname.herokuapp.com or a custom domain)",
"required": true
},
"LOCAL_HTTPS": {
"description": "Will your domain support HTTPS? (Automatic for herokuapp, requires manual configuration for custom domains)",
"value": "false",
"required": true
},
"PAPERCLIP_SECRET": {
"description": "The secret key for storing media files",
"generator": "secret"
},
"SECRET_KEY_BASE": {
"description": "The secret key base",
"generator": "secret"

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
class AccountsIndex < Chewy::Index
settings index: { refresh_interval: '5m' }, analysis: {
analyzer: {
content: {
tokenizer: 'whitespace',
filter: %w(lowercase asciifolding cjk_width),
},
edge_ngram: {
tokenizer: 'edge_ngram',
filter: %w(lowercase asciifolding cjk_width),
},
},
tokenizer: {
edge_ngram: {
type: 'edge_ngram',
min_gram: 1,
max_gram: 15,
},
},
}
define_type ::Account.searchable.includes(:account_stat), delete_if: ->(account) { account.destroyed? || !account.searchable? } do
root date_detection: false do
field :id, type: 'long'
field :display_name, type: 'text', analyzer: 'content' do
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
end
field :acct, type: 'text', analyzer: 'content', value: ->(account) { [account.username, account.domain].compact.join('@') } do
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
end
field :following_count, type: 'long', value: ->(account) { account.following.local.count }
field :followers_count, type: 'long', value: ->(account) { account.followers.local.count }
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
end
end
end

View File

@ -31,19 +31,19 @@ class StatusesIndex < Chewy::Index
},
}
define_type ::Status.unscoped.without_reblogs.includes(:media_attachments) do
define_type ::Status.unscoped.kept.without_reblogs.includes(:media_attachments), delete_if: ->(status) { status.searchable_by.empty? } do
crutch :mentions do |collection|
data = ::Mention.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id)
data = ::Mention.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
end
crutch :favourites do |collection|
data = ::Favourite.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id)
data = ::Favourite.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
end
crutch :reblogs do |collection|
data = ::Status.where(reblog_of_id: collection.map(&:id)).pluck(:reblog_of_id, :account_id)
data = ::Status.where(reblog_of_id: collection.map(&:id)).where(account: Account.local).pluck(:reblog_of_id, :account_id)
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
end
@ -51,7 +51,7 @@ class StatusesIndex < Chewy::Index
field :id, type: 'long'
field :account_id, type: 'long'
field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).concat(status.preloadable_poll ? status_preloadable_poll.options : []).join("\n\n") } do
field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).concat(status.preloadable_poll ? status.preloadable_poll.options : []).join("\n\n") } do
field :stemmed, type: 'text', analyzer: 'content'
end

37
app/chewy/tags_index.rb Normal file
View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
class TagsIndex < Chewy::Index
settings index: { refresh_interval: '15m' }, analysis: {
analyzer: {
content: {
tokenizer: 'keyword',
filter: %w(lowercase asciifolding cjk_width),
},
edge_ngram: {
tokenizer: 'edge_ngram',
filter: %w(lowercase asciifolding cjk_width),
},
},
tokenizer: {
edge_ngram: {
type: 'edge_ngram',
min_gram: 2,
max_gram: 15,
},
},
}
define_type ::Tag.listable, delete_if: ->(tag) { tag.destroyed? || !tag.listable? } do
root date_detection: false do
field :name, type: 'text', analyzer: 'content' do
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
end
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
field :usage, type: 'long', value: ->(tag) { tag.history.reduce(0) { |total, day| total + day[:accounts].to_i } }
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }
end
end
end

View File

@ -3,20 +3,46 @@
class AboutController < ApplicationController
layout 'public'
before_action :set_instance_presenter, only: [:show, :more, :terms]
before_action :require_open_federation!, only: [:show, :more]
before_action :set_body_classes, only: :show
before_action :set_instance_presenter
before_action :set_expires_in, only: [:show, :more, :terms]
skip_before_action :check_user_permissions, only: [:more, :terms]
skip_before_action :require_functional!, only: [:more, :terms]
def show
@hide_navbar = true
def show; end
def more
flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor]
toc_generator = TOCGenerator.new(@instance_presenter.site_extended_description)
@contents = toc_generator.html
@table_of_contents = toc_generator.toc
@blocks = DomainBlock.with_user_facing_limitations.by_severity if display_blocks?
end
def more; end
def terms; end
helper_method :display_blocks?
helper_method :display_blocks_rationale?
helper_method :public_fetch_mode?
helper_method :new_user
private
def require_open_federation!
not_found if whitelist_mode?
end
def display_blocks?
Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?)
end
def display_blocks_rationale?
Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?)
end
def new_user
User.new.tap do |user|
user.build_account
@ -24,9 +50,15 @@ class AboutController < ApplicationController
end
end
helper_method :new_user
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
def set_body_classes
@hide_navbar = true
end
def set_expires_in
expires_in 0, public: true
end
end

View File

@ -4,17 +4,22 @@ class AccountsController < ApplicationController
PAGE_SIZE = 20
include AccountControllerConcern
include SignatureAuthentication
before_action :set_cache_headers
before_action :set_body_classes
skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format) }
skip_before_action :require_functional!
def show
respond_to do |format|
format.html do
mark_cacheable! unless user_signed_in?
expires_in 0, public: true unless user_signed_in?
@body_classes = 'with-modals'
@pinned_statuses = []
@endorsed_accounts = @account.endorsed_accounts.to_a.sample(4)
@featured_hashtags = @account.featured_tags.order(statuses_count: :desc)
if current_account && @account.blocking?(current_account)
@statuses = []
@ -24,6 +29,7 @@ class AccountsController < ApplicationController
@pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
@statuses = filtered_status_page(params)
@statuses = cache_collection(@statuses, Status)
@rss_url = rss_url
unless @statuses.empty?
@older_url = older_url if @statuses.last.id > filtered_statuses.last.id
@ -31,30 +37,27 @@ class AccountsController < ApplicationController
end
end
format.atom do
mark_cacheable!
@entries = @account.stream_entries.where(hidden: false).with_includes.without_local_only.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id])
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? }))
end
format.rss do
mark_cacheable!
expires_in 1.minute, public: true
@statuses = cache_collection(default_statuses.without_local_only.without_reblogs.without_replies.limit(PAGE_SIZE), Status)
render xml: RSS::AccountSerializer.render(@account, @statuses)
@statuses = filtered_statuses.without_local_only.without_reblogs.without_replies.limit(PAGE_SIZE)
@statuses = cache_collection(@statuses, Status)
render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag])
end
format.json do
render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do
ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter)
end
expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?)
render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to
end
end
end
private
def set_body_classes
@body_classes = 'with-modals'
end
def show_pinned_statuses?
[replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
end
@ -101,6 +104,14 @@ class AccountsController < ApplicationController
params[:username]
end
def rss_url
if tag_requested?
short_account_tag_url(@account, params[:tag], format: 'rss')
else
short_account_url(@account, format: 'rss')
end
end
def older_url
pagination_url(max_id: @statuses.last.id)
end
@ -122,15 +133,15 @@ class AccountsController < ApplicationController
end
def media_requested?
request.path.ends_with?('/media')
request.path.ends_with?('/media') && !tag_requested?
end
def replies_requested?
request.path.ends_with?('/with_replies')
request.path.ends_with?('/with_replies') && !tag_requested?
end
def tag_requested?
request.path.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
request.path.split('.').first.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
end
def filtered_status_page(params)
@ -140,4 +151,12 @@ class AccountsController < ApplicationController
filtered_statuses.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id]).to_a
end
end
def restrict_fields_to
if signed_request_account.present? || public_fetch_mode?
# Return all fields
else
%i(id type preferred_username inbox public_key endpoints)
end
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class ActivityPub::BaseController < Api::BaseController
skip_before_action :require_authenticated_user!
private
def set_cache_headers
response.headers['Vary'] = 'Signature' if authorized_fetch_mode?
end
end

View File

@ -1,30 +1,21 @@
# frozen_string_literal: true
class ActivityPub::CollectionsController < Api::BaseController
class ActivityPub::CollectionsController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern
before_action :set_account
before_action :require_signature!, if: :authorized_fetch_mode?
before_action :set_size
before_action :set_statuses
before_action :set_cache_headers
def show
render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do
ActiveModelSerializers::SerializableResource.new(
collection_presenter,
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
skip_activities: true
)
end
expires_in 3.minutes, public: public_fetch_mode?
render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, skip_activities: true
end
private
def set_account
@account = Account.find_local!(params[:account_username])
end
def set_statuses
@statuses = scope_for_collection
@statuses = cache_collection(@statuses, Status)
@ -42,9 +33,9 @@ class ActivityPub::CollectionsController < Api::BaseController
def scope_for_collection
case params[:id]
when 'featured'
@account.statuses.permitted_for(@account, signed_request_account).tap do |scope|
scope.merge!(@account.pinned_statuses)
end
return Status.none if @account.blocking?(signed_request_account)
@account.pinned_statuses
else
raise ActiveRecord::RecordNotFound
end

View File

@ -1,40 +1,44 @@
# frozen_string_literal: true
class ActivityPub::InboxesController < Api::BaseController
class ActivityPub::InboxesController < ActivityPub::BaseController
include SignatureVerification
include JsonLdHelper
include AccountOwnedConcern
before_action :set_account
before_action :skip_unknown_actor_delete
before_action :require_signature!
def create
if unknown_deleted_account?
head 202
elsif signed_request_account
upgrade_account
process_payload
head 202
else
render plain: signature_verification_failure_reason, status: 401
end
end
private
def skip_unknown_actor_delete
head 202 if unknown_deleted_account?
end
def unknown_deleted_account?
json = Oj.load(body, mode: :strict)
json['type'] == 'Delete' && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists?
json.is_a?(Hash) && json['type'] == 'Delete' && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists?
rescue Oj::ParseError
false
end
def set_account
@account = Account.find_local!(params[:account_username]) if params[:account_username]
def account_required?
params[:account_username].present?
end
def body
return @body if defined?(@body)
@body = request.body.read.force_encoding('UTF-8')
@body = request.body.read
@body.force_encoding('UTF-8') if @body.present?
request.body.rewind if request.body.respond_to?(:rewind)
@body
end
@ -44,7 +48,6 @@ class ActivityPub::InboxesController < Api::BaseController
ResolveAccountWorker.perform_async(signed_request_account.acct)
end
Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) if signed_request_account.subscribed?
DeliveryFailureTracker.track_inverse_success!(signed_request_account)
end

View File

@ -1,26 +1,22 @@
# frozen_string_literal: true
class ActivityPub::OutboxesController < Api::BaseController
class ActivityPub::OutboxesController < ActivityPub::BaseController
LIMIT = 20
include SignatureVerification
include AccountOwnedConcern
before_action :set_account
before_action :require_signature!, if: :authorized_fetch_mode?
before_action :set_statuses
before_action :set_cache_headers
def show
expires_in 1.minute, public: true unless page_requested?
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
private
def set_account
@account = Account.find_local!(params[:account_username])
end
def outbox_presenter
if page_requested?
ActivityPub::CollectionPresenter.new(

View File

@ -0,0 +1,71 @@
# frozen_string_literal: true
class ActivityPub::RepliesController < ActivityPub::BaseController
include SignatureAuthentication
include Authorization
include AccountOwnedConcern
DESCENDANTS_LIMIT = 60
before_action :require_signature!, if: :authorized_fetch_mode?
before_action :set_status
before_action :set_cache_headers
before_action :set_replies
def index
expires_in 0, public: public_fetch_mode?
render json: replies_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json', skip_activities: true
end
private
def set_status
@status = @account.statuses.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
raise ActiveRecord::RecordNotFound
end
def set_replies
@replies = page_params[:only_other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses
@replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted])
@replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
end
def replies_collection_presenter
page = ActivityPub::CollectionPresenter.new(
id: account_status_replies_url(@account, @status, page_params),
type: :unordered,
part_of: account_status_replies_url(@account, @status),
next: next_page,
items: @replies.map { |status| status.local ? status : status.uri }
)
return page if page_requested?
ActivityPub::CollectionPresenter.new(
id: account_status_replies_url(@account, @status),
type: :unordered,
first: page
)
end
def page_requested?
params[:page] == 'true'
end
def next_page
only_other_accounts = !(@replies&.last&.account_id == @account.id && @replies.size == DESCENDANTS_LIMIT)
account_status_replies_url(
@account,
@status,
page: true,
min_id: only_other_accounts && !page_params[:only_other_accounts] ? nil : @replies&.last&.id,
only_other_accounts: only_other_accounts
)
end
def page_params
params_slice(:only_other_accounts, :min_id).merge(page: true)
end
end

View File

@ -5,7 +5,7 @@ module Admin
before_action :set_account
def new
@account_action = Admin::AccountAction.new(type: params[:type], report_id: params[:report_id], send_email_notification: true)
@account_action = Admin::AccountAction.new(type: params[:type], report_id: params[:report_id], send_email_notification: true, include_statuses: true)
@warning_presets = AccountWarningPreset.all
end
@ -30,7 +30,7 @@ module Admin
end
def resource_params
params.require(:admin_account_action).permit(:type, :report_id, :warning_preset_id, :text, :send_email_notification)
params.require(:admin_account_action).permit(:type, :report_id, :warning_preset_id, :text, :send_email_notification, :include_statuses)
end
end
end

View File

@ -2,8 +2,8 @@
module Admin
class AccountsController < BaseController
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject]
before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject]
before_action :require_remote_account!, only: [:redownload]
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
def index
@ -19,18 +19,6 @@ module Admin
@warnings = @account.targeted_account_warnings.latest.custom
end
def subscribe
authorize @account, :subscribe?
Pubsubhubbub::SubscribeWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id)
end
def unsubscribe
authorize @account, :unsubscribe?
Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id)
end
def memorialize
authorize @account, :memorialize?
@account.memorialize!
@ -53,7 +41,7 @@ module Admin
def reject
authorize @account.user, :reject?
SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true)
SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false)
redirect_to admin_pending_accounts_path
end

View File

@ -2,19 +2,20 @@
module Admin
class CustomEmojisController < BaseController
before_action :set_custom_emoji, except: [:index, :new, :create]
before_action :set_filter_params
include ObfuscateFilename
obfuscate_filename [:custom_emoji, :image]
def index
authorize :custom_emoji, :index?
@custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page])
@form = Form::CustomEmojiBatch.new
end
def new
authorize :custom_emoji, :create?
@custom_emoji = CustomEmoji.new
end
@ -31,69 +32,17 @@ module Admin
end
end
def update
authorize @custom_emoji, :update?
if @custom_emoji.update(resource_params)
log_action :update, @custom_emoji
flash[:notice] = I18n.t('admin.custom_emojis.updated_msg')
else
flash[:alert] = I18n.t('admin.custom_emojis.update_failed_msg')
end
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
end
def destroy
authorize @custom_emoji, :destroy?
@custom_emoji.destroy!
log_action :destroy, @custom_emoji
flash[:notice] = I18n.t('admin.custom_emojis.destroyed_msg')
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
end
def copy
authorize @custom_emoji, :copy?
emoji = CustomEmoji.find_or_initialize_by(domain: nil,
shortcode: @custom_emoji.shortcode)
emoji.image = @custom_emoji.image
if emoji.save
log_action :create, emoji
flash[:notice] = I18n.t('admin.custom_emojis.copied_msg')
else
flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
end
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
end
def enable
authorize @custom_emoji, :enable?
@custom_emoji.update!(disabled: false)
log_action :enable, @custom_emoji
flash[:notice] = I18n.t('admin.custom_emojis.enabled_msg')
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
end
def disable
authorize @custom_emoji, :disable?
@custom_emoji.update!(disabled: true)
log_action :disable, @custom_emoji
flash[:notice] = I18n.t('admin.custom_emojis.disabled_msg')
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
def batch
@form = Form::CustomEmojiBatch.new(form_custom_emoji_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
ensure
redirect_to admin_custom_emojis_path(filter_params)
end
private
def set_custom_emoji
@custom_emoji = CustomEmoji.find(params[:id])
end
def set_filter_params
@filter_params = filter_params.to_hash.symbolize_keys
end
def resource_params
params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker)
end
@ -103,12 +52,29 @@ module Admin
end
def filter_params
params.permit(
:local,
:remote,
:by_domain,
:shortcode
)
params.slice(:local, :remote, :by_domain, :shortcode, :page).permit(:local, :remote, :by_domain, :shortcode, :page)
end
def action_from_button
if params[:update]
'update'
elsif params[:list]
'list'
elsif params[:unlist]
'unlist'
elsif params[:enable]
'enable'
elsif params[:disable]
'disable'
elsif params[:copy]
'copy'
elsif params[:delete]
'delete'
end
end
def form_custom_emoji_batch_params
params.require(:form_custom_emoji_batch).permit(:action, :category_id, :category_name, custom_emoji_ids: [])
end
end
end

View File

@ -5,6 +5,7 @@ module Admin
class DashboardController < BaseController
def index
@users_count = User.count
@pending_users_count = User.pending.count
@registrations_week = Redis.current.get("activity:accounts:local:#{current_week}") || 0
@logins_week = Redis.current.pfcount("activity:logins:#{current_week}")
@interactions_week = Redis.current.get("activity:interactions:#{current_week}") || 0
@ -19,7 +20,7 @@ module Admin
@redis_version = redis_info['redis_version']
@reports_count = Report.unresolved.count
@queue_backlog = Sidekiq::Stats.new.enqueued
@recent_users = User.confirmed.recent.includes(:account).limit(4)
@recent_users = User.confirmed.recent.includes(:account).limit(8)
@database_size = ActiveRecord::Base.connection.execute('SELECT pg_database_size(current_database())').first['pg_database_size']
@redis_size = redis_info['used_memory']
@ldap_enabled = ENV['LDAP_ENABLED'] == 'true'
@ -27,9 +28,14 @@ module Admin
@saml_enabled = ENV['SAML_ENABLED'] == 'true'
@pam_enabled = ENV['PAM_ENABLED'] == 'true'
@hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true'
@trending_hashtags = TrendingTags.get(7)
@trending_hashtags = TrendingTags.get(10, filtered: false)
@pending_tags_count = Tag.pending_review.count
@authorized_fetch = authorized_fetch_mode?
@whitelist_enabled = whitelist_mode?
@profile_directory = Setting.profile_directory
@timeline_preview = Setting.timeline_preview
@spam_check_enabled = Setting.spam_check_enabled
@trends_enabled = Setting.trends
end
private
@ -39,7 +45,13 @@ module Admin
end
def redis_info
@redis_info ||= Redis.current.info
@redis_info ||= begin
if Redis.current.is_a?(Redis::Namespace)
Redis.current.redis.info
else
Redis.current.info
end
end
end
end
end

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
class Admin::DomainAllowsController < Admin::BaseController
before_action :set_domain_allow, only: [:destroy]
def new
authorize :domain_allow, :create?
@domain_allow = DomainAllow.new(domain: params[:_domain])
end
def create
authorize :domain_allow, :create?
@domain_allow = DomainAllow.new(resource_params)
if @domain_allow.save
log_action :create, @domain_allow
redirect_to admin_instances_path, notice: I18n.t('admin.domain_allows.created_msg')
else
render :new
end
end
def destroy
authorize @domain_allow, :destroy?
UnallowDomainService.new.call(@domain_allow)
redirect_to admin_instances_path, notice: I18n.t('admin.domain_allows.destroyed_msg')
end
private
def set_domain_allow
@domain_allow = DomainAllow.find(params[:id])
end
def resource_params
params.require(:domain_allow).permit(:domain)
end
end

View File

@ -2,13 +2,17 @@
module Admin
class DomainBlocksController < BaseController
before_action :set_domain_block, only: [:show, :destroy]
before_action :set_domain_block, only: [:show, :destroy, :edit, :update]
def new
authorize :domain_block, :create?
@domain_block = DomainBlock.new(domain: params[:_domain])
end
def edit
authorize :domain_block, :create?
end
def create
authorize :domain_block, :create?
@ -35,6 +39,22 @@ module Admin
end
end
def update
authorize :domain_block, :create?
@domain_block.update(update_params)
severity_changed = @domain_block.severity_changed?
if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id, severity_changed)
log_action :create, @domain_block
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
else
render :edit
end
end
def show
authorize @domain_block, :show?
end
@ -52,8 +72,12 @@ module Admin
@domain_block = DomainBlock.find(params[:id])
end
def update_params
params.require(:domain_block).permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment)
end
def resource_params
params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports)
params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment)
end
end
end

View File

@ -2,6 +2,10 @@
module Admin
class InstancesController < BaseController
before_action :set_domain_block, only: :show
before_action :set_domain_allow, only: :show
before_action :set_instance, only: :show
def index
authorize :instance, :index?
@ -11,20 +15,40 @@ module Admin
def show
authorize :instance, :show?
@instance = Instance.new(Account.by_domain_accounts.find_by(domain: params[:id]) || DomainBlock.find_by!(domain: params[:id]))
@following_count = Follow.where(account: Account.where(domain: params[:id])).count
@followers_count = Follow.where(target_account: Account.where(domain: params[:id])).count
@reports_count = Report.where(target_account: Account.where(domain: params[:id])).count
@blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count
@available = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url)
@media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size)
@domain_block = DomainBlock.rule_for(params[:id])
@private_comment = @domain_block&.private_comment
@public_comment = @domain_block&.public_comment
end
private
def set_domain_block
@domain_block = DomainBlock.rule_for(params[:id])
end
def set_domain_allow
@domain_allow = DomainAllow.rule_for(params[:id])
end
def set_instance
resource = Account.by_domain_accounts.find_by(domain: params[:id])
resource ||= @domain_block
resource ||= @domain_allow
if resource
@instance = Instance.new(resource)
else
not_found
end
end
def filtered_instances
InstanceFilter.new(filter_params).results
InstanceFilter.new(whitelist_mode? ? { allowed: true } : filter_params).results
end
def paginated_instances

View File

@ -3,6 +3,7 @@
module Admin
class RelaysController < BaseController
before_action :set_relay, except: [:index, :new, :create]
before_action :require_signatures_enabled!, only: [:new, :create, :enable]
def index
authorize :relay, :update?
@ -11,7 +12,7 @@ module Admin
def new
authorize :relay, :update?
@relay = Relay.new(inbox_url: Relay::PRESET_RELAY)
@relay = Relay.new
end
def create
@ -54,5 +55,9 @@ module Admin
def resource_params
params.require(:relay).permit(:inbox_url)
end
def require_signatures_enabled!
redirect_to admin_relays_path, alert: I18n.t('admin.relays.signatures_not_enabled') if authorized_fetch_mode?
end
end
end

View File

@ -5,7 +5,7 @@ module Admin
before_action :set_report_note, only: [:destroy]
def create
authorize ReportNote, :create?
authorize :report_note, :create?
@report_note = current_account.report_notes.new(resource_params)
@report = @report_note.report
@ -26,8 +26,7 @@ module Admin
redirect_to admin_report_path(@report), notice: I18n.t('admin.report_notes.created_msg')
else
@report_notes = @report.notes.latest
@report_history = @report.history
@report_notes = (@report.notes.latest + @report.history + @report.target_account.targeted_account_warnings.latest.custom).sort_by(&:created_at)
@form = Form::StatusBatch.new
render template: 'admin/reports/show'

View File

@ -2,43 +2,102 @@
module Admin
class TagsController < BaseController
before_action :set_tags, only: :index
before_action :set_tag, except: :index
before_action :set_filter_params
before_action :set_tag, except: [:index, :batch, :approve_all, :reject_all]
before_action :set_usage_by_domain, except: [:index, :batch, :approve_all, :reject_all]
before_action :set_counters, except: [:index, :batch, :approve_all, :reject_all]
def index
authorize :tag, :index?
@tags = filtered_tags.page(params[:page])
@form = Form::TagBatch.new
end
def hide
authorize @tag, :hide?
@tag.account_tag_stat.update!(hidden: true)
redirect_to admin_tags_path(@filter_params)
def batch
@form = Form::TagBatch.new(form_tag_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
ensure
redirect_to admin_tags_path(filter_params)
end
def unhide
authorize @tag, :unhide?
@tag.account_tag_stat.update!(hidden: false)
redirect_to admin_tags_path(@filter_params)
def approve_all
Form::TagBatch.new(current_account: current_account, tag_ids: Tag.pending_review.pluck(:id), action: 'approve').save
redirect_to admin_tags_path(filter_params)
end
def reject_all
Form::TagBatch.new(current_account: current_account, tag_ids: Tag.pending_review.pluck(:id), action: 'reject').save
redirect_to admin_tags_path(filter_params)
end
def show
authorize @tag, :show?
end
def update
authorize @tag, :update?
if @tag.update(tag_params.merge(reviewed_at: Time.now.utc))
redirect_to admin_tag_path(@tag.id), notice: I18n.t('admin.tags.updated_msg')
else
render :show
end
end
private
def set_tags
@tags = Tag.discoverable
@tags.merge!(Tag.hidden) if filter_params[:hidden]
end
def set_tag
@tag = Tag.find(params[:id])
end
def set_filter_params
@filter_params = filter_params.to_hash.symbolize_keys
def set_usage_by_domain
@usage_by_domain = @tag.statuses
.with_public_visibility
.excluding_silenced_accounts
.where(Status.arel_table[:id].gteq(Mastodon::Snowflake.id_at(Time.now.utc.beginning_of_day)))
.joins(:account)
.group('accounts.domain')
.reorder('statuses_count desc')
.pluck('accounts.domain, count(*) AS statuses_count')
end
def set_counters
@accounts_today = @tag.history.first[:accounts]
@accounts_week = Redis.current.pfcount(*current_week_days.map { |day| "activity:tags:#{@tag.id}:#{day}:accounts" })
end
def filtered_tags
TagFilter.new(filter_params).results
end
def filter_params
params.permit(:hidden)
params.slice(:directory, :reviewed, :unreviewed, :pending_review, :page, :popular, :active, :name).permit(:directory, :reviewed, :unreviewed, :pending_review, :page, :popular, :active, :name)
end
def tag_params
params.require(:tag).permit(:name, :trendable, :usable, :listable)
end
def current_week_days
now = Time.now.utc.beginning_of_day.to_date
(Date.commercial(now.cwyear, now.cweek)..now).map do |date|
date.to_time(:utc).beginning_of_day.to_i
end
end
def form_tag_batch_params
params.require(:form_tag_batch).permit(:action, tag_ids: [])
end
def action_from_button
if params[:approve]
'approve'
elsif params[:reject]
'reject'
end
end
end
end

View File

@ -8,6 +8,7 @@ module Admin
authorize @user, :disable_2fa?
@user.disable_two_factor!
log_action :disable_2fa, @user
UserMailer.two_factor_disabled(@user).deliver_later!
redirect_to admin_accounts_path
end

View File

@ -7,12 +7,15 @@ class Api::BaseController < ApplicationController
include RateLimitHeaders
skip_before_action :store_current_location
skip_before_action :check_user_permissions
skip_before_action :require_functional!
before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access?
before_action :set_cache_headers
protect_from_forgery with: :null_session
skip_around_action :set_locale
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
render json: { error: e.to_s }, status: 422
end
@ -33,6 +36,14 @@ class Api::BaseController < ApplicationController
render json: { error: 'This action is not allowed' }, status: 403
end
rescue_from Mastodon::RaceConditionError do
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
end
rescue_from ActionController::ParameterMissing do |e|
render json: { error: e.to_s }, status: 400
end
def doorkeeper_unauthorized_render_options(error: nil)
{ json: { error: (error.try(:description) || 'Not authorized') } }
end
@ -69,6 +80,10 @@ class Api::BaseController < ApplicationController
nil
end
def require_authenticated_user!
render json: { error: 'This API requires an authenticated user' }, status: 401 unless current_user
end
def require_user!
if !current_user
render json: { error: 'This method requires an authenticated user' }, status: 422
@ -94,4 +109,8 @@ class Api::BaseController < ApplicationController
def set_cache_headers
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
end
def disallow_unauthenticated_api_access?
authorized_fetch_mode?
end
end

View File

@ -1,10 +1,9 @@
# frozen_string_literal: true
class Api::ProofsController < Api::BaseController
before_action :set_account
include AccountOwnedConcern
before_action :set_provider
before_action :check_account_approval
before_action :check_account_suspension
def index
render json: @account, serializer: @provider.serializer_class
@ -16,15 +15,7 @@ class Api::ProofsController < Api::BaseController
@provider = ProofProvider.find(params[:provider]) || raise(ActiveRecord::RecordNotFound)
end
def set_account
@account = Account.find_local!(params[:username])
end
def check_account_approval
not_found if @account.user_pending?
end
def check_account_suspension
gone if @account.suspended?
def username_param
params[:username]
end
end

View File

@ -1,73 +0,0 @@
# frozen_string_literal: true
class Api::PushController < Api::BaseController
include SignatureVerification
def update
response, status = process_push_request
render plain: response, status: status
end
private
def process_push_request
case hub_mode
when 'subscribe'
Pubsubhubbub::SubscribeService.new.call(account_from_topic, hub_callback, hub_secret, hub_lease_seconds, verified_domain)
when 'unsubscribe'
Pubsubhubbub::UnsubscribeService.new.call(account_from_topic, hub_callback)
else
["Unknown mode: #{hub_mode}", 422]
end
end
def hub_mode
params['hub.mode']
end
def hub_topic
params['hub.topic']
end
def hub_callback
params['hub.callback']
end
def hub_lease_seconds
params['hub.lease_seconds']
end
def hub_secret
params['hub.secret']
end
def account_from_topic
if hub_topic.present? && local_domain? && account_feed_path?
Account.find_local(hub_topic_params[:username])
end
end
def hub_topic_params
@_hub_topic_params ||= Rails.application.routes.recognize_path(hub_topic_uri.path)
end
def hub_topic_uri
@_hub_topic_uri ||= Addressable::URI.parse(hub_topic).normalize
end
def local_domain?
TagManager.instance.web_domain?(hub_topic_domain)
end
def verified_domain
return signed_request_account.domain if signed_request_account
end
def hub_topic_domain
hub_topic_uri.host + (hub_topic_uri.port ? ":#{hub_topic_uri.port}" : '')
end
def account_feed_path?
hub_topic_params[:controller] == 'accounts' && hub_topic_params[:action] == 'show' && hub_topic_params[:format] == 'atom'
end
end

View File

@ -1,37 +0,0 @@
# frozen_string_literal: true
class Api::SalmonController < Api::BaseController
include SignatureVerification
before_action :set_account
respond_to :txt
def update
if verify_payload?
process_salmon
head 202
elsif payload.present?
render plain: signature_verification_failure_reason, status: 401
else
head 400
end
end
private
def set_account
@account = Account.find(params[:id])
end
def payload
@_payload ||= request.body.read
end
def verify_payload?
payload.present? && VerifySalmonService.new.call(payload)
end
def process_salmon
SalmonWorker.perform_async(@account.id, payload.force_encoding('UTF-8'))
end
end

View File

@ -1,51 +0,0 @@
# frozen_string_literal: true
class Api::SubscriptionsController < Api::BaseController
before_action :set_account
respond_to :txt
def show
if subscription.valid?(params['hub.topic'])
@account.update(subscription_expires_at: future_expires)
render plain: encoded_challenge, status: 200
else
head 404
end
end
def update
if subscription.verify(body, request.headers['HTTP_X_HUB_SIGNATURE'])
ProcessingWorker.perform_async(@account.id, body.force_encoding('UTF-8'))
end
head 200
end
private
def subscription
@_subscription ||= @account.subscription(
api_subscription_url(@account.id)
)
end
def body
@_body ||= request.body.read
end
def encoded_challenge
HTMLEntities.new.encode(params['hub.challenge'])
end
def future_expires
Time.now.utc + lease_seconds_or_default
end
def lease_seconds_or_default
(params['hub.lease_seconds'] || 1.day).to_i.seconds
end
def set_account
@account = Account.find(params[:id])
end
end

View File

@ -29,14 +29,13 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
def account_statuses
statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses
statuses = statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
statuses.merge!(only_media_scope) if truthy_param?(:only_media)
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
statuses.merge!(hashtag_scope) if params[:tagged].present?
statuses
statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def permitted_account_statuses
@ -58,6 +57,8 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
end
def pinned_scope
return Status.none if @account.blocking?(current_account)
@account.pinned_statuses
end

View File

@ -12,6 +12,8 @@ class Api::V1::AccountsController < Api::BaseController
before_action :check_account_suspension, only: [:show]
before_action :check_enabled_registrations, only: [:create]
skip_before_action :require_authenticated_user!, only: :create
respond_to :json
def show
@ -31,7 +33,7 @@ class Api::V1::AccountsController < Api::BaseController
def follow
FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs))
options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
end
@ -76,7 +78,7 @@ class Api::V1::AccountsController < Api::BaseController
end
def account_params
params.permit(:username, :email, :password, :agreement, :locale)
params.permit(:username, :email, :password, :agreement, :locale, :reason)
end
def check_enabled_registrations

View File

@ -58,7 +58,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController
def reject
authorize @account.user, :reject?
SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true)
SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false)
render json: @account, serializer: REST::Admin::AccountSerializer
end

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class Api::V1::AppsController < Api::BaseController
skip_before_action :require_authenticated_user!
def create
@app = Doorkeeper::Application.create!(application_options)
render json: @app, serializer: REST::ApplicationSerializer

View File

@ -6,8 +6,7 @@ class Api::V1::CustomEmojisController < Api::BaseController
skip_before_action :set_cache_headers
def index
render_cached_json('api:v1:custom_emojis', expires_in: 1.minute) do
ActiveModelSerializers::SerializableResource.new(CustomEmoji.local.where(disabled: false), each_serializer: REST::CustomEmojiSerializer)
end
expires_in 3.minutes, public: true
render_with_cache(each_serializer: REST::CustomEmojiSerializer) { CustomEmoji.listed.includes(:category) }
end
end

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
class Api::V1::DirectoriesController < Api::BaseController
before_action :require_enabled!
before_action :set_accounts
def show
render json: @accounts, each_serializer: REST::AccountSerializer
end
private
def require_enabled!
return not_found unless Setting.profile_directory
end
def set_accounts
@accounts = accounts_scope.offset(params[:offset]).limit(limit_param(DEFAULT_ACCOUNTS_LIMIT))
end
def accounts_scope
Account.discoverable.tap do |scope|
scope.merge!(Account.local) if truthy_param?(:local)
scope.merge!(Account.by_recent_status) if params[:order].blank? || params[:order] == 'active'
scope.merge!(Account.order(id: :desc)) if params[:order] == 'new'
scope.merge!(Account.not_excluded_by_account(current_account)) if current_account
scope.merge!(Account.not_domain_blocked_by_account(current_account)) if current_account && !truthy_param?(:local)
end
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
before_action :require_user!
before_action :set_most_used_tags, only: :index
respond_to :json
def index
render json: @most_used_tags, each_serializer: REST::TagSerializer
end
private
def set_most_used_tags
@most_used_tags = Tag.most_used(current_account).where.not(id: current_account.featured_tags).limit(10)
end
end

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
class Api::V1::FeaturedTagsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index
before_action :require_user!
before_action :set_featured_tags, only: :index
before_action :set_featured_tag, except: [:index, :create]
def index
render json: @featured_tags, each_serializer: REST::FeaturedTagSerializer
end
def create
@featured_tag = current_account.featured_tags.new(featured_tag_params)
@featured_tag.reset_data
@featured_tag.save!
render json: @featured_tag, serializer: REST::FeaturedTagSerializer
end
def destroy
@featured_tag.destroy!
render_empty
end
private
def set_featured_tag
@featured_tag = current_account.featured_tags.find(params[:id])
end
def set_featured_tags
@featured_tags = current_account.featured_tags.order(statuses_count: :desc)
end
def featured_tag_params
params.permit(:name)
end
end

View File

@ -14,12 +14,12 @@ class Api::V1::FollowRequestsController < Api::BaseController
def authorize
AuthorizeFollowService.new.call(account, current_account)
NotifyService.new.call(current_account, Follow.find_by(account: account, target_account: current_account))
render_empty
render json: account, serializer: REST::RelationshipSerializer, relationships: relationships
end
def reject
RejectFollowService.new.call(account, current_account)
render_empty
render json: account, serializer: REST::RelationshipSerializer, relationships: relationships
end
private
@ -28,6 +28,10 @@ class Api::V1::FollowRequestsController < Api::BaseController
Account.find(params[:id])
end
def relationships(**options)
AccountRelationshipsPresenter.new([params[:id]], current_user.account_id, options)
end
def load_accounts
default_accounts.merge(paginated_follow_requests).to_a
end

View File

@ -1,31 +0,0 @@
# frozen_string_literal: true
class Api::V1::FollowsController < Api::BaseController
before_action -> { doorkeeper_authorize! :follow, :'write:follows' }
before_action :require_user!
respond_to :json
def create
raise ActiveRecord::RecordNotFound if follow_params[:uri].blank?
@account = FollowService.new.call(current_user.account, target_uri).try(:target_account)
if @account.nil?
username, domain = target_uri.split('@')
@account = Account.find_remote!(username, domain)
end
render json: @account, serializer: REST::AccountSerializer
end
private
def target_uri
follow_params[:uri].strip.gsub(/\A@/, '')
end
def follow_params
params.permit(:uri)
end
end

View File

@ -2,12 +2,14 @@
class Api::V1::Instances::ActivityController < Api::BaseController
before_action :require_enabled_api!
skip_before_action :set_cache_headers
respond_to :json
def show
render_cached_json('api:v1:instances:activity:show', expires_in: 1.day) { activity }
expires_in 1.day, public: true
render_with_cache json: :activity, expires_in: 1.day
end
private
@ -32,6 +34,6 @@ class Api::V1::Instances::ActivityController < Api::BaseController
end
def require_enabled_api!
head 404 unless Setting.activity_api_enabled
head 404 unless Setting.activity_api_enabled && !whitelist_mode?
end
end

View File

@ -2,17 +2,19 @@
class Api::V1::Instances::PeersController < Api::BaseController
before_action :require_enabled_api!
skip_before_action :set_cache_headers
respond_to :json
def index
render_cached_json('api:v1:instances:peers:index', expires_in: 1.day) { Account.remote.domains }
expires_in 1.day, public: true
render_with_cache(expires_in: 1.day) { Account.remote.domains }
end
private
def require_enabled_api!
head 404 unless Setting.peers_api_enabled
head 404 unless Setting.peers_api_enabled && !whitelist_mode?
end
end

View File

@ -2,11 +2,11 @@
class Api::V1::InstancesController < Api::BaseController
respond_to :json
skip_before_action :set_cache_headers
def show
render_cached_json('api:v1:instances', expires_in: 5.minutes) do
ActiveModelSerializers::SerializableResource.new({}, serializer: REST::InstanceSerializer)
end
expires_in 3.minutes, public: true
render_with_cache json: {}, serializer: REST::InstanceSerializer, root: 'instance'
end
end

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
class Api::V1::MarkersController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:index]
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, except: [:index]
before_action :require_user!
def index
@markers = current_user.markers.where(timeline: Array(params[:timeline])).each_with_object({}) { |marker, h| h[marker.timeline] = marker }
render json: serialize_map(@markers)
end
def create
Marker.transaction do
@markers = {}
resource_params.each_pair do |timeline, timeline_params|
@markers[timeline] = current_user.markers.find_or_initialize_by(timeline: timeline)
@markers[timeline].update!(timeline_params)
end
end
render json: serialize_map(@markers)
rescue ActiveRecord::StaleObjectError
render json: { error: 'Conflict during update, please try again' }, status: 409
end
private
def serialize_map(map)
serialized = {}
map.each_pair do |key, value|
serialized[key] = ActiveModelSerializers::SerializableResource.new(value, serializer: REST::MarkerSerializer).as_json
end
Oj.dump(serialized)
end
def resource_params
params.slice(*Marker::TIMELINES).permit(*Marker::TIMELINES.map { |timeline| { timeline.to_sym => [:last_read_id] } })
end
end

View File

@ -21,7 +21,7 @@ class Api::V1::ReportsController < Api::BaseController
private
def reported_status_ids
reported_account.statuses.find(status_ids).pluck(:id)
reported_account.statuses.with_discarded.find(status_ids).pluck(:id)
end
def status_ids

View File

@ -1,32 +0,0 @@
# frozen_string_literal: true
class Api::V1::SearchController < Api::BaseController
include Authorization
RESULTS_LIMIT = 20
before_action -> { doorkeeper_authorize! :read, :'read:search' }
before_action :require_user!
respond_to :json
def index
@search = Search.new(search_results)
render json: @search, serializer: REST::SearchSerializer
end
private
def search_results
SearchService.new.call(
params[:q],
current_account,
limit_param(RESULTS_LIMIT),
search_params.merge(resolve: truthy_param?(:resolve))
)
end
def search_params
params.permit(:type, :offset, :min_id, :max_id, :account_id)
end
end

View File

@ -18,6 +18,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
@reblogs_map = { @status.id => false }
authorize status_for_destroy, :unreblog?
status_for_destroy.discard
RemovalWorker.perform_async(status_for_destroy.id)
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_user&.account_id, reblogs_map: @reblogs_map)
@ -30,7 +31,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
end
def status_for_destroy
current_user.account.statuses.where(reblog_of_id: params[:status_id]).first!
@status_for_destroy ||= current_user.account.statuses.where(reblog_of_id: params[:status_id]).first!
end
def reblog_params

View File

@ -5,8 +5,8 @@ class Api::V1::StatusesController < Api::BaseController
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy]
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy]
before_action :require_user!, except: [:show, :context, :card]
before_action :set_status, only: [:show, :context, :card]
before_action :require_user!, except: [:show, :context]
before_action :set_status, only: [:show, :context]
respond_to :json
@ -33,16 +33,6 @@ class Api::V1::StatusesController < Api::BaseController
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
end
def card
@card = @status.preview_cards.first
if @card.nil?
render_empty
else
render json: @card, serializer: REST::PreviewCardSerializer
end
end
def create
@status = PostStatusService.new.call(current_user.account,
text: status_params[:status],
@ -64,7 +54,8 @@ class Api::V1::StatusesController < Api::BaseController
@status = Status.where(account_id: current_user.account).find(params[:id])
authorize @status, :destroy?
RemovalWorker.perform_async(@status.id)
@status.discard
RemovalWorker.perform_async(@status.id, redraft: true)
render json: @status, serializer: REST::StatusSerializer, source_requested: true
end

View File

@ -1,63 +0,0 @@
# frozen_string_literal: true
class Api::V1::Timelines::DirectController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:show]
before_action :require_user!, only: [:show]
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
respond_to :json
def show
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
end
private
def load_statuses
cached_direct_statuses
end
def cached_direct_statuses
cache_collection direct_statuses, Status
end
def direct_statuses
direct_timeline_statuses
end
def direct_timeline_statuses
# this query requires built in pagination.
Status.as_direct_timeline(
current_account,
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id],
true # returns array of cache_ids object
)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def pagination_params(core_params)
params.permit(:local, :limit).merge(core_params)
end
def next_path
api_v1_timelines_direct_url pagination_params(max_id: pagination_max_id)
end
def prev_path
api_v1_timelines_direct_url pagination_params(since_id: pagination_since_id)
end
def pagination_max_id
@statuses.last.id
end
def pagination_since_id
@statuses.first.id
end
end

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true
class Api::V1::Timelines::PublicController < Api::BaseController
before_action :require_user!, only: [:show], if: :require_auth?
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
respond_to :json
@ -12,6 +13,10 @@ class Api::V1::Timelines::PublicController < Api::BaseController
private
def require_auth?
!Setting.timeline_preview
end
def load_statuses
cached_public_statuses
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class Api::V1::TrendsController < Api::BaseController
before_action :set_tags
respond_to :json
def index
render json: @tags, each_serializer: REST::TagSerializer
end
private
def set_tags
@tags = TrendingTags.get(limit_param(10))
end
end

View File

@ -1,8 +1,32 @@
# frozen_string_literal: true
class Api::V2::SearchController < Api::V1::SearchController
class Api::V2::SearchController < Api::BaseController
include Authorization
RESULTS_LIMIT = 20
before_action -> { doorkeeper_authorize! :read, :'read:search' }
before_action :require_user!
respond_to :json
def index
@search = Search.new(search_results)
render json: @search, serializer: REST::V2::SearchSerializer
render json: @search, serializer: REST::SearchSerializer
end
private
def search_results
SearchService.new.call(
params[:q],
current_account,
limit_param(RESULTS_LIMIT),
search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed))
)
end
def search_params
params.permit(:type, :offset, :min_id, :max_id, :account_id)
end
end

View File

@ -10,21 +10,29 @@ class ApplicationController < ActionController::Base
include Localized
include UserTrackingConcern
include SessionTrackingConcern
include CacheConcern
include DomainControlHelper
helper_method :current_account
helper_method :current_session
helper_method :current_theme
helper_method :single_user_mode?
helper_method :use_seamless_external_login?
helper_method :whitelist_mode?
rescue_from ActionController::RoutingError, with: :not_found
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
rescue_from ActionController::UnknownFormat, with: :not_acceptable
rescue_from ActionController::ParameterMissing, with: :bad_request
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from Mastodon::NotPermittedError, with: :forbidden
rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
rescue_from Mastodon::RaceConditionError, with: :service_unavailable
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
before_action :check_user_permissions, if: :user_signed_in?
before_action :require_functional!, if: :user_signed_in?
skip_before_action :verify_authenticity_token, only: :raise_not_found
def raise_not_found
raise ActionController::RoutingError, "No route matches #{params[:unmatched_route]}"
@ -33,7 +41,15 @@ class ApplicationController < ActionController::Base
private
def https_enabled?
Rails.env.production?
Rails.env.production? && !request.path.start_with?('/health')
end
def authorized_fetch_mode?
ENV['AUTHORIZED_FETCH'] == 'true' || Rails.configuration.x.whitelist_mode
end
def public_fetch_mode?
!authorized_fetch_mode?
end
def store_current_location
@ -48,8 +64,8 @@ class ApplicationController < ActionController::Base
forbidden unless current_user&.staff?
end
def check_user_permissions
forbidden if current_user.disabled? || current_user.account.suspended?
def require_functional!
redirect_to edit_user_registration_path unless current_user.functional?
end
def after_sign_out_path_for(_resource_or_scope)
@ -82,8 +98,20 @@ class ApplicationController < ActionController::Base
respond_with_error(406)
end
def bad_request
respond_with_error(400)
end
def internal_server_error
respond_with_error(500)
end
def service_unavailable
respond_with_error(503)
end
def single_user_mode?
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.exists?
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists?
end
def use_seamless_external_login?
@ -107,51 +135,10 @@ class ApplicationController < ActionController::Base
current_user.setting_theme
end
def cache_collection(raw, klass)
return raw unless klass.respond_to?(:with_includes)
raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id)
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!)
unless uncached_ids.empty?
uncached = klass.where(id: uncached_ids).with_includes.each_with_object({}) { |item, h| h[item.id] = item }
uncached.each_value do |item|
Rails.cache.write(item, item)
end
end
raw.map { |item| cached_keys_with_value[item.id] || uncached[item.id] }.compact
end
def respond_with_error(code)
respond_to do |format|
format.any { head code }
format.html { render "errors/#{code}", layout: 'error', status: code }
end
end
def render_cached_json(cache_key, **options)
options[:expires_in] ||= 3.minutes
cache_public = options.key?(:public) ? options.delete(:public) : true
content_type = options.delete(:content_type) || 'application/json'
data = Rails.cache.fetch(cache_key, { raw: true }.merge(options)) do
yield.to_json
end
expires_in options[:expires_in], public: cache_public
render json: data, content_type: content_type
end
def set_cache_headers
response.headers['Vary'] = 'Accept'
end
def mark_cacheable!
expires_in 0, public: true
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
class Auth::ChallengesController < ApplicationController
include ChallengableConcern
layout 'auth'
before_action :authenticate_user!
skip_before_action :require_functional!
def create
if challenge_passed?
session[:challenge_passed_at] = Time.now.utc
redirect_to challenge_params[:return_to]
else
@challenge = Form::Challenge.new(return_to: challenge_params[:return_to])
flash.now[:alert] = I18n.t('challenge.invalid_password')
render_challenge
end
end
end

View File

@ -4,32 +4,36 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
layout 'auth'
before_action :set_body_classes
before_action :set_user, only: [:finish_signup]
before_action :require_unconfirmed!
def finish_signup
return unless request.patch? && params[:user]
skip_before_action :require_functional!
if @user.update(user_params)
@user.skip_reconfirmation!
bypass_sign_in(@user)
redirect_to root_path, notice: I18n.t('devise.confirmations.send_instructions')
else
@show_errors = true
end
def new
super
resource.email = current_user.unconfirmed_email || current_user.email if user_signed_in?
end
private
def set_user
@user = current_user
def require_unconfirmed!
redirect_to edit_user_registration_path if user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank?
end
def set_body_classes
@body_classes = 'lighter'
end
def user_params
params.require(:user).permit(:email)
def after_resending_confirmation_instructions_path_for(_resource_name)
if user_signed_in?
if current_user.confirmed? && current_user.approved?
edit_user_registration_path
else
auth_setup_path
end
else
new_user_session_path
end
end
def after_confirmation_path_for(_resource_name, user)

View File

@ -27,7 +27,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
if resource.email_verified?
root_path
else
finish_signup_path
auth_setup_path(missing_email: '1')
end
end
end

View File

@ -9,6 +9,9 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :set_sessions, only: [:edit, :update]
before_action :set_instance_presenter, only: [:new, :create, :update]
before_action :set_body_classes, only: [:new, :create, :edit, :update]
before_action :require_not_suspended!, only: [:update]
skip_before_action :require_functional!, only: [:edit, :update]
def new
super(&:build_invite_request)
@ -43,7 +46,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end
def after_sign_up_path_for(_resource)
new_user_session_path
auth_setup_path
end
def after_sign_in_path_for(_resource)
@ -102,4 +105,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController
def set_sessions
@sessions = current_user.session_activations
end
def require_not_suspended!
forbidden if current_account.suspended?
end
end

View File

@ -6,8 +6,10 @@ class Auth::SessionsController < Devise::SessionsController
layout 'auth'
skip_before_action :require_no_authentication, only: [:create]
skip_before_action :check_user_permissions, only: [:destroy]
skip_before_action :require_functional!
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
before_action :set_instance_presenter, only: [:new]
before_action :set_body_classes
@ -29,6 +31,7 @@ class Auth::SessionsController < Devise::SessionsController
def destroy
tmp_stored_location = stored_location_for(:user)
super
session.delete(:challenge_passed_at)
flash.delete(:notice)
store_location_for(:user, tmp_stored_location) if continue_after?
end
@ -38,12 +41,10 @@ class Auth::SessionsController < Devise::SessionsController
def find_user
if session[:otp_user_id]
User.find(session[:otp_user_id])
elsif user_params[:email]
if use_seamless_external_login? && Devise.check_at_sign && user_params[:email].index('@').nil?
User.joins(:account).find_by(accounts: { username: user_params[:email] })
else
User.find_for_authentication(email: user_params[:email])
end
user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication
user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication
user ||= User.find_for_authentication(email: user_params[:email])
end
end
@ -70,13 +71,13 @@ class Auth::SessionsController < Devise::SessionsController
end
def two_factor_enabled?
find_user.try(:otp_required_for_login?)
find_user&.otp_required_for_login?
end
def valid_otp_attempt?(user)
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
rescue OpenSSL::Cipher::CipherError => _error
rescue OpenSSL::Cipher::CipherError
false
end
@ -85,7 +86,10 @@ class Auth::SessionsController < Devise::SessionsController
if user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user)
elsif user&.valid_password?(user_params[:password])
elsif user.present? && (user.encrypted_password.blank? || user.valid_password?(user_params[:password]))
# If encrypted_password is blank, we got the user from LDAP or PAM,
# so credentials are already valid
prompt_for_two_factor(user)
end
end
@ -103,6 +107,7 @@ class Auth::SessionsController < Devise::SessionsController
def prompt_for_two_factor(user)
session[:otp_user_id] = user.id
@body_classes = 'lighter'
render :two_factor
end

View File

@ -0,0 +1,58 @@
# frozen_string_literal: true
class Auth::SetupController < ApplicationController
layout 'auth'
before_action :authenticate_user!
before_action :require_unconfirmed_or_pending!
before_action :set_body_classes
before_action :set_user
skip_before_action :require_functional!
def show
flash.now[:notice] = begin
if @user.pending?
I18n.t('devise.registrations.signed_up_but_pending')
else
I18n.t('devise.registrations.signed_up_but_unconfirmed')
end
end
end
def update
# This allows updating the e-mail without entering a password as is required
# on the account settings page; however, we only allow this for accounts
# that were not confirmed yet
if @user.update(user_params)
redirect_to auth_setup_path, notice: I18n.t('devise.confirmations.send_instructions')
else
render :show
end
end
helper_method :missing_email?
private
def require_unconfirmed_or_pending!
redirect_to root_path if current_user.confirmed? && current_user.approved?
end
def set_user
@user = current_user
end
def set_body_classes
@body_classes = 'lighter'
end
def user_params
params.require(:user).permit(:email)
end
def missing_email?
truthy_param?(:missing_email)
end
end

View File

@ -3,24 +3,19 @@
module AccountControllerConcern
extend ActiveSupport::Concern
include AccountOwnedConcern
FOLLOW_PER_PAGE = 12
included do
layout 'public'
before_action :set_account
before_action :check_account_approval
before_action :check_account_suspension
before_action :set_instance_presenter
before_action :set_link_headers
before_action :set_link_headers, if: -> { request.format.nil? || request.format == :html }
end
private
def set_account
@account = Account.find_local!(username_param)
end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
@ -29,27 +24,15 @@ module AccountControllerConcern
response.headers['Link'] = LinkHeader.new(
[
webfinger_account_link,
atom_account_url_link,
actor_url_link,
]
)
end
def username_param
params[:account_username]
end
def webfinger_account_link
[
webfinger_account_url,
[%w(rel lrdd), %w(type application/xrd+xml)],
]
end
def atom_account_url_link
[
account_url(@account, format: 'atom'),
[%w(rel alternate), %w(type application/atom+xml)],
[%w(rel lrdd), %w(type application/jrd+json)],
]
end
@ -63,15 +46,4 @@ module AccountControllerConcern
def webfinger_account_url
webfinger_url(resource: @account.to_webfinger_s)
end
def check_account_approval
not_found if @account.user_pending?
end
def check_account_suspension
if @account.suspended?
expires_in(3.minutes, public: true)
gone
end
end
end

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
module AccountOwnedConcern
extend ActiveSupport::Concern
included do
before_action :authenticate_user!, if: -> { whitelist_mode? && request.format != :json }
before_action :set_account, if: :account_required?
before_action :check_account_approval, if: :account_required?
before_action :check_account_suspension, if: :account_required?
end
private
def account_required?
true
end
def set_account
@account = Account.find_local!(username_param)
end
def username_param
params[:account_username]
end
def check_account_approval
not_found if @account.local? && @account.user_pending?
end
def check_account_suspension
expires_in(3.minutes, public: true) && gone if @account.suspended?
end
end

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
module CacheConcern
extend ActiveSupport::Concern
def render_with_cache(**options)
raise ArgumentError, 'only JSON render calls are supported' unless options.key?(:json) || block_given?
key = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields].nil? ? nil : options[:fields].join(',')].compact.join(':')
expires_in = options.delete(:expires_in) || 3.minutes
body = Rails.cache.read(key, raw: true)
if body
render(options.except(:json, :serializer, :each_serializer, :adapter, :fields).merge(json: body))
else
if block_given?
options[:json] = yield
elsif options[:json].is_a?(Symbol)
options[:json] = send(options[:json])
end
render(options)
Rails.cache.write(key, response.body, expires_in: expires_in, raw: true)
end
end
def set_cache_headers
response.headers['Vary'] = public_fetch_mode? ? 'Accept' : 'Accept, Signature'
end
def cache_collection(raw, klass)
return raw unless klass.respond_to?(:with_includes)
raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id)
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!)
unless uncached_ids.empty?
uncached = klass.where(id: uncached_ids).with_includes.each_with_object({}) { |item, h| h[item.id] = item }
uncached.each_value do |item|
Rails.cache.write(item, item)
end
end
raw.map { |item| cached_keys_with_value[item.id] || uncached[item.id] }.compact
end
end

View File

@ -0,0 +1,65 @@
# frozen_string_literal: true
# This concern is inspired by "sudo mode" on GitHub. It
# is a way to re-authenticate a user before allowing them
# to see or perform an action.
#
# Add `before_action :require_challenge!` to actions you
# want to protect.
#
# The user will be shown a page to enter the challenge (which
# is either the password, or just the username when no
# password exists). Upon passing, there is a grace period
# during which no challenge will be asked from the user.
#
# Accessing challenge-protected resources during the grace
# period will refresh the grace period.
module ChallengableConcern
extend ActiveSupport::Concern
CHALLENGE_TIMEOUT = 1.hour.freeze
def require_challenge!
return if skip_challenge?
if challenge_passed_recently?
session[:challenge_passed_at] = Time.now.utc
return
end
@challenge = Form::Challenge.new(return_to: request.url)
if params.key?(:form_challenge)
if challenge_passed?
session[:challenge_passed_at] = Time.now.utc
return
else
flash.now[:alert] = I18n.t('challenge.invalid_password')
render_challenge
end
else
render_challenge
end
end
def render_challenge
@body_classes = 'lighter'
render template: 'auth/challenges/new', layout: 'auth'
end
def challenge_passed?
current_user.valid_password?(challenge_params[:current_password])
end
def skip_challenge?
current_user.encrypted_password.blank?
end
def challenge_passed_recently?
session[:challenge_passed_at].present? && session[:challenge_passed_at] >= CHALLENGE_TIMEOUT.ago
end
def challenge_params
params.require(:form_challenge).permit(:current_password, :return_to)
end
end

View File

@ -5,7 +5,10 @@ module ExportControllerConcern
included do
before_action :authenticate_user!
before_action :require_not_suspended!
before_action :load_export
skip_before_action :require_functional!
end
private
@ -27,4 +30,8 @@ module ExportControllerConcern
def export_filename
"#{controller_name}.csv"
end
def require_not_suspended!
forbidden if current_account.suspended?
end
end

View File

@ -5,12 +5,35 @@
module SignatureVerification
extend ActiveSupport::Concern
include DomainControlHelper
def require_signature!
render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
end
def signed_request?
request.headers['Signature'].present?
end
def signature_verification_failure_reason
return @signature_verification_failure_reason if defined?(@signature_verification_failure_reason)
@signature_verification_failure_reason
end
def signature_verification_failure_code
@signature_verification_failure_code || 401
end
def signature_key_id
raw_signature = request.headers['Signature']
signature_params = {}
raw_signature.split(',').each do |part|
parsed_parts = part.match(/([a-z]+)="([^"]+)"/i)
next if parsed_parts.nil? || parsed_parts.size != 3
signature_params[parsed_parts[1]] = parsed_parts[2]
end
signature_params['keyId']
end
def signed_request_account
@ -123,6 +146,13 @@ module SignatureVerification
end
def account_from_key_id(key_id)
domain = key_id.start_with?('acct:') ? key_id.split('@').last : key_id
if domain_not_allowed?(domain)
@signature_verification_failure_code = 403
return
end
if key_id.start_with?('acct:')
stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, '')) }
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
@ -137,7 +167,7 @@ module SignatureVerification
.with_fallback { nil }
.with_threshold(1)
.with_cool_off_time(5.minutes.seconds)
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) ? handle.call(error) : raise(error) }
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) }
.run
end

View File

@ -0,0 +1,87 @@
# frozen_string_literal: true
module StatusControllerConcern
extend ActiveSupport::Concern
ANCESTORS_LIMIT = 40
DESCENDANTS_LIMIT = 60
DESCENDANTS_DEPTH_LIMIT = 20
def create_descendant_thread(starting_depth, statuses)
depth = starting_depth + statuses.size
if depth < DESCENDANTS_DEPTH_LIMIT
{
statuses: statuses,
starting_depth: starting_depth,
}
else
next_status = statuses.pop
{
statuses: statuses,
starting_depth: starting_depth,
next_status: next_status,
}
end
end
def set_ancestors
@ancestors = @status.reply? ? cache_collection(@status.ancestors(ANCESTORS_LIMIT, current_account), Status) : []
@next_ancestor = @ancestors.size < ANCESTORS_LIMIT ? nil : @ancestors.shift
end
def set_descendants
@max_descendant_thread_id = params[:max_descendant_thread_id]&.to_i
@since_descendant_thread_id = params[:since_descendant_thread_id]&.to_i
descendants = cache_collection(
@status.descendants(
DESCENDANTS_LIMIT,
current_account,
@max_descendant_thread_id,
@since_descendant_thread_id,
DESCENDANTS_DEPTH_LIMIT
),
Status
)
@descendant_threads = []
if descendants.present?
statuses = [descendants.first]
starting_depth = 0
descendants.drop(1).each_with_index do |descendant, index|
if descendants[index].id == descendant.in_reply_to_id
statuses << descendant
else
@descendant_threads << create_descendant_thread(starting_depth, statuses)
# The thread is broken, assume it's a reply to the root status
starting_depth = 0
# ... unless we can find its ancestor in one of the already-processed threads
@descendant_threads.reverse_each do |descendant_thread|
statuses = descendant_thread[:statuses]
index = statuses.find_index do |thread_status|
thread_status.id == descendant.in_reply_to_id
end
if index.present?
starting_depth = descendant_thread[:starting_depth] + index + 1
break
end
end
statuses = [descendant]
end
end
@descendant_threads << create_descendant_thread(starting_depth, statuses)
end
@max_descendant_thread_id = @descendant_threads.pop[:statuses].first.id if descendants.size >= DESCENDANTS_LIMIT
end
end

View File

@ -2,10 +2,12 @@
class CustomCssController < ApplicationController
skip_before_action :store_current_location
skip_before_action :require_functional!
before_action :set_cache_headers
def show
expires_in 3.minutes, public: true
render plain: Setting.custom_css || '', content_type: 'text/css'
end
end

View File

@ -3,12 +3,14 @@
class DirectoriesController < ApplicationController
layout 'public'
before_action :check_enabled
before_action :authenticate_user!, if: :whitelist_mode?
before_action :require_enabled!
before_action :set_instance_presenter
before_action :set_tag, only: :show
before_action :set_tags
before_action :set_accounts
skip_before_action :require_functional!
def index
render :index
end
@ -19,21 +21,18 @@ class DirectoriesController < ApplicationController
private
def check_enabled
def require_enabled!
return not_found unless Setting.profile_directory
end
def set_tag
@tag = Tag.discoverable.find_by!(name: params[:id].downcase)
end
def set_tags
@tags = Tag.discoverable.limit(30).reject { |tag| tag.cached_sample_accounts.empty? }
@tag = Tag.discoverable.find_normalized!(params[:id])
end
def set_accounts
@accounts = Account.discoverable.by_recent_status.page(params[:page]).per(40).tap do |query|
@accounts = Account.local.discoverable.by_recent_status.page(params[:page]).per(20).tap do |query|
query.merge!(Account.tagged_with(@tag.id)) if @tag
query.merge!(Account.not_excluded_by_account(current_account)) if current_account
end
end

View File

@ -7,9 +7,8 @@ class EmojisController < ApplicationController
def show
respond_to do |format|
format.json do
render_cached_json(['activitypub', 'emoji', @emoji], content_type: 'application/activity+json') do
ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter)
end
expires_in 3.minutes, public: true
render_with_cache json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter
end
end
end

View File

@ -2,13 +2,18 @@
class FollowerAccountsController < ApplicationController
include AccountControllerConcern
include SignatureVerification
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
skip_around_action :set_locale, if: -> { request.format == :json }
skip_before_action :require_functional!
def index
respond_to do |format|
format.html do
mark_cacheable! unless user_signed_in?
expires_in 0, public: true unless user_signed_in?
next if @account.user_hides_network?
@ -17,9 +22,9 @@ class FollowerAccountsController < ApplicationController
end
format.json do
raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
raise Mastodon::NotPermittedError if page_requested? && @account.user_hides_network?
expires_in 3.minutes, public: true if params[:page].blank?
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
render json: collection_presenter,
serializer: ActivityPub::CollectionSerializer,
@ -35,12 +40,16 @@ class FollowerAccountsController < ApplicationController
@follows ||= Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
end
def page_requested?
params[:page].present?
end
def page_url(page)
account_followers_url(@account, page: page) unless page.nil?
end
def collection_presenter
if params[:page].present?
if page_requested?
ActivityPub::CollectionPresenter.new(
id: account_followers_url(@account, page: params.fetch(:page, 1)),
type: :ordered,

View File

@ -2,13 +2,18 @@
class FollowingAccountsController < ApplicationController
include AccountControllerConcern
include SignatureVerification
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
skip_around_action :set_locale, if: -> { request.format == :json }
skip_before_action :require_functional!
def index
respond_to do |format|
format.html do
mark_cacheable! unless user_signed_in?
expires_in 0, public: true unless user_signed_in?
next if @account.user_hides_network?
@ -17,9 +22,9 @@ class FollowingAccountsController < ApplicationController
end
format.json do
raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
raise Mastodon::NotPermittedError if page_requested? && @account.user_hides_network?
expires_in 3.minutes, public: true if params[:page].blank?
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
render json: collection_presenter,
serializer: ActivityPub::CollectionSerializer,
@ -35,12 +40,16 @@ class FollowingAccountsController < ApplicationController
@follows ||= Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
end
def page_requested?
params[:page].present?
end
def page_url(page)
account_following_index_url(@account, page: page) unless page.nil?
end
def collection_presenter
if params[:page].present?
if page_requested?
ActivityPub::CollectionPresenter.new(
id: account_following_index_url(@account, page: params.fetch(:page, 1)),
type: :ordered,

View File

@ -3,7 +3,6 @@
class HomeController < ApplicationController
before_action :authenticate_user!
before_action :set_referrer_policy_header
before_action :set_initial_state_json
def index
@body_classes = 'app-body'
@ -21,7 +20,7 @@ class HomeController < ApplicationController
when 'statuses'
status = Status.find_by(id: matches[2])
if status && (status.public_visibility? || status.unlisted_visibility?)
if status&.distributable?
redirect_to(ActivityPub::TagManager.instance.url_for(status))
return
end
@ -39,26 +38,11 @@ class HomeController < ApplicationController
redirect_to(matches ? tag_path(CGI.unescape(matches[:tag])) : default_redirect_path)
end
def set_initial_state_json
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
@initial_state_json = serializable_resource.to_json
end
def initial_state_params
{
settings: Web::Setting.find_by(user: current_user)&.data || {},
push_subscription: current_account.user.web_push_subscription(current_session),
current_account: current_account,
token: current_session.token,
admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')),
}
end
def default_redirect_path
if request.path.start_with?('/web')
if request.path.start_with?('/web') || whitelist_mode?
new_user_session_path
elsif single_user_mode?
short_account_path(Account.local.without_suspended.first)
short_account_path(Account.local.without_suspended.where('id > 0').first)
else
about_path
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
class InstanceActorsController < ApplicationController
include AccountControllerConcern
skip_around_action :set_locale
def show
expires_in 10.minutes, public: true
render json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to
end
private
def set_account
@account = Account.find(-99)
end
def restrict_fields_to
%i(id type preferred_username inbox public_key endpoints url manually_approves_followers)
end
end

View File

@ -2,6 +2,7 @@
class IntentsController < ApplicationController
before_action :check_uri
rescue_from Addressable::URI::InvalidURIError, with: :handle_invalid_uri
def show

View File

@ -43,7 +43,7 @@ class InvitesController < ApplicationController
end
def resource_params
params.require(:invite).permit(:max_uses, :expires_in, :autofollow)
params.require(:invite).permit(:max_uses, :expires_in, :autofollow, :comment)
end
def set_body_classes

View File

@ -2,8 +2,10 @@
class ManifestsController < ApplicationController
skip_before_action :store_current_location
skip_before_action :require_functional!
def show
render json: InstancePresenter.new, serializer: ManifestSerializer
expires_in 3.minutes, public: true
render json: InstancePresenter.new, serializer: ManifestSerializer, root: 'instance'
end
end

View File

@ -4,7 +4,9 @@ class MediaController < ApplicationController
include Authorization
skip_before_action :store_current_location
skip_before_action :require_functional!
before_action :authenticate_user!, if: :whitelist_mode?
before_action :set_media_attachment
before_action :verify_permitted_status!
before_action :check_playable, only: :player
@ -31,7 +33,6 @@ class MediaController < ApplicationController
def verify_permitted_status!
authorize @media_attachment.status, :show?
rescue Mastodon::NotPermittedError
# Reraise in order to get a 404 instead of a 403 error code
raise ActiveRecord::RecordNotFound
end

View File

@ -4,6 +4,13 @@ class MediaProxyController < ApplicationController
include RoutingHelper
skip_before_action :store_current_location
skip_before_action :require_functional!
before_action :authenticate_user!, if: :whitelist_mode?
rescue_from ActiveRecord::RecordInvalid, with: :not_found
rescue_from Mastodon::UnexpectedResponseError, with: :not_found
rescue_from HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, with: :internal_server_error
def show
RedisLock.acquire(lock_options) do |lock|

View File

@ -7,6 +7,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
before_action :authenticate_resource_owner!
before_action :set_body_classes
skip_before_action :require_functional!
include Localized
def destroy

View File

@ -3,25 +3,17 @@
class PublicTimelinesController < ApplicationController
layout 'public'
before_action :check_enabled
before_action :authenticate_user!, if: :whitelist_mode?
before_action :require_enabled!
before_action :set_body_classes
before_action :set_instance_presenter
def show
respond_to do |format|
format.html do
@initial_state_json = ActiveModelSerializers::SerializableResource.new(
InitialStatePresenter.new(settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token),
serializer: InitialStateSerializer
).to_json
end
end
end
def show; end
private
def check_enabled
raise ActiveRecord::RecordNotFound unless Setting.timeline_preview
def require_enabled!
not_found unless Setting.timeline_preview
end
def set_body_classes

View File

@ -1,12 +1,14 @@
# frozen_string_literal: true
class RemoteFollowController < ApplicationController
include AccountOwnedConcern
layout 'modal'
before_action :set_account
before_action :gone, if: :suspended_account?
before_action :set_body_classes
skip_before_action :require_functional!
def new
@remote_follow = RemoteFollow.new(session_params)
end
@ -29,15 +31,7 @@ class RemoteFollowController < ApplicationController
end
def session_params
{ acct: session[:remote_follow] }
end
def set_account
@account = Account.find_local!(params[:account_username])
end
def suspended_account?
@account.suspended?
{ acct: session[:remote_follow] || current_account&.username }
end
def set_body_classes

View File

@ -5,10 +5,13 @@ class RemoteInteractionController < ApplicationController
layout 'modal'
before_action :authenticate_user!, if: :whitelist_mode?
before_action :set_interaction_type
before_action :set_status
before_action :set_body_classes
skip_before_action :require_functional!
def new
@remote_follow = RemoteFollow.new(session_params)
end
@ -31,14 +34,13 @@ class RemoteInteractionController < ApplicationController
end
def session_params
{ acct: session[:remote_follow] }
{ acct: session[:remote_follow] || current_account&.username }
end
def set_status
@status = Status.find(params[:id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
# Reraise in order to get a 404
raise ActiveRecord::RecordNotFound
end

View File

@ -1,39 +0,0 @@
# frozen_string_literal: true
class RemoteUnfollowsController < ApplicationController
layout 'modal'
before_action :authenticate_user!
before_action :set_body_classes
def create
@account = unfollow_attempt.try(:target_account)
if @account.nil?
render :error
else
render :success
end
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
render :error
end
private
def unfollow_attempt
username, domain = acct_without_prefix.split('@')
UnfollowService.new.call(current_account, Account.find_remote!(username, domain))
end
def acct_without_prefix
acct_params.gsub(/\Aacct:/, '')
end
def acct_params
params.fetch(:acct, '')
end
def set_body_classes
@body_classes = 'modal-layout'
end
end

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
class Settings::AliasesController < Settings::BaseController
layout 'admin'
before_action :authenticate_user!
before_action :set_aliases, except: :destroy
before_action :set_alias, only: :destroy
def index
@alias = current_account.aliases.build
end
def create
@alias = current_account.aliases.build(resource_params)
if @alias.save
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
redirect_to settings_aliases_path, notice: I18n.t('aliases.created_msg')
else
render :index
end
end
def destroy
@alias.destroy!
redirect_to settings_aliases_path, notice: I18n.t('aliases.deleted_msg')
end
private
def resource_params
params.require(:account_alias).permit(:acct)
end
def set_alias
@alias = current_account.aliases.find(params[:id])
end
def set_aliases
@aliases = current_account.aliases.order(id: :desc).reject(&:new_record?)
end
end

View File

@ -5,18 +5,20 @@ class Settings::DeletesController < Settings::BaseController
before_action :check_enabled_deletion
before_action :authenticate_user!
before_action :require_not_suspended!
skip_before_action :require_functional!
def show
@confirmation = Form::DeleteConfirmation.new
end
def destroy
if current_user.valid_password?(delete_params[:password])
Admin::SuspensionWorker.perform_async(current_user.account_id, true)
sign_out
if challenge_passed?
destroy_account!
redirect_to new_user_session_path, notice: I18n.t('deletes.success_msg')
else
redirect_to settings_delete_path, alert: I18n.t('deletes.bad_password_msg')
redirect_to settings_delete_path, alert: I18n.t('deletes.challenge_not_passed')
end
end
@ -26,7 +28,25 @@ class Settings::DeletesController < Settings::BaseController
redirect_to root_path unless Setting.open_deletion
end
def delete_params
params.require(:form_delete_confirmation).permit(:password)
def resource_params
params.require(:form_delete_confirmation).permit(:password, :username)
end
def require_not_suspended!
forbidden if current_account.suspended?
end
def challenge_passed?
if current_user.encrypted_password.blank?
current_account.username == resource_params[:username]
else
current_user.valid_password?(resource_params[:password])
end
end
def destroy_account!
current_account.suspend!
Admin::SuspensionWorker.perform_async(current_user.account_id, true)
sign_out
end
end

View File

@ -6,6 +6,9 @@ class Settings::ExportsController < Settings::BaseController
layout 'admin'
before_action :authenticate_user!
before_action :require_not_suspended!
skip_before_action :require_functional!
def show
@export = Export.new(current_account)
@ -34,4 +37,8 @@ class Settings::ExportsController < Settings::BaseController
def lock_options
{ redis: Redis.current, key: "backup:#{current_user.id}" }
end
def require_not_suspended!
forbidden if current_account.suspended?
end
end

View File

@ -0,0 +1,45 @@
# frozen_string_literal: true
class Settings::Migration::RedirectsController < Settings::BaseController
layout 'admin'
before_action :authenticate_user!
before_action :require_not_suspended!
skip_before_action :require_functional!
def new
@redirect = Form::Redirect.new
end
def create
@redirect = Form::Redirect.new(resource_params.merge(account: current_account))
if @redirect.valid_with_challenge?(current_user)
current_account.update!(moved_to_account: @redirect.target_account)
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
redirect_to settings_migration_path, notice: I18n.t('migrations.moved_msg', acct: current_account.moved_to_account.acct)
else
render :new
end
end
def destroy
if current_account.moved_to_account_id.present?
current_account.update!(moved_to_account: nil)
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
end
redirect_to settings_migration_path, notice: I18n.t('migrations.cancelled_msg')
end
private
def resource_params
params.require(:form_redirect).permit(:acct, :current_password, :current_username)
end
def require_not_suspended!
forbidden if current_account.suspended?
end
end

View File

@ -4,31 +4,48 @@ class Settings::MigrationsController < Settings::BaseController
layout 'admin'
before_action :authenticate_user!
before_action :require_not_suspended!
before_action :set_migrations
before_action :set_cooldown
skip_before_action :require_functional!
def show
@migration = Form::Migration.new(account: current_account.moved_to_account)
@migration = current_account.migrations.build
end
def update
@migration = Form::Migration.new(resource_params)
def create
@migration = current_account.migrations.build(resource_params)
if @migration.valid? && migration_account_changed?
current_account.update!(moved_to_account: @migration.account)
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
redirect_to settings_migration_path, notice: I18n.t('migrations.updated_msg')
if @migration.save_with_challenge(current_user)
MoveService.new.call(@migration)
redirect_to settings_migration_path, notice: I18n.t('migrations.moved_msg', acct: current_account.moved_to_account.acct)
else
render :show
end
end
helper_method :on_cooldown?
private
def resource_params
params.require(:migration).permit(:acct)
params.require(:account_migration).permit(:acct, :current_password, :current_username)
end
def migration_account_changed?
current_account.moved_to_account_id != @migration.account&.id &&
current_account.id != @migration.account&.id
def set_migrations
@migrations = current_account.migrations.includes(:target_account).order(id: :desc).reject(&:new_record?)
end
def set_cooldown
@cooldown = current_account.migrations.within_cooldown.first
end
def on_cooldown?
@cooldown.present?
end
def require_not_suspended!
forbidden if current_account.suspended?
end
end

View File

@ -55,7 +55,10 @@ class Settings::PreferencesController < Settings::BaseController
:setting_aggregate_reblogs,
:setting_show_application,
:setting_advanced_layout,
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account),
:setting_use_blurhash,
:setting_use_pending_items,
:setting_trends,
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag),
interactions: %i(must_be_follower must_be_following must_be_following_dm)
)
end

View File

@ -4,6 +4,8 @@ class Settings::SessionsController < Settings::BaseController
before_action :authenticate_user!
before_action :set_session, only: :destroy
skip_before_action :require_functional!
def destroy
@session.destroy!
flash[:notice] = I18n.t('sessions.revoke_success')

View File

@ -3,23 +3,30 @@
module Settings
module TwoFactorAuthentication
class ConfirmationsController < BaseController
include ChallengableConcern
layout 'admin'
before_action :authenticate_user!
before_action :require_challenge!
before_action :ensure_otp_secret
skip_before_action :require_functional!
def new
prepare_two_factor_form
end
def create
if current_user.validate_and_consume_otp!(confirmation_params[:code])
if current_user.validate_and_consume_otp!(confirmation_params[:otp_attempt])
flash.now[:notice] = I18n.t('two_factor_authentication.enabled_success')
current_user.otp_required_for_login = true
@recovery_codes = current_user.generate_otp_backup_codes!
current_user.save!
UserMailer.two_factor_enabled(current_user).deliver_later!
render 'settings/two_factor_authentication/recovery_codes/index'
else
flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
@ -31,7 +38,7 @@ module Settings
private
def confirmation_params
params.require(:form_two_factor_confirmation).permit(:code)
params.require(:form_two_factor_confirmation).permit(:otp_attempt)
end
def prepare_two_factor_form

View File

@ -3,14 +3,22 @@
module Settings
module TwoFactorAuthentication
class RecoveryCodesController < BaseController
include ChallengableConcern
layout 'admin'
before_action :authenticate_user!
before_action :require_challenge!, on: :create
skip_before_action :require_functional!
def create
@recovery_codes = current_user.generate_otp_backup_codes!
current_user.save!
UserMailer.two_factor_recovery_codes_changed(current_user).deliver_later!
flash.now[:notice] = I18n.t('two_factor_authentication.recovery_codes_regenerated')
render :index
end
end

View File

@ -2,10 +2,15 @@
module Settings
class TwoFactorAuthenticationsController < BaseController
include ChallengableConcern
layout 'admin'
before_action :authenticate_user!
before_action :verify_otp_required, only: [:create]
before_action :require_challenge!, only: [:create]
skip_before_action :require_functional!
def show
@confirmation = Form::TwoFactorConfirmation.new
@ -21,6 +26,7 @@ module Settings
if acceptable_code?
current_user.otp_required_for_login = false
current_user.save!
UserMailer.two_factor_disabled(current_user).deliver_later!
redirect_to settings_two_factor_authentication_path
else
flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
@ -32,7 +38,7 @@ module Settings
private
def confirmation_params
params.require(:form_two_factor_confirmation).permit(:code)
params.require(:form_two_factor_confirmation).permit(:otp_attempt)
end
def verify_otp_required
@ -40,8 +46,8 @@ module Settings
end
def acceptable_code?
current_user.validate_and_consume_otp!(confirmation_params[:code]) ||
current_user.invalidate_otp_backup_code!(confirmation_params[:code])
current_user.validate_and_consume_otp!(confirmation_params[:otp_attempt]) ||
current_user.invalidate_otp_backup_code!(confirmation_params[:otp_attempt])
end
end
end

View File

@ -6,26 +6,10 @@ class SharesController < ApplicationController
before_action :authenticate_user!
before_action :set_body_classes
def show
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
@initial_state_json = serializable_resource.to_json
end
def show; end
private
def initial_state_params
text = [params[:title], params[:text], params[:url]].compact.join(' ')
{
settings: Web::Setting.find_by(user: current_user)&.data || {},
push_subscription: current_account.user.web_push_subscription(current_session),
current_account: current_account,
token: current_session.token,
admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')),
text: text,
}
end
def set_body_classes
@body_classes = 'modal-layout compose-standalone'
end

View File

@ -1,24 +1,25 @@
# frozen_string_literal: true
class StatusesController < ApplicationController
include StatusControllerConcern
include SignatureAuthentication
include Authorization
ANCESTORS_LIMIT = 40
DESCENDANTS_LIMIT = 60
DESCENDANTS_DEPTH_LIMIT = 20
include AccountOwnedConcern
layout 'public'
before_action :set_account
before_action :require_signature!, only: :show, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_status
before_action :set_instance_presenter
before_action :set_link_headers
before_action :check_account_suspension
before_action :redirect_to_original, only: [:show]
before_action :set_referrer_policy_header, only: [:show]
before_action :redirect_to_original, only: :show
before_action :set_referrer_policy_header, only: :show
before_action :set_cache_headers
before_action :set_replies, only: [:replies]
before_action :set_body_classes
before_action :set_autoplay, only: :embed
skip_around_action :set_locale, if: -> { request.format == :json }
skip_before_action :require_functional!, only: [:show, :embed]
content_security_policy only: :embed do |p|
p.frame_ancestors(false)
@ -28,27 +29,20 @@ class StatusesController < ApplicationController
respond_to do |format|
format.html do
expires_in 10.seconds, public: true if current_account.nil?
@body_classes = 'with-modals'
set_ancestors
set_descendants
render 'stream_entries/show'
end
format.json do
render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter)
end
expires_in 3.minutes, public: @status.distributable? && public_fetch_mode?
render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter
end
end
end
def activity
render_cached_json(['activitypub', 'activity', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter)
end
expires_in 3.minutes, public: @status.distributable? && public_fetch_mode?
render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter
end
def embed
@ -56,130 +50,24 @@ class StatusesController < ApplicationController
expires_in 180, public: true
response.headers['X-Frame-Options'] = 'ALLOWALL'
@autoplay = ActiveModel::Type::Boolean.new.cast(params[:autoplay])
render 'stream_entries/embed', layout: 'embedded'
end
def replies
render json: replies_collection_presenter,
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json',
skip_activities: true
render layout: 'embedded'
end
private
def replies_collection_presenter
page = ActivityPub::CollectionPresenter.new(
id: replies_account_status_url(@account, @status, page_params),
type: :unordered,
part_of: replies_account_status_url(@account, @status),
next: next_page,
items: @replies.map { |status| status.local ? status : status.id }
)
if page_requested?
page
else
ActivityPub::CollectionPresenter.new(
id: replies_account_status_url(@account, @status),
type: :unordered,
first: page
)
end
end
def create_descendant_thread(starting_depth, statuses)
depth = starting_depth + statuses.size
if depth < DESCENDANTS_DEPTH_LIMIT
{ statuses: statuses, starting_depth: starting_depth }
else
next_status = statuses.pop
{ statuses: statuses, starting_depth: starting_depth, next_status: next_status }
end
end
def set_account
@account = Account.find_local!(params[:account_username])
end
def set_ancestors
@ancestors = @status.reply? ? cache_collection(@status.ancestors(ANCESTORS_LIMIT, current_account), Status) : []
@next_ancestor = @ancestors.size < ANCESTORS_LIMIT ? nil : @ancestors.shift
end
def set_descendants
@max_descendant_thread_id = params[:max_descendant_thread_id]&.to_i
@since_descendant_thread_id = params[:since_descendant_thread_id]&.to_i
descendants = cache_collection(
@status.descendants(
DESCENDANTS_LIMIT,
current_account,
@max_descendant_thread_id,
@since_descendant_thread_id,
DESCENDANTS_DEPTH_LIMIT
),
Status
)
@descendant_threads = []
if descendants.present?
statuses = [descendants.first]
starting_depth = 0
descendants.drop(1).each_with_index do |descendant, index|
if descendants[index].id == descendant.in_reply_to_id
statuses << descendant
else
@descendant_threads << create_descendant_thread(starting_depth, statuses)
# The thread is broken, assume it's a reply to the root status
starting_depth = 0
# ... unless we can find its ancestor in one of the already-processed threads
@descendant_threads.reverse_each do |descendant_thread|
statuses = descendant_thread[:statuses]
index = statuses.find_index do |thread_status|
thread_status.id == descendant.in_reply_to_id
end
if index.present?
starting_depth = descendant_thread[:starting_depth] + index + 1
break
end
end
statuses = [descendant]
end
end
@descendant_threads << create_descendant_thread(starting_depth, statuses)
end
@max_descendant_thread_id = @descendant_threads.pop[:statuses].first.id if descendants.size >= DESCENDANTS_LIMIT
def set_body_classes
@body_classes = 'with-modals'
end
def set_link_headers
response.headers['Link'] = LinkHeader.new(
[
[account_stream_entry_url(@account, @status.stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]],
[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]],
]
)
response.headers['Link'] = LinkHeader.new([[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]])
end
def set_status
@status = @account.statuses.find(params[:id])
@stream_entry = @status.stream_entry
@type = @stream_entry.activity_type.downcase
authorize @status, :show?
rescue Mastodon::NotPermittedError
# Reraise in order to get a 404
raise ActiveRecord::RecordNotFound
end
@ -187,39 +75,15 @@ class StatusesController < ApplicationController
@instance_presenter = InstancePresenter.new
end
def check_account_suspension
gone if @account.suspended?
end
def redirect_to_original
redirect_to ::TagManager.instance.url_for(@status.reblog) if @status.reblog?
redirect_to ActivityPub::TagManager.instance.url_for(@status.reblog) if @status.reblog?
end
def set_referrer_policy_header
return if @status.public_visibility? || @status.unlisted_visibility?
response.headers['Referrer-Policy'] = 'origin'
response.headers['Referrer-Policy'] = 'origin' unless @status.distributable?
end
def page_requested?
params[:page] == 'true'
end
def set_replies
@replies = page_params[:other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses
@replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted])
@replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
end
def next_page
last_reply = @replies.last
return if last_reply.nil?
same_account = last_reply.account_id == @account.id
return unless same_account || @replies.size == DESCENDANTS_LIMIT
same_account = false unless @replies.size == DESCENDANTS_LIMIT
replies_account_status_url(@account, @status, page: true, min_id: last_reply.id, other_accounts: !same_account)
end
def page_params
{ page: true, other_accounts: params[:other_accounts], min_id: params[:min_id] }.compact
def set_autoplay
@autoplay = truthy_param?(:autoplay)
end
end

Some files were not shown because too many files have changed in this diff Show More