From 0ca7a50e96f8ff596de7432462a5e92a98de5e38 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 4 Dec 2023 09:50:28 +0100
Subject: [PATCH 01/73] Update devDependencies (non-major) (#28200)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 yarn.lock | 142 +++++++++++++++++++++++++++++++++++++++---------------
 1 file changed, 102 insertions(+), 40 deletions(-)

diff --git a/yarn.lock b/yarn.lock
index ad792d26c..3ec023533 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2893,8 +2893,8 @@ __metadata:
   linkType: hard
 
 "@testing-library/jest-dom@npm:^6.0.0":
-  version: 6.1.4
-  resolution: "@testing-library/jest-dom@npm:6.1.4"
+  version: 6.1.5
+  resolution: "@testing-library/jest-dom@npm:6.1.5"
   dependencies:
     "@adobe/css-tools": "npm:^4.3.1"
     "@babel/runtime": "npm:^7.9.2"
@@ -2918,7 +2918,7 @@ __metadata:
       optional: true
     vitest:
       optional: true
-  checksum: 2e23f120613fd8ae6d5169bbc94f1a2e4c82b07182057dc94db8ec54ebf32555833442e6c43a187e59715d83704ffb5df49ba88a71f6f32d2683f3d95ba721c7
+  checksum: f3643a56fcd970b5c7e8fd10faf3c4817d8ab0e74fb1198d726643bdc5ac675ceaac3b0068c5b4fbad254470e8f98ed50028741de875a29ceaa2f854570979c9
   languageName: node
   linkType: hard
 
@@ -4168,12 +4168,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"ansi-escapes@npm:^5.0.0":
-  version: 5.0.0
-  resolution: "ansi-escapes@npm:5.0.0"
+"ansi-escapes@npm:^6.2.0":
+  version: 6.2.0
+  resolution: "ansi-escapes@npm:6.2.0"
   dependencies:
-    type-fest: "npm:^1.0.2"
-  checksum: f705cc7fbabb981ddf51562cd950792807bccd7260cc3d9478a619dda62bff6634c87ca100f2545ac7aade9b72652c4edad8c7f0d31a0b949b5fa58f33eaf0d0
+    type-fest: "npm:^3.0.0"
+  checksum: 3eec75deedd8b10192c5f98e4cd9715cc3ff268d33fc463c24b7d22446668bfcd4ad1803993ea89c0f51f88b5a3399572bacb7c8cb1a067fc86e189c5f3b0c7e
   languageName: node
   linkType: hard
 
@@ -4239,7 +4239,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0":
+"ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1":
   version: 6.2.1
   resolution: "ansi-styles@npm:6.2.1"
   checksum: 5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c
@@ -5584,13 +5584,13 @@ __metadata:
   languageName: node
   linkType: hard
 
-"cli-truncate@npm:^3.1.0":
-  version: 3.1.0
-  resolution: "cli-truncate@npm:3.1.0"
+"cli-truncate@npm:^4.0.0":
+  version: 4.0.0
+  resolution: "cli-truncate@npm:4.0.0"
   dependencies:
     slice-ansi: "npm:^5.0.0"
-    string-width: "npm:^5.0.0"
-  checksum: a19088878409ec0e5dc2659a5166929629d93cfba6d68afc9cde2282fd4c751af5b555bf197047e31c87c574396348d011b7aa806fec29c4139ea4f7f00b324c
+    string-width: "npm:^7.0.0"
+  checksum: d7f0b73e3d9b88cb496e6c086df7410b541b56a43d18ade6a573c9c18bd001b1c3fba1ad578f741a4218fdc794d042385f8ac02c25e1c295a2d8b9f3cb86eb4c
   languageName: node
   linkType: hard
 
@@ -6997,7 +6997,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"emoji-regex@npm:10.3.0, emoji-regex@npm:^10.2.1":
+"emoji-regex@npm:10.3.0, emoji-regex@npm:^10.2.1, emoji-regex@npm:^10.3.0":
   version: 10.3.0
   resolution: "emoji-regex@npm:10.3.0"
   checksum: b4838e8dcdceb44cf47f59abe352c25ff4fe7857acaf5fb51097c427f6f75b44d052eb907a7a3b86f86bc4eae3a93f5c2b7460abe79c407307e6212d65c91163
@@ -8387,6 +8387,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"get-east-asian-width@npm:^1.0.0":
+  version: 1.2.0
+  resolution: "get-east-asian-width@npm:1.2.0"
+  checksum: 914b1e217cf38436c24b4c60b4c45289e39a45bf9e65ef9fd343c2815a1a02b8a0215aeec8bf9c07c516089004b6e3826332481f40a09529fcadbf6e579f286b
+  languageName: node
+  linkType: hard
+
 "get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.0, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.2":
   version: 1.2.2
   resolution: "get-intrinsic@npm:1.2.2"
@@ -9610,6 +9617,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"is-fullwidth-code-point@npm:^5.0.0":
+  version: 5.0.0
+  resolution: "is-fullwidth-code-point@npm:5.0.0"
+  dependencies:
+    get-east-asian-width: "npm:^1.0.0"
+  checksum: cd591b27d43d76b05fa65ed03eddce57a16e1eca0b7797ff7255de97019bcaf0219acfc0c4f7af13319e13541f2a53c0ace476f442b13267b9a6a7568f2b65c8
+  languageName: node
+  linkType: hard
+
 "is-generator-fn@npm:^2.0.0":
   version: 2.1.0
   resolution: "is-generator-fn@npm:2.1.0"
@@ -10892,7 +10908,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"lilconfig@npm:2.1.0, lilconfig@npm:^2.1.0":
+"lilconfig@npm:3.0.0":
+  version: 3.0.0
+  resolution: "lilconfig@npm:3.0.0"
+  checksum: 7f5ee7a658dc016cacf146815e8d88b06f06f4402823b8b0934e305a57a197f55ccc9c5cd4fb5ea1b2b821c8ccaf2d54abd59602a4931af06eabda332388d3e6
+  languageName: node
+  linkType: hard
+
+"lilconfig@npm:^2.1.0":
   version: 2.1.0
   resolution: "lilconfig@npm:2.1.0"
   checksum: 64645641aa8d274c99338e130554abd6a0190533c0d9eb2ce7ebfaf2e05c7d9961f3ffe2bfa39efd3b60c521ba3dd24fa236fe2775fc38501bf82bf49d4678b8
@@ -10907,36 +10930,36 @@ __metadata:
   linkType: hard
 
 "lint-staged@npm:^15.0.0":
-  version: 15.1.0
-  resolution: "lint-staged@npm:15.1.0"
+  version: 15.2.0
+  resolution: "lint-staged@npm:15.2.0"
   dependencies:
     chalk: "npm:5.3.0"
     commander: "npm:11.1.0"
     debug: "npm:4.3.4"
     execa: "npm:8.0.1"
-    lilconfig: "npm:2.1.0"
-    listr2: "npm:7.0.2"
+    lilconfig: "npm:3.0.0"
+    listr2: "npm:8.0.0"
     micromatch: "npm:4.0.5"
     pidtree: "npm:0.6.0"
     string-argv: "npm:0.3.2"
     yaml: "npm:2.3.4"
   bin:
     lint-staged: bin/lint-staged.js
-  checksum: d427408be98df7558e918593cb765d5caaa67a5cdca89671fb54280a6c959f4e448db36d4f85e8e0bd9c2c1e996aa133916925cf47c9df573b47308d5e298d84
+  checksum: 4a1ff25dd06dbd4346fd244c9a0ebb936532ba18c0caedeb895c2e232f3c6c5fd08f6667624716660bc29e3e0f9f0440a9175114394616e991ebd5fab4b1f092
   languageName: node
   linkType: hard
 
-"listr2@npm:7.0.2":
-  version: 7.0.2
-  resolution: "listr2@npm:7.0.2"
+"listr2@npm:8.0.0":
+  version: 8.0.0
+  resolution: "listr2@npm:8.0.0"
   dependencies:
-    cli-truncate: "npm:^3.1.0"
+    cli-truncate: "npm:^4.0.0"
     colorette: "npm:^2.0.20"
     eventemitter3: "npm:^5.0.1"
-    log-update: "npm:^5.0.1"
+    log-update: "npm:^6.0.0"
     rfdc: "npm:^1.3.0"
-    wrap-ansi: "npm:^8.1.0"
-  checksum: 37b6501be84ebea66dcce07c5f86c224aff0c01c9fb43f5055cc38a063030281d58198aad0aad481f174438309831ddf5f763b890e820cd7b7b4f4a5dfa229c9
+    wrap-ansi: "npm:^9.0.0"
+  checksum: 6e356df9127c68b69186c927c993645223557e941a76b0bb210e35786aedc53f577df437251db804606ff37ac509c5d945289a84b3daee7fadf2e3dcb889ecc9
   languageName: node
   linkType: hard
 
@@ -11104,16 +11127,16 @@ __metadata:
   languageName: node
   linkType: hard
 
-"log-update@npm:^5.0.1":
-  version: 5.0.1
-  resolution: "log-update@npm:5.0.1"
+"log-update@npm:^6.0.0":
+  version: 6.0.0
+  resolution: "log-update@npm:6.0.0"
   dependencies:
-    ansi-escapes: "npm:^5.0.0"
+    ansi-escapes: "npm:^6.2.0"
     cli-cursor: "npm:^4.0.0"
-    slice-ansi: "npm:^5.0.0"
-    strip-ansi: "npm:^7.0.1"
-    wrap-ansi: "npm:^8.0.1"
-  checksum: 1050ea2027e80f32e132aace909987cb00c2719368c78b82ffca681a5b3f4020eeb5f4b4e310c47c35c6c36aff258c1d1bc51485ac44d6fdac9eb0a4275c539f
+    slice-ansi: "npm:^7.0.0"
+    strip-ansi: "npm:^7.1.0"
+    wrap-ansi: "npm:^9.0.0"
+  checksum: e0b3c3401ef49ce3eb17e2f83d644765e4f7988498fc1344eaa4f31ab30e510dcc469a7fb64dc01bd1c8d9237d917598fa677a9818705fb3774c10f6e9d4b27c
   languageName: node
   linkType: hard
 
@@ -15050,6 +15073,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"slice-ansi@npm:^7.0.0":
+  version: 7.1.0
+  resolution: "slice-ansi@npm:7.1.0"
+  dependencies:
+    ansi-styles: "npm:^6.2.1"
+    is-fullwidth-code-point: "npm:^5.0.0"
+  checksum: 631c971d4abf56cf880f034d43fcc44ff883624867bf11ecbd538c47343911d734a4656d7bc02362b40b89d765652a7f935595441e519b59e2ad3f4d5d6fe7ca
+  languageName: node
+  linkType: hard
+
 "smart-buffer@npm:^4.2.0":
   version: 4.2.0
   resolution: "smart-buffer@npm:4.2.0"
@@ -15493,7 +15526,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"string-width@npm:^5.0.0, string-width@npm:^5.0.1, string-width@npm:^5.1.2":
+"string-width@npm:^5.0.1, string-width@npm:^5.1.2":
   version: 5.1.2
   resolution: "string-width@npm:5.1.2"
   dependencies:
@@ -15504,6 +15537,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"string-width@npm:^7.0.0":
+  version: 7.0.0
+  resolution: "string-width@npm:7.0.0"
+  dependencies:
+    emoji-regex: "npm:^10.3.0"
+    get-east-asian-width: "npm:^1.0.0"
+    strip-ansi: "npm:^7.1.0"
+  checksum: 8ffaeeccf4a56ccce5b6235d0b99ee3a581e3e3e5d453708efe7aa8e264fa3a858b4fe2244310cb71c6a20d8c05921cedc8b2ccd88cbaad9f5c92051ff68edc6
+  languageName: node
+  linkType: hard
+
 "string.prototype.matchall@npm:^4.0.6, string.prototype.matchall@npm:^4.0.8":
   version: 4.0.8
   resolution: "string.prototype.matchall@npm:4.0.8"
@@ -15618,7 +15662,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"strip-ansi@npm:^7.0.1":
+"strip-ansi@npm:^7.0.1, strip-ansi@npm:^7.1.0":
   version: 7.1.0
   resolution: "strip-ansi@npm:7.1.0"
   dependencies:
@@ -16379,13 +16423,20 @@ __metadata:
   languageName: node
   linkType: hard
 
-"type-fest@npm:^1.0.1, type-fest@npm:^1.0.2, type-fest@npm:^1.2.1, type-fest@npm:^1.2.2":
+"type-fest@npm:^1.0.1, type-fest@npm:^1.2.1, type-fest@npm:^1.2.2":
   version: 1.4.0
   resolution: "type-fest@npm:1.4.0"
   checksum: a3c0f4ee28ff6ddf800d769eafafcdeab32efa38763c1a1b8daeae681920f6e345d7920bf277245235561d8117dab765cb5f829c76b713b4c9de0998a5397141
   languageName: node
   linkType: hard
 
+"type-fest@npm:^3.0.0":
+  version: 3.13.1
+  resolution: "type-fest@npm:3.13.1"
+  checksum: 547d22186f73a8c04590b70dcf63baff390078c75ea8acd366bbd510fd0646e348bd1970e47ecf795b7cff0b41d26e9c475c1fedd6ef5c45c82075fbf916b629
+  languageName: node
+  linkType: hard
+
 "type-is@npm:~1.6.18":
   version: 1.6.18
   resolution: "type-is@npm:1.6.18"
@@ -17613,7 +17664,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"wrap-ansi@npm:^8.0.1, wrap-ansi@npm:^8.1.0":
+"wrap-ansi@npm:^8.1.0":
   version: 8.1.0
   resolution: "wrap-ansi@npm:8.1.0"
   dependencies:
@@ -17624,6 +17675,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"wrap-ansi@npm:^9.0.0":
+  version: 9.0.0
+  resolution: "wrap-ansi@npm:9.0.0"
+  dependencies:
+    ansi-styles: "npm:^6.2.1"
+    string-width: "npm:^7.0.0"
+    strip-ansi: "npm:^7.1.0"
+  checksum: a139b818da9573677548dd463bd626a5a5286271211eb6e4e82f34a4f643191d74e6d4a9bb0a3c26ec90e6f904f679e0569674ac099ea12378a8b98e20706066
+  languageName: node
+  linkType: hard
+
 "wrappy@npm:1":
   version: 1.0.2
   resolution: "wrappy@npm:1.0.2"

From b4fef6c26fe63aeda60a6359a17a7f357b2c923e Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 4 Dec 2023 09:50:43 +0100
Subject: [PATCH 02/73] Update DefinitelyTyped types (non-major) (#28199)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 yarn.lock | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/yarn.lock b/yarn.lock
index 3ec023533..288d75925 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3495,13 +3495,13 @@ __metadata:
   linkType: hard
 
 "@types/react@npm:*, @types/react@npm:16 || 17 || 18, @types/react@npm:>=16.9.11, @types/react@npm:^18.2.7":
-  version: 18.2.38
-  resolution: "@types/react@npm:18.2.38"
+  version: 18.2.41
+  resolution: "@types/react@npm:18.2.41"
   dependencies:
     "@types/prop-types": "npm:*"
     "@types/scheduler": "npm:*"
     csstype: "npm:^3.0.2"
-  checksum: 56edd4756081b677e38ee23ad6d340658c5e2468785cb20968318cec357e1ea7ccf3ecd9534981741192dd1b894200acfaf0f1551b4795c6077668f6afc19345
+  checksum: 5cc72491ce8be95e7bbedd8bf039ca971772ecd22d989feb045af7e73247c7e6cff25a2f1c2200be461fb2f6b5aacef739e1ba9fd83c744209dfd3ce8aa75afe
   languageName: node
   linkType: hard
 
@@ -3657,11 +3657,11 @@ __metadata:
   linkType: hard
 
 "@types/ws@npm:^8.5.9":
-  version: 8.5.9
-  resolution: "@types/ws@npm:8.5.9"
+  version: 8.5.10
+  resolution: "@types/ws@npm:8.5.10"
   dependencies:
     "@types/node": "npm:*"
-  checksum: 678bdd6461c4653f2975c537fb673cb1918c331558e2d2422b69761c9ced67200bb07c664e2593f3864077a891cb7c13ef2a40d303b4aacb06173d095d8aa3ce
+  checksum: e9af279b984c4a04ab53295a40aa95c3e9685f04888df5c6920860d1dd073fcc57c7bd33578a04b285b2c655a0b52258d34bee0a20569dca8defb8393e1e5d29
   languageName: node
   linkType: hard
 

From 3ec263bf155c87dd5c513f181682e051e9f3e105 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 4 Dec 2023 09:51:08 +0100
Subject: [PATCH 03/73] Update dependency irb to v1.10.0 (#28198)

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 4c78e6b0a..6820c12f9 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -377,7 +377,7 @@ GEM
       terminal-table (>= 1.5.1)
     idn-ruby (0.1.5)
     io-console (0.6.0)
-    irb (1.9.1)
+    irb (1.10.0)
       rdoc
       reline (>= 0.3.8)
     jmespath (1.6.2)
@@ -747,7 +747,7 @@ GEM
     statsd-ruby (1.5.0)
     stoplight (3.0.2)
       redlock (~> 1.0)
-    stringio (3.0.9)
+    stringio (3.1.0)
     strong_migrations (1.6.4)
       activerecord (>= 5.2)
     swd (1.3.0)

From d848d8d87cbef49f5f4635b3378b582464bae98a Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 4 Dec 2023 03:52:21 -0500
Subject: [PATCH 04/73] Add helper methods for domains allow and export blocks
 files (#28196)

---
 .../admin/export_domain_allows_controller_spec.rb      | 10 ++++++++--
 .../admin/export_domain_blocks_controller_spec.rb      |  8 +++++++-
 2 files changed, 15 insertions(+), 3 deletions(-)

diff --git a/spec/controllers/admin/export_domain_allows_controller_spec.rb b/spec/controllers/admin/export_domain_allows_controller_spec.rb
index e1e5ecc1f..0a2e34262 100644
--- a/spec/controllers/admin/export_domain_allows_controller_spec.rb
+++ b/spec/controllers/admin/export_domain_allows_controller_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Admin::ExportDomainAllowsController do
 
       get :export, params: { format: :csv }
       expect(response).to have_http_status(200)
-      expect(response.body).to eq(File.read(File.join(file_fixture_path, 'domain_allows.csv')))
+      expect(response.body).to eq(domain_allows_csv_file)
     end
   end
 
@@ -40,7 +40,7 @@ RSpec.describe Admin::ExportDomainAllowsController do
       # Domains should now be added
       get :export, params: { format: :csv }
       expect(response).to have_http_status(200)
-      expect(response.body).to eq(File.read(File.join(file_fixture_path, 'domain_allows.csv')))
+      expect(response.body).to eq(domain_allows_csv_file)
     end
 
     it 'displays error on no file selected' do
@@ -49,4 +49,10 @@ RSpec.describe Admin::ExportDomainAllowsController do
       expect(flash[:error]).to eq(I18n.t('admin.export_domain_allows.no_file'))
     end
   end
+
+  private
+
+  def domain_allows_csv_file
+    File.read(File.join(file_fixture_path, 'domain_allows.csv'))
+  end
 end
diff --git a/spec/controllers/admin/export_domain_blocks_controller_spec.rb b/spec/controllers/admin/export_domain_blocks_controller_spec.rb
index 5a282c957..bfcccfa06 100644
--- a/spec/controllers/admin/export_domain_blocks_controller_spec.rb
+++ b/spec/controllers/admin/export_domain_blocks_controller_spec.rb
@@ -26,7 +26,13 @@ RSpec.describe Admin::ExportDomainBlocksController do
 
       get :export, params: { format: :csv }
       expect(response).to have_http_status(200)
-      expect(response.body).to eq(File.read(File.join(file_fixture_path, 'domain_blocks.csv')))
+      expect(response.body).to eq(domain_blocks_csv_file)
+    end
+
+    private
+
+    def domain_blocks_csv_file
+      File.read(File.join(file_fixture_path, 'domain_blocks.csv'))
     end
   end
 

From 154fb95e44da94d8473ecdc081cf8b9648f41a52 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 4 Dec 2023 09:52:53 +0100
Subject: [PATCH 05/73] Update dependency postcss to v8.4.32 (#28185)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 yarn.lock | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/yarn.lock b/yarn.lock
index 288d75925..4226fcc0f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11784,12 +11784,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"nanoid@npm:^3.3.6":
-  version: 3.3.6
-  resolution: "nanoid@npm:3.3.6"
+"nanoid@npm:^3.3.7":
+  version: 3.3.7
+  resolution: "nanoid@npm:3.3.7"
   bin:
     nanoid: bin/nanoid.cjs
-  checksum: 606b355960d0fcbe3d27924c4c52ef7d47d3b57208808ece73279420d91469b01ec1dce10fae512b6d4a8c5a5432b352b228336a8b2202a6ea68e67fa348e2ee
+  checksum: e3fb661aa083454f40500473bb69eedb85dc160e763150b9a2c567c7e9ff560ce028a9f833123b618a6ea742e311138b591910e795614a629029e86e180660f3
   languageName: node
   linkType: hard
 
@@ -13218,13 +13218,13 @@ __metadata:
   linkType: hard
 
 "postcss@npm:^8.2.15, postcss@npm:^8.4.24, postcss@npm:^8.4.28":
-  version: 8.4.31
-  resolution: "postcss@npm:8.4.31"
+  version: 8.4.32
+  resolution: "postcss@npm:8.4.32"
   dependencies:
-    nanoid: "npm:^3.3.6"
+    nanoid: "npm:^3.3.7"
     picocolors: "npm:^1.0.0"
     source-map-js: "npm:^1.0.2"
-  checksum: 748b82e6e5fc34034dcf2ae88ea3d11fd09f69b6c50ecdd3b4a875cfc7cdca435c958b211e2cb52355422ab6fccb7d8f2f2923161d7a1b281029e4a913d59acf
+  checksum: 39308a9195fa34d4dbdd7b58a896cff0c7809f84f7a4ac1b95b68ca86c9138a395addff33075668ed3983d41b90aac05754c445237a9365eb1c3a5602ebd03ad
   languageName: node
   linkType: hard
 

From 19ad51253d255c26ba71b0754c8b21197fcd0113 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 4 Dec 2023 04:02:40 -0500
Subject: [PATCH 06/73] Prevent triple-subject run in admin/domain_blocks spec
 (#28195)

---
 .../admin/domain_blocks_controller_spec.rb    | 40 ++++++++++---------
 1 file changed, 22 insertions(+), 18 deletions(-)

diff --git a/spec/controllers/admin/domain_blocks_controller_spec.rb b/spec/controllers/admin/domain_blocks_controller_spec.rb
index 13826be36..22960f531 100644
--- a/spec/controllers/admin/domain_blocks_controller_spec.rb
+++ b/spec/controllers/admin/domain_blocks_controller_spec.rb
@@ -192,16 +192,11 @@ RSpec.describe Admin::DomainBlocksController do
       let(:original_severity) { 'suspend' }
       let(:new_severity)      { 'silence' }
 
-      it 'changes the block severity' do
-        expect { subject }.to change { domain_block.reload.severity }.from('suspend').to('silence')
-      end
-
-      it 'undoes individual suspensions' do
-        expect { subject }.to change { remote_account.reload.suspended? }.from(true).to(false)
-      end
-
-      it 'performs individual silences' do
-        expect { subject }.to change { remote_account.reload.silenced? }.from(false).to(true)
+      it 'changes the block severity, suspensions, and silences' do
+        expect { subject }
+          .to change_severity('suspend', 'silence')
+          .and change_suspended(true, false)
+          .and change_silenced(false, true)
       end
     end
 
@@ -209,17 +204,26 @@ RSpec.describe Admin::DomainBlocksController do
       let(:original_severity) { 'silence' }
       let(:new_severity)      { 'suspend' }
 
-      it 'changes the block severity' do
-        expect { subject }.to change { domain_block.reload.severity }.from('silence').to('suspend')
+      it 'changes the block severity, silences, and suspensions' do
+        expect { subject }
+          .to change_severity('silence', 'suspend')
+          .and change_silenced(true, false)
+          .and change_suspended(false, true)
       end
+    end
 
-      it 'undoes individual silences' do
-        expect { subject }.to change { remote_account.reload.silenced? }.from(true).to(false)
-      end
+    private
 
-      it 'performs individual suspends' do
-        expect { subject }.to change { remote_account.reload.suspended? }.from(false).to(true)
-      end
+    def change_severity(from, to)
+      change { domain_block.reload.severity }.from(from).to(to)
+    end
+
+    def change_silenced(from, to)
+      change { remote_account.reload.silenced? }.from(from).to(to)
+    end
+
+    def change_suspended(from, to)
+      change { remote_account.reload.suspended? }.from(from).to(to)
     end
   end
 

From 1bf2230fd1a25a1f8c012e4ff70c08a0a48d88d3 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 4 Dec 2023 04:08:47 -0500
Subject: [PATCH 07/73] Add spec coverage for `CLI::Upgrade#storage_schema`
 command (#28180)

---
 spec/lib/mastodon/cli/upgrade_spec.rb | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/spec/lib/mastodon/cli/upgrade_spec.rb b/spec/lib/mastodon/cli/upgrade_spec.rb
index 817044f7e..0d6494eee 100644
--- a/spec/lib/mastodon/cli/upgrade_spec.rb
+++ b/spec/lib/mastodon/cli/upgrade_spec.rb
@@ -4,5 +4,24 @@ require 'rails_helper'
 require 'mastodon/cli/upgrade'
 
 describe Mastodon::CLI::Upgrade do
+  let(:cli) { described_class.new }
+
   it_behaves_like 'CLI Command'
+
+  describe '#storage_schema' do
+    context 'with records that dont need upgrading' do
+      let(:options) { {} }
+
+      before do
+        Fabricate(:account)
+        Fabricate(:media_attachment)
+      end
+
+      it 'does not upgrade storage for the attachments' do
+        expect { cli.invoke(:storage_schema, [], options) }.to output(
+          a_string_including('Upgraded storage schema of 0 records')
+        ).to_stdout
+      end
+    end
+  end
 end

From 9603198982fce7e064a4ec60ac36420173a91cd5 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 4 Dec 2023 04:09:05 -0500
Subject: [PATCH 08/73] Add spec coverage for `CLI::Domains#purge` command
 (#28179)

---
 spec/lib/mastodon/cli/domains_spec.rb | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/spec/lib/mastodon/cli/domains_spec.rb b/spec/lib/mastodon/cli/domains_spec.rb
index 765b63e2a..add754159 100644
--- a/spec/lib/mastodon/cli/domains_spec.rb
+++ b/spec/lib/mastodon/cli/domains_spec.rb
@@ -4,5 +4,22 @@ require 'rails_helper'
 require 'mastodon/cli/domains'
 
 describe Mastodon::CLI::Domains do
+  let(:cli) { described_class.new }
+
   it_behaves_like 'CLI Command'
+
+  describe '#purge' do
+    context 'with accounts from the domain' do
+      let(:options) { {} }
+      let(:domain) { 'host.example' }
+      let!(:account) { Fabricate(:account, domain: domain) }
+
+      it 'removes the account' do
+        expect { cli.invoke(:purge, [domain], options) }.to output(
+          a_string_including('Removed 1 accounts')
+        ).to_stdout
+        expect { account.reload }.to raise_error(ActiveRecord::RecordNotFound)
+      end
+    end
+  end
 end

From a2bcfeb887a0ae1d437bb727333d769a8248b578 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Mon, 4 Dec 2023 10:09:43 +0100
Subject: [PATCH 09/73] Fix `Style/HashEachMethods` cop in HAML files (#28178)

---
 app/views/admin/reports/index.html.haml           | 2 +-
 app/views/settings/applications/_fields.html.haml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml
index e94847d67..e2a9868aa 100644
--- a/app/views/admin/reports/index.html.haml
+++ b/app/views/admin/reports/index.html.haml
@@ -27,7 +27,7 @@
       %button.button= t('admin.accounts.search')
       = link_to t('admin.accounts.reset'), admin_reports_path, class: 'button negative'
 
-- @reports.group_by(&:target_account_id).each do |_target_account_id, reports|
+- @reports.group_by(&:target_account_id).each_value do |reports|
   - target_account = reports.first.target_account
   .report-card
     .report-card__profile
diff --git a/app/views/settings/applications/_fields.html.haml b/app/views/settings/applications/_fields.html.haml
index 4f5077d83..29e2bcb5a 100644
--- a/app/views/settings/applications/_fields.html.haml
+++ b/app/views/settings/applications/_fields.html.haml
@@ -14,5 +14,5 @@
     %label= t('activerecord.attributes.doorkeeper/application.scopes')
     %span.hint= t('simple_form.hints.defaults.scopes')
 
-  - Doorkeeper.configuration.scopes.group_by { |s| s.split(':').first }.each do |_key, value|
+  - Doorkeeper.configuration.scopes.group_by { |s| s.split(':').first }.each_value do |value|
     = f.input :scopes, label: false, hint: false, collection: value.sort, wrapper: :with_block_label, include_blank: false, label_method: ->(scope) { safe_join([content_tag(:samp, scope, class: class_for_scope(scope)), content_tag(:span, t("doorkeeper.scopes.#{scope}"), class: 'hint')]) }, selected: f.object.scopes.all, required: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'

From 829457212ee2ae933db273b8dce402d2d8e659cf Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 4 Dec 2023 10:38:49 +0100
Subject: [PATCH 10/73] Update dependency rubocop to v1.58.0 (#28170)

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 6820c12f9..037fe145d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -381,7 +381,7 @@ GEM
       rdoc
       reline (>= 0.3.8)
     jmespath (1.6.2)
-    json (2.6.3)
+    json (2.7.0)
     json-canonicalization (0.3.2)
     json-jwt (1.15.3)
       activesupport (>= 4.2)
@@ -656,7 +656,7 @@ GEM
       rspec-mocks (~> 3.0)
       sidekiq (>= 5, < 8)
     rspec-support (3.12.1)
-    rubocop (1.57.2)
+    rubocop (1.58.0)
       json (~> 2.3)
       language_server-protocol (>= 3.17.0)
       parallel (~> 1.10)
@@ -664,7 +664,7 @@ GEM
       rainbow (>= 2.2.2, < 4.0)
       regexp_parser (>= 1.8, < 3.0)
       rexml (>= 3.2.5, < 4.0)
-      rubocop-ast (>= 1.28.1, < 2.0)
+      rubocop-ast (>= 1.30.0, < 2.0)
       ruby-progressbar (~> 1.7)
       unicode-display_width (>= 2.4.0, < 3.0)
     rubocop-ast (1.30.0)

From b3b009e6aa29d9051aa61113da344b3ab26d98e4 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 4 Dec 2023 04:44:54 -0500
Subject: [PATCH 11/73] Add spec coverage for `CLI::EmailDomainBlocks` commands
 (#28181)

---
 lib/mastodon/cli/email_domain_blocks.rb       |  4 +-
 .../mastodon/cli/email_domain_blocks_spec.rb  | 93 +++++++++++++++++++
 2 files changed, 95 insertions(+), 2 deletions(-)

diff --git a/lib/mastodon/cli/email_domain_blocks.rb b/lib/mastodon/cli/email_domain_blocks.rb
index 88a84ecb4..022b1dcbb 100644
--- a/lib/mastodon/cli/email_domain_blocks.rb
+++ b/lib/mastodon/cli/email_domain_blocks.rb
@@ -7,10 +7,10 @@ module Mastodon::CLI
   class EmailDomainBlocks < Base
     desc 'list', 'List blocked e-mail domains'
     def list
-      EmailDomainBlock.where(parent_id: nil).order(id: 'DESC').find_each do |entry|
+      EmailDomainBlock.where(parent_id: nil).find_each do |entry|
         say(entry.domain.to_s, :white)
 
-        EmailDomainBlock.where(parent_id: entry.id).order(id: 'DESC').find_each do |child|
+        EmailDomainBlock.where(parent_id: entry.id).find_each do |child|
           say("  #{child.domain}", :cyan)
         end
       end
diff --git a/spec/lib/mastodon/cli/email_domain_blocks_spec.rb b/spec/lib/mastodon/cli/email_domain_blocks_spec.rb
index 060943b18..f5cb6c332 100644
--- a/spec/lib/mastodon/cli/email_domain_blocks_spec.rb
+++ b/spec/lib/mastodon/cli/email_domain_blocks_spec.rb
@@ -4,5 +4,98 @@ require 'rails_helper'
 require 'mastodon/cli/email_domain_blocks'
 
 describe Mastodon::CLI::EmailDomainBlocks do
+  let(:cli) { described_class.new }
+
   it_behaves_like 'CLI Command'
+
+  describe '#list' do
+    context 'with email domain block records' do
+      let!(:parent_block) { Fabricate(:email_domain_block) }
+      let!(:child_block) { Fabricate(:email_domain_block, parent: parent_block) }
+      let(:options) { {} }
+
+      it 'lists the blocks' do
+        expect { cli.invoke(:list, [], options) }.to output(
+          a_string_including(parent_block.domain)
+          .and(a_string_including(child_block.domain))
+        ).to_stdout
+      end
+    end
+  end
+
+  describe '#add' do
+    context 'without any options' do
+      let(:options) { {} }
+
+      it 'warns about usage and exits' do
+        expect { cli.invoke(:add, [], options) }.to output(
+          a_string_including('No domain(s) given')
+        ).to_stdout.and raise_error(SystemExit)
+      end
+    end
+
+    context 'when blocks exist' do
+      let(:options) { {} }
+      let(:domain) { 'host.example' }
+
+      before { Fabricate(:email_domain_block, domain: domain) }
+
+      it 'does not add a new block' do
+        expect { cli.invoke(:add, [domain], options) }.to output(
+          a_string_including('is already blocked')
+        ).to_stdout
+          .and(not_change(EmailDomainBlock, :count))
+      end
+    end
+
+    context 'when no blocks exist' do
+      let(:options) { {} }
+      let(:domain) { 'host.example' }
+
+      it 'adds a new block' do
+        expect { cli.invoke(:add, [domain], options) }.to output(
+          a_string_including('Added 1')
+        ).to_stdout
+          .and(change(EmailDomainBlock, :count).by(1))
+      end
+    end
+  end
+
+  describe '#remove' do
+    context 'without any options' do
+      let(:options) { {} }
+
+      it 'warns about usage and exits' do
+        expect { cli.invoke(:remove, [], options) }.to output(
+          a_string_including('No domain(s) given')
+        ).to_stdout.and raise_error(SystemExit)
+      end
+    end
+
+    context 'when blocks exist' do
+      let(:options) { {} }
+      let(:domain) { 'host.example' }
+
+      before { Fabricate(:email_domain_block, domain: domain) }
+
+      it 'removes the block' do
+        expect { cli.invoke(:remove, [domain], options) }.to output(
+          a_string_including('Removed 1')
+        ).to_stdout
+          .and(change(EmailDomainBlock, :count).by(-1))
+      end
+    end
+
+    context 'when no blocks exist' do
+      let(:options) { {} }
+      let(:domain) { 'host.example' }
+
+      it 'does not remove a block' do
+        expect { cli.invoke(:remove, [domain], options) }.to output(
+          a_string_including('is not yet blocked')
+        ).to_stdout
+          .and(not_change(EmailDomainBlock, :count))
+      end
+    end
+  end
 end

From cca19f5fbb568bf7f145fe98d6d2497632c8987c Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 4 Dec 2023 07:56:28 -0500
Subject: [PATCH 12/73] Use the `Admin::ActionLog` fabricator in
 admin/action_logs spec (#28194)

---
 .../admin/accounts_controller_spec.rb         | 32 ++++++++++++-------
 .../admin/action_logs_controller_spec.rb      | 31 +++++++++++++++---
 .../api/v1/admin/account_actions_spec.rb      | 17 +++++++---
 spec/requests/api/v1/admin/accounts_spec.rb   | 32 ++++++++++++-------
 4 files changed, 78 insertions(+), 34 deletions(-)

diff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb
index 307e81950..1882ea838 100644
--- a/spec/controllers/admin/accounts_controller_spec.rb
+++ b/spec/controllers/admin/accounts_controller_spec.rb
@@ -161,12 +161,13 @@ RSpec.describe Admin::AccountsController do
       it 'logs action' do
         expect(subject).to have_http_status 302
 
-        log_item = Admin::ActionLog.last
-
-        expect(log_item).to_not be_nil
-        expect(log_item.action).to eq :approve
-        expect(log_item.account_id).to eq current_user.account_id
-        expect(log_item.target_id).to eq account.user.id
+        expect(latest_admin_action_log)
+          .to be_present
+          .and have_attributes(
+            action: eq(:approve),
+            account_id: eq(current_user.account_id),
+            target_id: eq(account.user.id)
+          )
       end
     end
 
@@ -201,12 +202,13 @@ RSpec.describe Admin::AccountsController do
       it 'logs action' do
         expect(subject).to have_http_status 302
 
-        log_item = Admin::ActionLog.last
-
-        expect(log_item).to_not be_nil
-        expect(log_item.action).to eq :reject
-        expect(log_item.account_id).to eq current_user.account_id
-        expect(log_item.target_id).to eq account.user.id
+        expect(latest_admin_action_log)
+          .to be_present
+          .and have_attributes(
+            action: eq(:reject),
+            account_id: eq(current_user.account_id),
+            target_id: eq(account.user.id)
+          )
       end
     end
 
@@ -427,4 +429,10 @@ RSpec.describe Admin::AccountsController do
       end
     end
   end
+
+  private
+
+  def latest_admin_action_log
+    Admin::ActionLog.last
+  end
 end
diff --git a/spec/controllers/admin/action_logs_controller_spec.rb b/spec/controllers/admin/action_logs_controller_spec.rb
index b7854469d..be4222df0 100644
--- a/spec/controllers/admin/action_logs_controller_spec.rb
+++ b/spec/controllers/admin/action_logs_controller_spec.rb
@@ -9,11 +9,9 @@ describe Admin::ActionLogsController do
   let!(:account) { Fabricate(:account) }
 
   before do
-    _orphaned_logs = %w(
-      Account User UserRole Report DomainBlock DomainAllow
-      EmailDomainBlock UnavailableDomain Status AccountWarning
-      Announcement IpBlock Instance CustomEmoji CanonicalEmailBlock Appeal
-    ).map { |type| Admin::ActionLog.new(account: account, action: 'destroy', target_type: type, target_id: 1312).save! }
+    orphaned_log_types.map do |type|
+      Fabricate(:action_log, account: account, action: 'destroy', target_type: type, target_id: 1312)
+    end
   end
 
   describe 'GET #index' do
@@ -24,4 +22,27 @@ describe Admin::ActionLogsController do
       expect(response).to have_http_status(200)
     end
   end
+
+  private
+
+  def orphaned_log_types
+    %w(
+      Account
+      AccountWarning
+      Announcement
+      Appeal
+      CanonicalEmailBlock
+      CustomEmoji
+      DomainAllow
+      DomainBlock
+      EmailDomainBlock
+      Instance
+      IpBlock
+      Report
+      Status
+      UnavailableDomain
+      User
+      UserRole
+    )
+  end
 end
diff --git a/spec/requests/api/v1/admin/account_actions_spec.rb b/spec/requests/api/v1/admin/account_actions_spec.rb
index c14e08c21..4167911a1 100644
--- a/spec/requests/api/v1/admin/account_actions_spec.rb
+++ b/spec/requests/api/v1/admin/account_actions_spec.rb
@@ -21,12 +21,19 @@ RSpec.describe 'Account actions' do
     it 'logs action' do
       subject
 
-      log_item = Admin::ActionLog.last
+      expect(latest_admin_action_log)
+        .to be_present
+        .and have_attributes(
+          action: eq(action_type),
+          account_id: eq(user.account_id),
+          target_id: eq(target_type == :user ? target_account.user.id : target_account.id)
+        )
+    end
 
-      expect(log_item).to be_present
-      expect(log_item.action).to eq(action_type)
-      expect(log_item.account_id).to eq(user.account_id)
-      expect(log_item.target_id).to eq(target_type == :user ? target_account.user.id : target_account.id)
+    private
+
+    def latest_admin_action_log
+      Admin::ActionLog.last
     end
   end
 
diff --git a/spec/requests/api/v1/admin/accounts_spec.rb b/spec/requests/api/v1/admin/accounts_spec.rb
index 8e158f623..1615581f0 100644
--- a/spec/requests/api/v1/admin/accounts_spec.rb
+++ b/spec/requests/api/v1/admin/accounts_spec.rb
@@ -151,12 +151,13 @@ RSpec.describe 'Accounts' do
       it 'logs action', :aggregate_failures do
         subject
 
-        log_item = Admin::ActionLog.last
-
-        expect(log_item).to be_present
-        expect(log_item.action).to eq :approve
-        expect(log_item.account_id).to eq user.account_id
-        expect(log_item.target_id).to eq account.user.id
+        expect(latest_admin_action_log)
+          .to be_present
+          .and have_attributes(
+            action: eq(:approve),
+            account_id: eq(user.account_id),
+            target_id: eq(account.user.id)
+          )
       end
     end
 
@@ -202,12 +203,13 @@ RSpec.describe 'Accounts' do
       it 'logs action', :aggregate_failures do
         subject
 
-        log_item = Admin::ActionLog.last
-
-        expect(log_item).to be_present
-        expect(log_item.action).to eq :reject
-        expect(log_item.account_id).to eq user.account_id
-        expect(log_item.target_id).to eq account.user.id
+        expect(latest_admin_action_log)
+          .to be_present
+          .and have_attributes(
+            action: eq(:reject),
+            account_id: eq(user.account_id),
+            target_id: eq(account.user.id)
+          )
       end
     end
 
@@ -398,4 +400,10 @@ RSpec.describe 'Accounts' do
       end
     end
   end
+
+  private
+
+  def latest_admin_action_log
+    Admin::ActionLog.last
+  end
 end

From 71e5a16ebaf67d5e55d7f4e31436c36e6801ce5a Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 4 Dec 2023 10:28:19 -0500
Subject: [PATCH 13/73] Remove triple subject call in `api/v1/lists` spec
 (#28210)

---
 spec/requests/api/v1/lists_spec.rb | 19 +++++++++++--------
 1 file changed, 11 insertions(+), 8 deletions(-)

diff --git a/spec/requests/api/v1/lists_spec.rb b/spec/requests/api/v1/lists_spec.rb
index 22dde43a1..4635e936f 100644
--- a/spec/requests/api/v1/lists_spec.rb
+++ b/spec/requests/api/v1/lists_spec.rb
@@ -135,8 +135,11 @@ RSpec.describe 'Lists' do
 
     it_behaves_like 'forbidden for wrong scope', 'read read:lists'
 
-    it 'returns the updated list', :aggregate_failures do
-      subject
+    it 'returns the updated list and updates values', :aggregate_failures do
+      expect { subject }
+        .to change_list_title
+        .and change_list_replies_policy
+        .and change_list_exclusive
 
       expect(response).to have_http_status(200)
       list.reload
@@ -149,16 +152,16 @@ RSpec.describe 'Lists' do
       })
     end
 
-    it 'updates the list title' do
-      expect { subject }.to change { list.reload.title }.from('my list').to('list')
+    def change_list_title
+      change { list.reload.title }.from('my list').to('list')
     end
 
-    it 'updates the list replies_policy' do
-      expect { subject }.to change { list.reload.replies_policy }.from('list').to('followed')
+    def change_list_replies_policy
+      change { list.reload.replies_policy }.from('list').to('followed')
     end
 
-    it 'updates the list exclusive' do
-      expect { subject }.to change { list.reload.exclusive }.from(false).to(true)
+    def change_list_exclusive
+      change { list.reload.exclusive }.from(false).to(true)
     end
 
     context 'when the list does not exist' do

From 89a8e6e6227eb901b5811d8417d81dc8ab1427a9 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 4 Dec 2023 10:41:43 -0500
Subject: [PATCH 14/73] Remove 2x double subject call in
 `models/form/account_batch` spec (#28209)

---
 spec/models/form/account_batch_spec.rb | 40 +++++++++++++++++++-------
 1 file changed, 29 insertions(+), 11 deletions(-)

diff --git a/spec/models/form/account_batch_spec.rb b/spec/models/form/account_batch_spec.rb
index fd8e90901..26fb1b953 100644
--- a/spec/models/form/account_batch_spec.rb
+++ b/spec/models/form/account_batch_spec.rb
@@ -37,12 +37,10 @@ RSpec.describe Form::AccountBatch do
         let(:select_all_matching) { '0' }
         let(:account_ids)         { [target_account.id, target_account2.id] }
 
-        it 'suspends the expected users' do
-          expect { subject }.to change { [target_account.reload.suspended?, target_account2.reload.suspended?] }.from([false, false]).to([true, true])
-        end
-
-        it 'closes open reports targeting the suspended users' do
-          expect { subject }.to change { Report.unresolved.where(target_account: [target_account, target_account2]).count }.from(2).to(0)
+        it 'suspends the expected users and closes open reports' do
+          expect { subject }
+            .to change_account_suspensions
+            .and change_open_reports_for_accounts
         end
       end
 
@@ -50,13 +48,33 @@ RSpec.describe Form::AccountBatch do
         let(:select_all_matching) { '1' }
         let(:query)               { Account.where(id: [target_account.id, target_account2.id]) }
 
-        it 'suspends the expected users' do
-          expect { subject }.to change { [target_account.reload.suspended?, target_account2.reload.suspended?] }.from([false, false]).to([true, true])
+        it 'suspends the expected users and closes open reports' do
+          expect { subject }
+            .to change_account_suspensions
+            .and change_open_reports_for_accounts
         end
+      end
 
-        it 'closes open reports targeting the suspended users' do
-          expect { subject }.to change { Report.unresolved.where(target_account: [target_account, target_account2]).count }.from(2).to(0)
-        end
+      private
+
+      def change_account_suspensions
+        change { relevant_account_suspension_statuses }
+          .from([false, false])
+          .to([true, true])
+      end
+
+      def change_open_reports_for_accounts
+        change(relevant_account_unresolved_reports, :count)
+          .from(2)
+          .to(0)
+      end
+
+      def relevant_account_unresolved_reports
+        Report.unresolved.where(target_account: [target_account, target_account2])
+      end
+
+      def relevant_account_suspension_statuses
+        [target_account.reload, target_account2.reload].map(&:suspended?)
       end
     end
   end

From f944a767c46e3d887e070d67958f4a9866c3d1f1 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 5 Dec 2023 13:57:16 +0100
Subject: [PATCH 15/73] Update dependency json-ld to v3.3.1 (#28229)

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 037fe145d..602df6a22 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -382,15 +382,15 @@ GEM
       reline (>= 0.3.8)
     jmespath (1.6.2)
     json (2.7.0)
-    json-canonicalization (0.3.2)
+    json-canonicalization (1.0.0)
     json-jwt (1.15.3)
       activesupport (>= 4.2)
       aes_key_wrap
       bindata
       httpclient
-    json-ld (3.3.0)
+    json-ld (3.3.1)
       htmlentities (~> 4.3)
-      json-canonicalization (~> 0.3, >= 0.3.2)
+      json-canonicalization (~> 1.0)
       link_header (~> 0.0, >= 0.0.8)
       multi_json (~> 1.15)
       rack (>= 2.2, < 4)

From 2d2e23c68dde6ac6fa14fe8fbeb0efac02ad9803 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 5 Dec 2023 12:59:31 +0000
Subject: [PATCH 16/73] Update dependency brakeman to v6.1.0 (#28231)

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 602df6a22..9308a41c8 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -175,7 +175,7 @@ GEM
     blurhash (0.1.7)
     bootsnap (1.17.0)
       msgpack (~> 1.2)
-    brakeman (6.0.1)
+    brakeman (6.1.0)
     browser (5.3.1)
     brpoplpush-redis_script (0.1.3)
       concurrent-ruby (~> 1.0, >= 1.0.5)

From d0a5ebf914f7ad74a69beb15d9c011e44e84c0dc Mon Sep 17 00:00:00 2001
From: Jonathan de Jong <jonathandejong02@gmail.com>
Date: Tue, 5 Dec 2023 14:59:15 +0100
Subject: [PATCH 17/73] Fix error when encountering malformed Tag objects from
 Kbin (#28235)

---
 app/services/activitypub/process_status_update_service.rb | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb
index 2db0e80e7..fb2b33114 100644
--- a/app/services/activitypub/process_status_update_service.rb
+++ b/app/services/activitypub/process_status_update_service.rb
@@ -170,9 +170,9 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
 
     as_array(@json['tag']).each do |tag|
       if equals_or_includes?(tag['type'], 'Hashtag')
-        @raw_tags << tag['name']
+        @raw_tags << tag['name'] if tag['name'].present?
       elsif equals_or_includes?(tag['type'], 'Mention')
-        @raw_mentions << tag['href']
+        @raw_mentions << tag['href'] if tag['href'].present?
       elsif equals_or_includes?(tag['type'], 'Emoji')
         @raw_emojis << tag
       end

From 4238ec844d61774a02daa4a677a69099ff494388 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue, 5 Dec 2023 17:07:53 +0100
Subject: [PATCH 18/73] New Crowdin Translations (automated) (#28120)

Co-authored-by: GitHub Actions <noreply@github.com>
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
---
 app/javascript/mastodon/locales/an.json    |   1 +
 app/javascript/mastodon/locales/ca.json    |   1 +
 app/javascript/mastodon/locales/es-MX.json |   2 +-
 app/javascript/mastodon/locales/fa.json    |   7 +-
 app/javascript/mastodon/locales/fil.json   |   1 +
 app/javascript/mastodon/locales/ko.json    |   4 +-
 app/javascript/mastodon/locales/lt.json    |  74 ++++++++++++++
 app/javascript/mastodon/locales/ne.json    |   1 +
 app/javascript/mastodon/locales/ry.json    |   1 +
 app/javascript/mastodon/locales/sc.json    |  36 +++++++
 app/javascript/mastodon/locales/sk.json    |  10 ++
 app/javascript/mastodon/locales/sq.json    |   1 +
 app/javascript/mastodon/locales/th.json    |  14 +--
 app/javascript/mastodon/locales/tlh.json   |   1 +
 app/javascript/mastodon/locales/zh-TW.json |   2 +-
 config/i18n-tasks.yml                      |   1 +
 config/locales/activerecord.fil.yml        |   1 +
 config/locales/activerecord.ne.yml         |   1 +
 config/locales/activerecord.ry.yml         |   1 +
 config/locales/activerecord.tlh.yml        |   1 +
 config/locales/devise.fil.yml              |   1 +
 config/locales/devise.ne.yml               |   1 +
 config/locales/devise.ry.yml               |   1 +
 config/locales/devise.tlh.yml              |   1 +
 config/locales/doorkeeper.fil.yml          |   1 +
 config/locales/doorkeeper.ne.yml           |   1 +
 config/locales/doorkeeper.ry.yml           |   1 +
 config/locales/doorkeeper.sc.yml           |  15 +++
 config/locales/doorkeeper.tlh.yml          |   1 +
 config/locales/fil.yml                     |   1 +
 config/locales/fr-QC.yml                   |   1 +
 config/locales/fr.yml                      |   1 +
 config/locales/lt.yml                      |  26 ++++-
 config/locales/ne.yml                      |   1 +
 config/locales/ry.yml                      |   1 +
 config/locales/sc.yml                      | 113 +++++++++++++++++++++
 config/locales/simple_form.fil.yml         |   1 +
 config/locales/simple_form.lt.yml          |  35 ++++++-
 config/locales/simple_form.ne.yml          |   1 +
 config/locales/simple_form.ry.yml          |   1 +
 config/locales/simple_form.sc.yml          |  17 ++++
 config/locales/simple_form.tlh.yml         |   1 +
 config/locales/tlh.yml                     |   1 +
 43 files changed, 366 insertions(+), 19 deletions(-)
 create mode 100644 app/javascript/mastodon/locales/fil.json
 create mode 100644 app/javascript/mastodon/locales/ne.json
 create mode 100644 app/javascript/mastodon/locales/ry.json
 create mode 100644 app/javascript/mastodon/locales/tlh.json
 create mode 100644 config/locales/activerecord.fil.yml
 create mode 100644 config/locales/activerecord.ne.yml
 create mode 100644 config/locales/activerecord.ry.yml
 create mode 100644 config/locales/activerecord.tlh.yml
 create mode 100644 config/locales/devise.fil.yml
 create mode 100644 config/locales/devise.ne.yml
 create mode 100644 config/locales/devise.ry.yml
 create mode 100644 config/locales/devise.tlh.yml
 create mode 100644 config/locales/doorkeeper.fil.yml
 create mode 100644 config/locales/doorkeeper.ne.yml
 create mode 100644 config/locales/doorkeeper.ry.yml
 create mode 100644 config/locales/doorkeeper.tlh.yml
 create mode 100644 config/locales/fil.yml
 create mode 100644 config/locales/ne.yml
 create mode 100644 config/locales/ry.yml
 create mode 100644 config/locales/simple_form.fil.yml
 create mode 100644 config/locales/simple_form.ne.yml
 create mode 100644 config/locales/simple_form.ry.yml
 create mode 100644 config/locales/simple_form.tlh.yml
 create mode 100644 config/locales/tlh.yml

diff --git a/app/javascript/mastodon/locales/an.json b/app/javascript/mastodon/locales/an.json
index a652272fa..b2134551b 100644
--- a/app/javascript/mastodon/locales/an.json
+++ b/app/javascript/mastodon/locales/an.json
@@ -499,6 +499,7 @@
   "report_notification.open": "Ubrir informe",
   "search.placeholder": "Buscar",
   "search.search_or_paste": "Buscar u apegar URL",
+  "search_popout.full_text_search_logged_out_message": "Nomás disponible iniciando la sesión.",
   "search_results.all": "Totz",
   "search_results.hashtags": "Etiquetas",
   "search_results.nothing_found": "No se podió trobar cosa pa estes termins de busqueda",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 87121b7c5..5ae49325f 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -482,6 +482,7 @@
   "onboarding.follows.lead": "La teva línia de temps inici només està a les teves mans. Com més gent segueixis, més activa i interessant serà. Aquests perfils poden ser un bon punt d'inici—sempre pots acabar deixant de seguir-los!:",
   "onboarding.follows.title": "Personalitza la pantalla d'inci",
   "onboarding.profile.discoverable": "Fes el meu perfil descobrible",
+  "onboarding.profile.discoverable_hint": "En acceptar d'ésser descobert a Mastodon els teus missatges poden aparèixer dins les tendències i els resultats de cerques, i el teu perfil es pot suggerir a qui tingui interessos semblants als teus.",
   "onboarding.profile.display_name": "Nom que es mostrarà",
   "onboarding.profile.display_name_hint": "El teu nom complet o el teu malnom…",
   "onboarding.profile.lead": "Sempre ho pots completar més endavant a la configuració, on hi ha encara més opcions disponibles.",
diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json
index f7cd1b330..0d26afef2 100644
--- a/app/javascript/mastodon/locales/es-MX.json
+++ b/app/javascript/mastodon/locales/es-MX.json
@@ -606,7 +606,7 @@
   "search.quick_action.status_search": "Publicaciones que coinciden con {x}",
   "search.search_or_paste": "Buscar o pegar URL",
   "search_popout.full_text_search_disabled_message": "No disponible en {domain}.",
-  "search_popout.full_text_search_logged_out_message": "Solo disponible si inicias sesión.",
+  "search_popout.full_text_search_logged_out_message": "Sólo disponible al iniciar sesión.",
   "search_popout.language_code": "Código de idioma ISO",
   "search_popout.options": "Opciones de búsqueda",
   "search_popout.quick_actions": "Acciones rápidas",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index 6951d5cb0..8e8930bfe 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -1,7 +1,7 @@
 {
   "about.blocks": "کارسازهای نظارت شده",
   "about.contact": "تماس:",
-  "about.disclaimer": "ماستودون نرم‌افزار آزاد و یک شرکت غیر انتفاعی آلمانی با مسئولیت محدود است.",
+  "about.disclaimer": "ماستودون نرم‌افزار آزاد و نشان تجاری یک شرکت غیر انتفاعی با مسئولیت محدود آلمانی است.",
   "about.domain_blocks.no_reason_available": "دلیلی موجود نیست",
   "about.domain_blocks.preamble": "ماستودون عموماً می‌گذارد محتوا را از از هر کارساز دیگری در دنیای شبکه‌های اجتماعی غیرمتمرکز دیده و با آنان برهم‌کنش داشته باشید. این‌ها استثناهایی هستند که روی این کارساز خاص وضع شده‌اند.",
   "about.domain_blocks.silenced.explanation": "عموماً نمایه‌ها و محتوا از این کارساز را نمی‌بینید، مگر این که به طور خاص دنبالشان گشته یا با پی گیری، داوطلب دیدنشان شوید.",
@@ -21,6 +21,7 @@
   "account.blocked": "مسدود",
   "account.browse_more_on_origin_server": "مرور بیش‌تر روی نمایهٔ اصلی",
   "account.cancel_follow_request": "رد کردن درخواست پی‌گیری",
+  "account.copy": "رونوشت از پیوند به نمایه",
   "account.direct": "اشارهٔ خصوصی به ‪@{name}‬",
   "account.disable_notifications": "آگاه کردن من هنگام فرسته‌های ‎@{name} را متوقّف کن",
   "account.domain_blocked": "دامنه مسدود شد",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "علامت‌گذاری به عنوان خوانده شده",
   "conversation.open": "دیدن گفتگو",
   "conversation.with": "با {names}",
+  "copy_icon_button.copied": "در بریده‌دان رونوشت شد",
   "copypaste.copied": "رونوشت شد",
   "copypaste.copy_to_clipboard": "رونوشت به تخته‌گیره",
   "directory.federated": "از کارسازهای شناخته‌شده",
@@ -486,6 +488,8 @@
   "onboarding.profile.note_hint": "می‌توانید افراد دیگر را @نام‌بردن یا #برچسب بزنید…",
   "onboarding.profile.save_and_continue": "ذخیره کن و ادامه بده",
   "onboarding.profile.title": "تنظیم نمایه",
+  "onboarding.profile.upload_avatar": "بازگذاری تصویر نمایه",
+  "onboarding.profile.upload_header": "بارگذاری تصویر سردر نمایه",
   "onboarding.share.lead": "بگذارید افراد بدانند چگونه می‌توانند در ماستادون بیابندتان!",
   "onboarding.share.message": "من {username} روی #ماستودون هستم! مرا در {url} پی‌بگیرید",
   "onboarding.share.next_steps": "گام‌های ممکن بعدی:",
@@ -600,6 +604,7 @@
   "search.quick_action.status_search": "فرسته‌های جور با {x}",
   "search.search_or_paste": "جست‌وجو یا جایگذاری نشانی",
   "search_popout.full_text_search_disabled_message": "روی {domain} موجود نیست.",
+  "search_popout.full_text_search_logged_out_message": "تنها زمانی که وارد شده‌اید دردسترس است.",
   "search_popout.language_code": "کد زبان ایزو",
   "search_popout.options": "گزینه‌های جست‌وجو",
   "search_popout.quick_actions": "کنش‌های سریع",
diff --git a/app/javascript/mastodon/locales/fil.json b/app/javascript/mastodon/locales/fil.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/app/javascript/mastodon/locales/fil.json
@@ -0,0 +1 @@
+{}
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 46caf32b0..5b76cd67c 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -302,8 +302,8 @@
   "hashtag.counter_by_accounts": "{count, plural, other {{counter} 명의 참여자}}",
   "hashtag.counter_by_uses": "{count, plural, other {{counter} 개의 게시물}}",
   "hashtag.counter_by_uses_today": "오늘 {count, plural, 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/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json
index a976d5907..e588b8538 100644
--- a/app/javascript/mastodon/locales/lt.json
+++ b/app/javascript/mastodon/locales/lt.json
@@ -180,29 +180,71 @@
   "confirmations.logout.message": "Ar tikrai nori atsijungti?",
   "confirmations.mute.confirm": "Nutildyti",
   "confirmations.mute.explanation": "Tai paslėps jų įrašus ir įrašus, kuriuose jie menėmi, tačiau jie vis tiek galės matyti tavo įrašus ir sekti.",
+  "confirmations.mute.message": "Ar tikrai norite nutildyti {name}?",
+  "confirmations.redraft.confirm": "Ištrinti ir perrašyti",
   "confirmations.reply.confirm": "Atsakyti",
   "confirmations.reply.message": "Atsakant dabar, bus perrašyta metu kuriama žinutė. Ar tikrai nori tęsti?",
   "confirmations.unfollow.confirm": "Nebesekti",
+  "confirmations.unfollow.message": "Ar tikrai norite atsisakyti sekimo {name}?",
+  "conversation.delete": "Ištrinti pokalbį",
   "conversation.mark_as_read": "Žymėti kaip skaitytą",
   "conversation.open": "Peržiūrėti pokalbį",
   "conversation.with": "Su {names}",
   "copy_icon_button.copied": "Nukopijuota į iškarpinę",
   "copypaste.copied": "Nukopijuota",
   "copypaste.copy_to_clipboard": "Kopijuoti į iškarpinę",
+  "directory.local": "Iš {domain} tik",
+  "directory.new_arrivals": "Naujos prekės",
+  "directory.recently_active": "Neseniai aktyvus",
   "disabled_account_banner.account_settings": "Paskyros nustatymai",
+  "disabled_account_banner.text": "Jūsų paskyra {disabledAccount} šiuo metu yra išjungta.",
+  "dismissable_banner.dismiss": "Atmesti",
   "dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralized network right now.",
+  "dismissable_banner.explore_statuses": "Tai įrašai iš viso socialinio tinklo, kurie šiandien sulaukia vis daugiau dėmesio. Naujesni įrašai, turintys daugiau boosts ir mėgstamiausių įrašų, yra vertinami aukščiau.",
   "dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.",
   "embed.instructions": "Embed this status on your website by copying the code below.",
   "embed.preview": "Štai kaip tai atrodys:",
+  "emoji_button.activity": "Veikla",
+  "emoji_button.clear": "Išvalyti",
+  "emoji_button.custom": "Pasirinktinis",
+  "emoji_button.flags": "Vėliavos",
+  "emoji_button.food": "Maistas ir Gėrimai",
+  "emoji_button.label": "Įterpti veidelius",
+  "emoji_button.nature": "Gamta",
+  "emoji_button.not_found": "Nerasta jokių tinkamų jaustukų",
   "emoji_button.objects": "Objektai",
+  "emoji_button.people": "Žmonės",
+  "emoji_button.recent": "Dažniausiai naudojama",
   "emoji_button.search": "Paieška...",
+  "emoji_button.search_results": "Paieškos rezultatai",
+  "emoji_button.symbols": "Simboliai",
+  "emoji_button.travel": "Kelionės ir Vietos",
   "empty_column.account_hides_collections": "Šis naudotojas (-a) pasirinko nepadaryti šią informaciją prieinamą",
+  "empty_column.account_suspended": "Paskyra sustabdyta",
   "empty_column.account_timeline": "No toots here!",
+  "empty_column.account_unavailable": "Profilis neprieinamas",
+  "empty_column.blocks": "Dar neužblokavote nė vieno naudotojo.",
   "empty_column.bookmarked_statuses": "You don't have any bookmarked toots yet. When you bookmark one, it will show up here.",
+  "empty_column.community": "Vietinė laiko juosta yra tuščia. Parašykite ką nors viešai, kad pradėtumėte veikti!",
+  "empty_column.direct": "Dar neturite jokių privačių paminėjimų. Kai išsiųsite arba gausite tokį pranešimą, jis bus rodomas čia.",
   "empty_column.domain_blocks": "There are no hidden domains yet.",
+  "empty_column.favourited_statuses": "Dar neturite mėgstamiausių įrašų. Kai vieną iš jų pamėgsite, jis bus rodomas čia.",
+  "empty_column.follow_requests": "Dar neturite jokių sekimo užklausų. Kai gausite tokį prašymą, jis bus rodomas čia.",
+  "empty_column.followed_tags": "Dar nesekėte jokių grotažymių. Kai tai padarysite, jie bus rodomi čia.",
   "empty_column.hashtag": "Nėra nieko šiame saitažodyje kol kas.",
   "empty_column.home": "Your home timeline is empty! Follow more people to fill it up. {suggestions}",
   "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
+  "empty_column.lists": "Dar neturite jokių sąrašų. Kai jį sukursite, jis bus rodomas čia.",
+  "empty_column.mutes": "Dar nesate nutildę nė vieno naudotojo.",
+  "empty_column.notifications": "Dar neturite jokių pranešimų. Kai kiti žmonės su jumis bendraus, matysite tai čia.",
+  "empty_column.public": "Čia nieko nėra! Parašykite ką nors viešai arba rankiniu būdu sekite naudotojus iš kitų serverių, kad jį užpildytumėte",
+  "error.unexpected_crash.explanation": "Dėl mūsų kodo klaidos arba naršyklės suderinamumo problemos šis puslapis negalėjo būti rodomas teisingai.",
+  "error.unexpected_crash.explanation_addons": "Šį puslapį nepavyko teisingai parodyti. Šią klaidą greičiausiai sukėlė naršyklės priedas arba automatinio vertimo įrankiai.",
+  "error.unexpected_crash.next_steps": "Pabandykite atnaujinti puslapį. Jei tai nepadeda, galbūt vis dar galėsite naudotis \"Mastodon\" naudodami kitą naršyklę arba vietinę programėlę.",
+  "error.unexpected_crash.next_steps_addons": "Pabandykite juos išjungti ir atnaujinti puslapį. Jei tai nepadeda, galbūt vis dar galėsite naudotis \"Mastodon\" naudodami kitą naršyklę arba vietinę programėlę.",
+  "errors.unexpected_crash.report_issue": "Pranešti apie triktį",
+  "explore.search_results": "Paieškos rezultatai",
+  "explore.suggested_follows": "Žmonės",
   "explore.title": "Naršyti",
   "explore.trending_links": "Naujienos",
   "explore.trending_statuses": "Įrašai",
@@ -304,7 +346,13 @@
   "moved_to_account_banner.text": "Tavo paskyra {disabledAccount} šiuo metu yra išjungta, nes persikėlei į {movedToAccount}.",
   "mute_modal.duration": "Trukmė",
   "mute_modal.hide_notifications": "Slėpti šio naudotojo pranešimus?",
+  "mute_modal.indefinite": "Neribotas",
+  "navigation_bar.about": "Apie",
+  "navigation_bar.advanced_interface": "Atidarykite išplėstinę žiniatinklio sąsają",
+  "navigation_bar.blocks": "Užblokuoti naudotojai",
+  "navigation_bar.bookmarks": "Žymės",
   "navigation_bar.compose": "Compose new toot",
+  "navigation_bar.direct": "Privatūs paminėjimai",
   "navigation_bar.discover": "Atrasti",
   "navigation_bar.domain_blocks": "Hidden domains",
   "navigation_bar.edit_profile": "Redaguoti profilį",
@@ -372,6 +420,7 @@
   "notifications.permission_required": "Darbalaukio pranešimai nepasiekiami, nes nesuteiktas reikiamas leidimas.",
   "notifications_permission_banner.enable": "Įjungti darbalaukio pranešimus",
   "notifications_permission_banner.how_to_control": "Jei norite gauti pranešimus, kai \"Mastodon\" nėra atidarytas, įjunkite darbalaukio pranešimus. Įjungę darbalaukio pranešimus, galite tiksliai valdyti, kokių tipų sąveikos generuoja darbalaukio pranešimus, naudodamiesi pirmiau esančiu mygtuku {icon}.",
+  "notifications_permission_banner.title": "Niekada nieko nepraleiskite",
   "onboarding.action.back": "Gražinkite mane atgal",
   "onboarding.actions.back": "Gražinkite mane atgal",
   "onboarding.actions.go_to_explore": "See what's trending",
@@ -394,8 +443,10 @@
   "onboarding.share.lead": "Praneškite žmonėms, kaip jus rasti \"Mastodon\"!",
   "onboarding.share.message": "Aš {username} #Mastodon! Ateik sekti manęs adresu {url}",
   "onboarding.share.next_steps": "Galimi kiti žingsniai:",
+  "onboarding.share.title": "Bendrinkite savo profilį",
   "onboarding.start.lead": "Dabar esi Mastodon dalis – unikalios decentralizuotos socialinės žiniasklaidos platformos, kurioje tu, o ne algoritmas, pats nustatai savo patirtį. Pradėkime tavo kelionę šioje naujoje socialinėje erdvėje:",
   "onboarding.start.skip": "Want to skip right ahead?",
+  "onboarding.start.title": "Jums pavyko!",
   "onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.",
   "onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}",
   "onboarding.steps.publish_status.body": "Say hello to the world.",
@@ -404,23 +455,46 @@
   "onboarding.steps.setup_profile.title": "Customize your profile",
   "onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
   "onboarding.steps.share_profile.title": "Share your profile",
+  "picture_in_picture.restore": "Padėkite jį atgal",
+  "poll.closed": "Uždaryti",
+  "poll.refresh": "Atnaujinti",
+  "poll.reveal": "Peržiūrėti rezultatus",
   "poll.vote": "Balsuoti",
   "poll.voted": "Tu balsavai už šį atsakymą",
   "poll.votes": "{votes, plural, one {# balsas} few {# balsai} many {# balso} other {# balsų}}",
+  "poll_button.add_poll": "Pridėti apklausą",
+  "poll_button.remove_poll": "Šalinti apklausą",
   "privacy.change": "Adjust status privacy",
   "privacy.direct.long": "Post to mentioned users only",
   "privacy.direct.short": "Direct",
   "privacy.private.long": "Post to followers only",
   "privacy.private.short": "Followers-only",
+  "privacy.public.long": "Visiems matomas",
+  "privacy.public.short": "Viešas",
   "privacy.unlisted.long": "Matomas visiems, bet atsisakyta atradimo funkcijų",
   "privacy.unlisted.short": "Neįtrauktas į sąrašą",
   "privacy_policy.last_updated": "Paskutinį kartą atnaujinta {date}",
+  "privacy_policy.title": "Privatumo politika",
   "recommended": "Rekomenduojama",
+  "refresh": "Atnaujinti",
+  "regeneration_indicator.label": "Kraunasi…",
+  "relative_time.full.just_now": "ką tik",
   "relative_time.hours": "{number} val.",
   "relative_time.just_now": "dabar",
   "relative_time.minutes": "{number} min.",
   "relative_time.seconds": "{number} sek.",
   "relative_time.today": "šiandien",
+  "reply_indicator.cancel": "Atšaukti",
+  "report.block": "Blokuoti",
+  "report.categories.legal": "Legalus",
+  "report.categories.other": "Kita",
+  "report.categories.spam": "Šlamštas",
+  "report.categories.violation": "Turinys pažeidžia vieną ar daugiau serverio taisyklių",
+  "report.category.subtitle": "Pasirinkite tinkamiausią variantą",
+  "report.category.title_account": "profilis",
+  "report.category.title_status": "įrašas",
+  "report.close": "Atlikta",
+  "report.comment.title": "Ar yra dar kas nors, ką, jūsų manymu, turėtume žinoti?",
   "report.mute_explanation": "Jų įrašų nematysi. Jie vis tiek gali tave sekti ir matyti įrašus, bet nežinos, kad jie nutildyti.",
   "report.next": "Tęsti",
   "report.placeholder": "Papildomi komentarai",
diff --git a/app/javascript/mastodon/locales/ne.json b/app/javascript/mastodon/locales/ne.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/app/javascript/mastodon/locales/ne.json
@@ -0,0 +1 @@
+{}
diff --git a/app/javascript/mastodon/locales/ry.json b/app/javascript/mastodon/locales/ry.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/app/javascript/mastodon/locales/ry.json
@@ -0,0 +1 @@
+{}
diff --git a/app/javascript/mastodon/locales/sc.json b/app/javascript/mastodon/locales/sc.json
index 59c834b95..7f29525e7 100644
--- a/app/javascript/mastodon/locales/sc.json
+++ b/app/javascript/mastodon/locales/sc.json
@@ -14,6 +14,7 @@
   "account.badges.group": "Grupu",
   "account.block": "Bloca @{name}",
   "account.block_domain": "Bloca su domìniu {domain}",
+  "account.block_short": "Bloca",
   "account.blocked": "Blocadu",
   "account.browse_more_on_origin_server": "Esplora de prus in su profilu originale",
   "account.cancel_follow_request": "Withdraw follow request",
@@ -31,17 +32,20 @@
   "account.follows.empty": "Custa persone non sighit ancora a nemos.",
   "account.follows_you": "Ti sighit",
   "account.hide_reblogs": "Cua is cumpartziduras de @{name}",
+  "account.in_memoriam": "In memoriam.",
   "account.joined_short": "At aderidu",
   "account.link_verified_on": "Sa propiedade de custu ligòngiu est istada controllada su {date}",
   "account.locked_info": "S'istadu de riservadesa de custu contu est istadu cunfiguradu comente blocadu. Sa persone chi tenet sa propiedade revisionat a manu chie dda podet sighire.",
   "account.media": "Cuntenutu multimediale",
   "account.mention": "Mèntova a @{name}",
   "account.mute": "Pone a @{name} a sa muda",
+  "account.mute_short": "A sa muda",
   "account.muted": "A sa muda",
   "account.posts": "Publicatziones",
   "account.posts_with_replies": "Publicatziones e rispostas",
   "account.report": "Signala @{name}",
   "account.requested": "Abetende s'aprovatzione. Incarca pro annullare sa rechesta de sighidura",
+  "account.requested_follow": "{name} at dimandadu de ti sighire",
   "account.share": "Cumpartzi su profilu de @{name}",
   "account.show_reblogs": "Ammustra is cumpartziduras de @{name}",
   "account.statuses_counter": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}}",
@@ -106,6 +110,7 @@
   "compose_form.publish": "Pùblica",
   "compose_form.publish_form": "Publish",
   "compose_form.publish_loud": "{publish}!",
+  "compose_form.save_changes": "Sarva is modìficas",
   "compose_form.sensitive.hide": "{count, plural, one {Marca elementu multimediale comente a sensìbile} other {Marca elementos multimediales comente sensìbiles}}",
   "compose_form.sensitive.marked": "{count, plural, one {Elementu multimediale marcadu comente a sensìbile} other {Elementos multimediales marcados comente a sensìbiles}}",
   "compose_form.sensitive.unmarked": "{count, plural, one {Elementu multimediale non marcadu comente a sensìbile} other {Elementos multimediales non marcados comente a sensìbiles}}",
@@ -122,6 +127,7 @@
   "confirmations.delete_list.message": "Seguru chi boles cantzellare custa lista in manera permanente?",
   "confirmations.domain_block.confirm": "Bloca totu su domìniu",
   "confirmations.domain_block.message": "Boles de seguru, ma a beru a beru, blocare {domain}? In sa parte manna de is casos, pagos blocos o silentziamentos de persones sunt sufitzientes e preferìbiles. No as a bìdere cuntenutos dae custu domìniu in peruna lìnia de tempus pùblica o in is notìficas tuas. Sa gente chi ti sighit dae cussu domìniu at a èssere bogada.",
+  "confirmations.edit.confirm": "Modìfica",
   "confirmations.logout.confirm": "Essi·nche",
   "confirmations.logout.message": "Seguru chi boles essire?",
   "confirmations.mute.confirm": "A sa muda",
@@ -140,6 +146,7 @@
   "directory.local": "Isceti dae {domain}",
   "directory.new_arrivals": "Arribos noos",
   "directory.recently_active": "Cun atividade dae pagu",
+  "disabled_account_banner.account_settings": "Cunfiguratziones de su contu",
   "dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralized network right now.",
   "dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.",
   "embed.instructions": "Inserta custa publicatzione in su situ web tuo copiende su còdighe de suta.",
@@ -180,13 +187,19 @@
   "errors.unexpected_crash.copy_stacktrace": "Còpia stacktrace in punta de billete",
   "errors.unexpected_crash.report_issue": "Sinnala unu problema",
   "explore.search_results": "Resurtados de sa chirca",
+  "explore.suggested_follows": "Gente",
+  "explore.trending_statuses": "Publicatziones",
+  "explore.trending_tags": "Etichetas",
   "filter_modal.select_filter.expired": "iscadidu",
+  "firehose.all": "Totus",
   "follow_request.authorize": "Autoriza",
   "follow_request.reject": "Refuda",
   "follow_requests.unlocked_explanation": "Fintzas si su contu tuo no est blocadu, su personale de {domain} at pensadu chi forsis bolias revisionare a manu is rechestas de custos contos.",
   "footer.about": "Informatziones",
   "footer.invite": "Invita gente",
+  "footer.keyboard_shortcuts": "Incurtzaduras de tecladu",
   "footer.privacy_policy": "Polìtica de riservadesa",
+  "footer.status": "Istadu",
   "generic.saved": "Sarvadu",
   "getting_started.heading": "Comente cumintzare",
   "hashtag.column_header.tag_mode.all": "e {additional}",
@@ -263,6 +276,7 @@
   "lists.search": "Chirca intre sa gente chi ses sighende",
   "lists.subheading": "Is listas tuas",
   "load_pending": "{count, plural, one {# elementu nou} other {# elementos noos}}",
+  "loading_indicator.label": "Carrighende…",
   "media_gallery.toggle_visible": "Cua {number, plural, one {immàgine} other {immàgines}}",
   "mute_modal.duration": "Durada",
   "mute_modal.hide_notifications": "Boles cuare is notìficas de custa persone?",
@@ -288,6 +302,7 @@
   "navigation_bar.search": "Chirca",
   "navigation_bar.security": "Seguresa",
   "not_signed_in_indicator.not_signed_in": "You need to sign in to access this resource.",
+  "notification.favourite": "{name} at marcadu comente a preferidu s'istadu tuo",
   "notification.follow": "{name} ti sighit",
   "notification.follow_request": "{name} at dimandadu de ti sighire",
   "notification.mention": "{name} t'at mentovadu",
@@ -328,6 +343,8 @@
   "onboarding.actions.go_to_home": "Go to your home feed",
   "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": "Nòmine visìbile",
+  "onboarding.profile.note": "Biografia",
   "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?",
   "onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.",
@@ -344,6 +361,7 @@
   "poll.total_votes": "{count, plural, one {# votu} other {# votos}}",
   "poll.vote": "Vota",
   "poll.voted": "As votadu custa risposta",
+  "poll.votes": "{votes, plural, one {# votu} other {# votos}}",
   "poll_button.add_poll": "Agiunghe unu sondàgiu",
   "poll_button.remove_poll": "Cantzella su sondàgiu",
   "privacy.change": "Modìfica s'istadu de riservadesa",
@@ -353,25 +371,41 @@
   "privacy.private.short": "Followers-only",
   "privacy.public.short": "Pùblicu",
   "privacy.unlisted.short": "Esclùidu de sa lista",
+  "recommended": "Cussigiadu",
   "refresh": "Atualiza",
   "regeneration_indicator.label": "Carrighende…",
   "regeneration_indicator.sublabel": "Preparende sa lìnia de tempus printzipale tua.",
   "relative_time.days": "{number} dies a oe",
+  "relative_time.full.just_now": "immoe etotu",
   "relative_time.hours": "{number} oras a immoe",
   "relative_time.just_now": "immoe",
   "relative_time.minutes": "{number} minutos a immoe",
   "relative_time.seconds": "{number} segundos a immoe",
   "relative_time.today": "oe",
   "reply_indicator.cancel": "Annulla",
+  "report.block": "Bloca",
+  "report.categories.other": "Àteru",
+  "report.category.title_account": "profilu",
+  "report.category.title_status": "publicatzione",
+  "report.close": "Fatu",
   "report.forward": "Torra a imbiare a {target}",
   "report.forward_hint": "Custu contu est de un'àteru serbidore. Ddi boles imbiare puru una còpia anònima de custu informe?",
+  "report.mute": "A sa muda",
+  "report.next": "Imbeniente",
   "report.placeholder": "Cummentos additzionales",
   "report.submit": "Imbia",
   "report.target": "Informende de {target}",
   "report_notification.attached_statuses": "{count, plural, one {# post} other {# posts}} attached",
+  "report_notification.categories.other": "Àteru",
   "search.placeholder": "Chirca",
+  "search_popout.user": "utente",
+  "search_results.accounts": "Profilos",
+  "search_results.all": "Totus",
   "search_results.hashtags": "Etichetas",
   "search_results.statuses": "Publicatziones",
+  "server_banner.administered_by": "Amministradu dae:",
+  "server_banner.learn_more": "Àteras informatziones",
+  "server_banner.server_stats": "Istatìsticas de su serbidore:",
   "sign_in_banner.sign_in": "Sign in",
   "status.admin_account": "Aberi s'interfache de moderatzione pro @{name}",
   "status.admin_status": "Aberi custa publicatzione in s'interfache de moderatzione",
@@ -382,6 +416,7 @@
   "status.copy": "Còpia su ligòngiu a sa publicatzione tua",
   "status.delete": "Cantzella",
   "status.detailed_status": "Visualizatzione de detàlliu de arresonada",
+  "status.edit": "Modìfica",
   "status.edited_x_times": "Edited {count, plural, one {# time} other {# times}}",
   "status.embed": "Afissa",
   "status.filtered": "Filtradu",
@@ -413,6 +448,7 @@
   "status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}",
   "status.unmute_conversation": "Torra a ativare s'arresonada",
   "status.unpin": "Boga dae pitzu de su profilu",
+  "subscribed_languages.save": "Sarva is modìficas",
   "tabs_bar.home": "Printzipale",
   "tabs_bar.notifications": "Notìficas",
   "time_remaining.days": "{number, plural, one {abarrat # die} other {abarrant # dies}}",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index 9115695d8..c4ce6f8cf 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -391,6 +391,7 @@
   "lists.search": "Vyhľadávaj medzi užívateľmi, ktorých sleduješ",
   "lists.subheading": "Tvoje zoznamy",
   "load_pending": "{count, plural, one {# nová položka} other {# nových položiek}}",
+  "loading_indicator.label": "Načítam…",
   "media_gallery.toggle_visible": "Zapni/Vypni viditeľnosť",
   "moved_to_account_banner.text": "Vaše konto {disabledAccount} je momentálne zablokované, pretože ste sa presunuli na {movedToAccount}.",
   "mute_modal.duration": "Trvanie",
@@ -480,6 +481,14 @@
   "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.discoverable": "Urob môj profil objaviteľný",
+  "onboarding.profile.display_name": "Zobrazované meno",
+  "onboarding.profile.display_name_hint": "Tvoje plné meno, alebo tvoje zábavné meno…",
+  "onboarding.profile.lead": "Toto môžeš vždy dokončiť neskôr v nastaveniach, kde je dostupných ešte viac volieb na prispôsobenie.",
+  "onboarding.profile.note": "O tebe",
+  "onboarding.profile.note_hint": "Môžeš @spomenúť iných ľudí, alebo #haštagy…",
+  "onboarding.profile.save_and_continue": "Ulož a pokračuj",
+  "onboarding.profile.upload_avatar": "Nahraj profilový obrázok",
+  "onboarding.profile.upload_header": "Nahraj profilové záhlavie",
   "onboarding.share.lead": "Daj ľudom vedieť, ako ťa môžu na Mastodone nájsť!",
   "onboarding.share.message": "Na Mastodone som {username}. Príď ma nasledovať na {url}",
   "onboarding.share.next_steps": "Ďalšie možné kroky:",
@@ -594,6 +603,7 @@
   "search.quick_action.status_search": "Príspevky zodpovedajúce {x}",
   "search.search_or_paste": "Hľadaj, alebo vlož URL adresu",
   "search_popout.full_text_search_disabled_message": "Nie je k dispozícii v doméne {domain}.",
+  "search_popout.full_text_search_logged_out_message": "Dostupné iba keď si prihlásený/á.",
   "search_popout.language_code": "ISO kód jazyka",
   "search_popout.options": "Možnosti vyhľadávania",
   "search_popout.quick_actions": "Rýchle akcie",
diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json
index 86042a91e..710224e1c 100644
--- a/app/javascript/mastodon/locales/sq.json
+++ b/app/javascript/mastodon/locales/sq.json
@@ -606,6 +606,7 @@
   "search.quick_action.status_search": "Postime me përputhje me {x}",
   "search.search_or_paste": "Kërkoni, ose hidhni një URL",
   "search_popout.full_text_search_disabled_message": "Jo i passhëm në {domain}.",
+  "search_popout.full_text_search_logged_out_message": "E përdorshme vetëm kur keni bërë hyrjen në llogari.",
   "search_popout.language_code": "Kod ISO gjuhe",
   "search_popout.options": "Mundësi kërkimi",
   "search_popout.quick_actions": "Veprime të shpejta",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index c020dc362..0be9bf6d7 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -60,7 +60,7 @@
   "account.report": "รายงาน @{name}",
   "account.requested": "กำลังรอการอนุมัติ คลิกเพื่อยกเลิกคำขอติดตาม",
   "account.requested_follow": "{name} ได้ขอติดตามคุณ",
-  "account.share": "แบ่งปันโปรไฟล์ของ @{name}",
+  "account.share": "แชร์โปรไฟล์ของ @{name}",
   "account.show_reblogs": "แสดงการดันจาก @{name}",
   "account.statuses_counter": "{count, plural, other {{counter} โพสต์}}",
   "account.unblock": "เลิกปิดกั้น @{name}",
@@ -319,7 +319,7 @@
   "home.show_announcements": "แสดงประกาศ",
   "interaction_modal.description.favourite": "ด้วยบัญชีใน Mastodon คุณสามารถชื่นชอบโพสต์นี้เพื่อแจ้งให้ผู้สร้างทราบว่าคุณชื่นชมโพสต์และบันทึกโพสต์ไว้สำหรับภายหลัง",
   "interaction_modal.description.follow": "ด้วยบัญชีใน Mastodon คุณสามารถติดตาม {name} เพื่อรับโพสต์ของเขาในฟีดหน้าแรกของคุณ",
-  "interaction_modal.description.reblog": "ด้วยบัญชีใน Mastodon คุณสามารถดันโพสต์นี้เพื่อแบ่งปันโพสต์กับผู้ติดตามของคุณเอง",
+  "interaction_modal.description.reblog": "ด้วยบัญชีใน Mastodon คุณสามารถดันโพสต์นี้เพื่อแชร์โพสต์กับผู้ติดตามของคุณเอง",
   "interaction_modal.description.reply": "ด้วยบัญชีใน Mastodon คุณสามารถตอบกลับโพสต์นี้",
   "interaction_modal.login.action": "นำฉันกลับบ้าน",
   "interaction_modal.login.prompt": "โดเมนของเซิร์ฟเวอร์บ้านของคุณ เช่น mastodon.social",
@@ -495,7 +495,7 @@
   "onboarding.share.lead": "แจ้งให้ผู้คนทราบวิธีที่เขาสามารถค้นหาคุณใน Mastodon!",
   "onboarding.share.message": "ฉันคือ {username} ใน #Mastodon! มาติดตามฉันที่ {url}",
   "onboarding.share.next_steps": "ขั้นตอนถัดไปที่เป็นไปได้:",
-  "onboarding.share.title": "แบ่งปันโปรไฟล์ของคุณ",
+  "onboarding.share.title": "แชร์โปรไฟล์ของคุณ",
   "onboarding.start.lead": "ตอนนี้คุณเป็นส่วนหนึ่งของ Mastodon แพลตฟอร์มสื่อสังคมที่มีเอกลักษณ์เฉพาะตัว กระจายศูนย์ ที่ซึ่งคุณ—ไม่ใช่อัลกอริทึม—เรียบเรียงประสบการณ์ของคุณเอง มาช่วยให้คุณเริ่มต้นใช้งานพรมแดนทางสังคมใหม่นี้กันเลย:",
   "onboarding.start.skip": "ไม่ต้องการความช่วยเหลือในการเริ่มต้นใช้งาน?",
   "onboarding.start.title": "คุณทำสำเร็จแล้ว!",
@@ -506,7 +506,7 @@
   "onboarding.steps.setup_profile.body": "เพิ่มการโต้ตอบของคุณโดยการมีโปรไฟล์ที่ครอบคลุม",
   "onboarding.steps.setup_profile.title": "ปรับแต่งโปรไฟล์ของคุณ",
   "onboarding.steps.share_profile.body": "แจ้งให้เพื่อน ๆ ของคุณทราบวิธีค้นหาคุณใน Mastodon",
-  "onboarding.steps.share_profile.title": "แบ่งปันโปรไฟล์ Mastodon ของคุณ",
+  "onboarding.steps.share_profile.title": "แชร์โปรไฟล์ Mastodon ของคุณ",
   "onboarding.tips.2fa": "<strong>คุณทราบหรือไม่?</strong> คุณสามารถรักษาความปลอดภัยบัญชีของคุณได้โดยตั้งค่าการรับรองความถูกต้องด้วยสองปัจจัยในการตั้งค่าบัญชีของคุณ การรับรองความถูกต้องด้วยสองปัจจัยทำงานร่วมกับแอป TOTP ใด ๆ ที่คุณเลือก ไม่จำเป็นต้องมีหมายเลขโทรศัพท์!",
   "onboarding.tips.accounts_from_other_servers": "<strong>คุณทราบหรือไม่?</strong> เนื่องจาก Mastodon เป็นแบบกระจายศูนย์ โปรไฟล์บางส่วนที่คุณเจอจะได้รับการโฮสต์ในเซิร์ฟเวอร์อื่น ๆ ที่ไม่ใช่ของคุณ และคุณยังสามารถโต้ตอบกับเขาได้อย่างไร้รอยต่อ! เซิร์ฟเวอร์ของเขาอยู่ในครึ่งหลังของชื่อผู้ใช้ของเขา!",
   "onboarding.tips.migration": "<strong>คุณทราบหรือไม่?</strong> หากคุณรู้สึกว่า {domain} ไม่ใช่ตัวเลือกเซิร์ฟเวอร์ที่ยอดเยี่ยมสำหรับคุณในอนาคต คุณสามารถย้ายไปยังเซิร์ฟเวอร์ Mastodon อื่นได้โดยไม่สูญเสียผู้ติดตามของคุณ คุณยังสามารถโฮสต์เซิร์ฟเวอร์ของคุณเอง!",
@@ -558,7 +558,7 @@
   "report.categories.spam": "สแปม",
   "report.categories.violation": "เนื้อหาละเมิดกฎของเซิร์ฟเวอร์จำนวนหนึ่งหรือมากกว่า",
   "report.category.subtitle": "เลือกที่ตรงกันที่สุด",
-  "report.category.title": "บอกเราถึงสิ่งที่กำลังเกิดขึ้นกับ {type} นี้",
+  "report.category.title": "บอกเราถึงสิ่งที่กำลังเกิดขึ้นกับ{type}นี้",
   "report.category.title_account": "โปรไฟล์",
   "report.category.title_status": "โพสต์",
   "report.close": "เสร็จสิ้น",
@@ -629,7 +629,7 @@
   "sign_in_banner.create_account": "สร้างบัญชี",
   "sign_in_banner.sign_in": "เข้าสู่ระบบ",
   "sign_in_banner.sso_redirect": "เข้าสู่ระบบหรือลงทะเบียน",
-  "sign_in_banner.text": "เข้าสู่ระบบเพื่อติดตามโปรไฟล์หรือแฮชแท็ก ชื่นชอบ แบ่งปัน และตอบกลับโพสต์ คุณยังสามารถโต้ตอบจากบัญชีของคุณในเซิร์ฟเวอร์อื่น",
+  "sign_in_banner.text": "เข้าสู่ระบบเพื่อติดตามโปรไฟล์หรือแฮชแท็ก ชื่นชอบ แชร์ และตอบกลับโพสต์ คุณยังสามารถโต้ตอบจากบัญชีของคุณในเซิร์ฟเวอร์อื่น",
   "status.admin_account": "เปิดส่วนติดต่อการควบคุมสำหรับ @{name}",
   "status.admin_domain": "เปิดส่วนติดต่อการควบคุมสำหรับ {domain}",
   "status.admin_status": "เปิดโพสต์นี้ในส่วนติดต่อการควบคุม",
@@ -675,7 +675,7 @@
   "status.replyAll": "ตอบกลับกระทู้",
   "status.report": "รายงาน @{name}",
   "status.sensitive_warning": "เนื้อหาที่ละเอียดอ่อน",
-  "status.share": "แบ่งปัน",
+  "status.share": "แชร์",
   "status.show_filter_reason": "แสดงต่อไป",
   "status.show_less": "แสดงน้อยลง",
   "status.show_less_all": "แสดงน้อยลงทั้งหมด",
diff --git a/app/javascript/mastodon/locales/tlh.json b/app/javascript/mastodon/locales/tlh.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/app/javascript/mastodon/locales/tlh.json
@@ -0,0 +1 @@
+{}
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 7ba66ae8c..e6dd008bf 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -376,7 +376,7 @@
   "lightbox.previous": "上一步",
   "limited_account_hint.action": "一律顯示個人檔案",
   "limited_account_hint.title": "此個人檔案已被 {domain} 的管理員隱藏。",
-  "link_preview.author": "由 {name} 提供",
+  "link_preview.author": "來自 {name}",
   "lists.account.add": "新增至列表",
   "lists.account.remove": "自列表中移除",
   "lists.delete": "刪除列表",
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index e2ea4153c..8463d4297 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -70,6 +70,7 @@ ignore_unused:
   - 'imports.preambles.{following,blocking,muting,domain_blocking,bookmarks,lists}_html'
   - 'mail_subscriptions.unsubscribe.emails.*'
   - 'preferences.other' # some locales are missing other keys, therefore leading i18n-tasks to detect `preferences` as plural and not finding use
+  - 'edit_profile.other' # some locales are missing other keys, therefore leading i18n-tasks to detect `preferences` as plural and not finding use
 
 ignore_inconsistent_interpolations:
   - '*.one'
diff --git a/config/locales/activerecord.fil.yml b/config/locales/activerecord.fil.yml
new file mode 100644
index 000000000..4084bf2f9
--- /dev/null
+++ b/config/locales/activerecord.fil.yml
@@ -0,0 +1 @@
+fil:
diff --git a/config/locales/activerecord.ne.yml b/config/locales/activerecord.ne.yml
new file mode 100644
index 000000000..db03c5186
--- /dev/null
+++ b/config/locales/activerecord.ne.yml
@@ -0,0 +1 @@
+ne:
diff --git a/config/locales/activerecord.ry.yml b/config/locales/activerecord.ry.yml
new file mode 100644
index 000000000..6fe57b65c
--- /dev/null
+++ b/config/locales/activerecord.ry.yml
@@ -0,0 +1 @@
+ry:
diff --git a/config/locales/activerecord.tlh.yml b/config/locales/activerecord.tlh.yml
new file mode 100644
index 000000000..884714fb7
--- /dev/null
+++ b/config/locales/activerecord.tlh.yml
@@ -0,0 +1 @@
+tlh:
diff --git a/config/locales/devise.fil.yml b/config/locales/devise.fil.yml
new file mode 100644
index 000000000..4084bf2f9
--- /dev/null
+++ b/config/locales/devise.fil.yml
@@ -0,0 +1 @@
+fil:
diff --git a/config/locales/devise.ne.yml b/config/locales/devise.ne.yml
new file mode 100644
index 000000000..db03c5186
--- /dev/null
+++ b/config/locales/devise.ne.yml
@@ -0,0 +1 @@
+ne:
diff --git a/config/locales/devise.ry.yml b/config/locales/devise.ry.yml
new file mode 100644
index 000000000..6fe57b65c
--- /dev/null
+++ b/config/locales/devise.ry.yml
@@ -0,0 +1 @@
+ry:
diff --git a/config/locales/devise.tlh.yml b/config/locales/devise.tlh.yml
new file mode 100644
index 000000000..884714fb7
--- /dev/null
+++ b/config/locales/devise.tlh.yml
@@ -0,0 +1 @@
+tlh:
diff --git a/config/locales/doorkeeper.fil.yml b/config/locales/doorkeeper.fil.yml
new file mode 100644
index 000000000..4084bf2f9
--- /dev/null
+++ b/config/locales/doorkeeper.fil.yml
@@ -0,0 +1 @@
+fil:
diff --git a/config/locales/doorkeeper.ne.yml b/config/locales/doorkeeper.ne.yml
new file mode 100644
index 000000000..db03c5186
--- /dev/null
+++ b/config/locales/doorkeeper.ne.yml
@@ -0,0 +1 @@
+ne:
diff --git a/config/locales/doorkeeper.ry.yml b/config/locales/doorkeeper.ry.yml
new file mode 100644
index 000000000..6fe57b65c
--- /dev/null
+++ b/config/locales/doorkeeper.ry.yml
@@ -0,0 +1 @@
+ry:
diff --git a/config/locales/doorkeeper.sc.yml b/config/locales/doorkeeper.sc.yml
index 1f1d38f3a..297d6bd8f 100644
--- a/config/locales/doorkeeper.sc.yml
+++ b/config/locales/doorkeeper.sc.yml
@@ -69,6 +69,7 @@ sc:
       confirmations:
         revoke: Seguru?
       index:
+        scopes: Permissos
         title: Is aplicatziones autorizadas tuas
     errors:
       messages:
@@ -104,6 +105,20 @@ sc:
       authorized_applications:
         destroy:
           notice: Aplicatzione revocada.
+    grouped_scopes:
+      title:
+        accounts: Contos
+        bookmarks: Sinnalibros
+        conversations: Arresonadas
+        filters: Filtros
+        follows: Sighende
+        lists: Listas
+        media: Allegados multimediales
+        notifications: Notìficas
+        push: Notìficas push
+        reports: Informes
+        search: Chirca
+        statuses: Publicatziones
     layouts:
       admin:
         nav:
diff --git a/config/locales/doorkeeper.tlh.yml b/config/locales/doorkeeper.tlh.yml
new file mode 100644
index 000000000..884714fb7
--- /dev/null
+++ b/config/locales/doorkeeper.tlh.yml
@@ -0,0 +1 @@
+tlh:
diff --git a/config/locales/fil.yml b/config/locales/fil.yml
new file mode 100644
index 000000000..4084bf2f9
--- /dev/null
+++ b/config/locales/fil.yml
@@ -0,0 +1 @@
+fil:
diff --git a/config/locales/fr-QC.yml b/config/locales/fr-QC.yml
index 3aba8713f..00a59463c 100644
--- a/config/locales/fr-QC.yml
+++ b/config/locales/fr-QC.yml
@@ -611,6 +611,7 @@ fr-QC:
       created_at: Signalé
       delete_and_resolve: Supprimer les messages
       forwarded: Transféré
+      forwarded_replies_explanation: Ce rapport provient d'un utilisateur sur une autre instance et concerne du contenu non-local. Il vous a été transmis car le contenu signalé est en réponse à l'un de vos utilisateurs.
       forwarded_to: Transféré à %{domain}
       mark_as_resolved: Marquer comme résolu
       mark_as_sensitive: Marquer comme sensible
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index a69a5b535..0a6601bbc 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -611,6 +611,7 @@ fr:
       created_at: Signalé
       delete_and_resolve: Supprimer les messages
       forwarded: Transféré
+      forwarded_replies_explanation: Ce rapport provient d'un utilisateur sur une autre instance et concerne du contenu non-local. Il vous a été transmis car le contenu signalé est en réponse à l'un de vos utilisateurs.
       forwarded_to: Transféré à %{domain}
       mark_as_resolved: Marquer comme résolu
       mark_as_sensitive: Marquer comme sensible
diff --git a/config/locales/lt.yml b/config/locales/lt.yml
index 111127749..b1d8772b6 100644
--- a/config/locales/lt.yml
+++ b/config/locales/lt.yml
@@ -37,10 +37,12 @@ lt:
     accounts:
       add_email_domain_block: Blokuoti el. pašto domeną
       approve: Patvirtinti
+      approved_msg: Sėkmingai patvirtinta %{username} registracijos paraiška
       are_you_sure: Ar esi įsitikinęs (-usi)?
       avatar: Avataras
       by_domain: Domenas
       change_email:
+        changed_msg: El. paštas sėkmingai pakeistas!
         current_email: Dabartinis el paštas
         label: Pakeisti el pašto adresą
         new_email: Naujas el pašto adresas
@@ -466,6 +468,8 @@ lt:
     prev: Ankstesnis
   preferences:
     other: Kita
+  privacy:
+    hint_html: "<strong>Tikrink, kaip nori, kad tavo profilis ir įrašai būtų randami.</strong> Į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ą."
   remote_follow:
     missing_resource: Jūsų paskyros nukreipimo URL nerasta
   scheduled_statuses:
@@ -515,6 +519,11 @@ lt:
       public_long: Visi gali matyti
       unlisted: Neįtrauktas į sąrašus
       unlisted_long: Matyti gali visi, tačiau nėra įtraukti į viešąsias laiko skales
+  statuses_cleanup:
+    enabled_hint: Automatiškai ištrina įrašus, kai jie pasiekia nustatytą amžiaus ribą, nebent jie atitinka vieną iš toliau nurodytų išimčių
+    keep_polls_hint: Neištrina jokių tavo apklausų
+    keep_self_bookmark: Laikyti įrašus, kuriuos pažymėjai
+    keep_self_bookmark_hint: Neištrina tavo pačių įrašų, jei esi juos pažymėjęs (-usi)
   stream_entries:
     sensitive_content: Jautrus turinys
   themes:
@@ -551,22 +560,31 @@ lt:
       explanation: Štai keletas patarimų, kaip pradėti
       final_action: Pradėti kelti įrašus
       final_step: 'Pradėk skelbti! Net jei ir neturi sekėjų, tavo viešus įrašus gali matyti kiti, pavyzdžiui, vietinėje laiko skalėje arba saitažodžiuose. Galbūt norėsi prisistatyti saitažodyje #introductions.'
-      full_handle: Jūsų pilnas slapyvardis
-      full_handle_hint: Štai ką jūs sakytumėte savo draugams, kad jie galėtų jums siųsti žinutes arba just sekti iš kitų serverių.
+      full_handle: Tavo pilnas slapyvardis
+      full_handle_hint: Štai ką pasakytum savo draugams, kad jie galėtų parašyti arba sekti tave iš kito serverio.
       subject: Sveiki atvykę į Mastodon
       title: Sveiki atvykę, %{name}!
   users:
-    follow_limit_reached: Negalite sekti daugiau nei %{limit} žmonių
+    follow_limit_reached: Negali sekti daugiau nei %{limit} žmonių
+    go_to_sso_account_settings: Eik į savo tapatybės teikėjo paskyros nustatymus
     invalid_otp_token: Netinkamas dviejų veiksnių kodas
     otp_lost_help_html: Jei praradai prieigą prie abiejų, gali susisiek su %{email}
-    seamless_external_login: Jūs esate prisijungę per išorini įrenginį, todėl slaptąžodis ir el pašto nustatymai neprieinami.
+    seamless_external_login: Esi prisijungęs (-usi) per išorinę paslaugą, todėl slaptažodžio ir el. pašto nustatymai nepasiekiami.
     signed_in_as: 'Prisijungta kaip:'
   verification:
+    extra_instructions_html: <strong>Patarimas:</strong> nuoroda tavo svetainėje gali būti nematoma. Svarbi dalis – tai, kas <code>rel="me"</code> neleidžia apsimesti interneto svetainėse, kuriose yra naudotojų sukurto turinio. Vietoj to gali naudoti net <code>nuorodą</code> puslapio antraštėje esančią žymę <code>a</code>, tačiau HTML turi būti pasiekiamas nevykdant JavaScript.
     hint_html: "<strong>Savo tapatybės patvirtinimas Mastodon skirtas visiems.</strong> Remiantis atviraisiais žiniatinklio standartais, dabar ir visam laikui nemokamas. Viskas, ko tau reikia, yra asmeninė svetainė, pagal kurią žmonės tave atpažįsta. Kai iš savo profilio pateiksi nuorodą į šią svetainę, patikrinsime, ar svetainėje yra nuoroda į tavo profilį, ir parodysime vizualinį indikatorių."
+    instructions_html: Nukopijuok ir įklijuok toliau pateiktą kodą į savo svetainės HTML. Tada į vieną iš papildomų profilio laukų skirtuke „Redaguoti profilį“ įrašyk savo svetainės adresą ir išsaugok pakeitimus.
     verification: Patvirtinimas
     verified_links: Tavo patikrintos nuorodos
   webauthn_credentials:
     create:
       error: Kilo problema pridedant saugumo raktą. Bandyk dar kartą.
+      success: Tavo saugumo raktas buvo sėkmingai pridėtas.
+    delete_confirmation: Ar tikrai nori ištrinti šį saugumo raktą?
+    description_html: Jei įjungsi <strong>saugumo rakto tapatybės nustatymą</strong>, prisijungiant reikės naudoti vieną iš savo saugumo raktų.
+    destroy:
+      error: Kilo problema ištrinant saugumo raktą. Bandyk dar kartą.
+      success: Tavo saugumo raktas buvo sėkmingai ištrintas.
     nickname_hint: Įvesk naujojo saugumo rakto slapyvardį
     not_enabled: Dar neįjungei WebAuthn
diff --git a/config/locales/ne.yml b/config/locales/ne.yml
new file mode 100644
index 000000000..db03c5186
--- /dev/null
+++ b/config/locales/ne.yml
@@ -0,0 +1 @@
+ne:
diff --git a/config/locales/ry.yml b/config/locales/ry.yml
new file mode 100644
index 000000000..6fe57b65c
--- /dev/null
+++ b/config/locales/ry.yml
@@ -0,0 +1 @@
+ry:
diff --git a/config/locales/sc.yml b/config/locales/sc.yml
index fa7603f2b..d92b64783 100644
--- a/config/locales/sc.yml
+++ b/config/locales/sc.yml
@@ -5,6 +5,7 @@ sc:
     contact_missing: No cunfiguradu
     contact_unavailable: No a disponimentu
     hosted_on: Mastodon allogiadu in %{domain}
+    title: Informatziones
   accounts:
     follow: Sighi
     followers:
@@ -45,6 +46,7 @@ sc:
       confirm: Cunfirma
       confirmed: Cunfirmadu
       confirming: Cunfirmende
+      custom: Personalizadu
       delete: Cantzella datos
       deleted: Cantzelladu
       demote: Degrada
@@ -81,7 +83,9 @@ sc:
       moderation:
         active: Ativu
         all: Totus
+        disabled: Disativadu
         pending: De imbiare
+        silenced: Limitadu
         suspended: Suspèndidu
         title: Moderatzione
       moderation_notes: Notas de moderatzione
@@ -112,6 +116,7 @@ sc:
       search: Chirca
       search_same_email_domain: Àteras persones cun su pròpiu domìniu de posta
       search_same_ip: Àteras persones cun sa pròpiu IP
+      security: Seguresa
       sensitive: Sensìbile
       sensitized: marcadu comente a sensìbile
       shared_inbox_url: URL de intrada cumpartzida
@@ -122,6 +127,7 @@ sc:
       silenced: Limitadas
       statuses: Tuts
       subscribe: Sutascrie·ti
+      suspend: Suspensione
       suspended: Suspèndidu
       suspension_irreversible: Is datos de custu contu sunt istados cantzellados in manera irreversìbile. Podes bogare sa suspensione a su contu pro chi si potzat impreare, ma no at a recuperare datu perunu de is chi teniat in antis.
       suspension_reversible_hint_html: Su contu est istadu suspèndidu, e is datos ant a èssere cantzelladu de su totu su %{date}. Finas a tando, su contu si podet ripristinare sena efetu malu perunu. Si boles cantzellare totu is datos de su contu immediatamente ddu podes fàghere inoghe in bassu.
@@ -274,6 +280,7 @@ sc:
       updated_msg: Emoji atualizadu
       upload: Càrriga
     dashboard:
+      media_storage: Immagasinamentu
       software: Programmas
       space: Impreu de ispàtziu
       title: Pannellu
@@ -281,21 +288,28 @@ sc:
       add_new: Permite sa federatzione cun domìniu
       created_msg: Sa federatzione cun su domìniu est istada permìtida
       destroyed_msg: Sa federatzione cun su domìniu no est istada permìtida
+      import: Importatzione
       undo: Non permitas sa federatzione cun su domìniu
     domain_blocks:
       add_new: Agiunghe blocu de domìniu nou
+      confirm_suspension:
+        cancel: Annulla
+        confirm: Suspensione
       created_msg: Protzessende su blocu de domìniu
       destroyed_msg: Su blocu de domìniu est istadu iscontzadu
       domain: Domìniu
       edit: Modìfica su blocu de su domìniu
       existing_domain_block_html: As giai impostu lìmites prus astrintos a %{name}, ddu dias dèpere <a href="%{unblock_url}">isblocare</a> prima.
+      import: Importatzione
       new:
         create: Crea unu blocu
         hint: Su blocu de domìniu no at a impedire sa creatzione de contos noos in sa base de datos, ma ant a èssere aplicados in manera retroativa mètodos de moderatzione ispetzìficos subra custos contos.
         severity:
           noop: Perunu
+          silence: A sa muda
           suspend: Suspensione
         title: Blocu de domìniu nou
+      not_permitted: Non tenes su permissu de fàghere custa atzione
       obfuscate: Cua su nòmine de domìniu
       obfuscate_hint: Cua una parte de su nòmine de domìniu in sa lista si sa visualizatzione de sa lista de domìnios limitados est ativa
       private_comment: Cummentu privadu
@@ -326,9 +340,24 @@ sc:
       title: Cussìgios de sighidura
       unsuppress: Recùpera su cussìgiu de sighidura
     instances:
+      back_to_all: Totus
+      back_to_limited: Limitadu
+      back_to_warning: Atentzione
       by_domain: Domìniu
+      content_policies:
+        policies:
+          reject_reports: Refuda informes
+          silence: A sa muda
+          suspend: Suspensione
+      dashboard:
+        instance_reports_measure: informes a subra de àtere
+      delivery:
+        all: Totus
       delivery_available: Sa cunsigna est a disponimentu
       empty: Perunu domìniu agatadu.
+      known_accounts:
+        one: "%{count} contu connòschidu"
+        other: "%{count} contos connòschidos"
       moderation:
         all: Totus
         limited: Limitadas
@@ -390,18 +419,23 @@ sc:
         notes:
           one: "%{count} nota"
           other: "%{count} notas"
+      action_log: Registru de controllu
       action_taken_by: Mesuras adotadas dae
       are_you_sure: Seguru?
       assign_to_self: Assigna a mie
       assigned: Moderatzione assignada
       by_target_domain: Domìniu de su contu signaladu
+      cancel: Annulla
       comment:
         none: Perunu
+      confirm: Cunfirma
       created_at: Sinnaladu
       forwarded: Torradu a imbiare
       forwarded_to: Torradu a imbiare a %{domain}
       mark_as_resolved: Marca comente a isòrvidu
+      mark_as_sensitive: Signala comente a sensìbile
       mark_as_unresolved: Marcare comente a non isòrvidu
+      no_one_assigned: Nemos
       notes:
         create: Agiunghe una nota
         create_and_resolve: Isorve cun una nota
@@ -419,6 +453,15 @@ sc:
       unassign: Boga s'assignatzione
       unresolved: No isòrvidu
       updated_at: Atualizadu
+      view_profile: Visualiza profilu
+    roles:
+      categories:
+        administration: Amministratzione
+        invites: Invitos
+        moderation: Moderatzione
+      delete: Cantzella
+      privileges:
+        administrator: Amministratzione
     rules:
       add_new: Agiunghe règula
       delete: Cantzella
@@ -427,10 +470,26 @@ sc:
       empty: Peruna règula de serbidore definida ancora.
       title: Règulas de su serbidore
     settings:
+      about:
+        manage_rules: Gesti is règulas de su serbidore
+        title: Informatziones
+      appearance:
+        title: Aspetu
+      default_noindex:
+        desc_html: Ìmplicat a totu is utentes chi no apant modificadu custa cunfiguratzione
+        title: Esclude in manera predefinida is utentes dae s'inditzamentu de is motores de chirca
+      discovery:
+        follow_recommendations: Cussìgios de sighidura
+        profile_directory: Diretòriu de profilos
+        public_timelines: Lìnias de tempos pùblicas
+        title: Iscoberta
+        trends: Tendèntzias
       domain_blocks:
         all: Pro totus
         disabled: Pro nemos
         users: Pro utentes locales in lìnia
+      registrations:
+        title: Registros
       registrations_mode:
         modes:
           approved: Aprovatzione rechesta pro si registrare
@@ -439,7 +498,10 @@ sc:
     site_uploads:
       delete: Cantzella s'archìviu carrigadu
       destroyed_msg: Càrriga de su situ cantzellada.
+    software_updates:
+      documentation_link: Àteras informatziones
     statuses:
+      application: Aplicatzione
       back_to_account: Torra a sa pàgina de su contu
       deleted: Cantzelladu
       media:
@@ -447,6 +509,10 @@ sc:
       no_status_selected: Perunu istadu est istadu mudadu dae chi non nd'as seletzionadu
       title: Istados de su contu
       with_media: Cun elementos multimediales
+    strikes:
+      actions:
+        none: "%{name} at imbiadu un'avisu a %{target}"
+        suspend: "%{name} at suspèndidu su contu de %{target}"
     system_checks:
       database_schema_check:
         message_html: Ddoe at tràmudas de base de datos in suspesu. Pone·ddas in esecutzione pro ti assegurare chi s'aplicatzione funtzionet comente si tocat
@@ -459,12 +525,24 @@ sc:
       review: Revisiona s'istadu
       updated_msg: Cunfiguratzione de etichetas atualizada
     title: Amministratzione
+    trends:
+      pending_review: De revisionare
+      tags:
+        title: Etichetas de tendèntzia
+      title: Tendèntzias
     warning_presets:
       add_new: Agiunghe noa
       delete: Cantzella
       edit_preset: Modìfica s'avisu predefinidu
       empty: No as cunfiguradu ancora perunu avisu predefinidu.
       title: Gesti is cunfiguratziones predefinidas de is avisos
+    webhooks:
+      delete: Cantzella
+      disable: Disativa
+      disabled: Disativadu
+      enable: Ativa
+      enabled: Ativu
+      status: Istadu
   admin_mailer:
     new_pending_account:
       body: Is detàllios de su contu nou sunt a suta. Podes aprovare o refudare custa rechesta.
@@ -473,6 +551,9 @@ sc:
       body: "%{reporter} at sinnaladu %{target}"
       body_remote: Una persone de su domìniu %{domain} at sinnaladu %{target}
       subject: Informe nou pro %{instance} (#%{id})
+    new_trends:
+      new_trending_tags:
+        title: Etichetas de tendèntzia
   aliases:
     add_new: Crea unu nomìngiu
     created_msg: Nomìngiu creadu. Immoe podes cumintzare a tramudare dae su contu betzu.
@@ -495,17 +576,21 @@ sc:
     notification_preferences: Muda is preferèntzias de posta
     salutation: "%{name},"
     settings: 'Muda is preferèntzias de posta: %{link}'
+    unsubscribe: Annulla sa sutiscritzione
     view: 'Visualizatzione:'
     view_profile: Visualiza profilu
     view_status: Ammustra s'istadu
   applications:
     created: Aplicatzione creada
     destroyed: Aplicatzione cantzellada
+    logout: Essi
     regenerate_token: Torra a generare s'identificadore de atzessu
     token_regenerated: Identificadore de atzessu generadu
     warning: Dae cara a custos datos. Non ddos cumpartzas mai cun nemos!
     your_token: S'identificadore tuo de atzessu
   auth:
+    confirmations:
+      login_link: intra
     delete_account: Cantzella su contu
     delete_account_html: Si boles cantzellare su contu, ddu podes <a href="%{path}">fàghere inoghe</a>. T'amus a dimandare una cunfirmatzione.
     description:
@@ -528,11 +613,14 @@ sc:
     register: Registru
     registration_closed: "%{instance} no atzetat àteras persones"
     reset_password: Reseta sa crae
+    rules:
+      back: A coa
     security: Seguresa
     set_new_password: Cunfigura una crae noa
     status:
       account_status: Istadu de su contu
       confirming: Isetende chi sa posta eletrònica siat cumpletada.
+      functional: Su contu tuo est operativu.
       pending: Sa dimanda tua est in protzessu de revisione dae su personale nostru. Podet serbire unu pagu de tempus. As a retzire unu messàgiu eletrònicu si sa dimanda est aprovada.
       redirecting_to: Su contu tuo est inativu pro ite in die de oe est torrende a indiritzare a %{acct}.
     too_fast: Formulàriu imbiadu tropu a lestru, torra a proare.
@@ -581,8 +669,14 @@ sc:
       more_details_html: Pro àteros detàllios, bide sa <a href="%{terms_path}">normativa de riservadesa</a>.
       username_available: Su nòmine de utente tuo at a torrare a èssere a disponimentu
       username_unavailable: Su nòmine de utente tuo no at a abarrare a disponimentu
+  disputes:
+    strikes:
+      title_actions:
+        none: Atentzione
   domain_validator:
     invalid_domain: no est unu nòmine de domìniu vàlidu
+  edit_profile:
+    other: Àteru
   errors:
     '400': Sa dimanda chi as imbiadu non fiat vàlida o non fiat curreta.
     '403': Non tenes permissu pro bìdere custa pàgina.
@@ -638,11 +732,15 @@ sc:
       title: Agiunghe unu filtru nou
   generic:
     all: Totus
+    cancel: Annulla
     changes_saved_msg: Modìficas sarvadas.
+    confirm: Cunfirma
     copy: Còpia
     delete: Cantzella
+    none: Perunu
     order_by: Òrdina pro
     save_changes: Sarva is modìficas
+    today: oe
     validation_errors:
       one: Calicuna cosa ancora no est andende. Bide sa faddina in bàsciu
       other: Calicuna cosa ancora no est andende. Bide is %{count} faddinas in bàsciu
@@ -655,12 +753,15 @@ sc:
       overwrite: Subrascrie
       overwrite_long: Sostitui is registros atuales cun cussos noos
     preface: Podes importare datos chi as esportadu dae unu àteru serbidore, che a sa lista de sa gente chi ses sighende o blochende.
+    status: Istadu
     success: Datos carrigados; ant a èssere protzessados luego
+    type: Casta de importatzione
     types:
       blocking: Lista de blocos
       bookmarks: Sinnalibros
       domain_blocking: Lista domìnios blocados
       following: Lista de sighiduras
+      lists: Listas
       muting: Lista gente a sa muda
     upload: Càrriga
   invites:
@@ -685,6 +786,13 @@ sc:
       expires_at: Iscadit
       uses: Impreos
     title: Invita gente
+  login_activities:
+    authentication_methods:
+      password: crae
+      webauthn: craes de seguresa
+  mail_subscriptions:
+    unsubscribe:
+      title: Annulla sa sutiscritzione
   media_attachments:
     validations:
       images_and_video: Non si podet allegare unu vìdeu in una publicatzione chi cuntenet giai immàgines
@@ -797,6 +905,8 @@ sc:
     other: Àteru
     posting_defaults: Valores predefinidos de publicatzione
     public_timelines: Lìnias de tempos pùblicas
+  privacy:
+    search: Chirca
   reactions:
     errors:
       limit_reached: Lìmite de reatziones diferentes cròmpidu
@@ -850,6 +960,7 @@ sc:
     platforms:
       adobe_air: Adobe Air
       android: Android
+      chrome_os: ChromeOS
       firefox_os: Firefox OS
       ios: iOS
       linux: Linux
@@ -934,6 +1045,7 @@ sc:
       '2629746': 1 mese
       '31556952': 1 annu
       '5259492': 2 meses
+      '604800': 1 chida
       '63113904': 2 annos
       '7889238': 3 meses
   stream_entries:
@@ -969,6 +1081,7 @@ sc:
       subject: S'archìviu tuo est prontu pro èssere iscarrigadu
       title: Collida dae s'archìviu
     warning:
+      reason: 'Resone:'
       subject:
         disable: Su contu tuo %{acct} est istadu cungeladu
         none: Avisu pro %{acct}
diff --git a/config/locales/simple_form.fil.yml b/config/locales/simple_form.fil.yml
new file mode 100644
index 000000000..4084bf2f9
--- /dev/null
+++ b/config/locales/simple_form.fil.yml
@@ -0,0 +1 @@
+fil:
diff --git a/config/locales/simple_form.lt.yml b/config/locales/simple_form.lt.yml
index 39caaf6ba..d53b7105e 100644
--- a/config/locales/simple_form.lt.yml
+++ b/config/locales/simple_form.lt.yml
@@ -27,14 +27,33 @@ lt:
           none: Naudok šią parinktį norėdamas (-a) išsiųsti įspėjimą naudotojui, nesukeldamas (-a) jokio kito veiksmo.
           sensitive: Priversk visus šio naudotojo medijos priedus pažymėti kaip jautrius.
           silence: Neleisk naudotojui skelbti viešai matomų įrašų, paslėpk jų įrašus ir pranešimus nuo žmonių, kurie neseka jo. Uždaro visus su šia paskyra susijusius ataskaitas.
+          suspend: Neleisk jokios sąveikos iš šios paskyros arba į ją ir ištrink jos turinį. Sugrąžinama per 30 dienų. Uždaro visas su šia paskyra susijusias ataskaitas.
+        warning_preset_id: Pasirinktinai. Gali pridėti pasirinktinį tekstą iš anksto nustatyto rinkinio pabaigoje
+      announcement:
+        all_day: Jei pažymėta, bus rodomos tik laikotarpio datos
+        ends_at: Pasirinktinai. Skelbimas šiuo laiku bus automatiškai panaikintas
+        scheduled_at: Palik tuščią, kad skelbimas būtų paskelbtas iš karto
+        starts_at: Pasirinktinai. Jei skelbimas susietas su tam tikru laiko tarpu
+        text: Gali naudoti įrašo sintaksę. Būk dėmesingas (-a), kiek vietos naudotojo ekrane užims skelbimas
+      appeal:
+        text: Gali pateikti apeliaciją dėl streiko tik vieną kartą
       defaults:
+        autofollow: Žmonės, kurie užsiregistruos per kvietimą, automatiškai seks tave
         avatar: PNG, GIF arba JPG. Ne daugiau kaip %{size}. Bus sumažintas iki %{dimensions} tšk.
-        header: PNG, GIF arba JPG. Ne daugiau kaip %{size}. Bus sumažintas iki %{dimensions}tšk.
+        bot: Signalizuoti kitiems, kad paskyroje daugiausia atliekami automatiniai veiksmai ir kad ji gali būti nestebima
+        context: Vienas arba keli kontekstai, kuriems turėtų būti taikomas filtras
+        current_password: Saugumo sumetimais įvesk dabartinės paskyros slaptažodį
+        current_username: Kad patvirtintum, įvesk dabartinės paskyros naudotojo vardą
+        digest: Siunčiama tik po ilgo neaktyvumo laikotarpio ir tik tuo atveju, jei negavai jokių asmeninių žinučių
+        email: Tau bus išsiųstas patvirtinimo el. laiškas
+        header: PNG, GIF arba JPG. Ne daugiau kaip %{size}. Bus sumažintas iki %{dimensions} tšk.
         inbox_url: Nukopijuok URL adresą iš pradinio puslapio perdavėjo, kurį nori naudoti
         irreversible: Filtruoti įrašai išnyks negrįžtamai, net jei vėliau filtras bus pašalintas
         locale: Naudotojo sąsajos kalba, el. laiškai ir stumiamieji pranešimai
         password: Naudok bent 8 simbolius
-        phrase: Bus suderinta, neatsižvelgiant į teksto korpusą arba įrašo turinio įspėjimą
+        phrase: Bus suderinta, neatsižvelgiant į teksto lygį arba įrašo turinio įspėjimą
+        scopes: Prie kurių API programai bus leidžiama pasiekti. Pasirinkus aukščiausio lygio sritį, atskirų sričių pasirinkti nereikia.
+        setting_aggregate_reblogs: Nerodyti naujų pakėlimų įrašams, kurie neseniai buvo pakelti (taikoma tik naujai gautiems pakėlimams)
         setting_always_send_emails: Paprastai pranešimai el. paštu nebus siunčiami, kai aktyviai naudoji Mastodon
         setting_default_sensitive: Jautrioji medija pagal numatytuosius nustatymus yra paslėpta ir gali būti atskleista paspaudus
         setting_display_media_default: Slėpti mediją, pažymėtą kaip jautrią
@@ -42,16 +61,26 @@ lt:
         setting_display_media_show_all: Visada rodyti mediją
         setting_use_blurhash: Gradientai pagrįsti paslėptų vaizdų spalvomis, tačiau užgožia bet kokias detales
         setting_use_pending_items: Slėpti laiko skalės naujienas po paspaudimo, vietoj automatinio kanalo slinkimo
+        username: Gali naudoti raides, skaičius ir pabraukimus
+        whole_word: Kai raktažodis ar frazė yra tik raidinis ir skaitmeninis, jis bus taikomas tik tada, jei atitiks visą žodį
       featured_tag:
         name: 'Štai keletas pastaruoju metu dažniausiai saitažodžių, kurių tu naudojai:'
+      filters:
+        action: Pasirink, kokį veiksmą atlikti, kai įrašas atitinka filtrą
+        actions:
+          hide: Visiškai paslėpti filtruotą turinį ir elgtis taip, tarsi jo neegzistuotų
+          warn: Slėpti filtruojamą turinį po įspėjimu, paminint filtro pavadinimą
       form_admin_settings:
+        activity_api_enabled: Vietinių paskelbtų įrašų, aktyvių naudotojų ir naujų registracijų skaičiai kas savaitę
+        backups_retention_period: Laikyti sukurtus naudotojų archyvus nurodytą dienų skaičių.
         peers_api_enabled: Domenų pavadinimų sąrašas, su kuriais šis serveris susidūrė fediverse. Čia nėra duomenų apie tai, ar tu bendrauji su tam tikru serveriu, tik apie tai, kad tavo serveris apie jį žino. Tai naudojama tarnybose, kurios renka federacijos statistiką bendrąja prasme.
         site_contact_email: Kaip žmonės gali su tavimi susisiekti teisiniais ar pagalbos užklausimais.
         site_contact_username: Kaip žmonės gali tave pasiekti Mastodon.
         site_extended_description: Bet kokia papildoma informacija, kuri gali būti naudinga lankytojams ir naudotojams. Gali būti struktūrizuota naudojant Markdown sintaksę.
         trends: Trendai rodo, kurios įrašai, saitažodžiai ir naujienų istorijos tavo serveryje sulaukia didžiausio susidomėjimo.
       sessions:
-        webauthn: Jei tai USB raktas, būtinai jį įkišk ir, jei reikia, paliesk.
+        otp: 'Įvesk telefono programėlėje sugeneruotą dviejų tapatybės kodą arba naudok vieną iš atkūrimo kodų:'
+        webauthn: Jei tai USB raktas, būtinai jį įkišk ir, jei reikia, paspausk.
       settings:
         indexable: Tavo profilio puslapis gali būti rodomas paieškos rezultatuose Google, Bing ir kituose.
     labels:
diff --git a/config/locales/simple_form.ne.yml b/config/locales/simple_form.ne.yml
new file mode 100644
index 000000000..db03c5186
--- /dev/null
+++ b/config/locales/simple_form.ne.yml
@@ -0,0 +1 @@
+ne:
diff --git a/config/locales/simple_form.ry.yml b/config/locales/simple_form.ry.yml
new file mode 100644
index 000000000..6fe57b65c
--- /dev/null
+++ b/config/locales/simple_form.ry.yml
@@ -0,0 +1 @@
+ry:
diff --git a/config/locales/simple_form.sc.yml b/config/locales/simple_form.sc.yml
index 2c4725996..5f5d63307 100644
--- a/config/locales/simple_form.sc.yml
+++ b/config/locales/simple_form.sc.yml
@@ -53,6 +53,8 @@ sc:
         domain: Custu domìniu at a pòdere recuperare datos dae custu serbidore e is datos in intrada dae cue ant a èssere protzessados e archiviados
       email_domain_block:
         with_dns_records: S'at a fàghere unu tentativu de risòlvere is registros DNS de su domìniu e fintzas is risultados ant a èssere blocados
+      form_admin_settings:
+        activity_api_enabled: Nùmeru de tuts publicados in locale, utentes ativos e registros noos in perìodos chidajolos
       form_challenge:
         current_password: Ses intrende in un'àrea segura
       imports:
@@ -155,6 +157,7 @@ sc:
         setting_use_pending_items: Modalidade lenta
         severity: Severidade
         sign_in_token_attempt: Còdighe de seguresa
+        title: Tìtulu
         type: Casta de importatzione
         username: Nòmine utente
         username_or_email: Nòmine utente o indiritzu de posta eletrònica
@@ -163,6 +166,16 @@ sc:
         with_dns_records: Include registros MX e indiritzos IP de su domìniu
       featured_tag:
         name: Eticheta
+      form_admin_settings:
+        activity_api_enabled: Pùblica istatìsticas agregadas subra s'atividade de s'utente
+        custom_css: CSS personalizadu
+        peers_api_enabled: Pùblica sa lista de serbidores iscobertos in s'API
+        profile_directory: Ativa diretòriu de profilos
+        show_domain_blocks: Ammustra blocos de domìniu
+        site_contact_username: Nòmine de utente de su cuntatu
+        site_short_description: Descritzione de su serbidore
+        site_title: Nòmine de su serbidore
+        thumbnail: Miniadura de su serbidore
       interactions:
         must_be_follower: Bloca is notìficas dae chie non ti sighit
         must_be_following: Bloca is notìficas dae gente chi non sighis
@@ -186,6 +199,7 @@ sc:
         mention: Una persone t'at mentovadu
         pending_account: Unu contu nou tenet bisòngiu de una revisione
         reblog: Una persone at cumpartzidu s'istadu tuo
+        report: Imbiu de un'informe nou
       rule:
         text: Règula
       tag:
@@ -193,6 +207,9 @@ sc:
         name: Eticheta
         trendable: Permite a custa eticheta de apàrrere in is tendèntzias
         usable: Permite a is tuts de impreare custa eticheta
+      user_role:
+        name: Nòmine
+        permissions_as_keys: Permissos
     'no': Nono
     recommended: Cussigiadu
     required:
diff --git a/config/locales/simple_form.tlh.yml b/config/locales/simple_form.tlh.yml
new file mode 100644
index 000000000..884714fb7
--- /dev/null
+++ b/config/locales/simple_form.tlh.yml
@@ -0,0 +1 @@
+tlh:
diff --git a/config/locales/tlh.yml b/config/locales/tlh.yml
new file mode 100644
index 000000000..884714fb7
--- /dev/null
+++ b/config/locales/tlh.yml
@@ -0,0 +1 @@
+tlh:

From 996c13a24d941e02458fa21d30e8334d04bdb2b5 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 6 Dec 2023 09:33:11 +0100
Subject: [PATCH 19/73] Update dependency core-js to v3.34.0 (#28241)

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 4226fcc0f..9e1622680 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5960,9 +5960,9 @@ __metadata:
   linkType: hard
 
 "core-js@npm:^3.30.2":
-  version: 3.33.3
-  resolution: "core-js@npm:3.33.3"
-  checksum: 08abdc9470c8228b9d09f61e62ab312738681202c4c34e9638889125b304b235f34c4fe22e9d41c20906ac0fcc807dca57c5ff7d6b90021bf64e8fe23461d9ab
+  version: 3.34.0
+  resolution: "core-js@npm:3.34.0"
+  checksum: 408a77898abe03bf3e5dec2a451c36f4745081cca9022f8bdf9b817d57bb6d3a534d555f47a4b95e1daa5e21dbc79122eac2402e25720d425f5925127e55dcd8
   languageName: node
   linkType: hard
 

From 3b710b96cf4a7ff743313f20b054c6220da3dd6b Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 6 Dec 2023 09:33:27 +0100
Subject: [PATCH 20/73] Update dependency irb to v1.10.1 (#28240)

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 9308a41c8..4a409a0ad 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -377,7 +377,7 @@ GEM
       terminal-table (>= 1.5.1)
     idn-ruby (0.1.5)
     io-console (0.6.0)
-    irb (1.10.0)
+    irb (1.10.1)
       rdoc
       reline (>= 0.3.8)
     jmespath (1.6.2)
@@ -608,7 +608,7 @@ GEM
       link_header (~> 0.0, >= 0.0.8)
     rdf-normalize (0.6.1)
       rdf (~> 3.2)
-    rdoc (6.6.0)
+    rdoc (6.6.1)
       psych (>= 4.0.0)
     redcarpet (3.6.0)
     redis (4.8.1)

From faffd81976092fc5a95fb359ec5844c2af76101d Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Wed, 6 Dec 2023 03:44:07 -0500
Subject: [PATCH 21/73] Remove double subject call in
 `services/unsuspend_account_service` spec (#28215)

---
 .../unsuspend_account_service_spec.rb         | 44 +++++++++++--------
 1 file changed, 26 insertions(+), 18 deletions(-)

diff --git a/spec/services/unsuspend_account_service_spec.rb b/spec/services/unsuspend_account_service_spec.rb
index c555b661e..2f737c621 100644
--- a/spec/services/unsuspend_account_service_spec.rb
+++ b/spec/services/unsuspend_account_service_spec.rb
@@ -45,14 +45,19 @@ RSpec.describe UnsuspendAccountService, type: :service do
         remote_follower.follow!(account)
       end
 
-      it "merges back into local followers' feeds" do
+      it 'merges back into feeds of local followers and sends update' do
         subject
+
+        expect_feeds_merged
+        expect_updates_sent
+      end
+
+      def expect_feeds_merged
         expect(FeedManager.instance).to have_received(:merge_into_home).with(account, local_follower)
         expect(FeedManager.instance).to have_received(:merge_into_list).with(account, list)
       end
 
-      it 'sends an update actor to followers and reporters' do
-        subject
+      def expect_updates_sent
         expect(a_request(:post, remote_follower.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once
         expect(a_request(:post, remote_reporter.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once
       end
@@ -73,19 +78,20 @@ RSpec.describe UnsuspendAccountService, type: :service do
           allow(resolve_account_service).to receive(:call).with(account).and_return(account)
         end
 
-        it 're-fetches the account' do
-          subject
+        it 're-fetches the account, merges feeds, and preserves suspended' do
+          expect { subject }
+            .to_not change_suspended_flag
+          expect_feeds_merged
           expect(resolve_account_service).to have_received(:call).with(account)
         end
 
-        it "merges back into local followers' feeds" do
-          subject
+        def expect_feeds_merged
           expect(FeedManager.instance).to have_received(:merge_into_home).with(account, local_follower)
           expect(FeedManager.instance).to have_received(:merge_into_list).with(account, list)
         end
 
-        it 'does not change the “suspended” flag' do
-          expect { subject }.to_not change(account, :suspended?)
+        def change_suspended_flag
+          change(account, :suspended?)
         end
       end
 
@@ -97,19 +103,20 @@ RSpec.describe UnsuspendAccountService, type: :service do
           end
         end
 
-        it 're-fetches the account' do
-          subject
+        it 're-fetches the account, does not merge feeds, marks suspended' do
+          expect { subject }
+            .to change_suspended_to_true
           expect(resolve_account_service).to have_received(:call).with(account)
+          expect_feeds_not_merged
         end
 
-        it "does not merge back into local followers' feeds" do
-          subject
+        def expect_feeds_not_merged
           expect(FeedManager.instance).to_not have_received(:merge_into_home).with(account, local_follower)
           expect(FeedManager.instance).to_not have_received(:merge_into_list).with(account, list)
         end
 
-        it 'marks account as suspended' do
-          expect { subject }.to change(account, :suspended?).from(false).to(true)
+        def change_suspended_to_true
+          change(account, :suspended?).from(false).to(true)
         end
       end
 
@@ -118,13 +125,14 @@ RSpec.describe UnsuspendAccountService, type: :service do
           allow(resolve_account_service).to receive(:call).with(account).and_return(nil)
         end
 
-        it 're-fetches the account' do
+        it 're-fetches the account and does not merge feeds' do
           subject
+
           expect(resolve_account_service).to have_received(:call).with(account)
+          expect_feeds_not_merged
         end
 
-        it "does not merge back into local followers' feeds" do
-          subject
+        def expect_feeds_not_merged
           expect(FeedManager.instance).to_not have_received(:merge_into_home).with(account, local_follower)
           expect(FeedManager.instance).to_not have_received(:merge_into_list).with(account, list)
         end

From 5517df61de1e867afa531268755ee893ffca3c99 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Wed, 6 Dec 2023 03:44:51 -0500
Subject: [PATCH 22/73] Remove double subject call in
 `services/activitypub/process_account_service` spec (#28214)

---
 .../process_account_service_spec.rb           | 30 +++++++++++--------
 1 file changed, 18 insertions(+), 12 deletions(-)

diff --git a/spec/services/activitypub/process_account_service_spec.rb b/spec/services/activitypub/process_account_service_spec.rb
index c02a0800a..09eb5ddee 100644
--- a/spec/services/activitypub/process_account_service_spec.rb
+++ b/spec/services/activitypub/process_account_service_spec.rb
@@ -129,12 +129,10 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do
       stub_const 'ActivityPub::ProcessAccountService::SUBDOMAINS_RATELIMIT', 5
     end
 
-    it 'creates at least some accounts' do
-      expect { subject }.to change { Account.remote.count }.by_at_least(2)
-    end
-
-    it 'creates no more account than the limit allows' do
-      expect { subject }.to change { Account.remote.count }.by_at_most(5)
+    it 'creates accounts without exceeding rate limit' do
+      expect { subject }
+        .to create_some_remote_accounts
+        .and create_fewer_than_rate_limit_accounts
     end
   end
 
@@ -195,12 +193,20 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do
       end
     end
 
-    it 'creates at least some accounts' do
-      expect { subject.call('user1', 'foo.test', payload) }.to change { Account.remote.count }.by_at_least(2)
-    end
-
-    it 'creates no more account than the limit allows' do
-      expect { subject.call('user1', 'foo.test', payload) }.to change { Account.remote.count }.by_at_most(5)
+    it 'creates accounts without exceeding rate limit' do
+      expect { subject.call('user1', 'foo.test', payload) }
+        .to create_some_remote_accounts
+        .and create_fewer_than_rate_limit_accounts
     end
   end
+
+  private
+
+  def create_some_remote_accounts
+    change(Account.remote, :count).by_at_least(2)
+  end
+
+  def create_fewer_than_rate_limit_accounts
+    change(Account.remote, :count).by_at_most(5)
+  end
 end

From be6bb1a10d6f1f23151198c6487f44145a0692ee Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Wed, 6 Dec 2023 03:45:19 -0500
Subject: [PATCH 23/73] Remove double subject call in
 `services/suspend_account_service` spec (#28213)

---
 spec/services/suspend_account_service_spec.rb | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/spec/services/suspend_account_service_spec.rb b/spec/services/suspend_account_service_spec.rb
index edb705008..c258995b7 100644
--- a/spec/services/suspend_account_service_spec.rb
+++ b/spec/services/suspend_account_service_spec.rb
@@ -18,14 +18,15 @@ RSpec.describe SuspendAccountService, type: :service do
       account.suspend!
     end
 
-    it "unmerges from local followers' feeds" do
-      subject
+    it 'unmerges from feeds of local followers and preserves suspended flag' do
+      expect { subject }
+        .to_not change_suspended_flag
       expect(FeedManager.instance).to have_received(:unmerge_from_home).with(account, local_follower)
       expect(FeedManager.instance).to have_received(:unmerge_from_list).with(account, list)
     end
 
-    it 'does not change the “suspended” flag' do
-      expect { subject }.to_not change(account, :suspended?)
+    def change_suspended_flag
+      change(account, :suspended?)
     end
   end
 

From ed7b5c091b62d63e695d2f1ff946e021fb37fa18 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Wed, 6 Dec 2023 03:51:09 -0500
Subject: [PATCH 24/73] Remove double subject call in
 `services/delete_account_service` spec (#28212)

---
 spec/services/delete_account_service_spec.rb | 25 +++++++++++++-------
 1 file changed, 16 insertions(+), 9 deletions(-)

diff --git a/spec/services/delete_account_service_spec.rb b/spec/services/delete_account_service_spec.rb
index 68ab491e4..8a19d3cf7 100644
--- a/spec/services/delete_account_service_spec.rb
+++ b/spec/services/delete_account_service_spec.rb
@@ -27,8 +27,15 @@ RSpec.describe DeleteAccountService, type: :service do
 
     let!(:account_note) { Fabricate(:account_note, account: account) }
 
-    it 'deletes associated owned records' do
-      expect { subject }.to change {
+    it 'deletes associated owned and target records and target notifications' do
+      expect { subject }
+        .to delete_associated_owned_records
+        .and delete_associated_target_records
+        .and delete_associated_target_notifications
+    end
+
+    def delete_associated_owned_records
+      change do
         [
           account.statuses,
           account.media_attachments,
@@ -39,23 +46,23 @@ RSpec.describe DeleteAccountService, type: :service do
           account.polls,
           account.account_notes,
         ].map(&:count)
-      }.from([2, 1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0, 0])
+      end.from([2, 1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0, 0])
     end
 
-    it 'deletes associated target records' do
-      expect { subject }.to change {
+    def delete_associated_target_records
+      change do
         [
           AccountPin.where(target_account: account),
         ].map(&:count)
-      }.from([1]).to([0])
+      end.from([1]).to([0])
     end
 
-    it 'deletes associated target notifications' do
-      expect { subject }.to change {
+    def delete_associated_target_notifications
+      change do
         %w(
           poll favourite status mention follow
         ).map { |type| Notification.where(type: type).count }
-      }.from([1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0])
+      end.from([1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0])
     end
   end
 

From 0e8ba19113182f74a0adde0ac75a35694833316c Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Wed, 6 Dec 2023 03:52:30 -0500
Subject: [PATCH 25/73] Add spec coverage for `CLI::Emoji` class (#28182)

---
 spec/fixtures/files/elite-assets.tar.gz | Bin 0 -> 17590 bytes
 spec/lib/mastodon/cli/emoji_spec.rb     |  54 ++++++++++++++++++++++++
 2 files changed, 54 insertions(+)
 create mode 100644 spec/fixtures/files/elite-assets.tar.gz

diff --git a/spec/fixtures/files/elite-assets.tar.gz b/spec/fixtures/files/elite-assets.tar.gz
new file mode 100644
index 0000000000000000000000000000000000000000..7b4f4425705c62fe4b538fa52148937e51a69552
GIT binary patch
literal 17590
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

literal 0
HcmV?d00001

diff --git a/spec/lib/mastodon/cli/emoji_spec.rb b/spec/lib/mastodon/cli/emoji_spec.rb
index 5d109eb52..530da91e7 100644
--- a/spec/lib/mastodon/cli/emoji_spec.rb
+++ b/spec/lib/mastodon/cli/emoji_spec.rb
@@ -4,5 +4,59 @@ require 'rails_helper'
 require 'mastodon/cli/emoji'
 
 describe Mastodon::CLI::Emoji do
+  subject { cli.invoke(action, args, options) }
+
+  let(:cli) { described_class.new }
+  let(:args) { [] }
+  let(:options) { {} }
+
   it_behaves_like 'CLI Command'
+
+  describe '#purge' do
+    let(:action) { :purge }
+
+    context 'with existing custom emoji' do
+      before { Fabricate(:custom_emoji) }
+
+      it 'reports a successful purge' do
+        expect { subject }
+          .to output_results('OK')
+      end
+    end
+  end
+
+  describe '#import' do
+    context 'with existing custom emoji' do
+      let(:import_path) { Rails.root.join('spec', 'fixtures', 'files', 'elite-assets.tar.gz') }
+      let(:action) { :import }
+      let(:args) { [import_path] }
+
+      it 'reports about imported emoji' do
+        expect { subject }
+          .to output_results('Imported 1')
+          .and change(CustomEmoji, :count).by(1)
+      end
+    end
+  end
+
+  describe '#export' do
+    context 'with existing custom emoji' do
+      before { Fabricate(:custom_emoji) }
+      after { File.delete(export_path) }
+
+      let(:export_path) { Rails.root.join('tmp', 'export.tar.gz') }
+      let(:args) { [Rails.root.join('tmp')] }
+      let(:action) { :export }
+
+      it 'reports about exported emoji' do
+        expect { subject }
+          .to output_results('Exported 1')
+          .and change { File.exist?(export_path) }.from(false).to(true)
+      end
+    end
+  end
+
+  def output_results(string)
+    output(a_string_including(string)).to_stdout
+  end
 end

From 954169966b23214a044fa8deda16be4b903526d5 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Wed, 6 Dec 2023 08:52:55 +0000
Subject: [PATCH 26/73] New Crowdin Translations (automated) (#28245)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/lt.json | 26 +++----
 config/locales/simple_form.lt.yml       | 91 ++++++++++++++++++++++++-
 2 files changed, 104 insertions(+), 13 deletions(-)

diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json
index e588b8538..58c80e411 100644
--- a/app/javascript/mastodon/locales/lt.json
+++ b/app/javascript/mastodon/locales/lt.json
@@ -40,7 +40,7 @@
   "account.follows.empty": "Šis (-i) naudotojas (-a) dar nieko neseka.",
   "account.follows_you": "Seka tave",
   "account.go_to_profile": "Eiti į profilį",
-  "account.hide_reblogs": "Slėpti \"boosts\" iš @{name}",
+  "account.hide_reblogs": "Slėpti pakėlimus iš @{name}",
   "account.in_memoriam": "Atminimui.",
   "account.joined_short": "Prisijungė",
   "account.languages": "Keisti prenumeruojamas kalbas",
@@ -49,19 +49,19 @@
   "account.media": "Medija",
   "account.mention": "Paminėti @{name}",
   "account.moved_to": "{name} nurodė, kad dabar jų nauja paskyra yra:",
-  "account.mute": "Užtildyti @{name}",
+  "account.mute": "Nutildyti @{name}",
   "account.mute_notifications_short": "Nutildyti pranešimus",
   "account.mute_short": "Nutildyti",
-  "account.muted": "Užtildytas",
+  "account.muted": "Nutildytas",
   "account.no_bio": "Nėra pateikto aprašymo.",
-  "account.open_original_page": "Atidaryti originalinį tinklalapį",
+  "account.open_original_page": "Atidaryti originalinį puslapį",
   "account.posts": "Įrašai",
   "account.posts_with_replies": "Įrašai ir atsakymai",
   "account.report": "Pranešti @{name}",
-  "account.requested": "Laukiama patvirtinimo. Spausk, kad atšaukti sekimo užklausą.",
+  "account.requested": "Laukiama patvirtinimo. Spausk, kad atšaukti sekimo užklausą",
   "account.requested_follow": "{name} paprašė tave sekti",
   "account.share": "Bendrinti @{name} profilį",
-  "account.show_reblogs": "Rodyti \"boosts\" iš @{name}",
+  "account.show_reblogs": "Rodyti pakėlimus iš @{name}",
   "account.statuses_counter": "{count, plural, one {{counter} įrašas} few {{counter} įrašai} many {{counter} įrašo} other {{counter} įrašų}}",
   "account.unblock": "Atblokuoti @{name}",
   "account.unblock_domain": "Atblokuoti domeną {domain}",
@@ -73,7 +73,7 @@
   "account.unmute_short": "Atitildyti",
   "account_note.placeholder": "Spausk norėdamas (-a) pridėti pastabą",
   "admin.dashboard.daily_retention": "Vartotojų išbuvimo rodiklis pagal dieną po registracijos",
-  "admin.dashboard.monthly_retention": "Vartotojų išbuvimo rodiklis pagal mėnesį po registracijos",
+  "admin.dashboard.monthly_retention": "Naudotojų išlaikymo rodiklis pagal mėnesį po registracijos",
   "admin.dashboard.retention.average": "Vidurkis",
   "admin.dashboard.retention.cohort": "Registravimo mėnuo",
   "admin.dashboard.retention.cohort_size": "Nauji naudotojai",
@@ -117,9 +117,9 @@
   "column.favourites": "Mėgstamiausi",
   "column.firehose": "Tiesioginiai padavimai",
   "column.follow_requests": "Sekti prašymus",
-  "column.home": "Pradžia",
+  "column.home": "Pagrindinis",
   "column.lists": "Sąrašai",
-  "column.mutes": "Užtildyti naudotojai",
+  "column.mutes": "Nutildyti naudotojai",
   "column.notifications": "Pranešimai",
   "column.pins": "Prisegti įrašai",
   "column.public": "Federacinė laiko skalė",
@@ -141,13 +141,13 @@
   "compose.saved.body": "Įrašas išsaugotas.",
   "compose_form.direct_message_warning_learn_more": "Sužinoti daugiau",
   "compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.",
-  "compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
-  "compose_form.lock_disclaimer": "Jūsų paskyra nėra {locked}. Kiekvienas gali jus sekti ir peržiūrėti tik sekėjams skirtus įrašus.",
+  "compose_form.hashtag_warning": "Šis įrašas nebus įtraukta į jokį saitažodį, nes ji nėra vieša. Tik viešų įrašų galima ieškoti pagal saitažodį.",
+  "compose_form.lock_disclaimer": "Tavo paskyra nėra {locked}. Bet kas gali sekti tave ir peržiūrėti tik sekėjams skirtus įrašus.",
   "compose_form.lock_disclaimer.lock": "užrakinta",
   "compose_form.placeholder": "Kas tavo mintyse?",
   "compose_form.poll.add_option": "Pridėti pasirinkimą",
   "compose_form.poll.duration": "Apklausos trukmė",
-  "compose_form.poll.option_placeholder": "Pasirinkimas {number}",
+  "compose_form.poll.option_placeholder": "{number} pasirinkimas",
   "compose_form.poll.remove_option": "Pašalinti šį pasirinkimą",
   "compose_form.poll.switch_to_multiple": "Keisti apklausą, kad būtų galima pasirinkti kelis pasirinkimus",
   "compose_form.poll.switch_to_single": "Pakeisti apklausą, kad būtų galima pasirinkti vieną variantą",
@@ -528,6 +528,8 @@
   "search_results.hashtags": "Saitažodžiai",
   "search_results.nothing_found": "Nepavyko rasti nieko pagal šiuos paieškos terminus.",
   "search_results.statuses": "Toots",
+  "server_banner.about_active_users": "Žmonės, kurie naudojosi šiuo serveriu per pastarąsias 30 dienų (mėnesio aktyvūs naudotojai)",
+  "server_banner.active_users": "aktyvūs naudotojai",
   "sign_in_banner.sign_in": "Prisijungimas",
   "sign_in_banner.text": "Prisijunk, kad galėtum sekti profilius arba saitažodžius, mėgsti, bendrinti ir atsakyti į įrašus. Taip pat gali bendrauti iš savo paskyros kitame serveryje.",
   "status.admin_status": "Open this status in the moderation interface",
diff --git a/config/locales/simple_form.lt.yml b/config/locales/simple_form.lt.yml
index d53b7105e..6eb90340d 100644
--- a/config/locales/simple_form.lt.yml
+++ b/config/locales/simple_form.lt.yml
@@ -63,6 +63,8 @@ lt:
         setting_use_pending_items: Slėpti laiko skalės naujienas po paspaudimo, vietoj automatinio kanalo slinkimo
         username: Gali naudoti raides, skaičius ir pabraukimus
         whole_word: Kai raktažodis ar frazė yra tik raidinis ir skaitmeninis, jis bus taikomas tik tada, jei atitiks visą žodį
+      email_domain_block:
+        with_dns_records: Bus bandoma išspręsti nurodyto domeno DNS įrašus, o rezultatai taip pat bus blokuojami
       featured_tag:
         name: 'Štai keletas pastaruoju metu dažniausiai saitažodžių, kurių tu naudojai:'
       filters:
@@ -77,15 +79,98 @@ lt:
         site_contact_email: Kaip žmonės gali su tavimi susisiekti teisiniais ar pagalbos užklausimais.
         site_contact_username: Kaip žmonės gali tave pasiekti Mastodon.
         site_extended_description: Bet kokia papildoma informacija, kuri gali būti naudinga lankytojams ir naudotojams. Gali būti struktūrizuota naudojant Markdown sintaksę.
+        thumbnail: Maždaug 2:1 dydžio vaizdas, rodomas šalia tavo serverio informacijos.
+        timeline_preview: Atsijungę lankytojai galės naršyti naujausius viešus įrašus, esančius serveryje.
         trends: Trendai rodo, kurios įrašai, saitažodžiai ir naujienų istorijos tavo serveryje sulaukia didžiausio susidomėjimo.
       sessions:
         otp: 'Įvesk telefono programėlėje sugeneruotą dviejų tapatybės kodą arba naudok vieną iš atkūrimo kodų:'
         webauthn: Jei tai USB raktas, būtinai jį įkišk ir, jei reikia, paspausk.
       settings:
         indexable: Tavo profilio puslapis gali būti rodomas paieškos rezultatuose Google, Bing ir kituose.
+      user:
+        chosen_languages: Kai pažymėta, viešose laiko skalėse bus rodomi tik įrašai pasirinktomis kalbomis
+        role: Vaidmuo valdo, kokius leidimus naudotojas (-a) turi
     labels:
+      account:
+        indexable: Įtraukti viešus įrašus į paieškos rezultatus
+        show_collections: Rodyti sekimus ir sekėjus profilyje
+        unlocked: Automatiškai priimti naujus sekėjus
+      account_warning_preset:
+        title: Pavadinimas
+      admin_account_action:
+        include_statuses: Įtraukti praneštus įrašus į el. laišką
+      defaults:
+        avatar: Profilio nuotrauka
+        bot: Tai automatinė paskyra
+        chosen_languages: Filtruoti kalbas
+        display_name: Rodomas vardas
+        email: El. pašto adresas
+        expires_in: Nustoja galioti po
+        fields: Papildomi laukai
+        irreversible: Mesti vietoj slėpti
+        locale: Sąsajos kalba
+        max_uses: Maksimalus naudojimo skaičius
+        new_password: Naujas slaptažodis
+        note: Biografija
+        password: Slaptažodis
+        phrase: Raktažodis arba frazė
+        setting_auto_play_gif: Automatiškai leisti animuotų GIF
+        setting_boost_modal: Rodyti patvirtinimo dialogą prieš pakėliant įrašą
+        setting_default_language: Skelbimo kalba
+        setting_default_privacy: Skelbimo privatumas
+        setting_default_sensitive: Visada žymėti mediją kaip jautrią
+        setting_delete_modal: Rodyti patvirtinimo dialogą prieš ištrinant įrašą
+        setting_display_media: Medijos rodymas
+        setting_display_media_hide_all: Slėpti viską
+        setting_display_media_show_all: Rodyti viską
+        setting_expand_spoilers: Visada išplėsti įrašus, pažymėtus turinio įspėjimais
+        setting_hide_network: Slėpti savo socialinę diagramą
+        setting_system_font_ui: Naudoti numatytąjį sistemos šriftą
+        setting_theme: Svetainės tema
+        setting_use_pending_items: Lėtas režimas
+        title: Pavadinimas
+        type: Importo tipas
+        username: Naudotojo vardas
+        username_or_email: Naudotojo vardas arba el. paštas
+        whole_word: Visas žodis
+      email_domain_block:
+        with_dns_records: Įtraukti MX įrašus ir domeno IP adresus
       featured_tag:
         name: Saitažodis
+      filters:
+        actions:
+          hide: Slėpti visiškai
+          warn: Slėpti su įspėjimu
+      form_admin_settings:
+        activity_api_enabled: Skelbti suvestinį statistiką apie naudotojų veiklą per API
+        bootstrap_timeline_accounts: Visada rekomenduoti šias paskyras naujiems naudotojams
+        content_cache_retention_period: Turinio talpyklos išlaikymo laikotarpis
+        custom_css: Pasirinktinis CSS
+        mascot: Pasirinktinis talismanas (pasenęs)
+        registrations_mode: Kas gali užsiregistruoti
+        show_domain_blocks_rationale: Rodyti, kodėl domenai buvo užblokuoti
+        site_extended_description: Išplėstas aprašymas
+        site_short_description: Serverio aprašymas
+        site_terms: Privatumo politika
+        site_title: Serverio pavadinimas
+        theme: Numatytoji tema
+        thumbnail: Serverio miniatūra
+      invite_request:
+        text: Kodėl nori prisijungti?
+      notification_emails:
+        favourite: Kažkas pamėgo tavo įrašą
+        follow: Kažkas seka tave
+        follow_request: Kažkas paprašė sekti tave
+        mention: Kažkas paminėjo tave
+        pending_account: Reikia peržiūros naujam paskyrui
+        reblog: Kažkas pakėlė tavo įrašą
+        software_updates:
+          label: Yra nauja Mastodon versija
+          patch: Pranešti apie klaidų ištaisymo atnaujinimus
+      rule:
+        text: Taisyklė
+      settings:
+        show_application: Rodyti, iš kurios programėles išsiuntei įrašą
       tag:
         listable: Leisti šį saitažodį rodyti paieškose ir pasiūlymuose
         name: Saitažodis
@@ -93,11 +178,15 @@ lt:
         usable: Leisti įrašams naudoti šį saitažodį
       user:
         role: Vaidmuo
+        time_zone: Laiko juosta
       user_role:
+        color: Ženklelio spalva
+        highlighted: Rodyti vaidmenį kaip ženklelį naudotojo profiliuose
+        name: Pavadinimas
         permissions_as_keys: Leidimai
         position: Prioritetas
       webhook:
-        events: Įgalinti įvykiai
+        events: Įjungti įvykiai
         template: Naudingosios apkrovos šablonas
         url: Galutinio taško URL
     'no': Ne

From 42afd303246abe1cf61752ab53cca466ea3cefdf Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Wed, 6 Dec 2023 05:19:24 -0500
Subject: [PATCH 27/73] Replace Sprockets with Propshaft (#28239)

---
 .github/renovate.json5             |  1 -
 Gemfile                            |  3 +--
 Gemfile.lock                       | 15 ++++++---------
 config/application.rb              |  1 -
 config/environments/development.rb | 11 -----------
 config/initializers/assets.rb      | 16 ----------------
 6 files changed, 7 insertions(+), 40 deletions(-)
 delete mode 100644 config/initializers/assets.rb

diff --git a/.github/renovate.json5 b/.github/renovate.json5
index 895dbfbad..a7998ddfd 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -50,7 +50,6 @@
       matchManagers: ['bundler'],
       matchPackageNames: [
         'rack', // Needs to be synced with Rails version
-        'sprockets', // Requires manual upgrade https://github.com/rails/sprockets/blob/master/UPGRADING.md#guide-to-upgrading-from-sprockets-3x-to-4x
         'strong_migrations', // Requires manual upgrade
         'sidekiq', // Requires manual upgrade
         'sidekiq-unique-jobs', // Requires manual upgrades and sync with Sidekiq version
diff --git a/Gemfile b/Gemfile
index e3fb39e16..cfcbcc0d3 100644
--- a/Gemfile
+++ b/Gemfile
@@ -5,7 +5,7 @@ ruby '>= 3.0.0'
 
 gem 'puma', '~> 6.3'
 gem 'rails', '~> 7.1.1'
-gem 'sprockets', '~> 3.7.2'
+gem 'propshaft'
 gem 'thor', '~> 1.2'
 gem 'rack', '~> 2.2.7'
 
@@ -89,7 +89,6 @@ gem 'sidekiq-unique-jobs', '~> 7.1'
 gem 'sidekiq-bulk', '~> 0.2.0'
 gem 'simple-navigation', '~> 4.4'
 gem 'simple_form', '~> 5.2'
-gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie'
 gem 'stoplight', '~> 3.0.1'
 gem 'strong_migrations', '1.6.4'
 gem 'tty-prompt', '~> 0.23', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 4a409a0ad..738562116 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -534,6 +534,11 @@ GEM
       net-smtp
       premailer (~> 1.7, >= 1.7.9)
     private_address_check (0.5.0)
+    propshaft (0.8.0)
+      actionpack (>= 7.0.0)
+      activesupport (>= 7.0.0)
+      rack
+      railties (>= 7.0.0)
     psych (5.1.1.1)
       stringio
     public_suffix (5.0.4)
@@ -736,13 +741,6 @@ GEM
     simplecov-lcov (0.8.0)
     simplecov_json_formatter (0.1.4)
     smart_properties (1.17.0)
-    sprockets (3.7.2)
-      concurrent-ruby (~> 1.0)
-      rack (> 1, < 3)
-    sprockets-rails (3.4.2)
-      actionpack (>= 5.2)
-      activesupport (>= 5.2)
-      sprockets (>= 3.0.0)
     stackprof (0.2.25)
     statsd-ruby (1.5.0)
     stoplight (3.0.2)
@@ -911,6 +909,7 @@ DEPENDENCIES
   posix-spawn
   premailer-rails
   private_address_check (~> 0.5)
+  propshaft
   public_suffix (~> 5.0)
   puma (~> 6.3)
   pundit (~> 2.3)
@@ -949,8 +948,6 @@ DEPENDENCIES
   simple_form (~> 5.2)
   simplecov (~> 0.22)
   simplecov-lcov (~> 0.8)
-  sprockets (~> 3.7.2)
-  sprockets-rails (~> 3.4)
   stackprof
   stoplight (~> 3.0.1)
   strong_migrations (= 1.6.4)
diff --git a/config/application.rb b/config/application.rb
index 99ee4ffd7..b6426516e 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -14,7 +14,6 @@ require 'active_job/railtie'
 # require 'action_mailbox/engine'
 # require 'action_text/engine'
 # require 'rails/test_unit/railtie'
-require 'sprockets/railtie'
 
 # Used to be implicitly required in action_mailbox/engine
 require 'mail'
diff --git a/config/environments/development.rb b/config/environments/development.rb
index e601fc014..3c13ada38 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -71,17 +71,6 @@ Rails.application.configure do
   # Highlight code that enqueued background job in logs.
   config.active_job.verbose_enqueue_logs = true
 
-  # Debug mode disables concatenation and preprocessing of assets.
-  config.assets.debug = true
-
-  # Suppress logger output for asset requests.
-  config.assets.quiet = true
-
-  # Adds additional error checking when serving assets at runtime.
-  # Checks for improperly declared sprockets dependencies.
-  # Raises helpful error messages.
-  config.assets.raise_runtime_errors = true
-
   # Raises error for missing translations.
   # config.i18n.raise_on_missing_translations = true
 
diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb
deleted file mode 100644
index e1fd5f8ce..000000000
--- a/config/initializers/assets.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-# Be sure to restart your server when you modify this file.
-
-# Version of your assets, change this if you want to expire all your assets.
-Rails.application.config.assets.version = '1.0'
-
-# Add additional assets to the asset load path.
-# Rails.application.config.assets.paths << Emoji.images_path
-
-# Precompile additional assets.
-# application.js, application.css, and all non-JS/CSS in the app/assets
-# folder are already added.
-# Rails.application.config.assets.precompile += %w( admin.js admin.css )
-
-Rails.application.config.assets.initialize_on_precompile = true

From ee83d5c7600b2cdc96db78c5ec40d8747853ba30 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Wed, 6 Dec 2023 08:42:12 -0500
Subject: [PATCH 28/73] Enable the eslint `react/no-unknown-property` rule
 (#28217)

---
 .eslintrc.js                                                | 1 -
 .../mastodon/features/interaction_modal/index.jsx           | 6 +++---
 .../mastodon/features/ui/components/navigation_panel.jsx    | 2 +-
 app/javascript/mastodon/features/video/index.jsx            | 1 -
 package.json                                                | 2 +-
 yarn.lock                                                   | 4 ++--
 6 files changed, 7 insertions(+), 9 deletions(-)

diff --git a/.eslintrc.js b/.eslintrc.js
index 176879034..e2d16a54a 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -120,7 +120,6 @@ module.exports = defineConfig({
     'react/jsx-uses-react': 'off', // not needed with new JSX transform
     'react/jsx-wrap-multilines': 'error',
     'react/no-deprecated': 'off',
-    'react/no-unknown-property': 'off',
     'react/react-in-jsx-scope': 'off', // not needed with new JSX transform
     'react/self-closing-comp': 'error',
 
diff --git a/app/javascript/mastodon/features/interaction_modal/index.jsx b/app/javascript/mastodon/features/interaction_modal/index.jsx
index 4f145f9ed..216c63a7e 100644
--- a/app/javascript/mastodon/features/interaction_modal/index.jsx
+++ b/app/javascript/mastodon/features/interaction_modal/index.jsx
@@ -298,9 +298,9 @@ class LoginForm extends React.PureComponent {
             onFocus={this.handleFocus}
             onBlur={this.handleBlur}
             onKeyDown={this.handleKeyDown}
-            autocomplete='off'
-            autocapitalize='off'
-            spellcheck='false'
+            autoComplete='off'
+            autoCapitalize='off'
+            spellCheck='false'
           />
 
           <Button onClick={this.handleSubmit} disabled={isSubmitting || error}><FormattedMessage id='interaction_modal.login.action' defaultMessage='Take me home' /></Button>
diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
index 4b0e03a0f..d1b2a0910 100644
--- a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
+++ b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
@@ -82,7 +82,7 @@ class NavigationPanel extends Component {
         </div>
 
         {banner &&
-          <div class='navigation-panel__banner'>
+          <div className='navigation-panel__banner'>
             {banner}
           </div>
         }
diff --git a/app/javascript/mastodon/features/video/index.jsx b/app/javascript/mastodon/features/video/index.jsx
index bef14ea27..9ff6d3589 100644
--- a/app/javascript/mastodon/features/video/index.jsx
+++ b/app/javascript/mastodon/features/video/index.jsx
@@ -612,7 +612,6 @@ class Video extends PureComponent {
             aria-label={alt}
             title={alt}
             lang={lang}
-            volume={volume}
             onClick={this.togglePlay}
             onKeyDown={this.handleVideoKeyDown}
             onPlay={this.handlePlay}
diff --git a/package.json b/package.json
index 543af8a4b..500d6d2cd 100644
--- a/package.json
+++ b/package.json
@@ -193,7 +193,7 @@
     "eslint-plugin-jsx-a11y": "~6.8.0",
     "eslint-plugin-prettier": "^5.0.0",
     "eslint-plugin-promise": "~6.1.1",
-    "eslint-plugin-react": "~7.33.0",
+    "eslint-plugin-react": "^7.33.2",
     "eslint-plugin-react-hooks": "^4.6.0",
     "husky": "^8.0.3",
     "jest": "^29.5.0",
diff --git a/yarn.lock b/yarn.lock
index 9e1622680..774eefec8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2371,7 +2371,7 @@ __metadata:
     eslint-plugin-jsx-a11y: "npm:~6.8.0"
     eslint-plugin-prettier: "npm:^5.0.0"
     eslint-plugin-promise: "npm:~6.1.1"
-    eslint-plugin-react: "npm:~7.33.0"
+    eslint-plugin-react: "npm:^7.33.2"
     eslint-plugin-react-hooks: "npm:^4.6.0"
     file-loader: "npm:^6.2.0"
     font-awesome: "npm:^4.7.0"
@@ -7501,7 +7501,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"eslint-plugin-react@npm:~7.33.0":
+"eslint-plugin-react@npm:^7.33.2":
   version: 7.33.2
   resolution: "eslint-plugin-react@npm:7.33.2"
   dependencies:

From 23b16aaab0f475d8584e948d2f4ce6d64a68cac3 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 6 Dec 2023 14:50:59 +0100
Subject: [PATCH 29/73] Update dependency selenium-webdriver to v4.16.0
 (#28246)

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 738562116..cd04f2782 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -707,7 +707,7 @@ GEM
     scenic (1.7.0)
       activerecord (>= 4.0.0)
       railties (>= 4.0.0)
-    selenium-webdriver (4.15.0)
+    selenium-webdriver (4.16.0)
       rexml (~> 3.2, >= 3.2.5)
       rubyzip (>= 1.2.2, < 3.0)
       websocket (~> 1.0)

From af66d3d836b89286423d4ba9fe1518bb6317b499 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Wed, 6 Dec 2023 09:15:54 -0500
Subject: [PATCH 30/73] Use `following` and `followers` scopes in CLI (#28154)

---
 lib/mastodon/cli/accounts.rb | 12 ++++--------
 1 file changed, 4 insertions(+), 8 deletions(-)

diff --git a/lib/mastodon/cli/accounts.rb b/lib/mastodon/cli/accounts.rb
index 33520df25..414675303 100644
--- a/lib/mastodon/cli/accounts.rb
+++ b/lib/mastodon/cli/accounts.rb
@@ -472,15 +472,13 @@ module Mastodon::CLI
       end
 
       total     = 0
-      total    += Account.where(id: ::Follow.where(account: account).select(:target_account_id)).count if options[:follows]
-      total    += Account.where(id: ::Follow.where(target_account: account).select(:account_id)).count if options[:followers]
+      total    += account.following.reorder(nil).count if options[:follows]
+      total    += account.followers.reorder(nil).count if options[:followers]
       progress  = create_progress_bar(total)
       processed = 0
 
       if options[:follows]
-        scope = Account.where(id: ::Follow.where(account: account).select(:target_account_id))
-
-        scope.find_each do |target_account|
+        account.following.reorder(nil).find_each do |target_account|
           UnfollowService.new.call(account, target_account)
         rescue => e
           progress.log pastel.red("Error processing #{target_account.id}: #{e}")
@@ -493,9 +491,7 @@ module Mastodon::CLI
       end
 
       if options[:followers]
-        scope = Account.where(id: ::Follow.where(target_account: account).select(:account_id))
-
-        scope.find_each do |target_account|
+        account.followers.reorder(nil).find_each do |target_account|
           UnfollowService.new.call(target_account, account)
         rescue => e
           progress.log pastel.red("Error processing #{target_account.id}: #{e}")

From 658ad7a6cabe0c8b6b3ae11db1a4cc8434152ec4 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 6 Dec 2023 23:22:25 +0100
Subject: [PATCH 31/73] Fix flaky tests related to file creation (#28248)

---
 .../post_deployment_migration_generator_spec.rb    |  2 +-
 spec/lib/mastodon/cli/emoji_spec.rb                | 14 ++++++++++----
 2 files changed, 11 insertions(+), 5 deletions(-)

diff --git a/spec/generators/post_deployment_migration_generator_spec.rb b/spec/generators/post_deployment_migration_generator_spec.rb
index d770a78e9..55e70a791 100644
--- a/spec/generators/post_deployment_migration_generator_spec.rb
+++ b/spec/generators/post_deployment_migration_generator_spec.rb
@@ -12,7 +12,7 @@ describe PostDeploymentMigrationGenerator, type: :generator do
   include FileUtils
 
   tests described_class
-  destination File.expand_path('../../tmp', __dir__)
+  destination Rails.root.join('tmp', 'generator-test')
   before { prepare_destination }
   after { rm_rf(destination_root) }
 
diff --git a/spec/lib/mastodon/cli/emoji_spec.rb b/spec/lib/mastodon/cli/emoji_spec.rb
index 530da91e7..3441413b9 100644
--- a/spec/lib/mastodon/cli/emoji_spec.rb
+++ b/spec/lib/mastodon/cli/emoji_spec.rb
@@ -41,11 +41,17 @@ describe Mastodon::CLI::Emoji do
 
   describe '#export' do
     context 'with existing custom emoji' do
-      before { Fabricate(:custom_emoji) }
-      after { File.delete(export_path) }
+      before do
+        FileUtils.rm_rf(export_path.dirname)
+        FileUtils.mkdir_p(export_path.dirname)
 
-      let(:export_path) { Rails.root.join('tmp', 'export.tar.gz') }
-      let(:args) { [Rails.root.join('tmp')] }
+        Fabricate(:custom_emoji)
+      end
+
+      after { FileUtils.rm_rf(export_path.dirname) }
+
+      let(:export_path) { Rails.root.join('tmp', 'cli-tests', 'export.tar.gz') }
+      let(:args) { [export_path.dirname.to_s] }
       let(:action) { :export }
 
       it 'reports about exported emoji' do

From 5e8a38e2540e4c067fe8756bb78a1cc854943db6 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 7 Dec 2023 09:42:47 +0100
Subject: [PATCH 32/73] Update dependency oj to v3.16.2 (#28263)

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 cd04f2782..45ad60558 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -484,7 +484,8 @@ GEM
     nokogiri (1.15.5)
       mini_portile2 (~> 2.8.2)
       racc (~> 1.4)
-    oj (3.16.1)
+    oj (3.16.2)
+      bigdecimal (~> 3.1)
     omniauth (2.1.1)
       hashie (>= 3.4.6)
       rack (>= 2.2.3)

From 54db2006a96639a51d42353bef8c18b5f55b1c3c Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 7 Dec 2023 08:44:16 +0000
Subject: [PATCH 33/73] Update dependency sass-loader to v10.5.0 (#28254)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 yarn.lock | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/yarn.lock b/yarn.lock
index 774eefec8..45f58eaad 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -14663,8 +14663,8 @@ __metadata:
   linkType: hard
 
 "sass-loader@npm:^10.2.0":
-  version: 10.4.1
-  resolution: "sass-loader@npm:10.4.1"
+  version: 10.5.0
+  resolution: "sass-loader@npm:10.5.0"
   dependencies:
     klona: "npm:^2.0.4"
     loader-utils: "npm:^2.0.0"
@@ -14673,7 +14673,7 @@ __metadata:
     semver: "npm:^7.3.2"
   peerDependencies:
     fibers: ">= 3.1.0"
-    node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
+    node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0
     sass: ^1.3.0
     webpack: ^4.36.0 || ^5.0.0
   peerDependenciesMeta:
@@ -14683,7 +14683,7 @@ __metadata:
       optional: true
     sass:
       optional: true
-  checksum: bf04a440fe471928f3cf884bc12c6b70bc391795b35510b1b9021e8a2cca3b8f966aef9518f4171e87e9cb78193a774f695921e6b61881a1580ae0a3c7b1b5e4
+  checksum: be5da7784fd21c4f526cc3afaa1a765ba44cdc2f9798ecbac87b296ab44184ac5ba9bbda68a7a86f8cdcb6130acceefeb8912260fca14bdfc97f9dad02658400
   languageName: node
   linkType: hard
 

From 336d6260ba8ace929bfb5482cc72740d013dd1a0 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 7 Dec 2023 09:44:45 +0100
Subject: [PATCH 34/73] Update dependency chewy to v7.3.5 (#28253)

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 45ad60558..7a7fdb01c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -197,7 +197,7 @@ GEM
       activesupport
     cbor (0.5.9.6)
     charlock_holmes (0.7.7)
-    chewy (7.3.4)
+    chewy (7.3.5)
       activesupport (>= 5.2)
       elasticsearch (>= 7.12.0, < 7.14.0)
       elasticsearch-dsl

From 7593465c23d4b0c4d44304712f7957aa1d9ce9d7 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Thu, 7 Dec 2023 10:05:08 +0100
Subject: [PATCH 35/73] Fix error when processing link preview with an array as
 `inLanguage` (#28252)

---
 app/lib/link_details_extractor.rb | 1 +
 1 file changed, 1 insertion(+)

diff --git a/app/lib/link_details_extractor.rb b/app/lib/link_details_extractor.rb
index a96612cab..bb031986d 100644
--- a/app/lib/link_details_extractor.rb
+++ b/app/lib/link_details_extractor.rb
@@ -37,6 +37,7 @@ class LinkDetailsExtractor
 
     def language
       lang = json['inLanguage']
+      lang = lang.first if lang.is_a?(Array)
       lang.is_a?(Hash) ? (lang['alternateName'] || lang['name']) : lang
     end
 

From e0dacf6b4ce090cf6a30f8b1f37283b174b32c40 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu, 7 Dec 2023 10:38:44 +0100
Subject: [PATCH 36/73] New Crowdin Translations (automated) (#28264)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/hu.json | 54 ++++++++++++-------------
 config/locales/activerecord.hu.yml      |  2 +-
 config/locales/devise.hu.yml            |  8 ++--
 config/locales/hu.yml                   | 28 ++++++-------
 config/locales/simple_form.hu.yml       |  8 ++--
 config/locales/simple_form.hy.yml       |  2 +-
 6 files changed, 51 insertions(+), 51 deletions(-)

diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 9bef81417..d890e4188 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -13,7 +13,7 @@
   "about.rules": "Kiszolgáló szabályai",
   "account.account_note_header": "Feljegyzés",
   "account.add_or_remove_from_list": "Hozzáadás vagy eltávolítás a listákról",
-  "account.badges.bot": "Bot",
+  "account.badges.bot": "Automatizált",
   "account.badges.group": "Csoport",
   "account.block": "@{name} letiltása",
   "account.block_domain": "Domain blokkolása: {domain}",
@@ -63,8 +63,8 @@
   "account.share": "@{name} profiljának megosztása",
   "account.show_reblogs": "@{name} megtolásainak mutatása",
   "account.statuses_counter": "{count, plural, one {{counter} Bejegyzés} other {{counter} Bejegyzés}}",
-  "account.unblock": "@{name} tiltásának feloldása",
-  "account.unblock_domain": "{domain} domain tiltás feloldása",
+  "account.unblock": "@{name} letiltásának feloldása",
+  "account.unblock_domain": "{domain} domain tiltásának feloldása",
   "account.unblock_short": "Tiltás feloldása",
   "account.unendorse": "Ne jelenjen meg a profilodon",
   "account.unfollow": "Követés megszüntetése",
@@ -138,21 +138,21 @@
   "compose.language.search": "Nyelv keresése...",
   "compose.published.body": "A bejegyzés publikálásra került.",
   "compose.published.open": "Megnyitás",
-  "compose.saved.body": "A bejegyzés mentésre került.",
+  "compose.saved.body": "A bejegyzés mentve.",
   "compose_form.direct_message_warning_learn_more": "Tudj meg többet",
-  "compose_form.encryption_warning": "A bejegyzések Mastodonon nem használnak végpontok közötti titkosítást. Ne ossz meg semmilyen érzékeny információt Mastodonon.",
+  "compose_form.encryption_warning": "A bejegyzések a Mastodonon nem használnak végpontok közti titkosítást. Ne ossz meg semmilyen érzékeny információt a Mastodonon.",
   "compose_form.hashtag_warning": "Ez a bejegyzésed nem fog megjelenni semmilyen hashtag alatt, mivel nem nyilvános. Csak a nyilvános bejegyzések kereshetők hashtaggel.",
   "compose_form.lock_disclaimer": "A fiókod nincs {locked}. Bárki követni tud, hogy megtekintse a kizárólag követőknek szánt bejegyzéseket.",
-  "compose_form.lock_disclaimer.lock": "lezárva",
+  "compose_form.lock_disclaimer.lock": "zárolva",
   "compose_form.placeholder": "Mi jár a fejedben?",
   "compose_form.poll.add_option": "Lehetőség hozzáadása",
   "compose_form.poll.duration": "Szavazás időtartama",
   "compose_form.poll.option_placeholder": "{number}. lehetőség",
-  "compose_form.poll.remove_option": "Lehetőség törlése",
+  "compose_form.poll.remove_option": "Lehetőség eltávolítása",
   "compose_form.poll.switch_to_multiple": "Szavazás megváltoztatása több választásosra",
   "compose_form.poll.switch_to_single": "Szavazás megváltoztatása egyetlen választásosra",
   "compose_form.publish": "Közzététel",
-  "compose_form.publish_form": "Közzététel",
+  "compose_form.publish_form": "Új bejegyzés",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.save_changes": "Módosítások mentése",
   "compose_form.sensitive.hide": "{count, plural, one {Média kényesnek jelölése} other {Média kényesnek jelölése}}",
@@ -195,13 +195,13 @@
   "copy_icon_button.copied": "A szöveg a vágólapra másolva",
   "copypaste.copied": "Másolva",
   "copypaste.copy_to_clipboard": "Másolás vágólapra",
-  "directory.federated": "Az ismert fediverzumból",
+  "directory.federated": "Az ismert födiverzumból",
   "directory.local": "Csak {domain} tartományból",
   "directory.new_arrivals": "Új csatlakozók",
   "directory.recently_active": "Nemrég aktív",
   "disabled_account_banner.account_settings": "Fiókbeállítások",
   "disabled_account_banner.text": "A(z) {disabledAccount} fiókod jelenleg le van tiltva.",
-  "dismissable_banner.community_timeline": "Ezek a legfrissebb nyilvános bejegyzések, amelyeket {domain} tartományban levő kiszolgáló fiókjait használó emberek tettek közzé.",
+  "dismissable_banner.community_timeline": "Ezek a legfrissebb nyilvános bejegyzések, amelyeket a(z) {domain} kiszolgáló fiókjait használó emberek tették közzé.",
   "dismissable_banner.dismiss": "Elvetés",
   "dismissable_banner.explore_links": "Jelenleg ezekről a hírekről beszélgetnek az ezen és a központosítás nélküli hálózat többi kiszolgálóján lévő emberek.",
   "dismissable_banner.explore_statuses": "Ezek jelenleg népszerűvé váló bejegyzések a háló különböző szegleteiből. Az újabb vagy több megtolással rendelkező bejegyzéseket, illetve a kedvencnek jelöléssel rendelkezőeket rangsoroljuk előrébb.",
@@ -216,14 +216,14 @@
   "emoji_button.food": "Étel és Ital",
   "emoji_button.label": "Emodzsi beszúrása",
   "emoji_button.nature": "Természet",
-  "emoji_button.not_found": "Nincsenek emodzsik!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.not_found": "Nem találhatók emodzsik",
   "emoji_button.objects": "Tárgyak",
   "emoji_button.people": "Emberek",
   "emoji_button.recent": "Gyakran használt",
   "emoji_button.search": "Keresés...",
   "emoji_button.search_results": "Keresési találatok",
   "emoji_button.symbols": "Szimbólumok",
-  "emoji_button.travel": "Utazás és Helyek",
+  "emoji_button.travel": "Utazás és helyek",
   "empty_column.account_hides_collections": "Ez a felhasználó úgy döntött, hogy nem teszi elérhetővé ezt az információt.",
   "empty_column.account_suspended": "Fiók felfüggesztve",
   "empty_column.account_timeline": "Itt nincs bejegyzés!",
@@ -245,7 +245,7 @@
   "empty_column.mutes": "Még egy felhasználót sem némítottál le.",
   "empty_column.notifications": "Jelenleg még nincsenek értesítéseid. Ha mások kapcsolatba lépnek veled, ezek itt lesznek láthatóak.",
   "empty_column.public": "Jelenleg itt nincs semmi! Írj valamit nyilvánosan vagy kövess más kiszolgálón levő felhasználókat, hogy megtöltsd.",
-  "error.unexpected_crash.explanation": "Egy hiba vagy böngésző inkompatibilitás miatt ez az oldal nem jeleníthető meg rendesen.",
+  "error.unexpected_crash.explanation": "Egy kód- vagy böngészőkompatibilitási hiba miatt ez az oldal nem jeleníthető meg helyesen.",
   "error.unexpected_crash.explanation_addons": "Ezt az oldalt nem lehet helyesen megjeleníteni. Ezt a hibát valószínűleg egy böngésző kiegészítő vagy egy automatikus fordító okozza.",
   "error.unexpected_crash.next_steps": "Próbáld frissíteni az oldalt. Ha ez nem segít, egy másik böngészőn vagy appon keresztül még mindig használhatod a Mastodont.",
   "error.unexpected_crash.next_steps_addons": "Próbáld letiltani őket és frissíteni az oldalt. Ha ez nem segít, egy másik böngészőn vagy appon keresztül még mindig használhatod a Mastodont.",
@@ -278,13 +278,13 @@
   "firehose.remote": "Egyéb kiszolgálók",
   "follow_request.authorize": "Hitelesítés",
   "follow_request.reject": "Elutasítás",
-  "follow_requests.unlocked_explanation": "Bár a fiókod nincs zárolva, a(z) {domain} csapata úgy gondolta, hogy talán kézzel szeretnéd ellenőrizni a fiók követési kéréseit.",
+  "follow_requests.unlocked_explanation": "Bár a fiókod nincs zárolva, a(z) {domain} csapata úgy gondolta, hogy talán kézzel szeretnéd ellenőrizni ezen fiókok követési kéréseit.",
   "followed_tags": "Követett hashtagek",
   "footer.about": "Névjegy",
   "footer.directory": "Profiltár",
   "footer.get_app": "Alkalmazás beszerzése",
   "footer.invite": "Emberek meghívása",
-  "footer.keyboard_shortcuts": "Billentyűparancsok",
+  "footer.keyboard_shortcuts": "Gyorsbillentyűk",
   "footer.privacy_policy": "Adatvédelmi szabályzat",
   "footer.source_code": "Forráskód megtekintése",
   "footer.status": "Állapot",
@@ -347,13 +347,13 @@
   "keyboard_shortcuts.favourite": "Bejegyzés kedvencnek jelölése",
   "keyboard_shortcuts.favourites": "Kedvencek lista megnyitása",
   "keyboard_shortcuts.federated": "Föderációs idővonal megnyitása",
-  "keyboard_shortcuts.heading": "Billentyűparancsok",
+  "keyboard_shortcuts.heading": "Gyorsbillentyűk",
   "keyboard_shortcuts.home": "Saját idővonal megnyitása",
   "keyboard_shortcuts.hotkey": "Gyorsbillentyű",
   "keyboard_shortcuts.legend": "jelmagyarázat megjelenítése",
-  "keyboard_shortcuts.local": "helyi idővonal megnyitása",
+  "keyboard_shortcuts.local": "Helyi idővonal megnyitása",
   "keyboard_shortcuts.mention": "Szerző megemlítése",
-  "keyboard_shortcuts.muted": "némított felhasználók listájának megnyitása",
+  "keyboard_shortcuts.muted": "Némított felhasználók listájának megnyitása",
   "keyboard_shortcuts.my_profile": "Saját profil megnyitása",
   "keyboard_shortcuts.notifications": "Értesítések oszlop megnyitása",
   "keyboard_shortcuts.open_media": "Média megnyitása",
@@ -389,7 +389,7 @@
   "lists.replies_policy.list": "A lista tagjai",
   "lists.replies_policy.none": "Senki",
   "lists.replies_policy.title": "Nekik mutassuk a válaszokat:",
-  "lists.search": "Keresés a követett személyek között",
+  "lists.search": "Keresés a követett emberek között",
   "lists.subheading": "Saját listák",
   "load_pending": "{count, plural, one {# új elem} other {# új elem}}",
   "loading_indicator.label": "Betöltés…",
@@ -399,7 +399,7 @@
   "mute_modal.hide_notifications": "Rejtsük el a felhasználótól származó értesítéseket?",
   "mute_modal.indefinite": "Határozatlan",
   "navigation_bar.about": "Névjegy",
-  "navigation_bar.advanced_interface": "Haladó webes felület engedélyezése",
+  "navigation_bar.advanced_interface": "Megnyitás a speciális webes felületben",
   "navigation_bar.blocks": "Letiltott felhasználók",
   "navigation_bar.bookmarks": "Könyvjelzők",
   "navigation_bar.community_timeline": "Helyi idővonal",
@@ -411,9 +411,9 @@
   "navigation_bar.explore": "Felfedezés",
   "navigation_bar.favourites": "Kedvencek",
   "navigation_bar.filters": "Némított szavak",
-  "navigation_bar.follow_requests": "Követési kérelmek",
+  "navigation_bar.follow_requests": "Követési kérések",
   "navigation_bar.followed_tags": "Követett hashtagek",
-  "navigation_bar.follows_and_followers": "Követettek és követők",
+  "navigation_bar.follows_and_followers": "Követések és követők",
   "navigation_bar.lists": "Listák",
   "navigation_bar.logout": "Kijelentkezés",
   "navigation_bar.mutes": "Némított felhasználók",
@@ -449,7 +449,7 @@
   "notifications.column_settings.follow_request": "Új követési kérelmek:",
   "notifications.column_settings.mention": "Megemlítések:",
   "notifications.column_settings.poll": "Szavazási eredmények:",
-  "notifications.column_settings.push": "Push értesítések",
+  "notifications.column_settings.push": "Leküldéses értesítések",
   "notifications.column_settings.reblog": "Megtolások:",
   "notifications.column_settings.show": "Megjelenítés az oszlopban",
   "notifications.column_settings.sound": "Hang lejátszása",
@@ -480,7 +480,7 @@
   "onboarding.compose.template": "Üdvözlet, #Mastodon!",
   "onboarding.follows.empty": "Sajnos jelenleg nem jeleníthető meg eredmény. Kipróbálhatod a keresést vagy böngészheted a felfedező oldalon a követni kívánt személyeket, vagy próbáld meg később.",
   "onboarding.follows.lead": "A saját hírfolyamod az elsődleges tapasztalás a Mastodonon. Minél több embert követsz, annál aktívabb és érdekesebb a dolog. Az induláshoz itt van néhány javaslat:",
-  "onboarding.follows.title": "Népszerű a Mastodonon",
+  "onboarding.follows.title": "Szabd személyre a kezdőlapodat",
   "onboarding.profile.discoverable": "Saját profil beállítása felfedezhetőként",
   "onboarding.profile.discoverable_hint": "A Mastodonon a felfedezhetőség választása esetén a saját bejegyzéseid megjelenhetnek a keresési eredmények és a felkapott tartalmak között, valamint a profilod a hozzád hasonló érdeklődési körrel rendelkező embereknél is ajánlásra kerülhet.",
   "onboarding.profile.display_name": "Megjelenített név",
@@ -499,14 +499,14 @@
   "onboarding.start.lead": "Az új Mastodon-fiók használatra kész. Így hozhatod ki belőle a legtöbbet:",
   "onboarding.start.skip": "Szeretnél előreugrani?",
   "onboarding.start.title": "Ez sikerült!",
-  "onboarding.steps.follow_people.body": "Te állítod össze a saját hírfolyamodat. Töltsd meg érdekes emberekkel.",
+  "onboarding.steps.follow_people.body": "A Mastodon az érdekes emberek követéséről szól.",
   "onboarding.steps.follow_people.title": "{count, plural, one {egy ember} other {# ember}} követése",
   "onboarding.steps.publish_status.body": "Üdvözöljük a világot.",
   "onboarding.steps.publish_status.title": "Az első bejegyzés létrehozása",
   "onboarding.steps.setup_profile.body": "Mások nagyobb valószínűséggel lépnek kapcsolatba veled egy kitöltött profil esetén.",
   "onboarding.steps.setup_profile.title": "Profilod testreszabása",
-  "onboarding.steps.share_profile.body": "Tudasd az ismerőseiddel, hogyan találhatnak meg a Mastodonon!",
-  "onboarding.steps.share_profile.title": "Profilod megosztása",
+  "onboarding.steps.share_profile.body": "Tudasd az ismerőseiddel, hogyan találhatnak meg a Mastodonon",
+  "onboarding.steps.share_profile.title": "Oszd meg a Mastodon profilodat",
   "onboarding.tips.2fa": "<strong>Tudtad?</strong> A fiókod biztonságossá teheted, ha a fiók beállításaiban beállítod a kétlépcsős hitelesítést. Bármilyen választott TOTP alkalmazással működik, nincs szükség telefonszámra!",
   "onboarding.tips.accounts_from_other_servers": "<strong>Tudtad?</strong> Mivel a Mastodon decentralizált, egyes profilok, amelyekkel találkozol, más kiszolgálókon lesznek tárolva. És mégis zökkenőmentesen kommunikálhatsz velük! A kiszolgáló a felhasználónevük második felében található!",
   "onboarding.tips.migration": "<strong>Tudtad?</strong> Ha úgy érzed, hogy a {domain} már nem jó kiszolgáló a számodra, átköltözhetsz egy másik Mastodon kiszolgálóra anélkül, hogy elveszítenéd a követőidet. Akár saját kiszolgálót is üzemeltethetsz!",
diff --git a/config/locales/activerecord.hu.yml b/config/locales/activerecord.hu.yml
index e5757ba64..f34ade044 100644
--- a/config/locales/activerecord.hu.yml
+++ b/config/locales/activerecord.hu.yml
@@ -20,7 +20,7 @@ hu:
           attributes:
             username:
               invalid: csak betűket, számokat vagy alávonást tartalmazhat
-              reserved: fenntartott
+              reserved: foglalt
         admin/webhook:
           attributes:
             url:
diff --git a/config/locales/devise.hu.yml b/config/locales/devise.hu.yml
index bbf6a6399..6eb1524ed 100644
--- a/config/locales/devise.hu.yml
+++ b/config/locales/devise.hu.yml
@@ -44,7 +44,7 @@ hu:
         action: Jelszó módosítása
         explanation: A fiókodhoz tartozó jelszó módosítását kezdeményezted.
         extra: Amennyiben nem te kezdeményezted a módosítást, kérjük tekintsd ezt az emailt tárgytalannak. A jelszavad változatlan marad mindaddig, amíg újat nem hozol létre a fenti linkre kattintva.
-        subject: 'Mastodon: Jelszó visszaállítási lépések'
+        subject: 'Mastodon: Jelszóvisszaállítási utasítások'
         title: Jelszó visszaállítása
       two_factor_disabled:
         explanation: A fiókod kétlépcsős hitelesítését kikapcsoltuk. A bejelentkezés mostantól csak az e-mail cím és a jelszó használatával lesz lehetséges.
@@ -52,14 +52,14 @@ hu:
         title: Kétlépcsős hitelesítés kikapcsolva
       two_factor_enabled:
         explanation: A kétlépcsős hitelesítést engedélyeztük a fiókodban. A bejelentkezéshez a párosított TOTP alkalmazás által generált tokenre lesz szükség.
-        subject: Kétlépcsős azonosítás engedélyezve
+        subject: 'Mastodon: Kétlépcsős azonosítás engedélyezve'
         title: Kétlépcsős hitelesítés engedélyezve
       two_factor_recovery_codes_changed:
         explanation: A korábbi helyreállítási kódok letiltásra és újragenerálásra kerültek.
-        subject: Kétlépcsős helyreállítási kódok újra létrejöttek
+        subject: 'Mastodon: Kétlépcsős helyreállítási kódok újból előállítva'
         title: A kétlépcsős kódok megváltoztak
       unlock_instructions:
-        subject: 'Mastodon: Feloldási lépések'
+        subject: 'Mastodon: Feloldási utasítások'
       webauthn_credential:
         added:
           explanation: A következő biztonsági kulcsot hozzáadtuk a fiókodhoz
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
index 7057883e1..5da1a4e06 100644
--- a/config/locales/hu.yml
+++ b/config/locales/hu.yml
@@ -4,7 +4,7 @@ hu:
     about_mastodon_html: 'A jövő közösségi hálózata: Hirdetések és céges megfigyelés nélkül, etikus dizájnnal és decentralizációval! Legyél a saját adataid ura a Mastodonnal!'
     contact_missing: Nincs megadva
     contact_unavailable: N/A
-    hosted_on: "%{domain} Mastodon szerver"
+    hosted_on: "%{domain} Mastodon-kiszolgáló"
     title: Névjegy
   accounts:
     follow: Követés
@@ -747,7 +747,7 @@ hu:
         desc_html: Ez hCaptcha-ból származó külső scripteket használ, mely biztonsági vagy adatvédelmi résnek bizonyulhat. Ezen kívül ez <strong>a regisztrációs folyamatot jelentősen megnehezítheti bizonyos (kifejezetten különleges szükségletű) emberek számára</strong>. Emiatt fontold meg más módszerek, mint pl. jóváhagyás-alapú vagy meghívásalapú regisztráció használatát.
         title: Az új felhasználóknak egy CAPTCHA-t kell megoldaniuk, hogy megerősítsék a fiókjuk regisztrációját
       content_retention:
-        preamble: Felhasználók által generált tartalom Mastodonon való tárolásának szabályozása.
+        preamble: A felhasználók által előállított tartalom Mastodonon való tárolásának szabályozása.
         title: Tartalom megtartása
       default_noindex:
         desc_html: Azokat a felhasználókat érinti, akik nem módosították ezt a beállítást
@@ -1007,8 +1007,8 @@ hu:
     hint_html: Ha másik fiókról kívánsz átlépni erre a fiókra, itt létrehozhatsz egy aliast, amelyre szükség van, mielőtt folytathatod a követők áthelyezését a régi fiókból erre. Ez az áthelyezés önmagában <strong>ártalmatlan és visszafordítható</strong> folyamat. <strong>A fiók áttelepítése a régi fiókból indul el. </strong>
     remove: Alias szétkapcsolása
   appearance:
-    advanced_web_interface: Haladó webes felület
-    advanced_web_interface_hint: 'Ha szeretnéd, a teljes képernyőszélességet felhasználhatod. A haladó webes felülettel különböző oszlopokat állíthatsz be, hogy egyszerre annyi infót láthass, amennyit csak akarsz: Saját idővonal, értesítések, föderációs idővonal, bármennyi lista vagy hashtag.'
+    advanced_web_interface: Speciális webes felület
+    advanced_web_interface_hint: 'Ha szeretnéd, a képernyő teljes szélességét kihasználhatod. A speciális webes felülettel különböző oszlopokat állíthatsz be, hogy egyszerre annyi információt láthass, amennyit csak akarsz: Kezdőoldal, értesítések, föderációs idővonal, bármennyi lista vagy hashtag.'
     animations_and_accessibility: Animáció és akadálymentesítés
     confirmation_dialogs: Megerősítő párbeszédablakok
     discovery: Felfedezés
@@ -1052,7 +1052,7 @@ hu:
     delete_account: Felhasználói fiók törlése
     delete_account_html: Felhasználói fiókod törléséhez <a href="%{path}">kattints ide</a>. A rendszer újbóli megerősítést fog kérni.
     description:
-      prefix_invited_by_user: "@%{name} meghív téged, hogy csatlakozz ehhez a Mastodon kiszolgálóhoz."
+      prefix_invited_by_user: "@%{name} meghív téged, hogy csatlakozz ehhez a Mastodon-kiszolgálóhoz."
       prefix_sign_up: Regisztrláj még ma a Mastodonra!
       suffix: Egy fiókkal követhetsz másokat, bejegyzéseket tehetsz közzé, eszmét cserélhetsz más Mastodon szerverek felhasználóival!
     didnt_get_confirmation: Nem kaptál visszaigazoló hivatkozást?
@@ -1101,7 +1101,7 @@ hu:
       title: 'Bejelentkezés ide: %{domain}'
     sign_up:
       manual_review: A(z) %{domain} regisztrációi a moderátorok kézi felülvizsgálatán mennek át. Hogy segítsd a regisztráció feldolgozását, írj röviden magadról, és hogy miért szeretnél fiókot a(z) %{domain} oldalon.
-      preamble: Egy fiókkal ezen a Mastodon kiszolgálón követhetsz bárkit a hálózaton, függetlenül attól, hogy az illető fiókja melyik kiszolgálón található.
+      preamble: Egy fiókkal ezen a Mastodon-kiszolgálón követhetsz bárkit a hálózaton, függetlenül attól, hogy az illető fiókja melyik kiszolgálón található.
       title: Állítsuk be a fiókod a %{domain} kiszolgálón.
     status:
       account_status: Fiók állapota
@@ -1234,7 +1234,7 @@ hu:
   filters:
     contexts:
       account: Profil
-      home: Saját idővonal
+      home: Kezdőlap és listák
       notifications: Értesítések
       public: Nyilvános idővonalak
       thread: Beszélgetések
@@ -1245,7 +1245,7 @@ hu:
       statuses_hint_html: Ez a szűrő egyedi bejegyzések kiválasztására vonatkozik, függetlenül attól, hogy megfelelnek-e a lenti kulcsszavaknak. <a href="%{path}">Engedélyezze vagy távolítsa el a bejegyzéseket a szűrőből</a>.
       title: Szűrő szerkesztése
     errors:
-      deprecated_api_multiple_keywords: Ezek a paraméterek nem módosíthatóak az alkalmazásból, mert több mint egy szűrőkulcsszóra is hatással vannak. Használd az alkalmazás vagy a webes felület újabb verzióját.
+      deprecated_api_multiple_keywords: Ezek a paraméterek nem módosíthatók az alkalmazásból, mert egynél több szűrőkulcsszóra is hatással vannak. Használd az alkalmazás vagy a webes felület újabb verzióját.
       invalid_context: A megadott kontextus hamis vagy hiányzik
     index:
       contexts: 'Szűrés helye: %{contexts}'
@@ -1396,7 +1396,7 @@ hu:
     unsubscribe:
       action: Igen, leiratkozás
       complete: Leiratkozva
-      confirmation_html: Biztos vagy benne, hogy le szeretnél iratkozni arról, hogy %{type} típusú üzeneteket kapj a %{domain} Mastodon kiszolgálón a %{email} címedre? Bármikor újra feliratkozhatsz az <a href="%{settings_path}">email értesítések beállításainál</a>.
+      confirmation_html: 'Biztos, hogy leiratkozol arról, hogy %{type} típusú üzeneteket kapj a %{domain} Mastodon-kiszolgálótól erre a címedre: %{email}? Bármikor újra feliratkozhatsz az <a href="%{settings_path}">e-mail-értesítések beállításánál</a>.'
       emails:
         notification_emails:
           favourite: kedvencnek jelölésről email értesítő
@@ -1405,7 +1405,7 @@ hu:
           mention: megemlítésről email értesítő
           reblog: megtolásról email értesítő
       resubscribe_html: Ha tévedésből iratkoztál le, újra feliratkozhatsz az <a href="%{settings_path}">email értesítések beállításainál</a>.
-      success_html: Mostantól nem kapsz %{type} típusú üzeneket a %{domain} Mastodon kiszolgálón a %{email} címedre.
+      success_html: 'Mostantól nem kapsz %{type} típusú üzeneket a(z) %{domain} Mastodon-kiszolgálón erre a címedre: %{email}.'
       title: Leiratkozás
   media_attachments:
     validations:
@@ -1743,9 +1743,9 @@ hu:
   tags:
     does_not_match_previous_name: nem illeszkedik az előző névvel
   themes:
-    contrast: Mastodon (Nagy kontrasztú)
-    default: Mastodon (Sötét)
-    mastodon-light: Mastodon (Világos)
+    contrast: Mastodon (nagy kontrasztú)
+    default: Mastodon (sötét)
+    mastodon-light: Mastodon (világos)
   time:
     formats:
       default: "%Y. %b %d., %H:%M"
@@ -1782,7 +1782,7 @@ hu:
       subject: A %{date}-i fellebbezésedet visszautasították
       title: Fellebbezés visszautasítva
     backup_ready:
-      explanation: A Mastodon fiókod teljes mentését kérted. A mentés kész ás letölthető!
+      explanation: A Mastodon-fiókod teljes mentését kérted. A mentés elkészült, és letölthető.
       subject: Az adataidról készült archív letöltésre kész
       title: Archiválás
     suspicious_sign_in:
diff --git a/config/locales/simple_form.hu.yml b/config/locales/simple_form.hu.yml
index 70c225e8f..6e11f7fb9 100644
--- a/config/locales/simple_form.hu.yml
+++ b/config/locales/simple_form.hu.yml
@@ -81,8 +81,8 @@ hu:
         bootstrap_timeline_accounts: Ezek a fiókok ki lesznek tűzve az új felhasználók követési javaslatainak élére.
         closed_registrations_message: Akkor jelenik meg, amikor a regisztráció le van zárva
         content_cache_retention_period: A más kiszolgálókról származó bejegyzések megadott számú nap után törölve lesznek, ha pozitív értékre van állítva. Ez lehet, hogy nem fordítható vissza.
-        custom_css: A Mastodon webes verziójában használhatsz egyedi stílusokat.
-        mascot: Felülvágja a haladó webes felületen található illusztrációt.
+        custom_css: A Mastodon webes verziójában használhatsz egyéni stílusokat.
+        mascot: Felülbírálja a speciális webes felületen található illusztrációt.
         media_cache_retention_period: A letöltött médiafájlok megadott számú nap után törölve lesznek, ha pozitív értékre van állítva, és igény szerint újból le lesznek töltve.
         peers_api_enabled: Azon domainek listája, melyekkel ez a kiszolgáló találkozott a fediverzumban. Nem csatolunk adatot arról, hogy föderált kapcsolatban vagy-e az adott kiszolgálóval, csak arról, hogy a kiszolgálód tud a másikról. Ezt olyan szolgáltatások használják, melyek általában a föderációról készítenek statisztikákat.
         profile_directory: A profilok jegyzéke minden olyan felhasználót felsorol, akik engedélyezték a felfedezhetőségüket.
@@ -103,7 +103,7 @@ hu:
       form_challenge:
         current_password: Beléptél egy biztonsági térben
       imports:
-        data: Egy másik Mastodon kiszolgálóról exportált CSV-fájl
+        data: Egy másik Mastodon-kiszolgálóról exportált CSV-fájl
       invite_request:
         text: Ez segít nekünk átnézni a jelentkezésedet
       ip_block:
@@ -199,7 +199,7 @@ hu:
         otp_attempt: Kétlépcsős azonosító kód
         password: Jelszó
         phrase: Kulcsszó vagy kifejezés
-        setting_advanced_layout: Haladó webes felület engedélyezése
+        setting_advanced_layout: Speciális webes felület engedélyezése
         setting_aggregate_reblogs: Megtolások csoportosítása az idővonalakon
         setting_always_send_emails: E-mail értesítések küldése mindig
         setting_auto_play_gif: GIF-ek automatikus lejátszása
diff --git a/config/locales/simple_form.hy.yml b/config/locales/simple_form.hy.yml
index 56aa1d66b..9dbcd1301 100644
--- a/config/locales/simple_form.hy.yml
+++ b/config/locales/simple_form.hy.yml
@@ -43,7 +43,7 @@ hy:
         setting_display_media_hide_all: Երբեք մեդիա ցոյց չտալ
         setting_display_media_show_all: Մեդիա միշտ ցոյց տալ
         setting_use_blurhash: Կտորները հիմնուում են թաքցուած վիզուալի վրայ՝ խամրեցնելով դետալները
-        setting_use_pending_items: Թաքցնել հոսքի թարմացումները կտտոի ետեւում՝ աւտօմատ թարմացուող հոսքի փոխարէն
+        setting_use_pending_items: Թաքցնել հոսքի թարմացումները կոճակի ետեւում՝ աւտօմատ թարմացուող հոսքի փոխարէն
         username: Միայն լատինատառեր, թուեր եւ տակի գծիկ
         whole_word: Եթէ բանալի բառը կամ արտայայտութիւնը պարունակում է միայն այբբենական նիշեր եւ թուեր, ապա այն կիրառուելու է ամբողջ բառի հետ համընկնելու դէպքում միայն
       domain_allow:

From 1d7b8234c94e13ad3f00455e7a5f0702e9379228 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 7 Dec 2023 04:41:00 -0500
Subject: [PATCH 37/73] Remove useless `reorder(nil)` call in `tootctl
 statuses` (#28141)

---
 lib/mastodon/cli/statuses.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/mastodon/cli/statuses.rb b/lib/mastodon/cli/statuses.rb
index 0d6018a2b..7acf3f9b7 100644
--- a/lib/mastodon/cli/statuses.rb
+++ b/lib/mastodon/cli/statuses.rb
@@ -120,7 +120,7 @@ module Mastodon::CLI
 
       say('Beginning removal of now-orphaned media attachments to free up disk space...')
 
-      scope     = MediaAttachment.reorder(nil).unattached.where('created_at < ?', options[:days].pred.days.ago)
+      scope     = MediaAttachment.unattached.where('created_at < ?', options[:days].pred.days.ago)
       processed = 0
       removed   = 0
       progress  = create_progress_bar(scope.count)

From 0b4a3a04378ce43f2f314b9446b5053f6b374c6d Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 7 Dec 2023 07:15:50 -0500
Subject: [PATCH 38/73] Remove remaining `without_partial_double_verification`
 usage (#28127)

---
 spec/helpers/application_helper_spec.rb     | 24 +++++++++-----
 spec/helpers/home_helper_spec.rb            | 17 +++++++---
 spec/helpers/media_component_helper_spec.rb | 36 +++++----------------
 3 files changed, 36 insertions(+), 41 deletions(-)

diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 3cc88014c..0a55770ba 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -29,15 +29,25 @@ describe ApplicationHelper do
 
   describe 'body_classes' do
     context 'with a body class string from a controller' do
-      before do
-        without_partial_double_verification do
-          allow(helper).to receive_messages(body_class_string: 'modal-layout compose-standalone', current_theme: 'default', current_account: Fabricate(:account))
-        end
-      end
+      before { helper.extend controller_helpers }
 
       it 'uses the controller body classes in the result' do
         expect(helper.body_classes).to match(/modal-layout compose-standalone/)
       end
+
+      private
+
+      def controller_helpers
+        Module.new do
+          def body_class_string = 'modal-layout compose-standalone'
+
+          def current_account
+            @current_account ||= Fabricate(:account)
+          end
+
+          def current_theme = 'default'
+        end
+      end
     end
   end
 
@@ -122,9 +132,7 @@ describe ApplicationHelper do
   describe 'available_sign_up_path' do
     context 'when registrations are closed' do
       before do
-        without_partial_double_verification do
-          allow(Setting).to receive(:registrations_mode).and_return('none')
-        end
+        allow(Setting).to receive(:[]).with('registrations_mode').and_return 'none'
       end
 
       it 'redirects to joinmastodon site' do
diff --git a/spec/helpers/home_helper_spec.rb b/spec/helpers/home_helper_spec.rb
index c6baec5a1..befc8a5c8 100644
--- a/spec/helpers/home_helper_spec.rb
+++ b/spec/helpers/home_helper_spec.rb
@@ -23,12 +23,19 @@ RSpec.describe HomeHelper do
     context 'with a valid account' do
       let(:account) { Fabricate(:account) }
 
-      it 'returns a link to the account' do
-        without_partial_double_verification do
-          allow(helper).to receive_messages(current_account: account, prefers_autoplay?: false)
-          result = helper.account_link_to(account)
+      before { helper.extend controller_helpers }
 
-          expect(result).to match "@#{account.acct}"
+      it 'returns a link to the account' do
+        result = helper.account_link_to(account)
+
+        expect(result).to match "@#{account.acct}"
+      end
+
+      private
+
+      def controller_helpers
+        Module.new do
+          def current_account = Account.last
         end
       end
     end
diff --git a/spec/helpers/media_component_helper_spec.rb b/spec/helpers/media_component_helper_spec.rb
index 149f6a83a..af5d92769 100644
--- a/spec/helpers/media_component_helper_spec.rb
+++ b/spec/helpers/media_component_helper_spec.rb
@@ -3,16 +3,12 @@
 require 'rails_helper'
 
 describe MediaComponentHelper do
+  before { helper.extend controller_helpers }
+
   describe 'render_video_component' do
     let(:media) { Fabricate(:media_attachment, type: :video, status: Fabricate(:status)) }
     let(:result) { helper.render_video_component(media.status) }
 
-    before do
-      without_partial_double_verification do
-        allow(helper).to receive(:current_account).and_return(media.account)
-      end
-    end
-
     it 'renders a react component for the video' do
       expect(parsed_html.div['data-component']).to eq('Video')
     end
@@ -22,12 +18,6 @@ describe MediaComponentHelper do
     let(:media) { Fabricate(:media_attachment, type: :audio, status: Fabricate(:status)) }
     let(:result) { helper.render_audio_component(media.status) }
 
-    before do
-      without_partial_double_verification do
-        allow(helper).to receive(:current_account).and_return(media.account)
-      end
-    end
-
     it 'renders a react component for the audio' do
       expect(parsed_html.div['data-component']).to eq('Audio')
     end
@@ -37,12 +27,6 @@ describe MediaComponentHelper do
     let(:media) { Fabricate(:media_attachment, type: :audio, status: Fabricate(:status)) }
     let(:result) { helper.render_media_gallery_component(media.status) }
 
-    before do
-      without_partial_double_verification do
-        allow(helper).to receive(:current_account).and_return(media.account)
-      end
-    end
-
     it 'renders a react component for the media gallery' do
       expect(parsed_html.div['data-component']).to eq('MediaGallery')
     end
@@ -54,10 +38,6 @@ describe MediaComponentHelper do
 
     before do
       PreviewCardsStatus.create(status: status, preview_card: Fabricate(:preview_card))
-
-      without_partial_double_verification do
-        allow(helper).to receive(:current_account).and_return(status.account)
-      end
     end
 
     it 'returns the correct react component markup' do
@@ -69,12 +49,6 @@ describe MediaComponentHelper do
     let(:status) { Fabricate(:status, poll: Fabricate(:poll)) }
     let(:result) { helper.render_poll_component(status) }
 
-    before do
-      without_partial_double_verification do
-        allow(helper).to receive(:current_account).and_return(status.account)
-      end
-    end
-
     it 'returns the correct react component markup' do
       expect(parsed_html.div['data-component']).to eq('Poll')
     end
@@ -85,4 +59,10 @@ describe MediaComponentHelper do
   def parsed_html
     Nokogiri::Slop(result)
   end
+
+  def controller_helpers
+    Module.new do
+      def current_account = Account.last
+    end
+  end
 end

From 5d97a897c8984d5f4fc0e2dca0799b5ab59a7c65 Mon Sep 17 00:00:00 2001
From: Ken Greeff <ken@kengreeff.com>
Date: Thu, 7 Dec 2023 23:45:55 +1100
Subject: [PATCH 39/73] Add note to install libidn on MacOS (#28259)

---
 README.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/README.md b/README.md
index beab2c355..7b22d61b5 100644
--- a/README.md
+++ b/README.md
@@ -94,6 +94,7 @@ To set up **MacOS** for native development, complete the following steps:
 - Run `brew install postgresql@14`
 - Run `brew install redis`
 - Run `brew install imagemagick`
+- Run `brew install libidn`
 - Install Foreman or a similar tool (such as [overmind](https://github.com/DarthSim/overmind)) to handle multiple process launching.
 - Navigate to Mastodon's root directory and run `brew install nvm` then `nvm use` to use the version from .nvmrc
 - Run `corepack enable && corepack prepare`

From ad34d33bfd10d3ff8078032e9f390c30892e80c1 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 7 Dec 2023 08:49:14 -0500
Subject: [PATCH 40/73] Formalize some patterns in cli specs (#28255)

---
 spec/lib/mastodon/cli/accounts_spec.rb        | 431 +++++++++---------
 spec/lib/mastodon/cli/cache_spec.rb           |  29 +-
 .../cli/canonical_email_blocks_spec.rb        |  26 +-
 spec/lib/mastodon/cli/domains_spec.rb         |  14 +-
 .../mastodon/cli/email_domain_blocks_spec.rb  |  63 +--
 spec/lib/mastodon/cli/emoji_spec.rb           |  12 +-
 spec/lib/mastodon/cli/feeds_spec.rb           |  29 +-
 spec/lib/mastodon/cli/ip_blocks_spec.rb       | 113 ++---
 spec/lib/mastodon/cli/main_spec.rb            |  15 +-
 spec/lib/mastodon/cli/maintenance_spec.rb     |  24 +-
 spec/lib/mastodon/cli/media_spec.rb           |  73 ++-
 spec/lib/mastodon/cli/preview_cards_spec.rb   |  33 +-
 spec/lib/mastodon/cli/settings_spec.rb        |  39 +-
 spec/lib/mastodon/cli/statuses_spec.rb        |  17 +-
 spec/lib/mastodon/cli/upgrade_spec.rb         |  13 +-
 spec/rails_helper.rb                          |   1 +
 spec/support/command_line_helpers.rb          |   9 +
 17 files changed, 492 insertions(+), 449 deletions(-)
 create mode 100644 spec/support/command_line_helpers.rb

diff --git a/spec/lib/mastodon/cli/accounts_spec.rb b/spec/lib/mastodon/cli/accounts_spec.rb
index 3216d0d1b..06860c2ff 100644
--- a/spec/lib/mastodon/cli/accounts_spec.rb
+++ b/spec/lib/mastodon/cli/accounts_spec.rb
@@ -4,7 +4,11 @@ require 'rails_helper'
 require 'mastodon/cli/accounts'
 
 describe Mastodon::CLI::Accounts do
+  subject { cli.invoke(action, arguments, options) }
+
   let(:cli) { described_class.new }
+  let(:arguments) { [] }
+  let(:options) { {} }
 
   it_behaves_like 'CLI Command'
 
@@ -27,15 +31,17 @@ describe Mastodon::CLI::Accounts do
   end
 
   describe '#create' do
+    let(:action) { :create }
+
     shared_examples 'a new user with given email address and username' do
       it 'creates a new user with the specified email address' do
-        cli.invoke(:create, arguments, options)
+        subject
 
         expect(User.find_by(email: options[:email])).to be_present
       end
 
       it 'creates a new local account with the specified username' do
-        cli.invoke(:create, arguments, options)
+        subject
 
         expect(Account.find_local('tootctl_username')).to be_present
       end
@@ -43,9 +49,8 @@ describe Mastodon::CLI::Accounts do
       it 'returns "OK" and newly generated password' do
         allow(SecureRandom).to receive(:hex).and_return('test_password')
 
-        expect { cli.invoke(:create, arguments, options) }.to output(
-          a_string_including("OK\nNew password: test_password")
-        ).to_stdout
+        expect { subject }
+          .to output_results("OK\nNew password: test_password")
       end
     end
 
@@ -61,9 +66,8 @@ describe Mastodon::CLI::Accounts do
           let(:options) { { email: 'invalid' } }
 
           it 'exits with an error message' do
-            expect { cli.invoke(:create, arguments, options) }.to output(
-              a_string_including('Failure/Error: email')
-            ).to_stdout
+            expect { subject }
+              .to output_results('Failure/Error: email')
               .and raise_error(SystemExit)
           end
         end
@@ -75,7 +79,7 @@ describe Mastodon::CLI::Accounts do
         it_behaves_like 'a new user with given email address and username'
 
         it 'creates a new user with confirmed status' do
-          cli.invoke(:create, arguments, options)
+          subject
 
           user = User.find_by(email: options[:email])
 
@@ -93,7 +97,7 @@ describe Mastodon::CLI::Accounts do
         it_behaves_like 'a new user with given email address and username'
 
         it 'creates a new user with approved status' do
-          cli.invoke(:create, arguments, options)
+          subject
 
           user = User.find_by(email: options[:email])
 
@@ -109,7 +113,7 @@ describe Mastodon::CLI::Accounts do
           it_behaves_like 'a new user with given email address and username'
 
           it 'creates a new user and assigns the specified role' do
-            cli.invoke(:create, arguments, options)
+            subject
 
             role = User.find_by(email: options[:email])&.role
 
@@ -121,9 +125,8 @@ describe Mastodon::CLI::Accounts do
           let(:options) { { email: 'tootctl@example.com', role: '404' } }
 
           it 'exits with an error message indicating the role name was not found' do
-            expect { cli.invoke(:create, arguments, options) }.to output(
-              a_string_including('Cannot find user role with that name')
-            ).to_stdout
+            expect { subject }
+              .to output_results('Cannot find user role with that name')
               .and raise_error(SystemExit)
           end
         end
@@ -139,16 +142,15 @@ describe Mastodon::CLI::Accounts do
           end
 
           it 'returns an error message indicating the username is already taken' do
-            expect { cli.invoke(:create, arguments, options) }.to output(
-              a_string_including("The chosen username is currently in use\nUse --force to reattach it anyway and delete the other user")
-            ).to_stdout
+            expect { subject }
+              .to output_results("The chosen username is currently in use\nUse --force to reattach it anyway and delete the other user")
           end
 
           context 'with --force option' do
             let(:options) { { email: 'tootctl_new@example.com', reattach: true, force: true } }
 
             it 'reattaches the account to the new user and deletes the previous user' do
-              cli.invoke(:create, arguments, options)
+              subject
 
               user = Account.find_local('tootctl_username')&.user
 
@@ -173,20 +175,21 @@ describe Mastodon::CLI::Accounts do
       let(:arguments) { ['tootctl_username'] }
 
       it 'raises a required argument missing error (Thor::RequiredArgumentMissingError)' do
-        expect { cli.invoke(:create, arguments) }
+        expect { subject }
           .to raise_error(Thor::RequiredArgumentMissingError)
       end
     end
   end
 
   describe '#modify' do
+    let(:action) { :modify }
+
     context 'when the given username is not found' do
       let(:arguments) { ['non_existent_username'] }
 
       it 'exits with an error message indicating the user was not found' do
-        expect { cli.invoke(:modify, arguments) }.to output(
-          a_string_including('No user with such username')
-        ).to_stdout
+        expect { subject }
+          .to output_results('No user with such username')
           .and raise_error(SystemExit)
       end
     end
@@ -197,13 +200,12 @@ describe Mastodon::CLI::Accounts do
 
       context 'when no option is provided' do
         it 'returns a successful message' do
-          expect { cli.invoke(:modify, arguments) }.to output(
-            a_string_including('OK')
-          ).to_stdout
+          expect { subject }
+            .to output_results('OK')
         end
 
         it 'does not modify the user' do
-          cli.invoke(:modify, arguments)
+          subject
 
           expect(user).to eq(user.reload)
         end
@@ -214,9 +216,8 @@ describe Mastodon::CLI::Accounts do
           let(:options) { { role: '404' } }
 
           it 'exits with an error message indicating the role was not found' do
-            expect { cli.invoke(:modify, arguments, options) }.to output(
-              a_string_including('Cannot find user role with that name')
-            ).to_stdout
+            expect { subject }
+              .to output_results('Cannot find user role with that name')
               .and raise_error(SystemExit)
           end
         end
@@ -226,7 +227,7 @@ describe Mastodon::CLI::Accounts do
           let(:options) { { role: default_role.name } }
 
           it "updates the user's role to the specified role" do
-            cli.invoke(:modify, arguments, options)
+            subject
 
             role = user.reload.role
 
@@ -241,7 +242,7 @@ describe Mastodon::CLI::Accounts do
         let(:user) { Fabricate(:user, role: role) }
 
         it "removes the user's role successfully" do
-          cli.invoke(:modify, arguments, options)
+          subject
 
           role = user.reload.role
 
@@ -254,13 +255,13 @@ describe Mastodon::CLI::Accounts do
         let(:options) { { email: 'new_email@email.com' } }
 
         it "sets the user's unconfirmed email to the provided email address" do
-          cli.invoke(:modify, arguments, options)
+          subject
 
           expect(user.reload.unconfirmed_email).to eq(options[:email])
         end
 
         it "does not update the user's original email address" do
-          cli.invoke(:modify, arguments, options)
+          subject
 
           expect(user.reload.email).to eq('old_email@email.com')
         end
@@ -270,13 +271,13 @@ describe Mastodon::CLI::Accounts do
           let(:options) { { email: 'new_email@email.com', confirm: true } }
 
           it "updates the user's email address to the provided email" do
-            cli.invoke(:modify, arguments, options)
+            subject
 
             expect(user.reload.email).to eq(options[:email])
           end
 
           it "sets the user's email address as confirmed" do
-            cli.invoke(:modify, arguments, options)
+            subject
 
             expect(user.reload.confirmed?).to be(true)
           end
@@ -288,7 +289,7 @@ describe Mastodon::CLI::Accounts do
         let(:options) { { confirm: true } }
 
         it "confirms the user's email address" do
-          cli.invoke(:modify, arguments, options)
+          subject
 
           expect(user.reload.confirmed?).to be(true)
         end
@@ -303,7 +304,7 @@ describe Mastodon::CLI::Accounts do
         end
 
         it 'approves the user' do
-          expect { cli.invoke(:modify, arguments, options) }.to change { user.reload.approved }.from(false).to(true)
+          expect { subject }.to change { user.reload.approved }.from(false).to(true)
         end
       end
 
@@ -312,7 +313,7 @@ describe Mastodon::CLI::Accounts do
         let(:options) { { disable: true } }
 
         it 'disables the user' do
-          expect { cli.invoke(:modify, arguments, options) }.to change { user.reload.disabled }.from(false).to(true)
+          expect { subject }.to change { user.reload.disabled }.from(false).to(true)
         end
       end
 
@@ -321,7 +322,7 @@ describe Mastodon::CLI::Accounts do
         let(:options) { { enable: true } }
 
         it 'enables the user' do
-          expect { cli.invoke(:modify, arguments, options) }.to change { user.reload.disabled }.from(true).to(false)
+          expect { subject }.to change { user.reload.disabled }.from(true).to(false)
         end
       end
 
@@ -331,9 +332,8 @@ describe Mastodon::CLI::Accounts do
         it 'returns a new password for the user' do
           allow(SecureRandom).to receive(:hex).and_return('new_password')
 
-          expect { cli.invoke(:modify, arguments, options) }.to output(
-            a_string_including('new_password')
-          ).to_stdout
+          expect { subject }
+            .to output_results('new_password')
         end
       end
 
@@ -342,7 +342,7 @@ describe Mastodon::CLI::Accounts do
         let(:options) { { disable_2fa: true } }
 
         it 'disables the two-factor authentication for the user' do
-          expect { cli.invoke(:modify, arguments, options) }.to change { user.reload.otp_required_for_login }.from(true).to(false)
+          expect { subject }.to change { user.reload.otp_required_for_login }.from(true).to(false)
         end
       end
 
@@ -351,9 +351,8 @@ describe Mastodon::CLI::Accounts do
         let(:options) { { email: 'invalid' } }
 
         it 'exits with an error message' do
-          expect { cli.invoke(:modify, arguments, options) }.to output(
-            a_string_including('Failure/Error: email')
-          ).to_stdout
+          expect { subject }
+            .to output_results('Failure/Error: email')
             .and raise_error(SystemExit)
         end
       end
@@ -361,9 +360,8 @@ describe Mastodon::CLI::Accounts do
   end
 
   describe '#delete' do
+    let(:action) { :delete }
     let(:account) { Fabricate(:account) }
-    let(:arguments) { [account.username] }
-    let(:options) { { email: account.user.email } }
     let(:delete_account_service) { instance_double(DeleteAccountService) }
 
     before do
@@ -372,26 +370,29 @@ describe Mastodon::CLI::Accounts do
     end
 
     context 'when both username and --email are provided' do
+      let(:arguments) { [account.username] }
+      let(:options) { { email: account.user.email } }
+
       it 'exits with an error message indicating that only one should be used' do
-        expect { cli.invoke(:delete, arguments, options) }.to output(
-          a_string_including('Use username or --email, not both')
-        ).to_stdout
+        expect { subject }
+          .to output_results('Use username or --email, not both')
           .and raise_error(SystemExit)
       end
     end
 
     context 'when neither username nor --email are provided' do
       it 'exits with an error message indicating that no username was provided' do
-        expect { cli.invoke(:delete) }.to output(
-          a_string_including('No username provided')
-        ).to_stdout
+        expect { subject }
+          .to output_results('No username provided')
           .and raise_error(SystemExit)
       end
     end
 
     context 'when username is provided' do
+      let(:arguments) { [account.username] }
+
       it 'deletes the specified user successfully' do
-        cli.invoke(:delete, arguments)
+        subject
 
         expect(delete_account_service).to have_received(:call).with(account, reserve_email: false).once
       end
@@ -400,15 +401,14 @@ describe Mastodon::CLI::Accounts do
         let(:options) { { dry_run: true } }
 
         it 'does not delete the specified user' do
-          cli.invoke(:delete, arguments, options)
+          subject
 
           expect(delete_account_service).to_not have_received(:call).with(account, reserve_email: false)
         end
 
         it 'outputs a successful message in dry run mode' do
-          expect { cli.invoke(:delete, arguments, options) }.to output(
-            a_string_including('OK (DRY RUN)')
-          ).to_stdout
+          expect { subject }
+            .to output_results('OK (DRY RUN)')
         end
       end
 
@@ -416,17 +416,18 @@ describe Mastodon::CLI::Accounts do
         let(:arguments) { ['non_existent_username'] }
 
         it 'exits with an error message indicating that no user was found' do
-          expect { cli.invoke(:delete, arguments) }.to output(
-            a_string_including('No user with such username')
-          ).to_stdout
+          expect { subject }
+            .to output_results('No user with such username')
             .and raise_error(SystemExit)
         end
       end
     end
 
     context 'when --email is provided' do
+      let(:options) { { email: account.user.email } }
+
       it 'deletes the specified user successfully' do
-        cli.invoke(:delete, nil, options)
+        subject
 
         expect(delete_account_service).to have_received(:call).with(account, reserve_email: false).once
       end
@@ -435,15 +436,14 @@ describe Mastodon::CLI::Accounts do
         let(:options) { { email: account.user.email, dry_run: true } }
 
         it 'does not delete the user' do
-          cli.invoke(:delete, nil, options)
+          subject
 
           expect(delete_account_service).to_not have_received(:call).with(account, reserve_email: false)
         end
 
         it 'outputs a successful message in dry run mode' do
-          expect { cli.invoke(:delete, nil, options) }.to output(
-            a_string_including('OK (DRY RUN)')
-          ).to_stdout
+          expect { subject }
+            .to output_results('OK (DRY RUN)')
         end
       end
 
@@ -451,9 +451,8 @@ describe Mastodon::CLI::Accounts do
         let(:options) { { email: '404@example.com' } }
 
         it 'exits with an error message indicating that no user was found' do
-          expect { cli.invoke(:delete, nil, options) }.to output(
-            a_string_including('No user with such email')
-          ).to_stdout
+          expect { subject }
+            .to output_results('No user with such email')
             .and raise_error(SystemExit)
         end
       end
@@ -461,6 +460,7 @@ describe Mastodon::CLI::Accounts do
   end
 
   describe '#approve' do
+    let(:action) { :approve }
     let(:total_users) { 4 }
 
     before do
@@ -469,8 +469,10 @@ describe Mastodon::CLI::Accounts do
     end
 
     context 'with --all option' do
+      let(:options) { { all: true } }
+
       it 'approves all pending registrations' do
-        cli.invoke(:approve, nil, all: true)
+        subject
 
         expect(User.pluck(:approved).all?(true)).to be(true)
       end
@@ -481,7 +483,7 @@ describe Mastodon::CLI::Accounts do
         let(:options) { { number: 2 } }
 
         it 'approves the earliest n pending registrations' do
-          cli.invoke(:approve, nil, options)
+          subject
 
           n_earliest_pending_registrations = User.order(created_at: :asc).first(options[:number])
 
@@ -489,7 +491,7 @@ describe Mastodon::CLI::Accounts do
         end
 
         it 'does not approve the remaining pending registrations' do
-          cli.invoke(:approve, nil, options)
+          subject
 
           pending_registrations = User.order(created_at: :asc).last(total_users - options[:number])
 
@@ -498,10 +500,11 @@ describe Mastodon::CLI::Accounts do
       end
 
       context 'when the number is negative' do
+        let(:options) { { number: -1 } }
+
         it 'exits with an error message indicating that the number must be positive' do
-          expect { cli.invoke(:approve, nil, number: -1) }.to output(
-            a_string_including('Number must be positive')
-          ).to_stdout
+          expect { subject }
+            .to output_results('Number must be positive')
             .and raise_error(SystemExit)
         end
       end
@@ -510,13 +513,13 @@ describe Mastodon::CLI::Accounts do
         let(:options) { { number: total_users * 2 } }
 
         it 'approves all users' do
-          cli.invoke(:approve, nil, options)
+          subject
 
           expect(User.pluck(:approved).all?(true)).to be(true)
         end
 
         it 'does not raise any error' do
-          expect { cli.invoke(:approve, nil, options) }
+          expect { subject }
             .to_not raise_error
         end
       end
@@ -528,7 +531,7 @@ describe Mastodon::CLI::Accounts do
         let(:arguments) { [user.account.username] }
 
         it 'approves the specified user successfully' do
-          cli.invoke(:approve, arguments)
+          subject
 
           expect(user.reload.approved?).to be(true)
         end
@@ -538,9 +541,8 @@ describe Mastodon::CLI::Accounts do
         let(:arguments) { ['non_existent_username'] }
 
         it 'exits with an error message indicating that no such account was found' do
-          expect { cli.invoke(:approve, arguments) }.to output(
-            a_string_including('No such account')
-          ).to_stdout
+          expect { subject }
+            .to output_results('No such account')
             .and raise_error(SystemExit)
         end
       end
@@ -548,13 +550,14 @@ describe Mastodon::CLI::Accounts do
   end
 
   describe '#follow' do
+    let(:action) { :follow }
+
     context 'when the given username is not found' do
       let(:arguments) { ['non_existent_username'] }
 
       it 'exits with an error message indicating that no account with the given username was found' do
-        expect { cli.invoke(:follow, arguments) }.to output(
-          a_string_including('No such account')
-        ).to_stdout
+        expect { subject }
+          .to output_results('No such account')
           .and raise_error(SystemExit)
       end
     end
@@ -565,6 +568,7 @@ describe Mastodon::CLI::Accounts do
       let!(:follower_rony)    { Fabricate(:account, username: 'rony') }
       let!(:follower_charles) { Fabricate(:account, username: 'charles') }
       let(:follow_service)    { instance_double(FollowService, call: nil) }
+      let(:arguments) { [target_account.username] }
 
       before do
         allow(FollowService).to receive(:new).and_return(follow_service)
@@ -572,7 +576,7 @@ describe Mastodon::CLI::Accounts do
       end
 
       it 'makes all local accounts follow the target account' do
-        cli.follow(target_account.username)
+        subject
 
         expect(follow_service).to have_received(:call).with(follower_bob, target_account, any_args).once
         expect(follow_service).to have_received(:call).with(follower_rony, target_account, any_args).once
@@ -580,21 +584,21 @@ describe Mastodon::CLI::Accounts do
       end
 
       it 'displays a successful message' do
-        expect { cli.follow(target_account.username) }.to output(
-          a_string_including("OK, followed target from #{Account.local.count} accounts")
-        ).to_stdout
+        expect { subject }
+          .to output_results("OK, followed target from #{Account.local.count} accounts")
       end
     end
   end
 
   describe '#unfollow' do
+    let(:action) { :unfollow }
+
     context 'when the given username is not found' do
       let(:arguments) { ['non_existent_username'] }
 
       it 'exits with an error message indicating that no account with the given username was found' do
-        expect { cli.invoke(:unfollow, arguments) }.to output(
-          a_string_including('No such account')
-        ).to_stdout
+        expect { subject }
+          .to output_results('No such account')
           .and raise_error(SystemExit)
       end
     end
@@ -605,6 +609,7 @@ describe Mastodon::CLI::Accounts do
       let!(:follower_rambo)  { Fabricate(:account, username: 'rambo', domain: nil) }
       let!(:follower_ana)    { Fabricate(:account, username: 'ana', domain: nil) }
       let(:unfollow_service) { instance_double(UnfollowService, call: nil) }
+      let(:arguments) { [target_account.username] }
 
       before do
         accounts = [follower_chris, follower_rambo, follower_ana]
@@ -614,7 +619,7 @@ describe Mastodon::CLI::Accounts do
       end
 
       it 'makes all local accounts unfollow the target account' do
-        cli.unfollow(target_account.username)
+        subject
 
         expect(unfollow_service).to have_received(:call).with(follower_chris, target_account).once
         expect(unfollow_service).to have_received(:call).with(follower_rambo, target_account).once
@@ -622,21 +627,21 @@ describe Mastodon::CLI::Accounts do
       end
 
       it 'displays a successful message' do
-        expect { cli.unfollow(target_account.username) }.to output(
-          a_string_including('OK, unfollowed target from 3 accounts')
-        ).to_stdout
+        expect { subject }
+          .to output_results('OK, unfollowed target from 3 accounts')
       end
     end
   end
 
   describe '#backup' do
+    let(:action) { :backup }
+
     context 'when the given username is not found' do
       let(:arguments) { ['non_existent_username'] }
 
       it 'exits with an error message indicating that there is no such account' do
-        expect { cli.invoke(:backup, arguments) }.to output(
-          a_string_including('No user with such username')
-        ).to_stdout
+        expect { subject }
+          .to output_results('No user with such username')
           .and raise_error(SystemExit)
       end
     end
@@ -647,22 +652,21 @@ describe Mastodon::CLI::Accounts do
       let(:arguments) { [account.username] }
 
       it 'creates a new backup for the specified user' do
-        expect { cli.invoke(:backup, arguments) }.to change { user.backups.count }.by(1)
+        expect { subject }.to change { user.backups.count }.by(1)
       end
 
       it 'creates a backup job' do
         allow(BackupWorker).to receive(:perform_async)
 
-        cli.invoke(:backup, arguments)
+        subject
         latest_backup = user.backups.last
 
         expect(BackupWorker).to have_received(:perform_async).with(latest_backup.id).once
       end
 
       it 'displays a successful message' do
-        expect { cli.invoke(:backup, arguments) }.to output(
-          a_string_including('OK')
-        ).to_stdout
+        expect { subject }
+          .to output_results('OK')
       end
     end
   end
@@ -724,9 +728,8 @@ describe Mastodon::CLI::Accounts do
       end
 
       it 'displays a successful message' do
-        expect { cli.refresh }.to output(
-          a_string_including('Refreshed 2 accounts')
-        ).to_stdout
+        expect { cli.refresh }
+          .to output_results('Refreshed 2 accounts')
       end
 
       context 'with --dry-run option' do
@@ -761,9 +764,8 @@ describe Mastodon::CLI::Accounts do
         end
 
         it 'displays a successful message with (DRY RUN)' do
-          expect { cli.refresh }.to output(
-            a_string_including('Refreshed 2 accounts (DRY RUN)')
-          ).to_stdout
+          expect { cli.refresh }
+            .to output_results('Refreshed 2 accounts (DRY RUN)')
         end
       end
     end
@@ -823,9 +825,7 @@ describe Mastodon::CLI::Accounts do
           allow(account_example_com_a).to receive(:reset_avatar!).and_raise(Mastodon::UnexpectedResponseError)
 
           expect { cli.refresh(*arguments) }
-            .to output(
-              a_string_including("Account failed: #{account_example_com_a.username}@#{account_example_com_a.domain}")
-            ).to_stdout
+            .to output_results("Account failed: #{account_example_com_a.username}@#{account_example_com_a.domain}")
         end
       end
 
@@ -833,9 +833,8 @@ describe Mastodon::CLI::Accounts do
         it 'exits with an error message' do
           allow(Account).to receive(:find_remote).with(account_example_com_b.username, account_example_com_b.domain).and_return(nil)
 
-          expect { cli.refresh(*arguments) }.to output(
-            a_string_including('No such account')
-          ).to_stdout
+          expect { cli.refresh(*arguments) }
+            .to output_results('No such account')
             .and raise_error(SystemExit)
         end
       end
@@ -878,7 +877,6 @@ describe Mastodon::CLI::Accounts do
         allow(cli).to receive(:parallelize_with_progress).and_yield(account_example_com_a)
                                                          .and_yield(account_example_com_b)
                                                          .and_return([2, nil])
-
         cli.options = { domain: domain }
       end
 
@@ -925,32 +923,33 @@ describe Mastodon::CLI::Accounts do
 
     context 'when neither a list of accts nor options are provided' do
       it 'exits with an error message' do
-        expect { cli.refresh }.to output(
-          a_string_including('No account(s) given')
-        ).to_stdout
+        expect { cli.refresh }
+          .to output_results('No account(s) given')
           .and raise_error(SystemExit)
       end
     end
   end
 
   describe '#rotate' do
+    let(:action) { :rotate }
+
     context 'when neither username nor --all option are given' do
       it 'exits with an error message' do
-        expect { cli.rotate }.to output(
-          a_string_including('No account(s) given')
-        ).to_stdout
+        expect { subject }
+          .to output_results('No account(s) given')
           .and raise_error(SystemExit)
       end
     end
 
     context 'when a username is given' do
       let(:account) { Fabricate(:account) }
+      let(:arguments) { [account.username] }
 
       it 'correctly rotates keys for the specified account' do
         old_private_key = account.private_key
         old_public_key = account.public_key
 
-        cli.rotate(account.username)
+        subject
         account.reload
 
         expect(account.private_key).to_not eq(old_private_key)
@@ -960,16 +959,17 @@ describe Mastodon::CLI::Accounts do
       it 'broadcasts the new keys for the specified account' do
         allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_in)
 
-        cli.rotate(account.username)
+        subject
 
         expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_in).with(anything, account.id, anything).once
       end
 
       context 'when the given username is not found' do
+        let(:arguments) { ['non_existent_username'] }
+
         it 'exits with an error message when the specified username is not found' do
-          expect { cli.rotate('non_existent_username') }.to output(
-            a_string_including('No such account')
-          ).to_stdout
+          expect { subject }
+            .to output_results('No such account')
             .and raise_error(SystemExit)
         end
       end
@@ -977,17 +977,13 @@ describe Mastodon::CLI::Accounts do
 
     context 'when --all option is provided' do
       let!(:accounts) { Fabricate.times(2, :account) }
-      let(:options)   { { all: true } }
-
-      before do
-        cli.options = { all: true }
-      end
+      let(:options) { { all: true } }
 
       it 'correctly rotates keys for all local accounts' do
         old_private_keys = accounts.map(&:private_key)
         old_public_keys = accounts.map(&:public_key)
 
-        cli.rotate
+        subject
         accounts.each(&:reload)
 
         expect(accounts.map(&:private_key)).to_not eq(old_private_keys)
@@ -997,7 +993,7 @@ describe Mastodon::CLI::Accounts do
       it 'broadcasts the new keys for each account' do
         allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_in)
 
-        cli.rotate
+        subject
 
         accounts.each do |account|
           expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_in).with(anything, account.id, anything).once
@@ -1007,11 +1003,12 @@ describe Mastodon::CLI::Accounts do
   end
 
   describe '#merge' do
+    let(:action) { :merge }
+
     shared_examples 'an account not found' do |acct|
       it 'exits with an error message indicating that there is no such account' do
-        expect { cli.invoke(:merge, arguments) }.to output(
-          a_string_including("No such account (#{acct})")
-        ).to_stdout
+        expect { subject }
+          .to output_results("No such account (#{acct})")
           .and raise_error(SystemExit)
       end
     end
@@ -1061,9 +1058,8 @@ describe Mastodon::CLI::Accounts do
       end
 
       it 'exits with an error message indicating that the accounts do not have the same pub key' do
-        expect { cli.invoke(:merge, arguments) }.to output(
-          a_string_including("Accounts don't have the same public key, might not be duplicates!\nOverride with --force")
-        ).to_stdout
+        expect { subject }
+          .to output_results("Accounts don't have the same public key, might not be duplicates!\nOverride with --force")
           .and raise_error(SystemExit)
       end
 
@@ -1076,13 +1072,13 @@ describe Mastodon::CLI::Accounts do
         end
 
         it 'merges "from_account" into "to_account"' do
-          cli.invoke(:merge, arguments, options)
+          subject
 
           expect(to_account).to have_received(:merge_with!).with(from_account).once
         end
 
         it 'deletes "from_account"' do
-          cli.invoke(:merge, arguments, options)
+          subject
 
           expect(from_account).to have_received(:destroy).once
         end
@@ -1104,13 +1100,13 @@ describe Mastodon::CLI::Accounts do
       end
 
       it 'merges "from_account" into "to_account"' do
-        cli.invoke(:merge, arguments)
+        subject
 
         expect(to_account).to have_received(:merge_with!).with(from_account).once
       end
 
       it 'deletes "from_account"' do
-        cli.invoke(:merge, arguments)
+        subject
 
         expect(from_account).to have_received(:destroy)
       end
@@ -1118,6 +1114,7 @@ describe Mastodon::CLI::Accounts do
   end
 
   describe '#cull' do
+    let(:action) { :cull }
     let(:delete_account_service) { instance_double(DeleteAccountService, call: nil) }
     let!(:tom)   { Fabricate(:account, updated_at: 30.days.ago, username: 'tom', uri: 'https://example.com/users/tom', domain: 'example.com', protocol: :activitypub) }
     let!(:bob)   { Fabricate(:account, updated_at: 30.days.ago, last_webfingered_at: nil, username: 'bob', uri: 'https://example.org/users/bob', domain: 'example.org', protocol: :activitypub) }
@@ -1138,14 +1135,14 @@ describe Mastodon::CLI::Accounts do
       end
 
       it 'deletes all inactive remote accounts that longer exist in the origin server' do
-        cli.cull
+        subject
 
         expect(delete_account_service).to have_received(:call).with(bob, reserve_username: false).once
         expect(delete_account_service).to have_received(:call).with(gon, reserve_username: false).once
       end
 
       it 'does not delete any active remote account that still exists in the origin server' do
-        cli.cull
+        subject
 
         expect(delete_account_service).to_not have_received(:call).with(tom, reserve_username: false)
         expect(delete_account_service).to_not have_received(:call).with(ana, reserve_username: false)
@@ -1153,18 +1150,17 @@ describe Mastodon::CLI::Accounts do
       end
 
       it 'touches inactive remote accounts that have not been deleted' do
-        expect { cli.cull }.to(change { tales.reload.updated_at })
+        expect { subject }.to(change { tales.reload.updated_at })
       end
 
       it 'displays the summary correctly' do
-        expect { cli.cull }.to output(
-          a_string_including('Visited 5 accounts, removed 2')
-        ).to_stdout
+        expect { subject }
+          .to output_results('Visited 5 accounts, removed 2')
       end
     end
 
     context 'when a domain is specified' do
-      let(:domain) { 'example.net' }
+      let(:arguments) { ['example.net'] }
 
       before do
         stub_parallelize_with_progress!
@@ -1173,16 +1169,15 @@ describe Mastodon::CLI::Accounts do
       end
 
       it 'deletes inactive remote accounts that longer exist in the specified domain' do
-        cli.cull(domain)
+        subject
 
         expect(delete_account_service).to have_received(:call).with(gon, reserve_username: false).once
         expect(delete_account_service).to have_received(:call).with(tales, reserve_username: false).once
       end
 
       it 'displays the summary correctly' do
-        expect { cli.cull(domain) }.to output(
-          a_string_including('Visited 2 accounts, removed 2')
-        ).to_stdout
+        expect { subject }
+          .to output_results('Visited 2 accounts, removed 2')
       end
     end
 
@@ -1195,15 +1190,14 @@ describe Mastodon::CLI::Accounts do
         end
 
         it 'skips accounts from the unavailable domain' do
-          cli.cull
+          subject
 
           expect(delete_account_service).to_not have_received(:call).with(tales, reserve_username: false)
         end
 
         it 'displays the summary correctly' do
-          expect { cli.cull }.to output(
-            a_string_including("Visited 5 accounts, removed 0\nThe following domains were not available during the check:\n    example.net")
-          ).to_stdout
+          expect { subject }
+            .to output_results("Visited 5 accounts, removed 0\nThe following domains were not available during the check:\n    example.net")
         end
       end
 
@@ -1242,25 +1236,25 @@ describe Mastodon::CLI::Accounts do
   end
 
   describe '#reset_relationships' do
+    let(:action) { :reset_relationships }
     let(:target_account) { Fabricate(:account) }
     let(:arguments)      { [target_account.username] }
 
     context 'when no option is given' do
       it 'exits with an error message indicating that at least one option is required' do
-        expect { cli.invoke(:reset_relationships, arguments) }.to output(
-          a_string_including('Please specify either --follows or --followers, or both')
-        ).to_stdout
+        expect { subject }
+          .to output_results('Please specify either --follows or --followers, or both')
           .and raise_error(SystemExit)
       end
     end
 
     context 'when the given username is not found' do
       let(:arguments) { ['non_existent_username'] }
+      let(:options) { { follows: true } }
 
       it 'exits with an error message indicating that there is no such account' do
-        expect { cli.invoke(:reset_relationships, arguments, follows: true) }.to output(
-          a_string_including('No such account')
-        ).to_stdout
+        expect { subject }
+          .to output_results('No such account')
           .and raise_error(SystemExit)
       end
     end
@@ -1277,7 +1271,7 @@ describe Mastodon::CLI::Accounts do
         end
 
         it 'resets all "following" relationships from the target account' do
-          cli.invoke(:reset_relationships, arguments, options)
+          subject
 
           expect(target_account.reload.following).to be_empty
         end
@@ -1285,15 +1279,14 @@ describe Mastodon::CLI::Accounts do
         it 'calls BootstrapTimelineWorker once to rebuild the timeline' do
           allow(BootstrapTimelineWorker).to receive(:perform_async)
 
-          cli.invoke(:reset_relationships, arguments, options)
+          subject
 
           expect(BootstrapTimelineWorker).to have_received(:perform_async).with(target_account.id).once
         end
 
         it 'displays a successful message' do
-          expect { cli.invoke(:reset_relationships, arguments, options) }.to output(
-            a_string_including("Processed #{total_relationships} relationships")
-          ).to_stdout
+          expect { subject }
+            .to output_results("Processed #{total_relationships} relationships")
         end
       end
 
@@ -1305,15 +1298,14 @@ describe Mastodon::CLI::Accounts do
         end
 
         it 'resets all "followers" relationships from the target account' do
-          cli.invoke(:reset_relationships, arguments, options)
+          subject
 
           expect(target_account.reload.followers).to be_empty
         end
 
         it 'displays a successful message' do
-          expect { cli.invoke(:reset_relationships, arguments, options) }.to output(
-            a_string_including("Processed #{total_relationships} relationships")
-          ).to_stdout
+          expect { subject }
+            .to output_results("Processed #{total_relationships} relationships")
         end
       end
 
@@ -1326,13 +1318,13 @@ describe Mastodon::CLI::Accounts do
         end
 
         it 'resets all "followers" relationships from the target account' do
-          cli.invoke(:reset_relationships, arguments, options)
+          subject
 
           expect(target_account.reload.followers).to be_empty
         end
 
         it 'resets all "following" relationships from the target account' do
-          cli.invoke(:reset_relationships, arguments, options)
+          subject
 
           expect(target_account.reload.following).to be_empty
         end
@@ -1340,21 +1332,21 @@ describe Mastodon::CLI::Accounts do
         it 'calls BootstrapTimelineWorker once to rebuild the timeline' do
           allow(BootstrapTimelineWorker).to receive(:perform_async)
 
-          cli.invoke(:reset_relationships, arguments, options)
+          subject
 
           expect(BootstrapTimelineWorker).to have_received(:perform_async).with(target_account.id).once
         end
 
         it 'displays a successful message' do
-          expect { cli.invoke(:reset_relationships, arguments, options) }.to output(
-            a_string_including("Processed #{total_relationships} relationships")
-          ).to_stdout
+          expect { subject }
+            .to output_results("Processed #{total_relationships} relationships")
         end
       end
     end
   end
 
   describe '#prune' do
+    let(:action) { :prune }
     let!(:local_account)     { Fabricate(:account) }
     let!(:bot_account)       { Fabricate(:account, bot: true, domain: 'example.com') }
     let!(:group_account)     { Fabricate(:account, actor_type: 'Group', domain: 'example.com') }
@@ -1369,7 +1361,7 @@ describe Mastodon::CLI::Accounts do
     end
 
     it 'prunes all remote accounts with no interactions with local users' do
-      cli.prune
+      subject
 
       prunable_account_ids = prunable_accounts.pluck(:id)
 
@@ -1377,42 +1369,39 @@ describe Mastodon::CLI::Accounts do
     end
 
     it 'displays a successful message' do
-      expect { cli.prune }.to output(
-        a_string_including("OK, pruned #{prunable_accounts.size} accounts")
-      ).to_stdout
+      expect { subject }
+        .to output_results("OK, pruned #{prunable_accounts.size} accounts")
     end
 
     it 'does not prune local accounts' do
-      cli.prune
+      subject
 
       expect(Account.exists?(id: local_account.id)).to be(true)
     end
 
     it 'does not prune bot accounts' do
-      cli.prune
+      subject
 
       expect(Account.exists?(id: bot_account.id)).to be(true)
     end
 
     it 'does not prune group accounts' do
-      cli.prune
+      subject
 
       expect(Account.exists?(id: group_account.id)).to be(true)
     end
 
     it 'does not prune accounts that have been mentioned' do
-      cli.prune
+      subject
 
       expect(Account.exists?(id: mentioned_account.id)).to be true
     end
 
     context 'with --dry-run option' do
-      before do
-        cli.options = { dry_run: true }
-      end
+      let(:options) { { dry_run: true } }
 
       it 'does not prune any account' do
-        cli.prune
+        subject
 
         prunable_account_ids = prunable_accounts.pluck(:id)
 
@@ -1420,14 +1409,14 @@ describe Mastodon::CLI::Accounts do
       end
 
       it 'displays a successful message with (DRY RUN)' do
-        expect { cli.prune }.to output(
-          a_string_including("OK, pruned #{prunable_accounts.size} accounts (DRY RUN)")
-        ).to_stdout
+        expect { subject }
+          .to output_results("OK, pruned #{prunable_accounts.size} accounts (DRY RUN)")
       end
     end
   end
 
   describe '#migrate' do
+    let(:action) { :migrate }
     let!(:source_account)         { Fabricate(:account) }
     let!(:target_account)         { Fabricate(:account, domain: 'example.com') }
     let(:arguments)               { [source_account.username] }
@@ -1441,7 +1430,7 @@ describe Mastodon::CLI::Accounts do
 
     shared_examples 'a successful migration' do
       it 'calls the MoveService for the last migration' do
-        cli.invoke(:migrate, arguments, options)
+        subject
 
         last_migration = source_account.migrations.last
 
@@ -1449,9 +1438,8 @@ describe Mastodon::CLI::Accounts do
       end
 
       it 'displays a successful message' do
-        expect { cli.invoke(:migrate, arguments, options) }.to output(
-          a_string_including("OK, migrated #{source_account.acct} to #{target_account.acct}")
-        ).to_stdout
+        expect { subject }
+          .to output_results("OK, migrated #{source_account.acct} to #{target_account.acct}")
       end
     end
 
@@ -1459,29 +1447,27 @@ describe Mastodon::CLI::Accounts do
       let(:options) { { replay: true, target: "#{target_account.username}@example.com" } }
 
       it 'exits with an error message indicating that using both options is not possible' do
-        expect { cli.invoke(:migrate, arguments, options) }.to output(
-          a_string_including('Use --replay or --target, not both')
-        ).to_stdout
+        expect { subject }
+          .to output_results('Use --replay or --target, not both')
           .and raise_error(SystemExit)
       end
     end
 
     context 'when no option is given' do
       it 'exits with an error message indicating that at least one option must be used' do
-        expect { cli.invoke(:migrate, arguments, {}) }.to output(
-          a_string_including('Use either --replay or --target')
-        ).to_stdout
+        expect { subject }
+          .to output_results('Use either --replay or --target')
           .and raise_error(SystemExit)
       end
     end
 
     context 'when the given username is not found' do
       let(:arguments) { ['non_existent_username'] }
+      let(:options) { { replay: true } }
 
       it 'exits with an error message indicating that there is no such account' do
-        expect { cli.invoke(:migrate, arguments, replay: true) }.to output(
-          a_string_including("No such account: #{arguments.first}")
-        ).to_stdout
+        expect { subject }
+          .to output_results("No such account: #{arguments.first}")
           .and raise_error(SystemExit)
       end
     end
@@ -1491,9 +1477,8 @@ describe Mastodon::CLI::Accounts do
 
       context 'when the specified account has no previous migrations' do
         it 'exits with an error message indicating that the given account has no previous migrations' do
-          expect { cli.invoke(:migrate, arguments, options) }.to output(
-            a_string_including('The specified account has not performed any migration')
-          ).to_stdout
+          expect { subject }
+            .to output_results('The specified account has not performed any migration')
             .and raise_error(SystemExit)
         end
       end
@@ -1515,9 +1500,8 @@ describe Mastodon::CLI::Accounts do
           end
 
           it 'exits with an error message' do
-            expect { cli.invoke(:migrate, arguments, options) }.to output(
-              a_string_including('The specified account is not redirecting to its last migration target. Use --force if you want to replay the migration anyway')
-            ).to_stdout
+            expect { subject }
+              .to output_results('The specified account is not redirecting to its last migration target. Use --force if you want to replay the migration anyway')
               .and raise_error(SystemExit)
           end
         end
@@ -1544,9 +1528,8 @@ describe Mastodon::CLI::Accounts do
         end
 
         it 'exits with an error message indicating that there is no such account' do
-          expect { cli.invoke(:migrate, arguments, options) }.to output(
-            a_string_including("The specified target account could not be found: #{options[:target]}")
-          ).to_stdout
+          expect { subject }
+            .to output_results("The specified target account could not be found: #{options[:target]}")
             .and raise_error(SystemExit)
         end
       end
@@ -1557,7 +1540,7 @@ describe Mastodon::CLI::Accounts do
         end
 
         it 'creates a migration for the specified account with the target account' do
-          cli.invoke(:migrate, arguments, options)
+          subject
 
           last_migration = source_account.migrations.last
 
@@ -1569,9 +1552,8 @@ describe Mastodon::CLI::Accounts do
 
       context 'when the migration record is invalid' do
         it 'exits with an error indicating that the validation failed' do
-          expect { cli.invoke(:migrate, arguments, options) }.to output(
-            a_string_including('Error: Validation failed')
-          ).to_stdout
+          expect { subject }
+            .to output_results('Error: Validation failed')
             .and raise_error(SystemExit)
         end
       end
@@ -1582,9 +1564,8 @@ describe Mastodon::CLI::Accounts do
         end
 
         it 'exits with an error message' do
-          expect { cli.invoke(:migrate, arguments, options) }.to output(
-            a_string_including('The specified account is redirecting to a different target account. Use --force if you want to change the migration target')
-          ).to_stdout
+          expect { subject }
+            .to output_results('The specified account is redirecting to a different target account. Use --force if you want to change the migration target')
             .and raise_error(SystemExit)
         end
       end
diff --git a/spec/lib/mastodon/cli/cache_spec.rb b/spec/lib/mastodon/cli/cache_spec.rb
index c1ce04710..b1515801e 100644
--- a/spec/lib/mastodon/cli/cache_spec.rb
+++ b/spec/lib/mastodon/cli/cache_spec.rb
@@ -4,22 +4,29 @@ require 'rails_helper'
 require 'mastodon/cli/cache'
 
 describe Mastodon::CLI::Cache do
+  subject { cli.invoke(action, arguments, options) }
+
   let(:cli) { described_class.new }
+  let(:arguments) { [] }
+  let(:options) { {} }
 
   it_behaves_like 'CLI Command'
 
   describe '#clear' do
+    let(:action) { :clear }
+
     before { allow(Rails.cache).to receive(:clear) }
 
     it 'clears the Rails cache' do
-      expect { cli.invoke(:clear) }.to output(
-        a_string_including('OK')
-      ).to_stdout
+      expect { subject }
+        .to output_results('OK')
       expect(Rails.cache).to have_received(:clear)
     end
   end
 
   describe '#recount' do
+    let(:action) { :recount }
+
     context 'with the `accounts` argument' do
       let(:arguments) { ['accounts'] }
       let(:account_stat) { Fabricate(:account_stat) }
@@ -29,9 +36,8 @@ describe Mastodon::CLI::Cache do
       end
 
       it 're-calculates account records in the cache' do
-        expect { cli.invoke(:recount, arguments) }.to output(
-          a_string_including('OK')
-        ).to_stdout
+        expect { subject }
+          .to output_results('OK')
 
         expect(account_stat.reload.statuses_count).to be_zero
       end
@@ -46,9 +52,8 @@ describe Mastodon::CLI::Cache do
       end
 
       it 're-calculates account records in the cache' do
-        expect { cli.invoke(:recount, arguments) }.to output(
-          a_string_including('OK')
-        ).to_stdout
+        expect { subject }
+          .to output_results('OK')
 
         expect(status_stat.reload.replies_count).to be_zero
       end
@@ -58,9 +63,9 @@ describe Mastodon::CLI::Cache do
       let(:arguments) { ['other-type'] }
 
       it 'Exits with an error message' do
-        expect { cli.invoke(:recount, arguments) }.to output(
-          a_string_including('Unknown')
-        ).to_stdout.and raise_error(SystemExit)
+        expect { subject }
+          .to output_results('Unknown')
+          .and raise_error(SystemExit)
       end
     end
   end
diff --git a/spec/lib/mastodon/cli/canonical_email_blocks_spec.rb b/spec/lib/mastodon/cli/canonical_email_blocks_spec.rb
index 6e4675748..1745ea01b 100644
--- a/spec/lib/mastodon/cli/canonical_email_blocks_spec.rb
+++ b/spec/lib/mastodon/cli/canonical_email_blocks_spec.rb
@@ -4,42 +4,45 @@ require 'rails_helper'
 require 'mastodon/cli/canonical_email_blocks'
 
 describe Mastodon::CLI::CanonicalEmailBlocks do
+  subject { cli.invoke(action, arguments, options) }
+
   let(:cli) { described_class.new }
+  let(:arguments) { [] }
+  let(:options) { {} }
 
   it_behaves_like 'CLI Command'
 
   describe '#find' do
+    let(:action) { :find }
     let(:arguments) { ['user@example.com'] }
 
     context 'when a block is present' do
       before { Fabricate(:canonical_email_block, email: 'user@example.com') }
 
       it 'announces the presence of the block' do
-        expect { cli.invoke(:find, arguments) }.to output(
-          a_string_including('user@example.com is blocked')
-        ).to_stdout
+        expect { subject }
+          .to output_results('user@example.com is blocked')
       end
     end
 
     context 'when a block is not present' do
       it 'announces the absence of the block' do
-        expect { cli.invoke(:find, arguments) }.to output(
-          a_string_including('user@example.com is not blocked')
-        ).to_stdout
+        expect { subject }
+          .to output_results('user@example.com is not blocked')
       end
     end
   end
 
   describe '#remove' do
+    let(:action) { :remove }
     let(:arguments) { ['user@example.com'] }
 
     context 'when a block is present' do
       before { Fabricate(:canonical_email_block, email: 'user@example.com') }
 
       it 'removes the block' do
-        expect { cli.invoke(:remove, arguments) }.to output(
-          a_string_including('Unblocked user@example.com')
-        ).to_stdout
+        expect { subject }
+          .to output_results('Unblocked user@example.com')
 
         expect(CanonicalEmailBlock.matching_email('user@example.com')).to be_empty
       end
@@ -47,9 +50,8 @@ describe Mastodon::CLI::CanonicalEmailBlocks do
 
     context 'when a block is not present' do
       it 'announces the absence of the block' do
-        expect { cli.invoke(:remove, arguments) }.to output(
-          a_string_including('user@example.com is not blocked')
-        ).to_stdout
+        expect { subject }
+          .to output_results('user@example.com is not blocked')
       end
     end
   end
diff --git a/spec/lib/mastodon/cli/domains_spec.rb b/spec/lib/mastodon/cli/domains_spec.rb
index add754159..a10907f76 100644
--- a/spec/lib/mastodon/cli/domains_spec.rb
+++ b/spec/lib/mastodon/cli/domains_spec.rb
@@ -4,20 +4,26 @@ require 'rails_helper'
 require 'mastodon/cli/domains'
 
 describe Mastodon::CLI::Domains do
+  subject { cli.invoke(action, arguments, options) }
+
   let(:cli) { described_class.new }
+  let(:arguments) { [] }
+  let(:options) { {} }
 
   it_behaves_like 'CLI Command'
 
   describe '#purge' do
+    let(:action) { :purge }
+
     context 'with accounts from the domain' do
-      let(:options) { {} }
       let(:domain) { 'host.example' }
       let!(:account) { Fabricate(:account, domain: domain) }
+      let(:arguments) { [domain] }
 
       it 'removes the account' do
-        expect { cli.invoke(:purge, [domain], options) }.to output(
-          a_string_including('Removed 1 accounts')
-        ).to_stdout
+        expect { subject }
+          .to output_results('Removed 1 accounts')
+
         expect { account.reload }.to raise_error(ActiveRecord::RecordNotFound)
       end
     end
diff --git a/spec/lib/mastodon/cli/email_domain_blocks_spec.rb b/spec/lib/mastodon/cli/email_domain_blocks_spec.rb
index f5cb6c332..13deb05b6 100644
--- a/spec/lib/mastodon/cli/email_domain_blocks_spec.rb
+++ b/spec/lib/mastodon/cli/email_domain_blocks_spec.rb
@@ -4,96 +4,99 @@ require 'rails_helper'
 require 'mastodon/cli/email_domain_blocks'
 
 describe Mastodon::CLI::EmailDomainBlocks do
+  subject { cli.invoke(action, arguments, options) }
+
   let(:cli) { described_class.new }
+  let(:arguments) { [] }
+  let(:options) { {} }
 
   it_behaves_like 'CLI Command'
 
   describe '#list' do
+    let(:action) { :list }
+
     context 'with email domain block records' do
       let!(:parent_block) { Fabricate(:email_domain_block) }
       let!(:child_block) { Fabricate(:email_domain_block, parent: parent_block) }
-      let(:options) { {} }
 
       it 'lists the blocks' do
-        expect { cli.invoke(:list, [], options) }.to output(
-          a_string_including(parent_block.domain)
-          .and(a_string_including(child_block.domain))
-        ).to_stdout
+        expect { subject }
+          .to output_results(
+            parent_block.domain,
+            child_block.domain
+          )
       end
     end
   end
 
   describe '#add' do
-    context 'without any options' do
-      let(:options) { {} }
+    let(:action) { :add }
 
+    context 'without any options' do
       it 'warns about usage and exits' do
-        expect { cli.invoke(:add, [], options) }.to output(
-          a_string_including('No domain(s) given')
-        ).to_stdout.and raise_error(SystemExit)
+        expect { subject }
+          .to output_results('No domain(s) given')
+          .and raise_error(SystemExit)
       end
     end
 
     context 'when blocks exist' do
       let(:options) { {} }
       let(:domain) { 'host.example' }
+      let(:arguments) { [domain] }
 
       before { Fabricate(:email_domain_block, domain: domain) }
 
       it 'does not add a new block' do
-        expect { cli.invoke(:add, [domain], options) }.to output(
-          a_string_including('is already blocked')
-        ).to_stdout
+        expect { subject }
+          .to output_results('is already blocked')
           .and(not_change(EmailDomainBlock, :count))
       end
     end
 
     context 'when no blocks exist' do
-      let(:options) { {} }
       let(:domain) { 'host.example' }
+      let(:arguments) { [domain] }
 
       it 'adds a new block' do
-        expect { cli.invoke(:add, [domain], options) }.to output(
-          a_string_including('Added 1')
-        ).to_stdout
+        expect { subject }
+          .to output_results('Added 1')
           .and(change(EmailDomainBlock, :count).by(1))
       end
     end
   end
 
   describe '#remove' do
-    context 'without any options' do
-      let(:options) { {} }
+    let(:action) { :remove }
 
+    context 'without any options' do
       it 'warns about usage and exits' do
-        expect { cli.invoke(:remove, [], options) }.to output(
-          a_string_including('No domain(s) given')
-        ).to_stdout.and raise_error(SystemExit)
+        expect { subject }
+          .to output_results('No domain(s) given')
+          .and raise_error(SystemExit)
       end
     end
 
     context 'when blocks exist' do
-      let(:options) { {} }
       let(:domain) { 'host.example' }
+      let(:arguments) { [domain] }
 
       before { Fabricate(:email_domain_block, domain: domain) }
 
       it 'removes the block' do
-        expect { cli.invoke(:remove, [domain], options) }.to output(
-          a_string_including('Removed 1')
-        ).to_stdout
+        expect { subject }
+          .to output_results('Removed 1')
           .and(change(EmailDomainBlock, :count).by(-1))
       end
     end
 
     context 'when no blocks exist' do
-      let(:options) { {} }
       let(:domain) { 'host.example' }
+      let(:arguments) { [domain] }
 
       it 'does not remove a block' do
-        expect { cli.invoke(:remove, [domain], options) }.to output(
-          a_string_including('is not yet blocked')
-        ).to_stdout
+        expect { subject }
+          .to output_results('is not yet blocked')
           .and(not_change(EmailDomainBlock, :count))
       end
     end
diff --git a/spec/lib/mastodon/cli/emoji_spec.rb b/spec/lib/mastodon/cli/emoji_spec.rb
index 3441413b9..d05e972e7 100644
--- a/spec/lib/mastodon/cli/emoji_spec.rb
+++ b/spec/lib/mastodon/cli/emoji_spec.rb
@@ -4,10 +4,10 @@ require 'rails_helper'
 require 'mastodon/cli/emoji'
 
 describe Mastodon::CLI::Emoji do
-  subject { cli.invoke(action, args, options) }
+  subject { cli.invoke(action, arguments, options) }
 
   let(:cli) { described_class.new }
-  let(:args) { [] }
+  let(:arguments) { [] }
   let(:options) { {} }
 
   it_behaves_like 'CLI Command'
@@ -29,7 +29,7 @@ describe Mastodon::CLI::Emoji do
     context 'with existing custom emoji' do
       let(:import_path) { Rails.root.join('spec', 'fixtures', 'files', 'elite-assets.tar.gz') }
       let(:action) { :import }
-      let(:args) { [import_path] }
+      let(:arguments) { [import_path] }
 
       it 'reports about imported emoji' do
         expect { subject }
@@ -51,7 +51,7 @@ describe Mastodon::CLI::Emoji do
       after { FileUtils.rm_rf(export_path.dirname) }
 
       let(:export_path) { Rails.root.join('tmp', 'cli-tests', 'export.tar.gz') }
-      let(:args) { [export_path.dirname.to_s] }
+      let(:arguments) { [export_path.dirname.to_s] }
       let(:action) { :export }
 
       it 'reports about exported emoji' do
@@ -61,8 +61,4 @@ describe Mastodon::CLI::Emoji do
       end
     end
   end
-
-  def output_results(string)
-    output(a_string_including(string)).to_stdout
-  end
 end
diff --git a/spec/lib/mastodon/cli/feeds_spec.rb b/spec/lib/mastodon/cli/feeds_spec.rb
index e16113c85..199798052 100644
--- a/spec/lib/mastodon/cli/feeds_spec.rb
+++ b/spec/lib/mastodon/cli/feeds_spec.rb
@@ -4,20 +4,25 @@ require 'rails_helper'
 require 'mastodon/cli/feeds'
 
 describe Mastodon::CLI::Feeds do
+  subject { cli.invoke(action, arguments, options) }
+
   let(:cli) { described_class.new }
+  let(:arguments) { [] }
+  let(:options) { {} }
 
   it_behaves_like 'CLI Command'
 
   describe '#build' do
+    let(:action) { :build }
+
     before { Fabricate(:account) }
 
     context 'with --all option' do
       let(:options) { { all: true } }
 
       it 'regenerates feeds for all accounts' do
-        expect { cli.invoke(:build, [], options) }.to output(
-          a_string_including('Regenerated feeds')
-        ).to_stdout
+        expect { subject }
+          .to output_results('Regenerated feeds')
       end
     end
 
@@ -27,9 +32,8 @@ describe Mastodon::CLI::Feeds do
       let(:arguments) { ['alice'] }
 
       it 'regenerates feeds for the account' do
-        expect { cli.invoke(:build, arguments) }.to output(
-          a_string_including('OK')
-        ).to_stdout
+        expect { subject }
+          .to output_results('OK')
       end
     end
 
@@ -37,22 +41,23 @@ describe Mastodon::CLI::Feeds do
       let(:arguments) { ['invalid-username'] }
 
       it 'displays an error and exits' do
-        expect { cli.invoke(:build, arguments) }.to output(
-          a_string_including('No such account')
-        ).to_stdout.and raise_error(SystemExit)
+        expect { subject }
+          .to output_results('No such account')
+          .and raise_error(SystemExit)
       end
     end
   end
 
   describe '#clear' do
+    let(:action) { :clear }
+
     before do
       allow(redis).to receive(:del).with(key_namespace)
     end
 
     it 'clears the redis `feed:*` namespace' do
-      expect { cli.invoke(:clear) }.to output(
-        a_string_including('OK')
-      ).to_stdout
+      expect { subject }
+        .to output_results('OK')
 
       expect(redis).to have_received(:del).with(key_namespace).once
     end
diff --git a/spec/lib/mastodon/cli/ip_blocks_spec.rb b/spec/lib/mastodon/cli/ip_blocks_spec.rb
index 684314dc7..dc967a69c 100644
--- a/spec/lib/mastodon/cli/ip_blocks_spec.rb
+++ b/spec/lib/mastodon/cli/ip_blocks_spec.rb
@@ -4,11 +4,16 @@ require 'rails_helper'
 require 'mastodon/cli/ip_blocks'
 
 describe Mastodon::CLI::IpBlocks do
+  subject { cli.invoke(action, arguments, options) }
+
   let(:cli) { described_class.new }
+  let(:arguments) { [] }
+  let(:options) { {} }
 
   it_behaves_like 'CLI Command'
 
   describe '#add' do
+    let(:action) { :add }
     let(:ip_list) do
       [
         '192.0.2.1',
@@ -25,10 +30,11 @@ describe Mastodon::CLI::IpBlocks do
       ]
     end
     let(:options) { { severity: 'no_access' } }
+    let(:arguments) { ip_list }
 
     shared_examples 'ip address blocking' do
       it 'blocks all specified IP addresses' do
-        cli.invoke(:add, ip_list, options)
+        subject
 
         blocked_ip_addresses = IpBlock.where(ip: ip_list).pluck(:ip)
         expected_ip_addresses = ip_list.map { |ip| IPAddr.new(ip) }
@@ -37,7 +43,7 @@ describe Mastodon::CLI::IpBlocks do
       end
 
       it 'sets the severity for all blocked IP addresses' do
-        cli.invoke(:add, ip_list, options)
+        subject
 
         blocked_ips_severity = IpBlock.where(ip: ip_list).pluck(:severity).all?(options[:severity])
 
@@ -45,9 +51,8 @@ describe Mastodon::CLI::IpBlocks do
       end
 
       it 'displays a success message with a summary' do
-        expect { cli.invoke(:add, ip_list, options) }.to output(
-          a_string_including("Added #{ip_list.size}, skipped 0, failed 0")
-        ).to_stdout
+        expect { subject }
+          .to output_results("Added #{ip_list.size}, skipped 0, failed 0")
       end
     end
 
@@ -57,19 +62,19 @@ describe Mastodon::CLI::IpBlocks do
 
     context 'when a specified IP address is already blocked' do
       let!(:blocked_ip) { IpBlock.create(ip: ip_list.last, severity: options[:severity]) }
+      let(:arguments) { ip_list }
 
       it 'skips the already blocked IP address' do
         allow(IpBlock).to receive(:new).and_call_original
 
-        cli.invoke(:add, ip_list, options)
+        subject
 
         expect(IpBlock).to_not have_received(:new).with(ip: ip_list.last)
       end
 
       it 'displays the correct summary' do
-        expect { cli.invoke(:add, ip_list, options) }.to output(
-          a_string_including("#{ip_list.last} is already blocked\nAdded #{ip_list.size - 1}, skipped 1, failed 0")
-        ).to_stdout
+        expect { subject }
+          .to output_results("#{ip_list.last} is already blocked\nAdded #{ip_list.size - 1}, skipped 1, failed 0")
       end
 
       context 'with --force option' do
@@ -77,7 +82,7 @@ describe Mastodon::CLI::IpBlocks do
         let(:options) { { severity: 'sign_up_requires_approval', force: true } }
 
         it 'overwrites the existing IP block record' do
-          expect { cli.invoke(:add, ip_list, options) }
+          expect { subject }
             .to change { blocked_ip.reload.severity }
             .from('no_access')
             .to('sign_up_requires_approval')
@@ -89,11 +94,11 @@ describe Mastodon::CLI::IpBlocks do
 
     context 'when a specified IP address is invalid' do
       let(:ip_list) { ['320.15.175.0', '9.5.105.255', '0.0.0.0'] }
+      let(:arguments) { ip_list }
 
       it 'displays the correct summary' do
-        expect { cli.invoke(:add, ip_list, options) }.to output(
-          a_string_including("#{ip_list.first} is invalid\nAdded #{ip_list.size - 1}, skipped 0, failed 1")
-        ).to_stdout
+        expect { subject }
+          .to output_results("#{ip_list.first} is invalid\nAdded #{ip_list.size - 1}, skipped 0, failed 1")
       end
     end
 
@@ -124,6 +129,7 @@ describe Mastodon::CLI::IpBlocks do
     context 'when a specified IP address fails to be blocked' do
       let(:ip_address) { '127.0.0.1' }
       let(:ip_block) { instance_double(IpBlock, ip: ip_address, save: false) }
+      let(:arguments) { [ip_address] }
 
       before do
         allow(IpBlock).to receive(:new).and_return(ip_block)
@@ -132,24 +138,25 @@ describe Mastodon::CLI::IpBlocks do
       end
 
       it 'displays an error message' do
-        expect { cli.invoke(:add, [ip_address], options) }
-          .to output(
-            a_string_including("#{ip_address} could not be saved")
-          ).to_stdout
+        expect { subject }
+          .to output_results("#{ip_address} could not be saved")
       end
     end
 
     context 'when no IP address is provided' do
+      let(:arguments) { [] }
+
       it 'exits with an error message' do
-        expect { cli.add }.to output(
-          a_string_including('No IP(s) given')
-        ).to_stdout
+        expect { subject }
+          .to output_results('No IP(s) given')
           .and raise_error(SystemExit)
       end
     end
   end
 
   describe '#remove' do
+    let(:action) { :remove }
+
     context 'when removing exact matches' do
       let(:ip_list) do
         [
@@ -166,21 +173,21 @@ describe Mastodon::CLI::IpBlocks do
           '::/128',
         ]
       end
+      let(:arguments) { ip_list }
 
       before do
         ip_list.each { |ip| IpBlock.create(ip: ip, severity: :no_access) }
       end
 
       it 'removes exact IP blocks' do
-        cli.invoke(:remove, ip_list)
+        subject
 
         expect(IpBlock.where(ip: ip_list)).to_not exist
       end
 
       it 'displays success message with a summary' do
-        expect { cli.invoke(:remove, ip_list) }.to output(
-          a_string_including("Removed #{ip_list.size}, skipped 0")
-        ).to_stdout
+        expect { subject }
+          .to output_results("Removed #{ip_list.size}, skipped 0")
       end
     end
 
@@ -192,13 +199,13 @@ describe Mastodon::CLI::IpBlocks do
       let(:options) { { force: true } }
 
       it 'removes blocks for IP ranges that cover given IP(s)' do
-        cli.invoke(:remove, arguments, options)
+        subject
 
         expect(IpBlock.where(id: [first_ip_range_block.id, second_ip_range_block.id])).to_not exist
       end
 
       it 'does not remove other IP ranges' do
-        cli.invoke(:remove, arguments, options)
+        subject
 
         expect(IpBlock.where(id: third_ip_range_block.id)).to exist
       end
@@ -206,47 +213,46 @@ describe Mastodon::CLI::IpBlocks do
 
     context 'when a specified IP address is not blocked' do
       let(:unblocked_ip) { '192.0.2.1' }
+      let(:arguments) { [unblocked_ip] }
 
       it 'skips the IP address' do
-        expect { cli.invoke(:remove, [unblocked_ip]) }.to output(
-          a_string_including("#{unblocked_ip} is not yet blocked")
-        ).to_stdout
+        expect { subject }
+          .to output_results("#{unblocked_ip} is not yet blocked")
       end
 
       it 'displays the summary correctly' do
-        expect { cli.invoke(:remove, [unblocked_ip]) }.to output(
-          a_string_including('Removed 0, skipped 1')
-        ).to_stdout
+        expect { subject }
+          .to output_results('Removed 0, skipped 1')
       end
     end
 
     context 'when a specified IP address is invalid' do
       let(:invalid_ip) { '320.15.175.0' }
+      let(:arguments) { [invalid_ip] }
 
       it 'skips the invalid IP address' do
-        expect { cli.invoke(:remove, [invalid_ip]) }.to output(
-          a_string_including("#{invalid_ip} is invalid")
-        ).to_stdout
+        expect { subject }
+          .to output_results("#{invalid_ip} is invalid")
       end
 
       it 'displays the summary correctly' do
-        expect { cli.invoke(:remove, [invalid_ip]) }.to output(
-          a_string_including('Removed 0, skipped 1')
-        ).to_stdout
+        expect { subject }
+          .to output_results('Removed 0, skipped 1')
       end
     end
 
     context 'when no IP address is provided' do
       it 'exits with an error message' do
-        expect { cli.remove }.to output(
-          a_string_including('No IP(s) given')
-        ).to_stdout
+        expect { subject }
+          .to output_results('No IP(s) given')
           .and raise_error(SystemExit)
       end
     end
   end
 
   describe '#export' do
+    let(:action) { :export }
+
     let(:first_ip_range_block) { IpBlock.create(ip: '192.168.0.0/24', severity: :no_access) }
     let(:second_ip_range_block) { IpBlock.create(ip: '10.0.0.0/16', severity: :no_access) }
     let(:third_ip_range_block) { IpBlock.create(ip: '127.0.0.1', severity: :sign_up_block) }
@@ -255,15 +261,13 @@ describe Mastodon::CLI::IpBlocks do
       let(:options) { { format: 'plain' } }
 
       it 'exports blocked IPs with "no_access" severity in plain format' do
-        expect { cli.invoke(:export, nil, options) }.to output(
-          a_string_including("#{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}\n#{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix}")
-        ).to_stdout
+        expect { subject }
+          .to output_results("#{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}\n#{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix}")
       end
 
       it 'does not export bloked IPs with different severities' do
-        expect { cli.invoke(:export, nil, options) }.to_not output(
-          a_string_including("#{third_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}")
-        ).to_stdout
+        expect { subject }
+          .to_not output_results("#{third_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}")
       end
     end
 
@@ -271,23 +275,20 @@ describe Mastodon::CLI::IpBlocks do
       let(:options) { { format: 'nginx' } }
 
       it 'exports blocked IPs with "no_access" severity in plain format' do
-        expect { cli.invoke(:export, nil, options) }.to output(
-          a_string_including("deny #{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix};\ndeny #{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix};")
-        ).to_stdout
+        expect { subject }
+          .to output_results("deny #{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix};\ndeny #{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix};")
       end
 
       it 'does not export bloked IPs with different severities' do
-        expect { cli.invoke(:export, nil, options) }.to_not output(
-          a_string_including("deny #{third_ip_range_block.ip}/#{first_ip_range_block.ip.prefix};")
-        ).to_stdout
+        expect { subject }
+          .to_not output_results("deny #{third_ip_range_block.ip}/#{first_ip_range_block.ip.prefix};")
       end
     end
 
     context 'when --format option is not provided' do
       it 'exports blocked IPs in plain format by default' do
-        expect { cli.export }.to output(
-          a_string_including("#{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}\n#{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix}")
-        ).to_stdout
+        expect { subject }
+          .to output_results("#{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}\n#{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix}")
       end
     end
   end
diff --git a/spec/lib/mastodon/cli/main_spec.rb b/spec/lib/mastodon/cli/main_spec.rb
index b5b5d6906..59f1fc478 100644
--- a/spec/lib/mastodon/cli/main_spec.rb
+++ b/spec/lib/mastodon/cli/main_spec.rb
@@ -4,13 +4,20 @@ require 'rails_helper'
 require 'mastodon/cli/main'
 
 describe Mastodon::CLI::Main do
+  subject { cli.invoke(action, arguments, options) }
+
+  let(:cli) { described_class.new }
+  let(:arguments) { [] }
+  let(:options) { {} }
+
   it_behaves_like 'CLI Command'
 
-  describe 'version' do
+  describe '#version' do
+    let(:action) { :version }
+
     it 'returns the Mastodon version' do
-      expect { described_class.new.invoke(:version) }.to output(
-        a_string_including(Mastodon::Version.to_s)
-      ).to_stdout
+      expect { subject }
+        .to output_results(Mastodon::Version.to_s)
     end
   end
 end
diff --git a/spec/lib/mastodon/cli/maintenance_spec.rb b/spec/lib/mastodon/cli/maintenance_spec.rb
index 95e695ab5..02169b7a4 100644
--- a/spec/lib/mastodon/cli/maintenance_spec.rb
+++ b/spec/lib/mastodon/cli/maintenance_spec.rb
@@ -4,20 +4,26 @@ require 'rails_helper'
 require 'mastodon/cli/maintenance'
 
 describe Mastodon::CLI::Maintenance do
+  subject { cli.invoke(action, arguments, options) }
+
   let(:cli) { described_class.new }
+  let(:arguments) { [] }
+  let(:options) { {} }
 
   it_behaves_like 'CLI Command'
 
   describe '#fix_duplicates' do
+    let(:action) { :fix_duplicates }
+
     context 'when the database version is too old' do
       before do
         allow(ActiveRecord::Migrator).to receive(:current_version).and_return(2000_01_01_000000) # Earlier than minimum
       end
 
       it 'Exits with error message' do
-        expect { cli.invoke :fix_duplicates }.to output(
-          a_string_including('is too old')
-        ).to_stdout.and raise_error(SystemExit)
+        expect { subject }
+          .to output_results('is too old')
+          .and raise_error(SystemExit)
       end
     end
 
@@ -28,9 +34,9 @@ describe Mastodon::CLI::Maintenance do
       end
 
       it 'Exits with error message' do
-        expect { cli.invoke :fix_duplicates }.to output(
-          a_string_including('more recent')
-        ).to_stdout.and raise_error(SystemExit)
+        expect { subject }
+          .to output_results('more recent')
+          .and raise_error(SystemExit)
       end
     end
 
@@ -41,9 +47,9 @@ describe Mastodon::CLI::Maintenance do
       end
 
       it 'Exits with error message' do
-        expect { cli.invoke :fix_duplicates }.to output(
-          a_string_including('Sidekiq is running')
-        ).to_stdout.and raise_error(SystemExit)
+        expect { subject }
+          .to output_results('Sidekiq is running')
+          .and raise_error(SystemExit)
       end
     end
   end
diff --git a/spec/lib/mastodon/cli/media_spec.rb b/spec/lib/mastodon/cli/media_spec.rb
index 6d510c1f5..6bbe7e746 100644
--- a/spec/lib/mastodon/cli/media_spec.rb
+++ b/spec/lib/mastodon/cli/media_spec.rb
@@ -4,18 +4,24 @@ require 'rails_helper'
 require 'mastodon/cli/media'
 
 describe Mastodon::CLI::Media do
+  subject { cli.invoke(action, arguments, options) }
+
   let(:cli) { described_class.new }
+  let(:arguments) { [] }
+  let(:options) { {} }
 
   it_behaves_like 'CLI Command'
 
   describe '#remove' do
+    let(:action) { :remove }
+
     context 'with --prune-profiles and --remove-headers' do
       let(:options) { { prune_profiles: true, remove_headers: true } }
 
       it 'warns about usage and exits' do
-        expect { cli.invoke(:remove, [], options) }.to output(
-          a_string_including('--prune-profiles and --remove-headers should not be specified simultaneously')
-        ).to_stdout.and raise_error(SystemExit)
+        expect { subject }
+          .to output_results('--prune-profiles and --remove-headers should not be specified simultaneously')
+          .and raise_error(SystemExit)
       end
     end
 
@@ -23,9 +29,9 @@ describe Mastodon::CLI::Media do
       let(:options) { { include_follows: true } }
 
       it 'warns about usage and exits' do
-        expect { cli.invoke(:remove, [], options) }.to output(
-          a_string_including('--include-follows can only be used with --prune-profiles or --remove-headers')
-        ).to_stdout.and raise_error(SystemExit)
+        expect { subject }
+          .to output_results('--include-follows can only be used with --prune-profiles or --remove-headers')
+          .and raise_error(SystemExit)
       end
     end
 
@@ -38,9 +44,8 @@ describe Mastodon::CLI::Media do
         let(:options) { { prune_profiles: true } }
 
         it 'removes account avatars' do
-          expect { cli.invoke(:remove, [], options) }.to output(
-            a_string_including('Visited 1')
-          ).to_stdout
+          expect { subject }
+            .to output_results('Visited 1')
 
           expect(account.reload.avatar).to be_blank
         end
@@ -50,9 +55,8 @@ describe Mastodon::CLI::Media do
         let(:options) { { remove_headers: true } }
 
         it 'removes account header' do
-          expect { cli.invoke(:remove, [], options) }.to output(
-            a_string_including('Visited 1')
-          ).to_stdout
+          expect { subject }
+            .to output_results('Visited 1')
 
           expect(account.reload.header).to be_blank
         end
@@ -64,9 +68,8 @@ describe Mastodon::CLI::Media do
 
       context 'without options' do
         it 'removes account avatars' do
-          expect { cli.invoke(:remove) }.to output(
-            a_string_including('Removed 1')
-          ).to_stdout
+          expect { subject }
+            .to output_results('Removed 1')
 
           expect(media_attachment.reload.file).to be_blank
           expect(media_attachment.reload.thumbnail).to be_blank
@@ -76,25 +79,24 @@ describe Mastodon::CLI::Media do
   end
 
   describe '#usage' do
-    context 'without options' do
-      let(:options) { {} }
+    let(:action) { :usage }
 
+    context 'without options' do
       it 'reports about storage size' do
-        expect { cli.invoke(:usage, [], options) }.to output(
-          a_string_including('0 Bytes')
-        ).to_stdout
+        expect { subject }
+          .to output_results('0 Bytes')
       end
     end
   end
 
   describe '#refresh' do
-    context 'without any options' do
-      let(:options) { {} }
+    let(:action) { :refresh }
 
+    context 'without any options' do
       it 'warns about usage and exits' do
-        expect { cli.invoke(:refresh, [], options) }.to output(
-          a_string_including('Specify the source')
-        ).to_stdout.and raise_error(SystemExit)
+        expect { subject }
+          .to output_results('Specify the source')
+          .and raise_error(SystemExit)
       end
     end
 
@@ -108,9 +110,8 @@ describe Mastodon::CLI::Media do
       let(:status) { Fabricate(:status) }
 
       it 'redownloads the attachment file' do
-        expect { cli.invoke(:refresh, [], options) }.to output(
-          a_string_including('Downloaded 1 media')
-        ).to_stdout
+        expect { subject }
+          .to output_results('Downloaded 1 media')
       end
     end
 
@@ -119,9 +120,9 @@ describe Mastodon::CLI::Media do
         let(:options) { { account: 'not-real-user@example.host' } }
 
         it 'warns about usage and exits' do
-          expect { cli.invoke(:refresh, [], options) }.to output(
-            a_string_including('No such account')
-          ).to_stdout.and raise_error(SystemExit)
+          expect { subject }
+            .to output_results('No such account')
+            .and raise_error(SystemExit)
         end
       end
 
@@ -135,9 +136,8 @@ describe Mastodon::CLI::Media do
         let(:account) { Fabricate(:account) }
 
         it 'redownloads the attachment file' do
-          expect { cli.invoke(:refresh, [], options) }.to output(
-            a_string_including('Downloaded 1 media')
-          ).to_stdout
+          expect { subject }
+            .to output_results('Downloaded 1 media')
         end
       end
     end
@@ -153,9 +153,8 @@ describe Mastodon::CLI::Media do
       let(:account) { Fabricate(:account, domain: domain) }
 
       it 'redownloads the attachment file' do
-        expect { cli.invoke(:refresh, [], options) }.to output(
-          a_string_including('Downloaded 1 media')
-        ).to_stdout
+        expect { subject }
+          .to output_results('Downloaded 1 media')
       end
     end
   end
diff --git a/spec/lib/mastodon/cli/preview_cards_spec.rb b/spec/lib/mastodon/cli/preview_cards_spec.rb
index a766d250e..951ae3758 100644
--- a/spec/lib/mastodon/cli/preview_cards_spec.rb
+++ b/spec/lib/mastodon/cli/preview_cards_spec.rb
@@ -4,11 +4,17 @@ require 'rails_helper'
 require 'mastodon/cli/preview_cards'
 
 describe Mastodon::CLI::PreviewCards do
+  subject { cli.invoke(action, arguments, options) }
+
   let(:cli) { described_class.new }
+  let(:arguments) { [] }
+  let(:options) { {} }
 
   it_behaves_like 'CLI Command'
 
   describe '#remove' do
+    let(:action) { :remove }
+
     context 'with relevant preview cards' do
       before do
         Fabricate(:preview_card, updated_at: 10.years.ago, type: :link)
@@ -18,10 +24,11 @@ describe Mastodon::CLI::PreviewCards do
 
       context 'with no arguments' do
         it 'deletes thumbnails for local preview cards' do
-          expect { cli.invoke(:remove) }.to output(
-            a_string_including('Removed 2 preview cards')
-              .and(a_string_including('approx. 119 KB'))
-          ).to_stdout
+          expect { subject }
+            .to output_results(
+              'Removed 2 preview cards',
+              'approx. 119 KB'
+            )
         end
       end
 
@@ -29,10 +36,11 @@ describe Mastodon::CLI::PreviewCards do
         let(:options) { { link: true } }
 
         it 'deletes thumbnails for local preview cards' do
-          expect { cli.invoke(:remove, [], options) }.to output(
-            a_string_including('Removed 1 link-type preview cards')
-              .and(a_string_including('approx. 59.6 KB'))
-          ).to_stdout
+          expect { subject }
+            .to output_results(
+              'Removed 1 link-type preview cards',
+              'approx. 59.6 KB'
+            )
         end
       end
 
@@ -40,10 +48,11 @@ describe Mastodon::CLI::PreviewCards do
         let(:options) { { days: 365 } }
 
         it 'deletes thumbnails for local preview cards' do
-          expect { cli.invoke(:remove, [], options) }.to output(
-            a_string_including('Removed 1 preview cards')
-              .and(a_string_including('approx. 59.6 KB'))
-          ).to_stdout
+          expect { subject }
+            .to output_results(
+              'Removed 1 preview cards',
+              'approx. 59.6 KB'
+            )
         end
       end
     end
diff --git a/spec/lib/mastodon/cli/settings_spec.rb b/spec/lib/mastodon/cli/settings_spec.rb
index 7dcd1110b..02d1042c5 100644
--- a/spec/lib/mastodon/cli/settings_spec.rb
+++ b/spec/lib/mastodon/cli/settings_spec.rb
@@ -7,59 +7,64 @@ describe Mastodon::CLI::Settings do
   it_behaves_like 'CLI Command'
 
   describe 'subcommand "registrations"' do
+    subject { cli.invoke(action, arguments, options) }
+
     let(:cli) { Mastodon::CLI::Registrations.new }
+    let(:arguments) { [] }
+    let(:options) { {} }
 
     before do
       Setting.registrations_mode = nil
     end
 
     describe '#open' do
+      let(:action) { :open }
+
       it 'changes "registrations_mode" to "open"' do
-        expect { cli.open }.to change(Setting, :registrations_mode).from(nil).to('open')
+        expect { subject }.to change(Setting, :registrations_mode).from(nil).to('open')
       end
 
       it 'displays success message' do
-        expect { cli.open }.to output(
-          a_string_including('OK')
-        ).to_stdout
+        expect { subject }
+          .to output_results('OK')
       end
     end
 
     describe '#approved' do
+      let(:action) { :approved }
+
       it 'changes "registrations_mode" to "approved"' do
-        expect { cli.approved }.to change(Setting, :registrations_mode).from(nil).to('approved')
+        expect { subject }.to change(Setting, :registrations_mode).from(nil).to('approved')
       end
 
       it 'displays success message' do
-        expect { cli.approved }.to output(
-          a_string_including('OK')
-        ).to_stdout
+        expect { subject }
+          .to output_results('OK')
       end
 
       context 'with --require-reason' do
-        before do
-          cli.options = { require_reason: true }
-        end
+        let(:options) { { require_reason: true } }
 
         it 'changes "registrations_mode" to "approved"' do
-          expect { cli.approved }.to change(Setting, :registrations_mode).from(nil).to('approved')
+          expect { subject }.to change(Setting, :registrations_mode).from(nil).to('approved')
         end
 
         it 'sets "require_invite_text" to "true"' do
-          expect { cli.approved }.to change(Setting, :require_invite_text).from(false).to(true)
+          expect { subject }.to change(Setting, :require_invite_text).from(false).to(true)
         end
       end
     end
 
     describe '#close' do
+      let(:action) { :close }
+
       it 'changes "registrations_mode" to "none"' do
-        expect { cli.close }.to change(Setting, :registrations_mode).from(nil).to('none')
+        expect { subject }.to change(Setting, :registrations_mode).from(nil).to('none')
       end
 
       it 'displays success message' do
-        expect { cli.close }.to output(
-          a_string_including('OK')
-        ).to_stdout
+        expect { subject }
+          .to output_results('OK')
       end
     end
   end
diff --git a/spec/lib/mastodon/cli/statuses_spec.rb b/spec/lib/mastodon/cli/statuses_spec.rb
index 70e4e2c08..63d494bbb 100644
--- a/spec/lib/mastodon/cli/statuses_spec.rb
+++ b/spec/lib/mastodon/cli/statuses_spec.rb
@@ -4,26 +4,31 @@ require 'rails_helper'
 require 'mastodon/cli/statuses'
 
 describe Mastodon::CLI::Statuses do
+  subject { cli.invoke(action, arguments, options) }
+
   let(:cli) { described_class.new }
+  let(:arguments) { [] }
+  let(:options) { {} }
 
   it_behaves_like 'CLI Command'
 
   describe '#remove', use_transactional_tests: false do
+    let(:action) { :remove }
+
     context 'with small batch size' do
       let(:options) { { batch_size: 0 } }
 
       it 'exits with error message' do
-        expect { cli.invoke :remove, [], options }.to output(
-          a_string_including('Cannot run')
-        ).to_stdout.and raise_error(SystemExit)
+        expect { subject }
+          .to output_results('Cannot run')
+          .and raise_error(SystemExit)
       end
     end
 
     context 'with default batch size' do
       it 'removes unreferenced statuses' do
-        expect { cli.invoke :remove }.to output(
-          a_string_including('Done after')
-        ).to_stdout
+        expect { subject }
+          .to output_results('Done after')
       end
     end
   end
diff --git a/spec/lib/mastodon/cli/upgrade_spec.rb b/spec/lib/mastodon/cli/upgrade_spec.rb
index 0d6494eee..6861e0488 100644
--- a/spec/lib/mastodon/cli/upgrade_spec.rb
+++ b/spec/lib/mastodon/cli/upgrade_spec.rb
@@ -4,23 +4,26 @@ require 'rails_helper'
 require 'mastodon/cli/upgrade'
 
 describe Mastodon::CLI::Upgrade do
+  subject { cli.invoke(action, arguments, options) }
+
   let(:cli) { described_class.new }
+  let(:arguments) { [] }
+  let(:options) { {} }
 
   it_behaves_like 'CLI Command'
 
   describe '#storage_schema' do
-    context 'with records that dont need upgrading' do
-      let(:options) { {} }
+    let(:action) { :storage_schema }
 
+    context 'with records that dont need upgrading' do
       before do
         Fabricate(:account)
         Fabricate(:media_attachment)
       end
 
       it 'does not upgrade storage for the attachments' do
-        expect { cli.invoke(:storage_schema, [], options) }.to output(
-          a_string_including('Upgraded storage schema of 0 records')
-        ).to_stdout
+        expect { subject }
+          .to output_results('Upgraded storage schema of 0 records')
       end
     end
   end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index d30e7201c..4394b470e 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -88,6 +88,7 @@ RSpec.configure do |config|
   config.include Chewy::Rspec::Helpers
   config.include Redisable
   config.include SignedRequestHelpers, type: :request
+  config.include CommandLineHelpers, type: :cli
 
   config.around(:each, use_transactional_tests: false) do |example|
     self.use_transactional_tests = false
diff --git a/spec/support/command_line_helpers.rb b/spec/support/command_line_helpers.rb
new file mode 100644
index 000000000..6f9d63d93
--- /dev/null
+++ b/spec/support/command_line_helpers.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module CommandLineHelpers
+  def output_results(*args)
+    output(
+      include(*args)
+    ).to_stdout
+  end
+end

From da3d8aff799e9b92ebe76e2dc8f6f953869a1e42 Mon Sep 17 00:00:00 2001
From: Michael Stanclift <mx@vmstan.com>
Date: Thu, 7 Dec 2023 08:40:44 -0600
Subject: [PATCH 41/73] Error handling for attachment batch delete process
 (#28184)

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
---
 app/lib/attachment_batch.rb | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/app/lib/attachment_batch.rb b/app/lib/attachment_batch.rb
index 13a9da828..b28f5c3d7 100644
--- a/app/lib/attachment_batch.rb
+++ b/app/lib/attachment_batch.rb
@@ -4,7 +4,8 @@ class AttachmentBatch
   # Maximum amount of objects you can delete in an S3 API call. It's
   # important to remember that this does not correspond to the number
   # of records in the batch, since records can have multiple attachments
-  LIMIT = 1_000
+  LIMIT = ENV.fetch('S3_BATCH_DELETE_LIMIT', 1000).to_i
+  MAX_RETRY = ENV.fetch('S3_BATCH_DELETE_RETRY', 3).to_i
 
   # Attributes generated and maintained by Paperclip (not all of them
   # are always used on every class, however)
@@ -95,6 +96,7 @@ class AttachmentBatch
     # objects can be processed at once, so we have to potentially
     # separate them into multiple calls.
 
+    retries = 0
     keys.each_slice(LIMIT) do |keys_slice|
       logger.debug { "Deleting #{keys_slice.size} objects" }
 
@@ -102,6 +104,17 @@ class AttachmentBatch
         objects: keys_slice.map { |key| { key: key } },
         quiet: true,
       })
+    rescue => e
+      retries += 1
+
+      if retries < MAX_RETRY
+        logger.debug "Retry #{retries}/#{MAX_RETRY} after #{e.message}"
+        sleep 2**retries
+        retry
+      else
+        logger.error "Batch deletion from S3 failed after #{e.message}"
+        raise e
+      end
     end
   end
 

From 3918dc68c7cf7d6f7d2352f9aa4b68d4d02b7d6a Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 7 Dec 2023 09:49:05 -0500
Subject: [PATCH 42/73] Use composite primary key for `PreviewCardsStatus`
 model (#28208)

---
 app/models/preview_cards_status.rb | 4 +---
 app/models/status.rb               | 4 +---
 2 files changed, 2 insertions(+), 6 deletions(-)

diff --git a/app/models/preview_cards_status.rb b/app/models/preview_cards_status.rb
index 341771e4d..214eec22e 100644
--- a/app/models/preview_cards_status.rb
+++ b/app/models/preview_cards_status.rb
@@ -9,9 +9,7 @@
 #  url             :string
 #
 class PreviewCardsStatus < ApplicationRecord
-  # Composite primary keys are not properly supported in Rails. However,
-  # we shouldn't need this anyway...
-  self.primary_key = nil
+  self.primary_key = [:preview_card_id, :status_id]
 
   belongs_to :preview_card
   belongs_to :status
diff --git a/app/models/status.rb b/app/models/status.rb
index eb7159dc6..4bdadae06 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -82,8 +82,7 @@ class Status < ApplicationRecord
 
   has_and_belongs_to_many :tags
 
-  # Because of a composite primary key, the `dependent` option cannot be used on this association
-  has_one :preview_cards_status, inverse_of: :status # rubocop:disable Rails/HasManyOrHasOneDependent
+  has_one :preview_cards_status, inverse_of: :status, dependent: :delete
 
   has_one :notification, as: :activity, dependent: :destroy
   has_one :status_stat, inverse_of: :status, dependent: nil
@@ -146,7 +145,6 @@ class Status < ApplicationRecord
   # The `prepend: true` option below ensures this runs before
   # the `dependent: destroy` callbacks remove relevant records
   before_destroy :unlink_from_conversations!, prepend: true
-  before_destroy :reset_preview_card!
 
   cache_associated :application,
                    :media_attachments,

From 8d8ae05a186bb8a5fca719dc6ea5a057d1aa197f Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 7 Dec 2023 10:27:41 -0500
Subject: [PATCH 43/73] Add spec coverage for `CLI::Media#lookup` command
 (#28266)

---
 spec/lib/mastodon/cli/media_spec.rb | 26 ++++++++++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/spec/lib/mastodon/cli/media_spec.rb b/spec/lib/mastodon/cli/media_spec.rb
index 6bbe7e746..24e1467a3 100644
--- a/spec/lib/mastodon/cli/media_spec.rb
+++ b/spec/lib/mastodon/cli/media_spec.rb
@@ -89,6 +89,32 @@ describe Mastodon::CLI::Media do
     end
   end
 
+  describe '#lookup' do
+    let(:action) { :lookup }
+    let(:arguments) { [url] }
+
+    context 'with valid url not connected to a record' do
+      let(:url) { 'https://example.host/assets/1' }
+
+      it 'warns about url and exits' do
+        expect { subject }
+          .to output_results('Not a media URL')
+          .and raise_error(SystemExit)
+      end
+    end
+
+    context 'with a valid media url' do
+      let(:status) { Fabricate(:status) }
+      let(:media_attachment) { Fabricate(:media_attachment, status: status) }
+      let(:url) { media_attachment.file.url(:original) }
+
+      it 'displays the url of a connected status' do
+        expect { subject }
+          .to output_results(status.id.to_s)
+      end
+    end
+  end
+
   describe '#refresh' do
     let(:action) { :refresh }
 

From 7e514688b3db1a88636ebb24afcaf1d637366f40 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Fri, 8 Dec 2023 04:27:33 -0500
Subject: [PATCH 44/73] Convert `api/v2/search` controller spec to request spec
 (#28269)

---
 .../api/v2/search_spec.rb}                    | 34 ++++++++-----------
 1 file changed, 15 insertions(+), 19 deletions(-)
 rename spec/{controllers/api/v2/search_controller_spec.rb => requests/api/v2/search_spec.rb} (79%)

diff --git a/spec/controllers/api/v2/search_controller_spec.rb b/spec/requests/api/v2/search_spec.rb
similarity index 79%
rename from spec/controllers/api/v2/search_controller_spec.rb
rename to spec/requests/api/v2/search_spec.rb
index a16716a10..d0778cba4 100644
--- a/spec/controllers/api/v2/search_controller_spec.rb
+++ b/spec/requests/api/v2/search_spec.rb
@@ -2,25 +2,21 @@
 
 require 'rails_helper'
 
-RSpec.describe Api::V2::SearchController do
-  render_views
-
+describe 'Search API' do
   context 'with token' do
-    let(:user)  { Fabricate(:user) }
-    let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:search') }
+    let(:user)    { Fabricate(:user) }
+    let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+    let(:scopes)  { 'read:search' }
+    let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
 
-    before do
-      allow(controller).to receive(:doorkeeper_token) { token }
-    end
-
-    describe 'GET #index' do
+    describe 'GET /api/v2/search' do
       let!(:bob)   { Fabricate(:account, username: 'bob_test') }
       let!(:ana)   { Fabricate(:account, username: 'ana_test') }
       let!(:tom)   { Fabricate(:account, username: 'tom_test') }
       let(:params) { { q: 'test' } }
 
       it 'returns http success' do
-        get :index, params: params
+        get '/api/v2/search', headers: headers, params: params
 
         expect(response).to have_http_status(200)
       end
@@ -29,7 +25,7 @@ RSpec.describe Api::V2::SearchController do
         let(:params) { { q: 'test', type: 'accounts' } }
 
         it 'returns all matching accounts' do
-          get :index, params: params
+          get '/api/v2/search', headers: headers, params: params
 
           expect(body_as_json[:accounts].pluck(:id)).to contain_exactly(bob.id.to_s, ana.id.to_s, tom.id.to_s)
         end
@@ -38,7 +34,7 @@ RSpec.describe Api::V2::SearchController do
           let(:params) { { q: 'test1', resolve: '1' } }
 
           it 'returns http unauthorized' do
-            get :index, params: params
+            get '/api/v2/search', headers: headers, params: params
 
             expect(response).to have_http_status(200)
           end
@@ -48,7 +44,7 @@ RSpec.describe Api::V2::SearchController do
           let(:params) { { q: 'test1', offset: 1 } }
 
           it 'returns http unauthorized' do
-            get :index, params: params
+            get '/api/v2/search', headers: headers, params: params
 
             expect(response).to have_http_status(200)
           end
@@ -62,7 +58,7 @@ RSpec.describe Api::V2::SearchController do
           end
 
           it 'returns only the followed accounts' do
-            get :index, params: params
+            get '/api/v2/search', headers: headers, params: params
 
             expect(body_as_json[:accounts].pluck(:id)).to contain_exactly(ana.id.to_s)
           end
@@ -73,7 +69,7 @@ RSpec.describe Api::V2::SearchController do
         before { allow(Search).to receive(:new).and_raise(Mastodon::SyntaxError) }
 
         it 'returns http unprocessable_entity' do
-          get :index, params: params
+          get '/api/v2/search', headers: headers, params: params
 
           expect(response).to have_http_status(422)
         end
@@ -83,7 +79,7 @@ RSpec.describe Api::V2::SearchController do
         before { allow(Search).to receive(:new).and_raise(ActiveRecord::RecordNotFound) }
 
         it 'returns http not_found' do
-          get :index, params: params
+          get '/api/v2/search', headers: headers, params: params
 
           expect(response).to have_http_status(404)
         end
@@ -92,11 +88,11 @@ RSpec.describe Api::V2::SearchController do
   end
 
   context 'without token' do
-    describe 'GET #index' do
+    describe 'GET /api/v2/search' do
       let(:search_params) { nil }
 
       before do
-        get :index, params: search_params
+        get '/api/v2/search', params: search_params
       end
 
       context 'without a `q` param' do

From 8de86eabbff9d5ff42f2340b1906a8595cd898e8 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Fri, 8 Dec 2023 04:51:57 -0500
Subject: [PATCH 45/73] Add spec for `CLI::Domains#crawl` command (#28271)

---
 lib/mastodon/cli/domains.rb           |  6 ++--
 spec/lib/mastodon/cli/domains_spec.rb | 47 +++++++++++++++++++++++++++
 2 files changed, 51 insertions(+), 2 deletions(-)

diff --git a/lib/mastodon/cli/domains.rb b/lib/mastodon/cli/domains.rb
index 329f17167..e092497dc 100644
--- a/lib/mastodon/cli/domains.rb
+++ b/lib/mastodon/cli/domains.rb
@@ -97,6 +97,8 @@ module Mastodon::CLI
       say("Removed #{custom_emojis_count} custom emojis#{dry_run_mode_suffix}", :green)
     end
 
+    CRAWL_SLEEP_TIME = 20
+
     option :concurrency, type: :numeric, default: 50, aliases: [:c]
     option :format, type: :string, default: 'summary', aliases: [:f]
     option :exclude_suspended, type: :boolean, default: false, aliases: [:x]
@@ -168,8 +170,8 @@ module Mastodon::CLI
         pool.post(domain, &work_unit)
       end
 
-      sleep 20
-      sleep 20 until pool.queue_length.zero?
+      sleep CRAWL_SLEEP_TIME
+      sleep CRAWL_SLEEP_TIME until pool.queue_length.zero?
 
       pool.shutdown
       pool.wait_for_termination(20)
diff --git a/spec/lib/mastodon/cli/domains_spec.rb b/spec/lib/mastodon/cli/domains_spec.rb
index a10907f76..24f341c12 100644
--- a/spec/lib/mastodon/cli/domains_spec.rb
+++ b/spec/lib/mastodon/cli/domains_spec.rb
@@ -28,4 +28,51 @@ describe Mastodon::CLI::Domains do
       end
     end
   end
+
+  describe '#crawl' do
+    let(:action) { :crawl }
+
+    context 'with accounts from the domain' do
+      let(:domain) { 'host.example' }
+
+      before do
+        Fabricate(:account, domain: domain)
+        stub_request(:get, 'https://host.example/api/v1/instance').to_return(status: 200, body: {}.to_json)
+        stub_request(:get, 'https://host.example/api/v1/instance/peers').to_return(status: 200, body: {}.to_json)
+        stub_request(:get, 'https://host.example/api/v1/instance/activity').to_return(status: 200, body: {}.to_json)
+        stub_const('Mastodon::CLI::Domains::CRAWL_SLEEP_TIME', 0)
+      end
+
+      context 'with --format of summary' do
+        let(:options) { { format: 'summary' } }
+
+        it 'crawls the domains and summarizes results' do
+          expect { subject }
+            .to output_results('Visited 1 domains, 0 failed')
+        end
+      end
+
+      context 'with --format of domains' do
+        let(:options) { { format: 'domains' } }
+
+        it 'crawls the domains and summarizes results' do
+          expect { subject }
+            .to output_results(domain)
+        end
+      end
+
+      context 'with --format of json' do
+        let(:options) { { format: 'json' } }
+
+        it 'crawls the domains and summarizes results' do
+          expect { subject }
+            .to output_results(json_summary)
+        end
+
+        def json_summary
+          Oj.dump('host.example': { activity: {} })
+        end
+      end
+    end
+  end
 end

From f37a1535efc7913c0f8b5d2c8e6588ff496c9984 Mon Sep 17 00:00:00 2001
From: JakePaustian <87162217+JakePaustian@users.noreply.github.com>
Date: Fri, 8 Dec 2023 03:52:28 -0600
Subject: [PATCH 46/73] Update CONTRIBUTING.md with additional requirements for
 API additions (#28274)

Co-authored-by: Jake Paustian <jake1500@iastate.edu>
---
 CONTRIBUTING.md | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c1a5fef79..b68a9bde3 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -11,6 +11,10 @@ You can contribute in the following ways:
 
 If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon).
 
+## API Changes and Additions
+
+Please note that any changes or additions made to the API should have an accompanying pull request on [our documentation repository](https://github.com/mastodon/documentation).
+
 ## Bug reports
 
 Bug reports and feature suggestions must use descriptive and concise titles and be submitted to [GitHub Issues](https://github.com/mastodon/mastodon/issues). Please use the search function to make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected.

From ca1b7efb069a3cd4532d0d635f70ea47358714c8 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 8 Dec 2023 10:52:32 +0100
Subject: [PATCH 47/73] New Crowdin Translations (automated) (#28275)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/fr.json    |  2 +-
 app/javascript/mastodon/locales/hu.json    |  4 ++--
 app/javascript/mastodon/locales/my.json    | 12 ++++++++++++
 app/javascript/mastodon/locales/pt-PT.json |  2 +-
 config/locales/my.yml                      |  1 +
 5 files changed, 17 insertions(+), 4 deletions(-)

diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 03b656cc4..7db4bf7bc 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -702,7 +702,7 @@
   "timeline_hint.resources.followers": "Les abonnés",
   "timeline_hint.resources.follows": "Les abonnements",
   "timeline_hint.resources.statuses": "Messages plus anciens",
-  "trends.counter_by_accounts": "{count, plural, one {{counter} personne} other {{counter} personnes}} au cours {days, plural, one {des dernières 24h} other {des {days} derniers jours}}",
+  "trends.counter_by_accounts": "{count, plural, one {{counter} pers.} other {{counter} pers.}} sur {days, plural, one {les dernières 24h} other {les {days} derniers jours}}",
   "trends.trending_now": "Tendance en ce moment",
   "ui.beforeunload": "Votre brouillon sera perdu si vous quittez Mastodon.",
   "units.short.billion": "{count}Md",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index d890e4188..386b15811 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -479,7 +479,7 @@
   "onboarding.actions.go_to_home": "Ugrás a saját hírfolyamra",
   "onboarding.compose.template": "Üdvözlet, #Mastodon!",
   "onboarding.follows.empty": "Sajnos jelenleg nem jeleníthető meg eredmény. Kipróbálhatod a keresést vagy böngészheted a felfedező oldalon a követni kívánt személyeket, vagy próbáld meg később.",
-  "onboarding.follows.lead": "A saját hírfolyamod az elsődleges tapasztalás a Mastodonon. Minél több embert követsz, annál aktívabb és érdekesebb a dolog. Az induláshoz itt van néhány javaslat:",
+  "onboarding.follows.lead": "A kezdőlapod a Mastodon használatának elsődleges módja. Minél több embert követsz, annál aktívabbak és érdekesebbek lesznek a dolgok. Az induláshoz itt van néhány javaslat:",
   "onboarding.follows.title": "Szabd személyre a kezdőlapodat",
   "onboarding.profile.discoverable": "Saját profil beállítása felfedezhetőként",
   "onboarding.profile.discoverable_hint": "A Mastodonon a felfedezhetőség választása esetén a saját bejegyzéseid megjelenhetnek a keresési eredmények és a felkapott tartalmak között, valamint a profilod a hozzád hasonló érdeklődési körrel rendelkező embereknél is ajánlásra kerülhet.",
@@ -720,7 +720,7 @@
   "upload_form.undo": "Törlés",
   "upload_form.video_description": "Leírás siket, hallássérült, vak vagy gyengénlátó emberek számára",
   "upload_modal.analyzing_picture": "Kép elemzése…",
-  "upload_modal.apply": "Alkalmazás",
+  "upload_modal.apply": "Alkalmaz",
   "upload_modal.applying": "Alkalmazás…",
   "upload_modal.choose_image": "Kép kiválasztása",
   "upload_modal.description_placeholder": "A gyors, barna róka átugrik a lusta kutya fölött",
diff --git a/app/javascript/mastodon/locales/my.json b/app/javascript/mastodon/locales/my.json
index 4078a4c06..917419d17 100644
--- a/app/javascript/mastodon/locales/my.json
+++ b/app/javascript/mastodon/locales/my.json
@@ -21,6 +21,7 @@
   "account.blocked": "ဘလော့ထားသည်",
   "account.browse_more_on_origin_server": "မူရင်းပရိုဖိုင်တွင် ပိုမိုကြည့်ရှုပါ။",
   "account.cancel_follow_request": "စောင့်ကြည့်မှု ပယ်ဖျက်ခြင်း",
+  "account.copy": "လင့်ခ်ကို ပရိုဖိုင်သို့ ကူးယူပါ",
   "account.direct": "@{name} သီးသန့် သိရှိနိုင်အောင် မန်းရှင်းခေါ်မည်",
   "account.disable_notifications": "@{name} ပို့စ်တင်သည့်အခါ ကျွန်ုပ်ထံ အသိပေးခြင်း မပြုလုပ်ရန်။",
   "account.domain_blocked": "ဒိုမိန်း ပိတ်ပင်ထားခဲ့သည်",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "ဖတ်ပြီးသားအဖြစ်မှတ်ထားပါ",
   "conversation.open": "Conversation ကိုကြည့်မည်",
   "conversation.with": "{အမည်များ} ဖြင့်",
+  "copy_icon_button.copied": "ကလစ်ဘုတ်သို့ ကူးပါ",
   "copypaste.copied": "ကူယူပြီးပါပြီ",
   "copypaste.copy_to_clipboard": "ကလစ်ဘုတ်သို့ ကူးပါ",
   "directory.federated": "သင် သိသော ဖက်ဒီမှ",
@@ -389,6 +391,7 @@
   "lists.search": "မိမိဖောလိုးထားသူများမှရှာဖွေမည်",
   "lists.subheading": "သင့်၏စာရင်းများ",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "loading_indicator.label": "လုပ်ဆောင်နေသည်…",
   "media_gallery.toggle_visible": "{number, plural, one {Hide image} other {Hide images}}",
   "moved_to_account_banner.text": "{movedToAccount} အကောင့်သို့ပြောင်းလဲထားသဖြင့် {disabledAccount} အကောင့်မှာပိတ်ထားသည်",
   "mute_modal.duration": "ကြာချိန်",
@@ -477,6 +480,13 @@
   "onboarding.follows.empty": "ယခုအချိန် မည်သည့်ရလဒ်ကိုမျှ မပြသနိုင်ပါ။ လူများကိုစောင့်ကြည့်ရန်အတွက် Explore စာမျက်နှာကို အသုံးပြု၍ စမ်းကြည့်နိုင်သည် သို့မဟုတ် နောက်မှ ထပ်စမ်းကြည့်ပါ။",
   "onboarding.follows.lead": "သင့်ကိုယ်ပိုင်ပို့စ်များ တင်နိုင်သည်။ သင်စောင့်ကြည့်သူ များလေလေ၊ စိတ်ဝင်စားစရာကောင်းသောပို့စ်များ တွေ့ရလေဖြစ်သည်။ ဤပရိုဖိုင်များမှာ ကောင်းမွန်သောအစပြုမှုတစ်ခုဖြစ်ပြီး ၎င်းတို့ကိုစောင့်ကြည့်ခြင်းမှလည်း အချိန်မရွေး ပယ်ဖျက်နိုင်ပါသည်။",
   "onboarding.follows.title": "Mastodon တွင် ရေပန်းစားခြင်း",
+  "onboarding.profile.discoverable": "ပရိုဖိုင် ရှာဖွေနိုင်ပါမည်",
+  "onboarding.profile.display_name": "ဖော်ပြမည့်အမည်",
+  "onboarding.profile.display_name_hint": "သင့်အမည်အပြည့်အစုံ သို့မဟုတ် သင့်အမည်ပြောင်။",
+  "onboarding.profile.note": "ကိုယ်ရေးအကျဉ်း",
+  "onboarding.profile.save_and_continue": "သိမ်းပြီး ဆက်လုပ်ပါ",
+  "onboarding.profile.title": "ပရိုဖိုင်စနစ် ထည့်သွင်းခြင်း",
+  "onboarding.profile.upload_avatar": "ပရိုဖိုင်ပုံ အပ်လုဒ်လုပ်ပါ",
   "onboarding.share.lead": "Mastodon တွင် သင့်အား မည်သို့ရှာတွေ့နိုင်သည်ကို အသိပေးပါ။",
   "onboarding.share.message": "Mastodon ရှိ ကျွန်ုပ်၏အမည်မှာ {username} ဖြစ်သည်။ ကျွန်ုပ်ကို {url} တွင် စောင့်ကြည့်နိုင်ပါသည်",
   "onboarding.share.next_steps": "ဖြစ်နိုင်ချေရှိသော နောက်အဆင့်များ -",
@@ -520,6 +530,7 @@
   "privacy.unlisted.short": "စာရင်းမသွင်းထားပါ",
   "privacy_policy.last_updated": "နောက်ဆုံး ပြင်ဆင်ခဲ့သည့်ရက်စွဲ {date}",
   "privacy_policy.title": "ကိုယ်ရေးအချက်အလက်မူဝါဒ",
+  "recommended": "အကြံပြုသည်",
   "refresh": "ပြန်လည်စတင်ပါ",
   "regeneration_indicator.label": "လုပ်ဆောင်နေသည်…",
   "regeneration_indicator.sublabel": "သင့်ပင်မစာမျက်နှာကို ပြင်ဆင်နေပါသည်။",
@@ -590,6 +601,7 @@
   "search.quick_action.status_search": "{x} နှင့် ကိုက်ညီသော ပို့စ်များ",
   "search.search_or_paste": "URL ရိုက်ထည့်ပါ သို့မဟုတ် ရှာဖွေပါ",
   "search_popout.full_text_search_disabled_message": "{domain} တွင် မရနိုင်ပါ။",
+  "search_popout.full_text_search_logged_out_message": "အကောင့်ဝင်ထားမှသာ ရနိုင်သည်။",
   "search_popout.language_code": "ISO ဘာသာစကားကုဒ်",
   "search_popout.options": "ရွေးချယ်ထားသည်များ ရှာဖွေရန်",
   "search_popout.quick_actions": "အမြန်လုပ်ဆောင်မှုများ",
diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json
index 2d1616960..d5055b0dc 100644
--- a/app/javascript/mastodon/locales/pt-PT.json
+++ b/app/javascript/mastodon/locales/pt-PT.json
@@ -7,7 +7,7 @@
   "about.domain_blocks.silenced.explanation": "Normalmente não verá perfis e conteúdo deste servidor, a menos que os procure explicitamente ou opte por os seguir.",
   "about.domain_blocks.silenced.title": "Limitados",
   "about.domain_blocks.suspended.explanation": "Nenhum dado deste servidor será processado, armazenado ou trocado, impossibilitando qualquer interação ou comunicação com os utilizadores dessas instâncias.",
-  "about.domain_blocks.suspended.title": "Supensos",
+  "about.domain_blocks.suspended.title": "Suspensos",
   "about.not_available": "Esta informação não foi disponibilizada neste servidor.",
   "about.powered_by": "Rede social descentralizada baseada no {mastodon}",
   "about.rules": "Regras do servidor",
diff --git a/config/locales/my.yml b/config/locales/my.yml
index 03ed771a4..4ba4fcfad 100644
--- a/config/locales/my.yml
+++ b/config/locales/my.yml
@@ -1324,6 +1324,7 @@ my:
       '86400': ၁ ရက်
     expires_in_prompt: ဘယ်တော့မှ
     generate: ဖိတ်ကြားချက်လင့်ခ် ဖန်တီးပါ
+    invalid: ဤဖိတ်ကြားချက်မှာ မမှန်ကန်ပါ
     invited_by: သင့်ကို ဖိတ်ခေါ်ထားသည် -
     max_uses:
       other: "%{count} အသုံးပြုမှုများ"

From a3cbb4b2f1aff565582c280c6408d01cf0b54350 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Fri, 8 Dec 2023 08:54:20 -0500
Subject: [PATCH 48/73] Add spec for `CLI::Search#deploy` command (#28272)

---
 spec/lib/mastodon/cli/search_spec.rb | 74 ++++++++++++++++++++++++++++
 1 file changed, 74 insertions(+)

diff --git a/spec/lib/mastodon/cli/search_spec.rb b/spec/lib/mastodon/cli/search_spec.rb
index 785dc2bd6..cb0c80c11 100644
--- a/spec/lib/mastodon/cli/search_spec.rb
+++ b/spec/lib/mastodon/cli/search_spec.rb
@@ -4,5 +4,79 @@ require 'rails_helper'
 require 'mastodon/cli/search'
 
 describe Mastodon::CLI::Search do
+  subject { cli.invoke(action, arguments, options) }
+
+  let(:cli) { described_class.new }
+  let(:arguments) { [] }
+  let(:options) { {} }
+
   it_behaves_like 'CLI Command'
+
+  describe '#deploy' do
+    let(:action) { :deploy }
+
+    context 'with concurrency out of range' do
+      let(:options) { { concurrency: -100 } }
+
+      it 'Exits with error message' do
+        expect { subject }
+          .to output_results('this concurrency setting')
+          .and raise_error(SystemExit)
+      end
+    end
+
+    context 'with batch size out of range' do
+      let(:options) { { batch_size: -100_000 } }
+
+      it 'Exits with error message' do
+        expect { subject }
+          .to output_results('this batch_size setting')
+          .and raise_error(SystemExit)
+      end
+    end
+
+    context 'without options' do
+      before { stub_search_indexes }
+
+      let(:indexed_count) { 1 }
+      let(:deleted_count) { 2 }
+
+      it 'reports about storage size' do
+        expect { subject }
+          .to output_results(
+            "Indexed #{described_class::INDICES.size * indexed_count} records",
+            "de-indexed #{described_class::INDICES.size * deleted_count}"
+          )
+      end
+    end
+
+    def stub_search_indexes
+      described_class::INDICES.each do |index|
+        allow(index)
+          .to receive_messages(
+            specification: instance_double(Chewy::Index::Specification, changed?: true, lock!: nil),
+            purge: nil
+          )
+
+        importer_double = importer_double_for(index)
+        allow(importer_double).to receive(:on_progress).and_yield([indexed_count, deleted_count])
+        allow("Importer::#{index}Importer".constantize)
+          .to receive(:new)
+          .and_return(importer_double)
+      end
+    end
+
+    def importer_double_for(index)
+      instance_double(
+        "Importer::#{index}Importer".constantize,
+        clean_up!: nil,
+        estimate!: 100,
+        import!: nil,
+        on_failure: nil,
+        # on_progress: nil,
+        optimize_for_import!: nil,
+        optimize_for_search!: nil
+      )
+    end
+  end
 end

From 490e4969a1a657cefbc9e4a9c11d5bffb5dc7a50 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Fri, 8 Dec 2023 08:54:48 -0500
Subject: [PATCH 49/73] Correct section naming in maintenance cli script
 (#28279)

---
 lib/mastodon/cli/maintenance.rb | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/lib/mastodon/cli/maintenance.rb b/lib/mastodon/cli/maintenance.rb
index c53d74254..d0eff7da6 100644
--- a/lib/mastodon/cli/maintenance.rb
+++ b/lib/mastodon/cli/maintenance.rb
@@ -346,7 +346,7 @@ module Mastodon::CLI
 
       remove_index_if_exists!(:announcement_reactions, 'index_announcement_reactions_on_account_id_and_announcement_id')
 
-      say 'Removing duplicate account identity proofs…'
+      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|
         AnnouncementReaction.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
       end
@@ -431,7 +431,7 @@ module Mastodon::CLI
     def deduplicate_domain_blocks!
       remove_index_if_exists!(:domain_blocks, 'index_domain_blocks_on_domain')
 
-      say 'Deduplicating domain_allows…'
+      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|
         domain_blocks = DomainBlock.where(id: row['ids'].split(',')).by_severity.reverse.to_a
 
@@ -462,7 +462,7 @@ module Mastodon::CLI
         UnavailableDomain.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
       end
 
-      say 'Restoring domain_allows indexes…'
+      say 'Restoring unavailable_domains indexes…'
       ActiveRecord::Base.connection.add_index :unavailable_domains, ['domain'], name: 'index_unavailable_domains_on_domain', unique: true
     end
 

From afc8550b63434232bb06b196132f2a180516061b Mon Sep 17 00:00:00 2001
From: Michael Stanclift <mx@vmstan.com>
Date: Mon, 11 Dec 2023 01:49:10 -0600
Subject: [PATCH 50/73] Change preview card deletes to be done using batch
 method (#28183)

---
 app/lib/vacuum/preview_cards_vacuum.rb | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/app/lib/vacuum/preview_cards_vacuum.rb b/app/lib/vacuum/preview_cards_vacuum.rb
index 14fdeda1c..9e34c87c3 100644
--- a/app/lib/vacuum/preview_cards_vacuum.rb
+++ b/app/lib/vacuum/preview_cards_vacuum.rb
@@ -14,9 +14,8 @@ class Vacuum::PreviewCardsVacuum
   private
 
   def vacuum_cached_images!
-    preview_cards_past_retention_period.find_each do |preview_card|
-      preview_card.image.destroy
-      preview_card.save
+    preview_cards_past_retention_period.find_in_batches do |preview_card|
+      AttachmentBatch.new(PreviewCard, preview_card).clear
     end
   end
 

From bd88883b6e6c7c614eac3c97fc40783ea7b8c91b Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 11 Dec 2023 07:52:53 +0000
Subject: [PATCH 51/73] Update eslint (non-major) (#28313)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 yarn.lock | 128 +++++++++++++++++++++++++++---------------------------
 1 file changed, 64 insertions(+), 64 deletions(-)

diff --git a/yarn.lock b/yarn.lock
index 45f58eaad..4e1d0bd8c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1726,9 +1726,9 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@eslint/eslintrc@npm:^2.1.3":
-  version: 2.1.3
-  resolution: "@eslint/eslintrc@npm:2.1.3"
+"@eslint/eslintrc@npm:^2.1.4":
+  version: 2.1.4
+  resolution: "@eslint/eslintrc@npm:2.1.4"
   dependencies:
     ajv: "npm:^6.12.4"
     debug: "npm:^4.3.2"
@@ -1739,14 +1739,14 @@ __metadata:
     js-yaml: "npm:^4.1.0"
     minimatch: "npm:^3.1.2"
     strip-json-comments: "npm:^3.1.1"
-  checksum: f4103f4346126292eb15581c5a1d12bef03410fd3719dedbdb92e1f7031d46a5a2d60de8566790445d5d4b70b75ba050876799a11f5fff8265a91ee3fa77dab0
+  checksum: 32f67052b81768ae876c84569ffd562491ec5a5091b0c1e1ca1e0f3c24fb42f804952fdd0a137873bc64303ba368a71ba079a6f691cee25beee9722d94cc8573
   languageName: node
   linkType: hard
 
-"@eslint/js@npm:8.54.0":
-  version: 8.54.0
-  resolution: "@eslint/js@npm:8.54.0"
-  checksum: d61fb4a0be6af2d8cb290121c329697664a75d6255a29926d5454fb02aeb02b87112f67fdf218d10abac42f90c570ac366126751baefc5405d0e017ed0c946c5
+"@eslint/js@npm:8.55.0":
+  version: 8.55.0
+  resolution: "@eslint/js@npm:8.55.0"
+  checksum: 88ab9fc57a651becd2b32ec40a3958db27fae133b1ae77bebd733aa5bbd00a92f325bb02f20ad680d31c731fa49b22f060a4777dd52eb3e27da013d940bd978d
   languageName: node
   linkType: hard
 
@@ -3682,14 +3682,14 @@ __metadata:
   linkType: hard
 
 "@typescript-eslint/eslint-plugin@npm:^6.0.0":
-  version: 6.11.0
-  resolution: "@typescript-eslint/eslint-plugin@npm:6.11.0"
+  version: 6.13.2
+  resolution: "@typescript-eslint/eslint-plugin@npm:6.13.2"
   dependencies:
     "@eslint-community/regexpp": "npm:^4.5.1"
-    "@typescript-eslint/scope-manager": "npm:6.11.0"
-    "@typescript-eslint/type-utils": "npm:6.11.0"
-    "@typescript-eslint/utils": "npm:6.11.0"
-    "@typescript-eslint/visitor-keys": "npm:6.11.0"
+    "@typescript-eslint/scope-manager": "npm:6.13.2"
+    "@typescript-eslint/type-utils": "npm:6.13.2"
+    "@typescript-eslint/utils": "npm:6.13.2"
+    "@typescript-eslint/visitor-keys": "npm:6.13.2"
     debug: "npm:^4.3.4"
     graphemer: "npm:^1.4.0"
     ignore: "npm:^5.2.4"
@@ -3702,44 +3702,44 @@ __metadata:
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 6645aa09b9d51c5e3ea781eaf74da75b94f83f3e2d7b3dd988d5ce7eb82dd87e3509471cf2ee8c6b2428d907df5f1b02f29dbd04f54c2653f9566c8c4ce98009
+  checksum: 531a4406d872738d165c6a66cb26e976523c94053b022a8210dc9fd10e91b79b705bc0fcc77145e9744e4108b53bdba55e02a10dc17757b22be92aff57849384
   languageName: node
   linkType: hard
 
 "@typescript-eslint/parser@npm:^6.0.0":
-  version: 6.11.0
-  resolution: "@typescript-eslint/parser@npm:6.11.0"
+  version: 6.13.2
+  resolution: "@typescript-eslint/parser@npm:6.13.2"
   dependencies:
-    "@typescript-eslint/scope-manager": "npm:6.11.0"
-    "@typescript-eslint/types": "npm:6.11.0"
-    "@typescript-eslint/typescript-estree": "npm:6.11.0"
-    "@typescript-eslint/visitor-keys": "npm:6.11.0"
+    "@typescript-eslint/scope-manager": "npm:6.13.2"
+    "@typescript-eslint/types": "npm:6.13.2"
+    "@typescript-eslint/typescript-estree": "npm:6.13.2"
+    "@typescript-eslint/visitor-keys": "npm:6.13.2"
     debug: "npm:^4.3.4"
   peerDependencies:
     eslint: ^7.0.0 || ^8.0.0
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: e7caeb20069102e21f468fc0dbe7ff6fb6b1efa9e72f4c9f39d4a865ed0633f39130b593ef9ae8f394ca1d70563e15410faf30a482a97809951eaac6ed3a67da
+  checksum: 2c62b8cd8a37eb2ea59cd00e559f51a9f57af746e2040e872af3c58ddd3f4071ad7b7009789bdeb0e0d4ee0343bfe96ee77288020f3ae22d08e1674203f5e156
   languageName: node
   linkType: hard
 
-"@typescript-eslint/scope-manager@npm:6.11.0":
-  version: 6.11.0
-  resolution: "@typescript-eslint/scope-manager@npm:6.11.0"
+"@typescript-eslint/scope-manager@npm:6.13.2":
+  version: 6.13.2
+  resolution: "@typescript-eslint/scope-manager@npm:6.13.2"
   dependencies:
-    "@typescript-eslint/types": "npm:6.11.0"
-    "@typescript-eslint/visitor-keys": "npm:6.11.0"
-  checksum: d8999e2d1a4cbde8a79df5e3ec416f0e3db9532d39f2f4bb5a0ebdf954ae75c183d3277579ba05268fe2c88e88ef87f0fa12f02bb8d95d9e67d92e411241f3a3
+    "@typescript-eslint/types": "npm:6.13.2"
+    "@typescript-eslint/visitor-keys": "npm:6.13.2"
+  checksum: 9b159e5bb10dfb5953e71488200b4126378fc7e987ce7d90946aea9ec40cd66c7ada92399657c5d9794189b764ca6f4eb38a8dcb9e4c5aa50ab6000a39636b9c
   languageName: node
   linkType: hard
 
-"@typescript-eslint/type-utils@npm:6.11.0":
-  version: 6.11.0
-  resolution: "@typescript-eslint/type-utils@npm:6.11.0"
+"@typescript-eslint/type-utils@npm:6.13.2":
+  version: 6.13.2
+  resolution: "@typescript-eslint/type-utils@npm:6.13.2"
   dependencies:
-    "@typescript-eslint/typescript-estree": "npm:6.11.0"
-    "@typescript-eslint/utils": "npm:6.11.0"
+    "@typescript-eslint/typescript-estree": "npm:6.13.2"
+    "@typescript-eslint/utils": "npm:6.13.2"
     debug: "npm:^4.3.4"
     ts-api-utils: "npm:^1.0.1"
   peerDependencies:
@@ -3747,23 +3747,23 @@ __metadata:
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: ff68f2e052b8d688f1dc1a0050746704c8e0ab6263b47f1f52da73a7d251678e4950af23a95e1cd8e3fcea2457e6e5294ddbe01d29dafa2fdfb5b11ed9452a3f
+  checksum: 1ca97c78abdf479aea0c54e869fda2ae2f69de1974cc063062ce7b5b16c7fdf497ea15c50a29dd5941ea1b6b77e8f1213a5c272a747e334ac69ede083f327468
   languageName: node
   linkType: hard
 
-"@typescript-eslint/types@npm:6.11.0":
-  version: 6.11.0
-  resolution: "@typescript-eslint/types@npm:6.11.0"
-  checksum: 23182813db39a5e9b9bcc1e85306c953f7b8b22d3885e41fcac0bd725c170fbcb70f4ce55633678cc5921dcf062fa0e55635eb39480c118a4411a00354820223
+"@typescript-eslint/types@npm:6.13.2":
+  version: 6.13.2
+  resolution: "@typescript-eslint/types@npm:6.13.2"
+  checksum: 029918ca5b1442bb4bc435773504ce32191e2c3e2fde8d4176bb6513f03e3dfa2aa9724b2d22b1640656d666b97f7a7ebfeaf67b881d5e07250828fa83e3ebe8
   languageName: node
   linkType: hard
 
-"@typescript-eslint/typescript-estree@npm:6.11.0":
-  version: 6.11.0
-  resolution: "@typescript-eslint/typescript-estree@npm:6.11.0"
+"@typescript-eslint/typescript-estree@npm:6.13.2":
+  version: 6.13.2
+  resolution: "@typescript-eslint/typescript-estree@npm:6.13.2"
   dependencies:
-    "@typescript-eslint/types": "npm:6.11.0"
-    "@typescript-eslint/visitor-keys": "npm:6.11.0"
+    "@typescript-eslint/types": "npm:6.13.2"
+    "@typescript-eslint/visitor-keys": "npm:6.13.2"
     debug: "npm:^4.3.4"
     globby: "npm:^11.1.0"
     is-glob: "npm:^4.0.3"
@@ -3772,34 +3772,34 @@ __metadata:
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 3e183e554e1bc74f065da3015f7137eb40c262f989c547701b1e3f4f20134e574e56b749288cd00d77b9d1ddb705546613c2457661ffc63b6060ffa97ba3aac8
+  checksum: 1c4c59dce0c51fdfee34d9f418e64fe28e3ec1a97661efc8a3d2780bdff36aff38de9090d356a968f394fa6d4e9c058936ce9cd260d4c44a52761ecd74915bce
   languageName: node
   linkType: hard
 
-"@typescript-eslint/utils@npm:6.11.0, @typescript-eslint/utils@npm:^6.5.0":
-  version: 6.11.0
-  resolution: "@typescript-eslint/utils@npm:6.11.0"
+"@typescript-eslint/utils@npm:6.13.2, @typescript-eslint/utils@npm:^6.5.0":
+  version: 6.13.2
+  resolution: "@typescript-eslint/utils@npm:6.13.2"
   dependencies:
     "@eslint-community/eslint-utils": "npm:^4.4.0"
     "@types/json-schema": "npm:^7.0.12"
     "@types/semver": "npm:^7.5.0"
-    "@typescript-eslint/scope-manager": "npm:6.11.0"
-    "@typescript-eslint/types": "npm:6.11.0"
-    "@typescript-eslint/typescript-estree": "npm:6.11.0"
+    "@typescript-eslint/scope-manager": "npm:6.13.2"
+    "@typescript-eslint/types": "npm:6.13.2"
+    "@typescript-eslint/typescript-estree": "npm:6.13.2"
     semver: "npm:^7.5.4"
   peerDependencies:
     eslint: ^7.0.0 || ^8.0.0
-  checksum: c91eb4578607959acc2b43ddc791571682e45601a19b25d5d120786ed4af607656f83c5c1fa71972e549ddfb5542acf2f7d443ae93b32ee28192c22c106b8883
+  checksum: 84969be91e7949868eaaa289288c9d71927f0e427b572501b0991d8d62b40a4234f7287c35b35d276ccbb53e9ea5457b8250fcf4941e60e6b9ba4065fbfba416
   languageName: node
   linkType: hard
 
-"@typescript-eslint/visitor-keys@npm:6.11.0":
-  version: 6.11.0
-  resolution: "@typescript-eslint/visitor-keys@npm:6.11.0"
+"@typescript-eslint/visitor-keys@npm:6.13.2":
+  version: 6.13.2
+  resolution: "@typescript-eslint/visitor-keys@npm:6.13.2"
   dependencies:
-    "@typescript-eslint/types": "npm:6.11.0"
+    "@typescript-eslint/types": "npm:6.13.2"
     eslint-visitor-keys: "npm:^3.4.1"
-  checksum: 5f48329422b7f286196661d39e93e9defd7c5cf80e6c84c8d03459853f5d9f86a5e91c5e80ea572dcdb907ebbe503bbcc77aeb8b468c294b2aa7b3ccfc81cb88
+  checksum: c173bc1fcc42c3075a5ee094e7f3bf0279d98315c25ff49e20d02d79022b1d0402accfa113b070afb4d52a6f6d180594b67baa8b6a784eabdf82b54dd1ff454c
   languageName: node
   linkType: hard
 
@@ -7313,13 +7313,13 @@ __metadata:
   linkType: hard
 
 "eslint-config-prettier@npm:^9.0.0":
-  version: 9.0.0
-  resolution: "eslint-config-prettier@npm:9.0.0"
+  version: 9.1.0
+  resolution: "eslint-config-prettier@npm:9.1.0"
   peerDependencies:
     eslint: ">=7.0.0"
   bin:
     eslint-config-prettier: bin/cli.js
-  checksum: bc1f661915845c631824178942e5d02f858fe6d0ea796f0050d63e0f681927b92696e81139dd04714c08c3e7de580fd079c66162e40070155ba79eaee78ab5d0
+  checksum: 6d332694b36bc9ac6fdb18d3ca2f6ac42afa2ad61f0493e89226950a7091e38981b66bac2b47ba39d15b73fff2cd32c78b850a9cf9eed9ca9a96bfb2f3a2f10d
   languageName: node
   linkType: hard
 
@@ -7555,13 +7555,13 @@ __metadata:
   linkType: hard
 
 "eslint@npm:^8.41.0":
-  version: 8.54.0
-  resolution: "eslint@npm:8.54.0"
+  version: 8.55.0
+  resolution: "eslint@npm:8.55.0"
   dependencies:
     "@eslint-community/eslint-utils": "npm:^4.2.0"
     "@eslint-community/regexpp": "npm:^4.6.1"
-    "@eslint/eslintrc": "npm:^2.1.3"
-    "@eslint/js": "npm:8.54.0"
+    "@eslint/eslintrc": "npm:^2.1.4"
+    "@eslint/js": "npm:8.55.0"
     "@humanwhocodes/config-array": "npm:^0.11.13"
     "@humanwhocodes/module-importer": "npm:^1.0.1"
     "@nodelib/fs.walk": "npm:^1.2.8"
@@ -7598,7 +7598,7 @@ __metadata:
     text-table: "npm:^0.2.0"
   bin:
     eslint: bin/eslint.js
-  checksum: 4f205f832bdbd0218cde374b067791f4f76d7abe8de86b2dc849c273899051126d912ebf71531ee49b8eeaa22cad77febdc8f2876698dc2a76e84a8cb976af22
+  checksum: d28c0b60f19bb7d355cb8393e77b018c8f548dba3f820b799c89bb2e0c436ee26084e700c5e57e1e97e7972ec93065277849141b82e7b0c0d02c2dc1e553a2a1
   languageName: node
   linkType: hard
 

From d0e7999a904d6d5eb290f2378ce36809bfecb502 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 11 Dec 2023 07:53:13 +0000
Subject: [PATCH 52/73] Update dependency typescript to v5.3.3 (#28312)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 yarn.lock | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/yarn.lock b/yarn.lock
index 4e1d0bd8c..34e22f052 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -16495,22 +16495,22 @@ __metadata:
   linkType: hard
 
 "typescript@npm:5, typescript@npm:^5.0.4":
-  version: 5.3.2
-  resolution: "typescript@npm:5.3.2"
+  version: 5.3.3
+  resolution: "typescript@npm:5.3.3"
   bin:
     tsc: bin/tsc
     tsserver: bin/tsserver
-  checksum: d7dbe1fbe19039e36a65468ea64b5d338c976550394ba576b7af9c68ed40c0bc5d12ecce390e4b94b287a09a71bd3229f19c2d5680611f35b7c53a3898791159
+  checksum: e33cef99d82573624fc0f854a2980322714986bc35b9cb4d1ce736ed182aeab78e2cb32b385efa493b2a976ef52c53e20d6c6918312353a91850e2b76f1ea44f
   languageName: node
   linkType: hard
 
 "typescript@patch:typescript@npm%3A5#optional!builtin<compat/typescript>, typescript@patch:typescript@npm%3A^5.0.4#optional!builtin<compat/typescript>":
-  version: 5.3.2
-  resolution: "typescript@patch:typescript@npm%3A5.3.2#optional!builtin<compat/typescript>::version=5.3.2&hash=e012d7"
+  version: 5.3.3
+  resolution: "typescript@patch:typescript@npm%3A5.3.3#optional!builtin<compat/typescript>::version=5.3.3&hash=e012d7"
   bin:
     tsc: bin/tsc
     tsserver: bin/tsserver
-  checksum: 73c8bad74e732d93211c9d77f28b03307e2f5fc6a0afc73f4b783261ab567686a16d6ae958bdaef383a00be1b0b8c8b6741dd6ca3d13af4963fa7e47456d49c7
+  checksum: 1d0a5f4ce496c42caa9a30e659c467c5686eae15d54b027ee7866744952547f1be1262f2d40de911618c242b510029d51d43ff605dba8fb740ec85ca2d3f9500
   languageName: node
   linkType: hard
 

From 7ddd937330103cc9030ee375c3e62904b0b7508f Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 11 Dec 2023 08:53:32 +0100
Subject: [PATCH 53/73] Update dependency prettier to v3.1.1 (#28311)

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 34e22f052..7a1524204 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -13312,11 +13312,11 @@ __metadata:
   linkType: hard
 
 "prettier@npm:^3.0.0":
-  version: 3.1.0
-  resolution: "prettier@npm:3.1.0"
+  version: 3.1.1
+  resolution: "prettier@npm:3.1.1"
   bin:
     prettier: bin/prettier.cjs
-  checksum: a45ea70aa97fde162ea4c4aba3dfc7859aa6a732a1db34458d9535dc3c2c16d3bc3fb5689e6cd76aa835562555303b02d9449fd2e15af3b73c8053557e25c5b6
+  checksum: facc944ba20e194ff4db765e830ffbcb642803381f0d2033ed397e79904fa4ccc877dc25ad68f42d36985c01d051c990ca1b905fb83d2d7d65fe69e4386fa1a3
   languageName: node
   linkType: hard
 

From a117155728319e36f3a5d2256de83b0985a40e0c Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 11 Dec 2023 07:54:01 +0000
Subject: [PATCH 54/73] Update DefinitelyTyped types (non-major) (#28310)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 yarn.lock | 24 ++++++++++++------------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/yarn.lock b/yarn.lock
index 7a1524204..f246cf412 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3027,11 +3027,11 @@ __metadata:
   linkType: hard
 
 "@types/emoji-mart@npm:^3.0.9":
-  version: 3.0.13
-  resolution: "@types/emoji-mart@npm:3.0.13"
+  version: 3.0.14
+  resolution: "@types/emoji-mart@npm:3.0.14"
   dependencies:
     "@types/react": "npm:*"
-  checksum: 840f920c3242e1d274f0102e67cb2d00434e1fd370e0bcc8983b43b6b62322b01e3ddcd5fb078c60883e613530a7c70b8c40060624897543cd4da9441ca81486
+  checksum: 23ded65fce9b3355fbe903d3971cb67cc827a5d587464bb7e3f349615527ef4a9197b3bb59fa84c4391d1b901e7f200f686a7fc83f649ae2a51a0fb948cbadfb
   languageName: node
   linkType: hard
 
@@ -3175,12 +3175,12 @@ __metadata:
   linkType: hard
 
 "@types/jest@npm:^29.5.2":
-  version: 29.5.10
-  resolution: "@types/jest@npm:29.5.10"
+  version: 29.5.11
+  resolution: "@types/jest@npm:29.5.11"
   dependencies:
     expect: "npm:^29.0.0"
     pretty-format: "npm:^29.0.0"
-  checksum: b46171d59d12a5f69bbe710f65eaf59a8073337c6b4a67dff8158575caec53f1c61f8a7d645b34d6ac3c4ea398acd30f0c5d1c4a131c0c918798019264a3397d
+  checksum: 524a3394845214581278bf4d75055927261fbeac7e1a89cd621bd0636da37d265fe0a85eac58b5778758faad1cbd7c7c361dfc190c78ebde03a91cce33463261
   languageName: node
   linkType: hard
 
@@ -3374,11 +3374,11 @@ __metadata:
   linkType: hard
 
 "@types/react-helmet@npm:^6.1.6":
-  version: 6.1.9
-  resolution: "@types/react-helmet@npm:6.1.9"
+  version: 6.1.11
+  resolution: "@types/react-helmet@npm:6.1.11"
   dependencies:
     "@types/react": "npm:*"
-  checksum: d1823582903d6e70f1f447c7bec9e844b6f85f5de84cbcde5c8bbeecc064db1394c786ed9b9ded30544afe5c91e57c7e8105171df1643998f64c0aeab9f7f2aa
+  checksum: f7b3bb2151d992a108ae46fed876fb9c8119108397d9a01d150c5642782997542c8b3c52e742b56e8689b7dbfa62ca9cfc76aa7e05dec4e60c652f7ef53fa783
   languageName: node
   linkType: hard
 
@@ -3495,13 +3495,13 @@ __metadata:
   linkType: hard
 
 "@types/react@npm:*, @types/react@npm:16 || 17 || 18, @types/react@npm:>=16.9.11, @types/react@npm:^18.2.7":
-  version: 18.2.41
-  resolution: "@types/react@npm:18.2.41"
+  version: 18.2.43
+  resolution: "@types/react@npm:18.2.43"
   dependencies:
     "@types/prop-types": "npm:*"
     "@types/scheduler": "npm:*"
     csstype: "npm:^3.0.2"
-  checksum: 5cc72491ce8be95e7bbedd8bf039ca971772ecd22d989feb045af7e73247c7e6cff25a2f1c2200be461fb2f6b5aacef739e1ba9fd83c744209dfd3ce8aa75afe
+  checksum: 10477a50fbd3c0cc5b8a2ade679f442717f68fb27c8460b2aa1d3256cd18c48f742bbe5b9ee37a8c4c5f832ffa37b3a23c09fd96dd880a8e3182d8929c05e803
   languageName: node
   linkType: hard
 

From b82fc8a2ca29f03f821add41bea5221ceac9cafd Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 11 Dec 2023 07:54:22 +0000
Subject: [PATCH 55/73] Update dependency ws to v8.15.0 (#28308)

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 f246cf412..0ef881126 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -17738,8 +17738,8 @@ __metadata:
   linkType: hard
 
 "ws@npm:^8.11.0, ws@npm:^8.12.1, ws@npm:^8.14.2":
-  version: 8.14.2
-  resolution: "ws@npm:8.14.2"
+  version: 8.15.0
+  resolution: "ws@npm:8.15.0"
   peerDependencies:
     bufferutil: ^4.0.1
     utf-8-validate: ">=5.0.2"
@@ -17748,7 +17748,7 @@ __metadata:
       optional: true
     utf-8-validate:
       optional: true
-  checksum: 35b4c2da048b8015c797fd14bcb5a5766216ce65c8a5965616a5440ca7b6c3681ee3cbd0ea0c184a59975556e9d58f2002abf8485a14d11d3371770811050a16
+  checksum: b778a405b2589ffbf549323e2f404f1f72e372a049d332d2f0b1f33057e9fbb14a05aa474cb156e4584b418cd95edf4297c0ca5263d6519e8009064bf8e0b80d
   languageName: node
   linkType: hard
 

From 11d2bd97165bf68439a8d2293c48b1fcefc8930c Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 11 Dec 2023 02:55:07 -0500
Subject: [PATCH 56/73] Fix intermittent failure from unspecified order in
 `api/v1/accounts/relationships` spec (#28306)

---
 .../api/v1/accounts/relationships_spec.rb     | 42 +++++++++++--------
 1 file changed, 24 insertions(+), 18 deletions(-)

diff --git a/spec/requests/api/v1/accounts/relationships_spec.rb b/spec/requests/api/v1/accounts/relationships_spec.rb
index cea45168a..b06ce0509 100644
--- a/spec/requests/api/v1/accounts/relationships_spec.rb
+++ b/spec/requests/api/v1/accounts/relationships_spec.rb
@@ -31,8 +31,8 @@ describe 'GET /api/v1/accounts/relationships' do
         .to have_http_status(200)
       expect(body_as_json)
         .to be_an(Enumerable)
-        .and have_attributes(
-          first: include(
+        .and contain_exactly(
+          include(
             following: true,
             followed_by: false
           )
@@ -53,9 +53,11 @@ describe 'GET /api/v1/accounts/relationships' do
           expect(body_as_json)
             .to be_an(Enumerable)
             .and have_attributes(
-              size: 2,
-              first: include(simon_item),
-              second: include(lewis_item)
+              size: 2
+            )
+            .and contain_exactly(
+              include(simon_item),
+              include(lewis_item)
             )
         end
       end
@@ -71,10 +73,12 @@ describe 'GET /api/v1/accounts/relationships' do
           expect(body_as_json)
             .to be_an(Enumerable)
             .and have_attributes(
-              size: 3,
-              first: include(simon_item),
-              second: include(lewis_item),
-              third: include(bob_item)
+              size: 3
+            )
+            .and contain_exactly(
+              include(simon_item),
+              include(lewis_item),
+              include(bob_item)
             )
         end
       end
@@ -88,9 +92,11 @@ describe 'GET /api/v1/accounts/relationships' do
           expect(body_as_json)
             .to be_an(Enumerable)
             .and have_attributes(
-              size: 2,
-              first: include(simon_item),
-              second: include(lewis_item)
+              size: 2
+            )
+            .and contain_exactly(
+              include(simon_item),
+              include(lewis_item)
             )
         end
       end
@@ -116,7 +122,6 @@ describe 'GET /api/v1/accounts/relationships' do
           muting: false,
           requested: false,
           domain_blocking: false,
-
         }
       end
 
@@ -129,7 +134,6 @@ describe 'GET /api/v1/accounts/relationships' do
           muting: false,
           requested: false,
           domain_blocking: false,
-
         }
       end
     end
@@ -149,8 +153,10 @@ describe 'GET /api/v1/accounts/relationships' do
       expect(body_as_json)
         .to be_an(Enumerable)
         .and have_attributes(
-          size: 1,
-          first: include(
+          size: 1
+        )
+        .and contain_exactly(
+          include(
             following: true,
             showing_reblogs: true
           )
@@ -168,8 +174,8 @@ describe 'GET /api/v1/accounts/relationships' do
 
       expect(body_as_json)
         .to be_an(Enumerable)
-        .and have_attributes(
-          first: include(
+        .and contain_exactly(
+          include(
             following: false,
             showing_reblogs: false
           )

From 78347d25567b1b8cf2cb9e1fd4cab002fd6aff59 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 11 Dec 2023 02:55:45 -0500
Subject: [PATCH 57/73] Controller spec to request spec:
 `api/v1/accounts/familiar_followers` (#28305)

---
 .../v1/accounts/familiar_followers_spec.rb}   | 20 ++++++++-----------
 1 file changed, 8 insertions(+), 12 deletions(-)
 rename spec/{controllers/api/v1/accounts/familiar_followers_controller_spec.rb => requests/api/v1/accounts/familiar_followers_spec.rb} (53%)

diff --git a/spec/controllers/api/v1/accounts/familiar_followers_controller_spec.rb b/spec/requests/api/v1/accounts/familiar_followers_spec.rb
similarity index 53%
rename from spec/controllers/api/v1/accounts/familiar_followers_controller_spec.rb
rename to spec/requests/api/v1/accounts/familiar_followers_spec.rb
index 3c7c7e8b8..fdc0a3a93 100644
--- a/spec/controllers/api/v1/accounts/familiar_followers_controller_spec.rb
+++ b/spec/requests/api/v1/accounts/familiar_followers_spec.rb
@@ -2,20 +2,16 @@
 
 require 'rails_helper'
 
-describe Api::V1::Accounts::FamiliarFollowersController do
-  render_views
-
-  let(:user)    { Fabricate(:user) }
-  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:follows') }
+describe 'Accounts Familiar Followers API' do
+  let(:user)     { Fabricate(:user) }
+  let(:token)    { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+  let(:scopes)   { 'read:follows' }
+  let(:headers)  { { 'Authorization' => "Bearer #{token.token}" } }
   let(:account) { Fabricate(:account) }
 
-  before do
-    allow(controller).to receive(:doorkeeper_token) { token }
-  end
-
-  describe 'GET #index' do
+  describe 'GET /api/v1/accounts/familiar_followers' do
     it 'returns http success' do
-      get :index, params: { account_id: account.id, limit: 2 }
+      get '/api/v1/accounts/familiar_followers', params: { account_id: account.id, limit: 2 }, headers: headers
 
       expect(response).to have_http_status(200)
     end
@@ -26,7 +22,7 @@ describe Api::V1::Accounts::FamiliarFollowersController do
 
       it 'removes duplicate account IDs from params' do
         account_ids = [account_a, account_b, account_b, account_a, account_a].map { |a| a.id.to_s }
-        get :index, params: { id: account_ids }
+        get '/api/v1/accounts/familiar_followers', params: { id: account_ids }, headers: headers
 
         expect(body_as_json.pluck(:id)).to contain_exactly(account_a.id.to_s, account_b.id.to_s)
       end

From 8f94502e7d092584c7bd723ae3e866c0aae2c95b Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 11 Dec 2023 02:56:13 -0500
Subject: [PATCH 58/73] Controller spec to request spec:
 `api/v1/accounts/identify_proofs` (#28304)

---
 .../identity_proofs_controller_spec.rb        | 23 -------------------
 .../api/v1/accounts/identity_proofs_spec.rb   | 19 +++++++++++++++
 2 files changed, 19 insertions(+), 23 deletions(-)
 delete mode 100644 spec/controllers/api/v1/accounts/identity_proofs_controller_spec.rb
 create mode 100644 spec/requests/api/v1/accounts/identity_proofs_spec.rb

diff --git a/spec/controllers/api/v1/accounts/identity_proofs_controller_spec.rb b/spec/controllers/api/v1/accounts/identity_proofs_controller_spec.rb
deleted file mode 100644
index 6351de761..000000000
--- a/spec/controllers/api/v1/accounts/identity_proofs_controller_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe Api::V1::Accounts::IdentityProofsController do
-  render_views
-
-  let(:user)    { Fabricate(:user) }
-  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
-  let(:account) { Fabricate(:account) }
-
-  before do
-    allow(controller).to receive(:doorkeeper_token) { token }
-  end
-
-  describe 'GET #index' do
-    it 'returns http success' do
-      get :index, params: { account_id: account.id, limit: 2 }
-
-      expect(response).to have_http_status(200)
-    end
-  end
-end
diff --git a/spec/requests/api/v1/accounts/identity_proofs_spec.rb b/spec/requests/api/v1/accounts/identity_proofs_spec.rb
new file mode 100644
index 000000000..3727af7e8
--- /dev/null
+++ b/spec/requests/api/v1/accounts/identity_proofs_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'Accounts Identity Proofs API' do
+  let(:user)     { Fabricate(:user) }
+  let(:token)    { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+  let(:scopes)   { 'read:accounts' }
+  let(:headers)  { { 'Authorization' => "Bearer #{token.token}" } }
+  let(:account) { Fabricate(:account) }
+
+  describe 'GET /api/v1/accounts/identity_proofs' do
+    it 'returns http success' do
+      get "/api/v1/accounts/#{account.id}/identity_proofs", params: { limit: 2 }, headers: headers
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end

From a968898dc7d7a9c6ddea850a40d4f2fc9e7b446f Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 11 Dec 2023 02:56:47 -0500
Subject: [PATCH 59/73] Controller spec to request spec:
 `api/v1/accounts/lists` (#28303)

---
 .../api/v1/accounts/lists_controller_spec.rb  | 25 -------------------
 spec/requests/api/v1/accounts/lists_spec.rb   | 25 +++++++++++++++++++
 2 files changed, 25 insertions(+), 25 deletions(-)
 delete mode 100644 spec/controllers/api/v1/accounts/lists_controller_spec.rb
 create mode 100644 spec/requests/api/v1/accounts/lists_spec.rb

diff --git a/spec/controllers/api/v1/accounts/lists_controller_spec.rb b/spec/controllers/api/v1/accounts/lists_controller_spec.rb
deleted file mode 100644
index 418839cfa..000000000
--- a/spec/controllers/api/v1/accounts/lists_controller_spec.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe Api::V1::Accounts::ListsController do
-  render_views
-
-  let(:user)    { Fabricate(:user) }
-  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:lists') }
-  let(:account) { Fabricate(:account) }
-  let(:list)    { Fabricate(:list, account: user.account) }
-
-  before do
-    allow(controller).to receive(:doorkeeper_token) { token }
-    user.account.follow!(account)
-    list.accounts << account
-  end
-
-  describe 'GET #index' do
-    it 'returns http success' do
-      get :index, params: { account_id: account.id }
-      expect(response).to have_http_status(200)
-    end
-  end
-end
diff --git a/spec/requests/api/v1/accounts/lists_spec.rb b/spec/requests/api/v1/accounts/lists_spec.rb
new file mode 100644
index 000000000..48c0337e5
--- /dev/null
+++ b/spec/requests/api/v1/accounts/lists_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'Accounts Lists API' do
+  let(:user)     { Fabricate(:user) }
+  let(:token)    { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+  let(:scopes)   { 'read:lists' }
+  let(:headers)  { { 'Authorization' => "Bearer #{token.token}" } }
+  let(:account) { Fabricate(:account) }
+  let(:list)    { Fabricate(:list, account: user.account) }
+
+  before do
+    user.account.follow!(account)
+    list.accounts << account
+  end
+
+  describe 'GET /api/v1/accounts/lists' do
+    it 'returns http success' do
+      get "/api/v1/accounts/#{account.id}/lists", headers: headers
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end

From e544b6df42a5ed8c2365f0a95d7e3e94c53c61f9 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 11 Dec 2023 02:57:33 -0500
Subject: [PATCH 60/73] Controller spec to request spec:
 `api/v1/accounts/lookup` (#28302)

---
 .../api/v1/accounts/lookup_controller_spec.rb | 23 -------------------
 spec/requests/api/v1/accounts/lookup_spec.rb  | 19 +++++++++++++++
 2 files changed, 19 insertions(+), 23 deletions(-)
 delete mode 100644 spec/controllers/api/v1/accounts/lookup_controller_spec.rb
 create mode 100644 spec/requests/api/v1/accounts/lookup_spec.rb

diff --git a/spec/controllers/api/v1/accounts/lookup_controller_spec.rb b/spec/controllers/api/v1/accounts/lookup_controller_spec.rb
deleted file mode 100644
index 37407766f..000000000
--- a/spec/controllers/api/v1/accounts/lookup_controller_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe Api::V1::Accounts::LookupController do
-  render_views
-
-  let(:user)    { Fabricate(:user) }
-  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
-  let(:account) { Fabricate(:account) }
-
-  before do
-    allow(controller).to receive(:doorkeeper_token) { token }
-  end
-
-  describe 'GET #show' do
-    it 'returns http success' do
-      get :show, params: { account_id: account.id, acct: account.acct }
-
-      expect(response).to have_http_status(200)
-    end
-  end
-end
diff --git a/spec/requests/api/v1/accounts/lookup_spec.rb b/spec/requests/api/v1/accounts/lookup_spec.rb
new file mode 100644
index 000000000..4c022c7c1
--- /dev/null
+++ b/spec/requests/api/v1/accounts/lookup_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'Accounts Lookup API' do
+  let(:user)     { Fabricate(:user) }
+  let(:token)    { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+  let(:scopes)   { 'read:accounts' }
+  let(:headers)  { { 'Authorization' => "Bearer #{token.token}" } }
+  let(:account) { Fabricate(:account) }
+
+  describe 'GET /api/v1/accounts/lookup' do
+    it 'returns http success' do
+      get '/api/v1/accounts/lookup', params: { account_id: account.id, acct: account.acct }, headers: headers
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end

From 94cc707ab398339dfe74472300056876fac81856 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 11 Dec 2023 02:58:48 -0500
Subject: [PATCH 61/73] Controller spec to request spec:
 `api/v1/accounts/notes` (#28301)

---
 .../api/v1/accounts/notes_spec.rb}             | 18 +++++++-----------
 1 file changed, 7 insertions(+), 11 deletions(-)
 rename spec/{controllers/api/v1/accounts/notes_controller_spec.rb => requests/api/v1/accounts/notes_spec.rb} (66%)

diff --git a/spec/controllers/api/v1/accounts/notes_controller_spec.rb b/spec/requests/api/v1/accounts/notes_spec.rb
similarity index 66%
rename from spec/controllers/api/v1/accounts/notes_controller_spec.rb
rename to spec/requests/api/v1/accounts/notes_spec.rb
index 75599b32b..4f3ac68c7 100644
--- a/spec/controllers/api/v1/accounts/notes_controller_spec.rb
+++ b/spec/requests/api/v1/accounts/notes_spec.rb
@@ -2,21 +2,17 @@
 
 require 'rails_helper'
 
-describe Api::V1::Accounts::NotesController do
-  render_views
-
-  let(:user)    { Fabricate(:user) }
-  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:accounts') }
+describe 'Accounts Notes API' do
+  let(:user)     { Fabricate(:user) }
+  let(:token)    { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+  let(:scopes)   { 'write:accounts' }
+  let(:headers)  { { 'Authorization' => "Bearer #{token.token}" } }
   let(:account) { Fabricate(:account) }
   let(:comment) { 'foo' }
 
-  before do
-    allow(controller).to receive(:doorkeeper_token) { token }
-  end
-
-  describe 'POST #create' do
+  describe 'POST /api/v1/accounts/:account_id/note' do
     subject do
-      post :create, params: { account_id: account.id, comment: comment }
+      post "/api/v1/accounts/#{account.id}/note", params: { comment: comment }, headers: headers
     end
 
     context 'when account note has reasonable length', :aggregate_failures do

From 809506bdd40871ae8c56fa755d8d61bc279095a0 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 11 Dec 2023 02:59:40 -0500
Subject: [PATCH 62/73] Controller spec to request spec: `api/v1/accounts/pins`
 (#28300)

---
 .../api/v1/accounts/pins_controller_spec.rb   | 40 ------------------
 spec/requests/api/v1/accounts/pins_spec.rb    | 41 +++++++++++++++++++
 2 files changed, 41 insertions(+), 40 deletions(-)
 delete mode 100644 spec/controllers/api/v1/accounts/pins_controller_spec.rb
 create mode 100644 spec/requests/api/v1/accounts/pins_spec.rb

diff --git a/spec/controllers/api/v1/accounts/pins_controller_spec.rb b/spec/controllers/api/v1/accounts/pins_controller_spec.rb
deleted file mode 100644
index 36f525e75..000000000
--- a/spec/controllers/api/v1/accounts/pins_controller_spec.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Api::V1::Accounts::PinsController do
-  let(:john)  { Fabricate(:user) }
-  let(:kevin) { Fabricate(:user) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: john.id, scopes: 'write:accounts') }
-
-  before do
-    kevin.account.followers << john.account
-    allow(controller).to receive(:doorkeeper_token) { token }
-  end
-
-  describe 'POST #create' do
-    subject { post :create, params: { account_id: kevin.account.id } }
-
-    it 'creates account_pin', :aggregate_failures do
-      expect do
-        subject
-      end.to change { AccountPin.where(account: john.account, target_account: kevin.account).count }.by(1)
-      expect(response).to have_http_status(200)
-    end
-  end
-
-  describe 'DELETE #destroy' do
-    subject { delete :destroy, params: { account_id: kevin.account.id } }
-
-    before do
-      Fabricate(:account_pin, account: john.account, target_account: kevin.account)
-    end
-
-    it 'destroys account_pin', :aggregate_failures do
-      expect do
-        subject
-      end.to change { AccountPin.where(account: john.account, target_account: kevin.account).count }.by(-1)
-      expect(response).to have_http_status(200)
-    end
-  end
-end
diff --git a/spec/requests/api/v1/accounts/pins_spec.rb b/spec/requests/api/v1/accounts/pins_spec.rb
new file mode 100644
index 000000000..c293715f7
--- /dev/null
+++ b/spec/requests/api/v1/accounts/pins_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'Accounts Pins API' do
+  let(:user)     { Fabricate(:user) }
+  let(:token)    { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+  let(:scopes)   { 'write:accounts' }
+  let(:headers)  { { 'Authorization' => "Bearer #{token.token}" } }
+  let(:kevin) { Fabricate(:user) }
+
+  before do
+    kevin.account.followers << user.account
+  end
+
+  describe 'POST /api/v1/accounts/:account_id/pin' do
+    subject { post "/api/v1/accounts/#{kevin.account.id}/pin", headers: headers }
+
+    it 'creates account_pin', :aggregate_failures do
+      expect do
+        subject
+      end.to change { AccountPin.where(account: user.account, target_account: kevin.account).count }.by(1)
+      expect(response).to have_http_status(200)
+    end
+  end
+
+  describe 'POST /api/v1/accounts/:account_id/unpin' do
+    subject { post "/api/v1/accounts/#{kevin.account.id}/unpin", headers: headers }
+
+    before do
+      Fabricate(:account_pin, account: user.account, target_account: kevin.account)
+    end
+
+    it 'destroys account_pin', :aggregate_failures do
+      expect do
+        subject
+      end.to change { AccountPin.where(account: user.account, target_account: kevin.account).count }.by(-1)
+      expect(response).to have_http_status(200)
+    end
+  end
+end

From 16ede59d0a01759a32e3abb6a352a3c397bd982c Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 11 Dec 2023 03:00:41 -0500
Subject: [PATCH 63/73] Controller spec to request spec:
 `api/v1/featured_tags/suggestions` (#28298)

---
 .../suggestions_controller_spec.rb            | 23 -------------------
 .../api/v1/featured_tags/suggestions_spec.rb  | 19 +++++++++++++++
 2 files changed, 19 insertions(+), 23 deletions(-)
 delete mode 100644 spec/controllers/api/v1/featured_tags/suggestions_controller_spec.rb
 create mode 100644 spec/requests/api/v1/featured_tags/suggestions_spec.rb

diff --git a/spec/controllers/api/v1/featured_tags/suggestions_controller_spec.rb b/spec/controllers/api/v1/featured_tags/suggestions_controller_spec.rb
deleted file mode 100644
index 54c63dcc6..000000000
--- a/spec/controllers/api/v1/featured_tags/suggestions_controller_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe Api::V1::FeaturedTags::SuggestionsController do
-  render_views
-
-  let(:user)    { Fabricate(:user) }
-  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
-  let(:account) { Fabricate(:account) }
-
-  before do
-    allow(controller).to receive(:doorkeeper_token) { token }
-  end
-
-  describe 'GET #index' do
-    it 'returns http success' do
-      get :index, params: { account_id: account.id, limit: 2 }
-
-      expect(response).to have_http_status(200)
-    end
-  end
-end
diff --git a/spec/requests/api/v1/featured_tags/suggestions_spec.rb b/spec/requests/api/v1/featured_tags/suggestions_spec.rb
new file mode 100644
index 000000000..f7b453b74
--- /dev/null
+++ b/spec/requests/api/v1/featured_tags/suggestions_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'Featured Tags Suggestions API' do
+  let(:user)    { Fabricate(:user) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+  let(:scopes)  { 'read:accounts' }
+  let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
+  let(:account) { Fabricate(:account) }
+
+  describe 'GET /api/v1/featured_tags/suggestions' do
+    it 'returns http success' do
+      get '/api/v1/featured_tags/suggestions', params: { account_id: account.id, limit: 2 }, headers: headers
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end

From f5d6143aa1eba03391841d6df8ed80e09a4b5e05 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 11 Dec 2023 09:01:17 +0100
Subject: [PATCH 64/73] Update dependency addressable to v2.8.6 (#28296)

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 7a7fdb01c..738bc7b8b 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -118,7 +118,7 @@ GEM
       minitest (>= 5.1)
       mutex_m
       tzinfo (~> 2.0)
-    addressable (2.8.5)
+    addressable (2.8.6)
       public_suffix (>= 2.0.2, < 6.0)
     aes_key_wrap (1.1.0)
     android_key_attestation (0.3.0)

From 253393f3a8281dcf5850aabd917da727518f49c4 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 11 Dec 2023 03:04:45 -0500
Subject: [PATCH 65/73] Only attempt to remove indexes that exist in
 `CLI::Maintenance` script (#28286)

---
 lib/mastodon/cli/maintenance.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/mastodon/cli/maintenance.rb b/lib/mastodon/cli/maintenance.rb
index d0eff7da6..553ca056d 100644
--- a/lib/mastodon/cli/maintenance.rb
+++ b/lib/mastodon/cli/maintenance.rb
@@ -712,7 +712,7 @@ module Mastodon::CLI
     end
 
     def remove_index_if_exists!(table, name)
-      ActiveRecord::Base.connection.remove_index(table, name: name)
+      ActiveRecord::Base.connection.remove_index(table, name: name) if ActiveRecord::Base.connection.index_name_exists?(table, name)
     rescue ArgumentError, ActiveRecord::StatementInvalid
       nil
     end

From 0c640925009a8dbfb8a2be9e4b45d3e40f727a52 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 11 Dec 2023 03:13:28 -0500
Subject: [PATCH 66/73] Controller spec to request spec:
 `api/v1/accounts/search` (#28299)

---
 .../api/v1/accounts/search_controller_spec.rb | 22 -------------------
 spec/requests/api/v1/accounts/search_spec.rb  | 18 +++++++++++++++
 2 files changed, 18 insertions(+), 22 deletions(-)
 delete mode 100644 spec/controllers/api/v1/accounts/search_controller_spec.rb
 create mode 100644 spec/requests/api/v1/accounts/search_spec.rb

diff --git a/spec/controllers/api/v1/accounts/search_controller_spec.rb b/spec/controllers/api/v1/accounts/search_controller_spec.rb
deleted file mode 100644
index aa9455a4a..000000000
--- a/spec/controllers/api/v1/accounts/search_controller_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Api::V1::Accounts::SearchController do
-  render_views
-
-  let(:user)  { Fabricate(:user) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
-
-  before do
-    allow(controller).to receive(:doorkeeper_token) { token }
-  end
-
-  describe 'GET #show' do
-    it 'returns http success' do
-      get :show, params: { q: 'query' }
-
-      expect(response).to have_http_status(200)
-    end
-  end
-end
diff --git a/spec/requests/api/v1/accounts/search_spec.rb b/spec/requests/api/v1/accounts/search_spec.rb
new file mode 100644
index 000000000..76b32e7b2
--- /dev/null
+++ b/spec/requests/api/v1/accounts/search_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'Accounts Search API' do
+  let(:user)     { Fabricate(:user) }
+  let(:token)    { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+  let(:scopes)   { 'read:accounts' }
+  let(:headers)  { { 'Authorization' => "Bearer #{token.token}" } }
+
+  describe 'GET /api/v1/accounts/search' do
+    it 'returns http success' do
+      get '/api/v1/accounts/search', params: { q: 'query' }, headers: headers
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end

From 0e4233de9d8a0912b0d837e32043c325dfc2e0ff Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 11 Dec 2023 03:13:57 -0500
Subject: [PATCH 67/73] Controller spec to request spec: `api/v2/suggestions`
 (#28297)

---
 .../api/v2/suggestions_controller_spec.rb     | 22 -------------------
 spec/requests/api/v2/suggestions_spec.rb      | 18 +++++++++++++++
 2 files changed, 18 insertions(+), 22 deletions(-)
 delete mode 100644 spec/controllers/api/v2/suggestions_controller_spec.rb
 create mode 100644 spec/requests/api/v2/suggestions_spec.rb

diff --git a/spec/controllers/api/v2/suggestions_controller_spec.rb b/spec/controllers/api/v2/suggestions_controller_spec.rb
deleted file mode 100644
index 5e6508bfd..000000000
--- a/spec/controllers/api/v2/suggestions_controller_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe Api::V2::SuggestionsController do
-  render_views
-
-  let(:user)  { Fabricate(:user) }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
-
-  before do
-    allow(controller).to receive(:doorkeeper_token) { token }
-  end
-
-  describe 'GET #index' do
-    it 'returns http success' do
-      get :index
-
-      expect(response).to have_http_status(200)
-    end
-  end
-end
diff --git a/spec/requests/api/v2/suggestions_spec.rb b/spec/requests/api/v2/suggestions_spec.rb
new file mode 100644
index 000000000..5f1c97b8a
--- /dev/null
+++ b/spec/requests/api/v2/suggestions_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'Suggestions API' do
+  let(:user)    { Fabricate(:user) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+  let(:scopes)  { 'read' }
+  let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
+
+  describe 'GET /api/v2/suggestions' do
+    it 'returns http success' do
+      get '/api/v2/suggestions', headers: headers
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end

From 79a81da69c86276fe17143954f06f6e6b58356d4 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Mon, 11 Dec 2023 09:22:52 +0100
Subject: [PATCH 68/73] New Crowdin Translations (automated) (#28291)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/en-GB.json |  7 +++
 app/javascript/mastodon/locales/eo.json    | 50 ++++++++++++++++++++
 app/javascript/mastodon/locales/ko.json    |  2 +-
 app/javascript/mastodon/locales/la.json    | 16 +++----
 app/javascript/mastodon/locales/ne.json    | 54 +++++++++++++++++++++-
 app/javascript/mastodon/locales/sk.json    |  1 +
 config/locales/activerecord.hr.yml         | 15 ++++++
 config/locales/activerecord.ru.yml         |  2 +-
 config/locales/en-GB.yml                   | 16 +++++++
 config/locales/sk.yml                      |  4 ++
 10 files changed, 156 insertions(+), 11 deletions(-)

diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json
index 7745311be..1a67fecb6 100644
--- a/app/javascript/mastodon/locales/en-GB.json
+++ b/app/javascript/mastodon/locales/en-GB.json
@@ -21,6 +21,7 @@
   "account.blocked": "Blocked",
   "account.browse_more_on_origin_server": "Browse more on the original profile",
   "account.cancel_follow_request": "Cancel follow",
+  "account.copy": "Copy link to profile",
   "account.direct": "Privately mention @{name}",
   "account.disable_notifications": "Stop notifying me when @{name} posts",
   "account.domain_blocked": "Domain blocked",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Mark as read",
   "conversation.open": "View conversation",
   "conversation.with": "With {names}",
+  "copy_icon_button.copied": "Copied to clipboard",
   "copypaste.copied": "Copied",
   "copypaste.copy_to_clipboard": "Copy to clipboard",
   "directory.federated": "From known fediverse",
@@ -222,6 +224,7 @@
   "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Symbols",
   "emoji_button.travel": "Travel & Places",
+  "empty_column.account_hides_collections": "This user has chosen to not make this information available",
   "empty_column.account_suspended": "Account suspended",
   "empty_column.account_timeline": "No posts here!",
   "empty_column.account_unavailable": "Profile unavailable",
@@ -478,6 +481,8 @@
   "onboarding.follows.empty": "Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.",
   "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": "Personalize your home feed",
+  "onboarding.profile.discoverable": "Make my profile discoverable",
+  "onboarding.profile.discoverable_hint": "When you opt in to discoverability on Mastodon, your posts may appear in search results and trending, and your profile may be suggested to people with similar interests to you.",
   "onboarding.profile.display_name": "Display name",
   "onboarding.profile.display_name_hint": "Your full name or your fun name…",
   "onboarding.profile.lead": "You can always complete this later in the settings, where even more customisation options are available.",
@@ -530,6 +535,7 @@
   "privacy.unlisted.short": "Unlisted",
   "privacy_policy.last_updated": "Last updated {date}",
   "privacy_policy.title": "Privacy Policy",
+  "recommended": "Recommended",
   "refresh": "Refresh",
   "regeneration_indicator.label": "Loading…",
   "regeneration_indicator.sublabel": "Your home feed is being prepared!",
@@ -600,6 +606,7 @@
   "search.quick_action.status_search": "Posts matching {x}",
   "search.search_or_paste": "Search or paste URL",
   "search_popout.full_text_search_disabled_message": "Unavailable on {domain}.",
+  "search_popout.full_text_search_logged_out_message": "Only available when logged in.",
   "search_popout.language_code": "ISO language code",
   "search_popout.options": "Search options",
   "search_popout.quick_actions": "Quick actions",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index 537b8d0af..2678d83a5 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -21,6 +21,7 @@
   "account.blocked": "Blokita",
   "account.browse_more_on_origin_server": "Foliumi pli ĉe la originala profilo",
   "account.cancel_follow_request": "Nuligi peton por sekvado",
+  "account.copy": "Kopii ligilon al profilo",
   "account.direct": "Private mencii @{name}",
   "account.disable_notifications": "Ne plu sciigi min, kiam @{name} mesaĝas",
   "account.domain_blocked": "Domajno blokita",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Marki legita",
   "conversation.open": "Vidi konversacion",
   "conversation.with": "Kun {names}",
+  "copy_icon_button.copied": "Kopiis al kliptabulo",
   "copypaste.copied": "Kopiita",
   "copypaste.copy_to_clipboard": "Kopii al dosierujo",
   "directory.federated": "El konata fediverso",
@@ -202,7 +204,9 @@
   "dismissable_banner.community_timeline": "Jen la plej novaj publikaj afiŝoj de uzantoj, kies kontojn gastigas {domain}.",
   "dismissable_banner.dismiss": "Eksigi",
   "dismissable_banner.explore_links": "Tiuj novaĵoj estas aktuale priparolataj de uzantoj en tiu ĉi kaj aliaj serviloj, sur la malcentrigita reto.",
+  "dismissable_banner.explore_statuses": "Ĉi tioj estas afiŝoj de socia reto kiu populariĝas hodiau.",
   "dismissable_banner.explore_tags": "Ĉi tiuj kradvostoj populariĝas en ĉi tiu kaj aliaj serviloj en la malcentraliza reto nun.",
+  "dismissable_banner.public_timeline": "Ĉi tioj estas plej lastaj publikaj afiŝoj de personoj ĉe socia reto kiu personoj ĉe {domain} sekvas.",
   "embed.instructions": "Enkorpigu ĉi tiun afiŝon en vian retejon per kopio de la suba kodo.",
   "embed.preview": "Ĝi aperos tiel:",
   "emoji_button.activity": "Agadoj",
@@ -220,6 +224,7 @@
   "emoji_button.search_results": "Serĉaj rezultoj",
   "emoji_button.symbols": "Simboloj",
   "emoji_button.travel": "Vojaĝoj kaj lokoj",
+  "empty_column.account_hides_collections": "Ĉi tiu uzanto elektis ne disponebligi ĉi tiu informon",
   "empty_column.account_suspended": "Konto suspendita",
   "empty_column.account_timeline": "Neniu afiŝo ĉi tie!",
   "empty_column.account_unavailable": "Profilo ne disponebla",
@@ -229,6 +234,8 @@
   "empty_column.direct": "Vi ankoraŭ ne havas privatan mencion. Kiam vi sendos aŭ ricevos iun, tiu aperos ĉi tie.",
   "empty_column.domain_blocks": "Ankoraŭ neniu domajno estas blokita.",
   "empty_column.explore_statuses": "Nenio tendencas nun. Rekontrolu poste!",
+  "empty_column.favourited_statuses": "Vi ankoraŭ ne havas stelumitan afiŝon.",
+  "empty_column.favourites": "Ankoraŭ neniu stelumis tiun afiŝon.",
   "empty_column.follow_requests": "Vi ne ankoraŭ havas iun peton de sekvado. Kiam vi ricevos unu, ĝi aperos ĉi tie.",
   "empty_column.followed_tags": "Vi ankoraŭ ne sekvas iujn kradvortojn. Kiam vi faras, ili aperos ĉi tie.",
   "empty_column.hashtag": "Ankoraŭ estas nenio per ĉi tiu kradvorto.",
@@ -292,19 +299,36 @@
   "hashtag.column_settings.tag_mode.any": "Iu ajn",
   "hashtag.column_settings.tag_mode.none": "Neniu",
   "hashtag.column_settings.tag_toggle": "Aldoni pliajn etikedojn por ĉi tiu kolumno",
+  "hashtag.counter_by_accounts": "{count, plural,one {{counter} partoprenanto} other {{counter} partoprenantoj}}",
+  "hashtag.counter_by_uses": "{count, plural,one {{counter} afiŝo} other {{counter} afiŝoj}}",
+  "hashtag.counter_by_uses_today": "{count, plural,one {{counter} afiŝo} other {{counter} afiŝoj}} hodiau",
   "hashtag.follow": "Sekvi la kradvorton",
   "hashtag.unfollow": "Ne plu sekvi la kradvorton",
+  "hashtags.and_other": "…kaj {count, plural,other {# pli}}",
+  "home.actions.go_to_explore": "Vidi kio populariĝas",
   "home.actions.go_to_suggestions": "Trovi homojn por sekvi",
   "home.column_settings.basic": "Bazaj agordoj",
   "home.column_settings.show_reblogs": "Montri diskonigojn",
   "home.column_settings.show_replies": "Montri respondojn",
+  "home.explore_prompt.body": "Via hejmafiŝaro havos miksitajn afiŝojn de kradvortoj kiujn vi elektis sekvi, personoj kiujn vi elektis sekvi, kaj afiŝoj kiujn ili suprenigis.",
+  "home.explore_prompt.title": "Ĉi tio estas via hejma paĝo en Mastodon.",
   "home.hide_announcements": "Kaŝi la anoncojn",
+  "home.pending_critical_update.body": "Ĝisdatigu vian servilon de Mastodon kiel eble plej baldau!",
+  "home.pending_critical_update.link": "Vidi ĝisdatigojn",
+  "home.pending_critical_update.title": "Kritika sekurĝisdatigo estas disponebla!",
   "home.show_announcements": "Montri anoncojn",
+  "interaction_modal.description.favourite": "Per konto ĉe Mastodon, vi povas stelumiti ĉi tiun afiŝon por sciigi la afiŝanton ke vi aprezigas ŝin kaj konservas por la estonteco.",
   "interaction_modal.description.follow": "Kun konto ĉe Mastodon, vi povos sekvi {name} por vidi ties mesaĝojn en via hejmo.",
   "interaction_modal.description.reblog": "Kun konto ĉe Mastodon, vi povas diskonigi ĉi tiun afiŝon, por ke viaj propraj sekvantoj vidu ĝin.",
   "interaction_modal.description.reply": "Kun konto ĉe Mastodon, vi povos respondi al ĉi tiu mesaĝo.",
+  "interaction_modal.login.action": "Prenu min hejmen",
+  "interaction_modal.login.prompt": "Domajno de via hejma servilo, ekz. mastodon.social",
+  "interaction_modal.no_account_yet": "Ĉu ne estas ĉe Mastodon?",
   "interaction_modal.on_another_server": "En alia servilo",
   "interaction_modal.on_this_server": "En ĉi tiu servilo",
+  "interaction_modal.sign_in": "Vi ne estas ensalutita al ĉi tiu servilo.",
+  "interaction_modal.sign_in_hint": "Gvideto: Tio estas la retejo kie vi registris. Vi ankau povas tajpi vian plenan uzantonomon!",
+  "interaction_modal.title.favourite": "Stelumi la afiŝon de {name}",
   "interaction_modal.title.follow": "Sekvi {name}",
   "interaction_modal.title.reblog": "Akceli la afiŝon de {name}",
   "interaction_modal.title.reply": "Respondi al la afiŝo de {name}",
@@ -320,6 +344,8 @@
   "keyboard_shortcuts.direct": "por malfermi la kolumnon pri privataj mencioj",
   "keyboard_shortcuts.down": "iri suben en la listo",
   "keyboard_shortcuts.enter": "malfermi mesaĝon",
+  "keyboard_shortcuts.favourite": "Stelumi afiŝon",
+  "keyboard_shortcuts.favourites": "Malfermi la liston de la stelumoj",
   "keyboard_shortcuts.federated": "Malfermi la frataran templinion",
   "keyboard_shortcuts.heading": "Klavaraj mallongigoj",
   "keyboard_shortcuts.home": "Malfermi la hejman templinion",
@@ -366,6 +392,7 @@
   "lists.search": "Serĉi inter la homoj, kiujn vi sekvas",
   "lists.subheading": "Viaj listoj",
   "load_pending": "{count,plural, one {# nova elemento} other {# novaj elementoj}}",
+  "loading_indicator.label": "Ŝargado…",
   "media_gallery.toggle_visible": "{number, plural, one {Kaŝi la bildon} other {Kaŝi la bildojn}}",
   "moved_to_account_banner.text": "Via konto {disabledAccount} estas malvalidigita ĉar vi movis ĝin al {movedToAccount}.",
   "mute_modal.duration": "Daŭro",
@@ -382,6 +409,7 @@
   "navigation_bar.domain_blocks": "Blokitaj domajnoj",
   "navigation_bar.edit_profile": "Redakti profilon",
   "navigation_bar.explore": "Esplori",
+  "navigation_bar.favourites": "Stelumoj",
   "navigation_bar.filters": "Silentigitaj vortoj",
   "navigation_bar.follow_requests": "Petoj de sekvado",
   "navigation_bar.followed_tags": "Sekvataj kradvortoj",
@@ -389,6 +417,7 @@
   "navigation_bar.lists": "Listoj",
   "navigation_bar.logout": "Adiaŭi",
   "navigation_bar.mutes": "Silentigitaj uzantoj",
+  "navigation_bar.opened_in_classic_interface": "Afiŝoj, kontoj, kaj aliaj specifaj paĝoj kiuj estas malfermititaj defaulta en la klasika reta interfaco.",
   "navigation_bar.personal": "Persone",
   "navigation_bar.pins": "Alpinglitaj mesaĝoj",
   "navigation_bar.preferences": "Preferoj",
@@ -398,6 +427,7 @@
   "not_signed_in_indicator.not_signed_in": "Necesas saluti por aliri tiun rimedon.",
   "notification.admin.report": "{name} raportis {target}",
   "notification.admin.sign_up": "{name} kreis konton",
+  "notification.favourite": "{name} stelumis vian afiŝon",
   "notification.follow": "{name} eksekvis vin",
   "notification.follow_request": "{name} petis sekvi vin",
   "notification.mention": "{name} menciis vin",
@@ -411,6 +441,7 @@
   "notifications.column_settings.admin.report": "Novaj raportoj:",
   "notifications.column_settings.admin.sign_up": "Novaj registriĝoj:",
   "notifications.column_settings.alert": "Sciigoj de la retumilo",
+  "notifications.column_settings.favourite": "Stelumoj:",
   "notifications.column_settings.filter_bar.advanced": "Montri ĉiujn kategoriojn",
   "notifications.column_settings.filter_bar.category": "Rapida filtra breto",
   "notifications.column_settings.filter_bar.show_bar": "Montri la breton de filtrilo",
@@ -428,6 +459,7 @@
   "notifications.column_settings.update": "Redaktoj:",
   "notifications.filter.all": "Ĉiuj",
   "notifications.filter.boosts": "Diskonigoj",
+  "notifications.filter.favourites": "Stelumoj",
   "notifications.filter.follows": "Sekvoj",
   "notifications.filter.mentions": "Mencioj",
   "notifications.filter.polls": "Balotenketaj rezultoj",
@@ -441,14 +473,29 @@
   "notifications_permission_banner.enable": "Ŝalti retumilajn sciigojn",
   "notifications_permission_banner.how_to_control": "Por ricevi sciigojn kiam Mastodon ne estas malfermita, ebligu labortablajn sciigojn. Vi povas regi precize kiuj specoj de interagoj generas labortablajn sciigojn per la supra butono {icon} post kiam ili estas ebligitaj.",
   "notifications_permission_banner.title": "Neniam preterlasas iun ajn",
+  "onboarding.action.back": "Prenu min reen",
+  "onboarding.actions.back": "Prenu min reen",
   "onboarding.actions.go_to_explore": "See what's trending",
   "onboarding.actions.go_to_home": "Go to your home feed",
   "onboarding.compose.template": "Saluton #Mastodon!",
   "onboarding.follows.empty": "Bedaŭrinde, neniu rezulto estas montrebla nuntempe. Vi povas provi serĉi aŭ foliumi la esploran paĝon por trovi kontojn por sekvi, aŭ retrovi baldaŭ.",
   "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.discoverable": "Trovebligi mian profilon",
+  "onboarding.profile.discoverable_hint": "Kiam vi aliĝi al trovebleco ĉe Mastodon, viaj afiŝoj eble aperos en serĉaj rezultoj kaj populariĝoj, kaj via profilo eble estas sugestota al personoj kun similaj intereseoj al vi.",
+  "onboarding.profile.display_name": "Publika nomo",
+  "onboarding.profile.display_name_hint": "Via plena nomo aŭ via kromnomo…",
+  "onboarding.profile.lead": "Vi ĉiam povas plenigi ĉi tion poste en la agordoj, kie eĉ pli da personecigagordoj estas disponeblaj.",
+  "onboarding.profile.note": "Sinprezento",
+  "onboarding.profile.note_hint": "Vi povas @mencii aliajn homojn aŭ #kradvortojn…",
   "onboarding.profile.save_and_continue": "Konservi kaj daŭrigi",
+  "onboarding.profile.title": "Profila fikso",
+  "onboarding.profile.upload_avatar": "Alŝuti profilbildon",
+  "onboarding.profile.upload_header": "Alŝuti profilkapbildon",
+  "onboarding.share.lead": "Sciigi personojn pri kiel ili povas trovi vin ĉe Mastodon!",
   "onboarding.share.message": "Mi estas {username} en #Mastodon! Sekvu min ĉe {url}",
+  "onboarding.share.next_steps": "Eblaj malantauaj paŝoj:",
+  "onboarding.share.title": "Disvastigi vian profilon",
   "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?",
   "onboarding.start.title": "Vi atingas ĝin!",
@@ -460,6 +507,9 @@
   "onboarding.steps.setup_profile.title": "Customize your profile",
   "onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
   "onboarding.steps.share_profile.title": "Share your profile",
+  "onboarding.tips.2fa": "<strong>Ĉu vi scias?</strong> Vi povas sekurigi vian konton per efektivigi dufaktora autentigo en via kontoagordoj.",
+  "onboarding.tips.accounts_from_other_servers": "<strong>Ĉu vi scias?</strong> Ĉar Mastodon estas sencentra, kelkaj profiloj kiujn vi trovi estas gastigitaj ĉe aliaj serviloj kiuj ne estas via.",
+  "onboarding.tips.migration": "<strong>Ĉu vi scias?</strong> Se vi sentas ke {domain} ne estas bona servilelekto por vi en la estonteco, vi povas translokiĝi al alia servilo de Mastodon sen malgajni viajn sekvantojn.",
   "password_confirmation.mismatching": "Pasvorto konfirmo ne kongruas",
   "picture_in_picture.restore": "Remetu ĝin",
   "poll.closed": "Finita",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 5b76cd67c..4606916c1 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -239,7 +239,7 @@
   "empty_column.follow_requests": "아직 팔로우 요청이 없습니다. 요청을 받았을 때 여기에 나타납니다.",
   "empty_column.followed_tags": "아직 아무 해시태그도 팔로우하고 있지 않습니다. 해시태그를 팔로우하면, 여기에 표시됩니다.",
   "empty_column.hashtag": "이 해시태그는 아직 사용되지 않았습니다.",
-  "empty_column.home": "당신의 홈 타임라인은 비어있습니다! 더 많은 사람들을 팔로우 하여 채워보세요. {suggestions}",
+  "empty_column.home": "당신의 홈 타임라인은 비어있습니다! 더 많은 사람을 팔로우하여 채워보세요. {suggestions}",
   "empty_column.list": "리스트에 아직 아무것도 없습니다. 리스트의 누군가가 게시물을 올리면 여기에 나타납니다.",
   "empty_column.lists": "아직 리스트가 없습니다. 리스트를 만들면 여기에 나타납니다.",
   "empty_column.mutes": "아직 아무도 뮤트하지 않았습니다.",
diff --git a/app/javascript/mastodon/locales/la.json b/app/javascript/mastodon/locales/la.json
index e4bd9365a..3e5747ba8 100644
--- a/app/javascript/mastodon/locales/la.json
+++ b/app/javascript/mastodon/locales/la.json
@@ -1,6 +1,6 @@
 {
   "about.contact": "Ratio:",
-  "about.domain_blocks.no_reason_available": "ratio abdere est",
+  "about.domain_blocks.no_reason_available": "Ratio abdere est",
   "account.account_note_header": "Annotatio",
   "account.badges.bot": "Robotum",
   "account.badges.group": "Congregatio",
@@ -49,7 +49,7 @@
   "dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralized network right now.",
   "dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.",
   "embed.instructions": "Embed this status on your website by copying the code below.",
-  "emoji_button.food": "cibus et potus",
+  "emoji_button.food": "Cibus et potus",
   "emoji_button.people": "Homines",
   "emoji_button.search": "Quaerere...",
   "empty_column.account_timeline": "Hic nulla contributa!",
@@ -57,13 +57,13 @@
   "empty_column.home": "Your home timeline is empty! Follow more people to fill it up. {suggestions}",
   "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
   "explore.trending_statuses": "Contributa",
-  "generic.saved": "servavit",
+  "generic.saved": "Servavit",
   "hashtag.column_settings.tag_toggle": "Include additional tags in this column",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.blocked": "to open blocked users list",
-  "keyboard_shortcuts.boost": "to boost",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.back": "Re navigare",
+  "keyboard_shortcuts.blocked": "Aperire listam usorum obstructorum",
+  "keyboard_shortcuts.boost": "Inlustrare publicatio",
+  "keyboard_shortcuts.column": "Columnam dirigere",
+  "keyboard_shortcuts.compose": "TextArea Compositi Attendere",
   "keyboard_shortcuts.description": "Descriptio",
   "keyboard_shortcuts.direct": "to open direct messages column",
   "keyboard_shortcuts.down": "to move down in the list",
diff --git a/app/javascript/mastodon/locales/ne.json b/app/javascript/mastodon/locales/ne.json
index 0967ef424..95f8e703c 100644
--- a/app/javascript/mastodon/locales/ne.json
+++ b/app/javascript/mastodon/locales/ne.json
@@ -1 +1,53 @@
-{}
+{
+  "about.contact": "सम्पर्क:",
+  "about.disclaimer": "Mastodon नि:शुल्क, खुला स्रोत सफ्टवेयर, र Mastodon gGmbH को ट्रेडमार्क हो।",
+  "about.domain_blocks.no_reason_available": "कारण उपलब्ध छैन",
+  "about.domain_blocks.preamble": "Mastodon ले तपाइँलाई सामान्यतया फेडिभर्समा कुनै पनि अन्य सर्भरका सामग्री हेर्न र प्रयोगकर्ताहरूसँग अन्तरक्रिया गर्न दिन्छ। यी अपवादहरू हुन् जुन यस विशेष सर्भरमा बनाइएका छन्।",
+  "about.domain_blocks.silenced.title": "सीमित",
+  "about.domain_blocks.suspended.explanation": "यस सर्भरबाट कुनै पनि डेटा प्रशोधन, भण्डारण वा आदानप्रदान गरिने छैन, जसले यस सर्भरका प्रयोगकर्ताहरूसँग कुनै पनि अन्तरक्रिया वा सञ्चारलाई असम्भव बनाउँछ।",
+  "about.domain_blocks.suspended.title": "निलम्बित",
+  "about.not_available": "यो जानकारी यस सर्भरमा उपलब्ध गराइएको छैन।",
+  "about.powered_by": "{mastodon} द्वारा संचालित विकेन्द्रीकृत सामाजिक मिडिया",
+  "about.rules": "सर्भर नियमहरू",
+  "account.add_or_remove_from_list": "सूचीबाट थप्नुहोस् वा हटाउनुहोस्",
+  "account.badges.group": "समूह",
+  "account.block": "@{name} लाई ब्लक गर्नुहोस्",
+  "account.block_domain": "{domain} डोमेनलाई ब्लक गर्नुहोस्",
+  "account.block_short": "ब्लक",
+  "account.blocked": "ब्लक गरिएको",
+  "account.browse_more_on_origin_server": "मूल प्रोफाइलमा थप ब्राउज गर्नुहोस्",
+  "account.cancel_follow_request": "फलो अनुरोध रद्द गर्नुहोस",
+  "account.copy": "प्रोफाइलको लिङ्क प्रतिलिपि गर्नुहोस्",
+  "account.direct": "@{name} लाई निजी रूपमा उल्लेख गर्नुहोस्",
+  "account.disable_notifications": "@{name} ले पोस्ट गर्दा मलाई सूचित नगर्नुहोस्",
+  "account.domain_blocked": "डोमेन ब्लक गरिएको छ",
+  "account.edit_profile": "प्रोफाइल सम्पादन गर्नुहोस्",
+  "account.enable_notifications": "@{name} ले पोस्ट गर्दा मलाई सूचित गर्नुहोस्",
+  "account.endorse": "प्रोफाइलमा फिचर गर्नुहोस्",
+  "account.featured_tags.last_status_never": "कुनै पोस्ट छैन",
+  "account.follow": "फलो गर्नुहोस",
+  "account.followers.empty": "यस प्रयोगकर्तालाई अहिलेसम्म कसैले फलो गर्दैन।",
+  "account.follows.empty": "यो प्रयोगकर्ताले अहिलेसम्म कसैलाई फलो गरेको छैन।",
+  "account.go_to_profile": "प्रोफाइलमा जानुहोस्",
+  "account.hide_reblogs": "@{name} को बूस्टहरू लुकाउनुहोस्",
+  "account.link_verified_on": "यस लिङ्कको स्वामित्व {date} मा जाँच गरिएको थियो",
+  "account.media": "मिडिया",
+  "account.mention": "@{name} लाई उल्लेख गर्नुहोस्",
+  "account.no_bio": "कुनै विवरण प्रदान गरिएको छैन।",
+  "account.posts": "पोस्टहरू",
+  "account.requested": "स्वीकृतिको पर्खाइमा। फलो अनुरोध रद्द गर्न क्लिक गर्नुहोस्",
+  "account.requested_follow": "{name} ले तपाईंलाई फलो गर्न अनुरोध गर्नुभएको छ",
+  "account.share": "@{name} को प्रोफाइल सेयर गर्नुहोस्",
+  "account.show_reblogs": "@{name} को बूस्टहरू देखाउनुहोस्",
+  "account.statuses_counter": "{count, plural, one {{counter} पोस्ट} other {{counter} पोस्टहरू}}",
+  "account.unblock": "@{name} लाई अनब्लक गर्नुहोस्",
+  "account.unblock_domain": "{domain} डोमेनलाई अनब्लक गर्नुहोस्",
+  "account.unblock_short": "अनब्लक गर्नुहोस्",
+  "account.unendorse": "प्रोफाइलमा फिचर नगर्नुहोस्",
+  "account.unfollow": "अनफलो गर्नुहोस्",
+  "account_note.placeholder": "नोट लेख्न क्लिक गर्नुहोस्",
+  "admin.dashboard.retention.average": "औसत",
+  "admin.dashboard.retention.cohort_size": "नयाँ प्रयोगकर्ताहरू",
+  "alert.rate_limited.message": "कृपया {retry_time, time, medium} पछि पुन: प्रयास गर्नुहोस्।",
+  "alert.unexpected.message": "एउटा अनपेक्षित त्रुटि भयो।"
+}
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index c4ce6f8cf..46fcc0116 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -224,6 +224,7 @@
   "emoji_button.search_results": "Výsledky hľadania",
   "emoji_button.symbols": "Symboly",
   "emoji_button.travel": "Cestovanie a miesta",
+  "empty_column.account_hides_collections": "Tento užívateľ si zvolil nesprístupniť túto informáciu",
   "empty_column.account_suspended": "Účet bol pozastavený",
   "empty_column.account_timeline": "Nie sú tu žiadne príspevky!",
   "empty_column.account_unavailable": "Profil nedostupný",
diff --git a/config/locales/activerecord.hr.yml b/config/locales/activerecord.hr.yml
index b095244dd..a3e7d6d49 100644
--- a/config/locales/activerecord.hr.yml
+++ b/config/locales/activerecord.hr.yml
@@ -6,7 +6,9 @@ hr:
         expires_at: Krajnji rok
         options: Opcije
       user:
+        agreement: Ugovor o uslugama
         email: E-mail adresa
+        locale: Lokalitet
         password: Lozinka
       user/account:
         username: Korisničko ime
@@ -18,3 +20,16 @@ hr:
           attributes:
             username:
               invalid: mora sadržavati samo slova, brojeve i _
+              reserved: je rezervisano
+        admin/webhook:
+          attributes:
+            url:
+              invalid: nije validan URL
+        doorkeeper/application:
+          attributes:
+            website:
+              invalid: nije validan URL
+        import:
+          attributes:
+            data:
+              malformed: je neispravan
diff --git a/config/locales/activerecord.ru.yml b/config/locales/activerecord.ru.yml
index 14f9f61f6..92d85af4d 100644
--- a/config/locales/activerecord.ru.yml
+++ b/config/locales/activerecord.ru.yml
@@ -36,7 +36,7 @@ ru:
         status:
           attributes:
             reblog:
-              taken: поста уже существует
+              taken: пост уже существует
         user:
           attributes:
             email:
diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml
index 987788a7a..a6e74c483 100644
--- a/config/locales/en-GB.yml
+++ b/config/locales/en-GB.yml
@@ -534,6 +534,7 @@ en-GB:
       total_reported: Reports about them
       total_storage: Media attachments
       totals_time_period_hint_html: The totals displayed below include data for all time.
+      unknown_instance: There is currently no record of this domain on this server.
     invites:
       deactivate_all: Deactivate all
       filter:
@@ -610,6 +611,7 @@ en-GB:
       created_at: Reported
       delete_and_resolve: Delete posts
       forwarded: Forwarded
+      forwarded_replies_explanation: This report is from a remote user and about remote content. It has been forwarded to you because the reported content is in reply to one of your users.
       forwarded_to: Forwarded to %{domain}
       mark_as_resolved: Mark as resolved
       mark_as_sensitive: Mark as sensitive
@@ -1038,6 +1040,14 @@ en-GB:
       hint_html: Just one more thing! We need to confirm you're a human (this is so we can keep the spam out!). Solve the CAPTCHA below and click "Continue".
       title: Security check
     confirmations:
+      awaiting_review: Your e-mail address is confirmed! The %{domain} staff is now reviewing your registration. You will receive an e-mail if they approve your account!
+      awaiting_review_title: Your registration is being reviewed
+      clicking_this_link: clicking this link
+      login_link: log in
+      proceed_to_login_html: You can now proceed to %{login_link}.
+      redirect_to_app_html: You should have been redirected to the <strong>%{app_name}</strong> app. If that did not happen, try %{clicking_this_link} or manually return to the app.
+      registration_complete: Your registration on %{domain} is now complete!
+      welcome_title: Welcome, %{name}!
       wrong_email_hint: If that e-mail address is not correct, you can change it in account settings.
     delete_account: Delete account
     delete_account_html: If you wish to delete your account, you can <a href="%{path}">proceed here</a>. You will be asked for confirmation.
@@ -1099,6 +1109,7 @@ en-GB:
       functional: Your account is fully operational.
       pending: Your application is pending review by our staff. This may take some time. You will receive an e-mail if your application is approved.
       redirecting_to: Your account is inactive because it is currently redirecting to %{acct}.
+      self_destruct: As %{domain} is closing down, you will only get limited access to your account.
       view_strikes: View past strikes against your account
     too_fast: Form submitted too fast, try again.
     use_security_key: Use security key
@@ -1356,6 +1367,7 @@ en-GB:
       '86400': 1 day
     expires_in_prompt: Never
     generate: Generate invite link
+    invalid: This invite is not valid
     invited_by: 'You were invited by:'
     max_uses:
       one: 1 use
@@ -1568,6 +1580,9 @@ en-GB:
     over_daily_limit: You have exceeded the limit of %{limit} scheduled posts for today
     over_total_limit: You have exceeded the limit of %{limit} scheduled posts
     too_soon: The scheduled date must be in the future
+  self_destruct:
+    lead_html: Unfortunately, <strong>%{domain}</strong> is permanently closing down. If you had an account there, you will not be able to continue using it, but you can still request a backup of your data.
+    title: This server is closing down
   sessions:
     activity: Last activity
     browser: Browser
@@ -1736,6 +1751,7 @@ en-GB:
       default: "%b %d, %Y, %H:%M"
       month: "%b %Y"
       time: "%H:%M"
+      with_time_zone: "%b %d, %Y, %H:%M %Z"
   translation:
     errors:
       quota_exceeded: The server-wide usage quota for the translation service has been exceeded.
diff --git a/config/locales/sk.yml b/config/locales/sk.yml
index 63779e5bd..caf253c69 100644
--- a/config/locales/sk.yml
+++ b/config/locales/sk.yml
@@ -656,6 +656,10 @@ sk:
       rules_check:
         action: Spravuj serverové pravidlá
         message_html: Neurčil/a si žiadne serverové pravidlá.
+      software_version_critical_check:
+        action: Pozri dostupné aktualizácie
+      software_version_patch_check:
+        action: Pozri dostupné aktualizácie
       upload_check_privacy_error:
         action: Pozri tu pre viac informácií
       upload_check_privacy_error_object_storage:

From 4ad2e87c48f8ebdeeb6eab56dc195b9e28f7a5d2 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 11 Dec 2023 09:45:30 +0100
Subject: [PATCH 69/73] Update dependency debug to v1.9.0 (#28315)

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 738bc7b8b..4b39692d0 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -220,9 +220,9 @@ GEM
       database_cleaner-core (~> 2.0.0)
     database_cleaner-core (2.0.1)
     date (3.3.4)
-    debug (1.8.0)
-      irb (>= 1.5.0)
-      reline (>= 0.3.1)
+    debug (1.9.0)
+      irb (~> 1.10)
+      reline (>= 0.3.8)
     debug_inspector (1.1.0)
     devise (4.9.3)
       bcrypt (~> 3.0)

From b5a1013ae36a0ab495aa727a397b9fc9f4340a2c Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 11 Dec 2023 05:23:45 -0500
Subject: [PATCH 70/73] Combine `CLI::...` spec example subjects (#28285)

---
 spec/lib/mastodon/cli/accounts_spec.rb  | 263 ++++++++----------------
 spec/lib/mastodon/cli/ip_blocks_spec.rb |  89 ++++----
 spec/lib/mastodon/cli/settings_spec.rb  |  37 ++--
 3 files changed, 138 insertions(+), 251 deletions(-)

diff --git a/spec/lib/mastodon/cli/accounts_spec.rb b/spec/lib/mastodon/cli/accounts_spec.rb
index 06860c2ff..563f6e877 100644
--- a/spec/lib/mastodon/cli/accounts_spec.rb
+++ b/spec/lib/mastodon/cli/accounts_spec.rb
@@ -34,23 +34,21 @@ describe Mastodon::CLI::Accounts do
     let(:action) { :create }
 
     shared_examples 'a new user with given email address and username' do
-      it 'creates a new user with the specified email address' do
-        subject
-
-        expect(User.find_by(email: options[:email])).to be_present
-      end
-
-      it 'creates a new local account with the specified username' do
-        subject
-
-        expect(Account.find_local('tootctl_username')).to be_present
-      end
-
-      it 'returns "OK" and newly generated password' do
+      it 'creates user and accounts from options and displays success message' do
         allow(SecureRandom).to receive(:hex).and_return('test_password')
 
         expect { subject }
-          .to output_results("OK\nNew password: test_password")
+          .to output_results('OK', 'New password: test_password')
+        expect(user_from_options).to be_present
+        expect(account_from_options).to be_present
+      end
+
+      def user_from_options
+        User.find_by(email: options[:email])
+      end
+
+      def account_from_options
+        Account.find_local('tootctl_username')
       end
     end
 
@@ -199,14 +197,9 @@ describe Mastodon::CLI::Accounts do
       let(:arguments) { [user.account.username] }
 
       context 'when no option is provided' do
-        it 'returns a successful message' do
+        it 'returns a successful message and preserves user' do
           expect { subject }
             .to output_results('OK')
-        end
-
-        it 'does not modify the user' do
-          subject
-
           expect(user).to eq(user.reload)
         end
       end
@@ -400,15 +393,10 @@ describe Mastodon::CLI::Accounts do
       context 'with --dry-run option' do
         let(:options) { { dry_run: true } }
 
-        it 'does not delete the specified user' do
-          subject
-
-          expect(delete_account_service).to_not have_received(:call).with(account, reserve_email: false)
-        end
-
-        it 'outputs a successful message in dry run mode' do
+        it 'outputs a successful message in dry run mode and does not delete the user' do
           expect { subject }
             .to output_results('OK (DRY RUN)')
+          expect(delete_account_service).to_not have_received(:call).with(account, reserve_email: false)
         end
       end
 
@@ -435,15 +423,12 @@ describe Mastodon::CLI::Accounts do
       context 'with --dry-run option' do
         let(:options) { { email: account.user.email, dry_run: true } }
 
-        it 'does not delete the user' do
-          subject
-
-          expect(delete_account_service).to_not have_received(:call).with(account, reserve_email: false)
-        end
-
-        it 'outputs a successful message in dry run mode' do
+        it 'outputs a successful message in dry run mode and does not delete the user' do
           expect { subject }
             .to output_results('OK (DRY RUN)')
+          expect(delete_account_service)
+            .to_not have_received(:call)
+            .with(account, reserve_email: false)
         end
       end
 
@@ -482,20 +467,19 @@ describe Mastodon::CLI::Accounts do
       context 'when the number is positive' do
         let(:options) { { number: 2 } }
 
-        it 'approves the earliest n pending registrations' do
+        it 'approves the earliest n pending registrations but not the remaining ones' do
           subject
 
-          n_earliest_pending_registrations = User.order(created_at: :asc).first(options[:number])
-
           expect(n_earliest_pending_registrations.all?(&:approved?)).to be(true)
+          expect(pending_registrations.all?(&:approved?)).to be(false)
         end
 
-        it 'does not approve the remaining pending registrations' do
-          subject
+        def n_earliest_pending_registrations
+          User.order(created_at: :asc).first(options[:number])
+        end
 
-          pending_registrations = User.order(created_at: :asc).last(total_users - options[:number])
-
-          expect(pending_registrations.all?(&:approved?)).to be(false)
+        def pending_registrations
+          User.order(created_at: :asc).last(total_users - options[:number])
         end
       end
 
@@ -512,15 +496,10 @@ describe Mastodon::CLI::Accounts do
       context 'when the given number is greater than the number of users' do
         let(:options) { { number: total_users * 2 } }
 
-        it 'approves all users' do
-          subject
-
-          expect(User.pluck(:approved).all?(true)).to be(true)
-        end
-
-        it 'does not raise any error' do
+        it 'approves all users and does not raise any error' do
           expect { subject }
             .to_not raise_error
+          expect(User.pluck(:approved).all?(true)).to be(true)
         end
       end
     end
@@ -575,18 +554,13 @@ describe Mastodon::CLI::Accounts do
         stub_parallelize_with_progress!
       end
 
-      it 'makes all local accounts follow the target account' do
-        subject
-
+      it 'displays a successful message and makes all local accounts follow the target account' do
+        expect { subject }
+          .to output_results("OK, followed target from #{Account.local.count} accounts")
         expect(follow_service).to have_received(:call).with(follower_bob, target_account, any_args).once
         expect(follow_service).to have_received(:call).with(follower_rony, target_account, any_args).once
         expect(follow_service).to have_received(:call).with(follower_charles, target_account, any_args).once
       end
-
-      it 'displays a successful message' do
-        expect { subject }
-          .to output_results("OK, followed target from #{Account.local.count} accounts")
-      end
     end
   end
 
@@ -618,18 +592,13 @@ describe Mastodon::CLI::Accounts do
         stub_parallelize_with_progress!
       end
 
-      it 'makes all local accounts unfollow the target account' do
-        subject
-
+      it 'displays a successful message and makes all local accounts unfollow the target account' do
+        expect { subject }
+          .to output_results('OK, unfollowed target from 3 accounts')
         expect(unfollow_service).to have_received(:call).with(follower_chris, target_account).once
         expect(unfollow_service).to have_received(:call).with(follower_rambo, target_account).once
         expect(unfollow_service).to have_received(:call).with(follower_ana, target_account).once
       end
-
-      it 'displays a successful message' do
-        expect { subject }
-          .to output_results('OK, unfollowed target from 3 accounts')
-      end
     end
   end
 
@@ -651,22 +620,17 @@ describe Mastodon::CLI::Accounts do
       let(:user) { account.user }
       let(:arguments) { [account.username] }
 
-      it 'creates a new backup for the specified user' do
-        expect { subject }.to change { user.backups.count }.by(1)
-      end
-
-      it 'creates a backup job' do
-        allow(BackupWorker).to receive(:perform_async)
-
-        subject
-        latest_backup = user.backups.last
+      before { allow(BackupWorker).to receive(:perform_async) }
 
+      it 'creates a new backup and backup job for the specified user and outputs success message' do
+        expect { subject }
+          .to change { user.backups.count }.by(1)
+          .and output_results('OK')
         expect(BackupWorker).to have_received(:perform_async).with(latest_backup.id).once
       end
 
-      it 'displays a successful message' do
-        expect { subject }
-          .to output_results('OK')
+      def latest_backup
+        user.backups.last
       end
     end
   end
@@ -963,15 +927,15 @@ describe Mastodon::CLI::Accounts do
 
         expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_in).with(anything, account.id, anything).once
       end
+    end
 
-      context 'when the given username is not found' do
-        let(:arguments) { ['non_existent_username'] }
+    context 'when the given username is not found' do
+      let(:arguments) { ['non_existent_username'] }
 
-        it 'exits with an error message when the specified username is not found' do
-          expect { subject }
-            .to output_results('No such account')
-            .and raise_error(SystemExit)
-        end
+      it 'exits with an error message when the specified username is not found' do
+        expect { subject }
+          .to output_results('No such account')
+          .and raise_error(SystemExit)
       end
     end
 
@@ -1071,15 +1035,10 @@ describe Mastodon::CLI::Accounts do
           allow(from_account).to receive(:destroy)
         end
 
-        it 'merges "from_account" into "to_account"' do
+        it 'merges `from_account` into `to_account` and deletes `from_account`' do
           subject
 
           expect(to_account).to have_received(:merge_with!).with(from_account).once
-        end
-
-        it 'deletes "from_account"' do
-          subject
-
           expect(from_account).to have_received(:destroy).once
         end
       end
@@ -1099,15 +1058,10 @@ describe Mastodon::CLI::Accounts do
         allow(from_account).to receive(:destroy)
       end
 
-      it 'merges "from_account" into "to_account"' do
+      it 'merges "from_account" into "to_account" and deletes from_account' do
         subject
 
         expect(to_account).to have_received(:merge_with!).with(from_account).once
-      end
-
-      it 'deletes "from_account"' do
-        subject
-
         expect(from_account).to have_received(:destroy)
       end
     end
@@ -1134,28 +1088,23 @@ describe Mastodon::CLI::Accounts do
         stub_request(:head, 'https://example.net/users/tales').to_return(status: 200)
       end
 
-      it 'deletes all inactive remote accounts that longer exist in the origin server' do
-        subject
-
+      def expect_delete_inactive_remote_accounts
         expect(delete_account_service).to have_received(:call).with(bob, reserve_username: false).once
         expect(delete_account_service).to have_received(:call).with(gon, reserve_username: false).once
       end
 
-      it 'does not delete any active remote account that still exists in the origin server' do
-        subject
-
+      def expect_not_delete_active_accounts
         expect(delete_account_service).to_not have_received(:call).with(tom, reserve_username: false)
         expect(delete_account_service).to_not have_received(:call).with(ana, reserve_username: false)
         expect(delete_account_service).to_not have_received(:call).with(tales, reserve_username: false)
       end
 
-      it 'touches inactive remote accounts that have not been deleted' do
-        expect { subject }.to(change { tales.reload.updated_at })
-      end
-
-      it 'displays the summary correctly' do
+      it 'touches inactive remote accounts that have not been deleted and summarizes activity' do
         expect { subject }
-          .to output_results('Visited 5 accounts, removed 2')
+          .to change { tales.reload.updated_at }
+          .and output_results('Visited 5 accounts, removed 2')
+        expect_delete_inactive_remote_accounts
+        expect_not_delete_active_accounts
       end
     end
 
@@ -1168,16 +1117,15 @@ describe Mastodon::CLI::Accounts do
         stub_request(:head, 'https://example.net/users/tales').to_return(status: 404)
       end
 
-      it 'deletes inactive remote accounts that longer exist in the specified domain' do
-        subject
-
+      def expect_delete_inactive_remote_accounts
         expect(delete_account_service).to have_received(:call).with(gon, reserve_username: false).once
         expect(delete_account_service).to have_received(:call).with(tales, reserve_username: false).once
       end
 
-      it 'displays the summary correctly' do
+      it 'displays the summary correctly and deletes inactive remote accounts' do
         expect { subject }
           .to output_results('Visited 2 accounts, removed 2')
+        expect_delete_inactive_remote_accounts
       end
     end
 
@@ -1189,15 +1137,14 @@ describe Mastodon::CLI::Accounts do
           stub_request(:head, 'https://example.net/users/gon').to_return(status: 200)
         end
 
-        it 'skips accounts from the unavailable domain' do
-          subject
-
+        def expect_skip_accounts_from_unavailable_domain
           expect(delete_account_service).to_not have_received(:call).with(tales, reserve_username: false)
         end
 
-        it 'displays the summary correctly' do
+        it 'displays the summary correctly and skip accounts from unavailable domains' do
           expect { subject }
             .to output_results("Visited 5 accounts, removed 0\nThe following domains were not available during the check:\n    example.net")
+          expect_skip_accounts_from_unavailable_domain
         end
       end
 
@@ -1268,25 +1215,14 @@ describe Mastodon::CLI::Accounts do
 
         before do
           accounts.each { |account| target_account.follow!(account) }
-        end
-
-        it 'resets all "following" relationships from the target account' do
-          subject
-
-          expect(target_account.reload.following).to be_empty
-        end
-
-        it 'calls BootstrapTimelineWorker once to rebuild the timeline' do
           allow(BootstrapTimelineWorker).to receive(:perform_async)
-
-          subject
-
-          expect(BootstrapTimelineWorker).to have_received(:perform_async).with(target_account.id).once
         end
 
-        it 'displays a successful message' do
+        it 'resets following relationships and displays a successful message and rebuilds timeline' do
           expect { subject }
             .to output_results("Processed #{total_relationships} relationships")
+          expect(target_account.reload.following).to be_empty
+          expect(BootstrapTimelineWorker).to have_received(:perform_async).with(target_account.id).once
         end
       end
 
@@ -1297,15 +1233,10 @@ describe Mastodon::CLI::Accounts do
           accounts.each { |account| account.follow!(target_account) }
         end
 
-        it 'resets all "followers" relationships from the target account' do
-          subject
-
-          expect(target_account.reload.followers).to be_empty
-        end
-
-        it 'displays a successful message' do
+        it 'resets followers relationships and displays a successful message' do
           expect { subject }
             .to output_results("Processed #{total_relationships} relationships")
+          expect(target_account.reload.followers).to be_empty
         end
       end
 
@@ -1315,31 +1246,15 @@ describe Mastodon::CLI::Accounts do
         before do
           accounts.first(2).each { |account| account.follow!(target_account) }
           accounts.last(1).each  { |account| target_account.follow!(account) }
-        end
-
-        it 'resets all "followers" relationships from the target account' do
-          subject
-
-          expect(target_account.reload.followers).to be_empty
-        end
-
-        it 'resets all "following" relationships from the target account' do
-          subject
-
-          expect(target_account.reload.following).to be_empty
-        end
-
-        it 'calls BootstrapTimelineWorker once to rebuild the timeline' do
           allow(BootstrapTimelineWorker).to receive(:perform_async)
-
-          subject
-
-          expect(BootstrapTimelineWorker).to have_received(:perform_async).with(target_account.id).once
         end
 
-        it 'displays a successful message' do
+        it 'resets followers and following and displays a successful message and rebuilds timeline' do
           expect { subject }
             .to output_results("Processed #{total_relationships} relationships")
+          expect(target_account.reload.followers).to be_empty
+          expect(target_account.reload.following).to be_empty
+          expect(BootstrapTimelineWorker).to have_received(:perform_async).with(target_account.id).once
         end
       end
     end
@@ -1360,57 +1275,51 @@ describe Mastodon::CLI::Accounts do
       stub_parallelize_with_progress!
     end
 
-    it 'prunes all remote accounts with no interactions with local users' do
-      subject
-
+    def expect_prune_remote_accounts_without_interaction
       prunable_account_ids = prunable_accounts.pluck(:id)
 
       expect(Account.where(id: prunable_account_ids).count).to eq(0)
     end
 
-    it 'displays a successful message' do
+    it 'displays a successful message and handles accounts correctly' do
       expect { subject }
         .to output_results("OK, pruned #{prunable_accounts.size} accounts")
+      expect_prune_remote_accounts_without_interaction
+      expect_not_prune_local_accounts
+      expect_not_prune_bot_accounts
+      expect_not_prune_group_accounts
+      expect_not_prune_mentioned_accounts
     end
 
-    it 'does not prune local accounts' do
-      subject
-
+    def expect_not_prune_local_accounts
       expect(Account.exists?(id: local_account.id)).to be(true)
     end
 
-    it 'does not prune bot accounts' do
-      subject
-
+    def expect_not_prune_bot_accounts
       expect(Account.exists?(id: bot_account.id)).to be(true)
     end
 
-    it 'does not prune group accounts' do
-      subject
-
+    def expect_not_prune_group_accounts
       expect(Account.exists?(id: group_account.id)).to be(true)
     end
 
-    it 'does not prune accounts that have been mentioned' do
-      subject
-
+    def expect_not_prune_mentioned_accounts
       expect(Account.exists?(id: mentioned_account.id)).to be true
     end
 
     context 'with --dry-run option' do
       let(:options) { { dry_run: true } }
 
-      it 'does not prune any account' do
-        subject
-
+      def expect_no_account_prunes
         prunable_account_ids = prunable_accounts.pluck(:id)
 
         expect(Account.where(id: prunable_account_ids).count).to eq(prunable_accounts.size)
       end
 
-      it 'displays a successful message with (DRY RUN)' do
+      it 'displays a successful message with (DRY RUN) and doesnt prune anything' do
         expect { subject }
           .to output_results("OK, pruned #{prunable_accounts.size} accounts (DRY RUN)")
+        expect_no_account_prunes
       end
     end
   end
diff --git a/spec/lib/mastodon/cli/ip_blocks_spec.rb b/spec/lib/mastodon/cli/ip_blocks_spec.rb
index dc967a69c..1d6c47268 100644
--- a/spec/lib/mastodon/cli/ip_blocks_spec.rb
+++ b/spec/lib/mastodon/cli/ip_blocks_spec.rb
@@ -33,26 +33,25 @@ describe Mastodon::CLI::IpBlocks do
     let(:arguments) { ip_list }
 
     shared_examples 'ip address blocking' do
-      it 'blocks all specified IP addresses' do
-        subject
-
-        blocked_ip_addresses = IpBlock.where(ip: ip_list).pluck(:ip)
-        expected_ip_addresses = ip_list.map { |ip| IPAddr.new(ip) }
-
-        expect(blocked_ip_addresses).to match_array(expected_ip_addresses)
+      def blocked_ip_addresses
+        IpBlock.where(ip: ip_list).pluck(:ip)
       end
 
-      it 'sets the severity for all blocked IP addresses' do
-        subject
-
-        blocked_ips_severity = IpBlock.where(ip: ip_list).pluck(:severity).all?(options[:severity])
-
-        expect(blocked_ips_severity).to be(true)
+      def expected_ip_addresses
+        ip_list.map { |ip| IPAddr.new(ip) }
       end
 
-      it 'displays a success message with a summary' do
+      def blocked_ips_severity
+        IpBlock.where(ip: ip_list).pluck(:severity).all?(options[:severity])
+      end
+
+      it 'blocks and sets severity for ip address and displays summary' do
         expect { subject }
           .to output_results("Added #{ip_list.size}, skipped 0, failed 0")
+        expect(blocked_ip_addresses)
+          .to match_array(expected_ip_addresses)
+        expect(blocked_ips_severity)
+          .to be(true)
       end
     end
 
@@ -64,17 +63,13 @@ describe Mastodon::CLI::IpBlocks do
       let!(:blocked_ip) { IpBlock.create(ip: ip_list.last, severity: options[:severity]) }
       let(:arguments) { ip_list }
 
-      it 'skips the already blocked IP address' do
-        allow(IpBlock).to receive(:new).and_call_original
+      before { allow(IpBlock).to receive(:new).and_call_original }
 
-        subject
-
-        expect(IpBlock).to_not have_received(:new).with(ip: ip_list.last)
-      end
-
-      it 'displays the correct summary' do
+      it 'skips already block ip and displays the correct summary' do
         expect { subject }
           .to output_results("#{ip_list.last} is already blocked\nAdded #{ip_list.size - 1}, skipped 1, failed 0")
+
+        expect(IpBlock).to_not have_received(:new).with(ip: ip_list.last)
       end
 
       context 'with --force option' do
@@ -179,15 +174,10 @@ describe Mastodon::CLI::IpBlocks do
         ip_list.each { |ip| IpBlock.create(ip: ip, severity: :no_access) }
       end
 
-      it 'removes exact IP blocks' do
-        subject
-
-        expect(IpBlock.where(ip: ip_list)).to_not exist
-      end
-
-      it 'displays success message with a summary' do
+      it 'removes exact ip blocks and displays success message with a summary' do
         expect { subject }
           .to output_results("Removed #{ip_list.size}, skipped 0")
+        expect(IpBlock.where(ip: ip_list)).to_not exist
       end
     end
 
@@ -198,16 +188,19 @@ describe Mastodon::CLI::IpBlocks do
       let(:arguments) { ['192.168.0.5', '10.0.1.50'] }
       let(:options) { { force: true } }
 
-      it 'removes blocks for IP ranges that cover given IP(s)' do
+      it 'removes blocks for IP ranges that cover given IP(s) and keeps other ranges' do
         subject
 
-        expect(IpBlock.where(id: [first_ip_range_block.id, second_ip_range_block.id])).to_not exist
+        expect(covered_ranges).to_not exist
+        expect(other_ranges).to exist
       end
 
-      it 'does not remove other IP ranges' do
-        subject
+      def covered_ranges
+        IpBlock.where(id: [first_ip_range_block.id, second_ip_range_block.id])
+      end
 
-        expect(IpBlock.where(id: third_ip_range_block.id)).to exist
+      def other_ranges
+        IpBlock.where(id: third_ip_range_block.id)
       end
     end
 
@@ -215,14 +208,12 @@ describe Mastodon::CLI::IpBlocks do
       let(:unblocked_ip) { '192.0.2.1' }
       let(:arguments) { [unblocked_ip] }
 
-      it 'skips the IP address' do
+      it 'skips the IP address and displays summary' do
         expect { subject }
-          .to output_results("#{unblocked_ip} is not yet blocked")
-      end
-
-      it 'displays the summary correctly' do
-        expect { subject }
-          .to output_results('Removed 0, skipped 1')
+          .to output_results(
+            "#{unblocked_ip} is not yet blocked",
+            'Removed 0, skipped 1'
+          )
       end
     end
 
@@ -230,14 +221,12 @@ describe Mastodon::CLI::IpBlocks do
       let(:invalid_ip) { '320.15.175.0' }
       let(:arguments) { [invalid_ip] }
 
-      it 'skips the invalid IP address' do
+      it 'skips the invalid IP address and displays summary' do
         expect { subject }
-          .to output_results("#{invalid_ip} is invalid")
-      end
-
-      it 'displays the summary correctly' do
-        expect { subject }
-          .to output_results('Removed 0, skipped 1')
+          .to output_results(
+            "#{invalid_ip} is invalid",
+            'Removed 0, skipped 1'
+          )
       end
     end
 
@@ -265,7 +254,7 @@ describe Mastodon::CLI::IpBlocks do
           .to output_results("#{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}\n#{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix}")
       end
 
-      it 'does not export bloked IPs with different severities' do
+      it 'does not export blocked IPs with different severities' do
         expect { subject }
           .to_not output_results("#{third_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}")
       end
@@ -279,7 +268,7 @@ describe Mastodon::CLI::IpBlocks do
           .to output_results("deny #{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix};\ndeny #{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix};")
       end
 
-      it 'does not export bloked IPs with different severities' do
+      it 'does not export blocked IPs with different severities' do
         expect { subject }
           .to_not output_results("deny #{third_ip_range_block.ip}/#{first_ip_range_block.ip.prefix};")
       end
diff --git a/spec/lib/mastodon/cli/settings_spec.rb b/spec/lib/mastodon/cli/settings_spec.rb
index 02d1042c5..568ee0039 100644
--- a/spec/lib/mastodon/cli/settings_spec.rb
+++ b/spec/lib/mastodon/cli/settings_spec.rb
@@ -20,37 +20,29 @@ describe Mastodon::CLI::Settings do
     describe '#open' do
       let(:action) { :open }
 
-      it 'changes "registrations_mode" to "open"' do
-        expect { subject }.to change(Setting, :registrations_mode).from(nil).to('open')
-      end
-
-      it 'displays success message' do
+      it 'changes "registrations_mode" to "open" and displays success' do
         expect { subject }
-          .to output_results('OK')
+          .to change(Setting, :registrations_mode).from(nil).to('open')
+          .and output_results('OK')
       end
     end
 
     describe '#approved' do
       let(:action) { :approved }
 
-      it 'changes "registrations_mode" to "approved"' do
-        expect { subject }.to change(Setting, :registrations_mode).from(nil).to('approved')
-      end
-
-      it 'displays success message' do
+      it 'changes "registrations_mode" to "approved" and displays success' do
         expect { subject }
-          .to output_results('OK')
+          .to change(Setting, :registrations_mode).from(nil).to('approved')
+          .and output_results('OK')
       end
 
       context 'with --require-reason' do
         let(:options) { { require_reason: true } }
 
-        it 'changes "registrations_mode" to "approved"' do
-          expect { subject }.to change(Setting, :registrations_mode).from(nil).to('approved')
-        end
-
-        it 'sets "require_invite_text" to "true"' do
-          expect { subject }.to change(Setting, :require_invite_text).from(false).to(true)
+        it 'changes registrations_mode and require_invite_text' do
+          expect { subject }
+            .to change(Setting, :registrations_mode).from(nil).to('approved')
+            .and change(Setting, :require_invite_text).from(false).to(true)
         end
       end
     end
@@ -58,13 +50,10 @@ describe Mastodon::CLI::Settings do
     describe '#close' do
       let(:action) { :close }
 
-      it 'changes "registrations_mode" to "none"' do
-        expect { subject }.to change(Setting, :registrations_mode).from(nil).to('none')
-      end
-
-      it 'displays success message' do
+      it 'changes "registrations_mode" to "none" and displays success' do
         expect { subject }
-          .to output_results('OK')
+          .to change(Setting, :registrations_mode).from(nil).to('none')
+          .and output_results('OK')
       end
     end
   end

From f3864db4090b8cec212fb99c3dabccfb6a6c7642 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Mon, 11 Dec 2023 15:23:30 +0100
Subject: [PATCH 71/73] Fix notification sounds (#28316)

---
 app/javascript/mastodon/actions/notifications_typed.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/javascript/mastodon/actions/notifications_typed.ts b/app/javascript/mastodon/actions/notifications_typed.ts
index 7e51fa51e..176362f4b 100644
--- a/app/javascript/mastodon/actions/notifications_typed.ts
+++ b/app/javascript/mastodon/actions/notifications_typed.ts
@@ -18,6 +18,6 @@ export const notificationsUpdate = createAction(
     playSound: boolean;
   }) => ({
     payload: args,
-    meta: { playSound: playSound ? { sound: 'boop' } : undefined },
+    meta: { sound: playSound ? 'boop' : undefined },
   }),
 );

From 9dfa94063626bc2351e2098e226deee7b0e3eff8 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 11 Dec 2023 15:58:10 +0100
Subject: [PATCH 72/73] Update babel monorepo to v7.23.6 (#28319)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 yarn.lock | 159 +++++++++++++++++++++++++++---------------------------
 1 file changed, 80 insertions(+), 79 deletions(-)

diff --git a/yarn.lock b/yarn.lock
index 0ef881126..71f1ad160 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -52,7 +52,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.22.9, @babel/compat-data@npm:^7.23.3, @babel/compat-data@npm:^7.23.5":
+"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.23.3, @babel/compat-data@npm:^7.23.5":
   version: 7.23.5
   resolution: "@babel/compat-data@npm:7.23.5"
   checksum: 081278ed46131a890ad566a59c61600a5f9557bd8ee5e535890c8548192532ea92590742fd74bd9db83d74c669ef8a04a7e1c85cdea27f960233e3b83c3a957c
@@ -60,37 +60,37 @@ __metadata:
   linkType: hard
 
 "@babel/core@npm:^7.10.4, @babel/core@npm:^7.11.1, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.22.1":
-  version: 7.23.5
-  resolution: "@babel/core@npm:7.23.5"
+  version: 7.23.6
+  resolution: "@babel/core@npm:7.23.6"
   dependencies:
     "@ampproject/remapping": "npm:^2.2.0"
     "@babel/code-frame": "npm:^7.23.5"
-    "@babel/generator": "npm:^7.23.5"
-    "@babel/helper-compilation-targets": "npm:^7.22.15"
+    "@babel/generator": "npm:^7.23.6"
+    "@babel/helper-compilation-targets": "npm:^7.23.6"
     "@babel/helper-module-transforms": "npm:^7.23.3"
-    "@babel/helpers": "npm:^7.23.5"
-    "@babel/parser": "npm:^7.23.5"
+    "@babel/helpers": "npm:^7.23.6"
+    "@babel/parser": "npm:^7.23.6"
     "@babel/template": "npm:^7.22.15"
-    "@babel/traverse": "npm:^7.23.5"
-    "@babel/types": "npm:^7.23.5"
+    "@babel/traverse": "npm:^7.23.6"
+    "@babel/types": "npm:^7.23.6"
     convert-source-map: "npm:^2.0.0"
     debug: "npm:^4.1.0"
     gensync: "npm:^1.0.0-beta.2"
     json5: "npm:^2.2.3"
     semver: "npm:^6.3.1"
-  checksum: 311a512a870ee330a3f9a7ea89e5df790b2b5af0b1bd98b10b4edc0de2ac440f0df4d69ea2c0ee38a4b89041b9a495802741d93603be7d4fd834ec8bb6970bd2
+  checksum: a02bae7d916029b70706dc301535e1b31e5d216f55d4ee6f64a15825c6b69ee2c14c52a213d1497ec414e925ed4e9d897d41fb0d75df9fea28ed2c0008790e31
   languageName: node
   linkType: hard
 
-"@babel/generator@npm:^7.23.5, @babel/generator@npm:^7.7.2":
-  version: 7.23.5
-  resolution: "@babel/generator@npm:7.23.5"
+"@babel/generator@npm:^7.23.6, @babel/generator@npm:^7.7.2":
+  version: 7.23.6
+  resolution: "@babel/generator@npm:7.23.6"
   dependencies:
-    "@babel/types": "npm:^7.23.5"
+    "@babel/types": "npm:^7.23.6"
     "@jridgewell/gen-mapping": "npm:^0.3.2"
     "@jridgewell/trace-mapping": "npm:^0.3.17"
     jsesc: "npm:^2.5.1"
-  checksum: 14c6e874f796c4368e919bed6003bb0adc3ce837760b08f9e646d20aeb5ae7d309723ce6e4f06bcb4a2b5753145446c8e4425851380f695e40e71e1760f49e7b
+  checksum: 53540e905cd10db05d9aee0a5304e36927f455ce66f95d1253bb8a179f286b88fa7062ea0db354c566fe27f8bb96567566084ffd259f8feaae1de5eccc8afbda
   languageName: node
   linkType: hard
 
@@ -122,16 +122,16 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/helper-compilation-targets@npm:^7.22.15, @babel/helper-compilation-targets@npm:^7.22.6":
-  version: 7.22.15
-  resolution: "@babel/helper-compilation-targets@npm:7.22.15"
+"@babel/helper-compilation-targets@npm:^7.22.15, @babel/helper-compilation-targets@npm:^7.22.6, @babel/helper-compilation-targets@npm:^7.23.6":
+  version: 7.23.6
+  resolution: "@babel/helper-compilation-targets@npm:7.23.6"
   dependencies:
-    "@babel/compat-data": "npm:^7.22.9"
-    "@babel/helper-validator-option": "npm:^7.22.15"
-    browserslist: "npm:^4.21.9"
+    "@babel/compat-data": "npm:^7.23.5"
+    "@babel/helper-validator-option": "npm:^7.23.5"
+    browserslist: "npm:^4.22.2"
     lru-cache: "npm:^5.1.1"
     semver: "npm:^6.3.1"
-  checksum: 45b9286861296e890f674a3abb199efea14a962a27d9b8adeb44970a9fd5c54e73a9e342e8414d2851cf4f98d5994537352fbce7b05ade32e9849bbd327f9ff1
+  checksum: ba38506d11185f48b79abf439462ece271d3eead1673dd8814519c8c903c708523428806f05f2ec5efd0c56e4e278698fac967e5a4b5ee842c32415da54bc6fa
   languageName: node
   linkType: hard
 
@@ -342,14 +342,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/helpers@npm:^7.23.5":
-  version: 7.23.5
-  resolution: "@babel/helpers@npm:7.23.5"
+"@babel/helpers@npm:^7.23.6":
+  version: 7.23.6
+  resolution: "@babel/helpers@npm:7.23.6"
   dependencies:
     "@babel/template": "npm:^7.22.15"
-    "@babel/traverse": "npm:^7.23.5"
-    "@babel/types": "npm:^7.23.5"
-  checksum: a37e2728eb4378a4888e5d614e28de7dd79b55ac8acbecd0e5c761273e2a02a8f33b34b1932d9069db55417ace2937cbf8ec37c42f1030ce6d228857d7ccaa4f
+    "@babel/traverse": "npm:^7.23.6"
+    "@babel/types": "npm:^7.23.6"
+  checksum: df1cf6607676ad36f52f652ec03536f2732d70aef5e76dba5c964e34d49f3c2d3dcf9fb3740db359f53071d74b64606a833d5ba156f79f437f71bfe06e2e7e19
   languageName: node
   linkType: hard
 
@@ -364,12 +364,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.15, @babel/parser@npm:^7.23.5":
-  version: 7.23.5
-  resolution: "@babel/parser@npm:7.23.5"
+"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.15, @babel/parser@npm:^7.23.6":
+  version: 7.23.6
+  resolution: "@babel/parser@npm:7.23.6"
   bin:
     parser: ./bin/babel-parser.js
-  checksum: 3356aa90d7bafb4e2c7310e7c2c3d443c4be4db74913f088d3d577a1eb914ea4188e05fd50a47ce907a27b755c4400c4e3cbeee73dbeb37761f6ca85954f5a20
+  checksum: 6f76cd5ccae1fa9bcab3525b0865c6222e9c1d22f87abc69f28c5c7b2c8816a13361f5bd06bddbd5faf903f7320a8feba02545c981468acec45d12a03db7755e
   languageName: node
   linkType: hard
 
@@ -836,14 +836,15 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/plugin-transform-for-of@npm:^7.23.3":
-  version: 7.23.3
-  resolution: "@babel/plugin-transform-for-of@npm:7.23.3"
+"@babel/plugin-transform-for-of@npm:^7.23.6":
+  version: 7.23.6
+  resolution: "@babel/plugin-transform-for-of@npm:7.23.6"
   dependencies:
     "@babel/helper-plugin-utils": "npm:^7.22.5"
+    "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5"
   peerDependencies:
     "@babel/core": ^7.0.0-0
-  checksum: 8a36202cfee312ba80e509c7c2131e6773524e572b4dc64a8ee95bd912634fdeb5ea91c6c7747ee30e03562d0f0d333f88ed7dbb929b36b60b8d74189189e12f
+  checksum: 46681b6ab10f3ca2d961f50d4096b62ab5d551e1adad84e64be1ee23e72eb2f26a1e30e617e853c74f1349fffe4af68d33921a128543b6f24b6d46c09a3e2aec
   languageName: node
   linkType: hard
 
@@ -1200,8 +1201,8 @@ __metadata:
   linkType: hard
 
 "@babel/plugin-transform-runtime@npm:^7.22.4":
-  version: 7.23.4
-  resolution: "@babel/plugin-transform-runtime@npm:7.23.4"
+  version: 7.23.6
+  resolution: "@babel/plugin-transform-runtime@npm:7.23.6"
   dependencies:
     "@babel/helper-module-imports": "npm:^7.22.15"
     "@babel/helper-plugin-utils": "npm:^7.22.5"
@@ -1211,7 +1212,7 @@ __metadata:
     semver: "npm:^6.3.1"
   peerDependencies:
     "@babel/core": ^7.0.0-0
-  checksum: 6ac29012550cdd10b65ec43fef0c7f43904ec458c43d597f627d8f52807413e57ea94e3986dbace576d734e67c2d09be5e43e77c72567d18f8c4ac5e19844625
+  checksum: 94a7ee92f073df53fd8bebf9ed391a95553716077da1c6c3a57f10f042358c938495d55e6b09b4b50544c01f03560c4770c17698e1c24817a15d3668e8231249
   languageName: node
   linkType: hard
 
@@ -1333,11 +1334,11 @@ __metadata:
   linkType: hard
 
 "@babel/preset-env@npm:^7.11.0, @babel/preset-env@npm:^7.12.1, @babel/preset-env@npm:^7.22.4":
-  version: 7.23.5
-  resolution: "@babel/preset-env@npm:7.23.5"
+  version: 7.23.6
+  resolution: "@babel/preset-env@npm:7.23.6"
   dependencies:
     "@babel/compat-data": "npm:^7.23.5"
-    "@babel/helper-compilation-targets": "npm:^7.22.15"
+    "@babel/helper-compilation-targets": "npm:^7.23.6"
     "@babel/helper-plugin-utils": "npm:^7.22.5"
     "@babel/helper-validator-option": "npm:^7.23.5"
     "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "npm:^7.23.3"
@@ -1377,7 +1378,7 @@ __metadata:
     "@babel/plugin-transform-dynamic-import": "npm:^7.23.4"
     "@babel/plugin-transform-exponentiation-operator": "npm:^7.23.3"
     "@babel/plugin-transform-export-namespace-from": "npm:^7.23.4"
-    "@babel/plugin-transform-for-of": "npm:^7.23.3"
+    "@babel/plugin-transform-for-of": "npm:^7.23.6"
     "@babel/plugin-transform-function-name": "npm:^7.23.3"
     "@babel/plugin-transform-json-strings": "npm:^7.23.4"
     "@babel/plugin-transform-literals": "npm:^7.23.3"
@@ -1418,7 +1419,7 @@ __metadata:
     semver: "npm:^6.3.1"
   peerDependencies:
     "@babel/core": ^7.0.0-0
-  checksum: 2a0e1274dec045186e131c6433659b75492583290e8d41633c616f6bff829cb2e4b2f9a57f556283a54db3bd6aa697911e56a36f607911a29b731c445a5b5a06
+  checksum: 5b24d179af52f082d04b9b98cc4777e37bf31a97cef5a91d8917e996dbd75f2f743c88c40f80744cb8529355bb674619d150c0260c32d834aa4067e21d0c8962
   languageName: node
   linkType: hard
 
@@ -1483,11 +1484,11 @@ __metadata:
   linkType: hard
 
 "@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.22.3, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
-  version: 7.23.5
-  resolution: "@babel/runtime@npm:7.23.5"
+  version: 7.23.6
+  resolution: "@babel/runtime@npm:7.23.6"
   dependencies:
     regenerator-runtime: "npm:^0.14.0"
-  checksum: ca679cc91bb7e424bc2db87bb58cc3b06ade916b9adb21fbbdc43e54cdaacb3eea201ceba2a0464b11d2eb65b9fe6a6ffcf4d7521fa52994f19be96f1af14788
+  checksum: d886954e985ef8e421222f7a2848884d96a752e0020d3078b920dd104e672fdf23bcc6f51a44313a048796319f1ac9d09c2c88ec8cbb4e1f09174bcd3335b9ff
   languageName: node
   linkType: hard
 
@@ -1502,32 +1503,32 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/traverse@npm:7, @babel/traverse@npm:^7.23.5":
-  version: 7.23.5
-  resolution: "@babel/traverse@npm:7.23.5"
+"@babel/traverse@npm:7, @babel/traverse@npm:^7.23.6":
+  version: 7.23.6
+  resolution: "@babel/traverse@npm:7.23.6"
   dependencies:
     "@babel/code-frame": "npm:^7.23.5"
-    "@babel/generator": "npm:^7.23.5"
+    "@babel/generator": "npm:^7.23.6"
     "@babel/helper-environment-visitor": "npm:^7.22.20"
     "@babel/helper-function-name": "npm:^7.23.0"
     "@babel/helper-hoist-variables": "npm:^7.22.5"
     "@babel/helper-split-export-declaration": "npm:^7.22.6"
-    "@babel/parser": "npm:^7.23.5"
-    "@babel/types": "npm:^7.23.5"
-    debug: "npm:^4.1.0"
+    "@babel/parser": "npm:^7.23.6"
+    "@babel/types": "npm:^7.23.6"
+    debug: "npm:^4.3.1"
     globals: "npm:^11.1.0"
-  checksum: c5ea793080ca6719b0a1612198fd25e361cee1f3c14142d7a518d2a1eeb5c1d21f7eec1b26c20ea6e1ddd8ed12ab50b960ff95ffd25be353b6b46e1b54d6f825
+  checksum: 5b4ebb94a00a7e1daf111e4b0b45a7998d5b7598637a14e75e855e88cc1b702789e09a958726b5d599a003be1e9032dbdfde4b88ea6061332228738950d5582d
   languageName: node
   linkType: hard
 
-"@babel/types@npm:^7.0.0, @babel/types@npm:^7.0.0-beta.49, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.10, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.5, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3":
-  version: 7.23.5
-  resolution: "@babel/types@npm:7.23.5"
+"@babel/types@npm:^7.0.0, @babel/types@npm:^7.0.0-beta.49, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.10, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.6, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3":
+  version: 7.23.6
+  resolution: "@babel/types@npm:7.23.6"
   dependencies:
     "@babel/helper-string-parser": "npm:^7.23.4"
     "@babel/helper-validator-identifier": "npm:^7.22.20"
     to-fast-properties: "npm:^2.0.0"
-  checksum: 7dd5e2f59828ed046ad0b06b039df2524a8b728d204affb4fc08da2502b9dd3140b1356b5166515d229dc811539a8b70dcd4bc507e06d62a89f4091a38d0b0fb
+  checksum: 42cefce8a68bd09bb5828b4764aa5586c53c60128ac2ac012e23858e1c179347a4aac9c66fc577994fbf57595227611c5ec8270bf0cfc94ff033bbfac0550b70
   languageName: node
   linkType: hard
 
@@ -5170,17 +5171,17 @@ __metadata:
   languageName: node
   linkType: hard
 
-"browserslist@npm:^4.0.0, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.21.9, browserslist@npm:^4.22.1":
-  version: 4.22.1
-  resolution: "browserslist@npm:4.22.1"
+"browserslist@npm:^4.0.0, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.22.1, browserslist@npm:^4.22.2":
+  version: 4.22.2
+  resolution: "browserslist@npm:4.22.2"
   dependencies:
-    caniuse-lite: "npm:^1.0.30001541"
-    electron-to-chromium: "npm:^1.4.535"
-    node-releases: "npm:^2.0.13"
+    caniuse-lite: "npm:^1.0.30001565"
+    electron-to-chromium: "npm:^1.4.601"
+    node-releases: "npm:^2.0.14"
     update-browserslist-db: "npm:^1.0.13"
   bin:
     browserslist: cli.js
-  checksum: 6810f2d63f171d0b7b8d38cf091708e00cb31525501810a507839607839320d66e657293b0aa3d7f051ecbc025cb07390a90c037682c1d05d12604991e41050b
+  checksum: 2a331aab90503130043ca41dd5d281fa1e89d5e076d07a2d75e76bf4d693bd56e73d5abcd8c4f39119da6328d450578c216cf1cd5c99b82d8a90a2ae6271b465
   languageName: node
   linkType: hard
 
@@ -5408,10 +5409,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001538, caniuse-lite@npm:^1.0.30001541":
-  version: 1.0.30001561
-  resolution: "caniuse-lite@npm:1.0.30001561"
-  checksum: 6e84c84026fee53edbdbb5aded7a04a036aae4c2e367cf6bdc90c6783a591e2fdcfcdebcc4e774aca61092e542a61200c8c16b06659396492426033c4dbcc618
+"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001538, caniuse-lite@npm:^1.0.30001565":
+  version: 1.0.30001568
+  resolution: "caniuse-lite@npm:1.0.30001568"
+  checksum: 13f01e5a2481134bd61cf565ce9fecbd8e107902927a0dcf534230a92191a81f1715792170f5f39719c767c3a96aa6df9917a8d5601f15bbd5e4041a8cfecc99
   languageName: node
   linkType: hard
 
@@ -6413,7 +6414,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"debug@npm:4, debug@npm:4.3.4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.4":
+"debug@npm:4, debug@npm:4.3.4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4":
   version: 4.3.4
   resolution: "debug@npm:4.3.4"
   dependencies:
@@ -6955,10 +6956,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"electron-to-chromium@npm:^1.4.535":
-  version: 1.4.576
-  resolution: "electron-to-chromium@npm:1.4.576"
-  checksum: b0b9e7ba803bf93ffac9cb830ed2b0e0eb07f20066127065f9ab9e08e4e6a5812040e03d76f6ee9bc59e03fb938fd414e83d4883b29111303e9e88633cf2dce4
+"electron-to-chromium@npm:^1.4.601":
+  version: 1.4.609
+  resolution: "electron-to-chromium@npm:1.4.609"
+  checksum: 9675a79388acbaff5953a4c61589af7da93e0d1f9d6a3b284c7630f10126eb0998557b07448514214d5a3d19025310039b55f405ab701b1253130fc94907f743
   languageName: node
   linkType: hard
 
@@ -11930,10 +11931,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"node-releases@npm:^2.0.13":
-  version: 2.0.13
-  resolution: "node-releases@npm:2.0.13"
-  checksum: 2fb44bf70fc949d27f3a48a7fd1a9d1d603ddad4ccd091f26b3fb8b1da976605d919330d7388ccd55ca2ade0dc8b2e12841ba19ef249c8bb29bf82532d401af7
+"node-releases@npm:^2.0.14":
+  version: 2.0.14
+  resolution: "node-releases@npm:2.0.14"
+  checksum: 199fc93773ae70ec9969bc6d5ac5b2bbd6eb986ed1907d751f411fef3ede0e4bfdb45ceb43711f8078bea237b6036db8b1bf208f6ff2b70c7d615afd157f3ab9
   languageName: node
   linkType: hard
 

From 2c6369918c61a7f948f39926f987fcfa0b02fb82 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 11 Dec 2023 09:58:29 -0500
Subject: [PATCH 73/73] Fix `Style/RedundantArgument` cop (#28321)

---
 app/helpers/application_helper.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 48d9119fb..135bbb0fd 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -114,7 +114,7 @@ module ApplicationHelper
   end
 
   def fa_icon(icon, attributes = {})
-    class_names = attributes[:class]&.split(' ') || []
+    class_names = attributes[:class]&.split || []
     class_names << 'fa'
     class_names += icon.split.map { |cl| "fa-#{cl}" }