diff --git a/.buildpacks b/.buildpacks
index 3450683ce84ce6..5e73304a5d7c5b 100644
--- a/.buildpacks
+++ b/.buildpacks
@@ -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
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 9f43a057356bbb..862fa126b7f084 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -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)
diff --git a/.dependabot/config.yml b/.dependabot/config.yml
deleted file mode 100644
index 06df775c2c7f22..00000000000000
--- a/.dependabot/config.yml
+++ /dev/null
@@ -1,28 +0,0 @@
-version: 1
-
-update_configs:
- - package_manager: "ruby:bundler"
- directory: "/"
- update_schedule: "weekly"
- # Supported update schedule: live daily weekly monthly
- version_requirement_updates: "auto"
- # Supported version requirements: auto widen_ranges increase_versions increase_versions_if_necessary
- allowed_updates:
- - match:
- dependency_type: "all"
- # Supported dependency types: all indirect direct production development
- update_type: "all"
- # Supported update types: all security
-
- - package_manager: "javascript"
- directory: "/"
- update_schedule: "weekly"
- # Supported update schedule: live daily weekly monthly
- version_requirement_updates: "auto"
- # Supported version requirements: auto widen_ranges increase_versions increase_versions_if_necessary
- allowed_updates:
- - match:
- dependency_type: "all"
- # Supported dependency types: all indirect direct production development
- update_type: "all"
- # Supported update types: all security
diff --git a/.env.production.sample b/.env.production.sample
index 5762b24edfa1df..6f14c5804777ca 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -1,264 +1,60 @@
-# 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)
+# Redis
+# -----
+REDIS_HOST=localhost
+REDIS_PORT=6379
-# 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.
-# WEB_DOMAIN=mastodon.example.com
+# PostgreSQL
+# ----------
+DB_HOST=/var/run/postgresql
+DB_USER=mastodon
+DB_NAME=mastodon_production
+DB_PASS=
+DB_PORT=5432
-# Use this if you want to have several aliases handler@example1.com
-# handler@example2.com etc. for the same user. LOCAL_DOMAIN should not
-# be added. Comma separated values
-# ALTERNATE_DOMAINS=example1.com,example2.com
+# ElasticSearch (optional)
+# ------------------------
+ES_ENABLED=true
+ES_HOST=localhost
+ES_PORT=9200
-# Application 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)
+# Secrets
+# -------
+# Make sure to use `rake secret` to generate secrets
+# -------
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)
-# 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
+# Web Push
+# --------
+# Generate with `rake mastodon:webpush:generate_vapid_key`
+# --------
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
-
-# 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
-
-# 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
-
-# S3 (optional)
-# The attachment host must allow cross origin request from WEB_DOMAIN or
-# LOCAL_DOMAIN if WEB_DOMAIN is not set. For example, the server may have the
-# following header field:
-# Access-Control-Allow-Origin: https://192.168.1.123:9000/
-# S3_ENABLED=true
-# S3_BUCKET=
-# AWS_ACCESS_KEY_ID=
-# 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
-
-# Swift (optional)
-# The attachment host must allow cross origin request - see the description
-# above.
-# SWIFT_ENABLED=true
-# SWIFT_USERNAME=
-# For Keystone V3, the value for SWIFT_TENANT should be the project name
-# SWIFT_TENANT=
-# SWIFT_PASSWORD=
-# Some OpenStack V3 providers require PROJECT_ID (optional)
-# SWIFT_PROJECT_ID=
-# Keystone V2 and V3 URLs are supported. Use a V3 URL if possible to avoid
-# issues with token rate-limiting during high load.
-# SWIFT_AUTH_URL=
-# SWIFT_CONTAINER=
-# SWIFT_OBJECT_URL=
-# SWIFT_REGION=
-# Defaults to 'default'
-# SWIFT_DOMAIN_NAME=
-# Defaults to 60 seconds. Set to 0 to disable
-# SWIFT_CACHE_TTL=
-
-# 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
-
-# LDAP authentication (optional)
-# LDAP_ENABLED=true
-# LDAP_HOST=localhost
-# LDAP_PORT=389
-# LDAP_METHOD=simple_tls
-# LDAP_BASE=
-# LDAP_BIND_DN=
-# LDAP_PASSWORD=
-# LDAP_UID=cn
-# LDAP_MAIL=mail
-# LDAP_SEARCH_FILTER=(|(%{uid}=%{email})(%{mail}=%{email}))
-# LDAP_UID_CONVERSION_ENABLED=true
-# LDAP_UID_CONVERSION_SEARCH=., -
-# LDAP_UID_CONVERSION_REPLACE=_
-
-# PAM authentication (optional)
-# PAM authentication uses for the email generation the "email" pam variable
-# and optional as fallback PAM_DEFAULT_SUFFIX
-# The pam environment variable "email" is provided by:
-# https://github.com/devkral/pam_email_extractor
-# PAM_ENABLED=true
-# Fallback email domain for email address generation (LOCAL_DOMAIN by default)
-# PAM_EMAIL_DOMAIN=example.com
-# Name of the pam service (pam "auth" section is evaluated)
-# PAM_DEFAULT_SERVICE=rpam
-# Name of the pam service used for checking if an user can register (pam "account" section is evaluated) (nil (disabled) by default)
-# PAM_CONTROLLED_SERVICE=rpam
-
-# Global OAuth settings (optional) :
-# If you have only one strategy, you may want to enable this
-# OAUTH_REDIRECT_AT_SIGN_IN=true
-
-# Optional CAS authentication (cf. omniauth-cas) :
-# CAS_ENABLED=true
-# CAS_URL=https://sso.myserver.com/
-# CAS_HOST=sso.myserver.com/
-# CAS_PORT=443
-# CAS_SSL=true
-# CAS_VALIDATE_URL=
-# CAS_CALLBACK_URL=
-# CAS_LOGOUT_URL=
-# CAS_LOGIN_URL=
-# CAS_UID_FIELD='user'
-# CAS_CA_PATH=
-# CAS_DISABLE_SSL_VERIFICATION=false
-# CAS_UID_KEY='user'
-# CAS_NAME_KEY='name'
-# CAS_EMAIL_KEY='email'
-# CAS_NICKNAME_KEY='nickname'
-# CAS_FIRST_NAME_KEY='firstname'
-# CAS_LAST_NAME_KEY='lastname'
-# CAS_LOCATION_KEY='location'
-# CAS_IMAGE_KEY='image'
-# CAS_PHONE_KEY='phone'
-
-# Optional SAML authentication (cf. omniauth-saml)
-# SAML_ENABLED=true
-# SAML_ACS_URL=http://localhost:3000/auth/auth/saml/callback
-# SAML_ISSUER=https://example.com
-# SAML_IDP_SSO_TARGET_URL=https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO
-# SAML_IDP_CERT=
-# SAML_IDP_CERT_FINGERPRINT=
-# SAML_NAME_IDENTIFIER_FORMAT=
-# SAML_CERT=
-# SAML_PRIVATE_KEY=
-# SAML_SECURITY_WANT_ASSERTION_SIGNED=true
-# SAML_SECURITY_WANT_ASSERTION_ENCRYPTED=true
-# SAML_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true
-# SAML_ATTRIBUTES_STATEMENTS_UID="urn:oid:0.9.2342.19200300.100.1.1"
-# SAML_ATTRIBUTES_STATEMENTS_EMAIL="urn:oid:1.3.6.1.4.1.5923.1.1.1.6"
-# SAML_ATTRIBUTES_STATEMENTS_FULL_NAME="urn:oid:2.16.840.1.113730.3.1.241"
-# SAML_ATTRIBUTES_STATEMENTS_FIRST_NAME="urn:oid:2.5.4.42"
-# SAML_ATTRIBUTES_STATEMENTS_LAST_NAME="urn:oid:2.5.4.4"
-# SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1"
-# SAML_ATTRIBUTES_STATEMENTS_VERIFIED=
-# SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL=
-
-# Use HTTP proxy for outgoing request (optional)
-# http_proxy=http://gateway.local:8118
-# Access control for hidden service.
-# ALLOW_ACCESS_TO_HIDDEN_SERVICE=true
-
-GITHUB_REPOSITORY=koba-lab/mastodon
-
-# Authorized fetch mode (optional)
-# Require remote servers to authentify when fetching toots, see
-# https://docs.joinmastodon.org/admin/config/#authorized_fetch
-# AUTHORIZED_FETCH=true
-
-# Whitelist mode (optional)
-# Only allow federation with whitelisted domains, see
-# https://docs.joinmastodon.org/admin/config/#whitelist_mode
-# WHITELIST_MODE=true
+SMTP_FROM_ADDRESS=notificatons@example.com
+
+# File storage (optional)
+# -----------------------
+S3_ENABLED=true
+S3_BUCKET=files.example.com
+AWS_ACCESS_KEY_ID=
+AWS_SECRET_ACCESS_KEY=
+S3_ALIAS_HOST=files.example.com
diff --git a/.eslintrc.js b/.eslintrc.js
index 177496d3a3d593..7dda01108218e5 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -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,
+ },
+ ],
},
};
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 91ee92a2e56f2d..9526e17db73554 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,2 +1,3 @@
patreon: mastodon
open_collective: mastodon
+github: [Gargron]
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000000000..6b47350a4dd29e
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,22 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: npm
+ directory: "/"
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 99
+ allow:
+ - dependency-type: all
+
+ - package-ecosystem: bundler
+ directory: "/"
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 99
+ allow:
+ - dependency-type: all
diff --git a/.gitignore b/.gitignore
index 87689785500f06..e97883d100f04d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,31 +17,36 @@
/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 lockfile, dependency charts, and local values file
+/chart/Chart.lock
+/chart/charts/*.tgz
+/chart/values.yaml
# Ignore Apple files
.DS_Store
diff --git a/.rubocop.yml b/.rubocop.yml
index 3a11f700092a0d..25e0fa940b719f 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -28,6 +28,10 @@ Layout/EmptyLineAfterMagicComment:
Layout/SpaceInsideHashLiteralBraces:
EnforcedStyle: space
+Lint/UselessAccessModifier:
+ ContextCreatingMethods:
+ - class_methods
+
Metrics/AbcSize:
Max: 100
diff --git a/AUTHORS.md b/AUTHORS.md
index 5f5985fba8328c..12d0736bdefda7 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -5,62 +5,66 @@ Mastodon is available on [GitHub](https://github.com/tootsuite/mastodon)
and provided thanks to the work of the following contributors:
* [Gargron](https://github.com/Gargron)
+* [dependabot-preview[bot]](https://github.com/apps/dependabot-preview)
* [ThibG](https://github.com/ThibG)
* [ykzts](https://github.com/ykzts)
* [dependabot[bot]](https://github.com/apps/dependabot)
* [akihikodaki](https://github.com/akihikodaki)
-* [dependabot-preview[bot]](https://github.com/apps/dependabot-preview)
* [mjankowski](https://github.com/mjankowski)
* [unarist](https://github.com/unarist)
* [yiskah](https://github.com/yiskah)
* [nolanlawson](https://github.com/nolanlawson)
-* [ysksn](https://github.com/ysksn)
* [abcang](https://github.com/abcang)
+* [ysksn](https://github.com/ysksn)
+* [mayaeh](https://github.com/mayaeh)
* [sorin-davidoi](https://github.com/sorin-davidoi)
* [lynlynlynx](https://github.com/lynlynlynx)
-* [mayaeh](https://github.com/mayaeh)
* [m4sk1n](mailto:me@m4sk.in)
* [Marcin Mikołajczak](mailto:me@m4sk.in)
* [Kjwon15](https://github.com/Kjwon15)
+* [noellabo](https://github.com/noellabo)
* [renatolond](https://github.com/renatolond)
* [alpaca-tc](https://github.com/alpaca-tc)
* [jeroenpraat](https://github.com/jeroenpraat)
* [nclm](https://github.com/nclm)
* [ineffyble](https://github.com/ineffyble)
-* [mabkenar](https://github.com/mabkenar)
+* [shleeable](https://github.com/shleeable)
+* [zunda](https://github.com/zunda)
+* [Masoud Abkenar](mailto:ampbox@gmail.com)
* [blackle](https://github.com/blackle)
* [Quent-in](https://github.com/Quent-in)
* [JantsoP](https://github.com/JantsoP)
-* [zunda](https://github.com/zunda)
* [nullkal](https://github.com/nullkal)
* [yookoala](https://github.com/yookoala)
+* [Sasha-Sorokin](https://github.com/Sasha-Sorokin)
* [Aditoo17](https://github.com/Aditoo17)
* [Quenty31](https://github.com/Quenty31)
* [marek-lach](https://github.com/marek-lach)
* [shuheiktgw](https://github.com/shuheiktgw)
* [ashfurrow](https://github.com/ashfurrow)
+* [danhunsaker](https://github.com/danhunsaker)
* [eramdam](https://github.com/eramdam)
-* [noellabo](https://github.com/noellabo)
* [takayamaki](https://github.com/takayamaki)
-* [danhunsaker](https://github.com/danhunsaker)
+* [ariasuni](https://github.com/ariasuni)
* [masarakki](https://github.com/masarakki)
* [ticky](https://github.com/ticky)
* [ThisIsMissEm](https://github.com/ThisIsMissEm)
+* [hinaloe](https://github.com/hinaloe)
* [hcmiya](https://github.com/hcmiya)
* [stephenburgess8](https://github.com/stephenburgess8)
-* [Wonderfall](https://github.com/Wonderfall)
+* [Wonderfall](mailto:wonderfall@targaryen.house)
* [matteoaquila](https://github.com/matteoaquila)
* [yukimochi](https://github.com/yukimochi)
* [palindromordnilap](https://github.com/palindromordnilap)
* [rkarabut](https://github.com/rkarabut)
-* [Artoria2e5](https://github.com/Artoria2e5)
+* [trwnh](https://github.com/trwnh)
* [nightpool](https://github.com/nightpool)
+* [Artoria2e5](https://github.com/Artoria2e5)
* [marrus-sh](https://github.com/marrus-sh)
-* [hinaloe](https://github.com/hinaloe)
* [krainboltgreene](https://github.com/krainboltgreene)
* [pfigel](https://github.com/pfigel)
-* [Aldarone](https://github.com/Aldarone)
* [BoFFire](https://github.com/BoFFire)
+* [Aldarone](https://github.com/Aldarone)
* [clworld](https://github.com/clworld)
* [MasterGroosha](https://github.com/MasterGroosha)
* [dracos](https://github.com/dracos)
@@ -68,52 +72,50 @@ and provided thanks to the work of the following contributors:
* [SerCom_KC](mailto:sercom-kc@users.noreply.github.com)
* [Sylvhem](https://github.com/Sylvhem)
* [MitarashiDango](https://github.com/MitarashiDango)
+* [angristan](https://github.com/angristan)
* [JeanGauthier](https://github.com/JeanGauthier)
* [kschaper](https://github.com/kschaper)
* [beatrix-bitrot](https://github.com/beatrix-bitrot)
-* [angristan](https://github.com/angristan)
+* [koyuawsmbrtn](https://github.com/koyuawsmbrtn)
+* [BenLubar](https://github.com/BenLubar)
* [adbelle](https://github.com/adbelle)
* [evanminto](https://github.com/evanminto)
* [MightyPork](https://github.com/MightyPork)
-* [ashleyhull-versent](mailto:ashley.hull@versent.com.au)
+* [ashleyhull-versent](https://github.com/ashleyhull-versent)
* [yhirano55](https://github.com/yhirano55)
* [rinsuki](https://github.com/rinsuki)
+* [dunn](https://github.com/dunn)
+* [devkral](https://github.com/devkral)
* [camponez](https://github.com/camponez)
+* [hugogameiro](https://github.com/hugogameiro)
* [SerCom_KC](mailto:szescxz@gmail.com)
* [aschmitz](https://github.com/aschmitz)
-* [trwnh](https://github.com/trwnh)
-* [devkral](https://github.com/devkral)
* [fpiesche](https://github.com/fpiesche)
-* [hugogameiro](https://github.com/hugogameiro)
* [gandaro](https://github.com/gandaro)
* [johnsudaar](https://github.com/johnsudaar)
-* [ariasuni](https://github.com/ariasuni)
* [trebmuh](https://github.com/trebmuh)
* [rmhasan](https://github.com/rmhasan)
* [kedamaDQ](https://github.com/kedamaDQ)
* [lindwurm](https://github.com/lindwurm)
* [victorhck](mailto:victorhck@geeko.site)
* [voidsatisfaction](https://github.com/voidsatisfaction)
-* [BenLubar](https://github.com/BenLubar)
* [hikari-no-yume](https://github.com/hikari-no-yume)
* [seefood](https://github.com/seefood)
* [jackjennings](https://github.com/jackjennings)
-* [koyuawsmbrtn](https://github.com/koyuawsmbrtn)
+* [mfmfuyu](https://github.com/mfmfuyu)
+* [puckipedia](https://github.com/puckipedia)
* [spla](mailto:spla@mastodont.cat)
-* [expenses](https://github.com/expenses)
* [walf443](https://github.com/walf443)
* [JoelQ](https://github.com/JoelQ)
* [mistydemeo](https://github.com/mistydemeo)
-* [dunn](https://github.com/dunn)
+* [Ashley](mailto:expenses@airmail.cc)
* [xqus](https://github.com/xqus)
* [pfm-eyesightjp](https://github.com/pfm-eyesightjp)
-* [fakenine](https://github.com/fakenine)
-* [Shleeble](https://github.com/Shleeble)
+* [Samy KACIMI](mailto:samy.kacimi@gmail.com)
* [tsuwatch](https://github.com/tsuwatch)
* [victorhck](https://github.com/victorhck)
* [mkljczk](https://github.com/mkljczk)
* [manuelviens](https://github.com/manuelviens)
-* [puckipedia](https://github.com/puckipedia)
* [fvh-P](https://github.com/fvh-P)
* [rtucker](https://github.com/rtucker)
* [Anna e só](mailto:contraexemplos@gmail.com)
@@ -123,6 +125,7 @@ and provided thanks to the work of the following contributors:
* [diomed](https://github.com/diomed)
* [Neetshin](mailto:neetshin@neetsh.in)
* [rainyday](https://github.com/rainyday)
+* [tcitworld](https://github.com/tcitworld)
* [ProgVal](https://github.com/ProgVal)
* [valentin2105](https://github.com/valentin2105)
* [yuntan](https://github.com/yuntan)
@@ -136,44 +139,53 @@ and provided thanks to the work of the following contributors:
* [TheKinrar](https://github.com/TheKinrar)
* [AA4ch1](https://github.com/AA4ch1)
* [alexgleason](https://github.com/alexgleason)
+* [Bèr Kessels](mailto:ber@berk.es)
* [cpytel](https://github.com/cpytel)
* [northerner](https://github.com/northerner)
* [fhemberger](https://github.com/fhemberger)
+* [Gomasy](https://github.com/Gomasy)
* [greysteil](https://github.com/greysteil)
* [hencatsmith](https://github.com/hencatsmith)
* [d6rkaiz](https://github.com/d6rkaiz)
* [Reverite](https://github.com/Reverite)
* [JohnD28](https://github.com/JohnD28)
* [znz](https://github.com/znz)
+* [saper](https://github.com/saper)
* [Naouak](https://github.com/Naouak)
* [pawelngei](https://github.com/pawelngei)
* [reneklacan](https://github.com/reneklacan)
* [ekiru](https://github.com/ekiru)
-* [tcitworld](https://github.com/tcitworld)
* [geta6](https://github.com/geta6)
* [happycoloredbanana](https://github.com/happycoloredbanana)
* [leopku](https://github.com/leopku)
* [SansPseudoFix](https://github.com/SansPseudoFix)
-* [salvadorpla](https://github.com/salvadorpla)
+* [spla](mailto:sp@mastodont.cat)
+* [tateisu](https://github.com/tateisu)
* [tomfhowe](https://github.com/tomfhowe)
* [noraworld](https://github.com/noraworld)
+* [lfuelling](https://github.com/lfuelling)
* [theboss](https://github.com/theboss)
* [nzws](https://github.com/nzws)
+* [duxovni](https://github.com/duxovni)
+* [smorimoto](https://github.com/smorimoto)
* [178inaba](https://github.com/178inaba)
+* [acid-chicken](https://github.com/acid-chicken)
* [xgess](https://github.com/xgess)
* [alyssais](https://github.com/alyssais)
* [aablinov](https://github.com/aablinov)
* [stalker314314](https://github.com/stalker314314)
* [cutls](https://github.com/cutls)
+* [dariusk](https://github.com/dariusk)
* [huertanix](https://github.com/huertanix)
-* [genesixx](https://github.com/genesixx)
+* [eleboucher](https://github.com/eleboucher)
* [halkeye](https://github.com/halkeye)
+* [Hanage999](https://github.com/Hanage999)
* [treby](https://github.com/treby)
* [jpdevries](https://github.com/jpdevries)
* [gdpelican](https://github.com/gdpelican)
* [kmichl](https://github.com/kmichl)
* [Kurtis Rainbolt-Greene](mailto:me@kurtisrainboltgreene.name)
-* [saper](https://github.com/saper)
+* [panarom](https://github.com/panarom)
* [Dar13](https://github.com/Dar13)
* [nevillepark](https://github.com/nevillepark)
* [ornithocoder](https://github.com/ornithocoder)
@@ -181,7 +193,7 @@ and provided thanks to the work of the following contributors:
* [pierreozoux](https://github.com/pierreozoux)
* [qguv](https://github.com/qguv)
* [Ram Lmn](mailto:ramlmn@users.noreply.github.com)
-* [aurelia-sl](https://github.com/aurelia-sl)
+* [Sascha](mailto:sascha@serenitylabs.cloud)
* [harukasan](https://github.com/harukasan)
* [stamak](https://github.com/stamak)
* [Technowix](https://github.com/Technowix)
@@ -196,9 +208,9 @@ and provided thanks to the work of the following contributors:
* [chr-1x](https://github.com/chr-1x)
* [esetomo](https://github.com/esetomo)
* [foxiehkins](https://github.com/foxiehkins)
+* [highemerly](https://github.com/highemerly)
* [hoodie](mailto:hoodiekitten@outlook.com)
* [luzi82](https://github.com/luzi82)
-* [duxovni](https://github.com/duxovni)
* [slice](https://github.com/slice)
* [tmm576](https://github.com/tmm576)
* [unsmell](mailto:unsmell@users.noreply.github.com)
@@ -209,13 +221,12 @@ and provided thanks to the work of the following contributors:
* [AndreLewin](https://github.com/AndreLewin)
* [0xflotus](https://github.com/0xflotus)
* [redtachyons](https://github.com/redtachyons)
-* [acid-chicken](https://github.com/acid-chicken)
* [thurloat](https://github.com/thurloat)
* [aaribaud](https://github.com/aaribaud)
* [pointlessone](https://github.com/pointlessone)
* [Andrew](mailto:andrewlchronister@gmail.com)
* [aurelien-reeves](https://github.com/aurelien-reeves)
-* [AnaGelez](https://github.com/AnaGelez)
+* [elegaanz](https://github.com/elegaanz)
* [estuans](https://github.com/estuans)
* [dissolve](https://github.com/dissolve)
* [PurpleBooth](https://github.com/PurpleBooth)
@@ -227,16 +238,14 @@ and provided thanks to the work of the following contributors:
* [muffinista](https://github.com/muffinista)
* [cdutson](https://github.com/cdutson)
* [farlistener](https://github.com/farlistener)
-* [dariusk](https://github.com/dariusk)
* [DavidLibeau](https://github.com/DavidLibeau)
+* [dmerejkowsky](https://github.com/dmerejkowsky)
* [ddevault](https://github.com/ddevault)
* [Fjoerfoks](https://github.com/Fjoerfoks)
* [fmauNeko](https://github.com/fmauNeko)
* [gloaec](https://github.com/gloaec)
-* [Gomasy](https://github.com/Gomasy)
* [unstabler](https://github.com/unstabler)
* [potato4d](https://github.com/potato4d)
-* [Hanage999](https://github.com/Hanage999)
* [h-izumi](https://github.com/h-izumi)
* [ErikXXon](https://github.com/ErikXXon)
* [ian-kelling](https://github.com/ian-kelling)
@@ -251,13 +260,17 @@ and provided thanks to the work of the following contributors:
* [tkbky](https://github.com/tkbky)
* [Kaylee](mailto:kaylee@codethat.sucks)
* [Kazhnuz](https://github.com/Kazhnuz)
+* [mkody](https://github.com/mkody)
* [connyduck](https://github.com/connyduck)
* [LindseyB](https://github.com/LindseyB)
* [Lorenz Diener](mailto:halcyon@icosahedron.website)
-* [alimony](https://github.com/alimony)
+* [Markus Amalthea Magnuson](mailto:markus.magnuson@gmail.com)
+* [madmath03](https://github.com/madmath03)
* [mig5](https://github.com/mig5)
* [moritzheiber](https://github.com/moritzheiber)
+* [Nathaniel Suchy](mailto:me@lunorian.is)
* [ndarville](https://github.com/ndarville)
+* [NimaBoscarino](https://github.com/NimaBoscarino)
* [Abzol](https://github.com/Abzol)
* [PatOnTheBack](https://github.com/PatOnTheBack)
* [xPaw](https://github.com/xPaw)
@@ -287,16 +300,15 @@ and provided thanks to the work of the following contributors:
* [amazedkoumei](https://github.com/amazedkoumei)
* [anon5r](https://github.com/anon5r)
* [aus-social](https://github.com/aus-social)
-* [imbsky](https://github.com/imbsky)
* [bsky](mailto:me@imbsky.net)
* [codl](https://github.com/codl)
* [cpsdqs](https://github.com/cpsdqs)
* [barzamin](https://github.com/barzamin)
* [fhalna](https://github.com/fhalna)
-* [highemerly](https://github.com/highemerly)
* [haoyayoi](https://github.com/haoyayoi)
* [ik11235](https://github.com/ik11235)
* [kawax](https://github.com/kawax)
+* [shrft](https://github.com/shrft)
* [007lva](https://github.com/007lva)
* [mbajur](https://github.com/mbajur)
* [matsurai25](https://github.com/matsurai25)
@@ -307,15 +319,18 @@ and provided thanks to the work of the following contributors:
* [pinfort](https://github.com/pinfort)
* [rbaumert](https://github.com/rbaumert)
* [rhoio](https://github.com/rhoio)
+* [sclaire-1](https://github.com/sclaire-1)
+* [umonaca](https://github.com/umonaca)
* [usagi-f](https://github.com/usagi-f)
* [vidarlee](https://github.com/vidarlee)
* [vjackson725](https://github.com/vjackson725)
* [wxcafe](https://github.com/wxcafe)
+* [Grawl](https://github.com/Grawl)
* [新都心(Neet Shin)](mailto:nucx@dio-vox.com)
* [clarfon](https://github.com/clarfon)
* [cygnan](https://github.com/cygnan)
* [Awea](https://github.com/Awea)
-* [halcy](https://github.com/halcy)
+* [eai04191](https://github.com/eai04191)
* [8398a7](https://github.com/8398a7)
* [857b](https://github.com/857b)
* [insom](https://github.com/insom)
@@ -332,6 +347,7 @@ and provided thanks to the work of the following contributors:
* [a2](https://github.com/a2)
* [alfiedotwtf](https://github.com/alfiedotwtf)
* [0xa](https://github.com/0xa)
+* [ArisuOngaku](https://github.com/ArisuOngaku)
* [virtualpain](https://github.com/virtualpain)
* [sapphirus](https://github.com/sapphirus)
* [amandavisconti](https://github.com/amandavisconti)
@@ -342,15 +358,22 @@ and provided thanks to the work of the following contributors:
* [schas002](https://github.com/schas002)
* [contraexemplo](https://github.com/contraexemplo)
* [abackstrom](https://github.com/abackstrom)
+* [arielrodrigues](https://github.com/arielrodrigues)
+* [orlea](https://github.com/orlea)
* [armandfardeau](https://github.com/armandfardeau)
* [raboof](https://github.com/raboof)
* [jumbosushi](https://github.com/jumbosushi)
* [ayumin](https://github.com/ayumin)
* [bzg](https://github.com/bzg)
-* [benediktg](https://github.com/benediktg)
+* [BastienDurel](https://github.com/BastienDurel)
+* [li-bei](https://github.com/li-bei)
+* [Benedikt Geißler](mailto:benedikt@g5r.eu)
+* [BenisonSebastian](https://github.com/BenisonSebastian)
* [blakebarnett](https://github.com/blakebarnett)
-* [bradj](https://github.com/bradj)
+* [Brad Janke](mailto:brad.janke@gmail.com)
+* [bclindner](https://github.com/bclindner)
* [brycied00d](https://github.com/brycied00d)
+* [berkes](https://github.com/berkes)
* [carlosjs23](https://github.com/carlosjs23)
* [cgxxx](https://github.com/cgxxx)
* [kibitan](https://github.com/kibitan)
@@ -358,41 +381,48 @@ and provided thanks to the work of the following contributors:
* [chris-martin](https://github.com/chris-martin)
* [DoubleMalt](https://github.com/DoubleMalt)
* [Moosh-be](https://github.com/Moosh-be)
+* [cchoi12](https://github.com/cchoi12)
* [Motoma](https://github.com/Motoma)
* [Christopher Kolstad](mailto:christopher.kolstad@finn.no)
* [csu](https://github.com/csu)
* [kklleemm](https://github.com/kklleemm)
* [colindean](https://github.com/colindean)
+* [DeeUnderscore](https://github.com/DeeUnderscore)
* [dachinat](https://github.com/dachinat)
-* [multiple-creatures](https://github.com/multiple-creatures)
+* [shapeshifter-system](https://github.com/shapeshifter-system)
* [watilde](https://github.com/watilde)
* [daprice](https://github.com/daprice)
* [da2x](https://github.com/da2x)
+* [codesections](https://github.com/codesections)
* [dar5hak](https://github.com/dar5hak)
* [kant](https://github.com/kant)
* [maxolasersquad](https://github.com/maxolasersquad)
* [singingwolfboy](https://github.com/singingwolfboy)
+* [caldwell](https://github.com/caldwell)
* [davidcelis](https://github.com/davidcelis)
+* [divergentdave](https://github.com/divergentdave)
* [davefp](https://github.com/davefp)
* [yipdw](https://github.com/yipdw)
* [debanshuk](https://github.com/debanshuk)
+* [mascali33](https://github.com/mascali33)
* [DerekNonGeneric](https://github.com/DerekNonGeneric)
* [dblandin](https://github.com/dblandin)
* [Drew Gates](mailto:aranaur@users.noreply.github.com)
* [dtschust](https://github.com/dtschust)
* [Dryusdan](https://github.com/Dryusdan)
-* [eai04191](https://github.com/eai04191)
* [d3vgru](https://github.com/d3vgru)
* [Elizafox](https://github.com/Elizafox)
* [enewhuis](https://github.com/enewhuis)
* [ericblade](https://github.com/ericblade)
* [mikoim](https://github.com/mikoim)
* [espenronnevik](https://github.com/espenronnevik)
+* [Expenses](mailto:expenses@airmail.cc)
* [fabianonline](https://github.com/fabianonline)
* [Finariel](https://github.com/Finariel)
* [siuying](https://github.com/siuying)
* [zoc](https://github.com/zoc)
* [fwenzel](https://github.com/fwenzel)
+* [gabrielrumiranda](https://github.com/gabrielrumiranda)
* [GenbuHase](https://github.com/GenbuHase)
* [nilsding](https://github.com/nilsding)
* [hattori6789](https://github.com/hattori6789)
@@ -401,6 +431,7 @@ and provided thanks to the work of the following contributors:
* [myfreeweb](https://github.com/myfreeweb)
* [gfaivre](https://github.com/gfaivre)
* [Fiaxhs](https://github.com/Fiaxhs)
+* [rasjonell](https://github.com/rasjonell)
* [reedcourty](https://github.com/reedcourty)
* [anneau](https://github.com/anneau)
* [lanodan](https://github.com/lanodan)
@@ -421,46 +452,49 @@ and provided thanks to the work of the following contributors:
* [jack-michaud](https://github.com/jack-michaud)
* [Floppy](https://github.com/Floppy)
* [loomchild](https://github.com/loomchild)
+* [jglauche](https://github.com/jglauche)
* [jenkr55](https://github.com/jenkr55)
* [hyenagirl64](https://github.com/hyenagirl64)
* [press5](https://github.com/press5)
* [TrollDecker](https://github.com/TrollDecker)
* [jmontane](https://github.com/jmontane)
-* [jonathanklee](https://github.com/jonathanklee)
-* [jguerder](https://github.com/jguerder)
-* [Jehops](https://github.com/Jehops)
-* [joshuap](https://github.com/joshuap)
-* [Tiwy57](https://github.com/Tiwy57)
-* [xuv](https://github.com/xuv)
-* [Jnsll](https://github.com/Jnsll)
-* [j0k3r](https://github.com/j0k3r)
-* [KEINOS](https://github.com/KEINOS)
-* [futoase](https://github.com/futoase)
-* [pot8to](https://github.com/pot8to)
+* [Jonathan Klee](mailto:klee.jonathan@gmail.com)
+* [Jordan Guerder](mailto:jguerder@fr.pulseheberg.net)
+* [Joseph Mingrone](mailto:jehops@users.noreply.github.com)
+* [Joshua Wood](mailto:josh@joshuawood.net)
+* [Julien](mailto:tiwy57@users.noreply.github.com)
+* [Julien Deswaef](mailto:juego@requiem4tv.com)
+* [June Sallou](mailto:jnsll@users.noreply.github.com)
+* [Jérémy Benoist](mailto:j0k3r@users.noreply.github.com)
+* [KEINOS](mailto:github@keinos.com)
+* [Keiji Matsuzaki](mailto:futoase@gmail.com)
+* [Kevin Liu](mailto:kevin@potatofrom.space)
* [Kit Redgrave](mailto:qwertyitis@gmail.com)
* [Knut Erik](mailto:abjectio@users.noreply.github.com)
-* [mkody](https://github.com/mkody)
-* [k0ta0uchi](https://github.com/k0ta0uchi)
-* [KrzysiekJ](https://github.com/KrzysiekJ)
+* [Kota Ouchi](mailto:k0ta0uchi@gmail.com)
+* [Krzysztof Jurewicz](mailto:krzysztof.jurewicz@gmail.com)
* [Leo Wzukw](mailto:leowzukw@users.noreply.github.com)
-* [Tak](https://github.com/Tak)
-* [cacheflow](https://github.com/cacheflow)
-* [ldidry](https://github.com/ldidry)
-* [jemus42](https://github.com/jemus42)
-* [lfuelling](https://github.com/lfuelling)
-* [Grabacr07](https://github.com/Grabacr07)
-* [mistermantas](https://github.com/mistermantas)
-* [MareenaKunjachan](https://github.com/MareenaKunjachan)
-* [mareklach](https://github.com/mareklach)
-* [wirehack7](https://github.com/wirehack7)
-* [martymcguire](https://github.com/martymcguire)
-* [marvinkopf](https://github.com/marvinkopf)
-* [otsune](https://github.com/otsune)
-* [mbugowski](https://github.com/mbugowski)
+* [Leonie](mailto:62470640+bubblineyuri@users.noreply.github.com)
+* [Levi Bard](mailto:taktaktaktaktaktaktaktaktaktak@gmail.com)
+* [Lex Alexander](mailto:l.alexander10@gmail.com)
+* [Lorenz Diener](mailto:lorenzd@gmail.com)
+* [Luc Didry](mailto:ldidry@users.noreply.github.com)
+* [Lukas Burk](mailto:jemus42@users.noreply.github.com)
+* [Manato Kameya](mailto:grabacr07+github@gmail.com)
+* [Mantas](mailto:mistermantas@users.noreply.github.com)
+* [Marcin Mikołajczak](mailto:me@mkljczk.pl)
+* [Mareena Kunjachan](mailto:mareenakunjachan@gmail.com)
+* [Marek Lach](mailto:marek.brohatwack.lach@gmail.com)
+* [Markus R](mailto:wirehack7@users.noreply.github.com)
+* [Marty McGuire](mailto:schmartissimo@gmail.com)
+* [Marvin Kopf](mailto:marvinkopf@posteo.de)
+* [Masafumi Otsune](mailto:info@otsune.com)
+* [Matej Ľach](mailto:matejlach@users.noreply.github.com)
+* [Mateusz Bugowski](mailto:23140767+mbugowski@users.noreply.github.com)
* [Mathias B](mailto:10813340+mathias-b@users.noreply.github.com)
-* [madmath03](https://github.com/madmath03)
-* [matt-auckland](https://github.com/matt-auckland)
-* [webroo](https://github.com/webroo)
+* [Mathieu Brunot](mailto:mb.mathieu.brunot@gmail.com)
+* [Matt](mailto:matt-auckland@users.noreply.github.com)
+* [Matt Sweetman](mailto:webroo@gmail.com)
* [Matthias Beyer](mailto:mail@beyermatthias.de)
* [Matthias Jouan](mailto:matthias.jouan@gmail.com)
* [Matthieu Paret](mailto:matthieuparet69@gmail.com)
@@ -512,10 +546,11 @@ and provided thanks to the work of the following contributors:
* [S.H](mailto:gamelinks007@gmail.com)
* [Sadiq Saif](mailto:staticsafe@users.noreply.github.com)
* [Sam Hewitt](mailto:hewittsamuel@gmail.com)
-* [Sasha Sorokin](mailto:dafri.nochiterov8@gmail.com)
+* [Sara Aimée Smiseth](mailto:51710585+sarasmiseth@users.noreply.github.com)
* [Satoshi KOJIMA](mailto:skoji@mac.com)
* [ScienJus](mailto:i@scienjus.com)
* [Scott Larkin](mailto:scott@codeclimate.com)
+* [Scott Sweeny](mailto:scott@ssweeny.net)
* [Sebastian Hübner](mailto:imolein@users.noreply.github.com)
* [Sebastian Morr](mailto:sebastian@morr.cc)
* [Sergei Č](mailto:noiwex1911@gmail.com)
@@ -525,10 +560,12 @@ and provided thanks to the work of the following contributors:
* [Shin Kojima](mailto:shin@kojima.org)
* [Shouko Yu](mailto:imshouko@gmail.com)
* [Sina Mashek](mailto:sina@mashek.xyz)
+* [Soft. Dev](mailto:24978+nileshkumar@users.noreply.github.com)
* [Soshi Kato](mailto:mail@sossii.com)
* [Spanky](mailto:2788886+spankyworks@users.noreply.github.com)
* [StefOfficiel](mailto:pichard.stephane@free.fr)
* [Steven Tappert](mailto:admin@dark-it.net)
+* [Stéphane Guillou](mailto:stephane.guillou@member.fsf.org)
* [Svetlozar Todorov](mailto:svetlik@users.noreply.github.com)
* [Sébastien Santoro](mailto:dereckson@espace-win.org)
* [Tad Thorley](mailto:phaedryx@users.noreply.github.com)
@@ -536,7 +573,10 @@ and provided thanks to the work of the following contributors:
* [Takayuki KUSANO](mailto:github@tkusano.jp)
* [TakesxiSximada](mailto:takesxi.sximada@gmail.com)
* [Tao Bror Bojlén](mailto:brortao@users.noreply.github.com)
+* [Taras Gogol](mailto:taras2358@gmail.com)
+* [Tdxdxoz](mailto:tdxdxoz@gmail.com)
* [TheInventrix](mailto:theinventrix@users.noreply.github.com)
+* [TheMainOne](mailto:50847364+theevilskeleton@users.noreply.github.com)
* [Thomas Alberola](mailto:thomas@needacoffee.fr)
* [Toby Deshane](mailto:fortyseven@users.noreply.github.com)
* [Toby Pinder](mailto:gigitrix@gmail.com)
@@ -563,6 +603,7 @@ and provided thanks to the work of the following contributors:
* [Yann Klis](mailto:yann.klis@gmail.com)
* [Yağızhan](mailto:35808275+yagizhan49@users.noreply.github.com)
* [Yeechan Lu](mailto:wz.bluesnow@gmail.com)
+* [Your Name](mailto:lorenzd@gmail.com)
* [Yusuke Abe](mailto:moonset20@gmail.com)
* [Zachary Spector](mailto:logicaldash@gmail.com)
* [ZiiX](mailto:ziix@users.noreply.github.com)
@@ -572,6 +613,7 @@ and provided thanks to the work of the following contributors:
* [bsky](mailto:git@imbsky.net)
* [caesarologia](mailto:lopesgemelli.1@gmail.com)
* [cbayerlein](mailto:c.bayerlein@gmail.com)
+* [chr v1.x](mailto:chr@cybre.space)
* [chrolis](mailto:chrolis@users.noreply.github.com)
* [cormo](mailto:cormorant2+github@gmail.com)
* [d0p1](mailto:dopi-sama@hush.com)
@@ -582,6 +624,7 @@ and provided thanks to the work of the following contributors:
* [fusshi-](mailto:dikky1218@users.noreply.github.com)
* [gentaro](mailto:gentaroooo@gmail.com)
* [gol-cha](mailto:info@mevo.xyz)
+* [guigeekz](mailto:pattusg@gmail.com)
* [hakoai](mailto:hk--76@qa2.so-net.ne.jp)
* [haosbvnker](mailto:github@chaosbunker.com)
* [ichi_i](mailto:51489410+ichi-i@users.noreply.github.com)
@@ -593,10 +636,11 @@ and provided thanks to the work of the following contributors:
* [jooops](mailto:joops@autistici.org)
* [jukper](mailto:jukkaperanto@gmail.com)
* [jumoru](mailto:jumoru@mailbox.org)
+* [kaiyou](mailto:pierre@jaury.eu)
* [karlyeurl](mailto:karl.yeurl@gmail.com)
* [kedama](mailto:32974885+kedamadq@users.noreply.github.com)
-* [kodai](mailto:shirafuta.kodai@gmail.com)
* [kuro5hin](mailto:rusty@kuro5hin.org)
+* [leo60228](mailto:leo@60228.dev)
* [luzpaz](mailto:luzpaz@users.noreply.github.com)
* [maxypy](mailto:maxime@mpigou.fr)
* [mhe](mailto:mail@marcus-herrmann.com)
@@ -607,21 +651,25 @@ and provided thanks to the work of the following contributors:
* [muan](mailto:muan@github.com)
* [namelessGonbai](mailto:43787036+namelessgonbai@users.noreply.github.com)
* [neetshin](mailto:neetshin@neetsh.in)
+* [noiob](mailto:8197071+noiob@users.noreply.github.com)
+* [notozeki](mailto:notozeki@users.noreply.github.com)
+* [ntl-purism](mailto:57806346+ntl-purism@users.noreply.github.com)
* [nzws](mailto:git-yuzu@svk.jp)
* [rch850](mailto:rich850@gmail.com)
* [roikale](mailto:roikale@users.noreply.github.com)
* [rysiekpl](mailto:rysiek@hackerspace.pl)
* [saturday06](mailto:dyob@lunaport.net)
+* [scd31](mailto:57571338+scd31@users.noreply.github.com)
* [scriptjunkie](mailto:scriptjunkie@scriptjunkie.us)
* [seekr](mailto:mario.drs@gmail.com)
+* [sternenseemann](mailto:git@lukasepple.de)
* [sundevour](mailto:31990469+sundevour@users.noreply.github.com)
* [syui](mailto:syui@users.noreply.github.com)
* [tackeyy](mailto:mailto.takita.yusuke@gmail.com)
-* [tateisu](mailto:tateisu@gmail.com)
+* [taicv](mailto:chuvantai@gmail.com)
* [tmyt](mailto:shigure@refy.net)
* [trevDev()](mailto:trev@trevdev.ca)
* [tsia](mailto:github@tsia.de)
-* [umonaca](mailto:53662960+umonaca@users.noreply.github.com)
* [utam0k](mailto:k0ma@utam0k.jp)
* [vpzomtrrfrt](mailto:vpzomtrrfrt@gmail.com)
* [walfie](mailto:walfington@gmail.com)
@@ -634,6 +682,7 @@ and provided thanks to the work of the following contributors:
* [りんすき](mailto:6533808+rinsuki@users.noreply.github.com)
* [ヨイツの賢狼ホロ | 3rd style](mailto:horo@yoitsu.moe)
* [唐宗勛](mailto:tangzongxun@hotmail.com)
+* [夕日](mailto:xirikm@gmail.com)
* [猫吸血鬼ディフリス / 猫ロキP](mailto:deflis@gmail.com)
* [艮 鮟鱇](mailto:ushitora_anqou@yahoo.co.jp)
* [西小倉宏信](mailto:nishiko@mindia.jp)
@@ -645,122 +694,308 @@ This document is provided for informational purposes only. Since it is only upda
Following people have contributed to translation of Mastodon:
+- ᏦᏁᎢᎵᏫ 😷 (*Spanish, Argentina*)
+- Sveinn í Felli (*Icelandic*)
+- taicv (*Vietnamese*)
+- ButterflyOfFire (*Arabic; French; Kabyle*)
+- Duy (*Vietnamese*)
+- Evert Prants (*Estonian*)
- Zoltán Gera (*Hungarian*)
+- Daniele Lira Mereb (*Portuguese, Brazilian*)
- Kristijan Tkalec (*Slovenian*)
-- Evert Prants (*Estonian*)
+- stan ionut (*Romanian*)
+- Ramdziana F Y (*Indonesian*)
+- Michal Stanke (*Czech*)
+- Xosé M. (*Galician; Spanish*)
+- 奈卜拉 (*Chinese Simplified*)
- borys_sh (*Ukrainian*)
-- ButterflyOfFire (*Arabic; French*)
+- Miguel Mayol (*Spanish; Catalan*)
+- Besnik_b (*Albanian*)
+- Thai Localization (*Thai*)
+- Emanuel Pina (*Portuguese*)
+- Jeong Arm (*Korean; Esperanto; Japanese*)
+- Imre Kristoffer Eilertsen (*Norwegian*)
+- Danial Behzadi (*Persian*)
- Osoitz (*Basque*)
-- oɹʇuʞ (*Spanish, Argentina*)
-- koyu (*German*)
+- Peterandre (*Norwegian Nynorsk; Norwegian*)
- Jeroen (*Dutch*)
+- spla (*Catalan; Spanish*)
+- Iváns (*Galician*)
+- koyu (*German*)
+- Sasha Sorokin (*Russian; Vietnamese; Swedish; Catalan; Greek; Hungarian; Armenian; Albanian; Galician; French; Danish; German; Korean; Ukrainian*)
+- enolp (*Asturian*)
+- Masoud Abkenar (*Persian*)
+- lamnatos (*Greek*)
+- Alix Rossi (*Corsican; French*)
+- arshat (*Kazakh*)
+- FédiQuébec (*French*)
+- Marek Ľach (*Slovak; Polish*)
- Muha Aliss (*Turkish*)
-- 唐宗勛 (*Chinese Simplified*)
-- Jeong Arm (*Korean; Esperanto; Japanese*)
-- Oguz Ersen (*Turkish*)
-- spla (*Catalan*)
-- Ramdziana F Y (*Indonesian*)
+- tolstoevsky (*Russian*)
+- Emyn-Russell Nt Nefydd (*Welsh*)
- Aditoo17 (*Czech*)
-- Xosé M. (*Galician*)
-- Roboron (*Spanish*)
-- Alix Rossi (*Corsican; French*)
- Maya Minatsuki (*Japanese*)
-- Masoud Abkenar (*Persian*)
-- Thai Localization (*Thai*)
-- Marek Ľach (*Slovak; Polish*)
-- d5Ziif3K (*Ukrainian*)
-- lamnatos (*Greek*)
-- Emyn Nant Nefydd (*Welsh*)
+- ariasuni (*French; Esperanto*)
+- Roboron (*Spanish*)
+- Alessandro Levati (*Italian*)
- Diluns (*Occitan*)
+- regulartranslator (*Portuguese, Brazilian*)
+- vishnuvaratharajan (*Tamil*)
+- Marcin Mikołajczak (*Polish*)
+- Yi-Jyun Pan (*Chinese Traditional*)
+- adrmzz (*Sardinian*)
+- d5Ziif3K (*Ukrainian*)
+- GiorgioHerbie (*Italian*)
+- christalleras (*Norwegian Nynorsk*)
+- Taloran (*Norwegian Nynorsk*)
+- ThibG (*French; Icelandic*)
+- Akarshan Biswas (*Bengali*)
- atarashiako (*Chinese Simplified*)
- 101010 (*Polish*)
-- Yi-Jyun Pan (*Chinese Traditional*)
- silkevicious (*Italian*)
-- FédiQuébec (*French*)
+- Bertil Hedkvist (*Swedish*)
+- cybergene (*Japanese*)
+- norayr (*Armenian*)
+- William(ѕ)ⁿ (*Spanish*)
+- Tiago Epifânio (*Portuguese*)
+- Mentor Gashi (*Albanian*)
- Jaz-Michael King (*Welsh*)
-- christalleras (*Norwegian Nynorsk*)
-- tykayn (*French*)
-- Alessandro Levati (*Italian*)
- carolinagiorno (*Portuguese, Brazilian*)
+- Roby Thomas (*Malayalam*)
+- Bharat Kumar (*Hindi*)
+- tykayn (*French*)
+- axi (*Finnish*)
+- Selyan Slimane AMIRI (*Kabyle*)
- taoxvx (*Danish*)
-- sabri (*Spanish*)
-- Sasha Sorokin (*Russian*)
+- Hrach Mkrtchyan (*Armenian*)
+- sabri (*Spanish; Spanish, Argentina*)
+- Dewi (*Breton; French*)
+- SteinarK (*Norwegian Nynorsk*)
+- Mathias B. Vagnes (*Norwegian*)
+- dashersyed (*Urdu*)
+- ThonyVezbe (*Breton*)
+- Acolyte (*Ukrainian*)
+- Conight Wang (*Chinese Simplified*)
+- Damjan Dimitrioski (*Macedonian*)
+- PPNplus (*Thai*)
+- Tagomago (*Spanish; French*)
- shioko (*Chinese Simplified*)
+- Balázs Meskó (*Hungarian*)
- Evgeny Petrov (*Russian*)
-- ariasuni (*French; Esperanto*)
-- Tiago Epifânio (*Portuguese*)
-- dxwc (*Bengali*)
+- Gwenn (*Breton*)
+- Ryo (*Korean*)
+- Rafael H L Moretti (*Portuguese, Brazilian*)
+- jaranta (*Finnish*)
+- gagik_ (*Armenian*)
+- Felicia (*Swedish*)
+- Jess Rafn (*Danish*)
+- Stasiek Michalski (*Polish*)
- liffon (*Swedish*)
+- dxwc (*Bengali*)
+- Saederup92 (*Danish*)
- Vanege (*Esperanto*)
+- jmontane (*Catalan*)
- Johan Schiff (*Swedish*)
+- Arunmozhi (*Tamil*)
- kat (*Ukrainian; Russian*)
+- Laura (*Polish*)
- oti4500 (*Hungarian; Ukrainian*)
+- diazepan (*Spanish; Spanish, Argentina*)
+- Sokratis Alichanidis (*Greek*)
+- Rikard Linde (*Swedish*)
- Juan José Salvador Piedra (*Spanish*)
-- diazepan (*Spanish*)
+- marzuquccen (*Kabyle*)
+- BurekzFinezt (*Serbian*)
- SHeija (*Finnish*)
- Jack R (*Spanish*)
-- Saederup92 (*Danish*)
-- Stasiek Michalski (*Polish*)
-- Dewi (*Breton; French*)
-- cybergene (*Japanese*)
+- andruhov (*Ukrainian; Russian*)
+- 森の子リスのミーコの大冒険 (*Japanese*)
+- るいーね (*Japanese*)
+- Sam Tux (*Bengali*)
+- Unmual (*Spanish*)
- AW Unad (*Indonesian*)
-- Andrea Lo Iacono (*Italian*)
+- Cutls (*Japanese*)
- Ray (*Spanish*)
-- Unmual (*Spanish*)
-- Ryo (*Korean*)
+- Falling Snowdin (*Vietnamese*)
+- Andrea Lo Iacono (*Italian*)
+- EPEMA (*German*)
+- Kinshuk Sunil (*Hindi*)
+- Ullas Joseph (*Malayalam*)
+- Yu-Pai Liu (*Chinese Traditional*)
+- Amarin Cemthong (*Thai*)
- juanda097 (*Spanish*)
- Anunnakey (*Macedonian*)
-- Cutls (*Japanese*)
+- StanleyFrew (*French*)
- erikstl (*Esperanto*)
-- ruine (*Japanese*)
- MadeInSteak (*Finnish*)
-- Sokratis Alichanidis (*Greek*)
-- dragnucs2 (*Arabic*)
-- frumble (*German*)
-- Rikard Linde (*Swedish*)
-- PPNplus (*Thai*)
+- Heimen Stoffels (*Dutch*)
+- Rajarshi Guha (*Bengali*)
+- Andrew (*Romanian*)
+- Goudarz Jafari (*Persian*)
- arethsu (*Swedish*)
-- EPEMA YT (*German*)
+- Carlos Solís (*Esperanto*)
+- Parthan S Ramanujam (*Tamil*)
+- Ali Demirtaş (*Turkish*)
+- Kasper Nymand (*Danish*)
+- TS (*Finnish*)
+- SensDeViata (*Ukrainian*)
+- SergioFMiranda (*Portuguese, Brazilian*)
+- OctolinGamer (*Portuguese, Brazilian*)
+- AzureNya (*Chinese Simplified*)
+- Ram varma (*Tamil*)
+- 北䑓如法 (*Japanese*)
+- frumble (*German*)
+- kekkepikkuni (*Tamil*)
+- oorsutri (*Tamil*)
+- Nithin V (*Tamil*)
+- Miro Rauhala (*Finnish*)
+- diorama (*Italian*)
- Rhys Harrison (*Esperanto*)
+- Guillaume Turchini (*French*)
+- Ganesh D (*Marathi*)
+- dragnucs2 (*Arabic*)
+- Pedro Henrique (*Portuguese, Brazilian*)
+- Tejas Harad (*Marathi*)
+- Vasanthan (*Tamil*)
+- 硫酸鶏 (*Japanese*)
+- manukp (*Malayalam*)
+- psymyn (*Hebrew*)
+- earth dweller (*Marathi*)
+- meijerivoi (*Finnish*)
+- essaar (*Tamil*)
+- serubeena (*Swedish*)
+- Rintan (*Japanese*)
+- Karol Kosek (*Polish*)
+- valarivan (*Tamil*)
+- Sebastián Andil (*Slovak*)
+- v4vachan (*Malayalam*)
- KEINOS (*Japanese*)
+- Ivan T. (*Chinese Traditional, Hong Kong*)
- filippodb (*Italian*)
+- Balázs Meskó (*Hungarian*)
- JzshAC (*Chinese Simplified*)
-- Rintan1 (*Japanese*)
+- Bottle (*Tamil*)
+- Khóo (*Chinese Traditional*)
+- Steven Tappert (*German*)
- Antillion (*Spanish*)
+- ZiriSut (*Kabyle*)
+- gowthamanb (*Tamil*)
- hiphipvargas (*Portuguese*)
+- Arttu Ylhävuori (*Finnish*)
- Ch. (*Korean*)
- tctovsli (*Norwegian Nynorsk*)
+- Hinaloe (*Japanese*)
+- strubbl (*German*)
- vjasiegd (*Polish*)
- SamitiMed (*Thai*)
+- Reg3xp (*Persian*)
+- AlexKoala (*Korean*)
- umelard (*Hebrew*)
-- 硫酸鶏 (*Japanese*)
-- Adrián Lattes (*Spanish*)
-- Hinaloe (*Japanese*)
-- Renato "Lond" Cerqueira (*Portuguese, Brazilian*)
+- VSx86 (*Russian*)
+- Daniel Dimitrov (*Bulgarian*)
+- mynameismonkey (*Welsh*)
- parnikkapore (*Thai*)
-- Marcin Mikołajczak (*Polish*)
-- 森の子リスのミーコの大冒険 (*Japanese*)
-- Marcepanek_ (*Polish*)
+- Mo_der Steven (*Chinese Simplified*)
+- SKELET (*Danish*)
+- Renato "Lond" Cerqueira (*Portuguese, Brazilian*)
+- enipra (*Armenian*)
+- musix (*Persian*)
+- ギャラ (*Chinese Simplified; Japanese*)
+- ALEM FARID (*Kabyle*)
+- ybardapurkar (*Marathi*)
+- Adrián Lattes (*Spanish*)
+- rasheedgm (*Kannada*)
+- omquylzu (*Latvian*)
+- Belkacem Mohammed (*Kabyle*)
+- Navjot Singh (*Hindi*)
+- Ozai (*German*)
- Sahak Petrosyan (*Armenian*)
-- Daniel Dimitrov (*Bulgarian*)
+- siamano (*Thai; Esperanto*)
+- se7entime (*Indonesian*)
+- Viorel-Cătălin Răpițeanu (*Romanian*)
+- Siddhartha Sarathi Basu (*Bengali*)
+- Pachara Chantawong (*Thai*)
+- Skew (*French*)
+- Zijian Zhao (*Chinese Simplified*)
+- Guru Prasath Anandapadmanaban (*Tamil*)
+- turtle836 (*German*)
+- GatoOscuro (*Spanish*)
+- Lamin (*Japanese*)
+- Marcepanek_ (*Polish*)
+- Yann Aguettaz (*French*)
+- Feruz Oripov (*Russian*)
+- Mick Onio (*Asturian*)
+- hg6 (*Hindi*)
+- Malik Mann (*German*)
+- padulafacundo (*Spanish*)
+- r3dsp1 (*Chinese Traditional, Hong Kong*)
+- Tianqi Zhang (*Chinese Simplified*)
+- Padraic Calpin (*Slovenian*)
+- cenegd (*Chinese Simplified*)
+- piupiupiudiu (*Chinese Simplified*)
- Hugh Liu (*Chinese Simplified*)
- Rakino (*Chinese Simplified*)
+- Jothipazhani Nagarajan (*Tamil*)
+- Miquel Sabaté Solà (*Catalan*)
+- AmazighNM (*Kabyle*)
+- Solid Rhino (*Dutch*)
+- hallomaurits (*Dutch*)
- hussama (*Portuguese, Brazilian*)
-- ThibG (*French*)
+- shafouz (*Portuguese, Brazilian*)
+- Tagada (*French*)
+- Tom_ (*Czech*)
- SnDer (*Dutch*)
-- PifyZ (*French*)
- eichkat3r (*German*)
-- Karol Kosek (*Polish*)
-- Akarshan Biswas (*Bengali*)
+- PifyZ (*French*)
+- OminousCry (*Russian*)
+- Shrinivasan T (*Tamil*)
+- Nathaël Noguès (*French*)
+- Daniel M. (*Catalan*)
+- Swati Sani (*Urdu*)
+- Kk (*Kannada*)
+- SusVersiva (*Catalan*)
+- Robin van der Vliet (*Esperanto*)
+- Zinkokooo (*Basque*)
- Tradjincal (*French*)
-- Steven Tappert (*German*)
-- sergioaraujo1 (*Portuguese, Brazilian*)
+- Vikatakavi (*Kannada*)
+- prabhjot (*Hindi*)
+- twpenguin (*Chinese Traditional*)
- mmokhi (*Persian*)
-- fedot (*Russian*)
+- sergioaraujo1 (*Portuguese, Brazilian*)
+- Livingston Samuel (*Tamil*)
+- tsundoker (*Malayalam*)
- skaaarrr (*German*)
+- 夜楓Yoka (*Chinese Simplified*)
+- kiwi0 (*Italian*)
+- fedot (*Russian*)
+- mkljczk (*Polish*)
+- igordrozniak (*Polish*)
+- Ricardo Colin (*Spanish*)
+- Esther (*Portuguese*)
+- Paz Galindo (*Spanish*)
+- Philipp Fischbeck (*German*)
+- ralozkolya (*Georgian*)
- JackXu (*Chinese Simplified*)
-- Lukas Fülling (*German*)
+- Allen Zhong (*Chinese Simplified*)
- Zoé Bőle (*German*)
+- Lukas Fülling (*German*)
+- Albatroz Jeremias (*Portuguese*)
+- Samir Tighzert (*Kabyle*)
+- Nocta (*French*)
+- Anoop (*Malayalam*)
+- pezcurrel (*Italian*)
- Dremski (*Bulgarian*)
+- Aymeric (*French*)
- tamaina (*Japanese*)
+- Doug (*Portuguese, Brazilian*)
+- Matias Lavik (*Norwegian Nynorsk*)
+- Fleva (*Sardinian*)
- OpenAlgeria (*Arabic*)
+- koppe-pan (*Japanese*)
+- Amith Raj Shetty (*Kannada*)
+- smedvedev (*Russian*)
+- Trond Boksasp (*Norwegian*)
+- random_person (*Spanish*)
+- Sais Lakshmanan (*Tamil*)
+- mikel (*Spanish*)
+- Mohammad Adnan Mahmood (*Arabic*)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6296f00167385f..cfc95dcf61d18a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,214 @@ Changelog
All notable changes to this project will be documented in this file.
+## [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)
+
## [v3.1.5] - 2020-07-07
### Security
diff --git a/Dockerfile b/Dockerfile
index 0537d8facdd416..fa6abad5a13690 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,11 +1,11 @@
-FROM ubuntu:18.04 as build-dep
+FROM ubuntu:20.04 as build-dep
# Use bash for the shell
SHELL ["bash", "-c"]
# Install Node v12 (LTS)
-ENV NODE_VER="12.16.1"
-RUN ARCH= && \
+ENV NODE_VER="12.16.3"
+RUN ARCH= && \
dpkgArch="$(dpkg --print-architecture)" && \
case "${dpkgArch##*-}" in \
amd64) ARCH='x64';; \
@@ -74,7 +74,7 @@ RUN cd /opt/mastodon && \
bundle install -j$(nproc) && \
yarn install --pure-lockfile
-FROM ubuntu:18.04
+FROM ubuntu:20.04
# Copy over all the langs needed for runtime
COPY --from=build-dep /opt/node /opt/node
@@ -98,8 +98,8 @@ RUN apt update && \
# Install mastodon runtime deps
RUN apt -y --no-install-recommends install \
libssl1.1 libpq5 imagemagick ffmpeg \
- libicu60 libprotobuf10 libidn11 libyaml-0-2 \
- file ca-certificates tzdata libreadline7 && \
+ libicu66 libprotobuf17 libidn11 libyaml-0-2 \
+ file ca-certificates tzdata libreadline8 && \
apt -y install gcc && \
ln -s /opt/mastodon /mastodon && \
gem install bundler && \
diff --git a/Gemfile b/Gemfile
index 3150c368d053a0..5ac1c79c16f9aa 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,10 +6,10 @@ ruby '>= 2.5.0', '< 3.0.0'
gem 'pkg-config', '~> 1.4'
gem 'puma', '~> 4.3'
-gem 'rails', '~> 5.2.4.2'
+gem 'rails', '~> 5.2.4.3'
gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 0.20'
-gem 'rack', '~> 2.2.2'
+gem 'rack', '~> 2.2.3'
gem 'thwait', '~> 0.1.0'
gem 'e2mmap', '~> 0.1.0'
@@ -17,10 +17,10 @@ gem 'e2mmap', '~> 0.1.0'
gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 1.2'
gem 'makara', '~> 0.4'
-gem 'pghero', '~> 2.4'
+gem 'pghero', '~> 2.5'
gem 'dotenv-rails', '~> 2.7'
-gem 'aws-sdk-s3', '~> 1.64', require: false
+gem 'aws-sdk-s3', '~> 1.73', require: false
gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false
gem 'paperclip', '~> 6.0'
@@ -48,19 +48,19 @@ gem 'omniauth-cas', '~> 1.1'
gem 'omniauth-saml', '~> 1.10'
gem 'omniauth', '~> 1.9'
+gem 'color_diff', '~> 0.1'
gem 'discard', '~> 1.2'
gem 'doorkeeper', '~> 5.4'
+gem 'ed25519', '~> 1.2'
gem 'fast_blank', '~> 1.0'
gem 'fastimage'
-gem 'goldfinger', '~> 2.1'
gem 'hiredis', '~> 0.6'
gem 'redis-namespace', '~> 1.7'
gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b'
gem 'htmlentities', '~> 4.3'
gem 'http', '~> 4.4'
gem 'http_accept_language', '~> 2.1'
-gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2', submodules: true
-gem 'httplog', '~> 1.4.2'
+gem 'httplog', '~> 1.4.3'
gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0'
@@ -79,11 +79,11 @@ gem 'rack-attack', '~> 6.3'
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
gem 'rails-i18n', '~> 5.1'
gem 'rails-settings-cached', '~> 0.6'
-gem 'redis', '~> 4.1', require: ['redis', 'redis/connection/hiredis']
+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 'sanitize', '~> 5.1'
+gem 'sanitize', '~> 5.2'
gem 'sidekiq', '~> 6.0'
gem 'sidekiq-scheduler', '~> 3.0'
gem 'sidekiq-unique-jobs', '~> 6.0'
@@ -93,7 +93,6 @@ gem 'simple_form', '~> 5.0'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
gem 'stoplight', '~> 2.2.0'
gem 'strong_migrations', '~> 0.6'
-gem 'tty-command', '~> 0.9', require: false
gem 'tty-prompt', '~> 0.21', require: false
gem 'twitter-text', '~> 1.14'
gem 'tzinfo-data', '~> 1.2020'
@@ -118,15 +117,15 @@ group :production, :test do
end
group :test do
- gem 'capybara', '~> 3.32'
+ gem 'capybara', '~> 3.33'
gem 'climate_control', '~> 0.2'
- gem 'faker', '~> 2.11'
+ gem 'faker', '~> 2.13'
gem 'microformats', '~> 4.2'
gem 'rails-controller-testing', '~> 1.0'
- gem 'rspec-sidekiq', '~> 3.0'
+ gem 'rspec-sidekiq', '~> 3.1'
gem 'simplecov', '~> 0.18', require: false
gem 'webmock', '~> 3.8'
- gem 'parallel_tests', '~> 2.32'
+ gem 'parallel_tests', '~> 3.0'
gem 'rspec_junit_formatter', '~> 0.4'
end
@@ -139,13 +138,13 @@ group :development do
gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 1.4'
gem 'memory_profiler'
- gem 'rubocop', '~> 0.82', require: false
- gem 'rubocop-rails', '~> 2.5', require: false
+ gem 'rubocop', '~> 0.86', require: false
+ gem 'rubocop-rails', '~> 2.6', require: false
gem 'brakeman', '~> 4.8', require: false
- gem 'bundler-audit', '~> 0.6', require: false
+ gem 'bundler-audit', '~> 0.7', require: false
gem 'capistrano', '~> 3.14'
- gem 'capistrano-rails', '~> 1.4'
+ gem 'capistrano-rails', '~> 1.5'
gem 'capistrano-rbenv', '~> 2.1'
gem 'capistrano-yarn', '~> 2.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index accac821b5bfbe..c3216870de98c0 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -13,14 +13,6 @@ GIT
specs:
posix-spawn (0.3.13)
-GIT
- remote: https://github.com/tmm1/http_parser.rb
- revision: 54b17ba8c7d8d20a16dfc65d1775241833219cf2
- ref: 54b17ba8c7d8d20a16dfc65d1775241833219cf2
- submodules: true
- specs:
- http_parser.rb (0.6.1)
-
GIT
remote: https://github.com/witgo/nilsimsa
revision: fd184883048b922b176939f851338d0a4971a532
@@ -31,25 +23,25 @@ GIT
GEM
remote: https://rubygems.org/
specs:
- actioncable (5.2.4.2)
- actionpack (= 5.2.4.2)
+ actioncable (5.2.4.3)
+ actionpack (= 5.2.4.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
- actionmailer (5.2.4.2)
- actionpack (= 5.2.4.2)
- actionview (= 5.2.4.2)
- activejob (= 5.2.4.2)
+ actionmailer (5.2.4.3)
+ actionpack (= 5.2.4.3)
+ actionview (= 5.2.4.3)
+ activejob (= 5.2.4.3)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
- actionpack (5.2.4.2)
- actionview (= 5.2.4.2)
- activesupport (= 5.2.4.2)
+ actionpack (5.2.4.3)
+ actionview (= 5.2.4.3)
+ activesupport (= 5.2.4.3)
rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- actionview (5.2.4.2)
- activesupport (= 5.2.4.2)
+ actionview (5.2.4.3)
+ activesupport (= 5.2.4.3)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@@ -60,20 +52,20 @@ GEM
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_record_query_trace (1.7)
- activejob (5.2.4.2)
- activesupport (= 5.2.4.2)
+ activejob (5.2.4.3)
+ activesupport (= 5.2.4.3)
globalid (>= 0.3.6)
- activemodel (5.2.4.2)
- activesupport (= 5.2.4.2)
- activerecord (5.2.4.2)
- activemodel (= 5.2.4.2)
- activesupport (= 5.2.4.2)
+ 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.2)
- actionpack (= 5.2.4.2)
- activerecord (= 5.2.4.2)
+ activestorage (5.2.4.3)
+ actionpack (= 5.2.4.3)
+ activerecord (= 5.2.4.3)
marcel (~> 0.3.1)
- activesupport (5.2.4.2)
+ activesupport (5.2.4.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
@@ -86,29 +78,29 @@ GEM
activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 14.0)
arel (9.0.0)
- ast (2.4.0)
+ ast (2.4.1)
attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
av (0.9.0)
cocaine (~> 0.5.3)
aws-eventstream (1.1.0)
- aws-partitions (1.312.0)
- aws-sdk-core (3.95.0)
+ aws-partitions (1.338.0)
+ aws-sdk-core (3.103.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
- aws-sdk-kms (1.31.0)
- aws-sdk-core (~> 3, >= 3.71.0)
+ aws-sdk-kms (1.36.0)
+ aws-sdk-core (~> 3, >= 3.99.0)
aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.64.0)
- aws-sdk-core (~> 3, >= 3.83.0)
+ aws-sdk-s3 (1.73.0)
+ aws-sdk-core (~> 3, >= 3.102.1)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
- aws-sigv4 (1.1.3)
- aws-eventstream (~> 1.0, >= 1.0.2)
+ aws-sigv4 (1.2.1)
+ aws-eventstream (~> 1, >= 1.0.2)
bcrypt (3.1.13)
- better_errors (2.7.0)
+ better_errors (2.7.1)
coderay (>= 1.0.0)
erubi (>= 1.0.0)
rack (>= 0.9.0)
@@ -118,24 +110,24 @@ GEM
ffi (~> 1.10.0)
bootsnap (1.4.6)
msgpack (~> 1.0)
- brakeman (4.8.1)
- browser (4.1.0)
+ brakeman (4.8.2)
+ browser (4.2.0)
builder (3.2.4)
bullet (6.1.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
- bundler-audit (0.6.1)
+ bundler-audit (0.7.0.1)
bundler (>= 1.2.0, < 3)
- thor (~> 0.18)
+ thor (>= 0.18, < 2)
byebug (11.1.3)
- capistrano (3.14.0)
+ capistrano (3.14.1)
airbrussh (>= 1.0.0)
i18n
rake (>= 10.0.0)
sshkit (>= 1.9.0)
capistrano-bundler (1.6.0)
capistrano (~> 3.1)
- capistrano-rails (1.4.0)
+ capistrano-rails (1.5.0)
capistrano (~> 3.1)
capistrano-bundler (~> 1.1)
capistrano-rbenv (2.1.6)
@@ -143,7 +135,7 @@ GEM
sshkit (~> 1.3)
capistrano-yarn (2.0.2)
capistrano (~> 3.0)
- capybara (3.32.1)
+ capybara (3.33.0)
addressable
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
@@ -164,16 +156,17 @@ GEM
climate_control (0.2.0)
cocaine (0.5.8)
climate_control (>= 0.0.3, < 1.0)
- coderay (1.1.2)
+ coderay (1.1.3)
+ color_diff (0.1)
concurrent-ruby (1.1.6)
- connection_pool (2.2.2)
+ connection_pool (2.2.3)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.6)
css_parser (1.7.1)
addressable
debug_inspector (0.0.3)
- devise (4.7.1)
+ devise (4.7.2)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
@@ -188,7 +181,7 @@ GEM
devise_pam_authenticatable2 (9.2.0)
devise (>= 4.0.0)
rpam2 (~> 4.0)
- diff-lcs (1.3)
+ diff-lcs (1.4.4)
discard (1.2.0)
activerecord (>= 4.2, < 7)
docile (1.3.2)
@@ -201,13 +194,14 @@ GEM
dotenv (= 2.7.5)
railties (>= 3.2, < 6.1)
e2mmap (0.1.0)
- elasticsearch (7.6.0)
- elasticsearch-api (= 7.6.0)
- elasticsearch-transport (= 7.6.0)
- elasticsearch-api (7.6.0)
+ ed25519 (1.2.4)
+ elasticsearch (7.8.0)
+ elasticsearch-api (= 7.8.0)
+ elasticsearch-transport (= 7.8.0)
+ elasticsearch-api (7.8.0)
multi_json
elasticsearch-dsl (0.1.9)
- elasticsearch-transport (7.6.0)
+ elasticsearch-transport (7.8.0)
faraday (~> 1)
multi_json
encryptor (3.0.0)
@@ -215,9 +209,9 @@ GEM
erubi (1.9.0)
et-orbi (1.2.4)
tzinfo
- excon (0.73.0)
+ excon (0.75.0)
fabrication (2.21.1)
- faker (2.11.0)
+ faker (2.13.0)
i18n (>= 1.6, < 2)
faraday (1.0.1)
multipart-post (>= 1.2, < 3)
@@ -235,24 +229,19 @@ GEM
fog-json (1.2.0)
fog-core
multi_json (~> 1.10)
- fog-openstack (0.3.7)
+ fog-openstack (0.3.10)
fog-core (>= 1.45, <= 2.1.0)
fog-json (>= 1.0)
ipaddress (>= 0.8)
formatador (0.2.5)
- fugit (1.3.5)
+ fugit (1.3.6)
et-orbi (~> 1.1, >= 1.1.8)
- raabro (~> 1.1)
+ raabro (~> 1.3)
fuubar (2.5.0)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
globalid (0.4.2)
activesupport (>= 4.2.0)
- goldfinger (2.1.1)
- addressable (~> 2.5)
- http (~> 4.0)
- nokogiri (~> 1.8)
- oj (~> 3.0)
hamlit (2.11.0)
temple (>= 0.8.2)
thor
@@ -281,10 +270,10 @@ GEM
http-parser (1.2.1)
ffi-compiler (>= 1.0, < 2.0)
http_accept_language (2.1.1)
- httplog (1.4.2)
+ httplog (1.4.3)
rack (>= 1.0)
rainbow (>= 2.0.0)
- i18n (1.8.2)
+ i18n (1.8.3)
concurrent-ruby (~> 1.0)
i18n-tasks (0.9.31)
activesupport (>= 4.0.2)
@@ -299,9 +288,8 @@ GEM
idn-ruby (0.1.0)
ipaddress (0.8.3)
iso-639 (0.3.5)
- jaro_winkler (1.5.4)
jmespath (1.4.0)
- json (2.3.0)
+ json (2.3.1)
json-canonicalization (0.2.0)
json-ld (3.1.4)
htmlentities (~> 4.3)
@@ -310,23 +298,23 @@ GEM
multi_json (~> 1.14)
rack (~> 2.0)
rdf (~> 3.1)
- json-ld-preloaded (3.1.2)
+ json-ld-preloaded (3.1.3)
json-ld (~> 3.1)
rdf (~> 3.1)
jsonapi-renderer (0.2.2)
jwt (2.2.1)
- kaminari (1.2.0)
+ kaminari (1.2.1)
activesupport (>= 4.1.0)
- kaminari-actionview (= 1.2.0)
- kaminari-activerecord (= 1.2.0)
- kaminari-core (= 1.2.0)
- kaminari-actionview (1.2.0)
+ kaminari-actionview (= 1.2.1)
+ kaminari-activerecord (= 1.2.1)
+ kaminari-core (= 1.2.1)
+ kaminari-actionview (1.2.1)
actionview
- kaminari-core (= 1.2.0)
- kaminari-activerecord (1.2.0)
+ kaminari-core (= 1.2.1)
+ kaminari-activerecord (1.2.1)
activerecord
- kaminari-core (= 1.2.0)
- kaminari-core (1.2.0)
+ kaminari-core (= 1.2.1)
+ kaminari-core (1.2.1)
launchy (2.5.0)
addressable (~> 2.7)
letter_opener (1.7.0)
@@ -341,7 +329,7 @@ GEM
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
- loofah (2.5.0)
+ loofah (2.6.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@@ -359,11 +347,11 @@ GEM
nokogiri (~> 1.10)
mime-types (3.3.1)
mime-types-data (~> 3.2015)
- mime-types-data (3.2020.0425)
+ mime-types-data (3.2020.0512)
mimemagic (0.3.5)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
- minitest (5.14.0)
+ minitest (5.14.1)
msgpack (1.3.3)
multi_json (1.14.1)
multipart-post (2.1.1)
@@ -371,7 +359,7 @@ GEM
net-ldap (0.16.2)
net-scp (3.0.0)
net-ssh (>= 2.6.5, < 7.0.0)
- net-ssh (6.0.2)
+ net-ssh (6.1.0)
nio4r (2.5.2)
nokogiri (1.10.9)
mini_portile2 (~> 2.4.0)
@@ -390,9 +378,9 @@ GEM
addressable (~> 2.3)
nokogiri (~> 1.5)
omniauth (~> 1.2)
- omniauth-saml (1.10.1)
+ omniauth-saml (1.10.2)
omniauth (~> 1.3, >= 1.3.2)
- ruby-saml (~> 1.7)
+ ruby-saml (~> 1.9)
orm_adapter (0.5.0)
ox (2.13.2)
paperclip (6.0.0)
@@ -404,17 +392,17 @@ GEM
paperclip-av-transcoder (0.6.4)
av (~> 0.9.0)
paperclip (>= 2.5.2)
- parallel (1.19.1)
- parallel_tests (2.32.0)
+ parallel (1.19.2)
+ parallel_tests (3.0.0)
parallel
- parser (2.7.1.2)
- ast (~> 2.4.0)
+ parser (2.7.1.4)
+ ast (~> 2.4.1)
parslet (2.0.0)
pastel (0.7.4)
equatable (~> 0.6)
tty-color (~> 0.5)
pg (1.2.3)
- pghero (2.4.2)
+ pghero (2.5.1)
activerecord (>= 5)
pkg-config (1.4.1)
premailer (1.11.1)
@@ -434,39 +422,37 @@ GEM
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.5)
- puma (4.3.3)
+ puma (4.3.5)
nio4r (~> 2.0)
pundit (2.1.0)
activesupport (>= 3.0.0)
raabro (1.3.1)
- rack (2.2.2)
- rack-attack (6.3.0)
+ rack (2.2.3)
+ rack-attack (6.3.1)
rack (>= 1.0, < 3)
rack-cors (1.1.1)
rack (>= 2.0.0)
- rack-protection (2.0.8.1)
- rack
rack-proxy (0.6.5)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
- rails (5.2.4.2)
- actioncable (= 5.2.4.2)
- actionmailer (= 5.2.4.2)
- actionpack (= 5.2.4.2)
- actionview (= 5.2.4.2)
- activejob (= 5.2.4.2)
- activemodel (= 5.2.4.2)
- activerecord (= 5.2.4.2)
- activestorage (= 5.2.4.2)
- activesupport (= 5.2.4.2)
+ 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.2)
+ railties (= 5.2.4.3)
sprockets-rails (>= 2.0.0)
- rails-controller-testing (1.0.4)
- actionpack (>= 5.0.1.x)
- actionview (>= 5.0.1.x)
- activesupport (>= 5.0.1.x)
+ rails-controller-testing (1.0.5)
+ actionpack (>= 5.0.1.rc1)
+ actionview (>= 5.0.1.rc1)
+ activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
@@ -477,20 +463,20 @@ GEM
railties (>= 5.0, < 6)
rails-settings-cached (0.6.6)
rails (>= 4.2.0)
- railties (5.2.4.2)
- actionpack (= 5.2.4.2)
- activesupport (= 5.2.4.2)
+ railties (5.2.4.3)
+ actionpack (= 5.2.4.3)
+ activesupport (= 5.2.4.3)
method_source
rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0)
rainbow (3.0.0)
rake (13.0.1)
- rdf (3.1.1)
+ rdf (3.1.4)
hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.4.0)
rdf (~> 3.1)
- redis (4.1.4)
+ redis (4.2.1)
redis-actionpack (5.2.0)
actionpack (>= 5, < 7)
redis-rack (>= 2.1.0, < 3)
@@ -507,12 +493,12 @@ GEM
redis-actionpack (>= 5.0, < 6)
redis-activesupport (>= 5.0, < 6)
redis-store (>= 1.2, < 2)
- redis-store (1.8.2)
+ redis-store (1.9.0)
redis (>= 4, < 5)
- regexp_parser (1.7.0)
+ regexp_parser (1.7.1)
request_store (1.5.0)
rack (>= 1.4)
- responders (3.0.0)
+ responders (3.0.1)
actionpack (>= 5.0)
railties (>= 5.0)
rexml (3.2.4)
@@ -530,7 +516,7 @@ GEM
rspec-mocks (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
- rspec-rails (4.0.0)
+ rspec-rails (4.0.1)
actionpack (>= 4.2)
activesupport (>= 4.2)
railties (>= 4.2)
@@ -538,40 +524,42 @@ GEM
rspec-expectations (~> 3.9)
rspec-mocks (~> 3.9)
rspec-support (~> 3.9)
- rspec-sidekiq (3.0.3)
+ rspec-sidekiq (3.1.0)
rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0)
rspec-support (3.9.3)
rspec_junit_formatter (0.4.1)
rspec-core (>= 2, < 4, != 2.12.0)
- rubocop (0.82.0)
- jaro_winkler (~> 1.5.1)
+ rubocop (0.86.0)
parallel (~> 1.10)
parser (>= 2.7.0.1)
rainbow (>= 2.2.2, < 4.0)
+ regexp_parser (>= 1.7)
rexml
+ rubocop-ast (>= 0.0.3, < 1.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 2.0)
- rubocop-rails (2.5.2)
- activesupport
+ rubocop-ast (0.1.0)
+ parser (>= 2.7.0.1)
+ rubocop-rails (2.6.0)
+ activesupport (>= 4.2.0)
rack (>= 1.1)
- rubocop (>= 0.72.0)
+ rubocop (>= 0.82.0)
ruby-progressbar (1.10.1)
ruby-saml (1.11.0)
nokogiri (>= 1.5.10)
rufus-scheduler (3.6.0)
fugit (~> 1.1, >= 1.1.6)
safe_yaml (1.0.5)
- sanitize (5.1.0)
+ sanitize (5.2.1)
crass (~> 1.0.2)
nokogiri (>= 1.8.0)
nokogumbo (~> 2.0)
semantic_range (2.3.0)
- sidekiq (6.0.7)
+ sidekiq (6.1.0)
connection_pool (>= 2.2.2)
rack (~> 2.0)
- rack-protection (>= 2.0.0)
- redis (>= 4.1.0)
+ redis (>= 4.2.0)
sidekiq-bulk (0.2.0)
sidekiq
sidekiq-scheduler (3.0.1)
@@ -581,7 +569,7 @@ GEM
sidekiq (>= 3)
thwait
tilt (>= 1.4.0)
- sidekiq-unique-jobs (6.0.21)
+ sidekiq-unique-jobs (6.0.22)
concurrent-ruby (~> 1.0, >= 1.0.5)
sidekiq (>= 4.0, < 7.0)
thor (~> 0)
@@ -609,7 +597,7 @@ GEM
stoplight (2.2.0)
streamio-ffmpeg (3.0.2)
multi_json (~> 1.8)
- strong_migrations (0.6.6)
+ strong_migrations (0.6.8)
activerecord (>= 5)
temple (0.8.2)
terminal-table (1.8.0)
@@ -621,8 +609,6 @@ GEM
thwait (0.1.0)
tilt (2.0.10)
tty-color (0.5.1)
- tty-command (0.9.0)
- pastel (~> 0.7.0)
tty-cursor (0.7.1)
tty-prompt (0.21.0)
necromancer (~> 0.5.0)
@@ -632,7 +618,7 @@ GEM
tty-cursor (~> 0.7)
tty-screen (~> 0.7)
wisper (~> 2.0.0)
- tty-screen (0.7.1)
+ tty-screen (0.8.0)
twitter-text (1.14.7)
unf (~> 0.1.0)
tzinfo (1.2.7)
@@ -658,9 +644,9 @@ GEM
webpush (0.3.8)
hkdf (~> 0.2)
jwt (~> 2.0)
- websocket-driver (0.7.1)
+ websocket-driver (0.7.2)
websocket-extensions (>= 0.1.0)
- websocket-extensions (0.1.4)
+ websocket-extensions (0.1.5)
wisper (2.0.1)
xpath (3.2.0)
nokogiri (~> 1.8)
@@ -673,7 +659,7 @@ DEPENDENCIES
active_record_query_trace (~> 1.7)
addressable (~> 2.7)
annotate (~> 3.1)
- aws-sdk-s3 (~> 1.64)
+ aws-sdk-s3 (~> 1.73)
better_errors (~> 2.7)
binding_of_caller (~> 0.7)
blurhash (~> 0.1)
@@ -681,16 +667,17 @@ DEPENDENCIES
brakeman (~> 4.8)
browser
bullet (~> 6.1)
- bundler-audit (~> 0.6)
+ bundler-audit (~> 0.7)
capistrano (~> 3.14)
- capistrano-rails (~> 1.4)
+ capistrano-rails (~> 1.5)
capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0)
- capybara (~> 3.32)
+ capybara (~> 3.33)
charlock_holmes (~> 0.7.7)
chewy (~> 5.1)
cld3 (~> 3.3.0)
climate_control (~> 0.2)
+ color_diff (~> 0.1)
concurrent-ruby
connection_pool
devise (~> 4.7)
@@ -700,22 +687,21 @@ DEPENDENCIES
doorkeeper (~> 5.4)
dotenv-rails (~> 2.7)
e2mmap (~> 0.1.0)
+ ed25519 (~> 1.2)
fabrication (~> 2.21)
- faker (~> 2.11)
+ faker (~> 2.13)
fast_blank (~> 1.0)
fastimage
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)
http_accept_language (~> 2.1)
- http_parser.rb (~> 0.6)!
- httplog (~> 1.4.2)
+ httplog (~> 1.4.3)
i18n-tasks (~> 0.9)
idn-ruby
iso-639
@@ -743,10 +729,10 @@ DEPENDENCIES
paperclip (~> 6.0)
paperclip-av-transcoder (~> 0.6)
parallel (~> 1.19)
- parallel_tests (~> 2.32)
+ parallel_tests (~> 3.0)
parslet
pg (~> 1.2)
- pghero (~> 2.4)
+ pghero (~> 2.5)
pkg-config (~> 1.4)
posix-spawn!
premailer-rails
@@ -755,25 +741,25 @@ DEPENDENCIES
pry-rails (~> 0.3)
puma (~> 4.3)
pundit (~> 2.1)
- rack (~> 2.2.2)
+ rack (~> 2.2.3)
rack-attack (~> 6.3)
rack-cors (~> 1.1)
- rails (~> 5.2.4.2)
+ rails (~> 5.2.4.3)
rails-controller-testing (~> 1.0)
rails-i18n (~> 5.1)
rails-settings-cached (~> 0.6)
rdf-normalize (~> 0.4)
- redis (~> 4.1)
+ redis (~> 4.2)
redis-namespace (~> 1.7)
redis-rails (~> 5.0)
rqrcode (~> 1.1)
rspec-rails (~> 4.0)
- rspec-sidekiq (~> 3.0)
+ rspec-sidekiq (~> 3.1)
rspec_junit_formatter (~> 0.4)
- rubocop (~> 0.82)
- rubocop-rails (~> 2.5)
+ rubocop (~> 0.86)
+ rubocop-rails (~> 2.6)
ruby-progressbar (~> 1.10)
- sanitize (~> 5.1)
+ sanitize (~> 5.2)
sidekiq (~> 6.0)
sidekiq-bulk (~> 0.2.0)
sidekiq-scheduler (~> 3.0)
@@ -789,7 +775,6 @@ DEPENDENCIES
strong_migrations (~> 0.6)
thor (~> 0.20)
thwait (~> 0.1.0)
- tty-command (~> 0.9)
tty-prompt (~> 0.21)
twitter-text (~> 1.14)
tzinfo-data (~> 1.2020)
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000000000..7625597fe1df09
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,12 @@
+# Security Policy
+
+## Supported Versions
+
+| Version | Supported |
+| ------- | ------------------ |
+| 3.1.x | :white_check_mark: |
+| < 3.1 | :x: |
+
+## Reporting a Vulnerability
+
+hello@joinmastodon.org
diff --git a/app.json b/app.json
index 211f17d812e221..e4f7cf403ec4a0 100644
--- a/app.json
+++ b/app.json
@@ -88,9 +88,6 @@
{
"url": "https://github.com/heroku/heroku-buildpack-apt"
},
- {
- "url": "heroku/nodejs"
- },
{
"url": "heroku/ruby"
}
diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb
index bec9ed88b22b94..47cb856ea944a4 100644
--- a/app/chewy/statuses_index.rb
+++ b/app/chewy/statuses_index.rb
@@ -31,9 +31,9 @@ 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).pluck(:status_id, :account_id)
+ 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) }
end
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index db9f45b4e9270f..b42f711f09d79d 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -1,16 +1,18 @@
# frozen_string_literal: true
class AccountsController < ApplicationController
- PAGE_SIZE = 20
+ PAGE_SIZE = 20
+ PAGE_SIZE_MAX = 200
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
skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) }
- skip_before_action :require_functional!
+ skip_before_action :require_functional!, unless: :whitelist_mode?
def show
respond_to do |format|
@@ -40,14 +42,15 @@ def show
format.rss do
expires_in 1.minute, public: true
- @statuses = filtered_statuses.without_reblogs.limit(PAGE_SIZE)
+ limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
+ @statuses = filtered_statuses.without_reblogs.limit(limit)
@statuses = cache_collection(@statuses, Status)
render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag])
end
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
@@ -79,7 +82,7 @@ def only_media_scope
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
@@ -147,12 +150,4 @@ def filtered_status_page
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
diff --git a/app/controllers/activitypub/claims_controller.rb b/app/controllers/activitypub/claims_controller.rb
new file mode 100644
index 00000000000000..08ad952df14d9d
--- /dev/null
+++ b/app/controllers/activitypub/claims_controller.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class ActivityPub::ClaimsController < ActivityPub::BaseController
+ include SignatureVerification
+ include AccountOwnedConcern
+
+ skip_before_action :authenticate_user!
+
+ before_action :require_signature!
+ before_action :set_claim_result
+
+ def create
+ render json: @claim_result, serializer: ActivityPub::OneTimeKeySerializer
+ end
+
+ private
+
+ def set_claim_result
+ @claim_result = ::Keys::ClaimService.new.call(@account.id, params[:id])
+ end
+end
diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb
index c1e7aa550239bc..380de54f5dc413 100644
--- a/app/controllers/activitypub/collections_controller.rb
+++ b/app/controllers/activitypub/collections_controller.rb
@@ -5,8 +5,9 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
include AccountOwnedConcern
before_action :require_signature!, if: :authorized_fetch_mode?
+ before_action :set_items
before_action :set_size
- before_action :set_statuses
+ before_action :set_type
before_action :set_cache_headers
def show
@@ -16,40 +17,53 @@ def show
private
- def set_statuses
- @statuses = scope_for_collection
- @statuses = cache_collection(@statuses, Status)
+ 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, Status)
+ end
+ end
+ when 'devices'
+ @items = @account.devices
+ else
+ not_found
+ end
end
def set_size
case params[:id]
- when 'featured'
- @size = @account.pinned_statuses.count
+ when 'featured', 'devices'
+ @size = @items.size
else
not_found
end
end
- def scope_for_collection
+ def set_type
case params[:id]
when 'featured'
- # 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)))
- Status.none
- else
- @account.pinned_statuses
- end
+ @type = :ordered
+ when 'devices'
+ @type = :unordered
+ else
+ not_found
end
end
def collection_presenter
ActivityPub::CollectionPresenter.new(
id: account_collection_url(@account, params[:id]),
- type: :ordered,
+ type: @type,
size: @size,
- items: @statuses
+ items: @items
)
end
end
diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb
index efa8f29505d257..71efb543e1aef4 100644
--- a/app/controllers/admin/custom_emojis_controller.rb
+++ b/app/controllers/admin/custom_emojis_controller.rb
@@ -33,6 +33,8 @@ def batch
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
+ rescue Mastodon::NotPermittedError
+ flash[:alert] = I18n.t('admin.custom_emojis.not_permitted')
ensure
redirect_to admin_custom_emojis_path(filter_params)
end
diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb
index 65019503469166..d7c192f0d6c9ed 100644
--- a/app/controllers/admin/statuses_controller.rb
+++ b/app/controllers/admin/statuses_controller.rb
@@ -14,7 +14,7 @@ def index
@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
diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb
index 153ade253d69b0..055cd416d283bb 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -7,7 +7,7 @@ class Api::BaseController < ApplicationController
include RateLimitHeaders
skip_before_action :store_current_location
- skip_before_action :require_functional!
+ skip_before_action :require_functional!, unless: :whitelist_mode?
before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access?
before_action :set_cache_headers
@@ -102,7 +102,7 @@ def require_user!
elsif !current_user.approved?
render json: { error: 'Your login is currently pending approval' }, status: 403
else
- set_user_activity
+ update_user_sign_in
end
end
diff --git a/app/controllers/api/v1/accounts/notes_controller.rb b/app/controllers/api/v1/accounts/notes_controller.rb
new file mode 100644
index 00000000000000..032e807d11ff94
--- /dev/null
+++ b/app/controllers/api/v1/accounts/notes_controller.rb
@@ -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
diff --git a/app/controllers/api/v1/crypto/deliveries_controller.rb b/app/controllers/api/v1/crypto/deliveries_controller.rb
new file mode 100644
index 00000000000000..aa9df6e03b20f2
--- /dev/null
+++ b/app/controllers/api/v1/crypto/deliveries_controller.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class Api::V1::Crypto::DeliveriesController < Api::BaseController
+ before_action -> { doorkeeper_authorize! :crypto }
+ before_action :require_user!
+ before_action :set_current_device
+
+ def create
+ devices.each do |device_params|
+ DeliverToDeviceService.new.call(current_account, @current_device, device_params)
+ end
+
+ render_empty
+ end
+
+ private
+
+ def set_current_device
+ @current_device = Device.find_by!(access_token: doorkeeper_token)
+ end
+
+ def resource_params
+ params.require(:device)
+ params.permit(device: [:account_id, :device_id, :type, :body, :hmac])
+ end
+
+ def devices
+ Array(resource_params[:device])
+ end
+end
diff --git a/app/controllers/api/v1/crypto/encrypted_messages_controller.rb b/app/controllers/api/v1/crypto/encrypted_messages_controller.rb
new file mode 100644
index 00000000000000..c764915e578e19
--- /dev/null
+++ b/app/controllers/api/v1/crypto/encrypted_messages_controller.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController
+ LIMIT = 80
+
+ before_action -> { doorkeeper_authorize! :crypto }
+ before_action :require_user!
+ before_action :set_current_device
+
+ before_action :set_encrypted_messages, only: :index
+ after_action :insert_pagination_headers, only: :index
+
+ def index
+ render json: @encrypted_messages, each_serializer: REST::EncryptedMessageSerializer
+ end
+
+ def clear
+ @current_device.encrypted_messages.up_to(params[:up_to_id]).delete_all
+ render_empty
+ end
+
+ private
+
+ def set_current_device
+ @current_device = Device.find_by!(access_token: doorkeeper_token)
+ 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))
+ end
+
+ def insert_pagination_headers
+ set_pagination_headers(next_path, prev_path)
+ end
+
+ def next_path
+ api_v1_crypto_encrypted_messages_url pagination_params(max_id: pagination_max_id) if records_continue?
+ end
+
+ def prev_path
+ api_v1_crypto_encrypted_messages_url pagination_params(min_id: pagination_since_id) unless @encrypted_messages.empty?
+ end
+
+ def pagination_max_id
+ @encrypted_messages.last.id
+ end
+
+ def pagination_since_id
+ @encrypted_messages.first.id
+ end
+
+ def records_continue?
+ @encrypted_messages.size == limit_param(LIMIT)
+ end
+
+ def pagination_params(core_params)
+ params.slice(:limit).permit(:limit).merge(core_params)
+ end
+end
diff --git a/app/controllers/api/v1/crypto/keys/claims_controller.rb b/app/controllers/api/v1/crypto/keys/claims_controller.rb
new file mode 100644
index 00000000000000..34b21a38096a97
--- /dev/null
+++ b/app/controllers/api/v1/crypto/keys/claims_controller.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class Api::V1::Crypto::Keys::ClaimsController < Api::BaseController
+ before_action -> { doorkeeper_authorize! :crypto }
+ before_action :require_user!
+ before_action :set_claim_results
+
+ def create
+ render json: @claim_results, each_serializer: REST::Keys::ClaimResultSerializer
+ end
+
+ 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
+ end
+
+ def resource_params
+ params.permit(device: [:account_id, :device_id])
+ end
+
+ def devices
+ Array(resource_params[:device])
+ end
+end
diff --git a/app/controllers/api/v1/crypto/keys/counts_controller.rb b/app/controllers/api/v1/crypto/keys/counts_controller.rb
new file mode 100644
index 00000000000000..ffd7151b78291e
--- /dev/null
+++ b/app/controllers/api/v1/crypto/keys/counts_controller.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class Api::V1::Crypto::Keys::CountsController < Api::BaseController
+ before_action -> { doorkeeper_authorize! :crypto }
+ before_action :require_user!
+ before_action :set_current_device
+
+ def show
+ render json: { one_time_keys: @current_device.one_time_keys.count }
+ end
+
+ private
+
+ def set_current_device
+ @current_device = Device.find_by!(access_token: doorkeeper_token)
+ end
+end
diff --git a/app/controllers/api/v1/crypto/keys/queries_controller.rb b/app/controllers/api/v1/crypto/keys/queries_controller.rb
new file mode 100644
index 00000000000000..0851d797d33b8d
--- /dev/null
+++ b/app/controllers/api/v1/crypto/keys/queries_controller.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class Api::V1::Crypto::Keys::QueriesController < Api::BaseController
+ before_action -> { doorkeeper_authorize! :crypto }
+ before_action :require_user!
+ before_action :set_accounts
+ before_action :set_query_results
+
+ def create
+ render json: @query_results, each_serializer: REST::Keys::QueryResultSerializer
+ end
+
+ private
+
+ def set_accounts
+ @accounts = Account.where(id: account_ids).includes(:devices)
+ end
+
+ def set_query_results
+ @query_results = @accounts.map { |account| ::Keys::QueryService.new.call(account) }.compact
+ end
+
+ def account_ids
+ Array(params[:id]).map(&:to_i)
+ end
+end
diff --git a/app/controllers/api/v1/crypto/keys/uploads_controller.rb b/app/controllers/api/v1/crypto/keys/uploads_controller.rb
new file mode 100644
index 00000000000000..fc4abf63b3a421
--- /dev/null
+++ b/app/controllers/api/v1/crypto/keys/uploads_controller.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class Api::V1::Crypto::Keys::UploadsController < Api::BaseController
+ before_action -> { doorkeeper_authorize! :crypto }
+ before_action :require_user!
+
+ def create
+ device = Device.find_or_initialize_by(access_token: doorkeeper_token)
+
+ device.transaction do
+ device.account = current_account
+ device.update!(resource_params[:device])
+
+ if resource_params[:one_time_keys].present? && resource_params[:one_time_keys].is_a?(Enumerable)
+ resource_params[:one_time_keys].each do |one_time_key_params|
+ device.one_time_keys.create!(one_time_key_params)
+ end
+ end
+ end
+
+ render json: device, serializer: REST::Keys::DeviceSerializer
+ end
+
+ private
+
+ def resource_params
+ params.permit(device: [:device_id, :name, :fingerprint_key, :identity_key], one_time_keys: [:key_id, :key, :signature])
+ end
+end
diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb
index 0bb3d0d27b7ec2..a2a919a3e6f256 100644
--- a/app/controllers/api/v1/media_controller.rb
+++ b/app/controllers/api/v1/media_controller.rb
@@ -39,7 +39,7 @@ def check_processing
end
def media_attachment_params
- params.permit(:file, :description, :focus)
+ params.permit(:file, :thumbnail, :description, :focus)
end
def file_type_error
diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb
index 7fa774a4d727b6..1be15a5a439604 100644
--- a/app/controllers/api/v1/statuses/reblogs_controller.rb
+++ b/app/controllers/api/v1/statuses/reblogs_controller.rb
@@ -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 @@ def create
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
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index 8d6cb84b6675f0..106fc8224e2876 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -57,6 +57,7 @@ def destroy
@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
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 973db6aca97654..2201e463e68d05 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -55,7 +55,7 @@ def public_fetch_mode?
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!
diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb
index e95909447ce733..78333845048fa0 100644
--- a/app/controllers/auth/sessions_controller.rb
+++ b/app/controllers/auth/sessions_controller.rb
@@ -7,8 +7,10 @@ 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 :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
+ include TwoFactorAuthenticationConcern
+ include SignInTokenAuthenticationConcern
before_action :set_instance_presenter, only: [:new]
before_action :set_body_classes
@@ -23,6 +25,7 @@ def new
def create
super do |resource|
+ resource.update_sign_in!(request, new_sign_in: true)
remember_me(resource)
flash.delete(:notice)
end
@@ -39,17 +42,18 @@ def destroy
protected
def find_user
- if session[:otp_user_id]
- User.find(session[:otp_user_id])
+ if 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)
+ params.require(:user).permit(:email, :password, :otp_attempt, :sign_in_token_attempt)
end
def after_sign_in_path_for(resource)
@@ -70,49 +74,9 @@ def after_sign_out_path_for(_resource_or_scope)
super
end
- def two_factor_enabled?
- find_user&.otp_required_for_login?
- end
-
- def valid_otp_attempt?(user)
- user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
- user.invalidate_otp_backup_code!(user_params[:otp_attempt])
- rescue OpenSSL::Cipher::CipherError
- false
- end
-
- def authenticate_with_two_factor
- user = self.resource = find_user
-
- if user_params[:otp_attempt].present? && session[:otp_user_id]
- authenticate_with_two_factor_via_otp(user)
- elsif user.present? && (user.encrypted_password.blank? || user.valid_password?(user_params[:password]))
- # If encrypted_password is blank, we got the user from LDAP or PAM,
- # so credentials are already valid
-
- prompt_for_two_factor(user)
- end
- end
-
- def authenticate_with_two_factor_via_otp(user)
- if valid_otp_attempt?(user)
- session.delete(:otp_user_id)
- remember_me(user)
- sign_in(user)
- else
- flash.now[:alert] = I18n.t('users.invalid_otp_token')
- prompt_for_two_factor(user)
- end
- end
-
- def prompt_for_two_factor(user)
- session[:otp_user_id] = user.id
- @body_classes = 'lighter'
- render :two_factor
- end
-
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')
@@ -130,13 +94,30 @@ def set_body_classes
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
diff --git a/app/controllers/concerns/localized.rb b/app/controllers/concerns/localized.rb
index d1384ed56ff38f..fe1142f34530d1 100644
--- a/app/controllers/concerns/localized.rb
+++ b/app/controllers/concerns/localized.rb
@@ -7,8 +7,6 @@ module Localized
around_action :set_locale
end
- private
-
def set_locale
locale = current_user.locale if respond_to?(:user_signed_in?) && user_signed_in?
locale ||= session[:locale] ||= default_locale
@@ -19,6 +17,8 @@ def set_locale
end
end
+ private
+
def default_locale
if ENV['DEFAULT_LOCALE'].present?
I18n.default_locale
diff --git a/app/controllers/concerns/sign_in_token_authentication_concern.rb b/app/controllers/concerns/sign_in_token_authentication_concern.rb
new file mode 100644
index 00000000000000..3c95a4afd2cf66
--- /dev/null
+++ b/app/controllers/concerns/sign_in_token_authentication_concern.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module SignInTokenAuthenticationConcern
+ extend ActiveSupport::Concern
+
+ included do
+ prepend_before_action :authenticate_with_sign_in_token, if: :sign_in_token_required?, only: [:create]
+ end
+
+ def sign_in_token_required?
+ find_user&.suspicious_sign_in?(request.remote_ip)
+ end
+
+ def valid_sign_in_token_attempt?(user)
+ Devise.secure_compare(user.sign_in_token, user_params[:sign_in_token_attempt])
+ end
+
+ def authenticate_with_sign_in_token
+ user = self.resource = find_user
+
+ 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)
+ end
+ end
+
+ def authenticate_with_sign_in_token_attempt(user)
+ if valid_sign_in_token_attempt?(user)
+ clear_attempt_from_session
+ remember_me(user)
+ sign_in(user)
+ else
+ flash.now[:alert] = I18n.t('users.invalid_sign_in_token')
+ prompt_for_sign_in_token(user)
+ end
+ end
+
+ def prompt_for_sign_in_token(user)
+ if user.sign_in_token_expired?
+ user.generate_sign_in_token && user.save
+ UserMailer.sign_in_token(user, request.remote_ip, request.user_agent, Time.now.utc.to_s).deliver_later!
+ end
+
+ set_attempt_session(user)
+
+ @body_classes = 'lighter'
+
+ set_locale { render :sign_in_token }
+ end
+end
diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb
index 10efbf2e0b8bef..18f549de9439b1 100644
--- a/app/controllers/concerns/signature_verification.rb
+++ b/app/controllers/concerns/signature_verification.rb
@@ -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,40 @@ def signature_verification_failure_code
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
-
- 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
+ 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?
- 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!
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,6 +105,31 @@ def request_body
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' unless 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_signature(account, signature, compare_signed_string)
if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
@signed_request_account = account
@@ -108,12 +139,20 @@ def verify_signature(account, signature, compare_signed_string)
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 == '(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']}"
elsif signed_header == 'digest'
"digest: #{body_digest}"
else
@@ -123,13 +162,28 @@ def build_signed_string(signed_headers)
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
@@ -140,9 +194,8 @@ 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)
diff --git a/app/controllers/concerns/two_factor_authentication_concern.rb b/app/controllers/concerns/two_factor_authentication_concern.rb
new file mode 100644
index 00000000000000..f54a3c82920c9a
--- /dev/null
+++ b/app/controllers/concerns/two_factor_authentication_concern.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module TwoFactorAuthenticationConcern
+ extend ActiveSupport::Concern
+
+ included do
+ prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
+ end
+
+ def two_factor_enabled?
+ find_user&.otp_required_for_login?
+ end
+
+ def valid_otp_attempt?(user)
+ user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
+ user.invalidate_otp_backup_code!(user_params[:otp_attempt])
+ rescue OpenSSL::Cipher::CipherError
+ false
+ end
+
+ def authenticate_with_two_factor
+ user = self.resource = find_user
+
+ if user.present? && session[:attempt_user_id].present? && session[:attempt_user_updated_at] != user.updated_at.to_s
+ restart_session
+ elsif user_params[:otp_attempt].present? && session[:attempt_user_id]
+ authenticate_with_two_factor_attempt(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)
+ if valid_otp_attempt?(user)
+ clear_attempt_from_session
+ remember_me(user)
+ sign_in(user)
+ else
+ flash.now[:alert] = I18n.t('users.invalid_otp_token')
+ prompt_for_two_factor(user)
+ end
+ end
+
+ def prompt_for_two_factor(user)
+ set_attempt_session(user)
+
+ @body_classes = 'lighter'
+
+ set_locale { render :two_factor }
+ end
+end
diff --git a/app/controllers/concerns/user_tracking_concern.rb b/app/controllers/concerns/user_tracking_concern.rb
index be10705fcc1d8b..efda37fae75cb7 100644
--- a/app/controllers/concerns/user_tracking_concern.rb
+++ b/app/controllers/concerns/user_tracking_concern.rb
@@ -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?
diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb
index 750c835ddab471..f198ad5ba5bdb2 100644
--- a/app/controllers/directories_controller.rb
+++ b/app/controllers/directories_controller.rb
@@ -9,7 +9,7 @@ class DirectoriesController < ApplicationController
before_action :set_tag, only: :show
before_action :set_accounts
- skip_before_action :require_functional!
+ skip_before_action :require_functional!, unless: :whitelist_mode?
def index
render :index
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index 14e22dd1ec1bce..ab074996340354 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -8,7 +8,7 @@ class FollowerAccountsController < ApplicationController
before_action :set_cache_headers
skip_around_action :set_locale, if: -> { request.format == :json }
- skip_before_action :require_functional!
+ skip_before_action :require_functional!, unless: :whitelist_mode?
def index
respond_to do |format|
diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb
index 95849ffb983ac6..918bdac0a826e1 100644
--- a/app/controllers/following_accounts_controller.rb
+++ b/app/controllers/following_accounts_controller.rb
@@ -8,7 +8,7 @@ class FollowingAccountsController < ApplicationController
before_action :set_cache_headers
skip_around_action :set_locale, if: -> { request.format == :json }
- skip_before_action :require_functional!
+ skip_before_action :require_functional!, unless: :whitelist_mode?
def index
respond_to do |format|
diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb
index 1d166d6e73bfac..ce015dd1b216d3 100644
--- a/app/controllers/media_controller.rb
+++ b/app/controllers/media_controller.rb
@@ -4,7 +4,7 @@ class MediaController < ApplicationController
include Authorization
skip_before_action :store_current_location
- skip_before_action :require_functional!
+ skip_before_action :require_functional!, unless: :whitelist_mode?
before_action :authenticate_user!, if: :whitelist_mode?
before_action :set_media_attachment
diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb
index e36673fc40e0b2..0b1d09de935c3c 100644
--- a/app/controllers/media_proxy_controller.rb
+++ b/app/controllers/media_proxy_controller.rb
@@ -31,8 +31,8 @@ def show
private
def redownload!
- @media_attachment.file_remote_url = @media_attachment.remote_url
- @media_attachment.created_at = Time.now.utc
+ @media_attachment.download_file!
+ @media_attachment.created_at = Time.now.utc
@media_attachment.save!
end
diff --git a/app/controllers/remote_interaction_controller.rb b/app/controllers/remote_interaction_controller.rb
index 3b9202a5c71112..6c29a2b9ffed27 100644
--- a/app/controllers/remote_interaction_controller.rb
+++ b/app/controllers/remote_interaction_controller.rb
@@ -10,7 +10,7 @@ class RemoteInteractionController < ApplicationController
before_action :set_status
before_action :set_body_classes
- skip_before_action :require_functional!
+ skip_before_action :require_functional!, unless: :whitelist_mode?
def new
@remote_follow = RemoteFollow.new(session_params)
diff --git a/app/controllers/settings/migration/redirects_controller.rb b/app/controllers/settings/migration/redirects_controller.rb
index 6e5b72ffba2480..97193ade02d58d 100644
--- a/app/controllers/settings/migration/redirects_controller.rb
+++ b/app/controllers/settings/migration/redirects_controller.rb
@@ -18,7 +18,7 @@ def create
if @redirect.valid_with_challenge?(current_user)
current_account.update!(moved_to_account: @redirect.target_account)
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
- redirect_to settings_migration_path, notice: I18n.t('migrations.moved_msg', acct: current_account.moved_to_account.acct)
+ redirect_to settings_migration_path, notice: I18n.t('migrations.redirected_msg', acct: current_account.moved_to_account.acct)
else
render :new
end
diff --git a/app/controllers/settings/pictures_controller.rb b/app/controllers/settings/pictures_controller.rb
index 73926707bdb566..df2a6eed3ec13e 100644
--- a/app/controllers/settings/pictures_controller.rb
+++ b/app/controllers/settings/pictures_controller.rb
@@ -7,13 +7,8 @@ class PicturesController < BaseController
before_action :set_picture
def destroy
- if valid_picture
- account_params = {
- @picture => nil,
- (@picture + '_remote_url') => nil,
- }
-
- msg = UpdateAccountService.new.call(@account, account_params) ? I18n.t('generic.changes_saved_msg') : nil
+ if valid_picture?
+ msg = I18n.t('generic.changes_saved_msg') if UpdateAccountService.new.call(@account, { @picture => nil, "#{@picture}_remote_url" => '' })
redirect_to settings_profile_path, notice: msg, status: 303
else
bad_request
@@ -30,8 +25,8 @@ def set_picture
@picture = params[:id]
end
- def valid_picture
- @picture == 'avatar' || @picture == 'header'
+ def valid_picture?
+ %w(avatar header).include?(@picture)
end
end
end
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index d362b97dc6916c..17ddd31fbbf845 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -19,7 +19,7 @@ class StatusesController < ApplicationController
before_action :set_autoplay, only: :embed
skip_around_action :set_locale, if: -> { request.format == :json }
- skip_before_action :require_functional!, only: [:show, :embed]
+ skip_before_action :require_functional!, only: [:show, :embed], unless: :whitelist_mode?
content_security_policy only: :embed do |p|
p.frame_ancestors(false)
@@ -42,7 +42,7 @@ def show
def activity
expires_in 3.minutes, public: @status.distributable? && public_fetch_mode?
- render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter
+ render_with_cache json: ActivityPub::ActivityPresenter.from_status(@status), content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter
end
def embed
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index da0add71a91bb5..6426a7d695260f 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -3,7 +3,8 @@
class TagsController < ApplicationController
include SignatureVerification
- PAGE_SIZE = 20
+ PAGE_SIZE = 20
+ PAGE_SIZE_MAX = 200
layout 'public'
@@ -14,7 +15,7 @@ class TagsController < ApplicationController
before_action :set_body_classes
before_action :set_instance_presenter
- skip_before_action :require_functional!
+ skip_before_action :require_functional!, unless: :whitelist_mode?
def show
respond_to do |format|
@@ -25,7 +26,8 @@ def show
format.rss do
expires_in 0, public: true
- @statuses = HashtagQueryService.new.call(@tag, filter_params, nil, @local).limit(PAGE_SIZE)
+ limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
+ @statuses = HashtagQueryService.new.call(@tag, filter_params, nil, @local).limit(limit)
@statuses = cache_collection(@statuses, Status)
render xml: RSS::TagSerializer.render(@tag, @statuses)
diff --git a/app/controllers/well_known/webfinger_controller.rb b/app/controllers/well_known/webfinger_controller.rb
index 480e58f3f043fc..9de9db6ba8c1b4 100644
--- a/app/controllers/well_known/webfinger_controller.rb
+++ b/app/controllers/well_known/webfinger_controller.rb
@@ -8,7 +8,8 @@ class WebfingerController < ActionController::Base
before_action :set_account
before_action :check_account_suspension
- rescue_from ActiveRecord::RecordNotFound, ActionController::ParameterMissing, with: :not_found
+ rescue_from ActiveRecord::RecordNotFound, with: :not_found
+ rescue_from ActionController::ParameterMissing, WebfingerResource::InvalidRequest, with: :bad_request
def show
expires_in 3.days, public: true
@@ -37,6 +38,10 @@ def check_account_suspension
expires_in(3.minutes, public: true) && gone if @account.suspended?
end
+ def bad_request
+ head 400
+ end
+
def not_found
head 404
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index defd97609ac5cd..716df0baccd97e 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -77,6 +77,18 @@ def fa_icon(icon, attributes = {})
content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
end
+ def visibility_icon(status)
+ if status.public_visibility?
+ fa_icon('globe', title: I18n.t('statuses.visibilities.public'))
+ elsif status.unlisted_visibility?
+ fa_icon('unlock', title: I18n.t('statuses.visibilities.unlisted'))
+ elsif status.private_visibility? || status.limited_visibility?
+ fa_icon('lock', title: I18n.t('statuses.visibilities.private'))
+ elsif status.direct_visibility?
+ fa_icon('envelope', title: I18n.t('statuses.visibilities.direct'))
+ end
+ end
+
def custom_emoji_tag(custom_emoji, animate = true)
if animate
image_tag(custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:")
@@ -136,6 +148,11 @@ def render_initial_state
text: [params[:title], params[:text], params[:url]].compact.join(' '),
}
+ permit_visibilities = %w(public unlisted private direct)
+ default_privacy = current_account&.user&.setting_default_privacy
+ permit_visibilities.shift(permit_visibilities.index(default_privacy) + 1) if default_privacy.present?
+ state_params[:visibility] = params[:visibility] if permit_visibilities.include? params[:visibility]
+
if user_signed_in?
state_params[:settings] = state_params[:settings].merge(Web::Setting.find_by(user: current_user)&.data || {})
state_params[:push_subscription] = current_account.user.web_push_subscription(current_session)
diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb
index 866a9902c3e4da..a51597cf353e01 100644
--- a/app/helpers/statuses_helper.rb
+++ b/app/helpers/statuses_helper.rb
@@ -15,11 +15,13 @@ def nothing_here(extra_classes = '')
end
def media_summary(status)
- attachments = { image: 0, video: 0 }
+ attachments = { image: 0, video: 0, audio: 0 }
status.media_attachments.each do |media|
if media.video?
attachments[:video] += 1
+ elsif media.audio?
+ attachments[:audio] += 1
else
attachments[:image] += 1
end
diff --git a/app/helpers/webfinger_helper.rb b/app/helpers/webfinger_helper.rb
index 70c49321005166..482f4e19eabef0 100644
--- a/app/helpers/webfinger_helper.rb
+++ b/app/helpers/webfinger_helper.rb
@@ -2,18 +2,6 @@
module WebfingerHelper
def webfinger!(uri)
- hidden_service_uri = /\.(onion|i2p)(:\d+)?$/.match(uri)
-
- raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if !Rails.configuration.x.access_to_hidden_service && hidden_service_uri
-
- opts = {
- ssl: !hidden_service_uri,
-
- headers: {
- 'User-Agent': Mastodon::Version.user_agent,
- },
- }
-
- Goldfinger::Client.new(uri, opts.merge(Rails.configuration.x.http_client_proxy)).finger
+ Webfinger.new(uri).perform
end
end
diff --git a/app/javascript/mastodon/actions/account_notes.js b/app/javascript/mastodon/actions/account_notes.js
new file mode 100644
index 00000000000000..d1744100025aa9
--- /dev/null
+++ b/app/javascript/mastodon/actions/account_notes.js
@@ -0,0 +1,37 @@
+import api from '../api';
+
+export const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST';
+export const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS';
+export const ACCOUNT_NOTE_SUBMIT_FAIL = 'ACCOUNT_NOTE_SUBMIT_FAIL';
+
+export function submitAccountNote(id, value) {
+ return (dispatch, getState) => {
+ dispatch(submitAccountNoteRequest());
+
+ api(getState).post(`/api/v1/accounts/${id}/note`, {
+ comment: value,
+ }).then(response => {
+ dispatch(submitAccountNoteSuccess(response.data));
+ }).catch(error => dispatch(submitAccountNoteFail(error)));
+ };
+};
+
+export function submitAccountNoteRequest() {
+ return {
+ type: ACCOUNT_NOTE_SUBMIT_REQUEST,
+ };
+};
+
+export function submitAccountNoteSuccess(relationship) {
+ return {
+ type: ACCOUNT_NOTE_SUBMIT_SUCCESS,
+ relationship,
+ };
+};
+
+export function submitAccountNoteFail(error) {
+ return {
+ type: ACCOUNT_NOTE_SUBMIT_FAIL,
+ error,
+ };
+};
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 6b73fc90e09a18..030922520264f0 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -28,6 +28,11 @@ export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
+export const THUMBNAIL_UPLOAD_REQUEST = 'THUMBNAIL_UPLOAD_REQUEST';
+export const THUMBNAIL_UPLOAD_SUCCESS = 'THUMBNAIL_UPLOAD_SUCCESS';
+export const THUMBNAIL_UPLOAD_FAIL = 'THUMBNAIL_UPLOAD_FAIL';
+export const THUMBNAIL_UPLOAD_PROGRESS = 'THUMBNAIL_UPLOAD_PROGRESS';
+
export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
@@ -158,7 +163,6 @@ export function submitCompose(routerHistory) {
// To make the app more responsive, immediately push the status
// into the columns
-
const insertIfOnline = timelineId => {
const timeline = getState().getIn(['timelines', timelineId]);
@@ -174,6 +178,7 @@ export function submitCompose(routerHistory) {
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
insertIfOnline('community');
insertIfOnline('public');
+ insertIfOnline(`account:${response.data.account.id}`);
}
}).catch(function (error) {
dispatch(submitComposeFail(error));
@@ -260,6 +265,49 @@ export function uploadCompose(files) {
};
};
+export const uploadThumbnail = (id, file) => (dispatch, getState) => {
+ dispatch(uploadThumbnailRequest());
+
+ const total = file.size;
+ const data = new FormData();
+
+ data.append('thumbnail', file);
+
+ api(getState).put(`/api/v1/media/${id}`, data, {
+ onUploadProgress: ({ loaded }) => {
+ dispatch(uploadThumbnailProgress(loaded, total));
+ },
+ }).then(({ data }) => {
+ dispatch(uploadThumbnailSuccess(data));
+ }).catch(error => {
+ dispatch(uploadThumbnailFail(id, error));
+ });
+};
+
+export const uploadThumbnailRequest = () => ({
+ type: THUMBNAIL_UPLOAD_REQUEST,
+ skipLoading: true,
+});
+
+export const uploadThumbnailProgress = (loaded, total) => ({
+ type: THUMBNAIL_UPLOAD_PROGRESS,
+ loaded,
+ total,
+ skipLoading: true,
+});
+
+export const uploadThumbnailSuccess = media => ({
+ type: THUMBNAIL_UPLOAD_SUCCESS,
+ media,
+ skipLoading: true,
+});
+
+export const uploadThumbnailFail = error => ({
+ type: THUMBNAIL_UPLOAD_FAIL,
+ error,
+ skipLoading: true,
+});
+
export function changeUploadCompose(id, params) {
return (dispatch, getState) => {
dispatch(changeUploadComposeRequest());
@@ -278,6 +326,7 @@ export function changeUploadComposeRequest() {
skipLoading: true,
};
};
+
export function changeUploadComposeSuccess(media) {
return {
type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
diff --git a/app/javascript/mastodon/actions/dropdown_menu.js b/app/javascript/mastodon/actions/dropdown_menu.js
index 14f2939c78db9c..fb6e55612e708e 100644
--- a/app/javascript/mastodon/actions/dropdown_menu.js
+++ b/app/javascript/mastodon/actions/dropdown_menu.js
@@ -1,8 +1,8 @@
export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
-export function openDropdownMenu(id, placement, keyboard) {
- return { type: DROPDOWN_MENU_OPEN, id, placement, keyboard };
+export function openDropdownMenu(id, placement, keyboard, scroll_key) {
+ return { type: DROPDOWN_MENU_OPEN, id, placement, keyboard, scroll_key };
}
export function closeDropdownMenu(id) {
diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js
index 24b8368db9a82a..706a5824c3bd02 100644
--- a/app/javascript/mastodon/actions/importer/normalizer.js
+++ b/app/javascript/mastodon/actions/importer/normalizer.js
@@ -13,7 +13,7 @@ const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => {
export function searchTextFromRawStatus (status) {
const spoilerText = status.spoiler_text || '';
- const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).join('\n\n').replace(/
/g, '\n').replace(/<\/p>
/g, '\n\n');
+ const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/
/g, '\n').replace(/<\/p>
/g, '\n\n');
return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
}
diff --git a/app/javascript/mastodon/actions/markers.js b/app/javascript/mastodon/actions/markers.js
index c3a5fe86f1460c..37d1ddccfeecae 100644
--- a/app/javascript/mastodon/actions/markers.js
+++ b/app/javascript/mastodon/actions/markers.js
@@ -1,30 +1,102 @@
-export const submitMarkers = () => (dispatch, getState) => {
+import api from '../api';
+import { debounce } from 'lodash';
+import compareId from '../compare_id';
+import { showAlertForError } from './alerts';
+
+export const MARKERS_SUBMIT_SUCCESS = 'MARKERS_SUBMIT_SUCCESS';
+
+export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
const accessToken = getState().getIn(['meta', 'access_token'], '');
- const params = {};
+ const params = _buildParams(getState());
+
+ if (Object.keys(params).length === 0) {
+ return;
+ }
- const lastHomeId = getState().getIn(['timelines', 'home', 'items', 0]);
- const lastNotificationId = getState().getIn(['notifications', 'items', 0, 'id']);
+ // The Fetch API allows us to perform requests that will be carried out
+ // after the page closes. But that only works if the `keepalive` attribute
+ // is supported.
+ if (window.fetch && 'keepalive' in new Request('')) {
+ fetch('/api/v1/markers', {
+ keepalive: true,
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${accessToken}`,
+ },
+ body: JSON.stringify(params),
+ });
+ return;
+ } else if (navigator && navigator.sendBeacon) {
+ // Failing that, we can use sendBeacon, but we have to encode the data as
+ // FormData for DoorKeeper to recognize the token.
+ const formData = new FormData();
+ formData.append('bearer_token', accessToken);
+ for (const [id, value] of Object.entries(params)) {
+ formData.append(`${id}[last_read_id]`, value.last_read_id);
+ }
+ if (navigator.sendBeacon('/api/v1/markers', formData)) {
+ return;
+ }
+ }
- if (lastHomeId) {
+ // If neither Fetch nor sendBeacon worked, try to perform a synchronous
+ // request.
+ try {
+ const client = new XMLHttpRequest();
+
+ client.open('POST', '/api/v1/markers', false);
+ client.setRequestHeader('Content-Type', 'application/json');
+ client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
+ client.SUBMIT(JSON.stringify(params));
+ } catch (e) {
+ // Do not make the BeforeUnload handler error out
+ }
+};
+
+const _buildParams = (state) => {
+ const params = {};
+
+ const lastHomeId = state.getIn(['timelines', 'home', 'items', 0]);
+ const lastNotificationId = state.getIn(['notifications', 'items', 0, 'id']);
+
+ if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {
params.home = {
last_read_id: lastHomeId,
};
}
- if (lastNotificationId) {
+ if (lastNotificationId && compareId(lastNotificationId, state.getIn(['markers', 'notifications'])) > 0) {
params.notifications = {
last_read_id: lastNotificationId,
};
}
+ return params;
+};
+
+const debouncedSubmitMarkers = debounce((dispatch, getState) => {
+ const params = _buildParams(getState());
+
if (Object.keys(params).length === 0) {
return;
}
- const client = new XMLHttpRequest();
+ api().post('/api/v1/markers', params).then(() => {
+ dispatch(submitMarkersSuccess(params));
+ }).catch(error => {
+ dispatch(showAlertForError(error));
+ });
+}, 300000, { leading: true, trailing: true });
+
+export function submitMarkersSuccess({ home, notifications }) {
+ return {
+ type: MARKERS_SUBMIT_SUCCESS,
+ home: (home || {}).last_read_id,
+ notifications: (notifications || {}).last_read_id,
+ };
+};
- client.open('POST', '/api/v1/markers', false);
- client.setRequestHeader('Content-Type', 'application/json');
- client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
- client.send(JSON.stringify(params));
+export function submitMarkers() {
+ return (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState);
};
diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js
index 8a066b896a0c8d..a26844f84821d3 100644
--- a/app/javascript/mastodon/actions/notifications.js
+++ b/app/javascript/mastodon/actions/notifications.js
@@ -7,6 +7,7 @@ import {
importFetchedStatus,
importFetchedStatuses,
} from './importer';
+import { submitMarkers } from './markers';
import { saveSettings } from './settings';
import { defineMessages } from 'react-intl';
import { List as ImmutableList } from 'immutable';
@@ -70,6 +71,8 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
filtered = regex && regex.test(searchIndex);
}
+ dispatch(submitMarkers());
+
if (showInColumn) {
dispatch(importFetchedAccount(notification.account));
@@ -157,6 +160,7 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems));
fetchRelatedRelationships(dispatch, response.data);
+ dispatch(submitMarkers());
}).catch(error => {
dispatch(expandNotificationsFail(error, isLoadingMore));
}).finally(() => {
diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js
index 5640201c621f0f..e565e0b0ab59d5 100644
--- a/app/javascript/mastodon/actions/statuses.js
+++ b/app/javascript/mastodon/actions/statuses.js
@@ -3,7 +3,7 @@ import openDB from '../storage/db';
import { evictStatus } from '../storage/modifier';
import { deleteFromTimelines } from './timelines';
-import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer';
+import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus, importFetchedAccount } from './importer';
import { ensureComposeIsVisible } from './compose';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
@@ -155,6 +155,7 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
evictStatus(id);
dispatch(deleteStatusSuccess(id));
dispatch(deleteFromTimelines(id));
+ dispatch(importFetchedAccount(response.data.account));
if (withRedraft) {
dispatch(redraft(status, response.data.text));
diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js
index 080d665f4eaf85..d998fcac480238 100644
--- a/app/javascript/mastodon/actions/streaming.js
+++ b/app/javascript/mastodon/actions/streaming.js
@@ -74,6 +74,6 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) => connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`);
-export const connectHashtagStream = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept);
+export const connectHashtagStream = (id, tag, local, accept) => connectTimelineStream(`hashtag:${id}${local ? ':local' : ''}`, `hashtag${local ? ':local' : ''}&tag=${tag}`, null, accept);
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index 01f0fb015177da..de1725acf64203 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -1,4 +1,5 @@
import { importFetchedStatus, importFetchedStatuses } from './importer';
+import { submitMarkers } from './markers';
import api, { getLinks } from 'mastodon/api';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import compareId from 'mastodon/compare_id';
@@ -36,6 +37,10 @@ export function updateTimeline(timeline, status, accept) {
status,
usePendingItems: preferPendingItems,
});
+
+ if (timeline === 'home') {
+ dispatch(submitMarkers());
+ }
};
};
@@ -98,6 +103,10 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems));
+
+ if (timelineId === 'home') {
+ dispatch(submitMarkers());
+ }
}).catch(error => {
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
}).finally(() => {
@@ -114,7 +123,7 @@ export const expandAccountFeaturedTimeline = accountId => expandTimeline(`accoun
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
- return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
+ return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {
max_id: maxId,
any: parseTags(tags, 'any'),
all: parseTags(tags, 'all'),
diff --git a/app/javascript/mastodon/components/__tests__/button-test.js b/app/javascript/mastodon/components/__tests__/button-test.js
index 160cd3cbc75306..f5a649f70e32cc 100644
--- a/app/javascript/mastodon/components/__tests__/button-test.js
+++ b/app/javascript/mastodon/components/__tests__/button-test.js
@@ -1,4 +1,4 @@
-import { shallow } from 'enzyme';
+import { render, fireEvent, screen } from '@testing-library/react';
import React from 'react';
import renderer from 'react-test-renderer';
import Button from '../button';
@@ -21,16 +21,16 @@ describe('', () => {
it('handles click events using the given handler', () => {
const handler = jest.fn();
- const button = shallow();
- button.find('button').simulate('click');
+ render();
+ fireEvent.click(screen.getByText('button'));
expect(handler.mock.calls.length).toEqual(1);
});
it('does not handle click events if props.disabled given', () => {
const handler = jest.fn();
- const button = shallow();
- button.find('button').simulate('click');
+ render();
+ fireEvent.click(screen.getByText('button'));
expect(handler.mock.calls.length).toEqual(0);
});
diff --git a/app/javascript/mastodon/components/autosuggest_hashtag.js b/app/javascript/mastodon/components/autosuggest_hashtag.js
index e2f4e320dd1db6..9e9d888f83b48e 100644
--- a/app/javascript/mastodon/components/autosuggest_hashtag.js
+++ b/app/javascript/mastodon/components/autosuggest_hashtag.js
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { shortNumberFormat } from 'mastodon/utils/numbers';
+import ShortNumber from 'mastodon/components/short_number';
import { FormattedMessage } from 'react-intl';
export default class AutosuggestHashtag extends React.PureComponent {
@@ -13,14 +13,28 @@ export default class AutosuggestHashtag extends React.PureComponent {
}).isRequired,
};
- render () {
+ render() {
const { tag } = this.props;
- const weeklyUses = tag.history && shortNumberFormat(tag.history.reduce((total, day) => total + (day.uses * 1), 0));
+ const weeklyUses = tag.history && (
+