diff --git a/.buildpacks b/.buildpacks index 3450683ce..5e73304a5 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 9f43a0573..862fa126b 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/.codeclimate.yml b/.codeclimate.yml index d8d5c0ac7..b4ec9400e 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -27,7 +27,7 @@ plugins: enabled: true eslint: enabled: true - channel: eslint-6 + channel: eslint-7 rubocop: enabled: true channel: rubocop-0-82 diff --git a/.dependabot/config.yml b/.dependabot/config.yml deleted file mode 100644 index 06df775c2..000000000 --- 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 1292b752e..d51144d96 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -1,27 +1,15 @@ -# Service dependencies -# You may set REDIS_URL instead for more advanced options -# You may also set REDIS_NAMESPACE to share Redis between multiple Mastodon servers -REDIS_HOST=redis -REDIS_PORT=6379 -# You may set DATABASE_URL instead for more advanced options -DB_HOST=db -DB_USER=postgres -DB_NAME=postgres -DB_PASS= -DB_PORT=5432 -# Optional ElasticSearch configuration -# You may also set ES_PREFIX to share the same cluster between multiple Mastodon servers (falls back to REDIS_NAMESPACE if not set) -# ES_ENABLED=true -# ES_HOST=es -# ES_PORT=9200 +# This is a sample configuration file. You can generate your configuration +# with the `rake mastodon:setup` interactive setup wizard, but to customize +# your setup even further, you'll need to edit it manually. This sample does +# not demonstrate all available configuration options. Please look at +# https://docs.joinmastodon.org/admin/config/ for the full documentation. # Federation -# Note: Changing LOCAL_DOMAIN at a later time will cause unwanted side effects, including breaking all existing federation. -# LOCAL_DOMAIN should *NOT* contain the protocol part of the domain e.g https://example.com. +# ---------- +# This identifies your server and cannot be changed safely later +# ---------- LOCAL_DOMAIN=example.com -# Changing LOCAL_HTTPS in production is no longer supported. (Mastodon will always serve https:// links) - # Use this only if you need to run mastodon on a different domain than the one used for federation. # You can read more about this option on https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Serving_a_different_domain.md # DO *NOT* USE THIS UNLESS YOU KNOW *EXACTLY* WHAT YOU ARE DOING. @@ -32,107 +20,99 @@ LOCAL_DOMAIN=example.com # be added. Comma separated values # ALTERNATE_DOMAINS=example1.com,example2.com -# Application secrets +# Use HTTP proxy for outgoing request (optional) +# http_proxy=http://gateway.local:8118 +# Access control for hidden service. +# ALLOW_ACCESS_TO_HIDDEN_SERVICE=true + +# Authorized fetch mode (optional) +# Require remote servers to authentify when fetching toots, see +# https://docs.joinmastodon.org/admin/config/#authorized_fetch +# AUTHORIZED_FETCH=true + +# Limited federation mode (optional) +# Only allow federation with specific domains, see +# https://docs.joinmastodon.org/admin/config/#whitelist_mode +# LIMITED_FEDERATION_MODE=true + +# Redis +# ----- +REDIS_HOST=localhost +REDIS_PORT=6379 + + +# PostgreSQL +# ---------- +DB_HOST=/var/run/postgresql +DB_USER=mastodon +DB_NAME=mastodon_production +DB_PASS= +DB_PORT=5432 + + +# ElasticSearch (optional) +# ------------------------ +#ES_ENABLED=true +#ES_HOST=localhost +#ES_PORT=9200 + + +# Secrets +# ------- # Generate each with the `RAILS_ENV=production bundle exec rake secret` task (`docker-compose run --rm web bundle exec rake secret` if you use docker compose) +# ------- SECRET_KEY_BASE= OTP_SECRET= -# VAPID keys (used for push notifications -# You can generate the keys using the following command (first is the private key, second is the public one) + +# Web Push +# -------- +# Generate with `rake mastodon:webpush:generate_vapid_key` (first is the private key, second is the public one) # You should only generate this once per instance. If you later decide to change it, all push subscription will # be invalidated, requiring the users to access the website again to resubscribe. -# -# Generate with `RAILS_ENV=production bundle exec rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web bundle exec rake mastodon:webpush:generate_vapid_key` if you use docker compose) -# -# For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html +# -------- VAPID_PRIVATE_KEY= VAPID_PUBLIC_KEY= + # Registrations +# ------------- + # Single user mode will disable registrations and redirect frontpage to the first profile # SINGLE_USER_MODE=true -# Prevent registrations with following e-mail domains -# EMAIL_DOMAIN_BLACKLIST=example1.com|example2.de|etc -# Only allow registrations with the following e-mail domains -# EMAIL_DOMAIN_WHITELIST=example1.com|example2.de|etc +# Prevent registrations with following e-mail domains +# EMAIL_DOMAIN_DENYLIST=example1.com|example2.de|etc + +# Only allow registrations with the following e-mail domains +# EMAIL_DOMAIN_ALLOWLIST=example1.com|example2.de|etc + +#TODO move this # Optionally change default language # DEFAULT_LOCALE=de -# E-mail configuration -# Note: Mailgun and SparkPost (https://sparkpo.st/smtp) each have good free tiers -# If you want to use an SMTP server without authentication (e.g local Postfix relay) -# then set SMTP_AUTH_METHOD and SMTP_OPENSSL_VERIFY_MODE to 'none' and -# *comment* SMTP_LOGIN and SMTP_PASSWORD (leaving them blank is not enough). + +# Sending mail +# ------------ SMTP_SERVER=smtp.mailgun.org SMTP_PORT=587 SMTP_LOGIN= SMTP_PASSWORD= -SMTP_FROM_ADDRESS=notifications@example.com -#SMTP_REPLY_TO= -#SMTP_DOMAIN= # defaults to LOCAL_DOMAIN -#SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail -#SMTP_AUTH_METHOD=plain -#SMTP_CA_FILE=/etc/ssl/certs/ca-certificates.crt -#SMTP_OPENSSL_VERIFY_MODE=peer -#SMTP_ENABLE_STARTTLS_AUTO=true -#SMTP_TLS=true +SMTP_FROM_ADDRESS=notificatons@example.com -# Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files. -# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system -# PAPERCLIP_ROOT_URL=/system -# Optional asset host for multi-server setups -# The asset host must allow cross origin request from WEB_DOMAIN or LOCAL_DOMAIN -# if WEB_DOMAIN is not set. For example, the server may have the -# following header field: -# Access-Control-Allow-Origin: https://example.com/ -# CDN_HOST=https://assets.example.com - -# Optional list of hosts that are allowed to serve media for your instance -# This is useful if you include external media in your custom CSS or about page, -# or if your data storage provider makes use of redirects to other domains. -# EXTRA_DATA_HOSTS=https://data.example1.com|https://data.example2.com - -# S3 (optional) +# File storage (optional) +# ----------------------- # The attachment host must allow cross origin request from WEB_DOMAIN or # LOCAL_DOMAIN if WEB_DOMAIN is not set. For example, the server may have the # following header field: # Access-Control-Allow-Origin: https://192.168.1.123:9000/ -# S3_ENABLED=true -# S3_BUCKET= -# AWS_ACCESS_KEY_ID= -# AWS_SECRET_ACCESS_KEY= -# S3_REGION= -# S3_PROTOCOL=http -# S3_HOSTNAME=192.168.1.123:9000 - -# S3 (Minio Config (optional) Please check Minio instance for details) -# The attachment host must allow cross origin request - see the description -# above. -# S3_ENABLED=true -# S3_BUCKET= -# AWS_ACCESS_KEY_ID= -# AWS_SECRET_ACCESS_KEY= -# S3_REGION= -# S3_PROTOCOL=https -# S3_HOSTNAME= -# S3_ENDPOINT= -# S3_SIGNATURE_VERSION= - -# Google Cloud Storage (optional) -# Use S3 compatible API. Since GCS does not support Multipart Upload, -# increase the value of S3_MULTIPART_THRESHOLD to disable Multipart Upload. -# The attachment host must allow cross origin request - see the description -# above. -# S3_ENABLED=true -# AWS_ACCESS_KEY_ID= -# AWS_SECRET_ACCESS_KEY= -# S3_REGION= -# S3_PROTOCOL=https -# S3_HOSTNAME=storage.googleapis.com -# S3_ENDPOINT=https://storage.googleapis.com -# S3_MULTIPART_THRESHOLD=52428801 # 50.megabytes +# ----------------------- +#S3_ENABLED=true +#S3_BUCKET=files.example.com +#AWS_ACCESS_KEY_ID= +#AWS_SECRET_ACCESS_KEY= +#S3_ALIAS_HOST=files.example.com # Swift (optional) # The attachment host must allow cross origin request - see the description @@ -155,50 +135,27 @@ SMTP_FROM_ADDRESS=notifications@example.com # Defaults to 60 seconds. Set to 0 to disable # SWIFT_CACHE_TTL= +# Optional asset host for multi-server setups +# The asset host must allow cross origin request from WEB_DOMAIN or LOCAL_DOMAIN +# if WEB_DOMAIN is not set. For example, the server may have the +# following header field: +# Access-Control-Allow-Origin: https://example.com/ +# CDN_HOST=https://assets.example.com + +# Optional list of hosts that are allowed to serve media for your instance +# This is useful if you include external media in your custom CSS or about page, +# or if your data storage provider makes use of redirects to other domains. +# EXTRA_DATA_HOSTS=https://data.example1.com|https://data.example2.com + # Optional alias for S3 (e.g. to serve files on a custom domain, possibly using Cloudfront or Cloudflare) # S3_ALIAS_HOST= # Streaming API integration # STREAMING_API_BASE_URL= -# Advanced settings -# If you need to use pgBouncer, you need to disable prepared statements: -# PREPARED_STATEMENTS=false - -# Cluster number setting for streaming API server. -# If you comment out following line, cluster number will be `numOfCpuCores - 1`. -STREAMING_CLUSTER_NUM=1 - -# Docker mastodon user -# If you use Docker, you may want to assign UID/GID manually. -# UID=1000 -# GID=1000 - -# Maximum allowed character count -# MAX_TOOT_CHARS=500 - -# Maximum number of pinned posts -# MAX_PINNED_TOOTS=5 - -# Maximum allowed bio characters -# MAX_BIO_CHARS=500 - -# Maximim number of profile fields allowed -# MAX_PROFILE_FIELDS=4 - -# Maximum allowed display name characters -# MAX_DISPLAY_NAME_CHARS=30 - -# Maximum image and video/audio upload sizes -# Units are in bytes -# 1048576 bytes equals 1 megabyte -# MAX_IMAGE_SIZE=8388608 -# MAX_VIDEO_SIZE=41943040 - -# Maximum search results to display -# Only relevant when elasticsearch is installed -# MAX_SEARCH_RESULTS=20 +# External authentication (optional) +# ---------------------------------- # LDAP authentication (optional) # LDAP_ENABLED=true # LDAP_HOST=localhost @@ -276,17 +233,33 @@ STREAMING_CLUSTER_NUM=1 # SAML_ATTRIBUTES_STATEMENTS_VERIFIED= # SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL= -# Use HTTP proxy for outgoing request (optional) -# http_proxy=http://gateway.local:8118 -# Access control for hidden service. -# ALLOW_ACCESS_TO_HIDDEN_SERVICE=true -# Authorized fetch mode (optional) -# Require remote servers to authentify when fetching toots, see -# https://docs.joinmastodon.org/admin/config/#authorized_fetch -# AUTHORIZED_FETCH=true +# Custom settings +# --------------- +# Various ways to customize Mastodon's behavior +# --------------- + +# Maximum allowed character count +MAX_TOOT_CHARS=500 -# Whitelist mode (optional) -# Only allow federation with whitelisted domains, see -# https://docs.joinmastodon.org/admin/config/#whitelist_mode -# WHITELIST_MODE=true +# Maximum number of pinned posts +MAX_PINNED_TOOTS=5 + +# Maximum allowed bio characters +MAX_BIO_CHARS=500 + +# Maximim number of profile fields allowed +MAX_PROFILE_FIELDS=4 + +# Maximum allowed display name characters +MAX_DISPLAY_NAME_CHARS=30 + +# Maximum image and video/audio upload sizes +# Units are in bytes +# 1048576 bytes equals 1 megabyte +# MAX_IMAGE_SIZE=8388608 +# MAX_VIDEO_SIZE=41943040 + +# Maximum search results to display +# Only relevant when elasticsearch is installed +# MAX_SEARCH_RESULTS=20 diff --git a/.env.vagrant b/.env.vagrant index c2d26fa45..32ed9b922 100644 --- a/.env.vagrant +++ b/.env.vagrant @@ -1,3 +1,4 @@ VAGRANT=true LOCAL_DOMAIN=mastodon.local BIND=0.0.0.0 +DB_HOST=/var/run/postgresql/ diff --git a/.eslintrc.js b/.eslintrc.js index 177496d3a..7dda01108 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 91ee92a2e..9526e17db 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,3 @@ patreon: mastodon open_collective: mastodon +github: [Gargron] diff --git a/.gitignore b/.gitignore index 5b4878241..61de9a287 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 3a11f7000..25e0fa940 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 5f5985fba..12d0736bd 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) -* [eramdam](https://github.com/eramdam) -* [noellabo](https://github.com/noellabo) -* [takayamaki](https://github.com/takayamaki) * [danhunsaker](https://github.com/danhunsaker) +* [eramdam](https://github.com/eramdam) +* [takayamaki](https://github.com/takayamaki) +* [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: -- Zoltán Gera (*Hungarian*) -- Kristijan Tkalec (*Slovenian*) +- ᏦᏁᎢᎵᏫ 😷 (*Spanish, Argentina*) +- Sveinn í Felli (*Icelandic*) +- taicv (*Vietnamese*) +- ButterflyOfFire (*Arabic; French; Kabyle*) +- Duy (*Vietnamese*) - Evert Prants (*Estonian*) -- borys_sh (*Ukrainian*) -- ButterflyOfFire (*Arabic; French*) -- Osoitz (*Basque*) -- oɹʇuʞ (*Spanish, Argentina*) -- koyu (*German*) -- Jeroen (*Dutch*) -- Muha Aliss (*Turkish*) -- 唐宗勛 (*Chinese Simplified*) -- Jeong Arm (*Korean; Esperanto; Japanese*) -- Oguz Ersen (*Turkish*) -- spla (*Catalan*) +- Zoltán Gera (*Hungarian*) +- Daniele Lira Mereb (*Portuguese, Brazilian*) +- Kristijan Tkalec (*Slovenian*) +- stan ionut (*Romanian*) - Ramdziana F Y (*Indonesian*) -- Aditoo17 (*Czech*) -- Xosé M. (*Galician*) -- Roboron (*Spanish*) -- Alix Rossi (*Corsican; French*) -- Maya Minatsuki (*Japanese*) -- Masoud Abkenar (*Persian*) +- Michal Stanke (*Czech*) +- Xosé M. (*Galician; Spanish*) +- 奈卜拉 (*Chinese Simplified*) +- borys_sh (*Ukrainian*) +- Miguel Mayol (*Spanish; Catalan*) +- Besnik_b (*Albanian*) - Thai Localization (*Thai*) -- Marek Ľach (*Slovak; Polish*) -- d5Ziif3K (*Ukrainian*) +- Emanuel Pina (*Portuguese*) +- Jeong Arm (*Korean; Esperanto; Japanese*) +- Imre Kristoffer Eilertsen (*Norwegian*) +- Danial Behzadi (*Persian*) +- Osoitz (*Basque*) +- 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*) -- Emyn Nant Nefydd (*Welsh*) +- Alix Rossi (*Corsican; French*) +- arshat (*Kazakh*) +- FédiQuébec (*French*) +- Marek Ľach (*Slovak; Polish*) +- Muha Aliss (*Turkish*) +- tolstoevsky (*Russian*) +- Emyn-Russell Nt Nefydd (*Welsh*) +- Aditoo17 (*Czech*) +- Maya Minatsuki (*Japanese*) +- 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*) -- Jaz-Michael King (*Welsh*) -- christalleras (*Norwegian Nynorsk*) -- tykayn (*French*) -- Alessandro Levati (*Italian*) -- carolinagiorno (*Portuguese, Brazilian*) -- taoxvx (*Danish*) -- sabri (*Spanish*) -- Sasha Sorokin (*Russian*) -- shioko (*Chinese Simplified*) -- Evgeny Petrov (*Russian*) -- ariasuni (*French; Esperanto*) +- Bertil Hedkvist (*Swedish*) +- cybergene (*Japanese*) +- norayr (*Armenian*) +- William(ѕ)ⁿ (*Spanish*) - Tiago Epifânio (*Portuguese*) -- dxwc (*Bengali*) +- Mentor Gashi (*Albanian*) +- Jaz-Michael King (*Welsh*) +- carolinagiorno (*Portuguese, Brazilian*) +- Roby Thomas (*Malayalam*) +- Bharat Kumar (*Hindi*) +- tykayn (*French*) +- axi (*Finnish*) +- Selyan Slimane AMIRI (*Kabyle*) +- taoxvx (*Danish*) +- 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*) +- 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*) -- AW Unad (*Indonesian*) -- Andrea Lo Iacono (*Italian*) -- Ray (*Spanish*) +- andruhov (*Ukrainian; Russian*) +- 森の子リスのミーコの大冒険 (*Japanese*) +- るいーね (*Japanese*) +- Sam Tux (*Bengali*) - Unmual (*Spanish*) -- Ryo (*Korean*) +- AW Unad (*Indonesian*) +- Cutls (*Japanese*) +- Ray (*Spanish*) +- 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*) -- parnikkapore (*Thai*) -- Marcin Mikołajczak (*Polish*) -- 森の子リスのミーコの大冒険 (*Japanese*) -- Marcepanek_ (*Polish*) -- Sahak Petrosyan (*Armenian*) +- VSx86 (*Russian*) - Daniel Dimitrov (*Bulgarian*) +- mynameismonkey (*Welsh*) +- parnikkapore (*Thai*) +- 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*) +- 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 7d0110936..18790e860 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,166 @@ Changelog All notable changes to this project will be documented in this file. +## [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 + +- Fix media attachment enumeration ([ThibG](https://github.com/tootsuite/mastodon/pull/14254)) +- Change rate limits for various paths ([Gargron](https://github.com/tootsuite/mastodon/pull/14253)) +- Fix other sessions not being logged out on password change ([Gargron](https://github.com/tootsuite/mastodon/pull/14252)) + ## [v3.1.4] - 2020-05-14 ### Added diff --git a/Dockerfile b/Dockerfile index 0537d8fac..fa6abad5a 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 2cd2da270..e54b6665a 100644 --- a/Gemfile +++ b/Gemfile @@ -6,21 +6,21 @@ 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 'thwait', '~> 0.2.0' 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.6' gem 'dotenv-rails', '~> 2.7' -gem 'aws-sdk-s3', '~> 1.64', require: false +gem 'aws-sdk-s3', '~> 1.75', require: false gem 'fog-core', '<= 2.1.0' gem 'fog-openstack', '~> 0.3', require: false gem 'paperclip', '~> 6.0' @@ -48,8 +48,10 @@ 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' @@ -60,7 +62,7 @@ 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' @@ -72,29 +74,28 @@ gem 'oj', '~> 3.10' gem 'ox', '~> 2.13' gem 'parslet' gem 'parallel', '~> 1.19' -gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c' +gem 'posix-spawn' gem 'pundit', '~> 2.1' gem 'premailer-rails' 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 'sidekiq', '~> 6.0' +gem 'sanitize', '~> 5.2' +gem 'sidekiq', '~> 6.1' gem 'sidekiq-scheduler', '~> 3.0' gem 'sidekiq-unique-jobs', '~> 6.0' gem 'sidekiq-bulk', '~>0.2.0' gem 'simple-navigation', '~> 4.1' 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 'stoplight', '~> 2.2.1' +gem 'strong_migrations', '~> 0.7' +gem 'tty-prompt', '~> 0.22', require: false gem 'twitter-text', '~> 1.14' gem 'tzinfo-data', '~> 1.2020' gem 'webpacker', '~> 5.1' @@ -120,15 +121,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.1' gem 'rspec_junit_formatter', '~> 0.4' end @@ -141,14 +142,14 @@ 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-rbenv', '~> 2.1' + gem 'capistrano-rails', '~> 1.6' + gem 'capistrano-rbenv', '~> 2.2' gem 'capistrano-yarn', '~> 2.0' gem 'stackprof' diff --git a/Gemfile.lock b/Gemfile.lock index e4af7b195..e9be9fecc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,13 +6,6 @@ GIT health_check (4.0.0.pre) rails (>= 4.0) -GIT - remote: https://github.com/rtomayko/posix-spawn - revision: 58465d2e213991f8afb13b984854a49fcdcc980c - ref: 58465d2e213991f8afb13b984854a49fcdcc980c - specs: - posix-spawn (0.3.13) - GIT remote: https://github.com/tmm1/http_parser.rb revision: 54b17ba8c7d8d20a16dfc65d1775241833219cf2 @@ -31,25 +24,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 +53,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 +79,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.345.0) + aws-sdk-core (3.104.3) 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.75.0) + aws-sdk-core (~> 3, >= 3.104.1) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) - aws-sigv4 (1.1.3) - aws-eventstream (~> 1.0, >= 1.0.2) - bcrypt (3.1.13) - better_errors (2.7.0) + aws-sigv4 (1.2.1) + aws-eventstream (~> 1, >= 1.0.2) + bcrypt (3.1.15) + better_errors (2.7.1) coderay (>= 1.0.0) erubi (>= 1.0.0) rack (>= 0.9.0) @@ -116,34 +109,34 @@ GEM debug_inspector (>= 0.0.1) blurhash (0.1.4) ffi (~> 1.10.0) - bootsnap (1.4.6) + bootsnap (1.4.7) 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-bundler (2.0.1) capistrano (~> 3.1) - capistrano-rails (1.4.0) + capistrano-rails (1.6.1) capistrano (~> 3.1) - capistrano-bundler (~> 1.1) - capistrano-rbenv (2.1.6) + capistrano-bundler (>= 1.1, < 3) + capistrano-rbenv (2.2.0) capistrano (~> 3.1) 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 +157,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 +182,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) @@ -196,33 +190,33 @@ GEM unf (>= 0.0.5, < 1.0.0) doorkeeper (5.4.0) railties (>= 5) - dotenv (2.7.5) - dotenv-rails (2.7.5) - dotenv (= 2.7.5) - railties (>= 3.2, < 6.1) + dotenv (2.7.6) + dotenv-rails (2.7.6) + dotenv (= 2.7.6) + railties (>= 3.2) 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) - equatable (0.6.1) 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) fast_blank (1.0.0) - fastimage (2.1.7) + fastimage (2.2.0) ffi (1.10.0) ffi-compiler (1.0.1) ffi (>= 1.0.0) @@ -235,14 +229,14 @@ 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) @@ -281,10 +275,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.5) concurrent-ruby (~> 1.0) i18n-tasks (0.9.31) activesupport (>= 4.0.2) @@ -299,9 +293,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 +303,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 +334,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,21 +352,20 @@ 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) + multi_json (1.15.0) multipart-post (2.1.1) - necromancer (0.5.1) 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) + nokogiri (1.10.10) mini_portile2 (~> 2.4.0) nokogumbo (2.0.2) nokogiri (~> 1.8, >= 1.8.4) @@ -382,7 +374,7 @@ GEM concurrent-ruby (~> 1.0, >= 1.0.2) sidekiq (>= 3.5) statsd-ruby (~> 1.4, >= 1.4.0) - oj (3.10.6) + oj (3.10.8) omniauth (1.9.1) hashie (>= 3.4.6) rack (>= 1.6.2, < 3) @@ -390,9 +382,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,20 +396,20 @@ 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.1.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) + pastel (0.8.0) tty-color (~> 0.5) pg (1.2.3) - pghero (2.4.2) + pghero (2.6.0) activerecord (>= 5) pkg-config (1.4.1) - premailer (1.11.1) + posix-spawn (0.3.15) + premailer (1.12.1) addressable css_parser (>= 1.6.0) htmlentities (>= 4.0.0) @@ -434,39 +426,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,21 +467,21 @@ 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) redcarpet (3.5.0) - redis (4.1.4) + redis (4.2.1) redis-actionpack (5.2.0) actionpack (>= 5, < 7) redis-rack (>= 2.1.0, < 3) @@ -508,12 +498,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) @@ -531,7 +521,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) @@ -539,40 +529,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.2.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.1) 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) @@ -582,7 +574,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) @@ -607,10 +599,10 @@ GEM net-ssh (>= 2.8.0) stackprof (0.2.15) statsd-ruby (1.4.0) - stoplight (2.2.0) + stoplight (2.2.1) streamio-ffmpeg (3.0.2) multi_json (~> 1.8) - strong_migrations (0.6.6) + strong_migrations (0.7.1) activerecord (>= 5) temple (0.8.2) terminal-table (1.8.0) @@ -619,21 +611,19 @@ GEM climate_control (>= 0.0.3, < 1.0) thor (0.20.3) thread_safe (0.3.6) - thwait (0.1.0) + thwait (0.2.0) + e2mmap 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) - pastel (~> 0.7.0) - tty-reader (~> 0.7.0) - tty-reader (0.7.0) + tty-prompt (0.22.0) + pastel (~> 0.8) + tty-reader (~> 0.8) + tty-reader (0.8.0) tty-cursor (~> 0.7) - tty-screen (~> 0.7) - wisper (~> 2.0.0) - tty-screen (0.7.1) + tty-screen (~> 0.8) + wisper (~> 2.0) + tty-screen (0.8.1) twitter-text (1.14.7) unf (~> 0.1.0) tzinfo (1.2.7) @@ -659,9 +649,9 @@ GEM webpush (0.3.8) hkdf (~> 0.2) jwt (~> 2.0) - websocket-driver (0.7.1) + websocket-driver (0.7.3) 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) @@ -674,7 +664,7 @@ DEPENDENCIES active_record_query_trace (~> 1.7) addressable (~> 2.7) annotate (~> 3.1) - aws-sdk-s3 (~> 1.64) + aws-sdk-s3 (~> 1.75) better_errors (~> 2.7) binding_of_caller (~> 0.7) blurhash (~> 0.1) @@ -682,16 +672,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-rbenv (~> 2.1) + capistrano-rails (~> 1.6) + capistrano-rbenv (~> 2.2) 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) @@ -701,8 +692,9 @@ 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) @@ -716,7 +708,7 @@ DEPENDENCIES 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 @@ -744,39 +736,39 @@ DEPENDENCIES paperclip (~> 6.0) paperclip-av-transcoder (~> 0.6) parallel (~> 1.19) - parallel_tests (~> 2.32) + parallel_tests (~> 3.1) parslet pg (~> 1.2) - pghero (~> 2.4) + pghero (~> 2.6) pkg-config (~> 1.4) - posix-spawn! + posix-spawn premailer-rails private_address_check (~> 0.5) pry-byebug (~> 3.9) 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) redcarpet (~> 3.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) - sidekiq (~> 6.0) + sanitize (~> 5.2) + sidekiq (~> 6.1) sidekiq-bulk (~> 0.2.0) sidekiq-scheduler (~> 3.0) sidekiq-unique-jobs (~> 6.0) @@ -786,13 +778,12 @@ DEPENDENCIES sprockets (~> 3.7.2) sprockets-rails (~> 3.2) stackprof - stoplight (~> 2.2.0) + stoplight (~> 2.2.1) streamio-ffmpeg (~> 3.0) - strong_migrations (~> 0.6) + strong_migrations (~> 0.7) thor (~> 0.20) - thwait (~> 0.1.0) - tty-command (~> 0.9) - tty-prompt (~> 0.21) + thwait (~> 0.2.0) + tty-prompt (~> 0.22) twitter-text (~> 1.14) tzinfo-data (~> 1.2020) webmock (~> 3.8) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..7625597fe --- /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 211f17d81..e4f7cf403 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 bec9ed88b..47cb856ea 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 e3d8c1061..5c8cdd174 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true class AccountsController < ApplicationController - PAGE_SIZE = 20 + PAGE_SIZE = 20 + PAGE_SIZE_MAX = 200 include AccountControllerConcern include SignatureAuthentication @@ -10,7 +11,7 @@ class AccountsController < ApplicationController 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| @@ -27,7 +28,7 @@ class AccountsController < ApplicationController return end - @pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses? + @pinned_statuses = cache_collection(@account.pinned_statuses.not_local_only, Status) if show_pinned_statuses? @statuses = filtered_status_page @statuses = cache_collection(@statuses, Status) @rss_url = rss_url @@ -41,7 +42,8 @@ class AccountsController < ApplicationController 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 diff --git a/app/controllers/activitypub/claims_controller.rb b/app/controllers/activitypub/claims_controller.rb new file mode 100644 index 000000000..08ad952df --- /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 c1e7aa550..e62fba748 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 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController private - def set_statuses - @statuses = scope_for_collection - @statuses = cache_collection(@statuses, Status) - end - - def set_size + def set_items case params[:id] when 'featured' - @size = @account.pinned_statuses.count + @items = begin + # Because in public fetch mode we cache the response, there would be no + # benefit from performing the check below, since a blocked account or domain + # would likely be served the cache from the reverse proxy anyway + + if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain))) + [] + else + cache_collection(@account.pinned_statuses.not_local_only, Status) + end + end + when 'devices' + @items = @account.devices else not_found end end - def scope_for_collection + def set_size + case params[:id] + when 'featured', 'devices' + @size = @items.size + else + not_found + end + end + + 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 efa8f2950..71efb543e 100644 --- a/app/controllers/admin/custom_emojis_controller.rb +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -33,6 +33,8 @@ module Admin @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/api/base_controller.rb b/app/controllers/api/base_controller.rb index 153ade253..045e7dd26 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 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 000000000..032e807d1 --- /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 000000000..aa9df6e03 --- /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 000000000..c764915e5 --- /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 000000000..34b21a380 --- /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 000000000..ffd7151b7 --- /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 000000000..0851d797d --- /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 000000000..fc4abf63b --- /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 0bb3d0d27..a2a919a3e 100644 --- a/app/controllers/api/v1/media_controller.rb +++ b/app/controllers/api/v1/media_controller.rb @@ -39,7 +39,7 @@ class Api::V1::MediaController < Api::BaseController end def media_attachment_params - params.permit(:file, :description, :focus) + params.permit(:file, :thumbnail, :description, :focus) end def file_type_error diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb index 7fa774a4d..1be15a5a4 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 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController end def destroy - @status = current_account.statuses.find_by(reblog_of_id: @reblog.id) + @status = current_account.statuses.find_by(reblog_of_id: params[:status_id]) if @status authorize @status, :unreblog? @status.discard RemovalWorker.perform_async(@status.id) + @reblog = @status.reblog + else + @reblog = Status.find(params[:status_id]) + authorize @reblog, :show? end render json: @reblog, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, reblogs_map: { @reblog.id => false }) + rescue Mastodon::NotPermittedError + not_found end private diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index b3edce676..c8529318f 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -58,6 +58,7 @@ class Api::V1::StatusesController < Api::BaseController @status.discard RemovalWorker.perform_async(@status.id, redraft: true) + @status.account.statuses_count = @status.account.statuses_count - 1 render json: @status, serializer: REST::StatusSerializer, source_requested: true end diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index c6e7854d9..b449bcadf 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -29,6 +29,8 @@ class Api::V1::Timelines::PublicController < Api::BaseController params_slice(:max_id, :since_id, :min_id) ) + statuses = statuses.not_local_only unless truthy_param?(:local) || truthy_param?(:allow_local_only) + if truthy_param?(:only_media) # `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids. status_ids = statuses.joins(:media_attachments).distinct(:id).pluck(:id) @@ -47,7 +49,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController end def pagination_params(core_params) - params.slice(:local, :remote, :limit, :only_media).permit(:local, :remote, :limit, :only_media).merge(core_params) + params.slice(:local, :remote, :limit, :only_media, :allow_local_only).permit(:local, :remote, :limit, :only_media, :allow_local_only).merge(core_params) end def next_path diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 63d9f91fb..e996c2217 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -56,7 +56,7 @@ class ApplicationController < ActionController::Base end def store_current_location - store_location_for(:user, request.url) unless request.format == :json + store_location_for(:user, request.url) unless [:json, :rss].include?(request.format&.to_sym) end def require_admin! diff --git a/app/controllers/auth/passwords_controller.rb b/app/controllers/auth/passwords_controller.rb index c224e1a03..42534f8ce 100644 --- a/app/controllers/auth/passwords_controller.rb +++ b/app/controllers/auth/passwords_controller.rb @@ -9,7 +9,10 @@ class Auth::PasswordsController < Devise::PasswordsController def update super do |resource| - resource.session_activations.destroy_all if resource.errors.empty? + if resource.errors.empty? + resource.session_activations.destroy_all + resource.forget_me! + end end end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index f6a85d87e..96d973394 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Auth::RegistrationsController < Devise::RegistrationsController + include Devise::Controllers::Rememberable + layout :determine_layout before_action :set_invite, only: [:new, :create] @@ -25,7 +27,11 @@ class Auth::RegistrationsController < Devise::RegistrationsController def update super do |resource| - resource.clear_other_sessions(current_session.session_id) if resource.saved_change_to_encrypted_password? + if resource.saved_change_to_encrypted_password? + resource.clear_other_sessions(current_session.session_id) + resource.forget_me! + remember_me(resource) + end end end diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index c36561b86..441833e85 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -9,7 +9,9 @@ class Auth::SessionsController < Devise::SessionsController skip_before_action :require_functional! prepend_before_action :set_pack - 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 @@ -40,17 +42,18 @@ class Auth::SessionsController < Devise::SessionsController protected def find_user - if session[:otp_user_id] - User.find(session[:otp_user_id]) + if session[:attempt_user_id] + User.find(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) @@ -71,48 +74,6 @@ class Auth::SessionsController < Devise::SessionsController 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 - use_pack 'auth' - @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 diff --git a/app/controllers/concerns/localized.rb b/app/controllers/concerns/localized.rb index d1384ed56..fe1142f34 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 @@ module Localized 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 000000000..f5178930b --- /dev/null +++ b/app/controllers/concerns/sign_in_token_authentication_concern.rb @@ -0,0 +1,52 @@ +# 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_params[:sign_in_token_attempt].present? && 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) + session.delete(:attempt_user_id) + 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_locale do + session[:attempt_user_id] = user.id + use_pack 'auth' + @body_classes = 'lighter' + render :sign_in_token + end + end +end 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 000000000..35c0c27cf --- /dev/null +++ b/app/controllers/concerns/two_factor_authentication_concern.rb @@ -0,0 +1,50 @@ +# 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_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) + session.delete(:attempt_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) + set_locale do + session[:attempt_user_id] = user.id + use_pack 'auth' + @body_classes = 'lighter' + render :two_factor + end + end +end diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb index adf2bd014..549c6a39e 100644 --- a/app/controllers/directories_controller.rb +++ b/app/controllers/directories_controller.rb @@ -10,7 +10,7 @@ class DirectoriesController < ApplicationController before_action :set_accounts before_action :set_pack - 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 eb223c3f7..5ffbdae79 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 4ddccf607..69820ebb7 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/home_controller.rb b/app/controllers/home_controller.rb index efdb1d226..c9b840881 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class HomeController < ApplicationController + before_action :redirect_unauthenticated_to_permalinks! before_action :authenticate_user! before_action :set_pack @@ -12,7 +13,7 @@ class HomeController < ApplicationController private - def authenticate_user! + def redirect_unauthenticated_to_permalinks! return if user_signed_in? matches = request.path.match(/\A\/web\/(statuses|accounts)\/([\d]+)\z/) @@ -37,6 +38,7 @@ class HomeController < ApplicationController end matches = request.path.match(%r{\A/web/timelines/tag/(?.+)\z}) + redirect_to(matches ? tag_path(CGI.unescape(matches[:tag])) : default_redirect_path) end diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index 1d166d6e7..ce015dd1b 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 014b89de1..0b1d09de9 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -2,6 +2,7 @@ class MediaProxyController < ApplicationController include RoutingHelper + include Authorization skip_before_action :store_current_location skip_before_action :require_functional! @@ -10,12 +11,14 @@ class MediaProxyController < ApplicationController rescue_from ActiveRecord::RecordInvalid, with: :not_found rescue_from Mastodon::UnexpectedResponseError, with: :not_found + rescue_from Mastodon::NotPermittedError, with: :not_found rescue_from HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, with: :internal_server_error def show RedisLock.acquire(lock_options) do |lock| if lock.acquired? - @media_attachment = MediaAttachment.remote.find(params[:id]) + @media_attachment = MediaAttachment.remote.attached.find(params[:id]) + authorize @media_attachment.status, :show? redownload! if @media_attachment.needs_redownload? && !reject_media? else raise Mastodon::RaceConditionError @@ -28,8 +31,8 @@ class MediaProxyController < ApplicationController 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 51bb9bdea..a277bfa10 100644 --- a/app/controllers/remote_interaction_controller.rb +++ b/app/controllers/remote_interaction_controller.rb @@ -11,7 +11,7 @@ class RemoteInteractionController < ApplicationController before_action :set_body_classes before_action :set_pack - 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 6e5b72ffb..97193ade0 100644 --- a/app/controllers/settings/migration/redirects_controller.rb +++ b/app/controllers/settings/migration/redirects_controller.rb @@ -18,7 +18,7 @@ class Settings::Migration::RedirectsController < Settings::BaseController 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 73926707b..df2a6eed3 100644 --- a/app/controllers/settings/pictures_controller.rb +++ b/app/controllers/settings/pictures_controller.rb @@ -7,13 +7,8 @@ module Settings 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 @@ module Settings @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 a1b7f4320..a6ab8828f 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) @@ -44,7 +44,7 @@ class StatusesController < ApplicationController 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 3d12c9eaf..69db89eb3 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| @@ -26,7 +27,8 @@ class TagsController < ApplicationController 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 480e58f3f..9de9db6ba 100644 --- a/app/controllers/well_known/webfinger_controller.rb +++ b/app/controllers/well_known/webfinger_controller.rb @@ -8,7 +8,8 @@ module WellKnown 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 @@ module WellKnown 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 40f914f1e..9ca11d573 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -77,6 +77,18 @@ module ApplicationHelper 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}:") @@ -137,6 +149,11 @@ module ApplicationHelper 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 866a9902c..a51597cf3 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -15,11 +15,13 @@ module StatusesHelper 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 70c493210..ab7ca4698 100644 --- a/app/helpers/webfinger_helper.rb +++ b/app/helpers/webfinger_helper.rb @@ -1,5 +1,16 @@ # frozen_string_literal: true +# Monkey-patch on monkey-patch. +# Because it conflicts with the request.rb patch. +class HTTP::Timeout::PerOperationOriginal < HTTP::Timeout::PerOperation + def connect(socket_class, host, port, nodelay = false) + ::Timeout.timeout(@connect_timeout, HTTP::TimeoutError) do + @socket = socket_class.open(host, port) + @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay + end + end +end + module WebfingerHelper def webfinger!(uri) hidden_service_uri = /\.(onion|i2p)(:\d+)?$/.match(uri) @@ -12,6 +23,14 @@ module WebfingerHelper headers: { 'User-Agent': Mastodon::Version.user_agent, }, + + timeout_class: HTTP::Timeout::PerOperationOriginal, + + timeout_options: { + write_timeout: 10, + connect_timeout: 5, + read_timeout: 10, + }, } Goldfinger::Client.new(uri, opts.merge(Rails.configuration.x.http_client_proxy)).finger diff --git a/app/javascript/core/settings.js b/app/javascript/core/settings.js index 9fe03f90c..9403e339b 100644 --- a/app/javascript/core/settings.js +++ b/app/javascript/core/settings.js @@ -34,10 +34,12 @@ delegate(document, '#account_header', 'change', ({ target }) => { delegate(document, '#account_locked', 'change', ({ target }) => { const lock = document.querySelector('.card .display-name i'); - if (target.checked) { - lock.style.display = 'inline'; - } else { - lock.style.display = 'none'; + if (lock) { + if (target.checked) { + delete lock.dataset.hidden; + } else { + lock.dataset.hidden = 'true'; + } } }); diff --git a/app/javascript/flavours/glitch/actions/account_notes.js b/app/javascript/flavours/glitch/actions/account_notes.js new file mode 100644 index 000000000..c1cce3193 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/account_notes.js @@ -0,0 +1,69 @@ +import api from 'flavours/glitch/util/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 const ACCOUNT_NOTE_INIT_EDIT = 'ACCOUNT_NOTE_INIT_EDIT'; +export const ACCOUNT_NOTE_CANCEL = 'ACCOUNT_NOTE_CANCEL'; + +export const ACCOUNT_NOTE_CHANGE_COMMENT = 'ACCOUNT_NOTE_CHANGE_COMMENT'; + +export function submitAccountNote() { + return (dispatch, getState) => { + dispatch(submitAccountNoteRequest()); + + const id = getState().getIn(['account_notes', 'edit', 'account_id']); + + api(getState).post(`/api/v1/accounts/${id}/note`, { + comment: getState().getIn(['account_notes', 'edit', 'comment']), + }).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, + }; +}; + +export function initEditAccountNote(account) { + return (dispatch, getState) => { + const comment = getState().getIn(['relationships', account.get('id'), 'note']); + + dispatch({ + type: ACCOUNT_NOTE_INIT_EDIT, + account, + comment, + }); + }; +}; + +export function cancelAccountNote() { + return { + type: ACCOUNT_NOTE_CANCEL, + }; +}; + +export function changeAccountNoteComment(comment) { + return { + type: ACCOUNT_NOTE_CHANGE_COMMENT, + comment, + }; +}; diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index f98cb7bf8..f83738093 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -30,6 +30,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'; @@ -193,7 +198,9 @@ export function submitCompose(routerHistory) { if (response.data.in_reply_to_id === null && response.data.visibility === 'public') { insertIfOnline('community'); - insertIfOnline('public'); + if (!response.data.local_only) { + insertIfOnline('public'); + } } else if (response.data.visibility === 'direct') { insertIfOnline('direct'); } @@ -289,6 +296,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()); @@ -307,6 +357,7 @@ export function changeUploadComposeRequest() { skipLoading: true, }; }; + export function changeUploadComposeSuccess(media) { return { type: COMPOSE_UPLOAD_CHANGE_SUCCESS, diff --git a/app/javascript/flavours/glitch/actions/dropdown_menu.js b/app/javascript/flavours/glitch/actions/dropdown_menu.js index 14f2939c7..fb6e55612 100644 --- a/app/javascript/flavours/glitch/actions/dropdown_menu.js +++ b/app/javascript/flavours/glitch/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/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index 52ad17779..05955963c 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -12,7 +12,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/flavours/glitch/actions/markers.js b/app/javascript/flavours/glitch/actions/markers.js index 7ffab404d..96e29accf 100644 --- a/app/javascript/flavours/glitch/actions/markers.js +++ b/app/javascript/flavours/glitch/actions/markers.js @@ -1,38 +1,107 @@ import api from 'flavours/glitch/util/api'; +import { debounce } from 'lodash'; +import compareId from 'flavours/glitch/util/compare_id'; +import { showAlertForError } from './alerts'; export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST'; export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS'; export const MARKERS_FETCH_FAIL = 'MARKERS_FETCH_FAIL'; +export const MARKERS_SUBMIT_SUCCESS = 'MARKERS_SUBMIT_SUCCESS'; -export const submitMarkers = () => (dispatch, getState) => { +export const synchronouslySubmitMarkers = () => (dispatch, getState) => { const accessToken = getState().getIn(['meta', 'access_token'], ''); - const params = {}; - - const lastHomeId = getState().getIn(['timelines', 'home', 'items', 0]); - const lastNotificationId = getState().getIn(['notifications', 'lastReadId']); - - if (lastHomeId) { - params.home = { - last_read_id: lastHomeId, - }; - } - - if (lastNotificationId && lastNotificationId !== '0') { - params.notifications = { - last_read_id: lastNotificationId, - }; - } + const params = _buildParams(getState()); if (Object.keys(params).length === 0) { return; } - const client = new XMLHttpRequest(); + // 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; + } + } - client.open('POST', '/api/v1/markers', false); - client.setRequestHeader('Content-Type', 'application/json'); - client.setRequestHeader('Authorization', `Bearer ${accessToken}`); - client.send(JSON.stringify(params)); + // 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', 'lastReadId']); + + if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) { + params.home = { + last_read_id: lastHomeId, + }; + } + + if (lastNotificationId && lastNotificationId !== '0' && 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; + } + + 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, + }; +}; + +export function submitMarkers() { + return (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState); }; export const fetchMarkers = () => (dispatch, getState) => { diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index b3de7b5bf..ceb1e6df6 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/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'; @@ -81,6 +82,8 @@ export function updateNotifications(notification, intlMessages, intlLocale) { filtered = regex && regex.test(searchIndex); } + dispatch(submitMarkers()); + if (showInColumn) { dispatch(importFetchedAccount(notification.account)); @@ -168,6 +171,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/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index 875013efc..0253c24b2 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -73,7 +73,7 @@ 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 connectPublicStream = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}) => connectTimelineStream(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`); +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/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index b01109134..b19666e62 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -1,4 +1,5 @@ import { importFetchedStatus, importFetchedStatuses } from './importer'; +import { submitMarkers } from './markers'; import api, { getLinks } from 'flavours/glitch/util/api'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import compareId from 'flavours/glitch/util/compare_id'; @@ -49,6 +50,10 @@ export function updateTimeline(timeline, status, accept) { usePendingItems: preferPendingItems, filtered }); + + if (timeline === 'home') { + dispatch(submitMarkers()); + } }; }; @@ -112,6 +117,10 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { 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(() => { @@ -121,7 +130,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { }; export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); -export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done); +export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done); export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done); export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); @@ -129,11 +138,12 @@ 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'), none: parseTags(tags, 'none'), + local: local, }, done); }; diff --git a/app/javascript/flavours/glitch/components/autosuggest_hashtag.js b/app/javascript/flavours/glitch/components/autosuggest_hashtag.js index 648987dfd..d787ed07a 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_hashtag.js +++ b/app/javascript/flavours/glitch/components/autosuggest_hashtag.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { shortNumberFormat } from 'flavours/glitch/util/numbers'; +import ShortNumber from 'flavours/glitch/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/flavours/glitch/components/autosuggest_textarea.js b/app/javascript/flavours/glitch/components/autosuggest_textarea.js index ec2fbbe4b..1ce2f42b4 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_textarea.js +++ b/app/javascript/flavours/glitch/components/autosuggest_textarea.js @@ -208,7 +208,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { {placeholder}