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

View File

@ -11,24 +11,14 @@ DB_NAME=gonano
DB_PASS=$DATA_DB_PASS DB_PASS=$DATA_DB_PASS
DB_PORT=5432 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 # Optional ElasticSearch configuration
ES_ENABLED=true ES_ENABLED=true
ES_HOST=$DATA_ELASTIC_HOST ES_HOST=$DATA_ELASTIC_HOST
ES_PORT=9200 ES_PORT=9200
# Optimizations BIND=0.0.0.0
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
# Federation # Federation
# Note: Changing LOCAL_DOMAIN at a later time will cause unwanted side effects, including breaking all existing 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_LOGIN=$SMTP_LOGIN
SMTP_PASSWORD=$SMTP_PASSWORD SMTP_PASSWORD=$SMTP_PASSWORD
SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
#SMTP_REPLY_TO=
#SMTP_DOMAIN= # defaults to LOCAL_DOMAIN #SMTP_DOMAIN= # defaults to LOCAL_DOMAIN
#SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail #SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail
#SMTP_AUTH_METHOD=plain #SMTP_AUTH_METHOD=plain
@ -97,9 +88,17 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# PAPERCLIP_ROOT_URL=/system # PAPERCLIP_ROOT_URL=/system
# Optional asset host for multi-server setups # 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 # CDN_HOST=https://assets.example.com
# S3 (optional) # 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_ENABLED=true
# S3_BUCKET= # S3_BUCKET=
# AWS_ACCESS_KEY_ID= # AWS_ACCESS_KEY_ID=
@ -109,6 +108,8 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# S3_HOSTNAME=192.168.1.123:9000 # S3_HOSTNAME=192.168.1.123:9000
# S3 (Minio Config (optional) Please check Minio instance for details) # 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_ENABLED=true
# S3_BUCKET= # S3_BUCKET=
# AWS_ACCESS_KEY_ID= # AWS_ACCESS_KEY_ID=
@ -119,12 +120,30 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# S3_ENDPOINT= # S3_ENDPOINT=
# S3_SIGNATURE_VERSION= # 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) # Swift (optional)
# The attachment host must allow cross origin request - see the description
# above.
# SWIFT_ENABLED=true # SWIFT_ENABLED=true
# SWIFT_USERNAME= # SWIFT_USERNAME=
# For Keystone V3, the value for SWIFT_TENANT should be the project name # For Keystone V3, the value for SWIFT_TENANT should be the project name
# SWIFT_TENANT= # SWIFT_TENANT=
# SWIFT_PASSWORD= # 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 # Keystone V2 and V3 URLs are supported. Use a V3 URL if possible to avoid
# issues with token rate-limiting during high load. # issues with token rate-limiting during high load.
# SWIFT_AUTH_URL= # SWIFT_AUTH_URL=
@ -171,8 +190,8 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# The pam environment variable "email" is provided by: # The pam environment variable "email" is provided by:
# https://github.com/devkral/pam_email_extractor # https://github.com/devkral/pam_email_extractor
# PAM_ENABLED=true # PAM_ENABLED=true
# Fallback Suffix for email address generation (nil by default) # Fallback email domain for email address generation (LOCAL_DOMAIN by default)
# PAM_DEFAULT_SUFFIX=pam # PAM_EMAIL_DOMAIN=example.com
# Name of the pam service (pam "auth" section is evaluated) # Name of the pam service (pam "auth" section is evaluated)
# PAM_DEFAULT_SERVICE=rpam # 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) # 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_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true
# SAML_ATTRIBUTES_STATEMENTS_UID="urn:oid:0.9.2342.19200300.100.1.1" # 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_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_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1"
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED= # SAML_ATTRIBUTES_STATEMENTS_VERIFIED=
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL= # 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_LOGIN=
SMTP_PASSWORD= SMTP_PASSWORD=
SMTP_FROM_ADDRESS=notifications@example.com SMTP_FROM_ADDRESS=notifications@example.com
#SMTP_REPLY_TO=
#SMTP_DOMAIN= # defaults to LOCAL_DOMAIN #SMTP_DOMAIN= # defaults to LOCAL_DOMAIN
#SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail #SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail
#SMTP_AUTH_METHOD=plain #SMTP_AUTH_METHOD=plain
@ -114,6 +115,20 @@ SMTP_FROM_ADDRESS=notifications@example.com
# S3_ENDPOINT= # S3_ENDPOINT=
# S3_SIGNATURE_VERSION= # 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) # Swift (optional)
# The attachment host must allow cross origin request - see the description # The attachment host must allow cross origin request - see the description
# above. # above.
@ -163,7 +178,7 @@ STREAMING_CLUSTER_NUM=1
# LDAP_BIND_DN= # LDAP_BIND_DN=
# LDAP_PASSWORD= # LDAP_PASSWORD=
# LDAP_UID=cn # LDAP_UID=cn
# LDAP_SEARCH_FILTER="%{uid}=%{email}" # LDAP_SEARCH_FILTER=%{uid}=%{email}
# PAM authentication (optional) # PAM authentication (optional)
# PAM authentication uses for the email generation the "email" pam variable # 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. 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 ## [2.9.3] - 2019-08-10
### Added ### Added

View File

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

53
Gemfile
View File

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

View File

@ -4,17 +4,22 @@ class AccountsController < ApplicationController
PAGE_SIZE = 20 PAGE_SIZE = 20
include AccountControllerConcern include AccountControllerConcern
include SignatureAuthentication
before_action :set_cache_headers 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 def show
respond_to do |format| respond_to do |format|
format.html do format.html do
mark_cacheable! unless user_signed_in? expires_in 0, public: true unless user_signed_in?
@body_classes = 'with-modals'
@pinned_statuses = [] @pinned_statuses = []
@endorsed_accounts = @account.endorsed_accounts.to_a.sample(4) @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) if current_account && @account.blocking?(current_account)
@statuses = [] @statuses = []
@ -24,6 +29,7 @@ class AccountsController < ApplicationController
@pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses? @pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
@statuses = filtered_status_page(params) @statuses = filtered_status_page(params)
@statuses = cache_collection(@statuses, Status) @statuses = cache_collection(@statuses, Status)
@rss_url = rss_url
unless @statuses.empty? unless @statuses.empty?
@older_url = older_url if @statuses.last.id > filtered_statuses.last.id @older_url = older_url if @statuses.last.id > filtered_statuses.last.id
@ -31,30 +37,27 @@ class AccountsController < ApplicationController
end end
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 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) @statuses = filtered_statuses.without_local_only.without_reblogs.without_replies.limit(PAGE_SIZE)
render xml: RSS::AccountSerializer.render(@account, @statuses) @statuses = cache_collection(@statuses, Status)
render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag])
end end
format.json do format.json do
render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?)
ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter) render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to
end
end end
end end
end end
private private
def set_body_classes
@body_classes = 'with-modals'
end
def show_pinned_statuses? def show_pinned_statuses?
[replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none? [replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
end end
@ -101,6 +104,14 @@ class AccountsController < ApplicationController
params[:username] params[:username]
end 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 def older_url
pagination_url(max_id: @statuses.last.id) pagination_url(max_id: @statuses.last.id)
end end
@ -122,15 +133,15 @@ class AccountsController < ApplicationController
end end
def media_requested? def media_requested?
request.path.ends_with?('/media') request.path.ends_with?('/media') && !tag_requested?
end end
def replies_requested? def replies_requested?
request.path.ends_with?('/with_replies') request.path.ends_with?('/with_replies') && !tag_requested?
end end
def tag_requested? 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 end
def filtered_status_page(params) 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 filtered_statuses.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id]).to_a
end end
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 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 # frozen_string_literal: true
class ActivityPub::CollectionsController < Api::BaseController class ActivityPub::CollectionsController < ActivityPub::BaseController
include SignatureVerification include SignatureVerification
include AccountOwnedConcern
before_action :set_account before_action :require_signature!, if: :authorized_fetch_mode?
before_action :set_size before_action :set_size
before_action :set_statuses before_action :set_statuses
before_action :set_cache_headers before_action :set_cache_headers
def show def show
render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do expires_in 3.minutes, public: public_fetch_mode?
ActiveModelSerializers::SerializableResource.new( render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, skip_activities: true
collection_presenter,
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
skip_activities: true
)
end
end end
private private
def set_account
@account = Account.find_local!(params[:account_username])
end
def set_statuses def set_statuses
@statuses = scope_for_collection @statuses = scope_for_collection
@statuses = cache_collection(@statuses, Status) @statuses = cache_collection(@statuses, Status)
@ -42,9 +33,9 @@ class ActivityPub::CollectionsController < Api::BaseController
def scope_for_collection def scope_for_collection
case params[:id] case params[:id]
when 'featured' when 'featured'
@account.statuses.permitted_for(@account, signed_request_account).tap do |scope| return Status.none if @account.blocking?(signed_request_account)
scope.merge!(@account.pinned_statuses)
end @account.pinned_statuses
else else
raise ActiveRecord::RecordNotFound raise ActiveRecord::RecordNotFound
end end

View File

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

View File

@ -1,26 +1,22 @@
# frozen_string_literal: true # frozen_string_literal: true
class ActivityPub::OutboxesController < Api::BaseController class ActivityPub::OutboxesController < ActivityPub::BaseController
LIMIT = 20 LIMIT = 20
include SignatureVerification include SignatureVerification
include AccountOwnedConcern
before_action :set_account before_action :require_signature!, if: :authorized_fetch_mode?
before_action :set_statuses before_action :set_statuses
before_action :set_cache_headers before_action :set_cache_headers
def show 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' render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end end
private private
def set_account
@account = Account.find_local!(params[:account_username])
end
def outbox_presenter def outbox_presenter
if page_requested? if page_requested?
ActivityPub::CollectionPresenter.new( 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 before_action :set_account
def new 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 @warning_presets = AccountWarningPreset.all
end end
@ -30,7 +30,7 @@ module Admin
end end
def resource_params 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 end
end end

View File

@ -2,8 +2,8 @@
module Admin module Admin
class AccountsController < BaseController class AccountsController < BaseController
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject] before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject]
before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload] before_action :require_remote_account!, only: [:redownload]
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject] before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
def index def index
@ -19,18 +19,6 @@ module Admin
@warnings = @account.targeted_account_warnings.latest.custom @warnings = @account.targeted_account_warnings.latest.custom
end 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 def memorialize
authorize @account, :memorialize? authorize @account, :memorialize?
@account.memorialize! @account.memorialize!
@ -53,7 +41,7 @@ module Admin
def reject def reject
authorize @account.user, :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 redirect_to admin_pending_accounts_path
end end

View File

@ -2,19 +2,20 @@
module Admin module Admin
class CustomEmojisController < BaseController class CustomEmojisController < BaseController
before_action :set_custom_emoji, except: [:index, :new, :create]
before_action :set_filter_params
include ObfuscateFilename include ObfuscateFilename
obfuscate_filename [:custom_emoji, :image] obfuscate_filename [:custom_emoji, :image]
def index def index
authorize :custom_emoji, :index? authorize :custom_emoji, :index?
@custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]) @custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page])
@form = Form::CustomEmojiBatch.new
end end
def new def new
authorize :custom_emoji, :create? authorize :custom_emoji, :create?
@custom_emoji = CustomEmoji.new @custom_emoji = CustomEmoji.new
end end
@ -31,69 +32,17 @@ module Admin
end end
end end
def update def batch
authorize @custom_emoji, :update? @form = Form::CustomEmojiBatch.new(form_custom_emoji_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
if @custom_emoji.update(resource_params) rescue ActionController::ParameterMissing
log_action :update, @custom_emoji flash[:alert] = I18n.t('admin.accounts.no_account_selected')
flash[:notice] = I18n.t('admin.custom_emojis.updated_msg') ensure
else redirect_to admin_custom_emojis_path(filter_params)
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)
end end
private 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 def resource_params
params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker) params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker)
end end
@ -103,12 +52,29 @@ module Admin
end end
def filter_params def filter_params
params.permit( params.slice(:local, :remote, :by_domain, :shortcode, :page).permit(:local, :remote, :by_domain, :shortcode, :page)
:local, end
:remote,
:by_domain, def action_from_button
:shortcode 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 end
end end

View File

@ -5,6 +5,7 @@ module Admin
class DashboardController < BaseController class DashboardController < BaseController
def index def index
@users_count = User.count @users_count = User.count
@pending_users_count = User.pending.count
@registrations_week = Redis.current.get("activity:accounts:local:#{current_week}") || 0 @registrations_week = Redis.current.get("activity:accounts:local:#{current_week}") || 0
@logins_week = Redis.current.pfcount("activity:logins:#{current_week}") @logins_week = Redis.current.pfcount("activity:logins:#{current_week}")
@interactions_week = Redis.current.get("activity:interactions:#{current_week}") || 0 @interactions_week = Redis.current.get("activity:interactions:#{current_week}") || 0
@ -19,7 +20,7 @@ module Admin
@redis_version = redis_info['redis_version'] @redis_version = redis_info['redis_version']
@reports_count = Report.unresolved.count @reports_count = Report.unresolved.count
@queue_backlog = Sidekiq::Stats.new.enqueued @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'] @database_size = ActiveRecord::Base.connection.execute('SELECT pg_database_size(current_database())').first['pg_database_size']
@redis_size = redis_info['used_memory'] @redis_size = redis_info['used_memory']
@ldap_enabled = ENV['LDAP_ENABLED'] == 'true' @ldap_enabled = ENV['LDAP_ENABLED'] == 'true'
@ -27,9 +28,14 @@ module Admin
@saml_enabled = ENV['SAML_ENABLED'] == 'true' @saml_enabled = ENV['SAML_ENABLED'] == 'true'
@pam_enabled = ENV['PAM_ENABLED'] == 'true' @pam_enabled = ENV['PAM_ENABLED'] == 'true'
@hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == '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 @profile_directory = Setting.profile_directory
@timeline_preview = Setting.timeline_preview @timeline_preview = Setting.timeline_preview
@spam_check_enabled = Setting.spam_check_enabled
@trends_enabled = Setting.trends
end end
private private
@ -39,7 +45,13 @@ module Admin
end end
def redis_info 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 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 module Admin
class DomainBlocksController < BaseController class DomainBlocksController < BaseController
before_action :set_domain_block, only: [:show, :destroy] before_action :set_domain_block, only: [:show, :destroy, :edit, :update]
def new def new
authorize :domain_block, :create? authorize :domain_block, :create?
@domain_block = DomainBlock.new(domain: params[:_domain]) @domain_block = DomainBlock.new(domain: params[:_domain])
end end
def edit
authorize :domain_block, :create?
end
def create def create
authorize :domain_block, :create? authorize :domain_block, :create?
@ -35,6 +39,22 @@ module Admin
end end
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 def show
authorize @domain_block, :show? authorize @domain_block, :show?
end end
@ -52,8 +72,12 @@ module Admin
@domain_block = DomainBlock.find(params[:id]) @domain_block = DomainBlock.find(params[:id])
end end
def update_params
params.require(:domain_block).permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment)
end
def resource_params 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 end
end end

View File

@ -2,6 +2,10 @@
module Admin module Admin
class InstancesController < BaseController 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 def index
authorize :instance, :index? authorize :instance, :index?
@ -11,20 +15,40 @@ module Admin
def show def show
authorize :instance, :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 @following_count = Follow.where(account: Account.where(domain: params[:id])).count
@followers_count = Follow.where(target_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 @reports_count = Report.where(target_account: Account.where(domain: params[:id])).count
@blocks_count = Block.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) @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) @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 end
private 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 def filtered_instances
InstanceFilter.new(filter_params).results InstanceFilter.new(whitelist_mode? ? { allowed: true } : filter_params).results
end end
def paginated_instances def paginated_instances

View File

@ -3,6 +3,7 @@
module Admin module Admin
class RelaysController < BaseController class RelaysController < BaseController
before_action :set_relay, except: [:index, :new, :create] before_action :set_relay, except: [:index, :new, :create]
before_action :require_signatures_enabled!, only: [:new, :create, :enable]
def index def index
authorize :relay, :update? authorize :relay, :update?
@ -11,7 +12,7 @@ module Admin
def new def new
authorize :relay, :update? authorize :relay, :update?
@relay = Relay.new(inbox_url: Relay::PRESET_RELAY) @relay = Relay.new
end end
def create def create
@ -54,5 +55,9 @@ module Admin
def resource_params def resource_params
params.require(:relay).permit(:inbox_url) params.require(:relay).permit(:inbox_url)
end 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
end end

View File

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

View File

@ -2,43 +2,102 @@
module Admin module Admin
class TagsController < BaseController class TagsController < BaseController
before_action :set_tags, only: :index before_action :set_tag, except: [:index, :batch, :approve_all, :reject_all]
before_action :set_tag, except: :index before_action :set_usage_by_domain, except: [:index, :batch, :approve_all, :reject_all]
before_action :set_filter_params before_action :set_counters, except: [:index, :batch, :approve_all, :reject_all]
def index def index
authorize :tag, :index? authorize :tag, :index?
@tags = filtered_tags.page(params[:page])
@form = Form::TagBatch.new
end end
def hide def batch
authorize @tag, :hide? @form = Form::TagBatch.new(form_tag_batch_params.merge(current_account: current_account, action: action_from_button))
@tag.account_tag_stat.update!(hidden: true) @form.save
redirect_to admin_tags_path(@filter_params) rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
ensure
redirect_to admin_tags_path(filter_params)
end end
def unhide def approve_all
authorize @tag, :unhide? Form::TagBatch.new(current_account: current_account, tag_ids: Tag.pending_review.pluck(:id), action: 'approve').save
@tag.account_tag_stat.update!(hidden: false) redirect_to admin_tags_path(filter_params)
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 end
private private
def set_tags
@tags = Tag.discoverable
@tags.merge!(Tag.hidden) if filter_params[:hidden]
end
def set_tag def set_tag
@tag = Tag.find(params[:id]) @tag = Tag.find(params[:id])
end end
def set_filter_params def set_usage_by_domain
@filter_params = filter_params.to_hash.symbolize_keys @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 end
def filter_params 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 end
end end

View File

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

View File

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

View File

@ -1,10 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::ProofsController < Api::BaseController class Api::ProofsController < Api::BaseController
before_action :set_account include AccountOwnedConcern
before_action :set_provider before_action :set_provider
before_action :check_account_approval
before_action :check_account_suspension
def index def index
render json: @account, serializer: @provider.serializer_class render json: @account, serializer: @provider.serializer_class
@ -16,15 +15,7 @@ class Api::ProofsController < Api::BaseController
@provider = ProofProvider.find(params[:provider]) || raise(ActiveRecord::RecordNotFound) @provider = ProofProvider.find(params[:provider]) || raise(ActiveRecord::RecordNotFound)
end end
def set_account def username_param
@account = Account.find_local!(params[:username]) params[:username]
end
def check_account_approval
not_found if @account.user_pending?
end
def check_account_suspension
gone if @account.suspended?
end end
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 def account_statuses
statuses = truthy_param?(:pinned) ? pinned_scope : permitted_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!(only_media_scope) if truthy_param?(:only_media)
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies) statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs) statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
statuses.merge!(hashtag_scope) if params[:tagged].present? 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 end
def permitted_account_statuses def permitted_account_statuses
@ -58,6 +57,8 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
end end
def pinned_scope def pinned_scope
return Status.none if @account.blocking?(current_account)
@account.pinned_statuses @account.pinned_statuses
end end

View File

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

View File

@ -58,7 +58,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController
def reject def reject
authorize @account.user, :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 render json: @account, serializer: REST::Admin::AccountSerializer
end end

View File

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

View File

@ -6,8 +6,7 @@ class Api::V1::CustomEmojisController < Api::BaseController
skip_before_action :set_cache_headers skip_before_action :set_cache_headers
def index def index
render_cached_json('api:v1:custom_emojis', expires_in: 1.minute) do expires_in 3.minutes, public: true
ActiveModelSerializers::SerializableResource.new(CustomEmoji.local.where(disabled: false), each_serializer: REST::CustomEmojiSerializer) render_with_cache(each_serializer: REST::CustomEmojiSerializer) { CustomEmoji.listed.includes(:category) }
end
end end
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 def authorize
AuthorizeFollowService.new.call(account, current_account) AuthorizeFollowService.new.call(account, current_account)
NotifyService.new.call(current_account, Follow.find_by(account: account, target_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 end
def reject def reject
RejectFollowService.new.call(account, current_account) RejectFollowService.new.call(account, current_account)
render_empty render json: account, serializer: REST::RelationshipSerializer, relationships: relationships
end end
private private
@ -28,6 +28,10 @@ class Api::V1::FollowRequestsController < Api::BaseController
Account.find(params[:id]) Account.find(params[:id])
end end
def relationships(**options)
AccountRelationshipsPresenter.new([params[:id]], current_user.account_id, options)
end
def load_accounts def load_accounts
default_accounts.merge(paginated_follow_requests).to_a default_accounts.merge(paginated_follow_requests).to_a
end 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 class Api::V1::Instances::ActivityController < Api::BaseController
before_action :require_enabled_api! before_action :require_enabled_api!
skip_before_action :set_cache_headers skip_before_action :set_cache_headers
respond_to :json respond_to :json
def show 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 end
private private
@ -32,6 +34,6 @@ class Api::V1::Instances::ActivityController < Api::BaseController
end end
def require_enabled_api! def require_enabled_api!
head 404 unless Setting.activity_api_enabled head 404 unless Setting.activity_api_enabled && !whitelist_mode?
end end
end end

View File

@ -2,17 +2,19 @@
class Api::V1::Instances::PeersController < Api::BaseController class Api::V1::Instances::PeersController < Api::BaseController
before_action :require_enabled_api! before_action :require_enabled_api!
skip_before_action :set_cache_headers skip_before_action :set_cache_headers
respond_to :json respond_to :json
def index 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 end
private private
def require_enabled_api! def require_enabled_api!
head 404 unless Setting.peers_api_enabled head 404 unless Setting.peers_api_enabled && !whitelist_mode?
end end
end end

View File

@ -2,11 +2,11 @@
class Api::V1::InstancesController < Api::BaseController class Api::V1::InstancesController < Api::BaseController
respond_to :json respond_to :json
skip_before_action :set_cache_headers skip_before_action :set_cache_headers
def show def show
render_cached_json('api:v1:instances', expires_in: 5.minutes) do expires_in 3.minutes, public: true
ActiveModelSerializers::SerializableResource.new({}, serializer: REST::InstanceSerializer) render_with_cache json: {}, serializer: REST::InstanceSerializer, root: 'instance'
end
end end
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 private
def reported_status_ids def reported_status_ids
reported_account.statuses.find(status_ids).pluck(:id) reported_account.statuses.with_discarded.find(status_ids).pluck(:id)
end end
def status_ids 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 } @reblogs_map = { @status.id => false }
authorize status_for_destroy, :unreblog? authorize status_for_destroy, :unreblog?
status_for_destroy.discard
RemovalWorker.perform_async(status_for_destroy.id) 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) 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 end
def status_for_destroy 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 end
def reblog_params 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 -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy]
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy] before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy]
before_action :require_user!, except: [:show, :context, :card] before_action :require_user!, except: [:show, :context]
before_action :set_status, only: [:show, :context, :card] before_action :set_status, only: [:show, :context]
respond_to :json 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) render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
end end
def card
@card = @status.preview_cards.first
if @card.nil?
render_empty
else
render json: @card, serializer: REST::PreviewCardSerializer
end
end
def create def create
@status = PostStatusService.new.call(current_user.account, @status = PostStatusService.new.call(current_user.account,
text: status_params[:status], 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]) @status = Status.where(account_id: current_user.account).find(params[:id])
authorize @status, :destroy? 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 render json: @status, serializer: REST::StatusSerializer, source_requested: true
end 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 # frozen_string_literal: true
class Api::V1::Timelines::PublicController < Api::BaseController class Api::V1::Timelines::PublicController < Api::BaseController
before_action :require_user!, only: [:show], if: :require_auth?
after_action :insert_pagination_headers, unless: -> { @statuses.empty? } after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
respond_to :json respond_to :json
@ -12,6 +13,10 @@ class Api::V1::Timelines::PublicController < Api::BaseController
private private
def require_auth?
!Setting.timeline_preview
end
def load_statuses def load_statuses
cached_public_statuses cached_public_statuses
end 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 # 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 def index
@search = Search.new(search_results) @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
end end

View File

@ -10,21 +10,29 @@ class ApplicationController < ActionController::Base
include Localized include Localized
include UserTrackingConcern include UserTrackingConcern
include SessionTrackingConcern include SessionTrackingConcern
include CacheConcern
include DomainControlHelper
helper_method :current_account helper_method :current_account
helper_method :current_session helper_method :current_session
helper_method :current_theme helper_method :current_theme
helper_method :single_user_mode? helper_method :single_user_mode?
helper_method :use_seamless_external_login? helper_method :use_seamless_external_login?
helper_method :whitelist_mode?
rescue_from ActionController::RoutingError, with: :not_found rescue_from ActionController::RoutingError, with: :not_found
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
rescue_from ActionController::UnknownFormat, with: :not_acceptable 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 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 :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 def raise_not_found
raise ActionController::RoutingError, "No route matches #{params[:unmatched_route]}" raise ActionController::RoutingError, "No route matches #{params[:unmatched_route]}"
@ -33,7 +41,15 @@ class ApplicationController < ActionController::Base
private private
def https_enabled? 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 end
def store_current_location def store_current_location
@ -48,8 +64,8 @@ class ApplicationController < ActionController::Base
forbidden unless current_user&.staff? forbidden unless current_user&.staff?
end end
def check_user_permissions def require_functional!
forbidden if current_user.disabled? || current_user.account.suspended? redirect_to edit_user_registration_path unless current_user.functional?
end end
def after_sign_out_path_for(_resource_or_scope) def after_sign_out_path_for(_resource_or_scope)
@ -82,8 +98,20 @@ class ApplicationController < ActionController::Base
respond_with_error(406) respond_with_error(406)
end 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? 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 end
def use_seamless_external_login? def use_seamless_external_login?
@ -107,51 +135,10 @@ class ApplicationController < ActionController::Base
current_user.setting_theme current_user.setting_theme
end 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) def respond_with_error(code)
respond_to do |format| respond_to do |format|
format.any { head code } format.any { head code }
format.html { render "errors/#{code}", layout: 'error', status: code } format.html { render "errors/#{code}", layout: 'error', status: code }
end end
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 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' layout 'auth'
before_action :set_body_classes before_action :set_body_classes
before_action :set_user, only: [:finish_signup] before_action :require_unconfirmed!
def finish_signup skip_before_action :require_functional!
return unless request.patch? && params[:user]
if @user.update(user_params) def new
@user.skip_reconfirmation! super
bypass_sign_in(@user)
redirect_to root_path, notice: I18n.t('devise.confirmations.send_instructions') resource.email = current_user.unconfirmed_email || current_user.email if user_signed_in?
else
@show_errors = true
end
end end
private private
def set_user def require_unconfirmed!
@user = current_user redirect_to edit_user_registration_path if user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank?
end end
def set_body_classes def set_body_classes
@body_classes = 'lighter' @body_classes = 'lighter'
end end
def user_params def after_resending_confirmation_instructions_path_for(_resource_name)
params.require(:user).permit(:email) 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 end
def after_confirmation_path_for(_resource_name, user) def after_confirmation_path_for(_resource_name, user)

View File

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

View File

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

View File

@ -6,8 +6,10 @@ class Auth::SessionsController < Devise::SessionsController
layout 'auth' layout 'auth'
skip_before_action :require_no_authentication, only: [:create] 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] prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
before_action :set_instance_presenter, only: [:new] before_action :set_instance_presenter, only: [:new]
before_action :set_body_classes before_action :set_body_classes
@ -29,6 +31,7 @@ class Auth::SessionsController < Devise::SessionsController
def destroy def destroy
tmp_stored_location = stored_location_for(:user) tmp_stored_location = stored_location_for(:user)
super super
session.delete(:challenge_passed_at)
flash.delete(:notice) flash.delete(:notice)
store_location_for(:user, tmp_stored_location) if continue_after? store_location_for(:user, tmp_stored_location) if continue_after?
end end
@ -38,12 +41,10 @@ class Auth::SessionsController < Devise::SessionsController
def find_user def find_user
if session[:otp_user_id] if session[:otp_user_id]
User.find(session[:otp_user_id]) User.find(session[:otp_user_id])
elsif user_params[:email] else
if use_seamless_external_login? && Devise.check_at_sign && user_params[:email].index('@').nil? user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication
User.joins(:account).find_by(accounts: { username: user_params[:email] }) user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication
else user ||= User.find_for_authentication(email: user_params[:email])
User.find_for_authentication(email: user_params[:email])
end
end end
end end
@ -70,13 +71,13 @@ class Auth::SessionsController < Devise::SessionsController
end end
def two_factor_enabled? def two_factor_enabled?
find_user.try(:otp_required_for_login?) find_user&.otp_required_for_login?
end end
def valid_otp_attempt?(user) def valid_otp_attempt?(user)
user.validate_and_consume_otp!(user_params[:otp_attempt]) || user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
user.invalidate_otp_backup_code!(user_params[:otp_attempt]) user.invalidate_otp_backup_code!(user_params[:otp_attempt])
rescue OpenSSL::Cipher::CipherError => _error rescue OpenSSL::Cipher::CipherError
false false
end end
@ -85,7 +86,10 @@ class Auth::SessionsController < Devise::SessionsController
if user_params[:otp_attempt].present? && session[:otp_user_id] if user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user) 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) prompt_for_two_factor(user)
end end
end end
@ -103,6 +107,7 @@ class Auth::SessionsController < Devise::SessionsController
def prompt_for_two_factor(user) def prompt_for_two_factor(user)
session[:otp_user_id] = user.id session[:otp_user_id] = user.id
@body_classes = 'lighter'
render :two_factor render :two_factor
end 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 module AccountControllerConcern
extend ActiveSupport::Concern extend ActiveSupport::Concern
include AccountOwnedConcern
FOLLOW_PER_PAGE = 12 FOLLOW_PER_PAGE = 12
included do included do
layout 'public' layout 'public'
before_action :set_account
before_action :check_account_approval
before_action :check_account_suspension
before_action :set_instance_presenter before_action :set_instance_presenter
before_action :set_link_headers before_action :set_link_headers, if: -> { request.format.nil? || request.format == :html }
end end
private private
def set_account
@account = Account.find_local!(username_param)
end
def set_instance_presenter def set_instance_presenter
@instance_presenter = InstancePresenter.new @instance_presenter = InstancePresenter.new
end end
@ -29,27 +24,15 @@ module AccountControllerConcern
response.headers['Link'] = LinkHeader.new( response.headers['Link'] = LinkHeader.new(
[ [
webfinger_account_link, webfinger_account_link,
atom_account_url_link,
actor_url_link, actor_url_link,
] ]
) )
end end
def username_param
params[:account_username]
end
def webfinger_account_link def webfinger_account_link
[ [
webfinger_account_url, webfinger_account_url,
[%w(rel lrdd), %w(type application/xrd+xml)], [%w(rel lrdd), %w(type application/jrd+json)],
]
end
def atom_account_url_link
[
account_url(@account, format: 'atom'),
[%w(rel alternate), %w(type application/atom+xml)],
] ]
end end
@ -63,15 +46,4 @@ module AccountControllerConcern
def webfinger_account_url def webfinger_account_url
webfinger_url(resource: @account.to_webfinger_s) webfinger_url(resource: @account.to_webfinger_s)
end 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 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 included do
before_action :authenticate_user! before_action :authenticate_user!
before_action :require_not_suspended!
before_action :load_export before_action :load_export
skip_before_action :require_functional!
end end
private private
@ -27,4 +30,8 @@ module ExportControllerConcern
def export_filename def export_filename
"#{controller_name}.csv" "#{controller_name}.csv"
end end
def require_not_suspended!
forbidden if current_account.suspended?
end
end end

View File

@ -5,12 +5,35 @@
module SignatureVerification module SignatureVerification
extend ActiveSupport::Concern 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? def signed_request?
request.headers['Signature'].present? request.headers['Signature'].present?
end end
def signature_verification_failure_reason 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 end
def signed_request_account def signed_request_account
@ -123,6 +146,13 @@ module SignatureVerification
end end
def account_from_key_id(key_id) 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:') if key_id.start_with?('acct:')
stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, '')) } stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, '')) }
elsif !ActivityPub::TagManager.instance.local_uri?(key_id) elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
@ -137,7 +167,7 @@ module SignatureVerification
.with_fallback { nil } .with_fallback { nil }
.with_threshold(1) .with_threshold(1)
.with_cool_off_time(5.minutes.seconds) .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 .run
end 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 class CustomCssController < ApplicationController
skip_before_action :store_current_location skip_before_action :store_current_location
skip_before_action :require_functional!
before_action :set_cache_headers before_action :set_cache_headers
def show def show
expires_in 3.minutes, public: true
render plain: Setting.custom_css || '', content_type: 'text/css' render plain: Setting.custom_css || '', content_type: 'text/css'
end end
end end

View File

@ -3,12 +3,14 @@
class DirectoriesController < ApplicationController class DirectoriesController < ApplicationController
layout 'public' 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_instance_presenter
before_action :set_tag, only: :show before_action :set_tag, only: :show
before_action :set_tags
before_action :set_accounts before_action :set_accounts
skip_before_action :require_functional!
def index def index
render :index render :index
end end
@ -19,21 +21,18 @@ class DirectoriesController < ApplicationController
private private
def check_enabled def require_enabled!
return not_found unless Setting.profile_directory return not_found unless Setting.profile_directory
end end
def set_tag def set_tag
@tag = Tag.discoverable.find_by!(name: params[:id].downcase) @tag = Tag.discoverable.find_normalized!(params[:id])
end
def set_tags
@tags = Tag.discoverable.limit(30).reject { |tag| tag.cached_sample_accounts.empty? }
end end
def set_accounts 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.tagged_with(@tag.id)) if @tag
query.merge!(Account.not_excluded_by_account(current_account)) if current_account
end end
end end

View File

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

View File

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

View File

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

View File

@ -3,7 +3,6 @@
class HomeController < ApplicationController class HomeController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_referrer_policy_header before_action :set_referrer_policy_header
before_action :set_initial_state_json
def index def index
@body_classes = 'app-body' @body_classes = 'app-body'
@ -21,7 +20,7 @@ class HomeController < ApplicationController
when 'statuses' when 'statuses'
status = Status.find_by(id: matches[2]) 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)) redirect_to(ActivityPub::TagManager.instance.url_for(status))
return return
end end
@ -39,26 +38,11 @@ class HomeController < ApplicationController
redirect_to(matches ? tag_path(CGI.unescape(matches[:tag])) : default_redirect_path) redirect_to(matches ? tag_path(CGI.unescape(matches[:tag])) : default_redirect_path)
end 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 def default_redirect_path
if request.path.start_with?('/web') if request.path.start_with?('/web') || whitelist_mode?
new_user_session_path new_user_session_path
elsif single_user_mode? elsif single_user_mode?
short_account_path(Account.local.without_suspended.first) short_account_path(Account.local.without_suspended.where('id > 0').first)
else else
about_path about_path
end 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 class IntentsController < ApplicationController
before_action :check_uri before_action :check_uri
rescue_from Addressable::URI::InvalidURIError, with: :handle_invalid_uri rescue_from Addressable::URI::InvalidURIError, with: :handle_invalid_uri
def show def show

View File

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

View File

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

View File

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

View File

@ -4,6 +4,13 @@ class MediaProxyController < ApplicationController
include RoutingHelper include RoutingHelper
skip_before_action :store_current_location 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 def show
RedisLock.acquire(lock_options) do |lock| RedisLock.acquire(lock_options) do |lock|

View File

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

View File

@ -3,25 +3,17 @@
class PublicTimelinesController < ApplicationController class PublicTimelinesController < ApplicationController
layout 'public' 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_body_classes
before_action :set_instance_presenter before_action :set_instance_presenter
def show def show; end
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
private private
def check_enabled def require_enabled!
raise ActiveRecord::RecordNotFound unless Setting.timeline_preview not_found unless Setting.timeline_preview
end end
def set_body_classes def set_body_classes

View File

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

View File

@ -5,10 +5,13 @@ class RemoteInteractionController < ApplicationController
layout 'modal' layout 'modal'
before_action :authenticate_user!, if: :whitelist_mode?
before_action :set_interaction_type before_action :set_interaction_type
before_action :set_status before_action :set_status
before_action :set_body_classes before_action :set_body_classes
skip_before_action :require_functional!
def new def new
@remote_follow = RemoteFollow.new(session_params) @remote_follow = RemoteFollow.new(session_params)
end end
@ -31,14 +34,13 @@ class RemoteInteractionController < ApplicationController
end end
def session_params def session_params
{ acct: session[:remote_follow] } { acct: session[:remote_follow] || current_account&.username }
end end
def set_status def set_status
@status = Status.find(params[:id]) @status = Status.find(params[:id])
authorize @status, :show? authorize @status, :show?
rescue Mastodon::NotPermittedError rescue Mastodon::NotPermittedError
# Reraise in order to get a 404
raise ActiveRecord::RecordNotFound raise ActiveRecord::RecordNotFound
end 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 :check_enabled_deletion
before_action :authenticate_user! before_action :authenticate_user!
before_action :require_not_suspended!
skip_before_action :require_functional!
def show def show
@confirmation = Form::DeleteConfirmation.new @confirmation = Form::DeleteConfirmation.new
end end
def destroy def destroy
if current_user.valid_password?(delete_params[:password]) if challenge_passed?
Admin::SuspensionWorker.perform_async(current_user.account_id, true) destroy_account!
sign_out
redirect_to new_user_session_path, notice: I18n.t('deletes.success_msg') redirect_to new_user_session_path, notice: I18n.t('deletes.success_msg')
else 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
end end
@ -26,7 +28,25 @@ class Settings::DeletesController < Settings::BaseController
redirect_to root_path unless Setting.open_deletion redirect_to root_path unless Setting.open_deletion
end end
def delete_params def resource_params
params.require(:form_delete_confirmation).permit(:password) 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
end end

View File

@ -6,6 +6,9 @@ class Settings::ExportsController < Settings::BaseController
layout 'admin' layout 'admin'
before_action :authenticate_user! before_action :authenticate_user!
before_action :require_not_suspended!
skip_before_action :require_functional!
def show def show
@export = Export.new(current_account) @export = Export.new(current_account)
@ -34,4 +37,8 @@ class Settings::ExportsController < Settings::BaseController
def lock_options def lock_options
{ redis: Redis.current, key: "backup:#{current_user.id}" } { redis: Redis.current, key: "backup:#{current_user.id}" }
end end
def require_not_suspended!
forbidden if current_account.suspended?
end
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' layout 'admin'
before_action :authenticate_user! before_action :authenticate_user!
before_action :require_not_suspended!
before_action :set_migrations
before_action :set_cooldown
skip_before_action :require_functional!
def show def show
@migration = Form::Migration.new(account: current_account.moved_to_account) @migration = current_account.migrations.build
end end
def update def create
@migration = Form::Migration.new(resource_params) @migration = current_account.migrations.build(resource_params)
if @migration.valid? && migration_account_changed? if @migration.save_with_challenge(current_user)
current_account.update!(moved_to_account: @migration.account) MoveService.new.call(@migration)
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)
redirect_to settings_migration_path, notice: I18n.t('migrations.updated_msg')
else else
render :show render :show
end end
end end
helper_method :on_cooldown?
private private
def resource_params def resource_params
params.require(:migration).permit(:acct) params.require(:account_migration).permit(:acct, :current_password, :current_username)
end end
def migration_account_changed? def set_migrations
current_account.moved_to_account_id != @migration.account&.id && @migrations = current_account.migrations.includes(:target_account).order(id: :desc).reject(&:new_record?)
current_account.id != @migration.account&.id 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
end end

View File

@ -55,7 +55,10 @@ class Settings::PreferencesController < Settings::BaseController
:setting_aggregate_reblogs, :setting_aggregate_reblogs,
:setting_show_application, :setting_show_application,
:setting_advanced_layout, :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) interactions: %i(must_be_follower must_be_following must_be_following_dm)
) )
end end

View File

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

View File

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

View File

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

View File

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

View File

@ -6,26 +6,10 @@ class SharesController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_body_classes before_action :set_body_classes
def show def show; end
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
@initial_state_json = serializable_resource.to_json
end
private 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 def set_body_classes
@body_classes = 'modal-layout compose-standalone' @body_classes = 'modal-layout compose-standalone'
end end

View File

@ -1,24 +1,25 @@
# frozen_string_literal: true # frozen_string_literal: true
class StatusesController < ApplicationController class StatusesController < ApplicationController
include StatusControllerConcern
include SignatureAuthentication include SignatureAuthentication
include Authorization include Authorization
include AccountOwnedConcern
ANCESTORS_LIMIT = 40
DESCENDANTS_LIMIT = 60
DESCENDANTS_DEPTH_LIMIT = 20
layout 'public' 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_status
before_action :set_instance_presenter before_action :set_instance_presenter
before_action :set_link_headers before_action :set_link_headers
before_action :check_account_suspension before_action :redirect_to_original, only: :show
before_action :redirect_to_original, only: [:show] before_action :set_referrer_policy_header, only: :show
before_action :set_referrer_policy_header, only: [:show]
before_action :set_cache_headers 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| content_security_policy only: :embed do |p|
p.frame_ancestors(false) p.frame_ancestors(false)
@ -28,27 +29,20 @@ class StatusesController < ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
expires_in 10.seconds, public: true if current_account.nil? expires_in 10.seconds, public: true if current_account.nil?
@body_classes = 'with-modals'
set_ancestors set_ancestors
set_descendants set_descendants
render 'stream_entries/show'
end end
format.json do format.json do
render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do expires_in 3.minutes, public: @status.distributable? && public_fetch_mode?
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter) render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter
end
end end
end end
end end
def activity def activity
render_cached_json(['activitypub', 'activity', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do expires_in 3.minutes, public: @status.distributable? && public_fetch_mode?
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter) render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter
end
end end
def embed def embed
@ -56,130 +50,24 @@ class StatusesController < ApplicationController
expires_in 180, public: true expires_in 180, public: true
response.headers['X-Frame-Options'] = 'ALLOWALL' response.headers['X-Frame-Options'] = 'ALLOWALL'
@autoplay = ActiveModel::Type::Boolean.new.cast(params[:autoplay])
render 'stream_entries/embed', layout: 'embedded' render layout: 'embedded'
end
def replies
render json: replies_collection_presenter,
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json',
skip_activities: true
end end
private private
def replies_collection_presenter def set_body_classes
page = ActivityPub::CollectionPresenter.new( @body_classes = 'with-modals'
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
end end
def set_link_headers def set_link_headers
response.headers['Link'] = LinkHeader.new( response.headers['Link'] = LinkHeader.new([[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]])
[
[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)]],
]
)
end end
def set_status def set_status
@status = @account.statuses.find(params[:id]) @status = @account.statuses.find(params[:id])
@stream_entry = @status.stream_entry
@type = @stream_entry.activity_type.downcase
authorize @status, :show? authorize @status, :show?
rescue Mastodon::NotPermittedError rescue Mastodon::NotPermittedError
# Reraise in order to get a 404
raise ActiveRecord::RecordNotFound raise ActiveRecord::RecordNotFound
end end
@ -187,39 +75,15 @@ class StatusesController < ApplicationController
@instance_presenter = InstancePresenter.new @instance_presenter = InstancePresenter.new
end end
def check_account_suspension
gone if @account.suspended?
end
def redirect_to_original 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 end
def set_referrer_policy_header def set_referrer_policy_header
return if @status.public_visibility? || @status.unlisted_visibility? response.headers['Referrer-Policy'] = 'origin' unless @status.distributable?
response.headers['Referrer-Policy'] = 'origin'
end end
def page_requested? def set_autoplay
params[:page] == 'true' @autoplay = truthy_param?(:autoplay)
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
end end
end end

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