From b19ae521b7d28a76e8e1d8da8157e051e9d8de6c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 24 Jan 2024 11:49:19 +0100 Subject: [PATCH 01/45] Add confirmation when redirecting logged-out requests to permalink (#27792) Co-authored-by: Claire --- .../concerns/web_app_controller_concern.rb | 15 ++++- .../redirect/accounts_controller.rb | 10 +++ app/controllers/redirect/base_controller.rb | 24 +++++++ .../redirect/statuses_controller.rb | 10 +++ .../styles/mastodon/containers.scss | 56 +++++++++++++++++ app/lib/permalink_redirector.rb | 63 +++++++++++-------- app/views/redirects/show.html.haml | 8 +++ config/locales/en.yml | 3 + config/routes.rb | 5 ++ 9 files changed, 165 insertions(+), 29 deletions(-) create mode 100644 app/controllers/redirect/accounts_controller.rb create mode 100644 app/controllers/redirect/base_controller.rb create mode 100644 app/controllers/redirect/statuses_controller.rb create mode 100644 app/views/redirects/show.html.haml diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb index 5687d6e5b60ade..b8c909877b651b 100644 --- a/app/controllers/concerns/web_app_controller_concern.rb +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -21,10 +21,19 @@ def set_app_body_class def redirect_unauthenticated_to_permalinks! return if user_signed_in? && current_account.moved_to_account_id.nil? - redirect_path = PermalinkRedirector.new(request.path).redirect_path - return if redirect_path.blank? + permalink_redirector = PermalinkRedirector.new(request.path) + return if permalink_redirector.redirect_path.blank? expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? - redirect_to(redirect_path) + + respond_to do |format| + format.html do + redirect_to(permalink_redirector.redirect_confirmation_path, allow_other_host: false) + end + + format.json do + redirect_to(permalink_redirector.redirect_uri, allow_other_host: true) + end + end end end diff --git a/app/controllers/redirect/accounts_controller.rb b/app/controllers/redirect/accounts_controller.rb new file mode 100644 index 00000000000000..98d2cc2b1f916d --- /dev/null +++ b/app/controllers/redirect/accounts_controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class Redirect::AccountsController < ApplicationController + private + + def set_resource + @resource = Account.find(params[:id]) + not_found if @resource.local? + end +end diff --git a/app/controllers/redirect/base_controller.rb b/app/controllers/redirect/base_controller.rb new file mode 100644 index 00000000000000..90894ec1ed832c --- /dev/null +++ b/app/controllers/redirect/base_controller.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class Redirect::BaseController < ApplicationController + vary_by 'Accept-Language' + + before_action :set_resource + before_action :set_app_body_class + + def show + @redirect_path = ActivityPub::TagManager.instance.url_for(@resource) + + render 'redirects/show', layout: 'application' + end + + private + + def set_app_body_class + @body_classes = 'app-body' + end + + def set_resource + raise NotImplementedError + end +end diff --git a/app/controllers/redirect/statuses_controller.rb b/app/controllers/redirect/statuses_controller.rb new file mode 100644 index 00000000000000..37a938c651a70a --- /dev/null +++ b/app/controllers/redirect/statuses_controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class Redirect::StatusesController < Redirect::BaseController + private + + def set_resource + @resource = Status.find(params[:id]) + not_found if @resource.local? || !@resource.distributable? + end +end diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index 3d646da2391364..b6e995787d2d48 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -104,3 +104,59 @@ margin-inline-start: 10px; } } + +.redirect { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + font-size: 14px; + line-height: 18px; + + &__logo { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 30px; + + img { + height: 48px; + } + } + + &__message { + text-align: center; + + h1 { + font-size: 17px; + line-height: 22px; + font-weight: 700; + margin-bottom: 30px; + } + + p { + margin-bottom: 30px; + + &:last-child { + margin-bottom: 0; + } + } + + a { + color: $highlight-text-color; + font-weight: 500; + text-decoration: none; + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } + } + + &__link { + margin-top: 15px; + } +} diff --git a/app/lib/permalink_redirector.rb b/app/lib/permalink_redirector.rb index 0dd37483e2341c..f551f69db852f9 100644 --- a/app/lib/permalink_redirector.rb +++ b/app/lib/permalink_redirector.rb @@ -5,17 +5,46 @@ class PermalinkRedirector def initialize(path) @path = path + @object = nil + end + + def object + @object ||= begin + if at_username_status_request? || statuses_status_request? + status = Status.find_by(id: second_segment) + status if status&.distributable? && !status&.local? + elsif at_username_request? + username, domain = first_segment.delete_prefix('@').split('@') + domain = nil if TagManager.instance.local_domain?(domain) + account = Account.find_remote(username, domain) + account unless account&.local? + elsif accounts_request? && record_integer_id_request? + account = Account.find_by(id: second_segment) + account unless account&.local? + end + end end def redirect_path - if at_username_status_request? || statuses_status_request? - find_status_url_by_id(second_segment) - elsif at_username_request? - find_account_url_by_name(first_segment) - elsif accounts_request? && record_integer_id_request? - find_account_url_by_id(second_segment) - elsif @path.start_with?('/deck') - @path.delete_prefix('/deck') + return ActivityPub::TagManager.instance.url_for(object) if object.present? + + @path.delete_prefix('/deck') if @path.start_with?('/deck') + end + + def redirect_uri + return ActivityPub::TagManager.instance.uri_for(object) if object.present? + + @path.delete_prefix('/deck') if @path.start_with?('/deck') + end + + def redirect_confirmation_path + case object.class.name + when 'Account' + redirect_account_path(object.id) + when 'Status' + redirect_status_path(object.id) + else + @path.delete_prefix('/deck') if @path.start_with?('/deck') end end @@ -56,22 +85,4 @@ def second_segment def path_segments @path_segments ||= @path.delete_prefix('/deck').delete_prefix('/').split('/') end - - def find_status_url_by_id(id) - status = Status.find_by(id: id) - ActivityPub::TagManager.instance.url_for(status) if status&.distributable? && !status.account.local? - end - - def find_account_url_by_id(id) - account = Account.find_by(id: id) - ActivityPub::TagManager.instance.url_for(account) if account.present? && !account.local? - end - - def find_account_url_by_name(name) - username, domain = name.gsub(/\A@/, '').split('@') - domain = nil if TagManager.instance.local_domain?(domain) - account = Account.find_remote(username, domain) - - ActivityPub::TagManager.instance.url_for(account) if account.present? && !account.local? - end end diff --git a/app/views/redirects/show.html.haml b/app/views/redirects/show.html.haml new file mode 100644 index 00000000000000..0d09387a9c7fef --- /dev/null +++ b/app/views/redirects/show.html.haml @@ -0,0 +1,8 @@ +.redirect + .redirect__logo + = link_to render_logo, root_path + + .redirect__message + %h1= t('redirects.title', instance: site_hostname) + %p= t('redirects.prompt') + %p= link_to @redirect_path, @redirect_path, rel: 'noreferrer noopener' diff --git a/config/locales/en.yml b/config/locales/en.yml index 83eaaa4552c4fc..9d739be07f8a4e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1547,6 +1547,9 @@ en: errors: limit_reached: Limit of different reactions reached unrecognized_emoji: is not a recognized emoji + redirects: + prompt: If you trust this link, click it to continue. + title: You are leaving %{instance}. relationships: activity: Account activity confirm_follow_selected_followers: Are you sure you want to follow selected followers? diff --git a/config/routes.rb b/config/routes.rb index 85c3b18556ed69..c4f862acafa51f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -162,6 +162,11 @@ def redirect_with_vary(path) end end + namespace :redirect do + resources :accounts, only: :show + resources :statuses, only: :show + end + resources :media, only: [:show] do get :player end From 41c2af22705d9a8b265f77dae5da960d2eef2150 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 12:50:41 +0100 Subject: [PATCH 02/45] chore(deps): update dependency rubocop to v1.60.1 (#28731) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 93931d87245f19..54955000b1be5c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -504,7 +504,7 @@ GEM orm_adapter (0.5.0) ox (2.14.17) parallel (1.24.0) - parser (3.2.2.4) + parser (3.3.0.5) ast (~> 2.4.1) racc parslet (2.0.0) @@ -610,7 +610,7 @@ GEM redis (>= 4) redlock (1.3.2) redis (>= 3.0.0, < 6.0) - regexp_parser (2.8.3) + regexp_parser (2.9.0) reline (0.4.2) io-console (~> 0.5) request_store (1.5.1) @@ -650,11 +650,11 @@ GEM rspec-mocks (~> 3.0) sidekiq (>= 5, < 8) rspec-support (3.12.1) - rubocop (1.59.0) + rubocop (1.60.2) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.2.4) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) From 1290fede651b47585de7dbfe4a9b118dc9d59856 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 24 Jan 2024 06:51:09 -0500 Subject: [PATCH 03/45] Fix `Rails/WhereExists` cop in app/lib (#28862) --- .rubocop_todo.yml | 4 ---- app/lib/activitypub/activity/create.rb | 4 ++-- app/lib/delivery_failure_tracker.rb | 2 +- app/lib/feed_manager.rb | 6 +++--- app/lib/suspicious_sign_in_detector.rb | 2 +- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b62dfa72a84790..302c66a16a04a4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -77,10 +77,6 @@ Rails/WhereExists: Exclude: - 'app/controllers/activitypub/inboxes_controller.rb' - 'app/controllers/admin/email_domain_blocks_controller.rb' - - 'app/lib/activitypub/activity/create.rb' - - 'app/lib/delivery_failure_tracker.rb' - - 'app/lib/feed_manager.rb' - - 'app/lib/suspicious_sign_in_detector.rb' - 'app/policies/status_policy.rb' - 'app/serializers/rest/announcement_serializer.rb' - 'app/workers/move_worker.rb' diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 5a2d33c1fa76f6..62c35d4dd34dc2 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -320,7 +320,7 @@ def poll_vote! already_voted = true with_redis_lock("vote:#{replied_to_status.poll_id}:#{@account.id}") do - already_voted = poll.votes.where(account: @account).exists? + already_voted = poll.votes.exists?(account: @account) poll.votes.create!(account: @account, choice: poll.options.index(@object['name']), uri: object_uri) end @@ -406,7 +406,7 @@ def addresses_local_accounts? return false if local_usernames.empty? - Account.local.where(username: local_usernames).exists? + Account.local.exists?(username: local_usernames) end def tombstone_exists? diff --git a/app/lib/delivery_failure_tracker.rb b/app/lib/delivery_failure_tracker.rb index d9382698292d25..e17b45d667a354 100644 --- a/app/lib/delivery_failure_tracker.rb +++ b/app/lib/delivery_failure_tracker.rb @@ -28,7 +28,7 @@ def days end def available? - !UnavailableDomain.where(domain: @host).exists? + !UnavailableDomain.exists?(domain: @host) end def exhausted_deliveries_days diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 53767486ff80aa..38a177e6453ef0 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -420,8 +420,8 @@ def filter_from_mentions?(status, receiver_id) check_for_blocks = status.active_mentions.pluck(:account_id) check_for_blocks.push(status.in_reply_to_account) if status.reply? && !status.in_reply_to_account_id.nil? - should_filter = blocks_or_mutes?(receiver_id, check_for_blocks, :mentions) # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked (or muted) - should_filter ||= status.account.silenced? && !Follow.where(account_id: receiver_id, target_account_id: status.account_id).exists? # of if the account is silenced and I'm not following them + should_filter = blocks_or_mutes?(receiver_id, check_for_blocks, :mentions) # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked (or muted) + should_filter ||= status.account.silenced? && !Follow.exists?(account_id: receiver_id, target_account_id: status.account_id) # Filter if the account is silenced and I'm not following them should_filter end @@ -434,7 +434,7 @@ def filter_from_list?(status, list) if status.reply? && status.in_reply_to_account_id != status.account_id should_filter = status.in_reply_to_account_id != list.account_id should_filter &&= !list.show_followed? - should_filter &&= !(list.show_list? && ListAccount.where(list_id: list.id, account_id: status.in_reply_to_account_id).exists?) + should_filter &&= !(list.show_list? && ListAccount.exists?(list_id: list.id, account_id: status.in_reply_to_account_id)) return !!should_filter end diff --git a/app/lib/suspicious_sign_in_detector.rb b/app/lib/suspicious_sign_in_detector.rb index 1af5188c658980..74f49aa5587e7d 100644 --- a/app/lib/suspicious_sign_in_detector.rb +++ b/app/lib/suspicious_sign_in_detector.rb @@ -19,7 +19,7 @@ def sufficient_security_measures? end def previously_seen_ip?(request) - @user.ips.where('ip <<= ?', masked_ip(request)).exists? + @user.ips.exists?(['ip <<= ?', masked_ip(request)]) end def freshly_signed_up? From 5a838ceaa9a003bc2e2fdee727d4aa87cd53de4f Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 24 Jan 2024 13:37:43 +0100 Subject: [PATCH 04/45] Use active variants for boost icons and increase icon size (#27924) --- app/javascript/mastodon/components/status_action_bar.jsx | 4 +++- .../mastodon/features/status/components/action_bar.jsx | 4 +++- app/javascript/svg-icons/repeat_active.svg | 4 ++++ app/javascript/svg-icons/repeat_disabled.svg | 0 app/javascript/svg-icons/repeat_private.svg | 0 app/javascript/svg-icons/repeat_private_active.svg | 6 ++++++ 6 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 app/javascript/svg-icons/repeat_active.svg mode change 100755 => 100644 app/javascript/svg-icons/repeat_disabled.svg mode change 100755 => 100644 app/javascript/svg-icons/repeat_private.svg create mode 100644 app/javascript/svg-icons/repeat_private_active.svg diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx index bfe77a4900d417..b111a653859358 100644 --- a/app/javascript/mastodon/components/status_action_bar.jsx +++ b/app/javascript/mastodon/components/status_action_bar.jsx @@ -18,8 +18,10 @@ import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react'; import StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; import StarBorderIcon from '@/material-icons/400-24px/star.svg?react'; import VisibilityIcon from '@/material-icons/400-24px/visibility.svg?react'; +import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react'; import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react'; import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react'; +import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; @@ -366,7 +368,7 @@ class StatusActionBar extends ImmutablePureComponent { if (status.get('reblogged')) { reblogTitle = intl.formatMessage(messages.cancel_reblog_private); - reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon; + reblogIconComponent = publicStatus ? RepeatActiveIcon : RepeatPrivateActiveIcon; } else if (publicStatus) { reblogTitle = intl.formatMessage(messages.reblog); reblogIconComponent = RepeatIcon; diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx index 4cb06aac2cfe7b..c243a49129eb4e 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -17,8 +17,10 @@ import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react'; import StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; import StarBorderIcon from '@/material-icons/400-24px/star.svg?react'; +import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react'; import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react'; import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react'; +import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; @@ -296,7 +298,7 @@ class ActionBar extends PureComponent { if (status.get('reblogged')) { reblogTitle = intl.formatMessage(messages.cancel_reblog_private); - reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon; + reblogIconComponent = publicStatus ? RepeatActiveIcon : RepeatPrivateActiveIcon; } else if (publicStatus) { reblogTitle = intl.formatMessage(messages.reblog); reblogIconComponent = RepeatIcon; diff --git a/app/javascript/svg-icons/repeat_active.svg b/app/javascript/svg-icons/repeat_active.svg new file mode 100644 index 00000000000000..a5bbb8fc4f5f7e --- /dev/null +++ b/app/javascript/svg-icons/repeat_active.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/javascript/svg-icons/repeat_disabled.svg b/app/javascript/svg-icons/repeat_disabled.svg old mode 100755 new mode 100644 diff --git a/app/javascript/svg-icons/repeat_private.svg b/app/javascript/svg-icons/repeat_private.svg old mode 100755 new mode 100644 diff --git a/app/javascript/svg-icons/repeat_private_active.svg b/app/javascript/svg-icons/repeat_private_active.svg new file mode 100644 index 00000000000000..cf2a05c84e5edc --- /dev/null +++ b/app/javascript/svg-icons/repeat_private_active.svg @@ -0,0 +1,6 @@ + + + + + + From 64993d3f779a6e01c104e1a2024a87c9785bc79d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 14:37:37 +0100 Subject: [PATCH 05/45] chore(deps): update dependency haml_lint to v0.55.0 (#28856) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 54955000b1be5c..97d2b5f7a0deff 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -319,7 +319,7 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) - haml_lint (0.53.0) + haml_lint (0.55.0) haml (>= 5.0) parallel (~> 1.10) rainbow From ea5397c3735145ad62374798506f57afa122959d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 15:55:06 +0100 Subject: [PATCH 06/45] chore(deps): update dependency selenium-webdriver to v4.17.0 (#28858) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 97d2b5f7a0deff..77d924f94827cd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -696,7 +696,8 @@ GEM scenic (1.7.0) activerecord (>= 4.0.0) railties (>= 4.0.0) - selenium-webdriver (4.16.0) + selenium-webdriver (4.17.0) + base64 (~> 0.2) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) From 559bbf0aa6ceca1cbf417fcc76ea7ef359e42099 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 17:51:18 +0100 Subject: [PATCH 07/45] chore(deps): update artifact actions (major) to v4 (major) (#28415) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test-ruby.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index ae25648a0be0dd..346703ced4328c 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -52,7 +52,7 @@ jobs: run: | tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs* - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: matrix.mode == 'test' with: path: |- @@ -117,7 +117,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: path: './' name: ${{ github.sha }} @@ -193,7 +193,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: path: './public' name: ${{ github.sha }} @@ -213,14 +213,14 @@ jobs: - run: bundle exec rake spec:system - name: Archive logs - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: e2e-logs-${{ matrix.ruby-version }} path: log/ - name: Archive test screenshots - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: e2e-screenshots @@ -297,7 +297,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: path: './public' name: ${{ github.sha }} @@ -317,14 +317,14 @@ jobs: - run: bin/rspec --tag search - name: Archive logs - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: test-search-logs-${{ matrix.ruby-version }} path: log/ - name: Archive test screenshots - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: test-search-screenshots From 7019af431d2b91a4d31ede9558bb3a17c3875f37 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 17:51:36 +0100 Subject: [PATCH 08/45] fix(deps): update dependency dotenv to v16.4.0 (#28872) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 61f699d19e4d9c..53d2caaed9f92d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6901,9 +6901,9 @@ __metadata: linkType: hard "dotenv@npm:^16.0.3": - version: 16.3.2 - resolution: "dotenv@npm:16.3.2" - checksum: a87d62cef0810b670cb477db1a24a42a093b6b428c9e65c185ce1d6368ad7175234b13547718ba08da18df43faae4f814180cc0366e11be1ded2277abc4dd22e + version: 16.4.0 + resolution: "dotenv@npm:16.4.0" + checksum: 70c3b422cefaffdba300aecd9157668590c3b5e66efb3742b7dec207f85023e5997364f04030fc0393fae52bf3a874979632d289ab4fafc1386ff2c68f2f2e8d languageName: node linkType: hard From 9c5be139806ba323f760ceb24476c1261af2ed41 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 17:51:47 +0100 Subject: [PATCH 09/45] chore(deps): update dependency chewy to v7.5.0 (#28730) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 77d924f94827cd..d573debe4be228 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -180,7 +180,7 @@ GEM activesupport cbor (0.5.9.6) charlock_holmes (0.7.7) - chewy (7.4.0) + chewy (7.5.0) activesupport (>= 5.2) elasticsearch (>= 7.12.0, < 7.14.0) elasticsearch-dsl @@ -445,7 +445,7 @@ GEM mime-types-data (3.2023.1205) mini_mime (1.1.5) mini_portile2 (2.8.5) - minitest (5.20.0) + minitest (5.21.1) msgpack (1.7.2) multi_json (1.15.0) multipart-post (2.3.0) From 38f7f8b9096a3c56c676942730184f01e17bd93d Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 24 Jan 2024 12:30:28 -0500 Subject: [PATCH 10/45] Tidy up association declaration in `Instance` model (#28880) --- app/models/instance.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/instance.rb b/app/models/instance.rb index 8f8d87c62a6a90..2dec75d6feb549 100644 --- a/app/models/instance.rb +++ b/app/models/instance.rb @@ -13,12 +13,12 @@ class Instance < ApplicationRecord attr_accessor :failure_days - has_many :accounts, foreign_key: :domain, primary_key: :domain, inverse_of: false - with_options foreign_key: :domain, primary_key: :domain, inverse_of: false do belongs_to :domain_block belongs_to :domain_allow - belongs_to :unavailable_domain # skipcq: RB-RL1031 + belongs_to :unavailable_domain + + has_many :accounts, dependent: nil end scope :searchable, -> { where.not(domain: DomainBlock.select(:domain)) } From 9a8293f58d0317e3917b855483a5b2958226f2fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KMY=EF=BC=88=E9=9B=AA=E3=81=82=E3=81=99=E3=81=8B=EF=BC=89?= Date: Thu, 25 Jan 2024 19:37:09 +0900 Subject: [PATCH 11/45] Fix process of receiving posts with bearcaps is not working (#26527) --- app/lib/activitypub/activity/create.rb | 2 +- app/lib/activitypub/parser/status_parser.rb | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 62c35d4dd34dc2..85195f4c395e44 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -108,7 +108,7 @@ def find_existing_status end def process_status_params - @status_parser = ActivityPub::Parser::StatusParser.new(@json, followers_collection: @account.followers_url) + @status_parser = ActivityPub::Parser::StatusParser.new(@json, followers_collection: @account.followers_url, object: @object) attachment_ids = process_attachments.take(4).map(&:id) diff --git a/app/lib/activitypub/parser/status_parser.rb b/app/lib/activitypub/parser/status_parser.rb index 45f5fc5bf2d549..cfc2b8788b1319 100644 --- a/app/lib/activitypub/parser/status_parser.rb +++ b/app/lib/activitypub/parser/status_parser.rb @@ -4,12 +4,13 @@ class ActivityPub::Parser::StatusParser include JsonLdHelper # @param [Hash] json - # @param [Hash] magic_values - # @option magic_values [String] :followers_collection - def initialize(json, magic_values = {}) - @json = json - @object = json['object'] || json - @magic_values = magic_values + # @param [Hash] options + # @option options [String] :followers_collection + # @option options [Hash] :object + def initialize(json, **options) + @json = json + @object = options[:object] || json['object'] || json + @options = options end def uri @@ -78,7 +79,7 @@ def visibility :public elsif audience_cc.any? { |cc| ActivityPub::TagManager.instance.public_collection?(cc) } :unlisted - elsif audience_to.include?(@magic_values[:followers_collection]) + elsif audience_to.include?(@options[:followers_collection]) :private else :direct From c50274a0acd3679160984bbf410bff46fd76179c Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 25 Jan 2024 11:44:25 +0100 Subject: [PATCH 12/45] Fix redirect confirmation for accounts (#28902) --- app/controllers/redirect/accounts_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/redirect/accounts_controller.rb b/app/controllers/redirect/accounts_controller.rb index 98d2cc2b1f916d..713ccf2ca1f410 100644 --- a/app/controllers/redirect/accounts_controller.rb +++ b/app/controllers/redirect/accounts_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Redirect::AccountsController < ApplicationController +class Redirect::AccountsController < Redirect::BaseController private def set_resource From 0471a780556720338fef29266f2fc2578ab2010c Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 25 Jan 2024 12:13:33 +0100 Subject: [PATCH 13/45] Add tests for redirect confirmations (#28903) --- spec/features/redirections_spec.rb | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 spec/features/redirections_spec.rb diff --git a/spec/features/redirections_spec.rb b/spec/features/redirections_spec.rb new file mode 100644 index 00000000000000..f73ab58470194c --- /dev/null +++ b/spec/features/redirections_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'redirection confirmations' do + let(:account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/foo', url: 'https://example.com/@foo') } + let(:status) { Fabricate(:status, account: account, uri: 'https://example.com/users/foo/statuses/1', url: 'https://example.com/@foo/1') } + + context 'when a logged out user visits a local page for a remote account' do + it 'shows a confirmation page' do + visit "/@#{account.pretty_acct}" + + # It explains about the redirect + expect(page).to have_content(I18n.t('redirects.title', instance: 'cb6e6126.ngrok.io')) + + # It features an appropriate link + expect(page).to have_link(account.url, href: account.url) + end + end + + context 'when a logged out user visits a local page for a remote status' do + it 'shows a confirmation page' do + visit "/@#{account.pretty_acct}/#{status.id}" + + # It explains about the redirect + expect(page).to have_content(I18n.t('redirects.title', instance: 'cb6e6126.ngrok.io')) + + # It features an appropriate link + expect(page).to have_link(status.url, href: status.url) + end + end +end From 087415d0fe8d9a719eb2df7e915b08f4d4d0360a Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 25 Jan 2024 12:13:36 +0100 Subject: [PATCH 14/45] Add tests for processing statuses using bearcap URIs (#28904) --- spec/lib/activitypub/activity/create_spec.rb | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 3e3a4978c898f1..e4966cffa33f1f 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -893,6 +893,49 @@ def activity_for_object(json) end end + context 'when object URI uses bearcaps' do + subject { described_class.new(json, sender) } + + let(:token) { 'foo' } + + let(:json) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: [ActivityPub::TagManager.instance.uri_for(sender), '#foo'].join, + type: 'Create', + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: Addressable::URI.new(scheme: 'bear', query_values: { t: token, u: object_json[:id] }).to_s, + }.with_indifferent_access + end + + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + to: 'https://www.w3.org/ns/activitystreams#Public', + } + end + + before do + stub_request(:get, object_json[:id]) + .with(headers: { Authorization: "Bearer #{token}" }) + .to_return(body: Oj.dump(object_json), headers: { 'Content-Type': 'application/activity+json' }) + + subject.perform + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status).to have_attributes( + visibility: 'public', + text: 'Lorem ipsum' + ) + end + end + context 'with an encrypted message' do subject { described_class.new(json, sender, delivery: true, delivered_to_account_id: recipient.id) } From d158f7e6228e9c3f9d93bff86eccd71bdb542558 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 13:33:56 +0100 Subject: [PATCH 15/45] chore(deps): update dependency rspec-rails to v6.1.1 (#28905) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index d573debe4be228..a31d0a929c0e55 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -360,7 +360,7 @@ GEM rainbow (>= 2.2.2, < 4.0) terminal-table (>= 1.5.1) idn-ruby (0.1.5) - io-console (0.7.1) + io-console (0.7.2) irb (1.11.1) rdoc reline (>= 0.4.2) @@ -445,7 +445,7 @@ GEM mime-types-data (3.2023.1205) mini_mime (1.1.5) mini_portile2 (2.8.5) - minitest (5.21.1) + minitest (5.21.2) msgpack (1.7.2) multi_json (1.15.0) multipart-post (2.3.0) @@ -636,7 +636,7 @@ GEM rspec-mocks (3.12.6) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-rails (6.1.0) + rspec-rails (6.1.1) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) From 1a565e4bea45828bfbe16c2c581ec5bc676c5223 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 12:36:20 +0000 Subject: [PATCH 16/45] fix(deps): update dependency axios to v1.6.6 (#28895) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 53d2caaed9f92d..29bcb3575ba070 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4673,13 +4673,13 @@ __metadata: linkType: hard "axios@npm:^1.4.0": - version: 1.6.5 - resolution: "axios@npm:1.6.5" + version: 1.6.6 + resolution: "axios@npm:1.6.6" dependencies: follow-redirects: "npm:^1.15.4" form-data: "npm:^4.0.0" proxy-from-env: "npm:^1.1.0" - checksum: aeb9acf87590d8aa67946072ced38e01ca71f5dfe043782c0ccea667e5dd5c45830c08afac9be3d7c894f09684b8ab2a458f497d197b73621233bcf202d9d468 + checksum: 974f54cfade94fd4c0191309122a112c8d233089cecb0070cd8e0904e9bd9c364ac3a6fd0f981c978508077249788950427c565f54b7b2110e5c3426006ff343 languageName: node linkType: hard From 7c9c6c7f80d57ea0fd504b59debe6439d28cb1b5 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 25 Jan 2024 07:37:07 -0500 Subject: [PATCH 17/45] Fix remaining `Rails/WhereExists` cop violations, regenerate todo (#28892) --- .rubocop_todo.yml | 17 +---------------- .../activitypub/inboxes_controller.rb | 2 +- .../admin/email_domain_blocks_controller.rb | 2 +- app/policies/status_policy.rb | 2 +- app/serializers/rest/announcement_serializer.rb | 2 +- app/workers/move_worker.rb | 2 +- .../process_collection_service_spec.rb | 2 +- 7 files changed, 7 insertions(+), 22 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 302c66a16a04a4..77f7e70734099a 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.59.0. +# using RuboCop version 1.60.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -70,21 +70,6 @@ Rails/UniqueValidationWithoutIndex: - 'app/models/identity.rb' - 'app/models/webauthn_credential.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: exists, where -Rails/WhereExists: - Exclude: - - 'app/controllers/activitypub/inboxes_controller.rb' - - 'app/controllers/admin/email_domain_blocks_controller.rb' - - 'app/policies/status_policy.rb' - - 'app/serializers/rest/announcement_serializer.rb' - - 'app/workers/move_worker.rb' - - 'spec/models/account_spec.rb' - - 'spec/services/activitypub/process_collection_service_spec.rb' - - 'spec/services/purge_domain_service_spec.rb' - - 'spec/services/unallow_domain_service_spec.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowedMethods, AllowedPatterns. # AllowedMethods: ==, equal?, eql? diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index 5ee85474e7efc1..ba85e0a7229a94 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -24,7 +24,7 @@ def skip_unknown_actor_activity def unknown_affected_account? json = Oj.load(body, mode: :strict) - json.is_a?(Hash) && %w(Delete Update).include?(json['type']) && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists? + json.is_a?(Hash) && %w(Delete Update).include?(json['type']) && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.exists?(uri: json['actor']) rescue Oj::ParseError false end diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb index ff754bc0b47033..faa0a061a6ddd1 100644 --- a/app/controllers/admin/email_domain_blocks_controller.rb +++ b/app/controllers/admin/email_domain_blocks_controller.rb @@ -38,7 +38,7 @@ def create log_action :create, @email_domain_block (@email_domain_block.other_domains || []).uniq.each do |domain| - next if EmailDomainBlock.where(domain: domain).exists? + next if EmailDomainBlock.exists?(domain: domain) other_email_domain_block = EmailDomainBlock.create!(domain: domain, allow_with_approval: @email_domain_block.allow_with_approval, parent: @email_domain_block) log_action :create, other_email_domain_block diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb index 322d3aec5cdff7..540e266427f787 100644 --- a/app/policies/status_policy.rb +++ b/app/policies/status_policy.rb @@ -57,7 +57,7 @@ def mention_exists? if record.mentions.loaded? record.mentions.any? { |mention| mention.account_id == current_account.id } else - record.mentions.where(account: current_account).exists? + record.mentions.exists?(account: current_account) end end diff --git a/app/serializers/rest/announcement_serializer.rb b/app/serializers/rest/announcement_serializer.rb index 23b2fa514b92b0..8cee27127241e9 100644 --- a/app/serializers/rest/announcement_serializer.rb +++ b/app/serializers/rest/announcement_serializer.rb @@ -23,7 +23,7 @@ def id end def read - object.announcement_mutes.where(account: current_user.account).exists? + object.announcement_mutes.exists?(account: current_user.account) end def content diff --git a/app/workers/move_worker.rb b/app/workers/move_worker.rb index 73ae268beea3b9..a18f38556bbc5c 100644 --- a/app/workers/move_worker.rb +++ b/app/workers/move_worker.rb @@ -123,7 +123,7 @@ def carry_mutes_over! end def add_account_note_if_needed!(account, id) - unless AccountNote.where(account: account, target_account: @target_account).exists? + unless AccountNote.exists?(account: account, target_account: @target_account) text = I18n.with_locale(account.user&.locale.presence || I18n.default_locale) do I18n.t(id, acct: @source_account.acct) end diff --git a/spec/services/activitypub/process_collection_service_spec.rb b/spec/services/activitypub/process_collection_service_spec.rb index f4a2b8fec691b0..63502c546e29c6 100644 --- a/spec/services/activitypub/process_collection_service_spec.rb +++ b/spec/services/activitypub/process_collection_service_spec.rb @@ -265,7 +265,7 @@ anything ) - expect(Status.where(uri: 'https://example.com/users/bob/fake-status').exists?).to be false + expect(Status.exists?(uri: 'https://example.com/users/bob/fake-status')).to be false end end end From a69506a434a605fd8ad67f4eb938a328753bbe06 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 13:37:23 +0100 Subject: [PATCH 18/45] fix(deps): update dependency dotenv to v16.4.1 (#28889) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 29bcb3575ba070..78261b3b8427e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6901,9 +6901,9 @@ __metadata: linkType: hard "dotenv@npm:^16.0.3": - version: 16.4.0 - resolution: "dotenv@npm:16.4.0" - checksum: 70c3b422cefaffdba300aecd9157668590c3b5e66efb3742b7dec207f85023e5997364f04030fc0393fae52bf3a874979632d289ab4fafc1386ff2c68f2f2e8d + version: 16.4.1 + resolution: "dotenv@npm:16.4.1" + checksum: ef3d95f48f38146df0881a4b58447ae437d2da3f6d645074b84de4e64ef64ba75fc357c5ed66b3c2b813b5369fdeb6a4777d6ade2d50e54eed6aa06dddc98bc4 languageName: node linkType: hard From 6b6586f5d099a9089c8bf9dc7d7406a86c0a48eb Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 25 Jan 2024 08:00:34 -0500 Subject: [PATCH 19/45] Add `CustomFilterKeyword#to_regex` method (#28893) --- app/models/custom_filter.rb | 11 +------ app/models/custom_filter_keyword.rb | 16 +++++++++++ spec/models/custom_filter_keyword_spec.rb | 35 +++++++++++++++++++++++ 3 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 spec/models/custom_filter_keyword_spec.rb diff --git a/app/models/custom_filter.rb b/app/models/custom_filter.rb index 371267fc2810ba..c8120c239547a0 100644 --- a/app/models/custom_filter.rb +++ b/app/models/custom_filter.rb @@ -68,16 +68,7 @@ def self.cached_filters_for(account_id) scope = CustomFilterKeyword.includes(:custom_filter).where(custom_filter: { account_id: account_id }).where(Arel.sql('expires_at IS NULL OR expires_at > NOW()')) scope.to_a.group_by(&:custom_filter).each do |filter, keywords| - keywords.map! do |keyword| - if keyword.whole_word - sb = /\A[[:word:]]/.match?(keyword.keyword) ? '\b' : '' - eb = /[[:word:]]\z/.match?(keyword.keyword) ? '\b' : '' - - /(?mix:#{sb}#{Regexp.escape(keyword.keyword)}#{eb})/ - else - /#{Regexp.escape(keyword.keyword)}/i - end - end + keywords.map!(&:to_regex) filters_hash[filter.id] = { keywords: Regexp.union(keywords), filter: filter } end.to_h diff --git a/app/models/custom_filter_keyword.rb b/app/models/custom_filter_keyword.rb index 3158b3b79a04f5..979d0b822e6960 100644 --- a/app/models/custom_filter_keyword.rb +++ b/app/models/custom_filter_keyword.rb @@ -23,8 +23,24 @@ class CustomFilterKeyword < ApplicationRecord before_destroy :prepare_cache_invalidation! after_commit :invalidate_cache! + def to_regex + if whole_word? + /(?mix:#{to_regex_sb}#{Regexp.escape(keyword)}#{to_regex_eb})/ + else + /#{Regexp.escape(keyword)}/i + end + end + private + def to_regex_sb + /\A[[:word:]]/.match?(keyword) ? '\b' : '' + end + + def to_regex_eb + /[[:word:]]\z/.match?(keyword) ? '\b' : '' + end + def prepare_cache_invalidation! custom_filter.prepare_cache_invalidation! end diff --git a/spec/models/custom_filter_keyword_spec.rb b/spec/models/custom_filter_keyword_spec.rb new file mode 100644 index 00000000000000..4e3ab060a042d1 --- /dev/null +++ b/spec/models/custom_filter_keyword_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe CustomFilterKeyword do + describe '#to_regex' do + context 'when whole_word is true' do + it 'builds a regex with boundaries and the keyword' do + keyword = described_class.new(whole_word: true, keyword: 'test') + + expect(keyword.to_regex).to eq(/(?mix:\b#{Regexp.escape(keyword.keyword)}\b)/) + end + + it 'builds a regex with starting boundary and the keyword when end with non-word' do + keyword = described_class.new(whole_word: true, keyword: 'test#') + + expect(keyword.to_regex).to eq(/(?mix:\btest\#)/) + end + + it 'builds a regex with end boundary and the keyword when start with non-word' do + keyword = described_class.new(whole_word: true, keyword: '#test') + + expect(keyword.to_regex).to eq(/(?mix:\#test\b)/) + end + end + + context 'when whole_word is false' do + it 'builds a regex with the keyword' do + keyword = described_class.new(whole_word: false, keyword: 'test') + + expect(keyword.to_regex).to eq(/test/i) + end + end + end +end From 59d2ea0d82493fdd948c91c6c715ea461b60619b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 14:00:44 +0100 Subject: [PATCH 20/45] New Crowdin Translations (automated) (#28899) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/af.json | 1 + app/javascript/mastodon/locales/ca.json | 2 +- app/javascript/mastodon/locales/oc.json | 13 ++++++++ app/javascript/mastodon/locales/zh-TW.json | 12 +++---- config/locales/br.yml | 3 ++ config/locales/ca.yml | 3 ++ config/locales/da.yml | 3 ++ config/locales/de.yml | 3 ++ config/locales/devise.fr-CA.yml | 4 +++ config/locales/devise.fr.yml | 4 +++ config/locales/devise.sv.yml | 1 + config/locales/devise.zh-TW.yml | 14 ++++---- config/locales/es-AR.yml | 3 ++ config/locales/es-MX.yml | 3 ++ config/locales/es.yml | 3 ++ config/locales/eu.yml | 3 ++ config/locales/fi.yml | 7 ++-- config/locales/fo.yml | 3 ++ config/locales/fr-CA.yml | 10 ++++++ config/locales/fr.yml | 10 ++++++ config/locales/gl.yml | 3 ++ config/locales/he.yml | 5 ++- config/locales/hu.yml | 2 ++ config/locales/is.yml | 3 ++ config/locales/it.yml | 3 ++ config/locales/ja.yml | 6 ++++ config/locales/ko.yml | 7 ++++ config/locales/lad.yml | 3 ++ config/locales/lt.yml | 3 ++ config/locales/nl.yml | 6 +++- config/locales/nn.yml | 3 ++ config/locales/no.yml | 3 ++ config/locales/pl.yml | 3 ++ config/locales/pt-BR.yml | 3 ++ config/locales/pt-PT.yml | 3 ++ config/locales/ru.yml | 3 ++ config/locales/sk.yml | 3 ++ config/locales/sq.yml | 3 ++ config/locales/sr-Latn.yml | 3 ++ config/locales/sr.yml | 3 ++ config/locales/sv.yml | 3 ++ config/locales/tr.yml | 3 ++ config/locales/uk.yml | 3 ++ config/locales/vi.yml | 3 ++ config/locales/zh-CN.yml | 3 ++ config/locales/zh-HK.yml | 3 ++ config/locales/zh-TW.yml | 39 ++++++++++++---------- 47 files changed, 197 insertions(+), 36 deletions(-) diff --git a/app/javascript/mastodon/locales/af.json b/app/javascript/mastodon/locales/af.json index 6c37cdf5ca4622..d1873d6dce1e5d 100644 --- a/app/javascript/mastodon/locales/af.json +++ b/app/javascript/mastodon/locales/af.json @@ -3,6 +3,7 @@ "about.contact": "Kontak:", "about.disclaimer": "Mastodon is gratis oopbronsagteware en ’n handelsmerk van Mastodon gGmbH.", "about.domain_blocks.no_reason_available": "Rede nie beskikbaar nie", + "about.domain_blocks.preamble": "Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.", "about.domain_blocks.silenced.title": "Beperk", "about.domain_blocks.suspended.title": "Opgeskort", "about.not_available": "Hierdie inligting is nie op hierdie bediener beskikbaar gestel nie.", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index 7d1049a30f4fc5..c763a32ba846dc 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -521,7 +521,7 @@ "poll.total_people": "{count, plural, one {# persona} other {# persones}}", "poll.total_votes": "{count, plural, one {# vot} other {# vots}}", "poll.vote": "Vota", - "poll.voted": "Vas votar per aquesta resposta", + "poll.voted": "Vau votar aquesta resposta", "poll.votes": "{votes, plural, one {# vot} other {# vots}}", "poll_button.add_poll": "Afegeix una enquesta", "poll_button.remove_poll": "Elimina l'enquesta", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index 833bfe6acef261..1ecfbcaf065173 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -18,6 +18,7 @@ "account.blocked": "Blocat", "account.browse_more_on_origin_server": "Navigar sul perfil original", "account.cancel_follow_request": "Retirar la demanda d’abonament", + "account.copy": "Copiar lo ligam del perfil", "account.direct": "Mencionar @{name} en privat", "account.disable_notifications": "Quitar de m’avisar quand @{name} publica quicòm", "account.domain_blocked": "Domeni amagat", @@ -28,6 +29,7 @@ "account.featured_tags.last_status_never": "Cap de publicacion", "account.featured_tags.title": "Etiquetas en avant de {name}", "account.follow": "Sègre", + "account.follow_back": "Sègre en retorn", "account.followers": "Seguidors", "account.followers.empty": "Degun sèc pas aqueste utilizaire pel moment.", "account.followers_counter": "{count, plural, one {{counter} Seguidor} other {{counter} Seguidors}}", @@ -48,6 +50,7 @@ "account.mute_notifications_short": "Amudir las notificacions", "account.mute_short": "Amudir", "account.muted": "Mes en silenci", + "account.mutual": "Mutual", "account.no_bio": "Cap de descripcion pas fornida.", "account.open_original_page": "Dobrir la pagina d’origina", "account.posts": "Tuts", @@ -172,6 +175,7 @@ "conversation.mark_as_read": "Marcar coma legida", "conversation.open": "Veire la conversacion", "conversation.with": "Amb {names}", + "copy_icon_button.copied": "Copiat al quichapapièr", "copypaste.copied": "Copiat", "copypaste.copy_to_clipboard": "Copiar al quichapapièr", "directory.federated": "Del fediverse conegut", @@ -294,6 +298,8 @@ "keyboard_shortcuts.direct": "to open direct messages column", "keyboard_shortcuts.down": "far davalar dins la lista", "keyboard_shortcuts.enter": "dobrir los estatuts", + "keyboard_shortcuts.favourite": "Marcar coma favorit", + "keyboard_shortcuts.favourites": "Dobrir la lista dels favorits", "keyboard_shortcuts.federated": "dobrir lo flux public global", "keyboard_shortcuts.heading": "Acorchis clavièr", "keyboard_shortcuts.home": "dobrir lo flux public local", @@ -339,6 +345,7 @@ "lists.search": "Cercar demest lo mond que seguètz", "lists.subheading": "Vòstras listas", "load_pending": "{count, plural, one {# nòu element} other {# nòu elements}}", + "loading_indicator.label": "Cargament…", "media_gallery.toggle_visible": "Modificar la visibilitat", "mute_modal.duration": "Durada", "mute_modal.hide_notifications": "Rescondre las notificacions d’aquesta persona ?", @@ -371,6 +378,7 @@ "not_signed_in_indicator.not_signed_in": "Devètz vos connectar per accedir a aquesta ressorsa.", "notification.admin.report": "{name} senhalèt {target}", "notification.admin.sign_up": "{name} se marquèt", + "notification.favourite": "{name} a mes vòstre estatut en favorit", "notification.follow": "{name} vos sèc", "notification.follow_request": "{name} a demandat a vos sègre", "notification.mention": "{name} vos a mencionat", @@ -423,6 +431,8 @@ "onboarding.compose.template": "Adiu #Mastodon !", "onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!", "onboarding.follows.title": "Popular on Mastodon", + "onboarding.profile.display_name": "Nom d’afichatge", + "onboarding.profile.note": "Biografia", "onboarding.share.title": "Partejar vòstre perfil", "onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:", "onboarding.start.skip": "Want to skip right ahead?", @@ -504,6 +514,7 @@ "report_notification.categories.spam": "Messatge indesirable", "report_notification.categories.violation": "Violacion de las règlas", "report_notification.open": "Dobrir lo senhalament", + "search.no_recent_searches": "Cap de recèrcas recentas", "search.placeholder": "Recercar", "search.search_or_paste": "Recercar o picar una URL", "search_popout.language_code": "Còdi ISO de lenga", @@ -536,6 +547,7 @@ "status.copy": "Copiar lo ligam de l’estatut", "status.delete": "Escafar", "status.detailed_status": "Vista detalhada de la convèrsa", + "status.direct": "Mencionar @{name} en privat", "status.direct_indicator": "Mencion privada", "status.edit": "Modificar", "status.edited": "Modificat {date}", @@ -626,6 +638,7 @@ "upload_modal.preview_label": "Apercebut ({ratio})", "upload_progress.label": "Mandadís…", "upload_progress.processing": "Tractament…", + "username.taken": "Aqueste nom d’utilizaire es pres. Ensajatz-ne un autre", "video.close": "Tampar la vidèo", "video.download": "Telecargar lo fichièr", "video.exit_fullscreen": "Sortir plen ecran", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 99839369535ac8..cc8b5831202927 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -48,7 +48,7 @@ "account.locked_info": "此帳號的隱私狀態設定為鎖定。該擁有者會手動審核能跟隨此帳號的人。", "account.media": "媒體", "account.mention": "提及 @{name}", - "account.moved_to": "{name} 現在的新帳號為:", + "account.moved_to": "{name} 目前的新帳號為:", "account.mute": "靜音 @{name}", "account.mute_notifications_short": "靜音推播通知", "account.mute_short": "靜音", @@ -59,7 +59,7 @@ "account.posts": "嘟文", "account.posts_with_replies": "嘟文與回覆", "account.report": "檢舉 @{name}", - "account.requested": "正在等待核准。按一下以取消跟隨請求", + "account.requested": "正在等候審核。按一下以取消跟隨請求", "account.requested_follow": "{name} 要求跟隨您", "account.share": "分享 @{name} 的個人檔案", "account.show_reblogs": "顯示來自 @{name} 的嘟文", @@ -84,7 +84,7 @@ "admin.impact_report.title": "影響總結", "alert.rate_limited.message": "請於 {retry_time, time, medium} 後重試。", "alert.rate_limited.title": "已限速", - "alert.unexpected.message": "發生了非預期的錯誤。", + "alert.unexpected.message": "發生非預期的錯誤。", "alert.unexpected.title": "哎呀!", "announcement.announcement": "公告", "attachments_list.unprocessed": "(未經處理)", @@ -241,7 +241,7 @@ "empty_column.followed_tags": "您還沒有跟隨任何主題標籤。當您跟隨主題標籤時,它們將於此顯示。", "empty_column.hashtag": "這個主題標籤下什麼也沒有。", "empty_column.home": "您的首頁時間軸是空的!跟隨更多人來將它填滿吧!", - "empty_column.list": "這份列表下什麼也沒有。當此列表的成員嘟出了新的嘟文時,它們將顯示於此。", + "empty_column.list": "這份列表下什麼也沒有。當此列表的成員嘟出新的嘟文時,它們將顯示於此。", "empty_column.lists": "您還沒有建立任何列表。當您建立列表時,它將於此顯示。", "empty_column.mutes": "您尚未靜音任何使用者。", "empty_column.notifications": "您還沒有收到任何通知,當您與別人開始互動時,它將於此顯示。", @@ -303,8 +303,8 @@ "hashtag.counter_by_accounts": "{count, plural, one {{counter} 名} other {{counter} 名}}參與者", "hashtag.counter_by_uses": "{count, plural, one {{counter} 則} other {{counter} 則}}嘟文", "hashtag.counter_by_uses_today": "本日有 {count, plural, one {{counter} 則} other {{counter} 則}}嘟文", - "hashtag.follow": "追蹤主題標籤", - "hashtag.unfollow": "取消追蹤主題標籤", + "hashtag.follow": "跟隨主題標籤", + "hashtag.unfollow": "取消跟隨主題標籤", "hashtags.and_other": "…及其他 {count, plural, other {# 個}}", "home.actions.go_to_explore": "看看發生什麼新鮮事", "home.actions.go_to_suggestions": "尋找一些人來跟隨", diff --git a/config/locales/br.yml b/config/locales/br.yml index 7af72457d0c338..d20609a8ce7adf 100644 --- a/config/locales/br.yml +++ b/config/locales/br.yml @@ -443,6 +443,9 @@ br: preferences: other: All posting_defaults: Arventennoù embann dre ziouer + redirects: + prompt: M'ho peus fiziañs el liamm-mañ, klikit warnañ evit kenderc'hel. + title: O kuitaat %{instance} emaoc'h. relationships: dormant: O kousket followers: Heulier·ezed·ien diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 38ef976b83ea23..58f6e26374ba9b 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -1546,6 +1546,9 @@ ca: errors: limit_reached: Límit de diferents reaccions assolit unrecognized_emoji: no és un emoji reconegut + redirects: + prompt: Si confieu en aquest enllaç, feu-hi clic per a continuar. + title: Esteu sortint de %{instance}. relationships: activity: Activitat del compte confirm_follow_selected_followers: Segur que vols seguir els seguidors seleccionats? diff --git a/config/locales/da.yml b/config/locales/da.yml index d92d00190573ac..57899d5f71f4be 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -1546,6 +1546,9 @@ da: errors: limit_reached: Grænse for forskellige reaktioner nået unrecognized_emoji: er ikke en genkendt emoji + redirects: + prompt: Er der tillid til dette link, så klik på det for at fortsætte. + title: Nu forlades %{instance}. relationships: activity: Kontoaktivitet confirm_follow_selected_followers: Sikker på, at de valgte følgere skal følges? diff --git a/config/locales/de.yml b/config/locales/de.yml index 9568f698d101b5..b77f4151904498 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1546,6 +1546,9 @@ de: errors: limit_reached: Limit für verschiedene Reaktionen erreicht unrecognized_emoji: ist ein unbekanntes Emoji + redirects: + prompt: Wenn du diesem Link vertraust, dann klicke ihn an, um fortzufahren. + title: Du verlässt %{instance}. relationships: activity: Kontoaktivität confirm_follow_selected_followers: Möchtest du den ausgewählten Followern folgen? diff --git a/config/locales/devise.fr-CA.yml b/config/locales/devise.fr-CA.yml index 34104e0ac56206..7f13f67828b8f4 100644 --- a/config/locales/devise.fr-CA.yml +++ b/config/locales/devise.fr-CA.yml @@ -73,9 +73,13 @@ fr-CA: subject: 'Mastodon: Clé de sécurité supprimée' title: Une de vos clés de sécurité a été supprimée webauthn_disabled: + explanation: L'authentification avec les clés de sécurité a été désactivée pour votre compte. + extra: La connexion est maintenant possible en utilisant uniquement le jeton généré par l'application TOTP associée. subject: 'Mastodon: Authentification avec clés de sécurité désactivée' title: Clés de sécurité désactivées webauthn_enabled: + explanation: L'authentification par clé de sécurité a été activée pour votre compte. + extra: Votre clé de sécurité peut maintenant être utilisée pour vous connecter. subject: 'Mastodon: Authentification de la clé de sécurité activée' title: Clés de sécurité activées omniauth_callbacks: diff --git a/config/locales/devise.fr.yml b/config/locales/devise.fr.yml index 1fc6663bfe1894..8a5b8384e0f249 100644 --- a/config/locales/devise.fr.yml +++ b/config/locales/devise.fr.yml @@ -73,9 +73,13 @@ fr: subject: 'Mastodon: Clé de sécurité supprimée' title: Une de vos clés de sécurité a été supprimée webauthn_disabled: + explanation: L'authentification avec les clés de sécurité a été désactivée pour votre compte. + extra: La connexion est maintenant possible en utilisant uniquement le jeton généré par l'application TOTP associée. subject: 'Mastodon: Authentification avec clés de sécurité désactivée' title: Clés de sécurité désactivées webauthn_enabled: + explanation: L'authentification par clé de sécurité a été activée pour votre compte. + extra: Votre clé de sécurité peut maintenant être utilisée pour vous connecter. subject: 'Mastodon: Authentification de la clé de sécurité activée' title: Clés de sécurité activées omniauth_callbacks: diff --git a/config/locales/devise.sv.yml b/config/locales/devise.sv.yml index b089f214271432..6544f426bd617b 100644 --- a/config/locales/devise.sv.yml +++ b/config/locales/devise.sv.yml @@ -77,6 +77,7 @@ sv: subject: 'Mastodon: Autentisering med säkerhetsnycklar är inaktiverat' title: Säkerhetsnycklar inaktiverade webauthn_enabled: + extra: Din säkerhetsnyckel kan nu användas för inloggning. subject: 'Mastodon: Autentisering med säkerhetsnyckel är aktiverat' title: Säkerhetsnycklar aktiverade omniauth_callbacks: diff --git a/config/locales/devise.zh-TW.yml b/config/locales/devise.zh-TW.yml index 762c8eba84bd15..06438971a794a1 100644 --- a/config/locales/devise.zh-TW.yml +++ b/config/locales/devise.zh-TW.yml @@ -47,14 +47,14 @@ zh-TW: subject: Mastodon:重設密碼指引 title: 重設密碼 two_factor_disabled: - explanation: 現在僅可使用電子郵件地址與密碼登入。 + explanation: 目前僅可使用電子郵件地址與密碼登入。 subject: Mastodon:已停用兩階段驗證 - subtitle: 您帳號的兩步驟驗證已停用。 + subtitle: 您帳號之兩階段驗證已停用。 title: 已停用兩階段驗證 two_factor_enabled: - explanation: 登入時需要配對的 TOTP 應用程式產生的權杖。 + explanation: 登入時需要配對的 TOTP 應用程式產生之 token。 subject: Mastodon:已啟用兩階段驗證 - subtitle: 您的帳號已啟用兩步驟驗證。 + subtitle: 您的帳號之兩階段驗證已啟用。 title: 已啟用兩階段驗證 two_factor_recovery_codes_changed: explanation: 之前的備用驗證碼已經失效,且已產生新的。 @@ -74,12 +74,12 @@ zh-TW: title: 您的一支安全密鑰已經被移除 webauthn_disabled: explanation: 您的帳號已停用安全金鑰身份驗證。 - extra: 現在僅可使用配對的 TOTP 應用程式產生的權杖登入。 + extra: 現在僅可使用配對的 TOTP 應用程式產生之 token 登入。 subject: Mastodon:安全密鑰認證方式已停用 title: 已停用安全密鑰 webauthn_enabled: - explanation: 您的帳號已啟用安全金鑰驗證。 - extra: 您的安全金鑰現在可用於登入。 + explanation: 您的帳號已啟用安全金鑰身分驗證。 + extra: 您的安全金鑰現在已可用於登入。 subject: Mastodon:已啟用安全密鑰認證 title: 已啟用安全密鑰 omniauth_callbacks: diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml index cc55d3d3ffad29..d1dbdbf0b835ad 100644 --- a/config/locales/es-AR.yml +++ b/config/locales/es-AR.yml @@ -1546,6 +1546,9 @@ es-AR: errors: limit_reached: Se alcanzó el límite de reacciones diferentes unrecognized_emoji: no es un emoji conocido + redirects: + prompt: Si confiás en este enlace, dale clic o un toque para continuar. + title: Estás dejando %{instance}. relationships: activity: Actividad de la cuenta confirm_follow_selected_followers: "¿Estás seguro que querés seguir a los seguidores seleccionados?" diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml index b84fb7cf96d40f..4d228e98d477f4 100644 --- a/config/locales/es-MX.yml +++ b/config/locales/es-MX.yml @@ -1546,6 +1546,9 @@ es-MX: errors: limit_reached: Límite de reacciones diferentes alcanzado unrecognized_emoji: no es un emoji conocido + redirects: + prompt: Si confías en este enlace, púlsalo para continuar. + title: Vas a salir de %{instance}. relationships: activity: Actividad de la cuenta confirm_follow_selected_followers: "¿Estás seguro de que quieres seguir a las cuentas seleccionadas?" diff --git a/config/locales/es.yml b/config/locales/es.yml index 95816d6bcb4acf..08fc0988e4eb6f 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1546,6 +1546,9 @@ es: errors: limit_reached: Límite de reacciones diferentes alcanzado unrecognized_emoji: no es un emoji conocido + redirects: + prompt: Si confías en este enlace, púlsalo para continuar. + title: Vas a salir de %{instance}. relationships: activity: Actividad de la cuenta confirm_follow_selected_followers: "¿Estás seguro de que quieres seguir a las cuentas seleccionadas?" diff --git a/config/locales/eu.yml b/config/locales/eu.yml index bd6ea8c8321bba..44688577a9e94a 100644 --- a/config/locales/eu.yml +++ b/config/locales/eu.yml @@ -1550,6 +1550,9 @@ eu: errors: limit_reached: Erreakzio desberdinen muga gaindituta unrecognized_emoji: ez da emoji ezaguna + redirects: + prompt: Esteka honetan fidatzen bazara, egin klik jarraitzeko. + title: "%{instance} instantziatik zoaz." relationships: activity: Kontuaren aktibitatea confirm_follow_selected_followers: Ziur hautatutako jarraitzaileei jarraitu nahi dituzula? diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 8e61c7b2a0fa6f..856532f8f13b7b 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -1546,6 +1546,9 @@ fi: errors: limit_reached: Erilaisten reaktioiden raja saavutettu unrecognized_emoji: ei ole tunnistettu emoji + redirects: + prompt: Jos luotat tähän linkkiin, jatka napsauttamalla. + title: Olet poistumassa palvelimelta %{instance}. relationships: activity: Tilin aktiivisuus confirm_follow_selected_followers: Haluatko varmasti seurata valittuja seuraajia? @@ -1791,8 +1794,8 @@ fi: subject: Arkisto on valmiina ladattavaksi title: Arkiston tallennus failed_2fa: - details: 'Tässä on tiedot kirjautumisyrityksestä:' - explanation: Joku on yrittänyt kirjautua tilillesi, mutta antanut virheellisen kaksivaiheisen todennuksen. + details: 'Tässä on tietoja kirjautumisyrityksestä:' + explanation: Joku on yrittänyt kirjautua tilillesi mutta on antanut virheellisen toisen vaiheen todennustekijän. further_actions_html: Jos se et ollut sinä, suosittelemme, että %{action} välittömästi, sillä se on saattanut vaarantua. subject: Kaksivaiheisen todennuksen virhe title: Epäonnistunut kaksivaiheinen todennus diff --git a/config/locales/fo.yml b/config/locales/fo.yml index 8e34265313052e..10b1e76f5f06f8 100644 --- a/config/locales/fo.yml +++ b/config/locales/fo.yml @@ -1546,6 +1546,9 @@ fo: errors: limit_reached: Mark fyri ymisk aftursvar rokkið unrecognized_emoji: er ikki eitt kenslutekn, sum kennist aftur + redirects: + prompt: Um tú lítir á hetta leinkið, so kanst tú klikkja á tað fyri at halda fram. + title: Tú fer burtur úr %{instance}. relationships: activity: Kontuvirksemi confirm_follow_selected_followers: Vil tú veruliga fylgja valdu fylgjarunum? diff --git a/config/locales/fr-CA.yml b/config/locales/fr-CA.yml index dbdff5f52c99c1..3676d0b7b5d01b 100644 --- a/config/locales/fr-CA.yml +++ b/config/locales/fr-CA.yml @@ -1546,6 +1546,9 @@ fr-CA: errors: limit_reached: Limite de réactions différentes atteinte unrecognized_emoji: n’est pas un émoji reconnu + redirects: + prompt: Si vous faites confiance à ce lien, cliquez pour continuer. + title: Vous quittez %{instance}. relationships: activity: Activité du compte confirm_follow_selected_followers: Voulez-vous vraiment suivre les abonné⋅e⋅s sélectionné⋅e⋅s ? @@ -1790,6 +1793,12 @@ fr-CA: extra: Elle est maintenant prête à être téléchargée ! subject: Votre archive est prête à être téléchargée title: Récupération de l’archive + failed_2fa: + details: 'Voici les détails de la tentative de connexion :' + explanation: Quelqu'un a essayé de se connecter à votre compte mais a fourni un second facteur d'authentification invalide. + further_actions_html: Si ce n'était pas vous, nous vous recommandons %{action} immédiatement car il pourrait être compromis. + subject: Échec de l'authentification à double facteur + title: Échec de l'authentification à double facteur suspicious_sign_in: change_password: changer votre mot de passe details: 'Voici les détails de la connexion :' @@ -1843,6 +1852,7 @@ fr-CA: go_to_sso_account_settings: Accédez aux paramètres du compte de votre fournisseur d'identité invalid_otp_token: Le code d’authentification à deux facteurs est invalide otp_lost_help_html: Si vous perdez accès aux deux, vous pouvez contacter %{email} + rate_limited: Trop de tentatives d'authentification, réessayez plus tard. seamless_external_login: Vous êtes connecté via un service externe, donc les paramètres concernant le mot de passe et le courriel ne sont pas disponibles. signed_in_as: 'Connecté·e en tant que :' verification: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index fe1a219a3157f5..a3aaf7a26e4d04 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1546,6 +1546,9 @@ fr: errors: limit_reached: Limite de réactions différentes atteinte unrecognized_emoji: n’est pas un émoji reconnu + redirects: + prompt: Si vous faites confiance à ce lien, cliquez pour continuer. + title: Vous quittez %{instance}. relationships: activity: Activité du compte confirm_follow_selected_followers: Voulez-vous vraiment suivre les abonné⋅e⋅s sélectionné⋅e⋅s ? @@ -1790,6 +1793,12 @@ fr: extra: Elle est maintenant prête à être téléchargée ! subject: Votre archive est prête à être téléchargée title: Récupération de l’archive + failed_2fa: + details: 'Voici les détails de la tentative de connexion :' + explanation: Quelqu'un a essayé de se connecter à votre compte mais a fourni un second facteur d'authentification invalide. + further_actions_html: Si ce n'était pas vous, nous vous recommandons %{action} immédiatement car il pourrait être compromis. + subject: Échec de l'authentification à double facteur + title: Échec de l'authentification à double facteur suspicious_sign_in: change_password: changer votre mot de passe details: 'Voici les détails de la connexion :' @@ -1843,6 +1852,7 @@ fr: go_to_sso_account_settings: Accédez aux paramètres du compte de votre fournisseur d'identité invalid_otp_token: Le code d’authentification à deux facteurs est invalide otp_lost_help_html: Si vous perdez accès aux deux, vous pouvez contacter %{email} + rate_limited: Trop de tentatives d'authentification, réessayez plus tard. seamless_external_login: Vous êtes connecté via un service externe, donc les paramètres concernant le mot de passe et le courriel ne sont pas disponibles. signed_in_as: 'Connecté·e en tant que :' verification: diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 087ed2ec76ca39..7b3fd1a6eb752e 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -1546,6 +1546,9 @@ gl: errors: limit_reached: Acadouse o límite das diferentes reaccións unrecognized_emoji: non é unha emoticona recoñecida + redirects: + prompt: Se confías nesta ligazón, preme nela para continuar. + title: Vas saír de %{instance}. relationships: activity: Actividade da conta confirm_follow_selected_followers: Tes a certeza de querer seguir as seguidoras seleccionadas? diff --git a/config/locales/he.yml b/config/locales/he.yml index 1f5fd096ac8d08..05b52213a74e00 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -1598,6 +1598,9 @@ he: errors: limit_reached: גבול מספר התגובות השונות הושג unrecognized_emoji: הוא לא אמוג'י מוכר + redirects: + prompt: יש ללחוץ על הקישור, אם לדעתך ניתן לסמוך עליו. + title: יציאה מתוך %{instance}. relationships: activity: רמת פעילות confirm_follow_selected_followers: האם את/ה בטוח/ה שברצונך לעקוב אחרי החשבונות שסומנו? @@ -1856,7 +1859,7 @@ he: title: הוצאת ארכיון failed_2fa: details: 'הנה פרטי נסיון ההתחברות:' - explanation: פולני אלמוני ניסה להתחבר לחשבונך אך האימות המשני נכשל. + explanation: פלוני אלמוני ניסה להתחבר לחשבונך אך האימות המשני נכשל. further_actions_html: אם הנסיון לא היה שלך, אנו ממליצים על %{action} באופן מיידי כדי שהחשבון לא יפול קורבן. subject: נכשל אימות בגורם שני title: אימות בגורם שני נכשל diff --git a/config/locales/hu.yml b/config/locales/hu.yml index 2870435ea72684..8cbbb64c97f59e 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -1546,6 +1546,8 @@ hu: errors: limit_reached: A különböző reakciók száma elérte a határértéket unrecognized_emoji: nem ismert emodzsi + redirects: + prompt: Ha megbízunk ebben a hivatkozásban, kattintsunk rá a folytatáshoz. relationships: activity: Fiók aktivitás confirm_follow_selected_followers: Biztos, hogy követni akarod a kiválasztott követőket? diff --git a/config/locales/is.yml b/config/locales/is.yml index 191383f56c190a..d374c60755c42f 100644 --- a/config/locales/is.yml +++ b/config/locales/is.yml @@ -1550,6 +1550,9 @@ is: errors: limit_reached: Hámarki mismunandi viðbragða náð unrecognized_emoji: er ekki þekkt tjáningartákn + redirects: + prompt: Ef þú treystir þessum tengli, geturðu smellt á hann til að halda áfram. + title: Þú ert að yfirgefa %{instance}. relationships: activity: Virkni aðgangs confirm_follow_selected_followers: Ertu viss um að þú viljir fylgjast með völdum fylgjendum? diff --git a/config/locales/it.yml b/config/locales/it.yml index 89ff071f36dd36..31de2252d181fb 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -1548,6 +1548,9 @@ it: errors: limit_reached: Raggiunto il limite di reazioni diverse unrecognized_emoji: non è un emoji riconosciuto + redirects: + prompt: Se ti fidi di questo collegamento, fai clic su di esso per continuare. + title: Stai lasciando %{instance}. relationships: activity: Attività dell'account confirm_follow_selected_followers: Sei sicuro di voler seguire i follower selezionati? diff --git a/config/locales/ja.yml b/config/locales/ja.yml index c966cbe36f99fc..2051e30aeeaeff 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1758,6 +1758,12 @@ ja: extra: ダウンロードの準備ができました! subject: アーカイブの準備ができました title: アーカイブの取り出し + failed_2fa: + details: '試行されたログインの詳細は以下のとおりです:' + explanation: アカウントへのログインが試行されましたが、二要素認証で不正な回答が送信されました。 + further_actions_html: このログインに心当たりがない場合は、ただちに%{action}してください。 + subject: 二要素認証に失敗しました + title: 二要素認証に失敗した記録があります suspicious_sign_in: change_password: パスワードを変更 details: 'ログインの詳細は以下のとおりです:' diff --git a/config/locales/ko.yml b/config/locales/ko.yml index b3c786e2654489..9f4f1343c7e40d 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -1522,6 +1522,9 @@ ko: errors: limit_reached: 리액션 갯수 제한에 도달했습니다 unrecognized_emoji: 인식 되지 않은 에모지입니다 + redirects: + prompt: 이 링크를 믿을 수 있다면, 클릭해서 계속하세요. + title: "%{instance}를 떠나려고 합니다." relationships: activity: 계정 활동 confirm_follow_selected_followers: 정말로 선택된 팔로워들을 팔로우하시겠습니까? @@ -1762,6 +1765,10 @@ ko: title: 아카이브 테이크아웃 failed_2fa: details: '로그인 시도에 대한 상세 정보입니다:' + explanation: 누군가가 내 계정에 로그인을 시도했지만 2차인증에 올바른 값을 입력하지 못했습니다. + further_actions_html: 만약 당신이 한 게 아니었다면 유출의 가능성이 있으니 가능한 빨리 %{action} 하시기 바랍니다. + subject: 2차 인증 실패 + title: 2차 인증에 실패했습니다 suspicious_sign_in: change_password: 암호 변경 details: '로그인에 대한 상세 정보입니다:' diff --git a/config/locales/lad.yml b/config/locales/lad.yml index be5d2d21bd5369..02308cf2f0c582 100644 --- a/config/locales/lad.yml +++ b/config/locales/lad.yml @@ -1516,6 +1516,9 @@ lad: errors: limit_reached: Limito de reaksyones desferentes alkansado unrecognized_emoji: no es un emoji konesido + redirects: + prompt: Si konfiyas en este atadijo, klikalo para kontinuar. + title: Estas salyendo de %{instance}. relationships: activity: Aktivita del kuento confirm_follow_selected_followers: Estas siguro ke keres segir a los suivantes eskojidos? diff --git a/config/locales/lt.yml b/config/locales/lt.yml index ba8b53fdc931de..1d159bf45aedeb 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -478,6 +478,9 @@ lt: other: Kita privacy: hint_html: "Tikrink, kaip nori, kad tavo profilis ir įrašai būtų randami. Įjungus įvairias Mastodon funkcijas, jos gali padėti pasiekti platesnę auditoriją. Akimirką peržiūrėk šiuos nustatymus, kad įsitikintum, jog jie atitinka tavo naudojimo būdą." + redirects: + prompt: Jei pasitiki šia nuoroda, spustelėk ją, kad tęstum. + title: Palieki %{instance} remote_follow: missing_resource: Jūsų paskyros nukreipimo URL nerasta scheduled_statuses: diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 2d27f9165d72d8..a3657890daeaf1 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -1546,6 +1546,9 @@ nl: errors: limit_reached: Limiet van verschillende emoji-reacties bereikt unrecognized_emoji: is geen bestaande emoji-reactie + redirects: + prompt: Als je deze link vertrouwt, klik er dan op om door te gaan. + title: Je verlaat %{instance}. relationships: activity: Accountactiviteit confirm_follow_selected_followers: Weet je zeker dat je de geselecteerde volgers wilt volgen? @@ -1792,7 +1795,8 @@ nl: title: Archief ophalen failed_2fa: details: 'Hier zijn details van de aanmeldpoging:' - explanation: Iemand heeft geprobeerd om in te loggen op uw account maar heeft een ongeldige tweede verificatiefactor opgegeven. + explanation: Iemand heeft geprobeerd om in te loggen op jouw account maar heeft een ongeldige tweede verificatiefactor opgegeven. + further_actions_html: Als jij dit niet was, raden we je aan om onmiddellijk %{action} aangezien het in gevaar kan zijn. subject: Tweede factor authenticatiefout title: Tweestapsverificatie mislukt suspicious_sign_in: diff --git a/config/locales/nn.yml b/config/locales/nn.yml index 95eed4978533d3..ffa5198a3a4ebf 100644 --- a/config/locales/nn.yml +++ b/config/locales/nn.yml @@ -1546,6 +1546,9 @@ nn: errors: limit_reached: Grensen for forskjellige reaksjoner nådd unrecognized_emoji: er ikke en gjenkjent emoji + redirects: + prompt: Hvis du stoler på denne lenken, så trykk på den for å fortsette. + title: Du forlater %{instance}. relationships: activity: Kontoaktivitet confirm_follow_selected_followers: Er du sikker på at du ynskjer å fylgja dei valde fylgjarane? diff --git a/config/locales/no.yml b/config/locales/no.yml index 7ece8564fc0a08..d26b20379e46d5 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -1546,6 +1546,9 @@ errors: limit_reached: Grensen for ulike reaksjoner nådd unrecognized_emoji: er ikke en gjenkjent emoji + redirects: + prompt: Hvis du stoler på denne lenken, så trykk på den for å fortsette. + title: Du forlater %{instance}. relationships: activity: Kontoaktivitet confirm_follow_selected_followers: Er du sikker på at du vil følge valgte følgere? diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 6718f1994be3e4..5bc78a6adfa4d7 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -1598,6 +1598,9 @@ pl: errors: limit_reached: Przekroczono limit różnych reakcji unrecognized_emoji: nie jest znanym emoji + redirects: + prompt: Kliknij ten link jeżeli mu ufasz. + title: Opuszczasz %{instance}. relationships: activity: Aktywność konta confirm_follow_selected_followers: Czy na pewno chcesz obserwować wybranych obserwujących? diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index c1a47c01612922..79396d627f4743 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -1546,6 +1546,9 @@ pt-BR: errors: limit_reached: Limite de reações diferentes atingido unrecognized_emoji: não é um emoji reconhecido + redirects: + prompt: Se você confia neste link, clique nele para continuar. + title: Você está saindo de %{instance}. relationships: activity: Atividade da conta confirm_follow_selected_followers: Tem certeza que deseja seguir os seguidores selecionados? diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml index 268531718df7b2..8a20bc68a148b0 100644 --- a/config/locales/pt-PT.yml +++ b/config/locales/pt-PT.yml @@ -1546,6 +1546,9 @@ pt-PT: errors: limit_reached: Alcançado limite de reações diferentes unrecognized_emoji: não é um emoji reconhecido + redirects: + prompt: Se confia nesta hiperligação, clique nela para continuar. + title: Está a deixar %{instance}. relationships: activity: Atividade da conta confirm_follow_selected_followers: Tem a certeza que deseja seguir os seguidores selecionados? diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 24edbdc75ea9a7..04e49e04277268 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -1598,6 +1598,9 @@ ru: errors: limit_reached: Достигнут лимит разных реакций unrecognized_emoji: не является распознанным эмодзи + redirects: + prompt: Если вы доверяете этой ссылке, нажмите на нее, чтобы продолжить. + title: Вы покидаете %{instance}. relationships: activity: Активность учётной записи confirm_follow_selected_followers: Вы уверены, что хотите подписаться на выбранных подписчиков? diff --git a/config/locales/sk.yml b/config/locales/sk.yml index e83ae348f6a22f..20df763463b70b 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -1101,6 +1101,9 @@ sk: errors: limit_reached: Maximálny počet rôznorodých reakcií bol dosiahnutý unrecognized_emoji: je neznámy smajlík + redirects: + prompt: Ak tomuto odkazu veríš, klikni naňho pre pokračovanie. + title: Opúšťaš %{instance}. relationships: activity: Aktivita účtu confirm_follow_selected_followers: Si si istý/á, že chceš nasledovať vybraných sledujúcich? diff --git a/config/locales/sq.yml b/config/locales/sq.yml index d6e6925c70ed51..3dd47312096656 100644 --- a/config/locales/sq.yml +++ b/config/locales/sq.yml @@ -1542,6 +1542,9 @@ sq: errors: limit_reached: U mbërrit në kufirin e reagimeve të ndryshme unrecognized_emoji: s’është emotikon i pranuar + redirects: + prompt: Nëse e besoni këtë lidhje, klikoni që të vazhdohet. + title: Po e braktisni %{instance}. relationships: activity: Veprimtari llogarie confirm_follow_selected_followers: Jeni i sigurt se doni të ndiqet ndjekësit e përzgjedhur? diff --git a/config/locales/sr-Latn.yml b/config/locales/sr-Latn.yml index 9cb555c94348bc..b55b6e0d19117a 100644 --- a/config/locales/sr-Latn.yml +++ b/config/locales/sr-Latn.yml @@ -1572,6 +1572,9 @@ sr-Latn: errors: limit_reached: Dostignuto je ograničenje različitih reakcija unrecognized_emoji: nije prepoznat emodži + redirects: + prompt: Ako verujete ovoj vezi, kliknite na nju za nastavak. + title: Napuštate %{instance}. relationships: activity: Aktivnost naloga confirm_follow_selected_followers: Da li ste sigurni da želite da pratite izabrane pratioce? diff --git a/config/locales/sr.yml b/config/locales/sr.yml index e1c2e992ed1104..8de7c90e73b594 100644 --- a/config/locales/sr.yml +++ b/config/locales/sr.yml @@ -1572,6 +1572,9 @@ sr: errors: limit_reached: Достигнуто је ограничење различитих реакција unrecognized_emoji: није препознат емоџи + redirects: + prompt: Ако верујете овој вези, кликните на њу за наставак. + title: Напуштате %{instance}. relationships: activity: Активност налога confirm_follow_selected_followers: Да ли сте сигурни да желите да пратите изабране пратиоце? diff --git a/config/locales/sv.yml b/config/locales/sv.yml index c9000d50fc1ae3..deac7cc63899bd 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -1545,6 +1545,9 @@ sv: errors: limit_reached: Gränsen för unika reaktioner uppnådd unrecognized_emoji: är inte en igenkänd emoji + redirects: + prompt: Om du litar på denna länk, klicka på den för att fortsätta. + title: Du lämnar %{instance}. relationships: activity: Kontoaktivitet confirm_follow_selected_followers: Är du säker på att du vill följa valda följare? diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 2b5b5ad45b4567..b3a52715b7f06c 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -1546,6 +1546,9 @@ tr: errors: limit_reached: Farklı reaksiyonların sınırına ulaşıldı unrecognized_emoji: tanınan bir emoji değil + redirects: + prompt: Eğer bu bağlantıya güveniyorsanız, tıklayıp devam edebilirsiniz. + title: "%{instance} sunucusundan ayrılıyorsunuz." relationships: activity: Hesap etkinliği confirm_follow_selected_followers: Seçili takipçileri takip etmek istediğinizden emin misiniz? diff --git a/config/locales/uk.yml b/config/locales/uk.yml index 40a858d72a1dd4..531bdb3d592341 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -1598,6 +1598,9 @@ uk: errors: limit_reached: Досягнуто обмеження різних реакцій unrecognized_emoji: не є розпізнаним емоджі + redirects: + prompt: Якщо ви довіряєте цьому посиланню, натисніть, щоб продовжити. + title: Ви покидаєте %{instance}. relationships: activity: Діяльність облікового запису confirm_follow_selected_followers: Ви справді бажаєте підписатися на обраних підписників? diff --git a/config/locales/vi.yml b/config/locales/vi.yml index 1ece72e154cfe6..045a000e38bd41 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -1520,6 +1520,9 @@ vi: errors: limit_reached: Bạn không nên thao tác liên tục unrecognized_emoji: không phải là emoji + redirects: + prompt: Nếu bạn tin tưởng, hãy nhấn tiếp tục. + title: Bạn đang thoát khỏi %{instance}. relationships: activity: Tương tác confirm_follow_selected_followers: Bạn có chắc muốn theo dõi những người đã chọn? diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 272787ce254c61..d1255bfefe2321 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -1520,6 +1520,9 @@ zh-CN: errors: limit_reached: 互动种类的限制 unrecognized_emoji: 不是一个可识别的表情 + redirects: + prompt: 如果您信任此链接,请单击以继续跳转。 + title: 您正在离开 %{instance} 。 relationships: activity: 账号活动 confirm_follow_selected_followers: 您确定想要关注所选的关注者吗? diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml index 0c39aa8c0b6268..b010a75c0473f8 100644 --- a/config/locales/zh-HK.yml +++ b/config/locales/zh-HK.yml @@ -1520,6 +1520,9 @@ zh-HK: errors: limit_reached: 已達到可以給予反應極限 unrecognized_emoji: 不能識別這個emoji + redirects: + prompt: 如果你信任此連結,點擊它繼續。 + title: 你即將離開 %{instance}。 relationships: activity: 帳戶活動 confirm_follow_selected_followers: 你確定要追蹤選取的追蹤者嗎? diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index 8726ea72a48651..72e63e47d3aa9c 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -57,7 +57,7 @@ zh-TW: destroyed_msg: 即將刪除 %{username} 的資料 disable: 停用 disable_sign_in_token_auth: 停用電子郵件 token 驗證 - disable_two_factor_authentication: 停用兩階段認證 + disable_two_factor_authentication: 停用兩階段驗證 disabled: 已停用 display_name: 暱稱 domain: 站點 @@ -195,7 +195,7 @@ zh-TW: destroy_status: 刪除狀態 destroy_unavailable_domain: 刪除無法存取的網域 destroy_user_role: 移除角色 - disable_2fa_user: 停用兩階段認證 + disable_2fa_user: 停用兩階段驗證 disable_custom_emoji: 停用自訂顏文字 disable_sign_in_token_auth_user: 停用使用者電子郵件 token 驗證 disable_user: 停用帳號 @@ -254,7 +254,7 @@ zh-TW: destroy_status_html: "%{name} 已刪除 %{target} 的嘟文" destroy_unavailable_domain_html: "%{name} 已恢復對網域 %{target} 的發送" destroy_user_role_html: "%{name} 已刪除 %{target} 角色" - disable_2fa_user_html: "%{name} 已停用使用者 %{target} 的兩階段認證 (2FA) " + disable_2fa_user_html: "%{name} 已停用使用者 %{target} 的兩階段驗證 (2FA) " disable_custom_emoji_html: "%{name} 已停用自訂表情符號 %{target}" disable_sign_in_token_auth_user_html: "%{name} 已停用 %{target} 之使用者電子郵件 token 驗證" disable_user_html: "%{name} 將使用者 %{target} 設定為禁止登入" @@ -418,7 +418,7 @@ zh-TW: view: 顯示已封鎖網域 email_domain_blocks: add_new: 加入新項目 - allow_registrations_with_approval: 經允許後可註冊 + allow_registrations_with_approval: 經審核後可註冊 attempts_over_week: other: 上週共有 %{count} 次註冊嘗試 created_msg: 已成功將電子郵件網域加入黑名單 @@ -505,7 +505,7 @@ zh-TW: delivery_available: 可傳送 delivery_error_days: 遞送失敗天數 delivery_error_hint: 若 %{count} 日皆無法遞送 ,則會自動標記無法遞送。 - destroyed_msg: 來自 %{domain} 的資料現在正在佇列中等待刪除。 + destroyed_msg: 來自 %{domain} 的資料目前正在佇列中等待刪除。 empty: 找不到網域 known_accounts: other: "%{count} 個已知帳號" @@ -759,7 +759,7 @@ zh-TW: title: 註冊 registrations_mode: modes: - approved: 註冊需要核准 + approved: 註冊需要審核 none: 沒有人可註冊 open: 任何人皆能註冊 security: @@ -870,7 +870,7 @@ zh-TW: links: allow: 允許連結 allow_provider: 允許發行者 - description_html: 這些連結是正在被您伺服器上看到該嘟文之帳號大量分享。這些連結可以幫助您的使用者探索現在世界上正在發生的事情。除非您核准該發行者,連結將不被公開展示。您也可以核准或駁回個別連結。 + description_html: 這些連結是正在被您伺服器上看到該嘟文之帳號大量分享。這些連結可以幫助您的使用者探索目前世界上正在發生的事情。除非您核准該發行者,連結將不被公開展示。您也可以核准或駁回個別連結。 disallow: 不允許連結 disallow_provider: 不允許發行者 no_link_selected: 因未選取任何連結,所以什麼事都沒發生 @@ -1062,7 +1062,7 @@ zh-TW: cas: CAS saml: SAML register: 註冊 - registration_closed: "%{instance} 現在不開放新成員" + registration_closed: "%{instance} 目前不開放新成員" resend_confirmation: 重新傳送確認連結 reset_password: 重設密碼 rules: @@ -1522,6 +1522,9 @@ zh-TW: errors: limit_reached: 達到可回應之上限 unrecognized_emoji: 並非一個可識別的 emoji + redirects: + prompt: 若您信任此連結,請點擊以繼續。 + title: 您將要離開 %{instance} 。 relationships: activity: 帳號動態 confirm_follow_selected_followers: 您確定要跟隨選取的跟隨者嗎? @@ -1627,7 +1630,7 @@ zh-TW: relationships: 跟隨中與跟隨者 statuses_cleanup: 自動嘟文刪除 strikes: 管理警告 - two_factor_authentication: 兩階段認證 + two_factor_authentication: 兩階段驗證 webauthn_authentication: 安全金鑰 statuses: attached: @@ -1733,11 +1736,11 @@ zh-TW: disable: 停用兩階段驗證 disabled_success: 已成功啟用兩階段驗證 edit: 編輯 - enabled: 兩階段認證已啟用 - enabled_success: 已成功啟用兩階段認證 + enabled: 兩階段驗證已啟用 + enabled_success: 兩階段驗證已成功啟用 generate_recovery_codes: 產生備用驗證碼 lost_recovery_codes: 讓您能於遺失手機時,使用備用驗證碼登入。若您已遺失備用驗證碼,可於此產生一批新的,舊有的備用驗證碼將會失效。 - methods: 兩步驟方式 + methods: 兩階段驗證 otp: 驗證應用程式 recovery_codes: 備份備用驗證碼 recovery_codes_regenerated: 成功產生新的備用驗證碼 @@ -1757,15 +1760,15 @@ zh-TW: title: 申訴被駁回 backup_ready: explanation: 您要求完整備份您的 Mastodon 帳號。 - extra: 準備好下載了! + extra: 準備好可供下載了! subject: 您的備份檔已可供下載 title: 檔案匯出 failed_2fa: details: 以下是該登入嘗試之詳細資訊: - explanation: 有人嘗試登入您的帳號,但提供了無效的第二個驗證因子。 + explanation: 有人嘗試登入您的帳號,但提供了無效的兩階段驗證。 further_actions_html: 若這並非您所為,我們建議您立刻 %{action},因為其可能已被入侵。 - subject: 第二因子驗證失敗 - title: 第二因子身份驗證失敗 + subject: 兩階段驗證失敗 + title: 兩階段驗證失敗 suspicious_sign_in: change_password: 變更密碼 details: 以下是該登入之詳細資訊: @@ -1817,9 +1820,9 @@ zh-TW: users: follow_limit_reached: 您無法跟隨多於 %{limit} 個人 go_to_sso_account_settings: 前往您的身分提供商 (identity provider) 之帳號設定 - invalid_otp_token: 兩階段認證碼不正確 + invalid_otp_token: 兩階段驗證碼不正確 otp_lost_help_html: 如果您無法存取這兩者,您可以透過 %{email} 與我們聯繫 - rate_limited: 身份驗證嘗試太多次,請稍後再試。 + rate_limited: 過多次身份驗證嘗試,請稍後再試。 seamless_external_login: 由於您是由外部系統登入,所以不能設定密碼與電子郵件。 signed_in_as: 目前登入的帳號: verification: From ca7053f19c425616b90774cd2f2553f0842cd314 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 25 Jan 2024 08:10:39 -0500 Subject: [PATCH 21/45] Consolidate db test prep steps to rake task (#28886) --- .../workflows/test-migrations-one-step.yml | 19 ++-------------- .../workflows/test-migrations-two-step.yml | 22 +++---------------- lib/tasks/tests.rake | 18 +++++++++++++++ 3 files changed, 23 insertions(+), 36 deletions(-) diff --git a/.github/workflows/test-migrations-one-step.yml b/.github/workflows/test-migrations-one-step.yml index 5dca8e376da09f..1ff5cc06b9f096 100644 --- a/.github/workflows/test-migrations-one-step.yml +++ b/.github/workflows/test-migrations-one-step.yml @@ -78,23 +78,8 @@ jobs: - name: Create database run: './bin/rails db:create' - - name: Run migrations up to v2.0.0 - run: './bin/rails db:migrate VERSION=20171010025614' - - - name: Populate database with test data - run: './bin/rails tests:migrations:populate_v2' - - - name: Run migrations up to v2.4.0 - run: './bin/rails db:migrate VERSION=20180514140000' - - - name: Populate database with test data - run: './bin/rails tests:migrations:populate_v2_4' - - - name: Run migrations up to v2.4.3 - run: './bin/rails db:migrate VERSION=20180707154237' - - - name: Populate database with test data - run: './bin/rails tests:migrations:populate_v2_4_3' + - name: Run historical migrations with data population + run: './bin/rails tests:migrations:prepare_database' - name: Run all remaining migrations run: './bin/rails db:migrate' diff --git a/.github/workflows/test-migrations-two-step.yml b/.github/workflows/test-migrations-two-step.yml index 59485d285df50d..66988473152655 100644 --- a/.github/workflows/test-migrations-two-step.yml +++ b/.github/workflows/test-migrations-two-step.yml @@ -45,6 +45,7 @@ jobs: --health-retries 5 ports: - 5432:5432 + redis: image: redis:7-alpine options: >- @@ -77,28 +78,11 @@ jobs: - name: Create database run: './bin/rails db:create' - - name: Run migrations up to v2.0.0 - run: './bin/rails db:migrate VERSION=20171010025614' - - - name: Populate database with test data - run: './bin/rails tests:migrations:populate_v2' - - - name: Run pre-deployment migrations up to v2.4.0 - run: './bin/rails db:migrate VERSION=20180514140000' - env: - SKIP_POST_DEPLOYMENT_MIGRATIONS: true - - - name: Populate database with test data - run: './bin/rails tests:migrations:populate_v2_4' - - - name: Run migrations up to v2.4.3 - run: './bin/rails db:migrate VERSION=20180707154237' + - name: Run historical migrations with data population + run: './bin/rails tests:migrations:prepare_database' env: SKIP_POST_DEPLOYMENT_MIGRATIONS: true - - name: Populate database with test data - run: './bin/rails tests:migrations:populate_v2_4_3' - - name: Run all remaining pre-deployment migrations run: './bin/rails db:migrate' env: diff --git a/lib/tasks/tests.rake b/lib/tasks/tests.rake index 45f055e2186e14..885be79f4168dc 100644 --- a/lib/tasks/tests.rake +++ b/lib/tasks/tests.rake @@ -2,6 +2,22 @@ namespace :tests do namespace :migrations do + desc 'Prepares all migrations and test data for consistency checks' + task prepare_database: :environment do + { + '2' => 2017_10_10_025614, + '2_4' => 2018_05_14_140000, + '2_4_3' => 2018_07_07_154237, + }.each do |release, version| + ActiveRecord::Tasks::DatabaseTasks + .migration_connection + .migration_context + .migrate(version) + Rake::Task["tests:migrations:populate_v#{release}"] + .invoke + end + end + desc 'Check that database state is consistent with a successful migration from populated data' task check_database: :environment do unless Account.find_by(username: 'admin', domain: nil)&.hide_collections? == false @@ -88,6 +104,8 @@ namespace :tests do puts 'Locale for fr-QC users not updated to fr-CA as expected' exit(1) end + + puts 'No errors found. Database state is consistent with a successful migration process.' end desc 'Populate the database with test data for 2.4.3' From c8f59d2ca4d693f328d8f68a21749ab4efc8c820 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 25 Jan 2024 08:28:49 -0500 Subject: [PATCH 22/45] Fix `Style/TernaryParentheses` cop (#28387) --- .rubocop_todo.yml | 7 ------- config/environments/development.rb | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 77f7e70734099a..fd9dc18ac7b5ef 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -279,13 +279,6 @@ Style/StringLiterals: - 'config/initializers/webauthn.rb' - 'config/routes.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, AllowSafeAssignment. -# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex -Style/TernaryParentheses: - Exclude: - - 'config/environments/development.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyleForMultiline. # SupportedStylesForMultiline: comma, consistent_comma, no_comma diff --git a/config/environments/development.rb b/config/environments/development.rb index 3c13ada380a19c..a855f5a16be0a3 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -85,7 +85,7 @@ # If using a Heroku, Vagrant or generic remote development environment, # use letter_opener_web, accessible at /letter_opener. # Otherwise, use letter_opener, which launches a browser window to view sent mail. - config.action_mailer.delivery_method = (ENV['HEROKU'] || ENV['VAGRANT'] || ENV['REMOTE_DEV']) ? :letter_opener_web : :letter_opener + config.action_mailer.delivery_method = ENV['HEROKU'] || ENV['VAGRANT'] || ENV['REMOTE_DEV'] ? :letter_opener_web : :letter_opener # We provide a default secret for the development environment here. # This value should not be used in production environments! From 2866106ec1fb1ac5b30f432730bd20c6459f6600 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 25 Jan 2024 08:37:25 -0500 Subject: [PATCH 23/45] Reduce factory creation in `spec/models/account_statuses_cleanup_policy` (#28361) --- .../account_statuses_cleanup_policy_spec.rb | 165 +++++++----------- 1 file changed, 59 insertions(+), 106 deletions(-) diff --git a/spec/models/account_statuses_cleanup_policy_spec.rb b/spec/models/account_statuses_cleanup_policy_spec.rb index da2a774b2d5b97..a08fd723a4d8f4 100644 --- a/spec/models/account_statuses_cleanup_policy_spec.rb +++ b/spec/models/account_statuses_cleanup_policy_spec.rb @@ -296,16 +296,11 @@ let!(:old_status) { Fabricate(:status, created_at: 1.year.ago, account: account) } let!(:slightly_less_old_status) { Fabricate(:status, created_at: 6.months.ago, account: account) } - it 'returns statuses including max_id' do - expect(subject).to include(old_status.id) - end - - it 'returns statuses including older than max_id' do - expect(subject).to include(very_old_status.id) - end - - it 'does not return statuses newer than max_id' do - expect(subject).to_not include(slightly_less_old_status.id) + it 'returns statuses included the max_id and older than the max_id but not newer than max_id' do + expect(subject) + .to include(old_status.id) + .and include(very_old_status.id) + .and not_include(slightly_less_old_status.id) end end @@ -315,16 +310,11 @@ let!(:old_status) { Fabricate(:status, created_at: 1.year.ago, account: account) } let!(:slightly_less_old_status) { Fabricate(:status, created_at: 6.months.ago, account: account) } - it 'returns statuses including min_id' do - expect(subject).to include(old_status.id) - end - - it 'returns statuses including newer than max_id' do - expect(subject).to include(slightly_less_old_status.id) - end - - it 'does not return statuses older than min_id' do - expect(subject).to_not include(very_old_status.id) + it 'returns statuses including min_id and newer than min_id, but not older than min_id' do + expect(subject) + .to include(old_status.id) + .and include(slightly_less_old_status.id) + .and not_include(very_old_status.id) end end @@ -339,12 +329,10 @@ account_statuses_cleanup_policy.min_status_age = 2.years.seconds end - it 'does not return unrelated old status' do - expect(subject.pluck(:id)).to_not include(unrelated_status.id) - end - - it 'returns only oldest status for deletion' do - expect(subject.pluck(:id)).to eq [very_old_status.id] + it 'does not return unrelated old status and does return oldest status' do + expect(subject.pluck(:id)) + .to not_include(unrelated_status.id) + .and eq [very_old_status.id] end end @@ -358,12 +346,10 @@ account_statuses_cleanup_policy.keep_self_bookmark = false end - it 'does not return the old direct message for deletion' do - expect(subject.pluck(:id)).to_not include(direct_message.id) - end - - it 'returns every other old status for deletion' do - expect(subject.pluck(:id)).to include(very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) + it 'returns every old status except does not return the old direct message for deletion' do + expect(subject.pluck(:id)) + .to not_include(direct_message.id) + .and include(very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) end end @@ -377,12 +363,10 @@ account_statuses_cleanup_policy.keep_self_bookmark = true end - it 'does not return the old self-bookmarked message for deletion' do - expect(subject.pluck(:id)).to_not include(self_bookmarked.id) - end - - it 'returns every other old status for deletion' do - expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) + it 'returns every old status but does not return the old self-bookmarked message for deletion' do + expect(subject.pluck(:id)) + .to not_include(self_bookmarked.id) + .and include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) end end @@ -396,12 +380,10 @@ account_statuses_cleanup_policy.keep_self_bookmark = false end - it 'does not return the old self-bookmarked message for deletion' do - expect(subject.pluck(:id)).to_not include(self_faved.id) - end - - it 'returns every other old status for deletion' do - expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) + it 'returns every old status but does not return the old self-faved message for deletion' do + expect(subject.pluck(:id)) + .to not_include(self_faved.id) + .and include(direct_message.id, very_old_status.id, pinned_status.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) end end @@ -415,12 +397,10 @@ account_statuses_cleanup_policy.keep_self_bookmark = false end - it 'does not return the old message with media for deletion' do - expect(subject.pluck(:id)).to_not include(status_with_media.id) - end - - it 'returns every other old status for deletion' do - expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) + it 'returns every old status but does not return the old message with media for deletion' do + expect(subject.pluck(:id)) + .to not_include(status_with_media.id) + .and include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) end end @@ -434,12 +414,10 @@ account_statuses_cleanup_policy.keep_self_bookmark = false end - it 'does not return the old poll message for deletion' do - expect(subject.pluck(:id)).to_not include(status_with_poll.id) - end - - it 'returns every other old status for deletion' do - expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) + it 'returns every old status but does not return the old poll message for deletion' do + expect(subject.pluck(:id)) + .to not_include(status_with_poll.id) + .and include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) end end @@ -453,12 +431,10 @@ account_statuses_cleanup_policy.keep_self_bookmark = false end - it 'does not return the old pinned message for deletion' do - expect(subject.pluck(:id)).to_not include(pinned_status.id) - end - - it 'returns every other old status for deletion' do - expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) + it 'returns every old status but does not return the old pinned message for deletion' do + expect(subject.pluck(:id)) + .to not_include(pinned_status.id) + .and include(direct_message.id, very_old_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) end end @@ -472,16 +448,11 @@ account_statuses_cleanup_policy.keep_self_bookmark = false end - it 'does not return the recent toot' do - expect(subject.pluck(:id)).to_not include(recent_status.id) - end - - it 'does not return the unrelated toot' do - expect(subject.pluck(:id)).to_not include(unrelated_status.id) - end - - it 'returns every other old status for deletion' do - expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) + it 'returns every old status but does not return the recent or unrelated statuses' do + expect(subject.pluck(:id)) + .to not_include(recent_status.id) + .and not_include(unrelated_status.id) + .and include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) end end @@ -495,12 +466,10 @@ account_statuses_cleanup_policy.keep_self_bookmark = true end - it 'does not return unrelated old status' do - expect(subject.pluck(:id)).to_not include(unrelated_status.id) - end - - it 'returns only normal statuses for deletion' do - expect(subject.pluck(:id)).to contain_exactly(very_old_status.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) + it 'returns normal statuses and does not return unrelated old status' do + expect(subject.pluck(:id)) + .to not_include(unrelated_status.id) + .and contain_exactly(very_old_status.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) end end @@ -509,20 +478,12 @@ account_statuses_cleanup_policy.min_reblogs = 5 end - it 'does not return the recent toot' do - expect(subject.pluck(:id)).to_not include(recent_status.id) - end - - it 'does not return the toot reblogged 5 times' do - expect(subject.pluck(:id)).to_not include(reblogged_secondary.id) - end - - it 'does not return the unrelated toot' do - expect(subject.pluck(:id)).to_not include(unrelated_status.id) - end - - it 'returns old statuses not reblogged as much' do - expect(subject.pluck(:id)).to include(very_old_status.id, faved_primary.id, faved_secondary.id, reblogged_primary.id) + it 'returns old not-reblogged statuses but does not return the recent, 5-times reblogged, or unrelated statuses' do + expect(subject.pluck(:id)) + .to not_include(recent_status.id) + .and not_include(reblogged_secondary.id) + .and not_include(unrelated_status.id) + .and include(very_old_status.id, faved_primary.id, faved_secondary.id, reblogged_primary.id) end end @@ -531,20 +492,12 @@ account_statuses_cleanup_policy.min_favs = 5 end - it 'does not return the recent toot' do - expect(subject.pluck(:id)).to_not include(recent_status.id) - end - - it 'does not return the toot faved 5 times' do - expect(subject.pluck(:id)).to_not include(faved_secondary.id) - end - - it 'does not return the unrelated toot' do - expect(subject.pluck(:id)).to_not include(unrelated_status.id) - end - - it 'returns old statuses not faved as much' do - expect(subject.pluck(:id)).to include(very_old_status.id, faved_primary.id, reblogged_primary.id, reblogged_secondary.id) + it 'returns old not-faved statuses but does not return the recent, 5-times faved, or unrelated statuses' do + expect(subject.pluck(:id)) + .to not_include(recent_status.id) + .and not_include(faved_secondary.id) + .and not_include(unrelated_status.id) + .and include(very_old_status.id, faved_primary.id, reblogged_primary.id, reblogged_secondary.id) end end end From 274a48a9f4cd5bb36ad6933d736c04d04db75af0 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 25 Jan 2024 08:49:33 -0500 Subject: [PATCH 24/45] Extract helper methods for db connection and table existence check in `CLI::Maintenance` task (#28281) --- lib/mastodon/cli/maintenance.rb | 150 +++++++++++++++++--------------- 1 file changed, 81 insertions(+), 69 deletions(-) diff --git a/lib/mastodon/cli/maintenance.rb b/lib/mastodon/cli/maintenance.rb index 73012812fdddf3..c644729b5e9154 100644 --- a/lib/mastodon/cli/maintenance.rb +++ b/lib/mastodon/cli/maintenance.rb @@ -72,6 +72,10 @@ def acct local? ? username : "#{username}@#{domain}" end + def db_table_exists?(table) + ActiveRecord::Base.connection.table_exists?(table) + end + # This is a duplicate of the Account::Merging concern because we need it # to be independent from code version. def merge_with!(other_account) @@ -88,12 +92,12 @@ def merge_with!(other_account) AccountModerationNote, AccountPin, AccountStat, ListAccount, PollVote, Mention ] - owned_classes << AccountDeletionRequest if ActiveRecord::Base.connection.table_exists?(:account_deletion_requests) - owned_classes << AccountNote if ActiveRecord::Base.connection.table_exists?(:account_notes) - owned_classes << FollowRecommendationSuppression if ActiveRecord::Base.connection.table_exists?(:follow_recommendation_suppressions) - owned_classes << AccountIdentityProof if ActiveRecord::Base.connection.table_exists?(:account_identity_proofs) - owned_classes << Appeal if ActiveRecord::Base.connection.table_exists?(:appeals) - owned_classes << BulkImport if ActiveRecord::Base.connection.table_exists?(:bulk_imports) + owned_classes << AccountDeletionRequest if db_table_exists?(:account_deletion_requests) + owned_classes << AccountNote if db_table_exists?(:account_notes) + owned_classes << FollowRecommendationSuppression if db_table_exists?(:follow_recommendation_suppressions) + owned_classes << AccountIdentityProof if db_table_exists?(:account_identity_proofs) + owned_classes << Appeal if db_table_exists?(:appeals) + owned_classes << BulkImport if db_table_exists?(:bulk_imports) owned_classes.each do |klass| klass.where(account_id: other_account.id).find_each do |record| @@ -104,7 +108,7 @@ def merge_with!(other_account) end target_classes = [Follow, FollowRequest, Block, Mute, AccountModerationNote, AccountPin] - target_classes << AccountNote if ActiveRecord::Base.connection.table_exists?(:account_notes) + target_classes << AccountNote if db_table_exists?(:account_notes) target_classes.each do |klass| klass.where(target_account_id: other_account.id).find_each do |record| @@ -114,13 +118,13 @@ def merge_with!(other_account) end end - if ActiveRecord::Base.connection.table_exists?(:canonical_email_blocks) + if db_table_exists?(:canonical_email_blocks) CanonicalEmailBlock.where(reference_account_id: other_account.id).find_each do |record| record.update_attribute(:reference_account_id, id) end end - if ActiveRecord::Base.connection.table_exists?(:appeals) + if db_table_exists?(:appeals) Appeal.where(account_warning_id: other_account.id).find_each do |record| record.update_attribute(:account_warning_id, id) end @@ -234,16 +238,16 @@ def deduplicate_accounts! say 'Restoring index_accounts_on_username_and_domain_lower…' if migrator_version < 2020_06_20_164023 - ActiveRecord::Base.connection.add_index :accounts, 'lower (username), lower(domain)', name: 'index_accounts_on_username_and_domain_lower', unique: true + database_connection.add_index :accounts, 'lower (username), lower(domain)', name: 'index_accounts_on_username_and_domain_lower', unique: true else - ActiveRecord::Base.connection.add_index :accounts, "lower (username), COALESCE(lower(domain), '')", name: 'index_accounts_on_username_and_domain_lower', unique: true + database_connection.add_index :accounts, "lower (username), COALESCE(lower(domain), '')", name: 'index_accounts_on_username_and_domain_lower', unique: true end say 'Reindexing textual indexes on accounts…' - ActiveRecord::Base.connection.execute('REINDEX INDEX search_index;') - ActiveRecord::Base.connection.execute('REINDEX INDEX index_accounts_on_uri;') - ActiveRecord::Base.connection.execute('REINDEX INDEX index_accounts_on_url;') - ActiveRecord::Base.connection.execute('REINDEX INDEX index_accounts_on_domain_and_id;') if migrator_version >= 2023_05_24_190515 + database_connection.execute('REINDEX INDEX search_index;') + database_connection.execute('REINDEX INDEX index_accounts_on_uri;') + database_connection.execute('REINDEX INDEX index_accounts_on_url;') + database_connection.execute('REINDEX INDEX index_accounts_on_domain_and_id;') if migrator_version >= 2023_05_24_190515 end def deduplicate_users! @@ -260,21 +264,21 @@ def deduplicate_users! deduplicate_users_process_password_token say 'Restoring users indexes…' - ActiveRecord::Base.connection.add_index :users, ['confirmation_token'], name: 'index_users_on_confirmation_token', unique: true - ActiveRecord::Base.connection.add_index :users, ['email'], name: 'index_users_on_email', unique: true - ActiveRecord::Base.connection.add_index :users, ['remember_token'], name: 'index_users_on_remember_token', unique: true if migrator_version < 2022_01_18_183010 + database_connection.add_index :users, ['confirmation_token'], name: 'index_users_on_confirmation_token', unique: true + database_connection.add_index :users, ['email'], name: 'index_users_on_email', unique: true + database_connection.add_index :users, ['remember_token'], name: 'index_users_on_remember_token', unique: true if migrator_version < 2022_01_18_183010 if migrator_version < 2022_03_10_060641 - ActiveRecord::Base.connection.add_index :users, ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true + database_connection.add_index :users, ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true else - ActiveRecord::Base.connection.add_index :users, ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true, where: 'reset_password_token IS NOT NULL', opclass: :text_pattern_ops + database_connection.add_index :users, ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true, where: 'reset_password_token IS NOT NULL', opclass: :text_pattern_ops end - ActiveRecord::Base.connection.execute('REINDEX INDEX index_users_on_unconfirmed_email;') if migrator_version >= 2023_07_02_151753 + database_connection.execute('REINDEX INDEX index_users_on_unconfirmed_email;') if migrator_version >= 2023_07_02_151753 end def deduplicate_users_process_email - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users GROUP BY email HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users GROUP BY email HAVING count(*) > 1").each do |row| users = User.where(id: row['ids'].split(',')).order(updated_at: :desc).includes(:account).to_a ref_user = users.shift say "Multiple users registered with e-mail address #{ref_user.email}.", :yellow @@ -288,7 +292,7 @@ def deduplicate_users_process_email end def deduplicate_users_process_confirmation_token - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE confirmation_token IS NOT NULL GROUP BY confirmation_token HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE confirmation_token IS NOT NULL GROUP BY confirmation_token HAVING count(*) > 1").each do |row| users = User.where(id: row['ids'].split(',')).order(created_at: :desc).includes(:account).to_a.drop(1) say "Unsetting confirmation token for those accounts: #{users.map { |user| user.account.acct }.join(', ')}", :yellow @@ -300,7 +304,7 @@ def deduplicate_users_process_confirmation_token def deduplicate_users_process_remember_token if migrator_version < 2022_01_18_183010 - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE remember_token IS NOT NULL GROUP BY remember_token HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE remember_token IS NOT NULL GROUP BY remember_token HAVING count(*) > 1").each do |row| users = User.where(id: row['ids'].split(',')).order(updated_at: :desc).to_a.drop(1) say "Unsetting remember token for those accounts: #{users.map { |user| user.account.acct }.join(', ')}", :yellow @@ -312,7 +316,7 @@ def deduplicate_users_process_remember_token end def deduplicate_users_process_password_token - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE reset_password_token IS NOT NULL GROUP BY reset_password_token HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE reset_password_token IS NOT NULL GROUP BY reset_password_token HAVING count(*) > 1").each do |row| users = User.where(id: row['ids'].split(',')).order(updated_at: :desc).includes(:account).to_a.drop(1) say "Unsetting password reset token for those accounts: #{users.map { |user| user.account.acct }.join(', ')}", :yellow @@ -326,47 +330,47 @@ def deduplicate_account_domain_blocks! remove_index_if_exists!(:account_domain_blocks, 'index_account_domain_blocks_on_account_id_and_domain') say 'Removing duplicate account domain blocks…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM account_domain_blocks GROUP BY account_id, domain HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM account_domain_blocks GROUP BY account_id, domain HAVING count(*) > 1").each do |row| AccountDomainBlock.where(id: row['ids'].split(',').drop(1)).delete_all end say 'Restoring account domain blocks indexes…' - ActiveRecord::Base.connection.add_index :account_domain_blocks, %w(account_id domain), name: 'index_account_domain_blocks_on_account_id_and_domain', unique: true + database_connection.add_index :account_domain_blocks, %w(account_id domain), name: 'index_account_domain_blocks_on_account_id_and_domain', unique: true end def deduplicate_account_identity_proofs! - return unless ActiveRecord::Base.connection.table_exists?(:account_identity_proofs) + return unless db_table_exists?(:account_identity_proofs) remove_index_if_exists!(:account_identity_proofs, 'index_account_proofs_on_account_and_provider_and_username') say 'Removing duplicate account identity proofs…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM account_identity_proofs GROUP BY account_id, provider, provider_username HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM account_identity_proofs GROUP BY account_id, provider, provider_username HAVING count(*) > 1").each do |row| AccountIdentityProof.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy) end say 'Restoring account identity proofs indexes…' - ActiveRecord::Base.connection.add_index :account_identity_proofs, %w(account_id provider provider_username), name: 'index_account_proofs_on_account_and_provider_and_username', unique: true + database_connection.add_index :account_identity_proofs, %w(account_id provider provider_username), name: 'index_account_proofs_on_account_and_provider_and_username', unique: true end def deduplicate_announcement_reactions! - return unless ActiveRecord::Base.connection.table_exists?(:announcement_reactions) + return unless db_table_exists?(:announcement_reactions) remove_index_if_exists!(:announcement_reactions, 'index_announcement_reactions_on_account_id_and_announcement_id') say 'Removing duplicate announcement reactions…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM announcement_reactions GROUP BY account_id, announcement_id, name HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM announcement_reactions GROUP BY account_id, announcement_id, name HAVING count(*) > 1").each do |row| AnnouncementReaction.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy) end say 'Restoring announcement_reactions indexes…' - ActiveRecord::Base.connection.add_index :announcement_reactions, %w(account_id announcement_id name), name: 'index_announcement_reactions_on_account_id_and_announcement_id', unique: true + database_connection.add_index :announcement_reactions, %w(account_id announcement_id name), name: 'index_announcement_reactions_on_account_id_and_announcement_id', unique: true end def deduplicate_conversations! remove_index_if_exists!(:conversations, 'index_conversations_on_uri') say 'Deduplicating conversations…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM conversations WHERE uri IS NOT NULL GROUP BY uri HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM conversations WHERE uri IS NOT NULL GROUP BY uri HAVING count(*) > 1").each do |row| conversations = Conversation.where(id: row['ids'].split(',')).order(id: :desc).to_a ref_conversation = conversations.shift @@ -379,9 +383,9 @@ def deduplicate_conversations! say 'Restoring conversations indexes…' if migrator_version < 2022_03_07_083603 - ActiveRecord::Base.connection.add_index :conversations, ['uri'], name: 'index_conversations_on_uri', unique: true + database_connection.add_index :conversations, ['uri'], name: 'index_conversations_on_uri', unique: true else - ActiveRecord::Base.connection.add_index :conversations, ['uri'], name: 'index_conversations_on_uri', unique: true, where: 'uri IS NOT NULL', opclass: :text_pattern_ops + database_connection.add_index :conversations, ['uri'], name: 'index_conversations_on_uri', unique: true, where: 'uri IS NOT NULL', opclass: :text_pattern_ops end end @@ -389,7 +393,7 @@ def deduplicate_custom_emojis! remove_index_if_exists!(:custom_emojis, 'index_custom_emojis_on_shortcode_and_domain') say 'Deduplicating custom_emojis…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM custom_emojis GROUP BY shortcode, domain HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM custom_emojis GROUP BY shortcode, domain HAVING count(*) > 1").each do |row| emojis = CustomEmoji.where(id: row['ids'].split(',')).order(id: :desc).to_a ref_emoji = emojis.shift @@ -401,14 +405,14 @@ def deduplicate_custom_emojis! end say 'Restoring custom_emojis indexes…' - ActiveRecord::Base.connection.add_index :custom_emojis, %w(shortcode domain), name: 'index_custom_emojis_on_shortcode_and_domain', unique: true + database_connection.add_index :custom_emojis, %w(shortcode domain), name: 'index_custom_emojis_on_shortcode_and_domain', unique: true end def deduplicate_custom_emoji_categories! remove_index_if_exists!(:custom_emoji_categories, 'index_custom_emoji_categories_on_name') say 'Deduplicating custom_emoji_categories…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM custom_emoji_categories GROUP BY name HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM custom_emoji_categories GROUP BY name HAVING count(*) > 1").each do |row| categories = CustomEmojiCategory.where(id: row['ids'].split(',')).order(id: :desc).to_a ref_category = categories.shift @@ -420,26 +424,26 @@ def deduplicate_custom_emoji_categories! end say 'Restoring custom_emoji_categories indexes…' - ActiveRecord::Base.connection.add_index :custom_emoji_categories, ['name'], name: 'index_custom_emoji_categories_on_name', unique: true + database_connection.add_index :custom_emoji_categories, ['name'], name: 'index_custom_emoji_categories_on_name', unique: true end def deduplicate_domain_allows! remove_index_if_exists!(:domain_allows, 'index_domain_allows_on_domain') say 'Deduplicating domain_allows…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM domain_allows GROUP BY domain HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM domain_allows GROUP BY domain HAVING count(*) > 1").each do |row| DomainAllow.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy) end say 'Restoring domain_allows indexes…' - ActiveRecord::Base.connection.add_index :domain_allows, ['domain'], name: 'index_domain_allows_on_domain', unique: true + database_connection.add_index :domain_allows, ['domain'], name: 'index_domain_allows_on_domain', unique: true end def deduplicate_domain_blocks! remove_index_if_exists!(:domain_blocks, 'index_domain_blocks_on_domain') say 'Deduplicating domain_blocks…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM domain_blocks GROUP BY domain HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM domain_blocks GROUP BY domain HAVING count(*) > 1").each do |row| domain_blocks = DomainBlock.where(id: row['ids'].split(',')).by_severity.reverse.to_a reject_media = domain_blocks.any?(&:reject_media?) @@ -456,49 +460,49 @@ def deduplicate_domain_blocks! end say 'Restoring domain_blocks indexes…' - ActiveRecord::Base.connection.add_index :domain_blocks, ['domain'], name: 'index_domain_blocks_on_domain', unique: true + database_connection.add_index :domain_blocks, ['domain'], name: 'index_domain_blocks_on_domain', unique: true end def deduplicate_unavailable_domains! - return unless ActiveRecord::Base.connection.table_exists?(:unavailable_domains) + return unless db_table_exists?(:unavailable_domains) remove_index_if_exists!(:unavailable_domains, 'index_unavailable_domains_on_domain') say 'Deduplicating unavailable_domains…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM unavailable_domains GROUP BY domain HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM unavailable_domains GROUP BY domain HAVING count(*) > 1").each do |row| UnavailableDomain.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy) end say 'Restoring unavailable_domains indexes…' - ActiveRecord::Base.connection.add_index :unavailable_domains, ['domain'], name: 'index_unavailable_domains_on_domain', unique: true + database_connection.add_index :unavailable_domains, ['domain'], name: 'index_unavailable_domains_on_domain', unique: true end def deduplicate_email_domain_blocks! remove_index_if_exists!(:email_domain_blocks, 'index_email_domain_blocks_on_domain') say 'Deduplicating email_domain_blocks…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM email_domain_blocks GROUP BY domain HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM email_domain_blocks GROUP BY domain HAVING count(*) > 1").each do |row| domain_blocks = EmailDomainBlock.where(id: row['ids'].split(',')).order(EmailDomainBlock.arel_table[:parent_id].asc.nulls_first).to_a domain_blocks.drop(1).each(&:destroy) end say 'Restoring email_domain_blocks indexes…' - ActiveRecord::Base.connection.add_index :email_domain_blocks, ['domain'], name: 'index_email_domain_blocks_on_domain', unique: true + database_connection.add_index :email_domain_blocks, ['domain'], name: 'index_email_domain_blocks_on_domain', unique: true end def deduplicate_media_attachments! remove_index_if_exists!(:media_attachments, 'index_media_attachments_on_shortcode') say 'Deduplicating media_attachments…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM media_attachments WHERE shortcode IS NOT NULL GROUP BY shortcode HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM media_attachments WHERE shortcode IS NOT NULL GROUP BY shortcode HAVING count(*) > 1").each do |row| MediaAttachment.where(id: row['ids'].split(',').drop(1)).update_all(shortcode: nil) end say 'Restoring media_attachments indexes…' if migrator_version < 2022_03_10_060626 - ActiveRecord::Base.connection.add_index :media_attachments, ['shortcode'], name: 'index_media_attachments_on_shortcode', unique: true + database_connection.add_index :media_attachments, ['shortcode'], name: 'index_media_attachments_on_shortcode', unique: true else - ActiveRecord::Base.connection.add_index :media_attachments, ['shortcode'], name: 'index_media_attachments_on_shortcode', unique: true, where: 'shortcode IS NOT NULL', opclass: :text_pattern_ops + database_connection.add_index :media_attachments, ['shortcode'], name: 'index_media_attachments_on_shortcode', unique: true, where: 'shortcode IS NOT NULL', opclass: :text_pattern_ops end end @@ -506,19 +510,19 @@ def deduplicate_preview_cards! remove_index_if_exists!(:preview_cards, 'index_preview_cards_on_url') say 'Deduplicating preview_cards…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM preview_cards GROUP BY url HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM preview_cards GROUP BY url HAVING count(*) > 1").each do |row| PreviewCard.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy) end say 'Restoring preview_cards indexes…' - ActiveRecord::Base.connection.add_index :preview_cards, ['url'], name: 'index_preview_cards_on_url', unique: true + database_connection.add_index :preview_cards, ['url'], name: 'index_preview_cards_on_url', unique: true end def deduplicate_statuses! remove_index_if_exists!(:statuses, 'index_statuses_on_uri') say 'Deduplicating statuses…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM statuses WHERE uri IS NOT NULL GROUP BY uri HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM statuses WHERE uri IS NOT NULL GROUP BY uri HAVING count(*) > 1").each do |row| statuses = Status.where(id: row['ids'].split(',')).order(id: :asc).to_a ref_status = statuses.shift statuses.each do |status| @@ -529,9 +533,9 @@ def deduplicate_statuses! say 'Restoring statuses indexes…' if migrator_version < 2022_03_10_060706 - ActiveRecord::Base.connection.add_index :statuses, ['uri'], name: 'index_statuses_on_uri', unique: true + database_connection.add_index :statuses, ['uri'], name: 'index_statuses_on_uri', unique: true else - ActiveRecord::Base.connection.add_index :statuses, ['uri'], name: 'index_statuses_on_uri', unique: true, where: 'uri IS NOT NULL', opclass: :text_pattern_ops + database_connection.add_index :statuses, ['uri'], name: 'index_statuses_on_uri', unique: true, where: 'uri IS NOT NULL', opclass: :text_pattern_ops end end @@ -540,7 +544,7 @@ def deduplicate_tags! remove_index_if_exists!(:tags, 'index_tags_on_name_lower_btree') say 'Deduplicating tags…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM tags GROUP BY lower((name)::text) HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM tags GROUP BY lower((name)::text) HAVING count(*) > 1").each do |row| tags = Tag.where(id: row['ids'].split(',')).order(Arel.sql('(usable::int + trendable::int + listable::int) desc')).to_a ref_tag = tags.shift tags.each do |tag| @@ -551,38 +555,38 @@ def deduplicate_tags! say 'Restoring tags indexes…' if migrator_version < 2021_04_21_121431 - ActiveRecord::Base.connection.add_index :tags, 'lower((name)::text)', name: 'index_tags_on_name_lower', unique: true + database_connection.add_index :tags, 'lower((name)::text)', name: 'index_tags_on_name_lower', unique: true else - ActiveRecord::Base.connection.execute 'CREATE UNIQUE INDEX index_tags_on_name_lower_btree ON tags (lower(name) text_pattern_ops)' + database_connection.execute 'CREATE UNIQUE INDEX index_tags_on_name_lower_btree ON tags (lower(name) text_pattern_ops)' end end def deduplicate_webauthn_credentials! - return unless ActiveRecord::Base.connection.table_exists?(:webauthn_credentials) + return unless db_table_exists?(:webauthn_credentials) remove_index_if_exists!(:webauthn_credentials, 'index_webauthn_credentials_on_external_id') say 'Deduplicating webauthn_credentials…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM webauthn_credentials GROUP BY external_id HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM webauthn_credentials GROUP BY external_id HAVING count(*) > 1").each do |row| WebauthnCredential.where(id: row['ids'].split(',')).order(id: :desc).to_a.drop(1).each(&:destroy) end say 'Restoring webauthn_credentials indexes…' - ActiveRecord::Base.connection.add_index :webauthn_credentials, ['external_id'], name: 'index_webauthn_credentials_on_external_id', unique: true + database_connection.add_index :webauthn_credentials, ['external_id'], name: 'index_webauthn_credentials_on_external_id', unique: true end def deduplicate_webhooks! - return unless ActiveRecord::Base.connection.table_exists?(:webhooks) + return unless db_table_exists?(:webhooks) remove_index_if_exists!(:webhooks, 'index_webhooks_on_url') say 'Deduplicating webhooks…' - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM webhooks GROUP BY url HAVING count(*) > 1").each do |row| + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM webhooks GROUP BY url HAVING count(*) > 1").each do |row| Webhook.where(id: row['ids'].split(',')).order(id: :desc).drop(1).each(&:destroy) end say 'Restoring webhooks indexes…' - ActiveRecord::Base.connection.add_index :webhooks, ['url'], name: 'index_webhooks_on_url', unique: true + database_connection.add_index :webhooks, ['url'], name: 'index_webhooks_on_url', unique: true end def deduplicate_software_updates! @@ -672,7 +676,7 @@ def merge_custom_emoji_categories!(main_category, duplicate_category) def merge_statuses!(main_status, duplicate_status) owned_classes = [Favourite, Mention, Poll] - owned_classes << Bookmark if ActiveRecord::Base.connection.table_exists?(:bookmarks) + owned_classes << Bookmark if db_table_exists?(:bookmarks) owned_classes.each do |klass| klass.where(status_id: duplicate_status.id).find_each do |record| record.update_attribute(:status_id, main_status.id) @@ -715,13 +719,21 @@ def migrator_version end def find_duplicate_accounts - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM accounts GROUP BY lower(username), COALESCE(lower(domain), '') HAVING count(*) > 1") + database_connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM accounts GROUP BY lower(username), COALESCE(lower(domain), '') HAVING count(*) > 1") end def remove_index_if_exists!(table, name) - ActiveRecord::Base.connection.remove_index(table, name: name) if ActiveRecord::Base.connection.index_name_exists?(table, name) + database_connection.remove_index(table, name: name) if database_connection.index_name_exists?(table, name) rescue ArgumentError, ActiveRecord::StatementInvalid nil end + + def database_connection + ActiveRecord::Base.connection + end + + def db_table_exists?(table) + database_connection.table_exists?(table) + end end end From 3205a654caf903002c2db872f802a3332201678b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 25 Jan 2024 15:34:26 +0100 Subject: [PATCH 25/45] Refactor conversations components in web UI (#28833) Co-authored-by: Claire --- .../components/conversation.jsx | 310 ++++++++++-------- .../components/conversations_list.jsx | 127 ++++--- .../containers/conversation_container.js | 80 ----- .../conversations_list_container.js | 16 - .../features/direct_timeline/index.jsx | 146 ++++----- 5 files changed, 296 insertions(+), 383 deletions(-) delete mode 100644 app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js delete mode 100644 app/javascript/mastodon/features/direct_timeline/containers/conversations_list_container.js diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx index 274cfa69f53db3..3af89f99745ff7 100644 --- a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx +++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx @@ -1,17 +1,24 @@ import PropTypes from 'prop-types'; +import { useCallback } from 'react'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; -import { Link, withRouter } from 'react-router-dom'; +import { Link, useHistory } from 'react-router-dom'; +import { createSelector } from '@reduxjs/toolkit'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; +import { useDispatch, useSelector } from 'react-redux'; + import { HotKeys } from 'react-hotkeys'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; +import { replyCompose } from 'mastodon/actions/compose'; +import { markConversationRead, deleteConversation } from 'mastodon/actions/conversations'; +import { openModal } from 'mastodon/actions/modal'; +import { muteStatus, unmuteStatus, revealStatus, hideStatus } from 'mastodon/actions/statuses'; import AttachmentList from 'mastodon/components/attachment_list'; import AvatarComposite from 'mastodon/components/avatar_composite'; import { IconButton } from 'mastodon/components/icon_button'; @@ -19,7 +26,7 @@ import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; import StatusContent from 'mastodon/components/status_content'; import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; import { autoPlayGif } from 'mastodon/initial_state'; -import { WithRouterPropTypes } from 'mastodon/utils/react_router'; +import { makeGetStatus } from 'mastodon/selectors'; const messages = defineMessages({ more: { id: 'status.more', defaultMessage: 'More' }, @@ -29,25 +36,31 @@ const messages = defineMessages({ delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' }, muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, + replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, + replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, }); -class Conversation extends ImmutablePureComponent { - - static propTypes = { - conversationId: PropTypes.string.isRequired, - accounts: ImmutablePropTypes.list.isRequired, - lastStatus: ImmutablePropTypes.map, - unread:PropTypes.bool.isRequired, - scrollKey: PropTypes.string, - onMoveUp: PropTypes.func, - onMoveDown: PropTypes.func, - markRead: PropTypes.func.isRequired, - delete: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - ...WithRouterPropTypes, - }; - - handleMouseEnter = ({ currentTarget }) => { +const getAccounts = createSelector( + (state) => state.get('accounts'), + (_, accountIds) => accountIds, + (accounts, accountIds) => + accountIds.map(id => accounts.get(id)) +); + +const getStatus = makeGetStatus(); + +export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown }) => { + const id = conversation.get('id'); + const unread = conversation.get('unread'); + const lastStatusId = conversation.get('last_status'); + const accountIds = conversation.get('accounts'); + const intl = useIntl(); + const dispatch = useDispatch(); + const history = useHistory(); + const lastStatus = useSelector(state => getStatus(state, { id: lastStatusId })); + const accounts = useSelector(state => getAccounts(state, accountIds)); + + const handleMouseEnter = useCallback(({ currentTarget }) => { if (autoPlayGif) { return; } @@ -58,9 +71,9 @@ class Conversation extends ImmutablePureComponent { let emoji = emojis[i]; emoji.src = emoji.getAttribute('data-original'); } - }; + }, []); - handleMouseLeave = ({ currentTarget }) => { + const handleMouseLeave = useCallback(({ currentTarget }) => { if (autoPlayGif) { return; } @@ -71,136 +84,161 @@ class Conversation extends ImmutablePureComponent { let emoji = emojis[i]; emoji.src = emoji.getAttribute('data-static'); } - }; - - handleClick = () => { - if (!this.props.history) { - return; - } - - const { lastStatus, unread, markRead } = this.props; + }, []); + const handleClick = useCallback(() => { if (unread) { - markRead(); + dispatch(markConversationRead(id)); } - this.props.history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`); - }; - - handleMarkAsRead = () => { - this.props.markRead(); - }; - - handleReply = () => { - this.props.reply(this.props.lastStatus, this.props.history); - }; - - handleDelete = () => { - this.props.delete(); - }; - - handleHotkeyMoveUp = () => { - this.props.onMoveUp(this.props.conversationId); - }; - - handleHotkeyMoveDown = () => { - this.props.onMoveDown(this.props.conversationId); - }; - - handleConversationMute = () => { - this.props.onMute(this.props.lastStatus); - }; - - handleShowMore = () => { - this.props.onToggleHidden(this.props.lastStatus); - }; - - render () { - const { accounts, lastStatus, unread, scrollKey, intl } = this.props; - - if (lastStatus === null) { - return null; + history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`); + }, [dispatch, history, unread, id, lastStatus]); + + const handleMarkAsRead = useCallback(() => { + dispatch(markConversationRead(id)); + }, [dispatch, id]); + + const handleReply = useCallback(() => { + dispatch((_, getState) => { + let state = getState(); + + if (state.getIn(['compose', 'text']).trim().length !== 0) { + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onConfirm: () => dispatch(replyCompose(lastStatus, history)), + }, + })); + } else { + dispatch(replyCompose(lastStatus, history)); + } + }); + }, [dispatch, lastStatus, history, intl]); + + const handleDelete = useCallback(() => { + dispatch(deleteConversation(id)); + }, [dispatch, id]); + + const handleHotkeyMoveUp = useCallback(() => { + onMoveUp(id); + }, [id, onMoveUp]); + + const handleHotkeyMoveDown = useCallback(() => { + onMoveDown(id); + }, [id, onMoveDown]); + + const handleConversationMute = useCallback(() => { + if (lastStatus.get('muted')) { + dispatch(unmuteStatus(lastStatus.get('id'))); + } else { + dispatch(muteStatus(lastStatus.get('id'))); } + }, [dispatch, lastStatus]); - const menu = [ - { text: intl.formatMessage(messages.open), action: this.handleClick }, - null, - ]; - - menu.push({ text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMute }); - - if (unread) { - menu.push({ text: intl.formatMessage(messages.markAsRead), action: this.handleMarkAsRead }); - menu.push(null); + const handleShowMore = useCallback(() => { + if (lastStatus.get('hidden')) { + dispatch(revealStatus(lastStatus.get('id'))); + } else { + dispatch(hideStatus(lastStatus.get('id'))); } + }, [dispatch, lastStatus]); + + if (!lastStatus) { + return null; + } - menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete }); + const menu = [ + { text: intl.formatMessage(messages.open), action: handleClick }, + null, + { text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: handleConversationMute }, + ]; - const names = accounts.map(a => ).reduce((prev, cur) => [prev, ', ', cur]); + if (unread) { + menu.push({ text: intl.formatMessage(messages.markAsRead), action: handleMarkAsRead }); + menu.push(null); + } - const handlers = { - reply: this.handleReply, - open: this.handleClick, - moveUp: this.handleHotkeyMoveUp, - moveDown: this.handleHotkeyMoveDown, - toggleHidden: this.handleShowMore, - }; + menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete }); + + const names = accounts.map(a => ( + + + + + + )).reduce((prev, cur) => [prev, ', ', cur]); + + const handlers = { + reply: handleReply, + open: handleClick, + moveUp: handleHotkeyMoveUp, + moveDown: handleHotkeyMoveDown, + toggleHidden: handleShowMore, + }; - return ( - -
-
- -
+ return ( + +
+
+ +
-
-
-
- {unread && } -
+
+
+
+ {unread && } +
-
- {names} }} /> -
+
+ {names} }} />
+
- + + {lastStatus.get('media_attachments').size > 0 && ( + - - {lastStatus.get('media_attachments').size > 0 && ( - + + +
+ - )} - -
- - -
- -
- - ); - } - -} - -export default withRouter(injectIntl(Conversation)); +
+ + ); +}; + +Conversation.propTypes = { + conversation: ImmutablePropTypes.map.isRequired, + scrollKey: PropTypes.string, + onMoveUp: PropTypes.func, + onMoveDown: PropTypes.func, +}; diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversations_list.jsx b/app/javascript/mastodon/features/direct_timeline/components/conversations_list.jsx index 8c12ea9e5f68a2..c9fc098a527ceb 100644 --- a/app/javascript/mastodon/features/direct_timeline/components/conversations_list.jsx +++ b/app/javascript/mastodon/features/direct_timeline/components/conversations_list.jsx @@ -1,77 +1,72 @@ import PropTypes from 'prop-types'; +import { useRef, useMemo, useCallback } from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; +import { useSelector, useDispatch } from 'react-redux'; import { debounce } from 'lodash'; -import ScrollableList from '../../../components/scrollable_list'; -import ConversationContainer from '../containers/conversation_container'; +import { expandConversations } from 'mastodon/actions/conversations'; +import ScrollableList from 'mastodon/components/scrollable_list'; -export default class ConversationsList extends ImmutablePureComponent { +import { Conversation } from './conversation'; - static propTypes = { - conversations: ImmutablePropTypes.list.isRequired, - scrollKey: PropTypes.string.isRequired, - hasMore: PropTypes.bool, - isLoading: PropTypes.bool, - onLoadMore: PropTypes.func, - }; +const focusChild = (node, index, alignTop) => { + const element = node.querySelector(`article:nth-of-type(${index + 1}) .focusable`); - getCurrentIndex = id => this.props.conversations.findIndex(x => x.get('id') === id); - - handleMoveUp = id => { - const elementIndex = this.getCurrentIndex(id) - 1; - this._selectChild(elementIndex, true); - }; - - handleMoveDown = id => { - const elementIndex = this.getCurrentIndex(id) + 1; - this._selectChild(elementIndex, false); - }; - - _selectChild (index, align_top) { - const container = this.node.node; - const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`); - - if (element) { - if (align_top && container.scrollTop > element.offsetTop) { - element.scrollIntoView(true); - } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) { - element.scrollIntoView(false); - } - element.focus(); - } - } - - setRef = c => { - this.node = c; - }; - - handleLoadOlder = debounce(() => { - const last = this.props.conversations.last(); - - if (last && last.get('last_status')) { - this.props.onLoadMore(last.get('last_status')); + if (element) { + if (alignTop && node.scrollTop > element.offsetTop) { + element.scrollIntoView(true); + } else if (!alignTop && node.scrollTop + node.clientHeight < element.offsetTop + element.offsetHeight) { + element.scrollIntoView(false); } - }, 300, { leading: true }); - render () { - const { conversations, isLoading, onLoadMore, ...other } = this.props; - - return ( - - {conversations.map(item => ( - - ))} - - ); + element.focus(); } - -} +}; + +export const ConversationsList = ({ scrollKey, ...other }) => { + const listRef = useRef(); + const conversations = useSelector(state => state.getIn(['conversations', 'items'])); + const isLoading = useSelector(state => state.getIn(['conversations', 'isLoading'], true)); + const hasMore = useSelector(state => state.getIn(['conversations', 'hasMore'], false)); + const dispatch = useDispatch(); + const lastStatusId = conversations.last()?.get('last_status'); + + const handleMoveUp = useCallback(id => { + const elementIndex = conversations.findIndex(x => x.get('id') === id) - 1; + focusChild(listRef.current.node, elementIndex, true); + }, [listRef, conversations]); + + const handleMoveDown = useCallback(id => { + const elementIndex = conversations.findIndex(x => x.get('id') === id) + 1; + focusChild(listRef.current.node, elementIndex, false); + }, [listRef, conversations]); + + const debouncedLoadMore = useMemo(() => debounce(id => { + dispatch(expandConversations({ maxId: id })); + }, 300, { leading: true }), [dispatch]); + + const handleLoadMore = useCallback(() => { + if (lastStatusId) { + debouncedLoadMore(lastStatusId); + } + }, [debouncedLoadMore, lastStatusId]); + + return ( + + {conversations.map(item => ( + + ))} + + ); +}; + +ConversationsList.propTypes = { + scrollKey: PropTypes.string.isRequired, +}; diff --git a/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js b/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js deleted file mode 100644 index 456fc7d7cc2a2c..00000000000000 --- a/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js +++ /dev/null @@ -1,80 +0,0 @@ -import { defineMessages, injectIntl } from 'react-intl'; - -import { connect } from 'react-redux'; - -import { replyCompose } from 'mastodon/actions/compose'; -import { markConversationRead, deleteConversation } from 'mastodon/actions/conversations'; -import { openModal } from 'mastodon/actions/modal'; -import { muteStatus, unmuteStatus, hideStatus, revealStatus } from 'mastodon/actions/statuses'; -import { makeGetStatus } from 'mastodon/selectors'; - -import Conversation from '../components/conversation'; - -const messages = defineMessages({ - replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, - replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, -}); - -const mapStateToProps = () => { - const getStatus = makeGetStatus(); - - return (state, { conversationId }) => { - const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId); - const lastStatusId = conversation.get('last_status', null); - - return { - accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)), - unread: conversation.get('unread'), - lastStatus: lastStatusId && getStatus(state, { id: lastStatusId }), - }; - }; -}; - -const mapDispatchToProps = (dispatch, { intl, conversationId }) => ({ - - markRead () { - dispatch(markConversationRead(conversationId)); - }, - - reply (status, router) { - dispatch((_, getState) => { - let state = getState(); - - if (state.getIn(['compose', 'text']).trim().length !== 0) { - dispatch(openModal({ - modalType: 'CONFIRM', - modalProps: { - message: intl.formatMessage(messages.replyMessage), - confirm: intl.formatMessage(messages.replyConfirm), - onConfirm: () => dispatch(replyCompose(status, router)), - }, - })); - } else { - dispatch(replyCompose(status, router)); - } - }); - }, - - delete () { - dispatch(deleteConversation(conversationId)); - }, - - onMute (status) { - if (status.get('muted')) { - dispatch(unmuteStatus(status.get('id'))); - } else { - dispatch(muteStatus(status.get('id'))); - } - }, - - onToggleHidden (status) { - if (status.get('hidden')) { - dispatch(revealStatus(status.get('id'))); - } else { - dispatch(hideStatus(status.get('id'))); - } - }, - -}); - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Conversation)); diff --git a/app/javascript/mastodon/features/direct_timeline/containers/conversations_list_container.js b/app/javascript/mastodon/features/direct_timeline/containers/conversations_list_container.js deleted file mode 100644 index 1dcd3ec1bd4ad3..00000000000000 --- a/app/javascript/mastodon/features/direct_timeline/containers/conversations_list_container.js +++ /dev/null @@ -1,16 +0,0 @@ -import { connect } from 'react-redux'; - -import { expandConversations } from '../../../actions/conversations'; -import ConversationsList from '../components/conversations_list'; - -const mapStateToProps = state => ({ - conversations: state.getIn(['conversations', 'items']), - isLoading: state.getIn(['conversations', 'isLoading'], true), - hasMore: state.getIn(['conversations', 'hasMore'], false), -}); - -const mapDispatchToProps = dispatch => ({ - onLoadMore: maxId => dispatch(expandConversations({ maxId })), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ConversationsList); diff --git a/app/javascript/mastodon/features/direct_timeline/index.jsx b/app/javascript/mastodon/features/direct_timeline/index.jsx index af29d7a5b83257..7aee83ec10e21e 100644 --- a/app/javascript/mastodon/features/direct_timeline/index.jsx +++ b/app/javascript/mastodon/features/direct_timeline/index.jsx @@ -1,11 +1,11 @@ import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; +import { useRef, useCallback, useEffect } from 'react'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { Helmet } from 'react-helmet'; -import { connect } from 'react-redux'; +import { useDispatch } from 'react-redux'; import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns'; @@ -14,103 +14,79 @@ import { connectDirectStream } from 'mastodon/actions/streaming'; import Column from 'mastodon/components/column'; import ColumnHeader from 'mastodon/components/column_header'; -import ConversationsListContainer from './containers/conversations_list_container'; +import { ConversationsList } from './components/conversations_list'; const messages = defineMessages({ title: { id: 'column.direct', defaultMessage: 'Private mentions' }, }); -class DirectTimeline extends PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - columnId: PropTypes.string, - intl: PropTypes.object.isRequired, - hasUnread: PropTypes.bool, - multiColumn: PropTypes.bool, - }; - - handlePin = () => { - const { columnId, dispatch } = this.props; +const DirectTimeline = ({ columnId, multiColumn }) => { + const columnRef = useRef(); + const intl = useIntl(); + const dispatch = useDispatch(); + const pinned = !!columnId; + const handlePin = useCallback(() => { if (columnId) { dispatch(removeColumn(columnId)); } else { dispatch(addColumn('DIRECT', {})); } - }; + }, [dispatch, columnId]); - handleMove = (dir) => { - const { columnId, dispatch } = this.props; + const handleMove = useCallback((dir) => { dispatch(moveColumn(columnId, dir)); - }; - - handleHeaderClick = () => { - this.column.scrollTop(); - }; + }, [dispatch, columnId]); - componentDidMount () { - const { dispatch } = this.props; + const handleHeaderClick = useCallback(() => { + columnRef.current.scrollTop(); + }, [columnRef]); + useEffect(() => { dispatch(mountConversations()); dispatch(expandConversations()); - this.disconnect = dispatch(connectDirectStream()); - } - componentWillUnmount () { - this.props.dispatch(unmountConversations()); - - if (this.disconnect) { - this.disconnect(); - this.disconnect = null; - } - } - - setRef = c => { - this.column = c; - }; - - handleLoadMore = maxId => { - this.props.dispatch(expandConversations({ maxId })); - }; - - render () { - const { intl, hasUnread, columnId, multiColumn } = this.props; - const pinned = !!columnId; - - return ( - - - -
} - alwaysPrepend - emptyMessage={} - /> - - - {intl.formatMessage(messages.title)} - - - - ); - } - -} - -export default connect()(injectIntl(DirectTimeline)); + const disconnect = dispatch(connectDirectStream()); + + return () => { + dispatch(unmountConversations()); + disconnect(); + }; + }, [dispatch]); + + return ( + + + + } + bindToDocument={!multiColumn} + prepend={
} + alwaysPrepend + /> + + + {intl.formatMessage(messages.title)} + + +
+ ); +}; + +DirectTimeline.propTypes = { + columnId: PropTypes.string, + multiColumn: PropTypes.bool, +}; + +export default DirectTimeline; From 17ea22671de0705ec805bb157754d5ae5f24f9e3 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 25 Jan 2024 10:13:41 -0500 Subject: [PATCH 26/45] Fix `Style/GuardClause` cop in app/controllers (#28420) --- .rubocop_todo.yml | 4 ---- .../admin/confirmations_controller.rb | 14 +++++++------ .../auth/confirmations_controller.rb | 12 ++++++----- app/controllers/auth/passwords_controller.rb | 10 ++++------ .../webauthn_credentials_controller.rb | 20 ++++++++----------- 5 files changed, 27 insertions(+), 33 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index fd9dc18ac7b5ef..c8165c1edf6483 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -121,10 +121,6 @@ Style/GlobalStdStream: # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. Style/GuardClause: Exclude: - - 'app/controllers/admin/confirmations_controller.rb' - - 'app/controllers/auth/confirmations_controller.rb' - - 'app/controllers/auth/passwords_controller.rb' - - 'app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb' - 'app/lib/activitypub/activity/block.rb' - 'app/lib/request.rb' - 'app/lib/request_pool.rb' diff --git a/app/controllers/admin/confirmations_controller.rb b/app/controllers/admin/confirmations_controller.rb index 7ccf5c9012de7c..702550eecc1f9f 100644 --- a/app/controllers/admin/confirmations_controller.rb +++ b/app/controllers/admin/confirmations_controller.rb @@ -3,7 +3,7 @@ module Admin class ConfirmationsController < BaseController before_action :set_user - before_action :check_confirmation, only: [:resend] + before_action :redirect_confirmed_user, only: [:resend], if: :user_confirmed? def create authorize @user, :confirm? @@ -25,11 +25,13 @@ def resend private - def check_confirmation - if @user.confirmed? - flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed') - redirect_to admin_accounts_path - end + def redirect_confirmed_user + flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed') + redirect_to admin_accounts_path + end + + def user_confirmed? + @user.confirmed? end end end diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb index d9cd630905b418..7ca7be5f8ef8ec 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -7,7 +7,7 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController before_action :set_body_classes before_action :set_confirmation_user!, only: [:show, :confirm_captcha] - before_action :require_unconfirmed! + before_action :redirect_confirmed_user, if: :signed_in_confirmed_user? before_action :extend_csp_for_captcha!, only: [:show, :confirm_captcha] before_action :require_captcha_if_needed!, only: [:show] @@ -65,10 +65,12 @@ def captcha_user_bypass? @confirmation_user.nil? || @confirmation_user.confirmed? end - def require_unconfirmed! - if user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank? - redirect_to(current_user.approved? ? root_path : edit_user_registration_path) - end + def redirect_confirmed_user + redirect_to(current_user.approved? ? root_path : edit_user_registration_path) + end + + def signed_in_confirmed_user? + user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank? end def set_body_classes diff --git a/app/controllers/auth/passwords_controller.rb b/app/controllers/auth/passwords_controller.rb index a752194d5b52e2..de001f062b04d7 100644 --- a/app/controllers/auth/passwords_controller.rb +++ b/app/controllers/auth/passwords_controller.rb @@ -2,7 +2,7 @@ class Auth::PasswordsController < Devise::PasswordsController skip_before_action :check_self_destruct! - before_action :check_validity_of_reset_password_token, only: :edit + before_action :redirect_invalid_reset_token, only: :edit, unless: :reset_password_token_is_valid? before_action :set_body_classes layout 'auth' @@ -19,11 +19,9 @@ def update private - def check_validity_of_reset_password_token - unless reset_password_token_is_valid? - flash[:error] = I18n.t('auth.invalid_reset_password_token') - redirect_to new_password_path(resource_name) - end + def redirect_invalid_reset_token + flash[:error] = I18n.t('auth.invalid_reset_password_token') + redirect_to new_password_path(resource_name) end def set_body_classes diff --git a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb index c86ede4f3adf64..9714d54f954ffe 100644 --- a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb +++ b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb @@ -6,8 +6,8 @@ class WebauthnCredentialsController < BaseController skip_before_action :check_self_destruct! skip_before_action :require_functional! - before_action :require_otp_enabled - before_action :require_webauthn_enabled, only: [:index, :destroy] + before_action :redirect_invalid_otp, unless: -> { current_user.otp_enabled? } + before_action :redirect_invalid_webauthn, only: [:index, :destroy], unless: -> { current_user.webauthn_enabled? } def index; end def new; end @@ -85,18 +85,14 @@ def destroy private - def require_otp_enabled - unless current_user.otp_enabled? - flash[:error] = t('webauthn_credentials.otp_required') - redirect_to settings_two_factor_authentication_methods_path - end + def redirect_invalid_otp + flash[:error] = t('webauthn_credentials.otp_required') + redirect_to settings_two_factor_authentication_methods_path end - def require_webauthn_enabled - unless current_user.webauthn_enabled? - flash[:error] = t('webauthn_credentials.not_enabled') - redirect_to settings_two_factor_authentication_methods_path - end + def redirect_invalid_webauthn + flash[:error] = t('webauthn_credentials.not_enabled') + redirect_to settings_two_factor_authentication_methods_path end end end From 0b38946c874f4e02295a303514aaf8895cf8a918 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 25 Jan 2024 10:18:15 -0500 Subject: [PATCH 27/45] Update paperclip and climate_control gems (#28379) --- Gemfile | 2 +- Gemfile.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 2d4e504ac7d438..4951304e39e244 100644 --- a/Gemfile +++ b/Gemfile @@ -123,7 +123,7 @@ group :test do gem 'database_cleaner-active_record' # Used to mock environment variables - gem 'climate_control', '~> 0.2' + gem 'climate_control' # Generating fake data for specs gem 'faker', '~> 3.2' diff --git a/Gemfile.lock b/Gemfile.lock index a31d0a929c0e55..57b25807222386 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -185,7 +185,7 @@ GEM elasticsearch (>= 7.12.0, < 7.14.0) elasticsearch-dsl chunky_png (1.4.0) - climate_control (0.2.0) + climate_control (1.2.0) cocoon (1.2.15) color_diff (0.1) concurrent-ruby (1.2.3) @@ -746,8 +746,8 @@ GEM temple (0.10.3) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) - terrapin (0.6.0) - climate_control (>= 0.0.3, < 1.0) + terrapin (1.0.1) + climate_control test-prof (1.3.1) thor (1.3.0) tilt (2.3.0) @@ -836,7 +836,7 @@ DEPENDENCIES capybara (~> 3.39) charlock_holmes (~> 0.7.7) chewy (~> 7.3) - climate_control (~> 0.2) + climate_control cocoon (~> 1.2) color_diff (~> 0.1) concurrent-ruby From 4cdf62e576488e8c41f79c2a04a1630df9685592 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 25 Jan 2024 10:26:51 -0500 Subject: [PATCH 28/45] Extract `rebuild_index` method in maintenance CLI (#28911) --- lib/mastodon/cli/maintenance.rb | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/mastodon/cli/maintenance.rb b/lib/mastodon/cli/maintenance.rb index c644729b5e9154..a64206065d518a 100644 --- a/lib/mastodon/cli/maintenance.rb +++ b/lib/mastodon/cli/maintenance.rb @@ -244,10 +244,10 @@ def deduplicate_accounts! end say 'Reindexing textual indexes on accounts…' - database_connection.execute('REINDEX INDEX search_index;') - database_connection.execute('REINDEX INDEX index_accounts_on_uri;') - database_connection.execute('REINDEX INDEX index_accounts_on_url;') - database_connection.execute('REINDEX INDEX index_accounts_on_domain_and_id;') if migrator_version >= 2023_05_24_190515 + rebuild_index(:search_index) + rebuild_index(:index_accounts_on_uri) + rebuild_index(:index_accounts_on_url) + rebuild_index(:index_accounts_on_domain_and_id) if migrator_version >= 2023_05_24_190515 end def deduplicate_users! @@ -274,7 +274,7 @@ def deduplicate_users! database_connection.add_index :users, ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true, where: 'reset_password_token IS NOT NULL', opclass: :text_pattern_ops end - database_connection.execute('REINDEX INDEX index_users_on_unconfirmed_email;') if migrator_version >= 2023_07_02_151753 + rebuild_index(:index_users_on_unconfirmed_email) if migrator_version >= 2023_07_02_151753 end def deduplicate_users_process_email @@ -735,5 +735,9 @@ def database_connection def db_table_exists?(table) database_connection.table_exists?(table) end + + def rebuild_index(name) + database_connection.execute("REINDEX INDEX #{name}") + end end end From 42ab855b2339c5cea3229c856ab539f883736b12 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 25 Jan 2024 10:28:27 -0500 Subject: [PATCH 29/45] Add specs for `Instance` model scopes and add `with_domain_follows` scope (#28767) --- .../admin/export_domain_blocks_controller.rb | 6 +- app/models/instance.rb | 14 +++ spec/models/instance_spec.rb | 104 ++++++++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 spec/models/instance_spec.rb diff --git a/app/controllers/admin/export_domain_blocks_controller.rb b/app/controllers/admin/export_domain_blocks_controller.rb index ffc4478172634d..9caafd9684fef6 100644 --- a/app/controllers/admin/export_domain_blocks_controller.rb +++ b/app/controllers/admin/export_domain_blocks_controller.rb @@ -49,7 +49,7 @@ def import next end - @warning_domains = Instance.where(domain: @domain_blocks.map(&:domain)).where('EXISTS (SELECT 1 FROM follows JOIN accounts ON follows.account_id = accounts.id OR follows.target_account_id = accounts.id WHERE accounts.domain = instances.domain)').pluck(:domain) + @warning_domains = instances_from_imported_blocks.pluck(:domain) rescue ActionController::ParameterMissing flash.now[:alert] = I18n.t('admin.export_domain_blocks.no_file') set_dummy_import! @@ -58,6 +58,10 @@ def import private + def instances_from_imported_blocks + Instance.with_domain_follows(@domain_blocks.map(&:domain)) + end + def export_filename 'domain_blocks.csv' end diff --git a/app/models/instance.rb b/app/models/instance.rb index 2dec75d6feb549..0fd31c8097cab6 100644 --- a/app/models/instance.rb +++ b/app/models/instance.rb @@ -25,11 +25,25 @@ class Instance < ApplicationRecord scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } scope :domain_starts_with, ->(value) { where(arel_table[:domain].matches("#{sanitize_sql_like(value)}%", false, true)) } scope :by_domain_and_subdomains, ->(domain) { where("reverse('.' || domain) LIKE reverse(?)", "%.#{domain}") } + scope :with_domain_follows, ->(domains) { where(domain: domains).where(domain_account_follows) } def self.refresh Scenic.database.refresh_materialized_view(table_name, concurrently: true, cascade: false) end + def self.domain_account_follows + Arel.sql( + <<~SQL.squish + EXISTS ( + SELECT 1 + FROM follows + JOIN accounts ON follows.account_id = accounts.id OR follows.target_account_id = accounts.id + WHERE accounts.domain = instances.domain + ) + SQL + ) + end + def readonly? true end diff --git a/spec/models/instance_spec.rb b/spec/models/instance_spec.rb new file mode 100644 index 00000000000000..3e811d3325c5c8 --- /dev/null +++ b/spec/models/instance_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Instance do + describe 'Scopes' do + before { described_class.refresh } + + describe '#searchable' do + let(:expected_domain) { 'host.example' } + let(:blocked_domain) { 'other.example' } + + before do + Fabricate :account, domain: expected_domain + Fabricate :account, domain: blocked_domain + Fabricate :domain_block, domain: blocked_domain + end + + it 'returns records not domain blocked' do + results = described_class.searchable.pluck(:domain) + + expect(results) + .to include(expected_domain) + .and not_include(blocked_domain) + end + end + + describe '#matches_domain' do + let(:host_domain) { 'host.example.com' } + let(:host_under_domain) { 'host_under.example.com' } + let(:other_domain) { 'other.example' } + + before do + Fabricate :account, domain: host_domain + Fabricate :account, domain: host_under_domain + Fabricate :account, domain: other_domain + end + + it 'returns matching records' do + expect(described_class.matches_domain('host.exa').pluck(:domain)) + .to include(host_domain) + .and not_include(other_domain) + + expect(described_class.matches_domain('ple.com').pluck(:domain)) + .to include(host_domain) + .and not_include(other_domain) + + expect(described_class.matches_domain('example').pluck(:domain)) + .to include(host_domain) + .and include(other_domain) + + expect(described_class.matches_domain('host_').pluck(:domain)) # Preserve SQL wildcards + .to include(host_domain) + .and include(host_under_domain) + .and not_include(other_domain) + end + end + + describe '#by_domain_and_subdomains' do + let(:exact_match_domain) { 'example.com' } + let(:subdomain_domain) { 'foo.example.com' } + let(:partial_domain) { 'grexample.com' } + + before do + Fabricate(:account, domain: exact_match_domain) + Fabricate(:account, domain: subdomain_domain) + Fabricate(:account, domain: partial_domain) + end + + it 'returns matching instances' do + results = described_class.by_domain_and_subdomains('example.com').pluck(:domain) + + expect(results) + .to include(exact_match_domain) + .and include(subdomain_domain) + .and not_include(partial_domain) + end + end + + describe '#with_domain_follows' do + let(:example_domain) { 'example.host' } + let(:other_domain) { 'other.host' } + let(:none_domain) { 'none.host' } + + before do + example_account = Fabricate(:account, domain: example_domain) + other_account = Fabricate(:account, domain: other_domain) + Fabricate(:account, domain: none_domain) + + Fabricate :follow, account: example_account + Fabricate :follow, target_account: other_account + end + + it 'returns instances with domain accounts that have follows' do + results = described_class.with_domain_follows(['example.host', 'other.host', 'none.host']).pluck(:domain) + + expect(results) + .to include(example_domain) + .and include(other_domain) + .and not_include(none_domain) + end + end + end +end From e5f50478b517444d46776c5f5f81042c80fdf9b7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 24 Jan 2024 11:49:19 +0100 Subject: [PATCH 30/45] [Glitch] Add confirmation when redirecting logged-out requests to permalink Port SCSS changes from b19ae521b7d28a76e8e1d8da8157e051e9d8de6c to glitch-soc Co-authored-by: Claire Signed-off-by: Claire --- .../flavours/glitch/styles/containers.scss | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/app/javascript/flavours/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss index 4d3d4c546c1ffe..6d72e43924a650 100644 --- a/app/javascript/flavours/glitch/styles/containers.scss +++ b/app/javascript/flavours/glitch/styles/containers.scss @@ -107,3 +107,59 @@ margin-inline-start: 10px; } } + +.redirect { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + font-size: 14px; + line-height: 18px; + + &__logo { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 30px; + + img { + height: 48px; + } + } + + &__message { + text-align: center; + + h1 { + font-size: 17px; + line-height: 22px; + font-weight: 700; + margin-bottom: 30px; + } + + p { + margin-bottom: 30px; + + &:last-child { + margin-bottom: 0; + } + } + + a { + color: $highlight-text-color; + font-weight: 500; + text-decoration: none; + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } + } + + &__link { + margin-top: 15px; + } +} From 54ece5040d50c155c4ebe4f732caf3b29546ad7f Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 24 Jan 2024 13:37:43 +0100 Subject: [PATCH 31/45] [Glitch] Use active variants for boost icons and increase icon size Port 5a838ceaa9a003bc2e2fdee727d4aa87cd53de4f to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/components/status_action_bar.jsx | 4 +++- .../flavours/glitch/features/status/components/action_bar.jsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/javascript/flavours/glitch/components/status_action_bar.jsx b/app/javascript/flavours/glitch/components/status_action_bar.jsx index 5727f36c86be6f..c10122d234d824 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.jsx +++ b/app/javascript/flavours/glitch/components/status_action_bar.jsx @@ -17,8 +17,10 @@ import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react'; import StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; import StarBorderIcon from '@/material-icons/400-24px/star.svg?react'; import VisibilityIcon from '@/material-icons/400-24px/visibility.svg?react'; +import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react'; import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg'; import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg'; +import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; @@ -303,7 +305,7 @@ class StatusActionBar extends ImmutablePureComponent { if (status.get('reblogged')) { reblogTitle = intl.formatMessage(messages.cancel_reblog_private); - reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon; + reblogIconComponent = publicStatus ? RepeatActiveIcon : RepeatPrivateActiveIcon; } else if (publicStatus) { reblogTitle = intl.formatMessage(messages.reblog); reblogIconComponent = RepeatIcon; diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx index 9edd47f5b0c1b4..1d8707ccbf0361 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx @@ -16,8 +16,10 @@ import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react'; import StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; import StarBorderIcon from '@/material-icons/400-24px/star.svg?react'; +import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react'; import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg'; import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg'; +import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; @@ -235,7 +237,7 @@ class ActionBar extends PureComponent { if (status.get('reblogged')) { reblogTitle = intl.formatMessage(messages.cancel_reblog_private); - reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon; + reblogIconComponent = publicStatus ? RepeatActiveIcon : RepeatPrivateActiveIcon; } else if (publicStatus) { reblogTitle = intl.formatMessage(messages.reblog); reblogIconComponent = RepeatIcon; From dd7a66949aca9e2d8b793664f2f19d3681e9178f Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 26 Jan 2024 21:04:02 +0100 Subject: [PATCH 32/45] Fix CSS loading in redirect controller --- app/controllers/redirect/base_controller.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controllers/redirect/base_controller.rb b/app/controllers/redirect/base_controller.rb index 90894ec1ed832c..ce55cfc53a72dc 100644 --- a/app/controllers/redirect/base_controller.rb +++ b/app/controllers/redirect/base_controller.rb @@ -3,6 +3,7 @@ class Redirect::BaseController < ApplicationController vary_by 'Accept-Language' + before_action :set_pack before_action :set_resource before_action :set_app_body_class @@ -21,4 +22,8 @@ def set_app_body_class def set_resource raise NotImplementedError end + + def set_pack + use_pack 'public' + end end From 80308d384a9f914c6500961f3e0fa5b4444fd30d Mon Sep 17 00:00:00 2001 From: Claire Date: Sun, 28 Jan 2024 14:53:08 +0100 Subject: [PATCH 33/45] [Glitch] Refactor conversations components in web UI (#2589) Port 3205a654caf903002c2db872f802a3332201678b to glitch-soc Signed-off-by: Claire Co-authored-by: Eugen Rochko --- .../components/conversation.jsx | 333 ++++++++++-------- .../components/conversations_list.jsx | 127 ++++--- .../containers/conversation_container.js | 81 ----- .../conversations_list_container.js | 16 - .../glitch/features/direct_timeline/index.jsx | 186 ++++------ 5 files changed, 313 insertions(+), 430 deletions(-) delete mode 100644 app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js delete mode 100644 app/javascript/flavours/glitch/features/direct_timeline/containers/conversations_list_container.js diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx index 00fbc8d46470bf..6ccd1497a7cd13 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx +++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx @@ -1,17 +1,24 @@ import PropTypes from 'prop-types'; +import { useCallback, useState } from 'react'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; -import { withRouter } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; +import { createSelector } from '@reduxjs/toolkit'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; +import { useDispatch, useSelector } from 'react-redux'; + import { HotKeys } from 'react-hotkeys'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; +import { replyCompose } from 'flavours/glitch/actions/compose'; +import { markConversationRead, deleteConversation } from 'flavours/glitch/actions/conversations'; +import { openModal } from 'flavours/glitch/actions/modal'; +import { muteStatus, unmuteStatus, revealStatus, hideStatus } from 'flavours/glitch/actions/statuses'; import AttachmentList from 'flavours/glitch/components/attachment_list'; import AvatarComposite from 'flavours/glitch/components/avatar_composite'; import { IconButton } from 'flavours/glitch/components/icon_button'; @@ -20,7 +27,7 @@ import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp import StatusContent from 'flavours/glitch/components/status_content'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import { autoPlayGif } from 'flavours/glitch/initial_state'; -import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; +import { makeGetStatus } from 'flavours/glitch/selectors'; const messages = defineMessages({ more: { id: 'status.more', defaultMessage: 'More' }, @@ -30,45 +37,48 @@ const messages = defineMessages({ delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' }, muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, + replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, + replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, }); -class Conversation extends ImmutablePureComponent { - - static propTypes = { - conversationId: PropTypes.string.isRequired, - accounts: ImmutablePropTypes.list.isRequired, - lastStatus: ImmutablePropTypes.map, - unread:PropTypes.bool.isRequired, - scrollKey: PropTypes.string, - onMoveUp: PropTypes.func, - onMoveDown: PropTypes.func, - markRead: PropTypes.func.isRequired, - delete: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - ...WithRouterPropTypes, - }; - - state = { - isExpanded: undefined, - }; - - parseClick = (e, destination) => { - const { history, lastStatus, unread, markRead } = this.props; - if (!history) return; - +const getAccounts = createSelector( + (state) => state.get('accounts'), + (_, accountIds) => accountIds, + (accounts, accountIds) => + accountIds.map(id => accounts.get(id)) +); + +const getStatus = makeGetStatus(); + +export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown }) => { + const id = conversation.get('id'); + const unread = conversation.get('unread'); + const lastStatusId = conversation.get('last_status'); + const accountIds = conversation.get('accounts'); + const intl = useIntl(); + const dispatch = useDispatch(); + const history = useHistory(); + const lastStatus = useSelector(state => getStatus(state, { id: lastStatusId })); + const accounts = useSelector(state => getAccounts(state, accountIds)); + + // glitch-soc additions + const sharedCWState = useSelector(state => state.getIn(['state', 'content_warnings', 'shared_state'])); + const [expanded, setExpanded] = useState(undefined); + + const parseClick = useCallback((e, destination) => { if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) { if (destination === undefined) { if (unread) { - markRead(); + dispatch(markConversationRead(id)); } destination = `/statuses/${lastStatus.get('id')}`; } history.push(destination); e.preventDefault(); } - }; + }, [dispatch, history, unread, id, lastStatus]); - handleMouseEnter = ({ currentTarget }) => { + const handleMouseEnter = useCallback(({ currentTarget }) => { if (autoPlayGif) { return; } @@ -79,9 +89,9 @@ class Conversation extends ImmutablePureComponent { let emoji = emojis[i]; emoji.src = emoji.getAttribute('data-original'); } - }; + }, []); - handleMouseLeave = ({ currentTarget }) => { + const handleMouseLeave = useCallback(({ currentTarget }) => { if (autoPlayGif) { return; } @@ -92,145 +102,160 @@ class Conversation extends ImmutablePureComponent { let emoji = emojis[i]; emoji.src = emoji.getAttribute('data-static'); } - }; - - handleClick = () => { - if (!this.props.history) { - return; - } - - const { lastStatus, unread, markRead } = this.props; + }, []); + const handleClick = useCallback(() => { if (unread) { - markRead(); + dispatch(markConversationRead(id)); } - this.props.history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`); - }; - - handleMarkAsRead = () => { - this.props.markRead(); - }; - - handleReply = () => { - this.props.reply(this.props.lastStatus, this.props.history); - }; - - handleDelete = () => { - this.props.delete(); - }; - - handleHotkeyMoveUp = () => { - this.props.onMoveUp(this.props.conversationId); - }; - - handleHotkeyMoveDown = () => { - this.props.onMoveDown(this.props.conversationId); - }; - - handleConversationMute = () => { - this.props.onMute(this.props.lastStatus); - }; - - handleShowMore = () => { - this.props.onToggleHidden(this.props.lastStatus); - - if (this.props.lastStatus.get('spoiler_text')) { - this.setExpansion(!this.state.isExpanded); + history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`); + }, [dispatch, history, unread, id, lastStatus]); + + const handleMarkAsRead = useCallback(() => { + dispatch(markConversationRead(id)); + }, [dispatch, id]); + + const handleReply = useCallback(() => { + dispatch((_, getState) => { + let state = getState(); + + if (state.getIn(['compose', 'text']).trim().length !== 0) { + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onConfirm: () => dispatch(replyCompose(lastStatus, history)), + }, + })); + } else { + dispatch(replyCompose(lastStatus, history)); + } + }); + }, [dispatch, lastStatus, history, intl]); + + const handleDelete = useCallback(() => { + dispatch(deleteConversation(id)); + }, [dispatch, id]); + + const handleHotkeyMoveUp = useCallback(() => { + onMoveUp(id); + }, [id, onMoveUp]); + + const handleHotkeyMoveDown = useCallback(() => { + onMoveDown(id); + }, [id, onMoveDown]); + + const handleConversationMute = useCallback(() => { + if (lastStatus.get('muted')) { + dispatch(unmuteStatus(lastStatus.get('id'))); + } else { + dispatch(muteStatus(lastStatus.get('id'))); } - }; + }, [dispatch, lastStatus]); - setExpansion = value => { - this.setState({ isExpanded: value }); - }; - - render () { - const { accounts, lastStatus, unread, scrollKey, intl } = this.props; - - if (lastStatus === null) { - return null; + const handleShowMore = useCallback(() => { + if (lastStatus.get('hidden')) { + dispatch(revealStatus(lastStatus.get('id'))); + } else { + dispatch(hideStatus(lastStatus.get('id'))); } - const isExpanded = this.props.settings.getIn(['content_warnings', 'shared_state']) ? !lastStatus.get('hidden') : this.state.isExpanded; - - const menu = [ - { text: intl.formatMessage(messages.open), action: this.handleClick }, - null, - ]; - - menu.push({ text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMute }); - - if (unread) { - menu.push({ text: intl.formatMessage(messages.markAsRead), action: this.handleMarkAsRead }); - menu.push(null); + if (lastStatus.get('spoiler_text')) { + setExpanded(!expanded); } + }, [dispatch, lastStatus, expanded]); - menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete }); + const menu = [ + { text: intl.formatMessage(messages.open), action: handleClick }, + null, + { text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: handleConversationMute }, + ]; - const names = accounts.map(a => ).reduce((prev, cur) => [prev, ', ', cur]); + if (unread) { + menu.push({ text: intl.formatMessage(messages.markAsRead), action: handleMarkAsRead }); + menu.push(null); + } - const handlers = { - reply: this.handleReply, - open: this.handleClick, - moveUp: this.handleHotkeyMoveUp, - moveDown: this.handleHotkeyMoveDown, - toggleHidden: this.handleShowMore, - }; + menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete }); + + const names = accounts.map(a => ( + + + + + + )).reduce((prev, cur) => [prev, ', ', cur]); + + const handlers = { + reply: handleReply, + open: handleClick, + moveUp: handleHotkeyMoveUp, + moveDown: handleHotkeyMoveDown, + toggleHidden: handleShowMore, + }; - let media = null; - if (lastStatus.get('media_attachments').size > 0) { - media = ; - } + let media = null; + if (lastStatus.get('media_attachments').size > 0) { + media = ; + } - return ( - -
-
- -
+ return ( + +
+
+ +
-
-
-
- {unread && } -
+
+
+
+ {unread && } +
-
- {names} }} /> -
+
+ {names} }} />
+
- - -
- - -
- -
+ + +
+ + +
+
- - ); - } - -} - -export default withRouter(injectIntl(Conversation)); +
+ + ); +}; + +Conversation.propTypes = { + conversation: ImmutablePropTypes.map.isRequired, + scrollKey: PropTypes.string, + onMoveUp: PropTypes.func, + onMoveDown: PropTypes.func, +}; diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversations_list.jsx b/app/javascript/flavours/glitch/features/direct_timeline/components/conversations_list.jsx index 8c12ea9e5f68a2..b1a8fd09b6471b 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversations_list.jsx +++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversations_list.jsx @@ -1,77 +1,72 @@ import PropTypes from 'prop-types'; +import { useRef, useMemo, useCallback } from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; +import { useSelector, useDispatch } from 'react-redux'; import { debounce } from 'lodash'; -import ScrollableList from '../../../components/scrollable_list'; -import ConversationContainer from '../containers/conversation_container'; +import { expandConversations } from 'flavours/glitch/actions/conversations'; +import ScrollableList from 'flavours/glitch/components/scrollable_list'; -export default class ConversationsList extends ImmutablePureComponent { +import { Conversation } from './conversation'; - static propTypes = { - conversations: ImmutablePropTypes.list.isRequired, - scrollKey: PropTypes.string.isRequired, - hasMore: PropTypes.bool, - isLoading: PropTypes.bool, - onLoadMore: PropTypes.func, - }; +const focusChild = (node, index, alignTop) => { + const element = node.querySelector(`article:nth-of-type(${index + 1}) .focusable`); - getCurrentIndex = id => this.props.conversations.findIndex(x => x.get('id') === id); - - handleMoveUp = id => { - const elementIndex = this.getCurrentIndex(id) - 1; - this._selectChild(elementIndex, true); - }; - - handleMoveDown = id => { - const elementIndex = this.getCurrentIndex(id) + 1; - this._selectChild(elementIndex, false); - }; - - _selectChild (index, align_top) { - const container = this.node.node; - const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`); - - if (element) { - if (align_top && container.scrollTop > element.offsetTop) { - element.scrollIntoView(true); - } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) { - element.scrollIntoView(false); - } - element.focus(); - } - } - - setRef = c => { - this.node = c; - }; - - handleLoadOlder = debounce(() => { - const last = this.props.conversations.last(); - - if (last && last.get('last_status')) { - this.props.onLoadMore(last.get('last_status')); + if (element) { + if (alignTop && node.scrollTop > element.offsetTop) { + element.scrollIntoView(true); + } else if (!alignTop && node.scrollTop + node.clientHeight < element.offsetTop + element.offsetHeight) { + element.scrollIntoView(false); } - }, 300, { leading: true }); - render () { - const { conversations, isLoading, onLoadMore, ...other } = this.props; - - return ( - - {conversations.map(item => ( - - ))} - - ); + element.focus(); } - -} +}; + +export const ConversationsList = ({ scrollKey, ...other }) => { + const listRef = useRef(); + const conversations = useSelector(state => state.getIn(['conversations', 'items'])); + const isLoading = useSelector(state => state.getIn(['conversations', 'isLoading'], true)); + const hasMore = useSelector(state => state.getIn(['conversations', 'hasMore'], false)); + const dispatch = useDispatch(); + const lastStatusId = conversations.last()?.get('last_status'); + + const handleMoveUp = useCallback(id => { + const elementIndex = conversations.findIndex(x => x.get('id') === id) - 1; + focusChild(listRef.current.node, elementIndex, true); + }, [listRef, conversations]); + + const handleMoveDown = useCallback(id => { + const elementIndex = conversations.findIndex(x => x.get('id') === id) + 1; + focusChild(listRef.current.node, elementIndex, false); + }, [listRef, conversations]); + + const debouncedLoadMore = useMemo(() => debounce(id => { + dispatch(expandConversations({ maxId: id })); + }, 300, { leading: true }), [dispatch]); + + const handleLoadMore = useCallback(() => { + if (lastStatusId) { + debouncedLoadMore(lastStatusId); + } + }, [debouncedLoadMore, lastStatusId]); + + return ( + + {conversations.map(item => ( + + ))} + + ); +}; + +ConversationsList.propTypes = { + scrollKey: PropTypes.string.isRequired, +}; diff --git a/app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js b/app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js deleted file mode 100644 index 207d3ebb65a8ef..00000000000000 --- a/app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js +++ /dev/null @@ -1,81 +0,0 @@ -import { defineMessages, injectIntl } from 'react-intl'; - -import { connect } from 'react-redux'; - -import { replyCompose } from 'flavours/glitch/actions/compose'; -import { markConversationRead, deleteConversation } from 'flavours/glitch/actions/conversations'; -import { openModal } from 'flavours/glitch/actions/modal'; -import { muteStatus, unmuteStatus, hideStatus, revealStatus } from 'flavours/glitch/actions/statuses'; -import { makeGetStatus } from 'flavours/glitch/selectors'; - -import Conversation from '../components/conversation'; - -const messages = defineMessages({ - replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, - replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, -}); - -const mapStateToProps = () => { - const getStatus = makeGetStatus(); - - return (state, { conversationId }) => { - const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId); - const lastStatusId = conversation.get('last_status', null); - - return { - accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)), - unread: conversation.get('unread'), - lastStatus: lastStatusId && getStatus(state, { id: lastStatusId }), - settings: state.get('local_settings'), - }; - }; -}; - -const mapDispatchToProps = (dispatch, { intl, conversationId }) => ({ - - markRead () { - dispatch(markConversationRead(conversationId)); - }, - - reply (status, router) { - dispatch((_, getState) => { - let state = getState(); - - if (state.getIn(['compose', 'text']).trim().length !== 0) { - dispatch(openModal({ - modalType: 'CONFIRM', - modalProps: { - message: intl.formatMessage(messages.replyMessage), - confirm: intl.formatMessage(messages.replyConfirm), - onConfirm: () => dispatch(replyCompose(status, router)), - }, - })); - } else { - dispatch(replyCompose(status, router)); - } - }); - }, - - delete () { - dispatch(deleteConversation(conversationId)); - }, - - onMute (status) { - if (status.get('muted')) { - dispatch(unmuteStatus(status.get('id'))); - } else { - dispatch(muteStatus(status.get('id'))); - } - }, - - onToggleHidden (status) { - if (status.get('hidden')) { - dispatch(revealStatus(status.get('id'))); - } else { - dispatch(hideStatus(status.get('id'))); - } - }, - -}); - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Conversation)); diff --git a/app/javascript/flavours/glitch/features/direct_timeline/containers/conversations_list_container.js b/app/javascript/flavours/glitch/features/direct_timeline/containers/conversations_list_container.js deleted file mode 100644 index 1dcd3ec1bd4ad3..00000000000000 --- a/app/javascript/flavours/glitch/features/direct_timeline/containers/conversations_list_container.js +++ /dev/null @@ -1,16 +0,0 @@ -import { connect } from 'react-redux'; - -import { expandConversations } from '../../../actions/conversations'; -import ConversationsList from '../components/conversations_list'; - -const mapStateToProps = state => ({ - conversations: state.getIn(['conversations', 'items']), - isLoading: state.getIn(['conversations', 'isLoading'], true), - hasMore: state.getIn(['conversations', 'hasMore'], false), -}); - -const mapDispatchToProps = dispatch => ({ - onLoadMore: maxId => dispatch(expandConversations({ maxId })), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ConversationsList); diff --git a/app/javascript/flavours/glitch/features/direct_timeline/index.jsx b/app/javascript/flavours/glitch/features/direct_timeline/index.jsx index 9de5751ffbebd2..25f0dd9997f590 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/direct_timeline/index.jsx @@ -1,12 +1,11 @@ import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; +import { useRef, useCallback, useEffect } from 'react'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { Helmet } from 'react-helmet'; -import { connect } from 'react-redux'; - +import { useDispatch, useSelector } from 'react-redux'; import MailIcon from '@/material-icons/400-24px/mail.svg?react'; import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; @@ -17,51 +16,44 @@ import Column from 'flavours/glitch/components/column'; import ColumnHeader from 'flavours/glitch/components/column_header'; import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; +import { ConversationsList } from './components/conversations_list'; import ColumnSettingsContainer from './containers/column_settings_container'; -import ConversationsListContainer from './containers/conversations_list_container'; const messages = defineMessages({ title: { id: 'column.direct', defaultMessage: 'Private mentions' }, }); -const mapStateToProps = state => ({ - hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0, - conversationsMode: state.getIn(['settings', 'direct', 'conversations']), -}); - -class DirectTimeline extends PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - columnId: PropTypes.string, - intl: PropTypes.object.isRequired, - hasUnread: PropTypes.bool, - multiColumn: PropTypes.bool, - conversationsMode: PropTypes.bool, - }; +const DirectTimeline = ({ columnId, multiColumn }) => { + const columnRef = useRef(); + const intl = useIntl(); + const dispatch = useDispatch(); + const pinned = !!columnId; - handlePin = () => { - const { columnId, dispatch } = this.props; + // glitch-soc additions + const hasUnread = useSelector(state => state.getIn(['timelines', 'direct', 'unread']) > 0); + const conversationsMode = useSelector(state => state.getIn(['settings', 'direct', 'conversations'])); + const handlePin = useCallback(() => { if (columnId) { dispatch(removeColumn(columnId)); } else { dispatch(addColumn('DIRECT', {})); } - }; + }, [dispatch, columnId]); - handleMove = (dir) => { - const { columnId, dispatch } = this.props; + const handleMove = useCallback((dir) => { dispatch(moveColumn(columnId, dir)); - }; + }, [dispatch, columnId]); - handleHeaderClick = () => { - this.column.scrollTop(); - }; + const handleHeaderClick = useCallback(() => { + columnRef.current.scrollTop(); + }, [columnRef]); - componentDidMount () { - const { dispatch, conversationsMode } = this.props; + const handleLoadMoreTimeline = useCallback(maxId => { + dispatch(expandDirectTimeline({ maxId })); + }, [dispatch]); + useEffect(() => { dispatch(mountConversations()); if (conversationsMode) { @@ -70,99 +62,67 @@ class DirectTimeline extends PureComponent { dispatch(expandDirectTimeline()); } - this.disconnect = dispatch(connectDirectStream()); - } - - componentDidUpdate(prevProps) { - const { dispatch, conversationsMode } = this.props; - - if (prevProps.conversationsMode && !conversationsMode) { - dispatch(expandDirectTimeline()); - } else if (!prevProps.conversationsMode && conversationsMode) { - dispatch(expandConversations()); - } - } - - componentWillUnmount () { - this.props.dispatch(unmountConversations()); - - if (this.disconnect) { - this.disconnect(); - this.disconnect = null; - } - } - - setRef = c => { - this.column = c; - }; - - handleLoadMoreTimeline = maxId => { - this.props.dispatch(expandDirectTimeline({ maxId })); - }; - - handleLoadMoreConversations = maxId => { - this.props.dispatch(expandConversations({ maxId })); - }; - - render () { - const { intl, hasUnread, columnId, multiColumn, conversationsMode } = this.props; - const pinned = !!columnId; - - let contents; - if (conversationsMode) { - contents = ( - { + dispatch(unmountConversations()); + disconnect(); + }; + }, [dispatch, conversationsMode]); + + return ( + + + + + + {conversationsMode ? ( + } bindToDocument={!multiColumn} - onLoadMore={this.handleLoadMore} prepend={
} alwaysPrepend - emptyMessage={} /> - ); - } else { - contents = ( + ) : (
} + onLoadMore={handleLoadMoreTimeline} + prepend={ +
+ +
+ } alwaysPrepend emptyMessage={} /> - ); - } - - return ( - - - - - - {contents} - - - {intl.formatMessage(messages.title)} - - - - ); - } - -} - -export default connect(mapStateToProps)(injectIntl(DirectTimeline)); + )} + + + {intl.formatMessage(messages.title)} + + + + ); +}; + +DirectTimeline.propTypes = { + columnId: PropTypes.string, + multiColumn: PropTypes.bool, +}; + +export default DirectTimeline; From 3ede233146426a95d12349e2e7dd74915a83d99e Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 30 Jan 2024 22:42:22 +0100 Subject: [PATCH 34/45] Fix crash in private mention conversations in glitch-soc flavor (#2595) --- .../features/direct_timeline/components/conversation.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx index 6ccd1497a7cd13..458a547d020c56 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx +++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx @@ -167,6 +167,10 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown }) } }, [dispatch, lastStatus, expanded]); + if (!lastStatus) { + return null; + } + const menu = [ { text: intl.formatMessage(messages.open), action: handleClick }, null, From 8b87673f5edeaced6b5e4dad29557ff53b11aa85 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 30 Jan 2024 23:21:35 +0100 Subject: [PATCH 35/45] Remove obsolete locale file (#2596) --- .../flavours/glitch/locales/fr-QC.json | 158 ------------------ 1 file changed, 158 deletions(-) delete mode 100644 app/javascript/flavours/glitch/locales/fr-QC.json diff --git a/app/javascript/flavours/glitch/locales/fr-QC.json b/app/javascript/flavours/glitch/locales/fr-QC.json deleted file mode 100644 index a9d0108ce4b89b..00000000000000 --- a/app/javascript/flavours/glitch/locales/fr-QC.json +++ /dev/null @@ -1,158 +0,0 @@ -{ - "about.fork_disclaimer": "Glitch-soc est un logiciel gratuit et open source, fork de Mastodon.", - "account.disclaimer_full": "Les informations ci-dessous peuvent être incomplètes.", - "account.follows": "Abonnements", - "account.joined": "Ici depuis {date}", - "account.suspended_disclaimer_full": "Cet utilisateur a été suspendu par un modérateur.", - "account.view_full_profile": "Voir le profil complet", - "advanced_options.icon_title": "Options avancées", - "advanced_options.local-only.long": "Ne pas envoyer aux autres instances", - "advanced_options.local-only.short": "Uniquement en local", - "advanced_options.local-only.tooltip": "Ce post est uniquement local", - "advanced_options.threaded_mode.long": "Ouvre automatiquement une réponse lors de la publication", - "advanced_options.threaded_mode.short": "Mode thread", - "advanced_options.threaded_mode.tooltip": "Mode thread activé", - "boost_modal.missing_description": "Ce post contient des médias sans description", - "column.favourited_by": "Ajouté en favori par", - "column.heading": "Divers", - "column.reblogged_by": "Partagé par", - "column.subheading": "Autres options", - "column_header.profile": "Profil", - "column_subheading.lists": "Listes", - "column_subheading.navigation": "Navigation", - "community.column_settings.allow_local_only": "Afficher seulement les posts locaux", - "compose.attach": "Joindre…", - "compose.attach.doodle": "Dessiner quelque chose", - "compose.attach.upload": "Téléverser un fichier", - "compose.content-type.html": "HTML", - "compose.content-type.markdown": "Markdown", - "compose.content-type.plain": "Text brut", - "compose_form.poll.multiple_choices": "Choix multiples", - "compose_form.poll.single_choice": "Choix unique", - "compose_form.spoiler": "Cacher le texte derrière un avertissement", - "confirmation_modal.do_not_ask_again": "Ne plus demander confirmation", - "confirmations.deprecated_settings.confirm": "Utiliser les préférences de Mastodon", - "confirmations.deprecated_settings.message": "Certaines {app_settings} de glitch-soc que vous utilisez ont été remplacées par les {preferences} de Mastodon et seront remplacées :", - "confirmations.missing_media_description.confirm": "Envoyer quand même", - "confirmations.missing_media_description.edit": "Modifier le média", - "confirmations.missing_media_description.message": "Au moins un média joint manque d'une description. Pensez à décrire tous les médias attachés pour les malvoyant·e·s avant de publier votre post.", - "confirmations.unfilter.author": "Auteur", - "confirmations.unfilter.confirm": "Afficher", - "confirmations.unfilter.edit_filter": "Modifier le filtre", - "confirmations.unfilter.filters": "Correspondance avec {count, plural, one {un filtre} other {plusieurs filtres}}", - "content-type.change": "Type de contenu", - "direct.group_by_conversations": "Grouper par conversation", - "endorsed_accounts_editor.endorsed_accounts": "Comptes mis en avant", - "favourite_modal.combo": "Vous pouvez appuyer sur {combo} pour passer ceci la prochaine fois", - "firehose.column_settings.allow_local_only": "Afficher les messages locaux dans \"Tous\"", - "home.column_settings.advanced": "Avancé", - "home.column_settings.filter_regex": "Filtrer par expression régulière", - "home.column_settings.show_direct": "Afficher les MPs", - "home.settings": "Paramètres de la colonne", - "keyboard_shortcuts.bookmark": "ajouter aux marque-pages", - "keyboard_shortcuts.secondary_toot": "Envoyer le post en utilisant les paramètres secondaires de confidentialité", - "keyboard_shortcuts.toggle_collapse": "Plier/déplier les posts", - "media_gallery.sensitive": "Sensible", - "moved_to_warning": "Ce compte a déménagé vers {moved_to_link} et ne peut donc plus accepter de nouveaux abonné·e·s.", - "navigation_bar.app_settings": "Paramètres de l'application", - "navigation_bar.featured_users": "Utilisateurs mis en avant", - "navigation_bar.keyboard_shortcuts": "Raccourcis clavier", - "navigation_bar.misc": "Autres", - "notification.markForDeletion": "Ajouter aux éléments à supprimer", - "notification_purge.btn_all": "Sélectionner\ntout", - "notification_purge.btn_apply": "Effacer\nla sélection", - "notification_purge.btn_invert": "Inverser\nla sélection", - "notification_purge.btn_none": "Annuler\nla sélection", - "notification_purge.start": "Activer le mode de nettoyage des notifications", - "notifications.marked_clear": "Effacer les notifications sélectionnées", - "notifications.marked_clear_confirmation": "Voulez-vous vraiment effacer de manière permanente toutes les notifications sélectionnées ?", - "settings.always_show_spoilers_field": "Toujours activer le champ de rédaction de l'avertissement de contenu", - "settings.auto_collapse": "Repliage automatique", - "settings.auto_collapse_all": "Tout", - "settings.auto_collapse_height": "Hauteur (en pixels) pour qu'un pouet soit considéré comme long", - "settings.auto_collapse_lengthy": "Posts longs", - "settings.auto_collapse_media": "Posts avec média", - "settings.auto_collapse_notifications": "Notifications", - "settings.auto_collapse_reblogs": "Boosts", - "settings.auto_collapse_replies": "Réponses", - "settings.close": "Fermer", - "settings.collapsed_statuses": "Posts repliés", - "settings.compose_box_opts": "Zone de rédaction", - "settings.confirm_before_clearing_draft": "Afficher une fenêtre de confirmation avant d'écraser le message en cours de rédaction", - "settings.confirm_boost_missing_media_description": "Afficher une fenêtre de confirmation avant de partager des posts manquant de description des médias", - "settings.confirm_missing_media_description": "Afficher une fenêtre de confirmation avant de publier des posts manquant de description de média", - "settings.content_warnings": "Content warnings", - "settings.content_warnings.regexp": "Expression rationnelle", - "settings.content_warnings_filter": "Avertissement de contenu à ne pas automatiquement déplier :", - "settings.content_warnings_media_outside": "Afficher les médias en dehors des avertissements de contenu", - "settings.content_warnings_media_outside_hint": "Reproduit le comportement par défaut de Mastodon, les médias attachés ne sont plus affectés par le bouton d'affichage d'un post avec avertissement", - "settings.content_warnings_shared_state": "Affiche/cache le contenu de toutes les copies à la fois", - "settings.content_warnings_shared_state_hint": "Reproduit le comportement par défaut de Mastodon, le bouton d'avertissement de contenu affecte toutes les copies d'un post à la fois. Cela empêchera le repliement automatique de n'importe quelle copie d'un post avec un avertissement déplié", - "settings.content_warnings_unfold_opts": "Options de dépliement automatique", - "settings.deprecated_setting": "Cette option est maintenant définie par les {settings_page_link} de Mastodon", - "settings.enable_collapsed": "Activer le repliement des posts", - "settings.enable_collapsed_hint": "Les posts repliés ont une partie de leur contenu caché pour libérer de l'espace sur l'écran. C'est une option différente de l'avertissement de contenu", - "settings.enable_content_warnings_auto_unfold": "Déplier automatiquement les avertissements de contenu", - "settings.general": "Général", - "settings.hicolor_privacy_icons": "Indicateurs de confidentialité en couleurs", - "settings.hicolor_privacy_icons.hint": "Affiche les indicateurs de confidentialité dans des couleurs facilement distinguables", - "settings.image_backgrounds": "Images en arrière-plan", - "settings.image_backgrounds_media": "Prévisualiser les médias d'un post replié", - "settings.image_backgrounds_media_hint": "Si le post a un média attaché, utiliser le premier comme arrière-plan du post", - "settings.image_backgrounds_users": "Donner aux posts repliés une image en arrière-plan", - "settings.inline_preview_cards": "Cartes d'aperçu pour les liens externes", - "settings.layout_opts": "Mise en page", - "settings.media": "Média", - "settings.media_fullwidth": "Utiliser toute la largeur pour les aperçus", - "settings.media_letterbox": "Afficher les médias en Letterbox", - "settings.media_letterbox_hint": "Réduit le média et utilise une letterbox pour afficher l'image entière plutôt que de l'étirer et de la rogner", - "settings.media_reveal_behind_cw": "Toujours afficher les médias sensibles avec avertissement", - "settings.notifications.favicon_badge": "Badge de notifications non lues dans la favicon", - "settings.notifications.favicon_badge.hint": "Ajoute un badge dans la favicon pour alerter d'une notification non lue", - "settings.notifications.tab_badge": "Badge de notifications non lues", - "settings.notifications.tab_badge.hint": "Affiche un badge de notifications non lues dans les icônes des colonnes quand la colonne n'est pas ouverte", - "settings.notifications_opts": "Options des notifications", - "settings.pop_in_left": "Gauche", - "settings.pop_in_player": "Activer le lecteur pop-in", - "settings.pop_in_position": "Position du lecteur pop-in :", - "settings.pop_in_right": "Droite", - "settings.preferences": "Preferences", - "settings.prepend_cw_re": "Préfixer les avertissements avec \"re: \" lors d'une réponse", - "settings.preselect_on_reply": "Présélectionner les noms d’utilisateur·rices lors de la réponse", - "settings.preselect_on_reply_hint": "Présélectionner les noms d'utilisateurs après le premier lors d'une réponse à une conversation à plusieurs participants", - "settings.rewrite_mentions": "Réécrire les mentions dans les posts affichés", - "settings.rewrite_mentions_acct": "Réécrire avec le nom d'utilisateur·rice et le domaine (lorsque le compte est distant)", - "settings.rewrite_mentions_no": "Ne pas réécrire les mentions", - "settings.rewrite_mentions_username": "Réécrire avec le nom d’utilisateur·rice", - "settings.shared_settings_link": "préférences de l'utilisateur", - "settings.show_action_bar": "Afficher les boutons d'action dans les posts repliés", - "settings.show_content_type_choice": "Afficher le choix du type de contenu lors de la création des posts", - "settings.show_reply_counter": "Afficher une estimation du nombre de réponses", - "settings.side_arm": "Bouton secondaire de publication :", - "settings.side_arm.none": "Aucun", - "settings.side_arm_reply_mode": "Quand vous répondez à un post, le bouton secondaire de publication devrait :", - "settings.side_arm_reply_mode.copy": "Copier la confidentialité du post auquel vous répondez", - "settings.side_arm_reply_mode.keep": "Garder la confidentialité établie", - "settings.side_arm_reply_mode.restrict": "Restreindre la confidentialité de la réponse à celle du post auquel vous répondez", - "settings.status_icons": "Icônes des posts", - "settings.status_icons_language": "Indicateur de langue", - "settings.status_icons_local_only": "Indicateur de post local", - "settings.status_icons_media": "Indicateur de médias et sondage", - "settings.status_icons_reply": "Indicateur de réponses", - "settings.status_icons_visibility": "Indicateur de la confidentialité du post", - "settings.swipe_to_change_columns": "Glissement latéral pour changer de colonne (mobile uniquement)", - "settings.tag_misleading_links": "Étiqueter les liens trompeurs", - "settings.tag_misleading_links.hint": "Ajouter une indication visuelle avec l'hôte cible du lien à chaque lien ne le mentionnant pas explicitement", - "settings.wide_view": "Vue élargie (mode ordinateur uniquement)", - "settings.wide_view_hint": "Étire les colonnes pour mieux remplir l'espace disponible.", - "status.collapse": "Replier", - "status.has_audio": "Contient des fichiers audio attachés", - "status.has_pictures": "Contient des images attachées", - "status.has_preview_card": "Contient une carte de prévisualisation attachée", - "status.has_video": "Contient des vidéos attachées", - "status.in_reply_to": "Ce post est une réponse", - "status.is_poll": "Ce post est un sondage", - "status.local_only": "Visible uniquement depuis votre instance", - "status.sensitive_toggle": "Cliquer pour voir", - "status.uncollapse": "Déplier" -} From a48447a6b2e6acf421e8634ac76814fa8d01c0f3 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 1 Feb 2024 10:33:12 +0100 Subject: [PATCH 36/45] Add github action workflow for manual security builds (#29040) --- .github/workflows/build-security.yml | 62 ++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .github/workflows/build-security.yml diff --git a/.github/workflows/build-security.yml b/.github/workflows/build-security.yml new file mode 100644 index 00000000000000..cc9bae922781a4 --- /dev/null +++ b/.github/workflows/build-security.yml @@ -0,0 +1,62 @@ +name: Build security nightly container image + +permissions: + contents: read + packages: write + +jobs: + compute-suffix: + runs-on: ubuntu-latest + if: github.repository == 'mastodon/mastodon' + steps: + - id: version_vars + env: + TZ: Etc/UTC + run: | + echo mastodon_version_prerelease=nightly.$(date --date='next day' +'%Y-%m-%d')-security>> $GITHUB_OUTPUT + outputs: + prerelease: ${{ steps.version_vars.outputs.mastodon_version_prerelease }} + + build-image: + needs: compute-suffix + uses: ./.github/workflows/build-container-image.yml + with: + file_to_build: Dockerfile + platforms: linux/amd64,linux/arm64 + use_native_arm64_builder: true + cache: false + push_to_images: | + tootsuite/mastodon + ghcr.io/mastodon/mastodon + version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }} + labels: | + org.opencontainers.image.description=Nightly build image used for testing purposes + flavor: | + latest=auto + tags: | + type=raw,value=edge + type=raw,value=nightly + type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }} + secrets: inherit + + build-image-streaming: + needs: compute-suffix + uses: ./.github/workflows/build-container-image.yml + with: + file_to_build: streaming/Dockerfile + platforms: linux/amd64,linux/arm64 + use_native_arm64_builder: true + cache: false + push_to_images: | + tootsuite/mastodon-streaming + ghcr.io/mastodon/mastodon-streaming + version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }} + labels: | + org.opencontainers.image.description=Nightly build image used for testing purposes + flavor: | + latest=auto + tags: | + type=raw,value=edge + type=raw,value=nightly + type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }} + secrets: inherit From 85bdd145dc9aee5ae003126c9fd4e490215f7a9a Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 1 Feb 2024 10:40:04 +0100 Subject: [PATCH 37/45] Adapt workflow to glitch-soc --- .github/workflows/build-security.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-security.yml b/.github/workflows/build-security.yml index cc9bae922781a4..fbc85f52eb8b06 100644 --- a/.github/workflows/build-security.yml +++ b/.github/workflows/build-security.yml @@ -7,7 +7,6 @@ permissions: jobs: compute-suffix: runs-on: ubuntu-latest - if: github.repository == 'mastodon/mastodon' steps: - id: version_vars env: @@ -26,8 +25,7 @@ jobs: use_native_arm64_builder: true cache: false push_to_images: | - tootsuite/mastodon - ghcr.io/mastodon/mastodon + ghcr.io/${{ github.repository_owner }}/mastodon version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }} labels: | org.opencontainers.image.description=Nightly build image used for testing purposes @@ -45,11 +43,10 @@ jobs: with: file_to_build: streaming/Dockerfile platforms: linux/amd64,linux/arm64 - use_native_arm64_builder: true + use_native_arm64_builder: false cache: false push_to_images: | - tootsuite/mastodon-streaming - ghcr.io/mastodon/mastodon-streaming + ghcr.io/${{ github.repository_owner }}/mastodon version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }} labels: | org.opencontainers.image.description=Nightly build image used for testing purposes From 883f5896534a74399740766848323e7d59d5464c Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 1 Feb 2024 10:52:01 +0100 Subject: [PATCH 38/45] Fix missing `workflow_dispatch` trigger for `build-security` (#29041) --- .github/workflows/build-security.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-security.yml b/.github/workflows/build-security.yml index fbc85f52eb8b06..00944daeb33363 100644 --- a/.github/workflows/build-security.yml +++ b/.github/workflows/build-security.yml @@ -1,4 +1,6 @@ name: Build security nightly container image +on: + workflow_dispatch: permissions: contents: read From f4416e6b3a58a3fa70f9abae80e49253dd4b3d62 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 1 Feb 2024 04:46:31 -0500 Subject: [PATCH 39/45] Configure selenium to use Chrome version 120 (#29038) --- spec/support/capybara.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index d4f27e209e11a6..4aba65b4047a4f 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -14,6 +14,7 @@ options = Selenium::WebDriver::Chrome::Options.new options.add_argument '--headless=new' options.add_argument '--window-size=1680,1050' + options.browser_version = '120' Capybara::Selenium::Driver.new( app, From a7b16003e11c17df39451aa98c145137a4e9b147 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 1 Feb 2024 11:31:29 +0100 Subject: [PATCH 40/45] Fix security builds not being marked latest --- .github/workflows/build-security.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-security.yml b/.github/workflows/build-security.yml index 00944daeb33363..9919fbf138be48 100644 --- a/.github/workflows/build-security.yml +++ b/.github/workflows/build-security.yml @@ -32,7 +32,7 @@ jobs: labels: | org.opencontainers.image.description=Nightly build image used for testing purposes flavor: | - latest=auto + latest=true tags: | type=raw,value=edge type=raw,value=nightly @@ -53,7 +53,7 @@ jobs: labels: | org.opencontainers.image.description=Nightly build image used for testing purposes flavor: | - latest=auto + latest=true tags: | type=raw,value=edge type=raw,value=nightly From ff58ec0103441615ecb92141113b104f13132642 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 1 Feb 2024 15:56:46 +0100 Subject: [PATCH 41/45] Merge pull request from GHSA-3fjr-858r-92rw * Fix insufficient origin validation * Bump version to 4.3.0-alpha.1 --- .../concerns/signature_verification.rb | 2 +- app/helpers/jsonld_helper.rb | 4 ++-- app/lib/activitypub/activity.rb | 2 +- app/lib/activitypub/linked_data_signature.rb | 2 +- .../activitypub/fetch_remote_account_service.rb | 2 +- .../activitypub/fetch_remote_actor_service.rb | 6 +++--- .../activitypub/fetch_remote_key_service.rb | 17 ++--------------- .../activitypub/fetch_remote_status_service.rb | 8 ++++---- .../activitypub/process_account_service.rb | 2 +- app/services/fetch_resource_service.rb | 10 +++++++++- lib/mastodon/version.rb | 2 +- .../activitypub/linked_data_signature_spec.rb | 4 ++-- .../fetch_remote_account_service_spec.rb | 2 +- .../fetch_remote_actor_service_spec.rb | 2 +- .../fetch_remote_key_service_spec.rb | 2 +- spec/services/fetch_resource_service_spec.rb | 10 +++++----- spec/services/resolve_url_service_spec.rb | 1 + 17 files changed, 37 insertions(+), 41 deletions(-) diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 35391e64c44390..92f1eb5a168280 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -266,7 +266,7 @@ def actor_from_key_id(key_id) stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) } elsif !ActivityPub::TagManager.instance.local_uri?(key_id) account = ActivityPub::TagManager.instance.uri_to_actor(key_id) - account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) } + account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) } account end rescue Mastodon::PrivateNetworkAddressError => e diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index b3d0d032c4d3ce..cc05b7a403483e 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -155,8 +155,8 @@ def safe_for_forwarding?(original, compacted) end end - def fetch_resource(uri, id, on_behalf_of = nil, request_options: {}) - unless id + def fetch_resource(uri, id_is_known, on_behalf_of = nil, request_options: {}) + unless id_is_known json = fetch_resource_without_id_validation(uri, on_behalf_of) return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id']) diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 51384ef984657d..322f3e27adb604 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -154,7 +154,7 @@ def fetch_remote_original_status if object_uri.start_with?('http') return if ActivityPub::TagManager.instance.local_uri?(object_uri) - ActivityPub::FetchRemoteStatusService.new.call(object_uri, id: true, on_behalf_of: @account.followers.local.first, request_id: @options[:request_id]) + ActivityPub::FetchRemoteStatusService.new.call(object_uri, on_behalf_of: @account.followers.local.first, request_id: @options[:request_id]) elsif @object['url'].present? ::FetchRemoteStatusService.new.call(@object['url'], request_id: @options[:request_id]) end diff --git a/app/lib/activitypub/linked_data_signature.rb b/app/lib/activitypub/linked_data_signature.rb index faea63e8f12c6e..9459fdd8b76972 100644 --- a/app/lib/activitypub/linked_data_signature.rb +++ b/app/lib/activitypub/linked_data_signature.rb @@ -19,7 +19,7 @@ def verify_actor! return unless type == 'RsaSignature2017' creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri) - creator = ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false) if creator&.public_key.blank? + creator = ActivityPub::FetchRemoteKeyService.new.call(creator_uri) if creator&.public_key.blank? return if creator.nil? diff --git a/app/services/activitypub/fetch_remote_account_service.rb b/app/services/activitypub/fetch_remote_account_service.rb index 567dd8a14abc04..7b083d889b21fb 100644 --- a/app/services/activitypub/fetch_remote_account_service.rb +++ b/app/services/activitypub/fetch_remote_account_service.rb @@ -2,7 +2,7 @@ class ActivityPub::FetchRemoteAccountService < ActivityPub::FetchRemoteActorService # Does a WebFinger roundtrip on each call, unless `only_key` is true - def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true, request_id: nil) + def call(uri, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true, request_id: nil) actor = super return actor if actor.nil? || actor.is_a?(Account) diff --git a/app/services/activitypub/fetch_remote_actor_service.rb b/app/services/activitypub/fetch_remote_actor_service.rb index 8df8c75876644d..86a134bb4ed911 100644 --- a/app/services/activitypub/fetch_remote_actor_service.rb +++ b/app/services/activitypub/fetch_remote_actor_service.rb @@ -10,15 +10,15 @@ class Error < StandardError; end SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze # Does a WebFinger roundtrip on each call, unless `only_key` is true - def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true, request_id: nil) + def call(uri, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true, request_id: nil) return if domain_not_allowed?(uri) return ActivityPub::TagManager.instance.uri_to_actor(uri) if ActivityPub::TagManager.instance.local_uri?(uri) @json = begin if prefetched_body.nil? - fetch_resource(uri, id) + fetch_resource(uri, true) else - body_to_json(prefetched_body, compare_id: id ? uri : nil) + body_to_json(prefetched_body, compare_id: uri) end rescue Oj::ParseError raise Error, "Error parsing JSON-LD document #{uri}" diff --git a/app/services/activitypub/fetch_remote_key_service.rb b/app/services/activitypub/fetch_remote_key_service.rb index 8eb97c1e66d188..e96b5ad3bb012c 100644 --- a/app/services/activitypub/fetch_remote_key_service.rb +++ b/app/services/activitypub/fetch_remote_key_service.rb @@ -6,23 +6,10 @@ class ActivityPub::FetchRemoteKeyService < BaseService class Error < StandardError; end # Returns actor that owns the key - def call(uri, id: true, prefetched_body: nil, suppress_errors: true) + def call(uri, suppress_errors: true) raise Error, 'No key URI given' if uri.blank? - if prefetched_body.nil? - if id - @json = fetch_resource_without_id_validation(uri) - if actor_type? - @json = fetch_resource(@json['id'], true) - elsif uri != @json['id'] - raise Error, "Fetched URI #{uri} has wrong id #{@json['id']}" - end - else - @json = fetch_resource(uri, id) - end - else - @json = body_to_json(prefetched_body, compare_id: id ? uri : nil) - end + @json = fetch_resource(uri, false) raise Error, "Unable to fetch key JSON at #{uri}" if @json.nil? raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?(@json) diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb index e3a9b60b5679f2..6f8882378f32ec 100644 --- a/app/services/activitypub/fetch_remote_status_service.rb +++ b/app/services/activitypub/fetch_remote_status_service.rb @@ -8,14 +8,14 @@ class ActivityPub::FetchRemoteStatusService < BaseService DISCOVERIES_PER_REQUEST = 1000 # Should be called when uri has already been checked for locality - def call(uri, id: true, prefetched_body: nil, on_behalf_of: nil, expected_actor_uri: nil, request_id: nil) + def call(uri, prefetched_body: nil, on_behalf_of: nil, expected_actor_uri: nil, request_id: nil) return if domain_not_allowed?(uri) @request_id = request_id || "#{Time.now.utc.to_i}-status-#{uri}" @json = if prefetched_body.nil? - fetch_resource(uri, id, on_behalf_of) + fetch_resource(uri, true, on_behalf_of) else - body_to_json(prefetched_body, compare_id: id ? uri : nil) + body_to_json(prefetched_body, compare_id: uri) end return unless supported_context? @@ -65,7 +65,7 @@ def trustworthy_attribution?(uri, attributed_to) def account_from_uri(uri) actor = ActivityPub::TagManager.instance.uri_to_resource(uri, Account) - actor = ActivityPub::FetchRemoteAccountService.new.call(uri, id: true, request_id: @request_id) if actor.nil? || actor.possibly_stale? + actor = ActivityPub::FetchRemoteAccountService.new.call(uri, request_id: @request_id) if actor.nil? || actor.possibly_stale? actor end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 8fc0989a3f7e91..9e787ace508a0d 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -277,7 +277,7 @@ def collection_info(type) def moved_account account = ActivityPub::TagManager.instance.uri_to_resource(@json['movedTo'], Account) - account ||= ActivityPub::FetchRemoteAccountService.new.call(@json['movedTo'], id: true, break_on_redirect: true, request_id: @options[:request_id]) + account ||= ActivityPub::FetchRemoteAccountService.new.call(@json['movedTo'], break_on_redirect: true, request_id: @options[:request_id]) account end diff --git a/app/services/fetch_resource_service.rb b/app/services/fetch_resource_service.rb index a3406e5a579c57..71c6cca790c6ee 100644 --- a/app/services/fetch_resource_service.rb +++ b/app/services/fetch_resource_service.rb @@ -48,7 +48,15 @@ def process_response(response, terminal = false) body = response.body_with_limit json = body_to_json(body) - [json['id'], { prefetched_body: body, id: true }] if supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES) || expected_type?(json)) + return unless supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES) || expected_type?(json)) + + if json['id'] != @url + return if terminal + + return process(json['id'], terminal: true) + end + + [@url, { prefetched_body: body }] elsif !terminal link_header = response['Link'] && parse_link_header(response) diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index b55873a3c926a8..511c647787dd61 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -17,7 +17,7 @@ def patch end def default_prerelease - 'alpha.0' + 'alpha.1' end def prerelease diff --git a/spec/lib/activitypub/linked_data_signature_spec.rb b/spec/lib/activitypub/linked_data_signature_spec.rb index 97268eea6d04b2..1af45673c049bc 100644 --- a/spec/lib/activitypub/linked_data_signature_spec.rb +++ b/spec/lib/activitypub/linked_data_signature_spec.rb @@ -56,7 +56,7 @@ allow(ActivityPub::FetchRemoteKeyService).to receive(:new).and_return(service_stub) - allow(service_stub).to receive(:call).with('http://example.com/alice', id: false) do + allow(service_stub).to receive(:call).with('http://example.com/alice') do sender.update!(public_key: old_key) sender end @@ -64,7 +64,7 @@ it 'fetches key and returns creator' do expect(subject.verify_actor!).to eq sender - expect(service_stub).to have_received(:call).with('http://example.com/alice', id: false).once + expect(service_stub).to have_received(:call).with('http://example.com/alice').once end end diff --git a/spec/services/activitypub/fetch_remote_account_service_spec.rb b/spec/services/activitypub/fetch_remote_account_service_spec.rb index ac7484d96d1a44..f33a928da6dd2c 100644 --- a/spec/services/activitypub/fetch_remote_account_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_account_service_spec.rb @@ -18,7 +18,7 @@ end describe '#call' do - let(:account) { subject.call('https://example.com/alice', id: true) } + let(:account) { subject.call('https://example.com/alice') } shared_examples 'sets profile data' do it 'returns an account' do diff --git a/spec/services/activitypub/fetch_remote_actor_service_spec.rb b/spec/services/activitypub/fetch_remote_actor_service_spec.rb index 93d31b69d5190f..944a2f8b1c63eb 100644 --- a/spec/services/activitypub/fetch_remote_actor_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_actor_service_spec.rb @@ -18,7 +18,7 @@ end describe '#call' do - let(:account) { subject.call('https://example.com/alice', id: true) } + let(:account) { subject.call('https://example.com/alice') } shared_examples 'sets profile data' do it 'returns an account' do diff --git a/spec/services/activitypub/fetch_remote_key_service_spec.rb b/spec/services/activitypub/fetch_remote_key_service_spec.rb index e210d20ec77d40..0b14da4f446e5f 100644 --- a/spec/services/activitypub/fetch_remote_key_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_key_service_spec.rb @@ -55,7 +55,7 @@ end describe '#call' do - let(:account) { subject.call(public_key_id, id: false) } + let(:account) { subject.call(public_key_id) } context 'when the key is a sub-object from the actor' do before do diff --git a/spec/services/fetch_resource_service_spec.rb b/spec/services/fetch_resource_service_spec.rb index 0f1068471f8e38..78037a06ce4fdb 100644 --- a/spec/services/fetch_resource_service_spec.rb +++ b/spec/services/fetch_resource_service_spec.rb @@ -57,7 +57,7 @@ let(:json) do { - id: 1, + id: 'http://example.com/foo', '@context': ActivityPub::TagManager::CONTEXT, type: 'Note', }.to_json @@ -83,27 +83,27 @@ let(:content_type) { 'application/activity+json; charset=utf-8' } let(:body) { json } - it { is_expected.to eq [1, { prefetched_body: body, id: true }] } + it { is_expected.to eq ['http://example.com/foo', { prefetched_body: body }] } end context 'when content type is ld+json with profile' do let(:content_type) { 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' } let(:body) { json } - it { is_expected.to eq [1, { prefetched_body: body, id: true }] } + it { is_expected.to eq ['http://example.com/foo', { prefetched_body: body }] } end context 'when link header is present' do let(:headers) { { 'Link' => '; rel="alternate"; type="application/activity+json"' } } - it { is_expected.to eq [1, { prefetched_body: json, id: true }] } + it { is_expected.to eq ['http://example.com/foo', { prefetched_body: json }] } end context 'when content type is text/html' do let(:content_type) { 'text/html' } let(:body) { '' } - it { is_expected.to eq [1, { prefetched_body: json, id: true }] } + it { is_expected.to eq ['http://example.com/foo', { prefetched_body: json }] } end end end diff --git a/spec/services/resolve_url_service_spec.rb b/spec/services/resolve_url_service_spec.rb index bcfb9dbfb0f893..5270cc10dd8f12 100644 --- a/spec/services/resolve_url_service_spec.rb +++ b/spec/services/resolve_url_service_spec.rb @@ -139,6 +139,7 @@ stub_request(:get, url).to_return(status: 302, headers: { 'Location' => status_url }) body = ActiveModelSerializers::SerializableResource.new(status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter).to_json stub_request(:get, status_url).to_return(body: body, headers: { 'Content-Type' => 'application/activity+json' }) + stub_request(:get, uri).to_return(body: body, headers: { 'Content-Type' => 'application/activity+json' }) end it 'returns status by url' do From 63d7a30503c803f1960ca6a1ba6a33c6f9463c1b Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 1 Feb 2024 16:09:51 +0100 Subject: [PATCH 42/45] Fix build-security workflow for glitch-soc --- .github/workflows/build-security.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-security.yml b/.github/workflows/build-security.yml index 9919fbf138be48..6fca32619829f2 100644 --- a/.github/workflows/build-security.yml +++ b/.github/workflows/build-security.yml @@ -24,7 +24,7 @@ jobs: with: file_to_build: Dockerfile platforms: linux/amd64,linux/arm64 - use_native_arm64_builder: true + use_native_arm64_builder: false cache: false push_to_images: | ghcr.io/${{ github.repository_owner }}/mastodon From 970320d73cf0870060e72a1be633bed14c7be3ac Mon Sep 17 00:00:00 2001 From: Aaron Brady Date: Thu, 1 Feb 2024 11:20:08 -0500 Subject: [PATCH 43/45] Restore -streaming suffix for security builds (#2602) --- .github/workflows/build-security.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-security.yml b/.github/workflows/build-security.yml index 6fca32619829f2..e96d55e4619b2f 100644 --- a/.github/workflows/build-security.yml +++ b/.github/workflows/build-security.yml @@ -48,7 +48,7 @@ jobs: use_native_arm64_builder: false cache: false push_to_images: | - ghcr.io/${{ github.repository_owner }}/mastodon + ghcr.io/${{ github.repository_owner }}/mastodon-streaming version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }} labels: | org.opencontainers.image.description=Nightly build image used for testing purposes From c3936cbbe3851f376df2b8edc3ac65ec20e181b8 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 1 Feb 2024 18:39:28 +0100 Subject: [PATCH 44/45] =?UTF-8?q?Temporary=20hack=20to=20correctly=20tag?= =?UTF-8?q?=20the=20security=20docker=20image=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-security.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-security.yml b/.github/workflows/build-security.yml index e96d55e4619b2f..6683ef47277ae6 100644 --- a/.github/workflows/build-security.yml +++ b/.github/workflows/build-security.yml @@ -14,7 +14,7 @@ jobs: env: TZ: Etc/UTC run: | - echo mastodon_version_prerelease=nightly.$(date --date='next day' +'%Y-%m-%d')-security>> $GITHUB_OUTPUT + echo mastodon_version_prerelease=nightly.2024-02-02-security>> $GITHUB_OUTPUT outputs: prerelease: ${{ steps.version_vars.outputs.mastodon_version_prerelease }} From 5bc39b3196db8c79719c5a6c920c780284b8266a Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 1 Feb 2024 20:30:35 +0100 Subject: [PATCH 45/45] Fix build-security docker tags --- .github/workflows/build-security.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-security.yml b/.github/workflows/build-security.yml index 6683ef47277ae6..e9f1862f5d1b54 100644 --- a/.github/workflows/build-security.yml +++ b/.github/workflows/build-security.yml @@ -14,7 +14,7 @@ jobs: env: TZ: Etc/UTC run: | - echo mastodon_version_prerelease=nightly.2024-02-02-security>> $GITHUB_OUTPUT + echo mastodon_version_prerelease=nightly.$(date --date='next day' +'%Y-%m-%d')-security>> $GITHUB_OUTPUT outputs: prerelease: ${{ steps.version_vars.outputs.mastodon_version_prerelease }} @@ -36,7 +36,7 @@ jobs: tags: | type=raw,value=edge type=raw,value=nightly - type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }} + type=raw,value=${{ needs.compute-suffix.outputs.prerelease }} secrets: inherit build-image-streaming: @@ -57,5 +57,5 @@ jobs: tags: | type=raw,value=edge type=raw,value=nightly - type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }} + type=raw,value=${{ needs.compute-suffix.outputs.prerelease }} secrets: inherit