Merge tag 'v3.0.0' into hometown-dev
This commit is contained in:
commit
228e0f0f6e
@ -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
|
||||||
|
56
.env.nanobox
56
.env.nanobox
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -1 +1 @@
|
|||||||
2.6.1
|
2.6.5
|
||||||
|
196
CHANGELOG.md
196
CHANGELOG.md
@ -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
|
||||||
|
|
||||||
|
20
Dockerfile
20
Dockerfile
@ -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
53
Gemfile
@ -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
|
||||||
|
270
Gemfile.lock
270
Gemfile.lock
@ -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
|
||||||
|
9
app.json
9
app.json
@ -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"
|
||||||
|
43
app/chewy/accounts_index.rb
Normal file
43
app/chewy/accounts_index.rb
Normal 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
|
@ -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
37
app/chewy/tags_index.rb
Normal 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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
11
app/controllers/activitypub/base_controller.rb
Normal file
11
app/controllers/activitypub/base_controller.rb
Normal 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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
71
app/controllers/activitypub/replies_controller.rb
Normal file
71
app/controllers/activitypub/replies_controller.rb
Normal 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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
40
app/controllers/admin/domain_allows_controller.rb
Normal file
40
app/controllers/admin/domain_allows_controller.rb
Normal 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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
30
app/controllers/api/v1/directories_controller.rb
Normal file
30
app/controllers/api/v1/directories_controller.rb
Normal 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
|
@ -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
|
40
app/controllers/api/v1/featured_tags_controller.rb
Normal file
40
app/controllers/api/v1/featured_tags_controller.rb
Normal 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
|
@ -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
|
||||||
|
@ -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
|
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
44
app/controllers/api/v1/markers_controller.rb
Normal file
44
app/controllers/api/v1/markers_controller.rb
Normal 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
|
@ -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
|
||||||
|
@ -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
|
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
@ -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
|
||||||
|
17
app/controllers/api/v1/trends_controller.rb
Normal file
17
app/controllers/api/v1/trends_controller.rb
Normal 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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
22
app/controllers/auth/challenges_controller.rb
Normal file
22
app/controllers/auth/challenges_controller.rb
Normal 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
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
58
app/controllers/auth/setup_controller.rb
Normal file
58
app/controllers/auth/setup_controller.rb
Normal 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
|
@ -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
|
||||||
|
34
app/controllers/concerns/account_owned_concern.rb
Normal file
34
app/controllers/concerns/account_owned_concern.rb
Normal 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
|
50
app/controllers/concerns/cache_concern.rb
Normal file
50
app/controllers/concerns/cache_concern.rb
Normal 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
|
65
app/controllers/concerns/challengable_concern.rb
Normal file
65
app/controllers/concerns/challengable_concern.rb
Normal 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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
87
app/controllers/concerns/status_controller_concern.rb
Normal file
87
app/controllers/concerns/status_controller_concern.rb
Normal 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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
22
app/controllers/instance_actors_controller.rb
Normal file
22
app/controllers/instance_actors_controller.rb
Normal 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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
|
43
app/controllers/settings/aliases_controller.rb
Normal file
43
app/controllers/settings/aliases_controller.rb
Normal 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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
45
app/controllers/settings/migration/redirects_controller.rb
Normal file
45
app/controllers/settings/migration/redirects_controller.rb
Normal 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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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')
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
Loading…
Reference in New Issue
Block a user