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('); + 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(); + 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 && ( + total + day.uses * 1, 0)} + /> + ); return (

-
#{tag.name}
- {tag.history !== undefined &&
} +
+ #{tag.name} +
+ {tag.history !== undefined && ( +
+ +
+ )}
); } diff --git a/app/javascript/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js index ac2a6366a07025..58ec4f6eb66fb2 100644 --- a/app/javascript/mastodon/components/autosuggest_textarea.js +++ b/app/javascript/mastodon/components/autosuggest_textarea.js @@ -208,7 +208,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { {placeholder}