From ef196c913c77338be5ebb1e02af2f6225f857080 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 31 Mar 2022 00:49:24 +0200 Subject: Fix error MethodError in Chewy::Strategy::Sidekiq::Worker (#17912) Also refactor a bit to reduce code duplication. --- app/chewy/statuses_index.rb | 2 +- app/helpers/formatting_helper.rb | 1 + app/lib/feed_manager.rb | 11 +---------- app/models/status.rb | 9 +++++++++ 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb index bfd61a048..1381a96ed 100644 --- a/app/chewy/statuses_index.rb +++ b/app/chewy/statuses_index.rb @@ -59,7 +59,7 @@ class StatusesIndex < Chewy::Index field :id, type: 'long' field :account_id, type: 'long' - field :text, type: 'text', value: ->(status) { [status.spoiler_text, extract_status_plain_text(status)].concat(status.ordered_media_attachments.map(&:description)).concat(status.preloadable_poll ? status.preloadable_poll.options : []).join("\n\n") } do + field :text, type: 'text', value: ->(status) { status.searchable_text } do field :stemmed, type: 'text', analyzer: 'content' end diff --git a/app/helpers/formatting_helper.rb b/app/helpers/formatting_helper.rb index e11156999..a58dd608f 100644 --- a/app/helpers/formatting_helper.rb +++ b/app/helpers/formatting_helper.rb @@ -12,6 +12,7 @@ module FormattingHelper def extract_status_plain_text(status) PlainTextFormatter.new(status.text, status.local?).to_s end + module_function :extract_status_plain_text def status_content_format(status) html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : [])) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 709450080..085f1b274 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -5,7 +5,6 @@ require 'singleton' class FeedManager include Singleton include Redisable - include FormattingHelper # Maximum number of items stored in a single feed MAX_ITEMS = 400 @@ -443,16 +442,8 @@ class FeedManager return false if active_filters.empty? combined_regex = Regexp.union(active_filters) - status = status.reblog if status.reblog? - combined_text = [ - extract_status_plain_text(status), - status.spoiler_text, - status.preloadable_poll ? status.preloadable_poll.options.join("\n\n") : nil, - status.ordered_media_attachments.map(&:description).join("\n\n"), - ].compact.join("\n\n") - - combined_regex.match?(combined_text) + combined_regex.match?(status.proper.searchable_text) end # Adds a status to an account's feed, returning true if a status was diff --git a/app/models/status.rb b/app/models/status.rb index bf5a49b7f..a71451f50 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -155,6 +155,15 @@ class Status < ApplicationRecord ids.uniq end + def searchable_text + [ + spoiler_text, + FormattingHelper.extract_status_plain_text(self), + preloadable_poll ? preloadable_poll.options.join("\n\n") : nil, + ordered_media_attachments.map(&:description).join("\n\n"), + ].compact.join("\n\n") + end + def reply? !in_reply_to_id.nil? || attributes['reply'] end -- cgit From ea0cfd8e7ed41b32aaa47eabb1c73845ae843fcb Mon Sep 17 00:00:00 2001 From: Holger Date: Thu, 31 Mar 2022 17:20:26 +0800 Subject: fix: PWA web manifest not changed to new routes (#17921) --- app/serializers/manifest_serializer.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/serializers/manifest_serializer.rb b/app/serializers/manifest_serializer.rb index 4786aa760..ad05fdf6b 100644 --- a/app/serializers/manifest_serializer.rb +++ b/app/serializers/manifest_serializer.rb @@ -44,7 +44,7 @@ class ManifestSerializer < ActiveModel::Serializer end def start_url - '/web/timelines/home' + '/web/home' end def scope @@ -69,7 +69,7 @@ class ManifestSerializer < ActiveModel::Serializer [ { name: 'New toot', - url: '/web/statuses/new', + url: '/web/publish', icons: [ { src: '/shortcuts/new-status.png', @@ -91,7 +91,7 @@ class ManifestSerializer < ActiveModel::Serializer }, { name: 'Direct messages', - url: '/web/timelines/direct', + url: '/web/conversations', icons: [ { src: '/shortcuts/direct.png', -- cgit From 24d446adf22644c61e4d61ef612458cf087326dd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Mar 2022 19:10:40 +0900 Subject: Bump puma from 5.6.2 to 5.6.4 (#17914) Bumps [puma](https://github.com/puma/puma) from 5.6.2 to 5.6.4. - [Release notes](https://github.com/puma/puma/releases) - [Changelog](https://github.com/puma/puma/blob/master/History.md) - [Commits](https://github.com/puma/puma/compare/v5.6.2...v5.6.4) --- updated-dependencies: - dependency-name: puma dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index d8b684ce8..16492c41e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -459,7 +459,7 @@ GEM pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (4.0.6) - puma (5.6.2) + puma (5.6.4) nio4r (~> 2.0) pundit (2.2.0) activesupport (>= 3.0.0) -- cgit From 44b7be45f1161d6378dba13e083b3c05d90cf0fa Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 1 Apr 2022 23:55:32 +0200 Subject: Fix assets failing to build with OpenSSL 3 because of deprecated hash algorithm (#17930) Fixes #17924 --- config/webpack/shared.js | 1 + 1 file changed, 1 insertion(+) diff --git a/config/webpack/shared.js b/config/webpack/shared.js index 05828aebe..3447e711c 100644 --- a/config/webpack/shared.js +++ b/config/webpack/shared.js @@ -37,6 +37,7 @@ module.exports = { filename: 'js/[name]-[chunkhash].js', chunkFilename: 'js/[name]-[chunkhash].chunk.js', hotUpdateChunkFilename: 'js/[id]-[hash].hot-update.js', + hashFunction: 'sha256', path: output.path, publicPath: output.publicPath, }, -- cgit From 39b489ba4c362997b41dd039971b8b510f6fe10d Mon Sep 17 00:00:00 2001 From: Holger Date: Sat, 2 Apr 2022 05:56:23 +0800 Subject: fix: `s3_force_single_request` not parsed (#17922) --- config/application.rb | 1 - config/initializers/paperclip.rb | 20 ++++++++++++++++++++ lib/paperclip/storage_extensions.rb | 21 --------------------- 3 files changed, 20 insertions(+), 22 deletions(-) delete mode 100644 lib/paperclip/storage_extensions.rb diff --git a/config/application.rb b/config/application.rb index eb5af9cc0..bed935ce3 100644 --- a/config/application.rb +++ b/config/application.rb @@ -27,7 +27,6 @@ require_relative '../lib/sanitize_ext/sanitize_config' require_relative '../lib/redis/namespace_extensions' require_relative '../lib/paperclip/url_generator_extensions' require_relative '../lib/paperclip/attachment_extensions' -require_relative '../lib/paperclip/storage_extensions' require_relative '../lib/paperclip/lazy_thumbnail' require_relative '../lib/paperclip/gif_transcoder' require_relative '../lib/paperclip/transcoder' diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb index e2a045647..26b0a2f7c 100644 --- a/config/initializers/paperclip.rb +++ b/config/initializers/paperclip.rb @@ -83,6 +83,26 @@ if ENV['S3_ENABLED'] == 'true' s3_host_alias: ENV['S3_ALIAS_HOST'] || ENV['S3_CLOUDFRONT_HOST'] ) end + + # Some S3-compatible providers might not actually be compatible with some APIs + # used by kt-paperclip, see https://github.com/mastodon/mastodon/issues/16822 + if ENV['S3_FORCE_SINGLE_REQUEST'] == 'true' + module Paperclip + module Storage + module S3Extensions + def copy_to_local_file(style, local_dest_path) + log("copying #{path(style)} to local file #{local_dest_path}") + s3_object(style).download_file(local_dest_path, { mode: 'single_request' }) + rescue Aws::Errors::ServiceError => e + warn("#{e} - cannot copy #{path(style)} to local file #{local_dest_path}") + false + end + end + end + end + + Paperclip::Storage::S3.prepend(Paperclip::Storage::S3Extensions) + end elsif ENV['SWIFT_ENABLED'] == 'true' require 'fog/openstack' diff --git a/lib/paperclip/storage_extensions.rb b/lib/paperclip/storage_extensions.rb deleted file mode 100644 index 95c35641e..000000000 --- a/lib/paperclip/storage_extensions.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -# Some S3-compatible providers might not actually be compatible with some APIs -# used by kt-paperclip, see https://github.com/mastodon/mastodon/issues/16822 -if ENV['S3_ENABLED'] == 'true' && ENV['S3_FORCE_SINGLE_REQUEST'] == 'true' - module Paperclip - module Storage - module S3Extensions - def copy_to_local_file(style, local_dest_path) - log("copying #{path(style)} to local file #{local_dest_path}") - s3_object(style).download_file(local_dest_path, { mode: 'single_request' }) - rescue Aws::Errors::ServiceError => e - warn("#{e} - cannot copy #{path(style)} to local file #{local_dest_path}") - false - end - end - end - end - - Paperclip::Storage::S3.prepend(Paperclip::Storage::S3Extensions) -end -- cgit From 0a8a0fb5990eb6d3842948654d0b6140e776d8a9 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 1 Apr 2022 23:59:13 +0200 Subject: Fix unusual number formatting in some locales (#17929) * Fix unusual number formatting in some locales Fixes #17904 * Fix typo --- app/helpers/application_helper.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 651a98a85..9984820da 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -19,8 +19,11 @@ module ApplicationHelper # is looked up from the locales definition, and rails-i18n comes with # values that don't seem to make much sense for many languages, so # override these values with a default of 3 digits of precision. - options[:precision] = 3 - options[:strip_insignificant_zeros] = true + options = options.merge( + precision: 3, + strip_insignificant_zeros: true, + significant: true + ) number_to_human(number, **options) end -- cgit From 80ded02a4bd2ea6d5b9b69198753063224773f66 Mon Sep 17 00:00:00 2001 From: Ondřej Pokorný Date: Sun, 3 Apr 2022 14:02:29 +0200 Subject: Update en.yml (#17942) typo --- config/locales/en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index 829cd61d0..4a16098a8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -598,7 +598,7 @@ en: action_taken_by: Action taken by actions: delete_description_html: The reported posts will be deleted and a strike will be recorded to help you escalate on future infractions by the same account. - mark_as_sensitive_description_html: The media in the reported posts will be marked as sensitive and a strike will be recorded to help you escalate on future refractions by the same account. + mark_as_sensitive_description_html: The media in the reported posts will be marked as sensitive and a strike will be recorded to help you escalate on future infractions by the same account. other_description_html: See more options for controlling the account's behaviour and customize communication to the reported account. resolve_description_html: No action will be taken against the reported account, no strike recorded, and the report will be closed. silence_description_html: The profile will be visible only to those who already follow it or manually look it up, severely limiting its reach. Can always be reverted. -- cgit From 0ec695e036dab45d57598f451266bd0b176df9fd Mon Sep 17 00:00:00 2001 From: CommanderRoot Date: Mon, 4 Apr 2022 18:19:45 +0200 Subject: Replace deprecated String.prototype.substr() (#17949) * Replace deprecated String.prototype.substr() .substr() is deprecated so we replace it with .slice() which works similarily but isn't deprecated * Change String.prototype.substring() to String.prototype.slice() .substring() and .slice() work very similary but .slice() is a bit faster and stricter * Add ESLint rule to forbid usage of .substr and .substring .substr() is deprecated and .substring() is very similar to .slice() so better to use .slice() at all times Signed-off-by: Tobias Speicher --- .eslintrc.js | 5 +++++ app/javascript/mastodon/features/emoji/emoji_mart_search_light.js | 2 +- app/javascript/mastodon/features/status/components/card.js | 2 +- app/javascript/mastodon/features/video/index.js | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 7dda01108..2a882f59c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -79,6 +79,11 @@ module.exports = { 'no-irregular-whitespace': 'error', 'no-mixed-spaces-and-tabs': 'warn', 'no-nested-ternary': 'warn', + 'no-restricted-properties': [ + 'error', + { property: 'substring', message: 'Use .slice instead of .substring.' }, + { property: 'substr', message: 'Use .slice instead of .substr.' }, + ], 'no-trailing-spaces': 'warn', 'no-undef': 'error', 'no-unreachable': 'error', diff --git a/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js b/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js index e4519a13e..70694ab6d 100644 --- a/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js +++ b/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js @@ -124,7 +124,7 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo for (let id in aPool) { let emoji = aPool[id], { search } = emoji, - sub = value.substr(0, length), + sub = value.slice(0, length), subIndex = search.indexOf(sub); if (subIndex !== -1) { diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js index 90f9ae7ae..3d81bcb29 100644 --- a/app/javascript/mastodon/features/status/components/card.js +++ b/app/javascript/mastodon/features/status/components/card.js @@ -32,7 +32,7 @@ const trim = (text, len) => { return text; } - return text.substring(0, cut) + (text.length > len ? '…' : ''); + return text.slice(0, cut) + (text.length > len ? '…' : ''); }; const domParser = new DOMParser(); diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js index 8d47e479a..4f90e955f 100644 --- a/app/javascript/mastodon/features/video/index.js +++ b/app/javascript/mastodon/features/video/index.js @@ -91,7 +91,7 @@ export const fileNameFromURL = str => { const pathname = url.pathname; const index = pathname.lastIndexOf('/'); - return pathname.substring(index + 1); + return pathname.slice(index + 1); }; export default @injectIntl -- cgit From e989147a9117e640d451085d2a0eb77966761810 Mon Sep 17 00:00:00 2001 From: quinn <87563261+OrichalcumCosmonaut@users.noreply.github.com> Date: Tue, 5 Apr 2022 17:16:21 +1000 Subject: Update composer.scss --- app/javascript/flavours/glitch/styles/components/composer.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss index 96ea096e1..55b1fc599 100644 --- a/app/javascript/flavours/glitch/styles/components/composer.scss +++ b/app/javascript/flavours/glitch/styles/components/composer.scss @@ -206,7 +206,7 @@ sub { font-size: smaller; - text-align: sub; + vertical-align: sub; } ul, ol { -- cgit From 1f0ff1ea9820c6c96d8bd6865e90afd2df83f67e Mon Sep 17 00:00:00 2001 From: quinn <87563261+OrichalcumCosmonaut@users.noreply.github.com> Date: Tue, 5 Apr 2022 17:17:00 +1000 Subject: Update status.scss --- app/javascript/flavours/glitch/styles/components/status.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index 0d7dfd64d..1534ba913 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -125,7 +125,7 @@ sub { font-size: smaller; - text-align: sub; + vertical-align: sub; } sup { -- cgit From a73806ea32a288a2ef18a9ff355a582ce59f722a Mon Sep 17 00:00:00 2001 From: quinn <87563261+OrichalcumCosmonaut@users.noreply.github.com> Date: Tue, 5 Apr 2022 17:35:50 +1000 Subject: Update composer.scss --- app/javascript/flavours/glitch/styles/components/composer.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss index 55b1fc599..3137b2dea 100644 --- a/app/javascript/flavours/glitch/styles/components/composer.scss +++ b/app/javascript/flavours/glitch/styles/components/composer.scss @@ -209,6 +209,11 @@ vertical-align: sub; } + sup { + font-size: smaller; + vertical-align: super; + } + ul, ol { margin-left: 1em; -- cgit From 275dad97024d010853d995350b5224c0a3edc400 Mon Sep 17 00:00:00 2001 From: rinsuki <428rinsuki+git@gmail.com> Date: Tue, 5 Apr 2022 19:00:31 +0900 Subject: fix: returns nil instead of empty URL on status.application.website (#17962) --- app/serializers/rest/status_serializer.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 32c4e405e..6bd6a23e5 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -137,6 +137,10 @@ class REST::StatusSerializer < ActiveModel::Serializer class ApplicationSerializer < ActiveModel::Serializer attributes :name, :website + + def website + object.website.presence + end end class MentionSerializer < ActiveModel::Serializer -- cgit From 95256f26f5160abd1b28f087e119c2a762420524 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Apr 2022 03:01:48 +0900 Subject: Bump pg from 1.3.4 to 1.3.5 (#17953) Bumps [pg](https://github.com/ged/ruby-pg) from 1.3.4 to 1.3.5. - [Release notes](https://github.com/ged/ruby-pg/releases) - [Changelog](https://github.com/ged/ruby-pg/blob/master/History.rdoc) - [Commits](https://github.com/ged/ruby-pg/commits) --- updated-dependencies: - dependency-name: pg dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 16492c41e..1019ebe10 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -437,7 +437,7 @@ GEM parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) - pg (1.3.4) + pg (1.3.5) pghero (2.8.2) activerecord (>= 5) pkg-config (1.4.7) -- cgit From b15b41cb2c0d137af277f83d55afbbe3d41dd3d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Apr 2022 03:02:07 +0900 Subject: Bump ox from 2.14.10 to 2.14.11 (#17954) Bumps [ox](https://github.com/ohler55/ox) from 2.14.10 to 2.14.11. - [Release notes](https://github.com/ohler55/ox/releases) - [Changelog](https://github.com/ohler55/ox/blob/develop/CHANGELOG.md) - [Commits](https://github.com/ohler55/ox/compare/v2.14.10...v2.14.11) --- updated-dependencies: - dependency-name: ox dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1019ebe10..c17a1dedf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -430,7 +430,7 @@ GEM openssl (2.2.0) openssl-signature_algorithm (0.4.0) orm_adapter (0.5.0) - ox (2.14.10) + ox (2.14.11) parallel (1.22.1) parser (3.1.1.0) ast (~> 2.4.1) -- cgit From bf29651fe32d01146c758b88c429ff193868f8ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Apr 2022 03:02:35 +0900 Subject: Bump react-redux from 7.2.6 to 7.2.8 (#17955) Bumps [react-redux](https://github.com/reduxjs/react-redux) from 7.2.6 to 7.2.8. - [Release notes](https://github.com/reduxjs/react-redux/releases) - [Changelog](https://github.com/reduxjs/react-redux/blob/master/CHANGELOG.md) - [Commits](https://github.com/reduxjs/react-redux/compare/v7.2.6...v7.2.8) --- updated-dependencies: - dependency-name: react-redux dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 35966f2f4..86c41c98a 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "react-motion": "^0.5.2", "react-notification": "^6.8.5", "react-overlays": "^0.9.3", - "react-redux": "^7.2.6", + "react-redux": "^7.2.8", "react-redux-loading-bar": "^4.0.8", "react-router-dom": "^4.1.1", "react-router-scroll-4": "^1.0.0-beta.1", diff --git a/yarn.lock b/yarn.lock index f59fcc41b..9cb1ee1b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9011,10 +9011,10 @@ react-redux-loading-bar@^4.0.8: prop-types "^15.6.2" react-lifecycles-compat "^3.0.2" -react-redux@^7.2.6: - version "7.2.6" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.6.tgz#49633a24fe552b5f9caf58feb8a138936ddfe9aa" - integrity sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ== +react-redux@^7.2.8: + version "7.2.8" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.8.tgz#a894068315e65de5b1b68899f9c6ee0923dd28de" + integrity sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw== dependencies: "@babel/runtime" "^7.15.4" "@types/react-redux" "^7.1.20" -- cgit From c35ef5cb498c69769dc791df01d43852df6d4e95 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Apr 2022 03:02:51 +0900 Subject: Bump sidekiq-unique-jobs from 7.1.15 to 7.1.16 (#17956) Bumps [sidekiq-unique-jobs](https://github.com/mhenrixon/sidekiq-unique-jobs) from 7.1.15 to 7.1.16. - [Release notes](https://github.com/mhenrixon/sidekiq-unique-jobs/releases) - [Changelog](https://github.com/mhenrixon/sidekiq-unique-jobs/blob/main/CHANGELOG.md) - [Commits](https://github.com/mhenrixon/sidekiq-unique-jobs/compare/v7.1.15...v7.1.16) --- updated-dependencies: - dependency-name: sidekiq-unique-jobs dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index c17a1dedf..e9e246be9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -604,7 +604,7 @@ GEM sidekiq (>= 3) thwait tilt (>= 1.4.0) - sidekiq-unique-jobs (7.1.15) + sidekiq-unique-jobs (7.1.16) brpoplpush-redis_script (> 0.1.1, <= 2.0.0) concurrent-ruby (~> 1.0, >= 1.0.5) sidekiq (>= 5.0, < 8.0) -- cgit From 76f7759ecbc1e0522c7c88965e9e1d3a7953f0b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Apr 2022 03:03:13 +0900 Subject: Bump redis from 4.0.4 to 4.0.6 (#17957) Bumps [redis](https://github.com/redis/node-redis) from 4.0.4 to 4.0.6. - [Release notes](https://github.com/redis/node-redis/releases) - [Changelog](https://github.com/redis/node-redis/blob/master/CHANGELOG.md) - [Commits](https://github.com/redis/node-redis/compare/redis@4.0.4...redis@4.0.6) --- updated-dependencies: - dependency-name: redis dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 86c41c98a..215c03f39 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "react-swipeable-views": "^0.14.0", "react-textarea-autosize": "^8.3.3", "react-toggle": "^4.1.2", - "redis": "^4.0.4", + "redis": "^4.0.6", "redux": "^4.1.2", "redux-immutable": "^4.0.0", "redux-thunk": "^2.4.1", diff --git a/yarn.lock b/yarn.lock index 9cb1ee1b1..7a1a22eeb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1415,10 +1415,10 @@ resolved "https://registry.yarnpkg.com/@node-redis/bloom/-/bloom-1.0.1.tgz#144474a0b7dc4a4b91badea2cfa9538ce0a1854e" integrity sha512-mXEBvEIgF4tUzdIN89LiYsbi6//EdpFA7L8M+DHCvePXg+bfHWi+ct5VI6nHUFQE5+ohm/9wmgihCH3HSkeKsw== -"@node-redis/client@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@node-redis/client/-/client-1.0.4.tgz#fe185750df3bcc07524f63fe8dbc8d14d22d6cbb" - integrity sha512-IM/NRAqg7MvNC3bIRQipXGrEarunrdgvrbAzsd3ty93LSHi/M+ybQulOERQi8a3M+P5BL8HenwXjiIoKm6ml2g== +"@node-redis/client@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@node-redis/client/-/client-1.0.5.tgz#ebac5e2bbf12214042a37621604973a954ede755" + integrity sha512-ESZ3bd1f+od62h4MaBLKum+klVJfA4wAeLHcVQBkoXa1l0viFesOWnakLQqKg+UyrlJhZmXJWtu0Y9v7iTMrig== dependencies: cluster-key-slot "1.1.0" generic-pool "3.8.2" @@ -1435,10 +1435,10 @@ resolved "https://registry.yarnpkg.com/@node-redis/json/-/json-1.0.2.tgz#8ad2d0f026698dc1a4238cc3d1eb099a3bee5ab8" integrity sha512-qVRgn8WfG46QQ08CghSbY4VhHFgaTY71WjpwRBGEuqGPfWwfRcIf3OqSpR7Q/45X+v3xd8mvYjywqh0wqJ8T+g== -"@node-redis/search@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@node-redis/search/-/search-1.0.3.tgz#7c3d026bf994caf82019fd0c3924cfc09f041a29" - integrity sha512-rsrzkGWI84di/uYtEctS/4qLusWt0DESx/psjfB0TFpORDhe7JfC0h8ary+eHulTksumor244bXLRSqQXbFJmw== +"@node-redis/search@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@node-redis/search/-/search-1.0.5.tgz#96050007eb7c50a7e47080320b4f12aca8cf94c4" + integrity sha512-MCOL8iCKq4v+3HgEQv8zGlSkZyXSXtERgrAJ4TSryIG/eLFy84b57KmNNa/V7M1Q2Wd2hgn2nPCGNcQtk1R1OQ== "@node-redis/time-series@1.0.2": version "1.0.2" @@ -9229,16 +9229,16 @@ redis-parser@3.0.0: dependencies: redis-errors "^1.0.0" -redis@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/redis/-/redis-4.0.4.tgz#b567f82f59086df38433982f7f424b48e924ec7a" - integrity sha512-KaM1OAj/nGrSeybmmOWSMY0LXTGT6FVWgUZZrd2MYzXKJ+VGtqVaciGQeNMfZiQX+kDM8Ke4uttb54m2rm6V0A== +redis@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/redis/-/redis-4.0.6.tgz#a2ded4d9f4f4bad148e54781051618fc684cd858" + integrity sha512-IaPAxgF5dV0jx+A9l6yd6R9/PAChZIoAskDVRzUODeLDNhsMlq7OLLTmu0AwAr0xjrJ1bibW5xdpRwqIQ8Q0Xg== dependencies: "@node-redis/bloom" "1.0.1" - "@node-redis/client" "1.0.4" + "@node-redis/client" "1.0.5" "@node-redis/graph" "1.0.0" "@node-redis/json" "1.0.2" - "@node-redis/search" "1.0.3" + "@node-redis/search" "1.0.5" "@node-redis/time-series" "1.0.2" redux-immutable@^4.0.0: -- cgit From 5e49cb8f0f35f2fa718fb86fccd996661624c02a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Apr 2022 03:03:26 +0900 Subject: Bump prettier from 2.6.1 to 2.6.2 (#17958) Bumps [prettier](https://github.com/prettier/prettier) from 2.6.1 to 2.6.2. - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/2.6.1...2.6.2) --- updated-dependencies: - dependency-name: prettier dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 215c03f39..388eea139 100644 --- a/package.json +++ b/package.json @@ -184,7 +184,7 @@ "eslint-plugin-promise": "~6.0.0", "eslint-plugin-react": "~7.29.4", "jest": "^27.5.1", - "prettier": "^2.6.1", + "prettier": "^2.6.2", "raf": "^3.4.1", "react-intl-translations-manager": "^5.0.3", "react-test-renderer": "^16.14.0", diff --git a/yarn.lock b/yarn.lock index 7a1a22eeb..db9fb41d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8651,10 +8651,10 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= -prettier@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.1.tgz#d472797e0d7461605c1609808e27b80c0f9cfe17" - integrity sha512-8UVbTBYGwN37Bs9LERmxCPjdvPxlEowx2urIL6urHzdb3SDq4B/Z6xLFCblrSnE4iKWcS6ziJ3aOYrc1kz/E2A== +prettier@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.2.tgz#e26d71a18a74c3d0f0597f55f01fb6c06c206032" + integrity sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew== pretty-format@^25.2.1, pretty-format@^25.5.0: version "25.5.0" -- cgit From 04b4b541f81e82f4ee3c8e5283123450df949a4b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Apr 2022 03:03:45 +0900 Subject: Bump sass from 1.49.9 to 1.49.11 (#17959) Bumps [sass](https://github.com/sass/dart-sass) from 1.49.9 to 1.49.11. - [Release notes](https://github.com/sass/dart-sass/releases) - [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md) - [Commits](https://github.com/sass/dart-sass/compare/1.49.9...1.49.11) --- updated-dependencies: - dependency-name: sass dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 388eea139..e39fe7bcb 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,7 @@ "requestidlecallback": "^0.3.0", "reselect": "^4.1.5", "rimraf": "^3.0.2", - "sass": "^1.49.9", + "sass": "^1.49.11", "sass-loader": "^10.2.0", "stacktrace-js": "^2.0.2", "stringz": "^2.1.0", diff --git a/yarn.lock b/yarn.lock index db9fb41d6..aa290014c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9616,10 +9616,10 @@ sass-loader@^10.2.0: schema-utils "^3.0.0" semver "^7.3.2" -sass@^1.49.9: - version "1.49.9" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.49.9.tgz#b15a189ecb0ca9e24634bae5d1ebc191809712f9" - integrity sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A== +sass@^1.49.11: + version "1.49.11" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.49.11.tgz#1ffeb77faeed8b806a2a1e021d7c9fd3fc322cb7" + integrity sha512-wvS/geXgHUGs6A/4ud5BFIWKO1nKd7wYIGimDk4q4GFkJicILActpv9ueMT4eRGSsp1BdKHuw1WwAHXbhsJELQ== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" -- cgit From 9b95077885c4a7c03e9f4124f81cd39e2c6cbf26 Mon Sep 17 00:00:00 2001 From: CommanderRoot Date: Mon, 4 Apr 2022 18:19:45 +0200 Subject: [Glitch] Replace deprecated String.prototype.substr() Port 0ec695e036dab45d57598f451266bd0b176df9fd to glitch-soc Signed-off-by: Tobias Speicher Signed-off-by: Claire --- app/javascript/flavours/glitch/features/status/components/card.js | 2 +- app/javascript/flavours/glitch/features/video/index.js | 2 +- app/javascript/flavours/glitch/util/emoji/emoji_mart_search_light.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/javascript/flavours/glitch/features/status/components/card.js b/app/javascript/flavours/glitch/features/status/components/card.js index 14abe9838..0ca2508e7 100644 --- a/app/javascript/flavours/glitch/features/status/components/card.js +++ b/app/javascript/flavours/glitch/features/status/components/card.js @@ -24,7 +24,7 @@ const trim = (text, len) => { return text; } - return text.substring(0, cut) + (text.length > len ? '…' : ''); + return text.slice(0, cut) + (text.length > len ? '…' : ''); }; const domParser = new DOMParser(); diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js index 53e3dfda3..25c94bb2c 100644 --- a/app/javascript/flavours/glitch/features/video/index.js +++ b/app/javascript/flavours/glitch/features/video/index.js @@ -90,7 +90,7 @@ export const fileNameFromURL = str => { const pathname = url.pathname; const index = pathname.lastIndexOf('/'); - return pathname.substring(index + 1); + return pathname.slice(index + 1); }; export default @injectIntl diff --git a/app/javascript/flavours/glitch/util/emoji/emoji_mart_search_light.js b/app/javascript/flavours/glitch/util/emoji/emoji_mart_search_light.js index e4519a13e..70694ab6d 100644 --- a/app/javascript/flavours/glitch/util/emoji/emoji_mart_search_light.js +++ b/app/javascript/flavours/glitch/util/emoji/emoji_mart_search_light.js @@ -124,7 +124,7 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo for (let id in aPool) { let emoji = aPool[id], { search } = emoji, - sub = value.substr(0, length), + sub = value.slice(0, length), subIndex = search.indexOf(sub); if (subIndex !== -1) { -- cgit From d116cb7733bb535bb72207b20fba9a7d0da371ed Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 6 Apr 2022 20:56:57 +0200 Subject: Fix `GET /api/v1/trends/tags` missing `offset` param in REST API (#17973) --- app/controllers/api/v1/trends/tags_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/v1/trends/tags_controller.rb b/app/controllers/api/v1/trends/tags_controller.rb index d77857871..329ef5ae7 100644 --- a/app/controllers/api/v1/trends/tags_controller.rb +++ b/app/controllers/api/v1/trends/tags_controller.rb @@ -16,7 +16,7 @@ class Api::V1::Trends::TagsController < Api::BaseController def set_tags @tags = begin if Setting.trends - Trends.tags.query.allowed.limit(limit_param(DEFAULT_TAGS_LIMIT)) + Trends.tags.query.allowed.offset(offset_param).limit(limit_param(DEFAULT_TAGS_LIMIT)) else [] end -- cgit From 62c6e12fa58adea57954e395d10d0ffc2c0cd73c Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 6 Apr 2022 20:57:18 +0200 Subject: Fix admin API unconditionally requiring CSRF token (#17975) Fixes #17898 Since #17204, the admin API has only been available through the web application because of the unconditional requirement to provide a valid CSRF token. This commit changes it back to `null_session`, which should make it work both with session-based authentication (provided a CSRF token) and with a bearer token. --- app/controllers/api/v1/admin/account_actions_controller.rb | 2 -- app/controllers/api/v1/admin/accounts_controller.rb | 2 -- app/controllers/api/v1/admin/dimensions_controller.rb | 2 -- app/controllers/api/v1/admin/measures_controller.rb | 2 -- app/controllers/api/v1/admin/reports_controller.rb | 2 -- app/controllers/api/v1/admin/retention_controller.rb | 2 -- app/controllers/api/v1/admin/trends/links_controller.rb | 2 -- app/controllers/api/v1/admin/trends/statuses_controller.rb | 2 -- app/controllers/api/v1/admin/trends/tags_controller.rb | 2 -- 9 files changed, 18 deletions(-) diff --git a/app/controllers/api/v1/admin/account_actions_controller.rb b/app/controllers/api/v1/admin/account_actions_controller.rb index 15af50822..6c9e04402 100644 --- a/app/controllers/api/v1/admin/account_actions_controller.rb +++ b/app/controllers/api/v1/admin/account_actions_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::V1::Admin::AccountActionsController < Api::BaseController - protect_from_forgery with: :exception - before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:accounts' } before_action :require_staff! before_action :set_account diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb index 4b6dab208..dc9d3402f 100644 --- a/app/controllers/api/v1/admin/accounts_controller.rb +++ b/app/controllers/api/v1/admin/accounts_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::V1::Admin::AccountsController < Api::BaseController - protect_from_forgery with: :exception - include Authorization include AccountableConcern diff --git a/app/controllers/api/v1/admin/dimensions_controller.rb b/app/controllers/api/v1/admin/dimensions_controller.rb index b1f738990..49a5be1c3 100644 --- a/app/controllers/api/v1/admin/dimensions_controller.rb +++ b/app/controllers/api/v1/admin/dimensions_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::V1::Admin::DimensionsController < Api::BaseController - protect_from_forgery with: :exception - before_action -> { authorize_if_got_token! :'admin:read' } before_action :require_staff! before_action :set_dimensions diff --git a/app/controllers/api/v1/admin/measures_controller.rb b/app/controllers/api/v1/admin/measures_controller.rb index d64c3cdf7..da95d3422 100644 --- a/app/controllers/api/v1/admin/measures_controller.rb +++ b/app/controllers/api/v1/admin/measures_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::V1::Admin::MeasuresController < Api::BaseController - protect_from_forgery with: :exception - before_action -> { authorize_if_got_token! :'admin:read' } before_action :require_staff! before_action :set_measures diff --git a/app/controllers/api/v1/admin/reports_controller.rb b/app/controllers/api/v1/admin/reports_controller.rb index fbfd0ee12..865ba3d23 100644 --- a/app/controllers/api/v1/admin/reports_controller.rb +++ b/app/controllers/api/v1/admin/reports_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::V1::Admin::ReportsController < Api::BaseController - protect_from_forgery with: :exception - include Authorization include AccountableConcern diff --git a/app/controllers/api/v1/admin/retention_controller.rb b/app/controllers/api/v1/admin/retention_controller.rb index 4af5a5c4d..98d1a3d81 100644 --- a/app/controllers/api/v1/admin/retention_controller.rb +++ b/app/controllers/api/v1/admin/retention_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::V1::Admin::RetentionController < Api::BaseController - protect_from_forgery with: :exception - before_action -> { authorize_if_got_token! :'admin:read' } before_action :require_staff! before_action :set_cohorts diff --git a/app/controllers/api/v1/admin/trends/links_controller.rb b/app/controllers/api/v1/admin/trends/links_controller.rb index 63b3d9358..0a191fe4b 100644 --- a/app/controllers/api/v1/admin/trends/links_controller.rb +++ b/app/controllers/api/v1/admin/trends/links_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::V1::Admin::Trends::LinksController < Api::BaseController - protect_from_forgery with: :exception - before_action -> { authorize_if_got_token! :'admin:read' } before_action :require_staff! before_action :set_links diff --git a/app/controllers/api/v1/admin/trends/statuses_controller.rb b/app/controllers/api/v1/admin/trends/statuses_controller.rb index 86633cc74..cb145f165 100644 --- a/app/controllers/api/v1/admin/trends/statuses_controller.rb +++ b/app/controllers/api/v1/admin/trends/statuses_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::V1::Admin::Trends::StatusesController < Api::BaseController - protect_from_forgery with: :exception - before_action -> { authorize_if_got_token! :'admin:read' } before_action :require_staff! before_action :set_statuses diff --git a/app/controllers/api/v1/admin/trends/tags_controller.rb b/app/controllers/api/v1/admin/trends/tags_controller.rb index 5cc4c269d..9c28b0412 100644 --- a/app/controllers/api/v1/admin/trends/tags_controller.rb +++ b/app/controllers/api/v1/admin/trends/tags_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::V1::Admin::Trends::TagsController < Api::BaseController - protect_from_forgery with: :exception - before_action -> { authorize_if_got_token! :'admin:read' } before_action :require_staff! before_action :set_tags -- cgit From abb11778d7d9ac04fe1feeccf5cefc6d2ed58780 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 6 Apr 2022 20:57:52 +0200 Subject: Fix inconsistency in error handling when removing a status (#17974) Not completely sure this could actually have any ill effect, but if `RemoveStatusService` fails to acquire a lock in an `ActivityPub::ProcessingWorker` job processing a `Delete`, the status is currently discarded and causes a job failure but the next time the job is attempted, it will skip deleting the status due to it being discarded. This commit makes the behavior of `RemoveStatusService` a bit more consistent in case of failure to acquire the lock. --- app/services/remove_status_service.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 159aec1f2..41730154d 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -17,10 +17,10 @@ class RemoveStatusService < BaseService @account = status.account @options = options - @status.discard - RedisLock.acquire(lock_options) do |lock| if lock.acquired? + @status.discard + remove_from_self if @account.local? remove_from_followers remove_from_lists -- cgit From 6221b36b278c02cdbf5b6d1c0753654b506b44fd Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 6 Apr 2022 20:58:12 +0200 Subject: Remove sign-in token authentication, instead send e-mail about new sign-in (#17970) --- .../sign_in_token_authentications_controller.rb | 27 ---- app/controllers/auth/sessions_controller.rb | 9 +- .../sign_in_token_authentication_concern.rb | 56 -------- app/javascript/styles/mailer.scss | 4 + app/lib/suspicious_sign_in_detector.rb | 42 ++++++ app/mailers/user_mailer.rb | 12 +- app/models/user.rb | 16 +-- app/policies/user_policy.rb | 8 -- app/views/admin/accounts/show.html.haml | 8 +- app/views/auth/sessions/sign_in_token.html.haml | 14 -- app/views/user_mailer/sign_in_token.html.haml | 105 -------------- app/views/user_mailer/sign_in_token.text.erb | 17 --- app/views/user_mailer/suspicious_sign_in.html.haml | 67 +++++++++ app/views/user_mailer/suspicious_sign_in.text.erb | 15 ++ config/locales/en.yml | 17 +-- config/routes.rb | 1 - lib/mastodon/accounts_cli.rb | 11 +- spec/controllers/auth/sessions_controller_spec.rb | 151 --------------------- spec/lib/suspicious_sign_in_detector_spec.rb | 57 ++++++++ spec/mailers/previews/user_mailer_preview.rb | 6 +- 20 files changed, 209 insertions(+), 434 deletions(-) delete mode 100644 app/controllers/admin/sign_in_token_authentications_controller.rb delete mode 100644 app/controllers/concerns/sign_in_token_authentication_concern.rb create mode 100644 app/lib/suspicious_sign_in_detector.rb delete mode 100644 app/views/auth/sessions/sign_in_token.html.haml delete mode 100644 app/views/user_mailer/sign_in_token.html.haml delete mode 100644 app/views/user_mailer/sign_in_token.text.erb create mode 100644 app/views/user_mailer/suspicious_sign_in.html.haml create mode 100644 app/views/user_mailer/suspicious_sign_in.text.erb create mode 100644 spec/lib/suspicious_sign_in_detector_spec.rb diff --git a/app/controllers/admin/sign_in_token_authentications_controller.rb b/app/controllers/admin/sign_in_token_authentications_controller.rb deleted file mode 100644 index e620ab292..000000000 --- a/app/controllers/admin/sign_in_token_authentications_controller.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -module Admin - class SignInTokenAuthenticationsController < BaseController - before_action :set_target_user - - def create - authorize @user, :enable_sign_in_token_auth? - @user.update(skip_sign_in_token: false) - log_action :enable_sign_in_token_auth, @user - redirect_to admin_account_path(@user.account_id) - end - - def destroy - authorize @user, :disable_sign_in_token_auth? - @user.update(skip_sign_in_token: true) - log_action :disable_sign_in_token_auth, @user - redirect_to admin_account_path(@user.account_id) - end - - private - - def set_target_user - @user = User.find(params[:user_id]) - end - end -end diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index 4d2695bf5..c4c8151e3 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -8,7 +8,6 @@ class Auth::SessionsController < Devise::SessionsController skip_before_action :update_user_sign_in include TwoFactorAuthenticationConcern - include SignInTokenAuthenticationConcern before_action :set_instance_presenter, only: [:new] before_action :set_body_classes @@ -66,7 +65,7 @@ class Auth::SessionsController < Devise::SessionsController end def user_params - params.require(:user).permit(:email, :password, :otp_attempt, :sign_in_token_attempt, credential: {}) + params.require(:user).permit(:email, :password, :otp_attempt, credential: {}) end def after_sign_in_path_for(resource) @@ -142,6 +141,12 @@ class Auth::SessionsController < Devise::SessionsController ip: request.remote_ip, user_agent: request.user_agent ) + + UserMailer.suspicious_sign_in(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later! if suspicious_sign_in?(user) + end + + def suspicious_sign_in?(user) + SuspiciousSignInDetector.new(user).suspicious?(request) end def on_authentication_failure(user, security_measure, failure_reason) diff --git a/app/controllers/concerns/sign_in_token_authentication_concern.rb b/app/controllers/concerns/sign_in_token_authentication_concern.rb deleted file mode 100644 index 384c5c50c..000000000 --- a/app/controllers/concerns/sign_in_token_authentication_concern.rb +++ /dev/null @@ -1,56 +0,0 @@ -# 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 - if user_params[:email].present? - user = self.resource = find_user_from_params - prompt_for_sign_in_token(user) if user&.external_or_valid_password?(user_params[:password]) - elsif session[:attempt_user_id] - user = self.resource = User.find_by(id: session[:attempt_user_id]) - return if user.nil? - - if session[:attempt_user_updated_at] != user.updated_at.to_s - restart_session - elsif user_params.key?(:sign_in_token_attempt) - authenticate_with_sign_in_token_attempt(user) - end - end - end - - def authenticate_with_sign_in_token_attempt(user) - if valid_sign_in_token_attempt?(user) - on_authentication_success(user, :sign_in_token) - else - on_authentication_failure(user, :sign_in_token, :invalid_sign_in_token) - flash.now[:alert] = I18n.t('users.invalid_sign_in_token') - prompt_for_sign_in_token(user) - end - end - - def prompt_for_sign_in_token(user) - if user.sign_in_token_expired? - user.generate_sign_in_token && user.save - UserMailer.sign_in_token(user, request.remote_ip, request.user_agent, Time.now.utc.to_s).deliver_later! - end - - set_attempt_session(user) - - @body_classes = 'lighter' - - set_locale { render :sign_in_token } - end -end diff --git a/app/javascript/styles/mailer.scss b/app/javascript/styles/mailer.scss index 34852178e..18fe522eb 100644 --- a/app/javascript/styles/mailer.scss +++ b/app/javascript/styles/mailer.scss @@ -435,6 +435,10 @@ h5 { background: $success-green; } + &.warning-icon td { + background: $gold-star; + } + &.alert-icon td { background: $error-red; } diff --git a/app/lib/suspicious_sign_in_detector.rb b/app/lib/suspicious_sign_in_detector.rb new file mode 100644 index 000000000..1af5188c6 --- /dev/null +++ b/app/lib/suspicious_sign_in_detector.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +class SuspiciousSignInDetector + IPV6_TOLERANCE_MASK = 64 + IPV4_TOLERANCE_MASK = 16 + + def initialize(user) + @user = user + end + + def suspicious?(request) + !sufficient_security_measures? && !freshly_signed_up? && !previously_seen_ip?(request) + end + + private + + def sufficient_security_measures? + @user.otp_required_for_login? + end + + def previously_seen_ip?(request) + @user.ips.where('ip <<= ?', masked_ip(request)).exists? + end + + def freshly_signed_up? + @user.current_sign_in_at.blank? + end + + def masked_ip(request) + masked_ip_addr = begin + ip_addr = IPAddr.new(request.remote_ip) + + if ip_addr.ipv6? + ip_addr.mask(IPV6_TOLERANCE_MASK) + else + ip_addr.mask(IPV4_TOLERANCE_MASK) + end + end + + "#{masked_ip_addr}/#{masked_ip_addr.prefix}" + end +end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 1a823328c..ce36dd6f5 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -167,9 +167,7 @@ class UserMailer < Devise::Mailer @statuses = @warning.statuses.includes(:account, :preloadable_poll, :media_attachments, active_mentions: [:account]) I18n.with_locale(@resource.locale || I18n.default_locale) do - mail to: @resource.email, - subject: I18n.t("user_mailer.warning.subject.#{@warning.action}", acct: "@#{user.account.local_username_and_domain}"), - reply_to: ENV['SMTP_REPLY_TO'] + mail to: @resource.email, subject: I18n.t("user_mailer.warning.subject.#{@warning.action}", acct: "@#{user.account.local_username_and_domain}") end end @@ -193,7 +191,7 @@ class UserMailer < Devise::Mailer end end - def sign_in_token(user, remote_ip, user_agent, timestamp) + def suspicious_sign_in(user, remote_ip, user_agent, timestamp) @resource = user @instance = Rails.configuration.x.local_domain @remote_ip = remote_ip @@ -201,12 +199,8 @@ class UserMailer < Devise::Mailer @detection = Browser.new(user_agent) @timestamp = timestamp.to_time.utc - return unless @resource.active_for_authentication? - I18n.with_locale(@resource.locale || I18n.default_locale) do - mail to: @resource.email, - subject: I18n.t('user_mailer.sign_in_token.subject'), - reply_to: ENV['SMTP_REPLY_TO'] + mail to: @resource.email, subject: I18n.t('user_mailer.suspicious_sign_in.subject') end end end diff --git a/app/models/user.rb b/app/models/user.rb index e25c0ddb0..d19fe2c92 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -47,6 +47,7 @@ class User < ApplicationRecord remember_token current_sign_in_ip last_sign_in_ip + skip_sign_in_token ) include Settings::Extend @@ -132,7 +133,7 @@ class User < ApplicationRecord :disable_swiping, to: :settings, prefix: :setting, allow_nil: false - attr_reader :invite_code, :sign_in_token_attempt + attr_reader :invite_code attr_writer :external, :bypass_invite_request_check def confirmed? @@ -200,10 +201,6 @@ class User < ApplicationRecord !account.memorial? end - def suspicious_sign_in?(ip) - !otp_required_for_login? && !skip_sign_in_token? && current_sign_in_at.present? && !ips.where(ip: ip).exists? - end - def functional? confirmed? && approved? && !disabled? && !account.suspended? && !account.memorial? && account.moved_to_account_id.nil? end @@ -368,15 +365,6 @@ class User < ApplicationRecord setting_display_media == 'hide_all' end - def sign_in_token_expired? - sign_in_token_sent_at.nil? || sign_in_token_sent_at < 5.minutes.ago - end - - def generate_sign_in_token - self.sign_in_token = Devise.friendly_token(6) - self.sign_in_token_sent_at = Time.now.utc - end - protected def send_devise_notification(notification, *args, **kwargs) diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 92e2c4f4b..140905e1f 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -13,14 +13,6 @@ class UserPolicy < ApplicationPolicy admin? && !record.staff? end - def disable_sign_in_token_auth? - staff? - end - - def enable_sign_in_token_auth? - staff? - end - def confirm? staff? && !record.confirmed? end diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index 1230294fe..a69832b04 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -128,17 +128,11 @@ %td{ rowspan: can?(:reset_password, @account.user) ? 2 : 1 } - if @account.user&.two_factor_enabled? = t 'admin.accounts.security_measures.password_and_2fa' - - elsif @account.user&.skip_sign_in_token? - = t 'admin.accounts.security_measures.only_password' - else - = t 'admin.accounts.security_measures.password_and_sign_in_token' + = t 'admin.accounts.security_measures.only_password' %td - if @account.user&.two_factor_enabled? = table_link_to 'unlock', t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete if can?(:disable_2fa, @account.user) - - elsif @account.user&.skip_sign_in_token? - = table_link_to 'lock', t('admin.accounts.enable_sign_in_token_auth'), admin_user_sign_in_token_authentication_path(@account.user.id), method: :post if can?(:enable_sign_in_token_auth, @account.user) - - else - = table_link_to 'unlock', t('admin.accounts.disable_sign_in_token_auth'), admin_user_sign_in_token_authentication_path(@account.user.id), method: :delete if can?(:disable_sign_in_token_auth, @account.user) - if can?(:reset_password, @account.user) %tr diff --git a/app/views/auth/sessions/sign_in_token.html.haml b/app/views/auth/sessions/sign_in_token.html.haml deleted file mode 100644 index 8923203cd..000000000 --- a/app/views/auth/sessions/sign_in_token.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -- content_for :page_title do - = t('auth.login') - -= simple_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| - %p.hint.otp-hint= t('users.suspicious_sign_in_confirmation') - - .fields-group - = f.input :sign_in_token_attempt, type: :number, wrapper: :with_label, label: t('simple_form.labels.defaults.sign_in_token_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.sign_in_token_attempt'), :autocomplete => 'off' }, autofocus: true - - .actions - = f.button :button, t('auth.login'), type: :submit - - - if Setting.site_contact_email.present? - %p.hint.subtle-hint= t('users.generic_access_help_html', email: mail_to(Setting.site_contact_email, nil)) diff --git a/app/views/user_mailer/sign_in_token.html.haml b/app/views/user_mailer/sign_in_token.html.haml deleted file mode 100644 index 826b34e7c..000000000 --- a/app/views/user_mailer/sign_in_token.html.haml +++ /dev/null @@ -1,105 +0,0 @@ -%table.email-table{ cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.email-body - .email-container - %table.content-section{ cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.content-cell.hero - .email-row - .col-6 - %table.column{ cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.column-cell.text-center.padded - %table.hero-icon.alert-icon{ align: 'center', cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td - = image_tag full_pack_url('media/images/mailer/icon_email.png'), alt: '' - - %h1= t 'user_mailer.sign_in_token.title' - %p.lead= t 'user_mailer.sign_in_token.explanation' - -%table.email-table{ cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.email-body - .email-container - %table.content-section{ cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.content-cell.content-start - %table.column{ cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.column-cell.input-cell - %table.input{ align: 'center', cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td= @resource.sign_in_token - -%table.email-table{ cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.email-body - .email-container - %table.content-section{ cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.content-cell - .email-row - .col-6 - %table.column{ cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.column-cell.text-center - %p= t 'user_mailer.sign_in_token.details' - %tr - %td.column-cell.text-center - %p - %strong= "#{t('sessions.ip')}:" - = @remote_ip - %br/ - %strong= "#{t('sessions.browser')}:" - %span{ title: @user_agent }= t 'sessions.description', browser: t("sessions.browsers.#{@detection.id}", default: "#{@detection.id}"), platform: t("sessions.platforms.#{@detection.platform.id}", default: "#{@detection.platform.id}") - %br/ - = l(@timestamp) - -%table.email-table{ cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.email-body - .email-container - %table.content-section{ cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.content-cell - .email-row - .col-6 - %table.column{ cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.column-cell.text-center - %p= t 'user_mailer.sign_in_token.further_actions' - -%table.email-table{ cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.email-body - .email-container - %table.content-section{ cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.content-cell - %table.column{ cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.column-cell.button-cell - %table.button{ align: 'center', cellspacing: 0, cellpadding: 0 } - %tbody - %tr - %td.button-primary - = link_to edit_user_registration_url do - %span= t 'settings.account_settings' diff --git a/app/views/user_mailer/sign_in_token.text.erb b/app/views/user_mailer/sign_in_token.text.erb deleted file mode 100644 index 2539ddaf6..000000000 --- a/app/views/user_mailer/sign_in_token.text.erb +++ /dev/null @@ -1,17 +0,0 @@ -<%= t 'user_mailer.sign_in_token.title' %> - -=== - -<%= t 'user_mailer.sign_in_token.explanation' %> - -=> <%= @resource.sign_in_token %> - -<%= t 'user_mailer.sign_in_token.details' %> - -<%= t('sessions.ip') %>: <%= @remote_ip %> -<%= t('sessions.browser') %>: <%= t('sessions.description', browser: t("sessions.browsers.#{@detection.id}", default: "#{@detection.id}"), platform: t("sessions.platforms.#{@detection.platform.id}", default: "#{@detection.platform.id}")) %> -<%= l(@timestamp) %> - -<%= t 'user_mailer.sign_in_token.further_actions' %> - -=> <%= edit_user_registration_url %> diff --git a/app/views/user_mailer/suspicious_sign_in.html.haml b/app/views/user_mailer/suspicious_sign_in.html.haml new file mode 100644 index 000000000..856f9fb7c --- /dev/null +++ b/app/views/user_mailer/suspicious_sign_in.html.haml @@ -0,0 +1,67 @@ +%table.email-table{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.email-body + .email-container + %table.content-section{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.content-cell.hero + .email-row + .col-6 + %table.column{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.column-cell.text-center.padded + %table.hero-icon.warning-icon{ align: 'center', cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td + = image_tag full_pack_url('media/images/mailer/icon_lock_open.png'), alt: '' + + %h1= t 'user_mailer.suspicious_sign_in.title' + %p= t 'user_mailer.suspicious_sign_in.explanation' + +%table.email-table{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.email-body + .email-container + %table.content-section{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.content-cell + .email-row + .col-6 + %table.column{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.column-cell.text-center + %p= t 'user_mailer.suspicious_sign_in.details' + %tr + %td.column-cell.text-center + %p + %strong= "#{t('sessions.ip')}:" + = @remote_ip + %br/ + %strong= "#{t('sessions.browser')}:" + %span{ title: @user_agent }= t 'sessions.description', browser: t("sessions.browsers.#{@detection.id}", default: "#{@detection.id}"), platform: t("sessions.platforms.#{@detection.platform.id}", default: "#{@detection.platform.id}") + %br/ + = l(@timestamp) + +%table.email-table{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.email-body + .email-container + %table.content-section{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.content-cell + .email-row + .col-6 + %table.column{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.column-cell.text-center + %p= t 'user_mailer.suspicious_sign_in.further_actions_html', action: link_to(t('user_mailer.suspicious_sign_in.change_password'), edit_user_registration_url) diff --git a/app/views/user_mailer/suspicious_sign_in.text.erb b/app/views/user_mailer/suspicious_sign_in.text.erb new file mode 100644 index 000000000..7d2ca28e8 --- /dev/null +++ b/app/views/user_mailer/suspicious_sign_in.text.erb @@ -0,0 +1,15 @@ +<%= t 'user_mailer.suspicious_sign_in.title' %> + +=== + +<%= t 'user_mailer.suspicious_sign_in.explanation' %> + +<%= t 'user_mailer.suspicious_sign_in.details' %> + +<%= t('sessions.ip') %>: <%= @remote_ip %> +<%= t('sessions.browser') %>: <%= t('sessions.description', browser: t("sessions.browsers.#{@detection.id}", default: "#{@detection.id}"), platform: t("sessions.platforms.#{@detection.platform.id}", default: "#{@detection.platform.id}")) %> +<%= l(@timestamp) %> + +<%= t 'user_mailer.suspicious_sign_in.further_actions_html', action: t('user_mailer.suspicious_sign_in.change_password') %> + +=> <%= edit_user_registration_url %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 4a16098a8..4fa9abc51 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -199,7 +199,6 @@ en: security_measures: only_password: Only password password_and_2fa: Password and 2FA - password_and_sign_in_token: Password and e-mail token sensitive: Force-sensitive sensitized: Marked as sensitive shared_inbox_url: Shared inbox URL @@ -1634,12 +1633,13 @@ en: explanation: You requested a full backup of your Mastodon account. It's now ready for download! subject: Your archive is ready for download title: Archive takeout - sign_in_token: - details: 'Here are details of the attempt:' - explanation: 'We detected an attempt to sign in to your account from an unrecognized IP address. If this is you, please enter the security code below on the sign in challenge page:' - further_actions: 'If this wasn''t you, please change your password and enable two-factor authentication on your account. You can do so here:' - subject: Please confirm attempted sign in - title: Sign in attempt + suspicious_sign_in: + change_password: change your password + details: 'Here are details of the sign-in:' + explanation: We've detected a sign-in to your account from a new IP address. + further_actions_html: If this wasn't you, we recommend that you %{action} immediately and enable two-factor authentication to keep your account secure. + subject: Your account has been accessed from a new IP address + title: A new sign-in warning: appeal: Submit an appeal appeal_description: If you believe this is an error, you can submit an appeal to the staff of %{instance}. @@ -1690,13 +1690,10 @@ en: title: Welcome aboard, %{name}! users: follow_limit_reached: You cannot follow more than %{limit} people - generic_access_help_html: Trouble accessing your account? You may get in touch with %{email} for assistance invalid_otp_token: Invalid two-factor code - invalid_sign_in_token: Invalid security code otp_lost_help_html: If you lost access to both, you may get in touch with %{email} seamless_external_login: You are logged in via an external service, so password and e-mail settings are not available. signed_in_as: 'Signed in as:' - suspicious_sign_in_confirmation: You appear to not have logged in from this device before, so we're sending a security code to your e-mail address to confirm that it's you. verification: explanation_html: 'You can verify yourself as the owner of the links in your profile metadata. For that, the linked website must contain a link back to your Mastodon profile. The link back must have a rel="me" attribute. The text content of the link does not matter. Here is an example:' verification: Verification diff --git a/config/routes.rb b/config/routes.rb index 7c9a13dc4..b05d31e4e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -295,7 +295,6 @@ Rails.application.routes.draw do resources :users, only: [] do resource :two_factor_authentication, only: [:destroy] - resource :sign_in_token_authentication, only: [:create, :destroy] end resources :custom_emojis, only: [:index, :new, :create] do diff --git a/lib/mastodon/accounts_cli.rb b/lib/mastodon/accounts_cli.rb index 2ef85d0a9..7256d1da9 100644 --- a/lib/mastodon/accounts_cli.rb +++ b/lib/mastodon/accounts_cli.rb @@ -55,7 +55,6 @@ module Mastodon option :email, required: true option :confirmed, type: :boolean option :role, default: 'user', enum: %w(user moderator admin) - option :skip_sign_in_token, type: :boolean option :reattach, type: :boolean option :force, type: :boolean desc 'create USERNAME', 'Create a new user' @@ -69,9 +68,6 @@ module Mastodon With the --role option one of "user", "admin" or "moderator" can be supplied. Defaults to "user" - With the --skip-sign-in-token option, you can ensure that - the user is never asked for an e-mailed security code. - With the --reattach option, the new user will be reattached to a given existing username of an old account. If the old account is still in use by someone else, you can supply @@ -81,7 +77,7 @@ module Mastodon def create(username) account = Account.new(username: username) password = SecureRandom.hex - user = User.new(email: options[:email], password: password, agreement: true, approved: true, admin: options[:role] == 'admin', moderator: options[:role] == 'moderator', confirmed_at: options[:confirmed] ? Time.now.utc : nil, bypass_invite_request_check: true, skip_sign_in_token: options[:skip_sign_in_token]) + user = User.new(email: options[:email], password: password, agreement: true, approved: true, admin: options[:role] == 'admin', moderator: options[:role] == 'moderator', confirmed_at: options[:confirmed] ? Time.now.utc : nil, bypass_invite_request_check: true) if options[:reattach] account = Account.find_local(username) || Account.new(username: username) @@ -125,7 +121,6 @@ module Mastodon option :disable_2fa, type: :boolean option :approve, type: :boolean option :reset_password, type: :boolean - option :skip_sign_in_token, type: :boolean desc 'modify USERNAME', 'Modify a user' long_desc <<-LONG_DESC Modify a user account. @@ -147,9 +142,6 @@ module Mastodon With the --reset-password option, the user's password is replaced by a randomly-generated one, printed in the output. - - With the --skip-sign-in-token option, you can ensure that - the user is never asked for an e-mailed security code. LONG_DESC def modify(username) user = Account.find_local(username)&.user @@ -171,7 +163,6 @@ module Mastodon user.disabled = true if options[:disable] user.approved = true if options[:approve] user.otp_required_for_login = false if options[:disable_2fa] - user.skip_sign_in_token = options[:skip_sign_in_token] unless options[:skip_sign_in_token].nil? user.confirm if options[:confirm] if user.save diff --git a/spec/controllers/auth/sessions_controller_spec.rb b/spec/controllers/auth/sessions_controller_spec.rb index 64ec7b794..1b8fd0b7b 100644 --- a/spec/controllers/auth/sessions_controller_spec.rb +++ b/spec/controllers/auth/sessions_controller_spec.rb @@ -225,22 +225,6 @@ RSpec.describe Auth::SessionsController, type: :controller do end end - context 'using email and password after an unfinished log-in attempt with a sign-in token challenge' do - let!(:other_user) do - Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: false, current_sign_in_at: 1.month.ago) - end - - before do - post :create, params: { user: { email: other_user.email, password: other_user.password } } - post :create, params: { user: { email: user.email, password: user.password } } - end - - it 'renders two factor authentication page' do - expect(controller).to render_template("two_factor") - expect(controller).to render_template(partial: "_otp_authentication_form") - end - end - context 'using upcase email and password' do before do post :create, params: { user: { email: user.email.upcase, password: user.password } } @@ -266,21 +250,6 @@ RSpec.describe Auth::SessionsController, type: :controller do end end - context 'using a valid OTP, attempting to leverage previous half-login to bypass password auth' do - let!(:other_user) do - Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: false, current_sign_in_at: 1.month.ago) - end - - before do - post :create, params: { user: { email: other_user.email, password: other_user.password } } - post :create, params: { user: { email: user.email, otp_attempt: user.current_otp } }, session: { attempt_user_updated_at: user.updated_at.to_s } - end - - it "doesn't log the user in" do - expect(controller.current_user).to be_nil - end - end - context 'when the server has an decryption error' do before do allow_any_instance_of(User).to receive(:validate_and_consume_otp!).and_raise(OpenSSL::Cipher::CipherError) @@ -401,126 +370,6 @@ RSpec.describe Auth::SessionsController, type: :controller do end end end - - context 'when 2FA is disabled and IP is unfamiliar' do - let!(:user) { Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', current_sign_in_at: 3.weeks.ago) } - - before do - request.remote_ip = '10.10.10.10' - request.user_agent = 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0' - - allow(UserMailer).to receive(:sign_in_token).and_return(double('email', deliver_later!: nil)) - end - - context 'using email and password' do - before do - post :create, params: { user: { email: user.email, password: user.password } } - end - - it 'renders sign in token authentication page' do - expect(controller).to render_template("sign_in_token") - end - - it 'generates sign in token' do - expect(user.reload.sign_in_token).to_not be_nil - end - - it 'sends sign in token e-mail' do - expect(UserMailer).to have_received(:sign_in_token) - end - end - - context 'using email and password after an unfinished log-in attempt to a 2FA-protected account' do - let!(:other_user) do - Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32)) - end - - before do - post :create, params: { user: { email: other_user.email, password: other_user.password } } - post :create, params: { user: { email: user.email, password: user.password } } - end - - it 'renders sign in token authentication page' do - expect(controller).to render_template("sign_in_token") - end - - it 'generates sign in token' do - expect(user.reload.sign_in_token).to_not be_nil - end - - it 'sends sign in token e-mail' do - expect(UserMailer).to have_received(:sign_in_token) - end - end - - context 'using email and password after an unfinished log-in attempt with a sign-in token challenge' do - let!(:other_user) do - Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: false, current_sign_in_at: 1.month.ago) - end - - before do - post :create, params: { user: { email: other_user.email, password: other_user.password } } - post :create, params: { user: { email: user.email, password: user.password } } - end - - it 'renders sign in token authentication page' do - expect(controller).to render_template("sign_in_token") - end - - it 'generates sign in token' do - expect(user.reload.sign_in_token).to_not be_nil - end - - it 'sends sign in token e-mail' do - expect(UserMailer).to have_received(:sign_in_token).with(user, any_args) - end - end - - context 'using a valid sign in token' do - before do - user.generate_sign_in_token && user.save - post :create, params: { user: { sign_in_token_attempt: user.sign_in_token } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s } - end - - it 'redirects to home' do - expect(response).to redirect_to(root_path) - end - - it 'logs the user in' do - expect(controller.current_user).to eq user - end - end - - context 'using a valid sign in token, attempting to leverage previous half-login to bypass password auth' do - let!(:other_user) do - Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: false, current_sign_in_at: 1.month.ago) - end - - before do - user.generate_sign_in_token && user.save - post :create, params: { user: { email: other_user.email, password: other_user.password } } - post :create, params: { user: { email: user.email, sign_in_token_attempt: user.sign_in_token } }, session: { attempt_user_updated_at: user.updated_at.to_s } - end - - it "doesn't log the user in" do - expect(controller.current_user).to be_nil - end - end - - context 'using an invalid sign in token' do - before do - post :create, params: { user: { sign_in_token_attempt: 'wrongotp' } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s } - end - - it 'shows a login error' do - expect(flash[:alert]).to match I18n.t('users.invalid_sign_in_token') - end - - it "doesn't log the user in" do - expect(controller.current_user).to be_nil - end - end - end end describe 'GET #webauthn_options' do diff --git a/spec/lib/suspicious_sign_in_detector_spec.rb b/spec/lib/suspicious_sign_in_detector_spec.rb new file mode 100644 index 000000000..101a18aa0 --- /dev/null +++ b/spec/lib/suspicious_sign_in_detector_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' + +RSpec.describe SuspiciousSignInDetector do + describe '#suspicious?' do + let(:user) { Fabricate(:user, current_sign_in_at: 1.day.ago) } + let(:request) { double(remote_ip: remote_ip) } + let(:remote_ip) { nil } + + subject { described_class.new(user).suspicious?(request) } + + context 'when user has 2FA enabled' do + before do + user.update!(otp_required_for_login: true) + end + + it 'returns false' do + expect(subject).to be false + end + end + + context 'when exact IP has been used before' do + let(:remote_ip) { '1.1.1.1' } + + before do + user.update!(sign_up_ip: remote_ip) + end + + it 'returns false' do + expect(subject).to be false + end + end + + context 'when similar IP has been used before' do + let(:remote_ip) { '1.1.2.2' } + + before do + user.update!(sign_up_ip: '1.1.1.1') + end + + it 'returns false' do + expect(subject).to be false + end + end + + context 'when IP is completely unfamiliar' do + let(:remote_ip) { '2.2.2.2' } + + before do + user.update!(sign_up_ip: '1.1.1.1') + end + + it 'returns true' do + expect(subject).to be true + end + end + end +end diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb index 8de7d8669..95712e6cf 100644 --- a/spec/mailers/previews/user_mailer_preview.rb +++ b/spec/mailers/previews/user_mailer_preview.rb @@ -87,8 +87,8 @@ class UserMailerPreview < ActionMailer::Preview UserMailer.appeal_approved(User.first, Appeal.last) end - # Preview this email at http://localhost:3000/rails/mailers/user_mailer/sign_in_token - def sign_in_token - UserMailer.sign_in_token(User.first.tap { |user| user.generate_sign_in_token }, '127.0.0.1', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0', Time.now.utc) + # Preview this email at http://localhost:3000/rails/mailers/user_mailer/suspicious_sign_in + def suspicious_sign_in + UserMailer.suspicious_sign_in(User.first, '127.0.0.1', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0', Time.now.utc) end end -- cgit From 454ef42aab48e73613c4588faaacfb5941bd3e6a Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 6 Apr 2022 20:58:23 +0200 Subject: Fix error when encountering invalid pinned posts (#17964) --- .../activitypub/fetch_featured_collection_service.rb | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/app/services/activitypub/fetch_featured_collection_service.rb b/app/services/activitypub/fetch_featured_collection_service.rb index 780741feb..66234b711 100644 --- a/app/services/activitypub/fetch_featured_collection_service.rb +++ b/app/services/activitypub/fetch_featured_collection_service.rb @@ -22,9 +22,19 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService private def process_items(items) - status_ids = items.map { |item| value_or_id(item) } - .filter_map { |uri| ActivityPub::FetchRemoteStatusService.new.call(uri, on_behalf_of: local_follower) unless ActivityPub::TagManager.instance.local_uri?(uri) } - .filter_map { |status| status.id if status.account_id == @account.id } + status_ids = items.filter_map do |item| + uri = value_or_id(item) + next if ActivityPub::TagManager.instance.local_uri?(uri) + + status = ActivityPub::FetchRemoteStatusService.new.call(uri, on_behalf_of: local_follower) + next unless status.account_id == @account.id + + status.id + rescue ActiveRecord::RecordInvalid => e + Rails.logger.debug "Invalid pinned status #{uri}: #{e.message}" + nil + end + to_remove = [] to_add = status_ids -- cgit From 8f91e304a5adb98b657a5c096359d0423a5d7e84 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 6 Apr 2022 21:01:02 +0200 Subject: Fix spurious edits and require incoming edits to be explicitly marked as such (#17918) * Change post text edit to not be considered significant if it's identical after reformatting * We don't need to clear previous change information anymore * Require status edits to be explicit, except for poll tallies * Fix tests * Add some tests * Add poll-related tests * Add HTML-formatting related tests --- .../activitypub/process_status_update_service.rb | 50 ++++-- .../fetch_remote_status_service_spec.rb | 4 +- .../process_status_update_service_spec.rb | 178 +++++++++++++++++++++ 3 files changed, 220 insertions(+), 12 deletions(-) diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index 6dc14d8c2..3d9d9cb84 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -17,10 +17,19 @@ class ActivityPub::ProcessStatusUpdateService < BaseService # Only native types can be updated at the moment return @status if !expected_type? || already_updated_more_recently? - last_edit_date = status.edited_at.presence || status.created_at + if @status_parser.edited_at.present? && (@status.edited_at.nil? || @status_parser.edited_at > @status.edited_at) + handle_explicit_update! + else + handle_implicit_update! + end + + @status + end + + private - # Since we rely on tracking of previous changes, ensure clean slate - status.clear_changes_information + def handle_explicit_update! + last_edit_date = @status.edited_at.presence || @status.created_at # Only allow processing one create/update per status at a time RedisLock.acquire(lock_options) do |lock| @@ -45,12 +54,20 @@ class ActivityPub::ProcessStatusUpdateService < BaseService end end - forward_activity! if significant_changes? && @status_parser.edited_at.present? && @status_parser.edited_at > last_edit_date - - @status + forward_activity! if significant_changes? && @status_parser.edited_at > last_edit_date end - private + def handle_implicit_update! + RedisLock.acquire(lock_options) do |lock| + if lock.acquired? + update_poll!(allow_significant_changes: false) + else + raise Mastodon::RaceConditionError + end + end + + queue_poll_notifications! + end def update_media_attachments! previous_media_attachments = @status.media_attachments.to_a @@ -98,7 +115,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService @media_attachments_changed = true if @status.ordered_media_attachment_ids != previous_media_attachments_ids end - def update_poll! + def update_poll!(allow_significant_changes: true) previous_poll = @status.preloadable_poll @previous_expires_at = previous_poll&.expires_at poll_parser = ActivityPub::Parser::PollParser.new(@json) @@ -109,6 +126,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService # If for some reasons the options were changed, it invalidates all previous # votes, so we need to remove them @poll_changed = true if poll_parser.significantly_changes?(poll) + return if @poll_changed && !allow_significant_changes poll.last_fetched_at = Time.now.utc poll.options = poll_parser.options @@ -121,6 +139,8 @@ class ActivityPub::ProcessStatusUpdateService < BaseService @status.poll_id = poll.id elsif previous_poll.present? + return unless allow_significant_changes + previous_poll.destroy! @poll_changed = true @status.poll_id = nil @@ -132,7 +152,10 @@ class ActivityPub::ProcessStatusUpdateService < BaseService @status.spoiler_text = @status_parser.spoiler_text || '' @status.sensitive = @account.sensitized? || @status_parser.sensitive || false @status.language = @status_parser.language - @status.edited_at = @status_parser.edited_at || Time.now.utc if significant_changes? + + @significant_changes = text_significantly_changed? || @status.spoiler_text_changed? || @media_attachments_changed || @poll_changed + + @status.edited_at = @status_parser.edited_at if significant_changes? @status.save! end @@ -243,7 +266,14 @@ class ActivityPub::ProcessStatusUpdateService < BaseService end def significant_changes? - @status.text_changed? || @status.text_previously_changed? || @status.spoiler_text_changed? || @status.spoiler_text_previously_changed? || @media_attachments_changed || @poll_changed + @significant_changes + end + + def text_significantly_changed? + return false unless @status.text_changed? + + old, new = @status.text_change + HtmlAwareFormatter.new(old, false).to_s != HtmlAwareFormatter.new(new, false).to_s end def already_updated_more_recently? diff --git a/spec/services/activitypub/fetch_remote_status_service_spec.rb b/spec/services/activitypub/fetch_remote_status_service_spec.rb index 68816e554..943cb161d 100644 --- a/spec/services/activitypub/fetch_remote_status_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_status_service_spec.rb @@ -195,7 +195,7 @@ RSpec.describe ActivityPub::FetchRemoteStatusService, type: :service do let(:existing_status) { Fabricate(:status, account: sender, text: 'Foo', uri: note[:id]) } context 'with a Note object' do - let(:object) { note } + let(:object) { note.merge(updated: '2021-09-08T22:39:25Z') } it 'updates status' do existing_status.reload @@ -211,7 +211,7 @@ RSpec.describe ActivityPub::FetchRemoteStatusService, type: :service do id: "https://#{valid_domain}/@foo/1234/create", type: 'Create', actor: ActivityPub::TagManager.instance.uri_for(sender), - object: note, + object: note.merge(updated: '2021-09-08T22:39:25Z'), } end diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index f87adcae1..481572742 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -1,5 +1,9 @@ require 'rails_helper' +def poll_option_json(name, votes) + { type: 'Note', name: name, replies: { type: 'Collection', totalItems: votes } } +end + RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do let!(:status) { Fabricate(:status, text: 'Hello world', account: Fabricate(:account, domain: 'example.com')) } @@ -46,6 +50,180 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do expect(status.reload.spoiler_text).to eq 'Show more' end + context 'when the changes are only in sanitized-out HTML' do + let!(:status) { Fabricate(:status, text: '

Hello world joinmastodon.org

', account: Fabricate(:account, domain: 'example.com')) } + + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'foo', + type: 'Note', + updated: '2021-09-08T22:39:25Z', + content: '

Hello world joinmastodon.org

', + } + end + + before do + subject.call(status, json) + end + + it 'does not create any edits' do + expect(status.reload.edits).to be_empty + end + + it 'does not mark status as edited' do + expect(status.edited?).to be false + end + end + + context 'when the status has not been explicitly edited' do + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'foo', + type: 'Note', + content: 'Updated text', + } + end + + before do + subject.call(status, json) + end + + it 'does not create any edits' do + expect(status.reload.edits).to be_empty + end + + it 'does not mark status as edited' do + expect(status.reload.edited?).to be false + end + + it 'does not update the text' do + expect(status.reload.text).to eq 'Hello world' + end + end + + context 'when the status has not been explicitly edited and features a poll' do + let(:account) { Fabricate(:account, domain: 'example.com') } + let!(:expiration) { 10.days.from_now.utc } + let!(:status) do + Fabricate(:status, + text: 'Hello world', + account: account, + poll_attributes: { + options: %w(Foo Bar), + account: account, + multiple: false, + hide_totals: false, + expires_at: expiration + } + ) + end + + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'https://example.com/foo', + type: 'Question', + content: 'Hello world', + endTime: expiration.iso8601, + oneOf: [ + poll_option_json('Foo', 4), + poll_option_json('Bar', 3), + ], + } + end + + before do + subject.call(status, json) + end + + it 'does not create any edits' do + expect(status.reload.edits).to be_empty + end + + it 'does not mark status as edited' do + expect(status.reload.edited?).to be false + end + + it 'does not update the text' do + expect(status.reload.text).to eq 'Hello world' + end + + it 'updates tallies' do + expect(status.poll.reload.cached_tallies).to eq [4, 3] + end + end + + context 'when the status changes a poll despite being not explicitly marked as updated' do + let(:account) { Fabricate(:account, domain: 'example.com') } + let!(:expiration) { 10.days.from_now.utc } + let!(:status) do + Fabricate(:status, + text: 'Hello world', + account: account, + poll_attributes: { + options: %w(Foo Bar), + account: account, + multiple: false, + hide_totals: false, + expires_at: expiration + } + ) + end + + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'https://example.com/foo', + type: 'Question', + content: 'Hello world', + endTime: expiration.iso8601, + oneOf: [ + poll_option_json('Foo', 4), + poll_option_json('Bar', 3), + poll_option_json('Baz', 3), + ], + } + end + + before do + subject.call(status, json) + end + + it 'does not create any edits' do + expect(status.reload.edits).to be_empty + end + + it 'does not mark status as edited' do + expect(status.reload.edited?).to be false + end + + it 'does not update the text' do + expect(status.reload.text).to eq 'Hello world' + end + + it 'does not update tallies' do + expect(status.poll.reload.cached_tallies).to eq [0, 0] + end + end + + context 'when receiving an edit older than the latest processed' do + before do + status.snapshot!(at_time: status.created_at, rate_limit: false) + status.update!(text: 'Hello newer world', edited_at: Time.now.utc) + status.snapshot!(rate_limit: false) + end + + it 'does not create any edits' do + expect { subject.call(status, json) }.not_to change { status.reload.edits.pluck(&:id) } + end + + it 'does not update the text, spoiler_text or edited_at' do + expect { subject.call(status, json) }.not_to change { s = status.reload; [s.text, s.spoiler_text, s.edited_at] } + end + end + context 'with no changes at all' do let(:payload) do { -- cgit From dd4c156f33a24b8bb89b45b2697aa4036c3ae5be Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 6 Apr 2022 21:01:41 +0200 Subject: Fix possible duplicate statuses in timelines in some edge cases (#17971) In some rare cases, when receiving statuses out of order from the streaming API then polling from the REST API, it was possible for the `expandNormalizedTimeline` function to insert duplicates in the timeline, which would then result in several bugs. This commits ensures that there are no duplicates inserted in the timeline. --- app/javascript/mastodon/reducers/timelines.js | 37 ++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index b66c19fd5..301997567 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -16,7 +16,7 @@ import { ACCOUNT_MUTE_SUCCESS, ACCOUNT_UNFOLLOW_SUCCESS, } from '../actions/accounts'; -import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; import compareId from '../compare_id'; const initialState = ImmutableMap(); @@ -32,6 +32,13 @@ const initialTimeline = ImmutableMap({ }); const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent, usePendingItems) => { + // This method is pretty tricky because: + // - existing items in the timeline might be out of order + // - the existing timeline may have gaps, most often explicitly noted with a `null` item + // - ideally, we don't want it to reorder existing items of the timeline + // - `statuses` may include items that are already included in the timeline + // - this function can be called either to fill in a gap, or load newer items + return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { mMap.set('isLoading', false); mMap.set('isPartial', isPartial); @@ -46,15 +53,33 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is mMap.update(usePendingItems ? 'pendingItems' : 'items', ImmutableList(), oldIds => { const newIds = statuses.map(status => status.get('id')); + // Now this gets tricky, as we don't necessarily know for sure where the gap to fill is + // and some items in the timeline may not be properly ordered. + + // However, we know that `newIds.last()` is the oldest item that was requested and that + // there is no “hole” between `newIds.last()` and `newIds.first()`. + + // First, find the furthest (if properly sorted, oldest) item in the timeline that is + // newer than the oldest fetched one, as it's most likely that it delimits the gap. + // Start the gap *after* that item. const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1; - const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0); - if (firstIndex < 0) { - return (isPartial ? newIds.unshift(null) : newIds).concat(oldIds.skip(lastIndex)); + // Then, try to find the furthest (if properly sorted, oldest) item in the timeline that + // is newer than the most recent fetched one, as it delimits a section comprised of only + // items present in `newIds` (or that were deleted from the server, so should be removed + // anyway). + // Stop the gap *after* that item. + const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0) + 1; + + // Make sure we aren't inserting duplicates + let insertedIds = ImmutableOrderedSet(newIds).subtract(oldIds.take(firstIndex), oldIds.skip(lastIndex)).toList(); + // Finally, insert a gap marker if the data is marked as partial by the server + if (isPartial && (firstIndex === 0 || oldIds.get(firstIndex - 1) !== null)) { + insertedIds = insertedIds.unshift(null); } - return oldIds.take(firstIndex + 1).concat( - isPartial && oldIds.get(firstIndex) !== null ? newIds.unshift(null) : newIds, + return oldIds.take(firstIndex).concat( + insertedIds, oldIds.skip(lastIndex), ); }); -- cgit From e2f4bafc136cb0853321ed4be1b02106bec8bc93 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 6 Apr 2022 21:01:41 +0200 Subject: [Glitch] Fix possible duplicate statuses in timelines in some edge cases Port dd4c156f33a24b8bb89b45b2697aa4036c3ae5be to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/reducers/timelines.js | 38 ++++++++++++++++++---- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/app/javascript/flavours/glitch/reducers/timelines.js b/app/javascript/flavours/glitch/reducers/timelines.js index 7d815d850..b40e74c5b 100644 --- a/app/javascript/flavours/glitch/reducers/timelines.js +++ b/app/javascript/flavours/glitch/reducers/timelines.js @@ -16,7 +16,7 @@ import { ACCOUNT_MUTE_SUCCESS, ACCOUNT_UNFOLLOW_SUCCESS, } from 'flavours/glitch/actions/accounts'; -import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; import compareId from 'flavours/glitch/util/compare_id'; const initialState = ImmutableMap(); @@ -32,6 +32,13 @@ const initialTimeline = ImmutableMap({ }); const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent, usePendingItems) => { + // This method is pretty tricky because: + // - existing items in the timeline might be out of order + // - the existing timeline may have gaps, most often explicitly noted with a `null` item + // - ideally, we don't want it to reorder existing items of the timeline + // - `statuses` may include items that are already included in the timeline + // - this function can be called either to fill in a gap, or load newer items + return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { mMap.set('isLoading', false); mMap.set('isPartial', isPartial); @@ -45,15 +52,34 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is mMap.update(usePendingItems ? 'pendingItems' : 'items', ImmutableList(), oldIds => { const newIds = statuses.map(status => status.get('id')); + + // Now this gets tricky, as we don't necessarily know for sure where the gap to fill is + // and some items in the timeline may not be properly ordered. + + // However, we know that `newIds.last()` is the oldest item that was requested and that + // there is no “hole” between `newIds.last()` and `newIds.first()`. + + // First, find the furthest (if properly sorted, oldest) item in the timeline that is + // newer than the oldest fetched one, as it's most likely that it delimits the gap. + // Start the gap *after* that item. const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1; - const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0); - if (firstIndex < 0) { - return (isPartial ? newIds.unshift(null) : newIds).concat(oldIds.skip(lastIndex)); + // Then, try to find the furthest (if properly sorted, oldest) item in the timeline that + // is newer than the most recent fetched one, as it delimits a section comprised of only + // items present in `newIds` (or that were deleted from the server, so should be removed + // anyway). + // Stop the gap *after* that item. + const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0) + 1; + + // Make sure we aren't inserting duplicates + let insertedIds = ImmutableOrderedSet(newIds).subtract(oldIds.take(firstIndex), oldIds.skip(lastIndex)).toList(); + // Finally, insert a gap marker if the data is marked as partial by the server + if (isPartial && (firstIndex === 0 || oldIds.get(firstIndex - 1) !== null)) { + insertedIds = insertedIds.unshift(null); } - return oldIds.take(firstIndex + 1).concat( - isPartial && oldIds.get(firstIndex) !== null ? newIds.unshift(null) : newIds, + return oldIds.take(firstIndex).concat( + insertedIds, oldIds.skip(lastIndex), ); }); -- cgit From f382192862893c48cf97f13e9fbfb85b80cdc97d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 6 Apr 2022 22:53:29 +0200 Subject: Add pagination for trending statuses in web UI (#17976) --- app/javascript/mastodon/actions/trends.js | 54 ++++++++++++++++++++-- .../mastodon/features/explore/statuses.js | 15 ++++-- app/javascript/mastodon/reducers/status_lists.js | 7 +++ 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/app/javascript/mastodon/actions/trends.js b/app/javascript/mastodon/actions/trends.js index 304bbebef..edda0b5b5 100644 --- a/app/javascript/mastodon/actions/trends.js +++ b/app/javascript/mastodon/actions/trends.js @@ -1,4 +1,4 @@ -import api from '../api'; +import api, { getLinks } from '../api'; import { importFetchedStatuses } from './importer'; export const TRENDS_TAGS_FETCH_REQUEST = 'TRENDS_TAGS_FETCH_REQUEST'; @@ -13,6 +13,10 @@ export const TRENDS_STATUSES_FETCH_REQUEST = 'TRENDS_STATUSES_FETCH_REQUEST'; export const TRENDS_STATUSES_FETCH_SUCCESS = 'TRENDS_STATUSES_FETCH_SUCCESS'; export const TRENDS_STATUSES_FETCH_FAIL = 'TRENDS_STATUSES_FETCH_FAIL'; +export const TRENDS_STATUSES_EXPAND_REQUEST = 'TRENDS_STATUSES_EXPAND_REQUEST'; +export const TRENDS_STATUSES_EXPAND_SUCCESS = 'TRENDS_STATUSES_EXPAND_SUCCESS'; +export const TRENDS_STATUSES_EXPAND_FAIL = 'TRENDS_STATUSES_EXPAND_FAIL'; + export const fetchTrendingHashtags = () => (dispatch, getState) => { dispatch(fetchTrendingHashtagsRequest()); @@ -68,11 +72,16 @@ export const fetchTrendingLinksFail = error => ({ }); export const fetchTrendingStatuses = () => (dispatch, getState) => { + if (getState().getIn(['status_lists', 'trending', 'isLoading'])) { + return; + } + dispatch(fetchTrendingStatusesRequest()); - api(getState).get('/api/v1/trends/statuses').then(({ data }) => { - dispatch(importFetchedStatuses(data)); - dispatch(fetchTrendingStatusesSuccess(data)); + api(getState).get('/api/v1/trends/statuses').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); + dispatch(fetchTrendingStatusesSuccess(response.data, next ? next.uri : null)); }).catch(err => dispatch(fetchTrendingStatusesFail(err))); }; @@ -81,9 +90,10 @@ export const fetchTrendingStatusesRequest = () => ({ skipLoading: true, }); -export const fetchTrendingStatusesSuccess = statuses => ({ +export const fetchTrendingStatusesSuccess = (statuses, next) => ({ type: TRENDS_STATUSES_FETCH_SUCCESS, statuses, + next, skipLoading: true, }); @@ -93,3 +103,37 @@ export const fetchTrendingStatusesFail = error => ({ skipLoading: true, skipAlert: true, }); + + +export const expandTrendingStatuses = () => (dispatch, getState) => { + const url = getState().getIn(['status_lists', 'trending', 'next'], null); + + if (url === null || getState().getIn(['status_lists', 'trending', 'isLoading'])) { + return; + } + + dispatch(expandTrendingStatusesRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); + dispatch(expandTrendingStatusesSuccess(response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(expandTrendingStatusesFail(error)); + }); +}; + +export const expandTrendingStatusesRequest = () => ({ + type: TRENDS_STATUSES_EXPAND_REQUEST, +}); + +export const expandTrendingStatusesSuccess = (statuses, next) => ({ + type: TRENDS_STATUSES_EXPAND_SUCCESS, + statuses, + next, +}); + +export const expandTrendingStatusesFail = error => ({ + type: TRENDS_STATUSES_EXPAND_FAIL, + error, +}); diff --git a/app/javascript/mastodon/features/explore/statuses.js b/app/javascript/mastodon/features/explore/statuses.js index 4e5530d84..33e5b4179 100644 --- a/app/javascript/mastodon/features/explore/statuses.js +++ b/app/javascript/mastodon/features/explore/statuses.js @@ -4,11 +4,13 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import StatusList from 'mastodon/components/status_list'; import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; -import { fetchTrendingStatuses } from 'mastodon/actions/trends'; +import { fetchTrendingStatuses, expandTrendingStatuses } from 'mastodon/actions/trends'; +import { debounce } from 'lodash'; const mapStateToProps = state => ({ statusIds: state.getIn(['status_lists', 'trending', 'items']), isLoading: state.getIn(['status_lists', 'trending', 'isLoading'], true), + hasMore: !!state.getIn(['status_lists', 'trending', 'next']), }); export default @connect(mapStateToProps) @@ -17,6 +19,7 @@ class Statuses extends React.PureComponent { static propTypes = { statusIds: ImmutablePropTypes.list, isLoading: PropTypes.bool, + hasMore: PropTypes.bool, multiColumn: PropTypes.bool, dispatch: PropTypes.func.isRequired, }; @@ -26,8 +29,13 @@ class Statuses extends React.PureComponent { dispatch(fetchTrendingStatuses()); } + handleLoadMore = debounce(() => { + const { dispatch } = this.props; + dispatch(expandTrendingStatuses()); + }, 300, { leading: true }) + render () { - const { isLoading, statusIds, multiColumn } = this.props; + const { isLoading, hasMore, statusIds, multiColumn } = this.props; const emptyMessage = ; @@ -36,8 +44,9 @@ class Statuses extends React.PureComponent { trackScroll statusIds={statusIds} scrollKey='explore-statuses' - hasMore={false} + hasMore={hasMore} isLoading={isLoading} + onLoadMore={this.handleLoadMore} emptyMessage={emptyMessage} bindToDocument={!multiColumn} withCounters diff --git a/app/javascript/mastodon/reducers/status_lists.js b/app/javascript/mastodon/reducers/status_lists.js index 49bc94a40..a7c56cc19 100644 --- a/app/javascript/mastodon/reducers/status_lists.js +++ b/app/javascript/mastodon/reducers/status_lists.js @@ -21,6 +21,9 @@ import { TRENDS_STATUSES_FETCH_REQUEST, TRENDS_STATUSES_FETCH_SUCCESS, TRENDS_STATUSES_FETCH_FAIL, + TRENDS_STATUSES_EXPAND_REQUEST, + TRENDS_STATUSES_EXPAND_SUCCESS, + TRENDS_STATUSES_EXPAND_FAIL, } from '../actions/trends'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { @@ -111,11 +114,15 @@ export default function statusLists(state = initialState, action) { case BOOKMARKED_STATUSES_EXPAND_SUCCESS: return appendToList(state, 'bookmarks', action.statuses, action.next); case TRENDS_STATUSES_FETCH_REQUEST: + case TRENDS_STATUSES_EXPAND_REQUEST: return state.setIn(['trending', 'isLoading'], true); case TRENDS_STATUSES_FETCH_FAIL: + case TRENDS_STATUSES_EXPAND_FAIL: return state.setIn(['trending', 'isLoading'], false); case TRENDS_STATUSES_FETCH_SUCCESS: return normalizeList(state, 'trending', action.statuses, action.next); + case TRENDS_STATUSES_EXPAND_SUCCESS: + return appendToList(state, 'trending', action.statuses, action.next); case FAVOURITE_SUCCESS: return prependOneToList(state, 'favourites', action.status); case UNFAVOURITE_SUCCESS: -- cgit From 1b91359a4508b3068207ef4fd798a56549575591 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 7 Apr 2022 11:27:35 +0200 Subject: Fix older items possibly disappearing on timeline updates (#17980) In some rare cases, when receiving statuses out of order from the streaming API then polling from the REST API, it was possible for the `expandNormalizedTimeline` function to remove older items from the timeline. This commit ensures that any item from the replaced slice that is older than the oldest item retrieved from the API gets added back to the replaced slice. --- app/javascript/mastodon/reducers/timelines.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index 301997567..53a644e47 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -66,13 +66,22 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is // Then, try to find the furthest (if properly sorted, oldest) item in the timeline that // is newer than the most recent fetched one, as it delimits a section comprised of only - // items present in `newIds` (or that were deleted from the server, so should be removed + // items older or within `newIds` (or that were deleted from the server, so should be removed // anyway). // Stop the gap *after* that item. const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0) + 1; - // Make sure we aren't inserting duplicates - let insertedIds = ImmutableOrderedSet(newIds).subtract(oldIds.take(firstIndex), oldIds.skip(lastIndex)).toList(); + let insertedIds = ImmutableOrderedSet(newIds).withMutations(insertedIds => { + // It is possible, though unlikely, that the slice we are replacing contains items older + // than the elements we got from the API. Get them and add them back at the back of the + // slice. + const olderIds = oldIds.slice(firstIndex, lastIndex).filter(id => id !== null && compareId(id, newIds.last()) < 0); + insertedIds.union(olderIds); + + // Make sure we aren't inserting duplicates + insertedIds.subtract(oldIds.take(firstIndex), oldIds.skip(lastIndex)); + }).toList(); + // Finally, insert a gap marker if the data is marked as partial by the server if (isPartial && (firstIndex === 0 || oldIds.get(firstIndex - 1) !== null)) { insertedIds = insertedIds.unshift(null); -- cgit From 8c03b45fffc54a66d1c9c3eec75a7c6f741d0947 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 7 Apr 2022 13:32:12 +0200 Subject: Fix unset `SMTP_RETURN_PATH` environment variable causing e-mail not to send (#17982) --- config/environments/production.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index b003cce9e..95f8a6f32 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -95,11 +95,12 @@ Rails.application.configure do config.action_mailer.default_options = { from: outgoing_email_address, - reply_to: ENV['SMTP_REPLY_TO'], - return_path: ENV['SMTP_RETURN_PATH'], message_id: -> { "<#{Mail.random_tag}@#{outgoing_email_domain}>" }, } + config.action_mailer.default_options[:reply_to] = ENV['SMTP_REPLY_TO'] if ENV['SMTP_REPLY_TO'].present? + config.action_mailer.default_options[:return_path] = ENV['SMTP_RETURN_PATH'] if ENV['SMTP_RETURN_PATH'].present? + config.action_mailer.smtp_settings = { :port => ENV['SMTP_PORT'], :address => ENV['SMTP_SERVER'], -- cgit From ce9dcbea32e7212dc19d83bbb7a21afe8756ced0 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 7 Apr 2022 14:47:30 +0200 Subject: Fix failure when sending warning emails with custom text (#17983) * Add tests * Fix failure when sending warning emails with custom text --- app/mailers/user_mailer.rb | 1 + spec/mailers/user_mailer_spec.rb | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index ce36dd6f5..e47bedec6 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -7,6 +7,7 @@ class UserMailer < Devise::Mailer helper :application helper :instance helper :statuses + helper :formatting helper RoutingHelper diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 9c866788f..2ed33c1e4 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -83,4 +83,15 @@ describe UserMailer, type: :mailer do include_examples 'localized subject', 'devise.mailer.email_changed.subject' end + + describe 'warning' do + let(:strike) { Fabricate(:account_warning, target_account: receiver.account, text: 'dont worry its just the testsuite', action: 'suspend') } + let(:mail) { UserMailer.warning(receiver, strike) } + + it 'renders warning notification' do + receiver.update!(locale: nil) + expect(mail.body.encoded).to include I18n.t("user_mailer.warning.title.suspend", acct: receiver.account.acct) + expect(mail.body.encoded).to include strike.text + end + end end -- cgit From ed8a0bfbb80a759dc6944f599b89e52acb1e83b0 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 7 Apr 2022 11:27:35 +0200 Subject: [Glitch] Fix older items possibly disappearing on timeline updates Port 1b91359a4508b3068207ef4fd798a56549575591 to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/reducers/timelines.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/javascript/flavours/glitch/reducers/timelines.js b/app/javascript/flavours/glitch/reducers/timelines.js index b40e74c5b..29e02a864 100644 --- a/app/javascript/flavours/glitch/reducers/timelines.js +++ b/app/javascript/flavours/glitch/reducers/timelines.js @@ -66,13 +66,22 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is // Then, try to find the furthest (if properly sorted, oldest) item in the timeline that // is newer than the most recent fetched one, as it delimits a section comprised of only - // items present in `newIds` (or that were deleted from the server, so should be removed + // items older or within `newIds` (or that were deleted from the server, so should be removed // anyway). // Stop the gap *after* that item. const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0) + 1; - // Make sure we aren't inserting duplicates - let insertedIds = ImmutableOrderedSet(newIds).subtract(oldIds.take(firstIndex), oldIds.skip(lastIndex)).toList(); + let insertedIds = ImmutableOrderedSet(newIds).withMutations(insertedIds => { + // It is possible, though unlikely, that the slice we are replacing contains items older + // than the elements we got from the API. Get them and add them back at the back of the + // slice. + const olderIds = oldIds.slice(firstIndex, lastIndex).filter(id => id !== null && compareId(id, newIds.last()) < 0); + insertedIds.union(olderIds); + + // Make sure we aren't inserting duplicates + insertedIds.subtract(oldIds.take(firstIndex), oldIds.skip(lastIndex)); + }).toList(); + // Finally, insert a gap marker if the data is marked as partial by the server if (isPartial && (firstIndex === 0 || oldIds.get(firstIndex - 1) !== null)) { insertedIds = insertedIds.unshift(null); -- cgit