This commit is contained in:
Izalia Mae 2021-04-21 11:23:26 -04:00
commit a22789ee00
1445 changed files with 58244 additions and 18754 deletions

View file

@ -1,4 +1,3 @@
https://github.com/heroku/heroku-buildpack-apt
https://github.com/Scalingo/ffmpeg-buildpack
https://github.com/Scalingo/nodejs-buildpack
https://github.com/Scalingo/ruby-buildpack

View file

@ -72,11 +72,12 @@ aliases:
- run:
name: Set bundler settings
command: |
bundle config clean 'true'
bundle config deployment 'true'
bundle config with 'pam_authentication'
bundle config without 'development production'
bundle config frozen 'true'
bundle config --local clean 'true'
bundle config --local deployment 'true'
bundle config --local with 'pam_authentication'
bundle config --local without 'development production'
bundle config --local frozen 'true'
bundle config --local path $BUNDLE_PATH
- run:
name: Install bundler dependencies
command: bundle check || (bundle install && bundle clean)

View file

@ -27,10 +27,10 @@ plugins:
enabled: true
eslint:
enabled: true
channel: eslint-6
channel: eslint-7
rubocop:
enabled: true
channel: rubocop-0-82
channel: rubocop-1-9-1
sass-lint:
enabled: true
exclude_patterns:

23
.deepsource.toml Normal file
View file

@ -0,0 +1,23 @@
version = 1
test_patterns = ["app/javascript/mastodon/**/__tests__/**"]
exclude_patterns = [
"db/migrate/**",
"db/post_migrate/**"
]
[[analyzers]]
name = "ruby"
enabled = true
[[analyzers]]
name = "javascript"
enabled = true
[analyzers.meta]
environment = [
"browser",
"jest",
"nodejs"
]

View file

@ -13,3 +13,4 @@ vendor/bundle
postgres
redis
elasticsearch
chart

View file

@ -1,27 +1,15 @@
# Service dependencies
# You may set REDIS_URL instead for more advanced options
# You may also set REDIS_NAMESPACE to share Redis between multiple Mastodon servers
REDIS_HOST=redis
REDIS_PORT=6379
# You may set DATABASE_URL instead for more advanced options
DB_HOST=db
DB_USER=postgres
DB_NAME=postgres
DB_PASS=
DB_PORT=5432
# Optional ElasticSearch configuration
# You may also set ES_PREFIX to share the same cluster between multiple Mastodon servers (falls back to REDIS_NAMESPACE if not set)
# ES_ENABLED=true
# ES_HOST=es
# ES_PORT=9200
# This is a sample configuration file. You can generate your configuration
# with the `rake mastodon:setup` interactive setup wizard, but to customize
# your setup even further, you'll need to edit it manually. This sample does
# not demonstrate all available configuration options. Please look at
# https://docs.joinmastodon.org/admin/config/ for the full documentation.
# Federation
# Note: Changing LOCAL_DOMAIN at a later time will cause unwanted side effects, including breaking all existing federation.
# LOCAL_DOMAIN should *NOT* contain the protocol part of the domain e.g https://example.com.
# ----------
# This identifies your server and cannot be changed safely later
# ----------
LOCAL_DOMAIN=example.com
# Changing LOCAL_HTTPS in production is no longer supported. (Mastodon will always serve https:// links)
# Use this only if you need to run mastodon on a different domain than the one used for federation.
# You can read more about this option on https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Serving_a_different_domain.md
# DO *NOT* USE THIS UNLESS YOU KNOW *EXACTLY* WHAT YOU ARE DOING.
@ -32,107 +20,99 @@ LOCAL_DOMAIN=example.com
# be added. Comma separated values
# ALTERNATE_DOMAINS=example1.com,example2.com
# Application secrets
# Use HTTP proxy for outgoing request (optional)
# http_proxy=http://gateway.local:8118
# Access control for hidden service.
# ALLOW_ACCESS_TO_HIDDEN_SERVICE=true
# Authorized fetch mode (optional)
# Require remote servers to authentify when fetching toots, see
# https://docs.joinmastodon.org/admin/config/#authorized_fetch
# AUTHORIZED_FETCH=true
# Limited federation mode (optional)
# Only allow federation with specific domains, see
# https://docs.joinmastodon.org/admin/config/#whitelist_mode
# LIMITED_FEDERATION_MODE=true
# Redis
# -----
REDIS_HOST=localhost
REDIS_PORT=6379
# PostgreSQL
# ----------
DB_HOST=/var/run/postgresql
DB_USER=mastodon
DB_NAME=mastodon_production
DB_PASS=
DB_PORT=5432
# ElasticSearch (optional)
# ------------------------
#ES_ENABLED=true
#ES_HOST=localhost
#ES_PORT=9200
# Secrets
# -------
# Generate each with the `RAILS_ENV=production bundle exec rake secret` task (`docker-compose run --rm web bundle exec rake secret` if you use docker compose)
# -------
SECRET_KEY_BASE=
OTP_SECRET=
# VAPID keys (used for push notifications
# You can generate the keys using the following command (first is the private key, second is the public one)
# Web Push
# --------
# Generate with `rake mastodon:webpush:generate_vapid_key` (first is the private key, second is the public one)
# You should only generate this once per instance. If you later decide to change it, all push subscription will
# be invalidated, requiring the users to access the website again to resubscribe.
#
# Generate with `RAILS_ENV=production bundle exec rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web bundle exec rake mastodon:webpush:generate_vapid_key` if you use docker compose)
#
# For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html
# --------
VAPID_PRIVATE_KEY=
VAPID_PUBLIC_KEY=
# Registrations
# -------------
# Single user mode will disable registrations and redirect frontpage to the first profile
# SINGLE_USER_MODE=true
# Prevent registrations with following e-mail domains
# EMAIL_DOMAIN_BLACKLIST=example1.com|example2.de|etc
# Only allow registrations with the following e-mail domains
# EMAIL_DOMAIN_WHITELIST=example1.com|example2.de|etc
# Prevent registrations with following e-mail domains
# EMAIL_DOMAIN_DENYLIST=example1.com|example2.de|etc
# Only allow registrations with the following e-mail domains
# EMAIL_DOMAIN_ALLOWLIST=example1.com|example2.de|etc
#TODO move this
# Optionally change default language
# DEFAULT_LOCALE=de
# E-mail configuration
# Note: Mailgun and SparkPost (https://sparkpo.st/smtp) each have good free tiers
# If you want to use an SMTP server without authentication (e.g local Postfix relay)
# then set SMTP_AUTH_METHOD and SMTP_OPENSSL_VERIFY_MODE to 'none' and
# *comment* SMTP_LOGIN and SMTP_PASSWORD (leaving them blank is not enough).
# Sending mail
# ------------
SMTP_SERVER=smtp.mailgun.org
SMTP_PORT=587
SMTP_LOGIN=
SMTP_PASSWORD=
SMTP_FROM_ADDRESS=notifications@example.com
#SMTP_REPLY_TO=
#SMTP_DOMAIN= # defaults to LOCAL_DOMAIN
#SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail
#SMTP_AUTH_METHOD=plain
#SMTP_CA_FILE=/etc/ssl/certs/ca-certificates.crt
#SMTP_OPENSSL_VERIFY_MODE=peer
#SMTP_ENABLE_STARTTLS_AUTO=true
#SMTP_TLS=true
SMTP_FROM_ADDRESS=notificatons@example.com
# Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files.
# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system
# PAPERCLIP_ROOT_URL=/system
# Optional asset host for multi-server setups
# The asset host must allow cross origin request from WEB_DOMAIN or LOCAL_DOMAIN
# if WEB_DOMAIN is not set. For example, the server may have the
# following header field:
# Access-Control-Allow-Origin: https://example.com/
# CDN_HOST=https://assets.example.com
# Optional list of hosts that are allowed to serve media for your instance
# This is useful if you include external media in your custom CSS or about page,
# or if your data storage provider makes use of redirects to other domains.
# EXTRA_DATA_HOSTS=https://data.example1.com|https://data.example2.com
# S3 (optional)
# File storage (optional)
# -----------------------
# The attachment host must allow cross origin request from WEB_DOMAIN or
# LOCAL_DOMAIN if WEB_DOMAIN is not set. For example, the server may have the
# following header field:
# Access-Control-Allow-Origin: https://192.168.1.123:9000/
# S3_ENABLED=true
# S3_BUCKET=
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# S3_REGION=
# S3_PROTOCOL=http
# S3_HOSTNAME=192.168.1.123:9000
# S3 (Minio Config (optional) Please check Minio instance for details)
# The attachment host must allow cross origin request - see the description
# above.
# S3_ENABLED=true
# S3_BUCKET=
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# S3_REGION=
# S3_PROTOCOL=https
# S3_HOSTNAME=
# S3_ENDPOINT=
# S3_SIGNATURE_VERSION=
# Google Cloud Storage (optional)
# Use S3 compatible API. Since GCS does not support Multipart Upload,
# increase the value of S3_MULTIPART_THRESHOLD to disable Multipart Upload.
# The attachment host must allow cross origin request - see the description
# above.
# S3_ENABLED=true
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# S3_REGION=
# S3_PROTOCOL=https
# S3_HOSTNAME=storage.googleapis.com
# S3_ENDPOINT=https://storage.googleapis.com
# S3_MULTIPART_THRESHOLD=52428801 # 50.megabytes
# -----------------------
#S3_ENABLED=true
#S3_BUCKET=files.example.com
#AWS_ACCESS_KEY_ID=
#AWS_SECRET_ACCESS_KEY=
#S3_ALIAS_HOST=files.example.com
# Swift (optional)
# The attachment host must allow cross origin request - see the description
@ -155,50 +135,27 @@ SMTP_FROM_ADDRESS=notifications@example.com
# Defaults to 60 seconds. Set to 0 to disable
# SWIFT_CACHE_TTL=
# 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
# Optional list of hosts that are allowed to serve media for your instance
# This is useful if you include external media in your custom CSS or about page,
# or if your data storage provider makes use of redirects to other domains.
# EXTRA_DATA_HOSTS=https://data.example1.com|https://data.example2.com
# Optional alias for S3 (e.g. to serve files on a custom domain, possibly using Cloudfront or Cloudflare)
# S3_ALIAS_HOST=
# Streaming API integration
# STREAMING_API_BASE_URL=
# Advanced settings
# If you need to use pgBouncer, you need to disable prepared statements:
# PREPARED_STATEMENTS=false
# Cluster number setting for streaming API server.
# If you comment out following line, cluster number will be `numOfCpuCores - 1`.
STREAMING_CLUSTER_NUM=1
# Docker mastodon user
# If you use Docker, you may want to assign UID/GID manually.
# UID=1000
# GID=1000
# Maximum allowed character count
# MAX_TOOT_CHARS=500
# Maximum number of pinned posts
# MAX_PINNED_TOOTS=5
# Maximum allowed bio characters
# MAX_BIO_CHARS=500
# Maximim number of profile fields allowed
# MAX_PROFILE_FIELDS=4
# Maximum allowed display name characters
# MAX_DISPLAY_NAME_CHARS=30
# Maximum image and video/audio upload sizes
# Units are in bytes
# 1048576 bytes equals 1 megabyte
# MAX_IMAGE_SIZE=8388608
# MAX_VIDEO_SIZE=41943040
# Maximum search results to display
# Only relevant when elasticsearch is installed
# MAX_SEARCH_RESULTS=20
# External authentication (optional)
# ----------------------------------
# LDAP authentication (optional)
# LDAP_ENABLED=true
# LDAP_HOST=localhost
@ -276,17 +233,39 @@ STREAMING_CLUSTER_NUM=1
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED=
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL=
# Use HTTP proxy for outgoing request (optional)
# http_proxy=http://gateway.local:8118
# Access control for hidden service.
# ALLOW_ACCESS_TO_HIDDEN_SERVICE=true
# Authorized fetch mode (optional)
# Require remote servers to authentify when fetching toots, see
# https://docs.joinmastodon.org/admin/config/#authorized_fetch
# AUTHORIZED_FETCH=true
# Custom settings
# ---------------
# Various ways to customize Mastodon's behavior
# ---------------
# Maximum allowed character count
MAX_TOOT_CHARS=500
# Whitelist mode (optional)
# Only allow federation with whitelisted domains, see
# https://docs.joinmastodon.org/admin/config/#whitelist_mode
# WHITELIST_MODE=true
# Maximum number of pinned posts
MAX_PINNED_TOOTS=5
# Maximum allowed bio characters
MAX_BIO_CHARS=500
# Maximim number of profile fields allowed
MAX_PROFILE_FIELDS=4
# Maximum allowed display name characters
MAX_DISPLAY_NAME_CHARS=30
# Maximum allowed poll options
MAX_POLL_OPTIONS=5
# Maximum allowed poll option characters
MAX_POLL_OPTION_CHARS=100
# Maximum image and video/audio upload sizes
# Units are in bytes
# 1048576 bytes equals 1 megabyte
# MAX_IMAGE_SIZE=8388608
# MAX_VIDEO_SIZE=41943040
# Maximum search results to display
# Only relevant when elasticsearch is installed
# MAX_SEARCH_RESULTS=20

View file

@ -199,6 +199,11 @@ module.exports = {
'import/no-unresolved': 'error',
'import/no-webpack-loader-syntax': 'error',
'promise/catch-or-return': 'error',
'promise/catch-or-return': [
'error',
{
allowFinally: true,
},
],
},
};

1
.github/FUNDING.yml vendored
View file

@ -1,2 +1,3 @@
patreon: mastodon
open_collective: mastodon
github: [Gargron]

View file

@ -1,7 +1,7 @@
---
name: Bug Report
about: Create a report to help us improve
about: If something isn't working as expected
labels: bug
---
[Issue text goes here].

View file

@ -1,7 +1,6 @@
---
name: Feature Request
about: I have a suggestion
---
<!-- Please use a concise and distinct title for the issue -->

25
.gitignore vendored
View file

@ -17,31 +17,34 @@
/log/*
!/log/.keep
/tmp
coverage
public/system
public/assets
public/packs
public/packs-test
/coverage
/public/system
/public/assets
/public/packs
/public/packs-test
.env
.env.production
.env.development
node_modules/
build/
/node_modules/
/build/
# Ignore Vagrant files
.vagrant/
# Ignore Capistrano customizations
config/deploy/*
/config/deploy/*
# Ignore IDE files
.vscode/
.idea/
# Ignore postgres + redis + elasticsearch volume optionally created by docker-compose
postgres
redis
elasticsearch
/postgres
/redis
/elasticsearch
# ignore Helm dependency charts
/chart/charts/*.tgz
# Ignore Apple files
.DS_Store

View file

@ -2,7 +2,8 @@ require:
- rubocop-rails
AllCops:
TargetRubyVersion: 2.4
TargetRubyVersion: 2.5
NewCops: disable
Exclude:
- 'spec/**/*'
- 'db/**/*'
@ -25,26 +26,68 @@ Layout/AccessModifierIndentation:
Layout/EmptyLineAfterMagicComment:
Enabled: false
Layout/EmptyLineAfterGuardClause:
Enabled: false
Layout/EmptyLinesAroundAttributeAccessor:
Enabled: true
Layout/HashAlignment:
Enabled: false
# EnforcedHashRocketStyle: table
# EnforcedColonStyle: table
Layout/SpaceAroundMethodCallOperator:
Enabled: true
Layout/SpaceInsideHashLiteralBraces:
EnforcedStyle: space
Lint/DeprecatedOpenSSLConstant:
Enabled: true
Lint/DuplicateElsifCondition:
Enabled: true
Lint/MixedRegexpCaptureTypes:
Enabled: true
Lint/RaiseException:
Enabled: true
Lint/StructNewOverride:
Enabled: true
Lint/UselessAccessModifier:
ContextCreatingMethods:
- class_methods
Metrics/AbcSize:
Max: 100
Exclude:
- 'lib/mastodon/*_cli.rb'
Metrics/BlockLength:
Max: 35
Max: 55
Exclude:
- 'lib/tasks/**/*'
- 'lib/mastodon/*_cli.rb'
Metrics/BlockNesting:
Max: 3
Exclude:
- 'lib/mastodon/*_cli.rb'
Metrics/ClassLength:
CountComments: false
Max: 300
Max: 400
Exclude:
- 'lib/mastodon/*_cli.rb'
Metrics/CyclomaticComplexity:
Max: 25
Exclude:
- 'lib/mastodon/*_cli.rb'
Layout/LineLength:
AllowURI: true
@ -52,7 +95,9 @@ Layout/LineLength:
Metrics/MethodLength:
CountComments: false
Max: 55
Max: 65
Exclude:
- 'lib/mastodon/*_cli.rb'
Metrics/ModuleLength:
CountComments: false
@ -63,34 +108,90 @@ Metrics/ParameterLists:
CountKeywordArgs: true
Metrics/PerceivedComplexity:
Max: 20
Max: 25
Naming/MemoizedInstanceVariableName:
Enabled: false
Naming/MethodParameterName:
Enabled: true
Rails:
Enabled: true
Rails/ApplicationController:
Enabled: false
Exclude:
- 'app/controllers/well_known/**/*.rb'
Rails/BelongsTo:
Enabled: false
Rails/ContentTag:
Enabled: false
Rails/EnumHash:
Enabled: false
Rails/HasAndBelongsToMany:
Enabled: false
Rails/SkipsModelValidations:
Enabled: false
Rails/HttpStatus:
Enabled: false
Rails/Exit:
Exclude:
- 'lib/mastodon/*'
- 'lib/cli.rb'
Rails/FilePath:
Enabled: false
Rails/HasAndBelongsToMany:
Enabled: false
Rails/HasManyOrHasOneDependent:
Enabled: false
Rails/HelperInstanceVariable:
Enabled: false
Rails/HttpStatus:
Enabled: false
Rails/IndexBy:
Enabled: false
Rails/InverseOf:
Enabled: false
Rails/LexicallyScopedActionFilter:
Enabled: false
Rails/OutputSafety:
Enabled: true
Rails/RakeEnvironment:
Enabled: false
Rails/RedundantForeignKey:
Enabled: false
Rails/SkipsModelValidations:
Enabled: false
Rails/UniqueValidationWithoutIndex:
Enabled: false
Style/AccessorGrouping:
Enabled: true
Style/AccessModifierDeclarations:
Enabled: false
Style/ArrayCoercion:
Enabled: true
Style/BisectedAttrAccessor:
Enabled: true
Style/CaseLikeIf:
Enabled: false
Style/ClassAndModuleChildren:
Enabled: false
@ -105,6 +206,15 @@ Style/Documentation:
Style/DoubleNegation:
Enabled: true
Style/ExpandPathArguments:
Enabled: false
Style/ExponentialNotation:
Enabled: true
Style/FormatString:
Enabled: false
Style/FormatStringToken:
Enabled: false
@ -114,9 +224,33 @@ Style/FrozenStringLiteralComment:
Style/GuardClause:
Enabled: false
Style/HashAsLastArrayItem:
Enabled: false
Style/HashEachMethods:
Enabled: true
Style/HashLikeCase:
Enabled: true
Style/HashTransformKeys:
Enabled: true
Style/HashTransformValues:
Enabled: false
Style/IfUnlessModifier:
Enabled: false
Style/InverseMethods:
Enabled: false
Style/Lambda:
Enabled: false
Style/MutableConstant:
Enabled: false
Style/PercentLiteralDelimiters:
PreferredDelimiters:
'%i': '()'
@ -125,9 +259,36 @@ Style/PercentLiteralDelimiters:
Style/PerlBackrefs:
AutoCorrect: false
Style/RedundantAssignment:
Enabled: false
Style/RedundantFetchBlock:
Enabled: true
Style/RedundantFileExtensionInRequire:
Enabled: true
Style/RedundantRegexpCharacterClass:
Enabled: false
Style/RedundantRegexpEscape:
Enabled: false
Style/RedundantReturn:
Enabled: true
Style/RegexpLiteral:
Enabled: false
Style/RescueStandardError:
Enabled: false
Style/SignalException:
Enabled: false
Style/SlicingWithRange:
Enabled: true
Style/SymbolArray:
Enabled: false
@ -136,3 +297,6 @@ Style/TrailingCommaInArrayLiteral:
Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: 'comma'
Style/UnpackFirst:
Enabled: false

View file

@ -1 +1 @@
2.6.6
2.7.3

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,6 @@ libidn11
libidn11-dev
libpq-dev
libprotobuf-dev
libssl-dev
libxdamage1
libxfixes3
protobuf-compiler
@ -23,7 +22,7 @@ libpixman-1-0
librsvg2-2
libthai-data
libthai0
libvpx5
libvpx[5-9]
libxcb-render0
libxcb-shm0
libxrender1

View file

@ -3,7 +3,398 @@ Changelog
All notable changes to this project will be documented in this file.
## [v3.1.4] - 2020-05-14
## [3.3.0] - 2020-12-27
### Added
- **Add hotkeys for audio/video control in web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/15158), [Gargron](https://github.com/tootsuite/mastodon/pull/15198))
- `Space` and `k` to toggle playback
- `m` to toggle mute
- `f` to toggle fullscreen
- `j` and `l` to go back and forward by 10 seconds
- `.` and `,` to go back and forward by a frame (video only)
- Add expand/compress button on media modal in web UI ([mashirozx](https://github.com/tootsuite/mastodon/pull/15068), [mashirozx](https://github.com/tootsuite/mastodon/pull/15088), [mashirozx](https://github.com/tootsuite/mastodon/pull/15094))
- Add border around 🕺 emoji in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14769))
- Add border around 🐞 emoji in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14712))
- Add home link to the getting started column when home isn't mounted ([ThibG](https://github.com/tootsuite/mastodon/pull/14707))
- Add option to disable swiping motions across the web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13885))
- **Add pop-out player for audio/video in web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/14870), [Gargron](https://github.com/tootsuite/mastodon/pull/15157), [Gargron](https://github.com/tootsuite/mastodon/pull/14915), [noellabo](https://github.com/tootsuite/mastodon/pull/15309))
- Continue watching/listening when you scroll away
- Action bar to interact with/open toot from the pop-out player
- Add unread notification markers in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14818), [ThibG](https://github.com/tootsuite/mastodon/pull/14960), [ThibG](https://github.com/tootsuite/mastodon/pull/14954), [noellabo](https://github.com/tootsuite/mastodon/pull/14897), [noellabo](https://github.com/tootsuite/mastodon/pull/14907))
- Add paragraph about browser add-ons when encountering errors in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14801))
- Add import and export for bookmarks ([ThibG](https://github.com/tootsuite/mastodon/pull/14956))
- Add cache buster feature for media files ([Gargron](https://github.com/tootsuite/mastodon/pull/15155))
- If you have a proxy cache in front of object storage, deleted files will persist until the cache expires
- If enabled, cache buster will make a special request to the proxy to signal a cache reset
- Add duration option to the mute function ([aquarla](https://github.com/tootsuite/mastodon/pull/13831))
- Add replies policy option to the list function ([ThibG](https://github.com/tootsuite/mastodon/pull/9205), [trwnh](https://github.com/tootsuite/mastodon/pull/15304))
- Add `og:published_time` OpenGraph tags on toots ([nornagon](https://github.com/tootsuite/mastodon/pull/14865))
- **Add option to be notified when a followed user posts** ([Gargron](https://github.com/tootsuite/mastodon/pull/13546), [ThibG](https://github.com/tootsuite/mastodon/pull/14896), [Gargron](https://github.com/tootsuite/mastodon/pull/14822))
- If you don't want to miss a toot, click the bell button!
- Add client-side validation in password change forms ([ThibG](https://github.com/tootsuite/mastodon/pull/14564))
- Add client-side validation in the registration form ([ThibG](https://github.com/tootsuite/mastodon/pull/14560), [ThibG](https://github.com/tootsuite/mastodon/pull/14599))
- Add support for Gemini URLs ([joshleeb](https://github.com/tootsuite/mastodon/pull/15013))
- Add app shortcuts to web app manifest ([mkljczk](https://github.com/tootsuite/mastodon/pull/15234))
- Add WebAuthn as an alternative 2FA method ([santiagorodriguez96](https://github.com/tootsuite/mastodon/pull/14466), [jiikko](https://github.com/tootsuite/mastodon/pull/14806))
- Add honeypot fields and minimum fill-out time for sign-up form ([ThibG](https://github.com/tootsuite/mastodon/pull/15276))
- Add icon for mutual relationships in relationship manager ([noellabo](https://github.com/tootsuite/mastodon/pull/15149))
- Add follow selected followers button in relationship manager ([noellabo](https://github.com/tootsuite/mastodon/pull/15148))
- **Add subresource integrity for JS and CSS assets** ([Gargron](https://github.com/tootsuite/mastodon/pull/15096))
- If you use a CDN for static assets (JavaScript, CSS, and so on), you have to trust that the CDN does not modify the assets maliciously
- Subresource integrity compares server-generated asset digests with what's actually served from the CDN and prevents such attacks
- Add `ku`, `sa`, `sc`, `zgh` to available locales ([ykzts](https://github.com/tootsuite/mastodon/pull/15138))
- Add ability to force an account to mark media as sensitive ([noellabo](https://github.com/tootsuite/mastodon/pull/14361))
- **Add ability to block access or limit sign-ups from chosen IPs** ([Gargron](https://github.com/tootsuite/mastodon/pull/14963), [ThibG](https://github.com/tootsuite/mastodon/pull/15263))
- Add rules for IPs or CIDR ranges that automatically expire after a configurable amount of time
- Choose the severity of the rule, either blocking all access or merely limiting sign-ups
- **Add support for reversible suspensions through ActivityPub** ([Gargron](https://github.com/tootsuite/mastodon/pull/14989))
- Servers can signal that one of their accounts has been suspended
- During suspension, the account can only delete its own content
- A reversal of the suspension can be signalled the same way
- A local suspension always overrides a remote one
- Add indication to admin UI of whether a report has been forwarded ([ThibG](https://github.com/tootsuite/mastodon/pull/13237))
- Add display of reasons for joining of an account in admin UI ([mashirozx](https://github.com/tootsuite/mastodon/pull/15265))
- Add option to obfuscate domain name in public list of domain blocks ([Gargron](https://github.com/tootsuite/mastodon/pull/15355))
- Add option to make reasons for joining required on sign-up ([ThibG](https://github.com/tootsuite/mastodon/pull/15326), [ThibG](https://github.com/tootsuite/mastodon/pull/15358), [ThibG](https://github.com/tootsuite/mastodon/pull/15385), [ThibG](https://github.com/tootsuite/mastodon/pull/15405))
- Add ActivityPub follower synchronization mechanism ([ThibG](https://github.com/tootsuite/mastodon/pull/14510), [ThibG](https://github.com/tootsuite/mastodon/pull/15026))
- Add outbox attribute to instance actor ([ThibG](https://github.com/tootsuite/mastodon/pull/14721))
- Add featured hashtags as an ActivityPub collection ([Gargron](https://github.com/tootsuite/mastodon/pull/11595), [noellabo](https://github.com/tootsuite/mastodon/pull/15277))
- Add support for dereferencing objects through bearcaps ([Gargron](https://github.com/tootsuite/mastodon/pull/14683), [noellabo](https://github.com/tootsuite/mastodon/pull/14981))
- Add `S3_READ_TIMEOUT` environment variable ([tateisu](https://github.com/tootsuite/mastodon/pull/14952))
- Add `ALLOWED_PRIVATE_ADDRESSES` environment variable ([ThibG](https://github.com/tootsuite/mastodon/pull/14722))
- Add `--fix-permissions` option to `tootctl media remove-orphans` ([Gargron](https://github.com/tootsuite/mastodon/pull/14383), [uist1idrju3i](https://github.com/tootsuite/mastodon/pull/14715))
- Add `tootctl accounts merge` ([Gargron](https://github.com/tootsuite/mastodon/pull/15201), [ThibG](https://github.com/tootsuite/mastodon/pull/15264), [ThibG](https://github.com/tootsuite/mastodon/pull/15256))
- Has someone changed their domain or subdomain thereby creating two accounts where there should be one?
- This command will fix it on your end
- Add `tootctl maintenance fix-duplicates` ([ThibG](https://github.com/tootsuite/mastodon/pull/14860), [Gargron](https://github.com/tootsuite/mastodon/pull/15223), [ThibG](https://github.com/tootsuite/mastodon/pull/15373))
- Index corruption in the database?
- This command is for you
- **Add support for managing multiple stream subscriptions in a single connection** ([Gargron](https://github.com/tootsuite/mastodon/pull/14524), [Gargron](https://github.com/tootsuite/mastodon/pull/14566), [mfmfuyu](https://github.com/tootsuite/mastodon/pull/14859), [zunda](https://github.com/tootsuite/mastodon/pull/14608))
- Previously, getting live updates for multiple timelines required opening a HTTP or WebSocket connection for each
- More connections means more resource consumption on both ends, not to mention the (ever so slight) delay when establishing a new connection
- Now, with just a single WebSocket connection you can subscribe and unsubscribe to and from multiple streams
- Add support for limiting results by both `min_id` and `max_id` at the same time in REST API ([tateisu](https://github.com/tootsuite/mastodon/pull/14776))
- Add `GET /api/v1/accounts/:id/featured_tags` to REST API ([noellabo](https://github.com/tootsuite/mastodon/pull/11817), [noellabo](https://github.com/tootsuite/mastodon/pull/15270))
- Add stoplight for object storage failures, return HTTP 503 in REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/13043))
- Add optional `tootctl remove media` cronjob in Helm chart ([dunn](https://github.com/tootsuite/mastodon/pull/14396))
- Add clean error message when `RAILS_ENV` is unset ([ThibG](https://github.com/tootsuite/mastodon/pull/15381))
### Changed
- **Change media modals look in web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/15217), [Gargron](https://github.com/tootsuite/mastodon/pull/15221), [Gargron](https://github.com/tootsuite/mastodon/pull/15284), [Gargron](https://github.com/tootsuite/mastodon/pull/15283), [Kjwon15](https://github.com/tootsuite/mastodon/pull/15308), [noellabo](https://github.com/tootsuite/mastodon/pull/15305), [ThibG](https://github.com/tootsuite/mastodon/pull/15417))
- Background of the overlay matches the color of the image
- Action bar to interact with or open the toot from the modal
- Change order of announcements in admin UI to be newest-first ([ThibG](https://github.com/tootsuite/mastodon/pull/15091))
- **Change account suspensions to be reversible by default** ([Gargron](https://github.com/tootsuite/mastodon/pull/14726), [ThibG](https://github.com/tootsuite/mastodon/pull/15152), [ThibG](https://github.com/tootsuite/mastodon/pull/15106), [ThibG](https://github.com/tootsuite/mastodon/pull/15100), [ThibG](https://github.com/tootsuite/mastodon/pull/15099), [noellabo](https://github.com/tootsuite/mastodon/pull/14855), [ThibG](https://github.com/tootsuite/mastodon/pull/15380), [Gargron](https://github.com/tootsuite/mastodon/pull/15420), [Gargron](https://github.com/tootsuite/mastodon/pull/15414))
- Suspensions no longer equal deletions
- A suspended account can be unsuspended with minimal consequences for 30 days
- Immediate deletion of data is still available as an explicit option
- Suspended accounts can request an archive of their data through the UI
- Change REST API to return empty data for suspended accounts (14765)
- Change web UI to show empty profile for suspended accounts ([Gargron](https://github.com/tootsuite/mastodon/pull/14766), [Gargron](https://github.com/tootsuite/mastodon/pull/15345))
- Change featured hashtag suggestions to be recently used instead of most used ([abcang](https://github.com/tootsuite/mastodon/pull/14760))
- Change direct toots to appear in the home feed again ([Gargron](https://github.com/tootsuite/mastodon/pull/14711), [ThibG](https://github.com/tootsuite/mastodon/pull/15182), [noellabo](https://github.com/tootsuite/mastodon/pull/14727))
- Return to treating all toots the same instead of trying to retrofit direct visibility into an instant messaging model
- Change email address validation to return more specific errors ([ThibG](https://github.com/tootsuite/mastodon/pull/14565))
- Change HTTP signature requirements to include `Digest` header on `POST` requests ([ThibG](https://github.com/tootsuite/mastodon/pull/15069))
- Change click area of video/audio player buttons to be bigger in web UI ([ariasuni](https://github.com/tootsuite/mastodon/pull/15049))
- Change order of filters by alphabetic by "keyword or phrase" ([ariasuni](https://github.com/tootsuite/mastodon/pull/15050))
- Change suspension of remote accounts to also undo outgoing follows ([ThibG](https://github.com/tootsuite/mastodon/pull/15188))
- Change string "Home" to "Home and lists" in the filter creation screen ([ariasuni](https://github.com/tootsuite/mastodon/pull/15139))
- Change string "Boost to original audience" to "Boost with original visibility" in web UI ([3n-k1](https://github.com/tootsuite/mastodon/pull/14598))
- Change string "Show more" to "Show newer" and "Show older" on public pages ([ariasuni](https://github.com/tootsuite/mastodon/pull/15052))
- Change order of announcements to be reverse chronological in web UI ([dariusk](https://github.com/tootsuite/mastodon/pull/15065), [dariusk](https://github.com/tootsuite/mastodon/pull/15070))
- Change RTL detection to rely on unicode-bidi paragraph by paragraph in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/14573))
- Change visibility icon next to timestamp to be clickable in web UI ([ariasuni](https://github.com/tootsuite/mastodon/pull/15053), [mayaeh](https://github.com/tootsuite/mastodon/pull/15055))
- Change public thread view to hide "Show thread" link ([ThibG](https://github.com/tootsuite/mastodon/pull/15266))
- Change number format on about page from full to shortened ([Gargron](https://github.com/tootsuite/mastodon/pull/15327))
- Change how scheduled tasks run in multi-process environments ([noellabo](https://github.com/tootsuite/mastodon/pull/15314))
- New dedicated queue `scheduler`
- Runs by default when Sidekiq is executed with no options
- Has to be added manually in a multi-process environment
### Removed
- Remove fade-in animation from modals in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/15199))
- Remove auto-redirect to direct messages in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/15142))
- Remove obsolete IndexedDB operations from web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/14730))
- Remove dependency on unused and unmaintained http_parser.rb gem ([ThibG](https://github.com/tootsuite/mastodon/pull/14574))
### Fixed
- Fix layout on about page when contact account has a long username ([ThibG](https://github.com/tootsuite/mastodon/pull/15357))
- Fix follow limit preventing re-following of a moved account ([Gargron](https://github.com/tootsuite/mastodon/pull/14207), [ThibG](https://github.com/tootsuite/mastodon/pull/15384))
- **Fix deletes not reaching every server that interacted with toot** ([Gargron](https://github.com/tootsuite/mastodon/pull/15200))
- Previously, delete of a toot would be primarily sent to the followers of its author, people mentioned in the toot, and people who reblogged the toot
- Now, additionally, it is ensured that it is sent to people who replied to it, favourited it, and to the person it replies to even if that person is not mentioned
- Fix resolving an account through its non-canonical form (i.e. alternate domain) ([ThibG](https://github.com/tootsuite/mastodon/pull/15187))
- Fix sending redundant ActivityPub events when processing remote account deletion ([ThibG](https://github.com/tootsuite/mastodon/pull/15104))
- Fix Move handler not being triggered when failing to fetch target account ([ThibG](https://github.com/tootsuite/mastodon/pull/15107))
- Fix downloading remote media files when server returns empty filename ([ThibG](https://github.com/tootsuite/mastodon/pull/14867))
- Fix account processing failing because of large collections ([ThibG](https://github.com/tootsuite/mastodon/pull/15027))
- Fix not being able to unfavorite toots one has lost access to ([ThibG](https://github.com/tootsuite/mastodon/pull/15192))
- Fix not being able to unbookmark toots one has lost access to ([ThibG](https://github.com/tootsuite/mastodon/pull/14604))
- Fix possible casing inconsistencies in hashtag search ([ThibG](https://github.com/tootsuite/mastodon/pull/14906))
- Fix updating account counters when association is not yet created ([Gargron](https://github.com/tootsuite/mastodon/pull/15108))
- Fix cookies not having a SameSite attribute ([Gargron](https://github.com/tootsuite/mastodon/pull/15098))
- Fix poll ending notifications being created for each vote ([ThibG](https://github.com/tootsuite/mastodon/pull/15071))
- Fix multiple boosts of a same toot erroneously appearing in TL ([ThibG](https://github.com/tootsuite/mastodon/pull/14759))
- Fix asset builds not picking up `CDN_HOST` change ([ThibG](https://github.com/tootsuite/mastodon/pull/14381))
- Fix desktop notifications permission prompt in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/14985), [Gargron](https://github.com/tootsuite/mastodon/pull/15141), [ThibG](https://github.com/tootsuite/mastodon/pull/13543), [ThibG](https://github.com/tootsuite/mastodon/pull/15176))
- Some time ago, browsers added a requirement that desktop notification prompts could only be displayed in response to a user-generated event (such as a click)
- This means that for some time, users who haven't already given the permission before were not getting a prompt and as such were not receiving desktop notifications
- Fix "Mark media as sensitive" string not supporting pluralizations in other languages in web UI ([ariasuni](https://github.com/tootsuite/mastodon/pull/15051))
- Fix glitched image uploads when canvas read access is blocked in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/15180))
- Fix some account gallery items having empty labels in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/15073))
- Fix alt-key hotkeys activating while typing in a text field in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14942))
- Fix wrong seek bar width on media player in web UI ([mfmfuyu](https://github.com/tootsuite/mastodon/pull/15060))
- Fix logging out on mobile in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14901))
- Fix wrong click area for GIFVs in media modal in web UI ([noellabo](https://github.com/tootsuite/mastodon/pull/14615))
- Fix unreadable placeholder text color in high contrast theme in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/14803))
- Fix scrolling issues when closing some dropdown menus in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14606))
- Fix notification filter bar incorrectly filtering gaps in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14808))
- Fix disabled boost icon being replaced by private boost icon on hover in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14456))
- Fix hashtag detection in compose form being different to server-side in web UI ([kedamaDQ](https://github.com/tootsuite/mastodon/pull/14484), [ThibG](https://github.com/tootsuite/mastodon/pull/14513))
- Fix home last read marker mishandling gaps in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14809))
- Fix unnecessary re-rendering of various components when typing in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/15286))
- Fix notifications being unnecessarily re-rendered in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/15312))
- Fix column swiping animation logic in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/15301))
- Fix inefficiency when fetching hashtag timeline ([noellabo](https://github.com/tootsuite/mastodon/pull/14861), [akihikodaki](https://github.com/tootsuite/mastodon/pull/14662))
- Fix inefficiency when fetching bookmarks ([akihikodaki](https://github.com/tootsuite/mastodon/pull/14674))
- Fix inefficiency when fetching favourites ([akihikodaki](https://github.com/tootsuite/mastodon/pull/14673))
- Fix inefficiency when fetching media-only account timeline ([akihikodaki](https://github.com/tootsuite/mastodon/pull/14675))
- Fix inefficieny when deleting accounts ([Gargron](https://github.com/tootsuite/mastodon/pull/15387), [ThibG](https://github.com/tootsuite/mastodon/pull/15409), [ThibG](https://github.com/tootsuite/mastodon/pull/15407), [ThibG](https://github.com/tootsuite/mastodon/pull/15408), [ThibG](https://github.com/tootsuite/mastodon/pull/15402), [ThibG](https://github.com/tootsuite/mastodon/pull/15416), [Gargron](https://github.com/tootsuite/mastodon/pull/15421))
- Fix redundant query when processing batch actions on custom emojis ([niwatori24](https://github.com/tootsuite/mastodon/pull/14534))
- Fix slow distinct queries where grouped queries are faster ([Gargron](https://github.com/tootsuite/mastodon/pull/15287))
- Fix performance on instances list in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/15282))
- Fix server actor appearing in list of accounts in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14567))
- Fix "bootstrap timeline accounts" toggle in site settings in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/15325))
- Fix PostgreSQL secret name for cronjob in Helm chart ([metal3d](https://github.com/tootsuite/mastodon/pull/15072))
- Fix Procfile not being compatible with herokuish ([acuteaura](https://github.com/tootsuite/mastodon/pull/12685))
- Fix installation of tini being split into multiple steps in Dockerfile ([ryncsn](https://github.com/tootsuite/mastodon/pull/14686))
### Security
- Fix streaming API allowing connections to persist after access token invalidation ([Gargron](https://github.com/tootsuite/mastodon/pull/15111))
- Fix 2FA/sign-in token sessions being valid after password change ([Gargron](https://github.com/tootsuite/mastodon/pull/14802))
- Fix resolving accounts sometimes creating duplicate records for a given ActivityPub identifier ([ThibG](https://github.com/tootsuite/mastodon/pull/15364))
## [3.2.2] - 2020-12-19
### Added
- Add `tootctl maintenance fix-duplicates` ([ThibG](https://github.com/tootsuite/mastodon/pull/14860), [Gargron](https://github.com/tootsuite/mastodon/pull/15223))
- Index corruption in the database?
- This command is for you
### Removed
- Remove dependency on unused and unmaintained http_parser.rb gem ([ThibG](https://github.com/tootsuite/mastodon/pull/14574))
### Fixed
- Fix Move handler not being triggered when failing to fetch target account ([ThibG](https://github.com/tootsuite/mastodon/pull/15107))
- Fix downloading remote media files when server returns empty filename ([ThibG](https://github.com/tootsuite/mastodon/pull/14867))
- Fix possible casing inconsistencies in hashtag search ([ThibG](https://github.com/tootsuite/mastodon/pull/14906))
- Fix updating account counters when association is not yet created ([Gargron](https://github.com/tootsuite/mastodon/pull/15108))
- Fix account processing failing because of large collections ([ThibG](https://github.com/tootsuite/mastodon/pull/15027))
- Fix resolving an account through its non-canonical form (i.e. alternate domain) ([ThibG](https://github.com/tootsuite/mastodon/pull/15187))
- Fix slow distinct queries where grouped queries are faster ([Gargron](https://github.com/tootsuite/mastodon/pull/15287))
### Security
- Fix 2FA/sign-in token sessions being valid after password change ([Gargron](https://github.com/tootsuite/mastodon/pull/14802))
- Fix resolving accounts sometimes creating duplicate records for a given ActivityPub identifier ([ThibG](https://github.com/tootsuite/mastodon/pull/15364))
## [3.2.1] - 2020-10-19
### Added
- Add support for latest HTTP Signatures spec draft ([ThibG](https://github.com/tootsuite/mastodon/pull/14556))
- Add support for inlined objects in ActivityPub `to`/`cc` ([ThibG](https://github.com/tootsuite/mastodon/pull/14514))
### Changed
- Change actors to not be served at all without authentication in limited federation mode ([ThibG](https://github.com/tootsuite/mastodon/pull/14800))
- Previously, a bare version of an actor was served when not authenticated, i.e. username and public key
- Because all actor fetch requests are signed using a separate system actor, that is no longer required
### Fixed
- Fix `tootctl media` commands not recognizing very large IDs ([ThibG](https://github.com/tootsuite/mastodon/pull/14536))
- Fix crash when failing to load emoji picker in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14525))
- Fix contrast requirements in thumbnail color extraction ([ThibG](https://github.com/tootsuite/mastodon/pull/14464))
- Fix audio/video player not using `CDN_HOST` on public pages ([ThibG](https://github.com/tootsuite/mastodon/pull/14486))
- Fix private boost icon not being used on public pages ([OmmyZhang](https://github.com/tootsuite/mastodon/pull/14471))
- Fix audio player on Safari in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14485), [ThibG](https://github.com/tootsuite/mastodon/pull/14465))
- Fix dereferencing remote statuses not using the correct account for signature when receiving a targeted inbox delivery ([ThibG](https://github.com/tootsuite/mastodon/pull/14656))
- Fix nil error in `tootctl media remove` ([noellabo](https://github.com/tootsuite/mastodon/pull/14657))
- Fix videos with near-60 fps being rejected ([Gargron](https://github.com/tootsuite/mastodon/pull/14684))
- Fix reported statuses not being included in warning e-mail ([Gargron](https://github.com/tootsuite/mastodon/pull/14778))
- Fix `Reject` activities of `Follow` objects not correctly destroying a follow relationship ([ThibG](https://github.com/tootsuite/mastodon/pull/14479))
- Fix inefficiencies in fan-out-on-write service ([Gargron](https://github.com/tootsuite/mastodon/pull/14682), [noellabo](https://github.com/tootsuite/mastodon/pull/14709))
- Fix timeout errors when trying to webfinger some IPv6 configurations ([Gargron](https://github.com/tootsuite/mastodon/pull/14919))
- Fix files served as `application/octet-stream` being rejected without attempting mime type detection ([ThibG](https://github.com/tootsuite/mastodon/pull/14452))
## [3.2.0] - 2020-07-27
### Added
- Add `SMTP_SSL` environment variable ([OmmyZhang](https://github.com/tootsuite/mastodon/pull/14309))
- Add hotkey for toggling content warning input in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13987))
- **Add e-mail-based sign in challenge for users with disabled 2FA** ([Gargron](https://github.com/tootsuite/mastodon/pull/14013))
- If user tries signing in after:
- Being inactive for a while
- With a previously unknown IP
- Without 2FA being enabled
- Require to enter a token sent via e-mail before sigining in
- Add `limit` param to RSS feeds ([noellabo](https://github.com/tootsuite/mastodon/pull/13743))
- Add `visibility` param to share page ([noellabo](https://github.com/tootsuite/mastodon/pull/13023))
- Add blurhash to link previews ([ThibG](https://github.com/tootsuite/mastodon/pull/13984), [ThibG](https://github.com/tootsuite/mastodon/pull/14143), [ThibG](https://github.com/tootsuite/mastodon/pull/13985), [Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/14267), [Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/14278), [ThibG](https://github.com/tootsuite/mastodon/pull/14126), [ThibG](https://github.com/tootsuite/mastodon/pull/14261), [ThibG](https://github.com/tootsuite/mastodon/pull/14260))
- In web UI, toots cannot be marked as sensitive unless there is media attached
- However, it's possible to do via API or ActivityPub
- Thumnails of link previews of such posts now use blurhash in web UI
- The Card entity in REST API has a new `blurhash` attribute
- Add support for `summary` field for media description in ActivityPub ([ThibG](https://github.com/tootsuite/mastodon/pull/13763))
- Add hints about incomplete remote content to web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/14031), [noellabo](https://github.com/tootsuite/mastodon/pull/14195))
- **Add personal notes for accounts** ([ThibG](https://github.com/tootsuite/mastodon/pull/14148), [Gargron](https://github.com/tootsuite/mastodon/pull/14208), [Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/14251))
- To clarify, these are notes only you can see, to help you remember details
- Notes can be viewed and edited from profiles in web UI
- New REST API: `POST /api/v1/accounts/:id/note` with `comment` param
- The Relationship entity in REST API has a new `note` attribute
- Add Helm chart ([dunn](https://github.com/tootsuite/mastodon/pull/14090), [dunn](https://github.com/tootsuite/mastodon/pull/14256), [dunn](https://github.com/tootsuite/mastodon/pull/14245))
- **Add customizable thumbnails for audio and video attachments** ([Gargron](https://github.com/tootsuite/mastodon/pull/14145), [Gargron](https://github.com/tootsuite/mastodon/pull/14244), [Gargron](https://github.com/tootsuite/mastodon/pull/14273), [Gargron](https://github.com/tootsuite/mastodon/pull/14203), [ThibG](https://github.com/tootsuite/mastodon/pull/14255), [ThibG](https://github.com/tootsuite/mastodon/pull/14306), [noellabo](https://github.com/tootsuite/mastodon/pull/14358), [noellabo](https://github.com/tootsuite/mastodon/pull/14357))
- Metadata (album, artist, etc) is no longer stripped from audio files
- Album art is automatically extracted from audio files
- Thumbnail can be manually uploaded for both audio and video attachments
- Media upload APIs now support `thumbnail` param
- On `POST /api/v1/media` and `POST /api/v2/media`
- And on `PUT /api/v1/media/:id`
- ActivityPub representation of media attachments represents custom thumbnails with an `icon` attribute
- The Media Attachment entity in REST API now has a `preview_remote_url` to its `preview_url`, equivalent to `remote_url` to its `url`
- **Add color extraction for thumbnails** ([Gargron](https://github.com/tootsuite/mastodon/pull/14209), [ThibG](https://github.com/tootsuite/mastodon/pull/14264))
- The `meta` attribute on the Media Attachment entity in REST API can now have a `colors` attribute which in turn contains three hex colors: `background`, `foreground`, and `accent`
- The background color is chosen from the most dominant color around the edges of the thumbnail
- The foreground and accent colors are chosen from the colors that are the most different from the background color using the CIEDE2000 algorithm
- The most satured color of the two is designated as the accent color
- The one with the highest W3C contrast is designated as the foreground color
- If there are not enough colors in the thumbnail, new ones are generated using a monochrome pattern
- Add a visibility indicator to toots in web UI ([noellabo](https://github.com/tootsuite/mastodon/pull/14123), [highemerly](https://github.com/tootsuite/mastodon/pull/14292))
- Add `tootctl email_domain_blocks` ([tateisu](https://github.com/tootsuite/mastodon/pull/13589), [Gargron](https://github.com/tootsuite/mastodon/pull/14147))
- Add "Add new domain block" to header of federation page in admin UI ([ariasuni](https://github.com/tootsuite/mastodon/pull/13934))
- Add ability to keep emoji picker open with ctrl+click in web UI ([bclindner](https://github.com/tootsuite/mastodon/pull/13896), [noellabo](https://github.com/tootsuite/mastodon/pull/14096))
- Add custom icon for private boosts in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14380))
- Add support for Create and Update activities that don't inline objects in ActivityPub ([ThibG](https://github.com/tootsuite/mastodon/pull/14359))
- Add support for Undo activities that don't inline activities in ActivityPub ([ThibG](https://github.com/tootsuite/mastodon/pull/14346))
### Changed
- Change `.env.production.sample` to be leaner and cleaner ([Gargron](https://github.com/tootsuite/mastodon/pull/14206))
- It was overloaded as de-facto documentation and getting quite crowded
- Defer to the actual documentation while still giving a minimal example
- Change `tootctl search deploy` to work faster and display progress ([Gargron](https://github.com/tootsuite/mastodon/pull/14300))
- Change User-Agent of link preview fetching service to include "Bot" ([Gargron](https://github.com/tootsuite/mastodon/pull/14248))
- Some websites may not render OpenGraph tags into HTML if that's not the case
- Change behaviour to carry blocks over when someone migrates their followers ([ThibG](https://github.com/tootsuite/mastodon/pull/14144))
- Change volume control and download buttons in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/14122))
- **Change design of audio players in web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/14095), [ThibG](https://github.com/tootsuite/mastodon/pull/14281), [Gargron](https://github.com/tootsuite/mastodon/pull/14282), [ThibG](https://github.com/tootsuite/mastodon/pull/14118), [Gargron](https://github.com/tootsuite/mastodon/pull/14199), [ThibG](https://github.com/tootsuite/mastodon/pull/14338))
- Change reply filter to never filter own toots in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14128))
- Change boost button to no longer serve as visibility indicator in web UI ([noellabo](https://github.com/tootsuite/mastodon/pull/14132), [ThibG](https://github.com/tootsuite/mastodon/pull/14373))
- Change contrast of flash messages ([cchoi12](https://github.com/tootsuite/mastodon/pull/13892))
- Change wording from "Hide media" to "Hide image/images" in web UI ([ariasuni](https://github.com/tootsuite/mastodon/pull/13834))
- Change appearence of settings pages to be more consistent ([ariasuni](https://github.com/tootsuite/mastodon/pull/13938))
- Change "Add media" tooltip to not include long list of formats in web UI ([ariasuni](https://github.com/tootsuite/mastodon/pull/13954))
- Change how badly contrasting emoji are rendered in web UI ([leo60228](https://github.com/tootsuite/mastodon/pull/13773), [ThibG](https://github.com/tootsuite/mastodon/pull/13772), [mfmfuyu](https://github.com/tootsuite/mastodon/pull/14020), [ThibG](https://github.com/tootsuite/mastodon/pull/14015))
- Change structure of unavailable content section on about page ([ariasuni](https://github.com/tootsuite/mastodon/pull/13930))
- Change behaviour to accept ActivityPub activities relayed through group actor ([noellabo](https://github.com/tootsuite/mastodon/pull/14279))
- Change amount of processing retries for ActivityPub activities ([noellabo](https://github.com/tootsuite/mastodon/pull/14355))
### Removed
- Remove the terms "blacklist" and "whitelist" from UX ([Gargron](https://github.com/tootsuite/mastodon/pull/14149), [mayaeh](https://github.com/tootsuite/mastodon/pull/14192))
- Environment variables changed (old versions continue to work):
- `WHITELIST_MODE``LIMITED_FEDERATION_MODE`
- `EMAIL_DOMAIN_BLACKLIST``EMAIL_DOMAIN_DENYLIST`
- `EMAIL_DOMAIN_WHITELIST``EMAIL_DOMAIN_ALLOWLIST`
- CLI option changed:
- `tootctl domains purge --whitelist-mode``tootctl domains purge --limited-federation-mode`
- Remove some unnecessary database indices ([lfuelling](https://github.com/tootsuite/mastodon/pull/13695), [noellabo](https://github.com/tootsuite/mastodon/pull/14259))
- Remove unnecessary Node.js version upper bound ([ykzts](https://github.com/tootsuite/mastodon/pull/14139))
### Fixed
- Fix `following` param not working when exact match is found in account search ([noellabo](https://github.com/tootsuite/mastodon/pull/14394))
- Fix sometimes occuring duplicate mention notifications ([noellabo](https://github.com/tootsuite/mastodon/pull/14378))
- Fix RSS feeds not being cachable ([ThibG](https://github.com/tootsuite/mastodon/pull/14368))
- Fix lack of locking around processing of Announce activities in ActivityPub ([noellabo](https://github.com/tootsuite/mastodon/pull/14365))
- Fix boosted toots from blocked account not being retroactively removed from TL ([ThibG](https://github.com/tootsuite/mastodon/pull/14339))
- Fix large shortened numbers (like 1.2K) using incorrect pluralization ([Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/14061))
- Fix streaming server trying to use empty password to connect to Redis when `REDIS_PASSWORD` is given but blank ([ThibG](https://github.com/tootsuite/mastodon/pull/14135))
- Fix being unable to unboost posts when blocked by their author ([ThibG](https://github.com/tootsuite/mastodon/pull/14308))
- Fix account domain block not properly unfollowing accounts from domain ([Gargron](https://github.com/tootsuite/mastodon/pull/14304))
- Fix removing a domain allow wiping known accounts in open federation mode ([ThibG](https://github.com/tootsuite/mastodon/pull/14298))
- Fix blocks and mutes pagination in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14275))
- Fix new posts pushing down origin of opened dropdown in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14271), [ThibG](https://github.com/tootsuite/mastodon/pull/14348))
- Fix timeline markers not being saved sometimes ([ThibG](https://github.com/tootsuite/mastodon/pull/13887), [ThibG](https://github.com/tootsuite/mastodon/pull/13889), [ThibG](https://github.com/tootsuite/mastodon/pull/14155))
- Fix CSV uploads being rejected ([noellabo](https://github.com/tootsuite/mastodon/pull/13835))
- Fix incompatibility with ElasticSearch 7.x ([noellabo](https://github.com/tootsuite/mastodon/pull/13828))
- Fix being able to search posts where you're in the target audience but not actively mentioned ([noellabo](https://github.com/tootsuite/mastodon/pull/13829))
- Fix non-local posts appearing on local-only hashtag timelines in web UI ([noellabo](https://github.com/tootsuite/mastodon/pull/13827))
- Fix `tootctl media remove-orphans` choking on unknown files in storage ([Gargron](https://github.com/tootsuite/mastodon/pull/13765))
- Fix `tootctl upgrade storage-schema` misbehaving ([Gargron](https://github.com/tootsuite/mastodon/pull/13761), [angristan](https://github.com/tootsuite/mastodon/pull/13768))
- Fix it marking records as upgraded even though no files were moved
- Fix it not working with S3 storage
- Fix it not working with custom emojis
- Fix GIF reader raising incorrect exceptions ([ThibG](https://github.com/tootsuite/mastodon/pull/13760))
- Fix hashtag search performing account search as well ([ThibG](https://github.com/tootsuite/mastodon/pull/13758))
- Fix Webfinger returning wrong status code on malformed or missing param ([ThibG](https://github.com/tootsuite/mastodon/pull/13759))
- Fix `rake mastodon:setup` error when some environment variables are set ([ThibG](https://github.com/tootsuite/mastodon/pull/13928))
- Fix admin page crashing when trying to block an invalid domain name in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13884))
- Fix unsent toot confirmation dialog not popping up in single column mode in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13888))
- Fix performance of follow import ([noellabo](https://github.com/tootsuite/mastodon/pull/13836))
- Reduce timeout of Webfinger requests to that of other requests
- Use circuit breakers to stop hitting unresponsive servers
- Avoid hitting servers that are already known to be generally unavailable
- Fix filters ignoring media descriptions ([BenLubar](https://github.com/tootsuite/mastodon/pull/13837))
- Fix some actions on custom emojis leading to cryptic errors in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13951))
- Fix ActivityPub serialization of replies when some of them are URIs ([ThibG](https://github.com/tootsuite/mastodon/pull/13957))
- Fix `rake mastodon:setup` choking on environment variables containing `%` ([ThibG](https://github.com/tootsuite/mastodon/pull/13940))
- Fix account redirect confirmation message talking about moved followers ([ThibG](https://github.com/tootsuite/mastodon/pull/13950))
- Fix avatars having the wrong size on public detailed status pages ([ThibG](https://github.com/tootsuite/mastodon/pull/14140))
- Fix various issues around OpenGraph representation of media ([Gargron](https://github.com/tootsuite/mastodon/pull/14133))
- Pages containing audio no longer say "Attached: 1 image" in description
- Audio attachments now represented as OpenGraph `og:audio`
- The `twitter:player` page now uses Mastodon's proper audio/video player
- Audio/video buffered bars now display correctly in audio/video player
- Volume and progress bars now respond to movement/move smoother
- Fix audio/video/images/cards not reacting to window resizes in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/14130))
- Fix very wide media attachments resulting in too thin a thumbnail in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14127))
- Fix crash when merging posts into home feed after following someone ([ThibG](https://github.com/tootsuite/mastodon/pull/14129))
- Fix unique username constraint for local users not being enforced in database ([ThibG](https://github.com/tootsuite/mastodon/pull/14099))
- Fix unnecessary gap under video modal in web UI ([mfmfuyu](https://github.com/tootsuite/mastodon/pull/14098))
- Fix 2FA and sign in token pages not respecting user locale ([mfmfuyu](https://github.com/tootsuite/mastodon/pull/14087))
- Fix unapproved users being able to view profiles when in limited-federation mode *and* requiring approval for sign-ups ([ThibG](https://github.com/tootsuite/mastodon/pull/14093))
- Fix initial audio volume not corresponding to what's displayed in audio player in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14057))
- Fix timelines sometimes jumping when closing modals in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14019))
- Fix memory usage of downloading remote files ([Gargron](https://github.com/tootsuite/mastodon/pull/14184), [Gargron](https://github.com/tootsuite/mastodon/pull/14181), [noellabo](https://github.com/tootsuite/mastodon/pull/14356))
- Don't read entire file (up to 40 MB) into memory
- Read and write it to temp file in small chunks
- Fix inconsistent account header padding in web UI ([trwnh](https://github.com/tootsuite/mastodon/pull/14179))
- Fix Thai being skipped from language detection ([Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/13989))
- Since Thai has its own alphabet, it can be detected more reliably
- Fix broken hashtag column options styling in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14247))
- Fix pointer cursor being shown on toots that are not clickable in web UI ([arielrodrigues](https://github.com/tootsuite/mastodon/pull/14185))
- Fix lock icon not being shown when locking account in profile settings ([ThibG](https://github.com/tootsuite/mastodon/pull/14190))
- Fix domain blocks doing work the wrong way around ([ThibG](https://github.com/tootsuite/mastodon/pull/13424))
- Instead of suspending accounts one by one, mark all as suspended first (quick)
- Only then proceed to start removing their data (slow)
- Clear out media attachments in a separate worker (slow)
## [3.1.5] - 2020-07-07
### Security
- Fix media attachment enumeration ([ThibG](https://github.com/tootsuite/mastodon/pull/14254))
- Change rate limits for various paths ([Gargron](https://github.com/tootsuite/mastodon/pull/14253))
- Fix other sessions not being logged out on password change ([Gargron](https://github.com/tootsuite/mastodon/pull/14252))
## [3.1.4] - 2020-05-14
### Added
- Add `vi` to available locales ([taicv](https://github.com/tootsuite/mastodon/pull/13542))
@ -70,7 +461,7 @@ All notable changes to this project will be documented in this file.
- For apps that self-register on behalf of every individual user (such as most mobile apps), this is a non-issue
- The issue only affects developers of apps who are shared between multiple users, such as server-side apps like cross-posters
## [v3.1.3] - 2020-04-05
## [3.1.3] - 2020-04-05
### Added
- Add ability to filter audit log in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/13381))

View file

@ -70,6 +70,6 @@ The smaller the set of changes in the pull request is, the quicker it can be rev
## Documentation
The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/docs](https://source.joinmastodon.org/mastodon/docs).
The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to tootsuite/documentation](https://github.com/tootsuite/documentation).
</blockquote>

View file

@ -1,10 +1,10 @@
FROM ubuntu:20.04 as build-dep
# Use bash for the shell
SHELL ["bash", "-c"]
SHELL ["/bin/bash", "-c"]
# Install Node v12 (LTS)
ENV NODE_VER="12.16.3"
ENV NODE_VER="12.21.0"
RUN ARCH= && \
dpkgArch="$(dpkg --print-architecture)" && \
case "${dpkgArch##*-}" in \
@ -17,34 +17,19 @@ RUN ARCH= && \
*) echo "unsupported architecture"; exit 1 ;; \
esac && \
echo "Etc/UTC" > /etc/localtime && \
apt update && \
apt -y install wget python && \
apt-get update && \
apt-get install -y --no-install-recommends ca-certificates wget python && \
cd ~ && \
wget https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER-linux-$ARCH.tar.gz && \
wget -q https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER-linux-$ARCH.tar.gz && \
tar xf node-v$NODE_VER-linux-$ARCH.tar.gz && \
rm node-v$NODE_VER-linux-$ARCH.tar.gz && \
mv node-v$NODE_VER-linux-$ARCH /opt/node
# Install jemalloc
ENV JE_VER="5.2.1"
RUN apt update && \
apt -y install make autoconf gcc g++ && \
cd ~ && \
wget https://github.com/jemalloc/jemalloc/archive/$JE_VER.tar.gz && \
tar xf $JE_VER.tar.gz && \
cd jemalloc-$JE_VER && \
./autogen.sh && \
./configure --prefix=/opt/jemalloc && \
make -j$(nproc) > /dev/null && \
make install_bin install_include install_lib
# Install Ruby
ENV RUBY_VER="2.6.6"
ENV CPPFLAGS="-I/opt/jemalloc/include"
ENV LDFLAGS="-L/opt/jemalloc/lib/"
RUN apt update && \
apt -y install build-essential \
bison libyaml-dev libgdbm-dev libreadline-dev \
ENV RUBY_VER="2.7.3"
RUN apt-get update && \
apt-get install -y --no-install-recommends build-essential \
bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \
libncurses5-dev libffi-dev zlib1g-dev libssl-dev && \
cd ~ && \
wget https://cache.ruby-lang.org/pub/ruby/${RUBY_VER%.*}/ruby-$RUBY_VER.tar.gz && \
@ -54,24 +39,24 @@ RUN apt update && \
--with-jemalloc \
--with-shared \
--disable-install-doc && \
ln -s /opt/jemalloc/lib/* /usr/lib/ && \
make -j$(nproc) > /dev/null && \
make install
make -j"$(nproc)" > /dev/null && \
make install && \
rm -rf ../ruby-$RUBY_VER.tar.gz ../ruby-$RUBY_VER
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin"
RUN npm install -g yarn && \
gem install bundler && \
apt update && \
apt -y install git libicu-dev libidn11-dev \
libpq-dev libprotobuf-dev protobuf-compiler
apt-get update && \
apt-get install -y --no-install-recommends git libicu-dev libidn11-dev \
libpq-dev libprotobuf-dev protobuf-compiler shared-mime-info
COPY Gemfile* package.json yarn.lock /opt/mastodon/
RUN cd /opt/mastodon && \
bundle config set deployment 'true' && \
bundle config set without 'development test' && \
bundle install -j$(nproc) && \
bundle install -j"$(nproc)" && \
yarn install --pure-lockfile
FROM ubuntu:20.04
@ -79,7 +64,6 @@ FROM ubuntu:20.04
# Copy over all the langs needed for runtime
COPY --from=build-dep /opt/node /opt/node
COPY --from=build-dep /opt/ruby /opt/ruby
COPY --from=build-dep /opt/jemalloc /opt/jemalloc
# Add more PATHs to the PATH
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin:/opt/mastodon/bin"
@ -87,32 +71,26 @@ ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin:/opt/mastodon/bin"
# Create the mastodon user
ARG UID=991
ARG GID=991
RUN apt update && \
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update && \
echo "Etc/UTC" > /etc/localtime && \
ln -s /opt/jemalloc/lib/* /usr/lib/ && \
apt install -y whois wget && \
apt-get install -y --no-install-recommends whois wget && \
addgroup --gid $GID mastodon && \
useradd -m -u $UID -g $GID -d /opt/mastodon mastodon && \
echo "mastodon:`head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24 | mkpasswd -s -m sha-256`" | chpasswd
echo "mastodon:$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24 | mkpasswd -s -m sha-256)" | chpasswd && \
rm -rf /var/lib/apt/lists/*
# Install mastodon runtime deps
RUN apt -y --no-install-recommends install \
libssl1.1 libpq5 imagemagick ffmpeg \
RUN apt-get update && \
apt-get -y --no-install-recommends install \
libssl1.1 libpq5 imagemagick ffmpeg libjemalloc2 \
libicu66 libprotobuf17 libidn11 libyaml-0-2 \
file ca-certificates tzdata libreadline8 && \
apt -y install gcc && \
file ca-certificates tzdata libreadline8 gcc tini && \
ln -s /opt/mastodon /mastodon && \
gem install bundler && \
rm -rf /var/cache && \
rm -rf /var/lib/apt/lists/*
# Add tini
ENV TINI_VERSION="0.18.0"
ENV TINI_SUM="12d20136605531b09a2c2dac02ccee85e1b874eb322ef6baf7561cd93f93c855"
ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini /tini
RUN echo "$TINI_SUM tini" | sha256sum -c -
RUN chmod +x /tini
# Copy over mastodon source, and dependencies from building, and set permissions
COPY --chown=mastodon:mastodon . /opt/mastodon
COPY --from=build-dep --chown=mastodon:mastodon /opt/mastodon /opt/mastodon
@ -135,5 +113,5 @@ RUN cd ~ && \
# Set the work dir and the container entry point
WORKDIR /opt/mastodon
ENTRYPOINT ["/tini", "--"]
ENTRYPOINT ["/usr/bin/tini", "--"]
EXPOSE 3000 4000

58
Gemfile
View file

@ -5,10 +5,10 @@ ruby '>= 2.5.0', '< 3.0.0'
gem 'pkg-config', '~> 1.4'
gem 'puma', '~> 4.3'
gem 'rails', '~> 5.2.4.3'
gem 'puma', '~> 5.2'
gem 'rails', '~> 6.1.3'
gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 0.20'
gem 'thor', '~> 1.1'
gem 'rack', '~> 2.2.3'
gem 'thwait', '~> 0.2.0'
@ -30,31 +30,31 @@ gem 'blurhash', '~> 0.1'
gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.7'
gem 'bootsnap', '~> 1.4', require: false
gem 'bootsnap', '~> 1.6.0', require: false
gem 'browser'
gem 'charlock_holmes', '~> 0.7.7'
gem 'iso-639'
gem 'chewy', '~> 5.1'
gem 'cld3', '~> 3.3.0'
gem 'chewy', '~> 5.2'
gem 'cld3', '~> 3.4.2'
gem 'devise', '~> 4.7'
gem 'devise-two-factor', '~> 3.1'
gem 'devise-two-factor', '~> 4.0'
group :pam_authentication, optional: true do
gem 'devise_pam_authenticatable2', '~> 9.2'
end
gem 'net-ldap', '~> 0.16'
gem 'omniauth-cas', '~> 1.1'
gem 'net-ldap', '~> 0.17'
gem 'omniauth-cas', '~> 2.0'
gem 'omniauth-saml', '~> 1.10'
gem 'omniauth', '~> 1.9'
gem 'omniauth-rails_csrf_protection', '~> 0.1'
gem 'color_diff', '~> 0.1'
gem 'discard', '~> 1.2'
gem 'doorkeeper', '~> 5.4'
gem 'doorkeeper', '~> 5.5'
gem 'ed25519', '~> 1.2'
gem 'fast_blank', '~> 1.0'
gem 'fastimage'
gem 'goldfinger', '~> 2.1'
gem 'hiredis', '~> 0.6'
gem 'redis-namespace', '~> 1.8'
gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b'
@ -66,31 +66,30 @@ gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar'
gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b176939f851338d0a4971a532'
gem 'nokogiri', '~> 1.10'
gem 'nokogiri', '~> 1.11'
gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.10'
gem 'ox', '~> 2.13'
gem 'oj', '~> 3.11'
gem 'ox', '~> 2.14'
gem 'parslet'
gem 'parallel', '~> 1.19'
gem 'posix-spawn'
gem 'pundit', '~> 2.1'
gem 'premailer-rails'
gem 'rack-attack', '~> 6.3'
gem 'rack-attack', '~> 6.5'
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
gem 'rails-i18n', '~> 5.1'
gem 'rails-i18n', '~> 6.0'
gem 'rails-settings-cached', '~> 0.6'
gem 'redis', '~> 4.2', require: ['redis', 'redis/connection/hiredis']
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 1.1'
gem 'ruby-progressbar', '~> 1.10'
gem 'rqrcode', '~> 1.2'
gem 'ruby-progressbar', '~> 1.11'
gem 'sanitize', '~> 5.2'
gem 'sidekiq', '~> 6.1'
gem 'sidekiq-scheduler', '~> 3.0'
gem 'sidekiq-unique-jobs', '~> 6.0'
gem 'sidekiq-unique-jobs', '~> 7.0'
gem 'sidekiq-bulk', '~>0.2.0'
gem 'simple-navigation', '~> 4.1'
gem 'simple_form', '~> 5.0'
gem 'simple_form', '~> 5.1'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
gem 'stoplight', '~> 2.2.1'
gem 'strong_migrations', '~> 0.7'
@ -105,15 +104,15 @@ gem 'json-ld'
gem 'json-ld-preloaded', '~> 3.1'
gem 'rdf-normalize', '~> 0.4'
gem 'redcarpet', '~> 3.4'
gem 'redcarpet', '~> 3.5'
group :development, :test do
gem 'fabrication', '~> 2.21'
gem 'fabrication', '~> 2.22'
gem 'fuubar', '~> 2.5'
gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-byebug', '~> 3.9'
gem 'pry-rails', '~> 0.3'
gem 'rspec-rails', '~> 4.0'
gem 'rspec-rails', '~> 5.0'
end
group :production, :test do
@ -121,7 +120,7 @@ group :production, :test do
end
group :test do
gem 'capybara', '~> 3.33'
gem 'capybara', '~> 3.35'
gem 'climate_control', '~> 0.2'
gem 'faker', '~> 2.13'
gem 'microformats', '~> 4.2'
@ -134,10 +133,10 @@ group :test do
end
group :development do
gem 'active_record_query_trace', '~> 1.7'
gem 'active_record_query_trace', '~> 1.8'
gem 'annotate', '~> 3.1'
gem 'better_errors', '~> 2.7'
gem 'binding_of_caller', '~> 0.7'
gem 'better_errors', '~> 2.9'
gem 'binding_of_caller', '~> 1.0'
gem 'bullet', '~> 6.1'
gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 1.4'
@ -157,8 +156,9 @@ end
group :production do
gem 'lograge', '~> 0.11'
gem 'redis-rails', '~> 5.0'
end
gem 'concurrent-ruby', require: false
gem 'connection_pool', require: false
gem 'xorcist', '~> 1.1'

View file

@ -16,53 +16,71 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (5.2.4.3)
actionpack (= 5.2.4.3)
actioncable (6.1.3.1)
actionpack (= 6.1.3.1)
activesupport (= 6.1.3.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailer (5.2.4.3)
actionpack (= 5.2.4.3)
actionview (= 5.2.4.3)
activejob (= 5.2.4.3)
actionmailbox (6.1.3.1)
actionpack (= 6.1.3.1)
activejob (= 6.1.3.1)
activerecord (= 6.1.3.1)
activestorage (= 6.1.3.1)
activesupport (= 6.1.3.1)
mail (>= 2.7.1)
actionmailer (6.1.3.1)
actionpack (= 6.1.3.1)
actionview (= 6.1.3.1)
activejob (= 6.1.3.1)
activesupport (= 6.1.3.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.2.4.3)
actionview (= 5.2.4.3)
activesupport (= 5.2.4.3)
rack (~> 2.0, >= 2.0.8)
actionpack (6.1.3.1)
actionview (= 6.1.3.1)
activesupport (= 6.1.3.1)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.2.4.3)
activesupport (= 5.2.4.3)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.3.1)
actionpack (= 6.1.3.1)
activerecord (= 6.1.3.1)
activestorage (= 6.1.3.1)
activesupport (= 6.1.3.1)
nokogiri (>= 1.8.5)
actionview (6.1.3.1)
activesupport (= 6.1.3.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
active_model_serializers (0.10.10)
actionpack (>= 4.1, < 6.1)
activemodel (>= 4.1, < 6.1)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_model_serializers (0.10.12)
actionpack (>= 4.1, < 6.2)
activemodel (>= 4.1, < 6.2)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_record_query_trace (1.7)
activejob (5.2.4.3)
activesupport (= 5.2.4.3)
active_record_query_trace (1.8)
activejob (6.1.3.1)
activesupport (= 6.1.3.1)
globalid (>= 0.3.6)
activemodel (5.2.4.3)
activesupport (= 5.2.4.3)
activerecord (5.2.4.3)
activemodel (= 5.2.4.3)
activesupport (= 5.2.4.3)
arel (>= 9.0)
activestorage (5.2.4.3)
actionpack (= 5.2.4.3)
activerecord (= 5.2.4.3)
marcel (~> 0.3.1)
activesupport (5.2.4.3)
activemodel (6.1.3.1)
activesupport (= 6.1.3.1)
activerecord (6.1.3.1)
activemodel (= 6.1.3.1)
activesupport (= 6.1.3.1)
activestorage (6.1.3.1)
actionpack (= 6.1.3.1)
activejob (= 6.1.3.1)
activerecord (= 6.1.3.1)
activesupport (= 6.1.3.1)
marcel (~> 1.0.0)
mini_mime (~> 1.0.2)
activesupport (6.1.3.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
airbrussh (1.4.0)
@ -71,8 +89,7 @@ GEM
annotate (3.1.1)
activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 14.0)
arel (9.0.0)
ast (2.4.1)
ast (2.4.2)
attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
av (0.9.0)
@ -108,15 +125,18 @@ GEM
msgpack (~> 1.0)
brakeman (4.9.0)
browser (4.2.0)
brpoplpush-redis_script (0.1.2)
concurrent-ruby (~> 1.0, >= 1.0.5)
redis (>= 1.0, <= 5.0)
builder (3.2.4)
bullet (6.1.0)
bullet (6.1.4)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
bundler-audit (0.7.0.1)
bundler-audit (0.8.0)
bundler (>= 1.2.0, < 3)
thor (>= 0.18, < 2)
thor (~> 1.0)
byebug (11.1.3)
capistrano (3.14.1)
capistrano (3.16.0)
airbrussh (>= 1.0.0)
i18n
rake (>= 10.0.0)
@ -131,20 +151,20 @@ GEM
sshkit (~> 1.3)
capistrano-yarn (2.0.2)
capistrano (~> 3.0)
capybara (3.33.0)
capybara (3.35.3)
addressable
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (~> 1.5)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
case_transform (0.2)
activesupport
cbor (0.5.9.6)
charlock_holmes (0.7.7)
chewy (5.1.0)
activesupport (>= 4.0)
chewy (5.2.0)
activesupport (>= 5.2)
elasticsearch (>= 2.0.0)
elasticsearch-dsl
chunky_png (1.3.12)
@ -165,29 +185,29 @@ GEM
crass (1.0.6)
css_parser (1.7.1)
addressable
debug_inspector (0.0.3)
devise (4.7.2)
debug_inspector (1.0.0)
devise (4.7.3)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
devise-two-factor (3.1.0)
activesupport (< 6.1)
devise-two-factor (4.0.0)
activesupport (< 6.2)
attr_encrypted (>= 1.3, < 4, != 2)
devise (~> 4.0)
railties (< 6.1)
rotp (~> 2.0)
railties (< 6.2)
rotp (~> 6.0)
devise_pam_authenticatable2 (9.2.0)
devise (>= 4.0.0)
rpam2 (~> 4.0)
diff-lcs (1.4.4)
discard (1.2.0)
activerecord (>= 4.2, < 7)
docile (1.3.2)
docile (1.3.4)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.4.0)
doorkeeper (5.5.1)
railties (>= 5)
dotenv (2.7.6)
dotenv-rails (2.7.6)
@ -212,8 +232,11 @@ GEM
fabrication (2.21.1)
faker (2.13.0)
i18n (>= 1.6, < 2)
faraday (1.0.1)
faraday (1.3.0)
faraday-net_http (~> 1.0)
multipart-post (>= 1.2, < 3)
ruby2_keywords
faraday-net_http (1.0.1)
fast_blank (1.0.0)
fastimage (2.2.0)
ffi (1.10.0)
@ -236,7 +259,7 @@ GEM
fugit (1.3.8)
et-orbi (~> 1.1, >= 1.1.8)
raabro (~> 1.3)
fuubar (2.5.0)
fuubar (2.5.1)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
globalid (0.4.2)
@ -279,7 +302,7 @@ GEM
rainbow (>= 2.0.0)
i18n (1.8.5)
concurrent-ruby (~> 1.0)
i18n-tasks (0.9.31)
i18n-tasks (0.9.34)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
erubi
@ -302,7 +325,7 @@ GEM
multi_json (~> 1.14)
rack (~> 2.0)
rdf (~> 3.1)
json-ld-preloaded (3.1.3)
json-ld-preloaded (3.1.5)
json-ld (~> 3.1)
rdf (~> 3.1)
jsonapi-renderer (0.2.2)
@ -338,13 +361,12 @@ GEM
nokogiri (>= 1.5.9)
mail (2.7.1)
mini_mime (>= 0.1.1)
makara (0.4.1)
makara (0.5.0)
activerecord (>= 3.0.0)
marcel (0.3.3)
mimemagic (~> 0.3.2)
marcel (1.0.0)
mario-redis-lock (1.2.1)
redis (>= 3.0.5)
memory_profiler (0.9.14)
memory_profiler (1.0.0)
method_source (1.0.0)
microformats (4.2.1)
json (~> 2.2)
@ -368,8 +390,8 @@ GEM
mini_portile2 (~> 2.4.0)
nokogumbo (2.0.2)
nokogiri (~> 1.8, >= 1.8.4)
nsa (0.2.7)
activesupport (>= 4.2, < 6)
nsa (0.2.8)
activesupport (>= 4.2, < 7)
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0)
@ -377,7 +399,7 @@ GEM
omniauth (1.9.1)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
omniauth-cas (1.1.1)
omniauth-cas (2.0.0)
addressable (~> 2.3)
nokogiri (~> 1.5)
omniauth (~> 1.2)
@ -387,7 +409,7 @@ GEM
openssl (2.2.0)
openssl-signature_algorithm (0.4.0)
orm_adapter (0.5.0)
ox (2.13.2)
ox (2.14.4)
paperclip (6.0.0)
activemodel (>= 4.2.0)
activesupport (>= 4.2.0)
@ -400,7 +422,7 @@ GEM
parallel (1.19.2)
parallel_tests (3.2.0)
parallel
parser (2.7.1.4)
parser (3.0.1.0)
ast (~> 2.4.1)
parslet (2.0.0)
pastel (0.8.0)
@ -426,14 +448,15 @@ GEM
pry (~> 0.13.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.5)
puma (4.3.5)
public_suffix (4.0.6)
puma (5.2.2)
nio4r (~> 2.0)
pundit (2.1.0)
activesupport (>= 3.0.0)
raabro (1.3.1)
raabro (1.3.3)
racc (1.5.2)
rack (2.2.3)
rack-attack (6.3.1)
rack-attack (6.5.0)
rack (>= 1.0, < 3)
rack-cors (1.1.1)
rack (>= 2.0.0)
@ -441,18 +464,20 @@ GEM
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (5.2.4.3)
actioncable (= 5.2.4.3)
actionmailer (= 5.2.4.3)
actionpack (= 5.2.4.3)
actionview (= 5.2.4.3)
activejob (= 5.2.4.3)
activemodel (= 5.2.4.3)
activerecord (= 5.2.4.3)
activestorage (= 5.2.4.3)
activesupport (= 5.2.4.3)
bundler (>= 1.3.0)
railties (= 5.2.4.3)
rails (6.1.3.1)
actioncable (= 6.1.3.1)
actionmailbox (= 6.1.3.1)
actionmailer (= 6.1.3.1)
actionpack (= 6.1.3.1)
actiontext (= 6.1.3.1)
actionview (= 6.1.3.1)
activejob (= 6.1.3.1)
activemodel (= 6.1.3.1)
activerecord (= 6.1.3.1)
activestorage (= 6.1.3.1)
activesupport (= 6.1.3.1)
bundler (>= 1.15.0)
railties (= 6.1.3.1)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
@ -463,17 +488,17 @@ GEM
nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
rails-i18n (5.1.3)
rails-i18n (6.0.0)
i18n (>= 0.7, < 2)
railties (>= 5.0, < 6)
railties (>= 6.0.0, < 7)
rails-settings-cached (0.6.6)
rails (>= 4.2.0)
railties (5.2.4.3)
actionpack (= 5.2.4.3)
activesupport (= 5.2.4.3)
railties (6.1.3.1)
actionpack (= 6.1.3.1)
activesupport (= 6.1.3.1)
method_source
rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0)
thor (~> 1.0)
rainbow (3.0.0)
rake (13.0.1)
rdf (3.1.5)
@ -507,40 +532,40 @@ GEM
responders (3.0.1)
actionpack (>= 5.0)
railties (>= 5.0)
rexml (3.2.4)
rotp (2.1.2)
rexml (3.2.5)
rotp (6.2.0)
rpam2 (4.0.2)
rqrcode (1.1.2)
rqrcode (1.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 0.1)
rqrcode_core (0.1.2)
rspec-core (3.9.2)
rspec-support (~> 3.9.3)
rspec-expectations (3.9.2)
rqrcode_core (~> 0.2)
rqrcode_core (0.2.0)
rspec-core (3.10.1)
rspec-support (~> 3.10.0)
rspec-expectations (3.10.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-mocks (3.9.1)
rspec-support (~> 3.10.0)
rspec-mocks (3.10.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-rails (4.0.1)
actionpack (>= 4.2)
activesupport (>= 4.2)
railties (>= 4.2)
rspec-core (~> 3.9)
rspec-expectations (~> 3.9)
rspec-mocks (~> 3.9)
rspec-support (~> 3.9)
rspec-support (~> 3.10.0)
rspec-rails (5.0.1)
actionpack (>= 5.2)
activesupport (>= 5.2)
railties (>= 5.2)
rspec-core (~> 3.10)
rspec-expectations (~> 3.10)
rspec-mocks (~> 3.10)
rspec-support (~> 3.10)
rspec-sidekiq (3.1.0)
rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0)
rspec-support (3.9.3)
rspec-support (3.10.2)
rspec_junit_formatter (0.4.1)
rspec-core (>= 2, < 4, != 2.12.0)
rubocop (0.88.0)
parallel (~> 1.10)
parser (>= 2.7.1.1)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.7)
regexp_parser (>= 1.8, < 3.0)
rexml
rubocop-ast (>= 0.1.0, < 1.0)
ruby-progressbar (~> 1.7)
@ -550,10 +575,11 @@ GEM
rubocop-rails (2.6.0)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 0.82.0)
ruby-progressbar (1.10.1)
rubocop (>= 0.90.0, < 2.0)
ruby-progressbar (1.11.0)
ruby-saml (1.11.0)
nokogiri (>= 1.5.10)
ruby2_keywords (0.0.4)
rufus-scheduler (3.6.0)
fugit (~> 1.1, >= 1.1.6)
safe_yaml (1.0.5)
@ -578,10 +604,11 @@ GEM
sidekiq (>= 3)
thwait
tilt (>= 1.4.0)
sidekiq-unique-jobs (6.0.22)
sidekiq-unique-jobs (7.0.8)
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
concurrent-ruby (~> 1.0, >= 1.0.5)
sidekiq (>= 4.0, < 7.0)
thor (~> 0)
sidekiq (>= 5.0, < 7.0)
thor (>= 0.20, < 2.0)
simple-navigation (4.1.0)
activesupport (>= 2.3.2)
simple_form (5.0.2)
@ -590,15 +617,17 @@ GEM
simplecov (0.19.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov-html (0.12.2)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.2)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.1)
sprockets-rails (3.2.2)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sshkit (1.21.0)
sshkit (1.21.2)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
stackprof (0.2.15)
@ -609,7 +638,7 @@ GEM
strong_migrations (0.7.1)
activerecord (>= 5)
temple (0.8.2)
terminal-table (1.8.0)
terminal-table (3.0.0)
unicode-display_width (~> 1.1, >= 1.1.1)
terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0)
@ -633,9 +662,9 @@ GEM
tty-screen (0.8.1)
twitter-text (1.14.7)
unf (~> 0.1.0)
tzinfo (1.2.7)
thread_safe (~> 0.1)
tzinfo-data (1.2020.1)
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
tzinfo-data (1.2021.1)
tzinfo (>= 1.0.0)
unf (0.1.4)
unf_ext
@ -670,15 +699,17 @@ GEM
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
wisper (2.0.1)
xorcist (1.1.2)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.4.2)
PLATFORMS
ruby
DEPENDENCIES
active_model_serializers (~> 0.10)
active_record_query_trace (~> 1.7)
active_record_query_trace (~> 1.8)
addressable (~> 2.7)
annotate (~> 3.1)
aws-sdk-s3 (~> 1.79)
@ -694,21 +725,20 @@ DEPENDENCIES
capistrano-rails (~> 1.6)
capistrano-rbenv (~> 2.2)
capistrano-yarn (~> 2.0)
capybara (~> 3.33)
capybara (~> 3.35)
charlock_holmes (~> 0.7.7)
chewy (~> 5.1)
cld3 (~> 3.3.0)
chewy (~> 5.2)
cld3 (~> 3.4.2)
climate_control (~> 0.2)
color_diff (~> 0.1)
concurrent-ruby
connection_pool
devise (~> 4.7)
devise-two-factor (~> 3.1)
devise-two-factor (~> 4.0)
devise_pam_authenticatable2 (~> 9.2)
discard (~> 1.2)
doorkeeper (~> 5.4)
doorkeeper (~> 5.5)
dotenv-rails (~> 2.7)
e2mmap (~> 0.1.0)
ed25519 (~> 1.2)
fabrication (~> 2.21)
faker (~> 2.13)
@ -717,9 +747,7 @@ DEPENDENCIES
fog-core (<= 2.1.0)
fog-openstack (~> 0.3)
fuubar (~> 2.5)
goldfinger (~> 2.1)
hamlit-rails (~> 0.2)
health_check!
hiredis (~> 0.6)
htmlentities (~> 4.3)
http (~> 4.4)
@ -735,20 +763,20 @@ DEPENDENCIES
letter_opener_web (~> 1.4)
link_header (~> 0.0)
lograge (~> 0.11)
makara (~> 0.4)
makara (~> 0.5)
mario-redis-lock (~> 1.2)
memory_profiler
microformats (~> 4.2)
mime-types (~> 3.3.1)
net-ldap (~> 0.16)
nilsimsa!
nokogiri (~> 1.10)
net-ldap (~> 0.17)
nokogiri (~> 1.11)
nsa (~> 0.2)
oj (~> 3.10)
oj (~> 3.11)
omniauth (~> 1.9)
omniauth-cas (~> 1.1)
omniauth-cas (~> 2.0)
omniauth-rails_csrf_protection (~> 0.1)
omniauth-saml (~> 1.10)
ox (~> 2.13)
ox (~> 2.14)
paperclip (~> 6.0)
paperclip-av-transcoder (~> 0.6)
parallel (~> 1.19)
@ -762,17 +790,17 @@ DEPENDENCIES
private_address_check (~> 0.5)
pry-byebug (~> 3.9)
pry-rails (~> 0.3)
puma (~> 4.3)
puma (~> 5.2)
pundit (~> 2.1)
rack (~> 2.2.3)
rack-attack (~> 6.3)
rack-attack (~> 6.5)
rack-cors (~> 1.1)
rails (~> 5.2.4.3)
rails (~> 6.1.3)
rails-controller-testing (~> 1.0)
rails-i18n (~> 5.1)
rails-i18n (~> 6.0)
rails-settings-cached (~> 0.6)
rdf-normalize (~> 0.4)
redcarpet (~> 3.4)
redcarpet (~> 3.5)
redis (~> 4.2)
redis-namespace (~> 1.8)
redis-rails (~> 5.0)
@ -787,7 +815,7 @@ DEPENDENCIES
sidekiq (~> 6.1)
sidekiq-bulk (~> 0.2.0)
sidekiq-scheduler (~> 3.0)
sidekiq-unique-jobs (~> 6.0)
sidekiq-unique-jobs (~> 7.0)
simple-navigation (~> 4.1)
simple_form (~> 5.0)
simplecov (~> 0.19)

View file

@ -1,4 +1,4 @@
web: env PORT=3000 bundle exec puma -C config/puma.rb
sidekiq: env PORT=3000 bundle exec sidekiq
web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb
sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq
stream: env PORT=4000 yarn run start
webpack: ./bin/webpack-dev-server --listen-host 0.0.0.0

2
Vagrantfile vendored
View file

@ -72,10 +72,12 @@ bundle install
yarn install
# Build Mastodon
export RAILS_ENV=development
export $(cat ".env.vagrant" | xargs)
bundle exec rails db:setup
# Configure automatic loading of environment variable
echo 'export RAILS_ENV=development' >> ~/.bash_profile
echo 'export $(cat "/vagrant/.env.vagrant" | xargs)' >> ~/.bash_profile
SCRIPT

View file

@ -88,9 +88,6 @@
{
"url": "https://github.com/heroku/heroku-buildpack-apt"
},
{
"url": "heroku/nodejs"
},
{
"url": "heroku/ruby"
}

View file

@ -31,7 +31,7 @@ class StatusesIndex < Chewy::Index
},
}
define_type ::Status.unscoped.kept.without_reblogs.includes(:media_attachments), delete_if: ->(status) { status.searchable_by.empty? } do
define_type ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll) do
crutch :mentions do |collection|
data = ::Mention.where(status_id: collection.map(&:id)).where(account: Account.local, silent: false).pluck(:status_id, :account_id)
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }

View file

@ -1,13 +1,17 @@
# frozen_string_literal: true
class AboutController < ApplicationController
include RegistrationSpamConcern
before_action :set_pack
layout 'public'
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]
before_action :set_expires_in, only: [:more, :terms]
before_action :set_registration_form_time, only: :show
skip_before_action :require_functional!, only: [:more, :terms]
@ -18,6 +22,7 @@ class AboutController < ApplicationController
toc_generator = TOCGenerator.new(@instance_presenter.site_extended_description)
@rules = Rule.ordered
@contents = toc_generator.html
@table_of_contents = toc_generator.toc
@blocks = DomainBlock.with_user_facing_limitations.by_severity if display_blocks?

View file

@ -7,6 +7,7 @@ class AccountsController < ApplicationController
include AccountControllerConcern
include SignatureAuthentication
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
before_action :set_body_classes
@ -29,8 +30,7 @@ class AccountsController < ApplicationController
end
@pinned_statuses = cache_collection(@account.pinned_statuses.not_local_only, Status) if show_pinned_statuses?
@statuses = filtered_status_page
@statuses = cache_collection(@statuses, Status)
@statuses = cached_filtered_status_page
@rss_url = rss_url
unless @statuses.empty?
@ -50,7 +50,7 @@ class AccountsController < ApplicationController
format.json do
expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?)
render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to
render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter
end
end
end
@ -82,7 +82,7 @@ class AccountsController < ApplicationController
end
def account_media_status_ids
@account.media_attachments.attached.reorder(nil).select(:status_id).distinct
@account.media_attachments.attached.reorder(nil).select(:status_id).group(:status_id)
end
def no_replies_scope
@ -103,6 +103,10 @@ class AccountsController < ApplicationController
params[:username]
end
def skip_temporary_suspension_response?
request.format == :json
end
def rss_url
if tag_requested?
short_account_tag_url(@account, params[:tag], format: 'rss')
@ -132,30 +136,27 @@ class AccountsController < ApplicationController
end
def media_requested?
request.path.split('.').first.ends_with?('/media') && !tag_requested?
request.path.split('.').first.end_with?('/media') && !tag_requested?
end
def replies_requested?
request.path.split('.').first.ends_with?('/with_replies') && !tag_requested?
request.path.split('.').first.end_with?('/with_replies') && !tag_requested?
end
def tag_requested?
request.path.split('.').first.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
request.path.split('.').first.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
end
def filtered_status_page
filtered_statuses.paginate_by_id(PAGE_SIZE, params_slice(:max_id, :min_id, :since_id))
def cached_filtered_status_page
cache_collection_paginated_by_id(
filtered_statuses,
Status,
PAGE_SIZE,
params_slice(:max_id, :min_id, :since_id)
)
end
def params_slice(*keys)
params.slice(*keys).permit(*keys)
end
def restrict_fields_to
if signed_request_account.present? || public_fetch_mode?
# Return all fields
else
%i(id type preferred_username inbox public_key endpoints)
end
end
end

View file

@ -8,4 +8,8 @@ class ActivityPub::BaseController < Api::BaseController
def set_cache_headers
response.headers['Vary'] = 'Signature' if authorized_fetch_mode?
end
def skip_temporary_suspension_response?
false
end
end

View file

@ -12,7 +12,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
def show
expires_in 3.minutes, public: public_fetch_mode?
render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, skip_activities: true
render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
end
private
@ -20,17 +20,9 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
def set_items
case params[:id]
when 'featured'
@items = begin
# Because in public fetch mode we cache the response, there would be no
# benefit from performing the check below, since a blocked account or domain
# would likely be served the cache from the reverse proxy anyway
if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
[]
else
cache_collection(@account.pinned_statuses.not_local_only, Status)
end
end
@items = for_signed_account { cache_collection(@account.pinned_statuses.not_local_only, Status) }
when 'tags'
@items = for_signed_account { @account.featured_tags }
when 'devices'
@items = @account.devices
else
@ -40,7 +32,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
def set_size
case params[:id]
when 'featured', 'devices'
when 'featured', 'devices', 'tags'
@size = @items.size
else
not_found
@ -51,7 +43,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
case params[:id]
when 'featured'
@type = :ordered
when 'devices'
when 'devices', 'tags'
@type = :unordered
else
not_found
@ -66,4 +58,16 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
items: @items
)
end
def for_signed_account
# Because in public fetch mode we cache the response, there would be no
# benefit from performing the check below, since a blocked account or domain
# would likely be served the cache from the reverse proxy anyway
if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
[]
else
yield
end
end
end

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern
before_action :require_signature!
before_action :set_items
before_action :set_cache_headers
def show
expires_in 0, public: false
render json: collection_presenter,
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
end
private
def uri_prefix
signed_request_account.uri[/http(s?):\/\/[^\/]+\//]
end
def set_items
@items = @account.followers.where(Account.arel_table[:uri].matches(uri_prefix + '%', false, true)).pluck(:uri)
end
def collection_presenter
ActivityPub::CollectionPresenter.new(
id: account_followers_synchronization_url(@account),
type: :ordered,
items: @items
)
end
end

View file

@ -5,25 +5,26 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
include JsonLdHelper
include AccountOwnedConcern
before_action :skip_unknown_actor_delete
before_action :skip_unknown_actor_activity
before_action :require_signature!
skip_before_action :authenticate_user!
def create
upgrade_account
process_collection_synchronization
process_payload
head 202
end
private
def skip_unknown_actor_delete
head 202 if unknown_deleted_account?
def skip_unknown_actor_activity
head 202 if unknown_affected_account?
end
def unknown_deleted_account?
def unknown_affected_account?
json = Oj.load(body, mode: :strict)
json.is_a?(Hash) && json['type'] == 'Delete' && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists?
json.is_a?(Hash) && %w(Delete Update).include?(json['type']) && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists?
rescue Oj::ParseError
false
end
@ -32,6 +33,10 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
params[:account_username].present?
end
def skip_temporary_suspension_response?
true
end
def body
return @body if defined?(@body)
@ -52,6 +57,19 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
DeliveryFailureTracker.reset!(signed_request_account.inbox_url)
end
def process_collection_synchronization
raw_params = request.headers['Collection-Synchronization']
return if raw_params.blank? || ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] == 'true'
# Re-using the syntax for signature parameters
tree = SignatureParamsParser.new.parse(raw_params)
params = SignatureParamsTransformer.new.apply(tree)
ActivityPub::PrepareFollowersSynchronizationService.new.call(signed_request_account, params)
rescue Parslet::ParseFailed
Rails.logger.warn 'Error parsing Collection-Synchronization header'
end
def process_payload
ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body, @account&.id)
end

View file

@ -20,9 +20,9 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
def outbox_presenter
if page_requested?
ActivityPub::CollectionPresenter.new(
id: account_outbox_url(@account, page_params),
id: outbox_url(page_params),
type: :ordered,
part_of: account_outbox_url(@account),
part_of: outbox_url,
prev: prev_page,
next: next_page,
items: @statuses
@ -32,12 +32,20 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
id: account_outbox_url(@account),
type: :ordered,
size: @account.statuses_count,
first: account_outbox_url(@account, page: true),
last: account_outbox_url(@account, page: true, min_id: 0)
first: outbox_url(page: true),
last: outbox_url(page: true, min_id: 0)
)
end
end
def outbox_url(**kwargs)
if params[:account_username].present?
account_outbox_url(@account, **kwargs)
else
instance_actor_outbox_url(**kwargs)
end
end
def next_page
account_outbox_url(@account, page: true, max_id: @statuses.last.id) if @statuses.size == LIMIT
end
@ -49,9 +57,12 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
def set_statuses
return unless page_requested?
@statuses = @account.statuses.permitted_for(@account, signed_request_account)
@statuses = @statuses.paginate_by_id(LIMIT, params_slice(:max_id, :min_id, :since_id))
@statuses = cache_collection(@statuses, Status)
@statuses = cache_collection_paginated_by_id(
@account.statuses.permitted_for(@account, signed_request_account),
Status,
LIMIT,
params_slice(:max_id, :min_id, :since_id)
)
end
def page_requested?
@ -61,4 +72,8 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
def page_params
{ page: true, max_id: params[:max_id], min_id: params[:min_id] }.compact
end
def set_account
@account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative
end
end

View file

@ -31,7 +31,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
end
def set_replies
@replies = only_other_accounts? ? Status.where.not(account_id: @account.id) : @account.statuses
@replies = only_other_accounts? ? Status.where.not(account_id: @account.id).joins(:account).merge(Account.without_suspended) : @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

View file

@ -2,7 +2,7 @@
module Admin
class AccountsController < BaseController
before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject]
before_action :set_account, except: [:index]
before_action :require_remote_account!, only: [:redownload]
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
@ -14,49 +14,65 @@ module Admin
def show
authorize @account, :show?
@deletion_request = @account.deletion_request
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
@moderation_notes = @account.targeted_moderation_notes.latest
@warnings = @account.targeted_account_warnings.latest.custom
@domain_block = DomainBlock.rule_for(@account.domain)
end
def memorialize
authorize @account, :memorialize?
@account.memorialize!
log_action :memorialize, @account
redirect_to admin_account_path(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.memorialized_msg', username: @account.acct)
end
def enable
authorize @account.user, :enable?
@account.user.enable!
log_action :enable, @account.user
redirect_to admin_account_path(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.enabled_msg', username: @account.acct)
end
def approve
authorize @account.user, :approve?
@account.user.approve!
redirect_to admin_pending_accounts_path
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.approved_msg', username: @account.acct)
end
def reject
authorize @account.user, :reject?
SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false)
redirect_to admin_pending_accounts_path
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct)
end
def destroy
authorize @account, :destroy?
Admin::AccountDeletionWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.destroyed_msg', username: @account.acct)
end
def unsensitive
authorize @account, :unsensitive?
@account.unsensitize!
log_action :unsensitive, @account
redirect_to admin_account_path(@account.id)
end
def unsilence
authorize @account, :unsilence?
@account.unsilence!
log_action :unsilence, @account
redirect_to admin_account_path(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unsilenced_msg', username: @account.acct)
end
def unsuspend
authorize @account, :unsuspend?
@account.unsuspend!
Admin::UnsuspensionWorker.perform_async(@account.id)
log_action :unsuspend, @account
redirect_to admin_account_path(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unsuspended_msg', username: @account.acct)
end
def redownload
@ -65,7 +81,7 @@ module Admin
@account.update!(last_webfingered_at: nil)
ResolveAccountService.new.call(@account)
redirect_to admin_account_path(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.redownloaded_msg', username: @account.acct)
end
def remove_avatar
@ -76,7 +92,7 @@ module Admin
log_action :remove_avatar, @account.user
redirect_to admin_account_path(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_avatar_msg', username: @account.acct)
end
def remove_header
@ -87,7 +103,7 @@ module Admin
log_action :remove_header, @account.user
redirect_to admin_account_path(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_header_msg', username: @account.acct)
end
private

View file

@ -71,7 +71,7 @@ class Admin::AnnouncementsController < Admin::BaseController
private
def set_announcements
@announcements = AnnouncementFilter.new(filter_params).results.page(params[:page])
@announcements = AnnouncementFilter.new(filter_params).results.reverse_chronological.page(params[:page])
end
def set_announcement

View file

@ -4,6 +4,7 @@ require 'sidekiq/api'
module Admin
class DashboardController < BaseController
def index
@system_checks = Admin::SystemCheck.perform
@users_count = User.count
@pending_users_count = User.pending.count
@registrations_week = Redis.current.get("activity:accounts:local:#{current_week}") || 0
@ -35,7 +36,6 @@ module Admin
@profile_directory = Setting.profile_directory
@timeline_preview = Setting.timeline_preview
@keybase_integration = Setting.enable_keybase
@spam_check_enabled = Setting.spam_check_enabled
@trends_enabled = Setting.trends
end

View file

@ -22,13 +22,14 @@ module Admin
if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
@domain_block.save
flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe # rubocop:disable Rails/OutputSafety
@domain_block.errors[:domain].clear
@domain_block.errors.delete(:domain)
render :new
else
if existing_domain_block.present?
@domain_block = existing_domain_block
@domain_block.update(resource_params)
end
if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id)
log_action :create, @domain_block
@ -40,7 +41,7 @@ module Admin
end
def update
authorize :domain_block, :create?
authorize :domain_block, :update?
@domain_block.update(update_params)
@ -48,7 +49,7 @@ module Admin
if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id, severity_changed)
log_action :create, @domain_block
log_action :update, @domain_block
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
else
render :edit
@ -73,11 +74,11 @@ module Admin
end
def update_params
params.require(:domain_block).permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment)
params.require(:domain_block).permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate)
end
def resource_params
params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment)
params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate)
end
end
end

View file

@ -27,7 +27,7 @@ module Admin
ips = []
Resolv::DNS.open do |dns|
dns.timeouts = 1
dns.timeouts = 5
hostnames = dns.getresources(@email_domain_block.domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s }

View file

@ -0,0 +1,53 @@
# frozen_string_literal: true
module Admin
class FollowRecommendationsController < BaseController
before_action :set_language
def show
authorize :follow_recommendation, :show?
@form = Form::AccountBatch.new
@accounts = filtered_follow_recommendations
end
def update
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
# Do nothing
ensure
redirect_to admin_follow_recommendations_path(filter_params)
end
private
def set_language
@language = follow_recommendation_filter.language
end
def filtered_follow_recommendations
follow_recommendation_filter.results
end
def follow_recommendation_filter
@follow_recommendation_filter ||= FollowRecommendationFilter.new(filter_params)
end
def form_account_batch_params
params.require(:form_account_batch).permit(:action, account_ids: [])
end
def filter_params
params.slice(*FollowRecommendationFilter::KEYS).permit(*FollowRecommendationFilter::KEYS)
end
def action_from_button
if params[:suppress]
'suppress_follow_recommendation'
elsif params[:unsuppress]
'unsuppress_follow_recommendation'
end
end
end
end

View file

@ -2,65 +2,31 @@
module Admin
class InstancesController < BaseController
before_action :set_domain_block, only: :show
before_action :set_domain_allow, only: :show
before_action :set_instances, only: :index
before_action :set_instance, only: :show
def index
authorize :instance, :index?
@instances = ordered_instances
end
def show
authorize :instance, :show?
@following_count = Follow.where(account: Account.where(domain: params[:id])).count
@followers_count = Follow.where(target_account: Account.where(domain: params[:id])).count
@reports_count = Report.where(target_account: Account.where(domain: params[:id])).count
@blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count
@available = DeliveryFailureTracker.available?(params[:id])
@media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size)
@private_comment = @domain_block&.private_comment
@public_comment = @domain_block&.public_comment
end
private
def set_domain_block
@domain_block = DomainBlock.rule_for(params[:id])
end
def set_domain_allow
@domain_allow = DomainAllow.rule_for(params[:id])
end
def set_instance
resource = Account.by_domain_accounts.find_by(domain: params[:id])
resource ||= @domain_block
resource ||= @domain_allow
@instance = Instance.find(params[:id])
end
if resource
@instance = Instance.new(resource)
else
not_found
end
def set_instances
@instances = filtered_instances.page(params[:page])
end
def filtered_instances
InstanceFilter.new(whitelist_mode? ? { allowed: true } : filter_params).results
end
def paginated_instances
filtered_instances.page(params[:page])
end
helper_method :paginated_instances
def ordered_instances
paginated_instances.map { |resource| Instance.new(resource) }
end
def filter_params
params.slice(*InstanceFilter::KEYS).permit(*InstanceFilter::KEYS)
end

View file

@ -0,0 +1,56 @@
# frozen_string_literal: true
module Admin
class IpBlocksController < BaseController
def index
authorize :ip_block, :index?
@ip_blocks = IpBlock.page(params[:page])
@form = Form::IpBlockBatch.new
end
def new
authorize :ip_block, :create?
@ip_block = IpBlock.new(ip: '', severity: :no_access, expires_in: 1.year)
end
def create
authorize :ip_block, :create?
@ip_block = IpBlock.new(resource_params)
if @ip_block.save
log_action :create, @ip_block
redirect_to admin_ip_blocks_path, notice: I18n.t('admin.ip_blocks.created_msg')
else
render :new
end
end
def batch
@form = Form::IpBlockBatch.new(form_ip_block_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.ip_blocks.no_ip_block_selected')
rescue Mastodon::NotPermittedError
flash[:alert] = I18n.t('admin.custom_emojis.not_permitted')
ensure
redirect_to admin_ip_blocks_path
end
private
def resource_params
params.require(:ip_block).permit(:ip, :severity, :comment, :expires_in)
end
def action_from_button
'delete' if params[:delete]
end
def form_ip_block_batch_params
params.require(:form_ip_block_batch).permit(ip_block_ids: [])
end
end
end

View file

@ -0,0 +1,59 @@
# frozen_string_literal: true
module Admin
class RulesController < BaseController
before_action :set_rule, except: [:index, :create]
def index
authorize :rule, :index?
@rules = Rule.ordered
@rule = Rule.new
end
def create
authorize :rule, :create?
@rule = Rule.new(resource_params)
if @rule.save
redirect_to admin_rules_path
else
@rules = Rule.ordered
render :index
end
end
def edit
authorize @rule, :update?
end
def update
authorize @rule, :update?
if @rule.update(resource_params)
redirect_to admin_rules_path
else
render :edit
end
end
def destroy
authorize @rule, :destroy?
@rule.discard
redirect_to admin_rules_path
end
private
def set_rule
@rule = Rule.find(params[:id])
end
def resource_params
params.require(:rule).permit(:text, :priority)
end
end
end

View file

@ -14,7 +14,7 @@ module Admin
@statuses = @account.statuses.where(visibility: [:public, :unlisted])
if params[:media]
account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).group(:status_id)
@statuses.merge!(Status.where(id: account_media_status_ids))
end

View file

@ -59,8 +59,8 @@ module Admin
.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')
.reorder(statuses_count: :desc)
.pluck(Arel.sql('accounts.domain, count(*) AS statuses_count'))
end
def set_counters

View file

@ -40,7 +40,7 @@ class Api::BaseController < ApplicationController
render json: { error: 'This action is not allowed' }, status: 403
end
rescue_from Mastodon::RaceConditionError do
rescue_from Mastodon::RaceConditionError, Seahorse::Client::NetworkingError, Stoplight::Error::RedLight do
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
end
@ -71,6 +71,7 @@ class Api::BaseController < ApplicationController
def limit_param(default_limit)
return default_limit unless params[:limit]
[params[:limit].to_i.abs, default_limit * 2].min
end
@ -95,14 +96,14 @@ class Api::BaseController < ApplicationController
def require_user!
if !current_user
render json: { error: 'This method requires an authenticated user' }, status: 422
elsif current_user.disabled?
render json: { error: 'Your login is currently disabled' }, status: 403
elsif !current_user.confirmed?
render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403
elsif !current_user.approved?
render json: { error: 'Your login is currently pending approval' }, status: 403
elsif !current_user.functional?
render json: { error: 'Your login is currently disabled' }, status: 403
else
set_user_activity
update_user_sign_in
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
class Api::V1::Accounts::FeaturedTagsController < Api::BaseController
before_action :set_account
before_action :set_featured_tags
respond_to :json
def index
render json: @featured_tags, each_serializer: REST::FeaturedTagSerializer
end
private
def set_account
@account = Account.find(params[:account_id])
end
def set_featured_tags
@featured_tags = @account.suspended? ? [] : @account.featured_tags
end
end

View file

@ -25,7 +25,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
end
def hide_results?
(@account.hides_followers? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
@account.suspended? || (@account.hides_followers? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
end
def default_accounts

View file

@ -25,7 +25,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
end
def hide_results?
(@account.hides_following? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
@account.suspended? || (@account.hides_following? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
end
def default_accounts

View file

@ -5,7 +5,7 @@ class Api::V1::Accounts::IdentityProofsController < Api::BaseController
before_action :set_account
def index
@proofs = @account.identity_proofs.active
@proofs = @account.suspended? ? [] : @account.identity_proofs.active
render json: @proofs, each_serializer: REST::IdentityProofSerializer
end

View file

@ -6,7 +6,7 @@ class Api::V1::Accounts::ListsController < Api::BaseController
before_action :set_account
def index
@lists = @account.lists.where(account: current_account)
@lists = @account.suspended? ? [] : @account.lists.where(account: current_account)
render json: @lists, each_serializer: REST::ListSerializer
end

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
class Api::V1::Accounts::LookupController < Api::BaseController
before_action -> { authorize_if_got_token! :read, :'read:accounts' }
before_action :set_account
def show
render json: @account, serializer: REST::AccountSerializer
end
private
def set_account
@account = ResolveAccountService.new.call(params[:acct], skip_webfinger: true) || raise(ActiveRecord::RecordNotFound)
end
end

View file

@ -0,0 +1,30 @@
# frozen_string_literal: true
class Api::V1::Accounts::NotesController < Api::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }
before_action :require_user!
before_action :set_account
def create
if params[:comment].blank?
AccountNote.find_by(account: current_account, target_account: @account)&.destroy
else
@note = AccountNote.find_or_initialize_by(account: current_account, target_account: @account)
@note.comment = params[:comment]
@note.save! if @note.changed?
end
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter
end
private
def set_account
@account = Account.find(params[:account_id])
end
def relationships_presenter
AccountRelationshipsPresenter.new([@account.id], current_user.account_id)
end
end

View file

@ -18,14 +18,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
end
def load_statuses
cached_account_statuses
@account.suspended? ? [] : cached_account_statuses
end
def cached_account_statuses
cache_collection account_statuses, Status
end
def account_statuses
statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses
statuses.merge!(only_media_scope) if truthy_param?(:only_media)
@ -33,7 +29,12 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
statuses.merge!(hashtag_scope) if params[:tagged].present?
statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
cache_collection_paginated_by_id(
statuses,
Status,
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
end
def permitted_account_statuses
@ -41,17 +42,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
end
def only_media_scope
Status.where(id: account_media_status_ids)
end
def account_media_status_ids
# `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids.
# Also, Avoid getting slow by not narrowing down by `statuses.account_id`.
# When narrowing down by `statuses.account_id`, `index_statuses_20180106` will be used
# and the table will be joined by `Merge Semi Join`, so the query will be slow.
@account.statuses.joins(:media_attachments).merge(@account.media_attachments).permitted_for(@account, current_account)
.paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
.reorder(id: :desc).distinct(:id).pluck(:id)
Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id)
end
def pinned_scope

View file

@ -9,7 +9,6 @@ class Api::V1::AccountsController < Api::BaseController
before_action :require_user!, except: [:show, :create]
before_action :set_account, except: [:create]
before_action :check_account_suspension, only: [:show]
before_action :check_enabled_registrations, only: [:create]
skip_before_action :require_authenticated_user!, only: :create
@ -21,19 +20,20 @@ class Api::V1::AccountsController < Api::BaseController
end
def create
token = AppSignUpService.new.call(doorkeeper_token.application, account_params)
token = AppSignUpService.new.call(doorkeeper_token.application, request.remote_ip, account_params)
response = Doorkeeper::OAuth::TokenResponse.new(token)
headers.merge!(response.headers)
self.response_body = Oj.dump(response.body)
self.status = response.status
rescue ActiveRecord::RecordInvalid => e
render json: ValidationErrorFormatter.new(e, :'account.username' => :username, :'invite_request.text' => :reason).as_json, status: :unprocessable_entity
end
def follow
FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs), with_rate_limit: true)
options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, with_rate_limit: true)
options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify? } }, requested_map: { @account.id => false } }
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
end
@ -44,7 +44,7 @@ class Api::V1::AccountsController < Api::BaseController
end
def mute
MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications))
MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications), duration: (params[:duration]&.to_i || 0))
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
end
@ -73,10 +73,6 @@ class Api::V1::AccountsController < Api::BaseController
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options)
end
def check_account_suspension
gone if @account.suspended?
end
def account_params
params.permit(:username, :email, :password, :agreement, :locale, :reason)
end

View file

@ -22,6 +22,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController
active
pending
disabled
sensitized
silenced
suspended
username
@ -58,7 +59,20 @@ class Api::V1::Admin::AccountsController < Api::BaseController
def reject
authorize @account.user, :reject?
SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false)
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
render json: @account, serializer: REST::Admin::AccountSerializer
end
def destroy
authorize @account, :destroy?
Admin::AccountDeletionWorker.perform_async(@account.id)
render json: @account, serializer: REST::Admin::AccountSerializer
end
def unsensitive
authorize @account, :unsensitive?
@account.unsensitize!
log_action :unsensitive, @account
render json: @account, serializer: REST::Admin::AccountSerializer
end
@ -72,6 +86,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController
def unsuspend
authorize @account, :unsuspend?
@account.unsuspend!
Admin::UnsuspensionWorker.perform_async(@account.id)
log_action :unsuspend, @account
render json: @account, serializer: REST::Admin::AccountSerializer
end
@ -79,7 +94,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController
private
def set_accounts
@accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite]).paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
@accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite]).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def set_account

View file

@ -63,7 +63,7 @@ class Api::V1::Admin::ReportsController < Api::BaseController
private
def set_reports
@reports = filtered_reports.order(id: :desc).with_accounts.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
@reports = filtered_reports.order(id: :desc).with_accounts.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def set_report

View file

@ -18,6 +18,8 @@ class Api::V1::BlocksController < Api::BaseController
def paginated_blocks
@paginated_blocks ||= Block.eager_load(target_account: :account_stat)
.joins(:target_account)
.merge(Account.without_suspended)
.where(account: current_account)
.paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT),

View file

@ -17,14 +17,11 @@ class Api::V1::BookmarksController < Api::BaseController
end
def cached_bookmarks
cache_collection(
Status.reorder(nil).joins(:bookmarks).merge(results),
Status
)
cache_collection(results.map(&:status), Status)
end
def results
@_results ||= account_bookmarks.paginate_by_id(
@_results ||= account_bookmarks.eager_load(:status).to_a_paginated_by_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)

View file

@ -32,7 +32,7 @@ class Api::V1::ConversationsController < Api::BaseController
def paginated_conversations
AccountConversation.where(account: current_account)
.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def insert_pagination_headers

View file

@ -26,7 +26,7 @@ class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController
end
def set_encrypted_messages
@encrypted_messages = @current_device.encrypted_messages.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
@encrypted_messages = @current_device.encrypted_messages.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def insert_pagination_headers

View file

@ -12,7 +12,7 @@ class Api::V1::Crypto::Keys::ClaimsController < Api::BaseController
private
def set_claim_results
@claim_results = devices.map { |device_params| ::Keys::ClaimService.new.call(current_account, device_params[:account_id], device_params[:device_id]) }.compact
@claim_results = devices.filter_map { |device_params| ::Keys::ClaimService.new.call(current_account, device_params[:account_id], device_params[:device_id]) }
end
def resource_params

View file

@ -17,7 +17,7 @@ class Api::V1::Crypto::Keys::QueriesController < Api::BaseController
end
def set_query_results
@query_results = @accounts.map { |account| ::Keys::QueryService.new.call(account) }.compact
@query_results = @accounts.filter_map { |account| ::Keys::QueryService.new.call(account) }
end
def account_ids

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
class Api::V1::Emails::ConfirmationsController < Api::BaseController
before_action :doorkeeper_authorize!
before_action :require_user_owned_by_application!
def create
if !current_user.confirmed? && current_user.unconfirmed_email.present?
current_user.update!(email: params[:email]) if params.key?(:email)
current_user.resend_confirmation_instructions
end
render_empty
end
private
def require_user_owned_by_application!
render json: { error: 'This method is only available to the application the user originally signed-up with' }, status: :forbidden unless current_user && current_user.created_by_application_id == doorkeeper_token.application_id
end
end

View file

@ -25,7 +25,7 @@ class Api::V1::EndorsementsController < Api::BaseController
end
def endorsed_accounts
current_account.endorsed_accounts.includes(:account_stat)
current_account.endorsed_accounts.includes(:account_stat).without_suspended
end
def insert_pagination_headers

View file

@ -17,14 +17,11 @@ class Api::V1::FavouritesController < Api::BaseController
end
def cached_favourites
cache_collection(
Status.reorder(nil).joins(:favourites).merge(results),
Status
)
cache_collection(results.map(&:status), Status)
end
def results
@_results ||= account_favourites.paginate_by_id(
@_results ||= account_favourites.eager_load(:status).to_a_paginated_by_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)

View file

@ -3,15 +3,15 @@
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
before_action :set_recently_used_tags, only: :index
def index
render json: @most_used_tags, each_serializer: REST::TagSerializer
render json: @recently_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)
def set_recently_used_tags
@recently_used_tags = Tag.recently_used(current_account).where.not(id: current_account.featured_tags).limit(10)
end
end

View file

@ -13,7 +13,7 @@ class Api::V1::FollowRequestsController < Api::BaseController
def authorize
AuthorizeFollowService.new.call(account, current_account)
NotifyService.new.call(current_account, Follow.find_by(account: account, target_account: current_account))
NotifyService.new.call(current_account, :follow, Follow.find_by(account: account, target_account: current_account))
render json: account, serializer: REST::RelationshipSerializer, relationships: relationships
end
@ -37,7 +37,7 @@ class Api::V1::FollowRequestsController < Api::BaseController
end
def default_accounts
Account.includes(:follow_requests, :account_stat).references(:follow_requests)
Account.without_suspended.includes(:follow_requests, :account_stat).references(:follow_requests)
end
def paginated_follow_requests

View file

@ -8,7 +8,7 @@ class Api::V1::Instances::PeersController < Api::BaseController
def index
expires_in 1.day, public: true
render_with_cache(expires_in: 1.day) { Account.remote.domains }
render_with_cache(expires_in: 1.day) { Instance.where.not(domain: DomainBlock.select(:domain)).pluck(:domain) }
end
private

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
class Api::V1::Instances::RulesController < Api::BaseController
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
before_action :set_rules
def index
render json: @rules, each_serializer: REST::RuleSerializer
end
private
def set_rules
@rules = Rule.ordered
end
end

View file

@ -37,9 +37,9 @@ class Api::V1::Lists::AccountsController < Api::BaseController
def load_accounts
if unlimited?
@list.accounts.includes(:account_stat).all
@list.accounts.without_suspended.includes(:account_stat).all
else
@list.accounts.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
@list.accounts.without_suspended.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
end
end

View file

@ -7,7 +7,7 @@ class Api::V1::MarkersController < Api::BaseController
before_action :require_user!
def index
@markers = current_user.markers.where(timeline: Array(params[:timeline])).each_with_object({}) { |marker, h| h[marker.timeline] = marker }
@markers = current_user.markers.where(timeline: Array(params[:timeline])).index_by(&:timeline)
render json: serialize_map(@markers)
end

View file

@ -39,7 +39,7 @@ class Api::V1::MediaController < Api::BaseController
end
def media_attachment_params
params.permit(:file, :description, :focus)
params.permit(:file, :thumbnail, :description, :focus)
end
def file_type_error

View file

@ -6,27 +6,20 @@ class Api::V1::MutesController < Api::BaseController
after_action :insert_pagination_headers
def index
@data = @accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer
@accounts = load_accounts
render json: @accounts, each_serializer: REST::MutedAccountSerializer
end
def details
@data = @mutes = load_mutes
render json: @mutes, each_serializer: REST::MuteSerializer
end
private
def load_accounts
paginated_mutes.map(&:target_account)
end
def load_mutes
paginated_mutes.includes(:account, :target_account).to_a
end
def paginated_mutes
@paginated_mutes ||= Mute.eager_load(:target_account)
.joins(:target_account)
.merge(Account.without_suspended)
.where(account: current_account)
.paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT),
@ -41,34 +34,26 @@ class Api::V1::MutesController < Api::BaseController
def next_path
if records_continue?
url_for pagination_params(max_id: pagination_max_id)
api_v1_mutes_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
unless @data.empty?
url_for pagination_params(since_id: pagination_since_id)
unless paginated_mutes.empty?
api_v1_mutes_url pagination_params(since_id: pagination_since_id)
end
end
def pagination_max_id
if params[:action] == "details"
@mutes.last.id
else
paginated_mutes.last.id
end
paginated_mutes.last.id
end
def pagination_since_id
if params[:action] == "details"
@mutes.first.id
else
paginated_mutes.first.id
end
paginated_mutes.first.id
end
def records_continue?
@data.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
paginated_mutes.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end
def pagination_params(core_params)

View file

@ -14,7 +14,7 @@ class Api::V1::NotificationsController < Api::BaseController
end
def show
@notification = current_account.notifications.find(params[:id])
@notification = current_account.notifications.without_suspended.find(params[:id])
render json: @notification, serializer: REST::NotificationSerializer
end
@ -40,18 +40,17 @@ class Api::V1::NotificationsController < Api::BaseController
private
def load_notifications
cache_collection paginated_notifications, Notification
end
def paginated_notifications
browserable_account_notifications.paginate_by_id(
notifications = browserable_account_notifications.includes(from_account: :account_stat).to_a_paginated_by_id(
limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses|
cache_collection(target_statuses, Status)
end
end
def browserable_account_notifications
current_account.notifications.browserable(exclude_types, from_account)
current_account.notifications.without_suspended.browserable(exclude_types, from_account)
end
def target_statuses_from_notifications

View file

@ -3,13 +3,13 @@
class Api::V1::Push::SubscriptionsController < Api::BaseController
before_action -> { doorkeeper_authorize! :push }
before_action :require_user!
before_action :set_web_push_subscription
before_action :check_web_push_subscription, only: [:show, :update]
before_action :set_push_subscription
before_action :check_push_subscription, only: [:show, :update]
def create
@web_subscription&.destroy!
@push_subscription&.destroy!
@web_subscription = ::Web::PushSubscription.create!(
@push_subscription = Web::PushSubscription.create!(
endpoint: subscription_params[:endpoint],
key_p256dh: subscription_params[:keys][:p256dh],
key_auth: subscription_params[:keys][:auth],
@ -18,31 +18,31 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
access_token_id: doorkeeper_token.id
)
render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
end
def show
render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
end
def update
@web_subscription.update!(data: data_params)
render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
@push_subscription.update!(data: data_params)
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
end
def destroy
@web_subscription&.destroy!
@push_subscription&.destroy!
render_empty
end
private
def set_web_push_subscription
@web_subscription = ::Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id)
def set_push_subscription
@push_subscription = Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id)
end
def check_web_push_subscription
not_found if @web_subscription.nil?
def check_push_subscription
not_found if @push_subscription.nil?
end
def subscription_params
@ -52,6 +52,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
def data_params
return {} if params[:data].blank?
params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
params.require(:data).permit(:policy, alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status])
end
end

View file

@ -32,7 +32,7 @@ class Api::V1::ScheduledStatusesController < Api::BaseController
private
def set_statuses
@statuses = current_account.scheduled_statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
@statuses = current_account.scheduled_statuses.to_a_paginated_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def set_status

View file

@ -5,7 +5,7 @@ class Api::V1::Statuses::BookmarksController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:bookmarks' }
before_action :require_user!
before_action :set_status
before_action :set_status, only: [:create]
def create
current_account.bookmarks.find_or_create_by!(account: current_account, status: @status)
@ -13,10 +13,20 @@ class Api::V1::Statuses::BookmarksController < Api::BaseController
end
def destroy
bookmark = current_account.bookmarks.find_by(status: @status)
bookmark = current_account.bookmarks.find_by(status_id: params[:status_id])
if bookmark
@status = bookmark.status
else
@status = Status.find(params[:status_id])
authorize @status, :show?
end
bookmark&.destroy!
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, bookmarks_map: { @status.id => false })
rescue Mastodon::NotPermittedError
not_found
end
private

View file

@ -22,6 +22,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
def default_accounts
Account
.without_suspended
.includes(:favourites, :account_stat)
.references(:favourites)
.where(favourites: { status_id: @status.id })

View file

@ -5,7 +5,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:favourites' }
before_action :require_user!
before_action :set_status
before_action :set_status, only: [:create]
def create
FavouriteService.new.call(current_account, @status)
@ -13,8 +13,19 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
end
def destroy
UnfavouriteWorker.perform_async(current_account.id, @status.id)
fav = current_account.favourites.find_by(status_id: params[:status_id])
if fav
@status = fav.status
UnfavouriteWorker.perform_async(current_account.id, @status.id)
else
@status = Status.find(params[:status_id])
authorize @status, :show?
end
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, favourites_map: { @status.id => false })
rescue Mastodon::NotPermittedError
not_found
end
private

View file

@ -21,7 +21,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
end
def default_accounts
Account.includes(:statuses, :account_stat).references(:statuses)
Account.without_suspended.includes(:statuses, :account_stat).references(:statuses)
end
def paginated_statuses

View file

@ -5,7 +5,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
before_action :require_user!
before_action :set_reblog
before_action :set_reblog, only: [:create]
override_rate_limit_headers :create, family: :statuses
@ -16,15 +16,21 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
end
def destroy
@status = current_account.statuses.find_by(reblog_of_id: @reblog.id)
@status = current_account.statuses.find_by(reblog_of_id: params[:status_id])
if @status
authorize @status, :unreblog?
@status.discard
RemovalWorker.perform_async(@status.id)
@reblog = @status.reblog
else
@reblog = Status.find(params[:status_id])
authorize @reblog, :show?
end
render json: @reblog, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, reblogs_map: { @reblog.id => false })
rescue Mastodon::NotPermittedError
not_found
end
private

View file

@ -58,6 +58,7 @@ class Api::V1::StatusesController < Api::BaseController
@status.discard
RemovalWorker.perform_async(@status.id, redraft: true)
@status.account.statuses_count = @status.account.statuses_count - 1
render json: @status, serializer: REST::StatusSerializer, source_requested: true
end

View file

@ -19,6 +19,6 @@ class Api::V1::SuggestionsController < Api::BaseController
private
def set_accounts
@accounts = PotentialFriendshipTracker.get(current_account.id, limit: limit_param(DEFAULT_ACCOUNTS_LIMIT))
@accounts = PotentialFriendshipTracker.get(current_account, limit_param(DEFAULT_ACCOUNTS_LIMIT))
end
end

View file

@ -16,30 +16,32 @@ class Api::V1::Timelines::PublicController < Api::BaseController
end
def load_statuses
cached_public_statuses
cached_public_statuses_page
end
def cached_public_statuses
cache_collection public_statuses, Status
def cached_public_statuses_page
cache_collection(public_statuses, Status)
end
def public_statuses
statuses = public_timeline_statuses.paginate_by_id(
public_feed.get(
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
params[:max_id],
params[:since_id],
params[:min_id]
)
if truthy_param?(:only_media)
# `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids.
status_ids = statuses.joins(:media_attachments).distinct(:id).pluck(:id)
statuses.where(id: status_ids)
else
statuses
end
end
def public_timeline_statuses
Status.as_public_timeline(current_account, truthy_param?(:remote) ? :remote : truthy_param?(:local))
def public_feed
PublicFeed.new(
current_account,
local: truthy_param?(:local),
remote: truthy_param?(:remote),
only_media: truthy_param?(:only_media),
allow_local_only: truthy_param?(:allow_local_only),
with_replies: Setting.show_replies_in_public_timelines,
with_reblogs: Setting.show_reblogs_in_public_timelines,
)
end
def insert_pagination_headers
@ -47,7 +49,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
end
def pagination_params(core_params)
params.slice(:local, :remote, :limit, :only_media).permit(:local, :remote, :limit, :only_media).merge(core_params)
params.slice(:local, :remote, :limit, :only_media, :allow_local_only).permit(:local, :remote, :limit, :only_media, :allow_local_only).merge(core_params)
end
def next_path

View file

@ -20,30 +20,29 @@ class Api::V1::Timelines::TagController < Api::BaseController
end
def cached_tagged_statuses
cache_collection tagged_statuses, Status
end
def tagged_statuses
if @tag.nil?
[]
else
statuses = tag_timeline_statuses.paginate_by_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
if truthy_param?(:only_media)
# `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids.
status_ids = statuses.joins(:media_attachments).distinct(:id).pluck(:id)
statuses.where(id: status_ids)
else
statuses
end
end
@tag.nil? ? [] : cache_collection(tag_timeline_statuses, Status)
end
def tag_timeline_statuses
HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, truthy_param?(:local))
tag_feed.get(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id],
params[:min_id]
)
end
def tag_feed
TagFeed.new(
@tag,
current_account,
any: params[:any],
all: params[:all],
none: params[:none],
local: truthy_param?(:local),
remote: truthy_param?(:remote),
only_media: truthy_param?(:only_media)
)
end
def insert_pagination_headers

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
class Api::V2::SuggestionsController < Api::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :read }
before_action :require_user!
before_action :set_suggestions
def index
render json: @suggestions, each_serializer: REST::SuggestionSerializer
end
private
def set_suggestions
@suggestions = AccountSuggestions.get(current_account, limit_param(DEFAULT_ACCOUNTS_LIMIT))
end
end

View file

@ -2,6 +2,7 @@
class Api::Web::PushSubscriptionsController < Api::Web::BaseController
before_action :require_user!
before_action :set_push_subscription, only: :update
def create
active_session = current_session
@ -15,19 +16,22 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
alerts_enabled = active_session.detection.device.mobile? || active_session.detection.device.tablet?
data = {
policy: 'all',
alerts: {
follow: alerts_enabled,
follow_request: false,
follow_request: alerts_enabled,
favourite: alerts_enabled,
reblog: alerts_enabled,
mention: alerts_enabled,
poll: alerts_enabled,
status: alerts_enabled,
},
}
data.deep_merge!(data_params) if params[:data]
web_subscription = ::Web::PushSubscription.create!(
push_subscription = ::Web::PushSubscription.create!(
endpoint: subscription_params[:endpoint],
key_p256dh: subscription_params[:keys][:p256dh],
key_auth: subscription_params[:keys][:auth],
@ -36,27 +40,27 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
access_token_id: active_session.access_token_id
)
active_session.update!(web_push_subscription: web_subscription)
active_session.update!(web_push_subscription: push_subscription)
render json: web_subscription, serializer: REST::WebPushSubscriptionSerializer
render json: push_subscription, serializer: REST::WebPushSubscriptionSerializer
end
def update
params.require([:id])
web_subscription = ::Web::PushSubscription.find(params[:id])
web_subscription.update!(data: data_params)
render json: web_subscription, serializer: REST::WebPushSubscriptionSerializer
@push_subscription.update!(data: data_params)
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
end
private
def set_push_subscription
@push_subscription = ::Web::PushSubscription.find(params[:id])
end
def subscription_params
@subscription_params ||= params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
end
def data_params
@data_params ||= params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
@data_params ||= params.require(:data).permit(:policy, alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status])
end
end

View file

@ -2,17 +2,16 @@
class Api::Web::SettingsController < Api::Web::BaseController
before_action :require_user!
before_action :set_setting
def update
setting.data = params[:data]
setting.save!
@setting.update!(data: params[:data])
render_empty
end
private
def setting
@_setting ||= ::Web::Setting.where(user: current_user).first_or_initialize(user: current_user)
def set_setting
@setting = ::Web::Setting.where(user: current_user).first_or_initialize(user: current_user)
end
end

View file

@ -5,8 +5,6 @@ class ApplicationController < ActionController::Base
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
force_ssl if: :https_enabled?
include Localized
include UserTrackingConcern
include SessionTrackingConcern
@ -29,7 +27,7 @@ class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from Mastodon::NotPermittedError, with: :forbidden
rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
rescue_from Mastodon::RaceConditionError, with: :service_unavailable
rescue_from Mastodon::RaceConditionError, Seahorse::Client::NetworkingError, Stoplight::Error::RedLight, with: :service_unavailable
rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
@ -43,10 +41,6 @@ class ApplicationController < ActionController::Base
private
def https_enabled?
Rails.env.production? && !request.path.start_with?('/health')
end
def authorized_fetch_mode?
ENV['AUTHORIZED_FETCH'] == 'true' || Rails.configuration.x.whitelist_mode
end
@ -56,7 +50,7 @@ class ApplicationController < ActionController::Base
end
def store_current_location
store_location_for(:user, request.url) unless request.format == :json
store_location_for(:user, request.url) unless [:json, :rss].include?(request.format&.to_sym)
end
def require_admin!

View file

@ -9,7 +9,10 @@ class Auth::PasswordsController < Devise::PasswordsController
def update
super do |resource|
resource.session_activations.destroy_all if resource.errors.empty?
if resource.errors.empty?
resource.session_activations.destroy_all
resource.forget_me!
end
end
end

View file

@ -1,6 +1,9 @@
# frozen_string_literal: true
class Auth::RegistrationsController < Devise::RegistrationsController
include Devise::Controllers::Rememberable
include RegistrationSpamConcern
layout :determine_layout
before_action :set_invite, only: [:new, :create]
@ -12,6 +15,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :set_body_classes, only: [:new, :create, :edit, :update]
before_action :require_not_suspended!, only: [:update]
before_action :set_cache_headers, only: [:edit, :update]
before_action :set_registration_form_time, only: :new
skip_before_action :require_functional!, only: [:edit, :update]
@ -25,7 +29,11 @@ class Auth::RegistrationsController < Devise::RegistrationsController
def update
super do |resource|
resource.clear_other_sessions(current_session.session_id) if resource.saved_change_to_encrypted_password?
if resource.saved_change_to_encrypted_password?
resource.clear_other_sessions(current_session.session_id)
resource.forget_me!
remember_me(resource)
end
end
end
@ -40,16 +48,17 @@ class Auth::RegistrationsController < Devise::RegistrationsController
def build_resource(hash = nil)
super(hash)
resource.locale = I18n.locale
resource.invite_code = params[:invite_code] if resource.invite_code.blank?
resource.current_sign_in_ip = request.remote_ip
resource.locale = I18n.locale
resource.invite_code = params[:invite_code] if resource.invite_code.blank?
resource.registration_form_time = session[:registration_form_time]
resource.sign_up_ip = request.remote_ip
resource.build_account if resource.account.nil?
end
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up) do |u|
u.permit({ account_attributes: [:username], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement)
u.permit({ account_attributes: [:username], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password)
end
end

View file

@ -7,6 +7,7 @@ class Auth::SessionsController < Devise::SessionsController
skip_before_action :require_no_authentication, only: [:create]
skip_before_action :require_functional!
skip_before_action :update_user_sign_in
prepend_before_action :set_pack
@ -26,6 +27,7 @@ class Auth::SessionsController < Devise::SessionsController
def create
super do |resource|
resource.update_sign_in!(request, new_sign_in: true)
remember_me(resource)
flash.delete(:notice)
end
@ -39,20 +41,37 @@ class Auth::SessionsController < Devise::SessionsController
store_location_for(:user, tmp_stored_location) if continue_after?
end
def webauthn_options
user = find_user
if user.webauthn_enabled?
options_for_get = WebAuthn::Credential.options_for_get(
allow: user.webauthn_credentials.pluck(:external_id)
)
session[:webauthn_challenge] = options_for_get.challenge
render json: options_for_get, status: :ok
else
render json: { error: t('webauthn_credentials.not_enabled') }, status: :unauthorized
end
end
protected
def find_user
if session[:attempt_user_id]
User.find(session[:attempt_user_id])
User.find_by(id: session[:attempt_user_id])
else
user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication
user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication
user ||= User.find_for_authentication(email: user_params[:email])
user
end
end
def user_params
params.require(:user).permit(:email, :password, :otp_attempt, :sign_in_token_attempt)
params.require(:user).permit(:email, :password, :otp_attempt, :sign_in_token_attempt, credential: {})
end
def after_sign_in_path_for(resource)
@ -75,6 +94,7 @@ class Auth::SessionsController < Devise::SessionsController
def require_no_authentication
super
# Delete flash message that isn't entirely useful and may be confusing in
# most cases because /web doesn't display/clear flash messages.
flash.delete(:alert) if flash[:alert] == I18n.t('devise.failure.already_authenticated')
@ -96,13 +116,30 @@ class Auth::SessionsController < Devise::SessionsController
def home_paths(resource)
paths = [about_path]
if single_user_mode? && resource.is_a?(User)
paths << short_account_path(username: resource.account)
end
paths
end
def continue_after?
truthy_param?(:continue)
end
def restart_session
clear_attempt_from_session
redirect_to new_user_session_path, alert: I18n.t('devise.failure.timeout')
end
def set_attempt_session(user)
session[:attempt_user_id] = user.id
session[:attempt_user_updated_at] = user.updated_at.to_s
end
def clear_attempt_from_session
session.delete(:attempt_user_id)
session.delete(:attempt_user_updated_at)
end
end

View file

@ -29,6 +29,24 @@ module AccountOwnedConcern
end
def check_account_suspension
expires_in(3.minutes, public: true) && gone if @account.suspended?
if @account.suspended_permanently?
permanent_suspension_response
elsif @account.suspended? && !skip_temporary_suspension_response?
temporary_suspension_response
end
end
def skip_temporary_suspension_response?
false
end
def permanent_suspension_response
expires_in(3.minutes, public: true)
gone
end
def temporary_suspension_response
expires_in(3.minutes, public: true)
forbidden
end
end

View file

@ -31,20 +31,26 @@ module CacheConcern
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)
raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
return [] if raw.empty?
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 = klass.where(id: uncached_ids).with_includes.index_by(&:id)
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
raw.filter_map { |item| cached_keys_with_value[item.id] || uncached[item.id] }
end
def cache_collection_paginated_by_id(raw, klass, limit, options)
cache_collection raw.cache_ids.to_a_paginated_by_id(limit, options), klass
end
end

View file

@ -32,7 +32,6 @@ module ChallengableConcern
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

View file

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

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
module RegistrationSpamConcern
extend ActiveSupport::Concern
def set_registration_form_time
session[:registration_form_time] = Time.now.utc
end
end

View file

@ -18,7 +18,9 @@ module SignInTokenAuthenticationConcern
def authenticate_with_sign_in_token
user = self.resource = find_user
if user_params[:sign_in_token_attempt].present? && session[:attempt_user_id]
if user.present? && session[:attempt_user_id].present? && session[:attempt_user_updated_at] != user.updated_at.to_s
restart_session
elsif user_params.key?(:sign_in_token_attempt) && session[:attempt_user_id]
authenticate_with_sign_in_token_attempt(user)
elsif user.present? && user.external_or_valid_password?(user_params[:password])
prompt_for_sign_in_token(user)
@ -27,7 +29,7 @@ module SignInTokenAuthenticationConcern
def authenticate_with_sign_in_token_attempt(user)
if valid_sign_in_token_attempt?(user)
session.delete(:attempt_user_id)
clear_attempt_from_session
remember_me(user)
sign_in(user)
else
@ -42,11 +44,11 @@ module SignInTokenAuthenticationConcern
UserMailer.sign_in_token(user, request.remote_ip, request.user_agent, Time.now.utc.to_s).deliver_later!
end
set_locale do
session[:attempt_user_id] = user.id
use_pack 'auth'
@body_classes = 'lighter'
render :sign_in_token
end
set_attempt_session(user)
use_pack 'auth'
@body_classes = 'lighter'
set_locale { render :sign_in_token }
end
end

View file

@ -7,6 +7,44 @@ module SignatureVerification
include DomainControlHelper
EXPIRATION_WINDOW_LIMIT = 12.hours
CLOCK_SKEW_MARGIN = 1.hour
class SignatureVerificationError < StandardError; end
class SignatureParamsParser < Parslet::Parser
rule(:token) { match("[0-9a-zA-Z!#$%&'*+.^_`|~-]").repeat(1).as(:token) }
rule(:quoted_string) { str('"') >> (qdtext | quoted_pair).repeat.as(:quoted_string) >> str('"') }
# qdtext and quoted_pair are not exactly according to spec but meh
rule(:qdtext) { match('[^\\\\"]') }
rule(:quoted_pair) { str('\\') >> any }
rule(:bws) { match('\s').repeat }
rule(:param) { (token.as(:key) >> bws >> str('=') >> bws >> (token | quoted_string).as(:value)).as(:param) }
rule(:comma) { bws >> str(',') >> bws }
# Old versions of node-http-signature add an incorrect "Signature " prefix to the header
rule(:buggy_prefix) { str('Signature ') }
rule(:params) { buggy_prefix.maybe >> (param >> (comma >> param).repeat).as(:params) }
root(:params)
end
class SignatureParamsTransformer < Parslet::Transform
rule(params: subtree(:p)) do
(p.is_a?(Array) ? p : [p]).each_with_object({}) { |(key, val), h| h[key] = val }
end
rule(param: { key: simple(:key), value: simple(:val) }) do
[key, val]
end
rule(quoted_string: simple(:string)) do
string.to_s
end
rule(token: simple(:string)) do
string.to_s
end
end
def require_signature!
render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
end
@ -24,72 +62,41 @@ module SignatureVerification
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']
rescue SignatureVerificationError
nil
end
def signed_request_account
return @signed_request_account if defined?(@signed_request_account)
unless signed_request?
@signature_verification_failure_reason = 'Request not signed'
@signed_request_account = nil
return
end
raise SignatureVerificationError, 'Request not signed' unless signed_request?
raise SignatureVerificationError, 'Incompatible request signature. keyId and signature are required' if missing_required_signature_parameters?
raise SignatureVerificationError, 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)' unless %w(rsa-sha256 hs2019).include?(signature_algorithm)
raise SignatureVerificationError, 'Signed request date outside acceptable time window' unless matches_time_window?
if request.headers['Date'].present? && !matches_time_window?
@signature_verification_failure_reason = 'Signed request date outside acceptable time window'
@signed_request_account = nil
return
end
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
if incompatible_signature?(signature_params)
@signature_verification_failure_reason = 'Incompatible request signature'
@signed_request_account = nil
return
end
verify_signature_strength!
verify_body_digest!
account = account_from_key_id(signature_params['keyId'])
if account.nil?
@signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
@signed_request_account = nil
return
end
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if account.nil?
signature = Base64.decode64(signature_params['signature'])
compare_signed_string = build_signed_string(signature_params['headers'])
compare_signed_string = build_signed_string
return account unless verify_signature(account, signature, compare_signed_string).nil?
account = stoplight_wrap_request { account.possibly_stale? ? account.refresh! : account_refresh_key(account) }
if account.nil?
@signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
@signed_request_account = nil
return
end
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if account.nil?
return account unless verify_signature(account, signature, compare_signed_string).nil?
@signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri}"
@signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)"
@signed_request_account = nil
rescue SignatureVerificationError => e
@signature_verification_failure_reason = e.message
@signed_request_account = nil
end
@ -99,8 +106,43 @@ module SignatureVerification
private
def signature_params
@signature_params ||= begin
raw_signature = request.headers['Signature']
tree = SignatureParamsParser.new.parse(raw_signature)
SignatureParamsTransformer.new.apply(tree)
end
rescue Parslet::ParseFailed
raise SignatureVerificationError, 'Error parsing signature parameters'
end
def signature_algorithm
signature_params.fetch('algorithm', 'hs2019')
end
def signed_headers
signature_params.fetch('headers', signature_algorithm == 'hs2019' ? '(created)' : 'date').downcase.split(' ')
end
def verify_signature_strength!
raise SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)')
raise SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(Request::REQUEST_TARGET) || signed_headers.include?('digest')
raise SignatureVerificationError, 'Mastodon requires the Host header to be signed when doing a GET request' if request.get? && !signed_headers.include?('host')
raise SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if request.post? && !signed_headers.include?('digest')
end
def verify_body_digest!
return unless signed_headers.include?('digest')
raise SignatureVerificationError, 'Digest header missing' unless request.headers.key?('Digest')
digests = request.headers['Digest'].split(',').map { |digest| digest.split('=', 2) }.map { |key, value| [key.downcase, value] }
sha256 = digests.assoc('sha-256')
raise SignatureVerificationError, "Mastodon only supports SHA-256 in Digest header. Offered algorithms: #{digests.map(&:first).join(', ')}" if sha256.nil?
raise SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}" if body_digest != sha256[1]
end
def verify_signature(account, signature, compare_signed_string)
if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
if account.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), signature, compare_signed_string)
@signed_request_account = account
@signed_request_account
end
@ -108,14 +150,20 @@ module SignatureVerification
nil
end
def build_signed_string(signed_headers)
signed_headers = 'date' if signed_headers.blank?
signed_headers.downcase.split(' ').map do |signed_header|
def build_signed_string
signed_headers.map do |signed_header|
if signed_header == Request::REQUEST_TARGET
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
elsif signed_header == 'digest'
"digest: #{body_digest}"
elsif signed_header == '(created)'
raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
raise SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank?
"(created): #{signature_params['created']}"
elsif signed_header == '(expires)'
raise SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019'
raise SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank?
"(expires): #{signature_params['expires']}"
else
"#{signed_header}: #{request.headers[to_header_name(signed_header)]}"
end
@ -123,26 +171,40 @@ module SignatureVerification
end
def matches_time_window?
created_time = nil
expires_time = nil
begin
time_sent = Time.httpdate(request.headers['Date'])
if signature_algorithm == 'hs2019' && signature_params['created'].present?
created_time = Time.at(signature_params['created'].to_i).utc
elsif request.headers['Date'].present?
created_time = Time.httpdate(request.headers['Date']).utc
end
expires_time = Time.at(signature_params['expires'].to_i).utc if signature_params['expires'].present?
rescue ArgumentError
return false
end
(Time.now.utc - time_sent).abs <= 12.hours
expires_time ||= created_time + 5.minutes unless created_time.nil?
expires_time = [expires_time, created_time + EXPIRATION_WINDOW_LIMIT].min unless created_time.nil?
return false if created_time.present? && created_time > Time.now.utc + CLOCK_SKEW_MARGIN
return false if expires_time.present? && Time.now.utc > expires_time + CLOCK_SKEW_MARGIN
true
end
def body_digest
"SHA-256=#{Digest::SHA256.base64digest(request_body)}"
@body_digest ||= Digest::SHA256.base64digest(request_body)
end
def to_header_name(name)
name.split(/-/).map(&:capitalize).join('-')
end
def incompatible_signature?(signature_params)
signature_params['keyId'].blank? ||
signature_params['signature'].blank?
def missing_required_signature_parameters?
signature_params['keyId'].blank? || signature_params['signature'].blank?
end
def account_from_key_id(key_id)

View file

@ -8,7 +8,23 @@ module TwoFactorAuthenticationConcern
end
def two_factor_enabled?
find_user&.otp_required_for_login?
find_user&.two_factor_enabled?
end
def valid_webauthn_credential?(user, webauthn_credential)
user_credential = user.webauthn_credentials.find_by!(external_id: webauthn_credential.id)
begin
webauthn_credential.verify(
session[:webauthn_challenge],
public_key: user_credential.public_key,
sign_count: user_credential.sign_count
)
user_credential.update!(sign_count: webauthn_credential.sign_count)
rescue WebAuthn::Error
false
end
end
def valid_otp_attempt?(user)
@ -21,16 +37,33 @@ module TwoFactorAuthenticationConcern
def authenticate_with_two_factor
user = self.resource = find_user
if user_params[:otp_attempt].present? && session[:attempt_user_id]
authenticate_with_two_factor_attempt(user)
if user.present? && session[:attempt_user_id].present? && session[:attempt_user_updated_at] != user.updated_at.to_s
restart_session
elsif user.webauthn_enabled? && user_params.key?(:credential) && session[:attempt_user_id]
authenticate_with_two_factor_via_webauthn(user)
elsif user_params.key?(:otp_attempt) && session[:attempt_user_id]
authenticate_with_two_factor_via_otp(user)
elsif user.present? && user.external_or_valid_password?(user_params[:password])
prompt_for_two_factor(user)
end
end
def authenticate_with_two_factor_attempt(user)
def authenticate_with_two_factor_via_webauthn(user)
webauthn_credential = WebAuthn::Credential.from_get(user_params[:credential])
if valid_webauthn_credential?(user, webauthn_credential)
clear_attempt_from_session
remember_me(user)
sign_in(user)
render json: { redirect_path: root_path }, status: :ok
else
render json: { error: t('webauthn_credentials.invalid_credential') }, status: :unprocessable_entity
end
end
def authenticate_with_two_factor_via_otp(user)
if valid_otp_attempt?(user)
session.delete(:attempt_user_id)
clear_attempt_from_session
remember_me(user)
sign_in(user)
else
@ -40,11 +73,20 @@ module TwoFactorAuthenticationConcern
end
def prompt_for_two_factor(user)
set_locale do
session[:attempt_user_id] = user.id
use_pack 'auth'
@body_classes = 'lighter'
render :two_factor
set_attempt_session(user)
use_pack 'auth'
@body_classes = 'lighter'
@webauthn_enabled = user.webauthn_enabled?
@scheme_type = begin
if user.webauthn_enabled? && user_params[:otp_attempt].blank?
'webauthn'
else
'totp'
end
end
set_locale { render :two_factor }
end
end

View file

@ -6,14 +6,13 @@ module UserTrackingConcern
UPDATE_SIGN_IN_HOURS = 24
included do
before_action :set_user_activity
before_action :update_user_sign_in
end
private
def set_user_activity
return unless user_needs_sign_in_update?
current_user.update_tracked_fields!(request)
def update_user_sign_in
current_user.update_sign_in!(request) if user_needs_sign_in_update?
end
def user_needs_sign_in_update?

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